Laravel链路追踪怎么接入

wen PHP项目 1

本文目录导读:

Laravel链路追踪怎么接入

  1. 常见链路追踪工具
  2. 使用 Zipkin 接入示例
  3. 使用 OpenTelemetry(更现代的方案)
  4. 自定义 Span 示例
  5. Docker Compose 部署 Zipkin
  6. 常用配置建议
  7. 常见问题解决

我来详细介绍Laravel链路追踪的接入方案。

常见链路追踪工具

Zipkin(推荐)

Jaeger

SkyWalking

OpenTelemetry

使用 Zipkin 接入示例

安装依赖

composer require openzipkin/zipkin-laravel
composer require guzzlehttp/guzzle

配置文件

创建 config/zipkin.php

<?php
return [
    'enabled' => env('ZIPKIN_ENABLED', true),
    'endpoint_url' => env('ZIPKIN_ENDPOINT', 'http://localhost:9411/api/v2/spans'),
    'app_name' => env('ZIPKIN_APP_NAME', 'laravel-app'),
    'sample_rate' => env('ZIPKIN_SAMPLE_RATE', 1.0), // 采样率 0-1
    'timeout' => env('ZIPKIN_TIMEOUT', 1),
    'tags' => [
        'env' => env('APP_ENV', 'production'),
        'version' => env('APP_VERSION', '1.0.0'),
    ],
];

创建 Zipkin 服务提供者

// app/Providers/ZipkinServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Zipkin\Annotation;
use Zipkin\Endpoint;
use Zipkin\Samplers\BinarySampler;
use Zipkin\TracingBuilder;
use Zipkin\Reporters\Http;
use Illuminate\Support\Facades\Log;
use Zipkin\Instrumentation\Http\Client\HttpClient;
use Zipkin\Instrumentation\Http\Server\PSR15Middleware;
class ZipkinServiceProvider extends ServiceProvider
{
    protected $tracer;
    protected $tracing;
    public function register()
    {
        $this->app->singleton('zipkin.tracer', function ($app) {
            return $this->createTracer();
        });
    }
    public function boot()
    {
        if (!config('zipkin.enabled')) {
            return;
        }
        $this->app['events']->listen('kernel.handled', function ($request, $response) {
            $this->addTraceToResponse($request, $response);
        });
    }
    protected function createTracer()
    {
        $endpoint = Endpoint::create(
            config('zipkin.app_name'),
            gethostbyname(gethostname()),
            null,
            config('zipkin.port', 80)
        );
        $reporter = new Http(
            ['endpoint_url' => config('zipkin.endpoint_url')],
            function () {
                return ['timeout' => config('zipkin.timeout')];
            }
        );
        $sampler = BinarySampler::createAsAlwaysSample();
        // 或者使用概率采样
        // $sampler = new PercentageSampler(config('zipkin.sample_rate'));
        $this->tracing = TracingBuilder::create()
            ->havingLocalEndpoint($endpoint)
            ->havingSampler($sampler)
            ->havingReporter($reporter)
            ->build();
        return $this->tracing->getTracer();
    }
    protected function addTraceToResponse($request, $response)
    {
        try {
            $tracer = $this->app['zipkin.tracer'];
            if ($tracer && $request && $response) {
                // 为当前请求创建 span
                $span = $tracer->nextSpan();
                $span->start();
                // 添加请求信息
                $span->setName($request->method() . ' ' . $request->path());
                $span->tag('http.method', $request->method());
                $span->tag('http.path', $request->path());
                $span->tag('http.status_code', $response->getStatusCode());
                // 添加性能指标
                if (defined('LARAVEL_START')) {
                    $duration = (microtime(true) - LARAVEL_START) * 1000;
                    $span->tag('duration_ms', round($duration, 2));
                }
                $span->finish();
                $tracer->flush();
            }
        } catch (\Exception $e) {
            Log::error('Zipkin trace error: ' . $e->getMessage());
        }
    }
}

