Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -46,6 +46,22 @@ internal class AcquireTokenCommonParameters
public bool IsMtlsPopRequested { get; set; }
public string ExtraClientAssertionClaims { get; internal set; }

/// <summary>
/// Optional caller-supplied delegate that adds extra tags to the OpenTelemetry metrics MSAL records
/// for this request. It receives the <see cref="ExecutionResult"/> of the acquisition (success or failure)
/// and a mutable list of tags to which additional dimensions can be appended.
/// Set via <c>WithOtelTagsEnricher</c>.
Comment thread
neha-bhargava marked this conversation as resolved.
/// </summary>
/// <remarks>
/// The tags returned by the enricher are applied to every metric MSAL records for the request, so keep
/// both their value cardinality and their number low. High-cardinality tag values (for example correlation
/// ids, timestamps, or user identifiers) are the dominant cost: each distinct value multiplies the number
/// of metric time series the downstream backend must store and aggregate. A large number of tags is a
/// secondary cost that adds per-record overhead on MSAL's metric-recording path. Prefer a small set of
/// low-cardinality dimensions; avoid using request-unique values as tags.
/// </remarks>
public Action<ExecutionResult, IList<KeyValuePair<string, object>>> OtelTagsEnricher { get; set; }

/// <summary>
/// Optional delegate for obtaining attestation JWT for Credential Guard keys.
/// Set by the KeyAttestation package via .WithAttestationSupport().
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,5 +395,41 @@ public static AbstractAcquireTokenParameterBuilder<T> WithExtraClientAssertionCl

return builder;
}
}

/// <summary>
/// Registers a delegate that adds additional tags (dimensions) to the OpenTelemetry metrics MSAL emits
/// for this token acquisition. The delegate is invoked while MSAL records its metrics and receives the
/// <see cref="ExecutionResult"/> of the acquisition (indicating success or failure, with the result or
/// exception) together with a mutable list of tags. Tags appended to that list are attached to every metric
/// recorded for the request.
/// </summary>
/// <typeparam name="T">The concrete builder type.</typeparam>
/// <param name="builder">The builder to chain options to.</param>
/// <param name="tagsEnricher">
/// A delegate that receives the <see cref="ExecutionResult"/> and a mutable list of tags to enrich.
/// The delegate runs on MSAL's metric-recording path, so it should be fast, non-blocking and must not throw.
/// </param>
/// <returns>The builder to chain the .With methods.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="tagsEnricher"/> is null.</exception>
/// <remarks>
/// Keep both the number of added tags and — more importantly — their value cardinality low. High-cardinality
/// tag values (such as correlation ids, timestamps, or user identifiers) can cause an unbounded number of
/// metric time series in the downstream telemetry backend. The tags are applied to every metric MSAL records
/// for the request, so a large number of tags also adds overhead on the metric-recording path.
/// </remarks>
public static AbstractAcquireTokenParameterBuilder<T> WithOtelTagsEnricher<T>(
this AbstractAcquireTokenParameterBuilder<T> builder,
Action<ExecutionResult, IList<KeyValuePair<string, object>>> tagsEnricher)
where T : AbstractAcquireTokenParameterBuilder<T>
{
if (tagsEnricher == null)
{
throw new ArgumentNullException(nameof(tagsEnricher));
}

builder.CommonParameters.OtelTagsEnricher = tagsEnricher;
Comment thread
neha-bhargava marked this conversation as resolved.

return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ public string LoginHint

public string ExtraClientAssertionClaims => _commonParameters.ExtraClientAssertionClaims;

/// <summary>
/// Optional caller-supplied delegate that adds extra tags to the OpenTelemetry metrics MSAL records
/// for this request. Configured via <c>WithOtelTagsEnricher</c>.
/// </summary>
public Action<ExecutionResult, IList<KeyValuePair<string, object>>> OtelTagsEnricher => _commonParameters.OtelTagsEnricher;

public void LogParameters()
{
var logger = RequestContext.Logger;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,9 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
return await GetAccessTokenAsync(tokenSource.Token, logger).ConfigureAwait(false);
}, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent,
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId,
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion);
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId,
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion,
AuthenticationRequestParameters.OtelTagsEnricher);
}
}
catch (MsalServiceException e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
return await GetAccessTokenAsync(tokenSource.Token, logger).ConfigureAwait(false);
}, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent,
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId,
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion);
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion,
AuthenticationRequestParameters.OtelTagsEnricher);
}
}
catch (MsalServiceException e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
return await RefreshRtOrFetchNewAccessTokenAsync(tokenSource.Token).ConfigureAwait(false);
}, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent,
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId,
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion);
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion,
AuthenticationRequestParameters.OtelTagsEnricher);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Microsoft.Identity.Client.Internal.Broker;
using System.Runtime.ConstrainedExecution;
using Microsoft.Identity.Client.AuthScheme;
using Microsoft.Identity.Client.Extensibility;

