PostHog Adapter
PostHog is an open-source product analytics platform. The evlog PostHog adapter sends your wide events as PostHog events, letting you query server-side logs alongside your product analytics, session replays, and feature flags.
Installation
The PostHog adapter comes bundled with evlog:
import { createPostHogDrain } from 'evlog/posthog'
Quick Start
1. Get your PostHog project API key
- Log in to your PostHog dashboard
- Go to Settings > Project > Project API Key
- Copy the key (starts with
phc_)
2. Set environment variables
NUXT_POSTHOG_API_KEY=phc_your-project-api-key
3. Create the drain plugin
import { createPostHogDrain } from 'evlog/posthog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createPostHogDrain())
})
That's it! Your wide events will now appear in PostHog as evlog_wide_event events.
Configuration
The adapter reads configuration from multiple sources (highest priority first):
- Overrides passed to
createPostHogDrain() - Runtime config at
runtimeConfig.evlog.posthog - Runtime config at
runtimeConfig.posthog - Environment variables (
NUXT_POSTHOG_*orPOSTHOG_*)
Environment Variables
| Variable | Description |
|---|---|
NUXT_POSTHOG_API_KEY | Project API key (starts with phc_) |
NUXT_POSTHOG_HOST | PostHog host URL (for EU or self-hosted) |
You can also use POSTHOG_API_KEY and POSTHOG_HOST as fallbacks.
Runtime Config
Configure via nuxt.config.ts for type-safe configuration:
export default defineNuxtConfig({
runtimeConfig: {
posthog: {
apiKey: '', // Set via NUXT_POSTHOG_API_KEY
host: '', // Set via NUXT_POSTHOG_HOST
},
},
})
Override Options
Pass options directly to override any configuration:
import { createPostHogDrain } from 'evlog/posthog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createPostHogDrain({
eventName: 'server_request',
distinctId: 'my-backend-service',
timeout: 10000,
}))
})
Full Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | - | Project API key (required) |
host | string | https://us.i.posthog.com | PostHog host URL |
eventName | string | evlog_wide_event | PostHog event name |
distinctId | string | event.service | Override distinct_id for all events |
timeout | number | 5000 | Request timeout in milliseconds |
Regions
PostHog offers US and EU cloud hosting. Set the host to match your region:
| Region | Host |
|---|---|
| US (default) | https://us.i.posthog.com |
| EU | https://eu.i.posthog.com |
| Self-hosted | Your instance URL |
# EU region
NUXT_POSTHOG_API_KEY=phc_xxx
NUXT_POSTHOG_HOST=https://eu.i.posthog.com
Event Format
evlog maps wide events to PostHog events:
| evlog Field | PostHog Field |
|---|---|
config.distinctId or userId or service | distinct_id (fallback chain) |
timestamp | timestamp |
level | properties.level |
service | properties.service |
environment | properties.environment |
| All other fields | properties.* |
The event name defaults to evlog_wide_event but can be customized via the eventName option.
Custom Event Name
Use a custom event name to differentiate log types in PostHog:
import { createPostHogDrain } from 'evlog/posthog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createPostHogDrain({
eventName: 'server_wide_event',
}))
})
Custom Distinct ID
The distinct_id follows a fallback chain:
config.distinctId— explicit override increatePostHogDrain()event.userId— automatically picked up if present as a stringevent.service— final fallback
This means if you use setIdentity({ userId: 'usr_123' }) on the client, the userId flows through client transport → server ingest → PostHog drain, and is automatically used as distinct_id. No additional configuration needed.
// Client-side — identity is set once (e.g. after login)
setIdentity({ userId: 'usr_123' })
// Every log now includes userId
log.info({ action: 'checkout' })
// → PostHog event with distinct_id: 'usr_123'
To override distinct_id for all events regardless of userId, pass distinctId to the drain:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createPostHogDrain({
distinctId: 'my-backend-service', // Always uses this value
}))
})
Querying Logs in PostHog
Once your events are flowing, you can query them in PostHog:
- Go to Events and filter by
evlog_wide_event - Use Insights to build dashboards on your wide event properties
- Create Cohorts based on server-side behavior (e.g., users who triggered errors)
Example queries you can build:
- Error rate by endpoint (
properties.path,properties.level = error) - Slow requests over time (
properties.duration > 1000) - Request volume by service (
properties.service)
Troubleshooting
Missing apiKey error
[evlog/posthog] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogDrain()
Make sure your environment variable is set and the server was restarted after adding it.
Events not appearing
PostHog processes events asynchronously. There may be a short delay (typically under 1 minute) before events appear in the dashboard.
- Check the server console for
[evlog/posthog]error messages - Verify your API key is correct and starts with
phc_ - Confirm your
hostmatches your PostHog region (US vs EU)
Wrong region
If you're on PostHog EU but using the default US host, event delivery will fail and the adapter will log errors (for example under [evlog/posthog]) to your server console. Set the correct host:
NUXT_POSTHOG_HOST=https://eu.i.posthog.com
Direct API Usage
For advanced use cases, you can use the lower-level functions:
import { sendToPostHog, sendBatchToPostHog, toPostHogEvent } from 'evlog/posthog'
// Send a single event
await sendToPostHog(event, {
apiKey: 'phc_xxx',
})
// Send multiple events in one request
await sendBatchToPostHog(events, {
apiKey: 'phc_xxx',
})
// Convert event to PostHog format (for inspection)
const posthogEvent = toPostHogEvent(event, { apiKey: 'phc_xxx' })
PostHog Logs (OTLP)
PostHog has a dedicated Logs product that accepts logs via the standard OTLP format. Instead of sending events to the PostHog Events pipeline, createPostHogLogsDrain() sends structured OTLP logs directly to PostHog Logs.
Why use PostHog Logs?
- Purpose-built UI — PostHog Logs provides a dedicated log viewer with filtering, search, and tail mode
- OTLP standard — Uses the OpenTelemetry log format, so your logs include severity levels, trace context, and structured attributes
- Same API key — Authenticates with your existing PostHog project API key
Quick Start
import { createPostHogLogsDrain } from 'evlog/posthog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createPostHogLogsDrain())
})
Configuration
createPostHogLogsDrain() uses the same configuration resolution chain as createPostHogDrain():
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | - | Project API key (required) |
host | string | https://us.i.posthog.com | PostHog host URL |
timeout | number | 5000 | Request timeout in milliseconds |
import { createPostHogLogsDrain } from 'evlog/posthog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createPostHogLogsDrain({
apiKey: 'phc_...',
host: 'https://eu.i.posthog.com', // EU region
}))
})
How It Works
Under the hood, createPostHogLogsDrain() wraps the OTLP adapter's sendBatchToOTLP() with PostHog-specific defaults:
- Endpoint:
{host}/i/v1/logs(PostHog's OTLP log ingest endpoint) - Auth:
Authorization: Bearer {apiKey}header - Format: Standard OTLP
ExportLogsServiceRequestwith severity levels, trace context, and structured attributes
Events vs Logs
createPostHogDrain() | createPostHogLogsDrain() | |
|---|---|---|
| Format | PostHog Events (/batch/) | OTLP Logs (/i/v1/logs) |
| PostHog UI | Events explorer | Logs viewer |
| Best for | Product analytics, cohorts, funnels | Debugging, log search, observability |
You can use both drains simultaneously to get the best of both worlds:
import { createPostHogDrain, createPostHogLogsDrain } from 'evlog/posthog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createPostHogDrain())
nitroApp.hooks.hook('evlog:drain', createPostHogLogsDrain())
})
Next Steps
- Axiom Adapter - Send logs to Axiom
- OTLP Adapter - Send logs via OpenTelemetry Protocol
- Custom Adapters - Build your own adapter
- Best Practices - Security and production tips