A command-line tool for managing Elasticsearch Watcher alerts as code.
Orwell lets you define, version-control, and deploy watcher-based alerts to Elasticsearch from your terminal. Instead of clicking through Kibana, you keep alert definitions in your repo and push changes through git.
Documentation: orwell.pages.dev
npm install -g @orwellg/cliOr run locally from the repo:
git clone <repo-url> && cd orwell
npm install
node ./bin/orwell.ts --versionRequires Node.js >= 24.
Scaffold a new alert project:
orwell scaffold --name my-alertThis creates:
src/
alert-group/
my-alert/
watcher.non-prod.json
script.groovy
shared/
shared.groovy
Preview what would be deployed:
orwell push --endpoint $ELASTIC_ENDPOINT --api-key $ELASTIC_API_KEY \
--target server-a.non-prod --dry-runDeploy for real:
orwell push --endpoint $ELASTIC_ENDPOINT --api-key $ELASTIC_API_KEY \
--target server-a.non-prodOrwell uses git diff to detect which alerts changed between your branch and main, then deploys only those alerts to Elasticsearch via the Watcher API. Each alert is a folder containing a watcher definition (JSON or JS) and an optional Painless script.
The alert ID is derived from the folder path: {group}-{alert-name}, optionally prefixed with a project ID.
Create a new alert project with the right folder structure.
orwell scaffold \
-n, --name <alert-name> \
-g, --group-name <group> # default: alert-group
--base-dir <dir> # default: src
--dest <dir> # default: current directory
--no-git # skip git initDeploy changed alerts to Elasticsearch. Only alerts that differ from the main branch get pushed.
orwell push \
--endpoint <url> \
--api-key <key> \
--target <server.env> # e.g. server-a.non-prod
-p, --project-id <id> # prefix for alert IDs
--base-dir <dir> # default: src
--main-branch <branch> # default: main
--dry-run # preview without deployingFull bidirectional sync: pushes changed alerts and deletes removed ones.
orwell sync \
--endpoint <url> \
--api-key <key> \
--target <server.env>
--remove-only # only delete
--push-only # only push
--dry-runAccepts the same options as push, plus --remove-only and --push-only.
Evaluate a JavaScript watcher file and print the resulting JSON. Useful for debugging JS-based watchers.
orwell eval:watch <path-to-watcher.js> --project-id <id>Build and deploy the heartbeat watcher directly to Elasticsearch.
orwell heartbeat:deploy \
--base-dir src \
--config-path ./orwell.js \
--endpoint <url> \
--api-key <key> \
--dry-run # preview without deployingThe watcher ID is derived from the project ID: {projectId}-heartbeat (or orwell-heartbeat if no project ID is set). Supports --dry-run to print the watcher JSON without deploying.
Authentication options are the same as push and sync (--api-key, --username/--password, or the corresponding env vars).
src/
shared/
shared.groovy # shared Painless code, available via #include
<group>/
<alert-name>/
watcher.json # watcher definition
script.groovy # optional Painless script
Alerts are organized by group. Each alert folder contains at least one watcher file and optionally a Painless script.
The filename determines which server and environment the watcher deploys to:
| Filename | Deploys to |
|---|---|
watcher.json |
All servers, all environments |
watcher.server-a.json |
server-a only, all environments |
watcher.server-b.prod.json |
server-b production only |
watcher.server-a.non-prod.json |
server-a non-prod only |
Pattern: watcher[.server][.environment].json (or .js)
Scripts must be named script.groovy. They support #include directives to pull in shared code:
#include "../../shared/shared.groovy"
// your script logic hereFor more complex watchers, you can use JavaScript instead of JSON. This lets you compose watchers programmatically, reference scripts, and use environment variables.
// watcher.server-a.non-prod.js
const { webhook } = require('./base');
const transformScript = script('../path/to/transform.groovy');
module.exports = {
trigger: { schedule: { interval: "2h" } },
input: {
chain: {
inputs: [
{ static: { simple: { env: "NON-PROD" } } },
{ logs: { search: { /* ... */ } } }
]
}
},
condition: { script: script('./condition.groovy') },
transform: { chain: [{ script: transformScript }] },
actions: {
send_slack_message: {
transform: { script: transformScript },
webhook
}
}
};The script() function is globally available in JS watchers. It takes a path to a .groovy file and returns { id: "<alert-id>-<filename>" }, which Elasticsearch uses to reference stored scripts.
The orwell.js config file is used by the heartbeat:deploy command:
module.exports = {
baseDir: 'src',
heartbeat: {
projectId: 'my-project', // optional, prefixes alert and watcher IDs
alerts: [
'in-person-selling/reconext-shipment-failure',
'in-person-selling/shipping-release-scanner',
],
action: {
slack: { path: process.env.SLACK_HOOK_PATH }
},
indices: ['.watcher-history-*'],
interval: '2h',
}
};Orwell supports two authentication methods for Elasticsearch:
API Key (preferred):
orwell push --endpoint https://my-elastic:9200 --api-key <key>Basic auth:
orwell push --endpoint https://my-elastic:9200 --username <user> --password <pass>Both can also be set via environment variables:
export ELASTIC_ENDPOINT=https://my-elastic:9200
export ELASTIC_API_KEY=your-api-key
# or
export ELASTIC_USERNAME=user
export ELASTIC_PASSWORD=passnpm testUses the Node.js built-in test runner (node --test).
Integration tests deploy real watchers to a local Elasticsearch instance:
docker-compose up -d # starts ES on localhost:19200 + Kibana on localhost:15601
npm test # runs all tests including integration
docker-compose downMIT