From a7206200a51c0b7c95f94d8310ffde13c6baecc8 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Tue, 18 Feb 2020 15:16:15 -0800 Subject: [PATCH] Zipkin Exporter: Specify RemoteEndpoint (#483) * Switched to System.Text.Json for .NET Standard 2.0 target. Added caching of ZipkinEndpoints. Added support for sending RemoteEndpoint to Zipkin API. * Code review feedback. * Code review. * Zipkin performance improvements. * Made json header static. * Removed debug code. * Bumped up the numbers of spans in benchmark to get more consistent results. * Code review. Co-authored-by: Sergey Kanzhelev --- OpenTelemetry.sln | 6 + benchmarks/Benchmarks.csproj | 3 +- .../Exporter/ZipkinExporterBenchmarks.cs | 232 ++++++++++++++++++ benchmarks/OpenTelemetrySdkBenchmarks.cs | 6 - benchmarks/Program.cs | 27 ++ .../AssemblyInfo.cs | 7 + .../Implementation/ZipkinAnnotation.cs | 4 - .../ZipkinConversionExtensions.cs | 188 ++++++++++++++ .../Implementation/ZipkinEndpoint.cs | 6 - .../Implementation/ZipkinSpan.cs | 18 +- .../OpenTelemetry.Exporter.Zipkin.csproj | 8 +- .../ZipkinTraceExporter.cs | 184 +++----------- .../ZipkinSpanConverterTests.cs | 151 ++++++++++++ ...OpenTelemetry.Exporter.Zipkin.Tests.csproj | 32 +++ .../xunit.runner.json | 4 + 15 files changed, 691 insertions(+), 185 deletions(-) create mode 100644 benchmarks/Exporter/ZipkinExporterBenchmarks.cs create mode 100644 benchmarks/Program.cs create mode 100644 src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs create mode 100644 test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinSpanConverterTests.cs create mode 100644 test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj create mode 100644 test/OpenTelemetry.Exporter.Zipkin.Tests/xunit.runner.json diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index e288beb02..75b087dfb 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -113,6 +113,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exporters", "samples\Export EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.Web", "samples\Exporters\Web\OpenTelemetry.Exporter.Web.csproj", "{25C06046-C7D0-46B4-AAAC-90C50C43DE7A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.Zipkin.Tests", "test\OpenTelemetry.Exporter.Zipkin.Tests\OpenTelemetry.Exporter.Zipkin.Tests.csproj", "{1D778D2E-9523-450E-A6E0-A36897C7E78E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -259,6 +261,10 @@ Global {25C06046-C7D0-46B4-AAAC-90C50C43DE7A}.Debug|Any CPU.Build.0 = Debug|Any CPU {25C06046-C7D0-46B4-AAAC-90C50C43DE7A}.Release|Any CPU.ActiveCfg = Release|Any CPU {25C06046-C7D0-46B4-AAAC-90C50C43DE7A}.Release|Any CPU.Build.0 = Release|Any CPU + {1D778D2E-9523-450E-A6E0-A36897C7E78E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D778D2E-9523-450E-A6E0-A36897C7E78E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D778D2E-9523-450E-A6E0-A36897C7E78E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D778D2E-9523-450E-A6E0-A36897C7E78E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj index 742a3170e..b337d0e6e 100644 --- a/benchmarks/Benchmarks.csproj +++ b/benchmarks/Benchmarks.csproj @@ -15,10 +15,11 @@ - + + diff --git a/benchmarks/Exporter/ZipkinExporterBenchmarks.cs b/benchmarks/Exporter/ZipkinExporterBenchmarks.cs new file mode 100644 index 000000000..d076556c2 --- /dev/null +++ b/benchmarks/Exporter/ZipkinExporterBenchmarks.cs @@ -0,0 +1,232 @@ +// +// 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.Generic; +using System.Diagnostics; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using OpenTelemetry.Exporter.Zipkin; +using OpenTelemetry.Trace; +using OpenTelemetry.Trace.Export; + +namespace Benchmarks.Exporter +{ + [MemoryDiagnoser] +#if !NET462 + [ThreadingDiagnoser] +#endif + public class ZipkinExporterBenchmarks + { + [Params(2000, 5000)] + public int NumberOfSpans { get; set; } + + private SpanData testSpan; + + private IDisposable server; + private string serverHost; + private int serverPort; + + [GlobalSetup] + public void GlobalSetup() + { + this.testSpan = this.CreateTestSpan(); + this.server = TestServer.RunServer( + (ctx) => + { + ctx.Response.StatusCode = 200; + ctx.Response.OutputStream.Close(); + }, + out this.serverHost, + out this.serverPort); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + this.server.Dispose(); + } + + [Benchmark] + public async Task ZipkinExporter_ExportAsync() + { + var zipkinExporter = new ZipkinTraceExporter( + new ZipkinTraceExporterOptions + { + Endpoint = new Uri($"http://{this.serverHost}:{this.serverPort}"), + }); + + var spans = new List(); + for (int i = 0; i < this.NumberOfSpans; i++) + { + spans.Add(this.testSpan); + } + + await zipkinExporter.ExportAsync(spans, CancellationToken.None).ConfigureAwait(false); + } + + private SpanData CreateTestSpan() + { + var startTimestamp = new DateTimeOffset(2019, 1, 1, 0, 0, 0, TimeSpan.Zero); + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = new DateTimeOffset(2019, 1, 1, 0, 0, 0, TimeSpan.Zero); + + var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); + var spanId = ActivitySpanId.CreateFromString("6a69db47429ea340".AsSpan()); + var parentSpanId = ActivitySpanId.CreateFromBytes(new byte[] { 12, 23, 34, 45, 56, 67, 78, 89 }); + var attributes = new Dictionary + { + { "stringKey", "value"}, + { "longKey", 1L}, + { "longKey2", 1 }, + { "doubleKey", 1D}, + { "doubleKey2", 1F}, + { "boolKey", true}, + }; + var events = new List + { + new Event( + "Event1", + eventTimestamp, + new Dictionary + { + { "key", "value" }, + } + ), + new Event( + "Event2", + eventTimestamp, + new Dictionary + { + { "key", "value" }, + } + ), + }; + + var linkedSpanId = ActivitySpanId.CreateFromString("888915b6286b9c41".AsSpan()); + + var link = new Link(new SpanContext( + traceId, + linkedSpanId, + ActivityTraceFlags.Recorded)); + + return new SpanData( + "Name", + new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded), + parentSpanId, + SpanKind.Client, + startTimestamp, + attributes, + events, + new[] { link, }, + null, + Status.Ok, + endTimestamp); + } + + public class TestServer + { + private static readonly Random GlobalRandom = new Random(); + + private class RunningServer : IDisposable + { + private readonly Task httpListenerTask; + private readonly HttpListener listener; + private readonly CancellationTokenSource cts; + private readonly AutoResetEvent initialized = new AutoResetEvent(false); + + public RunningServer(Action action, string host, int port) + { + this.cts = new CancellationTokenSource(); + this.listener = new HttpListener(); + + var token = this.cts.Token; + + this.listener.Prefixes.Add($"http://{host}:{port}/"); + this.listener.Start(); + + this.httpListenerTask = new Task(() => + { + while (!token.IsCancellationRequested) + { + var ctxTask = this.listener.GetContextAsync(); + + this.initialized.Set(); + + try + { + ctxTask.Wait(token); + + if (ctxTask.Status == TaskStatus.RanToCompletion) + { + action(ctxTask.Result); + } + } + catch (OperationCanceledException) + { + } + } + }); + } + + public void Start() + { + this.httpListenerTask.Start(); + this.initialized.WaitOne(); + } + + public void Dispose() + { + try + { + this.listener?.Stop(); + this.cts.Cancel(); + } + catch (ObjectDisposedException) + { + // swallow this exception just in case + } + } + } + + public static IDisposable RunServer(Action action, out string host, out int port) + { + host = "localhost"; + port = 0; + RunningServer server = null; + + var retryCount = 5; + while (retryCount > 0) + { + try + { + port = GlobalRandom.Next(2000, 5000); + server = new RunningServer(action, host, port); + server.Start(); + break; + } + catch (HttpListenerException) + { + retryCount--; + } + } + + return server; + } + } + } +} diff --git a/benchmarks/OpenTelemetrySdkBenchmarks.cs b/benchmarks/OpenTelemetrySdkBenchmarks.cs index 446d91ec4..896b7142e 100644 --- a/benchmarks/OpenTelemetrySdkBenchmarks.cs +++ b/benchmarks/OpenTelemetrySdkBenchmarks.cs @@ -15,7 +15,6 @@ // using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; using Benchmarks.Tracing; using OpenTelemetry.Trace; using OpenTelemetry.Trace.Configuration; @@ -41,11 +40,6 @@ namespace Benchmarks this.noopTracer = TracerFactoryBase.Default.GetTracer(null); } - public static void Main(string[] args) - { - var summary = BenchmarkRunner.Run(); - } - [Benchmark] public TelemetrySpan CreateSpan_Sampled() => SpanCreationScenarios.CreateSpan(this.alwaysSampleTracer); diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs new file mode 100644 index 000000000..4616cb098 --- /dev/null +++ b/benchmarks/Program.cs @@ -0,0 +1,27 @@ +// +// 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 BenchmarkDotNet.Running; + +namespace Benchmarks +{ + internal static class Program + { + public static void Main(string[] args) + { + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs index 23da982fd..0acbb239b 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/AssemblyInfo.cs @@ -13,3 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. // +using System.Runtime.CompilerServices; + +#if SIGNED +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Zipkin.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +#else +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Zipkin.Tests")] +#endif diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs index c9001326f..6aba9ba3b 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinAnnotation.cs @@ -13,16 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. // -using Newtonsoft.Json; - namespace OpenTelemetry.Exporter.Zipkin.Implementation { internal class ZipkinAnnotation { - [JsonProperty("timestamp")] public long Timestamp { get; set; } - [JsonProperty("value")] public string Value { get; set; } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs new file mode 100644 index 000000000..6009f8302 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinConversionExtensions.cs @@ -0,0 +1,188 @@ +// +// 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.Diagnostics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using OpenTelemetry.Trace.Export; + +namespace OpenTelemetry.Exporter.Zipkin.Implementation +{ + internal static class ZipkinConversionExtensions + { + private const string StatusCode = "ot.status_code"; + private const string StatusDescription = "ot.status_description"; + + private static readonly Dictionary RemoteEndpointServiceNameKeyResolutionDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["net.peer.name"] = 0, // RemoteEndpoint.ServiceName primary. + ["peer.service"] = 0, // RemoteEndpoint.ServiceName primary. + ["peer.hostname"] = 1, // RemoteEndpoint.ServiceName alternative. + ["peer.address"] = 1, // RemoteEndpoint.ServiceName alternative. + ["http.host"] = 2, // RemoteEndpoint.ServiceName for Http. + ["db.instance"] = 2, // RemoteEndpoint.ServiceName for Redis. + }; + + private static readonly ConcurrentDictionary LocalEndpointCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary RemoteEndpointCache = new ConcurrentDictionary(); + + internal static ZipkinSpan ToZipkinSpan(this SpanData otelSpan, ZipkinEndpoint defaultLocalEndpoint, bool useShortTraceIds = false) + { + var context = otelSpan.Context; + 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); + + if (otelSpan.ParentSpanId != default) + { + spanBuilder.ParentId(EncodeSpanId(otelSpan.ParentSpanId)); + } + + Tuple remoteEndpointServiceName = null; + foreach (var label in otelSpan.Attributes) + { + string key = label.Key; + string strVal = label.Value.ToString(); + + if (strVal != null + && RemoteEndpointServiceNameKeyResolutionDictionary.TryGetValue(key, out int priority) + && (remoteEndpointServiceName == null || priority < remoteEndpointServiceName.Item2)) + { + remoteEndpointServiceName = new Tuple(strVal, priority); + } + + spanBuilder.PutTag(key, strVal); + } + + // 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; + + if (key == Resource.ServiceNameKey && strVal != null) + { + serviceName = strVal; + } + else if (key == Resource.ServiceNamespaceKey && strVal != null) + { + serviceNamespace = strVal; + } + else + { + spanBuilder.PutTag(key, strVal ?? val?.ToString()); + } + } + + if (serviceNamespace != string.Empty) + { + serviceName = serviceNamespace + "." + serviceName; + } + + var endpoint = defaultLocalEndpoint; + + // override default service name + if (serviceName != string.Empty) + { + endpoint = LocalEndpointCache.GetOrAdd(serviceName, _ => new ZipkinEndpoint() + { + Ipv4 = defaultLocalEndpoint.Ipv4, + Ipv6 = defaultLocalEndpoint.Ipv6, + Port = defaultLocalEndpoint.Port, + ServiceName = serviceName, + }); + } + + spanBuilder.LocalEndpoint(endpoint); + + if ((otelSpan.Kind == SpanKind.Client || otelSpan.Kind == SpanKind.Producer) && remoteEndpointServiceName != null) + { + spanBuilder.RemoteEndpoint(RemoteEndpointCache.GetOrAdd(remoteEndpointServiceName.Item1, _ => new ZipkinEndpoint + { + ServiceName = remoteEndpointServiceName.Item1, + })); + } + + var status = otelSpan.Status; + + if (status.IsValid) + { + spanBuilder.PutTag(StatusCode, status.CanonicalCode.ToString()); + + if (status.Description != null) + { + spanBuilder.PutTag(StatusDescription, status.Description); + } + } + + foreach (var annotation in otelSpan.Events) + { + spanBuilder.AddAnnotation(ToEpochMicroseconds(annotation.Timestamp), annotation.Name); + } + + return spanBuilder.Build(); + } + + private static long ToEpochMicroseconds(DateTimeOffset timestamp) + { + return timestamp.ToUnixTimeMilliseconds() * 1000; + } + + private static string EncodeTraceId(ActivityTraceId traceId, bool useShortTraceIds) + { + var id = traceId.ToHexString(); + + if (id.Length > 16 && useShortTraceIds) + { + id = id.Substring(id.Length - 16, 16); + } + + return id; + } + + private static string EncodeSpanId(ActivitySpanId spanId) + { + return spanId.ToHexString(); + } + + private static ZipkinSpanKind ToSpanKind(SpanData otelSpan) + { + if (otelSpan.Kind == SpanKind.Server) + { + return ZipkinSpanKind.SERVER; + } + else if (otelSpan.Kind == SpanKind.Client) + { + return ZipkinSpanKind.CLIENT; + } + + return ZipkinSpanKind.CLIENT; + } + } +} diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs index f5c41a1e7..50cacb179 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs @@ -13,22 +13,16 @@ // See the License for the specific language governing permissions and // limitations under the License. // -using Newtonsoft.Json; - namespace OpenTelemetry.Exporter.Zipkin.Implementation { internal class ZipkinEndpoint { - [JsonProperty("serviceName")] public string ServiceName { get; set; } - [JsonProperty("ipv4")] public string Ipv4 { get; set; } - [JsonProperty("ipv6")] public string Ipv6 { get; set; } - [JsonProperty("port")] public int Port { get; set; } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs index d67679435..dd7f6b5cb 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs @@ -15,51 +15,37 @@ // using System; using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json.Serialization; namespace OpenTelemetry.Exporter.Zipkin.Implementation { internal class ZipkinSpan { - [JsonProperty("traceId")] public string TraceId { get; set; } - [JsonProperty("parentId")] public string ParentId { get; set; } - [JsonProperty("id")] public string Id { get; set; } - [JsonProperty("kind")] - [JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public ZipkinSpanKind Kind { get; set; } - [JsonProperty("name")] public string Name { get; set; } - [JsonProperty("timestamp")] public long Timestamp { get; set; } - [JsonProperty("duration")] public long Duration { get; set; } - [JsonProperty("localEndpoint")] public ZipkinEndpoint LocalEndpoint { get; set; } - [JsonProperty("remoteEndpoint")] public ZipkinEndpoint RemoteEndpoint { get; set; } - [JsonProperty("annotations")] public IList Annotations { get; set; } - [JsonProperty("tags")] public Dictionary Tags { get; set; } - [JsonProperty("debug")] public bool Debug { get; set; } - [JsonProperty("shared")] public bool Shared { get; set; } public static Builder NewBuilder() diff --git a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj index e53935a68..0f81d3019 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj +++ b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj @@ -1,17 +1,13 @@  - net46;netstandard2.0 + netstandard2.0 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 0f847c8c2..24a7c2035 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinTraceExporter.cs @@ -15,17 +15,15 @@ // using System; using System.Collections.Generic; -using System.Diagnostics; +using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Sockets; -using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; using OpenTelemetry.Exporter.Zipkin.Implementation; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; using OpenTelemetry.Trace.Export; namespace OpenTelemetry.Exporter.Zipkin @@ -39,8 +37,10 @@ namespace OpenTelemetry.Exporter.Zipkin private const long NanosPerMillisecond = 1000 * 1000; private const long NanosPerSecond = NanosPerMillisecond * MillisPerSecond; - private static readonly string StatusCode = "ot.status_code"; - private static readonly string StatusDescription = "ot.status_description"; + private static readonly JsonSerializerOptions Options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; private readonly ZipkinTraceExporterOptions options; private readonly ZipkinEndpoint localEndpoint; @@ -84,7 +84,7 @@ namespace OpenTelemetry.Exporter.Zipkin if (shouldExport) { - var zipkinSpan = this.GenerateSpan(data, this.localEndpoint); + var zipkinSpan = data.ToZipkinSpan(this.localEndpoint, this.options.UseShortTraceIds); zipkinSpans.Add(zipkinSpan); } } @@ -112,133 +112,6 @@ namespace OpenTelemetry.Exporter.Zipkin return Task.CompletedTask; } - internal ZipkinSpan GenerateSpan(SpanData otelSpan, ZipkinEndpoint defaultLocalEndpoint) - { - var context = otelSpan.Context; - var startTimestamp = this.ToEpochMicroseconds(otelSpan.StartTimestamp); - var endTimestamp = this.ToEpochMicroseconds(otelSpan.EndTimestamp); - - var spanBuilder = - ZipkinSpan.NewBuilder() - .TraceId(this.EncodeTraceId(context.TraceId)) - .Id(this.EncodeSpanId(context.SpanId)) - .Kind(this.ToSpanKind(otelSpan)) - .Name(otelSpan.Name) - .Timestamp(this.ToEpochMicroseconds(otelSpan.StartTimestamp)) - .Duration(endTimestamp - startTimestamp); - - if (otelSpan.ParentSpanId != default) - { - spanBuilder.ParentId(this.EncodeSpanId(otelSpan.ParentSpanId)); - } - - foreach (var label in otelSpan.Attributes) - { - spanBuilder.PutTag(label.Key, label.Value.ToString()); - } - - // 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 = (string)val; - - if (key == Resource.ServiceNameKey && strVal != null) - { - serviceName = strVal; - } - else if (key == Resource.ServiceNamespaceKey && strVal != null) - { - serviceNamespace = strVal; - } - else - { - spanBuilder.PutTag(key, val?.ToString()); - } - } - - if (serviceNamespace != string.Empty) - { - serviceName = serviceNamespace + "." + serviceName; - } - - var endpoint = defaultLocalEndpoint; - - // override default service name - // TODO: add caching - if (serviceName != string.Empty) - { - endpoint = new ZipkinEndpoint() - { - Ipv4 = defaultLocalEndpoint.Ipv4, - Ipv6 = defaultLocalEndpoint.Ipv6, - Port = defaultLocalEndpoint.Port, - ServiceName = serviceName, - }; - } - - spanBuilder.LocalEndpoint(endpoint); - - var status = otelSpan.Status; - - if (status.IsValid) - { - spanBuilder.PutTag(StatusCode, status.CanonicalCode.ToString()); - - if (status.Description != null) - { - spanBuilder.PutTag(StatusDescription, status.Description); - } - } - - foreach (var annotation in otelSpan.Events) - { - spanBuilder.AddAnnotation(this.ToEpochMicroseconds(annotation.Timestamp), annotation.Name); - } - - return spanBuilder.Build(); - } - - private long ToEpochMicroseconds(DateTimeOffset timestamp) - { - return timestamp.ToUnixTimeMilliseconds() * 1000; - } - - private string EncodeTraceId(ActivityTraceId traceId) - { - var id = traceId.ToHexString(); - - if (id.Length > 16 && this.options.UseShortTraceIds) - { - id = id.Substring(id.Length - 16, 16); - } - - return id; - } - - private string EncodeSpanId(ActivitySpanId spanId) - { - return spanId.ToHexString(); - } - - private ZipkinSpanKind ToSpanKind(SpanData otelSpan) - { - if (otelSpan.Kind == SpanKind.Server) - { - return ZipkinSpanKind.SERVER; - } - else if (otelSpan.Kind == SpanKind.Client) - { - return ZipkinSpanKind.CLIENT; - } - - return ZipkinSpanKind.CLIENT; - } - private Task SendSpansAsync(IEnumerable spans, CancellationToken cancellationToken) { var requestUri = this.options.Endpoint; @@ -271,17 +144,7 @@ namespace OpenTelemetry.Exporter.Zipkin private HttpContent GetRequestContent(IEnumerable toSerialize) { - var content = string.Empty; - try - { - content = JsonConvert.SerializeObject(toSerialize); - } - catch (Exception) - { - // Ignored - } - - return new StringContent(content, Encoding.UTF8, "application/json"); + return new JsonContent(toSerialize, Options); } private ZipkinEndpoint GetLocalZipkinEndpoint() @@ -358,5 +221,34 @@ namespace OpenTelemetry.Exporter.Zipkin return result; } + + private class JsonContent : HttpContent + { + private static readonly MediaTypeHeaderValue JsonHeader = new MediaTypeHeaderValue("application/json") + { + CharSet = "utf-8", + }; + + private readonly IEnumerable spans; + private readonly JsonSerializerOptions options; + + public JsonContent(IEnumerable spans, JsonSerializerOptions options) + { + 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 bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } + } } } diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinSpanConverterTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinSpanConverterTests.cs new file mode 100644 index 000000000..af3adaa14 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/Implementation/ZipkinSpanConverterTests.cs @@ -0,0 +1,151 @@ +// +// 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.Generic; +using System.Diagnostics; +using OpenTelemetry.Exporter.Zipkin.Implementation; +using OpenTelemetry.Trace; +using OpenTelemetry.Trace.Export; +using Xunit; + +namespace OpenTelemetry.Exporter.Zipkin.Tests.Implementation +{ + public class ZipkinTraceExporterRemoteEndpointTests + { + private static readonly ZipkinEndpoint DefaultZipkinEndpoint = new ZipkinEndpoint + { + ServiceName = "TestService", + }; + + [Fact] + public void ZipkinSpanConverterTest_GenerateSpan_RemoteEndpointOmittedByDefault() + { + // Arrange + var span = CreateTestSpan(); + + // Act & Assert + var zipkinSpan = ZipkinConversionExtensions.ToZipkinSpan(span, DefaultZipkinEndpoint); + + Assert.Null(zipkinSpan.RemoteEndpoint); + } + + [Fact] + public void ZipkinSpanConverterTest_GenerateSpan_RemoteEndpointResolution() + { + // Arrange + var span = CreateTestSpan( + additionalAttributes: new Dictionary + { + ["net.peer.name"] = "RemoteServiceName", + }); + + // Act & Assert + var zipkinSpan = ZipkinConversionExtensions.ToZipkinSpan(span, DefaultZipkinEndpoint); + + Assert.NotNull(zipkinSpan.RemoteEndpoint); + Assert.Equal("RemoteServiceName", zipkinSpan.RemoteEndpoint.ServiceName); + } + + [Fact] + public void ZipkinSpanConverterTest_GenerateSpan_RemoteEndpointResolutionPriority() + { + // Arrange + var span = CreateTestSpan( + additionalAttributes: new Dictionary + { + ["http.host"] = "DiscardedRemoteServiceName", + ["net.peer.name"] = "RemoteServiceName", + ["peer.hostname"] = "DiscardedRemoteServiceName", + }); + + // Act & Assert + var zipkinSpan = ZipkinConversionExtensions.ToZipkinSpan(span, DefaultZipkinEndpoint); + + Assert.NotNull(zipkinSpan.RemoteEndpoint); + Assert.Equal("RemoteServiceName", zipkinSpan.RemoteEndpoint.ServiceName); + } + + internal SpanData CreateTestSpan( + bool setAttributes = true, + Dictionary additionalAttributes = null, + bool addEvents = true, + bool addLinks = true) + { + var startTimestamp = DateTime.UtcNow; + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = DateTime.UtcNow; + var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); + + var spanId = ActivitySpanId.CreateRandom(); + var parentSpanId = ActivitySpanId.CreateFromBytes(new byte[] { 12, 23, 34, 45, 56, 67, 78, 89 }); + + var attributes = new Dictionary + { + { "stringKey", "value"}, + { "longKey", 1L}, + { "longKey2", 1 }, + { "doubleKey", 1D}, + { "doubleKey2", 1F}, + { "boolKey", true}, + }; + if (additionalAttributes != null) + { + foreach (var attribute in additionalAttributes) + { + attributes.Add(attribute.Key, attribute.Value); + } + } + + var events = new List + { + new Event( + "Event1", + eventTimestamp, + new Dictionary + { + { "key", "value" }, + } + ), + new Event( + "Event2", + eventTimestamp, + new Dictionary + { + { "key", "value" }, + } + ), + }; + + var linkedSpanId = ActivitySpanId.CreateFromString("888915b6286b9c41".AsSpan()); + + return new SpanData( + "Name", + new SpanContext(traceId, spanId, ActivityTraceFlags.Recorded), + parentSpanId, + SpanKind.Client, + startTimestamp, + setAttributes ? attributes : null, + addEvents ? events : null, + addLinks ? new[] { new Link(new SpanContext( + traceId, + linkedSpanId, + ActivityTraceFlags.Recorded)), } : null, + null, + Status.Ok, + endTimestamp); + } + } +} diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj b/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj new file mode 100644 index 000000000..2b9ec69ab --- /dev/null +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj @@ -0,0 +1,32 @@ + + + Unit test project for Zipkin Exporter for OpenTelemetry + netcoreapp3.1 + $(TargetFrameworks);net461 + false + + + + + PreserveNewest + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/xunit.runner.json b/test/OpenTelemetry.Exporter.Zipkin.Tests/xunit.runner.json new file mode 100644 index 000000000..9fbc90115 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "maxParallelThreads": 1, + "parallelizeTestCollections": false +} \ No newline at end of file