Replicant is a synthetic transaction execution framework named after the bioengineered androids from Blade Runner. (all synthetics came from Blade Runner :)
It defines a common interface for transactions and results, provides a transaction manager, execution scheduler, api and facilities for emitting result data to external systems.
Under heavy development and API changes are expected. Please file an issue if anything breaks.
- Go 1.13
- External URL for API tests that require webhook based callbacks
- Chrome with remote debugging (CDP) either in headless mode or in foreground (useful for testing)
Running the server with the example config from the project root dir.
go run cmd/replicant/*.go -config $PWD/example-config.yaml
-
The web application testing support is based on the FQL (Ferret Query Language), documentation.
-
Start the Chrome browser with Chrome DevTools Protocol enabled:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 &
POST http://127.0.0.1:8080/api/v1/run
content-type: application/yaml
name: duckduckgo-search
type: web
schedule: '@every 1m'
timeout: 200s
retry_count: 2
inputs:
url: "https://duckduckgo.com"
cdp_address: "http://127.0.0.1:9222"
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36"
timeout: 5000000
text: "blade runner"
metadata:
application: duckduckgo-search
environment: production
component: web
script: |
LET doc = DOCUMENT('{{ index . "url" }}', { driver: "cdp", userAgent: "{{ index . "user_agent" }}"})
INPUT(doc, '#search_form_input_homepage', "{{ index . "text" }}")
CLICK(doc, '#search_button_homepage')
WAIT_NAVIGATION(doc)
LET result = ELEMENT(doc, '#r1-0 > div > div.result__snippet.js-result-snippet').innerText
RETURN {
failed: result == "",
message: "search result",
data: result,
}
{
"data": [
{
"name": "duckduckgo-search",
"type": "web",
"failed": false,
"message": "search result",
"data": "A blade runner must pursue and terminate four replicants who stole a ship in space, and have returned to Earth to find their creator.",
"time": "2019-10-30T06:18:20.511246Z",
"metadata": {
"application": "duckduckgo-search",
"component": "web",
"environment": "production"
},
"retry_count": 0,
"with_callback": false,
"duration_seconds": 5.242629701
}
]
}
- The api testing support is based on interpreted go code, documentation.
POST http://127.0.0.1:8080/api/v1/run
content-type: application/yaml
name: duckduckgo-search
type: go
schedule: '@every 20s'
timeout: 200s
retry_count: 2
inputs:
url: "https://api.duckduckgo.com/"
text: "blade runner"
metadata:
application: duckduckgo-search
environment: production
component: api
script: |
package transaction
import "bytes"
import "context"
import "fmt"
import "net/http"
import "io/ioutil"
import "net/http"
import "regexp"
func Run(ctx context.Context) (m string, d string, err error) {
req, err := http.NewRequest(http.MethodGet, "{{ index . "url" }}", nil)
if err != nil {
return "request build failed", "", err
}
req.Header.Add("Accept-Charset","utf-8")
q := req.URL.Query()
q.Add("q", "{{ index . "text" }}")
q.Add("format", "json")
q.Add("pretty", "1")
q.Add("no_redirect", "1")
req.URL.RawQuery = q.Encode()
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "failed to send request", "", err
}
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "failed to read response", "", err
}
rx, err := regexp.Compile(`"Text"\s*:\s*"(.*?)"`)
if err != nil {
return "failed to compile regexp", "", err
}
s := rx.FindSubmatch(buf)
if len(s) < 2 {
return "failed to find data", "", fmt.Errorf("failed to find data")
}
return "search result", fmt.Sprintf("%s", s[1]), nil
}
{
"data": [
{
"name": "duckduckgo-search",
"type": "go",
"failed": false,
"message": "search result",
"data": "Blade Runner A 1982 American neo-noir science fiction film directed by Ridley Scott, written by Hampton...",
"time": "2019-10-30T06:10:12.835481Z",
"metadata": {
"application": "duckduckgo-search",
"component": "api",
"environment": "production"
},
"retry_count": 0,
"with_callback": false,
"duration_seconds": 0.602482443
}
]
}
Method | Resource | Action |
---|---|---|
POST | /v1/transaction | Add a managed transaction |
GET | /v1/transaction | Get all managed transaction definitions |
GET | /v1/transaction/:name | Get a managed transaction definition by name |
DELETE | /v1/transaction/:name | Remove a managed transaction |
POST | /v1/run | Run an ad-hoc transaction |
POST | /v1/run/:name | Run a managed transaction by name |
GET | /v1/result | Get all managed transaction last execution results |
GET | /v1/result/:name | Get the latest result for a managed transaction by name |
GET | /metrics | Get metrics (prometheus emitter must be enabled) |
GET | /debug/pprof | Get available runtime profile data (debug enabled) |
GET | /debug/pprof/:profile | Get profile data (for pprof, debug enabled) |
- Tests
- Developer and user documentation
- Add support for more conventional persistent stores
- Vault integration for secrets (inputs)
- Architecture and API documentation
- Javascript driver transaction support
Bruno Moura brunotm@gmail.com
Replicant source code is available under the Apache Version 2.0 License