Winston Logs installation

Contents

Send your existing Winston logs to PostHog with the OpenTelemetry Winston instrumentation. You don't need to change your logger calls. Requires Winston v3.

  1. Install OpenTelemetry packages

    Required
    Terminal
    npm install @opentelemetry/sdk-node @opentelemetry/exporter-logs-otlp-http @opentelemetry/sdk-logs @opentelemetry/resources @opentelemetry/instrumentation-winston @opentelemetry/winston-transport

    The instrumentation only sends logs when @opentelemetry/winston-transport is installed.

  2. Get your project token

    Required

    You'll need your PostHog project token to authenticate log requests. This is the same key you use for capturing events and exceptions with the PostHog SDK.

    Important: Use your project token which starts with phc_. Do not use a personal API key (which starts with phx_).

    You can find your project token in Project Settings.

  3. Create an instrumentation file

    Required

    Create an instrumentation.js file. It starts the OpenTelemetry SDK and tells it to send every Winston log to PostHog.

    instrumentation.js
    const { NodeSDK } = require('@opentelemetry/sdk-node');
    const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
    const { BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
    const { resourceFromAttributes } = require('@opentelemetry/resources');
    const { WinstonInstrumentation } = require('@opentelemetry/instrumentation-winston');
    const sdk = new NodeSDK({
    resource: resourceFromAttributes({
    'service.name': 'my-node-service',
    }),
    logRecordProcessors: [
    new BatchLogRecordProcessor({
    exporter: new OTLPLogExporter({
    url: 'https://us.i.posthog.com/i/v1/logs',
    headers: {
    'Authorization': 'Bearer <ph_project_token>'
    }
    })
    })
    ],
    instrumentations: [new WinstonInstrumentation()]
    });
    sdk.start();
    // Send buffered logs before the process exits
    process.on('SIGTERM', () => {
    sdk.shutdown().finally(() => process.exit(0));
    });

    If your app already handles SIGTERM (for example, to drain open requests), don't add a second handler. Export sdk and call await sdk.shutdown() at the end of your existing shutdown code instead.

  4. Load the instrumentation before your app

    Required

    The instrumentation must load before your app imports winston. Otherwise it can't patch Winston and no logs are sent. Use the --require flag when you start your app:

    Terminal
    node --require ./instrumentation.js app.js

    If you can't change the start command (for example, on a hosting platform), set NODE_OPTIONS instead:

    Terminal
    NODE_OPTIONS="--require ./instrumentation.js" npm start

    If your app uses ES modules

    --require doesn't patch import statements. Install the OpenTelemetry instrumentation package:

    Terminal
    npm install @opentelemetry/instrumentation

    Name the file instrumentation.mjs, change the require calls to import statements, and register the OpenTelemetry loader hook before you start the SDK:

    instrumentation.mjs
    import { register } from 'node:module';
    // ...the same imports as instrumentation.js
    register('@opentelemetry/instrumentation/hook.mjs', import.meta.url);
    // ...the same SDK setup as instrumentation.js

    Then start your app with --import:

    Terminal
    node --import ./instrumentation.mjs app.js
  5. Use your Winston logger

    Required

    Keep using Winston as before. Your existing transports still work, and a copy of each log goes to PostHog:

    JavaScript
    const winston = require('winston');
    const logger = winston.createLogger({
    level: 'info',
    defaultMeta: { service: 'checkout' },
    transports: [new winston.transports.Console()],
    });
    logger.info('Server started');
    logger.warn('Payment retry', { userId: 'user_123', retries: 3 });
    logger.error('Query failed', new Error('Database connection failed'));

    PostHog receives each Winston record like this:

    WinstonPostHog Logs
    MessageLog body
    Level (info, warn, error, etc.)Severity
    Metadata, defaultMeta, and child logger fieldsLog attributes
    Error objectsError message appended to the body, a stack attribute, and exception.type, exception.message, and exception.stacktrace attributes

    Logs below the logger's level are not sent. In the example above, logger.debug() calls don't reach PostHog.

    To link logs to a person and their session replay, add the posthogDistinctId and sessionId fields. A child logger adds them to every log in a request:

    JavaScript
    const requestLogger = logger.child({
    posthogDistinctId: userId,
    sessionId: sessionId,
    });
    requestLogger.info('Checkout completed');
  6. Test your setup

    Recommended

    Once everything is configured, test that logs are flowing into PostHog:

    1. Start your app with the instrumentation loaded and trigger a log
    2. Check the PostHog Logs interface for your log entries
    3. Verify the logs appear in your project
    View your logs in PostHog
  7. Next steps

    Checkpoint
    What you can do with your logs

    ActionDescription
    Why you need logsWhat logs show you that nothing else does
    Search logsUse the search interface to find specific log entries
    Filter by levelFilter by INFO, WARN, ERROR, etc.
    Link session replayConnect logs to users and session replays by passing posthogDistinctId and sessionId
    Link logs to a personSurface every log emitted on behalf of a user on their PostHog person profile
    Logging best practicesLearn what to log, how to structure logs, and patterns that make logs useful in production

    Troubleshoot common issues

Troubleshooting

Logs not appearing in PostHog

  1. Check that @opentelemetry/winston-transport is installed. Without it, the instrumentation doesn't send logs.
  2. Make sure the instrumentation file loads before winston. If your app uses ES modules, you need the --import flag and the loader hook.
  3. Check your Winston version. Log sending requires Winston v3.
  4. The SDK sends logs in batches, so a short-lived script that ends on its own loses the last batch. Add module.exports = sdk; to instrumentation.js and call await require('./instrumentation').shutdown() before the script ends.

For general Logs troubleshooting (authentication, connection issues, format problems), see the Logs troubleshooting guide.

Still have questions?

Was this page useful?