OpenTelemetry PHP Tracing API

Installation

OpenTelemetry-PHP is the PHP implementation of OpenTelemetry. It provides the OpenTelemetry PHP API, which you can use to instrument your application with OpenTelemetry traces.

Install Composer following the official installation guide. Then, install uptrace-php:

shell
composer require uptrace/uptrace

Quickstart

Step 1. Let's instrument the following function:

php
function insertUser(string $name, string $email) {
    DB::insert('insert into users (name, email) values (?, ?)', [$name, $email]);
}

Step 2. Wrap the operation with a span:

php
$tracerProvider = $uptrace->createTracerProvider();
$tracer = $tracerProvider->getTracer('app_or_package_name');

function insertUser(string $name, string $email) {
    $span = $tracer->spanBuilder('insert-user')->startSpan();
    $spanScope = $span->activate();

    DB::insert('insert into users (name, email) values (?, ?)', [$name, $email]);

    $spanScope->detach();
    $span->end();
}

Step 3. Record exceptions and set status code:

php
function insertUser(string $name, string $email) {
    $span = $tracer->spanBuilder('insert-user')->startSpan();
    $spanScope = $span->activate();

    try {
        DB::insert('insert into users (name, email) values (?, ?)', [$name, $email]);
    } catch (\Exception $exc) {
        $span->recordException($exc);
        $span->setStatus(StatusCode::STATUS_ERROR, $exc->getMessage());
        throw $exc; // Re-throw the exception
    }

    $spanScope->detach();
    $span->end();
}

Step 4. Record contextual information with attributes:

php
function insertUser(string $name, string $email) {
    $span = $tracer->spanBuilder('insert-user')->startSpan();
    $spanScope = $span->activate();

    // Check if span is being recorded to avoid unnecessary work
    if ($span->isRecording()) {
        $span->setAttribute('enduser.name', $name);
        $span->setAttribute('enduser.email', $email);
    }

    try {
        DB::insert('insert into users (name, email) values (?, ?)', [$name, $email]);
    } catch (\Exception $exc) {
        $span->recordException($exc);
        $span->setStatus(StatusCode::STATUS_ERROR, $exc->getMessage());
        throw $exc;
    } finally {
        $spanScope->detach();
        $span->end();
    }
}

That's it! The operation is now fully instrumented with proper error handling and cleanup.

Tracer

To start creating spans, you need a tracer. You can create a tracer by providing the name and version of the library or application doing the instrumentation:

php
use OpenTelemetry\API\Common\Instrumentation\Globals;

// Option 1: Using Uptrace
$tracerProvider = $uptrace->createTracerProvider();
$tracer = $tracerProvider->getTracer('app_or_package_name', '1.0.0');

// Option 2: Using global tracer provider
$tracer = Globals::tracerProvider()->getTracer('app_or_package_name', '1.0.0');

You can have as many tracers as you want, but usually you need only one tracer per application or library. Later, you can use tracer names to identify the instrumentation that produces the spans.

Tracer Configuration

When creating a tracer, you can provide additional metadata:

php
$tracer = Globals::tracerProvider()->getTracer(
    'app_or_package_name',    // Instrumentation name
    '1.0.0',                  // Instrumentation version
    'https://example.com/schema',  // Schema URL (optional)
    ['library.language' => 'php']  // Attributes (optional)
);

Creating Spans

Once you have a tracer, creating spans is straightforward:

php
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;

// Create a span with name "operation-name" and kind="server"
$span = $tracer->spanBuilder('operation-name')
    ->setSpanKind(SpanKind::KIND_SERVER)
    ->startSpan();
$spanScope = $span->activate();

try {
    doSomeWork();
} finally {
    // Always end the span when the operation we are measuring is complete
    $spanScope->detach();
    $span->end();
}

Span Kinds

Specify the type of span using span kinds:

php
use OpenTelemetry\API\Trace\SpanKind;

// For incoming requests (server-side)
$span = $tracer->spanBuilder('handle-request')
    ->setSpanKind(SpanKind::KIND_SERVER)
    ->startSpan();

// For outgoing requests (client-side)
$span = $tracer->spanBuilder('http-request')
    ->setSpanKind(SpanKind::KIND_CLIENT)
    ->startSpan();

// For async operations (producer/consumer)
$span = $tracer->spanBuilder('publish-message')
    ->setSpanKind(SpanKind::KIND_PRODUCER)
    ->startSpan();

// For internal operations (default)
$span = $tracer->spanBuilder('internal-operation')
    ->setSpanKind(SpanKind::KIND_INTERNAL)
    ->startSpan();

Span Builder Options

Configure spans before starting them:

php
use OpenTelemetry\API\Trace\SpanKind;

$span = $tracer->spanBuilder('complex-operation')
    ->setSpanKind(SpanKind::KIND_INTERNAL)
    ->setParent($parentContext)  // Set explicit parent
    ->setStartTimestamp(microtime(true) * 1000000) // Custom start time
    ->addLink($linkedSpanContext)  // Link to other spans
    ->setAttribute('operation.type', 'batch')  // Set initial attributes
    ->startSpan();

