OpenTelemetry
Laravel
Metrics

Metrics

The OTel auto-instrumentation captures default request, DB, Redis, and outbound-HTTP metrics automatically (http.server.request.duration, http.client.request.duration, db.client.operation.duration). Queued jobs are traced as spans but emit no metrics, so build your own queue counters with the Meter facade if you want them. The same facade covers any other custom application metric.

The Meter Facade

keepsuit/laravel-opentelemetry ships a Meter facade that gives you direct access to all OTel instrument types. Instruments are cached by name, so calling Meter::counter('orders.created') twice returns the same counter. It is safe to call from anywhere.

use Keepsuit\LaravelOpenTelemetry\Facades\Meter;
 
$counter = Meter::counter('orders.created', 'orders', 'Total orders created');
$counter->add(1);

Supported Instruments

MethodUse case
Meter::counter($name, $unit, $description)Monotonic counter (only goes up)
Meter::upDownCounter(...)Counter that can go up or down
Meter::observableCounter(...)Counter whose value is read via callback at export time
Meter::observableUpDownCounter(...)Up/down counter read via callback
Meter::gauge(...)Synchronous gauge: record a value at a point in time
Meter::observableGauge(...)Gauge read via callback at export time
Meter::histogram($name, $unit, $description)Distribution of values (durations, sizes)

Counter

Counters track cumulative values that only go up (total orders, requests processed):

use Keepsuit\LaravelOpenTelemetry\Facades\Meter;
 
$orderCounter = Meter::counter('orders.created', 'orders', 'Total orders created');
 
// Increment by 1
$orderCounter->add(1);
 
// Increment with attributes
$orderCounter->add(1, ['plan' => 'pro', 'region' => 'eu']);

Histogram

Histograms track distributions of values (response times, payload sizes):

use Keepsuit\LaravelOpenTelemetry\Facades\Meter;
 
$durationHistogram = Meter::histogram('order.processing_ms', 'ms', 'Order processing time');
 
$start = hrtime(true);
$this->processOrder($order);
$durationMs = (hrtime(true) - $start) / 1e6;
 
$durationHistogram->record($durationMs, ['plan' => $order->plan]);

TracePath converts histograms into average and count metrics (see OTel metrics).

Gauge

Gauges track point-in-time values that can go up or down (queue depth, active connections):

use Keepsuit\LaravelOpenTelemetry\Facades\Meter;
 
// Synchronous: record a value when you know it
$gauge = Meter::gauge('cache.size_mb', 'MB', 'Cache memory usage');
$gauge->record(42.3);
 
// Observable: value is read by a callback at export time
Meter::observableGauge('queue.depth', 'items', 'Pending jobs in the default queue')
    ->observe(function ($observer) {
        $observer->observe(\Queue::size('default'));
    });

Register observable instruments once, at boot. The observable instruments take a callback that the SDK invokes at export time. Registering one per request re-adds the callback every iteration in long-lived processes (Octane, Horizon, queue:work). Put them in a service provider's boot():

// app/Providers/AppServiceProvider.php
use Keepsuit\LaravelOpenTelemetry\Facades\Meter;
 
public function boot(): void
{
    Meter::observableGauge('queue.depth', 'items', 'Pending jobs in the default queue')
        ->observe(fn ($observer) => $observer->observe(\Queue::size('default')));
}

The synchronous instruments (counter, upDownCounter, gauge, histogram) are cached by name and safe to call from anywhere, including controllers and jobs.

Batch Observation

When several observable instruments share an expensive data source, use batchObserve so the source is only consulted once per export cycle. Like the other observable instruments, register this once at boot:

// app/Providers/AppServiceProvider.php, inside boot()
use Keepsuit\LaravelOpenTelemetry\Facades\Meter;
use OpenTelemetry\API\Metrics\ObserverInterface;
 
Meter::batchObserve([
    Meter::observableCounter('usage',    description: 'count of items used'),
    Meter::observableGauge('pressure',   description: 'force per unit area'),
], function (ObserverInterface $usageObserver, ObserverInterface $pressureObserver): void {
    // Swap this line for your own lookup. The point of batchObserve is that it
    // runs once per export cycle instead of once per instrument.
    [$usage, $pressure] = [\Queue::size('default'), memory_get_usage(true) / 1048576];
 
    $usageObserver->observe($usage);
    $pressureObserver->observe($pressure);
});

Use Cases

Business Metrics

use Keepsuit\LaravelOpenTelemetry\Facades\Meter;
 
class PaymentService
{
    public function processPayment(Order $order): void
    {
        $this->gateway->charge($order->total);
 
        Meter::counter('payments.revenue', 'usd', 'Total revenue')
            ->add($order->total, ['plan' => $order->plan]);
    }
}
 
class RegisterController
{
    public function store(Request $request)
    {
        $user = $this->userService->create($request->validated());
 
        Meter::counter('users.signups', 'users')->add(1);
 
        return response()->json($user, 201);
    }
}

Performance Metrics

use Keepsuit\LaravelOpenTelemetry\Facades\Meter;
 
public function fetchExternalData(string $endpoint): array
{
    $latency = Meter::histogram('external_api.latency_ms', 'ms', 'External API call duration');
 
    $start = hrtime(true);
    try {
        return \Http::get($endpoint)->json();
    } finally {
        $durationMs = (hrtime(true) - $start) / 1e6;
        $latency->record($durationMs, ['endpoint' => $endpoint]);
    }
}

Resource Metrics

// app/Providers/AppServiceProvider.php, inside boot()
use Keepsuit\LaravelOpenTelemetry\Facades\Meter;
 
Meter::observableGauge('redis.connected_clients', 'connections', 'Active Redis connections')
    ->observe(function ($observer) {
        $info = \Redis::connection()->client('info');
        $observer->observe((int) ($info['connected_clients'] ?? 0));
    });

Temporality

The OTLP exporter supports a preferred temporality (Delta vs Cumulative) for exported metrics. Set it via env:

OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=Delta

Leave it unset to use the SDK default.

Metric Naming Conventions

Use dot-separated names for organization:

// Good
Meter::counter('orders.created');
Meter::histogram('db.query_ms');
Meter::counter('cache.hits');
 
// Bad
Meter::counter('created');     // Too vague
Meter::counter('orderCount');  // Inconsistent style