diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs index c8269bb7cd..36657671d8 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs @@ -121,16 +121,19 @@ public async Task RunAsync(CancellationToken cancellationT private void LogSuccessTelemetryToOtel(AuthenticationResult authenticationResult, ApiEvent apiEvent, long durationInUs) { + CacheLevel cacheLevel = GetCacheLevel(authenticationResult); + // Log metrics ServiceBundle.PlatformProxy.OtelInstrumentation.LogSuccessMetrics( ServiceBundle.PlatformProxy.GetProductName(), apiEvent.ApiId, apiEvent.CallerSdkApiId, apiEvent.CallerSdkVersion, - GetCacheLevel(authenticationResult), + cacheLevel, durationInUs, authenticationResult.AuthenticationResultMetadata, - AuthenticationRequestParameters.RequestContext.Logger); + AuthenticationRequestParameters.RequestContext.Logger, + authenticationResult.ExpiresOn); } private void LogFailureTelemetryToOtel(string errorCodeToLog, ApiEvent apiEvent, CacheRefreshReason cacheRefreshReason) diff --git a/src/client/Microsoft.Identity.Client/Platforms/Features/OpenTelemetry/OtelInstrumentation.cs b/src/client/Microsoft.Identity.Client/Platforms/Features/OpenTelemetry/OtelInstrumentation.cs index 51f94757e6..fee3a8cc7f 100644 --- a/src/client/Microsoft.Identity.Client/Platforms/Features/OpenTelemetry/OtelInstrumentation.cs +++ b/src/client/Microsoft.Identity.Client/Platforms/Features/OpenTelemetry/OtelInstrumentation.cs @@ -28,6 +28,7 @@ internal class OtelInstrumentation : IOtelInstrumentation private const string DurationInL2CacheHistogramName = "MsalDurationInL2Cache.1A"; private const string DurationInHttpHistogramName = "MsalDurationInHttp.1A"; private const string DurationInExtensionInMsHistogram = "MsalDurationInExtensionInMs.1B"; + private const string RemainingTokenLifetimeHistogramName = "MsalRemainingTokenLifetime.1A"; /// /// Meter to hold the MSAL metrics. @@ -88,6 +89,14 @@ internal class OtelInstrumentation : IOtelInstrumentation unit: "us", description: "Performance of token acquisition calls extension latency.")); + /// + /// Histogram to record the remaining lifetime of acquired tokens in seconds. + /// + internal static readonly Lazy> s_remainingTokenLifetime = new(() => Meter.CreateHistogram( + RemainingTokenLifetimeHistogramName, + unit: "s", + description: "Remaining lifetime of acquired tokens at the time of acquisition.")); + public OtelInstrumentation() { // Needed to fail fast if the runtime, like in-process Azure Functions, doesn't support OpenTelemetry @@ -103,7 +112,8 @@ public void LogSuccessMetrics( CacheLevel cacheLevel, long totalDurationInUs, AuthenticationResultMetadata authResultMetadata, - ILoggerAdapter logger) + ILoggerAdapter logger, + DateTimeOffset expiresOn) { IncrementSuccessCounter( platform, @@ -171,6 +181,19 @@ public void LogSuccessMetrics( new(TelemetryConstants.CacheLevel, authResultMetadata.CacheLevel), new(TelemetryConstants.TokenType, authResultMetadata.TelemetryTokenType)); } + + if (s_remainingTokenLifetime.Value.Enabled) + { + long remainingSeconds = Math.Max(0, (long)(expiresOn - DateTimeOffset.UtcNow).TotalSeconds); + + s_remainingTokenLifetime.Value.Record(remainingSeconds, + new(TelemetryConstants.MsalVersionPlatform, $"{MsalIdHelper.GetMsalVersion()},{platform}"), + new(TelemetryConstants.ApiId, apiId), + new(TelemetryConstants.TokenSource, authResultMetadata.TokenSource), + new(TelemetryConstants.CacheLevel, cacheLevel), + new(TelemetryConstants.CacheRefreshReason, authResultMetadata.CacheRefreshReason), + new(TelemetryConstants.TokenType, authResultMetadata.TelemetryTokenType)); + } } public void IncrementSuccessCounter(string platform, @@ -218,5 +241,6 @@ public void LogFailureMetrics(string platform, new(TelemetryConstants.TokenType, tokenType)); } } + } } diff --git a/src/client/Microsoft.Identity.Client/TelemetryCore/OpenTelemetry/IOtelInstrumentation.cs b/src/client/Microsoft.Identity.Client/TelemetryCore/OpenTelemetry/IOtelInstrumentation.cs index 7802ab4211..a7b5bc024f 100644 --- a/src/client/Microsoft.Identity.Client/TelemetryCore/OpenTelemetry/IOtelInstrumentation.cs +++ b/src/client/Microsoft.Identity.Client/TelemetryCore/OpenTelemetry/IOtelInstrumentation.cs @@ -19,7 +19,8 @@ internal void LogSuccessMetrics( CacheLevel cacheLevel, long totalDurationInUs, AuthenticationResultMetadata authResultMetadata, - ILoggerAdapter logger); + ILoggerAdapter logger, + DateTimeOffset expiresOn); internal void IncrementSuccessCounter(string platform, ApiEvent.ApiIds apiId, diff --git a/src/client/Microsoft.Identity.Client/TelemetryCore/OpenTelemetry/NullOtelInstrumentation.cs b/src/client/Microsoft.Identity.Client/TelemetryCore/OpenTelemetry/NullOtelInstrumentation.cs index 7fcdbbcf9c..55b6d35074 100644 --- a/src/client/Microsoft.Identity.Client/TelemetryCore/OpenTelemetry/NullOtelInstrumentation.cs +++ b/src/client/Microsoft.Identity.Client/TelemetryCore/OpenTelemetry/NullOtelInstrumentation.cs @@ -22,7 +22,8 @@ public void LogSuccessMetrics( CacheLevel cacheLevel, long totalDurationInUs, AuthenticationResultMetadata authResultMetadata, - ILoggerAdapter logger) + ILoggerAdapter logger, + DateTimeOffset expiresOn) { // No op } @@ -50,5 +51,6 @@ void IOtelInstrumentation.IncrementSuccessCounter(string platform, { // No op } + } } diff --git a/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryConstants.cs b/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryConstants.cs index bed6e7db55..c2820f8ea1 100644 --- a/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryConstants.cs +++ b/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryConstants.cs @@ -46,6 +46,7 @@ internal static class TelemetryConstants public const string IsProactiveRefresh = "IsProactiveRefresh"; public const string CallerSdkId = "CallerSdkId"; public const string CallerSdkVersion = "CallerSdkVersion"; + public const string MsalVersionPlatform = "MsalVersionPlatform"; #endregion } diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ProactiveRefreshTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ProactiveRefreshTests.cs index 0fff1d847b..dc82d93b68 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ProactiveRefreshTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ProactiveRefreshTests.cs @@ -92,7 +92,7 @@ public async Task ProactiveRefreshTriggers_WithTelemetry_Test() private long ValidateSuccessMetrics(MeterProvider meterProvider, List exportedMetrics) { - Assert.HasCount(5, exportedMetrics); + Assert.HasCount(6, exportedMetrics); foreach (var metric in exportedMetrics) { diff --git a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs index 131869ac44..19c1d2aedd 100644 --- a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs @@ -76,7 +76,7 @@ public async Task AcquireTokenOTelTestAsync() await AcquireTokenMsalClientExceptionAsync().ConfigureAwait(false); s_meterProvider.ForceFlush(); - VerifyMetrics(6, _exportedMetrics, 2, 2); + VerifyMetrics(7, _exportedMetrics, 2, 2); } } @@ -91,7 +91,7 @@ public async Task AcquireTokenOTelTestWithExtensionAsync() await AcquireTokenMsalClientExceptionAsync().ConfigureAwait(false); s_meterProvider.ForceFlush(); - VerifyMetrics(6, _exportedMetrics, 2, 2); + VerifyMetrics(7, _exportedMetrics, 2, 2); } } @@ -147,7 +147,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_ClientCredential_Async() Assert.AreEqual(CacheRefreshReason.NotApplicable, result.AuthenticationResultMetadata.CacheRefreshReason); s_meterProvider.ForceFlush(); - VerifyMetrics(5, _exportedMetrics, 4, 0); + VerifyMetrics(6, _exportedMetrics, 4, 0); } } @@ -222,7 +222,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_MSI_Async() Assert.AreEqual(CacheRefreshReason.NotApplicable, result.AuthenticationResultMetadata.CacheRefreshReason); s_meterProvider.ForceFlush(); - VerifyMetrics(5, _exportedMetrics, 4, 0); + VerifyMetrics(6, _exportedMetrics, 4, 0); } } @@ -279,7 +279,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_OBO_Async() Assert.AreEqual(CacheRefreshReason.NotApplicable, result.AuthenticationResultMetadata.CacheRefreshReason); s_meterProvider.ForceFlush(); - VerifyMetrics(5, _exportedMetrics, 4, 0); + VerifyMetrics(6, _exportedMetrics, 4, 0); } } @@ -329,7 +329,7 @@ public async Task ProactiveTokenRefresh_AadUnavailableResponse_Async() Thread.Sleep(1000); s_meterProvider.ForceFlush(); - VerifyMetrics(4, _exportedMetrics, 3, 1); + VerifyMetrics(5, _exportedMetrics, 3, 1); } } @@ -577,6 +577,26 @@ private void VerifyMetrics(int expectedMetricCount, List exportedMetrics AssertTags(metricPoint.Tags, expectedTags); } + break; + + case "MsalRemainingTokenLifetime.1A": + Trace.WriteLine("Verify the metrics captured for MsalRemainingTokenLifetime.1A histogram."); + Assert.AreEqual(MetricType.Histogram, exportedItem.MetricType); + + expectedTags.Add(TelemetryConstants.MsalVersionPlatform); + expectedTags.Add(TelemetryConstants.ApiId); + expectedTags.Add(TelemetryConstants.TokenSource); + expectedTags.Add(TelemetryConstants.CacheLevel); + expectedTags.Add(TelemetryConstants.CacheRefreshReason); + expectedTags.Add(TelemetryConstants.TokenType); + + foreach (var metricPoint in exportedItem.GetMetricPoints()) + { + AssertTags(metricPoint.Tags, expectedTags); + Assert.IsGreaterThan((long)0, metricPoint.GetHistogramCount(), "Histogram should have at least one recorded value."); + Assert.IsGreaterThanOrEqualTo(0.0, metricPoint.GetHistogramSum(), "Remaining token lifetime should be non-negative."); + } + break; default: Assert.Fail("Unexpected metrics logged.");