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:
parent
3cba598345
commit
a7206200a5
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"maxParallelThreads": 1,
|
||||
"parallelizeTestCollections": false
|
||||
}
|
||||
Loading…
Reference in New Issue