-
Notifications
You must be signed in to change notification settings - Fork 295
Expand file tree
/
Copy pathTelemetryClientExtensions.cs
More file actions
324 lines (284 loc) · 14.5 KB
/
TelemetryClientExtensions.cs
File metadata and controls
324 lines (284 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
namespace Microsoft.ApplicationInsights
{
using System;
using System.ComponentModel;
using System.Diagnostics;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.Implementation;
using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing;
/// <summary>
/// Extension class providing operation lifecycle helpers for <see cref="TelemetryClient"/>.
/// Enables automatic correlation of telemetry with <see cref="Activity"/> instances.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public static class TelemetryClientExtensions
{
/// <summary>
/// Starts a new telemetry operation (e.g., request, dependency, etc.) with the specified name.
/// Creates and starts an <see cref="Activity"/> that defines the operation context.
/// </summary>
/// <typeparam name="T">The type of telemetry item (e.g., <see cref="RequestTelemetry"/> or <see cref="DependencyTelemetry"/>).</typeparam>
/// <param name="telemetryClient">The <see cref="TelemetryClient"/> instance used to create and track the operation.</param>
/// <param name="operationName">The operation name, used as the <see cref="Activity.DisplayName"/>.</param>
/// <returns>
/// An <see cref="IOperationHolder{T}"/> that holds the telemetry and the corresponding activity.
/// Disposing this object stops the operation and sends telemetry automatically.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="telemetryClient"/> is null.</exception>
public static IOperationHolder<T> StartOperation<T>(
this TelemetryClient telemetryClient, string operationName)
where T : OperationTelemetry, new()
{
return StartOperation<T>(telemetryClient, operationName, operationId: null, parentOperationId: null);
}
/// <summary>
/// Starts a new telemetry operation with a specific operation and parent operation ID for correlation.
/// </summary>
/// <typeparam name="T">The type of telemetry item (e.g., <see cref="RequestTelemetry"/> or <see cref="DependencyTelemetry"/>).</typeparam>
/// <param name="telemetryClient">The <see cref="TelemetryClient"/> instance used to create and track the operation.</param>
/// <param name="operationName">The name of the operation to create.</param>
/// <param name="operationId">The W3C trace ID (32-character hex string) to use for the operation.</param>
/// <param name="parentOperationId">The optional parent span ID (16-character hex string) to correlate with the parent operation.</param>
/// <returns>
/// An <see cref="IOperationHolder{T}"/> containing the telemetry item and associated <see cref="Activity"/>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="telemetryClient"/> is null.</exception>
public static IOperationHolder<T> StartOperation<T>(
this TelemetryClient telemetryClient,
string operationName,
string operationId,
string parentOperationId = null)
where T : OperationTelemetry, new()
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(telemetryClient);
#else
if (telemetryClient == null)
{
throw new ArgumentNullException(nameof(telemetryClient));
}
#endif
telemetryClient.Configuration.FeatureReporter.MarkFeatureInUse(Internal.StatsbeatFeatures.StartOperation);
var effectiveName = string.IsNullOrEmpty(operationName) ? typeof(T).Name : operationName;
var kind = ResolveActivityKind<T>();
var source = telemetryClient.TelemetryConfiguration.ApplicationInsightsActivitySource;
ActivityContext parentContext = default;
Activity savedActivity = null;
if (!string.IsNullOrEmpty(operationId) && operationId.Length == 32)
{
try
{
var traceId = ActivityTraceId.CreateFromString(operationId.AsSpan());
var spanId = !string.IsNullOrEmpty(parentOperationId) && parentOperationId.Length == 16
? ActivitySpanId.CreateFromString(parentOperationId.AsSpan())
: default(ActivitySpanId);
parentContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded);
// When creating a root operation (spanId is default), suppress ambient activity
// to prevent it from becoming the parent
if (spanId == default && Activity.Current != null)
{
savedActivity = Activity.Current;
Activity.Current = null;
}
}
catch
{
// ignore malformed IDs
}
}
var activity = source.StartActivity(effectiveName, kind, parentContext);
if (activity == null)
{
return new OperationHolder<T>(
telemetryClient,
new T { Name = effectiveName, Timestamp = DateTimeOffset.UtcNow },
null);
}
// Store the operation name as a tag for retrieval
activity.SetOperationName(effectiveName);
var telemetry = new T
{
Name = effectiveName,
Timestamp = DateTimeOffset.UtcNow,
Id = activity.SpanId.ToHexString(),
};
telemetry.Context.Operation.Id = activity.TraceId.ToHexString();
// CHANGE: Only set ParentId if it's not default (all zeros)
telemetry.Context.Operation.ParentId = activity.ParentSpanId != default
? activity.ParentSpanId.ToHexString()
: null;
telemetry.Context.Operation.Name = effectiveName;
return new OperationHolder<T>(telemetryClient, telemetry, activity, savedActivity);
}
/// <summary>
/// Starts a telemetry operation using an existing telemetry object.
/// This overload is useful when the telemetry item is pre-populated with metadata or custom properties.
/// </summary>
/// <typeparam name="T">The type of telemetry item.</typeparam>
/// <param name="telemetryClient">The <see cref="TelemetryClient"/> used to create and track the operation.</param>
/// <param name="operationTelemetry">The telemetry item to associate with the new operation.</param>
/// <returns>
/// An <see cref="IOperationHolder{T}"/> containing the provided telemetry and its associated activity.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="telemetryClient"/> is null.</exception>
public static IOperationHolder<T> StartOperation<T>(
this TelemetryClient telemetryClient,
T operationTelemetry)
where T : OperationTelemetry
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(telemetryClient);
ArgumentNullException.ThrowIfNull(operationTelemetry);
#else
if (telemetryClient == null)
{
throw new ArgumentNullException(nameof(telemetryClient));
}
if (operationTelemetry == null)
{
throw new ArgumentNullException(nameof(operationTelemetry));
}
#endif
if (string.IsNullOrEmpty(operationTelemetry.Name))
{
operationTelemetry.Name = typeof(T).Name;
}
telemetryClient.Configuration.FeatureReporter.MarkFeatureInUse(Internal.StatsbeatFeatures.StartOperation);
var kind = ResolveActivityKind<T>();
var source = telemetryClient.TelemetryConfiguration.ApplicationInsightsActivitySource;
ActivityContext parentContext = default;
Activity savedActivity = null;
if (!string.IsNullOrEmpty(operationTelemetry.Context?.Operation?.Id) &&
operationTelemetry.Context.Operation.Id.Length == 32)
{
try
{
var traceId = ActivityTraceId.CreateFromString(operationTelemetry.Context.Operation.Id.AsSpan());
var spanId = !string.IsNullOrEmpty(operationTelemetry.Context.Operation.ParentId) &&
operationTelemetry.Context.Operation.ParentId.Length == 16
? ActivitySpanId.CreateFromString(operationTelemetry.Context.Operation.ParentId.AsSpan())
: default(ActivitySpanId);
parentContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded);
// When creating a root operation (spanId is default), suppress ambient activity
// to prevent it from becoming the parent
if (spanId == default && Activity.Current != null)
{
savedActivity = Activity.Current;
Activity.Current = null;
}
}
catch
{
// ignore malformed IDs
}
}
var activity = source.StartActivity(operationTelemetry.Name, kind, parentContext);
if (activity == null)
{
return new OperationHolder<T>(telemetryClient, operationTelemetry, null);
}
// Store the operation name as a tag for retrieval
activity.SetOperationName(operationTelemetry.Name);
operationTelemetry.Timestamp = DateTimeOffset.UtcNow;
operationTelemetry.Id = activity.SpanId.ToHexString();
operationTelemetry.Context.Operation.Id = activity.TraceId.ToHexString();
operationTelemetry.Context.Operation.ParentId = activity.ParentSpanId != default
? activity.ParentSpanId.ToHexString()
: null;
operationTelemetry.Context.Operation.Name = operationTelemetry.Name;
return new OperationHolder<T>(telemetryClient, operationTelemetry, activity, savedActivity);
}
/// <summary>
/// Starts a telemetry operation based on an existing <see cref="Activity"/> instance that carries trace context.
/// The activity must have been created by the caller or extracted from incoming telemetry.
/// </summary>
/// <typeparam name="T">The type of telemetry item (request, dependency, etc.).</typeparam>
/// <param name="telemetryClient">The <see cref="TelemetryClient"/> that will manage and emit the telemetry.</param>
/// <param name="activity">The existing <see cref="Activity"/> to associate with this operation.</param>
/// <returns>An <see cref="IOperationHolder{T}"/> linking telemetry and activity for unified tracking.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="telemetryClient"/> or <paramref name="activity"/> is null.</exception>
public static IOperationHolder<T> StartOperation<T>(
this TelemetryClient telemetryClient,
Activity activity)
where T : OperationTelemetry, new()
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(telemetryClient);
#else
if (telemetryClient == null)
{
throw new ArgumentNullException(nameof(telemetryClient));
}
#endif
if (activity == null)
{
return null;
}
telemetryClient.Configuration.FeatureReporter.MarkFeatureInUse(Internal.StatsbeatFeatures.StartOperation);
// if already started activity, we just link it — not create a new one
if (activity.Id == null)
{
activity.SetIdFormat(ActivityIdFormat.W3C);
activity.Start();
}
var telemetry = new T
{
Name = activity.DisplayName ?? typeof(T).Name,
Timestamp = activity.StartTimeUtc == default ? DateTimeOffset.UtcNow : activity.StartTimeUtc,
};
// Copy baggage (cross-process context)
foreach (var item in activity.Baggage)
{
if (!telemetry.Properties.ContainsKey(item.Key))
{
telemetry.Properties[item.Key] = item.Value;
}
}
foreach (var kvp in activity.Tags)
{
telemetry.Properties[kvp.Key] = kvp.Value?.ToString();
}
return new OperationHolder<T>(telemetryClient, telemetry, activity);
}
/// <summary>
/// Stops a telemetry operation started by <see cref="StartOperation{T}(TelemetryClient, string)"/> or its overloads.
/// Disposes the operation holder, stopping the associated activity and emitting telemetry.
/// </summary>
/// <typeparam name="T">The type of telemetry item being tracked.</typeparam>
/// <param name="telemetryClient">The <see cref="TelemetryClient"/> used to stop the operation.</param>
/// <param name="operation">The <see cref="IOperationHolder{T}"/> representing the operation to stop.</param>
public static void StopOperation<T>(
this TelemetryClient telemetryClient,
IOperationHolder<T> operation)
where T : OperationTelemetry
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(telemetryClient);
#else
if (telemetryClient == null)
{
throw new ArgumentNullException(nameof(telemetryClient));
}
#endif
if (operation == null)
{
CoreEventSource.Log.OperationIsNullWarning();
return;
}
operation.Dispose(); // ensures Activity.Stop() is called
}
private static ActivityKind ResolveActivityKind<T>() where T : OperationTelemetry
{
if (typeof(T) == typeof(RequestTelemetry))
{
return ActivityKind.Server;
}
if (typeof(T) == typeof(DependencyTelemetry))
{
return ActivityKind.Client;
}
return ActivityKind.Internal;
}
}
}