Skip to content

Commit

Permalink
attribute-based sampler
Browse files Browse the repository at this point in the history
this adds an attribute-based sampler, which can be configured to inspect a semconv attribute on a span, and either sample it
in our out by applying a PCRE regex to it.
adding an alternative syntax to define samplers and sampler args from env vars. now we also support "sampler1,...,samplerN" chaining
of samplers, and the ability to define sampler args via "<samplername>.<arg>=<value>,..."
  • Loading branch information
brettmc committed Jan 11, 2024
1 parent a196c13 commit f09cf80
Show file tree
Hide file tree
Showing 6 changed files with 464 additions and 3 deletions.
39 changes: 39 additions & 0 deletions examples/traces/features/attribute_based_sampling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Example;

require __DIR__ . '/../../../vendor/autoload.php';

use OpenTelemetry\SDK\Trace\TracerProviderFactory;

//@see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md
putenv('OTEL_RESOURCE_ATTRIBUTES=service.version=1.0.0');
putenv('OTEL_SERVICE_NAME=example-app');
putenv('OTEL_PHP_DETECTORS=none');
putenv('OTEL_LOG_LEVEL=warning');
putenv('OTEL_TRACES_SAMPLER=parentbased,attribute,traceidratio');
putenv('OTEL_TRACES_SAMPLER_ARG=attribute.name=http.path,attribute.mode=deny,attribute.pattern=\/health$|\/test$,traceidratio.probability=1.0');
putenv('OTEL_TRACES_EXPORTER=console');

echo 'Starting attribute-based sampler example' . PHP_EOL;

$tracerProvider = (new TracerProviderFactory())->create();

$tracer = $tracerProvider->getTracer('io.opentelemetry.contrib.php');

echo 'Starting Tracer' . PHP_EOL;

$span = $tracer->spanBuilder('root')->setAttribute('http.path', '/health')->startSpan();
$scope = $span->activate();

try {
//this span will be sampled iff the root was sampled (parent-based)
$tracer->spanBuilder('child')->startSpan()->end();
} finally {
$scope->detach();
}
$span->end();

$tracerProvider->shutdown();
2 changes: 2 additions & 0 deletions src/SDK/Common/Configuration/KnownValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ interface KnownValues
public const VALUE_ALWAYS_ON = 'always_on';
public const VALUE_ALWAYS_OFF = 'always_off';
public const VALUE_TRACE_ID_RATIO = 'traceidratio';
public const VALUE_ATTRIBUTE = 'attribute';
public const VALUE_PARENT_BASED = 'parentbased';
public const VALUE_PARENT_BASED_ALWAYS_ON = 'parentbased_always_on';
public const VALUE_PARENT_BASED_ALWAYS_OFF = 'parentbased_always_off';
public const VALUE_PARENT_BASED_TRACE_ID_RATIO = 'parentbased_traceidratio';
Expand Down
103 changes: 103 additions & 0 deletions src/SDK/Trace/Sampler/AttributeBasedSampler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\SDK\Trace\Sampler;

use InvalidArgumentException;
use OpenTelemetry\API\Behavior\LogsMessagesTrait;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\SDK\Common\Attribute\AttributesInterface;
use OpenTelemetry\SDK\Trace\SamplerInterface;
use OpenTelemetry\SDK\Trace\SamplingResult;

