Sitelet https://maple.dev/docs/guides/instrumentation-laravel/
Skip to content
Maple Docs
Open app
Browse the docs
On this page

Laravel instrumentation

Instrument a Laravel application with OpenTelemetry and send traces, logs, and metrics to Maple.

This guide instruments a Laravel application with the keepsuit/laravel-opentelemetry package, which hooks OpenTelemetry into Laravel’s HTTP kernel, database, queue, cache and logging, and sends traces, logs and metrics to Maple.

To have a coding agent do this setup, use the maple-onboard skill, and maple-audit to check an existing setup.

Prerequisites

  • PHP 8.2+ and Laravel 11.31 or later (the versions the current package release supports)
  • Composer
  • An ingest key from Settings → Ingestion in Maple. Use the private key (maple_sk_…) for server applications.

Install

composer require keepsuit/laravel-opentelemetry

Publish the config file so you can turn individual instrumentations on and off later:

php artisan vendor:publish \
  --provider="Keepsuit\LaravelOpenTelemetry\LaravelOpenTelemetryServiceProvider" \
  --tag="opentelemetry-config"

The package pulls in the OpenTelemetry PHP SDK and OTLP exporter, and registers its middleware and instrumentation through a service provider.

Configure

The package reads the standard OpenTelemetry environment variables and exports traces, metrics and logs over OTLP by default. Point them at Maple’s ingest endpoint with your ingest key:

# .env
OTEL_SERVICE_NAME=my-laravel-app

OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp

OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.maple.dev
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY"

OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=production,vcs.repository.url.full=https://github.com/acme/my-laravel-app"

For an EU organization, use https://ingest.eu.maple.dev. The endpoint is the base URL; the exporters append /v1/traces, /v1/metrics and /v1/logs.

The package applies OTEL_EXPORTER_OTLP_HEADERS to every signal. If you run an older release that ignores it, set OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_METRICS_HEADERS and OTEL_EXPORTER_OTLP_LOGS_HEADERS to the same value.

An ingest key can only write telemetry to your organization. It cannot read data or call the Maple API. Still keep it in .env or your secret store rather than in committed config.

http/protobuf or http/json

Maple accepts both encodings. The difference is on the PHP side:

  • http/protobuf needs a protobuf implementation. The pure-PHP google/protobuf library works but is slow. For production, install the protobuf C extension (pecl install protobuf, then extension=protobuf.so).
  • http/json needs no extension. If protobuf export fails in your environment, for example in a minimal Laravel Sail container, set OTEL_EXPORTER_OTLP_PROTOCOL=http/json.

Auto-instrumentation

Once installed, the package creates telemetry for:

  • HTTP requests: a server span per incoming request, and the http.server.request.duration metric
  • HTTP client: a client span for each request made with the Http facade, with the trace context propagated to the service you call, and the http.client.request.duration metric
  • Database: a span per Eloquent or query builder statement, and the db.client.operation.duration metric
  • Queue jobs: producer and consumer spans, with context carried across the queue
  • Redis: commands
  • Cache: hits and misses, recorded as span events
  • Views and Livewire components: a span per render
  • Console commands: a span per run, for the commands you list in the ConsoleInstrumentation config
  • Events: recorded as span events, with configurable exclusions

Each instrumentation can be turned off with its OTEL_INSTRUMENTATION_* variable (for example OTEL_INSTRUMENTATION_QUERY=false) or in the published config/opentelemetry.php.

Custom spans

Use the Tracer facade. measure() starts the span, makes it the active span while the callback runs, and ends it:

use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;

Tracer::newSpan('process-order')
    ->setAttributes([
        'order.id' => $orderId,
        'payment.method' => 'card',
    ])
    ->measure(fn () => $this->chargePayment($orderId));

To record an exception or set the status yourself, start and activate the span by hand. The span has the OpenTelemetry PHP API (setAttribute, recordException, setStatus):

use Keepsuit\LaravelOpenTelemetry\Facades\Tracer;
use OpenTelemetry\API\Trace\StatusCode;

$span = Tracer::newSpan('process-order')->start();
$scope = $span->activate();

try {
    return $this->chargePayment($orderId);
} catch (\Throwable $e) {
    $span->recordException($e);
    $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
    throw $e;
} finally {
    $scope->detach();
    $span->end();
}

Service map edges come from instrumented client spans that propagate traceparent to an instrumented callee, not from attributes such as peer.service. See Service map. The facade also has Tracer::traceId(), Tracer::activeSpan() and Tracer::propagationHeaders() for correlating logs and propagating context by hand.

For custom metrics, use the Meter facade:

use Keepsuit\LaravelOpenTelemetry\Facades\Meter;

Meter::counter('orders.processed', 'orders', 'Orders processed')->add(1);

Log correlation

The package adds a log channel named otlp that sends log records to Maple with the active trace ID attached. Route logs to it with LOG_CHANNEL=otlp, or add it to a stack so logs also keep going to your files:

// config/logging.php
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single', 'otlp'],
    ],
],

Logs then line up with the trace that produced them in Maple.

Local mode and Docker (Laravel Sail)

To send to Maple Local running on your host, the exporter inside the Sail container must reach back to the host. Maple Local binds 127.0.0.1 by default, which a container cannot reach, so start it on all interfaces:

maple start --host 0.0.0.0

A non-loopback bind exposes ingest, the UI and queries to your network without authentication, so do this only on a trusted network. See the CLI reference.

Then point the exporter at host.docker.internal and the OTLP/HTTP port 4318:

# .env (Sail container sending to Maple Local on the host)
OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
  • Maple Local needs no Authorization header, so drop the headers variable.
  • On Linux hosts where host.docker.internal does not resolve, add extra_hosts: ["host.docker.internal:host-gateway"] to the service in docker-compose.yml.

Verify

  1. Start your application and send it a few requests, or dispatch a queued job.
  2. In Maple, open Explore → Traces. If nothing appears after a minute, see Troubleshooting.
  3. Each request should show up as one trace with a single root server span, named after the method and route (for example GET /orders), and child spans for the queries and jobs it triggered.
  4. If you routed logs to the otlp channel, open Explore → Logs and check that log lines link to their traces.

Your service also appears on the Services page once its first spans arrive.

Troubleshooting

  • 401 responses. The key is wrong, was copied from the other region, or the header is malformed. OTEL_EXPORTER_OTLP_HEADERS takes Authorization=Bearer YOUR_INGEST_KEY. See Ingest API status codes.
  • Protocol. Maple accepts OTLP over HTTP as protobuf or JSON. If exports fail with protobuf errors, switch to http/json, or install the protobuf extension.
  • Network. From inside the container or server, run curl -i https://ingest.maple.dev/v1/traces -X POST. Any HTTP status code means the host can reach Maple. A timeout or DNS error means a firewall or proxy is blocking outbound HTTPS.
  • Nothing exported. Run php artisan config:clear after changing .env; a cached config keeps the old values. Check that OTEL_SDK_DISABLED is not set to true.

Next steps