Skip to content

Commit c00d3f9

Browse files
committed
feat: Enhance telemetry with operation context properties
1 parent 2b3e6cd commit c00d3f9

File tree

4 files changed

+144
-32
lines changed

4 files changed

+144
-32
lines changed

README.md

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ By default, trace telemetry submits:
143143
- **rendered message** in trace's standard *message* property.
144144
- **severity** in trace's standard *severityLevel* property.
145145
- **timestamp** in trace's standard *timestamp* property.
146+
- **operation id** from `operationId` property, `TraceId` property, or `Activity.TraceId` (captured at log time).
147+
- **operation parent id** from `ParentSpanId` property.
148+
- **operation name** from `OperationName` property.
149+
- **component version** from `version` property.
146150
- **messageTemplate** in *customDimensions*.
147151
- **custom log properties** as *customDimensions*.
148152

@@ -151,13 +155,21 @@ Event telemetry submits:
151155
- **message template** as *event name*.
152156
- **renderedMessage** in *customDimensions*.
153157
- **timestamp** in event's standard *timestamp* property.
158+
- **operation id** from `operationId` property, `TraceId` property, or `Activity.TraceId` (captured at log time).
159+
- **operation parent id** from `ParentSpanId` property.
160+
- **operation name** from `OperationName` property.
161+
- **component version** from `version` property.
154162
- **custom log properties** as *customDimensions*.
155163

156164
Exception telemetry submits:
157165

158166
- **exception** as standard AI exception.
159167
- **severity** in trace's standard *severityLevel* property.
160168
- **timestamp** in trace's standard *timestamp* property.
169+
- **operation id** from `operationId` property, `TraceId` property, or `Activity.TraceId` (captured at log time).
170+
- **operation parent id** from `ParentSpanId` property.
171+
- **operation name** from `OperationName` property.
172+
- **component version** from `version` property.
161173
- **custom log properties** as *customDimensions*.
162174

163175
> Note that **log context** properties are also included in *customDimensions* when Serilog is configured
@@ -232,26 +244,26 @@ private class CustomConverter : TraceTelemetryConverter
232244
telemetry.Context.User.Id = logEvent.Properties["UserId"].ToString();
233245
}
234246
// post-process the telemetry's context to contain the operation id
235-
if (logEvent.Properties.ContainsKey("operation_Id"))
247+
if (logEvent.Properties.ContainsKey("operationId"))
236248
{
237-
telemetry.Context.Operation.Id = logEvent.Properties["operation_Id"].ToString();
249+
telemetry.Context.Operation.Id = logEvent.Properties["operationId"].ToString();
238250
}
239251
// post-process the telemetry's context to contain the operation parent id
240-
if (logEvent.Properties.ContainsKey("operation_parentId"))
252+
if (logEvent.Properties.ContainsKey("ParentSpanId"))
241253
{
242-
telemetry.Context.Operation.ParentId = logEvent.Properties["operation_parentId"].ToString();
254+
telemetry.Context.Operation.ParentId = logEvent.Properties["ParentSpanId"].ToString();
243255
}
244256
// typecast to ISupportProperties so you can manipulate the properties as desired
245-
ISupportProperties propTelematry = (ISupportProperties)telemetry;
257+
ISupportProperties propTelemetry = (ISupportProperties)telemetry;
246258

247259
// find redundant properties
248-
var removeProps = new[] { "UserId", "operation_parentId", "operation_Id" };
249-
removeProps = removeProps.Where(prop => propTelematry.Properties.ContainsKey(prop)).ToArray();
260+
var removeProps = new[] { "UserId", "ParentSpanId", "operationId" };
261+
removeProps = removeProps.Where(prop => propTelemetry.Properties.ContainsKey(prop)).ToArray();
250262

251263
foreach (var prop in removeProps)
252264
{
253265
// remove redundant properties
254-
propTelematry.Properties.Remove(prop);
266+
propTelemetry.Properties.Remove(prop);
255267
}
256268

