From 27cb8f6c833094890761e026cd296e920a6ca92a Mon Sep 17 00:00:00 2001 From: dgates82 <48529034+dgates82@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:29:07 +0100 Subject: [PATCH 1/6] feat: add logging to ConsoleExample via custom sync ILogger provider - add SyncConsoleLoggerProvider/SyncConsoleLogger in Logging/, writing synchronously to Console.Out to avoid Microsoft.Extensions.Logging.Console's async queue, which caused log output to interleave out of order with Program's own narration - wire logger into SecretsManagerServiceFactory.Create via ILoggerFactory.SetMinimumLevel(Debug), demonstrating a fully custom, non-Microsoft ILogger consumed by the library - restructure demo sequence to insert a fetch between InvalidateCache and RefreshSecretAsync, so each step's log output actually proves its narration (invalidate clearing the cache, refresh bypassing a warm cache) rather than asserting it - remove hardcoded "[cache hit]" console message, now redundant with the library's real Debug-level cache-hit log --- src/ConsoleExample/ConsoleExample.csproj | 9 +- .../Logging/SyncConsoleLoggerProvider.cs | 62 +++++++++++++ src/ConsoleExample/Program.cs | 88 +++++++++++-------- .../MvcExample.Infrastructure.csproj | 6 +- 4 files changed, 121 insertions(+), 44 deletions(-) create mode 100644 src/ConsoleExample/Logging/SyncConsoleLoggerProvider.cs diff --git a/src/ConsoleExample/ConsoleExample.csproj b/src/ConsoleExample/ConsoleExample.csproj index ab6870d..a0aae9c 100644 --- a/src/ConsoleExample/ConsoleExample.csproj +++ b/src/ConsoleExample/ConsoleExample.csproj @@ -8,10 +8,6 @@ disable - - - - @@ -25,4 +21,9 @@ + + + + + diff --git a/src/ConsoleExample/Logging/SyncConsoleLoggerProvider.cs b/src/ConsoleExample/Logging/SyncConsoleLoggerProvider.cs new file mode 100644 index 0000000..8b488bc --- /dev/null +++ b/src/ConsoleExample/Logging/SyncConsoleLoggerProvider.cs @@ -0,0 +1,62 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace ConsoleExample.Logging +{ + /// + /// Provides instances that write log entries to the console + /// synchronously on the calling thread, rather than through the built-in console logger's + /// background queue. Guarantees log output is flushed before a short-lived console app like + /// ConsoleExample exits, at the cost of blocking the caller on each write. + /// + public class SyncConsoleLoggerProvider : ILoggerProvider + { + /// Creates a logger for the given category name. + public ILogger CreateLogger(string categoryName) => + new SyncConsoleLogger(categoryName); + + /// No unmanaged resources to release; present only to satisfy . + public void Dispose() { } + + /// + /// Writes each log entry directly to when it's + /// logged, instead of queuing it for a background writer. + /// + private sealed class SyncConsoleLogger : ILogger + { + + private readonly string _categoryName; + + public SyncConsoleLogger(string categoryName) + { + _categoryName = categoryName; + } + + /// Always enabled — this demo logger does not filter by level. + public bool IsEnabled(LogLevel logLevel) => true; + + /// Scopes aren't supported; always returns null. + public IDisposable BeginScope(TState state) => null; + + /// + /// Formats the log entry as [Level] Category: Message (plus the exception + /// details on a following line, if one was supplied) and writes it to the console + /// immediately, blocking the calling thread until the write completes. + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) return; + + var message = formatter(state, exception); + var logEntry = $"[{logLevel}] {_categoryName}: {message}"; + if (exception != null) + { + logEntry += Environment.NewLine + exception; + } + + Console.WriteLine(logEntry); + } + + } + } +} \ No newline at end of file diff --git a/src/ConsoleExample/Program.cs b/src/ConsoleExample/Program.cs index ca2cde2..32daf41 100644 --- a/src/ConsoleExample/Program.cs +++ b/src/ConsoleExample/Program.cs @@ -1,8 +1,10 @@ using System; using System.Configuration; using System.Threading.Tasks; +using ConsoleExample.Logging; using ConsoleExample.Models; using DGates.AwsSecretsManager; +using Microsoft.Extensions.Logging; namespace ConsoleExample { @@ -10,44 +12,56 @@ internal static class Program { private static async Task Main() { - var settings = new SecretsManagerSettings + using (var loggerFactory = + LoggerFactory.Create(builder => + builder.SetMinimumLevel(LogLevel.Debug) + .AddProvider(new SyncConsoleLoggerProvider()))) { - ServiceUrl = ConfigurationManager.AppSettings["AWSServiceURL"], - Region = ConfigurationManager.AppSettings["AWSRegion"], - LocalJsonFallbackPath = ConfigurationManager.AppSettings["LocalJsonFallbackPath"] - }; - - var secretName = ConfigurationManager.AppSettings["Secrets:DbConfigName"]; - var service = SecretsManagerServiceFactory.Create(settings); - - // 1. Initial fetch — result is cached after this call - Console.WriteLine("Fetching secret (initial)..."); - var config = await service.GetSecretAsync(secretName); - PrintConfig(config); - - // 2. Same secret as raw JSON string - Console.WriteLine("\nFetching secret as raw string..."); - var raw = await service.GetSecretStringAsync(secretName); - Console.WriteLine($" Raw: {raw}"); - - // 3. Evict from cache - Console.WriteLine("\nInvalidating cache..."); - service.InvalidateCache(secretName); - Console.WriteLine(" Cache invalidated."); - - // 4. Force fresh fetch from Secrets Manager, repopulates cache - Console.WriteLine("\nRefreshing secret..."); - config = await service.RefreshSecretAsync(secretName); - PrintConfig(config); - - // 5. Fetch again — served from cache repopulated by RefreshSecretAsync - Console.WriteLine("\nFetching secret (cache hit)..."); - config = await service.GetSecretAsync(secretName); - Console.WriteLine("[cache hit] Secret served from in-memory cache."); - PrintConfig(config); - - Console.WriteLine("\nDone. Press any key to exit."); - Console.ReadKey(); + var logger = loggerFactory.CreateLogger(); + + var settings = new SecretsManagerSettings + { + ServiceUrl = ConfigurationManager.AppSettings["AWSServiceURL"], + Region = ConfigurationManager.AppSettings["AWSRegion"], + LocalJsonFallbackPath = ConfigurationManager.AppSettings["LocalJsonFallbackPath"] + }; + + var secretName = ConfigurationManager.AppSettings["Secrets:DbConfigName"]; + var service = SecretsManagerServiceFactory.Create(settings, logger); + + // 1. Initial fetch — result is cached after this call + Console.WriteLine("Fetching secret (initial)..."); + var config = await service.GetSecretAsync(secretName); + PrintConfig(config); + + // 2. Same secret as raw JSON string + Console.WriteLine("\nFetching secret as raw string..."); + var raw = await service.GetSecretStringAsync(secretName); + Console.WriteLine($" Raw: {raw}"); + + // 3. Evict from cache + Console.WriteLine("\nInvalidating cache..."); + service.InvalidateCache(secretName); + Console.WriteLine(" Cache invalidated."); + + // 4. Fetch - this will be a cache miss and will fetch from Secrets Manager again + Console.WriteLine("\nFetching secret (after cache invalidation)..."); + config = await service.GetSecretAsync(secretName); + PrintConfig(config); + + // 5. Force fresh fetch from Secrets Manager, repopulates cache + Console.WriteLine("\nRefreshing secret..."); + config = await service.RefreshSecretAsync(secretName); + PrintConfig(config); + + // 6. Fetch again — served from cache repopulated by RefreshSecretAsync + Console.WriteLine("\nFetching secret (cache hit)..."); + config = await service.GetSecretAsync(secretName); + PrintConfig(config); + + Console.WriteLine("\nDone. Press any key to exit."); + Console.ReadKey(); + } } private static void PrintConfig(DbConfig config) diff --git a/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj b/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj index 87f36ea..0e2cc8e 100644 --- a/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj +++ b/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj @@ -8,12 +8,12 @@ - + + - - + From 3a64dde8f6a9c4ec647a7dd99c91902e98b5e77a Mon Sep 17 00:00:00 2001 From: dgates82 <48529034+dgates82@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:25:29 +0100 Subject: [PATCH 2/6] feat: wire NLog into MvcExample via ILogger bridge for library logging - add NLog.Extensions.Logging to MvcExample.Infrastructure; bridge NLogLoggerProvider -> ILogger in SecretsManagerAccessor.Initialize(), passed into SecretsManagerServiceFactory.Create - add nlog.config (File + Debugger targets, minlevel=Debug) - add one Log.Info call in WeatherController.Index(cityName) as a smoke test for the wiring --- .../MvcExample.Infrastructure.csproj | 1 + .../SecretsManagerAccessor.cs | 6 ++++- .../Controllers/WeatherController.cs | 5 ++++ src/MvcExample/MvcExample.csproj | 11 ++++++++ src/MvcExample/nlog.config | 26 +++++++++++++++++++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/MvcExample/nlog.config diff --git a/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj b/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj index 0e2cc8e..14efc4a 100644 --- a/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj +++ b/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj @@ -14,6 +14,7 @@ + diff --git a/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs b/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs index e3abbe9..2358671 100644 --- a/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs +++ b/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Web.Hosting; using DGates.AwsSecretsManager; +using NLog.Extensions.Logging; namespace MvcExample.Infrastructure { @@ -37,7 +38,10 @@ public static void Initialize() ? "LocalStack (via ServiceUrl override)" : "AWS Secrets Manager"; - _instance = SecretsManagerServiceFactory.Create(settings); + var loggerProvider = new NLogLoggerProvider(); + var logger = loggerProvider.CreateLogger(nameof(SecretsManagerService)); + + _instance = SecretsManagerServiceFactory.Create(settings, logger); } public static async Task> GetSecretAsync(string secretName) where T : class diff --git a/src/MvcExample/Controllers/WeatherController.cs b/src/MvcExample/Controllers/WeatherController.cs index 2b14d31..a9a5d92 100644 --- a/src/MvcExample/Controllers/WeatherController.cs +++ b/src/MvcExample/Controllers/WeatherController.cs @@ -7,6 +7,7 @@ using System.Web.Mvc; using MvcExample.Core; using MvcExample.Infrastructure; +using NLog; namespace MvcExample.Controllers { @@ -14,6 +15,8 @@ public class WeatherController : Controller { private static readonly HttpClient HttpClient = new HttpClient(); + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + [HttpGet] public async Task Index() { @@ -44,6 +47,8 @@ public async Task Index() [HttpPost] public async Task Index(string cityName) { + Log.Info("Received request for weather data for city: {CityName}", cityName); + if (string.IsNullOrWhiteSpace(cityName)) { return View(WeatherViewModelBuilder.MissingCityName()); diff --git a/src/MvcExample/MvcExample.csproj b/src/MvcExample/MvcExample.csproj index bb8acff..dbaaca8 100644 --- a/src/MvcExample/MvcExample.csproj +++ b/src/MvcExample/MvcExample.csproj @@ -68,6 +68,8 @@ + + @@ -130,6 +132,12 @@ $(PkgMicrosoft_CodeDom_Providers_DotNetCompilerPlatform)\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + $(PkgNLog)\lib\net46\NLog.dll + + + $(PkgNLog_Web)\lib\net46\NLog.Web.dll + @@ -184,6 +192,9 @@ + + Always + diff --git a/src/MvcExample/nlog.config b/src/MvcExample/nlog.config new file mode 100644 index 0000000..72a47d5 --- /dev/null +++ b/src/MvcExample/nlog.config @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + \ No newline at end of file From 360ebb77d1a350595b64756eb8a14aebdf5f32a1 Mon Sep 17 00:00:00 2001 From: dgates82 <48529034+dgates82@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:49:45 +0100 Subject: [PATCH 3/6] fix: bump DGates.AwsSecretsManager to 1.0.0-beta.3, resolve mixed Microsoft.Extensions.* versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Library now floors Microsoft.Extensions.Logging.Abstractions at 8.0.0 instead of hard-requiring 10.0.10, so it no longer forces a version split against NLog.Extensions.Logging's net48-compatible dependency set (which only offers the 8.x generation) - Confirmed via the built assemblies: every Microsoft.Extensions.* DLL and System.Diagnostics.DiagnosticSource now resolve to a single consistent 8.0.0.0 — no binding redirects needed for this cluster - Add to nlog.config so NLog registers NLog.Web's layout renderers (${aspnet-appbasepath}) instead of failing to parse the config --- src/ConsoleExample/ConsoleExample.csproj | 2 +- .../MvcExample.Infrastructure.csproj | 2 +- src/MvcExample/nlog.config | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ConsoleExample/ConsoleExample.csproj b/src/ConsoleExample/ConsoleExample.csproj index a0aae9c..9cd59a8 100644 --- a/src/ConsoleExample/ConsoleExample.csproj +++ b/src/ConsoleExample/ConsoleExample.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj b/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj index 14efc4a..0ff8586 100644 --- a/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj +++ b/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/MvcExample/nlog.config b/src/MvcExample/nlog.config index 72a47d5..d6bc197 100644 --- a/src/MvcExample/nlog.config +++ b/src/MvcExample/nlog.config @@ -5,14 +5,17 @@ throwConfigExceptions="true" internalLogLevel="Off"> + + + + + maxArchiveFiles="5" /> Date: Thu, 16 Jul 2026 17:56:32 +0100 Subject: [PATCH 4/6] refactor: convert Trace calls to NLog in WeatherController/Global.asax - Replace Trace.TraceError with Log.Error(ex, "...") using NLog's exception-first pattern and structured placeholders, consistent with the existing Log.Info call in WeatherController - Wire up a Logger in Global.asax.cs for the Application_Start init-failure case --- src/MvcExample/Controllers/WeatherController.cs | 11 +++++------ src/MvcExample/Global.asax.cs | 6 ++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/MvcExample/Controllers/WeatherController.cs b/src/MvcExample/Controllers/WeatherController.cs index a9a5d92..4dc139e 100644 --- a/src/MvcExample/Controllers/WeatherController.cs +++ b/src/MvcExample/Controllers/WeatherController.cs @@ -1,6 +1,5 @@ using System; using System.Configuration; -using System.Diagnostics; using System.IO; using System.Net.Http; using System.Threading.Tasks; @@ -27,12 +26,12 @@ public async Task Index() } catch (FileNotFoundException ex) { - Trace.TraceError("LocalJsonFallbackPath file not found on page load: " + ex); + Log.Error(ex, "LocalJsonFallbackPath file not found on page load"); return View(WeatherViewModelBuilder.LocalFallbackFileNotFound()); } catch (Exception ex) { - Trace.TraceError("Failed to retrieve OpenWeatherMap secret on page load: " + ex); + Log.Error(ex, "Failed to retrieve OpenWeatherMap secret on page load"); return View(WeatherViewModelBuilder.BackendUnavailable()); } @@ -61,12 +60,12 @@ public async Task Index(string cityName) } catch (FileNotFoundException ex) { - Trace.TraceError("LocalJsonFallbackPath file not found: " + ex); + Log.Error(ex, "LocalJsonFallbackPath file not found"); return View(WeatherViewModelBuilder.LocalFallbackFileNotFound()); } catch (Exception ex) { - Trace.TraceError("Failed to retrieve OpenWeatherMap secret: " + ex); + Log.Error(ex, "Failed to retrieve OpenWeatherMap secret"); return View(WeatherViewModelBuilder.Error(cityName)); } @@ -97,7 +96,7 @@ public async Task Index(string cityName) } catch (Exception ex) { - Trace.TraceError("Failed to retrieve weather data for '" + cityName + "': " + ex); + Log.Error(ex, "Failed to retrieve weather data for '{CityName}'", cityName); return View(WeatherViewModelBuilder.Error(cityName)); } } diff --git a/src/MvcExample/Global.asax.cs b/src/MvcExample/Global.asax.cs index 9b7591c..fc3d426 100644 --- a/src/MvcExample/Global.asax.cs +++ b/src/MvcExample/Global.asax.cs @@ -1,17 +1,19 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; using MvcExample.Infrastructure; +using NLog; namespace MvcExample { public class MvcApplication : System.Web.HttpApplication { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + protected void Application_Start() { try @@ -24,7 +26,7 @@ protected void Application_Start() // requests that actually need the secrets service will fail individually // (SecretsManagerAccessor throws a clear "not initialized" error) instead of // every request 500ing until this is fixed and the app pool recycled. - Trace.TraceError("Failed to initialize SecretsManagerAccessor: " + ex); + Log.Error(ex, "Failed to initialize SecretsManagerAccessor"); } AreaRegistration.RegisterAllAreas(); From 8cebb9219c35932161ba5232150994206b75a4e1 Mon Sep 17 00:00:00 2001 From: dgates82 <48529034+dgates82@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:05:17 +0100 Subject: [PATCH 5/6] Update CHANGELOG.md --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abbad1f..a35929b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to this repository will be documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- +## [Unreleased] +### Added +- Logging demonstration in both example apps, showing `DGates.AwsSecretsManager`'s `ILogger` + integration works with arbitrary logging frameworks: + - ConsoleExample — custom synchronous `ILoggerProvider`/`ILogger` (`Logging/SyncConsoleLoggerProvider.cs`), + writing directly to `Console.Out` to keep log output correctly interleaved with the demo's + step-by-step narration (`Microsoft.Extensions.Logging.Console`'s async queue caused + out-of-order output when mixed with synchronous console writes) + - MvcExample — NLog, bridged into the library via `NLogLoggerProvider` in + `SecretsManagerAccessor.Initialize()`, with `WeatherController` and `Global.asax.cs` also + converted to NLog directly (`LogManager.GetCurrentClassLogger()`), replacing + `System.Diagnostics.Trace` — one unified logging pipeline for both app and library output + - MvcExample logs to `App_Data/logs/mvcexample.log` and the VS debugger output via + `nlog.config`, `minlevel="Debug"` by default + +### Changed +- ConsoleExample's demo sequence restructured to insert a fetch between `InvalidateCache` and + `RefreshSecretAsync`, so each step's log output proves its narration (invalidate actually + clearing the cache, refresh bypassing a warm cache) rather than asserting it +- Removed ConsoleExample's hardcoded `"[cache hit]"` console message, now redundant with the + library's real `Debug`-level cache-hit log +- `DGates.AwsSecretsManager` dependency bumped to `1.0.0-beta.3` +--- ## [0.2.0] - 2026-07-09 ### Added - MvcExample (ASP.NET MVC 5, .NET Framework 4.8) — demo app retrieving an OpenWeatherMap API From 374e308591e40f594a4befd2b8a4f097444036fa Mon Sep 17 00:00:00 2001 From: dgates82 <48529034+dgates82@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:07:04 +0100 Subject: [PATCH 6/6] feat: log backend source and resolved fallback path in SecretsManagerAccessor.Initialize() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add a dedicated NLog.Logger to SecretsManagerAccessor (GetCurrentClassLogger, same pattern as WeatherController/Global.asax.cs), distinct from the ILogger bridge passed into SecretsManagerServiceFactory.Create, which is scoped to SecretsManagerService specifically - log the resolved backend source (AWS/LocalStack/local fallback) and, when set, the resolved absolute LocalJsonFallbackPath, before the service is constructed — surfaces the accessor's own startup decisions in the log rather than only becoming visible via the "how this worked" panel on first request - resolved path logging specifically targets the IIS working-directory fallback-path bug already documented in ResolveLocalJsonFallbackPath --- src/MvcExample.Infrastructure/SecretsManagerAccessor.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs b/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs index 2358671..510565c 100644 --- a/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs +++ b/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs @@ -16,6 +16,8 @@ public static class SecretsManagerAccessor { private static ISecretsManagerService _instance; private static string _source; + + private static readonly NLog.Logger Log = NLog.LogManager.GetCurrentClassLogger(); public static void Initialize() { @@ -37,6 +39,12 @@ public static void Initialize() : !string.IsNullOrWhiteSpace(serviceUrl) ? "LocalStack (via ServiceUrl override)" : "AWS Secrets Manager"; + + Log.Info("SecretsManagerAccessor initializing, backend source: {Source}", _source); + if (!string.IsNullOrWhiteSpace(localJsonFallbackPath)) + { + Log.Info("Resolved LocalJsonFallbackPath: {ResolvedPath}", localJsonFallbackPath); + } var loggerProvider = new NLogLoggerProvider(); var logger = loggerProvider.CreateLogger(nameof(SecretsManagerService));