Skip to content
Draft
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 @@ -50,6 +50,14 @@ internal class AcquireTokenCommonParameters
/// </summary>
public Func<string, SafeHandle, string, CancellationToken, Task<string>> AttestationTokenProvider { get; set; }

/// <summary>
/// Optional callback invoked before OpenTelemetry metrics are recorded for a token acquisition.
/// The callback receives an <see cref="TokenAcquisitionResult"/> (carrying either the
/// <see cref="AuthenticationResult"/> on success or the <see cref="Exception"/> on failure)
/// and the mutable tag list, allowing callers to append custom tags to every metric emitted for this request.
/// </summary>
public Action<TokenAcquisitionResult, IList<KeyValuePair<string, object>>> OtelTagsEnricher { get; set; }

/// <summary>
/// This tries to see if the token request should be done over mTLS or over normal HTTP
/// and set the correct parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,29 @@ public static AbstractAcquireTokenParameterBuilder<T> WithFmiPathForClientAssert
}

/// <summary>
/// Specifies extra claims to be included in the client assertion.
/// Registers a callback that is invoked just before OpenTelemetry metrics are recorded for this
/// token acquisition call. Use this to append tags to every metric emitted for the request.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="builder">The builder to chain options to.</param>
/// <param name="otelTagsEnricher">
/// A delegate that receives the <see cref="TokenAcquisitionResult"/> (carrying either the
/// successful <see cref="AuthenticationResult"/> or the failure <see cref="Exception"/>)
/// and a mutable tag list. Add <see cref="KeyValuePair{String, Object}"/> entries to the list
/// to include them as dimensions on all metrics emitted for this call.
Comment on lines 206 to +216
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs for this enricher don’t call out important constraints: avoid adding secrets/PII (e.g., access tokens) to metric tags, keep tag cardinality low, and ensure the delegate is fast and non-throwing (or clarify that MSAL will ignore exceptions). Adding these warnings would help prevent accidental data exposure and high-cardinality metric explosions.

Copilot uses AI. Check for mistakes.
/// </param>
/// <returns>The builder to chain other options to.</returns>
public static AbstractAcquireTokenParameterBuilder<T> WithOtelTagsEnricher<T>(
this AbstractAcquireTokenParameterBuilder<T> builder,
Action<TokenAcquisitionResult, IList<KeyValuePair<string, object>>> otelTagsEnricher)
where T : AbstractAcquireTokenParameterBuilder<T>
{
builder.CommonParameters.OtelTagsEnricher = otelTagsEnricher;
return builder;
}
Comment on lines +219 to +226
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior/API surface (WithOtelTagsEnricher + extra tags propagation) doesn’t appear to have corresponding test coverage. Consider adding/adjusting telemetry unit tests (e.g., in OTelInstrumentationTests) to verify extra tags are appended to all emitted metrics and that enricher exceptions are handled as best-effort.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Specifies extra claims to be included in the client assertion.
/// These claims will be merged with default claims when the client assertion is generated.
/// This lets higher level APIs like Microsoft.Identity.Web provide additional claims for the client assertion.
/// Important: tokens are associated with the extra client assertion claims, which impacts cache lookups.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public AuthenticationRequestParameters(

public ApiEvent.ApiIds ApiId => _commonParameters.ApiId;

public Action<TokenAcquisitionResult, IList<KeyValuePair<string, object>>> OtelTagsEnricher => _commonParameters.OtelTagsEnricher;

public RequestContext RequestContext { get; }

#region Authority
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
return GetAccessTokenAsync(tokenSource.Token, logger);
}, 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 @@ -130,7 +130,8 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
return GetAccessTokenAsync(tokenSource.Token, logger);
}, 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 @@ -141,7 +141,8 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
return RefreshRtOrFetchNewAccessTokenAsync(tokenSource.Token);
}, 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 @@ -106,44 +106,46 @@ public async Task<AuthenticationResult> RunAsync(CancellationToken cancellationT
}
AuthenticationRequestParameters.RequestContext.Logger.ErrorPii(ex);

LogFailureTelemetryToOtel(ex.ErrorCode, apiEvent, apiEvent.CacheInfo);
LogFailureTelemetryToOtel(apiEvent, apiEvent.CacheInfo, ex);
throw;
}
catch (Exception ex)
{
apiEvent.ApiErrorCode = ex.GetType().Name;
AuthenticationRequestParameters.RequestContext.Logger.ErrorPii(ex);

LogFailureTelemetryToOtel(ex.GetType().Name, apiEvent, apiEvent.CacheInfo);
LogFailureTelemetryToOtel(apiEvent, apiEvent.CacheInfo, ex);
throw;
}
}

private void LogSuccessTelemetryToOtel(AuthenticationResult authenticationResult, ApiEvent apiEvent, long durationInUs)
{
// Log metrics
var context = new TokenAcquisitionResult { AuthenticationResult = authenticationResult };
ServiceBundle.PlatformProxy.OtelInstrumentation.LogSuccessMetrics(
ServiceBundle.PlatformProxy.GetProductName(),
apiEvent.ApiId,
apiEvent.CallerSdkApiId,
apiEvent.CallerSdkVersion,
GetCacheLevel(authenticationResult),
durationInUs,
authenticationResult.AuthenticationResultMetadata,
context,
AuthenticationRequestParameters.OtelTagsEnricher,
AuthenticationRequestParameters.RequestContext.Logger);
}

