Skip to content

Commit 3b50856

Browse files
authored
Merge pull request #2021 from DuendeSoftware/beh/diagnostic-logging-setup
Diagnostic Summary Setup
2 parents 3007d9d + 38d193e commit 3b50856

File tree

8 files changed

+206
-5
lines changed

8 files changed

+206
-5
lines changed

identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
using Duende.IdentityServer.Internal;
1818
using Duende.IdentityServer.Licensing;
1919
using Duende.IdentityServer.Licensing.V2;
20+
using Duende.IdentityServer.Licensing.V2.Diagnostics;
21+
using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
2022
using Duende.IdentityServer.Logging;
2123
using Duende.IdentityServer.Models;
2224
using Duende.IdentityServer.ResponseHandling;
@@ -210,6 +212,10 @@ public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder
210212
builder.Services.AddSingleton<LicenseUsageTracker>();
211213
builder.Services.AddSingleton<LicenseExpirationChecker>();
212214

215+
builder.Services.AddSingleton<IDiagnosticEntry, AssemblyInfoDiagnosticEntry>();
216+
builder.Services.AddSingleton<DiagnosticSummary>();
217+
builder.Services.AddHostedService<DiagnosticHostedService>();
218+
213219
return builder;
214220
}
215221

@@ -306,7 +312,7 @@ public static IIdentityServerBuilder AddDynamicProvidersCore(this IIdentityServe
306312
builder.Services.AddTransientDecorator<IAuthenticationSchemeProvider, DynamicAuthenticationSchemeProvider>();
307313
builder.Services.TryAddSingleton<IIdentityProviderStore, NopIdentityProviderStore>();
308314
// the per-request cache is to ensure that a scheme loaded from the cache is still available later in the
309-
// request and made available anywhere else during this request (in case the static cache times out across
315+
// request and made available anywhere else during this request (in case the static cache times out across
310316
// 2 calls within the same request)
311317
builder.Services.AddScoped<DynamicAuthenticationSchemeCache>();
312318

identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/IdentityServerOptions.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,11 @@ public class IdentityServerOptions
207207
/// <summary>
208208
/// The allowed clock skew for JWT lifetime validation. Except for DPoP proofs,
209209
/// all JWTs that have their lifetime validated use this setting to control the
210-
/// clock skew of lifetime validation. This includes JWT access tokens passed
211-
/// to the user info, introspection, and local api endpoints, client
210+
/// clock skew of lifetime validation. This includes JWT access tokens passed
211+
/// to the user info, introspection, and local api endpoints, client
212212
/// authentication JWTs used in private_key_jwt authentication, JWT secured
213-
/// authorization requests (JAR), and custom usage of the
214-
/// <see cref="TokenValidator"/>, such as in a token exchange implementation.
213+
/// authorization requests (JAR), and custom usage of the
214+
/// <see cref="TokenValidator"/>, such as in a token exchange implementation.
215215
/// Defaults to five minutes.
216216
/// </summary>
217217
public TimeSpan JwtValidationClockSkew { get; set; } = TimeSpan.FromMinutes(5);
@@ -236,4 +236,10 @@ public class IdentityServerOptions
236236
/// Options for configuring preview features in the server.
237237
/// </value>
238238
public PreviewFeatureOptions Preview { get; set; } = new PreviewFeatureOptions();
239+
240+
/// <summary>
241+
/// Frequency at which the diagnostic summary is logged.
242+
/// The default value is 1 hour.
243+
/// </summary>
244+
public TimeSpan DiagnosticSummaryLogFrequency { get; set; } = TimeSpan.FromHours(1);
239245
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using System.Reflection;
5+
using System.Runtime.Loader;
6+
using System.Text.Json;
7+
8+
namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
9+
10+
internal class AssemblyInfoDiagnosticEntry : IDiagnosticEntry
11+
{
12+
public Task WriteAsync(Utf8JsonWriter writer)
13+
{
14+
var assemblies = GetAssemblyInfo();
15+
writer.WriteStartObject("AssemblyInfo");
16+
writer.WriteNumber("AssemblyCount", assemblies.Count);
17+
18+
writer.WriteStartArray("Assemblies");
19+
foreach (var assembly in assemblies)
20+
{
21+
writer.WriteStartObject();
22+
writer.WriteString("Name", assembly.GetName().Name);
23+
writer.WriteString("Version", assembly.GetName().Version?.ToString() ?? "Unknown");
24+
writer.WriteEndObject();
25+
}
26+
27+
writer.WriteEndArray();
28+
writer.WriteEndObject();
29+
30+
return Task.CompletedTask;
31+
}
32+
33+
private List<Assembly> GetAssemblyInfo()
34+
{
35+
var assemblies = AssemblyLoadContext.Default.Assemblies
36+
.OrderBy(a => a.FullName)
37+
.ToList();
38+
39+
return assemblies;
40+
}
41+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using Duende.IdentityServer.Configuration;
5+
using Microsoft.Extensions.Hosting;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Options;
8+
9+
namespace Duende.IdentityServer.Licensing.V2.Diagnostics;
10+
11+
internal class DiagnosticHostedService(DiagnosticSummary diagnosticSummary, IOptions<IdentityServerOptions> options, ILogger<DiagnosticHostedService> logger) : BackgroundService
12+
{
13+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
14+
{
15+
using var timer = new PeriodicTimer(options.Value.DiagnosticSummaryLogFrequency);
16+
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
17+
{
18+
try
19+
{
20+
await diagnosticSummary.PrintSummary();
21+
}
22+
catch (Exception ex)
23+
{
24+
logger.LogError(ex, "An error occurred while logging the diagnostic summary: {Message}", ex.Message);
25+
}
26+
}
27+
}
28+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using System.Buffers;
5+
using System.Text;
6+
using System.Text.Json;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace Duende.IdentityServer.Licensing.V2.Diagnostics;
10+
11+
internal class DiagnosticSummary(IEnumerable<IDiagnosticEntry> entries, ILogger<DiagnosticSummary> logger)
12+
{
13+
public async Task PrintSummary()
14+
{
15+
var bufferWriter = new ArrayBufferWriter<byte>();
16+
await using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = false });
17+
18+
writer.WriteStartObject();
19+
20+
foreach (var diagnosticEntry in entries)
21+
{
22+
await diagnosticEntry.WriteAsync(writer);
23+
}
24+
25+
writer.WriteEndObject();
26+
27+
await writer.FlushAsync();
28+
29+
logger.LogInformation("{Message}", Encoding.UTF8.GetString(bufferWriter.WrittenSpan));
30+
}
31+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using System.Text.Json;
5+
6+
namespace Duende.IdentityServer.Licensing.V2.Diagnostics;
7+
8+
internal interface IDiagnosticEntry
9+
{
10+
Task WriteAsync(Utf8JsonWriter writer);
11+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using System.Buffers;
5+
using System.Text;
6+
using System.Text.Json;
7+
using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
8+
9+
namespace IdentityServer.UnitTests.Licensing.V2.DiagnosticEntries;
10+
11+
public class AssemblyInfoDiagnosticEntryTests
12+
{
13+
[Fact]
14+
public async Task WriteAsync_ShouldWriteAssemblyInfo()
15+
{
16+
var subject = new AssemblyInfoDiagnosticEntry();
17+
var bufferWriter = new ArrayBufferWriter<byte>();
18+
await using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = false });
19+
writer.WriteStartObject();
20+
21+
await subject.WriteAsync(writer);
22+
23+
writer.WriteEndObject();
24+
await writer.FlushAsync();
25+
var json = Encoding.UTF8.GetString(bufferWriter.WrittenSpan);
26+
var jsonDocument = JsonDocument.Parse(json);
27+
var assemblyInfo = jsonDocument.RootElement.GetProperty("AssemblyInfo");
28+
assemblyInfo.GetProperty("AssemblyCount").ValueKind.ShouldBe(JsonValueKind.Number);
29+
var assemblies = assemblyInfo.GetProperty("Assemblies");
30+
assemblies.ValueKind.ShouldBe(JsonValueKind.Array);
31+
var firstEntry = assemblies.EnumerateArray().First();
32+
firstEntry.GetProperty("Name").ValueKind.ShouldBe(JsonValueKind.String);
33+
firstEntry.GetProperty("Version").ValueKind.ShouldBe(JsonValueKind.String);
34+
}
35+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using System.Text.Json;
5+
using Duende.IdentityServer.Licensing.V2.Diagnostics;
6+
using Microsoft.Extensions.Logging.Abstractions;
7+
8+
namespace IdentityServer.UnitTests.Licensing.V2;
9+
10+
public class DiagnosticSummaryTests
11+
{
12+
[Fact]
13+
public async Task PrintSummary_ShouldCallWriteAsyncOnEveryDiagnosticEntry()
14+
{
15+
var fakeLogger = new NullLogger<DiagnosticSummary>();
16+
var firstDiagnosticEntry = new TestDiagnosticEntry();
17+
var secondDiagnosticEntry = new TestDiagnosticEntry();
18+
var thirdDiagnosticEntry = new TestDiagnosticEntry();
19+
var entries = new List<IDiagnosticEntry>
20+
{
21+
firstDiagnosticEntry,
22+
secondDiagnosticEntry,
23+
thirdDiagnosticEntry
24+
};
25+
var summary = new DiagnosticSummary(entries, fakeLogger);
26+
27+
await summary.PrintSummary();
28+
29+
firstDiagnosticEntry.WasCalled.ShouldBeTrue();
30+
secondDiagnosticEntry.WasCalled.ShouldBeTrue();
31+
thirdDiagnosticEntry.WasCalled.ShouldBeTrue();
32+
}
33+
34+
private class TestDiagnosticEntry : IDiagnosticEntry
35+
{
36+
public bool WasCalled { get; private set; }
37+
public Task WriteAsync(Utf8JsonWriter writer)
38+
{
39+
WasCalled = true;
40+
return Task.CompletedTask;
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)