diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 273275062..99e99a6ea 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -59,9 +59,12 @@ jobs: 'Propagation/Instana', 'Propagation/ServerTiming', 'Propagation/TraceResponse', + 'ResourceDetectors/Apache', 'ResourceDetectors/Azure', 'ResourceDetectors/Container', 'ResourceDetectors/DigitalOcean', + 'ResourceDetectors/Fpm', + 'ResourceDetectors/Kubernetes', 'Sampler/RuleBased', 'Sampler/Xray', 'Shims/OpenTracing', diff --git a/.gitsplit.yml b/.gitsplit.yml index eac359c84..b37d6a224 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -82,12 +82,18 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-propagator-server-timing.git" - prefix: "src/Propagation/TraceResponse" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-propagator-traceresponse.git" + - prefix: "src/ResourceDetectors/Apache" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-detector-apache.git" - prefix: "src/ResourceDetectors/Azure" 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/ResourceDetectors/DigitalOcean" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-detector-digitalocean.git" + - prefix: "src/ResourceDetectors/Fpm" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-detector-fpm.git" + - prefix: "src/ResourceDetectors/Kubernetes" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-detector-k8s.git" - prefix: "src/Sampler/RuleBased" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-sampler-rulebased.git" - prefix: "src/Sampler/Xray" diff --git a/src/ResourceDetectors/Apache/.gitattributes b/src/ResourceDetectors/Apache/.gitattributes new file mode 100644 index 000000000..e088d9982 --- /dev/null +++ b/src/ResourceDetectors/Apache/.gitattributes @@ -0,0 +1,15 @@ +* text=auto + +*.md diff=markdown +*.php diff=php + +/.gitattributes export-ignore +/.gitignore export-ignore +/.phan export-ignore +/.php-cs-fixer.php export-ignore +/coverage.clover export-ignore +/phpstan.neon.dist export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml.dist export-ignore +/tests export-ignore diff --git a/src/ResourceDetectors/Apache/.gitignore b/src/ResourceDetectors/Apache/.gitignore new file mode 100644 index 000000000..57872d0f1 --- /dev/null +++ b/src/ResourceDetectors/Apache/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/src/ResourceDetectors/Apache/.phan/config.php b/src/ResourceDetectors/Apache/.phan/config.php new file mode 100644 index 000000000..6473a9aa8 --- /dev/null +++ b/src/ResourceDetectors/Apache/.phan/config.php @@ -0,0 +1,371 @@ + '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/', + 'proto/', + 'thrift/' + ], + + // Enable this to enable checks of require/include statements referring to valid paths. + 'enable_include_path_checks' => true, + + // The number of processes to fork off during the analysis + // phase. + 'processes' => 1, + + // List of case-insensitive file extensions supported by Phan. + // (e.g. `['php', 'html', 'htm']`) + 'analyzed_file_extensions' => [ + 'php', + ], + + // You can put paths to stubs of internal extensions in this config option. + // If the corresponding extension is **not** loaded, then Phan will use the stubs instead. + // Phan will continue using its detailed type annotations, + // but load the constants, classes, functions, and classes (and their Reflection types) + // from these stub files (doubling as valid php files). + // Use a different extension from php to avoid accidentally loading these. + // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now) + // + // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`) + 'autoload_internal_extension_signatures' => [], + + // A list of plugin files to execute. + // + // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`) + // + // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/master/.phan/plugins). + // + // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`) + 'plugins' => [ + 'AlwaysReturnPlugin', + 'PregRegexCheckerPlugin', + 'UnreachableCodePlugin', + ], + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in `exclude_analysis_directory_list`, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'src', + 'vendor' + ], + + // A list of individual files to include in analysis + // with a path relative to the root directory of the + // project. + 'file_list' => [], +]; diff --git a/src/ResourceDetectors/Apache/.php-cs-fixer.php b/src/ResourceDetectors/Apache/.php-cs-fixer.php new file mode 100644 index 000000000..68f94b4b6 --- /dev/null +++ b/src/ResourceDetectors/Apache/.php-cs-fixer.php @@ -0,0 +1,44 @@ +exclude('vendor') + ->exclude('var/cache') + ->exclude('proto') + ->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/ResourceDetectors/Apache/README.md b/src/ResourceDetectors/Apache/README.md new file mode 100644 index 000000000..cb2bb1e4a --- /dev/null +++ b/src/ResourceDetectors/Apache/README.md @@ -0,0 +1,31 @@ +[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-aws/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/Azure) +[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/detector-azure) +[![Latest Version](http://poser.pugx.org/open-telemetry/detector-azure/v/unstable)](https://packagist.org/packages/open-telemetry/detector-azure/) +[![Stable](http://poser.pugx.org/open-telemetry/detector-azure/v/stable)](https://packagist.org/packages/open-telemetry/detector-azure/) + +This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib. + +# OpenTelemetry Apache Resource Detectors + +This package provides OpenTelemetry `ResourceDetector`s which will detect a stable service instance id. + +## Installation via composer + +```bash +$ composer require open-telemetry/detector-apache +``` + +## Usage + +The detector will be automatically registered as part of composer autoloading. + +By default, all built-in and registered custom resource detectors are used, and will be added to the default resources associated with traces, metrics, and logs. + +You can also provide a list of detectors via the `OTEL_PHP_DETECTORS` config (environment variable or php.ini setting): +```php +putenv('OTEL_PHP_DETECTORS=apache,env,os,') + +var_dump(ResourceInfoFactory::defaultResource()); +``` diff --git a/src/ResourceDetectors/Apache/_register.php b/src/ResourceDetectors/Apache/_register.php new file mode 100644 index 000000000..4e1ba9e30 --- /dev/null +++ b/src/ResourceDetectors/Apache/_register.php @@ -0,0 +1,7 @@ + + + + + + + + + + + tests/Unit + + + + + src + + + diff --git a/src/ResourceDetectors/Apache/psalm.xml.dist b/src/ResourceDetectors/Apache/psalm.xml.dist new file mode 100644 index 000000000..155711712 --- /dev/null +++ b/src/ResourceDetectors/Apache/psalm.xml.dist @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/ResourceDetectors/Apache/src/Apache.php b/src/ResourceDetectors/Apache/src/Apache.php new file mode 100644 index 000000000..4d970fd06 --- /dev/null +++ b/src/ResourceDetectors/Apache/src/Apache.php @@ -0,0 +1,129 @@ +isApacheSapi()) { + return ResourceInfoFactory::emptyResource(); + } + + $attributes = [ + ServiceIncubatingAttributes::SERVICE_INSTANCE_ID => $this->getStableInstanceId(), + ]; + + // Add Apache-specific attributes + if (function_exists('apache_get_version')) { + $attributes['webengine.name'] = 'apache'; //@todo add webengine.* to semconv + $apacheFullVersion = apache_get_version(); + if ($apacheFullVersion !== false) { + // Extract just the version number for webengine.version (e.g. "2.4.41" from "Apache/2.4.41 (Ubuntu)") + $versionNumber = $this->extractApacheVersionNumber($apacheFullVersion); + if ($versionNumber !== null) { + $attributes['webengine.version'] = $versionNumber; + } + + // webengine.description should contain detailed version and edition information + $attributes['webengine.description'] = $apacheFullVersion; + } + } + + $serverName = $this->getServerName(); + if ($serverName !== null) { + // Use a custom attribute for server name since it's not part of webengine semantics + $attributes['webserver.server_name'] = $serverName; + } + + return ResourceInfo::create(Attributes::create($attributes), Version::VERSION_1_36_0->url()); + } + + /** + * Generate a stable service instance ID for Apache processes. + * + * Uses server name + hostname + document root to create a deterministic UUID v5 that remains + * consistent across Apache process restarts within the same virtual host. + */ + private function getStableInstanceId(): string + { + $components = [ + 'apache', + $this->getServerName() ?? 'default', + gethostname() ?: 'localhost', + $this->getDocumentRoot() ?? '/var/www', + ]; + + // Create a stable UUID v5 using a namespace UUID and deterministic name + $namespace = Uuid::fromString('4d63009a-8d0f-11ee-aad7-4c796ed8e320'); + $name = implode('-', $components); + + return Uuid::uuid5($namespace, $name)->toString(); + } + + /** + * Check if running under Apache SAPI. + */ + private function isApacheSapi(): bool + { + $sapi = php_sapi_name(); + + return $sapi === 'apache2handler' || + $sapi === 'apache' || + str_starts_with($sapi, 'apache'); + } + + /** + * Get the Apache server name from configuration. + */ + private function getServerName(): ?string + { + return $_SERVER['SERVER_NAME'] ?? $_SERVER['HTTP_HOST'] ?? null; + } + + /** + * Get the document root for this Apache instance. + */ + private function getDocumentRoot(): ?string + { + return $_SERVER['DOCUMENT_ROOT'] ?? null; + } + + /** + * Extract version number from Apache version string. + * + * Examples: + * "Apache/2.4.41 (Ubuntu)" -> "2.4.41" + * "Apache/2.2.34 (Amazon)" -> "2.2.34" + */ + private function extractApacheVersionNumber(string $apacheVersion): ?string + { + // Match pattern like "Apache/2.4.41" and extract the version number + if (preg_match('/Apache\/(\d+\.\d+(?:\.\d+)?)/', $apacheVersion, $matches)) { + return $matches[1]; + } + + return null; + } +} diff --git a/src/ResourceDetectors/Apache/tests/Unit/ApacheTest.php b/src/ResourceDetectors/Apache/tests/Unit/ApacheTest.php new file mode 100644 index 000000000..8266e5ceb --- /dev/null +++ b/src/ResourceDetectors/Apache/tests/Unit/ApacheTest.php @@ -0,0 +1,117 @@ +getResource(); + + $this->assertCount(0, $resource->getAttributes()); + } + + public function test_apache_generates_stable_instance_id(): void + { + $resourceDetector = new Apache(); + + // Mock Apache environment by creating a reflection to access private methods + $reflection = new \ReflectionClass($resourceDetector); + $getStableInstanceIdMethod = $reflection->getMethod('getStableInstanceId'); + + // Call the method twice to ensure it's deterministic + $instanceId1 = $getStableInstanceIdMethod->invoke($resourceDetector); + $instanceId2 = $getStableInstanceIdMethod->invoke($resourceDetector); + + $this->assertSame($instanceId1, $instanceId2); + // Should be a valid UUID format (v5) + $this->assertMatchesRegularExpression('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $instanceId1); + } + + public function test_apache_sapi_detection(): void + { + $resourceDetector = new Apache(); + $reflection = new \ReflectionClass($resourceDetector); + $isApacheSapiMethod = $reflection->getMethod('isApacheSapi'); + + // Test detection logic (will be false in CLI test environment) + $result = $isApacheSapiMethod->invoke($resourceDetector); + $this->assertFalse($result); + } + + public function test_server_name_detection(): void + { + $resourceDetector = new Apache(); + $reflection = new \ReflectionClass($resourceDetector); + $getServerNameMethod = $reflection->getMethod('getServerName'); + + // Test with SERVER_NAME in $_SERVER + $_SERVER['SERVER_NAME'] = 'example.com'; + $serverName = $getServerNameMethod->invoke($resourceDetector); + $this->assertSame('example.com', $serverName); + + // Test with HTTP_HOST fallback + unset($_SERVER['SERVER_NAME']); + $_SERVER['HTTP_HOST'] = 'fallback.com'; + $serverName = $getServerNameMethod->invoke($resourceDetector); + $this->assertSame('fallback.com', $serverName); + + // Test without either set + unset($_SERVER['HTTP_HOST']); + $serverName = $getServerNameMethod->invoke($resourceDetector); + $this->assertNull($serverName); + } + + public function test_document_root_detection(): void + { + $resourceDetector = new Apache(); + $reflection = new \ReflectionClass($resourceDetector); + $getDocumentRootMethod = $reflection->getMethod('getDocumentRoot'); + + // Test with DOCUMENT_ROOT in $_SERVER + $_SERVER['DOCUMENT_ROOT'] = '/var/www/html'; + $documentRoot = $getDocumentRootMethod->invoke($resourceDetector); + $this->assertSame('/var/www/html', $documentRoot); + + // Test without DOCUMENT_ROOT set + unset($_SERVER['DOCUMENT_ROOT']); + $documentRoot = $getDocumentRootMethod->invoke($resourceDetector); + $this->assertNull($documentRoot); + } + + public function test_extract_apache_version_number(): void + { + $resourceDetector = new Apache(); + $reflection = new \ReflectionClass($resourceDetector); + $extractVersionMethod = $reflection->getMethod('extractApacheVersionNumber'); + + // Test typical Apache version strings + $this->assertEquals('2.4.41', $extractVersionMethod->invoke($resourceDetector, 'Apache/2.4.41 (Ubuntu)')); + $this->assertEquals('2.2.34', $extractVersionMethod->invoke($resourceDetector, 'Apache/2.2.34 (Amazon)')); + $this->assertEquals('2.4.53', $extractVersionMethod->invoke($resourceDetector, 'Apache/2.4.53 (Debian)')); + + // Test edge cases + $this->assertNull($extractVersionMethod->invoke($resourceDetector, 'nginx/1.18.0')); + $this->assertNull($extractVersionMethod->invoke($resourceDetector, 'Invalid version string')); + $this->assertNull($extractVersionMethod->invoke($resourceDetector, '')); + } + + protected function tearDown(): void + { + // Clean up $_SERVER variables that might affect other tests + unset($_SERVER['SERVER_NAME']); + unset($_SERVER['HTTP_HOST']); + unset($_SERVER['DOCUMENT_ROOT']); + + parent::tearDown(); + } +} diff --git a/src/ResourceDetectors/Fpm/.gitattributes b/src/ResourceDetectors/Fpm/.gitattributes new file mode 100644 index 000000000..e088d9982 --- /dev/null +++ b/src/ResourceDetectors/Fpm/.gitattributes @@ -0,0 +1,15 @@ +* text=auto + +*.md diff=markdown +*.php diff=php + +/.gitattributes export-ignore +/.gitignore export-ignore +/.phan export-ignore +/.php-cs-fixer.php export-ignore +/coverage.clover export-ignore +/phpstan.neon.dist export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml.dist export-ignore +/tests export-ignore diff --git a/src/ResourceDetectors/Fpm/.gitignore b/src/ResourceDetectors/Fpm/.gitignore new file mode 100644 index 000000000..5197e1cca --- /dev/null +++ b/src/ResourceDetectors/Fpm/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +.phpunit.cache diff --git a/src/ResourceDetectors/Fpm/.phan/config.php b/src/ResourceDetectors/Fpm/.phan/config.php new file mode 100644 index 000000000..6473a9aa8 --- /dev/null +++ b/src/ResourceDetectors/Fpm/.phan/config.php @@ -0,0 +1,371 @@ + '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/', + 'proto/', + 'thrift/' + ], + + // Enable this to enable checks of require/include statements referring to valid paths. + 'enable_include_path_checks' => true, + + // The number of processes to fork off during the analysis + // phase. + 'processes' => 1, + + // List of case-insensitive file extensions supported by Phan. + // (e.g. `['php', 'html', 'htm']`) + 'analyzed_file_extensions' => [ + 'php', + ], + + // You can put paths to stubs of internal extensions in this config option. + // If the corresponding extension is **not** loaded, then Phan will use the stubs instead. + // Phan will continue using its detailed type annotations, + // but load the constants, classes, functions, and classes (and their Reflection types) + // from these stub files (doubling as valid php files). + // Use a different extension from php to avoid accidentally loading these. + // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now) + // + // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`) + 'autoload_internal_extension_signatures' => [], + + // A list of plugin files to execute. + // + // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`) + // + // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/master/.phan/plugins). + // + // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`) + 'plugins' => [ + 'AlwaysReturnPlugin', + 'PregRegexCheckerPlugin', + 'UnreachableCodePlugin', + ], + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in `exclude_analysis_directory_list`, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'src', + 'vendor' + ], + + // A list of individual files to include in analysis + // with a path relative to the root directory of the + // project. + 'file_list' => [], +]; diff --git a/src/ResourceDetectors/Fpm/.php-cs-fixer.php b/src/ResourceDetectors/Fpm/.php-cs-fixer.php new file mode 100644 index 000000000..68f94b4b6 --- /dev/null +++ b/src/ResourceDetectors/Fpm/.php-cs-fixer.php @@ -0,0 +1,44 @@ +exclude('vendor') + ->exclude('var/cache') + ->exclude('proto') + ->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/ResourceDetectors/Fpm/README.md b/src/ResourceDetectors/Fpm/README.md new file mode 100644 index 000000000..3fc8828c4 --- /dev/null +++ b/src/ResourceDetectors/Fpm/README.md @@ -0,0 +1,31 @@ +[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-aws/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/Azure) +[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/detector-azure) +[![Latest Version](http://poser.pugx.org/open-telemetry/detector-azure/v/unstable)](https://packagist.org/packages/open-telemetry/detector-azure/) +[![Stable](http://poser.pugx.org/open-telemetry/detector-azure/v/stable)](https://packagist.org/packages/open-telemetry/detector-azure/) + +This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib. + +# OpenTelemetry FPM Resource Detectors + +This package provides OpenTelemetry `ResourceDetector`s which will detect a stable service instance id. + +## Installation via composer + +```bash +$ composer require open-telemetry/detector-fpm +``` + +## Usage + +The detector will be automatically registered as part of composer autoloading. + +By default, all built-in and registered custom resource detectors are used, and will be added to the default resources associated with traces, metrics, and logs. + +You can also provide a list of detectors via the `OTEL_PHP_DETECTORS` config (environment variable or php.ini setting): +```php +putenv('OTEL_PHP_DETECTORS=fpm,env,os,') + +var_dump(ResourceInfoFactory::defaultResource()); +``` diff --git a/src/ResourceDetectors/Fpm/_register.php b/src/ResourceDetectors/Fpm/_register.php new file mode 100644 index 000000000..ada0f4bf0 --- /dev/null +++ b/src/ResourceDetectors/Fpm/_register.php @@ -0,0 +1,7 @@ + + + + + + + + + + + tests/Unit + + + + + src + + + diff --git a/src/ResourceDetectors/Fpm/psalm.xml.dist b/src/ResourceDetectors/Fpm/psalm.xml.dist new file mode 100644 index 000000000..155711712 --- /dev/null +++ b/src/ResourceDetectors/Fpm/psalm.xml.dist @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/ResourceDetectors/Fpm/src/Fpm.php b/src/ResourceDetectors/Fpm/src/Fpm.php new file mode 100644 index 000000000..15feea293 --- /dev/null +++ b/src/ResourceDetectors/Fpm/src/Fpm.php @@ -0,0 +1,93 @@ + $this->getStableInstanceId(), + ]; + + // Add FPM-specific attributes + if (function_exists('fastcgi_finish_request')) { + $poolName = $this->getFpmPoolName(); + if ($poolName !== null) { + $attributes['process.runtime.pool'] = $poolName; + } + } + + return ResourceInfo::create(Attributes::create($attributes), Version::VERSION_1_36_0->url()); + } + + /** + * Generate a stable service instance ID for FPM processes. + * + * Uses pool name + hostname to create a deterministic UUID v5 that remains + * consistent across FPM process restarts within the same pool. + */ + private function getStableInstanceId(): string + { + $components = [ + 'fpm', + $this->getFpmPoolName() ?? 'default', + gethostname() ?: 'localhost', + ]; + + // Create a stable UUID v5 using a namespace UUID and deterministic name + $namespace = Uuid::fromString('4d63009a-8d0f-11ee-aad7-4c796ed8e320'); // DNS namespace UUID + $name = implode('-', $components); + + return Uuid::uuid5($namespace, $name)->toString(); + } + + /** + * Attempt to determine the FPM pool name from environment or server variables. + */ + private function getFpmPoolName(): ?string + { + // Try common FPM pool identification methods + if (isset($_SERVER['FPM_POOL'])) { + return $_SERVER['FPM_POOL']; + } + + if (isset($_ENV['FPM_POOL'])) { + return $_ENV['FPM_POOL']; + } + + // Fallback: try to extract from process title if available + if (function_exists('cli_get_process_title')) { + $title = cli_get_process_title(); + if ($title && preg_match('/pool\s+(\w+)/', $title, $matches)) { + return $matches[1]; + } + } + + return null; + } +} diff --git a/src/ResourceDetectors/Fpm/tests/Unit/FpmTest.php b/src/ResourceDetectors/Fpm/tests/Unit/FpmTest.php new file mode 100644 index 000000000..0580478bf --- /dev/null +++ b/src/ResourceDetectors/Fpm/tests/Unit/FpmTest.php @@ -0,0 +1,62 @@ +getResource(); + + $this->assertCount(0, $resource->getAttributes()); + } + + public function test_fpm_generates_stable_instance_id(): void + { + $resourceDetector = new Fpm(); + + // Mock FPM environment by creating a reflection to access private methods + $reflection = new \ReflectionClass($resourceDetector); + $getStableInstanceIdMethod = $reflection->getMethod('getStableInstanceId'); + + // Call the method twice to ensure it's deterministic + $instanceId1 = $getStableInstanceIdMethod->invoke($resourceDetector); + $instanceId2 = $getStableInstanceIdMethod->invoke($resourceDetector); + + $this->assertSame($instanceId1, $instanceId2); + // Should be a valid UUID format (v5) + $this->assertMatchesRegularExpression('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $instanceId1); + } + + public function test_fpm_pool_name_detection(): void + { + $resourceDetector = new Fpm(); + $reflection = new \ReflectionClass($resourceDetector); + $getFpmPoolNameMethod = $reflection->getMethod('getFpmPoolName'); + + // Test with FPM_POOL in $_SERVER + $_SERVER['FPM_POOL'] = 'test-pool'; + $poolName = $getFpmPoolNameMethod->invoke($resourceDetector); + $this->assertSame('test-pool', $poolName); + unset($_SERVER['FPM_POOL']); + + // Test with FPM_POOL in $_ENV + $_ENV['FPM_POOL'] = 'env-pool'; + $poolName = $getFpmPoolNameMethod->invoke($resourceDetector); + $this->assertSame('env-pool', $poolName); + unset($_ENV['FPM_POOL']); + + // Test without FPM_POOL set + $poolName = $getFpmPoolNameMethod->invoke($resourceDetector); + $this->assertNull($poolName); + } +} diff --git a/src/ResourceDetectors/Kubernetes/.gitattributes b/src/ResourceDetectors/Kubernetes/.gitattributes new file mode 100644 index 000000000..e088d9982 --- /dev/null +++ b/src/ResourceDetectors/Kubernetes/.gitattributes @@ -0,0 +1,15 @@ +* text=auto + +*.md diff=markdown +*.php diff=php + +/.gitattributes export-ignore +/.gitignore export-ignore +/.phan export-ignore +/.php-cs-fixer.php export-ignore +/coverage.clover export-ignore +/phpstan.neon.dist export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm.xml.dist export-ignore +/tests export-ignore diff --git a/src/ResourceDetectors/Kubernetes/.gitignore b/src/ResourceDetectors/Kubernetes/.gitignore new file mode 100644 index 000000000..57872d0f1 --- /dev/null +++ b/src/ResourceDetectors/Kubernetes/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/src/ResourceDetectors/Kubernetes/.phan/config.php b/src/ResourceDetectors/Kubernetes/.phan/config.php new file mode 100644 index 000000000..6473a9aa8 --- /dev/null +++ b/src/ResourceDetectors/Kubernetes/.phan/config.php @@ -0,0 +1,371 @@ + '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/', + 'proto/', + 'thrift/' + ], + + // Enable this to enable checks of require/include statements referring to valid paths. + 'enable_include_path_checks' => true, + + // The number of processes to fork off during the analysis + // phase. + 'processes' => 1, + + // List of case-insensitive file extensions supported by Phan. + // (e.g. `['php', 'html', 'htm']`) + 'analyzed_file_extensions' => [ + 'php', + ], + + // You can put paths to stubs of internal extensions in this config option. + // If the corresponding extension is **not** loaded, then Phan will use the stubs instead. + // Phan will continue using its detailed type annotations, + // but load the constants, classes, functions, and classes (and their Reflection types) + // from these stub files (doubling as valid php files). + // Use a different extension from php to avoid accidentally loading these. + // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now) + // + // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`) + 'autoload_internal_extension_signatures' => [], + + // A list of plugin files to execute. + // + // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`) + // + // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/master/.phan/plugins). + // + // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`) + 'plugins' => [ + 'AlwaysReturnPlugin', + 'PregRegexCheckerPlugin', + 'UnreachableCodePlugin', + ], + + // A list of directories that should be parsed for class and + // method information. After excluding the directories + // defined in `exclude_analysis_directory_list`, the remaining + // files will be statically analyzed for errors. + // + // Thus, both first-party and third-party code being used by + // your application should be included in this list. + 'directory_list' => [ + 'src', + 'vendor' + ], + + // A list of individual files to include in analysis + // with a path relative to the root directory of the + // project. + 'file_list' => [], +]; diff --git a/src/ResourceDetectors/Kubernetes/.php-cs-fixer.php b/src/ResourceDetectors/Kubernetes/.php-cs-fixer.php new file mode 100644 index 000000000..68f94b4b6 --- /dev/null +++ b/src/ResourceDetectors/Kubernetes/.php-cs-fixer.php @@ -0,0 +1,44 @@ +exclude('vendor') + ->exclude('var/cache') + ->exclude('proto') + ->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/ResourceDetectors/Kubernetes/README.md b/src/ResourceDetectors/Kubernetes/README.md new file mode 100644 index 000000000..41e13b612 --- /dev/null +++ b/src/ResourceDetectors/Kubernetes/README.md @@ -0,0 +1,31 @@ +[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-aws/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/Azure) +[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/detector-azure) +[![Latest Version](http://poser.pugx.org/open-telemetry/detector-azure/v/unstable)](https://packagist.org/packages/open-telemetry/detector-azure/) +[![Stable](http://poser.pugx.org/open-telemetry/detector-azure/v/stable)](https://packagist.org/packages/open-telemetry/detector-azure/) + +This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib. + +# OpenTelemetry Kubernetes Resource Detectors + +This package provides OpenTelemetry `ResourceDetector`s which will detect a stable service instance id. + +## Installation via composer + +```bash +$ composer require open-telemetry/detector-k8s +``` + +## Usage + +The detector will be automatically registered as part of composer autoloading. + +By default, all built-in and registered custom resource detectors are used, and will be added to the default resources associated with traces, metrics, and logs. + +You can also provide a list of detectors via the `OTEL_PHP_DETECTORS` config (environment variable or php.ini setting): +```php +putenv('OTEL_PHP_DETECTORS=k8s,env,os,') + +var_dump(ResourceInfoFactory::defaultResource()); +``` diff --git a/src/ResourceDetectors/Kubernetes/_register.php b/src/ResourceDetectors/Kubernetes/_register.php new file mode 100644 index 000000000..a349ffb73 --- /dev/null +++ b/src/ResourceDetectors/Kubernetes/_register.php @@ -0,0 +1,7 @@ + + + + + + + + + + + tests/Unit + + + + + src + + + diff --git a/src/ResourceDetectors/Kubernetes/psalm.xml.dist b/src/ResourceDetectors/Kubernetes/psalm.xml.dist new file mode 100644 index 000000000..155711712 --- /dev/null +++ b/src/ResourceDetectors/Kubernetes/psalm.xml.dist @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/ResourceDetectors/Kubernetes/src/Kubernetes.php b/src/ResourceDetectors/Kubernetes/src/Kubernetes.php new file mode 100644 index 000000000..acd532044 --- /dev/null +++ b/src/ResourceDetectors/Kubernetes/src/Kubernetes.php @@ -0,0 +1,538 @@ +isKubernetesEnvironment()) { + return ResourceInfoFactory::emptyResource(); + } + + $attributes = []; + + // Get pod UID for stable service instance ID + $podUid = $this->getPodUid(); + if ($podUid !== null) { + $attributes[ResourceAttributes::SERVICE_INSTANCE_ID] = $this->getStableInstanceId($podUid); + } + + // Add Kubernetes-specific attributes + $this->addKubernetesAttributes($attributes); + + return ResourceInfo::create(Attributes::create($attributes), Version::VERSION_1_36_0->url()); + } + + /** + * Generate a stable service instance ID for Kubernetes pods. + * + * Uses pod UID directly as it's already a UUID that remains + * consistent for the lifetime of the pod. + */ + private function getStableInstanceId(string $podUid): string + { + // Pod UID is already a UUID, but we'll use our standard UUID v5 pattern for consistency + $components = [ + 'k8s', + $podUid, + $this->getPodName() ?? 'unknown-pod', + $this->getNamespace() ?? 'default', + ]; + + // Create a stable UUID v5 using a namespace UUID and deterministic name + $namespace = Uuid::fromString('4d63009a-8d0f-11ee-aad7-4c796ed8e320'); // DNS namespace UUID + $name = implode('-', $components); + + return Uuid::uuid5($namespace, $name)->toString(); + } + + /** + * Get environment variable value, checking both $_ENV and getenv(). + */ + private function getEnv(string $name): string|false + { + if (isset($_ENV[$name])) { + return (string) $_ENV[$name]; + } + + $value = getenv($name); + + return $value === false ? false : (string) $value; + } + + /** + * Check if running in a Kubernetes environment. + */ + private function isKubernetesEnvironment(): bool + { + // Check for Kubernetes environment variables + if ($this->getEnv('KUBERNETES_SERVICE_HOST') !== false) { + return true; + } + + // Check for service account token file + if (file_exists(self::K8S_TOKEN_FILE) && is_readable(self::K8S_TOKEN_FILE)) { + return true; + } + + // Check for downward API environment variables + if ($this->getEnv('K8S_POD_NAME') !== false || $this->getEnv('K8S_POD_UID') !== false) { + return true; + } + + return false; + } + + /** + * Get the pod UID from environment variables. + */ + private function getPodUid(): ?string + { + // Try Downward API environment variable first + $podUid = $this->getEnv('K8S_POD_UID'); + if ($podUid !== false) { + return $podUid; + } + + // Alternative environment variable names + $podUid = $this->getEnv('POD_UID'); + if ($podUid !== false) { + return $podUid; + } + + return null; + } + + /** + * Get the pod name from environment variables. + */ + private function getPodName(): ?string + { + // Try Downward API environment variable first + $podName = $this->getEnv('K8S_POD_NAME'); + if ($podName !== false) { + return $podName; + } + + // Alternative environment variable names + $podName = $this->getEnv('POD_NAME'); + if ($podName !== false) { + return $podName; + } + + // Fallback to hostname which is usually the pod name in K8s + $hostname = gethostname(); + if ($hostname !== false) { + return $hostname; + } + + return null; + } + + /** + * Get the namespace from service account or environment variables. + */ + private function getNamespace(): ?string + { + // Try Downward API environment variable first + $namespace = $this->getEnv('K8S_NAMESPACE'); + if ($namespace !== false) { + return $namespace; + } + + // Alternative environment variable names + $namespace = $this->getEnv('POD_NAMESPACE'); + if ($namespace !== false) { + return $namespace; + } + + // Try reading from service account + if (file_exists(self::K8S_NAMESPACE_FILE) && is_readable(self::K8S_NAMESPACE_FILE)) { + $namespace = file_get_contents(self::K8S_NAMESPACE_FILE); + if ($namespace !== false) { + return trim($namespace); + } + } + + return null; + } + + /** + * Get the cluster name from environment variables. + */ + private function getClusterName(): ?string + { + $clusterName = $this->getEnv('K8S_CLUSTER_NAME'); + if ($clusterName !== false) { + return $clusterName; + } + + $clusterName = $this->getEnv('CLUSTER_NAME'); + if ($clusterName !== false) { + return $clusterName; + } + + return null; + } + + /** + * Get the node name from environment variables. + */ + private function getNodeName(): ?string + { + $nodeName = $this->getEnv('K8S_NODE_NAME'); + if ($nodeName !== false) { + return $nodeName; + } + + $nodeName = $this->getEnv('NODE_NAME'); + if ($nodeName !== false) { + return $nodeName; + } + + return null; + } + + /** + * Add Kubernetes-specific resource attributes. + */ + private function addKubernetesAttributes(array &$attributes): void + { + // Add pod attributes + $this->addPodAttributes($attributes); + + // Add container attributes + $this->addContainerAttributes($attributes); + + // Add namespace attributes + $this->addNamespaceAttributes($attributes); + + // Add node attributes + $this->addNodeAttributes($attributes); + + // Add cluster attributes + $this->addClusterAttributes($attributes); + + // Add workload resource attributes (deployment, replicaset, etc.) + $this->addWorkloadAttributes($attributes); + } + + /** + * Add pod-specific attributes. + */ + private function addPodAttributes(array &$attributes): void + { + $podName = $this->getPodName(); + if ($podName !== null) { + $attributes[ResourceAttributes::K8S_POD_NAME] = $podName; + } + + $podUid = $this->getPodUid(); + if ($podUid !== null) { + $attributes[ResourceAttributes::K8S_POD_UID] = $podUid; + } + + // Add pod labels and annotations + $this->addLabelsAndAnnotations($attributes, 'pod'); + } + + /** + * Add container-specific attributes. + */ + private function addContainerAttributes(array &$attributes): void + { + $containerName = $this->getEnv('K8S_CONTAINER_NAME'); + if ($containerName !== false) { + $attributes[ResourceAttributes::K8S_CONTAINER_NAME] = $containerName; + } + + // Container restart count + $restartCount = $this->getEnv('K8S_CONTAINER_RESTART_COUNT'); + if ($restartCount !== false && is_numeric($restartCount)) { + $attributes[ResourceAttributes::K8S_CONTAINER_RESTART_COUNT] = (int) $restartCount; + } + + // Last terminated reason + $lastTerminatedReason = $this->getEnv('K8S_CONTAINER_STATUS_LAST_TERMINATED_REASON'); + if ($lastTerminatedReason !== false) { + $attributes[ResourceAttributes::K8S_CONTAINER_STATUS_LAST_TERMINATED_REASON] = $lastTerminatedReason; + } + } + + /** + * Add namespace-specific attributes. + */ + private function addNamespaceAttributes(array &$attributes): void + { + $namespace = $this->getNamespace(); + if ($namespace !== null) { + $attributes[ResourceAttributes::K8S_NAMESPACE_NAME] = $namespace; + } + + // Add namespace labels and annotations + $this->addLabelsAndAnnotations($attributes, 'namespace'); + } + + /** + * Add node-specific attributes. + */ + private function addNodeAttributes(array &$attributes): void + { + $nodeName = $this->getNodeName(); + if ($nodeName !== null) { + $attributes[ResourceAttributes::K8S_NODE_NAME] = $nodeName; + } + + $nodeUid = $this->getEnv('K8S_NODE_UID'); + if ($nodeUid !== false) { + $attributes[ResourceAttributes::K8S_NODE_UID] = $nodeUid; + } + + // Add node labels and annotations + $this->addLabelsAndAnnotations($attributes, 'node'); + } + + /** + * Add cluster-specific attributes. + */ + private function addClusterAttributes(array &$attributes): void + { + $clusterName = $this->getClusterName(); + if ($clusterName !== null) { + $attributes[ResourceAttributes::K8S_CLUSTER_NAME] = $clusterName; + } + + $clusterUid = $this->getEnv('K8S_CLUSTER_UID'); + if ($clusterUid !== false) { + $attributes[ResourceAttributes::K8S_CLUSTER_UID] = $clusterUid; + } + } + + /** + * Add workload resource attributes (deployment, replicaset, etc.). + */ + private function addWorkloadAttributes(array &$attributes): void + { + $workloadTypes = [ + 'deployment', + 'replicaset', + 'statefulset', + 'daemonset', + 'job', + 'cronjob', + 'replicationcontroller', + ]; + + foreach ($workloadTypes as $type) { + $this->addWorkloadTypeAttributes($attributes, $type); + } + } + + /** + * Add attributes for a specific workload type. + */ + private function addWorkloadTypeAttributes(array &$attributes, string $type): void + { + $nameKey = strtoupper("K8S_{$type}_NAME"); + $uidKey = strtoupper("K8S_{$type}_UID"); + + $name = $this->getEnv($nameKey); + if ($name !== false) { + $nameConstant = $this->getResourceAttributeConstant($type, 'name'); + $attributes[$nameConstant] = $name; + } + + $uid = $this->getEnv($uidKey); + if ($uid !== false) { + $uidConstant = $this->getResourceAttributeConstant($type, 'uid'); + $attributes[$uidConstant] = $uid; + } + + // Add labels and annotations for this workload type + $this->addLabelsAndAnnotations($attributes, $type); + } + + /** + * Get the ResourceAttributes constant for a given workload type and attribute. + */ + private function getResourceAttributeConstant(string $type, string $attribute): string + { + return match ($type) { + 'deployment' => match ($attribute) { + 'name' => ResourceAttributes::K8S_DEPLOYMENT_NAME, + 'uid' => ResourceAttributes::K8S_DEPLOYMENT_UID, + default => "k8s.{$type}.{$attribute}", + }, + 'replicaset' => match ($attribute) { + 'name' => ResourceAttributes::K8S_REPLICASET_NAME, + 'uid' => ResourceAttributes::K8S_REPLICASET_UID, + default => "k8s.{$type}.{$attribute}", + }, + 'statefulset' => match ($attribute) { + 'name' => ResourceAttributes::K8S_STATEFULSET_NAME, + 'uid' => ResourceAttributes::K8S_STATEFULSET_UID, + default => "k8s.{$type}.{$attribute}", + }, + 'daemonset' => match ($attribute) { + 'name' => ResourceAttributes::K8S_DAEMONSET_NAME, + 'uid' => ResourceAttributes::K8S_DAEMONSET_UID, + default => "k8s.{$type}.{$attribute}", + }, + 'job' => match ($attribute) { + 'name' => ResourceAttributes::K8S_JOB_NAME, + 'uid' => ResourceAttributes::K8S_JOB_UID, + default => "k8s.{$type}.{$attribute}", + }, + 'cronjob' => match ($attribute) { + 'name' => ResourceAttributes::K8S_CRONJOB_NAME, + 'uid' => ResourceAttributes::K8S_CRONJOB_UID, + default => "k8s.{$type}.{$attribute}", + }, + 'replicationcontroller' => match ($attribute) { + 'name' => ResourceAttributes::K8S_REPLICATIONCONTROLLER_NAME, + 'uid' => ResourceAttributes::K8S_REPLICATIONCONTROLLER_UID, + default => "k8s.{$type}.{$attribute}", + }, + default => "k8s.{$type}.{$attribute}", + }; + } + + /** + * Add labels and annotations for a given resource type. + */ + private function addLabelsAndAnnotations(array &$attributes, string $resourceType): void + { + // Add labels + $this->addResourceMetadata($attributes, $resourceType, 'label'); + + // Add annotations + $this->addResourceMetadata($attributes, $resourceType, 'annotation'); + } + + /** + * Add metadata (labels or annotations) for a specific resource type. + */ + private function addResourceMetadata(array &$attributes, string $resourceType, string $metadataType): void + { + $prefix = strtoupper("K8S_{$resourceType}_{$metadataType}_"); + + // Check for environment variables with the pattern K8S___ + foreach ($_ENV as $envKey => $envValue) { + if (str_starts_with($envKey, $prefix)) { + $metadataKey = substr($envKey, strlen($prefix)); + // Convert from env var format to attribute format + // K8S_POD_LABEL_APP_KUBERNETES_IO_NAME -> app.kubernetes.io/name + $metadataKey = strtolower($metadataKey); + $metadataKey = str_replace('_kubernetes_io_', '.kubernetes.io/', $metadataKey); + $metadataKey = str_replace('_', '.', $metadataKey); + + // Use ResourceAttributes constant for base metadata type if available + $baseConstant = $this->getMetadataConstant($resourceType, $metadataType); + $attributeKey = $baseConstant . '.' . $metadataKey; + $attributes[$attributeKey] = $envValue; + } + } + + // Also check getenv() for cases where $_ENV might not be populated + // This is more limited as we can't enumerate all environment variables + $commonLabels = [ + 'app', 'app.kubernetes.io/name', 'app.kubernetes.io/instance', + 'app.kubernetes.io/version', 'app.kubernetes.io/component', + 'app.kubernetes.io/part-of', 'app.kubernetes.io/managed-by', + 'version', 'environment', 'tier', 'release', + ]; + + foreach ($commonLabels as $label) { + $envKey = $prefix . str_replace(['.', '-'], '_', strtoupper($label)); + $value = $this->getEnv($envKey); + if ($value !== false) { + $baseConstant = $this->getMetadataConstant($resourceType, $metadataType); + $attributeKey = $baseConstant . '.' . $label; + $attributes[$attributeKey] = $value; + } + } + } + + /** + * Get the ResourceAttributes constant for metadata (labels/annotations). + */ + private function getMetadataConstant(string $resourceType, string $metadataType): string + { + return match ($resourceType) { + 'pod' => match ($metadataType) { + 'label' => ResourceAttributes::K8S_POD_LABEL, + 'annotation' => ResourceAttributes::K8S_POD_ANNOTATION, + default => "k8s.{$resourceType}.{$metadataType}", + }, + 'namespace' => match ($metadataType) { + 'label' => ResourceAttributes::K8S_NAMESPACE_LABEL, + 'annotation' => ResourceAttributes::K8S_NAMESPACE_ANNOTATION, + default => "k8s.{$resourceType}.{$metadataType}", + }, + 'node' => match ($metadataType) { + 'label' => ResourceAttributes::K8S_NODE_LABEL, + 'annotation' => ResourceAttributes::K8S_NODE_ANNOTATION, + default => "k8s.{$resourceType}.{$metadataType}", + }, + 'deployment' => match ($metadataType) { + 'label' => ResourceAttributes::K8S_DEPLOYMENT_LABEL, + 'annotation' => ResourceAttributes::K8S_DEPLOYMENT_ANNOTATION, + default => "k8s.{$resourceType}.{$metadataType}", + }, + 'replicaset' => match ($metadataType) { + 'label' => ResourceAttributes::K8S_REPLICASET_LABEL, + 'annotation' => ResourceAttributes::K8S_REPLICASET_ANNOTATION, + default => "k8s.{$resourceType}.{$metadataType}", + }, + 'statefulset' => match ($metadataType) { + 'label' => ResourceAttributes::K8S_STATEFULSET_LABEL, + 'annotation' => ResourceAttributes::K8S_STATEFULSET_ANNOTATION, + default => "k8s.{$resourceType}.{$metadataType}", + }, + 'daemonset' => match ($metadataType) { + 'label' => ResourceAttributes::K8S_DAEMONSET_LABEL, + 'annotation' => ResourceAttributes::K8S_DAEMONSET_ANNOTATION, + default => "k8s.{$resourceType}.{$metadataType}", + }, + 'job' => match ($metadataType) { + 'label' => ResourceAttributes::K8S_JOB_LABEL, + 'annotation' => ResourceAttributes::K8S_JOB_ANNOTATION, + default => "k8s.{$resourceType}.{$metadataType}", + }, + 'cronjob' => match ($metadataType) { + 'label' => ResourceAttributes::K8S_CRONJOB_LABEL, + 'annotation' => ResourceAttributes::K8S_CRONJOB_ANNOTATION, + default => "k8s.{$resourceType}.{$metadataType}", + }, + default => "k8s.{$resourceType}.{$metadataType}", + }; + } +} diff --git a/src/ResourceDetectors/Kubernetes/tests/Unit/KubernetesTest.php b/src/ResourceDetectors/Kubernetes/tests/Unit/KubernetesTest.php new file mode 100644 index 000000000..4b6cc5890 --- /dev/null +++ b/src/ResourceDetectors/Kubernetes/tests/Unit/KubernetesTest.php @@ -0,0 +1,314 @@ +clearKubernetesEnvironment(); + + // Since we're not running in K8s environment in tests, should return empty resource + $resourceDetector = new Kubernetes(); + $resource = $resourceDetector->getResource(); + + $this->assertCount(0, $resource->getAttributes()); + } + + public function test_kubernetes_generates_stable_instance_id(): void + { + $resourceDetector = new Kubernetes(); + + // Mock K8s environment by creating a reflection to access private methods + $reflection = new \ReflectionClass($resourceDetector); + $getStableInstanceIdMethod = $reflection->getMethod('getStableInstanceId'); + + // Call the method twice with same pod UID to ensure it's deterministic + $podUid = 'test-pod-uid-123-456-789'; + $instanceId1 = $getStableInstanceIdMethod->invoke($resourceDetector, $podUid); + $instanceId2 = $getStableInstanceIdMethod->invoke($resourceDetector, $podUid); + + $this->assertSame($instanceId1, $instanceId2); + // Should be a valid UUID format (v5) + $this->assertMatchesRegularExpression('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $instanceId1); + } + + public function test_kubernetes_environment_detection(): void + { + // Ensure clean environment + $this->clearKubernetesEnvironment(); + + $resourceDetector = new Kubernetes(); + $reflection = new \ReflectionClass($resourceDetector); + $isKubernetesEnvironmentMethod = $reflection->getMethod('isKubernetesEnvironment'); + + // Test detection logic (will be false in CLI test environment) + $result = $isKubernetesEnvironmentMethod->invoke($resourceDetector); + $this->assertFalse($result); + } + + public function test_pod_uid_detection(): void + { + $this->clearKubernetesEnvironment(); + + $resourceDetector = new Kubernetes(); + $reflection = new \ReflectionClass($resourceDetector); + $getPodUidMethod = $reflection->getMethod('getPodUid'); + + // Test with K8S_POD_UID environment variable + $_ENV['K8S_POD_UID'] = 'test-pod-uid-123'; + $podUid = $getPodUidMethod->invoke($resourceDetector); + $this->assertSame('test-pod-uid-123', $podUid); + unset($_ENV['K8S_POD_UID']); + + // Test with POD_UID fallback + $_ENV['POD_UID'] = 'fallback-pod-uid'; + $podUid = $getPodUidMethod->invoke($resourceDetector); + $this->assertSame('fallback-pod-uid', $podUid); + unset($_ENV['POD_UID']); + + // Test without any environment variable set + $podUid = $getPodUidMethod->invoke($resourceDetector); + $this->assertNull($podUid); + } + + public function test_pod_name_detection(): void + { + $this->clearKubernetesEnvironment(); + + $resourceDetector = new Kubernetes(); + $reflection = new \ReflectionClass($resourceDetector); + $getPodNameMethod = $reflection->getMethod('getPodName'); + + // Test with K8S_POD_NAME environment variable + $_ENV['K8S_POD_NAME'] = 'test-pod-name'; + $podName = $getPodNameMethod->invoke($resourceDetector); + $this->assertSame('test-pod-name', $podName); + unset($_ENV['K8S_POD_NAME']); + + // Test with POD_NAME fallback + $_ENV['POD_NAME'] = 'fallback-pod'; + $podName = $getPodNameMethod->invoke($resourceDetector); + $this->assertSame('fallback-pod', $podName); + unset($_ENV['POD_NAME']); + + // Test without environment variables (should use hostname) + $podName = $getPodNameMethod->invoke($resourceDetector); + $this->assertNotNull($podName); // Should return hostname + } + + public function test_namespace_detection(): void + { + $this->clearKubernetesEnvironment(); + + $resourceDetector = new Kubernetes(); + $reflection = new \ReflectionClass($resourceDetector); + $getNamespaceMethod = $reflection->getMethod('getNamespace'); + + // Test with K8S_NAMESPACE environment variable + $_ENV['K8S_NAMESPACE'] = 'test-namespace'; + $namespace = $getNamespaceMethod->invoke($resourceDetector); + $this->assertSame('test-namespace', $namespace); + unset($_ENV['K8S_NAMESPACE']); + + // Test with POD_NAMESPACE fallback + $_ENV['POD_NAMESPACE'] = 'fallback-namespace'; + $namespace = $getNamespaceMethod->invoke($resourceDetector); + $this->assertSame('fallback-namespace', $namespace); + unset($_ENV['POD_NAMESPACE']); + + // Test without environment variables + $namespace = $getNamespaceMethod->invoke($resourceDetector); + $this->assertNull($namespace); + } + + public function test_cluster_name_detection(): void + { + $this->clearKubernetesEnvironment(); + + $resourceDetector = new Kubernetes(); + $reflection = new \ReflectionClass($resourceDetector); + $getClusterNameMethod = $reflection->getMethod('getClusterName'); + + // Test with K8S_CLUSTER_NAME environment variable + $_ENV['K8S_CLUSTER_NAME'] = 'test-cluster'; + $clusterName = $getClusterNameMethod->invoke($resourceDetector); + $this->assertSame('test-cluster', $clusterName); + unset($_ENV['K8S_CLUSTER_NAME']); + + // Test with CLUSTER_NAME fallback + $_ENV['CLUSTER_NAME'] = 'fallback-cluster'; + $clusterName = $getClusterNameMethod->invoke($resourceDetector); + $this->assertSame('fallback-cluster', $clusterName); + unset($_ENV['CLUSTER_NAME']); + + // Test without environment variables + $clusterName = $getClusterNameMethod->invoke($resourceDetector); + $this->assertNull($clusterName); + } + + public function test_node_name_detection(): void + { + $this->clearKubernetesEnvironment(); + + $resourceDetector = new Kubernetes(); + $reflection = new \ReflectionClass($resourceDetector); + $getNodeNameMethod = $reflection->getMethod('getNodeName'); + + // Test with K8S_NODE_NAME environment variable + $_ENV['K8S_NODE_NAME'] = 'test-node'; + $nodeName = $getNodeNameMethod->invoke($resourceDetector); + $this->assertSame('test-node', $nodeName); + unset($_ENV['K8S_NODE_NAME']); + + // Test with NODE_NAME fallback + $_ENV['NODE_NAME'] = 'fallback-node'; + $nodeName = $getNodeNameMethod->invoke($resourceDetector); + $this->assertSame('fallback-node', $nodeName); + unset($_ENV['NODE_NAME']); + + // Test without environment variables + $nodeName = $getNodeNameMethod->invoke($resourceDetector); + $this->assertNull($nodeName); + } + + public function test_container_attributes(): void + { + $this->clearKubernetesEnvironment(); + + // Mock a K8s environment to test container attributes + $_ENV['KUBERNETES_SERVICE_HOST'] = '10.96.0.1'; + $_ENV['K8S_POD_UID'] = 'test-pod-uid'; + $_ENV['K8S_CONTAINER_NAME'] = 'my-container'; + $_ENV['K8S_CONTAINER_RESTART_COUNT'] = '3'; + $_ENV['K8S_CONTAINER_STATUS_LAST_TERMINATED_REASON'] = 'OOMKilled'; + + $resourceDetector = new Kubernetes(); + $resource = $resourceDetector->getResource(); + $attributes = $resource->getAttributes(); + + $this->assertSame('my-container', $attributes->get(ResourceAttributes::K8S_CONTAINER_NAME)); + $this->assertSame(3, $attributes->get(ResourceAttributes::K8S_CONTAINER_RESTART_COUNT)); + $this->assertSame('OOMKilled', $attributes->get(ResourceAttributes::K8S_CONTAINER_STATUS_LAST_TERMINATED_REASON)); + } + + public function test_workload_attributes(): void + { + $this->clearKubernetesEnvironment(); + + // Mock a K8s environment with deployment info + $_ENV['KUBERNETES_SERVICE_HOST'] = '10.96.0.1'; + $_ENV['K8S_POD_UID'] = 'test-pod-uid'; + $_ENV['K8S_DEPLOYMENT_NAME'] = 'my-deployment'; + $_ENV['K8S_DEPLOYMENT_UID'] = 'deployment-uid-123'; + $_ENV['K8S_REPLICASET_NAME'] = 'my-deployment-abc123'; + $_ENV['K8S_REPLICASET_UID'] = 'replicaset-uid-456'; + + $resourceDetector = new Kubernetes(); + $resource = $resourceDetector->getResource(); + $attributes = $resource->getAttributes(); + + $this->assertSame('my-deployment', $attributes->get(ResourceAttributes::K8S_DEPLOYMENT_NAME)); + $this->assertSame('deployment-uid-123', $attributes->get(ResourceAttributes::K8S_DEPLOYMENT_UID)); + $this->assertSame('my-deployment-abc123', $attributes->get(ResourceAttributes::K8S_REPLICASET_NAME)); + $this->assertSame('replicaset-uid-456', $attributes->get(ResourceAttributes::K8S_REPLICASET_UID)); + } + + public function test_labels_and_annotations(): void + { + $this->clearKubernetesEnvironment(); + + // Mock a K8s environment with labels and annotations + $_ENV['KUBERNETES_SERVICE_HOST'] = '10.96.0.1'; + $_ENV['K8S_POD_UID'] = 'test-pod-uid'; + $_ENV['K8S_POD_LABEL_APP'] = 'my-app'; + $_ENV['K8S_POD_LABEL_APP_KUBERNETES_IO_NAME'] = 'my-service'; + $_ENV['K8S_POD_ANNOTATION_DEPLOYMENT_KUBERNETES_IO_REVISION'] = '3'; + $_ENV['K8S_NAMESPACE_LABEL_ENVIRONMENT'] = 'production'; + + $resourceDetector = new Kubernetes(); + $resource = $resourceDetector->getResource(); + $attributes = $resource->getAttributes(); + + $this->assertSame('my-app', $attributes->get(ResourceAttributes::K8S_POD_LABEL . '.app')); + $this->assertSame('my-service', $attributes->get(ResourceAttributes::K8S_POD_LABEL . '.app.kubernetes.io/name')); + $this->assertSame('3', $attributes->get(ResourceAttributes::K8S_POD_ANNOTATION . '.deployment.kubernetes.io/revision')); + $this->assertSame('production', $attributes->get(ResourceAttributes::K8S_NAMESPACE_LABEL . '.environment')); + } + + public function test_cluster_and_node_uid(): void + { + $this->clearKubernetesEnvironment(); + + // Mock a K8s environment with cluster and node UIDs + $_ENV['KUBERNETES_SERVICE_HOST'] = '10.96.0.1'; + $_ENV['K8S_POD_UID'] = 'test-pod-uid'; + $_ENV['K8S_CLUSTER_UID'] = 'cluster-uid-789'; + $_ENV['K8S_NODE_UID'] = 'node-uid-101'; + + $resourceDetector = new Kubernetes(); + $resource = $resourceDetector->getResource(); + $attributes = $resource->getAttributes(); + + $this->assertSame('cluster-uid-789', $attributes->get(ResourceAttributes::K8S_CLUSTER_UID)); + $this->assertSame('node-uid-101', $attributes->get(ResourceAttributes::K8S_NODE_UID)); + } + + private function clearKubernetesEnvironment(): void + { + // Clean up environment variables that might affect tests + $envVars = [ + 'KUBERNETES_SERVICE_HOST', + 'K8S_POD_UID', + 'POD_UID', + 'K8S_POD_NAME', + 'POD_NAME', + 'K8S_NAMESPACE', + 'POD_NAMESPACE', + 'K8S_CLUSTER_NAME', + 'CLUSTER_NAME', + 'K8S_NODE_NAME', + 'NODE_NAME', + 'K8S_CONTAINER_NAME', + 'K8S_NODE_UID', + 'K8S_CLUSTER_UID', + 'K8S_CONTAINER_RESTART_COUNT', + 'K8S_CONTAINER_STATUS_LAST_TERMINATED_REASON', + 'K8S_DEPLOYMENT_NAME', + 'K8S_DEPLOYMENT_UID', + 'K8S_REPLICASET_NAME', + 'K8S_REPLICASET_UID', + ]; + + foreach ($envVars as $var) { + unset($_ENV[$var]); + putenv($var); + } + + // Clear any label/annotation environment variables + foreach (array_keys($_ENV) as $key) { + if (str_starts_with($key, 'K8S_POD_LABEL_') || + str_starts_with($key, 'K8S_POD_ANNOTATION_') || + str_starts_with($key, 'K8S_NAMESPACE_LABEL_') || + str_starts_with($key, 'K8S_NAMESPACE_ANNOTATION_')) { + unset($_ENV[$key]); + putenv($key); + } + } + } + + protected function tearDown(): void + { + $this->clearKubernetesEnvironment(); + parent::tearDown(); + } +}