PHP 语言 OpenTelemetry 接入指南

本文档提供 PHP 应用接入 OpenTelemetry 的完整指南,包括零代码接入和 SDK 手动接入两种方式,支持 Laravel、Symfony 等主流框架。

目录


概述

PHP 语言特性

PHP 的 OpenTelemetry 支持具有以下特点:

  • 成熟稳定: API 和 SDK 已达到稳定版本
  • 零代码接入: 通过 OpenTelemetry PHP 扩展实现完全零侵入
  • 丰富的埋点库: 支持 30+ 主流 PHP 框架和库
  • Composer 支持: 通过 Composer 轻松安装和管理
  • PSR 标准: 遵循 PSR-3、PSR-7 等 PHP 标准

版本要求

  • PHP 版本: 8.0+ (推荐 8.1+)
  • OpenTelemetry PHP: v1.0.0+
  • 自动埋点: PHP 8.0+(需要 OpenTelemetry PHP 扩展)
  • 支持架构: x86_64, arm64

支持的信号

  • ✅ Traces: 完全支持
  • ✅ Metrics: 完全支持
  • ✅ Logs: 完全支持

适用场景

  • Web 服务(Laravel、Symfony、Slim)
  • 微服务架构
  • API 服务
  • 数据库应用
  • 消息队列应用

零代码接入(推荐)

零代码接入是 PHP OpenTelemetry 最强大的特性,通过 OpenTelemetry PHP 扩展和自动埋点库即可实现自动埋点,无需修改任何源代码。

快速开始(5 分钟)

1. 安装 OpenTelemetry PHP 扩展

方法 1: 使用 PECL 安装(推荐)

# 安装开发环境依赖(Linux)
sudo apt-get install gcc make autoconf
# 或 macOS
brew install gcc make autoconf
# 安装扩展
pecl install opentelemetry

方法 2: 使用 Linux 包管理器

CentOS/RHEL (RPM):

yum update -y
yum install -y epel-release yum-utils
yum install -y http://rpms.remirepo.net/enterprise/remi-release-7.rpm
yum-config-manager --enable remi-php81
yum install -y php php-pecl-opentelemetry

Alpine Linux (APK):

echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
apk add php php81-pecl-opentelemetry@testing

启用扩展

在 php.ini 中添加:

[opentelemetry]
extension=opentelemetry.so

验证安装:

php -m | grep opentelemetry
php --ri opentelemetry

2. 安装 SDK 和自动埋点库

根据您使用的框架和库,安装相应的自动埋点包:

# 基础 SDK 和 OTLP Exporter
composer require open-telemetry/sdk
composer require open-telemetry/exporter-otlp
# 根据您的框架选择(示例:Slim Framework 和 PSR-18 HTTP 客户端)
composer require open-telemetry/opentelemetry-auto-slim
composer require open-telemetry/opentelemetry-auto-psr18
# 其他常用自动埋点库
composer require open-telemetry/opentelemetry-auto-psr15 # PSR-15
composer require open-telemetry/opentelemetry-auto-pdo # PDO
composer require open-telemetry/opentelemetry-auto-redis # Redis
composer require open-telemetry/opentelemetry-auto-guzzle # Guzzle

查看所有可用的自动埋点库:

访问 Packagist 搜索 opentelemetry-auto-* 查看完整的自动埋点库列表。

3. 配置环境变量

必需的环境变量:

# 启用自动加载(必需)
export OTEL_PHP_AUTOLOAD_ENABLED=true
# 服务信息
export OTEL_SERVICE_NAME=my-php-service
export OTEL_SERVICE_VERSION=1.0.0
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
# OTLP Exporter(内部平台)
export OTEL_EXPORTER_OTLP_ENDPOINT=http://uptrace-grpc.yodao.cn:80
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=http://token@uptrace-grpc.yodao.cn/project?grpc=4317
# Traces 配置
export OTEL_TRACES_EXPORTER=otlp
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1
# 传播器配置
export OTEL_PROPAGATORS=baggage,tracecontext

或使用 php.ini 配置:

在 php.ini 文件中添加:

