auto-instrumentation registration (#1304)

* [WIP] Add instrumentation configuration

* add autoloading for auto-instrumentations using SPI

* allow autoloading and non-autoloading to work

* fix attribute

* experimental config file

* fixing invalid dependencies
- deptrac was rightly complaining that API (indirectly) depended on SDK through config/sdk. For now,
remove usage of Config\SDK\Configuration\Context
- update deptrac config to allow some new dependencies

* dont register hook manager globally or in sdk

* remove unused function, psalm ignore missing extension function

* possibly fixing type-hint
psalm doesn't complain now, so that should be good

* load config files relative to cwd

* fixing hook manager enable/disable + psalm complaints

* fixing 8.1 psalm error

* use context to pass providers to instrumentations
- make "register global" a function of Sdk, but keep the sdk builder's interface intact
- invent an API instrumentation context, similar to the one in config/sdk, to pass providers
  to instrumentations
- add an initial test of autoloading from a config file

* adding tests for sdk::registerGlobal
in passing, remove some dead code for handling invalid booleans - config already handles this correctly

* linting

* test coverage for globals

* add opentelemetry extension to developer image and actions

* always register instrumentations via SPI

* register globals initializer for file-config sdk
allow SDK created from config file to coexist with components using globals initializer

* linting

* remove globals init function

* fix phan warning

* simplify hook manager
- drop storage from hook manager: can't guarantee that something else, eg swoole, won't modify storage
- drop context from hook manager: we must lazy-load globals via initializers, because not all instrumentations use SPI (although that may change in future)
- default hook manager to enabled

* add todo to deprecate Registry in future

* autoload instrumentations without config
if no config provided, still try to load them. wrap registration in a try/catch/log

* fixing phan ref, update doc

* remove phan suppress

* fix example

* bump SPI to 0.2.1

* adding late-binding tracer+provider
per review from Nevay, this will allow instrumentations to get things from Globals as late as possible

* adding late binding logger and meter providers

* more late binding test coverage

* tidy

* dont use CoversMethod yet
not available in phpunit 10, so comment out and leave a todo

* kitchen sink, remove unused var

* instrumentation config as a map, per config file spec

* adding general config

* move general config into sdk

* test config caching

* move general instrumentation config to api

* avoid bad version of sebastian/exporter

* bump config yaml files to 0.3

* fix tests

* disable hook manager during file-based init

* cleanup

* support multiple config files in autoloader

* move hook manager enable/disable out of extension hook manager
The most obvious place to do this is in a class named HookManager, which _was_ an
interface for hook managers. Rename existing HookManager to HookManagerInterface,
then re-purpose HookManager for globally enabling/disabling hook managers as well
as determining the disabled status.

* update spi config

---------

Co-authored-by: Tobias Bachert <git@b-privat.de>
This commit is contained in:
Brett McBride 2024-07-23 00:40:34 +10:00 committed by GitHub
parent 4ed0d870f7
commit 68b1b43cab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 1794 additions and 114 deletions

View File

@ -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

View File

@ -373,6 +373,7 @@ return [
'vendor/composer',
'vendor/grpc/grpc/src/lib',
'vendor/guzzlehttp',
'vendor/tbachert/spi/src',
'vendor/psr',
'vendor/php-http',
'vendor/phpunit/phpunit/src',

View File

@ -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"
]
}
}

View File

@ -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

View File

