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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
using Duende.IdentityServer.Internal;
using Duende.IdentityServer.Licensing;
using Duende.IdentityServer.Licensing.V2;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
using Duende.IdentityServer.Logging;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.ResponseHandling;
Expand Down Expand Up @@ -210,6 +212,10 @@ public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder
builder.Services.AddSingleton<LicenseUsageTracker>();
builder.Services.AddSingleton<LicenseExpirationChecker>();

builder.Services.AddSingleton<IDiagnosticEntry, AssemblyInfoDiagnosticEntry>();
builder.Services.AddSingleton<DiagnosticSummary>();
builder.Services.AddHostedService<DiagnosticHostedService>();

return builder;
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,11 @@ public class IdentityServerOptions
/// <summary>
/// The allowed clock skew for JWT lifetime validation. Except for DPoP proofs,
/// all JWTs that have their lifetime validated use this setting to control the
/// clock skew of lifetime validation. This includes JWT access tokens passed
/// to the user info, introspection, and local api endpoints, client
/// clock skew of lifetime validation. This includes JWT access tokens passed
/// to the user info, introspection, and local api endpoints, client
/// authentication JWTs used in private_key_jwt authentication, JWT secured
/// authorization requests (JAR), and custom usage of the
/// <see cref="TokenValidator"/>, such as in a token exchange implementation.
/// authorization requests (JAR), and custom usage of the
/// <see cref="TokenValidator"/>, such as in a token exchange implementation.
/// Defaults to five minutes.
/// </summary>
public TimeSpan JwtValidationClockSkew { get; set; } = TimeSpan.FromMinutes(5);
Expand All @@ -236,4 +236,10 @@ public class IdentityServerOptions
/// Options for configuring preview features in the server.
/// </value>
public PreviewFeatureOptions Preview { get; set; } = new PreviewFeatureOptions();

/// <summary>
/// Frequency at which the diagnostic summary is logged.
/// The default value is 1 hour.
/// </summary>
public TimeSpan DiagnosticSummaryLogFrequency { get; set; } = TimeSpan.FromHours(1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;

internal class AssemblyInfoDiagnosticEntry : IDiagnosticEntry
{
public Task WriteAsync(Utf8JsonWriter writer)
{
var assemblies = GetAssemblyInfo();
writer.WriteStartObject("AssemblyInfo");
writer.WriteNumber("AssemblyCount", assemblies.Count);

writer.WriteStartArray("Assemblies");
foreach (var assembly in assemblies)
{
writer.WriteStartObject();
writer.WriteString("Name", assembly.GetName().Name);
writer.WriteString("Version", assembly.GetName().Version?.ToString() ?? "Unknown");
writer.WriteEndObject();
}

writer.WriteEndArray();
writer.WriteEndObject();

return Task.CompletedTask;
}

private List<Assembly> GetAssemblyInfo()
{
var assemblies = AssemblyLoadContext.Default.Assemblies
.OrderBy(a => a.FullName)
.ToList();

return assemblies;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using Duende.IdentityServer.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics;

internal class DiagnosticHostedService(DiagnosticSummary diagnosticSummary, IOptions<IdentityServerOptions> options, ILogger<DiagnosticHostedService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(options.Value.DiagnosticSummaryLogFrequency);
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
await diagnosticSummary.PrintSummary();
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while logging the diagnostic summary: {Message}", ex.Message);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Buffers;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics;

internal class DiagnosticSummary(IEnumerable<IDiagnosticEntry> entries, ILogger<DiagnosticSummary> logger)
{
public async Task PrintSummary()
{
var bufferWriter = new ArrayBufferWriter<byte>();
await using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = false });

writer.WriteStartObject();

foreach (var diagnosticEntry in entries)
{
await diagnosticEntry.WriteAsync(writer);
}

writer.WriteEndObject();

await writer.FlushAsync();

logger.LogInformation("{Message}", Encoding.UTF8.GetString(bufferWriter.WrittenSpan));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Text.Json;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics;

internal interface IDiagnosticEntry
{
Task WriteAsync(Utf8JsonWriter writer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Buffers;
using System.Text;
using System.Text.Json;
using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;

namespace IdentityServer.UnitTests.Licensing.V2.DiagnosticEntries;

public class AssemblyInfoDiagnosticEntryTests
{
[Fact]
public async Task WriteAsync_ShouldWriteAssemblyInfo()
{
var subject = new AssemblyInfoDiagnosticEntry();
var bufferWriter = new ArrayBufferWriter<byte>();
await using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = false });
writer.WriteStartObject();

await subject.WriteAsync(writer);

writer.WriteEndObject();
await writer.FlushAsync();
var json = Encoding.UTF8.GetString(bufferWriter.WrittenSpan);
var jsonDocument = JsonDocument.Parse(json);
var assemblyInfo = jsonDocument.RootElement.GetProperty("AssemblyInfo");
assemblyInfo.GetProperty("AssemblyCount").ValueKind.ShouldBe(JsonValueKind.Number);
var assemblies = assemblyInfo.GetProperty("Assemblies");
assemblies.ValueKind.ShouldBe(JsonValueKind.Array);
var firstEntry = assemblies.EnumerateArray().First();
firstEntry.GetProperty("Name").ValueKind.ShouldBe(JsonValueKind.String);
firstEntry.GetProperty("Version").ValueKind.ShouldBe(JsonValueKind.String);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Text.Json;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
using Microsoft.Extensions.Logging.Abstractions;

namespace IdentityServer.UnitTests.Licensing.V2;

public class DiagnosticSummaryTests
{
[Fact]
public async Task PrintSummary_ShouldCallWriteAsyncOnEveryDiagnosticEntry()
{
var fakeLogger = new NullLogger<DiagnosticSummary>();
var firstDiagnosticEntry = new TestDiagnosticEntry();
var secondDiagnosticEntry = new TestDiagnosticEntry();
var thirdDiagnosticEntry = new TestDiagnosticEntry();
var entries = new List<IDiagnosticEntry>
{
firstDiagnosticEntry,
secondDiagnosticEntry,
thirdDiagnosticEntry
};
var summary = new DiagnosticSummary(entries, fakeLogger);

await summary.PrintSummary();

firstDiagnosticEntry.WasCalled.ShouldBeTrue();
secondDiagnosticEntry.WasCalled.ShouldBeTrue();
thirdDiagnosticEntry.WasCalled.ShouldBeTrue();
}

private class TestDiagnosticEntry : IDiagnosticEntry
{
public bool WasCalled { get; private set; }
public Task WriteAsync(Utf8JsonWriter writer)
{
WasCalled = true;
return Task.CompletedTask;
}
}
}
Loading