Skip to content

Commit 7037824

Browse files
[otlp] Fix issues with exporting using gRPC in .NET Framework apps (open-telemetry#6083)
Co-authored-by: Mikel Blanchard <[email protected]>
1 parent d9864b1 commit 7037824

File tree

9 files changed

+183
-2
lines changed

9 files changed

+183
-2
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
-->
6868
<PackageVersion Include="Google.Protobuf" Version="[3.22.5,4.0)" />
6969
<PackageVersion Include="Grpc" Version="[2.44.0,3.0)" />
70+
<PackageVersion Include="Grpc.Core" Version="[2.44.0,3.0)" />
7071
<PackageVersion Include="Grpc.Net.Client" Version="[2.52.0,3.0)" />
7172
</ItemGroup>
7273

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ Notes](../../RELEASENOTES.md).
77

88
## Unreleased
99

10+
* Fixed an issue where the OTLP gRPC exporter did not export logs, metrics, or
11+
traces in .NET Framework projects.
12+
([#6067](https://github.com/open-telemetry/opentelemetry-dotnet/issues/6067))
13+
1014
## 1.11.0
1115

1216
Released 2025-Jan-15
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#if NET462_OR_GREATER || NETSTANDARD2_0
5+
using Grpc.Core;
6+
using OpenTelemetry.Internal;
7+
8+
using InternalStatus = OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc.Status;
9+
using InternalStatusCode = OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc.StatusCode;
10+
using Status = Grpc.Core.Status;
11+
using StatusCode = Grpc.Core.StatusCode;
12+
13+
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
14+
15+
internal sealed class GrpcExportClient : IExportClient
16+
{
17+
private static readonly ExportClientGrpcResponse SuccessExportResponse = new(
18+
success: false,
19+
deadlineUtc: default,
20+
exception: null,
21+
status: null,
22+
grpcStatusDetailsHeader: null);
23+
24+
private static readonly Marshaller<byte[]> ByteArrayMarshaller = Marshallers.Create(
25+
serializer: static input => input,
26+
deserializer: static data => data);
27+
28+
private readonly Method<byte[], byte[]> exportMethod;
29+
30+
private readonly CallInvoker callInvoker;
31+
32+
public GrpcExportClient(OtlpExporterOptions options, string signalPath)
33+
{
34+
Guard.ThrowIfNull(options);
35+
Guard.ThrowIfInvalidTimeout(options.TimeoutMilliseconds);
36+
Guard.ThrowIfNull(signalPath);
37+
38+
var exporterEndpoint = options.Endpoint.AppendPathIfNotPresent(signalPath);
39+
this.Endpoint = new UriBuilder(exporterEndpoint).Uri;
40+
this.Channel = options.CreateChannel();
41+
this.Headers = options.GetMetadataFromHeaders();
42+
43+
var serviceAndMethod = signalPath.Split('/');
44+
this.exportMethod = new Method<byte[], byte[]>(MethodType.Unary, serviceAndMethod[0], serviceAndMethod[1], ByteArrayMarshaller, ByteArrayMarshaller);
45+
this.callInvoker = this.Channel.CreateCallInvoker();
46+
}
47+
48+
internal Channel Channel { get; }
49+
50+
internal Uri Endpoint { get; }
51+
52+
internal Metadata Headers { get; }
53+
54+
public ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default)
55+
{
56+
try
57+
{
58+
var contentSpan = buffer.AsSpan(0, contentLength);
59+
this.callInvoker?.BlockingUnaryCall(this.exportMethod, null, new CallOptions(this.Headers, deadlineUtc, cancellationToken), contentSpan.ToArray());
60+
return SuccessExportResponse;
61+
}
62+
catch (RpcException rpcException)
63+
{
64+
OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, rpcException);
65+
return new ExportClientGrpcResponse(success: false, deadlineUtc: deadlineUtc, exception: rpcException, ConvertGrpcStatusToStatus(rpcException.Status), rpcException.Trailers.ToString());
66+
}
67+
}
68+
69+
public bool Shutdown(int timeoutMilliseconds)
70+
{
71+
if (this.Channel == null)
72+
{
73+
return true;
74+
}
75+
76+
if (timeoutMilliseconds == -1)
77+
{
78+
this.Channel.ShutdownAsync().Wait();
79+
return true;
80+
}
81+
else
82+
{
83+
return Task.WaitAny([this.Channel.ShutdownAsync(), Task.Delay(timeoutMilliseconds)]) == 0;
84+
}
85+
}
86+
87+
private static InternalStatus ConvertGrpcStatusToStatus(Status grpcStatus) => grpcStatus.StatusCode switch
88+
{
89+
StatusCode.OK => new InternalStatus(InternalStatusCode.OK, grpcStatus.Detail),
90+
StatusCode.Cancelled => new InternalStatus(InternalStatusCode.Cancelled, grpcStatus.Detail),
91+
StatusCode.Unknown => new InternalStatus(InternalStatusCode.Unknown, grpcStatus.Detail),
92+
StatusCode.InvalidArgument => new InternalStatus(InternalStatusCode.InvalidArgument, grpcStatus.Detail),
93+
StatusCode.DeadlineExceeded => new InternalStatus(InternalStatusCode.DeadlineExceeded, grpcStatus.Detail),
94+
StatusCode.NotFound => new InternalStatus(InternalStatusCode.NotFound, grpcStatus.Detail),
95+
StatusCode.AlreadyExists => new InternalStatus(InternalStatusCode.AlreadyExists, grpcStatus.Detail),
96+
StatusCode.PermissionDenied => new InternalStatus(InternalStatusCode.PermissionDenied, grpcStatus.Detail),
97+
StatusCode.Unauthenticated => new InternalStatus(InternalStatusCode.Unauthenticated, grpcStatus.Detail),
98+
StatusCode.ResourceExhausted => new InternalStatus(InternalStatusCode.ResourceExhausted, grpcStatus.Detail),
99+
StatusCode.FailedPrecondition => new InternalStatus(InternalStatusCode.FailedPrecondition, grpcStatus.Detail),
100+
StatusCode.Aborted => new InternalStatus(InternalStatusCode.Aborted, grpcStatus.Detail),
101+
StatusCode.OutOfRange => new InternalStatus(InternalStatusCode.OutOfRange, grpcStatus.Detail),
102+
StatusCode.Unimplemented => new InternalStatus(InternalStatusCode.Unimplemented, grpcStatus.Detail),
103+
StatusCode.Internal => new InternalStatus(InternalStatusCode.Internal, grpcStatus.Detail),
104+
StatusCode.Unavailable => new InternalStatus(InternalStatusCode.Unavailable, grpcStatus.Detail),
105+
StatusCode.DataLoss => new InternalStatus(InternalStatusCode.DataLoss, grpcStatus.Detail),
106+
_ => new InternalStatus(InternalStatusCode.Unknown, grpcStatus.Detail),
107+
};
108+
}
109+
#endif

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry\OpenTelemetry.csproj" />
2424
</ItemGroup>
2525