/**
* @phan-file-suppress PhanParamTooFewUnpack
*
* Attribute-based sampler for root spans.
* "allow" mode: only sample root span if attribute exists and matches the pattern, else do not sample
* "deny" mode: do not sample root span if attribute exists and matches the pattern, else defer to next sampler
*/
class AttributeBasedSampler implements SamplerInterface
{
use LogsMessagesTrait;

public const ALLOW = 'allow';
public const DENY = 'deny';
public const MODES = [
self::ALLOW,
self::DENY,
];

private SamplerInterface $delegate;
private string $mode;
private string $attribute;
private string $pattern;

/**
* @param SamplerInterface $delegate The sampler to defer to if a decision is not made by this sampler
* @param string $mode Sampling mode (deny or allow)
* @param string $attribute The SemConv trace attribute to test against, eg http.path, http.method
* @param string $pattern The PCRE regex pattern to match against, eg /\/health$|\/test$/
*/
public function __construct(SamplerInterface $delegate, string $mode, string $attribute, string $pattern)
{
if (!in_array($mode, self::MODES)) {
throw new InvalidArgumentException('Unknown Attribute sampler mode: ' . $mode);
}
$this->delegate = $delegate;
$this->mode = $mode;
$this->attribute = $attribute;
$this->pattern = $pattern;
}

public function shouldSample(ContextInterface $parentContext, string $traceId, string $spanName, int $spanKind, AttributesInterface $attributes, array $links): SamplingResult
{
switch ($this->mode) {
case self::ALLOW:
if (!$attributes->has($this->attribute)) {
return new SamplingResult(SamplingResult::DROP);
}
if ($this->matches((string) $attributes->get($this->attribute))) {
return new SamplingResult(SamplingResult::RECORD_AND_SAMPLE);
}

break;
case self::DENY:
if (!$attributes->has($this->attribute)) {
break;
}
if ($this->matches((string) $attributes->get($this->attribute))) {
return new SamplingResult(SamplingResult::DROP);
}

break;
default:
//do nothing
}

return $this->delegate->shouldSample(...func_get_args());
}

private function matches(string $value): bool
{
$result = @preg_match($this->pattern, $value);
if ($result === false) {
self::logWarning('Error when pattern matching attribute', [
'attribute.name' => $this->attribute,
'attribute.value' => $value,
'pattern' => $this->pattern,
'error' => preg_last_error_msg(),
]);

return false;
}

return (bool) $result;
}

public function getDescription(): string
{
return sprintf('AttributeSampler{mode=%s,attribute=%s,pattern=%s}+%s', $this->mode, $this->attribute, $this->pattern, $this->delegate->getDescription());
}
}
75 changes: 75 additions & 0 deletions src/SDK/Trace/SamplerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use OpenTelemetry\SDK\Common\Configuration\Variables as Env;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOffSampler;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
use OpenTelemetry\SDK\Trace\Sampler\AttributeBasedSampler;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\Sampler\TraceIdRatioBasedSampler;

Expand All @@ -20,6 +21,12 @@ class SamplerFactory
public function create(): SamplerInterface
{
$name = Configuration::getString(Env::OTEL_TRACES_SAMPLER);
if (strpos($name, ',') !== false) {
$parts = explode(',', $name);
$arg = Configuration::has(Env::OTEL_TRACES_SAMPLER_ARG) ? Configuration::getString(Env::OTEL_TRACES_SAMPLER_ARG) : '';

return $this->buildCompositeSampler($parts, $arg);
}

if (strpos($name, self::TRACEIDRATIO_PREFIX) !== false) {
$arg = Configuration::getRatio(Env::OTEL_TRACES_SAMPLER_ARG);
Expand All @@ -45,4 +52,72 @@ public function create(): SamplerInterface
throw new InvalidArgumentException(sprintf('Unknown sampler: %s', $name));
}
}

private function buildCompositeSampler(array $names, string $args): SamplerInterface
{
$sampler = null;
foreach (array_reverse($names) as $name) {
$sampler = $this->buildSampler($name, $args, $sampler);
}
assert($sampler !== null);

return $sampler;
}

private function buildSampler(string $name, string $args, ?SamplerInterface $next): SamplerInterface
{
switch ($name) {
case Values::VALUE_ALWAYS_ON:
return new AlwaysOnSampler();
case Values::VALUE_ALWAYS_OFF:
return new AlwaysOffSampler();
case Values::VALUE_PARENT_BASED:
assert($next !== null);

return new ParentBased($next);
case Values::VALUE_TRACE_ID_RATIO:
if (strpos($args, '=') !== false) {
$probability = $this->splitArgs($args)[$name]['probability'] ?? null;
if ($probability === null) {
throw new InvalidArgumentException(sprintf('%s.probability=(numeric) not found in %s', $name, Env::OTEL_TRACES_SAMPLER_ARG));
}
} else {
//per specification, args may hold a single value
$probability = $args;
}

return new TraceIdRatioBasedSampler((float) $probability);
case Values::VALUE_ATTRIBUTE:
$split = $this->splitArgs($args)[$name];
assert($next !== null);

return new AttributeBasedSampler($next, $split['mode'], $split['name'], sprintf('/%s/', $split['pattern']));

default:
//@todo check a registry to support 3rd party samplers
throw new InvalidArgumentException('Unknown sampler: ' . $name);
}
}

/**
* Split arguments from <name>.<key>=<value>,<name2>.<key2>=<value2> into an associative array
*/
private function splitArgs(string $args): array
{
$return = [];
$parts = explode(',', $args);
foreach ($parts as $part) {
$equals = strpos($part, '=');
if ($equals === false) {
throw new InvalidArgumentException('Error parsing sampler arguments');
}
[$name, $key] = explode('.', substr($part, 0, $equals));
if (!array_key_exists($name, $return)) {
$return[$name] = [];
}
$return[$name][$key] = substr($part, $equals + 1);
}

return $return;
}
}
Loading

0 comments on commit f09cf80

Please sign in to comment.