diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4515fadea..8b9706e68 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -70,6 +70,7 @@ updates: - "/src/Instrumentation/ExtRdKafka" - "/src/Instrumentation/Guzzle" - "/src/Instrumentation/HttpAsyncClient" + - "/src/Instrumentation/HttpConfig" - "/src/Instrumentation/IO" - "/src/Instrumentation/Laravel" - "/src/Instrumentation/MongoDB" diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 3fd6f3007..8a5867579 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -30,6 +30,7 @@ jobs: 'Instrumentation/ExtRdKafka', 'Instrumentation/Guzzle', 'Instrumentation/HttpAsyncClient', + 'Instrumentation/HttpConfig', 'Instrumentation/IO', 'Instrumentation/Laravel', 'Instrumentation/MongoDB', diff --git a/.gitsplit.yml b/.gitsplit.yml index 4acf537c5..67386a743 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -24,6 +24,8 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-guzzle.git" - prefix: "src/Instrumentation/HttpAsyncClient" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-http-async.git" + - prefix: "src/Instrumentation/HttpConfig" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-config-http.git" - prefix: "src/Instrumentation/IO" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-io.git" - prefix: "src/Instrumentation/Laravel" diff --git a/Makefile b/Makefile index 07706512b..a120f8307 100644 --- a/Makefile +++ b/Makefile @@ -19,9 +19,9 @@ build: ## Build image install: ## Install dependencies $(DC_RUN_PHP) env XDEBUG_MODE=off composer install update: ## Update dependencies - $(DC_RUN_PHP) env XDEBUG_MODE=off composer update --no-plugins + $(DC_RUN_PHP) env XDEBUG_MODE=off composer update --no-interaction update-lowest: ## Update dependencies to lowest supported versions - $(DC_RUN_PHP) env XDEBUG_MODE=off composer update --no-plugins --prefer-lowest + $(DC_RUN_PHP) env XDEBUG_MODE=off composer update --no-interaction --prefer-lowest test: ## Run all tests $(DC_RUN_PHP) env XDEBUG_MODE=off vendor/bin/phpunit --testdox --colors=always test-unit: ## Run unit tests diff --git a/composer.json b/composer.json index f215fadd8..42e4a347e 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "OpenTelemetry\\Contrib\\Instrumentation\\ExtRdKafka\\": "src/Instrumentation/ExtRdKafka/src", "OpenTelemetry\\Contrib\\Instrumentation\\Laravel\\": "src/Instrumentation/Laravel/src", "OpenTelemetry\\Contrib\\Instrumentation\\HttpAsyncClient\\": "src/Instrumentation/HttpAsyncClient/src", + "OpenTelemetry\\Contrib\\Instrumentation\\HttpConfig\\": "src/Instrumentation/HttpConfig/src", "OpenTelemetry\\Contrib\\Instrumentation\\IO\\": "src/Instrumentation/IO/src", "OpenTelemetry\\Contrib\\Instrumentation\\MongoDB\\": "src/Instrumentation/MongoDB/src", "OpenTelemetry\\Contrib\\Instrumentation\\MySqli\\": "src/Instrumentation/MySqli/src", @@ -86,6 +87,7 @@ "open-telemetry/opentelemetry-auto-slim": "self.version", "open-telemetry/opentelemetry-auto-symfony": "self.version", "open-telemetry/opentelemetry-auto-wordpress": "self.version", + "open-telemetry/opentelemetry-config-http": "self.version", "open-telemetry/opentelemetry-propagation-instana": "self.version", "open-telemetry/opentelemetry-propagation-server-timing": "self.version", "open-telemetry/opentelemetry-propagation-traceresponse": "self.version", diff --git a/src/Instrumentation/HttpConfig/.gitattributes b/src/Instrumentation/HttpConfig/.gitattributes new file mode 100644 index 000000000..1676cf825 --- /dev/null +++ b/src/Instrumentation/HttpConfig/.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 diff --git a/src/Instrumentation/HttpConfig/.gitignore b/src/Instrumentation/HttpConfig/.gitignore new file mode 100644 index 000000000..57872d0f1 --- /dev/null +++ b/src/Instrumentation/HttpConfig/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/src/Instrumentation/HttpConfig/.phan/config.php b/src/Instrumentation/HttpConfig/.phan/config.php new file mode 100644 index 000000000..cbee12d1a --- /dev/null +++ b/src/Instrumentation/HttpConfig/.phan/config.php @@ -0,0 +1,370 @@ + '8.2', + + // 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/Config/' + ], + + // 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/HttpConfig/.php-cs-fixer.php b/src/Instrumentation/HttpConfig/.php-cs-fixer.php new file mode 100644 index 000000000..e35fa078c --- /dev/null +++ b/src/Instrumentation/HttpConfig/.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/Instrumentation/HttpConfig/README.md b/src/Instrumentation/HttpConfig/README.md new file mode 100644 index 000000000..158cd7aa3 --- /dev/null +++ b/src/Instrumentation/HttpConfig/README.md @@ -0,0 +1,74 @@ +[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-http-config/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/HttpConfig) +[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/contrib-config-http) +[![Latest Version](http://poser.pugx.org/open-telemetry/opentelemetry-config-http/v/unstable)](https://packagist.org/packages/open-telemetry/opentelemetry-config-http/) +[![Stable](http://poser.pugx.org/open-telemetry/opentelemetry-config-http/v/stable)](https://packagist.org/packages/open-telemetry/opentelemetry-config-http/) + +This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib. + +# OpenTelemetry HTTP configuration + +Provides configuration options for HTTP instrumentation packages. + +## Configuration + +### File-based configuration + +```yaml +instrumentation/development: + php: + http: + client: + capture_url_scheme: + capture_url_template: + capture_user_agent_original: + capture_user_agent_synthetic_type: + capture_network_transport: + capture_request_body_size: + capture_request_size: + capture_response_body_size: + capture_response_size: + server: + capture_client_port: + capture_user_agent_synthetic_type: + capture_network_transport: + capture_network_local_address: + capture_network_local_port: + capture_request_body_size: + capture_request_size: + capture_response_body_size: + capture_response_size: + uri_sanitizers: + - default: + - redact_query_string_values: + query_keys: [ passwd, secret ] + known_http_methods: [ CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE, CUSTOM ] +``` + +### Env-based configuration + +```dotenv +OTEL_PHP_INSTRUMENTATION_URL_SANITIZE_FIELD_NAMES="passwd,secret" +OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS="CONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE,CUSTOM" +``` + +## Usage + +```php +use OpenTelemetry\API\Configuration\ConfigProperties; +use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Context; +use OpenTelemetry\API\Instrumentation\AutoInstrumentation\HookManagerInterface; +use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Instrumentation; +use OpenTelemetry\Contrib\Instrumentation\HttpConfig\HttpConfig; + +final class CustomHttpInstrumentation implements Instrumentation +{ + public function register(HookManagerInterface $hookManager, ConfigProperties $configuration, Context $context): void + { + $httpConfig = $configuration->get(HttpConfig::class) ?? new HttpConfig(); + + $httpConfig->... + } +} +``` diff --git a/src/Instrumentation/HttpConfig/composer.json b/src/Instrumentation/HttpConfig/composer.json new file mode 100644 index 000000000..9627e8ded --- /dev/null +++ b/src/Instrumentation/HttpConfig/composer.json @@ -0,0 +1,61 @@ +{ + "name": "open-telemetry/opentelemetry-config-http", + "description": "OpenTelemetry HTTP configuration options", + "keywords": ["opentelemetry", "otel", "open-telemetry", "instrumentation"], + "type": "library", + "homepage": "https://opentelemetry.io/docs/php", + "readme": "./README.md", + "license": "Apache-2.0", + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "php": "^8.1", + "psr/http-message": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.7", + "league/uri": "^7.5", + "phan/phan": "^5.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "psalm/plugin-phpunit": "^0.19.3", + "open-telemetry/api": "^1.4", + "open-telemetry/sdk": "^1.6", + "open-telemetry/sdk-configuration": "^0.1.0", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^6.7", + "symfony/yaml": "^6.4", + "symfony/polyfill-php83": "^1.32" + }, + "autoload": { + "psr-4": { + "OpenTelemetry\\Contrib\\Instrumentation\\HttpConfig\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "OpenTelemetry\\Tests\\Instrumentation\\HttpConfig\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "tbachert/spi": true + } + }, + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + }, + "spi": { + "OpenTelemetry\\API\\Configuration\\Config\\ComponentProvider": [ + "OpenTelemetry\\Contrib\\Instrumentation\\HttpConfig\\Config\\InstrumentationConfigurationHttpConfig", + "OpenTelemetry\\Contrib\\Instrumentation\\HttpConfig\\Config\\UriSanitizerDefault", + "OpenTelemetry\\Contrib\\Instrumentation\\HttpConfig\\Config\\UriSanitizerRedactQueryStringValues", + "OpenTelemetry\\Contrib\\Instrumentation\\HttpConfig\\Config\\UriSanitizerRedactUserInfo" + ], + "OpenTelemetry\\API\\Configuration\\ConfigEnv\\EnvComponentLoader": [ + "OpenTelemetry\\Contrib\\Instrumentation\\HttpConfig\\ConfigEnv\\InstrumentationConfigurationHttpConfig" + ] + } + } +} diff --git a/src/Instrumentation/HttpConfig/phpstan.neon.dist b/src/Instrumentation/HttpConfig/phpstan.neon.dist new file mode 100644 index 000000000..b2f2a724d --- /dev/null +++ b/src/Instrumentation/HttpConfig/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/Config/* diff --git a/src/Instrumentation/HttpConfig/phpunit.xml.dist b/src/Instrumentation/HttpConfig/phpunit.xml.dist new file mode 100644 index 000000000..4f9d569ce --- /dev/null +++ b/src/Instrumentation/HttpConfig/phpunit.xml.dist @@ -0,0 +1,42 @@ + + + + + + + src + + + + + + + + + + + + + tests/Unit + + + tests/Integration + + + + diff --git a/src/Instrumentation/HttpConfig/psalm.xml.dist b/src/Instrumentation/HttpConfig/psalm.xml.dist new file mode 100644 index 000000000..61ed3c061 --- /dev/null +++ b/src/Instrumentation/HttpConfig/psalm.xml.dist @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Instrumentation/HttpConfig/src/Config/InstrumentationConfigurationHttpConfig.php b/src/Instrumentation/HttpConfig/src/Config/InstrumentationConfigurationHttpConfig.php new file mode 100644 index 000000000..bae4e337f --- /dev/null +++ b/src/Instrumentation/HttpConfig/src/Config/InstrumentationConfigurationHttpConfig.php @@ -0,0 +1,134 @@ + + */ +final class InstrumentationConfigurationHttpConfig implements ComponentProvider +{ + + /** + * @param array{ + * client?: array{ + * capture_url_scheme?: bool, + * capture_url_template?: bool, + * capture_user_agent_original?: bool, + * capture_user_agent_synthetic_type?: bool, + * capture_network_transport?: bool, + * capture_request_body_size?: bool, + * capture_request_size?: bool, + * capture_response_body_size?: bool, + * capture_response_size?: bool, + * }, + * server: array{ + * capture_client_port?: bool, + * capture_user_agent_synthetic_type?: bool, + * capture_network_transport?: bool, + * capture_network_local_address?: bool, + * capture_network_local_port?: bool, + * capture_request_body_size?: bool, + * capture_request_size?: bool, + * capture_response_body_size?: bool, + * capture_response_size?: bool, + * }, + * uri_sanitizers: ?list>, + * known_http_methods: list, + * } $properties + */ + #[Override] + public function createPlugin(array $properties, Context $context): InstrumentationConfiguration + { + return new HttpConfig( + client: new HttpClientConfig( + captureUrlScheme: $properties['client']['capture_url_scheme'] ?? false, + captureUrlTemplate: $properties['client']['capture_url_template'] ?? false, + captureUserAgentOriginal: $properties['client']['capture_user_agent_original'] ?? false, + captureUserAgentSyntheticType: $properties['client']['capture_user_agent_synthetic_type'] ?? false, + captureNetworkTransport: $properties['client']['capture_network_transport'] ?? false, + captureRequestBodySize: $properties['client']['capture_request_body_size'] ?? false, + captureRequestSize: $properties['client']['capture_request_size'] ?? false, + captureResponseBodySize: $properties['client']['capture_response_body_size'] ?? false, + captureResponseSize: $properties['client']['capture_response_size'] ?? false, + ), + server: new HttpServerConfig( + captureClientPort: $properties['server']['capture_client_port'] ?? false, + captureUserAgentSyntheticType: $properties['server']['capture_user_agent_synthetic_type'] ?? false, + captureNetworkTransport: $properties['server']['capture_network_transport'] ?? false, + captureNetworkLocalAddress: $properties['server']['capture_network_local_address'] ?? false, + captureNetworkLocalPort: $properties['server']['capture_network_local_port'] ?? false, + captureRequestBodySize: $properties['server']['capture_request_body_size'] ?? false, + captureRequestSize: $properties['server']['capture_request_size'] ?? false, + captureResponseBodySize: $properties['server']['capture_response_body_size'] ?? false, + captureResponseSize: $properties['server']['capture_response_size'] ?? false, + ), + sanitizer: $properties['uri_sanitizers'] === null + ? new DefaultSanitizer() + : MultiSanitizer::composite(array_map(static fn (ComponentPlugin $sanitizer) => $sanitizer->create($context), $properties['uri_sanitizers'])), + knownHttpMethods: $properties['known_http_methods'], + ); + } + + #[Override] + public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $builder): ArrayNodeDefinition + { + $node = $builder->arrayNode('http'); + $node + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('client') + ->children() + ->booleanNode('capture_url_scheme')->treatNullLike(true)->end() + ->booleanNode('capture_url_template')->treatNullLike(true)->end() + ->booleanNode('capture_user_agent_original')->treatNullLike(true)->end() + ->booleanNode('capture_user_agent_synthetic_type')->treatNullLike(true)->end() + ->booleanNode('capture_network_transport')->treatNullLike(true)->end() + ->booleanNode('capture_request_body_size')->treatNullLike(true)->end() + ->booleanNode('capture_request_size')->treatNullLike(true)->end() + ->booleanNode('capture_response_body_size')->treatNullLike(true)->end() + ->booleanNode('capture_response_size')->treatNullLike(true)->end() + ->end() + ->end() + ->arrayNode('server') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('capture_client_port')->treatNullLike(true)->end() + ->booleanNode('capture_user_agent_synthetic_type')->treatNullLike(true)->end() + ->booleanNode('capture_network_transport')->treatNullLike(true)->end() + ->booleanNode('capture_network_local_address')->treatNullLike(true)->end() + ->booleanNode('capture_network_local_port')->treatNullLike(true)->end() + ->booleanNode('capture_request_body_size')->treatNullLike(true)->end() + ->booleanNode('capture_request_size')->treatNullLike(true)->end() + ->booleanNode('capture_response_body_size')->treatNullLike(true)->end() + ->booleanNode('capture_response_size')->treatNullLike(true)->end() + ->end() + ->end() + ->append($registry->componentList('uri_sanitizers', UriSanitizer::class)->defaultNull()) + ->arrayNode('known_http_methods') + ->defaultValue(HttpConfig::HTTP_METHODS) + ->scalarPrototype()->end() + ->end() + ->end() + ; + + return $node; + } +} diff --git a/src/Instrumentation/HttpConfig/src/Config/UriSanitizerDefault.php b/src/Instrumentation/HttpConfig/src/Config/UriSanitizerDefault.php new file mode 100644 index 000000000..94f9eec45 --- /dev/null +++ b/src/Instrumentation/HttpConfig/src/Config/UriSanitizerDefault.php @@ -0,0 +1,37 @@ + + */ +final class UriSanitizerDefault implements ComponentProvider +{ + + /** + * @param array{ + * } $properties + */ + #[Override] + public function createPlugin(array $properties, Context $context): UriSanitizer + { + return new DefaultSanitizer(); + } + + #[Override] + public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $builder): ArrayNodeDefinition + { + return $builder->arrayNode('default'); + } +} diff --git a/src/Instrumentation/HttpConfig/src/Config/UriSanitizerRedactQueryStringValues.php b/src/Instrumentation/HttpConfig/src/Config/UriSanitizerRedactQueryStringValues.php new file mode 100644 index 000000000..8c29068c6 --- /dev/null +++ b/src/Instrumentation/HttpConfig/src/Config/UriSanitizerRedactQueryStringValues.php @@ -0,0 +1,48 @@ + + */ +final class UriSanitizerRedactQueryStringValues implements ComponentProvider +{ + + /** + * @param array{ + * query_keys: list, + * } $properties + */ + #[Override] + public function createPlugin(array $properties, Context $context): UriSanitizer + { + return new RedactSensitiveQueryStringValuesSanitizer($properties['query_keys']); + } + + #[Override] + public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $builder): ArrayNodeDefinition + { + $node = $builder->arrayNode('redact_query_string_values'); + $node + ->children() + ->arrayNode('query_keys') + ->isRequired() + ->scalarPrototype()->end() + ->end() + ->end() + ; + + return $node; + } +} diff --git a/src/Instrumentation/HttpConfig/src/Config/UriSanitizerRedactUserInfo.php b/src/Instrumentation/HttpConfig/src/Config/UriSanitizerRedactUserInfo.php new file mode 100644 index 000000000..5ac7f829a --- /dev/null +++ b/src/Instrumentation/HttpConfig/src/Config/UriSanitizerRedactUserInfo.php @@ -0,0 +1,37 @@ + + */ +final class UriSanitizerRedactUserInfo implements ComponentProvider +{ + + /** + * @param array{ + * } $properties + */ + #[Override] + public function createPlugin(array $properties, Context $context): UriSanitizer + { + return new RedactUsernamePasswordSanitizer(); + } + + #[Override] + public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $builder): ArrayNodeDefinition + { + return $builder->arrayNode('redact_userinfo'); + } +} diff --git a/src/Instrumentation/HttpConfig/src/ConfigEnv/InstrumentationConfigurationHttpConfig.php b/src/Instrumentation/HttpConfig/src/ConfigEnv/InstrumentationConfigurationHttpConfig.php new file mode 100644 index 000000000..b68223c4f --- /dev/null +++ b/src/Instrumentation/HttpConfig/src/ConfigEnv/InstrumentationConfigurationHttpConfig.php @@ -0,0 +1,43 @@ + + */ +final class InstrumentationConfigurationHttpConfig implements EnvComponentLoader +{ + + #[Override] + public function load(EnvResolver $env, EnvComponentLoaderRegistry $registry, Context $context): InstrumentationConfiguration + { + $sanitizers = [new DefaultSanitizer()]; + if ($sanitizeFieldNames = $env->list('OTEL_PHP_INSTRUMENTATION_URL_SANITIZE_FIELD_NAMES')) { + $sanitizers[] = new RedactSensitiveQueryStringValuesSanitizer($sanitizeFieldNames); + } + + return new HttpConfig( + sanitizer: MultiSanitizer::composite($sanitizers), + knownHttpMethods: $env->list('OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS') ?? HttpConfig::HTTP_METHODS, + ); + } + + #[Override] + public function name(): string + { + return HttpConfig::class; + } +} diff --git a/src/Instrumentation/HttpConfig/src/HttpClientConfig.php b/src/Instrumentation/HttpConfig/src/HttpClientConfig.php new file mode 100644 index 000000000..ae9c16b30 --- /dev/null +++ b/src/Instrumentation/HttpConfig/src/HttpClientConfig.php @@ -0,0 +1,22 @@ +sanitizer = MultiSanitizer::composite([ + new RedactUsernamePasswordSanitizer(), + new RedactSensitiveQueryStringValuesSanitizer(['AWSAccessKeyId', 'Signature', 'sig', 'X-Goog-Signature']), + ]); + } + + #[Override] + public function sanitize(UriInterface $uri): UriInterface + { + return $this->sanitizer->sanitize($uri); + } +} diff --git a/src/Instrumentation/HttpConfig/src/UriSanitizer/MultiSanitizer.php b/src/Instrumentation/HttpConfig/src/UriSanitizer/MultiSanitizer.php new file mode 100644 index 000000000..0d15a8400 --- /dev/null +++ b/src/Instrumentation/HttpConfig/src/UriSanitizer/MultiSanitizer.php @@ -0,0 +1,45 @@ + $sanitizers + */ + private function __construct( + private readonly iterable $sanitizers, + ) { + } + + /** + * @param array $sanitizers + */ + public static function composite(array $sanitizers): UriSanitizer + { + return match (count($sanitizers)) { + 0 => new NoopSanitizer(), + 1 => $sanitizers[array_key_first($sanitizers)], + default => new self($sanitizers), + }; + } + + #[Override] + public function sanitize(UriInterface $uri): UriInterface + { + foreach ($this->sanitizers as $sanitizer) { + $uri = $sanitizer->sanitize($uri); + } + + return $uri; + } +} diff --git a/src/Instrumentation/HttpConfig/src/UriSanitizer/NoopSanitizer.php b/src/Instrumentation/HttpConfig/src/UriSanitizer/NoopSanitizer.php new file mode 100644 index 000000000..cd55ce8ba --- /dev/null +++ b/src/Instrumentation/HttpConfig/src/UriSanitizer/NoopSanitizer.php @@ -0,0 +1,19 @@ + $redactedParameters sensitive query parameters to redact + */ + public function __construct( + private readonly array $redactedParameters, + ) { + } + + #[Override] + public function sanitize(UriInterface $uri): UriInterface + { + $query = $uri->getQuery(); + $offset = 0; + $sanitized = ''; + for ($i = 0, $n = strlen($query); $i < $n; $i = $d + 1) { + if (($d = strpos($query, '&', $i)) === false) { + $d = strlen($query); + } + + foreach ($this->redactedParameters as $parameter) { + $l = strlen($parameter); + if (($query[$i + $l] ?? '') === '=' && !substr_compare($query, $parameter, $i, $l)) { + $sanitized .= substr($query, $offset, $i + $l + 1 - $offset); + $sanitized .= 'REDACTED'; + $offset = $d; + + break; + } + } + } + + if ($offset === 0) { + return $uri; + } + + $sanitized .= substr($query, $offset); + + return $uri->withQuery($sanitized); + } +} diff --git a/src/Instrumentation/HttpConfig/src/UriSanitizer/RedactUsernamePasswordSanitizer.php b/src/Instrumentation/HttpConfig/src/UriSanitizer/RedactUsernamePasswordSanitizer.php new file mode 100644 index 000000000..e3227a215 --- /dev/null +++ b/src/Instrumentation/HttpConfig/src/UriSanitizer/RedactUsernamePasswordSanitizer.php @@ -0,0 +1,27 @@ +getUserInfo(); + if ($userInfo === '') { + return $uri; + } + + return str_contains($userInfo, ':') + ? $uri->withUserInfo('REDACTED', 'REDACTED') + : $uri->withUserInfo('REDACTED'); + } +} diff --git a/src/Instrumentation/HttpConfig/tests/Integration/ConfigTest.php b/src/Instrumentation/HttpConfig/tests/Integration/ConfigTest.php new file mode 100644 index 000000000..d9b0a7ffb --- /dev/null +++ b/src/Instrumentation/HttpConfig/tests/Integration/ConfigTest.php @@ -0,0 +1,121 @@ +get(HttpConfig::class); + $this->assertInstanceOf(HttpConfig::class, $httpConfig); + + $this->assertEquals( + Http::new('https://example.com?key=value&passwd=REDACTED&secret=REDACTED'), + $httpConfig->sanitizer->sanitize(Http::new('https://example.com?key=value&passwd=1234&secret=abc')), + ); + } + + public static function configRegistryProviderSanitizeFieldNames(): iterable + { + yield 'config' => [Instrumentation::parseFile(__DIR__ . '/fixtures/sdk-config-redact-query-string-values.yaml')->create()]; + } + + #[DataProvider('configPropertiesProviderKnownHttpMethods')] + public function testKnownHttpMethods(ConfigProperties $properties): void + { + $httpConfig = $properties->get(HttpConfig::class); + $this->assertInstanceOf(HttpConfig::class, $httpConfig); + + $this->assertSame( + ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'CUSTOM'], + $httpConfig->knownHttpMethods, + ); + } + + public static function configPropertiesProviderKnownHttpMethods(): iterable + { + yield 'config' => [Instrumentation::parseFile(__DIR__ . '/fixtures/sdk-config-known-http-methods.yaml')->create()]; + } + + #[DataProvider('configPropertiesProviderCaptureAttributes')] + public function testCaptureAttributes(ConfigProperties $properties): void + { + $httpConfig = $properties->get(HttpConfig::class); + $this->assertInstanceOf(HttpConfig::class, $httpConfig); + + $this->assertTrue($httpConfig->client->captureUrlScheme); + $this->assertTrue($httpConfig->client->captureUrlTemplate); + $this->assertTrue($httpConfig->client->captureUserAgentOriginal); + $this->assertTrue($httpConfig->client->captureUserAgentSyntheticType); + $this->assertTrue($httpConfig->client->captureNetworkTransport); + $this->assertTrue($httpConfig->client->captureRequestBodySize); + $this->assertTrue($httpConfig->client->captureRequestSize); + $this->assertTrue($httpConfig->client->captureResponseBodySize); + $this->assertTrue($httpConfig->client->captureResponseSize); + + $this->assertTrue($httpConfig->server->captureClientPort); + $this->assertTrue($httpConfig->server->captureUserAgentSyntheticType); + $this->assertTrue($httpConfig->server->captureNetworkTransport); + $this->assertTrue($httpConfig->server->captureNetworkLocalAddress); + $this->assertTrue($httpConfig->server->captureNetworkLocalPort); + $this->assertTrue($httpConfig->server->captureRequestBodySize); + $this->assertTrue($httpConfig->server->captureRequestSize); + $this->assertTrue($httpConfig->server->captureResponseBodySize); + $this->assertTrue($httpConfig->server->captureResponseSize); + } + + public static function configPropertiesProviderCaptureAttributes(): iterable + { + yield 'config' => [Instrumentation::parseFile(__DIR__ . '/fixtures/sdk-config-capture-attributes.yaml')->create()]; + } + + #[DataProvider('configEmptyInstrumentationNodeProvider')] + public function testEmptyInstrumentationNode(ConfigProperties $properties): void + { + $httpConfig = $properties->get(HttpConfig::class); + $this->assertInstanceOf(HttpConfig::class, $httpConfig); + + $this->assertFalse($httpConfig->client->captureUrlScheme); + $this->assertFalse($httpConfig->client->captureUrlTemplate); + $this->assertFalse($httpConfig->client->captureUserAgentOriginal); + $this->assertFalse($httpConfig->client->captureUserAgentSyntheticType); + $this->assertFalse($httpConfig->client->captureNetworkTransport); + $this->assertFalse($httpConfig->client->captureRequestBodySize); + $this->assertFalse($httpConfig->client->captureRequestSize); + $this->assertFalse($httpConfig->client->captureResponseBodySize); + $this->assertFalse($httpConfig->client->captureResponseSize); + + $this->assertFalse($httpConfig->server->captureClientPort); + $this->assertFalse($httpConfig->server->captureUserAgentSyntheticType); + $this->assertFalse($httpConfig->server->captureNetworkTransport); + $this->assertFalse($httpConfig->server->captureNetworkLocalAddress); + $this->assertFalse($httpConfig->server->captureNetworkLocalPort); + $this->assertFalse($httpConfig->server->captureRequestBodySize); + $this->assertFalse($httpConfig->server->captureRequestSize); + $this->assertFalse($httpConfig->server->captureResponseBodySize); + $this->assertFalse($httpConfig->server->captureResponseSize); + + $this->assertSame( + ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'], + $httpConfig->knownHttpMethods, + ); + $this->assertEquals(new DefaultSanitizer(), $httpConfig->sanitizer); + } + + public static function configEmptyInstrumentationNodeProvider(): iterable + { + yield 'config' => [Instrumentation::parseFile(__DIR__ . '/fixtures/sdk-config-empty-instrumentation-node.yaml')->create()]; + } +} diff --git a/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-capture-attributes.yaml b/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-capture-attributes.yaml new file mode 100644 index 000000000..a97fa31c7 --- /dev/null +++ b/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-capture-attributes.yaml @@ -0,0 +1,26 @@ +file_format: '0.4' +instrumentation/development: &config + php: + http: + client: + capture_url_scheme: + capture_url_template: + capture_user_agent_original: + capture_user_agent_synthetic_type: + capture_network_transport: + capture_request_body_size: + capture_request_size: + capture_response_body_size: + capture_response_size: + server: + capture_client_port: + capture_user_agent_synthetic_type: + capture_network_transport: + capture_network_local_address: + capture_network_local_port: + capture_request_body_size: + capture_request_size: + capture_response_body_size: + capture_response_size: + +instrumentation: *config # OpenTelemetry uses outdated 'instrumentation' key diff --git a/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-empty-instrumentation-node.yaml b/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-empty-instrumentation-node.yaml new file mode 100644 index 000000000..62c03757c --- /dev/null +++ b/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-empty-instrumentation-node.yaml @@ -0,0 +1,6 @@ +file_format: '0.4' +instrumentation/development: &config + php: + http: + +instrumentation: *config # OpenTelemetry uses outdated 'instrumentation' key diff --git a/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-known-http-methods.yaml b/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-known-http-methods.yaml new file mode 100644 index 000000000..be0ecf6f7 --- /dev/null +++ b/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-known-http-methods.yaml @@ -0,0 +1,7 @@ +file_format: '0.4' +instrumentation/development: &config + php: + http: + known_http_methods: [ CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE, CUSTOM ] + +instrumentation: *config # OpenTelemetry uses outdated 'instrumentation' key diff --git a/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-redact-query-string-values.yaml b/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-redact-query-string-values.yaml new file mode 100644 index 000000000..94f6d9ca1 --- /dev/null +++ b/src/Instrumentation/HttpConfig/tests/Integration/fixtures/sdk-config-redact-query-string-values.yaml @@ -0,0 +1,10 @@ +file_format: '0.4' +instrumentation/development: &config + php: + http: + uri_sanitizers: + - default: + - redact_query_string_values: + query_keys: [ passwd, secret ] + +instrumentation: *config # OpenTelemetry uses outdated 'instrumentation' key diff --git a/src/Instrumentation/HttpConfig/tests/Unit/UriSanitizerTest.php b/src/Instrumentation/HttpConfig/tests/Unit/UriSanitizerTest.php new file mode 100644 index 000000000..ffb5b0944 --- /dev/null +++ b/src/Instrumentation/HttpConfig/tests/Unit/UriSanitizerTest.php @@ -0,0 +1,74 @@ +assertEquals($expected, $sanitizer->sanitize($uri)); + } + + public static function redactUsernamePasswordProvider(): iterable + { + yield 'no userinfo' => [ + Http::new('https://example.com'), + Http::new('https://example.com'), + ]; + yield 'username only' => [ + Http::new('https://user@example.com'), + Http::new('https://REDACTED@example.com'), + ]; + yield 'username+password' => [ + Http::new('https://user:pass@example.com'), + Http::new('https://REDACTED:REDACTED@example.com'), + ]; + } + + #[DataProvider('redactSensitiveQueryStringValuesProvider')] + public function testRedactSensitiveQueryStringValues(UriInterface $uri, UriInterface $expected): void + { + $sanitizer = new RedactSensitiveQueryStringValuesSanitizer(['secret', 'pass', 'passwd']); + $this->assertEquals($expected, $sanitizer->sanitize($uri)); + } + + public static function redactSensitiveQueryStringValuesProvider(): iterable + { + yield 'no query string' => [ + Http::new('https://example.com'), + Http::new('https://example.com'), + ]; + yield 'non-sensitive query string' => [ + Http::new('https://example.com?key=value'), + Http::new('https://example.com?key=value'), + ]; + yield 'sensitive query string' => [ + Http::new('https://example.com?secret=value'), + Http::new('https://example.com?secret=REDACTED'), + ]; + yield 'multiple sensitive query strings' => [ + Http::new('https://example.com?secret=value&pass=test'), + Http::new('https://example.com?secret=REDACTED&pass=REDACTED'), + ]; + yield 'multiple sensitive query strings with values longer than "REDACTED"' => [ + Http::new('https://example.com?secret=value0123456789&pass=test0123456789'), + Http::new('https://example.com?secret=REDACTED&pass=REDACTED'), + ]; + yield 'mixed sensitive and non-sensitive query string' => [ + Http::new('https://example.com?secret=value&key=value&pass=1234×tamp=123456789'), + Http::new('https://example.com?secret=REDACTED&key=value&pass=REDACTED×tamp=123456789'), + ]; + } +}