|
1 | 1 | // Copyright (c) Microsoft. All rights reserved. |
2 | 2 |
|
3 | 3 | using KernelMemory.Core.Config; |
| 4 | +using KernelMemory.Core.Logging; |
4 | 5 | using KernelMemory.Main.CLI.Commands; |
5 | 6 | using KernelMemory.Main.CLI.Infrastructure; |
6 | 7 | using Microsoft.Extensions.DependencyInjection; |
| 8 | +using Microsoft.Extensions.Logging; |
| 9 | +using Serilog.Events; |
7 | 10 | using Spectre.Console.Cli; |
8 | 11 |
|
9 | 12 | namespace KernelMemory.Main.CLI; |
@@ -45,25 +48,36 @@ public sealed class CliApplicationBuilder |
45 | 48 | /// </summary> |
46 | 49 | /// <param name="args">Command line arguments (used to extract --config flag).</param> |
47 | 50 | /// <returns>A configured CommandApp ready to execute commands.</returns> |
| 51 | + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", |
| 52 | + Justification = "ILoggerFactory lifetime is managed by DI container and must remain alive for duration of CLI execution")] |
48 | 53 | public CommandApp Build(string[]? args = null) |
49 | 54 | { |
| 55 | + var actualArgs = args ?? []; |
| 56 | + |
50 | 57 | // 1. Determine config path from args early (before command execution) |
51 | | - string configPath = this.DetermineConfigPath(args ?? []); |
| 58 | + string configPath = this.DetermineConfigPath(actualArgs); |
52 | 59 |
|
53 | 60 | // 2. Load config ONCE (happens before any command runs) |
54 | 61 | AppConfig config = ConfigParser.LoadFromFile(configPath); |
55 | 62 |
|
56 | | - // 3. Create DI container and register AppConfig as singleton |
| 63 | + // 3. Parse logging options from args and config |
| 64 | + var loggingConfig = this.BuildLoggingConfig(actualArgs, config); |
| 65 | + |
| 66 | + // 4. Create logger factory using Serilog |
| 67 | + ILoggerFactory loggerFactory = SerilogFactory.CreateLoggerFactory(loggingConfig); |
| 68 | + |
| 69 | + // 5. Create DI container and register services |
57 | 70 | ServiceCollection services = new(); |
58 | 71 | services.AddSingleton(config); |
| 72 | + services.AddSingleton(loggerFactory); |
59 | 73 |
|
60 | 74 | // Also register the config path so commands can access it |
61 | 75 | services.AddSingleton(new ConfigPathService(configPath)); |
62 | 76 |
|
63 | | - // 4. Create type registrar for Spectre.Console.Cli DI integration |
| 77 | + // 6. Create type registrar for Spectre.Console.Cli DI integration |
64 | 78 | TypeRegistrar registrar = new(services); |
65 | 79 |
|
66 | | - // 5. Build CommandApp with DI support |
| 80 | + // 7. Build CommandApp with DI support |
67 | 81 | CommandApp app = new(registrar); |
68 | 82 | this.Configure(app); |
69 | 83 | return app; |
@@ -93,6 +107,61 @@ private string DetermineConfigPath(string[] args) |
93 | 107 | Constants.DefaultConfigFileName); |
94 | 108 | } |
95 | 109 |
|
| 110 | + /// <summary> |
| 111 | + /// Builds logging configuration from CLI args and app config. |
| 112 | + /// CLI args take precedence over config file settings. |
| 113 | + /// </summary> |
| 114 | + /// <param name="args">Command line arguments.</param> |
| 115 | + /// <param name="config">Application configuration.</param> |
| 116 | + /// <returns>Logging configuration for SerilogFactory.</returns> |
| 117 | + private LoggingConfig BuildLoggingConfig(string[] args, AppConfig config) |
| 118 | + { |
| 119 | + // Start with config file settings or defaults |
| 120 | + var loggingConfig = config.Logging ?? new LoggingConfig(); |
| 121 | + |
| 122 | + // Parse --verbosity / -v from args (takes precedence) |
| 123 | + var verbosity = this.ParseArgValue(args, "--verbosity", "-v"); |
| 124 | + if (verbosity != null) |
| 125 | + { |
| 126 | + loggingConfig.Level = verbosity.ToLowerInvariant() switch |
| 127 | + { |
| 128 | + "silent" => LogEventLevel.Fatal, |
| 129 | + "quiet" => LogEventLevel.Warning, |
| 130 | + "verbose" => LogEventLevel.Debug, |
| 131 | + _ => LogEventLevel.Information // "normal" and default |
| 132 | + }; |
| 133 | + } |
| 134 | + |
| 135 | + // Parse --log-file from args (takes precedence) |
| 136 | + var logFile = this.ParseArgValue(args, "--log-file", null); |
| 137 | + if (logFile != null) |
| 138 | + { |
| 139 | + loggingConfig.FilePath = logFile; |
| 140 | + } |
| 141 | + |
| 142 | + return loggingConfig; |
| 143 | + } |
| 144 | + |
| 145 | + /// <summary> |
| 146 | + /// Parses a value for a command line argument. |
| 147 | + /// </summary> |
| 148 | + /// <param name="args">Command line arguments.</param> |
| 149 | + /// <param name="longName">Long form of argument (e.g., "--verbosity").</param> |
| 150 | + /// <param name="shortName">Short form of argument (e.g., "-v"), or null if none.</param> |
| 151 | + /// <returns>The argument value, or null if not found.</returns> |
| 152 | + private string? ParseArgValue(string[] args, string longName, string? shortName) |
| 153 | + { |
| 154 | + for (int i = 0; i < args.Length - 1; i++) |
| 155 | + { |
| 156 | + if (args[i] == longName || (shortName != null && args[i] == shortName)) |
| 157 | + { |
| 158 | + return args[i + 1]; |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + return null; |
| 163 | + } |
| 164 | + |
96 | 165 | /// <summary> |
97 | 166 | /// Configures the CommandApp with all commands and examples. |
98 | 167 | /// Made public to allow tests to reuse command configuration. |
|
0 commit comments