When to Use
Use this skill when distributed algorithmic trading microservices (order routers, market data gateways, risk managers, execution engines) ship logs off-host into a shared store — Grafana Loki, an OpenTelemetry Collector, or an ELK cluster. Two things go wrong at that boundary and both are expensive: credentials that were harmless in a local file become an exfiltration target once they are centrally indexed and broadly readable, and unstructured text becomes unqueryable exactly when an incident makes querying urgent.
CentralizedLogAggregatorEngine takes a batch of RawLogRecords and returns an ObservabilityReport containing one structured JSON line per record: credential-bearing metadata keys replaced with [REDACTED], an OpenTelemetry severity_number normalised across logging dialects, RFC 3339 UTC timestamps at microsecond precision, optional deterministic sampling of TRACE/DEBUG, and an error-velocity check over the batch.
When NOT to Use
- You need alerting with a latency bound. This engine only evaluates the batch it is handed, so alert latency equals your flush cadence and a spike straddling two flushes may trip neither. Firms in scope of MiFID II RTS 6 should note Article 16(5): "Real-time alerts shall be generated within five seconds after the relevant event." Put that monitoring on its own path — not behind a log-shipping pipeline.
- You are relying on redaction to make it safe to log secrets. Redaction here is metadata-key scoped. It never inspects
message, sof"auth failed for {api_key}"ships the key verbatim. The control is "never put a credential in a log", not "the aggregator will catch it". - You need a literal OpenTelemetry log record. The emitted schema is this engine's own (
timestamp_iso,subsystem,level,message,metadata) plusseverity_number; it is not OTel'sTimestamp/Body/Attributesmodel. Map it explicitly in your Collector — seereferences/standards.md. - You are designing the event taxonomy itself (event types, sequence numbers, order-lifecycle correlation) — start at
structured-logging-for-post-incident-forensics; this skill assumes the schema already exists and handles the shipping boundary. - The record is a regulatory order record. Order records under RTS 6 Article 28 / Annex II are governed by retention and completeness rules; do not point a redactor at that path. See
record-retention-periods-by-jurisdiction.
Prerequisites
- Log records shaped as
RawLogRecord:subsystem(ORDER_ROUTER/RISK_GATEWAY/MARKET_DATA),level,message,correlation_id, optionalmetadatamapping andtimestamp_epoch. - A Loki / OTel Collector ingestion endpoint, and a decision about which fields become stream labels (see pitfalls —
correlation_idmust not be one). - Agreement on which metadata field names at your firm carry credentials; anything site-specific goes in
extra_sensitive_key_substrings.
Workflow
-
Configure the engine before the first flush, not after an incident.
error_spike_threshold_countfires at>=— a threshold of 10 alerts on the 10th error in the batch, not the 11th. It must be>= 1;0is rejected because it would alert on error-free batches.diagnostic_sample_ratedefaults to1(nothing is sampled). Raise it only for TRACE/DEBUG volume; INFO and above are never sampled, so an alert path can never be thinned by this setting.- Inject
clock_fnin tests soobserved_timestamp_isoandmax_ingest_lag_secondsare deterministic.
-
Redact on the emitting side, before the batch leaves the host.
- Keys are matched case- and separator-insensitively (
API-Key,api_key,apiKeyall collapse), by exact match and substring, sobroker_api_keyandaccess_tokenare caught — an exact-match blocklist misses both. - Traversal covers nested mappings and mappings inside lists (captured HTTP headers, retry attempts, multi-leg payloads).
tokenis not a bare substring pattern:token_symbol/token_addressin DEX metadata are data, not credentials. Add site-specific names viaextra_sensitive_key_substringsrather than widening to patterns that eat real fields.- Nesting deeper than
MAX_REDACTION_DEPTH(12) is replaced with[TRUNCATED: MAX_DEPTH]— bounded so a cyclic structure cannot raiseRecursionErrorand take the whole batch with it.
- Keys are matched case- and separator-insensitively (
-
Normalise severity before counting anything.
- Level text is mapped to an OpenTelemetry SeverityNumber (TRACE 1 / DEBUG 5 / INFO 9 / WARN 13 / ERROR 17 / FATAL 21), so
WARNING(Python),FATAL(log4j) anderr(syslog) bucket identically. Bucketing on raw strings silently drops whole dialects out of the error count. - A level string with no mapping is emitted with
severity_number: 0, counted inunknown_level_count, and logged as a warning. It is not counted as an error — assertunknown_level_count == 0in CI, or a service using an unknown dialect will never trip the spike alarm.
- Level text is mapped to an OpenTelemetry SeverityNumber (TRACE 1 / DEBUG 5 / INFO 9 / WARN 13 / ERROR 17 / FATAL 21), so
-
Sample only what is safe to lose.
- Sampling keeps 1 of every N TRACE/DEBUG records deterministically (first of each group), per batch, with no state carried between calls. Kept lines carry
sample_rateso downstream counts can be rescaled; without it every DEBUG-derived metric is silently wrong by a factor of N.
- Sampling keeps 1 of every N TRACE/DEBUG records deterministically (first of each group), per batch, with no state carried between calls. Kept lines carry
-
Audit the batch, then act on the report — not just on
status.has_error_spike_alert/statusis the headline.redacted_keys_count,coerced_values_count,malformed_record_count,unknown_level_count,sampled_out_countandmax_ingest_lag_secondsare the operational signals: a risingmax_ingest_lag_secondspredicts backend rejection, and a non-zeromalformed_record_countmeans some service is emitting records this pipeline had to repair.
Full procedure: see
references/workflows.md. Standards, sources, and the OTel/Loki field mapping: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Interpolating a secret into the message string.
logger.error(f"auth failed: {api_key}")defeats key-based redaction completely, and the line is already in central storage by the time anyone notices. Pass credentials-adjacent context as metadata keys, never as message text. - Trusting an exact-match credential blocklist.
{"api_key"}does not matchbroker_api_key,access_token,X-Api-Key, orAuthorization. The prefixed and suffixed forms are the common ones in real payloads — match on the normalised key, by substring. - Redacting only the top level of the mapping. Secrets arrive nested — inside
wallet, inside a list of captured request headers, inside per-retry attempt records. Dict-only recursion leaks every one held in a list. - Over-redacting with a bare
tokenrule. In crypto/DEX metadatatoken_symbolandtoken_addressare the fields you need for forensics. Destroying them protects nothing. - Promoting
correlation_id(or any per-order ID) to a Loki stream label. Loki's documentation warns that high cardinality "causes Loki to build a huge index and to flush thousands of tiny chunks", performs "very poorly", and advises "10 - 15 labels at a maximum" (default limit 15 index labels), directing high-cardinality search at structured metadata instead. Keepcorrelation_idin the JSON line, and label on bounded dimensions (subsystem,level, environment). - Buffering a long backlog and flushing it late. With unordered writes (Loki's default since 2.4) Loki still only accepts entries back to
highest_timestamp_written − max_chunk_age/2per stream, rejecting older ones astoo_far_behind. Replaying a post-outage backlog can therefore silently drop exactly the logs the incident review needs. Watchmax_ingest_lag_secondsagainst your cluster'smax_chunk_age. - Letting one bad field kill the flush.
Decimalprices,datetimeobjects andNaNratios are normal trading metadata: rawjson.dumpsraises on the first two and emits invalid JSON (NaN,Infinity) for the third. This engine coerces them and counts it incoerced_values_count— a pipeline that raises loses every other log in the batch, including the ones explaining the failure. - Formatting timestamps at whole-second resolution. A trading system emits many events per second; second-granularity stamps make a post-incident timeline unorderable. Timestamps here are microsecond RFC 3339 UTC.
- Reading
info_logs_countas "everything that wasn't an error". It is INFO only; WARN has its own counter. Reading a lumped count hides a warning surge that precedes most incidents.
Verification
- Run the unit suite:
python -m unittest discover -s skills/log-aggregation-and-centralized-observability/scripts— all tests must pass. - Ingest a record whose metadata holds
api_key,broker_api_key,access_token,Authorization, a nestedwallet.private_key, and a credential inside a list of headers; confirm all six become[REDACTED],redacted_keys_count == 6, and thattoken_symbolis untouched. - Ingest a batch of 15 records at
FATALandWARNINGwith the default threshold of 10; confirmerror_logs_count == 15,warn_logs_countis separate, andstatus == "OBSERVABILITY_ERROR_SPIKE_ALERT". Repeat with exactly 9 and 10 errors to confirm the boundary fires on the 10th. - Ingest metadata containing
Decimal("101.25"), adatetime, andfloat("nan"); confirm every payload survivesjson.loadsand the batch does not raise. - Ingest an unknown level string (e.g.
"LOUD"); confirmunknown_level_countrises,severity_numberis0, and the record is still emitted. - Run with
diagnostic_sample_rate=10over 100 DEBUG plus 2 ERROR records; confirm 10 DEBUG lines survive withsample_rate: 10, both ERROR lines survive, andsampled_out_count == 90.
Related Skills
structured-logging-for-post-incident-forensicsdata-lineage-tracking-for-audit-and-debuggingsandbox-credential-leakage-preventioncross-vendor-timestamp-precision-reconciliationrecord-retention-periods-by-jurisdiction