OTEL_PHP_AUTOLOAD_ENABLED="true"
OTEL_SERVICE_NAME=my-php-service
OTEL_TRACES_EXPORTER=otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=http://uptrace-grpc.yodao.cn:80
OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=http://token@uptrace-grpc.yodao.cn/project?grpc=4317
OTEL_PROPAGATORS=baggage,tracecontext

4. 启动应用

就这么简单!直接运行您的应用即可,无需任何代码修改:

# CLI 脚本
php myapp.php
# Web 服务器(PHP-FPM、Apache、Nginx)
# 正常启动即可,扩展会自动工作

重要提示:

  • ✅ 无需创建任何初始化文件
  • ✅ 无需修改应用代码
  • ✅ 只需安装扩展和 Composer 包,配置环境变量即可
  • ✅ 扩展会自动检测已安装的自动埋点库并启用它们

就这么简单!您的应用现在已经自动埋点并开始发送遥测数据了。

工作原理

OpenTelemetry PHP 扩展通过注册观察者函数(observer functions)来实现自动埋点。扩展会在类和方法执行前后执行这些函数,自动创建和管理 Span。

关键点:

  1. 扩展层:OpenTelemetry PHP 扩展提供底层钩子机制
  2. 自动埋点库:Composer 包提供具体的埋点逻辑
  3. 自动加载:通过 OTEL_PHP_AUTOLOAD_ENABLED=true 启用自动加载机制
  4. 零侵入:无需修改应用代码,只需安装和配置

支持的自动埋点库

根据您安装的自动埋点库,系统会自动检测并启用相应的埋点。常用库包括:

Web 框架

  • Slim Framework: open-telemetry/opentelemetry-auto-slim
  • Laravel: open-telemetry/opentelemetry-auto-laravel(如可用)
  • Symfony: open-telemetry/opentelemetry-auto-symfony(如可用)

HTTP 客户端

  • PSR-18: open-telemetry/opentelemetry-auto-psr18
  • PSR-15: open-telemetry/opentelemetry-auto-psr15
  • Guzzle: open-telemetry/opentelemetry-auto-guzzle

数据库

  • PDO: open-telemetry/opentelemetry-auto-pdo
  • MySQLi: open-telemetry/opentelemetry-auto-mysqli(如可用)

缓存

  • Redis: open-telemetry/opentelemetry-auto-redis
  • Memcached: open-telemetry/opentelemetry-auto-memcached(如可用)

查看完整列表:

访问 Packagist 搜索 opentelemetry-auto-* 查看所有可用的自动埋点库。

环境变量详解

服务信息:

# 服务名称(必需)
export OTEL_SERVICE_NAME=my-php-service
# 服务版本
export OTEL_SERVICE_VERSION=1.0.0
# 资源属性(键值对,逗号分隔)
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.namespace=backend

OTLP Exporter 配置:

# 端点地址(内部平台,HTTP 协议)
export OTEL_EXPORTER_OTLP_ENDPOINT=http://uptrace-grpc.yodao.cn:80
# 协议类型(http/protobuf 或 grpc)
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
# 请求头(包含 DSN)
export OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=http://token@uptrace-grpc.yodao.cn/project?grpc=4317
# 超时时间(毫秒)
export OTEL_EXPORTER_OTLP_TIMEOUT=10000
# 压缩(gzip,可选)
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip

重要提示:

  • 内部平台推荐使用 HTTP 协议(端口 80),兼容性更好
  • 如果使用 gRPC 协议,端点应为 uptrace-grpc.yodao.cn:4317
  • DSN 格式:http://token@uptrace-grpc.yodao.cn/project?grpc=4317

Traces 配置:

# Exporter 类型
export OTEL_TRACES_EXPORTER=otlp
# 采样器类型
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
# 采样率(0.0 到 1.0)
export OTEL_TRACES_SAMPLER_ARG=0.1

Metrics 配置:

# Exporter 类型
export OTEL_METRICS_EXPORTER=otlp
# 导出间隔(毫秒)
export OTEL_METRIC_EXPORT_INTERVAL=60000

Logs 配置:

# Exporter 类型
export OTEL_LOGS_EXPORTER=otlp

传播器配置:

# 传播器类型(逗号分隔)
export OTEL_PROPAGATORS=baggage,tracecontext

不同运行环境的配置

PHP CLI