@ -11,6 +11,7 @@ RUN chmod +x /usr/local/bin/install-php-extensions \
grpc \
intl\
opcache \
opentelemetry \
pcntl \
protobuf \
sockets \

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace _;
use Nevay\SPI\ServiceLoader;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\ExtensionHookManager;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Instrumentation;
use OpenTelemetry\API\Logs\NoopLoggerProvider;
use OpenTelemetry\API\Metrics\Noop\NoopMeterProvider;
use OpenTelemetry\Config\SDK\Configuration;
use OpenTelemetry\Config\SDK\Configuration\Context;
use OpenTelemetry\Example\Example;
use const PHP_EOL;
/**
* This example uses SPI (see root composer.json extra.spi) to configure an example auto-instrumentation from a YAML file
*/
// EXAMPLE_INSTRUMENTATION_SPAN_NAME=test1234 php examples/instrumentation/configure_instrumentation.php
require __DIR__ . '/../../vendor/autoload.php';
Configuration::parseFile(__DIR__ . '/otel-sdk.yaml')->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;

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace _;
use OpenTelemetry\Example\Example;
use const PHP_EOL;
/**
* This example uses SPI (see root composer.json extra.spi) to configure an example auto-instrumentation from a YAML file.
* The YAML file paths are relative to the current working directory.
*/
// EXAMPLE_INSTRUMENTATION_SPAN_NAME=test1234 php examples/instrumentation/configure_instrumentation_global.php
putenv('OTEL_PHP_AUTOLOAD_ENABLED=true');
putenv('OTEL_EXPERIMENTAL_CONFIG_FILE=examples/instrumentation/otel-sdk.yaml');
require __DIR__ . '/../../vendor/autoload.php';
echo (new Example())->test(), PHP_EOL;

View File

@ -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}

View File

@ -1,4 +1,4 @@
file_format: '0.1'
file_format: '0.3'
resource:
attributes:

View File

@ -1,4 +1,4 @@
file_format: '0.1'
file_format: '0.3'
disabled: ${OTEL_SDK_DISABLED}

