Skip to content

Commit 0f7fd2f

Browse files
author
Timothy Mothra
authored
[AzureMonitor] Add support for CustomEvents (Azure#48378)
* initial POC * fix ordering * cleanup else condition * cleanup * update CommonTestFramework * CustomEventsTests * unit tests * update tests * update readme * cleanup * cleanup * cleanup * remove test class no longer needed * changelog * pr feedback
1 parent 372aa89 commit 0f7fd2f

File tree

15 files changed

+680
-118
lines changed

15 files changed

+680
-118
lines changed

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
### Features Added
66

7+
* Added support for emitting Application Insights Custom Events.
8+
([#48378](https://github.com/Azure/azure-sdk-for-net/pull/48378))
9+
710
### Breaking Changes
811

912
### Bugs Fixed

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ The [OpenTelemetry .NET](https://github.com/open-telemetry/opentelemetry-dotnet)
1111

1212
### Migrating from Application Insights SDK
1313

14-
If you are currently using the Application Insights SDK and want to migrate to OpenTelemetry, please follow our [migration guide](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-dotnet-migrate?tabs=console).
14+
If you are currently using the Application Insights SDK and want to migrate to OpenTelemetry, please follow our [migration guide](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-dotnet-migrate?tabs=console).
1515

1616
### Already using OpenTelemetry?
1717

1818
If you are currently using OpenTelemetry and want to send telemetry data to Azure Monitor, please follow our [getting started guide](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=net).
1919

2020
### Install the package
2121

22-
#### Latest Version: [![Nuget](https://img.shields.io/nuget/vpre/Azure.Monitor.OpenTelemetry.Exporter.svg)](https://www.nuget.org/packages/Azure.Monitor.OpenTelemetry.Exporter/)
22+
#### Latest Version: [![Nuget](https://img.shields.io/nuget/vpre/Azure.Monitor.OpenTelemetry.Exporter.svg)](https://www.nuget.org/packages/Azure.Monitor.OpenTelemetry.Exporter/)
2323

2424
Install the Azure Monitor Exporter for OpenTelemetry .NET with [NuGet](https://www.nuget.org/):
2525
```dotnetcli
@@ -103,10 +103,10 @@ There are two options to enable AAD authentication. Note that if both have been
103103

104104
Some key concepts for .NET include:
105105

106-
- [Overview of .NET distributed tracing](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing):
107-
Distributed tracing is a diagnostic technique that helps engineers localize failures and performance issues within applications, especially those that may be distributed across multiple machines or processes.
106+
- [Overview of .NET distributed tracing](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing):
107+
Distributed tracing is a diagnostic technique that helps engineers localize failures and performance issues within applications, especially those that may be distributed across multiple machines or processes.
108108

109-
- [Overview of Logging in .NET](https://learn.microsoft.com/dotnet/core/extensions/logging):
109+
- [Overview of Logging in .NET](https://learn.microsoft.com/dotnet/core/extensions/logging):
110110
.NET supports a logging API that works with a variety of built-in and third-party logging providers.
111111

112112
Some key concepts for Azure Monitor include:
@@ -125,10 +125,10 @@ Some key concepts for OpenTelemetry include:
125125
The ability to call the OpenTelemetry API directly by any application is
126126
facilitated by instrumentation. A library that enables OpenTelemetry observability for another library is called an Instrumentation Library.
127127

128-
- [Tracing Signal](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#tracing-signal):
128+
- [Tracing Signal](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#tracing-signal):
129129
Trace refers to distributed tracing. It can be thought of as a directed acyclic graph (DAG) of Spans, where the edges between Spans are defined as parent/child relationship.
130130

131-
- [Sampling](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#sampling):
131+
- [Sampling](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk.md#sampling):
132132
Sampling is a mechanism to control the noise and overhead introduced by OpenTelemetry by reducing the number of samples of traces collected and sent to the backend.
133133

134134
- [Metric Signal](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#metric-signal):

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Customizations/Models/MessageData.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ namespace Azure.Monitor.OpenTelemetry.Exporter.Models
1212
{
1313
internal partial class MessageData
1414
{
15-
public MessageData(int version, LogRecord logRecord) : base(version)
15+
public MessageData(int version, LogRecord logRecord, string? message, ChangeTrackingDictionary<string, string> properties) : base(version)
1616
{
17-
Properties = new ChangeTrackingDictionary<string, string>();
17+
Properties = properties;
1818
Measurements = new ChangeTrackingDictionary<string, double>();
19-
Message = LogsHelper.GetMessageAndSetProperties(logRecord, Properties).Truncate(SchemaConstants.MessageData_Message_MaxLength);
19+
Message = message?.Truncate(SchemaConstants.MessageData_Message_MaxLength);
20+
2021
#pragma warning disable CS0618 // Type or member is obsolete
2122
// TODO: Remove warning disable with next Stable release.
2223
SeverityLevel = LogsHelper.GetSeverityLevel(logRecord.LogLevel);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using OpenTelemetry.Logs;
5+
6+
namespace Azure.Monitor.OpenTelemetry.Exporter.Models
7+
{
8+
internal partial class TelemetryEventData
9+
{
10+
public TelemetryEventData(int version, string name, ChangeTrackingDictionary<string, string> properties, string? message, LogRecord logRecord) : base(version)
11+
{
12+
Name = name;
13+
Properties = properties;
14+
Measurements = new ChangeTrackingDictionary<string, double>();
15+
}
16+
}
17+
}

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Customizations/Models/TelemetryExceptionData.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Globalization;
7-
8-
using Azure.Core;
97
using Azure.Monitor.OpenTelemetry.Exporter.Internals;
10-
118
using OpenTelemetry.Logs;
129

1310
namespace Azure.Monitor.OpenTelemetry.Exporter.Models
@@ -16,18 +13,16 @@ internal partial class TelemetryExceptionData
1613
{
1714
internal const int MaxExceptionCountToSave = 10;
1815

19-
public TelemetryExceptionData(int version, LogRecord logRecord) : base(version)
16+
public TelemetryExceptionData(int version, LogRecord logRecord, string? message, ChangeTrackingDictionary<string, string> properties) : base(version)
2017
{
2118
if (logRecord.Exception == null)
2219
{
2320
throw new ArgumentNullException(nameof(logRecord), "logRecord.Exception cannot be null.");
2421
}
2522

26-
Properties = new ChangeTrackingDictionary<string, string>();
23+
Properties = properties;
2724
Measurements = new ChangeTrackingDictionary<string, double>();
2825

29-
var message = LogsHelper.GetMessageAndSetProperties(logRecord, Properties);
30-
3126
#pragma warning disable CS0618 // Type or member is obsolete
3227
// TODO: Remove warning disable with next Stable release.
3328
SeverityLevel = LogsHelper.GetSeverityLevel(logRecord.LogLevel);

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Customizations/Models/TelemetryItem.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ public TelemetryItem(string name, TelemetryItem telemetryItem, ActivitySpanId ac
9191
}
9292
}
9393

94-
public TelemetryItem (LogRecord logRecord, AzureMonitorResource? resource, string instrumentationKey) :
95-
this(logRecord.Exception != null ? "Exception" : "Message", FormatUtcTimestamp(logRecord.Timestamp))
94+
public TelemetryItem (string name, LogRecord logRecord, AzureMonitorResource? resource, string instrumentationKey) :
95+
this(name, FormatUtcTimestamp(logRecord.Timestamp))
9696
{
9797
if (logRecord.TraceId != default)
9898
{

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/LogsHelper.cs

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace Azure.Monitor.OpenTelemetry.Exporter.Internals
1919
{
2020
internal static class LogsHelper
2121
{
22+
private const string CustomEventAttributeName = "microsoft.custom_event.name";
2223
private const int Version = 2;
2324
private static readonly Action<LogRecordScope, IDictionary<string, string>> s_processScope = (scope, properties) =>
2425
{
@@ -56,21 +57,40 @@ internal static List<TelemetryItem> OtelToAzureMonitorLogs(Batch<LogRecord> batc
5657
{
5758
try
5859
{
59-
telemetryItem = new TelemetryItem(logRecord, resource, instrumentationKey);
60-
if (logRecord.Exception != null)
60+
var properties = new ChangeTrackingDictionary<string, string>();
61+
ProcessLogRecordProperties(logRecord, properties, out string? message, out string? eventName);
62+
63+
if (logRecord.Exception is not null)
64+
{
65+
telemetryItem = new TelemetryItem("Exception", logRecord, resource, instrumentationKey)
66+
{
67+
Data = new MonitorBase
68+
{
69+
BaseType = "ExceptionData",
70+
BaseData = new TelemetryExceptionData(Version, logRecord, message, properties),
71+
}
72+
};
73+
}
74+
else if (eventName is not null)
6175
{
62-
telemetryItem.Data = new MonitorBase
76+
telemetryItem = new TelemetryItem("Event", logRecord, resource, instrumentationKey)
6377
{
64-
BaseType = "ExceptionData",
65-
BaseData = new TelemetryExceptionData(Version, logRecord),
78+
Data = new MonitorBase
79+
{
80+
BaseType = "EventData",
81+
BaseData = new TelemetryEventData(Version, eventName, properties, message, logRecord),
82+
}
6683
};
6784
}
6885
else
6986
{
70-
telemetryItem.Data = new MonitorBase
87+
telemetryItem = new TelemetryItem("Message", logRecord, resource, instrumentationKey)
7188
{
72-
BaseType = "MessageData",
73-
BaseData = new MessageData(Version, logRecord),
89+
Data = new MonitorBase
90+
{
91+
BaseType = "MessageData",
92+
BaseData = new MessageData(Version, logRecord, message, properties),
93+
}
7494
};
7595
}
7696

@@ -85,14 +105,19 @@ internal static List<TelemetryItem> OtelToAzureMonitorLogs(Batch<LogRecord> batc
85105
return telemetryItems;
86106
}
87107

88-
internal static string? GetMessageAndSetProperties(LogRecord logRecord, IDictionary<string, string> properties)
108+
internal static void ProcessLogRecordProperties(LogRecord logRecord, IDictionary<string, string> properties, out string? message, out string? eventName)
89109
{
90-
string? message = logRecord.Exception?.Message ?? logRecord.FormattedMessage;
110+
eventName = null;
111+
message = logRecord.Exception?.Message ?? logRecord.FormattedMessage;
91112

92113
foreach (KeyValuePair<string, object?> item in logRecord.Attributes ?? Enumerable.Empty<KeyValuePair<string, object?>>())
93114
{
115+
if (item.Key == CustomEventAttributeName)
116+
{
117+
eventName = item.Value?.ToString();
118+
}
94119
// Note: if Key exceeds MaxLength, the entire KVP will be dropped.
95-
if (item.Key.Length <= SchemaConstants.MessageData_Properties_MaxKeyLength && item.Value != null)
120+
else if (item.Key.Length <= SchemaConstants.MessageData_Properties_MaxKeyLength && item.Value != null)
96121
{
97122
try
98123
{
@@ -124,23 +149,24 @@ internal static List<TelemetryItem> OtelToAzureMonitorLogs(Batch<LogRecord> batc
124149

125150
logRecord.ForEachScope(s_processScope, properties);
126151

127-
var categoryName = logRecord.CategoryName;
128-
if (!properties.ContainsKey("CategoryName") && !string.IsNullOrEmpty(categoryName))
152+
if (eventName is null) // we will omit the following properties if we've detected a custom event.
129153
{
130-
properties.Add("CategoryName", categoryName.Truncate(SchemaConstants.KVP_MaxValueLength)!);
131-
}
154+
var categoryName = logRecord.CategoryName;
155+
if (!properties.ContainsKey("CategoryName") && !string.IsNullOrEmpty(categoryName))
156+
{
157+
properties.Add("CategoryName", categoryName.Truncate(SchemaConstants.KVP_MaxValueLength)!);
158+
}
132159

133-
if (!properties.ContainsKey("EventId") && logRecord.EventId.Id != 0)
134-
{
135-
properties.Add("EventId", logRecord.EventId.Id.ToString(CultureInfo.InvariantCulture));
136-
}
160+
if (!properties.ContainsKey("EventId") && logRecord.EventId.Id != 0)
161+
{
162+
properties.Add("EventId", logRecord.EventId.Id.ToString(CultureInfo.InvariantCulture));
163+
}
137164

138-
if (!properties.ContainsKey("EventName") && !string.IsNullOrEmpty(logRecord.EventId.Name))
139-
{
140-
properties.Add("EventName", logRecord.EventId.Name!.Truncate(SchemaConstants.KVP_MaxValueLength));
165+
if (!properties.ContainsKey("EventName") && !string.IsNullOrEmpty(logRecord.EventId.Name))
166+
{
167+
properties.Add("EventName", logRecord.EventId.Name!.Truncate(SchemaConstants.KVP_MaxValueLength));
168+
}
141169
}
142-
143-
return message;
144170
}
145171

146172
internal static string GetProblemId(Exception exception)

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/Azure.Monitor.OpenTelemetry.Exporter.Tests.csproj

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="xunit" />
9-
<PackageReference Include="xunit.runner.visualstudio" />
8+
<!--TODO: REMOVE REFERENCE TO Microsoft.Extensions.Telemetry-->
9+
<PackageReference Include="Microsoft.Extensions.Telemetry" VersionOverride="9.3.0" />
1010
<PackageReference Include="Microsoft.NET.Test.Sdk" />
1111
<PackageReference Include="Moq" />
12+
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
1213
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
14+
<PackageReference Include="xunit" />
15+
<PackageReference Include="xunit.runner.visualstudio" />
1316
</ItemGroup>
1417

1518
<ItemGroup>
1619
<ProjectReference Include="$(AzureCoreTestFramework)" />
1720
<ProjectReference Include="..\..\src\Azure.Monitor.OpenTelemetry.Exporter.csproj" />
18-
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
1921
</ItemGroup>
2022
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace Azure.Monitor.OpenTelemetry.Exporter.Tests.CommonTestFramework
7+
{
8+
// TODO: REMOVE THIS PRAGMA AFTER Microsoft.Extensions.Telemetry SHIPS THE TagName API AS STABLE.
9+
// https://github.com/dotnet/extensions/issues/5825
10+
#pragma warning disable EXTEXP0003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
11+
/// <summary>
12+
/// This class has ILogger extensions for use in unit tests.
13+
/// </summary>
14+
public static partial class CustomEventLoggerExtensions
15+
{
16+
[LoggerMessage(level: LogLevel.Information, Message = "{key1}")]
17+
public static partial void WriteSimpleLog(this ILogger logger, string key1);
18+
19+
[LoggerMessage(level: LogLevel.Information, Message = "{microsoft.custom_event.name}")]
20+
public static partial void WriteSimpleCustomEvent(this ILogger logger, [TagName("microsoft.custom_event.name")] string customEventName);
21+
22+
[LoggerMessage(level: LogLevel.Information, Message = "{microsoft.custom_event.name} {key1} {key2}")]
23+
public static partial void WriteCustomEventWithAdditionalProperties(this ILogger logger, [TagName("microsoft.custom_event.name")] string customEventName, string key1, string key2);
24+
}
25+
#pragma warning restore EXTEXP0003
26+
}

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/tests/Azure.Monitor.OpenTelemetry.Exporter.Tests/CommonTestFramework/TelemetryItemOutputHelper.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,26 @@ private void WriteBaseData(TelemetryItem telemetryItem)
7070
case "ExceptionData":
7171
WriteExceptionData((TelemetryExceptionData)baseData);
7272
break;
73+
case "EventData":
74+
WriteEventData((TelemetryEventData)baseData);
75+
break;
7376
default:
7477
output.WriteLine($"***WriteBaseData not implemented for '{baseType}'***");
7578
break;
7679
}
7780
}
7881

82+
private void WriteEventData(TelemetryEventData eventData)
83+
{
84+
output.WriteLine($"Name: {eventData.Name}");
85+
86+
output.WriteLine($"Properties: {eventData.Properties.Count}");
87+
foreach (var prop in eventData.Properties)
88+
{
89+
output.WriteLine($"\t{prop.Key}: {prop.Value}");
90+
}
91+
}
92+
7993
private void WriteExceptionData(TelemetryExceptionData exceptionData)
8094
{
8195
output.WriteLine($"SeverityLevel: {exceptionData.SeverityLevel}");

0 commit comments

Comments
 (0)