# OpenTelemetry PHP Metrics API

> Collect PHP application metrics with the OpenTelemetry Meter API, configure instruments, and forward data to Uptrace dashboards.

![undefined](/devicon/php-original.svg)This document teaches you how to use the OpenTelemetry PHP Metrics API to measure application performance with metrics.

## Prerequisites

<partial path="otel-prereq-php">



</partial>

If you are not familiar with metrics terminology such as timeseries or additive/synchronous/asynchronous instruments, read the introduction to [OpenTelemetry Metrics](/opentelemetry/metrics) first.

## 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](/opentelemetry/metrics#instruments) to measure performance. The simplest [Counter](/opentelemetry/metrics#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:

- [Learn about OpenTelemetry PHP Tracing API](/get/opentelemetry-php/tracing) - Learn about distributed tracing
- [Learn about OpenTelemetry PHP Logs](/get/opentelemetry-php/logs) - Send logs to Uptrace
- [Learn about OpenTelemetry PHP Resource Detectors](/get/opentelemetry-php/resources) - Automatic resource detection
- [Context Propagation](/get/opentelemetry-php/propagation) - Understanding context in distributed systems
- [Learn about OpenTelemetry PHP Sampling](/get/opentelemetry-php/sampling) - Optimize performance with sampling
- [Browse more examples](https://github.com/uptrace/uptrace-php/tree/master/example) - Practical implementation examples
