> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt # Pino Logs installation Canonical URL: https://posthog.com/docs/logs/installation/pino > **Cloud region:** This page uses US Cloud hosts. For an EU Cloud project, use `https://eu.i.posthog.com` instead of `https://us.i.posthog.com` (ingestion host), and use `https://eu.posthog.com` instead of `https://app.posthog.com` (app host for links). ## On this page - [Install OpenTelemetry packages](#install-opentelemetry-packages) - [Get your project token](#get-your-project-token) - [Create an instrumentation file](#create-an-instrumentation-file) - [Load the instrumentation before your app](#load-the-instrumentation-before-your-app) - [Use your Pino logger](#use-your-pino-logger) - [Test your setup](#test-your-setup) - [Next steps](#next-steps) - [Troubleshooting](#troubleshooting) Send your existing [Pino](https://getpino.io) logs to PostHog with the OpenTelemetry Pino instrumentation. You don't need to change your logger calls. Requires Pino v7 to v10. 1. 1 ## Install OpenTelemetry packages Required ```bash npm install @opentelemetry/sdk-node @opentelemetry/exporter-logs-otlp-http @opentelemetry/sdk-logs @opentelemetry/resources @opentelemetry/instrumentation-pino ``` 2. 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](https://app.posthog.com/settings). 3. 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 ```javascript 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 ' } }) }) ], 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. 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: ```bash node --require ./instrumentation.js app.js ``` If you can't change the start command (for example, on a hosting platform), set `NODE_OPTIONS` instead: ```bash NODE_OPTIONS="--require ./instrumentation.js" npm start ``` **If your app uses ES modules** `--require` doesn't patch `import` statements. Install the OpenTelemetry instrumentation package: ```bash 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 ```javascript 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`: ```bash node --import ./instrumentation.mjs app.js ``` 5. 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: | Pino | PostHog Logs | | --- | --- | | Message (`msg`) | Log body | | Level (`info`, `warn`, `error`, etc.) | Severity | | Object fields and child logger bindings | Log attributes | | `Error` objects | `exception.type`, `exception.message`, and `exception.stacktrace` attributes | To [link logs to a person and their session replay](https://posthog.com/docs/logs/link-session-replay.md), 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. 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](https://app.posthog.com/logs) 8. ## Next steps Checkpoint *What you can do with your logs* | Action | Description | | --- | --- | | **[Why you need logs](https://posthog.com/docs/logs/basics.md)** | What logs show you that nothing else does | | **[Search logs](https://posthog.com/docs/logs/search.md)** | Use the search interface to find specific log entries | | **Filter by level** | Filter by `INFO`, `WARN`, `ERROR`, etc. | | **[Link session replay](https://posthog.com/docs/logs/link-session-replay.md)** | Connect logs to users and session replays by passing `posthogDistinctId` and `sessionId` | | **[Link logs to a person](https://posthog.com/docs/logs/link-person.md)** | Surface every log emitted on behalf of a user on their PostHog person profile | | **[Logging best practices](https://posthog.com/docs/logs/best-practices.md)** | Learn what to log, how to structure logs, and patterns that make logs useful in production | [Troubleshoot common issues](https://posthog.com/docs/logs/troubleshooting.md) ## 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](https://posthog.com/docs/logs/troubleshooting.md).