14
examples/src/Example.php Normal file
View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Example;
final class Example
{
public function test(): int
{
return 42;
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Example;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\InstrumentationConfiguration;
final class ExampleConfig implements InstrumentationConfiguration
{
public function __construct(
public readonly string $spanName,
public readonly bool $enabled = true,
) {
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Example;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\InstrumentationConfiguration;
use OpenTelemetry\Config\SDK\Configuration\ComponentProvider;
use OpenTelemetry\Config\SDK\Configuration\ComponentProviderRegistry;
use OpenTelemetry\Config\SDK\Configuration\Context;
use OpenTelemetry\Config\SDK\Configuration\Validation;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
/**
* @implements ComponentProvider<InstrumentationConfiguration>
*/
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;
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Example;
use Exception;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\ConfigurationRegistry;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Context as InstrumentationContext;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\HookManagerInterface;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Instrumentation;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\Context\Context;
final class ExampleInstrumentation implements Instrumentation
{
public function register(HookManagerInterface $hookManager, ConfigurationRegistry $configuration, InstrumentationContext $context): void
{
$config = $configuration->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();
}
);
}
}

View File

@ -27,6 +27,11 @@
<referencedClass name="GMP"/>
</errorLevel>
</UndefinedClass>
<UndefinedFunction>
<errorLevel type="suppress">
<referencedFunction name="OpenTelemetry\Instrumentation\hook"/>
</errorLevel>
</UndefinedFunction>
<ArgumentTypeCoercion>
<errorLevel type="suppress">
<directory name="./examples"/>

View File

@ -6,7 +6,7 @@ namespace OpenTelemetry\API;
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\Context;
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 @@ final class Globals
*
* @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 @@ final class Globals
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 {

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\AutoInstrumentation;
final class ConfigurationRegistry
{
private array $configurations = [];
public function add(InstrumentationConfiguration $configuration): self
{
$this->configurations[$configuration::class] = $configuration;
return $this;
}
/**
* @template C of InstrumentationConfiguration
* @param class-string<C> $id
* @return C|null
*/
public function get(string $id): ?InstrumentationConfiguration
{
return $this->configurations[$id] ?? null;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\AutoInstrumentation;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Logs\NoopLoggerProvider;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Metrics\Noop\NoopMeterProvider;
use OpenTelemetry\API\Trace\NoopTracerProvider;
use OpenTelemetry\API\Trace\TracerProviderInterface;
/**
* Context used for component creation.
*/
final class Context
{
public function __construct(
public readonly TracerProviderInterface $tracerProvider = new NoopTracerProvider(),
public readonly MeterProviderInterface $meterProvider = new NoopMeterProvider(),
public readonly LoggerProviderInterface $loggerProvider = new NoopLoggerProvider(),
) {
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\AutoInstrumentation;
use function assert;
use Closure;
use function extension_loaded;
use Nevay\SPI\ServiceProviderDependency\ExtensionDependency;
use ReflectionFunction;
/** @phan-file-suppress PhanUndeclaredClassAttribute */
#[ExtensionDependency('opentelemetry', '^1.0')]
final class ExtensionHookManager implements HookManagerInterface
{
/**
* @phan-suppress PhanUndeclaredFunction
*/
public function hook(?string $class, string $function, ?Closure $preHook = null, ?Closure $postHook = null): void
{
assert(extension_loaded('opentelemetry'));
/** @noinspection PhpFullyQualifiedNameUsageInspection */
\OpenTelemetry\Instrumentation\hook($class, $function, $this->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);
};
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\AutoInstrumentation;
interface GeneralInstrumentationConfiguration extends InstrumentationConfiguration
{
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\AutoInstrumentation;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\Context\ContextKeyInterface;
class HookManager
{
public static function enable(?ContextInterface $context = null): ContextInterface
{
$context ??= Context::getCurrent();
return $context->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);
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\AutoInstrumentation;
use Closure;
use Throwable;
interface HookManagerInterface
{
/**
* @param Closure(object|string|null,array,string,string,string|null,int|null):void|null $preHook
* @param Closure(object|string|null,array,mixed,Throwable|null,string,string,string|null,int|null):void|null $postHook
*/
public function hook(?string $class, string $function, ?Closure $preHook = null, ?Closure $postHook = null): void;
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\AutoInstrumentation;
interface Instrumentation
{
public function register(HookManagerInterface $hookManager, ConfigurationRegistry $configuration, Context $context): void;
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\AutoInstrumentation;
interface InstrumentationConfiguration
{
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\AutoInstrumentation;
use Closure;
final class NoopHookManager implements HookManagerInterface
{
public function hook(?string $class, string $function, ?Closure $preHook = null, ?Closure $postHook = null): void
{
// no-op
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\Configuration\General;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\GeneralInstrumentationConfiguration;
class HttpConfig implements GeneralInstrumentationConfiguration
{
public function __construct(public readonly array $config)
{
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Instrumentation\Configuration\General;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\GeneralInstrumentationConfiguration;
class PeerConfig implements GeneralInstrumentationConfiguration
{
public function __construct(public readonly array $config)
{
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs;
use Closure;
class LateBindingLogger implements LoggerInterface
{
private ?LoggerInterface $logger = null;
/** @param Closure(): LoggerInterface $factory */
public function __construct(
private readonly Closure $factory,
) {
}
public function emit(LogRecord $logRecord): void
{
($this->logger ??= ($this->factory)())->emit($logRecord);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs;
use Closure;
class LateBindingLoggerProvider implements LoggerProviderInterface
{
private ?LoggerProviderInterface $loggerProvider = null;
/** @param Closure(): LoggerProviderInterface $factory */
public function __construct(
private readonly Closure $factory,
) {
}
public function getLogger(string $name, ?string $version = null, ?string $schemaUrl = null, iterable $attributes = []): LoggerInterface
{
return $this->loggerProvider?->getLogger($name, $version, $schemaUrl, $attributes)
?? new LateBindingLogger(fn (): LoggerInterface => ($this->loggerProvider ??= ($this->factory)())->getLogger($name, $version, $schemaUrl, $attributes));
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Metrics;
use Closure;
/**
* @psalm-suppress InvalidArgument
*/
class LateBindingMeter implements MeterInterface
{
private ?MeterInterface $meter = null;
/** @param Closure(): MeterInterface $factory */
public function __construct(
private readonly Closure $factory,
) {
}
public function batchObserve(callable $callback, AsynchronousInstrument $instrument, AsynchronousInstrument ...$instruments): ObservableCallbackInterface
{
return ($this->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);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Metrics;
use Closure;
class LateBindingMeterProvider implements MeterProviderInterface
{
private ?MeterProviderInterface $meterProvider = null;
/** @param Closure(): MeterProviderInterface $factory */
public function __construct(
private readonly Closure $factory,
) {
}
public function getMeter(string $name, ?string $version = null, ?string $schemaUrl = null, iterable $attributes = []): MeterInterface
{
return $this->meterProvider?->getMeter($name, $version, $schemaUrl, $attributes)
?? new LateBindingMeter(fn (): MeterInterface => ($this->meterProvider ??= ($this->factory)())->getMeter($name, $version, $schemaUrl, $attributes));
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Trace;
use Closure;
class LateBindingTracer implements TracerInterface
{
private ?TracerInterface $tracer = null;
/** @param Closure(): TracerInterface $factory */
public function __construct(
private readonly Closure $factory,
) {
}
public function spanBuilder(string $spanName): SpanBuilderInterface
{
return ($this->tracer ??= ($this->factory)())->spanBuilder($spanName);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Trace;
use Closure;
/**
* Late binding providers are designed to be used by Instrumentation, while we do not have control over when all components (propagators, etc)
* which are registered through composer.autoload.files are actually loaded. It means that tracers etc are not fetched
* from Globals until the last possible instant (ie, when they try to create a span, get an instrument, etc).
* In the future, when everything uses SPI, this will be removed.
*/
class LateBindingTracerProvider implements TracerProviderInterface
{
private ?TracerProviderInterface $tracerProvider = null;
/** @param Closure(): TracerProviderInterface $factory */
public function __construct(
private readonly Closure $factory,
) {
}
public function getTracer(string $name, ?string $version = null, ?string $schemaUrl = null, iterable $attributes = []): TracerInterface
{
return $this->tracerProvider?->getTracer($name, $version, $schemaUrl, $attributes)
?? new LateBindingTracer(fn (): TracerInterface => ($this->tracerProvider ??= ($this->factory)())->getTracer($name, $version, $schemaUrl, $attributes));
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Config\SDK\ComponentProvider\Instrumentation\General;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\GeneralInstrumentationConfiguration;
use OpenTelemetry\API\Instrumentation\Configuration\General\HttpConfig;
use OpenTelemetry\Config\SDK\Configuration\ComponentProvider;
use OpenTelemetry\Config\SDK\Configuration\ComponentProviderRegistry;
use OpenTelemetry\Config\SDK\Configuration\Context;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
/**
* @implements ComponentProvider<GeneralInstrumentationConfiguration>
*/
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;
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Config\SDK\ComponentProvider\Instrumentation\General;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\GeneralInstrumentationConfiguration;
use OpenTelemetry\API\Instrumentation\Configuration\General\PeerConfig;
use OpenTelemetry\Config\SDK\Configuration\ComponentProvider;
use OpenTelemetry\Config\SDK\Configuration\ComponentProviderRegistry;
use OpenTelemetry\Config\SDK\Configuration\Context;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
/**
* @implements ComponentProvider<GeneralInstrumentationConfiguration>
*/
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;
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Config\SDK\ComponentProvider;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\ConfigurationRegistry;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\GeneralInstrumentationConfiguration;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\InstrumentationConfiguration;
use OpenTelemetry\Config\SDK\Configuration\ComponentPlugin;
use OpenTelemetry\Config\SDK\Configuration\ComponentProvider;
use OpenTelemetry\Config\SDK\Configuration\ComponentProviderRegistry;
use OpenTelemetry\Config\SDK\Configuration\Context;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
/**
* @internal
* @todo In a future release, when all instrumentations use SPI (and not autoload.files), this could be moved into {@see OpenTelemetrySdk}>
* @implements ComponentProvider<ConfigurationRegistry>
*/
class InstrumentationConfigurationRegistry implements ComponentProvider
{
/**
* @param array{
* instrumentation: array{
* php: list<ComponentPlugin<InstrumentationConfiguration>>,
* general: list<ComponentPlugin<InstrumentationConfiguration>>
* }
* } $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;
}
}

View File

@ -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 @@ final class OpenTelemetrySdk implements ComponentProvider
->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 @@ final class OpenTelemetrySdk implements ComponentProvider
->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 @@ final class OpenTelemetrySdk implements ComponentProvider
->end()
->end()
->end()
->append($registry->componentList('readers', MetricReaderInterface::class))
->append($registry->componentArrayList('readers', MetricReaderInterface::class))
->end()
;
@ -394,7 +394,7 @@ final class OpenTelemetrySdk implements ComponentProvider
->integerNode('attribute_count_limit')->min(0)->defaultNull()->end()
->end()
->end()
->append($registry->componentList('processors', LogRecordProcessorInterface::class))
->append($registry->componentArrayList('processors', LogRecordProcessorInterface::class))
->end()
;

View File

@ -33,6 +33,7 @@ interface ComponentProvider
*
* @see ComponentProviderRegistry::component()
* @see ComponentProviderRegistry::componentList()
* @see ComponentProviderRegistry::componentArrayList()
* @see ComponentProviderRegistry::componentNames()
*/
public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition;

View File

@ -37,6 +37,26 @@ interface ComponentProviderRegistry
*
* ```
* $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<ComponentPlugin<$type>>`
*
* ```
* $name:
* - provider1:
* property: value
* anotherProperty: value
@ -48,8 +68,7 @@ interface ComponentProviderRegistry
* @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.
*

View File

@ -6,6 +6,7 @@ namespace OpenTelemetry\Config\SDK\Configuration;
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 @@ final class ConfigurationFactory
}
$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 @@ final class ConfigurationFactory
class_exists(VarExporter::class)
? sprintf('<?php return %s;', VarExporter::export($configuration))
: sprintf('<?php return unserialize(%s);', var_export(serialize($configuration), true)),
$resources->toArray() //@todo $resources possible null
$resources->toArray()
);
return $configuration;

View File

@ -69,6 +69,14 @@ final class ComponentProviderRegistry implements \OpenTelemetry\Config\SDK\Confi
}
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 @@ final class ComponentProviderRegistry implements \OpenTelemetry\Config\SDK\Confi
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 @@ final class ComponentProviderRegistry implements \OpenTelemetry\Config\SDK\Confi
}
}
$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]);
}
$name = array_key_first($value);
$provider = $this->providers[$type][$name];
$this->resources?->addClassResource($provider);
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'])
));
}
return new ComponentPlugin($value[$name], $this->providers[$type][$name]);
});
$name = array_key_first($value);
$provider = $this->providers[$type][$name];
$this->resources?->addClassResource($provider);
return new ComponentPlugin($value[$name], $this->providers[$type][$name]);
});
}
}
/**

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Config\SDK;
use Nevay\SPI\ServiceLoader;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\ConfigurationRegistry;
use OpenTelemetry\Config\SDK\ComponentProvider\InstrumentationConfigurationRegistry;
use OpenTelemetry\Config\SDK\Configuration\ComponentPlugin;
use OpenTelemetry\Config\SDK\Configuration\ComponentProvider;
use OpenTelemetry\Config\SDK\Configuration\ConfigurationFactory;
use OpenTelemetry\Config\SDK\Configuration\Context;
use OpenTelemetry\Config\SDK\Configuration\Environment\EnvSourceReader;
use OpenTelemetry\Config\SDK\Configuration\Environment\PhpIniEnvSource;
use OpenTelemetry\Config\SDK\Configuration\Environment\ServerEnvSource;
final class Instrumentation
{
/**
* @param ComponentPlugin<ConfigurationRegistry> $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<string> $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<ConfigurationRegistry>
*/
private static function factory(): ConfigurationFactory
{
static $factory;
return $factory ??= new ConfigurationFactory(
ServiceLoader::load(ComponentProvider::class),
new InstrumentationConfigurationRegistry(),
new EnvSourceReader([
new ServerEnvSource(),
new PhpIniEnvSource(),
]),
);
}
}

View File

@ -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"
]
}
}

View File

@ -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';
}

View File

@ -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';
}

View File

@ -16,6 +16,7 @@ use TypeError;
/**
* 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
{

View File

@ -4,9 +4,24 @@ declare(strict_types=1);
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\ExporterFactory;
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();
$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)
;
});
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();
return true;
}
/**
* Test whether a request URI is set, and if it matches the excluded urls configuration option
*
* @internal
*/
public static function isIgnoredUrl(): bool
private static function environmentBasedInitializer(Configurator $configurator): Configurator
{
$ignoreUrls = Configuration::getList(Variables::OTEL_PHP_EXCLUDED_URLS, []);
if ($ignoreUrls === []) {
return false;
$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);
}
$url = $_SERVER['REQUEST_URI'] ?? null;
if (!$url) {
return false;
}
foreach ($ignoreUrls as $ignore) {
if (preg_match(sprintf('|%s|', $ignore), (string) $url) === 1) {
return true;
}
$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();
$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)
;
}
/**
* @phan-suppress PhanPossiblyUndeclaredVariable
*/
private static function fileBasedInitializer(Configurator $configurator): Configurator
{
$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();
}
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]);
}
}
}
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 new NoopHookManager();
}
/**
@ -96,14 +202,7 @@ class SdkAutoloader
*/
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);
}
/**

View File

@ -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"
]
}
}
}

View File

@ -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

View File

@ -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.<language>.
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"

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Unit\API\Instrumentation\AutoInstrumentation;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\ConfigurationRegistry;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\InstrumentationConfiguration;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ConfigurationRegistry::class)]
class ConfigurationRegistryTest extends TestCase
{
public function test_registry(): void
{
$registry = new ConfigurationRegistry();
$config = new class() implements InstrumentationConfiguration {};
$registry->add($config);
$this->assertSame($config, $registry->get($config::class));
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Unit\API\Instrumentation\AutoInstrumentation;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Context;
use OpenTelemetry\API\Logs\NoopLoggerProvider;
use OpenTelemetry\API\Metrics\Noop\NoopMeterProvider;
use OpenTelemetry\API\Trace\NoopTracerProvider;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(Context::class)]
class ContextTest extends TestCase
{
public function test_default_noops(): void
{
$context = new Context();
$this->assertInstanceOf(NoopTracerProvider::class, $context->tracerProvider);
$this->assertInstanceOf(NoopMeterProvider::class, $context->meterProvider);
$this->assertInstanceOf(NoopLoggerProvider::class, $context->loggerProvider);
}
}

View File

@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\API\Instrumentation\AutoInstrumentation;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\ConfigurationRegistry;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Context as InstrumentationContext;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\ExtensionHookManager;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\HookManager;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\HookManagerInterface;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Instrumentation;
use OpenTelemetry\API\Instrumentation\Configurator;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Trace\TracerProviderInterface;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\ScopeInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(ExtensionHookManager::class)]
class ExtensionHookManagerTest extends TestCase
{
private ConfigurationRegistry $registry;
private ScopeInterface $scope;
private HookManagerInterface $hookManager;
private InstrumentationContext $context;
public function setUp(): void
{
if (!extension_loaded('opentelemetry')) {
$this->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);
}
};
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\API\Instrumentation\AutoInstrumentation;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\HookManager;
use OpenTelemetry\Context\Context;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(HookManager::class)]
class HookManagerTest extends TestCase
{
public function test_enable_disable(): void
{
$context = Context::getRoot();
$this->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());
}
}

View File

@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\API\Instrumentation\AutoInstrumentation;
use Nevay\SPI\ServiceLoader;
use OpenTelemetry\API\Behavior\Internal\Logging;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\ConfigurationRegistry;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Context;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\HookManagerInterface;
use OpenTelemetry\API\Instrumentation\AutoInstrumentation\Instrumentation;
use OpenTelemetry\API\Instrumentation\Configurator;
use OpenTelemetry\API\Logs\LateBindingLogger;
use OpenTelemetry\API\Logs\LateBindingLoggerProvider;
use OpenTelemetry\API\Logs\LoggerInterface;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\API\Metrics\LateBindingMeter;
use OpenTelemetry\API\Metrics\LateBindingMeterProvider;
use OpenTelemetry\API\Metrics\MeterInterface;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Trace\LateBindingTracer;
use OpenTelemetry\API\Trace\LateBindingTracerProvider;
use OpenTelemetry\API\Trace\TracerInterface;
use OpenTelemetry\API\Trace\TracerProviderInterface;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\SdkAutoloader;
use OpenTelemetry\Tests\TestState;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(LateBindingLoggerProvider::class)]
#[CoversClass(LateBindingLogger::class)]
#[CoversClass(LateBindingMeterProvider::class)]
#[CoversClass(LateBindingMeter::class)]
#[CoversClass(LateBindingTracerProvider::class)]
#[CoversClass(LateBindingTracer::class)]
// @todo phpunit 11 (8.2+) only, replace CoversClass(SdkAutoloader::class)
#[CoversClass(SdkAutoloader::class)]
//#[CoversMethod(SdkAutoloader::class, 'createLateBindingLoggerProvider')]
//#[CoversMethod(SdkAutoloader::class, 'createLateBindingMeterProvider')]
//#[CoversMethod(SdkAutoloader::class, 'createLateBindingTracerProvider')]
class LateBindingProviderTest extends TestCase
{
use TestState;
public function setUp(): void
{
Logging::disable();
}
public function test_late_binding_providers(): void
{
$instrumentation = new class() implements Instrumentation {
private static ?Context $context;
public function register(HookManagerInterface $hookManager, ConfigurationRegistry $configuration, Context $context): void
{
self::$context = $context;
}
public function getTracer(): TracerInterface
{
assert(self::$context !== null);
return self::$context->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);
}
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
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\API\Trace\TracerProviderInterface;
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 @@ use PHPUnit\Framework\TestCase;
#[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 @@ final class InstrumentationTest extends TestCase
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();
}
}

View File

@ -26,8 +26,19 @@ use Symfony\Component\Yaml\Yaml;
#[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 @@ final class ConfigurationFactoryTest extends TestCase
]),
);
}
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);
}
}

View File

@ -151,7 +151,7 @@ final class OpenTelemetryConfiguration implements ComponentProvider
->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 @@ final class OpenTelemetryConfiguration implements ComponentProvider
->end()
->end()
->end()
->append($registry->componentList('readers', MetricReader::class))
->append($registry->componentArrayList('readers', MetricReader::class))
->end()
;
@ -223,7 +223,7 @@ final class OpenTelemetryConfiguration implements ComponentProvider
->integerNode('attribute_count_limit')->min(0)->defaultNull()->end()
->end()
->end()
->append($registry->componentList('processors', LogRecordProcessor::class))
->append($registry->componentArrayList('processors', LogRecordProcessor::class))
->end()
;

View File

@ -0,0 +1 @@
**cache**

View File

@ -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
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

View File

@ -4,28 +4,39 @@ declare(strict_types=1);
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 @@ class SdkAutoloaderTest extends TestCase
$_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());
}
}

View File

@ -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 @@ class SdkTest extends TestCase
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());
}
}

View File

@ -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