OpenTelemetry + Laravel: Observability Production Vượt Xa Telescope
Laravel Telescope tuyệt vời cho development. Pulse tốt cho metrics tổng quan. Nhưng cho production debugging ở quy mô lớn — tracing request qua queues, databases, external APIs — bạn cần OpenTelemetry.
OpenTelemetry (OTel) là tiêu chuẩn ngành cho observability. Thu thập traces, metrics, và logs rồi export sang bất kỳ backend nào (Jaeger, Grafana Tempo, Datadog). Vendor-agnostic — đổi backend không cần thay code.
Telescope vs Pulse vs OpenTelemetry
| Feature | Telescope | Pulse | OpenTelemetry |
|---|---|---|---|
| Mục đích | Dev debugging | Production dashboard | Full observability |
| Traces | Request-level | Không | Distributed tracing |
| Metrics | Không | Server metrics, slow queries | Custom metrics, histograms |
| Logs | Không | Không | Structured logs với trace context |
| Distributed | Không | Không | Có (qua services, queues, APIs) |
| Backend | Local DB | Local DB | Jaeger, Grafana, Datadog, v.v. |
| Production? | Không nên | Có | Có |
Khi nào nên dùng OTel: Khi bạn cần biết "request này chậm WHY" — nó mất 200ms ở database, 100ms ở cache miss, và 500ms chờ external API. OTel trace cho bạn thấy toàn bộ journey.
Ba Trụ Cột Observability
Traces → Chuyện gì xảy ra từng bước (request → DB → cache → API → response)
Metrics → Các con số (request count, latency p99, error rate, queue depth)
Logs → Chi tiết (error messages, context, stack traces)
OTel hợp nhất cả ba với correlation — trace ID liên kết logs và metrics với request flows cụ thể. Khi bạn thấy error rate tăng (metrics), tìm trace ID (logs), rồi xem full request flow (traces).
User Request → Trace ID: abc123
├── [Span] HTTP GET /api/posts/42 12ms
│ ├── [Span] PostController.show 10ms
│ │ ├── [Span] Cache::get post_42 0.5ms (miss)
│ │ ├── [Span] MySQL SELECT 3ms
│ │ └── [Span] Cache::put post_42 0.8ms
│ └── [Span] View render 2ms
└── [Log] "Cache miss for post_42" → trace_id=abc123
Cài Đặt
composer require open-telemetry/sdk
composer require open-telemetry/exporter-otlp
composer require open-telemetry/contrib-auto-laravel
composer require open-telemetry/contrib-auto-pdo
composer require open-telemetry/contrib-auto-http-async
Giải thích packages:
sdk— OTel core SDKexporter-otlp— export data sang OTLP protocol (Jaeger, Grafana, v.v.)contrib-auto-laravel— auto-instrument Laravel: routes, middleware, exceptionscontrib-auto-pdo— auto-instrument database queriescontrib-auto-http-async— auto-instrument HTTP client calls
Environment Variables
OTEL_SERVICE_NAME=my-blog
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_PROPAGATORS=baggage,tracecontext
Local Development: Docker Compose Với Jaeger
# docker-compose.yml (thêm vào setup hiện tại)
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
environment:
COLLECTOR_OTLP_ENABLED: true
Truy cập Jaeger UI tại http://localhost:16686 — tìm service my-blog, xem traces.
Auto-Instrumentation
Với các packages contrib-auto-*, phần lớn instrumentation tự động:
✅ HTTP requests (route, method, status code, duration)
✅ Database queries (query, duration, connection)
✅ Cache operations (hit/miss, key, duration)
✅ Queue jobs (job name, duration, attempts)
✅ External HTTP calls (URL, method, status, duration)
✅ Exceptions (type, message, stack trace)
Không cần thêm code. Cài packages, set env vars, restart app → traces bắt đầu xuất hiện trong Jaeger.
Manual Tracing — Khi Auto Không Đủ
Auto-instrumentation cover infrastructure. Cho business logic quan trọng, thêm custom spans:
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\API\Trace\SpanKind;
class PostService
{
public function getPost(string $slug): Post
{
$tracer = Globals::tracerProvider()->getTracer('blog');
$span = $tracer->spanBuilder('PostService.getPost')
->setSpanKind(SpanKind::KIND_INTERNAL)
->setAttribute('post.slug', $slug)
->startSpan();
$scope = $span->activate();
try {
// Cache check — auto-instrumented, nhưng ta thêm business context
$post = Cache::remember("post_{$slug}", 3600, function () use ($slug, $tracer) {
$dbSpan = $tracer->spanBuilder('PostService.loadFromMarkdown')
->startSpan();
try {
$post = $this->markdownService->getPost($slug);
$dbSpan->setAttribute('post.title', $post->title);
$dbSpan->setAttribute('post.word_count', str_word_count($post->body));
return $post;
} finally {
$dbSpan->end();
}
});
$span->setStatus(StatusCode::STATUS_OK);
$span->setAttribute('post.cached', Cache::has("post_{$slug}"));
return $post;
} catch (\Exception $e) {
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
$span->recordException($e);
throw $e;
} finally {
$span->end();
$scope->detach();
}
}
}
Giải thích:
spanBuilder()— tạo span mới (một bước trong trace)setAttribute()— thêm metadata cho span (searchable trong Jaeger)activate()— set span là "current" — child spans tự động nestsetStatus()— mark OK hoặc ERRORrecordException()— ghi exception details vào span- QUAN TRỌNG: Luôn gọi
$span->end()và$scope->detach()trongfinally. Memory leak nếu quên.
Helper Trait Cho Code Sạch Hơn
trait Traceable
{
protected function trace(string $name, callable $callback, array $attributes = []): mixed
{
$tracer = Globals::tracerProvider()->getTracer('blog');
$span = $tracer->spanBuilder($name)->startSpan();
$scope = $span->activate();
foreach ($attributes as $key => $value) {
$span->setAttribute($key, $value);
}
try {
$result = $callback($span);
$span->setStatus(StatusCode::STATUS_OK);
return $result;
} catch (\Throwable $e) {
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
$span->recordException($e);
throw $e;
} finally {
$span->end();
$scope->detach();
}
}
}
// Sử dụng
class PostService
{
use Traceable;
public function getPost(string $slug): Post
{
return $this->trace('PostService.getPost', function ($span) use ($slug) {
$span->setAttribute('post.slug', $slug);
return Cache::remember("post_{$slug}", 3600, fn () => $this->loadPost($slug));
});
}
}
Structured Logging với Trace Context
Mỗi log line bao gồm trace_id → jump từ log entry trực tiếp sang full trace trong Jaeger:
// app/Logging/OtelFormatter.php
use Monolog\Formatter\JsonFormatter;
use Monolog\LogRecord;
use OpenTelemetry\API\Trace\Span;
class OtelFormatter extends JsonFormatter
{
public function format(LogRecord $record): string
{
$span = Span::getCurrent();
$context = $span->getContext();
$record->extra['trace_id'] = $context->getTraceId();
$record->extra['span_id'] = $context->getSpanId();
$record->extra['service_name'] = config('app.name');
return parent::format($record);
}
}
// config/logging.php
'channels' => [
'structured' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'formatter' => OtelFormatter::class,
'with' => ['stream' => 'php://stderr'],
],
],
Kết quả log:
{
"message": "Post not found",
"level": 400,
"context": {"slug": "nonexistent"},
"extra": {
"trace_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"span_id": "1234567890abcdef",
"service_name": "my-blog"
}
}
Copy trace_id → paste vào Jaeger search → xem toàn bộ request flow. Đây là superpower của correlated observability.
Custom Metrics
use OpenTelemetry\API\Globals;
class BlogMetrics
{
private static $meter = null;
private static function meter()
{
return self::$meter ??= Globals::meterProvider()->getMeter('blog');
}
public static function recordPageView(string $slug): void
{
self::meter()
->createCounter('blog.page_views', 'views', 'Total page views')
->add(1, ['slug' => $slug, 'locale' => app()->getLocale()]);
}
public static function recordResponseTime(float $durationMs, string $route): void
{
self::meter()
->createHistogram('blog.response_time', 'ms', 'Response time distribution')
->record($durationMs, ['route' => $route]);
}
public static function recordCacheHitRate(bool $hit, string $key): void
{
self::meter()
->createCounter('blog.cache', 'operations', 'Cache operations')
->add(1, ['result' => $hit ? 'hit' : 'miss', 'key_prefix' => explode('_', $key)[0]]);
}
public static function recordSearchQuery(string $query, int $resultCount): void
{
self::meter()
->createHistogram('blog.search_results', 'count', 'Search result count')
->record($resultCount, ['has_results' => $resultCount > 0]);
}
}
// Sử dụng trong middleware
class RecordMetrics
{
public function handle(Request $request, Closure $next): mixed
{
$start = microtime(true);
$response = $next($request);
$duration = (microtime(true) - $start) * 1000;
BlogMetrics::recordResponseTime($duration, $request->route()?->getName() ?? 'unknown');
return $response;
}
}
Metric types:
- Counter — chỉ tăng: page views, error count, requests total
- Histogram — distribution: response time (p50, p90, p99), result count
- UpDownCounter — tăng/giảm: active connections, queue depth
Gửi Sang Cloud Providers
Đổi backend chỉ cần thay env vars — code không thay đổi:
Datadog
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# Datadog Agent nhận OTLP trên port 4317
DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317
Grafana Cloud
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20<base64_encoded_credentials>
AWS X-Ray (qua ADOT Collector)
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# AWS Distro for OpenTelemetry Collector chạy local, forward sang X-Ray
Performance Impact
OTel thêm overhead. Giảm thiểu:
# Sampling: chỉ trace 10% requests trên production
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1
# Batch export (giảm network calls)
OTEL_BSP_MAX_QUEUE_SIZE=2048
OTEL_BSP_SCHEDULE_DELAY=5000
OTEL_BSP_MAX_EXPORT_BATCH_SIZE=512
Sampling strategies:
always_on— trace mọi request (development)traceidratio— trace X% requests (production)parentbased_traceidratio— trace X% top-level, nhưng trace 100% child spans nếu parent được traced
Kết Luận
OpenTelemetry cần setup nhiều hơn Telescope/Pulse, nhưng cho bạn observability production-grade:
- Traces cho thấy chính xác chuyện gì xảy ra — từng bước, từng millisecond
- Metrics cho thấy xu hướng — response time tăng? Error rate đột biến?
- Logs với trace context cho phép drill từ overview vào chi tiết
- Vendor-agnostic — đổi từ Jaeger sang Datadog không cần thay code
Bắt đầu thế nào:
- Cài auto-instrumentation packages + Jaeger Docker
- Xem traces trong Jaeger UI — đã có insights ngay lập tức
- Thêm custom spans cho business logic quan trọng
- Thêm custom metrics cho KPIs
- Configure sampling cho production
- Thêm structured logging với trace context