# 设置环境变量后直接运行
export OTEL_PHP_AUTOLOAD_ENABLED=true
export OTEL_SERVICE_NAME=my-php-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://uptrace-grpc.yodao.cn:80
export OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=http://token@uptrace-grpc.yodao.cn/project?grpc=4317
php myapp.php

PHP-FPM

在 php-fpm.conf 或 www.conf 中设置环境变量:

[www]
env[OTEL_PHP_AUTOLOAD_ENABLED] = true
env[OTEL_SERVICE_NAME] = my-php-service
env[OTEL_EXPORTER_OTLP_ENDPOINT] = http://uptrace-grpc.yodao.cn:80
env[OTEL_EXPORTER_OTLP_HEADERS] = uptrace-dsn=http://token@uptrace-grpc.yodao.cn/project?grpc=4317

Apache

在 httpd.conf 或虚拟主机配置中:

<VirtualHost *:80>
SetEnv OTEL_PHP_AUTOLOAD_ENABLED true
SetEnv OTEL_SERVICE_NAME my-php-service
SetEnv OTEL_EXPORTER_OTLP_ENDPOINT http://uptrace-grpc.yodao.cn:80
SetEnv OTEL_EXPORTER_OTLP_HEADERS "uptrace-dsn=http://token@uptrace-grpc.yodao.cn/project?grpc=4317"
</VirtualHost>

Docker

在 Dockerfile 或 docker-compose.yml 中:

environment:
- OTEL_PHP_AUTOLOAD_ENABLED=true
- OTEL_SERVICE_NAME=my-php-service
- OTEL_EXPORTER_OTLP_ENDPOINT=http://uptrace-grpc.yodao.cn:80
- OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=http://token@uptrace-grpc.yodao.cn/project?grpc=4317

验证零代码接入

启动应用后,访问您的服务端点,然后在 APM 平台查看数据:

  1. Traces: 查看调用链路
  2. Metrics: 查看请求量、延迟等指标
  3. Service Map: 查看服务拓扑图

快速验证

# 启动应用
php -d opentelemetry.enabled=1 -S localhost:8000
# 在另一个终端发送测试请求
curl http://localhost:8000/
# 等待几秒后,在 APM 平台查看数据

日志级别配置

可以通过环境变量设置日志级别:

# 设置日志级别
export OTEL_LOG_LEVEL=info # none, error, warn, info, debug, verbose, all

SDK 手动接入

SDK 手动接入提供更精细的控制和自定义能力。

安装依赖

使用 Composer 安装:

composer require open-telemetry/api
composer require open-telemetry/sdk
composer require open-telemetry/exporter-otlp
composer require open-telemetry/instrumentation-http
composer require open-telemetry/instrumentation-pdo

基础初始化

创建 OpenTelemetry 初始化文件

<?php
// otel.php
require_once __DIR__ . '/vendor/autoload.php';
use OpenTelemetry\API\Globals;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Trace\TracerProviderBuilder;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\Sampler\TraceIdRatioBasedSampler;
use OpenTelemetry\Exporter\Otlp\OtlpExporter;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
function initOpenTelemetry(): void
{
// 创建 Resource
$resource = ResourceInfoFactory::emptyResource()
->merge(ResourceInfoFactory::defaultResource())
->merge(ResourceInfoFactory::create([
'service.name' => $_ENV['OTEL_SERVICE_NAME'] ?? 'my-php-service',
'service.version' => $_ENV['OTEL_SERVICE_VERSION'] ?? '1.0.0',
]));
// 创建 OTLP Exporter
$exporter = new OtlpExporter([
'endpoint' => $_ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? 'uptrace-grpc.yodao.cn:4317',
'protocol' => $_ENV['OTEL_EXPORTER_OTLP_PROTOCOL'] ?? 'grpc',
'headers' => [
'uptrace-dsn' => $_ENV['OTEL_EXPORTER_OTLP_HEADERS'] ?? '',
],
]);
// 创建采样器
$sampler = new ParentBased(
new TraceIdRatioBasedSampler(
(float)($_ENV['OTEL_TRACES_SAMPLER_ARG'] ?? 0.1)
)
);
// 创建 TracerProvider
$tracerProvider = (new TracerProviderBuilder())
->setResource($resource)
->addSpanProcessor(new BatchSpanProcessor($exporter))
->setSampler($sampler)
->build();
// 注册全局 TracerProvider
Globals::registerInitializer(function () use ($tracerProvider) {
return $tracerProvider;
});
}
// 初始化
initOpenTelemetry();

