From b35311e75d0803aa29866522405b6d75db89006d Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 14 Apr 2020 14:09:18 -0700 Subject: [PATCH] ZipkinExporter performance improvements. (#596) * ZipkinExporter performance improvements. * Bug fixes. * Added a Zipkin integration test. * Bug fixes. * Fixed HttpClientCollectorOptions not filtering HttpWebRequest events. * Removed LangVersion from project file, it wasn't needed. * Fixed unit tests failing when IPv6 isn't available on host. Co-authored-by: Sergey Kanzhelev --- .../HttpClientCollectorOptions.cs | 38 +++- .../JaegerConversionExtensions.cs | 1 + .../Implementation/JaegerLog.cs | 2 +- .../Implementation/JaegerSpan.cs | 1 + .../OpenTelemetry.Exporter.Jaeger.csproj | 6 +- .../Implementation/ZipkinAnnotation.cs | 14 +- .../ZipkinConversionExtensions.cs | 205 +++++++++++------- .../Implementation/ZipkinEndpoint.cs | 68 +++++- .../Implementation/ZipkinSpan.cs | 185 ++++++++-------- .../Implementation/ZipkinSpanKind.cs | 41 ---- .../OpenTelemetry.Exporter.Zipkin.csproj | 4 + .../ZipkinTraceExporter.cs | 135 +++++------- .../Internal}/EnumerationHelper.cs | 2 +- .../Internal}/PooledList.cs | 11 +- src/OpenTelemetry/OpenTelemetry.csproj | 1 + .../ZipkinSpanConverterTests.cs | 7 +- ...OpenTelemetry.Exporter.Zipkin.Tests.csproj | 3 + .../ZipkinTraceExporterTests.cs | 101 +++++++++ 18 files changed, 513 insertions(+), 312 deletions(-) delete mode 100644 src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpanKind.cs rename src/{OpenTelemetry.Exporter.Jaeger/Implementation => OpenTelemetry/Internal}/EnumerationHelper.cs (99%) rename src/{OpenTelemetry.Exporter.Jaeger/Implementation => OpenTelemetry/Internal}/PooledList.cs (93%) create mode 100644 test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinTraceExporterTests.cs diff --git a/src/OpenTelemetry.Collector.Dependencies/HttpClientCollectorOptions.cs b/src/OpenTelemetry.Collector.Dependencies/HttpClientCollectorOptions.cs index 1b203cc7291..c9e1c33af81 100644 --- a/src/OpenTelemetry.Collector.Dependencies/HttpClientCollectorOptions.cs +++ b/src/OpenTelemetry.Collector.Dependencies/HttpClientCollectorOptions.cs @@ -14,7 +14,9 @@ // limitations under the License. // using System; +using System.Net; using System.Net.Http; +using OpenTelemetry.Collector.Dependencies.Implementation; using OpenTelemetry.Context.Propagation; namespace OpenTelemetry.Collector.Dependencies @@ -63,12 +65,9 @@ private static bool DefaultFilter(string activityName, object arg1, object unuse // TODO: there is some preliminary consensus that we should introduce 'terminal' spans or context. // exporters should ensure they set it - if (activityName == "System.Net.Http.HttpRequestOut" && - arg1 is HttpRequestMessage request && - request.RequestUri != null && - request.Method == HttpMethod.Post) + if (IsHttpOutgoingPostRequest(activityName, arg1, out Uri requestUri)) { - var originalString = request.RequestUri.OriginalString; + var originalString = requestUri.OriginalString; // zipkin if (originalString.Contains(":9411/api/v2/spans")) @@ -89,5 +88,34 @@ arg1 is HttpRequestMessage request && return true; } + + private static bool IsHttpOutgoingPostRequest(string activityName, object arg1, out Uri requestUri) + { + if (activityName == "System.Net.Http.HttpRequestOut") + { + if (arg1 is HttpRequestMessage request && + request.RequestUri != null && + request.Method == HttpMethod.Post) + { + requestUri = request.RequestUri; + return true; + } + } +#if NET461 + else if (activityName == HttpWebRequestDiagnosticSource.ActivityName) + { + if (arg1 is HttpWebRequest request && + request.RequestUri != null && + request.Method == "POST") + { + requestUri = request.RequestUri; + return true; + } + } +#endif + + requestUri = null; + return false; + } } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs index 324970e30f2..7f9d0172b82 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using OpenTelemetry.Internal; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using OpenTelemetry.Trace.Export; diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs index 6e0cb6dfaba..094880468cc 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs @@ -13,10 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. // -using System; using System.Text; using System.Threading; using System.Threading.Tasks; +using OpenTelemetry.Internal; using Thrift.Protocol; using Thrift.Protocol.Entities; diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs index ad6e6233a3b..46e55fddb3d 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs @@ -16,6 +16,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using OpenTelemetry.Internal; using Thrift.Protocol; using Thrift.Protocol.Entities; diff --git a/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj b/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj index 0a3ac291d84..e55a21bfca3 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj +++ b/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj @@ -13,9 +13,13 @@ $(NoWarn),1591 + + + + + - diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs index 6aba9ba3b85..f186997f8ec 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs @@ -15,10 +15,18 @@ // namespace OpenTelemetry.Exporter.Zipkin.Implementation { - internal class ZipkinAnnotation + internal readonly struct ZipkinAnnotation { - public long Timestamp { get; set; } + public ZipkinAnnotation( + long timestamp, + string value) + { + this.Timestamp = timestamp; + this.Value = value; + } - public string Value { get; set; } + public long Timestamp { get; } + + public string Value { get; } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs index 8f69cc94278..534cb0360f5 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs @@ -17,6 +17,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using OpenTelemetry.Internal; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using OpenTelemetry.Trace.Export; @@ -40,6 +41,11 @@ internal static class ZipkinConversionExtensions private static readonly ConcurrentDictionary LocalEndpointCache = new ConcurrentDictionary(); private static readonly ConcurrentDictionary RemoteEndpointCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary CanonicalCodeCache = new ConcurrentDictionary(); + + private static readonly DictionaryEnumerator.ForEachDelegate ProcessAttributesRef = ProcessAttributes; + private static readonly DictionaryEnumerator.ForEachDelegate ProcessLibraryResourcesRef = ProcessLibraryResources; + private static readonly ListEnumerator>.ForEachDelegate ProcessEventsRef = ProcessEvents; internal static ZipkinSpan ToZipkinSpan(this SpanData otelSpan, ZipkinEndpoint defaultLocalEndpoint, bool useShortTraceIds = false) { @@ -47,109 +53,88 @@ internal static ZipkinSpan ToZipkinSpan(this SpanData otelSpan, ZipkinEndpoint d var startTimestamp = ToEpochMicroseconds(otelSpan.StartTimestamp); var endTimestamp = ToEpochMicroseconds(otelSpan.EndTimestamp); - var spanBuilder = - ZipkinSpan.NewBuilder() - .TraceId(EncodeTraceId(context.TraceId, useShortTraceIds)) - .Id(EncodeSpanId(context.SpanId)) - .Kind(ToSpanKind(otelSpan)) - .Name(otelSpan.Name) - .Timestamp(ToEpochMicroseconds(otelSpan.StartTimestamp)) - .Duration(endTimestamp - startTimestamp); - + string parentId = null; if (otelSpan.ParentSpanId != default) { - spanBuilder.ParentId(EncodeSpanId(otelSpan.ParentSpanId)); + parentId = EncodeSpanId(otelSpan.ParentSpanId); } - Tuple remoteEndpointServiceName = null; - foreach (var label in otelSpan.Attributes) + var attributeEnumerationState = new AttributeEnumerationState { - string key = label.Key; - string strVal = label.Value.ToString(); + Tags = PooledList>.Create(), + }; - if (strVal != null - && RemoteEndpointServiceNameKeyResolutionDictionary.TryGetValue(key, out int priority) - && (remoteEndpointServiceName == null || priority < remoteEndpointServiceName.Item2)) - { - remoteEndpointServiceName = new Tuple(strVal, priority); - } + DictionaryEnumerator.AllocationFreeForEach(otelSpan.Attributes, ref attributeEnumerationState, ProcessAttributesRef); + DictionaryEnumerator.AllocationFreeForEach(otelSpan.LibraryResource.Attributes, ref attributeEnumerationState, ProcessLibraryResourcesRef); - spanBuilder.PutTag(key, strVal); - } + var localEndpoint = defaultLocalEndpoint; - // See https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-resource-semantic-conventions.md - string serviceName = string.Empty; - string serviceNamespace = string.Empty; - foreach (var label in otelSpan.LibraryResource.Attributes) - { - string key = label.Key; - object val = label.Value; - string strVal = val as string; + var serviceName = attributeEnumerationState.ServiceName; - if (key == Resource.ServiceNameKey && strVal != null) - { - serviceName = strVal; - } - else if (key == Resource.ServiceNamespaceKey && strVal != null) - { - serviceNamespace = strVal; - } - else + // override default service name + if (!string.IsNullOrWhiteSpace(serviceName)) + { + if (!string.IsNullOrWhiteSpace(attributeEnumerationState.ServiceNamespace)) { - spanBuilder.PutTag(key, strVal ?? val?.ToString()); + serviceName = attributeEnumerationState.ServiceNamespace + "." + serviceName; } - } - - if (serviceNamespace != string.Empty) - { - serviceName = serviceNamespace + "." + serviceName; - } - var endpoint = defaultLocalEndpoint; - - // override default service name - if (serviceName != string.Empty) - { - endpoint = LocalEndpointCache.GetOrAdd(serviceName, _ => new ZipkinEndpoint() + if (!LocalEndpointCache.TryGetValue(serviceName, out localEndpoint)) { - Ipv4 = defaultLocalEndpoint.Ipv4, - Ipv6 = defaultLocalEndpoint.Ipv6, - Port = defaultLocalEndpoint.Port, - ServiceName = serviceName, - }); + localEndpoint = defaultLocalEndpoint.Clone(serviceName); + LocalEndpointCache.TryAdd(serviceName, localEndpoint); + } } - spanBuilder.LocalEndpoint(endpoint); - - if ((otelSpan.Kind == SpanKind.Client || otelSpan.Kind == SpanKind.Producer) && remoteEndpointServiceName != null) + ZipkinEndpoint remoteEndpoint = null; + if ((otelSpan.Kind == SpanKind.Client || otelSpan.Kind == SpanKind.Producer) && attributeEnumerationState.RemoteEndpointServiceName != null) { - spanBuilder.RemoteEndpoint(RemoteEndpointCache.GetOrAdd(remoteEndpointServiceName.Item1, _ => new ZipkinEndpoint - { - ServiceName = remoteEndpointServiceName.Item1, - })); + remoteEndpoint = RemoteEndpointCache.GetOrAdd(attributeEnumerationState.RemoteEndpointServiceName, ZipkinEndpoint.Create); } var status = otelSpan.Status; if (status.IsValid) { - spanBuilder.PutTag(StatusCode, status.CanonicalCode.ToString()); + if (!CanonicalCodeCache.TryGetValue(status.CanonicalCode, out string canonicalCode)) + { + canonicalCode = status.CanonicalCode.ToString(); + CanonicalCodeCache.TryAdd(status.CanonicalCode, canonicalCode); + } + + PooledList>.Add(ref attributeEnumerationState.Tags, new KeyValuePair(StatusCode, canonicalCode)); if (status.Description != null) { - spanBuilder.PutTag(StatusDescription, status.Description); + PooledList>.Add(ref attributeEnumerationState.Tags, new KeyValuePair(StatusDescription, status.Description)); } } - foreach (var annotation in otelSpan.Events) - { - spanBuilder.AddAnnotation(ToEpochMicroseconds(annotation.Timestamp), annotation.Name); - } + var annotations = PooledList.Create(); + ListEnumerator>.AllocationFreeForEach(otelSpan.Events, ref annotations, ProcessEventsRef); + + return new ZipkinSpan( + EncodeTraceId(context.TraceId, useShortTraceIds), + parentId, + EncodeSpanId(context.SpanId), + ToSpanKind(otelSpan), + otelSpan.Name, + ToEpochMicroseconds(otelSpan.StartTimestamp), + duration: endTimestamp - startTimestamp, + localEndpoint, + remoteEndpoint, + annotations, + attributeEnumerationState.Tags, + null, + null); + } - return spanBuilder.Build(); + internal static string EncodeSpanId(ActivitySpanId spanId) + { + return spanId.ToHexString(); } - private static long ToEpochMicroseconds(DateTimeOffset timestamp) + internal static long ToEpochMicroseconds(DateTimeOffset timestamp) { return timestamp.ToUnixTimeMilliseconds() * 1000; } @@ -166,23 +151,83 @@ private static string EncodeTraceId(ActivityTraceId traceId, bool useShortTraceI return id; } - private static string EncodeSpanId(ActivitySpanId spanId) + private static string ToSpanKind(SpanData otelSpan) { - return spanId.ToHexString(); + switch (otelSpan.Kind) + { + case SpanKind.Server: + return "SERVER"; + case SpanKind.Producer: + return "PRODUCER"; + case SpanKind.Consumer: + return "CONSUMER"; + default: + return "CLIENT"; + } } - private static ZipkinSpanKind ToSpanKind(SpanData otelSpan) + private static bool ProcessEvents(ref PooledList annotations, Event @event) { - if (otelSpan.Kind == SpanKind.Server) + PooledList.Add(ref annotations, new ZipkinAnnotation(ToEpochMicroseconds(@event.Timestamp), @event.Name)); + return true; + } + + private static bool ProcessAttributes(ref AttributeEnumerationState state, KeyValuePair attribute) + { + string key = attribute.Key; + if (!(attribute.Value is string strVal)) { - return ZipkinSpanKind.SERVER; + strVal = attribute.Value?.ToString(); } - else if (otelSpan.Kind == SpanKind.Client) + + if (strVal != null + && RemoteEndpointServiceNameKeyResolutionDictionary.TryGetValue(key, out int priority) + && (state.RemoteEndpointServiceName == null || priority < state.RemoteEndpointServiceNamePriority)) { - return ZipkinSpanKind.CLIENT; + state.RemoteEndpointServiceName = strVal; + state.RemoteEndpointServiceNamePriority = priority; } - return ZipkinSpanKind.CLIENT; + PooledList>.Add(ref state.Tags, new KeyValuePair(key, strVal)); + + return true; + } + + private static bool ProcessLibraryResources(ref AttributeEnumerationState state, KeyValuePair label) + { + // See https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-resource-semantic-conventions.md + + string key = label.Key; + object val = label.Value; + string strVal = val as string; + + if (key == Resource.ServiceNameKey && strVal != null) + { + state.ServiceName = strVal; + } + else if (key == Resource.ServiceNamespaceKey && strVal != null) + { + state.ServiceNamespace = strVal; + } + else + { + PooledList>.Add(ref state.Tags, new KeyValuePair(key, strVal ?? val?.ToString())); + } + + return true; + } + + private struct AttributeEnumerationState + { + public PooledList> Tags; + + public string RemoteEndpointServiceName; + + public int RemoteEndpointServiceNamePriority; + + public string ServiceName; + + public string ServiceNamespace; } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs index 50cacb1794f..3edc9718da3 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs @@ -13,16 +13,76 @@ // See the License for the specific language governing permissions and // limitations under the License. // +using System.Text.Json; + namespace OpenTelemetry.Exporter.Zipkin.Implementation { internal class ZipkinEndpoint { - public string ServiceName { get; set; } + public ZipkinEndpoint(string serviceName) + : this(serviceName, null, null, null) + { + } + + public ZipkinEndpoint( + string serviceName, + string ipv4, + string ipv6, + int? port) + { + this.ServiceName = serviceName; + this.Ipv4 = ipv4; + this.Ipv6 = ipv6; + this.Port = port; + } + + public string ServiceName { get; } + + public string Ipv4 { get; } + + public string Ipv6 { get; } + + public int? Port { get; } + + public static ZipkinEndpoint Create(string serviceName) + { + return new ZipkinEndpoint(serviceName); + } + + public ZipkinEndpoint Clone(string serviceName) + { + return new ZipkinEndpoint( + serviceName, + this.Ipv4, + this.Ipv6, + this.Port); + } + + public void Write(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + if (this.ServiceName != null) + { + writer.WriteString("serviceName", this.ServiceName); + } + + if (this.Ipv4 != null) + { + writer.WriteString("ipv4", this.Ipv4); + } - public string Ipv4 { get; set; } + if (this.Ipv6 != null) + { + writer.WriteString("ipv6", this.Ipv6); + } - public string Ipv6 { get; set; } + if (this.Port.HasValue) + { + writer.WriteNumber("port", this.Port.Value); + } - public int Port { get; set; } + writer.WriteEndObject(); + } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs index dd7f6b5cb42..33b305c375a 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs @@ -15,157 +15,170 @@ // using System; using System.Collections.Generic; -using System.Text.Json.Serialization; +using System.Text.Json; +using OpenTelemetry.Internal; namespace OpenTelemetry.Exporter.Zipkin.Implementation { - internal class ZipkinSpan + internal readonly struct ZipkinSpan { - public string TraceId { get; set; } + public ZipkinSpan( + string traceId, + string parentId, + string id, + string kind, + string name, + long? timestamp, + long? duration, + ZipkinEndpoint localEndpoint, + ZipkinEndpoint remoteEndpoint, + in PooledList? annotations, + in PooledList>? tags, + bool? debug, + bool? shared) + { + if (string.IsNullOrWhiteSpace(traceId)) + { + throw new ArgumentNullException(nameof(traceId)); + } - public string ParentId { get; set; } + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + this.TraceId = traceId; + this.ParentId = parentId; + this.Id = id; + this.Kind = kind; + this.Name = name; + this.Timestamp = timestamp; + this.Duration = duration; + this.LocalEndpoint = localEndpoint; + this.RemoteEndpoint = remoteEndpoint; + this.Annotations = annotations; + this.Tags = tags; + this.Debug = debug; + this.Shared = shared; + } - public string Id { get; set; } + public string TraceId { get; } - [JsonConverter(typeof(JsonStringEnumConverter))] - public ZipkinSpanKind Kind { get; set; } + public string ParentId { get; } - public string Name { get; set; } + public string Id { get; } - public long Timestamp { get; set; } + public string Kind { get; } - public long Duration { get; set; } + public string Name { get; } - public ZipkinEndpoint LocalEndpoint { get; set; } + public long? Timestamp { get; } - public ZipkinEndpoint RemoteEndpoint { get; set; } + public long? Duration { get; } - public IList Annotations { get; set; } + public ZipkinEndpoint LocalEndpoint { get; } - public Dictionary Tags { get; set; } + public ZipkinEndpoint RemoteEndpoint { get; } - public bool Debug { get; set; } + public PooledList? Annotations { get; } - public bool Shared { get; set; } + public PooledList>? Tags { get; } - public static Builder NewBuilder() + public bool? Debug { get; } + + public bool? Shared { get; } + + public void Return() { - return new Builder(); + this.Annotations?.Return(); + this.Tags?.Return(); } - public class Builder + public void Write(Utf8JsonWriter writer) { - private readonly ZipkinSpan result = new ZipkinSpan(); + writer.WriteStartObject(); - internal Builder TraceId(string val) - { - this.result.TraceId = val; - return this; - } + writer.WriteString("traceId", this.TraceId); - internal Builder Id(string val) + if (this.Name != null) { - this.result.Id = val; - return this; + writer.WriteString("name", this.Name); } - internal Builder ParentId(string val) + if (this.ParentId != null) { - this.result.ParentId = val; - return this; + writer.WriteString("parentId", this.ParentId); } - internal Builder Kind(ZipkinSpanKind val) - { - this.result.Kind = val; - return this; - } + writer.WriteString("id", this.Id); - internal Builder Name(string val) - { - this.result.Name = val; - return this; - } + writer.WriteString("kind", this.Kind); - internal Builder Timestamp(long val) + if (this.Timestamp.HasValue) { - this.result.Timestamp = val; - return this; + writer.WriteNumber("timestamp", this.Timestamp.Value); } - internal Builder Duration(long val) + if (this.Duration.HasValue) { - this.result.Duration = val; - return this; + writer.WriteNumber("duration", this.Duration.Value); } - internal Builder LocalEndpoint(ZipkinEndpoint val) + if (this.Debug.HasValue) { - this.result.LocalEndpoint = val; - return this; + writer.WriteBoolean("debug", this.Debug.Value); } - internal Builder RemoteEndpoint(ZipkinEndpoint val) + if (this.Shared.HasValue) { - this.result.RemoteEndpoint = val; - return this; + writer.WriteBoolean("shared", this.Shared.Value); } - internal Builder Debug(bool val) + if (this.LocalEndpoint != null) { - this.result.Debug = val; - return this; + writer.WritePropertyName("localEndpoint"); + this.LocalEndpoint.Write(writer); } - internal Builder Shared(bool val) + if (this.RemoteEndpoint != null) { - this.result.Shared = val; - return this; + writer.WritePropertyName("remoteEndpoint"); + this.RemoteEndpoint.Write(writer); } - internal Builder PutTag(string key, string value) + if (this.Annotations.HasValue) { - if (this.result.Tags == null) - { - this.result.Tags = new Dictionary(); - } + writer.WritePropertyName("annotations"); + writer.WriteStartArray(); - if (key == null) + foreach (var annotation in this.Annotations.Value) { - throw new ArgumentNullException(nameof(key)); - } + writer.WriteStartObject(); - this.result.Tags[key] = value ?? throw new ArgumentNullException(nameof(value)); + writer.WriteNumber("timestamp", annotation.Timestamp); - return this; - } + writer.WriteString("value", annotation.Value); - internal Builder AddAnnotation(long timestamp, string value) - { - if (this.result.Annotations == null) - { - this.result.Annotations = new List(2); + writer.WriteEndObject(); } - this.result.Annotations.Add(new ZipkinAnnotation() { Timestamp = timestamp, Value = value }); - - return this; + writer.WriteEndArray(); } - internal ZipkinSpan Build() + if (this.Tags.HasValue) { - if (this.result.TraceId == null) - { - throw new ArgumentException("Trace ID should not be null"); - } + writer.WritePropertyName("tags"); + writer.WriteStartObject(); - if (this.result.Id == null) + foreach (var tag in this.Tags.Value) { - throw new ArgumentException("ID should not be null"); + writer.WriteString(tag.Key, tag.Value); } - return this.result; + writer.WriteEndObject(); } + + writer.WriteEndObject(); } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpanKind.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpanKind.cs deleted file mode 100644 index 624e232546d..00000000000 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpanKind.cs +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright 2018, OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Exporter.Zipkin.Implementation -{ - internal enum ZipkinSpanKind - { - /// - /// Client span. - /// - CLIENT, - - /// - /// Server span. - /// - SERVER, - - /// - /// Producer. - /// - PRODUCER, - - /// - /// Consumer. - /// - CONSUMER, - } -} diff --git a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj index 0f81d3019ca..b722822f0e5 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj +++ b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj @@ -4,6 +4,10 @@ Zipkin exporter for OpenTelemetry $(PackageTags);Zipkin;distributed-tracing + + + + diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinTraceExporter.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinTraceExporter.cs index 1a45ca5d797..6878f5a6c19 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinTraceExporter.cs @@ -33,19 +33,8 @@ namespace OpenTelemetry.Exporter.Zipkin /// public class ZipkinTraceExporter : SpanExporter { - private const long MillisPerSecond = 1000L; - private const long NanosPerMillisecond = 1000 * 1000; - private const long NanosPerSecond = NanosPerMillisecond * MillisPerSecond; - - private static readonly JsonSerializerOptions Options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }; - private readonly ZipkinTraceExporterOptions options; - private readonly ZipkinEndpoint localEndpoint; private readonly HttpClient httpClient; - private readonly string serviceEndpoint; /// /// Initializes a new instance of the class. @@ -55,48 +44,18 @@ public class ZipkinTraceExporter : SpanExporter public ZipkinTraceExporter(ZipkinTraceExporterOptions options, HttpClient client = null) { this.options = options; - this.localEndpoint = this.GetLocalZipkinEndpoint(); + this.LocalEndpoint = this.GetLocalZipkinEndpoint(); this.httpClient = client ?? new HttpClient(); - this.serviceEndpoint = options.Endpoint?.ToString(); } + internal ZipkinEndpoint LocalEndpoint { get; } + /// - public override async Task ExportAsync(IEnumerable otelSpanList, CancellationToken cancellationToken) + public override async Task ExportAsync(IEnumerable batch, CancellationToken cancellationToken) { - var zipkinSpans = new List(); - - foreach (var data in otelSpanList) - { - bool shouldExport = true; - foreach (var label in data.Attributes) - { - if (label.Key == "http.url") - { - if (label.Value is string urlStr && urlStr == this.serviceEndpoint) - { - // do not track calls to Zipkin - shouldExport = false; - } - - break; - } - } - - if (shouldExport) - { - var zipkinSpan = data.ToZipkinSpan(this.localEndpoint, this.options.UseShortTraceIds); - zipkinSpans.Add(zipkinSpan); - } - } - - if (zipkinSpans.Count == 0) - { - return ExportResult.Success; - } - try { - await this.SendSpansAsync(zipkinSpans, cancellationToken); + await this.SendSpansAsync(batch).ConfigureAwait(false); return ExportResult.Success; } catch (Exception) @@ -112,51 +71,37 @@ public override Task ShutdownAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - private Task SendSpansAsync(IEnumerable spans, CancellationToken cancellationToken) + private Task SendSpansAsync(IEnumerable spans) { var requestUri = this.options.Endpoint; - var request = this.GetHttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = this.GetRequestContent(spans); + + var request = new HttpRequestMessage(HttpMethod.Post, requestUri) + { + Content = new JsonContent(this, spans), + }; // avoid cancelling here: this is no return point: if we reached this point // and cancellation is requested, it's better if we try to finish sending spans rather than drop it - return this.DoPostAsync(this.httpClient, request); - } - - private async Task DoPostAsync(HttpClient client, HttpRequestMessage request) - { - await client.SendAsync(request).ConfigureAwait(false); - } - - private HttpRequestMessage GetHttpRequestMessage(HttpMethod method, Uri requestUri) - { - var request = new HttpRequestMessage(method, requestUri); - - return request; - } - - private HttpContent GetRequestContent(IEnumerable toSerialize) - { - return new JsonContent(toSerialize, Options); + return this.httpClient.SendAsync(request); } private ZipkinEndpoint GetLocalZipkinEndpoint() { - var result = new ZipkinEndpoint() - { - ServiceName = this.options.ServiceName, - }; - var hostName = this.ResolveHostName(); + string ipv4 = null; + string ipv6 = null; if (!string.IsNullOrEmpty(hostName)) { - result.Ipv4 = this.ResolveHostAddress(hostName, AddressFamily.InterNetwork); - - result.Ipv6 = this.ResolveHostAddress(hostName, AddressFamily.InterNetworkV6); + ipv4 = this.ResolveHostAddress(hostName, AddressFamily.InterNetwork); + ipv6 = this.ResolveHostAddress(hostName, AddressFamily.InterNetworkV6); } - return result; + return new ZipkinEndpoint( + this.options.ServiceName, + ipv4, + ipv6, + null); } private string ResolveHostAddress(string hostName, AddressFamily family) @@ -222,19 +167,45 @@ private class JsonContent : HttpContent CharSet = "utf-8", }; - private readonly IEnumerable spans; - private readonly JsonSerializerOptions options; + private static Utf8JsonWriter writer; + + private readonly ZipkinTraceExporter exporter; + private readonly IEnumerable spans; - public JsonContent(IEnumerable spans, JsonSerializerOptions options) + public JsonContent(ZipkinTraceExporter exporter, IEnumerable spans) { + this.exporter = exporter; this.spans = spans; - this.options = options; this.Headers.ContentType = JsonHeader; } - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) - => await JsonSerializer.SerializeAsync(stream, this.spans, this.options).ConfigureAwait(false); + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + if (writer == null) + { + writer = new Utf8JsonWriter(stream); + } + else + { + writer.Reset(stream); + } + + writer.WriteStartArray(); + + foreach (var span in this.spans) + { + var zipkinSpan = span.ToZipkinSpan(this.exporter.LocalEndpoint, this.exporter.options.UseShortTraceIds); + + zipkinSpan.Write(writer); + + zipkinSpan.Return(); + } + + writer.WriteEndArray(); + + return writer.FlushAsync(); + } protected override bool TryComputeLength(out long length) { diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/EnumerationHelper.cs b/src/OpenTelemetry/Internal/EnumerationHelper.cs similarity index 99% rename from src/OpenTelemetry.Exporter.Jaeger/Implementation/EnumerationHelper.cs rename to src/OpenTelemetry/Internal/EnumerationHelper.cs index 0e9c5966000..2ce98e84c78 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/EnumerationHelper.cs +++ b/src/OpenTelemetry/Internal/EnumerationHelper.cs @@ -22,7 +22,7 @@ using System.Reflection; using System.Reflection.Emit; -namespace OpenTelemetry.Exporter.Jaeger.Implementation +namespace OpenTelemetry.Internal { internal class DictionaryEnumerator : Enumerator >, diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/PooledList.cs b/src/OpenTelemetry/Internal/PooledList.cs similarity index 93% rename from src/OpenTelemetry.Exporter.Jaeger/Implementation/PooledList.cs rename to src/OpenTelemetry/Internal/PooledList.cs index 89bd412a390..f638303aa0c 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/PooledList.cs +++ b/src/OpenTelemetry/Internal/PooledList.cs @@ -18,7 +18,7 @@ using System.Collections; using System.Collections.Generic; -namespace OpenTelemetry.Exporter.Jaeger.Implementation +namespace OpenTelemetry.Internal { internal readonly struct PooledList : IEnumerable, ICollection { @@ -92,7 +92,12 @@ public void Return() void ICollection.CopyTo(Array array, int index) => throw new NotSupportedException(); - public IEnumerator GetEnumerator() + public Enumerator GetEnumerator() + { + return new Enumerator(in this); + } + + IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(in this); } @@ -102,7 +107,7 @@ IEnumerator IEnumerable.GetEnumerator() return new Enumerator(in this); } - private struct Enumerator : IEnumerator, IEnumerator + public struct Enumerator : IEnumerator, IEnumerator { private readonly T[] buffer; private readonly int count; diff --git a/src/OpenTelemetry/OpenTelemetry.csproj b/src/OpenTelemetry/OpenTelemetry.csproj index 8103161f037..b5e7fca2293 100644 --- a/src/OpenTelemetry/OpenTelemetry.csproj +++ b/src/OpenTelemetry/OpenTelemetry.csproj @@ -25,6 +25,7 @@ + diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinSpanConverterTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinSpanConverterTests.cs index af3adaa1489..0e526a7867a 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinSpanConverterTests.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinSpanConverterTests.cs @@ -25,10 +25,7 @@ namespace OpenTelemetry.Exporter.Zipkin.Tests.Implementation { public class ZipkinTraceExporterRemoteEndpointTests { - private static readonly ZipkinEndpoint DefaultZipkinEndpoint = new ZipkinEndpoint - { - ServiceName = "TestService", - }; + private static readonly ZipkinEndpoint DefaultZipkinEndpoint = new ZipkinEndpoint("TestService"); [Fact] public void ZipkinSpanConverterTest_GenerateSpan_RemoteEndpointOmittedByDefault() @@ -78,7 +75,7 @@ public void ZipkinSpanConverterTest_GenerateSpan_RemoteEndpointResolutionPriorit Assert.Equal("RemoteServiceName", zipkinSpan.RemoteEndpoint.ServiceName); } - internal SpanData CreateTestSpan( + internal static SpanData CreateTestSpan( bool setAttributes = true, Dictionary additionalAttributes = null, bool addEvents = true, diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj b/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj index 3d34a8bd617..59d5a504ed5 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj @@ -5,6 +5,9 @@ $(TargetFrameworks);net461 false + + + diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinTraceExporterTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinTraceExporterTests.cs new file mode 100644 index 00000000000..d83f778d8a5 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinTraceExporterTests.cs @@ -0,0 +1,101 @@ +// +// Copyright 2018, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using OpenTelemetry.Collector.Dependencies.Tests; +using OpenTelemetry.Exporter.Zipkin.Implementation; +using OpenTelemetry.Exporter.Zipkin.Tests.Implementation; +using OpenTelemetry.Trace.Export; +using Xunit; + +namespace OpenTelemetry.Exporter.Zipkin.Tests +{ + public class ZipkinTraceExporterTests : IDisposable + { + private static readonly ConcurrentDictionary Responses = new ConcurrentDictionary(); + + private readonly IDisposable testServer; + private readonly string testServerHost; + private readonly int testServerPort; + + public ZipkinTraceExporterTests() + { + this.testServer = TestServer.RunServer( + ctx => ProcessServerRequest(ctx), + out this.testServerHost, + out this.testServerPort); + + static void ProcessServerRequest(HttpListenerContext context) + { + context.Response.StatusCode = 200; + + using StreamReader readStream = new StreamReader(context.Request.InputStream); + + string requestContent = readStream.ReadToEnd(); + + Responses.TryAdd( + Guid.Parse(context.Request.QueryString["requestId"]), + requestContent); + + context.Response.OutputStream.Close(); + } + } + + public void Dispose() + { + this.testServer.Dispose(); + } + + [Fact] + public async Task ZipkinExporterIntegrationTest() + { + var spans = new List { ZipkinTraceExporterRemoteEndpointTests.CreateTestSpan() }; + + Guid requestId = Guid.NewGuid(); + + ZipkinTraceExporter exporter = new ZipkinTraceExporter( + new ZipkinTraceExporterOptions + { + Endpoint = new Uri($"http://{this.testServerHost}:{this.testServerPort}/api/v2/spans?requestId={requestId}"), + }); + + await exporter.ExportAsync(spans, CancellationToken.None).ConfigureAwait(false); + + await exporter.ShutdownAsync(CancellationToken.None).ConfigureAwait(false); + + var span = spans[0]; + var context = span.Context; + + var timestamp = ZipkinConversionExtensions.ToEpochMicroseconds(span.StartTimestamp); + + StringBuilder ipInformation = new StringBuilder(); + if (!string.IsNullOrEmpty(exporter.LocalEndpoint.Ipv4)) + ipInformation.Append($@",""ipv4"":""{exporter.LocalEndpoint.Ipv4}"""); + if (!string.IsNullOrEmpty(exporter.LocalEndpoint.Ipv6)) + ipInformation.Append($@",""ipv6"":""{exporter.LocalEndpoint.Ipv6}"""); + + Assert.Equal( + $@"[{{""traceId"":""e8ea7e9ac72de94e91fabc613f9686b2"",""name"":""Name"",""parentId"":""{ZipkinConversionExtensions.EncodeSpanId(span.ParentSpanId)}"",""id"":""{ZipkinConversionExtensions.EncodeSpanId(context.SpanId)}"",""kind"":""CLIENT"",""timestamp"":{timestamp},""duration"":60000000,""localEndpoint"":{{""serviceName"":""Open Telemetry Exporter""{ipInformation}}},""annotations"":[{{""timestamp"":{timestamp},""value"":""Event1""}},{{""timestamp"":{timestamp},""value"":""Event2""}}],""tags"":{{""stringKey"":""value"",""longKey"":""1"",""longKey2"":""1"",""doubleKey"":""1"",""doubleKey2"":""1"",""boolKey"":""True"",""ot.status_code"":""Ok""}}}}]", + Responses[requestId]); + } + } +}