Skip to content

Commit ba1bb38

Browse files
Azure Monitor Exporter - Match HTTP URL retrieval to OpenTelemetry specification (#14834)
1 parent 09b6c38 commit ba1bb38

File tree

14 files changed

+573
-147
lines changed

14 files changed

+573
-147
lines changed

sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/ActivityExtensions.cs

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,11 @@
22
// Licensed under the MIT License.
33

44
using System.Diagnostics;
5-
using OpenTelemetry.Trace;
65

76
namespace OpenTelemetry.Exporter.AzureMonitor
87
{
98
internal static class ActivityExtensions
109
{
11-
private const string StatusCode_0 = "0";
12-
private const string StatusCode_200 = "200";
13-
private const string StatusCode_400 = "400";
14-
private const string StatusCode_401 = "401";
15-
private const string StatusCode_403 = "403";
16-
private const string StatusCode_404 = "404";
17-
private const string StatusCode_409 = "409";
18-
private const string StatusCode_412 = "412";
19-
private const string StatusCode_500 = "500";
20-
private const string StatusCode_501 = "501";
21-
private const string StatusCode_503 = "503";
22-
private const string StatusCode_504 = "504";
23-
2410
internal static TelemetryType GetTelemetryType(this Activity activity)
2511
{
2612
var kind = activity.Kind switch
@@ -34,30 +20,5 @@ internal static TelemetryType GetTelemetryType(this Activity activity)
3420

3521
return kind;
3622
}
37-
38-
// TODO: Change the return type to integer once .NET support it.
39-
internal static string GetStatusCode(this Activity activity)
40-
{
41-
var status = activity.GetStatus().CanonicalCode switch
42-
{
43-
StatusCanonicalCode.Cancelled => StatusCode_400,
44-
StatusCanonicalCode.InvalidArgument => StatusCode_400,
45-
StatusCanonicalCode.DeadlineExceeded => StatusCode_504,
46-
StatusCanonicalCode.NotFound => StatusCode_404,
47-
StatusCanonicalCode.AlreadyExists => StatusCode_409,
48-
StatusCanonicalCode.PermissionDenied => StatusCode_403,
49-
StatusCanonicalCode.ResourceExhausted => StatusCode_409,
50-
StatusCanonicalCode.FailedPrecondition => StatusCode_412,
51-
StatusCanonicalCode.OutOfRange => StatusCode_400,
52-
StatusCanonicalCode.Unimplemented => StatusCode_501,
53-
StatusCanonicalCode.Internal => StatusCode_500,
54-
StatusCanonicalCode.Unavailable => StatusCode_503,
55-
StatusCanonicalCode.Unauthenticated => StatusCode_401,
56-
StatusCanonicalCode.Ok => StatusCode_200,
57-
_ => StatusCode_0
58-
};
59-
60-
return status;
61-
}
6223
}
6324
}

sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/AzureMonitorTransmitter.cs

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
using System.Diagnostics;
77
using System.Globalization;
88
using System.Linq;
9+
using System.Runtime.CompilerServices;
910
using System.Threading;
1011
using System.Threading.Tasks;
1112
using Azure.Core.Pipeline;
1213

1314
using OpenTelemetry.Exporter.AzureMonitor.ConnectionString;
1415
using OpenTelemetry.Exporter.AzureMonitor.Models;
15-
using OpenTelemetry.Trace;
1616

1717
namespace OpenTelemetry.Exporter.AzureMonitor
1818
{
@@ -101,42 +101,47 @@ private static TelemetryEnvelope GeneratePartAEnvelope(Activity activity)
101101

102102
private MonitorBase GenerateTelemetryData(Activity activity)
103103
{
104-
MonitorBase telemetry = new MonitorBase();
105-
106104
var telemetryType = activity.GetTelemetryType();
107-
telemetry.BaseType = Telemetry_Base_Type_Mapping[telemetryType];
108-
string url = GetHttpUrl(activity.Tags);
105+
var tags = activity.Tags.ToAzureMonitorTags(out var activityType);
106+
MonitorBase telemetry = new MonitorBase
107+
{
108+
BaseType = Telemetry_Base_Type_Mapping[telemetryType]
109+
};
109110

110111
if (telemetryType == TelemetryType.Request)
111112
{
112-
var request = new RequestData(2, activity.Context.SpanId.ToHexString(), activity.Duration.ToString("c", CultureInfo.InvariantCulture), activity.GetStatus().IsOk, activity.GetStatusCode())
113+
var url = activity.Kind == ActivityKind.Server ? UrlHelper.GetUrl(tags) : GetMessagingUrl(tags);
114+
var statusCode = GetHttpStatusCode(tags);
115+
var success = GetSuccessFromHttpStatusCode(statusCode);
116+
var request = new RequestData(2, activity.Context.SpanId.ToHexString(), activity.Duration.ToString("c", CultureInfo.InvariantCulture), success, statusCode)
113117
{
114118
Name = activity.DisplayName,
115119
Url = url,
116120
// TODO: Handle request.source.
117121
};
118122

119-
// TODO: Handle activity.TagObjects
120-
ExtractPropertiesFromTags(request.Properties, activity.Tags);
123+
// TODO: Handle activity.TagObjects, extract well-known tags
124+
// ExtractPropertiesFromTags(request.Properties, activity.Tags);
121125

122126
telemetry.BaseData = request;
123127
}
124128
else if (telemetryType == TelemetryType.Dependency)
125129
{
126130
var dependency = new RemoteDependencyData(2, activity.DisplayName, activity.Duration)
127131
{
128-
Id = activity.Context.SpanId.ToHexString(),
129-
Success = activity.GetStatus().IsOk
132+
Id = activity.Context.SpanId.ToHexString()
130133
};
131134

132135
// TODO: Handle activity.TagObjects
133-
ExtractPropertiesFromTags(dependency.Properties, activity.Tags);
136+
// ExtractPropertiesFromTags(dependency.Properties, activity.Tags);
134137

135-
if (url != null)
138+
if (activityType == PartBType.Http)
136139
{
137-
dependency.Data = url;
140+
dependency.Data = UrlHelper.GetUrl(tags);
138141
dependency.Type = "HTTP"; // TODO: Parse for storage / SB.
139-
dependency.ResultCode = activity.GetStatusCode();
142+
var statusCode = GetHttpStatusCode(tags);
143+
dependency.ResultCode = statusCode;
144+
dependency.Success = GetSuccessFromHttpStatusCode(statusCode);
140145
}
141146

142147
// TODO: Handle dependency.target.
@@ -146,30 +151,27 @@ private MonitorBase GenerateTelemetryData(Activity activity)
146151
return telemetry;
147152
}
148153

149-
private static string GetHttpUrl(IEnumerable<KeyValuePair<string, string>> tags)
154+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
155+
private static string GetHttpStatusCode(Dictionary<string, string> tags)
150156
{
151-
var httpTags = tags.Where(item => item.Key.StartsWith("http.", StringComparison.InvariantCulture))
152-
.ToDictionary(item => item.Key, item => item.Value);
153-
154-
155-
httpTags.TryGetValue(SemanticConventions.AttributeHttpUrl, out var url);
156-
if (url != null)
157+
if (tags.TryGetValue(SemanticConventions.AttributeHttpStatusCode, out var status))
157158
{
158-
return url;
159+
return status;
159160
}
160161

161-
httpTags.TryGetValue(SemanticConventions.AttributeHttpHost, out var httpHost);
162-
163-
if (httpHost != null)
164-
{
165-
httpTags.TryGetValue(SemanticConventions.AttributeHttpScheme, out var httpScheme);
166-
httpTags.TryGetValue(SemanticConventions.AttributeHttpTarget, out var httpTarget);
167-
url = httpScheme + httpHost + httpTarget;
168-
return url;
169-
}
162+
return "0";
163+
}
170164

171-
// TODO: Follow spec - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client
165+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
166+
private static bool GetSuccessFromHttpStatusCode(string statusCode)
167+
{
168+
return statusCode == "200" || statusCode == "Ok";
169+
}
172170

171+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
172+
private static string GetMessagingUrl(Dictionary<string, string> tags)
173+
{
174+
tags.TryGetValue(SemanticConventions.AttributeMessagingUrl, out var url);
173175
return url;
174176
}
175177

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+
namespace OpenTelemetry.Exporter.AzureMonitor
5+
{
6+
internal enum PartBType
7+
{
8+
Unknown,
9+
Http,
10+
Db,
11+
Messaging,
12+
Rpc,
13+
FaaS,
14+
Net
15+
};
16+
17+
}

sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/SemanticConventions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ internal static class SemanticConventions
114114

115115
public const string AttributeFaasTrigger = "faas.trigger";
116116
public const string AttributeFaasExecution = "faas.execution";
117+
public const string AttributeFaasColdStart = "faas.coldstart";
117118
public const string AttributeFaasDocumentCollection = "faas.document.collection";
118119
public const string AttributeFaasDocumentOperation = "faas.document.operation";
119120
public const string AttributeFaasDocumentTime = "faas.document.time";
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
6+
namespace OpenTelemetry.Exporter.AzureMonitor
7+
{
8+
internal static class TagsExtension
9+
{
10+
private static readonly IReadOnlyDictionary<string, PartBType> Part_B_Mapping = new Dictionary<string, PartBType>()
11+
{
12+
[SemanticConventions.AttributeDbSystem] = PartBType.Db,
13+
[SemanticConventions.AttributeDbConnectionString] = PartBType.Db,
14+
[SemanticConventions.AttributeDbUser] = PartBType.Db,
15+
16+
[SemanticConventions.AttributeHttpMethod] = PartBType.Http,
17+
[SemanticConventions.AttributeHttpUrl] = PartBType.Http,
18+
[SemanticConventions.AttributeHttpStatusCode] = PartBType.Http,
19+
[SemanticConventions.AttributeHttpScheme] = PartBType.Http,
20+
[SemanticConventions.AttributeHttpHost] = PartBType.Http,
21+
[SemanticConventions.AttributeHttpTarget] = PartBType.Http,
22+
23+
[SemanticConventions.AttributeNetPeerName] = PartBType.Net,
24+
[SemanticConventions.AttributeNetPeerIp] = PartBType.Net,
25+
[SemanticConventions.AttributeNetPeerPort] = PartBType.Net,
26+
[SemanticConventions.AttributeNetTransport] = PartBType.Net,
27+
[SemanticConventions.AttributeNetHostIp] = PartBType.Net,
28+
[SemanticConventions.AttributeNetHostPort] = PartBType.Net,
29+
[SemanticConventions.AttributeNetHostName] = PartBType.Net,
30+
31+
[SemanticConventions.AttributeRpcSystem] = PartBType.Rpc,
32+
[SemanticConventions.AttributeRpcService] = PartBType.Rpc,
33+
[SemanticConventions.AttributeRpcMethod] = PartBType.Rpc,
34+
35+
[SemanticConventions.AttributeFaasTrigger] = PartBType.FaaS,
36+
[SemanticConventions.AttributeFaasExecution] = PartBType.FaaS,
37+
[SemanticConventions.AttributeFaasColdStart] = PartBType.FaaS,
38+
[SemanticConventions.AttributeFaasDocumentCollection] = PartBType.FaaS,
39+
[SemanticConventions.AttributeFaasDocumentOperation] = PartBType.FaaS,
40+
[SemanticConventions.AttributeFaasDocumentTime] = PartBType.FaaS,
41+
[SemanticConventions.AttributeFaasDocumentName] = PartBType.FaaS,
42+
[SemanticConventions.AttributeFaasCron] = PartBType.FaaS,
43+
[SemanticConventions.AttributeFaasTime] = PartBType.FaaS,
44+
45+
[SemanticConventions.AttributeMessagingSystem] = PartBType.Messaging,
46+
[SemanticConventions.AttributeMessagingDestination] = PartBType.Messaging,
47+
[SemanticConventions.AttributeMessagingDestinationKind] = PartBType.Messaging,
48+
[SemanticConventions.AttributeMessagingTempDestination] = PartBType.Messaging,
49+
[SemanticConventions.AttributeMessagingUrl] = PartBType.Messaging
50+
};
51+
52+
internal static Dictionary<string, string> ToAzureMonitorTags(this IEnumerable<KeyValuePair<string, string>> tags, out PartBType activityType)
53+
{
54+
Dictionary<string, string> partBTags = new Dictionary<string, string>();
55+
activityType = PartBType.Unknown;
56+
57+
foreach (var entry in tags)
58+
{
59+
// TODO: May need to store unknown to write to properties as Part C
60+
if ((activityType == PartBType.Unknown || activityType == PartBType.Net) && !Part_B_Mapping.TryGetValue(entry.Key, out activityType))
61+
{
62+
if (activityType == PartBType.Net)
63+
{
64+
partBTags.Add(entry.Key, entry.Value);
65+
activityType = PartBType.Unknown;
66+
}
67+
68+
continue;
69+
}
70+
71+
if (Part_B_Mapping.TryGetValue(entry.Key, out var tempActivityType) && (tempActivityType == activityType || tempActivityType == PartBType.Net))
72+
{
73+
partBTags.Add(entry.Key, entry.Value);
74+
}
75+
}
76+
77+
return partBTags;
78+
}
79+
80+
}
81+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.Runtime.CompilerServices;
7+
8+
namespace OpenTelemetry.Exporter.AzureMonitor
9+
{
10+
internal class UrlHelper
11+
{
12+
private const string SchemePostfix = "://";
13+
private const string Colon = ":";
14+
15+
/// <summary>
16+
/// This method follows OpenTelemetry specification to retrieve http URL.
17+
/// Reference: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client.
18+
/// </summary>
19+
/// <param name="tags">Activity Tags</param>
20+
/// <returns></returns>
21+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
22+
internal static string GetUrl(Dictionary<string, string> tags)
23+
{
24+
if (tags.TryGetValue(SemanticConventions.AttributeHttpUrl, out var url))
25+
{
26+
if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri) && uri.IsAbsoluteUri)
27+
{
28+
return url;
29+
}
30+
}
31+
32+
if (tags.TryGetValue(SemanticConventions.AttributeHttpScheme, out var httpScheme))
33+
{
34+
tags.TryGetValue(SemanticConventions.AttributeHttpTarget, out var httpTarget);
35+
if (tags.TryGetValue(SemanticConventions.AttributeHttpHost, out var httpHost) && !string.IsNullOrWhiteSpace(httpHost))
36+
{
37+
tags.TryGetValue(SemanticConventions.AttributeHttpHostPort, out var httpPort);
38+
if (httpPort != null && httpPort != "80" && httpPort != "443")
39+
{
40+
url = $"{httpScheme}{SchemePostfix}{httpHost}{Colon}{httpPort}{httpTarget}";
41+
}
42+
else
43+
{
44+
url = $"{httpScheme}{SchemePostfix}{httpHost}{httpTarget}";
45+
}
46+
47+
return url;
48+
}
49+
else if (tags.TryGetValue(SemanticConventions.AttributeNetPeerName, out var netPeerName)
50+
&& tags.TryGetValue(SemanticConventions.AttributeNetPeerPort, out var netPeerPort))
51+
{
52+
return string.IsNullOrWhiteSpace(netPeerName) ? null : $"{httpScheme}{SchemePostfix}{netPeerName}{(string.IsNullOrWhiteSpace(netPeerPort) ? null : Colon)}{netPeerPort}{httpTarget}";
53+
}
54+
else if (tags.TryGetValue(SemanticConventions.AttributeNetPeerIp, out var netPeerIP)
55+
&& tags.TryGetValue(SemanticConventions.AttributeNetPeerPort, out netPeerPort))
56+
{
57+
return string.IsNullOrWhiteSpace(netPeerIP) ? null : $"{httpScheme}{SchemePostfix}{netPeerIP}{(string.IsNullOrWhiteSpace(netPeerPort) ? null : Colon)}{netPeerPort}{httpTarget}";
58+
}
59+
}
60+
61+
if (tags.TryGetValue(SemanticConventions.AttributeHttpHost, out var host) && !string.IsNullOrWhiteSpace(host))
62+
{
63+
tags.TryGetValue(SemanticConventions.AttributeHttpTarget, out var httpTarget);
64+
tags.TryGetValue(SemanticConventions.AttributeHttpHostPort, out var httpPort);
65+
url = $"{host}{(string.IsNullOrWhiteSpace(httpPort) ? null : Colon)}{httpPort}{httpTarget}";
66+
return url;
67+
}
68+
69+
return string.IsNullOrWhiteSpace(url) ? null : url;
70+
}
71+
}
72+
}

sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Demo.Tracing/InstrumentationWithActivitySource.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public void Start(string url)
6464
}
6565

6666
activity?.SetTag("http.url", context.Request.Url.AbsolutePath);
67+
activity?.SetTag("http.host", context.Request.Url.Host);
6768

6869
string requestContent;
6970
using (var childSpan = source.StartActivity("ReadStream", ActivityKind.Consumer))

0 commit comments

Comments
 (0)