257269
yield return telemetry;
@@ -270,7 +282,7 @@ instance, let's include `renderedMessage` in event telemetry:
270282
```csharp
271283
private class IncludeRenderedMessageConverter : EventTelemetryConverter
272284
{
273-
public override void ForwardPropertiesToTelemetryProperties(LogEvent logEvent,
285+
public override void ForwardPropertiesToTelemetryProperties(LogEvent logEvent,
274286
ISupportProperties telemetryProperties, IFormatProvider formatProvider)
275287
{
276288
base.ForwardPropertiesToTelemetryProperties(logEvent, telemetryProperties, formatProvider,
@@ -325,16 +337,19 @@ _telemetryClient.Flush();
325337
326338
await Task.Delay(1000);
327339

328-
// or
340+
// or
329341
330342
System.Threading.Thread.Sleep(1000);
331343

332344
```
333345

334346
## Including Operation Id
335347

336-
Application Insight's operation id is pushed out if you set `operationId` LogEvent property. If it's present, AI's
337-
operation id will be overridden by the value from this property.
348+
Application Insight's operation id is set from the following sources in order of precedence:
349+
350+
1. `operationId` LogEvent property
351+
2. `TraceId` LogEvent property
352+
3. `Activity.TraceId` (captured at log time)
338353

339354
This can be set like so:
340355

@@ -357,6 +372,23 @@ public class OperationIdEnricher : ILogEventEnricher
357372
Application Insight supports component version and is pushed out if you set `version` log event property. If it's
358373
present, AI's operation version will include the value from this property.
359374

