Skip to content

Commit 29fe734

Browse files
committed
feature: use psr3 telemetry bridge in telemetry bundle
- fix Path::realPath bug - register psr loggers adapters automatically for telemetry loggers - when no logger detected, bundle will register default logger and use as main symfony logger
1 parent 53695e0 commit 29fe734

15 files changed

Lines changed: 428 additions & 21 deletions

File tree

documentation/components/bridges/symfony-telemetry-bundle.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ flow_telemetry:
4242
static:
4343
cache:
4444
enabled: true # Cache static attributes (default: true)
45-
path: null # Cache path (default: kernel cache dir)
45+
path: null # Cache file path (default: sys_get_temp_dir()/flow_telemetry_resource.cache)
4646
os:
4747
enabled: true # Detect os.type, os.name, os.version, os.description
4848
host:
@@ -75,6 +75,12 @@ flow_telemetry:
7575

7676
Static detectors are cached by default. Dynamic detectors run on every request/command.
7777

78+
The cache file lives outside Symfony's cache lifecycle on purpose: building the Symfony cache
79+
(via `cache:warmup`) at image build time would otherwise freeze runtime-dependent attributes
80+
such as `host.name` or `process.pid` from the build container. Defaulting to
81+
`sys_get_temp_dir()` keeps the cache per-runtime and avoids that pitfall. To invalidate it,
82+
delete the cache file or restart the process; `cache:clear` does not touch it.
83+
7884
Custom attributes override auto-detected values.
7985

8086
### Clock Configuration
@@ -660,6 +666,33 @@ flow_telemetry:
660666
version: '1.0.0' # default: 'unknown'
661667
```
662668

669+
### Main Logger
670+
671+
The bundle depends on [PSR-3 Telemetry Bridge](/documentation/components/bridges/psr3-telemetry-bridge.md) and registers a PSR-3 wrapper service for every named Telemetry logger at `flow.telemetry.<name>.logger.psr3`. This makes Flow Telemetry loggers usable as Symfony's `logger` service, removing the need for Monolog when telemetry is the only logging destination.
672+
673+
In addition, the bundle always registers a `default` logger, meter, and tracer — `flow.telemetry.default.logger`, `flow.telemetry.default.logger.psr3`, `flow.telemetry.default.meter`, `flow.telemetry.default.tracer` — regardless of what is configured under `loggers`/`meters`/`tracers`. Defining your own `default` entry under those keys is allowed and will override the auto-default.
674+
675+
**Options:**
676+
677+
| Option | Type | Default | Description |
678+
|---------------|------------------|---------|--------------------------------------------------------------------------------------------------------------|
679+
| `main_logger` | `string \| null` | `null` | Name of a logger configured under `loggers` (or the always-available `default`) to alias as Symfony `logger` |
680+
681+
**Behavior:**
682+
683+
- When `main_logger` is set, the bundle aliases the Symfony `logger` service to `flow.telemetry.<main_logger>.logger.psr3`. If no logger with that name exists, container compilation fails with a clear error.
684+
- When `main_logger` is `null` and Symfony's `logger` service is the default `Symfony\Component\HttpKernel\Log\Logger`, the bundle automatically aliases `logger` to `flow.telemetry.default.logger.psr3`.
685+
- When `main_logger` is `null` and `logger` is provided by another bundle (Monolog, custom alias, etc.), the bundle leaves `logger` alone.
686+
687+
```yaml
688+
flow_telemetry:
689+
loggers:
690+
app:
691+
version: '1.0.0'
692+
693+
main_logger: app # Symfony "logger" service -> flow.telemetry.app.logger.psr3
694+
```
695+
663696
## Pattern Matching
664697

665698
Several configuration options support pattern matching for exclusion lists (paths, commands, templates, etc.).
@@ -840,6 +873,22 @@ flow_telemetry:
840873
transport:
841874
endpoint: '%env(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)%'
842875
876+
loggers:
877+
app:
878+
version: '%env(APP_VERSION)%'
879+
audit:
880+
version: '%env(APP_VERSION)%'
881+
attributes:
882+
channel: audit
883+
meters:
884+
business:
885+
version: '%env(APP_VERSION)%'
886+
tracers:
887+
checkout:
888+
version: '%env(APP_VERSION)%'
889+
890+
main_logger: app # Symfony "logger" service -> flow.telemetry.app.logger.psr3
891+
843892
instrumentation:
844893
http_kernel:
845894
enabled: true

src/bridge/symfony/telemetry-bundle/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
"license": "MIT",
1717
"require": {
1818
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
19-
"flow-php/telemetry": "self.version",
19+
"flow-php/psr3-telemetry-bridge": "self.version",
2020
"flow-php/symfony-http-foundation-telemetry-bridge": "self.version",
21+
"flow-php/telemetry": "self.version",
2122
"psr/clock": "^1.0",
2223
"symfony/config": "^6.4 || ^7.4 || ^8.0",
2324
"symfony/console": "^6.4 || ^7.4 || ^8.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Bridge\Symfony\TelemetryBundle\DependencyInjection\Compiler;
6+
7+
use Flow\Bridge\Symfony\TelemetryBundle\Exception\RuntimeException;
8+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
9+
use Symfony\Component\DependencyInjection\ContainerBuilder;
10+
11+
final class MainLoggerPass implements CompilerPassInterface
12+
{
13+
private const string SYMFONY_DEFAULT_LOGGER = 'Symfony\\Component\\HttpKernel\\Log\\Logger';
14+
15+
public function process(ContainerBuilder $container) : void
16+
{
17+
$mainLogger = $container->hasParameter('flow.telemetry.main_logger')
18+
? $container->getParameter('flow.telemetry.main_logger')
19+
: null;
20+
21+
if ($mainLogger !== null) {
22+
if (!\is_string($mainLogger) || $mainLogger === '') {
23+
throw new RuntimeException('flow_telemetry.main_logger must be a non-empty string referencing a configured logger name.');
24+
}
25+
26+
$targetId = 'flow.telemetry.' . $mainLogger . '.logger.psr3';
27+
28+
if (!$container->hasDefinition($targetId) && !$container->hasAlias($targetId)) {
29+
throw new RuntimeException(\sprintf(
30+
'Configured main_logger "%s" does not have a registered PSR-3 wrapper service "%s". Make sure a logger with that name is configured under flow_telemetry.loggers, or use the always-available "default".',
31+
$mainLogger,
32+
$targetId,
33+
));
34+
}
35+
36+
$container->setAlias('logger', $targetId)->setPublic(true);
37+
38+
return;
39+
}
40+
41+
if ($container->hasAlias('logger')) {
42+
return;
43+
}
44+
45+
if (!$container->hasDefinition('logger')) {
46+
return;
47+
}
48+
49+
if ($container->getDefinition('logger')->getClass() !== self::SYMFONY_DEFAULT_LOGGER) {
50+
return;
51+
}
52+
53+
$container->setAlias('logger', 'flow.telemetry.default.logger.psr3')->setPublic(true);
54+
}
55+
}

src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ public function getConfigTreeBuilder() : TreeBuilder
3434
->addDefaultsIfNotSet()
3535
->children()
3636
->arrayNode('cache')
37-
->info('Caching configuration for static resource attributes')
37+
->info('File-based caching of static resource attributes. The cache file is intentionally outside Symfony\'s cache lifecycle so build-time cache:warmup does not freeze runtime-dependent attributes (host, process).')
3838
->addDefaultsIfNotSet()
3939
->children()
4040
->booleanNode('enabled')
4141
->info('Enable caching of static resource attributes')
4242
->defaultTrue()
4343
->end()
4444
->scalarNode('path')
45-
->info('Cache file path (default: kernel cache dir)')
45+
->info('Absolute path to the cache file. Default: sys_get_temp_dir()/flow_telemetry_resource.cache.')
4646
->defaultNull()
4747
->end()
4848
->end()
@@ -111,6 +111,10 @@ public function getConfigTreeBuilder() : TreeBuilder
111111
->info('Custom PSR-20 clock service ID. If not provided, uses built-in SystemClock.')
112112
->defaultNull()
113113
->end()
114+
->scalarNode('main_logger')
115+
->info('Name of the logger (matching a key under "loggers", or "default") whose PSR-3 wrapper will be aliased to Symfony\'s "logger" service. Leave null to auto-replace only when Symfony\'s default HttpKernel Logger is currently bound.')
116+
->defaultNull()
117+
->end()
114118
->arrayNode('context_storage')
115119
->info('Context storage configuration')
116120
->addDefaultsIfNotSet()

src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Flow\Bridge\Symfony\TelemetryBundle\DependencyInjection;
66

7+
use Flow\Bridge\Psr3\Telemetry\{LogRecordConverter, TelemetryLogger};
78
use Flow\Bridge\Symfony\TelemetryBundle\Exception\RuntimeException;
89
use Flow\Bridge\Symfony\TelemetryBundle\Resource\Detector\SymfonyDeploymentDetector;
910
use Flow\Bridge\Telemetry\OTLP\Exporter\{OTLPLogExporter, OTLPMetricExporter, OTLPSpanExporter};
@@ -66,17 +67,23 @@ public function load(array $configs, ContainerBuilder $container) : void
6667
{
6768
$loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
6869
$configuration = new Configuration();
69-
/** @var 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}, environment?: array{enabled?: bool}}, dynamic?: array{process?: array{enabled?: bool}}}, custom?: array<string, mixed>}, clock_service_id?: null|string, context_storage?: array{type?: string, service_id?: null|string}, propagator?: array{type?: string, service_id?: null|string}, tracer_provider?: array<string, mixed>, meter_provider?: array<string, mixed>, logger_provider?: array<string, mixed>, instrumentation?: array{http_kernel?: array{enabled?: bool, exclude_paths?: array<array{path: string, method?: null|string}>, context_propagation?: bool}, console?: array{enabled?: bool, exclude_commands?: array<string>}, messenger?: array{enabled?: bool, context_propagation?: bool}, twig?: array{enabled?: bool, trace_templates?: bool, trace_blocks?: bool, trace_macros?: bool, exclude_templates?: array<string>}, http_client?: array{enabled?: bool, exclude_clients?: array<string>}, psr18_client?: array{enabled?: bool, exclude_clients?: array<string>}, dbal?: array{enabled?: bool, log_sql?: bool, max_sql_length?: int, exclude_connections?: array<string>}, cache?: array{enabled?: bool, exclude_pools?: array<string>}}, tracers?: array<string, array{version?: string, schema_url?: null|string, attributes?: array<string, mixed>}>, meters?: array<string, array{version?: string, schema_url?: null|string, attributes?: array<string, mixed>}>, loggers?: array<string, array{version?: string, schema_url?: null|string, attributes?: array<string, mixed>}>} $config */
70+
/** @var 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}, environment?: array{enabled?: bool}}, dynamic?: array{process?: array{enabled?: bool}}}, custom?: array<string, mixed>}, clock_service_id?: null|string, main_logger?: null|string, context_storage?: array{type?: string, service_id?: null|string}, propagator?: array{type?: string, service_id?: null|string}, tracer_provider?: array<string, mixed>, meter_provider?: array<string, mixed>, logger_provider?: array<string, mixed>, instrumentation?: array{http_kernel?: array{enabled?: bool, exclude_paths?: array<array{path: string, method?: null|string}>, context_propagation?: bool}, console?: array{enabled?: bool, exclude_commands?: array<string>}, messenger?: array{enabled?: bool, context_propagation?: bool}, twig?: array{enabled?: bool, trace_templates?: bool, trace_blocks?: bool, trace_macros?: bool, exclude_templates?: array<string>}, http_client?: array{enabled?: bool, exclude_clients?: array<string>}, psr18_client?: array{enabled?: bool, exclude_clients?: array<string>}, dbal?: array{enabled?: bool, log_sql?: bool, max_sql_length?: int, exclude_connections?: array<string>}, cache?: array{enabled?: bool, exclude_pools?: array<string>}}, tracers?: array<string, array{version?: string, schema_url?: null|string, attributes?: array<string, mixed>}>, meters?: array<string, array{version?: string, schema_url?: null|string, attributes?: array<string, mixed>}>, loggers?: array<string, array{version?: string, schema_url?: null|string, attributes?: array<string, mixed>}>} $config */
7071
$config = $this->processConfiguration($configuration, $configs);
7172

73+
$container->setParameter('flow.telemetry.main_logger', $config['main_logger'] ?? null);
74+
75+
$tracers = ($config['tracers'] ?? []) + ['default' => []];
76+
$meters = ($config['meters'] ?? []) + ['default' => []];
77+
$loggers = ($config['loggers'] ?? []) + ['default' => []];
78+
7279
$this->registerGlobalServices($config, $container);
7380
$this->registerPropagator($config['propagator'] ?? [], $container);
7481
$this->registerResource($config['resource'], $container);
7582
$this->registerTelemetry($config, $container);
7683
$this->registerInstrumentation($config['instrumentation'] ?? [], $container, $loader);
77-
$this->registerTracers($config['tracers'] ?? [], $container);
78-
$this->registerMeters($config['meters'] ?? [], $container);
79-
$this->registerLoggers($config['loggers'] ?? [], $container);
84+
$this->registerTracers($tracers, $container);
85+
$this->registerMeters($meters, $container);
86+
$this->registerLoggers($loggers, $container);
8087
}
8188

8289
/**
@@ -907,6 +914,11 @@ private function registerGlobalServices(array $config, ContainerBuilder $contain
907914
} else {
908915
$container->setDefinition('flow.telemetry.context_storage', new Definition(MemoryContextStorage::class));
909916
}
917+
918+
$container->setDefinition(
919+
'flow.telemetry.psr3.log_record_converter',
920+
new Definition(LogRecordConverter::class)
921+
);
910922
}
911923

912924
/**
@@ -986,7 +998,14 @@ private function registerLoggers(array $config, ContainerBuilder $container) : v
986998
}
987999

9881000
$definition->setPublic(true);
989-
$container->setDefinition('flow.telemetry.' . $name . '.logger', $definition);
1001+
$loggerServiceId = 'flow.telemetry.' . $name . '.logger';
1002+
$container->setDefinition($loggerServiceId, $definition);
1003+
1004+
$psr3Definition = new Definition(TelemetryLogger::class);
1005+
$psr3Definition->setArgument(0, new Reference($loggerServiceId));
1006+
$psr3Definition->setArgument(1, new Reference('flow.telemetry.psr3.log_record_converter'));
1007+
$psr3Definition->setPublic(true);
1008+
$container->setDefinition($loggerServiceId . '.psr3', $psr3Definition);
9901009
}
9911010
}
9921011

@@ -1195,10 +1214,9 @@ private function registerResource(array $resourceConfig, ContainerBuilder $conta
11951214
$cacheEnabled = $cacheConfig['enabled'] ?? true;
11961215

11971216
if ($cacheEnabled) {
1198-
$cachePath = $cacheConfig['path'] ?? '%kernel.cache_dir%/flow_telemetry_resource.cache';
11991217
$cachingDefinition = new Definition(CachingDetector::class);
12001218
$cachingDefinition->setArgument(0, new Reference('flow.telemetry.resource.detector.static.chain'));
1201-
$cachingDefinition->setArgument(1, $cachePath);
1219+
$cachingDefinition->setArgument(1, $cacheConfig['path'] ?? null);
12021220
$container->setDefinition('flow.telemetry.resource.detector.static', $cachingDefinition);
12031221
} else {
12041222
$container->setAlias('flow.telemetry.resource.detector.static', 'flow.telemetry.resource.detector.static.chain');

src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
namespace Flow\Bridge\Symfony\TelemetryBundle;
66

7-
use Flow\Bridge\Symfony\TelemetryBundle\DependencyInjection\Compiler\{CacheTelemetryPass, DBALTelemetryPass, HttpClientTelemetryPass, OTLPAvailabilityPass, Psr18ClientTelemetryPass};
7+
use Flow\Bridge\Symfony\TelemetryBundle\DependencyInjection\Compiler\{CacheTelemetryPass, DBALTelemetryPass, HttpClientTelemetryPass, MainLoggerPass, OTLPAvailabilityPass, Psr18ClientTelemetryPass};
8+
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
89
use Symfony\Component\DependencyInjection\ContainerBuilder;
910
use Symfony\Component\HttpKernel\Bundle\Bundle;
1011

@@ -25,6 +26,7 @@ public function build(ContainerBuilder $container) : void
2526
parent::build($container);
2627

2728
$container->addCompilerPass(new OTLPAvailabilityPass());
29+
$container->addCompilerPass(new MainLoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -64);
2830

2931
if (\interface_exists(self::HTTP_CLIENT_INTERFACE)) {
3032
$container->addCompilerPass(new HttpClientTelemetryPass());

src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,11 @@ public function shutdown() : void
103103
if ($filesystem->exists($logDir)) {
104104
$filesystem->remove($logDir);
105105
}
106+
107+
$defaultResourceCache = \sys_get_temp_dir() . '/flow_telemetry_resource.cache';
108+
109+
if ($filesystem->exists($defaultResourceCache)) {
110+
$filesystem->remove($defaultResourceCache);
111+
}
106112
}
107113
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Bridge\Symfony\TelemetryBundle\Tests\Fixtures\Logger;
6+
7+
use Psr\Log\AbstractLogger;
8+
9+
final class StubLogger extends AbstractLogger
10+
{
11+
/** @var array<int, array{level: mixed, message: string|\Stringable, context: array<array-key, mixed>}> */
12+
public array $records = [];
13+
14+
/**
15+
* @param array<array-key, mixed> $context
16+
*/
17+
public function log($level, string|\Stringable $message, array $context = []) : void
18+
{
19+
$this->records[] = [
20+
'level' => $level,
21+
'message' => $message,
22+
'context' => $context,
23+
];
24+
}
25+
}

src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ protected function build(ContainerBuilder $container) : void
121121
public function process(ContainerBuilder $container) : void
122122
{
123123
foreach ($container->getDefinitions() as $id => $definition) {
124-
if (\str_starts_with($id, 'flow.telemetry') || \str_ends_with($id, '.flow_telemetry') || \str_starts_with($id, 'test.')) {
124+
if (\str_starts_with($id, 'flow.telemetry') || \str_ends_with($id, '.flow_telemetry') || \str_starts_with($id, 'test.') || \str_starts_with($id, 'cache.flow_telemetry')) {
125125
$definition->setPublic(true);
126126
}
127127
}

0 commit comments

Comments
 (0)