private void LogFailureTelemetryToOtel(string errorCodeToLog, ApiEvent apiEvent, CacheRefreshReason cacheRefreshReason)
private void LogFailureTelemetryToOtel(ApiEvent apiEvent, CacheRefreshReason cacheRefreshReason, Exception exception)
{
// Log metrics
var context = new TokenAcquisitionResult { Exception = exception };
ServiceBundle.PlatformProxy.OtelInstrumentation.LogFailureMetrics(
ServiceBundle.PlatformProxy.GetProductName(),
errorCodeToLog,
apiEvent.ApiId,
apiEvent.CallerSdkApiId,
apiEvent.CallerSdkVersion,
cacheRefreshReason,
apiEvent.TokenType);
apiEvent.TokenType,
context,
AuthenticationRequestParameters.OtelTagsEnricher);
}

private Tuple<string, string> ParseScopesForTelemetry()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ public async Task<AuthenticationResult> ExecuteAsync(CancellationToken cancellat
return RefreshRtOrFailAsync(tokenSource.Token);
}, 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 @@ -83,27 +83,32 @@ 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<TokenAcquisitionResult, IList<KeyValuePair<string, object>>> tagsEnricher = null)
{
_ = Task.Run(async () =>
{
try
{
var authResult = await fetchAction().ConfigureAwait(false);
var context = new TokenAcquisitionResult { AuthenticationResult = authResult };
var extraTags = new List<KeyValuePair<string, object>>();
tagsEnricher?.Invoke(context, extraTags);
Comment on lines +99 to +100
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allocates extraTags and invokes the optional tagsEnricher unconditionally for proactive refresh. If the enricher is null, this is an unnecessary allocation; if the enricher throws, it will fault the fire-and-forget task (risking UnobservedTaskException). Consider allocating/invoking only when non-null and wrapping the callback in try/catch.

Suggested change
var extraTags = new List<KeyValuePair<string, object>>();
tagsEnricher?.Invoke(context, extraTags);
IList<KeyValuePair<string, object>> extraTags = null;
if (tagsEnricher != null)
{
extraTags = new List<KeyValuePair<string, object>>();
try
{
tagsEnricher(context, extraTags);
}
catch (Exception ex)
{
logger.ErrorPiiWithPrefix(ex, "Proactive refresh tags enricher callback failed.");
}
}

Copilot uses AI. Check for mistakes.
serviceBundle.PlatformProxy.OtelInstrumentation.IncrementSuccessCounter(
serviceBundle.PlatformProxy.GetProductName(),
apiEvent.ApiId,
callerSdkId,
callerSdkVersion,
TokenSource.IdentityProvider,
CacheRefreshReason.ProactivelyRefreshed,
TokenSource.IdentityProvider,
CacheRefreshReason.ProactivelyRefreshed,
Cache.CacheLevel.None,
logger,
apiEvent.TokenType);
apiEvent.TokenType,
extraTags);
}
catch (MsalServiceException ex)
{
Expand All @@ -117,38 +122,44 @@ internal static void ProcessFetchInBackground(
logger.ErrorPiiWithPrefix(ex, logMsg);
}

var context = new TokenAcquisitionResult { Exception = ex };
serviceBundle.PlatformProxy.OtelInstrumentation.LogFailureMetrics(
serviceBundle.PlatformProxy.GetProductName(),
ex.ErrorCode,
apiEvent.ApiId,
callerSdkId,
callerSdkVersion,
CacheRefreshReason.ProactivelyRefreshed,
apiEvent.TokenType);
apiEvent.TokenType,
context,
tagsEnricher);
}
catch (OperationCanceledException ex)
{
logger.WarningPiiWithPrefix(ex, ProactiveRefreshCancellationError);
var context = new TokenAcquisitionResult { Exception = ex };
serviceBundle.PlatformProxy.OtelInstrumentation.LogFailureMetrics(
serviceBundle.PlatformProxy.GetProductName(),
ex.GetType().Name,
apiEvent.ApiId,
callerSdkId,
callerSdkVersion,
callerSdkId,
callerSdkVersion,
CacheRefreshReason.ProactivelyRefreshed,
apiEvent.TokenType);
apiEvent.TokenType,
context,
tagsEnricher);
}
catch (Exception ex)
{
logger.ErrorPiiWithPrefix(ex, ProactiveRefreshGeneralError);
var context = new TokenAcquisitionResult { Exception = ex };
serviceBundle.PlatformProxy.OtelInstrumentation.LogFailureMetrics(
serviceBundle.PlatformProxy.GetProductName(),
ex.GetType().Name,
apiEvent.ApiId,
callerSdkId,
callerSdkVersion,
callerSdkId,
callerSdkVersion,
CacheRefreshReason.ProactivelyRefreshed,
apiEvent.TokenType);
apiEvent.TokenType,
context,
tagsEnricher);
}
});
}
Expand Down
Loading
Loading