OpenTelemetry PHP Tracing API
This document teaches you how to use the OpenTelemetry PHP API. To learn how to install and configure the OpenTelemetry PHP SDK, see Getting started with OpenTelemetry PHP.
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:
composer require uptrace/uptrace
Quickstart
Step 1. Let's instrument the following function:
function insertUser(string $name, string $email) {
DB::insert('insert into users (name, email) values (?, ?)', [$name, $email]);
}
Step 2. Wrap the operation with a span:
$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:
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:
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:
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:
$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:
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:
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:
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
.
// 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:
$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:
composer require open-telemetry/sem-conv
Then use predefined attribute names:
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:
// 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:
$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:
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
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:
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
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
// 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
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:
// 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
Span Links
Link spans to show relationships between operations:
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:
$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:
$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:
$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:
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:
if ($span->isRecording()) {
$expensiveAttribute = calculateExpensiveValue();
$span->setAttribute('expensive.attribute', $expensiveAttribute);
}
Span Naming
Use descriptive, low-cardinality span names:
// 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
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
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:
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:
$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:
- Learn about OpenTelemetry PHP Metrics API - Collect and export metrics
- Learn about OpenTelemetry PHP Resource Detectors - Automatic resource detection
- Browse more examples - Practical implementation examples
- Context Propagation - Understanding context in distributed systems