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
| Method | Use 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'sboot():// 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=DeltaLeave 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