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
diff --git a/src/ConsoleExample/ConsoleExample.csproj b/src/ConsoleExample/ConsoleExample.csproj
index ab6870d..9cd59a8 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..0ff8586 100644
--- a/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj
+++ b/src/MvcExample.Infrastructure/MvcExample.Infrastructure.csproj
@@ -8,12 +8,13 @@
-
+
+
-
-
+
+
diff --git a/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs b/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs
index e3abbe9..510565c 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
{
@@ -15,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()
{
@@ -36,8 +39,17 @@ 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);
+ }
- _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..4dc139e 100644
--- a/src/MvcExample/Controllers/WeatherController.cs
+++ b/src/MvcExample/Controllers/WeatherController.cs
@@ -1,12 +1,12 @@
using System;
using System.Configuration;
-using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Mvc;
using MvcExample.Core;
using MvcExample.Infrastructure;
+using NLog;
namespace MvcExample.Controllers
{
@@ -14,6 +14,8 @@ public class WeatherController : Controller
{
private static readonly HttpClient HttpClient = new HttpClient();
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
[HttpGet]
public async Task Index()
{
@@ -24,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());
}
@@ -44,6 +46,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());
@@ -56,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));
}
@@ -92,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();
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..d6bc197
--- /dev/null
+++ b/src/MvcExample/nlog.config
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file