Skip to content

Pino Logger

Xeno.JS provides a Pino integration through its application logger.

You do not replace the Xeno.JS logger with a Pino-specific API. Instead, you enable Pino as a logging provider and continue to use the Xeno.JS ILogger abstraction.

The basic configuration is:

import { AppBuilder } from '@xeno-js/core'
const app = new AppBuilder()
app.addLogger((options) => {
options.pino.config = {
destination: 'stdout',
}
})
const container = await app.build()

After the application is built, resolve TOKENS.LOGGER and use the returned logger.


Install Xeno.JS and Pino:

Terminal window
npm install @xeno-js/core @xeno-js/shared pino

pino is an optional peer dependency of @xeno-js/core.

If you enable pretty printing, Xeno.JS configures Pino to use the pino-pretty transport. Install it as well:

Terminal window
npm install pino-pretty

You only need pino-pretty when pretty printing is enabled.


Pino is enabled when options.pino.config is defined.

For example:

import { AppBuilder } from '@xeno-js/core'
const app = new AppBuilder()
app.addLogger((options) => {
options.pino.config = {}
})
const container = await app.build()

The empty configuration uses the Xeno.JS defaults.

Pino is not enabled by merely installing the pino package. It becomes a Xeno.JS logging provider when pino.config is configured.


The minimum log level is configured through the Xeno.JS logger configuration:

import { AppBuilder } from '@xeno-js/core'
import { LOG_LEVEL } from '@xeno-js/shared'
const app = new AppBuilder()
app.addLogger((options) => {
options.level = LOG_LEVEL.INFO
options.pino.config = {
destination: 'stdout',
}
})

Xeno.JS supports:

  • LOG_LEVEL.DEBUG
  • LOG_LEVEL.INFO
  • LOG_LEVEL.WARN
  • LOG_LEVEL.ERROR

The configured level is the minimum level accepted by the Xeno.JS logger.

For example:

options.level = LOG_LEVEL.WARN

allows:

WARN
ERROR

and filters:

DEBUG
INFO

The Pino adapter applies the same minimum level before calling the corresponding Pino method.


Xeno.JS supports two Pino destinations:

type Destination = 'stdout' | 'file'

This is the default:

app.addLogger((options) => {
options.pino.config = {
destination: 'stdout',
}
})

If destination is omitted, Xeno.JS uses stdout.

Configure:

app.addLogger((options) => {
options.pino.config = {
destination: 'file',
filePath: 'logs/app.log',
}
})

If destination is file and filePath is omitted, Xeno.JS uses:

logs/app.log

The file path is passed to Pino’s destination stream.

Make sure the application process has permission to create and write to the configured path.


Pino has an optional env configuration:

app.addLogger((options) => {
options.pino.config = {
env: 'production',
destination: 'stdout',
}
})

If env is not specified, Xeno.JS uses:

  1. process.env.NODE_ENV, when defined;
  2. otherwise development.

The environment affects the default value of prettyPrint.


Xeno.JS enables pretty printing by default when the configured environment is development.

For example:

app.addLogger((options) => {
options.pino.config = {
env: 'development',
}
})

is equivalent to using:

app.addLogger((options) => {
options.pino.config = {
env: 'development',
prettyPrint: true,
}
})

To explicitly disable it:

app.addLogger((options) => {
options.pino.config = {
env: 'development',
prettyPrint: false,
}
})

To explicitly enable it:

app.addLogger((options) => {
options.pino.config = {
env: 'production',
prettyPrint: true,
}
})

When pretty printing is enabled, Xeno.JS configures the Pino pino-pretty transport with:

  • colorized output;
  • standard translated timestamps;
  • pid and hostname omitted from the pretty output.

If you enable pretty printing, make sure pino-pretty is installed.


Pino is not exposed as a separate DI service token.

Xeno.JS registers the application logger under TOKENS.LOGGER.

Resolve it after building the application:

import { AppBuilder } from '@xeno-js/core'
import { TOKENS } from '@xeno-js/shared'
const app = new AppBuilder()
app.addLogger((options) => {
options.pino.config = {
destination: 'stdout',
}
})
const container = await app.build()
const logger = container.resolve(TOKENS.LOGGER)

The returned value implements the Xeno.JS ILogger interface.

Use the normal Xeno.JS logging methods:

logger.debug('Loading configuration')
logger.info('Application started')
logger.warn('Cache entry was not available')
logger.error('Unable to process request', error)

You do not need to call Pino’s debug(), info(), warn(), or error() methods directly in application code.

Xeno.JS forwards each accepted log event to the configured Pino provider.


Pino does not have to be the only provider.

For example, you can enable Console and Pino together:

import { AppBuilder } from '@xeno-js/core'
const app = new AppBuilder()
app.addLogger((options) => {
options.console = true
options.pino.config = {
destination: 'stdout',
}
})

You can also configure Pino and Sentry:

app.addLogger((options) => {
options.console = false
options.pino.config = {
destination: 'stdout',
}
options.sentry.config = {
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
}
})

The Xeno.JS logger sends accepted events to all configured logger clients.

This allows the same application logging API to feed different observability systems.


Xeno.JS passes the log context to Pino as structured data.

The Pino adapter maps Xeno.JS levels to Pino methods:

Xeno.JS level Pino method
DEBUG debug()
INFO info()
WARN warn()
ERROR error()

For example, an informational event is passed to Pino conceptually as:

