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();
+ }
+}