ecluse
Safe HaskellNone
LanguageGHC2021

Ecluse.Log

Description

The structured-logging pipeline.

Écluse sits in the install path of someone else's build, so when it refuses a package or runs slow the operator must see why from the logs alone. This module stands up a katip LogEnv — the single log stream every layer attaches context to — and chooses its on-the-wire shape:

  • JsonLog writes one compact JSON object per line to stdout (JSONL): the whole physical line is the JSON, with no pretty-printing and no level or timestamp prefix outside the object, and any newline inside a field escaped as \n so a record never spans two lines. This is the in-container default, the shape a log collector's stdout JSON autodiscovery consumes directly.
  • ConsoleLog writes the human-readable bracketed form for local development.

A LogEnv built here carries no colour codes even on a terminal, so a captured JSON line is always valid JSON. The selected format is parsed from PROXY_LOG_FORMAT at the configuration boundary (Ecluse.Config) and the resulting LogEnv is held in the composition root (Ecluse.Env).

Trace-ID correlation rides this stream as the dd object (ddField): service/ env/version from the resolved telemetry identity, plus the active span's trace_id/span_id in the id format Datadog expects (formatDdTraceId). The object is built here but stays free of any OpenTelemetry dependency — the active span is read and the ids rendered by Ecluse.Telemetry.Correlation, which composes ddField into a log site's payload.

Secrets

A bearer token is carried as the redacted Secret of Ecluse.Credential, whose Show renders only a placeholder, so token material cannot reach a log field through any structured payload or message built from it (see docs/architecture/observability.md). This module adds no field that would defeat that redaction.

The model is described in docs/architecture/observability.mdLogs.

Synopsis

Log format

data LogFormat Source #

The on-the-wire shape of the log stream, selected by configuration. A sum type rather than a Bool so each case names its intent and a new shape is a new constructor, not a second flag.

Constructors

JsonLog

One compact JSON object per line to stdout (JSONL) — the in-container default a log collector's stdout JSON parsing consumes.

ConsoleLog

The human-readable bracketed form, for local development.

Instances

Instances details
WireVocab LogFormat Source # 
Instance details

Defined in Ecluse.Log

Show LogFormat Source # 
Instance details

Defined in Ecluse.Log

Eq LogFormat Source # 
Instance details

Defined in Ecluse.Log

parseLogFormat :: Text -> Either Text LogFormat Source #

Parse a LogFormat from its wire name, naming the accepted set on failure. The same strict, fail-loud style as the other configuration enums (Ecluse.Config).

>>> parseLogFormat "json"
Right JsonLog
>>> parseLogFormat "yaml"
Left "unknown log format \"yaml\" (expected one of: json, console)"

renderLogFormat :: LogFormat -> Text Source #

The wire name of a LogFormat (the inverse of parseLogFormat).

Pipeline construction

newLogEnv :: LogFormat -> Environment -> IO LogEnv Source #

Build the application LogEnv: a katip environment under the ecluse namespace with a single stdout scribe in the chosen LogFormat. This is the value the composition root holds and every later layer logs through.

The scribe admits every severity (DebugS upward); a deployment narrows what it keeps through katip's own verbosity controls rather than by rebuilding the environment.

newScribe :: LogFormat -> IO Scribe Source #

Build the stdout Scribe for a LogFormat. Colour is forced off (ColorLog False) so a captured JsonLog line is always valid JSON — no ANSI escapes leak into the object even when stdout is a terminal. The handle scribe writes each item as exactly one line (the formatter output plus a single trailing newline), which is what makes JsonLog a true JSONL stream.

formatterFor :: LogItem a => LogFormat -> ItemFormatter a Source #

The katip ItemFormatter a LogFormat wires into its scribe: the compact one-line JSON encoder for JsonLog, the bracketed human form for ConsoleLog.

Exposed so a test can render an item through the exact formatter the scribe uses, asserting on the serialised line without writing to stdout (see renderLogLine).

Structured context

auditContext Source #

Arguments

:: Text

The package the decision concerns.

-> Text

The package version.

-> Text

The name of the rule that decided.

-> SimpleLogPayload 

The structured context for an audit event — a denial or other rule decision — carrying the package, version, and rule the operator needs to explain a 403 from the log line alone. These are the high-cardinality identifiers that belong on the log line, never on a metric label (see docs/architecture/observability.md → "Cardinality and attributes").

Attach it to a log call as the structured payload; katip renders the three keys into the line's data object.

moduleField :: Text -> SimpleLogPayload Source #

The structured context naming the source module a log line was emitted from, so every JSON record carries a module field (e.g. "module":Ecluse.Server.Pipeline). Compose it into a log site's payload alongside the event's own fields, so the stream can be filtered by emitter without leaning on the katip namespace. katip renders the key into the line's data object. This is the standard tag for a log raised off the Handler reader (a plain-IO path that opens its own context through the composition-root LogEnv).

Datadog trace correlation

data DdContext Source #

The unified-service identity stamped onto every log line as the dd object, plus the active span's ids when one is in scope. service/env/version come from the same resolved telemetry identity as the traces (Ecluse.Telemetry.Resolve), so logs and traces share one identity; the trace/span ids are present only when a span is active (filled by Ecluse.Telemetry.Correlation off the OpenTelemetry context).

Constructors

DdContext 

Fields

Instances

Instances details
Show DdContext Source # 
Instance details

Defined in Ecluse.Log

Eq DdContext Source # 
Instance details

Defined in Ecluse.Log

data DdSpan Source #

The active span's ids, already in the id format Datadog expects (see formatDdTraceId / formatDdSpanId). Held as rendered Text so this type stays free of any OpenTelemetry dependency.

Constructors

DdSpan 

Fields

Instances

Instances details
Show DdSpan Source # 
Instance details

Defined in Ecluse.Log

Eq DdSpan Source # 
Instance details

Defined in Ecluse.Log

Methods

(==) :: DdSpan -> DdSpan -> Bool #

(/=) :: DdSpan -> DdSpan -> Bool #

ddField :: DdContext -> SimpleLogPayload Source #

The dd object as a katip structured payload, nested under the dd key. Compose it into a log site's payload so the rendered JSON line carries "dd":{"service":…,"trace_id":…} for trace-to-log correlation.

ddObject :: DdContext -> Value Source #

The dd object as JSON: service always, env/version when configured, and trace_id/span_id only when a span is active. This is the object a log collector's unified-service tagging and trace-to-log correlation read.

formatDdTraceId :: ByteString -> Text Source #

Render a raw 16-byte trace id into the id format Datadog correlates on: the unsigned decimal of the low 64 bits. Datadog's log↔trace correlation matches dd.trace_id as a decimal 64-bit value (the low half of an OpenTelemetry 128-bit id); the full-128-bit-hex form is a separate opt-in not used here. Reads the last eight bytes big-endian, so a shorter id is taken whole and a longer one is truncated to its low 64 bits — never a partial-byte misread.

formatDdSpanId :: ByteString -> Text Source #

Render a raw 8-byte span id into the Datadog form: the unsigned decimal of the 64-bit id (read big-endian), matching dd.span_id.

Rendering (for serialise-and-assert)

renderLogLine :: LogItem a => LogFormat -> Item a -> Text Source #

Render a single log Item to the exact text the scribe for this LogFormat writes for it — the formatter output for one item, without the trailing newline the handle scribe appends to separate physical lines.

This is what the unit tests assert on: it reproduces the scribe's serialisation with no stdout dependency, so a JsonLog line can be checked for being a single compact object with escaped newlines, and a ConsoleLog line for the human-readable form.