diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/BackupCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/BackupCommands.cs index 28d35f0..2134d59 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/BackupCommands.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/BackupCommands.cs @@ -1,6 +1,7 @@ using System.CommandLine; using JdeScoping.ConfigManager.Core.Services; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JdeScoping.ConfigManager.Cli.Commands; @@ -104,38 +105,45 @@ public static class BackupCommands bool verbose, bool quiet) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("BackupCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var backupService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = "backup create", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var backupPath = await backupService.CreateBackupAsync(appSettingsPath); - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.WriteLine("Backup created successfully"); - Console.WriteLine($"Path: {backupPath}"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + var backupService = serviceProvider.GetRequiredService(); + var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); + + if (!File.Exists(appSettingsPath)) + { + 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 quiet) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("BackupCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var backupService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - try + ["Command"] = "backup list", + ["ConfigPath"] = configPath ?? "(default)" + })) { - var backups = await backupService.GetBackupsAsync(appSettingsPath); - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - 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(); + var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); + + try { - if (!quiet) - Console.WriteLine("No backups found"); + var backups = await backupService.GetBackupsAsync(appSettingsPath); + + 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; } - - if (!quiet) + catch (Exception ex) { - Console.WriteLine($"{"Timestamp",-20} {"Size",-12} {"Path"}"); - Console.WriteLine(new string('-', 60)); + logger.LogError(ex, "Failed to list backups"); + 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, string timestamp) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("BackupCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var backupService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - try + ["Command"] = "backup restore", + ["ConfigPath"] = configPath ?? "(default)", + ["BackupTimestamp"] = timestamp + })) { - var backups = await backupService.GetBackupsAsync(appSettingsPath); - var backup = backups.FirstOrDefault(b => b.Path.Contains(timestamp)); - - if (backup == null) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.Error.WriteLine($"Error: Backup with timestamp '{timestamp}' not found"); - - 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}"); - } - } - + logger.LogError("Could not find configuration folder. Use --config-path to specify"); return 1; } - // Create a backup of current config before restoring - if (File.Exists(appSettingsPath)) + var backupService = serviceProvider.GetRequiredService(); + var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); + + try { - var currentBackup = await backupService.CreateBackupAsync(appSettingsPath); - if (!quiet) + var backups = await backupService.GetBackupsAsync(appSettingsPath); + 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; } - - await backupService.RestoreBackupAsync(backup.Path, appSettingsPath); - - if (!quiet) + catch (Exception ex) { - Console.WriteLine("Backup restored successfully"); - Console.WriteLine($"Restored from: {Path.GetFileName(backup.Path)}"); + logger.LogError(ex, "Failed to restore backup"); + 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, int keep) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("BackupCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - if (keep < 1) + ["Command"] = "backup cleanup", + ["ConfigPath"] = configPath ?? "(default)", + ["KeepCount"] = keep + })) { - Console.Error.WriteLine("Error: --keep must be at least 1"); - return 1; - } - - var backupService = serviceProvider.GetRequiredService(); - 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) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { + 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(); + 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) { - Console.WriteLine($"Deleted {deleted} old backup(s)"); - Console.WriteLine($"Remaining: {backupsAfter.Count} backup(s)"); + logger.LogInformation("Deleted {Count} old backup(s)", deleted); + logger.LogInformation("Remaining: {Count} backup(s)", backupsAfter.Count); } else { - Console.WriteLine("No backups to delete"); - Console.WriteLine($"Current count: {countBefore} (keeping {keep})"); + logger.LogInformation("No backups to delete"); + logger.LogInformation("Current count: {Count} (keeping {Keep})", countBefore, keep); } - } - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + return 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to cleanup backups"); + return 1; + } } } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs index cb6dbe1..072a7c3 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs @@ -3,6 +3,7 @@ using System.Text.Json; using JdeScoping.ConfigManager.Core.Models; using JdeScoping.ConfigManager.Core.Services; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JdeScoping.ConfigManager.Cli.Commands; @@ -78,7 +79,7 @@ public static class ConfigCommands Option quietOption, Option jsonOption, Func selector, - Action formatter) + Action formatter) { var command = new Command(name, description); @@ -135,48 +136,55 @@ public static class ConfigCommands bool json, string sectionName, Func selector, - Action formatter) + Action formatter) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ConfigCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = $"config show {sectionName}", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - var section = selector(config); - - if (json) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - 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(); + 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; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + return 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to show section"); + return 1; + } } } @@ -187,57 +195,64 @@ public static class ConfigCommands bool quiet, bool json) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ConfigCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = "config show connections", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - - if (json) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - // Output only names and providers, no secrets - var safeConnections = config.ConnectionStrings.Entries.Select(e => new - { - 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}"); - } + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + var configFileService = serviceProvider.GetRequiredService(); + 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) + { + // 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 json) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ConfigCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = "config show all", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - - if (json) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - // Sanitize passwords before outputting - var sanitizedConfig = new + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; + } + + var configFileService = serviceProvider.GetRequiredService(); + 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, - config.DataAccess, - config.Auth, - config.Ldap, - config.Search, - config.ExcelExport, - config.SecureStore, - config.Pipelines, - Connections = config.ConnectionStrings.Entries.Select(e => new + // Sanitize passwords before outputting + var sanitizedConfig = new { - e.Name, - Provider = e.Provider.ToString() - }) - }; - Console.WriteLine(JsonSerializer.Serialize(sanitizedConfig, JsonOptions)); - } - else - { - Console.WriteLine("=== DataSync ==="); - FormatDataSync(config.DataSync, verbose); - Console.WriteLine(); - - Console.WriteLine("=== DataAccess ==="); - FormatDataAccess(config.DataAccess, verbose); - Console.WriteLine(); - - 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}"); + config.DataSync, + config.DataAccess, + config.Auth, + config.Ldap, + config.Search, + config.ExcelExport, + config.SecureStore, + config.Pipelines, + Connections = config.ConnectionStrings.Entries.Select(e => new + { + e.Name, + Provider = e.Provider.ToString() + }) + }; + logger.LogInformation("{Data}", JsonSerializer.Serialize(sanitizedConfig, JsonOptions)); } + 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}"); - Console.WriteLine($"CheckInterval: {section.CheckInterval}"); - Console.WriteLine($"MaxDegreeOfParallelism: {section.MaxDegreeOfParallelism}"); - Console.WriteLine($"BatchSize: {section.BatchSize}"); - Console.WriteLine($"BulkCopyBatchSize: {section.BulkCopyBatchSize}"); + logger.LogInformation("Enabled: {Value}", section.Enabled); + logger.LogInformation("CheckInterval: {Value}", section.CheckInterval); + logger.LogInformation("MaxDegreeOfParallelism: {Value}", section.MaxDegreeOfParallelism); + logger.LogInformation("BatchSize: {Value}", section.BatchSize); + logger.LogInformation("BulkCopyBatchSize: {Value}", section.BulkCopyBatchSize); if (verbose) { - Console.WriteLine($"LookbackMultiplier: {section.LookbackMultiplier}"); - Console.WriteLine($"PurgeRetentionDays: {section.PurgeRetentionDays}"); - Console.WriteLine($"SyncTimeoutSeconds: {section.SyncTimeoutSeconds}"); + logger.LogDebug("LookbackMultiplier: {Value}", section.LookbackMultiplier); + logger.LogDebug("PurgeRetentionDays: {Value}", section.PurgeRetentionDays); + 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}"); - Console.WriteLine($"LotUsageTimeoutSeconds: {section.LotUsageTimeoutSeconds}"); - Console.WriteLine($"MisDataTimeoutSeconds: {section.MisDataTimeoutSeconds}"); - Console.WriteLine($"EnableDetailedLogging: {section.EnableDetailedLogging}"); + logger.LogInformation("DefaultTimeoutSeconds: {Value}", section.DefaultTimeoutSeconds); + logger.LogInformation("LotUsageTimeoutSeconds: {Value}", section.LotUsageTimeoutSeconds); + logger.LogInformation("MisDataTimeoutSeconds: {Value}", section.MisDataTimeoutSeconds); + logger.LogInformation("EnableDetailedLogging: {Value}", section.EnableDetailedLogging); if (verbose) { - Console.WriteLine($"ProductionSchema: {section.ProductionSchema}"); - Console.WriteLine($"ArchiveSchema: {section.ArchiveSchema}"); - Console.WriteLine($"StageSchema: {section.StageSchema}"); + logger.LogDebug("ProductionSchema: {Value}", section.ProductionSchema); + logger.LogDebug("ArchiveSchema: {Value}", section.ArchiveSchema); + 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}"); - Console.WriteLine($"CookieExpirationMinutes: {section.CookieExpirationMinutes}"); + logger.LogInformation("CookieName: {Value}", section.CookieName); + 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}"); - Console.WriteLine($"ConnectionTimeoutSeconds: {section.ConnectionTimeoutSeconds}"); - Console.WriteLine($"ServerUrls: {(section.ServerUrls.Length > 0 ? string.Join(", ", section.ServerUrls) : "(none)")}"); - Console.WriteLine($"GroupDn: {(string.IsNullOrEmpty(section.GroupDn) ? "(not set)" : section.GroupDn)}"); + logger.LogInformation("UseFakeAuth: {Value}", section.UseFakeAuth); + logger.LogInformation("ConnectionTimeoutSeconds: {Value}", section.ConnectionTimeoutSeconds); + logger.LogInformation("ServerUrls: {Value}", section.ServerUrls.Length > 0 ? string.Join(", ", section.ServerUrls) : "(none)"); + logger.LogInformation("GroupDn: {Value}", string.IsNullOrEmpty(section.GroupDn) ? "(not set)" : section.GroupDn); if (verbose) { - Console.WriteLine($"SearchBase: {(string.IsNullOrEmpty(section.SearchBase) ? "(not set)" : section.SearchBase)}"); - Console.WriteLine($"AdminBypassUsers: {(section.AdminBypassUsers.Length > 0 ? string.Join(", ", section.AdminBypassUsers) : "(none)")}"); + logger.LogDebug("SearchBase: {Value}", string.IsNullOrEmpty(section.SearchBase) ? "(not set)" : section.SearchBase); + 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}"); - Console.WriteLine($"TimeoutSeconds: {section.TimeoutSeconds}"); - Console.WriteLine($"MaxConcurrentSearches: {section.MaxConcurrentSearches}"); + logger.LogInformation("MaxResultRows: {Value}", section.MaxResultRows); + logger.LogInformation("TimeoutSeconds: {Value}", section.TimeoutSeconds); + 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}"); - Console.WriteLine($"DefaultDateFormat: {section.DefaultDateFormat}"); - Console.WriteLine($"TimezoneId: {section.TimezoneId}"); - Console.WriteLine($"TimezoneAbbreviation: {section.TimezoneAbbreviation}"); - Console.WriteLine($"DebugWriteToFile: {section.DebugWriteToFile}"); + logger.LogInformation("MaxRowsPerSheet: {Value}", section.MaxRowsPerSheet); + logger.LogInformation("DefaultDateFormat: {Value}", section.DefaultDateFormat); + logger.LogInformation("TimezoneId: {Value}", section.TimezoneId); + logger.LogInformation("TimezoneAbbreviation: {Value}", section.TimezoneAbbreviation); + logger.LogInformation("DebugWriteToFile: {Value}", section.DebugWriteToFile); if (verbose && section.DebugWriteToFile) { - Console.WriteLine($"DebugOutputDirectory: {section.DebugOutputDirectory}"); + logger.LogDebug("DebugOutputDirectory: {Value}", section.DebugOutputDirectory); } // Note: passwords are not shown - Console.WriteLine("CriteriaSheetPassword: ********"); - Console.WriteLine("DataSheetPassword: ********"); + logger.LogInformation("CriteriaSheetPassword: {Value}", "********"); + 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}"); - Console.WriteLine($"KeyFilePath: {section.KeyFilePath}"); - Console.WriteLine($"AutoCreateStore: {section.AutoCreateStore}"); - Console.WriteLine($"RequiredKeys: {(section.RequiredKeys.Count > 0 ? string.Join(", ", section.RequiredKeys) : "(none)")}"); + logger.LogInformation("StorePath: {Value}", section.StorePath); + logger.LogInformation("KeyFilePath: {Value}", section.KeyFilePath); + logger.LogInformation("AutoCreateStore: {Value}", section.AutoCreateStore); + logger.LogInformation("RequiredKeys: {Value}", section.RequiredKeys.Count > 0 ? string.Join(", ", section.RequiredKeys) : "(none)"); } private static async Task GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath) @@ -980,54 +1005,58 @@ public static class ConfigCommands string sectionName, Func modifier) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ConfigCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var backupService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = $"config set {sectionName.ToLower()}", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - var modified = modifier(config); - - if (!modified) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - 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; } - // Create backup before saving - var backupPath = await backupService.CreateBackupAsync(appSettingsPath); - if (verbose) + var configFileService = serviceProvider.GetRequiredService(); + var backupService = serviceProvider.GetRequiredService(); + 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); - - if (!quiet) + try { - Console.WriteLine($"{sectionName} configuration updated successfully"); - } + var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); + var modified = modifier(config); - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + if (!modified) + { + logger.LogError("No changes specified. Use --help to see available options"); + 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; + } } } } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs index 36ad279..920600b 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs @@ -2,6 +2,7 @@ using System.CommandLine; using JdeScoping.ConfigManager.Core.Models; using JdeScoping.ConfigManager.Core.Services; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JdeScoping.ConfigManager.Cli.Commands; @@ -239,52 +240,59 @@ public static class ConnectionCommands bool verbose, bool quiet) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ConnectionCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = "connection list", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ==="); - Console.WriteLine($"{"Name",-25} {"Provider",-15} {"Server/Host",-30}"); - Console.WriteLine(new string('-', 70)); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name)) + var configFileService = serviceProvider.GetRequiredService(); + 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 ?? "-", - ConnectionProvider.Oracle => entry.Host ?? "-", - ConnectionProvider.Generic => "(raw)", - _ => "-" - }; + var serverOrHost = entry.Provider switch + { + 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, string name) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ConnectionCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = "connection show", + ["ConfigPath"] = configPath ?? "(default)", + ["ConnectionName"] = name + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - var entry = config.ConnectionStrings.Entries.FirstOrDefault(e => - e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - - if (entry == null) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.Error.WriteLine($"Error: Connection '{name}' not found"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); return 1; } - if (!quiet) + var configFileService = serviceProvider.GetRequiredService(); + 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}"); - Console.WriteLine($"Provider: {entry.Provider}"); - - switch (entry.Provider) + try { - case ConnectionProvider.SqlServer: - Console.WriteLine($"Server: {entry.Server ?? "(not set)"}"); - if (entry.SqlServerPort.HasValue) - 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; + var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); + var entry = config.ConnectionStrings.Entries.FirstOrDefault(e => + e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - case ConnectionProvider.Oracle: - Console.WriteLine($"Host: {entry.Host ?? "(not set)"}"); - Console.WriteLine($"Port: {entry.Port}"); - Console.WriteLine($"ServiceName: {entry.ServiceName ?? "(not set)"}"); - Console.WriteLine($"User ID: {entry.UserId ?? "(not set)"}"); - Console.WriteLine($"Password: {MaskPassword(entry.Password)}"); - break; + if (entry == null) + { + logger.LogError("Connection '{Name}' not found", name); + return 1; + } - case ConnectionProvider.Generic: - Console.WriteLine($"RawConnectionString: {MaskConnectionString(entry.RawConnectionString)}"); - break; + logger.LogInformation("=== Connection: {Name} ===", entry.Name); + + 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; } - - if (verbose) + catch (Exception ex) { - Console.WriteLine(); - Console.WriteLine("Generated connection string (masked):"); - Console.WriteLine($" {MaskConnectionString(entry.GenerateConnectionString())}"); + logger.LogError(ex, "Failed to show connection"); + return 1; } - - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; } } @@ -391,76 +407,84 @@ public static class ConnectionCommands string? serviceName, string? raw) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ConnectionCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = "connection add", + ["ConfigPath"] = configPath ?? "(default)", + ["ConnectionName"] = name + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - - // Check if connection already exists - if (config.ConnectionStrings.Entries.Any(e => - e.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.Error.WriteLine($"Error: Connection '{name}' already exists"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); return 1; } - // Validate required fields based on provider - if (provider == ConnectionProvider.Generic && string.IsNullOrEmpty(raw)) + var configFileService = serviceProvider.GetRequiredService(); + 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; } - var entry = new ConnectionStringEntry + try { - Name = name, - Provider = provider, - Server = server, - Database = database, - UserId = user, - Password = password, - Host = server, // For Oracle, use server as host - ServiceName = serviceName, - RawConnectionString = raw - }; + var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - if (port.HasValue) - { - if (provider == ConnectionProvider.SqlServer) - entry.SqlServerPort = port; - else if (provider == ConnectionProvider.Oracle) - entry.Port = port.Value; + // Check if connection already exists + if (config.ConnectionStrings.Entries.Any(e => + e.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + logger.LogError("Connection '{Name}' already exists", name); + 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; } - - config.ConnectionStrings.Entries.Add(entry); - await configFileService.SaveAppSettingsAsync(appSettingsPath, config); - - if (!quiet) + catch (Exception ex) { - 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, string name) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ConnectionCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = "connection remove", + ["ConfigPath"] = configPath ?? "(default)", + ["ConnectionName"] = name + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - var entry = config.ConnectionStrings.Entries.FirstOrDefault(e => - e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - - if (entry == null) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.Error.WriteLine($"Error: Connection '{name}' not found"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); return 1; } - config.ConnectionStrings.Entries.Remove(entry); - await configFileService.SaveAppSettingsAsync(appSettingsPath, config); + var configFileService = serviceProvider.GetRequiredService(); + 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; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + try + { + var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); + var entry = config.ConnectionStrings.Entries.FirstOrDefault(e => + e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + + 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? raw) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ConnectionCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - - if (!File.Exists(appSettingsPath)) + ["Command"] = "connection update", + ["ConfigPath"] = configPath ?? "(default)", + ["ConnectionName"] = name + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - var entry = config.ConnectionStrings.Entries.FirstOrDefault(e => - e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - - if (entry == null) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.Error.WriteLine($"Error: Connection '{name}' not found"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); return 1; } - var modified = false; + var configFileService = serviceProvider.GetRequiredService(); + var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - if (provider.HasValue) + if (!File.Exists(appSettingsPath)) { - 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) - { - Console.Error.WriteLine("Error: No changes specified. Use --help to see available options."); + logger.LogError("appsettings.json not found at {Path}", appSettingsPath); return 1; } - await configFileService.SaveAppSettingsAsync(appSettingsPath, config); - - if (!quiet) + try { - 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; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + if (entry == null) + { + logger.LogError("Connection '{Name}' not found", name); + 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; + } } } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/PipelineCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/PipelineCommands.cs index 5d016e6..892edc8 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/PipelineCommands.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/PipelineCommands.cs @@ -2,6 +2,7 @@ using System.CommandLine; using JdeScoping.ConfigManager.Core.Services; using JdeScoping.DataSync.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JdeScoping.ConfigManager.Cli.Commands; @@ -167,44 +168,51 @@ public static class PipelineCommands bool verbose, bool quiet) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("PipelineCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var pipelinesDir = Path.Combine(folderPath, "Pipelines"); - - try + ["Command"] = "pipeline list", + ["ConfigPath"] = configPath ?? "(default)" + })) { - var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir); - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.WriteLine($"=== Pipelines ({pipelines.Count}) ==="); - Console.WriteLine($"{"Name",-20} {"Enabled",-8} {"Mass",-8} {"Daily",-8} {"Hourly",-8}"); - Console.WriteLine(new string('-', 52)); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - foreach (var kvp in pipelines.OrderBy(p => p.Key)) + var configFileService = serviceProvider.GetRequiredService(); + var pipelinesDir = Path.Combine(folderPath, "Pipelines"); + + try { - 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"; + var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir); - 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, string name) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("PipelineCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json"); - - if (!File.Exists(pipelinePath)) + ["Command"] = "pipeline show", + ["ConfigPath"] = configPath ?? "(default)", + ["PipelineName"] = name + })) { - Console.Error.WriteLine($"Error: Pipeline '{name}' not found"); - return 1; - } - - try - { - var pipeline = await configFileService.LoadPipelineAsync(pipelinePath); - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.WriteLine($"=== Pipeline: {pipeline.Name} ==="); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - Console.WriteLine($"Name: {pipeline.Name}"); - Console.WriteLine($"Enabled: {pipeline.IsEnabled}"); - Console.WriteLine($"Manual Only: {pipeline.IsManualOnly}"); - Console.WriteLine(); + var configFileService = serviceProvider.GetRequiredService(); + var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json"); - Console.WriteLine("Sync Intervals:"); - Console.WriteLine($" Mass: {FormatInterval(pipeline.MassSyncIntervalMinutes)}"); - Console.WriteLine($" Daily: {FormatInterval(pipeline.DailySyncIntervalMinutes)}"); - Console.WriteLine($" Hourly: {FormatInterval(pipeline.HourlySyncIntervalMinutes)}"); - - if (pipeline.Source != null) + if (!File.Exists(pipelinePath)) { - Console.WriteLine(); - Console.WriteLine("Source:"); - 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)}"); + logger.LogError("Pipeline '{Name}' not found", name); + return 1; } - if (pipeline.Destination != null) + try { - Console.WriteLine(); - Console.WriteLine("Destination:"); - Console.WriteLine($" Table: {pipeline.Destination.Table}"); - if (pipeline.Destination.MatchColumns.Count > 0) - Console.WriteLine($" Match Columns: {string.Join(", ", pipeline.Destination.MatchColumns)}"); - } + var pipeline = await configFileService.LoadPipelineAsync(pipelinePath); - if (verbose) - { - if (pipeline.PreScripts.Count > 0) + logger.LogInformation("=== Pipeline: {Name} ===", pipeline.Name); + + 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(); - Console.WriteLine($"Pre-Scripts: {pipeline.PreScripts.Count}"); + logger.LogInformation(""); + 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(); - Console.WriteLine($"Transforms: {pipeline.Transforms.Count}"); + logger.LogInformation(""); + 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(); - Console.WriteLine($"Post-Scripts: {pipeline.PostScripts.Count}"); + if (pipeline.PreScripts.Count > 0) + { + 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 manualOnly) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) - { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("PipelineCommands"); - var configFileService = serviceProvider.GetRequiredService(); - var pipelinesDir = Path.Combine(folderPath, "Pipelines"); - var pipelinePath = Path.Combine(pipelinesDir, $"pipeline.{name}.json"); - - if (File.Exists(pipelinePath)) + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine($"Error: Pipeline '{name}' already exists"); - return 1; - } - - try + ["Command"] = "pipeline create", + ["ConfigPath"] = configPath ?? "(default)", + ["PipelineName"] = name + })) { - // Create Pipelines directory if it doesn't exist - if (!Directory.Exists(pipelinesDir)) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Directory.CreateDirectory(pipelinesDir); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - var pipeline = new EtlPipelineConfig - { - Name = name, - IsEnabled = enabled, - IsManualOnly = manualOnly, - Source = new SourceElement { Connection = "jde" }, - Destination = new DestinationElement { Table = name } - }; + var configFileService = serviceProvider.GetRequiredService(); + var pipelinesDir = Path.Combine(folderPath, "Pipelines"); + var pipelinePath = Path.Combine(pipelinesDir, $"pipeline.{name}.json"); - await configFileService.SavePipelineAsync(pipelinePath, pipeline); - - if (!quiet) + if (File.Exists(pipelinePath)) { - Console.WriteLine($"Pipeline '{name}' created successfully"); - Console.WriteLine($"File: {pipelinePath}"); + logger.LogError("Pipeline '{Name}' already exists", name); + return 1; } - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + try + { + // Create Pipelines directory if it doesn't exist + if (!Directory.Exists(pipelinesDir)) + { + 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, bool force) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) - { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("PipelineCommands"); - var configFileService = serviceProvider.GetRequiredService(); - var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json"); - - if (!File.Exists(pipelinePath)) + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine($"Error: Pipeline '{name}' not found"); - return 1; - } - - if (!force) + ["Command"] = "pipeline delete", + ["ConfigPath"] = configPath ?? "(default)", + ["PipelineName"] = name + })) { - Console.Write($"Delete pipeline '{name}'? [y/N] "); - var response = Console.ReadLine(); - if (!string.Equals(response, "y", StringComparison.OrdinalIgnoreCase) && - !string.Equals(response, "yes", StringComparison.OrdinalIgnoreCase)) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - if (!quiet) - Console.WriteLine("Cancelled"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; + } + + var configFileService = serviceProvider.GetRequiredService(); + 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; } - } - - try - { - await configFileService.DeletePipelineFileAsync(pipelinePath); - - if (!quiet) + catch (Exception ex) { - 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, bool enabled) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("PipelineCommands"); + + var action = enabled ? "enable" : "disable"; + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json"); - - if (!File.Exists(pipelinePath)) + ["Command"] = $"pipeline {action}", + ["ConfigPath"] = configPath ?? "(default)", + ["PipelineName"] = name + })) { - Console.Error.WriteLine($"Error: Pipeline '{name}' not found"); - return 1; - } - - try - { - var pipeline = await configFileService.LoadPipelineAsync(pipelinePath); - pipeline.IsEnabled = enabled; - await configFileService.SavePipelineAsync(pipelinePath, pipeline); - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - var action = enabled ? "enabled" : "disabled"; - Console.WriteLine($"Pipeline '{name}' {action} successfully"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + var configFileService = serviceProvider.GetRequiredService(); + var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json"); + + if (!File.Exists(pipelinePath)) + { + 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; + } } } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/SecretCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/SecretCommands.cs index a5ec5cf..a4c1b1d 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/SecretCommands.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/SecretCommands.cs @@ -2,6 +2,7 @@ using System.CommandLine; using JdeScoping.ConfigManager.Core.Services; using JdeScoping.ConfigManager.Core.Services.SecureStore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JdeScoping.ConfigManager.Cli.Commands; @@ -138,29 +139,36 @@ public static class SecretCommands bool verbose, bool quiet) { - var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath); - if (manager == null) - return 1; + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("SecretCommands"); - try + using (logger.BeginScope(new Dictionary { - var keys = manager.GetKeys(); - - if (!quiet) - { - Console.WriteLine($"=== SecureStore Keys ({keys.Count}) ==="); - } - - foreach (var key in keys.OrderBy(k => k)) - { - Console.WriteLine(key); - } - - return 0; - } - finally + ["Command"] = "secret list", + ["ConfigPath"] = configPath ?? "(default)" + })) { - 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, string key) { - var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath); - if (manager == null) - return 1; + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("SecretCommands"); - try + using (logger.BeginScope(new Dictionary { - var value = manager.GetSecret(key); - Console.WriteLine(value); - return 0; - } - catch (KeyNotFoundException) + ["Command"] = "secret get", + ["ConfigPath"] = configPath ?? "(default)", + ["SecretKey"] = key + })) { - Console.Error.WriteLine($"Error: Secret '{key}' not found"); - return 1; - } - finally - { - manager.CloseStore(); + var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath, logger); + if (manager == null) + return 1; + + try + { + 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 value) { - var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath); - if (manager == null) - return 1; + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("SecretCommands"); - try + using (logger.BeginScope(new Dictionary { - manager.SetSecret(key, value); - manager.Save(); + ["Command"] = "secret set", + ["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; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; - } - finally - { - manager.CloseStore(); + logger.LogInformation("Secret '{Key}' set successfully", key); + + return 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to set secret"); + return 1; + } + finally + { + manager.CloseStore(); + } } } @@ -234,35 +261,43 @@ public static class SecretCommands bool quiet, string key) { - var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath); - if (manager == null) - return 1; + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("SecretCommands"); - try + using (logger.BeginScope(new Dictionary { - manager.RemoveSecret(key); - manager.Save(); + ["Command"] = "secret remove", + ["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; - } - catch (KeyNotFoundException) - { - Console.Error.WriteLine($"Error: Secret '{key}' not found"); - return 1; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; - } - finally - { - manager.CloseStore(); + logger.LogInformation("Secret '{Key}' removed successfully", key); + + return 0; + } + catch (KeyNotFoundException) + { + logger.LogError("Secret '{Key}' not found", key); + return 1; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to remove secret"); + return 1; + } + finally + { + manager.CloseStore(); + } } } @@ -274,56 +309,64 @@ public static class SecretCommands string? storePath, string? keyPath) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("SecretCommands"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var effectiveStorePath = storePath ?? Path.Combine(folderPath, "data", "secrets.json"); - var effectiveKeyPath = keyPath ?? Path.Combine(folderPath, "data", "secrets.key"); - - if (File.Exists(effectiveStorePath)) + ["Command"] = "secret init", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.Error.WriteLine($"Error: Store already exists at {effectiveStorePath}"); - return 1; - } - - var manager = serviceProvider.GetRequiredService(); - - try - { - manager.CreateStore(effectiveStorePath, effectiveKeyPath); - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.WriteLine("SecureStore initialized successfully"); - Console.WriteLine($"Store: {effectiveStorePath}"); - Console.WriteLine($"Key: {effectiveKeyPath}"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error creating store: {ex.Message}"); - return 1; - } - finally - { - manager.CloseStore(); + var effectiveStorePath = storePath ?? Path.Combine(folderPath, "data", "secrets.json"); + var effectiveKeyPath = keyPath ?? Path.Combine(folderPath, "data", "secrets.key"); + + if (File.Exists(effectiveStorePath)) + { + logger.LogError("Store already exists at {StorePath}", effectiveStorePath); + return 1; + } + + var manager = serviceProvider.GetRequiredService(); + + 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( IServiceProvider serviceProvider, - string? configPath) + string? configPath, + ILogger logger) { var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); 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); } @@ -333,7 +376,7 @@ public static class SecretCommands 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); } @@ -351,14 +394,14 @@ public static class SecretCommands if (!File.Exists(storePath)) { - Console.Error.WriteLine($"Error: SecureStore not found at {storePath}"); - Console.Error.WriteLine("Use 'secret init' to create a new store."); + logger.LogError("SecureStore not found at {Path}", storePath); + logger.LogError("Use 'secret init' to create a new store"); return (null, null); } 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); } @@ -369,7 +412,7 @@ public static class SecretCommands } catch (Exception ex) { - Console.Error.WriteLine($"Error opening store: {ex.Message}"); + logger.LogError(ex, "Failed to open store"); return (null, null); } } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/TestConnectionCommand.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/TestConnectionCommand.cs index a564474..b54e890 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/TestConnectionCommand.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/TestConnectionCommand.cs @@ -2,6 +2,7 @@ using System.CommandLine; using JdeScoping.ConfigManager.Core.Models; using JdeScoping.ConfigManager.Core.Services; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JdeScoping.ConfigManager.Cli.Commands; @@ -90,68 +91,73 @@ public static class TestConnectionCommand string? connectionName, ConnectionProvider provider) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("TestConnectionCommand"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var connectionTestService = serviceProvider.GetRequiredService(); - - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - if (!File.Exists(appSettingsPath)) + ["Command"] = $"test-connection {provider.ToString().ToLower()}", + ["ConfigPath"] = configPath ?? "(default)", + ["ConnectionName"] = connectionName ?? "(auto)" + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - 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 folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - var message = connectionName != null - ? $"Connection '{connectionName}' not found" - : $"No {provider} connection found"; - Console.Error.WriteLine($"Error: {message}"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); return 1; } - if (!quiet) + var configFileService = serviceProvider.GetRequiredService(); + var connectionTestService = serviceProvider.GetRequiredService(); + + 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(); - var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider); - - if (result.Success) + try { - 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"); - if (result.Duration.HasValue) - Console.WriteLine($"Duration: {result.Duration.Value.TotalMilliseconds:F0}ms"); + var message = connectionName != null + ? $"Connection '{connectionName}' not found" + : $"No {provider} connection found"; + logger.LogError("{Message}", message); + return 1; } - return 0; - } - Console.Error.WriteLine($"Status: Failed"); - Console.Error.WriteLine($"Message: {result.Message}"); - return 1; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + logger.LogInformation("Testing connection: {Name}", entry.Name); + + var connectionString = entry.GenerateConnectionString(); + var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider); + + if (result.Success) + { + 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 quiet) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("TestConnectionCommand"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var connectionTestService = serviceProvider.GetRequiredService(); - - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - if (!File.Exists(appSettingsPath)) + ["Command"] = "test-connection all", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - var hasFailures = false; - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.WriteLine("=== Testing All Connections ==="); - Console.WriteLine($"Found {config.ConnectionStrings.Entries.Count} connection(s)"); - Console.WriteLine(); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - foreach (var entry in config.ConnectionStrings.Entries) + var configFileService = serviceProvider.GetRequiredService(); + var connectionTestService = serviceProvider.GetRequiredService(); + + var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); + if (!File.Exists(appSettingsPath)) { - if (entry.Provider == ConnectionProvider.Generic) - { - if (!quiet) - Console.WriteLine($"{entry.Name}: Skipped (generic provider)"); - continue; - } + logger.LogError("appsettings.json not found at {Path}", appSettingsPath); + return 1; + } - var connectionString = entry.GenerateConnectionString(); - var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider); + try + { + 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 ? $" ({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; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error: {ex.Message}"); - return 1; + return hasFailures ? 1 : 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to test connections"); + return 1; + } } } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ValidateCommand.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ValidateCommand.cs index 83247f9..1440438 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ValidateCommand.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ValidateCommand.cs @@ -1,6 +1,7 @@ using System.CommandLine; using JdeScoping.ConfigManager.Core.Services; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JdeScoping.ConfigManager.Cli.Commands; @@ -96,58 +97,63 @@ public static class ValidateCommand bool verbose, bool quiet) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ValidateCommand"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var validationService = serviceProvider.GetRequiredService(); - - var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); - if (!File.Exists(appSettingsPath)) + ["Command"] = "validate appsettings", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}"); - return 1; - } - - try - { - var config = await configFileService.LoadAppSettingsAsync(appSettingsPath); - var result = validationService.ValidateAppSettings(config); - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.WriteLine("=== AppSettings Validation ==="); - Console.WriteLine($"File: {appSettingsPath}"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - if (result.IsValid) + var configFileService = serviceProvider.GetRequiredService(); + var validationService = serviceProvider.GetRequiredService(); + + var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); + if (!File.Exists(appSettingsPath)) { - if (!quiet) - Console.WriteLine("Status: Valid"); - return 0; + logger.LogError("appsettings.json not found at {Path}", appSettingsPath); + return 1; } - 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) - Console.WriteLine($"Warning: {warning}"); + logger.LogError(ex, "Failed to load configuration"); + 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 quiet) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ValidateCommand"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var configFileService = serviceProvider.GetRequiredService(); - var validationService = serviceProvider.GetRequiredService(); - - var pipelinesDir = Path.Combine(folderPath, "Pipelines"); - - try + ["Command"] = "validate pipelines", + ["ConfigPath"] = configPath ?? "(default)" + })) { - var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir); - var result = validationService.ValidatePipelines(pipelines); - - if (!quiet) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - Console.WriteLine("=== Pipelines Validation ==="); - Console.WriteLine($"Directory: {pipelinesDir}"); - Console.WriteLine($"Pipelines found: {pipelines.Count}"); + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; } - if (result.IsValid) - { - if (!quiet) - Console.WriteLine("Status: Valid"); - return 0; - } + var configFileService = serviceProvider.GetRequiredService(); + var validationService = serviceProvider.GetRequiredService(); - foreach (var error in result.Errors) - { - Console.Error.WriteLine($"Error: {error}"); - } + var pipelinesDir = Path.Combine(folderPath, "Pipelines"); - foreach (var warning in result.Warnings) + try { - if (!quiet) - Console.WriteLine($"Warning: {warning}"); - } + var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir); + var result = validationService.ValidatePipelines(pipelines); - return result.Errors.Count > 0 ? 1 : 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error validating pipelines: {ex.Message}"); - return 1; + logger.LogInformation("=== Pipelines Validation ==="); + logger.LogInformation("Directory: {Path}", pipelinesDir); + logger.LogInformation("Pipelines found: {Count}", pipelines.Count); + + 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 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 quiet) { - var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); - if (folderPath == null) + var loggerFactory = serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("ValidateCommand"); + + using (logger.BeginScope(new Dictionary { - Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify."); - return 1; - } - - var runtimeValidationService = serviceProvider.GetRequiredService(); - - if (!quiet) + ["Command"] = "validate runtime", + ["ConfigPath"] = configPath ?? "(default)" + })) { - Console.WriteLine("=== Runtime Configuration Validation ==="); - Console.WriteLine($"Folder: {folderPath}"); - } - - var results = runtimeValidationService.ValidateRuntimeConfig(folderPath); - var hasErrors = false; - - foreach (var result in results) - { - if (result.Errors.Count > 0) + var folderPath = await GetConfigFolderAsync(serviceProvider, configPath); + if (folderPath == null) { - hasErrors = true; - Console.Error.WriteLine($"\n[{result.ValidatorName}]"); - foreach (var error in result.Errors) + logger.LogError("Could not find configuration folder. Use --config-path to specify"); + return 1; + } + + var runtimeValidationService = serviceProvider.GetRequiredService(); + + 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) - { - if (!quiet) - Console.WriteLine($" Warning: {warning}"); - } + return hasErrors ? 1 : 0; } - - return hasErrors ? 1 : 0; } private static async Task GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath) diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/JdeScoping.ConfigManager.Cli.csproj b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/JdeScoping.ConfigManager.Cli.csproj index a9759dd..a7ab85b 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/JdeScoping.ConfigManager.Cli.csproj +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/JdeScoping.ConfigManager.Cli.csproj @@ -10,7 +10,9 @@ - + + + diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs index c6dbbfe..10014e9 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs @@ -2,7 +2,8 @@ using System.CommandLine; using JdeScoping.ConfigManager.Cli.Commands; using JdeScoping.ConfigManager.Core.DependencyInjection; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; namespace JdeScoping.ConfigManager.Cli; @@ -18,8 +19,12 @@ public static class Program /// Exit code: 0 for success, non-zero for errors. public static async Task 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(); - ConfigureServices(services); + ConfigureServices(services, verbose, quiet); using var serviceProvider = services.BuildServiceProvider(); 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)); 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 - .AddConsole() - .SetMinimumLevel(LogLevel.Warning)); + // Log level mapping: + // --quiet: Warning level (hide success/info messages, only show warnings and errors) + // 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(); } } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/logging_style.md b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/logging_style.md new file mode 100644 index 0000000..ed2a2af --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/logging_style.md @@ -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 SomeCommandAsync( + IServiceProvider serviceProvider, + string? configPath, + bool verbose, + bool quiet) +{ + var loggerFactory = serviceProvider.GetRequiredService(); + 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 +{ + ["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 diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs index 5f08617..71cbac8 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs @@ -23,6 +23,7 @@ public class BackupCommandsTests var services = new ServiceCollection(); services.AddSingleton(_backupService); services.AddSingleton(_autoDiscoveryService); + services.AddTestLogging(); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); @@ -272,10 +273,10 @@ public class BackupCommandsTests rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); - // Capture console error output - var originalErr = Console.Error; + // Capture console output (Serilog writes all output to stdout) + var originalOut = Console.Out; using var writer = new StringWriter(); - Console.SetError(writer); + Console.SetOut(writer); try { @@ -288,7 +289,7 @@ public class BackupCommandsTests } finally { - Console.SetError(originalErr); + Console.SetOut(originalOut); } } finally diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs index fcc3d8a..e8d802f 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs @@ -27,6 +27,7 @@ public class ConfigCommandsTests services.AddSingleton(_configFileService); services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_backupService); + services.AddTestLogging(); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); @@ -366,10 +367,10 @@ public class ConfigCommandsTests rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); - // Capture console error output - var originalErr = Console.Error; + // Capture console output (Serilog writes all output to stdout) + var originalOut = Console.Out; using var writer = new StringWriter(); - Console.SetError(writer); + Console.SetOut(writer); try { @@ -382,7 +383,7 @@ public class ConfigCommandsTests } finally { - Console.SetError(originalErr); + Console.SetOut(originalOut); } } finally diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs index be7cd6b..e468074 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs @@ -24,6 +24,7 @@ public class ConnectionCommandsTests var services = new ServiceCollection(); services.AddSingleton(_configFileService); services.AddSingleton(_autoDiscoveryService); + services.AddTestLogging(); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); @@ -219,10 +220,10 @@ public class ConnectionCommandsTests rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); - // Capture console error output - var originalErr = Console.Error; + // Capture console output (Serilog writes all output to stdout) + var originalOut = Console.Out; using var writer = new StringWriter(); - Console.SetError(writer); + Console.SetOut(writer); try { @@ -235,7 +236,7 @@ public class ConnectionCommandsTests } finally { - Console.SetError(originalErr); + Console.SetOut(originalOut); } } finally @@ -331,10 +332,10 @@ public class ConnectionCommandsTests rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); - // Capture console error output - var originalErr = Console.Error; + // Capture console output (Serilog writes all output to stdout) + var originalOut = Console.Out; using var writer = new StringWriter(); - Console.SetError(writer); + Console.SetOut(writer); try { @@ -347,7 +348,7 @@ public class ConnectionCommandsTests } finally { - Console.SetError(originalErr); + Console.SetOut(originalOut); } } finally @@ -393,10 +394,10 @@ public class ConnectionCommandsTests rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); - // Capture console error output - var originalErr = Console.Error; + // Capture console output (Serilog writes all output to stdout) + var originalOut = Console.Out; using var writer = new StringWriter(); - Console.SetError(writer); + Console.SetOut(writer); try { @@ -409,7 +410,7 @@ public class ConnectionCommandsTests } finally { - Console.SetError(originalErr); + Console.SetOut(originalOut); } } finally @@ -478,10 +479,10 @@ public class ConnectionCommandsTests rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); - // Capture console error output - var originalErr = Console.Error; + // Capture console output (Serilog writes all output to stdout) + var originalOut = Console.Out; using var writer = new StringWriter(); - Console.SetError(writer); + Console.SetOut(writer); try { @@ -494,7 +495,7 @@ public class ConnectionCommandsTests } finally { - Console.SetError(originalErr); + Console.SetOut(originalOut); } } finally diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs index b815625..e56fd9f 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs @@ -24,6 +24,7 @@ public class PipelineCommandsTests var services = new ServiceCollection(); services.AddSingleton(_configFileService); services.AddSingleton(_autoDiscoveryService); + services.AddTestLogging(); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/SecretCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/SecretCommandsTests.cs index 964f0b2..ec094e2 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/SecretCommandsTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/SecretCommandsTests.cs @@ -28,6 +28,7 @@ public class SecretCommandsTests services.AddSingleton(_configFileService); services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_secureStoreManager); + services.AddTestLogging(); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); @@ -169,10 +170,10 @@ public class SecretCommandsTests rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); - // Capture console error output - var originalErr = Console.Error; + // Capture console output (Serilog writes all output to stdout) + var originalOut = Console.Out; using var writer = new StringWriter(); - Console.SetError(writer); + Console.SetOut(writer); try { @@ -185,7 +186,7 @@ public class SecretCommandsTests } finally { - Console.SetError(originalErr); + Console.SetOut(originalOut); } } finally diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/TestConnectionCommandTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/TestConnectionCommandTests.cs index e8cc6b9..f023868 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/TestConnectionCommandTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/TestConnectionCommandTests.cs @@ -27,6 +27,7 @@ public class TestConnectionCommandTests services.AddSingleton(_configFileService); services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_connectionTestService); + services.AddTestLogging(); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); @@ -243,10 +244,10 @@ public class TestConnectionCommandTests rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); - // Capture console error output - var originalErr = Console.Error; + // Capture console output (Serilog writes all output to stdout) + var originalOut = Console.Out; using var writer = new StringWriter(); - Console.SetError(writer); + Console.SetOut(writer); try { @@ -259,7 +260,7 @@ public class TestConnectionCommandTests } finally { - Console.SetError(originalErr); + Console.SetOut(originalOut); } } finally diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ValidateCommandTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ValidateCommandTests.cs index 2723940..2149a22 100644 --- a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ValidateCommandTests.cs +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ValidateCommandTests.cs @@ -31,6 +31,7 @@ public class ValidateCommandTests services.AddSingleton(_autoDiscoveryService); services.AddSingleton(_validationService); services.AddSingleton(_runtimeValidationService); + services.AddTestLogging(); _serviceProvider = services.BuildServiceProvider(); _configPathOption = new Option(["--config-path", "-c"]); @@ -193,10 +194,10 @@ public class ValidateCommandTests rootCommand.AddGlobalOption(_verboseOption); rootCommand.AddGlobalOption(_quietOption); - // Capture console error output - var originalErr = Console.Error; + // Capture console output (Serilog writes all output to stdout) + var originalOut = Console.Out; using var writer = new StringWriter(); - Console.SetError(writer); + Console.SetOut(writer); try { @@ -205,11 +206,11 @@ public class ValidateCommandTests // Assert var output = writer.ToString(); - output.ShouldContain("Error"); + output.ShouldContain("Connection string 'LocalCache' is required"); } finally { - Console.SetError(originalErr); + Console.SetOut(originalOut); } } finally diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/TestLoggingHelper.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/TestLoggingHelper.cs new file mode 100644 index 0000000..303165c --- /dev/null +++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/TestLoggingHelper.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using Serilog.Events; + +namespace JdeScoping.ConfigManager.Cli.Tests; + +/// +/// Helper for setting up logging in CLI tests. +/// +public static class TestLoggingHelper +{ + /// + /// Adds Serilog logging to a service collection configured to write to Console. + /// This allows tests to capture log output via Console.SetOut. + /// + 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; + } +}