Skip to content
32 changes: 32 additions & 0 deletions src/Config/SDK/ComponentProvider/Detector/Apache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Config\SDK\ComponentProvider\Detector;

use OpenTelemetry\Config\SDK\Configuration\ComponentProvider;
use OpenTelemetry\Config\SDK\Configuration\ComponentProviderRegistry;
use OpenTelemetry\Config\SDK\Configuration\Context;
use OpenTelemetry\SDK\Resource\Detectors\Apache as ApacheDetector;
use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;

/**
* @implements ComponentProvider<ResourceDetectorInterface>
*/
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');
}
}
32 changes: 32 additions & 0 deletions src/Config/SDK/ComponentProvider/Detector/Fpm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Config\SDK\ComponentProvider\Detector;

use OpenTelemetry\Config\SDK\Configuration\ComponentProvider;
use OpenTelemetry\Config\SDK\Configuration\ComponentProviderRegistry;
use OpenTelemetry\Config\SDK\Configuration\Context;
use OpenTelemetry\SDK\Resource\Detectors\Fpm as FpmDetector;
use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;

/**
* @implements ComponentProvider<ResourceDetectorInterface>
*/
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');
}
}
32 changes: 32 additions & 0 deletions src/Config/SDK/ComponentProvider/Detector/Kubernetes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Config\SDK\ComponentProvider\Detector;

use OpenTelemetry\Config\SDK\Configuration\ComponentProvider;
use OpenTelemetry\Config\SDK\Configuration\ComponentProviderRegistry;
use OpenTelemetry\Config\SDK\Configuration\Context;
use OpenTelemetry\SDK\Resource\Detectors\Kubernetes as KubernetesDetector;
use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;

/**
* @implements ComponentProvider<ResourceDetectorInterface>
*/
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');
}
}
3 changes: 3 additions & 0 deletions src/Config/SDK/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions src/Contrib/Otlp/MetricConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
2 changes: 1 addition & 1 deletion src/Contrib/Otlp/ProtobufSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/SDK/Common/Configuration/KnownValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = [
Expand Down
139 changes: 139 additions & 0 deletions src/SDK/Resource/Detectors/Apache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\SDK\Resource\Detectors;

use function apache_get_version;
use function function_exists;
use function gethostname;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Configuration\Configuration;
use OpenTelemetry\SDK\Common\Configuration\Variables;

use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SemConv\ResourceAttributes;
use function php_sapi_name;
use Ramsey\Uuid\Uuid;

/**
* Apache resource detector that provides stable service instance IDs to avoid high cardinality issues.
*
* For Apache mod_php environments, generates a stable instance ID based on the server name and hostname
* rather than using random UUIDs which cause cardinality explosion in metrics.
*/
final class Apache implements ResourceDetectorInterface
{
public function getResource(): ResourceInfo
{
// Only activate for Apache SAPI
if (!$this->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;
}
Comment on lines +40 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this should not be part of the detectors. Users can use these detectors together with the service detector if they want to read OTEL_SERVICE_NAME.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. This duplicates the service detector, which is mandatory since a recent change (so it should always run). In practice, that might also clobber the service.instance.id that this detector sets? I think we need an integration test for ServiceInfoFactory, because users generally rely on this to set up resources, and it applies the service detector last.


// 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;
}
}
104 changes: 104 additions & 0 deletions src/SDK/Resource/Detectors/Fpm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\SDK\Resource\Detectors;

use function function_exists;
use function gethostname;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Configuration\Configuration;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;

use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SemConv\ResourceAttributes;
use function php_sapi_name;
use Ramsey\Uuid\Uuid;

/**
* FPM resource detector that provides stable service instance IDs to avoid high cardinality issues.
*
* For FPM environments, generates a stable instance ID based on the pool name and hostname
* rather than using random UUIDs which cause cardinality explosion in metrics.
*/
final class Fpm implements ResourceDetectorInterface
{
public function getResource(): ResourceInfo
{
// Only activate for FPM SAPI
if (php_sapi_name() !== 'fpm-fcgi') {
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 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;
}
}
Loading