Skip to content
Last9 named a Gartner Cool Vendor in AI for SRE Observability for 2025! Read more →
Last9

Winston

Send structured logs from Winston logger to Last9 using OpenTelemetry for comprehensive application logging

Integrate Winston logger with OpenTelemetry to send structured logs directly to Last9. This integration enables comprehensive logging for Node.js applications with automatic correlation to traces and metrics.

Prerequisites

  • Node.js 16.0 or higher
  • Winston logger in your Node.js application
  • Last9 account with OTLP endpoint configured

Installation

Install the required OpenTelemetry logging packages:

npm install \
@opentelemetry/api@1.9.0 \
@opentelemetry/resources@2.0.1 \
@opentelemetry/sdk-logs@0.201.1 \
@opentelemetry/api-logs@0.201.1 \
@opentelemetry/winston-transport@0.11.0 \
@opentelemetry/exporter-logs-otlp-http@0.201.1 \
winston

Configuration

  1. Set Environment Variables

    Configure the required environment variables for Last9 OTLP integration:

    export OTEL_SERVICE_NAME="your-service-name"
    export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"
    export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
    export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"
    export LOG_LEVEL="info"
    export NODE_ENV="production"
  2. Create Logger Configuration

    Choose between JavaScript or TypeScript setup:

    Create logger.js:

    const winston = require("winston");
    const {
    OTLPLogExporter,
    } = require("@opentelemetry/exporter-logs-otlp-http");
    const {
    OpenTelemetryTransportV3,
    } = require("@opentelemetry/winston-transport");
    const { resourceFromAttributes } = require("@opentelemetry/resources");
    const logsAPI = require("@opentelemetry/api-logs");
    const {
    LoggerProvider,
    SimpleLogRecordProcessor,
    } = require("@opentelemetry/sdk-logs");
    // Uncomment for debugging OpenTelemetry issues
    // const { diag, DiagConsoleLogger, DiagLogLevel } = require("@opentelemetry/api");
    // diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
    // Initialize logger provider with service information
    const loggerProvider = new LoggerProvider({
    resource: resourceFromAttributes({
    "service.name": process.env.OTEL_SERVICE_NAME || "nodejs-app",
    "service.version": process.env.npm_package_version || "1.0.0",
    "deployment.environment": process.env.NODE_ENV || "development",
    }),
    });
    // Configure OTLP exporter for logs
    const otlpExporter = new OTLPLogExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
    headers: {
    Authorization:
    process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace(
    "Authorization=",
    "",
    ) || "",
    },
    });
    // Add log record processor
    loggerProvider.addLogRecordProcessor(
    new SimpleLogRecordProcessor(otlpExporter),
    );
    // Set global logger provider
    logsAPI.logs.setGlobalLoggerProvider(loggerProvider);
    // Define comprehensive log format
    const logFormat = winston.format.combine(
    winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
    winston.format.errors({ stack: true }),
    winston.format.splat(),
    winston.format.metadata({
    fillExcept: ["message", "level", "timestamp"],
    }),
    winston.format.json(),
    );
    // Create Winston logger with multiple transports
    const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || "info",
    format: logFormat,
    defaultMeta: {
    service: process.env.OTEL_SERVICE_NAME || "nodejs-app",
    version: process.env.npm_package_version || "1.0.0",
    environment: process.env.NODE_ENV || "development",
    },
    transports: [
    // Console transport for local development
    new winston.transports.Console({
    format: winston.format.combine(
    winston.format.colorize({ all: true }),
    winston.format.printf(
    ({ timestamp, level, message, service, ...meta }) => {
    const metaStr = Object.keys(meta).length
    ? JSON.stringify(meta, null, 2)
    : "";
    return `${timestamp} [${service}] ${level}: ${message} ${metaStr}`;
    },
    ),
    ),
    }),
    // OpenTelemetry transport for Last9
    new OpenTelemetryTransportV3({
    loggerProvider: loggerProvider,
    }),
    // File transport for local file logging (optional)
    new winston.transports.File({
    filename: "logs/error.log",
    level: "error",
    maxsize: 5242880, // 5MB
    maxFiles: 5,
    handleExceptions: true,
    }),
    new winston.transports.File({
    filename: "logs/combined.log",
    maxsize: 5242880, // 5MB
    maxFiles: 5,
    }),
    ],
    exitOnError: false,
    });
    // Add request logging stream for HTTP middleware (Morgan, etc.)
    logger.stream = {
    write: function (message) {
    logger.info(message.trim());
    },
    };
    // Add correlation ID support
    logger.addCorrelationId = function (correlationId) {
    return logger.child({ correlationId });
    };
    // Add structured logging helpers
    logger.logError = function (error, context = {}) {
    logger.error(error.message, {
    error: {
    name: error.name,
    message: error.message,
    stack: error.stack,
    },
    ...context,
    });
    };
    logger.logRequest = function (req, res, duration) {
    const logData = {
    http: {
    method: req.method,
    url: req.url,
    status_code: res.statusCode,
    user_agent: req.get("User-Agent"),
    remote_addr: req.ip || req.connection.remoteAddress,
    duration_ms: duration,
    },
    request_id:
    req.headers["x-request-id"] || req.headers["x-correlation-id"],
    };
    if (res.statusCode >= 400) {
    logger.warn(`HTTP ${res.statusCode} ${req.method} ${req.url}`, logData);
    } else {
    logger.info(`HTTP ${res.statusCode} ${req.method} ${req.url}`, logData);
    }
    };
    // Handle uncaught exceptions
    logger.exceptions.handle(
    new winston.transports.Console({
    format: winston.format.combine(
    winston.format.colorize(),
    winston.format.simple(),
    ),
    }),
    new winston.transports.File({ filename: "logs/exceptions.log" }),
    );
    // Handle unhandled rejections
    logger.rejections.handle(
    new winston.transports.Console({
    format: winston.format.combine(
    winston.format.colorize(),
    winston.format.simple(),
    ),
    }),
    new winston.transports.File({ filename: "logs/rejections.log" }),
    );
    module.exports = logger;
  3. Use Logger in Your Application

    Import and use the logger throughout your application:

    const logger = require("./logger");
    const express = require("express");
    const app = express();
    // Add request ID middleware
    app.use((req, res, next) => {
    req.id = req.headers["x-request-id"] || require("crypto").randomUUID();
    res.set("X-Request-ID", req.id);
    req.startTime = Date.now();
    next();
    });
    // Log all requests
    app.use((req, res, next) => {
    const originalEnd = res.end;
    res.end = function (...args) {
    const duration = Date.now() - req.startTime;
    logger.logRequest(req, res, duration);
    originalEnd.apply(res, args);
    };
    next();
    });
    // Sample routes with structured logging
    app.get("/", (req, res) => {
    const reqLogger = logger.addCorrelationId(req.id);
    reqLogger.info("Processing root request", {
    user_agent: req.get("User-Agent"),
    ip: req.ip,
    });
    res.json({ message: "Hello World", requestId: req.id });
    });
    app.get("/users/:id", async (req, res) => {
    const reqLogger = logger.addCorrelationId(req.id);
    const userId = req.params.id;
    reqLogger.info("Fetching user", { userId });
    try {
    // Simulate database operation
    if (userId === "404") {
    throw new Error("User not found");
    }
    const user = { id: userId, name: `User ${userId}` };
    reqLogger.info("User fetched successfully", {
    userId,
    userName: user.name,
    });
    res.json(user);
    } catch (error) {
    reqLogger.logError(error, { userId, operation: "fetch_user" });
    res.status(404).json({ error: error.message });
    }
    });
    // Error handling middleware
    app.use((error, req, res, next) => {
    const reqLogger = logger.addCorrelationId(req.id);
    reqLogger.logError(error, {
    url: req.url,
    method: req.method,
    operation: "request_processing",
    });
    res.status(500).json({
    error: "Internal Server Error",
    requestId: req.id,
    });
    });
    const PORT = process.env.PORT || 3000;
    app.listen(PORT, () => {
    logger.info("Server started", { port: PORT });
    });

