Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
330 changes: 165 additions & 165 deletions composer.lock

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions documentation/components/bridges/symfony-telemetry-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ This bundle integrates Flow PHP's Telemetry library with Symfony applications. I
- **Configurable exporters** - Console, memory, void, or OTLP with multiple transport options
- **Full Symfony configuration** - Configure everything through Symfony's config system

## Enabling and Disabling

The bundle is enabled by default. `enabled: false` keeps `Flow\Telemetry\Telemetry` autowirable but removes
everything behind it — exporters, transports, resource detectors, instrumentation, profiler and the shutdown hook.

```yaml
# config/packages/prod/flow_telemetry.yaml
flow_telemetry:
enabled: false
```

The surviving `Telemetry` uses void processors with sampling off: spans are non-recording, logs and metrics go
nowhere, `flush()` is a no-op. Named tracers, meters, loggers and `#[WithTelemetryChannel]` channel loggers are
still registered, so no injection point breaks. No Symfony service is decorated, and `framework_logger` /
`capture_framework_channels` are ignored — aliasing `logger` to a void logger would discard application logs.

`flow.telemetry.enabled` is always defined as a container parameter, `true` or `false`.

`enabled` is read at compile time, so `%env()%` is rejected — a placeholder would leave telemetry wired while you
believed it was off. Use a per-environment config file, or a parameter holding a literal bool. Environment
variables work everywhere else in the tree, including sections disabled mode drops.

For a runtime switch that keeps services and only stops egress, use `exporters.<name>.enabled`.

## Configuration Reference

> Any `error_handler:` field that appears under a provider, processor, or `otlp` exporter references a name from the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ final class ChannelLoggerPass implements CompilerPassInterface

private const string CAPTURE_PARAMETER = 'flow.telemetry.capture_framework_channels';

private const string ENABLED_PARAMETER = 'flow.telemetry.enabled';

private const string ATTRIBUTE_TARGET_PARAMETER = 'flow.telemetry.channel_attribute_target';

private const string FRAMEWORK_TAG = 'monolog.logger';
Expand Down Expand Up @@ -119,6 +121,13 @@ private function camelize(string $channel): string

