diff --git a/ComponentProvider/Logs/LogRecordExporterConsole.php b/ComponentProvider/Logs/LogRecordExporterConsole.php new file mode 100644 index 0000000..a978516 --- /dev/null +++ b/ComponentProvider/Logs/LogRecordExporterConsole.php @@ -0,0 +1,36 @@ + + */ +final class LogRecordExporterConsole implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): LogRecordExporterInterface + { + return new ConsoleExporter(Registry::transportFactory('stream')->create( + endpoint: 'php://stdout', + contentType: 'application/json', + )); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('console'); + } +} diff --git a/ComponentProvider/Logs/LogRecordExporterOtlp.php b/ComponentProvider/Logs/LogRecordExporterOtlp.php new file mode 100644 index 0000000..2be5fd1 --- /dev/null +++ b/ComponentProvider/Logs/LogRecordExporterOtlp.php @@ -0,0 +1,75 @@ + + */ +#[PackageDependency('open-telemetry/exporter-otlp', '^1.0.5')] +final class LogRecordExporterOtlp implements ComponentProvider +{ + + /** + * @param array{ + * protocol: 'http/protobuf'|'http/json'|'grpc', + * endpoint: string, + * certificate: ?string, + * client_key: ?string, + * client_certificate: ?string, + * headers: array, + * compression: 'gzip'|null, + * timeout: int<0, max>, + * } $properties + */ + public function createPlugin(array $properties, Context $context): LogRecordExporterInterface + { + $protocol = $properties['protocol']; + + return new LogsExporter(Registry::transportFactory($protocol)->create( + endpoint: $properties['endpoint'] . OtlpUtil::path(Signals::LOGS, $protocol), + contentType: Protocols::contentType($protocol), + headers: $properties['headers'], + compression: $properties['compression'], + timeout: $properties['timeout'], + cacert: $properties['certificate'], + cert: $properties['client_certificate'], + key: $properties['client_certificate'], + )); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('otlp'); + $node + ->children() + ->enumNode('protocol')->isRequired()->values(['http/protobuf', 'http/json', 'grpc'])->end() + ->scalarNode('endpoint')->isRequired()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('certificate')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('client_key')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('client_certificate')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->arrayNode('headers') + ->scalarPrototype()->end() + ->end() + ->enumNode('compression')->values(['gzip'])->defaultNull()->end() + ->integerNode('timeout')->min(0)->defaultValue(10)->end() + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Logs/LogRecordProcessorBatch.php b/ComponentProvider/Logs/LogRecordProcessorBatch.php new file mode 100644 index 0000000..712ca87 --- /dev/null +++ b/ComponentProvider/Logs/LogRecordProcessorBatch.php @@ -0,0 +1,60 @@ + + */ +final class LogRecordProcessorBatch implements ComponentProvider +{ + + /** + * @param array{ + * schedule_delay: int<0, max>, + * export_timeout: int<0, max>, + * max_queue_size: int<0, max>, + * max_export_batch_size: int<0, max>, + * exporter: ComponentPlugin, + * } $properties + */ + public function createPlugin(array $properties, Context $context): LogRecordProcessorInterface + { + return new BatchLogRecordProcessor( + exporter: $properties['exporter']->create($context), + clock: ClockFactory::getDefault(), + maxQueueSize: $properties['max_queue_size'], + scheduledDelayMillis: $properties['schedule_delay'], + exportTimeoutMillis: $properties['export_timeout'], + maxExportBatchSize: $properties['max_export_batch_size'], + meterProvider: $context->meterProvider, + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('batch'); + $node + ->children() + ->integerNode('schedule_delay')->min(0)->defaultValue(5000)->end() + ->integerNode('export_timeout')->min(0)->defaultValue(30000)->end() + ->integerNode('max_queue_size')->min(0)->defaultValue(2048)->end() + ->integerNode('max_export_batch_size')->min(0)->defaultValue(512)->end() + ->append($registry->component('exporter', LogRecordExporterInterface::class)->isRequired()) + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Logs/LogRecordProcessorSimple.php b/ComponentProvider/Logs/LogRecordProcessorSimple.php new file mode 100644 index 0000000..edbaaa5 --- /dev/null +++ b/ComponentProvider/Logs/LogRecordProcessorSimple.php @@ -0,0 +1,45 @@ + + */ +final class LogRecordProcessorSimple implements ComponentProvider +{ + + /** + * @param array{ + * exporter: ComponentPlugin, + * } $properties + */ + public function createPlugin(array $properties, Context $context): LogRecordProcessorInterface + { + return new SimpleLogRecordProcessor( + exporter: $properties['exporter']->create($context), + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('simple'); + $node + ->children() + ->append($registry->component('exporter', LogRecordExporterInterface::class)->isRequired()) + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Metrics/AggregationResolverDefault.php b/ComponentProvider/Metrics/AggregationResolverDefault.php new file mode 100644 index 0000000..d96cf30 --- /dev/null +++ b/ComponentProvider/Metrics/AggregationResolverDefault.php @@ -0,0 +1,35 @@ + + */ +final class AggregationResolverDefault implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): DefaultAggregationProviderInterface + { + // TODO Implement proper aggregation providers (default, drop, explicit_bucket_histogram, last_value, sum) to handle advisory + return new class() implements DefaultAggregationProviderInterface { + use DefaultAggregationProviderTrait; + }; + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('default'); + } +} diff --git a/ComponentProvider/Metrics/MetricExporterConsole.php b/ComponentProvider/Metrics/MetricExporterConsole.php new file mode 100644 index 0000000..6fba58c --- /dev/null +++ b/ComponentProvider/Metrics/MetricExporterConsole.php @@ -0,0 +1,32 @@ + + */ +final class MetricExporterConsole implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): MetricExporterInterface + { + return new ConsoleMetricExporter(); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('console'); + } +} diff --git a/ComponentProvider/Metrics/MetricExporterOtlp.php b/ComponentProvider/Metrics/MetricExporterOtlp.php new file mode 100644 index 0000000..5547de0 --- /dev/null +++ b/ComponentProvider/Metrics/MetricExporterOtlp.php @@ -0,0 +1,92 @@ + + */ +#[PackageDependency('open-telemetry/exporter-otlp', '^1.0.5')] +final class MetricExporterOtlp implements ComponentProvider +{ + + /** + * @param array{ + * protocol: 'http/protobuf'|'http/json'|'grpc', + * endpoint: string, + * certificate: ?string, + * client_key: ?string, + * client_certificate: ?string, + * headers: array, + * compression: 'gzip'|null, + * timeout: int<0, max>, + * temporality_preference: 'cumulative'|'delta'|'lowmemory', + * default_histogram_aggregation: 'explicit_bucket_histogram', + * } $properties + */ + public function createPlugin(array $properties, Context $context): MetricExporterInterface + { + $protocol = $properties['protocol']; + + $temporality = match ($properties['temporality_preference']) { + 'cumulative' => Temporality::CUMULATIVE, + 'delta' => Temporality::DELTA, + 'lowmemory' => null, + }; + + return new MetricExporter(Registry::transportFactory($protocol)->create( + endpoint: $properties['endpoint'] . OtlpUtil::path(Signals::METRICS, $protocol), + contentType: Protocols::contentType($protocol), + headers: $properties['headers'], + compression: $properties['compression'], + timeout: $properties['timeout'], + cacert: $properties['certificate'], + cert: $properties['client_certificate'], + key: $properties['client_certificate'], + ), $temporality); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('otlp'); + $node + ->children() + ->enumNode('protocol')->isRequired()->values(['http/protobuf', 'http/json', 'grpc'])->end() + ->scalarNode('endpoint')->isRequired()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('certificate')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('client_key')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('client_certificate')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->arrayNode('headers') + ->scalarPrototype()->end() + ->end() + ->enumNode('compression')->values(['gzip'])->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->integerNode('timeout')->min(0)->defaultValue(10)->end() + ->enumNode('temporality_preference') + ->values(['cumulative', 'delta', 'lowmemory']) + ->defaultValue('cumulative') + ->end() + ->enumNode('default_histogram_aggregation') + ->values(['explicit_bucket_histogram']) + ->defaultValue('explicit_bucket_histogram') + ->end() + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Metrics/MetricReaderPeriodic.php b/ComponentProvider/Metrics/MetricReaderPeriodic.php new file mode 100644 index 0000000..0e77e20 --- /dev/null +++ b/ComponentProvider/Metrics/MetricReaderPeriodic.php @@ -0,0 +1,49 @@ + + */ +final class MetricReaderPeriodic implements ComponentProvider +{ + + /** + * @param array{ + * interval: int<0, max>, + * timeout: int<0, max>, + * exporter: ComponentPlugin, + * } $properties + */ + public function createPlugin(array $properties, Context $context): MetricReaderInterface + { + return new ExportingReader( + exporter: $properties['exporter']->create($context), + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('periodic'); + $node + ->children() + ->integerNode('interval')->min(0)->defaultValue(5000)->end() + ->integerNode('timeout')->min(0)->defaultValue(30000)->end() + ->append($registry->component('exporter', MetricExporterInterface::class)->isRequired()) + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/OpenTelemetrySdk.php b/ComponentProvider/OpenTelemetrySdk.php new file mode 100644 index 0000000..c8a11fb --- /dev/null +++ b/ComponentProvider/OpenTelemetrySdk.php @@ -0,0 +1,400 @@ + + */ +final class OpenTelemetrySdk implements ComponentProvider +{ + + /** + * @param array{ + * file_format: '0.1', + * disabled: bool, + * resource: array{ + * attributes: array, + * schema_url: ?string, + * }, + * attribute_limits: array{ + * attribute_value_length_limit: ?int<0, max>, + * attribute_count_limit: int<0, max>, + * }, + * propagator: ?ComponentPlugin, + * tracer_provider: array{ + * limits: array{ + * attribute_value_length_limit: ?int<0, max>, + * attribute_count_limit: ?int<0, max>, + * event_count_limit: int<0, max>, + * link_count_limit: int<0, max>, + * event_attribute_count_limit: ?int<0, max>, + * link_attribute_count_limit: ?int<0, max>, + * }, + * sampler: ?ComponentPlugin, + * processors: list>, + * }, + * meter_provider: array{ + * views: list, + * aggregation: ?ComponentPlugin, + * }, + * selector: array{ + * instrument_type: 'counter'|'histogram'|'observable_counter'|'observable_gauge'|'observable_up_down_counter'|'up_down_counter'|null, + * instrument_name: ?non-empty-string, + * unit: ?string, + * meter_name: ?string, + * meter_version: ?string, + * meter_schema_url: ?string, + * }, + * }>, + * readers: list>, + * }, + * logger_provider: array{ + * limits: array{ + * attribute_value_length_limit: ?int<0, max>, + * attribute_count_limit: ?int<0, max>, + * }, + * processors: list>, + * }, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SdkBuilder + { + $sdkBuilder = new SdkBuilder(); + + $propagator = $properties['propagator']?->create($context) ?? NoopTextMapPropagator::getInstance(); + $sdkBuilder->setPropagator($propagator); + + if ($properties['disabled']) { + return $sdkBuilder; + } + + $resource = ResourceInfoFactory::defaultResource() + ->merge(ResourceInfo::create( + attributes: Attributes::create($properties['resource']['attributes']), + schemaUrl: $properties['resource']['schema_url'], + )); + + $spanProcessors = []; + foreach ($properties['tracer_provider']['processors'] as $processor) { + $spanProcessors[] = $processor->create($context); + } + + // + + $tracerProvider = new TracerProvider( + spanProcessors: $spanProcessors, + sampler: $properties['tracer_provider']['sampler']?->create($context) ?? new ParentBased(new AlwaysOnSampler()), + resource: $resource, + spanLimits: new SpanLimits( + attributesFactory: Attributes::factory( + attributeCountLimit: $properties['tracer_provider']['limits']['attribute_count_limit'] + ?? $properties['attribute_limits']['attribute_count_limit'], + attributeValueLengthLimit: $properties['tracer_provider']['limits']['attribute_value_length_limit'] + ?? $properties['attribute_limits']['attribute_value_length_limit'], + ), + eventAttributesFactory: Attributes::factory( + attributeCountLimit: $properties['tracer_provider']['limits']['event_attribute_count_limit'] + ?? $properties['tracer_provider']['limits']['attribute_count_limit'] + ?? $properties['attribute_limits']['attribute_count_limit'], + attributeValueLengthLimit: $properties['tracer_provider']['limits']['attribute_value_length_limit'] + ?? $properties['attribute_limits']['attribute_value_length_limit'], + ), + linkAttributesFactory: Attributes::factory( + attributeCountLimit: $properties['tracer_provider']['limits']['link_attribute_count_limit'] + ?? $properties['tracer_provider']['limits']['attribute_count_limit'] + ?? $properties['attribute_limits']['attribute_count_limit'], + attributeValueLengthLimit: $properties['tracer_provider']['limits']['attribute_value_length_limit'] + ?? $properties['attribute_limits']['attribute_value_length_limit'], + ), + eventCountLimit: $properties['tracer_provider']['limits']['event_count_limit'], + linkCountLimit: $properties['tracer_provider']['limits']['link_count_limit'], + ), + ); + + // + + // + + $metricReaders = []; + foreach ($properties['meter_provider']['readers'] as $reader) { + $metricReaders[] = $reader->create($context); + } + + $viewRegistry = new CriteriaViewRegistry(); + foreach ($properties['meter_provider']['views'] as $view) { + $criteria = []; + if (isset($view['selector']['instrument_type'])) { + $criteria[] = new InstrumentTypeCriteria(match ($view['selector']['instrument_type']) { + 'counter' => InstrumentType::COUNTER, + 'histogram' => InstrumentType::HISTOGRAM, + 'observable_counter' => InstrumentType::ASYNCHRONOUS_COUNTER, + 'observable_gauge' => InstrumentType::ASYNCHRONOUS_GAUGE, + 'observable_up_down_counter' => InstrumentType::ASYNCHRONOUS_UP_DOWN_COUNTER, + 'up_down_counter' => InstrumentType::UP_DOWN_COUNTER, + }); + } + if (isset($view['selector']['instrument_name'])) { + $criteria[] = new InstrumentNameCriteria($view['selector']['instrument_name']); + } + if (isset($view['selector']['unit'])) { + // TODO Add unit criteria + } + if (isset($view['selector']['meter_name'])) { + $criteria[] = new InstrumentationScopeNameCriteria($view['selector']['meter_name']); + } + if (isset($view['selector']['meter_version'])) { + $criteria[] = new InstrumentationScopeVersionCriteria($view['selector']['meter_version']); + } + if (isset($view['selector']['meter_schema_url'])) { + $criteria[] = new InstrumentationScopeSchemaUrlCriteria($view['selector']['meter_schema_url']); + } + + $viewTemplate = ViewTemplate::create(); + if (isset($view['stream']['name'])) { + $viewTemplate = $viewTemplate->withName($view['stream']['name']); + } + if (isset($view['stream']['description'])) { + $viewTemplate = $viewTemplate->withDescription($view['stream']['description']); + } + if ($view['stream']['attribute_keys']) { + $viewTemplate = $viewTemplate->withAttributeKeys($view['stream']['attribute_keys']); + } + if (isset($view['stream']['aggregation'])) { + // TODO Add support for aggregation providers in views to allow usage of advisory + } + + $viewRegistry->register(new AllCriteria($criteria), $viewTemplate); + } + + /** @psalm-suppress InvalidArgument TODO update metric reader interface */ + $meterProvider = new MeterProvider( + contextStorage: null, + resource: $resource, + clock: ClockFactory::getDefault(), + attributesFactory: Attributes::factory(), + instrumentationScopeFactory: new InstrumentationScopeFactory(Attributes::factory()), + metricReaders: $metricReaders, // @phpstan-ignore-line + viewRegistry: $viewRegistry, + exemplarFilter: null, + stalenessHandlerFactory: new NoopStalenessHandlerFactory(), + ); + + // + + // + + $logRecordProcessors = []; + foreach ($properties['logger_provider']['processors'] as $processor) { + $logRecordProcessors[] = $processor->create($context); + } + + // TODO Allow injecting log record attributes factory + $loggerProvider = new LoggerProvider( + processor: new MultiLogRecordProcessor($logRecordProcessors), + instrumentationScopeFactory: new InstrumentationScopeFactory(Attributes::factory()), + resource: $resource, + ); + + // + + $sdkBuilder->setTracerProvider($tracerProvider); + $sdkBuilder->setMeterProvider($meterProvider); + $sdkBuilder->setLoggerProvider($loggerProvider); + + return $sdkBuilder; + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('open_telemetry'); + $node + ->addDefaultsIfNotSet() + ->ignoreExtraKeys() + ->children() + ->scalarNode('file_format') + ->isRequired() + ->example('0.1') + ->validate()->always(Validation::ensureString())->end() + ->validate()->ifNotInArray(['0.1'])->thenInvalid('unsupported version')->end() + ->end() + ->booleanNode('disabled')->defaultFalse()->end() + ->append($this->getResourceConfig()) + ->append($this->getAttributeLimitsConfig()) + ->append($registry->component('propagator', TextMapPropagatorInterface::class)) + ->append($this->getTracerProviderConfig($registry)) + ->append($this->getMeterProviderConfig($registry)) + ->append($this->getLoggerProviderConfig($registry)) + ->end(); + + return $node; + } + + private function getResourceConfig(): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('resource'); + $node + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('attributes') + ->variablePrototype()->end() + ->end() + ->scalarNode('schema_url')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->end(); + + return $node; + } + + private function getAttributeLimitsConfig(): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('attribute_limits'); + $node + ->addDefaultsIfNotSet() + ->children() + ->integerNode('attribute_value_length_limit')->min(0)->defaultNull()->end() + ->integerNode('attribute_count_limit')->min(0)->defaultValue(128)->end() + ->end(); + + return $node; + } + + private function getTracerProviderConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('tracer_provider'); + $node + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('limits') + ->addDefaultsIfNotSet() + ->children() + ->integerNode('attribute_value_length_limit')->min(0)->defaultNull()->end() + ->integerNode('attribute_count_limit')->min(0)->defaultNull()->end() + ->integerNode('event_count_limit')->min(0)->defaultValue(128)->end() + ->integerNode('link_count_limit')->min(0)->defaultValue(128)->end() + ->integerNode('event_attribute_count_limit')->min(0)->defaultNull()->end() + ->integerNode('link_attribute_count_limit')->min(0)->defaultNull()->end() + ->end() + ->end() + ->append($registry->component('sampler', SamplerInterface::class)) + ->append($registry->componentList('processors', SpanProcessorInterface::class)) + ->end() + ; + + return $node; + } + + private function getMeterProviderConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('meter_provider'); + $node + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('views') + ->arrayPrototype() + ->children() + ->arrayNode('stream') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('name')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('description')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->arrayNode('attribute_keys') + ->scalarPrototype()->validate()->always(Validation::ensureString())->end()->end() + ->end() + ->append($registry->component('aggregation', DefaultAggregationProviderInterface::class)) + ->end() + ->end() + ->arrayNode('selector') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('instrument_type') + ->values([ + 'counter', + 'histogram', + 'observable_counter', + 'observable_gauge', + 'observable_up_down_counter', + 'up_down_counter', + ]) + ->defaultNull() + ->end() + ->scalarNode('instrument_name')->defaultNull()->validate()->always(Validation::ensureString())->end()->cannotBeEmpty()->end() + ->scalarNode('unit')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('meter_name')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('meter_version')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('meter_schema_url')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->append($registry->componentList('readers', MetricReaderInterface::class)) + ->end() + ; + + return $node; + } + + private function getLoggerProviderConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('logger_provider'); + $node + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('limits') + ->addDefaultsIfNotSet() + ->children() + ->integerNode('attribute_value_length_limit')->min(0)->defaultNull()->end() + ->integerNode('attribute_count_limit')->min(0)->defaultNull()->end() + ->end() + ->end() + ->append($registry->componentList('processors', LogRecordProcessorInterface::class)) + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Propagator/TextMapPropagatorB3.php b/ComponentProvider/Propagator/TextMapPropagatorB3.php new file mode 100644 index 0000000..9640f1e --- /dev/null +++ b/ComponentProvider/Propagator/TextMapPropagatorB3.php @@ -0,0 +1,34 @@ + + */ +#[PackageDependency('open-telemetry/extension-propagator-b3', '^1.0.1')] +final class TextMapPropagatorB3 implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): TextMapPropagatorInterface + { + return B3Propagator::getB3SingleHeaderInstance(); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('b3'); + } +} diff --git a/ComponentProvider/Propagator/TextMapPropagatorB3Multi.php b/ComponentProvider/Propagator/TextMapPropagatorB3Multi.php new file mode 100644 index 0000000..787e999 --- /dev/null +++ b/ComponentProvider/Propagator/TextMapPropagatorB3Multi.php @@ -0,0 +1,34 @@ + + */ +#[PackageDependency('open-telemetry/extension-propagator-b3', '^1.0.1')] +final class TextMapPropagatorB3Multi implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): TextMapPropagatorInterface + { + return B3Propagator::getB3MultiHeaderInstance(); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('b3multi'); + } +} diff --git a/ComponentProvider/Propagator/TextMapPropagatorBaggage.php b/ComponentProvider/Propagator/TextMapPropagatorBaggage.php new file mode 100644 index 0000000..ecfde7f --- /dev/null +++ b/ComponentProvider/Propagator/TextMapPropagatorBaggage.php @@ -0,0 +1,32 @@ + + */ +final class TextMapPropagatorBaggage implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): TextMapPropagatorInterface + { + return BaggagePropagator::getInstance(); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('baggage'); + } +} diff --git a/ComponentProvider/Propagator/TextMapPropagatorComposite.php b/ComponentProvider/Propagator/TextMapPropagatorComposite.php new file mode 100644 index 0000000..6eba539 --- /dev/null +++ b/ComponentProvider/Propagator/TextMapPropagatorComposite.php @@ -0,0 +1,38 @@ + + */ +final class TextMapPropagatorComposite implements ComponentProvider +{ + + /** + * @param list> $properties + */ + public function createPlugin(array $properties, Context $context): TextMapPropagatorInterface + { + $propagators = []; + foreach ($properties as $plugin) { + $propagators[] = $plugin->create($context); + } + + return new MultiTextMapPropagator($propagators); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return $registry->componentNames('composite', TextMapPropagatorInterface::class); + } +} diff --git a/ComponentProvider/Propagator/TextMapPropagatorJaeger.php b/ComponentProvider/Propagator/TextMapPropagatorJaeger.php new file mode 100644 index 0000000..6d8e2f4 --- /dev/null +++ b/ComponentProvider/Propagator/TextMapPropagatorJaeger.php @@ -0,0 +1,34 @@ + + */ +#[PackageDependency('open-telemetry/extension-propagator-jaeger', '^0.0.2')] +final class TextMapPropagatorJaeger implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): TextMapPropagatorInterface + { + return JaegerPropagator::getInstance(); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('jaeger'); + } +} diff --git a/ComponentProvider/Propagator/TextMapPropagatorTraceContext.php b/ComponentProvider/Propagator/TextMapPropagatorTraceContext.php new file mode 100644 index 0000000..0913be3 --- /dev/null +++ b/ComponentProvider/Propagator/TextMapPropagatorTraceContext.php @@ -0,0 +1,32 @@ + + */ +final class TextMapPropagatorTraceContext implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): TextMapPropagatorInterface + { + return TraceContextPropagator::getInstance(); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('tracecontext'); + } +} diff --git a/ComponentProvider/Trace/SamplerAlwaysOff.php b/ComponentProvider/Trace/SamplerAlwaysOff.php new file mode 100644 index 0000000..7da8a2d --- /dev/null +++ b/ComponentProvider/Trace/SamplerAlwaysOff.php @@ -0,0 +1,32 @@ + + */ +final class SamplerAlwaysOff implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): SamplerInterface + { + return new AlwaysOffSampler(); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('always_off'); + } +} diff --git a/ComponentProvider/Trace/SamplerAlwaysOn.php b/ComponentProvider/Trace/SamplerAlwaysOn.php new file mode 100644 index 0000000..15dbe61 --- /dev/null +++ b/ComponentProvider/Trace/SamplerAlwaysOn.php @@ -0,0 +1,32 @@ + + */ +final class SamplerAlwaysOn implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): SamplerInterface + { + return new AlwaysOnSampler(); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('always_on'); + } +} diff --git a/ComponentProvider/Trace/SamplerParentBased.php b/ComponentProvider/Trace/SamplerParentBased.php new file mode 100644 index 0000000..1212823 --- /dev/null +++ b/ComponentProvider/Trace/SamplerParentBased.php @@ -0,0 +1,58 @@ + + */ +final class SamplerParentBased implements ComponentProvider +{ + + /** + * @param array{ + * root: ComponentPlugin, + * remote_parent_sampled: ?ComponentPlugin, + * remote_parent_not_sampled: ?ComponentPlugin, + * local_parent_sampled: ?ComponentPlugin, + * local_parent_not_sampled: ?ComponentPlugin, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SamplerInterface + { + return new ParentBased( + root: $properties['root']->create($context), + remoteParentSampler: $properties['remote_parent_sampled']?->create($context) ?? new AlwaysOnSampler(), + remoteParentNotSampler: $properties['remote_parent_not_sampled']?->create($context) ?? new AlwaysOffSampler(), + localParentSampler: $properties['local_parent_sampled']?->create($context) ?? new AlwaysOnSampler(), + localParentNotSampler: $properties['local_parent_not_sampled']?->create($context) ?? new AlwaysOffSampler(), + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('parent_based'); + $node + ->children() + ->append($registry->component('root', SamplerInterface::class)->isRequired()) + ->append($registry->component('remote_parent_sampled', SamplerInterface::class)) + ->append($registry->component('remote_parent_not_sampled', SamplerInterface::class)) + ->append($registry->component('local_parent_sampled', SamplerInterface::class)) + ->append($registry->component('local_parent_not_sampled', SamplerInterface::class)) + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Trace/SamplerTraceIdRatioBased.php b/ComponentProvider/Trace/SamplerTraceIdRatioBased.php new file mode 100644 index 0000000..c66d203 --- /dev/null +++ b/ComponentProvider/Trace/SamplerTraceIdRatioBased.php @@ -0,0 +1,43 @@ + + */ +final class SamplerTraceIdRatioBased implements ComponentProvider +{ + + /** + * @param array{ + * ratio: float, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SamplerInterface + { + return new TraceIdRatioBasedSampler( + probability: $properties['ratio'], + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('trace_id_ratio_based'); + $node + ->children() + ->floatNode('ratio')->min(0)->max(1)->isRequired()->end() + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Trace/SpanExporterConsole.php b/ComponentProvider/Trace/SpanExporterConsole.php new file mode 100644 index 0000000..f36fe52 --- /dev/null +++ b/ComponentProvider/Trace/SpanExporterConsole.php @@ -0,0 +1,36 @@ + + */ +final class SpanExporterConsole implements ComponentProvider +{ + + /** + * @param array{} $properties + */ + public function createPlugin(array $properties, Context $context): SpanExporterInterface + { + return new ConsoleSpanExporter(Registry::transportFactory('stream')->create( + endpoint: 'php://stdout', + contentType: 'application/json', + )); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + return new ArrayNodeDefinition('console'); + } +} diff --git a/ComponentProvider/Trace/SpanExporterOtlp.php b/ComponentProvider/Trace/SpanExporterOtlp.php new file mode 100644 index 0000000..6166bd7 --- /dev/null +++ b/ComponentProvider/Trace/SpanExporterOtlp.php @@ -0,0 +1,75 @@ + + */ +#[PackageDependency('open-telemetry/exporter-otlp', '^1.0.5')] +final class SpanExporterOtlp implements ComponentProvider +{ + + /** + * @param array{ + * protocol: 'http/protobuf'|'http/json'|'grpc', + * endpoint: string, + * certificate: ?string, + * client_key: ?string, + * client_certificate: ?string, + * headers: array, + * compression: 'gzip'|null, + * timeout: int<0, max>, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SpanExporterInterface + { + $protocol = $properties['protocol']; + + return new SpanExporter(Registry::transportFactory($protocol)->create( + endpoint: $properties['endpoint'] . OtlpUtil::path(Signals::TRACE, $protocol), + contentType: Protocols::contentType($protocol), + headers: $properties['headers'], + compression: $properties['compression'], + timeout: $properties['timeout'], + cacert: $properties['certificate'], + cert: $properties['client_certificate'], + key: $properties['client_certificate'], + )); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('otlp'); + $node + ->children() + ->enumNode('protocol')->isRequired()->values(['http/protobuf', 'http/json', 'grpc'])->end() + ->scalarNode('endpoint')->isRequired()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('certificate')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('client_key')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->scalarNode('client_certificate')->defaultNull()->validate()->always(Validation::ensureString())->end()->end() + ->arrayNode('headers') + ->scalarPrototype()->end() + ->end() + ->enumNode('compression')->values(['gzip'])->defaultNull()->end() + ->integerNode('timeout')->min(0)->defaultValue(10)->end() + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Trace/SpanExporterZipkin.php b/ComponentProvider/Trace/SpanExporterZipkin.php new file mode 100644 index 0000000..4e13f49 --- /dev/null +++ b/ComponentProvider/Trace/SpanExporterZipkin.php @@ -0,0 +1,51 @@ + + */ +#[PackageDependency('open-telemetry/exporter-zipkin', '^1.0')] +final class SpanExporterZipkin implements ComponentProvider +{ + + /** + * @param array{ + * endpoint: string, + * timeout: int<0, max>, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SpanExporterInterface + { + return new Zipkin\Exporter(Registry::transportFactory('http')->create( + endpoint: $properties['endpoint'], + contentType: 'application/json', + timeout: $properties['timeout'], + )); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('zipkin'); + $node + ->children() + ->scalarNode('endpoint')->isRequired()->validate()->always(Validation::ensureString())->end()->end() + ->integerNode('timeout')->min(0)->defaultValue(10)->end() + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Trace/SpanProcessorBatch.php b/ComponentProvider/Trace/SpanProcessorBatch.php new file mode 100644 index 0000000..3688473 --- /dev/null +++ b/ComponentProvider/Trace/SpanProcessorBatch.php @@ -0,0 +1,60 @@ + + */ +final class SpanProcessorBatch implements ComponentProvider +{ + + /** + * @param array{ + * schedule_delay: int<0, max>, + * export_timeout: int<0, max>, + * max_queue_size: int<0, max>, + * max_export_batch_size: int<0, max>, + * exporter: ComponentPlugin, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SpanProcessorInterface + { + return new BatchSpanProcessor( + exporter: $properties['exporter']->create($context), + clock: ClockFactory::getDefault(), + maxQueueSize: $properties['max_queue_size'], + scheduledDelayMillis: $properties['schedule_delay'], + exportTimeoutMillis: $properties['export_timeout'], + maxExportBatchSize: $properties['max_export_batch_size'], + meterProvider: $context->meterProvider, + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('batch'); + $node + ->children() + ->integerNode('schedule_delay')->min(0)->defaultValue(5000)->end() + ->integerNode('export_timeout')->min(0)->defaultValue(30000)->end() + ->integerNode('max_queue_size')->min(0)->defaultValue(2048)->end() + ->integerNode('max_export_batch_size')->min(0)->defaultValue(512)->end() + ->append($registry->component('exporter', SpanExporterInterface::class)->isRequired()) + ->end() + ; + + return $node; + } +} diff --git a/ComponentProvider/Trace/SpanProcessorSimple.php b/ComponentProvider/Trace/SpanProcessorSimple.php new file mode 100644 index 0000000..ad15e00 --- /dev/null +++ b/ComponentProvider/Trace/SpanProcessorSimple.php @@ -0,0 +1,45 @@ + + */ +final class SpanProcessorSimple implements ComponentProvider +{ + + /** + * @param array{ + * exporter: ComponentPlugin, + * } $properties + */ + public function createPlugin(array $properties, Context $context): SpanProcessorInterface + { + return new SimpleSpanProcessor( + exporter: $properties['exporter']->create($context), + ); + } + + public function getConfig(ComponentProviderRegistry $registry): ArrayNodeDefinition + { + $node = new ArrayNodeDefinition('simple'); + $node + ->children() + ->append($registry->component('exporter', SpanExporterInterface::class)->isRequired()) + ->end() + ; + + return $node; + } +} diff --git a/Configuration.php b/Configuration.php new file mode 100644 index 0000000..b06f596 --- /dev/null +++ b/Configuration.php @@ -0,0 +1,61 @@ + $sdkPlugin + */ + private function __construct( + private readonly ComponentPlugin $sdkPlugin, + ) { + } + + public function create(Context $context = new Context()): SdkBuilder + { + return $this->sdkPlugin->create($context); + } + + /** + * @param string|list $file + */ + public static function parseFile( + string|array $file, + ?string $cacheFile = null, + bool $debug = true, + ): Configuration { + 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 OpenTelemetrySdk(), + new EnvSourceReader([ + new ServerEnvSource(), + new PhpIniEnvSource(), + ]), + ); + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..022d2ce --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# OpenTelemetry SDK configuration + +## Installation + +```shell +composer require open-telemetry/sdk-configuration +``` + +## Usage + +### Initialization from [configuration file](https://opentelemetry.io/docs/specs/otel/configuration/file-configuration/) + +```php +$configuration = Configuration::parseFile(__DIR__ . '/kitchen-sink.yaml'); +$sdkBuilder = $configuration->create(); +``` + +#### Performance considerations + +Parsing and processing the configuration is rather expensive. It is highly recommended to provide the `$cacheFile` +parameter when running in a shared-nothing setup. + +```php +$configuration = Configuration::parseFile( + __DIR__ . '/kitchen-sink.yaml', + __DIR__ . '/var/cache/opentelemetry.php', +); +$sdkBuilder = $configuration->create(); +``` + +## Contributing + +This repository is a read-only git subtree split. +To contribute, please see the main [OpenTelemetry PHP monorepo](https://github.com/open-telemetry/opentelemetry-php). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7b6809c --- /dev/null +++ b/composer.json @@ -0,0 +1,65 @@ +{ + "name": "open-telemetry/sdk-configuration", + "description": "SDK configuration for OpenTelemetry PHP.", + "keywords": ["opentelemetry", "otel", "sdk", "configuration"], + "type": "library", + "support": { + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php", + "docs": "https://opentelemetry.io/docs/php", + "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V" + }, + "license": "Apache-2.0", + "authors": [ + { + "name": "opentelemetry-php contributors", + "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" + } + ], + "require": { + "php": "^8.1", + "open-telemetry/sdk": "^1.0", + "tbachert/otel-sdk-configuration": "^0.1", + "tbachert/spi": "^0.2" + }, + "autoload": { + "psr-4": { + "OpenTelemetry\\Config\\SDK\\": "" + } + }, + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + }, + "spi": { + "Nevay\\OTelSDK\\Configuration\\ComponentProvider": [ + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Propagator\\TextMapPropagatorB3", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Propagator\\TextMapPropagatorB3Multi", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Propagator\\TextMapPropagatorBaggage", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Propagator\\TextMapPropagatorComposite", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Propagator\\TextMapPropagatorJaeger", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Propagator\\TextMapPropagatorTraceContext", + + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Trace\\SamplerAlwaysOff", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Trace\\SamplerAlwaysOn", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Trace\\SamplerParentBased", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Trace\\SamplerTraceIdRatioBased", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Trace\\SpanExporterConsole", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Trace\\SpanExporterOtlp", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Trace\\SpanExporterZipkin", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Trace\\SpanProcessorBatch", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Trace\\SpanProcessorSimple", + + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Metrics\\AggregationResolverDefault", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Metrics\\MetricExporterConsole", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Metrics\\MetricExporterOtlp", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Metrics\\MetricReaderPeriodic", + + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordExporterConsole", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordExporterOtlp", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorBatch", + "OpenTelemetry\\Config\\SDK\\ComponentProvider\\Logs\\LogRecordProcessorSimple" + ] + } + } +}