From 673f2f9aed197fb12193d52a6136959ed0718e1a Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Thu, 24 Apr 2025 12:12:58 -0700 Subject: [PATCH 01/18] [aws-sdk]Added Initial auto-instrumentation library for aws-sdk --- src/Instrumentation/AwsSdk/.gitignore | 2 + src/Instrumentation/AwsSdk/.php-cs-fixer.php | 45 ++++++++++ src/Instrumentation/AwsSdk/README.md | 25 ++++++ src/Instrumentation/AwsSdk/_register.php | 20 +++++ src/Instrumentation/AwsSdk/composer.json | 47 ++++++++++ src/Instrumentation/AwsSdk/phpstan.neon.dist | 12 +++ src/Instrumentation/AwsSdk/phpunit.xml.dist | 47 ++++++++++ src/Instrumentation/AwsSdk/psalm.xml.dist | 15 ++++ .../AwsSdk/src/AwsSdkInstrumentation.php | 89 +++++++++++++++++++ .../Integration/AwsSdkInstrumentationTest.php | 83 +++++++++++++++++ .../AwsSdk/tests/Unit/.gitkeep | 0 11 files changed, 385 insertions(+) create mode 100644 src/Instrumentation/AwsSdk/.gitignore create mode 100644 src/Instrumentation/AwsSdk/.php-cs-fixer.php create mode 100644 src/Instrumentation/AwsSdk/README.md create mode 100644 src/Instrumentation/AwsSdk/_register.php create mode 100644 src/Instrumentation/AwsSdk/composer.json create mode 100644 src/Instrumentation/AwsSdk/phpstan.neon.dist create mode 100644 src/Instrumentation/AwsSdk/phpunit.xml.dist create mode 100644 src/Instrumentation/AwsSdk/psalm.xml.dist create mode 100644 src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php create mode 100644 src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php create mode 100644 src/Instrumentation/AwsSdk/tests/Unit/.gitkeep diff --git a/src/Instrumentation/AwsSdk/.gitignore b/src/Instrumentation/AwsSdk/.gitignore new file mode 100644 index 000000000..3a9875b46 --- /dev/null +++ b/src/Instrumentation/AwsSdk/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/src/Instrumentation/AwsSdk/.php-cs-fixer.php b/src/Instrumentation/AwsSdk/.php-cs-fixer.php new file mode 100644 index 000000000..73e33d1ef --- /dev/null +++ b/src/Instrumentation/AwsSdk/.php-cs-fixer.php @@ -0,0 +1,45 @@ +exclude('vendor') + ->exclude('tests/Unit81') //contains php8.1 syntax + ->exclude('var/cache') + ->exclude('tests/coverage') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +return $config->setRules([ + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'none'], + 'is_null' => true, + 'modernize_types_casting' => true, + 'ordered_imports' => true, + 'php_unit_construct' => true, + 'single_line_comment_style' => true, + 'yoda_style' => false, + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => true, + 'cast_spaces' => true, + 'declare_strict_types' => true, + 'type_declaration_spaces' => true, + 'include' => true, + 'lowercase_cast' => true, + 'new_with_parentheses' => true, + 'no_extra_blank_lines' => true, + 'no_leading_import_slash' => true, + 'echo_tag_syntax' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'phpdoc_order' => true, + 'phpdoc_scalar' => true, + 'phpdoc_types' => true, + 'short_scalar_cast' => true, + 'blank_lines_before_namespace' => true, + 'single_quote' => true, + 'trailing_comma_in_multiline' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder($finder); + diff --git a/src/Instrumentation/AwsSdk/README.md b/src/Instrumentation/AwsSdk/README.md new file mode 100644 index 000000000..78646ed18 --- /dev/null +++ b/src/Instrumentation/AwsSdk/README.md @@ -0,0 +1,25 @@ +[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-auto-aws-sdk/releases) +[![Issues](https://img.shields.io/badge/issues-pink)](https://github.com/open-telemetry/opentelemetry-php/issues) +[![Source](https://img.shields.io/badge/source-contrib-green)](https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Instrumentation/AwsSdk) +[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/contrib-auto-aws-sdk) +[![Latest Version](http://poser.pugx.org/open-telemetry/opentelemetry-auto-guzzle/v/unstable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-aws-sdk/) +[![Stable](http://poser.pugx.org/open-telemetry/opentelemetry-auto-aws-sdk/v/stable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-aws-sdk/) + +This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib. + +# OpenTelemetry AWS SDK auto-instrumentation +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. + +* create spans automatically for each AWS SDK request that is sent + +## Configuration + +The extension can be disabled via [runtime configuration](https://opentelemetry.io/docs/instrumentation/php/sdk/#configuration): + +```shell +OTEL_PHP_DISABLED_INSTRUMENTATIONS=aws-sdk +``` diff --git a/src/Instrumentation/AwsSdk/_register.php b/src/Instrumentation/AwsSdk/_register.php new file mode 100644 index 000000000..b701f8a0c --- /dev/null +++ b/src/Instrumentation/AwsSdk/_register.php @@ -0,0 +1,20 @@ + + + + + + + src + + + + + + + + + + + + + tests/Unit + + + tests/Integration + + + + diff --git a/src/Instrumentation/AwsSdk/psalm.xml.dist b/src/Instrumentation/AwsSdk/psalm.xml.dist new file mode 100644 index 000000000..155711712 --- /dev/null +++ b/src/Instrumentation/AwsSdk/psalm.xml.dist @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php b/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php new file mode 100644 index 000000000..09b245cc2 --- /dev/null +++ b/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php @@ -0,0 +1,89 @@ +tracer() + ->spanBuilder("{$c->getApi()->getServiceName()}.{$cmd->getName()}") + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttribute('rpc.system', 'aws-api') + ->setAttribute('rpc.method', $cmd->getName()) + ->setAttribute('rpc.service', $c->getApi()->getServiceName()) + ->setAttribute('aws.region', $c->getRegion()) + ->setAttribute('code.function', $func) + ->setAttribute('code.namespace', $class) + ->setAttribute('code.filepath', $file) + ->setAttribute('code.line_number', $line); + + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext(Context::getCurrent())); + }, + post: static function ( + AwsClient $c, + array $params, + mixed $result, + ?\Throwable $ex + ) { + $scope = Context::storage()->scope(); + if (!$scope) { + return; + } + $span = Span::fromContext($scope->context()); + $scope->detach(); + + if ($result instanceof ResultInterface && isset($result['@metadata'])) { + $span->setAttribute('http.status_code', $result['@metadata']['statusCode']); + $span->setAttribute('aws.requestId', $result['@metadata']['headers']['x-amz-request-id']); + } + if ($ex) { + if ($ex instanceof AwsException && $ex->getAwsRequestId() !== null) { + $span->setAttribute('aws.requestId', $ex->getAwsRequestId()); + } + $span->recordException($ex); + $span->setStatus(StatusCode::STATUS_ERROR, $ex->getMessage()); + } + $span->end(); + } + ); + } +} diff --git a/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php b/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php new file mode 100644 index 000000000..13612df16 --- /dev/null +++ b/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php @@ -0,0 +1,83 @@ +spans = new ArrayObject(); + $tracerProvider = new TracerProvider( + new SimpleSpanProcessor(new InMemoryExporter($this->spans)) + ); + $this->scope = Configurator::create() + ->withTracerProvider($tracerProvider) + ->withPropagator(TraceContextPropagator::getInstance()) + ->activate(); + + $this->mock = new MockHandler(); + $this->mock->append(new Result([ + '@metadata' => [ + 'statusCode' => 200, + 'headers' => [ + 'x-amz-request-id' => 'TEST-REQUEST-ID', + ], + ], + ])); + + $this->client = new S3Client([ + 'region' => 'us-west-2', + 'version' => 'latest', + 'handler' => $this->mock, + ]); + } + + public function tearDown(): void + { + $this->scope->detach(); + } + + public function test_listBuckets_generates_one_aws_span_with_expected_attributes(): void + { + $this->client->listBuckets(); + + $this->assertCount(1, $this->spans); + + $span = $this->spans->offsetGet(0); + + $this->assertInstanceOf(ImmutableSpan::class, $span); + + $this->assertSame('s3.ListBuckets', $span->getName()); + + $attrs = $span->getAttributes(); + $this->assertSame('aws-api', $attrs->get('rpc.system')); + $this->assertSame('s3', $attrs->get('rpc.service')); + $this->assertSame('ListBuckets', $attrs->get('rpc.method')); + $this->assertSame('us-west-2', $attrs->get('aws.region')); + $this->assertSame(200, $attrs->get('http.status_code')); + $this->assertSame('TEST-REQUEST-ID', $attrs->get('aws.requestId')); + } +} diff --git a/src/Instrumentation/AwsSdk/tests/Unit/.gitkeep b/src/Instrumentation/AwsSdk/tests/Unit/.gitkeep new file mode 100644 index 000000000..e69de29bb From 28a512eb1705c1d807127a50890e42cc95776868 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Tue, 29 Apr 2025 12:07:36 -0700 Subject: [PATCH 02/18] Updated to use sem covs instead of hardcoding --- src/Instrumentation/AwsSdk/composer.json | 3 +- .../AwsSdk/src/AwsSdkInstrumentation.php | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Instrumentation/AwsSdk/composer.json b/src/Instrumentation/AwsSdk/composer.json index e5bb1c5e7..ed2221efa 100644 --- a/src/Instrumentation/AwsSdk/composer.json +++ b/src/Instrumentation/AwsSdk/composer.json @@ -12,7 +12,8 @@ "php": "^8.2", "aws/aws-sdk-php": "^3", "ext-opentelemetry": "*", - "open-telemetry/api": "^1.0" + "open-telemetry/api": "^1.0", + "open-telemetry/sem-conv": "^1.30" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3", diff --git a/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php b/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php index 09b245cc2..7a061337e 100644 --- a/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php +++ b/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php @@ -14,6 +14,7 @@ use OpenTelemetry\API\Trace\StatusCode; use OpenTelemetry\Context\Context; use function OpenTelemetry\Instrumentation\hook; +use OpenTelemetry\SemConv\TraceAttributes; final class AwsSdkInstrumentation { @@ -38,22 +39,22 @@ public static function register(): void AwsClient $c, array $params, string $class, - string $func, - ?string $file, - ?int $line + string $function, + ?string $filename, + ?int $lineno ) use ($inst) { $cmd = $params[0]; $builder = $inst->tracer() ->spanBuilder("{$c->getApi()->getServiceName()}.{$cmd->getName()}") ->setSpanKind(SpanKind::KIND_CLIENT) - ->setAttribute('rpc.system', 'aws-api') - ->setAttribute('rpc.method', $cmd->getName()) - ->setAttribute('rpc.service', $c->getApi()->getServiceName()) - ->setAttribute('aws.region', $c->getRegion()) - ->setAttribute('code.function', $func) - ->setAttribute('code.namespace', $class) - ->setAttribute('code.filepath', $file) - ->setAttribute('code.line_number', $line); + ->setAttribute(TraceAttributes::RPC_SYSTEM, 'aws-api') + ->setAttribute(TraceAttributes::RPC_METHOD, $cmd->getName()) + ->setAttribute(TraceAttributes::RPC_SERVICE, $c->getApi()->getServiceName()) + ->setAttribute(TraceAttributes::CLOUD_REGION, $c->getRegion()) + ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno); $span = $builder->startSpan(); Context::storage()->attach($span->storeInContext(Context::getCurrent())); @@ -72,12 +73,12 @@ public static function register(): void $scope->detach(); if ($result instanceof ResultInterface && isset($result['@metadata'])) { - $span->setAttribute('http.status_code', $result['@metadata']['statusCode']); - $span->setAttribute('aws.requestId', $result['@metadata']['headers']['x-amz-request-id']); + $span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $result['@metadata']['statusCode']); + $span->setAttribute(TraceAttributes::AWS_REQUEST_ID, $result['@metadata']['headers']['x-amz-request-id']); } if ($ex) { if ($ex instanceof AwsException && $ex->getAwsRequestId() !== null) { - $span->setAttribute('aws.requestId', $ex->getAwsRequestId()); + $span->setAttribute(TraceAttributes::AWS_REQUEST_ID, $ex->getAwsRequestId()); } $span->recordException($ex); $span->setStatus(StatusCode::STATUS_ERROR, $ex->getMessage()); From 45224ce124266c40e31ad25eed22e299f8e81a2c Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Tue, 13 May 2025 11:12:33 -0700 Subject: [PATCH 03/18] added to gitsplit --- .gitsplit.yml | 2 ++ src/Instrumentation/AwsSdk/README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitsplit.yml b/.gitsplit.yml index 072982784..330f1c222 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -60,6 +60,8 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-yii.git" - prefix: "src/Instrumentation/Doctrine" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-doctrine.git" + - prefix: "src/Instrumentation/AwsSdk" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-aws-sdk.git" - prefix: "src/Context/Swoole" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/context-swoole.git" - prefix: "src/AutoInstrumentationInstaller" diff --git a/src/Instrumentation/AwsSdk/README.md b/src/Instrumentation/AwsSdk/README.md index 78646ed18..8ab0a8a70 100644 --- a/src/Instrumentation/AwsSdk/README.md +++ b/src/Instrumentation/AwsSdk/README.md @@ -2,7 +2,7 @@ [![Issues](https://img.shields.io/badge/issues-pink)](https://github.com/open-telemetry/opentelemetry-php/issues) [![Source](https://img.shields.io/badge/source-contrib-green)](https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Instrumentation/AwsSdk) [![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/contrib-auto-aws-sdk) -[![Latest Version](http://poser.pugx.org/open-telemetry/opentelemetry-auto-guzzle/v/unstable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-aws-sdk/) +[![Latest Version](http://poser.pugx.org/open-telemetry/opentelemetry-auto-aws-sdk/v/unstable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-aws-sdk/) [![Stable](http://poser.pugx.org/open-telemetry/opentelemetry-auto-aws-sdk/v/stable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-aws-sdk/) This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib. From be7848eb62849050a7b5b2dc9109aacaa1903669 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Tue, 13 May 2025 11:14:42 -0700 Subject: [PATCH 04/18] added gitattributes file --- src/Instrumentation/AwsSdk/.gitattributes | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/Instrumentation/AwsSdk/.gitattributes diff --git a/src/Instrumentation/AwsSdk/.gitattributes b/src/Instrumentation/AwsSdk/.gitattributes new file mode 100644 index 000000000..1676cf825 --- /dev/null +++ b/src/Instrumentation/AwsSdk/.gitattributes @@ -0,0 +1,12 @@ +* text=auto + +*.md diff=markdown +*.php diff=php + +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-cs-fixer.php export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml.dist export-ignore +/tests export-ignore From 5f3a9ed65fe30c1b8386d0a2fcce8636075a64fa Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Wed, 14 May 2025 13:28:39 -0700 Subject: [PATCH 05/18] Updated to use the latest semcov 1.32 --- src/Instrumentation/AwsSdk/composer.json | 2 +- src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php | 7 +++---- .../AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php | 7 ++++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Instrumentation/AwsSdk/composer.json b/src/Instrumentation/AwsSdk/composer.json index ed2221efa..92bfaa96f 100644 --- a/src/Instrumentation/AwsSdk/composer.json +++ b/src/Instrumentation/AwsSdk/composer.json @@ -13,7 +13,7 @@ "aws/aws-sdk-php": "^3", "ext-opentelemetry": "*", "open-telemetry/api": "^1.0", - "open-telemetry/sem-conv": "^1.30" + "open-telemetry/sem-conv": "^1.32" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3", diff --git a/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php b/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php index 7a061337e..0a50b63d9 100644 --- a/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php +++ b/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php @@ -25,7 +25,7 @@ public static function register(): void $inst = new CachedInstrumentation( 'io.opentelemetry.contrib.php.aws-sdk', null, - 'https://opentelemetry.io/schemas/1.30.0', + 'https://opentelemetry.io/schemas/1.32.0', ); /** @@ -51,9 +51,8 @@ public static function register(): void ->setAttribute(TraceAttributes::RPC_METHOD, $cmd->getName()) ->setAttribute(TraceAttributes::RPC_SERVICE, $c->getApi()->getServiceName()) ->setAttribute(TraceAttributes::CLOUD_REGION, $c->getRegion()) - ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function) - ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) - ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, sprintf('%s::%s', $class, $function)) + ->setAttribute(TraceAttributes::CODE_FILE_PATH, $filename) ->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno); $span = $builder->startSpan(); diff --git a/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php b/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php index 13612df16..95354f51e 100644 --- a/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php +++ b/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php @@ -73,11 +73,12 @@ public function test_listBuckets_generates_one_aws_span_with_expected_attributes $this->assertSame('s3.ListBuckets', $span->getName()); $attrs = $span->getAttributes(); + $this->assertSame('Aws\AwsClient::execute', $attrs->get('code.function.name')); $this->assertSame('aws-api', $attrs->get('rpc.system')); $this->assertSame('s3', $attrs->get('rpc.service')); $this->assertSame('ListBuckets', $attrs->get('rpc.method')); - $this->assertSame('us-west-2', $attrs->get('aws.region')); - $this->assertSame(200, $attrs->get('http.status_code')); - $this->assertSame('TEST-REQUEST-ID', $attrs->get('aws.requestId')); + $this->assertSame('us-west-2', $attrs->get('cloud.region')); + $this->assertSame(200, $attrs->get('http.response.status_code')); + $this->assertSame('TEST-REQUEST-ID', $attrs->get('aws.request_id')); } } From 500afb63b9bc0b306189063d3922cda79d0b4f72 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Thu, 15 May 2025 15:21:31 -0700 Subject: [PATCH 06/18] Added aws tests to php.yml --- .github/workflows/php.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 601a3f5d0..99d19925a 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -22,6 +22,7 @@ jobs: project: [ 'Aws', 'Context/Swoole', + 'Instrumentation/AwsSdk', 'Instrumentation/CakePHP', 'Instrumentation/CodeIgniter', 'Instrumentation/Curl', From 9a940088f79f99c15569a39267c40a91a4564511 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Wed, 21 May 2025 11:32:24 -0700 Subject: [PATCH 07/18] Updated composer for php 8.1 and fixed style issue --- src/Instrumentation/AwsSdk/_register.php | 4 +++- src/Instrumentation/AwsSdk/composer.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Instrumentation/AwsSdk/_register.php b/src/Instrumentation/AwsSdk/_register.php index b701f8a0c..b0a0dec1d 100644 --- a/src/Instrumentation/AwsSdk/_register.php +++ b/src/Instrumentation/AwsSdk/_register.php @@ -1,4 +1,5 @@ Date: Thu, 22 May 2025 10:54:04 -0700 Subject: [PATCH 08/18] Fixed psalm errors --- src/Instrumentation/AwsSdk/.phan/config.php | 371 ++++++++++++++++++ .../AwsSdk/src/AwsSdkInstrumentation.php | 5 +- .../Integration/AwsSdkInstrumentationTest.php | 1 + 3 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 src/Instrumentation/AwsSdk/.phan/config.php diff --git a/src/Instrumentation/AwsSdk/.phan/config.php b/src/Instrumentation/AwsSdk/.phan/config.php new file mode 100644 index 000000000..da2ac2d99 --- /dev/null +++ b/src/Instrumentation/AwsSdk/.phan/config.php @@ -0,0 +1,371 @@ + '8.0', + + // If enabled, missing properties will be created when + // they are first seen. If false, we'll report an + // error message if there is an attempt to write + // to a class property that wasn't explicitly + // defined. + 'allow_missing_properties' => false, + + // If enabled, null can be cast to any type and any + // type can be cast to null. Setting this to true + // will cut down on false positives. + 'null_casts_as_any_type' => false, + + // If enabled, allow null to be cast as any array-like type. + // + // This is an incremental step in migrating away from `null_casts_as_any_type`. + // If `null_casts_as_any_type` is true, this has no effect. + 'null_casts_as_array' => true, + + // If enabled, allow any array-like type to be cast to null. + // This is an incremental step in migrating away from `null_casts_as_any_type`. + // If `null_casts_as_any_type` is true, this has no effect. + 'array_casts_as_null' => true, + + // If enabled, scalars (int, float, bool, string, null) + // are treated as if they can cast to each other. + // This does not affect checks of array keys. See `scalar_array_key_cast`. + 'scalar_implicit_cast' => false, + + // If enabled, any scalar array keys (int, string) + // are treated as if they can cast to each other. + // E.g. `array` can cast to `array` and vice versa. + // Normally, a scalar type such as int could only cast to/from int and mixed. + 'scalar_array_key_cast' => true, + + // If this has entries, scalars (int, float, bool, string, null) + // are allowed to perform the casts listed. + // + // E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]` + // allows casting null to a string, but not vice versa. + // (subset of `scalar_implicit_cast`) + 'scalar_implicit_partial' => [], + + // If enabled, Phan will warn if **any** type in a method invocation's object + // is definitely not an object, + // or if **any** type in an invoked expression is not a callable. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_method_checking' => false, + + // If enabled, Phan will warn if **any** type of the object expression for a property access + // does not contain that property. + 'strict_object_checking' => false, + + // If enabled, Phan will warn if **any** type in the argument's union type + // cannot be cast to a type in the parameter's expected union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_param_checking' => false, + + // If enabled, Phan will warn if **any** type in a property assignment's union type + // cannot be cast to a type in the property's declared union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_property_checking' => false, + + // If enabled, Phan will warn if **any** type in a returned value's union type + // cannot be cast to the declared return type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_return_checking' => false, + + // If true, seemingly undeclared variables in the global + // scope will be ignored. + // + // This is useful for projects with complicated cross-file + // globals that you have no hope of fixing. + 'ignore_undeclared_variables_in_global_scope' => true, + + // Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for, + // but aren't available in the codebase, or from Reflection. + // (may lead to false positives if an extension isn't loaded) + // + // If this is true(default), then Phan will not warn. + // + // Even when this is false, Phan will still infer return values and check parameters of internal functions + // if Phan has the signatures. + 'ignore_undeclared_functions_with_known_signatures' => true, + + // Backwards Compatibility Checking. This is slow + // and expensive, but you should consider running + // it before upgrading your version of PHP to a + // new version that has backward compatibility + // breaks. + // + // If you are migrating from PHP 5 to PHP 7, + // you should also look into using + // [php7cc (no longer maintained)](https://github.com/sstalle/php7cc) + // and [php7mar](https://github.com/Alexia/php7mar), + // which have different backwards compatibility checks. + 'backward_compatibility_checks' => false, + + // If true, check to make sure the return type declared + // in the doc-block (if any) matches the return type + // declared in the method signature. + 'check_docblock_signature_return_type_match' => false, + + // If true, make narrowed types from phpdoc params override + // the real types from the signature, when real types exist. + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // + // Affects analysis of the body of the method and the param types passed in by callers. + // + // (*Requires `check_docblock_signature_param_type_match` to be true*) + 'prefer_narrowed_phpdoc_param_type' => true, + + // (*Requires `check_docblock_signature_return_type_match` to be true*) + // + // If true, make narrowed types from phpdoc returns override + // the real types from the signature, when real types exist. + // + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // + // This setting affects the analysis of return statements in the body of the method and the return types passed in by callers. + 'prefer_narrowed_phpdoc_return_type' => true, + + // If enabled, check all methods that override a + // parent method to make sure its signature is + // compatible with the parent's. + // + // This check can add quite a bit of time to the analysis. + // + // This will also check if final methods are overridden, etc. + 'analyze_signature_compatibility' => true, + + // This setting maps case-insensitive strings to union types. + // + // This is useful if a project uses phpdoc that differs from the phpdoc2 standard. + // + // If the corresponding value is the empty string, + // then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`) + // + // If the corresponding value is not empty, + // then Phan will act as though it saw the corresponding UnionTypes(s) + // when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc. + // + // This matches the **entire string**, not parts of the string. + // (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting) + // + // (These are not aliases, this setting is ignored outside of doc comments). + // (Phan does not check if classes with these names exist) + // + // Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']` + 'phpdoc_type_mapping' => [], + + // Set to true in order to attempt to detect dead + // (unreferenced) code. Keep in mind that the + // results will only be a guess given that classes, + // properties, constants and methods can be referenced + // as variables (like `$class->$property` or + // `$class->$method()`) in ways that we're unable + // to make sense of. + 'dead_code_detection' => false, + + // Set to true in order to attempt to detect unused variables. + // `dead_code_detection` will also enable unused variable detection. + // + // This has a few known false positives, e.g. for loops or branches. + 'unused_variable_detection' => false, + + // Set to true in order to attempt to detect redundant and impossible conditions. + // + // This has some false positives involving loops, + // variables set in branches of loops, and global variables. + 'redundant_condition_detection' => false, + + // If enabled, Phan will act as though it's certain of real return types of a subset of internal functions, + // even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version). + // + // Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect. + // As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x. + 'assume_real_types_for_internal_functions' => false, + + // If true, this runs a quick version of checks that takes less + // time at the cost of not running as thorough + // of an analysis. You should consider setting this + // to true only when you wish you had more **undiagnosed** issues + // to fix in your code base. + // + // In quick-mode the scanner doesn't rescan a function + // or a method's code block every time a call is seen. + // This means that the problem here won't be detected: + // + // ```php + // false, + + // Enable or disable support for generic templated + // class types. + 'generic_types_enabled' => true, + + // Override to hardcode existence and types of (non-builtin) globals in the global scope. + // Class names should be prefixed with `\`. + // + // (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`) + 'globals_type_map' => [], + + // The minimum severity level to report on. This can be + // set to `Issue::SEVERITY_LOW`, `Issue::SEVERITY_NORMAL` or + // `Issue::SEVERITY_CRITICAL`. Setting it to only + // critical issues is a good place to start on a big + // sloppy mature code base. + 'minimum_severity' => Issue::SEVERITY_LOW, + + // Add any issue types (such as `'PhanUndeclaredMethod'`) + // to this deny-list to inhibit them from being reported. + 'suppress_issue_types' => [], + + // A regular expression to match files to be excluded + // from parsing and analysis and will not be read at all. + // + // This is useful for excluding groups of test or example + // directories/files, unanalyzable files, or files that + // can't be removed for whatever reason. + // (e.g. `'@Test\.php$@'`, or `'@vendor/.*/(tests|Tests)/@'`) + 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', + + // A list of files that will be excluded from parsing and analysis + // and will not be read at all. + // + // This is useful for excluding hopelessly unanalyzable + // files that can't be removed for whatever reason. + 'exclude_file_list' => [ + 'vendor/composer/composer/src/Composer/InstalledVersions.php' + ], + + // A directory list that defines files that will be excluded + // from static analysis, but whose class and method + // information should be included. + // + // Generally, you'll want to include the directories for + // third-party code (such as "vendor/") in this list. + // + // n.b.: If you'd like to parse but not analyze 3rd + // party code, directories containing that code + // should be added to the `directory_list` as well as + // to `exclude_analysis_directory_list`. + 'exclude_analysis_directory_list' => [ + 'vendor/', + 'proto/', + 'thrift/' + ], + + // Enable this to enable checks of require/include statements referring to valid paths. + 'enable_include_path_checks' => true, + + // The number of processes to fork off during the analysis + // phase. + 'processes' => 1, + + // List of case-insensitive file extensions supported by Phan. + // (e.g. `['php', 'html', 'htm']`) + 'analyzed_file_extensions' => [ + 'php', + ], + + // You can put paths to stubs of internal extensions in this config option. + // If the corresponding extension is **not** loaded, then Phan will use the stubs instead. + // Phan will continue using its detailed type annotations, + // but load the constants, classes, functions, and classes (and their Reflection types) + // from these stub files (doubling as valid php files). + // Use a different extension from php to avoid accidentally loading these. + // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now) + // + // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`) + 'autoload_internal_extension_signatures' => [], + + // A list of plugin files to execute. + // + // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`) + // + // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/master/.phan/plugins). + // + // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`) + 'plugins' => [ + 'AlwaysReturnPlugin', + 'PregRegexCheckerPlugin', + 'UnreachableCodePlugin', + ], + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in `exclude_analysis_directory_list`, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'src', + 'vendor' + ], + + // A list of individual files to include in analysis + // with a path relative to the root directory of the + // project. + 'file_list' => [], +]; diff --git a/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php b/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php index 0a50b63d9..3ceae8637 100644 --- a/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php +++ b/src/Instrumentation/AwsSdk/src/AwsSdkInstrumentation.php @@ -16,8 +16,10 @@ use function OpenTelemetry\Instrumentation\hook; use OpenTelemetry\SemConv\TraceAttributes; +/** @psalm-suppress UnusedClass */ final class AwsSdkInstrumentation { + /** @psalm-suppress ArgumentTypeCoercion */ public const NAME = 'aws-sdk'; public static function register(): void @@ -32,7 +34,8 @@ public static function register(): void * ② Intercept the low‑level `execute` call that actually * performs the HTTP request and has the Command object. */ - hook( + /** @psalm-suppress UnusedFunctionCall */ + hook( AwsClient::class, 'execute', pre: static function ( diff --git a/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php b/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php index 95354f51e..6ce033e57 100644 --- a/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php +++ b/src/Instrumentation/AwsSdk/tests/Integration/AwsSdkInstrumentationTest.php @@ -20,6 +20,7 @@ /** * @covers \OpenTelemetry\Contrib\Instrumentation\AwsSdk\AwsSdkInstrumentation */ +/** @psalm-suppress TooManyArguments */ class AwsSdkInstrumentationTest extends TestCase { private S3Client $client; From 942b7b16b365dd60c43d5c29953ba1c246f8491a Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Fri, 23 May 2025 01:32:31 -0700 Subject: [PATCH 09/18] initial sampler implementation --- src/Sampler/Xray/.gitattributes | 13 + src/Sampler/Xray/.gitignore | 6 + src/Sampler/Xray/.phan/config.php | 370 ++++++++++++++++++ src/Sampler/Xray/.php-cs-fixer.php | 43 ++ src/Sampler/Xray/README.md | 87 ++++ src/Sampler/Xray/composer.json | 37 ++ src/Sampler/Xray/phpstan.neon.dist | 14 + src/Sampler/Xray/phpunit.xml.dist | 22 ++ src/Sampler/Xray/psalm.xml.dist | 29 ++ src/Sampler/Xray/src/AWSXRayRemoteSampler.php | 88 +++++ src/Sampler/Xray/src/AWSXRaySamplerClient.php | 51 +++ src/Sampler/Xray/src/Clock.php | 16 + src/Sampler/Xray/src/FallbackSampler.php | 41 ++ src/Sampler/Xray/src/Matcher.php | 51 +++ src/Sampler/Xray/src/RateLimiter.php | 54 +++ src/Sampler/Xray/src/RateLimitingSampler.php | 47 +++ src/Sampler/Xray/src/RulesCache.php | 108 +++++ src/Sampler/Xray/src/SamplingRule.php | 58 +++ src/Sampler/Xray/src/SamplingRuleApplier.php | 217 ++++++++++ .../Xray/src/SamplingStatisticsDocument.php | 23 ++ src/Sampler/Xray/src/Statistics.php | 10 + 21 files changed, 1385 insertions(+) create mode 100644 src/Sampler/Xray/.gitattributes create mode 100644 src/Sampler/Xray/.gitignore create mode 100644 src/Sampler/Xray/.phan/config.php create mode 100644 src/Sampler/Xray/.php-cs-fixer.php create mode 100644 src/Sampler/Xray/README.md create mode 100644 src/Sampler/Xray/composer.json create mode 100644 src/Sampler/Xray/phpstan.neon.dist create mode 100644 src/Sampler/Xray/phpunit.xml.dist create mode 100644 src/Sampler/Xray/psalm.xml.dist create mode 100644 src/Sampler/Xray/src/AWSXRayRemoteSampler.php create mode 100644 src/Sampler/Xray/src/AWSXRaySamplerClient.php create mode 100644 src/Sampler/Xray/src/Clock.php create mode 100644 src/Sampler/Xray/src/FallbackSampler.php create mode 100644 src/Sampler/Xray/src/Matcher.php create mode 100644 src/Sampler/Xray/src/RateLimiter.php create mode 100644 src/Sampler/Xray/src/RateLimitingSampler.php create mode 100644 src/Sampler/Xray/src/RulesCache.php create mode 100644 src/Sampler/Xray/src/SamplingRule.php create mode 100644 src/Sampler/Xray/src/SamplingRuleApplier.php create mode 100644 src/Sampler/Xray/src/SamplingStatisticsDocument.php create mode 100644 src/Sampler/Xray/src/Statistics.php diff --git a/src/Sampler/Xray/.gitattributes b/src/Sampler/Xray/.gitattributes new file mode 100644 index 000000000..ac40e9f84 --- /dev/null +++ b/src/Sampler/Xray/.gitattributes @@ -0,0 +1,13 @@ +* text=auto + +*.md diff=markdown +*.php diff=php + +/.gitattributes export-ignore +/.gitignore export-ignore +/.phan export-ignore +/.php-cs-fixer.php export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml.dist export-ignore +/tests export-ignore diff --git a/src/Sampler/Xray/.gitignore b/src/Sampler/Xray/.gitignore new file mode 100644 index 000000000..b557f8918 --- /dev/null +++ b/src/Sampler/Xray/.gitignore @@ -0,0 +1,6 @@ +composer.lock + +.phpunit.cache + +var +vendor \ No newline at end of file diff --git a/src/Sampler/Xray/.phan/config.php b/src/Sampler/Xray/.phan/config.php new file mode 100644 index 000000000..d3c9f4911 --- /dev/null +++ b/src/Sampler/Xray/.phan/config.php @@ -0,0 +1,370 @@ + '8.1', + + // If enabled, missing properties will be created when + // they are first seen. If false, we'll report an + // error message if there is an attempt to write + // to a class property that wasn't explicitly + // defined. + 'allow_missing_properties' => false, + + // If enabled, null can be cast to any type and any + // type can be cast to null. Setting this to true + // will cut down on false positives. + 'null_casts_as_any_type' => false, + + // If enabled, allow null to be cast as any array-like type. + // + // This is an incremental step in migrating away from `null_casts_as_any_type`. + // If `null_casts_as_any_type` is true, this has no effect. + 'null_casts_as_array' => true, + + // If enabled, allow any array-like type to be cast to null. + // This is an incremental step in migrating away from `null_casts_as_any_type`. + // If `null_casts_as_any_type` is true, this has no effect. + 'array_casts_as_null' => true, + + // If enabled, scalars (int, float, bool, string, null) + // are treated as if they can cast to each other. + // This does not affect checks of array keys. See `scalar_array_key_cast`. + 'scalar_implicit_cast' => false, + + // If enabled, any scalar array keys (int, string) + // are treated as if they can cast to each other. + // E.g. `array` can cast to `array` and vice versa. + // Normally, a scalar type such as int could only cast to/from int and mixed. + 'scalar_array_key_cast' => true, + + // If this has entries, scalars (int, float, bool, string, null) + // are allowed to perform the casts listed. + // + // E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]` + // allows casting null to a string, but not vice versa. + // (subset of `scalar_implicit_cast`) + 'scalar_implicit_partial' => [], + + // If enabled, Phan will warn if **any** type in a method invocation's object + // is definitely not an object, + // or if **any** type in an invoked expression is not a callable. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_method_checking' => false, + + // If enabled, Phan will warn if **any** type of the object expression for a property access + // does not contain that property. + 'strict_object_checking' => false, + + // If enabled, Phan will warn if **any** type in the argument's union type + // cannot be cast to a type in the parameter's expected union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_param_checking' => false, + + // If enabled, Phan will warn if **any** type in a property assignment's union type + // cannot be cast to a type in the property's declared union type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_property_checking' => false, + + // If enabled, Phan will warn if **any** type in a returned value's union type + // cannot be cast to the declared return type. + // Setting this to true will introduce numerous false positives + // (and reveal some bugs). + 'strict_return_checking' => false, + + // If true, seemingly undeclared variables in the global + // scope will be ignored. + // + // This is useful for projects with complicated cross-file + // globals that you have no hope of fixing. + 'ignore_undeclared_variables_in_global_scope' => true, + + // Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for, + // but aren't available in the codebase, or from Reflection. + // (may lead to false positives if an extension isn't loaded) + // + // If this is true(default), then Phan will not warn. + // + // Even when this is false, Phan will still infer return values and check parameters of internal functions + // if Phan has the signatures. + 'ignore_undeclared_functions_with_known_signatures' => true, + + // Backwards Compatibility Checking. This is slow + // and expensive, but you should consider running + // it before upgrading your version of PHP to a + // new version that has backward compatibility + // breaks. + // + // If you are migrating from PHP 5 to PHP 7, + // you should also look into using + // [php7cc (no longer maintained)](https://github.com/sstalle/php7cc) + // and [php7mar](https://github.com/Alexia/php7mar), + // which have different backwards compatibility checks. + 'backward_compatibility_checks' => false, + + // If true, check to make sure the return type declared + // in the doc-block (if any) matches the return type + // declared in the method signature. + 'check_docblock_signature_return_type_match' => false, + + // If true, make narrowed types from phpdoc params override + // the real types from the signature, when real types exist. + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // + // Affects analysis of the body of the method and the param types passed in by callers. + // + // (*Requires `check_docblock_signature_param_type_match` to be true*) + 'prefer_narrowed_phpdoc_param_type' => true, + + // (*Requires `check_docblock_signature_return_type_match` to be true*) + // + // If true, make narrowed types from phpdoc returns override + // the real types from the signature, when real types exist. + // + // (E.g. allows specifying desired lists of subclasses, + // or to indicate a preference for non-nullable types over nullable types) + // + // This setting affects the analysis of return statements in the body of the method and the return types passed in by callers. + 'prefer_narrowed_phpdoc_return_type' => true, + + // If enabled, check all methods that override a + // parent method to make sure its signature is + // compatible with the parent's. + // + // This check can add quite a bit of time to the analysis. + // + // This will also check if final methods are overridden, etc. + 'analyze_signature_compatibility' => true, + + // This setting maps case-insensitive strings to union types. + // + // This is useful if a project uses phpdoc that differs from the phpdoc2 standard. + // + // If the corresponding value is the empty string, + // then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`) + // + // If the corresponding value is not empty, + // then Phan will act as though it saw the corresponding UnionTypes(s) + // when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc. + // + // This matches the **entire string**, not parts of the string. + // (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting) + // + // (These are not aliases, this setting is ignored outside of doc comments). + // (Phan does not check if classes with these names exist) + // + // Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']` + 'phpdoc_type_mapping' => [], + + // Set to true in order to attempt to detect dead + // (unreferenced) code. Keep in mind that the + // results will only be a guess given that classes, + // properties, constants and methods can be referenced + // as variables (like `$class->$property` or + // `$class->$method()`) in ways that we're unable + // to make sense of. + 'dead_code_detection' => false, + + // Set to true in order to attempt to detect unused variables. + // `dead_code_detection` will also enable unused variable detection. + // + // This has a few known false positives, e.g. for loops or branches. + 'unused_variable_detection' => false, + + // Set to true in order to attempt to detect redundant and impossible conditions. + // + // This has some false positives involving loops, + // variables set in branches of loops, and global variables. + 'redundant_condition_detection' => false, + + // If enabled, Phan will act as though it's certain of real return types of a subset of internal functions, + // even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version). + // + // Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect. + // As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x. + 'assume_real_types_for_internal_functions' => false, + + // If true, this runs a quick version of checks that takes less + // time at the cost of not running as thorough + // of an analysis. You should consider setting this + // to true only when you wish you had more **undiagnosed** issues + // to fix in your code base. + // + // In quick-mode the scanner doesn't rescan a function + // or a method's code block every time a call is seen. + // This means that the problem here won't be detected: + // + // ```php + // false, + + // Enable or disable support for generic templated + // class types. + 'generic_types_enabled' => true, + + // Override to hardcode existence and types of (non-builtin) globals in the global scope. + // Class names should be prefixed with `\`. + // + // (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`) + 'globals_type_map' => [], + + // The minimum severity level to report on. This can be + // set to `Issue::SEVERITY_LOW`, `Issue::SEVERITY_NORMAL` or + // `Issue::SEVERITY_CRITICAL`. Setting it to only + // critical issues is a good place to start on a big + // sloppy mature code base. + 'minimum_severity' => Issue::SEVERITY_LOW, + + // Add any issue types (such as `'PhanUndeclaredMethod'`) + // to this deny-list to inhibit them from being reported. + 'suppress_issue_types' => [], + + // A regular expression to match files to be excluded + // from parsing and analysis and will not be read at all. + // + // This is useful for excluding groups of test or example + // directories/files, unanalyzable files, or files that + // can't be removed for whatever reason. + // (e.g. `'@Test\.php$@'`, or `'@vendor/.*/(tests|Tests)/@'`) + 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', + + // A list of files that will be excluded from parsing and analysis + // and will not be read at all. + // + // This is useful for excluding hopelessly unanalyzable + // files that can't be removed for whatever reason. + 'exclude_file_list' => [ + 'vendor/composer/composer/src/Composer/InstalledVersions.php' + ], + + // A directory list that defines files that will be excluded + // from static analysis, but whose class and method + // information should be included. + // + // Generally, you'll want to include the directories for + // third-party code (such as "vendor/") in this list. + // + // n.b.: If you'd like to parse but not analyze 3rd + // party code, directories containing that code + // should be added to the `directory_list` as well as + // to `exclude_analysis_directory_list`. + 'exclude_analysis_directory_list' => [ + 'vendor/', + 'src/ComponentProvider/', + ], + + // Enable this to enable checks of require/include statements referring to valid paths. + 'enable_include_path_checks' => true, + + // The number of processes to fork off during the analysis + // phase. + 'processes' => 1, + + // List of case-insensitive file extensions supported by Phan. + // (e.g. `['php', 'html', 'htm']`) + 'analyzed_file_extensions' => [ + 'php', + ], + + // You can put paths to stubs of internal extensions in this config option. + // If the corresponding extension is **not** loaded, then Phan will use the stubs instead. + // Phan will continue using its detailed type annotations, + // but load the constants, classes, functions, and classes (and their Reflection types) + // from these stub files (doubling as valid php files). + // Use a different extension from php to avoid accidentally loading these. + // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now) + // + // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`) + 'autoload_internal_extension_signatures' => [], + + // A list of plugin files to execute. + // + // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`) + // + // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/master/.phan/plugins). + // + // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`) + 'plugins' => [ + 'AlwaysReturnPlugin', + 'PregRegexCheckerPlugin', + 'UnreachableCodePlugin', + ], + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in `exclude_analysis_directory_list`, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'src', + 'vendor' + ], + + // A list of individual files to include in analysis + // with a path relative to the root directory of the + // project. + 'file_list' => [], +]; diff --git a/src/Sampler/Xray/.php-cs-fixer.php b/src/Sampler/Xray/.php-cs-fixer.php new file mode 100644 index 000000000..e35fa078c --- /dev/null +++ b/src/Sampler/Xray/.php-cs-fixer.php @@ -0,0 +1,43 @@ +exclude('vendor') + ->exclude('var/cache') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +return $config->setRules([ + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'none'], + 'is_null' => true, + 'modernize_types_casting' => true, + 'ordered_imports' => true, + 'php_unit_construct' => true, + 'single_line_comment_style' => true, + 'yoda_style' => false, + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => true, + 'cast_spaces' => true, + 'declare_strict_types' => true, + 'type_declaration_spaces' => true, + 'include' => true, + 'lowercase_cast' => true, + 'new_with_parentheses' => true, + 'no_extra_blank_lines' => true, + 'no_leading_import_slash' => true, + 'echo_tag_syntax' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'phpdoc_order' => true, + 'phpdoc_scalar' => true, + 'phpdoc_types' => true, + 'short_scalar_cast' => true, + 'blank_lines_before_namespace' => true, + 'single_quote' => true, + 'trailing_comma_in_multiline' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder($finder); + diff --git a/src/Sampler/Xray/README.md b/src/Sampler/Xray/README.md new file mode 100644 index 000000000..ccde5d7bc --- /dev/null +++ b/src/Sampler/Xray/README.md @@ -0,0 +1,87 @@ +# Contrib Sampler + +Provides additional samplers that are not part of the official specification. + +## Installation + +```shell +composer require open-telemetry/sampler-rule-based +``` + +## RuleBasedSampler + +Allows sampling based on a list of rule sets. The first matching rule set will decide the sampling result. + +```php +$sampler = new RuleBasedSampler( + [ + new RuleSet( + [ + new SpanKindRule(Kind::Server), + new AttributeRule('url.path', '~^/health$~'), + ], + new AlwaysOffSampler(), + ), + ], + new AlwaysOnSampler(), +); +``` + +### Configuration + +###### Example: drop spans for the /health endpoint + +```yaml +contrib_rule_based: + rule_sets: + - rules: + - span_kind: { kind: SERVER } + - attribute: { key: url.path, pattern: ~^/health$~ } + delegate: + always_off: {} + fallback: # ... +``` + +###### Example: sample spans with at least one sampled link + +```yaml +contrib_rule_based: + rule_sets: + - rules: [ link: { sampled: true } ] + delegate: + always_on: {} + fallback: # ... +``` + +###### Example: modeling parent based sampler as rule based sampler + +```yaml +rule_based: + rule_sets: + - rules: [ parent: { sampled: true, remote: true } ] + delegate: # remote_parent_sampled + - rules: [ parent: { sampled: false, remote: true } ] + delegate: # remote_parent_not_sampled + - rules: [ parent: { sampled: true, remote: false } ] + delegate: # local_parent_sampled + - rules: [ parent: { sampled: false, remote: false } ] + delegate: # local_parent_not_sampled + fallback: # root +``` + +## AlwaysRecordingSampler + +Records all spans to allow the usage of span processors that generate metrics from spans. + +```php +$sampler = new AlwaysRecordingSampler( + new ParentBasedSampler(new AlwaysOnSampler()), +); +``` + +### Configuration + +```yaml +always_recording: + sampler: # ... +``` diff --git a/src/Sampler/Xray/composer.json b/src/Sampler/Xray/composer.json new file mode 100644 index 000000000..80b339daa --- /dev/null +++ b/src/Sampler/Xray/composer.json @@ -0,0 +1,37 @@ +{ + "name": "open-telemetry/contrib-aws-xray-sampler", + "description": "AWS X-Ray Remote Sampler for OpenTelemetry PHP Contrib", + "type": "library", + "license": "Apache-2.0", + "require": { + "php": "^8.1", + "aws/aws-sdk-php": "^3.0", + "react/event-loop": "^1.2", + "open-telemetry/sdk": "^1.1.0", + "open-telemetry/sampler-rule-based": "dev-main" + }, + "require-dev": { + "symfony/config": "^5.4 || ^6.4 || ^7.0", + "symfony/yaml": "^6 || ^7", + "friendsofphp/php-cs-fixer": "^3", + "phan/phan": "^5.0", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "psalm/plugin-phpunit": "^0.19.2", + "phpunit/phpunit": "^10 || ^11", + "vimeo/psalm": "^4|^5|6.4.0" + }, + "autoload": { + "psr-4": { + "OpenTelemetry\\Contrib\\Sampler\\Xray\\": "src/" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "tbachert/spi": true + } + } +} diff --git a/src/Sampler/Xray/phpstan.neon.dist b/src/Sampler/Xray/phpstan.neon.dist new file mode 100644 index 000000000..f9cb5fac4 --- /dev/null +++ b/src/Sampler/Xray/phpstan.neon.dist @@ -0,0 +1,14 @@ +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon + +parameters: + tmpDir: var/cache/phpstan + level: 5 + paths: + - src + - tests + ignoreErrors: + - + message: "#Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface::.*#" + paths: + - src/ \ No newline at end of file diff --git a/src/Sampler/Xray/phpunit.xml.dist b/src/Sampler/Xray/phpunit.xml.dist new file mode 100644 index 000000000..aebce8c3c --- /dev/null +++ b/src/Sampler/Xray/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + + + + + + + tests/Unit + + + tests/Integration + + + + + src + + + diff --git a/src/Sampler/Xray/psalm.xml.dist b/src/Sampler/Xray/psalm.xml.dist new file mode 100644 index 000000000..c9650afeb --- /dev/null +++ b/src/Sampler/Xray/psalm.xml.dist @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sampler/Xray/src/AWSXRayRemoteSampler.php b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php new file mode 100644 index 000000000..33fca7344 --- /dev/null +++ b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php @@ -0,0 +1,88 @@ +resource = $resource; + $this->pollInterval= $pollIntervalSec; + $this->clock = new Clock(); + $this->client = new AWSXRaySamplerClient($awsConfig); + $this->fallback = new FallbackSampler($this->clock); + $this->rulesCache = new RulesCache($this->clock, bin2hex(random_bytes(12)), $resource, $this->fallback); + + // initial rule load + $this->rulesCache->updateRules($this->client->getSamplingRules()); + } + + /** + * Call this once, passing in your ReactPHP loop, before starting your app. + */ + public function start(LoopInterface $loop): void + { + // poll rules + $loop->addPeriodicTimer($this->pollInterval, function () { + $this->rulesCache->updateRules($this->client->getSamplingRules()); + }); + + // poll targets + $loop->addPeriodicTimer(self::DEFAULT_TARGET_INTERVAL, function () { + $statsDocs = array_map( + fn(SamplingStatisticsDocument $d) => (array)$d, + array_map([$this->rulesCache, 'snapshot'], [$this->clock->now()]) + ); + $resp = $this->client->getSamplingTargets($statsDocs); + if (isset($resp['SamplingTargetDocuments'])) { + $map = []; + foreach ($resp['SamplingTargetDocuments'] as $tgt) { + $map[$tgt['RuleName']] = (object)$tgt; + } + $this->rulesCache->updateTargets($map); + } + }); + } + + public function shouldSample( + ContextInterface $parentContext, + string $traceId, + string $spanName, + int $spanKind, + AttributesInterface $attributes, + array $links, + ): SamplingResult + { + // if cache expired, fallback + if ($this->rulesCache->expired()) { + return $this->fallback->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + } + // delegate + return $this->rulesCache->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + } + + public function getDescription(): string + { + return 'AWSXRayRemoteSampler{pollInterval='.$this->pollInterval.'s}'; + } +} diff --git a/src/Sampler/Xray/src/AWSXRaySamplerClient.php b/src/Sampler/Xray/src/AWSXRaySamplerClient.php new file mode 100644 index 000000000..862ffab47 --- /dev/null +++ b/src/Sampler/Xray/src/AWSXRaySamplerClient.php @@ -0,0 +1,51 @@ +client = new XRayClient($config); + } + + /** @return SamplingRule[] */ + public function getSamplingRules(): array + { + $out = []; + $p = $this->client->getPaginator('GetSamplingRules'); + foreach ($p as $page) { + foreach ($page['SamplingRuleRecords'] as $rec) { + $r = $rec['SamplingRule']; + $out[] = new SamplingRule( + $r['RuleName'], + $r['Priority'], + $r['FixedRate'], + $r['ReservoirSize'], + $r['Host'] ?? '*', + $r['HTTPMethod'] ?? '*', + $r['ResourceARN'] ?? '*', + $r['ServiceName'] ?? '*', + $r['ServiceType'] ?? '*', + $r['URLPath'] ?? '*', + $r['Version'] ?? 1, + $r['Attributes'] ?? [] + ); + } + } + return $out; + } + + /** @return object|null – raw SamplingTargets response */ + public function getSamplingTargets(array $statistics): ?array + { + $resp = $this->client->getSamplingTargets([ + 'SamplingStatisticsDocuments' => $statistics, + ]); + return $resp->toArray(); + } +} diff --git a/src/Sampler/Xray/src/Clock.php b/src/Sampler/Xray/src/Clock.php new file mode 100644 index 000000000..fad3dfda7 --- /dev/null +++ b/src/Sampler/Xray/src/Clock.php @@ -0,0 +1,16 @@ +getTimestamp() * 1000) + ($dt->format('v') / 1); + } +} diff --git a/src/Sampler/Xray/src/FallbackSampler.php b/src/Sampler/Xray/src/FallbackSampler.php new file mode 100644 index 000000000..745930f3a --- /dev/null +++ b/src/Sampler/Xray/src/FallbackSampler.php @@ -0,0 +1,41 @@ +reservoir = new RateLimitingSampler(1, $clock); // 1/sec + $this->fixedRate = new TraceIdRatioBasedSampler(0.05); // 5% + } + + public function shouldSample( + ContextInterface $parentContext, + string $traceId, + string $spanName, + int $spanKind, + AttributesInterface $attributes, + array $links, + ): SamplingResult { + $res = $this->reservoir->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + if ($res->getDecision() !== SamplingResult::DROP) { + return $res; + } + return $this->fixedRate->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + } + + public function getDescription(): string + { + return 'AWSXRayFallbackSampler'; + } +} diff --git a/src/Sampler/Xray/src/Matcher.php b/src/Sampler/Xray/src/Matcher.php new file mode 100644 index 000000000..9524dbc82 --- /dev/null +++ b/src/Sampler/Xray/src/Matcher.php @@ -0,0 +1,51 @@ + 'AWS::ECS::Container', + 'aws_lambda'=> 'AWS::Lambda::Function', + ]; + + /** + * Check that every rule attribute key/value is present and equal in the span tags. + */ + public static function attributeMatch(?array $tags, array $attributes): bool + { + foreach ($attributes as $key => $value) { + if ($tags === null || !array_key_exists($key, $tags)) { + return false; + } + if ((string) $tags[$key] !== (string) $value) { + return false; + } + } + return true; + } + + /** + * Wildcard match (‘*’ → any chars). Null value only matches if pattern is '*' or empty. + */ + public static function wildcardMatch(?string $value, string $pattern): bool + { + if ($pattern === '' || $pattern === '*') { + return true; + } + if ($value === null) { + return false; + } + // escape regex, then replace \* with .* + $regex = '/^' . str_replace('\*', '.*', preg_quote($pattern, '/')) . '$/'; + return (bool) preg_match($regex, $value); + } + + /** + * Map OpenTelemetry cloud.platform values to X-Ray service type strings. + */ + public static function getXRayCloudPlatform(?string $platform): string + { + return self::$xrayCloudPlatform[$platform] ?? ''; + } +} diff --git a/src/Sampler/Xray/src/RateLimiter.php b/src/Sampler/Xray/src/RateLimiter.php new file mode 100644 index 000000000..48908c118 --- /dev/null +++ b/src/Sampler/Xray/src/RateLimiter.php @@ -0,0 +1,54 @@ +capacity = $capacity; + $this->tokens = $capacity; + $this->intervalMillis = $intervalMillis; + $this->lastRefillTime = microtime(true) * 1000; + } + + /** + * Attempt to take one token. Returns true if successful, false if rate‐limited. + */ + public function tryAcquire(): bool + { + $now = microtime(true) * 1000; + $elapsed = $now - $this->lastRefillTime; + + if ($elapsed >= $this->intervalMillis) { + // Time to refill the bucket + $this->tokens = $this->capacity; + $this->lastRefillTime = $now; + } + + if ($this->tokens > 0) { + $this->tokens--; + return true; + } + + return false; + } + + public function getCapacity(): int + { + return $this->capacity; + } +} diff --git a/src/Sampler/Xray/src/RateLimitingSampler.php b/src/Sampler/Xray/src/RateLimitingSampler.php new file mode 100644 index 000000000..0f52255b3 --- /dev/null +++ b/src/Sampler/Xray/src/RateLimitingSampler.php @@ -0,0 +1,47 @@ +limiter = new RateLimiter($maxTracesPerSecond); + } + + public function shouldSample( + ContextInterface $parentContext, + string $traceId, + string $spanName, + int $spanKind, + AttributesInterface $attributes, + array $links, + ): SamplingResult + { + if ($this->limiter->tryAcquire()) { + return new SamplingResult(SamplingResult::RECORD_AND_SAMPLE, [], null); + } + + return new SamplingResult(SamplingResult::DROP, [], null); + } + + public function getDescription(): string + { + return sprintf('RateLimitingSampler{%d/s}', $this->limiter->getCapacity()); + } +} diff --git a/src/Sampler/Xray/src/RulesCache.php b/src/Sampler/Xray/src/RulesCache.php new file mode 100644 index 000000000..93504d839 --- /dev/null +++ b/src/Sampler/Xray/src/RulesCache.php @@ -0,0 +1,108 @@ +clock = $clock; + $this->clientId = $clientId; + $this->resource = $resource; + $this->fallbackSampler = $fallback; + $this->updatedAt = $clock->now(); + } + + public function expired(): bool + { + return $this->clock->now()->getTimestamp() > $this->updatedAt->getTimestamp() + self::CACHE_TTL; + } + + public function updateRules(array $newRules): void + { + usort($newRules, fn(SamplingRule $a, SamplingRule $b) => $a->compareTo($b)); + $newAppliers = []; + foreach ($newRules as $rule) { + // reuse existing applier if same ruleName + $found = null; + foreach ($this->appliers as $ap) { + if ($ap->getRuleName() === $rule->RuleName) { + $found = $ap; + break; + } + } + $applier = $found ?? new SamplingRuleApplier($this->clientId, $this->clock, $rule); + // update rule in applier + // $applier->setRule($rule); + $newAppliers[] = $applier; + } + $this->appliers = $newAppliers; + $this->updatedAt = $this->clock->now(); + } + + public function shouldSample( + ContextInterface $parentContext, + string $traceId, + string $spanName, + int $spanKind, + AttributesInterface $attributes, + array $links, + ): SamplingResult + { + foreach ($this->appliers as $applier) { + if ($applier->matches($attributes, $this->resource)) { + return $applier->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + } + } + // fallback if no rule matched + return $this->fallbackSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + } + + public function nextTargetFetchTime(): \DateTimeImmutable + { + if (empty($this->appliers)) { + return $this->clock->now()->add(new \DateInterval('PT10S')); + } + $times = array_map(fn($a) => $a->getNextSnapshotTime(), $this->appliers); + $min = min($times); + return $min < $this->clock->now() + ? $this->clock->now()->add(new \DateInterval('PT10S')) + : $min; + } + + /** Update reservoir/fixed rates from GetSamplingTargets response */ + public function updateTargets(array $targets): void + { + $new = []; + foreach ($this->appliers as $applier) { + $name = $applier->getRuleName(); + if (isset($targets[$name])) { + $new[] = $applier->withTarget($targets[$name], $this->clock->now()); + } else { + $new[] = $applier; + } + } + $this->appliers = $new; + } + + public function getDescription(): string + { + return 'RulesCache'; + } +} diff --git a/src/Sampler/Xray/src/SamplingRule.php b/src/Sampler/Xray/src/SamplingRule.php new file mode 100644 index 000000000..f0bafa168 --- /dev/null +++ b/src/Sampler/Xray/src/SamplingRule.php @@ -0,0 +1,58 @@ +RuleName = $ruleName; + $this->Priority = $priority; + $this->FixedRate = $fixedRate; + $this->ReservoirSize= $reservoirSize; + $this->Host = $host; + $this->HttpMethod = $httpMethod; + $this->ResourceArn = $resourceArn; + $this->ServiceName = $serviceName; + $this->ServiceType = $serviceType; + $this->UrlPath = $urlPath; + $this->Version = $version; + $this->Attributes = $attributes; + } + + public function jsonSerialize(): array + { + return get_object_vars($this); + } + + public function compareTo($other): int + { + $cmp = $this->Priority <=> $other->Priority; + return $cmp !== 0 ? $cmp : strcmp($this->RuleName, $other->RuleName); + } +} diff --git a/src/Sampler/Xray/src/SamplingRuleApplier.php b/src/Sampler/Xray/src/SamplingRuleApplier.php new file mode 100644 index 000000000..3665c3f3f --- /dev/null +++ b/src/Sampler/Xray/src/SamplingRuleApplier.php @@ -0,0 +1,217 @@ +clientId = $clientId; + $this->clock = $clock; + $this->rule = $rule; + $this->ruleName = $rule->RuleName; + $this->statistics = $stats ?? new Statistics(); + + if ($rule->ReservoirSize > 0) { + $this->reservoirSampler = new RateLimitingSampler($rule->ReservoirSize, $clock); + $this->borrowing = true; + } else { + $this->reservoirSampler = new AlwaysOffSampler(); + $this->borrowing = false; + } + + $this->fixedRateSampler = new TraceIdRatioBasedSampler($rule->FixedRate); + $this->reservoirEndTime = new \DateTimeImmutable('@'.PHP_INT_MAX); + $this->nextSnapshotTime = $clock->now(); + } + + /** + * Private full constructor: accept *all* fields. + * Used by withTarget() to clone with new samplers & timings. + */ + private function __constructFull( + string $clientId, + Clock $clock, + SamplingRule $rule, + SamplerInterface $reservoirSampler, + SamplerInterface $fixedRateSampler, + bool $borrowing, + Statistics $statistics, + \DateTimeImmutable $reservoirEndTime, + \DateTimeImmutable $nextSnapshotTime + ) { + $this->clientId = $clientId; + $this->clock = $clock; + $this->rule = $rule; + $this->reservoirSampler= $reservoirSampler; + $this->fixedRateSampler= $fixedRateSampler; + $this->borrowing = $borrowing; + $this->statistics = $statistics; + $this->reservoirEndTime= $reservoirEndTime; + $this->nextSnapshotTime= $nextSnapshotTime; + } + + + public function matches(AttributesInterface $attributes, ResourceInfo $resource): bool + { + // Extract HTTP path + $httpTarget = $attributes->get('http.target') + ?? ( + null !== $attributes->get('http.url') + ? (preg_match('~^[^:]+://[^/]+(/.*)?$~', $attributes->get('http.url'), $m) + ? ($m[1] ?? '/') + : null + ) + : null + ); + + $httpMethod = $attributes->get('http.method') ?? null; + $httpHost = $attributes->get('http.host') ?? null; + $serviceName= $resource->getAttributes()->get('service.name') ?? ''; + $cloudPlat = $resource->getAttributes()->get('cloud.platform') ?? null; + $serviceType= Matcher::getXRayCloudPlatform($cloudPlat); + + // ARN: ECS container ARN or Lambda faas.id + $arn = $resource->getAttributes()->get('aws.ecs.container.arn') + ?? ($serviceType === 'AWS::Lambda::Function' ? ($attributes->get('faas.id') ?? null) : null) + ?? ''; + + return Matcher::attributeMatch($attributes->toArray(), $this->rule->Attributes) + && Matcher::wildcardMatch($httpTarget, $this->rule->UrlPath) + && Matcher::wildcardMatch($httpMethod, $this->rule->HttpMethod) + && Matcher::wildcardMatch($httpHost, $this->rule->Host) + && Matcher::wildcardMatch($serviceName, $this->rule->ServiceName) + && Matcher::wildcardMatch($serviceType, $this->rule->ServiceType) + && Matcher::wildcardMatch($arn, $this->rule->ResourceArn); + } + + + public function shouldSample( + ContextInterface $parentContext, + string $traceId, + string $spanName, + int $spanKind, + AttributesInterface $attributes, + array $links, + ): SamplingResult + { + $this->statistics->requestCount++; + $now = $this->clock->now(); + if ($now < $this->reservoirEndTime) { + $res = $this->reservoirSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links);; + if ($res->getDecision() !== SamplingResult::DROP) { + if ($this->borrowing) { + $this->statistics->borrowCount++; + } + $this->statistics->sampleCount++; + return $res; + } + } + $res = $this->fixedRateSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links);; + if ($res->getDecision() !== SamplingResult::DROP) { + $this->statistics->sampleCount++; + } + return $res; + } + + public function snapshot(\DateTimeImmutable $now): SamplingStatisticsDocument + { + $ts = $this->clock->toUnixMillis($now); + $req = $this->statistics->requestCount; + $smp = $this->statistics->sampleCount; + $brw = $this->statistics->borrowCount; + // reset + $this->statistics->requestCount = 0; + $this->statistics->sampleCount = 0; + $this->statistics->borrowCount = 0; + + return new SamplingStatisticsDocument( + $this->clientId, + $this->ruleName, + $req, + $smp, + $brw, + $ts + ); + } + + /** + * Apply an AWS X-Ray SamplingTargets response to this rule applier, + * returning a new applier with updated reservoir & fixed-rate samplers. + * + * @param object $targetDoc stdClass from AWS SDK getSamplingTargets() + * @param \DateTimeImmutable $now “now” timestamp for computing next snapshot + */ + public function withTarget(object $targetDoc, \DateTimeImmutable $now): self + { + // 1) Determine new fixed-rate sampler + if (isset($targetDoc->FixedRate)) { + $newFixedRateSampler = new TraceIdRatioBasedSampler((float) $targetDoc->FixedRate); + } else { + $newFixedRateSampler = $this->fixedRateSampler; + } + + // 2) Determine new reservoir sampler & end time + $newReservoirEndTime = new \DateTimeImmutable('9999-12-31T23:59:59+00:00'); + if (isset($targetDoc->ReservoirQuota, $targetDoc->ReservoirQuotaTTL)) { + $quota = (int) floor($targetDoc->ReservoirQuota); + $ttlSeconds = (int) floor($targetDoc->ReservoirQuotaTTL); + $newReservoirSampler = $quota > 0 + ? new RateLimitingSampler($quota) + : new AlwaysOffSampler(); + $newReservoirEndTime = \DateTimeImmutable::createFromFormat('U', (string)$ttlSeconds) + ?: $newReservoirEndTime; + } else { + // if no quota provided, turn off reservoir + $newReservoirSampler = new AlwaysOffSampler(); + } + + // 3) Next snapshot time (Interval in seconds, defaulting to 10s) + $intervalSec = isset($targetDoc->Interval) + ? (int) $targetDoc->Interval + : 10; + $newNextSnapshotTime = $now->add(new \DateInterval("PT{$intervalSec}S")); + + // 4) Clone & patch + $clone = clone $this; + $clone->fixedRateSampler = $newFixedRateSampler; + $clone->reservoirSampler = $newReservoirSampler; + $clone->borrowing = false; // once we’ve applied a target, no more borrowing + $clone->reservoirEndTime = $newReservoirEndTime; + $clone->nextSnapshotTime = $newNextSnapshotTime; + + return $clone; + } + + + public function getNextSnapshotTime(): \DateTimeImmutable + { + return $this->nextSnapshotTime; + } + + public function getRuleName(): string + { + return $this->ruleName; + } +} diff --git a/src/Sampler/Xray/src/SamplingStatisticsDocument.php b/src/Sampler/Xray/src/SamplingStatisticsDocument.php new file mode 100644 index 000000000..dc0815dea --- /dev/null +++ b/src/Sampler/Xray/src/SamplingStatisticsDocument.php @@ -0,0 +1,23 @@ +ClientId = $clientId; + $this->RuleName = $ruleName; + $this->RequestCount = $req; + $this->SampleCount = $samp; + $this->BorrowCount = $borrow; + $this->Timestamp = $ts; + } +} diff --git a/src/Sampler/Xray/src/Statistics.php b/src/Sampler/Xray/src/Statistics.php new file mode 100644 index 000000000..9300cf168 --- /dev/null +++ b/src/Sampler/Xray/src/Statistics.php @@ -0,0 +1,10 @@ + Date: Mon, 7 Jul 2025 09:39:39 -0700 Subject: [PATCH 10/18] Updated sampler code --- src/Sampler/Xray/composer.json | 7 +- src/Sampler/Xray/src/AWSXRayRemoteSampler.php | 211 ++++++++++++++---- src/Sampler/Xray/src/AWSXRaySamplerClient.php | 91 ++++++-- src/Sampler/Xray/src/FallbackSampler.php | 8 +- src/Sampler/Xray/src/Matcher.php | 7 +- src/Sampler/Xray/src/RateLimiter.php | 3 + src/Sampler/Xray/src/RateLimitingSampler.php | 2 +- src/Sampler/Xray/src/RulesCache.php | 22 +- src/Sampler/Xray/src/SamplingRule.php | 2 +- src/Sampler/Xray/src/SamplingRuleApplier.php | 63 +++--- 10 files changed, 291 insertions(+), 125 deletions(-) diff --git a/src/Sampler/Xray/composer.json b/src/Sampler/Xray/composer.json index 80b339daa..aab05fc21 100644 --- a/src/Sampler/Xray/composer.json +++ b/src/Sampler/Xray/composer.json @@ -6,9 +6,10 @@ "require": { "php": "^8.1", "aws/aws-sdk-php": "^3.0", - "react/event-loop": "^1.2", - "open-telemetry/sdk": "^1.1.0", - "open-telemetry/sampler-rule-based": "dev-main" + "open-telemetry/api": "^1.1.0", + "open-telemetry/sdk": "^1.1.0", + "open-telemetry/sdk-configuration": "^0.0.5", + "open-telemetry/sem-conv": "^1.32" }, "require-dev": { "symfony/config": "^5.4 || ^6.4 || ^7.0", diff --git a/src/Sampler/Xray/src/AWSXRayRemoteSampler.php b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php index 33fca7344..6010d855b 100644 --- a/src/Sampler/Xray/src/AWSXRayRemoteSampler.php +++ b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php @@ -1,69 +1,120 @@ root = new ParentBased(new _AWSXRayRemoteSampler($resource, $host, $rulePollingIntervalMillis)); + } + + public function shouldSample( + ContextInterface $parentContext, + string $traceId, + string $spanName, + int $spanKind, + AttributesInterface $attributes, + array $links, + ): SamplingResult + { + return $this->root->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + } + + public function getDescription(): string + { + return sprintf( + 'AWSXRayRemoteSampler{root=%s}', + $this->root->getDescription() + ); + } +} + +class _AWSXRayRemoteSampler implements SamplerInterface { - private const DEFAULT_TARGET_INTERVAL = 10; - - private ResourceInfo $resource; - private int $pollInterval; + // 5 minute default sampling rules polling interval + private const DEFAULT_RULES_POLLING_INTERVAL_SECONDS = 5 * 60; + // Default endpoint for awsproxy : https://aws-otel.github.io/docs/getting-started/remote-sampling#enable-awsproxy-extension + private const DEFAULT_AWS_PROXY_ENDPOINT = 'http://localhost:2000'; + + private Clock $clock; private RulesCache $rulesCache; - private AWSXRaySamplerClient $client; private FallbackSampler $fallback; - private Clock $clock; - + private AWSXRaySamplerClient $client; + + private int $rulePollingIntervalMillis; + private int $targetPollingIntervalMillis; + private DateTimeImmutable $nextRulesFetchTime; + private DateTimeImmutable $nextTargetFetchTime; + + private int $rulePollingJitterMillis; + private int $targetPollingJitterMillis; + + private string $awsProxyEndpoint; + + /** + * @param ResourceInfo $resource + * Must contain attributes like service.name, cloud.platform, etc. + * @param string $awsProxyEndpoint + * X-Ray awsProxyEndpoint, e.g. "xray.us-west-2.amazonaws.com" + * @param int $pollingInterval + * Base interval (seconds) between rule fetches (will be jittered). + */ public function __construct( ResourceInfo $resource, - array $awsConfig, - int $pollIntervalSec = 60 + string $awsProxyEndpoint = self::DEFAULT_AWS_PROXY_ENDPOINT, + int $pollingInterval = self::DEFAULT_RULES_POLLING_INTERVAL_SECONDS ) { - $this->resource = $resource; - $this->pollInterval= $pollIntervalSec; - $this->clock = new Clock(); - $this->client = new AWSXRaySamplerClient($awsConfig); - $this->fallback = new FallbackSampler($this->clock); - $this->rulesCache = new RulesCache($this->clock, bin2hex(random_bytes(12)), $resource, $this->fallback); - - // initial rule load - $this->rulesCache->updateRules($this->client->getSamplingRules()); + $this->clock = new Clock(); + $this->fallback = new FallbackSampler(); + $this->rulesCache = new RulesCache( + $this->clock, + bin2hex(random_bytes(12)), + $resource, + $this->fallback + ); + + $this->rulePollingIntervalMillis = $pollingInterval * 1000; + $this->rulePollingJitterMillis = rand(1, 5000); + + $this->targetPollingIntervalMillis = $this->rulesCache::DEFAULT_TARGET_INTERVAL_SEC * 1000; + $this->targetPollingJitterMillis = rand(1, 100); + + $this->awsProxyEndpoint = $awsProxyEndpoint; + + $this->client = new AWSXRaySamplerClient($awsProxyEndpoint); + + // 1) Initial fetch of rules + try { + $initialRules = $this->client->getSamplingRules(); + $this->rulesCache->updateRules($initialRules); + } catch (Exception $e) { + // ignore failures + } + + // 2) Schedule next fetch times with jitter + $now = $this->clock->now(); + $this->nextRulesFetchTime = $now->modify('+ '.$this->rulePollingJitterMillis + $this->rulePollingIntervalMillis.' milliseconds'); + $this->nextTargetFetchTime = $now->modify('+ '.$this->targetPollingJitterMillis + $this->targetPollingIntervalMillis.' milliseconds'); } - + /** - * Call this once, passing in your ReactPHP loop, before starting your app. + * Called on each sampling decision. If it’s time, refresh rules or targets. */ - public function start(LoopInterface $loop): void - { - // poll rules - $loop->addPeriodicTimer($this->pollInterval, function () { - $this->rulesCache->updateRules($this->client->getSamplingRules()); - }); - - // poll targets - $loop->addPeriodicTimer(self::DEFAULT_TARGET_INTERVAL, function () { - $statsDocs = array_map( - fn(SamplingStatisticsDocument $d) => (array)$d, - array_map([$this->rulesCache, 'snapshot'], [$this->clock->now()]) - ); - $resp = $this->client->getSamplingTargets($statsDocs); - if (isset($resp['SamplingTargetDocuments'])) { - $map = []; - foreach ($resp['SamplingTargetDocuments'] as $tgt) { - $map[$tgt['RuleName']] = (object)$tgt; - } - $this->rulesCache->updateTargets($map); - } - }); - } - public function shouldSample( ContextInterface $parentContext, string $traceId, @@ -73,6 +124,53 @@ public function shouldSample( array $links, ): SamplingResult { + $now = $this->clock->now(); + + // 1) Refresh rules if needed + if ($now >= $this->nextRulesFetchTime) { + $this->getAndUpdateRules($now); + } + + // 2) Refresh targets if needed + if ($now >= $this->nextTargetFetchTime) { + $appliers = $this->rulesCache->getAppliers(); + $statsDocs = []; + foreach ($appliers as $applier) { + $statsDocs[] = $applier->snapshot($now); + } + + try { + $resp = $this->client->getSamplingTargets($statsDocs); + if ($resp !== null && isset($resp->SamplingTargetDocuments)) { + $map = []; + foreach ($resp->SamplingTargetDocuments as $tgt) { + $map[$tgt->RuleName] = $tgt; + } + $this->rulesCache->updateTargets($map); + + if (isset($resp->LastRuleModification) && $resp->LastRuleModification > 0) { + if ($resp->LastRuleModification > $this->rulesCache->getUpdatedAt()->getTimestamp()) { + $this->getAndUpdateRules($now); + } + } + } + } catch (Exception $e) { + //ignore for now + } + + $nextTargetFetchTime = $this->rulesCache->nextTargetFetchTime(); + $nextTargetFetchInterval = $nextTargetFetchTime->getTimestamp() - $this->clock->now()->getTimestamp(); + if ($nextTargetFetchInterval < 0) { + $nextTargetFetchInterval = $this->rulesCache::DEFAULT_TARGET_INTERVAL_SEC; + } + + $nextTargetFetchInterval = $nextTargetFetchInterval * 1000; + + $this->nextTargetFetchTime = $now->modify('+ '.$this->targetPollingJitterMillis + $nextTargetFetchInterval.' milliseconds'); + + } + + // 3) Delegate decision to rulesCache or fallback // if cache expired, fallback if ($this->rulesCache->expired()) { return $this->fallback->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); @@ -80,9 +178,24 @@ public function shouldSample( // delegate return $this->rulesCache->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); } - + + private function getAndUpdateRules(DateTimeImmutable $now) + { + try { + $rules = $this->client->getSamplingRules(); + $this->rulesCache->updateRules($rules); + } catch (Exception $e) { + // ignore error + } + $this->nextRulesFetchTime = $now->modify('+ '.$this->rulePollingJitterMillis + $this->rulePollingIntervalMillis.' milliseconds'); + } + public function getDescription(): string { - return 'AWSXRayRemoteSampler{pollInterval='.$this->pollInterval.'s}'; + return sprintf( + '_AWSXRayRemoteSampler{awsProxyEndpoint=%s,rulePollingIntervalMillis=%ds}', + $this->awsProxyEndpoint, + $this->rulePollingIntervalMillis + ); } } diff --git a/src/Sampler/Xray/src/AWSXRaySamplerClient.php b/src/Sampler/Xray/src/AWSXRaySamplerClient.php index 862ffab47..8a7936a06 100644 --- a/src/Sampler/Xray/src/AWSXRaySamplerClient.php +++ b/src/Sampler/Xray/src/AWSXRaySamplerClient.php @@ -1,51 +1,94 @@ client = new XRayClient($config); + // Ensure no scheme is prepended; HttpClient will use HTTPS by default. + $this->host = rtrim($host, '/'); + $this->httpClient = new HttpClient([ + 'base_uri' => $this->host, + 'timeout' => 2.0, + ]); } - - /** @return SamplingRule[] */ + + /** + * Fetches all sampling rules from X-Ray by paging through NextToken. + * + * @return SamplingRule[] Array of SamplingRule instances. + * @throws GuzzleException on HTTP errors. + */ public function getSamplingRules(): array { - $out = []; - $p = $this->client->getPaginator('GetSamplingRules'); - foreach ($p as $page) { - foreach ($page['SamplingRuleRecords'] as $rec) { + $rules = []; + $response = $this->httpClient->post('GetSamplingRules'); + $data = json_decode((string) $response->getBody(), true); + + if (isset($data['SamplingRuleRecords'])) { + foreach ($data['SamplingRuleRecords'] as $rec) { $r = $rec['SamplingRule']; - $out[] = new SamplingRule( + $rules[] = new SamplingRule( $r['RuleName'], $r['Priority'], $r['FixedRate'], $r['ReservoirSize'], - $r['Host'] ?? '*', - $r['HTTPMethod'] ?? '*', + $r['Host'] ?? '*', + $r['HTTPMethod'] ?? '*', $r['ResourceARN'] ?? '*', $r['ServiceName'] ?? '*', $r['ServiceType'] ?? '*', - $r['URLPath'] ?? '*', - $r['Version'] ?? 1, - $r['Attributes'] ?? [] + $r['URLPath'] ?? '*', + $r['Version'] ?? 1, + $r['Attributes'] ?? [] ); } } - return $out; + + return $rules; } - - /** @return object|null – raw SamplingTargets response */ - public function getSamplingTargets(array $statistics): ?array + + /** + * Sends current statistics documents to X-Ray and returns the decoded response. + * + * @param SamplingStatisticsDocument[] $statistics + * @return object|null stdClass of the X-Ray GetSamplingTargets response. + * @throws GuzzleException on HTTP errors. + */ + public function getSamplingTargets(array $statistics): ?object { - $resp = $this->client->getSamplingTargets([ - 'SamplingStatisticsDocuments' => $statistics, + $docs = []; + foreach ($statistics as $d) { + $docs[] = [ + 'ClientID' => $d->ClientId, + 'RuleName' => $d->RuleName, + 'RequestCount' => $d->RequestCount, + 'SampleCount' => $d->SampleCount, + 'BorrowCount' => $d->BorrowCount, + 'Timestamp' => $d->Timestamp, + ]; + } + + $response = $this->httpClient->post('SamplingTargets', [ + 'json' => ['SamplingStatisticsDocuments' => $docs], ]); - return $resp->toArray(); + + return json_decode((string) $response->getBody()); } } diff --git a/src/Sampler/Xray/src/FallbackSampler.php b/src/Sampler/Xray/src/FallbackSampler.php index 745930f3a..9a5d6f711 100644 --- a/src/Sampler/Xray/src/FallbackSampler.php +++ b/src/Sampler/Xray/src/FallbackSampler.php @@ -8,14 +8,14 @@ use OpenTelemetry\SDK\Trace\SamplerInterface; use OpenTelemetry\SDK\Trace\SamplingResult; -class FallbackSampler extends SamplerInterface +class FallbackSampler implements SamplerInterface { private SamplerInterface $reservoir; private SamplerInterface $fixedRate; - public function __construct(Clock $clock) + public function __construct() { - $this->reservoir = new RateLimitingSampler(1, $clock); // 1/sec + $this->reservoir = new RateLimitingSampler(1); // 1/sec $this->fixedRate = new TraceIdRatioBasedSampler(0.05); // 5% } @@ -36,6 +36,6 @@ public function shouldSample( public function getDescription(): string { - return 'AWSXRayFallbackSampler'; + return 'FallbackSampler{fallback sampling with sampling config of 1 req/sec and 5% of additional requests'; } } diff --git a/src/Sampler/Xray/src/Matcher.php b/src/Sampler/Xray/src/Matcher.php index 9524dbc82..1ee255ec8 100644 --- a/src/Sampler/Xray/src/Matcher.php +++ b/src/Sampler/Xray/src/Matcher.php @@ -5,8 +5,11 @@ class Matcher { private static array $xrayCloudPlatform = [ - 'aws_ecs' => 'AWS::ECS::Container', - 'aws_lambda'=> 'AWS::Lambda::Function', + 'aws_ec2' => 'AWS::EC2::Instance', + 'aws_ecs' => 'AWS::ECS::Container', + 'aws_eks' => 'AWS::EKS::Container', + 'aws_elastic_beanstalk' => 'AWS::ElasticBeanstalk::Environment', + 'aws_lambda' => 'AWS::Lambda::Function', ]; /** diff --git a/src/Sampler/Xray/src/RateLimiter.php b/src/Sampler/Xray/src/RateLimiter.php index 48908c118..9b3727461 100644 --- a/src/Sampler/Xray/src/RateLimiter.php +++ b/src/Sampler/Xray/src/RateLimiter.php @@ -1,4 +1,6 @@ tokens > 0) { $this->tokens--; + return true; } diff --git a/src/Sampler/Xray/src/RateLimitingSampler.php b/src/Sampler/Xray/src/RateLimitingSampler.php index 0f52255b3..37b3babd9 100644 --- a/src/Sampler/Xray/src/RateLimitingSampler.php +++ b/src/Sampler/Xray/src/RateLimitingSampler.php @@ -42,6 +42,6 @@ public function shouldSample( public function getDescription(): string { - return sprintf('RateLimitingSampler{%d/s}', $this->limiter->getCapacity()); + return sprintf('RateLimitingSampler{rate limiting sampling with sampling config of %d req/sec and 0% of additional requests}', $this->limiter->getCapacity()); } } diff --git a/src/Sampler/Xray/src/RulesCache.php b/src/Sampler/Xray/src/RulesCache.php index 93504d839..d7bd7acfb 100644 --- a/src/Sampler/Xray/src/RulesCache.php +++ b/src/Sampler/Xray/src/RulesCache.php @@ -5,13 +5,13 @@ use OpenTelemetry\Context\ContextInterface; use OpenTelemetry\SDK\Common\Attribute\AttributesInterface; use OpenTelemetry\SDK\Trace\SamplerInterface; -use OpenTelemetry\SDK\Trace\SamplingDecision; use OpenTelemetry\SDK\Trace\SamplingResult; use OpenTelemetry\SDK\Resource\ResourceInfo; class RulesCache implements SamplerInterface { private const CACHE_TTL = 3600; // 1hr + public const DEFAULT_TARGET_INTERVAL_SEC = 10; private Clock $clock; private string $clientId; private ResourceInfo $resource; @@ -48,8 +48,9 @@ public function updateRules(array $newRules): void } } $applier = $found ?? new SamplingRuleApplier($this->clientId, $this->clock, $rule); + // update rule in applier - // $applier->setRule($rule); + $applier->setRule($rule); $newAppliers[] = $applier; } $this->appliers = $newAppliers; @@ -76,13 +77,15 @@ public function shouldSample( public function nextTargetFetchTime(): \DateTimeImmutable { + $defaultPollingTime = $this->clock->now()->add(new \DateInterval('PT'.self::DEFAULT_TARGET_INTERVAL_SEC.'S')); + if (empty($this->appliers)) { - return $this->clock->now()->add(new \DateInterval('PT10S')); + return $defaultPollingTime; } $times = array_map(fn($a) => $a->getNextSnapshotTime(), $this->appliers); $min = min($times); return $min < $this->clock->now() - ? $this->clock->now()->add(new \DateInterval('PT10S')) + ? $defaultPollingTime : $min; } @@ -100,9 +103,20 @@ public function updateTargets(array $targets): void } $this->appliers = $new; } + + public function getAppliers(): array + { + return $this->appliers; + } + public function getDescription(): string { return 'RulesCache'; } + + public function getUpdatedAt(): \DateTimeImmutable + { + return $this->updatedAt; + } } diff --git a/src/Sampler/Xray/src/SamplingRule.php b/src/Sampler/Xray/src/SamplingRule.php index f0bafa168..650e41a79 100644 --- a/src/Sampler/Xray/src/SamplingRule.php +++ b/src/Sampler/Xray/src/SamplingRule.php @@ -2,7 +2,7 @@ // src/Sampler/AWS/SamplingRule.php namespace OpenTelemetry\Contrib\Sampler\Xray; -class SamplingRule implements \JsonSerializable, \Comparable +class SamplingRule implements \JsonSerializable { public string $RuleName; public int $Priority; diff --git a/src/Sampler/Xray/src/SamplingRuleApplier.php b/src/Sampler/Xray/src/SamplingRuleApplier.php index 3665c3f3f..f8c384857 100644 --- a/src/Sampler/Xray/src/SamplingRuleApplier.php +++ b/src/Sampler/Xray/src/SamplingRuleApplier.php @@ -5,12 +5,11 @@ use OpenTelemetry\Context\ContextInterface; use OpenTelemetry\SDK\Common\Attribute\AttributesInterface; use OpenTelemetry\SDK\Trace\SamplerInterface; -use OpenTelemetry\SDK\Trace\SamplingParameters; use OpenTelemetry\SDK\Trace\SamplingResult; -use OpenTelemetry\SDK\Trace\SamplingDecision; use OpenTelemetry\SDK\Resource\ResourceInfo; use OpenTelemetry\SDK\Trace\Sampler\AlwaysOffSampler; use OpenTelemetry\SDK\Trace\Sampler\TraceIdRatioBasedSampler; +use OpenTelemetry\SemConv\TraceAttributes; class SamplingRuleApplier { @@ -34,7 +33,7 @@ public function __construct(string $clientId, Clock $clock, SamplingRule $rule, $this->statistics = $stats ?? new Statistics(); if ($rule->ReservoirSize > 0) { - $this->reservoirSampler = new RateLimitingSampler($rule->ReservoirSize, $clock); + $this->reservoirSampler = new RateLimitingSampler($rule->ReservoirSize); $this->borrowing = true; } else { $this->reservoirSampler = new AlwaysOffSampler(); @@ -45,51 +44,37 @@ public function __construct(string $clientId, Clock $clock, SamplingRule $rule, $this->reservoirEndTime = new \DateTimeImmutable('@'.PHP_INT_MAX); $this->nextSnapshotTime = $clock->now(); } - - /** - * Private full constructor: accept *all* fields. - * Used by withTarget() to clone with new samplers & timings. - */ - private function __constructFull( - string $clientId, - Clock $clock, - SamplingRule $rule, - SamplerInterface $reservoirSampler, - SamplerInterface $fixedRateSampler, - bool $borrowing, - Statistics $statistics, - \DateTimeImmutable $reservoirEndTime, - \DateTimeImmutable $nextSnapshotTime - ) { - $this->clientId = $clientId; - $this->clock = $clock; - $this->rule = $rule; - $this->reservoirSampler= $reservoirSampler; - $this->fixedRateSampler= $fixedRateSampler; - $this->borrowing = $borrowing; - $this->statistics = $statistics; - $this->reservoirEndTime= $reservoirEndTime; - $this->nextSnapshotTime= $nextSnapshotTime; - } - public function matches(AttributesInterface $attributes, ResourceInfo $resource): bool { // Extract HTTP path - $httpTarget = $attributes->get('http.target') + $httpTarget = $attributes->get(TraceAttributes::HTTP_TARGET) ?? ( - null !== $attributes->get('http.url') - ? (preg_match('~^[^:]+://[^/]+(/.*)?$~', $attributes->get('http.url'), $m) + null !== $attributes->get(TraceAttributes::HTTP_URL) + ? (preg_match('~^[^:]+://[^/]+(/.*)?$~', $attributes->get(TraceAttributes::HTTP_URL), $m) ? ($m[1] ?? '/') : null ) : null ); - $httpMethod = $attributes->get('http.method') ?? null; - $httpHost = $attributes->get('http.host') ?? null; - $serviceName= $resource->getAttributes()->get('service.name') ?? ''; - $cloudPlat = $resource->getAttributes()->get('cloud.platform') ?? null; + if (empty($httpTarget)) { + // Extract HTTP path + $httpTarget = $attributes->get(TraceAttributes::URL_PATH) + ?? ( + null !== $attributes->get(TraceAttributes::URL_FULL) + ? (preg_match('~^[^:]+://[^/]+(/.*)?$~', $attributes->get(TraceAttributes::URL_FULL), $m) + ? ($m[1] ?? '/') + : null + ) + : null + ); + } + + $httpMethod = $attributes->get(TraceAttributes::HTTP_METHOD) ?? $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD); + $httpHost = $attributes->get(TraceAttributes::HTTP_HOST) ?? null; + $serviceName= $resource->getAttributes()->get(TraceAttributes::SERVICE_NAME) ?? ''; + $cloudPlat = $resource->getAttributes()->get(TraceAttributes::CLOUD_PLATFORM) ?? null; $serviceType= Matcher::getXRayCloudPlatform($cloudPlat); // ARN: ECS container ARN or Lambda faas.id @@ -214,4 +199,8 @@ public function getRuleName(): string { return $this->ruleName; } + + public function setRule($rule) { + $this->rule = $rule; + } } From 8035f6f1607727bf256d200f4a3806bfb6744a34 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Wed, 9 Jul 2025 16:46:55 -0700 Subject: [PATCH 11/18] added unit tests --- src/Sampler/Xray/composer.json | 3 +- src/Sampler/Xray/src/AWSXRaySamplerClient.php | 2 +- src/Sampler/Xray/src/SamplingRuleApplier.php | 30 +- .../Xray/src/SamplingStatisticsDocument.php | 4 +- .../test/Unit/AWSXRayRemoteSamplerTest.php | 121 +++++++ .../test/Unit/AWSXRaySamplerClientTest.php | 176 +++++++++++ .../test/Unit/AWSXraySamplerClientTest.php | 103 ++++++ .../Xray/test/Unit/RateLimiterTest.php | 43 +++ src/Sampler/Xray/test/Unit/RulesCacheTest.php | 118 +++++++ .../test/Unit/SamplingRuleApplierTest.php | 294 ++++++++++++++++++ .../Xray/test/Unit/data/sampling_rules.json | 43 +++ .../Xray/test/Unit/data/sampling_targets.json | 20 ++ 12 files changed, 931 insertions(+), 26 deletions(-) create mode 100644 src/Sampler/Xray/test/Unit/AWSXRayRemoteSamplerTest.php create mode 100644 src/Sampler/Xray/test/Unit/AWSXRaySamplerClientTest.php create mode 100644 src/Sampler/Xray/test/Unit/AWSXraySamplerClientTest.php create mode 100644 src/Sampler/Xray/test/Unit/RateLimiterTest.php create mode 100644 src/Sampler/Xray/test/Unit/RulesCacheTest.php create mode 100644 src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php create mode 100644 src/Sampler/Xray/test/Unit/data/sampling_rules.json create mode 100644 src/Sampler/Xray/test/Unit/data/sampling_targets.json diff --git a/src/Sampler/Xray/composer.json b/src/Sampler/Xray/composer.json index aab05fc21..f93780f73 100644 --- a/src/Sampler/Xray/composer.json +++ b/src/Sampler/Xray/composer.json @@ -25,7 +25,8 @@ "autoload": { "psr-4": { "OpenTelemetry\\Contrib\\Sampler\\Xray\\": "src/" - } + }, + "classmap": ["src/AWSXRayRemoteSampler.php"] }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/src/Sampler/Xray/src/AWSXRaySamplerClient.php b/src/Sampler/Xray/src/AWSXRaySamplerClient.php index 8a7936a06..10ab62090 100644 --- a/src/Sampler/Xray/src/AWSXRaySamplerClient.php +++ b/src/Sampler/Xray/src/AWSXRaySamplerClient.php @@ -76,7 +76,7 @@ public function getSamplingTargets(array $statistics): ?object $docs = []; foreach ($statistics as $d) { $docs[] = [ - 'ClientID' => $d->ClientId, + 'ClientID' => $d->ClientID, 'RuleName' => $d->RuleName, 'RequestCount' => $d->RequestCount, 'SampleCount' => $d->SampleCount, diff --git a/src/Sampler/Xray/src/SamplingRuleApplier.php b/src/Sampler/Xray/src/SamplingRuleApplier.php index f8c384857..7bcb98691 100644 --- a/src/Sampler/Xray/src/SamplingRuleApplier.php +++ b/src/Sampler/Xray/src/SamplingRuleApplier.php @@ -48,31 +48,17 @@ public function __construct(string $clientId, Clock $clock, SamplingRule $rule, public function matches(AttributesInterface $attributes, ResourceInfo $resource): bool { // Extract HTTP path - $httpTarget = $attributes->get(TraceAttributes::HTTP_TARGET) - ?? ( - null !== $attributes->get(TraceAttributes::HTTP_URL) - ? (preg_match('~^[^:]+://[^/]+(/.*)?$~', $attributes->get(TraceAttributes::HTTP_URL), $m) - ? ($m[1] ?? '/') - : null - ) - : null - ); - - if (empty($httpTarget)) { - // Extract HTTP path - $httpTarget = $attributes->get(TraceAttributes::URL_PATH) - ?? ( - null !== $attributes->get(TraceAttributes::URL_FULL) - ? (preg_match('~^[^:]+://[^/]+(/.*)?$~', $attributes->get(TraceAttributes::URL_FULL), $m) - ? ($m[1] ?? '/') - : null - ) - : null - ); + $httpTarget = $attributes->get(TraceAttributes::HTTP_TARGET) ?? $attributes->get(TraceAttributes::URL_PATH); + $httpUrl = $attributes->get(TraceAttributes::HTTP_URL) ?? $attributes->get(TraceAttributes::URL_FULL); + if ($httpTarget == null && isset($httpUrl)) { + $httpTarget = parse_url($httpUrl, PHP_URL_PATH); } $httpMethod = $attributes->get(TraceAttributes::HTTP_METHOD) ?? $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD); - $httpHost = $attributes->get(TraceAttributes::HTTP_HOST) ?? null; + if ($httpMethod == "_OTHER") { + $httpMethod = $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD_ORIGINAL); + } + $httpHost = $attributes->get(TraceAttributes::HTTP_HOST) ?? $attributes->get(TraceAttributes::SERVER_ADDRESS) ; $serviceName= $resource->getAttributes()->get(TraceAttributes::SERVICE_NAME) ?? ''; $cloudPlat = $resource->getAttributes()->get(TraceAttributes::CLOUD_PLATFORM) ?? null; $serviceType= Matcher::getXRayCloudPlatform($cloudPlat); diff --git a/src/Sampler/Xray/src/SamplingStatisticsDocument.php b/src/Sampler/Xray/src/SamplingStatisticsDocument.php index dc0815dea..db03bb709 100644 --- a/src/Sampler/Xray/src/SamplingStatisticsDocument.php +++ b/src/Sampler/Xray/src/SamplingStatisticsDocument.php @@ -4,7 +4,7 @@ class SamplingStatisticsDocument { - public string $ClientId; + public string $ClientID; public string $RuleName; public int $RequestCount; public int $SampleCount; @@ -13,7 +13,7 @@ class SamplingStatisticsDocument public function __construct(string $clientId, string $ruleName, int $req, int $samp, int $borrow, float $ts) { - $this->ClientId = $clientId; + $this->ClientID = $clientId; $this->RuleName = $ruleName; $this->RequestCount = $req; $this->SampleCount = $samp; diff --git a/src/Sampler/Xray/test/Unit/AWSXRayRemoteSamplerTest.php b/src/Sampler/Xray/test/Unit/AWSXRayRemoteSamplerTest.php new file mode 100644 index 000000000..4a401cb54 --- /dev/null +++ b/src/Sampler/Xray/test/Unit/AWSXRayRemoteSamplerTest.php @@ -0,0 +1,121 @@ +createMock(AWSXRaySamplerClient::class); + $dummyRules = ['rule1', 'rule2']; + $mockClient->expects($this->once()) + ->method('getSamplingRules') + ->willReturn($dummyRules); + $mockClient->expects($this->once()) + ->method('getSamplingTargets') + ->willReturn((object)[ + 'SamplingTargetDocuments' => [], + 'Interval' => 5, + 'LastRuleModification' => 0, + ]); + + // 2) Mock RulesCache + $mockRulesCache = $this->getMockBuilder(RulesCache::class) + ->disableOriginalConstructor() + ->getMock(); + $mockRulesCache->expects($this->once()) + ->method('updateRules') + ->with($dummyRules); + $mockRulesCache->expects($this->once()) + ->method('getAppliers') + ->willReturn([]); + $mockRulesCache->expects($this->once()) + ->method('updateTargets') + ->with([]); + $mockRulesCache->expects($this->once()) + ->method('expired') + ->willReturn(false); + $expectedResult = new SamplingResult(SamplingResult::RECORD_AND_SAMPLE); + $mockRulesCache->expects($this->once()) + ->method('shouldSample') + ->willReturn($expectedResult); + + // 3) Instantiate and inject mocks + $sampler = new _AWSXRayRemoteSampler($resource, 'host', 0); + $ref = new ReflectionClass($sampler); + $ref->getProperty('client')->setAccessible(true); + $ref->getProperty('client')->setValue($sampler, $mockClient); + $ref->getProperty('rulesCache')->setAccessible(true); + $ref->getProperty('rulesCache')->setValue($sampler, $mockRulesCache); + $ref->getProperty('fallback')->setAccessible(true); + $ref->getProperty('fallback')->setValue($sampler, $this->createMock(FallbackSampler::class)); + + // 4) Force fetch times into the past so updates run + $now = (new Clock())->now(); + $ref->getProperty('nextRulesFetchTime')->setAccessible(true); + $ref->getProperty('nextRulesFetchTime')->setValue($sampler, $now->sub(new DateInterval('PT1S'))); + $ref->getProperty('nextTargetFetchTime')->setAccessible(true); + $ref->getProperty('nextTargetFetchTime')->setValue($sampler, $now->sub(new DateInterval('PT1S'))); + + // 5) Call shouldSample + $result = $sampler->shouldSample( + $this->createMock(ContextInterface::class), + 'traceId', 'spanName', 1, + Attributes::create([]), + [] + ); + $this->assertSame($expectedResult, $result); + } + + public function testShouldSampleFallbackWhenExpired(): void + { + $resource = ResourceInfo::create(Attributes::create([])); + $mockClient = $this->createMock(AWSXRaySamplerClient::class); + + $mockRulesCache = $this->getMockBuilder(RulesCache::class) + ->disableOriginalConstructor() + ->getMock(); + // No updateRules call since expired + $mockRulesCache->expects($this->never()) + ->method('updateRules'); + $mockRulesCache->expects($this->once()) + ->method('expired') + ->willReturn(true); + + $fallback = $this->createMock(FallbackSampler::class); + $expected = new SamplingResult(SamplingResult::DROP); + $fallback->expects($this->once()) + ->method('shouldSample') + ->willReturn($expected); + + $sampler = new _AWSXRayRemoteSampler($resource, 'host', 0); + $ref = new ReflectionClass($sampler); + $ref->getProperty('client')->setAccessible(true); + $ref->getProperty('client')->setValue($sampler, $mockClient); + $ref->getProperty('rulesCache')->setAccessible(true); + $ref->getProperty('rulesCache')->setValue($sampler, $mockRulesCache); + $ref->getProperty('fallback')->setAccessible(true); + $ref->getProperty('fallback')->setValue($sampler, $fallback); + + $result = $sampler->shouldSample( + $this->createMock(ContextInterface::class), + 't', 'n', 1, + Attributes::create([]), + [] + ); + $this->assertSame($expected, $result); + } +} diff --git a/src/Sampler/Xray/test/Unit/AWSXRaySamplerClientTest.php b/src/Sampler/Xray/test/Unit/AWSXRaySamplerClientTest.php new file mode 100644 index 000000000..d4f4f48c5 --- /dev/null +++ b/src/Sampler/Xray/test/Unit/AWSXRaySamplerClientTest.php @@ -0,0 +1,176 @@ +createMock(HttpClient::class); + $response = new Response(200, ['Content-Type' => 'application/json'], $json); + $mockHttp->expects($this->once()) + ->method('post') + ->with('GetSamplingRules') + ->willReturn($response); + + // Instantiate and inject the mock client + $client = new AWSXRaySamplerClient('https://dummy'); + $ref = new ReflectionClass($client); + $prop = $ref->getProperty('httpClient'); + $prop->setAccessible(true); + $prop->setValue($client, $mockHttp); + + // Exercise getSamplingRules() + $rules = $client->getSamplingRules(); + + // Assertions + $this->assertCount(2, $rules); + $this->assertInstanceOf(SamplingRule::class, $rules[0]); + $this->assertEquals('Default', $rules[0]->RuleName); + $this->assertEquals(10000, $rules[0]->Priority); + $this->assertEquals(0.05, $rules[0]->FixedRate); + $this->assertEquals(100, $rules[0]->ReservoirSize); + $this->assertEquals(['foo' => 'bar', 'abc' => '1234'], $rules[0]->Attributes); + + $this->assertEquals('test', $rules[1]->RuleName); + $this->assertEquals(20, $rules[1]->Priority); + $this->assertEquals(0.11, $rules[1]->FixedRate); + $this->assertEquals(1, $rules[1]->ReservoirSize); + $this->assertEquals(['abc' => '1234'], $rules[1]->Attributes); + } + + public function testGetSamplingTargetsParsesResponse(): void + { + $json = <<<'JSON' +{ + "LastRuleModification": 1707551387.0, + "SamplingTargetDocuments": [ + { + "FixedRate": 0.10, + "Interval": 10, + "ReservoirQuota": 30, + "ReservoirQuotaTTL": 1707764006.0, + "RuleName": "test" + }, + { + "FixedRate": 0.05, + "Interval": 10, + "ReservoirQuota": 0, + "ReservoirQuotaTTL": 1707764006.0, + "RuleName": "Default" + } + ], + "UnprocessedStatistics": [] +} +JSON; + + $stats = [ + (object)[ + 'ClientID' => 'cid', + 'RuleName' => 'test', + 'RequestCount' => 1, + 'SampleCount' => 1, + 'BorrowCount' => 0, + 'Timestamp' => 1234567890.0, + ] + ]; + + // Mock the Guzzle HTTP client + $mockHttp = $this->createMock(HttpClient::class); + $response = new Response(200, ['Content-Type' => 'application/json'], $json); + $mockHttp->expects($this->once()) + ->method('post') + ->with( + 'SamplingTargets', + $this->callback(fn($opts) => + isset($opts['json']['SamplingStatisticsDocuments']) && + count($opts['json']['SamplingStatisticsDocuments']) === 1 + ) + ) + ->willReturn($response); + + // Instantiate and inject the mock client + $client = new AWSXRaySamplerClient('https://dummy'); + $ref = new ReflectionClass($client); + $prop = $ref->getProperty('httpClient'); + $prop->setAccessible(true); + $prop->setValue($client, $mockHttp); + + // Exercise getSamplingTargets() + $result = $client->getSamplingTargets($stats); + + // Assertions on the stdClass response + $this->assertIsObject($result); + $this->assertEquals(1707551387.0, $result->LastRuleModification); + + $docs = $result->SamplingTargetDocuments; + $this->assertCount(2, $docs); + + $this->assertEquals('test', $docs[0]->RuleName); + $this->assertEquals(0.10, $docs[0]->FixedRate); + $this->assertEquals(30, $docs[0]->ReservoirQuota); + $this->assertEquals(1707764006.0, $docs[0]->ReservoirQuotaTTL); + $this->assertEquals(10, $docs[0]->Interval); + + $this->assertEquals('Default',$docs[1]->RuleName); + $this->assertEquals(0.05, $docs[1]->FixedRate); + $this->assertEquals(0, $docs[1]->ReservoirQuota); + } +} diff --git a/src/Sampler/Xray/test/Unit/AWSXraySamplerClientTest.php b/src/Sampler/Xray/test/Unit/AWSXraySamplerClientTest.php new file mode 100644 index 000000000..956539a0b --- /dev/null +++ b/src/Sampler/Xray/test/Unit/AWSXraySamplerClientTest.php @@ -0,0 +1,103 @@ +rulesJson = file_get_contents($dataDir . '/sampling_rules.json'); + $this->targetsJson = file_get_contents($dataDir . '/sampling_targets.json'); + } + + public function testGetSamplingRules(): void + { + // 1) Mock Guzzle client to return our sample JSON + $mockHttp = $this->createMock(\GuzzleHttp\Client::class); + $mockHttp->expects($this->once()) + ->method('post') + ->with('GetSamplingRules') + ->willReturn(new Response(200, [], $this->rulesJson)); + + // 2) Instantiate client and inject mock + $client = new AWSXRaySamplerClient('https://xray'); + $ref = new ReflectionClass($client); + $prop = $ref->getProperty('httpClient'); + $prop->setAccessible(true); + $prop->setValue($client, $mockHttp); + + // 3) Call method under test + $rules = $client->getSamplingRules(); + + // 4) Assertions: two rules, correct mapping + $this->assertCount(2, $rules); + + /** @var SamplingRule $r1 */ + $r1 = $rules[0]; + $this->assertSame('Default', $r1->RuleName); + $this->assertSame(0.05, $r1->FixedRate); + $this->assertSame(100, $r1->ReservoirSize); + $this->assertEquals(['foo'=>'bar','abc'=>'1234'], $r1->Attributes); + + /** @var SamplingRule $r2 */ + $r2 = $rules[1]; + $this->assertSame('test', $r2->RuleName); + $this->assertSame(0.11, $r2->FixedRate); + $this->assertSame(1, $r2->ReservoirSize); + } + + public function testGetSamplingTargets(): void + { + // 1) Mock Guzzle client for SamplingTargets + $mockHttp = $this->createMock(\GuzzleHttp\Client::class); + $mockHttp->expects($this->once()) + ->method('post') + ->with('SamplingTargets', $this->arrayHasKey('json')) + ->willReturn(new Response(200, [], $this->targetsJson)); + + // 2) Instantiate and inject mock + $client = new AWSXRaySamplerClient('https://xray'); + $ref = new ReflectionClass($client); + $prop = $ref->getProperty('httpClient'); + $prop->setAccessible(true); + $prop->setValue($client, $mockHttp); + + // 3) Build a sample SamplingStatisticsDocument + $statDoc = new SamplingStatisticsDocument( + 'client1', + 'test', + 10, + 5, + 2, + 1234.0 + ); + + // 4) Call method under test + $resp = $client->getSamplingTargets([$statDoc]); + + // 5) Assertions on returned stdClass + $this->assertIsObject($resp); + $this->assertSame(1707551387.0, $resp->LastRuleModification); + $this->assertObjectHasProperty('SamplingTargetDocuments', $resp); + $this->assertCount(2, $resp->SamplingTargetDocuments); + + $t1 = $resp->SamplingTargetDocuments[0]; + $this->assertSame('test', $t1->RuleName); + $this->assertSame(30, $t1->ReservoirQuota); + $this->assertSame(0.10, $t1->FixedRate); + + $t2 = $resp->SamplingTargetDocuments[1]; + $this->assertSame('Default', $t2->RuleName); + $this->assertSame(0, $t2->ReservoirQuota); + $this->assertSame(0.05, $t2->FixedRate); + } +} diff --git a/src/Sampler/Xray/test/Unit/RateLimiterTest.php b/src/Sampler/Xray/test/Unit/RateLimiterTest.php new file mode 100644 index 000000000..8d0b8dcbd --- /dev/null +++ b/src/Sampler/Xray/test/Unit/RateLimiterTest.php @@ -0,0 +1,43 @@ +assertTrue($limiter->tryAcquire()); + $this->assertTrue($limiter->tryAcquire()); + $this->assertTrue($limiter->tryAcquire()); + } + + public function testCannotAcquireAfterCapacity(): void + { + $limiter = new RateLimiter(2, 1000); + $this->assertTrue($limiter->tryAcquire()); + $this->assertTrue($limiter->tryAcquire()); + $this->assertFalse($limiter->tryAcquire(), 'Should not acquire more than capacity'); + } + + public function testRefillAfterInterval(): void + { + // Use a short interval for test (100 ms) + $limiter = new RateLimiter(1, 100); + $this->assertTrue($limiter->tryAcquire(), 'First acquire should succeed'); + $this->assertFalse($limiter->tryAcquire(), 'Second acquire before refill should fail'); + + // Wait >100 ms to allow refill + usleep(150000); // 150 ms + + $this->assertTrue($limiter->tryAcquire(), 'Acquire after interval should succeed'); + } + + public function testGetCapacity(): void + { + $limiter = new RateLimiter(5, 1000); + $this->assertEquals(5, $limiter->getCapacity()); + } +} diff --git a/src/Sampler/Xray/test/Unit/RulesCacheTest.php b/src/Sampler/Xray/test/Unit/RulesCacheTest.php new file mode 100644 index 000000000..b40d8dd17 --- /dev/null +++ b/src/Sampler/Xray/test/Unit/RulesCacheTest.php @@ -0,0 +1,118 @@ +clock = new Clock(); + $this->resource = ResourceInfo::create(Attributes::create([ + 'service.name' => 'test-service', + 'cloud.platform' => 'aws_ecs' + ])); + } + + public function testUpdateRulesSortsByPriorityThenName(): void + { + $fallback = new AlwaysOffSampler(); + $cache = new RulesCache($this->clock, 'client', $this->resource, $fallback); + + $rule1 = new SamplingRule('b', 2, 0.1, 0, '*','*','*','*','*','*', 1, []); + $rule2 = new SamplingRule('a', 1, 0.1, 0, '*','*','*','*','*','*', 1, []); + $rule3 = new SamplingRule('c', 1, 0.1, 0, '*','*','*','*','*','*', 1, []); + + // Provide in unsorted order + $cache->updateRules([$rule1, $rule2, $rule3]); + + $names = array_map( + fn($ap) => $ap->getRuleName(), + $cache->getAppliers() + ); + + // Expected order: a (prio1), c (prio1, name 'c' > 'a'), b (prio2) + $this->assertEquals(['a', 'c', 'b'], $names); + } + + public function testUpdateRulesReusesExistingAppliers(): void + { + $fallback = new AlwaysOffSampler(); + $cache = new RulesCache($this->clock, 'client', $this->resource, $fallback); + + $ruleA1 = new SamplingRule('ruleA', 1, 0.1, 0, '*','*','*','*','*','*',1,[]); + $ruleB = new SamplingRule('ruleB', 1, 0.1, 0, '*','*','*','*','*','*',1,[]); + + $cache->updateRules([$ruleA1, $ruleB]); + $appliers1 = $cache->getAppliers(); + + // Now update rules: change ruleA's FixedRate and remove ruleB; add ruleC + $ruleA2 = new SamplingRule('ruleA', 1, 0.5, 0, '*','*','*','*','*','*',1,[]); + $ruleC = new SamplingRule('ruleC', 2, 0.1, 0, '*','*','*','*','*','*',1,[]); + + $cache->updateRules([$ruleA2, $ruleC]); + $appliers2 = $cache->getAppliers(); + + // Extract applier objects for ruleA + $a1 = array_filter($appliers1, fn($a) => $a->getRuleName() === 'ruleA'); + $a2 = array_filter($appliers2, fn($a) => $a->getRuleName() === 'ruleA'); + $this->assertCount(1, $a1); + $this->assertCount(1, $a2); + $a1 = array_shift($a1); + $a2 = array_shift($a2); + + // The applier for ruleA should be the same instance (reused) + $this->assertSame($a1, $a2); + + // ruleB should be removed, ruleC added + $names2 = array_map(fn($a) => $a->getRuleName(), $appliers2); + $this->assertNotContains('ruleB', $names2); + $this->assertContains('ruleC', $names2); + } + + public function testUpdateTargetsClonesMatchingAppliers(): void + { + $fallback = new AlwaysOffSampler(); + $cache = new RulesCache($this->clock, 'client', $this->resource, $fallback); + + $ruleA = new SamplingRule('ruleA', 1, 0.1, 5, '*','*','*','*','*','*',1,[]); + $ruleB = new SamplingRule('ruleB', 1, 0.1, 5, '*','*','*','*','*','*',1,[]); + + $cache->updateRules([$ruleA, $ruleB]); + $appliers1 = $cache->getAppliers(); + + // Prepare a dummy target doc for ruleA + $targetDoc = (object)[ + 'RuleName' => 'ruleA', + 'FixedRate' => 0.2, + 'ReservoirQuota' => 2, + 'ReservoirQuotaTTL'=> time() + 60, + 'Interval' => 15, + ]; + $cache->updateTargets(['ruleA' => $targetDoc]); + $appliers2 = $cache->getAppliers(); + + // ruleA applier should be a new instance + $a1 = array_filter($appliers1, fn($a) => $a->getRuleName() === 'ruleA'); + $a2 = array_filter($appliers2, fn($a) => $a->getRuleName() === 'ruleA'); + $a1 = array_shift($a1); + $a2 = array_shift($a2); + $this->assertNotSame($a1, $a2); + + // ruleB applier should be unchanged + $b1 = array_filter($appliers1, fn($a) => $a->getRuleName() === 'ruleB'); + $b2 = array_filter($appliers2, fn($a) => $a->getRuleName() === 'ruleB'); + $b1 = array_shift($b1); + $b2 = array_shift($b2); + $this->assertSame($b1, $b2); + } +} diff --git a/src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php b/src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php new file mode 100644 index 000000000..a0d843fb5 --- /dev/null +++ b/src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php @@ -0,0 +1,294 @@ +clock = new Clock(); + } + + public function testWildcardRuleMatchesAnyAttributesOldSemanticConventions(): void + { + // Rule with all wildcards and no specific attributes + $rule = new SamplingRule( + 'wildcard', + 1, + 0.5, + 0, + '*', '*', '*', '*', '*', '*', 1, [] + ); + $applier = new SamplingRuleApplier('client', $this->clock, $rule); + + // Attributes that should all match '*' + $attrs = Attributes::create([ + TraceAttributes::HTTP_METHOD => 'POST', + TraceAttributes::HTTP_TARGET => '/foo/bar', + TraceAttributes::HTTP_HOST => 'example.com', + ]); + $resource = ResourceInfo::create(Attributes::create([ + TraceAttributes::SERVICE_NAME => 'AnyService', + TraceAttributes::CLOUD_PLATFORM => 'aws_lambda', + ])); + + $this->assertTrue( + $applier->matches($attrs, $resource), + 'Wildcard rule should match any attributes' + ); + } + + public function testSpecificRuleMatchesExactAttributesOldSemanticConventions(): void + { + // Rule with specific matching values + $rule = new SamplingRule( + 'specific', + 1, + 0.5, + 0, + 'example.com', + 'GET', + 'arn:aws:ecs:123', + 'MyService', + 'AWS::ECS::Container', + '/api/test', + 1, + ['env' => 'prod'] + ); + $applier = new SamplingRuleApplier('client', $this->clock, $rule); + + // Matching attributes + $attrs = Attributes::create([ + TraceAttributes::HTTP_METHOD => 'GET', + TraceAttributes::HTTP_URL => 'https://example.com/api/test?x=1', + TraceAttributes::HTTP_HOST => 'example.com', + 'env' => 'prod', + ]); + $resource = ResourceInfo::create(Attributes::create([ + TraceAttributes::SERVICE_NAME => 'MyService', + TraceAttributes::CLOUD_PLATFORM => 'aws_ecs', + 'aws.ecs.container.arn' => 'arn:aws:ecs:123' + ])); + + $this->assertTrue( + $applier->matches($attrs, $resource), + 'Specific rule should match when all values line up' + ); + } + + public function testWildcardRuleMatchesAnyAttributesNewSemanticConventions(): void + { + // Rule with all wildcards and no specific attributes + $rule = new SamplingRule( + 'wildcard', + 1, + 0.5, + 0, + '*', '*', '*', '*', '*', '*', 1, [] + ); + $applier = new SamplingRuleApplier('client', $this->clock, $rule); + + // Attributes that should all match '*' + $attrs = Attributes::create([ + TraceAttributes::HTTP_REQUEST_METHOD => 'POST', + TraceAttributes::URL_PATH => '/foo/bar', + TraceAttributes::SERVER_ADDRESS => 'example.com', + ]); + $resource = ResourceInfo::create(Attributes::create([ + TraceAttributes::SERVICE_NAME => 'AnyService', + TraceAttributes::CLOUD_PLATFORM => 'aws_lambda', + ])); + + $this->assertTrue( + $applier->matches($attrs, $resource), + 'Wildcard rule should match any attributes' + ); + } + + public function testSpecificRuleMatchesExactAttributesNewSemanticConventions(): void + { + // Rule with specific matching values + $rule = new SamplingRule( + 'specific', + 1, + 0.5, + 0, + 'example.com', + 'GET', + 'arn:aws:ecs:123', + 'MyService', + 'AWS::ECS::Container', + '/api/test', + 1, + ['env' => 'prod'] + ); + $applier = new SamplingRuleApplier('client', $this->clock, $rule); + + // Matching attributes + $attrs = Attributes::create([ + TraceAttributes::HTTP_REQUEST_METHOD => 'GET', + TraceAttributes::URL_FULL => 'https://example.com/api/test?x=1', + TraceAttributes::SERVER_ADDRESS => 'example.com', + 'env' => 'prod', + ]); + $resource = ResourceInfo::create(Attributes::create([ + TraceAttributes::SERVICE_NAME => 'MyService', + TraceAttributes::CLOUD_PLATFORM => 'aws_ecs', + 'aws.ecs.container.arn' => 'arn:aws:ecs:123' + ])); + + $this->assertTrue( + $applier->matches($attrs, $resource), + 'Specific rule should match when all values line up' + ); + } + + public function testRuleDoesNotMatchWhenOneAttributeDiffers(): void + { + // Same rule as above + $rule = new SamplingRule( + 'specific', + 1, + 0.5, + 0, + 'example.com', + 'GET', + 'arn:aws:ecs:123', + 'MyService', + 'AWS::ECS::Container', + '/api/test', + 1, + ['env' => 'prod'] + ); + $applier = new SamplingRuleApplier('client', $this->clock, $rule); + + // Attributes with wrong HTTP method + $attrs = Attributes::create([ + TraceAttributes::HTTP_METHOD => 'POST', + TraceAttributes::HTTP_URL => 'https://example.com/api/test', + TraceAttributes::HTTP_HOST => 'example.com', + 'env' => 'prod', + ]); + $resource = ResourceInfo::create(Attributes::create([ + TraceAttributes::SERVICE_NAME => 'MyService', + TraceAttributes::CLOUD_PLATFORM => 'aws_ecs', + 'aws.ecs.container.arn' => 'arn:aws:ecs:123' + ])); + + $this->assertFalse( + $applier->matches($attrs, $resource), + 'Rule should not match when HTTP method differs' + ); + } + + public function testShouldSample_incrementsStatistics_andHonorsReservoirSamplerDecision(): void + { + $rule = new SamplingRule('r', 1, 0.0, 1, '*', '*', '*', '*', '*', '*', 1, []); + $applier = new SamplingRuleApplier('c', new Clock(), $rule, null); + + // Mock reservoirSampler to RECORD + $reservoirMock = $this->createMock(SamplerInterface::class); + $reservoirMock->method('shouldSample') + ->willReturn(new SamplingResult(SamplingResult::RECORD_AND_SAMPLE, [], null)); + // Mock fixedRateSampler to DROP (should not be used) + $fixedMock = $this->createMock(SamplerInterface::class); + $fixedMock->method('shouldSample') + ->willReturn(new SamplingResult(SamplingResult::DROP, [], null)); + + // Inject mocks via reflection + $ref = new \ReflectionClass($applier); + $propRes = $ref->getProperty('reservoirSampler'); $propRes->setAccessible(true); + $propRes->setValue($applier, $reservoirMock); + $propFix = $ref->getProperty('fixedRateSampler'); $propFix->setAccessible(true); + $propFix->setValue($applier, $fixedMock); + // Ensure borrowing = true + $propBorrow = $ref->getProperty('borrowing'); $propBorrow->setAccessible(true); + $propBorrow->setValue($applier, true); + + $context = $this->createMock(ContextInterface::class); + $attributes = Attributes::create([]); + + // Perform sampling + $result = $applier->shouldSample($context, 'trace', 'span', 0, $attributes, []); + $this->assertSame(SamplingResult::RECORD_AND_SAMPLE, $result->getDecision()); + + // Snapshot statistics + $now = new \DateTimeImmutable(); + $statsDoc = $applier->snapshot($now); + + $this->assertSame(1, $statsDoc->RequestCount); + $this->assertSame(1, $statsDoc->SampleCount); + $this->assertSame(1, $statsDoc->BorrowCount); + } + + public function testShouldSample_onReservoirDrop_usesFixedRateSampler_andIncrementsSampleCountOnly(): void + { + $rule = new SamplingRule('r2', 1, 1.0, 0, '*', '*', '*', '*', '*', '*', 1, []); + $applier = new SamplingRuleApplier('c2', new Clock(), $rule, null); + + // reservoirSampler: always DROP + $reservoirMock = $this->createMock(SamplerInterface::class); + $reservoirMock->method('shouldSample') + ->willReturn(new SamplingResult(SamplingResult::DROP, [], null)); + // fixedRateSampler: RECORD + $fixedMock = $this->createMock(SamplerInterface::class); + $fixedMock->method('shouldSample') + ->willReturn(new SamplingResult(SamplingResult::RECORD_AND_SAMPLE, [], null)); + + $ref = new \ReflectionClass($applier); + $propRes = $ref->getProperty('reservoirSampler'); $propRes->setAccessible(true); + $propRes->setValue($applier, $reservoirMock); + $propFix = $ref->getProperty('fixedRateSampler'); $propFix->setAccessible(true); + $propFix->setValue($applier, $fixedMock); + + $context = $this->createMock(ContextInterface::class); + $attributes = Attributes::create([]); + + $result = $applier->shouldSample($context, 't2', 's2', 0, $attributes, []); + $this->assertSame(SamplingResult::RECORD_AND_SAMPLE, $result->getDecision()); + + $now = new \DateTimeImmutable(); + $statsDoc = $applier->snapshot($now); + $this->assertSame(1, $statsDoc->RequestCount); + $this->assertSame(1, $statsDoc->SampleCount); + $this->assertSame(0, $statsDoc->BorrowCount); + } + + public function testSnapshot_resetsStatisticsAfterCapture(): void + { + $rule = new SamplingRule('r3', 1, 0.0, 1, '*', '*', '*', '*', '*', '*', 1, []); + $applier = new SamplingRuleApplier('c3', new Clock(), $rule, null); + + // simulate stats by reflection + $refStats = new \ReflectionProperty($applier, 'statistics'); + $refStats->setAccessible(true); + $stats = $refStats->getValue($applier); + $stats->requestCount = 5; + $stats->sampleCount = 2; + $stats->borrowCount = 1; + + $now = new \DateTimeImmutable(); + $doc1 = $applier->snapshot($now); + $this->assertSame(5, $doc1->RequestCount); + $this->assertSame(2, $doc1->SampleCount); + $this->assertSame(1, $doc1->BorrowCount); + + // After snapshot, internal counters should reset + $doc2 = $applier->snapshot($now); + $this->assertSame(0, $doc2->RequestCount); + $this->assertSame(0, $doc2->SampleCount); + $this->assertSame(0, $doc2->BorrowCount); + } +} diff --git a/src/Sampler/Xray/test/Unit/data/sampling_rules.json b/src/Sampler/Xray/test/Unit/data/sampling_rules.json new file mode 100644 index 000000000..e686867f7 --- /dev/null +++ b/src/Sampler/Xray/test/Unit/data/sampling_rules.json @@ -0,0 +1,43 @@ +{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 1.676038494E9, + "ModifiedAt": 1.676038494E9, + "SamplingRule": { + "Attributes": {"foo":"bar","abc":"1234"}, + "FixedRate": 0.05, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 100, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 1.67799933E9, + "ModifiedAt": 1.67799933E9, + "SamplingRule": { + "Attributes": {"abc":"1234"}, + "FixedRate": 0.11, + "HTTPMethod": "*", + "Host": "*", + "Priority": 20, + "ReservoirSize": 1, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/test", + "RuleName": "test", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +} \ No newline at end of file diff --git a/src/Sampler/Xray/test/Unit/data/sampling_targets.json b/src/Sampler/Xray/test/Unit/data/sampling_targets.json new file mode 100644 index 000000000..498fe1505 --- /dev/null +++ b/src/Sampler/Xray/test/Unit/data/sampling_targets.json @@ -0,0 +1,20 @@ +{ + "LastRuleModification": 1707551387.0, + "SamplingTargetDocuments": [ + { + "FixedRate": 0.10, + "Interval": 10, + "ReservoirQuota": 30, + "ReservoirQuotaTTL": 1707764006.0, + "RuleName": "test" + }, + { + "FixedRate": 0.05, + "Interval": 10, + "ReservoirQuota": 0, + "ReservoirQuotaTTL": 1707764006.0, + "RuleName": "Default" + } + ], + "UnprocessedStatistics": [] +} \ No newline at end of file From 10dc6b5d80d6fdae8248adb1d4c6bf0553b2c5e7 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Thu, 10 Jul 2025 00:15:48 -0700 Subject: [PATCH 12/18] updated README --- .github/workflows/php.yml | 1 + src/Sampler/Xray/README.md | 104 ++++++++++++------------------------- 2 files changed, 35 insertions(+), 70 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 28025abbf..a9b80846c 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -60,6 +60,7 @@ jobs: 'ResourceDetectors/Container', 'ResourceDetectors/DigitalOcean', 'Sampler/RuleBased', + 'Sampler/Xray', 'Shims/OpenTracing', 'Symfony', 'Utils/Test' diff --git a/src/Sampler/Xray/README.md b/src/Sampler/Xray/README.md index ccde5d7bc..4438f4124 100644 --- a/src/Sampler/Xray/README.md +++ b/src/Sampler/Xray/README.md @@ -1,87 +1,51 @@ -# Contrib Sampler +# AWS X-Ray Sampler -Provides additional samplers that are not part of the official specification. +Provides a sampler which can get sampling configurations from AWS X-Ray to make sampling decisions. See: [AWS X-Ray Sampling](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-sampling) ## Installation ```shell -composer require open-telemetry/sampler-rule-based +composer require open-telemetry/contrib-aws-xray-sampler ``` -## RuleBasedSampler - -Allows sampling based on a list of rule sets. The first matching rule set will decide the sampling result. +## Configuration +You can configure the `AWSXRayRemoteSampler` as per the following example. +Note that you will need to configure your [OpenTelemetry Collector for +X-Ray remote sampling](https://aws-otel.github.io/docs/getting-started/remote-sampling). ```php -$sampler = new RuleBasedSampler( - [ - new RuleSet( - [ - new SpanKindRule(Kind::Server), - new AttributeRule('url.path', '~^/health$~'), - ], - new AlwaysOffSampler(), - ), - ], - new AlwaysOnSampler(), -); -``` - -### Configuration - -###### Example: drop spans for the /health endpoint - -```yaml -contrib_rule_based: - rule_sets: - - rules: - - span_kind: { kind: SERVER } - - attribute: { key: url.path, pattern: ~^/health$~ } - delegate: - always_off: {} - fallback: # ... -``` + 'MyServiceName', + 'service.version'=> '1.0.0', + 'cloud.provider' => 'aws', +])); -```php -$sampler = new AlwaysRecordingSampler( - new ParentBasedSampler(new AlwaysOnSampler()), +$xraySampler = new AWSXRayRemoteSampler( + $resource, + 'http://localhost:2000', + 2 ); -``` - -### Configuration -```yaml -always_recording: - sampler: # ... +$tracerProvider = TracerProvider::builder() + ->setResource($resource) + ->setSampler($xraySampler) + ->addSpanProcessor( + new SimpleSpanProcessor( + (new ConsoleSpanExporterFactory())->create() + ) + ) + ->build(); ``` From f9174d2a738bab19c9e1a9ebb06138b07e03868b Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Thu, 10 Jul 2025 00:36:25 -0700 Subject: [PATCH 13/18] updated after running style checks --- src/Sampler/Xray/src/AWSXRayRemoteSampler.php | 33 ++-- src/Sampler/Xray/src/AWSXRaySamplerClient.php | 6 +- src/Sampler/Xray/src/Clock.php | 3 + src/Sampler/Xray/src/FallbackSampler.php | 4 + src/Sampler/Xray/src/Matcher.php | 4 + src/Sampler/Xray/src/RateLimitingSampler.php | 5 +- src/Sampler/Xray/src/RulesCache.php | 18 +- src/Sampler/Xray/src/SamplingRule.php | 14 +- src/Sampler/Xray/src/SamplingRuleApplier.php | 41 ++-- .../Xray/src/SamplingStatisticsDocument.php | 11 +- src/Sampler/Xray/src/Statistics.php | 3 + .../test/Unit/AWSXRayRemoteSamplerTest.php | 21 ++- .../test/Unit/AWSXRaySamplerClientTest.php | 176 ------------------ .../Xray/test/Unit/RateLimiterTest.php | 3 +- src/Sampler/Xray/test/Unit/RulesCacheTest.php | 43 ++--- .../test/Unit/SamplingRuleApplierTest.php | 50 +++-- 16 files changed, 160 insertions(+), 275 deletions(-) delete mode 100644 src/Sampler/Xray/test/Unit/AWSXRaySamplerClientTest.php diff --git a/src/Sampler/Xray/src/AWSXRayRemoteSampler.php b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php index 6010d855b..e68bb2819 100644 --- a/src/Sampler/Xray/src/AWSXRayRemoteSampler.php +++ b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php @@ -1,24 +1,26 @@ root = new ParentBased(new _AWSXRayRemoteSampler($resource, $host, $rulePollingIntervalMillis)); } @@ -30,8 +32,7 @@ public function shouldSample( int $spanKind, AttributesInterface $attributes, array $links, - ): SamplingResult - { + ): SamplingResult { return $this->root->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); } @@ -76,8 +77,8 @@ class _AWSXRayRemoteSampler implements SamplerInterface */ public function __construct( ResourceInfo $resource, - string $awsProxyEndpoint = self::DEFAULT_AWS_PROXY_ENDPOINT, - int $pollingInterval = self::DEFAULT_RULES_POLLING_INTERVAL_SECONDS + string $awsProxyEndpoint = self::DEFAULT_AWS_PROXY_ENDPOINT, + int $pollingInterval = self::DEFAULT_RULES_POLLING_INTERVAL_SECONDS ) { $this->clock = new Clock(); $this->fallback = new FallbackSampler(); @@ -108,8 +109,8 @@ public function __construct( // 2) Schedule next fetch times with jitter $now = $this->clock->now(); - $this->nextRulesFetchTime = $now->modify('+ '.$this->rulePollingJitterMillis + $this->rulePollingIntervalMillis.' milliseconds'); - $this->nextTargetFetchTime = $now->modify('+ '.$this->targetPollingJitterMillis + $this->targetPollingIntervalMillis.' milliseconds'); + $this->nextRulesFetchTime = $now->modify('+ ' . $this->rulePollingJitterMillis + $this->rulePollingIntervalMillis . ' milliseconds'); + $this->nextTargetFetchTime = $now->modify('+ ' . $this->targetPollingJitterMillis + $this->targetPollingIntervalMillis . ' milliseconds'); } /** @@ -122,8 +123,7 @@ public function shouldSample( int $spanKind, AttributesInterface $attributes, array $links, - ): SamplingResult - { + ): SamplingResult { $now = $this->clock->now(); // 1) Refresh rules if needed @@ -166,7 +166,7 @@ public function shouldSample( $nextTargetFetchInterval = $nextTargetFetchInterval * 1000; - $this->nextTargetFetchTime = $now->modify('+ '.$this->targetPollingJitterMillis + $nextTargetFetchInterval.' milliseconds'); + $this->nextTargetFetchTime = $now->modify('+ ' . $this->targetPollingJitterMillis + $nextTargetFetchInterval . ' milliseconds'); } @@ -175,6 +175,7 @@ public function shouldSample( if ($this->rulesCache->expired()) { return $this->fallback->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); } + // delegate return $this->rulesCache->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); } @@ -187,7 +188,7 @@ private function getAndUpdateRules(DateTimeImmutable $now) } catch (Exception $e) { // ignore error } - $this->nextRulesFetchTime = $now->modify('+ '.$this->rulePollingJitterMillis + $this->rulePollingIntervalMillis.' milliseconds'); + $this->nextRulesFetchTime = $now->modify('+ ' . $this->rulePollingJitterMillis + $this->rulePollingIntervalMillis . ' milliseconds'); } public function getDescription(): string diff --git a/src/Sampler/Xray/src/AWSXRaySamplerClient.php b/src/Sampler/Xray/src/AWSXRaySamplerClient.php index 10ab62090..7019de0e7 100644 --- a/src/Sampler/Xray/src/AWSXRaySamplerClient.php +++ b/src/Sampler/Xray/src/AWSXRaySamplerClient.php @@ -1,4 +1,6 @@ getDecision() !== SamplingResult::DROP) { return $res; } + return $this->fixedRate->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); } diff --git a/src/Sampler/Xray/src/Matcher.php b/src/Sampler/Xray/src/Matcher.php index 1ee255ec8..1b6046a8a 100644 --- a/src/Sampler/Xray/src/Matcher.php +++ b/src/Sampler/Xray/src/Matcher.php @@ -1,5 +1,7 @@ limiter->tryAcquire()) { return new SamplingResult(SamplingResult::RECORD_AND_SAMPLE, [], null); } diff --git a/src/Sampler/Xray/src/RulesCache.php b/src/Sampler/Xray/src/RulesCache.php index d7bd7acfb..449a47d86 100644 --- a/src/Sampler/Xray/src/RulesCache.php +++ b/src/Sampler/Xray/src/RulesCache.php @@ -1,12 +1,15 @@ $a->compareTo($b)); + usort($newRules, fn (SamplingRule $a, SamplingRule $b) => $a->compareTo($b)); $newAppliers = []; foreach ($newRules as $rule) { // reuse existing applier if same ruleName @@ -44,6 +47,7 @@ public function updateRules(array $newRules): void foreach ($this->appliers as $ap) { if ($ap->getRuleName() === $rule->RuleName) { $found = $ap; + break; } } @@ -64,26 +68,27 @@ public function shouldSample( int $spanKind, AttributesInterface $attributes, array $links, - ): SamplingResult - { + ): SamplingResult { foreach ($this->appliers as $applier) { if ($applier->matches($attributes, $this->resource)) { return $applier->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); } } + // fallback if no rule matched return $this->fallbackSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); } public function nextTargetFetchTime(): \DateTimeImmutable { - $defaultPollingTime = $this->clock->now()->add(new \DateInterval('PT'.self::DEFAULT_TARGET_INTERVAL_SEC.'S')); + $defaultPollingTime = $this->clock->now()->add(new \DateInterval('PT' . self::DEFAULT_TARGET_INTERVAL_SEC . 'S')); if (empty($this->appliers)) { return $defaultPollingTime; } - $times = array_map(fn($a) => $a->getNextSnapshotTime(), $this->appliers); + $times = array_map(fn ($a) => $a->getNextSnapshotTime(), $this->appliers); $min = min($times); + return $min < $this->clock->now() ? $defaultPollingTime : $min; @@ -109,7 +114,6 @@ public function getAppliers(): array return $this->appliers; } - public function getDescription(): string { return 'RulesCache'; diff --git a/src/Sampler/Xray/src/SamplingRule.php b/src/Sampler/Xray/src/SamplingRule.php index 650e41a79..1d20c8071 100644 --- a/src/Sampler/Xray/src/SamplingRule.php +++ b/src/Sampler/Xray/src/SamplingRule.php @@ -1,5 +1,8 @@ RuleName = $ruleName; $this->Priority = $priority; @@ -53,6 +56,7 @@ public function jsonSerialize(): array public function compareTo($other): int { $cmp = $this->Priority <=> $other->Priority; + return $cmp !== 0 ? $cmp : strcmp($this->RuleName, $other->RuleName); } } diff --git a/src/Sampler/Xray/src/SamplingRuleApplier.php b/src/Sampler/Xray/src/SamplingRuleApplier.php index 7bcb98691..26963751e 100644 --- a/src/Sampler/Xray/src/SamplingRuleApplier.php +++ b/src/Sampler/Xray/src/SamplingRuleApplier.php @@ -1,14 +1,17 @@ fixedRateSampler = new TraceIdRatioBasedSampler($rule->FixedRate); - $this->reservoirEndTime = new \DateTimeImmutable('@'.PHP_INT_MAX); + $this->reservoirEndTime = new \DateTimeImmutable('@' . PHP_INT_MAX); $this->nextSnapshotTime = $clock->now(); } @@ -55,7 +58,7 @@ public function matches(AttributesInterface $attributes, ResourceInfo $resource) } $httpMethod = $attributes->get(TraceAttributes::HTTP_METHOD) ?? $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD); - if ($httpMethod == "_OTHER") { + if ($httpMethod == '_OTHER') { $httpMethod = $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD_ORIGINAL); } $httpHost = $attributes->get(TraceAttributes::HTTP_HOST) ?? $attributes->get(TraceAttributes::SERVER_ADDRESS) ; @@ -69,15 +72,14 @@ public function matches(AttributesInterface $attributes, ResourceInfo $resource) ?? ''; return Matcher::attributeMatch($attributes->toArray(), $this->rule->Attributes) - && Matcher::wildcardMatch($httpTarget, $this->rule->UrlPath) - && Matcher::wildcardMatch($httpMethod, $this->rule->HttpMethod) - && Matcher::wildcardMatch($httpHost, $this->rule->Host) - && Matcher::wildcardMatch($serviceName, $this->rule->ServiceName) - && Matcher::wildcardMatch($serviceType, $this->rule->ServiceType) - && Matcher::wildcardMatch($arn, $this->rule->ResourceArn); + && Matcher::wildcardMatch($httpTarget, $this->rule->UrlPath) + && Matcher::wildcardMatch($httpMethod, $this->rule->HttpMethod) + && Matcher::wildcardMatch($httpHost, $this->rule->Host) + && Matcher::wildcardMatch($serviceName, $this->rule->ServiceName) + && Matcher::wildcardMatch($serviceType, $this->rule->ServiceType) + && Matcher::wildcardMatch($arn, $this->rule->ResourceArn); } - public function shouldSample( ContextInterface $parentContext, string $traceId, @@ -85,24 +87,27 @@ public function shouldSample( int $spanKind, AttributesInterface $attributes, array $links, - ): SamplingResult - { + ): SamplingResult { $this->statistics->requestCount++; $now = $this->clock->now(); if ($now < $this->reservoirEndTime) { - $res = $this->reservoirSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links);; + $res = $this->reservoirSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + ; if ($res->getDecision() !== SamplingResult::DROP) { if ($this->borrowing) { $this->statistics->borrowCount++; } $this->statistics->sampleCount++; + return $res; } } - $res = $this->fixedRateSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links);; + $res = $this->fixedRateSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + ; if ($res->getDecision() !== SamplingResult::DROP) { $this->statistics->sampleCount++; } + return $res; } @@ -151,7 +156,7 @@ public function withTarget(object $targetDoc, \DateTimeImmutable $now): self $newReservoirSampler = $quota > 0 ? new RateLimitingSampler($quota) : new AlwaysOffSampler(); - $newReservoirEndTime = \DateTimeImmutable::createFromFormat('U', (string)$ttlSeconds) + $newReservoirEndTime = \DateTimeImmutable::createFromFormat('U', (string) $ttlSeconds) ?: $newReservoirEndTime; } else { // if no quota provided, turn off reservoir @@ -175,7 +180,6 @@ public function withTarget(object $targetDoc, \DateTimeImmutable $now): self return $clone; } - public function getNextSnapshotTime(): \DateTimeImmutable { return $this->nextSnapshotTime; @@ -186,7 +190,8 @@ public function getRuleName(): string return $this->ruleName; } - public function setRule($rule) { + public function setRule($rule) + { $this->rule = $rule; } } diff --git a/src/Sampler/Xray/src/SamplingStatisticsDocument.php b/src/Sampler/Xray/src/SamplingStatisticsDocument.php index db03bb709..a9b0bc44e 100644 --- a/src/Sampler/Xray/src/SamplingStatisticsDocument.php +++ b/src/Sampler/Xray/src/SamplingStatisticsDocument.php @@ -1,15 +1,18 @@ willReturn($dummyRules); $mockClient->expects($this->once()) ->method('getSamplingTargets') - ->willReturn((object)[ + ->willReturn((object) [ 'SamplingTargetDocuments' => [], 'Interval' => 5, 'LastRuleModification' => 0, @@ -73,7 +74,9 @@ public function testShouldSampleUpdatesRulesAndTargets(): void // 5) Call shouldSample $result = $sampler->shouldSample( $this->createMock(ContextInterface::class), - 'traceId', 'spanName', 1, + 'traceId', + 'spanName', + 1, Attributes::create([]), [] ); @@ -112,7 +115,9 @@ public function testShouldSampleFallbackWhenExpired(): void $result = $sampler->shouldSample( $this->createMock(ContextInterface::class), - 't', 'n', 1, + 't', + 'n', + 1, Attributes::create([]), [] ); diff --git a/src/Sampler/Xray/test/Unit/AWSXRaySamplerClientTest.php b/src/Sampler/Xray/test/Unit/AWSXRaySamplerClientTest.php deleted file mode 100644 index d4f4f48c5..000000000 --- a/src/Sampler/Xray/test/Unit/AWSXRaySamplerClientTest.php +++ /dev/null @@ -1,176 +0,0 @@ -createMock(HttpClient::class); - $response = new Response(200, ['Content-Type' => 'application/json'], $json); - $mockHttp->expects($this->once()) - ->method('post') - ->with('GetSamplingRules') - ->willReturn($response); - - // Instantiate and inject the mock client - $client = new AWSXRaySamplerClient('https://dummy'); - $ref = new ReflectionClass($client); - $prop = $ref->getProperty('httpClient'); - $prop->setAccessible(true); - $prop->setValue($client, $mockHttp); - - // Exercise getSamplingRules() - $rules = $client->getSamplingRules(); - - // Assertions - $this->assertCount(2, $rules); - $this->assertInstanceOf(SamplingRule::class, $rules[0]); - $this->assertEquals('Default', $rules[0]->RuleName); - $this->assertEquals(10000, $rules[0]->Priority); - $this->assertEquals(0.05, $rules[0]->FixedRate); - $this->assertEquals(100, $rules[0]->ReservoirSize); - $this->assertEquals(['foo' => 'bar', 'abc' => '1234'], $rules[0]->Attributes); - - $this->assertEquals('test', $rules[1]->RuleName); - $this->assertEquals(20, $rules[1]->Priority); - $this->assertEquals(0.11, $rules[1]->FixedRate); - $this->assertEquals(1, $rules[1]->ReservoirSize); - $this->assertEquals(['abc' => '1234'], $rules[1]->Attributes); - } - - public function testGetSamplingTargetsParsesResponse(): void - { - $json = <<<'JSON' -{ - "LastRuleModification": 1707551387.0, - "SamplingTargetDocuments": [ - { - "FixedRate": 0.10, - "Interval": 10, - "ReservoirQuota": 30, - "ReservoirQuotaTTL": 1707764006.0, - "RuleName": "test" - }, - { - "FixedRate": 0.05, - "Interval": 10, - "ReservoirQuota": 0, - "ReservoirQuotaTTL": 1707764006.0, - "RuleName": "Default" - } - ], - "UnprocessedStatistics": [] -} -JSON; - - $stats = [ - (object)[ - 'ClientID' => 'cid', - 'RuleName' => 'test', - 'RequestCount' => 1, - 'SampleCount' => 1, - 'BorrowCount' => 0, - 'Timestamp' => 1234567890.0, - ] - ]; - - // Mock the Guzzle HTTP client - $mockHttp = $this->createMock(HttpClient::class); - $response = new Response(200, ['Content-Type' => 'application/json'], $json); - $mockHttp->expects($this->once()) - ->method('post') - ->with( - 'SamplingTargets', - $this->callback(fn($opts) => - isset($opts['json']['SamplingStatisticsDocuments']) && - count($opts['json']['SamplingStatisticsDocuments']) === 1 - ) - ) - ->willReturn($response); - - // Instantiate and inject the mock client - $client = new AWSXRaySamplerClient('https://dummy'); - $ref = new ReflectionClass($client); - $prop = $ref->getProperty('httpClient'); - $prop->setAccessible(true); - $prop->setValue($client, $mockHttp); - - // Exercise getSamplingTargets() - $result = $client->getSamplingTargets($stats); - - // Assertions on the stdClass response - $this->assertIsObject($result); - $this->assertEquals(1707551387.0, $result->LastRuleModification); - - $docs = $result->SamplingTargetDocuments; - $this->assertCount(2, $docs); - - $this->assertEquals('test', $docs[0]->RuleName); - $this->assertEquals(0.10, $docs[0]->FixedRate); - $this->assertEquals(30, $docs[0]->ReservoirQuota); - $this->assertEquals(1707764006.0, $docs[0]->ReservoirQuotaTTL); - $this->assertEquals(10, $docs[0]->Interval); - - $this->assertEquals('Default',$docs[1]->RuleName); - $this->assertEquals(0.05, $docs[1]->FixedRate); - $this->assertEquals(0, $docs[1]->ReservoirQuota); - } -} diff --git a/src/Sampler/Xray/test/Unit/RateLimiterTest.php b/src/Sampler/Xray/test/Unit/RateLimiterTest.php index 8d0b8dcbd..ff4a04c24 100644 --- a/src/Sampler/Xray/test/Unit/RateLimiterTest.php +++ b/src/Sampler/Xray/test/Unit/RateLimiterTest.php @@ -1,8 +1,9 @@ clock = new Clock(); $this->resource = ResourceInfo::create(Attributes::create([ 'service.name' => 'test-service', - 'cloud.platform' => 'aws_ecs' + 'cloud.platform' => 'aws_ecs', ])); } @@ -28,15 +29,15 @@ public function testUpdateRulesSortsByPriorityThenName(): void $fallback = new AlwaysOffSampler(); $cache = new RulesCache($this->clock, 'client', $this->resource, $fallback); - $rule1 = new SamplingRule('b', 2, 0.1, 0, '*','*','*','*','*','*', 1, []); - $rule2 = new SamplingRule('a', 1, 0.1, 0, '*','*','*','*','*','*', 1, []); - $rule3 = new SamplingRule('c', 1, 0.1, 0, '*','*','*','*','*','*', 1, []); + $rule1 = new SamplingRule('b', 2, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); + $rule2 = new SamplingRule('a', 1, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); + $rule3 = new SamplingRule('c', 1, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); // Provide in unsorted order $cache->updateRules([$rule1, $rule2, $rule3]); $names = array_map( - fn($ap) => $ap->getRuleName(), + fn ($ap) => $ap->getRuleName(), $cache->getAppliers() ); @@ -49,22 +50,22 @@ public function testUpdateRulesReusesExistingAppliers(): void $fallback = new AlwaysOffSampler(); $cache = new RulesCache($this->clock, 'client', $this->resource, $fallback); - $ruleA1 = new SamplingRule('ruleA', 1, 0.1, 0, '*','*','*','*','*','*',1,[]); - $ruleB = new SamplingRule('ruleB', 1, 0.1, 0, '*','*','*','*','*','*',1,[]); + $ruleA1 = new SamplingRule('ruleA', 1, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); + $ruleB = new SamplingRule('ruleB', 1, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); $cache->updateRules([$ruleA1, $ruleB]); $appliers1 = $cache->getAppliers(); // Now update rules: change ruleA's FixedRate and remove ruleB; add ruleC - $ruleA2 = new SamplingRule('ruleA', 1, 0.5, 0, '*','*','*','*','*','*',1,[]); - $ruleC = new SamplingRule('ruleC', 2, 0.1, 0, '*','*','*','*','*','*',1,[]); + $ruleA2 = new SamplingRule('ruleA', 1, 0.5, 0, '*', '*', '*', '*', '*', '*', 1, []); + $ruleC = new SamplingRule('ruleC', 2, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); $cache->updateRules([$ruleA2, $ruleC]); $appliers2 = $cache->getAppliers(); // Extract applier objects for ruleA - $a1 = array_filter($appliers1, fn($a) => $a->getRuleName() === 'ruleA'); - $a2 = array_filter($appliers2, fn($a) => $a->getRuleName() === 'ruleA'); + $a1 = array_filter($appliers1, fn ($a) => $a->getRuleName() === 'ruleA'); + $a2 = array_filter($appliers2, fn ($a) => $a->getRuleName() === 'ruleA'); $this->assertCount(1, $a1); $this->assertCount(1, $a2); $a1 = array_shift($a1); @@ -74,7 +75,7 @@ public function testUpdateRulesReusesExistingAppliers(): void $this->assertSame($a1, $a2); // ruleB should be removed, ruleC added - $names2 = array_map(fn($a) => $a->getRuleName(), $appliers2); + $names2 = array_map(fn ($a) => $a->getRuleName(), $appliers2); $this->assertNotContains('ruleB', $names2); $this->assertContains('ruleC', $names2); } @@ -84,14 +85,14 @@ public function testUpdateTargetsClonesMatchingAppliers(): void $fallback = new AlwaysOffSampler(); $cache = new RulesCache($this->clock, 'client', $this->resource, $fallback); - $ruleA = new SamplingRule('ruleA', 1, 0.1, 5, '*','*','*','*','*','*',1,[]); - $ruleB = new SamplingRule('ruleB', 1, 0.1, 5, '*','*','*','*','*','*',1,[]); + $ruleA = new SamplingRule('ruleA', 1, 0.1, 5, '*', '*', '*', '*', '*', '*', 1, []); + $ruleB = new SamplingRule('ruleB', 1, 0.1, 5, '*', '*', '*', '*', '*', '*', 1, []); $cache->updateRules([$ruleA, $ruleB]); $appliers1 = $cache->getAppliers(); // Prepare a dummy target doc for ruleA - $targetDoc = (object)[ + $targetDoc = (object) [ 'RuleName' => 'ruleA', 'FixedRate' => 0.2, 'ReservoirQuota' => 2, @@ -102,15 +103,15 @@ public function testUpdateTargetsClonesMatchingAppliers(): void $appliers2 = $cache->getAppliers(); // ruleA applier should be a new instance - $a1 = array_filter($appliers1, fn($a) => $a->getRuleName() === 'ruleA'); - $a2 = array_filter($appliers2, fn($a) => $a->getRuleName() === 'ruleA'); + $a1 = array_filter($appliers1, fn ($a) => $a->getRuleName() === 'ruleA'); + $a2 = array_filter($appliers2, fn ($a) => $a->getRuleName() === 'ruleA'); $a1 = array_shift($a1); $a2 = array_shift($a2); $this->assertNotSame($a1, $a2); // ruleB applier should be unchanged - $b1 = array_filter($appliers1, fn($a) => $a->getRuleName() === 'ruleB'); - $b2 = array_filter($appliers2, fn($a) => $a->getRuleName() === 'ruleB'); + $b1 = array_filter($appliers1, fn ($a) => $a->getRuleName() === 'ruleB'); + $b2 = array_filter($appliers2, fn ($a) => $a->getRuleName() === 'ruleB'); $b1 = array_shift($b1); $b2 = array_shift($b2); $this->assertSame($b1, $b2); diff --git a/src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php b/src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php index a0d843fb5..6649da3ac 100644 --- a/src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php +++ b/src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php @@ -1,16 +1,17 @@ clock, $rule); @@ -79,7 +87,7 @@ public function testSpecificRuleMatchesExactAttributesOldSemanticConventions(): $resource = ResourceInfo::create(Attributes::create([ TraceAttributes::SERVICE_NAME => 'MyService', TraceAttributes::CLOUD_PLATFORM => 'aws_ecs', - 'aws.ecs.container.arn' => 'arn:aws:ecs:123' + 'aws.ecs.container.arn' => 'arn:aws:ecs:123', ])); $this->assertTrue( @@ -96,7 +104,14 @@ public function testWildcardRuleMatchesAnyAttributesNewSemanticConventions(): vo 1, 0.5, 0, - '*', '*', '*', '*', '*', '*', 1, [] + '*', + '*', + '*', + '*', + '*', + '*', + 1, + [] ); $applier = new SamplingRuleApplier('client', $this->clock, $rule); @@ -146,7 +161,7 @@ public function testSpecificRuleMatchesExactAttributesNewSemanticConventions(): $resource = ResourceInfo::create(Attributes::create([ TraceAttributes::SERVICE_NAME => 'MyService', TraceAttributes::CLOUD_PLATFORM => 'aws_ecs', - 'aws.ecs.container.arn' => 'arn:aws:ecs:123' + 'aws.ecs.container.arn' => 'arn:aws:ecs:123', ])); $this->assertTrue( @@ -184,7 +199,7 @@ public function testRuleDoesNotMatchWhenOneAttributeDiffers(): void $resource = ResourceInfo::create(Attributes::create([ TraceAttributes::SERVICE_NAME => 'MyService', TraceAttributes::CLOUD_PLATFORM => 'aws_ecs', - 'aws.ecs.container.arn' => 'arn:aws:ecs:123' + 'aws.ecs.container.arn' => 'arn:aws:ecs:123', ])); $this->assertFalse( @@ -209,12 +224,15 @@ public function testShouldSample_incrementsStatistics_andHonorsReservoirSamplerD // Inject mocks via reflection $ref = new \ReflectionClass($applier); - $propRes = $ref->getProperty('reservoirSampler'); $propRes->setAccessible(true); + $propRes = $ref->getProperty('reservoirSampler'); + $propRes->setAccessible(true); $propRes->setValue($applier, $reservoirMock); - $propFix = $ref->getProperty('fixedRateSampler'); $propFix->setAccessible(true); + $propFix = $ref->getProperty('fixedRateSampler'); + $propFix->setAccessible(true); $propFix->setValue($applier, $fixedMock); // Ensure borrowing = true - $propBorrow = $ref->getProperty('borrowing'); $propBorrow->setAccessible(true); + $propBorrow = $ref->getProperty('borrowing'); + $propBorrow->setAccessible(true); $propBorrow->setValue($applier, true); $context = $this->createMock(ContextInterface::class); @@ -248,9 +266,11 @@ public function testShouldSample_onReservoirDrop_usesFixedRateSampler_andIncreme ->willReturn(new SamplingResult(SamplingResult::RECORD_AND_SAMPLE, [], null)); $ref = new \ReflectionClass($applier); - $propRes = $ref->getProperty('reservoirSampler'); $propRes->setAccessible(true); + $propRes = $ref->getProperty('reservoirSampler'); + $propRes->setAccessible(true); $propRes->setValue($applier, $reservoirMock); - $propFix = $ref->getProperty('fixedRateSampler'); $propFix->setAccessible(true); + $propFix = $ref->getProperty('fixedRateSampler'); + $propFix->setAccessible(true); $propFix->setValue($applier, $fixedMock); $context = $this->createMock(ContextInterface::class); From 038731f314417be3235885f21f296762407c6a7b Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Thu, 10 Jul 2025 00:53:22 -0700 Subject: [PATCH 14/18] updated more files --- src/Sampler/Xray/psalm.xml.dist | 14 -------------- src/Sampler/Xray/src/AWSXRaySamplerClient.php | 3 --- src/Sampler/Xray/src/Clock.php | 2 +- src/Sampler/Xray/src/Matcher.php | 2 +- src/Sampler/Xray/src/SamplingRuleApplier.php | 10 +++++----- .../Unit/AWSXRayRemoteSamplerTest.php | 0 .../Unit/AWSXraySamplerClientTest.php | 15 ++++++++------- .../Xray/{test => tests}/Unit/RateLimiterTest.php | 0 .../Xray/{test => tests}/Unit/RulesCacheTest.php | 0 .../Unit/SamplingRuleApplierTest.php | 0 .../{test => tests}/Unit/data/sampling_rules.json | 0 .../Unit/data/sampling_targets.json | 0 12 files changed, 15 insertions(+), 31 deletions(-) rename src/Sampler/Xray/{test => tests}/Unit/AWSXRayRemoteSamplerTest.php (100%) rename src/Sampler/Xray/{test => tests}/Unit/AWSXraySamplerClientTest.php (94%) rename src/Sampler/Xray/{test => tests}/Unit/RateLimiterTest.php (100%) rename src/Sampler/Xray/{test => tests}/Unit/RulesCacheTest.php (100%) rename src/Sampler/Xray/{test => tests}/Unit/SamplingRuleApplierTest.php (100%) rename src/Sampler/Xray/{test => tests}/Unit/data/sampling_rules.json (100%) rename src/Sampler/Xray/{test => tests}/Unit/data/sampling_targets.json (100%) diff --git a/src/Sampler/Xray/psalm.xml.dist b/src/Sampler/Xray/psalm.xml.dist index c9650afeb..155711712 100644 --- a/src/Sampler/Xray/psalm.xml.dist +++ b/src/Sampler/Xray/psalm.xml.dist @@ -2,8 +2,6 @@ @@ -14,16 +12,4 @@ - - - - - - - - - - - - diff --git a/src/Sampler/Xray/src/AWSXRaySamplerClient.php b/src/Sampler/Xray/src/AWSXRaySamplerClient.php index 7019de0e7..175f9f168 100644 --- a/src/Sampler/Xray/src/AWSXRaySamplerClient.php +++ b/src/Sampler/Xray/src/AWSXRaySamplerClient.php @@ -6,7 +6,6 @@ namespace OpenTelemetry\Contrib\Sampler\Xray; use GuzzleHttp\Client as HttpClient; -use GuzzleHttp\Exception\GuzzleException; /** * A lightweight HTTP client for AWS X-Ray sampling endpoints. @@ -34,7 +33,6 @@ public function __construct(string $host) /** * Fetches all sampling rules from X-Ray by paging through NextToken. * - * @throws GuzzleException on HTTP errors. * @return SamplingRule[] Array of SamplingRule instances. */ public function getSamplingRules(): array @@ -70,7 +68,6 @@ public function getSamplingRules(): array * Sends current statistics documents to X-Ray and returns the decoded response. * * @param SamplingStatisticsDocument[] $statistics - * @throws GuzzleException on HTTP errors. * @return object|null stdClass of the X-Ray GetSamplingTargets response. */ public function getSamplingTargets(array $statistics): ?object diff --git a/src/Sampler/Xray/src/Clock.php b/src/Sampler/Xray/src/Clock.php index 021c540a9..0677f1bba 100644 --- a/src/Sampler/Xray/src/Clock.php +++ b/src/Sampler/Xray/src/Clock.php @@ -14,6 +14,6 @@ public function now(): \DateTimeImmutable public function toUnixMillis(\DateTimeImmutable $dt): float { - return ($dt->getTimestamp() * 1000) + ($dt->format('v') / 1); + return $dt->getTimestamp() * 1000; } } diff --git a/src/Sampler/Xray/src/Matcher.php b/src/Sampler/Xray/src/Matcher.php index 1b6046a8a..bb3549a06 100644 --- a/src/Sampler/Xray/src/Matcher.php +++ b/src/Sampler/Xray/src/Matcher.php @@ -51,7 +51,7 @@ public static function wildcardMatch(?string $value, string $pattern): bool /** * Map OpenTelemetry cloud.platform values to X-Ray service type strings. */ - public static function getXRayCloudPlatform(?string $platform): string + public static function getXRayCloudPlatform(string $platform): string { return self::$xrayCloudPlatform[$platform] ?? ''; } diff --git a/src/Sampler/Xray/src/SamplingRuleApplier.php b/src/Sampler/Xray/src/SamplingRuleApplier.php index 26963751e..ebbf33df2 100644 --- a/src/Sampler/Xray/src/SamplingRuleApplier.php +++ b/src/Sampler/Xray/src/SamplingRuleApplier.php @@ -51,19 +51,19 @@ public function __construct(string $clientId, Clock $clock, SamplingRule $rule, public function matches(AttributesInterface $attributes, ResourceInfo $resource): bool { // Extract HTTP path - $httpTarget = $attributes->get(TraceAttributes::HTTP_TARGET) ?? $attributes->get(TraceAttributes::URL_PATH); - $httpUrl = $attributes->get(TraceAttributes::HTTP_URL) ?? $attributes->get(TraceAttributes::URL_FULL); + $httpTarget = $attributes->get(TraceAttributes::HTTP_TARGET) ?? $attributes->get(TraceAttributes::URL_PATH); // @phan-suppress-current-line PhanDeprecatedClassConstant + $httpUrl = $attributes->get(TraceAttributes::HTTP_URL) ?? $attributes->get(TraceAttributes::URL_FULL); // @phan-suppress-current-line PhanDeprecatedClassConstant if ($httpTarget == null && isset($httpUrl)) { $httpTarget = parse_url($httpUrl, PHP_URL_PATH); } - $httpMethod = $attributes->get(TraceAttributes::HTTP_METHOD) ?? $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD); + $httpMethod = $attributes->get(TraceAttributes::HTTP_METHOD) ?? $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD); // @phan-suppress-current-line PhanDeprecatedClassConstant if ($httpMethod == '_OTHER') { $httpMethod = $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD_ORIGINAL); } - $httpHost = $attributes->get(TraceAttributes::HTTP_HOST) ?? $attributes->get(TraceAttributes::SERVER_ADDRESS) ; + $httpHost = $attributes->get(TraceAttributes::HTTP_HOST) ?? $attributes->get(TraceAttributes::SERVER_ADDRESS) ; // @phan-suppress-current-line PhanDeprecatedClassConstant $serviceName= $resource->getAttributes()->get(TraceAttributes::SERVICE_NAME) ?? ''; - $cloudPlat = $resource->getAttributes()->get(TraceAttributes::CLOUD_PLATFORM) ?? null; + $cloudPlat = $resource->getAttributes()->get(TraceAttributes::CLOUD_PLATFORM) ?? ''; $serviceType= Matcher::getXRayCloudPlatform($cloudPlat); // ARN: ECS container ARN or Lambda faas.id diff --git a/src/Sampler/Xray/test/Unit/AWSXRayRemoteSamplerTest.php b/src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php similarity index 100% rename from src/Sampler/Xray/test/Unit/AWSXRayRemoteSamplerTest.php rename to src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php diff --git a/src/Sampler/Xray/test/Unit/AWSXraySamplerClientTest.php b/src/Sampler/Xray/tests/Unit/AWSXraySamplerClientTest.php similarity index 94% rename from src/Sampler/Xray/test/Unit/AWSXraySamplerClientTest.php rename to src/Sampler/Xray/tests/Unit/AWSXraySamplerClientTest.php index 956539a0b..8148f10be 100644 --- a/src/Sampler/Xray/test/Unit/AWSXraySamplerClientTest.php +++ b/src/Sampler/Xray/tests/Unit/AWSXraySamplerClientTest.php @@ -1,11 +1,12 @@ SamplingTargetDocuments[0]; $this->assertSame('test', $t1->RuleName); - $this->assertSame(30, $t1->ReservoirQuota); - $this->assertSame(0.10, $t1->FixedRate); + $this->assertSame(30, $t1->ReservoirQuota); + $this->assertSame(0.10, $t1->FixedRate); $t2 = $resp->SamplingTargetDocuments[1]; $this->assertSame('Default', $t2->RuleName); - $this->assertSame(0, $t2->ReservoirQuota); - $this->assertSame(0.05, $t2->FixedRate); + $this->assertSame(0, $t2->ReservoirQuota); + $this->assertSame(0.05, $t2->FixedRate); } } diff --git a/src/Sampler/Xray/test/Unit/RateLimiterTest.php b/src/Sampler/Xray/tests/Unit/RateLimiterTest.php similarity index 100% rename from src/Sampler/Xray/test/Unit/RateLimiterTest.php rename to src/Sampler/Xray/tests/Unit/RateLimiterTest.php diff --git a/src/Sampler/Xray/test/Unit/RulesCacheTest.php b/src/Sampler/Xray/tests/Unit/RulesCacheTest.php similarity index 100% rename from src/Sampler/Xray/test/Unit/RulesCacheTest.php rename to src/Sampler/Xray/tests/Unit/RulesCacheTest.php diff --git a/src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php b/src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php similarity index 100% rename from src/Sampler/Xray/test/Unit/SamplingRuleApplierTest.php rename to src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php diff --git a/src/Sampler/Xray/test/Unit/data/sampling_rules.json b/src/Sampler/Xray/tests/Unit/data/sampling_rules.json similarity index 100% rename from src/Sampler/Xray/test/Unit/data/sampling_rules.json rename to src/Sampler/Xray/tests/Unit/data/sampling_rules.json diff --git a/src/Sampler/Xray/test/Unit/data/sampling_targets.json b/src/Sampler/Xray/tests/Unit/data/sampling_targets.json similarity index 100% rename from src/Sampler/Xray/test/Unit/data/sampling_targets.json rename to src/Sampler/Xray/tests/Unit/data/sampling_targets.json From 6b2801c31b0f7608226fc2f2041a04d35c77882b Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Fri, 11 Jul 2025 11:07:53 -0700 Subject: [PATCH 15/18] Fixed build issues --- src/Sampler/Xray/phpstan.neon.dist | 7 +------ src/Sampler/Xray/src/AWSXRayRemoteSampler.php | 10 ++++++---- src/Sampler/Xray/src/AWSXRaySamplerClient.php | 1 + src/Sampler/Xray/src/RateLimitingSampler.php | 2 +- src/Sampler/Xray/src/SamplingRule.php | 1 + src/Sampler/Xray/src/SamplingRuleApplier.php | 4 +++- .../Xray/tests/Unit/AWSXRayRemoteSamplerTest.php | 1 + .../Xray/tests/Unit/AWSXraySamplerClientTest.php | 13 ++++++++----- .../Xray/tests/Unit/SamplingRuleApplierTest.php | 1 + 9 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/Sampler/Xray/phpstan.neon.dist b/src/Sampler/Xray/phpstan.neon.dist index f9cb5fac4..4c06d6b2e 100644 --- a/src/Sampler/Xray/phpstan.neon.dist +++ b/src/Sampler/Xray/phpstan.neon.dist @@ -6,9 +6,4 @@ parameters: level: 5 paths: - src - - tests - ignoreErrors: - - - message: "#Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface::.*#" - paths: - - src/ \ No newline at end of file + - tests \ No newline at end of file diff --git a/src/Sampler/Xray/src/AWSXRayRemoteSampler.php b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php index e68bb2819..896152a51 100644 --- a/src/Sampler/Xray/src/AWSXRayRemoteSampler.php +++ b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php @@ -14,6 +14,7 @@ use OpenTelemetry\SDK\Trace\SamplerInterface; use OpenTelemetry\SDK\Trace\SamplingResult; +/** @psalm-suppress UnusedClass */ class AWSXRayRemoteSampler implements SamplerInterface { private SamplerInterface $root; @@ -58,6 +59,7 @@ class _AWSXRayRemoteSampler implements SamplerInterface private AWSXRaySamplerClient $client; private int $rulePollingIntervalMillis; + /** @psalm-suppress UnusedProperty */ private int $targetPollingIntervalMillis; private DateTimeImmutable $nextRulesFetchTime; private DateTimeImmutable $nextTargetFetchTime; @@ -109,8 +111,8 @@ public function __construct( // 2) Schedule next fetch times with jitter $now = $this->clock->now(); - $this->nextRulesFetchTime = $now->modify('+ ' . $this->rulePollingJitterMillis + $this->rulePollingIntervalMillis . ' milliseconds'); - $this->nextTargetFetchTime = $now->modify('+ ' . $this->targetPollingJitterMillis + $this->targetPollingIntervalMillis . ' milliseconds'); + $this->nextRulesFetchTime = $now->modify('+ ' . ($this->rulePollingJitterMillis + $this->rulePollingIntervalMillis) . ' milliseconds'); + $this->nextTargetFetchTime = $now->modify('+ ' . ($this->targetPollingJitterMillis + $this->targetPollingIntervalMillis) . ' milliseconds'); } /** @@ -166,7 +168,7 @@ public function shouldSample( $nextTargetFetchInterval = $nextTargetFetchInterval * 1000; - $this->nextTargetFetchTime = $now->modify('+ ' . $this->targetPollingJitterMillis + $nextTargetFetchInterval . ' milliseconds'); + $this->nextTargetFetchTime = $now->modify('+ ' . ($this->targetPollingJitterMillis + $nextTargetFetchInterval) . ' milliseconds'); } @@ -188,7 +190,7 @@ private function getAndUpdateRules(DateTimeImmutable $now) } catch (Exception $e) { // ignore error } - $this->nextRulesFetchTime = $now->modify('+ ' . $this->rulePollingJitterMillis + $this->rulePollingIntervalMillis . ' milliseconds'); + $this->nextRulesFetchTime = $now->modify('+ ' . ($this->rulePollingJitterMillis + $this->rulePollingIntervalMillis) . ' milliseconds'); } public function getDescription(): string diff --git a/src/Sampler/Xray/src/AWSXRaySamplerClient.php b/src/Sampler/Xray/src/AWSXRaySamplerClient.php index 175f9f168..665b5e1d7 100644 --- a/src/Sampler/Xray/src/AWSXRaySamplerClient.php +++ b/src/Sampler/Xray/src/AWSXRaySamplerClient.php @@ -14,6 +14,7 @@ class AWSXRaySamplerClient { private HttpClient $httpClient; + /** @psalm-suppress UnusedProperty */ private string $host; /** diff --git a/src/Sampler/Xray/src/RateLimitingSampler.php b/src/Sampler/Xray/src/RateLimitingSampler.php index db92a7ac5..4aaa10a9c 100644 --- a/src/Sampler/Xray/src/RateLimitingSampler.php +++ b/src/Sampler/Xray/src/RateLimitingSampler.php @@ -43,6 +43,6 @@ public function shouldSample( public function getDescription(): string { - return sprintf('RateLimitingSampler{rate limiting sampling with sampling config of %d req/sec and 0% of additional requests}', $this->limiter->getCapacity()); + return sprintf('RateLimitingSampler{rate limiting sampling with sampling config of %d req/sec and 0%% of additional requests}', $this->limiter->getCapacity()); } } diff --git a/src/Sampler/Xray/src/SamplingRule.php b/src/Sampler/Xray/src/SamplingRule.php index 1d20c8071..9d2cf806e 100644 --- a/src/Sampler/Xray/src/SamplingRule.php +++ b/src/Sampler/Xray/src/SamplingRule.php @@ -17,6 +17,7 @@ class SamplingRule implements \JsonSerializable public string $ServiceName; public string $ServiceType; public string $UrlPath; + /** @psalm-suppress PossiblyUnusedProperty */ public int $Version; public array $Attributes; diff --git a/src/Sampler/Xray/src/SamplingRuleApplier.php b/src/Sampler/Xray/src/SamplingRuleApplier.php index ebbf33df2..1fc9e6bc1 100644 --- a/src/Sampler/Xray/src/SamplingRuleApplier.php +++ b/src/Sampler/Xray/src/SamplingRuleApplier.php @@ -55,13 +55,14 @@ public function matches(AttributesInterface $attributes, ResourceInfo $resource) $httpUrl = $attributes->get(TraceAttributes::HTTP_URL) ?? $attributes->get(TraceAttributes::URL_FULL); // @phan-suppress-current-line PhanDeprecatedClassConstant if ($httpTarget == null && isset($httpUrl)) { $httpTarget = parse_url($httpUrl, PHP_URL_PATH); + $httpTarget = $httpTarget ? $httpTarget : null; } $httpMethod = $attributes->get(TraceAttributes::HTTP_METHOD) ?? $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD); // @phan-suppress-current-line PhanDeprecatedClassConstant if ($httpMethod == '_OTHER') { $httpMethod = $attributes->get(TraceAttributes::HTTP_REQUEST_METHOD_ORIGINAL); } - $httpHost = $attributes->get(TraceAttributes::HTTP_HOST) ?? $attributes->get(TraceAttributes::SERVER_ADDRESS) ; // @phan-suppress-current-line PhanDeprecatedClassConstant + $httpHost = $attributes->get(TraceAttributes::HTTP_HOST) ?? $attributes->get(TraceAttributes::SERVER_ADDRESS); // @phan-suppress-current-line PhanDeprecatedClassConstant $serviceName= $resource->getAttributes()->get(TraceAttributes::SERVICE_NAME) ?? ''; $cloudPlat = $resource->getAttributes()->get(TraceAttributes::CLOUD_PLATFORM) ?? ''; $serviceType= Matcher::getXRayCloudPlatform($cloudPlat); @@ -80,6 +81,7 @@ public function matches(AttributesInterface $attributes, ResourceInfo $resource) && Matcher::wildcardMatch($arn, $this->rule->ResourceArn); } + /** @psalm-suppress ArgumentTypeCoercion */ public function shouldSample( ContextInterface $parentContext, string $traceId, diff --git a/src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php b/src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php index a5811e2b9..4dc6bbcea 100644 --- a/src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php +++ b/src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php @@ -13,6 +13,7 @@ use OpenTelemetry\SDK\Trace\SamplingResult; use PHPUnit\Framework\TestCase; +/** @psalm-suppress UnusedMethodCall */ final class AWSXRayRemoteSamplerTest extends TestCase { public function testShouldSampleUpdatesRulesAndTargets(): void diff --git a/src/Sampler/Xray/tests/Unit/AWSXraySamplerClientTest.php b/src/Sampler/Xray/tests/Unit/AWSXraySamplerClientTest.php index 8148f10be..7e8f9d39e 100644 --- a/src/Sampler/Xray/tests/Unit/AWSXraySamplerClientTest.php +++ b/src/Sampler/Xray/tests/Unit/AWSXraySamplerClientTest.php @@ -4,14 +4,19 @@ use GuzzleHttp\Psr7\Response; use OpenTelemetry\Contrib\Sampler\Xray\AWSXRaySamplerClient; -use OpenTelemetry\Contrib\Sampler\Xray\SamplingRule; use OpenTelemetry\Contrib\Sampler\Xray\SamplingStatisticsDocument; use PHPUnit\Framework\TestCase; +/** + * @psalm-suppress UnusedMethodCall + * @psalm-suppress UndefinedInterfaceMethod + * @psalm-suppress PossiblyInvalidArrayAccess + * @psalm-suppress PossiblyFalseArgument +*/ final class AWSXRaySamplerClientTest extends TestCase { - private string $rulesJson; - private string $targetsJson; + private string|false $rulesJson; + private string|false $targetsJson; protected function setUp(): void { @@ -42,14 +47,12 @@ public function testGetSamplingRules(): void // 4) Assertions: two rules, correct mapping $this->assertCount(2, $rules); - /** @var SamplingRule $r1 */ $r1 = $rules[0]; $this->assertSame('Default', $r1->RuleName); $this->assertSame(0.05, $r1->FixedRate); $this->assertSame(100, $r1->ReservoirSize); $this->assertEquals(['foo'=>'bar','abc'=>'1234'], $r1->Attributes); - /** @var SamplingRule $r2 */ $r2 = $rules[1]; $this->assertSame('test', $r2->RuleName); $this->assertSame(0.11, $r2->FixedRate); diff --git a/src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php b/src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php index 6649da3ac..990c862ad 100644 --- a/src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php +++ b/src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php @@ -13,6 +13,7 @@ use OpenTelemetry\SemConv\TraceAttributes; use PHPUnit\Framework\TestCase; +/** @psalm-suppress UnusedMethodCall */ final class SamplingRuleApplierTest extends TestCase { private Clock $clock; From bd1b9d0a6cce005367ec54daea7600d97d08bd17 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Fri, 11 Jul 2025 11:21:34 -0700 Subject: [PATCH 16/18] added gitkeep file --- src/Sampler/Xray/tests/Integration/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/Sampler/Xray/tests/Integration/.gitkeep diff --git a/src/Sampler/Xray/tests/Integration/.gitkeep b/src/Sampler/Xray/tests/Integration/.gitkeep new file mode 100644 index 000000000..e69de29bb From 038bcbf092f72d4ea02f385ed877af2cccdb8e4e Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Wed, 16 Jul 2025 11:55:10 -0700 Subject: [PATCH 17/18] used otel clock instead of new class --- src/Sampler/Xray/src/AWSXRayRemoteSampler.php | 90 ++++++++++--------- src/Sampler/Xray/src/AWSXRaySamplerClient.php | 1 - src/Sampler/Xray/src/Clock.php | 19 ---- src/Sampler/Xray/src/RulesCache.php | 28 +++--- src/Sampler/Xray/src/SamplingRule.php | 51 +++-------- src/Sampler/Xray/src/SamplingRuleApplier.php | 34 +++---- .../Xray/src/SamplingStatisticsDocument.php | 23 ++--- .../tests/Unit/AWSXRayRemoteSamplerTest.php | 9 +- .../Xray/tests/Unit/RulesCacheTest.php | 9 +- .../tests/Unit/SamplingRuleApplierTest.php | 31 +++---- 10 files changed, 122 insertions(+), 173 deletions(-) delete mode 100644 src/Sampler/Xray/src/Clock.php diff --git a/src/Sampler/Xray/src/AWSXRayRemoteSampler.php b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php index 896152a51..69abfa559 100644 --- a/src/Sampler/Xray/src/AWSXRayRemoteSampler.php +++ b/src/Sampler/Xray/src/AWSXRayRemoteSampler.php @@ -5,8 +5,9 @@ namespace OpenTelemetry\Contrib\Sampler\Xray; -use DateTimeImmutable; use Exception; +use OpenTelemetry\API\Common\Time\Clock; +use OpenTelemetry\API\Common\Time\ClockInterface; use OpenTelemetry\Context\ContextInterface; use OpenTelemetry\SDK\Common\Attribute\AttributesInterface; use OpenTelemetry\SDK\Resource\ResourceInfo; @@ -17,13 +18,32 @@ /** @psalm-suppress UnusedClass */ class AWSXRayRemoteSampler implements SamplerInterface { + // 5 minute default sampling rules polling interval + private const DEFAULT_RULES_POLLING_INTERVAL_SECONDS = 5 * 60; + // Default endpoint for awsproxy : https://aws-otel.github.io/docs/getting-started/remote-sampling#enable-awsproxy-extension + private const DEFAULT_AWS_PROXY_ENDPOINT = 'http://localhost:2000'; + private SamplerInterface $root; + + /** + * @param ResourceInfo $resource + * Must contain attributes like service.name, cloud.platform, etc. + * @param string $host + * X-Ray host, e.g. "xray.us-west-2.amazonaws.com" + * @param int $pollingInterval + * Base interval (seconds) between rule fetches (will be jittered). + */ public function __construct( ResourceInfo $resource, - string $host, - int $rulePollingIntervalMillis = 60 + string $host = self::DEFAULT_AWS_PROXY_ENDPOINT, + int $pollingInterval = self::DEFAULT_RULES_POLLING_INTERVAL_SECONDS ) { - $this->root = new ParentBased(new _AWSXRayRemoteSampler($resource, $host, $rulePollingIntervalMillis)); + // pollingInterval shouldn't be less than 10 seconds + if ($pollingInterval < 10) { + $pollingInterval = self::DEFAULT_RULES_POLLING_INTERVAL_SECONDS; + } + + $this->root = new ParentBased(new _AWSXRayRemoteSampler($resource, $host, $pollingInterval)); } public function shouldSample( @@ -48,24 +68,20 @@ public function getDescription(): string class _AWSXRayRemoteSampler implements SamplerInterface { - // 5 minute default sampling rules polling interval - private const DEFAULT_RULES_POLLING_INTERVAL_SECONDS = 5 * 60; - // Default endpoint for awsproxy : https://aws-otel.github.io/docs/getting-started/remote-sampling#enable-awsproxy-extension - private const DEFAULT_AWS_PROXY_ENDPOINT = 'http://localhost:2000'; - - private Clock $clock; private RulesCache $rulesCache; private FallbackSampler $fallback; private AWSXRaySamplerClient $client; - private int $rulePollingIntervalMillis; + private int $rulePollingIntervalNanos; /** @psalm-suppress UnusedProperty */ - private int $targetPollingIntervalMillis; - private DateTimeImmutable $nextRulesFetchTime; - private DateTimeImmutable $nextTargetFetchTime; + private int $targetPollingIntervalNanos; + + // the times below are in nanoseconds + private int $nextRulesFetchTime; + private int $nextTargetFetchTime; - private int $rulePollingJitterMillis; - private int $targetPollingJitterMillis; + private int $rulePollingJitterNanos; + private int $targetPollingJitterNanos; private string $awsProxyEndpoint; @@ -79,23 +95,21 @@ class _AWSXRayRemoteSampler implements SamplerInterface */ public function __construct( ResourceInfo $resource, - string $awsProxyEndpoint = self::DEFAULT_AWS_PROXY_ENDPOINT, - int $pollingInterval = self::DEFAULT_RULES_POLLING_INTERVAL_SECONDS + string $awsProxyEndpoint, + int $pollingInterval ) { - $this->clock = new Clock(); $this->fallback = new FallbackSampler(); $this->rulesCache = new RulesCache( - $this->clock, bin2hex(random_bytes(12)), $resource, $this->fallback ); - $this->rulePollingIntervalMillis = $pollingInterval * 1000; - $this->rulePollingJitterMillis = rand(1, 5000); + $this->rulePollingIntervalNanos = $pollingInterval * ClockInterface::NANOS_PER_SECOND; + $this->rulePollingJitterNanos = rand(1, 5000) * ClockInterface::NANOS_PER_MILLISECOND; - $this->targetPollingIntervalMillis = $this->rulesCache::DEFAULT_TARGET_INTERVAL_SEC * 1000; - $this->targetPollingJitterMillis = rand(1, 100); + $this->targetPollingIntervalNanos = $this->rulesCache::DEFAULT_TARGET_INTERVAL_SEC * ClockInterface::NANOS_PER_SECOND; + $this->targetPollingJitterNanos = rand(1, 100) * ClockInterface::NANOS_PER_MILLISECOND; $this->awsProxyEndpoint = $awsProxyEndpoint; @@ -110,9 +124,9 @@ public function __construct( } // 2) Schedule next fetch times with jitter - $now = $this->clock->now(); - $this->nextRulesFetchTime = $now->modify('+ ' . ($this->rulePollingJitterMillis + $this->rulePollingIntervalMillis) . ' milliseconds'); - $this->nextTargetFetchTime = $now->modify('+ ' . ($this->targetPollingJitterMillis + $this->targetPollingIntervalMillis) . ' milliseconds'); + $now = Clock::getDefault()->now(); + $this->nextRulesFetchTime = $now + ($this->rulePollingJitterNanos + $this->rulePollingIntervalNanos); + $this->nextTargetFetchTime = $now + ($this->targetPollingJitterNanos + $this->targetPollingIntervalNanos); } /** @@ -126,7 +140,7 @@ public function shouldSample( AttributesInterface $attributes, array $links, ): SamplingResult { - $now = $this->clock->now(); + $now = Clock::getDefault()->now(); // 1) Refresh rules if needed if ($now >= $this->nextRulesFetchTime) { @@ -151,7 +165,7 @@ public function shouldSample( $this->rulesCache->updateTargets($map); if (isset($resp->LastRuleModification) && $resp->LastRuleModification > 0) { - if ($resp->LastRuleModification > $this->rulesCache->getUpdatedAt()->getTimestamp()) { + if (($resp->LastRuleModification * ClockInterface::NANOS_PER_SECOND) > $this->rulesCache->getUpdatedAt()) { $this->getAndUpdateRules($now); } } @@ -161,15 +175,11 @@ public function shouldSample( } $nextTargetFetchTime = $this->rulesCache->nextTargetFetchTime(); - $nextTargetFetchInterval = $nextTargetFetchTime->getTimestamp() - $this->clock->now()->getTimestamp(); + $nextTargetFetchInterval = $nextTargetFetchTime - Clock::getDefault()->now(); if ($nextTargetFetchInterval < 0) { - $nextTargetFetchInterval = $this->rulesCache::DEFAULT_TARGET_INTERVAL_SEC; + $nextTargetFetchInterval = $this->rulesCache::DEFAULT_TARGET_INTERVAL_SEC * ClockInterface::NANOS_PER_SECOND; } - - $nextTargetFetchInterval = $nextTargetFetchInterval * 1000; - - $this->nextTargetFetchTime = $now->modify('+ ' . ($this->targetPollingJitterMillis + $nextTargetFetchInterval) . ' milliseconds'); - + $this->nextTargetFetchTime = $now + ($this->targetPollingJitterNanos + $nextTargetFetchInterval); } // 3) Delegate decision to rulesCache or fallback @@ -182,7 +192,7 @@ public function shouldSample( return $this->rulesCache->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); } - private function getAndUpdateRules(DateTimeImmutable $now) + private function getAndUpdateRules(int $now) { try { $rules = $this->client->getSamplingRules(); @@ -190,15 +200,15 @@ private function getAndUpdateRules(DateTimeImmutable $now) } catch (Exception $e) { // ignore error } - $this->nextRulesFetchTime = $now->modify('+ ' . ($this->rulePollingJitterMillis + $this->rulePollingIntervalMillis) . ' milliseconds'); + $this->nextRulesFetchTime = $now + ($this->rulePollingJitterNanos + $this->rulePollingIntervalNanos); } public function getDescription(): string { return sprintf( - '_AWSXRayRemoteSampler{awsProxyEndpoint=%s,rulePollingIntervalMillis=%ds}', + '_AWSXRayRemoteSampler{awsProxyEndpoint=%s,rulePollingIntervalNanos=%ds}', $this->awsProxyEndpoint, - $this->rulePollingIntervalMillis + $this->rulePollingIntervalNanos ); } } diff --git a/src/Sampler/Xray/src/AWSXRaySamplerClient.php b/src/Sampler/Xray/src/AWSXRaySamplerClient.php index 665b5e1d7..edba28540 100644 --- a/src/Sampler/Xray/src/AWSXRaySamplerClient.php +++ b/src/Sampler/Xray/src/AWSXRaySamplerClient.php @@ -1,7 +1,6 @@ getTimestamp() * 1000; - } -} diff --git a/src/Sampler/Xray/src/RulesCache.php b/src/Sampler/Xray/src/RulesCache.php index 449a47d86..63de5a28b 100644 --- a/src/Sampler/Xray/src/RulesCache.php +++ b/src/Sampler/Xray/src/RulesCache.php @@ -5,6 +5,8 @@ namespace OpenTelemetry\Contrib\Sampler\Xray; +use OpenTelemetry\API\Common\Time\Clock; +use OpenTelemetry\API\Common\Time\ClockInterface; use OpenTelemetry\Context\ContextInterface; use OpenTelemetry\SDK\Common\Attribute\AttributesInterface; use OpenTelemetry\SDK\Resource\ResourceInfo; @@ -13,28 +15,26 @@ class RulesCache implements SamplerInterface { - private const CACHE_TTL = 3600; // 1hr + private const CACHE_TTL = 3600 * ClockInterface::NANOS_PER_SECOND; // 1hr public const DEFAULT_TARGET_INTERVAL_SEC = 10; - private Clock $clock; private string $clientId; private ResourceInfo $resource; private SamplerInterface $fallbackSampler; /** @var SamplingRuleApplier[] */ private array $appliers = []; - private \DateTimeImmutable $updatedAt; + private int $updatedAt; - public function __construct(Clock $clock, string $clientId, ResourceInfo $resource, SamplerInterface $fallback) + public function __construct(string $clientId, ResourceInfo $resource, SamplerInterface $fallback) { - $this->clock = $clock; $this->clientId = $clientId; $this->resource = $resource; $this->fallbackSampler = $fallback; - $this->updatedAt = $clock->now(); + $this->updatedAt = Clock::getDefault()->now(); } public function expired(): bool { - return $this->clock->now()->getTimestamp() > $this->updatedAt->getTimestamp() + self::CACHE_TTL; + return Clock::getDefault()->now() > $this->updatedAt + self::CACHE_TTL; } public function updateRules(array $newRules): void @@ -51,14 +51,14 @@ public function updateRules(array $newRules): void break; } } - $applier = $found ?? new SamplingRuleApplier($this->clientId, $this->clock, $rule); + $applier = $found ?? new SamplingRuleApplier($this->clientId, $rule); // update rule in applier $applier->setRule($rule); $newAppliers[] = $applier; } $this->appliers = $newAppliers; - $this->updatedAt = $this->clock->now(); + $this->updatedAt = Clock::getDefault()->now(); } public function shouldSample( @@ -79,9 +79,9 @@ public function shouldSample( return $this->fallbackSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); } - public function nextTargetFetchTime(): \DateTimeImmutable + public function nextTargetFetchTime(): int { - $defaultPollingTime = $this->clock->now()->add(new \DateInterval('PT' . self::DEFAULT_TARGET_INTERVAL_SEC . 'S')); + $defaultPollingTime = Clock::getDefault()->now() + (self::DEFAULT_TARGET_INTERVAL_SEC * ClockInterface::NANOS_PER_SECOND); if (empty($this->appliers)) { return $defaultPollingTime; @@ -89,7 +89,7 @@ public function nextTargetFetchTime(): \DateTimeImmutable $times = array_map(fn ($a) => $a->getNextSnapshotTime(), $this->appliers); $min = min($times); - return $min < $this->clock->now() + return $min < Clock::getDefault()->now() ? $defaultPollingTime : $min; } @@ -101,7 +101,7 @@ public function updateTargets(array $targets): void foreach ($this->appliers as $applier) { $name = $applier->getRuleName(); if (isset($targets[$name])) { - $new[] = $applier->withTarget($targets[$name], $this->clock->now()); + $new[] = $applier->withTarget($targets[$name], Clock::getDefault()->now()); } else { $new[] = $applier; } @@ -119,7 +119,7 @@ public function getDescription(): string return 'RulesCache'; } - public function getUpdatedAt(): \DateTimeImmutable + public function getUpdatedAt(): int { return $this->updatedAt; } diff --git a/src/Sampler/Xray/src/SamplingRule.php b/src/Sampler/Xray/src/SamplingRule.php index 9d2cf806e..6d4250b56 100644 --- a/src/Sampler/Xray/src/SamplingRule.php +++ b/src/Sampler/Xray/src/SamplingRule.php @@ -5,48 +5,23 @@ namespace OpenTelemetry\Contrib\Sampler\Xray; +/** @psalm-suppress PossiblyUnusedProperty */ class SamplingRule implements \JsonSerializable { - public string $RuleName; - public int $Priority; - public float $FixedRate; - public int $ReservoirSize; - public string $Host; - public string $HttpMethod; - public string $ResourceArn; - public string $ServiceName; - public string $ServiceType; - public string $UrlPath; - /** @psalm-suppress PossiblyUnusedProperty */ - public int $Version; - public array $Attributes; - public function __construct( - string $ruleName, - int $priority, - float $fixedRate, - int $reservoirSize, - string $host, - string $httpMethod, - string $resourceArn, - string $serviceName, - string $serviceType, - string $urlPath, - int $version, - array $attributes = [] + public string $RuleName, + public int $Priority, + public float $FixedRate, + public int $ReservoirSize, + public string $Host, + public string $HttpMethod, + public string $ResourceArn, + public string $ServiceName, + public string $ServiceType, + public string $UrlPath, + public int $Version, + public array $Attributes, ) { - $this->RuleName = $ruleName; - $this->Priority = $priority; - $this->FixedRate = $fixedRate; - $this->ReservoirSize= $reservoirSize; - $this->Host = $host; - $this->HttpMethod = $httpMethod; - $this->ResourceArn = $resourceArn; - $this->ServiceName = $serviceName; - $this->ServiceType = $serviceType; - $this->UrlPath = $urlPath; - $this->Version = $version; - $this->Attributes = $attributes; } public function jsonSerialize(): array diff --git a/src/Sampler/Xray/src/SamplingRuleApplier.php b/src/Sampler/Xray/src/SamplingRuleApplier.php index 1fc9e6bc1..f26fe3acf 100644 --- a/src/Sampler/Xray/src/SamplingRuleApplier.php +++ b/src/Sampler/Xray/src/SamplingRuleApplier.php @@ -5,6 +5,8 @@ namespace OpenTelemetry\Contrib\Sampler\Xray; +use OpenTelemetry\API\Common\Time\Clock; +use OpenTelemetry\API\Common\Time\ClockInterface; use OpenTelemetry\Context\ContextInterface; use OpenTelemetry\SDK\Common\Attribute\AttributesInterface; use OpenTelemetry\SDK\Resource\ResourceInfo; @@ -18,19 +20,17 @@ class SamplingRuleApplier { private string $clientId; private SamplingRule $rule; - private Clock $clock; private SamplerInterface $reservoirSampler; private SamplerInterface $fixedRateSampler; private bool $borrowing; private Statistics $statistics; - private \DateTimeImmutable $reservoirEndTime; - private \DateTimeImmutable $nextSnapshotTime; + private int $reservoirEndTime; + private int $nextSnapshotTime; private string $ruleName; - public function __construct(string $clientId, Clock $clock, SamplingRule $rule, ?Statistics $stats = null) + public function __construct(string $clientId, SamplingRule $rule, ?Statistics $stats = null) { $this->clientId = $clientId; - $this->clock = $clock; $this->rule = $rule; $this->ruleName = $rule->RuleName; $this->statistics = $stats ?? new Statistics(); @@ -44,8 +44,8 @@ public function __construct(string $clientId, Clock $clock, SamplingRule $rule, } $this->fixedRateSampler = new TraceIdRatioBasedSampler($rule->FixedRate); - $this->reservoirEndTime = new \DateTimeImmutable('@' . PHP_INT_MAX); - $this->nextSnapshotTime = $clock->now(); + $this->reservoirEndTime = PHP_INT_MAX; + $this->nextSnapshotTime = Clock::getDefault()->now(); } public function matches(AttributesInterface $attributes, ResourceInfo $resource): bool @@ -91,7 +91,7 @@ public function shouldSample( array $links, ): SamplingResult { $this->statistics->requestCount++; - $now = $this->clock->now(); + $now = Clock::getDefault()->now(); if ($now < $this->reservoirEndTime) { $res = $this->reservoirSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); ; @@ -104,8 +104,8 @@ public function shouldSample( return $res; } } + $res = $this->fixedRateSampler->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); - ; if ($res->getDecision() !== SamplingResult::DROP) { $this->statistics->sampleCount++; } @@ -113,9 +113,9 @@ public function shouldSample( return $res; } - public function snapshot(\DateTimeImmutable $now): SamplingStatisticsDocument + public function snapshot(int $now): SamplingStatisticsDocument { - $ts = $this->clock->toUnixMillis($now); + $ts = intdiv($now, ClockInterface::NANOS_PER_MILLISECOND); $req = $this->statistics->requestCount; $smp = $this->statistics->sampleCount; $brw = $this->statistics->borrowCount; @@ -139,9 +139,9 @@ public function snapshot(\DateTimeImmutable $now): SamplingStatisticsDocument * returning a new applier with updated reservoir & fixed-rate samplers. * * @param object $targetDoc stdClass from AWS SDK getSamplingTargets() - * @param \DateTimeImmutable $now “now” timestamp for computing next snapshot + * @param int $now “now” timestamp for computing next snapshot */ - public function withTarget(object $targetDoc, \DateTimeImmutable $now): self + public function withTarget(object $targetDoc, int $now): self { // 1) Determine new fixed-rate sampler if (isset($targetDoc->FixedRate)) { @@ -151,14 +151,14 @@ public function withTarget(object $targetDoc, \DateTimeImmutable $now): self } // 2) Determine new reservoir sampler & end time - $newReservoirEndTime = new \DateTimeImmutable('9999-12-31T23:59:59+00:00'); + $newReservoirEndTime = PHP_INT_MAX; if (isset($targetDoc->ReservoirQuota, $targetDoc->ReservoirQuotaTTL)) { $quota = (int) floor($targetDoc->ReservoirQuota); $ttlSeconds = (int) floor($targetDoc->ReservoirQuotaTTL); $newReservoirSampler = $quota > 0 ? new RateLimitingSampler($quota) : new AlwaysOffSampler(); - $newReservoirEndTime = \DateTimeImmutable::createFromFormat('U', (string) $ttlSeconds) + $newReservoirEndTime = $ttlSeconds * ClockInterface::NANOS_PER_SECOND ?: $newReservoirEndTime; } else { // if no quota provided, turn off reservoir @@ -169,7 +169,7 @@ public function withTarget(object $targetDoc, \DateTimeImmutable $now): self $intervalSec = isset($targetDoc->Interval) ? (int) $targetDoc->Interval : 10; - $newNextSnapshotTime = $now->add(new \DateInterval("PT{$intervalSec}S")); + $newNextSnapshotTime = $now + ($intervalSec * ClockInterface::NANOS_PER_SECOND); // 4) Clone & patch $clone = clone $this; @@ -182,7 +182,7 @@ public function withTarget(object $targetDoc, \DateTimeImmutable $now): self return $clone; } - public function getNextSnapshotTime(): \DateTimeImmutable + public function getNextSnapshotTime(): int { return $this->nextSnapshotTime; } diff --git a/src/Sampler/Xray/src/SamplingStatisticsDocument.php b/src/Sampler/Xray/src/SamplingStatisticsDocument.php index a9b0bc44e..0d1b177f6 100644 --- a/src/Sampler/Xray/src/SamplingStatisticsDocument.php +++ b/src/Sampler/Xray/src/SamplingStatisticsDocument.php @@ -7,20 +7,13 @@ class SamplingStatisticsDocument { - public string $ClientID; - public string $RuleName; - public int $RequestCount; - public int $SampleCount; - public int $BorrowCount; - public float $Timestamp; - - public function __construct(string $clientId, string $ruleName, int $req, int $samp, int $borrow, float $ts) - { - $this->ClientID = $clientId; - $this->RuleName = $ruleName; - $this->RequestCount = $req; - $this->SampleCount = $samp; - $this->BorrowCount = $borrow; - $this->Timestamp = $ts; + public function __construct( + public string $ClientID, + public string $RuleName, + public int $RequestCount, + public int $SampleCount, + public int $BorrowCount, + public float $Timestamp, + ) { } } diff --git a/src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php b/src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php index 4dc6bbcea..0e452bb85 100644 --- a/src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php +++ b/src/Sampler/Xray/tests/Unit/AWSXRayRemoteSamplerTest.php @@ -2,10 +2,11 @@ declare(strict_types=1); +use OpenTelemetry\API\Common\Time\Clock; +use OpenTelemetry\API\Common\Time\ClockInterface; use OpenTelemetry\Context\ContextInterface; use OpenTelemetry\Contrib\Sampler\Xray\_AWSXRayRemoteSampler; use OpenTelemetry\Contrib\Sampler\Xray\AWSXRaySamplerClient; -use OpenTelemetry\Contrib\Sampler\Xray\Clock; use OpenTelemetry\Contrib\Sampler\Xray\FallbackSampler; use OpenTelemetry\Contrib\Sampler\Xray\RulesCache; use OpenTelemetry\SDK\Common\Attribute\Attributes; @@ -66,11 +67,11 @@ public function testShouldSampleUpdatesRulesAndTargets(): void $ref->getProperty('fallback')->setValue($sampler, $this->createMock(FallbackSampler::class)); // 4) Force fetch times into the past so updates run - $now = (new Clock())->now(); + $now = Clock::getDefault()->now(); $ref->getProperty('nextRulesFetchTime')->setAccessible(true); - $ref->getProperty('nextRulesFetchTime')->setValue($sampler, $now->sub(new DateInterval('PT1S'))); + $ref->getProperty('nextRulesFetchTime')->setValue($sampler, $now - (1 * ClockInterface::NANOS_PER_SECOND)); $ref->getProperty('nextTargetFetchTime')->setAccessible(true); - $ref->getProperty('nextTargetFetchTime')->setValue($sampler, $now->sub(new DateInterval('PT1S'))); + $ref->getProperty('nextTargetFetchTime')->setValue($sampler, $now - (1 * ClockInterface::NANOS_PER_SECOND)); // 5) Call shouldSample $result = $sampler->shouldSample( diff --git a/src/Sampler/Xray/tests/Unit/RulesCacheTest.php b/src/Sampler/Xray/tests/Unit/RulesCacheTest.php index 8da3a43a8..7131d93fe 100644 --- a/src/Sampler/Xray/tests/Unit/RulesCacheTest.php +++ b/src/Sampler/Xray/tests/Unit/RulesCacheTest.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use OpenTelemetry\Contrib\Sampler\Xray\Clock; use OpenTelemetry\Contrib\Sampler\Xray\RulesCache; use OpenTelemetry\Contrib\Sampler\Xray\SamplingRule; use OpenTelemetry\SDK\Common\Attribute\Attributes; @@ -12,12 +11,10 @@ final class RulesCacheTest extends TestCase { - private Clock $clock; private ResourceInfo $resource; protected function setUp(): void { - $this->clock = new Clock(); $this->resource = ResourceInfo::create(Attributes::create([ 'service.name' => 'test-service', 'cloud.platform' => 'aws_ecs', @@ -27,7 +24,7 @@ protected function setUp(): void public function testUpdateRulesSortsByPriorityThenName(): void { $fallback = new AlwaysOffSampler(); - $cache = new RulesCache($this->clock, 'client', $this->resource, $fallback); + $cache = new RulesCache('client', $this->resource, $fallback); $rule1 = new SamplingRule('b', 2, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); $rule2 = new SamplingRule('a', 1, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); @@ -48,7 +45,7 @@ public function testUpdateRulesSortsByPriorityThenName(): void public function testUpdateRulesReusesExistingAppliers(): void { $fallback = new AlwaysOffSampler(); - $cache = new RulesCache($this->clock, 'client', $this->resource, $fallback); + $cache = new RulesCache('client', $this->resource, $fallback); $ruleA1 = new SamplingRule('ruleA', 1, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); $ruleB = new SamplingRule('ruleB', 1, 0.1, 0, '*', '*', '*', '*', '*', '*', 1, []); @@ -83,7 +80,7 @@ public function testUpdateRulesReusesExistingAppliers(): void public function testUpdateTargetsClonesMatchingAppliers(): void { $fallback = new AlwaysOffSampler(); - $cache = new RulesCache($this->clock, 'client', $this->resource, $fallback); + $cache = new RulesCache('client', $this->resource, $fallback); $ruleA = new SamplingRule('ruleA', 1, 0.1, 5, '*', '*', '*', '*', '*', '*', 1, []); $ruleB = new SamplingRule('ruleB', 1, 0.1, 5, '*', '*', '*', '*', '*', '*', 1, []); diff --git a/src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php b/src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php index 990c862ad..6bbfce6d9 100644 --- a/src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php +++ b/src/Sampler/Xray/tests/Unit/SamplingRuleApplierTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); +use OpenTelemetry\API\Common\Time\Clock; use OpenTelemetry\Context\ContextInterface; -use OpenTelemetry\Contrib\Sampler\Xray\Clock; use OpenTelemetry\Contrib\Sampler\Xray\SamplingRule; use OpenTelemetry\Contrib\Sampler\Xray\SamplingRuleApplier; use OpenTelemetry\SDK\Common\Attribute\Attributes; @@ -16,13 +16,6 @@ /** @psalm-suppress UnusedMethodCall */ final class SamplingRuleApplierTest extends TestCase { - private Clock $clock; - - protected function setUp(): void - { - $this->clock = new Clock(); - } - public function testWildcardRuleMatchesAnyAttributesOldSemanticConventions(): void { // Rule with all wildcards and no specific attributes @@ -40,7 +33,7 @@ public function testWildcardRuleMatchesAnyAttributesOldSemanticConventions(): vo 1, [] ); - $applier = new SamplingRuleApplier('client', $this->clock, $rule); + $applier = new SamplingRuleApplier('client', $rule); // Attributes that should all match '*' $attrs = Attributes::create([ @@ -76,7 +69,7 @@ public function testSpecificRuleMatchesExactAttributesOldSemanticConventions(): 1, ['env' => 'prod'] ); - $applier = new SamplingRuleApplier('client', $this->clock, $rule); + $applier = new SamplingRuleApplier('client', $rule); // Matching attributes $attrs = Attributes::create([ @@ -114,7 +107,7 @@ public function testWildcardRuleMatchesAnyAttributesNewSemanticConventions(): vo 1, [] ); - $applier = new SamplingRuleApplier('client', $this->clock, $rule); + $applier = new SamplingRuleApplier('client', $rule); // Attributes that should all match '*' $attrs = Attributes::create([ @@ -150,7 +143,7 @@ public function testSpecificRuleMatchesExactAttributesNewSemanticConventions(): 1, ['env' => 'prod'] ); - $applier = new SamplingRuleApplier('client', $this->clock, $rule); + $applier = new SamplingRuleApplier('client', $rule); // Matching attributes $attrs = Attributes::create([ @@ -188,7 +181,7 @@ public function testRuleDoesNotMatchWhenOneAttributeDiffers(): void 1, ['env' => 'prod'] ); - $applier = new SamplingRuleApplier('client', $this->clock, $rule); + $applier = new SamplingRuleApplier('client', $rule); // Attributes with wrong HTTP method $attrs = Attributes::create([ @@ -212,7 +205,7 @@ public function testRuleDoesNotMatchWhenOneAttributeDiffers(): void public function testShouldSample_incrementsStatistics_andHonorsReservoirSamplerDecision(): void { $rule = new SamplingRule('r', 1, 0.0, 1, '*', '*', '*', '*', '*', '*', 1, []); - $applier = new SamplingRuleApplier('c', new Clock(), $rule, null); + $applier = new SamplingRuleApplier('c', $rule, null); // Mock reservoirSampler to RECORD $reservoirMock = $this->createMock(SamplerInterface::class); @@ -244,7 +237,7 @@ public function testShouldSample_incrementsStatistics_andHonorsReservoirSamplerD $this->assertSame(SamplingResult::RECORD_AND_SAMPLE, $result->getDecision()); // Snapshot statistics - $now = new \DateTimeImmutable(); + $now = Clock::getDefault()->now(); $statsDoc = $applier->snapshot($now); $this->assertSame(1, $statsDoc->RequestCount); @@ -255,7 +248,7 @@ public function testShouldSample_incrementsStatistics_andHonorsReservoirSamplerD public function testShouldSample_onReservoirDrop_usesFixedRateSampler_andIncrementsSampleCountOnly(): void { $rule = new SamplingRule('r2', 1, 1.0, 0, '*', '*', '*', '*', '*', '*', 1, []); - $applier = new SamplingRuleApplier('c2', new Clock(), $rule, null); + $applier = new SamplingRuleApplier('c2', $rule, null); // reservoirSampler: always DROP $reservoirMock = $this->createMock(SamplerInterface::class); @@ -280,7 +273,7 @@ public function testShouldSample_onReservoirDrop_usesFixedRateSampler_andIncreme $result = $applier->shouldSample($context, 't2', 's2', 0, $attributes, []); $this->assertSame(SamplingResult::RECORD_AND_SAMPLE, $result->getDecision()); - $now = new \DateTimeImmutable(); + $now = Clock::getDefault()->now(); $statsDoc = $applier->snapshot($now); $this->assertSame(1, $statsDoc->RequestCount); $this->assertSame(1, $statsDoc->SampleCount); @@ -290,7 +283,7 @@ public function testShouldSample_onReservoirDrop_usesFixedRateSampler_andIncreme public function testSnapshot_resetsStatisticsAfterCapture(): void { $rule = new SamplingRule('r3', 1, 0.0, 1, '*', '*', '*', '*', '*', '*', 1, []); - $applier = new SamplingRuleApplier('c3', new Clock(), $rule, null); + $applier = new SamplingRuleApplier('c3', $rule, null); // simulate stats by reflection $refStats = new \ReflectionProperty($applier, 'statistics'); @@ -300,7 +293,7 @@ public function testSnapshot_resetsStatisticsAfterCapture(): void $stats->sampleCount = 2; $stats->borrowCount = 1; - $now = new \DateTimeImmutable(); + $now = Clock::getDefault()->now(); $doc1 = $applier->snapshot($now); $this->assertSame(5, $doc1->RequestCount); $this->assertSame(2, $doc1->SampleCount); From b2225071235dd7d83a5302ebe03b0fa114d85d6f Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Tue, 26 Aug 2025 09:57:42 -0700 Subject: [PATCH 18/18] added entry to gitsplit --- .gitsplit.yml | 2 ++ src/Sampler/Xray/README.md | 2 +- src/Sampler/Xray/composer.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitsplit.yml b/.gitsplit.yml index b357f4137..24ee46352 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -86,6 +86,8 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-detector-digitalocean.git" - prefix: "src/Sampler/RuleBased" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-sampler-rulebased.git" + - prefix: "src/Sampler/Xray" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-sampler-aws-xray.git" - prefix: "src/Shims/OpenTracing" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-shim-opentracing.git" - prefix: "src/Utils/Test" diff --git a/src/Sampler/Xray/README.md b/src/Sampler/Xray/README.md index 4438f4124..0d83091af 100644 --- a/src/Sampler/Xray/README.md +++ b/src/Sampler/Xray/README.md @@ -5,7 +5,7 @@ Provides a sampler which can get sampling configurations from AWS X-Ray to make ## Installation ```shell -composer require open-telemetry/contrib-aws-xray-sampler +composer require open-telemetry/sampler-aws-xray ``` ## Configuration diff --git a/src/Sampler/Xray/composer.json b/src/Sampler/Xray/composer.json index f93780f73..c5a80a7c8 100644 --- a/src/Sampler/Xray/composer.json +++ b/src/Sampler/Xray/composer.json @@ -1,5 +1,5 @@ { - "name": "open-telemetry/contrib-aws-xray-sampler", + "name": "open-telemetry/sampler-aws-xray", "description": "AWS X-Ray Remote Sampler for OpenTelemetry PHP Contrib", "type": "library", "license": "Apache-2.0",