Skip to content

feat: add flatten: 'otel' and otelAttributesFormat for winston-transport 0.29+ (2.3.0) - #154

Merged
cuzzlor merged 1 commit into
fix/date-metadatafrom
feat/otel-attributes-format
Oct 16, 2026
Merged

cuzzlor merged 1 commit into
fix/date-metadatafrom
feat/otel-attributes-format

Conversation

@cuzzlor

@cuzzlor cuzzlor commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #153 (2.2.1). Merge that first; GitHub then retargets this PR to main. Merging this one alone would ship both changes as 2.3.0.

Why

flatten: true was written for a transport that copied attributes through as they were. @opentelemetry/winston-transport has changed twice since:

Transport SDK line Change
0.29.0 (2026-06-11) 0.219 Lifts err/error off the record and gives it to the logs SDK as the exception (contrib#3441). The SDK keeps only name, message, stack (as exception.type/message/stacktrace) and a cause's message (error.cause.*).
0.32.0 (2026-08-31) 0.222 Normalises object attributes into nested OTLP maps, serialising Errors into maps (contrib#3672). It does not stringify them.

On 0.29 and later neither existing option is right, and the new spec proves both against the real transport:

  • flatten: true stringifies the error before the transport sees it, so the SDK records the whole JSON as one exception.message, with no exception.type or stack. This is the [object Object]-era shape seen in production on makerx-reports (MakerXStudio/makerx-reports#225).
  • No flattening records the exception, but the rest of the error (an HTTP status, a provider's error body, the cause chain) is dropped, and other objects arrive as nested maps, which Log Analytics does not index.

What

otelAttributesFormat() shapes a record for 0.29 and later:

  • error (or err, matching the transport's precedence) stays an object with only name, message, stack, and a cause the transport can describe. Everything else, including the full cause chain, goes to errorDetail as JSON.
  • code goes to errorDetail too, because the SDK takes a truthy code over name as exception.type. It stays when the error has no name.
  • Every other object value becomes a JSON string; scalars are left alone; a Date becomes its ISO string, unquoted. Errors nested elsewhere are serialised, BigInts are written as <n>n, and nothing throws out of the log call.

Two ways to apply it, one implementation:

// On the transport: other transports, the console included, stay nested.
transports: [new OpenTelemetryTransportV3({ format: otelAttributesFormat() })]

// At the logger level: the only option with WinstonInstrumentation, which
// constructs its transport with no format.
createLogger({ flatten: 'otel' })

The README's OpenTelemetry section is rewritten around a transport-line matrix: flatten: true / jsonStringifyValuesFormat before 0.29, flatten: 'otel' / otelAttributesFormat from 0.29.

Compatibility

2.3.0 — minor. flatten?: boolean widens to boolean | 'otel'; flatten: true and every other default are unchanged, so no existing consumer's output moves until it opts in. flattenReplacer applies in both modes.

No peer dependency on the transport is added: consumers on a transport before 0.29 legitimately keep flatten: true, and a peer range would warn them for it.

Next (not in this PR)

A 3.0.0 could make the OTel shape the default for flatten and drop flatten: true from createLogger (keeping jsonStringifyValuesFormat exported), with transport 0.29 as the supported minimum. That is a deliberate output change for a major.

Consumers

  • makerx-reports (transport 0.31, constructs its own): replaces its hand-rolled flattenForOtel with otelAttributesFormat() on the transport.
  • MakerBOS (transport 0.27): keeps jsonStringifyValuesFormat() on the transport until its OTel SDK moves past 0.218.
  • graphql-template-esm (transport 0.33, WinstonInstrumentation): moves from flatten: true to flatten: 'otel'.

Test plan

  • npm run build — lint, tsc, rollup, attw --pack, CJS smoke test
  • npm run audit clean (the audit fix is in fix: keep a Date in log metadata instead of writing {} (2.2.1) #153)
  • npm test — 185 tests. New: otel-attributes-format.spec.ts (16, the format on its own) and otel-transport.spec.ts (15), which drives the real transport at 0.27, 0.31 and 0.34, installed under npm aliases, into an in-memory logs SDK and asserts the exported attributes — including both failure modes above, so a future transport change shows up here first

…ort 0.29+

From 0.29, @opentelemetry/winston-transport lifts an `err`/`error` value off
the record and hands it to the logs SDK as the exception, which keeps only
name, message, stack and a cause's message. So:

- `flatten: true` stringifies the error first, and the SDK records the whole
  JSON as one `exception.message`, with no type or stack trace.
- No flattening records the exception but drops the rest of the error
  (status, provider data, cause chain), and 0.32+ sends other objects as
  nested maps that Log Analytics does not index.

`otelAttributesFormat` keeps the error an object with only the fields the
SDK reads, moves the rest to `<key>Detail` as JSON (code too, since the SDK
prefers a truthy code over name for exception.type), and stringifies every
other object value; a Date becomes its unquoted ISO string. It can sit on
the transport, keeping other transports nested, or be applied for every
transport with `createLogger({ flatten: 'otel' })` — the only option with
WinstonInstrumentation, which constructs its transport without a format.

`flatten: true` is unchanged and documented as the mode for transports
before 0.29. The README gains a transport-line matrix. A new spec drives
the real transport at 0.27, 0.31 and 0.34 (npm aliases) into the logs SDK
and asserts the exported attributes, including the two failure modes above.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant