next.jsloggingazure-application-insightsopen-telemetrywinston

How to send logs from NextJS to Azure Application Insights using OpenTelemetry and Winston?


I am using winston to log throughout my NextJS application. I want these logs to be collected in Azure Application Insights. Microsoft advises to do so using OpenTelemetry. My setup is as follows:

instrumentation.ts:

import { env } from "~/env";
import { logs } from "@opentelemetry/api-logs";
import { LoggerProvider, SimpleLogRecordProcessor, ConsoleLogRecordExporter } from "@opentelemetry/sdk-logs";
import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { Resource } from "@opentelemetry/resources";
import { registerOTel } from "@vercel/otel";
import { WinstonInstrumentation } from "@opentelemetry/instrumentation-winston";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { registerInstrumentations } from "@opentelemetry/instrumentation";

export async function register() {
    if (
        process.env.NEXT_RUNTIME == "nodejs" &&
        env.APPLICATIONINSIGHTS_CONNECTION_STRING != null
    ) {
        console.log("Registering Application Insights");
        const { AzureMonitorTraceExporter, AzureMonitorLogExporter } =
            await import("@azure/monitor-opentelemetry-exporter");

        const tracerProvider = new NodeTracerProvider({
            resource: new Resource({
                [ATTR_SERVICE_NAME]: env.APPLICATIONINSIGHTS_SERVICE_NAME,
            }),
        });
        tracerProvider.register();
        const traceExporter = new AzureMonitorTraceExporter({
            connectionString: env.APPLICATIONINSIGHTS_CONNECTION_STRING,
        });
        tracerProvider.addSpanProcessor(
            new BatchSpanProcessor(traceExporter, {
                maxQueueSize: 10,
                scheduledDelayMillis: 5000,
            }),
        );
        const logExporter = new AzureMonitorLogExporter({
            connectionString: env.APPLICATIONINSIGHTS_CONNECTION_STRING,
        });
        // Set up OpenTelemetry logging provider
        const loggerProvider = new LoggerProvider();

        // Add a log processor that will export the logs
        loggerProvider.addLogRecordProcessor(
            new SimpleLogRecordProcessor(logExporter),
        );
        // Add a processor to export log record
        loggerProvider.addLogRecordProcessor(
            new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()),
        );
        logs.setGlobalLoggerProvider(loggerProvider);

        const instrumentation = new WinstonInstrumentation({
            disableLogSending: false,
            enabled: true,
            disableLogCorrelation: false,
        });
        registerOTel({
            serviceName: env.APPLICATIONINSIGHTS_SERVICE_NAME,
            traceExporter,
            instrumentations: [instrumentation],
        });
        registerInstrumentations({
            instrumentations: [instrumentation],
        });
    } else {

    }
}

My winston setup is as follows:

import { env } from "~/env";
import winston, { type Logger } from "winston";
import { OpenTelemetryTransportV3 } from "@opentelemetry/winston-transport";

// Create a logger with custom format
const logger: Logger = winston.createLogger({
    transports: [
        new winston.transports.Console({
            level: env.NODE_ENV == "test" ? "error" : "debug",
            format: winston.format.simple(),
        }),
        new OpenTelemetryTransportV3(),
    ],
});
export default logger;

The application does show that the instrumentation code is being executed, and I do get request time information in Application Insights, but the traces table in Application Insights remains empty. What am I doing wrong?


Solution

  • I found out what the correct configuration is for using NextJS with Winston and Azure Application Insights through OpenTelemetry. Here is my instrumentation.ts

    import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
    import { Resource } from "@opentelemetry/resources";
    import {
        ConsoleSpanExporter,
        SimpleSpanProcessor,
    } from "@opentelemetry/sdk-trace-base";
    import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
    import { WinstonInstrumentation } from "@opentelemetry/instrumentation-winston";
    import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
    import { env } from "~/env";
    import { registerOTel } from "@vercel/otel";
    
    // Prevent registering the SDK multiple times
    let started = false;
    
    export async function register() {
        if (
            !started &&
            process.env.NEXT_RUNTIME == "nodejs" &&
            env.APPLICATIONINSIGHTS_CONNECTION_STRING != null
        ) {
            started = true;
            console.log("Registering Azure Application Insights");
    
            // Importing the Azure Monitor cannot be done when in Edge environment
            const { AzureMonitorLogExporter, AzureMonitorTraceExporter } =
                await import("@azure/monitor-opentelemetry-exporter");
    
            // Configure OpenTelemetry tracing provider
            const provider = new NodeTracerProvider({
                resource: new Resource({
                    [ATTR_SERVICE_NAME]: env.APPLICATIONINSIGHTS_SERVICE_NAME,
                }),
            });
    
            // Configure log provider
            const params = {
                connectionString: env.APPLICATIONINSIGHTS_CONNECTION_STRING,
            };
            const traceExporter = new AzureMonitorTraceExporter(params);
            const logExporter = new AzureMonitorLogExporter(params);
    
            // // Configure span processors
            provider.addSpanProcessor(new SimpleSpanProcessor(traceExporter));
            provider.addSpanProcessor(
                new SimpleSpanProcessor(new ConsoleSpanExporter()),
            );
    
            // Register the OpenTelemetry provider
            registerOTel({
                serviceName: env.APPLICATIONINSIGHTS_SERVICE_NAME,
                traceExporter,
                logRecordProcessor: new BatchLogRecordProcessor(logExporter, {
                    exportTimeoutMillis: 1000,
                    maxExportBatchSize: 100,
                }),
                instrumentations: [
                    new WinstonInstrumentation({
                        disableLogSending: false,
                        disableLogCorrelation: false,
                        enabled: true,
                    }),
                ],
            });
        }
    }
    

    And logger.ts which is used to log by the rest of the code:

    import { env } from "~/env";
    
    // For if not being run through 'next start' or 'next dev' 
    import { register } from "~/instrumentation";
    await register();
    
    import { OpenTelemetryTransportV3 } from "@opentelemetry/winston-transport";
    import winston, { type Logger } from "winston";
    import { context, trace } from "@opentelemetry/api";
    
    // Create a logger with custom format
    const logger: Logger = winston.createLogger({
        format: winston.format.combine(
            winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
            winston.format.printf(({ level, message, timestamp }) => {
                // Retrieve the current span from OpenTelemetry context
                const span = trace.getSpan(context.active());
                const traceId = span ? span.spanContext().traceId : "unknown";
                return `${timestamp} [traceId=${traceId}] ${level}: ${message}`;
            }),
        ),
        transports: [
            new winston.transports.Console({
                level: env.NODE_ENV == "test" ? "error" : "debug",
                format: winston.format.simple(),
            }),
            new OpenTelemetryTransportV3({}),
        ],
    });
    export default logger;