OpenTelemetry PHP distro for Uptrace

This guide shows you how to configure the OpenTelemetry PHP SDK to export spans, logs, and metrics to Uptrace using the OTLP/HTTP protocol.

For detailed API documentation, see:

Overview

OpenTelemetry provides observability for your PHP applications by collecting telemetry data (traces, metrics, and logs) and exporting it to monitoring systems like Uptrace.

Installation Options

uptrace-php is a thin wrapper around opentelemetry-php that pre-configures the OpenTelemetry SDK for Uptrace. It provides convenience without adding new functionality.

Prerequisites:

Install Composer following the official installation guide.

Installation:

bash
composer require uptrace/uptrace

Option 2: Direct OTLP Exporter

Skip the wrapper and use the OTLP exporter directly if you prefer more control over the configuration. See the Direct OTLP Configuration section below.

Quick Start Guide

Follow these steps to get your first trace running in 5 minutes:

Step 1: Create an Uptrace Project

  1. Create an Uptrace project to obtain your DSN (Data Source Name)
  2. Your DSN will look like: https://<secret>@api.uptrace.dev?grpc=4317

Step 2: Install the Package

bash
composer require uptrace/uptrace

Step 3: Basic Configuration

Create a PHP file with the following configuration:

php
<?php

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

use OpenTelemetry\API\Common\Instrumentation\Globals;
use OpenTelemetry\API\Trace\SpanKind;

// Configure Uptrace
$uptrace = Uptrace\Distro::builder()
    ->setDsn('YOUR_UPTRACE_DSN_HERE')  // Replace with your actual DSN
    ->setServiceName('myservice')
    ->setServiceVersion('1.0.0')
    ->setResourceAttributes(['deployment.environment' => 'production'])
    ->buildAndRegisterGlobal();

// Create a tracer instance
$tracer = Globals::tracerProvider()->getTracer('app_or_package_name');

Step 4: Create Your First Trace

php
// Create a root span to measure an operation
$main = $tracer->spanBuilder('main-operation')->startSpan();
$mainScope = $main->activate();

// Create a child span for an HTTP request
$httpSpan = $tracer->spanBuilder('GET /posts/:id')
    ->setSpanKind(SpanKind::KIND_SERVER)
    ->startSpan();
$httpScope = $httpSpan->activate();

// Add HTTP-specific attributes
$httpSpan->setAttribute('http.method', 'GET');
$httpSpan->setAttribute('http.route', '/posts/:id');
$httpSpan->setAttribute('http.url', 'http://localhost:8080/posts/123');
$httpSpan->setAttribute('http.status_code', 200);

// Handle errors within spans
try {
    throw new \Exception('Database connection failed');
} catch (\Exception $exc) {
    $httpSpan->setStatus('error', $exc->getMessage());
    $httpSpan->recordException($exc);
}

$httpScope->detach();
$httpSpan->end();

// Create a database operation span
$dbSpan = $tracer->spanBuilder('database-query')->startSpan();
$dbScope = $dbSpan->activate();

$dbSpan->setAttributes([
    'db.system' => 'mysql',
    'db.statement' => 'SELECT * FROM posts LIMIT 100',
    'db.name' => 'blog_db'
]);

$dbScope->detach();
$dbSpan->end();

// Clean up the main span
$mainScope->detach();
$main->end();

// Get the trace URL for debugging
echo "View trace: " . $uptrace->traceUrl($main) . PHP_EOL;

Step 5: Run Your Application

bash
UPTRACE_DSN="your_dsn_here" php your_script.php

You should see output like:

text
View trace: https://app.uptrace.dev/traces/<trace_id>

Configuration Options

Environment Variables

You can configure Uptrace using environment variables instead of hardcoding values:

bash
export UPTRACE_DSN="https://<secret>@api.uptrace.dev?grpc=4317"
export OTEL_SERVICE_NAME="myservice"
export OTEL_SERVICE_VERSION="1.0.0"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"

Then use in your code:

php
$uptrace = Uptrace\Distro::builder()
    // DSN will be read from UPTRACE_DSN environment variable
    ->setServiceName(getenv('OTEL_SERVICE_NAME') ?: 'myservice')
    ->setServiceVersion(getenv('OTEL_SERVICE_VERSION') ?: '1.0.0')
    ->buildAndRegisterGlobal();

Resource Attributes

Resource attributes provide metadata about your service:

php
$uptrace = Uptrace\Distro::builder()
    ->setDsn($dsn)
    ->setServiceName('user-service')
    ->setServiceVersion('2.1.0')
    ->setResourceAttributes([
        'deployment.environment' => 'production',
        'service.namespace' => 'backend',
        'service.instance.id' => gethostname(),
        'cloud.provider' => 'aws',
        'cloud.region' => 'us-west-2'
    ])
    ->buildAndRegisterGlobal();

Sampling Configuration

For production environments with high traffic, configure sampling to reduce overhead:

php
$uptrace = Uptrace\Distro::builder()
    ->setDsn($dsn)
    ->setServiceName('high-traffic-service')
    ->setSampling(0.1) // Sample 10% of traces
    ->buildAndRegisterGlobal();

Direct OTLP Configuration

If you prefer to configure OpenTelemetry directly without the Uptrace wrapper:

Dependencies

First, install the required packages:

bash
composer require \
    open-telemetry/opentelemetry \
    open-telemetry/exporter-otlp \
    open-telemetry/transport-grpc \
    open-telemetry/auto-instrumentation

Trace Export Configuration

php
<?php

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

use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\Contrib\Otlp\SpanExporter;

$dsn = getenv('UPTRACE_DSN');
if (!$dsn) {
    exit('UPTRACE_DSN environment variable is required');
}

// Configure service resource information
$resource = ResourceInfoFactory::emptyResource()->merge(
    ResourceInfo::create(Attributes::create([
        "service.name" => "my-php-service",
        "service.version" => "1.0.0",
    ])),
    ResourceInfoFactory::defaultResource()
);

// Set up OTLP HTTP transport
$transportFactory = new OtlpHttpTransportFactory();
$transport = $transportFactory->create(
    'https://api.uptrace.dev/v1/traces',
    'application/json',
    ['uptrace-dsn' => $dsn],
    TransportFactoryInterface::COMPRESSION_GZIP,
);

// Create span exporter and processor
$spanExporter = new SpanExporter($transport);
$spanProcessor = new BatchSpanProcessor(
    $spanExporter,
    ClockFactory::getDefault(),
    BatchSpanProcessor::DEFAULT_MAX_QUEUE_SIZE,
    BatchSpanProcessor::DEFAULT_SCHEDULE_DELAY,
    BatchSpanProcessor::DEFAULT_EXPORT_TIMEOUT,
    BatchSpanProcessor::DEFAULT_MAX_EXPORT_BATCH_SIZE,
    true,
);

// Build and register the tracer provider
$tracerProvider = TracerProvider::builder()
    ->addSpanProcessor($spanProcessor)
    ->setResource($resource)
    ->setSampler(new ParentBased(new AlwaysOnSampler()))
    ->build();

Sdk::builder()
    ->setTracerProvider($tracerProvider)
    ->setPropagator(TraceContextPropagator::getInstance())
    ->setAutoShutdown(true)
    ->buildAndRegisterGlobal();

// Now you can use the tracer
$tracer = \OpenTelemetry\API\Globals::tracerProvider()->getTracer('app_or_package_name');

Metrics Export Configuration

php
<?php

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

use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Metrics\MeterProvider;
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\Contrib\Otlp\MetricExporter;

$dsn = getenv('UPTRACE_DSN');
if (!$dsn) {
    exit('UPTRACE_DSN environment variable is required');
}

// Configure resource information
$resource = ResourceInfoFactory::emptyResource()->merge(
    ResourceInfo::create(Attributes::create([
        "service.name" => "my-php-service",
        "service.version" => "1.0.0",
    ])),
    ResourceInfoFactory::defaultResource()
);

// Set up metrics export
$transportFactory = new OtlpHttpTransportFactory();
$reader = new ExportingReader(
    new MetricExporter(
        $transportFactory->create(
            'https://api.uptrace.dev/v1/metrics',
            'application/json',
            ['uptrace-dsn' => $dsn],
            TransportFactoryInterface::COMPRESSION_GZIP,
        )
    ),
    ClockFactory::getDefault()
);

$meterProvider = MeterProvider::builder()
    ->setResource($resource)
    ->addReader($reader)
    ->build();

// Create and use metrics
$meter = $meterProvider->getMeter('app_or_package_name');
$counter = $meter->createCounter('http_requests_total', 'requests', 'Number of HTTP requests');
$histogram = $meter->createHistogram('http_request_duration', 'seconds', 'HTTP request duration');

// Example usage
$counter->add(1, ['method' => 'GET', 'status' => '200']);
$histogram->record(0.150, ['method' => 'GET', 'route' => '/api/users']);

Logs Export Configuration

php
<?php

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

use OpenTelemetry\API\Logs\EventLogger;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\Processor\BatchLogRecordProcessor;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\Contrib\Otlp\LogsExporter;

$dsn = getenv('UPTRACE_DSN');
if (!$dsn) {
    exit('UPTRACE_DSN environment variable is required');
}

// Configure resource information
$resource = ResourceInfoFactory::emptyResource()->merge(
    ResourceInfo::create(Attributes::create([
        "service.name" => "my-php-service",
        "service.version" => "1.0.0",
    ])),
    ResourceInfoFactory::defaultResource()
);

// Set up log export
$transportFactory = new OtlpHttpTransportFactory();
$transport = $transportFactory->create(
    'https://api.uptrace.dev/v1/logs',
    'application/json',
    ['uptrace-dsn' => $dsn],
    TransportFactoryInterface::COMPRESSION_GZIP,
);

$exporter = new LogsExporter($transport);
$processor = new BatchLogRecordProcessor($exporter, ClockFactory::getDefault());

$loggerProvider = LoggerProvider::builder()
    ->setResource($resource)
    ->addLogRecordProcessor($processor)
    ->build();

Sdk::builder()
    ->setLoggerProvider($loggerProvider)
    ->setAutoShutdown(true)
    ->buildAndRegisterGlobal();

// Create and use loggers
$logger = $loggerProvider->getLogger('my-app', '1.0.0');
$eventLogger = new EventLogger($logger, 'user-actions');

// Example usage
$record = (new LogRecord('User login successful'))
    ->setSeverityText('INFO')
    ->setAttributes([
        'user.id' => '12345',
        'user.email' => 'user@example.com',
        'request.ip' => '192.168.1.100'
    ]);

$eventLogger->logEvent('user.login', $record);

Auto-Instrumentation

OpenTelemetry PHP provides automatic instrumentation for popular libraries and frameworks. Enable it by installing the auto-instrumentation package:

bash
composer require open-telemetry/auto-instrumentation

Supported Libraries

Auto-instrumentation is available for:

  • HTTP clients: Guzzle, cURL
  • Databases: PDO, MySQLi, Doctrine DBAL
  • Frameworks: Symfony, Laravel, Slim
  • Template engines: Twig
  • Message queues: RabbitMQ, Redis

Enabling Auto-Instrumentation

Set the following environment variables:

bash
export OTEL_PHP_AUTOLOAD_ENABLED=true
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp

Or configure it programmatically:

php
use OpenTelemetry\Auto\Instrumentation\Instrumentations;

// Enable all available instrumentations
Instrumentations::registerAll();

// Or enable specific instrumentations
Instrumentations::register([
    'guzzle',
    'pdo',
    'symfony'
]);

Framework Integration

Symfony Integration

For Symfony applications, add the OpenTelemetry bundle:

bash
composer require open-telemetry/opentelemetry-bundle-symfony

Configure in config/packages/opentelemetry.yaml:

yaml
opentelemetry:
  resource:
    service_name: 'my-symfony-app'
    service_version: '1.0.0'
  tracing:
    enabled: true
    sampler: 'always_on'
  metrics:
    enabled: true

For detailed integration, see the OpenTelemetry Symfony Guide.

Laravel Integration

For Laravel applications, install the OpenTelemetry package:

bash
composer require open-telemetry/opentelemetry-laravel

Publish and configure the config file:

bash
php artisan vendor:publish --provider="OpenTelemetry\Laravel\OpenTelemetryServiceProvider"

For detailed integration, see the OpenTelemetry Laravel Guide.

Slim Framework Integration

For Slim applications, add middleware:

php
use OpenTelemetry\Contrib\Slim\SlimMiddleware;
use Slim\Factory\AppFactory;

$app = AppFactory::create();

// Add OpenTelemetry middleware
$app->add(new SlimMiddleware());

// Your routes...
$app->get('/', function ($request, $response) {
    $response->getBody()->write('Hello World!');
    return $response;
});

For detailed integration, see the OpenTelemetry Slim Guide.

Querying Data in Uptrace

Viewing Traces

Once your application is sending traces to Uptrace, you can:

  1. Browse traces in the Uptrace UI at https://app.uptrace.dev/traces
  2. Filter traces by service name, operation, duration, or custom attributes
  3. Analyze performance using the trace timeline and span details
  4. Set up alerts for error rates or slow requests

Querying Spans

Use the Uptrace query interface to find specific spans:

text
# Find all HTTP requests
where _kind = "server" AND http_method exists

# Find slow database queries
where db_system exists AND _dur_ms > 100ms

# Find errors in a specific service
where service_name = "user-service" AND _status_code = "error"

Analyzing Metrics

Access metrics in the Uptrace dashboard:

  1. System metrics: CPU, memory, disk usage
  2. Application metrics: Request rates, response times, error rates
  3. Custom metrics: Business-specific counters and histograms

Searching Logs

Query logs using the Uptrace log explorer:

text
# Find error logs
where log_severity = "ERROR"

# Find logs from specific user
where user_id = "12345"

# Find logs with specific message
where display_name contains "database"

Best Practices

Span Naming

Use descriptive span names that follow OpenTelemetry conventions:

php
// Good: Describes the operation
$span = $tracer->spanBuilder('GET /users/{id}')->startSpan();

// Bad: Too generic
$span = $tracer->spanBuilder('request')->startSpan();

Attribute Management

Add meaningful attributes to spans:

php
$span->setAttributes([
    'user.id' => $userId,
    'user.role' => $userRole,
    'request.size' => strlen($requestBody),
    'response.size' => strlen($responseBody)
]);

Error Handling

Properly record exceptions and errors:

php
try {
    // Your code here
} catch (\Exception $e) {
    $span->recordException($e);
    $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
    throw $e; // Re-throw if needed
}

Memory Management

For long-running processes, ensure proper cleanup:

php
// End spans explicitly
$span->end();

// Use try-finally for guaranteed cleanup
try {
    $span = $tracer->spanBuilder('operation')->startSpan();
    // Your code here
} finally {
    $span->end();
}

Performance Considerations

Sampling

Configure appropriate sampling rates for production:

php
use OpenTelemetry\SDK\Trace\Sampler\TraceIdRatioBasedSampler;

// Sample 1% of traces in high-traffic scenarios
$sampler = new TraceIdRatioBasedSampler(0.01);

Batch Processing

Optimize batch processor settings:

php
$spanProcessor = new BatchSpanProcessor(
    $spanExporter,
    ClockFactory::getDefault(),
    512,        // Max queue size
    5000,       // Schedule delay (ms)
    30000,      // Export timeout (ms)
    512,        // Max export batch size
    true        // Export on shutdown
);

Resource Usage

Monitor resource consumption:

php
// Check memory usage
$memoryUsage = memory_get_usage(true);
$span->setAttribute('memory.usage', $memoryUsage);

// Monitor CPU time
$cpuStart = microtime(true);
// ... your code ...
$cpuEnd = microtime(true);
$span->setAttribute('cpu.duration', ($cpuEnd - $cpuStart) * 1000);

Troubleshooting

Common Issues

Traces not appearing in Uptrace

  • Verify your DSN is correct
  • Check network connectivity to api.uptrace.dev
  • Ensure spans are properly ended before script termination

High memory usage

  • Reduce batch processor queue size
  • Implement sampling for high-traffic applications
  • Monitor span creation rate

Missing spans in auto-instrumentation

  • Verify auto-instrumentation is enabled
  • Check library versions for compatibility
  • Review instrumentation configuration

Performance impact

  • Implement appropriate sampling rates
  • Use asynchronous exporters when possible
  • Profile your application with and without telemetry

Debug Mode

Enable debug logging to troubleshoot issues:

php
$uptrace = Uptrace\Distro::builder()
    ->setDsn($dsn)
    ->setServiceName('myservice')
    ->setDebug(true)  // Enable debug mode
    ->buildAndRegisterGlobal();

Logging Configuration

Configure PHP logging to capture OpenTelemetry messages:

php
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php-errors.log');

Health Checks

Implement health checks to monitor telemetry:

php
function checkTelemetryHealth() {
    $tracer = Globals::tracerProvider()->getTracer('health-check');
    $span = $tracer->spanBuilder('health-check')->startSpan();

    try {
        // Test trace creation
        $span->setAttribute('health.status', 'ok');
        return true;
    } catch (\Exception $e) {
        $span->recordException($e);
        return false;
    } finally {
        $span->end();
    }
}

What's Next?

Now that you have OpenTelemetry PHP configured with Uptrace, explore these advanced topics: