diff --git a/examples/spansuppression/SemanticConventionResolver.php b/examples/spansuppression/SemanticConventionResolver.php new file mode 100644 index 000000000..045d610b0 --- /dev/null +++ b/examples/spansuppression/SemanticConventionResolver.php @@ -0,0 +1,31 @@ +setResource(ResourceInfoFactory::emptyResource()) + ->addSpanProcessor(new BatchSpanProcessor(new SpanExporter((new StreamTransportFactory())->create('php://stdout', 'application/x-ndjson')), Clock::getDefault())) + ->setSpanSuppressionStrategy(new SemanticConventionSuppressionStrategy([ + new SemanticConventionResolver(), + ])) + ->build() +; + +$t = $tp->getTracer('test'); +$c1 = $tp + ->getTracer('instrumentation-1', schemaUrl: 'https://opentelemetry.io/schemas/1.33.0') + ->spanBuilder('GET') + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttributes([ + 'http.request.method' => 'GET', + 'server.address' => 'http://example.com', + 'server.port' => '80', + 'url.full' => 'http://example.com', + ]) + ->startSpan(); +$s1 = $c1->activate(); + +try { + $c2 = $tp + ->getTracer('instrumentation-2', schemaUrl: 'https://opentelemetry.io/schemas/1.31.0') + ->spanBuilder('GET') + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttributes([ + 'http.request.method' => 'GET', + 'server.address' => 'http://example.com', + 'server.port' => '80', + 'url.full' => 'http://example.com', + ]) + ->startSpan(); + $s2 = $c2->activate(); + + try { + // ... + } finally { + $s2->detach(); + $c2->end(); + } +} finally { + $s1->detach(); + $c1->end(); +} + +$tp->shutdown(); diff --git a/examples/spansuppression/semanticconventionssuppression-2.php b/examples/spansuppression/semanticconventionssuppression-2.php new file mode 100644 index 000000000..14eb1eda1 --- /dev/null +++ b/examples/spansuppression/semanticconventionssuppression-2.php @@ -0,0 +1,71 @@ +setResource(ResourceInfoFactory::emptyResource()) + ->addSpanProcessor(new BatchSpanProcessor(new SpanExporter((new StreamTransportFactory())->create('php://stdout', 'application/x-ndjson')), Clock::getDefault())) + ->setSpanSuppressionStrategy(new SemanticConventionSuppressionStrategy([ + new SemanticConventionResolver(), + ])) + ->build() +; + +$t = $tp->getTracer('test'); + +$c1 = $tp + ->getTracer('elasticsearch-instrumentation', schemaUrl: 'https://opentelemetry.io/schemas/1.33.0') + ->spanBuilder('SELECT') + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttributes([ + 'http.request.method' => 'GET', + 'server.address' => 'http://example.com', + 'server.port' => '80', + 'url.full' => 'http://example.com', + 'db.operation.name' => 'SELECT', + ]) + ->startSpan(); +$s1 = $c1->activate(); + +try { + $c2 = $tp + ->getTracer('httpclient-instrumentation', schemaUrl: 'https://opentelemetry.io/schemas/1.33.0') + ->spanBuilder('GET') + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttributes([ + 'http.request.method' => 'GET', + 'server.address' => 'http://example.com', + 'server.port' => '80', + 'url.full' => 'http://example.com', + ]) + ->startSpan(); + $s2 = $c2->activate(); + + try { + // ... + } finally { + $s2->detach(); + $c2->end(); + } +} finally { + $s1->detach(); + $c1->end(); +} + +$tp->shutdown(); diff --git a/examples/spansuppression/semanticconventionssuppression-3.php b/examples/spansuppression/semanticconventionssuppression-3.php new file mode 100644 index 000000000..c9c90f70b --- /dev/null +++ b/examples/spansuppression/semanticconventionssuppression-3.php @@ -0,0 +1,64 @@ +setResource(ResourceInfoFactory::emptyResource()) + ->addSpanProcessor(new BatchSpanProcessor(new SpanExporter((new StreamTransportFactory())->create('php://stdout', 'application/x-ndjson')), Clock::getDefault())) + ->setSpanSuppressionStrategy(new SemanticConventionSuppressionStrategy([ + new class() implements \OpenTelemetry\API\Trace\SpanSuppression\SemanticConventionResolver { + #[\Override] + public function resolveSemanticConventions(string $name, ?string $version, ?string $schemaUrl): array + { + if ($name !== 'io.open-telemetry.php.twig') { + return []; + } + + return [ + new SemanticConvention('io.open-telemetry.php.twig.template.render', SpanKind::KIND_INTERNAL, ['twig.template.name'], []), + ]; + } + }, + ])) + ->build() +; + +$t = $tp->getTracer('io.open-telemetry.php.twig'); +$c1 = $t->spanBuilder('render index.html.twig')->setAttribute('twig.template.name', 'index.html.twig')->startSpan(); +$s1 = $c1->activate(); + +try { + $c2 = $t->spanBuilder('render header.html.twig')->setAttribute('twig.template.name', 'header.html.twig')->startSpan(); + $s2 = $c2->activate(); + + try { + for ($i = 0; $i < 5; $i++) { + $t->spanBuilder('render meta.html.twig')->setAttribute('twig.template.name', 'meta.html.twig')->startSpan()->end(); + } + // ... + } finally { + $s2->detach(); + $c2->end(); + } +} finally { + $s1->detach(); + $c1->end(); +} + +$tp->shutdown(); diff --git a/examples/spansuppression/spankindsuppression.php b/examples/spansuppression/spankindsuppression.php new file mode 100644 index 000000000..7deed6e0a --- /dev/null +++ b/examples/spansuppression/spankindsuppression.php @@ -0,0 +1,44 @@ +setResource(ResourceInfoFactory::emptyResource()) + ->addSpanProcessor(new BatchSpanProcessor(new SpanExporter((new StreamTransportFactory())->create('php://stdout', 'application/x-ndjson')), Clock::getDefault())) + ->setSpanSuppressionStrategy(new SpanKindSuppressionStrategy()) + ->build() +; + +$t = $tp->getTracer('test'); +$c1 = $t->spanBuilder('client-1')->setSpanKind(SpanKind::KIND_CLIENT)->startSpan(); +$s1 = $c1->activate(); + +try { + $c2 = $t->spanBuilder('client-2')->setSpanKind(SpanKind::KIND_CLIENT)->startSpan(); + $s2 = $c2->activate(); + + try { + // ... + } finally { + $s2->detach(); + $c2->end(); + } +} finally { + $s1->detach(); + $c1->end(); +} + +$tp->shutdown(); diff --git a/src/API/Trace/Span.php b/src/API/Trace/Span.php index 243bb474a..72d4c8658 100644 --- a/src/API/Trace/Span.php +++ b/src/API/Trace/Span.php @@ -58,7 +58,7 @@ final public function activate(): ScopeInterface /** @inheritDoc */ #[\Override] - final public function storeInContext(ContextInterface $context): ContextInterface + public function storeInContext(ContextInterface $context): ContextInterface { if (LocalRootSpan::isLocalRoot($context)) { $context = LocalRootSpan::store($context, $this); diff --git a/src/API/Trace/SpanSuppression/SemanticConvention.php b/src/API/Trace/SpanSuppression/SemanticConvention.php new file mode 100644 index 000000000..799372b1f --- /dev/null +++ b/src/API/Trace/SpanSuppression/SemanticConvention.php @@ -0,0 +1,22 @@ + $samplingAttributes + */ + public function __construct( + public readonly string $name, + public readonly int $spanKind, + public readonly array $samplingAttributes, + public readonly array $attributes, + ) { + } +} diff --git a/src/API/Trace/SpanSuppression/SemanticConventionResolver.php b/src/API/Trace/SpanSuppression/SemanticConventionResolver.php new file mode 100644 index 000000000..d8647fa8d --- /dev/null +++ b/src/API/Trace/SpanSuppression/SemanticConventionResolver.php @@ -0,0 +1,16 @@ + + */ + public function resolveSemanticConventions(string $name, ?string $version, ?string $schemaUrl): array; +} diff --git a/src/API/composer.json b/src/API/composer.json index 27793059d..7d7785586 100644 --- a/src/API/composer.json +++ b/src/API/composer.json @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.4.x-dev" + "dev-main": "1.7.x-dev" }, "spi": { "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ diff --git a/src/SDK/Trace/Span.php b/src/SDK/Trace/Span.php index 49242750d..e00965061 100644 --- a/src/SDK/Trace/Span.php +++ b/src/SDK/Trace/Span.php @@ -14,6 +14,8 @@ use OpenTelemetry\SDK\Common\Exception\StackTraceFormatter; use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface; use OpenTelemetry\SDK\Resource\ResourceInfo; +use OpenTelemetry\SDK\Trace\SpanSuppression\NoopSuppressionStrategy\NoopSuppression; +use OpenTelemetry\SDK\Trace\SpanSuppression\SpanSuppression; use Throwable; final class Span extends API\Span implements ReadWriteSpanInterface @@ -44,6 +46,7 @@ private function __construct( private array $links, private int $totalRecordedLinks, private readonly int $startEpochNanos, + private readonly SpanSuppression $spanSuppression, ) { $this->status = StatusData::unset(); } @@ -73,6 +76,7 @@ public static function startSpan( array $links, int $totalRecordedLinks, int $startEpochNanos, + SpanSuppression $spanSuppression = new NoopSuppression(), ): self { $span = new self( $name, @@ -86,7 +90,8 @@ public static function startSpan( $attributesBuilder, $links, $totalRecordedLinks, - $startEpochNanos !== 0 ? $startEpochNanos : Clock::getDefault()->now() + $startEpochNanos !== 0 ? $startEpochNanos : Clock::getDefault()->now(), + $spanSuppression, ); // Call onStart here to ensure the span is fully initialized. @@ -111,6 +116,12 @@ public static function formatStackTrace(Throwable $e, ?array &$seen = null): str return StackTraceFormatter::format($e); } + #[\Override] + public function storeInContext(ContextInterface $context): ContextInterface + { + return $this->spanSuppression->suppress(parent::storeInContext($context)); + } + /** @inheritDoc */ #[\Override] public function getContext(): API\SpanContextInterface diff --git a/src/SDK/Trace/SpanBuilder.php b/src/SDK/Trace/SpanBuilder.php index 844175d79..c627b0342 100644 --- a/src/SDK/Trace/SpanBuilder.php +++ b/src/SDK/Trace/SpanBuilder.php @@ -10,6 +10,8 @@ use OpenTelemetry\Context\ContextInterface; use OpenTelemetry\SDK\Common\Attribute\AttributesBuilderInterface; use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface; +use OpenTelemetry\SDK\Trace\SpanSuppression\NoopSuppressionStrategy\NoopSuppressor; +use OpenTelemetry\SDK\Trace\SpanSuppression\SpanSuppressor; final class SpanBuilder implements API\SpanBuilderInterface { @@ -32,6 +34,7 @@ public function __construct( private readonly string $spanName, private readonly InstrumentationScopeInterface $instrumentationScope, private readonly TracerSharedState $tracerSharedState, + private readonly SpanSuppressor $spanSuppressor = new NoopSuppressor(), ) { $this->attributesBuilder = $this->tracerSharedState->getSpanLimits()->getAttributesFactory()->builder(); } @@ -125,6 +128,11 @@ public function startSpan(): API\SpanInterface $parentSpan = Span::fromContext($parentContext); $parentSpanContext = $parentSpan->getContext(); + $spanSuppression = $this->spanSuppressor->resolveSuppression($this->spanKind, $this->attributesBuilder->build()->toArray()); + if ($spanSuppression->isSuppressed($parentContext)) { + return Span::wrap($parentSpanContext); + } + $spanId = $this->tracerSharedState->getIdGenerator()->generateSpanId(); if (!$parentSpanContext->isValid()) { @@ -155,6 +163,7 @@ public function startSpan(): API\SpanInterface ); if (!in_array($samplingDecision, [SamplingResult::RECORD_AND_SAMPLE, SamplingResult::RECORD_ONLY], true)) { + // TODO must suppress no-op spans too return Span::wrap($spanContext); } @@ -176,7 +185,8 @@ public function startSpan(): API\SpanInterface $attributesBuilder, $this->links, $this->totalNumberOfLinksAdded, - $this->startEpochNanos + $this->startEpochNanos, + $spanSuppression, ); } } diff --git a/src/SDK/Trace/SpanSuppression/NoopSuppressionStrategy/NoopSuppression.php b/src/SDK/Trace/SpanSuppression/NoopSuppressionStrategy/NoopSuppression.php new file mode 100644 index 000000000..77f4824d7 --- /dev/null +++ b/src/SDK/Trace/SpanSuppression/NoopSuppressionStrategy/NoopSuppression.php @@ -0,0 +1,26 @@ +get($this->contextKey); + if ($suppressedConventions === null) { + return false; + } + + foreach ($this->semanticConventions as $semanticConvention) { + if (!isset($suppressedConventions[$semanticConvention])) { + return false; + } + } + + return true; + } + + #[\Override] + public function suppress(ContextInterface $context): ContextInterface + { + $suppressedConventions = $context->get($this->contextKey) ?? []; + foreach ($this->semanticConventions as $semanticConvention) { + $suppressedConventions[$semanticConvention] ??= true; + } + + return $context->with($this->contextKey, $suppressedConventions); + } +} diff --git a/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/SemanticConventionSuppressionContextKey.php b/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/SemanticConventionSuppressionContextKey.php new file mode 100644 index 000000000..fed36a7b4 --- /dev/null +++ b/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/SemanticConventionSuppressionContextKey.php @@ -0,0 +1,21 @@ +> + * + * @internal + */ +enum SemanticConventionSuppressionContextKey implements ContextKeyInterface +{ + case Internal; + case Client; + case Server; + case Producer; + case Consumer; +} diff --git a/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/SemanticConventionSuppressionStrategy.php b/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/SemanticConventionSuppressionStrategy.php new file mode 100644 index 000000000..b4e5f5dbf --- /dev/null +++ b/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/SemanticConventionSuppressionStrategy.php @@ -0,0 +1,78 @@ + $resolvers + */ + public function __construct( + private readonly iterable $resolvers, + ) { + } + + #[\Override] + public function getSuppressor(string $name, ?string $version, ?string $schemaUrl): SpanSuppressor + { + $semanticConventions = []; + foreach ($this->resolvers as $resolver) { + $semanticConventions[] = $resolver->resolveSemanticConventions($name, $version, $schemaUrl); + } + $semanticConventions = array_merge(...$semanticConventions); + + $lookup = []; + foreach ($semanticConventions as $semanticConvention) { + foreach ($semanticConvention->samplingAttributes as $attribute) { + assert(strcspn($attribute, '*?') === strlen($attribute)); + $lookup[$semanticConvention->spanKind][$attribute] ??= [0, 0]; + } + } + + $compiledSemanticConventions = []; + foreach ($semanticConventions as $semanticConvention) { + $attributes = new WildcardPattern(); + foreach ($semanticConvention->samplingAttributes as $attribute) { + $attributes->add($attribute); + } + foreach ($semanticConvention->attributes as $attribute) { + $attributes->add($attribute); + } + + $compiledSemanticConventions[$semanticConvention->spanKind][] = $semanticConvention->name; + $i = array_key_last($compiledSemanticConventions[$semanticConvention->spanKind]); + + foreach ($semanticConvention->samplingAttributes as $attribute) { + $lookup[$semanticConvention->spanKind][$attribute][0] |= 1 << $i; + } + foreach ($lookup[$semanticConvention->spanKind] as $attribute => $_) { + if (!$attributes->matches($attribute)) { + $lookup[$semanticConvention->spanKind][$attribute][1] |= 1 << $i; + } + } + } + + $compiledLookupAttributes = []; + foreach ($lookup as $spanKind => $attributes) { + foreach ($attributes as $attribute => $masks) { + $compiledLookupAttributes[$spanKind][] = new CompiledSemanticConventionAttribute($attribute, ~$masks[0], ~$masks[1]); + } + } + + return new SemanticConventionSuppressor($compiledSemanticConventions, $compiledLookupAttributes); + } +} diff --git a/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/SemanticConventionSuppressor.php b/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/SemanticConventionSuppressor.php new file mode 100644 index 000000000..668232a5c --- /dev/null +++ b/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/SemanticConventionSuppressor.php @@ -0,0 +1,70 @@ +> $semanticConventions + * @param array> $attributeMap + */ + public function __construct( + private readonly array $semanticConventions, + private readonly array $attributeMap, + ) { + } + + #[\Override] + public function resolveSuppression(int $spanKind, array $attributes): SpanSuppression + { + $candidates = $filter = (1 << count($this->semanticConventions[$spanKind] ?? [])) - 1; + foreach ($this->attributeMap[$spanKind] ?? [] as $attribute) { + // If attribute is present: exclude all semconvs not containing this attribute + // If attribute is not present: exclude all semconvs containing this attribute as sampling relevant attribute + if (array_key_exists($attribute->name, $attributes)) { + $filter &= $attribute->includedIn; + } else { + $candidates &= $attribute->notSamplingRelevantIn; + } + } + + if (!$candidates && $spanKind === SpanKind::KIND_INTERNAL) { + static $suppression = new NoopSuppression(); + + return $suppression; + } + + if ($candidates & $filter) { + $candidates &= $filter; + } + + $semanticConventions = []; + foreach ($this->semanticConventions[$spanKind] ?? [] as $i => $semanticConvention) { + if ($candidates >> $i & 1) { + $semanticConventions[] = $semanticConvention; + } + } + + return new SemanticConventionSuppression( + contextKey: match ($spanKind) { + SpanKind::KIND_INTERNAL => SemanticConventionSuppressionContextKey::Internal, + SpanKind::KIND_CLIENT => SemanticConventionSuppressionContextKey::Client, + SpanKind::KIND_SERVER => SemanticConventionSuppressionContextKey::Server, + SpanKind::KIND_PRODUCER => SemanticConventionSuppressionContextKey::Producer, + SpanKind::KIND_CONSUMER => SemanticConventionSuppressionContextKey::Consumer, + }, + semanticConventions: $semanticConventions, + ); + } +} diff --git a/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/WildcardPattern.php b/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/WildcardPattern.php new file mode 100644 index 000000000..6f5e44c53 --- /dev/null +++ b/src/SDK/Trace/SpanSuppression/SemanticConventionSuppressionStrategy/WildcardPattern.php @@ -0,0 +1,45 @@ +static[$pattern] = true; + + return; + } + + $this->patterns[] = sprintf('/^%s$/', strtr(preg_quote($pattern, '/'), ['\\?' => '.', '\\*' => '.*'])); + } + + public function matches(string $value): bool + { + if (isset($this->static[$value])) { + return true; + } + + foreach ($this->patterns as $pattern) { + if (preg_match($pattern, $value)) { + return true; + } + } + + return false; + } +} diff --git a/src/SDK/Trace/SpanSuppression/SpanKindSuppressionStrategy/SpanKindSuppression.php b/src/SDK/Trace/SpanSuppression/SpanKindSuppressionStrategy/SpanKindSuppression.php new file mode 100644 index 000000000..713f2ee2c --- /dev/null +++ b/src/SDK/Trace/SpanSuppression/SpanKindSuppressionStrategy/SpanKindSuppression.php @@ -0,0 +1,32 @@ +get($this->contextKey) === true; + } + + #[\Override] + public function suppress(ContextInterface $context): ContextInterface + { + return $context->with($this->contextKey, true); + } +} diff --git a/src/SDK/Trace/SpanSuppression/SpanKindSuppressionStrategy/SpanKindSuppressionContextKey.php b/src/SDK/Trace/SpanSuppression/SpanKindSuppressionStrategy/SpanKindSuppressionContextKey.php new file mode 100644 index 000000000..11b91ff16 --- /dev/null +++ b/src/SDK/Trace/SpanSuppression/SpanKindSuppressionStrategy/SpanKindSuppressionContextKey.php @@ -0,0 +1,20 @@ + + * + * @internal + */ +enum SpanKindSuppressionContextKey implements ContextKeyInterface +{ + case Client; + case Server; + case Producer; + case Consumer; +} diff --git a/src/SDK/Trace/SpanSuppression/SpanKindSuppressionStrategy/SpanKindSuppressionStrategy.php b/src/SDK/Trace/SpanSuppression/SpanKindSuppressionStrategy/SpanKindSuppressionStrategy.php new file mode 100644 index 000000000..e9a880581 --- /dev/null +++ b/src/SDK/Trace/SpanSuppression/SpanKindSuppressionStrategy/SpanKindSuppressionStrategy.php @@ -0,0 +1,22 @@ + new NoopSuppression(), + SpanKind::KIND_CLIENT => new SpanKindSuppression(SpanKindSuppressionContextKey::Client), + SpanKind::KIND_SERVER => new SpanKindSuppression(SpanKindSuppressionContextKey::Server), + SpanKind::KIND_PRODUCER => new SpanKindSuppression(SpanKindSuppressionContextKey::Producer), + SpanKind::KIND_CONSUMER => new SpanKindSuppression(SpanKindSuppressionContextKey::Consumer), + }; + } +} diff --git a/src/SDK/Trace/SpanSuppression/SpanSuppression.php b/src/SDK/Trace/SpanSuppression/SpanSuppression.php new file mode 100644 index 000000000..c626ef540 --- /dev/null +++ b/src/SDK/Trace/SpanSuppression/SpanSuppression.php @@ -0,0 +1,17 @@ + $spanKind + */ + public function resolveSuppression(int $spanKind, array $attributes): SpanSuppression; +} diff --git a/src/SDK/Trace/Tracer.php b/src/SDK/Trace/Tracer.php index e95c02a51..16f1151f7 100644 --- a/src/SDK/Trace/Tracer.php +++ b/src/SDK/Trace/Tracer.php @@ -10,6 +10,8 @@ use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface; use OpenTelemetry\SDK\Common\InstrumentationScope\Config; use OpenTelemetry\SDK\Common\InstrumentationScope\Configurator; +use OpenTelemetry\SDK\Trace\SpanSuppression\NoopSuppressionStrategy\NoopSuppressor; +use OpenTelemetry\SDK\Trace\SpanSuppression\SpanSuppressor; class Tracer implements API\TracerInterface { @@ -20,6 +22,7 @@ public function __construct( private readonly TracerSharedState $tracerSharedState, private readonly InstrumentationScopeInterface $instrumentationScope, ?Configurator $configurator = null, + private readonly SpanSuppressor $spanSuppressor = new NoopSuppressor(), ) { $this->config = $configurator ? $configurator->resolve($this->instrumentationScope) : TracerConfig::default(); } @@ -40,6 +43,7 @@ public function spanBuilder(string $spanName): API\SpanBuilderInterface $spanName, $this->instrumentationScope, $this->tracerSharedState, + $this->spanSuppressor, ); } diff --git a/src/SDK/Trace/TracerProvider.php b/src/SDK/Trace/TracerProvider.php index 420054085..9c0e10e28 100644 --- a/src/SDK/Trace/TracerProvider.php +++ b/src/SDK/Trace/TracerProvider.php @@ -16,6 +16,8 @@ use OpenTelemetry\SDK\Resource\ResourceInfoFactory; use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler; use OpenTelemetry\SDK\Trace\Sampler\ParentBased; +use OpenTelemetry\SDK\Trace\SpanSuppression\NoopSuppressionStrategy\NoopSuppressionStrategy; +use OpenTelemetry\SDK\Trace\SpanSuppression\SpanSuppressionStrategy; use WeakMap; final class TracerProvider implements TracerProviderInterface @@ -33,6 +35,7 @@ public function __construct( ?IdGeneratorInterface $idGenerator = null, ?InstrumentationScopeFactoryInterface $instrumentationScopeFactory = null, private ?Configurator $configurator = null, + private SpanSuppressionStrategy $spanSuppressionStrategy = new NoopSuppressionStrategy(), ) { $spanProcessors ??= []; $spanProcessors = is_array($spanProcessors) ? $spanProcessors : [$spanProcessors]; @@ -77,6 +80,7 @@ public function getTracer( $this->tracerSharedState, $scope, $this->configurator, + $this->spanSuppressionStrategy->getSuppressor($name, $version, $schemaUrl), ); $this->tracers->offsetSet($tracer, null); diff --git a/src/SDK/Trace/TracerProviderBuilder.php b/src/SDK/Trace/TracerProviderBuilder.php index 7403a2c91..1a94460e6 100644 --- a/src/SDK/Trace/TracerProviderBuilder.php +++ b/src/SDK/Trace/TracerProviderBuilder.php @@ -6,6 +6,8 @@ use OpenTelemetry\SDK\Common\InstrumentationScope\Configurator; use OpenTelemetry\SDK\Resource\ResourceInfo; +use OpenTelemetry\SDK\Trace\SpanSuppression\NoopSuppressionStrategy\NoopSuppressionStrategy; +use OpenTelemetry\SDK\Trace\SpanSuppression\SpanSuppressionStrategy; class TracerProviderBuilder { @@ -14,6 +16,7 @@ class TracerProviderBuilder private ?ResourceInfo $resource = null; private ?SamplerInterface $sampler = null; private ?Configurator $configurator = null; + private ?SpanSuppressionStrategy $spanSuppressionStrategy = null; public function addSpanProcessor(SpanProcessorInterface $spanProcessor): self { @@ -43,6 +46,13 @@ public function setConfigurator(Configurator $configurator): self return $this; } + public function setSpanSuppressionStrategy(SpanSuppressionStrategy $spanSuppressionStrategy): self + { + $this->spanSuppressionStrategy = $spanSuppressionStrategy; + + return $this; + } + public function build(): TracerProviderInterface { return new TracerProvider( @@ -50,6 +60,7 @@ public function build(): TracerProviderInterface $this->sampler, $this->resource, configurator: $this->configurator ?? Configurator::tracer(), + spanSuppressionStrategy: $this->spanSuppressionStrategy ?? new NoopSuppressionStrategy(), ); } } diff --git a/src/SDK/composer.json b/src/SDK/composer.json index a331182c5..7eb96f7ea 100644 --- a/src/SDK/composer.json +++ b/src/SDK/composer.json @@ -20,7 +20,7 @@ "php": "^8.1", "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "^1.6", + "open-telemetry/api": "^1.7", "open-telemetry/context": "^1.4", "open-telemetry/sem-conv": "^1.0", "php-http/discovery": "^1.14", @@ -55,7 +55,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.0.x-dev" + "dev-main": "1.9.x-dev" }, "spi": { "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ diff --git a/tests/Unit/SDK/Trace/SpanSuppressionTest.php b/tests/Unit/SDK/Trace/SpanSuppressionTest.php new file mode 100644 index 000000000..373eeac2d --- /dev/null +++ b/tests/Unit/SDK/Trace/SpanSuppressionTest.php @@ -0,0 +1,181 @@ +getSuppressor('test', null, null); + $suppression = $suppressor->resolveSuppression(SpanKind::KIND_CLIENT, []); + + $context = Context::getCurrent(); + $this->assertFalse($suppression->isSuppressed($context)); + + $context = $suppression->suppress($context); + $this->assertFalse($suppression->isSuppressed($context)); + } + + public function test_span_kind_suppression_strategy_only_affects_specific_span_kind(): void + { + $strategy = new SpanKindSuppressionStrategy(); + $suppressor = $strategy->getSuppressor('test', null, null); + + $clientSuppression = $suppressor->resolveSuppression(SpanKind::KIND_CLIENT, []); + $serverSuppression = $suppressor->resolveSuppression(SpanKind::KIND_SERVER, []); + + $context = Context::getCurrent(); + $this->assertFalse($clientSuppression->isSuppressed($context)); + $this->assertFalse($serverSuppression->isSuppressed($context)); + + $context = $clientSuppression->suppress($context); + $this->assertTrue($clientSuppression->isSuppressed($context)); + $this->assertFalse($serverSuppression->isSuppressed($context)); + } + + public function test_span_kind_suppression_strategy_internal_is_never_suppressed(): void + { + $strategy = new SpanKindSuppressionStrategy(); + $suppressor = $strategy->getSuppressor('test', null, null); + + $internalSuppression = $suppressor->resolveSuppression(SpanKind::KIND_INTERNAL, []); + + $context = Context::getCurrent(); + $this->assertFalse($internalSuppression->isSuppressed($context)); + + $context = $internalSuppression->suppress($context); + $this->assertFalse($internalSuppression->isSuppressed($context)); + } + + public function test_semantic_convention_suppression_strategy_affects_only_resolved_semantic_convention(): void + { + $strategy = new SemanticConventionSuppressionStrategy([ + new class() implements SemanticConventionResolver { + #[\Override] + public function resolveSemanticConventions(string $name, ?string $version, ?string $schemaUrl): array + { + return [ + new SemanticConvention('span.db.elasticsearch.client', SpanKind::KIND_CLIENT, ['http.request.method', 'url.full', 'db.operation.name'], ['error.type', 'elasticsearch.node.name', 'db.operation.batch.size', 'server.address', 'server.port', 'db.collection.name', 'db.namespace', 'db.operation.parameter', 'db.query.text', 'db.response.status_code', 'code.*']), + new SemanticConvention('span.http.client', SpanKind::KIND_CLIENT, ['http.request.method', 'server.address', 'server.port', 'url.full'], ['network.peer.address', 'network.peer.port', 'error.type', 'http.request.body.size', 'http.request.header.*', 'http.request.method_original', 'http.request.resend_count', 'http.request.size', 'http.response.body.size', 'http.response.header.*', 'http.response.size', 'http.response.status_code', 'network.protocol.name', 'network.protocol.version', 'network.transport', 'user_agent.original', 'user_agent.synthetic.type', 'url.scheme', 'url.template', 'code.*']), + ]; + } + }, + ]); + $suppressor = $strategy->getSuppressor('test', null, null); + + $elasticsearchSuppression = $suppressor->resolveSuppression(SpanKind::KIND_CLIENT, [ + 'http.request.method' => 'GET', + 'server.address' => 'https://example.com', + 'server.port' => '443', + 'url.full' => 'https://example.com', + 'db.operation.name' => 'SELECT', + ]); + $httpSuppression = $suppressor->resolveSuppression(SpanKind::KIND_CLIENT, [ + 'http.request.method' => 'GET', + 'server.address' => 'https://example.com', + 'server.port' => '443', + 'url.full' => 'https://example.com', + ]); + $httpSuppression2 = $suppressor->resolveSuppression(SpanKind::KIND_CLIENT, [ + 'http.request.method' => 'GET', + 'server.address' => 'https://example.com', + 'server.port' => '443', + 'url.full' => 'https://example.com', + 'url.scheme' => 'https', + 'http.request.body.size' => 0, + 'network.peer.address' => '127.0.0.1', + 'network.peer.port' => '443', + 'network.protocol.name' => 'http', + 'network.transport' => 'tcp', + 'user_agent.original' => 'i-am-a-test', + ]); + + $context = Context::getCurrent(); + $this->assertFalse($elasticsearchSuppression->isSuppressed($context)); + $this->assertFalse($httpSuppression->isSuppressed($context)); + $this->assertFalse($httpSuppression2->isSuppressed($context)); + + $context = $elasticsearchSuppression->suppress($context); + $this->assertTrue($elasticsearchSuppression->isSuppressed($context)); + $this->assertFalse($httpSuppression->isSuppressed($context)); + $this->assertFalse($httpSuppression2->isSuppressed($context)); + + $context = $httpSuppression->suppress($context); + $this->assertTrue($elasticsearchSuppression->isSuppressed($context)); + $this->assertTrue($httpSuppression->isSuppressed($context)); + $this->assertTrue($httpSuppression2->isSuppressed($context)); + } + + public function test_semantic_convention_suppression_strategy_falls_back_to_span_kind_suppression_for_non_internal(): void + { + $strategy = new SemanticConventionSuppressionStrategy([ + new class() implements SemanticConventionResolver { + #[\Override] + public function resolveSemanticConventions(string $name, ?string $version, ?string $schemaUrl): array + { + return [ + new SemanticConvention('span.http.client', SpanKind::KIND_CLIENT, ['http.request.method', 'server.address', 'server.port', 'url.full'], ['network.peer.address', 'network.peer.port', 'error.type', 'http.request.body.size', 'http.request.header.*', 'http.request.method_original', 'http.request.resend_count', 'http.request.size', 'http.response.body.size', 'http.response.header.*', 'http.response.size', 'http.response.status_code', 'network.protocol.name', 'network.protocol.version', 'network.transport', 'user_agent.original', 'user_agent.synthetic.type', 'url.scheme', 'url.template', 'code.*']), + ]; + } + }, + ]); + $suppressor = $strategy->getSuppressor('test', null, null); + + $httpSuppression = $suppressor->resolveSuppression(SpanKind::KIND_CLIENT, [ + 'http.request.method' => 'GET', + 'server.address' => 'https://example.com', + 'server.port' => '443', + 'url.full' => 'https://example.com', + ]); + $clientSuppression = $suppressor->resolveSuppression(SpanKind::KIND_CLIENT, []); + + $context = Context::getCurrent(); + $this->assertFalse($httpSuppression->isSuppressed($context)); + $this->assertFalse($clientSuppression->isSuppressed($context)); + + $context = $httpSuppression->suppress($context); + $this->assertTrue($httpSuppression->isSuppressed($context)); + $this->assertTrue($clientSuppression->isSuppressed($context)); + + $context = Context::getCurrent(); + $context = $clientSuppression->suppress($context); + $this->assertFalse($httpSuppression->isSuppressed($context)); + $this->assertTrue($clientSuppression->isSuppressed($context)); + } + + public function test_semantic_convention_suppression_strategy_does_not_fail_on_empty_semantic_conventions(): void + { + $this->expectNotToPerformAssertions(); + + $strategy = new SemanticConventionSuppressionStrategy([]); + $strategy->getSuppressor('test', null, null)->resolveSuppression(SpanKind::KIND_CLIENT, []); + } +}