diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 538dc24cf98..73cbec1c345 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Added validation that insecure channel is configured correctly when using + .NET Core 3.x for gRPC-based exporting. + ([#2691](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2691)) + ## 1.2.0-rc1 Released 2021-Nov-29 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs index aee71c7fd91..cc5a5be13da 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs @@ -17,10 +17,10 @@ using System.Threading; using System.Threading.Tasks; using Grpc.Core; +using OpenTelemetry.Internal; #if NETSTANDARD2_1 || NET5_0_OR_GREATER using Grpc.Net.Client; #endif -using OpenTelemetry.Internal; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient { @@ -33,6 +33,8 @@ protected BaseOtlpGrpcExportClient(OtlpExporterOptions options) Guard.Null(options, nameof(options)); Guard.InvalidTimeout(options.TimeoutMilliseconds, nameof(options.TimeoutMilliseconds)); + ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options); + this.Options = options; this.Headers = options.GetMetadataFromHeaders(); } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExporterClientValidation.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExporterClientValidation.cs new file mode 100644 index 00000000000..468fd0402a3 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExporterClientValidation.cs @@ -0,0 +1,45 @@ +// +// 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; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +{ + internal static class ExporterClientValidation + { + internal static void EnsureUnencryptedSupportIsEnabled(OtlpExporterOptions options) + { + var version = System.Environment.Version; + + // This verification is only required for .NET Core 3.x + if (version.Major != 3) + { + return; + } + + if (options.Endpoint.Scheme.Equals("http", StringComparison.InvariantCultureIgnoreCase)) + { + if (AppContext.TryGetSwitch( + "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", out var unencryptedIsSupported) == false + || unencryptedIsSupported == false) + { + throw new InvalidOperationException( + "Calling insecure gRPC services on .NET Core 3.x requires enabling the 'System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport' switch. See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client"); + } + } + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs new file mode 100644 index 00000000000..5667385271f --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs @@ -0,0 +1,77 @@ +// +// 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 OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +{ + public class ExporterClientValidationTests : Http2UnencryptedSupportTests + { + private const string HttpEndpoint = "http://localhost:4173"; + private const string HttpsEndpoint = "https://localhost:4173"; + + [Fact] + public void ExporterClientValidation_FlagIsEnabledForHttpEndpoint() + { + var options = new OtlpExporterOptions + { + Endpoint = new Uri(HttpEndpoint), + }; + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); + Assert.Null(exception); + } + + [Fact] + public void ExporterClientValidation_FlagIsNotEnabledForHttpEndpoint() + { + var options = new OtlpExporterOptions + { + Endpoint = new Uri(HttpEndpoint), + }; + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); + + var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); + + if (Environment.Version.Major == 3) + { + Assert.NotNull(exception); + Assert.IsType(exception); + } + else + { + Assert.Null(exception); + } + } + + [Fact] + public void ExporterClientValidation_FlagIsNotEnabledForHttpsEndpoint() + { + var options = new OtlpExporterOptions + { + Endpoint = new Uri(HttpsEndpoint), + }; + + var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); + Assert.Null(exception); + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs new file mode 100644 index 00000000000..907905ff672 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs @@ -0,0 +1,45 @@ +// +// 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; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +{ + public class Http2UnencryptedSupportTests : IDisposable + { + private readonly bool initialFlagStatus; + + public Http2UnencryptedSupportTests() + { + this.initialFlagStatus = this.DetermineInitialFlagStatus(); + } + + public void Dispose() + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", this.initialFlagStatus); + } + + private bool DetermineInitialFlagStatus() + { + if (AppContext.TryGetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", out var flag)) + { + return flag; + } + + return false; + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs index ef13a3415b8..12403c22d0f 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs @@ -68,5 +68,32 @@ public void ExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint) Assert.Single(delegatingExporter.ExportResults); Assert.Equal(ExportResult.Success, delegatingExporter.ExportResults[0]); } + + [Trait("CategoryName", "CollectorIntegrationTests")] + [SkipUnlessEnvVarFoundFact(CollectorHostnameEnvVarName)] + public void ConstructingGrpcExporterFailsWhenHttp2UnencryptedSupportIsDisabledForNetcoreapp31() + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service. + // We want to fail fast so we are disabling it + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); + + var exporterOptions = new OtlpExporterOptions + { + Endpoint = new Uri($"http://{CollectorHostname}:4317"), + }; + + var exception = Record.Exception(() => new OtlpTraceExporter(exporterOptions)); + + if (Environment.Version.Major == 3) + { + Assert.NotNull(exception); + } + else + { + Assert.Null(exception); + } + } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj index 396ed50110a..1bce078c2cb 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj @@ -29,6 +29,7 @@ + diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs index 5e7e6d701e4..970f96daaea 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs @@ -22,7 +22,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests { - public class OtlpExporterOptionsExtensionsTests + public class OtlpExporterOptionsExtensionsTests : Http2UnencryptedSupportTests { [Theory] [InlineData("key=value", new string[] { "key" }, new string[] { "value" })] @@ -86,6 +86,14 @@ public void GetHeaders_NoOptionHeaders_ReturnsEmptyHeadres(string optionHeaders) [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient))] public void GetTraceExportClient_SupportedProtocol_ReturnsCorrectExportClient(OtlpExportProtocol protocol, Type expectedExportClientType) { + if (protocol == OtlpExportProtocol.Grpc && Environment.Version.Major == 3) + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + var options = new OtlpExporterOptions { Protocol = protocol, @@ -96,6 +104,32 @@ public void GetTraceExportClient_SupportedProtocol_ReturnsCorrectExportClient(Ot Assert.Equal(expectedExportClientType, exportClient.GetType()); } + [Fact] + public void GetTraceExportClient_GetClientForGrpcWithoutUnencryptedFlag_ThrowsException() + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); + + var options = new OtlpExporterOptions + { + Protocol = OtlpExportProtocol.Grpc, + }; + + var exception = Record.Exception(() => options.GetTraceExportClient()); + + if (Environment.Version.Major == 3) + { + Assert.NotNull(exception); + Assert.IsType(exception); + } + else + { + Assert.Null(exception); + } + } + [Fact] public void GetTraceExportClient_UnsupportedProtocol_Throws() { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index b040143c0df..64070b84772 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -35,7 +35,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests { - public class OtlpTraceExporterTests + public class OtlpTraceExporterTests : Http2UnencryptedSupportTests { static OtlpTraceExporterTests() { @@ -372,6 +372,14 @@ public void ToOtlpSpanPeerServiceTest() [Fact] public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor() { + if (Environment.Version.Major == 3) + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + const string ActivitySourceName = "otlp.test"; TestActivityProcessor testActivityProcessor = new TestActivityProcessor();