laravel-jaeger-client maintained by erfanmomeniii
Laravel Jaeger Client
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.