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 <S.Kanzhelev@live.com>
This commit is contained in:
Mikel Blanchard 2020-02-18 15:16:15 -08:00 committed by GitHub
parent 3cba598345
commit a7206200a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 691 additions and 185 deletions

View File

@ -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

View File

@ -15,10 +15,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\OpenTelemetry.Exporter.Zipkin\OpenTelemetry.Exporter.Zipkin.csproj" />
<ProjectReference Include="..\src\OpenTelemetry\OpenTelemetry.csproj" />
</ItemGroup>

View File

@ -0,0 +1,232 @@
// <copyright file="ZipkinExporterBenchmarks.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<SpanData>();
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<string, object>
{
{ "stringKey", "value"},
{ "longKey", 1L},
{ "longKey2", 1 },
{ "doubleKey", 1D},
{ "doubleKey2", 1F},
{ "boolKey", true},
};
var events = new List<Event>
{
new Event(
"Event1",
eventTimestamp,
new Dictionary<string, object>
{
{ "key", "value" },
}
),
new Event(
"Event2",
eventTimestamp,
new Dictionary<string, object>
{
{ "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<HttpListenerContext> 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<HttpListenerContext> 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;
}
}
}
}

View File

@ -15,7 +15,6 @@
// </copyright>
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<OpenTelemetrySdkBenchmarks>();
}
[Benchmark]
public TelemetrySpan CreateSpan_Sampled() => SpanCreationScenarios.CreateSpan(this.alwaysSampleTracer);

27
benchmarks/Program.cs Normal file
View File

@ -0,0 +1,27 @@
// <copyright file="Program.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using BenchmarkDotNet.Running;
namespace Benchmarks
{
internal static class Program
{
public static void Main(string[] args)
{
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
}
}

View File

@ -13,3 +13,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Runtime.CompilerServices;
#if SIGNED
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Zipkin.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")]
#else
[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Zipkin.Tests")]
#endif

View File

@ -13,16 +13,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
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; }
}
}

View File

@ -0,0 +1,188 @@
// <copyright file="ZipkinConversionExtensions.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<string, int> RemoteEndpointServiceNameKeyResolutionDictionary = new Dictionary<string, int>(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<string, ZipkinEndpoint> LocalEndpointCache = new ConcurrentDictionary<string, ZipkinEndpoint>();
private static readonly ConcurrentDictionary<string, ZipkinEndpoint> RemoteEndpointCache = new ConcurrentDictionary<string, ZipkinEndpoint>();
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<string, int> 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<string, int>(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;
}
}
}

View File

@ -13,22 +13,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
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; }
}
}

View File

@ -15,51 +15,37 @@
// </copyright>
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<ZipkinAnnotation> Annotations { get; set; }
[JsonProperty("tags")]
public Dictionary<string, string> Tags { get; set; }
[JsonProperty("debug")]
public bool Debug { get; set; }
[JsonProperty("shared")]
public bool Shared { get; set; }
public static Builder NewBuilder()

View File

@ -1,17 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net46;netstandard2.0</TargetFrameworks>
<TargetFramework>netstandard2.0</TargetFramework>
<Description>Zipkin exporter for OpenTelemetry</Description>
<PackageTags>$(PackageTags);Zipkin;distributed-tracing</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenTelemetry\OpenTelemetry.csproj" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net46'">
<Reference Include="System.Net.Http" />
<PackageReference Include="System.Text.Json" Version="4.7.0" />
</ItemGroup>
</Project>

View File

@ -15,17 +15,15 @@
// </copyright>
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<ZipkinSpan> spans, CancellationToken cancellationToken)
{
var requestUri = this.options.Endpoint;
@ -271,17 +144,7 @@ namespace OpenTelemetry.Exporter.Zipkin
private HttpContent GetRequestContent(IEnumerable<ZipkinSpan> 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<ZipkinSpan> spans;
private readonly JsonSerializerOptions options;
public JsonContent(IEnumerable<ZipkinSpan> 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;
}
}
}
}

View File

@ -0,0 +1,151 @@
// <copyright file="ZipkinSpanConverterTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<string, object>
{
["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<string, object>
{
["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<string, object> 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<string, object>
{
{ "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<Event>
{
new Event(
"Event1",
eventTimestamp,
new Dictionary<string, object>
{
{ "key", "value" },
}
),
new Event(
"Event2",
eventTimestamp,
new Dictionary<string, object>
{
{ "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);
}
}
}

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Unit test project for Zipkin Exporter for OpenTelemetry</Description>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<TargetFrameworks Condition="$(OS) == 'Windows_NT'">$(TargetFrameworks);net461</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Content Include="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.1.1" />
<PackageReference Include="Moq" Version="4.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenTelemetry.Api\OpenTelemetry.Api.csproj" />
<ProjectReference Include="..\..\src\OpenTelemetry.Exporter.Zipkin\OpenTelemetry.Exporter.Zipkin.csproj" />
<ProjectReference Include="..\..\lib\Thrift\Thrift.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
{
"maxParallelThreads": 1,
"parallelizeTestCollections": false
}