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
@@ -0,0 +1,24 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

#nullable enable
namespace Duende.IdentityServer.Configuration;

/// <summary>
/// Options that control the way that diagnostic data is logged.
/// </summary>
public class DiagnosticOptions
{
/// <summary>
/// Frequency at which the diagnostic data is logged.
/// The default value is 1 hour.
/// </summary>
public TimeSpan LogFrequency { get; set; } = TimeSpan.FromHours(1);

/// <summary>
/// Max size of diagnostic data log message chunks in kilobytes.
/// Defaults to 8160 bytes. 8 KB is a conservative limit for the max size of a log message that is imposed by
/// some logging tools. We take 32 bytes less than that to allow for additional formatting of the log message.
/// </summary>
public int ChunkSize { get; set; } = 1024 * 8 - 32;
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,7 @@ public class IdentityServerOptions
public PreviewFeatureOptions Preview { get; set; } = new PreviewFeatureOptions();

/// <summary>
/// Frequency at which the diagnostic summary is logged.
/// The default value is 1 hour.
/// Options that control the diagnostic data that is logged by IdentityServer.
/// </summary>
public TimeSpan DiagnosticSummaryLogFrequency { get; set; } = TimeSpan.FromHours(1);
public DiagnosticOptions Diagnostics { get; set; } = new DiagnosticOptions();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal class DiagnosticHostedService(DiagnosticSummary diagnosticSummary, IOpt
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(options.Value.DiagnosticSummaryLogFrequency);
using var timer = new PeriodicTimer(options.Value.Diagnostics.LogFrequency);
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
using System.Buffers;
using System.Text;
using System.Text.Json;
using Duende.IdentityServer.Configuration;
using Microsoft.Extensions.Logging;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics;

internal class DiagnosticSummary(IEnumerable<IDiagnosticEntry> entries, ILogger<DiagnosticSummary> logger)
internal class DiagnosticSummary(IEnumerable<IDiagnosticEntry> entries, IdentityServerOptions options, ILogger<DiagnosticSummary> logger)
{
public async Task PrintSummary()
{
Expand All @@ -26,6 +27,23 @@ public async Task PrintSummary()

await writer.FlushAsync();

logger.LogInformation("{Message}", Encoding.UTF8.GetString(bufferWriter.WrittenSpan));
var span = bufferWriter.WrittenSpan;

var chunkSize = options.Diagnostics.ChunkSize;
if (span.Length > chunkSize)
{
var totalChunks = (span.Length + options.Diagnostics.ChunkSize - 1) / chunkSize;
for (var i = 0; i < totalChunks; i++)
{
var offset = i * chunkSize;
var length = Math.Min(chunkSize, span.Length - offset);
var chunk = span.Slice(offset, length);
logger.LogInformation("Diagnostic data ({current} of {totalChunks}): {diagnosticData}", i + 1, totalChunks, Encoding.UTF8.GetString(chunk));
}
}
else
{
logger.LogInformation("Diagnostic data: {diagnosticData}", Encoding.UTF8.GetString(bufferWriter.WrittenSpan));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public async Task WriteAsync_ShouldIncludeOtherProperties()
JwtValidationClockSkew = TimeSpan.FromMinutes(1),
SupportedClientAssertionSigningAlgorithms = ["RS256", "ES256"],
SupportedRequestObjectSigningAlgorithms = ["SHA256", "SHA512"],
DiagnosticSummaryLogFrequency = TimeSpan.FromMinutes(30)
Diagnostics = new DiagnosticOptions { LogFrequency = TimeSpan.FromMinutes(30) }
};
var subject = new IdentityServerOptionsDiagnosticEntry(Options.Create(options));

Expand Down Expand Up @@ -76,12 +76,13 @@ public async Task WriteAsync_ShouldIncludeOtherProperties()
identityServerOptions.TryGetProperty("KeyManagement", out _).ShouldBeTrue();
identityServerOptions.TryGetProperty("PersistentGrants", out _).ShouldBeTrue();
identityServerOptions.TryGetProperty("DPoP", out _).ShouldBeTrue();
identityServerOptions.TryGetProperty("Diagnostics", out _).ShouldBeTrue();

identityServerOptions.GetProperty("JwtValidationClockSkew").GetString().ShouldBe(TimeSpan.FromMinutes(1).ToString());
var supportedClientAssertionSigningAlgorithms = identityServerOptions.TryGetStringArray("SupportedClientAssertionSigningAlgorithms");
supportedClientAssertionSigningAlgorithms.ShouldBe(["RS256", "ES256"]);
var supportedRequestObjectSigningAlgorithms = identityServerOptions.TryGetStringArray("SupportedRequestObjectSigningAlgorithms");
supportedRequestObjectSigningAlgorithms.ShouldBe(["SHA256", "SHA512"]);
identityServerOptions.TryGetProperty("Preview", out _).ShouldBeTrue();
identityServerOptions.GetProperty("DiagnosticSummaryLogFrequency").GetString().ShouldBe(TimeSpan.FromMinutes(30).ToString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// See LICENSE in the project root for license information.

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

namespace IdentityServer.UnitTests.Licensing.V2;

Expand All @@ -22,7 +24,7 @@ public async Task PrintSummary_ShouldCallWriteAsyncOnEveryDiagnosticEntry()
secondDiagnosticEntry,
thirdDiagnosticEntry
};
var summary = new DiagnosticSummary(entries, fakeLogger);
var summary = new DiagnosticSummary(entries, new IdentityServerOptions(), fakeLogger);

await summary.PrintSummary();

Expand All @@ -31,6 +33,58 @@ public async Task PrintSummary_ShouldCallWriteAsyncOnEveryDiagnosticEntry()
thirdDiagnosticEntry.WasCalled.ShouldBeTrue();
}

[Fact]
public async Task PrintSummary_ShouldChunkLargeOutput()
{
var chunkSize = 8;
var options = new IdentityServerOptions { Diagnostics = new DiagnosticOptions { ChunkSize = 8 } };

var logger = new FakeLogger<DiagnosticSummary>();
var diagnosticEntry = new LongDiagnosticEntry { OutputLength = chunkSize * 2 };
var summary = new DiagnosticSummary([diagnosticEntry], options, logger);

await summary.PrintSummary();

var logSnapshot = logger.Collector.GetSnapshot().Select(x => x.Message);
logSnapshot.ShouldBe([
"Diagnostic data (1 of 4): {\"test\":",
"Diagnostic data (2 of 4): \"xxxxxxx",
"Diagnostic data (3 of 4): xxxxxxxx",
"Diagnostic data (4 of 4): x\"}"]);
}

[Fact]
public async Task PrintSummary_ShouldChunkLargeOutputOfMultibyteCharacters()
{
var options = new IdentityServerOptions { Diagnostics = new DiagnosticOptions { ChunkSize = 8 } };

var logger = new FakeLogger<DiagnosticSummary>();
var diagnosticEntry = new LongDiagnosticEntry { OutputLength = 2, OutputCharacter = '€' };
var summary = new DiagnosticSummary([diagnosticEntry], options, logger);

await summary.PrintSummary();

var logSnapshot = logger.Collector.GetSnapshot().Select(x => x.Message);
logSnapshot.ShouldBe(["Diagnostic data (1 of 3): {\"test\":", "Diagnostic data (2 of 3): \"\\u20AC\\", "Diagnostic data (3 of 3): u20AC\"}"]);
}

[Fact]
public async Task PrintSummary_ShouldCreateChunksWithMaxSizeEightKB()
{
var options = new IdentityServerOptions();

var logger = new FakeLogger<DiagnosticSummary>();
var diagnosticEntry = new LongDiagnosticEntry { OutputLength = options.Diagnostics.ChunkSize * 2 };
var summary = new DiagnosticSummary([diagnosticEntry], options, logger);

await summary.PrintSummary();
foreach (var entry in logger.Collector.GetSnapshot())
{
entry.Message.Length.ShouldBeLessThanOrEqualTo(1024 * 8);
}
}


private class TestDiagnosticEntry : IDiagnosticEntry
{
public bool WasCalled { get; private set; }
Expand All @@ -40,4 +94,16 @@ public Task WriteAsync(Utf8JsonWriter writer)
return Task.CompletedTask;
}
}

private class LongDiagnosticEntry : IDiagnosticEntry
{
public int OutputLength { get; set; }
public char OutputCharacter { get; set; } = 'x';

public Task WriteAsync(Utf8JsonWriter writer)
{
writer.WriteString("test", new string(OutputCharacter, OutputLength));
return Task.CompletedTask;
}
}
}
Loading