Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/Instrumentation/ReactPHP/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@ This is a read-only subtree split of https://github.com/open-telemetry/opentelem

# OpenTelemetry ReactPHP HTTP Browser auto-instrumentation

This is an OpenTelemetry auto-instrumentation package for the [ReactPHP HTTP library](https://reactphp.org/http/). Currently only the Browser (client) component is instrumented.

Please read https://opentelemetry.io/docs/instrumentation/php/automatic/ for instructions on how to
install and configure the extension and SDK.

## Overview

Auto-instrumentation hooks are registered via composer, which will:
This library is provides the following:

* create spans automatically for each ReactPHP HTTP Browser request that is sent
* add a `traceparent` header to the request to facilitate distributed tracing
- OpenTelemetry Semantic Conventions v1.32.0:
- [HTTP Client Spans](https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span) - required and HTTP header (opt-in) attributes only
- [HTTP Client Metrics](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client) - required attributes only
- W3C Trace Context:
- [Trace Context HTTP Request Headers](https://www.w3.org/TR/trace-context/#trace-context-http-headers-format)

Note that span lifetime behavior differs based on how ReactPHP is utilized; see [examples/README.md](examples/README.md) for more information.
> [!NOTE]
> HTTP Client Span lifetime behavior differs based on how ReactPHP is utilized; see [examples/README.md](examples/README.md) for more information.

## Configuration

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

use OpenTelemetry\SDK\Metrics\Data\Temporality;
use OpenTelemetry\SDK\Metrics\MeterProvider;
use OpenTelemetry\SDK\Metrics\MetricExporter\ConsoleMetricExporter;
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Sdk;
use Psr\Http\Message\ResponseInterface;
use React\Http\Browser;
use React\Http\Message\Request;
use React\Http\Message\ResponseException;

require_once dirname(__DIR__) . '/vendor/autoload.php';

$exporter = new ConsoleMetricExporter(Temporality::CUMULATIVE);

$meterProvider = MeterProvider::builder()
->addReader(new ExportingReader($exporter))
->setResource(ResourceInfoFactory::emptyResource())
->build();

Sdk::builder()
->setMeterProvider($meterProvider)
->setAutoShutdown(true)
->buildAndRegisterGlobal();

try {
$browser = new Browser();

$requests = [
new Request('GET', 'https://postman-echo.com/get'),
new Request('GET', 'https://postman-echo.com/stream/33554432'),
new Request('POST', 'https://postman-echo.com/post', ['Content-Type' => 'application/json'], '{}'),
new Request('CUSTOM', 'http://postman-echo.com:443/get'),
new Request('GET', 'unknown://postman-echo.com/get'),
new Request('GET', 'https://postman-echo.com/delay/2'),
];

foreach ($requests as $request) {
$browser
->request($request->getMethod(), $request->getUri())
->then(function (ResponseInterface $response) use ($request) {
echo sprintf(
'[HTTP/%s %d %s] %s%s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase(),
$request->getUri(),
PHP_EOL
);
}, function (Throwable $t) use ($request) {
if (is_a($t, ResponseException::class)) {
$response = $t->getResponse();
echo sprintf(
'[HTTP/%s %d %s] %s%s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase(),
$request->getUri(),
PHP_EOL
);
} else {
echo sprintf(
'[%d: %s] %s%s',
$t->getCode(),
$t->getMessage(),
$request->getUri(),
PHP_EOL
);
}
});
}
} finally {
$meterProvider->forceFlush();
}
100 changes: 77 additions & 23 deletions src/Instrumentation/ReactPHP/src/ReactPHPInstrumentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Composer\InstalledVersions;
use GuzzleHttp\Psr7\Query;
use OpenTelemetry\API\Common\Time\Clock;
use OpenTelemetry\API\Common\Time\ClockInterface;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\Span;
Expand Down Expand Up @@ -81,6 +83,9 @@ class ReactPHPInstrumentation

public static function register(): void
{
/** @var \OpenTelemetry\API\Metrics\HistogramInterface|null */
static $histogram;

$instrumentation = new CachedInstrumentation(
self::INSTRUMENTATION_LIBRARY_NAME,
InstalledVersions::getPrettyVersion(self::COMPOSER_NAME),
Expand All @@ -102,24 +107,28 @@ public static function register(): void
$request = $request->withoutHeader($field);
}

/** @var non-empty-string|null */
$method = self::canonizeMethod($request->getMethod());
/** @var array{'http.request.method':non-empty-string|null,'server.address':non-empty-string,'server.port':int} $requestMeta */
$requestMeta = [
'http.request.method' => self::canonizeMethod($request->getMethod()),
'server.address' => $request->getUri()->getHost(),
'server.port' => $request->getUri()->getPort() ?? ($request->getUri()->getScheme() === 'https' ? 443 : 80),
];

$spanBuilder = $instrumentation
->tracer()
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
->spanBuilder($method ?? self::HTTP_REQUEST_METHOD_HTTP)
->spanBuilder($requestMeta['http.request.method'] ?? self::HTTP_REQUEST_METHOD_HTTP)
->setParent($parentContext)
->setSpanKind(SpanKind::KIND_CLIENT)
->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, $method ?? TraceAttributeValues::HTTP_REQUEST_METHOD_OTHER)
->setAttribute(TraceAttributes::SERVER_ADDRESS, $request->getUri()->getHost())
->setAttribute(TraceAttributes::SERVER_PORT, $request->getUri()->getPort() ?? ($request->getUri()->getScheme() === 'https' ? 443 : 80))
->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD, $requestMeta['http.request.method'] ?? TraceAttributeValues::HTTP_REQUEST_METHOD_OTHER)
->setAttribute(TraceAttributes::SERVER_ADDRESS, $requestMeta['server.address'])
->setAttribute(TraceAttributes::SERVER_PORT, $requestMeta['server.port'])
->setAttribute(TraceAttributes::URL_FULL, self::sanitizeUrl($request->getUri()))
// https://opentelemetry.io/docs/specs/semconv/code/
->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, sprintf('%s::%s', $class, $function));

// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
if ($method === null) {
if ($requestMeta['http.request.method'] === null) {
$spanBuilder->setAttribute(TraceAttributes::HTTP_REQUEST_METHOD_ORIGINAL, $request->getMethod());
}

Expand All @@ -133,7 +142,8 @@ public static function register(): void
$spanBuilder->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno);
}

$span = $spanBuilder->startSpan();
$requestStart = Clock::getDefault()->now();
$span = $spanBuilder->setStartTimestamp($requestStart)->startSpan();
$context = $span->storeInContext($parentContext);
$propagator->inject($request, HeadersPropagator::instance(), $context);

Expand All @@ -146,11 +156,13 @@ public static function register(): void
}
}

Context::storage()->attach($context);
$scope = Context::storage()->attach($context);
$scope->offsetSet('requestMeta', $requestMeta);
$scope->offsetSet('requestStart', $requestStart);

return [$request];
},
post: static function (Transaction $transaction, array $params, PromiseInterface $promise): PromiseInterface {
post: static function (Transaction $transaction, array $params, PromiseInterface $promise) use (&$histogram, $instrumentation): PromiseInterface {
$scope = Context::storage()->scope();
$scope?->detach();

Expand All @@ -160,21 +172,42 @@ public static function register(): void

$span = Span::fromContext($scope->context());

if (!$span->isRecording()) {
//https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-client
$histogram ??= $instrumentation->meter()->createHistogram(
'http.client.request.duration',
's',
'Duration of HTTP client requests.',
['ExplicitBucketBoundaries' => [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]]
);

if (!$span->isRecording() && !$histogram->isEnabled()) {
return $promise;
}

/** @var array{'http.request.method':non-empty-string|null,'server.address':non-empty-string,'server.port':int} $requestMeta */
$requestMeta = $scope->offsetGet('requestMeta');
$requestMeta['http.request.method'] ??= '_OTHER';
/** @var int $requestStart */
$requestStart = $scope->offsetGet('requestStart');

return $promise->then(
onFulfilled: function (ResponseInterface $response) use ($span) {
onFulfilled: function (ResponseInterface $response) use ($histogram, $requestMeta, $requestStart, $span) {
$requestEnd = Clock::getDefault()->now();
/** @var array{'http.response.status_code':int,'network.protocol.version':non-empty-string,'error.type'?:non-empty-string} $responseMeta */
$responseMeta = [
'http.response.status_code' => $response->getStatusCode(),
'network.protocol.version' => $response->getProtocolVersion(),
];
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
$span
->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode())
->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $response->getProtocolVersion());
->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $responseMeta['http.response.status_code'])
->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $responseMeta['network.protocol.version']);

if ($response->getStatusCode() >= 400 && $response->getStatusCode() < 600) {
if ($responseMeta['http.response.status_code'] >= 400 && $responseMeta['http.response.status_code'] < 600) {
$span
->setStatus(StatusCode::STATUS_ERROR)
->setAttribute(TraceAttributes::ERROR_TYPE, (string) $response->getStatusCode());
->setAttribute(TraceAttributes::ERROR_TYPE, (string) $responseMeta['http.response.status_code']);
$responseMeta['error.type'] = (string) $responseMeta['http.response.status_code'];
}

foreach (explode(',', $_ENV[self::ENV_HTTP_RESPONSE_HEADERS] ?? '') as $header) {
Expand All @@ -186,19 +219,31 @@ public static function register(): void
}
}

$span->end();
$span->end($requestEnd);

$histogram->record(
(float) (($requestEnd - $requestStart) / ClockInterface::NANOS_PER_SECOND),
array_merge($requestMeta, $responseMeta)
);

return $response;
},
onRejected: function (Throwable $t) use ($span) {
onRejected: function (Throwable $t) use ($histogram, $requestMeta, $requestStart, $span) {
$requestEnd = Clock::getDefault()->now();
$span->recordException($t);
if (is_a($t, ResponseException::class)) {
/** @var array{'http.response.status_code':int,'network.protocol.version':non-empty-string,'error.type':non-empty-string} $responseMeta */
$responseMeta = [
'error.type' => (string) $t->getCode(),
'http.response.status_code' => $t->getCode(),
'network.protocol.version' => $t->getResponse()->getProtocolVersion(),
];
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-client-span
$span
->setStatus(StatusCode::STATUS_ERROR)
->setAttribute(TraceAttributes::ERROR_TYPE, (string) $t->getCode())
->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $t->getCode())
->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $t->getResponse()->getProtocolVersion());
->setAttribute(TraceAttributes::ERROR_TYPE, $responseMeta['error.type'])
->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $responseMeta['http.response.status_code'])
->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $responseMeta['network.protocol.version']);

foreach (explode(',', $_ENV[self::ENV_HTTP_RESPONSE_HEADERS] ?? '') as $header) {
if ($t->getResponse()->hasHeader($header)) {
Expand All @@ -209,12 +254,21 @@ public static function register(): void
}
}
} else {
/** @var array{'error.type':non-empty-string} $responseMeta */
$responseMeta = [
'error.type' => $t::class,
];
$span
->setStatus(StatusCode::STATUS_ERROR, $t->getMessage())
->setAttribute(TraceAttributes::ERROR_TYPE, $t::class);
->setAttribute(TraceAttributes::ERROR_TYPE, $responseMeta['error.type']);
}

$span->end();
$span->end($requestEnd);

$histogram->record(
(float) (($requestEnd - $requestStart) / ClockInterface::NANOS_PER_SECOND),
array_merge($requestMeta, $responseMeta)
);

throw $t;
}
Expand Down
Loading