From 1a584314e714d2c9ecf47a54cdad659e32334bed Mon Sep 17 00:00:00 2001 From: Prashant Srivastava <50466688+srprash@users.noreply.github.com> Date: Tue, 25 Apr 2023 22:57:58 -0700 Subject: [PATCH] [Sampler.AWS] Part-2: Add rules cache and rule matching logic (#1124) --- .../.publicApi/net462/PublicAPI.Unshipped.txt | 2 +- .../.publicApi/net6.0/PublicAPI.Unshipped.txt | 2 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 2 +- .../AWSSamplerEventSource.cs | 6 + .../AWSXRayRemoteSampler.cs | 86 +++++-- .../AWSXRayRemoteSamplerBuilder.cs | 23 +- .../AWSXRaySamplerClient.cs | 3 - src/OpenTelemetry.Sampler.AWS/CHANGELOG.md | 3 +- src/OpenTelemetry.Sampler.AWS/Clock.cs | 36 +++ .../FallbackSampler.cs | 38 +++ src/OpenTelemetry.Sampler.AWS/Matcher.cs | 144 ++++++++++++ .../OpenTelemetry.Sampler.AWS.csproj | 1 + src/OpenTelemetry.Sampler.AWS/README.md | 30 ++- src/OpenTelemetry.Sampler.AWS/RulesCache.cs | 138 +++++++++++ src/OpenTelemetry.Sampler.AWS/SamplingRule.cs | 7 +- .../SamplingRuleApplier.cs | 154 ++++++++++++ src/OpenTelemetry.Sampler.AWS/Statistics.cs | 26 +++ src/OpenTelemetry.Sampler.AWS/SystemClock.cs | 62 +++++ .../TestAWSXRayRemoteSampler.cs | 15 +- .../TestAWSXRaySamplerClient.cs | 1 - .../TestClock.cs | 64 +++++ .../TestMatcher.cs | 94 ++++++++ .../TestRulesCache.cs | 123 ++++++++++ .../TestSamplingRuleApplier.cs | 221 ++++++++++++++++++ test/OpenTelemetry.Sampler.AWS.Tests/Utils.cs | 60 +++++ 25 files changed, 1301 insertions(+), 40 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/RulesCache.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/Statistics.cs create mode 100644 src/OpenTelemetry.Sampler.AWS/SystemClock.cs create mode 100644 test/OpenTelemetry.Sampler.AWS.Tests/TestClock.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/TestSamplingRuleApplier.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/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 676bf5bfe6..b688720016 100644 --- a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs +++ b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs @@ -15,7 +15,10 @@ // using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; +using OpenTelemetry.Resources; using OpenTelemetry.Trace; namespace OpenTelemetry.Sampler.AWS; @@ -25,38 +28,71 @@ namespace OpenTelemetry.Sampler.AWS; /// public sealed class AWSXRayRemoteSampler : Trace.Sampler, IDisposable { - internal AWSXRayRemoteSampler(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); + // execute the first update right away and schedule subsequent update later. + this.RulePollerTimer = new Timer(this.GetAndUpdateRules, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); } - internal TimeSpan PollingInterval { get; } + internal TimeSpan RulePollerJitter { get; set; } + + internal Clock Clock { get; set; } + + internal string ClientId { get; set; } - internal string Endpoint { get; } + internal Resource Resource { get; set; } - internal AWSXRaySamplerClient Client { get; } + internal string Endpoint { get; set; } - internal Timer RulePollerTimer { get; } + internal AWSXRaySamplerClient Client { get; set; } + + internal RulesCache RulesCache { get; set; } + + internal Timer RulePollerTimer { get; set; } + + internal TimeSpan PollingInterval { get; set; } + + internal Trace.Sampler FallbackSampler { get; set; } /// /// 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()) + { + AWSSamplerEventSource.Log.InfoUsingFallbackSampler(); + return this.FallbackSampler.ShouldSample(in samplingParameters); + } + + return this.RulesCache.ShouldSample(in samplingParameters); } /// @@ -66,19 +102,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) { - await this.Client.GetSamplingRules().ConfigureAwait(false); + List rules = await this.Client.GetSamplingRules().ConfigureAwait(false); + + this.RulesCache.UpdateRules(rules); - // TODO: more functionality to be added. + // 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 d5c39224a3..0aa3aa32d8 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,13 +28,17 @@ public class AWSXRayRemoteSamplerBuilder private static readonly TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); + private Resource resource; private TimeSpan pollingInterval; private string endpoint; + private Clock clock; - internal AWSXRayRemoteSamplerBuilder() + internal AWSXRayRemoteSamplerBuilder(Resource resource) { + this.resource = resource; this.pollingInterval = DefaultPollingInterval; this.endpoint = DefaultEndpoint; + this.clock = Clock.GetDefault(); } /// @@ -78,6 +83,20 @@ 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, 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/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..b50bb6dbc1 100644 --- a/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md +++ b/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md @@ -5,4 +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)) + ([#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..39218fbdf2 --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/Clock.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; + +namespace OpenTelemetry.Sampler.AWS; + +// A time keeper for the purpose of this sampler. +internal abstract class Clock +{ + public static Clock GetDefault() + { + return SystemClock.GetInstance(); + } + + public abstract DateTime Now(); + + 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 new file mode 100644 index 0000000000..3917c407fe --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/FallbackSampler.cs @@ -0,0 +1,38 @@ +// +// 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(); + + 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. + // 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..e135293e9f --- /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 IReadOnlyDictionary 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 341fabd468..8427c7659c 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..0ed8bb6d43 100644 --- a/src/OpenTelemetry.Sampler.AWS/README.md +++ b/src/OpenTelemetry.Sampler.AWS/README.md @@ -15,20 +15,34 @@ 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; +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() + .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/RulesCache.cs b/src/OpenTelemetry.Sampler.AWS/RulesCache.cs new file mode 100644 index 0000000000..182ab0b013 --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/RulesCache.cs @@ -0,0 +1,138 @@ +// +// 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(Clock clock, string clientId, Resource resource, Trace.Sampler fallbackSampler) + { + this.rwLock = new ReaderWriterLockSlim(); + this.Clock = clock; + this.ClientId = clientId; + this.Resource = resource; + this.FallbackSampler = fallbackSampler; + this.RuleAppliers = new List(); + this.UpdatedAt = this.Clock.Now(); + } + + internal Clock Clock { get; set; } + + internal string ClientId { get; set; } + + internal Resource Resource { get; set; } + + internal Trace.Sampler FallbackSampler { get; set; } + + internal List RuleAppliers { get; set; } + + internal DateTime UpdatedAt { get; set; } + + public bool Expired() + { + this.rwLock.EnterReadLock(); + try + { + return this.Clock.Now() > this.UpdatedAt.AddSeconds(CacheTTL); + } + finally + { + this.rwLock.ExitReadLock(); + } + } + + public void UpdateRules(List newRules) + { + // sort the new rules + newRules.Sort((x, y) => x.CompareTo(y)); + + List newRuleAppliers = new List(); + foreach (var rule in newRules) + { + var currentStatistics = this.RuleAppliers + .FirstOrDefault(currentApplier => currentApplier.RuleName == rule.RuleName) + ?.Statistics ?? new Statistics(); + + 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.ExitWriteLock(); + } + } + + public SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + foreach (var ruleApplier in this.RuleAppliers) + { + if (ruleApplier.Matches(samplingParameters, this.Resource)) + { + return ruleApplier.ShouldSample(in 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); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + internal DateTime GetUpdatedAt() + { + this.rwLock.EnterReadLock(); + try + { + return this.UpdatedAt; + } + finally + { + this.rwLock.ExitReadLock(); + } + } + + 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 1805951b6f..c4907675b5 100644 --- a/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs +++ b/src/OpenTelemetry.Sampler.AWS/SamplingRule.cs @@ -31,6 +31,7 @@ public SamplingRule( string httpMethod, string resourceArn, string serviceName, + string serviceType, string urlPath, int version, Dictionary attributes) @@ -43,6 +44,7 @@ public SamplingRule( this.HttpMethod = httpMethod; this.ResourceArn = resourceArn; this.ServiceName = serviceName; + this.ServiceType = serviceType; this.UrlPath = urlPath; this.Version = version; this.Attributes = attributes; @@ -72,6 +74,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 +84,7 @@ public SamplingRule( public int Version { get; set; } [JsonPropertyName("Attributes")] - public Dictionary? Attributes { get; set; } + public Dictionary Attributes { get; set; } public int CompareTo(SamplingRule? other) { diff --git a/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs b/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs new file mode 100644 index 0000000000..abfba82618 --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.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; +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) && + Matcher.WildcardMatch(GetArn(in samplingParameters, resource), this.Rule.ResourceArn); + } + + [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; + } + + 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/src/OpenTelemetry.Sampler.AWS/Statistics.cs b/src/OpenTelemetry.Sampler.AWS/Statistics.cs new file mode 100644 index 0000000000..9b790eb0e8 --- /dev/null +++ b/src/OpenTelemetry.Sampler.AWS/Statistics.cs @@ -0,0 +1,26 @@ +// +// 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. +// + +namespace OpenTelemetry.Sampler.AWS; + +internal class Statistics +{ + public int RequestCount { get; internal set; } + + public int BorrowCount { get; internal set; } + + public int SampleCount { get; internal set; } +} 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/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/TestClock.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs new file mode 100644 index 0000000000..a5be1d33df --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestClock.cs @@ -0,0 +1,64 @@ +// +// 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(); + } + + // 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/TestMatcher.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs new file mode 100644 index 0000000000..3c320f2d81 --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestMatcher.cs @@ -0,0 +1,94 @@ +// +// 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 +{ + [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(input, pattern)); + } + + [Theory] + [InlineData(null, "Hello*")] + [InlineData("HelloWorld", null)] + public void TestWildcardDoesNotMatch(string input, string pattern) + { + Assert.False(Matcher.WildcardMatch(input, pattern)); + } + + [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..57391ce932 --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestRulesCache.cs @@ -0,0 +1,123 @@ +// +// 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 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, 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()); + } + + [Fact] + public void TestUpdateRules() + { + var clock = new TestClock(); + + // set up rules cache with a default rule + var defaultRule = this.CreateDefaultRule(1, 0.05); + var stats = new Statistics() + { + RequestCount = 10, + SampleCount = 5, + BorrowCount = 5, + }; + + var cache = new RulesCache(clock, "test", ResourceBuilder.CreateEmpty().Build(), new AlwaysOffSampler()) + { + RuleAppliers = new List() + { + { new SamplingRuleApplier("testId", clock, defaultRule, stats) }, + }, + }; + + // update the default rule + var newDefaultRule = this.CreateDefaultRule(10, 0.20); + cache.UpdateRules(new List { newDefaultRule }); + + // asserts + Assert.Single(cache.RuleAppliers); + var rule = cache.RuleAppliers[0]; + Assert.Equal("Default", rule.RuleName); + + // 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); + } + + [Fact] + public void TestUpdateRulesRemovesOlderRule() + { + var clock = new TestClock(); + + // set up rule cache with 2 rules + var rulesCache = new RulesCache(clock, "test", ResourceBuilder.CreateEmpty().Build(), new AlwaysOffSampler()) + { + RuleAppliers = new List() + { + { new SamplingRuleApplier("testId", clock, this.CreateDefaultRule(1, 0.05), null) }, + { new SamplingRuleApplier("testId", clock, this.CreateRule("Rule1", 5, 0.20, 1), null) }, + }, + }; + + // 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.RuleAppliers); + Assert.Single(rulesCache.RuleAppliers); + Assert.Equal("Default", rulesCache.RuleAppliers[0].RuleName); + } + + // TODO: Add tests for matching sampling rules once the reservoir and fixed rate samplers are added. + + 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/TestSamplingRuleApplier.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRuleApplier.cs new file mode 100644 index 0000000000..b62fc93c41 --- /dev/null +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestSamplingRuleApplier.cs @@ -0,0 +1,221 @@ +// +// 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 TestSamplingRuleApplier +{ + [Fact] + public void TestRuleMatchesWithAllAttributes() + { + var rule = new SamplingRule( + ruleName: "testRule", + priority: 1, + fixedRate: 0.05, + reservoirSize: 1, + host: "localhost", + httpMethod: "GET", + resourceArn: "arn:aws:lambda:us-west-2:123456789012:function:my-function", + serviceName: "myServiceName", + serviceType: "AWS::Lambda::Function", + 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" }, + { "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_lambda"))); + } + + [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" }, + }; + + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.True(applier.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(); + + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.False(applier.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(); + + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.True(applier.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" }, + }; + + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.True(applier.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" }, + }; + + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.True(applier.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" }, + }; + + var applier = new SamplingRuleApplier("clientId", new TestClock(), rule, new Statistics()); + Assert.False(applier.Matches(Utils.CreateSamplingParametersWithTags(activityTags), Utils.CreateResource("myServiceName", "aws_ecs"))); + } + + // TODO: Add more test cases for ShouldSample once the sampling logic is added. +} 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(); + } +}