diff --git a/README.md b/README.md index 6833f31..3be5806 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ Create `CWS_conf.yaml` using: Environment placeholders (for example `${CWS_DISCORD_WEBHOOK}`) are resolved from process env vars and from a `.env` file next to your config. -Logging level can be set with `CWS_LOG_LEVEL` (or `LOG_LEVEL`): `trace`, -`debug`, `info`, `warning`, `error`, `fatal`. Minimal example: -[.env.example](.env.example). +Set logging with `--log-level trace|debug|info|warn|error` or env +`CWS_LOG_LEVEL` / `LOG_LEVEL`. Precedence: CLI > env > default `info`. +Minimal env example: [.env.example](.env.example). Generate starter config: @@ -64,6 +64,8 @@ Run continuously: ```bash dart run bin/code_work_spawner.dart +# With higher verbosity for troubleshooting: +dart run bin/code_work_spawner.dart --log-level debug ``` Override CLI paths if needed: diff --git a/bin/code_work_spawner.dart b/bin/code_work_spawner.dart index 0dc64fd..d80c87d 100644 --- a/bin/code_work_spawner.dart +++ b/bin/code_work_spawner.dart @@ -57,6 +57,10 @@ class _CodeWorkSpawnerCommandRunner extends CommandRunner { 'gosmee-command', help: 'Path to the gosmee executable.', defaultsTo: 'gosmee', + ) + ..addOption( + 'log-level', + help: 'Minimum log level: trace, debug, info, warn, error.', ); addCommand(_InitConfigCommand()); @@ -64,6 +68,14 @@ class _CodeWorkSpawnerCommandRunner extends CommandRunner { @override Future runCommand(ArgResults topLevelResults) async { + final cliMinimumLevel = _resolveCliMinimumLevel( + topLevelResults['log-level'] as String?, + ); + _configureLogging( + cliMinimumLevel: cliMinimumLevel, + environment: AppEnvironment.variables, + ); + if (topLevelResults['help'] as bool) { stdout.writeln(usage); return; @@ -76,7 +88,10 @@ class _CodeWorkSpawnerCommandRunner extends CommandRunner { final configPath = topLevelResults['config'] as String; AppEnvironment.loadForConfig(configPath); - _configureLogging(); + _configureLogging( + cliMinimumLevel: cliMinimumLevel, + environment: AppEnvironment.variables, + ); final config = await AppConfig.load(configPath); final logger = AppLogger('code_work_spawner.cli'); @@ -124,6 +139,21 @@ class _CodeWorkSpawnerCommandRunner extends CommandRunner { await app.close(); } } + + AppLogLevel? _resolveCliMinimumLevel(String? value) { + if (value == null) { + return null; + } + final parsed = AppLogger.parseLevel(value); + if (parsed == null || parsed == AppLogLevel.fatal) { + throw UsageException( + 'Invalid value for --log-level "$value". ' + 'Allowed values: trace, debug, info, warn, error.', + usage, + ); + } + return parsed; + } } String _cliEventSourceSettings(IssueEventSourceConfig eventSource) { @@ -210,9 +240,14 @@ class _InitConfigCommand extends Command { } } -void _configureLogging() { +void _configureLogging({ + required Map environment, + AppLogLevel? cliMinimumLevel, +}) { AppLogger.configure( - minimumLevel: AppLogger.resolveMinimumLevel(AppEnvironment.variables), + minimumLevel: + cliMinimumLevel ?? + AppLogger.resolveMinimumLevel(environment, fallback: AppLogLevel.info), useColors: stdout.hasTerminal, ); } diff --git a/test/config/log_level_cli_test.dart b/test/config/log_level_cli_test.dart new file mode 100644 index 0000000..5466288 --- /dev/null +++ b/test/config/log_level_cli_test.dart @@ -0,0 +1,98 @@ +import 'package:code_work_spawner/code_work_spawner.dart'; +import 'package:test/test.dart'; + +import '../support/cli_utils.dart'; + +void main() { + /// ```gherkin + /// Feature: CLI log level option + /// + /// As an operator running code_work_spawner + /// I want to configure log verbosity with a CLI flag + /// So that I can easily collect the right level of diagnostics + /// ``` + group('code_work_spawner --log-level', () { + setUp(AppEnvironment.resetForTest); + + tearDown(() { + AppEnvironment.resetForTest(); + AppLogger.configure(minimumLevel: AppLogLevel.info, useColors: false); + }); + + /// ```gherkin + /// Scenario: Parse a valid log level from the CLI + /// Given the operator passes --log-level WARN + /// When the CLI argument parser initializes logging + /// Then the configured minimum level is warning + /// ``` + test('Parse a valid log level from the CLI', () async { + // Given the operator passes --log-level WARN. + final result = await runCli(['--log-level', 'WARN', '--help']); + + // When the CLI argument parser initializes logging. + final minimumLevel = AppLogger.minimumLevel; + + // Then the configured minimum level is warning. + expect(result.exitCode, 0); + expect(minimumLevel, AppLogLevel.warning); + }); + + /// ```gherkin + /// Scenario: Reject an invalid CLI log level + /// Given the operator passes --log-level verbose + /// When the CLI validates the value + /// Then the command exits with a non-zero status and prints allowed values + /// ``` + test('Reject an invalid CLI log level', () async { + // Given the operator passes --log-level verbose. + final result = await runCli(['--log-level', 'verbose']); + + // When the CLI validates the value. + final output = result.stdout; + + // Then the command exits with a non-zero status and prints allowed values. + expect(result.exitCode, isNonZero); + expect(output, contains('Invalid value for --log-level "verbose".')); + expect( + output, + contains('Allowed values: trace, debug, info, warn, error.'), + ); + }); + + /// ```gherkin + /// Scenario: CLI log level overrides environment variables + /// Given LOG_LEVEL is set to error in the environment + /// When the operator passes --log-level debug + /// Then the minimum logging level is debug + /// ``` + test('CLI log level overrides environment variables', () async { + // Given LOG_LEVEL is set to error in the environment. + AppEnvironment.resetForTest(const {'LOG_LEVEL': 'error'}); + + // When the operator passes --log-level debug. + final result = await runCli(['--log-level', 'debug', '--help']); + + // Then the minimum logging level is debug. + expect(result.exitCode, 0); + expect(AppLogger.minimumLevel, AppLogLevel.debug); + }); + + /// ```gherkin + /// Scenario: Default log level falls back to info + /// Given no CLI or environment log level is configured + /// When the CLI initializes logging + /// Then the minimum logging level is info + /// ``` + test('Default log level falls back to info', () async { + // Given no CLI or environment log level is configured. + AppEnvironment.resetForTest(const {}); + + // When the CLI initializes logging. + final result = await runCli(['--help']); + + // Then the minimum logging level is info. + expect(result.exitCode, 0); + expect(AppLogger.minimumLevel, AppLogLevel.info); + }); + }); +}