diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index 0532a56ca84..4323e20a678 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -249,6 +249,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-console", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-jaeger", "docs\trace\getting-started-jaeger\getting-started-jaeger.csproj", "{A0C0B77C-6C7B-4EC2-AC61-EA1F489811B9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage", "src\OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage\OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.csproj", "{2DA349A7-814F-43A6-AA22-28D03421EAF7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests", "test\OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests\OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests.csproj", "{448837FD-6DEA-4B37-A1F5-42BC7AD8E9BE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -523,6 +527,14 @@ Global {A0C0B77C-6C7B-4EC2-AC61-EA1F489811B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0C0B77C-6C7B-4EC2-AC61-EA1F489811B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0C0B77C-6C7B-4EC2-AC61-EA1F489811B9}.Release|Any CPU.Build.0 = Release|Any CPU + {2DA349A7-814F-43A6-AA22-28D03421EAF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DA349A7-814F-43A6-AA22-28D03421EAF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DA349A7-814F-43A6-AA22-28D03421EAF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DA349A7-814F-43A6-AA22-28D03421EAF7}.Release|Any CPU.Build.0 = Release|Any CPU + {448837FD-6DEA-4B37-A1F5-42BC7AD8E9BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {448837FD-6DEA-4B37-A1F5-42BC7AD8E9BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {448837FD-6DEA-4B37-A1F5-42BC7AD8E9BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {448837FD-6DEA-4B37-A1F5-42BC7AD8E9BE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/Common.props b/build/Common.props index 7179ee4e35a..0b92c38fb0b 100644 --- a/build/Common.props +++ b/build/Common.props @@ -45,6 +45,7 @@ [1.1.1,2.0) [0.12.1,0.13) 1.4.0 + 1.0.0-beta.1 [2.8.0,3.0) [1.2.0-beta.435,2.0) 1.4.0 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.csproj new file mode 100644 index 00000000000..e532095d450 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.csproj @@ -0,0 +1,20 @@ + + + + + net6.0;netstandard2.1;netstandard2.0;net462 + OpenTelemetry protocol exporter persistent storage for OpenTelemetry .NET + $(PackageTags);OTLP + core- + + + + + false + + + + + + + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage/OtlpTraceExporterPersistentStorageExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage/OtlpTraceExporterPersistentStorageExtensions.cs new file mode 100644 index 00000000000..bda48f83791 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage/OtlpTraceExporterPersistentStorageExtensions.cs @@ -0,0 +1,95 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Exporter; +using OpenTelemetry.Extensions.PersistentStorage.Abstractions; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) exporter +/// with persistent storage. +/// +public static class OtlpTraceExporterPersistentStorageExtensions +{ + /// + /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider + /// with access to persistent storage. + /// + /// builder to use. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// Factory function to create a . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddOtlpExporterWithPersistentStorage( + this TracerProviderBuilder builder, + string? name, + Action configure, + Func persistentStorageFactory) + { + Guard.ThrowIfNull(persistentStorageFactory); + + Action? inlineConfigurationAction; + if (name is not null) + { + // Note: If we are using named options we can safely use Options API + // to act on the instance that will be created. See: + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4043 + builder.ConfigureServices(services => + { + services + .AddOptions(name) + .Configure(ConfigureOptionsAction); + }); + + inlineConfigurationAction = null; + } + else + { + // Note: If we are NOT using named options we need to execute inline + // to prevent potentially impacting options for other signals. + inlineConfigurationAction = ConfigureOptionsAction; + } + + return builder.AddOtlpExporter( + name, + configure, + inlineConfigurationAction); + + void ConfigureOptionsAction(OtlpExporterOptions options, IServiceProvider serviceProvider) + { + options.PersistentBlobProvider = persistentStorageFactory(serviceProvider); + } + } + + /// + /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider + /// with access to persistent storage. + /// + /// builder to use. + /// Callback action for configuring . + /// Factory function to create a . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddOtlpExporterWithPersistentStorage( + this TracerProviderBuilder builder, + Action configure, + Func persistentStorageFactory) + { + return builder.AddOtlpExporterWithPersistentStorage(name: null, configure, persistentStorageFactory); + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage/README.md new file mode 100644 index 00000000000..d7f35b4cd65 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage/README.md @@ -0,0 +1,14 @@ +# OTLP Exporter Persistent Storage for OpenTelemetry .NET + +## Usage + +```csharp +var storagePath = Path.GetTempPath(); + +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("ActivitySourceName") + .AddOtlpExporterWithPersistentStorage( + opt => {}, + serviceProvider => new FileBlobProvider(storagePath)) + .Build(); +``` diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs index 44e26f37879..56dcf3d457b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs @@ -21,6 +21,8 @@ [assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] [assembly: InternalsVisibleTo("MockOpenTelemetryCollector, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] // Used by Moq. [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] @@ -28,6 +30,8 @@ [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests")] [assembly: InternalsVisibleTo("Benchmarks")] [assembly: InternalsVisibleTo("MockOpenTelemetryCollector")] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage")] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests")] // Used by Moq. [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs index bc833bea2d0..616ec284db0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs @@ -15,6 +15,7 @@ // using Grpc.Core; +using OpenTelemetry.Extensions.PersistentStorage.Abstractions; using OpenTelemetry.Internal; #if NETSTANDARD2_1 || NET6_0_OR_GREATER using Grpc.Net.Client; @@ -36,6 +37,7 @@ protected BaseOtlpGrpcExportClient(OtlpExporterOptions options) this.Endpoint = new UriBuilder(options.Endpoint).Uri; this.Headers = options.GetMetadataFromHeaders(); this.TimeoutMilliseconds = options.TimeoutMilliseconds; + this.PersistentBlobProvider = options.PersistentBlobProvider; } #if NETSTANDARD2_1 || NET6_0_OR_GREATER @@ -50,6 +52,8 @@ protected BaseOtlpGrpcExportClient(OtlpExporterOptions options) internal int TimeoutMilliseconds { get; } + internal PersistentBlobProvider PersistentBlobProvider { get; } + /// public abstract bool SendExportRequest(TRequest request, CancellationToken cancellationToken = default); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs index b4c9084cd2c..d4cc416ceb6 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs @@ -14,6 +14,7 @@ // limitations under the License. // +using Google.Protobuf; using Grpc.Core; using OtlpCollector = OpenTelemetry.Proto.Collector.Trace.V1; @@ -49,6 +50,12 @@ public override bool SendExportRequest(OtlpCollector.ExportTraceServiceRequest r } catch (RpcException ex) { + // TODO: Add check to only create blob for a retryable error + if (this.PersistentBlobProvider != null) + { + this.PersistentBlobProvider.TryCreateBlob(request.ToByteArray(), out _); + } + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); return false; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj index 7a6c5164d07..8c8e4763f21 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj @@ -19,6 +19,7 @@ + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 1aed1cd6339..8f43f40e2b1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using OpenTelemetry.Extensions.PersistentStorage.Abstractions; using OpenTelemetry.Internal; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -195,6 +196,12 @@ public Uri Endpoint /// internal bool ProgrammaticallyModifiedEndpoint { get; private set; } + /// + /// Gets or sets the used at + /// runtime to save data from retryable errors to offline storage. + /// + internal PersistentBlobProvider PersistentBlobProvider { get; set; } + internal static void RegisterOtlpExporterOptionsFactory(IServiceCollection services) { services.RegisterOptionsFactory( diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs index 31e8cbbfe2f..26099aaec2a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs @@ -34,7 +34,7 @@ public static class OtlpTraceExporterHelperExtensions /// builder to use. /// The instance of to chain the calls. public static TracerProviderBuilder AddOtlpExporter(this TracerProviderBuilder builder) - => AddOtlpExporter(builder, name: null, configure: null); + => AddOtlpExporter(builder, name: null, configure: null, inlineConfigurationAction: null); /// /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider. @@ -43,7 +43,7 @@ public static TracerProviderBuilder AddOtlpExporter(this TracerProviderBuilder b /// Callback action for configuring . /// The instance of to chain the calls. public static TracerProviderBuilder AddOtlpExporter(this TracerProviderBuilder builder, Action configure) - => AddOtlpExporter(builder, name: null, configure); + => AddOtlpExporter(builder, name: null, configure, inlineConfigurationAction: null); /// /// Adds OpenTelemetry Protocol (OTLP) exporter to the TracerProvider. @@ -56,6 +56,13 @@ public static TracerProviderBuilder AddOtlpExporter( this TracerProviderBuilder builder, string name, Action configure) + => AddOtlpExporter(builder, name, configure, inlineConfigurationAction: null); + + internal static TracerProviderBuilder AddOtlpExporter( + this TracerProviderBuilder builder, + string name, + Action configure, + Action inlineConfigurationAction) { Guard.ThrowIfNull(builder); @@ -97,6 +104,8 @@ public static TracerProviderBuilder AddOtlpExporter( exporterOptions = sp.GetRequiredService>().Get(finalOptionsName); } + inlineConfigurationAction?.Invoke(exporterOptions, sp); + // Note: Not using finalOptionsName here for SdkLimitOptions. // There should only be one provider for a given service // collection so SdkLimitOptions is treated as a single default diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests.csproj b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests.csproj new file mode 100644 index 00000000000..8cb57a02960 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + disable + + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests/TestPersistentStorage.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests/TestPersistentStorage.cs new file mode 100644 index 00000000000..96849cdddaa --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests/TestPersistentStorage.cs @@ -0,0 +1,224 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if !NETFRAMEWORK +using System.Diagnostics; +using Grpc.Core; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry.Extensions.PersistentStorage.Abstractions; +using OpenTelemetry.Metrics; +using OpenTelemetry.Proto.Collector.Trace.V1; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.PersistentStorage.Tests; + +public sealed class TestPersistentStorage : IAsyncLifetime +{ + private readonly HttpClient httpClient; + private IHost host; + + public TestPersistentStorage() + { + this.httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:5050") }; + } + + public async Task InitializeAsync() + { + this.host = await new HostBuilder() + .ConfigureWebHostDefaults(webBuilder => webBuilder + .ConfigureKestrel(options => + { + options.ListenLocalhost(5050, listenOptions => listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1); + options.ListenLocalhost(4317, listenOptions => listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2); + }) + .ConfigureServices(services => + { + services.AddSingleton(new MockCollectorState()); + services.AddGrpc(); + }) + .Configure(app => + { + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGet( + "/MockCollector/SetResponseCodes/{responseCodesCsv}", + (MockCollectorState collectorState, string responseCodesCsv) => + { + var codes = responseCodesCsv.Split(",").Select(x => int.Parse(x)).ToArray(); + collectorState.SetStatusCodes(codes); + }); + + endpoints.MapGrpcService(); + }); + })) + .StartAsync().ConfigureAwait(false); + } + + public async Task DisposeAsync() + { + if (this.host != null) + { + await this.host.StopAsync().ConfigureAwait(false); + } + } + + [Fact] + public async Task TestOtlpTraceExportWithPersistentStorage() + { + await this.SetCollectorStatusCodes(new[] + { + Grpc.Core.StatusCode.Cancelled, + }); + + var activitySourceName = "otel.mock.collector.test.persistent-storage"; + MockFileProvider mockFileProvider = new(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(activitySourceName) + .AddOtlpExporterWithPersistentStorage( + otlpExporterOptions => + { + otlpExporterOptions.Endpoint = new Uri("http://localhost:4317"); + otlpExporterOptions.ExportProcessorType = ExportProcessorType.Simple; + }, + _ => mockFileProvider) + .Build(); + + using var source = new ActivitySource(activitySourceName); + source.StartActivity().Stop(); + tracerProvider.ForceFlush(); + + var blobs = mockFileProvider.TryGetBlobs(); + Assert.Single(blobs); + } + + private async Task SetCollectorStatusCodes(Grpc.Core.StatusCode[] codes) + { + await this.httpClient.GetAsync($"/MockCollector/SetResponseCodes/{string.Join(",", codes.Select(x => (int)x))}"); + } + + private class MockCollectorState + { + private Grpc.Core.StatusCode[] statusCodes = Array.Empty(); + private int statusCodeIndex = 0; + + public void SetStatusCodes(int[] statusCodes) + { + this.statusCodeIndex = 0; + this.statusCodes = statusCodes.Select(x => (Grpc.Core.StatusCode)x).ToArray(); + } + + public Grpc.Core.StatusCode NextStatus() + { + return this.statusCodeIndex < this.statusCodes.Length + ? this.statusCodes[this.statusCodeIndex++] + : Grpc.Core.StatusCode.OK; + } + } + + private class MockTraceService : TraceService.TraceServiceBase + { + private readonly MockCollectorState state; + + public MockTraceService(MockCollectorState state) + { + this.state = state; + } + + public override Task Export(ExportTraceServiceRequest request, ServerCallContext context) + { + var statusCode = this.state.NextStatus(); + if (statusCode != Grpc.Core.StatusCode.OK) + { + throw new RpcException(new Grpc.Core.Status(statusCode, "Error.")); + } + + return Task.FromResult(new ExportTraceServiceResponse()); + } + } + + private class MockFileProvider : PersistentBlobProvider + { + private readonly List mockStorage = new(); + + public IEnumerable TryGetBlobs() + { + return this.mockStorage.AsEnumerable(); + } + + protected override IEnumerable OnGetBlobs() + { + return this.mockStorage.AsEnumerable(); + } + + protected override bool OnTryCreateBlob(byte[] buffer, int leasePeriodMilliseconds, out PersistentBlob blob) + { + blob = new MockFileBlob(); + this.mockStorage.Add(blob); + return blob.TryWrite(buffer); + } + + protected override bool OnTryCreateBlob(byte[] buffer, out PersistentBlob blob) + { + blob = new MockFileBlob(); + this.mockStorage.Add(blob); + return blob.TryWrite(buffer); + } + + protected override bool OnTryGetBlob(out PersistentBlob blob) + { + blob = this.GetBlobs().FirstOrDefault(); + + return true; + } + } + + private class MockFileBlob : PersistentBlob + { + private byte[] buffer; + + protected override bool OnTryRead(out byte[] buffer) + { + buffer = this.buffer; + + return true; + } + + protected override bool OnTryWrite(byte[] buffer, int leasePeriodMilliseconds = 0) + { + this.buffer = buffer; + + return true; + } + + protected override bool OnTryLease(int leasePeriodMilliseconds) + { + return true; + } + + protected override bool OnTryDelete() + { + throw new NotImplementedException(); + } + } +} +#endif