OpenTelemetry Logs for PHP

This document covers OpenTelemetry Logs for PHP, focusing on integrating popular logging libraries like Monolog.

Prerequisites

Make sure your exporter is configured before you start instrumenting code. Follow Getting started with OpenTelemetry PHP or set up Direct OTLP Configuration 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 PHP:

  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.

Monolog integration

Monolog is PHP's most popular logging library. OpenTelemetry provides a handler to send Monolog logs to your observability backend.

Installation

bash
composer require monolog/monolog open-telemetry/opentelemetry-logger-monolog

Basic configuration

php
<?php

use Monolog\Logger;
use OpenTelemetry\Contrib\Logs\Monolog\Handler;
use OpenTelemetry\API\Logs\LoggerProviderInterface;

// Get the logger provider from your OpenTelemetry configuration
$loggerProvider = $container->get(LoggerProviderInterface::class);

// Create Monolog logger with OpenTelemetry handler
$logger = new Logger('app');
$logger->pushHandler(new Handler(
    $loggerProvider,
    'info' // Minimum log level
));

Complete example

php
<?php

declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';

use Monolog\Logger;
use OpenTelemetry\API\Common\Instrumentation\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\Contrib\Logs\Monolog\Handler;

// Configure Uptrace
$uptrace = Uptrace\Distro::builder()
    ->setDsn('YOUR_UPTRACE_DSN_HERE')
    ->setServiceName('myservice')
    ->setServiceVersion('1.0.0')
    ->buildAndRegisterGlobal();

// Create Monolog logger with OpenTelemetry handler
$loggerProvider = Globals::loggerProvider();
$logger = new Logger('myservice');
$logger->pushHandler(new Handler($loggerProvider, 'info'));

// Create tracer
$tracer = Globals::tracerProvider()->getTracer('myservice');

// Start a traced operation
$span = $tracer->spanBuilder('process-request')->startSpan();
$scope = $span->activate();

try {
    // Logs automatically include trace context
    $logger->info('Processing request', [
        'user_id' => '12345',
        'action' => 'login',
    ]);

    $logger->error('Authentication failed', [
        'reason' => 'invalid_credentials',
    ]);
} finally {
    $scope->detach();
    $span->end();
}

echo "Trace URL: " . $uptrace->traceUrl($span) . PHP_EOL;

PSR-3 logging integration

If you use a PSR-3 compatible logger, you can add trace context using a processor:

php
<?php

use Monolog\Logger;
use Monolog\Processor\ProcessorInterface;
use OpenTelemetry\API\Trace\Span;

class TraceContextProcessor implements ProcessorInterface
{
    public function __invoke(array $record): array
    {
        $spanContext = Span::getCurrent()->getContext();

        if ($spanContext->isValid()) {
            $record['extra']['trace_id'] = $spanContext->getTraceId();
            $record['extra']['span_id'] = $spanContext->getSpanId();
            $record['extra']['trace_flags'] = $spanContext->getTraceFlags();
        }

        return $record;
    }
}

// Add processor to your logger
$logger = new Logger('app');
$logger->pushProcessor(new TraceContextProcessor());

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, manually inject trace context:

php
<?php

use OpenTelemetry\API\Trace\Span;
use Psr\Log\LoggerInterface;

function logWithContext(LoggerInterface $logger, string $message, array $context = []): void
{
    $spanContext = Span::getCurrent()->getContext();

    if ($spanContext->isValid()) {
        $context['trace_id'] = $spanContext->getTraceId();
        $context['span_id'] = $spanContext->getSpanId();
    }

    $logger->info($message, $context);
}

// Usage
logWithContext($logger, 'User logged in', ['user_id' => '12345']);

Accessing trace context

Get trace context information for custom logging:

php
<?php

use OpenTelemetry\API\Trace\Span;

function getTraceContext(): array
{
    $spanContext = Span::getCurrent()->getContext();

    if (!$spanContext->isValid()) {
        return [];
    }

    return [
        'trace_id' => $spanContext->getTraceId(),
        'span_id' => $spanContext->getSpanId(),
        'trace_flags' => sprintf('%02x', $spanContext->getTraceFlags()),
        'is_remote' => $spanContext->isRemote(),
    ];
}

// Use in your logging
$context = getTraceContext();
echo sprintf("Current trace: %s\n", json_encode($context));

Best practices

Use structured logging

Use context arrays to add structured fields:

php
// Good: Structured fields enable filtering and analysis
$logger->info('Database query executed', [
    'query_type' => 'SELECT',
    'table' => 'users',
    'duration_ms' => 45,
    'rows_affected' => 1,
]);

// Avoid: Unstructured messages are harder to analyze
$logger->info('SELECT query on users took 45ms and returned 1 row');

Log within span context

Always log within an active span for automatic correlation:

php
// Good: Logs are correlated with trace
$span = $tracer->spanBuilder('process-order')->startSpan();
$scope = $span->activate();
try {
    $logger->info('Processing order', ['order_id' => $orderId]);
    processOrder($orderId);
    $logger->info('Order processed successfully');
} finally {
    $scope->detach();
    $span->end();
}

// Less useful: Logs not correlated with trace
$logger->info('Processing order', ['order_id' => $orderId]);
$span = $tracer->spanBuilder('process-order')->startSpan();
// ...

Use appropriate log levels

Choose log levels based on the information type:

php
$logger->debug('Detailed debugging information');    // Development only
$logger->info('General operational events');         // Normal operations
$logger->warning('Unexpected but handled events');   // Potential issues
$logger->error('Errors that need attention');        // Failures
$logger->critical('System-level failures');          // Critical issues

Avoid logging sensitive data

Never log passwords, tokens, or PII:

php
// Bad: Logging sensitive data
$logger->info('User login', ['password' => $password, 'token' => $token]);

// Good: Redact sensitive fields
$logger->info('User login', ['user_id' => $userId, 'ip_address' => $ip]);

What's next?