在应用中使用

<?php
// index.php
require_once __DIR__ . '/otel.php';
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
// 获取 Tracer
$tracer = Globals::tracerProvider()->getTracer('my-service');
// 创建 Span
$span = $tracer->spanBuilder('handle_request')
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
$scope = $span->activate();
try {
// 业务逻辑
$span->setAttribute('http.method', 'GET');
$span->setAttribute('http.url', '/');
echo "Hello, World!";
$span->setStatus(StatusCode::STATUS_OK);
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}

Traces - 手动埋点

创建和使用 Span

<?php
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\API\Globals;
$tracer = Globals::tracerProvider()->getTracer('my-service');
function processOrder(string $orderId): void
{
$tracer = Globals::tracerProvider()->getTracer('my-service');
$span = $tracer->spanBuilder('process_order')
->setSpanKind(SpanKind::KIND_INTERNAL)
->startSpan();
$scope = $span->activate();
try {
$span->setAttribute('order.id', $orderId);
$span->setAttribute('operation', 'process');
$span->addEvent('order_validation_started');
validateOrder($orderId);
$span->addEvent('order_validated');
chargePayment($orderId);
$span->addEvent('payment_processed');
$span->setStatus(StatusCode::STATUS_OK);
$span->setAttribute('order.success', true);
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}
}

Context 传播

<?php
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\API\Context\Propagation\TextMapPropagatorInterface;
// 获取当前 Context
$propagator = TraceContextPropagator::getInstance();
$context = Globals::propagator()->extract([], $_SERVER);
// 在 HTTP 请求中传播 Context
function makeHttpRequest(string $url): string
{
$tracer = Globals::tracerProvider()->getTracer('my-service');
$span = $tracer->spanBuilder('http_request')
->setSpanKind(SpanKind::KIND_CLIENT)
->startSpan();
$scope = $span->activate();
try {
$span->setAttribute('http.url', $url);
$span->setAttribute('http.method', 'GET');
// 注入 Context 到 HTTP headers
$headers = [];
Globals::propagator()->inject($headers);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, array_map(
fn($k, $v) => "$k: $v",
array_keys($headers),
$headers
));
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$span->setAttribute('http.status_code', $statusCode);
$span->setStatus($statusCode >= 400 ? StatusCode::STATUS_ERROR : StatusCode::STATUS_OK);
return $response;
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}
}

Metrics - 指标采集

Counter(计数器)

<?php
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Metrics\CounterInterface;
$meterProvider = Globals::meterProvider();
$meter = $meterProvider->getMeter('my-service');
$requestCounter = $meter->createCounter(
'http.server.requests',
'total',
'Total number of HTTP requests'
);
function handleRequest(string $method, string $route, int $statusCode): void
{
global $requestCounter;
$requestCounter->add(1, [
'http.method' => $method,
'http.route' => $route,
'http.status_code' => (string)$statusCode,
]);
}

Histogram(直方图)

<?php
$requestDuration = $meter->createHistogram(
'http.server.duration',
'ms',
'HTTP request duration'
);
function recordDuration(float $duration, string $method, string $route): void
{
global $requestDuration;
$requestDuration->record($duration, [
'http.method' => $method,
'http.route' => $route,
]);
}

UpDownCounter(升降计数器)

<?php
$activeConnections = $meter->createUpDownCounter(
'http.server.active_connections',
'connections',
'Number of active HTTP connections'
);
function connectionOpened(): void
{
global $activeConnections;
$activeConnections->add(1);
}
function connectionClosed(): void
{
global $activeConnections;
$activeConnections->add(-1);
}

Logs - 日志集成

集成 Monolog

<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use OpenTelemetry\API\Trace\SpanContextInterface;
use OpenTelemetry\API\Globals;
// 创建 logger
$logger = new Logger('my-service');
$logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO));
// 添加 trace 信息到日志
$logger->pushProcessor(function ($record) {
$span = Globals::tracerProvider()
->getTracer('my-service')
->spanBuilder('log')
->startSpan()
->getContext();
if ($span->isValid()) {
$record['extra']['trace_id'] = $span->getTraceId();
$record['extra']['span_id'] = $span->getSpanId();
}
return $record;
});
// 使用示例
function example(): void
{
global $logger;
$logger->info('Processing request', [
'user_id' => '12345',
'action' => 'create',
]);
}

框架集成示例

Laravel 框架集成

使用 Service Provider

创建 app/Providers/OpenTelemetryServiceProvider.php:

<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use OpenTelemetry\API\Globals;
use OpenTelemetry\SDK\Trace\TracerProviderBuilder;
use OpenTelemetry\Exporter\Otlp\OtlpExporter;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
class OpenTelemetryServiceProvider extends ServiceProvider
{
public function register(): void
{
$exporter = new OtlpExporter([
'endpoint' => env('OTEL_EXPORTER_OTLP_ENDPOINT', 'uptrace-grpc.yodao.cn:4317'),
'protocol' => env('OTEL_EXPORTER_OTLP_PROTOCOL', 'grpc'),
'headers' => [
'uptrace-dsn' => env('OTEL_EXPORTER_OTLP_HEADERS', ''),
],
]);
$tracerProvider = (new TracerProviderBuilder())
->addSpanProcessor(new BatchSpanProcessor($exporter))
->build();
Globals::registerInitializer(function () use ($tracerProvider) {
return $tracerProvider;
});
}
}

在 config/app.php 中注册:

'providers' => [
// ...
App\Providers\OpenTelemetryServiceProvider::class,
],

使用 Middleware

创建 app/Http/Middleware/OpenTelemetryMiddleware.php:

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
class OpenTelemetryMiddleware
{
public function handle(Request $request, Closure $next)
{
$tracer = Globals::tracerProvider()->getTracer('laravel');
$span = $tracer->spanBuilder('http.request')
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
$scope = $span->activate();
try {
$span->setAttribute('http.method', $request->method());
$span->setAttribute('http.url', $request->fullUrl());
$span->setAttribute('http.route', $request->route()?->getName());
$response = $next($request);
$span->setAttribute('http.status_code', $response->getStatusCode());
$span->setStatus(StatusCode::STATUS_OK);
return $response;
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}
}
}

Symfony 框架集成

使用 Event Subscriber

创建 src/EventSubscriber/OpenTelemetrySubscriber.php:

<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
class OpenTelemetrySubscriber implements EventSubscriberInterface
{
private $spans = [];
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 8],
KernelEvents::RESPONSE => ['onKernelResponse', -8],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$tracer = Globals::tracerProvider()->getTracer('symfony');
$span = $tracer->spanBuilder('http.request')
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
$scope = $span->activate();
$span->setAttribute('http.method', $request->getMethod());
$span->setAttribute('http.url', $request->getUri());
$span->setAttribute('http.route', $request->attributes->get('_route'));
$this->spans[spl_object_id($request)] = ['span' => $span, 'scope' => $scope];
}
public function onKernelResponse(ResponseEvent $event): void
{
$request = $event->getRequest();
$response = $event->getResponse();
$key = spl_object_id($request);
if (!isset($this->spans[$key])) {
return;
}
$span = $this->spans[$key]['span'];
$scope = $this->spans[$key]['scope'];
$span->setAttribute('http.status_code', $response->getStatusCode());
$span->setStatus(
$response->getStatusCode() >= 400
? StatusCode::STATUS_ERROR
: StatusCode::STATUS_OK
);
$span->end();
$scope->detach();
unset($this->spans[$key]);
}
}

Slim 框架集成

<?php
use Slim\Factory\AppFactory;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/otel.php';
$app = AppFactory::create();
// 添加 OpenTelemetry Middleware
$app->add(function ($request, $handler) {
$tracer = Globals::tracerProvider()->getTracer('slim');
$span = $tracer->spanBuilder('http.request')
->setSpanKind(SpanKind::KIND_SERVER)
->startSpan();
$scope = $span->activate();
try {
$span->setAttribute('http.method', $request->getMethod());
$span->setAttribute('http.url', (string)$request->getUri());
$response = $handler->handle($request);
$span->setAttribute('http.status_code', $response->getStatusCode());
$span->setStatus(StatusCode::STATUS_OK);
return $response;
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}
});
$app->get('/users/{id}', function ($request, $response, $args) {
$tracer = Globals::tracerProvider()->getTracer('slim');
$span = $tracer->spanBuilder('get_user')
->startSpan();
$scope = $span->activate();
try {
$span->setAttribute('user.id', $args['id']);
// 业务逻辑
$user = getUser($args['id']);
$span->setStatus(StatusCode::STATUS_OK);
return $response->withJson($user);
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}
});
$app->run();

数据库集成

PDO 集成

<?php
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
function queryDatabase(string $query, array $params = []): array
{
$tracer = Globals::tracerProvider()->getTracer('my-service');
$span = $tracer->spanBuilder('database.query')
->setSpanKind(SpanKind::KIND_CLIENT)
->startSpan();
$scope = $span->activate();
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=mydb',
'user',
'password'
);
$span->setAttribute('db.system', 'mysql');
$span->setAttribute('db.name', 'mydb');
$span->setAttribute('db.statement', $query);
$stmt = $pdo->prepare($query);
$stmt->execute($params);
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
$span->setStatus(StatusCode::STATUS_OK);
return $result;
} catch (\PDOException $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}
}

MySQLi 集成

<?php
function queryWithMysqli(string $query): array
{
$tracer = Globals::tracerProvider()->getTracer('my-service');
$span = $tracer->spanBuilder('database.query')
->setSpanKind(SpanKind::KIND_CLIENT)
->startSpan();
$scope = $span->activate();
try {
$mysqli = new mysqli('localhost', 'user', 'password', 'mydb');
$span->setAttribute('db.system', 'mysql');
$span->setAttribute('db.name', 'mydb');
$span->setAttribute('db.statement', $query);
$result = $mysqli->query($query);
$rows = $result->fetch_all(MYSQLI_ASSOC);
$span->setStatus(StatusCode::STATUS_OK);
return $rows;
} catch (\mysqli_sql_exception $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}
}

MongoDB 集成

<?php
use MongoDB\Client;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
function queryMongoDB(string $collection, array $filter): array
{
$tracer = Globals::tracerProvider()->getTracer('my-service');
$span = $tracer->spanBuilder('mongodb.query')
->setSpanKind(SpanKind::KIND_CLIENT)
->startSpan();
$scope = $span->activate();
try {
$client = new Client('mongodb://localhost:27017');
$db = $client->selectDatabase('mydb');
$collection = $db->selectCollection($collection);
$span->setAttribute('db.system', 'mongodb');
$span->setAttribute('db.name', 'mydb');
$span->setAttribute('db.collection', $collection);
$result = $collection->find($filter)->toArray();
$span->setStatus(StatusCode::STATUS_OK);
return $result;
} catch (\Exception $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}
}

Redis 集成

<?php
use Redis;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
function getCache(string $key): ?string
{
$tracer = Globals::tracerProvider()->getTracer('my-service');
$span = $tracer->spanBuilder('redis.get')
->setSpanKind(SpanKind::KIND_CLIENT)
->startSpan();
$scope = $span->activate();
try {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$span->setAttribute('db.system', 'redis');
$span->setAttribute('db.operation', 'GET');
$span->setAttribute('db.redis.key', $key);
$value = $redis->get($key);
$span->setStatus(StatusCode::STATUS_OK);
return $value;
} catch (\Exception $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
} finally {
$span->end();
$scope->detach();
}
}

高级配置

自定义采样策略

<?php
use OpenTelemetry\SDK\Trace\Sampler\SamplerInterface;
use OpenTelemetry\SDK\Trace\SamplingResult;
class CustomSampler implements SamplerInterface
{
private array $alwaysSample = ['/checkout', '/payment'];
private array $neverSample = ['/health', '/metrics'];
public function shouldSample(
ContextInterface $context,
string $traceId,
string $spanName,
int $spanKind,
array $attributes,
array $links
): SamplingResult {
// 总是采样
foreach ($this->alwaysSample as $pattern) {
if (str_contains($spanName, $pattern)) {
return new SamplingResult(SamplingResult::RECORD_AND_SAMPLE);
}
}
// 从不采样
foreach ($this->neverSample as $pattern) {
if (str_contains($spanName, $pattern)) {
return new SamplingResult(SamplingResult::DROP);
}
}
// 默认采样率 10%
return (new TraceIdRatioBasedSampler(0.1))
->shouldSample($context, $traceId, $spanName, $spanKind, $attributes, $links);
}
public function getDescription(): string
{
return 'CustomSampler';
}
}