创建中间件

// app/Http/Middleware/ZipkinTracing.php
<?php
namespace App\Http\Middleware;
use Closure;
use Zipkin\Tracer;
use Illuminate\Support\Facades\Log;
class ZipkinTracing
{
    protected $tracer;
    public function __construct()
    {
        $this->tracer = app('zipkin.tracer');
    }
    public function handle($request, Closure $next)
    {
        if (!config('zipkin.enabled') || !$this->tracer) {
            return $next($request);
        }
        // 创建 span
        $span = $this->tracer->nextSpan();
        $span->start();
        try {
            // 注入 trace 上下文到请求头
            $span->inject($request->headers->all(), function ($carrier, $key, $value) {
                $carrier[$key] = $value;
            });
            $response = $next($request);
            // 记录响应信息
            $span->tag('http.status_code', $response->status());
            return $response;
        } catch (\Exception $e) {
            $span->tag('error', $e->getMessage());
            throw $e;
        } finally {
            $span->finish();
            $this->tracer->flush();
        }
    }
}

注册中间件和服务提供者

// config/app.php
'providers' => [
    // ...
    App\Providers\ZipkinServiceProvider::class,
],
// app/Http/Kernel.php
protected $middleware = [
    // ...
    \App\Http\Middleware\ZipkinTracing::class,
];

使用 OpenTelemetry(更现代的方案)

安装依赖

composer require open-telemetry/opentelemetry-auto-laravel
composer require open-telemetry/exporter-otlp
composer require open-telemetry/sdk

配置文件

// config/opentelemetry.php
<?php
return [
    'enabled' => env('OTEL_ENABLED', true),
    'service_name' => env('OTEL_SERVICE_NAME', 'laravel-app'),
    'endpoint' => env('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4318'),
    'sampler' => [
        'type' => env('OTEL_TRACES_SAMPLER', 'always_on'),
        'ratio' => env('OTEL_TRACES_SAMPLER_ARG', 1.0),
    ],
    'exporter' => [
        'type' => env('OTEL_EXPORTER', 'otlp'),
        'timeout' => env('OTEL_EXPORTER_TIMEOUT', 10),
    ],
    'instrumentation' => [
        'http_client' => env('OTEL_PHP_INSTRUMENTATION_HTTP_CLIENT', true),
        'guzzle' => env('OTEL_PHP_INSTRUMENTATION_GUZZLE', true),
        'redis' => env('OTEL_PHP_INSTRUMENTATION_REDIS', true),
        'eloquent' => env('OTEL_PHP_INSTRUMENTATION_ELOQUENT', true),
    ],
];

创建 OpenTelemetry 服务提供者

// app/Providers/OpenTelemetryServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\Exporter\Otlp\OtlpHttpExporter;
use OpenTelemetry\API\Globals;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\Sampler\TraceIdRatioBasedSampler;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
class OpenTelemetryServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('opentelemetry.tracer', function ($app) {
            return $this->createTracerProvider();
        });
    }
    public function boot()
    {
        if (!config('opentelemetry.enabled')) {
            return;
        }
        // 自动检测框架
        if (class_exists(Globals::class)) {
            Globals::registerInitializer(function ($builder) {
                return $builder;
            });
        }
        // 记录启动时间
        $this->app['events']->listen('kernel.handled', function ($request, $response) {
            $this->addTraceHeaders($response);
        });
    }
    protected function createTracerProvider()
    {
        $resource = ResourceInfo::create(Attributes::create([
            'service.name' => config('opentelemetry.service_name'),
            'telemetry.sdk.name' => 'opentelemetry',
            'telemetry.sdk.language' => 'php',
        ]));
        $exporter = new OtlpHttpExporter(
            config('opentelemetry.endpoint')
        );
        $sampler = new ParentBased(
            new AlwaysOnSampler()
        );
        $tracerProvider = TracerProvider::builder()
            ->setResource($resource)
            ->addSpanProcessor(new SimpleSpanProcessor($exporter))
            ->setSampler($sampler)
            ->build();
        return $tracerProvider->getTracer('laravel-app');
    }
    protected function addTraceHeaders($response)
    {
        try {
            $tracer = $this->app['opentelemetry.tracer'];
            $span = $tracer->getCurrentSpan();
            if ($span && $response) {
                $span->end();
            }
        } catch (\Exception $e) {
            \Log::error('OpenTelemetry error: ' . $e->getMessage());
        }
    }
}

