# Winston Logger

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

Source: https://last9.io/docs/integrations/observability/winston-logger/

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**

```bash
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
```

**yarn**

```bash
yarn 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 \
  winston
```

## Configuration

1.  **Set Environment Variables**

    Configure the required environment variables for Last9 OTLP integration:

    ```bash
    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:

**JavaScript**

    Create `logger.js`:

    ```javascript
    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;
    ```

**TypeScript**

    Create `logger.ts`:

    ```typescript
    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 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 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 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,
    });

    // Extend logger with additional functionality
    const 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 support
    logger.addCorrelationId = function (correlationId: string): winston.Logger {
      return logger.child({ correlationId });
    };

    // Add structured logging helpers
    logger.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 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" }),
    );

    export default logger;
    ```

3.  **Use Logger in Your Application**

    Import and use the logger throughout your application:

**JavaScript**

    ```javascript
    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 });
    });
    ```

**TypeScript**

    ```typescript
    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 middleware
    app.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 requests
    app.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 logging
    app.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 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 as Error, { userId, operation: "fetch_user" });
        res.status(404).json({ error: (error as Error).message });
      }
    });

    // Error handling middleware
    app.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

```javascript
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

```typescript
// 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

```javascript
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

```javascript
// 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

```javascript
// 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
# 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:

```javascript
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.
