Correlation Ids

Cross-service correlation is handled by automatic W3C traceparent propagation (useW3CTraceContext(app), in @benzene/diagnostics), which continues a distributed trace from the incoming traceparent header on every transport. See Common Middleware for that surface. This page covers the smaller, still-useful ICorrelationId — a per-invocation marker you can put in your logs and forward to downstream calls.

Overview

ICorrelationId (@benzene/abstractions) tracks a single correlation id for the current invocation. It is a scoped service — one instance per request — with two methods:

export interface ICorrelationId {
  set(correlationId: string): void;
  get(): string;
}

The default implementation, CorrelationId (@benzene/diagnostics), self-generates a value with crypto.randomUUID() when it is constructed, so get() always returns a non-empty id even if nothing ever calls set(). That makes it a zero-configuration per-invocation marker for logs. set() only overwrites the id with a non-empty value — passing '' or undefined leaves the generated id in place.

This is the TypeScript equivalent of .NET's Guid.NewGuid().ToString() seed: crypto.randomUUID() (from the built-in node:crypto) produces an RFC 4122 v4 UUID in the same canonical string form.

Installation

npm install @benzene/diagnostics

ICorrelationId itself lives in @benzene/abstractions (already a transitive dependency of the core packages); @benzene/diagnostics supplies the CorrelationId implementation and the registration/log-scope helpers.

Adding the correlation id to your logs

The correlation extensions are exported under the CorrelationExtensions namespace (TypeScript has no extension methods, so the C# Extensions static class becomes a namespaced set of free functions). withCorrelationId attaches the current correlationId to the request's logging scope, registering the CorrelationId service for the pipeline if it is not already:

import { CorrelationExtensions } from '@benzene/diagnostics';

app.useLogResult((x) => CorrelationExtensions.withCorrelationId(x));

useLogResult is a pipeline-builder member; the action it passes you is an ILogContextBuilder<TContext>, and withCorrelationId adds a correlationId field to every log scope created for the rest of the pipeline. With nothing populating it, the id is the self-generated UUID — a stable marker that ties together all the log lines of one invocation.

Populating it from a custom source

To seed the id from your own source (for example, a partner's proprietary request-id header), register the service with addCorrelationId at startup and call ICorrelationId.set(...) from your own middleware:

import { addBenzene } from '@benzene/core-message-handlers';
import { InlineAwsLambdaStartUp } from '@benzene/aws-lambda-core';
import { CorrelationExtensions } from '@benzene/diagnostics';
import { ICorrelationId } from '@benzene/abstractions';
import { IMessageHeadersGetter } from '@benzene/abstractions-messages';

// startup
const entryPoint = new InlineAwsLambdaStartUp()
  .configureServices((services) => {
    addBenzene(services);
    CorrelationExtensions.addCorrelationId(services);
  })
  .configure((app) => {
    app.useFn('PartnerCorrelation', async (context, next, resolver) => {
      const headers = resolver.getService(IMessageHeadersGetter) as unknown as IMessageHeadersGetter<typeof context>;
      const partnerId = CorrelationExtensions.getHeader(headers, context, 'x-partner-request-id');
      resolver.getService(ICorrelationId).set(partnerId);
      await next();
    });
    // ... the rest of your pipeline
  })
  .build();

getHeader(headersGetter, context, keys) is the ported header lookup: it matches header keys case-insensitively, skips empty values, and — when given a list — returns the first key that is present, in the order you give them. It returns '' when none match, which set() then ignores, so the self-generated id survives a missing header:

// try a partner header first, then fall back to a standard one
const id = CorrelationExtensions.getHeader(headers, context, ['x-partner-request-id', 'x-correlation-id']);

Because addCorrelationId registers the service as scoped, each invocation gets its own CorrelationId, so setting it in one request never bleeds into another.

Forwarding it to outbound calls

An outbound message client can stamp the current invocation's correlation id onto every request it sends. withCorrelationId on a ClientBuilder (@benzene/clients) wraps the client in a decorator that copies ICorrelationId.get() into the outgoing headers before delegating:

import { withCorrelationId } from '@benzene/clients';

// on your outbound client builder
withCorrelationId(clientBuilder);

By default the value is written under the correlationId header key; the underlying CorrelationIdBenzeneMessageClient takes an optional third constructor argument to use a different key. Existing headers on the request are preserved — only the correlation header is added.

The default key is correlationId (not the x-correlation-id string the .NET docs mention); pass a custom key to the CorrelationIdBenzeneMessageClient constructor if you need a specific wire header.

See Also