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:

  1. Log bridges (recommended): Integrate with existing logging libraries to automatically capture logs and correlate them with traces.
  2. 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

shell
# npm
npm install winston @opentelemetry/api --save

# yarn
yarn add winston @opentelemetry/api

Basic configuration

js
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

js
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

shell
# npm
npm install pino @opentelemetry/api @opentelemetry/instrumentation-pino --save

# yarn
yarn add pino @opentelemetry/api @opentelemetry/instrumentation-pino

Basic configuration

js
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

js
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

shell
# npm
npm install bunyan @opentelemetry/api @opentelemetry/instrumentation-bunyan --save

# yarn
yarn add bunyan @opentelemetry/api @opentelemetry/instrumentation-bunyan

Basic configuration

js
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

js
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

shell
# npm
npm install @opentelemetry/api-logs @opentelemetry/sdk-logs --save

# yarn
yarn add @opentelemetry/api-logs @opentelemetry/sdk-logs

Basic configuration

js
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

js
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:

js
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:

js
// 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:

js
// 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:

js
// 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:

js
// 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 })

What's next?