375+
## Using with SerilogTracing
376+
377+
[SerilogTracing](https://github.com/serilog-tracing/serilog-tracing) provides tracing primitives that integrate with Serilog's structured logging. When used with this sink, tracing context is automatically included in Application Insights telemetry.
378+
379+
The following LogEvent properties are mapped to Application Insights telemetry:
380+
381+
| LogEvent Property | Application Insights Telemetry | Notes |
382+
|-------------------|---------------------------------|-------|
383+
| `TraceId` | `Context.Operation.Id` | From TraceId captured in LogEvent |
384+
| `SpanId` | `Id` (for Request/Dependency telemetry) | From SpanId captured in LogEvent |
385+
| `ParentSpanId` | `Context.Operation.ParentId` | |
386+
| `OperationName` | `Context.Operation.Name` | |
387+
| `operationId` | `Context.Operation.Id` | Overrides TraceId |
388+
| `version` | `Context.Component.Version` | |
389+
390+
Precedence for `Context.Operation.Id`: `operationId` property > `TraceId` property > `Activity.TraceId` (when both `operationId` and `TraceId` properties are absent).
391+
360392
## Using with Azure Functions
361393

362394
Azure functions has out of the box integration with Application Insights, which automatically logs functions execution
@@ -377,7 +409,7 @@ namespace MyFunctions
377409
{
378410
public override void Configure(IFunctionsHostBuilder builder)
379411
{
380-
builder.Services.AddSingleton<ILoggerProvider>((sp) =>
412+
builder.Services.AddSingleton<ILoggerProvider>((sp) =>
381413
{
382414
Log.Logger = new LoggerConfiguration()
383415
.Enrich.FromLogContext()
@@ -390,7 +422,7 @@ namespace MyFunctions
390422
}
391423
```
392424

393-
Copyright &copy; 2022 Serilog Contributors - Provided under
425+
Copyright &copy; 2025 Serilog Contributors - Provided under
394426
the [Apache License, Version 2.0](http://apache.org/licenses/LICENSE-2.0.html).
395427

396428
See also: [Serilog Documentation](https://github.com/serilog/serilog/wiki)

src/Serilog.Sinks.ApplicationInsights/Sinks/ApplicationInsights/TelemetryConverters/TelemetryConverterBase.cs

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,26 @@ public abstract class TelemetryConverterBase : ITelemetryConverter
3939
/// </summary>
4040
public const string OperationIdProperty = "operationId";
4141

42+
/// <summary>
43+
/// Property that is included when in log context, will be pushed out as AI trace id.
44+
/// </summary>
45+
public const string TraceIdProperty = "TraceId";
46+
47+
/// <summary>
48+
/// Property that is included when in log context, will be pushed out as AI parent span id.
49+
/// </summary>
50+
public const string ParentSpanIdProperty = "ParentSpanId";
51+
52+
/// <summary>
53+
/// Property that is included when in log context, will be pushed out as AI span id.
54+
/// </summary>
55+
public const string SpanIdProperty = "SpanId";
56+
57+
/// <summary>
58+
/// Property that is included when in log context, will be pushed out as AI operation name.
59+
/// </summary>
60+
public const string OperationNameProperty = "OperationName";
61+
4262
/// <summary>
4363
/// Property that is included when in log context, will be pushed out as AI component version.
4464
/// </summary>
@@ -72,7 +92,8 @@ public virtual ExceptionTelemetry ToExceptionTelemetry(
7292
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
7393
if (logEvent.Exception == null) throw new ArgumentException("Must have an Exception", nameof(logEvent));
7494

75-
var exceptionTelemetry = new ExceptionTelemetry(logEvent.Exception) {
95+
var exceptionTelemetry = new ExceptionTelemetry(logEvent.Exception)
96+
{
7697
SeverityLevel = ToSeverityLevel(logEvent.Level),
7798
Timestamp = logEvent.Timestamp
7899
};
@@ -143,15 +164,36 @@ public void ForwardPropertiesToTelemetryProperties(LogEvent logEvent,
143164

144165
if (telemetryProperties is ITelemetry telemetry)
145166
{
146-
if (logEvent.Properties.TryGetValue(OperationIdProperty, out var operationId))
147-
telemetry.Context.Operation.Id = operationId.ToString().Trim('\"');
148-
else
167+
// Operation.Id (TraceId)
168+
if (logEvent.Properties.TryGetValue(OperationIdProperty, out var operationIdProp))
169+
telemetry.Context.Operation.Id = operationIdProp.ToString().Trim('"');
170+
else if (logEvent.Properties.TryGetValue(TraceIdProperty, out var traceIdProp))
171+
telemetry.Context.Operation.Id = traceIdProp.ToString().Trim('"');
172+
else if (logEvent.TraceId is ActivityTraceId traceId)
173+
telemetry.Context.Operation.Id = traceId.ToHexString();
174+
175+
// Operation.ParentId (ParentSpanId)
176+
if (logEvent.Properties.TryGetValue(ParentSpanIdProperty, out var parentSpanIdProp))
177+
telemetry.Context.Operation.ParentId = parentSpanIdProp.ToString().Trim('"');
178+
179+
// Operation.Name (OperationName)
180+
if (logEvent.Properties.TryGetValue(OperationNameProperty, out var operationNameProp))
181+
telemetry.Context.Operation.Name = operationNameProp.ToString().Trim('"');
182+
183+
// Set Id for RequestTelemetry and DependencyTelemetry
184+
if (logEvent.Properties.TryGetValue(SpanIdProperty, out var spanIdProp))
149185
{
150-
if (logEvent.TraceId is ActivityTraceId traceId)
151-
telemetry.Context.Operation.Id = traceId.ToHexString();
152-
153-
if (logEvent.SpanId is ActivitySpanId spanId)
154-
telemetry.Context.Operation.ParentId = spanId.ToHexString();
186+
if (telemetry is RequestTelemetry req)
187+
req.Id = spanIdProp.ToString().Trim('"');
188+
else if (telemetry is DependencyTelemetry dep)
189+
dep.Id = spanIdProp.ToString().Trim('"');
190+
}
191+
else if (logEvent.SpanId is ActivitySpanId spanId)
192+
{
193+
if (telemetry is RequestTelemetry req)
194+
req.Id = spanId.ToHexString();
195+
else if (telemetry is DependencyTelemetry dep)
196+
dep.Id = spanId.ToHexString();
155197
}
156198

157199
if (logEvent.Properties.TryGetValue(VersionProperty, out var version)
@@ -188,4 +230,4 @@ public void ForwardPropertiesToTelemetryProperties(LogEvent logEvent,
188230

189231
return null;
190232
}
191-
}
233+
}

test/Serilog.Sinks.ApplicationInsights.Tests/EventTelemetryConverterTest.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,19 @@ public void DestructuredPropertyIsFormattedCorrectly()
4040
}
4141

4242
[Fact]
43-
public void TraceIdAndSpanIdDefaultByDefault()
43+
public void TraceIdIsNullByDefault()
4444
{
4545
Logger.Information("Hello, {Name}!", "world");
4646
Assert.Null(LastSubmittedEventTelemetry.Context.Operation.Id);
47-
Assert.Null(LastSubmittedEventTelemetry.Context.Operation.ParentId);
4847
}
4948

5049
[Fact]
51-
public void TraceIdAndSpanIdAreSet()
50+
public void TraceIdIsSet()
5251
{
5352
using Activity activity = new("TestActivity");
5453
activity.Start();
5554
Logger.Information("Hello, {Name}!", "world");
5655
Assert.Equal(activity.TraceId.ToHexString(), LastSubmittedEventTelemetry.Context.Operation.Id);
57-
Assert.Equal(activity.SpanId.ToHexString(), LastSubmittedEventTelemetry.Context.Operation.ParentId);
5856
}
5957

6058
[Fact]
@@ -67,4 +65,25 @@ public void OperationIdTakesPrecedenceOverTraceId()
6765
Assert.Equal(operationId, LastSubmittedEventTelemetry.Context.Operation.Id);
6866
Assert.Null(LastSubmittedEventTelemetry.Context.Operation.ParentId);
6967
}
68+
69+
[Fact]
70+
public void ParentSpanIdIsSet()
71+
{
72+
Logger.Information("Test {ParentSpanId}", "parent123");
73+
Assert.Equal("parent123", LastSubmittedEventTelemetry.Context.Operation.ParentId);
74+
}
75+
76+
[Fact]
77+
public void OperationNameIsSet()
78+
{
79+
Logger.Information("Test {OperationName}", "MyOperation");
80+
Assert.Equal("MyOperation", LastSubmittedEventTelemetry.Context.Operation.Name);
81+
}
82+
83+
[Fact]
84+
public void VersionIsSet()
85+
{
86+
Logger.Information("Test {version}", "1.2.3");
87+
Assert.Equal("1.2.3", LastSubmittedEventTelemetry.Context.Component.Version);
88+
}
7089
}

test/Serilog.Sinks.ApplicationInsights.Tests/TraceTelemetryConverterTest.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,19 @@ public void DestructuredPropertyIsFormattedCorrectly()
4040
}
4141

4242
[Fact]
43-
public void TraceIdAndSpanIdDefaultByDefault()
43+
public void TraceIdIsNullByDefault()
4444
{
4545
Logger.Information("Hello, {Name}!", "world");
4646
Assert.Null(LastSubmittedTraceTelemetry.Context.Operation.Id);
47-
Assert.Null(LastSubmittedTraceTelemetry.Context.Operation.ParentId);
4847
}
4948

5049
[Fact]
51-
public void TraceIdAndSpanIdAreSet()
50+
public void TraceIdIsSet()
5251
{
5352
using Activity activity = new("TestActivity");
5453
activity.Start();
5554
Logger.Information("Hello, {Name}!", "world");
5655
Assert.Equal(activity.TraceId.ToHexString(), LastSubmittedTraceTelemetry.Context.Operation.Id);
57-
Assert.Equal(activity.SpanId.ToHexString(), LastSubmittedTraceTelemetry.Context.Operation.ParentId);
5856
}
5957

6058
[Fact]
@@ -67,4 +65,25 @@ public void OperationIdTakesPrecedenceOverTraceId()
6765
Assert.Equal(operationId, LastSubmittedTraceTelemetry.Context.Operation.Id);
6866
Assert.Null(LastSubmittedTraceTelemetry.Context.Operation.ParentId);
6967
}
68+
69+
[Fact]
70+
public void ParentSpanIdIsSet()
71+
{
72+
Logger.Information("Test {ParentSpanId}", "parent123");
73+
Assert.Equal("parent123", LastSubmittedTraceTelemetry.Context.Operation.ParentId);
74+
}
75+
76+
[Fact]
77+
public void OperationNameIsSet()
78+
{
79+
Logger.Information("Test {OperationName}", "MyOperation");
80+
Assert.Equal("MyOperation", LastSubmittedTraceTelemetry.Context.Operation.Name);
81+
}
82+
83+
[Fact]
84+
public void VersionIsSet()
85+
{
86+
Logger.Information("Test {version}", "1.2.3");
87+
Assert.Equal("1.2.3", LastSubmittedTraceTelemetry.Context.Component.Version);
88+
}
7089
}

0 commit comments

Comments
 (0)