Skip to content

Commit

Permalink
generate registry from composer autoload-dump
Browse files Browse the repository at this point in the history
To resolve our long-standing race conditions stemming from using composer's autoload->files
to registry SDK components at runtime, this changes things so that:
- components are registed in various composer.json files, under the extra.opentelemetry.registry key
- a composer script is registered to write this config out to a JSON file
- the SDK Registry is modified to make manually registering components a no-op (currently behind a flag, OTEL_PHP_EXPERIMENTAL_JSON_REGISTRY),
  and instead configure itself from the generated JSON file

If we move ahead with this approach, a follow-up PR could tidy up the Registry and remove our various late-binding providers.
  • Loading branch information
brettmc committed Oct 21, 2024
1 parent fd654b9 commit abd9944
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 2 deletions.
71 changes: 71 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,83 @@
"ext-yaml": "Allows loading config from yaml files",
"symfony/yaml": "Allows loading config from yaml files"
},
"scripts": {
"pre-autoload-dump": "OpenTelemetry\\SDK\\ComposerRegistry::generate"
},
"extra": {
"bamarni-bin": {
"bin-links": false,
"target-directory": "vendor-bin",
"forward-command": true
},
"opentelemetry": {
"registry": {
"OpenTelemetry\\SDK\\Common\\Export\\TransportFactoryInterface": {
"http": "OpenTelemetry\\Contrib\\Otlp\\OtlpHttpTransportFactory",
"grpc": "OpenTelemetry\\Contrib\\Grpc\\GrpcTransportFactory",
"stream": "OpenTelemetry\\SDK\\Common\\Export\\Stream\\StreamTransportFactory"
},
"OpenTelemetry\\SDK\\Trace\\SpanExporter\\SpanExporterFactoryInterface": {
"console": "OpenTelemetry\\SDK\\Trace\\SpanExporter\\ConsoleSpanExporterFactory",
"memory": "OpenTelemetry\\SDK\\Trace\\SpanExporter\\InMemorySpanExporterFactory",
"stream": "OpenTelemetry\\SDK\\Common\\Export\\Stream\\StreamTransportFactory",
"otlp": "OpenTelemetry\\Contrib\\Otlp\\SpanExporterFactory",
"zipkin": "OpenTelemetry\\Contrib\\Zipkin\\SpanExporterFactory"
},
"OpenTelemetry\\SDK\\Metrics\\MetricExporterFactoryInterface": {
"memory": "OpenTelemetry\\SDK\\Metrics\\MetricExporter\\InMemoryExporterFactory",
"console": "OpenTelemetry\\SDK\\Metrics\\MetricExporter\\ConsoleMetricExporterFactory",
"none": "OpenTelemetry\\SDK\\Metrics\\MetricExporter\\NoopMetricExporterFactory",
"otlp": "OpenTelemetry\\Contrib\\Otlp\\MetricExporterFactory"
},
"OpenTelemetry\\Context\\Propagation\\TextMapPropagatorInterface": {
"baggage": [
"OpenTelemetry\\API\\Baggage\\Propagation\\BaggagePropagator",
"getInstance"
],
"tracecontext": [
"OpenTelemetry\\API\\Trace\\Propagation\\TraceContextPropagator",
"getInstance"
],
"none": [
"OpenTelemetry\\Context\\Propagation\\NoopTextMapPropagator",
"getInstance"
],
"b3": [
"OpenTelemetry\\Extension\\Propagator\\B3\\B3Propagator",
"getB3SingleHeaderInstance"
],
"b3multi": [
"OpenTelemetry\\Extension\\Propagator\\B3\\B3Propagator",
"getB3MultiHeaderInstance"
],
"cloudtrace": [
"OpenTelemetry\\Extension\\Propagator\\CloudTrace\\CloudTracePropagator",
"getInstance"
],
"cloudtrace-oneway": [
"OpenTelemetry\\Extension\\Propagator\\CloudTrace\\CloudTracePropagator",
"getOneWayInstance"
],
"jaeger": [
"OpenTelemetry\\Extension\\Propagator\\Jaeger\\JaegerPropagator",
"getInstance"
],
"jaeger-baggage": [
"OpenTelemetry\\Extension\\Propagator\\Jaeger\\JaegerBaggagePropagator",
"getInstance"
]
},
"OpenTelemetry\\SDK\\Logs\\LogRecordExporterFactoryInterface": {
"console": "OpenTelemetry\\SDK\\Logs\\Exporter\\ConsoleExporterFactory",
"memory": "OpenTelemetry\\SDK\\Logs\\Exporter\\InMemoryExporterFactory",
"otlp": "OpenTelemetry\\Contrib\\Otlp\\LogsExporterFactory"
},
"OpenTelemetry\\SDK\\Resource\\ResourceDetectorInterface": {
"test": "OpenTelemetry\\Example\\TestResourceDetector"
}
}
},
"spi": {
"OpenTelemetry\\Config\\SDK\\Configuration\\ComponentProvider": [
"OpenTelemetry\\Config\\SDK\\ComponentProvider\\Propagator\\TextMapPropagatorB3",
Expand Down
22 changes: 22 additions & 0 deletions examples/src/TestResourceDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Example;

use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SemConv\ResourceAttributes;

class TestResourceDetector implements ResourceDetectorInterface
{
public function getResource(): ResourceInfo
{
$attributes = [
'test-resource' => 'test-value',
];

return ResourceInfo::create(Attributes::create($attributes), ResourceAttributes::SCHEMA_URL);
}
}
8 changes: 6 additions & 2 deletions examples/traces/features/auto_root_span.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
use OpenTelemetry\API\Logs\LogRecord;

putenv('OTEL_PHP_AUTOLOAD_ENABLED=true');
putenv('OTEL_TRACES_EXPORTER=console');
putenv('OTEL_TRACES_EXPORTER=otlp');
putenv('OTEL_METRICS_EXPORTER=none');
putenv('OTEL_LOGS_EXPORTER=console');
putenv('OTEL_LOGS_EXPORTER=otlp');
putenv('OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318');
putenv('OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf');
putenv('OTEL_PROPAGATORS=tracecontext');
putenv('OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN=true');
putenv('OTEL_PHP_EXPERIMENTAL_JSON_REGISTRY=true');
putenv('OTEL_PHP_DETECTORS=all');

//Usage: php -S localhost:8080 examples/traces/features/auto_root_span.php

Expand Down
1 change: 1 addition & 0 deletions src/SDK/Common/Configuration/Variables.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,5 @@ interface Variables
public const OTEL_PHP_EXCLUDED_URLS = 'OTEL_PHP_EXCLUDED_URLS';
public const OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN = 'OTEL_PHP_EXPERIMENTAL_AUTO_ROOT_SPAN';
public const OTEL_EXPERIMENTAL_CONFIG_FILE = 'OTEL_EXPERIMENTAL_CONFIG_FILE';
public const OTEL_PHP_EXPERIMENTAL_JSON_REGISTRY = 'OTEL_PHP_EXPERIMENTAL_JSON_REGISTRY'; //use composer-generated registry
}
28 changes: 28 additions & 0 deletions src/SDK/ComposerRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\SDK;

use Composer\Script\Event;
use Composer\Util\Filesystem;

class ComposerRegistry
{
public const FILENAME = 'opentelemetry_registry.json';

/**
* Generate a JSON file for the SDK registry from `composer.extra.opentelemetry.registry`, so that
* all required factories etc can be registered immediately.
*/
public static function generate(Event $event): void
{
$composer = $event->getComposer();
$extra = $composer->getPackage()->getExtra();
$json = json_encode($extra['opentelemetry']['registry'], JSON_PRETTY_PRINT);
$filesystem = new Filesystem();
$vendorDir = $filesystem->normalizePath($composer->getConfig()->get('vendor-dir'));
$filesystem->ensureDirectoryExists($vendorDir . '/composer');
$filesystem->filePutContentsIfModified($vendorDir . '/composer/' . self::FILENAME, $json);
}
}
52 changes: 52 additions & 0 deletions src/SDK/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace OpenTelemetry\SDK;

use function file_exists;
use function is_readable;
use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterFactoryInterface;
Expand All @@ -20,19 +22,46 @@
*/
class Registry
{
private static bool $initialized = false;
private static array $spanExporterFactories = [];
private static array $transportFactories = [];
private static array $metricExporterFactories = [];
private static array $textMapPropagators = [];
private static array $logRecordExporterFactories = [];
private static array $resourceDetectors = [];

public static function maybeInitFromComposerExtra(): void
{
if (self::$initialized || Sdk::useJsonRegistry() === false) {
return;
}
$file = dirname(__DIR__, 2) . '/vendor/composer/' . ComposerRegistry::FILENAME;
if (!file_exists($file) || !is_readable($file)) {
throw new RuntimeException("Generated file {$file} not found or not readable");
}
$data = json_decode(file_get_contents($file), true);
self::$spanExporterFactories = $data[SpanExporterFactoryInterface::class] ?? [];
self::$transportFactories = $data[TransportFactoryInterface::class] ?? [];
self::$metricExporterFactories = $data[MetricExporterFactoryInterface::class] ?? [];
self::$logRecordExporterFactories = $data[LogRecordExporterFactoryInterface::class] ?? [];
foreach ($data[ResourceDetectorInterface::class] ?? [] as $key => $class) {
self::$resourceDetectors[$key] = new $class();
}
foreach ($data[TextMapPropagatorInterface::class] ?? [] as $key => $pair) {
self::$textMapPropagators[$key] = call_user_func($pair);
}
self::$initialized = true;
}

/**
* @param TransportFactoryInterface|class-string<TransportFactoryInterface> $factory
* @throws TypeError
*/
public static function registerTransportFactory(string $protocol, TransportFactoryInterface|string $factory, bool $clobber = false): void
{
if (Sdk::useJsonRegistry()) {
return;
}
if (!$clobber && array_key_exists($protocol, self::$transportFactories)) {
return;
}
Expand All @@ -54,6 +83,9 @@ public static function registerTransportFactory(string $protocol, TransportFacto
*/
public static function registerSpanExporterFactory(string $exporter, SpanExporterFactoryInterface|string $factory, bool $clobber = false): void
{
if (Sdk::useJsonRegistry()) {
return;
}
if (!$clobber && array_key_exists($exporter, self::$spanExporterFactories)) {
return;
}
Expand All @@ -75,6 +107,9 @@ public static function registerSpanExporterFactory(string $exporter, SpanExporte
*/
public static function registerMetricExporterFactory(string $exporter, MetricExporterFactoryInterface|string $factory, bool $clobber = false): void
{
if (Sdk::useJsonRegistry()) {
return;
}
if (!$clobber && array_key_exists($exporter, self::$metricExporterFactories)) {
return;
}
Expand All @@ -96,6 +131,9 @@ public static function registerMetricExporterFactory(string $exporter, MetricExp
*/
public static function registerLogRecordExporterFactory(string $exporter, LogRecordExporterFactoryInterface|string $factory, bool $clobber = false): void
{
if (Sdk::useJsonRegistry()) {
return;
}
if (!$clobber && array_key_exists($exporter, self::$logRecordExporterFactories)) {
return;
}
Expand All @@ -113,6 +151,9 @@ public static function registerLogRecordExporterFactory(string $exporter, LogRec

public static function registerTextMapPropagator(string $name, TextMapPropagatorInterface $propagator, bool $clobber = false): void
{
if (Sdk::useJsonRegistry()) {
return;
}
if (!$clobber && array_key_exists($name, self::$textMapPropagators)) {
return;
}
Expand All @@ -121,11 +162,15 @@ public static function registerTextMapPropagator(string $name, TextMapPropagator

public static function registerResourceDetector(string $name, ResourceDetectorInterface $detector): void
{
if (Sdk::useJsonRegistry()) {
return;
}
self::$resourceDetectors[$name] = $detector;
}

public static function spanExporterFactory(string $exporter): SpanExporterFactoryInterface
{
self::maybeInitFromComposerExtra();
if (!array_key_exists($exporter, self::$spanExporterFactories)) {
throw new RuntimeException('Span exporter factory not defined for: ' . $exporter);
}
Expand All @@ -138,6 +183,7 @@ public static function spanExporterFactory(string $exporter): SpanExporterFactor

public static function logRecordExporterFactory(string $exporter): LogRecordExporterFactoryInterface
{
self::maybeInitFromComposerExtra();
if (!array_key_exists($exporter, self::$logRecordExporterFactories)) {
throw new RuntimeException('LogRecord exporter factory not defined for: ' . $exporter);
}
Expand All @@ -154,6 +200,7 @@ public static function logRecordExporterFactory(string $exporter): LogRecordExpo
*/
public static function transportFactory(string $protocol): TransportFactoryInterface
{
self::maybeInitFromComposerExtra();
$protocol = explode('/', $protocol)[0];
if (!array_key_exists($protocol, self::$transportFactories)) {
throw new RuntimeException('Transport factory not defined for protocol: ' . $protocol);
Expand All @@ -167,6 +214,7 @@ public static function transportFactory(string $protocol): TransportFactoryInter

public static function metricExporterFactory(string $exporter): MetricExporterFactoryInterface
{
self::maybeInitFromComposerExtra();
if (!array_key_exists($exporter, self::$metricExporterFactories)) {
throw new RuntimeException('Metric exporter factory not registered for protocol: ' . $exporter);
}
Expand All @@ -179,6 +227,7 @@ public static function metricExporterFactory(string $exporter): MetricExporterFa

public static function textMapPropagator(string $name): TextMapPropagatorInterface
{
self::maybeInitFromComposerExtra();
if (!array_key_exists($name, self::$textMapPropagators)) {
throw new RuntimeException('Text map propagator not registered for: ' . $name);
}
Expand All @@ -188,6 +237,7 @@ public static function textMapPropagator(string $name): TextMapPropagatorInterfa

public static function resourceDetector(string $name): ResourceDetectorInterface
{
self::maybeInitFromComposerExtra();
if (!array_key_exists($name, self::$resourceDetectors)) {
throw new RuntimeException('Resource detector not registered for: ' . $name);
}
Expand All @@ -200,6 +250,8 @@ public static function resourceDetector(string $name): ResourceDetectorInterface
*/
public static function resourceDetectors(): array
{
self::maybeInitFromComposerExtra();

return array_values(self::$resourceDetectors);
}
}
5 changes: 5 additions & 0 deletions src/SDK/Sdk.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public static function isDisabled(): bool
return Configuration::getBoolean(Variables::OTEL_SDK_DISABLED);
}

public static function useJsonRegistry(): bool
{
return Configuration::getBoolean(Variables::OTEL_PHP_EXPERIMENTAL_JSON_REGISTRY, false);
}

/**
* Tests whether an auto-instrumentation package has been disabled by config
*/
Expand Down
13 changes: 13 additions & 0 deletions tests/Unit/SDK/FactoryRegistryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
namespace OpenTelemetry\Tests\Unit\SDK;

use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
use OpenTelemetry\SDK\ComposerRegistry;
use OpenTelemetry\SDK\Logs\LogRecordExporterFactoryInterface;
use OpenTelemetry\SDK\Metrics\MetricExporterFactoryInterface;
use OpenTelemetry\SDK\Registry;
use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
use OpenTelemetry\SDK\Trace\SpanExporter\SpanExporterFactoryInterface;
use OpenTelemetry\Tests\TestState;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
Expand All @@ -18,6 +22,8 @@
#[CoversClass(Registry::class)]
class FactoryRegistryTest extends TestCase
{
use TestState;

#[DataProvider('transportProtocolsProvider')]
public function test_default_transport_factories(string $name): void
{
Expand Down Expand Up @@ -141,4 +147,11 @@ public static function invalidFactoryProvider(): array
['\stdClass'],
];
}

public function test_retrieve_from_composer_extra(): void
{
$this->setEnvironmentVariable(Variables::OTEL_PHP_EXPERIMENTAL_JSON_REGISTRY, 'true');
$this->assertFileExists(dirname(__DIR__, 3) . '/vendor/composer/' . ComposerRegistry::FILENAME);
$this->assertInstanceOf(ResourceDetectorInterface::class, Registry::resourceDetector('test'));
}
}

0 comments on commit abd9944

Please sign in to comment.