From 820de74c9aa5c572797520438dbd259ab0aaa22a Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Fri, 19 Jun 2026 13:03:17 +0200 Subject: [PATCH] feat(flow-php/symfony-telemetry-bundle): align log output with CLI verbosity - tee log records to the running command output, filtered by -v/-vv/-vvv - opt-in logger_provider.console_output config with overridable verbosity_levels --- .../bridges/symfony-telemetry-bundle.md | 58 +++++++ .../TelemetryBundle/FlowTelemetryBundle.php | 73 ++++++++ .../Console/ConsoleLogOutputSubscriber.php | 36 ++++ .../Logger/ConsoleOutputLogProcessor.php | 92 ++++++++++ .../Logger/ConsoleVerbosityLevels.php | 113 +++++++++++++ .../Tests/Fixtures/Command/LoggingCommand.php | 33 ++++ .../Logger/ConsoleOutputLogTest.php | 159 ++++++++++++++++++ .../Tests/Mother/LogEntryMother.php | 13 ++ .../ConsoleLogOutputSubscriberTest.php | 59 +++++++ .../Logger/ConsoleOutputLogProcessorTest.php | 143 ++++++++++++++++ .../Logger/ConsoleVerbosityLevelsTest.php | 60 +++++++ 11 files changed, 839 insertions(+) create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/Console/ConsoleLogOutputSubscriber.php create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Logger/ConsoleOutputLogProcessor.php create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Logger/ConsoleVerbosityLevels.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/LoggingCommand.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Logger/ConsoleOutputLogTest.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Console/ConsoleLogOutputSubscriberTest.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Logger/ConsoleOutputLogProcessorTest.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Logger/ConsoleVerbosityLevelsTest.php diff --git a/documentation/components/bridges/symfony-telemetry-bundle.md b/documentation/components/bridges/symfony-telemetry-bundle.md index 1bbff8c543..fe79419fbc 100644 --- a/documentation/components/bridges/symfony-telemetry-bundle.md +++ b/documentation/components/bridges/symfony-telemetry-bundle.md @@ -413,6 +413,64 @@ flow_telemetry: batch_size: 200 ``` +#### Console output (CLI verbosity) + +The Flow equivalent of Symfony's Monolog `ConsoleHandler`: while a console command runs, +emitted log records are also printed to that command's output, filtered by the command's +verbosity (`-v`/`-vv`/`-vvv`). This is **display only** — it does not change what the +configured processor exports (OTLP, etc.). It is **off by default**; like MonologBundle's +console handler, enable it explicitly (typically only in `dev`): + +```yaml +# config/packages/flow_telemetry.yaml +when@dev: + flow_telemetry: + logger_provider: + console_output: + enabled: true +``` + +The verbosity → minimum-severity thresholds default to: + +| Verbosity | Flag | Minimum severity | +|--------------------------|--------|------------------| +| `VERBOSITY_QUIET` | `-q` | `ERROR` | +| `VERBOSITY_NORMAL` | (none) | `ERROR` | +| `VERBOSITY_VERBOSE` | `-v` | `WARN` | +| `VERBOSITY_VERY_VERBOSE` | `-vv` | `INFO` | +| `VERBOSITY_DEBUG` | `-vvv` | `DEBUG` | + +Flow has no `NOTICE` level (the PSR-3 bridge collapses `NOTICE` into `INFO`), so there is +no rung between `WARN` and `INFO`. The defaults keep the terminal quiet by default (errors +only) and reveal exactly one more severity per `-v` step. + +`-vvv` is the most verbose flag Symfony has and the default map stops at `DEBUG`, so with +the defaults `TRACE` logs are never echoed to the console (they still reach your exporters, +e.g. OTLP). To see `TRACE` on the console, remap a flag to it with `verbosity_levels` — +keyed by the Symfony verbosity constant name, valued by a Flow severity name +(`TRACE|DEBUG|INFO|WARN|ERROR|FATAL`). For example, point `-vvv` at `TRACE`: + +```yaml +flow_telemetry: + logger_provider: + console_output: + enabled: true + verbosity_levels: + VERBOSITY_DEBUG: TRACE # -vvv now shows TRACE and everything above it +``` + +You can override any rung the same way — for instance, show `INFO` and above with no flag at all: + +```yaml +flow_telemetry: + logger_provider: + console_output: + enabled: true + verbosity_levels: + VERBOSITY_NORMAL: INFO # show INFO and above without any -v flag + VERBOSITY_VERBOSE: DEBUG +``` + ### Processor Configuration Processor types available for `tracer_provider`, `meter_provider`, and `logger_provider`. diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php index c2a1256871..b489fc0779 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php @@ -16,7 +16,10 @@ use Flow\Bridge\Symfony\TelemetryBundle\DependencyInjection\Compiler\ProfilerSignalCapturePass; use Flow\Bridge\Symfony\TelemetryBundle\DependencyInjection\Compiler\Psr18ClientTelemetryPass; use Flow\Bridge\Symfony\TelemetryBundle\Exception\RuntimeException; +use Flow\Bridge\Symfony\TelemetryBundle\Instrumentation\Console\ConsoleLogOutputSubscriber; use Flow\Bridge\Symfony\TelemetryBundle\Instrumentation\Messenger\MessengerTracePropagation; +use Flow\Bridge\Symfony\TelemetryBundle\Logger\ConsoleOutputLogProcessor; +use Flow\Bridge\Symfony\TelemetryBundle\Logger\ConsoleVerbosityLevels; use Flow\Bridge\Symfony\TelemetryBundle\Resource\Detector\SymfonyDeploymentDetector; use Flow\Bridge\Telemetry\OTLP\Exporter\OTLPExporter; use Flow\Bridge\Telemetry\OTLP\Serializer\JsonSerializer; @@ -446,6 +449,24 @@ public function configure(DefinitionConfigurator $definition): void ->info('Name of an error_handler entry forwarded to the LoggerProvider') ->defaultValue('default') ->end() + ->arrayNode('console_output') + ->info( + 'Tee emitted log records to the running console command output, filtered by CLI verbosity (-v/-vv/-vvv). The Flow equivalent of Symfony\'s Monolog ConsoleHandler; display only, exporters are unaffected. Off by default.', + ) + ->canBeEnabled() + ->children() + ->arrayNode('verbosity_levels') + ->info( + 'Override the verbosity->minimum-severity thresholds. Keys: VERBOSITY_QUIET, VERBOSITY_NORMAL, VERBOSITY_VERBOSE, VERBOSITY_VERY_VERBOSE, VERBOSITY_DEBUG. Values: TRACE, DEBUG, INFO, WARN, ERROR, FATAL. Defaults: QUIET=ERROR, NORMAL=WARN, VERBOSE=INFO, VERY_VERBOSE=DEBUG, DEBUG=TRACE.', + ) + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->enumPrototype() + ->values(['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL']) + ->end() + ->end() + ->end() + ->end() ->append($this->processorNode('log')) ->end() ->end() @@ -1275,6 +1296,18 @@ private function buildLoggerProvider(array $config, ContainerBuilder $builder): $processorServiceId = $this->buildLogProcessor($processorConfig, $providerServiceId, $builder); $errorHandlerRef = $this->resolveErrorHandlerReference($config['error_handler'] ?? 'default', $builder); + $consoleOutputConfig = is_array($config['console_output'] ?? null) ? $config['console_output'] : []; + + if ((bool) ($consoleOutputConfig['enabled'] ?? false)) { + $processorServiceId = $this->buildConsoleOutputLogging( + $consoleOutputConfig, + $providerServiceId, + $processorServiceId, + $errorHandlerRef, + $builder, + ); + } + $definition = new Definition(LoggerProvider::class); $definition->setArgument(0, new Reference($processorServiceId)); $definition->setArgument(1, new Reference('flow.telemetry.clock')); @@ -1285,6 +1318,46 @@ private function buildLoggerProvider(array $config, ContainerBuilder $builder): return $providerServiceId; } + /** + * Wraps the configured export processor in a composite that also tees records to + * the running console command output, and registers the supporting holder and + * event subscriber. Returns the id of the composite to use as the provider's processor. + * + * @param array $config + */ + private function buildConsoleOutputLogging( + array $config, + string $providerServiceId, + string $exportProcessorServiceId, + Reference $errorHandlerRef, + ContainerBuilder $builder, + ): string { + /** @var array $verbosityLevels */ + $verbosityLevels = is_array($config['verbosity_levels'] ?? null) ? $config['verbosity_levels'] : []; + $levelsDefinition = new Definition( + ConsoleVerbosityLevels::class, + [ConsoleVerbosityLevels::fromOverrides($verbosityLevels)->thresholds()], + ); + + $consoleProcessorId = $providerServiceId . '.console_output.processor'; + $builder->setDefinition( + $consoleProcessorId, + new Definition(ConsoleOutputLogProcessor::class, [$levelsDefinition]), + ); + + $subscriberDefinition = new Definition(ConsoleLogOutputSubscriber::class, [new Reference($consoleProcessorId)]); + $subscriberDefinition->addTag('kernel.event_subscriber'); + $builder->setDefinition($providerServiceId . '.console_output.subscriber', $subscriberDefinition); + + $compositeId = $exportProcessorServiceId . '.with_console_output'; + $compositeDefinition = new Definition(CompositeLogProcessor::class); + $compositeDefinition->setArgument(0, [new Reference($exportProcessorServiceId), new Reference($consoleProcessorId)]); + $compositeDefinition->setArgument(1, $errorHandlerRef); + $builder->setDefinition($compositeId, $compositeDefinition); + + return $compositeId; + } + /** * @param array $config */ diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/Console/ConsoleLogOutputSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/Console/ConsoleLogOutputSubscriber.php new file mode 100644 index 0000000000..8e14a406af --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/Console/ConsoleLogOutputSubscriber.php @@ -0,0 +1,36 @@ + ['onCommand', 9000], + ConsoleEvents::TERMINATE => ['onTerminate', -30000], + ]; + } + + public function onCommand(ConsoleCommandEvent $event): void + { + $this->processor->setOutput($event->getOutput()); + } + + public function onTerminate(ConsoleTerminateEvent $event): void + { + $this->processor->clearOutput(); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Logger/ConsoleOutputLogProcessor.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Logger/ConsoleOutputLogProcessor.php new file mode 100644 index 0000000000..7a64652be4 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Logger/ConsoleOutputLogProcessor.php @@ -0,0 +1,92 @@ +output = null; + } + + public function flush(): bool + { + return true; + } + + public function process(LogEntry $entry): void + { + if ($this->output === null) { + return; + } + + if (!$entry->record->severity->isAtLeast($this->levels->thresholdFor($this->output->getVerbosity()))) { + return; + } + + $this->output->writeln($this->format($entry), OutputInterface::VERBOSITY_QUIET); + } + + public function setOutput(OutputInterface $output): void + { + $this->output = $output; + } + + public function shutdown(): void {} + + /** + * @param array|bool|float|int|string> $attributes + */ + private function formatAttributes(array $attributes): string + { + $parts = []; + + foreach ($attributes as $key => $value) { + $rendered = is_scalar($value) ? (string) $value : (json_encode($value) ?: ''); + $parts[] = $key . '=' . OutputFormatter::escape($rendered); + } + + return '{' . implode(', ', $parts) . '}'; + } + + private function format(LogEntry $entry): string + { + $severity = $entry->record->severity; + $level = match ($severity) { + Severity::FATAL, Severity::ERROR => '' . $severity->name() . '', + Severity::WARN => '' . $severity->name() . '', + Severity::INFO => '' . $severity->name() . '', + default => $severity->name(), + }; + + $line = + $entry->timestamp->format('H:i:s.v') . ' [' . $level . '] ' . OutputFormatter::escape($entry->record->body); + + $attributes = $entry->record->attributes->normalize(); + + if (count($attributes) > 0) { + $line .= ' ' . $this->formatAttributes($attributes); + } + + return $line; + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Logger/ConsoleVerbosityLevels.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Logger/ConsoleVerbosityLevels.php new file mode 100644 index 0000000000..88f133565f --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Logger/ConsoleVerbosityLevels.php @@ -0,0 +1,113 @@ + ERROR + * NORMAL => ERROR + * VERBOSE -v => WARN + * VERY.. -vv => INFO + * DEBUG -vvv => DEBUG + * + * TRACE is never reached by the default ladder; opt into it via verbosity_levels. + */ +final readonly class ConsoleVerbosityLevels +{ + private const array CONSTANT_NAMES = [ + 'VERBOSITY_QUIET' => OutputInterface::VERBOSITY_QUIET, + 'VERBOSITY_NORMAL' => OutputInterface::VERBOSITY_NORMAL, + 'VERBOSITY_VERBOSE' => OutputInterface::VERBOSITY_VERBOSE, + 'VERBOSITY_VERY_VERBOSE' => OutputInterface::VERBOSITY_VERY_VERBOSE, + 'VERBOSITY_DEBUG' => OutputInterface::VERBOSITY_DEBUG, + ]; + + /** + * @param array $thresholds verbosity constant => minimum severity + */ + public function __construct( + private array $thresholds, + ) {} + + public static function default(): self + { + return new self([ + OutputInterface::VERBOSITY_QUIET => Severity::ERROR, + OutputInterface::VERBOSITY_NORMAL => Severity::ERROR, + OutputInterface::VERBOSITY_VERBOSE => Severity::WARN, + OutputInterface::VERBOSITY_VERY_VERBOSE => Severity::INFO, + OutputInterface::VERBOSITY_DEBUG => Severity::DEBUG, + ]); + } + + /** + * Build from the configured overrides, keyed by Symfony verbosity constant name + * (e.g. "VERBOSITY_NORMAL") mapped to a Flow severity name (e.g. "WARN"). + * + * @param array $overrides + */ + public static function fromOverrides(array $overrides): self + { + $thresholds = self::default()->thresholds; + + foreach ($overrides as $constantName => $severityName) { + if (!array_key_exists($constantName, self::CONSTANT_NAMES)) { + throw new RuntimeException(sprintf( + 'Unknown console verbosity level "%s"; expected one of: %s.', + $constantName, + implode(', ', array_keys(self::CONSTANT_NAMES)), + )); + } + + $thresholds[self::CONSTANT_NAMES[$constantName]] = self::severityFromName($severityName); + } + + return new self($thresholds); + } + + public function thresholdFor(int $verbosity): Severity + { + return $this->thresholds[$verbosity] ?? Severity::WARN; + } + + /** + * @return array + */ + public function thresholds(): array + { + return $this->thresholds; + } + + private static function severityFromName(string $name): Severity + { + return match ($name) { + 'TRACE' => Severity::TRACE, + 'DEBUG' => Severity::DEBUG, + 'INFO' => Severity::INFO, + 'WARN' => Severity::WARN, + 'ERROR' => Severity::ERROR, + 'FATAL' => Severity::FATAL, + default => throw new RuntimeException(sprintf( + 'Unknown severity "%s"; expected one of: TRACE, DEBUG, INFO, WARN, ERROR, FATAL.', + $name, + )), + }; + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/LoggingCommand.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/LoggingCommand.php new file mode 100644 index 0000000000..77cb30ecb6 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/LoggingCommand.php @@ -0,0 +1,33 @@ +telemetry->logger('test'); + $logger->error('error-message'); + $logger->warn('warn-message'); + $logger->info('info-message'); + $logger->debug('debug-message'); + $logger->trace('trace-message'); + + return Command::SUCCESS; + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Logger/ConsoleOutputLogTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Logger/ConsoleOutputLogTest.php new file mode 100644 index 0000000000..3e165f37e5 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Logger/ConsoleOutputLogTest.php @@ -0,0 +1,159 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel): void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => ['utf8' => true, 'resource' => __DIR__ . '/../../Fixtures/config/routes.php'], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'resource' => [], + 'exporters' => ['memory' => ['memory' => null], 'void' => ['void' => null]], + 'logger_provider' => [ + 'processor' => ['type' => 'memory', 'exporter' => 'memory'], + 'console_output' => ['enabled' => true], + ], + 'instrumentation' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + /** @var Telemetry $telemetry */ + $telemetry = $this->getContainer()->get('flow.telemetry'); + $application = new Application($kernel); + $this->addCommand($application, new LoggingCommand($telemetry)); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $output = new BufferedOutput(); + $application->run(new ArrayInput(['command' => 'test:logging']), $output); + $written = $output->fetch(); + + static::assertStringContainsString('error-message', $written); + static::assertStringNotContainsString('warn-message', $written); + static::assertStringNotContainsString('info-message', $written); + static::assertStringNotContainsString('debug-message', $written); + static::assertStringNotContainsString('trace-message', $written); + + /** @var MemoryLogProcessor $processor */ + $processor = $this->getContainer()->get('flow.telemetry.logger_provider.processor'); + static::assertSame(5, $processor->countLogs()); + } + + public function test_quiet_verbosity_shows_errors_only(): void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel): void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => ['utf8' => true, 'resource' => __DIR__ . '/../../Fixtures/config/routes.php'], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'resource' => [], + 'exporters' => ['memory' => ['memory' => null], 'void' => ['void' => null]], + 'logger_provider' => [ + 'processor' => ['type' => 'memory', 'exporter' => 'memory'], + 'console_output' => ['enabled' => true], + ], + 'instrumentation' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + /** @var Telemetry $telemetry */ + $telemetry = $this->getContainer()->get('flow.telemetry'); + $application = new Application($kernel); + $this->addCommand($application, new LoggingCommand($telemetry)); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $output = new BufferedOutput(); + $application->run(new ArrayInput(['command' => 'test:logging', '-q' => true]), $output); + $written = $output->fetch(); + + static::assertStringContainsString('error-message', $written); + static::assertStringNotContainsString('warn-message', $written); + } + + public function test_verbose_verbosity_shows_warnings_but_not_info(): void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel): void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => ['utf8' => true, 'resource' => __DIR__ . '/../../Fixtures/config/routes.php'], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'resource' => [], + 'exporters' => ['memory' => ['memory' => null], 'void' => ['void' => null]], + 'logger_provider' => [ + 'processor' => ['type' => 'memory', 'exporter' => 'memory'], + 'console_output' => ['enabled' => true], + ], + 'instrumentation' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + /** @var Telemetry $telemetry */ + $telemetry = $this->getContainer()->get('flow.telemetry'); + $application = new Application($kernel); + $this->addCommand($application, new LoggingCommand($telemetry)); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $output = new BufferedOutput(); + $application->run(new ArrayInput(['command' => 'test:logging', '-v' => true]), $output); + $written = $output->fetch(); + + static::assertStringContainsString('warn-message', $written); + static::assertStringNotContainsString('info-message', $written); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Mother/LogEntryMother.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Mother/LogEntryMother.php index 7606df7d08..4643ead561 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Mother/LogEntryMother.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Mother/LogEntryMother.php @@ -20,4 +20,17 @@ public static function onChannel(Severity $severity, string $channel): LogEntry 'log.channel' => $channel, ]), resource(), instrumentation_scope($channel), new DateTimeImmutable()); } + + /** + * @param array $attributes + */ + public static function with(Severity $severity, string $body = 'message', array $attributes = []): LogEntry + { + return new LogEntry( + new LogRecord($severity, $body, $attributes), + resource(), + instrumentation_scope('test'), + new DateTimeImmutable(), + ); + } } diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Console/ConsoleLogOutputSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Console/ConsoleLogOutputSubscriberTest.php new file mode 100644 index 0000000000..865c497c86 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Instrumentation/Console/ConsoleLogOutputSubscriberTest.php @@ -0,0 +1,59 @@ +onCommand( + new ConsoleCommandEvent(new Command('t'), new ArrayInput([]), $output), + ); + + $processor->process(LogEntryMother::with(Severity::ERROR, 'error-message')); + + static::assertStringContainsString('error-message', $output->fetch()); + } + + public function test_on_terminate_clears_the_output_from_the_processor(): void + { + $processor = new ConsoleOutputLogProcessor(ConsoleVerbosityLevels::default()); + $output = new BufferedOutput(); + $subscriber = new ConsoleLogOutputSubscriber($processor); + + $subscriber->onCommand(new ConsoleCommandEvent(new Command('t'), new ArrayInput([]), $output)); + $subscriber->onTerminate(new ConsoleTerminateEvent(new Command('t'), new ArrayInput([]), $output, 0)); + + $processor->process(LogEntryMother::with(Severity::ERROR, 'error-message')); + + static::assertSame('', $output->fetch()); + } + + public function test_subscribes_to_command_and_terminate(): void + { + $events = ConsoleLogOutputSubscriber::getSubscribedEvents(); + + static::assertArrayHasKey(ConsoleEvents::COMMAND, $events); + static::assertArrayHasKey(ConsoleEvents::TERMINATE, $events); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Logger/ConsoleOutputLogProcessorTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Logger/ConsoleOutputLogProcessorTest.php new file mode 100644 index 0000000000..365704b246 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/Logger/ConsoleOutputLogProcessorTest.php @@ -0,0 +1,143 @@ +setOutput($output); + + $processor->process(LogEntryMother::with(Severity::DEBUG, 'debug-message')); + + static::assertStringContainsString('debug-message', $output->fetch()); + } + + public function test_flush_is_a_noop_returning_true(): void + { + static::assertTrue((new ConsoleOutputLogProcessor(ConsoleVerbosityLevels::default()))->flush()); + } + + public function test_includes_attributes(): void + { + $output = new BufferedOutput(); + $processor = new ConsoleOutputLogProcessor(ConsoleVerbosityLevels::default()); + $processor->setOutput($output); + + $processor->process(LogEntryMother::with(Severity::ERROR, 'with-attrs', ['user.id' => 42])); + + static::assertStringContainsString('user.id=42', $output->fetch()); + } + + public function test_normal_verbosity_skips_warning(): void + { + $output = new BufferedOutput(); + $processor = new ConsoleOutputLogProcessor(ConsoleVerbosityLevels::default()); + $processor->setOutput($output); + + $processor->process(LogEntryMother::with(Severity::WARN, 'warn-message')); + + static::assertStringNotContainsString('warn-message', $output->fetch()); + } + + public function test_normal_verbosity_writes_error(): void + { + $output = new BufferedOutput(); + $processor = new ConsoleOutputLogProcessor(ConsoleVerbosityLevels::default()); + $processor->setOutput($output); + + $processor->process(LogEntryMother::with(Severity::ERROR, 'error-message')); + + $written = $output->fetch(); + static::assertStringContainsString('ERROR', $written); + static::assertStringContainsString('error-message', $written); + } + + public function test_nothing_written_after_output_is_cleared(): void + { + $output = new BufferedOutput(); + $processor = new ConsoleOutputLogProcessor(ConsoleVerbosityLevels::default()); + $processor->setOutput($output); + $processor->clearOutput(); + + $processor->process(LogEntryMother::with(Severity::ERROR, 'error-message')); + + static::assertSame('', $output->fetch()); + } + + public function test_nothing_written_when_no_output_is_set(): void + { + $processor = new ConsoleOutputLogProcessor(ConsoleVerbosityLevels::default()); + + $this->expectNotToPerformAssertions(); + + $processor->process(LogEntryMother::with(Severity::ERROR, 'error-message')); + } + + public function test_quiet_verbosity_shows_only_errors(): void + { + $output = new BufferedOutput(OutputInterface::VERBOSITY_QUIET); + $processor = new ConsoleOutputLogProcessor(ConsoleVerbosityLevels::default()); + $processor->setOutput($output); + + $processor->process(LogEntryMother::with(Severity::WARN, 'warn-message')); + $processor->process(LogEntryMother::with(Severity::ERROR, 'error-message')); + + $written = $output->fetch(); + static::assertStringNotContainsString('warn-message', $written); + static::assertStringContainsString('error-message', $written); + } + + public function test_user_angle_brackets_in_body_are_escaped(): void + { + $output = new BufferedOutput(); + $processor = new ConsoleOutputLogProcessor(ConsoleVerbosityLevels::default()); + $processor->setOutput($output); + + $processor->process(LogEntryMother::with(Severity::ERROR, 'rendered