erfanmomeniii/laravel-jaeger-client

A production-grade Jaeger distributed tracing client for Laravel with deep framework integration

Maintainers

Package info

github.com/ErfanMomeniii/laravel-jaeger-client

pkg:composer/erfanmomeniii/laravel-jaeger-client

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-04-26 07:50 UTC

This package is auto-updated.

Last update: 2026-04-26 07:56:50 UTC


README

A production-grade Jaeger distributed tracing client for Laravel.

Zero-config automatic tracing for HTTP requests, database queries, queue jobs, cache, Redis, and more — plus a clean API for manual instrumentation.

License: MIT

Why This Package?

Feature This package Raw PHP Jaeger clients OpenTelemetry SDK
Automatic HTTP tracing
DB / Queue / Cache watchers Partial
Jaeger::fake() for testing
Trace context in logs Partial
Zero external PHP deps
Lightweight (no gRPC/protobuf)
Laravel auto-discovery

Requirements

  • PHP 8.1+
  • Laravel 10, 11, or 12
  • ext-sockets (for UDP transport, enabled by default in most PHP installations)

Installation

composer require erfanmomeniii/laravel-jaeger-client

Publish the config file:

php artisan vendor:publish --tag=jaeger-config

That's it. The package auto-discovers and starts tracing immediately.

Quick Start

1. Start Jaeger

docker run -d --name jaeger \
  -p 6831:6831/udp \
  -p 16686:16686 \
  jaegertracing/all-in-one:latest

2. Add to your .env

JAEGER_ENABLED=true
JAEGER_SERVICE_NAME=my-api

3. Make a request to your app, then open http://localhost:16686

You'll see traces for every HTTP request, including child spans for each database query, outbound HTTP call, and more — without writing a single line of tracing code.

What Gets Traced Automatically

Component Span name Enabled by default
HTTP requests HTTP GET /api/users
Database queries db.query
Queue jobs queue.process ProcessPayment
Outbound HTTP HTTP POST api.stripe.com
Log context (injects trace_id/span_id)
Cache operations cache.hit, cache.put
Redis commands redis.GET, redis.SET
Events event.OrderCreated
Artisan commands artisan:migrate
View rendering view.render welcome

Enable any disabled watcher in config/jaeger.php:

'cache' => ['enabled' => true],
'redis' => ['enabled' => true],

Manual Instrumentation

measure() — Trace a block of code (recommended)

The simplest way. The span auto-finishes when the callback returns, and errors are auto-tagged:

use LaravelJaeger\Laravel\Facades\Jaeger;

$result = Jaeger::build('payment.charge')
    ->withTag('payment.id', $payment->id)
    ->withTag('payment.amount', $payment->amount)
    ->measure(function () use ($payment) {
        return $this->gateway->charge($payment);
    });

jaeger_span() — Quick scope for manual control

When you need to add tags or logs during execution:

$scope = jaeger_span('order.process', ['order.id' => $order->id]);

try {
    $this->processOrder($order);
    $scope->getSpan()->setTag('order.status', 'completed');
} catch (\Throwable $e) {
    $scope->getSpan()->setTag('error', true);
    $scope->getSpan()->log([
        'event' => 'error',
        'error.kind' => get_class($e),
        'error.message' => $e->getMessage(),
    ]);
    throw $e;
} finally {
    $scope->close();
}

Other Helpers

// Tag the current active span (from middleware or a parent)
jaeger_active_span()?->setTag('user.id', auth()->id());

// Access the tracer directly
$tracer = jaeger();

Configuration

All settings are in config/jaeger.php, driven by environment variables:

Core Settings

JAEGER_ENABLED=true                  # Master switch (false = zero overhead)
JAEGER_SERVICE_NAME=my-api           # Service name shown in Jaeger UI
JAEGER_SERVICE_VERSION=1.2.0         # Shown as process tag
JAEGER_ENVIRONMENT=production        # Shown as process tag

Transport

# UDP to Jaeger agent (default — recommended for production)
JAEGER_TRANSPORT=udp
JAEGER_AGENT_HOST=127.0.0.1
JAEGER_AGENT_PORT=6831

# HTTP direct to collector (useful for serverless / no sidecar)
JAEGER_TRANSPORT=http
JAEGER_COLLECTOR_ENDPOINT=http://jaeger-collector:14268/api/traces
JAEGER_AUTH_TOKEN=your-secret-token

# Debug: write spans to Laravel log
JAEGER_TRANSPORT=log

# Disable sending entirely
JAEGER_TRANSPORT=null

Sampling

# Sample everything (development)
JAEGER_SAMPLER_TYPE=const
JAEGER_SAMPLER_PARAM=1

# Sample 10% of traces (production)
JAEGER_SAMPLER_TYPE=probabilistic
JAEGER_SAMPLER_PARAM=0.1

# Max 2 traces per second (high-traffic production)
JAEGER_SAMPLER_TYPE=rate_limiting
JAEGER_SAMPLER_PARAM=2.0

Propagation Format

# Jaeger native (default) — uber-trace-id header
JAEGER_PROPAGATION=jaeger

