手动仪表化

OpenTelemetry PHP 的手动仪表化

Manual instrumentation is the act of adding observability code to an app yourself.

If you’re instrumenting an app, you need to use the OpenTelemetry SDK for your language. You’ll then use the SDK to initialize OpenTelemetry and the API to instrument your code. This will emit telemetry from your app, and any library you installed that also comes with instrumentation.

If you’re instrumenting a library, only install the OpenTelemetry API package for your language. Your library will not emit telemetry on its own. It will only emit telemetry when it is part of an app that uses the OpenTelemetry SDK. For more on instrumenting libraries, see Libraries.

For more information about the OpenTelemetry API and SDK, see the specification.

安装

以下演示如何安装、初始化和运行使用 OpenTelemetry 进行仪表化的应用程序。遥测数据将显示在控制台中。

要使用 OpenTelemetry SDK for PHP,您需要满足 psr/http-client-implementationpsr/http-factory-implementation 的依赖项。这里我们将使用 Guzzle,它提供了这两个依赖项:

composer require guzzlehttp/guzzle

现在,您可以安装 OpenTelemetry SDK 和 OTLP 导出器:

composer require \
  open-telemetry/sdk \
  open-telemetry/exporter-otlp

设置

第一步是获取 OpenTelemetry 接口的实例。

如果您是应用程序开发人员,您需要尽早地配置 OpenTelemetry SDK 实例。这里我们将使用 Sdk::builder() 方法,并全局注册提供者。

您可以使用 TracerProvider::builder()LoggerProvider::builder()MeterProvider::builder() 方法来构建提供者。还建议定义一个 Resource 实例,表示生成遥测数据的实体,特别是 service.name 属性。

示例

<?php

use OpenTelemetry\API\Common\Instrumentation\Globals;
use OpenTelemetry\API\Logs\EventLogger;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\Contrib\Otlp\LogsExporter;
use OpenTelemetry\Contrib\Otlp\MetricExporter;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Export\Stream\StreamTransportFactory;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\Processor\SimpleLogsProcessor;
use OpenTelemetry\SDK\Metrics\MeterProvider;
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Sdk;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SemConv\ResourceAttributes;

require 'vendor/autoload.php';

$resource = ResourceInfoFactory::emptyResource()->merge(ResourceInfo::create(Attributes::create([
    ResourceAttributes::SERVICE_NAMESPACE => 'demo',
    ResourceAttributes::SERVICE_NAME => 'test-application',
    ResourceAttributes::SERVICE_VERSION => '0.1',
    ResourceAttributes::DEPLOYMENT_ENVIRONMENT => 'development',
])));
$spanExporter = new SpanExporter(
    (new StreamTransportFactory())->create('php://stdout', 'application/json')
);

$logExporter = new LogsExporter(
    (new StreamTransportFactory())->create('php://stdout', 'application/json')
);

$reader = new ExportingReader(
    new MetricExporter(
        (new StreamTransportFactory())->create('php://stdout', 'application/json')
    )
);

$meterProvider = MeterProvider::builder()
    ->setResource($resource)
    ->addReader($reader)
    ->build();

$tracerProvider = TracerProvider::builder()
    ->addSpanProcessor(
        new SimpleSpanProcessor($spanExporter)
    )
    ->setResource($resource)
    ->setSampler(new ParentBased(new AlwaysOnSampler()))
    ->build();

$loggerProvider = LoggerProvider::builder()
    ->setResource($resource)
    ->addLogRecordProcessor(
        new SimpleLogsProcessor($logExporter)
    )
    ->build();

Sdk::builder()
    ->setTracerProvider($tracerProvider)
    ->setMeterProvider($meterProvider)
    ->setLoggerProvider($loggerProvider)
    ->setPropagator(TraceContextPropagator::getInstance())
    ->setAutoShutdown(true)
    ->buildAndRegisterGlobal();

在以下示例中,我们通常使用 OpenTelemetry\API\Globals 获取全局注册的提供者:

$tracerProvider = \OpenTelemetry\API\Globals::tracerProvider();
$meterProvider = \OpenTelemetry\API\Globals::meterProvider();
$loggerProvider = \OpenTelemetry\API\Globals::loggerProvider();

关闭

重要的是,在 PHP 进程结束时运行每个提供者的 shutdown() 方法,以便刷新任何排队的遥测数据。在上面的示例中,我们使用 setAutoShutdown(true) 来处理这个问题。

您还可以使用 ShutdownHandler 在 PHP 的关闭过程中注册每个提供者的关闭函数:

\OpenTelemetry\SDK\Common\Util\ShutdownHandler::register([$tracerProvider, 'shutdown']);
\OpenTelemetry\SDK\Common\Util\ShutdownHandler::register([$meterProvider, 'shutdown']);
\OpenTelemetry\SDK\Common\Util\ShutdownHandler::register([$loggerProvider, 'shutdown']);

跟踪

获取跟踪器

要进行跟踪,您需要获取一个Tracer

跟踪器负责创建跨度并与上下文交互。您可以从 TracerProvider 获取跟踪器,指定名称和其他(可选)有关要监视的被仪表化库或应用程序的库的相关信息

有关如何获取跟踪器的更多信息,请参阅规范章节获取 Tracer

$tracerProvider = Globals::tracerProvider();
$tracer = $tracerProvider->getTracer(
  'instrumentation-library-name', // name(必需)
  '1.0.0', // 版本
  'http://example.com/my-schema', // 模式 url
  ['foo' => 'bar'] // 属性
);

重要说明:获取跟踪器时使用的参数仅用于信息目的 - 这些值将作为任何由该跟踪器发出的遥测的范围的一部分发出。由单个 OpenTelemetry 实例创建的所有 Tracer 都可以互操作,而不管这些参数的差异。

创建跨度

要创建跨度,您只需要指定跨度的名称。跨度的起始时间和结束时间将由 OpenTelemetry SDK 自动设置。

$span = $tracer->spanBuilder("my span")->startSpan();
// do some work
$span->end();

必须调用 end() 方法来结束跨度,否则它将不会被发送。

创建嵌套跨度

大部分时间,我们想要将跨度与嵌套操作相关联。OpenTelemetry 支持在进程内和跨进程跟踪。有关如何在跨进程之间共享上下文的详细信息,请参阅上下文传播

对于调用方法 parent 的方法 child,我们可以在创建子跨度之前将父跨度设为活动跨度:

$parent = $tracer->spanBuilder("parent")->startSpan();
$scope = $parent->activate();
try {
  $child = $tracer->spanBuilder("child")->startSpan();
  $child->end();
} finally {
  $parent->end();
  $scope->detach();
}

如果您已经激活了活动跨度,则必须 detach 它。

获取当前跨度

有时在程序执行的特定点上,使用当前/活动的跨度可能会很有帮助。

$span = OpenTelemetry\API\Trace\Span::getCurrent();

如果要获得特定 Context 对象的当前跨度:

$span = OpenTelemetry\API\Trace\Span::fromContext($context);

跨度属性

在 OpenTelemetry 中,跨度可以随意地创建,并且实现方可以使用与所表示操作特定的属性对其进行注释。属性为跨度提供有关其所跟踪的特定操作的其他上下文,例如结果或操作属性。

$span = $tracer->spanBuilder("/resource/path")->setSpanKind(SpanKind::CLIENT)->startSpan();
$span->setAttribute("http.method", "GET");
$span->setAttribute("http.url", (string) $url);

使用事件创建跨度

跨度可以使用命名事件(称为跨度事件)进行注释,每个事件可以携带零个或多个跨度属性,每个属性本身都与时间戳自动配对。

$span->addEvent("Init");
...
$span->addEvent("End");
$eventAttributes = Attributes::create([
    "operation" => "calculate-pi",
    "result" => 3.14159,
]);
$span->addEvent("End Computation", $eventAttributes);

使用链接创建跨度

