Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/ConsoleExample/ConsoleExample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
<Nullable>disable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DGates.AwsSecretsManager" Version="0.1.0-beta.1" />
</ItemGroup>

<ItemGroup>
<Reference Include="System.Configuration" />
</ItemGroup>
Expand All @@ -25,4 +21,9 @@
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="DGates.AwsSecretsManager" Version="1.0.0-beta.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.10" />
</ItemGroup>

</Project>
62 changes: 62 additions & 0 deletions src/ConsoleExample/Logging/SyncConsoleLoggerProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using Microsoft.Extensions.Logging;

namespace ConsoleExample.Logging
{
/// <summary>
/// Provides <see cref="SyncConsoleLogger"/> 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.
/// </summary>
public class SyncConsoleLoggerProvider : ILoggerProvider
{
/// <summary>Creates a logger for the given category name.</summary>
public ILogger CreateLogger(string categoryName) =>
new SyncConsoleLogger(categoryName);

/// <summary>No unmanaged resources to release; present only to satisfy <see cref="ILoggerProvider"/>.</summary>
public void Dispose() { }

/// <summary>
/// Writes each log entry directly to <see cref="Console.WriteLine(string)"/> when it's
/// logged, instead of queuing it for a background writer.
/// </summary>
private sealed class SyncConsoleLogger : ILogger
{

private readonly string _categoryName;

public SyncConsoleLogger(string categoryName)
{
_categoryName = categoryName;
}

/// <summary>Always enabled — this demo logger does not filter by level.</summary>
public bool IsEnabled(LogLevel logLevel) => true;

/// <summary>Scopes aren't supported; always returns <c>null</c>.</summary>
public IDisposable BeginScope<TState>(TState state) => null;

/// <summary>
/// Formats the log entry as <c>[Level] Category: Message</c> (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.
/// </summary>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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);
}

}
}
}
88 changes: 51 additions & 37 deletions src/ConsoleExample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,53 +1,67 @@
using System;
using System.Configuration;
using System.Threading.Tasks;
using ConsoleExample.Logging;
using ConsoleExample.Models;
using DGates.AwsSecretsManager;
using Microsoft.Extensions.Logging;

namespace ConsoleExample
{
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<DbConfig>(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<DbConfig>(secretName);
PrintConfig(config);

// 5. Fetch again — served from cache repopulated by RefreshSecretAsync
Console.WriteLine("\nFetching secret (cache hit)...");
config = await service.GetSecretAsync<DbConfig>(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<SecretsManagerService>();

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<DbConfig>(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<DbConfig>(secretName);
PrintConfig(config);

// 5. Force fresh fetch from Secrets Manager, repopulates cache
Console.WriteLine("\nRefreshing secret...");
config = await service.RefreshSecretAsync<DbConfig>(secretName);
PrintConfig(config);

// 6. Fetch again — served from cache repopulated by RefreshSecretAsync
Console.WriteLine("\nFetching secret (cache hit)...");
config = await service.GetSecretAsync<DbConfig>(secretName);
PrintConfig(config);

Console.WriteLine("\nDone. Press any key to exit.");
Console.ReadKey();
}
}

private static void PrintConfig(DbConfig config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DGates.AwsSecretsManager" Version="0.1.0-beta.1" />
<Reference Include="System.Configuration" />
<Reference Include="System.Web" />
</ItemGroup>

<ItemGroup>
<Reference Include="System.Configuration" />
<Reference Include="System.Web" />
<PackageReference Include="DGates.AwsSecretsManager" Version="1.0.0-beta.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="6.1.4" />
</ItemGroup>

</Project>
14 changes: 13 additions & 1 deletion src/MvcExample.Infrastructure/SecretsManagerAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using System.Web.Hosting;
using DGates.AwsSecretsManager;
using NLog.Extensions.Logging;

namespace MvcExample.Infrastructure
{
Expand All @@ -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()
{
Expand All @@ -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<SecretFetchResult<T>> GetSecretAsync<T>(string secretName) where T : class
Expand Down
16 changes: 10 additions & 6 deletions src/MvcExample/Controllers/WeatherController.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
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
{
public class WeatherController : Controller
{
private static readonly HttpClient HttpClient = new HttpClient();

private static readonly Logger Log = LogManager.GetCurrentClassLogger();

[HttpGet]
public async Task<ActionResult> Index()
{
Expand All @@ -24,12 +26,12 @@ public async Task<ActionResult> 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());
}

Expand All @@ -44,6 +46,8 @@ public async Task<ActionResult> Index()
[HttpPost]
public async Task<ActionResult> Index(string cityName)
{
Log.Info("Received request for weather data for city: {CityName}", cityName);

if (string.IsNullOrWhiteSpace(cityName))
{
return View(WeatherViewModelBuilder.MissingCityName());
Expand All @@ -56,12 +60,12 @@ public async Task<ActionResult> 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));
}

Expand Down Expand Up @@ -92,7 +96,7 @@ public async Task<ActionResult> 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));
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/MvcExample/Global.asax.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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();
Expand Down
Loading
Loading