diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..bab9f248015 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,8 @@ +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.Enrich.get -> System.Action +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.Enrich.set -> void +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.GrpcClientInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.SuppressDownstreamInstrumentation.get -> bool +OpenTelemetry.Instrumentation.GrpcNetClient.GrpcClientInstrumentationOptions.SuppressDownstreamInstrumentation.set -> void +OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddGrpcClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md index e83038b3ca2..4085ea49f92 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Add `netstandard2.0` target enabling the Grpc.Net.Client instrumentation to + be consumed by .NET Framework applications. + ([#3105](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3105)) + ## 1.0.0-rc9.3 Released 2022-Apr-15 diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj b/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj index a67006d11c4..ddf5e9dd460 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj @@ -1,7 +1,7 @@ - + - netstandard2.1 + netstandard2.1;netstandard2.0 gRPC for .NET client instrumentation for OpenTelemetry .NET $(PackageTags);distributed-tracing true diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs index 9c9f9470354..85e7e11967a 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs @@ -14,6 +14,7 @@ // limitations under the License. // +#if !NETFRAMEWORK using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -101,3 +102,4 @@ public void Configure(IApplicationBuilder app) } } } +#endif diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs new file mode 100644 index 00000000000..8f7f1a31ed3 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ClientTestHelpers.cs @@ -0,0 +1,92 @@ +// +// 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 System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Google.Protobuf; +using Grpc.Net.Compression; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +{ + internal static class ClientTestHelpers + { + public static HttpClient CreateTestClient(Func> sendAsync, Uri baseAddress = null) + { + var handler = TestHttpMessageHandler.Create(sendAsync); + var httpClient = new HttpClient(handler); + httpClient.BaseAddress = baseAddress ?? new Uri("https://localhost"); + + return httpClient; + } + + public static Task CreateResponseContent(TResponse response, ICompressionProvider compressionProvider = null) + where TResponse : IMessage + { + return CreateResponseContentCore(new[] { response }, compressionProvider); + } + + public static async Task WriteResponseAsync(Stream ms, TResponse response, ICompressionProvider compressionProvider) + where TResponse : IMessage + { + var compress = false; + + byte[] data; + if (compressionProvider != null) + { + compress = true; + + var output = new MemoryStream(); + var compressionStream = compressionProvider.CreateCompressionStream(output, System.IO.Compression.CompressionLevel.Fastest); + var compressedData = response.ToByteArray(); + + compressionStream.Write(compressedData, 0, compressedData.Length); + compressionStream.Flush(); + compressionStream.Dispose(); + data = output.ToArray(); + } + else + { + data = response.ToByteArray(); + } + + await ResponseUtils.WriteHeaderAsync(ms, data.Length, compress, CancellationToken.None); +#if NET5_0_OR_GREATER + await ms.WriteAsync(data); +#else + await ms.WriteAsync(data, 0, data.Length); +#endif + } + + private static async Task CreateResponseContentCore(TResponse[] responses, ICompressionProvider compressionProvider) + where TResponse : IMessage + { + var ms = new MemoryStream(); + foreach (var response in responses) + { + await WriteResponseAsync(ms, response, compressionProvider); + } + + ms.Seek(0, SeekOrigin.Begin); + var streamContent = new StreamContent(ms); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/grpc"); + return streamContent; + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs new file mode 100644 index 00000000000..066f79865c6 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/ResponseUtils.cs @@ -0,0 +1,92 @@ +// +// 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 System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +{ + internal static class ResponseUtils + { + internal const string MessageEncodingHeader = "grpc-encoding"; + internal const string IdentityGrpcEncoding = "identity"; + internal const string StatusTrailer = "grpc-status"; + internal static readonly MediaTypeHeaderValue GrpcContentTypeHeaderValue = new MediaTypeHeaderValue("application/grpc"); + internal static readonly Version ProtocolVersion = new Version(2, 0); + private const int MessageDelimiterSize = 4; // how many bytes it takes to encode "Message-Length" + private const int HeaderSize = MessageDelimiterSize + 1; // message length + compression flag + + public static HttpResponseMessage CreateResponse( + HttpStatusCode statusCode, + HttpContent payload, + global::Grpc.Core.StatusCode? grpcStatusCode = global::Grpc.Core.StatusCode.OK) + { + payload.Headers.ContentType = GrpcContentTypeHeaderValue; + + var message = new HttpResponseMessage(statusCode) + { + Content = payload, + Version = ProtocolVersion, + }; + + message.RequestMessage = new HttpRequestMessage(); +#if NETFRAMEWORK + message.RequestMessage.Properties[TrailingHeadersHelpers.ResponseTrailersKey] = new ResponseTrailers(); +#endif + message.Headers.Add(MessageEncodingHeader, IdentityGrpcEncoding); + + if (grpcStatusCode != null) + { + message.TrailingHeaders().Add(StatusTrailer, grpcStatusCode.Value.ToString("D")); + } + + return message; + } + + public static Task WriteHeaderAsync(Stream stream, int length, bool compress, CancellationToken cancellationToken) + { + var headerData = new byte[HeaderSize]; + + // Compression flag + headerData[0] = compress ? (byte)1 : (byte)0; + + // Message length + EncodeMessageLength(length, headerData.AsSpan(1)); + + return stream.WriteAsync(headerData, 0, headerData.Length, cancellationToken); + } + + private static void EncodeMessageLength(int messageLength, Span destination) + { + Debug.Assert(destination.Length >= MessageDelimiterSize, "Buffer too small to encode message length."); + + BinaryPrimitives.WriteUInt32BigEndian(destination, (uint)messageLength); + } + +#if NETFRAMEWORK + private class ResponseTrailers : HttpHeaders + { + } +#endif + } +} diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs new file mode 100644 index 00000000000..71a800adba1 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TestHttpMessageHandler.cs @@ -0,0 +1,56 @@ +// +// 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 System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +{ + public class TestHttpMessageHandler : HttpMessageHandler + { + private readonly Func> sendAsync; + + public TestHttpMessageHandler(Func> sendAsync) + { + this.sendAsync = sendAsync; + } + + public static TestHttpMessageHandler Create(Func> sendAsync) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + return new TestHttpMessageHandler(async (request, cancellationToken) => + { + using var registration = cancellationToken.Register(() => tcs.TrySetCanceled()); + + var result = await Task.WhenAny(sendAsync(request), tcs.Task); + return await result; + }); + } + + public static TestHttpMessageHandler Create(Func> sendAsync) + { + return new TestHttpMessageHandler(sendAsync); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return this.sendAsync(request, cancellationToken); + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs new file mode 100644 index 00000000000..429f1d1b581 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTestHelpers/TrailingHeadersHelpers.cs @@ -0,0 +1,59 @@ +// +// 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 System.Net.Http; +using System.Net.Http.Headers; + +namespace OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers +{ + internal static class TrailingHeadersHelpers + { + public static readonly string ResponseTrailersKey = "__ResponseTrailers"; + + public static HttpHeaders TrailingHeaders(this HttpResponseMessage responseMessage) + { +#if !NETFRAMEWORK + return responseMessage.TrailingHeaders; +#else + if (responseMessage.RequestMessage.Properties.TryGetValue(ResponseTrailersKey, out var headers) && + headers is HttpHeaders httpHeaders) + { + return httpHeaders; + } + + // App targets .NET Standard 2.0 and the handler hasn't set trailers + // in RequestMessage.Properties with known key. Return empty collection. + // Client call will likely fail because it is unable to get a grpc-status. + return ResponseTrailers.Empty; +#endif + } + +#if NETFRAMEWORK + public static void EnsureTrailingHeaders(this HttpResponseMessage responseMessage) + { + if (!responseMessage.RequestMessage.Properties.ContainsKey(ResponseTrailersKey)) + { + responseMessage.RequestMessage.Properties[ResponseTrailersKey] = new ResponseTrailers(); + } + } + + private class ResponseTrailers : HttpHeaders + { + public static readonly ResponseTrailers Empty = new ResponseTrailers(); + } +#endif + } +} diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs index 5de334755b5..666e9081eef 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.client.cs @@ -17,6 +17,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using Greet; @@ -25,6 +26,7 @@ using Microsoft.AspNetCore.Http; using Moq; using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Instrumentation.Grpc.Tests.GrpcTestHelpers; using OpenTelemetry.Instrumentation.GrpcNetClient; using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; using OpenTelemetry.Trace; @@ -44,9 +46,17 @@ public partial class GrpcTests [InlineData("http://[::1]", false)] public void GrpcClientCallsAreCollectedSuccessfully(string baseAddress, bool shouldEnrich = true) { - var uri = new Uri($"{baseAddress}:{this.server.Port}"); + var uri = new Uri($"{baseAddress}:1234"); var uriHostNameType = Uri.CheckHostName(uri.Host); + var httpClient = ClientTestHelpers.CreateTestClient(async request => + { + var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()); + var response = ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent, grpcStatusCode: global::Grpc.Core.StatusCode.OK); + response.TrailingHeaders().Add("grpc-message", "value"); + return response; + }); + var processor = new Mock>(); var parent = new Activity("parent") @@ -65,7 +75,10 @@ public void GrpcClientCallsAreCollectedSuccessfully(string baseAddress, bool sho .AddProcessor(processor.Object) .Build()) { - var channel = GrpcChannel.ForAddress(uri); + var channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions + { + HttpClient = httpClient, + }); var client = new Greeter.GreeterClient(channel); var rs = client.SayHello(new HelloRequest()); } @@ -104,6 +117,7 @@ public void GrpcClientCallsAreCollectedSuccessfully(string baseAddress, bool sho Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeRpcGrpcStatusCode)); } +#if !NETFRAMEWORK [Theory] [InlineData(true)] [InlineData(false)] @@ -427,6 +441,7 @@ public void GrpcClientInstrumentationRespectsSdkSuppressInstrumentation() })); } } +#endif [Fact] public void Grpc_BadArgs() diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs index 2c32f7d347a..5809d2e2d0f 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs @@ -14,6 +14,7 @@ // limitations under the License. // +#if !NETFRAMEWORK using System; using System.Collections.Generic; using System.Diagnostics; @@ -247,3 +248,4 @@ private static Activity GetActivityFromProcessorInvocation(MockUnit test project for OpenTelemetry Grpc for .NET instrumentation net6.0;netcoreapp3.1 + $(TargetFrameworks);net462 @@ -20,11 +21,15 @@ - + + + + +