private function capturesFrameworkChannels(ContainerBuilder $container): bool
{
if (
$container->hasParameter(self::ENABLED_PARAMETER)
&& $container->getParameter(self::ENABLED_PARAMETER) !== true
) {
return false;
}

if (!$container->hasParameter(self::CAPTURE_PARAMETER)) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ final class FrameworkLoggerPass implements CompilerPassInterface

public function process(ContainerBuilder $container): void
{
if (
$container->hasParameter('flow.telemetry.enabled')
&& $container->getParameter('flow.telemetry.enabled') !== true
) {
return;
}

$frameworkLogger = $container->hasParameter('flow.telemetry.framework_logger')
? $container->getParameter('flow.telemetry.framework_logger')
: null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ final class OTLPAvailabilityPass implements CompilerPassInterface

public function process(ContainerBuilder $container): void
{
if (
$container->hasParameter('flow.telemetry.enabled')
&& $container->getParameter('flow.telemetry.enabled') !== true
) {
return;
}

$otlpAvailable = class_exists(self::OTLP_BRIDGE_CLASS);
$container->setParameter('flow.telemetry.otlp_available', $otlpAvailable);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ public function configure(DefinitionConfigurator $definition): void
$definition
->rootNode()
->children()
->booleanNode('enabled')
->info('Enable the bundle. When false no flow.telemetry.* service is registered (default: true)')
->defaultTrue()
->end()
->arrayNode('resource')
->info(
'OpenTelemetry Resource configuration with automatic detection (https://opentelemetry.io/docs/specs/semconv/resource/)',
Expand Down Expand Up @@ -919,19 +923,41 @@ public function configure(DefinitionConfigurator $definition): void
}

/**
* @param array{resource: array{detectors?: array{enabled?: bool, static?: array{cache?: array{enabled?: bool, path?: null|string}, os?: array{enabled?: bool}, host?: array{enabled?: bool}, service?: array{enabled?: bool}, deployment?: array{enabled?: bool}, git?: array{enabled?: bool, binary?: string, working_directory?: null|string}, environment?: array{enabled?: bool}}, dynamic?: array{process?: array{enabled?: bool}}}, custom?: array<string, mixed>}, clock_service_id?: null|string, framework_logger?: null|string, capture_framework_channels?: bool, channel_attribute_target?: 'scope'|'signal'|'both', context_storage?: array{type?: string, service_id?: null|string}, propagator?: array{type?: string, service_id?: null|string}, exporters?: array<string, array<string, mixed>>, error_handlers?: array<string, array<string, mixed>>, tracer_provider?: array<string, mixed>, meter_provider?: array<string, mixed>, logger_provider?: array<string, mixed>, instrumentation?: FlowTelemetryInstrumentationConfig, profiler?: array{enabled?: bool|null, capture_logs?: bool}, tracers?: array<string, array{version?: string, schema_url?: null|string, attributes?: array{scope?: array<string, mixed>, signal?: array<string, mixed>}}>, meters?: array<string, array{version?: string, schema_url?: null|string, attributes?: array{scope?: array<string, mixed>, signal?: array<string, mixed>}}>, loggers?: array<string, array{version?: string, schema_url?: null|string, attributes?: array{scope?: array<string, mixed>, signal?: array<string, mixed>}}>} $config
* @param array{enabled: bool, resource: array{detectors?: array{enabled?: bool, static?: array{cache?: array{enabled?: bool, path?: null|string}, os?: array{enabled?: bool}, host?: array{enabled?: bool}, service?: array{enabled?: bool}, deployment?: array{enabled?: bool}, git?: array{enabled?: bool, binary?: string, working_directory?: null|string}, environment?: array{enabled?: bool}}, dynamic?: array{process?: array{enabled?: bool}}}, custom?: array<string, mixed>}, clock_service_id?: null|string, framework_logger?: null|string, capture_framework_channels?: bool, channel_attribute_target?: 'scope'|'signal'|'both', context_storage?: array{type?: string, service_id?: null|string}, propagator?: array{type?: string, service_id?: null|string}, exporters?: array<string, array<string, mixed>>, error_handlers?: array<string, array<string, mixed>>, tracer_provider?: array<string, mixed>, meter_provider?: array<string, mixed>, logger_provider?: array<string, mixed>, instrumentation?: FlowTelemetryInstrumentationConfig, profiler?: array{enabled?: bool|null, capture_logs?: bool}, tracers?: array<string, array{version?: string, schema_url?: null|string, attributes?: array{scope?: array<string, mixed>, signal?: array<string, mixed>}}>, meters?: array<string, array{version?: string, schema_url?: null|string, attributes?: array{scope?: array<string, mixed>, signal?: array<string, mixed>}}>, loggers?: array<string, array{version?: string, schema_url?: null|string, attributes?: array{scope?: array<string, mixed>, signal?: array<string, mixed>}}>} $config
*/
#[Override]
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
$builder->setParameter('flow.telemetry.framework_logger', $config['framework_logger'] ?? null);
$builder->setParameter('flow.telemetry.capture_framework_channels', $config['capture_framework_channels'] ?? false);
$builder->setParameter('flow.telemetry.channel_attribute_target', $config['channel_attribute_target'] ?? 'both');
if (!is_bool($config['enabled'])) {
throw new InvalidConfigurationException(
'flow_telemetry.enabled is evaluated when the container is compiled, so environment variables '
. 'are not supported. Use a per-environment config file (config/packages/prod/flow_telemetry.yaml) '
. 'or a container parameter instead.',
);
}

$builder->resolveEnvPlaceholders($config);

$builder->setParameter('flow.telemetry.enabled', $config['enabled']);

$tracers = ($config['tracers'] ?? []) + ['default' => []];
$meters = ($config['meters'] ?? []) + ['default' => []];
$loggers = ($config['loggers'] ?? []) + ['default' => []];

if (!$config['enabled']) {
$this->registerGlobalServices($config, $builder);
$this->registerNoOpTelemetry($config, $builder);
$this->registerTracers($tracers, $builder);
$this->registerMeters($meters, $builder);
$this->registerLoggers($loggers, $builder);

return;
}

$builder->setParameter('flow.telemetry.framework_logger', $config['framework_logger'] ?? null);
$builder->setParameter('flow.telemetry.capture_framework_channels', $config['capture_framework_channels'] ?? false);
$builder->setParameter('flow.telemetry.channel_attribute_target', $config['channel_attribute_target'] ?? 'both');

$this->registerGlobalServices($config, $builder);
$errorHandlers = $config['error_handlers'] ?? [];
$this->registerErrorHandlers($errorHandlers, $builder);
Expand Down Expand Up @@ -3496,6 +3522,44 @@ private function wrapExporterWithRuntimeToggle(string $serviceId, mixed $enabled
/**
* @param FlowTelemetryInstrumentationConfig $config
*/
/**
* @param array{resource?: array{custom?: array<string, mixed>}} $config
*/
private function registerNoOpTelemetry(array $config, ContainerBuilder $builder): void
{
$resource = new Definition(Resource::class);
$resource->setFactory([Resource::class, 'create']);
$resource->setArgument(0, $config['resource']['custom'] ?? []);

$tracerProvider = new Definition(TracerProvider::class);
$tracerProvider->setArguments([
new Definition(VoidSpanProcessor::class),
new Reference('flow.telemetry.clock'),
new Reference('flow.telemetry.context_storage'),
new Definition(AlwaysOffSampler::class),
]);

$meterProvider = new Definition(MeterProvider::class);
$meterProvider->setArguments([
new Definition(VoidMetricProcessor::class),
new Reference('flow.telemetry.clock'),
]);

$loggerProvider = new Definition(LoggerProvider::class);
$loggerProvider->setArguments([
new Definition(VoidLogProcessor::class),
new Reference('flow.telemetry.clock'),
new Reference('flow.telemetry.context_storage'),
]);

$telemetry = new Definition(Telemetry::class);
$telemetry->setArguments([$resource, $tracerProvider, $meterProvider, $loggerProvider]);
$telemetry->setPublic(true);

$builder->setDefinition('flow.telemetry', $telemetry);
$builder->setAlias(Telemetry::class, 'flow.telemetry')->setPublic(true);
}

private function registerParameterOnlyInstrumentation(array $config, ContainerBuilder $builder): void
{
$httpClientConfig = $config['http_client'] ?? [];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\TelemetryBundle\Tests\Context;

use Flow\Bridge\Symfony\TelemetryBundle\Tests\Fixtures\Channel\FrameworkChannelConsumer;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

final readonly class ChannelLoggerContext
{
public function frameworkConsumer(string $channel): Definition
{
$definition = new Definition(FrameworkChannelConsumer::class);
$definition->setArgument(0, new Reference('logger'));
$definition->addTag('monolog.logger', ['channel' => $channel]);
$definition->setPublic(true);

return $definition;
}

/**
* @param class-string $class
*/
public function publicDefinition(string $class): Definition
{
$definition = new Definition($class);
$definition->setPublic(true);

return $definition;
}

/**
* @param class-string $class
*/
public function autoconfiguredConsumer(string $class): Definition
{
$definition = new Definition($class);
$definition->setAutowired(true);
$definition->setAutoconfigured(true);
$definition->setPublic(true);

return $definition;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
* @return array<mixed>
*/
public function processConfig(array $config): array
{
return $this->processRawConfig($config);
}

/**
* @return array<mixed>
*/
public function processRawConfig(mixed $config): array
{
$extension = (new FlowTelemetryBundle())->getContainerExtension();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,47 @@ public function bootKernel(array $options = []): TestKernel
return $this->kernel;
}

/**
* @return array<string, mixed>
*/
public function fullyPopulatedTelemetryConfig(bool $enabled): array
{
return [
'enabled' => $enabled,
'resource' => ['custom' => ['deployment.tenant' => '%env(default::FLOW_TELEMETRY_TEST_TENANT)%']],
'exporters' => [
'memory' => ['memory' => null],
'void' => ['void' => null],
'otlp_http' => [
'enabled' => '%env(bool:FLOW_TELEMETRY_TEST_EXPORTERS_ENABLED)%',
'otlp' => [
'transport' => [
'endpoint' => '%env(string:FLOW_TELEMETRY_TEST_OTLP_ENDPOINT)%',
],
],
],
],
'tracer_provider' => ['processor' => ['type' => 'memory', 'exporter' => 'memory']],
'instrumentation' => [
'http_kernel' => [
'enabled' => true,
'trace_controller_resolution' => true,
'trace_controller_arguments' => true,
'trace_controller_argument_resolvers' => true,
],
'console' => ['enabled' => true],
'messenger' => ['enabled' => true],
'http_client' => ['enabled' => true],
'psr18_client' => ['enabled' => true],
'dbal' => ['enabled' => true],
'cache' => ['enabled' => true],
],
'tracers' => ['app' => []],
'meters' => ['app' => []],
'loggers' => ['app' => []],
];
}

public function getContainer(): ContainerInterface
{
if ($this->kernel === null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\TelemetryBundle\Tests\Fixtures;

use Flow\Telemetry\Telemetry;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

use function array_filter;
use function array_keys;
use function array_merge;
use function array_values;
use function str_ends_with;
use function str_starts_with;

final class CompiledIdCollectorPass implements CompilerPassInterface
{
/** @var array<string> */
public array $ids = [];

/** @var array<string> */
public array $parameters = [];

public function process(ContainerBuilder $container): void
{
$this->ids = array_merge(array_keys($container->getDefinitions()), array_keys($container->getAliases()));

/** @var array<string> $parameterNames */
$parameterNames = array_keys($container->getParameterBag()->all());
$this->parameters = $parameterNames;
}

/**
* @return array<string>
*/
public function flowTelemetryIds(): array
{
return array_values(array_filter(
$this->ids,
static fn(string $id): bool => (
str_starts_with($id, 'flow.telemetry')
|| str_ends_with($id, '.flow_telemetry')
|| $id === Telemetry::class
),
));
}

/**
* @return array<string>
*/
public function flowTelemetryParameters(): array
{
return array_values(array_filter($this->parameters, static fn(string $name): bool => str_starts_with(
$name,
'flow.telemetry',
)));
}
}
Loading
Loading