Send logs from Node.js Winston without requiring Otel collectors to SigNoz Cloud and Self-hosted
Overview
In this documentation, you'll learn how to seamlessly export logs from your Node.js application to SigNoz using the winston
logging library. Winston provides various transports that make it easy to forward logs to external systems, and SigNoz offers you a platform for log monitoring and analysis.
Setup
Step 1: Install Dependencies
npm install @opentelemetry/api-logs @opentelemetry/sdk-logs @opentelemetry/exporter-logs-otlp-http @opentelemetry/winston-transport @opentelemetry/resources winston dotenv
logger.js
file
Step 2: Create In the src
directory of your web application, create a new JavaScript file logger.js
. The logger.js
file is set up to provide the initial configuration for logging in your Node.js application. It creates a logger using winston
and configures it to send logs to SigNoz for monitoring. You can use this logger as-is or customize it further based on your project’s requirements. Once set up, you can import and use it wherever logging is needed in your application.
const logsAPI = require('@opentelemetry/api-logs')
const { LoggerProvider, SimpleLogRecordProcessor } = require('@opentelemetry/sdk-logs')
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http')
const { OpenTelemetryTransportV3 } = require('@opentelemetry/winston-transport')
const { Resource } = require('@opentelemetry/resources')
const winston = require('winston')
require('dotenv').config()
The LoggerProvider
manages logs for your application. Resource
: Adds metadata about the application, such as service.name, service.version, and the current environment (development by default).
// Initialize the Logger provider
const loggerProvider = new LoggerProvider({
resource: new Resource({
'service.name': 'winston-logger',
'service.version': '1.0.0',
'deployment.environment': process.env.NODE_ENV || 'development',
}),
})
OTLPLogExporter
defines the endpoint where logs are exported. It uses the environment variables OTEL_EXPORTER_OTLP_ENDPOINT
and SIGNOZ_INGESTION_KEY
. Create a .env
file with the environment variables needed in the following steps.
NODE_ENV=<your-environment-name> #Ex. -> dev, prod etc.
OTEL_EXPORTER_OTLP_ENDPOINT= https://ingest.<region>.signoz.cloud:443/v1/logs
SIGNOZ_INGESTION_KEY=<your-ingestion-key>
For sending logs to SigNoz cloud, while running the above example set the below environment variables
The value of
OTEL_EXPORTER_OTLP_ENDPOINT
environment variable will behttps://ingest.{region}.signoz.cloud:443/v1/logs
where depending on the choice of your region for SigNoz cloud, the otlp endpoint will vary according to this table.The value of
SIGNOZ_INGESTION_KEY
environment variable will be<SIGNOZ_INGESTION_KEY>
where<SIGNOZ_INGESTION_KEY>
is your ingestion key
// Configure OTLP exporter for SigNoz
const otlpExporter = new OTLPLogExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: {
'signoz-access-token': process.env.SIGNOZ_INGESTION_KEY,
},
})
Add Log Record Processor and Set Global Logger Provider
// Add processor with the OTLP exporter
loggerProvider.addLogRecordProcessor(new SimpleLogRecordProcessor(otlpExporter))
// Set the global logger provider
logsAPI.logs.setGlobalLoggerProvider(loggerProvider)
Create Winston logger with OpenTelemetry transports. Here we used OpenTelemetryTransportV3
, which sends logs to the global loggerProvider.
const logger = winston.createLogger({
level: 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({
stack: true
}),
winston.format.metadata(),
winston.format.json()
),
defaultMeta: {
service: 'winston-logger',
environment: process.env.NODE_ENV || 'development',
},
transports: [
new OpenTelemetryTransportV3({
loggerProvider,
logAttributes: {
'service.name': 'winston-logger',
'deployment.environment': process.env.NODE_ENV || 'development',
},
}),
],
})
module.exports = logger
Step 3: Send Node Winston logs to SigNoz
To send logs to SigNoz, import logger.js
into index.js
and save the changes.
const logger = require('./logger')
Logging scenarios
- Request Logging Middleware - Track all incoming requests and their completion with detailed context:
app.use((req, res, next) => {
const requestId = uuidv4();
req.requestId = requestId;
logger.info({
message: 'Incoming Request',
method: req.method,
path: req.path,
requestId: requestId,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
});
req.startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - req.startTime;
logger.info({
message: 'Request Processed',
method: req.method,
path: req.path,
requestId: requestId,
statusCode: res.statusCode,
responseTime: `${duration}ms`,
});
});
next();
});
- Debug Logging - Use debug logging during development for troubleshooting:
app.get('/', (req, res) => {
try {
logger.debug({
message: 'Accessing root endpoint',
requestId: req.requestId,
});
// Route logic...
} catch (error) {
logger.error({
message: 'Error in root endpoint',
error: error.message,
stack: error.stack,
requestId: req.requestId,
});
res.status(500).json({ error: 'Internal Server Error' });
}
});
- Warning Logging - Log non-critical issues that require attention:
app.get('/404', (req, res) => {
logger.warn({
message: '404 Route Accessed',
requestId: req.requestId,
});
res.status(404).json({
error: 'Not Found',
requestId: req.requestId,
});
});
- Error Logging - Capture and log critical errors with full context:
app.get('/user', (req, res) => {
try {
throw new Error('Authentication Failed');
} catch (error) {
logger.error({
message: 'User Authentication Error',
error: error.message,
stack: error.stack,
requestId: req.requestId,
});
res.status(401).json({
error: 'Unauthorized',
requestId: req.requestId,
});
}
});
- Global Error Handler - Implement a catch-all error handler:
app.use((err, req, res, next) => {
logger.error({
message: 'Unhandled Error',
error: err.message,
stack: err.stack,
requestId: req.requestId || 'unknown',
});
res.status(500).json({
error: 'Unexpected Server Error',
requestId: req.requestId || 'unknown',
});
});
- Application Lifecycle Logging - Track application startup and shutdown events:
const server = app.listen(PORT, () => {
logger.info({
message: 'Server Started',
port: PORT,
environment: process.env.NODE_ENV || 'development',
});
});
process.on('SIGTERM', () => {
logger.warn('SIGTERM received. Shutting down gracefully.');
server.close(() => {
logger.info('Server closed. Process terminating.');
process.exit(0);
});
});
Step 4: Run your application
npm start
Application is deployed on Docker or Kubernetes
When your application is deployed in Docker or a Kubernetes cluster, the logs from the console are automatically collected and stored in the node. The SigNoz collector will automatically collect the logs and they will be visible on the SigNoz UI but for that we need to add Console transports
to the logger created as shown above in logger.js
.
Console Transport Configuration
You can easily add a console transport to your Winston logger with the following code:
logger.add(new winston.transports.Console(options))
The other way is to add console transport in the list of transports while initializing the logger.
const logger = winston.createLogger({
level: 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({
stack: true
}),
winston.format.metadata(),
winston.format.json()
),
defaultMeta: {
service: 'winston-logger',
environment: process.env.NODE_ENV || 'development',
},
transports: [
new OpenTelemetryTransportV3({
loggerProvider,
logAttributes: {
'service.name': 'winston-logger',
'deployment.environment': process.env.NODE_ENV || 'development',
},
}),
// Console transport to print logs in the console
new winston.transports.Console(),
],
})
The Console transport supports several configuration options:
- level: Specifies the level of messages this transport should log (default: level set on parent logger).
- silent: A boolean flag to suppress output (default is false).
- eol: Defines the end-of-line characters to use (default is
os.EOL
). - stderrLevels: An array of log levels to be sent to stderr instead of stdout. For example:
['error', 'debug', 'info']
(default is an empty array). - consoleWarnLevels: An array of log levels to use
console.warn()
or stderr (in Node.js) instead of stdout. For example:['warn', 'debug']
(default is an empty array).
Example configuration:
logger.add(
new winston.transports.Console({
level: 'info',
format: winston.format.simple(),
silent: false,
stderrLevels: ['error'],
})
)