Skip to content

Commit f9ed9b7

Browse files
committed
Implement structured logging with Serilog (#9)
- Add Serilog packages and configuration - Implement --verbose and --json command-line arguments - Add LoggingEvents class with predefined EventIds - Create SerilogConfiguration for dynamic logging setup - Add StructuredLogger wrapper for consistent logging - Support both human-readable and JSON output formats - Add file logging with daily rotation - Integrate with existing DiscordHostedService logging - Add environment variable support for automated testing Fixes #9
1 parent 0f6393f commit f9ed9b7

File tree

8 files changed

+608
-14
lines changed

8 files changed

+608
-14
lines changed

DiscordArchitect/DiscordArchitect.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.9" />
1818
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.9" />
1919
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.9" />
20+
<PackageReference Include="Serilog" Version="4.2.0" />
21+
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
22+
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
23+
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
24+
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
25+
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
2026
</ItemGroup>
2127

2228
<ItemGroup>

DiscordArchitect/Hosting/DiscordHostedService.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public async Task StartAsync(CancellationToken ct)
7777
if (_opt.TestMode)
7878
{
7979
_log.LogInformation("🧪 Running in TEST MODE - resources will be tracked for cleanup");
80+
_log.LogInformation("🔍 Debug: TestMode={TestMode}, AutoCleanup={AutoCleanup}", _opt.TestMode, _opt.AutoCleanup);
8081
var createdResources = await _cloner.CloneWithTrackingAsync(server, _opt.SourceCategoryName, newCategoryName, _opt);
8182

8283
if (createdResources != null)
@@ -94,11 +95,40 @@ public async Task StartAsync(CancellationToken ct)
9495

9596
Console.WriteLine();
9697
Console.WriteLine("🔍 Please verify the created resources in Discord, then press ENTER to continue...");
97-
Console.ReadLine();
98+
99+
// Check if running in automated mode
100+
var isAutomated = _opt.AutoCleanup ||
101+
Console.IsInputRedirected ||
102+
Environment.GetEnvironmentVariable("DISCORD_ARCHITECT_AUTO") == "true" ||
103+
_opt.TestMode && Environment.GetEnvironmentVariable("CI") == "true";
104+
105+
_log.LogInformation("🔍 Debug: AutoCleanup={AutoCleanup}, IsInputRedirected={IsInputRedirected}, DISCORD_ARCHITECT_AUTO={DiscordAuto}, CI={CI}, isAutomated={IsAutomated}",
106+
_opt.AutoCleanup, Console.IsInputRedirected, Environment.GetEnvironmentVariable("DISCORD_ARCHITECT_AUTO"), Environment.GetEnvironmentVariable("CI"), isAutomated);
107+
108+
if (isAutomated)
109+
{
110+
_log.LogInformation("🤖 Automated mode detected - proceeding with automatic cleanup");
111+
await Task.Delay(2000); // Give a moment to see the logs
112+
}
113+
else
114+
{
115+
_log.LogInformation("👤 Manual mode - waiting for user input");
116+
Console.ReadLine();
117+
}
98118

99119
Console.WriteLine();
100120
Console.Write("🗑️ Do you want to delete the created resources? (y/n): ");
101-
var response = Console.ReadLine();
121+
122+
string response;
123+
if (isAutomated)
124+
{
125+
response = "y"; // Automatically delete in automated mode
126+
_log.LogInformation("🤖 Automated mode - automatically deleting resources");
127+
}
128+
else
129+
{
130+
response = Console.ReadLine() ?? "n";
131+
}
102132

103133
if (response?.ToLowerInvariant().StartsWith("y") == true)
104134
{
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace DiscordArchitect.Logging;
4+
5+
/// <summary>
6+
/// Defines structured logging events with consistent event IDs for the DiscordArchitect application.
7+
/// </summary>
8+
public static class LoggingEvents
9+
{
10+
// Application lifecycle events (1000-1099)
11+
public static readonly EventId ApplicationStarting = new(1001, "ApplicationStarting");
12+
public static readonly EventId ApplicationStarted = new(1002, "ApplicationStarted");
13+
public static readonly EventId ApplicationStopping = new(1003, "ApplicationStopping");
14+
public static readonly EventId ApplicationStopped = new(1004, "ApplicationStopped");
15+
16+
// Configuration events (1100-1199)
17+
public static readonly EventId ConfigurationValidationStarted = new(1101, "ConfigurationValidationStarted");
18+
public static readonly EventId ConfigurationValidationPassed = new(1102, "ConfigurationValidationPassed");
19+
public static readonly EventId ConfigurationValidationFailed = new(1103, "ConfigurationValidationFailed");
20+
public static readonly EventId ConfigurationLoaded = new(1104, "ConfigurationLoaded");
21+
22+
// Discord connection events (1200-1299)
23+
public static readonly EventId DiscordConnecting = new(1201, "DiscordConnecting");
24+
public static readonly EventId DiscordConnected = new(1202, "DiscordConnected");
25+
public static readonly EventId DiscordDisconnected = new(1203, "DiscordDisconnected");
26+
public static readonly EventId DiscordReady = new(1204, "DiscordReady");
27+
public static readonly EventId DiscordLoginFailed = new(1205, "DiscordLoginFailed");
28+
29+
// Guild operations (1300-1399)
30+
public static readonly EventId GuildFound = new(1301, "GuildFound");
31+
public static readonly EventId GuildNotFound = new(1302, "GuildNotFound");
32+
public static readonly EventId GuildPermissionsChecked = new(1303, "GuildPermissionsChecked");
33+
34+
// Category operations (1400-1499)
35+
public static readonly EventId CategorySearchStarted = new(1401, "CategorySearchStarted");
36+
public static readonly EventId CategoryFound = new(1402, "CategoryFound");
37+
public static readonly EventId CategoryNotFound = new(1403, "CategoryNotFound");
38+
public static readonly EventId CategoryCreated = new(1404, "CategoryCreated");
39+
public static readonly EventId CategoryDeleted = new(1405, "CategoryDeleted");
40+
41+
// Channel operations (1500-1599)
42+
public static readonly EventId ChannelCreated = new(1501, "ChannelCreated");
43+
public static readonly EventId ChannelDeleted = new(1501, "ChannelDeleted");
44+
public static readonly EventId ChannelSynced = new(1503, "ChannelSynced");
45+
public static readonly EventId ChannelPermissionsApplied = new(1504, "ChannelPermissionsApplied");
46+
47+
// Role operations (1600-1699)
48+
public static readonly EventId RoleCreated = new(1601, "RoleCreated");
49+
public static readonly EventId RoleDeleted = new(1602, "RoleDeleted");
50+
public static readonly EventId RolePermissionsApplied = new(1603, "RolePermissionsApplied");
51+
52+
// Forum operations (1700-1799)
53+
public static readonly EventId ForumTagsPatching = new(1701, "ForumTagsPatching");
54+
public static readonly EventId ForumTagsPatched = new(1702, "ForumTagsPatched");
55+
public static readonly EventId ForumTagsPatchFailed = new(1703, "ForumTagsPatchFailed");
56+
57+
// Test mode operations (1800-1899)
58+
public static readonly EventId TestModeStarted = new(1801, "TestModeStarted");
59+
public static readonly EventId TestModeCompleted = new(1802, "TestModeCompleted");
60+
public static readonly EventId TestModeCleanupStarted = new(1803, "TestModeCleanupStarted");
61+
public static readonly EventId TestModeCleanupCompleted = new(1804, "TestModeCleanupCompleted");
62+
63+
// Verification operations (1900-1999)
64+
public static readonly EventId VerificationStarted = new(1901, "VerificationStarted");
65+
public static readonly EventId VerificationCompleted = new(1902, "VerificationCompleted");
66+
public static readonly EventId VerificationFinding = new(1903, "VerificationFinding");
67+
public static readonly EventId VerificationSummary = new(1904, "VerificationSummary");
68+
69+
// Error events (2000-2099)
70+
public static readonly EventId UnexpectedError = new(2001, "UnexpectedError");
71+
public static readonly EventId ValidationError = new(2002, "ValidationError");
72+
public static readonly EventId DiscordApiError = new(2003, "DiscordApiError");
73+
public static readonly EventId NetworkError = new(2004, "NetworkError");
74+
75+
// Warning events (2100-2199)
76+
public static readonly EventId PermissionWarning = new(2101, "PermissionWarning");
77+
public static readonly EventId ConfigurationWarning = new(2102, "ConfigurationWarning");
78+
public static readonly EventId ResourceWarning = new(2103, "ResourceWarning");
79+
80+
// Info events (2200-2299)
81+
public static readonly EventId OperationInfo = new(2201, "OperationInfo");
82+
public static readonly EventId DiagnosticInfo = new(2202, "DiagnosticInfo");
83+
public static readonly EventId PerformanceInfo = new(2203, "PerformanceInfo");
84+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using Microsoft.Extensions.Configuration;
2+
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.Options;
4+
using Serilog;
5+
using Serilog.Events;
6+
using Serilog.Formatting.Json;
7+
using Serilog.Configuration;
8+
using DiscordArchitect.Options;
9+
10+
namespace DiscordArchitect.Logging;
11+
12+
/// <summary>
13+
/// Provides Serilog configuration for structured logging with support for verbose and JSON output modes.
14+
/// </summary>
15+
public static class SerilogConfiguration
16+
{
17+
/// <summary>
18+
/// Configures Serilog based on the provided options.
19+
/// </summary>
20+
/// <param name="options">Discord options containing logging preferences.</param>
21+
/// <param name="configuration">Application configuration.</param>
22+
/// <returns>Configured Serilog logger.</returns>
23+
public static ILogger CreateLogger(DiscordOptions options, IConfiguration configuration)
24+
{
25+
var loggerConfig = new LoggerConfiguration()
26+
.Enrich.FromLogContext()
27+
.Enrich.WithEnvironmentName()
28+
.Enrich.WithMachineName()
29+
.Enrich.WithThreadId()
30+
.Enrich.WithProperty("Application", "DiscordArchitect")
31+
.Enrich.WithProperty("Version", "1.0.0");
32+
33+
// Set minimum level based on verbose mode
34+
var minimumLevel = options.Verbose ? LogEventLevel.Debug : LogEventLevel.Information;
35+
loggerConfig.MinimumLevel.Is(minimumLevel);
36+
37+
// Configure console output
38+
if (options.JsonOutput)
39+
{
40+
// JSON output for structured logging
41+
loggerConfig.WriteTo.Console(
42+
new JsonFormatter(renderMessage: true),
43+
restrictedToMinimumLevel: minimumLevel);
44+
}
45+
else
46+
{
47+
// Human-readable output with emojis and colors
48+
loggerConfig.WriteTo.Console(
49+
outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
50+
restrictedToMinimumLevel: minimumLevel);
51+
}
52+
53+
// Add file logging
54+
loggerConfig.WriteTo.File(
55+
path: "logs/discord-architect-.log",
56+
rollingInterval: RollingInterval.Day,
57+
retainedFileCountLimit: 7,
58+
outputTemplate: options.JsonOutput
59+
? "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
60+
: "{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
61+
restrictedToMinimumLevel: LogEventLevel.Information);
62+
63+
return loggerConfig.CreateLogger();
64+
}
65+
66+
/// <summary>
67+
/// Configures Serilog for the host builder.
68+
/// </summary>
69+
/// <param name="hostBuilder">The host builder to configure.</param>
70+
/// <returns>Configured host builder.</returns>
71+
public static IHostBuilder UseSerilog(this IHostBuilder hostBuilder)
72+
{
73+
return hostBuilder.UseSerilog((context, services, configuration) =>
74+
{
75+
configuration
76+
.Enrich.FromLogContext()
77+
.Enrich.WithEnvironmentName()
78+
.Enrich.WithMachineName()
79+
.Enrich.WithThreadId()
80+
.Enrich.WithProperty("Application", "DiscordArchitect")
81+
.Enrich.WithProperty("Version", "1.0.0");
82+
83+
// Set minimum level
84+
configuration.MinimumLevel.Is(LogEventLevel.Information);
85+
86+
// Configure console output
87+
configuration.WriteTo.Console(
88+
outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
89+
restrictedToMinimumLevel: LogEventLevel.Information);
90+
91+
// Add file logging
92+
configuration.WriteTo.File(
93+
path: "logs/discord-architect-.log",
94+
rollingInterval: RollingInterval.Day,
95+
retainedFileCountLimit: 7,
96+
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
97+
restrictedToMinimumLevel: LogEventLevel.Information);
98+
});
99+
}
100+
}

0 commit comments

Comments
 (0)