Logging¶
Every time a task finishes its lifecycle, CMDx logs once at INFO with the full Result#to_h payload. Think of it as a structured trail you can ship to Splunk, Datadog, ELK, or plain files—pick a formatter that matches how your team searches logs.
Formatters¶
| Formatter | Good when you want… | Output style |
|---|---|---|
Line |
Classic log lines (default) | Single-line Logger::Formatter style |
JSON |
Pipelines that eat JSON | One compact JSON object per line |
KeyValue |
Grepping key=value |
key=value.inspect pairs |
Logstash |
ELK-style stacks | JSON with @version / @timestamp |
Raw |
Bare payload | Message body only—no severity/timestamp wrapper |
Note
The JSON class is CMDx::LogFormatters::JSON (JSON in caps). The others are CamelCase: Line, KeyValue, Logstash, Raw.
{"severity":"INFO","timestamp":"2026-04-19T10:30:45.123456Z","progname":"cmdx","pid":12345,"message":{"cid":"...","index":0,"root":true,"type":"Task","task":"MyTask","tid":"...","state":"complete","status":"success","reason":null,"metadata":{},"strict":false,"deprecated":false,"retried":false,"retries":0,"duration":12.34,"tags":[]}}
Sample lifecycle¶
Here’s a real-ish line: a leaf task failed and propagation fields show how the failure relates to the chain:
I, [2026-04-19T17:04:15.875306Z #20173] INFO -- cmdx: cid="019b4c2b-2a02-..." index=1 root=false type="Task" task=CalculateTax tid="019b4c2b-2a00-..." state="interrupted" status="failed" reason="tax service unavailable" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"} duration=8.92 cause=nil origin=nil threw_failure=<CalculateTax ...> caused_failure=<CalculateTax ...> rolled_back=false
Tip
cid is your friend for tracing: pair it with your APM’s correlation id. To thread an external request id through, set a correlation_id resolver and filter on xid—see Configuration – correlation id. If Ruby rescues a plain StandardError (not fail!), you’ll see cause=#<TheError: …> and reason becomes "[TheError] message".
What’s in the log?¶
Each line is built from Result#to_h. Fields fall into a few buckets:
Severity / time (formatter adds these)¶
| Field | What it is | Example |
|---|---|---|
severity |
Log level name | INFO, WARN, ERROR |
timestamp |
UTC, ISO 8601, microseconds | 2026-04-19T18:43:15.000000Z |
pid |
OS process id | 3784 |
progname |
Logger progname | cmdx (unless you changed it) |
Raw skips the wrapper and prints only the message body.
Identity¶
| Field | What it is | Example |
|---|---|---|
cid |
Chain id (uuid_v7) | "018c2b95-b764-7615-..." |
xid |
External correlation (e.g. Rails request_id); nil unless you configure correlation_id |
"req-abc-123" |
index |
Step in the chain (root = 0) | 0, 1, 2 |
root |
Is this the root task’s result? | true / false |
type |
"Task" or "Workflow" |
"Task" |
task |
Task class name | GenerateInvoice |
tid |
This task run’s id (uuid_v7) | "018c2b95-..." |
context |
Frozen CMDx::Context (root teardown) |
#<CMDx::Context ...> |
tags |
From settings(tags: [...]) |
["billing"] |
Outcome¶
| Field | What it is | Example |
|---|---|---|
state |
Where the lifecycle ended | "complete", "interrupted" |
status |
Business result | "success", "skipped", "failed" |
reason |
String from fail! / halt |
"payment declined" or nil |
metadata |
Hash from halt | { code: "INSUFFICIENT_FUNDS" } |
Lifecycle extras¶
| Field | What it is | Example |
|---|---|---|
strict |
Ran via execute!? |
false |
deprecated |
Task class is deprecated? | false |
retried |
At least one retry? | false |
retries |
How many retries | 0 |
duration |
Milliseconds | 12.34 |
Only when status == "failed"¶
| Field | Meaning |
|---|---|
cause |
Underlying exception, or nil for fail! |
origin |
{ task:, tid: } if this failure was echoed from upstream; nil if it started here |
threw_failure |
{ task:, tid: } of the nearest upstream failure |
caused_failure |
{ task:, tid: } of the failure that actually caused the chain to fail |
rolled_back |
true if #rollback ran |
Configuration¶
Set formatter, level, and logger on CMDx.configure, or override per task with settings(...). Full list: Configuration – logging.
Note
If a task tweaks :log_level or :log_formatter, LoggerProxy dups the global logger so siblings don’t accidentally inherit those tweaks.
Custom logger¶
Point one task at its own Logger—different file, syslog, fancy structured gem, whatever:
class AuditTransfer < CMDx::Task
settings(
logger: Logger.new(Rails.root.join("log/audit.log"), progname: "audit"),
log_formatter: CMDx::LogFormatters::JSON.new
)
end
CMDx uses that logger as-is; it only dup’s when level or formatter differ from what you passed.
Silence the lifecycle line¶
Bump the task’s log level above INFO so the automatic completion line doesn’t fire:
Drop fields from the log (not from the result)¶
log_exclusions strips top-level keys from the logged hash—handy for huge :context or sensitive :metadata while keeping them on the Result and telemetry.
CMDx.configure do |config|
config.log_exclusions = [:context, :metadata]
end
class ImportPayroll < CMDx::Task
settings(log_exclusions: [:context])
end
Only top-level keys; no nested paths. Default is empty = log everything.
Log levels¶
CMDx writes the lifecycle line at INFO when the run completes. The framework itself doesn’t spam WARN / ERROR for you—use callbacks (on_failed, on_skipped) or telemetry (:task_retried, :task_executed) if you want louder logs.
class VerboseTask < CMDx::Task
settings(log_level: Logger::DEBUG)
def work
logger.debug { "feature flags: #{Features.active_flags.inspect}" }
# ...
end
end
Note
execute! still logs the lifecycle line and still emits :task_executed—Runtime finishes the result before it re-raises the Fault.
Logging inside work¶
Task#logger is the per-task logger (respects your settings):