From e5a13ce32f6841da88358a7f6b8f911e7d0c5218 Mon Sep 17 00:00:00 2001 From: Brett McBride Date: Fri, 13 Sep 2024 09:58:36 +1000 Subject: [PATCH] implement rule-based sampler (#279) * initial port from Nevay/otel-sdk-contrib-sampler * rename key to contrib_rule_based to avoid future collision with an official rule-based sampler, change the name to something less likely to conflict. --- .github/workflows/php.yml | 5 + .gitsplit.yml | 2 + src/Sampler/RuleBased/.gitattributes | 13 + src/Sampler/RuleBased/.gitignore | 1 + src/Sampler/RuleBased/.phan/config.php | 370 ++++++++++++++++++ src/Sampler/RuleBased/.php-cs-fixer.php | 43 ++ src/Sampler/RuleBased/README.md | 87 ++++ src/Sampler/RuleBased/composer.json | 50 +++ src/Sampler/RuleBased/phpstan.neon.dist | 14 + src/Sampler/RuleBased/phpunit.xml.dist | 22 ++ src/Sampler/RuleBased/psalm.xml.dist | 34 ++ .../ComponentProvider/SamplerRuleBased.php | 71 ++++ .../SamplingRuleAttribute.php | 55 +++ .../ComponentProvider/SamplingRuleLink.php | 46 +++ .../ComponentProvider/SamplingRuleParent.php | 46 +++ .../SamplingRuleSpanKind.php | 59 +++ .../SamplingRuleSpanName.php | 48 +++ .../RuleBased/src/RuleBasedSampler.php | 59 +++ src/Sampler/RuleBased/src/RuleSet.php | 39 ++ .../RuleBased/src/RuleSetInterface.php | 20 + src/Sampler/RuleBased/src/SamplingRule.php | 38 ++ .../src/SamplingRule/AttributeRule.php | 44 +++ .../RuleBased/src/SamplingRule/LinkRule.php | 52 +++ .../RuleBased/src/SamplingRule/ParentRule.php | 45 +++ .../src/SamplingRule/SpanKindRule.php | 38 ++ .../src/SamplingRule/SpanNameRule.php | 38 ++ .../Integration/RuleBasedSamplerTest.php | 26 ++ .../tests/Integration/config/sdk-config.yaml | 25 ++ .../tests/Unit/RuleBasedSamplerTest.php | 99 +++++ .../RuleBased/tests/Unit/RuleSetTest.php | 49 +++ .../Unit/SamplingRule/AttributeRuleTest.php | 50 +++ .../tests/Unit/SamplingRule/LinkRuleTest.php | 68 ++++ .../Unit/SamplingRule/ParentRuleTest.php | 71 ++++ .../Unit/SamplingRule/SpanKindRuleTest.php | 39 ++ .../Unit/SamplingRule/SpanNameRuleTest.php | 44 +++ 35 files changed, 1810 insertions(+) create mode 100644 src/Sampler/RuleBased/.gitattributes create mode 100644 src/Sampler/RuleBased/.gitignore create mode 100644 src/Sampler/RuleBased/.phan/config.php create mode 100644 src/Sampler/RuleBased/.php-cs-fixer.php create mode 100644 src/Sampler/RuleBased/README.md create mode 100644 src/Sampler/RuleBased/composer.json create mode 100644 src/Sampler/RuleBased/phpstan.neon.dist create mode 100644 src/Sampler/RuleBased/phpunit.xml.dist create mode 100644 src/Sampler/RuleBased/psalm.xml.dist create mode 100644 src/Sampler/RuleBased/src/ComponentProvider/SamplerRuleBased.php create mode 100644 src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleAttribute.php create mode 100644 src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleLink.php create mode 100644 src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleParent.php create mode 100644 src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleSpanKind.php create mode 100644 src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleSpanName.php create mode 100644 src/Sampler/RuleBased/src/RuleBasedSampler.php create mode 100644 src/Sampler/RuleBased/src/RuleSet.php create mode 100644 src/Sampler/RuleBased/src/RuleSetInterface.php create mode 100644 src/Sampler/RuleBased/src/SamplingRule.php create mode 100644 src/Sampler/RuleBased/src/SamplingRule/AttributeRule.php create mode 100644 src/Sampler/RuleBased/src/SamplingRule/LinkRule.php create mode 100644 src/Sampler/RuleBased/src/SamplingRule/ParentRule.php create mode 100644 src/Sampler/RuleBased/src/SamplingRule/SpanKindRule.php create mode 100644 src/Sampler/RuleBased/src/SamplingRule/SpanNameRule.php create mode 100644 src/Sampler/RuleBased/tests/Integration/RuleBasedSamplerTest.php create mode 100644 src/Sampler/RuleBased/tests/Integration/config/sdk-config.yaml create mode 100644 src/Sampler/RuleBased/tests/Unit/RuleBasedSamplerTest.php create mode 100644 src/Sampler/RuleBased/tests/Unit/RuleSetTest.php create mode 100644 src/Sampler/RuleBased/tests/Unit/SamplingRule/AttributeRuleTest.php create mode 100644 src/Sampler/RuleBased/tests/Unit/SamplingRule/LinkRuleTest.php create mode 100644 src/Sampler/RuleBased/tests/Unit/SamplingRule/ParentRuleTest.php create mode 100644 src/Sampler/RuleBased/tests/Unit/SamplingRule/SpanKindRuleTest.php create mode 100644 src/Sampler/RuleBased/tests/Unit/SamplingRule/SpanNameRuleTest.php diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 57fba9e6..aaa66b31 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -47,6 +47,7 @@ jobs: 'Propagation/TraceResponse', 'ResourceDetectors/Azure', 'ResourceDetectors/Container', + 'Sampler/RuleBased', 'Shims/OpenTracing', 'Symfony' ] @@ -111,6 +112,10 @@ jobs: php-version: 7.4 - project: 'ResourceDetectors/Container' php-version: 7.4 + - project: 'Sampler/RuleBased' + php-version: 7.4 + - project: 'Sampler/RuleBased' + php-version: 8.0 steps: - uses: actions/checkout@v4 diff --git a/.gitsplit.yml b/.gitsplit.yml index 3edb52b7..db0a0dd0 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -68,6 +68,8 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-detector-azure.git" - prefix: "src/ResourceDetectors/Container" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-detector-container.git" + - prefix: "src/Sampler/RuleBased" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-sampler-rulebased.git" - prefix: "src/Shims/OpenTracing" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-shim-opentracing.git" # List of references to split (defined as regexp) diff --git a/src/Sampler/RuleBased/.gitattributes b/src/Sampler/RuleBased/.gitattributes new file mode 100644 index 00000000..ac40e9f8 --- /dev/null +++ b/src/Sampler/RuleBased/.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/RuleBased/.gitignore b/src/Sampler/RuleBased/.gitignore new file mode 100644 index 00000000..723504e3 --- /dev/null +++ b/src/Sampler/RuleBased/.gitignore @@ -0,0 +1 @@ +.phpunit.cache diff --git a/src/Sampler/RuleBased/.phan/config.php b/src/Sampler/RuleBased/.phan/config.php new file mode 100644 index 00000000..d3c9f491 --- /dev/null +++ b/src/Sampler/RuleBased/.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/RuleBased/.php-cs-fixer.php b/src/Sampler/RuleBased/.php-cs-fixer.php new file mode 100644 index 00000000..e35fa078 --- /dev/null +++ b/src/Sampler/RuleBased/.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/RuleBased/README.md b/src/Sampler/RuleBased/README.md new file mode 100644 index 00000000..ccde5d7b --- /dev/null +++ b/src/Sampler/RuleBased/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/RuleBased/composer.json b/src/Sampler/RuleBased/composer.json new file mode 100644 index 00000000..51060165 --- /dev/null +++ b/src/Sampler/RuleBased/composer.json @@ -0,0 +1,50 @@ +{ + "name": "open-telemetry/sampler-rule-based", + "description": "OpenTelemetry SDK rule-based sampler", + "keywords": ["opentelemetry", "otel", "sdk", "tracing", "sampler"], + "license": "Apache-2.0", + "require": { + "php": "^8.1", + "open-telemetry/api": "dev-main as 1.1.0", + "open-telemetry/sdk": "dev-main as 1.1.0", + "open-telemetry/sdk-configuration": "dev-main as 0.99" + }, + "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.18.4", + "phpunit/phpunit": "^10 || ^11", + "vimeo/psalm": "^4|^5" + }, + "autoload": { + "psr-4": { + "OpenTelemetry\\Contrib\\Sampler\\RuleBased\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + }, + "spi": { + "OpenTelemetry\\Config\\SDK\\Configuration\\ComponentProvider": [ + "OpenTelemetry\\Contrib\\Sampler\\RuleBased\\ComponentProvider\\SamplerRuleBased", + + "OpenTelemetry\\Contrib\\Sampler\\RuleBased\\ComponentProvider\\SamplingRuleAttribute", + "OpenTelemetry\\Contrib\\Sampler\\RuleBased\\ComponentProvider\\SamplingRuleLink", + "OpenTelemetry\\Contrib\\Sampler\\RuleBased\\ComponentProvider\\SamplingRuleParent", + "OpenTelemetry\\Contrib\\Sampler\\RuleBased\\ComponentProvider\\SamplingRuleSpanKind", + "OpenTelemetry\\Contrib\\Sampler\\RuleBased\\ComponentProvider\\SamplingRuleSpanName" + ] + } + }, + "config": { + "allow-plugins": { + "php-http/discovery": false, + "tbachert/spi": true + } + } +} diff --git a/src/Sampler/RuleBased/phpstan.neon.dist b/src/Sampler/RuleBased/phpstan.neon.dist new file mode 100644 index 00000000..f9cb5fac --- /dev/null +++ b/src/Sampler/RuleBased/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/RuleBased/phpunit.xml.dist b/src/Sampler/RuleBased/phpunit.xml.dist new file mode 100644 index 00000000..aebce8c3 --- /dev/null +++ b/src/Sampler/RuleBased/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + + + + + + + tests/Unit + + + tests/Integration + + + + + src + + + diff --git a/src/Sampler/RuleBased/psalm.xml.dist b/src/Sampler/RuleBased/psalm.xml.dist new file mode 100644 index 00000000..995b2a65 --- /dev/null +++ b/src/Sampler/RuleBased/psalm.xml.dist @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sampler/RuleBased/src/ComponentProvider/SamplerRuleBased.php b/src/Sampler/RuleBased/src/ComponentProvider/SamplerRuleBased.php new file mode 100644 index 00000000..0a714940 --- /dev/null +++ b/src/Sampler/RuleBased/src/ComponentProvider/SamplerRuleBased.php @@ -0,0 +1,71 @@ + + */ +final class SamplerRuleBased implements ComponentProvider +{ + /** + * @param array{ + * rule_sets: list>, + * delegate: ComponentPlugin, + * }>, + * fallback: ComponentPlugin, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SamplerInterface + { + $ruleSets = []; + foreach ($properties['rule_sets'] as $ruleSet) { + $samplingRules = []; + foreach ($ruleSet['rules'] as $rule) { + $samplingRules[] = $rule->create($context); + } + + $ruleSets[] = new RuleSet( + $samplingRules, + $ruleSet['delegate']->create($context), + ); + } + + return new RuleBasedSampler( + $ruleSets, + $properties['fallback']->create($context), + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('contrib_rule_based'); + $node + ->children() + ->arrayNode('rule_sets') + ->arrayPrototype() + ->children() + ->append($registry->componentArrayList('rules', SamplingRule::class)->isRequired()->cannotBeEmpty()) + ->append($registry->component('delegate', SamplerInterface::class)->isRequired()) + ->end() + ->end() + ->end() + ->append($registry->component('fallback', SamplerInterface::class)->isRequired()) + ->end() + ; + + return $node; + } +} diff --git a/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleAttribute.php b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleAttribute.php new file mode 100644 index 00000000..6c67736b --- /dev/null +++ b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleAttribute.php @@ -0,0 +1,55 @@ + + */ +final class SamplingRuleAttribute implements ComponentProvider +{ + + /** + * @param array{ + * key: string, + * pattern: string, + * } $properties + * @psalm-suppress ArgumentTypeCoercion + */ + public function createPlugin(array $properties, Context $context): SamplingRule + { + return new AttributeRule( + $properties['key'], + $properties['pattern'], + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('attribute'); + $node + ->children() + ->scalarNode('key') + ->isRequired() + ->cannotBeEmpty() + ->validate()->always(Validation::ensureString())->end() + ->end() + ->scalarNode('pattern') + ->isRequired() + ->validate()->always(Validation::ensureRegexPattern())->end() + ->end() + ->end() + ; + + return $node; + } +} diff --git a/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleLink.php b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleLink.php new file mode 100644 index 00000000..17cc5511 --- /dev/null +++ b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleLink.php @@ -0,0 +1,46 @@ + + */ +final class SamplingRuleLink implements ComponentProvider +{ + + /** + * @param array{ + * sampled: bool, + * remote: ?bool, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SamplingRule + { + return new LinkRule( + $properties['sampled'], + $properties['remote'], + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('link'); + $node + ->children() + ->booleanNode('sampled')->isRequired()->end() + ->booleanNode('remote')->defaultNull()->end() + ->end() + ; + + return $node; + } +} diff --git a/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleParent.php b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleParent.php new file mode 100644 index 00000000..af818520 --- /dev/null +++ b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleParent.php @@ -0,0 +1,46 @@ + + */ +final class SamplingRuleParent implements ComponentProvider +{ + + /** + * @param array{ + * sampled: bool, + * remote: ?bool, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SamplingRule + { + return new ParentRule( + $properties['sampled'], + $properties['remote'], + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('parent'); + $node + ->children() + ->booleanNode('sampled')->isRequired()->end() + ->booleanNode('remote')->defaultNull()->end() + ->end() + ; + + return $node; + } +} diff --git a/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleSpanKind.php b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleSpanKind.php new file mode 100644 index 00000000..429bcc21 --- /dev/null +++ b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleSpanKind.php @@ -0,0 +1,59 @@ + + */ +final class SamplingRuleSpanKind implements ComponentProvider +{ + + /** + * @param array{ + * kind: 'INTERNAL'|'CLIENT'|'SERVER'|'PRODUCER'|'CONSUMER', + * } $properties + */ + public function createPlugin(array $properties, Context $context): SamplingRule + { + return new SpanKindRule( + match ($properties['kind']) { + 'INTERNAL' => SpanKind::KIND_INTERNAL, + 'CLIENT' => SpanKind::KIND_CLIENT, + 'SERVER' => SpanKind::KIND_SERVER, + 'PRODUCER' => SpanKind::KIND_PRODUCER, + 'CONSUMER' => SpanKind::KIND_CONSUMER, + }, + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('span_kind'); + $node + ->children() + ->enumNode('kind') + ->isRequired() + ->values([ + 'INTERNAL', + 'CLIENT', + 'SERVER', + 'PRODUCER', + 'CONSUMER', + ]) + ->end() + ->end() + ; + + return $node; + } +} diff --git a/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleSpanName.php b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleSpanName.php new file mode 100644 index 00000000..10c49633 --- /dev/null +++ b/src/Sampler/RuleBased/src/ComponentProvider/SamplingRuleSpanName.php @@ -0,0 +1,48 @@ + + */ +final class SamplingRuleSpanName implements ComponentProvider +{ + + /** + * @param array{ + * pattern: string, + * } $properties + * @psalm-suppress ArgumentTypeCoercion + */ + public function createPlugin(array $properties, Context $context): SamplingRule + { + return new SpanNameRule( + $properties['pattern'], + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('span_name'); + $node + ->children() + ->scalarNode('pattern') + ->isRequired() + ->validate()->always(Validation::ensureRegexPattern())->end() + ->end() + ->end() + ; + + return $node; + } +} diff --git a/src/Sampler/RuleBased/src/RuleBasedSampler.php b/src/Sampler/RuleBased/src/RuleBasedSampler.php new file mode 100644 index 00000000..8b6019cd --- /dev/null +++ b/src/Sampler/RuleBased/src/RuleBasedSampler.php @@ -0,0 +1,59 @@ + $ruleSets + */ + public function __construct( + private readonly array $ruleSets, + private readonly SamplerInterface $fallback, + ) { + } + + public function shouldSample( + ContextInterface $parentContext, + string $traceId, + string $spanName, + int $spanKind, + AttributesInterface $attributes, + array $links, + ): SamplingResult { + foreach ($this->ruleSets as $ruleSet) { + foreach ($ruleSet->samplingRules() as $samplingRule) { + if (!$samplingRule->matches($parentContext, $traceId, $spanName, $spanKind, $attributes, $links)) { + continue 2; + } + } + + return $ruleSet->delegate()->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + } + + return $this->fallback->shouldSample($parentContext, $traceId, $spanName, $spanKind, $attributes, $links); + } + + public function __toString(): string + { + return sprintf('RuleBasedSampler{rules=[%s],fallback=%s}', implode(',', $this->ruleSets), $this->fallback->getDescription()); + } + + public function getDescription(): string + { + return (string) $this; + } +} diff --git a/src/Sampler/RuleBased/src/RuleSet.php b/src/Sampler/RuleBased/src/RuleSet.php new file mode 100644 index 00000000..74f2ae18 --- /dev/null +++ b/src/Sampler/RuleBased/src/RuleSet.php @@ -0,0 +1,39 @@ + $samplingRules + */ + public function __construct( + private readonly array $samplingRules, + private readonly SamplerInterface $delegate, + ) { + } + + public function __toString(): string + { + return sprintf('RuleSet{rules=[%s],delegate=%s}', implode(',', $this->samplingRules), $this->delegate->getDescription()); + } + + /** + * @return list + */ + public function samplingRules(): array + { + return $this->samplingRules; + } + + public function delegate(): SamplerInterface + { + return $this->delegate; + } +} diff --git a/src/Sampler/RuleBased/src/RuleSetInterface.php b/src/Sampler/RuleBased/src/RuleSetInterface.php new file mode 100644 index 00000000..ca042c3a --- /dev/null +++ b/src/Sampler/RuleBased/src/RuleSetInterface.php @@ -0,0 +1,20 @@ + + */ + public function samplingRules(): array; + public function delegate(): SamplerInterface; +} diff --git a/src/Sampler/RuleBased/src/SamplingRule.php b/src/Sampler/RuleBased/src/SamplingRule.php new file mode 100644 index 00000000..fbc81df6 --- /dev/null +++ b/src/Sampler/RuleBased/src/SamplingRule.php @@ -0,0 +1,38 @@ + $links span links + * @return bool whether this rule matches the given data + * + * @see Sampler::shouldSample() + */ + public function matches( + ContextInterface $context, + string $traceId, + string $spanName, + int $spanKind, + AttributesInterface $attributes, + array $links, + ): bool; +} diff --git a/src/Sampler/RuleBased/src/SamplingRule/AttributeRule.php b/src/Sampler/RuleBased/src/SamplingRule/AttributeRule.php new file mode 100644 index 00000000..25b11e08 --- /dev/null +++ b/src/Sampler/RuleBased/src/SamplingRule/AttributeRule.php @@ -0,0 +1,44 @@ +has($this->attributeKey) + && preg_match($this->pattern, (string) $attributes->get($this->attributeKey)); + } + + public function __toString(): string + { + return sprintf('Attribute{key=%s,pattern=%s}', $this->attributeKey, $this->pattern); + } +} diff --git a/src/Sampler/RuleBased/src/SamplingRule/LinkRule.php b/src/Sampler/RuleBased/src/SamplingRule/LinkRule.php new file mode 100644 index 00000000..787c8f3f --- /dev/null +++ b/src/Sampler/RuleBased/src/SamplingRule/LinkRule.php @@ -0,0 +1,52 @@ +getSpanContext()->isSampled() !== $this->sampled) { + continue; + } + if ($this->remote !== null && $link->getSpanContext()->isRemote() !== $this->remote) { + continue; + } + + return true; + } + + return false; + } + + public function __toString(): string + { + return sprintf('Link{sampled=%s,remote=%s}', var_export($this->sampled, true), var_export($this->remote, true)); + } +} diff --git a/src/Sampler/RuleBased/src/SamplingRule/ParentRule.php b/src/Sampler/RuleBased/src/SamplingRule/ParentRule.php new file mode 100644 index 00000000..e2521e80 --- /dev/null +++ b/src/Sampler/RuleBased/src/SamplingRule/ParentRule.php @@ -0,0 +1,45 @@ +getContext(); + + return $parent->isValid() + && $parent->isSampled() === $this->sampled + && ($this->remote === null || $parent->isRemote() === $this->remote); + } + + public function __toString(): string + { + return sprintf('Parent{sampled=%s,remote=%s}', var_export($this->sampled, true), var_export($this->remote, true)); + } +} diff --git a/src/Sampler/RuleBased/src/SamplingRule/SpanKindRule.php b/src/Sampler/RuleBased/src/SamplingRule/SpanKindRule.php new file mode 100644 index 00000000..b3083e7c --- /dev/null +++ b/src/Sampler/RuleBased/src/SamplingRule/SpanKindRule.php @@ -0,0 +1,38 @@ +spanKind === $spanKind; + } + + public function __toString(): string + { + return sprintf('SpanKind{kind=%s}', $this->spanKind); //@todo SpanKind enum? + } +} diff --git a/src/Sampler/RuleBased/src/SamplingRule/SpanNameRule.php b/src/Sampler/RuleBased/src/SamplingRule/SpanNameRule.php new file mode 100644 index 00000000..5861f49a --- /dev/null +++ b/src/Sampler/RuleBased/src/SamplingRule/SpanNameRule.php @@ -0,0 +1,38 @@ +pattern, $spanName); + } + + public function __toString(): string + { + return sprintf('SpanName{pattern=%s}', $this->pattern); + } +} diff --git a/src/Sampler/RuleBased/tests/Integration/RuleBasedSamplerTest.php b/src/Sampler/RuleBased/tests/Integration/RuleBasedSamplerTest.php new file mode 100644 index 00000000..420fc7e9 --- /dev/null +++ b/src/Sampler/RuleBased/tests/Integration/RuleBasedSamplerTest.php @@ -0,0 +1,26 @@ +expectNotToPerformAssertions(); + Configuration::parseFile($file)->create(); + } + + public static function configFileProvider(): iterable + { + yield [__DIR__ . '/config/sdk-config.yaml']; + } +} diff --git a/src/Sampler/RuleBased/tests/Integration/config/sdk-config.yaml b/src/Sampler/RuleBased/tests/Integration/config/sdk-config.yaml new file mode 100644 index 00000000..c12a8859 --- /dev/null +++ b/src/Sampler/RuleBased/tests/Integration/config/sdk-config.yaml @@ -0,0 +1,25 @@ +file_format: "0.3" + +tracer_provider: + sampler: + contrib_rule_based: + rule_sets: + - rules: + - attribute: {key: is.important, pattern: ~false~ } + delegate: + always_off: {} + - rules: + - link: { sampled: true } + delegate: + always_off: {} + - rules: + - span_name: { pattern: ~foobar~ } + delegate: + always_off: {} + - rules: + - span_kind: { kind: SERVER } + - attribute: { key: url.path, pattern: ~^/health$~ } + delegate: + always_off: {} + fallback: + always_on: {} \ No newline at end of file diff --git a/src/Sampler/RuleBased/tests/Unit/RuleBasedSamplerTest.php b/src/Sampler/RuleBased/tests/Unit/RuleBasedSamplerTest.php new file mode 100644 index 00000000..d7ab145e --- /dev/null +++ b/src/Sampler/RuleBased/tests/Unit/RuleBasedSamplerTest.php @@ -0,0 +1,99 @@ + */ + private array $links; + + public function setUp(): void + { + $this->context = $this->createMock(ContextInterface::class); + $this->traceId = 'some.trace.id'; + $this->spanName = 'my-span'; + $this->spanKind = SpanKind::KIND_SERVER; + $this->attributes = $this->createMock(AttributesInterface::class); + $this->links = []; + + $this->delegate = $this->createMock(SamplerInterface::class); + $this->rule = $this->createMock(SamplingRule::class); + $this->ruleSet = new RuleSet([$this->rule], $this->delegate); + + $this->fallback = $this->createMock(SamplerInterface::class); + $this->sampler = new RuleBasedSampler([$this->ruleSet], $this->fallback); + } + + public function test_delegates_on_ruleset_match(): void + { + $this->fallback + ->expects($this->never()) + ->method('shouldSample'); + $this->delegate + ->expects($this->once()) + ->method('shouldSample') + ->willReturn(new SamplingResult(SamplingResult::RECORD_AND_SAMPLE)); + $this->rule + ->expects($this->once()) + ->method('matches') + ->willReturn(true); + + $this->sampler->shouldSample($this->context, $this->traceId, $this->spanName, $this->spanKind, $this->attributes, $this->links); + } + + public function test_uses_fallback_when_no_match(): void + { + $this->fallback + ->expects($this->once()) + ->method('shouldSample') + ->willReturn(new SamplingResult(SamplingResult::RECORD_AND_SAMPLE)); + $this->delegate + ->expects($this->never()) + ->method('shouldSample'); + $this->rule + ->expects($this->once()) + ->method('matches') + ->willReturn(false); + + $this->sampler->shouldSample($this->context, $this->traceId, $this->spanName, $this->spanKind, $this->attributes, $this->links); + } + + public function test_get_description(): void + { + $this->rule->method('__toString')->willReturn('rule-one'); + $this->fallback->method('getDescription')->willReturn('fallback'); + $this->delegate->method('getDescription')->willReturn('delegate'); + + $this->assertSame('RuleBasedSampler{rules=[RuleSet{rules=[rule-one],delegate=delegate}],fallback=fallback}', $this->sampler->getDescription()); + } +} diff --git a/src/Sampler/RuleBased/tests/Unit/RuleSetTest.php b/src/Sampler/RuleBased/tests/Unit/RuleSetTest.php new file mode 100644 index 00000000..03aadbaa --- /dev/null +++ b/src/Sampler/RuleBased/tests/Unit/RuleSetTest.php @@ -0,0 +1,49 @@ +delegate = $this->createMock(SamplerInterface::class); + $this->rule_one = $this->createMock(SamplingRule::class); + $this->rule_two = $this->createMock(SamplingRule::class); + } + + public function test_getters(): void + { + $rules = [$this->rule_one, $this->rule_two]; + $ruleSet = new RuleSet($rules, $this->delegate); + + $this->assertSame($rules, $ruleSet->samplingRules()); + $this->assertSame($this->delegate, $ruleSet->delegate()); + } + + public function test_to_string(): void + { + $this->rule_one->expects($this->once())->method('__toString')->willReturn('rule_one'); + $this->rule_two->expects($this->once())->method('__toString')->willReturn('rule_two'); + $this->delegate->expects($this->once())->method('getDescription')->willReturn('delegate'); + $ruleSet = new RuleSet([$this->rule_one, $this->rule_two], $this->delegate); + + $this->assertSame('RuleSet{rules=[rule_one,rule_two],delegate=delegate}', (string) $ruleSet); + } +} diff --git a/src/Sampler/RuleBased/tests/Unit/SamplingRule/AttributeRuleTest.php b/src/Sampler/RuleBased/tests/Unit/SamplingRule/AttributeRuleTest.php new file mode 100644 index 00000000..b26ee377 --- /dev/null +++ b/src/Sampler/RuleBased/tests/Unit/SamplingRule/AttributeRuleTest.php @@ -0,0 +1,50 @@ +assertSame( + $expected, + $rule->matches( + $this->createMock(ContextInterface::class), + 'trace-id', + 'span-name', + SpanKind::KIND_SERVER, + $attributes, + [] + ) + ); + } + + public static function attributesProvider(): iterable + { + yield [Attributes::create(['foo' => 'foo']), true]; + yield [Attributes::create(['foo' => 'bar']), false]; + yield [Attributes::create(['bar' => 'foo']), false]; + } + + public function test_to_string(): void + { + $rule = new AttributeRule('foo', '~foo~'); + + $this->assertSame('Attribute{key=foo,pattern=~foo~}', (string) $rule); + } +} diff --git a/src/Sampler/RuleBased/tests/Unit/SamplingRule/LinkRuleTest.php b/src/Sampler/RuleBased/tests/Unit/SamplingRule/LinkRuleTest.php new file mode 100644 index 00000000..b54b1ada --- /dev/null +++ b/src/Sampler/RuleBased/tests/Unit/SamplingRule/LinkRuleTest.php @@ -0,0 +1,68 @@ +createMock(AttributesInterface::class) + ); + + $rule = new LinkRule($sampled, $remote); + $this->assertSame( + $expected, + $rule->matches( + $this->createMock(ContextInterface::class), + 'trace-id', + 'span-name', + SpanKind::KIND_SERVER, + $this->createMock(AttributesInterface::class), + [$link] + ) + ); + } + + public static function matchesProvider(): array + { + //isSampled, isRemote, sampled, remote, expected + return [ + 'is sampled, allow sampled' => [true, false, true, false, true], + 'is sampled, allow remote' => [true, false, false, true, false], + 'is remote, allow sampled' => [false, true, true, false, false], + 'is remote, allow remote' => [false, true, false, true, true], + 'is sampled+remote, allow only sampled' => [true, true, true, false, false], + 'is sampled+remote, allow only remote' => [true, true, false, true, false], + 'is sampled+remote, allow sampled+remote' => [true, true, true, true, true], + ]; + } + + public function test_to_string(): void + { + $rule = new LinkRule(true, false); + $this->assertSame('Link{sampled=true,remote=false}', (string) $rule); + } +} diff --git a/src/Sampler/RuleBased/tests/Unit/SamplingRule/ParentRuleTest.php b/src/Sampler/RuleBased/tests/Unit/SamplingRule/ParentRuleTest.php new file mode 100644 index 00000000..3a375177 --- /dev/null +++ b/src/Sampler/RuleBased/tests/Unit/SamplingRule/ParentRuleTest.php @@ -0,0 +1,71 @@ +createMock(SpanInterface::class); + $span->method('getContext')->willReturn($spanContext); + $context = $this->createMock(ContextInterface::class); + $context->method('get')->willReturn($span); + + $rule = new ParentRule($sampled, $remote); + $this->assertSame( + $expected, + $rule->matches( + $context, + 'trace-id', + 'span-name', + SpanKind::KIND_SERVER, + $this->createMock(AttributesInterface::class), + [] + ) + ); + } + + public static function matchesProvider(): array + { + //isSampled, isRemote, sampled, remote, expected + return [ + 'is sampled, allow sampled' => [true, false, true, false, true], + 'is sampled, allow remote' => [true, false, false, true, false], + 'is remote, allow sampled' => [false, true, true, false, false], + 'is remote, allow remote' => [false, true, false, true, true], + 'is sampled+remote, allow only sampled' => [true, true, true, false, false], + 'is sampled+remote, allow only remote' => [true, true, false, true, false], + 'is sampled+remote, allow sampled+remote' => [true, true, true, true, true], + ]; + } + + public function test_to_string(): void + { + $rule = new ParentRule(true, false); + $this->assertSame('Parent{sampled=true,remote=false}', (string) $rule); + } +} diff --git a/src/Sampler/RuleBased/tests/Unit/SamplingRule/SpanKindRuleTest.php b/src/Sampler/RuleBased/tests/Unit/SamplingRule/SpanKindRuleTest.php new file mode 100644 index 00000000..8db7ed2b --- /dev/null +++ b/src/Sampler/RuleBased/tests/Unit/SamplingRule/SpanKindRuleTest.php @@ -0,0 +1,39 @@ +assertSame($expected, $rule->matches(Context::getRoot(), 'trace-id', 'foo', $spanKind, Attributes::create([]), [])); + } + + public static function matchesProvider(): array + { + return [ + [SpanKind::KIND_INTERNAL, SpanKind::KIND_INTERNAL, true], + [SpanKind::KIND_INTERNAL, SpanKind::KIND_SERVER, false], + ]; + } + + public function test_to_string(): void + { + $rule = new SpanKindRule(SpanKind::KIND_SERVER); + + $this->assertSame('SpanKind{kind=2}', (string) $rule); + } +} diff --git a/src/Sampler/RuleBased/tests/Unit/SamplingRule/SpanNameRuleTest.php b/src/Sampler/RuleBased/tests/Unit/SamplingRule/SpanNameRuleTest.php new file mode 100644 index 00000000..75301553 --- /dev/null +++ b/src/Sampler/RuleBased/tests/Unit/SamplingRule/SpanNameRuleTest.php @@ -0,0 +1,44 @@ +assertSame($expected, $rule->matches(Context::getRoot(), 'trace-id', $spanName, SpanKind::KIND_INTERNAL, Attributes::create([]), [])); + } + + public static function matchesProvider(): array + { + return [ + ['~foo~', 'foo', true], + ['~bar~', 'foobarbaz', true], + ['~foo~', 'bar', false], + ['~^bar$~', 'Xbar', false], + ]; + } + + public function test_to_string(): void + { + $rule = new SpanNameRule('~foo~'); + + $this->assertSame('SpanName{pattern=~foo~}', (string) $rule); + } +}