A set of winston formats, a console transport and a logger factory that simplify structured logging in Node.js services, with colourised YAML output for local development and records shaped for OpenTelemetry. Start with the quick start.
npm install @makerx/node-winstonwinston, logform, winston-transport, triple-beam and es-toolkit are peer dependencies so the package shares a single instance with your app's winston. npm 7+ and pnpm install them automatically; on Yarn Classic, install them yourself.
@opentelemetry/winston-transport >=0.29 is an optional peer dependency. Install it only when you ship logs through OpenTelemetry: see step 1 of the quick start.
Requires Node.js >=20.
See Migrating from v2 and Migrating from v1 at the bottom for breaking changes and new functionality.
A typical service instruments itself for OpenTelemetry, creates one logger, and sends audit records to a store of their own. Steps 1 and 3 are optional.
Install @opentelemetry/sdk-node, @opentelemetry/instrumentation-winston and @opentelemetry/winston-transport, and the other instrumentations you use, such as @opentelemetry/instrumentation-http below. The winston instrumentation loads the transport at runtime but does not depend on it, and without it no record is sent. An ES module app also needs @opentelemetry/instrumentation, for the hook that tracing.ts registers.
An instrumentation patches a module as that module loads, so tracing must start before anything imports winston. In an ES module app, an entry point with no static imports enforces that order:
// main.ts
await import('./tracing.js')
await import('./server.js')// tracing.ts
import { register } from 'node:module'
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'
import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston'
import { NodeSDK } from '@opentelemetry/sdk-node'
const instrumentations = [new HttpInstrumentation(), new WinstonInstrumentation()]
// For an ES module app: winston is loaded by `import`, which only this hook can patch.
register('@opentelemetry/instrumentation/hook.mjs', import.meta.url)
export const sdk = new NodeSDK({ instrumentations })
sdk.start()A CommonJS app can require('./tracing') first instead, and needs no register call. NodeSDK reads its exporters from the standard OTEL_* environment variables. Call sdk.shutdown() last on shutdown, to send what is still queued.
// logger.ts
import { isLocalDev } from '@makerx/node-common'
import { createLogger } from '@makerx/node-winston'
export const logger = createLogger({
consoleFormat: isLocalDev ? 'pretty' : 'json',
loggerOptions: {
level: process.env.LOG_LEVEL ?? 'info',
defaultMeta: { service: 'my-api' },
},
redactPaths: ['headers.authorization', 'token'],
otel: true,
})otel: true shapes what the OpenTelemetry transport from step 1 receives. The console keeps nested JSON, or coloured YAML locally. Without step 1, or where the SDK does not run, as in most tests, otel changes nothing. To silence the console in tests, pass consoleOptions: { silent: true }. Every option is listed under Options.
logger.audit() writes at the custom audit level (see Log levels). To keep audit records in a Log Analytics table of their own as well, send them through a CallbackTransport. This example uses the Azure Monitor Logs Ingestion API, with a data collection rule for the custom table:
// audit.ts
import { DefaultAzureCredential } from '@azure/identity'
import { LogsIngestionClient } from '@azure/monitor-ingestion'
import { CallbackTransport, type LogError } from '@makerx/node-winston'
const client = new LogsIngestionClient(process.env.AUDIT_ENDPOINT_URL!, new DefaultAzureCredential())
export const createAuditTransport = (logError: LogError) =>
new CallbackTransport(
({ message, meta }) =>
client.upload(process.env.AUDIT_RULE_ID!, process.env.AUDIT_STREAM_NAME!, [
{ TimeGenerated: new Date().toISOString(), Message: message, Info: meta },
]),
logError,
{ onlyLevels: ['audit'], closeTimeoutMs: 5_000 },
)// logger.ts
import { createAuditTransport } from './audit.js'
const auditTransport = createAuditTransport((message, ...args) => logger.error(message, ...args))
export const logger = createLogger({
// ...the options from step 2
transports: [auditTransport],
})
/** Call on shutdown, before `sdk.shutdown()`. Waits up to 5 seconds for audit uploads still running. */
export const closeAuditTransport = () => auditTransport.close()onlyLevels: ['audit']forwardsauditrecords only. Theleveloption would also forwarderrorandwarn.- An upload that fails goes to
logError, which logs it as an error. Errors never reach this transport, so a failing endpoint cannot loop. - Winston does not wait for a transport to close, so await
close()yourself.closeTimeoutMsbounds the wait, and any uploads still running are reported throughlogError. - The record's columns,
TimeGenerated,MessageandInfo, must match the stream that the data collection rule declares.
createLogger builds a winston Logger with a pre-configured Console transport and a set of logger-level formats that apply to every transport.
Formats are applied in layers:
- Logger-level (applied to all transports, in order):
serializeErrorFormat→omitFormat(ifomitPaths) →redactFormat(ifredactPaths) →loggerOptions.format(if supplied). - Console transport (applied to Console output only):
omitNilFormat→ anyconsoleFormats→ eitherprettyConsoleFormat(whenconsoleFormat: 'pretty') ortimestamp+json(whenconsoleFormat: 'json'). - OpenTelemetry transport (with
otel):otelAttributesFormat, after the logger-level formats. See OpenTelemetry.
| Option | Type | Description |
|---|---|---|
consoleFormat |
'json' | 'pretty' |
Output format for the Console transport. json (default) for deployed environments, pretty for colourised YAML during local development. |
consoleOptions |
ConsoleTransportOptions |
Options forwarded to the Console transport (e.g. silent, per-transport level). The format property is managed by this library. |
consoleFormats |
Format[] |
Extra formats appended to the Console transport's format chain, before the final json/pretty step. Applies to the Console transport only. |
transports |
Transport[] |
Additional winston transports attached alongside the Console transport. |
omitPaths |
string[] |
Dot-notation paths to remove from every log entry. Applied at the logger level, so affects all transports. |
redactPaths |
string[] |
Dot-notation paths whose values are replaced with redactedValue. A plain key matches at every depth, inside serialized errors too. Applied at the logger level, so affects all transports. |
redactedValue |
string |
Replacement value used by redactPaths. Defaults to '<redacted>'. |
otel |
boolean | OtelAttributesFormatOptions |
Gives each OpenTelemetry transport on the logger, such as the one @opentelemetry/instrumentation-winston adds, the otelAttributesFormat. Other transports are unchanged. true uses the format's defaults, and an object is passed to the format as its options. See OpenTelemetry. |
errorSerializer |
ErrorSerializer |
Custom serializer applied to every Error instance at the logger level (via serializeErrorFormat) and as the Console transport's format.json replacer. Defaults to the library's serializeError, which captures name/message/stack/code/cause/errors even when non-enumerable, walks own enumerable properties, and is safe against circular references. Build one with createErrorSerializer to leave properties out of every error. |
loggerOptions |
LoggerOptions |
Winston logger options, for example level and defaultMeta. A format supplied here is appended after the library's logger-level formats. |
createLogger applies a level set where debug is noisier than verbose (unlike winston's default, which has them reversed). This matches the convention used by Seq, CloudWatch and most log aggregators, where Verbose/Trace is expected to be the lowest, noisiest level. An audit level sits between warn and info for audit-trail events that should flow through at info and above but be filtered at warn:
{ error: 0, warn: 1, audit: 2, info: 3, debug: 4, verbose: 5 }audit is for audit-trail events: a sign-in, a role change, a data export. It is a log level, not an audit store, and it adds no delivery or retention guarantee. To send audit records somewhere else as well, see step 3 of the quick start. OpenTelemetry has no audit severity, so with otel an audit record is exported at info severity with logLevel: 'audit'. See Audit severity.
Colours for the default levels (including audit) are registered on the first createLogger call that uses them, so colorize / pretty output works out of the box.
Pass your own levels via loggerOptions to override. When you do, the returned logger is typed against your level keys (so logger.audit is only present when the default levels are in use), and you should register colours for your levels via winston.addColors:
import { addColors } from 'winston'
import { createLogger } from '@makerx/node-winston'
const logger = createLogger({
loggerOptions: { levels: { fatal: 0, error: 1, info: 2, trace: 3 }, level: 'info' },
})
addColors({ fatal: 'red', error: 'red', info: 'green', trace: 'cyan' })
logger.fatal('process is exiting') // typed; logger.audit would be a type errorTo opt back into winston's stock npm levels (error/warn/info/http/verbose/debug/silly) — for example to integrate with tooling that assumes them — pass the re-exported winstonDefaultLevels. Colours register automatically on first use:
import { createLogger, winstonDefaultLevels } from '@makerx/node-winston'
const logger = createLogger({
loggerOptions: { levels: winstonDefaultLevels, level: 'silly' },
})
logger.http('GET /items') // typed; logger.audit would be a type errorcreateLogger creates a Console transport by default. Add other transports via the transports option — they share the logger-level formats:
const logger = createLogger({
transports: [
new DailyRotateFile({
level: 'info',
filename: 'application-%DATE%.log',
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
}),
],
})CallbackTransport sends records to a promise-returning handler, such as an audit table, a queue or an analytics service. It takes the handler, a LogError sink for handler failures, and options:
| Option | Description |
|---|---|
onlyLevels |
Forwards only records at these levels. |
level |
Forwards a level and every level above it. It compares the string info.level, so it stays accurate when a logger-level format such as mapAuditLevelForOtel rewrites the triple-beam LEVEL symbol. |
closeTimeoutMs |
The longest close() waits for dispatches still running. By default it waits for all of them. |
The handler receives { level, message, meta }, the shape extractTransformableInfo returns. A handler that rejects is reported to logError and never fails the log call. Give it a logError whose records this transport does not forward, or a failing endpoint produces a record that fails again. Step 3 of the quick start is a complete example.
Send logs through OpenTelemetry with @opentelemetry/instrumentation-winston and createLogger({ otel: true }), as in steps 1 and 2 of the quick start. The instrumentation adds an @opentelemetry/winston-transport transport to every logger and stamps trace and span ids on each record. otel: true gives that transport otelAttributesFormat, which shapes each record into the attributes the logs SDK and most backends need. The console and every other transport are unchanged.
Do not also add an OpenTelemetryTransportV3 yourself while the instrumentation sends logs: every record would be exported twice. otel also shapes a transport passed in transports, but a transport added later with logger.add gets no format.
@opentelemetry/winston-transport |
Matching SDK line | What the transport does |
|---|---|---|
| 0.29 – 0.31 | 0.219 – 0.221 | Records an err/error value as the record's exception; passes every other value through |
| 0.32 – 0.33 | 0.222 – 0.223 | As 0.29, and normalises object values into nested map attributes |
| 0.34 and later | 0.223 and later | As 0.32, and takes severityMapping and logSeverity options |
A transport before 0.29 is not supported: see Migrating from v2.
The transport is tied to an SDK line through its @opentelemetry/api-logs dependency, so move the two together. With two copies of api-logs in the tree, a transport constructed before the SDK registers its logger provider stays bound to a no-op logger, and every line it writes is dropped.
From 0.29 the transport gives the logs SDK an err/error value as the exception. The SDK keeps only name, message, stack and a cause's message: they become exception.type, exception.message, exception.stacktrace and error.cause.*, and every other field of the error is dropped. So otelAttributesFormat:
- leaves the error an object with only the fields the SDK reads, and moves the rest of it, such as an HTTP status, a provider's error body or the cause chain, to
errorDetailas JSON - writes every other object value as a JSON string, because most backends, Azure Log Analytics included, index only scalar attributes
- leaves scalars as they are, and writes a
Dateas its ISO string - copies every record's level onto
logLevel, and exports anauditrecord atinfoseverity (see Audit severity)
A provider error logged as logger.error('Chat failed', { error, request: { route: '/chat' } }) is exported as:
{
'exception.type': 'ProviderError',
'exception.message': 'Request too large',
'exception.stacktrace': 'ProviderError: Request too large\n at …',
'error.cause.type': 'Error',
'error.cause.message': 'body exceeds limit',
errorDetail: '{"statusCode":413,"data":{"error":{"code":"request_too_large"}},"cause":{…}}',
request: '{"route":"/chat"}',
logLevel: 'error',
}The SDK takes a truthy code over name as exception.type, so code goes to errorDetail unless the error has no name.
Pass options as otel: { … }.
| Option | Default | Description |
|---|---|---|
maxAttributeLength |
none | The longest string the format writes. A longer one is cut short and ends in [Truncated]. By default every string is written whole. Strings the record already held are never cut. |
replacer |
none | JSON.stringify replacer for every value the format stringifies, errorDetail included. It is called with the object that holds the value as this, so it can decide on a property from the other properties of that object. |
OpenTelemetry's log spec has no severity for the custom audit level. Without one, a backend stores the record with no severity or a default one, and a logSeverity filter drops it. So the format exports an audit record at info severity. A logger whose levels have no audit has nothing to rewrite.
The format also copies every record's level onto logLevel. App Insights stores only a coarse severity, which puts audit with info and debug with verbose, so logLevel is how to tell them apart. In Azure Log Analytics, query customDimensions.logLevel == "audit".
The format runs on the OpenTelemetry transport only, after that transport's level filter. A logger or transport set to level: 'audit' keeps audit records, and the console still shows "level": "audit".
Three options control what a record holds, for errors from any library:
| Option | Effect | Applies to |
|---|---|---|
redactPaths |
Replaces values on every record. A plain key matches at every depth, inside serialized errors too. | Every transport |
errorSerializer, built with createErrorSerializer |
Leaves properties out of every error with omit. With dedupe, writes an object two properties share only once. See Error serialization. |
Every transport |
maxAttributeLength |
Cuts any string the format writes at a backend's limit, and ends it in [Truncated] so the cut is visible. |
OpenTelemetry output |
The logs SDK's own attributeValueLengthLimit cuts a value without a marker, and a cut JSON value does not parse. To change only the OpenTelemetry output, use otel: { replacer }.
The Vercel AI SDK's APICallError carries the whole request body as requestBodyValues, the prompt included, and the provider's response as responseHeaders, responseBody and data. Its RetryError holds every attempt under errors, and the last attempt again under lastError. A failed retry can overrun the 8,192-character limit of an App Insights custom dimension, which App Insights then cuts short so it no longer parses as JSON.
import { createErrorSerializer, createLogger } from '@makerx/node-winston'
const logger = createLogger({
otel: { maxAttributeLength: 8192 },
redactPaths: ['requestBodyValues'],
errorSerializer: createErrorSerializer({
omit: (key, error) => key === 'responseHeaders' || (key === 'lastError' && 'errors' in error),
}),
})dedupe: true is an alternative to the lastError rule: the last attempt is written in full under errors, and lastError as '[Duplicate]'.
With no err or error on a record, the transport takes the first metadata object that has a name, code, message or stack as the record's exception. So logger.info('created', { name: 'Alice' }) is exported with exception.type: 'Alice', and a backend may file it as an exception. This is the transport's behaviour, not the format's. Log such values under a more specific key, such as userName.
Every format used by createLogger is also exported for direct use with your own winston setup.
| Format | Purpose |
|---|---|
serializeErrorFormat |
Walks the log info (including nested objects and arrays) and replaces Error instances with plain objects that include the normally non-enumerable message/stack. A record that is itself an error, or holds one under message, is nested under error. |
omitFormat |
Removes fields by dot-notation path via es-toolkit's compat omit (lodash-compatible). |
omitNilFormat |
Removes top-level null or undefined values. |
redactFormat |
Recursively replaces values at the given paths with redactedValue (default '<redacted>'). |
otelAttributesFormat |
Shapes a record into OpenTelemetry log attributes for @opentelemetry/winston-transport 0.29 and later: the error stays an object the transport records as the exception, the rest of it goes to errorDetail, and every other object value becomes a JSON string. It also writes logLevel and exports audit at info severity. otel puts it on the OpenTelemetry transport. See OpenTelemetry. |
jsonStringifyValuesFormat |
Serialises every top-level value to a JSON string, producing a flat { key: string } shape. Accepts an optional replacer. For a sink other than the OpenTelemetry transport; from transport 0.29 use otelAttributesFormat. |
prettyConsoleFormat |
Applies colorize and timestamp, then renders logs as coloured YAML using yamlify-object. |
mapAuditLevelForOtel |
Rewrites the triple-beam LEVEL symbol from audit to info and copies the original onto logLevel, so the custom audit level gets an OpenTelemetry severity. otelAttributesFormat already does this; use it in a chain you build yourself. |
redactFormat paths accept plain keys (email, matched at every level), dot-notation paths (user.email), and [*] array wildcards to iterate every element of an array segment — for example files[*].name, users[*].addresses[*].zip, or tags[*] to redact each element of a primitive array.
Direct usage example:
import { format, createLogger, transports } from 'winston'
import { redactFormat, serializeErrorFormat } from '@makerx/node-winston'
const logger = createLogger({
format: format.combine(serializeErrorFormat(), redactFormat({ paths: ['user.email', 'files[*].name'] })),
transports: [new transports.Console({ format: format.json() })],
})The Error class's message and stack properties are not enumerable, so JSON.stringify(new Error('message')) returns '{}'.
Worse, winston treats an Error differently depending on where in the call it appears, and none of the four results resemble each other:
logger.error(new Error('cause')) // the record IS the error — { ...info } yields no message, no stack
logger.error(new Error('')) // { message: <the error> } — the branch turns on a truthy message
logger.error('failed', new Error('cause')) // { message: 'failed cause', stack: '…' } — messages concatenated
logger.error('failed', { error: new Error('cause') }) // { message: 'failed', error: <the error> } — no message, no stackcreateLogger solves all of it with three complementary mechanisms:
- The logger's own level methods (and
log) normalise their arguments, so anErrorreaches winston as{ error }metadata rather than as the record or the message. This is what makes the four calls above agree; the rest is winston's ordinary metadata handling. Interpolation is the one exception — see below. serializeErrorFormatruns at the logger level and walks the log info, replacing anyErrorinstance (at any depth, the record itself included) with a plain, JSON-serializable object via the library'sserializeError. This applies to every transport, and still covers the record shapes directly — forlogger.write, winston's exception handlers, or the format used on its own outsidecreateLogger.serializableErrorReplaceris passed to the Console transport's finalformat.json()as a safety net — logform uses safe-stable-stringify, which accepts a replacer, so anyErrorthat slips through is still serialised correctly.
format.json({ replacer: serializableErrorReplacer })Error detail is always at error, and message is always the line you wrote:
logger.error(new Error('boom')) // { level, message: 'boom', error: { name, message, stack } }
logger.error('failed', new Error('boom')) // { level, message: 'failed', error: { name, message, stack } }
logger.error('failed', { error: new Error('boom') }) // { level, message: 'failed', error: { name, message, stack } }
logger.error(new Error('')) // { level, message: '', error: { name, message, stack } }
logger.error(new AggregateError([inner])) // { level, message: '', error: { name, stack, errors: [ … ] } }An error given as the whole call keeps its own message on the line, so nothing is lost; anything else you pass alongside it is kept as metadata, and a child logger behaves the same way.
Nesting rather than spreading keeps the error out of the record's namespace, which is where level, defaultMeta and your own metadata live — they overlap on name, message, stack, code, cause and errors, and something has to give. A logger with defaultMeta: { name: 'my-service' } keeps both names: name is the service, error.name is the error.
Normalising the arguments also means the error instance is never written as the record, so winston never assigns level or defaultMeta onto the error itself. That mattered more than it sounds: V8 formats stack lazily on first access, so a defaultMeta.name used to rewrite the stack's header to my-service: boom.
Interpolation is left alone. A message holding a util.format token means the arguments after it are interpolation values rather than metadata — winston merges nothing onto the record for such a call — so logger.error('failed: %s', err) keeps the error in the splat position it was passed in, as does any Error past the metadata position. Those are still serialised by serializeErrorFormat where they lie, so format.splat() finds them there.
A value that defines toJSON — a Date, a URL, a date-library instance — is left as it is in log metadata, so a transport receives the Date itself and any JSON step writes its ISO string, as 1.x did. Inside a serialized error, where the output must be plain, the value is replaced by what its toJSON returns: { at: new Date(…) } on an error becomes at: '2026-01-02T03:04:05.000Z', and an invalid Date becomes null. An error is never asked for its toJSON, so one that defines it (an AxiosError, say) is still captured whole.
Fixed in 2.2.1. From 2.0.0 to 2.2.0 a
Dateanywhere in log metadata reached every transport as{}.
Upgrading from 2.1.
logger.error(err)used to putname,messageandstackat the top of the record; error detail now sits undererrorinstead, matchinglogger.error('msg', { error }). Readerror.stackrather thanstack, and adjust anyomitPaths/redactPathsthat pointed at the old top-level keys.
logger.error('msg', err)no longer has the error's message concatenated onto yours, and no longer copiesstackto the top of the record:messageis'msg', and the detail is undererror.
Upgrading from 2.0. A record that is itself an
Errornow reaches transports as a plain object rather than anErrorinstance. A custom transport that testedinfo instanceof Errorshould read the serialised properties instead.
To plug in a custom transformation (for example, an Error-normalising function previously applied via a custom winston-transport), pass it via errorSerializer — it's threaded into both mechanisms:
import { createLogger } from '@makerx/node-winston'
const logger = createLogger({
errorSerializer: (error) => ({ kind: error.name, detail: error.message, trace: error.stack }),
})createErrorSerializer builds a serializer that works like serializeError, with two options:
| Option | Effect |
|---|---|
omit |
Leaves a property out of every error the serializer reaches: the error itself, its cause, each entry of its errors, and any error held in a property. It is called with the property name and the error that holds it, so a rule can depend on the error's other properties. An error is an Error instance, or an object with a string name, message and stack, such as an error already serialized elsewhere. It is not called for any other object. |
dedupe |
Writes an object that two properties of one error share only once, and each later occurrence as '[Duplicate]'. The first property in key order keeps the object. |
import { createErrorSerializer, createLogger } from '@makerx/node-winston'
const logger = createLogger({
errorSerializer: createErrorSerializer({ omit: (key) => key === 'headers', dedupe: true }),
})An AbortSignal carries a DOMException as its reason, and an operation cancelled through one rejects with that reason. A DOMException is therefore what a catch block receives whenever a fetch, a stream or a job is abandoned on a deadline. It needs no special treatment: log it like any other error.
try {
await fetch(url, { signal: AbortSignal.timeout(5_000) })
} catch (error) {
logger.error('delivery failed', { error }) // { error: { name: 'TimeoutError', message: ..., stack: ... } }
}A DOMException keeps message and name as getter-only accessors on its prototype, which makes it the one error type a deep clone cannot rebuild. The library substitutes a serialized object before it clones the log record, using the configured errorSerializer, so redactPaths handles a DOMException like any other value.
For direct format usage, serializeErrorFormat accepts the same override and createSerializableErrorReplacer(serializer) builds a matching JSON replacer:
import { format } from 'winston'
import { createSerializableErrorReplacer, serializeErrorFormat } from '@makerx/node-winston'
const serializer = (error: Error) => ({ kind: error.name, detail: error.message })
format.combine(serializeErrorFormat({ serializer }), format.json({ replacer: createSerializableErrorReplacer(serializer) }))v3 has one OpenTelemetry setup, shown in the quick start: WinstonInstrumentation adds the transport, and createLogger({ otel: true }) shapes what it receives. It supports @opentelemetry/winston-transport 0.29 and later only. 2.x gets fixes only, published under the latest-2 dist-tag.
Breaking changes:
flatten,flattenReplacerandmapAuditLevelForOtelare removed fromcreateLogger. They are type errors, andcreateLoggerthrows if one is passed, so config-built options cannot silently fall back to nested output. Replace them as follows:
| 2.x | 3.0 |
|---|---|
flatten: 'otel' or flatten: true, with mapAuditLevelForOtel: true |
otel: true |
flatten: 'otel' without mapAuditLevelForOtel |
otel: true. Records also get logLevel, and audit is exported at info severity. |
flattenReplacer: r |
otel: { replacer: r } |
An OpenTelemetryTransportV3 you add yourself, with otelAttributesFormat() or jsonStringifyValuesFormat(), and disableLogSending: true on WinstonInstrumentation |
Remove the transport and disableLogSending, and use otel: true. See the quick start. |
| A transport before 0.29 | Move the transport and the SDK to 0.29 or later. If you cannot move yet, stay on 2.x. |
A hand-written errorSerializer that walks serializeError's output to drop properties |
createErrorSerializer({ omit }), with the rule as the predicate. See the AI SDK example. |
@opentelemetry/winston-transportis an optional peer dependency at>=0.29. npm 7 and later refuse to install alongside an older transport, and pnpm and Yarn warn. Consumers that do not ship logs through OpenTelemetry are unaffected.otelshapes the OpenTelemetry transport only. The console and other transports keep nested values andlevel: 'audit', so a localflatten: falseis no longer needed. A logger or transport set tolevel: 'audit'is allowed.otelAttributesFormatalways writeslogLeveland exportsauditatinfoseverity. In 2.x,flatten: 'otel'did neither withoutmapAuditLevelForOtel.- A
Dateunderotelis its ISO string, unquoted.flatten: truewrote it as a quoted JSON string.
New functionality:
otelfinds the transportWinstonInstrumentationadds, so the console stays readable while OpenTelemetry receives flat attributes.otelAttributesFormattakes amaxAttributeLengthoption, beside itsreplacer.createErrorSerializerbuilds an error serializer that can leave properties out of every error, or write a shared object once.CallbackTransporttakesonlyLevels, to forward only some levels, andcloseTimeoutMs, to bound how longclose()waits.
Breaking changes:
- The
lodashdependency has been replaced withes-toolkitas a peer dependency. - Node.js
>=20is required. - Error serialisation has changed.
serializeErrornow capturesname(in addition tomessageandstack), followscausechains, walks own enumerable properties, capturesAggregateError.errors, stringifiesBigIntvalues, replacesBufferand stream values with sentinel strings, and is safe against circular references. Any consumer asserting against v1's exact{ message, stack, ...rest }shape will need to update. - Errors nested in structured metadata are now serialised on every transport, not just the Console transport. v1 only ran
serializableErrorReplacerinside the Console'sformat.json, so custom transports received the rawErrorinstance (with non-enumerablemessage/stackhidden). v2 prepends the newserializeErrorFormatat the logger level, which walks the full info tree and substitutes plain objects before any transport sees them. If a custom transport relied on receiving anErrorinstance, switch it to read the serialised shape — or passerrorSerializertocreateLogger(orserializertoserializeErrorFormatdirectly) to plug in your own transformation. omitPathsnow applies at the logger level and affects every transport, not just the Console transport. If you added custom transports expecting the un-omitted object, move omit handling into that transport's format.- A new
auditlevel sits betweenwarnandinfo. Loggers configured atlevel: 'info'(or more verbose) will now includeauditmessages; loggers atlevel: 'warn'or higher still filter them out. - Pass a custom
levelsmap vialoggerOptionsto opt out of the default level set (includingaudit); the returned logger type narrows to your keys. - Submodule deep-imports (e.g.
@makerx/node-winston/redact-format,@makerx/node-winston/serialize-error) are no longer exported. The package'sexportsfield declares a single.entry; every public format, helper and type is re-exported from the root, so import them from@makerx/node-winstondirectly.
New functionality:
- New
redactPaths/redactedValueoptions replace values at dot-notation paths with a placeholder (default'<redacted>'). LikeomitPathsthey apply at the logger level and affect every transport, so they're a drop-in replacement for any hand-rolled redaction you previously bolted ontologgerOptions.formator a custom transport. Also exported as the standaloneredactFormat, plus theredactValues/redactValuesWithhelpers for direct use. - New
flatten/flattenReplaceroptions serialise every top-level value on the log info to a JSON string, producing a flat{ key: string }shape suited to OTEL + Azure Log Analytics and other scalar-only aggregators. Also available asjsonStringifyValuesFormatandjsonStringifyValues. (The options were replaced byotelin v3.) - The new
serializeErrorFormatis exported for direct use in your own format chains, and accepts aserializeroverride for swapping in custom error normalisation. createLoggeris now generic over the level map. When you passloggerOptions.levels, the returned logger's method signatures narrow to your level keys (logger.fatal(...)becomes valid,logger.auditbecomes a type error).- Colours for the default levels (including
audit) are registered on first use of the default levels, socolorize/ pretty output works out of the box without a module-load side effect. defaultLevelsanddefaultLevelColorsare exported directly if you want to extend or re-use them.winstonDefaultLevelsandwinstonDefaultLevelColorsre-export winston's stocknpmlevel set (error/warn/info/http/verbose/debug/silly) so consumers can opt back into the standard winston levels vialoggerOptions.levels— colours register automatically on first use.- New
mapAuditLevelForOteloption (and standalone format) rewrites the triple-beamLEVELsymbol fromaudittoinfoso OTEL maps the record onto a known severity tier, while preserving the original level on alogLevelproperty. (The option was replaced byotelin v3; see Audit severity.)