多 Exporter 配置

<?php
use OpenTelemetry\Exporter\Otlp\OtlpExporter;
use OpenTelemetry\Exporter\Zipkin\ZipkinExporter;
use OpenTelemetry\SDK\Trace\SpanExporter\ConsoleSpanExporter;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
// 创建多个 exporter
$otlpExporter = new OtlpExporter([
'endpoint' => 'uptrace-grpc.yodao.cn:4317',
'headers' => [
'uptrace-dsn' => 'http://token@uptrace-grpc.yodao.cn/project?grpc=4317',
],
]);
$zipkinExporter = new ZipkinExporter([
'endpoint' => 'http://localhost:9411/api/v2/spans',
]);
$consoleExporter = new ConsoleSpanExporter();
// 添加多个处理器
$tracerProvider = (new TracerProviderBuilder())
->addSpanProcessor(new BatchSpanProcessor($otlpExporter))
->addSpanProcessor(new BatchSpanProcessor($zipkinExporter))
->addSpanProcessor(new BatchSpanProcessor($consoleExporter))
->build();

性能优化配置

<?php
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
$exporter = new OtlpExporter([
'endpoint' => 'uptrace-grpc.yodao.cn:4317',
'headers' => [
'uptrace-dsn' => 'http://token@uptrace-grpc.yodao.cn/project?grpc=4317',
],
'compression' => 'gzip',
'timeout' => 10000,
]);
$spanProcessor = new BatchSpanProcessor(
$exporter,
null, // Clock
512, // maxExportBatchSize
5000, // scheduledDelayMillis
2048, // maxQueueSize
30000 // exportTimeoutMillis
);
$tracerProvider = (new TracerProviderBuilder())
->addSpanProcessor($spanProcessor)
->build();

最佳实践

1. Context 传递

<?php
// ✅ 正确:在函数调用中传递 context
function processOrder(ContextInterface $context, string $orderId): void
{
$tracer = Globals::tracerProvider()->getTracer('my-service');
$span = $tracer->spanBuilder('process_order')
->setParent($context)
->startSpan();
$newContext = $span->storeInContext($context);
// 传递 context 给下游函数
validateOrder($newContext, $orderId);
chargePayment($newContext, $orderId);
$span->end();
}

2. 错误处理

<?php
// ✅ 正确:记录错误并设置状态
try {
processOrder($orderId);
$span->setStatus(StatusCode::STATUS_OK);
} catch (\Throwable $e) {
$span->recordException($e);
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
throw $e;
}

3. 资源管理

<?php
// ✅ 正确:使用 try-finally 确保 span 结束
function operation(): void
{
$span = $tracer->spanBuilder('operation')->startSpan();
$scope = $span->activate();
try {
// 业务逻辑
} finally {
$span->end();
$scope->detach();
}
}

故障排查

常见问题

1. 数据未发送到后端

排查步骤:

  1. 检查环境变量:
echo $OTEL_SERVICE_NAME
echo $OTEL_EXPORTER_OTLP_ENDPOINT
  1. 检查 PHP 扩展是否加载:
php -m | grep opentelemetry
  1. 启用调试日志:
export OTEL_LOG_LEVEL=debug

2. Span 未关联

确保正确传递 context:

$newContext = $span->storeInContext($context);
downstreamFunction($newContext);

3. 性能问题

  • 降低采样率
  • 增加批量大小
  • 使用异步导出

调试技巧

启用详细日志

export OTEL_LOG_LEVEL=debug

使用 Console Exporter

use OpenTelemetry\SDK\Trace\SpanExporter\ConsoleSpanExporter;
$tracerProvider = (new TracerProviderBuilder())
->addSpanProcessor(new BatchSpanProcessor(new ConsoleSpanExporter()))
->build();

下一步

参考资源

在Gitlab上编辑本文档