Integration with Web Frameworks

Express.js Integration

const logger = require("./logger");
const morgan = require("morgan");
// Use Morgan with Winston for access logs
app.use(morgan("combined", { stream: logger.stream }));
// Add structured logging middleware
app.use((req, res, next) => {
req.logger = logger.addCorrelationId(
req.headers["x-request-id"] || require("crypto").randomUUID(),
);
next();
});

NestJS Integration

// Create a Winston logger service for NestJS
import { Injectable, LoggerService } from "@nestjs/common";
import logger from "./logger";
@Injectable()
export class WinstonLoggerService implements LoggerService {
log(message: string, context?: string) {
logger.info(message, { context });
}
error(message: string, trace?: string, context?: string) {
logger.error(message, { trace, context });
}
warn(message: string, context?: string) {
logger.warn(message, { context });
}
debug(message: string, context?: string) {
logger.debug(message, { context });
}
verbose(message: string, context?: string) {
logger.verbose(message, { context });
}
}
// Use in main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { WinstonLoggerService } from "./winston-logger.service";
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new WinstonLoggerService(),
});
await app.listen(3000);
}
bootstrap();

Advanced Configuration

Log Correlation with Traces

const { trace } = require("@opentelemetry/api");
// Add trace correlation to logs
const addTraceContext = winston.format((info) => {
const span = trace.getActiveSpan();
if (span) {
const spanContext = span.spanContext();
info.traceId = spanContext.traceId;
info.spanId = spanContext.spanId;
}
return info;
});
// Add to logger format
const logFormat = winston.format.combine(
addTraceContext(),
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
winston.format.errors({ stack: true }),
winston.format.json(),
);