自定义 Span 示例

在控制器中使用

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
    public function index()
    {
        // 使用 OpenTelemetry
        if (config('opentelemetry.enabled')) {
            $tracer = app('opentelemetry.tracer');
            $span = $tracer->spanBuilder('users.list')
                ->setAttribute('user.type', 'admin')
                ->startSpan();
        }
        try {
            // 业务逻辑
            $users = \App\Models\User::all();
            // 添加数据库查询 span
            if (isset($span)) {
                $span->setAttribute('db.statement', 'SELECT * FROM users');
                $span->setAttribute('db.system', 'mysql');
                $span->setAttribute('db.count', $users->count());
            }
            return response()->json($users);
        } catch (\Exception $e) {
            if (isset($span)) {
                $span->setAttribute('error', true);
                $span->setAttribute('error.message', $e->getMessage());
            }
            throw $e;
        } finally {
            if (isset($span)) {
                $span->end();
                $tracer->forceFlush();
            }
        }
    }
}

在队列任务中使用

// app/Jobs/ProcessPodcast.php
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    public function handle()
    {
        if (config('opentelemetry.enabled')) {
            $tracer = app('opentelemetry.tracer');
            $span = $tracer->spanBuilder('job.process')
                ->setAttribute('job.name', static::class)
                ->setAttribute('job.queue', $this->queue)
                ->startSpan();
        }
        try {
            // 任务处理逻辑
            sleep(2);
        } finally {
            if (isset($span)) {
                $span->end();
            }
        }
    }
}

Docker Compose 部署 Zipkin

version: '3.8'
services:
  zipkin:
    image: openzipkin/zipkin:latest
    container_name: zipkin
    ports:
      - "9411:9411"
    environment:
      - STORAGE_TYPE=elasticsearch
      - ES_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch
  elasticsearch:
    image: elasticsearch:7.9.3
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
  # 或者使用内存存储(简单部署)
  # zipkin-inmemory:
  #   image: openzipkin/zipkin:latest
  #   container_name: zipkin-inmemory
  #   ports:
  #     - "9411:9411"

常用配置建议

.env 配置

# Zipkin 配置
ZIPKIN_ENABLED=true
ZIPKIN_ENDPOINT=http://localhost:9411/api/v2/spans
ZIPKIN_APP_NAME=laravel-app
ZIPKIN_SAMPLE_RATE=0.5
# OpenTelemetry 配置
OTEL_ENABLED=true
OTEL_SERVICE_NAME=laravel-app
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_TRACES_SAMPLER=always_on

采样策略

// 动态采样
public function shouldSample($request)
{
    // 只采样 10% 的请求
    if (mt_rand(1, 100) > 10) {
        return false;
    }
    // 始终采样错误请求
    if ($request->is('api/*')) {
        return true;
    }
    return true;
}

常见问题解决

  1. 性能影响:设置合适的采样率,避免全量采集
  2. 内存泄漏:确保 Span 正确结束和释放
  3. 网络超时:设置合理的超时时间
  4. 数据丢失:使用缓冲策略,异步发送

选择哪种方案取决于你的具体需求、团队熟悉度和基础设施,Zipkin 更适合传统架构,OpenTelemetry 面向云原生场景。

抱歉,评论功能暂时关闭!