-
Notifications
You must be signed in to change notification settings - Fork 364
[Exporter.Geneva] Add httpUrl for HTTP server spans #2818
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
c5d6e55
0ab10f3
a03b089
ead1320
58a9870
b5452df
faec951
d25678e
c08c1f7
7333b72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| using System.Diagnostics; | ||
| using System.Globalization; | ||
| using System.Runtime.InteropServices; | ||
| using System.Text; | ||
| using OpenTelemetry.Exporter.Geneva.Transports; | ||
| using OpenTelemetry.Internal; | ||
|
|
||
|
|
@@ -32,14 +33,29 @@ | |
| ["messaging.url"] = "messagingUrl", | ||
| }; | ||
|
|
||
| internal static readonly Dictionary<string, int> CS40_PART_B_HTTPURL_MAPPING_DICTIONARY = new() | ||
| { | ||
| // Mapping from HTTP semconv to httpUrl | ||
| // Combination of url.scheme, server.address, server.port, url.path and url.query attributes for HTTP server spans | ||
| ["url.scheme"] = 0, | ||
| ["server.address"] = 1, | ||
| ["server.port"] = 2, | ||
| ["url.path"] = 3, | ||
| ["url.query"] = 4, | ||
| }; | ||
|
|
||
| #if NET | ||
| internal static readonly FrozenDictionary<string, string> CS40_PART_B_MAPPING = CS40_PART_B_MAPPING_DICTIONARY.ToFrozenDictionary(); | ||
| internal static readonly FrozenDictionary<string, int> CS40_PART_B_HTTPURL_MAPPING = CS40_PART_B_HTTPURL_MAPPING_DICTIONARY.ToFrozenDictionary(); | ||
| #else | ||
| internal static readonly Dictionary<string, string> CS40_PART_B_MAPPING = CS40_PART_B_MAPPING_DICTIONARY; | ||
| internal static readonly Dictionary<string, int> CS40_PART_B_HTTPURL_MAPPING = CS40_PART_B_HTTPURL_MAPPING_DICTIONARY; | ||
| #endif | ||
|
|
||
| internal readonly ThreadLocal<byte[]> Buffer = new(); | ||
|
|
||
| internal readonly ThreadLocal<object?[]> HttpUrlParts = new(); | ||
|
|
||
| #if NET | ||
| internal readonly FrozenSet<string>? CustomFields; | ||
|
|
||
|
|
@@ -243,6 +259,7 @@ | |
| { | ||
| (this.dataTransport as IDisposable)?.Dispose(); | ||
| this.Buffer.Dispose(); | ||
| this.HttpUrlParts.Dispose(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
|
|
@@ -252,6 +269,49 @@ | |
| this.isDisposed = true; | ||
| } | ||
|
|
||
| internal static bool CacheIfPartOfHttpUrl(KeyValuePair<string, object?> entry, object?[] httpUrlParts) | ||
| { | ||
| if (CS40_PART_B_HTTPURL_MAPPING.TryGetValue(entry.Key, out var index)) | ||
| { | ||
| if (index < httpUrlParts.Length) | ||
| { | ||
| httpUrlParts[index] = entry.Value; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| internal static string? GetHttpUrl(object?[] httpUrlParts) | ||
| { | ||
| // OpenTelemetry Semantic Convention: https://github.com/open-telemetry/semantic-conventions/blob/v1.28.0/docs/http/http-spans.md#http-server-semantic-conventions | ||
| var scheme = httpUrlParts[0]?.ToString() ?? string.Empty; // 0 => CS40_PART_B_HTTPURL_MAPPING["url.scheme"] | ||
| var address = httpUrlParts[1]?.ToString() ?? string.Empty; // 1 => CS40_PART_B_HTTPURL_MAPPING["server.address"] | ||
| var port = httpUrlParts[2]?.ToString(); // 2 => CS40_PART_B_HTTPURL_MAPPING["server.port"] | ||
| port = port != null ? $":{port}" : string.Empty; | ||
| var path = httpUrlParts[3]?.ToString() ?? string.Empty; // 3 => CS40_PART_B_HTTPURL_MAPPING["url.path"] | ||
| var query = httpUrlParts[4]?.ToString(); // 4 => CS40_PART_B_HTTPURL_MAPPING["url.query"] | ||
| query = query != null ? $"?{query}" : string.Empty; | ||
|
|
||
| var length = scheme.Length + Uri.SchemeDelimiter.Length + address.Length + port.Length + path.Length + query.Length; | ||
|
|
||
| if (length == Uri.SchemeDelimiter.Length) // No URL elements found, i.e. no address, no port, no path, no query | ||
|
Check failure on line 299 in src/OpenTelemetry.Exporter.Geneva/Internal/MsgPack/MsgPackTraceExporter.cs
|
||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var urlStringBuilder = new StringBuilder(length) | ||
| .Append(scheme) | ||
| .Append(Uri.SchemeDelimiter) | ||
| .Append(address) | ||
| .Append(port) | ||
| .Append(path) | ||
| .Append(query); | ||
|
|
||
| return urlStringBuilder.ToString(); | ||
| } | ||
|
|
||
| internal ArraySegment<byte> SerializeActivity(Activity activity) | ||
| { | ||
| var buffer = this.Buffer.Value; | ||
|
|
@@ -367,8 +427,27 @@ | |
| var isStatusSuccess = true; | ||
| string? statusDescription = null; | ||
|
|
||
| var isServerActivity = activity.Kind == ActivityKind.Server; | ||
| var httpUrlParts = this.HttpUrlParts.Value ?? new object?[CS40_PART_B_HTTPURL_MAPPING.Count]; | ||
| if (isServerActivity) | ||
| { | ||
| if (this.HttpUrlParts.Value == null) | ||
| { | ||
| this.HttpUrlParts.Value = httpUrlParts; | ||
| } | ||
| else | ||
| { | ||
| Array.Clear(httpUrlParts, 0, httpUrlParts.Length); | ||
| } | ||
| } | ||
|
|
||
| foreach (ref readonly var entry in activity.EnumerateTagObjects()) | ||
| { | ||
| if (isServerActivity && CacheIfPartOfHttpUrl(entry, httpUrlParts)) | ||
| { | ||
| continue; // Skip this entry, since it is part of httpUrl. | ||
| } | ||
|
|
||
| // TODO: check name collision | ||
| if (CS40_PART_B_MAPPING.TryGetValue(entry.Key, out var replacementKey)) | ||
| { | ||
|
|
@@ -393,6 +472,18 @@ | |
| cntFields += 1; | ||
| } | ||
|
|
||
| if (isServerActivity) | ||
| { | ||
| var httpUrl = GetHttpUrl(httpUrlParts); | ||
| if (httpUrl != null) | ||
| { | ||
| // If the activity is a server activity and has http.url, we need to add it as a dedicated field. | ||
| cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, "httpUrl"); | ||
| cursor = MessagePackSerializer.SerializeUnicodeString(buffer, cursor, httpUrl); | ||
| cntFields += 1; | ||
| } | ||
| } | ||
|
|
||
| if (hasEnvProperties) | ||
| { | ||
| // Iteration #2 - Get all "other" fields and collapse them into single field | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| using OpenTelemetry.Exporter.Geneva.MsgPack; | ||
| using Xunit; | ||
|
|
||
| namespace OpenTelemetry.Exporter.Geneva.Tests; | ||
|
|
||
| public class MsgPackTraceExporterTests | ||
| { | ||
| [Fact] | ||
| public void CacheIfPartOfHttpUrl_KeyPresent_IndexInRange_SetsValueAndReturnsTrue() | ||
| { | ||
| var entry = new KeyValuePair<string, object?>("url.scheme", "https"); | ||
| var arr = new object?[MsgPackTraceExporter.CS40_PART_B_HTTPURL_MAPPING.Count]; | ||
| var result = MsgPackTraceExporter.CacheIfPartOfHttpUrl(entry, arr); | ||
| Assert.True(result); | ||
| Assert.Equal("https", arr[0]); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void CacheIfPartOfHttpUrl_KeyPresent_IndexOutOfRange_ReturnsFalse() | ||
| { | ||
| var entry = new KeyValuePair<string, object?>("url.scheme", "https"); | ||
| var arr = Array.Empty<object?>(); // zero-length array | ||
| var result = MsgPackTraceExporter.CacheIfPartOfHttpUrl(entry, arr); | ||
| Assert.False(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void CacheIfPartOfHttpUrl_KeyNotPresent_ReturnsFalse() | ||
| { | ||
| var entry = new KeyValuePair<string, object?>("not.a.key", "value"); | ||
| var arr = new object?[MsgPackTraceExporter.CS40_PART_B_HTTPURL_MAPPING.Count]; | ||
| var result = MsgPackTraceExporter.CacheIfPartOfHttpUrl(entry, arr); | ||
| Assert.False(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void CacheIfPartOfHttpUrl_NullValue_SetsNull() | ||
| { | ||
| var entry = new KeyValuePair<string, object?>("url.scheme", null); | ||
| var arr = new object?[MsgPackTraceExporter.CS40_PART_B_HTTPURL_MAPPING.Count]; | ||
| var result = MsgPackTraceExporter.CacheIfPartOfHttpUrl(entry, arr); | ||
| Assert.True(result); | ||
| Assert.Null(arr[0]); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("", "", "", "", null)] | ||
| [InlineData("http", "host", "", "", "http://host")] | ||
| [InlineData("http", "host", "8080", "/x", "http://host:8080/x")] | ||
| [InlineData("https", "server", "443", "/api", "https://server:443/api")] | ||
| [InlineData("http", "host", "", "/x?y=1", "http://host/x?y=1")] | ||
| public void GetHttpUrl_ReturnsExpectedUrl(string scheme, string hostOrAddress, string port, string pathAndQuery, string? expected) | ||
| { | ||
| var arr = new object?[MsgPackTraceExporter.CS40_PART_B_MAPPING_DICTIONARY.Count]; | ||
| arr[0] = scheme; | ||
| arr[1] = hostOrAddress; | ||
| arr[2] = string.IsNullOrEmpty(port) ? null : port; | ||
| if (!string.IsNullOrEmpty(pathAndQuery) && pathAndQuery.Contains('?')) | ||
| { | ||
| var split = pathAndQuery.Split(['?'], 2); | ||
| arr[3] = split[0]; | ||
| arr[4] = split.Length > 1 ? split[1] : null; | ||
| } | ||
| else | ||
| { | ||
| arr[3] = string.IsNullOrEmpty(pathAndQuery) ? null : pathAndQuery; | ||
| } | ||
|
|
||
| var url = MsgPackTraceExporter.GetHttpUrl(arr); | ||
| Assert.Equal(expected, url); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetHttpUrl_UnknownMethod_ReturnsNull() | ||
| { | ||
| var arr = new object?[MsgPackTraceExporter.CS40_PART_B_HTTPURL_MAPPING.Count]; | ||
| var url = MsgPackTraceExporter.GetHttpUrl(arr); | ||
| Assert.Null(url); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.