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

Step 2: Create logger.js file

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.

logger.js
  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).

logger.js
// 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 be https://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

logger.js
// 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

logger.js
// 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.

logger.js
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.

index.js
  const logger = require('./logger')

Logging scenarios

  1. Request Logging Middleware - Track all incoming requests and their completion with detailed context:
index.js
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();
});
  1. Debug Logging - Use debug logging during development for troubleshooting:
index.js
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' });
  }
});
  1. Warning Logging - Log non-critical issues that require attention:
index.js
app.get('/404', (req, res) => {
  logger.warn({
    message: '404 Route Accessed',
    requestId: req.requestId,
  });

  res.status(404).json({
    error: 'Not Found',
    requestId: req.requestId,
  });
});
  1. Error Logging - Capture and log critical errors with full context:
index.js
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,
    });
  }
});
  1. Global Error Handler - Implement a catch-all error handler:
index.js
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',
  });
});
  1. Application Lifecycle Logging - Track application startup and shutdown events:
index.js
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
Send Nodejs winston logs to Signoz
Send Nodejs winston logs to Signoz

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.js
logger.add(new winston.transports.Console(options))

The other way is to add console transport in the list of transports while initializing the logger.

logger.js
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'],
  })
)

Was this page helpful?