Skip to content

Commit ba41658

Browse files
ssmelovCopilotneha-bhargava
authored
Expose canonical tag names per-metric (#6076)
* Expose canonical tag names per-metric * Move MsalMetricsCatalog to Extensibiliry namespace * Adjust the comments --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Neha Bhargava <61847233+neha-bhargava@users.noreply.github.com>
1 parent 1360db1 commit ba41658

9 files changed

Lines changed: 338 additions & 36 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.ObjectModel;
7+
using Microsoft.Identity.Client.TelemetryCore;
8+
9+
namespace Microsoft.Identity.Client.Extensibility
10+
{
11+
///
12+
/// Describes the OpenTelemetry metrics that MSAL emits and, for each metric, the canonical
13+
/// (MSAL-owned) tag names it records. Consumers such as downstream metric pipelines can use
14+
/// to discover which tags belong to a given MSAL metric and,
15+
/// for example, keep only those tags.
16+
///
17+
///
18+
///
19+
/// The tag names listed for a metric are the canonical base tags MSAL emits for it (some are conditional). They do not
20+
/// include any extra tags supplied through
21+
/// ,
22+
/// which are caller-defined and therefore not part of this catalog. Some canonical tags are emitted only
23+
/// under certain conditions (for example the raw STS error-code tag is present on the failure counter only
24+
/// when the STS returns a sub-error); they are listed here because they are part of the metric's canonical
25+
/// schema.
26+
///
27+
///
28+
/// Both the metric names (the dictionary keys) and the tag names (the dictionary values) are plain strings
29+
/// that match what MSAL records, so consumers can compare them directly against the metric and tag names
30+
/// observed on the OpenTelemetry pipeline, then drive per-metric tag filtering from this mapping (for
31+
/// example via OpenTelemetry Views and MetricStreamConfiguration.TagKeys). Keeping the mapping here
32+
/// means it stays in sync with MSAL automatically: updating the MSAL package reference updates the canonical
33+
/// tag set, with nothing to maintain on the consumer side.
34+
///
35+
///
36+
public static class MsalMetricsCatalog
37+
{
38+
// Metric names are defined once here (internal) so the published catalog and the instruments created in
39+
// OtelInstrumentation share a single source of truth and cannot drift. They are intentionally not part of
40+
// the public surface: consumers read metric names from the keys of CanonicalTagsByMetric (and from the
41+
// OpenTelemetry pipeline), so there is no need to expose them as separate public constants.
42+
internal const string SuccessCounterName = "MsalSuccess";
43+
internal const string FailureCounterName = "MsalFailure";
44+
internal const string TotalDurationHistogramName = "MsalTotalDuration.1A";
45+
internal const string TotalDurationV2HistogramName = "MsalTotalDurationV2.1A";
46+
internal const string DurationInL1CacheHistogramName = "MsalDurationInL1CacheInUs.1B";
47+
internal const string DurationInL2CacheHistogramName = "MsalDurationInL2Cache.1A";
48+
internal const string DurationInHttpHistogramName = "MsalDurationInHttp.1A";
49+
internal const string DurationInHttpV2HistogramName = "MsalDurationInHttpV2.1A";
50+
internal const string DurationInExtensionHistogramName = "MsalDurationInExtensionInMs.1B";
51+
internal const string RemainingTokenLifetimeHistogramName = "MsalRemainingTokenLifetime.1A";
52+
53+
///
54+
/// Maps each MSAL metric name to the read-only list of canonical tag names that metric records.
55+
/// Keys are compared with .
56+
///
57+
public static IReadOnlyDictionary<string, IReadOnlyList<string>> CanonicalTagsByMetric { get; } =
58+
BuildCanonicalTagsByMetric();
59+
60+
private static IReadOnlyDictionary<string, IReadOnlyList<string>> BuildCanonicalTagsByMetric()
61+
{
62+
// Tag names reference the internal TelemetryConstants used when recording, so the mapping cannot
63+
// drift from the tags MSAL actually emits.
64+
var map = new Dictionary<string, string[]>(StringComparer.Ordinal)
65+
{
66+
[SuccessCounterName] = new[]
67+
{
68+
TelemetryConstants.MsalVersion,
69+
TelemetryConstants.Platform,
70+
TelemetryConstants.ApiId,
71+
TelemetryConstants.CallerSdkId,
72+
TelemetryConstants.TokenSource,
73+
TelemetryConstants.CacheRefreshReason,
74+
TelemetryConstants.CacheLevel,
75+
TelemetryConstants.TokenType,
76+
},
77+
[FailureCounterName] = new[]
78+
{
79+
TelemetryConstants.MsalVersion,
80+
TelemetryConstants.Platform,
81+
TelemetryConstants.ErrorCode,
82+
TelemetryConstants.ApiId,
83+
TelemetryConstants.CallerSdkId,
84+
TelemetryConstants.CacheRefreshReason,
85+
TelemetryConstants.TokenType,
86+
TelemetryConstants.RawStsErrorCode,
87+
},
88+
[TotalDurationHistogramName] = new[]
89+
{
90+
TelemetryConstants.MsalVersion,
91+
TelemetryConstants.Platform,
92+
TelemetryConstants.ApiId,
93+
TelemetryConstants.TokenSource,
94+
TelemetryConstants.CacheLevel,
95+
TelemetryConstants.CacheRefreshReason,
96+
TelemetryConstants.TokenType,
97+
},
98+
[TotalDurationV2HistogramName] = new[]
99+
{
100+
TelemetryConstants.MsalVersionPlatform,
101+
TelemetryConstants.ApiId,
102+
TelemetryConstants.TokenSource,
103+
TelemetryConstants.CacheLevel,
104+
TelemetryConstants.CacheRefreshReason,
105+
TelemetryConstants.TokenType,
106+
TelemetryConstants.ErrorCode,
107+
TelemetryConstants.Succeeded,
108+
},
109+
[DurationInL1CacheHistogramName] = new[]
110+
{
111+
TelemetryConstants.MsalVersion,
112+
TelemetryConstants.Platform,
113+
TelemetryConstants.ApiId,
114+
TelemetryConstants.TokenSource,
115+
TelemetryConstants.CacheLevel,
116+
TelemetryConstants.CacheRefreshReason,
117+
},
118+
[DurationInL2CacheHistogramName] = new[]
119+
{
120+
TelemetryConstants.MsalVersion,
121+
TelemetryConstants.Platform,
122+
TelemetryConstants.ApiId,
123+
TelemetryConstants.CacheRefreshReason,
124+
},
125+
[DurationInHttpHistogramName] = new[]
126+
{
127+
TelemetryConstants.MsalVersion,
128+
TelemetryConstants.Platform,
129+
TelemetryConstants.ApiId,
130+
TelemetryConstants.TokenType,
131+
},
132+
[DurationInHttpV2HistogramName] = new[]
133+
{
134+
TelemetryConstants.MsalVersionPlatform,
135+
TelemetryConstants.ApiId,
136+
TelemetryConstants.TokenType,
137+
TelemetryConstants.HttpStatusCode,
138+
},
139+
[DurationInExtensionHistogramName] = new[]
140+
{
141+
TelemetryConstants.MsalVersion,
142+
TelemetryConstants.Platform,
143+
TelemetryConstants.ApiId,
144+
TelemetryConstants.TokenSource,
145+
TelemetryConstants.CacheLevel,
146+
TelemetryConstants.TokenType,
147+
},
148+
[RemainingTokenLifetimeHistogramName] = new[]
149+
{
150+
TelemetryConstants.MsalVersionPlatform,
151+
TelemetryConstants.ApiId,
152+
TelemetryConstants.TokenSource,
153+
TelemetryConstants.CacheLevel,
154+
TelemetryConstants.CacheRefreshReason,
155+
TelemetryConstants.TokenType,
156+
},
157+
};
158+
159+
// Expose each tag list as a ReadOnlyCollection rather than the backing array: an IReadOnlyList that is
160+
// actually a string[] can be cast back and mutated by a consumer, which would corrupt this shared
161+
// static catalog process-wide. The dictionary itself is already read-only.
162+
var readOnlyMap = new Dictionary<string, IReadOnlyList<string>>(map.Count, StringComparer.Ordinal);
163+
foreach (KeyValuePair<string, string[]> entry in map)
164+
{
165+
readOnlyMap[entry.Key] = Array.AsReadOnly(entry.Value);
166+
}
167+
168+
return new ReadOnlyDictionary<string, IReadOnlyList<string>>(readOnlyMap);
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)