diff --git a/.github/workflows/ci-contrib.yml b/.github/workflows/ci-contrib.yml index 65e48a6afb..63c7a0d23e 100644 --- a/.github/workflows/ci-contrib.yml +++ b/.github/workflows/ci-contrib.yml @@ -193,3 +193,46 @@ jobs: with: gem: "opentelemetry-processor-${{ matrix.gem }}" ruby: "jruby-9.4.12.0" + + samplers: + strategy: + fail-fast: false + matrix: + gem: + - xray + os: + - ubuntu-latest + name: "samplers-${{ matrix.gem }} / ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: "Test Ruby 3.4" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-sampler-${{ matrix.gem }}" + ruby: "3.4" + - name: "Test Ruby 3.3" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-sampler-${{ matrix.gem }}" + ruby: "3.3" + - name: "Test Ruby 3.2" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-sampler-${{ matrix.gem }}" + ruby: "3.2" + - name: "Test Ruby 3.1" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-sampler-${{ matrix.gem }}" + ruby: "3.1" + yard: true + rubocop: true + coverage: true + build: true + - name: "Test JRuby" + if: "${{ matrix.os == 'ubuntu-latest' }}" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-sampler-${{ matrix.gem }}" + ruby: "jruby-9.4.12.0" diff --git a/CODEOWNERS b/CODEOWNERS index ffc248e282..d75b763ac2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,3 +35,5 @@ instrumentation/rspec/ @chrisholmes @open-telemetry/ruby-contrib-maintainers @op instrumentation/que/ @indrekj @open-telemetry/ruby-contrib-maintainers @open-telemetry/ruby-contrib-approvers @fbogsany @mwear @robertlaurin @dazuma @ericmustin @arielvalentin @ahayworth @plantfansam @robbkidd @simi @kaylareopelle @xuan-cao-swi processor/baggage/ @robbkidd @mikegoldsmith @open-telemetry/ruby-contrib-maintainers @open-telemetry/ruby-contrib-approvers @fbogsany @mwear @robertlaurin @dazuma @ericmustin @arielvalentin @ahayworth @plantfansam @robbkidd @simi @kaylareopelle @xuan-cao-swi + +sampler/xray/ @jj22ee @open-telemetry/ruby-contrib-maintainers @open-telemetry/ruby-contrib-approvers @fbogsany @mwear @robertlaurin @dazuma @ericmustin @arielvalentin @ahayworth @plantfansam @robbkidd @simi @kaylareopelle @xuan-cao-swi diff --git a/sampler/xray/.rubocop.yml b/sampler/xray/.rubocop.yml new file mode 100644 index 0000000000..237b385c02 --- /dev/null +++ b/sampler/xray/.rubocop.yml @@ -0,0 +1,4 @@ +inherit_from: ../../.rubocop.yml + +Metrics/ParameterLists: + Enabled: false diff --git a/sampler/xray/.yardopts b/sampler/xray/.yardopts new file mode 100644 index 0000000000..6f7981aa5c --- /dev/null +++ b/sampler/xray/.yardopts @@ -0,0 +1,9 @@ +--no-private +--title=OpenTelemetry Sampler XRay +--markup=markdown +--main=README.md +./lib/opentelemetry/sampler/xray/**/*.rb +./lib/opentelemetry/sampler/xray.rb +- +README.md +CHANGELOG.md diff --git a/sampler/xray/CHANGELOG.md b/sampler/xray/CHANGELOG.md new file mode 100644 index 0000000000..4ac3aa34b6 --- /dev/null +++ b/sampler/xray/CHANGELOG.md @@ -0,0 +1,2 @@ +# Release History: opentelemetry-sampler-xray + diff --git a/sampler/xray/Gemfile b/sampler/xray/Gemfile new file mode 100644 index 0000000000..1b6d489203 --- /dev/null +++ b/sampler/xray/Gemfile @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gemspec + +group :test do + gem 'bundler', '~> 2.4' + gem 'minitest', '~> 5.0' + gem 'rake', '~> 13.0' + gem 'rubocop', '~> 1.75.2' + gem 'rubocop-performance', '~> 1.24.0' + gem 'simplecov', '~> 0.22.0' + gem 'yard', '~> 0.9' + gem 'timecop', '~> 0.9.10' + gem 'webmock', '~> 3.24' + if RUBY_VERSION >= '3.4' + gem 'base64' + gem 'mutex_m' + end +end diff --git a/sampler/xray/LICENSE b/sampler/xray/LICENSE new file mode 100644 index 0000000000..ada534dc97 --- /dev/null +++ b/sampler/xray/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 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. diff --git a/sampler/xray/README.md b/sampler/xray/README.md new file mode 100644 index 0000000000..f620c844de --- /dev/null +++ b/sampler/xray/README.md @@ -0,0 +1,54 @@ +# opentelemetry-sampler-xray + +The `opentelemetry-sampler-xray` gem contains the AWS X-Ray Remote Sampler for OpenTelemetry. + +## What is OpenTelemetry? + +[OpenTelemetry][opentelemetry-home] is an open source observability framework, providing a general-purpose API, SDK, and related tools required for the instrumentation of cloud-native software, frameworks, and libraries. + +OpenTelemetry provides a single set of APIs, libraries, agents, and collector services to capture distributed traces and metrics from your application. You can analyze them using Prometheus, Jaeger, and other observability tools. + +## How does this gem fit in? + +This gem can be used with any OpenTelemetry SDK implementation. This can be the official `opentelemetry-sdk` gem or any other concrete implementation. + +## How do I get started? + +Install the gem using: + +```console +gem install opentelemetry-sampler-xray +``` + +Or, if you use [bundler][bundler-home], include `opentelemetry-sampler-xray` in your `Gemfile`. + +In your application: + +```ruby +OpenTelemetry.tracer_provider.sampler = OpenTelemetry::Sampler::XRay::AwsXRayRemoteSampler.new( + polling_interval: 300, resource: OpenTelemetry::SDK::Resources::Resource.create({ + "service.name"=>"my-service-name", + "cloud.platform"=>"aws_ec2" + }) +) +``` + +## How can I get involved? + +The `opentelemetry-sampler-xray` gem source is [on github][repo-github], along with related gems including `opentelemetry-api` and `opentelemetry-sdk`. + +The OpenTelemetry Ruby gems are maintained by the OpenTelemetry Ruby special interest group (SIG). You can get involved by joining us on our [GitHub Discussions][discussions-url], [Slack Channel][slack-channel] or attending our weekly meeting. See the [meeting calendar][community-meetings] for dates and times. For more information on this and other language SIGs, see the OpenTelemetry [community page][ruby-sig]. + +## License + +The `opentelemetry-sampler-xray` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information. + +[opentelemetry-home]: https://opentelemetry.io +[bundler-home]: https://bundler.io +[repo-github]: https://github.com/open-telemetry/opentelemetry-ruby +[license-github]: https://github.com/open-telemetry/opentelemetry-ruby-contrib/blob/main/LICENSE +[ruby-sig]: https://github.com/open-telemetry/community#ruby-sig +[community-meetings]: https://github.com/open-telemetry/community#community-meetings +[slack-channel]: https://cloud-native.slack.com/archives/C01NWKKMKMY +[discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions +[aws-xray]: https://docs.aws.amazon.com/xray/latest/devguide/aws-xray.html diff --git a/sampler/xray/Rakefile b/sampler/xray/Rakefile new file mode 100644 index 0000000000..fcb1f9afa4 --- /dev/null +++ b/sampler/xray/Rakefile @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'yard' +require 'rubocop/rake_task' + +RuboCop::RakeTask.new + +Rake::TestTask.new :test do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +YARD::Rake::YardocTask.new do |t| + t.stats_options = ['--list-undoc'] +end + +if RUBY_ENGINE == 'truffleruby' + task default: %i[test] +else + task default: %i[test rubocop yard] +end diff --git a/sampler/xray/lib/opentelemetry-sampler-xray.rb b/sampler/xray/lib/opentelemetry-sampler-xray.rb new file mode 100644 index 0000000000..1d325c678e --- /dev/null +++ b/sampler/xray/lib/opentelemetry-sampler-xray.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry-api' +require_relative 'opentelemetry/sampler/xray' diff --git a/sampler/xray/lib/opentelemetry/sampler/xray.rb b/sampler/xray/lib/opentelemetry/sampler/xray.rb new file mode 100644 index 0000000000..92b45f0d7a --- /dev/null +++ b/sampler/xray/lib/opentelemetry/sampler/xray.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +# OpenTelemetry is an open source observability framework, providing a +# general-purpose API, SDK, and related tools required for the instrumentation +# of cloud-native software, frameworks, and libraries. +# +# The OpenTelemetry module provides global accessors for telemetry objects. +# See the documentation for the `opentelemetry-api` gem for details. + +module OpenTelemetry + module Sampler + # Namespace for OpenTelemetry XRay Sampler + module XRay + end + end +end + +require_relative 'xray/version' +require_relative 'xray/aws_xray_remote_sampler' diff --git a/sampler/xray/lib/opentelemetry/sampler/xray/aws_xray_remote_sampler.rb b/sampler/xray/lib/opentelemetry/sampler/xray/aws_xray_remote_sampler.rb new file mode 100644 index 0000000000..13238f9c1a --- /dev/null +++ b/sampler/xray/lib/opentelemetry/sampler/xray/aws_xray_remote_sampler.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'net/http' +require 'json' +require 'opentelemetry/sdk' +require_relative 'sampling_rule' +require_relative 'sampling_rule_applier' +require_relative 'aws_xray_sampling_client' + +module OpenTelemetry + module Sampler + module XRay + # Constants + DEFAULT_RULES_POLLING_INTERVAL_SECONDS = 5 * 60 + DEFAULT_TARGET_POLLING_INTERVAL_SECONDS = 10 + DEFAULT_AWS_PROXY_ENDPOINT = 'http://localhost:2000' + + # AWSXRayRemoteSampler is a Wrapper class to ensure that all XRay Sampler Functionality + # in InternalAWSXRayRemoteSampler uses ParentBased logic to respect the parent span's sampling decision + class AWSXRayRemoteSampler + def initialize(endpoint: '127.0.0.1:2000', polling_interval: DEFAULT_RULES_POLLING_INTERVAL_SECONDS, resource: OpenTelemetry::SDK::Resources::Resource.create) + @root = OpenTelemetry::SDK::Trace::Samplers.parent_based( + root: OpenTelemetry::Sampler::XRay::InternalAWSXRayRemoteSampler.new(endpoint: endpoint, polling_interval: polling_interval, resource: resource) + ) + end + + def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:) + @root.should_sample?( + trace_id: trace_id, parent_context: parent_context, links: links, name: name, kind: kind, attributes: attributes + ) + end + + def description + "AWSXRayRemoteSampler{root=#{@root.description}}" + end + end + + # InternalAWSXRayRemoteSampler contains all core XRay Sampler Functionality, + # however it is NOT Parent-based (e.g. Sample logic runs for each span) + class InternalAWSXRayRemoteSampler + def initialize(endpoint: '127.0.0.1:2000', polling_interval: DEFAULT_RULES_POLLING_INTERVAL_SECONDS, resource: OpenTelemetry::SDK::Resources::Resource.create) + if polling_interval.nil? || polling_interval < 10 + OpenTelemetry.logger.warn( + "'polling_interval' is undefined or too small. Defaulting to #{DEFAULT_RULES_POLLING_INTERVAL_SECONDS} seconds" + ) + @rule_polling_interval_millis = DEFAULT_RULES_POLLING_INTERVAL_SECONDS * 1000 + else + @rule_polling_interval_millis = polling_interval * 1000 + end + + @rule_polling_jitter_millis = rand * 5 * 1000 + @target_polling_interval = DEFAULT_TARGET_POLLING_INTERVAL_SECONDS + @target_polling_jitter_millis = (rand / 10) * 1000 + + @aws_proxy_endpoint = endpoint || DEFAULT_AWS_PROXY_ENDPOINT + @client_id = self.class.generate_client_id + + @sampling_client = OpenTelemetry::Sampler::XRay::AWSXRaySamplingClient.new(@aws_proxy_endpoint) + + # Start the Sampling Rules poller + start_sampling_rules_poller + + # TODO: Start the Sampling Targets poller + end + + def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:) + OpenTelemetry::SDK::Trace::Samplers::Result.new( + decision: OpenTelemetry::SDK::Trace::Samplers::Decision::DROP, + tracestate: tracestate, + attributes: attributes + ) + end + + def description + "InternalAWSXRayRemoteSampler{aws_proxy_endpoint=#{@aws_proxy_endpoint}, rule_polling_interval_millis=#{@rule_polling_interval_millis}}" + end + + private + + def start_sampling_rules_poller + # Execute first update + retrieve_and_update_sampling_rules + + # Update sampling rules periodically + @rule_poller = Thread.new do + loop do + sleep((@rule_polling_interval_millis + @rule_polling_jitter_millis) / 1000.0) + retrieve_and_update_sampling_rules + end + end + end + + def retrieve_and_update_sampling_rules + sampling_rules_response = @sampling_client.fetch_sampling_rules + if sampling_rules_response&.body && sampling_rules_response.body != '' + rules = JSON.parse(sampling_rules_response.body) + update_sampling_rules(rules) + else + OpenTelemetry.logger.error('GetSamplingRules Response is falsy') + end + end + + def update_sampling_rules(response_object) + sampling_rules = [] + if response_object && response_object['SamplingRuleRecords'] + response_object['SamplingRuleRecords'].each do |record| + if record['SamplingRule'] + sampling_rule = OpenTelemetry::Sampler::XRay::SamplingRule.new(record['SamplingRule']) + sampling_rules << SamplingRuleApplier.new(sampling_rule) + end + end + # TODO: Add Sampling Rules to a Rule Cache + else + OpenTelemetry.logger.error('SamplingRuleRecords from GetSamplingRules request is not defined') + end + end + + class << self + def generate_client_id + hex_chars = ('0'..'9').to_a + ('a'..'f').to_a + Array.new(24) { hex_chars.sample }.join + end + end + end + end + end +end diff --git a/sampler/xray/lib/opentelemetry/sampler/xray/aws_xray_sampling_client.rb b/sampler/xray/lib/opentelemetry/sampler/xray/aws_xray_sampling_client.rb new file mode 100644 index 0000000000..84b1323b9c --- /dev/null +++ b/sampler/xray/lib/opentelemetry/sampler/xray/aws_xray_sampling_client.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'net/http' +require 'json' +require 'uri' + +module OpenTelemetry + module Sampler + module XRay + # AWSXRaySamplingClient is responsible for making '/GetSamplingRules' and '/SamplingTargets' calls + # to AWS X-Ray to retrieve Sampling Rules and Sampling Targets respectively + class AWSXRaySamplingClient + def initialize(endpoint) + @endpoint = endpoint + @host, @port = parse_endpoint(@endpoint) + + @sampling_rules_url = URI::HTTP.build(host: @host, path: '/GetSamplingRules', port: @port) + @sampling_targets_url = URI::HTTP.build(host: @host, path: '/SamplingTargets', port: @port) + @request_headers = { 'content-type': 'application/json' } + end + + def fetch_sampling_rules + begin + OpenTelemetry::Common::Utilities.untraced do + return Net::HTTP.post(@sampling_rules_url, '{}', @request_headers) + end + rescue StandardError => e + OpenTelemetry.logger.debug("Error occurred when fetching Sampling Rules: #{e}") + end + nil + end + + def fetch_sampling_targets(request_body) + begin + OpenTelemetry::Common::Utilities.untraced do + return Net::HTTP.post(@sampling_targets_url, request_body.to_json, @request_headers) + end + rescue StandardError => e + OpenTelemetry.logger.debug("Error occurred when fetching Sampling Targets: #{e}") + end + nil + end + + private + + def parse_endpoint(endpoint) + host, port = endpoint.split(':') + [host, port.to_i] + rescue StandardError => e + OpenTelemetry.logger.error("Invalid endpoint: #{endpoint}") + raise e + end + end + end + end +end diff --git a/sampler/xray/lib/opentelemetry/sampler/xray/sampling_rule.rb b/sampler/xray/lib/opentelemetry/sampler/xray/sampling_rule.rb new file mode 100644 index 0000000000..7bf038a914 --- /dev/null +++ b/sampler/xray/lib/opentelemetry/sampler/xray/sampling_rule.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Sampler + module XRay + # SamplingRule represent a Sampling Rule object for AWS X-Ray + # See: https://docs.aws.amazon.com/xray/latest/api/API_SamplingRule.html + class SamplingRule + attr_accessor :rule_name, :rule_arn, :priority, :reservoir_size, :fixed_rate, + :service_name, :service_type, :host, :http_method, :url_path, + :resource_arn, :attributes, :version + + def initialize(sampling_rule) + # The AWS API docs mark `rule_name` as an optional field but in practice it seems to always be + # present, and sampling targets could not be computed without it. For now provide an arbitrary fallback just in + # case the AWS API docs are correct. + @rule_name = sampling_rule['RuleName'] || 'Default' + @rule_arn = sampling_rule['RuleARN'] + @priority = sampling_rule['Priority'] + @reservoir_size = sampling_rule['ReservoirSize'] + @fixed_rate = sampling_rule['FixedRate'] + @service_name = sampling_rule['ServiceName'] + @service_type = sampling_rule['ServiceType'] + @host = sampling_rule['Host'] + @http_method = sampling_rule['HTTPMethod'] + @url_path = sampling_rule['URLPath'] + @resource_arn = sampling_rule['ResourceARN'] + @version = sampling_rule['Version'] + @attributes = sampling_rule['Attributes'] + end + + def equals?(other) + attributes_equals = if @attributes.nil? || other.attributes.nil? + @attributes == other.attributes + else + attributes_equal?(other.attributes) + end + + @fixed_rate == other.fixed_rate && + @http_method == other.http_method && + @host == other.host && + @priority == other.priority && + @reservoir_size == other.reservoir_size && + @resource_arn == other.resource_arn && + @rule_arn == other.rule_arn && + @rule_name == other.rule_name && + @service_name == other.service_name && + @service_type == other.service_type && + @url_path == other.url_path && + @version == other.version && + attributes_equals + end + + private + + def attributes_equal?(other_attributes) + return false unless @attributes.length == other_attributes.length + + other_attributes.each do |key, value| + return false unless @attributes.key?(key) && @attributes[key] == value + end + + true + end + end + end + end +end diff --git a/sampler/xray/lib/opentelemetry/sampler/xray/sampling_rule_applier.rb b/sampler/xray/lib/opentelemetry/sampler/xray/sampling_rule_applier.rb new file mode 100644 index 0000000000..d6f0aac971 --- /dev/null +++ b/sampler/xray/lib/opentelemetry/sampler/xray/sampling_rule_applier.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/sdk' +require 'opentelemetry-semantic_conventions' +require 'date' +require_relative 'statistics' +require_relative 'utils' + +module OpenTelemetry + module Sampler + module XRay + # SamplingRuleApplier is responsible for applying Reservoir Sampling and Probability Sampling + # from the Sampling Rule when determining the sampling decision for spans that matched the rule + class SamplingRuleApplier + attr_reader :sampling_rule + + MAX_DATE_TIME_SECONDS = Time.at(8_640_000_000_000) + + def initialize(sampling_rule, statistics = OpenTelemetry::Sampler::XRay::Statistics.new, target = nil) + @sampling_rule = sampling_rule + end + + def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:) + OpenTelemetry::SDK::Trace::Samplers::Result.new( + decision: OpenTelemetry::SDK::Trace::Samplers::Decision::DROP, + tracestate: OpenTelemetry::Trace::Tracestate::DEFAULT + ) + end + end + end + end +end diff --git a/sampler/xray/lib/opentelemetry/sampler/xray/statistics.rb b/sampler/xray/lib/opentelemetry/sampler/xray/statistics.rb new file mode 100644 index 0000000000..54cc27462f --- /dev/null +++ b/sampler/xray/lib/opentelemetry/sampler/xray/statistics.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Sampler + module XRay + # Statistics contains metric counters for each sampling attempt in each Sampling Rule Applier + class Statistics + attr_accessor :request_count, :sample_count, :borrow_count + + def initialize(request_count: 0, sample_count: 0, borrow_count: 0) + @request_count = request_count + @sample_count = sample_count + @borrow_count = borrow_count + end + + def retrieve_statistics + { + request_count: @request_count, + sample_count: @sample_count, + borrow_count: @borrow_count + } + end + + def reset_statistics + @request_count = 0 + @sample_count = 0 + @borrow_count = 0 + end + end + end + end +end diff --git a/sampler/xray/lib/opentelemetry/sampler/xray/utils.rb b/sampler/xray/lib/opentelemetry/sampler/xray/utils.rb new file mode 100644 index 0000000000..15698b622e --- /dev/null +++ b/sampler/xray/lib/opentelemetry/sampler/xray/utils.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Sampler + module XRay + # Utils contains utilities for X-Ray Sampling Rule matching logic + module Utils + module_function + + CLOUD_PLATFORM_MAPPING = { + 'aws_lambda' => 'AWS::Lambda::Function', + 'aws_elastic_beanstalk' => 'AWS::ElasticBeanstalk::Environment', + 'aws_ec2' => 'AWS::EC2::Instance', + 'aws_ecs' => 'AWS::ECS::Container', + 'aws_eks' => 'AWS::EKS::Container' + }.freeze + + def escape_regexp(regexp_pattern) + # Escapes special characters except * and ? to maintain wildcard functionality + regexp_pattern.gsub(/[.+^${}()|\[\]\\]/) { |match| "\\#{match}" } + end + + def convert_pattern_to_regexp(pattern) + escape_regexp(pattern).gsub('*', '.*').tr('?', '.') + end + + def wildcard_match(pattern = nil, text = nil) + return true if pattern == '*' + return false if pattern.nil? || !text.is_a?(String) + return text.empty? if pattern.empty? + + regexp = "^#{convert_pattern_to_regexp(pattern.downcase)}$" + match = text.downcase.match?(regexp) + + unless match + OpenTelemetry.logger.debug( + "WildcardMatch: no match found for #{text} against pattern #{pattern}" + ) + end + + match + end + + def attribute_match(attributes = nil, rule_attributes = nil) + return true if rule_attributes.nil? || rule_attributes.empty? + + return false if attributes.nil? || + attributes.empty? || + rule_attributes.length > attributes.length + + matched_count = 0 + attributes.each do |key, value| + found_key = rule_attributes.keys.find { |rule_key| rule_key == key } + next if found_key.nil? + + matched_count += 1 if wildcard_match(rule_attributes[found_key], value) + end + + matched_count == rule_attributes.length + end + end + end + end +end diff --git a/sampler/xray/lib/opentelemetry/sampler/xray/version.rb b/sampler/xray/lib/opentelemetry/sampler/xray/version.rb new file mode 100644 index 0000000000..8477587473 --- /dev/null +++ b/sampler/xray/lib/opentelemetry/sampler/xray/version.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Sampler + module XRay + VERSION = '0.0.0' + end + end +end diff --git a/sampler/xray/opentelemetry-sampler-xray.gemspec b/sampler/xray/opentelemetry-sampler-xray.gemspec new file mode 100644 index 0000000000..b759bc17e6 --- /dev/null +++ b/sampler/xray/opentelemetry-sampler-xray.gemspec @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) + +require 'opentelemetry/sampler/xray/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-sampler-xray' + spec.version = OpenTelemetry::Sampler::XRay::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'XRay Remote Sampler for the OpenTelemetry framework' + spec.description = 'XRay Remote Sampler for the OpenTelemetry framework' + spec.homepage = 'https://github.com/open-telemetry/opentelemetry-ruby-contrib' + spec.license = 'Apache-2.0' + + spec.files = Dir.glob('lib/**/*.rb') + + Dir.glob('*.md') + + ['LICENSE', '.yardopts'] + spec.require_paths = ['lib'] + spec.required_ruby_version = ">= #{File.read(File.expand_path('../../gemspecs/RUBY_REQUIREMENT', __dir__))}" + + spec.add_dependency 'opentelemetry-api', '~> 1.0' + spec.add_dependency 'opentelemetry-common', '~> 0.21' + spec.add_dependency 'opentelemetry-sdk', '~> 1.0' + spec.add_dependency 'opentelemetry-semantic_conventions', '~> 1.11' + + if spec.respond_to?(:metadata) + spec.metadata['changelog_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}/file/CHANGELOG.md" + spec.metadata['source_code_uri'] = 'https://github.com/open-telemetry/opentelemetry-ruby-contrib/tree/main/sampler/xray' + spec.metadata['bug_tracker_uri'] = 'https://github.com/open-telemetry/opentelemetry-ruby-contrib/issues' + spec.metadata['documentation_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}" + end + + spec.post_install_message = File.read(File.expand_path('../../gemspecs/POST_INSTALL_MESSAGE', __dir__)) +end diff --git a/sampler/xray/test/aws_xray_remote_sampler_test.rb b/sampler/xray/test/aws_xray_remote_sampler_test.rb new file mode 100644 index 0000000000..9dcb5a8c4f --- /dev/null +++ b/sampler/xray/test/aws_xray_remote_sampler_test.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +DATA_DIR_SAMPLING_RULES = File.join(__dir__, 'data/test-remote-sampler_sampling-rules-response-sample.json') +DATA_DIR_SAMPLING_TARGETS = File.join(__dir__, 'data/test-remote-sampler_sampling-targets-response-sample.json') +TEST_URL = 'localhost:2000' + +describe OpenTelemetry::Sampler::XRay::AWSXRayRemoteSampler do + it 'creates remote sampler with empty resource' do + stub_request(:post, "#{TEST_URL}/GetSamplingRules") + .to_return(status: 200, body: File.read(DATA_DIR_SAMPLING_RULES)) + stub_request(:post, "#{TEST_URL}/SamplingTargets") + .to_return(status: 200, body: File.read(DATA_DIR_SAMPLING_TARGETS)) + + sampler = OpenTelemetry::Sampler::XRay::InternalAWSXRayRemoteSampler.new(resource: OpenTelemetry::SDK::Resources::Resource.create) + + assert !sampler.instance_variable_get(:@rule_poller).nil? + assert_equal(sampler.instance_variable_get(:@rule_polling_interval_millis), 300 * 1000) + assert !sampler.instance_variable_get(:@sampling_client).nil? + assert_match(/[a-f0-9]{24}/, sampler.instance_variable_get(:@client_id)) + end + + it 'creates remote sampler with populated resource' do + stub_request(:post, "#{TEST_URL}/GetSamplingRules") + .to_return(status: 200, body: File.read(DATA_DIR_SAMPLING_RULES)) + stub_request(:post, "#{TEST_URL}/SamplingTargets") + .to_return(status: 200, body: File.read(DATA_DIR_SAMPLING_TARGETS)) + + resource = OpenTelemetry::SDK::Resources::Resource.create( + OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => 'test-service-name', + OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM => 'test-cloud-platform' + ) + sampler = OpenTelemetry::Sampler::XRay::InternalAWSXRayRemoteSampler.new(resource: resource) + + assert !sampler.instance_variable_get(:@rule_poller).nil? + assert_equal(sampler.instance_variable_get(:@rule_polling_interval_millis), 300 * 1000) + assert !sampler.instance_variable_get(:@sampling_client).nil? + assert_match(/[a-f0-9]{24}/, sampler.instance_variable_get(:@client_id)) + end + + it 'creates remote sampler with all fields populated' do + stub_request(:post, 'abc.com/GetSamplingRules') + .to_return(status: 200, body: File.read(DATA_DIR_SAMPLING_RULES)) + stub_request(:post, 'abc.com/SamplingTargets') + .to_return(status: 200, body: File.read(DATA_DIR_SAMPLING_TARGETS)) + + resource = OpenTelemetry::SDK::Resources::Resource.create( + OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME => 'test-service-name', + OpenTelemetry::SemanticConventions::Resource::CLOUD_PLATFORM => 'test-cloud-platform' + ) + sampler = OpenTelemetry::Sampler::XRay::InternalAWSXRayRemoteSampler.new( + resource: resource, + endpoint: 'abc.com', + polling_interval: 120 + ) + + assert !sampler.instance_variable_get(:@rule_poller).nil? + assert_equal(sampler.instance_variable_get(:@rule_polling_interval_millis), 120 * 1000) + assert !sampler.instance_variable_get(:@sampling_client).nil? + assert_equal(sampler.instance_variable_get(:@aws_proxy_endpoint), 'abc.com') + assert_match(/[a-f0-9]{24}/, sampler.instance_variable_get(:@client_id)) + end + + it 'generates valid client id' do + client_id = OpenTelemetry::Sampler::XRay::InternalAWSXRayRemoteSampler.generate_client_id + assert_match(/[0-9a-z]{24}/, client_id) + end + + it 'converts to string' do + stub_request(:post, "#{TEST_URL}/GetSamplingRules") + .to_return(status: 200, body: File.read(DATA_DIR_SAMPLING_RULES)) + stub_request(:post, "#{TEST_URL}/SamplingTargets") + .to_return(status: 200, body: File.read(DATA_DIR_SAMPLING_TARGETS)) + + sampler = OpenTelemetry::Sampler::XRay::InternalAWSXRayRemoteSampler.new(resource: OpenTelemetry::SDK::Resources::Resource.create) + expected_string = 'InternalAWSXRayRemoteSampler{aws_proxy_endpoint=127.0.0.1:2000, rule_polling_interval_millis=300000}' + assert_equal(sampler.description, expected_string) + end +end diff --git a/sampler/xray/test/aws_xray_sampling_client_test.rb b/sampler/xray/test/aws_xray_sampling_client_test.rb new file mode 100644 index 0000000000..413f7dfcf6 --- /dev/null +++ b/sampler/xray/test/aws_xray_sampling_client_test.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' +require 'json' + +describe OpenTelemetry::Sampler::XRay::AWSXRaySamplingClient do + DATA_DIR = File.join(__dir__, 'data') + TEST_URL = '127.0.0.1:2000' + + it 'test_get_no_sampling_rules' do + stub_request(:post, "#{TEST_URL}/GetSamplingRules") + .to_return(status: 200, body: { SamplingRuleRecords: [] }.to_json) + + client = OpenTelemetry::Sampler::XRay::AWSXRaySamplingClient.new(TEST_URL) + + client.fetch_sampling_rules do |response| + assert_equal 0, response[:SamplingRuleRecords]&.length + end + end + + it 'test_get_invalid_response' do + stub_request(:post, "#{TEST_URL}/GetSamplingRules") + .to_return(status: 200, body: {}.to_json) + + client = OpenTelemetry::Sampler::XRay::AWSXRaySamplingClient.new(TEST_URL) + + client.fetch_sampling_rules do |response| + assert_nil response[:SamplingRuleRecords]&.length + end + end + + it 'test_get_sampling_rule_missing_in_records' do + stub_request(:post, "#{TEST_URL}/GetSamplingRules") + .to_return(status: 200, body: { SamplingRuleRecords: [{}] }.to_json) + + client = OpenTelemetry::Sampler::XRay::AWSXRaySamplingClient.new(TEST_URL) + + client.fetch_sampling_rules do |response| + assert_equal 1, response[:SamplingRuleRecords]&.length + end + end + + it 'test_default_values_used_when_missing_properties_in_sampling_rule' do + stub_request(:post, "#{TEST_URL}/GetSamplingRules") + .to_return(status: 200, body: { SamplingRuleRecords: [{ SamplingRule: {} }] }.to_json) + + client = OpenTelemetry::Sampler::XRay::AWSXRaySamplingClient.new(TEST_URL) + + client.fetch_sampling_rules do |response| + assert_equal 1, response[:SamplingRuleRecords]&.length + rule = response[:SamplingRuleRecords]&.first&.[](:SamplingRule) + refute_nil rule + assert_nil rule[:Attributes] + assert_nil rule[:FixedRate] + assert_nil rule[:HTTPMethod] + assert_nil rule[:Host] + assert_nil rule[:Priority] + assert_nil rule[:ReservoirSize] + assert_nil rule[:ResourceARN] + assert_nil rule[:RuleARN] + assert_nil rule[:RuleName] + assert_nil rule[:ServiceName] + assert_nil rule[:ServiceType] + assert_nil rule[:URLPath] + assert_nil rule[:Version] + end + end + + it 'test_get_correct_number_of_sampling_rules' do + data = JSON.parse(File.read("#{DATA_DIR}/get-sampling-rules-response-sample.json")) + records = data['SamplingRuleRecords'] + + stub_request(:post, "#{TEST_URL}/GetSamplingRules") + .to_return(status: 200, body: data.to_json) + + client = OpenTelemetry::Sampler::XRay::AWSXRaySamplingClient.new(TEST_URL) + + client.fetch_sampling_rules do |response| + assert_equal records.length, response[:SamplingRuleRecords]&.length + + records.each_with_index do |record, i| + response_rule = response[:SamplingRuleRecords][i][:SamplingRule] + record_rule = record['SamplingRule'] + + assert_equal record_rule['Attributes'], response_rule[:Attributes] + assert_equal record_rule['FixedRate'], response_rule[:FixedRate] + assert_equal record_rule['HTTPMethod'], response_rule[:HTTPMethod] + assert_equal record_rule['Host'], response_rule[:Host] + assert_equal record_rule['Priority'], response_rule[:Priority] + assert_equal record_rule['ReservoirSize'], response_rule[:ReservoirSize] + assert_equal record_rule['ResourceARN'], response_rule[:ResourceARN] + assert_equal record_rule['RuleARN'], response_rule[:RuleARN] + assert_equal record_rule['RuleName'], response_rule[:RuleName] + assert_equal record_rule['ServiceName'], response_rule[:ServiceName] + assert_equal record_rule['ServiceType'], response_rule[:ServiceType] + assert_equal record_rule['URLPath'], response_rule[:URLPath] + assert_equal record_rule['Version'], response_rule[:Version] + end + end + end + + it 'test_get_sampling_targets' do + data = JSON.parse(File.read("#{DATA_DIR}/get-sampling-targets-response-sample.json")) + + stub_request(:post, "#{TEST_URL}/SamplingTargets") + .to_return(status: 200, body: data.to_json) + + client = OpenTelemetry::Sampler::XRay::AWSXRaySamplingClient.new(TEST_URL) + + client.fetch_sampling_targets(data) do |response| + assert_equal 2, response[:SamplingTargetDocuments].length + assert_equal 0, response[:UnprocessedStatistics].length + assert_equal 1_707_551_387, response[:LastRuleModification] + end + end + + it 'test_get_invalid_sampling_targets' do + data = { + LastRuleModification: nil, + SamplingTargetDocuments: nil, + UnprocessedStatistics: nil + } + + stub_request(:post, "#{TEST_URL}/SamplingTargets") + .to_return(status: 200, body: data.to_json) + + client = OpenTelemetry::Sampler::XRay::AWSXRaySamplingClient.new(TEST_URL) + + client.fetch_sampling_targets(data) do |response| + assert_nil response[:SamplingTargetDocuments] + assert_nil response[:UnprocessedStatistics] + assert_nil response[:LastRuleModification] + end + end +end diff --git a/sampler/xray/test/data/get-sampling-rules-response-sample-2.json b/sampler/xray/test/data/get-sampling-rules-response-sample-2.json new file mode 100644 index 0000000000..6bf24ebac9 --- /dev/null +++ b/sampler/xray/test/data/get-sampling-rules-response-sample-2.json @@ -0,0 +1,48 @@ +{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 1.676038494E9, + "ModifiedAt": 1.676038494E9, + "SamplingRule": { + "Attributes": { + "foo": "bar", + "abc": "1234" + }, + "FixedRate": 0.05, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 100, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 1.67799933E9, + "ModifiedAt": 1.67799933E9, + "SamplingRule": { + "Attributes": { + "abc": "1234" + }, + "FixedRate": 0.11, + "HTTPMethod": "*", + "Host": "*", + "Priority": 20, + "ReservoirSize": 1, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/test", + "RuleName": "test", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +} \ No newline at end of file diff --git a/sampler/xray/test/data/get-sampling-rules-response-sample-sample-all.json b/sampler/xray/test/data/get-sampling-rules-response-sample-sample-all.json new file mode 100644 index 0000000000..7a4b9ea0da --- /dev/null +++ b/sampler/xray/test/data/get-sampling-rules-response-sample-sample-all.json @@ -0,0 +1,24 @@ +{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 0.0, + "ModifiedAt": 1.611564245E9, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 1.00, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 1, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +} \ No newline at end of file diff --git a/sampler/xray/test/data/get-sampling-rules-response-sample.json b/sampler/xray/test/data/get-sampling-rules-response-sample.json new file mode 100644 index 0000000000..a0d3c5ba21 --- /dev/null +++ b/sampler/xray/test/data/get-sampling-rules-response-sample.json @@ -0,0 +1,65 @@ +{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 1.67799933E9, + "ModifiedAt": 1.67799933E9, + "SamplingRule": { + "Attributes": { + "foo": "bar", + "doo": "baz" + }, + "FixedRate": 0.05, + "HTTPMethod": "*", + "Host": "*", + "Priority": 1000, + "ReservoirSize": 10, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule1", + "RuleName": "Rule1", + "ServiceName": "*", + "ServiceType": "AWS::Foo::Bar", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 0.0, + "ModifiedAt": 1.611564245E9, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.05, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 1, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 1.676038494E9, + "ModifiedAt": 1.676038494E9, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 0.2, + "HTTPMethod": "GET", + "Host": "*", + "Priority": 1, + "ReservoirSize": 10, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-west-2:123456789000:sampling-rule/Rule2", + "RuleName": "Rule2", + "ServiceName": "FooBar", + "ServiceType": "*", + "URLPath": "/foo/bar", + "Version": 1 + } + } + ] +} \ No newline at end of file diff --git a/sampler/xray/test/data/get-sampling-targets-response-sample.json b/sampler/xray/test/data/get-sampling-targets-response-sample.json new file mode 100644 index 0000000000..498fe1505b --- /dev/null +++ b/sampler/xray/test/data/get-sampling-targets-response-sample.json @@ -0,0 +1,20 @@ +{ + "LastRuleModification": 1707551387.0, + "SamplingTargetDocuments": [ + { + "FixedRate": 0.10, + "Interval": 10, + "ReservoirQuota": 30, + "ReservoirQuotaTTL": 1707764006.0, + "RuleName": "test" + }, + { + "FixedRate": 0.05, + "Interval": 10, + "ReservoirQuota": 0, + "ReservoirQuotaTTL": 1707764006.0, + "RuleName": "Default" + } + ], + "UnprocessedStatistics": [] +} \ No newline at end of file diff --git a/sampler/xray/test/data/test-remote-sampler_sampling-rules-response-sample.json b/sampler/xray/test/data/test-remote-sampler_sampling-rules-response-sample.json new file mode 100644 index 0000000000..a5c0d2cb5b --- /dev/null +++ b/sampler/xray/test/data/test-remote-sampler_sampling-rules-response-sample.json @@ -0,0 +1,45 @@ +{ + "NextToken": null, + "SamplingRuleRecords": [ + { + "CreatedAt": 1.676038494E9, + "ModifiedAt": 1.676038494E9, + "SamplingRule": { + "Attributes": {}, + "FixedRate": 1.0, + "HTTPMethod": "*", + "Host": "*", + "Priority": 10000, + "ReservoirSize": 0, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/Default", + "RuleName": "Default", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + }, + { + "CreatedAt": 1.67799933E9, + "ModifiedAt": 1.67799933E9, + "SamplingRule": { + "Attributes": { + "abc": "1234" + }, + "FixedRate": 0, + "HTTPMethod": "*", + "Host": "*", + "Priority": 20, + "ReservoirSize": 0, + "ResourceARN": "*", + "RuleARN": "arn:aws:xray:us-east-1:999999999999:sampling-rule/test", + "RuleName": "test", + "ServiceName": "*", + "ServiceType": "*", + "URLPath": "*", + "Version": 1 + } + } + ] +} \ No newline at end of file diff --git a/sampler/xray/test/data/test-remote-sampler_sampling-targets-response-sample.json b/sampler/xray/test/data/test-remote-sampler_sampling-targets-response-sample.json new file mode 100644 index 0000000000..244bf0d06b --- /dev/null +++ b/sampler/xray/test/data/test-remote-sampler_sampling-targets-response-sample.json @@ -0,0 +1,20 @@ +{ + "LastRuleModification": 1707551387.0, + "SamplingTargetDocuments": [ + { + "FixedRate": 0.0, + "Interval": 100000, + "ReservoirQuota": 100000, + "ReservoirQuotaTTL": 9999999999.0, + "RuleName": "test" + }, + { + "FixedRate": 0.0, + "Interval": 1000, + "ReservoirQuota": 100, + "ReservoirQuotaTTL": 9999999999.0, + "RuleName": "Default" + } + ], + "UnprocessedStatistics": [] +} \ No newline at end of file diff --git a/sampler/xray/test/sampling_rule_applier_test.rb b/sampler/xray/test/sampling_rule_applier_test.rb new file mode 100644 index 0000000000..3509bfa9d2 --- /dev/null +++ b/sampler/xray/test/sampling_rule_applier_test.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' +require 'json' + +describe OpenTelemetry::Sampler::XRay::SamplingRuleApplier do +end diff --git a/sampler/xray/test/sampling_rule_test.rb b/sampler/xray/test/sampling_rule_test.rb new file mode 100644 index 0000000000..8ebedd3377 --- /dev/null +++ b/sampler/xray/test/sampling_rule_test.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Sampler::XRay::SamplingRule do + it 'test_sampling_rule_equality' do + rule = OpenTelemetry::Sampler::XRay::SamplingRule.new( + 'Attributes' => { 'abc' => '123', 'def' => '4?6', 'ghi' => '*89' }, + 'FixedRate' => 0.11, + 'HTTPMethod' => 'GET', + 'Host' => 'localhost', + 'Priority' => 20, + 'ReservoirSize' => 1, + 'ResourceARN' => '*', + 'RuleARN' => 'arn:aws:xray:us-east-1:999999999999:sampling-rule/test', + 'RuleName' => 'test', + 'ServiceName' => 'myServiceName', + 'ServiceType' => 'AWS::EKS::Container', + 'URLPath' => '/helloworld', + 'Version' => 1 + ) + + rule_unordered_attributes = OpenTelemetry::Sampler::XRay::SamplingRule.new( + 'Attributes' => { 'ghi' => '*89', 'abc' => '123', 'def' => '4?6' }, + 'FixedRate' => 0.11, + 'HTTPMethod' => 'GET', + 'Host' => 'localhost', + 'Priority' => 20, + 'ReservoirSize' => 1, + 'ResourceARN' => '*', + 'RuleARN' => 'arn:aws:xray:us-east-1:999999999999:sampling-rule/test', + 'RuleName' => 'test', + 'ServiceName' => 'myServiceName', + 'ServiceType' => 'AWS::EKS::Container', + 'URLPath' => '/helloworld', + 'Version' => 1 + ) + + rule_updated = OpenTelemetry::Sampler::XRay::SamplingRule.new( + 'Attributes' => { 'ghi' => '*89', 'abc' => '123', 'def' => '4?6' }, + 'FixedRate' => 0.11, + 'HTTPMethod' => 'GET', + 'Host' => 'localhost', + 'Priority' => 20, + 'ReservoirSize' => 1, + 'ResourceARN' => '*', + 'RuleARN' => 'arn:aws:xray:us-east-1:999999999999:sampling-rule/test', + 'RuleName' => 'test', + 'ServiceName' => 'myServiceName', + 'ServiceType' => 'AWS::EKS::Container', + 'URLPath' => '/helloworld_new', + 'Version' => 1 + ) + + rule_updated_two = OpenTelemetry::Sampler::XRay::SamplingRule.new( + 'Attributes' => { 'abc' => '128', 'def' => '4?6', 'ghi' => '*89' }, + 'FixedRate' => 0.11, + 'HTTPMethod' => 'GET', + 'Host' => 'localhost', + 'Priority' => 20, + 'ReservoirSize' => 1, + 'ResourceARN' => '*', + 'RuleARN' => 'arn:aws:xray:us-east-1:999999999999:sampling-rule/test', + 'RuleName' => 'test', + 'ServiceName' => 'myServiceName', + 'ServiceType' => 'AWS::EKS::Container', + 'URLPath' => '/helloworld', + 'Version' => 1 + ) + + assert_equal true, rule.equals?(rule_unordered_attributes) + assert_equal false, rule.equals?(rule_updated) + assert_equal false, rule.equals?(rule_updated_two) + end +end diff --git a/sampler/xray/test/statistics_test.rb b/sampler/xray/test/statistics_test.rb new file mode 100644 index 0000000000..bf14f05b22 --- /dev/null +++ b/sampler/xray/test/statistics_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Sampler::XRay::Statistics do + it 'test_construct_statistics_and_retrieve_statistics' do + statistics = OpenTelemetry::Sampler::XRay::Statistics.new(request_count: 12, sample_count: 3456, borrow_count: 7) + + assert_equal 12, statistics.instance_variable_get(:@request_count) + assert_equal 3456, statistics.instance_variable_get(:@sample_count) + assert_equal 7, statistics.instance_variable_get(:@borrow_count) + + obtained_statistics = statistics.retrieve_statistics + assert_equal 12, obtained_statistics[:request_count] + assert_equal 3456, obtained_statistics[:sample_count] + assert_equal 7, obtained_statistics[:borrow_count] + end +end diff --git a/sampler/xray/test/test_helper.rb b/sampler/xray/test/test_helper.rb new file mode 100644 index 0000000000..13ba2afe48 --- /dev/null +++ b/sampler/xray/test/test_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'simplecov' +require 'bundler/setup' +Bundler.require(:default, :development, :test) + +require 'opentelemetry-sampler-xray' +require 'minitest/autorun' +require 'webmock/minitest' + +OpenTelemetry.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym) diff --git a/sampler/xray/test/utils_test.rb b/sampler/xray/test/utils_test.rb new file mode 100644 index 0000000000..fdcf29952b --- /dev/null +++ b/sampler/xray/test/utils_test.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +# Copyright OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Sampler::XRay::Utils do + POSITIVE_TESTS = [ + ['*', ''], + ['foo', 'foo'], + ['foo*bar*?', 'foodbaris'], + ['?o?', 'foo'], + ['*oo', 'foo'], + ['foo*', 'foo'], + ['*o?', 'foo'], + ['*', 'boo'], + ['', ''], + ['a', 'a'], + ['*a', 'a'], + ['*a', 'ba'], + ['a*', 'a'], + ['a*', 'ab'], + ['a*a', 'aa'], + ['a*a', 'aba'], + ['a*a*', 'aaaaaaaaaaaaaaaaaaaaaaa'], + ['a*b*a*b*a*b*a*b*a*', 'akljd9gsdfbkjhaabajkhbbyiaahkjbjhbuykjakjhabkjhbabjhkaabbabbaaakljdfsjklababkjbsdabab'], + ['a*na*ha', 'anananahahanahanaha'], + ['***a', 'a'], + ['**a**', 'a'], + ['a**b', 'ab'], + ['*?', 'a'], + ['*??', 'aa'], + ['*?', 'a'], + ['*?*a*', 'ba'], + ['?at', 'bat'], + ['?at', 'cat'], + ['?o?se', 'horse'], + ['?o?se', 'mouse'], + ['*s', 'horses'], + ['J*', 'Jeep'], + ['J*', 'jeep'], + ['*/foo', '/bar/foo'], + ['ja*script', 'javascript'], + ['*', nil], + ['*', ''], + ['*', 'HelloWorld'], + ['HelloWorld', 'HelloWorld'], + ['Hello*', 'HelloWorld'], + ['*World', 'HelloWorld'], + ['?ello*', 'HelloWorld'], + ['Hell?W*d', 'HelloWorld'], + ['*.World', 'Hello.World'], + ['*.World', 'Bye.World'] + ].freeze + + NEGATIVE_TESTS = [ + ['', 'whatever'], + ['/', 'target'], + ['/', '/target'], + ['foo', 'bar'], + ['f?o', 'boo'], + ['f??', 'boo'], + ['fo*', 'boo'], + ['f?*', 'boo'], + ['abcd', 'abc'], + ['??', 'a'], + ['??', 'a'], + ['*?*a', 'a'], + ['a*na*ha', 'anananahahanahana'], + ['*s', 'horse'] + ].freeze + + it 'test_wildcard_match_with_only_wildcard' do + assert OpenTelemetry::Sampler::XRay::Utils.wildcard_match('*', nil) + end + + it 'test_wildcard_match_with_undefined_pattern' do + refute OpenTelemetry::Sampler::XRay::Utils.wildcard_match(nil, '') + end + + it 'test_wildcard_match_with_empty_pattern_and_text' do + assert OpenTelemetry::Sampler::XRay::Utils.wildcard_match('', '') + end + + it 'test_wildcard_match_with_regex_success' do + POSITIVE_TESTS.each do |test| + puts "#{test[0]} --- #{test[1]}" unless OpenTelemetry::Sampler::XRay::Utils.wildcard_match(test[0], test[1]) + assert OpenTelemetry::Sampler::XRay::Utils.wildcard_match(test[0], test[1]) + end + end + + it 'test_wildcard_match_with_regex_failure' do + NEGATIVE_TESTS.each do |test| + refute OpenTelemetry::Sampler::XRay::Utils.wildcard_match(test[0], test[1]) + end + end + + it 'test_attribute_match_with_undefined_attributes' do + rule_attributes = { 'string' => 'string', 'string2' => 'string2' } + refute OpenTelemetry::Sampler::XRay::Utils.attribute_match(nil, rule_attributes) + refute OpenTelemetry::Sampler::XRay::Utils.attribute_match({}, rule_attributes) + refute OpenTelemetry::Sampler::XRay::Utils.attribute_match({ 'string' => 'string' }, rule_attributes) + end + + it 'test_attribute_match_with_undefined_rule_attributes' do + attr = { + 'number' => 1, + 'string' => 'string', + 'undefined' => nil, + 'boolean' => true + } + assert OpenTelemetry::Sampler::XRay::Utils.attribute_match(attr, nil) + end + + it 'test_attribute_match_successful_match' do + attr = { 'language' => 'english' } + rule_attribute = { 'language' => 'en*sh' } + assert OpenTelemetry::Sampler::XRay::Utils.attribute_match(attr, rule_attribute) + end + + it 'test_attribute_match_failed_match' do + attr = { 'language' => 'french' } + rule_attribute = { 'language' => 'en*sh' } + refute OpenTelemetry::Sampler::XRay::Utils.attribute_match(attr, rule_attribute) + end + + it 'test_attribute_match_extra_attributes_success' do + attr = { + 'number' => 1, + 'string' => 'string', + 'undefined' => nil, + 'boolean' => true + } + rule_attribute = { 'string' => 'string' } + assert OpenTelemetry::Sampler::XRay::Utils.attribute_match(attr, rule_attribute) + end + + it 'test_attribute_match_extra_attributes_failure' do + attr = { + 'number' => 1, + 'string' => 'string', + 'undefined' => nil, + 'boolean' => true + } + rule_attribute = { 'string' => 'string', 'number' => '1' } + refute OpenTelemetry::Sampler::XRay::Utils.attribute_match(attr, rule_attribute) + end +end