diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index dcd49a8c6..8bd021faf 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -21,7 +21,7 @@ jobs: experimental: true composer_args: "--ignore-platform-reqs" env: - extensions: ast, grpc, protobuf + extensions: ast, grpc, opentelemetry, protobuf steps: - name: Set cache key diff --git a/.phan/config.php b/.phan/config.php index 7914c189a..a1258a628 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -373,6 +373,7 @@ 'vendor/composer', 'vendor/grpc/grpc/src/lib', 'vendor/guzzlehttp', + 'vendor/tbachert/spi/src', 'vendor/psr', 'vendor/php-http', 'vendor/phpunit/phpunit/src', diff --git a/composer.json b/composer.json index 40567c6eb..64f33fcf2 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "symfony/config": "^5.4 || ^6.4 || ^7.0", "symfony/polyfill-mbstring": "^1.23", "symfony/polyfill-php82": "^1.26", - "tbachert/spi": "^0.2" + "tbachert/spi": ">= 0.2.1" }, "config": { "sort-packages": true, @@ -101,6 +101,7 @@ "phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-phpunit": "^1.3", "phpunit/phpunit": "^10 || ^11", + "sebastian/exporter": "<= 6.0.1 || >= 6.1.3", "symfony/http-client": "^5.2", "symfony/var-exporter": "^5.4 || ^6.4 || ^7.0", "symfony/yaml": "^5.4 || ^6.4 || ^7.0" @@ -148,7 +149,18 @@ "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordExporterConsole", "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordExporterOtlp", "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorBatch", - "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorSimple" + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorSimple", + + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Instrumentation\\General\\HttpConfigProvider", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Instrumentation\\General\\PeerConfigProvider", + + "OpenTelemetry\\Example\\ExampleConfigProvider" + ], + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ], + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\Instrumentation": [ + "OpenTelemetry\\Example\\ExampleInstrumentation" ] } } diff --git a/deptrac.yaml b/deptrac.yaml index f9ad414d9..d1098460c 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -25,6 +25,10 @@ deptrac: collectors: - type: directory value: src/SDK/.* + - name: ConfigSDK + collectors: + - type: directory + value: src/Config/SDK/.* - name: Context collectors: - type: directory @@ -85,6 +89,18 @@ deptrac: value: ^GuzzleHttp\\* - type: className value: ^Buzz\\* + - name: SPI + collectors: + - type: className + value: ^Nevay\\SPI\\* + - name: SymfonyConfig + collectors: + - type: className + value: ^Symfony\\Component\\Config\\* + - type: className + value: ^Symfony\\Component\\Yaml\\* + - type: className + value: ^Symfony\\Component\\VarExporter\\* - name: RamseyUuid collectors: - type: className @@ -94,16 +110,29 @@ deptrac: Context: - FFI SemConv: ~ + ConfigSDK: + - SymfonyConfig + - API + - SDK + - SPI + - PsrLog + - Composer + - Context + - Contrib + - Extension API: - Context - PsrLog + - SPI SDK: - +API + - ConfigSDK - SemConv - PsrHttp - HttpPlug - Composer - HttpClients + - SPI - RamseyUuid Contrib: - +SDK diff --git a/docker/Dockerfile b/docker/Dockerfile index c78edea4f..36b37ec09 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,6 +11,7 @@ RUN chmod +x /usr/local/bin/install-php-extensions \ grpc \ intl\ opcache \ + opentelemetry \ pcntl \ protobuf \ sockets \ diff --git a/examples/instrumentation/configure_instrumentation.php b/examples/instrumentation/configure_instrumentation.php new file mode 100644 index 000000000..6910c4caa --- /dev/null +++ b/examples/instrumentation/configure_instrumentation.php @@ -0,0 +1,34 @@ +create(new Context())->setAutoShutdown(true)->buildAndRegisterGlobal(); +$configuration = \OpenTelemetry\Config\SDK\Instrumentation::parseFile(__DIR__ . '/otel-sdk.yaml')->create(); +$hookManager = new ExtensionHookManager(); +$context = new \OpenTelemetry\API\Instrumentation\AutoInstrumentation\Context(Globals::tracerProvider(), new NoopMeterProvider(), new NoopLoggerProvider()); + +foreach (ServiceLoader::load(Instrumentation::class) as $instrumentation) { + $instrumentation->register($hookManager, $configuration, $context); +} + +echo (new Example())->test(), PHP_EOL; diff --git a/examples/instrumentation/configure_instrumentation_global.php b/examples/instrumentation/configure_instrumentation_global.php new file mode 100644 index 000000000..189d4adc5 --- /dev/null +++ b/examples/instrumentation/configure_instrumentation_global.php @@ -0,0 +1,20 @@ +test(), PHP_EOL; diff --git a/examples/instrumentation/otel-sdk.yaml b/examples/instrumentation/otel-sdk.yaml new file mode 100644 index 000000000..53ce87914 --- /dev/null +++ b/examples/instrumentation/otel-sdk.yaml @@ -0,0 +1,12 @@ +file_format: '0.3' + +tracer_provider: + processors: + - simple: + exporter: + console: {} + +instrumentation: + php: + example_instrumentation: + span_name: ${EXAMPLE_INSTRUMENTATION_SPAN_NAME:-example span} diff --git a/examples/load_config.yaml b/examples/load_config.yaml index 9e1901de8..d4d8d4de4 100644 --- a/examples/load_config.yaml +++ b/examples/load_config.yaml @@ -1,4 +1,4 @@ -file_format: '0.1' +file_format: '0.3' resource: attributes: diff --git a/examples/load_config_env.yaml b/examples/load_config_env.yaml index b62262d29..b593419be 100644 --- a/examples/load_config_env.yaml +++ b/examples/load_config_env.yaml @@ -1,4 +1,4 @@ -file_format: '0.1' +file_format: '0.3' disabled: ${OTEL_SDK_DISABLED} diff --git a/examples/src/Example.php b/examples/src/Example.php new file mode 100644 index 000000000..c21077762 --- /dev/null +++ b/examples/src/Example.php @@ -0,0 +1,14 @@ + + */ +final class ExampleConfigProvider implements ComponentProvider +{ + + /** + * @psalm-suppress MoreSpecificImplementedParamType + * @param array{ + * span_name: string, + * enabled: bool, + * } $properties + */ + public function createPlugin(array $properties, Context $context): InstrumentationConfiguration + { + return new ExampleConfig( + spanName: $properties['span_name'], + enabled: $properties['enabled'], + ); + } + + /** + * @psalm-suppress UndefinedInterfaceMethod,PossiblyNullReference + */ + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $root = new ArrayNodeDefinition('example_instrumentation'); + $root + ->children() + ->scalarNode('span_name')->isRequired()->validate()->always(Validation::ensureString())->end()->end() + ->end() + ->canBeDisabled() + ; + + return $root; + } +} diff --git a/examples/src/ExampleInstrumentation.php b/examples/src/ExampleInstrumentation.php new file mode 100644 index 000000000..a29f2f6c7 --- /dev/null +++ b/examples/src/ExampleInstrumentation.php @@ -0,0 +1,52 @@ +get(ExampleConfig::class) ?? throw new Exception('example instrumentation must be configured'); + if (!$config->enabled) { + return; + } + + $tracer = $context->tracerProvider->getTracer('example-instrumentation'); + + $hookManager->hook( + Example::class, + 'test', + static function () use ($tracer, $config): void { + $context = Context::getCurrent(); + + $span = $tracer + ->spanBuilder($config->spanName) + ->setParent($context) + ->startSpan(); + + Context::storage()->attach($span->storeInContext($context)); + }, + static function (): void { + if (!$scope = Context::storage()->scope()) { + return; + } + + $scope->detach(); + + $span = Span::fromContext($scope->context()); + $span->end(); + } + ); + } +} diff --git a/psalm.xml.dist b/psalm.xml.dist index 0fe505df9..ed0d7ffc9 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -27,6 +27,11 @@ + + + + + diff --git a/src/API/Globals.php b/src/API/Globals.php index 7d296f4fc..aa3c06141 100644 --- a/src/API/Globals.php +++ b/src/API/Globals.php @@ -6,7 +6,7 @@ use function assert; use Closure; -use const E_USER_WARNING; +use OpenTelemetry\API\Behavior\LogsMessagesTrait; use OpenTelemetry\API\Instrumentation\Configurator; use OpenTelemetry\API\Instrumentation\ContextKeys; use OpenTelemetry\API\Logs\EventLoggerProviderInterface; @@ -17,13 +17,14 @@ use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface; use function sprintf; use Throwable; -use function trigger_error; /** * Provides access to the globally configured instrumentation instances. */ final class Globals { + use LogsMessagesTrait; + /** @var Closure[] */ private static array $initializers = []; private static ?self $globals = null; @@ -67,6 +68,7 @@ public static function eventLoggerProvider(): EventLoggerProviderInterface * * @internal * @psalm-internal OpenTelemetry + * @todo In a future (breaking) change, we can remove `Registry` and globals initializers, in favor of SPI. */ public static function registerInitializer(Closure $initializer): void { @@ -90,7 +92,7 @@ private static function globals(): self try { $configurator = $initializer($configurator); } catch (Throwable $e) { - trigger_error(sprintf("Error during opentelemetry initialization: %s\n%s", $e->getMessage(), $e->getTraceAsString()), E_USER_WARNING); + self::logWarning(sprintf("Error during opentelemetry initialization: %s\n%s", $e->getMessage(), $e->getTraceAsString())); } } } finally { diff --git a/src/API/Instrumentation/AutoInstrumentation/ConfigurationRegistry.php b/src/API/Instrumentation/AutoInstrumentation/ConfigurationRegistry.php new file mode 100644 index 000000000..06413c186 --- /dev/null +++ b/src/API/Instrumentation/AutoInstrumentation/ConfigurationRegistry.php @@ -0,0 +1,28 @@ +configurations[$configuration::class] = $configuration; + + return $this; + } + + /** + * @template C of InstrumentationConfiguration + * @param class-string $id + * @return C|null + */ + public function get(string $id): ?InstrumentationConfiguration + { + return $this->configurations[$id] ?? null; + } +} diff --git a/src/API/Instrumentation/AutoInstrumentation/Context.php b/src/API/Instrumentation/AutoInstrumentation/Context.php new file mode 100644 index 000000000..70005ccab --- /dev/null +++ b/src/API/Instrumentation/AutoInstrumentation/Context.php @@ -0,0 +1,25 @@ +bindHookScope($preHook), $this->bindHookScope($postHook)); + } + + private function bindHookScope(?Closure $closure): ?Closure + { + if (!$closure) { + return null; + } + + $reflection = new ReflectionFunction($closure); + + // TODO Add an option flag to ext-opentelemetry `hook` that configures whether return values should be used? + if (!$reflection->getReturnType() || (string) $reflection->getReturnType() === 'void') { + return static function (mixed ...$args) use ($closure): void { + if (HookManager::disabled()) { + return; + } + + $closure(...$args); + }; + } + + return static function (mixed ...$args) use ($closure): mixed { + if (HookManager::disabled()) { + return $args[2]; + } + + return $closure(...$args); + }; + } +} diff --git a/src/API/Instrumentation/AutoInstrumentation/GeneralInstrumentationConfiguration.php b/src/API/Instrumentation/AutoInstrumentation/GeneralInstrumentationConfiguration.php new file mode 100644 index 000000000..6360e5225 --- /dev/null +++ b/src/API/Instrumentation/AutoInstrumentation/GeneralInstrumentationConfiguration.php @@ -0,0 +1,9 @@ +with(self::contextKey(), true); + } + + public static function disable(?ContextInterface $context = null): ContextInterface + { + $context ??= Context::getCurrent(); + + return $context->with(self::contextKey(), false); + } + + public static function disabled(?ContextInterface $context = null): bool + { + $context ??= Context::getCurrent(); + + return $context->get(self::contextKey()) === false; + } + + private static function contextKey(): ContextKeyInterface + { + static $contextKey; + + return $contextKey ??= Context::createKey(self::class); + } +} diff --git a/src/API/Instrumentation/AutoInstrumentation/HookManagerInterface.php b/src/API/Instrumentation/AutoInstrumentation/HookManagerInterface.php new file mode 100644 index 000000000..29e11da7b --- /dev/null +++ b/src/API/Instrumentation/AutoInstrumentation/HookManagerInterface.php @@ -0,0 +1,18 @@ +logger ??= ($this->factory)())->emit($logRecord); + } +} diff --git a/src/API/Logs/LateBindingLoggerProvider.php b/src/API/Logs/LateBindingLoggerProvider.php new file mode 100644 index 000000000..755325de1 --- /dev/null +++ b/src/API/Logs/LateBindingLoggerProvider.php @@ -0,0 +1,24 @@ +loggerProvider?->getLogger($name, $version, $schemaUrl, $attributes) + ?? new LateBindingLogger(fn (): LoggerInterface => ($this->loggerProvider ??= ($this->factory)())->getLogger($name, $version, $schemaUrl, $attributes)); + } +} diff --git a/src/API/Metrics/LateBindingMeter.php b/src/API/Metrics/LateBindingMeter.php new file mode 100644 index 000000000..b146bfbbb --- /dev/null +++ b/src/API/Metrics/LateBindingMeter.php @@ -0,0 +1,61 @@ +meter ??= ($this->factory)())->batchObserve($callback, $instrument, ...$instruments); + } + + public function createCounter(string $name, ?string $unit = null, ?string $description = null, array $advisory = []): CounterInterface + { + return ($this->meter ??= ($this->factory)())->createCounter($name, $unit, $description, $advisory); + } + + public function createObservableCounter(string $name, ?string $unit = null, ?string $description = null, callable|array $advisory = [], callable ...$callbacks): ObservableCounterInterface + { + return ($this->meter ??= ($this->factory)())->createObservableCounter($name, $unit, $description, $advisory, $callbacks); + } + + public function createHistogram(string $name, ?string $unit = null, ?string $description = null, array $advisory = []): HistogramInterface + { + return ($this->meter ??= ($this->factory)())->createHistogram($name, $unit, $description, $advisory); + } + + public function createGauge(string $name, ?string $unit = null, ?string $description = null, array $advisory = []): GaugeInterface + { + return ($this->meter ??= ($this->factory)())->createGauge($name, $unit, $description, $advisory); + } + + public function createObservableGauge(string $name, ?string $unit = null, ?string $description = null, callable|array $advisory = [], callable ...$callbacks): ObservableGaugeInterface + { + return ($this->meter ??= ($this->factory)())->createObservableGauge($name, $unit, $description, $advisory, $callbacks); + } + + public function createUpDownCounter(string $name, ?string $unit = null, ?string $description = null, array $advisory = []): UpDownCounterInterface + { + return ($this->meter ??= ($this->factory)())->createUpDownCounter($name, $unit, $description, $advisory); + } + + public function createObservableUpDownCounter(string $name, ?string $unit = null, ?string $description = null, callable|array $advisory = [], callable ...$callbacks): ObservableUpDownCounterInterface + { + return ($this->meter ??= ($this->factory)())->createObservableUpDownCounter($name, $unit, $description, $advisory, $callbacks); + } +} diff --git a/src/API/Metrics/LateBindingMeterProvider.php b/src/API/Metrics/LateBindingMeterProvider.php new file mode 100644 index 000000000..39238f03f --- /dev/null +++ b/src/API/Metrics/LateBindingMeterProvider.php @@ -0,0 +1,24 @@ +meterProvider?->getMeter($name, $version, $schemaUrl, $attributes) + ?? new LateBindingMeter(fn (): MeterInterface => ($this->meterProvider ??= ($this->factory)())->getMeter($name, $version, $schemaUrl, $attributes)); + } +} diff --git a/src/API/Trace/LateBindingTracer.php b/src/API/Trace/LateBindingTracer.php new file mode 100644 index 000000000..9f3defcb2 --- /dev/null +++ b/src/API/Trace/LateBindingTracer.php @@ -0,0 +1,23 @@ +tracer ??= ($this->factory)())->spanBuilder($spanName); + } +} diff --git a/src/API/Trace/LateBindingTracerProvider.php b/src/API/Trace/LateBindingTracerProvider.php new file mode 100644 index 000000000..84552f427 --- /dev/null +++ b/src/API/Trace/LateBindingTracerProvider.php @@ -0,0 +1,30 @@ +tracerProvider?->getTracer($name, $version, $schemaUrl, $attributes) + ?? new LateBindingTracer(fn (): TracerInterface => ($this->tracerProvider ??= ($this->factory)())->getTracer($name, $version, $schemaUrl, $attributes)); + } +} diff --git a/src/Config/SDK/ComponentProvider/Instrumentation/General/HttpConfigProvider.php b/src/Config/SDK/ComponentProvider/Instrumentation/General/HttpConfigProvider.php new file mode 100644 index 000000000..01c86f73c --- /dev/null +++ b/src/Config/SDK/ComponentProvider/Instrumentation/General/HttpConfigProvider.php @@ -0,0 +1,54 @@ + + */ +class HttpConfigProvider implements ComponentProvider +{ + + public function createPlugin(array $properties, Context $context): GeneralInstrumentationConfiguration + { + return new HttpConfig($properties); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('http'); + $node + ->children() + ->append($this->capturedHeaders('client')) + ->append($this->capturedHeaders('server')) + ->end() + ; + + return $node; + } + + private function capturedHeaders(string $name): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition($name); + $node + ->children() + ->arrayNode('request_captured_headers') + ->scalarPrototype()->end() + ->end() + ->arrayNode('response_captured_headers') + ->scalarPrototype()->end() + ->end() + ->end() + ; + + return $node; + } +} diff --git a/src/Config/SDK/ComponentProvider/Instrumentation/General/PeerConfigProvider.php b/src/Config/SDK/ComponentProvider/Instrumentation/General/PeerConfigProvider.php new file mode 100644 index 000000000..f9273a422 --- /dev/null +++ b/src/Config/SDK/ComponentProvider/Instrumentation/General/PeerConfigProvider.php @@ -0,0 +1,42 @@ + + */ +class PeerConfigProvider implements ComponentProvider +{ + public function createPlugin(array $properties, Context $context): GeneralInstrumentationConfiguration + { + return new PeerConfig($properties); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('peer'); + $node + ->children() + ->arrayNode('service_mapping') + ->arrayPrototype() + ->children() + ->scalarNode('peer')->end() + ->scalarNode('service')->end() + ->end() + ->end() + ->end() + ->end() + ; + + return $node; + } +} diff --git a/src/Config/SDK/ComponentProvider/InstrumentationConfigurationRegistry.php b/src/Config/SDK/ComponentProvider/InstrumentationConfigurationRegistry.php new file mode 100644 index 000000000..44f4ae4c7 --- /dev/null +++ b/src/Config/SDK/ComponentProvider/InstrumentationConfigurationRegistry.php @@ -0,0 +1,64 @@ + + * @implements ComponentProvider + */ +class InstrumentationConfigurationRegistry implements ComponentProvider +{ + /** + * @param array{ + * instrumentation: array{ + * php: list>, + * general: list> + * } + * } $properties + */ + public function createPlugin(array $properties, Context $context): ConfigurationRegistry + { + $configurationRegistry = new ConfigurationRegistry(); + /** @phpstan-ignore-next-line */ + foreach ($properties['instrumentation']['php'] ?? [] as $configuration) { + $configurationRegistry->add($configuration->create($context)); + } + /** @phpstan-ignore-next-line */ + foreach ($properties['instrumentation']['general'] ?? [] as $configuration) { + $configurationRegistry->add($configuration->create($context)); + } + + return $configurationRegistry; + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $root = new ArrayNodeDefinition('open_telemetry'); + $root + ->ignoreExtraKeys() + ->children() + ->arrayNode('instrumentation') + ->ignoreExtraKeys() + ->children() + ->append($registry->componentList('php', InstrumentationConfiguration::class)) + ->append($registry->componentList('general', GeneralInstrumentationConfiguration::class)) + ->end() + ->end() + ->end() + ; + + return $root; + } +} diff --git a/src/Config/SDK/ComponentProvider/OpenTelemetrySdk.php b/src/Config/SDK/ComponentProvider/OpenTelemetrySdk.php index 730164e6c..3cd1c1b44 100644 --- a/src/Config/SDK/ComponentProvider/OpenTelemetrySdk.php +++ b/src/Config/SDK/ComponentProvider/OpenTelemetrySdk.php @@ -52,7 +52,7 @@ final class OpenTelemetrySdk implements ComponentProvider /** * @param array{ - * file_format: '0.1', + * file_format: '0.3', * disabled: bool, * resource: array{ * attributes: array, @@ -263,7 +263,7 @@ public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinit ->isRequired() ->example('0.1') ->validate()->always(Validation::ensureString())->end() - ->validate()->ifNotInArray(['0.1'])->thenInvalid('unsupported version')->end() + ->validate()->ifNotInArray(['0.3'])->thenInvalid('unsupported version')->end() ->end() ->booleanNode('disabled')->defaultFalse()->end() ->append($this->getResourceConfig()) @@ -323,7 +323,7 @@ private function getTracerProviderConfig(ComponentProviderRegistry $registry): A ->end() ->end() ->append($registry->component('sampler', SamplerInterface::class)) - ->append($registry->componentList('processors', SpanProcessorInterface::class)) + ->append($registry->componentArrayList('processors', SpanProcessorInterface::class)) ->end() ; @@ -374,7 +374,7 @@ private function getMeterProviderConfig(ComponentProviderRegistry $registry): Ar ->end() ->end() ->end() - ->append($registry->componentList('readers', MetricReaderInterface::class)) + ->append($registry->componentArrayList('readers', MetricReaderInterface::class)) ->end() ; @@ -394,7 +394,7 @@ private function getLoggerProviderConfig(ComponentProviderRegistry $registry): A ->integerNode('attribute_count_limit')->min(0)->defaultNull()->end() ->end() ->end() - ->append($registry->componentList('processors', LogRecordProcessorInterface::class)) + ->append($registry->componentArrayList('processors', LogRecordProcessorInterface::class)) ->end() ; diff --git a/src/Config/SDK/Configuration/ComponentProvider.php b/src/Config/SDK/Configuration/ComponentProvider.php index 18c605d83..86e079d73 100644 --- a/src/Config/SDK/Configuration/ComponentProvider.php +++ b/src/Config/SDK/Configuration/ComponentProvider.php @@ -33,6 +33,7 @@ public function createPlugin(array $properties, Context $context): mixed; * * @see ComponentProviderRegistry::component() * @see ComponentProviderRegistry::componentList() + * @see ComponentProviderRegistry::componentArrayList() * @see ComponentProviderRegistry::componentNames() */ public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition; diff --git a/src/Config/SDK/Configuration/ComponentProviderRegistry.php b/src/Config/SDK/Configuration/ComponentProviderRegistry.php index c1d1f3f30..363ca638a 100644 --- a/src/Config/SDK/Configuration/ComponentProviderRegistry.php +++ b/src/Config/SDK/Configuration/ComponentProviderRegistry.php @@ -37,6 +37,26 @@ public function component(string $name, string $type): NodeDefinition; * * ``` * $name: + * provider1: + * property: value + * anotherProperty: value + * provider2: + * property: value + * anotherProperty: value + * ``` + * + * @param string $name name of configuration node + * @param string $type type of the component plugin + */ + public function componentList(string $name, string $type): ArrayNodeDefinition; + + /** + * Creates a node to specify a list of component plugins represented as an array. + * + * `$name: list>` + * + * ``` + * $name: * - provider1: * property: value * anotherProperty: value @@ -48,8 +68,7 @@ public function component(string $name, string $type): NodeDefinition; * @param string $name name of configuration node * @param string $type type of the component plugin */ - public function componentList(string $name, string $type): ArrayNodeDefinition; - + public function componentArrayList(string $name, string $type): ArrayNodeDefinition; /** * Creates a node to specify a list of component plugin names. * diff --git a/src/Config/SDK/Configuration/ConfigurationFactory.php b/src/Config/SDK/Configuration/ConfigurationFactory.php index cb4987a46..66ce09c32 100644 --- a/src/Config/SDK/Configuration/ConfigurationFactory.php +++ b/src/Config/SDK/Configuration/ConfigurationFactory.php @@ -6,6 +6,7 @@ use function class_exists; use Exception; +use function getcwd; use function is_file; use OpenTelemetry\Config\SDK\Configuration\Environment\EnvReader; use OpenTelemetry\Config\SDK\Configuration\Environment\EnvResourceChecker; @@ -98,7 +99,7 @@ public function parseFile( } $loader = new ConfigurationLoader($resources); - $locator = new FileLocator(); + $locator = new FileLocator(getcwd()); $fileLoader = new DelegatingLoader(new LoaderResolver([ new YamlSymfonyFileLoader($loader, $locator), new YamlExtensionFileLoader($loader, $locator), @@ -115,7 +116,7 @@ public function parseFile( class_exists(VarExporter::class) ? sprintf('toArray() //@todo $resources possible null + $resources->toArray() ); return $configuration; diff --git a/src/Config/SDK/Configuration/Internal/ComponentProviderRegistry.php b/src/Config/SDK/Configuration/Internal/ComponentProviderRegistry.php index 472716636..1873f4d3a 100644 --- a/src/Config/SDK/Configuration/Internal/ComponentProviderRegistry.php +++ b/src/Config/SDK/Configuration/Internal/ComponentProviderRegistry.php @@ -69,6 +69,14 @@ public function component(string $name, string $type): NodeDefinition } public function componentList(string $name, string $type): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition($name); + $this->applyToArrayNode($node, $type, true); + + return $node; + } + + public function componentArrayList(string $name, string $type): ArrayNodeDefinition { $node = new ArrayNodeDefinition($name); $this->applyToArrayNode($node->arrayPrototype(), $type); @@ -107,7 +115,7 @@ public function componentNames(string $name, string $type): ArrayNodeDefinition return $node; } - private function applyToArrayNode(ArrayNodeDefinition $node, string $type): void + private function applyToArrayNode(ArrayNodeDefinition $node, string $type, bool $forceArray = false): void { $node->info(sprintf('Component "%s"', $type)); $node->performNoDeepMerging(); @@ -122,21 +130,35 @@ private function applyToArrayNode(ArrayNodeDefinition $node, string $type): void } } - $node->validate()->always(function (array $value) use ($type): ComponentPlugin { - if (count($value) !== 1) { - throw new InvalidArgumentException(sprintf( - 'Component "%s" must have exactly one element defined, got %s', - $type, - implode(', ', array_map(json_encode(...), array_keys($value)) ?: ['none']) - )); - } + if ($forceArray) { + // if the config was a map rather than an array, force it back to an array + $node->validate()->always(function (array $value) use ($type): array { + $validated = []; + foreach ($value as $name => $v) { + $provider = $this->providers[$type][$name]; + $this->resources?->addClassResource($provider); + $validated[] = new ComponentPlugin($v, $this->providers[$type][$name]); + } + + return $validated; + }); + } else { + $node->validate()->always(function (array $value) use ($type): ComponentPlugin { + if (count($value) !== 1) { + throw new InvalidArgumentException(sprintf( + 'Component "%s" must have exactly one element defined, got %s', + $type, + implode(', ', array_map(json_encode(...), array_keys($value)) ?: ['none']) + )); + } - $name = array_key_first($value); - $provider = $this->providers[$type][$name]; - $this->resources?->addClassResource($provider); + $name = array_key_first($value); + $provider = $this->providers[$type][$name]; + $this->resources?->addClassResource($provider); - return new ComponentPlugin($value[$name], $this->providers[$type][$name]); - }); + return new ComponentPlugin($value[$name], $this->providers[$type][$name]); + }); + } } /** diff --git a/src/Config/SDK/Instrumentation.php b/src/Config/SDK/Instrumentation.php new file mode 100644 index 000000000..aa974d8bf --- /dev/null +++ b/src/Config/SDK/Instrumentation.php @@ -0,0 +1,63 @@ + $plugin + */ + private function __construct( + private readonly ComponentPlugin $plugin, + ) { + } + + public function create(Context $context = new Context()): ConfigurationRegistry + { + $plugin = $this->plugin; + + return $plugin->create($context); + } + + /** + * @param string|list $file + */ + public static function parseFile( + string|array $file, + ?string $cacheFile = null, + bool $debug = true, + ): Instrumentation { + return new self(self::factory()->parseFile($file, $cacheFile, $debug)); + } + + /** + * @return ConfigurationFactory + */ + private static function factory(): ConfigurationFactory + { + static $factory; + + return $factory ??= new ConfigurationFactory( + ServiceLoader::load(ComponentProvider::class), + new InstrumentationConfigurationRegistry(), + new EnvSourceReader([ + new ServerEnvSource(), + new PhpIniEnvSource(), + ]), + ); + } +} diff --git a/src/Config/SDK/composer.json b/src/Config/SDK/composer.json index 75285bb65..8cb1259ae 100644 --- a/src/Config/SDK/composer.json +++ b/src/Config/SDK/composer.json @@ -21,7 +21,7 @@ "open-telemetry/context": "^1.0", "open-telemetry/sdk": "^1.0", "symfony/config": "^5.4 || ^6.4 || ^7.0", - "tbachert/spi": "^0.2" + "tbachert/spi": ">= 0.2.1" }, "autoload": { "psr-4": { @@ -64,7 +64,10 @@ "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordExporterConsole", "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordExporterOtlp", "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorBatch", - "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorSimple" + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorSimple", + + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Instrumentation\\General\\HttpConfigProvider", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Instrumentation\\General\\PeerConfigProvider" ] } } diff --git a/src/SDK/Common/Configuration/Defaults.php b/src/SDK/Common/Configuration/Defaults.php index fcfea6e4e..52709a15c 100644 --- a/src/SDK/Common/Configuration/Defaults.php +++ b/src/SDK/Common/Configuration/Defaults.php @@ -119,4 +119,5 @@ interface Defaults public const OTEL_PHP_DISABLED_INSTRUMENTATIONS = []; public const OTEL_PHP_LOGS_PROCESSOR = 'batch'; public const OTEL_PHP_LOG_DESTINATION = 'default'; + public const OTEL_EXPERIMENTAL_CONFIG_FILE = 'sdk-config.yaml'; } diff --git a/src/SDK/Common/Configuration/Variables.php b/src/SDK/Common/Configuration/Variables.php index 8ccb28ac1..f256b4526 100644 --- a/src/SDK/Common/Configuration/Variables.php +++ b/src/SDK/Common/Configuration/Variables.php @@ -140,4 +140,5 @@ interface Variables public const OTEL_PHP_INTERNAL_METRICS_ENABLED = 'OTEL_PHP_INTERNAL_METRICS_ENABLED'; //whether the SDK should emit its own metrics public const OTEL_PHP_DISABLED_INSTRUMENTATIONS = 'OTEL_PHP_DISABLED_INSTRUMENTATIONS'; public const OTEL_PHP_EXCLUDED_URLS = 'OTEL_PHP_EXCLUDED_URLS'; + public const OTEL_EXPERIMENTAL_CONFIG_FILE = 'OTEL_EXPERIMENTAL_CONFIG_FILE'; } diff --git a/src/SDK/Registry.php b/src/SDK/Registry.php index 32ffe1522..300a8a40c 100644 --- a/src/SDK/Registry.php +++ b/src/SDK/Registry.php @@ -16,6 +16,7 @@ /** * A registry to enable central registration of components that the SDK requires but which may be provided * by non-SDK modules, such as contrib and extension. + * @todo [breaking] deprecate this mechanism of setting up components, in favor of using SPI. */ class Registry { diff --git a/src/SDK/SdkAutoloader.php b/src/SDK/SdkAutoloader.php index f204d8540..564bb565c 100644 --- a/src/SDK/SdkAutoloader.php +++ b/src/SDK/SdkAutoloader.php @@ -4,9 +4,24 @@ namespace OpenTelemetry\SDK; -use InvalidArgumentException; +use Nevay\SPI\ServiceLoader; +use OpenTelemetry\API\Behavior\LogsMessagesTrait; use OpenTelemetry\API\Globals; +use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Context as InstrumentationContext; +use OpenTelemetry\API\Instrumentation\AutoInstrumentation\HookManager; +use OpenTelemetry\API\Instrumentation\AutoInstrumentation\HookManagerInterface; +use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Instrumentation; +use OpenTelemetry\API\Instrumentation\AutoInstrumentation\NoopHookManager; use OpenTelemetry\API\Instrumentation\Configurator; +use OpenTelemetry\API\Logs\LateBindingLoggerProvider; +use OpenTelemetry\API\Logs\LoggerProviderInterface; +use OpenTelemetry\API\Metrics\LateBindingMeterProvider; +use OpenTelemetry\API\Metrics\MeterProviderInterface; +use OpenTelemetry\API\Trace\LateBindingTracerProvider; +use OpenTelemetry\API\Trace\TracerProviderInterface; +use OpenTelemetry\Config\SDK\Configuration as SdkConfiguration; +use OpenTelemetry\Config\SDK\Instrumentation as SdkInstrumentation; +use OpenTelemetry\Context\Context; use OpenTelemetry\SDK\Common\Configuration\Configuration; use OpenTelemetry\SDK\Common\Configuration\Variables; use OpenTelemetry\SDK\Common\Util\ShutdownHandler; @@ -19,76 +34,167 @@ use OpenTelemetry\SDK\Trace\SamplerFactory; use OpenTelemetry\SDK\Trace\SpanProcessorFactory; use OpenTelemetry\SDK\Trace\TracerProviderBuilder; +use Throwable; /** * @psalm-suppress RedundantCast */ class SdkAutoloader { + use LogsMessagesTrait; + public static function autoload(): bool { if (!self::isEnabled() || self::isExcludedUrl()) { return false; } - Globals::registerInitializer(function (Configurator $configurator) { - $propagator = (new PropagatorFactory())->create(); - if (Sdk::isDisabled()) { - //@see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#general-sdk-configuration - return $configurator->withPropagator($propagator); - } - $emitMetrics = Configuration::getBoolean(Variables::OTEL_PHP_INTERNAL_METRICS_ENABLED); - - $resource = ResourceInfoFactory::defaultResource(); - $exporter = (new ExporterFactory())->create(); - $meterProvider = (new MeterProviderFactory())->create($resource); - $spanProcessor = (new SpanProcessorFactory())->create($exporter, $emitMetrics ? $meterProvider : null); - $tracerProvider = (new TracerProviderBuilder()) - ->addSpanProcessor($spanProcessor) - ->setResource($resource) - ->setSampler((new SamplerFactory())->create()) - ->build(); + if (Configuration::has(Variables::OTEL_EXPERIMENTAL_CONFIG_FILE)) { + Globals::registerInitializer(fn ($configurator) => self::fileBasedInitializer($configurator)); + } else { + Globals::registerInitializer(fn ($configurator) => self::environmentBasedInitializer($configurator)); + } + self::registerInstrumentations(); - $loggerProvider = (new LoggerProviderFactory())->create($emitMetrics ? $meterProvider : null, $resource); - $eventLoggerProvider = (new EventLoggerProviderFactory())->create($loggerProvider); + return true; + } - ShutdownHandler::register($tracerProvider->shutdown(...)); - ShutdownHandler::register($meterProvider->shutdown(...)); - ShutdownHandler::register($loggerProvider->shutdown(...)); + private static function environmentBasedInitializer(Configurator $configurator): Configurator + { + $propagator = (new PropagatorFactory())->create(); + if (Sdk::isDisabled()) { + //@see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#general-sdk-configuration + return $configurator->withPropagator($propagator); + } + $emitMetrics = Configuration::getBoolean(Variables::OTEL_PHP_INTERNAL_METRICS_ENABLED); - return $configurator - ->withTracerProvider($tracerProvider) - ->withMeterProvider($meterProvider) - ->withLoggerProvider($loggerProvider) - ->withEventLoggerProvider($eventLoggerProvider) - ->withPropagator($propagator) - ; - }); + $resource = ResourceInfoFactory::defaultResource(); + $exporter = (new ExporterFactory())->create(); + $meterProvider = (new MeterProviderFactory())->create($resource); + $spanProcessor = (new SpanProcessorFactory())->create($exporter, $emitMetrics ? $meterProvider : null); + $tracerProvider = (new TracerProviderBuilder()) + ->addSpanProcessor($spanProcessor) + ->setResource($resource) + ->setSampler((new SamplerFactory())->create()) + ->build(); - return true; + $loggerProvider = (new LoggerProviderFactory())->create($emitMetrics ? $meterProvider : null, $resource); + $eventLoggerProvider = (new EventLoggerProviderFactory())->create($loggerProvider); + + ShutdownHandler::register($tracerProvider->shutdown(...)); + ShutdownHandler::register($meterProvider->shutdown(...)); + ShutdownHandler::register($loggerProvider->shutdown(...)); + + return $configurator + ->withTracerProvider($tracerProvider) + ->withMeterProvider($meterProvider) + ->withLoggerProvider($loggerProvider) + ->withEventLoggerProvider($eventLoggerProvider) + ->withPropagator($propagator) + ; } /** - * Test whether a request URI is set, and if it matches the excluded urls configuration option - * - * @internal + * @phan-suppress PhanPossiblyUndeclaredVariable */ - public static function isIgnoredUrl(): bool + private static function fileBasedInitializer(Configurator $configurator): Configurator { - $ignoreUrls = Configuration::getList(Variables::OTEL_PHP_EXCLUDED_URLS, []); - if ($ignoreUrls === []) { - return false; + $file = Configuration::getString(Variables::OTEL_EXPERIMENTAL_CONFIG_FILE); + $config = SdkConfiguration::parseFile($file); + + //disable hook manager during SDK to avoid autoinstrumenting SDK exporters. + $scope = HookManager::disable(Context::getCurrent())->activate(); + + try { + $sdk = $config + ->create() + ->setAutoShutdown(true) + ->build(); + } finally { + $scope->detach(); } - $url = $_SERVER['REQUEST_URI'] ?? null; - if (!$url) { - return false; + + return $configurator + ->withTracerProvider($sdk->getTracerProvider()) + ->withMeterProvider($sdk->getMeterProvider()) + ->withLoggerProvider($sdk->getLoggerProvider()) + ->withPropagator($sdk->getPropagator()) + ->withEventLoggerProvider($sdk->getEventLoggerProvider()) + ; + } + + /** + * Register all {@link Instrumentation} configured through SPI + * @psalm-suppress ArgumentTypeCoercion + */ + private static function registerInstrumentations(): void + { + $files = Configuration::has(Variables::OTEL_EXPERIMENTAL_CONFIG_FILE) + ? Configuration::getList(Variables::OTEL_EXPERIMENTAL_CONFIG_FILE) + : []; + $configuration = SdkInstrumentation::parseFile($files)->create(); + $hookManager = self::getHookManager(); + $tracerProvider = self::createLateBindingTracerProvider(); + $meterProvider = self::createLateBindingMeterProvider(); + $loggerProvider = self::createLateBindingLoggerProvider(); + $context = new InstrumentationContext($tracerProvider, $meterProvider, $loggerProvider); + + foreach (ServiceLoader::load(Instrumentation::class) as $instrumentation) { + /** @var Instrumentation $instrumentation */ + try { + $instrumentation->register($hookManager, $configuration, $context); + } catch (Throwable $t) { + self::logError(sprintf('Unable to load instrumentation: %s', $instrumentation::class), ['exception' => $t]); + } + } - foreach ($ignoreUrls as $ignore) { - if (preg_match(sprintf('|%s|', $ignore), (string) $url) === 1) { - return true; + } + + private static function createLateBindingTracerProvider(): TracerProviderInterface + { + return new LateBindingTracerProvider(static function (): TracerProviderInterface { + $scope = Context::getRoot()->activate(); + + try { + return Globals::tracerProvider(); + } finally { + $scope->detach(); + } + }); + } + + private static function createLateBindingMeterProvider(): MeterProviderInterface + { + return new LateBindingMeterProvider(static function (): MeterProviderInterface { + $scope = Context::getRoot()->activate(); + + try { + return Globals::meterProvider(); + } finally { + $scope->detach(); } + }); + } + private static function createLateBindingLoggerProvider(): LoggerProviderInterface + { + return new LateBindingLoggerProvider(static function (): LoggerProviderInterface { + $scope = Context::getRoot()->activate(); + + try { + return Globals::loggerProvider(); + } finally { + $scope->detach(); + } + }); + } + + private static function getHookManager(): HookManagerInterface + { + /** @var HookManagerInterface $hookManager */ + foreach (ServiceLoader::load(HookManagerInterface::class) as $hookManager) { + return $hookManager; } - return false; + return new NoopHookManager(); } /** @@ -96,14 +202,7 @@ public static function isIgnoredUrl(): bool */ public static function isEnabled(): bool { - try { - $enabled = Configuration::getBoolean(Variables::OTEL_PHP_AUTOLOAD_ENABLED); - } catch (InvalidArgumentException) { - //invalid setting, assume false - return false; - } - - return $enabled; + return Configuration::getBoolean(Variables::OTEL_PHP_AUTOLOAD_ENABLED); } /** diff --git a/src/SDK/composer.json b/src/SDK/composer.json index 1c16a0046..972ab9ad1 100644 --- a/src/SDK/composer.json +++ b/src/SDK/composer.json @@ -30,7 +30,8 @@ "psr/log": "^1.1|^2.0|^3.0", "ramsey/uuid": "^3.0 || ^4.0", "symfony/polyfill-mbstring": "^1.23", - "symfony/polyfill-php82": "^1.26" + "symfony/polyfill-php82": "^1.26", + "tbachert/spi": ">= 0.2.1" }, "autoload": { "psr-4": { @@ -53,6 +54,11 @@ "extra": { "branch-alias": { "dev-main": "1.0.x-dev" + }, + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] } } } diff --git a/tests/Integration/Config/configurations/anchors.yaml b/tests/Integration/Config/configurations/anchors.yaml index 18e409d1a..1d821aeac 100644 --- a/tests/Integration/Config/configurations/anchors.yaml +++ b/tests/Integration/Config/configurations/anchors.yaml @@ -1,6 +1,6 @@ # anchors.yaml demonstrates anchor substitution to reuse OTLP exporter configuration across signals. -file_format: "0.1" +file_format: "0.3" exporters: otlp: &otlp-exporter protocol: http/protobuf diff --git a/tests/Integration/Config/configurations/kitchen-sink.yaml b/tests/Integration/Config/configurations/kitchen-sink.yaml index 1ebebeea4..261fff6e8 100644 --- a/tests/Integration/Config/configurations/kitchen-sink.yaml +++ b/tests/Integration/Config/configurations/kitchen-sink.yaml @@ -6,7 +6,7 @@ # Configuration values are set to their defaults when default values are defined. # The file format version -file_format: "0.1" +file_format: "0.3" # Configure if the SDK is disabled or not. This is not required to be provided # to ensure the SDK isn't disabled, the default value when this is not provided @@ -350,3 +350,107 @@ resource: service.name: !!str "unknown_service" # Configure the resource schema URL. schema_url: https://opentelemetry.io/schemas/1.26.0 + +# Configure instrumentation. +instrumentation: + # Configure general SemConv options that may apply to multiple languages and instrumentations. + # + # Instrumenation may merge general config options with the language specific configuration at .instrumentation.. + general: + # Configure instrumentations following the peer semantic conventions. + # + # See peer semantic conventions: https://opentelemetry.io/docs/specs/semconv/attributes-registry/peer/ + peer: + # Configure the service mapping for instrumentations following peer.service semantic conventions. + # + # Each entry is a key value pair where "peer" defines the IP address and "service" defines the corresponding logical name of the service. + # + # See peer.service semantic conventions: https://opentelemetry.io/docs/specs/semconv/general/attributes/#general-remote-service-attributes + service_mapping: + - peer: 1.2.3.4 + service: FooService + - peer: 2.3.4.5 + service: BarService + # Configure instrumentations following the http semantic conventions. + # + # See http semantic conventions: https://opentelemetry.io/docs/specs/semconv/http/ + http: + # Configure instrumentations following the http client semantic conventions. + client: + # Configure headers to capture for outbound http requests. + request_captured_headers: + - Content-Type + - Accept + # Configure headers to capture for outbound http responses. + response_captured_headers: + - Content-Type + - Content-Encoding + # Configure instrumentations following the http server semantic conventions. + server: + # Configure headers to capture for inbound http requests. + request_captured_headers: + - Content-Type + - Accept + # Configure headers to capture for outbound http responses. + response_captured_headers: + - Content-Type + - Content-Encoding + # Configure language-specific instrumentation libraries. + # + # Keys may refer to instrumentation libraries or collections of related configuration. Because there is no central schema defining the keys or their contents, instrumentation must carefully document their schema and avoid key collisions with other instrumentations. + # + # Configure C++ language-specific instrumentation libraries. + cpp: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure .NET language-specific instrumentation libraries. + dotnet: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure Erlang language-specific instrumentation libraries. + erlang: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure Go language-specific instrumentation libraries. + go: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure Java language-specific instrumentation libraries. + java: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure JavaScript language-specific instrumentation libraries. + js: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure PHP language-specific instrumentation libraries. + php: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure Python language-specific instrumentation libraries. + python: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure Ruby language-specific instrumentation libraries. + ruby: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure Rust language-specific instrumentation libraries. + rust: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" + # Configure Swift language-specific instrumentation libraries. + swift: + # Configure the instrumentation corresponding to key "example". + example: + property: "value" \ No newline at end of file diff --git a/tests/Unit/API/Instrumentation/AutoInstrumentation/ConfigurationRegistryTest.php b/tests/Unit/API/Instrumentation/AutoInstrumentation/ConfigurationRegistryTest.php new file mode 100644 index 000000000..0df4eeabb --- /dev/null +++ b/tests/Unit/API/Instrumentation/AutoInstrumentation/ConfigurationRegistryTest.php @@ -0,0 +1,23 @@ +add($config); + + $this->assertSame($config, $registry->get($config::class)); + } +} diff --git a/tests/Unit/API/Instrumentation/AutoInstrumentation/ContextTest.php b/tests/Unit/API/Instrumentation/AutoInstrumentation/ContextTest.php new file mode 100644 index 000000000..e5cb4c238 --- /dev/null +++ b/tests/Unit/API/Instrumentation/AutoInstrumentation/ContextTest.php @@ -0,0 +1,24 @@ +assertInstanceOf(NoopTracerProvider::class, $context->tracerProvider); + $this->assertInstanceOf(NoopMeterProvider::class, $context->meterProvider); + $this->assertInstanceOf(NoopLoggerProvider::class, $context->loggerProvider); + } +} diff --git a/tests/Unit/API/Instrumentation/AutoInstrumentation/ExtensionHookManagerTest.php b/tests/Unit/API/Instrumentation/AutoInstrumentation/ExtensionHookManagerTest.php new file mode 100644 index 000000000..09f254128 --- /dev/null +++ b/tests/Unit/API/Instrumentation/AutoInstrumentation/ExtensionHookManagerTest.php @@ -0,0 +1,141 @@ +markTestSkipped(); + } + $tracerProvider = $this->createMock(TracerProviderInterface::class); + $this->scope = Configurator::create() + ->withTracerProvider($tracerProvider) + ->activate(); + $this->registry = new ConfigurationRegistry(); + $this->hookManager = new ExtensionHookManager(); + $this->context = new InstrumentationContext( + $tracerProvider, + $this->createMock(MeterProviderInterface::class), + $this->createMock(LoggerProviderInterface::class) + ); + } + + public function tearDown(): void + { + $this->scope->detach(); + } + + public function test_modify_return_value_from_post_hook(): void + { + $target = new class() { + public function test(): int + { + return 1; + } + }; + $instrumentation = $this->createInstrumentation($target::class, 'test', function () { + }, function (): int { + return 99; + }); + $instrumentation->register($this->hookManager, $this->registry, $this->context); + + $returnVal = $target->test(); + $this->assertSame(99, $returnVal); + } + + public function test_hook_manager_disabled(): void + { + $target = new class() { + public function test(): int + { + return 2; + } + }; + $instrumentation = $this->createInstrumentation($target::class, 'test', function () { + }, function (): int { + $this->fail('post hook not expected to be called'); + }); + $instrumentation->register($this->hookManager, $this->registry, $this->context); + + $scope = HookManager::disable(Context::getCurrent())->activate(); + + try { + $returnVal = $target->test(); + } finally { + $scope->detach(); + } + $this->assertSame(2, $returnVal, 'original value, since hook did not run'); + } + + public function test_disable_hook_manager_after_use(): void + { + $target = new class() { + public function test(): int + { + return 3; + } + }; + $instrumentation = $this->createInstrumentation($target::class, 'test', function () { + }, function (): int { + return 123; + }); + $instrumentation->register($this->hookManager, $this->registry, $this->context); + $this->assertSame(123, $target->test(), 'post hook function ran and modified return value'); + + $scope = HookManager::disable(Context::getCurrent())->activate(); + + try { + $this->assertSame(3, $target->test(), 'post hook function did not run'); + } finally { + $scope->detach(); + } + } + + private function createInstrumentation(string $class, string $method, $pre, $post): Instrumentation + { + return new class($class, $method, $pre, $post) implements Instrumentation { + private $pre; + private $post; + + public function __construct( + private readonly string $class, + private readonly string $method, + ?callable $pre = null, + ?callable $post = null, + ) { + $this->pre = $pre; + $this->post = $post; + } + + public function register(HookManagerInterface $hookManager, ConfigurationRegistry $configuration, InstrumentationContext $context): void + { + $hookManager->hook($this->class, $this->method, $this->pre, $this->post); + } + }; + } +} diff --git a/tests/Unit/API/Instrumentation/AutoInstrumentation/HookManagerTest.php b/tests/Unit/API/Instrumentation/AutoInstrumentation/HookManagerTest.php new file mode 100644 index 000000000..8c726f624 --- /dev/null +++ b/tests/Unit/API/Instrumentation/AutoInstrumentation/HookManagerTest.php @@ -0,0 +1,40 @@ +assertFalse(HookManager::disabled($context)); + + $context = HookManager::disable($context); + $this->assertTrue(HookManager::disabled($context)); + + $context = HookManager::enable($context); + $this->assertFalse(HookManager::disabled($context)); + } + + public function test_global_disable(): void + { + $this->assertFalse(HookManager::disabled()); + $scope = HookManager::disable()->activate(); + + try { + $this->assertTrue(HookManager::disabled()); + } finally { + $scope->detach(); + } + $this->assertFalse(HookManager::disabled()); + } +} diff --git a/tests/Unit/API/Instrumentation/AutoInstrumentation/LateBindingProviderTest.php b/tests/Unit/API/Instrumentation/AutoInstrumentation/LateBindingProviderTest.php new file mode 100644 index 000000000..a8eba6b0f --- /dev/null +++ b/tests/Unit/API/Instrumentation/AutoInstrumentation/LateBindingProviderTest.php @@ -0,0 +1,133 @@ +tracerProvider->getTracer('test'); + } + public function getMeter(): MeterInterface + { + assert(self::$context !== null); + + return self::$context->meterProvider->getMeter('test'); + } + public function getLogger(): LoggerInterface + { + assert(self::$context !== null); + + return self::$context->loggerProvider->getLogger('test'); + } + }; + $this->setEnvironmentVariable(Variables::OTEL_PHP_AUTOLOAD_ENABLED, 'true'); + $tracer_accessed = false; + $logger_accessed = false; + $meter_accessed = false; + + $tracerProvider = $this->createMock(TracerProviderInterface::class); + $tracerProvider->method('getTracer')->willReturnCallback(function () use (&$tracer_accessed): TracerInterface { + $tracer_accessed = true; + + return $this->createMock(TracerInterface::class); + }); + $meterProvider = $this->createMock(MeterProviderInterface::class); + $meterProvider->method('getMeter')->willReturnCallback(function () use (&$meter_accessed): MeterInterface { + $meter_accessed = true; + + return $this->createMock(MeterInterface::class); + }); + $loggerProvider = $this->createMock(LoggerProviderInterface::class); + $loggerProvider->method('getLogger')->willReturnCallback(function () use (&$logger_accessed): LoggerInterface { + $logger_accessed = true; + + return $this->createMock(LoggerInterface::class); + }); + ServiceLoader::register(Instrumentation::class, $instrumentation::class); + $this->assertTrue(SdkAutoloader::autoload()); + //initializer added _after_ autoloader has run and instrumentation registered + Globals::registerInitializer(function (Configurator $configurator) use ($tracerProvider, $loggerProvider, $meterProvider): Configurator { + return $configurator + ->withTracerProvider($tracerProvider) + ->withMeterProvider($meterProvider) + ->withLoggerProvider($loggerProvider) + ; + }); + + $this->assertFalse($tracer_accessed); + $tracer = $instrumentation->getTracer(); + $this->assertFalse($tracer_accessed); + $tracer->spanBuilder('test-span'); /** @phpstan-ignore-next-line */ + $this->assertTrue($tracer_accessed); + + $this->assertFalse($meter_accessed); + $meter = $instrumentation->getMeter(); + $this->assertFalse($meter_accessed); + $meter->createCounter('cnt'); /** @phpstan-ignore-next-line */ + $this->assertTrue($meter_accessed); + + $this->assertFalse($logger_accessed); + $logger = $instrumentation->getLogger(); + $this->assertFalse($logger_accessed); + $logger->emit(new LogRecord()); /** @phpstan-ignore-next-line */ + $this->assertTrue($logger_accessed); + } +} diff --git a/tests/Unit/API/Instrumentation/InstrumentationTest.php b/tests/Unit/API/Instrumentation/InstrumentationTest.php index f3f85ea1e..847d0c1db 100644 --- a/tests/Unit/API/Instrumentation/InstrumentationTest.php +++ b/tests/Unit/API/Instrumentation/InstrumentationTest.php @@ -4,6 +4,8 @@ namespace OpenTelemetry\Tests\Unit\API\Instrumentation; +use OpenTelemetry\API\Behavior\Internal\Logging; +use OpenTelemetry\API\Behavior\Internal\LogWriter\LogWriterInterface; use OpenTelemetry\API\Globals; use OpenTelemetry\API\Instrumentation\CachedInstrumentation; use OpenTelemetry\API\Instrumentation\Configurator; @@ -25,7 +27,9 @@ use OpenTelemetry\Context\Propagation\NoopTextMapPropagator; use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LogLevel; #[CoversClass(Globals::class)] #[CoversClass(CachedInstrumentation::class)] @@ -33,7 +37,15 @@ #[CoversClass(ContextKeys::class)] final class InstrumentationTest extends TestCase { + private LogWriterInterface&MockObject $logWriter; + public function setUp(): void + { + $this->logWriter = $this->createMock(LogWriterInterface::class); + Logging::setLogWriter($this->logWriter); + } + + public function tearDown(): void { Globals::reset(); } @@ -131,4 +143,18 @@ public function test_initializers(): void Globals::propagator(); $this->assertTrue($called); //@phpstan-ignore-line } + + public function test_initializer_error(): void + { + $closure = function (Configurator $configurator): Configurator { + throw new \Exception('kaboom'); + }; + Globals::registerInitializer($closure); + $this->logWriter->expects($this->once())->method('write')->with( + $this->equalTo(LogLevel::WARNING), + $this->anything(), + $this->anything(), + ); + Globals::propagator(); + } } diff --git a/tests/Unit/Config/SDK/Configuration/ConfigurationFactoryTest.php b/tests/Unit/Config/SDK/Configuration/ConfigurationFactoryTest.php index af6ca88f0..04302026d 100644 --- a/tests/Unit/Config/SDK/Configuration/ConfigurationFactoryTest.php +++ b/tests/Unit/Config/SDK/Configuration/ConfigurationFactoryTest.php @@ -26,8 +26,19 @@ #[CoversClass(ConfigurationFactory::class)] final class ConfigurationFactoryTest extends TestCase { - + public string $cacheDir; public $properties; + + public function setUp(): void + { + $this->cacheDir = __DIR__ . '/configurations'; + } + + public function tearDown(): void + { + array_map('unlink', array_filter((array) glob($this->cacheDir . '/*cache*'))); + } + /** * @psalm-suppress MissingTemplateParam */ @@ -238,4 +249,15 @@ private function factory(): ConfigurationFactory ]), ); } + + public function test_cache_configuration(): void + { + $file = $this->cacheDir . '/kitchen-sink.yaml'; + $cacheFile = $this->cacheDir . '/kitchen-sink.cache'; + $this->assertFalse(file_exists($cacheFile), 'cache does not initially exist'); + $plugin = self::factory()->parseFile($file, $cacheFile); + $this->assertTrue(file_exists($cacheFile)); + $fromCache = self::factory()->parseFile($file, $cacheFile); + $this->assertEquals($fromCache, $plugin); + } } diff --git a/tests/Unit/Config/SDK/Configuration/ExampleSdk/OpenTelemetryConfiguration.php b/tests/Unit/Config/SDK/Configuration/ExampleSdk/OpenTelemetryConfiguration.php index 489f7f5b1..3731ab722 100644 --- a/tests/Unit/Config/SDK/Configuration/ExampleSdk/OpenTelemetryConfiguration.php +++ b/tests/Unit/Config/SDK/Configuration/ExampleSdk/OpenTelemetryConfiguration.php @@ -151,7 +151,7 @@ private function getTracerProviderConfig(ComponentProviderRegistry $registry): A ->end() ->end() ->append($registry->component('sampler', Sampler::class)) - ->append($registry->componentList('processors', SpanProcessor::class)) + ->append($registry->componentArrayList('processors', SpanProcessor::class)) ->end() ; @@ -203,7 +203,7 @@ private function getMeterProviderConfig(ComponentProviderRegistry $registry): Ar ->end() ->end() ->end() - ->append($registry->componentList('readers', MetricReader::class)) + ->append($registry->componentArrayList('readers', MetricReader::class)) ->end() ; @@ -223,7 +223,7 @@ private function getLoggerProviderConfig(ComponentProviderRegistry $registry): A ->integerNode('attribute_count_limit')->min(0)->defaultNull()->end() ->end() ->end() - ->append($registry->componentList('processors', LogRecordProcessor::class)) + ->append($registry->componentArrayList('processors', LogRecordProcessor::class)) ->end() ; diff --git a/tests/Unit/Config/SDK/Configuration/configurations/.gitignore b/tests/Unit/Config/SDK/Configuration/configurations/.gitignore new file mode 100644 index 000000000..69758e218 --- /dev/null +++ b/tests/Unit/Config/SDK/Configuration/configurations/.gitignore @@ -0,0 +1 @@ +**cache** diff --git a/tests/Unit/Config/SDK/Configuration/configurations/kitchen-sink.yaml b/tests/Unit/Config/SDK/Configuration/configurations/kitchen-sink.yaml index 8a8a5b800..41e9cfafc 100644 --- a/tests/Unit/Config/SDK/Configuration/configurations/kitchen-sink.yaml +++ b/tests/Unit/Config/SDK/Configuration/configurations/kitchen-sink.yaml @@ -385,4 +385,32 @@ resource: # Environment variable: OTEL_SERVICE_NAME service.name: !!str "unknown_service" # Configure the resource schema URL. - schema_url: https://opentelemetry.io/schemas/1.16.0 \ No newline at end of file + schema_url: https://opentelemetry.io/schemas/1.16.0 + +instrumentation: + php: + example_instrumentation: + span_name: ${EXAMPLE_INSTRUMENTATION_SPAN_NAME:-example span} + java: + general: + peer: + service_mapping: + - peer: 1.2.3.4 + service: FooService + - peer: 2.3.4.5 + service: BarService + http: + client: + request_captured_headers: + - Content-Type + - Accept + response_captured_headers: + - Content-Type + - Content-Encoding + server: + request_captured_headers: + - Content-Type + - Accept + response_captured_headers: + - Content-Type + - Content-Encoding diff --git a/tests/Unit/SDK/SdkAutoloaderTest.php b/tests/Unit/SDK/SdkAutoloaderTest.php index f77eedbd5..7cdded3e5 100644 --- a/tests/Unit/SDK/SdkAutoloaderTest.php +++ b/tests/Unit/SDK/SdkAutoloaderTest.php @@ -4,28 +4,39 @@ namespace OpenTelemetry\Tests\Unit\SDK; -use OpenTelemetry\API\Behavior\Internal\Logging; use OpenTelemetry\API\Globals; +use OpenTelemetry\API\Instrumentation\Configurator; +use OpenTelemetry\API\LoggerHolder; use OpenTelemetry\API\Logs\NoopEventLoggerProvider; use OpenTelemetry\API\Logs\NoopLoggerProvider; use OpenTelemetry\API\Metrics\Noop\NoopMeterProvider; use OpenTelemetry\API\Trace\NoopTracerProvider; use OpenTelemetry\Context\Propagation\NoopTextMapPropagator; +use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface; use OpenTelemetry\SDK\Common\Configuration\Variables; use OpenTelemetry\SDK\SdkAutoloader; use OpenTelemetry\Tests\TestState; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; #[CoversClass(SdkAutoloader::class)] class SdkAutoloaderTest extends TestCase { use TestState; + /** + * @var LoggerInterface&MockObject + */ + private LoggerInterface $logger; + public function setUp(): void { - Logging::disable(); + $this->logger = $this->createMock(LoggerInterface::class); + LoggerHolder::set($this->logger); Globals::reset(); } @@ -141,4 +152,32 @@ public function test_enabled_with_excluded_url(): void $_SERVER['REQUEST_URI'] = '/test'; $this->assertFalse(SdkAutoloader::autoload()); } + + public function test_autoload_from_config_file(): void + { + $this->logger->expects($this->never())->method('log')->with($this->equalTo(LogLevel::ERROR)); + $this->setEnvironmentVariable(Variables::OTEL_PHP_AUTOLOAD_ENABLED, 'true'); + $this->setEnvironmentVariable(Variables::OTEL_EXPERIMENTAL_CONFIG_FILE, __DIR__ . '/fixtures/otel-sdk.yaml'); + + $this->assertTrue(SdkAutoloader::autoload()); + $this->assertNotInstanceOf(NoopTracerProvider::class, Globals::tracerProvider()); + } + + /** + * Tests the scenario where the SDK is created from config file, but a custom component + * uses composer's autoload->files to add its own initializer + */ + public function test_autoload_with_late_globals_initializer(): void + { + $this->setEnvironmentVariable(Variables::OTEL_PHP_AUTOLOAD_ENABLED, 'true'); + $this->setEnvironmentVariable(Variables::OTEL_EXPERIMENTAL_CONFIG_FILE, __DIR__ . '/fixtures/otel-sdk.yaml'); + $this->assertTrue(SdkAutoloader::autoload()); + //SDK is configured, but globals have not been initialized yet, so we can add more initializers + $propagator = $this->createMock(TextMapPropagatorInterface::class); + Globals::registerInitializer(function (Configurator $configurator) use ($propagator) { + return $configurator->withPropagator($propagator); + }); + + $this->assertSame($propagator, Globals::propagator()); + } } diff --git a/tests/Unit/SDK/SdkTest.php b/tests/Unit/SDK/SdkTest.php index ab89c7d0e..084be21bb 100644 --- a/tests/Unit/SDK/SdkTest.php +++ b/tests/Unit/SDK/SdkTest.php @@ -22,6 +22,26 @@ class SdkTest extends TestCase { use TestState; + private TextMapPropagatorInterface $propagator; + private MeterProviderInterface $meterProvider; + private TracerProviderInterface $tracerProvider; + private LoggerProviderInterface $loggerProvider; + private EventLoggerProviderInterface $eventLoggerProvider; + + public function setUp(): void + { + $this->propagator = $this->createMock(TextMapPropagatorInterface::class); + $this->meterProvider = $this->createMock(MeterProviderInterface::class); + $this->tracerProvider = $this->createMock(TracerProviderInterface::class); + $this->loggerProvider = $this->createMock(LoggerProviderInterface::class); + $this->eventLoggerProvider = $this->createMock(EventLoggerProviderInterface::class); + } + + public function tearDown(): void + { + self::restoreEnvironmentVariables(); + } + public function test_is_not_disabled_by_default(): void { $this->assertFalse(Sdk::isDisabled()); @@ -70,16 +90,11 @@ public function test_builder(): void public function test_getters(): void { - $propagator = $this->createMock(TextMapPropagatorInterface::class); - $meterProvider = $this->createMock(MeterProviderInterface::class); - $tracerProvider = $this->createMock(TracerProviderInterface::class); - $loggerProvider = $this->createMock(LoggerProviderInterface::class); - $eventLoggerProvider = $this->createMock(EventLoggerProviderInterface::class); - $sdk = new Sdk($tracerProvider, $meterProvider, $loggerProvider, $eventLoggerProvider, $propagator); - $this->assertSame($propagator, $sdk->getPropagator()); - $this->assertSame($meterProvider, $sdk->getMeterProvider()); - $this->assertSame($tracerProvider, $sdk->getTracerProvider()); - $this->assertSame($loggerProvider, $sdk->getLoggerProvider()); - $this->assertSame($eventLoggerProvider, $sdk->getEventLoggerProvider()); + $sdk = new Sdk($this->tracerProvider, $this->meterProvider, $this->loggerProvider, $this->eventLoggerProvider, $this->propagator); + $this->assertSame($this->propagator, $sdk->getPropagator()); + $this->assertSame($this->meterProvider, $sdk->getMeterProvider()); + $this->assertSame($this->tracerProvider, $sdk->getTracerProvider()); + $this->assertSame($this->loggerProvider, $sdk->getLoggerProvider()); + $this->assertSame($this->eventLoggerProvider, $sdk->getEventLoggerProvider()); } } diff --git a/tests/Unit/SDK/fixtures/otel-sdk.yaml b/tests/Unit/SDK/fixtures/otel-sdk.yaml new file mode 100644 index 000000000..510810ea1 --- /dev/null +++ b/tests/Unit/SDK/fixtures/otel-sdk.yaml @@ -0,0 +1,34 @@ +file_format: '0.3' + +tracer_provider: + processors: + - simple: + exporter: + console: {} + +instrumentation: + php: + example_instrumentation: + span_name: my-span + general: + peer: + service_mapping: + - peer: 1.2.3.4 + service: FooService + - peer: 2.3.4.5 + service: BarService + http: + client: + request_captured_headers: + - Content-Type + - Accept + response_captured_headers: + - Content-Type + - Content-Encoding + server: + request_captured_headers: + - Content-Type + - Accept + response_captured_headers: + - Content-Type + - Content-Encoding