Pino Logs installation

Contents

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

  1. Install OpenTelemetry packages

    Required
    Terminal
    npm install @opentelemetry/sdk-node @opentelemetry/exporter-logs-otlp-http @opentelemetry/sdk-logs @opentelemetry/resources @opentelemetry/instrumentation-pino
  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 Pino 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 { PinoInstrumentation } = require('@opentelemetry/instrumentation-pino');
    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 PinoInstrumentation()]
    });
    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 pino. Otherwise it can't patch Pino 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 Pino logger

    Required

    Keep using Pino as before. Your logs still go to their usual destination, and a copy goes to PostHog:

    JavaScript
    const pino = require('pino');
    const logger = pino();
    logger.info('Server started');
    logger.warn({ userId: 'user_123', retries: 3 }, 'Payment retry');
    logger.error(new Error('Database connection failed'), 'Query failed');

    PostHog receives each Pino record like this:

    PinoPostHog Logs
    Message (msg)Log body
    Level (info, warn, error, etc.)Severity
    Object fields and child logger bindingsLog attributes
    Error objectsexception.type, exception.message, and exception.stacktrace attributes

    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. Make sure the instrumentation file loads before pino. If your app uses ES modules, you need the --import flag and the loader hook.
  2. Check your Pino version. Log sending requires Pino v7 to v10.
  3. 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?