OpenTelemetry Logs for JavaScript
Prerequisites
Make sure your exporter is configured before you start instrumenting code. Follow Getting started with OpenTelemetry JavaScript first.
If you are not familiar with logs terminology like structured logging or log-trace correlation, read the introduction to OpenTelemetry Logs first.
Overview
OpenTelemetry provides two approaches for collecting logs in JavaScript:
- Log bridges (recommended): Integrate with existing logging libraries to automatically capture logs and correlate them with traces.
- Logs API: Use the native OpenTelemetry Logs API directly for maximum control.
Log bridges are the recommended approach because they allow you to use familiar logging APIs while automatically adding trace context (trace_id, span_id) to your logs.
Winston integration
Winston is one of the most popular logging libraries for Node.js. You can integrate it with OpenTelemetry by adding trace context to your log format.
Installation
# npm
npm install winston @opentelemetry/api --save
# yarn
yarn add winston @opentelemetry/api
Basic configuration
const winston = require('winston')
const { trace } = require('@opentelemetry/api')
// Create a custom format that adds trace context
const otelFormat = winston.format((info) => {
const span = trace.getActiveSpan()
if (span) {
const spanContext = span.spanContext()
info.trace_id = spanContext.traceId
info.span_id = spanContext.spanId
info.trace_flags = spanContext.traceFlags
}
return info
})
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(otelFormat(), winston.format.json()),
transports: [new winston.transports.Console()],
})
Complete example
const { trace, context } = require('@opentelemetry/api')
const { NodeSDK } = require('@opentelemetry/sdk-node')
const {
OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http')
const winston = require('winston')
// Initialize OpenTelemetry
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(),
serviceName: 'myservice',
})
sdk.start()
// Create logger with trace context
const otelFormat = winston.format((info) => {
const span = trace.getActiveSpan()
if (span) {
const spanContext = span.spanContext()
info.trace_id = spanContext.traceId
info.span_id = spanContext.spanId
}
return info
})
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(otelFormat(), winston.format.json()),
transports: [new winston.transports.Console()],
})
// Get tracer
const tracer = trace.getTracer('myservice')
// Example function with tracing and logging
async function processRequest(userId) {
return tracer.startActiveSpan('process-request', async (span) => {
try {
// Logs automatically include trace context
logger.info('Processing request', {
user_id: userId,
action: 'login',
})
// Simulate work
await doWork()
logger.info('Request completed successfully')
span.end()
} catch (error) {
logger.error('Request failed', {
error: error.message,
user_id: userId,
})
span.recordException(error)
span.end()
throw error
}
})
}
Pino integration
Pino is a fast, low-overhead logging library for Node.js. OpenTelemetry provides an official instrumentation package for Pino.
Installation
# npm
npm install pino @opentelemetry/api @opentelemetry/instrumentation-pino --save
# yarn
yarn add pino @opentelemetry/api @opentelemetry/instrumentation-pino
Basic configuration
const { NodeSDK } = require('@opentelemetry/sdk-node')
const { PinoInstrumentation } = require('@opentelemetry/instrumentation-pino')
// Initialize OpenTelemetry with Pino instrumentation
const sdk = new NodeSDK({
instrumentations: [new PinoInstrumentation()],
})
sdk.start()
// Now create your Pino logger
const pino = require('pino')
const logger = pino()
// Logs will automatically include trace context when inside a span
Complete example
const { NodeSDK } = require('@opentelemetry/sdk-node')
const {
OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http')
const { PinoInstrumentation } = require('@opentelemetry/instrumentation-pino')
const { trace } = require('@opentelemetry/api')
// Initialize OpenTelemetry with Pino instrumentation
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(),
serviceName: 'myservice',
instrumentations: [
new PinoInstrumentation({
// Optional: customize log hook
logHook: (span, record) => {
record['resource.service.name'] = 'myservice'
},
}),
],
})
sdk.start()
// Create Pino logger after SDK initialization
const pino = require('pino')
const logger = pino({
level: 'info',
})
// Get tracer
const tracer = trace.getTracer('myservice')
// Example function with tracing and logging
async function processOrder(orderId) {
return tracer.startActiveSpan('process-order', async (span) => {
try {
// Pino automatically includes trace_id and span_id
logger.info({ order_id: orderId }, 'Processing order')
await validateOrder(orderId)
logger.info({ order_id: orderId }, 'Order validated')
await chargePayment(orderId)
logger.info({ order_id: orderId }, 'Payment processed')
span.end()
} catch (error) {
logger.error({ order_id: orderId, err: error }, 'Order processing failed')
span.recordException(error)
span.end()
throw error
}
})
}
Bunyan integration
Bunyan is a simple and fast JSON logging library. OpenTelemetry provides instrumentation for Bunyan as well.
Installation
# npm
npm install bunyan @opentelemetry/api @opentelemetry/instrumentation-bunyan --save
# yarn
yarn add bunyan @opentelemetry/api @opentelemetry/instrumentation-bunyan
Basic configuration
const { NodeSDK } = require('@opentelemetry/sdk-node')
const {
BunyanInstrumentation,
} = require('@opentelemetry/instrumentation-bunyan')
// Initialize OpenTelemetry with Bunyan instrumentation
const sdk = new NodeSDK({
instrumentations: [new BunyanInstrumentation()],
})
sdk.start()
// Now create your Bunyan logger
const bunyan = require('bunyan')
const logger = bunyan.createLogger({ name: 'myservice' })
// Logs will automatically include trace context when inside a span
Complete example
const { NodeSDK } = require('@opentelemetry/sdk-node')
const {
OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http')
const {
BunyanInstrumentation,
} = require('@opentelemetry/instrumentation-bunyan')
const { trace } = require('@opentelemetry/api')
// Initialize OpenTelemetry with Bunyan instrumentation
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(),
serviceName: 'myservice',
instrumentations: [new BunyanInstrumentation()],
})
sdk.start()
// Create Bunyan logger after SDK initialization
const bunyan = require('bunyan')
const logger = bunyan.createLogger({
name: 'myservice',
level: 'info',
})
// Get tracer
const tracer = trace.getTracer('myservice')
// Example function with tracing and logging
async function fetchUser(userId) {
return tracer.startActiveSpan('fetch-user', async (span) => {
try {
// Bunyan automatically includes trace_id and span_id
logger.info({ user_id: userId }, 'Fetching user from database')
const user = await database.findUser(userId)
if (!user) {
logger.warn({ user_id: userId }, 'User not found')
span.end()
return null
}
logger.info({ user_id: userId }, 'User fetched successfully')
span.end()
return user
} catch (error) {
logger.error({ user_id: userId, err: error }, 'Failed to fetch user')
span.recordException(error)
span.end()
throw error
}
})
}
Using the Logs API directly
For maximum control, you can use the OpenTelemetry Logs API directly. This is useful when building new applications without existing logging dependencies.
Installation
# npm
npm install @opentelemetry/api-logs @opentelemetry/sdk-logs --save
# yarn
yarn add @opentelemetry/api-logs @opentelemetry/sdk-logs
Basic configuration
const { logs, SeverityNumber } = require('@opentelemetry/api-logs')
const {
LoggerProvider,
SimpleLogRecordProcessor,
ConsoleLogRecordExporter,
} = require('@opentelemetry/sdk-logs')
// Set up the logger provider
const loggerProvider = new LoggerProvider()
loggerProvider.addLogRecordProcessor(
new SimpleLogRecordProcessor(new ConsoleLogRecordExporter())
)
// Set the global logger provider
logs.setGlobalLoggerProvider(loggerProvider)
// Get a logger
const logger = logs.getLogger('myservice', '1.0.0')
// Emit a log record
logger.emit({
severityNumber: SeverityNumber.INFO,
severityText: 'INFO',
body: 'Application started',
attributes: {
'service.version': '1.0.0',
},
})
Complete example with OTLP export
const { logs, SeverityNumber } = require('@opentelemetry/api-logs')
const {
LoggerProvider,
BatchLogRecordProcessor,
} = require('@opentelemetry/sdk-logs')
const {
OTLPLogExporter,
} = require('@opentelemetry/exporter-logs-otlp-http')
const { trace } = require('@opentelemetry/api')
// Set up OTLP log exporter
const logExporter = new OTLPLogExporter({
url: 'https://otlp.uptrace.dev/v1/logs',
headers: {
'uptrace-dsn': 'https://<token>@api.uptrace.dev/<project_id>',
},
})
// Set up the logger provider with batching
const loggerProvider = new LoggerProvider()
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter))
logs.setGlobalLoggerProvider(loggerProvider)
const logger = logs.getLogger('myservice', '1.0.0')
const tracer = trace.getTracer('myservice')
// Helper function for logging with severity
function logInfo(message, attributes = {}) {
logger.emit({
severityNumber: SeverityNumber.INFO,
severityText: 'INFO',
body: message,
attributes,
})
}
function logError(message, attributes = {}) {
logger.emit({
severityNumber: SeverityNumber.ERROR,
severityText: 'ERROR',
body: message,
attributes,
})
}
// Example usage
async function handleRequest(req) {
return tracer.startActiveSpan('handle-request', async (span) => {
logInfo('Request received', {
'http.method': req.method,
'http.url': req.url,
})
try {
const result = await processRequest(req)
logInfo('Request processed successfully', {
'response.status': 200,
})
span.end()
return result
} catch (error) {
logError('Request processing failed', {
'error.type': error.name,
'error.message': error.message,
})
span.recordException(error)
span.end()
throw error
}
})
}
Log-trace correlation
When you emit a log within an active trace span, OpenTelemetry automatically includes:
- trace_id: Links log to the entire distributed trace
- span_id: Links log to the specific operation
- trace_flags: Indicates if the trace is sampled
This enables bidirectional navigation between logs and traces in your observability backend.
Manual correlation
If you can't use log bridges or instrumentation, manually inject trace context:
const { trace } = require('@opentelemetry/api')
function logWithContext(level, message, attributes = {}) {
const span = trace.getActiveSpan()
if (span) {
const spanContext = span.spanContext()
attributes.trace_id = spanContext.traceId
attributes.span_id = spanContext.spanId
}
console.log(
JSON.stringify({
level,
message,
timestamp: new Date().toISOString(),
...attributes,
})
)
}
// Usage
logWithContext('info', 'Processing request', { user_id: '12345' })
Best practices
Use context-aware logging
Always log within the context of a span to include trace correlation:
// Good: Logs include trace context
tracer.startActiveSpan('operation', (span) => {
logger.info('Processing started')
// ...
span.end()
})
// Less useful: Logs outside span context
logger.info('Processing started') // No trace correlation
Use structured fields
Use key-value pairs for structured logging to enable filtering:
// Good: Structured logging
logger.info('Database query executed', {
query_type: 'SELECT',
table: 'users',
duration_ms: 45,
rows_affected: 1,
})
// Bad: Unstructured logging
logger.info(
`Database query SELECT on users took 45ms and returned 1 row`
)
Avoid logging sensitive data
Never log passwords, tokens, or PII:
// Bad: Logging sensitive data
logger.info('User login', { password: password })
// Good: Redact sensitive fields
logger.info('User login', { user_id: userId })
Choose appropriate log levels
Use log levels consistently:
// TRACE/DEBUG: Detailed debugging information
logger.debug('Cache lookup', { key: 'user:123', hit: true })
// INFO: Normal operation events
logger.info('User registered', { user_id: '123' })
// WARN: Potential issues that don't stop operation
logger.warn('Rate limit approaching', { current: 95, max: 100 })
// ERROR: Errors that need attention
logger.error('Payment failed', { order_id: '456', error: err.message })