Reference
Command states
Every traced command ends up in one of six states:
| State | Meaning |
|---|---|
Created |
The command was received but has not been executed yet. |
Successful |
The command completed without errors (HTTP 2xx). |
Cancelled |
The command was redirected or abandoned (HTTP 3xx). |
Rejected |
The command was refused due to client error — validation failure, missing data, forbidden (HTTP 4xx). |
Conflict |
A specific case of rejection indicating a resource conflict. Not automatically derived from HTTP status codes — available for application code to set directly. |
Failed |
The command failed due to a server error (HTTP 5xx) or an unhandled exception. |
How state is determined
In the HTTP path, state is derived from the response status code:
| HTTP status | State |
|---|---|
2xx |
Successful |
3xx |
Cancelled |
4xx |
Rejected |
5xx |
Failed |
In the direct (non-HTTP) path, state is derived from the Problem response:
-
No problem → Successful
-
Problem with status < 500 or null → Rejected
-
Problem with status >= 500 → Failed
Filtering with CommandStateCategory
The includeStates attribute on @CommandTracing controls which outcomes are persisted:
| Category | Which states are stored |
|---|---|
All |
Every command, regardless of outcome (default). |
NotSuccessful |
Only commands that did not succeed — Cancelled, Rejected, Conflict, Failed. |
Failure |
Only server-side failures — Failed. |
None |
Nothing is stored. Useful for disabling persistence while keeping lifecycle hooks active. |
Importance and retention
Every command is assigned an importance level, which determines its retention policy.
Importance
| Level | When to use |
|---|---|
Low |
Routine operations, background jobs, dry-run executions. Can be cleaned up after a short period. |
Normal |
Standard user-initiated operations (default). Kept for a reasonable period. |
High |
Critical operations — submissions, payments, signing. Kept permanently. |
Set importance on the annotation:
@CommandTracing(importance = CommandImportance.High)
public Report submitReport(@Valid @Body SubmitReportCommand cmd) { }
Or adjust it dynamically with a ParamsTransformer — for example, reducing importance for dry-run commands or partial-edit autosaves.
For very high-volume endpoints, skip persisting successful executions entirely while keeping non-successful ones.
Retention
Retention is derived automatically from importance:
| Importance | Retention |
|---|---|
Low |
ShortLived |
Normal |
LongLived |
High |
Permanent |
The retention value is stored in the command_log table and can be used by a cleanup job to purge old, low-importance entries while preserving critical ones.
The library does not include a cleanup job — retention is a policy marker that your application acts on.
Sensitive data
By default, the full command payload is stored in the cmd_body column as JSONB.
This is valuable for debugging and replay, but not always appropriate.
The library offers two mechanisms for keeping sensitive payloads out of cmd_body:
-
Redactable— per-cmd, can fully omit or return a payload-redacted copy. Choose this when the cmd type itself knows what is sensitive. -
ExcludeCmdBody— per-tracing-call, all-or-nothing. Choose this when the decision is endpoint-level (an@CommandTracingattribute) or runtime-contextual (programmatic).
Redactable
A cmd that implements Redactable controls what lands in cmd_body while keeping its identity intact.
The framework calls redactedForTracing() immediately before serialising and uses the returned value as the body source.
public record UploadInvoicePdfCommand(
UUID cmdUuid,
String cmdSourceRef,
String tenantId,
String filename,
byte[] pdfBytes
) implements TenantCommand, Redactable {
@Override
public Optional<Command> redactedForTracing() {
return Optional.of(new UploadInvoicePdfCommand(
cmdUuid, cmdSourceRef, tenantId, filename, null
));
}
}
Three shapes the implementation can take:
-
Hide the entire body — return
Optional.empty(). The audit row carries identity only;cmd_bodyis the empty map. -
Drop a field — return a copy with the sensitive field nulled. Other fields land in
cmd_bodyas usual. -
Mask a value — return a copy with the sensitive field replaced (e.g. with
"*").
Identity (cmd uuid, tenant/user/client refs, timestamps) is always preserved by the framework — the implementation cannot accidentally drop them.
Always return a new instance; mutating this will corrupt the in-flight cmd execution.
If the implementation throws, the framework falls back to publishing the un-redacted cmd and emits a warn on logger no.conta.command.tracing.Redaction starting with "Redactable.redactedForTracing threw".
This is fail-open by design — redaction must never mask the real outcome of the cmd execution — so set up an alert on that warn line: when it fires, the secret payload just landed in the audit log.
ExcludeCmdBody
CommandTracingOption.ExcludeCmdBody skips the body entirely for a specific traced call.
Useful when the cmd type doesn’t know it’s being traced sensitively, or when the decision is endpoint- or context-driven.
On the annotation:
@CommandTracing(options = CommandTracingOption.ExcludeCmdBody)
public Response uploadFile(@Valid @Body UploadFileCommand cmd) { }
Or programmatically:
CommandTracingContext.set(cmd,
CommandTracingParams.withOptions(CommandTracingOption.ExcludeCmdBody));
In both cases, the cmd is still traced — cmd_class, state, duration, and all other metadata are recorded.
Only the body is omitted.
Choosing between them
Use Redactable |
when the cmd type carries known-sensitive fields and other commands of the same type should always redact. Lives on the cmd; works regardless of how it is invoked. |
|---|---|
Use |
when the decision is endpoint- or context-specific. Lives on the |
Use both |
a cmd may implement |
Global schema vs tenant schema
In multi-tenant applications, the global command_log is often kept lean — just enough metadata for cross-tenant insights and monitoring.
A more detailed command log in the tenant-specific schema can store the full payload, including sensitive fields that shouldn’t leave the tenant boundary.
This dual-schema pattern is not yet built into the library, but the mechanisms above provide the building blocks: the global publisher excludes or redacts the body, while a tenant-scoped listener persists the full detail.
Problem+JSON integration
The library integrates with the conta-problem-json library to capture structured error information alongside every failed command.
How problems are captured
When a command fails in the HTTP path, the CommandTracingFilter reads the response body.
If it contains a problem+json response, it is deserialized into a Problem object and stored in the command log.
This gives you two pieces of information for every failure:
-
problem_code— the application-specific error code (stored as a separate, queryable column) -
problem— the full problem+json body as JSONB (including status, detail, and any additional info fields)
The two failure paths
The CommandTracingListener has two onCommandFailed overloads because errors surface differently depending on the execution path:
onCommandFailed(Command, Throwable)-
Called in the direct (non-HTTP) path, where the original exception is available. Also called as a fallback from the Problem overload.
onCommandFailed(Command, Problem)-
Called in the HTTP filter path, where the exception has already been serialized to a problem+json response. By default, this delegates to the Throwable overload via
problem.toThrowable(), so listeners that only implement the Throwable variant still receive notifications from both paths.
What this enables
With problem codes stored in the command log, you can:
-
Count how often each error code occurs across the platform
-
Detect new failure patterns as they emerge
-
Track whether a fix actually reduced the error rate
-
Build dashboards that show the most impactful errors — by frequency, by affected tenants, by trend
See Example Queries for SQL patterns that query by problem code.