Skip to content

Commit 4f59161

Browse files
committed
Added diagnostic entry for token issue counts
1 parent fa8a47b commit 4f59161

File tree

4 files changed

+274
-0
lines changed

4 files changed

+274
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder
217217
builder.Services.AddSingleton<IDiagnosticEntry, RegisteredImplementationsDiagnosticEntry>();
218218
builder.Services.AddSingleton<IDiagnosticEntry, IdentityServerOptionsDiagnosticEntry>();
219219
builder.Services.AddSingleton<IDiagnosticEntry, DataProtectionDiagnosticEntry>();
220+
builder.Services.AddSingleton<IDiagnosticEntry, TokenIssueCountDiagnosticEntry>();
220221
builder.Services.AddSingleton<DiagnosticSummary>();
221222
builder.Services.AddHostedService<DiagnosticHostedService>();
222223

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+
namespace Duende.IdentityServer.Licensing.V2;
5+
6+
internal class AtomicCounter
7+
{
8+
private long _count;
9+
public void Increment() => Interlocked.Increment(ref _count);
10+
public long Count => _count;
11+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
#nullable enable
5+
using System.Collections.Concurrent;
6+
using System.Diagnostics.Metrics;
7+
using System.Text.Json;
8+
using Duende.IdentityServer.Models;
9+
10+
namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
11+
12+
internal class TokenIssueCountDiagnosticEntry : IDiagnosticEntry
13+
{
14+
private readonly ConcurrentDictionary<string, AtomicCounter> _tokenCounts;
15+
private readonly MeterListener _meterListener;
16+
17+
public TokenIssueCountDiagnosticEntry()
18+
{
19+
_tokenCounts = new ConcurrentDictionary<string, AtomicCounter>([
20+
new("Jwt", new AtomicCounter()),
21+
new ("Reference", new AtomicCounter()),
22+
new ("Refresh", new AtomicCounter()),
23+
new("JwtPoPDPoP", new AtomicCounter()),
24+
new("ReferencePoPDPoP", new AtomicCounter()),
25+
new("JwtPoPmTLS", new AtomicCounter()),
26+
new("ReferencePoPmTLS", new AtomicCounter()),
27+
new("Id", new AtomicCounter())
28+
]);
29+
_meterListener = new MeterListener();
30+
31+
_meterListener.InstrumentPublished += (instrument, listener) =>
32+
{
33+
if (instrument.Name == Telemetry.Metrics.Counters.TokenIssued)
34+
{
35+
listener.EnableMeasurementEvents(instrument);
36+
}
37+
};
38+
39+
_meterListener.SetMeasurementEventCallback<long>(HandleLongMeasurementRecorded);
40+
41+
_meterListener.Start();
42+
}
43+
44+
public Task WriteAsync(Utf8JsonWriter writer)
45+
{
46+
writer.WritePropertyName("TokenIssueCounts");
47+
writer.WriteStartObject();
48+
49+
foreach (var (tokenType, counter) in _tokenCounts)
50+
{
51+
writer.WriteNumber(tokenType, counter.Count);
52+
}
53+
54+
writer.WriteEndObject();
55+
56+
return Task.CompletedTask;
57+
}
58+
59+
private void HandleLongMeasurementRecorded(Instrument instrument, long value, ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)
60+
{
61+
if (instrument.Name != Telemetry.Metrics.Counters.TokenIssued)
62+
{
63+
return;
64+
}
65+
66+
var accessTokenIssued = false;
67+
var accessTokenType = AccessTokenType.Jwt;
68+
var refreshTokenIssued = false;
69+
var proofType = ProofType.None;
70+
var identityTokenIssued = false;
71+
72+
foreach (var tag in tags)
73+
{
74+
switch (tag.Key)
75+
{
76+
case Telemetry.Metrics.Tags.AccessTokenType:
77+
if (!Enum.TryParse(tag.Value?.ToString(), out accessTokenType))
78+
{
79+
accessTokenType = AccessTokenType.Jwt;
80+
}
81+
break;
82+
case Telemetry.Metrics.Tags.RefreshTokenIssued:
83+
bool.TryParse(tag.Value?.ToString(), out refreshTokenIssued);
84+
break;
85+
case Telemetry.Metrics.Tags.ProofType:
86+
if (!Enum.TryParse(tag.Value?.ToString(), out proofType))
87+
{
88+
proofType = ProofType.None;
89+
}
90+
break;
91+
case Telemetry.Metrics.Tags.AccessTokenIssued:
92+
bool.TryParse(tag.Value?.ToString(), out accessTokenIssued);
93+
break;
94+
case Telemetry.Metrics.Tags.IdTokenIssued:
95+
bool.TryParse(tag.Value?.ToString(), out identityTokenIssued);
96+
break;
97+
}
98+
}
99+
100+
if (accessTokenIssued)
101+
{
102+
switch (proofType)
103+
{
104+
case ProofType.None when accessTokenType == AccessTokenType.Jwt:
105+
_tokenCounts["Jwt"].Increment();
106+
break;
107+
case ProofType.None when accessTokenType == AccessTokenType.Reference:
108+
_tokenCounts["Reference"].Increment();
109+
break;
110+
case ProofType.DPoP when accessTokenType == AccessTokenType.Jwt:
111+
_tokenCounts["JwtPoPDPoP"].Increment();
112+
break;
113+
case ProofType.DPoP when accessTokenType == AccessTokenType.Reference:
114+
_tokenCounts["ReferencePoPDPoP"].Increment();
115+
break;
116+
case ProofType.ClientCertificate when accessTokenType == AccessTokenType.Jwt:
117+
_tokenCounts["JwtPoPmTLS"].Increment();
118+
break;
119+
case ProofType.ClientCertificate when accessTokenType == AccessTokenType.Reference:
120+
_tokenCounts["ReferencePoPmTLS"].Increment();
121+
break;
122+
}
123+
}
124+
125+
if (refreshTokenIssued)
126+
{
127+
_tokenCounts["Refresh"].Increment();
128+
}
129+
130+
if (identityTokenIssued)
131+
{
132+
_tokenCounts["Id"].Increment();
133+
}
134+
}
135+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
5+
using Duende.IdentityServer.Models;
6+
7+
namespace IdentityServer.UnitTests.Licensing.V2.DiagnosticEntries;
8+
9+
public class TokenIssueCountDiagnosticEntryTests
10+
{
11+
private readonly TokenIssueCountDiagnosticEntry _subject = new();
12+
13+
[Fact]
14+
public async Task Should_Count_JwtAccessToken()
15+
{
16+
IssueToken(true, AccessTokenType.Jwt, false, ProofType.None, false);
17+
18+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
19+
20+
result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Jwt").GetInt64().ShouldBe(1);
21+
}
22+
23+
[Fact]
24+
public async Task Should_Count_JwtReferenceToken()
25+
{
26+
IssueToken(true, AccessTokenType.Reference, false, ProofType.None, false);
27+
28+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
29+
30+
result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Reference").GetInt64().ShouldBe(1);
31+
}
32+
33+
[Fact]
34+
public async Task Should_Count_JwtDPoPToken()
35+
{
36+
IssueToken(true, AccessTokenType.Jwt, false, ProofType.DPoP, false);
37+
38+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
39+
40+
result.RootElement.GetProperty("TokenIssueCounts").GetProperty("JwtPoPDPoP").GetInt64().ShouldBe(1);
41+
}
42+
43+
[Fact]
44+
public async Task Should_Count_ReferenceDPoPToken()
45+
{
46+
IssueToken(true, AccessTokenType.Reference, false, ProofType.DPoP, false);
47+
48+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
49+
50+
result.RootElement.GetProperty("TokenIssueCounts").GetProperty("ReferencePoPDPoP").GetInt64().ShouldBe(1);
51+
}
52+
53+
[Fact]
54+
public async Task Should_Count_JwtMTlsToken()
55+
{
56+
IssueToken(true, AccessTokenType.Jwt, false, ProofType.ClientCertificate, false);
57+
58+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
59+
60+
result.RootElement.GetProperty("TokenIssueCounts").GetProperty("JwtPoPmTLS").GetInt64().ShouldBe(1);
61+
}
62+
63+
[Fact]
64+
public async Task Should_Count_ReferenceMTlsToken()
65+
{
66+
IssueToken(true, AccessTokenType.Reference, false, ProofType.ClientCertificate, false);
67+
68+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
69+
70+
result.RootElement.GetProperty("TokenIssueCounts").GetProperty("ReferencePoPmTLS").GetInt64().ShouldBe(1);
71+
}
72+
73+
[Fact]
74+
public async Task Should_Count_RefreshToken()
75+
{
76+
IssueToken(false, AccessTokenType.Jwt, true, ProofType.None, false);
77+
78+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
79+
80+
result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Refresh").GetInt64().ShouldBe(1);
81+
}
82+
83+
[Fact]
84+
public async Task Should_Count_IdToken()
85+
{
86+
IssueToken(false, AccessTokenType.Jwt, false, ProofType.None, true);
87+
88+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
89+
90+
result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Id").GetInt64().ShouldBe(1);
91+
}
92+
93+
[Fact]
94+
public async Task Should_Handle_Multiple_Token_Types()
95+
{
96+
IssueToken(true, AccessTokenType.Jwt, true, ProofType.None, false);
97+
IssueToken(true, AccessTokenType.Jwt, false, ProofType.DPoP, false);
98+
99+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
100+
101+
var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts");
102+
tokenIssueCounts.GetProperty("Jwt").GetInt64().ShouldBe(1);
103+
tokenIssueCounts.GetProperty("JwtPoPDPoP").GetInt64().ShouldBe(1);
104+
tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(1);
105+
}
106+
107+
[Fact]
108+
public async Task Should_Ignore_Non_TokenIssued_Instruments()
109+
{
110+
Duende.IdentityServer.Telemetry.Metrics.TokenIssuedFailure("ClientId", "GrantType", null, "error");
111+
112+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject);
113+
114+
var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts");
115+
tokenIssueCounts.GetProperty("Jwt").GetInt64().ShouldBe(0);
116+
tokenIssueCounts.GetProperty("Reference").GetInt64().ShouldBe(0);
117+
tokenIssueCounts.GetProperty("JwtPoPDPoP").GetInt64().ShouldBe(0);
118+
tokenIssueCounts.GetProperty("JwtPoPmTLS").GetInt64().ShouldBe(0);
119+
tokenIssueCounts.GetProperty("ReferencePoPDPoP").GetInt64().ShouldBe(0);
120+
tokenIssueCounts.GetProperty("ReferencePoPmTLS").GetInt64().ShouldBe(0);
121+
tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(0);
122+
}
123+
124+
private void IssueToken(bool accessTokenIssued, AccessTokenType accessTokenType, bool refreshTokenIssued,
125+
ProofType proofType, bool idTokenIssued) =>
126+
Duende.IdentityServer.Telemetry.Metrics.TokenIssued("ClientId", "GrantType", null, accessTokenIssued, accessTokenType, refreshTokenIssued, proofType, idTokenIssued);
127+
}

0 commit comments

Comments
 (0)