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:

bash
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
<?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:

php
$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.

php
$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.

php
$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.

php
$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.

php
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.

php
$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.

php
$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

php
$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.

php
// 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.

php
$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.

php
$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

php
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

php
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

php
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

php
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:

php
// Force metric collection and export
$meterProvider->forceFlush();

// Shutdown cleanly
$meterProvider->shutdown();

Attribute Optimization

Optimize attribute usage to prevent cardinality explosion:

php
// 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:

bash
# 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:

php
$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:

php
// 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:

php
$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:

php
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:

php
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: