CLI & Agent
Use the BlazeRules multi-instance local ingest agent and configure agent instances from YAML.
BlazeRules ships a command-line local ingest agent that pulls records from HTTP, a tailed file, or stdin and runs them through the engine. This page covers building it and using its instances: configuration.
These are convenience binaries, not the productBlazeRules is a library, not a service. The CLI tools exist to make local testing and small ingest setups easy; they are not a hosted runtime, and they don't change how the engine evaluates rules. For production you normally embed the engine (C++ API / Python API).
The ingest agent
There is noblazerules agentsubcommandThe agent is a separate binary,
blazerules_agent. The mainblazerulesCLI has noagentsubcommand — runningblazerules agenterrors and points you to the standaloneblazerules_agentbinary documented on this page.
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, not just the final write. |
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, but --eval-shards does not — out of the box, an HTTP instance on a 16-core box still evaluates on a single engine until you explicitly raise --eval-shards. That's because --http-threads only accepts and reads connections; it doesn't evaluate rules or write decisions. --eval-shards is the flag that actually parallelizes parsing and rule evaluation across cores, and — as above — only shards stateless rulesets, auto-downgrading to one engine otherwise. --sink-workers is independent again: for --output arrow, encoding a batch into Arrow array builders happens outside the output-file lock and runs fully in parallel across sink workers — only the final WriteRecordBatch and flush are serialized behind one mutex — so raising --sink-workers is a genuine throughput lever rather than added contention on a single-threaded write.
--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.
As a starting point: leave --http-threads at its default, size --eval-shards to the cores you want doing rule evaluation, and raise --sink-workers if --sink-queue-depth keeps filling (visible as a rising rate of 503s). Queue-depth flags are shock absorbers for bursty traffic, not throughput levers by themselves — raising them buys burst tolerance at the cost of latency and memory, not more evaluation or write capacity.
--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
New in 0.5.0, the agent surfaces evaluation failures on its own terminal. When a record batch fails to evaluate — for example a missing lookup file, a schema mismatch, or an unreadable model — the agent returns HTTP 500 from POST /v1/logs and now writes instance '<name>': evaluation error: <message> to its own stderr, throttled to about once per second so a persistently broken config stays visible without flooding the log. Earlier versions surfaced these failures only in the per-request 500 body, so a misconfigured agent looked silent from the terminal. If the decision log stays empty, watch the agent's stderr for these lines.
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 12 hours ago