From c77546019c88b87cca58f46557a16c1b9434da76 Mon Sep 17 00:00:00 2001 From: Prashant Srivastava Date: Wed, 29 Mar 2023 15:30:26 -0700 Subject: [PATCH 1/5] add rules cache and rule matching logic --- .../.publicApi/net462/PublicAPI.Unshipped.txt | 2 +- .../.publicApi/net6.0/PublicAPI.Unshipped.txt | 2 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 2 +- .../AWSXRayRemoteSampler.cs | 42 ++- .../AWSXRayRemoteSamplerBuilder.cs | 7 +- .../AWSXRaySamplerClient.cs | 3 - src/OpenTelemetry.Sampler.AWS/CHANGELOG.md | 1 + src/OpenTelemetry.Sampler.AWS/Clock.cs | 42 +++ .../FallbackSampler.cs | 31 ++ src/OpenTelemetry.Sampler.AWS/Matcher.cs | 144 +++++++++ .../OpenTelemetry.Sampler.AWS.csproj | 1 + src/OpenTelemetry.Sampler.AWS/README.md | 26 +- src/OpenTelemetry.Sampler.AWS/Reservoir.cs | 60 ++++ src/OpenTelemetry.Sampler.AWS/RulesCache.cs | 151 ++++++++++ src/OpenTelemetry.Sampler.AWS/SamplingRule.cs | 145 +++++++++- src/OpenTelemetry.Sampler.AWS/Statistics.cs | 68 +++++ .../TestAWSXRayRemoteSampler.cs | 15 +- .../TestAWSXRaySamplerClient.cs | 1 - .../TestFallbackSampler.cs | 36 +++ .../TestMatcher.cs | 86 ++++++ .../TestRulesCache.cs | 154 ++++++++++ .../TestSamplingRule.cs | 273 ++++++++++++++++++ test/OpenTelemetry.Sampler.AWS.Tests/Utils.cs | 60 ++++ 23 files changed, 1321 insertions(+), 31 deletions(-) create mode 100644 src/OpenTelemetry.Sampler.AWS/Clock.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/FallbackSampler.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/Matcher.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/Reservoir.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/RulesCache.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/Statistics.cs create mode 100644 test/OpenTelemetry.Sampler.AWS.Tests/TestFallbackSampler.cs create mode 100644 test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs create mode 100644 test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs create mode 100644 test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRule.cs create mode 100644 test/OpenTelemetry.Sampler.AWS.Tests/Utils.cs diff --git a/src/OpenTelemetry.Sampler.AWS/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Sampler.AWS/.publicApi/net462/PublicAPI.Unshipped.txt index a2403cda2c..063bdbf96d 100644 --- a/src/OpenTelemetry.Sampler.AWS/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Sampler.AWS/.publicApi/net462/PublicAPI.Unshipped.txt @@ -6,4 +6,4 @@ OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder.Build() -> OpenTelemetry.S OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder.SetEndpoint(string! endpoint) -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder.SetPollingInterval(System.TimeSpan pollingInterval) -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! override OpenTelemetry.Sampler.AWS.AWSXRayRemoteSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -static OpenTelemetry.Sampler.AWS.AWSXRayRemoteSampler.Builder() -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! +static OpenTelemetry.Sampler.AWS.AWSXRayRemoteSampler.Builder(OpenTelemetry.Resources.Resource! resource) -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! diff --git a/src/OpenTelemetry.Sampler.AWS/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Sampler.AWS/.publicApi/net6.0/PublicAPI.Unshipped.txt index a2403cda2c..063bdbf96d 100644 --- a/src/OpenTelemetry.Sampler.AWS/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Sampler.AWS/.publicApi/net6.0/PublicAPI.Unshipped.txt @@ -6,4 +6,4 @@ OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder.Build() -> OpenTelemetry.S OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder.SetEndpoint(string! endpoint) -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder.SetPollingInterval(System.TimeSpan pollingInterval) -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! override OpenTelemetry.Sampler.AWS.AWSXRayRemoteSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -static OpenTelemetry.Sampler.AWS.AWSXRayRemoteSampler.Builder() -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! +static OpenTelemetry.Sampler.AWS.AWSXRayRemoteSampler.Builder(OpenTelemetry.Resources.Resource! resource) -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! diff --git a/src/OpenTelemetry.Sampler.AWS/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Sampler.AWS/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index a2403cda2c..063bdbf96d 100644 --- a/src/OpenTelemetry.Sampler.AWS/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Sampler.AWS/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -6,4 +6,4 @@ OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder.Build() -> OpenTelemetry.S OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder.SetEndpoint(string! endpoint) -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder.SetPollingInterval(System.TimeSpan pollingInterval) -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! override OpenTelemetry.Sampler.AWS.AWSXRayRemoteSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -static OpenTelemetry.Sampler.AWS.AWSXRayRemoteSampler.Builder() -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! +static OpenTelemetry.Sampler.AWS.AWSXRayRemoteSampler.Builder(OpenTelemetry.Resources.Resource! resource) -> OpenTelemetry.Sampler.AWS.AWSXRayRemoteSamplerBuilder! diff --git a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs index 676bf5bfe6..f049e1b201 100644 --- a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs +++ b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs @@ -15,7 +15,9 @@ // using System; +using System.Collections.Generic; using System.Threading; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace OpenTelemetry.Sampler.AWS; @@ -25,16 +27,21 @@ namespace OpenTelemetry.Sampler.AWS; /// public sealed class AWSXRayRemoteSampler : Trace.Sampler, IDisposable { - internal AWSXRayRemoteSampler(TimeSpan pollingInterval, string endpoint) + internal AWSXRayRemoteSampler(Resource resource, TimeSpan pollingInterval, string endpoint) { + this.Resource = resource; this.PollingInterval = pollingInterval; this.Endpoint = endpoint; this.Client = new AWSXRaySamplerClient(endpoint); // execute the first update right away this.RulePollerTimer = new Timer(this.GetAndUpdateSampler, null, TimeSpan.Zero, this.PollingInterval); + this.RulesCache = new RulesCache(); + this.FallbackSampler = new FallbackSampler(); } + internal Resource Resource { get; } + internal TimeSpan PollingInterval { get; } internal string Endpoint { get; } @@ -43,20 +50,41 @@ internal AWSXRayRemoteSampler(TimeSpan pollingInterval, string endpoint) internal Timer RulePollerTimer { get; } + private RulesCache RulesCache { get; } + + private Trace.Sampler FallbackSampler { get; } + /// /// Initializes a for the sampler. /// + /// an instance of + /// to identify the service attributes for sampling. This resource should + /// be the same as what the OpenTelemetry SDK is configured with. /// an instance of . - public static AWSXRayRemoteSamplerBuilder Builder() + public static AWSXRayRemoteSamplerBuilder Builder(Resource resource) { - return new AWSXRayRemoteSamplerBuilder(); + return new AWSXRayRemoteSamplerBuilder(resource); } /// public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { - // TODO: add the actual functionality for sampling. - throw new System.NotImplementedException(); + if (this.RulesCache.Expired()) + { + return this.FallbackSampler.ShouldSample(samplingParameters); + } + + SamplingRule? matchedRule = this.RulesCache.MatchRule(samplingParameters, this.Resource); + + // ideally this check shouldn't be required + // since the default rule must have matched. + if (matchedRule != null) + { + return matchedRule.Sample(samplingParameters); + } + + // and we shouldn't have reached here if the default rule is present. + return this.FallbackSampler.ShouldSample(samplingParameters); } /// @@ -77,8 +105,8 @@ private void Dispose(bool disposing) private async void GetAndUpdateSampler(object? state) { - await this.Client.GetSamplingRules().ConfigureAwait(false); + List rules = await this.Client.GetSamplingRules().ConfigureAwait(false); - // TODO: more functionality to be added. + this.RulesCache.UpdateRules(rules); } } diff --git a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSamplerBuilder.cs b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSamplerBuilder.cs index d5c39224a3..d68fa08932 100644 --- a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSamplerBuilder.cs +++ b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSamplerBuilder.cs @@ -15,6 +15,7 @@ // using System; +using OpenTelemetry.Resources; namespace OpenTelemetry.Sampler.AWS; @@ -27,11 +28,13 @@ public class AWSXRayRemoteSamplerBuilder private static readonly TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); + private Resource resource; private TimeSpan pollingInterval; private string endpoint; - internal AWSXRayRemoteSamplerBuilder() + internal AWSXRayRemoteSamplerBuilder(Resource resource) { + this.resource = resource; this.pollingInterval = DefaultPollingInterval; this.endpoint = DefaultEndpoint; } @@ -78,6 +81,6 @@ public AWSXRayRemoteSamplerBuilder SetEndpoint(string endpoint) /// an instance of . public AWSXRayRemoteSampler Build() { - return new AWSXRayRemoteSampler(this.pollingInterval, this.endpoint); + return new AWSXRayRemoteSampler(this.resource, this.pollingInterval, this.endpoint); } } diff --git a/src/OpenTelemetry.Sampler.AWS/AWSXRaySamplerClient.cs b/src/OpenTelemetry.Sampler.AWS/AWSXRaySamplerClient.cs index b4756e14f3..1a40673fb5 100644 --- a/src/OpenTelemetry.Sampler.AWS/AWSXRaySamplerClient.cs +++ b/src/OpenTelemetry.Sampler.AWS/AWSXRaySamplerClient.cs @@ -61,9 +61,6 @@ public async Task> GetSamplingRules() } } } - - // TODO: this line here is only for testing. Remove in next more complete iterations. - // Console.WriteLine("Got sampling rules! Count: " + samplingRules.Count); } } catch (Exception ex) diff --git a/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md b/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md index c847e5dc16..6ad421263d 100644 --- a/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md +++ b/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md @@ -6,3 +6,4 @@ Initial release of `OpenTelemetry.Sampler.AWS`. * Feature - AWSXRayRemoteSampler - Add support for AWS X-Ray remote sampling ([#1091](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1091)) + ([#1124](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1124)) diff --git a/src/OpenTelemetry.Sampler.AWS/Clock.cs b/src/OpenTelemetry.Sampler.AWS/Clock.cs new file mode 100644 index 0000000000..5b1f978029 --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/Clock.cs @@ -0,0 +1,42 @@ +// +// 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.Sampler.AWS; + +// A time keeper for the purpose of this sampler. +internal sealed class Clock +{ + private static readonly Clock Instance = new Clock(); + + private Clock() + { + } + + public static Clock GetInstance + { + get + { + return Instance; + } + } + + public static DateTime Now() + { + return DateTime.UtcNow; + } +} diff --git a/src/OpenTelemetry.Sampler.AWS/FallbackSampler.cs b/src/OpenTelemetry.Sampler.AWS/FallbackSampler.cs new file mode 100644 index 0000000000..24e71539bd --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/FallbackSampler.cs @@ -0,0 +1,31 @@ +// +// 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 OpenTelemetry.Trace; + +namespace OpenTelemetry.Sampler.AWS; + +internal class FallbackSampler : Trace.Sampler +{ + private static readonly Trace.Sampler AlwaysOn = new AlwaysOnSampler(); + + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + // For now just do an always on sampler. + // TODO: update to a rate limiting sampler. + return AlwaysOn.ShouldSample(samplingParameters); + } +} diff --git a/src/OpenTelemetry.Sampler.AWS/Matcher.cs b/src/OpenTelemetry.Sampler.AWS/Matcher.cs new file mode 100644 index 0000000000..475ca06c49 --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/Matcher.cs @@ -0,0 +1,144 @@ +// +// 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.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace OpenTelemetry.Sampler.AWS; + +internal static class Matcher +{ + public static readonly Dictionary XRayCloudPlatform = new Dictionary() + { + { "aws_ec2", "AWS::EC2::Instance" }, + { "aws_ecs", "AWS::ECS::Container" }, + { "aws_eks", "AWS::EKS::Container" }, + { "aws_elastic_beanstalk", "AWS::ElasticBeanstalk::Environment" }, + { "aws_lambda", "AWS::Lambda::Function" }, + }; + + public static bool WildcardMatch(string? text, string? globPattern) + { + if (globPattern == "*") + { + return true; + } + + if (text == null || globPattern == null) + { + return false; + } + + if (globPattern.Length == 0) + { + return text.Length == 0; + } + + // it is faster to check if we need a regex comparison than + // doing always regex comparison, even where we may not need it. + foreach (char c in globPattern) + { + if (c == '*' || c == '?') + { + return Regex.IsMatch(text, ToRegexPattern(globPattern)); + } + } + + return string.Equals(text, globPattern, StringComparison.OrdinalIgnoreCase); + } + + public static bool AttributeMatch(IEnumerable>? tags, Dictionary ruleAttributes) + { + if (ruleAttributes.Count == 0) + { + return true; + } + + if (tags == null) + { + return false; + } + + int matchedCount = 0; + + foreach (var tag in tags) + { + var textToMatch = tag.Value?.ToString(); + ruleAttributes.TryGetValue(tag.Key, out var globPattern); + + if (globPattern == null) + { + continue; + } + + if (WildcardMatch(textToMatch, globPattern)) + { + matchedCount++; + } + } + + if (matchedCount == ruleAttributes.Count) + { + return true; + } + + return false; + } + + private static string ToRegexPattern(string globPattern) + { + int tokenStart = -1; + StringBuilder patternBuilder = new StringBuilder(); + + for (int i = 0; i < globPattern.Length; i++) + { + char c = globPattern[i]; + if (c == '*' || c == '?') + { + if (tokenStart != -1) + { + patternBuilder.Append(Regex.Escape(globPattern.Substring(tokenStart, i - tokenStart))); + tokenStart = -1; + } + + if (c == '*') + { + patternBuilder.Append(".*"); + } + else + { + patternBuilder.Append('.'); + } + } + else + { + if (tokenStart == -1) + { + tokenStart = i; + } + } + } + + if (tokenStart != -1) + { + patternBuilder.Append(Regex.Escape(globPattern.Substring(tokenStart))); + } + + return patternBuilder.ToString(); + } +} diff --git a/src/OpenTelemetry.Sampler.AWS/OpenTelemetry.Sampler.AWS.csproj b/src/OpenTelemetry.Sampler.AWS/OpenTelemetry.Sampler.AWS.csproj index ed94f96cc4..c27afd5fff 100644 --- a/src/OpenTelemetry.Sampler.AWS/OpenTelemetry.Sampler.AWS.csproj +++ b/src/OpenTelemetry.Sampler.AWS/OpenTelemetry.Sampler.AWS.csproj @@ -19,6 +19,7 @@ + diff --git a/src/OpenTelemetry.Sampler.AWS/README.md b/src/OpenTelemetry.Sampler.AWS/README.md index f10d151e31..1f77fa519f 100644 --- a/src/OpenTelemetry.Sampler.AWS/README.md +++ b/src/OpenTelemetry.Sampler.AWS/README.md @@ -19,16 +19,28 @@ X-Ray remote sampling](https://aws-otel.github.io/docs/getting-started/remote-sa ```csharp using OpenTelemetry; +using OpenTelemetry.Contrib.Extensions.AWSXRay.Resources; using OpenTelemetry.Contrib.Extensions.AWSXRay.Trace; +using OpenTelemetry.Sampler.AWS; using OpenTelemetry.Trace; -var tracerProvider = Sdk.CreateTracerProviderBuilder() - // other configurations - .SetSampler(AWSXRayRemoteSampler.Builder() - .SetPollingInterval(TimeSpan.FromSeconds(10)) - .SetEndpoint("http://localhost:2000") - .Build()) - .Build(); +var serviceName = "MyServiceName"; + +var resourceBuilder = ResourceBuilder + .CreateDefault() + .AddService(serviceName: serviceName) + .AddDetector(new AWSEC2ResourceDetector()); + +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(serviceName) + .SetResourceBuilder(resourceBuilder) + .AddConsoleExporter() + .AddOtlpExporter() + .SetSampler(AWSXRayRemoteSampler.Builder(resourceBuilder.Build()) // you must provide a resource + .SetPollingInterval(TimeSpan.FromSeconds(5)) + .SetEndpoint("http://localhost:2000") + .Build()) + .Build(); ``` ## References diff --git a/src/OpenTelemetry.Sampler.AWS/Reservoir.cs b/src/OpenTelemetry.Sampler.AWS/Reservoir.cs new file mode 100644 index 0000000000..8cf91eeefd --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/Reservoir.cs @@ -0,0 +1,60 @@ +// +// 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.Threading; + +namespace OpenTelemetry.Sampler.AWS; + +internal class Reservoir : IDisposable +{ + private readonly ReaderWriterLockSlim rwLock; + + public Reservoir(int quota) + { + this.rwLock = new ReaderWriterLockSlim(); + this.Quota = quota; + } + + public int Quota { get; } + + public Reservoir DeepCopy() + { + this.rwLock.EnterReadLock(); + try + { + return new Reservoir(this.Quota); + } + finally + { + this.rwLock.ExitReadLock(); + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + this.rwLock.Dispose(); + } + } +} diff --git a/src/OpenTelemetry.Sampler.AWS/RulesCache.cs b/src/OpenTelemetry.Sampler.AWS/RulesCache.cs new file mode 100644 index 0000000000..06e033ae4d --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/RulesCache.cs @@ -0,0 +1,151 @@ +// +// 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.Collections.Generic; +using System.Linq; +using System.Threading; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Sampler.AWS; + +internal class RulesCache : IDisposable +{ + private const int CacheTTL = 60 * 60; // cache expires 1 hour after the refresh (in sec) + + private readonly ReaderWriterLockSlim rwLock; + + public RulesCache() + { + this.Rules = new Dictionary(); + this.rwLock = new ReaderWriterLockSlim(); + this.Clock = Clock.GetInstance; + } + + public Dictionary Rules { get; internal set; } + + public Clock Clock { get; internal set; } + + public DateTime? UpdatedAt { get; internal set; } + + public void UpdateRules(List newRules) + { + newRules.Sort((x, y) => x.CompareTo(y)); + + var oldRulesCopy = this.DeepCopyRules(); + + foreach (var newRule in newRules) + { + if (oldRulesCopy.TryGetValue(newRule.RuleName, out SamplingRule? oldRule)) + { + if (oldRule is not null) + { + newRule.Reservoir = oldRule.Reservoir; + newRule.Statistics = oldRule.Statistics; + } + } + } + + this.rwLock.EnterWriteLock(); + try + { + this.Rules = newRules.ToDictionary(x => x.RuleName, x => x); + this.UpdatedAt = Clock.Now(); + } + finally + { + this.rwLock.ExitWriteLock(); + } + } + + public bool Expired() + { + this.rwLock.EnterReadLock(); + try + { + if (this.UpdatedAt is null) + { + return true; + } + + return Clock.Now() > this.UpdatedAt.Value.AddSeconds(CacheTTL); + } + finally + { + this.rwLock.ExitReadLock(); + } + } + + public SamplingRule? MatchRule(SamplingParameters samplingParameters, Resource resource) + { + SamplingRule? matchedRule = null; + + this.rwLock.EnterReadLock(); + try + { + foreach (var ruleKeyValue in this.Rules) + { + if (ruleKeyValue.Value.Matches(samplingParameters, resource) || + string.Equals("Default", ruleKeyValue.Key, StringComparison.Ordinal)) + { + matchedRule = ruleKeyValue.Value; + break; + } + } + } + finally + { + this.rwLock.ExitReadLock(); + } + + return matchedRule; + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + this.rwLock.Dispose(); + } + } + + private Dictionary DeepCopyRules() + { + Dictionary copy = new Dictionary(); + + this.rwLock.EnterReadLock(); + try + { + foreach (var ruleKeyValue in this.Rules) + { + copy.Add(ruleKeyValue.Key, ruleKeyValue.Value.DeepCopy()); + } + } + finally + { + this.rwLock.ExitReadLock(); + } + + return copy; + } +} diff --git a/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs b/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs index 1805951b6f..24b23e71b6 100644 --- a/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs +++ b/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs @@ -16,12 +16,18 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; +using System.Threading; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; namespace OpenTelemetry.Sampler.AWS; -internal class SamplingRule : IComparable +internal class SamplingRule : IComparable, IDisposable { + private readonly ReaderWriterLockSlim rwLock; + public SamplingRule( string ruleName, int priority, @@ -31,6 +37,7 @@ public SamplingRule( string httpMethod, string resourceArn, string serviceName, + string serviceType, string urlPath, int version, Dictionary attributes) @@ -43,9 +50,15 @@ public SamplingRule( this.HttpMethod = httpMethod; this.ResourceArn = resourceArn; this.ServiceName = serviceName; + this.ServiceType = serviceType; this.UrlPath = urlPath; this.Version = version; this.Attributes = attributes; + + this.Reservoir = new Reservoir(this.ReservoirSize); + this.Statistics = new Statistics(); + + this.rwLock = new ReaderWriterLockSlim(); } [JsonPropertyName("RuleName")] @@ -72,6 +85,9 @@ public SamplingRule( [JsonPropertyName("ServiceName")] public string ServiceName { get; set; } + [JsonPropertyName("ServiceType")] + public string ServiceType { get; set; } + [JsonPropertyName("URLPath")] public string UrlPath { get; set; } @@ -79,7 +95,81 @@ public SamplingRule( public int Version { get; set; } [JsonPropertyName("Attributes")] - public Dictionary? Attributes { get; set; } + public Dictionary Attributes { get; set; } + + public Reservoir Reservoir { get; internal set; } + + public Statistics Statistics { get; internal set; } + + public bool Matches(SamplingParameters samplingParameters, Resource resource) + { + string? httpTarget = null; + string? httpUrl = null; + string? httpMethod = null; + string? httpHost = null; + + if (samplingParameters.Tags is not null) + { + foreach (var tag in samplingParameters.Tags) + { + if (tag.Key.Equals(SemanticConventions.AttributeHttpTarget, StringComparison.Ordinal)) + { + httpTarget = (string?)tag.Value; + } + else if (tag.Key.Equals(SemanticConventions.AttributeHttpUrl, StringComparison.Ordinal)) + { + httpUrl = (string?)tag.Value; + } + else if (tag.Key.Equals(SemanticConventions.AttributeHttpMethod, StringComparison.Ordinal)) + { + httpMethod = (string?)tag.Value; + } + else if (tag.Key.Equals(SemanticConventions.AttributeHttpHost, StringComparison.Ordinal)) + { + httpHost = (string?)tag.Value; + } + } + } + + // URL path may be in either http.target or http.url + if (httpTarget == null && httpUrl != null) + { + int schemeEndIndex = httpUrl.IndexOf("://", StringComparison.Ordinal); + + // Per spec, http.url is always populated with scheme://host/target. If scheme doesn't + // match, assume it's bad instrumentation and ignore. + if (schemeEndIndex > 0) + { + int pathIndex = httpUrl.IndexOf('/', schemeEndIndex + "://".Length); + if (pathIndex < 0) + { + httpTarget = "/"; + } + else + { + httpTarget = httpUrl.Substring(pathIndex); + } + } + } + + string serviceName = (string)resource.Attributes.FirstOrDefault(kvp => + kvp.Key.Equals("service.name", StringComparison.Ordinal)).Value; + + return Matcher.AttributeMatch(samplingParameters.Tags, this.Attributes) && + Matcher.WildcardMatch(httpTarget, this.UrlPath) && + Matcher.WildcardMatch(httpMethod, this.HttpMethod) && + Matcher.WildcardMatch(httpHost, this.Host) && + Matcher.WildcardMatch(serviceName, this.ServiceName) && + Matcher.WildcardMatch(GetServiceType(resource), this.ServiceType); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "method work in progress")] + public SamplingResult Sample(SamplingParameters samplingParameters) + { + // For now returning a drop sample decision. + // TODO: use reservoir to make a sampling decision. + return new SamplingResult(false); + } public int CompareTo(SamplingRule? other) { @@ -91,4 +181,55 @@ public int CompareTo(SamplingRule? other) return result; } + + public SamplingRule DeepCopy() + { + this.rwLock.EnterReadLock(); + try + { + return new SamplingRule( + this.RuleName, + this.Priority, + this.FixedRate, + this.ReservoirSize, + this.Host, + this.HttpMethod, + this.ResourceArn, + this.ServiceName, + this.ServiceType, + this.UrlPath, + this.Version, + this.Attributes = new Dictionary(this.Attributes)) + { + Reservoir = this.Reservoir.DeepCopy(), + Statistics = this.Statistics.DeepCopy(), + }; + } + finally + { + this.rwLock.ExitReadLock(); + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private static string GetServiceType(Resource resource) + { + string cloudPlatform = (string)resource.Attributes.FirstOrDefault(kvp => + kvp.Key.Equals("cloud.platform", StringComparison.Ordinal)).Value; + + return Matcher.XRayCloudPlatform.TryGetValue(cloudPlatform, out string? value) ? value : string.Empty; + } + + private void Dispose(bool disposing) + { + if (disposing) + { + this.rwLock.Dispose(); + } + } } diff --git a/src/OpenTelemetry.Sampler.AWS/Statistics.cs b/src/OpenTelemetry.Sampler.AWS/Statistics.cs new file mode 100644 index 0000000000..2d63b20b98 --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/Statistics.cs @@ -0,0 +1,68 @@ +// +// 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.Threading; + +namespace OpenTelemetry.Sampler.AWS; + +internal class Statistics : IDisposable +{ + private readonly ReaderWriterLockSlim rwLock; + + public Statistics() + { + this.rwLock = new ReaderWriterLockSlim(); + } + + public int RequestCount { get; internal set; } + + public int BorrowCount { get; internal set; } + + public int SampleCount { get; internal set; } + + public Statistics DeepCopy() + { + this.rwLock.EnterReadLock(); + try + { + return new Statistics() + { + RequestCount = this.RequestCount, + BorrowCount = this.BorrowCount, + SampleCount = this.SampleCount, + }; + } + finally + { + this.rwLock.ExitReadLock(); + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + this.rwLock.Dispose(); + } + } +} diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRayRemoteSampler.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRayRemoteSampler.cs index ee63f9ff9a..5cff06fa5f 100644 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRayRemoteSampler.cs +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRayRemoteSampler.cs @@ -15,7 +15,8 @@ // using System; -using OpenTelemetry.Sampler.AWS; +using System.Collections.Generic; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Xunit; @@ -29,7 +30,7 @@ public void TestSamplerWithConfiguration() TimeSpan pollingInterval = TimeSpan.FromSeconds(5); string endpoint = "http://localhost:3000"; - AWSXRayRemoteSampler sampler = AWSXRayRemoteSampler.Builder() + AWSXRayRemoteSampler sampler = AWSXRayRemoteSampler.Builder(ResourceBuilder.CreateEmpty().Build()) .SetPollingInterval(pollingInterval) .SetEndpoint(endpoint) .Build(); @@ -43,7 +44,7 @@ public void TestSamplerWithConfiguration() [Fact] public void TestSamplerWithDefaults() { - AWSXRayRemoteSampler sampler = AWSXRayRemoteSampler.Builder().Build(); + AWSXRayRemoteSampler sampler = AWSXRayRemoteSampler.Builder(ResourceBuilder.CreateEmpty().Build()).Build(); Assert.Equal(TimeSpan.FromMinutes(5), sampler.PollingInterval); Assert.Equal("http://localhost:2000", sampler.Endpoint); @@ -54,9 +55,11 @@ public void TestSamplerWithDefaults() [Fact] public void TestSamplerShouldSample() { - Trace.Sampler sampler = AWSXRayRemoteSampler.Builder().Build(); + Trace.Sampler sampler = AWSXRayRemoteSampler.Builder(ResourceBuilder.CreateEmpty().Build()).Build(); - // TODO: update the test when the method is implemented. - Assert.Throws(() => sampler.ShouldSample(default(SamplingParameters))); + // for now the fallback sampler should be making the sampling decision + Assert.Equal( + SamplingDecision.RecordAndSample, + sampler.ShouldSample(Utils.CreateSamplingParametersWithTags(new Dictionary())).Decision); } } diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRaySamplerClient.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRaySamplerClient.cs index cf2330c282..2b9863c1f8 100644 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRaySamplerClient.cs +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRaySamplerClient.cs @@ -17,7 +17,6 @@ using System; using System.Collections.Generic; using System.IO; -using OpenTelemetry.Sampler.AWS; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestFallbackSampler.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestFallbackSampler.cs new file mode 100644 index 0000000000..a2598613e6 --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestFallbackSampler.cs @@ -0,0 +1,36 @@ +// +// 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.Collections.Generic; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Sampler.AWS.Tests; + +public class TestFallbackSampler +{ + // this test is temporary. + // replace when fallback sampler has more logic implemented. + [Fact] + public void TestFallbackSamplerAlwaysOn() + { + Trace.Sampler sampler = new FallbackSampler(); + + Assert.Equal( + SamplingDecision.RecordAndSample, + sampler.ShouldSample(Utils.CreateSamplingParametersWithTags(new Dictionary())).Decision); + } +} diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs new file mode 100644 index 0000000000..3fb63b8156 --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs @@ -0,0 +1,86 @@ +// +// 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.Collections.Generic; +using Xunit; + +namespace OpenTelemetry.Sampler.AWS.Tests; + +public class TestMatcher +{ + [Fact] + public void TestWildcardMatching() + { + Assert.True(Matcher.WildcardMatch(null, "*")); + Assert.True(Matcher.WildcardMatch(string.Empty, "*")); + Assert.True(Matcher.WildcardMatch("HelloWorld", "*")); + + Assert.False(Matcher.WildcardMatch(null, "Hello*")); + Assert.False(Matcher.WildcardMatch("HelloWorld", null)); + + Assert.True(Matcher.WildcardMatch("HelloWorld", "HelloWorld")); + Assert.True(Matcher.WildcardMatch("HelloWorld", "Hello*")); + Assert.True(Matcher.WildcardMatch("HelloWorld", "*World")); + Assert.True(Matcher.WildcardMatch("HelloWorld", "?ello*")); + } + + [Fact] + public void TestAttributeMatching() + { + var tags = new List>() + { + new KeyValuePair("dog", "bark"), + new KeyValuePair("cat", "meow"), + new KeyValuePair("cow", "mooo"), + }; + + var ruleAttributes = new Dictionary() + { + { "dog", "bar?" }, + { "cow", "mooo" }, + }; + + Assert.True(Matcher.AttributeMatch(tags, ruleAttributes)); + } + + [Fact] + public void TestAttributeMatchingWithoutRuleAttributes() + { + var tags = new List>() + { + new KeyValuePair("dog", "bark"), + new KeyValuePair("cat", "meow"), + new KeyValuePair("cow", "mooo"), + }; + + var ruleAttributes = new Dictionary(); + + Assert.True(Matcher.AttributeMatch(tags, ruleAttributes)); + } + + [Fact] + public void TestAttributeMatchingWithoutSpanTags() + { + var ruleAttributes = new Dictionary() + { + { "dog", "bar?" }, + { "cow", "mooo" }, + }; + + Assert.False(Matcher.AttributeMatch(new List>(), ruleAttributes)); + Assert.False(Matcher.AttributeMatch(null, ruleAttributes)); + } +} diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs new file mode 100644 index 0000000000..053855e4b0 --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs @@ -0,0 +1,154 @@ +// +// 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.Collections.Generic; +using Xunit; + +namespace OpenTelemetry.Sampler.AWS.Tests; + +public class TestRulesCache +{ + [Fact] + public void TestUpdateRules() + { + // set up rules cache with a default rule + var defaultRule = this.CreateDefaultRule(1, 0.05); + defaultRule.Reservoir = new Reservoir(20); + defaultRule.Statistics = new Statistics() + { + RequestCount = 10, + BorrowCount = 5, + SampleCount = 5, + }; + + var rulesCache = new RulesCache() + { + Rules = new Dictionary() + { + { "Default", defaultRule }, + }, + }; + + // update the default rule + var newDefaultRule = this.CreateDefaultRule(10, 0.20); + rulesCache.UpdateRules(new List { newDefaultRule }); + + // assert that the default rule has been updated + Assert.True(rulesCache.Rules.TryGetValue("Default", out var rule)); + Assert.Equal("Default", rule.RuleName); + Assert.Equal(10, rule.ReservoirSize); + Assert.Equal(0.20, rule.FixedRate); + + // assert that the reservoir and statistics has been copied over to new rule + Assert.Equal(20, rule.Reservoir.Quota); + Assert.Equal(10, rule.Statistics.RequestCount); + Assert.Equal(5, rule.Statistics.BorrowCount); + Assert.Equal(5, rule.Statistics.SampleCount); + } + + [Fact] + public void TestUpdateRulesRemovesOlderRule() + { + // set up rule cache with 2 rules + var rulesCache = new RulesCache() + { + Rules = new Dictionary() + { + { "Default", this.CreateDefaultRule(1, 0.05) }, + { "Rule1", this.CreateRule("Rule1", 5, 0.20, 1) }, + }, + }; + + // the update contains only the default rule + var newDefaultRule = this.CreateDefaultRule(10, 0.20); + rulesCache.UpdateRules(new List { newDefaultRule }); + + // assert that Rule1 doesn't exist in rules cache + Assert.Single(rulesCache.Rules); + Assert.False(rulesCache.Rules.ContainsKey("Rule1")); + Assert.True(rulesCache.Rules.ContainsKey("Default")); + } + + [Fact] + public void TestMatchesWithHigherPriority() + { + // set up rule cache with 3 rules + var rulesCache = new RulesCache(); + + var rules = new List + { + this.CreateDefaultRule(1, 0.05), + this.CreateRule("Rule1", 5, 0.20, 100), + this.CreateRule("Rule2", 10, 0.20, 1), + }; + + rulesCache.UpdateRules(rules); + + var samplingParameters = Utils.CreateSamplingParametersWithTags(new Dictionary()); + var resource = Utils.CreateResource("myServiceName", "aws_ec2"); + + var matchedRule = rulesCache.MatchRule(samplingParameters, resource); + + // assert that the rule with higher priority matched + Assert.Equal("Rule2", matchedRule.RuleName); + } + + [Fact] + public void TestMatchWithSamePriority() + { + // set up rule cache with 3 rules with 2 rules at same priority + var rulesCache = new RulesCache(); + + var rules = new List + { + this.CreateDefaultRule(1, 0.05), + this.CreateRule("Rule1", 5, 0.20, 1), + this.CreateRule("Rule2", 10, 0.20, 1), + }; + + rulesCache.UpdateRules(rules); + + var samplingParameters = Utils.CreateSamplingParametersWithTags(new Dictionary()); + var resource = Utils.CreateResource("myServiceName", "aws_ec2"); + + var matchedRule = rulesCache.MatchRule(samplingParameters, resource); + + // assert that the rule is matched in alphabetical order + Assert.Equal("Rule1", matchedRule.RuleName); + } + + private SamplingRule CreateDefaultRule(int reservoirSize, double fixedRate) + { + return this.CreateRule("Default", reservoirSize, fixedRate, 10000); + } + + private SamplingRule CreateRule(string name, int reservoirSize, double fixedRate, int priority) + { + return new SamplingRule( + ruleName: name, + priority: priority, + fixedRate: fixedRate, + reservoirSize: reservoirSize, + host: "*", + httpMethod: "*", + resourceArn: "*", + serviceName: "*", + serviceType: "*", + urlPath: "*", + version: 1, + attributes: new Dictionary()); + } +} diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRule.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRule.cs new file mode 100644 index 0000000000..e143c30850 --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRule.cs @@ -0,0 +1,273 @@ +// +// 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.Collections.Generic; +using Xunit; + +namespace OpenTelemetry.Sampler.AWS.Tests; + +public class TestSamplingRule +{ + [Fact] + public void TestRuleMatchesWithAllAttributes() + { + var rule = new SamplingRule( + ruleName: "testRule", + priority: 1, + fixedRate: 0.05, + reservoirSize: 1, + host: "localhost", + httpMethod: "GET", + resourceArn: "*", + serviceName: "myServiceName", + serviceType: "AWS::EC2::Instance", + urlPath: "/helloworld", + version: 1, + attributes: new Dictionary()); + + var activityTags = new Dictionary() + { + { "http.host", "localhost" }, + { "http.method", "GET" }, + { "http.url", @"http://127.0.0.1:5000/helloworld" }, + }; + + Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); + } + + [Fact] + public void TestRuleMatchesWithWildcardAttributes() + { + var rule = new SamplingRule( + ruleName: "testRule", + priority: 1, + fixedRate: 0.05, + reservoirSize: 1, + host: "*", + httpMethod: "*", + resourceArn: "*", + serviceName: "myServiceName", + serviceType: "*", + urlPath: "/helloworld", + version: 1, + attributes: new Dictionary()); + + var activityTags = new Dictionary() + { + { "http.host", "localhost" }, + { "http.method", "GET" }, + { "http.url", @"http://127.0.0.1:5000/helloworld" }, + }; + + Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); + } + + [Fact] + public void TestRuleMatchesWithNoActivityAttributes() + { + var rule = new SamplingRule( + ruleName: "testRule", + priority: 1, + fixedRate: 0.05, + reservoirSize: 1, + host: "*", + httpMethod: "*", + resourceArn: "*", + serviceName: "myServiceName", + serviceType: "*", + urlPath: "/helloworld", + version: 1, + attributes: new Dictionary()); + + var activityTags = new Dictionary(); + + Assert.False(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); + } + + [Fact] + public void TestRuleMatchesWithNoActivityAttributesAndWildcardRules() + { + var rule = new SamplingRule( + ruleName: "testRule", + priority: 1, + fixedRate: 0.05, + reservoirSize: 1, + host: "*", + httpMethod: "*", + resourceArn: "*", + serviceName: "myServiceName", + serviceType: "*", + urlPath: "*", + version: 1, + attributes: new Dictionary()); + + var activityTags = new Dictionary(); + + Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); + } + + [Fact] + public void TestRuleMatchesWithHttpTarget() + { + var rule = new SamplingRule( + ruleName: "testRule", + priority: 1, + fixedRate: 0.05, + reservoirSize: 1, + host: "*", + httpMethod: "*", + resourceArn: "*", + serviceName: "*", + serviceType: "*", + urlPath: "/hello*", + version: 1, + attributes: new Dictionary()); + + var activityTags = new Dictionary() + { + { "http.target", "/helloworld" }, + }; + + Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", string.Empty))); + } + + [Fact] + public void TestAttributeMatching() + { + var ruleAttributes = new Dictionary() + { + { "dog", "bark" }, + { "cat", "meow" }, + }; + + var rule = new SamplingRule( + ruleName: "testRule", + priority: 1, + fixedRate: 0.05, + reservoirSize: 1, + host: "*", + httpMethod: "*", + resourceArn: "*", + serviceName: "*", + serviceType: "*", + urlPath: "*", + version: 1, + attributes: ruleAttributes); + + var activityTags = new Dictionary() + { + { "http.target", "/helloworld" }, + { "dog", "bark" }, + { "cat", "meow" }, + }; + + Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ecs"))); + } + + [Fact] + public void TestAttributeMatchingWithLessActivityTags() + { + var ruleAttributes = new Dictionary() + { + { "dog", "bark" }, + { "cat", "meow" }, + }; + + var rule = new SamplingRule( + ruleName: "testRule", + priority: 1, + fixedRate: 0.05, + reservoirSize: 1, + host: "*", + httpMethod: "*", + resourceArn: "*", + serviceName: "*", + serviceType: "*", + urlPath: "*", + version: 1, + attributes: ruleAttributes); + + var activityTags = new Dictionary() + { + { "http.target", "/helloworld" }, + { "dog", "bark" }, + }; + + Assert.False(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ecs"))); + } + + [Fact] + public void TestDeepCopy() + { + var ruleAttributes = new Dictionary() + { + { "dog", "bark" }, + { "cat", "meow" }, + }; + + var rule = new SamplingRule( + ruleName: "testRule", + priority: 1, + fixedRate: 0.05, + reservoirSize: 1, + host: "*", + httpMethod: "*", + resourceArn: "*", + serviceName: "myServiceName", + serviceType: "*", + urlPath: "*", + version: 1, + attributes: ruleAttributes) + { + Reservoir = new Reservoir(10), + }; + + var copy = rule.DeepCopy(); + + // assert that the objects are actually different + Assert.NotEqual(rule.GetHashCode(), copy.GetHashCode()); + Assert.NotEqual(rule.Reservoir.GetHashCode(), copy.Reservoir.GetHashCode()); + Assert.NotEqual(rule.Statistics.GetHashCode(), copy.Statistics.GetHashCode()); + + // assert that the property values are same + Assert.Equal(rule.RuleName, copy.RuleName); + Assert.Equal(rule.Priority, copy.Priority); + Assert.Equal(rule.FixedRate, copy.FixedRate); + Assert.Equal(rule.ReservoirSize, copy.ReservoirSize); + Assert.Equal(rule.Host, copy.Host); + Assert.Equal(rule.HttpMethod, copy.HttpMethod); + Assert.Equal(rule.ResourceArn, copy.ResourceArn); + Assert.Equal(rule.ServiceName, copy.ServiceName); + Assert.Equal(rule.ServiceType, copy.ServiceType); + Assert.Equal(rule.UrlPath, copy.UrlPath); + Assert.Equal(rule.Version, copy.Version); + Assert.True(this.CompareDicts(rule.Attributes, copy.Attributes)); + Assert.Equal(rule.Reservoir.Quota, copy.Reservoir.Quota); + } + + private bool CompareDicts(Dictionary d1, Dictionary d2) + { + foreach (var item in d1) + { + if (!d2.TryGetValue(item.Key, out var value) || value != item.Value) + { + return false; + } + } + + return true; + } +} diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/Utils.cs b/test/OpenTelemetry.Sampler.AWS.Tests/Utils.cs new file mode 100644 index 0000000000..d5a1babd46 --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/Utils.cs @@ -0,0 +1,60 @@ +// +// 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.Collections.Generic; +using System.Diagnostics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Sampler.AWS.Tests; + +internal static class Utils +{ + internal static SamplingParameters CreateSamplingParametersWithTags(Dictionary tags) + { + ActivityTraceId traceId = ActivityTraceId.CreateRandom(); + ActivitySpanId parentSpanId = ActivitySpanId.CreateRandom(); + ActivityTraceFlags traceFlags = ActivityTraceFlags.None; + + var parentContext = new ActivityContext(traceId, parentSpanId, traceFlags); + + var tagList = new List>(); + + foreach (var tag in tags) + { + tagList.Add(new KeyValuePair(tag.Key, tag.Value)); + } + + return new SamplingParameters( + parentContext, + traceId, + "myActivityName", + ActivityKind.Server, + tagList, + null); + } + + internal static Resource CreateResource(string serviceName, string cloudPlatform) + { + var resourceAttributes = new List>() + { + new KeyValuePair("service.name", serviceName), + new KeyValuePair("cloud.platform", cloudPlatform), + }; + + return ResourceBuilder.CreateEmpty().AddAttributes(resourceAttributes).Build(); + } +} From e1d16753750ed947c1d4cd0476166e9a4aa31b26 Mon Sep 17 00:00:00 2001 From: Prashant Srivastava Date: Sat, 15 Apr 2023 23:42:26 -0700 Subject: [PATCH 2/5] rewrite using less mutable states and locking --- .../AWSXRayRemoteSampler.cs | 79 ++++++---- .../AWSXRayRemoteSamplerBuilder.cs | 18 ++- src/OpenTelemetry.Sampler.AWS/Clock.cs | 24 ++- .../FallbackSampler.cs | 7 + src/OpenTelemetry.Sampler.AWS/Matcher.cs | 2 +- src/OpenTelemetry.Sampler.AWS/Reservoir.cs | 60 -------- src/OpenTelemetry.Sampler.AWS/RulesCache.cs | 120 +++++++-------- src/OpenTelemetry.Sampler.AWS/SamplingRule.cs | 138 +----------------- .../SamplingRuleApplier.cs | 129 ++++++++++++++++ src/OpenTelemetry.Sampler.AWS/Statistics.cs | 44 +----- src/OpenTelemetry.Sampler.AWS/SystemClock.cs | 62 ++++++++ .../TestClock.cs | 58 ++++++++ .../TestFallbackSampler.cs | 36 ----- .../TestRulesCache.cs | 101 +++++-------- ...lingRule.cs => TestSamplingRuleApplier.cs} | 87 +++-------- 15 files changed, 442 insertions(+), 523 deletions(-) delete mode 100644 src/OpenTelemetry.Sampler.AWS/Reservoir.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/SystemClock.cs create mode 100644 test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs delete mode 100644 test/OpenTelemetry.Sampler.AWS.Tests/TestFallbackSampler.cs rename test/OpenTelemetry.Sampler.AWS.Tests/{TestSamplingRule.cs => TestSamplingRuleApplier.cs} (63%) diff --git a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs index f049e1b201..6b3653fc68 100644 --- a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs +++ b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -27,32 +28,48 @@ namespace OpenTelemetry.Sampler.AWS; /// public sealed class AWSXRayRemoteSampler : Trace.Sampler, IDisposable { - internal AWSXRayRemoteSampler(Resource resource, TimeSpan pollingInterval, string endpoint) + internal static readonly TimeSpan DefaultTargetInterval = TimeSpan.FromSeconds(10); + + private static readonly Random Random = new Random(); + + [SuppressMessage("Performance", "CA5394: Do not use insecure randomness", Justification = "Secure random is not required for jitters.")] + internal AWSXRayRemoteSampler(Resource resource, TimeSpan pollingInterval, string endpoint, Clock clock) { this.Resource = resource; this.PollingInterval = pollingInterval; this.Endpoint = endpoint; - this.Client = new AWSXRaySamplerClient(endpoint); + this.Clock = clock; + this.ClientId = GenerateClientId(); + this.Client = new AWSXRaySamplerClient(this.Endpoint); + this.FallbackSampler = new FallbackSampler(this.Clock); + this.RulesCache = new RulesCache(this.Clock, this.ClientId, this.Resource, this.FallbackSampler); + + // upto 5 seconds of jitter for rule polling + this.RulePollerJitter = TimeSpan.FromMilliseconds(Random.Next(1, 5000)); - // execute the first update right away - this.RulePollerTimer = new Timer(this.GetAndUpdateSampler, null, TimeSpan.Zero, this.PollingInterval); - this.RulesCache = new RulesCache(); - this.FallbackSampler = new FallbackSampler(); + // execute the first update right away and schedule subsequent update later. + this.RulePollerTimer = new Timer(this.GetAndUpdateRules, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); } - internal Resource Resource { get; } + internal TimeSpan RulePollerJitter { get; set; } + + internal Clock Clock { get; set; } + + internal string ClientId { get; set; } - internal TimeSpan PollingInterval { get; } + internal Resource Resource { get; set; } - internal string Endpoint { get; } + internal string Endpoint { get; set; } - internal AWSXRaySamplerClient Client { get; } + internal AWSXRaySamplerClient Client { get; set; } - internal Timer RulePollerTimer { get; } + internal RulesCache RulesCache { get; set; } - private RulesCache RulesCache { get; } + internal Timer RulePollerTimer { get; set; } - private Trace.Sampler FallbackSampler { get; } + internal TimeSpan PollingInterval { get; set; } + + internal Trace.Sampler FallbackSampler { get; set; } /// /// Initializes a for the sampler. @@ -71,20 +88,10 @@ public override SamplingResult ShouldSample(in SamplingParameters samplingParame { if (this.RulesCache.Expired()) { - return this.FallbackSampler.ShouldSample(samplingParameters); + return this.FallbackSampler.ShouldSample(in samplingParameters); } - SamplingRule? matchedRule = this.RulesCache.MatchRule(samplingParameters, this.Resource); - - // ideally this check shouldn't be required - // since the default rule must have matched. - if (matchedRule != null) - { - return matchedRule.Sample(samplingParameters); - } - - // and we shouldn't have reached here if the default rule is present. - return this.FallbackSampler.ShouldSample(samplingParameters); + return this.RulesCache.ShouldSample(in samplingParameters); } /// @@ -94,19 +101,39 @@ public void Dispose() GC.SuppressFinalize(this); } + [SuppressMessage( + "Usage", + "CA5394: Do not use insecure randomness", + Justification = "using insecure random is fine here since clientId doesn't need to be secure.")] + private static string GenerateClientId() + { + char[] hex = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + char[] clientIdChars = new char[24]; + for (int i = 0; i < clientIdChars.Length; i++) + { + clientIdChars[i] = hex[Random.Next(hex.Length)]; + } + + return new string(clientIdChars); + } + private void Dispose(bool disposing) { if (disposing) { this.RulePollerTimer?.Dispose(); this.Client?.Dispose(); + this.RulesCache?.Dispose(); } } - private async void GetAndUpdateSampler(object? state) + private async void GetAndUpdateRules(object? state) { List rules = await this.Client.GetSamplingRules().ConfigureAwait(false); this.RulesCache.UpdateRules(rules); + + // schedule the next rule poll. + this.RulePollerTimer.Change(this.PollingInterval.Add(this.RulePollerJitter), Timeout.InfiniteTimeSpan); } } diff --git a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSamplerBuilder.cs b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSamplerBuilder.cs index d68fa08932..0aa3aa32d8 100644 --- a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSamplerBuilder.cs +++ b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSamplerBuilder.cs @@ -31,12 +31,14 @@ public class AWSXRayRemoteSamplerBuilder private Resource resource; private TimeSpan pollingInterval; private string endpoint; + private Clock clock; internal AWSXRayRemoteSamplerBuilder(Resource resource) { this.resource = resource; this.pollingInterval = DefaultPollingInterval; this.endpoint = DefaultEndpoint; + this.clock = Clock.GetDefault(); } /// @@ -81,6 +83,20 @@ public AWSXRayRemoteSamplerBuilder SetEndpoint(string endpoint) /// an instance of . public AWSXRayRemoteSampler Build() { - return new AWSXRayRemoteSampler(this.resource, this.pollingInterval, this.endpoint); + return new AWSXRayRemoteSampler(this.resource, this.pollingInterval, this.endpoint, this.clock); + } + + // This is intended for testing with a mock clock. + // Should not be exposed to public. + internal AWSXRayRemoteSamplerBuilder SetClock(Clock clock) + { + if (clock == null) + { + throw new ArgumentNullException(nameof(clock)); + } + + this.clock = clock; + + return this; } } diff --git a/src/OpenTelemetry.Sampler.AWS/Clock.cs b/src/OpenTelemetry.Sampler.AWS/Clock.cs index 5b1f978029..39218fbdf2 100644 --- a/src/OpenTelemetry.Sampler.AWS/Clock.cs +++ b/src/OpenTelemetry.Sampler.AWS/Clock.cs @@ -19,24 +19,18 @@ namespace OpenTelemetry.Sampler.AWS; // A time keeper for the purpose of this sampler. -internal sealed class Clock +internal abstract class Clock { - private static readonly Clock Instance = new Clock(); - - private Clock() + public static Clock GetDefault() { + return SystemClock.GetInstance(); } - public static Clock GetInstance - { - get - { - return Instance; - } - } + public abstract DateTime Now(); - public static DateTime Now() - { - return DateTime.UtcNow; - } + public abstract long NowInSeconds(); + + public abstract DateTime ToDateTime(double seconds); + + public abstract double ToDouble(DateTime dateTime); } diff --git a/src/OpenTelemetry.Sampler.AWS/FallbackSampler.cs b/src/OpenTelemetry.Sampler.AWS/FallbackSampler.cs index 24e71539bd..3917c407fe 100644 --- a/src/OpenTelemetry.Sampler.AWS/FallbackSampler.cs +++ b/src/OpenTelemetry.Sampler.AWS/FallbackSampler.cs @@ -22,6 +22,13 @@ internal class FallbackSampler : Trace.Sampler { private static readonly Trace.Sampler AlwaysOn = new AlwaysOnSampler(); + private readonly Clock clock; + + public FallbackSampler(Clock clock) + { + this.clock = clock; + } + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) { // For now just do an always on sampler. diff --git a/src/OpenTelemetry.Sampler.AWS/Matcher.cs b/src/OpenTelemetry.Sampler.AWS/Matcher.cs index 475ca06c49..e135293e9f 100644 --- a/src/OpenTelemetry.Sampler.AWS/Matcher.cs +++ b/src/OpenTelemetry.Sampler.AWS/Matcher.cs @@ -23,7 +23,7 @@ namespace OpenTelemetry.Sampler.AWS; internal static class Matcher { - public static readonly Dictionary XRayCloudPlatform = new Dictionary() + public static readonly IReadOnlyDictionary XRayCloudPlatform = new Dictionary() { { "aws_ec2", "AWS::EC2::Instance" }, { "aws_ecs", "AWS::ECS::Container" }, diff --git a/src/OpenTelemetry.Sampler.AWS/Reservoir.cs b/src/OpenTelemetry.Sampler.AWS/Reservoir.cs deleted file mode 100644 index 8cf91eeefd..0000000000 --- a/src/OpenTelemetry.Sampler.AWS/Reservoir.cs +++ /dev/null @@ -1,60 +0,0 @@ -// -// 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.Threading; - -namespace OpenTelemetry.Sampler.AWS; - -internal class Reservoir : IDisposable -{ - private readonly ReaderWriterLockSlim rwLock; - - public Reservoir(int quota) - { - this.rwLock = new ReaderWriterLockSlim(); - this.Quota = quota; - } - - public int Quota { get; } - - public Reservoir DeepCopy() - { - this.rwLock.EnterReadLock(); - try - { - return new Reservoir(this.Quota); - } - finally - { - this.rwLock.ExitReadLock(); - } - } - - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - this.rwLock.Dispose(); - } - } -} diff --git a/src/OpenTelemetry.Sampler.AWS/RulesCache.cs b/src/OpenTelemetry.Sampler.AWS/RulesCache.cs index 06e033ae4d..be057b7322 100644 --- a/src/OpenTelemetry.Sampler.AWS/RulesCache.cs +++ b/src/OpenTelemetry.Sampler.AWS/RulesCache.cs @@ -29,90 +29,83 @@ internal class RulesCache : IDisposable private readonly ReaderWriterLockSlim rwLock; - public RulesCache() + public RulesCache(Clock clock, string clientId, Resource resource, Trace.Sampler fallbackSampler) { - this.Rules = new Dictionary(); this.rwLock = new ReaderWriterLockSlim(); - this.Clock = Clock.GetInstance; + this.Clock = clock; + this.ClientId = clientId; + this.Resource = resource; + this.FallbackSampler = fallbackSampler; + this.RuleAppliers = new List(); + this.UpdatedAt = this.Clock.Now(); } - public Dictionary Rules { get; internal set; } + internal Clock Clock { get; set; } - public Clock Clock { get; internal set; } + internal string ClientId { get; set; } - public DateTime? UpdatedAt { get; internal set; } + internal Resource Resource { get; set; } - public void UpdateRules(List newRules) - { - newRules.Sort((x, y) => x.CompareTo(y)); + internal Trace.Sampler FallbackSampler { get; set; } - var oldRulesCopy = this.DeepCopyRules(); + internal List RuleAppliers { get; set; } - foreach (var newRule in newRules) - { - if (oldRulesCopy.TryGetValue(newRule.RuleName, out SamplingRule? oldRule)) - { - if (oldRule is not null) - { - newRule.Reservoir = oldRule.Reservoir; - newRule.Statistics = oldRule.Statistics; - } - } - } + internal DateTime UpdatedAt { get; set; } - this.rwLock.EnterWriteLock(); + public bool Expired() + { + this.rwLock.EnterReadLock(); try { - this.Rules = newRules.ToDictionary(x => x.RuleName, x => x); - this.UpdatedAt = Clock.Now(); + return this.Clock.Now() > this.UpdatedAt.AddSeconds(CacheTTL); } finally { - this.rwLock.ExitWriteLock(); + this.rwLock.ExitReadLock(); } } - public bool Expired() + public void UpdateRules(List newRules) { - this.rwLock.EnterReadLock(); - try + // sort the new rules + newRules.Sort((x, y) => x.CompareTo(y)); + + List newRuleAppliers = new List(); + foreach (var rule in newRules) { - if (this.UpdatedAt is null) - { - return true; - } + var currentStatistics = this.RuleAppliers + .FirstOrDefault(currentApplier => currentApplier.RuleName == rule.RuleName) + ?.Statistics ?? new Statistics(); - return Clock.Now() > this.UpdatedAt.Value.AddSeconds(CacheTTL); + var ruleApplier = new SamplingRuleApplier(this.ClientId, this.Clock, rule, currentStatistics); + newRuleAppliers.Add(ruleApplier); + } + + this.rwLock.EnterWriteLock(); + try + { + this.RuleAppliers = newRuleAppliers; + this.UpdatedAt = this.Clock.Now(); } finally { - this.rwLock.ExitReadLock(); + this.rwLock.ExitWriteLock(); } } - public SamplingRule? MatchRule(SamplingParameters samplingParameters, Resource resource) + public SamplingResult ShouldSample(in SamplingParameters samplingParameters) { - SamplingRule? matchedRule = null; - - this.rwLock.EnterReadLock(); - try + foreach (var ruleApplier in this.RuleAppliers) { - foreach (var ruleKeyValue in this.Rules) + if (ruleApplier.Matches(samplingParameters, this.Resource)) { - if (ruleKeyValue.Value.Matches(samplingParameters, resource) || - string.Equals("Default", ruleKeyValue.Key, StringComparison.Ordinal)) - { - matchedRule = ruleKeyValue.Value; - break; - } + return ruleApplier.ShouldSample(in samplingParameters); } } - finally - { - this.rwLock.ExitReadLock(); - } - return matchedRule; + // ideally the default rule should have matched. + // if we are here then likely due to a bug. + return this.FallbackSampler.ShouldSample(in samplingParameters); } public void Dispose() @@ -121,31 +114,24 @@ public void Dispose() GC.SuppressFinalize(this); } - private void Dispose(bool disposing) - { - if (disposing) - { - this.rwLock.Dispose(); - } - } - - private Dictionary DeepCopyRules() + internal DateTime GetUpdatedAt() { - Dictionary copy = new Dictionary(); - this.rwLock.EnterReadLock(); try { - foreach (var ruleKeyValue in this.Rules) - { - copy.Add(ruleKeyValue.Key, ruleKeyValue.Value.DeepCopy()); - } + return this.UpdatedAt; } finally { this.rwLock.ExitReadLock(); } + } - return copy; + private void Dispose(bool disposing) + { + if (disposing) + { + this.rwLock.Dispose(); + } } } diff --git a/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs b/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs index 24b23e71b6..c4907675b5 100644 --- a/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs +++ b/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs @@ -16,18 +16,12 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text.Json.Serialization; -using System.Threading; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; namespace OpenTelemetry.Sampler.AWS; -internal class SamplingRule : IComparable, IDisposable +internal class SamplingRule : IComparable { - private readonly ReaderWriterLockSlim rwLock; - public SamplingRule( string ruleName, int priority, @@ -54,11 +48,6 @@ public SamplingRule( this.UrlPath = urlPath; this.Version = version; this.Attributes = attributes; - - this.Reservoir = new Reservoir(this.ReservoirSize); - this.Statistics = new Statistics(); - - this.rwLock = new ReaderWriterLockSlim(); } [JsonPropertyName("RuleName")] @@ -97,80 +86,6 @@ public SamplingRule( [JsonPropertyName("Attributes")] public Dictionary Attributes { get; set; } - public Reservoir Reservoir { get; internal set; } - - public Statistics Statistics { get; internal set; } - - public bool Matches(SamplingParameters samplingParameters, Resource resource) - { - string? httpTarget = null; - string? httpUrl = null; - string? httpMethod = null; - string? httpHost = null; - - if (samplingParameters.Tags is not null) - { - foreach (var tag in samplingParameters.Tags) - { - if (tag.Key.Equals(SemanticConventions.AttributeHttpTarget, StringComparison.Ordinal)) - { - httpTarget = (string?)tag.Value; - } - else if (tag.Key.Equals(SemanticConventions.AttributeHttpUrl, StringComparison.Ordinal)) - { - httpUrl = (string?)tag.Value; - } - else if (tag.Key.Equals(SemanticConventions.AttributeHttpMethod, StringComparison.Ordinal)) - { - httpMethod = (string?)tag.Value; - } - else if (tag.Key.Equals(SemanticConventions.AttributeHttpHost, StringComparison.Ordinal)) - { - httpHost = (string?)tag.Value; - } - } - } - - // URL path may be in either http.target or http.url - if (httpTarget == null && httpUrl != null) - { - int schemeEndIndex = httpUrl.IndexOf("://", StringComparison.Ordinal); - - // Per spec, http.url is always populated with scheme://host/target. If scheme doesn't - // match, assume it's bad instrumentation and ignore. - if (schemeEndIndex > 0) - { - int pathIndex = httpUrl.IndexOf('/', schemeEndIndex + "://".Length); - if (pathIndex < 0) - { - httpTarget = "/"; - } - else - { - httpTarget = httpUrl.Substring(pathIndex); - } - } - } - - string serviceName = (string)resource.Attributes.FirstOrDefault(kvp => - kvp.Key.Equals("service.name", StringComparison.Ordinal)).Value; - - return Matcher.AttributeMatch(samplingParameters.Tags, this.Attributes) && - Matcher.WildcardMatch(httpTarget, this.UrlPath) && - Matcher.WildcardMatch(httpMethod, this.HttpMethod) && - Matcher.WildcardMatch(httpHost, this.Host) && - Matcher.WildcardMatch(serviceName, this.ServiceName) && - Matcher.WildcardMatch(GetServiceType(resource), this.ServiceType); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "method work in progress")] - public SamplingResult Sample(SamplingParameters samplingParameters) - { - // For now returning a drop sample decision. - // TODO: use reservoir to make a sampling decision. - return new SamplingResult(false); - } - public int CompareTo(SamplingRule? other) { int result = this.Priority.CompareTo(other?.Priority); @@ -181,55 +96,4 @@ public int CompareTo(SamplingRule? other) return result; } - - public SamplingRule DeepCopy() - { - this.rwLock.EnterReadLock(); - try - { - return new SamplingRule( - this.RuleName, - this.Priority, - this.FixedRate, - this.ReservoirSize, - this.Host, - this.HttpMethod, - this.ResourceArn, - this.ServiceName, - this.ServiceType, - this.UrlPath, - this.Version, - this.Attributes = new Dictionary(this.Attributes)) - { - Reservoir = this.Reservoir.DeepCopy(), - Statistics = this.Statistics.DeepCopy(), - }; - } - finally - { - this.rwLock.ExitReadLock(); - } - } - - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private static string GetServiceType(Resource resource) - { - string cloudPlatform = (string)resource.Attributes.FirstOrDefault(kvp => - kvp.Key.Equals("cloud.platform", StringComparison.Ordinal)).Value; - - return Matcher.XRayCloudPlatform.TryGetValue(cloudPlatform, out string? value) ? value : string.Empty; - } - - private void Dispose(bool disposing) - { - if (disposing) - { - this.rwLock.Dispose(); - } - } } diff --git a/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs b/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs new file mode 100644 index 0000000000..f9e96b764e --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs @@ -0,0 +1,129 @@ +// +// 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.CodeAnalysis; +using System.Linq; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Sampler.AWS; + +internal class SamplingRuleApplier +{ + public SamplingRuleApplier(string clientId, Clock clock, SamplingRule rule, Statistics statistics) + { + this.ClientId = clientId; + this.Clock = clock; + this.Rule = rule; + this.RuleName = this.Rule.RuleName; + this.Statistics = statistics ?? new Statistics(); + } + + internal string ClientId { get; set; } + + internal SamplingRule Rule { get; set; } + + internal string RuleName { get; set; } + + internal Clock Clock { get; set; } + + internal Statistics Statistics { get; set; } + + // check if this rule applier matches the request + public bool Matches(SamplingParameters samplingParameters, Resource resource) + { + string? httpTarget = null; + string? httpUrl = null; + string? httpMethod = null; + string? httpHost = null; + + if (samplingParameters.Tags is not null) + { + foreach (var tag in samplingParameters.Tags) + { + if (tag.Key.Equals(SemanticConventions.AttributeHttpTarget, StringComparison.Ordinal)) + { + httpTarget = (string?)tag.Value; + } + else if (tag.Key.Equals(SemanticConventions.AttributeHttpUrl, StringComparison.Ordinal)) + { + httpUrl = (string?)tag.Value; + } + else if (tag.Key.Equals(SemanticConventions.AttributeHttpMethod, StringComparison.Ordinal)) + { + httpMethod = (string?)tag.Value; + } + else if (tag.Key.Equals(SemanticConventions.AttributeHttpHost, StringComparison.Ordinal)) + { + httpHost = (string?)tag.Value; + } + } + } + + // URL path may be in either http.target or http.url + if (httpTarget == null && httpUrl != null) + { + int schemeEndIndex = httpUrl.IndexOf("://", StringComparison.Ordinal); + + // Per spec, http.url is always populated with scheme://host/target. If scheme doesn't + // match, assume it's bad instrumentation and ignore. + if (schemeEndIndex > 0) + { + int pathIndex = httpUrl.IndexOf('/', schemeEndIndex + "://".Length); + if (pathIndex < 0) + { + httpTarget = "/"; + } + else + { + httpTarget = httpUrl.Substring(pathIndex); + } + } + } + + string serviceName = (string)resource.Attributes.FirstOrDefault(kvp => + kvp.Key.Equals("service.name", StringComparison.Ordinal)).Value; + + return Matcher.AttributeMatch(samplingParameters.Tags, this.Rule.Attributes) && + Matcher.WildcardMatch(httpTarget, this.Rule.UrlPath) && + Matcher.WildcardMatch(httpMethod, this.Rule.HttpMethod) && + Matcher.WildcardMatch(httpHost, this.Rule.Host) && + Matcher.WildcardMatch(serviceName, this.Rule.ServiceName) && + Matcher.WildcardMatch(GetServiceType(resource), this.Rule.ServiceType); + } + + [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "method work in progress")] + public SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + // for now return drop sampling result. + // TODO: use reservoir and fixed rate sampler + return new SamplingResult(false); + } + + private static string GetServiceType(Resource resource) + { + string cloudPlatform = (string)resource.Attributes.FirstOrDefault(kvp => + kvp.Key.Equals("cloud.platform", StringComparison.Ordinal)).Value; + + if (cloudPlatform == null) + { + return string.Empty; + } + + return Matcher.XRayCloudPlatform.TryGetValue(cloudPlatform, out string? value) ? value : string.Empty; + } +} diff --git a/src/OpenTelemetry.Sampler.AWS/Statistics.cs b/src/OpenTelemetry.Sampler.AWS/Statistics.cs index 2d63b20b98..9b790eb0e8 100644 --- a/src/OpenTelemetry.Sampler.AWS/Statistics.cs +++ b/src/OpenTelemetry.Sampler.AWS/Statistics.cs @@ -14,55 +14,13 @@ // limitations under the License. // -using System; -using System.Threading; - namespace OpenTelemetry.Sampler.AWS; -internal class Statistics : IDisposable +internal class Statistics { - private readonly ReaderWriterLockSlim rwLock; - - public Statistics() - { - this.rwLock = new ReaderWriterLockSlim(); - } - public int RequestCount { get; internal set; } public int BorrowCount { get; internal set; } public int SampleCount { get; internal set; } - - public Statistics DeepCopy() - { - this.rwLock.EnterReadLock(); - try - { - return new Statistics() - { - RequestCount = this.RequestCount, - BorrowCount = this.BorrowCount, - SampleCount = this.SampleCount, - }; - } - finally - { - this.rwLock.ExitReadLock(); - } - } - - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - this.rwLock.Dispose(); - } - } } diff --git a/src/OpenTelemetry.Sampler.AWS/SystemClock.cs b/src/OpenTelemetry.Sampler.AWS/SystemClock.cs new file mode 100644 index 0000000000..d2ec6eb090 --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/SystemClock.cs @@ -0,0 +1,62 @@ +// +// 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; + +namespace OpenTelemetry.Sampler.AWS; + +// A clock based on System time. +internal class SystemClock : Clock +{ + private static readonly SystemClock Instance = new SystemClock(); + + private static readonly DateTime EpochStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + private SystemClock() + { + } + + public static Clock GetInstance() + { + return Instance; + } + + public override DateTime Now() + { + return DateTime.UtcNow; + } + + public override long NowInSeconds() + { + double ts = Stopwatch.GetTimestamp(); + double s = ts / Stopwatch.Frequency; + + return (long)s; + } + + public override DateTime ToDateTime(double seconds) + { + return EpochStart.AddSeconds(seconds); + } + + public override double ToDouble(DateTime dateTime) + { + var current = new TimeSpan(dateTime.ToUniversalTime().Ticks - EpochStart.Ticks); + double timestamp = Math.Round(current.TotalMilliseconds, 0) / 1000.0; + return timestamp; + } +} diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs new file mode 100644 index 0000000000..344cd30d9f --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs @@ -0,0 +1,58 @@ +// +// 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.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenTelemetry.Sampler.AWS.Tests; + +internal class TestClock : Clock +{ + private DateTime nowTime; + + public TestClock() + { + this.nowTime = DateTime.Now; + } + + public TestClock(DateTime time) + { + this.nowTime = time; + } + + public override DateTime Now() + { + return this.nowTime; + } + + public override long NowInSeconds() + { + throw new NotImplementedException(); + } + + public override DateTime ToDateTime(double seconds) + { + throw new NotImplementedException(); + } + + public override double ToDouble(DateTime dateTime) + { + throw new NotImplementedException(); + } +} diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestFallbackSampler.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestFallbackSampler.cs deleted file mode 100644 index a2598613e6..0000000000 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestFallbackSampler.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -// 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.Collections.Generic; -using OpenTelemetry.Trace; -using Xunit; - -namespace OpenTelemetry.Sampler.AWS.Tests; - -public class TestFallbackSampler -{ - // this test is temporary. - // replace when fallback sampler has more logic implemented. - [Fact] - public void TestFallbackSamplerAlwaysOn() - { - Trace.Sampler sampler = new FallbackSampler(); - - Assert.Equal( - SamplingDecision.RecordAndSample, - sampler.ShouldSample(Utils.CreateSamplingParametersWithTags(new Dictionary())).Decision); - } -} diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs index 053855e4b0..3aea1134de 100644 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs @@ -14,46 +14,57 @@ // limitations under the License. // +using System; using System.Collections.Generic; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using Xunit; namespace OpenTelemetry.Sampler.AWS.Tests; public class TestRulesCache { + [Fact] + public void TestExpiredRulesCache() + { + var testClock = new TestClock(new DateTime(2023, 1, 1)); + var rulesCache = new RulesCache(testClock, "testId", ResourceBuilder.CreateEmpty().Build(), new AlwaysOnSampler()); + + Assert.True(rulesCache.Expired()); + } + [Fact] public void TestUpdateRules() { + var clock = new TestClock(); + // set up rules cache with a default rule var defaultRule = this.CreateDefaultRule(1, 0.05); - defaultRule.Reservoir = new Reservoir(20); - defaultRule.Statistics = new Statistics() + var stats = new Statistics() { RequestCount = 10, - BorrowCount = 5, SampleCount = 5, + BorrowCount = 5, }; - var rulesCache = new RulesCache() + var cache = new RulesCache(clock, "test", ResourceBuilder.CreateEmpty().Build(), new AlwaysOffSampler()) { - Rules = new Dictionary() + RuleAppliers = new List() { - { "Default", defaultRule }, + { new SamplingRuleApplier("testId", clock, defaultRule, stats) }, }, }; // update the default rule var newDefaultRule = this.CreateDefaultRule(10, 0.20); - rulesCache.UpdateRules(new List { newDefaultRule }); + cache.UpdateRules(new List { newDefaultRule }); - // assert that the default rule has been updated - Assert.True(rulesCache.Rules.TryGetValue("Default", out var rule)); + // asserts + Assert.Single(cache.RuleAppliers); + var rule = cache.RuleAppliers[0]; Assert.Equal("Default", rule.RuleName); - Assert.Equal(10, rule.ReservoirSize); - Assert.Equal(0.20, rule.FixedRate); - // assert that the reservoir and statistics has been copied over to new rule - Assert.Equal(20, rule.Reservoir.Quota); + // assert that the statistics has been copied over to new rule Assert.Equal(10, rule.Statistics.RequestCount); Assert.Equal(5, rule.Statistics.BorrowCount); Assert.Equal(5, rule.Statistics.SampleCount); @@ -62,13 +73,15 @@ public void TestUpdateRules() [Fact] public void TestUpdateRulesRemovesOlderRule() { + var clock = new TestClock(); + // set up rule cache with 2 rules - var rulesCache = new RulesCache() + var rulesCache = new RulesCache(clock, "test", ResourceBuilder.CreateEmpty().Build(), new AlwaysOffSampler()) { - Rules = new Dictionary() + RuleAppliers = new List() { - { "Default", this.CreateDefaultRule(1, 0.05) }, - { "Rule1", this.CreateRule("Rule1", 5, 0.20, 1) }, + { new SamplingRuleApplier("testId", clock, this.CreateDefaultRule(1, 0.05), null) }, + { new SamplingRuleApplier("testId", clock, this.CreateRule("Rule1", 5, 0.20, 1), null) }, }, }; @@ -77,58 +90,12 @@ public void TestUpdateRulesRemovesOlderRule() rulesCache.UpdateRules(new List { newDefaultRule }); // assert that Rule1 doesn't exist in rules cache - Assert.Single(rulesCache.Rules); - Assert.False(rulesCache.Rules.ContainsKey("Rule1")); - Assert.True(rulesCache.Rules.ContainsKey("Default")); + Assert.Single(rulesCache.RuleAppliers); + Assert.Single(rulesCache.RuleAppliers); + Assert.Equal("Default", rulesCache.RuleAppliers[0].RuleName); } - [Fact] - public void TestMatchesWithHigherPriority() - { - // set up rule cache with 3 rules - var rulesCache = new RulesCache(); - - var rules = new List - { - this.CreateDefaultRule(1, 0.05), - this.CreateRule("Rule1", 5, 0.20, 100), - this.CreateRule("Rule2", 10, 0.20, 1), - }; - - rulesCache.UpdateRules(rules); - - var samplingParameters = Utils.CreateSamplingParametersWithTags(new Dictionary()); - var resource = Utils.CreateResource("myServiceName", "aws_ec2"); - - var matchedRule = rulesCache.MatchRule(samplingParameters, resource); - - // assert that the rule with higher priority matched - Assert.Equal("Rule2", matchedRule.RuleName); - } - - [Fact] - public void TestMatchWithSamePriority() - { - // set up rule cache with 3 rules with 2 rules at same priority - var rulesCache = new RulesCache(); - - var rules = new List - { - this.CreateDefaultRule(1, 0.05), - this.CreateRule("Rule1", 5, 0.20, 1), - this.CreateRule("Rule2", 10, 0.20, 1), - }; - - rulesCache.UpdateRules(rules); - - var samplingParameters = Utils.CreateSamplingParametersWithTags(new Dictionary()); - var resource = Utils.CreateResource("myServiceName", "aws_ec2"); - - var matchedRule = rulesCache.MatchRule(samplingParameters, resource); - - // assert that the rule is matched in alphabetical order - Assert.Equal("Rule1", matchedRule.RuleName); - } + // TODO: Add tests for matching sampling rules once the reservoir and fixed rate samplers are added. private SamplingRule CreateDefaultRule(int reservoirSize, double fixedRate) { diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRule.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRuleApplier.cs similarity index 63% rename from test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRule.cs rename to test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRuleApplier.cs index e143c30850..ec2c6af288 100644 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRule.cs +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRuleApplier.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,7 @@ namespace OpenTelemetry.Sampler.AWS.Tests; -public class TestSamplingRule +public class TestSamplingRuleApplier { [Fact] public void TestRuleMatchesWithAllAttributes() @@ -45,7 +45,8 @@ public void TestRuleMatchesWithAllAttributes() { "http.url", @"http://127.0.0.1:5000/helloworld" }, }; - Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.True(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); } [Fact] @@ -72,7 +73,8 @@ public void TestRuleMatchesWithWildcardAttributes() { "http.url", @"http://127.0.0.1:5000/helloworld" }, }; - Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.True(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); } [Fact] @@ -94,7 +96,8 @@ public void TestRuleMatchesWithNoActivityAttributes() var activityTags = new Dictionary(); - Assert.False(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.False(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); } [Fact] @@ -116,7 +119,8 @@ public void TestRuleMatchesWithNoActivityAttributesAndWildcardRules() var activityTags = new Dictionary(); - Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.True(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); } [Fact] @@ -141,7 +145,8 @@ public void TestRuleMatchesWithHttpTarget() { "http.target", "/helloworld" }, }; - Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", string.Empty))); + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.True(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", string.Empty))); } [Fact] @@ -174,7 +179,8 @@ public void TestAttributeMatching() { "cat", "meow" }, }; - Assert.True(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ecs"))); + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.True(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ecs"))); } [Fact] @@ -206,68 +212,9 @@ public void TestAttributeMatchingWithLessActivityTags() { "dog", "bark" }, }; - Assert.False(rule.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ecs"))); + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.False(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ecs"))); } - [Fact] - public void TestDeepCopy() - { - var ruleAttributes = new Dictionary() - { - { "dog", "bark" }, - { "cat", "meow" }, - }; - - var rule = new SamplingRule( - ruleName: "testRule", - priority: 1, - fixedRate: 0.05, - reservoirSize: 1, - host: "*", - httpMethod: "*", - resourceArn: "*", - serviceName: "myServiceName", - serviceType: "*", - urlPath: "*", - version: 1, - attributes: ruleAttributes) - { - Reservoir = new Reservoir(10), - }; - - var copy = rule.DeepCopy(); - - // assert that the objects are actually different - Assert.NotEqual(rule.GetHashCode(), copy.GetHashCode()); - Assert.NotEqual(rule.Reservoir.GetHashCode(), copy.Reservoir.GetHashCode()); - Assert.NotEqual(rule.Statistics.GetHashCode(), copy.Statistics.GetHashCode()); - - // assert that the property values are same - Assert.Equal(rule.RuleName, copy.RuleName); - Assert.Equal(rule.Priority, copy.Priority); - Assert.Equal(rule.FixedRate, copy.FixedRate); - Assert.Equal(rule.ReservoirSize, copy.ReservoirSize); - Assert.Equal(rule.Host, copy.Host); - Assert.Equal(rule.HttpMethod, copy.HttpMethod); - Assert.Equal(rule.ResourceArn, copy.ResourceArn); - Assert.Equal(rule.ServiceName, copy.ServiceName); - Assert.Equal(rule.ServiceType, copy.ServiceType); - Assert.Equal(rule.UrlPath, copy.UrlPath); - Assert.Equal(rule.Version, copy.Version); - Assert.True(this.CompareDicts(rule.Attributes, copy.Attributes)); - Assert.Equal(rule.Reservoir.Quota, copy.Reservoir.Quota); - } - - private bool CompareDicts(Dictionary d1, Dictionary d2) - { - foreach (var item in d1) - { - if (!d2.TryGetValue(item.Key, out var value) || value != item.Value) - { - return false; - } - } - - return true; - } + // TODO: Add more test cases for ShouldSample once the sampling logic is added. } From 65a2e7816511402861343deb54e009f243b4c05f Mon Sep 17 00:00:00 2001 From: Prashant Srivastava Date: Sun, 16 Apr 2023 16:28:06 -0700 Subject: [PATCH 3/5] fix unit test --- test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs | 6 ++++++ test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs index 344cd30d9f..a5be1d33df 100644 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs @@ -55,4 +55,10 @@ public override double ToDouble(DateTime dateTime) { throw new NotImplementedException(); } + + // Advnaces the clock by a given time span. + public void Advance(TimeSpan time) + { + this.nowTime = this.nowTime.Add(time); + } } diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs index 3aea1134de..57391ce932 100644 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs @@ -27,9 +27,11 @@ public class TestRulesCache [Fact] public void TestExpiredRulesCache() { - var testClock = new TestClock(new DateTime(2023, 1, 1)); + var testClock = new TestClock(new DateTime(2023, 4, 15)); var rulesCache = new RulesCache(testClock, "testId", ResourceBuilder.CreateEmpty().Build(), new AlwaysOnSampler()); + // advance the clock by 2 hours + testClock.Advance(TimeSpan.FromHours(2)); Assert.True(rulesCache.Expired()); } From 3c18dee781f16e5050988143c62628238dca049a Mon Sep 17 00:00:00 2001 From: Prashant Srivastava Date: Mon, 17 Apr 2023 15:11:33 -0700 Subject: [PATCH 4/5] addressing PR comments --- src/OpenTelemetry.Sampler.AWS/CHANGELOG.md | 4 +-- src/OpenTelemetry.Sampler.AWS/README.md | 6 ++-- .../SamplingRuleApplier.cs | 27 +++++++++++++++- .../TestMatcher.cs | 32 ++++++++++++------- .../TestSamplingRuleApplier.cs | 7 ++-- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md b/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md index 6ad421263d..b50bb6dbc1 100644 --- a/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md +++ b/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md @@ -5,5 +5,5 @@ Initial release of `OpenTelemetry.Sampler.AWS`. * Feature - AWSXRayRemoteSampler - Add support for AWS X-Ray remote sampling - ([#1091](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1091)) - ([#1124](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1124)) + ([#1091](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1091), + [#1124](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1124)) diff --git a/src/OpenTelemetry.Sampler.AWS/README.md b/src/OpenTelemetry.Sampler.AWS/README.md index 1f77fa519f..0ed8bb6d43 100644 --- a/src/OpenTelemetry.Sampler.AWS/README.md +++ b/src/OpenTelemetry.Sampler.AWS/README.md @@ -15,7 +15,10 @@ dotnet add package OpenTelemetry.Sampler.AWS You can configure the `AWSXRayRemoteSampler` as per the following example. Note that you will need to configure your [OpenTelemetry Collector for -X-Ray remote sampling](https://aws-otel.github.io/docs/getting-started/remote-sampling) +X-Ray remote sampling](https://aws-otel.github.io/docs/getting-started/remote-sampling). +This example also sets up the Console Exporter, +which requires adding the package [`OpenTelemetry.Exporter.Console`](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Console/README.md) +to the application. ```csharp using OpenTelemetry; @@ -35,7 +38,6 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(serviceName) .SetResourceBuilder(resourceBuilder) .AddConsoleExporter() - .AddOtlpExporter() .SetSampler(AWSXRayRemoteSampler.Builder(resourceBuilder.Build()) // you must provide a resource .SetPollingInterval(TimeSpan.FromSeconds(5)) .SetEndpoint("http://localhost:2000") diff --git a/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs b/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs index f9e96b764e..abfba82618 100644 --- a/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs +++ b/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs @@ -103,7 +103,8 @@ public bool Matches(SamplingParameters samplingParameters, Resource resource) Matcher.WildcardMatch(httpMethod, this.Rule.HttpMethod) && Matcher.WildcardMatch(httpHost, this.Rule.Host) && Matcher.WildcardMatch(serviceName, this.Rule.ServiceName) && - Matcher.WildcardMatch(GetServiceType(resource), this.Rule.ServiceType); + Matcher.WildcardMatch(GetServiceType(resource), this.Rule.ServiceType) && + Matcher.WildcardMatch(GetArn(in samplingParameters, resource), this.Rule.ResourceArn); } [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "method work in progress")] @@ -126,4 +127,28 @@ private static string GetServiceType(Resource resource) return Matcher.XRayCloudPlatform.TryGetValue(cloudPlatform, out string? value) ? value : string.Empty; } + + private static string GetArn(in SamplingParameters samplingParameters, Resource resource) + { + // currently the aws resource detectors only capture ARNs for ECS and Lambda environments. + string? arn = (string?)resource.Attributes.FirstOrDefault(kvp => + kvp.Key.Equals("aws.ecs.container.arn", StringComparison.Ordinal)).Value; + + if (arn != null) + { + return arn; + } + + if (GetServiceType(resource).Equals("AWS::Lambda::Function", StringComparison.Ordinal)) + { + arn = (string?)samplingParameters.Tags?.FirstOrDefault(kvp => kvp.Key.Equals("faas.id", StringComparison.Ordinal)).Value; + + if (arn != null) + { + return arn; + } + } + + return string.Empty; + } } diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs index 3fb63b8156..3c320f2d81 100644 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs @@ -21,20 +21,28 @@ namespace OpenTelemetry.Sampler.AWS.Tests; public class TestMatcher { - [Fact] - public void TestWildcardMatching() + [Theory] + [InlineData(null, "*")] + [InlineData("", "*")] + [InlineData("HelloWorld", "*")] + [InlineData("HelloWorld", "HelloWorld")] + [InlineData("HelloWorld", "Hello*")] + [InlineData("HelloWorld", "*World")] + [InlineData("HelloWorld", "?ello*")] + [InlineData("HelloWorld", "Hell?W*d")] + [InlineData("Hello.World", "*.World")] + [InlineData("Bye.World", "*.World")] + public void TestWildcardMatch(string input, string pattern) { - Assert.True(Matcher.WildcardMatch(null, "*")); - Assert.True(Matcher.WildcardMatch(string.Empty, "*")); - Assert.True(Matcher.WildcardMatch("HelloWorld", "*")); - - Assert.False(Matcher.WildcardMatch(null, "Hello*")); - Assert.False(Matcher.WildcardMatch("HelloWorld", null)); + Assert.True(Matcher.WildcardMatch(input, pattern)); + } - Assert.True(Matcher.WildcardMatch("HelloWorld", "HelloWorld")); - Assert.True(Matcher.WildcardMatch("HelloWorld", "Hello*")); - Assert.True(Matcher.WildcardMatch("HelloWorld", "*World")); - Assert.True(Matcher.WildcardMatch("HelloWorld", "?ello*")); + [Theory] + [InlineData(null, "Hello*")] + [InlineData("HelloWorld", null)] + public void TestWildcardDoesNotMatch(string input, string pattern) + { + Assert.False(Matcher.WildcardMatch(input, pattern)); } [Fact] diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRuleApplier.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRuleApplier.cs index ec2c6af288..b62fc93c41 100644 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRuleApplier.cs +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRuleApplier.cs @@ -31,9 +31,9 @@ public void TestRuleMatchesWithAllAttributes() reservoirSize: 1, host: "localhost", httpMethod: "GET", - resourceArn: "*", + resourceArn: "arn:aws:lambda:us-west-2:123456789012:function:my-function", serviceName: "myServiceName", - serviceType: "AWS::EC2::Instance", + serviceType: "AWS::Lambda::Function", urlPath: "/helloworld", version: 1, attributes: new Dictionary()); @@ -43,10 +43,11 @@ public void TestRuleMatchesWithAllAttributes() { "http.host", "localhost" }, { "http.method", "GET" }, { "http.url", @"http://127.0.0.1:5000/helloworld" }, + { "faas.id", "arn:aws:lambda:us-west-2:123456789012:function:my-function" }, }; var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); - Assert.True(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ec2"))); + Assert.True(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_lambda"))); } [Fact] From 9289865a8f5cb0839e3f5142f03bfc2a25eadabb Mon Sep 17 00:00:00 2001 From: Prashant Srivastava Date: Tue, 25 Apr 2023 09:08:13 -0700 Subject: [PATCH 5/5] adding info log when using fallback sampler --- src/OpenTelemetry.Sampler.AWS/AWSSamplerEventSource.cs | 6 ++++++ src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs | 1 + src/OpenTelemetry.Sampler.AWS/RulesCache.cs | 1 + 3 files changed, 8 insertions(+) diff --git a/src/OpenTelemetry.Sampler.AWS/AWSSamplerEventSource.cs b/src/OpenTelemetry.Sampler.AWS/AWSSamplerEventSource.cs index 6e5e58143d..de9bfe5f04 100644 --- a/src/OpenTelemetry.Sampler.AWS/AWSSamplerEventSource.cs +++ b/src/OpenTelemetry.Sampler.AWS/AWSSamplerEventSource.cs @@ -40,4 +40,10 @@ public void FailedToDeserializeResponse(string format, string error) { this.WriteEvent(3, format, error); } + + [Event(4, Message = "Using fallback sampler. Either rules cache has expired or no rules matched the request.", Level = EventLevel.Informational)] + public void InfoUsingFallbackSampler() + { + this.WriteEvent(4); + } } diff --git a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs index 6b3653fc68..b688720016 100644 --- a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs +++ b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs @@ -88,6 +88,7 @@ public override SamplingResult ShouldSample(in SamplingParameters samplingParame { if (this.RulesCache.Expired()) { + AWSSamplerEventSource.Log.InfoUsingFallbackSampler(); return this.FallbackSampler.ShouldSample(in samplingParameters); } diff --git a/src/OpenTelemetry.Sampler.AWS/RulesCache.cs b/src/OpenTelemetry.Sampler.AWS/RulesCache.cs index be057b7322..182ab0b013 100644 --- a/src/OpenTelemetry.Sampler.AWS/RulesCache.cs +++ b/src/OpenTelemetry.Sampler.AWS/RulesCache.cs @@ -105,6 +105,7 @@ public SamplingResult ShouldSample(in SamplingParameters samplingParameters) // ideally the default rule should have matched. // if we are here then likely due to a bug. + AWSSamplerEventSource.Log.InfoUsingFallbackSampler(); return this.FallbackSampler.ShouldSample(in samplingParameters); }