Adding Span Attributes

To record contextual information, you can annotate spans with attributes. For example, an HTTP endpoint may have attributes such as http.method = GET and http.route = /projects/:id.

php
// Check if span is being recorded to avoid expensive computations
if ($span->isRecording()) {
    $span->setAttribute('http.method', 'GET');
    $span->setAttribute('http.route', '/projects/:id');
    $span->setAttribute('http.status_code', 200);
    $span->setAttribute('user.id', $userId);
}

Setting Multiple Attributes

You can set multiple attributes at once:

php
$span->setAttributes([
    'http.method' => 'POST',
    'http.route' => '/api/users',
    'http.status_code' => 201,
    'user.role' => 'admin',
    'request.size' => strlen($requestBody)
]);

Semantic Conventions

Use semantic conventions for consistent attribute naming. Install the semantic conventions package:

bash
composer require open-telemetry/sem-conv

Then use predefined attribute names:

php
use OpenTelemetry\SemConv\TraceAttributes;

$span->setAttributes([
    TraceAttributes::HTTP_METHOD => 'GET',
    TraceAttributes::HTTP_ROUTE => '/projects/:id',
    TraceAttributes::HTTP_STATUS_CODE => 200,
    TraceAttributes::USER_ID => $userId
]);

Adding Span Events

You can annotate spans with events that represent significant moments during the span's lifetime:

php
// Simple event
$span->addEvent('cache.miss');

// Event with attributes
$span->addEvent('user.login', [
    'user.id' => '12345',
    'user.role' => 'admin',
    'login.method' => 'oauth'
]);

// Event with timestamp
$span->addEvent('database.query.start', [
    'db.statement' => 'SELECT * FROM users WHERE id = ?'
], microtime(true) * 1000000);

Logging with Events

Events are particularly useful for structured logging:

php
$span->addEvent('log', [
    'log.severity' => 'error',
    'log.message' => 'User not found',
    'enduser.id' => '123',
    'error.code' => 'USER_NOT_FOUND'
]);

Setting Span Status

Use status codes to indicate the outcome of operations:

php
use OpenTelemetry\API\Trace\StatusCode;

// Successful operation (default)
$span->setStatus(StatusCode::STATUS_OK);

// Operation with error
$span->setStatus(StatusCode::STATUS_ERROR, 'Database connection failed');

// Unset status (let the system decide)
$span->setStatus(StatusCode::STATUS_UNSET);

Status with Exception Handling

php
try {
    performDatabaseOperation();
    $span->setStatus(StatusCode::STATUS_OK);
} catch (\Exception $exc) {
    $span->setStatus(StatusCode::STATUS_ERROR, $exc->getMessage());
    $span->recordException($exc);
    throw $exc;
}

Recording Exceptions

OpenTelemetry provides a convenient method to record exceptions:

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

Exception with Additional Context

php
try {
    connectToDatabase($host, $port);
} catch (\PDOException $exc) {
    $span->recordException($exc, [
        'db.host' => $host,
        'db.port' => $port,
        'retry.count' => $retryCount
    ]);
    $span->setStatus(StatusCode::STATUS_ERROR, 'Database connection failed');
    throw $exc;
}

Current Span and Context

OpenTelemetry stores the active span in a context and saves the context in pluggable context storage. You can nest contexts inside each other, and OpenTelemetry will automatically activate the parent span context when you end the span.

Activating Spans

php
// Create and activate a span
$span = $tracer->spanBuilder('operation-name')->startSpan();
$spanScope = $span->activate();

// The span is now active and available to child operations
performChildOperations();

// Always detach to restore the previous context
$spanScope->detach();
$span->end();

Getting the Current Span

php
use OpenTelemetry\API\Trace\Span;

// Get the currently active span
$currentSpan = Span::getCurrent();

if ($currentSpan->isRecording()) {
    $currentSpan->setAttribute('additional.info', 'added from nested function');
}

Context Nesting

Contexts can be nested to create parent-child relationships:

php
// Parent span
$parentSpan = $tracer->spanBuilder('parent-operation')->startSpan();
$parentScope = $parentSpan->activate();

// Child span - automatically becomes a child of the parent
$childSpan = $tracer->spanBuilder('child-operation')->startSpan();
$childScope = $childSpan->activate();

// Perform child work
doChildWork();

// Clean up in reverse order
$childScope->detach();
$childSpan->end();

$parentScope->detach();
$parentSpan->end();

Advanced Span Features

Link spans to show relationships between operations:

php
use OpenTelemetry\API\Trace\SpanContext;

// Create a span with links to other operations
$span = $tracer->spanBuilder('batch-process')
    ->addLink($relatedSpanContext, ['link.type' => 'follows'])
    ->addLink($anotherSpanContext, ['link.type' => 'caused_by'])
    ->startSpan();

Custom Start and End Times

Set explicit timestamps for spans:

php
$startTime = microtime(true) * 1000000; // microseconds

$span = $tracer->spanBuilder('timed-operation')
    ->setStartTimestamp($startTime)
    ->startSpan();

// Perform operation
doWork();

