Skip to main content

Structured logging

The operator observability contract owns the JSON schema, event names, and correlation semantics. Preserve that contract when changing a log producer or its encoder. Logging is an operational interface: useful severity, trustworthy context, and bounded disclosure matter more than a console with no warnings.

Use the existing framework facilities​

Use SLF4J's fluent API for events operators need to query. Keep a stable event.name, a short human-readable message, and typed fields rather than embedding key=value fragments in the message. Use parameterized logging for ordinary diagnostic text. Do not build a second event abstraction or serialize a JSON string inside a log message.

log.atWarn()
.addKeyValue("event.name", "integration.request.failed")
.addKeyValue("http.request.method", exception.getMethod().name())
.addKeyValue("server.address", exception.getUri().getHost())
.addKeyValue("error.type", exception.getMostSpecificCause().getClass().getName())
.log("External service request failed");

Hephaestus writes Spring Boot's ECS format through Logback. Boot supports both MDC and the SLF4J key-value API. Two customizers in observability/ complete the format:

  • LogContextCustomizer writes the MDC and the event's key-value pairs as one nested context object.
  • CredentialMaskingCustomizer masks credential formats in every value.

Put a scope identifier in MDC: job.id, workspace.id, or the trace IDs. Put a fact of one event in its key-value pairs: event.name, an outcome, a duration. An event can name a scope identifier again, for example because it also runs outside the scope. Then the record carries the event's value once. Boot's own context pairs refuse such a name and the appender drops the whole record, so do not turn LogContextCustomizer off.

Keep private data out at the source​

Follow OWASP's logging guidance: never log credentials, full request/response bodies, signed URLs, private reviewed work, or raw SQL/row values. Exception messages can contain those same values. At a shared integration boundary, prefer the failure class and safe request metadata to the full exception. Preserve useful failure severity and correlation. Do not replace an operational failure with silence.

Use fixed field names. Request-controlled keys must not become top-level log keys. The configured encoder escapes string values and keeps MDC in a separate object. This prevents JSON record injection and context collisions. It does not make the underlying string public data. CWE-117 explains why an untrusted value must not change the structure or apparent origin of a log record.

Masking is a last defense. It masks a known sensitive field by name and a known credential format in every value. Patterns must work independently of their order. No regex can reliably recognize every secret or personal datum in arbitrary prose. Never enable request-body debugging as a substitute for safe structured diagnostics.

Scope and restore correlation​

Use Boot/Micrometer tracing and the existing task-context propagation. Do not generate an unrelated request ID or invent parent spans. Spring's tracing documentation defines MDC correlation and requires auto-configured HTTP-client builders for propagation. Spring's context-propagation guidance and Logback's MDC manual explain why thread-local context needs explicit lifecycle management when work moves between threads.

Test both directions: context reaches the asynchronous task, and completion or failure restores what was present before the task. Reuse the same worker thread in the regression so a workspace identifier left behind cannot pass unnoticed. Keep high-cardinality identifiers in logs/traces, not metric labels. The OpenTelemetry log data model distinguishes event attributes, severity, resource identity, and trace/span correlation. It does not make log text or an identifier proof of successful export.

Verify the emitted record​

RedactionContractTest loads the production encoder configuration and parses its actual bytes. ShippedLogEncoder gives other tests the same encoder. Use it to prove that an event of your own reaches the record with its fields, as ReviewJobTelemetryTest does for the review lifecycle events. Its contract includes typed values, correlation fields, severity isolation, nested exception redaction, opaque sensitive fields, and exactly one JSON record despite control characters.

GlobalControllerAdviceLoggingTest supplies hostile synthetic request/database values and verifies the actual emitted event and unchanged HTTP error contract. GitLab webhook and Outline client regressions exercise their own adapters with private response/transport payloads. The GitLab failure still reports its status. Outline retains its retryability contract. MdcPropagationTest proves propagation and restoration on a reused worker thread, including a failed task.

Run focused regressions first, then the unit baseline and affected server tiers. Also inspect current CI's raw reports: a green test count is not proof that every log is appropriate. Known upstream scanner, buildpack or compiler diagnostics need an explicit disposition, not a blanket logging-level change. Assert test failures in expected-negative fixtures narrowly. Keep unexpected diagnostics visible.