This document shows you how to send logs from your Node.js application using the Winston logging library to SigNoz. The setup automatically correlates your logs with traces when available, providing unified observability in SigNoz.
Prerequisites
- Existing Node.js application using Winston for logging
Setup
Step 1: Install Required Packages
Install the OpenTelemetry packages needed for auto-instrumentation:
npm install @opentelemetry/api-logs@^0.56.0 \
@opentelemetry/auto-instrumentations-node@^0.65.0 \
@opentelemetry/exporter-logs-otlp-http@^0.56.0 \
@opentelemetry/exporter-trace-otlp-http@^0.56.0 \
@opentelemetry/instrumentation-winston@^0.51.0 \
@opentelemetry/resources@^1.29.0 \
@opentelemetry/sdk-logs@^0.56.0 \
@opentelemetry/sdk-node@^0.56.0 \
@opentelemetry/semantic-conventions@^1.29.0 \
@opentelemetry/winston-transport@^0.17.0 \
The Resource class from @opentelemetry/resources@2.1.0 is incompatible with the v0.206.x series of sdk-node and related packages. The versions above ensure everything aligns correctly.
Package Breakdown
@opentelemetry/sdk-node
- Core OpenTelemetry SDK for Node.js runtime@opentelemetry/auto-instrumentations-node
- Automatic instrumentation for popular libraries (HTTP, Express, etc.)@opentelemetry/instrumentation-winston
- Specific instrumentation for Winston logging library@opentelemetry/exporter-trace-otlp-http
- OTLP HTTP exporter for traces@opentelemetry/exporter-logs-otlp-http
- OTLP HTTP exporter for logs@opentelemetry/winston-transport
- Enables automatic log forwarding to OpenTelemetry Logs SDK
Step 2: Create OpenTelemetry Configuration
Create an OpenTelemetry configuration file at your project root:
// otel.js
'use strict';
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { WinstonInstrumentation } = require('@opentelemetry/instrumentation-winston');
// traces
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
// logs
const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
const logsAPI = require('@opentelemetry/api-logs');
// resource
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
// describe the service
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'winston-app',
});
// logs pipeline via LoggerProvider
const loggerProvider = new LoggerProvider({
resource,
processors: [new BatchLogRecordProcessor(new OTLPLogExporter())],
});
logsAPI.logs.setGlobalLoggerProvider(loggerProvider);
// traces via NodeSDK
const spanProcessors = [new BatchSpanProcessor(new OTLPTraceExporter())];
const sdk = new NodeSDK({
resource,
spanProcessors,
instrumentations: [
getNodeAutoInstrumentations(), // Creates spans for HTTP, Express, and more
new WinstonInstrumentation({
disableLogCorrelation: false, // inject trace_id, span_id, trace_flags
disableLogSending: false, // forward logs to OpenTelemetry Logs SDK
}),
],
});
sdk.start();
// Optional graceful shutdown
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0));
});
// otel.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston';
// traces
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
// logs
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import * as logsAPI from '@opentelemetry/api-logs';
// resource
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
// describe the service
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'winston-app',
});
// logs pipeline via LoggerProvider
const loggerProvider = new LoggerProvider({
resource,
processors: [new BatchLogRecordProcessor(new OTLPLogExporter())],
});
logsAPI.logs.setGlobalLoggerProvider(loggerProvider);
// traces via NodeSDK
const spanProcessors = [new BatchSpanProcessor(new OTLPTraceExporter())];
const sdk = new NodeSDK({
resource,
spanProcessors,
instrumentations: [
getNodeAutoInstrumentations(), // Creates spans for HTTP, Express, and more
new WinstonInstrumentation({
disableLogCorrelation: false, // inject trace_id, span_id, trace_flags
disableLogSending: false, // forward logs to OpenTelemetry Logs SDK
// Optional: customize injected keys
// logKeys: { traceId: 'trace_id', spanId: 'span_id', traceFlags: 'trace_flags' }
}),
],
});
sdk.start();
// Optional graceful shutdown
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0));
});
Configuration Details
- Batch processors are used for production efficiency, batching spans and logs before export
- Auto-instrumentations automatically create spans for HTTP requests, Express routes, and other common libraries
- Winston instrumentation injects trace context into your logs and forwards log records to the OpenTelemetry Logs SDK
- LoggerProvider must be set up globally for Winston log forwarding to work
- The SDK must be started before your application imports Winston
Step 3: Create Winston Logger Configuration
Create a Winston logger configuration file that your application will use:
// logger.js
'use strict';
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
// simple format to keep output readable in terminal
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(info => {
const { timestamp, level, message, ...meta } = info;
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
return `${timestamp} ${level}: ${message}${metaStr}`;
})
),
transports: [new winston.transports.Console()],
defaultMeta: { service: 'winston-express-demo' } // adds to every line
});
module.exports = logger;
// logger.ts
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
// simple format to keep output readable in terminal
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(info => {
const { timestamp, level, message, ...meta } = info;
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
return `${timestamp} ${level}: ${message}${metaStr}`;
})
),
transports: [new winston.transports.Console()],
defaultMeta: { service: 'winston-express-demo' } // adds to every line
});
export default logger;
Logger Configuration Details
- Level: Controls which log levels are output (debug, info, warn, error)
- Format: Defines how log messages are formatted for readability
- Transports: Specifies where logs are sent (console, file, etc.)
- DefaultMeta: Adds metadata to every log entry
- OpenTelemetry Integration: The Winston instrumentation automatically injects trace context and forwards logs
Using the Logger in Your Application
Import and use the logger in your application code:
// Import the logger in your application main file (Ex: app.js)
const logger = require('./logger');
// Example usage in your application
app.get('/', (req, res) => {
logger.info('Handling request to root endpoint');
res.json({ message: 'Hello World' });
});
app.get('/error', (req, res) => {
logger.error('Simulated error occurred', {
userId: 123,
endpoint: '/error'
});
res.status(500).json({ error: 'Something went wrong' });
});
// Import the logger in your application main file (Ex: app.js)
import logger from './logger';
// Example usage in your application
app.get('/', (req, res) => {
logger.info('Handling request to root endpoint');
res.json({ message: 'Hello World' });
});
app.get('/error', (req, res) => {
logger.error('Simulated error occurred', {
userId: 123,
endpoint: '/error'
});
res.status(500).json({ error: 'Something went wrong' });
});
Step 4: Configure and Run Your Application
Set Environment Variables
export OTEL_SERVICE_NAME=<service_name>
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443"
export OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>"
- Set the
<region>
to match your SigNoz Cloud region - Replace
<your-ingestion-key>
with your SigNoz ingestion key <service_name>
is name of your service
export OTEL_SERVICE_NAME=<service_name>
export OTEL_EXPORTER_OTLP_ENDPOINT="http://<collector-host>:4318"
- Replace
<collector-host>
with your OpenTelemetry collector host <service_name>
is name of your service
Start Your Application
node -r ./otel.js app.js
- Set the
<region>
to match your SigNoz Cloud region - Replace
<your-ingestion-key>
with your SigNoz ingestion key <service_name>
is name of your service
node -r ./otel.ts app.ts
- Set the
<region>
to match your SigNoz Cloud region - Replace
<your-ingestion-key>
with your SigNoz ingestion key <service_name>
is name of your service
The -r
flag ensures the OpenTelemetry SDK loads and initializes before your application imports Winston.
Verification
After starting your application:
- Make some requests to your application endpoints
- Check SigNoz for incoming logs
- Verify correlation - your logs should contain trace IDs and be correlated with spans if available
