Skip to content

Commit eef0b2e

Browse files
authored
Merge pull request #24 from tgiachi/feature/aspnetcore-serilog
feat(aspnetcore): unified Serilog logging via AddSquidStdSerilog
2 parents 09908ff + 9e16d24 commit eef0b2e

9 files changed

Lines changed: 283 additions & 1 deletion

File tree

docs/tutorials/aspnetcore-app.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ Build the app, expose the health checks with the standard `MapHealthChecks`, add
3434

3535
[!code-csharp[](../../samples/SquidStd.Samples.AspNetCore/Program.cs#step-3)]
3636

37+
## Unified Serilog logging
38+
39+
Add `AddSquidStdSerilog()` to send ASP.NET Core framework logs (Kestrel, `Microsoft.Hosting.Lifetime`)
40+
through the same Serilog logger SquidStd configures from `squidstd.yaml`, giving a single format and a
41+
single configuration source:
42+
43+
```csharp
44+
builder.UseSquidStd(options => options.ConfigName = "squidstd");
45+
builder.AddSquidStdSerilog();
46+
builder.AddSquidStdHealthChecks();
47+
```
48+
49+
Without this call the two loggers stay separate (two console formats). With it, `squidstd.yaml` drives
50+
all logging.
51+
3752
## Run it
3853

3954
```bash
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using DryIoc;
2+
using Microsoft.AspNetCore.Builder;
3+
using Serilog;
4+
using SquidStd.Core.Interfaces.Bootstrap;
5+
6+
namespace SquidStd.AspNetCore.Extensions;
7+
8+
/// <summary>
9+
/// Extension methods that route ASP.NET Core and SquidStd logging through a single Serilog pipeline.
10+
/// </summary>
11+
public static class SquidStdSerilogExtensions
12+
{
13+
/// <param name="builder">ASP.NET Core application builder.</param>
14+
extension(WebApplicationBuilder builder)
15+
{
16+
/// <summary>
17+
/// Configures SquidStd's Serilog logger eagerly (from the SquidStd YAML configuration) and routes
18+
/// ASP.NET Core framework logging through it, so framework and SquidStd logs share one config and
19+
/// one format. Must be called after <c>UseSquidStd</c> and before <c>Build</c>.
20+
/// </summary>
21+
/// <returns>The same builder for chaining.</returns>
22+
public WebApplicationBuilder AddSquidStdSerilog()
23+
{
24+
ArgumentNullException.ThrowIfNull(builder);
25+
26+
if (!builder.Host.Properties.TryGetValue(
27+
SquidStdAspNetCoreBuilderExtensions.ContainerPropertyKey,
28+
out var value) || value is not IContainer container)
29+
{
30+
throw new InvalidOperationException("AddSquidStdSerilog must be called after UseSquidStd.");
31+
}
32+
33+
var bootstrap = container.Resolve<ISquidStdBootstrap>();
34+
bootstrap.ConfigureLogging();
35+
36+
builder.Host.UseSerilog(Log.Logger, dispose: false);
37+
38+
return builder;
39+
}
40+
}
41+
}

src/SquidStd.AspNetCore/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,30 @@ Each registered `IHealthCheck` appears as its own entry in the report. Check nam
5151
| `SquidStdHostedService` | Hosted service bridging SquidStd service lifecycle to the host. |
5252
| `SquidStdHealthChecksExtensions` | `AddSquidStdHealthChecks(...)` — bridge to ASP.NET Core health checks. |
5353

54+
## Unified logging (opt-in)
55+
56+
By default the SquidStd Serilog logger (configured from the `logger` section of `squidstd.yaml`) and the
57+
ASP.NET Core framework logger run as two separate pipelines, producing two console formats. Call
58+
`AddSquidStdSerilog()` after `UseSquidStd()` to route framework logging through SquidStd's Serilog logger,
59+
so everything shares one configuration and one format:
60+
61+
```csharp
62+
builder.UseSquidStd(options => options.ConfigName = "squidstd");
63+
builder.AddSquidStdSerilog();
64+
```
65+
66+
The logger is driven entirely by `squidstd.yaml` (keys are PascalCase and case-sensitive):
67+
68+
```yaml
69+
logger:
70+
MinimumLevel: Information # None disables all logging, framework included
71+
EnableConsole: true
72+
EnableFile: false
73+
LogDirectory: logs
74+
FileName: squidstd-.log
75+
RollingInterval: Day
76+
```
77+
5478
## Related
5579
5680
- Tutorial: [Build an ASP.NET Core app](https://tgiachi.github.io/squid-std/tutorials/aspnetcore-app.html)

src/SquidStd.AspNetCore/SquidStd.AspNetCore.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
<ItemGroup>
1515
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0"/>
16+
<PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0"/>
1617
</ItemGroup>
1718

1819
<ItemGroup>

src/SquidStd.Core/Interfaces/Bootstrap/ISquidStdBootstrap.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ public interface ISquidStdBootstrap : IAsyncDisposable
3939
/// <returns>The resolved service instance.</returns>
4040
TService Resolve<TService>();
4141

42+
/// <summary>
43+
/// Loads configuration and configures the Serilog logger immediately, if not already configured.
44+
/// Idempotent: subsequent calls are no-ops. Lets ASP.NET hosts wire Serilog before the host starts.
45+
/// </summary>
46+
void ConfigureLogging();
47+
4248
/// <summary>
4349
/// Starts services, waits until cancellation, and then stops services.
4450
/// </summary>

src/SquidStd.Services.Core/Services/Bootstrap/SquidStdBootstrap.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ public TService Resolve<TService>()
120120
return Container.Resolve<TService>();
121121
}
122122

123+
/// <inheritdoc />
124+
public void ConfigureLogging()
125+
{
126+
ThrowIfDisposed();
127+
128+
if (_loggerConfigured)
129+
{
130+
return;
131+
}
132+
133+
Container.Resolve<IConfigManagerService>().Load();
134+
ConfigureLogger();
135+
}
136+
123137
/// <inheritdoc />
124138
public async Task RunAsync(CancellationToken cancellationToken = default)
125139
{
@@ -224,7 +238,7 @@ public static SquidStdBootstrap Create(SquidStdOptions options, IContainer conta
224238

225239
private void ConfigureLogger()
226240
{
227-
if (!Container.IsRegistered<SquidStdLoggerOptions>())
241+
if (_loggerConfigured || !Container.IsRegistered<SquidStdLoggerOptions>())
228242
{
229243
return;
230244
}

tests/SquidStd.Tests/AspNetCore/FakeSquidStdBootstrap.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ internal sealed class FakeSquidStdBootstrap : ISquidStdBootstrap
1010

1111
public int StopCount { get; private set; }
1212

13+
public int ConfigureLoggingCount { get; private set; }
14+
1315
public SquidStdOptions Options { get; } = new();
1416

1517
public IContainer Container { get; } = new Container();
@@ -37,6 +39,9 @@ public ValueTask DisposeAsync()
3739
public TService Resolve<TService>()
3840
=> Container.Resolve<TService>();
3941

42+
public void ConfigureLogging()
43+
=> ConfigureLoggingCount++;
44+
4045
public async Task RunAsync(CancellationToken cancellationToken = default)
4146
=> await StartAsync(cancellationToken);
4247

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.AspNetCore.TestHost;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
using Serilog.Extensions.Logging;
7+
using SquidStd.AspNetCore.Extensions;
8+
using SquidStd.Tests.Support;
9+
using SerilogLog = Serilog.Log;
10+
11+
namespace SquidStd.Tests.AspNetCore;
12+
13+
[Collection(SerilogEventSinkCollection.Name)]
14+
public class SquidStdSerilogExtensionsTests
15+
{
16+
[Fact]
17+
public async Task AddSquidStdSerilog_RoutesFrameworkLoggingThroughSerilog()
18+
{
19+
using var temp = new TempDirectory();
20+
var builder = CreateBuilder(temp.Path);
21+
builder.UseSquidStd(options => options.ConfigName = "app");
22+
builder.AddSquidStdSerilog();
23+
24+
await using var app = builder.Build();
25+
26+
var factory = app.Services.GetRequiredService<ILoggerFactory>();
27+
Assert.IsType<SerilogLoggerFactory>(factory);
28+
}
29+
30+
[Fact]
31+
public async Task UseSquidStd_WithoutAddSquidStdSerilog_KeepsDefaultLoggerFactory()
32+
{
33+
using var temp = new TempDirectory();
34+
var builder = CreateBuilder(temp.Path);
35+
builder.UseSquidStd(options => options.ConfigName = "app");
36+
37+
await using var app = builder.Build();
38+
39+
var factory = app.Services.GetRequiredService<ILoggerFactory>();
40+
Assert.IsNotType<SerilogLoggerFactory>(factory);
41+
}
42+
43+
[Fact]
44+
public void AddSquidStdSerilog_WithoutUseSquidStd_Throws()
45+
{
46+
using var temp = new TempDirectory();
47+
var builder = CreateBuilder(temp.Path);
48+
49+
var ex = Assert.Throws<InvalidOperationException>(() => builder.AddSquidStdSerilog());
50+
Assert.Equal("AddSquidStdSerilog must be called after UseSquidStd.", ex.Message);
51+
}
52+
53+
[Fact]
54+
public async Task AddSquidStdSerilog_RoutesFrameworkLogsToSquidStdFileSink()
55+
{
56+
using var temp = new TempDirectory();
57+
var logDir = Path.Combine(temp.Path, "logs");
58+
File.WriteAllText(
59+
Path.Combine(temp.Path, "app.yaml"),
60+
"logger:\n MinimumLevel: Information\n EnableConsole: false\n EnableFile: true\n LogDirectory: logs\n FileName: app-.log\n RollingInterval: Day\n");
61+
62+
const string marker = "framework-log-marker-a1b2c3";
63+
64+
var builder = CreateBuilder(temp.Path);
65+
builder.UseSquidStd(options => options.ConfigName = "app");
66+
builder.AddSquidStdSerilog();
67+
68+
await using (var app = builder.Build())
69+
{
70+
await app.StartAsync();
71+
var logger = app.Services.GetRequiredService<ILogger<SquidStdSerilogExtensionsTests>>();
72+
#pragma warning disable CA1848, CA1873 // LoggerMessage delegates are overkill for a one-shot test log.
73+
logger.LogInformation("emitting {Marker}", marker);
74+
#pragma warning restore CA1848, CA1873
75+
await app.StopAsync();
76+
}
77+
78+
await SerilogLog.CloseAndFlushAsync();
79+
80+
var logFile = Directory.EnumerateFiles(logDir, "app-*.log").First();
81+
var contents = await File.ReadAllTextAsync(logFile);
82+
Assert.Contains(marker, contents, StringComparison.Ordinal);
83+
}
84+
85+
[Fact]
86+
public async Task AddSquidStdSerilog_CalledTwice_DoesNotThrow()
87+
{
88+
using var temp = new TempDirectory();
89+
var builder = CreateBuilder(temp.Path);
90+
builder.UseSquidStd(options => options.ConfigName = "app");
91+
92+
builder.AddSquidStdSerilog();
93+
builder.AddSquidStdSerilog();
94+
95+
await using var app = builder.Build();
96+
var factory = app.Services.GetRequiredService<ILoggerFactory>();
97+
Assert.IsType<SerilogLoggerFactory>(factory);
98+
}
99+
100+
private static WebApplicationBuilder CreateBuilder(string contentRootPath)
101+
{
102+
var builder = WebApplication.CreateBuilder(
103+
new WebApplicationOptions
104+
{
105+
ContentRootPath = contentRootPath,
106+
EnvironmentName = Environments.Development
107+
}
108+
);
109+
110+
builder.WebHost.UseTestServer();
111+
112+
return builder;
113+
}
114+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using DryIoc;
2+
using SquidStd.Core.Data.Bootstrap;
3+
using SquidStd.Services.Core.Services.Bootstrap;
4+
using SquidStd.Tests.Support;
5+
using SerilogILogger = Serilog.ILogger;
6+
7+
namespace SquidStd.Tests.Bootstrap;
8+
9+
[Collection(SerilogEventSinkCollection.Name)]
10+
public class SquidStdBootstrapLoggingTests
11+
{
12+
[Fact]
13+
public async Task ConfigureLogging_LoadsConfigurationAndRegistersLoggerEagerly()
14+
{
15+
using var temp = new TempDirectory();
16+
await using var bootstrap =
17+
SquidStdBootstrap.Create(new() { ConfigName = "app", RootDirectory = temp.Path });
18+
19+
bootstrap.ConfigureLogging();
20+
21+
Assert.NotNull(bootstrap.Container.Resolve<SquidStdLoggerOptions>());
22+
Assert.NotNull(bootstrap.Container.Resolve<SerilogILogger>());
23+
}
24+
25+
[Fact]
26+
public async Task ConfigureLogging_CalledTwice_ConfiguresLoggerOnce()
27+
{
28+
using var temp = new TempDirectory();
29+
await using var bootstrap =
30+
SquidStdBootstrap.Create(new() { ConfigName = "app", RootDirectory = temp.Path });
31+
32+
bootstrap.ConfigureLogging();
33+
var first = bootstrap.Container.Resolve<SerilogILogger>();
34+
35+
bootstrap.ConfigureLogging();
36+
var second = bootstrap.Container.Resolve<SerilogILogger>();
37+
38+
Assert.Same(first, second);
39+
}
40+
41+
[Fact]
42+
public async Task ConfigureLogging_ThenStartAsync_DoesNotRebuildLogger()
43+
{
44+
using var temp = new TempDirectory();
45+
await using var bootstrap =
46+
SquidStdBootstrap.Create(new() { ConfigName = "app", RootDirectory = temp.Path });
47+
48+
bootstrap.ConfigureLogging();
49+
var eager = bootstrap.Container.Resolve<SerilogILogger>();
50+
51+
await bootstrap.StartAsync();
52+
try
53+
{
54+
var afterStart = bootstrap.Container.Resolve<SerilogILogger>();
55+
Assert.Same(eager, afterStart);
56+
}
57+
finally
58+
{
59+
await bootstrap.StopAsync();
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)