Skip to content

Commit 4b14902

Browse files
Add support for Availability telemetry in Azure Monitor OpenTelemetry Exporter
1 parent c984023 commit 4b14902

File tree

7 files changed

+561
-23
lines changed

7 files changed

+561
-23
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Monitor.OpenTelemetry.Exporter.Internals;
5+
using OpenTelemetry.Logs;
6+
7+
namespace Azure.Monitor.OpenTelemetry.Exporter.Models
8+
{
9+
internal partial class AvailabilityData
10+
{
11+
public AvailabilityData(int version, AvailabilityInfo availabilityInfo, ChangeTrackingDictionary<string, string> properties, LogRecord logRecord)
12+
: this(version, availabilityInfo.Id, availabilityInfo.Name, availabilityInfo.Duration, availabilityInfo.Success)
13+
{
14+
RunLocation = availabilityInfo.RunLocation;
15+
Message = availabilityInfo.Message;
16+
Properties = properties;
17+
Measurements = new ChangeTrackingDictionary<string, double>();
18+
}
19+
}
20+
}

sdk/monitor/Azure.Monitor.OpenTelemetry.Exporter/src/Internals/CustomerSdkStats/TelemetrySchemaTypeCounter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ internal sealed class TelemetrySchemaTypeCounter
1111
internal int _eventCount;
1212
internal int _metricCount;
1313
internal int _traceCount;
14+
internal int _availabilityCount;
1415
}
1516
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@ internal static void IncrementCounterByType(TelemetrySchemaTypeCounter telemetry
340340
case "Message":
341341
telemetrySchemaTypeCounter._traceCount++;
342342
break;
343+
case "Availability":
344+
telemetrySchemaTypeCounter._availabilityCount++;
345+
break;
343346
// Unknown types are not tracked
344347
}
345348
}
@@ -464,13 +467,17 @@ private static void DecrementCounterByType(TelemetrySchemaTypeCounter telemetryS
464467
case "Message":
465468
telemetrySchemaTypeCounter._traceCount = Math.Max(0, telemetrySchemaTypeCounter._traceCount - 1);
466469
break;
470+
case "Availability":
471+
telemetrySchemaTypeCounter._availabilityCount = Math.Max(0, telemetrySchemaTypeCounter._availabilityCount - 1);
472+
break;
467473
}
468474
}
469475

