Recipe: Decision And DLQ Logs

Configure decision output, bad-record handling, and dashboard log inspection.

Decision logs are compact records for downstream routing. Dead-letter logs keep bad input out of the hot path while preserving samples for debugging.

Python Engine Logs

import blazerules

cfg = blazerules.EngineConfig()
cfg.output_detail = blazerules.OutputDetail.DECISIONS
cfg.decision_log_path = "decisions.ndjson"
cfg.ingest_error_mode = blazerules.IngestErrorMode.SKIP_TO_DEAD_LETTER
cfg.dead_letter_path = "dead_letters.ndjson"

engine = blazerules.RuleEngine(cfg)
engine.load_rules("rules.yaml")
result = engine.evaluate_ndjson(payload)
CLI equivalent: write compact decisions with the agent
blazerules_agent \
  --rules rules.yaml \
  --input stdin \
  --output ndjson \
  --output-path decisions.ndjson

The agent writes compact decisions. Engine-level dead-letter policy is configured through EngineConfig in Python/C++.

Agent Output

blazerules_agent \
  --rules rules.yaml \
  --input stdin \
  --output ndjson \
  --output-path decisions.ndjson
Python equivalent: evaluate a batch and write decisions yourself
import json
import time
import blazerules

engine = blazerules.RuleEngine()
engine.load_rules("rules.yaml")
result = engine.evaluate_ndjson(payload)

with open("decisions.ndjson", "a", encoding="utf-8") as out:
    ts_ms = int(time.time() * 1000)
    for row, decision in enumerate(result.decisions):
        out.write(json.dumps({
            "ts_ms": ts_ms,
            "batch_row": row,
            "decision": decision,
            "score": result.scores[row],
            "risk_band": result.risk_bands[row],
            "winning_rule_id": result.winning_rule_ids[row],
        }) + "\n")

The agent writes one decision per evaluated row, tagged with the instance name and ruleset version:

{"ts_ms":1782150000000,"instance":"checkout","batch_row":12,"ruleset_version":"1.0.0","matched":true,"decision":"BLOCK","score":91.0,"risk_band":"HIGH","winning_rule_id":"blocked_device"}

When the instance runs one or more ONNX models (model_score rules), each model's raw per-record prediction is appended too — as a model_scores object in NDJSON, or a model.<name> float column in Arrow output:

{"ts_ms":1782150000000,"instance":"fraud","decision":"REVIEW","score":72.0,"risk_band":"HIGH","winning_rule_id":"ml_high_risk","model_scores":{"model.risk_logistic":0.83,"model.loss_regression":118.4}}

The agent writes a dead-letter file when you pass --dead-letter-path PATH (or set output.dead_letter_path on an instance); dead-letter output is always NDJSON, even when the decision log is Arrow. The Python/C++ paths use EngineConfig.dead_letter_path, and the CLI uses blazerules eval --dead-letter-log PATH.

Dead-Letter Mode

Use dead-letter mode when bad records should be inspectable instead of silently counted:

cfg.ingest_error_mode = blazerules.IngestErrorMode.SKIP_TO_DEAD_LETTER
cfg.dead_letter_path = "dead_letters.ndjson"
CLI equivalent: inspect dead-letter output with the dashboard
blazerules_dashboard \
  --decision-log decisions.ndjson \
  --dead-letter-log dead_letters.ndjson \
  --host 127.0.0.1 \
  --port 9470

The result still reports counters:

print(result.messages_processed)
print(result.messages_skipped)
print(result.error_counts)
print(result.error_samples)

Dashboard

blazerules_dashboard \
  --decision-log decisions.ndjson \
  --dead-letter-log dead_letters.ndjson \
  --rules rules.yaml \
  --host 127.0.0.1 \
  --port 9470

Open http://127.0.0.1:9470.


Did this page help you?