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
58 changes: 58 additions & 0 deletions documentation/components/bridges/symfony-telemetry-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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'));
Expand All @@ -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<array-key, mixed> $config
*/
private function buildConsoleOutputLogging(
array $config,
string $providerServiceId,
string $exportProcessorServiceId,
Reference $errorHandlerRef,
ContainerBuilder $builder,
): string {
/** @var array<string, string> $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<array-key, mixed> $config
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\TelemetryBundle\Instrumentation\Console;

use Flow\Bridge\Symfony\TelemetryBundle\Logger\ConsoleOutputLogProcessor;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final readonly class ConsoleLogOutputSubscriber implements EventSubscriberInterface
{
public function __construct(
private ConsoleOutputLogProcessor $processor,
) {}

public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::COMMAND => ['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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\TelemetryBundle\Logger;

use Flow\Telemetry\Logger\LogEntry;
use Flow\Telemetry\Logger\LogProcessor;
use Flow\Telemetry\Logger\Severity;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\OutputInterface;

use function count;
use function implode;
use function is_scalar;
use function json_encode;

final class ConsoleOutputLogProcessor implements LogProcessor
{
private ?OutputInterface $output = null;

public function __construct(
private readonly ConsoleVerbosityLevels $levels,
) {}

public function clearOutput(): void
{
$this->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<string, array<array-key, mixed>|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 => '<error>' . $severity->name() . '</error>',
Severity::WARN => '<comment>' . $severity->name() . '</comment>',
Severity::INFO => '<info>' . $severity->name() . '</info>',
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;
}
}
Loading
Loading