470476
private static bool HasAnyCount(TelemetrySchemaTypeCounter telemetrySchemaTypeCounter)
471477
{
472478
return telemetrySchemaTypeCounter._requestCount > 0 || telemetrySchemaTypeCounter._dependencyCount > 0 || telemetrySchemaTypeCounter._exceptionCount > 0 ||
473-
telemetrySchemaTypeCounter._eventCount > 0 || telemetrySchemaTypeCounter._metricCount > 0 || telemetrySchemaTypeCounter._traceCount > 0;
479+
telemetrySchemaTypeCounter._eventCount > 0 || telemetrySchemaTypeCounter._metricCount > 0 || telemetrySchemaTypeCounter._traceCount > 0 ||
480+
telemetrySchemaTypeCounter._availabilityCount > 0;
474481
}
475482
}
476483
}

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

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ internal static class LogsHelper
2222
{
2323
private const string CustomEventAttributeName = "microsoft.custom_event.name";
2424
private const string ClientIpAttributeName = "microsoft.client.ip";
25+
private const string AvailabilityIdAttributeName = "microsoft.availability.id";
26+
private const string AvailabilityNameAttributeName = "microsoft.availability.name";
27+
private const string AvailabilityDurationAttributeName = "microsoft.availability.duration";
28+
private const string AvailabilitySuccessAttributeName = "microsoft.availability.success";
29+
private const string AvailabilityRunLocationAttributeName = "microsoft.availability.runLocation";
30+
private const string AvailabilityMessageAttributeName = "microsoft.availability.message";
2531
private const int Version = 2;
2632
private static readonly Action<LogRecordScope, IDictionary<string, string>> s_processScope = (scope, properties) =>
2733
{
@@ -61,7 +67,7 @@ internal static (List<TelemetryItem> TelemetryItems, TelemetrySchemaTypeCounter
6167
try
6268
{
6369
var properties = new ChangeTrackingDictionary<string, string>();
64-
ProcessLogRecordProperties(logRecord, properties, out string? message, out string? eventName, out string? microsoftClientIp);
70+
ProcessLogRecordProperties(logRecord, properties, out string? message, out string? eventName, out string? microsoftClientIp, out AvailabilityInfo? availabilityInfo);
6571

6672
if (logRecord.Exception is not null)
6773
{
@@ -75,6 +81,18 @@ internal static (List<TelemetryItem> TelemetryItems, TelemetrySchemaTypeCounter
7581
};
7682
telemetrySchemaTypeCounter._exceptionCount++;
7783
}
84+
else if (availabilityInfo is not null)
85+
{
86+
telemetryItem = new TelemetryItem("Availability", logRecord, resource, instrumentationKey, microsoftClientIp)
87+
{
88+
Data = new MonitorBase
89+
{
90+
BaseType = "AvailabilityData",
91+
BaseData = new AvailabilityData(Version, availabilityInfo.Value, properties, logRecord),
92+
}
93+
};
94+
telemetrySchemaTypeCounter._availabilityCount++;
95+
}
7896
else if (eventName is not null)
7997
{
8098
telemetryItem = new TelemetryItem("Event", logRecord, resource, instrumentationKey, microsoftClientIp)
@@ -111,19 +129,24 @@ internal static (List<TelemetryItem> TelemetryItems, TelemetrySchemaTypeCounter
111129
return (telemetryItems, telemetrySchemaTypeCounter);
112130
}
113131

114-
internal static void ProcessLogRecordProperties(LogRecord logRecord, IDictionary<string, string> properties, out string? message, out string? eventName, out string? microsoftClientIp)
132+
internal static void ProcessLogRecordProperties(LogRecord logRecord, IDictionary<string, string> properties, out string? message, out string? eventName, out string? microsoftClientIp, out AvailabilityInfo? availabilityInfo)
115133
{
116134
eventName = null;
135+
availabilityInfo = null;
117136
message = logRecord.Exception?.Message ?? logRecord.FormattedMessage;
118137
microsoftClientIp = null;
138+
bool hasAvailabilityData = false;
119139

120140
foreach (KeyValuePair<string, object?> item in logRecord.Attributes ?? Enumerable.Empty<KeyValuePair<string, object?>>())
121141
{
122142
if (item.Key == CustomEventAttributeName)
123143
{
124144
eventName = item.Value?.ToString();
125145
}
126-
146+
else if (item.Key == AvailabilityNameAttributeName)
147+
{
148+
hasAvailabilityData = true;
149+
}
127150
else if (item.Key == ClientIpAttributeName)
128151
{
129152
microsoftClientIp = item.Value?.ToString().Truncate(SchemaConstants.MessageData_Properties_MaxValueLength);
@@ -159,9 +182,15 @@ internal static void ProcessLogRecordProperties(LogRecord logRecord, IDictionary
159182
}
160183
}
161184

185+
// If we detected availability data, do a second pass to extract all availability attributes
186+
if (hasAvailabilityData)
187+
{
188+
availabilityInfo = ExtractAvailabilityInfo(logRecord, message, out microsoftClientIp);
189+
}
190+
162191
logRecord.ForEachScope(s_processScope, properties);
163192

164-
if (eventName is null) // we will omit the following properties if we've detected a custom event.
193+
if (eventName is null && availabilityInfo is null) // we will omit the following properties if we've detected a custom event or availability.
165194
{
166195
var categoryName = logRecord.CategoryName;
167196
if (!properties.ContainsKey("CategoryName") && !string.IsNullOrEmpty(categoryName))
@@ -181,6 +210,66 @@ internal static void ProcessLogRecordProperties(LogRecord logRecord, IDictionary
181210
}
182211
}
183212

213+
private static AvailabilityInfo? ExtractAvailabilityInfo(LogRecord logRecord, string? message, out string? microsoftClientIp)
214+
{
215+
string? availabilityId = null;
216+
string? availabilityName = null;
217+
string? availabilityDuration = null;
218+
string? availabilitySuccess = null;
219+
string? availabilityRunLocation = null;
220+
string? availabilityMessage = null;
221+
microsoftClientIp = null;
222+
223+
foreach (KeyValuePair<string, object?> item in logRecord.Attributes ?? Enumerable.Empty<KeyValuePair<string, object?>>())
224+
{
225+
if (item.Key == AvailabilityIdAttributeName)
226+
{
227+
availabilityId = item.Value?.ToString();
228+
}
229+
else if (item.Key == AvailabilityNameAttributeName)
230+
{
231+
availabilityName = item.Value?.ToString();
232+
}
233+
else if (item.Key == AvailabilityDurationAttributeName)
234+
{
235+
availabilityDuration = item.Value?.ToString();
236+
}
237+
else if (item.Key == AvailabilitySuccessAttributeName)
238+
{
239+
availabilitySuccess = item.Value?.ToString();
240+
}
241+
else if (item.Key == AvailabilityRunLocationAttributeName)
242+
{
243+
availabilityRunLocation = item.Value?.ToString();
244+
}
245+
else if (item.Key == AvailabilityMessageAttributeName)
246+
{
247+
availabilityMessage = item.Value?.ToString();
248+
}
249+
else if (item.Key == ClientIpAttributeName)
250+
{
251+
microsoftClientIp = item.Value?.ToString().Truncate(SchemaConstants.MessageData_Properties_MaxValueLength);
252+
}
253+
}
254+
255+
// Construct availability info if we have required fields
256+
if (!string.IsNullOrEmpty(availabilityId) && !string.IsNullOrEmpty(availabilityName) &&
257+
!string.IsNullOrEmpty(availabilityDuration) && !string.IsNullOrEmpty(availabilitySuccess))
258+
{
259+
return new AvailabilityInfo
260+
{
261+
Id = availabilityId!,
262+
Name = availabilityName!,
263+
Duration = availabilityDuration!,
264+
Success = bool.Parse(availabilitySuccess),
265+
RunLocation = availabilityRunLocation,
266+
Message = availabilityMessage ?? message
267+
};
268+
}
269+
270+
return null;
271+
}
272+
184273
internal static string GetProblemId(Exception exception)
185274
{
186275
string methodName = "UnknownMethod";
@@ -244,4 +333,17 @@ internal static SeverityLevel GetSeverityLevel(LogLevel logLevel)
244333
}
245334
}
246335
}
336+
337+
/// <summary>
338+
/// Struct to hold availability test information extracted from log attributes.
339+
/// </summary>
340+
internal struct AvailabilityInfo
341+
{
342+
public string Id { get; set; }
343+
public string Name { get; set; }
344+
public string Duration { get; set; }
345+
public bool Success { get; set; }
346+
public string? RunLocation { get; set; }
347+
public string? Message { get; set; }
348+
}
247349
}

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,77 @@ public static void AssertCustomEventTelemetry(
120120
Assert.Equal(expectedProperties.Count, eventData.Properties.Count);
121121
}
122122

123+
public static void AssertAvailabilityTelemetry(
124+
TelemetryItem telemetryItem,
125+
string expectedId,
126+
string expectedName,
127+
string expectedDuration,
128+
bool expectedSuccess,
129+
string? expectedRunLocation = null,
130+
string? expectedMessage = null,
131+
IDictionary<string, string>? expectedProperties = null,
132+
string? expectedSpanId = null,
133+
string? expectedTraceId = null,
134+
string? expectedClientIp = null,
135+
string expectedCloudRole = "[testNamespace]/testName",
136+
string expectedCloudInstance = "testInstance",
137+
string expectedApplicationVersion = "testVersion")
138+
{
139+
Assert.Equal("Availability", telemetryItem.Name); // telemetry type
140+
Assert.Equal("AvailabilityData", telemetryItem.Data.BaseType); // telemetry data type
141+
Assert.Equal(2, telemetryItem.Data.BaseData.Version); // telemetry api version
142+
Assert.Equal("00000000-0000-0000-0000-000000000000", telemetryItem.InstrumentationKey);
143+
144+
var expectedTagsCount = 4;
145+
146+
if (expectedSpanId != null && expectedTraceId != null)
147+
{
148+
expectedTagsCount += 2;
149+
150+
Assert.Equal(expectedSpanId, telemetryItem.Tags["ai.operation.parentId"]);
151+
Assert.Equal(expectedTraceId, telemetryItem.Tags["ai.operation.id"]);
152+
}
153+
154+
if (expectedClientIp != null)
155+
{
156+
expectedTagsCount += 1;
157+
Assert.Equal(expectedClientIp, telemetryItem.Tags["ai.location.ip"]);
158+
}
159+
160+
Assert.Equal(expectedTagsCount, telemetryItem.Tags.Count);
161+
Assert.Equal(expectedCloudRole, telemetryItem.Tags["ai.cloud.role"]);
162+
Assert.Equal(expectedApplicationVersion, telemetryItem.Tags["ai.application.ver"]);
163+
Assert.Equal(expectedCloudInstance, telemetryItem.Tags["ai.cloud.roleInstance"]);
164+
Assert.Contains("ai.internal.sdkVersion", telemetryItem.Tags.Keys);
165+
166+
var availabilityData = (AvailabilityData)telemetryItem.Data.BaseData;
167+
168+
Assert.Equal(expectedId, availabilityData.Id);
169+
Assert.Equal(expectedName, availabilityData.Name);
170+
Assert.Equal(expectedDuration, availabilityData.Duration);
171+
Assert.Equal(expectedSuccess, availabilityData.Success);
172+
173+
if (expectedRunLocation != null)
174+
{
175+
Assert.Equal(expectedRunLocation, availabilityData.RunLocation);
176+
}
177+
178+
if (expectedMessage != null)
179+
{
180+
Assert.Equal(expectedMessage, availabilityData.Message);
181+
}
182+
183+
if (expectedProperties != null && expectedProperties.Count > 0)
184+
{
185+
foreach (var prop in expectedProperties)
186+
{
187+
Assert.Equal(prop.Value, availabilityData.Properties[prop.Key]);
188+
}
189+
190+
Assert.Equal(expectedProperties.Count, availabilityData.Properties.Count);
191+
}
192+
}
193+
123194
public static void AssertLog_As_ExceptionTelemetry(
124195
TelemetryItem telemetryItem,
125196
string expectedSeverityLevel,

0 commit comments

Comments
 (0)