Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auto-instrumentation registration #1304

Open
wants to merge 36 commits into
base: main
Choose a base branch
from

Conversation

brettmc
Copy link
Collaborator

@brettmc brettmc commented May 6, 2024

  • porting Nevay's PoC registering auto-instrumentation packages via SPI with file-based config.
  • extend to support auto-loading:
    • invent a new env var, OTEL_PHP_INSTRUMENTATION_CONFIG_FILE to tell the autoloader where the config files are for auto-instrumentation
    • The existence of the OTEL_EXPERIMENTAL_CONFIG_FILE defines where the SDK config file resides (relative to CWD), and also acts as a trigger to autoload via SPI rather than our current env-based version
    • move single-use code (create hook manager + config parsing + actual registration) into classes

todo:

  • SDK autoloader should use noop hook manager if opentelemetry extension not available

Copy link

codecov bot commented May 6, 2024

Codecov Report

Attention: Patch coverage is 73.77049% with 48 lines in your changes are missing coverage. Please review.

Project coverage is 74.38%. Comparing base (eaba9e3) to head (1c0e1b5).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main    #1304      +/-   ##
============================================
+ Coverage     74.28%   74.38%   +0.10%     
- Complexity     2491     2539      +48     
============================================
  Files           353      365      +12     
  Lines          7135     7274     +139     
============================================
+ Hits           5300     5411     +111     
- Misses         1835     1863      +28     
Flag Coverage Δ
8.1 74.14% <73.77%> (+0.13%) ⬆️
8.2 74.26% <73.77%> (+0.03%) ⬆️
8.3 74.22% <73.77%> (+0.05%) ⬆️
8.4 74.23% <73.77%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
src/API/Globals.php 100.00% <100.00%> (+5.88%) ⬆️
...tion/AutoInstrumentation/ConfigurationRegistry.php 100.00% <100.00%> (ø)
...PI/Instrumentation/AutoInstrumentation/Context.php 100.00% <100.00%> (ø)
src/API/Logs/LateBindingLogger.php 100.00% <100.00%> (ø)
src/API/Logs/LateBindingLoggerProvider.php 100.00% <100.00%> (ø)
src/API/Metrics/LateBindingMeterProvider.php 100.00% <100.00%> (ø)
src/API/Trace/LateBindingTracer.php 100.00% <100.00%> (ø)
src/API/Trace/LateBindingTracerProvider.php 100.00% <100.00%> (ø)
.../Config/SDK/Configuration/ConfigurationFactory.php 74.50% <100.00%> (ø)
src/SDK/Registry.php 71.91% <ø> (ø)
... and 6 more

... and 5 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update eaba9e3...1c0e1b5. Read the comment docs.

src/SDK/Sdk.php Outdated Show resolved Hide resolved
src/API/Globals.php Outdated Show resolved Hide resolved
- 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
psalm doesn't complain now, so that should be good
@brettmc brettmc marked this pull request as ready for review May 8, 2024 00:54
@brettmc brettmc requested a review from a team as a code owner May 8, 2024 00:54
- 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
in passing, remove some dead code for handling invalid booleans - config already handles this correctly
src/SDK/SdkAutoloader.php Outdated Show resolved Hide resolved
src/SDK/SdkAutoloader.php Outdated Show resolved Hide resolved
src/SDK/SdkAutoloader.php Outdated Show resolved Hide resolved
{
/** @var HookManager $hookManager */
foreach (ServiceLoader::load(HookManager::class) as $hookManager) {
$scope = $hookManager->enable(Context::getCurrent())->activate();
Copy link
Contributor

Choose a reason for hiding this comment

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

I dislike this as it does not guarantee correct detach order for the scope. Also might lead to issues if Context::setStorage() is called afterwards. We could add a method that allows changing hook managers from default disabled to default enabled which would make this scope obsolete.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We could add a method that allows changing hook managers from default disabled to default enabled which would make this scope obsolete.

I do think it would be preferable if the hook manager was enabled by default. If you've installed auto-instrumentation packages, you should get auto-instrumentation (which is the current behaviour). Plus, I don't want developers to need to write code to enable hooks to run; the most common usage I think would be to instrument.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've switched ExtensionHookManager to being enabled by default. How one might access the hook manager to disable it isn't very clear if it was autoloaded - maybe it needs to be a singleton or registered in context?

src/SDK/SdkAutoloader.php Outdated Show resolved Hide resolved
if ($ignoreUrls === []) {
return false;
if (!Configuration::has(Variables::OTEL_PHP_INSTRUMENTATION_CONFIG_FILE)) {
return;
Copy link
Contributor

@Nevay Nevay May 10, 2024

Choose a reason for hiding this comment

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

IMO we should load instrumentations with empty configuration registry if the config file is not specified.

Edit: not possible yet as autoload.files might register factories using OpenTelemetry\SDK\Registry::registerXxxFactory()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It seems to work. autoload.files should not be an issue since everything continues to use globals initializers.
I've wrapped registering instrumentation in a try/catch/log, since some will not work without config. If we could work out a way to provide the default config rather than empty (ie, by processing the InstrumentationConfiguration without input), it might work a bit better?

Copy link
Contributor

Choose a reason for hiding this comment

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

If we could work out a way to provide the default config rather than empty (ie, by processing the InstrumentationConfiguration without input), it might work a bit better?

This is the bit I'm watching out for too, as I'm trying to keep existing behaviour locally without providing config files.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One solution could be to extend the InstrumentationConfiguration interface so that each config class can provide its own defaults:

interface InstrumentationConfiguration
{
    public static function default(): self;
}

class ExampleInstrumentation implements InstrumentationConfiguration
{
    public static function default(): self
    {
        return new self('default1', 'another-default');
    }
}

public function register(HookManager $hookManager, ConfigurationRegistry $configuration, InstrumentationContext $context): void
    {
        $config = $configuration->get(ExampleConfig::class) ?? ExampleConfig::default();
        //snip
    }
}

src/SDK/SdkAutoloader.php Outdated Show resolved Hide resolved
- 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
if no config provided, still try to load them. wrap registration in a try/catch/log
return;
}

$tracer = Globals::tracerProvider()->getTracer('example-instrumentation');
Copy link
Contributor

Choose a reason for hiding this comment

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

This triggers initialization during sdk autoloading; iff we want to use the Globals providers to delay initialization, we should specify that Globals must not be accessed outside of hook callbacks. Otherwise I don't see an advantage over injecting the providers.

Apart from that I would generally prefer if we inject the providers. It allows initializing telemetry instances outside of hook callbacks¹ and might allow us to change Globals to be a truly global state instead of being Context dependent.

¹ For example reusing a counter instance and calling ::add() directly is around three times faster than refetching the internal instrument from a CachedInstrumentation instance. (initial counter creation w/ single metric reader/metric stream takes ~33μs):

+---------------------------------+---------+----------+--------+---------+
| subject                         | memory  | mode     | rstdev | stdev   |
+---------------------------------+---------+----------+--------+---------+
| benchCounterAdd (empty)         | 2.716mb | 0.902μs  | ±1.95% | 0.018μs |
| benchCounterAddRecreate (empty) | 2.780mb | 3.319μs  | ±1.86% | 0.062μs |
| benchCounterAdd (http)          | 2.727mb | 1.954μs  | ±1.63% | 0.032μs |
| benchCounterAddRecreate (http)  | 2.783mb | 4.539μs  | ±1.73% | 0.079μs |
+---------------------------------+---------+----------+--------+---------+

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That makes sense, and I prefer the method of passing in the providers when registering an instrumentation. I don't think we can commit to file-based config and deprecate Registry just yet, given the experimental state of config.

But, I think we can start to move in that direction. How does this sound?

  1. update existing instrumentation classes to only retrieve providers from hook callbacks
  2. document this new restriction (probably in Globals)
  3. add a todo to Registry to deprecate in a future release (in case anybody's reading the source)
  4. pass in Noop providers to Instrumentations via ::register(), and document that these will become real implementations in a future release but to keep using Globals for now

We then wait for config to stabilise.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we can commit to file-based config and deprecate Registry just yet, given the experimental state of config.

The restriction / issue with Registry is not related to file-based config; any usage of Globals within an autoload.files file might trigger this issue already, for example open-telemetry/opentelemetry-auto-openai-php.

  1. If we have to stay compatible with Registry::register{Xxx}Factory for now:
    Instead of noop providers we could inject providers that resolve Globals on the first call to ::spanBuilder() / ::create{Instrument}() / ::emit(). This would allow us to use the injected providers immediately (with the same restriction as CachedInstrumentation that Tracer/Meter/Logger methods must only be called within hook callbacks for now).
TracerProvider example
$tracerProvider = new LateBindingTracerProvider(static function(): TracerProviderInterface {
    $scope = Context::getRoot()->activate();
    try {
        return Globals::tracerProvider();
    } finally {
        $scope->detach();
    }
});

final 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));
    }
}

final 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);
    }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Implemented late-binding providers based on the above example (thanks!)

// 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');
putenv('OTEL_PHP_INSTRUMENTATION_CONFIG_FILE=examples/instrumentation/otel-instrumentation.yaml');
Copy link
Collaborator Author

@brettmc brettmc May 21, 2024

Choose a reason for hiding this comment

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

NB that I'm working on a PR over in the config repo to incorporate instrumentation into the general file-based config. I did a quick test against this branch, and combining them was a pretty minor change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants