Skip to main content

Pino

Ship Pino logs from a Node.js application to Oodle over OpenTelemetry. The transport turns every line into an OTLP log record: the level becomes the severity, the message the body, and the other fields attributes you can filter on. With the optional instrumentation, every line also carries the trace id of the request it was logged in, so a log opens its trace and a trace lists its logs.

New to Oodle?

Oodle is a managed observability platform for metrics, logs, traces and agent traces. It ingests OpenTelemetry natively, so the snippet on this page is the complete setup.

Each trace shows the full transcript, the token counts and cost of every call, the agent structure (runs, steps and tool calls) and Signals: labels for loops, rate limits, refusals and tool failures, detected as the trace arrives.

Sign up for free to get an instance ID and an API key, or see the Agent Observability overview first.

You will need:

  • OODLE_INSTANCE: your Oodle instance ID (ap1, us1)
  • OODLE_API_KEY: an Oodle API key (ap1, us1)
  • OTLP_ENDPOINT: your OTLP collector domain, shown on the tile

Open the Pino tile on the ap1, us1 page to get these filled in for you, or let an agent do the setup with /oodle-onboarding set up the logs_pino integration.

Install

Install Pino with its OpenTelemetry transport (Pino 10 or later):

npm install pino pino-opentelemetry-transport

Create the logger

Create the logger once and use it everywhere. The transport builds its own resource, so name the service in resourceAttributes or with OTEL_SERVICE_NAME. Child loggers carry fields such as a request or order id on every line:

// logger.js
const pino = require('pino');

// Every line becomes an OTLP log record and is exported to the
// endpoint in OTEL_EXPORTER_OTLP_ENDPOINT, with the level as
// its severity and the other fields as attributes.
const transport = pino.transport({
target: 'pino-opentelemetry-transport',
options: {
// The transport builds its own resource. Name the service
// here, or set OTEL_SERVICE_NAME in the environment.
resourceAttributes: { 'service.name': 'my-node-app' },
},
});

module.exports = pino(transport);

// app.js
const logger = require('./logger');

logger.info({ orderId: 'ord-1001' }, 'order placed');
logger.error({ err: new Error('timeout') }, 'payment failed');

Environment

The transport reads the standard OTEL_EXPORTER_OTLP_* variables. The endpoint is the OTLP domain with no path; the exporter appends /v1/logs:

# OTLP endpoint (points straight at Oodle)
export OTEL_EXPORTER_OTLP_ENDPOINT=https://<OTLP_ENDPOINT>

export OTEL_EXPORTER_OTLP_HEADERS="X-API-KEY=<OODLE_API_KEY>,X-OODLE-INSTANCE=<OODLE_INSTANCE>"

# Compress the export
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip

node app.js

Through an OpenTelemetry Collector

To keep the credentials out of the application, and to redact or sample in one place, point the app at a collector you run instead:

# OTLP endpoint (points to your OTel Collector)
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318

node app.js

and have the collector forward to Oodle:

receivers:
otlp:
protocols:
http:
endpoint: "0.0.0.0:4318"

processors:
batch:
timeout: 5s
send_batch_size: 512

exporters:
otlphttp/oodle:
endpoint: "https://<OTLP_ENDPOINT>"
compression: gzip
headers:
"X-OODLE-INSTANCE": "<OODLE_INSTANCE>"
"X-API-KEY": "<OODLE_API_KEY>"

service:
pipelines:
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/oodle]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/oodle]

Correlate logs with traces

Optional. The Pino instrumentation writes the active span's trace_id and span_id on each log line, and the transport lifts them into the record's trace context. Install the instrumentations:

npm install @opentelemetry/instrumentation-pino \
@opentelemetry/instrumentation \
@opentelemetry/instrumentation-http \
@opentelemetry/exporter-trace-otlp-proto \
@opentelemetry/resources \
@opentelemetry/sdk-trace-base \
@opentelemetry/sdk-trace-node \
@opentelemetry/semantic-conventions

Register them in a file the app loads first, so pino and http are patched as they load:

// instrumentation.js
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { PinoInstrumentation } = require('@opentelemetry/instrumentation-pino');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');

const provider = new NodeTracerProvider({
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'my-node-app' }),
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())],
});
provider.register();

registerInstrumentations({
instrumentations: [
// One span per request, so every line logged while it is
// handled carries that trace id.
new HttpInstrumentation(),
// Writes trace_id and span_id on each line. The transport
// already exports the records, so sending them from here
// as well would deliver every line twice.
new PinoInstrumentation({ disableLogSending: true }),
],
});

process.on('SIGTERM', () => provider.shutdown());

// Load it before the app, so pino and http are patched as
// they load:
// node --require ./instrumentation.js app.js

disableLogSending matters: the transport already exports every record, and the instrumentation would send a second copy through the logs SDK.

Verify

Run the application, then open the Logs Explorer (ap1, us1) and filter on service for the name you set. Each line arrives with its level in norm_level, its message, its fields, and trace_id when the instrumentation is on.

If nothing arrives, check that the app can reach https://<OTLP_ENDPOINT> and that the instance and key are set: the OTLP gateway answers 401 without them. If every line arrives twice, the instrumentation is sending records as well as the transport; pass disableLogSending: true.


Support

If you need assistance or have any questions, please reach out to us through: