Skip to content

Commit aced06b

Browse files
authored
feat(otel): add OTLP integration (#2030)
1 parent b423e88 commit aced06b

23 files changed

Lines changed: 1130 additions & 46 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ jobs:
7979
if: ${{ matrix.os == 'windows-latest' || matrix.php.version == '7.2' || matrix.php.version == '7.3' || matrix.php.version == '7.4' || matrix.php.version == '8.0' }}
8080
run: composer remove spiral/roadrunner-http spiral/roadrunner-worker --dev --no-interaction --no-update
8181

82+
- name: Remove OpenTelemetry dependencies on unsupported PHP versions
83+
if: ${{ matrix.php.version == '7.2' || matrix.php.version == '7.3' || matrix.php.version == '7.4' || matrix.php.version == '8.0' }}
84+
run: composer remove open-telemetry/api open-telemetry/exporter-otlp open-telemetry/sdk --dev --no-interaction --no-update
85+
8286
- name: Set phpunit/phpunit version constraint
8387
run: composer require phpunit/phpunit:'${{ matrix.php.phpunit }}' --dev --no-interaction --no-update
8488

composer.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
"guzzlehttp/psr7": "^1.8.4|^2.1.1",
3838
"monolog/monolog": "^1.6|^2.0|^3.0",
3939
"nyholm/psr7": "^1.8",
40+
"open-telemetry/api": "^1.0",
41+
"open-telemetry/exporter-otlp": "^1.0",
42+
"open-telemetry/sdk": "^1.0",
4043
"phpbench/phpbench": "^1.0",
4144
"phpstan/phpstan": "^1.3",
4245
"phpunit/phpunit": "^8.5.52|^9.6.34",
@@ -74,7 +77,11 @@
7477
"phpstan": "vendor/bin/phpstan analyse"
7578
},
7679
"config": {
77-
"sort-packages": true
80+
"sort-packages": true,
81+
"allow-plugins": {
82+
"php-http/discovery": false,
83+
"tbachert/spi": false
84+
}
7885
},
7986
"prefer-stable": true
8087
}

src/Dsn.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,14 @@ public function getCspReportEndpointUrl(): string
192192
return $this->getBaseEndpointUrl() . '/security/?sentry_key=' . $this->publicKey;
193193
}
194194

195+
/**
196+
* Returns the URL of the API for the OTLP traces endpoint.
197+
*/
198+
public function getOtlpTracesEndpointUrl(): string
199+
{
200+
return $this->getBaseEndpointUrl() . '/integration/otlp/v1/traces/';
201+
}
202+
195203
/**
196204
* @see https://www.php.net/manual/en/language.oop5.magic.php#object.tostring
197205
*/
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Integration;
6+
7+
use Psr\Log\LoggerInterface;
8+
use Psr\Log\NullLogger;
9+
use Sentry\Client;
10+
use Sentry\Options;
11+
use Sentry\SentrySdk;
12+
use Sentry\State\Scope;
13+
use Sentry\Util\Http;
14+
15+
final class OTLPIntegration implements OptionAwareIntegrationInterface
16+
{
17+
/**
18+
* @var bool
19+
*/
20+
private $setupOtlpTracesExporter;
21+
22+
/**
23+
* @var string|null
24+
*/
25+
private $collectorUrl;
26+
27+
/**
28+
* @var Options|null
29+
*/
30+
private $options;
31+
32+
public function __construct(bool $setupOtlpTracesExporter = true, ?string $collectorUrl = null)
33+
{
34+
$this->setupOtlpTracesExporter = $setupOtlpTracesExporter;
35+
$this->collectorUrl = $collectorUrl;
36+
}
37+
38+
public function setOptions(Options $options): void
39+
{
40+
$this->options = $options;
41+
}
42+
43+
public function setupOnce(): void
44+
{
45+
$options = $this->options;
46+
47+
if ($options === null) {
48+
$this->logDebug('Skipping OTLPIntegration setup because client options were not provided.');
49+
50+
return;
51+
}
52+
53+
if ($options->isTracingEnabled()) {
54+
$this->logDebug('Skipping OTLPIntegration because Sentry tracing is enabled. Disable "traces_sample_rate", "traces_sampler", and "enable_tracing" before using OTLPIntegration.');
55+
56+
return;
57+
}
58+
59+
Scope::registerExternalPropagationContext(static function (): ?array {
60+
$currentHub = SentrySdk::getCurrentHub();
61+
$integration = $currentHub->getIntegration(self::class);
62+
63+
if (!$integration instanceof self) {
64+
return null;
65+
}
66+
67+
return $integration->getCurrentOpenTelemetryPropagationContext();
68+
});
69+
70+
if ($this->setupOtlpTracesExporter) {
71+
$this->configureOtlpTracesExporter($options);
72+
}
73+
}
74+
75+
public function getCollectorUrl(): ?string
76+
{
77+
return $this->collectorUrl;
78+
}
79+
80+
/**
81+
* @return array{trace_id: string, span_id: string}|null
82+
*/
83+
private function getCurrentOpenTelemetryPropagationContext(): ?array
84+
{
85+
if (!class_exists(\OpenTelemetry\API\Trace\Span::class)) {
86+
return null;
87+
}
88+
89+
$spanContext = \OpenTelemetry\API\Trace\Span::getCurrent()->getContext();
90+
91+
if (!$spanContext->isValid()) {
92+
return null;
93+
}
94+
95+
return [
96+
'trace_id' => $spanContext->getTraceId(),
97+
'span_id' => $spanContext->getSpanId(),
98+
];
99+
}
100+
101+
private function configureOtlpTracesExporter(Options $options): void
102+
{
103+
$endpoint = $this->collectorUrl;
104+
$headers = [];
105+
$dsn = $options->getDsn();
106+
107+
if ($endpoint === null && $dsn !== null) {
108+
$endpoint = $dsn->getOtlpTracesEndpointUrl();
109+
$headers['X-Sentry-Auth'] = Http::getSentryAuthHeader($dsn, Client::SDK_IDENTIFIER, Client::SDK_VERSION);
110+
}
111+
112+
if ($endpoint === null) {
113+
$this->logDebug('Skipping automatic OTLP exporter setup because neither a DSN nor a collector URL is configured.');
114+
115+
return;
116+
}
117+
118+
if (!$this->shouldConfigureOtlpTracesExporter()) {
119+
return;
120+
}
121+
122+
try {
123+
$transport = (new \OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory())->create(
124+
$endpoint,
125+
\OpenTelemetry\Contrib\Otlp\ContentTypes::PROTOBUF,
126+
$headers
127+
);
128+
$spanExporter = new \OpenTelemetry\Contrib\Otlp\SpanExporter($transport);
129+
$batchSpanProcessor = new \OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor(
130+
$spanExporter,
131+
\OpenTelemetry\API\Common\Time\Clock::getDefault()
132+
);
133+
134+
(new \OpenTelemetry\SDK\SdkBuilder())
135+
->setTracerProvider(new \OpenTelemetry\SDK\Trace\TracerProvider($batchSpanProcessor))
136+
->buildAndRegisterGlobal();
137+
} catch (\Throwable $exception) {
138+
$this->logDebug(\sprintf('Skipping automatic OTLP exporter setup because it could not be configured: %s', $exception->getMessage()));
139+
}
140+
}
141+
142+
private function shouldConfigureOtlpTracesExporter(): bool
143+
{
144+
if (\PHP_VERSION_ID < 80100) {
145+
$this->logDebug('Skipping automatic OTLP exporter setup because it requires PHP 8.1 or newer.');
146+
147+
return false;
148+
}
149+
150+
foreach ([
151+
\OpenTelemetry\API\Globals::class,
152+
\OpenTelemetry\API\Common\Time\Clock::class,
153+
\OpenTelemetry\SDK\SdkBuilder::class,
154+
\OpenTelemetry\SDK\Trace\TracerProvider::class,
155+
\OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor::class,
156+
\OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory::class,
157+
\OpenTelemetry\Contrib\Otlp\SpanExporter::class,
158+
] as $className) {
159+
if (!class_exists($className)) {
160+
$this->logDebug('Skipping automatic OTLP exporter setup because the required OpenTelemetry SDK/exporter classes are not available.');
161+
162+
return false;
163+
}
164+
}
165+
166+
try {
167+
if (!$this->isNoopTracerProvider(\OpenTelemetry\API\Globals::tracerProvider())) {
168+
$this->logDebug('Skipping automatic OTLP exporter setup because the existing OpenTelemetry tracer provider cannot be modified after construction.');
169+
170+
return false;
171+
}
172+
} catch (\Throwable $exception) {
173+
$this->logDebug(\sprintf('Skipping automatic OTLP exporter setup because the current OpenTelemetry tracer provider could not be inspected: %s', $exception->getMessage()));
174+
175+
return false;
176+
}
177+
178+
return true;
179+
}
180+
181+
private function isNoopTracerProvider(?object $tracerProvider): bool
182+
{
183+
return $tracerProvider === null || $tracerProvider instanceof \OpenTelemetry\API\Trace\NoopTracerProvider;
184+
}
185+
186+
private function logDebug(string $message): void
187+
{
188+
$this->getLogger()->debug($message);
189+
}
190+
191+
private function getLogger(): LoggerInterface
192+
{
193+
if ($this->options !== null) {
194+
return $this->options->getLoggerOrNullLogger();
195+
}
196+
197+
$currentHub = SentrySdk::getCurrentHub();
198+
$client = $currentHub->getClient();
199+
200+
if ($client !== null) {
201+
return $client->getOptions()->getLoggerOrNullLogger();
202+
}
203+
204+
return new NullLogger();
205+
}
206+
}

src/Logs/LogsAggregator.php

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,15 @@ public function add(
7272
$formattedMessage = $message;
7373
}
7474

75-
$log = (new Log($timestamp, $this->getTraceId($hub), $level, $formattedMessage))
75+
$traceData = $this->getTraceData($hub);
76+
$traceId = $traceData['trace_id'];
77+
$parentSpanId = $traceData['parent_span_id'];
78+
79+
$log = (new Log($timestamp, $traceId, $level, $formattedMessage))
7680
->setAttribute('sentry.release', $options->getRelease())
7781
->setAttribute('sentry.environment', $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT)
7882
->setAttribute('sentry.server.address', $options->getServerName())
79-
->setAttribute('sentry.trace.parent_span_id', $hub->getSpan() ? $hub->getSpan()->getSpanId() : null);
83+
->setAttribute('sentry.trace.parent_span_id', $parentSpanId);
8084

8185
if ($client instanceof Client) {
8286
$log->setAttribute('sentry.sdk.name', $client->getSdkIdentifier());
@@ -182,20 +186,41 @@ public function all(): array
182186
return $this->logs;
183187
}
184188

185-
private function getTraceId(HubInterface $hub): string
189+
/**
190+
* @return array{trace_id: string, parent_span_id: string|null}
191+
*/
192+
private function getTraceData(HubInterface $hub): array
186193
{
187194
$span = $hub->getSpan();
188195

189196
if ($span !== null) {
190-
return (string) $span->getTraceId();
197+
return [
198+
'trace_id' => (string) $span->getTraceId(),
199+
'parent_span_id' => (string) $span->getSpanId(),
200+
];
191201
}
192202

193-
$traceId = '';
203+
$traceData = null;
204+
205+
$hub->configureScope(static function (Scope $scope) use (&$traceData): void {
206+
$externalPropagationContext = Scope::getExternalPropagationContext();
207+
208+
if ($externalPropagationContext !== null) {
209+
$traceData = [
210+
'trace_id' => $externalPropagationContext['trace_id'],
211+
'parent_span_id' => $externalPropagationContext['span_id'],
212+
];
213+
214+
return;
215+
}
194216

195-
$hub->configureScope(static function (Scope $scope) use (&$traceId) {
196-
$traceId = (string) $scope->getPropagationContext()->getTraceId();
217+
$traceData = [
218+
'trace_id' => (string) $scope->getPropagationContext()->getTraceId(),
219+
'parent_span_id' => null,
220+
];
197221
});
198222

199-
return $traceId;
223+
/** @var array{trace_id: string, parent_span_id: string|null} $traceData */
224+
return $traceData;
200225
}
201226
}

src/Metrics/MetricsAggregator.php

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Sentry\SentrySdk;
1515
use Sentry\State\HubInterface;
1616
use Sentry\State\Scope;
17+
use Sentry\Tracing\SpanId;
18+
use Sentry\Tracing\TraceId;
1719
use Sentry\Unit;
1820
use Sentry\Util\RingBuffer;
1921

@@ -104,24 +106,12 @@ public function add(
104106
$attributes += $defaultAttributes;
105107
}
106108

107-
$spanId = null;
108-
$traceId = null;
109-
110-
$span = $hub->getSpan();
111-
if ($span !== null) {
112-
$spanId = $span->getSpanId();
113-
$traceId = $span->getTraceId();
114-
} else {
115-
$hub->configureScope(static function (Scope $scope) use (&$traceId, &$spanId) {
116-
$propagationContext = $scope->getPropagationContext();
117-
$traceId = $propagationContext->getTraceId();
118-
$spanId = $propagationContext->getSpanId();
119-
});
120-
}
109+
$traceContext = $this->getTraceContext($hub);
110+
$traceId = new TraceId($traceContext['trace_id']);
111+
$spanId = new SpanId($traceContext['span_id']);
121112

122113
$metricTypeClass = self::METRIC_TYPES[$type];
123114
/** @var Metric $metric */
124-
/** @phpstan-ignore-next-line */
125115
$metric = new $metricTypeClass($name, $value, $traceId, $spanId, $attributes, microtime(true), $unit);
126116

127117
if ($client !== null) {
@@ -146,4 +136,19 @@ public function flush(?HubInterface $hub = null): ?EventId
146136

147137
return $hub->captureEvent($event);
148138
}
139+
140+
/**
141+
* @return array{trace_id: string, span_id: string}
142+
*/
143+
private function getTraceContext(HubInterface $hub): array
144+
{
145+
$traceContext = null;
146+
147+
$hub->configureScope(static function (Scope $scope) use (&$traceContext): void {
148+
$traceContext = $scope->getTraceContext();
149+
});
150+
151+
/** @var array{trace_id: string, span_id: string} $traceContext */
152+
return $traceContext;
153+
}
149154
}

0 commit comments

Comments
 (0)