OpenTelemetry PHP Metrics API
This document teaches you how to use the OpenTelemetry PHP Metrics API to measure application performance with metrics. To learn how to install and configure the OpenTelemetry PHP SDK, see Getting started with OpenTelemetry PHP.
If you are not familiar with metrics terminology such as timeseries or additive/synchronous/asynchronous instruments, read the introduction to OpenTelemetry Metrics first.
Prerequisites
Before using the Metrics API, ensure you have the required packages installed:
composer require open-telemetry/sdk
composer require open-telemetry/exporter-otlp
composer require open-telemetry/transport-grpc # For gRPC transport
Getting Started
To get started with metrics, you need to create a MeterProvider
and use it to create a Meter
:
<?php
use OpenTelemetry\SDK\Metrics\MeterProvider;
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\Contrib\Otlp\MetricExporter;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
// Configure resource information
$resource = ResourceInfoFactory::emptyResource()->merge(
ResourceInfo::create(Attributes::create([
'service.name' => 'my-php-service',
'service.version' => '1.0.0',
'deployment.environment' => 'production'
])),
ResourceInfoFactory::defaultResource()
);
// Set up the metric exporter
$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()
);
// Create the meter provider
$meterProvider = MeterProvider::builder()
->setResource($resource)
->addReader($reader)
->build();
// Create a meter
$meter = $meterProvider->getMeter('app_or_package_name', '1.0.0');
Simple Counter Example
Using the meter, you can create instruments to measure performance. The simplest Counter instrument looks like this:
$counter = $meter->createCounter(
'requests.total',
'request',
'Total number of requests processed'
);
for ($i = 0; $i < 1000; $i++) {
$counter->add(1, ['status' => 'success']);
if ($i % 10 === 0) {
$reader->collect(); // Force collection for demonstration
}
usleep(100000); // 100ms delay
}
Metric Instruments
OpenTelemetry provides several types of instruments to capture different kinds of measurements. Each instrument serves a specific purpose and has distinct characteristics.
Counter
Counters track values that monotonically increase, representing cumulative totals like the number of requests, errors, or completed tasks.
$httpRequestCounter = $meter->createCounter(
'http.requests.total',
'request',
'Total number of HTTP requests'
);
$errorCounter = $meter->createCounter(
'http.errors.total',
'error',
'Total number of HTTP errors'
);
// Record measurements with attributes
$httpRequestCounter->add(1, [
'method' => 'GET',
'status_code' => '200',
'endpoint' => '/api/users'
]);
$errorCounter->add(1, [
'method' => 'POST',
'status_code' => '500',
'error_type' => 'database_connection'
]);
UpDownCounter
UpDownCounters track values that can both increase and decrease, such as the number of active connections or items in a queue.
$activeConnections = $meter->createUpDownCounter(
'database.connections.active',
'connection',
'Number of active database connections'
);
$queueSize = $meter->createUpDownCounter(
'queue.size',
'item',
'Number of items in the processing queue'
);
// Connection established
$activeConnections->add(1, ['database' => 'users', 'pool' => 'main']);
// Connection closed
$activeConnections->add(-1, ['database' => 'users', 'pool' => 'main']);
// Items added to queue
$queueSize->add(5, ['queue' => 'email', 'priority' => 'high']);
// Items processed from queue
$queueSize->add(-3, ['queue' => 'email', 'priority' => 'high']);
Histogram
Histograms measure the statistical distribution of values, such as request latencies or response sizes, grouping them into buckets.
$requestDuration = $meter->createHistogram(
'http.request.duration',
'ms',
'HTTP request duration in milliseconds'
);
$responseSize = $meter->createHistogram(
'http.response.size',
'byte',
'HTTP response size in bytes'
);
// Measure request duration
$startTime = microtime(true);
// ... handle request ...
$duration = (microtime(true) - $startTime) * 1000; // Convert to milliseconds
$requestDuration->record($duration, [
'method' => 'POST',
'endpoint' => '/api/users',
'status_code' => '201'
]);
// Record response size
$responseSize->record(strlen($responseBody), [
'content_type' => 'application/json',
'compressed' => 'gzip'
]);
Observable Gauge
Observable Gauges capture current values asynchronously, such as CPU usage, memory consumption, or temperature readings.
use OpenTelemetry\API\Metrics\ObserverInterface;
$memoryUsage = $meter->createObservableGauge(
'process.memory.usage',
'byte',
'Current memory usage of the process'
);
$cpuUsage = $meter->createObservableGauge(
'process.cpu.usage',
'percent',
'Current CPU usage percentage'
);
// Register callback for memory usage
$memoryUsage->observe(function (ObserverInterface $observer) {
$realMemory = memory_get_usage(true);
$peakMemory = memory_get_peak_usage(true);
$observer->observe($realMemory, ['type' => 'real']);
$observer->observe($peakMemory, ['type' => 'peak']);
});
// Register callback for CPU usage
$cpuUsage->observe(function (ObserverInterface $observer) {
$cpuUsage = sys_getloadavg()[0] * 100; // Simplified example
$observer->observe($cpuUsage, ['core' => 'average']);
});
Observable Counter
Observable Counters report monotonically increasing values asynchronously, useful for system-level metrics like total bytes read or CPU time.
$totalBytesRead = $meter->createObservableCounter(
'disk.bytes.read',
'byte',
'Total bytes read from disk'
);
$totalCpuTime = $meter->createObservableCounter(
'process.cpu.time',
'second',
'Total CPU time consumed by the process'
);
$totalBytesRead->observe(function (ObserverInterface $observer) {
$stats = disk_total_space('/') - disk_free_space('/');
$observer->observe($stats, ['device' => 'sda1']);
});
Observable UpDownCounter
Observable UpDownCounters track additive values that can increase or decrease, measured asynchronously.
$heapSize = $meter->createObservableUpDownCounter(
'process.heap.size',
'byte',
'Current heap size of the process'
);
$heapSize->observe(function (ObserverInterface $observer) {
$heapSize = getProcessHeapSize(); // Your custom function
$observer->observe($heapSize, ['gc_generation' => 'total']);
});
Working with Attributes
Attributes provide contextual information that makes metrics more useful for analysis and filtering.
Adding Attributes to Measurements
$requestCounter = $meter->createCounter(
'api.requests.total',
'request',
'Total API requests'
);
// Add detailed attributes
$requestCounter->add(1, [
'method' => 'GET',
'endpoint' => '/api/v1/users',
'status_code' => '200',
'user_type' => 'premium',
'region' => 'us-east-1',
'cache_hit' => 'true'
]);
Attribute Best Practices
Use meaningful attributes that provide valuable differentiation without creating excessive cardinality.
// Good: Low cardinality attributes
$httpRequests = $meter->createCounter('http.requests.total');
$httpRequests->add(1, [
'method' => 'GET', // Limited values: GET, POST, etc.
'status_class' => '2xx', // Grouped status codes
'endpoint_type' => 'api' // Categorized endpoints
]);
// Avoid: High cardinality attributes
// $httpRequests->add(1, [
// 'user_id' => '12345', // Unique per user
// 'timestamp' => time(), // Unique per request
// 'session_id' => $sessionId // Unique per session
// ]);
Recording Measurements
Synchronous Measurements
Synchronous instruments are recorded inline with application logic and can be associated with the current context.
$operationCounter = $meter->createCounter('operations.count');
$operationDuration = $meter->createHistogram('operation.duration', 'ms');
$errorCounter = $meter->createCounter('operations.errors');
function performOperation(string $operationType): mixed {
global $operationCounter, $operationDuration, $errorCounter;
$startTime = microtime(true);
try {
// Increment operation counter
$operationCounter->add(1, [
'operation' => $operationType,
'status' => 'started'
]);
// Perform the actual operation
$result = executeOperation($operationType);
// Record successful completion
$operationCounter->add(1, [
'operation' => $operationType,
'status' => 'success'
]);
return $result;
} catch (\Exception $e) {
// Record error
$errorCounter->add(1, [
'operation' => $operationType,
'error_type' => get_class($e),
'error_code' => $e->getCode()
]);
throw $e;
} finally {
// Always record duration
$duration = (microtime(true) - $startTime) * 1000;
$operationDuration->record($duration, [
'operation' => $operationType
]);
}
}
Asynchronous Measurements
Asynchronous instruments use callbacks that are invoked during metric collection.
$systemMetrics = $meter->createObservableGauge(
'system.resources',
'unit',
'System resource utilization'
);
$queueSize = $meter->createObservableUpDownCounter(
'queue.items',
'item',
'Number of items in various queues'
);
// Register callbacks for system metrics
$systemMetrics->observe(function (ObserverInterface $observer) {
// CPU usage
$cpuUsage = getCpuUsage(); // Your implementation
$observer->observe($cpuUsage, [
'resource' => 'cpu',
'unit' => 'percent'
]);
// Memory usage
$memoryUsage = memory_get_usage(true) / 1024 / 1024; // MB
$observer->observe($memoryUsage, [
'resource' => 'memory',
'unit' => 'mb'
]);
// Disk usage
$diskUsage = getDiskUsage(); // Your implementation
$observer->observe($diskUsage, [
'resource' => 'disk',
'unit' => 'percent'
]);
});
// Register callback for queue monitoring
$queueSize->observe(function (ObserverInterface $observer) {
$queues = ['email', 'image_processing', 'notifications'];
foreach ($queues as $queueName) {
$size = getQueueSize($queueName); // Your implementation
$observer->observe($size, ['queue' => $queueName]);
}
});
Practical Examples
HTTP Server Metrics
class HttpMetricsMiddleware {
private $requestCounter;
private $requestDuration;
private $activeRequests;
private $responseSize;
public function __construct($meter) {
$this->requestCounter = $meter->createCounter(
'http.requests.total',
'request',
'Total HTTP requests'
);
$this->requestDuration = $meter->createHistogram(
'http.request.duration',
'ms',
'HTTP request duration'
);
$this->activeRequests = $meter->createUpDownCounter(
'http.requests.active',
'request',
'Active HTTP requests'
);
$this->responseSize = $meter->createHistogram(
'http.response.size',
'byte',
'HTTP response size'
);
}
public function handle($request, $next) {
$startTime = microtime(true);
// Track active requests
$this->activeRequests->add(1, [
'method' => $request->getMethod(),
'route' => $request->getUri()->getPath()
]);
try {
$response = $next($request);
$attributes = [
'method' => $request->getMethod(),
'status_code' => $response->getStatusCode(),
'route' => $request->getUri()->getPath()
];
// Record request count
$this->requestCounter->add(1, $attributes);
// Record response size
$responseBody = $response->getBody()->getContents();
$this->responseSize->record(strlen($responseBody), $attributes);
return $response;
} catch (\Exception $e) {
// Record error
$this->requestCounter->add(1, [
'method' => $request->getMethod(),
'status_code' => '500',
'route' => $request->getUri()->getPath(),
'error' => 'true'
]);
throw $e;
} finally {
// Record duration and decrement active requests
$duration = (microtime(true) - $startTime) * 1000;
$this->requestDuration->record($duration, [
'method' => $request->getMethod(),
'route' => $request->getUri()->getPath()
]);
$this->activeRequests->add(-1, [
'method' => $request->getMethod(),
'route' => $request->getUri()->getPath()
]);
}
}
}
Database Connection Pool Metrics
class DatabaseMetrics {
private $connectionPool;
private $queryDuration;
private $queryCounter;
public function __construct($meter) {
$this->connectionPool = $meter->createObservableGauge(
'database.connections',
'connection',
'Database connection pool status'
);
$this->queryDuration = $meter->createHistogram(
'database.query.duration',
'ms',
'Database query duration'
);
$this->queryCounter = $meter->createCounter(
'database.queries.total',
'query',
'Total database queries'
);
// Register connection pool monitoring
$this->connectionPool->observe(function (ObserverInterface $observer) {
$poolStats = $this->getConnectionPoolStats();
$observer->observe($poolStats['active'], [
'state' => 'active',
'pool' => 'main'
]);
$observer->observe($poolStats['idle'], [
'state' => 'idle',
'pool' => 'main'
]);
$observer->observe($poolStats['max'], [
'state' => 'max',
'pool' => 'main'
]);
});
}
public function recordQuery(string $query, float $duration, bool $success): void {
$attributes = [
'operation' => $this->extractOperation($query),
'success' => $success ? 'true' : 'false'
];
$this->queryCounter->add(1, $attributes);
$this->queryDuration->record($duration * 1000, $attributes); // Convert to ms
}
private function extractOperation(string $query): string {
$query = trim(strtoupper($query));
if (str_starts_with($query, 'SELECT')) return 'SELECT';
if (str_starts_with($query, 'INSERT')) return 'INSERT';
if (str_starts_with($query, 'UPDATE')) return 'UPDATE';
if (str_starts_with($query, 'DELETE')) return 'DELETE';
return 'OTHER';
}
private function getConnectionPoolStats(): array {
// Implementation depends on your database library
return [
'active' => 5,
'idle' => 3,
'max' => 10
];
}
}
Business Metrics
class BusinessMetrics {
private $userRegistrations;
private $orderValue;
private $subscriptionStatus;
public function __construct($meter) {
$this->userRegistrations = $meter->createCounter(
'users.registrations.total',
'user',
'Total user registrations'
);
$this->orderValue = $meter->createHistogram(
'orders.value',
'usd',
'Order value in USD'
);
$this->subscriptionStatus = $meter->createObservableUpDownCounter(
'subscriptions.active',
'subscription',
'Active subscriptions by plan'
);
// Monitor subscription counts
$this->subscriptionStatus->observe(function (ObserverInterface $observer) {
$subscriptions = $this->getSubscriptionCounts();
foreach ($subscriptions as $plan => $count) {
$observer->observe($count, ['plan' => $plan]);
}
});
}
public function recordUserRegistration(string $source, string $plan): void {
$this->userRegistrations->add(1, [
'source' => $source,
'plan' => $plan,
'timestamp' => date('H') // Hour bucket for analysis
]);
}
public function recordOrder(float $value, string $currency, string $category): void {
// Convert to USD for consistent reporting
$usdValue = $this->convertToUsd($value, $currency);
$this->orderValue->record($usdValue, [
'category' => $category,
'currency' => $currency,
'value_range' => $this->getValueRange($usdValue)
]);
}
private function convertToUsd(float $value, string $currency): float {
// Implementation depends on your currency conversion service
return $value; // Simplified
}
private function getValueRange(float $value): string {
if ($value < 10) return 'small';
if ($value < 100) return 'medium';
if ($value < 1000) return 'large';
return 'premium';
}
private function getSubscriptionCounts(): array {
// Implementation depends on your database
return [
'basic' => 1250,
'premium' => 340,
'enterprise' => 45
];
}
}
Configuration and Performance
Metric Reader Configuration
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
// Configure collection interval
$reader = new ExportingReader(
$exporter,
ClockFactory::getDefault(),
60000 // Collect every 60 seconds
);
// Add multiple readers for different export destinations
$meterProvider = MeterProvider::builder()
->setResource($resource)
->addReader($prometheusReader) // Export to Prometheus
->addReader($uptracReader) // Export to Uptrace
->addReader($consoleReader) // Debug output
->build();
Memory Management
For long-running applications, consider memory usage:
// Force metric collection and export
$meterProvider->forceFlush();
// Shutdown cleanly
$meterProvider->shutdown();
Attribute Optimization
Optimize attribute usage to prevent cardinality explosion:
// Good: Limited attribute values
$httpMetrics = $meter->createCounter('http.requests');
$httpMetrics->add(1, [
'method' => 'GET', // ~10 possible values
'status_class' => '2xx', // 5 possible values
'endpoint_category' => 'api' // ~5 possible values
]);
// Total cardinality: 10 × 5 × 5 = 250 series
// Avoid: High cardinality
// $httpMetrics->add(1, [
// 'url' => '/api/users/12345', // Thousands of values
// 'timestamp' => time(), // Infinite values
// 'user_id' => $userId // Thousands of values
// ]);
// This could create millions of metric series!
Environment Variables
Configure metrics behavior using environment variables:
# Metric export settings
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://api.uptrace.dev/v1/metrics
export OTEL_EXPORTER_OTLP_METRICS_HEADERS="uptrace-dsn=YOUR_DSN"
# Collection interval (milliseconds)
export OTEL_METRIC_EXPORT_INTERVAL=60000
# Resource attributes
export OTEL_RESOURCE_ATTRIBUTES="service.name=my-service,service.version=1.0.0"
Use environment variables in your application:
$dsn = getenv('OTEL_EXPORTER_OTLP_METRICS_HEADERS');
$endpoint = getenv('OTEL_EXPORTER_OTLP_METRICS_ENDPOINT');
$serviceName = getenv('OTEL_SERVICE_NAME') ?: 'unknown-service';
Best Practices
Instrument Naming
Follow OpenTelemetry naming conventions:
// Good: Descriptive, hierarchical names
$meter->createCounter('http.requests.total');
$meter->createHistogram('http.request.duration');
$meter->createGauge('system.memory.usage');
// Avoid: Generic or unclear names
$meter->createCounter('requests');
$meter->createHistogram('time');
$meter->createGauge('memory');
Unit Specification
Always specify appropriate units:
$meter->createHistogram('request.duration', 'ms'); // milliseconds
$meter->createGauge('memory.usage', 'byte'); // bytes
$meter->createCounter('requests.total', 'request'); // requests
$meter->createHistogram('file.size', 'byte'); // bytes
$meter->createGauge('temperature', 'Cel'); // Celsius
Error Handling
Handle metric recording errors gracefully:
try {
$counter->add(1, ['status' => 'success']);
} catch (\Exception $e) {
// Log the error but don't let metrics break your application
error_log("Failed to record metric: " . $e->getMessage());
}
Testing Metrics
Create helper functions for testing:
class MetricsTestHelper {
private $meterProvider;
private $meter;
public function __construct() {
// Use in-memory exporter for testing
$exporter = new InMemoryMetricExporter();
$reader = new ExportingReader($exporter);
$this->meterProvider = MeterProvider::builder()
->addReader($reader)
->build();
$this->meter = $this->meterProvider->getMeter('test');
}
public function getMeter() {
return $this->meter;
}
public function getExportedMetrics(): array {
$this->meterProvider->forceFlush();
return $this->exporter->getExportedMetrics();
}
}
What's Next?
Now that you understand the OpenTelemetry PHP Metrics API, explore these related topics:
- Learn about OpenTelemetry PHP Resource Detectors - Automatic resource detection
- Learn about OpenTelemetry PHP Sampling - Optimize performance with sampling
- OpenTelemetry Metrics Concepts - Deep dive into metrics theory
- Browse more examples - Practical implementation examples
- OpenTelemetry PHP Tracing API - Learn about distributed tracing