pinoLogger.info(
{
...context,
},
message,
)

An error event includes the error in the structured payload:

pinoLogger.error(
{
...context,
error,
},
message,
)

Pino is configured by Xeno.JS with ISO timestamps and its standard error serializer.

A structured log therefore contains information such as:

{
"level": 30,
"time": "2026-10-03T16:00:00.000Z",
"msg": "[INFO] Application started",
"requestId": "...",
"correlationId": "..."
}

The exact JSON representation is produced by Pino.


Xeno.JS does not pass the complete request context directly to Pino.

The Xeno.JS logger builds a safe logging context before sending it to logger clients.

Depending on the active request context, this can contain information such as:

identity
├── userId
└── tenantId
network
├── requestId
├── path
├── origin
└── masked clientIp
tracing
├── correlationId
├── spanId
├── startTime
└── parentSpanId

The purpose is to provide useful correlation and diagnostic information without serializing the complete request context.

For example, request-specific information such as authentication state, tracing identifiers, and the request path can be available to Pino without automatically copying arbitrary request data.

The client IP is masked before being included in the safe logging context.

Application Data Is Still Your Responsibility

Section titled “Application Data Is Still Your Responsibility”

Safe context does not make arbitrary application logging safe.

If application code explicitly logs:

logger.info(`Password: ${password}`)

the password is part of the message.

Likewise, application-provided structured data or error objects can contain sensitive information.

Avoid logging:

  • passwords;
  • authentication tokens;
  • secrets;
  • credentials;
  • cookies;
  • authorization headers;
  • complete request bodies;
  • other sensitive application data.

Xeno.JS configures Pino redaction for the following paths:

password
token
secret
authorization
headers.authorization

These values are censored as:

***

For example, a payload containing:

{
token: 'secret-token',
userId: 'user-123',
}

is redacted by Pino before output.

Redaction is an additional protection layer. It does not replace careful logging practices.

Do not deliberately include sensitive information in log messages simply because Pino has redaction configured.


Use the Xeno.JS error API:

try {
await processOrder()
} catch (error) {
logger.error('Unable to process order', error)
}

Xeno.JS passes the error to the Pino adapter.

The adapter adds the error to the structured payload and calls Pino’s error() method.

Pino is configured with its standard error serializer, so error information is serialized using Pino’s error serialization behavior.


A complete application configuration can look like this:

import { AppBuilder } from '@xeno-js/core'
import { LOG_LEVEL, TOKENS } from '@xeno-js/shared'
const app = new AppBuilder()
app.addLogger((options, config) => {
options.level = LOG_LEVEL.INFO
options.console = false
options.pino.config = {
destination: 'file',
filePath: config.get('LOG_FILE_PATH', 'logs/app.log'),
env: 'production',
prettyPrint: false,
}
})
const container = await app.build()
const logger = container.resolve(TOKENS.LOGGER)
logger.info('Application started')
logger.warn('Example warning')
try {
throw new Error('Example failure')
} catch (error) {
logger.error('Application operation failed', error)
}

This configuration:

  • filters out DEBUG;
  • accepts INFO, WARN, and ERROR;
  • disables Console;
  • writes Pino logs to logs/app.log;
  • uses structured JSON output;
  • uses the Xeno.JS safe context;
  • applies Pino redaction;
  • exposes the logger through TOKENS.LOGGER.

For local development, pretty printing can make Pino output easier to read:

import { AppBuilder } from '@xeno-js/core'
import { LOG_LEVEL } from '@xeno-js/shared'
const app = new AppBuilder()
app.addLogger((options) => {
options.level = LOG_LEVEL.DEBUG
options.pino.config = {
env: 'development',
destination: 'stdout',
prettyPrint: true,
}
})
await app.build()

Make sure pino-pretty is installed when using this configuration.


For structured logs, disable pretty printing:

import { AppBuilder } from '@xeno-js/core'
import { LOG_LEVEL } from '@xeno-js/shared'
const app = new AppBuilder()
app.addLogger((options) => {
options.level = LOG_LEVEL.INFO
options.pino.config = {
env: 'production',
destination: 'stdout',
prettyPrint: false,
}
})
await app.build()

This keeps the output in Pino’s structured format, which can then be consumed by the application’s log collection infrastructure.


Make sure pino.config is defined:

app.addLogger((options) => {
options.pino.config = {}
})

Installing Pino alone does not enable the Xeno.JS Pino provider.

Also check the configured minimum level:

options.level = LOG_LEVEL.INFO

A DEBUG event will not be emitted with that configuration.

Make sure the provider dependency is installed:

Terminal window
npm install pino

pino is an optional peer dependency of @xeno-js/core.

If prettyPrint is enabled, install:

Terminal window
npm install pino-pretty

Xeno.JS configures the Pino transport with pino-pretty when pretty printing is enabled.

Check:

options.pino.config = {
destination: 'file',
filePath: 'logs/app.log',
}

If filePath is omitted, Xeno.JS uses:

logs/app.log

Also make sure the Node.js process has permission to write to the selected directory.

This is expected.

Xeno.JS registers the application ILogger, not the Pino instance, under the DI token:

TOKENS.LOGGER

Resolve:

const logger = container.resolve(TOKENS.LOGGER)

and use the Xeno.JS logging API.

Pino remains an implementation behind the Xeno.JS logger abstraction.



Xeno.JS is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section