diff --git a/src/Config/SDK/ComponentProvider/Detector/Apache.php b/src/Config/SDK/ComponentProvider/Detector/Apache.php new file mode 100644 index 000000000..8c2199e8f --- /dev/null +++ b/src/Config/SDK/ComponentProvider/Detector/Apache.php @@ -0,0 +1,32 @@ + + */ +final class Apache implements ComponentProvider +{ + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): ResourceDetectorInterface + { + return new ApacheDetector(); + } + + public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $builder): ArrayNodeDefinition + { + return $builder->arrayNode('apache'); + } +} diff --git a/src/Config/SDK/ComponentProvider/Detector/Fpm.php b/src/Config/SDK/ComponentProvider/Detector/Fpm.php new file mode 100644 index 000000000..39ed7f522 --- /dev/null +++ b/src/Config/SDK/ComponentProvider/Detector/Fpm.php @@ -0,0 +1,32 @@ + + */ +final class Fpm implements ComponentProvider +{ + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): ResourceDetectorInterface + { + return new FpmDetector(); + } + + public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $builder): ArrayNodeDefinition + { + return $builder->arrayNode('fpm'); + } +} diff --git a/src/Config/SDK/ComponentProvider/Detector/Kubernetes.php b/src/Config/SDK/ComponentProvider/Detector/Kubernetes.php new file mode 100644 index 000000000..a77c176ff --- /dev/null +++ b/src/Config/SDK/ComponentProvider/Detector/Kubernetes.php @@ -0,0 +1,32 @@ + + */ +final class Kubernetes implements ComponentProvider +{ + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): ResourceDetectorInterface + { + return new KubernetesDetector(); + } + + public function getConfig(ComponentProviderRegistry $registry, NodeBuilder $builder): ArrayNodeDefinition + { + return $builder->arrayNode('kubernetes'); + } +} diff --git a/src/Config/SDK/composer.json b/src/Config/SDK/composer.json index b093f99bf..57d0dba23 100644 --- a/src/Config/SDK/composer.json +++ b/src/Config/SDK/composer.json @@ -75,8 +75,11 @@ "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorBatch", "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorSimple", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Detector\\Apache", "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Detector\\Composer", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Detector\\Fpm", "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Detector\\Host", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Detector\\Kubernetes", "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Detector\\Process", "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Instrumentation\\General\\HttpConfigProvider", diff --git a/src/Contrib/Otlp/MetricConverter.php b/src/Contrib/Otlp/MetricConverter.php index eb46f547f..44fa429f2 100644 --- a/src/Contrib/Otlp/MetricConverter.php +++ b/src/Contrib/Otlp/MetricConverter.php @@ -204,9 +204,7 @@ private function convertHistogramDataPoint(SDK\Metrics\Data\HistogramDataPoint $ $pHistogramDataPoint->setTimeUnixNano($dataPoint->timestamp); $pHistogramDataPoint->setCount($dataPoint->count); $pHistogramDataPoint->setSum($dataPoint->sum); - /** @phpstan-ignore-next-line */ $pHistogramDataPoint->setBucketCounts($dataPoint->bucketCounts); - /** @phpstan-ignore-next-line */ $pHistogramDataPoint->setExplicitBounds($dataPoint->explicitBounds); foreach ($dataPoint->exemplars as $exemplar) { /** @psalm-suppress InvalidArgument */ diff --git a/src/Contrib/Otlp/ProtobufSerializer.php b/src/Contrib/Otlp/ProtobufSerializer.php index c73f3f4d3..ffc741521 100644 --- a/src/Contrib/Otlp/ProtobufSerializer.php +++ b/src/Contrib/Otlp/ProtobufSerializer.php @@ -103,7 +103,7 @@ private static function serializeToJsonString(Message $message): string // @phan-suppress-next-line PhanUndeclaredClassReference if (\class_exists(\Google\Protobuf\PrintOptions::class)) { try { - /** @psalm-suppress TooManyArguments @phan-suppress-next-line PhanParamTooManyInternal,PhanUndeclaredClassConstant @phpstan-ignore arguments.count */ + /** @psalm-suppress TooManyArguments @phan-suppress-next-line PhanParamTooManyInternal,PhanUndeclaredClassConstant */ return $message->serializeToJsonString(\Google\Protobuf\PrintOptions::ALWAYS_PRINT_ENUMS_AS_INTS); } catch (\TypeError) { // google/protobuf ^4.31 w/ ext-protobuf <4.31 installed diff --git a/src/SDK/Common/Configuration/KnownValues.php b/src/SDK/Common/Configuration/KnownValues.php index 02c8f2eb7..8bd6b9122 100644 --- a/src/SDK/Common/Configuration/KnownValues.php +++ b/src/SDK/Common/Configuration/KnownValues.php @@ -198,6 +198,9 @@ interface KnownValues public const VALUE_DETECTORS_SDK_PROVIDED = 'sdk_provided'; public const VALUE_DETECTORS_SERVICE = 'service'; public const VALUE_DETECTORS_COMPOSER = 'composer'; + public const VALUE_DETECTORS_APACHE = 'apache'; + public const VALUE_DETECTORS_FPM = 'fpm'; + public const VALUE_DETECTORS_KUBERNETES = 'k8s'; public const OTEL_PHP_DETECTORS = [ self::VALUE_ALL, self::VALUE_DETECTORS_ENVIRONMENT, @@ -208,6 +211,9 @@ interface KnownValues self::VALUE_DETECTORS_SDK, self::VALUE_DETECTORS_SDK_PROVIDED, self::VALUE_DETECTORS_COMPOSER, + self::VALUE_DETECTORS_APACHE, + self::VALUE_DETECTORS_FPM, + self::VALUE_DETECTORS_KUBERNETES, self::VALUE_NONE, ]; public const OTEL_PHP_LOG_DESTINATION = [ diff --git a/src/SDK/Resource/Detectors/Apache.php b/src/SDK/Resource/Detectors/Apache.php new file mode 100644 index 000000000..c1d7a6418 --- /dev/null +++ b/src/SDK/Resource/Detectors/Apache.php @@ -0,0 +1,139 @@ +isApacheSapi()) { + return ResourceInfoFactory::emptyResource(); + } + + $attributes = [ + ResourceAttributes::SERVICE_INSTANCE_ID => $this->getStableInstanceId(), + ]; + + // Add service name if configured + $serviceName = Configuration::has(Variables::OTEL_SERVICE_NAME) + ? Configuration::getString(Variables::OTEL_SERVICE_NAME) + : null; + + if ($serviceName !== null) { + $attributes[ResourceAttributes::SERVICE_NAME] = $serviceName; + } + + // Add Apache-specific attributes + if (function_exists('apache_get_version')) { + $attributes[ResourceAttributes::WEBENGINE_NAME] = 'apache'; + $apacheFullVersion = apache_get_version(); + + // 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[ResourceAttributes::WEBENGINE_VERSION] = $versionNumber; + } + + // webengine.description should contain detailed version and edition information + $attributes[ResourceAttributes::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), ResourceAttributes::SCHEMA_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/SDK/Resource/Detectors/Fpm.php b/src/SDK/Resource/Detectors/Fpm.php new file mode 100644 index 000000000..420cd01cb --- /dev/null +++ b/src/SDK/Resource/Detectors/Fpm.php @@ -0,0 +1,104 @@ + $this->getStableInstanceId(), + ]; + + // Add service name if configured + $serviceName = Configuration::has(Variables::OTEL_SERVICE_NAME) + ? Configuration::getString(Variables::OTEL_SERVICE_NAME) + : null; + + if ($serviceName !== null) { + $attributes[ResourceAttributes::SERVICE_NAME] = $serviceName; + } + + // 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), ResourceAttributes::SCHEMA_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('6ba7b810-9dad-11d1-80b4-00c04fd430c8'); // 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/SDK/Resource/Detectors/Kubernetes.php b/src/SDK/Resource/Detectors/Kubernetes.php new file mode 100644 index 000000000..c01a6e3ba --- /dev/null +++ b/src/SDK/Resource/Detectors/Kubernetes.php @@ -0,0 +1,546 @@ +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 service name if configured + $serviceName = Configuration::has(Variables::OTEL_SERVICE_NAME) + ? Configuration::getString(Variables::OTEL_SERVICE_NAME) + : null; + + if ($serviceName !== null) { + $attributes[ResourceAttributes::SERVICE_NAME] = $serviceName; + } + + // Add Kubernetes-specific attributes + $this->addKubernetesAttributes($attributes); + + return ResourceInfo::create(Attributes::create($attributes), ResourceAttributes::SCHEMA_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('6ba7b810-9dad-11d1-80b4-00c04fd430c8'); // 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 $_ENV[$name]; + } + + return getenv($name); + } + + /** + * 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/SDK/Resource/ResourceInfoFactory.php b/src/SDK/Resource/ResourceInfoFactory.php index 0a073436d..79fa345b4 100644 --- a/src/SDK/Resource/ResourceInfoFactory.php +++ b/src/SDK/Resource/ResourceInfoFactory.php @@ -30,9 +30,12 @@ public static function defaultResource(): ResourceInfo new Detectors\Host(), new Detectors\Process(), ...Registry::resourceDetectors(), - new Detectors\Environment(), + new Detectors\Environment(), // OTEL_RESOURCE_ATTRIBUTES new Detectors\Sdk(), - new Detectors\Service(), + new Detectors\Service(), // OTEL_SERVICE_NAME overrides OTEL_RESOURCE_ATTRIBUTES + new Detectors\Apache(), // Override Service UUID with stable ID for Apache + new Detectors\Fpm(), // Override Service UUID with stable ID for FPM + new Detectors\Kubernetes(), // Override Service UUID with stable ID for K8s ]))->getResource(); } @@ -56,6 +59,18 @@ public static function defaultResource(): ResourceInfo case Values::VALUE_DETECTORS_COMPOSER: $resourceDetectors[] = new Detectors\Composer(); + break; + case Values::VALUE_DETECTORS_APACHE: + $resourceDetectors[] = new Detectors\Apache(); + + break; + case Values::VALUE_DETECTORS_FPM: + $resourceDetectors[] = new Detectors\Fpm(); + + break; + case Values::VALUE_DETECTORS_KUBERNETES: + $resourceDetectors[] = new Detectors\Kubernetes(); + break; case Values::VALUE_DETECTORS_SDK_PROVIDED: //deprecated case Values::VALUE_DETECTORS_OS: //deprecated @@ -73,6 +88,8 @@ public static function defaultResource(): ResourceInfo } $resourceDetectors [] = new Detectors\Sdk(); $resourceDetectors [] = new Detectors\Service(); + $resourceDetectors [] = new Detectors\Apache(); // Override Service UUID with stable ID for Apache + $resourceDetectors [] = new Detectors\Fpm(); // Override Service UUID with stable ID for FPM return (new Detectors\Composite($resourceDetectors))->getResource(); } diff --git a/tests/Integration/SDK/Resource/CascadingDetectorsTest.php b/tests/Integration/SDK/Resource/CascadingDetectorsTest.php new file mode 100644 index 000000000..9c557d0fe --- /dev/null +++ b/tests/Integration/SDK/Resource/CascadingDetectorsTest.php @@ -0,0 +1,129 @@ +getResource(); + + $instanceId = $resource->getAttributes()->get(ResourceAttributes::SERVICE_INSTANCE_ID); + + // Service detector should provide a UUID + $this->assertIsString($instanceId); + $this->assertMatchesRegularExpression('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $instanceId); + } + + public function test_apache_and_fpm_override_service_uuid_when_applicable(): void + { + // Test that when composed, Apache/FPM detectors override Service detector's UUID + + $composite = new Composite([ + new Service(), // Generates UUID first + new Apache(), // Should override if running under Apache (empty in CLI) + new Fpm(), // Should override if running under FPM (empty in CLI) + ]); + + $resource = $composite->getResource(); + $instanceId = $resource->getAttributes()->get(ResourceAttributes::SERVICE_INSTANCE_ID); + + // Since we're running in CLI, Apache and FPM return empty resources, + // so Service detector's UUID should remain + $this->assertIsString($instanceId); + $this->assertMatchesRegularExpression('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $instanceId); + } + + public function test_detector_order_matters_for_overriding(): void + { + // Test that detector order affects which values are used (later wins) + + $serviceFirst = new Composite([ + new Service(), + new Apache(), + new Fpm(), + ]); + + $serviceAfter = new Composite([ + new Apache(), + new Fpm(), + new Service(), // This would override any stable IDs with UUID + ]); + + $resourceServiceFirst = $serviceFirst->getResource(); + $resourceServiceAfter = $serviceAfter->getResource(); + + $instanceIdFirst = $resourceServiceFirst->getAttributes()->get(ResourceAttributes::SERVICE_INSTANCE_ID); + $instanceIdAfter = $resourceServiceAfter->getAttributes()->get(ResourceAttributes::SERVICE_INSTANCE_ID); + + // Both should be UUIDs in CLI environment, but demonstrate order matters + $this->assertIsString($instanceIdFirst); + $this->assertIsString($instanceIdAfter); + + // In CLI environment, both will be UUIDs, but they'll be the same static UUID + // since Service detector uses static variable + $this->assertEquals($instanceIdFirst, $instanceIdAfter); + } + + public function test_stable_id_generation_consistency(): void + { + // Test that runtime detectors generate consistent IDs + + $apache = new Apache(); + $fpm = new Fpm(); + + // Get resources multiple times to verify consistency + $apacheResource1 = $apache->getResource(); + $apacheResource2 = $apache->getResource(); + + $fpmResource1 = $fpm->getResource(); + $fpmResource2 = $fpm->getResource(); + + // In CLI environment, these should be empty resources (no attributes) + $this->assertCount(0, $apacheResource1->getAttributes()); + $this->assertCount(0, $apacheResource2->getAttributes()); + $this->assertCount(0, $fpmResource1->getAttributes()); + $this->assertCount(0, $fpmResource2->getAttributes()); + } + + public function test_environment_variables_override_all_detectors(): void + { + // Test that Environment detector (via OTEL_RESOURCE_ATTRIBUTES) has highest priority + + // Set up environment variable to override service instance ID + $_SERVER['OTEL_RESOURCE_ATTRIBUTES'] = 'service.instance.id=custom-override-id,service.name=test-service'; + $_ENV['OTEL_RESOURCE_ATTRIBUTES'] = 'service.instance.id=custom-override-id,service.name=test-service'; + + try { + $composite = new Composite([ + new Service(), // Would generate UUID + new Apache(), // Would generate stable ID if in Apache + new Fpm(), // Would generate stable ID if in FPM + new \OpenTelemetry\SDK\Resource\Detectors\Environment(), // Should override all + ]); + + $resource = $composite->getResource(); + $instanceId = $resource->getAttributes()->get(ResourceAttributes::SERVICE_INSTANCE_ID); + $serviceName = $resource->getAttributes()->get(ResourceAttributes::SERVICE_NAME); + + // Environment should override with our custom value + $this->assertEquals('custom-override-id', $instanceId); + $this->assertEquals('test-service', $serviceName); + + } finally { + // Clean up environment variables + unset($_SERVER['OTEL_RESOURCE_ATTRIBUTES']); + unset($_ENV['OTEL_RESOURCE_ATTRIBUTES']); + } + } +} diff --git a/tests/Unit/SDK/Resource/Detectors/ApacheTest.php b/tests/Unit/SDK/Resource/Detectors/ApacheTest.php new file mode 100644 index 000000000..a55e5cb74 --- /dev/null +++ b/tests/Unit/SDK/Resource/Detectors/ApacheTest.php @@ -0,0 +1,122 @@ +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'); + $getStableInstanceIdMethod->setAccessible(true); + + // 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'); + $isApacheSapiMethod->setAccessible(true); + + // 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'); + $getServerNameMethod->setAccessible(true); + + // 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'); + $getDocumentRootMethod->setAccessible(true); + + // 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'); + $extractVersionMethod->setAccessible(true); + + // 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/tests/Unit/SDK/Resource/Detectors/FpmTest.php b/tests/Unit/SDK/Resource/Detectors/FpmTest.php new file mode 100644 index 000000000..7d5b90428 --- /dev/null +++ b/tests/Unit/SDK/Resource/Detectors/FpmTest.php @@ -0,0 +1,64 @@ +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'); + $getStableInstanceIdMethod->setAccessible(true); + + // 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'); + $getFpmPoolNameMethod->setAccessible(true); + + // 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/tests/Unit/SDK/Resource/Detectors/KubernetesTest.php b/tests/Unit/SDK/Resource/Detectors/KubernetesTest.php new file mode 100644 index 000000000..d4fab0279 --- /dev/null +++ b/tests/Unit/SDK/Resource/Detectors/KubernetesTest.php @@ -0,0 +1,321 @@ +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'); + $getStableInstanceIdMethod->setAccessible(true); + + // 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'); + $isKubernetesEnvironmentMethod->setAccessible(true); + + // 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'); + $getPodUidMethod->setAccessible(true); + + // 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'); + $getPodNameMethod->setAccessible(true); + + // 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'); + $getNamespaceMethod->setAccessible(true); + + // 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'); + $getClusterNameMethod->setAccessible(true); + + // 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'); + $getNodeNameMethod->setAccessible(true); + + // 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 ($_ENV as $key => $value) { + 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(); + } +}