Skip to content
Mehedi Hasan Sagor

// blog

Observability in Laravel: Logging, Metrics, and Tracing for Production‑Ready Apps

Mehedi Hasan Sagor Oct 8, 2026 6 min read
Observability in Laravel: Logging, Metrics, and Tracing for Production‑Ready Apps

Why Observability Matters for Production Laravel Apps

When a startup scales from a handful of users to thousands, the cost of a silent failure grows dramatically. Traditional debugging—reading stack traces after the fact—doesn't cut it. Observability gives you three lenses into a running system:

  1. Logs – the raw, time‑ordered narrative of what happened.
  2. Metrics – quantitative signals (request latency, error rates) that you can chart and alert on.
  3. Traces – a view of how a request flows through services, database calls, and queues.

Laravel ships with a solid logging foundation, but out‑of‑the‑box you miss structured logs, centralized aggregation, and correlation IDs. Adding metrics and tracing requires a few extra packages, but the payoff is faster incident response and data‑driven performance tuning.

1. Structured Logging with Monolog Channels

Laravel uses Monolog under the hood. By default it writes plain text to storage/logs/laravel.log. For production you want JSON logs that can be indexed by ELK, Loki, or CloudWatch.

// config/logging.php
return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['json', 'stderr'],
            'ignore_exceptions' => false,
        ],
        'json' => [
            'driver' => 'single',
            'path' => storage_path('logs/laravel.json'),
            'level' => env('LOG_LEVEL', 'debug'),
            'formatter' => Monolog\Formatter\JsonFormatter::class,
        ],
        'stderr' => [
            'driver' => 'errorlog',
            'level' => env('LOG_LEVEL', 'debug'),
        ],
    ],
];

Adding Context Automatically

A common source of noise is missing request identifiers. Laravel's middleware can inject a request_id into every log entry.

// app/Http/Middleware/RequestId.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class RequestId
{
    public function handle(Request $request, Closure $next)
    {
        $requestId = (string) Str::uuid();
        $request->headers->set('X-Request-Id', $requestId);
        // Make it available to all log calls
        logger()->withContext(['request_id' => $requestId]);
        return $next($request);
    }
}

Register the middleware in app/Http/Kernel.php under the web and api groups. Now every Log::info(), Log::error(), etc., will contain request_id, enabling you to stitch together logs across services.

2. Real‑Time Metrics with Prometheus or StatsD

Metrics give you a quantitative health check. Laravel doesn’t ship with a metrics client, but the community package spatie/laravel-metrics (or promphp/prometheus_client_php for Prometheus) integrates cleanly.

Installing Prometheus Client

composer require promphp/prometheus_client_php

Create a service provider to expose a /metrics endpoint:

// app/Providers/PrometheusServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Prometheus\CollectorRegistry;
use Prometheus\RenderTextFormat;

class PrometheusServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(CollectorRegistry::class, function () {
            return new CollectorRegistry(new \Prometheus\Storage\InMemory());
        });
    }

    public function boot()
    {
        \Route::get('/metrics', function (CollectorRegistry $registry) {
            $renderer = new RenderTextFormat();
            $metrics = $registry->getMetricFamilySamples();
            return response($renderer->render($metrics))
                ->header('Content-Type', $renderer->getContentType());
        });
    }
}

Instrumenting Requests

Add a middleware that records latency and status codes:

// app/Http/Middleware/PrometheusMetrics.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Prometheus\CollectorRegistry;

class PrometheusMetrics
{
    protected $registry;
    public function __construct(CollectorRegistry $registry)
    {
        $this->registry = $registry;
    }

    public function handle(Request $request, Closure $next)
    {
        $start = microtime(true);
        $response = $next($request);
        $duration = microtime(true) - $start;

        $histogram = $this->registry->getOrRegisterHistogram(
            'http', 'request_duration_seconds', ['method', 'uri', 'status'], [0.005, 0.01, 0.025, 0.05, 0.1, 0.5, 1, 2, 5]
        );
        $histogram->observe($duration, [$request->method(), $request->path(), $response->getStatusCode()]);

        return $response;
    }
}

Add the middleware to the global stack. Your Prometheus server can now scrape /metrics and you can set alerts on latency or 5xx error rates.

3. Distributed Tracing with OpenTelemetry and Sentry

When a request touches the database, a queue worker, and an external API, tracing helps you see where latency accumulates.

Installing OpenTelemetry

composer require open-telemetry/opentelemetry "^1.0"
composer require open-telemetry/exporter-otlp-http

Create a singleton tracer in a service provider:

// app/Providers/TracingServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\Exporter\OtlpHttp\Exporter;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;

class TracingServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('tracer', function () {
            $exporter = new Exporter('http://otel-collector:4318/v1/traces');
            $processor = new SimpleSpanProcessor($exporter);
            $provider = new TracerProvider($processor);
            return $provider->getTracer('laravel-app');
        });
    }
}

Tracing a Request

Wrap the request handling in a span via middleware:

// app/Http/Middleware/TraceRequest.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class TraceRequest
{
    public function handle(Request $request, Closure $next)
    {
        $tracer = app('tracer');
        $span = $tracer->spanBuilder($request->method() . ' ' . $request->path())
            ->setAttribute('http.method', $request->method())
            ->setAttribute('http.url', $request->fullUrl())
            ->startSpan();
        $scope = $span->activate();
        try {
            $response = $next($request);
            $span->setAttribute('http.status_code', $response->getStatusCode());
            return $response;
        } finally {
            $span->end();
            $scope->detach();
        }
    }
}

Add the middleware early in the stack so it captures downstream calls.

Correlating with Sentry

Sentry already captures errors and performance data. By installing the Sentry Laravel SDK, you get automatic trace IDs that match OpenTelemetry spans.

composer require sentry/sentry-laravel

In config/sentry.php enable traces_sample_rate (e.g., 0.2 for 20% of requests). The SDK will read the sentry-trace header generated by the OpenTelemetry middleware and link logs, errors, and spans together in the Sentry UI.

4. Putting It All Together: CI/CD, Alerts, and Documentation

CI Integration

  1. Static analysis – Run phpstan and larastan in CI to catch type errors before they hit production.
  2. Log format test – Add a simple test that asserts the JSON log contains request_id.
  3. Metrics sanity check – After deployment, hit the /metrics endpoint and verify required metric families exist.

Alerting

  • Latency – Alert if the 95th percentile of http_request_duration_seconds for /api/* exceeds your SLA (e.g., 300 ms).
  • Error rate – Alert on a sudden rise in http_requests_total with status 5xx.
  • Trace anomalies – Use OpenTelemetry’s trace_id to trigger alerts when a specific span exceeds a threshold (e.g., DB query > 200 ms).

Documentation for the Team

Create a README-OBSERVABILITY.md in the repo that lists:

  • Log aggregation endpoint (e.g., Loki URL) and required fields.
  • Prometheus scrape target and common alert rules.
  • How to view traces in Jaeger or Sentry.
  • A checklist for new services: add middleware, register metrics, write unit tests for logging context.

Keeping this documentation in code ensures new hires or contractors can get observability right from day one.

Takeaway

Observability is not a bolt‑on; it’s a core part of a production Laravel app. By switching to structured JSON logs, exposing Prometheus metrics, and wiring OpenTelemetry tracing (with Sentry as a safety net), you gain a unified view of health, performance, and failures. The upfront effort is a few hours of setup, but the long‑term payoff is faster mean‑time‑to‑detect, data‑driven scaling decisions, and confidence when you push new features.

Implement the three pillars today, embed them in your CI pipeline, and you’ll notice fewer firefighting sessions and more time building value for your users.

  • Laravel
  • Observability
  • Logging
  • Metrics
  • Tracing