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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- `Environments`: Comma-separated list of environments where logging is active
- `LogRequests`: Write one line per HTTP request with its duration, status and a `Route` property holding the route template (`/api/orders/{id}`), so requests can be grouped per endpoint, plus `User` (the caller's name identifier, when signed in) and `UserAgent` (cut to 200 characters), so requests can be counted per caller. Kept even when `LoggingLevel` is above Information. Default `true`.
- `SlowQueryMilliseconds`: EF Core commands taking at least this long are logged ("Executed DbCommand (Nms)"), so a slow request can be traced to its query; faster ones are not logged. Failed commands always are. `0` disables. Default `500`.
- `Properties`: added to every log line, such as which deployment wrote it. Set in code (`o.Properties["Deployment"] = "acme"`) or under `SwLogger:Properties`; configuration wins for the same name. `Environment`, `Application` and `ApplicationVersion` keep the logger's values, and an empty value is left out.
- `QuietRequestPaths`: Comma-separated path prefixes whose successful requests are dropped (default `/health,/healthz,/metrics`). Failures are still logged.

> **Upgrading:** before this change `LoggingLevel` defaulted to 1, which is Debug, not Information as this README claimed. Apps that never set it now log at Information, and framework chatter from ASP.NET Core and EF Core is Warning-only.
Expand Down
6 changes: 6 additions & 0 deletions SW.Logger.Console/IServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ namespace SW.Logger.Console;

public static class IServiceCollectionExtensions
{
static readonly HashSet<string> ReservedProperties = new(StringComparer.OrdinalIgnoreCase)
{ "Environment", "Application", "ApplicationVersion" };

public static IServiceCollection AddSWConsoleLogger(this IServiceCollection services,
Action<LoggerOptions> configure = null)
{
Expand Down Expand Up @@ -45,6 +48,9 @@ public static IServiceCollection AddSWConsoleLogger(this IServiceCollection serv
.Enrich.WithProperty("Environment", hostEnvironment.EnvironmentName)
.Enrich.WithProperty("ApplicationVersion", loggerOptions.ApplicationVersion)
.Enrich.WithProperty("Application", loggerOptions.ApplicationName);
foreach (var (name, value) in loggerOptions.Properties)
if (!string.IsNullOrEmpty(value) && !ReservedProperties.Contains(name))
loggerConfiguration = loggerConfiguration.Enrich.WithProperty(name, value);

loggerConfiguration = Debugger.IsAttached
? loggerConfiguration.WriteTo.Console()
Expand Down
7 changes: 7 additions & 0 deletions SW.Logger.Console/LoggerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,11 @@ public LoggerOptions()
/// volume down. 0 or less disables slow-query logging. Default 500.
/// </summary>
public int SlowQueryMilliseconds { get; set; }

/// <summary>
/// Properties added to every log line, such as which deployment wrote it. Set in code, or under
/// <c>SwLogger:Properties</c> in configuration (configuration wins for the same name). A name the logger sets
/// itself (Environment, Application, ApplicationVersion) keeps its value, and an empty value is left out.
/// </summary>
public IDictionary<string, string> Properties { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
22 changes: 21 additions & 1 deletion SW.Logger.UnitTests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ private static async Task<string> Run(int loggingLevel, params string[] paths) =
private static async Task<string> Run(int loggingLevel, Action<ILoggerFactory> act, params string[] paths) =>
await Run(loggingLevel, act, null, paths);

private static Task<string> Run(int loggingLevel, Action<ILoggerFactory> act,
Action<HttpClient> configureClient, params string[] paths) =>
Run(loggingLevel, act, configureClient, null, paths);

private static async Task<string> Run(int loggingLevel, Action<ILoggerFactory> act,
Action<HttpClient> configureClient, params string[] paths)
Action<HttpClient> configureClient, Action<LoggerOptions> configureLogger, params string[] paths)
{
var original = System.Console.Out;
var captured = new StringWriter();
Expand All @@ -33,6 +37,7 @@ private static async Task<string> Run(int loggingLevel, Action<ILoggerFactory> a
{
o.ApplicationName = "test";
o.LoggingLevel = loggingLevel;
configureLogger?.Invoke(o);
});
var app = builder.Build();
// Stands in for authentication: a request with an X-Test-User header is signed in as that user.
Expand Down Expand Up @@ -61,6 +66,21 @@ private static async Task<string> Run(int loggingLevel, Action<ILoggerFactory> a
return captured.ToString();
}

[Fact]
public async Task Every_line_carries_the_configured_properties()
{
var output = await Run(3, null, null, o =>
{
o.Properties["Deployment"] = "acme";
o.Properties["Application"] = "not-the-app";
o.Properties["Empty"] = "";
}, "/api/orders/1");
Assert.Contains("\"Deployment\":\"acme\"", output);
Assert.Contains("\"Application\":\"test\"", output);
Assert.DoesNotContain("not-the-app", output);
Assert.DoesNotContain("\"Empty\"", output);
}

[Theory]
[InlineData(2)]
[InlineData(3)]
Expand Down
23 changes: 23 additions & 0 deletions SW.Logger.UnitTests/LoggerOptionsBindingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.Extensions.Configuration;
using SW.Logger.Console;
using Xunit;

namespace SW.Logger.UnitTests;

public class LoggerOptionsBindingTests
{
[Fact]
public void Properties_bind_from_configuration_over_code()
{
var options = new LoggerOptions();
options.Properties["Deployment"] = "from-code";
options.Properties["Region"] = "eu";
new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { ["SwLogger:Properties:Deployment"] = "acme" })
.Build()
.GetSection(LoggerOptions.ConfigurationSection).Bind(options);

Assert.Equal("acme", options.Properties["Deployment"]);
Assert.Equal("eu", options.Properties["Region"]);
}
}
Loading