PHP
Implement OpenTelemetry custom instrumentation for PHP applications to collect traces, metrics, and logs using the PHP OpenTelemetry SDK. This guide covers manual instrumentation for any PHP application, including custom frameworks, legacy codebases, and popular frameworks like Symfony, WordPress, and others.
Overview​
This guide demonstrates how to:
- Set up OpenTelemetry SDK for manual instrumentation
- Create and manage custom spans
- Add attributes, events, and exception tracking
- Implement metrics collection
- Propagate context across service boundaries
- Instrument common PHP patterns and frameworks
Prerequisites​
Before starting, ensure you have:
- PHP 8.0 or later installed
- Composer for dependency management
- Basic understanding of OpenTelemetry concepts (traces, spans, attributes)
Required Packages​
Install the OpenTelemetry SDK and exporters:
composer require open-telemetry/sdk
composer require open-telemetry/exporter-otlp
composer require guzzlehttp/guzzle
For semantic conventions support:
composer require open-telemetry/sem-conv
Traces​
Traces provide a complete picture of request flows through your application, from initial request to final response, including all operations and services involved.
Initialization​
Initialize the TracerProvider and acquire a tracer:
<?php
require 'vendor/autoload.php';
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\SDK\Trace\SpanExporter\ConsoleSpanExporterFactory;
use OpenTelemetry\API\Globals;
// Create a tracer provider
$tracerProvider = new TracerProvider(
new SimpleSpanProcessor(
(new ConsoleSpanExporterFactory())->create()
)
);
// Set as global tracer provider
Globals::registerInitializer(function() use ($tracerProvider) {
return $tracerProvider;
});
// Get a tracer for your application
$tracer = Globals::tracerProvider()->getTracer(
'my-app',
'1.0.0'
);
Production Configuration with OTLP Exporter​
For production, export traces to Scout Collector:
<?php
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\API\Globals;
// Create resource with service information
$resource = ResourceInfoFactory::defaultResource()->merge(
\OpenTelemetry\SDK\Resource\ResourceInfo::create(
Attributes::create([
'service.name' => 'my-php-app',
'service.version' => '1.0.0',
'deployment.environment' => 'production',
])
)
);
// Create OTLP exporter
$transport = (new OtlpHttpTransportFactory())->create(
'http://localhost:4318',
'application/x-protobuf'
);
$exporter = new SpanExporter($transport);
// Create tracer provider with batch processor
$tracerProvider = new TracerProvider(
new BatchSpanProcessor($exporter),
null,
$resource
);
Globals::registerInitializer(function() use ($tracerProvider) {
return $tracerProvider;
});
Note: Ensure your Scout Collector is properly configured to receive trace data at the endpoint specified above.
Creating Spans​
A span represents a single operation within a trace:
$span = $tracer->spanBuilder('operation-name')->startSpan();
// Perform your operation
doSomeWork();
// Always end spans
$span->end();
Using Span Context​
Activate a span to make it current and automatically propagate context:
$span = $tracer->spanBuilder('parent-operation')->startSpan();
$scope = $span->activate();
try {
// Any spans created here will be children of this span
performOperation();
} finally {
$scope->detach();
$span->end();
}
Creating Nested Spans​
Create parent-child span relationships:
function processRequest($tracer) {
$parentSpan = $tracer->spanBuilder('process_request')->startSpan();
$parentScope = $parentSpan->activate();
try {
// Child span 1
$childSpan1 = $tracer->spanBuilder('validate_input')->startSpan();
validateInput();
$childSpan1->end();
// Child span 2
$childSpan2 = $tracer->spanBuilder('fetch_data')->startSpan();
fetchDataFromDatabase();
$childSpan2->end();
// Child span 3
$childSpan3 = $tracer->spanBuilder('process_data')->startSpan();
processData();
$childSpan3->end();
} finally {
$parentScope->detach();
$parentSpan->end();
}
}
Attributes​
Attributes add context to spans as key-value pairs:
Adding Custom Attributes​
$span = $tracer->spanBuilder('database-query')->startSpan();
$span->setAttribute('db.system', 'postgresql');
$span->setAttribute('db.name', 'production');
$span->setAttribute('db.operation', 'SELECT');
$span->setAttribute('query.rows_returned', 42);
// Perform database operation
$results = $db->query('SELECT * FROM users');
$span->end();
Using Semantic Conventions​
Use standardized attribute names for common operations:
use OpenTelemetry\SemConv\TraceAttributes;
$span = $tracer->spanBuilder('http-request')->startSpan();
$span->setAttribute(TraceAttributes::HTTP_METHOD, 'POST');
$span->setAttribute(TraceAttributes::HTTP_URL, 'https://api.example.com/users');
$span->setAttribute(TraceAttributes::HTTP_STATUS_CODE, 201);
$span->setAttribute(TraceAttributes::HTTP_REQUEST_CONTENT_LENGTH, strlen($body));
$span->end();
Events​
Events mark significant moments during a span's lifetime:
$span = $tracer->spanBuilder('order-processing')->startSpan();
$span->addEvent('order_received', [
'order.id' => '12345',
'order.amount' => 99.99,
]);
// Process the order
processOrder($orderId);
$span->addEvent('payment_processed', [
'payment.method' => 'credit_card',
'payment.status' => 'success',
]);
$span->addEvent('order_completed');
$span->end();
Exception Recording​
Capture and record exceptions in spans:
use OpenTelemetry\API\Trace\StatusCode;
$span = $tracer->spanBuilder('risky-operation')->startSpan();
try {
performRiskyOperation();
$span->setStatus(StatusCode::STATUS_OK);
} catch (\Exception $e) {
$span->recordException($e, [
'exception.escaped' => true,
]);
$span->setStatus(
StatusCode::STATUS_ERROR,
$e->getMessage()
);
throw $e;
} finally {
$span->end();
}
Metrics​
Collect custom metrics to track application performance and business KPIs:
Counter​
Track cumulative values that only increase:
use OpenTelemetry\API\Globals;
$meter = Globals::meterProvider()->getMeter('my-app');
$requestCounter = $meter->createCounter(
'http.requests',
'requests',
'Total number of HTTP requests'
);
// Increment counter
$requestCounter->add(1, [
'http.method' => 'GET',
'http.route' => '/api/users',
]);
Histogram​
Record distributions of values:
$requestDuration = $meter->createHistogram(
'http.request.duration',
'milliseconds',
'HTTP request duration'
);
$startTime = hrtime(true);
// Process request
handleRequest();
$duration = (hrtime(true) - $startTime) / 1e6; // Convert to milliseconds
$requestDuration->record($duration, [
'http.method' => 'POST',
'http.status_code' => 200,
]);
UpDownCounter​
Track values that can increase or decrease:
$activeConnections = $meter->createUpDownCounter(
'db.connections.active',
'connections',
'Currently active database connections'
);
// Connection opened
$activeConnections->add(1);
// Connection closed
$activeConnections->add(-1);
Context Propagation​
Propagate trace context across HTTP requests:
Outgoing HTTP Requests​
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
$span = $tracer->spanBuilder('external-api-call')->startSpan();
$scope = $span->activate();
try {
// Get current context
$context = \OpenTelemetry\Context\Context::getCurrent();
// Inject trace context into HTTP headers
$carrier = [];
TraceContextPropagator::getInstance()->inject($carrier, null, $context);
// Make HTTP request with trace headers
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.example.com/data', [
'headers' => $carrier
]);
} finally {
$scope->detach();
$span->end();
}
Incoming HTTP Requests​
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
// Extract context from incoming request headers
$headers = getallheaders();
$context = TraceContextPropagator::getInstance()->extract($headers);
// Start span with extracted context
$span = $tracer->spanBuilder('handle-request')
->setParent($context)
->startSpan();
$scope = $span->activate();
try {
handleRequest();
} finally {
$scope->detach();
$span->end();
}
Framework-Specific Examples​
Symfony Controller​
namespace App\Controller;
use OpenTelemetry\API\Globals;
use Symfony\Component\HttpFoundation\Response;
class UserController
{
private $tracer;
public function __construct()
{
$this->tracer = Globals::tracerProvider()->getTracer('symfony-app');
}
public function index(): Response
{
$span = $this->tracer->spanBuilder('UserController::index')->startSpan();
$scope = $span->activate();
try {
$users = $this->fetchUsers();
$span->setAttribute('user.count', count($users));
return new Response(json_encode($users));
} finally {
$scope->detach();
$span->end();
}
}
}
WordPress Plugin​
use OpenTelemetry\API\Globals;
add_action('init', function() {
$tracer = Globals::tracerProvider()->getTracer('wordpress-plugin');
add_filter('the_content', function($content) use ($tracer) {
$span = $tracer->spanBuilder('process_content')->startSpan();
try {
// Process content
$processed = processContent($content);
$span->setAttribute('content.length', strlen($processed));
return $processed;
} finally {
$span->end();
}
});
});
Plain PHP Application​
<?php
require 'vendor/autoload.php';
require 'config/telemetry.php';
use OpenTelemetry\API\Globals;
$tracer = Globals::tracerProvider()->getTracer('my-app');
// Start request span
$requestSpan = $tracer->spanBuilder('http.request')->startSpan();
$requestScope = $requestSpan->activate();
try {
$requestSpan->setAttribute('http.method', $_SERVER['REQUEST_METHOD']);
$requestSpan->setAttribute('http.url', $_SERVER['REQUEST_URI']);
// Route request
$route = $_GET['route'] ?? 'home';
$routeSpan = $tracer->spanBuilder("route.{$route}")->startSpan();
try {
handleRoute($route);
} finally {
$routeSpan->end();
}
http_response_code(200);
$requestSpan->setAttribute('http.status_code', 200);
} catch (\Exception $e) {
$requestSpan->recordException($e);
$requestSpan->setAttribute('http.status_code', 500);
http_response_code(500);
} finally {
$requestScope->detach();
$requestSpan->end();
}
Best Practices​
1. Always End Spans​
// Good
$span = $tracer->spanBuilder('operation')->startSpan();
try {
doWork();
} finally {
$span->end(); // Always called
}
// Bad - span may not end if exception thrown
$span = $tracer->spanBuilder('operation')->startSpan();
doWork();
$span->end();
2. Use Descriptive Span Names​
// Good
$span = $tracer->spanBuilder('UserRepository::findById')->startSpan();
$span = $tracer->spanBuilder('PaymentService::processPayment')->startSpan();
// Bad
$span = $tracer->spanBuilder('operation')->startSpan();
$span = $tracer->spanBuilder('query')->startSpan();
3. Add Relevant Attributes​
// Good
$span->setAttribute('user.id', $userId);
$span->setAttribute('order.amount', $amount);
$span->setAttribute('cache.hit', true);
// Bad - too verbose or sensitive data
$span->setAttribute('user.password', $password); // Never!
$span->setAttribute('full.sql.query', $query); // May contain sensitive data
4. Detach Scopes Properly​
// Good
$scope = $span->activate();
try {
doWork();
} finally {
$scope->detach(); // Always detach
$span->end();
}
// Bad - scope not detached, causes context pollution
$span->activate();
doWork();
$span->end();
5. Use Batch Processing in Production​
// Production - use BatchSpanProcessor
$tracerProvider = new TracerProvider(
new BatchSpanProcessor($exporter)
);
// Development - use SimpleSpanProcessor for immediate export
$tracerProvider = new TracerProvider(
new SimpleSpanProcessor($exporter)
);
Complete Example​
Here's a complete example of a PHP application with custom instrumentation:
<?php
require 'vendor/autoload.php';
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
// Initialize telemetry
$transport = (new OtlpHttpTransportFactory())->create(
'http://localhost:4318',
'application/x-protobuf'
);
$tracerProvider = new TracerProvider(
new BatchSpanProcessor(new SpanExporter($transport))
);
Globals::registerInitializer(function() use ($tracerProvider) {
return $tracerProvider;
});
$tracer = Globals::tracerProvider()->getTracer('my-app', '1.0.0');
$meter = Globals::meterProvider()->getMeter('my-app');
// Create metrics
$requestCounter = $meter->createCounter('requests.total', 'requests');
$requestDuration = $meter->createHistogram('requests.duration', 'ms');
// Handle request
$requestSpan = $tracer->spanBuilder('http.request')->startSpan();
$requestScope = $requestSpan->activate();
$startTime = hrtime(true);
try {
$requestSpan->setAttribute('http.method', $_SERVER['REQUEST_METHOD']);
$requestSpan->setAttribute('http.url', $_SERVER['REQUEST_URI']);
// Business logic
$result = processRequest();
$requestSpan->setStatus(StatusCode::STATUS_OK);
$statusCode = 200;
} catch (\Exception $e) {
$requestSpan->recordException($e);
$requestSpan->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
$statusCode = 500;
} finally {
$duration = (hrtime(true) - $startTime) / 1e6;
$requestSpan->setAttribute('http.status_code', $statusCode);
$requestCounter->add(1, ['status' => $statusCode]);
$requestDuration->record($duration, ['status' => $statusCode]);
$requestScope->detach();
$requestSpan->end();
// Ensure spans are flushed
$tracerProvider->shutdown();
}
References​
- Official OpenTelemetry PHP Documentation
- OpenTelemetry PHP GitHub
- OpenTelemetry Semantic Conventions
Related Guides​
- Laravel Auto-Instrumentation - Automatic tracing for Laravel applications
- Docker Compose Setup - Set up Scout Collector for local development
- Creating Alerts - Set up alerts for your telemetry data