// End with custom timestamp
$endTime = microtime(true) * 1000000;
$span->end($endTime);

Span Name Updates

Update span names based on operation results:

php
$span = $tracer->spanBuilder('dynamic-operation')->startSpan();

try {
    $result = performOperation();
    $span->updateName('successful-operation');
} catch (\Exception $e) {
    $span->updateName('failed-operation');
    throw $e;
}

Best Practices

Error Handling Pattern

Use try-finally blocks to ensure proper cleanup:

php
$span = $tracer->spanBuilder('operation')->startSpan();
$scope = $span->activate();

try {
    performOperation();
    $span->setStatus(StatusCode::STATUS_OK);
} catch (\Exception $e) {
    $span->recordException($e);
    $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
    throw $e;
} finally {
    $scope->detach();
    $span->end();
}

Scope Management Helper

Create a helper function for automatic scope management:

php
function withSpan(string $name, callable $operation, ?SpanKind $kind = null) {
    global $tracer;

    $builder = $tracer->spanBuilder($name);
    if ($kind !== null) {
        $builder->setSpanKind($kind);
    }

    $span = $builder->startSpan();
    $scope = $span->activate();

    try {
        $result = $operation($span);
        $span->setStatus(StatusCode::STATUS_OK);
        return $result;
    } catch (\Exception $e) {
        $span->recordException($e);
        $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
        throw $e;
    } finally {
        $scope->detach();
        $span->end();
    }
}

// Usage
$result = withSpan('database-query', function($span) {
    $span->setAttribute('db.statement', 'SELECT * FROM users');
    return DB::select('SELECT * FROM users');
}, SpanKind::KIND_CLIENT);

Attribute Optimization

Only set attributes when the span is being recorded:

php
if ($span->isRecording()) {
    $expensiveAttribute = calculateExpensiveValue();
    $span->setAttribute('expensive.attribute', $expensiveAttribute);
}

Span Naming

Use descriptive, low-cardinality span names:

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

// Bad: High cardinality
$span = $tracer->spanBuilder("GET /users/$userId")->startSpan();

Integration Examples

HTTP Client Instrumentation

php
function makeHttpRequest(string $url, array $options = []): array {
    $span = $tracer->spanBuilder('HTTP ' . ($options['method'] ?? 'GET'))
        ->setSpanKind(SpanKind::KIND_CLIENT)
        ->startSpan();

    $scope = $span->activate();

    try {
        if ($span->isRecording()) {
            $span->setAttributes([
                TraceAttributes::HTTP_METHOD => $options['method'] ?? 'GET',
                TraceAttributes::HTTP_URL => $url,
                TraceAttributes::HTTP_USER_AGENT => 'MyApp/1.0'
            ]);
        }

        $response = Http::request($url, $options);

        if ($span->isRecording()) {
            $span->setAttribute(TraceAttributes::HTTP_STATUS_CODE, $response['status']);
        }

        return $response;
    } catch (\Exception $e) {
        $span->recordException($e);
        $span->setStatus(StatusCode::STATUS_ERROR, 'HTTP request failed');
        throw $e;
    } finally {
        $scope->detach();
        $span->end();
    }
}

Database Query Instrumentation

php
function executeQuery(string $query, array $params = []): array {
    $span = $tracer->spanBuilder('db.query')
        ->setSpanKind(SpanKind::KIND_CLIENT)
        ->startSpan();

    $scope = $span->activate();

    try {
        if ($span->isRecording()) {
            $span->setAttributes([
                TraceAttributes::DB_SYSTEM => 'mysql',
                TraceAttributes::DB_STATEMENT => $query,
                TraceAttributes::DB_NAME => 'app_database'
            ]);
        }

        $start = microtime(true);
        $result = DB::select($query, $params);
        $duration = microtime(true) - $start;

        if ($span->isRecording()) {
            $span->setAttributes([
                'db.rows_affected' => count($result),
                'db.duration' => $duration
            ]);
        }

        return $result;
    } catch (\Exception $e) {
        $span->recordException($e);
        $span->setStatus(StatusCode::STATUS_ERROR, 'Database query failed');
        throw $e;
    } finally {
        $scope->detach();
        $span->end();
    }
}

Performance Considerations

Conditional Instrumentation

For high-performance scenarios, consider conditional instrumentation:

php
function conditionalInstrumentation(callable $operation, bool $enableTracing = true) {
    if (!$enableTracing) {
        return $operation();
    }

    return withSpan('conditional-operation', $operation);
}

Sampling Awareness

Check if spans are being sampled to avoid unnecessary work:

php
$span = $tracer->spanBuilder('operation')->startSpan();

// Only do expensive operations if the span is being recorded
if ($span->isRecording()) {
    $span->setAttribute('expensive.computation', computeExpensiveValue());
}

OpenTelemetry APM

Uptrace is a OpenTelemetry APM that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks.

Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.

In just a few minutes, you can try Uptrace by visiting the cloud demo (no login required) or running it locally with Docker. The source code is available on GitHub.

What's Next?

Now that you understand the OpenTelemetry PHP Tracing API, explore these related topics: