From 739c6b706fd1b5beacf9c7a8b5b1f0970b316370 Mon Sep 17 00:00:00 2001 From: Austin Tan Date: Mon, 23 Aug 2021 16:19:39 -0700 Subject: [PATCH] [Azure] Application Insights Sampler (#148) --- opentelemetry-dotnet-contrib.sln | 14 +++ .../ApplicationInsightsSampler.cs | 83 +++++++++++++++++ ...try.Contrib.Extensions.AzureMonitor.csproj | 11 +++ .../README.md | 15 +++ .../ApplicationInsightsSamplerTests.cs | 93 +++++++++++++++++++ ...ntrib.Extensions.AzureMonitor.Tests.csproj | 22 +++++ 6 files changed, 238 insertions(+) create mode 100644 src/OpenTelemetry.Contrib.Extensions.AzureMonitor/ApplicationInsightsSampler.cs create mode 100644 src/OpenTelemetry.Contrib.Extensions.AzureMonitor/OpenTelemetry.Contrib.Extensions.AzureMonitor.csproj create mode 100644 src/OpenTelemetry.Contrib.Extensions.AzureMonitor/README.md create mode 100644 test/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests/ApplicationInsightsSamplerTests.cs create mode 100644 test/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests.csproj diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln index 5601fe8f025..c78fe4d4ffd 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -139,6 +139,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Previ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instrumentation.EntityFrameworkCore.Tests", "test\OpenTelemetry.Contrib.Instrumentation.EntityFrameworkCore.Tests\OpenTelemetry.Contrib.Instrumentation.EntityFrameworkCore.Tests.csproj", "{4172D671-3AAC-443F-9EB0-A6608B154AF0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Extensions.AzureMonitor", "src\OpenTelemetry.Contrib.Extensions.AzureMonitor\OpenTelemetry.Contrib.Extensions.AzureMonitor.csproj", "{9C2D6D1A-8580-4527-B718-E206D0690635}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests", "test\OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests\OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests.csproj", "{771651AA-010F-4FF6-9CB2-BAFFE5E04FC2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -261,6 +265,14 @@ Global {4172D671-3AAC-443F-9EB0-A6608B154AF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {4172D671-3AAC-443F-9EB0-A6608B154AF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {4172D671-3AAC-443F-9EB0-A6608B154AF0}.Release|Any CPU.Build.0 = Release|Any CPU + {9C2D6D1A-8580-4527-B718-E206D0690635}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C2D6D1A-8580-4527-B718-E206D0690635}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C2D6D1A-8580-4527-B718-E206D0690635}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C2D6D1A-8580-4527-B718-E206D0690635}.Release|Any CPU.Build.0 = Release|Any CPU + {771651AA-010F-4FF6-9CB2-BAFFE5E04FC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {771651AA-010F-4FF6-9CB2-BAFFE5E04FC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {771651AA-010F-4FF6-9CB2-BAFFE5E04FC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {771651AA-010F-4FF6-9CB2-BAFFE5E04FC2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -300,6 +312,8 @@ Global {B978939B-278C-43A3-AD12-32EA9BBD27D0} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} {D2C68560-C252-41A9-B742-2CEB7D760E0F} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {4172D671-3AAC-443F-9EB0-A6608B154AF0} = {2097345F-4DD3-477D-BC54-A922F9B2B402} + {9C2D6D1A-8580-4527-B718-E206D0690635} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} + {771651AA-010F-4FF6-9CB2-BAFFE5E04FC2} = {2097345F-4DD3-477D-BC54-A922F9B2B402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66} diff --git a/src/OpenTelemetry.Contrib.Extensions.AzureMonitor/ApplicationInsightsSampler.cs b/src/OpenTelemetry.Contrib.Extensions.AzureMonitor/ApplicationInsightsSampler.cs new file mode 100644 index 00000000000..33a03dc8dbf --- /dev/null +++ b/src/OpenTelemetry.Contrib.Extensions.AzureMonitor/ApplicationInsightsSampler.cs @@ -0,0 +1,83 @@ +// +// 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.Trace; + +namespace OpenTelemetry.Contrib.Extensions.AzureMonitor +{ + /// + /// Sample configurable for OpenTelemetry exporters for compatibility + /// with Application Insight SDKs. + /// + public class ApplicationInsightsSampler : Sampler + { + private readonly float samplingRatio; + + /// + /// Initializes a new instance of the class. + /// + /// Ratio of telemetry that should be sampled. + public ApplicationInsightsSampler(float samplingRatio) + { + // Ensure passed ratio is between 0 and 1, inclusive + if (samplingRatio < 0 || samplingRatio > 1) + { + throw new ArgumentOutOfRangeException(nameof(samplingRatio), "Ratio must be between 0 and 1, inclusive."); + } + + this.samplingRatio = samplingRatio; + this.Description = "ApplicationInsightsSampler{" + samplingRatio + "}"; + } + + /// + /// Computational method using the DJB2 Hash algorithm to decide whether to sample + /// a given telemetry item, based on its Trace Id. + /// + /// Parameters of telemetry item used to make sampling decision. + /// Returns whether or not we should sample telemetry in the form of a class. + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + double sampleScore = DJB2SampleScore(samplingParameters.TraceId.ToHexString().ToLowerInvariant()); + return new SamplingResult(sampleScore < this.samplingRatio); + } + + private static double DJB2SampleScore(string traceIdHex) + { + // Calculate DJB2 hash code from hex-converted TraceId + int hash = 5381; + + for (int i = 0; i < traceIdHex.Length; i++) + { + hash = ((hash << 5) + hash) + (int)traceIdHex[i]; + } + + // Take the absolute value of the hash + if (hash == int.MinValue) + { + hash = int.MaxValue; + } + else + { + hash = Math.Abs(hash); + } + + // Divide by MaxValue for value between 0 and 1 for sampling score + double samplingScore = (double)hash / int.MaxValue; + return samplingScore; + } + } +} diff --git a/src/OpenTelemetry.Contrib.Extensions.AzureMonitor/OpenTelemetry.Contrib.Extensions.AzureMonitor.csproj b/src/OpenTelemetry.Contrib.Extensions.AzureMonitor/OpenTelemetry.Contrib.Extensions.AzureMonitor.csproj new file mode 100644 index 00000000000..4e1cb0c7a95 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Extensions.AzureMonitor/OpenTelemetry.Contrib.Extensions.AzureMonitor.csproj @@ -0,0 +1,11 @@ + + + + net461;net5.0 + + + + + + + diff --git a/src/OpenTelemetry.Contrib.Extensions.AzureMonitor/README.md b/src/OpenTelemetry.Contrib.Extensions.AzureMonitor/README.md new file mode 100644 index 00000000000..cedda76b62d --- /dev/null +++ b/src/OpenTelemetry.Contrib.Extensions.AzureMonitor/README.md @@ -0,0 +1,15 @@ +# Application Insights Sampler for OpenTelemetry .NET + +The ```Application Insights Sampler``` should be utilized when +compatibility with Application Insights SDKs is desired, as it +implements the same hash algorithm when deciding to sample telemetry. + +## Installation + +```shell +dotnet add package OpenTelemetry.Contrib.Extensions.AzureMonitor +``` + +## References + +* [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/test/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests/ApplicationInsightsSamplerTests.cs b/test/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests/ApplicationInsightsSamplerTests.cs new file mode 100644 index 00000000000..afa36c32ec7 --- /dev/null +++ b/test/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests/ApplicationInsightsSamplerTests.cs @@ -0,0 +1,93 @@ +// +// 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.Diagnostics; + +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests +{ + public class ApplicationInsightsSamplerTests + { + [Fact] + public void VerifyHashAlgorithmCorrectness() + { + byte[] testBytes = new byte[] + { + 0x8F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, + 0, 0, 0, 0, 0, 0, 0, 0, + }; + byte[] testBytes2 = new byte[] + { + 0x0F, 0x1F, 0x2F, 0x3F, + 0x4F, 0x5F, 0x6F, 0x7F, + 0x8F, 0x9F, 0xAF, 0xBF, + 0xCF, 0xDF, 0xEF, 0xFF, + }; + ActivityTraceId testId = ActivityTraceId.CreateFromBytes(testBytes); + ActivityTraceId testId2 = ActivityTraceId.CreateFromBytes(testBytes2); + + ActivityContext parentContext = default(ActivityContext); + SamplingParameters testParams = new SamplingParameters(parentContext, testId, "TestActivity", ActivityKind.Internal); + SamplingParameters testParams2 = new SamplingParameters(parentContext, testId2, "TestActivity", ActivityKind.Internal); + + var zeroSampler = new ApplicationInsightsSampler(0); + ApplicationInsightsSampler oneSampler = new ApplicationInsightsSampler(1); + + // 0.86 is below the sample score for testId1, but strict enough to drop testId2 + ApplicationInsightsSampler ratioSampler = new ApplicationInsightsSampler(0.86f); + + Assert.Equal(SamplingDecision.Drop, zeroSampler.ShouldSample(testParams).Decision); + Assert.Equal(SamplingDecision.Drop, zeroSampler.ShouldSample(testParams2).Decision); + + Assert.Equal(SamplingDecision.RecordAndSample, oneSampler.ShouldSample(testParams).Decision); + Assert.Equal(SamplingDecision.RecordAndSample, oneSampler.ShouldSample(testParams2).Decision); + + Assert.Equal(SamplingDecision.Drop, ratioSampler.ShouldSample(testParams).Decision); + Assert.Equal(SamplingDecision.RecordAndSample, ratioSampler.ShouldSample(testParams2).Decision); + } + + [Fact] + public void ApplicationInsightsSamplerGoodArgs() + { + ApplicationInsightsSampler pointFiveSampler = new ApplicationInsightsSampler(0.5f); + Assert.NotNull(pointFiveSampler); + + ApplicationInsightsSampler zeroSampler = new ApplicationInsightsSampler(0f); + Assert.NotNull(zeroSampler); + + ApplicationInsightsSampler oneSampler = new ApplicationInsightsSampler(1f); + Assert.NotNull(oneSampler); + } + + [Fact] + public void ApplicationInsightsSamplerBadArgs() + { + Assert.Throws(() => new ApplicationInsightsSampler(-2f)); + Assert.Throws(() => new ApplicationInsightsSampler(2f)); + } + + [Fact] + public void GetDescriptionMatchesSpec() + { + var expectedDescription = "ApplicationInsightsSampler{0.5}"; + Assert.Equal(expectedDescription, new ApplicationInsightsSampler(0.5f).Description); + } + } +} diff --git a/test/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests.csproj b/test/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests.csproj new file mode 100644 index 00000000000..b4a6c0edba3 --- /dev/null +++ b/test/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests/OpenTelemetry.Contrib.Extensions.AzureMonitor.Tests.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + +