From 5228c6e5470879b22d4885e8f2759cb0d294ba14 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 13:17:54 +0100 Subject: [PATCH 01/18] [Instana] Support configuration with code - Add support for configuring with options class. - Add `net8.0` and `net10.0` TFMs. - Use modern C# features. - Support batch export limit configuration and apply limits. - Remove sync-over-async. - Add support for custom `HttpClient` creation. - Remove redundant code. --- .../InfluxDBExporterExtensions.cs | 15 +- .../.publicApi/PublicAPI.Unshipped.txt | 15 ++ .../CHANGELOG.md | 10 + .../Implementation/ISpanSender.cs | 2 +- .../InstanaExporterEventSource.cs | 4 +- .../Implementation/InstanaSpan.cs | 190 ++++++++---------- .../Implementation/InstanaSpanFactory.cs | 24 +-- .../InstanaSpanTransformInfo.cs | 21 +- .../Processors/ActivityProcessorBase.cs | 11 +- .../Processors/DefaultActivityProcessor.cs | 26 +-- .../Processors/ErrorActivityProcessor.cs | 12 +- .../Processors/EventsActivityProcessor.cs | 8 +- .../Processors/IActivityProcessor.cs | 2 +- .../Processors/TagsActivityProcessor.cs | 19 +- .../Implementation/SpanSender.cs | 76 +++++-- .../Implementation/Transport.cs | 183 +++++++---------- .../InstanaExporter.cs | 80 ++++---- .../InstanaExporterHelper.cs | 16 +- .../InstanaExporterOptions.cs | 52 +++++ .../OpenTelemetry.Exporter.Instana.csproj | 4 +- .../TracerProviderBuilderExtensions.cs | 60 +++++- .../InstanaExporterTests.cs | 29 ++- .../InstanaSpanFactoryTests.cs | 15 +- ...penTelemetry.Exporter.Instana.Tests.csproj | 1 + .../DefaultActivityProcessorTests.cs | 21 +- .../Processors/ErrorActivityProcessorTests.cs | 38 +++- .../EventsActivityProcessorTests.cs | 32 +-- .../Processors/TagsActivityProcessorTests.cs | 20 +- .../TestActivityProcessor.cs | 8 +- .../TestSpanSender.cs | 3 +- 30 files changed, 559 insertions(+), 438 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs 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 5f5643e8f8..a31cf80884 100644 --- a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md @@ -5,6 +5,16 @@ * Updated OpenTelemetry core component version(s) to `1.15.2`. ([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080)) +* Add support for configuring the Instana exporter using `InstanaExporterOptions`. + ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + +* **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. + ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ## 1.0.6 Released 2026-Jan-21 diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/ISpanSender.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/ISpanSender.cs index 2d6f1ccf26..30807963cf 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/ISpanSender.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/ISpanSender.cs @@ -5,5 +5,5 @@ namespace OpenTelemetry.Exporter.Instana.Implementation; internal interface ISpanSender { - void Enqueue(InstanaSpan instanaSpan); + bool 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..1e1e99deb0 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 + { + data = 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,82 @@ 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); + public Data() + { + this.Events = new(8); + this.data = new(8); + this.Tags = new(2); + } #pragma warning disable SA1300 // Element should begin with upper-case letter - public Dictionary data { - 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..ad8ea8f596 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; - } + data = [], + Tags = [], + Events = new(8), + }, + TransformInfo = new(), + }; } 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..540df9fb04 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) { @@ -24,7 +19,7 @@ protected virtual void PreProcess(Activity activity, InstanaSpan instanaSpan) instanaSpan.Data ??= new Data() { data = [], - Events = new List(8), + 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..24679bd665 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs @@ -5,9 +5,9 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal class DefaultActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class DefaultActivityProcessor : ActivityProcessorBase, IActivityProcessor { - public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) + public override void Process(Activity activity, InstanaSpan instanaSpan) { this.PreProcess(activity, instanaSpan); @@ -47,19 +47,16 @@ 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) + private static SpanKind GetSpanKind(ActivityKind activityKind) => activityKind switch { - return activityKind switch - { - ActivityKind.Consumer or ActivityKind.Server => SpanKind.ENTRY, - ActivityKind.Client or ActivityKind.Producer => SpanKind.EXIT, - ActivityKind.Internal => SpanKind.INTERMEDIATE, - _ => SpanKind.NOT_SET, - }; - } + 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) { @@ -108,9 +105,6 @@ private static void SetKind(Activity activity, InstanaSpan instanaSpan) } } - 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..eed7d8e0d5 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs @@ -5,9 +5,9 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal class ErrorActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class ErrorActivityProcessor : ActivityProcessorBase, IActivityProcessor { - public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) + public override void Process(Activity activity, InstanaSpan instanaSpan) { this.PreProcess(activity, instanaSpan); @@ -23,18 +23,14 @@ public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSp } } } - 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..540fbeb4ce 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs @@ -5,9 +5,9 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal class EventsActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class EventsActivityProcessor : ActivityProcessorBase, IActivityProcessor { - public override async Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) + public override void Process(Activity activity, InstanaSpan instanaSpan) { this.PreProcess(activity, instanaSpan); @@ -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..7bc95e28a3 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, IActivityProcessor { - 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 index 9e20b6e805..5321a7a1d6 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs @@ -2,45 +2,77 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; +using System.Diagnostics; 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 +internal sealed class SpanSender : IDisposable, ISpanSender { - private readonly Task queueSenderTask; - private readonly ConcurrentQueue spansQueue = new(); + private readonly CancellationTokenSource cancellationTokenSource; + private readonly BatchExportProcessorOptions options; + private readonly ConcurrentQueue queue; + private readonly Transport transport; - public SpanSender() + public SpanSender(InstanaExporterOptions options) { - // 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(); + this.options = options.BatchExportProcessorOptions; + this.queue = new(); + this.cancellationTokenSource = new(); + this.transport = new Transport(options); + + Task.Factory.StartNew(this.ProcessingLoop, this.cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + public void Dispose() + { + if (this.cancellationTokenSource != null) + { + this.cancellationTokenSource.Cancel(); + this.cancellationTokenSource.Dispose(); + } + + this.transport?.Dispose(); + + GC.SuppressFinalize(this); } - public void Enqueue(InstanaSpan instanaSpan) + public bool Enqueue(InstanaSpan instanaSpan) { - if (Transport.IsAvailable) + if (this.queue.Count < this.options.MaxQueueSize) { - this.spansQueue.Enqueue(instanaSpan); + this.queue.Enqueue(instanaSpan); + return true; } + + return false; } - private async void TaskSpanSender() + private async Task ProcessingLoop() { - // this will be an infinite loop - while (true) + var delay = this.options.ScheduledDelayMilliseconds; + + try { - // check if we can send spans - if (this.spansQueue.TryPeek(out var _)) + while (!this.cancellationTokenSource.IsCancellationRequested) { - // actually send spans - await Transport.SendSpansAsync(this.spansQueue).ConfigureAwait(false); - } + if (!this.queue.IsEmpty) + { + try + { + await this.transport.SendAsync(this.queue, this.cancellationTokenSource.Token).ConfigureAwait(false); + } + catch (Exception ex) + { + InstanaExporterEventSource.Log.FailedExport(ex); + } + } - // rest for a while - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(delay, this.cancellationTokenSource.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + // Processing cancelled } } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index 97cee57f7b..afe1ba4c85 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Buffers; using System.Collections.Concurrent; using System.Globalization; using System.Net; @@ -11,167 +12,127 @@ 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 async Task SendAsync(ConcurrentQueue batch, CancellationToken cancellationToken) { + 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; - // 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) + int maxBatchSize = this.options.BatchExportProcessorOptions.MaxExportBatchSize; + int written = 0; + + while (batch.TryDequeue(out var span) && sendBuffer.Position < MultiSpanBufferLimit && written <= maxBatchSize) { - if (!first) + if (written > 0) { - await writer.WriteAsync(",").ConfigureAwait(false); + await writer.WriteAsync(',').ConfigureAwait(false); } await InstanaSpanSerializer.SerializeToStreamWriterAsync(span, writer).ConfigureAwait(false); - await writer.FlushAsync().ConfigureAwait(false); - first = false; +#if NET + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); +#else + await writer.FlushAsync().ConfigureAwait(false); +#endif - // 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); + +#if NET + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); +#else await writer.FlushAsync().ConfigureAwait(false); +#endif 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; + + content.Headers.Add("X-INSTANA-KEY", this.options.AgentKey); + content.Headers.Add("X-INSTANA-NOTRACE", "1"); + content.Headers.Add("X-INSTANA-TIME", this.options.UtcNow().ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture)); - 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; - } + this.client ??= this.CreateClient(); - if (string.IsNullOrEmpty(configuredEndpoint)) - { - configuredEndpoint = Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_ENDPOINT_URL); + using var response = await this.client.SendAsync(message, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); } - - if (string.IsNullOrEmpty(configuredEndpoint)) + finally { - return; + ArrayPool.Shared.Return(buffer); } + } - bundleUrl = configuredEndpoint + "/bundle"; - - if (string.IsNullOrEmpty(configuredAgentKey)) + private HttpClient CreateClient() + { + if (this.options.HttpClientFactory is { } factory) { - configuredAgentKey = Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_AGENT_KEY); + return factory(); } - if (string.IsNullOrEmpty(configuredAgentKey)) +#pragma warning disable CA2000 // Dispose objects before losing scope + var handler = new HttpClientHandler() { - return; - } + CheckCertificateRevocationList = true, + }; +#pragma warning restore CA2000 // Dispose objects before losing scope - if (backendTimeout == 0) + if (this.options.ProxyUri is { } proxyAddress) { - if (!int.TryParse(Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_TIMEOUT), out backendTimeout)) - { - backendTimeout = InstanaExporterConstants.BACKEND_DEFAULT_TIMEOUT; - } + handler.Proxy = new WebProxy(proxyAddress, true); + handler.UseProxy = true; } - ConfigureBackendClient(); - isConfigured = true; - } + var client = new HttpClient(handler, disposeHandler: true); - private static void ConfigureBackendClient() - { - if (Client != null) + try { - return; + client.Timeout = TimeSpan.FromMilliseconds(this.options.BatchExportProcessorOptions.ExporterTimeoutMilliseconds); + return client; } - -#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)) + catch (Exception) { - 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 + client.Dispose(); + throw; } - -#pragma warning disable CA5400 - Client = new InstanaHttpClient(backendTimeout, configuredHandler); -#pragma warning restore CA5400 - - Client.DefaultRequestHeaders.Add("X-INSTANA-KEY", configuredAgentKey); - } -} - -#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"); - } - - public InstanaHttpClient(int timeout, HttpClientHandler handler) - : base(handler) - { - this.Timeout = TimeSpan.FromMilliseconds(timeout); - this.DefaultRequestHeaders.Add("X-INSTANA-NOTRACE", "1"); } } diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs index 1c2fc4ad23..a5bfd34e4f 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs @@ -11,11 +11,15 @@ namespace OpenTelemetry.Exporter.Instana; internal sealed class InstanaExporter : BaseExporter { + private readonly SpanSender sender; private readonly IActivityProcessor activityProcessor; - private bool shutdownCalled; + private readonly string? processId; - public InstanaExporter(IActivityProcessor? activityProcessor = null) + private int wasShutdown; + + public InstanaExporter(InstanaExporterOptions options, IActivityProcessor? activityProcessor = null) { + this.sender = new SpanSender(options); this.activityProcessor = activityProcessor ?? new DefaultActivityProcessor { NextProcessor = new TagsActivityProcessor @@ -23,23 +27,32 @@ public InstanaExporter(IActivityProcessor? activityProcessor = null) NextProcessor = new EventsActivityProcessor { NextProcessor = new ErrorActivityProcessor() }, }, }; - } - internal ISpanSender SpanSender { get; set; } = new SpanSender(); + if (this.Helper.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(); + internal IInstanaExporterHelper Helper { get; set; } = new InstanaExporterHelper(); 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); @@ -51,29 +64,24 @@ public override ExportResult Export(in Batch batch) continue; } - var span = this.ParseActivityAsync(activity, serviceName, from).Result; - this.SpanSender.Enqueue(span); + var span = this.ParseActivity(activity, serviceName, from); + + _ = this.sender.Enqueue(span); } return ExportResult.Success; } - protected override bool OnShutdown(int timeoutMilliseconds) + protected override void Dispose(bool disposing) { - if (!this.shutdownCalled) - { - this.shutdownCalled = true; - return true; - } - else - { - return false; - } + this.sender?.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 +90,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.Helper.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,21 +143,18 @@ 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) { instanaSpan.Data.data[InstanaExporterConstants.SERVICE_FIELD] = serviceName; } - if (instanaSpan.Data.data != null) - { - instanaSpan.Data.data[InstanaExporterConstants.OPERATION_FIELD] = activity.DisplayName; - } + instanaSpan.Data.data?[InstanaExporterConstants.OPERATION_FIELD] = activity.DisplayName; if (activity.TraceStateString != null && !string.IsNullOrEmpty(activity.TraceStateString) && instanaSpan.Data.data != null) { diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs index 90bf9b97a2..dcc83567bc 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs @@ -6,15 +6,15 @@ namespace OpenTelemetry.Exporter.Instana; -internal class InstanaExporterHelper : IInstanaExporterHelper +internal sealed class InstanaExporterHelper : IInstanaExporterHelper { public Resource GetParentProviderResource(BaseExporter otelExporter) - { - return otelExporter.ParentProvider.GetResource(); - } + => otelExporter.ParentProvider.GetResource(); - public bool IsWindows() - { - return Environment.OSVersion.Platform == PlatformID.Win32NT; - } + public bool IsWindows() => +#if NET + OperatingSystem.IsWindows(); +#else + Environment.OSVersion.Platform == PlatformID.Win32NT; +#endif } diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs new file mode 100644 index 0000000000..6debcd5fbb --- /dev/null +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif + +using System.Diagnostics; + +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 agent. + /// + 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 configure + /// the to use to send telemetry to the Instana endpoint. + /// + 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 +} diff --git a/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj b/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj index aa269e1c06..f1246b06b4 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/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs index 46f40881ff..e9331a94e3 100644 --- a/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Globalization; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Trace; namespace OpenTelemetry.Exporter.Instana; @@ -11,20 +13,60 @@ 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 NET + ArgumentNullException.ThrowIfNull(builder); +#else + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } +#endif + + return builder.AddProcessor((serviceProvider) => + { + var options = serviceProvider.GetService() ?? new(); + + ConfigureFromEnvironment(options); + + configure?.Invoke(options); + + return new BatchActivityExportProcessor(new InstanaExporter(options)); + }); + } + + private static void ConfigureFromEnvironment(InstanaExporterOptions options) { - if (options == null) + if (options.EndpointUri is null && + Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_ENDPOINT_URL) is { Length: > 0 } endpointUrl) { - throw new ArgumentNullException(nameof(options)); + options.EndpointUri = new Uri(endpointUrl, UriKind.Absolute); } -#pragma warning disable CA2000 - return options.AddProcessor(new BatchActivityExportProcessor(new InstanaExporter())); -#pragma warning restore CA2000 + if (string.IsNullOrEmpty(options.AgentKey) && + Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_AGENT_KEY) is { Length: > 0 } agentKey) + { + options.AgentKey = agentKey; + } + + if (Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_TIMEOUT) is { Length: > 0 } timeout && + int.TryParse(timeout, NumberStyles.Integer, CultureInfo.InvariantCulture, out var timeoutMilliseconds)) + { + options.BatchExportProcessorOptions.ExporterTimeoutMilliseconds = timeoutMilliseconds; + } } } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs index d076911310..f61834e089 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs @@ -18,6 +18,10 @@ public class InstanaExporterTests [Fact] public void Export() { + var options = new InstanaExporterOptions() + { + }; + this.instanaExporterHelper.Attributes.Clear(); this.instanaExporterHelper.Attributes.Add("service.name", "serviceName"); this.instanaExporterHelper.Attributes.Add("service.instance.id", "serviceInstanceId"); @@ -26,10 +30,9 @@ public void Export() this.spanSender.OnEnqueue = span => this.CloneSpan(span); - this.instanaExporter = new InstanaExporter(this.activityProcessor) + this.instanaExporter = new InstanaExporter(options, this.activityProcessor) { - InstanaExporterHelper = this.instanaExporterHelper, - SpanSender = this.spanSender, + Helper = this.instanaExporterHelper, }; var activity = new Activity("testOperationName"); @@ -52,6 +55,10 @@ public void Export() [Fact] public void Export_ProcessPidDoesNotExistButServiceIdExists() { + var options = new InstanaExporterOptions() + { + }; + this.instanaExporterHelper.Attributes.Clear(); this.instanaExporterHelper.Attributes.Add("service.name", "serviceName"); this.instanaExporterHelper.Attributes.Add("service.instance.id", "serviceInstanceId"); @@ -59,10 +66,9 @@ public void Export_ProcessPidDoesNotExistButServiceIdExists() this.spanSender.OnEnqueue = span => this.CloneSpan(span); - this.instanaExporter = new InstanaExporter(this.activityProcessor) + this.instanaExporter = new InstanaExporter(options, this.activityProcessor) { - InstanaExporterHelper = this.instanaExporterHelper, - SpanSender = this.spanSender, + Helper = this.instanaExporterHelper, }; var activity = new Activity("testOperationName"); @@ -81,12 +87,15 @@ public void Export_ProcessPidDoesNotExistButServiceIdExists() } [Fact] - public void Export_ExporterIsShotDown() + public void Export_ExporterIsShutDown() { - this.instanaExporter = new InstanaExporter(this.activityProcessor) + var options = new InstanaExporterOptions() + { + }; + + this.instanaExporter = new InstanaExporter(options, this.activityProcessor) { - InstanaExporterHelper = this.instanaExporterHelper, - SpanSender = this.spanSender, + Helper = this.instanaExporterHelper, }; var activity = new Activity("testOperationName"); diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs index f53310e25c..d93c7aa6bd 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.data); + Assert.Empty(actual.Data.Tags); + Assert.Empty(actual.Data.Events); } } 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..159a75210c 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/OpenTelemetry.Exporter.Instana.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Instana.Tests/OpenTelemetry.Exporter.Instana.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs index bdf6907982..e9aeca29b8 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs @@ -10,28 +10,29 @@ 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); + + Thread.Sleep(200); // Simulate some work being done + activity.Stop(); 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.Contains(instanaSpan.Data.data, 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..d17475e0c9 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/ErrorActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/ErrorActivityProcessorTests.cs @@ -10,16 +10,19 @@ 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); @@ -28,22 +31,39 @@ public async Task Process_ErrorStatusCodeIsSet() } [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..d87725c5a7 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() { + // Act 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() { + // Act 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 index 62c972e85b..083abfa826 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/TestActivityProcessor.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/TestActivityProcessor.cs @@ -7,12 +7,12 @@ namespace OpenTelemetry.Exporter.Instana.Tests; -internal class TestActivityProcessor : IActivityProcessor +internal sealed class TestActivityProcessor : IActivityProcessor { - public IActivityProcessor? NextProcessor { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public IActivityProcessor? NextProcessor { get; set; } - public Task ProcessAsync(Activity activity, InstanaSpan instanaSpan) + public void Process(Activity activity, InstanaSpan instanaSpan) { - return Task.CompletedTask; + // No-op } } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/TestSpanSender.cs b/test/OpenTelemetry.Exporter.Instana.Tests/TestSpanSender.cs index 028ded1fa3..69c372e73a 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/TestSpanSender.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/TestSpanSender.cs @@ -9,8 +9,9 @@ internal class TestSpanSender : ISpanSender { public Action? OnEnqueue { get; set; } - public void Enqueue(InstanaSpan instanaSpan) + public bool Enqueue(InstanaSpan instanaSpan) { this.OnEnqueue?.Invoke(instanaSpan); + return true; } } From 344e533b5a5789ea035fbf6ebbf9ba498be8dc45 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 16:33:21 +0100 Subject: [PATCH 02/18] [Instana] Fix tests - Fix InstanaExporterTests. - Update README. - Extend assertions to verify JSON payloads. - Remove redundant code. - Remove redundant mocks. - Fix some StyleCop warnings. --- .../IInstanaExporterHelper.cs | 14 - .../Implementation/ISpanSender.cs | 9 - .../Implementation/InstanaSpanSerializer.cs | 141 +++---- .../Processors/DefaultActivityProcessor.cs | 13 +- .../Processors/ErrorActivityProcessor.cs | 2 +- .../Processors/EventsActivityProcessor.cs | 2 +- .../Processors/TagsActivityProcessor.cs | 2 +- .../Implementation/SpanSender.cs | 2 +- .../Implementation/Transport.cs | 2 + .../InstanaExporter.cs | 31 +- .../InstanaExporterHelper.cs | 20 - .../InstanaExporterOptions.cs | 3 + src/OpenTelemetry.Exporter.Instana/README.md | 28 +- .../TracerProviderBuilderExtensions.cs | 3 +- .../InstanaExporterTests.cs | 389 +++++++++++++++--- ...penTelemetry.Exporter.Instana.Tests.csproj | 6 +- .../TestActivityProcessor.cs | 18 - .../TestInstanaExporterHelper.cs | 22 - .../TestSpanSender.cs | 17 - 19 files changed, 464 insertions(+), 260 deletions(-) delete mode 100644 src/OpenTelemetry.Exporter.Instana/IInstanaExporterHelper.cs delete mode 100644 src/OpenTelemetry.Exporter.Instana/Implementation/ISpanSender.cs delete mode 100644 src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs delete mode 100644 test/OpenTelemetry.Exporter.Instana.Tests/TestActivityProcessor.cs delete mode 100644 test/OpenTelemetry.Exporter.Instana.Tests/TestInstanaExporterHelper.cs delete mode 100644 test/OpenTelemetry.Exporter.Instana.Tests/TestSpanSender.cs 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 30807963cf..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 -{ - bool Enqueue(InstanaSpan instanaSpan); -} diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs index 3046fa58ba..f11a9ffa3a 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs @@ -8,46 +8,44 @@ namespace OpenTelemetry.Exporter.Instana.Implementation; 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(); - } + 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 async Task SerializeToStreamWriterAsync(InstanaSpan instanaSpan, StreamWriter writer) { - await writer.WriteAsync(OPEN_BRACE).ConfigureAwait(false); + await writer.WriteAsync(OpenCurlyBrace).ConfigureAwait(false); await AppendProperty(instanaSpan.T, "t", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); await AppendProperty(instanaSpan.S, "s", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); if (!string.IsNullOrEmpty(instanaSpan.P)) { await AppendProperty(instanaSpan.P, "p", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); } if (!string.IsNullOrEmpty(instanaSpan.Lt)) { await AppendProperty(instanaSpan.Lt, "lt", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); } if (instanaSpan.Tp) @@ -55,27 +53,27 @@ internal static async Task SerializeToStreamWriterAsync(InstanaSpan instanaSpan, #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); + await writer.WriteAsync(Comma).ConfigureAwait(false); } 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); + await writer.WriteAsync(Comma).ConfigureAwait(false); } await AppendProperty(instanaSpan.N, "n", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); await AppendPropertyAsync(DateToUnixMillis(instanaSpan.Ts), "ts", writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).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 writer.WriteAsync(Comma).ConfigureAwait(false); await AppendObjectAsync(SerializeDataAsync, "data", instanaSpan, writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); await AppendObjectAsync(SerializeFromAsync, "f", instanaSpan, writer).ConfigureAwait(false); - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); await AppendPropertyAsync(instanaSpan.Ec, "ec", writer).ConfigureAwait(false); - await writer.WriteAsync(CLOSE_BRACE).ConfigureAwait(false); + await writer.WriteAsync(CloseCurlyBrace).ConfigureAwait(false); } private static async Task SerializeFromAsync(InstanaSpan instanaSpan, StreamWriter writer) @@ -85,19 +83,14 @@ private static async Task SerializeFromAsync(InstanaSpan instanaSpan, StreamWrit await writer.WriteAsync("\"}").ConfigureAwait(false); } - 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) - { + private static async Task SerializeTagsAsync(InstanaSpan instanaSpan, StreamWriter writer) => await SerializeTagsLogicAsync(instanaSpan.Data.Tags, writer).ConfigureAwait(false); - } private static async Task SerializeTagsLogicAsync(Dictionary? tags, StreamWriter writer) { - await writer.WriteAsync(OPEN_BRACE).ConfigureAwait(false); + await writer.WriteAsync(OpenCurlyBrace).ConfigureAwait(false); if (tags == null) { return; @@ -112,18 +105,18 @@ private static async Task SerializeTagsLogicAsync(Dictionary? ta { if (i > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); } else { i = 1; } - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + 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(QuoteColonQuote).ConfigureAwait(false); await writer.WriteAsync(enumerator.Current.Value).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + await writer.WriteAsync(Quote).ConfigureAwait(false); } } catch (InvalidOperationException) @@ -134,37 +127,37 @@ private static async Task SerializeTagsLogicAsync(Dictionary? ta } } - await writer.WriteAsync(CLOSE_BRACE).ConfigureAwait(false); + await writer.WriteAsync(CloseCurlyBrace).ConfigureAwait(false); } private static async Task AppendProperty(string? value, string? name, StreamWriter json) { - await json.WriteAsync(QUOTE).ConfigureAwait(false); + await json.WriteAsync(Quote).ConfigureAwait(false); await json.WriteAsync(name).ConfigureAwait(false); - await json.WriteAsync(QUOTE_COLON_QUOTE).ConfigureAwait(false); + await json.WriteAsync(QuoteColonQuote).ConfigureAwait(false); await json.WriteAsync(value).ConfigureAwait(false); - await json.WriteAsync(QUOTE).ConfigureAwait(false); + await json.WriteAsync(Quote).ConfigureAwait(false); } private static async Task AppendPropertyAsync(long value, string name, StreamWriter json) { - await json.WriteAsync(QUOTE).ConfigureAwait(false); + await json.WriteAsync(Quote).ConfigureAwait(false); await json.WriteAsync(name).ConfigureAwait(false); - await json.WriteAsync(QUOTE_COLON).ConfigureAwait(false); + await json.WriteAsync(QuoteColon).ConfigureAwait(false); await json.WriteAsync(value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); } private static async Task AppendObjectAsync(Func valueFunction, string name, InstanaSpan instanaSpan, StreamWriter json) { - await json.WriteAsync(QUOTE).ConfigureAwait(false); + await json.WriteAsync(Quote).ConfigureAwait(false); await json.WriteAsync(name).ConfigureAwait(false); - await json.WriteAsync(QUOTE_COLON).ConfigureAwait(false); + await json.WriteAsync(QuoteColon).ConfigureAwait(false); await valueFunction(instanaSpan, json).ConfigureAwait(false); } private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWriter writer) { - await writer.WriteAsync(OPEN_BRACE).ConfigureAwait(false); + await writer.WriteAsync(OpenCurlyBrace).ConfigureAwait(false); if (instanaSpan.Data.data == null) { return; @@ -179,18 +172,18 @@ private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWrit { if (i > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); } else { i = 1; } - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + 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(QuoteColonQuote).ConfigureAwait(false); await writer.WriteAsync(enumerator.Current.Value.ToString()).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + await writer.WriteAsync(Quote).ConfigureAwait(false); } } catch (InvalidOperationException) @@ -203,7 +196,7 @@ private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWrit if (instanaSpan.Data.Tags.Count > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); // serialize tags await AppendObjectAsync(SerializeTagsAsync, InstanaExporterConstants.TAGS_FIELD, instanaSpan, writer).ConfigureAwait(false); @@ -211,13 +204,13 @@ private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWrit if (instanaSpan.Data.Events.Count > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); // serialize tags await AppendObjectAsync(SerializeEventsAsync, InstanaExporterConstants.EVENTS_FIELD, instanaSpan, writer).ConfigureAwait(false); } - await writer.WriteAsync(CLOSE_BRACE).ConfigureAwait(false); + await writer.WriteAsync(CloseCurlyBrace).ConfigureAwait(false); } private static async Task SerializeEventsAsync(InstanaSpan instanaSpan, StreamWriter writer) @@ -236,35 +229,35 @@ private static async Task SerializeEventsAsync(InstanaSpan instanaSpan, StreamWr { if (i > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); + await writer.WriteAsync(Comma).ConfigureAwait(false); } else { i = 1; } - await writer.WriteAsync(OPEN_BRACE).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + await writer.WriteAsync(OpenCurlyBrace).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(QuoteColonQuote).ConfigureAwait(false); await writer.WriteAsync(enumerator.Current.Name).ConfigureAwait(false); - await writer.WriteAsync(QUOTE_COMMA_QUOTE).ConfigureAwait(false); + await writer.WriteAsync(QuoteCommaQuote).ConfigureAwait(false); await writer.WriteAsync(InstanaExporterConstants.EVENT_TIMESTAMP_FIELD).ConfigureAwait(false); - await writer.WriteAsync(QUOTE_COLON_QUOTE).ConfigureAwait(false); + await writer.WriteAsync(QuoteColonQuote).ConfigureAwait(false); await writer.WriteAsync(DateToUnixMillis(enumerator.Current.Ts).ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + await writer.WriteAsync(Quote).ConfigureAwait(false); if (enumerator.Current.Tags.Count > 0) { - await writer.WriteAsync(COMMA).ConfigureAwait(false); - await writer.WriteAsync(QUOTE).ConfigureAwait(false); + 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 writer.WriteAsync(Quote).ConfigureAwait(false); + await writer.WriteAsync(Colon).ConfigureAwait(false); await SerializeTagsLogicAsync(enumerator.Current.Tags, writer).ConfigureAwait(false); } - await writer.WriteAsync(CLOSE_BRACE).ConfigureAwait(false); + await writer.WriteAsync(CloseCurlyBrace).ConfigureAwait(false); } await writer.WriteAsync("]").ConfigureAwait(false); diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs index 24679bd665..f954795c1a 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal sealed class DefaultActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class DefaultActivityProcessor : ActivityProcessorBase { public override void Process(Activity activity, InstanaSpan instanaSpan) { @@ -50,6 +50,17 @@ public override void Process(Activity activity, InstanaSpan instanaSpan) base.Process(activity, instanaSpan); } + internal static DefaultActivityProcessor CreateDefault() => new() + { + NextProcessor = new TagsActivityProcessor() + { + NextProcessor = new EventsActivityProcessor() + { + NextProcessor = new ErrorActivityProcessor(), + }, + }, + }; + private static SpanKind GetSpanKind(ActivityKind activityKind) => activityKind switch { ActivityKind.Consumer or ActivityKind.Server => SpanKind.ENTRY, diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs index eed7d8e0d5..a164b97029 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal sealed class ErrorActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class ErrorActivityProcessor : ActivityProcessorBase { public override void Process(Activity activity, InstanaSpan instanaSpan) { diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs index 540fbeb4ce..c3a99991d7 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal sealed class EventsActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class EventsActivityProcessor : ActivityProcessorBase { public override void Process(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 7bc95e28a3..b0d4a9c4c3 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/TagsActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/TagsActivityProcessor.cs @@ -5,7 +5,7 @@ namespace OpenTelemetry.Exporter.Instana.Implementation.Processors; -internal sealed class TagsActivityProcessor : ActivityProcessorBase, IActivityProcessor +internal sealed class TagsActivityProcessor : ActivityProcessorBase { public override void Process(Activity activity, InstanaSpan instanaSpan) { diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs index 5321a7a1d6..50bd45ad06 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs @@ -6,7 +6,7 @@ namespace OpenTelemetry.Exporter.Instana.Implementation; -internal sealed class SpanSender : IDisposable, ISpanSender +internal sealed class SpanSender : IDisposable { private readonly CancellationTokenSource cancellationTokenSource; private readonly BatchExportProcessorOptions options; diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index afe1ba4c85..f05f816ec9 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -112,7 +112,9 @@ private HttpClient CreateClient() #pragma warning disable CA2000 // Dispose objects before losing scope var handler = new HttpClientHandler() { +#if !NETFRAMEWORK CheckCertificateRevocationList = true, +#endif }; #pragma warning restore CA2000 // Dispose objects before losing scope diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs index a5bfd34e4f..fdaef5c827 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs @@ -11,24 +11,20 @@ namespace OpenTelemetry.Exporter.Instana; internal sealed class InstanaExporter : BaseExporter { - private readonly SpanSender sender; private readonly IActivityProcessor activityProcessor; + private readonly InstanaExporterOptions options; + private readonly SpanSender sender; private readonly string? processId; private int wasShutdown; - public InstanaExporter(InstanaExporterOptions options, IActivityProcessor? activityProcessor = null) + public InstanaExporter(InstanaExporterOptions options, IActivityProcessor activityProcessor) { - this.sender = new SpanSender(options); - this.activityProcessor = activityProcessor ?? new DefaultActivityProcessor - { - NextProcessor = new TagsActivityProcessor - { - NextProcessor = new EventsActivityProcessor { NextProcessor = new ErrorActivityProcessor() }, - }, - }; + this.options = options; + this.sender = new SpanSender(this.options); + this.activityProcessor = activityProcessor; - if (this.Helper.IsWindows()) + if (IsWindows()) { #if NET this.processId = Environment.ProcessId.ToString(CultureInfo.InvariantCulture); @@ -37,9 +33,16 @@ public InstanaExporter(InstanaExporterOptions options, IActivityProcessor? activ this.processId = process.Id.ToString(CultureInfo.InvariantCulture); #endif } - } - internal IInstanaExporterHelper Helper { 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) { @@ -90,7 +93,7 @@ private string ExtractServiceName(ref From from) var serviceId = string.Empty; var processId = string.Empty; var hostId = string.Empty; - var resource = this.Helper.GetParentProviderResource(this); + var resource = this.options.GetParentProviderResource(this); if (resource != Resource.Empty && resource.Attributes.Any()) { diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporterHelper.cs deleted file mode 100644 index dcc83567bc..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 sealed class InstanaExporterHelper : IInstanaExporterHelper -{ - public Resource GetParentProviderResource(BaseExporter otelExporter) - => otelExporter.ParentProvider.GetResource(); - - public bool IsWindows() => -#if NET - OperatingSystem.IsWindows(); -#else - Environment.OSVersion.Platform == PlatformID.Win32NT; -#endif -} diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs index 6debcd5fbb..ae45f75fa5 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs @@ -6,6 +6,7 @@ #endif using System.Diagnostics; +using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter.Instana; @@ -49,4 +50,6 @@ public class InstanaExporterOptions #else static () => DateTimeOffset.UtcNow; #endif + + internal Func, Resource> GetParentProviderResource { get; set; } = static (exporter) => exporter.ParentProvider.GetResource(); } diff --git a/src/OpenTelemetry.Exporter.Instana/README.md b/src/OpenTelemetry.Exporter.Instana/README.md index b163667ff0..6d71d50e3a 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,28 @@ 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.Timeout` property. ## Troubleshooting diff --git a/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs index e9331a94e3..4b3b71b07d 100644 --- a/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs @@ -3,6 +3,7 @@ using System.Globalization; using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Exporter.Instana.Implementation.Processors; using OpenTelemetry.Trace; namespace OpenTelemetry.Exporter.Instana; @@ -45,7 +46,7 @@ public static TracerProviderBuilder AddInstanaExporter(this TracerProviderBuilde configure?.Invoke(options); - return new BatchActivityExportProcessor(new InstanaExporter(options)); + return new BatchActivityExportProcessor(new InstanaExporter(options, DefaultActivityProcessor.CreateDefault())); }); } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs index f61834e089..98de053a50 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs @@ -2,118 +2,407 @@ // 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() { + // Arrange + var startedUtc = DateTimeOffset.FromUnixTimeMilliseconds(1776522506123); + var utcNow = DateTimeOffset.FromUnixTimeMilliseconds(1776523555069); + var options = new InstanaExporterOptions() { + 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, }; - 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"); + var tcs = new TaskCompletionSource(); + + 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"]); - this.spanSender.OnEnqueue = span => this.CloneSpan(span); + using var reader = new StreamReader(context.Request.InputStream); + tcs.SetResult(reader.ReadToEnd()); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }, + out var host, + out var port); - this.instanaExporter = new InstanaExporter(options, this.activityProcessor) + options.EndpointUri = new UriBuilder(Uri.UriSchemeHttp, host, port).Uri; + options.ProxyUri = options.EndpointUri; + + var spans = new List(); + var processor = new TestActivityProcessor((activity, span) => { - Helper = this.instanaExporterHelper, - }; + Assert.NotNull(activity); + spans.Add(span); + }); - var activity = new Activity("testOperationName"); + 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.data["service"]); + Assert.Equal("testOperationName", instanaSpan.Data.data["operation"]); + Assert.Equal("TraceStateString", instanaSpan.Data.data["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() { + // Arrange + var startedUtc = DateTimeOffset.FromUnixTimeMilliseconds(1776522506123); + var utcNow = DateTimeOffset.FromUnixTimeMilliseconds(1776523555069); + var options = new InstanaExporterOptions() { + AgentKey = Guid.NewGuid().ToString(), + GetParentProviderResource = (_) => + { + return new Resource( + [ + new("service.name", "serviceName"), + new("service.instance.id", "serviceInstanceId"), + new("host.id", "hostId"), + ]); + }, + UtcNow = () => utcNow, }; - 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"); + var tcs = new TaskCompletionSource(); + + 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; - this.spanSender.OnEnqueue = span => this.CloneSpan(span); + var spans = new List(); - this.instanaExporter = new InstanaExporter(options, this.activityProcessor) + var processor = new TestActivityProcessor((activity, span) => { - Helper = this.instanaExporterHelper, - }; + Assert.NotNull(activity); + spans.Add(span); + }); + + using var exporter = new InstanaExporter(options, processor); - var activity = new Activity("testOperationName"); + 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.data["service"]); + Assert.Equal("testOperationName", instanaSpan.Data.data["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_ExporterIsShutDown() { + // Arrange var options = new InstanaExporterOptions() { + AgentKey = Guid.NewGuid().ToString(), + EndpointUri = new Uri("http://localhost:42699"), }; - this.instanaExporter = new InstanaExporter(options, this.activityProcessor) + var spans = new List(); + var processor = new TestActivityProcessor((activity, span) => { - Helper = this.instanaExporterHelper, - }; + Assert.NotNull(activity); + spans.Add(span); + }); + + using var exporter = new InstanaExporter(options, processor); - var activity = new Activity("testOperationName"); + 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(); + + 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 bool CloneSpan(InstanaSpan span) + private sealed class TestActivityProcessor : ActivityProcessorBase { - this.instanaSpan = span; - return true; + 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 sealed class TestHttpMessageHandler(TaskCompletionSource completionSource) : HttpMessageHandler + { + 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); + } } } 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 159a75210c..7c6b4142aa 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,9 @@ $(TargetFrameworks);$(NetFrameworkMinimumSupportedVersion) - - + + + @@ -16,6 +17,7 @@ + diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/TestActivityProcessor.cs b/test/OpenTelemetry.Exporter.Instana.Tests/TestActivityProcessor.cs deleted file mode 100644 index 083abfa826..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 sealed class TestActivityProcessor : IActivityProcessor -{ - public IActivityProcessor? NextProcessor { get; set; } - - public void Process(Activity activity, InstanaSpan instanaSpan) - { - // No-op - } -} 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 69c372e73a..0000000000 --- a/test/OpenTelemetry.Exporter.Instana.Tests/TestSpanSender.cs +++ /dev/null @@ -1,17 +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 bool Enqueue(InstanaSpan instanaSpan) - { - this.OnEnqueue?.Invoke(instanaSpan); - return true; - } -} From 30a9d21de897dd7bc976f43fb9319f8e9992d1f1 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 16:42:43 +0100 Subject: [PATCH 03/18] [Instana] Fix StyleCop warnings Fix most StyleCop warnings and remove unused constants. --- .../Implementation/InstanaSpan.cs | 8 ++-- .../Implementation/InstanaSpanFactory.cs | 2 +- .../Implementation/InstanaSpanSerializer.cs | 14 +++--- .../Processors/ActivityProcessorBase.cs | 2 +- .../Processors/DefaultActivityProcessor.cs | 14 +++--- .../Processors/ErrorActivityProcessor.cs | 6 +-- .../Processors/EventsActivityProcessor.cs | 2 +- .../InstanaExporter.cs | 10 ++-- .../InstanaExporterConstants.cs | 46 +++++++++---------- .../TracerProviderBuilderExtensions.cs | 6 +-- .../InstanaExporterTests.cs | 10 ++-- .../InstanaSpanFactoryTests.cs | 2 +- .../InstanaSpanSerializerTests.cs | 6 +-- .../DefaultActivityProcessorTests.cs | 4 +- .../Processors/ErrorActivityProcessorTests.cs | 6 +-- 15 files changed, 66 insertions(+), 72 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpan.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpan.cs index 1e1e99deb0..12cf31565a 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpan.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpan.cs @@ -29,7 +29,7 @@ public InstanaSpan() this.K = SpanKind.NOT_SET; this.Data = new Data { - data = new Dictionary(8), + Values = new Dictionary(8), Events = new(8), Tags = new Dictionary(2), }; @@ -186,12 +186,11 @@ internal sealed class Data public Data() { this.Events = new(8); - this.data = new(8); + this.Values = new(8); this.Tags = new(2); } -#pragma warning disable SA1300 // Element should begin with upper-case letter - public Dictionary data + public Dictionary Values { get => field; set @@ -200,7 +199,6 @@ public Dictionary data field = value; } } -#pragma warning restore SA1300 // Element should begin with upper-case letter public Dictionary Tags { diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanFactory.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanFactory.cs index ad8ea8f596..d299604f4e 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanFactory.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanFactory.cs @@ -9,7 +9,7 @@ internal static class InstanaSpanFactory { Data = new Data() { - data = [], + Values = [], Tags = [], Events = new(8), }, diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs index f11a9ffa3a..41528ca9cd 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs @@ -158,12 +158,12 @@ private static async Task AppendObjectAsync(Func 0) @@ -207,7 +207,7 @@ private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWrit await writer.WriteAsync(Comma).ConfigureAwait(false); // serialize tags - await AppendObjectAsync(SerializeEventsAsync, InstanaExporterConstants.EVENTS_FIELD, instanaSpan, writer).ConfigureAwait(false); + await AppendObjectAsync(SerializeEventsAsync, InstanaExporterConstants.EventsField, instanaSpan, writer).ConfigureAwait(false); } await writer.WriteAsync(CloseCurlyBrace).ConfigureAwait(false); @@ -238,11 +238,11 @@ private static async Task SerializeEventsAsync(InstanaSpan instanaSpan, StreamWr await writer.WriteAsync(OpenCurlyBrace).ConfigureAwait(false); await writer.WriteAsync(Quote).ConfigureAwait(false); - await writer.WriteAsync(InstanaExporterConstants.EVENT_NAME_FIELD).ConfigureAwait(false); + await writer.WriteAsync(InstanaExporterConstants.EventNameField).ConfigureAwait(false); await writer.WriteAsync(QuoteColonQuote).ConfigureAwait(false); await writer.WriteAsync(enumerator.Current.Name).ConfigureAwait(false); await writer.WriteAsync(QuoteCommaQuote).ConfigureAwait(false); - await writer.WriteAsync(InstanaExporterConstants.EVENT_TIMESTAMP_FIELD).ConfigureAwait(false); + await writer.WriteAsync(InstanaExporterConstants.EventTimestampField).ConfigureAwait(false); await writer.WriteAsync(QuoteColonQuote).ConfigureAwait(false); await writer.WriteAsync(DateToUnixMillis(enumerator.Current.Ts).ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); await writer.WriteAsync(Quote).ConfigureAwait(false); @@ -251,7 +251,7 @@ private static async Task SerializeEventsAsync(InstanaSpan instanaSpan, StreamWr { await writer.WriteAsync(Comma).ConfigureAwait(false); await writer.WriteAsync(Quote).ConfigureAwait(false); - await writer.WriteAsync(InstanaExporterConstants.TAGS_FIELD).ConfigureAwait(false); + await writer.WriteAsync(InstanaExporterConstants.TagsField).ConfigureAwait(false); await writer.WriteAsync(Quote).ConfigureAwait(false); await writer.WriteAsync(Colon).ConfigureAwait(false); await SerializeTagsLogicAsync(enumerator.Current.Tags, writer).ConfigureAwait(false); diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ActivityProcessorBase.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ActivityProcessorBase.cs index 540df9fb04..ddb70903d9 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ActivityProcessorBase.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ActivityProcessorBase.cs @@ -18,7 +18,7 @@ protected virtual void PreProcess(Activity activity, InstanaSpan instanaSpan) instanaSpan.Data ??= new Data() { - data = [], + 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 f954795c1a..7edb5692e7 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/DefaultActivityProcessor.cs @@ -11,7 +11,7 @@ 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) @@ -90,26 +90,26 @@ 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; diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs index a164b97029..202cbc516b 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/ErrorActivityProcessor.cs @@ -14,12 +14,12 @@ public override void Process(Activity activity, InstanaSpan 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; } } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs index c3a99991d7..bc4dcb26a2 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Processors/EventsActivityProcessor.cs @@ -13,7 +13,7 @@ public override void Process(Activity activity, InstanaSpan 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; } diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs index fdaef5c827..527cae3cef 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs @@ -152,16 +152,16 @@ private InstanaSpan ParseActivity(Activity activity, string? serviceName = null, 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; } - 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/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs index 4b3b71b07d..dd49690978 100644 --- a/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs @@ -53,18 +53,18 @@ public static TracerProviderBuilder AddInstanaExporter(this TracerProviderBuilde private static void ConfigureFromEnvironment(InstanaExporterOptions options) { if (options.EndpointUri is null && - Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_ENDPOINT_URL) is { Length: > 0 } endpointUrl) + Environment.GetEnvironmentVariable(InstanaExporterConstants.InstanaEndpointUrl) is { Length: > 0 } endpointUrl) { options.EndpointUri = new Uri(endpointUrl, UriKind.Absolute); } if (string.IsNullOrEmpty(options.AgentKey) && - Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_AGENT_KEY) is { Length: > 0 } agentKey) + Environment.GetEnvironmentVariable(InstanaExporterConstants.InstanaAgentKey) is { Length: > 0 } agentKey) { options.AgentKey = agentKey; } - if (Environment.GetEnvironmentVariable(InstanaExporterConstants.ENVVAR_INSTANA_TIMEOUT) is { Length: > 0 } timeout && + if (Environment.GetEnvironmentVariable(InstanaExporterConstants.InstanaTimeout) is { Length: > 0 } timeout && int.TryParse(timeout, NumberStyles.Integer, CultureInfo.InvariantCulture, out var timeoutMilliseconds)) { options.BatchExportProcessorOptions.ExporterTimeoutMilliseconds = timeoutMilliseconds; diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs index 98de053a50..f34c2789df 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs @@ -124,9 +124,9 @@ public async Task Export() var instanaSpan = Assert.Single(spans); Assert.NotNull(instanaSpan); - Assert.Equal("serviceName", instanaSpan.Data.data["service"]); - Assert.Equal("testOperationName", instanaSpan.Data.data["operation"]); - Assert.Equal("TraceStateString", instanaSpan.Data.data["trace_state"]); + 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(); @@ -251,8 +251,8 @@ public async Task Export_ProcessPidDoesNotExistButServiceIdExists() var instanaSpan = Assert.Single(spans); Assert.NotNull(instanaSpan); - Assert.Equal("serviceName", instanaSpan.Data.data["service"]); - Assert.Equal("testOperationName", instanaSpan.Data.data["operation"]); + Assert.Equal("serviceName", instanaSpan.Data.Values["service"]); + Assert.Equal("testOperationName", instanaSpan.Data.Values["operation"]); #if NETFRAMEWORK using var process = Process.GetCurrentProcess(); diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs index d93c7aa6bd..7322329066 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanFactoryTests.cs @@ -16,7 +16,7 @@ public static void CreateSpan() Assert.NotNull(actual); Assert.NotNull(actual.TransformInfo); Assert.NotNull(actual.Data); - Assert.Empty(actual.Data.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..194af6f35d 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs @@ -24,7 +24,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() @@ -77,8 +77,8 @@ public static async Task SerializeToStreamWriterAsync() 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.Equal(instanaOtelSpan.Data.Values["data1Key"], span.Data.data["data1Key"]); + Assert.Equal(instanaOtelSpan.Data.Values["data2Key"], span.Data.data["data2Key"]); 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/Processors/DefaultActivityProcessorTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs index e9aeca29b8..85f7114a34 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs @@ -32,7 +32,7 @@ public void Process_PopulatesInstanaSpan() 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 => 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 d17475e0c9..03cdc806ee 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/ErrorActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/ErrorActivityProcessorTests.cs @@ -25,9 +25,9 @@ public void Process_ErrorStatusCodeIsSet() // 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] From df83588eb9e7644674552236866a30862cfd0dbc Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 17:09:44 +0100 Subject: [PATCH 04/18] [Instana] Extend tests Add tests for TracerProviderBuilder extensions. --- .../TracerProviderBuilderExtensionsTests.cs | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs new file mode 100644 index 0000000000..e0c4d9f528 --- /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(); + + 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(); + + 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(); + + 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()); + } +} From d87ebc5f4c1084fdc0dc7af382818641013dcbc3 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 17:14:49 +0100 Subject: [PATCH 05/18] [Instana] Avoid redundant work - Write literal `true` string instead of computing it. - Fix-up comments. - Add TODO to remove manual JSON serialization. --- .../Implementation/InstanaSpanSerializer.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs index 41528ca9cd..ec770efcc2 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs @@ -6,6 +6,8 @@ namespace OpenTelemetry.Exporter.Instana.Implementation; +// TODO Use a proper JSON serializer that encodes strings safely. + internal static class InstanaSpanSerializer { private const string Comma = ","; @@ -50,9 +52,7 @@ internal static async Task SerializeToStreamWriterAsync(InstanaSpan instanaSpan, 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 AppendProperty("true", "tp", writer).ConfigureAwait(false); await writer.WriteAsync(Comma).ConfigureAwait(false); } @@ -188,9 +188,8 @@ private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWrit } 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. } } @@ -198,7 +197,7 @@ private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWrit { await writer.WriteAsync(Comma).ConfigureAwait(false); - // serialize tags + // Serialize tags await AppendObjectAsync(SerializeTagsAsync, InstanaExporterConstants.TagsField, instanaSpan, writer).ConfigureAwait(false); } @@ -206,7 +205,7 @@ private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWrit { await writer.WriteAsync(Comma).ConfigureAwait(false); - // serialize tags + // Serialize events await AppendObjectAsync(SerializeEventsAsync, InstanaExporterConstants.EventsField, instanaSpan, writer).ConfigureAwait(false); } @@ -264,9 +263,8 @@ private static async Task SerializeEventsAsync(InstanaSpan instanaSpan, StreamWr } 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. } } } From b7830feb69d29cf3f1d809465a36e5476568a06e Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 17:31:15 +0100 Subject: [PATCH 06/18] [Instana] Remove Newtonsoft.Json Use System.Text.Json in tests instead. --- .../InstanaSpanSerializerTests.cs | 25 ++- .../InstanaSpanTest.cs | 182 +++++------------- ...penTelemetry.Exporter.Instana.Tests.csproj | 1 - 3 files changed, 61 insertions(+), 147 deletions(-) diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs index 194af6f35d..5363ef925c 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,6 +9,11 @@ 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() { @@ -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(); + 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.Values["data1Key"], span.Data.data["data1Key"]); - Assert.Equal(instanaOtelSpan.Data.Values["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 7c6b4142aa..74623f3f0f 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/OpenTelemetry.Exporter.Instana.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Instana.Tests/OpenTelemetry.Exporter.Instana.Tests.csproj @@ -6,7 +6,6 @@ - From 17f3a3f66932ed5dd39ffd70519be3eda7687121 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 17:43:43 +0100 Subject: [PATCH 07/18] [Instana] Update CHANGELOG Add CHANGELOG entry for TFM additions. --- src/OpenTelemetry.Exporter.Instana/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md index a31cf80884..be9c4156e4 100644 --- a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md @@ -5,6 +5,9 @@ * Updated OpenTelemetry core component version(s) to `1.15.2`. ([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080)) +* Add `net8.0` and `net10.0` target frameworks. + ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + * Add support for configuring the Instana exporter using `InstanaExporterOptions`. ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) From 852bb2950b6304249abadd1489a283e291d0c5bf Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 17:58:01 +0100 Subject: [PATCH 08/18] [Instana] Fix build - Suppress `CA5399`. - Update CHANGELOG with PR number. - Tweak some XML documentation. --- src/OpenTelemetry.Exporter.Instana/CHANGELOG.md | 6 +++--- .../Implementation/Transport.cs | 2 ++ .../InstanaExporterOptions.cs | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md index be9c4156e4..6a1c816c14 100644 --- a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md @@ -6,17 +6,17 @@ ([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080)) * Add `net8.0` and `net10.0` target frameworks. - ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ([#4153](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4153)) * Add support for configuring the Instana exporter using `InstanaExporterOptions`. - ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ([#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. - ([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO)) + ([#4153](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4153)) ## 1.0.6 diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index f05f816ec9..f8e13665e4 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -112,9 +112,11 @@ private HttpClient CreateClient() #pragma warning disable CA2000 // Dispose objects before losing scope var handler = new HttpClientHandler() { +#pragma warning disable CA5399 // .NET Framework does not support CheckCertificateRevocationList #if !NETFRAMEWORK CheckCertificateRevocationList = true, #endif +#pragma warning restore CA5399 // .NET Framework does not support CheckCertificateRevocationList }; #pragma warning restore CA2000 // Dispose objects before losing scope diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs index ae45f75fa5..8c9bddf1e2 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs @@ -16,7 +16,7 @@ namespace OpenTelemetry.Exporter.Instana; public class InstanaExporterOptions { /// - /// Gets or sets the key used to authenticate with the Instana agent. + /// Gets or sets the key used to authenticate with the Instana endpoint. /// public string AgentKey { get; set; } = string.Empty; @@ -31,8 +31,8 @@ public class InstanaExporterOptions public Uri EndpointUri { get; set; } = default!; /// - /// Gets or sets an optional delegate to a method to configure - /// the to use to send telemetry to the Instana endpoint. + /// Gets or sets an optional delegate to a method to create an + /// to use to send telemetry to the Instana endpoint. /// public Func? HttpClientFactory { get; set; } From edd099dce5beff3b5e0c0fa36c3dcbe15407a3a8 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 18:17:29 +0100 Subject: [PATCH 09/18] [Instana] Fix build Move the suppression to the right place. --- .../Implementation/Transport.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index f8e13665e4..30d1e0d9aa 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -112,11 +112,9 @@ private HttpClient CreateClient() #pragma warning disable CA2000 // Dispose objects before losing scope var handler = new HttpClientHandler() { -#pragma warning disable CA5399 // .NET Framework does not support CheckCertificateRevocationList #if !NETFRAMEWORK CheckCertificateRevocationList = true, #endif -#pragma warning restore CA5399 // .NET Framework does not support CheckCertificateRevocationList }; #pragma warning restore CA2000 // Dispose objects before losing scope @@ -126,7 +124,9 @@ private HttpClient CreateClient() handler.UseProxy = true; } +#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 try { From dd67d47f5cf004cd8f80b72e1b90cf16955040cc Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 19:15:22 +0100 Subject: [PATCH 10/18] [Instana] Address review comments Address most Copilot code review comments. --- .../Implementation/Transport.cs | 10 +++++----- .../InstanaExporterOptions.cs | 4 ++++ src/OpenTelemetry.Exporter.Instana/README.md | 3 ++- .../TracerProviderBuilderExtensions.cs | 6 ++++++ .../Processors/DefaultActivityProcessorTests.cs | 7 ++++--- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index 30d1e0d9aa..54d16595c7 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -49,7 +49,7 @@ public async Task SendAsync(ConcurrentQueue batch, CancellationToke int maxBatchSize = this.options.BatchExportProcessorOptions.MaxExportBatchSize; int written = 0; - while (batch.TryDequeue(out var span) && sendBuffer.Position < MultiSpanBufferLimit && written <= maxBatchSize) + while (sendBuffer.Position < MultiSpanBufferLimit && written < maxBatchSize && batch.TryDequeue(out var span)) { if (written > 0) { @@ -82,15 +82,15 @@ public async Task SendAsync(ConcurrentQueue batch, CancellationToke using var content = new StreamContent(sendBuffer, (int)length); content.Headers.ContentType = MediaType; - content.Headers.Add("X-INSTANA-KEY", this.options.AgentKey); - content.Headers.Add("X-INSTANA-NOTRACE", "1"); - content.Headers.Add("X-INSTANA-TIME", this.options.UtcNow().ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture)); - using var message = new HttpRequestMessage(HttpMethod.Post, this.bundleUri) { Content = content, }; + 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)); + this.client ??= this.CreateClient(); using var response = await this.client.SendAsync(message, cancellationToken).ConfigureAwait(false); diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs index 8c9bddf1e2..2f348e3b4d 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporterOptions.cs @@ -34,6 +34,10 @@ public class InstanaExporterOptions /// 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; } /// diff --git a/src/OpenTelemetry.Exporter.Instana/README.md b/src/OpenTelemetry.Exporter.Instana/README.md index 6d71d50e3a..11f2eada73 100644 --- a/src/OpenTelemetry.Exporter.Instana/README.md +++ b/src/OpenTelemetry.Exporter.Instana/README.md @@ -40,7 +40,8 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() ``` Optionally backend communication timeout can be configured using the environment -variable `INSTANA_TIMEOUT` or the `InstanaExporterOptions.Timeout` property. +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 dd49690978..7ca01c7fc9 100644 --- a/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs @@ -69,5 +69,11 @@ private static void ConfigureFromEnvironment(InstanaExporterOptions options) { 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/Processors/DefaultActivityProcessorTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs index 85f7114a34..ecbf0224ec 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/DefaultActivityProcessorTests.cs @@ -15,11 +15,12 @@ public void Process_PopulatesInstanaSpan() { // Arrange var activity = new Activity("testOperationName"); - activity.Start(); - Thread.Sleep(200); // Simulate some work being done + var start = DateTime.UtcNow; + + activity.SetStartTime(start); + activity.SetEndTime(start.AddSeconds(1)); - activity.Stop(); var instanaSpan = new InstanaSpan(); // Act From 40f2e2c0dc578ec27022420834f0ab95252c65d2 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 19:18:50 +0100 Subject: [PATCH 11/18] [Instana] Address feedback Address more Copilot feedback. --- src/OpenTelemetry.Exporter.Instana/README.md | 3 ++- .../TracerProviderBuilderExtensions.cs | 4 +++- .../Processors/TagsActivityProcessorTests.cs | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/README.md b/src/OpenTelemetry.Exporter.Instana/README.md index 11f2eada73..993ee9fc27 100644 --- a/src/OpenTelemetry.Exporter.Instana/README.md +++ b/src/OpenTelemetry.Exporter.Instana/README.md @@ -41,7 +41,8 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() Optionally backend communication timeout can be configured using the environment variable `INSTANA_TIMEOUT` or the -`InstanaExporterOptions.BatchExportProcessorOptions.ExporterTimeoutMilliseconds` property. +`InstanaExporterOptions.BatchExportProcessorOptions.ExporterTimeoutMilliseconds` +property. ## Troubleshooting diff --git a/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs index 7ca01c7fc9..0b311dde03 100644 --- a/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Instana/TracerProviderBuilderExtensions.cs @@ -46,7 +46,9 @@ public static TracerProviderBuilder AddInstanaExporter(this TracerProviderBuilde configure?.Invoke(options); - return new BatchActivityExportProcessor(new InstanaExporter(options, DefaultActivityProcessor.CreateDefault())); + return options.EndpointUri is null + ? throw new InvalidOperationException("No Instana endpoint URL provided.") + : (BaseProcessor)new BatchActivityExportProcessor(new InstanaExporter(options, DefaultActivityProcessor.CreateDefault())); }); } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/TagsActivityProcessorTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/TagsActivityProcessorTests.cs index d87725c5a7..eb747bc8ff 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/Processors/TagsActivityProcessorTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/Processors/TagsActivityProcessorTests.cs @@ -13,7 +13,7 @@ public class TagsActivityProcessorTests [Fact] public void Process_StatusTagsExist() { - // Act + // Arrange var activity = new Activity("testOperationName"); activity.AddTag("otel.status_code", "testStatusCode"); activity.AddTag("otel.status_description", "testStatusDescription"); @@ -36,7 +36,7 @@ public void Process_StatusTagsExist() [Fact] public void Process_StatusTagsDoNotExist() { - // Act + // Arrange var activity = new Activity("testOperationName"); activity.AddTag("otel.testTag", "testTag"); From d212cb0512978fed50075c49879213fb29f09fd3 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 18 Apr 2026 19:37:07 +0100 Subject: [PATCH 12/18] [Instana] Address feedback Address final piece of Copilot review feedback. --- .../Implementation/SpanSender.cs | 48 ++++++++++++++----- .../Implementation/Transport.cs | 10 +++- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs index 50bd45ad06..c123cac759 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs @@ -13,6 +13,8 @@ internal sealed class SpanSender : IDisposable private readonly ConcurrentQueue queue; private readonly Transport transport; + private int queueSize; + public SpanSender(InstanaExporterOptions options) { this.options = options.BatchExportProcessorOptions; @@ -38,13 +40,21 @@ public void Dispose() public bool Enqueue(InstanaSpan instanaSpan) { - if (this.queue.Count < this.options.MaxQueueSize) + if (!this.TryReserveQueueSlot(this.cancellationTokenSource.Token)) + { + return false; + } + + try { this.queue.Enqueue(instanaSpan); return true; } - - return false; + catch + { + this.ReleaseQueueSlot(); + throw; + } } private async Task ProcessingLoop() @@ -57,14 +67,8 @@ private async Task ProcessingLoop() { if (!this.queue.IsEmpty) { - try - { - await this.transport.SendAsync(this.queue, this.cancellationTokenSource.Token).ConfigureAwait(false); - } - catch (Exception ex) - { - InstanaExporterEventSource.Log.FailedExport(ex); - } + int consumed = await this.transport.SendAsync(this.queue, this.cancellationTokenSource.Token).ConfigureAwait(false); + Interlocked.Add(ref this.queueSize, -consumed); } await Task.Delay(delay, this.cancellationTokenSource.Token).ConfigureAwait(false); @@ -75,4 +79,26 @@ private async Task ProcessingLoop() // Processing cancelled } } + + private void ReleaseQueueSlot() => Interlocked.Decrement(ref this.queueSize); + + private bool TryReserveQueueSlot(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var queueSize = Volatile.Read(ref this.queueSize); + + if (queueSize >= this.options.MaxQueueSize) + { + return false; + } + + if (Interlocked.CompareExchange(ref this.queueSize, queueSize + 1, queueSize) == queueSize) + { + return true; + } + } + + return false; + } } diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index 54d16595c7..15308aa5cd 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -36,8 +36,9 @@ public void Dispose() GC.SuppressFinalize(this); } - public async Task SendAsync(ConcurrentQueue batch, CancellationToken cancellationToken) + public async Task SendAsync(ConcurrentQueue batch, CancellationToken cancellationToken) { + int written = 0; var buffer = ArrayPool.Shared.Rent(MultiSpanBufferSize); try @@ -47,7 +48,6 @@ public async Task SendAsync(ConcurrentQueue batch, CancellationToke await writer.WriteAsync("{\"spans\":[").ConfigureAwait(false); int maxBatchSize = this.options.BatchExportProcessorOptions.MaxExportBatchSize; - int written = 0; while (sendBuffer.Position < MultiSpanBufferLimit && written < maxBatchSize && batch.TryDequeue(out var span)) { @@ -96,10 +96,16 @@ public async Task SendAsync(ConcurrentQueue batch, CancellationToke using var response = await this.client.SendAsync(message, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } + catch (Exception ex) + { + InstanaExporterEventSource.Log.FailedExport(ex); + } finally { ArrayPool.Shared.Return(buffer); } + + return written; } private HttpClient CreateClient() From 91977f6c2f15b5a76cf2a295b3c610d974bfbfcc Mon Sep 17 00:00:00 2001 From: martincostello Date: Sun, 19 Apr 2026 11:05:27 +0100 Subject: [PATCH 13/18] [Instana] Remove extra processing loop The batch exporter already runs a background task to export, so use that rather than have another one just to use async. --- .../Implementation/InstanaSpanSerializer.cs | 182 +++++++++--------- .../Implementation/SpanSender.cs | 104 ---------- .../Implementation/Transport.cs | 45 ++--- .../InstanaExporter.cs | 14 +- .../InstanaExporterTests.cs | 21 +- .../InstanaSpanSerializerTests.cs | 6 +- .../TracerProviderBuilderExtensionsTests.cs | 6 +- test/Shared/TestHttpServer.cs | 48 ++++- 8 files changed, 184 insertions(+), 242 deletions(-) delete mode 100644 src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs index ec770efcc2..c7d5a2494e 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs @@ -30,67 +30,67 @@ internal static class InstanaSpanSerializer internal static IEnumerator? GetSpanEventsEnumerator(InstanaSpan instanaSpan) => instanaSpan.Data.Events.GetEnumerator(); - internal static async Task SerializeToStreamWriterAsync(InstanaSpan instanaSpan, StreamWriter writer) + internal static void SerializeToStreamWriter(InstanaSpan instanaSpan, StreamWriter writer) { - await writer.WriteAsync(OpenCurlyBrace).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) { - await AppendProperty("true", "tp", writer).ConfigureAwait(false); - 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(CloseCurlyBrace).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) => (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(OpenCurlyBrace).ConfigureAwait(false); + writer.Write(OpenCurlyBrace); if (tags == null) { return; @@ -105,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(QuoteColonQuote).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) @@ -127,37 +127,37 @@ private static async Task SerializeTagsLogicAsync(Dictionary? ta } } - await writer.WriteAsync(CloseCurlyBrace).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(QuoteColonQuote).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(QuoteColon).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(QuoteColon).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(OpenCurlyBrace).ConfigureAwait(false); + writer.Write(OpenCurlyBrace); if (instanaSpan.Data.Values == null) { return; @@ -172,18 +172,18 @@ 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(QuoteColonQuote).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) @@ -195,24 +195,24 @@ private static async Task SerializeDataAsync(InstanaSpan instanaSpan, StreamWrit if (instanaSpan.Data.Tags.Count > 0) { - await writer.WriteAsync(Comma).ConfigureAwait(false); + writer.Write(Comma); // Serialize tags - await AppendObjectAsync(SerializeTagsAsync, InstanaExporterConstants.TagsField, instanaSpan, writer).ConfigureAwait(false); + AppendObject(SerializeTags, InstanaExporterConstants.TagsField, instanaSpan, writer); } if (instanaSpan.Data.Events.Count > 0) { - await writer.WriteAsync(Comma).ConfigureAwait(false); + writer.Write(Comma); // Serialize events - await AppendObjectAsync(SerializeEventsAsync, InstanaExporterConstants.EventsField, instanaSpan, writer).ConfigureAwait(false); + AppendObject(SerializeEvents, InstanaExporterConstants.EventsField, instanaSpan, writer); } - await writer.WriteAsync(CloseCurlyBrace).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) { @@ -223,43 +223,43 @@ 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(OpenCurlyBrace).ConfigureAwait(false); - await writer.WriteAsync(Quote).ConfigureAwait(false); - await writer.WriteAsync(InstanaExporterConstants.EventNameField).ConfigureAwait(false); - await writer.WriteAsync(QuoteColonQuote).ConfigureAwait(false); - await writer.WriteAsync(enumerator.Current.Name).ConfigureAwait(false); - await writer.WriteAsync(QuoteCommaQuote).ConfigureAwait(false); - await writer.WriteAsync(InstanaExporterConstants.EventTimestampField).ConfigureAwait(false); - await writer.WriteAsync(QuoteColonQuote).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.TagsField).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(CloseCurlyBrace).ConfigureAwait(false); + writer.Write(CloseCurlyBrace); } - await writer.WriteAsync("]").ConfigureAwait(false); + writer.Write("]"); } catch (InvalidOperationException) { diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs deleted file mode 100644 index c123cac759..0000000000 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/SpanSender.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Collections.Concurrent; -using System.Diagnostics; - -namespace OpenTelemetry.Exporter.Instana.Implementation; - -internal sealed class SpanSender : IDisposable -{ - private readonly CancellationTokenSource cancellationTokenSource; - private readonly BatchExportProcessorOptions options; - private readonly ConcurrentQueue queue; - private readonly Transport transport; - - private int queueSize; - - public SpanSender(InstanaExporterOptions options) - { - this.options = options.BatchExportProcessorOptions; - this.queue = new(); - this.cancellationTokenSource = new(); - this.transport = new Transport(options); - - Task.Factory.StartNew(this.ProcessingLoop, this.cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); - } - - public void Dispose() - { - if (this.cancellationTokenSource != null) - { - this.cancellationTokenSource.Cancel(); - this.cancellationTokenSource.Dispose(); - } - - this.transport?.Dispose(); - - GC.SuppressFinalize(this); - } - - public bool Enqueue(InstanaSpan instanaSpan) - { - if (!this.TryReserveQueueSlot(this.cancellationTokenSource.Token)) - { - return false; - } - - try - { - this.queue.Enqueue(instanaSpan); - return true; - } - catch - { - this.ReleaseQueueSlot(); - throw; - } - } - - private async Task ProcessingLoop() - { - var delay = this.options.ScheduledDelayMilliseconds; - - try - { - while (!this.cancellationTokenSource.IsCancellationRequested) - { - if (!this.queue.IsEmpty) - { - int consumed = await this.transport.SendAsync(this.queue, this.cancellationTokenSource.Token).ConfigureAwait(false); - Interlocked.Add(ref this.queueSize, -consumed); - } - - await Task.Delay(delay, this.cancellationTokenSource.Token).ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - // Processing cancelled - } - } - - private void ReleaseQueueSlot() => Interlocked.Decrement(ref this.queueSize); - - private bool TryReserveQueueSlot(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - var queueSize = Volatile.Read(ref this.queueSize); - - if (queueSize >= this.options.MaxQueueSize) - { - return false; - } - - if (Interlocked.CompareExchange(ref this.queueSize, queueSize + 1, queueSize) == queueSize) - { - return true; - } - } - - return false; - } -} diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index 15308aa5cd..39172db7ff 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using System.Buffers; -using System.Collections.Concurrent; using System.Globalization; using System.Net; #if NETFRAMEWORK @@ -36,44 +35,38 @@ public void Dispose() GC.SuppressFinalize(this); } - public async Task SendAsync(ConcurrentQueue batch, CancellationToken cancellationToken) + public bool Send(List batch) { - int written = 0; var buffer = ArrayPool.Shared.Rent(MultiSpanBufferSize); try { using var sendBuffer = new MemoryStream(buffer); using var writer = new StreamWriter(sendBuffer); - await writer.WriteAsync("{\"spans\":[").ConfigureAwait(false); + writer.Write("{\"spans\":["); int maxBatchSize = this.options.BatchExportProcessorOptions.MaxExportBatchSize; - while (sendBuffer.Position < MultiSpanBufferLimit && written < maxBatchSize && batch.TryDequeue(out var span)) + using var enumerator = batch.GetEnumerator(); + + int written = 0; + + while (sendBuffer.Position < MultiSpanBufferLimit && written < maxBatchSize && enumerator.MoveNext()) { if (written > 0) { - await writer.WriteAsync(',').ConfigureAwait(false); + writer.Write(','); } - await InstanaSpanSerializer.SerializeToStreamWriterAsync(span, writer).ConfigureAwait(false); + InstanaSpanSerializer.SerializeToStreamWriter(enumerator.Current, writer); -#if NET - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); -#else - await writer.FlushAsync().ConfigureAwait(false); -#endif + writer.Flush(); written++; } - await writer.WriteAsync("]}").ConfigureAwait(false); - -#if NET - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); -#else - await writer.FlushAsync().ConfigureAwait(false); -#endif + writer.Write("]}"); + writer.Flush(); var length = sendBuffer.Position; sendBuffer.Position = 0; @@ -93,19 +86,27 @@ public async Task SendAsync(ConcurrentQueue batch, Cancellatio this.client ??= this.CreateClient(); - using var response = await this.client.SendAsync(message, cancellationToken).ConfigureAwait(false); +#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 + response.EnsureSuccessStatusCode(); + + return true; } catch (Exception ex) { InstanaExporterEventSource.Log.FailedExport(ex); + return false; } finally { ArrayPool.Shared.Return(buffer); } - - return written; } private HttpClient CreateClient() diff --git a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs index 527cae3cef..aef8fb3b68 100644 --- a/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs +++ b/src/OpenTelemetry.Exporter.Instana/InstanaExporter.cs @@ -13,7 +13,7 @@ internal sealed class InstanaExporter : BaseExporter { private readonly IActivityProcessor activityProcessor; private readonly InstanaExporterOptions options; - private readonly SpanSender sender; + private readonly Transport transport; private readonly string? processId; private int wasShutdown; @@ -21,8 +21,8 @@ internal sealed class InstanaExporter : BaseExporter public InstanaExporter(InstanaExporterOptions options, IActivityProcessor activityProcessor) { this.options = options; - this.sender = new SpanSender(this.options); this.activityProcessor = activityProcessor; + this.transport = new(this.options); if (IsWindows()) { @@ -60,6 +60,8 @@ public override ExportResult Export(in Batch batch) var serviceName = this.ExtractServiceName(ref from); + var spans = new List((int)batch.Count); + foreach (var activity in batch) { if (activity == null) @@ -67,17 +69,15 @@ public override ExportResult Export(in Batch batch) continue; } - var span = this.ParseActivity(activity, serviceName, from); - - _ = this.sender.Enqueue(span); + spans.Add(this.ParseActivity(activity, serviceName, from)); } - return ExportResult.Success; + return this.transport.Send(spans) ? ExportResult.Success : ExportResult.Failure; } protected override void Dispose(bool disposing) { - this.sender?.Dispose(); + this.transport?.Dispose(); base.Dispose(disposing); } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs index f34c2789df..4c0ab247a9 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs @@ -42,7 +42,7 @@ public async Task Export() UtcNow = () => utcNow, }; - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var server = TestHttpServer.RunServer( (context) => @@ -171,7 +171,7 @@ public async Task Export_ProcessPidDoesNotExistButServiceIdExists() UtcNow = () => utcNow, }; - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var server = TestHttpServer.RunServer( (context) => @@ -313,7 +313,7 @@ public void Export_ExporterIsShutDown() public async Task Export_WithCustomHttpClient() { // Arrange - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var handler = new TestHttpMessageHandler(tcs); using var httpClient = new HttpClient(handler); @@ -404,5 +404,20 @@ protected override async Task SendAsync(HttpRequestMessage 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/InstanaSpanSerializerTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs index 5363ef925c..e0a1f1d5e3 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs @@ -15,7 +15,7 @@ public static class InstanaSpanSerializerTests }; [Fact] - public static async Task SerializeToStreamWriterAsync() + public static void SerializeToStreamWriterAsync() { var instanaOtelSpan = InstanaSpanFactory.CreateSpan(); instanaOtelSpan.F = new Implementation.From { E = "12345", H = "localhost" }; @@ -51,8 +51,8 @@ public static async Task SerializeToStreamWriterAsync() 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; diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs index e0c4d9f528..b3e298941d 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/TracerProviderBuilderExtensionsTests.cs @@ -22,7 +22,7 @@ public async Task AddInstanaExporter_WithEnvironmentVariables_Minimal() { // Arrange var agentKey = Guid.NewGuid().ToString(); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var server = TestHttpServer.RunServer( (context) => AssertResponse(context, agentKey, tcs), @@ -49,7 +49,7 @@ public async Task AddInstanaExporter_WithEnvironmentVariables_All() { // Arrange var agentKey = Guid.NewGuid().ToString(); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var server = TestHttpServer.RunServer( (context) => AssertResponse(context, agentKey, tcs), @@ -78,7 +78,7 @@ public async Task AddInstanaExporter_WithOptions() { // Arrange var agentKey = Guid.NewGuid().ToString(); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var server = TestHttpServer.RunServer( (context) => AssertResponse(context, agentKey, tcs), diff --git a/test/Shared/TestHttpServer.cs b/test/Shared/TestHttpServer.cs index 12f43c6791..9843669dc7 100644 --- a/test/Shared/TestHttpServer.cs +++ b/test/Shared/TestHttpServer.cs @@ -58,6 +58,7 @@ private sealed class RunningServer : IDisposable private readonly Task httpListenerTask; private readonly HttpListener listener; private readonly TaskCompletionSource initialized = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationTokenSource cancellationTokenSource = new(); public RunningServer(Action action, string host, int port) { @@ -66,18 +67,21 @@ public RunningServer(Action action, string host, int port) this.listener.Prefixes.Add($"http://{host}:{port}/"); this.listener.Start(); - this.httpListenerTask = Task.Run(() => this.ListenAsync(action)); + this.httpListenerTask = Task.Factory.StartNew( + () => this.ListenAsync(action), + this.cancellationTokenSource.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); } - public void Start() - { - this.initialized.Task.GetAwaiter().GetResult(); - } + public void Start() => this.initialized.Task.GetAwaiter().GetResult(); public void Dispose() { try { + this.cancellationTokenSource.Cancel(); + this.cancellationTokenSource.Dispose(); this.listener.Close(); this.httpListenerTask.GetAwaiter().GetResult(); } @@ -87,20 +91,25 @@ public void Dispose() } } - private bool IsListenerShutdownException(Exception ex) => + private static bool IsResponseAlreadyClosedException(Exception ex) => ex is ObjectDisposedException || - (ex is HttpListenerException httpEx && (httpEx.ErrorCode is 6 or 995 or 10057)) || + (ex is HttpListenerException httpEx && (httpEx.ErrorCode is 6 or 995 or 10057)); + + private bool IsListenerShutdownException(Exception ex) => + IsResponseAlreadyClosedException(ex) || (ex is InvalidOperationException && !this.listener.IsListening); private async Task ListenAsync(Action action) { this.initialized.TrySetResult(true); - while (true) + while (!this.cancellationTokenSource.IsCancellationRequested) { + HttpListenerContext? context = null; + try { - var context = await this.listener.GetContextAsync().ConfigureAwait(false); + context = await this.listener.GetContextAsync().ConfigureAwait(false); action(context); } catch (Exception ex) @@ -114,6 +123,27 @@ private async Task ListenAsync(Action action) throw; } + finally + { + if (context is not null) + { + this.TryCloseResponse(context.Response); + } + } + + await Task.Yield(); + } + } + + private void TryCloseResponse(HttpListenerResponse response) + { + try + { + response.Close(); + } + catch (Exception ex) when (IsResponseAlreadyClosedException(ex)) + { + // The handler completed the response explicitly. } } } From 3e710bbcd8e34e5da3838c7507cd4d21136ce0c4 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Tue, 21 Apr 2026 15:18:19 +0100 Subject: [PATCH 14/18] [Instana] Update CHANGELOG Move entry to Unreleased. --- src/OpenTelemetry.Exporter.Instana/CHANGELOG.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md index 8616c1c559..9d1048a38b 100644 --- a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md @@ -2,13 +2,6 @@ ## Unreleased -## 1.0.7 - -Released 2026-Apr-21 - -* Updated OpenTelemetry core component version(s) to `1.15.3`. - ([#4166](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4166)) - * Add `net8.0` and `net10.0` target frameworks. ([#4153](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4153)) @@ -22,6 +15,13 @@ Released 2026-Apr-21 `HttpClient` for the exporter to use. ([#4153](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4153)) +## 1.0.7 + +Released 2026-Apr-21 + +* Updated OpenTelemetry core component version(s) to `1.15.3`. + ([#4166](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4166)) + ## 1.0.6 Released 2026-Jan-21 From 81621aa0d1ce94a67a2ffa268d84babfe734feec Mon Sep 17 00:00:00 2001 From: martincostello Date: Sun, 19 Apr 2026 12:49:13 +0100 Subject: [PATCH 15/18] [Instana] Use System.Text.Json Use System.Text.Json to serialize spans. --- .../Implementation/InstanaSpanSerializer.cs | 233 ++++++------------ .../Implementation/Transport.cs | 33 ++- .../OpenTelemetry.Exporter.Instana.csproj | 4 + .../InstanaExporterTests.cs | 52 ++++ .../InstanaSpanSerializerTests.cs | 15 +- 5 files changed, 149 insertions(+), 188 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs index c7d5a2494e..41e25606eb 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/InstanaSpanSerializer.cs @@ -1,24 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Collections; using System.Globalization; +using System.Text.Json; namespace OpenTelemetry.Exporter.Instana.Implementation; -// TODO Use a proper JSON serializer that encodes strings safely. - internal static class InstanaSpanSerializer { - 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; @@ -26,164 +15,112 @@ internal static class InstanaSpanSerializer 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) + internal static void Serialize(InstanaSpan instanaSpan, Utf8JsonWriter writer) { - writer.Write(OpenCurlyBrace); + writer.WriteStartObject(); + AppendProperty(instanaSpan.T, "t", writer); - writer.Write(Comma); AppendProperty(instanaSpan.S, "s", writer); - writer.Write(Comma); if (!string.IsNullOrEmpty(instanaSpan.P)) { AppendProperty(instanaSpan.P, "p", writer); - writer.Write(Comma); } if (!string.IsNullOrEmpty(instanaSpan.Lt)) { AppendProperty(instanaSpan.Lt, "lt", writer); - writer.Write(Comma); } if (instanaSpan.Tp) { AppendProperty("true", "tp", writer); - writer.Write(Comma); } if (instanaSpan.K != SpanKind.NOT_SET) { AppendProperty((((int)instanaSpan.K) + 1).ToString(CultureInfo.InvariantCulture), "k", writer); - writer.Write(Comma); } 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 void SerializeFrom(InstanaSpan instanaSpan, StreamWriter writer) - { - writer.Write("{\"e\":\""); - writer.Write(instanaSpan.F.E); - writer.Write("\"}"); + writer.WritePropertyName("ts"); + writer.WriteNumberValue(DateToUnixMillis(instanaSpan.Ts)); + + writer.WritePropertyName("d"); + writer.WriteNumberValue(instanaSpan.D / 10_000L); + + SerializeData(instanaSpan, writer); + + writer.WritePropertyName("f"); + writer.WriteStartObject(); + + writer.WritePropertyName("e"); + writer.WriteStringValue(instanaSpan.F.E); + + writer.WriteEndObject(); + + writer.WritePropertyName("ec"); + writer.WriteNumberValue(instanaSpan.Ec); + + writer.WriteEndObject(); } private static long DateToUnixMillis(long timeStamp) => (timeStamp - UnixZeroTime) / 10_000; - private static void SerializeTags(InstanaSpan instanaSpan, StreamWriter writer) => - SerializeTagsLogic(instanaSpan.Data.Tags, writer); - - private static void SerializeTagsLogic(Dictionary? tags, StreamWriter writer) + private static void SerializeTags(Dictionary? tags, Utf8JsonWriter writer) { - writer.Write(OpenCurlyBrace); - if (tags == null) + if (tags == null || tags.Count < 1) { return; } - using (var enumerator = tags.GetEnumerator()) + writer.WritePropertyName(InstanaExporterConstants.TagsField); + writer.WriteStartObject(); + + using var enumerator = tags.GetEnumerator(); + + try { - byte i = 0; - try - { - while (enumerator.MoveNext()) - { - if (i > 0) - { - writer.Write(Comma); - } - else - { - i = 1; - } - - writer.Write(Quote); - writer.Write(enumerator.Current.Key); - writer.Write(QuoteColonQuote); - writer.Write(enumerator.Current.Value); - writer.Write(Quote); - } - } - catch (InvalidOperationException) + while (enumerator.MoveNext()) { - // 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 + writer.WritePropertyName(enumerator.Current.Key); + writer.WriteStringValue(enumerator.Current.Value); } } + 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. + } - writer.Write(CloseCurlyBrace); - } - - private static void AppendProperty(string? value, string? name, StreamWriter json) - { - json.Write(Quote); - json.Write(name); - json.Write(QuoteColonQuote); - json.Write(value); - json.Write(Quote); - } - - private static void AppendProperty(long value, string name, StreamWriter json) - { - json.Write(Quote); - json.Write(name); - json.Write(QuoteColon); - json.Write(value.ToString(CultureInfo.InvariantCulture)); + writer.WriteEndObject(); } - private static void AppendObject(Action valueFunction, string name, InstanaSpan instanaSpan, StreamWriter json) + private static void AppendProperty(string? value, string name, Utf8JsonWriter json) { - json.Write(Quote); - json.Write(name); - json.Write(QuoteColon); - valueFunction(instanaSpan, json); + json.WritePropertyName(name); + json.WriteStringValue(value); } - private static void SerializeData(InstanaSpan instanaSpan, StreamWriter writer) + private static void SerializeData(InstanaSpan instanaSpan, Utf8JsonWriter writer) { - writer.Write(OpenCurlyBrace); if (instanaSpan.Data.Values == null) { return; } + writer.WritePropertyName("data"); + writer.WriteStartObject(); + using (var enumerator = instanaSpan.Data.Values.GetEnumerator()) { - byte i = 0; try { while (enumerator.MoveNext()) { - if (i > 0) - { - writer.Write(Comma); - } - else - { - i = 1; - } - - writer.Write(Quote); - writer.Write(enumerator.Current.Key); - writer.Write(QuoteColonQuote); - writer.Write(enumerator.Current.Value.ToString()); - writer.Write(Quote); + writer.WritePropertyName(enumerator.Current.Key); + writer.WriteStringValue(enumerator.Current.Value.ToString()); } } catch (InvalidOperationException) @@ -193,73 +130,41 @@ private static void SerializeData(InstanaSpan instanaSpan, StreamWriter writer) } } - if (instanaSpan.Data.Tags.Count > 0) - { - writer.Write(Comma); - - // Serialize tags - AppendObject(SerializeTags, InstanaExporterConstants.TagsField, instanaSpan, writer); - } + SerializeTags(instanaSpan.Data.Tags, writer); - if (instanaSpan.Data.Events.Count > 0) + if (instanaSpan.Data.Events is { Count: > 0 } events) { - writer.Write(Comma); + writer.WritePropertyName(InstanaExporterConstants.EventsField); + writer.WriteStartArray(); - // Serialize events - AppendObject(SerializeEvents, InstanaExporterConstants.EventsField, instanaSpan, writer); + SerializeEvents(events, writer); + + writer.WriteEndArray(); } - writer.Write(CloseCurlyBrace); + writer.WriteEndObject(); } - private static void SerializeEvents(InstanaSpan instanaSpan, StreamWriter writer) + private static void SerializeEvents(List events, Utf8JsonWriter writer) { - if (instanaSpan.Data.Events == null) - { - return; - } + using var enumerator = events.GetEnumerator(); - using var enumerator = instanaSpan.Data.Events.GetEnumerator(); - byte i = 0; try { - writer.Write("["); while (enumerator.MoveNext()) { - if (i > 0) - { - writer.Write(Comma); - } - else - { - i = 1; - } + writer.WriteStartObject(); - 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) - { - writer.Write(Comma); - writer.Write(Quote); - writer.Write(InstanaExporterConstants.TagsField); - writer.Write(Quote); - writer.Write(Colon); - SerializeTagsLogic(enumerator.Current.Tags, writer); - } + writer.WritePropertyName(InstanaExporterConstants.EventNameField); + writer.WriteStringValue(enumerator.Current.Name); - writer.Write(CloseCurlyBrace); - } + writer.WritePropertyName(InstanaExporterConstants.EventTimestampField); + writer.WriteStringValue(DateToUnixMillis(enumerator.Current.Ts).ToString(CultureInfo.InvariantCulture)); + + SerializeTags(enumerator.Current.Tags, writer); - writer.Write("]"); + writer.WriteEndObject(); + } } catch (InvalidOperationException) { diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index 39172db7ff..35959a482a 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -8,6 +8,7 @@ using System.Net.Http; #endif using System.Net.Http.Headers; +using System.Text.Json; namespace OpenTelemetry.Exporter.Instana.Implementation; @@ -41,38 +42,36 @@ public bool Send(List batch) try { - using var sendBuffer = new MemoryStream(buffer); - using var writer = new StreamWriter(sendBuffer); - writer.Write("{\"spans\":["); + using var stream = new MemoryStream(buffer); + using var writer = new Utf8JsonWriter(stream); + + writer.WriteStartObject(); + writer.WritePropertyName("spans"); + writer.WriteStartArray(); int maxBatchSize = this.options.BatchExportProcessorOptions.MaxExportBatchSize; + int written = 0; using var enumerator = batch.GetEnumerator(); - int written = 0; - - while (sendBuffer.Position < MultiSpanBufferLimit && written < maxBatchSize && enumerator.MoveNext()) + while (stream.Position < MultiSpanBufferLimit && written < maxBatchSize && enumerator.MoveNext()) { - if (written > 0) - { - writer.Write(','); - } - - InstanaSpanSerializer.SerializeToStreamWriter(enumerator.Current, writer); + InstanaSpanSerializer.Serialize(enumerator.Current, writer); writer.Flush(); written++; } - writer.Write("]}"); + writer.WriteEndArray(); + writer.WriteEndObject(); writer.Flush(); - var length = sendBuffer.Position; - sendBuffer.Position = 0; - sendBuffer.SetLength(length); + var length = stream.Position; + stream.Position = 0; + stream.SetLength(length); - using var content = new StreamContent(sendBuffer, (int)length); + using var content = new StreamContent(stream, (int)length); content.Headers.ContentType = MediaType; using var message = new HttpRequestMessage(HttpMethod.Post, this.bundleUri) diff --git a/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj b/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj index f18ce9759c..a55e72bc0a 100644 --- a/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj +++ b/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj @@ -23,6 +23,10 @@ + + + + diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs index 4c0ab247a9..a9caaa5282 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaExporterTests.cs @@ -348,6 +348,58 @@ public async Task Export_WithCustomHttpClient() Assert.Equal(1, handler.InvocationCount); } + [Fact] + public async Task Export_EncodesJsonCorrectly() + { + // 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 \"quoted\" operation"); + activity.SetStatus(ActivityStatusCode.Error, "line1\r\n\"line2\""); + activity.SetTag("http.route", "/orders/\"id\""); + activity.AddEvent( + new ActivityEvent( + "event \"name\"", + DateTimeOffset.UtcNow, + [new("detail", "line1\r\n\"line2\"")])); + + Activity[] activities = [activity]; + var batch = new Batch(activities, activities.Length); + + // Act + var result = exporter.Export(batch); + + // Assert + Assert.Equal(ExportResult.Success, result); + + var actual = await WaitForExportAsync(tcs); + + using var document = JsonDocument.Parse(actual); + var exportedSpan = document.RootElement.GetProperty("spans").EnumerateArray().Single(); + var data = exportedSpan.GetProperty("data"); + var exportedEvent = data.GetProperty("events").EnumerateArray().Single(); + + Assert.Equal("my \"quoted\" operation", data.GetProperty("operation").GetString()); + Assert.Equal("line1\r\n\"line2\"", data.GetProperty("error_detail").GetString()); + Assert.Equal("/orders/\"id\"", data.GetProperty("tags").GetProperty("http.route").GetString()); + Assert.Equal("event \"name\"", exportedEvent.GetProperty("name").GetString()); + Assert.Equal("line1\r\n\"line2\"", exportedEvent.GetProperty("tags").GetProperty("detail").GetString()); + } + private static async Task WaitForExportAsync(TaskCompletionSource completionSource) { var timeout = ExportTimeout; diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs index e0a1f1d5e3..5694c88d47 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs @@ -48,17 +48,18 @@ public static void SerializeToStreamWriterAsync() ]; InstanaSpanTest? span; - using (var sendBuffer = new MemoryStream()) + using (var stream = new MemoryStream()) { - using var writer = new StreamWriter(sendBuffer); - InstanaSpanSerializer.SerializeToStreamWriter(instanaOtelSpan, writer); + using var writer = new Utf8JsonWriter(stream); + + InstanaSpanSerializer.Serialize(instanaOtelSpan, writer); writer.Flush(); - var length = sendBuffer.Position; - sendBuffer.Position = 0; - sendBuffer.SetLength(length); + var length = stream.Position; + stream.Position = 0; + stream.SetLength(length); - span = JsonSerializer.Deserialize(sendBuffer, SerializerOptions); + span = JsonSerializer.Deserialize(stream, SerializerOptions); } Assert.NotNull(span); From c7a3ffafc913fc0edab80c83e42004f1cdf3d1a4 Mon Sep 17 00:00:00 2001 From: martincostello Date: Mon, 27 Apr 2026 11:39:06 +0100 Subject: [PATCH 16/18] [Instana] Update CHANGELOG Add changelog entry for changes. --- src/OpenTelemetry.Exporter.Instana/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md index 9d1048a38b..edbbf3259b 100644 --- a/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Instana/CHANGELOG.md @@ -15,6 +15,9 @@ `HttpClient` for the exporter to use. ([#4153](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4153)) +* Use System.Text.Json for JSON serialization. + ([#4293](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4293)) + ## 1.0.7 Released 2026-Apr-21 From 6ac00ab2abb6a226fe9290421a3fb47e8081f123 Mon Sep 17 00:00:00 2001 From: martincostello Date: Mon, 27 Apr 2026 11:54:01 +0100 Subject: [PATCH 17/18] [Instana] Update System.Text.Json - Match version used elswhere in the repo. - Fix some formatting. --- .../OpenTelemetry.Exporter.Instana.csproj | 4 ++-- .../OpenTelemetry.Resources.Azure.csproj | 4 ++-- .../OpenTelemetry.Resources.Gcp.csproj | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj b/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj index a55e72bc0a..4617c2e264 100644 --- a/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj +++ b/src/OpenTelemetry.Exporter.Instana/OpenTelemetry.Exporter.Instana.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/src/OpenTelemetry.Resources.Azure/OpenTelemetry.Resources.Azure.csproj b/src/OpenTelemetry.Resources.Azure/OpenTelemetry.Resources.Azure.csproj index 8b45309066..a2ee6c923c 100644 --- a/src/OpenTelemetry.Resources.Azure/OpenTelemetry.Resources.Azure.csproj +++ b/src/OpenTelemetry.Resources.Azure/OpenTelemetry.Resources.Azure.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/src/OpenTelemetry.Resources.Gcp/OpenTelemetry.Resources.Gcp.csproj b/src/OpenTelemetry.Resources.Gcp/OpenTelemetry.Resources.Gcp.csproj index 10e099fa93..24c4ddab5b 100644 --- a/src/OpenTelemetry.Resources.Gcp/OpenTelemetry.Resources.Gcp.csproj +++ b/src/OpenTelemetry.Resources.Gcp/OpenTelemetry.Resources.Gcp.csproj @@ -19,8 +19,8 @@ - - + + From 82389a5db7a9459f731473a6dfd73cdf7e2251d4 Mon Sep 17 00:00:00 2001 From: martincostello Date: Mon, 27 Apr 2026 11:58:34 +0100 Subject: [PATCH 18/18] [Instana] Address feedback Address review feedback. --- .../Implementation/Transport.cs | 5 +---- .../InstanaSpanSerializerTests.cs | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs index 35959a482a..4df38e372f 100644 --- a/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs +++ b/src/OpenTelemetry.Exporter.Instana/Implementation/Transport.cs @@ -54,12 +54,9 @@ public bool Send(List batch) using var enumerator = batch.GetEnumerator(); - while (stream.Position < MultiSpanBufferLimit && written < maxBatchSize && enumerator.MoveNext()) + while ((writer.BytesCommitted + writer.BytesPending) < MultiSpanBufferLimit && written < maxBatchSize && enumerator.MoveNext()) { InstanaSpanSerializer.Serialize(enumerator.Current, writer); - - writer.Flush(); - written++; } diff --git a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs index 5694c88d47..ceead2b0a3 100644 --- a/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs +++ b/test/OpenTelemetry.Exporter.Instana.Tests/InstanaSpanSerializerTests.cs @@ -55,9 +55,7 @@ public static void SerializeToStreamWriterAsync() InstanaSpanSerializer.Serialize(instanaOtelSpan, writer); writer.Flush(); - var length = stream.Position; stream.Position = 0; - stream.SetLength(length); span = JsonSerializer.Deserialize(stream, SerializerOptions); }