CLI & Agent
Use the BlazeRules multi-instance local ingest agent and configure agent instances from YAML.
The blazerules_agent executable ingests records from HTTP, a tailed file, or stdin and evaluates them with BlazeRules. It supports direct CLI configuration and an instances: configuration for running multiple independent engines.
These are convenience binaries, not the productBlazeRules is a library, not a hosted service. The CLI tools support local testing and standalone ingestion while preserving the same evaluation semantics as the embedded engine. Applications can also embed the C++ API or Python API directly.
The ingest agent
There is noblazerules agentsubcommandThe agent is a separate executable named
blazerules_agent. TheblazerulesCLI does not provide anagentsubcommand.
blazerules_agent runs one or more instances, each reading from an input source, batching records, evaluating them against a rule set, optionally de-duplicating, and writing decisions out. Build it with the agent flag:
cmake -S . -B cmake-build-release -G Ninja
cmake --build cmake-build-release --target blazerules_agent -jFor the full list of flags, defaults, and valid values, use API and CLI Values Reference.
How an instance flows
flowchart LR input["input: http / file_tail / stdin"] --> batch["batch: batch_size + flush_ms"] batch --> engine["engine: load_rules(rules)"] engine --> dedupe["dedupe: key_fields within ttl_seconds"] dedupe --> output["output: ndjson / stdout"]
Each instance loads its own rules: file and accumulates incoming records until it has batch_size of them or flush_ms elapses, then evaluates the batch in one call. When dedupe.enabled is set, records repeating the same key_fields within ttl_seconds are dropped before evaluation.
input.type or --input selects the adapter: http, file_tail, or stdin. source / --source is a metadata label attached to wrapped log records and decisions; it does not select the input adapter.
Thehttpinput is unauthenticatedThe
httpadapter exposesPOST /v1/logswith no authentication. Bindinput.hostto127.0.0.1for local use, or run it on a trusted network behind an authenticating reverse proxy — never expose it directly to untrusted callers. The agent prints a warning if it binds to0.0.0.0.
instances: configuration reference
instances: configuration referenceThe agent reads a top-level instances: block (a list) from a YAML file. Each entry accepts:
| Key | Type | Required | Meaning |
|---|---|---|---|
name | string | yes | Instance label. |
rules | path | yes | Rule YAML this instance evaluates against. |
batch_size | int | yes | Records buffered before a forced flush. |
flush_ms | int | yes | Max milliseconds to wait before flushing a partial batch. |
eval_shards | int | no | Pool of cloned engines for parallel evaluation (default 1). Stateless rulesets only — auto-downgrades to one engine if dedupe is on or the ruleset uses windows / an unbound schema. |
http_threads | int | for http | httplib's own accept/read thread pool size (default max(4, cores)); doesn't add evaluation or write concurrency. |
http_queue_depth | int | for http | Depth of httplib's own request queue feeding http_threads (default 256); a full queue closes the connection immediately with no HTTP response. |
eval_queue_depth | int | for http | Depth of the bounded queue feeding eval-worker threads (default 64); a full queue returns HTTP 429. |
sink_queue_depth | int | for http | Depth of the bounded queue feeding sink-worker threads (default 64); a full queue returns HTTP 503. |
sink_workers | int | for http | Threads that write evaluated batches to the decision log (default 1); parallelizes Arrow encoding as well as sink writes. |
max_request_mb | int | for http | Hard cap on one HTTP request body in MiB (default 256); larger requests get HTTP 413. |
ack_mode | enum | for http | durable (default) acks after the write; evaluated acks right after evaluation, before the write. |
service | string | no | Free-form service label attached to output. |
source | string | no | Free-form source label attached to output. |
input.type | enum | yes | http | file_tail | stdin. |
input.host | string | for http | Bind host (e.g. 127.0.0.1). |
input.port | int | for http | Bind port. |
input.path | path | for file_tail | File to tail. |
output.type | enum | yes | ndjson | stdout | arrow. |
output.path | path or s3://…/prefix/ | for ndjson/arrow | Destination file, or an s3:// prefix for stateless rolled part-object output (see below). |
output.dead_letter_path | path | no | Write malformed/skipped records as a dead-letter NDJSON file (always local NDJSON). |
models | list | no | ONNX models for model_score rules, each {name, path} (or name=path). |
s3_roll_mb | int | no | For s3:// output: roll a part after ~this many MiB (default 64). |
s3_flush_seconds | int | no | For s3:// output: roll + upload cadence in seconds (default 10). |
dedupe.enabled | bool | no | Turn de-duplication on. |
dedupe.key_fields | list | with dedupe | Fields whose combination identifies a duplicate. |
dedupe.ttl_seconds | int | with dedupe | Window during which repeats are dropped. |
Stateless output to S3
Set an instance's output.path (or the single-instance --output-path) to an s3://bucket/prefix/ and the agent writes rolled part objects to that prefix — bounded local staging, background upload via the same aws CLI used for s3:// rules/models, custom endpoint honored. A final part is flushed on SIGINT/SIGTERM, so rolling deploys lose nothing. Point the dashboard at the same prefix with --decision-log-dir s3://… to view it. Region/endpoint come from AWS_REGION/AWS_ENDPOINT_URL or the --aws-region / --aws-endpoint-url flags. See the S3 recipe for a full example.
Parallel evaluation for stateless rulesets
--eval-shards N (or an instance's eval_shards:, default 1) evaluates each batch across a pool of N cloned engines instead of one, raising throughput for CPU-bound rulesets. It only helps stateless rulesets. The pool is built lazily on the first batch — once the schema is bound and the ruleset compiled — and the agent prints one line to its stderr describing what it did: instance '<name>': eval sharding ON with N engines (stateless ruleset) when sharding engages, or --eval-shards ignored (...) when it falls back to a single engine. It falls back whenever dedupe is enabled, the ruleset uses windows, or the schema is still unbound, because those need shared state.
stdinis supported tooThe example below shows
httpandfile_tailinputs.stdinis also a validinput.type— pipe NDJSON into the agent and route decisions tostdoutfor quick local experiments.
A real two-instance configuration
This is the instances: block from the repository's agent.yaml:
instances:
- name: payments-http
rules: rules.yaml
batch_size: 4096
flush_ms: 50
service: payments-api
source: http-json
input:
type: http
host: 127.0.0.1
port: 9480
output:
type: ndjson
path: decisions-payments.ndjson
dedupe:
enabled: true
key_fields: [event_id]
ttl_seconds: 86400
- name: checkout-log-tail
rules: rules.yaml
batch_size: 2048
flush_ms: 250
service: checkout
source: pod-stdout
input:
type: file_tail
path: app.log
output:
type: stdout
dedupe:
enabled: true
key_fields: [event_id]
ttl_seconds: 3600payments-http accepts JSON over HTTP on 127.0.0.1:9480, batches up to 4096 records (or every 50 ms), and appends decisions to decisions-payments.ndjson, de-duplicating by event_id for a day. checkout-log-tail tails app.log, batches more loosely (2048 / 250 ms), and prints decisions to stdout with a one-hour dedupe window.
Tuning the HTTP pipeline
flowchart LR accept["accept: http-threads / http-queue-depth"] --> evalq["eval queue: eval-queue-depth"] evalq --> evalw["eval workers: eval-shards"] evalw --> sinkq["sink queue: sink-queue-depth"] sinkq --> sinkw["sink workers: sink-workers"] sinkw --> log["decision log"]
These flags only matter for --input http (or input.type: http) — stdin and file_tail read and evaluate synchronously on the instance's own thread and never touch the queues above. For an HTTP instance, a single accept loop hands each new connection to httplib's own thread pool (--http-threads, default max(4, cores)), which reads the request line, headers, and body and invokes the route handler; connections wait in an internal queue up to --http-queue-depth deep (default 256) once every thread is busy. Once a request's body is fully read, it becomes one task on the bounded eval queue (--eval-queue-depth, default 64), which max(1, --eval-shards) eval-worker threads pull from — or exactly one thread regardless of --eval-shards, whenever dedupe is enabled (see "Parallel evaluation for stateless rulesets" above for when sharding does and doesn't apply). Once evaluated, the batch becomes one task on a second bounded queue (--sink-queue-depth, default 64) feeding --sink-workers threads (default 1), which are the only threads that call into the decision-log writer.
Each stage fails differently once it's saturated:
| Flag | Default | If exceeded |
|---|---|---|
--http-queue-depth | 256 | The connection is closed immediately — no HTTP response is sent, and nothing is logged (the agent doesn't register an httplib error logger). |
--eval-queue-depth | 64 | HTTP 429 with {"ok":false,"error":"evaluation queue is full"}. |
--sink-queue-depth | 64 | HTTP 503 with {"ok":false,"error":"output queue is full"}. |
--max-request-mb | 256 | HTTP 413, checked against the declared (or actual streamed) body size before the request reaches the eval queue. |
--http-threads scales with core count by default, while --eval-shards does not. An HTTP instance evaluates through one engine until --eval-shards is raised. HTTP threads accept and read connections; evaluation shards parallelize parsing and rule evaluation. Sharding applies only to stateless rulesets and automatically falls back to one engine for stateful rules. --sink-workers controls a separate stage. For --output arrow, Arrow array construction runs outside the output-file lock across sink workers; only WriteRecordBatch and flush operations are serialized.
--ack-mode controls response timing rather than concurrency. durable (the default) only responds once a sink worker has actually written the batch's decisions, so a 200 means the record is durably in the decision log. evaluated responds as soon as an eval worker hands the batch to the sink queue, before it's written — lower latency, but if the agent crashes (or the write itself fails) between ack and write, that batch's decision-log entry can be lost even though the record was evaluated; on a write failure the agent still logs instance '<name>': evaluation error: <message> to its own stderr, but a caller that already received 200 never learns of it over HTTP.
A practical baseline keeps --http-threads at its default, sizes --eval-shards to available evaluation cores, and raises --sink-workers when a full --sink-queue-depth produces HTTP 503 responses. Queue depths absorb bursts but do not increase sustained evaluation or sink capacity; larger queues trade memory and latency for burst tolerance.
--batch-sizedoes nothing for HTTP throughput
--batch-sizeonly pacesstdin/file_tailflushing. On the HTTP path, httplib already hands each POST body to the eval queue as a single task, and the common cases — a plain NDJSON body, a JSON array, or Arrow IPC — evaluate the whole body in one engine call regardless of--batch-size. The one exception is an instance withdedupeenabled: there, a POST's lines are canonicalized one at a time for the duplicate-key check and re-chunked into--batch-size-sized engine calls, but that's still one task end to end, so it doesn't change how many requests are handled concurrently. Tuning--batch-sizefor HTTP throughput is a no-op either way — use the flags above instead.
CLI flags
Run a multi-instance config:
./cmake-build-release/blazerules_agent --config agent.yamlOr run one instance directly from flags:
./cmake-build-release/blazerules_agent \
--name payments-http \
--rules rules.yaml \
--input http \
--host 127.0.0.1 \
--port 9480 \
--batch-size 4096 \
--flush-ms 50 \
--output ndjson \
--output-path decisions-payments.ndjson \
--service payments-api \
--source http-json \
--dedupe-key event_id \
--dedupe-ttl-seconds 86400Single-instance flags are --rules, --input stdin|file_tail|http, --path, --host, --port, --batch-size, --flush-ms, --eval-shards, --http-threads, --http-queue-depth, --eval-queue-depth, --sink-queue-depth, --sink-workers, --max-request-mb, --ack-mode, --output stdout|ndjson|arrow, --output-path, --dead-letter-path, repeated --model name=path, --service, --source, --aws-region, --aws-endpoint-url, repeated --dedupe-key, and --dedupe-ttl-seconds. Every binary also accepts --version, which prints the version string and exits.
--output arrow writes decisions as a compact binary Arrow IPC stream to --output-path instead of NDJSON. It carries the same columns (ts_ms, instance, batch_row, ruleset_version, matched, decision, score, risk_band, winning_rule_id), is several times smaller on disk, and reads directly in pyarrow/DuckDB/pandas (pyarrow.ipc.open_stream(path).read_all()).
Watching a running agent
Evaluation errors on stderr
When batch evaluation fails because of a missing lookup, schema mismatch, unreadable model, or another runtime error, POST /v1/logs returns HTTP 500. The agent also writes instance '<name>': evaluation error: <message> to stderr. Repeated messages are throttled to approximately one line per second.
Input-throughput sidecar
Whenever --output-path (or an instance's output.path) is set, the agent maintains a companion <output-path>.stats file, rewritten at most once per second, holding a single JSON object like {"ts_ms": …, "input_bytes": …, "input_records": …}. Both counters are cumulative since the instance started, and the dashboard reads this sidecar to show input throughput alongside the decision log.
Next steps
Updated about 2 months ago