一个跨度可以与零个或多个通过跨度链接因果相关的其他跨度相关联。链接可用于表示批量操作,其中一个跨度由多个起始跨度引发,每个跨度表示批处理中正在处理的单个传入项。

$span = $tracer->spanBuilder("span-with-links")
    ->addLink($parentSpan1->getContext())
    ->addLink($parentSpan2->getContext())
    ->addLink($parentSpan3->getContext())
    ->addLink($remoteSpanContext)
    ->startSpan();

有关如何从远程进程读取上下文的更多详细信息,请参阅上下文传播

设置跨度状态和记录异常

跨度可以设置跨度状态,通常用于指定跨度未成功完成 - SpanStatus::ERROR

当发生异常时,记录异常是一个好习惯。建议与设置跨度状态同时使用记录异常。

可以在跨度完成之前的任何时间设置状态:

$span = $tracer->spanBuilder("my-span")->startSpan();
try {
  // 执行可能失败的操作
  throw new \Exception('uh-oh');
} catch (\Throwable $t) {
  $span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR, "Something bad happened!");
  // 这将捕获跨度中的当前堆栈跟踪等信息。
  $span->recordException($t, ['exception.escaped' => true]);
  throw $t;
} finally {
  $span->end();
}

采样器

在应用程序中追踪和导出每个用户请求并不总是可行的。为了在可观察性和开销之间取得平衡,可以对跟踪进行采样

OpenTelemetry SDK 提供了四个采样器:

  • AlwaysOnSampler:无论上游采样决策如何,它都会对每个跟踪进行采样。
  • AlwaysOffSampler:它不对任何跟踪进行采样,无论上游采样决策如何。
  • TraceIdRatioBased:它对可配置百分比的跟踪进行采样,并且另外对上游已采样的任何跟踪进行采样。
  • ParentBased:如果存在,它使用父跨度来进行采样决策。该采样器需要与根采样器一起使用,后者用于确定是否应该对根跨度(即没有父跨度的跨度)进行采样。根采样器可以是其他采样器中的任何一个。
// 对 50% 的请求进行跟踪
$sampler = new TraceIdRatioBasedSampler(0.5);
// 始终跟踪
$sampler = new AlwaysOnSampler();
// 总是采样,如果父跨度已经采样,否则只对 10% 的跨度进行采样
$sampler = new ParentBased(new TraceIdRatioBasedSampler(0.1));
$tracerProvider = TracerProvider::builder()
  ->setSampler($sampler)
  ->build();

还可以通过实现 OpenTelemetry\SDK\Trace\SamplerInterface 提供其他采样器。其中一个示例是根据跨度创建时间设置的属性来进行采样决策。

跨度处理器

OpenTelemetry 提供了不同的跨度处理器。SimpleSpanProcessor 立即将已完成的跨度转发到导出器,而 BatchSpanProcessor 则将其批处理并定期发送。

$tracerProvider = TracerProvider::builder()
  ->addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporterFactory()->create()))
  ->build();

传输

所有导出器都需要一个 Transport,负责发送遥测数据:

  • PsrTransport:使用 PSR-18 客户端通过 HTTP 发送数据
  • StreamTransport:使用流发送数据(例如文件或 stdout
  • GrpcTransport:使用 gRPC 发送 protobuf 编码的数据

导出器

跨度处理器使用一个导出器来将遥测数据发送到特定的后端:

  • InMemory:将数据保存在内存中,用于测试和调试。
  • Console:将数据发送到流,例如 stdoutstderr
  • Zipkin:准备并通过 Zipkin API 将遥测数据发送到 Zipkin 后端。
  • 日志导出器:将遥测数据发送到 PSR-3 记录器。
  • OpenTelemetry 协议导出器:将数据以 OTLP 格式发送到 OpenTelemetry 收集器或其他 OTLP 接收器。支持以下格式:
    • 通过 HTTP 的 protobuf
    • 通过 gRPC 的 protobuf
    • 通过 HTTP 的 JSON

指标

OpenTelemetry可以用于测量和记录应用程序中的不同类型的指标,然后可以将其“推送”到诸如OpenTelemetry收集器一样的指标服务中:

  • 计数器
  • 异步计数器
  • 直方图
  • 异步测量
  • 上升/下降计数器
  • 异步上升/下降计数器

指标类型和用法在"指标概念"说明文档中有解释。

设置

首先,创建一个MeterProvider

<?php

use OpenTelemetry\Contrib\Otlp\ConsoleMetricExporterFactory;
use OpenTelemetry\SDK\Metrics\MeterProvider;
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;

require 'vendor/autoload.php';

$reader = new ExportingReader((new ConsoleMetricExporterFactory())->create());

$meterProvider = MeterProvider::builder()
    ->addReader($reader)
    ->build();

同步计量器

同步计量器必须在数据更改时手动进行调整:

$up_down = $meterProvider
    ->getMeter('demo_meter')
    ->createUpDownCounter('queued', 'jobs', 'The number of jobs enqueued');
//jobs come in
$up_down->add(5);
//job completed
$up_down->add(-1);
//more jobs come in
$up_down->add(2);

$meterProvider->forceFlush();

当在计量器提供程序上调用forceFlush()shutdown()时,同步指标将被导出。

查看输出结果
{
  "resourceMetrics": [
    {
      "resource": {},
      "scopeMetrics": [
        {
          "scope": { "name": "demo_meter" },
          "metrics": [
            {
              "name": "queued",
              "description": "The number of jobs enqueued",
              "unit": "jobs",
              "sum": {
                "dataPoints": [
                  {
                    "startTimeUnixNano": "1687332126443709851",
                    "timeUnixNano": "1687332126445544432",
                    "asInt": "6"
                  }
                ],
                "aggregationTemporality": "AGGREGATION_TEMPORALITY_DELTA"
              }
            }
          ]
        }
      ]
    }
  ]
}

异步计量器

异步计量器是“可观察的”,例如ObservableGauge。在注册可观察/异步计量器时,您需要提供一个或多个回调函数。当调用其collect()方法时,回调函数将由定期导出的度量记录器调用,例如基于事件循环计时器。回调函数负责返回计量器的当前数据。

在此示例中,回调函数在每次执行$reader->collect()时执行:

$queue = [
    'job1',
    'job2',
    'job3',
];
$meterProvider
    ->getMeter('demo_meter')
    ->createObservableGauge('queued', 'jobs', 'The number of jobs enqueued')
    ->observe(static function (ObserverInterface $observer) use (&$queue): void {
        $observer->observe(count($queue));
    });
$reader->collect();
array_pop($queue);
$reader->collect();
查看输出结果
{"resourceMetrics":[{"resource":{},"scopeMetrics":[{"scope":{"name":"demo_meter"},"metrics":[{"name":"queued","description":"The number of jobs enqueued","unit":"jobs","gauge":{"dataPoints":[{"startTimeUnixNano":"1687331630161510994","timeUnixNano":"1687331630162989144","asInt":"3"}]}}]}]}]}
{"resourceMetrics":[{"resource":{},"scopeMetrics":[{"scope":{"name":"demo_meter"},"metrics":[{"name":"queued","description":"The number of jobs enqueued","unit":"jobs","gauge":{"dataPoints":[{"startTimeUnixNano":"1687331630161510994","timeUnixNano":"1687331631164759171","asInt":"2"}]}}]}]}]}

日志

由于日志是一项成熟且经过充分验证的功能,因此"OpenTelemetry方法"在处理这个信号上有些不同。

OpenTelemetry日志记录器的设计并不是用于直接使用,而是用于集成到现有的日志库中。通过这种方式,您可以选择将部分或全部应用程序日志发送到与OpenTelemetry兼容的服务(例如收集器)中。

设置

首先,创建一个LoggerProvider

<?php

use OpenTelemetry\API\Logs\EventLogger;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\Contrib\Otlp\LogsExporter;
use OpenTelemetry\SDK\Common\Export\Stream\StreamTransportFactory;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\Processor\SimpleLogsProcessor;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;

require 'vendor/autoload.php';

$exporter = new LogsExporter(
    (new StreamTransportFactory())->create('php://stdout', 'application/json')
);

$loggerProvider = LoggerProvider::builder()
    ->addLogRecordProcessor(new SimpleLogsProcessor($exporter))
    ->setResource(ResourceInfoFactory::emptyResource())
    ->build();

记录日志事件

使用EventLogger可以使用Logger来发出日志事件:

$logger = $loggerProvider->getLogger('demo', '1.0', 'http://schema.url', [/*attributes*/]);
$eventLogger = new EventLogger($logger, 'my-domain');
$record = (new LogRecord('hello world'))
    ->setSeverityText('INFO')
    ->setAttributes([/*attributes*/]);

$eventLogger->logEvent('foo', $record);
查看输出结果示例
{
  "resourceLogs": [
    {
      "resource": {},
      "scopeLogs": [
        {
          "scope": {
            "name": "demo",
            "version": "1.0"
          },
          "logRecords": [
            {
              "observedTimeUnixNano": "1687496730010009088",
              "severityText": "INFO",
              "body": {
                "stringValue": "hello world"
              },
              "attributes": [
                {
                  "key": "event.name",
                  "value": {
                    "stringValue": "foo"
                  }
                },
                {
                  "key": "event.domain",
                  "value": {
                    "stringValue": "my-domain"
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

第三方日志库的集成

Monolog

您可以使用monolog handler将monolog日志发送到支持OpenTelemetry的接收器。首先,安装monolog库和一个处理程序:

composer require \
  monolog/monolog \
  open-telemetry/opentelemetry-logger-monolog

继续上述日志示例:

$handler = new \OpenTelemetry\Contrib\Logs\Monolog\Handler(
    $loggerProvider,
    \Psr\Log\LogLevel::ERROR,
);
$monolog = new \Monolog\Logger('example', [$handler]);

$monolog->info('hello, world');
$monolog->error('oh no', [
    'foo' => 'bar',
    'exception' => new \Exception('something went wrong'),
]);
查看输出结果示例
{
  "resourceLogs": [
    {
      "resource": {},
      "scopeLogs": [
        {
          "scope": {
            "name": "monolog"
          },
          "logRecords": [
            {
              "timeUnixNano": "1687496945597429000",
              "observedTimeUnixNano": "1687496945598242048",
              "severityNumber": "SEVERITY_NUMBER_ERROR",
              "severityText": "ERROR",
              "body": {
                "stringValue": "oh no"
              },
              "attributes": [
                {
                  "key": "channel",
                  "value": {
                    "stringValue": "example"
                  }
                },
                {
                  "key": "context",
                  "value": {
                    "arrayValue": {
                      "values": [
                        {
                          "stringValue": "bar"
                        },
                        {
                          "arrayValue": {
                            "values": [
                              {
                                "stringValue": "Exception"
                              },
                              {
                                "stringValue": "something went wrong"
                              },
                              {
                                "intValue": "0"
                              },
                              {
                                "stringValue": "/usr/src/myapp/logging.php:31"
                              }
                            ]
                          }
                        }
                      ]
                    }
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

错误处理

默认情况下,OpenTelemetry将通过PHP的error_log函数记录错误和警告。可以通过OTEL_LOG_LEVEL设置来控制详细程度或禁用错误记录。

可以使用OTEL_PHP_LOG_DESTINATION变量来控制日志目标或完全禁用错误记录。有效值为defaulterror_logstderrstdoutpsr3none。使用default(或如果未设置此变量),将使用error_log,除非配置了PSR-3记录器:

$logger = new \Example\Psr3Logger(LogLevel::INFO);
\OpenTelemetry\API\LoggerHolder::set($logger);

对于更细粒度的控制和特殊情况处理,可以将自定义处理程序和筛选器应用于PSR-3记录器(如果记录器提供此功能)。

下一步

您还需要配置适当的导出器将您的遥测数据导出到一个或多个遥测后端。

最后修改 December 13, 2023: improve glossary translation (46f8201b)