OTLP Adapter
The OTLP (OpenTelemetry Protocol) adapter sends logs in the standard OpenTelemetry format. This works with any OTLP-compatible backend including:
- Grafana Cloud (Loki)
- Datadog
- Honeycomb
- Jaeger
- Splunk
- New Relic
- Self-hosted OpenTelemetry Collector
- HyperDX
Add the OTLP drain adapter
Installation
The OTLP adapter comes bundled with evlog:
import { createOTLPDrain } from 'evlog/otlp'
Quick Start
1. Set your OTLP endpoint
OTLP_ENDPOINT=http://localhost:4318
2. Wire the drain to your framework
// server/plugins/evlog-drain.ts
import { createOTLPDrain } from 'evlog/otlp'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
})
// lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createOTLPDrain } from 'evlog/otlp'
export const { withEvlog, useLogger, log, createError } = createEvlog({
service: 'my-app',
drain: createOTLPDrain(),
})
import { createOTLPDrain } from 'evlog/otlp'
app.use(evlog({ drain: createOTLPDrain() }))
import { createOTLPDrain } from 'evlog/otlp'
app.use(evlog({ drain: createOTLPDrain() }))
import { createOTLPDrain } from 'evlog/otlp'
await app.register(evlog, { drain: createOTLPDrain() })
import { createOTLPDrain } from 'evlog/otlp'
app.use(evlog({ drain: createOTLPDrain() }))
import { createOTLPDrain } from 'evlog/otlp'
EvlogModule.forRoot({ drain: createOTLPDrain() })
import { createOTLPDrain } from 'evlog/otlp'
initLogger({ drain: createOTLPDrain() })
Configuration
The adapter reads configuration from multiple sources (highest priority first):
- Overrides passed to
createOTLPDrain() - Runtime config at
runtimeConfig.otlp(Nuxt/Nitro only) - Environment variables
Environment Variables
| Variable | Description |
|---|---|
OTLP_ENDPOINT | OTLP HTTP endpoint (e.g., http://localhost:4318). The standard OTEL_EXPORTER_OTLP_ENDPOINT also works. |
OTLP_HEADERS | Headers as key=value pairs, comma-separated. The standard OTEL_EXPORTER_OTLP_HEADERS also works. |
OTEL_SERVICE_NAME | Service name override |
Runtime Config (Nuxt only)
export default defineNuxtConfig({
runtimeConfig: {
otlp: {
endpoint: '', // Set via OTLP_ENDPOINT (or OTEL_EXPORTER_OTLP_ENDPOINT)
},
},
})
Override Options
const drain = createOTLPDrain({
endpoint: 'http://localhost:4318',
serviceName: 'my-api',
headers: {
'Authorization': 'Bearer xxx',
},
resourceAttributes: {
'deployment.environment': 'staging',
},
})
Full Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
endpoint | string | - | OTLP HTTP endpoint (required) |
serviceName | string | From event | Override service.name resource attribute |
headers | object | - | Custom HTTP headers for authentication |
resourceAttributes | object | - | Additional OTLP resource attributes |
timeout | number | 5000 | Request timeout in milliseconds |
Deployment
OTLP is a protocol, not a product — the same adapter talks to a collector you run yourself and to a managed gateway. Only the endpoint and headers change.
Self-hosted
Run an OpenTelemetry Collector and point evlog at it. Nothing else to configure:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
exporters:
debug:
verbosity: detailed
service:
pipelines:
logs:
receivers: [otlp]
exporters: [debug]
docker run --rm -p 4318:4318 \
-v $(pwd)/otel-collector.yaml:/etc/otelcol/config.yaml \
otel/opentelemetry-collector:latest
OTLP_ENDPOINT=http://localhost:4318
From there the collector fans out wherever you want — Loki, ClickHouse, Elasticsearch, a managed backend, or several at once. That indirection is the reason to pick OTLP over a direct adapter.
Managed gateways
Same adapter, a credentialed endpoint:
OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20base64-encoded-credentials
OTLP_ENDPOINT=https://http-intake.logs.datadoghq.com
OTLP_HEADERS=DD-API-KEY=your-api-key
OTLP_ENDPOINT=https://api.honeycomb.io
OTLP_HEADERS=x-honeycomb-team=your-api-key
%20 is a space. The adapter
decodes that format automatically.OTLP Log Format
evlog maps wide events to the OTLP log format:
| evlog Field | OTLP Field |
|---|---|
level | severityNumber / severityText |
timestamp | timeUnixNano |
service | Resource attribute service.name |
environment | Resource attribute deployment.environment |
version | Resource attribute service.version |
region | Resource attribute cloud.region |
traceId | traceId |
spanId | spanId |
| All other fields | Log attributes |
Severity Mapping
| evlog Level | OTLP Severity Number | OTLP Severity Text |
|---|---|---|
debug | 5 | DEBUG |
info | 9 | INFO |
warn | 13 | WARN |
error | 17 | ERROR |
Troubleshooting
Missing endpoint error
[evlog/otlp] Missing endpoint. Set OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT
Make sure your endpoint environment variable is set and the server was restarted.
401 Unauthorized
Your authentication headers may be missing or incorrect. Check:
- The
OTEL_EXPORTER_OTLP_HEADERSformat is correct - Credentials are valid and not expired
- The endpoint URL is correct
404 Not Found
The adapter sends to /v1/logs. Make sure your endpoint:
- Supports OTLP HTTP (not gRPC)
- Is the base URL without
/v1/logssuffix
Logs not appearing
- Check the server console for
[evlog/otlp]error messages - Test with a local collector first to verify the format
- Check your backend's ingestion delay (some have 1-2 minute delays)
Direct API Usage
For advanced use cases:
import { sendToOTLP, sendBatchToOTLP, toOTLPLogRecord } from 'evlog/otlp'
// Send a single event
await sendToOTLP(event, {
endpoint: 'http://localhost:4318',
})
// Send multiple events
await sendBatchToOTLP(events, {
endpoint: 'http://localhost:4318',
})
// Convert event to OTLP format (for inspection)
const otlpRecord = toOTLPLogRecord(event)
Next Steps
- Axiom Adapter - Send logs to Axiom
- PostHog Adapter - Send logs to PostHog
- Custom Adapters - Build your own adapter
- Best Practices - Security and production tips
ClickHouse
Insert wide events into ClickHouse over the HTTP interface — typed columns for what you aggregate, full JSON for everything else.
HyperDX
Send wide events to HyperDX via OTLP/HTTP using HyperDX’s documented OpenTelemetry endpoint and authorization header. Zero-config setup with environment variables.