> ## Documentation Index
> Fetch the complete documentation index at: https://docs.westyx.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Node.js SDK - Logging

> The NexusLogger interface, why values travel as fields, and adapters for pino, winston and console.

*Changed in v0.11.0 - the logger interface is structured and has four levels.*

The SDK writes its diagnostics to whatever you pass as `NexusConfig.logger`:

```ts theme={null}
interface NexusLogger {
  debug(message: string, fields?: Record<string, unknown>): void;
  info(message: string, fields?: Record<string, unknown>): void;
  warn(message: string, fields?: Record<string, unknown>): void;
  error(message: string, fields?: Record<string, unknown>): void;
}
```

```ts theme={null}
const client = await NexusClient.create({ endpoint, apiKey, logger: myLogger });
```

## Values are fields, not text

The message is a stable literal and every value travels in `fields`:

```
debug  nexus: sync applied        { component: 'client', configs: 12, flags: 4, fileSecrets: 1 }
warn   nexus: stream idle deadline exceeded, reconnecting  { component: 'stream', idleTimeoutMs: 90000 }
error  nexus: background sync failed  { component: 'client', err: Error }
```

This is what makes the output usable in an aggregator. A message that interpolates its values produces a unique string per occurrence, so nothing can group them, count them, or alert on them - you can only grep. With fields, "how many syncs failed in the last hour" and "show me every record where `component` is `stream`" are queries.

Every record carries a **`component`** field naming the subsystem that produced it: `client`, `stream`, or `wif`.

## Adapters

pino - note the argument order is `(fields, message)`:

```ts theme={null}
import pino from 'pino';
const log = pino();

const logger = {
  debug: (msg, fields) => log.debug(fields ?? {}, msg),
  info: (msg, fields) => log.info(fields ?? {}, msg),
  warn: (msg, fields) => log.warn(fields ?? {}, msg),
  error: (msg, fields) => log.error(fields ?? {}, msg),
};
```

winston:

```ts theme={null}
const logger = {
  debug: (msg, fields) => winstonLogger.debug(msg, fields),
  info: (msg, fields) => winstonLogger.info(msg, fields),
  warn: (msg, fields) => winstonLogger.warn(msg, fields),
  error: (msg, fields) => winstonLogger.error(msg, fields),
};
```

`console`, for development:

```ts theme={null}
const logger = {
  debug: (msg, fields) => console.debug(msg, fields ?? {}),
  info: (msg, fields) => console.info(msg, fields ?? {}),
  warn: (msg, fields) => console.warn(msg, fields ?? {}),
  error: (msg, fields) => console.error(msg, fields ?? {}),
};
```

In NestJS, [the module](/sdks/nodejs/nestjs) bridges Nest's own `Logger` in for you - there is nothing to configure.

### Why the SDK owns the interface

Node has no `log/slog` in its standard library, and the two most widely used loggers disagree on argument order: pino takes `(fields, message)`, winston takes `(message, meta)`. Adopting either signature would force that library's shape on every consumer. The SDK therefore defines four methods, depends on nothing, and documents the three-line adapter.

## When you pass nothing

`debug` and `info` are discarded - a library must not write to your output uninvited - while `warn` and `error` become Node process warnings:

```
(node:12345) NexusSDK: nexus: background sync failed component=client err=Error: connect ECONNREFUSED
```

So a failing background refresh is still discoverable with nothing configured. Pass `NOOP_LOGGER` to silence even that.

Newlines in a rendered value are collapsed to spaces, so a multi-line value cannot forge additional log records.

## Secrets never reach the logger

Not their values, and not their key names - a key names what its value is for, which is why v0.5.1 hashed it out of the filesystem path too.

This is enforced rather than intended: a test drives a sync carrying both a text secret and a file-type secret through a capturing logger at debug level, and fails if either value, or either key, appears anywhere in the output. Records about secrets report counts (`fileSecrets: 1`), never identities.

## What is logged where

| Level   | Examples                                                                                                                                       |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `debug` | sync applied (with counts), stream connected, stream event, WIF session refreshed, connect ignored                                             |
| `warn`  | stream idle deadline exceeded, falling back to polling, service quarantined, billing overdue, a suspicious `streamIdleTimeout` at construction |
| `error` | background sync failed, an observer callback threw, materialising a file secret failed, pre-emptive WIF refresh failed                         |

`info` is unused by the SDK today; it exists so an adapter does not have to guess.

## Next

* [Error handling](/sdks/nodejs/error-handling)
* [Stream observer](/sdks/nodejs/stream-observer) - the structured counterpart, for metrics rather than text