Performance Monitoring

// Add performance logging
logger.logPerformance = function (operation, duration, metadata = {}) {
logger.info(`Performance: ${operation}`, {
performance: {
operation,
duration_ms: duration,
...metadata,
},
});
};
// Usage example
const start = Date.now();
await performDatabaseQuery();
const duration = Date.now() - start;
logger.logPerformance("database_query", duration, {
query: "SELECT * FROM users",
});

Custom Log Levels

// Add custom log levels
const customLevels = {
levels: {
error: 0,
warn: 1,
info: 2,
audit: 3,
debug: 4,
trace: 5,
},
colors: {
error: "red",
warn: "yellow",
info: "green",
audit: "blue",
debug: "cyan",
trace: "magenta",
},
};
const logger = winston.createLogger({
levels: customLevels.levels,
// ... rest of configuration
});
winston.addColors(customLevels.colors);
// Usage
logger.audit("User login", { userId: "123", ip: "192.168.1.1" });

Docker Configuration

# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
# Create logs directory
RUN mkdir -p logs
# Set environment variables
ENV OTEL_SERVICE_NAME=winston-logger-app
ENV OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint
ENV OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
ENV LOG_LEVEL=info
EXPOSE 3000
CMD ["node", "app.js"]

Troubleshooting

Common Issues

  1. No logs appearing in Last9:

    • Verify OTLP endpoint and authorization header
    • Check network connectivity
    • Enable debug logging to see OpenTelemetry issues
  2. High memory usage:

    • Configure log rotation with maxsize and maxFiles
    • Use appropriate log levels in production
    • Consider batching log exports
  3. Performance impact:

    • Use async logging transports
    • Configure appropriate buffer sizes
    • Monitor logging overhead

Debug Mode

Enable OpenTelemetry debugging:

const { diag, DiagConsoleLogger, DiagLogLevel } = require("@opentelemetry/api");
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);

Log Analysis in Last9

Your structured logs will appear in Last9 with:

  • Correlation: Automatic correlation with traces and metrics
  • Structured Fields: All log metadata searchable and filterable
  • Error Tracking: Stack traces and error details
  • Performance Insights: Request/response logging with timing
  • Service Context: Service name, version, environment tagging

Best Practices

  1. Structured Logging: Always use structured logs with meaningful fields
  2. Correlation IDs: Include request/correlation IDs for tracing requests
  3. Log Levels: Use appropriate log levels (error, warn, info, debug)
  4. Error Context: Include relevant context when logging errors
  5. Performance: Monitor logging overhead and configure appropriately
  6. Sensitive Data: Never log sensitive information like passwords or API keys

Your Node.js application will now send comprehensive structured logs to Last9, providing powerful debugging and monitoring capabilities integrated with your traces and metrics.