26+
<ItemGroup>
27+
<PackageReference Include="Grpc.Core" Condition="'$(TargetFramework)' == 'netstandard2.0' OR '$(TargetFramework)' == '$(NetFrameworkMinimumSupportedVersion)'" />
28+
</ItemGroup>
29+
2630
<ItemGroup>
2731
<Reference Include="System.Net.Http" Condition="'$(TargetFramework)' == '$(NetFrameworkMinimumSupportedVersion)'" />
2832
</ItemGroup>

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
1010
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
1111
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Transmission;
12+
#if NET462_OR_GREATER || NETSTANDARD2_0
13+
using Grpc.Core;
14+
#endif
1215

1316
namespace OpenTelemetry.Exporter;
1417

@@ -22,6 +25,30 @@ internal static class OtlpExporterOptionsExtensions
2225
private const string MetricsHttpServicePath = "v1/metrics";
2326
private const string LogsHttpServicePath = "v1/logs";
2427

28+
#if NET462_OR_GREATER || NETSTANDARD2_0
29+
public static Channel CreateChannel(this OtlpExporterOptions options)
30+
{
31+
if (options.Endpoint.Scheme != Uri.UriSchemeHttp && options.Endpoint.Scheme != Uri.UriSchemeHttps)
32+
{
33+
throw new NotSupportedException($"Endpoint URI scheme ({options.Endpoint.Scheme}) is not supported. Currently only \"http\" and \"https\" are supported.");
34+
}
35+
36+
ChannelCredentials channelCredentials;
37+
if (options.Endpoint.Scheme == Uri.UriSchemeHttps)
38+
{
39+
channelCredentials = new SslCredentials();
40+
}
41+
else
42+
{
43+
channelCredentials = ChannelCredentials.Insecure;
44+
}
45+
46+
return new Channel(options.Endpoint.Authority, channelCredentials);
47+
}
48+
49+
public static Metadata GetMetadataFromHeaders(this OtlpExporterOptions options) => options.GetHeaders<Metadata>((m, k, v) => m.Add(k, v));
50+
#endif
51+
2552
public static THeaders GetHeaders<THeaders>(this OtlpExporterOptions options, Action<THeaders, string, string> addHeader)
2653
where THeaders : new()
2754
{
@@ -97,6 +124,20 @@ public static IExportClient GetExportClient(this OtlpExporterOptions options, Ot
97124
throw new NotSupportedException($"Protocol {options.Protocol} is not supported.");
98125
}
99126

127+
#if NET462_OR_GREATER || NETSTANDARD2_0
128+
if (options.Protocol == OtlpExportProtocol.Grpc)
129+
{
130+
var servicePath = otlpSignalType switch
131+
{
132+
OtlpSignalType.Traces => TraceGrpcServicePath,
133+
OtlpSignalType.Metrics => MetricsGrpcServicePath,
134+
OtlpSignalType.Logs => LogsGrpcServicePath,
135+
_ => throw new NotSupportedException($"OtlpSignalType {otlpSignalType} is not supported."),
136+
};
137+
return new GrpcExportClient(options, servicePath);
138+
}
139+
#endif
140+
100141
return otlpSignalType switch
101142
{
102143
OtlpSignalType.Traces => options.Protocol == OtlpExportProtocol.Grpc

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ internal OtlpLogExporter(
5858

5959
this.experimentalOptions = experimentalOptions!;
6060
this.sdkLimitOptions = sdkLimitOptions!;
61+
#if NET462_OR_GREATER || NETSTANDARD2_0
62+
this.startWritePosition = 0;
63+
#else
6164
this.startWritePosition = exporterOptions!.Protocol == OtlpExportProtocol.Grpc ? GrpcStartWritePosition : 0;
65+
#endif
6266
this.transmissionHandler = transmissionHandler ?? exporterOptions!.GetExportTransmissionHandler(experimentalOptions!, OtlpSignalType.Logs);
6367
}
6468

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ internal OtlpMetricExporter(
5151
Debug.Assert(exporterOptions != null, "exporterOptions was null");
5252
Debug.Assert(experimentalOptions != null, "experimentalOptions was null");
5353

54+
#if NET462_OR_GREATER || NETSTANDARD2_0
55+
this.startWritePosition = 0;
56+
#else
5457
this.startWritePosition = exporterOptions!.Protocol == OtlpExportProtocol.Grpc ? GrpcStartWritePosition : 0;
58+
#endif
5559
this.transmissionHandler = transmissionHandler ?? exporterOptions!.GetExportTransmissionHandler(experimentalOptions!, OtlpSignalType.Metrics);
5660
}
5761

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ internal OtlpTraceExporter(
5454
Debug.Assert(sdkLimitOptions != null, "sdkLimitOptions was null");
5555

5656
this.sdkLimitOptions = sdkLimitOptions!;
57+
#if NET462_OR_GREATER || NETSTANDARD2_0
58+
this.startWritePosition = 0;
59+
#else
5760
this.startWritePosition = exporterOptions!.Protocol == OtlpExportProtocol.Grpc ? GrpcStartWritePosition : 0;
61+
#endif
5862
this.transmissionHandler = transmissionHandler ?? exporterOptions!.GetExportTransmissionHandler(experimentalOptions, OtlpSignalType.Traces);
5963
}
6064

test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ public void GetHeaders_NoOptionHeaders_ReturnsStandardHeaders(string? optionHead
3535
}
3636

3737
[Theory]
38+
#if NET462_OR_GREATER
39+
[InlineData(OtlpExportProtocol.Grpc, typeof(GrpcExportClient))]
40+
#else
3841
[InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient))]
42+
#endif
3943
[InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient))]
4044
public void GetTraceExportClient_SupportedProtocol_ReturnsCorrectExportClient(OtlpExportProtocol protocol, Type expectedExportClientType)
4145
{
@@ -75,13 +79,19 @@ public void AppendPathIfNotPresent_TracesPath_AppendsCorrectly(string inputUri,
7579
}
7680

7781
[Theory]
82+
#if NET462_OR_GREATER
83+
[InlineData(OtlpExportProtocol.Grpc, typeof(GrpcExportClient), false, 10000, null)]
84+
[InlineData(OtlpExportProtocol.Grpc, typeof(GrpcExportClient), false, 10000, "in_memory")]
85+
[InlineData(OtlpExportProtocol.Grpc, typeof(GrpcExportClient), false, 10000, "disk")]
86+
#else
7887
[InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient), false, 10000, null)]
88+
[InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient), false, 10000, "in_memory")]
89+
[InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient), false, 10000, "disk")]
90+
#endif
7991
[InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), false, 10000, null)]
8092
[InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), true, 8000, null)]
81-
[InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient), false, 10000, "in_memory")]
8293
[InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), false, 10000, "in_memory")]
8394
[InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), true, 8000, "in_memory")]
84-
[InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcExportClient), false, 10000, "disk")]
8595
[InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), false, 10000, "disk")]
8696
[InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpExportClient), true, 8000, "disk")]
8797
public void GetTransmissionHandler_InitializesCorrectHandlerExportClientAndTimeoutValue(OtlpExportProtocol protocol, Type exportClientType, bool customHttpClient, int expectedTimeoutMilliseconds, string? retryStrategy)

0 commit comments

Comments
 (0)