# Zipkin B3 — X-B3-TraceId / X-B3-SpanId headers
JAEGER_PROPAGATION=b3

# W3C TraceContext — traceparent header
JAEGER_PROPAGATION=w3c

# Try all formats when extracting (for polyglot environments)
JAEGER_PROPAGATION=composite

Feature Toggles

JAEGER_MIDDLEWARE_ENABLED=true       # HTTP request tracing
JAEGER_DB_ENABLED=true               # Database query tracing
JAEGER_QUEUE_ENABLED=true            # Queue job tracing
JAEGER_HTTP_CLIENT_ENABLED=true      # Outbound HTTP tracing
JAEGER_LOG_CONTEXT_ENABLED=true      # Inject trace_id in logs
JAEGER_CACHE_ENABLED=false           # Cache operation tracing
JAEGER_REDIS_ENABLED=false           # Redis command tracing
JAEGER_EVENTS_ENABLED=false          # Event dispatch tracing
JAEGER_ARTISAN_ENABLED=false         # Artisan command tracing
JAEGER_VIEWS_ENABLED=false           # View rendering tracing

Queue Tracing

Trace context propagates automatically from the dispatching request to the job worker. Add the TracedJob trait to your jobs:

use Illuminate\Contracts\Queue\ShouldQueue;
use LaravelJaeger\Laravel\Traits\TracedJob;

class ProcessPayment implements ShouldQueue
{
    use TracedJob;

    public function handle(): void
    {
        // The parent trace from the HTTP request is linked here.
        jaeger_active_span()?->setTag('payment.status', 'completed');
    }
}

Log Correlation

When JAEGER_LOG_CONTEXT_ENABLED=true (default), every log line includes trace context:

{
  "message": "Payment processed successfully",
  "extra": {
    "trace_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "span_id": "1a2b3c4d5e6f7a8b"
  }
}

Search your logs by trace_id to find all log entries for a single request, then click through to the Jaeger UI to see the full trace.

Testing

Jaeger::fake()

Works just like Laravel's Queue::fake() and Event::fake():

use LaravelJaeger\Laravel\Facades\Jaeger;

public function test_checkout_creates_payment_trace(): void
{
    $fake = Jaeger::fake();

    $this->postJson('/api/checkout', ['item_id' => 1]);

    // Assert spans were created
    $fake->assertSpanCreated('HTTP POST /api/checkout');
    $fake->assertSpanCreated('db.query');
    $fake->assertSpanCreated('payment.charge');

    // Assert specific tags
    $fake->assertSpanHasTag('payment.charge', 'payment.amount', 99.99);

    // Assert errors were recorded
    $fake->assertSpanHasLog('payment.charge', ['event' => 'error']);

    // Assert span count
    $fake->assertSpanCount(5);
}

WithTracing Trait

For test classes that always need tracing:

use LaravelJaeger\Testing\WithTracing;

class PaymentTest extends TestCase
{
    use WithTracing;

    public function test_charge(): void
    {
        // $this->fakeTracer is automatically available
        $this->processPayment();

        $this->fakeTracer->assertSpanCreated('payment.charge');
    }
}

All Assertion Methods

Method Description
assertSpanCreated($name) A span with this operation name exists
assertSpanNotCreated($name) No span with this name exists
assertSpanCreatedWithTags($name, $tags) Span exists with these tag values
assertSpanCount($n) Exactly n spans were created
assertNoSpansCreated() Zero spans created
assertSpanHasTag($name, $key, $value) Specific tag value on a span
assertSpanHasLog($name, $fields) Span has a log entry with these fields
collectedSpans() Get raw span array for custom assertions
reset() Clear all collected spans

Extending

Every component is coded to an interface. Swap any part by rebinding in the container.

Custom Sampler

use LaravelJaeger\Contracts\SamplerInterface;

class UserBasedSampler implements SamplerInterface
{
    public function isSampled(string $traceId, string $operationName): array
    {
        $isAdmin = auth()->user()?->is_admin ?? false;

        return [$isAdmin, ['sampler.type' => 'user-based', 'sampler.param' => $isAdmin]];
    }

    public function close(): void {}
}

// Register in AppServiceProvider:
$this->app->bind(SamplerInterface::class, UserBasedSampler::class);

Custom Transport

use LaravelJaeger\Contracts\TransportInterface;

class KafkaTransport implements TransportInterface
{
    public function append(array $spans): void { /* buffer spans */ }
    public function flush(): void { /* publish to Kafka topic */ }
    public function close(): void { /* disconnect */ }
}

Resilience

This package is designed to never break your application:

  • All watchers and middleware wrap their logic in try/catch — tracing errors are silently swallowed
  • UDP transport has a circuit breaker — stops trying after 5 consecutive failures, retries after 30s
  • When JAEGER_ENABLED=false, a NullTracer is used with zero allocations (no performance impact)
  • Spans have hard limits on tags (256), logs (128), and tag value length (4KB) to prevent memory issues
  • Flush happens in app()->terminating() — after the response is sent to the client

License

MIT — see LICENSE.