diff --git a/src/OpenTelemetry.Exporter.InfluxDB/InfluxDBExporterExtensions.cs b/src/OpenTelemetry.Exporter.InfluxDB/InfluxDBExporterExtensions.cs index 88e917cf90..d025d5739d 100644 --- a/src/OpenTelemetry.Exporter.InfluxDB/InfluxDBExporterExtensions.cs +++ b/src/OpenTelemetry.Exporter.InfluxDB/InfluxDBExporterExtensions.cs @@ -56,14 +56,11 @@ public static MeterProviderBuilder AddInfluxDBMetricsExporter(this MeterProvider return builder; } - private static IMetricsWriter CreateMetricsWriter(MetricsSchema metricsSchema) + private static IMetricsWriter CreateMetricsWriter(MetricsSchema metricsSchema) => metricsSchema switch { - return metricsSchema switch - { - MetricsSchema.TelegrafPrometheusV2 => new TelegrafPrometheusWriterV2(), - MetricsSchema.TelegrafPrometheusV1 => new TelegrafPrometheusWriterV1(), - MetricsSchema.None => new TelegrafPrometheusWriterV1(), - _ => new TelegrafPrometheusWriterV1(), - }; - } + MetricsSchema.TelegrafPrometheusV2 => new TelegrafPrometheusWriterV2(), + MetricsSchema.TelegrafPrometheusV1 => new TelegrafPrometheusWriterV1(), + MetricsSchema.None => new TelegrafPrometheusWriterV1(), + _ => new TelegrafPrometheusWriterV1(), + }; } diff --git a/src/OpenTelemetry.Exporter.Instana/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Instana/.publicApi/PublicAPI.Unshipped.txt index e69de29bb2..c1a6c58297 100644 --- a/src/OpenTelemetry.Exporter.Instana/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Instana/.publicApi/PublicAPI.Unshipped.txt @@ -0,0 +1,15 @@ +OpenTelemetry.Exporter.Instana.InstanaExporterOptions +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.AgentKey.get -> string! +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.AgentKey.set -> void +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions! +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.BatchExportProcessorOptions.set -> void +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.EndpointUri.get -> System.Uri! +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.EndpointUri.set -> void +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.HttpClientFactory.get -> System.Func? +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.HttpClientFactory.set -> void +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.InstanaExporterOptions() -> void +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.ProxyUri.get -> System.Uri? +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.ProxyUri.set -> void +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.UtcNow.get -> System.Func! +OpenTelemetry.Exporter.Instana.InstanaExporterOptions.UtcNow.set -> void +static OpenTelemetry.Exporter.Instana.TracerProviderBuilderExtensions.AddInstanaExporter(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Action? configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder! \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md index 3b3c48378b..9d1048a38b 100644 --- a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md @@ -2,6 +2,19 @@ ## Unreleased +* Add `net8.0` and `net10.0` target frameworks. + ([#4153](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4153)) + +* Add support for configuring the Instana exporter using `InstanaExporterOptions`. + ([#4153](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4153)) + +* **Breaking change**: TLS certificate validation is no longer unconditionally + disabled when a proxy is configured using the `INSTANA_ENDPOINT_PROXY` environment + variable. To restore the previous behaviour and disable TLS certificate validation + use the `InstanaExporterOptions.HttpClientFactory` property to configure a custom + `HttpClient` for the exporter to use. + ([#4153](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4153)) + ## 1.0.7 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Exporter.Instana/IInstanaExporterHelper.cs b/src/OpenTelemetry.Exporter.Instana/IInstanaExporterHelper.cs deleted file mode 100644 index 2d234b33d3..0000000000 --- a/src/OpenTelemetry.Exporter.Instana/IInstanaExporterHelper.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using OpenTelemetry.Resources; - -namespace OpenTelemetry.Exporter.Instana; - -internal interface IInstanaExporterHelper -{ - bool IsWindows(); - - Resource GetParentProviderResource(BaseExporter otelExporter); -} diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/ISpanSender.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/ISpanSender.cs deleted file mode 100644 index 2d6f1ccf26..0000000000 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/ISpanSender.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Exporter.Instana.Implementation; - -internal interface ISpanSender -{ - void Enqueue(InstanaSpan instanaSpan); -} diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaExporterEventSource.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaExporterEventSource.cs index 8ddb58ce66..23dc11ec08 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaExporterEventSource.cs @@ -26,7 +26,5 @@ public void FailedExport(Exception ex) [Event(1, Message = "Failed to send spans: '{0}'", Level = EventLevel.Error)] public void FailedExport(string exception) - { - this.WriteEvent(1, exception); - } + => this.WriteEvent(1, exception); } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpan.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpan.cs index 159baf28cf..12cf31565a 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpan.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpan.cs @@ -5,177 +5,168 @@ namespace OpenTelemetry.Exporter.Instana.Implementation; +#pragma warning disable SA1402 // File may only contain a single type + internal enum SpanKind { -#pragma warning disable SA1602 // Enumeration items should be documented ENTRY, -#pragma warning restore SA1602 // Enumeration items should be documented -#pragma warning disable SA1602 // Enumeration items should be documented EXIT, -#pragma warning restore SA1602 // Enumeration items should be documented -#pragma warning disable SA1602 // Enumeration items should be documented INTERMEDIATE, -#pragma warning restore SA1602 // Enumeration items should be documented -#pragma warning disable SA1602 // Enumeration items should be documented NOT_SET, -#pragma warning restore SA1602 // Enumeration items should be documented } -internal class InstanaSpan +internal sealed class InstanaSpan { - private InstanaSpanTransformInfo transformInfo = new(); - private string n = string.Empty; - private string t = string.Empty; - private string lt = string.Empty; - private From f = new(); - private string p = string.Empty; - private string s = string.Empty; - private SpanKind k = SpanKind.NOT_SET; - private long ts; - private long d; - private bool tp; - private int ec; - private Data data = new() + public InstanaSpan() { - data = new Dictionary(8), - Events = new List(8), - Tags = new Dictionary(2), - }; + this.TransformInfo = new(); + this.N = string.Empty; + this.T = string.Empty; + this.Lt = string.Empty; + this.F = new(); + this.P = string.Empty; + this.S = string.Empty; + this.K = SpanKind.NOT_SET; + this.Data = new Data + { + Values = new Dictionary(8), + Events = new(8), + Tags = new Dictionary(2), + }; + } public InstanaSpanTransformInfo TransformInfo { - get => this.transformInfo; + get => field; set { Guard.ThrowIfNull(value); - this.transformInfo = value; + field = value; } } public string N { - get => this.n; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.n = value; + field = value; } } public string T { - get => this.t; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.t = value; + field = value; } } public string Lt { - get => this.lt; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.lt = value; + field = value; } } public From F { - get => this.f; + get => field; set { Guard.ThrowIfNull(value); - this.f = value; + field = value; } } public string P { - get => this.p; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.p = value; + field = value; } } public string S { - get => this.s; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.s = value; + field = value; } } public SpanKind K { - get => this.k; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.k = value; + field = value; } } public Data Data { - get => this.data; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.data = value; + field = value; } } public long Ts { - get => this.ts; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.ts = value; + field = value; } } public long D { - get => this.d; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.d = value; + field = value; } } public bool Tp { - get => this.tp; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.tp = value; + field = value; } } public int Ec { - get => this.ec; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.ec = value; + field = value; } } } -#pragma warning disable SA1402 // File may only contain a single type -internal class From -#pragma warning restore SA1402 // File may only contain a single type +internal sealed class From { internal From() { @@ -183,85 +174,80 @@ internal From() this.H = string.Empty; } - public string E { get; internal set; } + public string E { get; set; } - public string H { get; internal set; } + public string H { get; set; } - internal bool IsEmpty() - { - return string.IsNullOrEmpty(this.E) && string.IsNullOrEmpty(this.H); - } + internal bool IsEmpty() => string.IsNullOrEmpty(this.E) && string.IsNullOrEmpty(this.H); } -#pragma warning disable SA1402 // File may only contain a single type -internal class Data -#pragma warning restore SA1402 // File may only contain a single type +internal sealed class Data { - private List events = new(8); - private Dictionary dataField = new(8); - private Dictionary tags = new(2); - -#pragma warning disable SA1300 // Element should begin with upper-case letter + public Data() + { + this.Events = new(8); + this.Values = new(8); + this.Tags = new(2); + } - public Dictionary data + public Dictionary Values { - get => this.dataField; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.dataField = value; + field = value; } } -#pragma warning restore SA1300 // Element should begin with upper-case letter - public Dictionary Tags { - get => this.tags; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.tags = value; + field = value; } } public List Events { - get => this.events; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.events = value; + field = value; } } } -#pragma warning disable SA1402 // File may only contain a single type -internal class SpanEvent -#pragma warning restore SA1402 // File may only contain a single type +internal sealed class SpanEvent { - private string name = string.Empty; - private Dictionary tags = []; + public SpanEvent() + { + this.Name = string.Empty; + this.Tags = []; + } public string Name { - get => this.name; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.name = value; + field = value; } } - public long Ts { get; internal set; } + public long Ts { get; set; } public Dictionary Tags { - get => this.tags; - internal set + get => field; + set { Guard.ThrowIfNull(value); - this.tags = value; + field = value; } } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanFactory.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanFactory.cs index fe3dbf4843..d299604f4e 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanFactory.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanFactory.cs @@ -3,22 +3,16 @@ namespace OpenTelemetry.Exporter.Instana.Implementation; -internal class InstanaSpanFactory +internal static class InstanaSpanFactory { - internal static InstanaSpan CreateSpan() + internal static InstanaSpan CreateSpan() => new() { - var instanaSpan = new InstanaSpan + Data = new Data() { - Data = new Data() - { - data = [], - Tags = [], - Events = new List(8), - }, - - TransformInfo = new InstanaSpanTransformInfo(), - }; - - return instanaSpan; - } + Values = [], + Tags = [], + Events = new(8), + }, + TransformInfo = new(), + }; } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs index 3046fa58ba..c7d5a2494e 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs @@ -6,98 +6,91 @@ namespace OpenTelemetry.Exporter.Instana.Implementation; +// TODO Use a proper JSON serializer that encodes strings safely. + internal static class InstanaSpanSerializer { -#pragma warning disable SA1310 // Field names should not contain underscore - private const string COMMA = ","; - private const string OPEN_BRACE = "{"; - private const string CLOSE_BRACE = "}"; - private const string QUOTE = "\""; - private const string COLON = ":"; - private const string QUOTE_COLON = QUOTE + COLON; - private const string QUOTE_COLON_QUOTE = QUOTE + COLON + QUOTE; - private const string QUOTE_COMMA_QUOTE = QUOTE + COMMA + QUOTE; -#pragma warning restore SA1310 // Field names should not contain underscore - private static readonly long UnixZeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0).Ticks; - - internal static IEnumerator? GetSpanTagsEnumerator(InstanaSpan instanaSpan) - { - return instanaSpan.Data.Tags.GetEnumerator(); - } - - internal static IEnumerator? GetSpanEventsEnumerator(InstanaSpan instanaSpan) - { - return instanaSpan.Data.Events.GetEnumerator(); - } - - internal static async Task SerializeToStreamWriterAsync(InstanaSpan instanaSpan, StreamWriter writer) + private const string Comma = ","; + private const string OpenCurlyBrace = "{"; + private const string CloseCurlyBrace = "}"; + private const string Quote = "\""; + private const string Colon = ":"; + private const string QuoteColon = Quote + Colon; + private const string QuoteColonQuote = Quote + Colon + Quote; + private const string QuoteCommaQuote = Quote + Comma + Quote; + + private static readonly long UnixZeroTime = +#if NET + DateTimeOffset.UnixEpoch.Ticks; +#else + new DateTime(1970, 1, 1, 0, 0, 0, 0).Ticks; +#endif + + internal static IEnumerator? GetSpanTagsEnumerator(InstanaSpan instanaSpan) => instanaSpan.Data.Tags.GetEnumerator(); + + internal static IEnumerator? GetSpanEventsEnumerator(InstanaSpan instanaSpan) => instanaSpan.Data.Events.GetEnumerator(); + + internal static void SerializeToStreamWriter(InstanaSpan instanaSpan, StreamWriter writer) { - await writer.WriteAsync(OPEN_BRACE).ConfigureAwait(false); - await AppendProperty(instanaSpan.T, "t", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); - await AppendProperty(instanaSpan.S, "s", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + writer.Write(OpenCurlyBrace); + AppendProperty(instanaSpan.T, "t", writer); + writer.Write(Comma); + AppendProperty(instanaSpan.S, "s", writer); + writer.Write(Comma); if (!string.IsNullOrEmpty(instanaSpan.P)) { - await AppendProperty(instanaSpan.P, "p", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + AppendProperty(instanaSpan.P, "p", writer); + writer.Write(Comma); } if (!string.IsNullOrEmpty(instanaSpan.Lt)) { - await AppendProperty(instanaSpan.Lt, "lt", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + AppendProperty(instanaSpan.Lt, "lt", writer); + writer.Write(Comma); } if (instanaSpan.Tp) { -#pragma warning disable CA1308 // Normalize strings to uppercase - await AppendProperty(true.ToString().ToLowerInvariant(), "tp", writer).ConfigureAwait(false); -#pragma warning restore CA1308 // Normalize strings to uppercase - await writer.WriteAsync(COMMA).ConfigureAwait(false); + AppendProperty("true", "tp", writer); + writer.Write(Comma); } if (instanaSpan.K != SpanKind.NOT_SET) { - await AppendProperty((((int)instanaSpan.K) + 1).ToString(CultureInfo.InvariantCulture), "k", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + AppendProperty((((int)instanaSpan.K) + 1).ToString(CultureInfo.InvariantCulture), "k", writer); + writer.Write(Comma); } - await AppendProperty(instanaSpan.N, "n", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); - await AppendPropertyAsync(DateToUnixMillis(instanaSpan.Ts), "ts", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); - await AppendPropertyAsync(instanaSpan.D / 10_000L, "d", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); - await AppendObjectAsync(SerializeDataAsync, "data", instanaSpan, writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); - await AppendObjectAsync(SerializeFromAsync, "f", instanaSpan, writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); - await AppendPropertyAsync(instanaSpan.Ec, "ec", writer).ConfigureAwait(false); - await writer.WriteAsync(CLOSE_BRACE).ConfigureAwait(false); + AppendProperty(instanaSpan.N, "n", writer); + writer.Write(Comma); + AppendProperty(DateToUnixMillis(instanaSpan.Ts), "ts", writer); + writer.Write(Comma); + AppendProperty(instanaSpan.D / 10_000L, "d", writer); + writer.Write(Comma); + AppendObject(SerializeData, "data", instanaSpan, writer); + writer.Write(Comma); + AppendObject(SerializeFrom, "f", instanaSpan, writer); + writer.Write(Comma); + AppendProperty(instanaSpan.Ec, "ec", writer); + writer.Write(CloseCurlyBrace); } - private static async Task SerializeFromAsync(InstanaSpan instanaSpan, StreamWriter writer) + private static void SerializeFrom(InstanaSpan instanaSpan, StreamWriter writer) { - await writer.WriteAsync("{\"e\":\"").ConfigureAwait(false); - await writer.WriteAsync(instanaSpan.F.E).ConfigureAwait(false); - await writer.WriteAsync("\"}").ConfigureAwait(false); + writer.Write("{\"e\":\""); + writer.Write(instanaSpan.F.E); + writer.Write("\"}"); } - private static long DateToUnixMillis(long timeStamp) - { - return (timeStamp - UnixZeroTime) / 10000; - } + private static long DateToUnixMillis(long timeStamp) => (timeStamp - UnixZeroTime) / 10_000; - private static async Task SerializeTagsAsync(InstanaSpan instanaSpan, StreamWriter writer) - { - await SerializeTagsLogicAsync(instanaSpan.Data.Tags, writer).ConfigureAwait(false); - } + private static void SerializeTags(InstanaSpan instanaSpan, StreamWriter writer) => + SerializeTagsLogic(instanaSpan.Data.Tags, writer); - private static async Task SerializeTagsLogicAsync(Dictionary? tags, StreamWriter writer) + private static void SerializeTagsLogic(Dictionary? tags, StreamWriter writer) { - await writer.WriteAsync(OPEN_BRACE).ConfigureAwait(false); + writer.Write(OpenCurlyBrace); if (tags == null) { return; @@ -112,18 +105,18 @@ private static async Task SerializeTagsLogicAsync(Dictionary? ta { if (i > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + writer.Write(Comma); } else { i = 1; } - await writer.WriteAsync(QUOTE).ConfigureAwait(false); - await writer.WriteAsync(enumerator.Current.Key).ConfigureAwait(false); - await writer.WriteAsync(QUOTE_COLON_QUOTE).ConfigureAwait(false); - await writer.WriteAsync(enumerator.Current.Value).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + writer.Write(Quote); + writer.Write(enumerator.Current.Key); + writer.Write(QuoteColonQuote); + writer.Write(enumerator.Current.Value); + writer.Write(Quote); } } catch (InvalidOperationException) @@ -134,43 +127,43 @@ private static async Task SerializeTagsLogicAsync(Dictionary? ta } } - await writer.WriteAsync(CLOSE_BRACE).ConfigureAwait(false); + writer.Write(CloseCurlyBrace); } - private static async Task AppendProperty(string? value, string? name, StreamWriter json) + private static void AppendProperty(string? value, string? name, StreamWriter json) { - await json.WriteAsync(QUOTE).ConfigureAwait(false); - await json.WriteAsync(name).ConfigureAwait(false); - await json.WriteAsync(QUOTE_COLON_QUOTE).ConfigureAwait(false); - await json.WriteAsync(value).ConfigureAwait(false); - await json.WriteAsync(QUOTE).ConfigureAwait(false); + json.Write(Quote); + json.Write(name); + json.Write(QuoteColonQuote); + json.Write(value); + json.Write(Quote); } - private static async Task AppendPropertyAsync(long value, string name, StreamWriter json) + private static void AppendProperty(long value, string name, StreamWriter json) { - await json.WriteAsync(QUOTE).ConfigureAwait(false); - await json.WriteAsync(name).ConfigureAwait(false); - await json.WriteAsync(QUOTE_COLON).ConfigureAwait(false); - await json.WriteAsync(value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + json.Write(Quote); + json.Write(name); + json.Write(QuoteColon); + json.Write(value.ToString(CultureInfo.InvariantCulture)); } - private static async Task AppendObjectAsync(Func valueFunction, string name, InstanaSpan instanaSpan, StreamWriter json) + private static void AppendObject(Action valueFunction, string name, InstanaSpan instanaSpan, StreamWriter json) { - await json.WriteAsync(QUOTE).ConfigureAwait(false); - await json.WriteAsync(name).ConfigureAwait(false); - await json.WriteAsync(QUOTE_COLON).ConfigureAwait(false); - await valueFunction(instanaSpan, json).ConfigureAwait(false); + json.Write(Quote); + json.Write(name); + json.Write(QuoteColon); + valueFunction(instanaSpan, json); } - private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWriter writer) + private static void SerializeData(InstanaSpan instanaSpan, StreamWriter writer) { - await writer.WriteAsync(OPEN_BRACE).ConfigureAwait(false); - if (instanaSpan.Data.data == null) + writer.Write(OpenCurlyBrace); + if (instanaSpan.Data.Values == null) { return; } - using (var enumerator = instanaSpan.Data.data.GetEnumerator()) + using (var enumerator = instanaSpan.Data.Values.GetEnumerator()) { byte i = 0; try @@ -179,48 +172,47 @@ private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWrit { if (i > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + writer.Write(Comma); } else { i = 1; } - await writer.WriteAsync(QUOTE).ConfigureAwait(false); - await writer.WriteAsync(enumerator.Current.Key).ConfigureAwait(false); - await writer.WriteAsync(QUOTE_COLON_QUOTE).ConfigureAwait(false); - await writer.WriteAsync(enumerator.Current.Value.ToString()).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + writer.Write(Quote); + writer.Write(enumerator.Current.Key); + writer.Write(QuoteColonQuote); + writer.Write(enumerator.Current.Value.ToString()); + writer.Write(Quote); } } catch (InvalidOperationException) { - // if the collection gets modified while serializing, we might get a collision. - // There is no good way of preventing this and continuing normally except locking - // which needs investigation + // If the collection gets modified while serializing, we might get a collision. + // There is no good way of preventing this and continuing normally except locking. } } if (instanaSpan.Data.Tags.Count > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + writer.Write(Comma); - // serialize tags - await AppendObjectAsync(SerializeTagsAsync, InstanaExporterConstants.TAGS_FIELD, instanaSpan, writer).ConfigureAwait(false); + // Serialize tags + AppendObject(SerializeTags, InstanaExporterConstants.TagsField, instanaSpan, writer); } if (instanaSpan.Data.Events.Count > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + writer.Write(Comma); - // serialize tags - await AppendObjectAsync(SerializeEventsAsync, InstanaExporterConstants.EVENTS_FIELD, instanaSpan, writer).ConfigureAwait(false); + // Serialize events + AppendObject(SerializeEvents, InstanaExporterConstants.EventsField, instanaSpan, writer); } - await writer.WriteAsync(CLOSE_BRACE).ConfigureAwait(false); + writer.Write(CloseCurlyBrace); } - private static async Task SerializeEventsAsync(InstanaSpan instanaSpan, StreamWriter writer) + private static void SerializeEvents(InstanaSpan instanaSpan, StreamWriter writer) { if (instanaSpan.Data.Events == null) { @@ -231,49 +223,48 @@ private static async Task SerializeEventsAsync(InstanaSpan instanaSpan, StreamWr byte i = 0; try { - await writer.WriteAsync("[").ConfigureAwait(false); + writer.Write("["); while (enumerator.MoveNext()) { if (i > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + writer.Write(Comma); } else { i = 1; } - await writer.WriteAsync(OPEN_BRACE).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); - await writer.WriteAsync(InstanaExporterConstants.EVENT_NAME_FIELD).ConfigureAwait(false); - await writer.WriteAsync(QUOTE_COLON_QUOTE).ConfigureAwait(false); - await writer.WriteAsync(enumerator.Current.Name).ConfigureAwait(false); - await writer.WriteAsync(QUOTE_COMMA_QUOTE).ConfigureAwait(false); - await writer.WriteAsync(InstanaExporterConstants.EVENT_TIMESTAMP_FIELD).ConfigureAwait(false); - await writer.WriteAsync(QUOTE_COLON_QUOTE).ConfigureAwait(false); - await writer.WriteAsync(DateToUnixMillis(enumerator.Current.Ts).ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + writer.Write(OpenCurlyBrace); + writer.Write(Quote); + writer.Write(InstanaExporterConstants.EventNameField); + writer.Write(QuoteColonQuote); + writer.Write(enumerator.Current.Name); + writer.Write(QuoteCommaQuote); + writer.Write(InstanaExporterConstants.EventTimestampField); + writer.Write(QuoteColonQuote); + writer.Write(DateToUnixMillis(enumerator.Current.Ts).ToString(CultureInfo.InvariantCulture)); + writer.Write(Quote); if (enumerator.Current.Tags.Count > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); - await writer.WriteAsync(InstanaExporterConstants.TAGS_FIELD).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); - await writer.WriteAsync(COLON).ConfigureAwait(false); - await SerializeTagsLogicAsync(enumerator.Current.Tags, writer).ConfigureAwait(false); + writer.Write(Comma); + writer.Write(Quote); + writer.Write(InstanaExporterConstants.TagsField); + writer.Write(Quote); + writer.Write(Colon); + SerializeTagsLogic(enumerator.Current.Tags, writer); } - await writer.WriteAsync(CLOSE_BRACE).ConfigureAwait(false); + writer.Write(CloseCurlyBrace); } - await writer.WriteAsync("]").ConfigureAwait(false); + writer.Write("]"); } catch (InvalidOperationException) { - // if the collection gets modified while serializing, we might get a collision. - // There is no good way of preventing this and continuing normally except locking - // which needs investigation + // If the collection gets modified while serializing, we might get a collision. + // There is no good way of preventing this and continuing normally except locking. } } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanTransformInfo.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanTransformInfo.cs index 0fffa8e9c5..57fe8d2e80 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanTransformInfo.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanTransformInfo.cs @@ -5,32 +5,35 @@ namespace OpenTelemetry.Exporter.Instana.Implementation; -internal class InstanaSpanTransformInfo +internal sealed class InstanaSpanTransformInfo { - private string statusCode = string.Empty; - private string statusDesc = string.Empty; + public InstanaSpanTransformInfo() + { + this.StatusCode = string.Empty; + this.StatusDesc = string.Empty; + } public string StatusCode { - get => this.statusCode; + get => field; set { Guard.ThrowIfNull(value); - this.statusCode = value; + field = value; } } public string StatusDesc { - get => this.statusDesc; + get => field; set { Guard.ThrowIfNull(value); - this.statusDesc = value; + field = value; } } - public bool HasExceptionEvent { get; internal set; } + public bool HasExceptionEvent { get; set; } - public bool IsEntrySpan { get; internal set; } + public bool IsEntrySpan { get; set; } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ActivityProcessorBase.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ActivityProcessorBase.cs index ff86416aae..ddb70903d9 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ActivityProcessorBase.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ActivityProcessorBase.cs @@ -9,13 +9,8 @@ internal abstract class ActivityProcessorBase : IActivityProcessor { public IActivityProcessor? NextProcessor { get; set; } - public virtual async Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) - { - if (this.NextProcessor != null) - { - await this.NextProcessor.ProcessAsync(activity, instanaSpan).ConfigureAwait(false); - } - } + public virtual void Process(Activity activity, InstanaSpan instanaSpan) + => this.NextProcessor?.Process(activity, instanaSpan); protected virtual void PreProcess(Activity activity, InstanaSpan instanaSpan) { @@ -23,8 +18,8 @@ protected virtual void PreProcess(Activity activity, InstanaSpan instanaSpan) instanaSpan.Data ??= new Data() { - data = [], - Events = new List(8), + Values = [], + Events = new(8), Tags = [], }; } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs index 3c6447bcec..7edb5692e7 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs @@ -5,13 +5,13 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal class DefaultActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class DefaultActivityProcessor : ActivityProcessorBase { - public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) + public override void Process(Activity activity, InstanaSpan instanaSpan) { this.PreProcess(activity, instanaSpan); - instanaSpan.N = InstanaExporterConstants.OTEL_SPAN_TYPE; + instanaSpan.N = InstanaExporterConstants.OpenTelemetrySpanType; var traceId = activity.TraceId.ToHexString(); if (traceId.Length == 32) @@ -47,19 +47,27 @@ public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSp instanaSpan.Tp = true; } - await base.ProcessAsync(activity, instanaSpan).ConfigureAwait(false); + base.Process(activity, instanaSpan); } - private static SpanKind GetSpanKind(ActivityKind activityKind) + internal static DefaultActivityProcessor CreateDefault() => new() { - return activityKind switch + NextProcessor = new TagsActivityProcessor() { - ActivityKind.Consumer or ActivityKind.Server => SpanKind.ENTRY, - ActivityKind.Client or ActivityKind.Producer => SpanKind.EXIT, - ActivityKind.Internal => SpanKind.INTERMEDIATE, - _ => SpanKind.NOT_SET, - }; - } + NextProcessor = new EventsActivityProcessor() + { + NextProcessor = new ErrorActivityProcessor(), + }, + }, + }; + + private static SpanKind GetSpanKind(ActivityKind activityKind) => activityKind switch + { + ActivityKind.Consumer or ActivityKind.Server => SpanKind.ENTRY, + ActivityKind.Client or ActivityKind.Producer => SpanKind.EXIT, + ActivityKind.Internal => SpanKind.INTERMEDIATE, + _ => SpanKind.NOT_SET, + }; private static long GetLongFromHex(string hexValue) { @@ -82,35 +90,32 @@ private static void SetKind(Activity activity, InstanaSpan instanaSpan) { var isEntrySpan = false; - if (instanaSpan.Data.data != null) + if (instanaSpan.Data.Values != null) { switch (activity.Kind) { case ActivityKind.Server: isEntrySpan = true; - instanaSpan.Data.data[InstanaExporterConstants.KIND_FIELD] = InstanaExporterConstants.SERVER_KIND; + instanaSpan.Data.Values[InstanaExporterConstants.KindField] = InstanaExporterConstants.ServerKind; break; case ActivityKind.Client: - instanaSpan.Data.data[InstanaExporterConstants.KIND_FIELD] = InstanaExporterConstants.CLIENT_KIND; + instanaSpan.Data.Values[InstanaExporterConstants.KindField] = InstanaExporterConstants.ClientKind; break; case ActivityKind.Producer: - instanaSpan.Data.data[InstanaExporterConstants.KIND_FIELD] = InstanaExporterConstants.PRODUCER_KIND; + instanaSpan.Data.Values[InstanaExporterConstants.KindField] = InstanaExporterConstants.ProducerKind; break; case ActivityKind.Consumer: isEntrySpan = true; - instanaSpan.Data.data[InstanaExporterConstants.KIND_FIELD] = InstanaExporterConstants.CONSUMER_KIND; + instanaSpan.Data.Values[InstanaExporterConstants.KindField] = InstanaExporterConstants.ConsumerKind; break; case ActivityKind.Internal: - instanaSpan.Data.data[InstanaExporterConstants.KIND_FIELD] = InstanaExporterConstants.INTERNAL_KIND; + instanaSpan.Data.Values[InstanaExporterConstants.KindField] = InstanaExporterConstants.InternalKind; break; default: break; } } - if (instanaSpan.TransformInfo != null) - { - instanaSpan.TransformInfo.IsEntrySpan = isEntrySpan; - } + instanaSpan.TransformInfo?.IsEntrySpan = isEntrySpan; } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs index 133fe8e889..202cbc516b 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs @@ -5,36 +5,32 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal class ErrorActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class ErrorActivityProcessor : ActivityProcessorBase { - public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) + public override void Process(Activity activity, InstanaSpan instanaSpan) { this.PreProcess(activity, instanaSpan); if (activity.Status == ActivityStatusCode.Error) { instanaSpan.Ec = 1; - if (instanaSpan.Data.data != null) + if (instanaSpan.Data.Values != null) { - instanaSpan.Data.data[InstanaExporterConstants.ERROR_FIELD] = activity.Status.ToString(); + instanaSpan.Data.Values[InstanaExporterConstants.ErrorField] = activity.Status.ToString(); if (activity.StatusDescription != null && !string.IsNullOrEmpty(activity.StatusDescription)) { - instanaSpan.Data.data[InstanaExporterConstants.ERROR_DETAIL_FIELD] = activity.StatusDescription; + instanaSpan.Data.Values[InstanaExporterConstants.ErrorDetailField] = activity.StatusDescription; } } } - else if (instanaSpan.TransformInfo != null && instanaSpan.TransformInfo.HasExceptionEvent) - { - instanaSpan.Ec = 1; - } else { - instanaSpan.Ec = 0; + instanaSpan.Ec = instanaSpan.TransformInfo != null && instanaSpan.TransformInfo.HasExceptionEvent ? 1 : 0; } if (activity != null && instanaSpan != null) { - await base.ProcessAsync(activity, instanaSpan).ConfigureAwait(false); + base.Process(activity, instanaSpan); } } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs index df3e8decf8..bc4dcb26a2 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs @@ -5,15 +5,15 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal class EventsActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class EventsActivityProcessor : ActivityProcessorBase { - public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) + public override void Process(Activity activity, InstanaSpan instanaSpan) { this.PreProcess(activity, instanaSpan); foreach (var activityEvent in activity.Events) { - if (activityEvent.Name == InstanaExporterConstants.EXCEPTION_FIELD && instanaSpan.TransformInfo != null) + if (activityEvent.Name == InstanaExporterConstants.ExceptionField && instanaSpan.TransformInfo != null) { instanaSpan.TransformInfo.HasExceptionEvent = true; } @@ -29,13 +29,13 @@ public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSp { if (eventTag.Value != null) { - spanEvent.Tags[eventTag.Key] = eventTag.Value.ToString(); + spanEvent.Tags[eventTag.Key] = eventTag.Value.ToString() ?? string.Empty; } } instanaSpan.Data.Events.Add(spanEvent); } - await base.ProcessAsync(activity, instanaSpan).ConfigureAwait(false); + base.Process(activity, instanaSpan); } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/IActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/IActivityProcessor.cs index b66e437cb9..1394af31bd 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/IActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/IActivityProcessor.cs @@ -7,5 +7,5 @@ internal interface IActivityProcessor { IActivityProcessor? NextProcessor { get; set; } - Task ProcessAsync(System.Diagnostics.Activity activity, InstanaSpan instanaSpan); + void Process(System.Diagnostics.Activity activity, InstanaSpan instanaSpan); } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/TagsActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/TagsActivityProcessor.cs index 1d6341e7a9..b0d4a9c4c3 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/TagsActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/TagsActivityProcessor.cs @@ -5,16 +5,11 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal class TagsActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class TagsActivityProcessor : ActivityProcessorBase { - public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) + public override void Process(Activity activity, InstanaSpan instanaSpan) { - if (instanaSpan == null) - { - return; - } - - if (activity == null) + if (instanaSpan == null || activity == null) { return; } @@ -24,6 +19,7 @@ public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSp var statusCode = string.Empty; var statusDesc = string.Empty; var tags = new Dictionary(); + foreach (var tag in activity.Tags) { if (tag.Key == "otel.status_code") @@ -44,14 +40,11 @@ public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSp } } - if (instanaSpan.Data != null) - { - instanaSpan.Data.Tags = tags; - } + instanaSpan.Data?.Tags = tags; instanaSpan.TransformInfo.StatusCode = statusCode; instanaSpan.TransformInfo.StatusDesc = statusDesc; - await base.ProcessAsync(activity, instanaSpan).ConfigureAwait(false); + base.Process(activity, instanaSpan); } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs deleted file mode 100644 index 9e20b6e805..0000000000 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Collections.Concurrent; - -namespace OpenTelemetry.Exporter.Instana.Implementation; - -#pragma warning disable CA1001 // Types that own disposable fields should be disposable -internal sealed class SpanSender : ISpanSender -#pragma warning restore CA1001 // Types that own disposable fields should be disposable -{ - private readonly Task queueSenderTask; - private readonly ConcurrentQueue spansQueue = new(); - - public SpanSender() - { - // create a task that will send a batch of spans every second at least - this.queueSenderTask = new Task(this.TaskSpanSender, TaskCreationOptions.LongRunning); - this.queueSenderTask.Start(); - } - - public void Enqueue(InstanaSpan instanaSpan) - { - if (Transport.IsAvailable) - { - this.spansQueue.Enqueue(instanaSpan); - } - } - - private async void TaskSpanSender() - { - // this will be an infinite loop - while (true) - { - // check if we can send spans - if (this.spansQueue.TryPeek(out var _)) - { - // actually send spans - await Transport.SendSpansAsync(this.spansQueue).ConfigureAwait(false); - } - - // rest for a while - await Task.Delay(1000).ConfigureAwait(false); - } - } -} diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index 97cee57f7b..39172db7ff 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Collections.Concurrent; +using System.Buffers; using System.Globalization; using System.Net; #if NETFRAMEWORK @@ -11,167 +11,139 @@ namespace OpenTelemetry.Exporter.Instana.Implementation; -internal static class Transport +internal sealed class Transport : IDisposable { private const int MultiSpanBufferSize = 4096000; private const int MultiSpanBufferLimit = 4070000; - private static readonly MediaTypeHeaderValue MEDIAHEADER = new("application/json"); - private static readonly byte[] TracesBuffer = new byte[MultiSpanBufferSize]; - private static bool isConfigured; - private static int backendTimeout; - private static string configuredEndpoint = string.Empty; - private static string configuredAgentKey = string.Empty; - private static string bundleUrl = string.Empty; - - static Transport() + + private static readonly MediaTypeHeaderValue MediaType = new("application/json"); + + private readonly Uri bundleUri; + private readonly InstanaExporterOptions options; + + private HttpClient? client; + + public Transport(InstanaExporterOptions options) { - Configure(); + this.options = options; + this.bundleUri = new Uri(options.EndpointUri, "bundle"); } - internal static bool IsAvailable => isConfigured && Client != null; - - internal static InstanaHttpClient? Client { get; set; } + public void Dispose() + { + this.client?.Dispose(); + GC.SuppressFinalize(this); + } - internal static async Task SendSpansAsync(ConcurrentQueue spanQueue) + public bool Send(List batch) { + var buffer = ArrayPool.Shared.Rent(MultiSpanBufferSize); + try { - using var sendBuffer = new MemoryStream(TracesBuffer); + using var sendBuffer = new MemoryStream(buffer); using var writer = new StreamWriter(sendBuffer); - await writer.WriteAsync("{\"spans\":[").ConfigureAwait(false); - var first = true; + writer.Write("{\"spans\":["); + + int maxBatchSize = this.options.BatchExportProcessorOptions.MaxExportBatchSize; - // peek instead of dequeue, because we don't yet know whether the next span - // fits within our MULTI_SPAN_BUFFER_LIMIT - while (spanQueue.TryPeek(out var span) && sendBuffer.Position < MultiSpanBufferLimit) + using var enumerator = batch.GetEnumerator(); + + int written = 0; + + while (sendBuffer.Position < MultiSpanBufferLimit && written < maxBatchSize && enumerator.MoveNext()) { - if (!first) + if (written > 0) { - await writer.WriteAsync(",").ConfigureAwait(false); + writer.Write(','); } - await InstanaSpanSerializer.SerializeToStreamWriterAsync(span, writer).ConfigureAwait(false); - await writer.FlushAsync().ConfigureAwait(false); + InstanaSpanSerializer.SerializeToStreamWriter(enumerator.Current, writer); - first = false; + writer.Flush(); - // Now we can dequeue. Note, this means we'll be giving up/losing - // this span if we fail to send for any reason. - spanQueue.TryDequeue(out _); + written++; } - await writer.WriteAsync("]}").ConfigureAwait(false); - await writer.FlushAsync().ConfigureAwait(false); + writer.Write("]}"); + writer.Flush(); var length = sendBuffer.Position; sendBuffer.Position = 0; sendBuffer.SetLength(length); - HttpContent content = new StreamContent(sendBuffer, (int)length); - content.Headers.ContentType = MEDIAHEADER; - content.Headers.Add("X-INSTANA-TIME", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture)); + using var content = new StreamContent(sendBuffer, (int)length); + content.Headers.ContentType = MediaType; - using var httpMsg = new HttpRequestMessage() + using var message = new HttpRequestMessage(HttpMethod.Post, this.bundleUri) { - Method = HttpMethod.Post, - RequestUri = new Uri(bundleUrl), + Content = content, }; - httpMsg.Content = content; - if (Client != null) - { - await Client.SendAsync(httpMsg).ConfigureAwait(false); - } - } - catch (Exception e) - { - InstanaExporterEventSource.Log.FailedExport(e); - } - } - private static void Configure() - { - if (isConfigured) - { - return; - } + message.Headers.Add("X-INSTANA-KEY", this.options.AgentKey); + message.Headers.Add("X-INSTANA-NOTRACE", "1"); + message.Headers.Add("X-INSTANA-TIME", this.options.UtcNow().ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture)); - if (string.IsNullOrEmpty(configuredEndpoint)) - { - configuredEndpoint = Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_ENDPOINT_URL); - } + this.client ??= this.CreateClient(); - if (string.IsNullOrEmpty(configuredEndpoint)) - { - return; - } +#if NET + using var response = this.client.Send(message); +#else +#pragma warning disable CA2025 // Do not pass 'IDisposable' instances into unawaited tasks + using var response = this.client.SendAsync(message).GetAwaiter().GetResult(); +#pragma warning restore CA2025 // Do not pass 'IDisposable' instances into unawaited tasks +#endif - bundleUrl = configuredEndpoint + "/bundle"; + response.EnsureSuccessStatusCode(); - if (string.IsNullOrEmpty(configuredAgentKey)) - { - configuredAgentKey = Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_AGENT_KEY); + return true; } - - if (string.IsNullOrEmpty(configuredAgentKey)) + catch (Exception ex) { - return; + InstanaExporterEventSource.Log.FailedExport(ex); + return false; } - - if (backendTimeout == 0) + finally { - if (!int.TryParse(Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_TIMEOUT), out backendTimeout)) - { - backendTimeout = InstanaExporterConstants.BACKEND_DEFAULT_TIMEOUT; - } + ArrayPool.Shared.Return(buffer); } - - ConfigureBackendClient(); - isConfigured = true; } - private static void ConfigureBackendClient() + private HttpClient CreateClient() { - if (Client != null) + if (this.options.HttpClientFactory is { } factory) { - return; + return factory(); } -#pragma warning disable CA2000 - var configuredHandler = new HttpClientHandler(); -#pragma warning restore CA2000 - var proxy = Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_ENDPOINT_PROXY); - if (Uri.TryCreate(proxy, UriKind.Absolute, out var proxyAddress)) +#pragma warning disable CA2000 // Dispose objects before losing scope + var handler = new HttpClientHandler() { - configuredHandler.Proxy = new WebProxy(proxyAddress, true); - configuredHandler.UseProxy = true; -#pragma warning disable SA1130 // Use lambda syntax - configuredHandler.ServerCertificateCustomValidationCallback = delegate { return true; }; -#pragma warning restore SA1130 // Use lambda syntax - } - -#pragma warning disable CA5400 - Client = new InstanaHttpClient(backendTimeout, configuredHandler); -#pragma warning restore CA5400 +#if !NETFRAMEWORK + CheckCertificateRevocationList = true, +#endif + }; +#pragma warning restore CA2000 // Dispose objects before losing scope - Client.DefaultRequestHeaders.Add("X-INSTANA-KEY", configuredAgentKey); - } -} + if (this.options.ProxyUri is { } proxyAddress) + { + handler.Proxy = new WebProxy(proxyAddress, true); + handler.UseProxy = true; + } -#pragma warning disable SA1402 // File may only contain a single type -internal class InstanaHttpClient : HttpClient -#pragma warning restore SA1402 // File may only contain a single type -{ - public InstanaHttpClient(int timeout) - : base() - { - this.Timeout = TimeSpan.FromMilliseconds(timeout); - this.DefaultRequestHeaders.Add("X-INSTANA-NOTRACE", "1"); - } +#pragma warning disable CA5399 // .NET Framework does not support CheckCertificateRevocationList + var client = new HttpClient(handler, disposeHandler: true); +#pragma warning restore CA5399 // .NET Framework does not support CheckCertificateRevocationList - public InstanaHttpClient(int timeout, HttpClientHandler handler) - : base(handler) - { - this.Timeout = TimeSpan.FromMilliseconds(timeout); - this.DefaultRequestHeaders.Add("X-INSTANA-NOTRACE", "1"); + try + { + client.Timeout = TimeSpan.FromMilliseconds(this.options.BatchExportProcessorOptions.ExporterTimeoutMilliseconds); + return client; + } + catch (Exception) + { + client.Dispose(); + throw; + } } } diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs index 1c2fc4ad23..aef8fb3b68 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs @@ -12,38 +12,56 @@ namespace OpenTelemetry.Exporter.Instana; internal sealed class InstanaExporter : BaseExporter { private readonly IActivityProcessor activityProcessor; - private bool shutdownCalled; + private readonly InstanaExporterOptions options; + private readonly Transport transport; + private readonly string? processId; - public InstanaExporter(IActivityProcessor? activityProcessor = null) + private int wasShutdown; + + public InstanaExporter(InstanaExporterOptions options, IActivityProcessor activityProcessor) { - this.activityProcessor = activityProcessor ?? new DefaultActivityProcessor - { - NextProcessor = new TagsActivityProcessor - { - NextProcessor = new EventsActivityProcessor { NextProcessor = new ErrorActivityProcessor() }, - }, - }; - } + this.options = options; + this.activityProcessor = activityProcessor; + this.transport = new(this.options); - internal ISpanSender SpanSender { get; set; } = new SpanSender(); + if (IsWindows()) + { +#if NET + this.processId = Environment.ProcessId.ToString(CultureInfo.InvariantCulture); +#else + using var process = Process.GetCurrentProcess(); + this.processId = process.Id.ToString(CultureInfo.InvariantCulture); +#endif + } - internal IInstanaExporterHelper InstanaExporterHelper { get; set; } = new InstanaExporterHelper(); + static bool IsWindows() + { +#if NET + return OperatingSystem.IsWindows(); +#else + return Environment.OSVersion.Platform == PlatformID.Win32NT; +#endif + } + } public override ExportResult Export(in Batch batch) { - if (this.shutdownCalled) + if (this.wasShutdown is 1) { return ExportResult.Failure; } var from = new From(); - if (this.InstanaExporterHelper.IsWindows()) + + if (this.processId != null) { - from = new From { E = Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture) }; + from.E = this.processId; } var serviceName = this.ExtractServiceName(ref from); + var spans = new List((int)batch.Count); + foreach (var activity in batch) { if (activity == null) @@ -51,29 +69,22 @@ public override ExportResult Export(in Batch batch) continue; } - var span = this.ParseActivityAsync(activity, serviceName, from).Result; - this.SpanSender.Enqueue(span); + spans.Add(this.ParseActivity(activity, serviceName, from)); } - return ExportResult.Success; + return this.transport.Send(spans) ? ExportResult.Success : ExportResult.Failure; } - protected override bool OnShutdown(int timeoutMilliseconds) + protected override void Dispose(bool disposing) { - if (!this.shutdownCalled) - { - this.shutdownCalled = true; - return true; - } - else - { - return false; - } + this.transport?.Dispose(); + base.Dispose(disposing); } - protected override bool OnForceFlush(int timeoutMilliseconds) + protected override bool OnShutdown(int timeoutMilliseconds) { - return base.OnForceFlush(timeoutMilliseconds); + var wasShutdown = Interlocked.CompareExchange(ref this.wasShutdown, 1, 0); + return wasShutdown == 0; } private string ExtractServiceName(ref From from) @@ -82,29 +93,32 @@ private string ExtractServiceName(ref From from) var serviceId = string.Empty; var processId = string.Empty; var hostId = string.Empty; - var resource = this.InstanaExporterHelper.GetParentProviderResource(this); + var resource = this.options.GetParentProviderResource(this); + if (resource != Resource.Empty && resource.Attributes.Any()) { + var comparison = StringComparison.OrdinalIgnoreCase; + foreach (var resourceAttribute in resource.Attributes) { - if (resourceAttribute.Key.Equals("service.name", StringComparison.OrdinalIgnoreCase) - && resourceAttribute.Value is string servName - && !string.IsNullOrEmpty(servName)) + if (string.Equals(resourceAttribute.Key, "service.name", comparison) + && resourceAttribute.Value is string name + && !string.IsNullOrEmpty(name)) { - serviceName = servName; + serviceName = name; } if (from.IsEmpty()) { - if (resourceAttribute.Key.Equals("service.instance.id", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(resourceAttribute.Key, "service.instance.id", comparison)) { serviceId = resourceAttribute.Value.ToString(); } - else if (resourceAttribute.Key.Equals("process.pid", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(resourceAttribute.Key, "process.pid", comparison)) { processId = resourceAttribute.Value.ToString(); } - else if (resourceAttribute.Key.Equals("host.id", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(resourceAttribute.Key, "host.id", comparison)) { hostId = resourceAttribute.Value.ToString(); } @@ -132,25 +146,22 @@ private string ExtractServiceName(ref From from) return serviceName; } - private async Task ParseActivityAsync(Activity activity, string? serviceName = null, From? from = null) + private InstanaSpan ParseActivity(Activity activity, string? serviceName = null, From? from = null) { var instanaSpan = InstanaSpanFactory.CreateSpan(); - await this.activityProcessor.ProcessAsync(activity, instanaSpan).ConfigureAwait(false); + this.activityProcessor.Process(activity, instanaSpan); - if (serviceName != null && !string.IsNullOrEmpty(serviceName) && instanaSpan.Data.data != null) + if (serviceName != null && !string.IsNullOrEmpty(serviceName) && instanaSpan.Data.Values != null) { - instanaSpan.Data.data[InstanaExporterConstants.SERVICE_FIELD] = serviceName; + instanaSpan.Data.Values[InstanaExporterConstants.ServiceField] = serviceName; } - if (instanaSpan.Data.data != null) - { - instanaSpan.Data.data[InstanaExporterConstants.OPERATION_FIELD] = activity.DisplayName; - } + instanaSpan.Data.Values?[InstanaExporterConstants.OperationField] = activity.DisplayName; - if (activity.TraceStateString != null && !string.IsNullOrEmpty(activity.TraceStateString) && instanaSpan.Data.data != null) + if (activity.TraceStateString != null && !string.IsNullOrEmpty(activity.TraceStateString) && instanaSpan.Data.Values != null) { - instanaSpan.Data.data[InstanaExporterConstants.TRACE_STATE_FIELD] = activity.TraceStateString; + instanaSpan.Data.Values[InstanaExporterConstants.TraceStateField] = activity.TraceStateString; } if (from != null) diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporterConstants.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporterConstants.cs index 3d2dcdfca9..d7ca905957 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporterConstants.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporterConstants.cs @@ -5,30 +5,26 @@ namespace OpenTelemetry.Exporter.Instana; internal class InstanaExporterConstants { -#pragma warning disable SA1310 // Field names should not contain underscore - internal const string OTEL_SPAN_TYPE = "otel"; - internal const string KIND_FIELD = "kind"; - internal const string SERVER_KIND = "server"; - internal const string CLIENT_KIND = "client"; - internal const string PRODUCER_KIND = "producer"; - internal const string CONSUMER_KIND = "consumer"; - internal const string INTERNAL_KIND = "internal"; - internal const string SERVICE_FIELD = "service"; - internal const string OPERATION_FIELD = "operation"; - internal const string TRACE_STATE_FIELD = "trace_state"; - internal const string ERROR_FIELD = "error"; - internal const string ERROR_DETAIL_FIELD = "error_detail"; - internal const string EXCEPTION_FIELD = "exception"; - internal const string TAGS_FIELD = "tags"; - internal const string EVENTS_FIELD = "events"; - internal const string EVENT_NAME_FIELD = "name"; - internal const string EVENT_TIMESTAMP_FIELD = "ts"; + internal const string OpenTelemetrySpanType = "otel"; + internal const string KindField = "kind"; + internal const string ServerKind = "server"; + internal const string ClientKind = "client"; + internal const string ProducerKind = "producer"; + internal const string ConsumerKind = "consumer"; + internal const string InternalKind = "internal"; + internal const string ServiceField = "service"; + internal const string OperationField = "operation"; + internal const string TraceStateField = "trace_state"; + internal const string ErrorField = "error"; + internal const string ErrorDetailField = "error_detail"; + internal const string ExceptionField = "exception"; + internal const string TagsField = "tags"; + internal const string EventsField = "events"; + internal const string EventNameField = "name"; + internal const string EventTimestampField = "ts"; - internal const string ENVVAR_INSTANA_ENDPOINT_URL = "INSTANA_ENDPOINT_URL"; - internal const string ENVVAR_INSTANA_AGENT_KEY = "INSTANA_AGENT_KEY"; - internal const string ENVVAR_INSTANA_TIMEOUT = "INSTANA_TIMEOUT"; - internal const int BACKEND_DEFAULT_TIMEOUT = 20000; - internal const string ENVVAR_INSTANA_EXTRA_HTTP_HEADERS = "INSTANA_EXTRA_HTTP_HEADERS"; - internal const string ENVVAR_INSTANA_ENDPOINT_PROXY = "INSTANA_ENDPOINT_PROXY"; -#pragma warning restore SA1310 // Field names should not contain underscore + internal const string InstanaEndpointUrl = "INSTANA_ENDPOINT_URL"; + internal const string InstanaAgentKey = "INSTANA_AGENT_KEY"; + internal const string InstanaTimeout = "INSTANA_TIMEOUT"; + internal const string InstanaEndpointProxy = "INSTANA_ENDPOINT_PROXY"; } diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs deleted file mode 100644 index 90bf9b97a2..0000000000 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using OpenTelemetry.Resources; - -namespace OpenTelemetry.Exporter.Instana; - -internal class InstanaExporterHelper : IInstanaExporterHelper -{ - public Resource GetParentProviderResource(BaseExporter otelExporter) - { - return otelExporter.ParentProvider.GetResource(); - } - - public bool IsWindows() - { - return Environment.OSVersion.Platform == PlatformID.Win32NT; - } -} diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs new file mode 100644 index 0000000000..2f348e3b4d --- /dev/null +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif + +using System.Diagnostics; +using OpenTelemetry.Resources; + +namespace OpenTelemetry.Exporter.Instana; + +/// +/// A class representing the options for configuring the Instana exporter. +/// +public class InstanaExporterOptions +{ + /// + /// Gets or sets the key used to authenticate with the Instana endpoint. + /// + public string AgentKey { get; set; } = string.Empty; + + /// + /// Gets or sets the options to use. + /// + public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } = new() { ExporterTimeoutMilliseconds = 20_000 }; + + /// + /// Gets or sets the URI of the Instana endpoint. + /// + public Uri EndpointUri { get; set; } = default!; + + /// + /// Gets or sets an optional delegate to a method to create an + /// to use to send telemetry to the Instana endpoint. + /// + /// + /// The delegate should return a new instance of each time it is called. + /// The Instana exporter will dispose of the after use. + /// + public Func? HttpClientFactory { get; set; } + + /// + /// Gets or sets the optional proxy URI to use when sending data to the Instana endpoint. + /// + public Uri? ProxyUri { get; set; } + + /// + /// Gets or sets a delegate to a method that returns the current UTC time. + /// + public Func UtcNow { get; set; } = +#if NET + TimeProvider.System.GetUtcNow; +#else + static () => DateTimeOffset.UtcNow; +#endif + + internal Func, Resource> GetParentProviderResource { get; set; } = static (exporter) => exporter.ParentProvider.GetResource(); +} diff --git a/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj b/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj index d6b5528be7..f18ce9759c 100644 --- a/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj +++ b/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj @@ -1,8 +1,8 @@ - - $(NetStandardMinimumSupportedVersion);$(NetFrameworkMinimumSupportedVersion) + $(TargetFrameworksForLibraries) + $(TargetFrameworks);$(NetFrameworkMinimumSupportedVersion) Instana Tracing APM Instana .NET Exporter for OpenTelemetry. Exporter.Instana- diff --git a/src/OpenTelemetry.Exporter.Instana/README.md b/src/OpenTelemetry.Exporter.Instana/README.md index b163667ff0..993ee9fc27 100644 --- a/src/OpenTelemetry.Exporter.Instana/README.md +++ b/src/OpenTelemetry.Exporter.Instana/README.md @@ -9,7 +9,7 @@ [![NuGet download count badge](https://img.shields.io/nuget/dt/OpenTelemetry.Exporter.Instana)](https://www.nuget.org/packages/OpenTelemetry.Exporter.Instana) [![codecov.io](https://codecov.io/gh/open-telemetry/opentelemetry-dotnet-contrib/branch/main/graphs/badge.svg?flag=unittests-Exporter.Instana)](https://app.codecov.io/gh/open-telemetry/opentelemetry-dotnet-contrib?flags[0]=unittests-Exporter.Instana) -The Instana Exporter exports telemetry to Instana backend. +The Instana Exporter exports telemetry to an Instana backend. ## Installation @@ -19,28 +19,30 @@ dotnet add package OpenTelemetry.Exporter.Instana ## Configuration -The trace exporter is supported. +> [!NOTE] +> The Instana exporter only supports traces. -To report to Instana backend correct agent key and backend URL must be configured. -These values can be configured by environment variables INSTANA_AGENT_KEY -and INSTANA_ENDPOINT_URL. -Optionally backend communication timeout can be configured by environment -variable INSTANA_TIMEOUT. +To report to an Instana backend the correct agent key and backend URL must be +configured. -### Enable Traces - -This snippet shows how to configure the Instana Exporter for Traces +These values can be configured either by the environment variables `INSTANA_AGENT_KEY` +and `INSTANA_ENDPOINT_URL`, or using the `InstanaExporterOptions` class. ```csharp using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("DemoSource") - .AddInstanaExporter() + .AddInstanaExporter((options) => + { + options.AgentKey = "instana-agent-key"; + options.EndpointUrl = "https://instana.local"; + }) .Build(); ``` -The above code must be in application startup. In case of ASP.NET Core -applications, this should be in `ConfigureServices` of `Startup` class. -For ASP.NET applications, this should be in `Global.aspx.cs`. +Optionally backend communication timeout can be configured using the environment +variable `INSTANA_TIMEOUT` or the +`InstanaExporterOptions.BatchExportProcessorOptions.ExporterTimeoutMilliseconds` +property. ## Troubleshooting diff --git a/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs index 46f40881ff..0b311dde03 100644 --- a/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Globalization; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Exporter.Instana.Implementation.Processors; using OpenTelemetry.Trace; namespace OpenTelemetry.Exporter.Instana; @@ -11,20 +14,68 @@ namespace OpenTelemetry.Exporter.Instana; public static class TracerProviderBuilderExtensions { /// - /// Instana tracer provider builder extension. + /// Adds Instana exporter to the TracerProvider. /// - /// Tracer provider builder. - /// todo. - /// Tracer provider builder is null. + /// The builder to use. + /// The instance of to chain the calls. public static TracerProviderBuilder AddInstanaExporter(this TracerProviderBuilder options) + => options.AddInstanaExporter(null); + + /// + /// Adds Instana exporter to the TracerProvider. + /// + /// The builder to use. + /// The optional callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddInstanaExporter(this TracerProviderBuilder builder, Action? configure = default) { - if (options == null) +#if NET + ArgumentNullException.ThrowIfNull(builder); +#else + if (builder == null) { - throw new ArgumentNullException(nameof(options)); + throw new ArgumentNullException(nameof(builder)); } +#endif + + return builder.AddProcessor((serviceProvider) => + { + var options = serviceProvider.GetService() ?? new(); + + ConfigureFromEnvironment(options); -#pragma warning disable CA2000 - return options.AddProcessor(new BatchActivityExportProcessor(new InstanaExporter())); -#pragma warning restore CA2000 + configure?.Invoke(options); + + return options.EndpointUri is null + ? throw new InvalidOperationException("No Instana endpoint URL provided.") + : (BaseProcessor)new BatchActivityExportProcessor(new InstanaExporter(options, DefaultActivityProcessor.CreateDefault())); + }); + } + + private static void ConfigureFromEnvironment(InstanaExporterOptions options) + { + if (options.EndpointUri is null && + Environment.GetEnvironmentVariable(InstanaExporterConstants.InstanaEndpointUrl) is { Length: > 0 } endpointUrl) + { + options.EndpointUri = new Uri(endpointUrl, UriKind.Absolute); + } + + if (string.IsNullOrEmpty(options.AgentKey) && + Environment.GetEnvironmentVariable(InstanaExporterConstants.InstanaAgentKey) is { Length: > 0 } agentKey) + { + options.AgentKey = agentKey; + } + + if (Environment.GetEnvironmentVariable(InstanaExporterConstants.InstanaTimeout) is { Length: > 0 } timeout && + int.TryParse(timeout, NumberStyles.Integer, CultureInfo.InvariantCulture, out var timeoutMilliseconds)) + { + options.BatchExportProcessorOptions.ExporterTimeoutMilliseconds = timeoutMilliseconds; + } + + if (options.ProxyUri is null && + Environment.GetEnvironmentVariable(InstanaExporterConstants.InstanaEndpointProxy) is { Length: > 0 } proxyUrl) + { + options.ProxyUri = new Uri(proxyUrl, UriKind.Absolute); + } } } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs index d076911310..4c0ab247a9 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs @@ -2,109 +2,422 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Globalization; +using System.Text.Json; +#if NETFRAMEWORK +using System.Net.Http; +#endif using OpenTelemetry.Exporter.Instana.Implementation; +using OpenTelemetry.Exporter.Instana.Implementation.Processors; +using OpenTelemetry.Resources; +using OpenTelemetry.Tests; using Xunit; namespace OpenTelemetry.Exporter.Instana.Tests; public class InstanaExporterTests { - private readonly TestInstanaExporterHelper instanaExporterHelper = new(); - private readonly TestActivityProcessor activityProcessor = new(); - private readonly TestSpanSender spanSender = new(); - private InstanaSpan? instanaSpan; - private InstanaExporter? instanaExporter; + private static readonly TimeSpan ExportTimeout = TimeSpan.FromSeconds(15); [Fact] - public void Export() + public async Task Export() { - this.instanaExporterHelper.Attributes.Clear(); - this.instanaExporterHelper.Attributes.Add("service.name", "serviceName"); - this.instanaExporterHelper.Attributes.Add("service.instance.id", "serviceInstanceId"); - this.instanaExporterHelper.Attributes.Add("process.pid", "processPid"); - this.instanaExporterHelper.Attributes.Add("host.id", "hostId"); + // Arrange + var startedUtc = DateTimeOffset.FromUnixTimeMilliseconds(1776522506123); + var utcNow = DateTimeOffset.FromUnixTimeMilliseconds(1776523555069); - this.spanSender.OnEnqueue = span => this.CloneSpan(span); - - this.instanaExporter = new InstanaExporter(this.activityProcessor) + var options = new InstanaExporterOptions() { - InstanaExporterHelper = this.instanaExporterHelper, - SpanSender = this.spanSender, + AgentKey = Guid.NewGuid().ToString(), + GetParentProviderResource = (_) => + { + return new Resource( + [ + new("service.name", "serviceName"), + new("service.instance.id", "serviceInstanceId"), + new("process.pid", "processPid"), + new("host.id", "hostId"), + ]); + }, + UtcNow = () => utcNow, }; - var activity = new Activity("testOperationName"); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using var server = TestHttpServer.RunServer( + (context) => + { + try + { + Assert.Equal("POST", context.Request.HttpMethod); + Assert.Equal(new(options.EndpointUri, "/bundle"), context.Request.Url); + + Assert.Equal("application/json", context.Request.Headers["Content-Type"]); + Assert.Equal(options.AgentKey, context.Request.Headers["X-INSTANA-KEY"]); + Assert.Equal("1", context.Request.Headers["X-INSTANA-NOTRACE"]); + Assert.Equal("1776523555069", context.Request.Headers["X-INSTANA-TIME"]); + + using var reader = new StreamReader(context.Request.InputStream); + tcs.SetResult(reader.ReadToEnd()); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }, + out var host, + out var port); + + options.EndpointUri = new UriBuilder(Uri.UriSchemeHttp, host, port).Uri; + options.ProxyUri = options.EndpointUri; + + var spans = new List(); + var processor = new TestActivityProcessor((activity, span) => + { + Assert.NotNull(activity); + spans.Add(span); + }); + + using var exporter = new InstanaExporter(options, processor); + + using var activity = new Activity("testOperationName"); + activity.SetStartTime(startedUtc.UtcDateTime); activity.SetStatus(ActivityStatusCode.Error, "TestErrorDesc"); activity.TraceStateString = "TraceStateString"; Activity[] activities = [activity]; var batch = new Batch(activities, activities.Length); - var result = this.instanaExporter.Export(in batch); + // Act + var result = exporter.Export(batch); + + // Assert Assert.Equal(ExportResult.Success, result); - Assert.NotNull(this.instanaSpan); - Assert.Equal("processPid", this.instanaSpan.F.E); - Assert.Equal("hostId", this.instanaSpan.F.H); - Assert.Equal("serviceName", this.instanaSpan.Data.data["service"]); - Assert.Equal("testOperationName", this.instanaSpan.Data.data["operation"]); - Assert.Equal("TraceStateString", this.instanaSpan.Data.data["trace_state"]); + + var actual = await WaitForExportAsync(tcs); + + using var document = JsonDocument.Parse(actual); + Assert.NotNull(document); + + var exportedSpans = document.RootElement.GetProperty("spans"); + var exportedSpan = exportedSpans.EnumerateArray().Single(); + + Assert.Equal("0000000000000000", exportedSpan.GetProperty("t").GetString()); + Assert.Equal("0000000000000000", exportedSpan.GetProperty("s").GetString()); + Assert.Equal("00000000000000000000000000000000", exportedSpan.GetProperty("lt").GetString()); + Assert.Equal("3", exportedSpan.GetProperty("k").GetString()); + Assert.Equal("otel", exportedSpan.GetProperty("n").GetString()); + Assert.Equal(1776522506123, exportedSpan.GetProperty("ts").GetInt64()); + Assert.Equal(0, exportedSpan.GetProperty("d").GetInt32()); + Assert.Equal(1, exportedSpan.GetProperty("ec").GetInt32()); + + var from = exportedSpan.GetProperty("f"); + var data = exportedSpan.GetProperty("data"); + + Assert.Equal("internal", data.GetProperty("kind").GetString()); + Assert.Equal("Error", data.GetProperty("error").GetString()); + Assert.Equal("TestErrorDesc", data.GetProperty("error_detail").GetString()); + Assert.Equal("serviceName", data.GetProperty("service").GetString()); + Assert.Equal("testOperationName", data.GetProperty("operation").GetString()); + Assert.Equal("TraceStateString", data.GetProperty("trace_state").GetString()); + + var instanaSpan = Assert.Single(spans); + + Assert.NotNull(instanaSpan); + Assert.Equal("serviceName", instanaSpan.Data.Values["service"]); + Assert.Equal("testOperationName", instanaSpan.Data.Values["operation"]); + Assert.Equal("TraceStateString", instanaSpan.Data.Values["trace_state"]); + +#if NETFRAMEWORK + using var process = Process.GetCurrentProcess(); + + string expectedPid = process.Id.ToString(CultureInfo.InvariantCulture); + string expectedHostId = string.Empty; +#else + string expectedPid = OperatingSystem.IsWindows() ? + Environment.ProcessId.ToString(CultureInfo.InvariantCulture) : + "processPid"; + + string expectedHostId = OperatingSystem.IsWindows() ? + string.Empty : + "hostId"; +#endif + + Assert.Equal(expectedPid, instanaSpan.F.E); + Assert.Equal(expectedPid, from.GetProperty("e").GetString()); + + Assert.Equal(expectedHostId, instanaSpan.F.H); } [Fact] - public void Export_ProcessPidDoesNotExistButServiceIdExists() + public async Task Export_ProcessPidDoesNotExistButServiceIdExists() { - this.instanaExporterHelper.Attributes.Clear(); - this.instanaExporterHelper.Attributes.Add("service.name", "serviceName"); - this.instanaExporterHelper.Attributes.Add("service.instance.id", "serviceInstanceId"); - this.instanaExporterHelper.Attributes.Add("host.id", "hostId"); + // Arrange + var startedUtc = DateTimeOffset.FromUnixTimeMilliseconds(1776522506123); + var utcNow = DateTimeOffset.FromUnixTimeMilliseconds(1776523555069); - this.spanSender.OnEnqueue = span => this.CloneSpan(span); - - this.instanaExporter = new InstanaExporter(this.activityProcessor) + var options = new InstanaExporterOptions() { - InstanaExporterHelper = this.instanaExporterHelper, - SpanSender = this.spanSender, + AgentKey = Guid.NewGuid().ToString(), + GetParentProviderResource = (_) => + { + return new Resource( + [ + new("service.name", "serviceName"), + new("service.instance.id", "serviceInstanceId"), + new("host.id", "hostId"), + ]); + }, + UtcNow = () => utcNow, }; - var activity = new Activity("testOperationName"); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using var server = TestHttpServer.RunServer( + (context) => + { + try + { + Assert.Equal("POST", context.Request.HttpMethod); + Assert.Equal(new(options.EndpointUri, "/bundle"), context.Request.Url); + + Assert.Equal("application/json", context.Request.Headers["Content-Type"]); + Assert.Equal(options.AgentKey, context.Request.Headers["X-INSTANA-KEY"]); + Assert.Equal("1", context.Request.Headers["X-INSTANA-NOTRACE"]); + Assert.Equal("1776523555069", context.Request.Headers["X-INSTANA-TIME"]); + + using var reader = new StreamReader(context.Request.InputStream); + tcs.SetResult(reader.ReadToEnd()); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }, + out var host, + out var port); + + options.EndpointUri = new UriBuilder(Uri.UriSchemeHttp, host, port).Uri; + + var spans = new List(); + + var processor = new TestActivityProcessor((activity, span) => + { + Assert.NotNull(activity); + spans.Add(span); + }); + + using var exporter = new InstanaExporter(options, processor); + + using var activity = new Activity("testOperationName"); + activity.SetStartTime(startedUtc.UtcDateTime); activity.SetStatus(ActivityStatusCode.Error, "TestErrorDesc"); Activity[] activities = [activity]; var batch = new Batch(activities, activities.Length); - var result = this.instanaExporter.Export(in batch); + // Act + var result = exporter.Export(batch); + + // Assert Assert.Equal(ExportResult.Success, result); - Assert.NotNull(this.instanaSpan); - Assert.Equal("serviceInstanceId", this.instanaSpan.F.E); - Assert.Equal("hostId", this.instanaSpan.F.H); - Assert.Equal("serviceName", this.instanaSpan.Data.data["service"]); - Assert.Equal("testOperationName", this.instanaSpan.Data.data["operation"]); + + var actual = await WaitForExportAsync(tcs); + + using var document = JsonDocument.Parse(actual); + Assert.NotNull(document); + + var exportedSpans = document.RootElement.GetProperty("spans"); + var exportedSpan = exportedSpans.EnumerateArray().Single(); + + Assert.Equal("0000000000000000", exportedSpan.GetProperty("t").GetString()); + Assert.Equal("0000000000000000", exportedSpan.GetProperty("s").GetString()); + Assert.Equal("00000000000000000000000000000000", exportedSpan.GetProperty("lt").GetString()); + Assert.Equal("3", exportedSpan.GetProperty("k").GetString()); + Assert.Equal("otel", exportedSpan.GetProperty("n").GetString()); + Assert.Equal(1776522506123, exportedSpan.GetProperty("ts").GetInt64()); + Assert.Equal(0, exportedSpan.GetProperty("d").GetInt32()); + Assert.Equal(1, exportedSpan.GetProperty("ec").GetInt32()); + + var from = exportedSpan.GetProperty("f"); + var data = exportedSpan.GetProperty("data"); + + Assert.Equal("internal", data.GetProperty("kind").GetString()); + Assert.Equal("Error", data.GetProperty("error").GetString()); + Assert.Equal("TestErrorDesc", data.GetProperty("error_detail").GetString()); + Assert.Equal("serviceName", data.GetProperty("service").GetString()); + Assert.Equal("testOperationName", data.GetProperty("operation").GetString()); + + var instanaSpan = Assert.Single(spans); + + Assert.NotNull(instanaSpan); + Assert.Equal("serviceName", instanaSpan.Data.Values["service"]); + Assert.Equal("testOperationName", instanaSpan.Data.Values["operation"]); + +#if NETFRAMEWORK + using var process = Process.GetCurrentProcess(); + + string expectedPid = process.Id.ToString(CultureInfo.InvariantCulture); + string expectedHostId = string.Empty; +#else + string expectedPid = OperatingSystem.IsWindows() ? + Environment.ProcessId.ToString(CultureInfo.InvariantCulture) : + "serviceInstanceId"; + + string expectedHostId = OperatingSystem.IsWindows() ? + string.Empty : + "hostId"; +#endif + + Assert.Equal(expectedPid, instanaSpan.F.E); + Assert.Equal(expectedPid, from.GetProperty("e").GetString()); + + Assert.Equal(expectedHostId, instanaSpan.F.H); } [Fact] - public void Export_ExporterIsShotDown() + public void Export_ExporterIsShutDown() { - this.instanaExporter = new InstanaExporter(this.activityProcessor) + // Arrange + var options = new InstanaExporterOptions() { - InstanaExporterHelper = this.instanaExporterHelper, - SpanSender = this.spanSender, + AgentKey = Guid.NewGuid().ToString(), + EndpointUri = new Uri("http://localhost:42699"), }; - var activity = new Activity("testOperationName"); + var spans = new List(); + var processor = new TestActivityProcessor((activity, span) => + { + Assert.NotNull(activity); + spans.Add(span); + }); + + using var exporter = new InstanaExporter(options, processor); + + using var activity = new Activity("testOperationName"); activity.SetStatus(ActivityStatusCode.Error, "TestErrorDesc"); - this.instanaExporter.Shutdown(); + exporter.Shutdown(); + + var batch = new Batch([activity], 1); + + // Act + var actual = exporter.Export(batch); + + // Assert + Assert.Equal(ExportResult.Failure, actual); + Assert.Empty(spans); + } + + [Fact] + public async Task Export_WithCustomHttpClient() + { + // Arrange + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using var handler = new TestHttpMessageHandler(tcs); + using var httpClient = new HttpClient(handler); + + var options = new InstanaExporterOptions() + { + AgentKey = "instana-agent-key", + EndpointUri = new Uri("http://localhost:42699"), + HttpClientFactory = () => httpClient, + }; + + var processor = DefaultActivityProcessor.CreateDefault(); + + using var exporter = new InstanaExporter(options, processor); + + using var activity = new Activity("my-operation"); Activity[] activities = [activity]; var batch = new Batch(activities, activities.Length); - var result = this.instanaExporter.Export(in batch); - Assert.Equal(ExportResult.Failure, result); - Assert.Null(this.instanaSpan); + // Act + var result = exporter.Export(batch); + + // Assert + Assert.Equal(ExportResult.Success, result); + + var actual = await WaitForExportAsync(tcs); + + var exception = Record.Exception(() => JsonDocument.Parse(actual)); + Assert.Null(exception); + + Assert.Equal(1, handler.InvocationCount); + } + + private static async Task WaitForExportAsync(TaskCompletionSource completionSource) + { + var timeout = ExportTimeout; + +#if NET + await completionSource.Task.WaitAsync(timeout); +#else + using var cts = new CancellationTokenSource(timeout); + var completed = await Task.WhenAny(completionSource.Task, Task.Delay(timeout, cts.Token)); + Assert.Same(completionSource.Task, completed); +#endif + + return await completionSource.Task; + } + + private sealed class TestActivityProcessor : ActivityProcessorBase + { + private readonly Action callback; + + public TestActivityProcessor(Action callback) + { + this.callback = callback; + this.NextProcessor = DefaultActivityProcessor.CreateDefault(); + } + + public override void Process(Activity activity, InstanaSpan instanaSpan) + { + base.Process(activity, instanaSpan); + this.callback(activity, instanaSpan); + } } - private bool CloneSpan(InstanaSpan span) + private sealed class TestHttpMessageHandler(TaskCompletionSource completionSource) : HttpMessageHandler { - this.instanaSpan = span; - return true; + public int InvocationCount { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.InvocationCount++; + +#if NETFRAMEWORK + using (var stream = await request.Content!.ReadAsStreamAsync()) +#else + using (var stream = await request.Content!.ReadAsStreamAsync(cancellationToken)) +#endif + using (var reader = new StreamReader(stream)) + { +#if NETFRAMEWORK + completionSource.SetResult(await reader.ReadToEndAsync()); +#else + completionSource.SetResult(await reader.ReadToEndAsync(cancellationToken)); +#endif + } + + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + } + +#if NET + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.InvocationCount++; + + using (var stream = request.Content!.ReadAsStream(cancellationToken)) + using (var reader = new StreamReader(stream)) + { + completionSource.SetResult(reader.ReadToEnd()); + } + + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + } +#endif } } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs index f53310e25c..7322329066 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs @@ -11,14 +11,13 @@ public static class InstanaSpanFactoryTests [Fact] public static void CreateSpan() { - _ = new InstanaSpanFactory(); - var instanaSpan = InstanaSpanFactory.CreateSpan(); + var actual = InstanaSpanFactory.CreateSpan(); - Assert.NotNull(instanaSpan); - Assert.NotNull(instanaSpan.TransformInfo); - Assert.NotNull(instanaSpan.Data); - Assert.Empty(instanaSpan.Data.data); - Assert.Empty(instanaSpan.Data.Tags); - Assert.Empty(instanaSpan.Data.Events); + Assert.NotNull(actual); + Assert.NotNull(actual.TransformInfo); + Assert.NotNull(actual.Data); + Assert.Empty(actual.Data.Values); + Assert.Empty(actual.Data.Tags); + Assert.Empty(actual.Data.Events); } } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs index 6063f4befc..e0a1f1d5e3 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs @@ -1,8 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json; using OpenTelemetry.Exporter.Instana.Implementation; using Xunit; @@ -10,8 +9,13 @@ namespace OpenTelemetry.Exporter.Instana.Tests; public static class InstanaSpanSerializerTests { + private static readonly JsonSerializerOptions SerializerOptions = new() + { + NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString, + }; + [Fact] - public static async Task SerializeToStreamWriterAsync() + public static void SerializeToStreamWriterAsync() { var instanaOtelSpan = InstanaSpanFactory.CreateSpan(); instanaOtelSpan.F = new Implementation.From { E = "12345", H = "localhost" }; @@ -24,7 +28,7 @@ public static async Task SerializeToStreamWriterAsync() instanaOtelSpan.Lt = "hexNumberLT1234567890123"; instanaOtelSpan.Tp = true; instanaOtelSpan.Data.Tags = new Dictionary { ["tag1Key"] = "tag1Vale", ["tag2Key"] = "tag2Vale" }; - instanaOtelSpan.Data.data = new Dictionary { ["data1Key"] = "data1Vale", ["data2Key"] = "data2Vale" }; + instanaOtelSpan.Data.Values = new Dictionary { ["data1Key"] = "data1Vale", ["data2Key"] = "data2Vale" }; instanaOtelSpan.Data.Events = [ new() @@ -44,22 +48,17 @@ public static async Task SerializeToStreamWriterAsync() ]; InstanaSpanTest? span; - using (var sendBuffer = new MemoryStream(new byte[4096000])) + using (var sendBuffer = new MemoryStream()) { using var writer = new StreamWriter(sendBuffer); - await InstanaSpanSerializer.SerializeToStreamWriterAsync(instanaOtelSpan, writer); - await writer.FlushAsync(); + InstanaSpanSerializer.SerializeToStreamWriter(instanaOtelSpan, writer); + writer.Flush(); + var length = sendBuffer.Position; sendBuffer.Position = 0; sendBuffer.SetLength(length); - var serializer = new JsonSerializer(); - serializer.Converters.Add(new JavaScriptDateTimeConverter()); - serializer.NullValueHandling = NullValueHandling.Ignore; - - TextReader textReader = new StreamReader(sendBuffer); - JsonReader reader = new JsonTextReader(textReader); - span = serializer.Deserialize(reader); + span = JsonSerializer.Deserialize(sendBuffer, SerializerOptions); } Assert.NotNull(span); @@ -76,9 +75,9 @@ public static async Task SerializeToStreamWriterAsync() Assert.NotNull(span.Data.Tags); Assert.Equal(instanaOtelSpan.Data.Tags["tag1Key"], span.Data.Tags["tag1Key"]); Assert.Equal(instanaOtelSpan.Data.Tags["tag2Key"], span.Data.Tags["tag2Key"]); - Assert.NotNull(span.Data.data); - Assert.Equal(instanaOtelSpan.Data.data["data1Key"], span.Data.data["data1Key"]); - Assert.Equal(instanaOtelSpan.Data.data["data2Key"], span.Data.data["data2Key"]); + Assert.NotNull(span.Data.Values); + Assert.Equal(instanaOtelSpan.Data.Values["data1Key"], span.Data.Values["data1Key"].GetString()); + Assert.Equal(instanaOtelSpan.Data.Values["data2Key"], span.Data.Values["data2Key"].GetString()); Assert.NotNull(span.Data.Events); var event0 = span.Data.Events[0]; Assert.Equal(instanaOtelSpan.Data.Events[0].Name, event0.Name); diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanTest.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanTest.cs index 32bfef6851..cb353a3a77 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanTest.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanTest.cs @@ -1,187 +1,103 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; namespace OpenTelemetry.Exporter.Instana.Tests; +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1649 // File name should match first type name + internal enum SpanKind { -#pragma warning disable SA1602 // Enumeration items should be documented ENTRY, -#pragma warning restore SA1602 // Enumeration items should be documented -#pragma warning disable SA1602 // Enumeration items should be documented EXIT, -#pragma warning restore SA1602 // Enumeration items should be documented -#pragma warning disable SA1602 // Enumeration items should be documented INTERMEDIATE, -#pragma warning restore SA1602 // Enumeration items should be documented -#pragma warning disable SA1602 // Enumeration items should be documented NOT_SET, -#pragma warning restore SA1602 // Enumeration items should be documented } -#pragma warning disable SA1402 // File may only contain a single type -#pragma warning disable SA1649 // File name should match first type name internal class InstanaSpanTransformInfo -#pragma warning restore SA1649 // File name should match first type name -#pragma warning restore SA1402 // File may only contain a single type { - public string? StatusCode { get; internal set; } + public string? StatusCode { get; set; } - public string? StatusDesc { get; internal set; } + public string? StatusDesc { get; set; } - public bool HasExceptionEvent { get; internal set; } + public bool HasExceptionEvent { get; set; } - public bool IsEntrySpan { get; internal set; } + public bool IsEntrySpan { get; set; } } internal class InstanaSpanTest { public InstanaSpanTransformInfo? TransformInfo { get; set; } - [JsonProperty] - public string? N { get; internal set; } + [JsonPropertyName("n")] + public string? N { get; set; } - [JsonProperty] - public string? T { get; internal set; } + [JsonPropertyName("t")] + public string? T { get; set; } - [JsonProperty] - public string? Lt { get; internal set; } + [JsonPropertyName("lt")] + public string? Lt { get; set; } - [JsonProperty] - public From? F { get; internal set; } + [JsonPropertyName("f")] + public From? F { get; set; } - [JsonProperty] - public string? P { get; internal set; } + [JsonPropertyName("p")] + public string? P { get; set; } - [JsonProperty] - public string? S { get; internal set; } + [JsonPropertyName("s")] + public string? S { get; set; } - [JsonProperty] - public SpanKind? K { get; internal set; } + [JsonPropertyName("k")] + public SpanKind? K { get; set; } - [JsonProperty] - public Data? Data { get; internal set; } + [JsonPropertyName("data")] + public Data? Data { get; set; } - [JsonProperty] - public long Ts { get; internal set; } + [JsonPropertyName("ts")] + public long Ts { get; set; } - [JsonProperty] - public long D { get; internal set; } + [JsonPropertyName("d")] + public long D { get; set; } - [JsonProperty] - public bool Tp { get; internal set; } + [JsonPropertyName("tp")] + public string? Tp { get; set; } - [JsonProperty] - public int Ec { get; internal set; } + [JsonPropertyName("ec")] + public int Ec { get; set; } } -#pragma warning disable SA1402 // File may only contain a single type internal class From -#pragma warning restore SA1402 // File may only contain a single type { - [JsonProperty] - public string? E { get; internal set; } + [JsonPropertyName("e")] + public string? E { get; set; } - [JsonProperty] - public string? H { get; internal set; } + [JsonPropertyName("h")] + public string? H { get; set; } } -[JsonConverter(typeof(DataConverter))] -#pragma warning disable SA1402 // File may only contain a single type internal class Data -#pragma warning restore SA1402 // File may only contain a single type { - [JsonProperty] -#pragma warning disable SA1300 // Element should begin with upper-case letter - public Dictionary? data { get; internal set; } -#pragma warning restore SA1300 // Element should begin with upper-case letter + [JsonExtensionData] + public Dictionary? Values { get; set; } - [JsonProperty] - public Dictionary? Tags { get; internal set; } + [JsonPropertyName("tags")] + public Dictionary? Tags { get; set; } - [JsonProperty] - public List? Events { get; internal set; } + [JsonPropertyName("events")] + public List? Events { get; set; } } -#pragma warning disable SA1402 // File may only contain a single type internal class SpanEvent -#pragma warning restore SA1402 // File may only contain a single type { - [JsonProperty] - public string? Name { get; internal set; } - - [JsonProperty] - public long Ts { get; internal set; } + [JsonPropertyName("name")] + public string? Name { get; set; } - [JsonProperty] - public Dictionary? Tags { get; internal set; } -} + [JsonPropertyName("ts")] + public long Ts { get; set; } -#pragma warning disable SA1402 // File may only contain a single type -internal class DataConverter : JsonConverter -#pragma warning restore SA1402 // File may only contain a single type -{ - public override bool CanWrite => false; - - public override bool CanRead => true; - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(Data); - } - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - { - return string.Empty; - } - else if (reader.TokenType == JsonToken.String) - { - return serializer.Deserialize(reader, objectType); - } - else - { - var obj = JObject.Load(reader); - var data = obj.Root; - if (data != null) - { - var newData = new Data(); - foreach (var field in data) - { - if (((JProperty)field).Name is "tags" or "events") - { - continue; - } - - newData.data ??= []; - - newData.data[((JProperty)field).Name] = ((JProperty)field).Value.ToString(); - } - - var existingData = existingValue as Data ?? new Data(); - - // Populate the remaining standard properties - using (var subReader = data.CreateReader()) - { - serializer.Populate(subReader, existingData); - } - - newData.Events = existingData.Events; - newData.Tags = existingData.Tags; - - return newData; - } - - return null; - } - } - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } + [JsonPropertyName("tags")] + public Dictionary? Tags { get; set; } } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/OpenTelemetry.Exporter.Instana.Tests.csproj b/test/OpenTelemetry.Exporter.Instana.Tests/OpenTelemetry.Exporter.Instana.Tests.csproj index 3efefdbf6d..74623f3f0f 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/OpenTelemetry.Exporter.Instana.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Instana.Tests/OpenTelemetry.Exporter.Instana.Tests.csproj @@ -5,8 +5,8 @@ $(TargetFrameworks);$(NetFrameworkMinimumSupportedVersion) - - + + @@ -15,6 +15,8 @@ + + diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs index bdf6907982..ecbf0224ec 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs @@ -10,28 +10,30 @@ namespace OpenTelemetry.Exporter.Instana.Tests.Processors; public class DefaultActivityProcessorTests { - private readonly DefaultActivityProcessor defaultActivityProcessor = new(); - [Fact] - public async Task ProcessAsync() + public void Process_PopulatesInstanaSpan() { + // Arrange var activity = new Activity("testOperationName"); - activity.Start(); - await Task.Delay(200); - activity.Stop(); + + var start = DateTime.UtcNow; + + activity.SetStartTime(start); + activity.SetEndTime(start.AddSeconds(1)); + var instanaSpan = new InstanaSpan(); - await this.defaultActivityProcessor.ProcessAsync(activity, instanaSpan); + // Act + var processor = new DefaultActivityProcessor(); + processor.Process(activity, instanaSpan); + + // Assert Assert.False(string.IsNullOrEmpty(instanaSpan.S)); Assert.False(string.IsNullOrEmpty(instanaSpan.Lt)); Assert.True(instanaSpan.D > 0); Assert.True(instanaSpan.Ts > 0); Assert.NotNull(instanaSpan.Data); - Assert.NotNull(instanaSpan.Data.data); - Assert.Contains(instanaSpan.Data.data, filter: x => - { - return x.Key == "kind" - && x.Value.Equals("internal"); - }); + Assert.NotNull(instanaSpan.Data.Values); + Assert.Contains(instanaSpan.Data.Values, filter: x => x.Key == "kind" && x.Value.Equals("internal")); } } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/ErrorActivityProcessorTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/ErrorActivityProcessorTests.cs index 3d41ecf9bb..03cdc806ee 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/ErrorActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/ErrorActivityProcessorTests.cs @@ -10,40 +10,60 @@ namespace OpenTelemetry.Exporter.Instana.Tests.Processors; public class ErrorActivityProcessorTests { - private readonly ErrorActivityProcessor errorActivityProcessor = new(); - [Fact] - public async Task Process_ErrorStatusCodeIsSet() + public void Process_ErrorStatusCodeIsSet() { + // Arrange var activity = new Activity("testOperationName"); activity.SetStatus(ActivityStatusCode.Error, "TestErrorDesc"); var instanaSpan = new InstanaSpan(); - await this.errorActivityProcessor.ProcessAsync(activity, instanaSpan); + // Act + var processor = new ErrorActivityProcessor(); + processor.Process(activity, instanaSpan); + + // Assert Assert.Equal(1, instanaSpan.Ec); Assert.NotNull(instanaSpan.Data); - Assert.NotNull(instanaSpan.Data.data); - Assert.Equal("Error", instanaSpan.Data.data[InstanaExporterConstants.ERROR_FIELD]); - Assert.Equal("TestErrorDesc", instanaSpan.Data.data[InstanaExporterConstants.ERROR_DETAIL_FIELD]); + Assert.NotNull(instanaSpan.Data.Values); + Assert.Equal("Error", instanaSpan.Data.Values[InstanaExporterConstants.ErrorField]); + Assert.Equal("TestErrorDesc", instanaSpan.Data.Values[InstanaExporterConstants.ErrorDetailField]); } [Fact] - public async Task Process_ExistsExceptionEvent() + public void Process_ExistsExceptionEvent() { + // Arrange var activity = new Activity("testOperationName"); - var instanaSpan = new InstanaSpan() { TransformInfo = new Implementation.InstanaSpanTransformInfo() { HasExceptionEvent = true } }; - await this.errorActivityProcessor.ProcessAsync(activity, instanaSpan); + var instanaSpan = new InstanaSpan() + { + TransformInfo = new Implementation.InstanaSpanTransformInfo() + { + HasExceptionEvent = true, + }, + }; + + // Act + var processor = new ErrorActivityProcessor(); + processor.Process(activity, instanaSpan); + + // Assert Assert.Equal(1, instanaSpan.Ec); } [Fact] - public async Task Process_NoError() + public void Process_NoError() { + // Arrange var activity = new Activity("testOperationName"); var instanaSpan = new InstanaSpan() { TransformInfo = new Implementation.InstanaSpanTransformInfo() }; - await this.errorActivityProcessor.ProcessAsync(activity, instanaSpan); + // Act + var processor = new ErrorActivityProcessor(); + processor.Process(activity, instanaSpan); + + // Assert Assert.Equal(0, instanaSpan.Ec); } } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/EventsActivityProcessorTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/EventsActivityProcessorTests.cs index ef2730186d..9c847cca0f 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/EventsActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/EventsActivityProcessorTests.cs @@ -10,11 +10,10 @@ namespace OpenTelemetry.Exporter.Instana.Tests.Processors; public class EventsActivityProcessorTests { - private readonly EventsActivityProcessor eventsActivityProcessor = new(); - [Fact] - public async Task ProcessAsync() + public void Process_PopulatesSpan() { + // Arrange var activityTagsCollection = new ActivityTagsCollection { new KeyValuePair("eventTagKey", "eventTagValue") }; var activityEvent = new ActivityEvent( "testActivityEvent", @@ -31,25 +30,30 @@ public async Task ProcessAsync() activity.AddEvent(activityEvent); activity.AddEvent(activityEvent2); var instanaSpan = new InstanaSpan() { TransformInfo = new Implementation.InstanaSpanTransformInfo() }; - if (this.eventsActivityProcessor != null) - { - await this.eventsActivityProcessor.ProcessAsync(activity, instanaSpan); - } - Assert.NotNull(instanaSpan.Data?.Events); + // Act + var processor = new EventsActivityProcessor(); + processor.Process(activity, instanaSpan); + + // Assert + Assert.NotNull(instanaSpan.Data); + Assert.NotNull(instanaSpan.Data.Events); + Assert.Equal(0, instanaSpan.Ec); Assert.Equal(2, instanaSpan.Data.Events.Count); + Assert.Equal("testActivityEvent", instanaSpan.Data.Events[0].Name); Assert.True(instanaSpan.Data.Events[0].Ts > 0); - Assert.NotNull(instanaSpan.Data?.Events[0]?.Tags); - var eventTagValue = string.Empty; - _ = instanaSpan.Data.Events[0].Tags?.TryGetValue("eventTagKey", out eventTagValue); + Assert.NotNull(instanaSpan.Data.Events[0].Tags); + + Assert.True(instanaSpan.Data.Events[0].Tags.TryGetValue("eventTagKey", out var eventTagValue)); Assert.Equal("eventTagValue", eventTagValue); + Assert.Equal("testActivityEvent2", instanaSpan.Data.Events[1].Name); Assert.True(instanaSpan.Data.Events[1].Ts > 0); - Assert.NotNull(instanaSpan.Data?.Events[1]?.Tags); - eventTagValue = string.Empty; - _ = instanaSpan.Data.Events[1].Tags?.TryGetValue("eventTagKey2", out eventTagValue); + Assert.NotNull(instanaSpan.Data?.Events[1].Tags); + + Assert.True(instanaSpan.Data.Events[1].Tags.TryGetValue("eventTagKey2", out eventTagValue)); Assert.Equal("eventTagValue2", eventTagValue); } } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/TagsActivityProcessorTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/TagsActivityProcessorTests.cs index fb2a4d05a2..eb747bc8ff 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/TagsActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/TagsActivityProcessorTests.cs @@ -10,19 +10,22 @@ namespace OpenTelemetry.Exporter.Instana.Tests.Processors; public class TagsActivityProcessorTests { - private readonly TagsActivityProcessor tagsActivityProcessor = new(); - [Fact] - public async Task ProcessAsync_StatusTagsExist() + public void Process_StatusTagsExist() { + // Arrange var activity = new Activity("testOperationName"); activity.AddTag("otel.status_code", "testStatusCode"); activity.AddTag("otel.status_description", "testStatusDescription"); activity.AddTag("otel.testTag", "testTag"); var instanaSpan = new InstanaSpan(); - await this.tagsActivityProcessor.ProcessAsync(activity, instanaSpan); + // Act + var processor = new TagsActivityProcessor(); + processor.Process(activity, instanaSpan); + + // Assert Assert.NotNull(instanaSpan.Data); Assert.NotNull(instanaSpan.Data.Tags); Assert.Contains(instanaSpan.Data.Tags, x => x.Key == "otel.testTag" && x.Value == "testTag"); @@ -31,14 +34,19 @@ public async Task ProcessAsync_StatusTagsExist() } [Fact] - public async Task ProcessAsync_StatusTagsDoNotExist() + public void Process_StatusTagsDoNotExist() { + // Arrange var activity = new Activity("testOperationName"); activity.AddTag("otel.testTag", "testTag"); var instanaSpan = new InstanaSpan(); - await this.tagsActivityProcessor.ProcessAsync(activity, instanaSpan); + // Act + var processor = new TagsActivityProcessor(); + processor.Process(activity, instanaSpan); + + // Assert Assert.NotNull(instanaSpan.Data); Assert.NotNull(instanaSpan.Data.Tags); Assert.Contains(instanaSpan.Data.Tags, x => x.Key == "otel.testTag" && x.Value == "testTag"); diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/TestActivityProcessor.cs b/test/OpenTelemetry.Exporter.Instana.Tests/TestActivityProcessor.cs deleted file mode 100644 index 62c972e85b..0000000000 --- a/test/OpenTelemetry.Exporter.Instana.Tests/TestActivityProcessor.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using OpenTelemetry.Exporter.Instana.Implementation; -using OpenTelemetry.Exporter.Instana.Implementation.Processors; - -namespace OpenTelemetry.Exporter.Instana.Tests; - -internal class TestActivityProcessor : IActivityProcessor -{ - public IActivityProcessor? NextProcessor { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) - { - return Task.CompletedTask; - } -} diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/TestInstanaExporterHelper.cs b/test/OpenTelemetry.Exporter.Instana.Tests/TestInstanaExporterHelper.cs deleted file mode 100644 index 7a88315de0..0000000000 --- a/test/OpenTelemetry.Exporter.Instana.Tests/TestInstanaExporterHelper.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using OpenTelemetry.Resources; - -namespace OpenTelemetry.Exporter.Instana.Tests; - -internal class TestInstanaExporterHelper : IInstanaExporterHelper -{ - public Dictionary Attributes { get; } = []; - - public Resource GetParentProviderResource(BaseExporter otelExporter) - { - return new Resource(this.Attributes); - } - - public bool IsWindows() - { - return false; - } -} diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/TestSpanSender.cs b/test/OpenTelemetry.Exporter.Instana.Tests/TestSpanSender.cs deleted file mode 100644 index 028ded1fa3..0000000000 --- a/test/OpenTelemetry.Exporter.Instana.Tests/TestSpanSender.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using OpenTelemetry.Exporter.Instana.Implementation; - -namespace OpenTelemetry.Exporter.Instana.Tests; - -internal class TestSpanSender : ISpanSender -{ - public Action? OnEnqueue { get; set; } - - public void Enqueue(InstanaSpan instanaSpan) - { - this.OnEnqueue?.Invoke(instanaSpan); - } -} diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs new file mode 100644 index 0000000000..b3e298941d --- /dev/null +++ b/test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs @@ -0,0 +1,171 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Net; +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Text.Json; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Exporter.Instana.Tests; + +public class TracerProviderBuilderExtensionsTests +{ + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(15); + + [Fact] + public async Task AddInstanaExporter_WithEnvironmentVariables_Minimal() + { + // Arrange + var agentKey = Guid.NewGuid().ToString(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using var server = TestHttpServer.RunServer( + (context) => AssertResponse(context, agentKey, tcs), + out var host, + out var port); + + var endpoint = new UriBuilder(Uri.UriSchemeHttp, host, port).Uri; + + using (EnvironmentVariableScope.Create( + [ + new("INSTANA_AGENT_KEY", agentKey), + new("INSTANA_ENDPOINT_URL", endpoint.ToString()), + ])) + { + await this.AddInstanaExporterExportsTraces(tcs, (builder) => + { + builder.AddInstanaExporter(); + }); + } + } + + [Fact] + public async Task AddInstanaExporter_WithEnvironmentVariables_All() + { + // Arrange + var agentKey = Guid.NewGuid().ToString(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using var server = TestHttpServer.RunServer( + (context) => AssertResponse(context, agentKey, tcs), + out var host, + out var port); + + var endpoint = new UriBuilder(Uri.UriSchemeHttp, host, port).Uri; + + using (EnvironmentVariableScope.Create( + [ + new("INSTANA_AGENT_KEY", agentKey), + new("INSTANA_ENDPOINT_URL", endpoint.ToString()), + new("INSTANA_ENDPOINT_PROXY", endpoint.ToString()), + new("INSTANA_TIMEOUT", "60000"), + ])) + { + await this.AddInstanaExporterExportsTraces(tcs, (builder) => + { + builder.AddInstanaExporter(); + }); + } + } + + [Fact] + public async Task AddInstanaExporter_WithOptions() + { + // Arrange + var agentKey = Guid.NewGuid().ToString(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using var server = TestHttpServer.RunServer( + (context) => AssertResponse(context, agentKey, tcs), + out var host, + out var port); + + var endpoint = new UriBuilder(Uri.UriSchemeHttp, host, port).Uri; + + await this.AddInstanaExporterExportsTraces(tcs, (builder) => + { + builder.AddInstanaExporter((options) => + { + options.AgentKey = agentKey; + options.EndpointUri = endpoint; + + options.HttpClientFactory = () => + { + var handler = new HttpClientHandler() + { +#if NET + CheckCertificateRevocationList = true, + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, +#else + ServerCertificateCustomValidationCallback = static (_, _, _, _) => true, +#endif + }; + return new HttpClient(handler, disposeHandler: true); + }; + }); + }); + } + + private static void AssertResponse( + HttpListenerContext context, + string agentKey, + TaskCompletionSource completionSource) + { + try + { + Assert.Equal("POST", context.Request.HttpMethod); + Assert.Equal(agentKey, context.Request.Headers["X-INSTANA-KEY"]); + + using var reader = new StreamReader(context.Request.InputStream); + completionSource.SetResult(reader.ReadToEnd()); + } + catch (Exception ex) + { + completionSource.SetException(ex); + } + } + + private async Task AddInstanaExporterExportsTraces( + TaskCompletionSource completionSource, + Action configure) + { + // Arrange + using var activitySource = new ActivitySource(Guid.NewGuid().ToString()); + + var builder = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySource.Name) + .SetSampler(new AlwaysOnSampler()); + + configure(builder); + + using var provider = builder.Build(); + + // Act + using (var activity = activitySource.StartActivity(Guid.NewGuid().ToString())) + { + activity?.AddTag("service.name", Guid.NewGuid().ToString()); + } + + // Assert +#if NET + await completionSource.Task.WaitAsync(Timeout); +#else + using var cts = new CancellationTokenSource(Timeout); + var completed = await Task.WhenAny(completionSource.Task, Task.Delay(Timeout, cts.Token)); + Assert.Same(completionSource.Task, completed); +#endif + + var actual = await completionSource.Task; + + using var document = JsonDocument.Parse(actual); + Assert.NotNull(document); + + var exportedSpans = document.RootElement.GetProperty("spans"); + Assert.NotEmpty(exportedSpans.EnumerateArray()); + } +}