-
Notifications
You must be signed in to change notification settings - Fork 206
PoC: Implement span suppression strategies #1599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Noop: does not suppress anything SpanKind: suppresses nested spans with the same span kind (except for internal) SemConv: attempts to guess and suppress the semantic convention that is represented by the span
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty solid. There's a lot of configuration, it seems like this will need to use declarative configuration, but that can be a follow-up change.
I'm happy to accept this as-is and complete it incrementally, if you don't have much time. If you're happy with that, can you add |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1599 +/- ##
============================================
- Coverage 69.40% 68.11% -1.29%
- Complexity 2805 2899 +94
============================================
Files 414 435 +21
Lines 8585 8790 +205
============================================
+ Hits 5958 5987 +29
- Misses 2627 2803 +176
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 24 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
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']), | ||
new SemanticConvention('span.http.server', SpanKind::KIND_SERVER, ['http.request.method', 'url.path', 'url.scheme'], ['network.peer.address', 'network.peer.port', 'error.type', 'http.request.body.size', 'http.request.method_original', '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.synthetic.type', 'client.address', 'client.port', 'http.request.header', 'http.route', 'network.local.address', 'network.local.port', 'user_agent.original', 'server.address', 'server.port', 'url.query']), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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']), | |
new SemanticConvention('span.http.server', SpanKind::KIND_SERVER, ['http.request.method', 'url.path', 'url.scheme'], ['network.peer.address', 'network.peer.port', 'error.type', 'http.request.body.size', 'http.request.method_original', '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.synthetic.type', 'client.address', 'client.port', 'http.request.header', 'http.route', 'network.local.address', 'network.local.port', 'user_agent.original', 'server.address', 'server.port', 'url.query']), | |
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']), | |
new SemanticConvention('span.http.server', SpanKind::KIND_SERVER, ['http.request.method', 'url.path', 'url.scheme'], ['network.peer.address', 'network.peer.port', 'error.type', 'http.request.body.size', 'http.request.method_original', '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.synthetic.type', 'client.address', 'client.port', 'http.request.header.*', 'http.route', 'network.local.address', 'network.local.port', 'user_agent.original', 'server.address', 'server.port', 'url.query']), |
Implementation does not handle template types yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must also add general attributes https://opentelemetry.io/docs/specs/semconv/general/attributes/
The attributes described in this section are not specific to a particular operation but rather generic. They may be used in any Span they apply to.
*/ | ||
interface SpanSuppressionStrategy | ||
{ | ||
public function getSuppressor(string $name, ?string $version, ?string $schemaUrl): SpanSuppressor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could accept InstrumentationScopeInterface
instead.
public function resolveSuppression(int $spanKind, array $attributes): SpanSuppression | ||
{ | ||
$semanticConventions = []; | ||
foreach ($this->semanticConventions as $entry) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can precompute matching/non-matching semantic conventions for each sampling relevant attribute which allows iterating the sampling relevant attributes and combining the lists of matching/non-matching semantic conventions instead.
Was able to increase performance of SpanBuilder::startSpan()
calls by ~7% for empty context, 11 span.http.client
attributes and the OpenTelemetry\Example\SemanticConventionResolver
. (Code to compute the masks is quite messy, thus not committing :); mentioning it as this seems like a good approach to improve performance for the semantic convention strategy.)
+--------------------------------------------------------+---------+---------+--------+---------+
| subject | memory | mode | rstdev | stdev |
+--------------------------------------------------------+---------+---------+--------+---------+
| benchSpanBuilderStart (noop,sampled,http) | 1.460mb | 1.707μs | ±1.74% | 0.030μs |
| benchSpanBuilderStart (spanKind,sampled,http) | 1.461mb | 1.816μs | ±1.38% | 0.025μs |
| benchSpanBuilderStart (semConv,sampled,http) | 1.465mb | 2.473μs | ±1.47% | 0.036μs |
| benchSpanBuilderStart (semConv-optimized,sampled,http) | 1.475mb | 2.303μs | ±1.75% | 0.040μs |
| benchSpanBuilderStart (noop,dropped,http) | 1.455mb | 1.052μs | ±1.49% | 0.016μs |
| benchSpanBuilderStart (spanKind,dropped,http) | 1.456mb | 1.153μs | ±1.73% | 0.020μs |
| benchSpanBuilderStart (semConv,dropped,http) | 1.460mb | 1.790μs | ±1.68% | 0.030μs |
| benchSpanBuilderStart (semConv-optimized,dropped,http) | 1.471mb | 1.626μs | ±2.06% | 0.033μs |
+--------------------------------------------------------+---------+---------+--------+---------+
Impact on SpanSuppressor::resolveSuppression()
:
+--------------------------------------------------+---------+---------+--------+---------+
| subject | memory | mode | rstdev | stdev |
+--------------------------------------------------+---------+---------+--------+---------+
| benchResolveSuppression (noop,http) | 1.410mb | 0.056μs | ±2.40% | 0.001μs |
| benchResolveSuppression (spanKind,http) | 1.410mb | 0.102μs | ±1.96% | 0.002μs |
| benchResolveSuppression (semConv,http) | 1.427mb | 0.693μs | ±1.60% | 0.011μs |
| benchResolveSuppression (semConv-optimized,http) | 1.431mb | 0.444μs | ±1.66% | 0.007μs |
+--------------------------------------------------+---------+---------+--------+---------+
SemanticConventionSuppressor using precomputed masks
$mask = 0;
foreach ($this->attributes[$spanKind->name] ?? [] as $attribute) {
// $attribute->m: semantic conventions that do not contain this attribute
// $attribute->n: semantic conventions that contain this attribute as sampling relevant attribute
$mask |= array_key_exists($attribute->name, $attributes) ? $attribute->m : $attribute->n;
}
$semanticConventions = [];
foreach ($this->semanticConventions as $i => $semanticConvention) {
if ($mask & 1 << $i) {
continue;
}
foreach ($attributes as $attribute => $_) {
if (array_key_exists($attribute, $semanticConvention->attributes)) {
continue;
}
if (array_key_exists(strrchr($attribute, '.') . '.*', $semanticConvention->attributes)) {
continue;
}
continue 2;
}
$semanticConventions[] = $semanticConvention->name;
}
Benchmark code (using local branch of different SDK -> needs some minor changes to work with this PR)
#[RetryThreshold(5)]
#[Warmup(100)]
#[Revs(10000)]
#[Iterations(100)]
#[OutputTimeUnit('seconds')]
#[OutputMode('throughput')]
final class SpanSuppressionStrategyBench {
private readonly SpanBuilderInterface $builder;
private readonly SpanSuppressor $suppressor;
public function setupTracer(array $params): void {
$tp = (new TracerProviderBuilder())
->setSampler($params['sampler'])
->setSuppressionStrategy($params['strategy'])
->build();
$tracer = $tp->getTracer('bench', schemaUrl: 'https://opentelemetry.io/schemas/1.33.0');
$this->builder = $tracer->spanBuilder('bench')
->setSpanKind($params['apiKind'])
->setAttributes($params['attributes']);
}
public function setupSuppressor(array $params): void {
$this->suppressor = $params['strategy']->getSuppressor(new InstrumentationScope('bench', null, 'https://opentelemetry.io/schemas/1.33.0', new Attributes([])));
}
#[BeforeMethods('setupTracer')]
#[ParamProviders(['suppressionStrategies', 'samplers', 'attributes'])]
public function benchSpanBuilderStart(): void {
$this->builder->startSpan();
}
#[BeforeMethods('setupSuppressor')]
#[ParamProviders(['suppressionStrategies', 'attributes'])]
public function benchResolveSuppression(array $params): SpanSuppression {
return $this->suppressor->resolveSuppression($params['sdkKind'], $params['attributes']);
}
public function suppressionStrategies(): iterable {
yield 'noop' => ['strategy' => new NoopSuppressionStrategy()];
yield 'spanKind' => ['strategy' => new SpanKindSuppressionStrategy()];
yield 'semConv' => ['strategy' => new SemanticConventionSuppressionStrategySimple([new SemConvResolver()])];
yield 'semConv-optimized' => ['strategy' => new SemanticConventionSuppressionStrategy([new SemConvResolver()])];
}
public function samplers(): iterable {
yield 'sampled' => ['sampler' => new AlwaysOnSampler()];
yield 'dropped' => ['sampler' => new AlwaysOffSampler()];
}
public function attributes(): iterable {
yield 'http' => ['apiKind' => SpanKind::KIND_CLIENT, 'sdkKind' => Kind::Client, 'attributes' => new Attributes([
'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.type' => 'ipv4',
'network.protocol.name' => 'http',
'network.transport' => 'tcp',
'user_agent.original' => 'i-am-a-test',
])];
}
}
/** | ||
* @param int<0, 4> $spanKind | ||
*/ | ||
public function resolveSuppression(int $spanKind, array $attributes): SpanSuppression; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could accept AttributesInterface
instead.
Note: SemanticConventionSuppressor
should still operate on the underlying ::toArray()
to avoid unnecessary method calls.
b0205b3
to
9b3653c
Compare
Alternative to #1583
Span suppression is implemented in the SDK -> instrumentation does not need to handle suppression.
(Everything besides
SemanticConvention
andSemanticConventionResolver
could be moved to the SDK.)Provides three span suppression strategies:
Noop: does not suppress anything
SpanKind: suppresses nested spans with the same span kind (except for internal)
SemConv: attempts to resolve the semantic convention based on span attributes, otherwise falls back to span kind suppression