namespace Microsoft.Identity.Client.Internal.Requests
{
Expand Down Expand Up @@ -121,7 +122,8 @@ public async Task<AuthenticationResult> RunAsync(CancellationToken cancellationT
apiEvent.CacheInfo,
httpStatusCode,
requestStopwatch.ElapsedMilliseconds + measureTelemetryDurationResult.Milliseconds,
(ex as MsalServiceException)?.ErrorCodes?.FirstOrDefault());
exception: ex,
rawStsErrorCode: (ex as MsalServiceException)?.ErrorCodes?.FirstOrDefault());
throw;
}
catch (Exception ex)
Expand All @@ -138,6 +140,18 @@ private void LogSuccessTelemetryToOtel(AuthenticationResult authenticationResult
{
CacheLevel cacheLevel = GetCacheLevel(authenticationResult);

// Invoke the caller-supplied enricher once per acquisition and merge the resulting fixed set of
// extra tags into every instrument below, so the delegate is not re-run per metric.
IReadOnlyList<KeyValuePair<string, object>> extraTags = OtelEnrichmentHelper.MaterializeExtraTags(
AuthenticationRequestParameters.OtelTagsEnricher,
() => new ExecutionResult
{
Successful = true,
Result = authenticationResult,
ClientCertificate = AuthenticationRequestParameters.ResolvedCertificate
},
AuthenticationRequestParameters.RequestContext.Logger);

// Log metrics
ServiceBundle.PlatformProxy.OtelInstrumentation.LogSuccessMetrics(
ServiceBundle.PlatformProxy.GetProductName(),
Expand All @@ -148,11 +162,24 @@ private void LogSuccessTelemetryToOtel(AuthenticationResult authenticationResult
durationInUs,
authenticationResult.AuthenticationResultMetadata,
AuthenticationRequestParameters.RequestContext.Logger,
authenticationResult.ExpiresOn);
authenticationResult.ExpiresOn,
extraTags);
}

private void LogFailureTelemetryToOtel(string errorCodeToLog, ApiEvent apiEvent, CacheRefreshReason cacheRefreshReason, int httpStatusCode, long totalDurationInMs, string rawStsErrorCode = null)
private void LogFailureTelemetryToOtel(string errorCodeToLog, ApiEvent apiEvent, CacheRefreshReason cacheRefreshReason, int httpStatusCode, long totalDurationInMs, MsalException exception = null, string rawStsErrorCode = null)
{
// Invoke the caller-supplied enricher once per acquisition and merge the resulting fixed set of
// extra tags into every instrument below, so the delegate is not re-run per metric.
IReadOnlyList<KeyValuePair<string, object>> extraTags = OtelEnrichmentHelper.MaterializeExtraTags(
AuthenticationRequestParameters.OtelTagsEnricher,
() => new ExecutionResult
{
Successful = false,
Exception = exception,
ClientCertificate = AuthenticationRequestParameters.ResolvedCertificate
},
AuthenticationRequestParameters.RequestContext.Logger);

ServiceBundle.PlatformProxy.OtelInstrumentation.LogFailureMetrics(
ServiceBundle.PlatformProxy.GetProductName(),
errorCodeToLog,
Expand All @@ -163,7 +190,9 @@ private void LogFailureTelemetryToOtel(string errorCodeToLog, ApiEvent apiEvent,
apiEvent.TokenType,
httpStatusCode,
totalDurationInMs,
rawStsErrorCode);
rawStsErrorCode,
AuthenticationRequestParameters.RequestContext.Logger,
extraTags);
}

private Tuple<string, string> ParseScopesForTelemetry()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ public async Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellat
return await RefreshRtOrFailAsync(tokenSource.Token).ConfigureAwait(false);
}, logger, ServiceBundle, AuthenticationRequestParameters.RequestContext.ApiEvent,
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkApiId,
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion);
AuthenticationRequestParameters.RequestContext.ApiEvent.CallerSdkVersion,
AuthenticationRequestParameters.OtelTagsEnricher);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
using System.Threading.Tasks;
using Microsoft.Identity.Client.Cache.Items;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.Extensibility;
using Microsoft.Identity.Client.Internal.Requests;
using Microsoft.Identity.Client.OAuth2;
using Microsoft.Identity.Client.TelemetryCore.Internal.Events;
using Microsoft.Identity.Client.TelemetryCore.OpenTelemetry;
#if iOS
using Microsoft.Identity.Client.Platforms.iOS;
#endif
Expand Down Expand Up @@ -84,17 +86,26 @@ internal static bool NeedsRefresh(MsalAccessTokenCacheItem oldAccessToken, out D
internal static void ProcessFetchInBackground(
MsalAccessTokenCacheItem oldAccessToken,
Func<Task<AuthenticationResult>> fetchAction,
ILoggerAdapter logger,
IServiceBundle serviceBundle,
ApiEvent apiEvent,
string callerSdkId,
string callerSdkVersion)
ILoggerAdapter logger,
IServiceBundle serviceBundle,
ApiEvent apiEvent,
string callerSdkId,
string callerSdkVersion,
Action<ExecutionResult, IList<KeyValuePair<string, object>>> tagsEnricher = null)
{
_ = Task.Run(async () =>
{
try
{
var authResult = await fetchAction().ConfigureAwait(false);

// Invoke the enricher once for this background refresh and reuse the materialized
// tags across every instrument below.
IReadOnlyList<KeyValuePair<string, object>> extraTags = OtelEnrichmentHelper.MaterializeExtraTags(
tagsEnricher,
() => new ExecutionResult { Successful = true, Result = authResult },
logger);

serviceBundle.PlatformProxy.OtelInstrumentation.IncrementSuccessCounter(
serviceBundle.PlatformProxy.GetProductName(),
apiEvent.ApiId,
Expand All @@ -104,7 +115,8 @@ internal static void ProcessFetchInBackground(
CacheRefreshReason.ProactivelyRefreshed,
Cache.CacheLevel.None,
logger,
apiEvent.TokenType);
apiEvent.TokenType,
extraTags);

serviceBundle.PlatformProxy.OtelInstrumentation.LogRemainingTokenLifetime(
serviceBundle.PlatformProxy.GetProductName(),
Expand All @@ -113,12 +125,16 @@ internal static void ProcessFetchInBackground(
Cache.CacheLevel.None,
CacheRefreshReason.ProactivelyRefreshed,
apiEvent.TokenType,
authResult.ExpiresOn);
authResult.ExpiresOn,
logger,
extraTags);

serviceBundle.PlatformProxy.OtelInstrumentation.LogSuccessHttpDuration(
serviceBundle.PlatformProxy.GetProductName(),
apiEvent.ApiId,
authResult.AuthenticationResultMetadata);
authResult.AuthenticationResultMetadata,
logger,
extraTags);
}
catch (MsalServiceException ex)
{
Expand All @@ -133,19 +149,19 @@ internal static void ProcessFetchInBackground(
}

LogBackgroundFailureTelemetry(serviceBundle, apiEvent, callerSdkId, callerSdkVersion,
ex.ErrorCode, ex.StatusCode, ex.ErrorCodes?.FirstOrDefault());
ex.ErrorCode, ex.StatusCode, ex.ErrorCodes?.FirstOrDefault(), ex, tagsEnricher, logger);
}
catch (OperationCanceledException ex)
{
logger.WarningPiiWithPrefix(ex, ProactiveRefreshCancellationError);
LogBackgroundFailureTelemetry(serviceBundle, apiEvent, callerSdkId, callerSdkVersion,
ex.GetType().Name, httpStatusCode: 0);
ex.GetType().Name, httpStatusCode: 0, tagsEnricher: tagsEnricher, logger: logger);
}
catch (Exception ex)
{
logger.ErrorPiiWithPrefix(ex, ProactiveRefreshGeneralError);
LogBackgroundFailureTelemetry(serviceBundle, apiEvent, callerSdkId, callerSdkVersion,
ex.GetType().Name, httpStatusCode: 0);
ex.GetType().Name, httpStatusCode: 0, tagsEnricher: tagsEnricher, logger: logger);
}
});
}
Expand All @@ -161,17 +177,28 @@ private static void LogBackgroundFailureTelemetry(
string callerSdkVersion,
string errorCode,
int httpStatusCode,
string rawStsErrorCode = null)
string rawStsErrorCode = null,
MsalException exception = null,
Action<ExecutionResult, IList<KeyValuePair<string, object>>> tagsEnricher = null,
ILoggerAdapter logger = null)
{
var otel = serviceBundle.PlatformProxy.OtelInstrumentation;
var platform = serviceBundle.PlatformProxy.GetProductName();

// Invoke the enricher once for this background failure and reuse the materialized tags
// across both instruments below.
IReadOnlyList<KeyValuePair<string, object>> extraTags = OtelEnrichmentHelper.MaterializeExtraTags(
tagsEnricher,
() => new ExecutionResult { Successful = false, Exception = exception },
logger);

otel.IncrementFailureCounter(
platform, errorCode, apiEvent.ApiId, callerSdkId, callerSdkVersion,
CacheRefreshReason.ProactivelyRefreshed, apiEvent.TokenType, rawStsErrorCode);
CacheRefreshReason.ProactivelyRefreshed, apiEvent.TokenType, rawStsErrorCode,
logger, extraTags);

otel.LogFailureHttpDuration(
platform, apiEvent, httpStatusCode);
platform, apiEvent, httpStatusCode, logger, extraTags);
}

private static Random s_random = new Random();
Expand Down
Loading
Loading