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 \ winstonyarn add \ @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 \ winstonConfiguration
-
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" -
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 informationconst 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 logsconst otlpExporter = new OTLPLogExporter({url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,headers: {Authorization:process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace("Authorization=","",) || "",},});// Add log record processorloggerProvider.addLogRecordProcessor(new SimpleLogRecordProcessor(otlpExporter),);// Set global logger providerlogsAPI.logs.setGlobalLoggerProvider(loggerProvider);// Define comprehensive log formatconst 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 transportsconst 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 developmentnew 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 Last9new OpenTelemetryTransportV3({loggerProvider: loggerProvider,}),// File transport for local file logging (optional)new winston.transports.File({filename: "logs/error.log",level: "error",maxsize: 5242880, // 5MBmaxFiles: 5,handleExceptions: true,}),new winston.transports.File({filename: "logs/combined.log",maxsize: 5242880, // 5MBmaxFiles: 5,}),],exitOnError: false,});// Add request logging stream for HTTP middleware (Morgan, etc.)logger.stream = {write: function (message) {logger.info(message.trim());},};// Add correlation ID supportlogger.addCorrelationId = function (correlationId) {return logger.child({ correlationId });};// Add structured logging helperslogger.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 exceptionslogger.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 rejectionslogger.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;Create
logger.ts:import winston from "winston";import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";import { OpenTelemetryTransportV3 } from "@opentelemetry/winston-transport";import { resourceFromAttributes } from "@opentelemetry/resources";import * as logsAPI from "@opentelemetry/api-logs";import {LoggerProvider,SimpleLogRecordProcessor,} from "@opentelemetry/sdk-logs";// Uncomment for debugging OpenTelemetry issues// import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";// diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);interface LoggerStream {write: (message: string) => void;}interface ExtendedLogger extends winston.Logger {stream: LoggerStream;addCorrelationId: (correlationId: string) => winston.Logger;logError: (error: Error, context?: Record<string, any>) => void;logRequest: (req: any, res: any, duration?: number) => void;}// Initialize logger provider with service informationconst 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 logsconst otlpExporter = new OTLPLogExporter({url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,headers: {Authorization:process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace("Authorization=","",) || "",},});// Add log record processorloggerProvider.addLogRecordProcessor(new SimpleLogRecordProcessor(otlpExporter),);// Set global logger providerlogsAPI.logs.setGlobalLoggerProvider(loggerProvider);// Define comprehensive log formatconst 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 transportsconst baseLogger = 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 developmentnew 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 Last9new OpenTelemetryTransportV3({loggerProvider: loggerProvider,}),// File transport for local file logging (optional)new winston.transports.File({filename: "logs/error.log",level: "error",maxsize: 5242880, // 5MBmaxFiles: 5,handleExceptions: true,}),new winston.transports.File({filename: "logs/combined.log",maxsize: 5242880, // 5MBmaxFiles: 5,}),],exitOnError: false,});// Extend logger with additional functionalityconst logger = baseLogger as ExtendedLogger;// Add request logging stream for HTTP middleware (Morgan, etc.)logger.stream = {write: function (message: string): void {logger.info(message.trim());},};// Add correlation ID supportlogger.addCorrelationId = function (correlationId: string): winston.Logger {return logger.child({ correlationId });};// Add structured logging helperslogger.logError = function (error: Error,context: Record<string, any> = {},): void {logger.error(error.message, {error: {name: error.name,message: error.message,stack: error.stack,},...context,});};logger.logRequest = function (req: any, res: any, duration?: number): void {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 exceptionslogger.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 rejectionslogger.rejections.handle(new winston.transports.Console({format: winston.format.combine(winston.format.colorize(),winston.format.simple(),),}),new winston.transports.File({ filename: "logs/rejections.log" }),);export default logger; -
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 middlewareapp.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 requestsapp.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 loggingapp.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 operationif (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 middlewareapp.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 });});import logger from "./logger";import express, { Request, Response, NextFunction } from "express";import { randomUUID } from "crypto";interface RequestWithId extends Request {id: string;startTime: number;}const app = express();// Add request ID middlewareapp.use((req: RequestWithId, res: Response, next: NextFunction) => {req.id = (req.headers["x-request-id"] as string) || randomUUID();res.set("X-Request-ID", req.id);req.startTime = Date.now();next();});// Log all requestsapp.use((req: RequestWithId, res: Response, next: NextFunction) => {const originalEnd = res.end;res.end = function (...args: any[]) {const duration = Date.now() - req.startTime;logger.logRequest(req, res, duration);originalEnd.apply(res, args);};next();});// Sample routes with structured loggingapp.get("/", (req: RequestWithId, res: Response) => {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: RequestWithId, res: Response) => {const reqLogger = logger.addCorrelationId(req.id);const userId = req.params.id;reqLogger.info("Fetching user", { userId });try {// Simulate database operationif (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 as Error, { userId, operation: "fetch_user" });res.status(404).json({ error: (error as Error).message });}});// Error handling middlewareapp.use((error: Error, req: RequestWithId, res: Response, next: NextFunction) => {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 logsapp.use(morgan("combined", { stream: logger.stream }));
// Add structured logging middlewareapp.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 NestJSimport { 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.tsimport { 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 logsconst 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 formatconst 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 logginglogger.logPerformance = function (operation, duration, metadata = {}) { logger.info(`Performance: ${operation}`, { performance: { operation, duration_ms: duration, ...metadata, }, });};
// Usage exampleconst 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 levelsconst 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);
// Usagelogger.audit("User login", { userId: "123", ip: "192.168.1.1" });Docker Configuration
# DockerfileFROM node:18-alpine
WORKDIR /app
COPY package*.json ./RUN npm ci --production
COPY . .
# Create logs directoryRUN mkdir -p logs
# Set environment variablesENV OTEL_SERVICE_NAME=winston-logger-appENV OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpointENV OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"ENV LOG_LEVEL=info
EXPOSE 3000CMD ["node", "app.js"]Troubleshooting
Common Issues
-
No logs appearing in Last9:
- Verify OTLP endpoint and authorization header
- Check network connectivity
- Enable debug logging to see OpenTelemetry issues
-
High memory usage:
- Configure log rotation with maxsize and maxFiles
- Use appropriate log levels in production
- Consider batching log exports
-
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
- Structured Logging: Always use structured logs with meaningful fields
- Correlation IDs: Include request/correlation IDs for tracing requests
- Log Levels: Use appropriate log levels (error, warn, info, debug)
- Error Context: Include relevant context when logging errors
- Performance: Monitor logging overhead and configure appropriately
- 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.