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