DEV Community

LorenzHolm3752
LorenzHolm3752

Posted on

Express Error Tracking — Pino and Winston Correlation Across Tenant Cohorts

Run one correlation context through the request, application logs, and captured exception, while keeping log and exception delivery independent. The deciding constraint is signal quality: a support experiment becomes unreadable when one failure produces several unrelated records.

TL;DR: validate or generate one request ID, bind it to request-local context, and serialize the same field into Pino or Winston logs and the exception event. Capture an unhandled exception once, at the Express error boundary. Add server-assigned experiment and cohort keys, but never customer messages, credentials, or raw authorization values.

How should Express error tracking integrate Pino or Winston?

Every record for one request carries one canonical request_id. Treat an inbound header as untrusted input: accept it only after syntax and length checks, or replace it with an opaque generated value. Return the canonical value in the response so support staff can use it. Experiment and cohort values must come from server-side assignment; a client-selected cohort would corrupt the comparison.

Exception ownership is singular. Lower layers may log useful state while propagating an error, but only the terminal boundary captures it. A repository, service, controller, and error handler capturing the same database error would turn one defect into four events.

The 64-character ceiling in the example is a local policy, not a protocol limit, and that distinction belongs in the design record. Pick a bound that fits the identifiers admitted by the actual ingress layer, test it at that boundary, and ensure every downstream schema can store it without truncation. A tighter bound limits accidental cardinality and oversized fields; a looser one may preserve identifiers issued by an upstream system. The trade-off is compatibility versus control. Likewise, accepting an inbound value preserves correlation across a trusted proxy, but it is not suitable when arbitrary internet clients can choose IDs that later appear in privileged operator searches. In that case the application should generate its canonical ID and, if cross-system diagnosis still needs the inbound value, retain a sanitized upstream ID in a separate field with separate access rules. This is one reason a generic logger integration cannot make the architecture decision for the service: the correct trust boundary depends on deployment topology, not on whether Pino or Winston formats the eventual record.

Noise wins.

Decision and failure boundaries

Use request-local context as the source of correlation fields, then send data through two narrow adapters. They share a schema, not a transport. A shared key is not a distributed transaction: the process can terminate after one export and before the other, so each surviving record must independently identify its request and cohort.

Option Signal quality Failure boundary Decision
Shared context, separate bounded delivery Stable fields and one terminal event Either sink can fail alone Adopt
Logger forwards errors to exception tracking Severity changes alter event counts One transport can lose both views Reject here
Every layer captures and rethrows Duplicate-heavy One defect expands into many events Reject
Exceptions without request logs Clean count, weak reconstruction Surrounding decisions are absent Valid for tiny stateless handlers

Failure boundaries include invalid IDs, lost async context, serialization errors, full queues, remote rejection, and shutdown with buffered records. Replace invalid IDs, pass context explicitly to detached jobs, constrain the schema, cap queues, count drops, and use a fixed flush deadline. Never retry without a bound on the request thread.

Critical path in Python

This framework-neutral example shows the contract. In Express, early middleware creates the context; a Pino child logger or Winston metadata merge implements the logger adapter, while the exception client implements the second adapter.

from contextvars import ContextVar
from dataclasses import dataclass, asdict
from re import fullmatch
from uuid import uuid4

context_var = ContextVar("request_context", default=None)

@dataclass(frozen=True)
class RequestContext:
    request_id: str
    experiment_key: str
    cohort_key: str

def request_id(value):
    valid = value and len(value) <= 64 and fullmatch(r"[A-Za-z0-9._-]+", value)
    return value if valid else str(uuid4())

def run(logger, exceptions, inbound_id, experiment, cohort):
    context = RequestContext(request_id(inbound_id), experiment, cohort)
    token = context_var.set(context)
    fields = asdict(context)
    try:
        logger.info("support evaluation started", fields)
        evaluate_reply_candidate()
        logger.info("support evaluation completed", fields)
    except Exception as error:
        failure = {**fields, "error_type": type(error).__name__}
        logger.error("support evaluation failed", failure)
        exceptions.capture(error, failure)
        raise
    finally:
        context_var.reset(token)

def evaluate_reply_candidate():
    # Customer message content does not belong in telemetry.
    return None
Enter fullscreen mode Exit fullscreen mode

Preserve one spelling and type across both adapters. requestId in logs and request_id in exceptions creates permanent query translation. Capture the exception through the sink's native mechanism, but give the logger a controlled error representation. OWASP advises excluding or protecting secrets, tokens, passwords, sensitive personal data, and connection strings; the correlation ID lets an authorized operator retrieve context elsewhere instead of copying it into every record.

Measure failures, not instrumentation

Use requests as the denominator, then compare terminal exception events by server-assigned cohort. Keep request ID as a drill-down key, never an aggregation dimension. Also measure terminal exceptions per failed request, correlation misses, unknown cohort assignments, dropped queue items, and rejected payloads. These telemetry defects can bias the experiment before the application change does.

Deploy schema changes in two phases: emit fields while consumers tolerate absence, validate coverage and cardinality, then make dashboards depend on them. Test two interleaved requests to prove context never crosses. Inject independent adapter failures. Send representative secrets through test inputs and assert that serialized telemetry excludes them.

Why reject one forwarding pipeline?

Forwarding error-level logger records into exception tracking merges two decisions: what deserves a log and what counts as an exception. A severity change then changes experiment results; automatic boundary capture creates duplicates; synchronous forwarding leaks remote latency into the customer path.

The option remains valid for a small service with one error boundary, no second capture path, bounded queues, and an explicit rule that only one terminal record becomes an exception. For this cohort comparison, independent adapters preserve cleaner evidence. Pino versus Winston is secondary; evaluate structured fields, redaction, backpressure, and shutdown behavior, while keeping the envelope stable.

The rollout decision should be reversible. Begin with a shadow comparison in which cohort fields are emitted but do not drive an alert, then inspect whether each failed request has the expected terminal event and whether normal requests remain visible without excessive record volume. Set queue capacity, delivery timeout, and shutdown deadline from the service's own traffic and latency budget; no source supports a universal value for those limits. Record the chosen values beside the deployment configuration, because a bounded queue without an operator-visible drop counter is merely silent loss with a maximum size. If the sink cannot accept the required structured fields, or if its client can only perform synchronous delivery on the request path, this design is not suitable for that sink. Keep the generic adapter, select a destination that supports the contract, or accept a documented reduction in correlation rather than hiding the mismatch.

Keep it boring. One validated ID, one authoritative cohort assignment, one capture point, and two bounded deliveries are enough.

References

Top comments (0)