refactor(configmanager): convert CLI to structured logging with Serilog

Replace Console.WriteLine calls with ILogger usage across all CLI commands.
Serilog is configured via DI with clean message-only output suitable for
CLI tooling. Log levels map to --quiet (Warning), default (Information),
and --verbose (Debug) flags.

- Add Serilog packages and configure in Program.cs
- Convert all 7 command files to use ILoggerFactory from DI
- Add BeginScope with context properties (Command, ConfigPath, etc.)
- Create logging_style.md documenting patterns and best practices
- Update tests with TestLoggingHelper for Serilog test configuration
This commit is contained in:
Joseph Doherty
2026-01-28 15:53:08 -05:00
parent 61694ca50b
commit 6f3e12b3b4
18 changed files with 1684 additions and 1220 deletions
@@ -1,6 +1,7 @@
using System.CommandLine; using System.CommandLine;
using JdeScoping.ConfigManager.Core.Services; using JdeScoping.ConfigManager.Core.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Cli.Commands; namespace JdeScoping.ConfigManager.Cli.Commands;
@@ -104,38 +105,45 @@ public static class BackupCommands
bool verbose, bool verbose,
bool quiet) bool quiet)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("BackupCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "backup create",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var backupService = serviceProvider.GetRequiredService<IBackupService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var backupPath = await backupService.CreateBackupAsync(appSettingsPath);
if (!quiet)
{ {
Console.WriteLine("Backup created successfully"); logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.WriteLine($"Path: {backupPath}"); return 1;
} }
return 0; var backupService = serviceProvider.GetRequiredService<IBackupService>();
} var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
catch (Exception ex)
{ if (!File.Exists(appSettingsPath))
Console.Error.WriteLine($"Error: {ex.Message}"); {
return 1; logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1;
}
try
{
var backupPath = await backupService.CreateBackupAsync(appSettingsPath);
logger.LogInformation("Backup created successfully");
logger.LogInformation("Path: {BackupPath}", backupPath);
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create backup");
return 1;
}
} }
} }
@@ -145,53 +153,56 @@ public static class BackupCommands
bool verbose, bool verbose,
bool quiet) bool quiet)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("BackupCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "backup list",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var backupService = serviceProvider.GetRequiredService<IBackupService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
try
{ {
var backups = await backupService.GetBackupsAsync(appSettingsPath); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null)
if (!quiet)
{ {
Console.WriteLine($"=== Backups ({backups.Count}) ==="); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1;
} }
if (backups.Count == 0) var backupService = serviceProvider.GetRequiredService<IBackupService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
try
{ {
if (!quiet) var backups = await backupService.GetBackupsAsync(appSettingsPath);
Console.WriteLine("No backups found");
logger.LogInformation("=== Backups ({Count}) ===", backups.Count);
if (backups.Count == 0)
{
logger.LogInformation("No backups found");
return 0;
}
logger.LogInformation("{Timestamp,-20} {Size,-12} {Path}", "Timestamp", "Size", "Path");
logger.LogInformation("{Separator}", new string('-', 60));
foreach (var backup in backups)
{
var timestamp = backup.Timestamp.ToString("yyyy-MM-dd_HHmmss");
var size = FormatFileSize(backup.Size);
var path = verbose ? backup.Path : Path.GetFileName(backup.Path);
logger.LogInformation("{Timestamp,-20} {Size,-12} {Path}", timestamp, size, path);
}
return 0; return 0;
} }
catch (Exception ex)
if (!quiet)
{ {
Console.WriteLine($"{"Timestamp",-20} {"Size",-12} {"Path"}"); logger.LogError(ex, "Failed to list backups");
Console.WriteLine(new string('-', 60)); return 1;
} }
foreach (var backup in backups)
{
var timestamp = backup.Timestamp.ToString("yyyy-MM-dd_HHmmss");
var size = FormatFileSize(backup.Size);
var path = verbose ? backup.Path : Path.GetFileName(backup.Path);
Console.WriteLine($"{timestamp,-20} {size,-12} {path}");
}
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
} }
} }
@@ -202,62 +213,66 @@ public static class BackupCommands
bool quiet, bool quiet,
string timestamp) string timestamp)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("BackupCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "backup restore",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["BackupTimestamp"] = timestamp
}))
var backupService = serviceProvider.GetRequiredService<IBackupService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
try
{ {
var backups = await backupService.GetBackupsAsync(appSettingsPath); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
var backup = backups.FirstOrDefault(b => b.Path.Contains(timestamp)); if (folderPath == null)
if (backup == null)
{ {
Console.Error.WriteLine($"Error: Backup with timestamp '{timestamp}' not found"); logger.LogError("Could not find configuration folder. Use --config-path to specify");
if (backups.Count > 0)
{
Console.Error.WriteLine();
Console.Error.WriteLine("Available backups:");
foreach (var b in backups.Take(5))
{
Console.Error.WriteLine($" {b.Timestamp:yyyy-MM-dd_HHmmss}");
}
}
return 1; return 1;
} }
// Create a backup of current config before restoring var backupService = serviceProvider.GetRequiredService<IBackupService>();
if (File.Exists(appSettingsPath)) var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
try
{ {
var currentBackup = await backupService.CreateBackupAsync(appSettingsPath); var backups = await backupService.GetBackupsAsync(appSettingsPath);
if (!quiet) var backup = backups.FirstOrDefault(b => b.Path.Contains(timestamp));
if (backup == null)
{ {
Console.WriteLine($"Current config backed up to: {Path.GetFileName(currentBackup)}"); logger.LogError("Backup with timestamp '{Timestamp}' not found", timestamp);
if (backups.Count > 0)
{
logger.LogError("Available backups:");
foreach (var b in backups.Take(5))
{
logger.LogError(" {Timestamp}", b.Timestamp.ToString("yyyy-MM-dd_HHmmss"));
}
}
return 1;
} }
// Create a backup of current config before restoring
if (File.Exists(appSettingsPath))
{
var currentBackup = await backupService.CreateBackupAsync(appSettingsPath);
logger.LogInformation("Current config backed up to: {BackupName}", Path.GetFileName(currentBackup));
}
await backupService.RestoreBackupAsync(backup.Path, appSettingsPath);
logger.LogInformation("Backup restored successfully");
logger.LogInformation("Restored from: {BackupName}", Path.GetFileName(backup.Path));
return 0;
} }
catch (Exception ex)
await backupService.RestoreBackupAsync(backup.Path, appSettingsPath);
if (!quiet)
{ {
Console.WriteLine("Backup restored successfully"); logger.LogError(ex, "Failed to restore backup");
Console.WriteLine($"Restored from: {Path.GetFileName(backup.Path)}"); return 1;
} }
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
} }
} }
@@ -268,52 +283,60 @@ public static class BackupCommands
bool quiet, bool quiet,
int keep) int keep)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("BackupCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "backup cleanup",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["KeepCount"] = keep
}))
if (keep < 1)
{ {
Console.Error.WriteLine("Error: --keep must be at least 1"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
var backupService = serviceProvider.GetRequiredService<IBackupService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
try
{
var backupsBefore = await backupService.GetBackupsAsync(appSettingsPath);
var countBefore = backupsBefore.Count;
await backupService.CleanupOldBackupsAsync(appSettingsPath, keep);
var backupsAfter = await backupService.GetBackupsAsync(appSettingsPath);
var deleted = countBefore - backupsAfter.Count;
if (!quiet)
{ {
logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1;
}
if (keep < 1)
{
logger.LogError("--keep must be at least 1");
return 1;
}
var backupService = serviceProvider.GetRequiredService<IBackupService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
try
{
var backupsBefore = await backupService.GetBackupsAsync(appSettingsPath);
var countBefore = backupsBefore.Count;
await backupService.CleanupOldBackupsAsync(appSettingsPath, keep);
var backupsAfter = await backupService.GetBackupsAsync(appSettingsPath);
var deleted = countBefore - backupsAfter.Count;
if (deleted > 0) if (deleted > 0)
{ {
Console.WriteLine($"Deleted {deleted} old backup(s)"); logger.LogInformation("Deleted {Count} old backup(s)", deleted);
Console.WriteLine($"Remaining: {backupsAfter.Count} backup(s)"); logger.LogInformation("Remaining: {Count} backup(s)", backupsAfter.Count);
} }
else else
{ {
Console.WriteLine("No backups to delete"); logger.LogInformation("No backups to delete");
Console.WriteLine($"Current count: {countBefore} (keeping {keep})"); logger.LogInformation("Current count: {Count} (keeping {Keep})", countBefore, keep);
} }
}
return 0; return 0;
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"Error: {ex.Message}"); logger.LogError(ex, "Failed to cleanup backups");
return 1; return 1;
}
} }
} }
@@ -3,6 +3,7 @@ using System.Text.Json;
using JdeScoping.ConfigManager.Core.Models; using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services; using JdeScoping.ConfigManager.Core.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Cli.Commands; namespace JdeScoping.ConfigManager.Cli.Commands;
@@ -78,7 +79,7 @@ public static class ConfigCommands
Option<bool> quietOption, Option<bool> quietOption,
Option<bool> jsonOption, Option<bool> jsonOption,
Func<Core.Models.ConfigModel, T> selector, Func<Core.Models.ConfigModel, T> selector,
Action<T, bool> formatter) Action<T, bool, ILogger> formatter)
{ {
var command = new Command(name, description); var command = new Command(name, description);
@@ -135,48 +136,55 @@ public static class ConfigCommands
bool json, bool json,
string sectionName, string sectionName,
Func<Core.Models.ConfigModel, T> selector, Func<Core.Models.ConfigModel, T> selector,
Action<T, bool> formatter) Action<T, bool, ILogger> formatter)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ConfigCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = $"config show {sectionName}",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var section = selector(config);
if (json)
{ {
Console.WriteLine(JsonSerializer.Serialize(section, JsonOptions)); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1;
} }
else
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
if (!quiet) logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1;
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var section = selector(config);
if (json)
{ {
Console.WriteLine($"=== {sectionName} ==="); logger.LogInformation("{Data}", JsonSerializer.Serialize(section, JsonOptions));
}
else
{
logger.LogInformation("=== {SectionName} ===", sectionName);
formatter(section, verbose, logger);
} }
formatter(section, verbose);
}
return 0; return 0;
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"Error: {ex.Message}"); logger.LogError(ex, "Failed to show section");
return 1; return 1;
}
} }
} }
@@ -187,57 +195,64 @@ public static class ConfigCommands
bool quiet, bool quiet,
bool json) bool json)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ConfigCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "config show connections",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
if (json)
{ {
// Output only names and providers, no secrets logger.LogError("Could not find configuration folder. Use --config-path to specify");
var safeConnections = config.ConnectionStrings.Entries.Select(e => new return 1;
{
e.Name,
Provider = e.Provider.ToString()
});
Console.WriteLine(JsonSerializer.Serialize(safeConnections, JsonOptions));
}
else
{
if (!quiet)
{
Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
Console.WriteLine($"{"Name",-25} {"Provider",-15}");
Console.WriteLine(new string('-', 40));
}
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
{
Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15}");
}
} }
return 0; var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
} var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
catch (Exception ex)
{ if (!File.Exists(appSettingsPath))
Console.Error.WriteLine($"Error: {ex.Message}"); {
return 1; logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1;
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
if (json)
{
// Output only names and providers, no secrets
var safeConnections = config.ConnectionStrings.Entries.Select(e => new
{
e.Name,
Provider = e.Provider.ToString()
});
logger.LogInformation("{Data}", JsonSerializer.Serialize(safeConnections, JsonOptions));
}
else
{
logger.LogInformation("=== Connections ({Count}) ===", config.ConnectionStrings.Entries.Count);
logger.LogInformation("{Name,-25} {Provider,-15}", "Name", "Provider");
logger.LogInformation("{Separator}", new string('-', 40));
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
{
logger.LogInformation("{Name,-25} {Provider,-15}", entry.Name, entry.Provider);
}
}
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to show connections");
return 1;
}
} }
} }
@@ -248,177 +263,187 @@ public static class ConfigCommands
bool quiet, bool quiet,
bool json) bool json)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ConfigCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "config show all",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
if (json)
{ {
// Sanitize passwords before outputting logger.LogError("Could not find configuration folder. Use --config-path to specify");
var sanitizedConfig = new return 1;
}
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{
logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1;
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
if (json)
{ {
config.DataSync, // Sanitize passwords before outputting
config.DataAccess, var sanitizedConfig = new
config.Auth,
config.Ldap,
config.Search,
config.ExcelExport,
config.SecureStore,
config.Pipelines,
Connections = config.ConnectionStrings.Entries.Select(e => new
{ {
e.Name, config.DataSync,
Provider = e.Provider.ToString() config.DataAccess,
}) config.Auth,
}; config.Ldap,
Console.WriteLine(JsonSerializer.Serialize(sanitizedConfig, JsonOptions)); config.Search,
} config.ExcelExport,
else config.SecureStore,
{ config.Pipelines,
Console.WriteLine("=== DataSync ==="); Connections = config.ConnectionStrings.Entries.Select(e => new
FormatDataSync(config.DataSync, verbose); {
Console.WriteLine(); e.Name,
Provider = e.Provider.ToString()
Console.WriteLine("=== DataAccess ==="); })
FormatDataAccess(config.DataAccess, verbose); };
Console.WriteLine(); logger.LogInformation("{Data}", JsonSerializer.Serialize(sanitizedConfig, JsonOptions));
Console.WriteLine("=== Auth ===");
FormatAuth(config.Auth, verbose);
Console.WriteLine();
Console.WriteLine("=== LDAP ===");
FormatLdap(config.Ldap, verbose);
Console.WriteLine();
Console.WriteLine("=== Search ===");
FormatSearch(config.Search, verbose);
Console.WriteLine();
Console.WriteLine("=== ExcelExport ===");
FormatExcelExport(config.ExcelExport, verbose);
Console.WriteLine();
Console.WriteLine("=== SecureStore ===");
FormatSecureStore(config.SecureStore, verbose);
Console.WriteLine();
Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
Console.WriteLine($"{"Name",-25} {"Provider",-15}");
Console.WriteLine(new string('-', 40));
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
{
Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15}");
} }
else
{
logger.LogInformation("=== DataSync ===");
FormatDataSync(config.DataSync, verbose, logger);
logger.LogInformation("");
logger.LogInformation("=== DataAccess ===");
FormatDataAccess(config.DataAccess, verbose, logger);
logger.LogInformation("");
logger.LogInformation("=== Auth ===");
FormatAuth(config.Auth, verbose, logger);
logger.LogInformation("");
logger.LogInformation("=== LDAP ===");
FormatLdap(config.Ldap, verbose, logger);
logger.LogInformation("");
logger.LogInformation("=== Search ===");
FormatSearch(config.Search, verbose, logger);
logger.LogInformation("");
logger.LogInformation("=== ExcelExport ===");
FormatExcelExport(config.ExcelExport, verbose, logger);
logger.LogInformation("");
logger.LogInformation("=== SecureStore ===");
FormatSecureStore(config.SecureStore, verbose, logger);
logger.LogInformation("");
logger.LogInformation("=== Connections ({Count}) ===", config.ConnectionStrings.Entries.Count);
logger.LogInformation("{Name,-25} {Provider,-15}", "Name", "Provider");
logger.LogInformation("{Separator}", new string('-', 40));
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
{
logger.LogInformation("{Name,-25} {Provider,-15}", entry.Name, entry.Provider);
}
}
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to show all sections");
return 1;
} }
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
} }
} }
private static void FormatDataSync(Core.Models.DataSyncSection section, bool verbose) private static void FormatDataSync(Core.Models.DataSyncSection section, bool verbose, ILogger logger)
{ {
Console.WriteLine($"Enabled: {section.Enabled}"); logger.LogInformation("Enabled: {Value}", section.Enabled);
Console.WriteLine($"CheckInterval: {section.CheckInterval}"); logger.LogInformation("CheckInterval: {Value}", section.CheckInterval);
Console.WriteLine($"MaxDegreeOfParallelism: {section.MaxDegreeOfParallelism}"); logger.LogInformation("MaxDegreeOfParallelism: {Value}", section.MaxDegreeOfParallelism);
Console.WriteLine($"BatchSize: {section.BatchSize}"); logger.LogInformation("BatchSize: {Value}", section.BatchSize);
Console.WriteLine($"BulkCopyBatchSize: {section.BulkCopyBatchSize}"); logger.LogInformation("BulkCopyBatchSize: {Value}", section.BulkCopyBatchSize);
if (verbose) if (verbose)
{ {
Console.WriteLine($"LookbackMultiplier: {section.LookbackMultiplier}"); logger.LogDebug("LookbackMultiplier: {Value}", section.LookbackMultiplier);
Console.WriteLine($"PurgeRetentionDays: {section.PurgeRetentionDays}"); logger.LogDebug("PurgeRetentionDays: {Value}", section.PurgeRetentionDays);
Console.WriteLine($"SyncTimeoutSeconds: {section.SyncTimeoutSeconds}"); logger.LogDebug("SyncTimeoutSeconds: {Value}", section.SyncTimeoutSeconds);
} }
} }
private static void FormatDataAccess(Core.Models.DataAccessSection section, bool verbose) private static void FormatDataAccess(Core.Models.DataAccessSection section, bool verbose, ILogger logger)
{ {
Console.WriteLine($"DefaultTimeoutSeconds: {section.DefaultTimeoutSeconds}"); logger.LogInformation("DefaultTimeoutSeconds: {Value}", section.DefaultTimeoutSeconds);
Console.WriteLine($"LotUsageTimeoutSeconds: {section.LotUsageTimeoutSeconds}"); logger.LogInformation("LotUsageTimeoutSeconds: {Value}", section.LotUsageTimeoutSeconds);
Console.WriteLine($"MisDataTimeoutSeconds: {section.MisDataTimeoutSeconds}"); logger.LogInformation("MisDataTimeoutSeconds: {Value}", section.MisDataTimeoutSeconds);
Console.WriteLine($"EnableDetailedLogging: {section.EnableDetailedLogging}"); logger.LogInformation("EnableDetailedLogging: {Value}", section.EnableDetailedLogging);
if (verbose) if (verbose)
{ {
Console.WriteLine($"ProductionSchema: {section.ProductionSchema}"); logger.LogDebug("ProductionSchema: {Value}", section.ProductionSchema);
Console.WriteLine($"ArchiveSchema: {section.ArchiveSchema}"); logger.LogDebug("ArchiveSchema: {Value}", section.ArchiveSchema);
Console.WriteLine($"StageSchema: {section.StageSchema}"); logger.LogDebug("StageSchema: {Value}", section.StageSchema);
} }
} }
private static void FormatAuth(Core.Models.AuthSection section, bool verbose) private static void FormatAuth(Core.Models.AuthSection section, bool verbose, ILogger logger)
{ {
Console.WriteLine($"CookieName: {section.CookieName}"); logger.LogInformation("CookieName: {Value}", section.CookieName);
Console.WriteLine($"CookieExpirationMinutes: {section.CookieExpirationMinutes}"); logger.LogInformation("CookieExpirationMinutes: {Value}", section.CookieExpirationMinutes);
} }
private static void FormatLdap(Core.Models.LdapSection section, bool verbose) private static void FormatLdap(Core.Models.LdapSection section, bool verbose, ILogger logger)
{ {
Console.WriteLine($"UseFakeAuth: {section.UseFakeAuth}"); logger.LogInformation("UseFakeAuth: {Value}", section.UseFakeAuth);
Console.WriteLine($"ConnectionTimeoutSeconds: {section.ConnectionTimeoutSeconds}"); logger.LogInformation("ConnectionTimeoutSeconds: {Value}", section.ConnectionTimeoutSeconds);
Console.WriteLine($"ServerUrls: {(section.ServerUrls.Length > 0 ? string.Join(", ", section.ServerUrls) : "(none)")}"); logger.LogInformation("ServerUrls: {Value}", section.ServerUrls.Length > 0 ? string.Join(", ", section.ServerUrls) : "(none)");
Console.WriteLine($"GroupDn: {(string.IsNullOrEmpty(section.GroupDn) ? "(not set)" : section.GroupDn)}"); logger.LogInformation("GroupDn: {Value}", string.IsNullOrEmpty(section.GroupDn) ? "(not set)" : section.GroupDn);
if (verbose) if (verbose)
{ {
Console.WriteLine($"SearchBase: {(string.IsNullOrEmpty(section.SearchBase) ? "(not set)" : section.SearchBase)}"); logger.LogDebug("SearchBase: {Value}", string.IsNullOrEmpty(section.SearchBase) ? "(not set)" : section.SearchBase);
Console.WriteLine($"AdminBypassUsers: {(section.AdminBypassUsers.Length > 0 ? string.Join(", ", section.AdminBypassUsers) : "(none)")}"); logger.LogDebug("AdminBypassUsers: {Value}", section.AdminBypassUsers.Length > 0 ? string.Join(", ", section.AdminBypassUsers) : "(none)");
} }
} }
private static void FormatSearch(Core.Models.SearchSection section, bool verbose) private static void FormatSearch(Core.Models.SearchSection section, bool verbose, ILogger logger)
{ {
Console.WriteLine($"MaxResultRows: {section.MaxResultRows}"); logger.LogInformation("MaxResultRows: {Value}", section.MaxResultRows);
Console.WriteLine($"TimeoutSeconds: {section.TimeoutSeconds}"); logger.LogInformation("TimeoutSeconds: {Value}", section.TimeoutSeconds);
Console.WriteLine($"MaxConcurrentSearches: {section.MaxConcurrentSearches}"); logger.LogInformation("MaxConcurrentSearches: {Value}", section.MaxConcurrentSearches);
} }
private static void FormatExcelExport(Core.Models.ExcelExportSection section, bool verbose) private static void FormatExcelExport(Core.Models.ExcelExportSection section, bool verbose, ILogger logger)
{ {
Console.WriteLine($"MaxRowsPerSheet: {section.MaxRowsPerSheet}"); logger.LogInformation("MaxRowsPerSheet: {Value}", section.MaxRowsPerSheet);
Console.WriteLine($"DefaultDateFormat: {section.DefaultDateFormat}"); logger.LogInformation("DefaultDateFormat: {Value}", section.DefaultDateFormat);
Console.WriteLine($"TimezoneId: {section.TimezoneId}"); logger.LogInformation("TimezoneId: {Value}", section.TimezoneId);
Console.WriteLine($"TimezoneAbbreviation: {section.TimezoneAbbreviation}"); logger.LogInformation("TimezoneAbbreviation: {Value}", section.TimezoneAbbreviation);
Console.WriteLine($"DebugWriteToFile: {section.DebugWriteToFile}"); logger.LogInformation("DebugWriteToFile: {Value}", section.DebugWriteToFile);
if (verbose && section.DebugWriteToFile) if (verbose && section.DebugWriteToFile)
{ {
Console.WriteLine($"DebugOutputDirectory: {section.DebugOutputDirectory}"); logger.LogDebug("DebugOutputDirectory: {Value}", section.DebugOutputDirectory);
} }
// Note: passwords are not shown // Note: passwords are not shown
Console.WriteLine("CriteriaSheetPassword: ********"); logger.LogInformation("CriteriaSheetPassword: {Value}", "********");
Console.WriteLine("DataSheetPassword: ********"); logger.LogInformation("DataSheetPassword: {Value}", "********");
} }
private static void FormatSecureStore(Core.Models.SecureStoreSection section, bool verbose) private static void FormatSecureStore(Core.Models.SecureStoreSection section, bool verbose, ILogger logger)
{ {
Console.WriteLine($"StorePath: {section.StorePath}"); logger.LogInformation("StorePath: {Value}", section.StorePath);
Console.WriteLine($"KeyFilePath: {section.KeyFilePath}"); logger.LogInformation("KeyFilePath: {Value}", section.KeyFilePath);
Console.WriteLine($"AutoCreateStore: {section.AutoCreateStore}"); logger.LogInformation("AutoCreateStore: {Value}", section.AutoCreateStore);
Console.WriteLine($"RequiredKeys: {(section.RequiredKeys.Count > 0 ? string.Join(", ", section.RequiredKeys) : "(none)")}"); logger.LogInformation("RequiredKeys: {Value}", section.RequiredKeys.Count > 0 ? string.Join(", ", section.RequiredKeys) : "(none)");
} }
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath) private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
@@ -980,54 +1005,58 @@ public static class ConfigCommands
string sectionName, string sectionName,
Func<ConfigModel, bool> modifier) Func<ConfigModel, bool> modifier)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ConfigCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = $"config set {sectionName.ToLower()}",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var backupService = serviceProvider.GetRequiredService<IBackupService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var modified = modifier(config);
if (!modified)
{ {
Console.Error.WriteLine("Error: No changes specified. Use --help to see available options."); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1; return 1;
} }
// Create backup before saving var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var backupPath = await backupService.CreateBackupAsync(appSettingsPath); var backupService = serviceProvider.GetRequiredService<IBackupService>();
if (verbose) var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.WriteLine($"Backup created: {Path.GetFileName(backupPath)}"); logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1;
} }
await configFileService.SaveAppSettingsAsync(appSettingsPath, config); try
if (!quiet)
{ {
Console.WriteLine($"{sectionName} configuration updated successfully"); var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
} var modified = modifier(config);
return 0; if (!modified)
} {
catch (Exception ex) logger.LogError("No changes specified. Use --help to see available options");
{ return 1;
Console.Error.WriteLine($"Error: {ex.Message}"); }
return 1;
// Create backup before saving
var backupPath = await backupService.CreateBackupAsync(appSettingsPath);
logger.LogDebug("Backup created: {BackupName}", Path.GetFileName(backupPath));
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
logger.LogInformation("{SectionName} configuration updated successfully", sectionName);
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to update configuration");
return 1;
}
} }
} }
} }
@@ -2,6 +2,7 @@ using System.CommandLine;
using JdeScoping.ConfigManager.Core.Models; using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services; using JdeScoping.ConfigManager.Core.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Cli.Commands; namespace JdeScoping.ConfigManager.Cli.Commands;
@@ -239,52 +240,59 @@ public static class ConnectionCommands
bool verbose, bool verbose,
bool quiet) bool quiet)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ConnectionCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "connection list",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
if (!quiet)
{ {
Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ==="); logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.WriteLine($"{"Name",-25} {"Provider",-15} {"Server/Host",-30}"); return 1;
Console.WriteLine(new string('-', 70));
} }
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name)) var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
var serverOrHost = entry.Provider switch logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1;
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
logger.LogInformation("=== Connections ({Count}) ===", config.ConnectionStrings.Entries.Count);
logger.LogInformation("{Name,-25} {Provider,-15} {ServerHost,-30}", "Name", "Provider", "Server/Host");
logger.LogInformation("{Separator}", new string('-', 70));
foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
{ {
ConnectionProvider.SqlServer => entry.Server ?? "-", var serverOrHost = entry.Provider switch
ConnectionProvider.Oracle => entry.Host ?? "-", {
ConnectionProvider.Generic => "(raw)", ConnectionProvider.SqlServer => entry.Server ?? "-",
_ => "-" ConnectionProvider.Oracle => entry.Host ?? "-",
}; ConnectionProvider.Generic => "(raw)",
_ => "-"
};
Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15} {serverOrHost,-30}"); logger.LogInformation("{Name,-25} {Provider,-15} {ServerHost,-30}", entry.Name, entry.Provider, serverOrHost);
}
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to list connections");
return 1;
} }
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
} }
} }
@@ -295,84 +303,92 @@ public static class ConnectionCommands
bool quiet, bool quiet,
string name) string name)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ConnectionCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "connection show",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["ConnectionName"] = name
}))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (entry == null)
{ {
Console.Error.WriteLine($"Error: Connection '{name}' not found"); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1; return 1;
} }
if (!quiet) var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.WriteLine($"=== Connection: {entry.Name} ==="); logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1;
} }
Console.WriteLine($"Name: {entry.Name}"); try
Console.WriteLine($"Provider: {entry.Provider}");
switch (entry.Provider)
{ {
case ConnectionProvider.SqlServer: var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
Console.WriteLine($"Server: {entry.Server ?? "(not set)"}"); var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
if (entry.SqlServerPort.HasValue) e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
Console.WriteLine($"Port: {entry.SqlServerPort.Value}");
Console.WriteLine($"Database: {entry.Database ?? "(not set)"}");
Console.WriteLine($"User ID: {entry.UserId ?? "(not set)"}");
Console.WriteLine($"Password: {MaskPassword(entry.Password)}");
Console.WriteLine($"Encrypt: {entry.Encrypt}");
Console.WriteLine($"TrustServerCertificate: {entry.TrustServerCertificate}");
Console.WriteLine($"ConnectionTimeout: {entry.ConnectionTimeout}");
if (!string.IsNullOrEmpty(entry.ApplicationName))
Console.WriteLine($"ApplicationName: {entry.ApplicationName}");
break;
case ConnectionProvider.Oracle: if (entry == null)
Console.WriteLine($"Host: {entry.Host ?? "(not set)"}"); {
Console.WriteLine($"Port: {entry.Port}"); logger.LogError("Connection '{Name}' not found", name);
Console.WriteLine($"ServiceName: {entry.ServiceName ?? "(not set)"}"); return 1;
Console.WriteLine($"User ID: {entry.UserId ?? "(not set)"}"); }
Console.WriteLine($"Password: {MaskPassword(entry.Password)}");
break;
case ConnectionProvider.Generic: logger.LogInformation("=== Connection: {Name} ===", entry.Name);
Console.WriteLine($"RawConnectionString: {MaskConnectionString(entry.RawConnectionString)}");
break; logger.LogInformation("Name: {Value}", entry.Name);
logger.LogInformation("Provider: {Value}", entry.Provider);
switch (entry.Provider)
{
case ConnectionProvider.SqlServer:
logger.LogInformation("Server: {Value}", entry.Server ?? "(not set)");
if (entry.SqlServerPort.HasValue)
logger.LogInformation("Port: {Value}", entry.SqlServerPort.Value);
logger.LogInformation("Database: {Value}", entry.Database ?? "(not set)");
logger.LogInformation("User ID: {Value}", entry.UserId ?? "(not set)");
logger.LogInformation("Password: {Value}", MaskPassword(entry.Password));
logger.LogInformation("Encrypt: {Value}", entry.Encrypt);
logger.LogInformation("TrustServerCertificate: {Value}", entry.TrustServerCertificate);
logger.LogInformation("ConnectionTimeout: {Value}", entry.ConnectionTimeout);
if (!string.IsNullOrEmpty(entry.ApplicationName))
logger.LogInformation("ApplicationName: {Value}", entry.ApplicationName);
break;
case ConnectionProvider.Oracle:
logger.LogInformation("Host: {Value}", entry.Host ?? "(not set)");
logger.LogInformation("Port: {Value}", entry.Port);
logger.LogInformation("ServiceName: {Value}", entry.ServiceName ?? "(not set)");
logger.LogInformation("User ID: {Value}", entry.UserId ?? "(not set)");
logger.LogInformation("Password: {Value}", MaskPassword(entry.Password));
break;
case ConnectionProvider.Generic:
logger.LogInformation("RawConnectionString: {Value}", MaskConnectionString(entry.RawConnectionString));
break;
}
if (verbose)
{
logger.LogDebug("");
logger.LogDebug("Generated connection string (masked):");
logger.LogDebug(" {ConnectionString}", MaskConnectionString(entry.GenerateConnectionString()));
}
return 0;
} }
catch (Exception ex)
if (verbose)
{ {
Console.WriteLine(); logger.LogError(ex, "Failed to show connection");
Console.WriteLine("Generated connection string (masked):"); return 1;
Console.WriteLine($" {MaskConnectionString(entry.GenerateConnectionString())}");
} }
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
} }
} }
@@ -391,76 +407,84 @@ public static class ConnectionCommands
string? serviceName, string? serviceName,
string? raw) string? raw)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ConnectionCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "connection add",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["ConnectionName"] = name
}))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
// Check if connection already exists
if (config.ConnectionStrings.Entries.Any(e =>
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
{ {
Console.Error.WriteLine($"Error: Connection '{name}' already exists"); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1; return 1;
} }
// Validate required fields based on provider var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
if (provider == ConnectionProvider.Generic && string.IsNullOrEmpty(raw)) var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine("Error: --raw is required for Generic provider"); logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1; return 1;
} }
var entry = new ConnectionStringEntry try
{ {
Name = name, var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
Provider = provider,
Server = server,
Database = database,
UserId = user,
Password = password,
Host = server, // For Oracle, use server as host
ServiceName = serviceName,
RawConnectionString = raw
};
if (port.HasValue) // Check if connection already exists
{ if (config.ConnectionStrings.Entries.Any(e =>
if (provider == ConnectionProvider.SqlServer) e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
entry.SqlServerPort = port; {
else if (provider == ConnectionProvider.Oracle) logger.LogError("Connection '{Name}' already exists", name);
entry.Port = port.Value; return 1;
}
// Validate required fields based on provider
if (provider == ConnectionProvider.Generic && string.IsNullOrEmpty(raw))
{
logger.LogError("--raw is required for Generic provider");
return 1;
}
var entry = new ConnectionStringEntry
{
Name = name,
Provider = provider,
Server = server,
Database = database,
UserId = user,
Password = password,
Host = server, // For Oracle, use server as host
ServiceName = serviceName,
RawConnectionString = raw
};
if (port.HasValue)
{
if (provider == ConnectionProvider.SqlServer)
entry.SqlServerPort = port;
else if (provider == ConnectionProvider.Oracle)
entry.Port = port.Value;
}
config.ConnectionStrings.Entries.Add(entry);
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
logger.LogInformation("Connection '{Name}' added successfully", name);
return 0;
} }
catch (Exception ex)
config.ConnectionStrings.Entries.Add(entry);
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
if (!quiet)
{ {
Console.WriteLine($"Connection '{name}' added successfully"); logger.LogError(ex, "Failed to add connection");
return 1;
} }
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
} }
} }
@@ -471,48 +495,56 @@ public static class ConnectionCommands
bool quiet, bool quiet,
string name) string name)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ConnectionCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "connection remove",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["ConnectionName"] = name
}))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (entry == null)
{ {
Console.Error.WriteLine($"Error: Connection '{name}' not found"); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1; return 1;
} }
config.ConnectionStrings.Entries.Remove(entry); var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
await configFileService.SaveAppSettingsAsync(appSettingsPath, config); var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!quiet) if (!File.Exists(appSettingsPath))
{ {
Console.WriteLine($"Connection '{name}' removed successfully"); logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1;
} }
return 0; try
} {
catch (Exception ex) var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
{ var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
Console.Error.WriteLine($"Error: {ex.Message}"); e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
return 1;
if (entry == null)
{
logger.LogError("Connection '{Name}' not found", name);
return 1;
}
config.ConnectionStrings.Entries.Remove(entry);
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
logger.LogInformation("Connection '{Name}' removed successfully", name);
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to remove connection");
return 1;
}
} }
} }
@@ -531,100 +563,108 @@ public static class ConnectionCommands
string? serviceName, string? serviceName,
string? raw) string? raw)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ConnectionCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "connection update",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["ConnectionName"] = name
}))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (entry == null)
{ {
Console.Error.WriteLine($"Error: Connection '{name}' not found"); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1; return 1;
} }
var modified = false; var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (provider.HasValue) if (!File.Exists(appSettingsPath))
{ {
entry.Provider = provider.Value; logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
modified = true;
}
if (server != null)
{
entry.Server = server;
entry.Host = server; // For Oracle, use server as host
modified = true;
}
if (database != null)
{
entry.Database = database;
modified = true;
}
if (user != null)
{
entry.UserId = user;
modified = true;
}
if (password != null)
{
entry.Password = password;
modified = true;
}
if (port.HasValue)
{
if (entry.Provider == ConnectionProvider.SqlServer)
entry.SqlServerPort = port;
else if (entry.Provider == ConnectionProvider.Oracle)
entry.Port = port.Value;
modified = true;
}
if (serviceName != null)
{
entry.ServiceName = serviceName;
modified = true;
}
if (raw != null)
{
entry.RawConnectionString = raw;
modified = true;
}
if (!modified)
{
Console.Error.WriteLine("Error: No changes specified. Use --help to see available options.");
return 1; return 1;
} }
await configFileService.SaveAppSettingsAsync(appSettingsPath, config); try
if (!quiet)
{ {
Console.WriteLine($"Connection '{name}' updated successfully"); var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
} var entry = config.ConnectionStrings.Entries.FirstOrDefault(e =>
e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
return 0; if (entry == null)
} {
catch (Exception ex) logger.LogError("Connection '{Name}' not found", name);
{ return 1;
Console.Error.WriteLine($"Error: {ex.Message}"); }
return 1;
var modified = false;
if (provider.HasValue)
{
entry.Provider = provider.Value;
modified = true;
}
if (server != null)
{
entry.Server = server;
entry.Host = server; // For Oracle, use server as host
modified = true;
}
if (database != null)
{
entry.Database = database;
modified = true;
}
if (user != null)
{
entry.UserId = user;
modified = true;
}
if (password != null)
{
entry.Password = password;
modified = true;
}
if (port.HasValue)
{
if (entry.Provider == ConnectionProvider.SqlServer)
entry.SqlServerPort = port;
else if (entry.Provider == ConnectionProvider.Oracle)
entry.Port = port.Value;
modified = true;
}
if (serviceName != null)
{
entry.ServiceName = serviceName;
modified = true;
}
if (raw != null)
{
entry.RawConnectionString = raw;
modified = true;
}
if (!modified)
{
logger.LogError("No changes specified. Use --help to see available options");
return 1;
}
await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
logger.LogInformation("Connection '{Name}' updated successfully", name);
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to update connection");
return 1;
}
} }
} }
@@ -2,6 +2,7 @@ using System.CommandLine;
using JdeScoping.ConfigManager.Core.Services; using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.DataSync.Configuration; using JdeScoping.DataSync.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Cli.Commands; namespace JdeScoping.ConfigManager.Cli.Commands;
@@ -167,44 +168,51 @@ public static class PipelineCommands
bool verbose, bool verbose,
bool quiet) bool quiet)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("PipelineCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "pipeline list",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
try
{ {
var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null)
if (!quiet)
{ {
Console.WriteLine($"=== Pipelines ({pipelines.Count}) ==="); logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.WriteLine($"{"Name",-20} {"Enabled",-8} {"Mass",-8} {"Daily",-8} {"Hourly",-8}"); return 1;
Console.WriteLine(new string('-', 52));
} }
foreach (var kvp in pipelines.OrderBy(p => p.Key)) var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
try
{ {
var p = kvp.Value; var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir);
var mass = FormatInterval(p.MassSyncIntervalMinutes);
var daily = FormatInterval(p.DailySyncIntervalMinutes);
var hourly = FormatInterval(p.HourlySyncIntervalMinutes);
var enabled = p.IsEnabled ? "Yes" : "No";
Console.WriteLine($"{p.Name,-20} {enabled,-8} {mass,-8} {daily,-8} {hourly,-8}"); logger.LogInformation("=== Pipelines ({Count}) ===", pipelines.Count);
logger.LogInformation("{Name,-20} {Enabled,-8} {Mass,-8} {Daily,-8} {Hourly,-8}", "Name", "Enabled", "Mass", "Daily", "Hourly");
logger.LogInformation("{Separator}", new string('-', 52));
foreach (var kvp in pipelines.OrderBy(p => p.Key))
{
var p = kvp.Value;
var mass = FormatInterval(p.MassSyncIntervalMinutes);
var daily = FormatInterval(p.DailySyncIntervalMinutes);
var hourly = FormatInterval(p.HourlySyncIntervalMinutes);
var enabled = p.IsEnabled ? "Yes" : "No";
logger.LogInformation("{Name,-20} {Enabled,-8} {Mass,-8} {Daily,-8} {Hourly,-8}", p.Name, enabled, mass, daily, hourly);
}
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to list pipelines");
return 1;
} }
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
} }
} }
@@ -215,86 +223,94 @@ public static class PipelineCommands
bool quiet, bool quiet,
string name) string name)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("PipelineCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "pipeline show",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["PipelineName"] = name
}))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
if (!File.Exists(pipelinePath))
{ {
Console.Error.WriteLine($"Error: Pipeline '{name}' not found"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
if (!quiet)
{ {
Console.WriteLine($"=== Pipeline: {pipeline.Name} ==="); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1;
} }
Console.WriteLine($"Name: {pipeline.Name}"); var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
Console.WriteLine($"Enabled: {pipeline.IsEnabled}"); var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
Console.WriteLine($"Manual Only: {pipeline.IsManualOnly}");
Console.WriteLine();
Console.WriteLine("Sync Intervals:"); if (!File.Exists(pipelinePath))
Console.WriteLine($" Mass: {FormatInterval(pipeline.MassSyncIntervalMinutes)}");
Console.WriteLine($" Daily: {FormatInterval(pipeline.DailySyncIntervalMinutes)}");
Console.WriteLine($" Hourly: {FormatInterval(pipeline.HourlySyncIntervalMinutes)}");
if (pipeline.Source != null)
{ {
Console.WriteLine(); logger.LogError("Pipeline '{Name}' not found", name);
Console.WriteLine("Source:"); return 1;
Console.WriteLine($" Connection: {pipeline.Source.Connection}");
if (!string.IsNullOrEmpty(pipeline.Source.Query))
Console.WriteLine($" Query: {(pipeline.Source.Query.Length > 50 ? pipeline.Source.Query[..50] + "..." : pipeline.Source.Query)}");
} }
if (pipeline.Destination != null) try
{ {
Console.WriteLine(); var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
Console.WriteLine("Destination:");
Console.WriteLine($" Table: {pipeline.Destination.Table}");
if (pipeline.Destination.MatchColumns.Count > 0)
Console.WriteLine($" Match Columns: {string.Join(", ", pipeline.Destination.MatchColumns)}");
}
if (verbose) logger.LogInformation("=== Pipeline: {Name} ===", pipeline.Name);
{
if (pipeline.PreScripts.Count > 0) logger.LogInformation("Name: {Value}", pipeline.Name);
logger.LogInformation("Enabled: {Value}", pipeline.IsEnabled);
logger.LogInformation("Manual Only: {Value}", pipeline.IsManualOnly);
logger.LogInformation("");
logger.LogInformation("Sync Intervals:");
logger.LogInformation(" Mass: {Value}", FormatInterval(pipeline.MassSyncIntervalMinutes));
logger.LogInformation(" Daily: {Value}", FormatInterval(pipeline.DailySyncIntervalMinutes));
logger.LogInformation(" Hourly: {Value}", FormatInterval(pipeline.HourlySyncIntervalMinutes));
if (pipeline.Source != null)
{ {
Console.WriteLine(); logger.LogInformation("");
Console.WriteLine($"Pre-Scripts: {pipeline.PreScripts.Count}"); logger.LogInformation("Source:");
logger.LogInformation(" Connection: {Value}", pipeline.Source.Connection);
if (!string.IsNullOrEmpty(pipeline.Source.Query))
logger.LogInformation(" Query: {Value}", pipeline.Source.Query.Length > 50 ? pipeline.Source.Query[..50] + "..." : pipeline.Source.Query);
} }
if (pipeline.Transforms.Count > 0) if (pipeline.Destination != null)
{ {
Console.WriteLine(); logger.LogInformation("");
Console.WriteLine($"Transforms: {pipeline.Transforms.Count}"); logger.LogInformation("Destination:");
logger.LogInformation(" Table: {Value}", pipeline.Destination.Table);
if (pipeline.Destination.MatchColumns.Count > 0)
logger.LogInformation(" Match Columns: {Value}", string.Join(", ", pipeline.Destination.MatchColumns));
} }
if (pipeline.PostScripts.Count > 0) if (verbose)
{ {
Console.WriteLine(); if (pipeline.PreScripts.Count > 0)
Console.WriteLine($"Post-Scripts: {pipeline.PostScripts.Count}"); {
logger.LogDebug("");
logger.LogDebug("Pre-Scripts: {Count}", pipeline.PreScripts.Count);
}
if (pipeline.Transforms.Count > 0)
{
logger.LogDebug("");
logger.LogDebug("Transforms: {Count}", pipeline.Transforms.Count);
}
if (pipeline.PostScripts.Count > 0)
{
logger.LogDebug("");
logger.LogDebug("Post-Scripts: {Count}", pipeline.PostScripts.Count);
}
} }
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to show pipeline");
return 1;
} }
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
} }
} }
@@ -307,54 +323,62 @@ public static class PipelineCommands
bool enabled, bool enabled,
bool manualOnly) bool manualOnly)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("PipelineCommands");
{
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
return 1;
}
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>(); using (logger.BeginScope(new Dictionary<string, object>
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
var pipelinePath = Path.Combine(pipelinesDir, $"pipeline.{name}.json");
if (File.Exists(pipelinePath))
{ {
Console.Error.WriteLine($"Error: Pipeline '{name}' already exists"); ["Command"] = "pipeline create",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["PipelineName"] = name
}))
try
{ {
// Create Pipelines directory if it doesn't exist var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (!Directory.Exists(pipelinesDir)) if (folderPath == null)
{ {
Directory.CreateDirectory(pipelinesDir); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return 1;
} }
var pipeline = new EtlPipelineConfig var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
{ var pipelinesDir = Path.Combine(folderPath, "Pipelines");
Name = name, var pipelinePath = Path.Combine(pipelinesDir, $"pipeline.{name}.json");
IsEnabled = enabled,
IsManualOnly = manualOnly,
Source = new SourceElement { Connection = "jde" },
Destination = new DestinationElement { Table = name }
};
await configFileService.SavePipelineAsync(pipelinePath, pipeline); if (File.Exists(pipelinePath))
if (!quiet)
{ {
Console.WriteLine($"Pipeline '{name}' created successfully"); logger.LogError("Pipeline '{Name}' already exists", name);
Console.WriteLine($"File: {pipelinePath}"); return 1;
} }
return 0; try
} {
catch (Exception ex) // Create Pipelines directory if it doesn't exist
{ if (!Directory.Exists(pipelinesDir))
Console.Error.WriteLine($"Error: {ex.Message}"); {
return 1; Directory.CreateDirectory(pipelinesDir);
}
var pipeline = new EtlPipelineConfig
{
Name = name,
IsEnabled = enabled,
IsManualOnly = manualOnly,
Source = new SourceElement { Connection = "jde" },
Destination = new DestinationElement { Table = name }
};
await configFileService.SavePipelineAsync(pipelinePath, pipeline);
logger.LogInformation("Pipeline '{Name}' created successfully", name);
logger.LogInformation("File: {Path}", pipelinePath);
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create pipeline");
return 1;
}
} }
} }
@@ -366,50 +390,57 @@ public static class PipelineCommands
string name, string name,
bool force) bool force)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("PipelineCommands");
{
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
return 1;
}
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>(); using (logger.BeginScope(new Dictionary<string, object>
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
if (!File.Exists(pipelinePath))
{ {
Console.Error.WriteLine($"Error: Pipeline '{name}' not found"); ["Command"] = "pipeline delete",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["PipelineName"] = name
}))
if (!force)
{ {
Console.Write($"Delete pipeline '{name}'? [y/N] "); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
var response = Console.ReadLine(); if (folderPath == null)
if (!string.Equals(response, "y", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(response, "yes", StringComparison.OrdinalIgnoreCase))
{ {
if (!quiet) logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.WriteLine("Cancelled"); return 1;
}
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
if (!File.Exists(pipelinePath))
{
logger.LogError("Pipeline '{Name}' not found", name);
return 1;
}
if (!force)
{
Console.Write($"Delete pipeline '{name}'? [y/N] ");
var response = Console.ReadLine();
if (!string.Equals(response, "y", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(response, "yes", StringComparison.OrdinalIgnoreCase))
{
logger.LogInformation("Cancelled");
return 0;
}
}
try
{
await configFileService.DeletePipelineFileAsync(pipelinePath);
logger.LogInformation("Pipeline '{Name}' deleted successfully", name);
return 0; return 0;
} }
} catch (Exception ex)
try
{
await configFileService.DeletePipelineFileAsync(pipelinePath);
if (!quiet)
{ {
Console.WriteLine($"Pipeline '{name}' deleted successfully"); logger.LogError(ex, "Failed to delete pipeline");
return 1;
} }
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
} }
} }
@@ -421,40 +452,49 @@ public static class PipelineCommands
string name, string name,
bool enabled) bool enabled)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("PipelineCommands");
var action = enabled ? "enable" : "disable";
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = $"pipeline {action}",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["PipelineName"] = name
}))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
if (!File.Exists(pipelinePath))
{ {
Console.Error.WriteLine($"Error: Pipeline '{name}' not found"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
pipeline.IsEnabled = enabled;
await configFileService.SavePipelineAsync(pipelinePath, pipeline);
if (!quiet)
{ {
var action = enabled ? "enabled" : "disabled"; logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.WriteLine($"Pipeline '{name}' {action} successfully"); return 1;
} }
return 0; var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
} var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
catch (Exception ex)
{ if (!File.Exists(pipelinePath))
Console.Error.WriteLine($"Error: {ex.Message}"); {
return 1; logger.LogError("Pipeline '{Name}' not found", name);
return 1;
}
try
{
var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
pipeline.IsEnabled = enabled;
await configFileService.SavePipelineAsync(pipelinePath, pipeline);
var actionPast = enabled ? "enabled" : "disabled";
logger.LogInformation("Pipeline '{Name}' {Action} successfully", name, actionPast);
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to {Action} pipeline", action);
return 1;
}
} }
} }
@@ -2,6 +2,7 @@ using System.CommandLine;
using JdeScoping.ConfigManager.Core.Services; using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore; using JdeScoping.ConfigManager.Core.Services.SecureStore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Cli.Commands; namespace JdeScoping.ConfigManager.Cli.Commands;
@@ -138,29 +139,36 @@ public static class SecretCommands
bool verbose, bool verbose,
bool quiet) bool quiet)
{ {
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (manager == null) var logger = loggerFactory.CreateLogger("SecretCommands");
return 1;
try using (logger.BeginScope(new Dictionary<string, object>
{ {
var keys = manager.GetKeys(); ["Command"] = "secret list",
["ConfigPath"] = configPath ?? "(default)"
if (!quiet) }))
{
Console.WriteLine($"=== SecureStore Keys ({keys.Count}) ===");
}
foreach (var key in keys.OrderBy(k => k))
{
Console.WriteLine(key);
}
return 0;
}
finally
{ {
manager.CloseStore(); var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath, logger);
if (manager == null)
return 1;
try
{
var keys = manager.GetKeys();
logger.LogInformation("=== SecureStore Keys ({Count}) ===", keys.Count);
foreach (var key in keys.OrderBy(k => k))
{
logger.LogInformation("{Key}", key);
}
return 0;
}
finally
{
manager.CloseStore();
}
} }
} }
@@ -171,24 +179,35 @@ public static class SecretCommands
bool quiet, bool quiet,
string key) string key)
{ {
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (manager == null) var logger = loggerFactory.CreateLogger("SecretCommands");
return 1;
try using (logger.BeginScope(new Dictionary<string, object>
{ {
var value = manager.GetSecret(key); ["Command"] = "secret get",
Console.WriteLine(value); ["ConfigPath"] = configPath ?? "(default)",
return 0; ["SecretKey"] = key
} }))
catch (KeyNotFoundException)
{ {
Console.Error.WriteLine($"Error: Secret '{key}' not found"); var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath, logger);
return 1; if (manager == null)
} return 1;
finally
{ try
manager.CloseStore(); {
var value = manager.GetSecret(key);
logger.LogInformation("{Value}", value);
return 0;
}
catch (KeyNotFoundException)
{
logger.LogError("Secret '{Key}' not found", key);
return 1;
}
finally
{
manager.CloseStore();
}
} }
} }
@@ -200,30 +219,38 @@ public static class SecretCommands
string key, string key,
string value) string value)
{ {
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (manager == null) var logger = loggerFactory.CreateLogger("SecretCommands");
return 1;
try using (logger.BeginScope(new Dictionary<string, object>
{ {
manager.SetSecret(key, value); ["Command"] = "secret set",
manager.Save(); ["ConfigPath"] = configPath ?? "(default)",
["SecretKey"] = key
}))
{
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath, logger);
if (manager == null)
return 1;
if (!quiet) try
{ {
Console.WriteLine($"Secret '{key}' set successfully"); manager.SetSecret(key, value);
} manager.Save();
return 0; logger.LogInformation("Secret '{Key}' set successfully", key);
}
catch (Exception ex) return 0;
{ }
Console.Error.WriteLine($"Error: {ex.Message}"); catch (Exception ex)
return 1; {
} logger.LogError(ex, "Failed to set secret");
finally return 1;
{ }
manager.CloseStore(); finally
{
manager.CloseStore();
}
} }
} }
@@ -234,35 +261,43 @@ public static class SecretCommands
bool quiet, bool quiet,
string key) string key)
{ {
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (manager == null) var logger = loggerFactory.CreateLogger("SecretCommands");
return 1;
try using (logger.BeginScope(new Dictionary<string, object>
{ {
manager.RemoveSecret(key); ["Command"] = "secret remove",
manager.Save(); ["ConfigPath"] = configPath ?? "(default)",
["SecretKey"] = key
}))
{
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath, logger);
if (manager == null)
return 1;
if (!quiet) try
{ {
Console.WriteLine($"Secret '{key}' removed successfully"); manager.RemoveSecret(key);
} manager.Save();
return 0; logger.LogInformation("Secret '{Key}' removed successfully", key);
}
catch (KeyNotFoundException) return 0;
{ }
Console.Error.WriteLine($"Error: Secret '{key}' not found"); catch (KeyNotFoundException)
return 1; {
} logger.LogError("Secret '{Key}' not found", key);
catch (Exception ex) return 1;
{ }
Console.Error.WriteLine($"Error: {ex.Message}"); catch (Exception ex)
return 1; {
} logger.LogError(ex, "Failed to remove secret");
finally return 1;
{ }
manager.CloseStore(); finally
{
manager.CloseStore();
}
} }
} }
@@ -274,56 +309,64 @@ public static class SecretCommands
string? storePath, string? storePath,
string? keyPath) string? keyPath)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("SecretCommands");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "secret init",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var effectiveStorePath = storePath ?? Path.Combine(folderPath, "data", "secrets.json");
var effectiveKeyPath = keyPath ?? Path.Combine(folderPath, "data", "secrets.key");
if (File.Exists(effectiveStorePath))
{ {
Console.Error.WriteLine($"Error: Store already exists at {effectiveStorePath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
var manager = serviceProvider.GetRequiredService<ISecureStoreManager>();
try
{
manager.CreateStore(effectiveStorePath, effectiveKeyPath);
if (!quiet)
{ {
Console.WriteLine("SecureStore initialized successfully"); logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.WriteLine($"Store: {effectiveStorePath}"); return 1;
Console.WriteLine($"Key: {effectiveKeyPath}");
} }
return 0; var effectiveStorePath = storePath ?? Path.Combine(folderPath, "data", "secrets.json");
} var effectiveKeyPath = keyPath ?? Path.Combine(folderPath, "data", "secrets.key");
catch (Exception ex)
{ if (File.Exists(effectiveStorePath))
Console.Error.WriteLine($"Error creating store: {ex.Message}"); {
return 1; logger.LogError("Store already exists at {StorePath}", effectiveStorePath);
} return 1;
finally }
{
manager.CloseStore(); var manager = serviceProvider.GetRequiredService<ISecureStoreManager>();
try
{
manager.CreateStore(effectiveStorePath, effectiveKeyPath);
logger.LogInformation("SecureStore initialized successfully");
logger.LogInformation("Store: {StorePath}", effectiveStorePath);
logger.LogInformation("Key: {KeyPath}", effectiveKeyPath);
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create store");
return 1;
}
finally
{
manager.CloseStore();
}
} }
} }
private static async Task<(ISecureStoreManager?, string?)> OpenStoreAsync( private static async Task<(ISecureStoreManager?, string?)> OpenStoreAsync(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
string? configPath) string? configPath,
ILogger logger)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null) if (folderPath == null)
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); logger.LogError("Could not find configuration folder. Use --config-path to specify");
return (null, null); return (null, null);
} }
@@ -333,7 +376,7 @@ public static class SecretCommands
if (!File.Exists(appSettingsPath)) if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return (null, null); return (null, null);
} }
@@ -351,14 +394,14 @@ public static class SecretCommands
if (!File.Exists(storePath)) if (!File.Exists(storePath))
{ {
Console.Error.WriteLine($"Error: SecureStore not found at {storePath}"); logger.LogError("SecureStore not found at {Path}", storePath);
Console.Error.WriteLine("Use 'secret init' to create a new store."); logger.LogError("Use 'secret init' to create a new store");
return (null, null); return (null, null);
} }
if (!File.Exists(keyPath)) if (!File.Exists(keyPath))
{ {
Console.Error.WriteLine($"Error: Key file not found at {keyPath}"); logger.LogError("Key file not found at {Path}", keyPath);
return (null, null); return (null, null);
} }
@@ -369,7 +412,7 @@ public static class SecretCommands
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"Error opening store: {ex.Message}"); logger.LogError(ex, "Failed to open store");
return (null, null); return (null, null);
} }
} }
@@ -2,6 +2,7 @@ using System.CommandLine;
using JdeScoping.ConfigManager.Core.Models; using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services; using JdeScoping.ConfigManager.Core.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Cli.Commands; namespace JdeScoping.ConfigManager.Cli.Commands;
@@ -90,68 +91,73 @@ public static class TestConnectionCommand
string? connectionName, string? connectionName,
ConnectionProvider provider) ConnectionProvider provider)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("TestConnectionCommand");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = $"test-connection {provider.ToString().ToLower()}",
return 1; ["ConfigPath"] = configPath ?? "(default)",
} ["ConnectionName"] = connectionName ?? "(auto)"
}))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var entry = connectionName != null
? config.ConnectionStrings.Entries.FirstOrDefault(e =>
e.Name.Equals(connectionName, StringComparison.OrdinalIgnoreCase))
: config.ConnectionStrings.Entries.FirstOrDefault(e => e.Provider == provider);
if (entry == null)
{ {
var message = connectionName != null logger.LogError("Could not find configuration folder. Use --config-path to specify");
? $"Connection '{connectionName}' not found"
: $"No {provider} connection found";
Console.Error.WriteLine($"Error: {message}");
return 1; return 1;
} }
if (!quiet) var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.WriteLine($"Testing connection: {entry.Name}"); logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
return 1;
} }
var connectionString = entry.GenerateConnectionString(); try
var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider);
if (result.Success)
{ {
if (!quiet) var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var entry = connectionName != null
? config.ConnectionStrings.Entries.FirstOrDefault(e =>
e.Name.Equals(connectionName, StringComparison.OrdinalIgnoreCase))
: config.ConnectionStrings.Entries.FirstOrDefault(e => e.Provider == provider);
if (entry == null)
{ {
Console.WriteLine($"Status: Success"); var message = connectionName != null
if (result.Duration.HasValue) ? $"Connection '{connectionName}' not found"
Console.WriteLine($"Duration: {result.Duration.Value.TotalMilliseconds:F0}ms"); : $"No {provider} connection found";
logger.LogError("{Message}", message);
return 1;
} }
return 0;
}
Console.Error.WriteLine($"Status: Failed"); logger.LogInformation("Testing connection: {Name}", entry.Name);
Console.Error.WriteLine($"Message: {result.Message}");
return 1; var connectionString = entry.GenerateConnectionString();
} var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider);
catch (Exception ex)
{ if (result.Success)
Console.Error.WriteLine($"Error: {ex.Message}"); {
return 1; logger.LogInformation("Status: Success");
if (result.Duration.HasValue)
logger.LogInformation("Duration: {Duration}ms", result.Duration.Value.TotalMilliseconds.ToString("F0"));
return 0;
}
logger.LogError("Status: Failed");
logger.LogError("Message: {Message}", result.Message);
return 1;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to test connection");
return 1;
}
} }
} }
@@ -161,70 +167,73 @@ public static class TestConnectionCommand
bool verbose, bool verbose,
bool quiet) bool quiet)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("TestConnectionCommand");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "test-connection all",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var hasFailures = false;
if (!quiet)
{ {
Console.WriteLine("=== Testing All Connections ==="); logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.WriteLine($"Found {config.ConnectionStrings.Entries.Count} connection(s)"); return 1;
Console.WriteLine();
} }
foreach (var entry in config.ConnectionStrings.Entries) var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
if (entry.Provider == ConnectionProvider.Generic) logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
{ return 1;
if (!quiet) }
Console.WriteLine($"{entry.Name}: Skipped (generic provider)");
continue;
}
var connectionString = entry.GenerateConnectionString(); try
var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider); {
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var hasFailures = false;
if (result.Success) logger.LogInformation("=== Testing All Connections ===");
logger.LogInformation("Found {Count} connection(s)", config.ConnectionStrings.Entries.Count);
logger.LogInformation("");
foreach (var entry in config.ConnectionStrings.Entries)
{ {
if (!quiet) if (entry.Provider == ConnectionProvider.Generic)
{
logger.LogInformation("{Name}: Skipped (generic provider)", entry.Name);
continue;
}
var connectionString = entry.GenerateConnectionString();
var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider);
if (result.Success)
{ {
var duration = result.Duration.HasValue var duration = result.Duration.HasValue
? $" ({result.Duration.Value.TotalMilliseconds:F0}ms)" ? $" ({result.Duration.Value.TotalMilliseconds:F0}ms)"
: ""; : "";
Console.WriteLine($"{entry.Name}: OK{duration}"); logger.LogInformation("{Name}: OK{Duration}", entry.Name, duration);
}
else
{
hasFailures = true;
logger.LogError("{Name}: FAILED - {Message}", entry.Name, result.Message);
} }
} }
else
{
hasFailures = true;
Console.Error.WriteLine($"{entry.Name}: FAILED - {result.Message}");
}
}
return hasFailures ? 1 : 0; return hasFailures ? 1 : 0;
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine($"Error: {ex.Message}"); logger.LogError(ex, "Failed to test connections");
return 1; return 1;
}
} }
} }
@@ -1,6 +1,7 @@
using System.CommandLine; using System.CommandLine;
using JdeScoping.ConfigManager.Core.Services; using JdeScoping.ConfigManager.Core.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Cli.Commands; namespace JdeScoping.ConfigManager.Cli.Commands;
@@ -96,58 +97,63 @@ public static class ValidateCommand
bool verbose, bool verbose,
bool quiet) bool quiet)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ValidateCommand");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "validate appsettings",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var validationService = serviceProvider.GetRequiredService<IValidationService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
return 1; if (folderPath == null)
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var result = validationService.ValidateAppSettings(config);
if (!quiet)
{ {
Console.WriteLine("=== AppSettings Validation ==="); logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.WriteLine($"File: {appSettingsPath}"); return 1;
} }
if (result.IsValid) var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var validationService = serviceProvider.GetRequiredService<IValidationService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{ {
if (!quiet) logger.LogError("appsettings.json not found at {Path}", appSettingsPath);
Console.WriteLine("Status: Valid"); return 1;
return 0;
} }
foreach (var error in result.Errors) try
{ {
Console.Error.WriteLine($"Error: {error}"); var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
} var result = validationService.ValidateAppSettings(config);
foreach (var warning in result.Warnings) logger.LogInformation("=== AppSettings Validation ===");
logger.LogInformation("File: {Path}", appSettingsPath);
if (result.IsValid)
{
logger.LogInformation("Status: Valid");
return 0;
}
foreach (var error in result.Errors)
{
logger.LogError("{Error}", error);
}
foreach (var warning in result.Warnings)
{
logger.LogWarning("{Warning}", warning);
}
return 1;
}
catch (ConfigLoadException ex)
{ {
if (!quiet) logger.LogError(ex, "Failed to load configuration");
Console.WriteLine($"Warning: {warning}"); return 1;
} }
return 1;
}
catch (ConfigLoadException ex)
{
Console.Error.WriteLine($"Error loading configuration: {ex.Message}");
return 1;
} }
} }
@@ -157,54 +163,59 @@ public static class ValidateCommand
bool verbose, bool verbose,
bool quiet) bool quiet)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ValidateCommand");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "validate pipelines",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var validationService = serviceProvider.GetRequiredService<IValidationService>();
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
try
{ {
var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
var result = validationService.ValidatePipelines(pipelines); if (folderPath == null)
if (!quiet)
{ {
Console.WriteLine("=== Pipelines Validation ==="); logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.WriteLine($"Directory: {pipelinesDir}"); return 1;
Console.WriteLine($"Pipelines found: {pipelines.Count}");
} }
if (result.IsValid) var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
{ var validationService = serviceProvider.GetRequiredService<IValidationService>();
if (!quiet)
Console.WriteLine("Status: Valid");
return 0;
}
foreach (var error in result.Errors) var pipelinesDir = Path.Combine(folderPath, "Pipelines");
{
Console.Error.WriteLine($"Error: {error}");
}
foreach (var warning in result.Warnings) try
{ {
if (!quiet) var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir);
Console.WriteLine($"Warning: {warning}"); var result = validationService.ValidatePipelines(pipelines);
}
return result.Errors.Count > 0 ? 1 : 0; logger.LogInformation("=== Pipelines Validation ===");
} logger.LogInformation("Directory: {Path}", pipelinesDir);
catch (Exception ex) logger.LogInformation("Pipelines found: {Count}", pipelines.Count);
{
Console.Error.WriteLine($"Error validating pipelines: {ex.Message}"); if (result.IsValid)
return 1; {
logger.LogInformation("Status: Valid");
return 0;
}
foreach (var error in result.Errors)
{
logger.LogError("{Error}", error);
}
foreach (var warning in result.Warnings)
{
logger.LogWarning("{Warning}", warning);
}
return result.Errors.Count > 0 ? 1 : 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to validate pipelines");
return 1;
}
} }
} }
@@ -214,48 +225,54 @@ public static class ValidateCommand
bool verbose, bool verbose,
bool quiet) bool quiet)
{ {
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
if (folderPath == null) var logger = loggerFactory.CreateLogger("ValidateCommand");
using (logger.BeginScope(new Dictionary<string, object>
{ {
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); ["Command"] = "validate runtime",
return 1; ["ConfigPath"] = configPath ?? "(default)"
} }))
var runtimeValidationService = serviceProvider.GetRequiredService<IRuntimeConfigValidationService>();
if (!quiet)
{ {
Console.WriteLine("=== Runtime Configuration Validation ==="); var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
Console.WriteLine($"Folder: {folderPath}"); if (folderPath == null)
}
var results = runtimeValidationService.ValidateRuntimeConfig(folderPath);
var hasErrors = false;
foreach (var result in results)
{
if (result.Errors.Count > 0)
{ {
hasErrors = true; logger.LogError("Could not find configuration folder. Use --config-path to specify");
Console.Error.WriteLine($"\n[{result.ValidatorName}]"); return 1;
foreach (var error in result.Errors) }
var runtimeValidationService = serviceProvider.GetRequiredService<IRuntimeConfigValidationService>();
logger.LogInformation("=== Runtime Configuration Validation ===");
logger.LogInformation("Folder: {Path}", folderPath);
var results = runtimeValidationService.ValidateRuntimeConfig(folderPath);
var hasErrors = false;
foreach (var result in results)
{
if (result.Errors.Count > 0)
{ {
Console.Error.WriteLine($" Error: {error}"); hasErrors = true;
logger.LogError("[{ValidatorName}]", result.ValidatorName);
foreach (var error in result.Errors)
{
logger.LogError(" {Error}", error);
}
}
else
{
logger.LogInformation("[{ValidatorName}]: OK", result.ValidatorName);
}
foreach (var warning in result.Warnings)
{
logger.LogWarning(" {Warning}", warning);
} }
} }
else if (!quiet)
{
Console.WriteLine($"[{result.ValidatorName}]: OK");
}
foreach (var warning in result.Warnings) return hasErrors ? 1 : 0;
{
if (!quiet)
Console.WriteLine($" Warning: {warning}");
}
} }
return hasErrors ? 1 : 0;
} }
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath) private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
@@ -10,7 +10,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.*" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.*" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.*" /> <PackageReference Include="Serilog" Version="4.*" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.*" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.*" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup> </ItemGroup>
@@ -2,7 +2,8 @@ using System.CommandLine;
using JdeScoping.ConfigManager.Cli.Commands; using JdeScoping.ConfigManager.Cli.Commands;
using JdeScoping.ConfigManager.Core.DependencyInjection; using JdeScoping.ConfigManager.Core.DependencyInjection;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Serilog;
using Serilog.Events;
namespace JdeScoping.ConfigManager.Cli; namespace JdeScoping.ConfigManager.Cli;
@@ -18,8 +19,12 @@ public static class Program
/// <returns>Exit code: 0 for success, non-zero for errors.</returns> /// <returns>Exit code: 0 for success, non-zero for errors.</returns>
public static async Task<int> Main(string[] args) public static async Task<int> Main(string[] args)
{ {
// Pre-parse verbose/quiet flags for logging configuration
bool verbose = args.Contains("-v") || args.Contains("--verbose");
bool quiet = args.Contains("-q") || args.Contains("--quiet");
var services = new ServiceCollection(); var services = new ServiceCollection();
ConfigureServices(services); ConfigureServices(services, verbose, quiet);
using var serviceProvider = services.BuildServiceProvider(); using var serviceProvider = services.BuildServiceProvider();
var rootCommand = new RootCommand("JDE Scoping Tool configuration management CLI") var rootCommand = new RootCommand("JDE Scoping Tool configuration management CLI")
@@ -101,15 +106,34 @@ public static class Program
connectionCommand.AddCommand(ConnectionCommands.CreateRemoveCommand(serviceProvider, configPathOption, verboseOption, quietOption)); connectionCommand.AddCommand(ConnectionCommands.CreateRemoveCommand(serviceProvider, configPathOption, verboseOption, quietOption));
rootCommand.AddCommand(connectionCommand); rootCommand.AddCommand(connectionCommand);
return await rootCommand.InvokeAsync(args); try
{
return await rootCommand.InvokeAsync(args);
}
finally
{
await Log.CloseAndFlushAsync();
}
} }
private static void ConfigureServices(IServiceCollection services) private static void ConfigureServices(IServiceCollection services, bool verbose, bool quiet)
{ {
services.AddLogging(builder => builder // Log level mapping:
.AddConsole() // --quiet: Warning level (hide success/info messages, only show warnings and errors)
.SetMinimumLevel(LogLevel.Warning)); // default: Information level (show success messages)
// --verbose: Debug level (show all messages including debug details)
var logLevel = quiet ? LogEventLevel.Warning
: verbose ? LogEventLevel.Debug
: LogEventLevel.Information;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(logLevel)
.WriteTo.Console(
outputTemplate: "{Message:lj}{NewLine}{Exception}")
.Enrich.FromLogContext()
.CreateLogger();
services.AddLogging(builder => builder.AddSerilog(dispose: true));
services.AddConfigManagerCore(); services.AddConfigManagerCore();
} }
} }
@@ -0,0 +1,203 @@
# ConfigManager CLI Logging Style Guide
This document describes the logging approach used in the ConfigManager CLI application.
## Architecture
The CLI uses **Microsoft.Extensions.Logging** as the logging abstraction with **Serilog** as the provider. This provides:
- Structured logging with semantic parameter names
- Flexible log level control via command-line flags
- Clean, message-only console output for CLI tooling
## Configuration
### Dependency Injection Setup
Logging is configured via dependency injection in `Program.cs`. Serilog is registered with the DI container:
```csharp
private static void ConfigureServices(IServiceCollection services, bool verbose, bool quiet)
{
// Determine log level from command-line flags
var logLevel = quiet ? LogEventLevel.Warning
: verbose ? LogEventLevel.Debug
: LogEventLevel.Information;
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(logLevel)
.WriteTo.Console(outputTemplate: "{Message:lj}{NewLine}{Exception}")
.Enrich.FromLogContext()
.CreateLogger();
// Register logging with DI container
services.AddLogging(builder => builder.AddSerilog(dispose: true));
// Register other services...
}
```
This approach:
- Creates a Serilog logger configured for CLI output
- Registers it with `Microsoft.Extensions.Logging` via `AddSerilog()`
- Makes `ILoggerFactory` available to command handlers through the service provider
### Log Level Mapping
The CLI supports three verbosity modes controlled by command-line flags:
| Flag | Log Level | Visible Messages |
|------|-----------|------------------|
| `--quiet` / `-q` | Warning | Warnings and errors only |
| (default) | Information | Success messages, data output, warnings, errors |
| `--verbose` / `-v` | Debug | All messages including debug details |
### Output Format
The Serilog output template is configured for clean CLI output:
```
{Message:lj}{NewLine}{Exception}
```
This produces message-only output without timestamps or log levels, suitable for command-line tools.
## Logger Injection Pattern
Command handlers obtain a logger from the service provider:
```csharp
private static async Task<int> SomeCommandAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet)
{
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("CommandClassName");
// Command implementation using logger
}
```
## Structured Logging Patterns
### Log Scope with Context Properties
Wrap command logic in a log scope to add context:
```csharp
using (logger.BeginScope(new Dictionary<string, object>
{
["Command"] = "secret set",
["ConfigPath"] = configPath ?? "(default)",
["SecretKey"] = key
}))
{
// All log messages within this block include the scope properties
}
```
### Standard Context Properties
| Property | Description | Example |
|----------|-------------|---------|
| `Command` | Full command path | `"secret list"`, `"config set datasync"` |
| `ConfigPath` | Configuration folder path | `"/app/config"` or `"(default)"` |
| `ConnectionName` | Connection name for connection commands | `"jde"` |
| `PipelineName` | Pipeline name for pipeline commands | `"WorkOrder"` |
| `BackupTimestamp` | Timestamp for backup operations | `"2026-01-15_120000"` |
### Message Templates
Use semantic parameter names in message templates:
```csharp
// Good - uses named parameters
logger.LogInformation("Secret '{Key}' set successfully", key);
logger.LogError("Connection '{Name}' not found", name);
// Avoid - positional or embedded values
logger.LogInformation($"Secret '{key}' set successfully");
```
## Log Level Guidelines
### Information Level
Use for:
- Success messages
- Data output (lists, tables, JSON)
- Section headers
```csharp
logger.LogInformation("Secret '{Key}' set successfully", key);
logger.LogInformation("=== Connections ({Count}) ===", entries.Count);
logger.LogInformation("{Data}", JsonSerializer.Serialize(section, options));
```
### Debug Level
Use for:
- Verbose details (shown with `--verbose`)
- Intermediate progress updates
- Detailed configuration values
```csharp
logger.LogDebug("Backup created: {BackupName}", Path.GetFileName(backupPath));
logger.LogDebug("LookbackMultiplier: {Value}", section.LookbackMultiplier);
```
### Warning Level
Use for:
- Non-critical issues
- Validation warnings
```csharp
logger.LogWarning("{Warning}", warning);
```
### Error Level
Use for:
- Operation failures
- Missing required files
- Validation errors
- Exceptions
```csharp
logger.LogError("Could not find configuration folder. Use --config-path to specify");
logger.LogError("Connection '{Name}' not found", name);
logger.LogError(ex, "Failed to set secret");
```
## Converting from Console.WriteLine
| Pattern | Before | After |
|---------|--------|-------|
| Success message | `Console.WriteLine($"Secret '{key}' set successfully");` | `logger.LogInformation("Secret '{Key}' set successfully", key);` |
| Error message | `Console.Error.WriteLine($"Error: {ex.Message}");` | `logger.LogError(ex, "Operation failed");` |
| Data output | `Console.WriteLine(JsonSerializer.Serialize(...));` | `logger.LogInformation("{Data}", JsonSerializer.Serialize(...));` |
| Table row | `Console.WriteLine($"{name,-25} {value,-15}");` | `logger.LogInformation("{Name,-25} {Value,-15}", name, value);` |
| Verbose detail | `if (verbose) Console.WriteLine(...);` | `logger.LogDebug(...);` |
| Quiet guard | `if (!quiet) Console.WriteLine(...);` | `logger.LogInformation(...);` (level handles filtering) |
## Interactive Prompts
Keep `Console.Write` for interactive prompts that require user input:
```csharp
// This stays as Console.Write - interactive prompt
Console.Write($"Delete pipeline '{name}'? [y/N] ");
var response = Console.ReadLine();
```
## Best Practices
1. **Always use structured parameters** - Never embed values directly in message strings
2. **Use consistent context properties** - Follow the standard property names above
3. **Let log levels handle filtering** - Don't add manual `if (!quiet)` guards
4. **Include exceptions in error logs** - Use `logger.LogError(ex, "Message")` to capture stack traces
5. **Keep messages actionable** - Error messages should help users understand what to do next
@@ -23,6 +23,7 @@ public class BackupCommandsTests
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton(_backupService); services.AddSingleton(_backupService);
services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_autoDiscoveryService);
services.AddTestLogging();
_serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]); _configPathOption = new Option<string?>(["--config-path", "-c"]);
@@ -272,10 +273,10 @@ public class BackupCommandsTests
rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption); rootCommand.AddGlobalOption(_quietOption);
// Capture console error output // Capture console output (Serilog writes all output to stdout)
var originalErr = Console.Error; var originalOut = Console.Out;
using var writer = new StringWriter(); using var writer = new StringWriter();
Console.SetError(writer); Console.SetOut(writer);
try try
{ {
@@ -288,7 +289,7 @@ public class BackupCommandsTests
} }
finally finally
{ {
Console.SetError(originalErr); Console.SetOut(originalOut);
} }
} }
finally finally
@@ -27,6 +27,7 @@ public class ConfigCommandsTests
services.AddSingleton(_configFileService); services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_autoDiscoveryService);
services.AddSingleton(_backupService); services.AddSingleton(_backupService);
services.AddTestLogging();
_serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]); _configPathOption = new Option<string?>(["--config-path", "-c"]);
@@ -366,10 +367,10 @@ public class ConfigCommandsTests
rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption); rootCommand.AddGlobalOption(_quietOption);
// Capture console error output // Capture console output (Serilog writes all output to stdout)
var originalErr = Console.Error; var originalOut = Console.Out;
using var writer = new StringWriter(); using var writer = new StringWriter();
Console.SetError(writer); Console.SetOut(writer);
try try
{ {
@@ -382,7 +383,7 @@ public class ConfigCommandsTests
} }
finally finally
{ {
Console.SetError(originalErr); Console.SetOut(originalOut);
} }
} }
finally finally
@@ -24,6 +24,7 @@ public class ConnectionCommandsTests
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton(_configFileService); services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_autoDiscoveryService);
services.AddTestLogging();
_serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]); _configPathOption = new Option<string?>(["--config-path", "-c"]);
@@ -219,10 +220,10 @@ public class ConnectionCommandsTests
rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption); rootCommand.AddGlobalOption(_quietOption);
// Capture console error output // Capture console output (Serilog writes all output to stdout)
var originalErr = Console.Error; var originalOut = Console.Out;
using var writer = new StringWriter(); using var writer = new StringWriter();
Console.SetError(writer); Console.SetOut(writer);
try try
{ {
@@ -235,7 +236,7 @@ public class ConnectionCommandsTests
} }
finally finally
{ {
Console.SetError(originalErr); Console.SetOut(originalOut);
} }
} }
finally finally
@@ -331,10 +332,10 @@ public class ConnectionCommandsTests
rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption); rootCommand.AddGlobalOption(_quietOption);
// Capture console error output // Capture console output (Serilog writes all output to stdout)
var originalErr = Console.Error; var originalOut = Console.Out;
using var writer = new StringWriter(); using var writer = new StringWriter();
Console.SetError(writer); Console.SetOut(writer);
try try
{ {
@@ -347,7 +348,7 @@ public class ConnectionCommandsTests
} }
finally finally
{ {
Console.SetError(originalErr); Console.SetOut(originalOut);
} }
} }
finally finally
@@ -393,10 +394,10 @@ public class ConnectionCommandsTests
rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption); rootCommand.AddGlobalOption(_quietOption);
// Capture console error output // Capture console output (Serilog writes all output to stdout)
var originalErr = Console.Error; var originalOut = Console.Out;
using var writer = new StringWriter(); using var writer = new StringWriter();
Console.SetError(writer); Console.SetOut(writer);
try try
{ {
@@ -409,7 +410,7 @@ public class ConnectionCommandsTests
} }
finally finally
{ {
Console.SetError(originalErr); Console.SetOut(originalOut);
} }
} }
finally finally
@@ -478,10 +479,10 @@ public class ConnectionCommandsTests
rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption); rootCommand.AddGlobalOption(_quietOption);
// Capture console error output // Capture console output (Serilog writes all output to stdout)
var originalErr = Console.Error; var originalOut = Console.Out;
using var writer = new StringWriter(); using var writer = new StringWriter();
Console.SetError(writer); Console.SetOut(writer);
try try
{ {
@@ -494,7 +495,7 @@ public class ConnectionCommandsTests
} }
finally finally
{ {
Console.SetError(originalErr); Console.SetOut(originalOut);
} }
} }
finally finally
@@ -24,6 +24,7 @@ public class PipelineCommandsTests
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton(_configFileService); services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_autoDiscoveryService);
services.AddTestLogging();
_serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]); _configPathOption = new Option<string?>(["--config-path", "-c"]);
@@ -28,6 +28,7 @@ public class SecretCommandsTests
services.AddSingleton(_configFileService); services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_autoDiscoveryService);
services.AddSingleton(_secureStoreManager); services.AddSingleton(_secureStoreManager);
services.AddTestLogging();
_serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]); _configPathOption = new Option<string?>(["--config-path", "-c"]);
@@ -169,10 +170,10 @@ public class SecretCommandsTests
rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption); rootCommand.AddGlobalOption(_quietOption);
// Capture console error output // Capture console output (Serilog writes all output to stdout)
var originalErr = Console.Error; var originalOut = Console.Out;
using var writer = new StringWriter(); using var writer = new StringWriter();
Console.SetError(writer); Console.SetOut(writer);
try try
{ {
@@ -185,7 +186,7 @@ public class SecretCommandsTests
} }
finally finally
{ {
Console.SetError(originalErr); Console.SetOut(originalOut);
} }
} }
finally finally
@@ -27,6 +27,7 @@ public class TestConnectionCommandTests
services.AddSingleton(_configFileService); services.AddSingleton(_configFileService);
services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_autoDiscoveryService);
services.AddSingleton(_connectionTestService); services.AddSingleton(_connectionTestService);
services.AddTestLogging();
_serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]); _configPathOption = new Option<string?>(["--config-path", "-c"]);
@@ -243,10 +244,10 @@ public class TestConnectionCommandTests
rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption); rootCommand.AddGlobalOption(_quietOption);
// Capture console error output // Capture console output (Serilog writes all output to stdout)
var originalErr = Console.Error; var originalOut = Console.Out;
using var writer = new StringWriter(); using var writer = new StringWriter();
Console.SetError(writer); Console.SetOut(writer);
try try
{ {
@@ -259,7 +260,7 @@ public class TestConnectionCommandTests
} }
finally finally
{ {
Console.SetError(originalErr); Console.SetOut(originalOut);
} }
} }
finally finally
@@ -31,6 +31,7 @@ public class ValidateCommandTests
services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_autoDiscoveryService);
services.AddSingleton(_validationService); services.AddSingleton(_validationService);
services.AddSingleton(_runtimeValidationService); services.AddSingleton(_runtimeValidationService);
services.AddTestLogging();
_serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();
_configPathOption = new Option<string?>(["--config-path", "-c"]); _configPathOption = new Option<string?>(["--config-path", "-c"]);
@@ -193,10 +194,10 @@ public class ValidateCommandTests
rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_verboseOption);
rootCommand.AddGlobalOption(_quietOption); rootCommand.AddGlobalOption(_quietOption);
// Capture console error output // Capture console output (Serilog writes all output to stdout)
var originalErr = Console.Error; var originalOut = Console.Out;
using var writer = new StringWriter(); using var writer = new StringWriter();
Console.SetError(writer); Console.SetOut(writer);
try try
{ {
@@ -205,11 +206,11 @@ public class ValidateCommandTests
// Assert // Assert
var output = writer.ToString(); var output = writer.ToString();
output.ShouldContain("Error"); output.ShouldContain("Connection string 'LocalCache' is required");
} }
finally finally
{ {
Console.SetError(originalErr); Console.SetOut(originalOut);
} }
} }
finally finally
@@ -0,0 +1,27 @@
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Events;
namespace JdeScoping.ConfigManager.Cli.Tests;
/// <summary>
/// Helper for setting up logging in CLI tests.
/// </summary>
public static class TestLoggingHelper
{
/// <summary>
/// Adds Serilog logging to a service collection configured to write to Console.
/// This allows tests to capture log output via Console.SetOut.
/// </summary>
public static IServiceCollection AddTestLogging(this IServiceCollection services, LogEventLevel level = LogEventLevel.Information)
{
var logger = new LoggerConfiguration()
.MinimumLevel.Is(level)
.WriteTo.Console(outputTemplate: "{Message:lj}{NewLine}{Exception}")
.CreateLogger();
services.AddLogging(builder => builder.AddSerilog(logger, dispose: true));
return services;
}
}