erfanmomeniii / laravel-jaeger-client
A production-grade Jaeger distributed tracing client for Laravel with deep framework integration
Package info
github.com/ErfanMomeniii/laravel-jaeger-client
pkg:composer/erfanmomeniii/laravel-jaeger-client
Requires
- php: ^8.1
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/log: ^10.0|^11.0|^12.0
- illuminate/queue: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- larastan/larastan: ^2.0|^3.0
- laravel/pint: ^1.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
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.
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, aNullTraceris 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.