From 0cf248ee88fb2d43cad77a710b55e9875684d604 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 15 Sep 2023 13:57:59 +0200 Subject: [PATCH 01/45] implement CI's own instrument method, remove dependency on Datadog::Tracing::Contrib --- lib/datadog/ci/configuration/settings.rb | 19 +-- .../ci/contrib/cucumber/integration.rb | 6 +- lib/datadog/ci/contrib/integration.rb | 37 ++++++ .../ci/contrib/minitest/integration.rb | 6 +- lib/datadog/ci/contrib/rspec/integration.rb | 6 +- sig/datadog/ci/contrib/integration.rbs | 23 ++++ .../datadog/ci/configuration/settings_spec.rb | 116 ++++++++++++++++++ 7 files changed, 193 insertions(+), 20 deletions(-) create mode 100644 lib/datadog/ci/contrib/integration.rb create mode 100644 sig/datadog/ci/contrib/integration.rbs diff --git a/lib/datadog/ci/configuration/settings.rb b/lib/datadog/ci/configuration/settings.rb index fc3c38f9..53accfbc 100644 --- a/lib/datadog/ci/configuration/settings.rb +++ b/lib/datadog/ci/configuration/settings.rb @@ -21,16 +21,19 @@ def self.add_settings!(base) o.default false end - # DEV: Alias to Datadog::Tracing::Contrib::Extensions::Configuration::Settings#instrument. - # DEV: Should be removed when CI implement its own `c.ci.instrument`. define_method(:instrument) do |integration_name, options = {}, &block| - Datadog.configuration.tracing.instrument(integration_name, options, &block) - end + return unless enabled + + registered_integration = Datadog::CI::Contrib::Integration.registry[integration_name] + return unless registered_integration + + klass = registered_integration.klass + return unless klass.loaded? && klass.compatible? + + instance = klass.new + return if instance.patcher.patched? - # DEV: Alias to Datadog::Tracing::Contrib::Extensions::Configuration::Settings#instrument. - # DEV: Should be removed when CI implement its own `c.ci[]`. - define_method(:[]) do |integration_name, key = :default| - Datadog.configuration.tracing[integration_name, key] + instance.patcher.patch end # TODO: Deprecate in the next major version, as `instrument` better describes this method's purpose diff --git a/lib/datadog/ci/contrib/cucumber/integration.rb b/lib/datadog/ci/contrib/cucumber/integration.rb index 69b2e0fd..c22b8e03 100644 --- a/lib/datadog/ci/contrib/cucumber/integration.rb +++ b/lib/datadog/ci/contrib/cucumber/integration.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require "datadog/tracing/contrib" -require "datadog/tracing/contrib/integration" - +require_relative "../integration" require_relative "configuration/settings" require_relative "patcher" @@ -12,7 +10,7 @@ module Contrib module Cucumber # Description of Cucumber integration class Integration - include Datadog::Tracing::Contrib::Integration + include Datadog::CI::Contrib::Integration MINIMUM_VERSION = Gem::Version.new("3.0.0") diff --git a/lib/datadog/ci/contrib/integration.rb b/lib/datadog/ci/contrib/integration.rb new file mode 100644 index 00000000..a9073895 --- /dev/null +++ b/lib/datadog/ci/contrib/integration.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Datadog + module CI + module Contrib + # Base provides features that are shared across all integrations + module Integration + @registry = {} + + RegisteredIntegration = Struct.new(:name, :klass, :options) + + def self.included(base) + base.extend(ClassMethods) + end + + # Class-level methods for Integration + module ClassMethods + def register_as(name, options = {}) + Integration.register(self, name, options) + end + + def compatible? + true + end + end + + def self.register(integration, name, options) + registry[name] = RegisteredIntegration.new(name, integration, options) + end + + def self.registry + @registry + end + end + end + end +end diff --git a/lib/datadog/ci/contrib/minitest/integration.rb b/lib/datadog/ci/contrib/minitest/integration.rb index 557289ff..509506fb 100644 --- a/lib/datadog/ci/contrib/minitest/integration.rb +++ b/lib/datadog/ci/contrib/minitest/integration.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require "datadog/tracing/contrib" -require "datadog/tracing/contrib/integration" - +require_relative "../integration" require_relative "configuration/settings" require_relative "patcher" @@ -12,7 +10,7 @@ module Contrib module Minitest # Description of Minitest integration class Integration - include Datadog::Tracing::Contrib::Integration + include Datadog::CI::Contrib::Integration MINIMUM_VERSION = Gem::Version.new("5.0.0") diff --git a/lib/datadog/ci/contrib/rspec/integration.rb b/lib/datadog/ci/contrib/rspec/integration.rb index ac076b90..190fa9c4 100644 --- a/lib/datadog/ci/contrib/rspec/integration.rb +++ b/lib/datadog/ci/contrib/rspec/integration.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require "datadog/tracing/contrib" -require "datadog/tracing/contrib/integration" - +require_relative "../integration" require_relative "configuration/settings" require_relative "patcher" @@ -12,7 +10,7 @@ module Contrib module RSpec # Description of RSpec integration class Integration - include Datadog::Tracing::Contrib::Integration + include Datadog::CI::Contrib::Integration MINIMUM_VERSION = Gem::Version.new("3.0.0") diff --git a/sig/datadog/ci/contrib/integration.rbs b/sig/datadog/ci/contrib/integration.rbs new file mode 100644 index 00000000..835ea588 --- /dev/null +++ b/sig/datadog/ci/contrib/integration.rbs @@ -0,0 +1,23 @@ +module Datadog + module CI + module Contrib + module Integration + # TODO: replace with Struct[] when https://github.com/ruby/rbs/pull/1456 is merged + RegisteredIntegration: untyped + + self.@registry: Hash[Symbol, Struct[untyped]] + + def self.included: (Module base) -> void + module ClassMethods + def register_as: (Symbol name, ?::Hash[Symbol, untyped] options) -> void + + def compatible?: () -> bool + end + + def self.register: (Object integration, Symbol name, ::Hash[Symbol, untyped] options) -> void + + def self.registry: () -> Hash[Symbol, Struct[untyped]] + end + end + end +end diff --git a/spec/datadog/ci/configuration/settings_spec.rb b/spec/datadog/ci/configuration/settings_spec.rb index 5ff2dcc3..75765b67 100644 --- a/spec/datadog/ci/configuration/settings_spec.rb +++ b/spec/datadog/ci/configuration/settings_spec.rb @@ -1,3 +1,38 @@ +# Dummy Integration +class FakeIntegration + module Patcher + module_function + + def patched? + @patched + end + + def patch + @patched = true + end + + def reset + @patched = nil + end + end + + def self.loaded? + true + end + + def self.compatible? + true + end + + def self.auto_instrument? + false + end + + def patcher + Patcher + end +end + RSpec.describe Datadog::CI::Configuration::Settings do context "when used to extend Datadog::Core::Configuration::Settings" do subject(:settings) do @@ -53,6 +88,87 @@ end end + describe "#instrument" do + let(:registry) { {} } + let(:integration_name) { :fake } + + subject(:instrument) { settings.ci.instrument(integration_name) } + + before do + registry[integration_name] = instance_double( + Datadog::CI::Contrib::Integration::RegisteredIntegration, + klass: FakeIntegration + ) + + allow(Datadog::CI::Contrib::Integration).to receive(:registry).and_return(registry) + settings.ci.enabled = ci_enabled + end + + after do + FakeIntegration::Patcher.reset + end + + context "ci enabled" do + let(:ci_enabled) { true } + + context "when integration exists" do + context "when loaded and compatible" do + it "patches the integration" do + expect(FakeIntegration::Patcher).to receive(:patch) + + instrument + end + end + + context "when called multiple times" do + it "does not patch the integration multiple times" do + expect(FakeIntegration::Patcher).to receive(:patch).and_call_original.once + + instrument + instrument + end + end + + context "when not loaded" do + before { expect(FakeIntegration).to receive(:loaded?).and_return(false) } + + it "does not patch the integration" do + expect(FakeIntegration::Patcher).to_not receive(:patch) + + instrument + end + end + + context "when loaded and not compatible" do + before { expect(FakeIntegration).to receive(:compatible?).and_return(false) } + + it "does not patch the integration" do + expect(FakeIntegration::Patcher).to_not receive(:patch) + + instrument + end + end + + context "when integration does not exist" do + let(:integration_name) { :not_exiting } + + it "does not patch the integration" do + expect { instrument }.to_not raise_error + end + end + end + + context "ci is not enabled" do + let(:ci_enabled) { false } + + it "does not patch the integration" do + expect(FakeIntegration::Patcher).to_not receive(:patch) + instrument + end + end + end + end + describe "#trace_flush" do subject(:trace_flush) { settings.ci.trace_flush } From 4831625c053f0de7e09ce2c16f411974c2b01a3f Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 15 Sep 2023 17:01:47 +0200 Subject: [PATCH 02/45] continuing decoupling with Tracing where possible --- lib/datadog/ci/configuration/settings.rb | 29 ++++++++---- lib/datadog/ci/contrib/integration.rb | 17 +++---- lib/datadog/ci/test.rb | 9 ++-- .../datadog/ci/configuration/settings_spec.rb | 44 ++++++++++++------- .../ci/contrib/cucumber/integration_spec.rb | 2 +- .../ci/contrib/minitest/integration_spec.rb | 2 +- .../ci/contrib/rspec/integration_spec.rb | 2 +- .../ci/contrib/support/mode_helpers.rb | 3 ++ .../ci/contrib/support/tracer_helpers.rb | 2 + spec/support/tracer_helpers.rb | 1 + 10 files changed, 73 insertions(+), 38 deletions(-) diff --git a/lib/datadog/ci/configuration/settings.rb b/lib/datadog/ci/configuration/settings.rb index 53accfbc..f6abb636 100644 --- a/lib/datadog/ci/configuration/settings.rb +++ b/lib/datadog/ci/configuration/settings.rb @@ -5,8 +5,10 @@ module Datadog module CI module Configuration - # Adds CI behavior to Datadog trace settings + # Adds CI behavior to ddtrace settings module Settings + InvalidIntegrationError = Class.new(StandardError) + def self.extended(base) base = base.singleton_class unless base.is_a?(Class) add_settings!(base) @@ -24,16 +26,20 @@ def self.add_settings!(base) define_method(:instrument) do |integration_name, options = {}, &block| return unless enabled - registered_integration = Datadog::CI::Contrib::Integration.registry[integration_name] - return unless registered_integration + integration = fetch_integration(integration_name) + return unless integration.class.compatible? + + return unless integration.default_configuration.enabled + integration.configure(:default, options, &block) - klass = registered_integration.klass - return unless klass.loaded? && klass.compatible? + return if integration.patcher.patched? + integration.patcher.patch + end - instance = klass.new - return if instance.patcher.patched? + define_method(:[]) do |integration_name, key = :default| + integration = fetch_integration(integration_name) - instance.patcher.patch + integration.resolve(key) unless integration.nil? end # TODO: Deprecate in the next major version, as `instrument` better describes this method's purpose @@ -45,6 +51,13 @@ def self.add_settings!(base) o.type :hash o.default({}) end + + define_method(:fetch_integration) do |name| + registered = Datadog::CI::Contrib::Integration.registry[name] + raise(InvalidIntegrationError, "'#{name}' is not a valid integration.") if registered.nil? + + registered.integration + end end end end diff --git a/lib/datadog/ci/contrib/integration.rb b/lib/datadog/ci/contrib/integration.rb index a9073895..d6bb3631 100644 --- a/lib/datadog/ci/contrib/integration.rb +++ b/lib/datadog/ci/contrib/integration.rb @@ -1,16 +1,21 @@ # frozen_string_literal: true +require "datadog/tracing/contrib/configurable" +require "datadog/tracing/contrib/patchable" + module Datadog module CI module Contrib - # Base provides features that are shared across all integrations module Integration @registry = {} - RegisteredIntegration = Struct.new(:name, :klass, :options) + RegisteredIntegration = Struct.new(:name, :integration, :options) def self.included(base) base.extend(ClassMethods) + + base.include(Datadog::Tracing::Contrib::Patchable) + base.include(Datadog::Tracing::Contrib::Configurable) end # Class-level methods for Integration @@ -18,14 +23,10 @@ module ClassMethods def register_as(name, options = {}) Integration.register(self, name, options) end - - def compatible? - true - end end - def self.register(integration, name, options) - registry[name] = RegisteredIntegration.new(name, integration, options) + def self.register(klass, name, options) + registry[name] = RegisteredIntegration.new(name, klass.new, options) end def self.registry diff --git a/lib/datadog/ci/test.rb b/lib/datadog/ci/test.rb index 2548c062..04e5c132 100644 --- a/lib/datadog/ci/test.rb +++ b/lib/datadog/ci/test.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "datadog/tracing" require "datadog/tracing/contrib/analytics" require_relative "ext/app_types" @@ -19,13 +20,13 @@ def self.trace(span_name, options = {}) }.merge(options[:span_options] || {}) if block_given? - Tracing.trace(span_name, **span_options) do |span, trace| + ::Datadog::Tracing.trace(span_name, **span_options) do |span, trace| set_tags!(trace, span, options) yield(span, trace) end else - span = Tracing.trace(span_name, **span_options) - trace = Tracing.active_trace + span = ::Datadog::Tracing.trace(span_name, **span_options) + trace = ::Datadog::Tracing.active_trace set_tags!(trace, span, options) span end @@ -37,7 +38,7 @@ def self.set_tags!(trace, span, tags = {}) # Set default tags trace.origin = Ext::Test::CONTEXT_ORIGIN if trace - Datadog::Tracing::Contrib::Analytics.set_measured(span) + ::Datadog::Tracing::Contrib::Analytics.set_measured(span) span.set_tag(Ext::Test::TAG_SPAN_KIND, Ext::AppTypes::TYPE_TEST) # Set environment tags diff --git a/spec/datadog/ci/configuration/settings_spec.rb b/spec/datadog/ci/configuration/settings_spec.rb index 75765b67..69de4bbf 100644 --- a/spec/datadog/ci/configuration/settings_spec.rb +++ b/spec/datadog/ci/configuration/settings_spec.rb @@ -1,5 +1,9 @@ # Dummy Integration class FakeIntegration + def initialize(configuration) + @configuration = configuration + end + module Patcher module_function @@ -31,6 +35,13 @@ def self.auto_instrument? def patcher Patcher end + + def default_configuration + @configuration + end + + def configure(name, options, &block) + end end RSpec.describe Datadog::CI::Configuration::Settings do @@ -92,12 +103,15 @@ def patcher let(:registry) { {} } let(:integration_name) { :fake } + let(:integration_config) { double(enabled: true) } + let(:integration) { FakeIntegration.new(integration_config) } + subject(:instrument) { settings.ci.instrument(integration_name) } before do registry[integration_name] = instance_double( Datadog::CI::Contrib::Integration::RegisteredIntegration, - klass: FakeIntegration + integration: integration ) allow(Datadog::CI::Contrib::Integration).to receive(:registry).and_return(registry) @@ -112,12 +126,10 @@ def patcher let(:ci_enabled) { true } context "when integration exists" do - context "when loaded and compatible" do - it "patches the integration" do - expect(FakeIntegration::Patcher).to receive(:patch) + it "patches the integration" do + expect(FakeIntegration::Patcher).to receive(:patch) - instrument - end + instrument end context "when called multiple times" do @@ -129,8 +141,8 @@ def patcher end end - context "when not loaded" do - before { expect(FakeIntegration).to receive(:loaded?).and_return(false) } + context "when not compatible" do + before { expect(FakeIntegration).to receive(:compatible?).and_return(false) } it "does not patch the integration" do expect(FakeIntegration::Patcher).to_not receive(:patch) @@ -139,8 +151,10 @@ def patcher end end - context "when loaded and not compatible" do - before { expect(FakeIntegration).to receive(:compatible?).and_return(false) } + context "when not enabled" do + before do + allow(integration_config).to receive(:enabled).and_return(false) + end it "does not patch the integration" do expect(FakeIntegration::Patcher).to_not receive(:patch) @@ -148,13 +162,13 @@ def patcher instrument end end + end - context "when integration does not exist" do - let(:integration_name) { :not_exiting } + context "when integration does not exist" do + let(:integration_name) { :not_exiting } - it "does not patch the integration" do - expect { instrument }.to_not raise_error - end + it "does not patch the integration" do + expect { instrument }.to_not raise_error end end diff --git a/spec/datadog/ci/contrib/cucumber/integration_spec.rb b/spec/datadog/ci/contrib/cucumber/integration_spec.rb index 8a5a0321..03d30e61 100644 --- a/spec/datadog/ci/contrib/cucumber/integration_spec.rb +++ b/spec/datadog/ci/contrib/cucumber/integration_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Datadog::CI::Contrib::Cucumber::Integration do extend ConfigurationHelpers - let(:integration) { described_class.new(:cucumber) } + let(:integration) { described_class.new } describe ".version" do subject(:version) { described_class.version } diff --git a/spec/datadog/ci/contrib/minitest/integration_spec.rb b/spec/datadog/ci/contrib/minitest/integration_spec.rb index b0ce04a3..2bd1f4f5 100644 --- a/spec/datadog/ci/contrib/minitest/integration_spec.rb +++ b/spec/datadog/ci/contrib/minitest/integration_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Datadog::CI::Contrib::Minitest::Integration do extend ConfigurationHelpers - let(:integration) { described_class.new(:minitest) } + let(:integration) { described_class.new } describe ".version" do subject(:version) { described_class.version } diff --git a/spec/datadog/ci/contrib/rspec/integration_spec.rb b/spec/datadog/ci/contrib/rspec/integration_spec.rb index 6221e82f..40c6be49 100644 --- a/spec/datadog/ci/contrib/rspec/integration_spec.rb +++ b/spec/datadog/ci/contrib/rspec/integration_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Datadog::CI::Contrib::RSpec::Integration do extend ConfigurationHelpers - let(:integration) { described_class.new(:rspec) } + let(:integration) { described_class.new } describe ".version" do subject(:version) { described_class.version } diff --git a/spec/datadog/ci/contrib/support/mode_helpers.rb b/spec/datadog/ci/contrib/support/mode_helpers.rb index 434f9afa..9a9f84f1 100644 --- a/spec/datadog/ci/contrib/support/mode_helpers.rb +++ b/spec/datadog/ci/contrib/support/mode_helpers.rb @@ -8,6 +8,9 @@ let(:components) { Datadog::Core::Configuration::Components.new(settings) } before do + # TODO: this is a very hacky way that messes with Core's internals + allow_any_instance_of(Datadog::Core::Configuration).to receive(:configuration).and_return(settings) + allow(Datadog::Tracing) .to receive(:tracer) .and_return(components.tracer) diff --git a/spec/datadog/ci/contrib/support/tracer_helpers.rb b/spec/datadog/ci/contrib/support/tracer_helpers.rb index 03c7c3a3..100ca5ae 100644 --- a/spec/datadog/ci/contrib/support/tracer_helpers.rb +++ b/spec/datadog/ci/contrib/support/tracer_helpers.rb @@ -1,3 +1,5 @@ +require "datadog/tracing" + module Contrib # Contrib-specific tracer helpers. # For contrib, we only allow one tracer to be active: diff --git a/spec/support/tracer_helpers.rb b/spec/support/tracer_helpers.rb index a2e5a42e..16987012 100644 --- a/spec/support/tracer_helpers.rb +++ b/spec/support/tracer_helpers.rb @@ -1,3 +1,4 @@ +require "datadog/tracing" require_relative "faux_writer" module TracerHelpers From e7a1218e7daecd174b0002190caaeddc9ba5cb75 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 18 Sep 2023 14:21:47 +0200 Subject: [PATCH 03/45] migrate patchable and configurable logic for integration from tracing --- lib/datadog/ci/configuration/settings.rb | 19 ++- .../cucumber/configuration/settings.rb | 5 +- .../ci/contrib/cucumber/integration.rb | 2 +- lib/datadog/ci/contrib/integration.rb | 133 ++++++++++++++++-- .../minitest/configuration/settings.rb | 5 +- .../ci/contrib/minitest/integration.rb | 2 +- .../contrib/rspec/configuration/settings.rb | 5 +- lib/datadog/ci/contrib/rspec/integration.rb | 2 +- lib/datadog/ci/contrib/settings.rb | 33 +++++ sig/datadog/ci/configuration/settings.rbs | 2 + .../cucumber/configuration/settings.rbs | 2 +- .../ci/contrib/cucumber/integration.rbs | 3 +- sig/datadog/ci/contrib/integration.rbs | 30 +++- .../minitest/configuration/settings.rbs | 2 +- .../ci/contrib/minitest/integration.rbs | 7 +- .../contrib/rspec/configuration/settings.rbs | 2 +- sig/datadog/ci/contrib/rspec/integration.rbs | 5 +- sig/datadog/ci/contrib/settings.rbs | 19 +++ .../datadog/ci/configuration/settings_spec.rb | 63 +++++---- .../ci/contrib/cucumber/integration_spec.rb | 4 +- .../ci/contrib/minitest/integration_spec.rb | 4 +- .../ci/contrib/rspec/integration_spec.rb | 4 +- .../contrib/configuration/settings.rbs | 18 --- .../0/datadog/tracing/contrib/integration.rbs | 13 -- 24 files changed, 278 insertions(+), 106 deletions(-) create mode 100644 lib/datadog/ci/contrib/settings.rb create mode 100644 sig/datadog/ci/contrib/settings.rbs delete mode 100644 vendor/rbs/ddtrace/0/datadog/tracing/contrib/configuration/settings.rbs delete mode 100644 vendor/rbs/ddtrace/0/datadog/tracing/contrib/integration.rbs diff --git a/lib/datadog/ci/configuration/settings.rb b/lib/datadog/ci/configuration/settings.rb index f6abb636..8a6e856f 100644 --- a/lib/datadog/ci/configuration/settings.rb +++ b/lib/datadog/ci/configuration/settings.rb @@ -27,19 +27,24 @@ def self.add_settings!(base) return unless enabled integration = fetch_integration(integration_name) - return unless integration.class.compatible? + integration.configure(options, &block) - return unless integration.default_configuration.enabled - integration.configure(:default, options, &block) + return unless integration.configuration.enabled - return if integration.patcher.patched? - integration.patcher.patch + patch_results = integration.patch + next if patch_results == true + + error_message = <<-ERROR + Available?: #{patch_results[:available]}, Loaded?: #{patch_results[:loaded]}, + Compatible?: #{patch_results[:compatible]}, Patchable?: #{patch_results[:patchable]}" + ERROR + Datadog.logger.warn("Unable to patch #{integration_name} (#{error_message})") end - define_method(:[]) do |integration_name, key = :default| + define_method(:[]) do |integration_name| integration = fetch_integration(integration_name) - integration.resolve(key) unless integration.nil? + integration.configuration unless integration.nil? end # TODO: Deprecate in the next major version, as `instrument` better describes this method's purpose diff --git a/lib/datadog/ci/contrib/cucumber/configuration/settings.rb b/lib/datadog/ci/contrib/cucumber/configuration/settings.rb index 713094e6..da7bd635 100644 --- a/lib/datadog/ci/contrib/cucumber/configuration/settings.rb +++ b/lib/datadog/ci/contrib/cucumber/configuration/settings.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true require_relative "../ext" - -require "datadog/tracing/contrib/configuration/settings" +require_relative "../../settings" module Datadog module CI @@ -11,7 +10,7 @@ module Cucumber module Configuration # Custom settings for the Cucumber integration # TODO: mark as `@public_api` when GA - class Settings < Datadog::Tracing::Contrib::Configuration::Settings + class Settings < Datadog::CI::Contrib::Settings option :enabled do |o| o.type :bool o.env Ext::ENV_ENABLED diff --git a/lib/datadog/ci/contrib/cucumber/integration.rb b/lib/datadog/ci/contrib/cucumber/integration.rb index c22b8e03..1739a13c 100644 --- a/lib/datadog/ci/contrib/cucumber/integration.rb +++ b/lib/datadog/ci/contrib/cucumber/integration.rb @@ -14,7 +14,7 @@ class Integration MINIMUM_VERSION = Gem::Version.new("3.0.0") - register_as :cucumber, auto_patch: true + register_as :cucumber def self.version Gem.loaded_specs["cucumber"] \ diff --git a/lib/datadog/ci/contrib/integration.rb b/lib/datadog/ci/contrib/integration.rb index d6bb3631..e3cd4d31 100644 --- a/lib/datadog/ci/contrib/integration.rb +++ b/lib/datadog/ci/contrib/integration.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -require "datadog/tracing/contrib/configurable" -require "datadog/tracing/contrib/patchable" +require_relative "settings" module Datadog module CI @@ -9,28 +8,138 @@ module Contrib module Integration @registry = {} - RegisteredIntegration = Struct.new(:name, :integration, :options) + RegisteredIntegration = Struct.new(:name, :integration) def self.included(base) base.extend(ClassMethods) + base.include(InstanceMethods) + end + + def self.register(klass, name) + registry[name] = RegisteredIntegration.new(name, klass.new) + end - base.include(Datadog::Tracing::Contrib::Patchable) - base.include(Datadog::Tracing::Contrib::Configurable) + def self.registry + @registry end # Class-level methods for Integration module ClassMethods - def register_as(name, options = {}) - Integration.register(self, name, options) + def register_as(name) + Integration.register(self, name) + end + + # Version of the integration target code in the environment. + # + # This is the gem version, when the instrumentation target is a Ruby gem. + # + # If the target for instrumentation has concept of versioning, override {.version}, + # otherwise override {.available?} and implement a custom target presence check. + # @return [Object] the target version + def version + nil end - end - def self.register(klass, name, options) - registry[name] = RegisteredIntegration.new(name, klass.new, options) + # Is the target available to be instrumented? (e.g. gem installed?) + # + # The target doesn't have to be loaded (e.g. `require`) yet, but needs to be able + # to be loaded before instrumentation can commence. + # + # By default, {.available?} checks if {.version} returned a non-nil object. + # + # If the target for instrumentation has concept of versioning, override {.version}, + # otherwise override {.available?} and implement a custom target presence check. + # @return [Boolean] is the target available for instrumentation in this Ruby environment? + def available? + !version.nil? + end + + # Is the target loaded into the application? (e.g. gem required? Constant defined?) + # + # The target's objects should be ready to be referenced by the instrumented when {.loaded} + # returns `true`. + # + # @return [Boolean] is the target ready to be referenced during instrumentation? + def loaded? + true + end + + # Is this instrumentation compatible with the available target? (e.g. minimum version met?) + # @return [Boolean] is the available target compatible with this instrumentation? + def compatible? + available? + end + + # Can the patch for this integration be applied? + # + # By default, this is equivalent to {#available?}, {#loaded?}, and {#compatible?} + # all being truthy. + def patchable? + available? && loaded? && compatible? + end end - def self.registry - @registry + module InstanceMethods + # returns the configuration instance. + def configuration + @configuration ||= new_configuration + end + + def configure(options = {}, &block) + configuration.configure(options, &block) + configuration + end + + # Resets all configuration options + def reset_configuration! + @configuration = nil + end + + # The patcher module to inject instrumented objects into the instrumentation target. + # + # {Contrib::Patcher} includes the basic functionality of a patcher. `include`ing + # {Contrib::Patcher} into a new module is the recommend way to create a custom patcher. + # + # @return [Contrib::Patcher] a module that `include`s {Contrib::Patcher} + def patcher + nil + end + + # @!visibility private + def patch + # @type var patcher_klass: untyped + patcher_klass = patcher + if !self.class.patchable? || patcher_klass.nil? + return { + available: self.class.available?, + loaded: self.class.loaded?, + compatible: self.class.compatible?, + patchable: self.class.patchable? + } + end + + patcher_klass.patch + true + end + + # Can the patch for this integration be applied automatically? + # @return [Boolean] can the tracer activate this instrumentation without explicit user input? + def auto_instrument? + true + end + + protected + + # Returns a new configuration object for this integration. + # + # This method normally needs to be overridden for each integration + # as their settings, defaults and environment variables are + # specific for each integration. + # + # @return [Datadog::CI::Contrib::Settings] a new, integration-specific settings object + def new_configuration + Datadog::CI::Contrib::Settings.new + end end end end diff --git a/lib/datadog/ci/contrib/minitest/configuration/settings.rb b/lib/datadog/ci/contrib/minitest/configuration/settings.rb index f412978e..a631ad43 100644 --- a/lib/datadog/ci/contrib/minitest/configuration/settings.rb +++ b/lib/datadog/ci/contrib/minitest/configuration/settings.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true require_relative "../ext" - -require "datadog/tracing/contrib/configuration/settings" +require_relative "../../settings" module Datadog module CI @@ -11,7 +10,7 @@ module Minitest module Configuration # Custom settings for the Minitest integration # TODO: mark as `@public_api` when GA - class Settings < Datadog::Tracing::Contrib::Configuration::Settings + class Settings < Datadog::CI::Contrib::Settings option :enabled do |o| o.type :bool o.env Ext::ENV_ENABLED diff --git a/lib/datadog/ci/contrib/minitest/integration.rb b/lib/datadog/ci/contrib/minitest/integration.rb index 509506fb..66382afb 100644 --- a/lib/datadog/ci/contrib/minitest/integration.rb +++ b/lib/datadog/ci/contrib/minitest/integration.rb @@ -14,7 +14,7 @@ class Integration MINIMUM_VERSION = Gem::Version.new("5.0.0") - register_as :minitest, auto_patch: true + register_as :minitest def self.version Gem.loaded_specs["minitest"] && Gem.loaded_specs["minitest"].version diff --git a/lib/datadog/ci/contrib/rspec/configuration/settings.rb b/lib/datadog/ci/contrib/rspec/configuration/settings.rb index c573b9e5..82a12a01 100644 --- a/lib/datadog/ci/contrib/rspec/configuration/settings.rb +++ b/lib/datadog/ci/contrib/rspec/configuration/settings.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true require_relative "../ext" - -require "datadog/tracing/contrib/configuration/settings" +require_relative "../../settings" module Datadog module CI @@ -11,7 +10,7 @@ module RSpec module Configuration # Custom settings for the RSpec integration # TODO: mark as `@public_api` when GA - class Settings < Datadog::Tracing::Contrib::Configuration::Settings + class Settings < Datadog::CI::Contrib::Settings option :enabled do |o| o.type :bool o.env Ext::ENV_ENABLED diff --git a/lib/datadog/ci/contrib/rspec/integration.rb b/lib/datadog/ci/contrib/rspec/integration.rb index 190fa9c4..dbd625cc 100644 --- a/lib/datadog/ci/contrib/rspec/integration.rb +++ b/lib/datadog/ci/contrib/rspec/integration.rb @@ -14,7 +14,7 @@ class Integration MINIMUM_VERSION = Gem::Version.new("3.0.0") - register_as :rspec, auto_patch: true + register_as :rspec def self.version Gem.loaded_specs["rspec-core"] \ diff --git a/lib/datadog/ci/contrib/settings.rb b/lib/datadog/ci/contrib/settings.rb new file mode 100644 index 00000000..41878252 --- /dev/null +++ b/lib/datadog/ci/contrib/settings.rb @@ -0,0 +1,33 @@ +require "datadog/core/configuration/base" + +module Datadog + module CI + module Contrib + # Common settings for all integrations + # @public_api + class Settings + include Core::Configuration::Base + + option :enabled, default: true + option :service_name + option :operation_name + + def configure(options = {}) + self.class.options.each do |name, _value| + self[name] = options[name] if options.key?(name) + end + + yield(self) if block_given? + end + + def [](name) + respond_to?(name) ? send(name) : get_option(name) + end + + def []=(name, value) + respond_to?("#{name}=") ? send("#{name}=", value) : set_option(name, value) + end + end + end + end +end diff --git a/sig/datadog/ci/configuration/settings.rbs b/sig/datadog/ci/configuration/settings.rbs index 3d887b0f..d6498cb7 100644 --- a/sig/datadog/ci/configuration/settings.rbs +++ b/sig/datadog/ci/configuration/settings.rbs @@ -3,6 +3,8 @@ module Datadog module Configuration module Settings extend Datadog::Core::Configuration::Options::ClassMethods + include Datadog::Core::Configuration::Options::InstanceMethods + extend Datadog::Core::Configuration::Base::ClassMethods def self.extended: (untyped base) -> untyped diff --git a/sig/datadog/ci/contrib/cucumber/configuration/settings.rbs b/sig/datadog/ci/contrib/cucumber/configuration/settings.rbs index f14bef8a..def7f7a4 100644 --- a/sig/datadog/ci/contrib/cucumber/configuration/settings.rbs +++ b/sig/datadog/ci/contrib/cucumber/configuration/settings.rbs @@ -3,7 +3,7 @@ module Datadog module Contrib module Cucumber module Configuration - class Settings < Datadog::Tracing::Contrib::Configuration::Settings + class Settings < Datadog::CI::Contrib::Settings end end end diff --git a/sig/datadog/ci/contrib/cucumber/integration.rbs b/sig/datadog/ci/contrib/cucumber/integration.rbs index 1dd57b16..9aeee479 100644 --- a/sig/datadog/ci/contrib/cucumber/integration.rbs +++ b/sig/datadog/ci/contrib/cucumber/integration.rbs @@ -3,7 +3,8 @@ module Datadog module Contrib module Cucumber class Integration - extend Datadog::Tracing::Contrib::Integration + extend Datadog::CI::Contrib::Integration::ClassMethods + include Datadog::CI::Contrib::Integration::InstanceMethods MINIMUM_VERSION: Gem::Version diff --git a/sig/datadog/ci/contrib/integration.rbs b/sig/datadog/ci/contrib/integration.rbs index 835ea588..40128ecd 100644 --- a/sig/datadog/ci/contrib/integration.rbs +++ b/sig/datadog/ci/contrib/integration.rbs @@ -8,15 +8,39 @@ module Datadog self.@registry: Hash[Symbol, Struct[untyped]] def self.included: (Module base) -> void + + def self.register: (untyped integration, Symbol name) -> void + + def self.registry: () -> Hash[Symbol, Struct[untyped]] + module ClassMethods - def register_as: (Symbol name, ?::Hash[Symbol, untyped] options) -> void + def register_as: (Symbol name) -> void + + def version: () -> Gem::Version? + + def available?: () -> bool + + def loaded?: () -> bool def compatible?: () -> bool + + def patchable?: () -> bool end - def self.register: (Object integration, Symbol name, ::Hash[Symbol, untyped] options) -> void + module InstanceMethods + extend ClassMethods + @configuration: Datadog::CI::Contrib::Settings? - def self.registry: () -> Hash[Symbol, Struct[untyped]] + def configuration: () -> Datadog::CI::Contrib::Settings + + def configure: (?::Hash[Symbol, untyped] options) ?{ (Datadog::CI::Contrib::Settings) -> Datadog::CI::Contrib::Settings } -> Datadog::CI::Contrib::Settings + + def reset_configuration!: () -> void + + def patcher: () -> Datadog::Tracing::Contrib::Patcher? + + def new_configuration: () -> Datadog::CI::Contrib::Settings + end end end end diff --git a/sig/datadog/ci/contrib/minitest/configuration/settings.rbs b/sig/datadog/ci/contrib/minitest/configuration/settings.rbs index bd45cfde..1d4d86ed 100644 --- a/sig/datadog/ci/contrib/minitest/configuration/settings.rbs +++ b/sig/datadog/ci/contrib/minitest/configuration/settings.rbs @@ -3,7 +3,7 @@ module Datadog module Contrib module Minitest module Configuration - class Settings < Datadog::Tracing::Contrib::Configuration::Settings + class Settings < Datadog::CI::Contrib::Settings end end end diff --git a/sig/datadog/ci/contrib/minitest/integration.rbs b/sig/datadog/ci/contrib/minitest/integration.rbs index c4ea0821..ce86c736 100644 --- a/sig/datadog/ci/contrib/minitest/integration.rbs +++ b/sig/datadog/ci/contrib/minitest/integration.rbs @@ -3,13 +3,14 @@ module Datadog module Contrib module Minitest class Integration - extend Datadog::Tracing::Contrib::Integration + extend Datadog::CI::Contrib::Integration::ClassMethods + include Datadog::CI::Contrib::Integration::InstanceMethods - MINIMUM_VERSION: untyped + MINIMUM_VERSION: Gem::Version def self.version: () -> untyped - def self.loaded?: () -> untyped + def self.loaded?: () -> bool def compatible?: () -> bool diff --git a/sig/datadog/ci/contrib/rspec/configuration/settings.rbs b/sig/datadog/ci/contrib/rspec/configuration/settings.rbs index 055a9a86..9217aad3 100644 --- a/sig/datadog/ci/contrib/rspec/configuration/settings.rbs +++ b/sig/datadog/ci/contrib/rspec/configuration/settings.rbs @@ -3,7 +3,7 @@ module Datadog module Contrib module RSpec module Configuration - class Settings < Datadog::Tracing::Contrib::Configuration::Settings + class Settings < Datadog::CI::Contrib::Settings end end end diff --git a/sig/datadog/ci/contrib/rspec/integration.rbs b/sig/datadog/ci/contrib/rspec/integration.rbs index 22f8a820..c56a528f 100644 --- a/sig/datadog/ci/contrib/rspec/integration.rbs +++ b/sig/datadog/ci/contrib/rspec/integration.rbs @@ -3,9 +3,10 @@ module Datadog module Contrib module RSpec class Integration - extend Datadog::Tracing::Contrib::Integration + extend Datadog::CI::Contrib::Integration::ClassMethods + include Datadog::CI::Contrib::Integration::InstanceMethods - MINIMUM_VERSION: untyped + MINIMUM_VERSION: Gem::Version def self.version: () -> untyped diff --git a/sig/datadog/ci/contrib/settings.rbs b/sig/datadog/ci/contrib/settings.rbs new file mode 100644 index 00000000..e0e1c2d2 --- /dev/null +++ b/sig/datadog/ci/contrib/settings.rbs @@ -0,0 +1,19 @@ +module Datadog + module CI + module Contrib + class Settings + include Core::Configuration::Base + extend Datadog::Core::Configuration::Options::ClassMethods + include Datadog::Core::Configuration::Options::InstanceMethods + + extend Datadog::Core::Configuration::Base::ClassMethods + + def configure: (?::Hash[Symbol, untyped] options) ?{ (Datadog::CI::Contrib::Settings) -> Datadog::CI::Contrib::Settings } -> Datadog::CI::Contrib::Settings? + + def []: (Symbol name) -> Datadog::CI::Contrib::Settings + + def []=: (untyped name, untyped value) -> untyped + end + end + end +end diff --git a/spec/datadog/ci/configuration/settings_spec.rb b/spec/datadog/ci/configuration/settings_spec.rb index 69de4bbf..5b86ed8c 100644 --- a/spec/datadog/ci/configuration/settings_spec.rb +++ b/spec/datadog/ci/configuration/settings_spec.rb @@ -1,8 +1,8 @@ # Dummy Integration class FakeIntegration - def initialize(configuration) - @configuration = configuration - end + include Datadog::CI::Contrib::Integration + + register_as :fake module Patcher module_function @@ -20,6 +20,10 @@ def reset end end + def self.version + "0.1" + end + def self.loaded? true end @@ -35,13 +39,6 @@ def self.auto_instrument? def patcher Patcher end - - def default_configuration - @configuration - end - - def configure(name, options, &block) - end end RSpec.describe Datadog::CI::Configuration::Settings do @@ -100,21 +97,14 @@ def configure(name, options, &block) end describe "#instrument" do - let(:registry) { {} } let(:integration_name) { :fake } - let(:integration_config) { double(enabled: true) } - let(:integration) { FakeIntegration.new(integration_config) } + let(:integration) { FakeIntegration.new } + let(:enabled) { true } - subject(:instrument) { settings.ci.instrument(integration_name) } + subject(:instrument) { settings.ci.instrument(integration_name, enabled: enabled) } before do - registry[integration_name] = instance_double( - Datadog::CI::Contrib::Integration::RegisteredIntegration, - integration: integration - ) - - allow(Datadog::CI::Contrib::Integration).to receive(:registry).and_return(registry) settings.ci.enabled = ci_enabled end @@ -141,8 +131,28 @@ def configure(name, options, &block) end end + context "when not loaded" do + before { allow(FakeIntegration).to receive(:loaded?).and_return(false) } + + it "does not patch the integration" do + expect(FakeIntegration::Patcher).to_not receive(:patch) + + instrument + end + end + + context "when not available" do + before { allow(FakeIntegration).to receive(:available?).and_return(false) } + + it "does not patch the integration" do + expect(FakeIntegration::Patcher).to_not receive(:patch) + + instrument + end + end + context "when not compatible" do - before { expect(FakeIntegration).to receive(:compatible?).and_return(false) } + before { allow(FakeIntegration).to receive(:compatible?).and_return(false) } it "does not patch the integration" do expect(FakeIntegration::Patcher).to_not receive(:patch) @@ -152,9 +162,10 @@ def configure(name, options, &block) end context "when not enabled" do - before do - allow(integration_config).to receive(:enabled).and_return(false) - end + # before do + # allow(integration.configuration).to receive(:enabled).and_return(false) + # end + let(:enabled) { false } it "does not patch the integration" do expect(FakeIntegration::Patcher).to_not receive(:patch) @@ -165,10 +176,10 @@ def configure(name, options, &block) end context "when integration does not exist" do - let(:integration_name) { :not_exiting } + let(:integration_name) { :not_existing } it "does not patch the integration" do - expect { instrument }.to_not raise_error + expect { instrument }.to raise_error(Datadog::CI::Configuration::Settings::InvalidIntegrationError) end end diff --git a/spec/datadog/ci/contrib/cucumber/integration_spec.rb b/spec/datadog/ci/contrib/cucumber/integration_spec.rb index 03d30e61..268247eb 100644 --- a/spec/datadog/ci/contrib/cucumber/integration_spec.rb +++ b/spec/datadog/ci/contrib/cucumber/integration_spec.rb @@ -62,8 +62,8 @@ it { is_expected.to be(false) } end - describe "#default_configuration" do - subject(:default_configuration) { integration.default_configuration } + describe "#configuration" do + subject(:configuration) { integration.configuration } it { is_expected.to be_a_kind_of(Datadog::CI::Contrib::Cucumber::Configuration::Settings) } end diff --git a/spec/datadog/ci/contrib/minitest/integration_spec.rb b/spec/datadog/ci/contrib/minitest/integration_spec.rb index 2bd1f4f5..2438a245 100644 --- a/spec/datadog/ci/contrib/minitest/integration_spec.rb +++ b/spec/datadog/ci/contrib/minitest/integration_spec.rb @@ -54,8 +54,8 @@ it { is_expected.to be(false) } end - describe "#default_configuration" do - subject(:default_configuration) { integration.default_configuration } + describe "#configuration" do + subject(:configuration) { integration.configuration } it { is_expected.to be_a_kind_of(Datadog::CI::Contrib::Minitest::Configuration::Settings) } end diff --git a/spec/datadog/ci/contrib/rspec/integration_spec.rb b/spec/datadog/ci/contrib/rspec/integration_spec.rb index 40c6be49..f4becaff 100644 --- a/spec/datadog/ci/contrib/rspec/integration_spec.rb +++ b/spec/datadog/ci/contrib/rspec/integration_spec.rb @@ -54,8 +54,8 @@ it { is_expected.to be(false) } end - describe "#default_configuration" do - subject(:default_configuration) { integration.default_configuration } + describe "#configuration" do + subject(:configuration) { integration.configuration } it { is_expected.to be_a_kind_of(Datadog::CI::Contrib::RSpec::Configuration::Settings) } end diff --git a/vendor/rbs/ddtrace/0/datadog/tracing/contrib/configuration/settings.rbs b/vendor/rbs/ddtrace/0/datadog/tracing/contrib/configuration/settings.rbs deleted file mode 100644 index acd3fd9a..00000000 --- a/vendor/rbs/ddtrace/0/datadog/tracing/contrib/configuration/settings.rbs +++ /dev/null @@ -1,18 +0,0 @@ -module Datadog - module Tracing - module Contrib - module Configuration - class Settings - include Core::Configuration::Base - extend Core::Configuration::Options::ClassMethods - - def configure: (?::Hash[untyped, untyped] options) { (untyped) -> untyped } -> untyped - - def []: (untyped name) -> untyped - - def []=: (untyped name, untyped value) -> untyped - end - end - end - end -end diff --git a/vendor/rbs/ddtrace/0/datadog/tracing/contrib/integration.rbs b/vendor/rbs/ddtrace/0/datadog/tracing/contrib/integration.rbs deleted file mode 100644 index 5d148b75..00000000 --- a/vendor/rbs/ddtrace/0/datadog/tracing/contrib/integration.rbs +++ /dev/null @@ -1,13 +0,0 @@ -module Datadog - module Tracing - module Contrib - module Integration - def self.included: (untyped base) -> untyped - - def compatible?: () -> bool - - def register_as: (Symbol key, Hash[Symbol, untyped] options) -> untyped - end - end - end -end From 54de03b094abfc69b2a4a523f026996535db7d5f Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 18 Sep 2023 14:33:13 +0200 Subject: [PATCH 04/45] fix law of demeter violation between settings and integration --- lib/datadog/ci/configuration/settings.rb | 2 +- lib/datadog/ci/contrib/integration.rb | 4 ++++ sig/datadog/ci/contrib/settings.rbs | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/datadog/ci/configuration/settings.rb b/lib/datadog/ci/configuration/settings.rb index 8a6e856f..6525f510 100644 --- a/lib/datadog/ci/configuration/settings.rb +++ b/lib/datadog/ci/configuration/settings.rb @@ -29,7 +29,7 @@ def self.add_settings!(base) integration = fetch_integration(integration_name) integration.configure(options, &block) - return unless integration.configuration.enabled + return unless integration.enabled patch_results = integration.patch next if patch_results == true diff --git a/lib/datadog/ci/contrib/integration.rb b/lib/datadog/ci/contrib/integration.rb index e3cd4d31..cba4e329 100644 --- a/lib/datadog/ci/contrib/integration.rb +++ b/lib/datadog/ci/contrib/integration.rb @@ -95,6 +95,10 @@ def reset_configuration! @configuration = nil end + def enabled + configuration.enabled + end + # The patcher module to inject instrumented objects into the instrumentation target. # # {Contrib::Patcher} includes the basic functionality of a patcher. `include`ing diff --git a/sig/datadog/ci/contrib/settings.rbs b/sig/datadog/ci/contrib/settings.rbs index e0e1c2d2..ca38c544 100644 --- a/sig/datadog/ci/contrib/settings.rbs +++ b/sig/datadog/ci/contrib/settings.rbs @@ -13,6 +13,12 @@ module Datadog def []: (Symbol name) -> Datadog::CI::Contrib::Settings def []=: (untyped name, untyped value) -> untyped + + # default configuration options + # + def enabled: () -> bool + def service_name: () -> String + def operation_name: () -> String end end end From ebfad998dd4f30c9c390bd5e41b6f29240113520 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 18 Sep 2023 14:42:09 +0200 Subject: [PATCH 05/45] simplify registry and remove RegisteredIntegration struct --- lib/datadog/ci/configuration/settings.rb | 10 +++------- lib/datadog/ci/contrib/integration.rb | 4 +--- sig/datadog/ci/contrib/integration.rbs | 5 +---- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/lib/datadog/ci/configuration/settings.rb b/lib/datadog/ci/configuration/settings.rb index 6525f510..85500ce7 100644 --- a/lib/datadog/ci/configuration/settings.rb +++ b/lib/datadog/ci/configuration/settings.rb @@ -42,9 +42,7 @@ def self.add_settings!(base) end define_method(:[]) do |integration_name| - integration = fetch_integration(integration_name) - - integration.configuration unless integration.nil? + fetch_integration(integration_name).configuration end # TODO: Deprecate in the next major version, as `instrument` better describes this method's purpose @@ -58,10 +56,8 @@ def self.add_settings!(base) end define_method(:fetch_integration) do |name| - registered = Datadog::CI::Contrib::Integration.registry[name] - raise(InvalidIntegrationError, "'#{name}' is not a valid integration.") if registered.nil? - - registered.integration + Datadog::CI::Contrib::Integration.registry[name] || + raise(InvalidIntegrationError, "'#{name}' is not a valid integration.") end end end diff --git a/lib/datadog/ci/contrib/integration.rb b/lib/datadog/ci/contrib/integration.rb index cba4e329..24021ae9 100644 --- a/lib/datadog/ci/contrib/integration.rb +++ b/lib/datadog/ci/contrib/integration.rb @@ -8,15 +8,13 @@ module Contrib module Integration @registry = {} - RegisteredIntegration = Struct.new(:name, :integration) - def self.included(base) base.extend(ClassMethods) base.include(InstanceMethods) end def self.register(klass, name) - registry[name] = RegisteredIntegration.new(name, klass.new) + registry[name] = klass.new end def self.registry diff --git a/sig/datadog/ci/contrib/integration.rbs b/sig/datadog/ci/contrib/integration.rbs index 40128ecd..09908ce6 100644 --- a/sig/datadog/ci/contrib/integration.rbs +++ b/sig/datadog/ci/contrib/integration.rbs @@ -2,10 +2,7 @@ module Datadog module CI module Contrib module Integration - # TODO: replace with Struct[] when https://github.com/ruby/rbs/pull/1456 is merged - RegisteredIntegration: untyped - - self.@registry: Hash[Symbol, Struct[untyped]] + self.@registry: Hash[Symbol, untyped] def self.included: (Module base) -> void From 003eb823fd5e38d1181658c702aea945d68edf0f Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 18 Sep 2023 15:24:49 +0200 Subject: [PATCH 06/45] clean up "CI mode activated" shared context and remove hacky access to Core internals --- ...matter_spec.rb => instrumentation_spec.rb} | 11 ++++----- .../contrib/minitest/instrumentation_spec.rb | 9 ++++---- .../ci/contrib/rspec/instrumentation_spec.rb | 9 +++----- .../ci/contrib/support/mode_helpers.rb | 23 ++++++++----------- 4 files changed, 20 insertions(+), 32 deletions(-) rename spec/datadog/ci/contrib/cucumber/{formatter_spec.rb => instrumentation_spec.rb} (92%) diff --git a/spec/datadog/ci/contrib/cucumber/formatter_spec.rb b/spec/datadog/ci/contrib/cucumber/instrumentation_spec.rb similarity index 92% rename from spec/datadog/ci/contrib/cucumber/formatter_spec.rb rename to spec/datadog/ci/contrib/cucumber/instrumentation_spec.rb index 2bdebfb2..6389e985 100644 --- a/spec/datadog/ci/contrib/cucumber/formatter_spec.rb +++ b/spec/datadog/ci/contrib/cucumber/instrumentation_spec.rb @@ -6,7 +6,10 @@ RSpec.describe "Cucumber formatter" do extend ConfigurationHelpers - include_context "CI mode activated" + include_context "CI mode activated" do + let(:integration_name) { :cucumber } + let(:integration_options) { {service_name: "jalapenos"} } + end # Cucumber runtime setup let(:existing_runtime) { Cucumber::Runtime.new(runtime_options) } @@ -27,12 +30,6 @@ end end - before do - Datadog.configure do |c| - c.ci.instrument :cucumber, service_name: "jalapenos" - end - end - context "executing a test suite" do let(:args) { ["spec/datadog/ci/contrib/cucumber/cucumber.features"] } diff --git a/spec/datadog/ci/contrib/minitest/instrumentation_spec.rb b/spec/datadog/ci/contrib/minitest/instrumentation_spec.rb index fd93f86f..282a65b7 100644 --- a/spec/datadog/ci/contrib/minitest/instrumentation_spec.rb +++ b/spec/datadog/ci/contrib/minitest/instrumentation_spec.rb @@ -5,15 +5,14 @@ require_relative "../support/spec_helper" RSpec.describe "Minitest hooks" do - include_context "CI mode activated" + include_context "CI mode activated" do + let(:integration_name) { :minitest } + let(:integration_options) { {service_name: "ltest"} } + end before do # required to call .runnable_methods Minitest.seed = 1 - - Datadog.configure do |c| - c.ci.instrument :minitest, service_name: "ltest" - end end it "creates span for test" do diff --git a/spec/datadog/ci/contrib/rspec/instrumentation_spec.rb b/spec/datadog/ci/contrib/rspec/instrumentation_spec.rb index 715ca2f4..00011631 100644 --- a/spec/datadog/ci/contrib/rspec/instrumentation_spec.rb +++ b/spec/datadog/ci/contrib/rspec/instrumentation_spec.rb @@ -3,12 +3,9 @@ require_relative "../support/spec_helper" RSpec.describe "RSpec hooks" do - include_context "CI mode activated" - - before do - Datadog.configure do |c| - c.ci.instrument :rspec, service_name: "lspec" - end + include_context "CI mode activated" do + let(:integration_name) { :rspec } + let(:integration_options) { {service_name: "lspec"} } end # Yields to a block in a new RSpec global context. All RSpec diff --git a/spec/datadog/ci/contrib/support/mode_helpers.rb b/spec/datadog/ci/contrib/support/mode_helpers.rb index 9a9f84f1..9cdf6730 100644 --- a/spec/datadog/ci/contrib/support/mode_helpers.rb +++ b/spec/datadog/ci/contrib/support/mode_helpers.rb @@ -1,20 +1,15 @@ RSpec.shared_context "CI mode activated" do - let(:settings) do - Datadog::Core::Configuration::Settings.new.tap do |settings| - settings.ci.enabled = true - end - end - - let(:components) { Datadog::Core::Configuration::Components.new(settings) } + let(:integration_name) { :override_me } + let(:integration_options) { {} } before do - # TODO: this is a very hacky way that messes with Core's internals - allow_any_instance_of(Datadog::Core::Configuration).to receive(:configuration).and_return(settings) - - allow(Datadog::Tracing) - .to receive(:tracer) - .and_return(components.tracer) + Datadog.configure do |c| + c.ci.enabled = true + c.ci.instrument integration_name, integration_options + end end - after { components.shutdown! } + after do + ::Datadog::Tracing.shutdown! + end end From f741f3a97e49b04c5e7fe9677b5c49acee6d9b8d Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 18 Sep 2023 16:24:51 +0200 Subject: [PATCH 07/45] agentless_mode_enabled and api_key settings --- lib/datadog/ci/configuration/settings.rb | 11 +++ lib/datadog/ci/ext/settings.rb | 2 + sig/datadog/ci/ext/settings.rbs | 2 + .../datadog/ci/configuration/settings_spec.rb | 80 +++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/lib/datadog/ci/configuration/settings.rb b/lib/datadog/ci/configuration/settings.rb index 85500ce7..11f01c04 100644 --- a/lib/datadog/ci/configuration/settings.rb +++ b/lib/datadog/ci/configuration/settings.rb @@ -23,6 +23,17 @@ def self.add_settings!(base) o.default false end + option :agentless_mode_enabled do |o| + o.type :bool + o.env CI::Ext::Settings::ENV_AGENTLESS_MODE_ENABLED + o.default false + end + + option :api_key do |o| + o.type :string, nilable: true + o.env CI::Ext::Settings::ENV_API_KEY + end + define_method(:instrument) do |integration_name, options = {}, &block| return unless enabled diff --git a/lib/datadog/ci/ext/settings.rb b/lib/datadog/ci/ext/settings.rb index d74dbbd6..8907b6b6 100644 --- a/lib/datadog/ci/ext/settings.rb +++ b/lib/datadog/ci/ext/settings.rb @@ -6,6 +6,8 @@ module Ext # Defines constants for test tags module Settings ENV_MODE_ENABLED = "DD_TRACE_CI_ENABLED" + ENV_AGENTLESS_MODE_ENABLED = "DD_CIVISIBILITY_AGENTLESS_ENABLED" + ENV_API_KEY = "DD_API_KEY" end end end diff --git a/sig/datadog/ci/ext/settings.rbs b/sig/datadog/ci/ext/settings.rbs index b133c8d0..afa71d4c 100644 --- a/sig/datadog/ci/ext/settings.rbs +++ b/sig/datadog/ci/ext/settings.rbs @@ -3,6 +3,8 @@ module Datadog module Ext module Settings ENV_MODE_ENABLED: String + ENV_AGENTLESS_MODE_ENABLED: String + ENV_API_KEY: String end end end diff --git a/spec/datadog/ci/configuration/settings_spec.rb b/spec/datadog/ci/configuration/settings_spec.rb index 5b86ed8c..3b50516a 100644 --- a/spec/datadog/ci/configuration/settings_spec.rb +++ b/spec/datadog/ci/configuration/settings_spec.rb @@ -96,6 +96,86 @@ def patcher end end + describe "#agentless_mode_enabled" do + subject(:agentless_mode_enabled) { settings.ci.agentless_mode_enabled } + + it { is_expected.to be false } + + context "when #{Datadog::CI::Ext::Settings::ENV_AGENTLESS_MODE_ENABLED}" do + around do |example| + ClimateControl.modify(Datadog::CI::Ext::Settings::ENV_AGENTLESS_MODE_ENABLED => enable) do + example.run + end + end + + context "is not defined" do + let(:enable) { nil } + + it { is_expected.to be false } + end + + context "is set to true" do + let(:enable) { "true" } + + it { is_expected.to be true } + end + + context "is set to false" do + let(:enable) { "false" } + + it { is_expected.to be false } + end + end + end + + describe "#agentless_mode_enabled=" do + it "updates the #enabled setting" do + expect { settings.ci.agentless_mode_enabled = true } + .to change { settings.ci.agentless_mode_enabled } + .from(false) + .to(true) + end + end + + describe "#api_key" do + subject(:api_key) { settings.ci.api_key } + + context "when #{Datadog::CI::Ext::Settings::ENV_API_KEY}" do + around do |example| + ClimateControl.modify(Datadog::CI::Ext::Settings::ENV_API_KEY => api_key) do + example.run + end + end + + context "is not defined" do + let(:api_key) { nil } + + it { is_expected.to be nil } + end + + context "is set to dd_api_key" do + let(:api_key) { "dd_api_key" } + + it { is_expected.to eq("dd_api_key") } + end + end + end + + describe "#api_key=" do + around do |example| + ClimateControl.modify(Datadog::CI::Ext::Settings::ENV_API_KEY => nil) do + example.run + end + end + + it "updates the #api_key setting" do + expect { settings.ci.api_key = "dd_api_key" } + .to change { settings.ci.api_key } + .from(nil) + .to("dd_api_key") + end + end + describe "#instrument" do let(:integration_name) { :fake } From 48f786b90eb5754eacb7976dcb4a979455ca7967 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 18 Sep 2023 16:46:19 +0200 Subject: [PATCH 08/45] rename Ci::Test to CI::Recorder --- lib/datadog/ci/contrib/cucumber/formatter.rb | 19 +++++++++---------- lib/datadog/ci/contrib/minitest/hooks.rb | 13 +++++++++---- lib/datadog/ci/contrib/rspec/example.rb | 14 ++++++-------- lib/datadog/ci/{test.rb => recorder.rb} | 2 +- sig/datadog/ci/{test.rbs => recorder.rbs} | 2 +- .../ci/{test_spec.rb => recorder_spec.rb} | 2 +- 6 files changed, 27 insertions(+), 25 deletions(-) rename lib/datadog/ci/{test.rb => recorder.rb} (99%) rename sig/datadog/ci/{test.rbs => recorder.rbs} (96%) rename spec/datadog/ci/{test_spec.rb => recorder_spec.rb} (99%) diff --git a/lib/datadog/ci/contrib/cucumber/formatter.rb b/lib/datadog/ci/contrib/cucumber/formatter.rb index 150218ac..60238066 100644 --- a/lib/datadog/ci/contrib/cucumber/formatter.rb +++ b/lib/datadog/ci/contrib/cucumber/formatter.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require_relative "../../test" -require_relative "../../ext/app_types" -require_relative "../../ext/environment" +require_relative "../../recorder" require_relative "../../ext/test" require_relative "ext" +require_relative "integration" module Datadog module CI @@ -30,7 +29,7 @@ def bind_events(config) end def on_test_case_started(event) - @current_feature_span = CI::Test.trace( + @current_feature_span = CI::Recorder.trace( configuration[:operation_name], { span_options: { @@ -50,11 +49,11 @@ def on_test_case_finished(event) return if @current_feature_span.nil? if event.result.skipped? - CI::Test.skipped!(@current_feature_span) + CI::Recorder.skipped!(@current_feature_span) elsif event.result.ok? - CI::Test.passed!(@current_feature_span) + CI::Recorder.passed!(@current_feature_span) elsif event.result.failed? - CI::Test.failed!(@current_feature_span) + CI::Recorder.failed!(@current_feature_span) end @current_feature_span.finish @@ -72,11 +71,11 @@ def on_test_step_finished(event) return if @current_step_span.nil? if event.result.skipped? - CI::Test.skipped!(@current_step_span, event.result.exception) + CI::Recorder.skipped!(@current_step_span, event.result.exception) elsif event.result.ok? - CI::Test.passed!(@current_step_span) + CI::Recorder.passed!(@current_step_span) elsif event.result.failed? - CI::Test.failed!(@current_step_span, event.result.exception) + CI::Recorder.failed!(@current_step_span, event.result.exception) end @current_step_span.finish diff --git a/lib/datadog/ci/contrib/minitest/hooks.rb b/lib/datadog/ci/contrib/minitest/hooks.rb index 0bb61195..3e72c156 100644 --- a/lib/datadog/ci/contrib/minitest/hooks.rb +++ b/lib/datadog/ci/contrib/minitest/hooks.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true +require_relative "../../recorder" +require_relative "../../ext/test" +require_relative "ext" +require_relative "integration" + module Datadog module CI module Contrib @@ -15,7 +20,7 @@ def before_setup path, = method(name).source_location test_suite = Pathname.new(path.to_s).relative_path_from(Pathname.pwd).to_s - span = CI::Test.trace( + span = CI::Recorder.trace( configuration[:operation_name], { span_options: { @@ -41,11 +46,11 @@ def after_teardown case result_code when "." - CI::Test.passed!(span) + CI::Recorder.passed!(span) when "E", "F" - CI::Test.failed!(span, failure) + CI::Recorder.failed!(span, failure) when "S" - CI::Test.skipped!(span) + CI::Recorder.skipped!(span) span.set_tag(CI::Ext::Test::TAG_SKIP_REASON, failure.message) end diff --git a/lib/datadog/ci/contrib/rspec/example.rb b/lib/datadog/ci/contrib/rspec/example.rb index e45e26c6..85316b53 100644 --- a/lib/datadog/ci/contrib/rspec/example.rb +++ b/lib/datadog/ci/contrib/rspec/example.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_relative "../../test" - -require_relative "../../ext/app_types" -require_relative "../../ext/environment" +require_relative "../../recorder" require_relative "../../ext/test" require_relative "ext" +require_relative "integration" module Datadog module CI @@ -28,7 +26,7 @@ def run(example_group_instance, reporter) test_name += " #{description}" end - CI::Test.trace( + CI::Recorder.trace( configuration[:operation_name], { span_options: { @@ -46,11 +44,11 @@ def run(example_group_instance, reporter) case execution_result.status when :passed - CI::Test.passed!(span) + CI::Recorder.passed!(span) when :failed - CI::Test.failed!(span, execution_result.exception) + CI::Recorder.failed!(span, execution_result.exception) else - CI::Test.skipped!(span, execution_result.exception) if execution_result.example_skipped? + CI::Recorder.skipped!(span, execution_result.exception) if execution_result.example_skipped? end result diff --git a/lib/datadog/ci/test.rb b/lib/datadog/ci/recorder.rb similarity index 99% rename from lib/datadog/ci/test.rb rename to lib/datadog/ci/recorder.rb index 04e5c132..a298f2ad 100644 --- a/lib/datadog/ci/test.rb +++ b/lib/datadog/ci/recorder.rb @@ -12,7 +12,7 @@ module Datadog module CI # Common behavior for CI tests - module Test + module Recorder # Creates a new span for a CI test def self.trace(span_name, options = {}) span_options = { diff --git a/sig/datadog/ci/test.rbs b/sig/datadog/ci/recorder.rbs similarity index 96% rename from sig/datadog/ci/test.rbs rename to sig/datadog/ci/recorder.rbs index 7943c29a..248cfe9b 100644 --- a/sig/datadog/ci/test.rbs +++ b/sig/datadog/ci/recorder.rbs @@ -1,6 +1,6 @@ module Datadog module CI - module Test + module Recorder self.@environment_tags: Hash[String, String] def self.trace: (untyped span_name, ?::Hash[untyped, untyped] options) ?{ (untyped, untyped) -> untyped } -> untyped diff --git a/spec/datadog/ci/test_spec.rb b/spec/datadog/ci/recorder_spec.rb similarity index 99% rename from spec/datadog/ci/test_spec.rb rename to spec/datadog/ci/recorder_spec.rb index fefcbcae..cee9efac 100644 --- a/spec/datadog/ci/test_spec.rb +++ b/spec/datadog/ci/recorder_spec.rb @@ -1,4 +1,4 @@ -RSpec.describe Datadog::CI::Test do +RSpec.describe Datadog::CI::Recorder do let(:trace_op) { instance_double(Datadog::Tracing::TraceOperation) } let(:span_name) { "span name" } From 0eede7c2c4c512e356a576584be2758b53dcc92e Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 18 Sep 2023 17:00:19 +0200 Subject: [PATCH 09/45] trace serializers structure --- lib/datadog/ci/serializers/base.rb | 21 +++++ .../ci/serializers/v1/test_serializer.rb | 14 ++++ sig/datadog/ci/flush.rbs | 2 +- sig/datadog/ci/serializers/base.rbs | 15 ++++ .../ci/serializers/v1/test_serializer.rbs | 10 +++ .../0/datadog/tracing/trace_operation.rbs | 84 +++++++++++++++++++ 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 lib/datadog/ci/serializers/base.rb create mode 100644 lib/datadog/ci/serializers/v1/test_serializer.rb create mode 100644 sig/datadog/ci/serializers/base.rbs create mode 100644 sig/datadog/ci/serializers/v1/test_serializer.rbs create mode 100644 vendor/rbs/ddtrace/0/datadog/tracing/trace_operation.rbs diff --git a/lib/datadog/ci/serializers/base.rb b/lib/datadog/ci/serializers/base.rb new file mode 100644 index 00000000..f27b9620 --- /dev/null +++ b/lib/datadog/ci/serializers/base.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Datadog + module CI + module Serializers + class Base + attr_reader :trace + + def initialize(trace) + @trace = trace + end + + def to_json + end + + def to_msgpack + end + end + end + end +end diff --git a/lib/datadog/ci/serializers/v1/test_serializer.rb b/lib/datadog/ci/serializers/v1/test_serializer.rb new file mode 100644 index 00000000..6d71c685 --- /dev/null +++ b/lib/datadog/ci/serializers/v1/test_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "../base" + +module Datadog + module CI + module Serializers + module V1 + class TestSerializer < Base + end + end + end + end +end diff --git a/sig/datadog/ci/flush.rbs b/sig/datadog/ci/flush.rbs index a9c80605..cc955d6c 100644 --- a/sig/datadog/ci/flush.rbs +++ b/sig/datadog/ci/flush.rbs @@ -2,7 +2,7 @@ module Datadog module CI module Flush module Tagging - def get_trace: (untyped trace_op) -> untyped + def get_trace: (Datadog::Tracing::TraceOperation trace_op) -> untyped end class Finished < Tracing::Flush::Finished prepend Tagging diff --git a/sig/datadog/ci/serializers/base.rbs b/sig/datadog/ci/serializers/base.rbs new file mode 100644 index 00000000..d4e3dead --- /dev/null +++ b/sig/datadog/ci/serializers/base.rbs @@ -0,0 +1,15 @@ +module Datadog + module CI + module Serializers + class Base + attr_reader trace: Datadog::Tracing::TraceOperation + + def initialize: (Datadog::Tracing::TraceOperation trace) -> void + + def to_json: () -> nil + + def to_msgpack: () -> nil + end + end + end +end diff --git a/sig/datadog/ci/serializers/v1/test_serializer.rbs b/sig/datadog/ci/serializers/v1/test_serializer.rbs new file mode 100644 index 00000000..6134ae94 --- /dev/null +++ b/sig/datadog/ci/serializers/v1/test_serializer.rbs @@ -0,0 +1,10 @@ +module Datadog + module CI + module Serializers + module V1 + class TestSerializer + end + end + end + end +end diff --git a/vendor/rbs/ddtrace/0/datadog/tracing/trace_operation.rbs b/vendor/rbs/ddtrace/0/datadog/tracing/trace_operation.rbs new file mode 100644 index 00000000..06cdf5e8 --- /dev/null +++ b/vendor/rbs/ddtrace/0/datadog/tracing/trace_operation.rbs @@ -0,0 +1,84 @@ +module Datadog + module Tracing + class TraceOperation + include Metadata::Tagging + + DEFAULT_MAX_LENGTH: ::Integer + + attr_accessor agent_sample_rate: untyped + attr_accessor hostname: untyped + attr_accessor origin: untyped + attr_accessor rate_limiter_rate: untyped + attr_accessor rule_sample_rate: untyped + attr_accessor sample_rate: untyped + attr_accessor sampling_priority: untyped + attr_reader active_span_count: untyped + attr_reader active_span: untyped + attr_reader id: untyped + attr_reader max_length: untyped + attr_reader parent_span_id: untyped + attr_writer name: untyped + attr_writer resource: untyped + attr_writer sampled: untyped + attr_writer service: untyped + + def initialize: (?agent_sample_rate: untyped?, ?events: untyped?, ?hostname: untyped?, ?id: untyped?, ?max_length: untyped, ?name: untyped?, ?origin: untyped?, ?parent_span_id: untyped?, ?rate_limiter_rate: untyped?, ?resource: untyped?, ?rule_sample_rate: untyped?, ?sample_rate: untyped?, ?sampled: untyped?, ?sampling_priority: untyped?, ?service: untyped?, ?tags: untyped?, ?metrics: untyped?) -> void + def full?: () -> untyped + def finished_span_count: () -> untyped + def finished?: () -> untyped + def sampled?: () -> bool + def priority_sampled?: () -> bool + def keep!: () -> untyped + def reject!: () -> untyped + def name: () -> untyped + def resource: () -> untyped + def resource_override?: () -> bool + def service: () -> untyped + def measure: (untyped op_name, ?events: untyped?, ?on_error: untyped?, ?resource: untyped?, ?service: untyped?, ?start_time: untyped?, ?tags: untyped?, ?type: untyped?) { (untyped, untyped) -> untyped } -> untyped + def build_span: (untyped op_name, ?events: untyped?, ?on_error: untyped?, ?resource: untyped?, ?service: untyped?, ?start_time: untyped?, ?tags: untyped?, ?type: untyped?) -> untyped + def flush!: () ?{ (untyped) -> untyped } -> untyped + def to_digest: () -> untyped + def fork_clone: () -> untyped + + class Events + include Tracing::Events + + attr_reader span_before_start: untyped + attr_reader span_finished: untyped + attr_reader trace_finished: untyped + + def initialize: () -> void + + class SpanBeforeStart < Tracing::Event + def initialize: () -> void + end + + class SpanFinished < Tracing::Event + def initialize: () -> void + end + + class TraceFinished < Tracing::Event + def initialize: () -> void + + def deactivate_trace_subscribed?: () -> untyped + def subscribe_deactivate_trace: () ?{ () -> untyped } -> untyped + end + end + + private + + attr_reader events: untyped + attr_reader root_span: untyped + + def activate_span!: (untyped span_op) -> untyped + def deactivate_span!: (untyped span_op) -> untyped + def start_span: (untyped span_op) -> untyped + def finish_span: (untyped span, untyped span_op, untyped parent) -> untyped + + def set_root_span!: (untyped span) -> (nil | untyped) + + def build_trace: (untyped spans, ?bool partial) -> untyped + def distributed_tags: () -> untyped + end + end +end From a4be72443308fc5bd26c9f8e4e4ffd2af3857caf Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 18 Sep 2023 17:30:44 +0200 Subject: [PATCH 10/45] serializer must handle TraceSegment --- sig/datadog/ci/serializers/base.rbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sig/datadog/ci/serializers/base.rbs b/sig/datadog/ci/serializers/base.rbs index d4e3dead..d8946660 100644 --- a/sig/datadog/ci/serializers/base.rbs +++ b/sig/datadog/ci/serializers/base.rbs @@ -2,9 +2,9 @@ module Datadog module CI module Serializers class Base - attr_reader trace: Datadog::Tracing::TraceOperation + attr_reader trace: Datadog::Tracing::TraceSegment - def initialize: (Datadog::Tracing::TraceOperation trace) -> void + def initialize: (Datadog::Tracing::TraceSegment trace) -> void def to_json: () -> nil From 82534bcd1243122836405f06d35cfb414e6d9eee Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Tue, 19 Sep 2023 16:07:53 +0200 Subject: [PATCH 11/45] add msgpack to dependencies --- LICENSE-3rdparty.csv | 1 + datadog-ci.gemspec | 2 ++ gemfiles/jruby_9.4.0.0_cucumber_3.gemfile.lock | 1 + gemfiles/jruby_9.4.0.0_cucumber_4.gemfile.lock | 1 + gemfiles/jruby_9.4.0.0_cucumber_5.gemfile.lock | 1 + gemfiles/jruby_9.4.0.0_cucumber_6.gemfile.lock | 1 + gemfiles/jruby_9.4.0.0_cucumber_7.gemfile.lock | 1 + gemfiles/jruby_9.4.0.0_cucumber_8.gemfile.lock | 1 + gemfiles/jruby_9.4.0.0_minitest_5.gemfile.lock | 1 + gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock | 1 + gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock | 1 + gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock | 1 + gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock | 1 + gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock | 1 + gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock | 1 + gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock | 1 + gemfiles/ruby_2.7.6_minitest_5.gemfile.lock | 1 + gemfiles/ruby_2.7.6_rspec_3.gemfile.lock | 1 + gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock | 1 + gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock | 1 + gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock | 1 + gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock | 1 + gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock | 1 + gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock | 1 + gemfiles/ruby_3.0.4_minitest_5.gemfile.lock | 1 + gemfiles/ruby_3.0.4_rspec_3.gemfile.lock | 1 + gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock | 1 + gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock | 1 + gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock | 1 + gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock | 1 + gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock | 1 + gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock | 1 + gemfiles/ruby_3.1.2_minitest_5.gemfile.lock | 1 + gemfiles/ruby_3.1.2_rspec_3.gemfile.lock | 1 + gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock | 1 + gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock | 1 + gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock | 1 + gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock | 1 + gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock | 1 + gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock | 1 + gemfiles/ruby_3.2.0_minitest_5.gemfile.lock | 1 + gemfiles/ruby_3.2.0_rspec_3.gemfile.lock | 1 + gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock | 1 + gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock | 1 + gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock | 1 + gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock | 1 + gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock | 1 + gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock | 1 + gemfiles/ruby_3.3.0_minitest_5.gemfile.lock | 1 + gemfiles/ruby_3.3.0_rspec_3.gemfile.lock | 1 + 50 files changed, 51 insertions(+) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 049eb1b2..0abe6b1b 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -1,2 +1,3 @@ Component,Origin,License,Copyright dd-trace-rb,https://github.com/DataDog/dd-trace-rb,Apache 2.0,"Copyright 2016-Present Datadog, Inc." +msgpack,https://rubygems.org/gems/msgpack,Apache-2.0,"Copyright (c) 2008-2015 Sadayuki Furuhashi" diff --git a/datadog-ci.gemspec b/datadog-ci.gemspec index c7432083..33bbcafe 100644 --- a/datadog-ci.gemspec +++ b/datadog-ci.gemspec @@ -39,4 +39,6 @@ Gem::Specification.new do |spec| ]].select { |fn| File.file?(fn) } # We don't want directories, only files spec.require_paths = ["lib"] + + spec.add_dependency "msgpack" end diff --git a/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile.lock index e081c6b9..29c24a2d 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile.lock index 237113d3..0e128613 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile.lock index 12bc41d9..2977b088 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile.lock index 3bef169e..7300c715 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile.lock index 6da1c070..fd908e56 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile.lock index ebf38b35..ef36178d 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/jruby_9.4.0.0_minitest_5.gemfile.lock b/gemfiles/jruby_9.4.0.0_minitest_5.gemfile.lock index d8d27c91..4ebd4b2a 100644 --- a/gemfiles/jruby_9.4.0.0_minitest_5.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_minitest_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock b/gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock index d320e99b..822e2834 100644 --- a/gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock index f8d057d9..2b2bd47e 100644 --- a/gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock index 3fc6c2f4..94af4aab 100644 --- a/gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock index 26a3fdf1..410cf587 100644 --- a/gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock index 2791348a..c3913afe 100644 --- a/gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock index dfd43dda..024cf47b 100644 --- a/gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock index b057632e..4e62b81b 100644 --- a/gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_2.7.6_minitest_5.gemfile.lock b/gemfiles/ruby_2.7.6_minitest_5.gemfile.lock index 56c9d29a..b0289ef7 100644 --- a/gemfiles/ruby_2.7.6_minitest_5.gemfile.lock +++ b/gemfiles/ruby_2.7.6_minitest_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_2.7.6_rspec_3.gemfile.lock b/gemfiles/ruby_2.7.6_rspec_3.gemfile.lock index d57b57aa..9c199be2 100644 --- a/gemfiles/ruby_2.7.6_rspec_3.gemfile.lock +++ b/gemfiles/ruby_2.7.6_rspec_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock index f8d057d9..2b2bd47e 100644 --- a/gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock index 3fc6c2f4..94af4aab 100644 --- a/gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock index 26a3fdf1..410cf587 100644 --- a/gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock index 2791348a..c3913afe 100644 --- a/gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock index dfd43dda..024cf47b 100644 --- a/gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock index b057632e..4e62b81b 100644 --- a/gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.0.4_minitest_5.gemfile.lock b/gemfiles/ruby_3.0.4_minitest_5.gemfile.lock index 56c9d29a..b0289ef7 100644 --- a/gemfiles/ruby_3.0.4_minitest_5.gemfile.lock +++ b/gemfiles/ruby_3.0.4_minitest_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.0.4_rspec_3.gemfile.lock b/gemfiles/ruby_3.0.4_rspec_3.gemfile.lock index d57b57aa..9c199be2 100644 --- a/gemfiles/ruby_3.0.4_rspec_3.gemfile.lock +++ b/gemfiles/ruby_3.0.4_rspec_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock index f8d057d9..2b2bd47e 100644 --- a/gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock index 3fc6c2f4..94af4aab 100644 --- a/gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock index 26a3fdf1..410cf587 100644 --- a/gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock index 2791348a..c3913afe 100644 --- a/gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock index dfd43dda..024cf47b 100644 --- a/gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock index b057632e..4e62b81b 100644 --- a/gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.1.2_minitest_5.gemfile.lock b/gemfiles/ruby_3.1.2_minitest_5.gemfile.lock index 56c9d29a..b0289ef7 100644 --- a/gemfiles/ruby_3.1.2_minitest_5.gemfile.lock +++ b/gemfiles/ruby_3.1.2_minitest_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.1.2_rspec_3.gemfile.lock b/gemfiles/ruby_3.1.2_rspec_3.gemfile.lock index d57b57aa..9c199be2 100644 --- a/gemfiles/ruby_3.1.2_rspec_3.gemfile.lock +++ b/gemfiles/ruby_3.1.2_rspec_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock index f8d057d9..2b2bd47e 100644 --- a/gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock index 3fc6c2f4..94af4aab 100644 --- a/gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock index 26a3fdf1..410cf587 100644 --- a/gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock index 2791348a..c3913afe 100644 --- a/gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock index dfd43dda..024cf47b 100644 --- a/gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock index b057632e..4e62b81b 100644 --- a/gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.2.0_minitest_5.gemfile.lock b/gemfiles/ruby_3.2.0_minitest_5.gemfile.lock index 56c9d29a..b0289ef7 100644 --- a/gemfiles/ruby_3.2.0_minitest_5.gemfile.lock +++ b/gemfiles/ruby_3.2.0_minitest_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.2.0_rspec_3.gemfile.lock b/gemfiles/ruby_3.2.0_rspec_3.gemfile.lock index d57b57aa..9c199be2 100644 --- a/gemfiles/ruby_3.2.0_rspec_3.gemfile.lock +++ b/gemfiles/ruby_3.2.0_rspec_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock index f1157e86..463070d0 100644 --- a/gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock index 88623370..df9628b9 100644 --- a/gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock index 99c7a32b..edf29ca7 100644 --- a/gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock index 0f99035c..1adf4431 100644 --- a/gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock index 6ff9ea89..dc80381b 100644 --- a/gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock index 3c780d69..87fab104 100644 --- a/gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.3.0_minitest_5.gemfile.lock b/gemfiles/ruby_3.3.0_minitest_5.gemfile.lock index 0a6a6365..b1437dee 100644 --- a/gemfiles/ruby_3.3.0_minitest_5.gemfile.lock +++ b/gemfiles/ruby_3.3.0_minitest_5.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ diff --git a/gemfiles/ruby_3.3.0_rspec_3.gemfile.lock b/gemfiles/ruby_3.3.0_rspec_3.gemfile.lock index 825e55c0..2fa89013 100644 --- a/gemfiles/ruby_3.3.0_rspec_3.gemfile.lock +++ b/gemfiles/ruby_3.3.0_rspec_3.gemfile.lock @@ -2,6 +2,7 @@ PATH remote: .. specs: datadog-ci (0.1.1) + msgpack GEM remote: https://rubygems.org/ From 2a854ff79b5ddc5658bcc65297f10641698c3de2 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Tue, 19 Sep 2023 17:56:31 +0200 Subject: [PATCH 12/45] early structure for test visibility, transports and events --- lib/datadog/ci/serializers/base.rb | 21 ----------- lib/datadog/ci/test_visibility/events.rb | 29 +++++++++++++++ .../ci/test_visibility/events/event.rb | 17 +++++++++ lib/datadog/ci/test_visibility/events/span.rb | 16 +++++++++ lib/datadog/ci/test_visibility/events/test.rb | 16 +++++++++ lib/datadog/ci/test_visibility/transport.rb | 35 +++++++++++++++++++ .../test_serializer.rb => transport/http.rb} | 8 ++--- sig/datadog/ci/serializers/base.rbs | 15 -------- .../ci/serializers/v1/test_serializer.rbs | 10 ------ sig/datadog/ci/test_visibility/events.rbs | 11 ++++++ .../ci/test_visibility/events/event.rbs | 13 +++++++ .../ci/test_visibility/events/span.rbs | 11 ++++++ .../ci/test_visibility/events/test.rbs | 11 ++++++ sig/datadog/ci/test_visibility/transport.rbs | 18 ++++++++++ sig/datadog/ci/transport/http.rbs | 9 +++++ 15 files changed, 189 insertions(+), 51 deletions(-) delete mode 100644 lib/datadog/ci/serializers/base.rb create mode 100644 lib/datadog/ci/test_visibility/events.rb create mode 100644 lib/datadog/ci/test_visibility/events/event.rb create mode 100644 lib/datadog/ci/test_visibility/events/span.rb create mode 100644 lib/datadog/ci/test_visibility/events/test.rb create mode 100644 lib/datadog/ci/test_visibility/transport.rb rename lib/datadog/ci/{serializers/v1/test_serializer.rb => transport/http.rb} (51%) delete mode 100644 sig/datadog/ci/serializers/base.rbs delete mode 100644 sig/datadog/ci/serializers/v1/test_serializer.rbs create mode 100644 sig/datadog/ci/test_visibility/events.rbs create mode 100644 sig/datadog/ci/test_visibility/events/event.rbs create mode 100644 sig/datadog/ci/test_visibility/events/span.rbs create mode 100644 sig/datadog/ci/test_visibility/events/test.rbs create mode 100644 sig/datadog/ci/test_visibility/transport.rbs create mode 100644 sig/datadog/ci/transport/http.rbs diff --git a/lib/datadog/ci/serializers/base.rb b/lib/datadog/ci/serializers/base.rb deleted file mode 100644 index f27b9620..00000000 --- a/lib/datadog/ci/serializers/base.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Datadog - module CI - module Serializers - class Base - attr_reader :trace - - def initialize(trace) - @trace = trace - end - - def to_json - end - - def to_msgpack - end - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/events.rb b/lib/datadog/ci/test_visibility/events.rb new file mode 100644 index 00000000..26efe2b4 --- /dev/null +++ b/lib/datadog/ci/test_visibility/events.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "events/test" +require_relative "events/span" + +module Datadog + module CI + module TestVisibility + module Events + module_function + + def extract_from_trace(trace) + # TODO: replace with filter_map when 1.0 + trace.spans.map { |span| convert_span(span) }.reject(&:nil?) + end + + def convert_span(span) + case span.type + when Datadog::CI::Ext::AppTypes::TYPE_TEST + Events::Test.new(span) + # TODO move to constant + when "span" + Events::Span.new(span) + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/events/event.rb b/lib/datadog/ci/test_visibility/events/event.rb new file mode 100644 index 00000000..ba89cdc5 --- /dev/null +++ b/lib/datadog/ci/test_visibility/events/event.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Datadog + module CI + module TestVisibility + module Events + class Event + def initialize + end + + def to_msgpack + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/events/span.rb b/lib/datadog/ci/test_visibility/events/span.rb new file mode 100644 index 00000000..f8e22833 --- /dev/null +++ b/lib/datadog/ci/test_visibility/events/span.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "event" + +module Datadog + module CI + module TestVisibility + module Events + class Span < Event + def initialize + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/events/test.rb b/lib/datadog/ci/test_visibility/events/test.rb new file mode 100644 index 00000000..a6c3a183 --- /dev/null +++ b/lib/datadog/ci/test_visibility/events/test.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "event" + +module Datadog + module CI + module TestVisibility + module Events + class Test < Event + def initialize + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb new file mode 100644 index 00000000..47d161fc --- /dev/null +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "datadog/core/encoding" +# use it to chunk payloads by size +# require "datadog/core/chunker" + +module Datadog + module CI + module TestVisibility + class Transport + def initialize + @encoder = Datadog::Core::Encoding::MsgpackEncoder + end + + def send_traces(traces) + # convert traces to events and construct payload + events = traces.map { |trace| PayloadEvents.convert_from_trace(trace) } + # payload = Payload.new(events) + # @encoder.encode(payload) + end + + private + + # represents payload with some subset of serializable events to be sent to CI-APP intake + class Payload + def initialize(events) + end + + def to_msgpack + end + end + end + end + end +end diff --git a/lib/datadog/ci/serializers/v1/test_serializer.rb b/lib/datadog/ci/transport/http.rb similarity index 51% rename from lib/datadog/ci/serializers/v1/test_serializer.rb rename to lib/datadog/ci/transport/http.rb index 6d71c685..0d862449 100644 --- a/lib/datadog/ci/serializers/v1/test_serializer.rb +++ b/lib/datadog/ci/transport/http.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true -require "../base" - module Datadog module CI - module Serializers - module V1 - class TestSerializer < Base + module Transport + class HTTP + def initialize end end end diff --git a/sig/datadog/ci/serializers/base.rbs b/sig/datadog/ci/serializers/base.rbs deleted file mode 100644 index d8946660..00000000 --- a/sig/datadog/ci/serializers/base.rbs +++ /dev/null @@ -1,15 +0,0 @@ -module Datadog - module CI - module Serializers - class Base - attr_reader trace: Datadog::Tracing::TraceSegment - - def initialize: (Datadog::Tracing::TraceSegment trace) -> void - - def to_json: () -> nil - - def to_msgpack: () -> nil - end - end - end -end diff --git a/sig/datadog/ci/serializers/v1/test_serializer.rbs b/sig/datadog/ci/serializers/v1/test_serializer.rbs deleted file mode 100644 index 6134ae94..00000000 --- a/sig/datadog/ci/serializers/v1/test_serializer.rbs +++ /dev/null @@ -1,10 +0,0 @@ -module Datadog - module CI - module Serializers - module V1 - class TestSerializer - end - end - end - end -end diff --git a/sig/datadog/ci/test_visibility/events.rbs b/sig/datadog/ci/test_visibility/events.rbs new file mode 100644 index 00000000..dd805153 --- /dev/null +++ b/sig/datadog/ci/test_visibility/events.rbs @@ -0,0 +1,11 @@ +module Datadog + module CI + module TestVisibility + module Events + def self?.extract_from_trace: (untyped trace) -> untyped + + def self?.convert_span: (untyped span) -> untyped + end + end + end +end diff --git a/sig/datadog/ci/test_visibility/events/event.rbs b/sig/datadog/ci/test_visibility/events/event.rbs new file mode 100644 index 00000000..4261f820 --- /dev/null +++ b/sig/datadog/ci/test_visibility/events/event.rbs @@ -0,0 +1,13 @@ +module Datadog + module CI + module TestVisibility + module Events + class Event + def initialize: () -> void + + def to_msgpack: () -> nil + end + end + end + end +end diff --git a/sig/datadog/ci/test_visibility/events/span.rbs b/sig/datadog/ci/test_visibility/events/span.rbs new file mode 100644 index 00000000..be40dff5 --- /dev/null +++ b/sig/datadog/ci/test_visibility/events/span.rbs @@ -0,0 +1,11 @@ +module Datadog + module CI + module TestVisibility + module Events + class Span < Event + def initialize: () -> void + end + end + end + end +end diff --git a/sig/datadog/ci/test_visibility/events/test.rbs b/sig/datadog/ci/test_visibility/events/test.rbs new file mode 100644 index 00000000..e179a9e7 --- /dev/null +++ b/sig/datadog/ci/test_visibility/events/test.rbs @@ -0,0 +1,11 @@ +module Datadog + module CI + module TestVisibility + module Events + class Test < Event + def initialize: () -> void + end + end + end + end +end diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs new file mode 100644 index 00000000..204ae3e1 --- /dev/null +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -0,0 +1,18 @@ +module Datadog + module CI + module TestVisibility + class Transport + def initialize: () -> void + + def send_traces: (untyped traces) -> untyped + + private + class Payload + def initialize: (untyped events) -> void + + def to_msgpack: () -> nil + end + end + end + end +end diff --git a/sig/datadog/ci/transport/http.rbs b/sig/datadog/ci/transport/http.rbs new file mode 100644 index 00000000..909f6037 --- /dev/null +++ b/sig/datadog/ci/transport/http.rbs @@ -0,0 +1,9 @@ +module Datadog + module CI + module Transport + class HTTP + def initialize: () -> void + end + end + end +end From cd2d0702a97ad29fd36e44018f16ca25def03429 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Wed, 20 Sep 2023 12:47:03 +0200 Subject: [PATCH 13/45] move contrib tracer helpers to common helpers; serialize test event to msgpack --- lib/datadog/ci/test_visibility/events.rb | 29 ---- .../ci/test_visibility/events/event.rb | 17 --- lib/datadog/ci/test_visibility/events/span.rb | 16 -- lib/datadog/ci/test_visibility/events/test.rb | 16 -- .../ci/test_visibility/serializer/base.rb | 34 +++++ .../ci/test_visibility/serializer/span.rb | 17 +++ .../ci/test_visibility/serializer/test.rb | 61 ++++++++ .../something_that_converts_traces.rb | 27 ++++ lib/datadog/ci/test_visibility/transport.rb | 28 +++- .../contrib/cucumber/instrumentation_spec.rb | 2 - .../ci/contrib/cucumber/integration_spec.rb | 2 - .../ci/contrib/cucumber/patcher_spec.rb | 2 - .../contrib/minitest/instrumentation_spec.rb | 2 - .../ci/contrib/minitest/integration_spec.rb | 2 - .../ci/contrib/minitest/patcher_spec.rb | 2 - .../ci/contrib/rspec/instrumentation_spec.rb | 2 - .../ci/contrib/rspec/integration_spec.rb | 2 - spec/datadog/ci/contrib/rspec/patcher_spec.rb | 2 - .../datadog/ci/contrib/support/spec_helper.rb | 32 ---- .../ci/contrib/support/tracer_helpers.rb | 103 ------------- .../test_visibility/serializer/test_spec.rb | 66 +++++++++ spec/spec_helper.rb | 27 ++++ .../ci_mode_helpers.rb} | 0 spec/support/tracer_helpers.rb | 138 ++++++++++-------- 24 files changed, 333 insertions(+), 296 deletions(-) delete mode 100644 lib/datadog/ci/test_visibility/events.rb delete mode 100644 lib/datadog/ci/test_visibility/events/event.rb delete mode 100644 lib/datadog/ci/test_visibility/events/span.rb delete mode 100644 lib/datadog/ci/test_visibility/events/test.rb create mode 100644 lib/datadog/ci/test_visibility/serializer/base.rb create mode 100644 lib/datadog/ci/test_visibility/serializer/span.rb create mode 100644 lib/datadog/ci/test_visibility/serializer/test.rb create mode 100644 lib/datadog/ci/test_visibility/something_that_converts_traces.rb delete mode 100644 spec/datadog/ci/contrib/support/spec_helper.rb delete mode 100644 spec/datadog/ci/contrib/support/tracer_helpers.rb create mode 100644 spec/datadog/ci/test_visibility/serializer/test_spec.rb rename spec/{datadog/ci/contrib/support/mode_helpers.rb => support/ci_mode_helpers.rb} (100%) diff --git a/lib/datadog/ci/test_visibility/events.rb b/lib/datadog/ci/test_visibility/events.rb deleted file mode 100644 index 26efe2b4..00000000 --- a/lib/datadog/ci/test_visibility/events.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require_relative "events/test" -require_relative "events/span" - -module Datadog - module CI - module TestVisibility - module Events - module_function - - def extract_from_trace(trace) - # TODO: replace with filter_map when 1.0 - trace.spans.map { |span| convert_span(span) }.reject(&:nil?) - end - - def convert_span(span) - case span.type - when Datadog::CI::Ext::AppTypes::TYPE_TEST - Events::Test.new(span) - # TODO move to constant - when "span" - Events::Span.new(span) - end - end - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/events/event.rb b/lib/datadog/ci/test_visibility/events/event.rb deleted file mode 100644 index ba89cdc5..00000000 --- a/lib/datadog/ci/test_visibility/events/event.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Datadog - module CI - module TestVisibility - module Events - class Event - def initialize - end - - def to_msgpack - end - end - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/events/span.rb b/lib/datadog/ci/test_visibility/events/span.rb deleted file mode 100644 index f8e22833..00000000 --- a/lib/datadog/ci/test_visibility/events/span.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require_relative "event" - -module Datadog - module CI - module TestVisibility - module Events - class Span < Event - def initialize - end - end - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/events/test.rb b/lib/datadog/ci/test_visibility/events/test.rb deleted file mode 100644 index a6c3a183..00000000 --- a/lib/datadog/ci/test_visibility/events/test.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require_relative "event" - -module Datadog - module CI - module TestVisibility - module Events - class Test < Event - def initialize - end - end - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/serializer/base.rb b/lib/datadog/ci/test_visibility/serializer/base.rb new file mode 100644 index 00000000..01763fa3 --- /dev/null +++ b/lib/datadog/ci/test_visibility/serializer/base.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Datadog + module CI + module TestVisibility + module Serializer + class Base + attr_reader :trace, :span + + def initialize(trace, span) + @trace = trace + @span = span + end + + def runtime_id + @trace.runtime_id + end + + # Used for serialization + # @return [Integer] in nanoseconds since Epoch + def time_nano(time) + time.to_i * 1000000000 + time.nsec + end + + # Used for serialization + # @return [Integer] in nanoseconds since Epoch + def duration_nano(duration) + (duration * 1e9).to_i + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/serializer/span.rb b/lib/datadog/ci/test_visibility/serializer/span.rb new file mode 100644 index 00000000..96535baa --- /dev/null +++ b/lib/datadog/ci/test_visibility/serializer/span.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "base" + +module Datadog + module CI + module TestVisibility + module Serializer + class Span < Base + def to_msgpack(packer = nil) + packer ||= MessagePack::Packer.new + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/serializer/test.rb b/lib/datadog/ci/test_visibility/serializer/test.rb new file mode 100644 index 00000000..586f327e --- /dev/null +++ b/lib/datadog/ci/test_visibility/serializer/test.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require_relative "base" +require_relative "../../ext/test" + +module Datadog + module CI + module TestVisibility + module Serializer + class Test < Base + def to_msgpack(packer = nil) + packer ||= MessagePack::Packer.new unless defined?(@packer) + + packer.write_map_header(3) + + packer.write("type") + packer.write("test") + + packer.write("version") + packer.write(1) + + packer.write("content") + + packer.write_map_header(10) + + packer.write("trace_id") + packer.write(@trace.id) + + packer.write("span_id") + packer.write(@span.id) + + packer.write("name") + packer.write("#{@span.get_tag(Ext::Test::TAG_FRAMEWORK)}.test") + + packer.write("resource") + packer.write("#{@span.get_tag(Ext::Test::TAG_SUITE)}.#{@span.get_tag(Ext::Test::TAG_NAME)}") + + packer.write("service") + packer.write(@span.service) + + packer.write("type") + packer.write("test") + + packer.write("start") + packer.write(time_nano(@span.start_time)) + + packer.write("duration") + packer.write(duration_nano(@span.duration)) + + packer.write("meta") + packer.write(@span.meta) + + # metrics have the same value as meta + packer.write("metrics") + packer.write({}) + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/something_that_converts_traces.rb b/lib/datadog/ci/test_visibility/something_that_converts_traces.rb new file mode 100644 index 00000000..71a584b5 --- /dev/null +++ b/lib/datadog/ci/test_visibility/something_that_converts_traces.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative "serializer/test" +require_relative "serializer/span" + +module Datadog + module CI + module TestVisibility + module SomethingThatConvertsTraces + module_function + + def convert(trace) + trace.spans.map { |span| convert_span(trace, span) } + end + + def convert_span(trace, span) + case span.type + when Datadog::CI::Ext::AppTypes::TYPE_TEST + Serializer::Test.new(trace, span) + else + Serializer::Span.new(trace, span) + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 47d161fc..33b17a84 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "something_that_converts_traces" require "datadog/core/encoding" # use it to chunk payloads by size # require "datadog/core/chunker" @@ -14,8 +15,8 @@ def initialize def send_traces(traces) # convert traces to events and construct payload - events = traces.map { |trace| PayloadEvents.convert_from_trace(trace) } - # payload = Payload.new(events) + events = traces.flat_map { |trace| SomethingThatConvertsTraces.convert(trace) } + payload = Payload.new(events) # @encoder.encode(payload) end @@ -24,9 +25,30 @@ def send_traces(traces) # represents payload with some subset of serializable events to be sent to CI-APP intake class Payload def initialize(events) + @events = events end - def to_msgpack + def to_msgpack(packer) + packer ||= MessagePack::Packer.new + + packer.write_map_header(3) # Set header with how many elements in the map + packer.write("version") + packer.write(1) + + packer.write("metadata") + packer.write_map_header(3) + + packer.write("runtime-id") + packer.write(@events.first.runtime_id) + + packer.write("language") + packer.write("ruby") + + packer.write("library_version") + packer.write(Datadog::CI::VERSION::STRING) + + packer.write_array_header(@events.size) + packer.write(@events) end end end diff --git a/spec/datadog/ci/contrib/cucumber/instrumentation_spec.rb b/spec/datadog/ci/contrib/cucumber/instrumentation_spec.rb index 6389e985..7a7adcb7 100644 --- a/spec/datadog/ci/contrib/cucumber/instrumentation_spec.rb +++ b/spec/datadog/ci/contrib/cucumber/instrumentation_spec.rb @@ -1,5 +1,3 @@ -require_relative "../support/spec_helper" - require "stringio" require "cucumber" diff --git a/spec/datadog/ci/contrib/cucumber/integration_spec.rb b/spec/datadog/ci/contrib/cucumber/integration_spec.rb index 268247eb..be1a4e81 100644 --- a/spec/datadog/ci/contrib/cucumber/integration_spec.rb +++ b/spec/datadog/ci/contrib/cucumber/integration_spec.rb @@ -1,5 +1,3 @@ -require_relative "../support/spec_helper" - RSpec.describe Datadog::CI::Contrib::Cucumber::Integration do extend ConfigurationHelpers diff --git a/spec/datadog/ci/contrib/cucumber/patcher_spec.rb b/spec/datadog/ci/contrib/cucumber/patcher_spec.rb index a778e237..d7de0ec1 100644 --- a/spec/datadog/ci/contrib/cucumber/patcher_spec.rb +++ b/spec/datadog/ci/contrib/cucumber/patcher_spec.rb @@ -1,5 +1,3 @@ -require_relative "../support/spec_helper" - RSpec.describe Datadog::CI::Contrib::Cucumber::Patcher do describe ".patch" do subject!(:patch) { described_class.patch } diff --git a/spec/datadog/ci/contrib/minitest/instrumentation_spec.rb b/spec/datadog/ci/contrib/minitest/instrumentation_spec.rb index 282a65b7..d6ea4286 100644 --- a/spec/datadog/ci/contrib/minitest/instrumentation_spec.rb +++ b/spec/datadog/ci/contrib/minitest/instrumentation_spec.rb @@ -2,8 +2,6 @@ require "minitest" require "minitest/spec" -require_relative "../support/spec_helper" - RSpec.describe "Minitest hooks" do include_context "CI mode activated" do let(:integration_name) { :minitest } diff --git a/spec/datadog/ci/contrib/minitest/integration_spec.rb b/spec/datadog/ci/contrib/minitest/integration_spec.rb index 2438a245..5de418f6 100644 --- a/spec/datadog/ci/contrib/minitest/integration_spec.rb +++ b/spec/datadog/ci/contrib/minitest/integration_spec.rb @@ -1,5 +1,3 @@ -require_relative "../support/spec_helper" - RSpec.describe Datadog::CI::Contrib::Minitest::Integration do extend ConfigurationHelpers diff --git a/spec/datadog/ci/contrib/minitest/patcher_spec.rb b/spec/datadog/ci/contrib/minitest/patcher_spec.rb index 1107bcef..313c144f 100644 --- a/spec/datadog/ci/contrib/minitest/patcher_spec.rb +++ b/spec/datadog/ci/contrib/minitest/patcher_spec.rb @@ -1,5 +1,3 @@ -require_relative "../support/spec_helper" - require "minitest" RSpec.describe Datadog::CI::Contrib::Minitest::Patcher do diff --git a/spec/datadog/ci/contrib/rspec/instrumentation_spec.rb b/spec/datadog/ci/contrib/rspec/instrumentation_spec.rb index 00011631..5638451e 100644 --- a/spec/datadog/ci/contrib/rspec/instrumentation_spec.rb +++ b/spec/datadog/ci/contrib/rspec/instrumentation_spec.rb @@ -1,7 +1,5 @@ require "time" -require_relative "../support/spec_helper" - RSpec.describe "RSpec hooks" do include_context "CI mode activated" do let(:integration_name) { :rspec } diff --git a/spec/datadog/ci/contrib/rspec/integration_spec.rb b/spec/datadog/ci/contrib/rspec/integration_spec.rb index f4becaff..3588365e 100644 --- a/spec/datadog/ci/contrib/rspec/integration_spec.rb +++ b/spec/datadog/ci/contrib/rspec/integration_spec.rb @@ -1,5 +1,3 @@ -require_relative "../support/spec_helper" - RSpec.describe Datadog::CI::Contrib::RSpec::Integration do extend ConfigurationHelpers diff --git a/spec/datadog/ci/contrib/rspec/patcher_spec.rb b/spec/datadog/ci/contrib/rspec/patcher_spec.rb index 54ad8876..60e15a86 100644 --- a/spec/datadog/ci/contrib/rspec/patcher_spec.rb +++ b/spec/datadog/ci/contrib/rspec/patcher_spec.rb @@ -1,5 +1,3 @@ -require_relative "../support/spec_helper" - RSpec.describe Datadog::CI::Contrib::RSpec::Patcher do describe ".patch" do subject!(:patch) { described_class.patch } diff --git a/spec/datadog/ci/contrib/support/spec_helper.rb b/spec/datadog/ci/contrib/support/spec_helper.rb deleted file mode 100644 index afc41b0c..00000000 --- a/spec/datadog/ci/contrib/support/spec_helper.rb +++ /dev/null @@ -1,32 +0,0 @@ -require_relative "mode_helpers" -require_relative "tracer_helpers" - -if defined?(Warning.ignore) - # Caused by https://github.com/cucumber/cucumber-ruby/blob/47c8e2d7c97beae8541c895a43f9ccb96324f0f1/lib/cucumber/encoding.rb#L5-L6 - Gem.path.each do |path| - Warning.ignore(/setting Encoding.default_external/, path) - Warning.ignore(/setting Encoding.default_internal/, path) - end -end - -RSpec.configure do |config| - config.include Contrib::TracerHelpers - - # Raise error when patching an integration fails. - # This can be disabled by unstubbing +CommonMethods#on_patch_error+ - require "datadog/tracing/contrib/patcher" - config.before do - allow_any_instance_of(Datadog::Tracing::Contrib::Patcher::CommonMethods).to(receive(:on_patch_error)) { |_, e| raise e } - end - - # Ensure tracer environment is clean before running tests. - # - # This is done :before and not :after because doing so after - # can create noise for test assertions. For example: - # +expect(Datadog).to receive(:shutdown!).once+ - config.before do - Datadog.shutdown! - # without_warnings { Datadog.configuration.reset! } - Datadog.configuration.reset! - end -end diff --git a/spec/datadog/ci/contrib/support/tracer_helpers.rb b/spec/datadog/ci/contrib/support/tracer_helpers.rb deleted file mode 100644 index 100ca5ae..00000000 --- a/spec/datadog/ci/contrib/support/tracer_helpers.rb +++ /dev/null @@ -1,103 +0,0 @@ -require "datadog/tracing" - -module Contrib - # Contrib-specific tracer helpers. - # For contrib, we only allow one tracer to be active: - # the global tracer in +Datadog::Tracing+. - module TracerHelpers - # Returns the current tracer instance - def tracer - Datadog::Tracing.send(:tracer) - end - - # Returns spans and caches it (similar to +let(:spans)+). - def traces - @traces ||= fetch_traces - end - - # Returns spans and caches it (similar to +let(:spans)+). - def spans - @spans ||= fetch_spans - end - - # Retrieves all traces in the current tracer instance. - # This method does not cache its results. - def fetch_traces(tracer = self.tracer) - tracer.instance_variable_get(:@traces) || [] - end - - # Retrieves and sorts all spans in the current tracer instance. - # This method does not cache its results. - def fetch_spans(tracer = self.tracer) - traces = fetch_traces(tracer) - traces.collect(&:spans).flatten.sort! do |a, b| - if a.name == b.name - if a.resource == b.resource - if a.start_time == b.start_time - a.end_time <=> b.end_time - else - a.start_time <=> b.start_time - end - else - a.resource <=> b.resource - end - else - a.name <=> b.name - end - end - end - - # Remove all traces from the current tracer instance and - # busts cache of +#spans+ and +#span+. - def clear_traces! - tracer.instance_variable_set(:@traces, []) - - @traces = nil - @trace = nil - @spans = nil - @span = nil - end - - RSpec.configure do |config| - # Capture spans from the global tracer - config.before do - # DEV `*_any_instance_of` has concurrency issues when running with parallelism (e.g. JRuby). - # DEV Single object `allow` and `expect` work as intended with parallelism. - allow(Datadog::Tracing::Tracer).to receive(:new).and_wrap_original do |method, **args, &block| - instance = method.call(**args, &block) - - # The mutex must be eagerly initialized to prevent race conditions on lazy initialization - write_lock = Mutex.new - allow(instance).to receive(:write) do |trace| - instance.instance_exec do - write_lock.synchronize do - @traces ||= [] - @traces << trace - end - end - end - - instance - end - end - - # Execute shutdown! after the test has finished - # teardown and mock verifications. - # - # Changing this to `config.after(:each)` would - # put shutdown! inside the test scope, interfering - # with mock assertions. - config.around do |example| - example.run.tap do - Datadog::Tracing.shutdown! - end - end - end - - # Useful for integration testing. - def use_real_tracer! - @use_real_tracer = true - allow(Datadog::Tracing::Tracer).to receive(:new).and_call_original - end - end -end diff --git a/spec/datadog/ci/test_visibility/serializer/test_spec.rb b/spec/datadog/ci/test_visibility/serializer/test_spec.rb new file mode 100644 index 00000000..3890e3fe --- /dev/null +++ b/spec/datadog/ci/test_visibility/serializer/test_spec.rb @@ -0,0 +1,66 @@ +require_relative "../../../../../lib/datadog/ci/test_visibility/serializer/test" +require_relative "../../../../../lib/datadog/ci/recorder" + +RSpec.describe Datadog::CI::TestVisibility::Serializer::Test do + include_context "CI mode activated" do + let(:integration_name) { :rspec } + end + + subject { Datadog::CI::TestVisibility::Serializer::Test.new(trace, span) } + + describe "#to_msgpack" do + context "traced a single test execution with Recorder" do + before do + Datadog::CI::Recorder.trace( + "rspec.example", + { + span_options: { + resource: "test_add", + service: "rspec-test-suite" + }, + framework: "rspec", + framework_version: "3.0.0", + test_name: "test_add", + test_suite: "calculator_tests.rb", + test_type: "test" + } + ) do |span| + Datadog::CI::Recorder.passed!(span) + end + end + + let(:payload) { MessagePack.unpack(MessagePack.pack(subject)) } + + it "serializes test event to messagepack" do + expect(payload).to include( + { + "version" => 1, + "type" => "test" + } + ) + content = payload["content"] + expect(content).to include( + { + "trace_id" => trace.id, + "span_id" => span.id, + "name" => "rspec.test", + "service" => "rspec-test-suite", + "type" => "test" + } + ) + + tags = content["meta"] + expect(tags).to include( + { + "test.framework" => "rspec", + "_dd.origin" => "ciapp-test" + } + ) + # TODO: test start and duration with timecop + # expect(content["start"]).to eq(1) + # expect(content["duration"]).to eq(1) + # + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c55d75cd..47ba5b5e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,10 +16,19 @@ require_relative "support/platform_helpers" require_relative "support/git_helpers" require_relative "support/provider_test_helpers" +require_relative "support/ci_mode_helpers" require "rspec/collection_matchers" require "climate_control" +if defined?(Warning.ignore) + # Caused by https://github.com/cucumber/cucumber-ruby/blob/47c8e2d7c97beae8541c895a43f9ccb96324f0f1/lib/cucumber/encoding.rb#L5-L6 + Gem.path.each do |path| + Warning.ignore(/setting Encoding.default_external/, path) + Warning.ignore(/setting Encoding.default_internal/, path) + end +end + RSpec.configure do |config| config.include ConfigurationHelpers config.include TracerHelpers @@ -36,4 +45,22 @@ config.expect_with :rspec do |c| c.syntax = :expect end + + # Raise error when patching an integration fails. + # This can be disabled by unstubbing +CommonMethods#on_patch_error+ + require "datadog/tracing/contrib/patcher" + config.before do + allow_any_instance_of(Datadog::Tracing::Contrib::Patcher::CommonMethods).to(receive(:on_patch_error)) { |_, e| raise e } + end + + # Ensure tracer environment is clean before running tests. + # + # This is done :before and not :after because doing so after + # can create noise for test assertions. For example: + # +expect(Datadog).to receive(:shutdown!).once+ + config.before do + Datadog.shutdown! + # without_warnings { Datadog.configuration.reset! } + Datadog.configuration.reset! + end end diff --git a/spec/datadog/ci/contrib/support/mode_helpers.rb b/spec/support/ci_mode_helpers.rb similarity index 100% rename from spec/datadog/ci/contrib/support/mode_helpers.rb rename to spec/support/ci_mode_helpers.rb diff --git a/spec/support/tracer_helpers.rb b/spec/support/tracer_helpers.rb index 16987012..d57a96a7 100644 --- a/spec/support/tracer_helpers.rb +++ b/spec/support/tracer_helpers.rb @@ -1,67 +1,21 @@ require "datadog/tracing" -require_relative "faux_writer" +# For contrib, we only allow one tracer to be active: +# the global tracer in +Datadog::Tracing+. module TracerHelpers - # Return a test tracer instance with a faux writer. + # Returns the current tracer instance def tracer - @tracer ||= new_tracer - end - - def new_tracer(options = {}) - writer = FauxWriter.new( - transport: Datadog::Transport::HTTP.default do |t| - t.adapter :test - end - ) - - options = {writer: writer}.merge(options) - Datadog::Tracing::Tracer.new(**options) - end - - def get_test_writer(options = {}) - options = { - transport: Datadog::Transport::HTTP.default do |t| - t.adapter :test - end - }.merge(options) - - FauxWriter.new(options) - end - - # Return some test traces - def get_test_traces(n, service: "test-app", resource: "/traces", type: "web") - traces = [] - - n.times do - trace_op = Datadog::Tracing::TraceOperation.new - - trace_op.measure("client.testing", service: service, resource: resource, type: type) do - trace_op.measure("client.testing", service: service, resource: resource, type: type) do - end - end - - traces << trace_op.flush! - end - - traces - end - - # Return some test services - def get_test_services - {"rest-api" => {"app" => "rails", "app_type" => "web"}, - "master" => {"app" => "postgres", "app_type" => "db"}} - end - - def writer - tracer.writer + Datadog::Tracing.send(:tracer) end + # Returns spans and caches it (similar to +let(:spans)+). def traces - @traces ||= writer.traces + @traces ||= fetch_traces end + # Returns spans and caches it (similar to +let(:spans)+). def spans - @spans ||= writer.spans + @spans ||= fetch_spans end # Returns the only trace in the current tracer writer. @@ -88,8 +42,37 @@ def span end end + # Retrieves all traces in the current tracer instance. + # This method does not cache its results. + def fetch_traces(tracer = self.tracer) + tracer.instance_variable_get(:@traces) || [] + end + + # Retrieves and sorts all spans in the current tracer instance. + # This method does not cache its results. + def fetch_spans(tracer = self.tracer) + traces = fetch_traces(tracer) + traces.collect(&:spans).flatten.sort! do |a, b| + if a.name == b.name + if a.resource == b.resource + if a.start_time == b.start_time + a.end_time <=> b.end_time + else + a.start_time <=> b.start_time + end + else + a.resource <=> b.resource + end + else + a.name <=> b.name + end + end + end + + # Remove all traces from the current tracer instance and + # busts cache of +#spans+ and +#span+. def clear_traces! - writer.spans(:clear) + tracer.instance_variable_set(:@traces, []) @traces = nil @trace = nil @@ -97,14 +80,45 @@ def clear_traces! @span = nil end - def tracer_shutdown! - if defined?(@use_real_tracer) && @use_real_tracer - Datadog::Tracing.shutdown! - elsif defined?(@tracer) && @tracer - @tracer.shutdown! - @tracer = nil + RSpec.configure do |config| + # Capture spans from the global tracer + config.before do + # DEV `*_any_instance_of` has concurrency issues when running with parallelism (e.g. JRuby). + # DEV Single object `allow` and `expect` work as intended with parallelism. + allow(Datadog::Tracing::Tracer).to receive(:new).and_wrap_original do |method, **args, &block| + instance = method.call(**args, &block) + + # The mutex must be eagerly initialized to prevent race conditions on lazy initialization + write_lock = Mutex.new + allow(instance).to receive(:write) do |trace| + instance.instance_exec do + write_lock.synchronize do + @traces ||= [] + @traces << trace + end + end + end + + instance + end + end + + # Execute shutdown! after the test has finished + # teardown and mock verifications. + # + # Changing this to `config.after(:each)` would + # put shutdown! inside the test scope, interfering + # with mock assertions. + config.around do |example| + example.run.tap do + Datadog::Tracing.shutdown! + end end + end - without_warnings { Datadog.send(:reset!) } + # Useful for integration testing. + def use_real_tracer! + @use_real_tracer = true + allow(Datadog::Tracing::Tracer).to receive(:new).and_call_original end end From 0ce035bba7e0efd583f067e68262b5aa9ad74c1c Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Wed, 20 Sep 2023 13:37:23 +0200 Subject: [PATCH 14/45] serializable span event --- .../ci/test_visibility/serializer/span.rb | 48 ++++++++ .../ci/test_visibility/serializer/test.rb | 5 +- lib/datadog/ci/test_visibility/transport.rb | 114 +++++++++--------- .../test_visibility/serializer/span_spec.rb | 80 ++++++++++++ .../test_visibility/serializer/test_spec.rb | 7 +- 5 files changed, 191 insertions(+), 63 deletions(-) create mode 100644 spec/datadog/ci/test_visibility/serializer/span_spec.rb diff --git a/lib/datadog/ci/test_visibility/serializer/span.rb b/lib/datadog/ci/test_visibility/serializer/span.rb index 96535baa..9da993d6 100644 --- a/lib/datadog/ci/test_visibility/serializer/span.rb +++ b/lib/datadog/ci/test_visibility/serializer/span.rb @@ -9,6 +9,54 @@ module Serializer class Span < Base def to_msgpack(packer = nil) packer ||= MessagePack::Packer.new + + packer.write_map_header(3) + + packer.write("type") + packer.write("span") + + packer.write("version") + packer.write(1) + + packer.write("content") + + packer.write_map_header(12) + + packer.write("trace_id") + packer.write(@trace.id) + + packer.write("span_id") + packer.write(@span.id) + + packer.write("parent_id") + packer.write(@span.parent_id) + + packer.write("name") + packer.write(@span.name) + + packer.write("resource") + packer.write(@span.resource) + + packer.write("service") + packer.write(@span.service) + + packer.write("type") + packer.write(@span.type) + + packer.write("error") + packer.write(@span.status) + + packer.write("start") + packer.write(time_nano(@span.start_time)) + + packer.write("duration") + packer.write(duration_nano(@span.duration)) + + packer.write("meta") + packer.write(@span.meta) + + packer.write("metrics") + packer.write(@span.metrics) end end end diff --git a/lib/datadog/ci/test_visibility/serializer/test.rb b/lib/datadog/ci/test_visibility/serializer/test.rb index 586f327e..48d54c9e 100644 --- a/lib/datadog/ci/test_visibility/serializer/test.rb +++ b/lib/datadog/ci/test_visibility/serializer/test.rb @@ -9,7 +9,7 @@ module TestVisibility module Serializer class Test < Base def to_msgpack(packer = nil) - packer ||= MessagePack::Packer.new unless defined?(@packer) + packer ||= MessagePack::Packer.new packer.write_map_header(3) @@ -50,9 +50,8 @@ def to_msgpack(packer = nil) packer.write("meta") packer.write(@span.meta) - # metrics have the same value as meta packer.write("metrics") - packer.write({}) + packer.write(@span.metrics) end end end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 33b17a84..6712753f 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -1,57 +1,57 @@ -# frozen_string_literal: true - -require_relative "something_that_converts_traces" -require "datadog/core/encoding" -# use it to chunk payloads by size -# require "datadog/core/chunker" - -module Datadog - module CI - module TestVisibility - class Transport - def initialize - @encoder = Datadog::Core::Encoding::MsgpackEncoder - end - - def send_traces(traces) - # convert traces to events and construct payload - events = traces.flat_map { |trace| SomethingThatConvertsTraces.convert(trace) } - payload = Payload.new(events) - # @encoder.encode(payload) - end - - private - - # represents payload with some subset of serializable events to be sent to CI-APP intake - class Payload - def initialize(events) - @events = events - end - - def to_msgpack(packer) - packer ||= MessagePack::Packer.new - - packer.write_map_header(3) # Set header with how many elements in the map - packer.write("version") - packer.write(1) - - packer.write("metadata") - packer.write_map_header(3) - - packer.write("runtime-id") - packer.write(@events.first.runtime_id) - - packer.write("language") - packer.write("ruby") - - packer.write("library_version") - packer.write(Datadog::CI::VERSION::STRING) - - packer.write_array_header(@events.size) - packer.write(@events) - end - end - end - end - end -end +# # frozen_string_literal: true + +# require_relative "something_that_converts_traces" +# require "datadog/core/encoding" +# # use it to chunk payloads by size +# # require "datadog/core/chunker" + +# module Datadog +# module CI +# module TestVisibility +# class Transport +# def initialize +# @encoder = Datadog::Core::Encoding::MsgpackEncoder +# end + +# def send_traces(traces) +# # convert traces to events and construct payload +# events = traces.flat_map { |trace| SomethingThatConvertsTraces.convert(trace) } +# payload = Payload.new(events) +# # @encoder.encode(payload) +# end + +# private + +# # represents payload with some subset of serializable events to be sent to CI-APP intake +# class Payload +# def initialize(events) +# @events = events +# end + +# def to_msgpack(packer) +# packer ||= MessagePack::Packer.new + +# packer.write_map_header(3) # Set header with how many elements in the map +# packer.write("version") +# packer.write(1) + +# packer.write("metadata") +# packer.write_map_header(3) + +# packer.write("runtime-id") +# packer.write(@events.first.runtime_id) + +# packer.write("language") +# packer.write("ruby") + +# packer.write("library_version") +# packer.write(Datadog::CI::VERSION::STRING) + +# packer.write_array_header(@events.size) +# packer.write(@events) +# end +# end +# end +# end +# end +# end diff --git a/spec/datadog/ci/test_visibility/serializer/span_spec.rb b/spec/datadog/ci/test_visibility/serializer/span_spec.rb new file mode 100644 index 00000000..dfa572c9 --- /dev/null +++ b/spec/datadog/ci/test_visibility/serializer/span_spec.rb @@ -0,0 +1,80 @@ +require_relative "../../../../../lib/datadog/ci/test_visibility/serializer/span" +require_relative "../../../../../lib/datadog/ci/recorder" + +RSpec.describe Datadog::CI::TestVisibility::Serializer::Span do + include_context "CI mode activated" do + let(:integration_name) { :rspec } + end + + let(:test_span) do + spans.find { |span| span.type == "test" } + end + + let(:tracer_span) do + spans.find { |span| span.type != "test" } + end + subject { described_class.new(trace, tracer_span) } + + describe "#to_msgpack" do + context "traced a single test execution with Recorder" do + before do + Datadog::CI::Recorder.trace( + "rspec.example", + { + span_options: { + resource: "test_add", + service: "rspec-test-suite" + }, + framework: "rspec", + framework_version: "3.0.0", + test_name: "test_add", + test_suite: "calculator_tests.rb", + test_type: "test" + } + ) do |span| + Datadog::Tracing.trace("http-call", type: "http", service: "net-http") do |span, trace| + span.set_tag("custom_tag", "custom_tag_value") + end + + Datadog::CI::Recorder.passed!(span) + end + end + + let(:payload) { MessagePack.unpack(MessagePack.pack(subject)) } + + it "serializes test event to messagepack" do + expect(payload).to include( + { + "version" => 1, + "type" => "span" + } + ) + content = payload["content"] + expect(content).to include( + { + "trace_id" => trace.id, + "span_id" => tracer_span.id, + "parent_id" => test_span.id, + "name" => "http-call", + "service" => "net-http", + "type" => "http", + "error" => 0, + "resource" => "http-call" + } + ) + + tags = content["meta"] + expect(tags).to include( + { + "custom_tag" => "custom_tag_value", + "_dd.origin" => "ciapp-test" + } + ) + # TODO: test start and duration with timecop + # expect(content["start"]).to eq(1) + # expect(content["duration"]).to eq(1) + # + end + end + end +end diff --git a/spec/datadog/ci/test_visibility/serializer/test_spec.rb b/spec/datadog/ci/test_visibility/serializer/test_spec.rb index 3890e3fe..d06f81ee 100644 --- a/spec/datadog/ci/test_visibility/serializer/test_spec.rb +++ b/spec/datadog/ci/test_visibility/serializer/test_spec.rb @@ -6,7 +6,7 @@ let(:integration_name) { :rspec } end - subject { Datadog::CI::TestVisibility::Serializer::Test.new(trace, span) } + subject { described_class.new(trace, span) } describe "#to_msgpack" do context "traced a single test execution with Recorder" do @@ -21,7 +21,7 @@ framework: "rspec", framework_version: "3.0.0", test_name: "test_add", - test_suite: "calculator_tests.rb", + test_suite: "calculator_tests", test_type: "test" } ) do |span| @@ -45,7 +45,8 @@ "span_id" => span.id, "name" => "rspec.test", "service" => "rspec-test-suite", - "type" => "test" + "type" => "test", + "resource" => "calculator_tests.test_add" } ) From 040773c200162da0fb4239c24aa06116a0397da2 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Wed, 20 Sep 2023 17:15:56 +0200 Subject: [PATCH 15/45] extract serialization logic to the base serializer --- .../ci/test_visibility/serializer/base.rb | 34 ----- .../ci/test_visibility/serializer/span.rb | 65 --------- .../ci/test_visibility/serializer/test.rb | 60 -------- lib/datadog/ci/test_visibility/serializers.rb | 28 ++++ .../ci/test_visibility/serializers/base.rb | 136 ++++++++++++++++++ .../ci/test_visibility/serializers/span.rb | 26 ++++ .../ci/test_visibility/serializers/test_v1.rb | 34 +++++ .../something_that_converts_traces.rb | 27 ---- .../{serializer => serializers}/span_spec.rb | 30 +--- .../test_v1_spec.rb} | 21 +-- .../ci/test_visibility/serializers_spec.rb | 40 ++++++ spec/spec_helper.rb | 3 - spec/support/faux_transport.rb | 16 --- spec/support/faux_writer.rb | 54 ------- spec/support/test_helpers.rb | 31 ---- spec/support/tracer_helpers.rb | 37 +++++ 16 files changed, 310 insertions(+), 332 deletions(-) delete mode 100644 lib/datadog/ci/test_visibility/serializer/base.rb delete mode 100644 lib/datadog/ci/test_visibility/serializer/span.rb delete mode 100644 lib/datadog/ci/test_visibility/serializer/test.rb create mode 100644 lib/datadog/ci/test_visibility/serializers.rb create mode 100644 lib/datadog/ci/test_visibility/serializers/base.rb create mode 100644 lib/datadog/ci/test_visibility/serializers/span.rb create mode 100644 lib/datadog/ci/test_visibility/serializers/test_v1.rb delete mode 100644 lib/datadog/ci/test_visibility/something_that_converts_traces.rb rename spec/datadog/ci/test_visibility/{serializer => serializers}/span_spec.rb (64%) rename spec/datadog/ci/test_visibility/{serializer/test_spec.rb => serializers/test_v1_spec.rb} (70%) create mode 100644 spec/datadog/ci/test_visibility/serializers_spec.rb delete mode 100644 spec/support/faux_transport.rb delete mode 100644 spec/support/faux_writer.rb delete mode 100644 spec/support/test_helpers.rb diff --git a/lib/datadog/ci/test_visibility/serializer/base.rb b/lib/datadog/ci/test_visibility/serializer/base.rb deleted file mode 100644 index 01763fa3..00000000 --- a/lib/datadog/ci/test_visibility/serializer/base.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Datadog - module CI - module TestVisibility - module Serializer - class Base - attr_reader :trace, :span - - def initialize(trace, span) - @trace = trace - @span = span - end - - def runtime_id - @trace.runtime_id - end - - # Used for serialization - # @return [Integer] in nanoseconds since Epoch - def time_nano(time) - time.to_i * 1000000000 + time.nsec - end - - # Used for serialization - # @return [Integer] in nanoseconds since Epoch - def duration_nano(duration) - (duration * 1e9).to_i - end - end - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/serializer/span.rb b/lib/datadog/ci/test_visibility/serializer/span.rb deleted file mode 100644 index 9da993d6..00000000 --- a/lib/datadog/ci/test_visibility/serializer/span.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Datadog - module CI - module TestVisibility - module Serializer - class Span < Base - def to_msgpack(packer = nil) - packer ||= MessagePack::Packer.new - - packer.write_map_header(3) - - packer.write("type") - packer.write("span") - - packer.write("version") - packer.write(1) - - packer.write("content") - - packer.write_map_header(12) - - packer.write("trace_id") - packer.write(@trace.id) - - packer.write("span_id") - packer.write(@span.id) - - packer.write("parent_id") - packer.write(@span.parent_id) - - packer.write("name") - packer.write(@span.name) - - packer.write("resource") - packer.write(@span.resource) - - packer.write("service") - packer.write(@span.service) - - packer.write("type") - packer.write(@span.type) - - packer.write("error") - packer.write(@span.status) - - packer.write("start") - packer.write(time_nano(@span.start_time)) - - packer.write("duration") - packer.write(duration_nano(@span.duration)) - - packer.write("meta") - packer.write(@span.meta) - - packer.write("metrics") - packer.write(@span.metrics) - end - end - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/serializer/test.rb b/lib/datadog/ci/test_visibility/serializer/test.rb deleted file mode 100644 index 48d54c9e..00000000 --- a/lib/datadog/ci/test_visibility/serializer/test.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" -require_relative "../../ext/test" - -module Datadog - module CI - module TestVisibility - module Serializer - class Test < Base - def to_msgpack(packer = nil) - packer ||= MessagePack::Packer.new - - packer.write_map_header(3) - - packer.write("type") - packer.write("test") - - packer.write("version") - packer.write(1) - - packer.write("content") - - packer.write_map_header(10) - - packer.write("trace_id") - packer.write(@trace.id) - - packer.write("span_id") - packer.write(@span.id) - - packer.write("name") - packer.write("#{@span.get_tag(Ext::Test::TAG_FRAMEWORK)}.test") - - packer.write("resource") - packer.write("#{@span.get_tag(Ext::Test::TAG_SUITE)}.#{@span.get_tag(Ext::Test::TAG_NAME)}") - - packer.write("service") - packer.write(@span.service) - - packer.write("type") - packer.write("test") - - packer.write("start") - packer.write(time_nano(@span.start_time)) - - packer.write("duration") - packer.write(duration_nano(@span.duration)) - - packer.write("meta") - packer.write(@span.meta) - - packer.write("metrics") - packer.write(@span.metrics) - end - end - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/serializers.rb b/lib/datadog/ci/test_visibility/serializers.rb new file mode 100644 index 00000000..294241ef --- /dev/null +++ b/lib/datadog/ci/test_visibility/serializers.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require_relative "serializers/test_v1" +require_relative "serializers/span" + +module Datadog + module CI + module TestVisibility + module Serializers + module_function + + def convert_trace_to_serializable_events(trace) + trace.spans.map { |span| convert_span_to_serializable_event(trace, span) } + end + + # for test suite visibility we might need to make it configurable + def convert_span_to_serializable_event(trace, span) + case span.type + when Datadog::CI::Ext::AppTypes::TYPE_TEST + Serializers::TestV1.new(trace, span) + else + Serializers::Span.new(trace, span) + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/serializers/base.rb b/lib/datadog/ci/test_visibility/serializers/base.rb new file mode 100644 index 00000000..c5cfaa05 --- /dev/null +++ b/lib/datadog/ci/test_visibility/serializers/base.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +module Datadog + module CI + module TestVisibility + module Serializers + class Base + attr_reader :trace, :span + + def initialize(trace, span) + @trace = trace + @span = span + end + + def to_msgpack(packer = nil) + packer ||= MessagePack::Packer.new + + packer.write_map_header(3) + + write_field(packer, "type") + write_field(packer, "version") + + packer.write("content") + packer.write_map_header(content_fields_count) + + content_fields.each do |field| + if field.is_a?(Hash) + field.each do |field_name, method| + write_field(packer, field_name, method) + end + else + write_field(packer, field) + end + end + end + + def content_fields + [] + end + + def runtime_id + @trace.runtime_id + end + + def trace_id + @trace.id + end + + def span_id + @span.id + end + + def parent_id + @span.parent_id + end + + def type + end + + def version + 1 + end + + def span_type + @span.type + end + + def name + @span.name + end + + def resource + @span.resource + end + + def service + @span.service + end + + def start + time_nano(@span.start_time) + end + + def duration + duration_nano(@span.duration) + end + + def meta + @span.meta + end + + def metrics + @span.metrics + end + + def error + @span.status + end + + private + + def write_field(packer, field_name, method = nil) + method ||= field_name + + packer.write(field_name) + packer.write(send(method)) + end + + # Used for serialization + # @return [Integer] in nanoseconds since Epoch + def time_nano(time) + time.to_i * 1000000000 + time.nsec + end + + # Used for serialization + # @return [Integer] in nanoseconds since Epoch + def duration_nano(duration) + (duration * 1e9).to_i + end + + def content_fields_count + res = 0 + content_fields.each do |field| + res += if field.is_a?(Hash) + field.size + else + 1 + end + end + res + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/serializers/span.rb b/lib/datadog/ci/test_visibility/serializers/span.rb new file mode 100644 index 00000000..07bd45e4 --- /dev/null +++ b/lib/datadog/ci/test_visibility/serializers/span.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative "base" + +module Datadog + module CI + module TestVisibility + module Serializers + class Span < Base + def content_fields + @content_fields ||= [ + "trace_id", "span_id", "parent_id", "name", + "resource", "service", "error", "start", + "duration", "meta", "metrics", + "type" => "span_type" + ] + end + + def type + "span" + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/serializers/test_v1.rb b/lib/datadog/ci/test_visibility/serializers/test_v1.rb new file mode 100644 index 00000000..7200e75b --- /dev/null +++ b/lib/datadog/ci/test_visibility/serializers/test_v1.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative "base" +require_relative "../../ext/test" + +module Datadog + module CI + module TestVisibility + module Serializers + class TestV1 < Base + def content_fields + @content_fields ||= [ + "trace_id", "span_id", "name", "resource", "service", + "start", "duration", "meta", "metrics", "error", + "type" => "span_type" + ] + end + + def type + "test" + end + + def name + "#{@span.get_tag(Ext::Test::TAG_FRAMEWORK)}.test" + end + + def resource + "#{@span.get_tag(Ext::Test::TAG_SUITE)}.#{@span.get_tag(Ext::Test::TAG_NAME)}" + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/something_that_converts_traces.rb b/lib/datadog/ci/test_visibility/something_that_converts_traces.rb deleted file mode 100644 index 71a584b5..00000000 --- a/lib/datadog/ci/test_visibility/something_that_converts_traces.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require_relative "serializer/test" -require_relative "serializer/span" - -module Datadog - module CI - module TestVisibility - module SomethingThatConvertsTraces - module_function - - def convert(trace) - trace.spans.map { |span| convert_span(trace, span) } - end - - def convert_span(trace, span) - case span.type - when Datadog::CI::Ext::AppTypes::TYPE_TEST - Serializer::Test.new(trace, span) - else - Serializer::Span.new(trace, span) - end - end - end - end - end -end diff --git a/spec/datadog/ci/test_visibility/serializer/span_spec.rb b/spec/datadog/ci/test_visibility/serializers/span_spec.rb similarity index 64% rename from spec/datadog/ci/test_visibility/serializer/span_spec.rb rename to spec/datadog/ci/test_visibility/serializers/span_spec.rb index dfa572c9..3c96b7f7 100644 --- a/spec/datadog/ci/test_visibility/serializer/span_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/span_spec.rb @@ -1,7 +1,7 @@ -require_relative "../../../../../lib/datadog/ci/test_visibility/serializer/span" +require_relative "../../../../../lib/datadog/ci/test_visibility/serializers/span" require_relative "../../../../../lib/datadog/ci/recorder" -RSpec.describe Datadog::CI::TestVisibility::Serializer::Span do +RSpec.describe Datadog::CI::TestVisibility::Serializers::Span do include_context "CI mode activated" do let(:integration_name) { :rspec } end @@ -13,31 +13,13 @@ let(:tracer_span) do spans.find { |span| span.type != "test" } end + subject { described_class.new(trace, tracer_span) } describe "#to_msgpack" do context "traced a single test execution with Recorder" do before do - Datadog::CI::Recorder.trace( - "rspec.example", - { - span_options: { - resource: "test_add", - service: "rspec-test-suite" - }, - framework: "rspec", - framework_version: "3.0.0", - test_name: "test_add", - test_suite: "calculator_tests.rb", - test_type: "test" - } - ) do |span| - Datadog::Tracing.trace("http-call", type: "http", service: "net-http") do |span, trace| - span.set_tag("custom_tag", "custom_tag_value") - end - - Datadog::CI::Recorder.passed!(span) - end + produce_test_trace(with_http_span: true) end let(:payload) { MessagePack.unpack(MessagePack.pack(subject)) } @@ -53,8 +35,8 @@ expect(content).to include( { "trace_id" => trace.id, - "span_id" => tracer_span.id, - "parent_id" => test_span.id, + "span_id" => first_other_span.id, + "parent_id" => first_test_span.id, "name" => "http-call", "service" => "net-http", "type" => "http", diff --git a/spec/datadog/ci/test_visibility/serializer/test_spec.rb b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb similarity index 70% rename from spec/datadog/ci/test_visibility/serializer/test_spec.rb rename to spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb index d06f81ee..e043920a 100644 --- a/spec/datadog/ci/test_visibility/serializer/test_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb @@ -1,7 +1,7 @@ -require_relative "../../../../../lib/datadog/ci/test_visibility/serializer/test" +require_relative "../../../../../lib/datadog/ci/test_visibility/serializers/test_v1" require_relative "../../../../../lib/datadog/ci/recorder" -RSpec.describe Datadog::CI::TestVisibility::Serializer::Test do +RSpec.describe Datadog::CI::TestVisibility::Serializers::TestV1 do include_context "CI mode activated" do let(:integration_name) { :rspec } end @@ -11,22 +11,7 @@ describe "#to_msgpack" do context "traced a single test execution with Recorder" do before do - Datadog::CI::Recorder.trace( - "rspec.example", - { - span_options: { - resource: "test_add", - service: "rspec-test-suite" - }, - framework: "rspec", - framework_version: "3.0.0", - test_name: "test_add", - test_suite: "calculator_tests", - test_type: "test" - } - ) do |span| - Datadog::CI::Recorder.passed!(span) - end + produce_test_trace end let(:payload) { MessagePack.unpack(MessagePack.pack(subject)) } diff --git a/spec/datadog/ci/test_visibility/serializers_spec.rb b/spec/datadog/ci/test_visibility/serializers_spec.rb new file mode 100644 index 00000000..0297c01b --- /dev/null +++ b/spec/datadog/ci/test_visibility/serializers_spec.rb @@ -0,0 +1,40 @@ +require_relative "../../../../lib/datadog/ci/test_visibility/serializers" +require_relative "../../../../lib/datadog/ci/recorder" + +RSpec.describe Datadog::CI::TestVisibility::Serializers do + include_context "CI mode activated" do + let(:integration_name) { :rspec } + end + + subject { described_class.convert_trace_to_serializable_events(trace) } + + describe ".convert_trace_to_serializable_events" do + context "traced a single test execution with Recorder" do + before do + produce_test_trace(with_http_span: true) + end + + let(:payload) { MessagePack.unpack(MessagePack.pack(subject)) } + + it "converts trace to an array of serializable events" do + expect(subject.count).to eq(2) + end + + context "test event is present" do + let(:test_event) { subject.find { |event| event.type == "test" } } + + it "contains test span" do + expect(test_event.span_id).to eq(first_test_span.id) + end + end + + context "span event is present" do + let(:span_event) { subject.find { |event| event.type == "span" } } + + it "contains http span" do + expect(span_event.span_id).to eq(first_other_span.id) + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 47ba5b5e..fd20be7e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,7 +12,6 @@ require_relative "support/log_helpers" require_relative "support/tracer_helpers" require_relative "support/span_helpers" -require_relative "support/test_helpers" require_relative "support/platform_helpers" require_relative "support/git_helpers" require_relative "support/provider_test_helpers" @@ -34,8 +33,6 @@ config.include TracerHelpers config.include SpanHelpers - config.include TestHelpers::RSpec::Integration, :integration - # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" diff --git a/spec/support/faux_transport.rb b/spec/support/faux_transport.rb deleted file mode 100644 index 1474343f..00000000 --- a/spec/support/faux_transport.rb +++ /dev/null @@ -1,16 +0,0 @@ -require "ddtrace/transport/http" - -# FauxTransport is a dummy Datadog::Transport that doesn't send data to an agent. -class FauxTransport < Datadog::Transport::HTTP::Client - def initialize(*) - end - - def send_traces(*) - # Emulate an OK response - [Datadog::Transport::HTTP::Traces::Response.new( - Datadog::Transport::HTTP::Adapters::Net::Response.new( - Net::HTTPResponse.new(1.0, 200, "OK") - ) - )] - end -end diff --git a/spec/support/faux_writer.rb b/spec/support/faux_writer.rb deleted file mode 100644 index ebba7e36..00000000 --- a/spec/support/faux_writer.rb +++ /dev/null @@ -1,54 +0,0 @@ -require_relative "faux_transport" - -# FauxWriter is a dummy writer that buffers spans locally. -class FauxWriter < Datadog::Tracing::Writer - def initialize(options = {}) - options[:transport] ||= FauxTransport.new - options[:call_original] ||= true - @options = options - - super if options[:call_original] - @mutex = Mutex.new - - # easy access to registered components - @traces = [] - end - - def write(trace) - @mutex.synchronize do - super(trace) if @options[:call_original] - @traces << trace - end - end - - def traces(action = :clear) - traces = @traces - @traces = [] if action == :clear - traces - end - - def spans(action = :clear) - @mutex.synchronize do - traces = @traces - @traces = [] if action == :clear - spans = traces.collect(&:spans).flatten - # sort the spans to avoid test flakiness - - spans.sort! do |a, b| - if a.name == b.name - if a.resource == b.resource - if a.start_time == b.start_time - a.end_time <=> b.end_time - else - a.start_time <=> b.start_time - end - else - a.resource <=> b.resource - end - else - a.name <=> b.name - end - end - end - end -end diff --git a/spec/support/test_helpers.rb b/spec/support/test_helpers.rb deleted file mode 100644 index 037ecf6f..00000000 --- a/spec/support/test_helpers.rb +++ /dev/null @@ -1,31 +0,0 @@ -module TestHelpers - module_function - - # Integration tests are normally expensive (time-wise or resource-wise). - # They run in CI by default. - def run_integration_tests? - ENV["TEST_DATADOG_INTEGRATION"] - end - - module RSpec - # RSpec extension to allow for declaring integration tests - # using example group parameters: - # - # ```ruby - # describe 'end-to-end foo test', :integration do - # ... - # end - # ``` - module Integration - def self.included(base) - base.class_exec do - before do - unless run_integration_tests? - skip("Integration tests can be enabled by setting the environment variable `TEST_DATADOG_INTEGRATION=1`") - end - end - end - end - end - end -end diff --git a/spec/support/tracer_helpers.rb b/spec/support/tracer_helpers.rb index d57a96a7..7414909c 100644 --- a/spec/support/tracer_helpers.rb +++ b/spec/support/tracer_helpers.rb @@ -8,6 +8,43 @@ def tracer Datadog::Tracing.send(:tracer) end + def produce_test_trace( + framework: "rspec", operation: "rspec.example", + test_name: "test_add", test_suite: "calculator_tests", + service: "rspec-test-suite", with_http_span: false + ) + Datadog::CI::Recorder.trace( + operation, + { + span_options: { + resource: test_name, + service: service + }, + framework: framework, + framework_version: "1.0.0", + test_name: test_name, + test_suite: test_suite, + test_type: "test" + } + ) do |span| + if with_http_span + Datadog::Tracing.trace("http-call", type: "http", service: "net-http") do |span, trace| + span.set_tag("custom_tag", "custom_tag_value") + end + end + + Datadog::CI::Recorder.passed!(span) + end + end + + def first_test_span + spans.find { |span| span.type == "test" } + end + + def first_other_span + spans.find { |span| span.type != "test" } + end + # Returns spans and caches it (similar to +let(:spans)+). def traces @traces ||= fetch_traces From 622305a585e41ee2e9bbfcd0c92911be16678c2a Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 21 Sep 2023 10:42:13 +0200 Subject: [PATCH 16/45] fix static type checks --- Steepfile | 1 + .../ci/test_visibility/serializers/base.rb | 4 +- .../ci/test_visibility/serializers/span.rb | 7 ++- .../ci/test_visibility/serializers/test_v1.rb | 6 +- lib/datadog/ci/transport/http.rb | 22 +++---- sig/datadog/ci/test_visibility/events.rbs | 11 ---- .../ci/test_visibility/events/event.rbs | 13 ----- .../ci/test_visibility/events/span.rbs | 11 ---- .../ci/test_visibility/events/test.rbs | 11 ---- .../ci/test_visibility/serializers.rbs | 10 ++++ .../ci/test_visibility/serializers/base.rbs | 58 +++++++++++++++++++ .../ci/test_visibility/serializers/span.rbs | 14 +++++ .../test_visibility/serializers/test_v1.rbs | 18 ++++++ sig/datadog/ci/test_visibility/transport.rbs | 18 ------ vendor/rbs/ddtrace/0/datadog/tracing/span.rbs | 14 +++++ vendor/rbs/msgpack/0/message_pack/packer.rbs | 9 +++ 16 files changed, 146 insertions(+), 81 deletions(-) delete mode 100644 sig/datadog/ci/test_visibility/events.rbs delete mode 100644 sig/datadog/ci/test_visibility/events/event.rbs delete mode 100644 sig/datadog/ci/test_visibility/events/span.rbs delete mode 100644 sig/datadog/ci/test_visibility/events/test.rbs create mode 100644 sig/datadog/ci/test_visibility/serializers.rbs create mode 100644 sig/datadog/ci/test_visibility/serializers/base.rbs create mode 100644 sig/datadog/ci/test_visibility/serializers/span.rbs create mode 100644 sig/datadog/ci/test_visibility/serializers/test_v1.rbs delete mode 100644 sig/datadog/ci/test_visibility/transport.rbs create mode 100644 vendor/rbs/msgpack/0/message_pack/packer.rbs diff --git a/Steepfile b/Steepfile index 831f6f4a..825e971e 100644 --- a/Steepfile +++ b/Steepfile @@ -18,4 +18,5 @@ target :lib do library "open3" library "rspec" library "cucumber" + library "msgpack" end diff --git a/lib/datadog/ci/test_visibility/serializers/base.rb b/lib/datadog/ci/test_visibility/serializers/base.rb index c5cfaa05..ee171d4d 100644 --- a/lib/datadog/ci/test_visibility/serializers/base.rb +++ b/lib/datadog/ci/test_visibility/serializers/base.rb @@ -119,6 +119,8 @@ def duration_nano(duration) end def content_fields_count + return @content_fields_count if defined?(@content_fields_count) + res = 0 content_fields.each do |field| res += if field.is_a?(Hash) @@ -127,7 +129,7 @@ def content_fields_count 1 end end - res + @content_fields_count = res end end end diff --git a/lib/datadog/ci/test_visibility/serializers/span.rb b/lib/datadog/ci/test_visibility/serializers/span.rb index 07bd45e4..9906c8a6 100644 --- a/lib/datadog/ci/test_visibility/serializers/span.rb +++ b/lib/datadog/ci/test_visibility/serializers/span.rb @@ -9,9 +9,10 @@ module Serializers class Span < Base def content_fields @content_fields ||= [ - "trace_id", "span_id", "parent_id", "name", - "resource", "service", "error", "start", - "duration", "meta", "metrics", + "trace_id", "span_id", "parent_id", + "name", "resource", "service", + "error", "start", "duration", + "meta", "metrics", "type" => "span_type" ] end diff --git a/lib/datadog/ci/test_visibility/serializers/test_v1.rb b/lib/datadog/ci/test_visibility/serializers/test_v1.rb index 7200e75b..1c3684c9 100644 --- a/lib/datadog/ci/test_visibility/serializers/test_v1.rb +++ b/lib/datadog/ci/test_visibility/serializers/test_v1.rb @@ -10,8 +10,10 @@ module Serializers class TestV1 < Base def content_fields @content_fields ||= [ - "trace_id", "span_id", "name", "resource", "service", - "start", "duration", "meta", "metrics", "error", + "trace_id", "span_id", + "name", "resource", "service", + "error", "start", "duration", + "meta", "metrics", "type" => "span_type" ] end diff --git a/lib/datadog/ci/transport/http.rb b/lib/datadog/ci/transport/http.rb index 0d862449..3584b952 100644 --- a/lib/datadog/ci/transport/http.rb +++ b/lib/datadog/ci/transport/http.rb @@ -1,12 +1,12 @@ -# frozen_string_literal: true +# # frozen_string_literal: true -module Datadog - module CI - module Transport - class HTTP - def initialize - end - end - end - end -end +# module Datadog +# module CI +# module Transport +# class HTTP +# def initialize +# end +# end +# end +# end +# end diff --git a/sig/datadog/ci/test_visibility/events.rbs b/sig/datadog/ci/test_visibility/events.rbs deleted file mode 100644 index dd805153..00000000 --- a/sig/datadog/ci/test_visibility/events.rbs +++ /dev/null @@ -1,11 +0,0 @@ -module Datadog - module CI - module TestVisibility - module Events - def self?.extract_from_trace: (untyped trace) -> untyped - - def self?.convert_span: (untyped span) -> untyped - end - end - end -end diff --git a/sig/datadog/ci/test_visibility/events/event.rbs b/sig/datadog/ci/test_visibility/events/event.rbs deleted file mode 100644 index 4261f820..00000000 --- a/sig/datadog/ci/test_visibility/events/event.rbs +++ /dev/null @@ -1,13 +0,0 @@ -module Datadog - module CI - module TestVisibility - module Events - class Event - def initialize: () -> void - - def to_msgpack: () -> nil - end - end - end - end -end diff --git a/sig/datadog/ci/test_visibility/events/span.rbs b/sig/datadog/ci/test_visibility/events/span.rbs deleted file mode 100644 index be40dff5..00000000 --- a/sig/datadog/ci/test_visibility/events/span.rbs +++ /dev/null @@ -1,11 +0,0 @@ -module Datadog - module CI - module TestVisibility - module Events - class Span < Event - def initialize: () -> void - end - end - end - end -end diff --git a/sig/datadog/ci/test_visibility/events/test.rbs b/sig/datadog/ci/test_visibility/events/test.rbs deleted file mode 100644 index e179a9e7..00000000 --- a/sig/datadog/ci/test_visibility/events/test.rbs +++ /dev/null @@ -1,11 +0,0 @@ -module Datadog - module CI - module TestVisibility - module Events - class Test < Event - def initialize: () -> void - end - end - end - end -end diff --git a/sig/datadog/ci/test_visibility/serializers.rbs b/sig/datadog/ci/test_visibility/serializers.rbs new file mode 100644 index 00000000..c8adc17e --- /dev/null +++ b/sig/datadog/ci/test_visibility/serializers.rbs @@ -0,0 +1,10 @@ +module Datadog + module CI + module TestVisibility + module Serializers + def self?.convert_trace_to_serializable_events: (Datadog::Tracing::TraceSegment trace) -> untyped + def self?.convert_span_to_serializable_event: (Datadog::Tracing::TraceSegment trace, Datadog::Tracing::Span span) -> untyped + end + end + end +end diff --git a/sig/datadog/ci/test_visibility/serializers/base.rbs b/sig/datadog/ci/test_visibility/serializers/base.rbs new file mode 100644 index 00000000..3257c2f3 --- /dev/null +++ b/sig/datadog/ci/test_visibility/serializers/base.rbs @@ -0,0 +1,58 @@ +module Datadog + module CI + module TestVisibility + module Serializers + class Base + @content_fields_count: Integer + + attr_reader trace: Datadog::Tracing::TraceSegment + attr_reader span: Datadog::Tracing::Span + + def initialize: (Datadog::Tracing::TraceSegment trace, Datadog::Tracing::Span span) -> void + + def to_msgpack: (?untyped? packer) -> untyped + + def content_fields: () -> ::Array[String | Hash[String, String]] + + def runtime_id: () -> String + + def trace_id: () -> String + + def span_id: () -> String + + def parent_id: () -> String + + def type: () -> nil + + def version: () -> 1 + + def span_type: () -> String + + def name: () -> String + + def resource: () -> String + + def service: () -> String + + def start: () -> Integer + + def duration: () -> Integer + + def meta: () -> Hash[String, untyped] + + def metrics: () -> Hash[String, untyped] + + def error: () -> Integer + + private + + def write_field: (untyped packer, String field_name, ?String? method) -> untyped + def time_nano: (Time time) -> Integer + def duration_nano: (Integer duration) -> Integer + + def content_fields_count: () -> Integer + end + end + end + end +end diff --git a/sig/datadog/ci/test_visibility/serializers/span.rbs b/sig/datadog/ci/test_visibility/serializers/span.rbs new file mode 100644 index 00000000..1d18eec0 --- /dev/null +++ b/sig/datadog/ci/test_visibility/serializers/span.rbs @@ -0,0 +1,14 @@ +module Datadog + module CI + module TestVisibility + module Serializers + class Span < Base + @content_fields: Array[String | Hash[String, String]] + def content_fields: () -> Array[String | Hash[String, String]] + + def type: () -> "span" + end + end + end + end +end diff --git a/sig/datadog/ci/test_visibility/serializers/test_v1.rbs b/sig/datadog/ci/test_visibility/serializers/test_v1.rbs new file mode 100644 index 00000000..f66ee815 --- /dev/null +++ b/sig/datadog/ci/test_visibility/serializers/test_v1.rbs @@ -0,0 +1,18 @@ +module Datadog + module CI + module TestVisibility + module Serializers + class TestV1 < Base + @content_fields: Array[String | Hash[String, String]] + def content_fields: () -> Array[String | Hash[String, String]] + + def type: () -> "test" + + def name: () -> ::String + + def resource: () -> ::String + end + end + end + end +end diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs deleted file mode 100644 index 204ae3e1..00000000 --- a/sig/datadog/ci/test_visibility/transport.rbs +++ /dev/null @@ -1,18 +0,0 @@ -module Datadog - module CI - module TestVisibility - class Transport - def initialize: () -> void - - def send_traces: (untyped traces) -> untyped - - private - class Payload - def initialize: (untyped events) -> void - - def to_msgpack: () -> nil - end - end - end - end -end diff --git a/vendor/rbs/ddtrace/0/datadog/tracing/span.rbs b/vendor/rbs/ddtrace/0/datadog/tracing/span.rbs index 9adb31f1..0d749106 100644 --- a/vendor/rbs/ddtrace/0/datadog/tracing/span.rbs +++ b/vendor/rbs/ddtrace/0/datadog/tracing/span.rbs @@ -4,6 +4,20 @@ module Datadog attr_accessor span_id: Integer def set_tag: (String key, ?untyped? value) -> void + def get_tag: (String key) -> String? + def type: () -> String + def id: () -> String + def trace_id: () -> String + def parent_id: () -> String + def name: () -> String + def resource: () -> String + def service: () -> String + def status: () -> Integer + def start_time: () -> Time + def end_time: () -> Time + def duration: () -> Integer + def meta: () -> Hash[String, untyped] + def metrics: () -> Hash[String, untyped] end end end diff --git a/vendor/rbs/msgpack/0/message_pack/packer.rbs b/vendor/rbs/msgpack/0/message_pack/packer.rbs new file mode 100644 index 00000000..7d1419dc --- /dev/null +++ b/vendor/rbs/msgpack/0/message_pack/packer.rbs @@ -0,0 +1,9 @@ +module MessagePack + class Packer + def initialize: () -> void + + def write: (String input) -> self + def write_map_header: (Integer keys_number) -> self + def write_array_header: (Integer keys_number) -> self + end +end From 777899f4081d6518e4203a4b4fa292390eb6eb91 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 21 Sep 2023 11:55:31 +0200 Subject: [PATCH 17/45] cleaning up tests, test serialization of failed test event --- lib/datadog/ci/test_visibility/transport.rb | 113 +++++++++--------- lib/datadog/ci/transport/http.rb | 12 -- sig/datadog/ci/test_visibility/transport.rbs | 8 ++ sig/datadog/ci/transport/http.rbs | 9 -- .../test_visibility/serializers/span_spec.rb | 19 +-- .../serializers/test_v1_spec.rb | 32 +++-- .../ci/test_visibility/serializers_spec.rb | 2 - spec/spec_helper.rb | 1 + .../test_visibility_event_serialized.rb | 17 +++ spec/support/tracer_helpers.rb | 12 +- 10 files changed, 118 insertions(+), 107 deletions(-) delete mode 100644 lib/datadog/ci/transport/http.rb create mode 100644 sig/datadog/ci/test_visibility/transport.rbs delete mode 100644 sig/datadog/ci/transport/http.rbs create mode 100644 spec/support/test_visibility_event_serialized.rb diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 6712753f..2dc1af3c 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -1,57 +1,56 @@ -# # frozen_string_literal: true - -# require_relative "something_that_converts_traces" -# require "datadog/core/encoding" -# # use it to chunk payloads by size -# # require "datadog/core/chunker" - -# module Datadog -# module CI -# module TestVisibility -# class Transport -# def initialize -# @encoder = Datadog::Core::Encoding::MsgpackEncoder -# end - -# def send_traces(traces) -# # convert traces to events and construct payload -# events = traces.flat_map { |trace| SomethingThatConvertsTraces.convert(trace) } -# payload = Payload.new(events) -# # @encoder.encode(payload) -# end - -# private - -# # represents payload with some subset of serializable events to be sent to CI-APP intake -# class Payload -# def initialize(events) -# @events = events -# end - -# def to_msgpack(packer) -# packer ||= MessagePack::Packer.new - -# packer.write_map_header(3) # Set header with how many elements in the map -# packer.write("version") -# packer.write(1) - -# packer.write("metadata") -# packer.write_map_header(3) - -# packer.write("runtime-id") -# packer.write(@events.first.runtime_id) - -# packer.write("language") -# packer.write("ruby") - -# packer.write("library_version") -# packer.write(Datadog::CI::VERSION::STRING) - -# packer.write_array_header(@events.size) -# packer.write(@events) -# end -# end -# end -# end -# end -# end +# frozen_string_literal: true + +require "datadog/core/encoding" +# use it to chunk payloads by size +# require "datadog/core/chunker" + +module Datadog + module CI + module TestVisibility + class Transport + # def initialize + # @encoder = Datadog::Core::Encoding::MsgpackEncoder + # end + + # def send_traces(traces) + # # convert traces to events and construct payload + # events = traces.flat_map { |trace| SomethingThatConvertsTraces.convert(trace) } + # payload = Payload.new(events) + # # @encoder.encode(payload) + # end + + # private + + # # represents payload with some subset of serializable events to be sent to CI-APP intake + # class Payload + # def initialize(events) + # @events = events + # end + + # def to_msgpack(packer) + # packer ||= MessagePack::Packer.new + + # packer.write_map_header(3) # Set header with how many elements in the map + # packer.write("version") + # packer.write(1) + + # packer.write("metadata") + # packer.write_map_header(3) + + # packer.write("runtime-id") + # packer.write(@events.first.runtime_id) + + # packer.write("language") + # packer.write("ruby") + + # packer.write("library_version") + # packer.write(Datadog::CI::VERSION::STRING) + + # packer.write_array_header(@events.size) + # packer.write(@events) + # end + # end + end + end + end +end diff --git a/lib/datadog/ci/transport/http.rb b/lib/datadog/ci/transport/http.rb deleted file mode 100644 index 3584b952..00000000 --- a/lib/datadog/ci/transport/http.rb +++ /dev/null @@ -1,12 +0,0 @@ -# # frozen_string_literal: true - -# module Datadog -# module CI -# module Transport -# class HTTP -# def initialize -# end -# end -# end -# end -# end diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs new file mode 100644 index 00000000..f7df3bc7 --- /dev/null +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -0,0 +1,8 @@ +module Datadog + module CI + module TestVisibility + class Transport + end + end + end +end diff --git a/sig/datadog/ci/transport/http.rbs b/sig/datadog/ci/transport/http.rbs deleted file mode 100644 index 909f6037..00000000 --- a/sig/datadog/ci/transport/http.rbs +++ /dev/null @@ -1,9 +0,0 @@ -module Datadog - module CI - module Transport - class HTTP - def initialize: () -> void - end - end - end -end diff --git a/spec/datadog/ci/test_visibility/serializers/span_spec.rb b/spec/datadog/ci/test_visibility/serializers/span_spec.rb index 3c96b7f7..b50060cc 100644 --- a/spec/datadog/ci/test_visibility/serializers/span_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/span_spec.rb @@ -6,6 +6,10 @@ let(:integration_name) { :rspec } end + include_context "Test visibility event serialized" do + subject { described_class.new(trace, tracer_span) } + end + let(:test_span) do spans.find { |span| span.type == "test" } end @@ -14,24 +18,14 @@ spans.find { |span| span.type != "test" } end - subject { described_class.new(trace, tracer_span) } - describe "#to_msgpack" do context "traced a single test execution with Recorder" do before do produce_test_trace(with_http_span: true) end - let(:payload) { MessagePack.unpack(MessagePack.pack(subject)) } - it "serializes test event to messagepack" do - expect(payload).to include( - { - "version" => 1, - "type" => "span" - } - ) - content = payload["content"] + expect_event_header(type: "span") expect(content).to include( { "trace_id" => trace.id, @@ -45,8 +39,7 @@ } ) - tags = content["meta"] - expect(tags).to include( + expect(meta).to include( { "custom_tag" => "custom_tag_value", "_dd.origin" => "ciapp-test" diff --git a/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb index e043920a..5fba31e0 100644 --- a/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb @@ -6,7 +6,9 @@ let(:integration_name) { :rspec } end - subject { described_class.new(trace, span) } + include_context "Test visibility event serialized" do + subject { described_class.new(trace, span) } + end describe "#to_msgpack" do context "traced a single test execution with Recorder" do @@ -14,16 +16,9 @@ produce_test_trace end - let(:payload) { MessagePack.unpack(MessagePack.pack(subject)) } - it "serializes test event to messagepack" do - expect(payload).to include( - { - "version" => 1, - "type" => "test" - } - ) - content = payload["content"] + expect_event_header + expect(content).to include( { "trace_id" => trace.id, @@ -35,10 +30,10 @@ } ) - tags = content["meta"] - expect(tags).to include( + expect(meta).to include( { "test.framework" => "rspec", + "test.status" => "pass", "_dd.origin" => "ciapp-test" } ) @@ -48,5 +43,18 @@ # end end + + context "trace a failed test" do + before do + produce_test_trace(result: "FAILED", exception: StandardError.new("1 + 2 are not equal to 5")) + end + + it "has error" do + expect_event_header + + expect(content).to include({"error" => 1}) + expect(meta).to include({"test.status" => "fail"}) + end + end end end diff --git a/spec/datadog/ci/test_visibility/serializers_spec.rb b/spec/datadog/ci/test_visibility/serializers_spec.rb index 0297c01b..c816d6a7 100644 --- a/spec/datadog/ci/test_visibility/serializers_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers_spec.rb @@ -14,8 +14,6 @@ produce_test_trace(with_http_span: true) end - let(:payload) { MessagePack.unpack(MessagePack.pack(subject)) } - it "converts trace to an array of serializable events" do expect(subject.count).to eq(2) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fd20be7e..bb5261d7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,6 +16,7 @@ require_relative "support/git_helpers" require_relative "support/provider_test_helpers" require_relative "support/ci_mode_helpers" +require_relative "support/test_visibility_event_serialized" require "rspec/collection_matchers" require "climate_control" diff --git a/spec/support/test_visibility_event_serialized.rb b/spec/support/test_visibility_event_serialized.rb new file mode 100644 index 00000000..af6de9c2 --- /dev/null +++ b/spec/support/test_visibility_event_serialized.rb @@ -0,0 +1,17 @@ +RSpec.shared_context "Test visibility event serialized" do + subject {} + + let(:msgpack_json) { MessagePack.unpack(MessagePack.pack(subject)) } + let(:content) { msgpack_json["content"] } + let(:meta) { content["meta"] } + let(:metrics) { content["metrics"] } + + def expect_event_header(version: 1, type: "test") + expect(msgpack_json).to include( + { + "version" => version, + "type" => type + } + ) + end +end diff --git a/spec/support/tracer_helpers.rb b/spec/support/tracer_helpers.rb index 7414909c..d81ed861 100644 --- a/spec/support/tracer_helpers.rb +++ b/spec/support/tracer_helpers.rb @@ -11,7 +11,8 @@ def tracer def produce_test_trace( framework: "rspec", operation: "rspec.example", test_name: "test_add", test_suite: "calculator_tests", - service: "rspec-test-suite", with_http_span: false + service: "rspec-test-suite", result: "PASSED", exception: nil, + with_http_span: false ) Datadog::CI::Recorder.trace( operation, @@ -33,7 +34,14 @@ def produce_test_trace( end end - Datadog::CI::Recorder.passed!(span) + case result + when "FAILED" + Datadog::CI::Recorder.failed!(span, exception) + when "SKIPPED" + Datadog::CI::Recorder.skipped!(span, exception) + else + Datadog::CI::Recorder.passed!(span) + end end end From 648e83909051b151e520707acee0d54945d80ca6 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 21 Sep 2023 12:06:53 +0200 Subject: [PATCH 18/45] test serializers with custom metrics and custom tags --- spec/datadog/ci/test_visibility/serializers/span_spec.rb | 1 + spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb | 6 +++++- spec/support/tracer_helpers.rb | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spec/datadog/ci/test_visibility/serializers/span_spec.rb b/spec/datadog/ci/test_visibility/serializers/span_spec.rb index b50060cc..97cd6e71 100644 --- a/spec/datadog/ci/test_visibility/serializers/span_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/span_spec.rb @@ -45,6 +45,7 @@ "_dd.origin" => "ciapp-test" } ) + expect(metrics).to eq({"_dd.top_level" => 1.0, "custom_metric" => 42}) # TODO: test start and duration with timecop # expect(content["start"]).to eq(1) # expect(content["duration"]).to eq(1) diff --git a/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb index 5fba31e0..7dc952ba 100644 --- a/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb @@ -34,9 +34,13 @@ { "test.framework" => "rspec", "test.status" => "pass", - "_dd.origin" => "ciapp-test" + "_dd.origin" => "ciapp-test", + "test_owner" => "my_team" } ) + expect(metrics).to eq( + {"_dd.measured" => 1.0, "_dd.top_level" => 1.0, "memory_allocations" => 16} + ) # TODO: test start and duration with timecop # expect(content["start"]).to eq(1) # expect(content["duration"]).to eq(1) diff --git a/spec/support/tracer_helpers.rb b/spec/support/tracer_helpers.rb index d81ed861..2c9707d8 100644 --- a/spec/support/tracer_helpers.rb +++ b/spec/support/tracer_helpers.rb @@ -31,9 +31,13 @@ def produce_test_trace( if with_http_span Datadog::Tracing.trace("http-call", type: "http", service: "net-http") do |span, trace| span.set_tag("custom_tag", "custom_tag_value") + span.set_tag("custom_metric", 42) end end + Datadog::Tracing.active_span.set_tag("test_owner", "my_team") + Datadog::Tracing.active_span.set_metric("memory_allocations", 16) + case result when "FAILED" Datadog::CI::Recorder.failed!(span, exception) From 896b299387196c0fa411183e5a0e618856dabd35 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 21 Sep 2023 13:21:39 +0200 Subject: [PATCH 19/45] test serialization of start and duration fields --- Gemfile | 4 +++- gemfiles/jruby_9.4.0.0_cucumber_3.gemfile | 3 ++- .../jruby_9.4.0.0_cucumber_3.gemfile.lock | 2 ++ gemfiles/jruby_9.4.0.0_cucumber_4.gemfile | 3 ++- .../jruby_9.4.0.0_cucumber_4.gemfile.lock | 2 ++ gemfiles/jruby_9.4.0.0_cucumber_5.gemfile | 3 ++- .../jruby_9.4.0.0_cucumber_5.gemfile.lock | 2 ++ gemfiles/jruby_9.4.0.0_cucumber_6.gemfile | 3 ++- .../jruby_9.4.0.0_cucumber_6.gemfile.lock | 2 ++ gemfiles/jruby_9.4.0.0_cucumber_7.gemfile | 3 ++- .../jruby_9.4.0.0_cucumber_7.gemfile.lock | 2 ++ gemfiles/jruby_9.4.0.0_cucumber_8.gemfile | 3 ++- .../jruby_9.4.0.0_cucumber_8.gemfile.lock | 2 ++ gemfiles/jruby_9.4.0.0_minitest_5.gemfile | 3 ++- .../jruby_9.4.0.0_minitest_5.gemfile.lock | 2 ++ gemfiles/jruby_9.4.0.0_rspec_3.gemfile | 3 ++- gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock | 2 ++ gemfiles/ruby_2.7.6_cucumber_3.gemfile | 3 ++- gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock | 2 ++ gemfiles/ruby_2.7.6_cucumber_4.gemfile | 3 ++- gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock | 2 ++ gemfiles/ruby_2.7.6_cucumber_5.gemfile | 3 ++- gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock | 2 ++ gemfiles/ruby_2.7.6_cucumber_6.gemfile | 3 ++- gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock | 2 ++ gemfiles/ruby_2.7.6_cucumber_7.gemfile | 3 ++- gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock | 2 ++ gemfiles/ruby_2.7.6_cucumber_8.gemfile | 3 ++- gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock | 2 ++ gemfiles/ruby_2.7.6_minitest_5.gemfile | 3 ++- gemfiles/ruby_2.7.6_minitest_5.gemfile.lock | 2 ++ gemfiles/ruby_2.7.6_rspec_3.gemfile | 3 ++- gemfiles/ruby_2.7.6_rspec_3.gemfile.lock | 2 ++ gemfiles/ruby_3.0.4_cucumber_3.gemfile | 3 ++- gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock | 2 ++ gemfiles/ruby_3.0.4_cucumber_4.gemfile | 3 ++- gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock | 2 ++ gemfiles/ruby_3.0.4_cucumber_5.gemfile | 3 ++- gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock | 2 ++ gemfiles/ruby_3.0.4_cucumber_6.gemfile | 3 ++- gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock | 2 ++ gemfiles/ruby_3.0.4_cucumber_7.gemfile | 3 ++- gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock | 2 ++ gemfiles/ruby_3.0.4_cucumber_8.gemfile | 3 ++- gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock | 2 ++ gemfiles/ruby_3.0.4_minitest_5.gemfile | 3 ++- gemfiles/ruby_3.0.4_minitest_5.gemfile.lock | 2 ++ gemfiles/ruby_3.0.4_rspec_3.gemfile | 3 ++- gemfiles/ruby_3.0.4_rspec_3.gemfile.lock | 2 ++ gemfiles/ruby_3.1.2_cucumber_3.gemfile | 3 ++- gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock | 2 ++ gemfiles/ruby_3.1.2_cucumber_4.gemfile | 3 ++- gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock | 2 ++ gemfiles/ruby_3.1.2_cucumber_5.gemfile | 3 ++- gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock | 2 ++ gemfiles/ruby_3.1.2_cucumber_6.gemfile | 3 ++- gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock | 2 ++ gemfiles/ruby_3.1.2_cucumber_7.gemfile | 3 ++- gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock | 2 ++ gemfiles/ruby_3.1.2_cucumber_8.gemfile | 3 ++- gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock | 2 ++ gemfiles/ruby_3.1.2_minitest_5.gemfile | 3 ++- gemfiles/ruby_3.1.2_minitest_5.gemfile.lock | 2 ++ gemfiles/ruby_3.1.2_rspec_3.gemfile | 3 ++- gemfiles/ruby_3.1.2_rspec_3.gemfile.lock | 2 ++ gemfiles/ruby_3.2.0_cucumber_3.gemfile | 3 ++- gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock | 2 ++ gemfiles/ruby_3.2.0_cucumber_4.gemfile | 3 ++- gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock | 2 ++ gemfiles/ruby_3.2.0_cucumber_5.gemfile | 3 ++- gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock | 2 ++ gemfiles/ruby_3.2.0_cucumber_6.gemfile | 3 ++- gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock | 2 ++ gemfiles/ruby_3.2.0_cucumber_7.gemfile | 3 ++- gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock | 2 ++ gemfiles/ruby_3.2.0_cucumber_8.gemfile | 3 ++- gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock | 2 ++ gemfiles/ruby_3.2.0_minitest_5.gemfile | 3 ++- gemfiles/ruby_3.2.0_minitest_5.gemfile.lock | 2 ++ gemfiles/ruby_3.2.0_rspec_3.gemfile | 3 ++- gemfiles/ruby_3.2.0_rspec_3.gemfile.lock | 2 ++ gemfiles/ruby_3.3.0_cucumber_3.gemfile | 3 ++- gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock | 2 ++ gemfiles/ruby_3.3.0_cucumber_4.gemfile | 3 ++- gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock | 2 ++ gemfiles/ruby_3.3.0_cucumber_5.gemfile | 3 ++- gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock | 2 ++ gemfiles/ruby_3.3.0_cucumber_6.gemfile | 3 ++- gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock | 2 ++ gemfiles/ruby_3.3.0_cucumber_7.gemfile | 3 ++- gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock | 2 ++ gemfiles/ruby_3.3.0_cucumber_8.gemfile | 3 ++- gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock | 2 ++ gemfiles/ruby_3.3.0_minitest_5.gemfile | 3 ++- gemfiles/ruby_3.3.0_minitest_5.gemfile.lock | 2 ++ gemfiles/ruby_3.3.0_rspec_3.gemfile | 3 ++- gemfiles/ruby_3.3.0_rspec_3.gemfile.lock | 2 ++ .../ci/test_visibility/serializers/base.rbs | 2 +- .../test_visibility/serializers/span_spec.rb | 5 +---- .../serializers/test_v1_spec.rb | 20 +++++++++++++++---- spec/spec_helper.rb | 1 + spec/support/tracer_helpers.rb | 14 ++++++++++++- vendor/rbs/ddtrace/0/datadog/tracing/span.rbs | 2 +- .../0/datadog/tracing/span_operation.rbs | 2 +- 104 files changed, 229 insertions(+), 61 deletions(-) diff --git a/Gemfile b/Gemfile index d9eda6be..f8ad03d9 100644 --- a/Gemfile +++ b/Gemfile @@ -10,14 +10,16 @@ gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" + gem "standard", "~> 1.31.0" gem "yard" diff --git a/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile b/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile index e333024b..be1f65fc 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile +++ b/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile.lock index 29c24a2d..2fdacb92 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_3.gemfile.lock @@ -127,6 +127,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -150,6 +151,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile b/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile index 118961f1..5e26524b 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile +++ b/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile.lock index 0e128613..8f63e799 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_4.gemfile.lock @@ -157,6 +157,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6-java) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -182,6 +183,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile b/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile index 9050053c..bd99a42e 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile +++ b/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile.lock index 2977b088..526814ee 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_5.gemfile.lock @@ -157,6 +157,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6-java) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -182,6 +183,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile b/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile index 75eb8323..9b5a94ad 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile +++ b/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile.lock index 7300c715..fe990d8e 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_6.gemfile.lock @@ -161,6 +161,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6-java) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -186,6 +187,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile b/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile index b425e539..310fd418 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile +++ b/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile.lock index fd908e56..2da09f58 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_7.gemfile.lock @@ -143,6 +143,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -166,6 +167,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile b/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile index 1f065ae6..9dcc7fc6 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile +++ b/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile.lock b/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile.lock index ef36178d..9cc64944 100644 --- a/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_cucumber_8.gemfile.lock @@ -137,6 +137,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -160,6 +161,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/jruby_9.4.0.0_minitest_5.gemfile b/gemfiles/jruby_9.4.0.0_minitest_5.gemfile index b1f7cdf8..d03bd893 100644 --- a/gemfiles/jruby_9.4.0.0_minitest_5.gemfile +++ b/gemfiles/jruby_9.4.0.0_minitest_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/jruby_9.4.0.0_minitest_5.gemfile.lock b/gemfiles/jruby_9.4.0.0_minitest_5.gemfile.lock index 4ebd4b2a..6fadfb6f 100644 --- a/gemfiles/jruby_9.4.0.0_minitest_5.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_minitest_5.gemfile.lock @@ -107,6 +107,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -130,6 +131,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/jruby_9.4.0.0_rspec_3.gemfile b/gemfiles/jruby_9.4.0.0_rspec_3.gemfile index d59f8d57..8c8e2b81 100644 --- a/gemfiles/jruby_9.4.0.0_rspec_3.gemfile +++ b/gemfiles/jruby_9.4.0.0_rspec_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec", "~> 3" gem "os" gem "climate_control" +gem "rspec", "~> 3" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock b/gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock index 822e2834..5c78a90a 100644 --- a/gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock +++ b/gemfiles/jruby_9.4.0.0_rspec_3.gemfile.lock @@ -106,6 +106,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -128,6 +129,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_2.7.6_cucumber_3.gemfile b/gemfiles/ruby_2.7.6_cucumber_3.gemfile index e333024b..be1f65fc 100644 --- a/gemfiles/ruby_2.7.6_cucumber_3.gemfile +++ b/gemfiles/ruby_2.7.6_cucumber_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock index 2b2bd47e..750d41ea 100644 --- a/gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_3.gemfile.lock @@ -124,6 +124,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -147,6 +148,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_2.7.6_cucumber_4.gemfile b/gemfiles/ruby_2.7.6_cucumber_4.gemfile index 118961f1..5e26524b 100644 --- a/gemfiles/ruby_2.7.6_cucumber_4.gemfile +++ b/gemfiles/ruby_2.7.6_cucumber_4.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock index 94af4aab..b755182f 100644 --- a/gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_4.gemfile.lock @@ -154,6 +154,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -179,6 +180,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_2.7.6_cucumber_5.gemfile b/gemfiles/ruby_2.7.6_cucumber_5.gemfile index 9050053c..bd99a42e 100644 --- a/gemfiles/ruby_2.7.6_cucumber_5.gemfile +++ b/gemfiles/ruby_2.7.6_cucumber_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock index 410cf587..6596bbd2 100644 --- a/gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_5.gemfile.lock @@ -154,6 +154,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -179,6 +180,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_2.7.6_cucumber_6.gemfile b/gemfiles/ruby_2.7.6_cucumber_6.gemfile index 75eb8323..9b5a94ad 100644 --- a/gemfiles/ruby_2.7.6_cucumber_6.gemfile +++ b/gemfiles/ruby_2.7.6_cucumber_6.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock index c3913afe..39434a0c 100644 --- a/gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_6.gemfile.lock @@ -158,6 +158,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -183,6 +184,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_2.7.6_cucumber_7.gemfile b/gemfiles/ruby_2.7.6_cucumber_7.gemfile index b425e539..310fd418 100644 --- a/gemfiles/ruby_2.7.6_cucumber_7.gemfile +++ b/gemfiles/ruby_2.7.6_cucumber_7.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock index 024cf47b..f83ffd71 100644 --- a/gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_7.gemfile.lock @@ -140,6 +140,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -163,6 +164,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_2.7.6_cucumber_8.gemfile b/gemfiles/ruby_2.7.6_cucumber_8.gemfile index 1f065ae6..9dcc7fc6 100644 --- a/gemfiles/ruby_2.7.6_cucumber_8.gemfile +++ b/gemfiles/ruby_2.7.6_cucumber_8.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock b/gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock index 4e62b81b..f9a7073c 100644 --- a/gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_2.7.6_cucumber_8.gemfile.lock @@ -134,6 +134,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -157,6 +158,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_2.7.6_minitest_5.gemfile b/gemfiles/ruby_2.7.6_minitest_5.gemfile index b1f7cdf8..d03bd893 100644 --- a/gemfiles/ruby_2.7.6_minitest_5.gemfile +++ b/gemfiles/ruby_2.7.6_minitest_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_2.7.6_minitest_5.gemfile.lock b/gemfiles/ruby_2.7.6_minitest_5.gemfile.lock index b0289ef7..ac4f4509 100644 --- a/gemfiles/ruby_2.7.6_minitest_5.gemfile.lock +++ b/gemfiles/ruby_2.7.6_minitest_5.gemfile.lock @@ -104,6 +104,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -127,6 +128,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_2.7.6_rspec_3.gemfile b/gemfiles/ruby_2.7.6_rspec_3.gemfile index d59f8d57..8c8e2b81 100644 --- a/gemfiles/ruby_2.7.6_rspec_3.gemfile +++ b/gemfiles/ruby_2.7.6_rspec_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec", "~> 3" gem "os" gem "climate_control" +gem "rspec", "~> 3" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_2.7.6_rspec_3.gemfile.lock b/gemfiles/ruby_2.7.6_rspec_3.gemfile.lock index 9c199be2..723237d3 100644 --- a/gemfiles/ruby_2.7.6_rspec_3.gemfile.lock +++ b/gemfiles/ruby_2.7.6_rspec_3.gemfile.lock @@ -103,6 +103,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -125,6 +126,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.0.4_cucumber_3.gemfile b/gemfiles/ruby_3.0.4_cucumber_3.gemfile index e333024b..be1f65fc 100644 --- a/gemfiles/ruby_3.0.4_cucumber_3.gemfile +++ b/gemfiles/ruby_3.0.4_cucumber_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock index 2b2bd47e..750d41ea 100644 --- a/gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_3.gemfile.lock @@ -124,6 +124,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -147,6 +148,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.0.4_cucumber_4.gemfile b/gemfiles/ruby_3.0.4_cucumber_4.gemfile index 118961f1..5e26524b 100644 --- a/gemfiles/ruby_3.0.4_cucumber_4.gemfile +++ b/gemfiles/ruby_3.0.4_cucumber_4.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock index 94af4aab..b755182f 100644 --- a/gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_4.gemfile.lock @@ -154,6 +154,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -179,6 +180,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.0.4_cucumber_5.gemfile b/gemfiles/ruby_3.0.4_cucumber_5.gemfile index 9050053c..bd99a42e 100644 --- a/gemfiles/ruby_3.0.4_cucumber_5.gemfile +++ b/gemfiles/ruby_3.0.4_cucumber_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock index 410cf587..6596bbd2 100644 --- a/gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_5.gemfile.lock @@ -154,6 +154,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -179,6 +180,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.0.4_cucumber_6.gemfile b/gemfiles/ruby_3.0.4_cucumber_6.gemfile index 75eb8323..9b5a94ad 100644 --- a/gemfiles/ruby_3.0.4_cucumber_6.gemfile +++ b/gemfiles/ruby_3.0.4_cucumber_6.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock index c3913afe..39434a0c 100644 --- a/gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_6.gemfile.lock @@ -158,6 +158,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -183,6 +184,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.0.4_cucumber_7.gemfile b/gemfiles/ruby_3.0.4_cucumber_7.gemfile index b425e539..310fd418 100644 --- a/gemfiles/ruby_3.0.4_cucumber_7.gemfile +++ b/gemfiles/ruby_3.0.4_cucumber_7.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock index 024cf47b..f83ffd71 100644 --- a/gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_7.gemfile.lock @@ -140,6 +140,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -163,6 +164,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.0.4_cucumber_8.gemfile b/gemfiles/ruby_3.0.4_cucumber_8.gemfile index 1f065ae6..9dcc7fc6 100644 --- a/gemfiles/ruby_3.0.4_cucumber_8.gemfile +++ b/gemfiles/ruby_3.0.4_cucumber_8.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock b/gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock index 4e62b81b..f9a7073c 100644 --- a/gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_3.0.4_cucumber_8.gemfile.lock @@ -134,6 +134,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -157,6 +158,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.0.4_minitest_5.gemfile b/gemfiles/ruby_3.0.4_minitest_5.gemfile index b1f7cdf8..d03bd893 100644 --- a/gemfiles/ruby_3.0.4_minitest_5.gemfile +++ b/gemfiles/ruby_3.0.4_minitest_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.0.4_minitest_5.gemfile.lock b/gemfiles/ruby_3.0.4_minitest_5.gemfile.lock index b0289ef7..ac4f4509 100644 --- a/gemfiles/ruby_3.0.4_minitest_5.gemfile.lock +++ b/gemfiles/ruby_3.0.4_minitest_5.gemfile.lock @@ -104,6 +104,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -127,6 +128,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.0.4_rspec_3.gemfile b/gemfiles/ruby_3.0.4_rspec_3.gemfile index d59f8d57..8c8e2b81 100644 --- a/gemfiles/ruby_3.0.4_rspec_3.gemfile +++ b/gemfiles/ruby_3.0.4_rspec_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec", "~> 3" gem "os" gem "climate_control" +gem "rspec", "~> 3" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.0.4_rspec_3.gemfile.lock b/gemfiles/ruby_3.0.4_rspec_3.gemfile.lock index 9c199be2..723237d3 100644 --- a/gemfiles/ruby_3.0.4_rspec_3.gemfile.lock +++ b/gemfiles/ruby_3.0.4_rspec_3.gemfile.lock @@ -103,6 +103,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -125,6 +126,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.1.2_cucumber_3.gemfile b/gemfiles/ruby_3.1.2_cucumber_3.gemfile index e333024b..be1f65fc 100644 --- a/gemfiles/ruby_3.1.2_cucumber_3.gemfile +++ b/gemfiles/ruby_3.1.2_cucumber_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock index 2b2bd47e..750d41ea 100644 --- a/gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_3.gemfile.lock @@ -124,6 +124,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -147,6 +148,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.1.2_cucumber_4.gemfile b/gemfiles/ruby_3.1.2_cucumber_4.gemfile index 118961f1..5e26524b 100644 --- a/gemfiles/ruby_3.1.2_cucumber_4.gemfile +++ b/gemfiles/ruby_3.1.2_cucumber_4.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock index 94af4aab..b755182f 100644 --- a/gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_4.gemfile.lock @@ -154,6 +154,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -179,6 +180,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.1.2_cucumber_5.gemfile b/gemfiles/ruby_3.1.2_cucumber_5.gemfile index 9050053c..bd99a42e 100644 --- a/gemfiles/ruby_3.1.2_cucumber_5.gemfile +++ b/gemfiles/ruby_3.1.2_cucumber_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock index 410cf587..6596bbd2 100644 --- a/gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_5.gemfile.lock @@ -154,6 +154,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -179,6 +180,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.1.2_cucumber_6.gemfile b/gemfiles/ruby_3.1.2_cucumber_6.gemfile index 75eb8323..9b5a94ad 100644 --- a/gemfiles/ruby_3.1.2_cucumber_6.gemfile +++ b/gemfiles/ruby_3.1.2_cucumber_6.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock index c3913afe..39434a0c 100644 --- a/gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_6.gemfile.lock @@ -158,6 +158,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -183,6 +184,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.1.2_cucumber_7.gemfile b/gemfiles/ruby_3.1.2_cucumber_7.gemfile index b425e539..310fd418 100644 --- a/gemfiles/ruby_3.1.2_cucumber_7.gemfile +++ b/gemfiles/ruby_3.1.2_cucumber_7.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock index 024cf47b..f83ffd71 100644 --- a/gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_7.gemfile.lock @@ -140,6 +140,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -163,6 +164,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.1.2_cucumber_8.gemfile b/gemfiles/ruby_3.1.2_cucumber_8.gemfile index 1f065ae6..9dcc7fc6 100644 --- a/gemfiles/ruby_3.1.2_cucumber_8.gemfile +++ b/gemfiles/ruby_3.1.2_cucumber_8.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock b/gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock index 4e62b81b..f9a7073c 100644 --- a/gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_3.1.2_cucumber_8.gemfile.lock @@ -134,6 +134,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -157,6 +158,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.1.2_minitest_5.gemfile b/gemfiles/ruby_3.1.2_minitest_5.gemfile index b1f7cdf8..d03bd893 100644 --- a/gemfiles/ruby_3.1.2_minitest_5.gemfile +++ b/gemfiles/ruby_3.1.2_minitest_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.1.2_minitest_5.gemfile.lock b/gemfiles/ruby_3.1.2_minitest_5.gemfile.lock index b0289ef7..ac4f4509 100644 --- a/gemfiles/ruby_3.1.2_minitest_5.gemfile.lock +++ b/gemfiles/ruby_3.1.2_minitest_5.gemfile.lock @@ -104,6 +104,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -127,6 +128,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.1.2_rspec_3.gemfile b/gemfiles/ruby_3.1.2_rspec_3.gemfile index d59f8d57..8c8e2b81 100644 --- a/gemfiles/ruby_3.1.2_rspec_3.gemfile +++ b/gemfiles/ruby_3.1.2_rspec_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec", "~> 3" gem "os" gem "climate_control" +gem "rspec", "~> 3" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.1.2_rspec_3.gemfile.lock b/gemfiles/ruby_3.1.2_rspec_3.gemfile.lock index 9c199be2..723237d3 100644 --- a/gemfiles/ruby_3.1.2_rspec_3.gemfile.lock +++ b/gemfiles/ruby_3.1.2_rspec_3.gemfile.lock @@ -103,6 +103,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -125,6 +126,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.2.0_cucumber_3.gemfile b/gemfiles/ruby_3.2.0_cucumber_3.gemfile index e333024b..be1f65fc 100644 --- a/gemfiles/ruby_3.2.0_cucumber_3.gemfile +++ b/gemfiles/ruby_3.2.0_cucumber_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock index 2b2bd47e..750d41ea 100644 --- a/gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_3.gemfile.lock @@ -124,6 +124,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -147,6 +148,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.2.0_cucumber_4.gemfile b/gemfiles/ruby_3.2.0_cucumber_4.gemfile index 118961f1..5e26524b 100644 --- a/gemfiles/ruby_3.2.0_cucumber_4.gemfile +++ b/gemfiles/ruby_3.2.0_cucumber_4.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock index 94af4aab..b755182f 100644 --- a/gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_4.gemfile.lock @@ -154,6 +154,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -179,6 +180,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.2.0_cucumber_5.gemfile b/gemfiles/ruby_3.2.0_cucumber_5.gemfile index 9050053c..bd99a42e 100644 --- a/gemfiles/ruby_3.2.0_cucumber_5.gemfile +++ b/gemfiles/ruby_3.2.0_cucumber_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock index 410cf587..6596bbd2 100644 --- a/gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_5.gemfile.lock @@ -154,6 +154,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -179,6 +180,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.2.0_cucumber_6.gemfile b/gemfiles/ruby_3.2.0_cucumber_6.gemfile index 75eb8323..9b5a94ad 100644 --- a/gemfiles/ruby_3.2.0_cucumber_6.gemfile +++ b/gemfiles/ruby_3.2.0_cucumber_6.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock index c3913afe..39434a0c 100644 --- a/gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_6.gemfile.lock @@ -158,6 +158,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -183,6 +184,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.2.0_cucumber_7.gemfile b/gemfiles/ruby_3.2.0_cucumber_7.gemfile index b425e539..310fd418 100644 --- a/gemfiles/ruby_3.2.0_cucumber_7.gemfile +++ b/gemfiles/ruby_3.2.0_cucumber_7.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock index 024cf47b..f83ffd71 100644 --- a/gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_7.gemfile.lock @@ -140,6 +140,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -163,6 +164,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.2.0_cucumber_8.gemfile b/gemfiles/ruby_3.2.0_cucumber_8.gemfile index 1f065ae6..9dcc7fc6 100644 --- a/gemfiles/ruby_3.2.0_cucumber_8.gemfile +++ b/gemfiles/ruby_3.2.0_cucumber_8.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock b/gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock index 4e62b81b..f9a7073c 100644 --- a/gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_3.2.0_cucumber_8.gemfile.lock @@ -134,6 +134,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -157,6 +158,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.2.0_minitest_5.gemfile b/gemfiles/ruby_3.2.0_minitest_5.gemfile index b1f7cdf8..d03bd893 100644 --- a/gemfiles/ruby_3.2.0_minitest_5.gemfile +++ b/gemfiles/ruby_3.2.0_minitest_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.2.0_minitest_5.gemfile.lock b/gemfiles/ruby_3.2.0_minitest_5.gemfile.lock index b0289ef7..ac4f4509 100644 --- a/gemfiles/ruby_3.2.0_minitest_5.gemfile.lock +++ b/gemfiles/ruby_3.2.0_minitest_5.gemfile.lock @@ -104,6 +104,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -127,6 +128,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.2.0_rspec_3.gemfile b/gemfiles/ruby_3.2.0_rspec_3.gemfile index d59f8d57..8c8e2b81 100644 --- a/gemfiles/ruby_3.2.0_rspec_3.gemfile +++ b/gemfiles/ruby_3.2.0_rspec_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec", "~> 3" gem "os" gem "climate_control" +gem "rspec", "~> 3" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.2.0_rspec_3.gemfile.lock b/gemfiles/ruby_3.2.0_rspec_3.gemfile.lock index 9c199be2..723237d3 100644 --- a/gemfiles/ruby_3.2.0_rspec_3.gemfile.lock +++ b/gemfiles/ruby_3.2.0_rspec_3.gemfile.lock @@ -103,6 +103,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -125,6 +126,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.3.0_cucumber_3.gemfile b/gemfiles/ruby_3.3.0_cucumber_3.gemfile index e333024b..be1f65fc 100644 --- a/gemfiles/ruby_3.3.0_cucumber_3.gemfile +++ b/gemfiles/ruby_3.3.0_cucumber_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock index 463070d0..8d5dfc8b 100644 --- a/gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_3.gemfile.lock @@ -124,6 +124,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -147,6 +148,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.3.0_cucumber_4.gemfile b/gemfiles/ruby_3.3.0_cucumber_4.gemfile index 118961f1..5e26524b 100644 --- a/gemfiles/ruby_3.3.0_cucumber_4.gemfile +++ b/gemfiles/ruby_3.3.0_cucumber_4.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock index df9628b9..29e4f448 100644 --- a/gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_4.gemfile.lock @@ -150,6 +150,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -175,6 +176,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.3.0_cucumber_5.gemfile b/gemfiles/ruby_3.3.0_cucumber_5.gemfile index 9050053c..bd99a42e 100644 --- a/gemfiles/ruby_3.3.0_cucumber_5.gemfile +++ b/gemfiles/ruby_3.3.0_cucumber_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock index edf29ca7..15141f9a 100644 --- a/gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_5.gemfile.lock @@ -154,6 +154,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -179,6 +180,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.3.0_cucumber_6.gemfile b/gemfiles/ruby_3.3.0_cucumber_6.gemfile index 75eb8323..9b5a94ad 100644 --- a/gemfiles/ruby_3.3.0_cucumber_6.gemfile +++ b/gemfiles/ruby_3.3.0_cucumber_6.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock index 1adf4431..3206d933 100644 --- a/gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_6.gemfile.lock @@ -158,6 +158,7 @@ GEM ffi (~> 1.1) thor (1.2.2) thread_safe (0.3.6) + timecop (0.9.8) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) @@ -183,6 +184,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.3.0_cucumber_7.gemfile b/gemfiles/ruby_3.3.0_cucumber_7.gemfile index b425e539..310fd418 100644 --- a/gemfiles/ruby_3.3.0_cucumber_7.gemfile +++ b/gemfiles/ruby_3.3.0_cucumber_7.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock index dc80381b..dea59a78 100644 --- a/gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_7.gemfile.lock @@ -140,6 +140,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -163,6 +164,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.3.0_cucumber_8.gemfile b/gemfiles/ruby_3.3.0_cucumber_8.gemfile index 1f065ae6..9dcc7fc6 100644 --- a/gemfiles/ruby_3.3.0_cucumber_8.gemfile +++ b/gemfiles/ruby_3.3.0_cucumber_8.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock b/gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock index 87fab104..35be2d1b 100644 --- a/gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock +++ b/gemfiles/ruby_3.3.0_cucumber_8.gemfile.lock @@ -134,6 +134,7 @@ GEM sys-uname (1.2.3) ffi (~> 1.1) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -157,6 +158,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.3.0_minitest_5.gemfile b/gemfiles/ruby_3.3.0_minitest_5.gemfile index b1f7cdf8..d03bd893 100644 --- a/gemfiles/ruby_3.3.0_minitest_5.gemfile +++ b/gemfiles/ruby_3.3.0_minitest_5.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec" gem "os" gem "climate_control" +gem "rspec" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.3.0_minitest_5.gemfile.lock b/gemfiles/ruby_3.3.0_minitest_5.gemfile.lock index b1437dee..4a7ed582 100644 --- a/gemfiles/ruby_3.3.0_minitest_5.gemfile.lock +++ b/gemfiles/ruby_3.3.0_minitest_5.gemfile.lock @@ -104,6 +104,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -127,6 +128,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/gemfiles/ruby_3.3.0_rspec_3.gemfile b/gemfiles/ruby_3.3.0_rspec_3.gemfile index d59f8d57..8c8e2b81 100644 --- a/gemfiles/ruby_3.3.0_rspec_3.gemfile +++ b/gemfiles/ruby_3.3.0_rspec_3.gemfile @@ -5,12 +5,13 @@ source "https://rubygems.org" gem "ddtrace" gem "pry" gem "rake" -gem "rspec", "~> 3" gem "os" gem "climate_control" +gem "rspec", "~> 3" gem "rspec-collection_matchers" gem "rspec_junit_formatter" gem "appraisal" +gem "timecop" gem "standard", "~> 1.31.0" gem "yard" gem "webrick" diff --git a/gemfiles/ruby_3.3.0_rspec_3.gemfile.lock b/gemfiles/ruby_3.3.0_rspec_3.gemfile.lock index 2fa89013..3a1a2252 100644 --- a/gemfiles/ruby_3.3.0_rspec_3.gemfile.lock +++ b/gemfiles/ruby_3.3.0_rspec_3.gemfile.lock @@ -103,6 +103,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.19.0) thor (1.2.2) + timecop (0.9.8) unicode-display_width (2.4.2) webrick (1.8.1) yard (0.9.34) @@ -125,6 +126,7 @@ DEPENDENCIES simplecov simplecov-cobertura (~> 2.1.0) standard (~> 1.31.0) + timecop webrick yard diff --git a/sig/datadog/ci/test_visibility/serializers/base.rbs b/sig/datadog/ci/test_visibility/serializers/base.rbs index 3257c2f3..c63e51ab 100644 --- a/sig/datadog/ci/test_visibility/serializers/base.rbs +++ b/sig/datadog/ci/test_visibility/serializers/base.rbs @@ -48,7 +48,7 @@ module Datadog def write_field: (untyped packer, String field_name, ?String? method) -> untyped def time_nano: (Time time) -> Integer - def duration_nano: (Integer duration) -> Integer + def duration_nano: (Float duration) -> Integer def content_fields_count: () -> Integer end diff --git a/spec/datadog/ci/test_visibility/serializers/span_spec.rb b/spec/datadog/ci/test_visibility/serializers/span_spec.rb index 97cd6e71..57abf816 100644 --- a/spec/datadog/ci/test_visibility/serializers/span_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/span_spec.rb @@ -38,6 +38,7 @@ "resource" => "http-call" } ) + expect(content).to include("start", "duration") expect(meta).to include( { @@ -46,10 +47,6 @@ } ) expect(metrics).to eq({"_dd.top_level" => 1.0, "custom_metric" => 42}) - # TODO: test start and duration with timecop - # expect(content["start"]).to eq(1) - # expect(content["duration"]).to eq(1) - # end end end diff --git a/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb index 7dc952ba..aa2ffaed 100644 --- a/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb @@ -41,10 +41,6 @@ expect(metrics).to eq( {"_dd.measured" => 1.0, "_dd.top_level" => 1.0, "memory_allocations" => 16} ) - # TODO: test start and duration with timecop - # expect(content["start"]).to eq(1) - # expect(content["duration"]).to eq(1) - # end end @@ -60,5 +56,21 @@ expect(meta).to include({"test.status" => "fail"}) end end + + context "with time and duration expectations" do + let(:start_time) { Time.now } + let(:duration_seconds) { 3 } + + before do + produce_test_trace(start_time: start_time, duration_seconds: duration_seconds) + end + + it "correctly serializes start and duration in nanoseconds" do + expect(content).to include({ + "start" => start_time.to_i * 1_000_000_000 + start_time.nsec, + "duration" => 3 * 1_000_000_000 + }) + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bb5261d7..f566167d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,6 +20,7 @@ require "rspec/collection_matchers" require "climate_control" +require "timecop" if defined?(Warning.ignore) # Caused by https://github.com/cucumber/cucumber-ruby/blob/47c8e2d7c97beae8541c895a43f9ccb96324f0f1/lib/cucumber/encoding.rb#L5-L6 diff --git a/spec/support/tracer_helpers.rb b/spec/support/tracer_helpers.rb index 2c9707d8..71583c6c 100644 --- a/spec/support/tracer_helpers.rb +++ b/spec/support/tracer_helpers.rb @@ -12,8 +12,16 @@ def produce_test_trace( framework: "rspec", operation: "rspec.example", test_name: "test_add", test_suite: "calculator_tests", service: "rspec-test-suite", result: "PASSED", exception: nil, + start_time: Time.now, duration_seconds: 2, with_http_span: false ) + # each time monotonic clock is called it will return a number that is + # by `duration_seconds` bigger than the previous + allow(Process).to receive(:clock_gettime).and_return( + 0, duration_seconds, 2 * duration_seconds, 3 * duration_seconds + ) + Timecop.freeze(start_time) + Datadog::CI::Recorder.trace( operation, { @@ -31,7 +39,7 @@ def produce_test_trace( if with_http_span Datadog::Tracing.trace("http-call", type: "http", service: "net-http") do |span, trace| span.set_tag("custom_tag", "custom_tag_value") - span.set_tag("custom_metric", 42) + span.set_metric("custom_metric", 42) end end @@ -46,7 +54,11 @@ def produce_test_trace( else Datadog::CI::Recorder.passed!(span) end + + Timecop.travel(start_time + duration_seconds) end + + Timecop.return end def first_test_span diff --git a/vendor/rbs/ddtrace/0/datadog/tracing/span.rbs b/vendor/rbs/ddtrace/0/datadog/tracing/span.rbs index 0d749106..b8bb2171 100644 --- a/vendor/rbs/ddtrace/0/datadog/tracing/span.rbs +++ b/vendor/rbs/ddtrace/0/datadog/tracing/span.rbs @@ -15,7 +15,7 @@ module Datadog def status: () -> Integer def start_time: () -> Time def end_time: () -> Time - def duration: () -> Integer + def duration: () -> Float def meta: () -> Hash[String, untyped] def metrics: () -> Hash[String, untyped] end diff --git a/vendor/rbs/ddtrace/0/datadog/tracing/span_operation.rbs b/vendor/rbs/ddtrace/0/datadog/tracing/span_operation.rbs index 11f3c61d..ff5277fb 100644 --- a/vendor/rbs/ddtrace/0/datadog/tracing/span_operation.rbs +++ b/vendor/rbs/ddtrace/0/datadog/tracing/span_operation.rbs @@ -54,7 +54,7 @@ module Datadog def finished?: () -> untyped - def duration: () -> untyped + def duration: () -> Float def set_error: (untyped e) -> untyped From 6d3358cb535c819134a3f6a14a0d366748fe02a9 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 21 Sep 2023 14:49:20 +0200 Subject: [PATCH 20/45] remove api_key configuration option as it already exists in ddtrace core config --- lib/datadog/ci/configuration/settings.rb | 5 --- lib/datadog/ci/ext/settings.rb | 1 - .../datadog/ci/configuration/settings_spec.rb | 39 ------------------- 3 files changed, 45 deletions(-) diff --git a/lib/datadog/ci/configuration/settings.rb b/lib/datadog/ci/configuration/settings.rb index 11f01c04..bf967ca4 100644 --- a/lib/datadog/ci/configuration/settings.rb +++ b/lib/datadog/ci/configuration/settings.rb @@ -29,11 +29,6 @@ def self.add_settings!(base) o.default false end - option :api_key do |o| - o.type :string, nilable: true - o.env CI::Ext::Settings::ENV_API_KEY - end - define_method(:instrument) do |integration_name, options = {}, &block| return unless enabled diff --git a/lib/datadog/ci/ext/settings.rb b/lib/datadog/ci/ext/settings.rb index 8907b6b6..2de83aad 100644 --- a/lib/datadog/ci/ext/settings.rb +++ b/lib/datadog/ci/ext/settings.rb @@ -7,7 +7,6 @@ module Ext module Settings ENV_MODE_ENABLED = "DD_TRACE_CI_ENABLED" ENV_AGENTLESS_MODE_ENABLED = "DD_CIVISIBILITY_AGENTLESS_ENABLED" - ENV_API_KEY = "DD_API_KEY" end end end diff --git a/spec/datadog/ci/configuration/settings_spec.rb b/spec/datadog/ci/configuration/settings_spec.rb index 3b50516a..8f6b5152 100644 --- a/spec/datadog/ci/configuration/settings_spec.rb +++ b/spec/datadog/ci/configuration/settings_spec.rb @@ -137,45 +137,6 @@ def patcher end end - describe "#api_key" do - subject(:api_key) { settings.ci.api_key } - - context "when #{Datadog::CI::Ext::Settings::ENV_API_KEY}" do - around do |example| - ClimateControl.modify(Datadog::CI::Ext::Settings::ENV_API_KEY => api_key) do - example.run - end - end - - context "is not defined" do - let(:api_key) { nil } - - it { is_expected.to be nil } - end - - context "is set to dd_api_key" do - let(:api_key) { "dd_api_key" } - - it { is_expected.to eq("dd_api_key") } - end - end - end - - describe "#api_key=" do - around do |example| - ClimateControl.modify(Datadog::CI::Ext::Settings::ENV_API_KEY => nil) do - example.run - end - end - - it "updates the #api_key setting" do - expect { settings.ci.api_key = "dd_api_key" } - .to change { settings.ci.api_key } - .from(nil) - .to("dd_api_key") - end - end - describe "#instrument" do let(:integration_name) { :fake } From cde9e0d69c7d9bba6a14381856a5985bbe0d77d6 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 21 Sep 2023 16:56:09 +0200 Subject: [PATCH 21/45] add http transport --- lib/datadog/ci/ext/transport.rb | 17 + lib/datadog/ci/test_visibility/transport.rb | 4 + lib/datadog/ci/transport/http.rb | 134 ++++++++ sig/datadog/ci/ext/transport.rbs | 17 + sig/datadog/ci/transport/http.rbs | 60 ++++ spec/datadog/ci/transport/http_spec.rb | 329 ++++++++++++++++++++ 6 files changed, 561 insertions(+) create mode 100644 lib/datadog/ci/ext/transport.rb create mode 100644 lib/datadog/ci/transport/http.rb create mode 100644 sig/datadog/ci/ext/transport.rbs create mode 100644 sig/datadog/ci/transport/http.rbs create mode 100644 spec/datadog/ci/transport/http_spec.rb diff --git a/lib/datadog/ci/ext/transport.rb b/lib/datadog/ci/ext/transport.rb new file mode 100644 index 00000000..c78cdd40 --- /dev/null +++ b/lib/datadog/ci/ext/transport.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Datadog + module CI + module Ext + module Trasnport + HEADER_DD_API_KEY = "DD-API-KEY" + HEADER_CONTENT_TYPE = "Content-Type" + + TEST_VISIBILITY_INTAKE_HOST_PREFIX = "citestcycle-intake" + TEST_VISIBILITY_INTAKE_PATH = "api/v2/citestcycle" + + CONTENT_TYPE_MESSAGEPACK = "application/msgpack" + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 2dc1af3c..79d5c606 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -17,6 +17,10 @@ class Transport # events = traces.flat_map { |trace| SomethingThatConvertsTraces.convert(trace) } # payload = Payload.new(events) # # @encoder.encode(payload) + # + # url: https://citestcycle-intake.datadoghq.com + # url: {ssl=true}citestcycle-intake.#{Datadog.configuration.site || "datadoghq.com"} + # api_key: Datadog.configuration.api_key # end # private diff --git a/lib/datadog/ci/transport/http.rb b/lib/datadog/ci/transport/http.rb new file mode 100644 index 00000000..ba8d3bc9 --- /dev/null +++ b/lib/datadog/ci/transport/http.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require "net/http" + +module Datadog + module CI + module Transport + class HTTP + attr_reader \ + :host, + :port, + :ssl, + :timeout + + DEFAULT_TIMEOUT = 30 + + def initialize(host:, timeout: DEFAULT_TIMEOUT, port: nil, ssl: true) + @host = host + @port = port + @timeout = timeout + @ssl = ssl.nil? ? true : ssl + end + + def request(path:, payload:, headers:, method: "post") + raise "Unknown method #{method}" unless respond_to?(method, true) + + send(method, path: path, payload: payload, headers: headers) + end + + private + + def open(&block) + req = ::Net::HTTP.new(@host, @port) + + req.use_ssl = @ssl + req.open_timeout = req.read_timeout = @timeout + + req.start(&block) + end + + def post(path:, headers:, payload:) + post = ::Net::HTTP::Post.new(path, headers) + post.body = payload + + http_response = open do |http| + http.request(post) + end + + Response.new(http_response) + rescue => e + Datadog.logger.debug("Unable to send events: #{e}") + + InternalErrorResponse.new(e) + end + + # Data structure for an HTTP Response + class Response + attr_reader :http_response + + def initialize(http_response) + @http_response = http_response + end + + def payload + http_response.body + end + + def code + http_response.code.to_i + end + + def ok? + code.between?(200, 299) + end + + def unsupported? + code == 415 + end + + def not_found? + code == 404 + end + + def client_error? + code.between?(400, 499) + end + + def server_error? + code.between?(500, 599) + end + + def internal_error? + false + end + + def inspect + "#{self.class} ok?:#{ok?} unsupported?:#{unsupported?}, " \ + "not_found?:#{not_found?}, client_error?:#{client_error?}, " \ + "server_error?:#{server_error?}, internal_error?:#{internal_error?}, " \ + "payload:#{payload}" + end + end + + class InternalErrorResponse < Response + class DummyNetHTTPResponse + def body + "" + end + + def code + "-1" + end + end + + attr_reader :error + + def initialize(error) + super(DummyNetHTTPResponse.new) + + @error = error + end + + def internal_error? + true + end + + def inspect + "#{super}, error_class:#{error.class}, error:#{error}" + end + end + end + end + end +end diff --git a/sig/datadog/ci/ext/transport.rbs b/sig/datadog/ci/ext/transport.rbs new file mode 100644 index 00000000..ddd21d8c --- /dev/null +++ b/sig/datadog/ci/ext/transport.rbs @@ -0,0 +1,17 @@ +module Datadog + module CI + module Ext + module Trasnport + HEADER_DD_API_KEY: "DD-API-KEY" + + HEADER_CONTENT_TYPE: "Content-Type" + + TEST_VISIBILITY_INTAKE_HOST_PREFIX: "citestcycle-intake" + + TEST_VISIBILITY_INTAKE_PATH: "api/v2/citestcycle" + + CONTENT_TYPE_MESSAGEPACK: "application/msgpack" + end + end + end +end diff --git a/sig/datadog/ci/transport/http.rbs b/sig/datadog/ci/transport/http.rbs new file mode 100644 index 00000000..dc211264 --- /dev/null +++ b/sig/datadog/ci/transport/http.rbs @@ -0,0 +1,60 @@ +module Datadog + module CI + module Transport + class HTTP + attr_reader host: String + attr_reader port: Integer? + attr_reader ssl: bool + attr_reader timeout: Integer + + DEFAULT_TIMEOUT: 30 + + def initialize: (host: String, ?port: Integer?, ?ssl: bool, ?timeout: Integer) -> void + + def request: (?method: String, payload: String, headers: Hash[String, String], path: String) -> Response + + private + + def open: () { (::Net::HTTP) -> Net::HTTPResponse } -> Net::HTTPResponse + + def post: (payload: String, headers: Hash[String, String], path: String) -> Response + + class Response + attr_reader http_response: (Net::HTTPResponse | InternalErrorResponse::DummyNetHTTPResponse) + + def initialize: ((Net::HTTPResponse | InternalErrorResponse::DummyNetHTTPResponse) http_response) -> void + + def payload: () -> String + + def code: () -> Integer + + def ok?: () -> bool + + def unsupported?: () -> bool + + def not_found?: () -> bool + + def client_error?: () -> bool + + def server_error?: () -> bool + + def internal_error?: () -> bool + + def inspect: () -> ::String + end + + class InternalErrorResponse < Response + class DummyNetHTTPResponse + def body: () -> "" + def code: () -> "-1" + end + + attr_reader error: StandardError + @error: StandardError + + def initialize: (StandardError error) -> void + end + end + end + end +end diff --git a/spec/datadog/ci/transport/http_spec.rb b/spec/datadog/ci/transport/http_spec.rb new file mode 100644 index 00000000..91749508 --- /dev/null +++ b/spec/datadog/ci/transport/http_spec.rb @@ -0,0 +1,329 @@ +require_relative "../../../../lib/datadog/ci/transport/http" + +RSpec.describe Datadog::CI::Transport::HTTP do + subject(:transport) { described_class.new(host: host, **options) } + + let(:host) { "datadog-host" } + let(:port) { 8132 } + let(:timeout) { 10 } + let(:ssl) { true } + let(:options) { {} } + + shared_context "HTTP connection stub" do + let(:http_connection) { instance_double(::Net::HTTP) } + + before do + allow(::Net::HTTP).to receive(:new) + .with( + transport.host, + transport.port + ).and_return(http_connection) + + allow(http_connection).to receive(:open_timeout=).with(transport.timeout) + allow(http_connection).to receive(:read_timeout=).with(transport.timeout) + allow(http_connection).to receive(:use_ssl=).with(transport.ssl) + + allow(http_connection).to receive(:start).and_yield(http_connection) + end + end + + describe "#initialize" do + context "given no options" do + let(:options) { {} } + + it do + is_expected.to have_attributes( + host: host, + port: nil, + timeout: Datadog::CI::Transport::HTTP::DEFAULT_TIMEOUT, + ssl: true + ) + end + end + + context "given a :port option" do + let(:options) { {timeout: port} } + + it { is_expected.to have_attributes(timeout: port) } + end + + context "given a :timeout option" do + let(:options) { {timeout: timeout} } + + it { is_expected.to have_attributes(timeout: timeout) } + end + + context "given a :ssl option" do + let(:options) { {ssl: ssl} } + + context "with nil" do + let(:ssl) { nil } + + it { is_expected.to have_attributes(ssl: true) } + end + + context "with false" do + let(:ssl) { false } + + it { is_expected.to have_attributes(ssl: false) } + end + end + end + + describe "#request" do + include_context "HTTP connection stub" + + let(:path) { "/api/v1/intake" } + let(:payload) { '{ "key": "value" }' } + let(:headers) { {"Content-Type" => "application/json"} } + let(:request_options) { {} } + + let(:http_response) { double("http_response") } + + subject(:request) { transport.request(path: path, payload: payload, headers: headers, **request_options) } + + context "when request is successful" do + before { expect(http_connection).to receive(:request).and_return(http_response) } + + it "produces a response" do + is_expected.to be_a_kind_of(described_class::Response) + + expect(request.http_response).to be(http_response) + end + end + + context "when error in connecting to server" do + before { expect(http_connection).to receive(:request).and_raise(StandardError) } + + it { expect(request).to be_a_kind_of(described_class::InternalErrorResponse) } + end + + context "when method is unknown" do + let(:request_options) { {method: "delete"} } + + it { expect { request }.to raise_error("Unknown method delete") } + end + end +end + +RSpec.describe Datadog::CI::Transport::HTTP::Response do + subject(:response) { described_class.new(http_response) } + + let(:http_response) { instance_double(::Net::HTTPResponse) } + + describe "#initialize" do + it { is_expected.to have_attributes(http_response: http_response) } + end + + describe "#payload" do + subject(:payload) { response.payload } + + let(:http_response) { instance_double(::Net::HTTPResponse, body: double("body")) } + + it { is_expected.to be(http_response.body) } + end + + describe "#code" do + subject(:code) { response.code } + + let(:http_response) { instance_double(::Net::HTTPResponse, code: "200") } + + it { is_expected.to eq(200) } + end + + describe "#ok?" do + subject(:ok?) { response.ok? } + + let(:http_response) { instance_double(::Net::HTTPResponse, code: code) } + + context "when code not 2xx" do + let(:code) { 199 } + + it { is_expected.to be false } + end + + context "when code is 200" do + let(:code) { 200 } + + it { is_expected.to be true } + end + + context "when code is 299" do + let(:code) { 299 } + + it { is_expected.to be true } + end + + context "when code is greater than 299" do + let(:code) { 300 } + + it { is_expected.to be false } + end + end + + describe "#unsupported?" do + subject(:unsupported?) { response.unsupported? } + + let(:http_response) { instance_double(::Net::HTTPResponse, code: code) } + + context "when code is 400" do + let(:code) { 400 } + + it { is_expected.to be false } + end + + context "when code is 415" do + let(:code) { 415 } + + it { is_expected.to be true } + end + end + + describe "#not_found?" do + subject(:not_found?) { response.not_found? } + + let(:http_response) { instance_double(::Net::HTTPResponse, code: code) } + + context "when code is 400" do + let(:code) { 400 } + + it { is_expected.to be false } + end + + context "when code is 404" do + let(:code) { 404 } + + it { is_expected.to be true } + end + end + + describe "#client_error?" do + subject(:client_error?) { response.client_error? } + + let(:http_response) { instance_double(::Net::HTTPResponse, code: code) } + + context "when code is 399" do + let(:code) { 399 } + + it { is_expected.to be false } + end + + context "when code is 400" do + let(:code) { 400 } + + it { is_expected.to be true } + end + + context "when code is 499" do + let(:code) { 499 } + + it { is_expected.to be true } + end + + context "when code is 500" do + let(:code) { 500 } + + it { is_expected.to be false } + end + end + + describe "#server_error?" do + subject(:server_error?) { response.server_error? } + + let(:http_response) { instance_double(::Net::HTTPResponse, code: code) } + + context "when code is 499" do + let(:code) { 499 } + + it { is_expected.to be false } + end + + context "when code is 500" do + let(:code) { 500 } + + it { is_expected.to be true } + end + + context "when code is 599" do + let(:code) { 599 } + + it { is_expected.to be true } + end + + context "when code is 600" do + let(:code) { 600 } + + it { is_expected.to be false } + end + end + + describe "#internal_error?" do + subject(:internal_error?) { response.internal_error? } + + it { is_expected.to be false } + end +end + +RSpec.describe Datadog::CI::Transport::HTTP::InternalErrorResponse do + subject(:response) { described_class.new(error) } + + let(:error) { instance_double(StandardError, class: "StandardError", to_s: "error message") } + + describe "#initialize" do + it { is_expected.to have_attributes(error: error) } + end + + describe "#payload" do + subject(:payload) { response.payload } + + it { is_expected.to eq("") } + end + + describe "#code" do + subject(:code) { response.code } + + it { is_expected.to eq(-1) } + end + + describe "#ok?" do + subject(:ok?) { response.ok? } + + it { is_expected.to be false } + end + + describe "#unsupported?" do + subject(:unsupported?) { response.unsupported? } + + it { is_expected.to be false } + end + + describe "#not_found?" do + subject(:not_found?) { response.not_found? } + + it { is_expected.to be false } + end + + describe "#client_error?" do + subject(:client_error?) { response.client_error? } + + it { is_expected.to be false } + end + + describe "#server_error?" do + subject(:server_error?) { response.server_error? } + + it { is_expected.to be false } + end + + describe "#internal_error?" do + subject(:internal_error?) { response.internal_error? } + + it { is_expected.to be true } + end + + describe "#inspect" do + subject(:inspect) { response.inspect } + + it { is_expected.to include("error_class:StandardError, error:error message") } + end +end From 7faa73eae27da8362bcbd16011f8e9e8b10b0eec Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 22 Sep 2023 12:48:32 +0200 Subject: [PATCH 22/45] test visibility transport - first stab --- lib/datadog/ci/ext/transport.rb | 2 +- lib/datadog/ci/test_visibility/transport.rb | 119 +++++++++++------- sig/datadog/ci/ext/transport.rbs | 2 +- sig/datadog/ci/test_visibility/transport.rbs | 18 +++ .../rbs/ddtrace/0/datadog/core/encoding.rbs | 9 ++ vendor/rbs/msgpack/0/message_pack/packer.rbs | 2 +- 6 files changed, 103 insertions(+), 49 deletions(-) create mode 100644 vendor/rbs/ddtrace/0/datadog/core/encoding.rbs diff --git a/lib/datadog/ci/ext/transport.rb b/lib/datadog/ci/ext/transport.rb index c78cdd40..9fa3cef6 100644 --- a/lib/datadog/ci/ext/transport.rb +++ b/lib/datadog/ci/ext/transport.rb @@ -3,7 +3,7 @@ module Datadog module CI module Ext - module Trasnport + module Transport HEADER_DD_API_KEY = "DD-API-KEY" HEADER_CONTENT_TYPE = "Content-Type" diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 79d5c606..d15ae801 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -4,56 +4,83 @@ # use it to chunk payloads by size # require "datadog/core/chunker" +require_relative "serializers" + module Datadog module CI module TestVisibility class Transport - # def initialize - # @encoder = Datadog::Core::Encoding::MsgpackEncoder - # end - - # def send_traces(traces) - # # convert traces to events and construct payload - # events = traces.flat_map { |trace| SomethingThatConvertsTraces.convert(trace) } - # payload = Payload.new(events) - # # @encoder.encode(payload) - # - # url: https://citestcycle-intake.datadoghq.com - # url: {ssl=true}citestcycle-intake.#{Datadog.configuration.site || "datadoghq.com"} - # api_key: Datadog.configuration.api_key - # end - - # private - - # # represents payload with some subset of serializable events to be sent to CI-APP intake - # class Payload - # def initialize(events) - # @events = events - # end - - # def to_msgpack(packer) - # packer ||= MessagePack::Packer.new - - # packer.write_map_header(3) # Set header with how many elements in the map - # packer.write("version") - # packer.write(1) - - # packer.write("metadata") - # packer.write_map_header(3) - - # packer.write("runtime-id") - # packer.write(@events.first.runtime_id) - - # packer.write("language") - # packer.write("ruby") - - # packer.write("library_version") - # packer.write(Datadog::CI::VERSION::STRING) - - # packer.write_array_header(@events.size) - # packer.write(@events) - # end - # end + # TODO: rename Serializers module + def initialize(api_key:, site: "datadoghq.com", serializer: Datadog::CI::TestVisibility::Serializers) + @serializer = serializer + @api_key = api_key + @http = Datadog::CI::Transport::HTTP.new( + host: "#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{site}" + ) + end + + def send_traces(traces) + # convert traces to events and construct payload + # TODO: encode events immediately? + # TODO: batch events in order not to load a lot of them in memory at once + events = traces.flat_map { |trace| @serializer.convert_trace_to_serializable_events(trace) } + + payload = Payload.new(events) + + encoded_payload = encoder.encode(payload) + + response = @http.request( + path: Datadog::CI::Ext::Transport::TEST_VISIBILITY_INTAKE_PATH, + payload: encoded_payload, + headers: { + Ext::Transport::HEADER_DD_API_KEY => @api_key, + Ext::Transport::HEADER_CONTENT_TYPE => Ext::Transport::CONTENT_TYPE_MESSAGEPACK + } + ) + + # Tracing writers expect an array of responses + [response] + end + + private + + def encoder + Datadog::Core::Encoding::MsgpackEncoder + end + + # represents payload with some subset of serializable events to be sent to CI-APP intake + class Payload + def initialize(events) + @events = events + end + + def to_msgpack(packer) + packer ||= MessagePack::Packer.new + + packer.write_map_header(3) # Set header with how many elements in the map + packer.write("version") + packer.write(1) + + packer.write("metadata") + packer.write_map_header(3) + + # TODO: implement our own identity? + first_event = @events.first + if first_event + packer.write("runtime-id") + packer.write(first_event.runtime_id) + end + + packer.write("language") + packer.write("ruby") + + packer.write("library_version") + packer.write(Datadog::CI::VERSION::STRING) + + packer.write_array_header(@events.size) + packer.write(@events) + end + end end end end diff --git a/sig/datadog/ci/ext/transport.rbs b/sig/datadog/ci/ext/transport.rbs index ddd21d8c..9a25cdbc 100644 --- a/sig/datadog/ci/ext/transport.rbs +++ b/sig/datadog/ci/ext/transport.rbs @@ -1,7 +1,7 @@ module Datadog module CI module Ext - module Trasnport + module Transport HEADER_DD_API_KEY: "DD-API-KEY" HEADER_CONTENT_TYPE: "Content-Type" diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs index f7df3bc7..73f4cb15 100644 --- a/sig/datadog/ci/test_visibility/transport.rbs +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -2,6 +2,24 @@ module Datadog module CI module TestVisibility class Transport + @api_key: String + @http: Datadog::CI::Transport::HTTP + @serializer: singleton(Datadog::CI::TestVisibility::Serializers) + + def initialize: (api_key: String, ?site: ::String, ?serializer: singleton(Datadog::CI::TestVisibility::Serializers)) -> void + + def send_traces: (Array[Datadog::Tracing::TraceSegment] traces) -> ::Array[Datadog::CI::Transport::HTTP::Response] + + private + + def encoder: () -> singleton(Datadog::Core::Encoding::MsgpackEncoder) + + class Payload + @events: Array[Datadog::CI::TestVisibility::Serializers::Base] + + def initialize: (Array[Datadog::CI::TestVisibility::Serializers::Base] events) -> void + def to_msgpack: (untyped packer) -> untyped + end end end end diff --git a/vendor/rbs/ddtrace/0/datadog/core/encoding.rbs b/vendor/rbs/ddtrace/0/datadog/core/encoding.rbs new file mode 100644 index 00000000..63ec5d88 --- /dev/null +++ b/vendor/rbs/ddtrace/0/datadog/core/encoding.rbs @@ -0,0 +1,9 @@ +module Datadog + module Core + module Encoding + module MsgpackEncoder + def self.encode: (untyped payload) -> String + end + end + end +end diff --git a/vendor/rbs/msgpack/0/message_pack/packer.rbs b/vendor/rbs/msgpack/0/message_pack/packer.rbs index 7d1419dc..048169f1 100644 --- a/vendor/rbs/msgpack/0/message_pack/packer.rbs +++ b/vendor/rbs/msgpack/0/message_pack/packer.rbs @@ -2,7 +2,7 @@ module MessagePack class Packer def initialize: () -> void - def write: (String input) -> self + def write: (Object input) -> self def write_map_header: (Integer keys_number) -> self def write_array_header: (Integer keys_number) -> self end From 64be17c538d885cf9015d2dbdc7652c858cf665e Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 22 Sep 2023 13:18:56 +0200 Subject: [PATCH 23/45] construct agentless transport in CI component --- lib/datadog/ci/configuration/components.rb | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index aafad49f..edb4ca32 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -16,6 +16,38 @@ def initialize(settings) end def activate_ci!(settings) + agentless_transport = nil + writer_options = settings.ci.writer_options + + if settings.ci.agentless_mode_enabled + if settings.api_key.nil? + # agentless mode is requested but no API key is provided - + # we cannot continue and log an error + # Tests are running without CI visibility enabled + + Datadog.logger.error( + "Agentless mode was enabled but DD_API_KEY is not set: CI visibility is disabled. " \ + "Please make sure to set valid api key in DD_API_KEY environment variable" + ) + + settings.ci.enabled = false + return + else + agentless_transport = Datadog::CI::TestVisibility::Transport.new(api_key: settings.api_key) + end + end + + writer_options[:transport] = agentless_transport if agentless_transport + + # Deactivate telemetry + settings.telemetry.enabled = false + + # Deactivate remote configuration + settings.remote.enabled = false + + # enable tracing + settings.tracing.enabled = true + # Activate underlying tracing test mode settings.tracing.test_mode.enabled = true From abc35e087d8b1b1a63e518a808c457c958723936 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 22 Sep 2023 13:28:14 +0200 Subject: [PATCH 24/45] make content fields and size calculation constants to save time and memory --- .../ci/test_visibility/serializers/base.rb | 30 +++++++++---------- .../ci/test_visibility/serializers/span.rb | 22 +++++++++----- .../ci/test_visibility/serializers/test_v1.rb | 22 +++++++++----- .../ci/test_visibility/serializers/base.rbs | 3 ++ .../ci/test_visibility/serializers/span.rbs | 6 ++-- .../test_visibility/serializers/test_v1.rbs | 5 +++- 6 files changed, 56 insertions(+), 32 deletions(-) diff --git a/lib/datadog/ci/test_visibility/serializers/base.rb b/lib/datadog/ci/test_visibility/serializers/base.rb index ee171d4d..4b6513d5 100644 --- a/lib/datadog/ci/test_visibility/serializers/base.rb +++ b/lib/datadog/ci/test_visibility/serializers/base.rb @@ -21,7 +21,7 @@ def to_msgpack(packer = nil) write_field(packer, "version") packer.write("content") - packer.write_map_header(content_fields_count) + packer.write_map_header(content_map_size) content_fields.each do |field| if field.is_a?(Hash) @@ -38,6 +38,10 @@ def content_fields [] end + def content_map_size + 0 + end + def runtime_id @trace.runtime_id end @@ -97,6 +101,16 @@ def error @span.status end + def self.calculate_content_map_size(fields_list) + fields_list.reduce(0) do |size, field| + if field.is_a?(Hash) + size + field.size + else + size + 1 + end + end + end + private def write_field(packer, field_name, method = nil) @@ -117,20 +131,6 @@ def time_nano(time) def duration_nano(duration) (duration * 1e9).to_i end - - def content_fields_count - return @content_fields_count if defined?(@content_fields_count) - - res = 0 - content_fields.each do |field| - res += if field.is_a?(Hash) - field.size - else - 1 - end - end - @content_fields_count = res - end end end end diff --git a/lib/datadog/ci/test_visibility/serializers/span.rb b/lib/datadog/ci/test_visibility/serializers/span.rb index 9906c8a6..83f9b635 100644 --- a/lib/datadog/ci/test_visibility/serializers/span.rb +++ b/lib/datadog/ci/test_visibility/serializers/span.rb @@ -7,14 +7,22 @@ module CI module TestVisibility module Serializers class Span < Base + CONTENT_FIELDS = [ + "trace_id", "span_id", "parent_id", + "name", "resource", "service", + "error", "start", "duration", + "meta", "metrics", + "type" => "span_type" + ].freeze + + CONTENT_MAP_SIZE = calculate_content_map_size(CONTENT_FIELDS) + def content_fields - @content_fields ||= [ - "trace_id", "span_id", "parent_id", - "name", "resource", "service", - "error", "start", "duration", - "meta", "metrics", - "type" => "span_type" - ] + CONTENT_FIELDS + end + + def content_map_size + CONTENT_MAP_SIZE end def type diff --git a/lib/datadog/ci/test_visibility/serializers/test_v1.rb b/lib/datadog/ci/test_visibility/serializers/test_v1.rb index 1c3684c9..d8c8d5bb 100644 --- a/lib/datadog/ci/test_visibility/serializers/test_v1.rb +++ b/lib/datadog/ci/test_visibility/serializers/test_v1.rb @@ -8,14 +8,22 @@ module CI module TestVisibility module Serializers class TestV1 < Base + CONTENT_FIELDS = [ + "trace_id", "span_id", + "name", "resource", "service", + "error", "start", "duration", + "meta", "metrics", + "type" => "span_type" + ].freeze + + CONTENT_MAP_SIZE = calculate_content_map_size(CONTENT_FIELDS) + def content_fields - @content_fields ||= [ - "trace_id", "span_id", - "name", "resource", "service", - "error", "start", "duration", - "meta", "metrics", - "type" => "span_type" - ] + CONTENT_FIELDS + end + + def content_map_size + CONTENT_MAP_SIZE end def type diff --git a/sig/datadog/ci/test_visibility/serializers/base.rbs b/sig/datadog/ci/test_visibility/serializers/base.rbs index c63e51ab..04050d25 100644 --- a/sig/datadog/ci/test_visibility/serializers/base.rbs +++ b/sig/datadog/ci/test_visibility/serializers/base.rbs @@ -13,6 +13,7 @@ module Datadog def to_msgpack: (?untyped? packer) -> untyped def content_fields: () -> ::Array[String | Hash[String, String]] + def content_map_size: () -> Integer def runtime_id: () -> String @@ -44,6 +45,8 @@ module Datadog def error: () -> Integer + def self.calculate_content_map_size: (Array[String | Hash[String, String]] fields_list) -> Integer + private def write_field: (untyped packer, String field_name, ?String? method) -> untyped diff --git a/sig/datadog/ci/test_visibility/serializers/span.rbs b/sig/datadog/ci/test_visibility/serializers/span.rbs index 1d18eec0..52184d92 100644 --- a/sig/datadog/ci/test_visibility/serializers/span.rbs +++ b/sig/datadog/ci/test_visibility/serializers/span.rbs @@ -3,9 +3,11 @@ module Datadog module TestVisibility module Serializers class Span < Base - @content_fields: Array[String | Hash[String, String]] - def content_fields: () -> Array[String | Hash[String, String]] + CONTENT_FIELDS: Array[String | Hash[String, String]] + CONTENT_MAP_SIZE: Integer + def content_fields: () -> Array[String | Hash[String, String]] + def content_map_size: () -> Integer def type: () -> "span" end end diff --git a/sig/datadog/ci/test_visibility/serializers/test_v1.rbs b/sig/datadog/ci/test_visibility/serializers/test_v1.rbs index f66ee815..21ee4866 100644 --- a/sig/datadog/ci/test_visibility/serializers/test_v1.rbs +++ b/sig/datadog/ci/test_visibility/serializers/test_v1.rbs @@ -3,8 +3,11 @@ module Datadog module TestVisibility module Serializers class TestV1 < Base - @content_fields: Array[String | Hash[String, String]] + CONTENT_FIELDS: Array[String | Hash[String, String]] + CONTENT_MAP_SIZE: Integer + def content_fields: () -> Array[String | Hash[String, String]] + def content_map_size: () -> Integer def type: () -> "test" From f6e06c409079f659828f36d6b41df93278ba3f0a Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 22 Sep 2023 17:01:37 +0200 Subject: [PATCH 25/45] fixes to make agentless mode work --- lib/datadog/ci/configuration/components.rb | 4 +++- lib/datadog/ci/contrib/cucumber/formatter.rb | 1 - lib/datadog/ci/contrib/minitest/hooks.rb | 1 - lib/datadog/ci/contrib/rspec/example.rb | 1 - lib/datadog/ci/ext/transport.rb | 2 +- lib/datadog/ci/test_visibility/transport.rb | 12 ++++++++++-- sig/datadog/ci/ext/transport.rbs | 2 +- 7 files changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index edb4ca32..b6873793 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "../flush" +require_relative "../test_visibility/transport" module Datadog module CI @@ -26,6 +27,7 @@ def activate_ci!(settings) # Tests are running without CI visibility enabled Datadog.logger.error( + "DATADOG CONFIGURATION - CI VISIBILITY - ATTENTION - " \ "Agentless mode was enabled but DD_API_KEY is not set: CI visibility is disabled. " \ "Please make sure to set valid api key in DD_API_KEY environment variable" ) @@ -56,7 +58,7 @@ def activate_ci!(settings) || CI::Flush::Finished.new # Pass through any other options - settings.tracing.test_mode.writer_options = settings.ci.writer_options + settings.tracing.test_mode.writer_options = writer_options end end end diff --git a/lib/datadog/ci/contrib/cucumber/formatter.rb b/lib/datadog/ci/contrib/cucumber/formatter.rb index 60238066..b41cded5 100644 --- a/lib/datadog/ci/contrib/cucumber/formatter.rb +++ b/lib/datadog/ci/contrib/cucumber/formatter.rb @@ -3,7 +3,6 @@ require_relative "../../recorder" require_relative "../../ext/test" require_relative "ext" -require_relative "integration" module Datadog module CI diff --git a/lib/datadog/ci/contrib/minitest/hooks.rb b/lib/datadog/ci/contrib/minitest/hooks.rb index 3e72c156..41253cde 100644 --- a/lib/datadog/ci/contrib/minitest/hooks.rb +++ b/lib/datadog/ci/contrib/minitest/hooks.rb @@ -3,7 +3,6 @@ require_relative "../../recorder" require_relative "../../ext/test" require_relative "ext" -require_relative "integration" module Datadog module CI diff --git a/lib/datadog/ci/contrib/rspec/example.rb b/lib/datadog/ci/contrib/rspec/example.rb index 85316b53..e695cd0a 100644 --- a/lib/datadog/ci/contrib/rspec/example.rb +++ b/lib/datadog/ci/contrib/rspec/example.rb @@ -3,7 +3,6 @@ require_relative "../../recorder" require_relative "../../ext/test" require_relative "ext" -require_relative "integration" module Datadog module CI diff --git a/lib/datadog/ci/ext/transport.rb b/lib/datadog/ci/ext/transport.rb index 9fa3cef6..231d48b0 100644 --- a/lib/datadog/ci/ext/transport.rb +++ b/lib/datadog/ci/ext/transport.rb @@ -8,7 +8,7 @@ module Transport HEADER_CONTENT_TYPE = "Content-Type" TEST_VISIBILITY_INTAKE_HOST_PREFIX = "citestcycle-intake" - TEST_VISIBILITY_INTAKE_PATH = "api/v2/citestcycle" + TEST_VISIBILITY_INTAKE_PATH = "/api/v2/citestcycle" CONTENT_TYPE_MESSAGEPACK = "application/msgpack" end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index d15ae801..1ec47ee9 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true +require "msgpack" require "datadog/core/encoding" # use it to chunk payloads by size # require "datadog/core/chunker" require_relative "serializers" +require_relative "../ext/transport" +require_relative "../transport/http" module Datadog module CI @@ -15,7 +18,8 @@ def initialize(api_key:, site: "datadoghq.com", serializer: Datadog::CI::TestVis @serializer = serializer @api_key = api_key @http = Datadog::CI::Transport::HTTP.new( - host: "#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{site}" + host: "#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{site}", + port: 443 ) end @@ -58,10 +62,14 @@ def to_msgpack(packer) packer ||= MessagePack::Packer.new packer.write_map_header(3) # Set header with how many elements in the map + packer.write("version") packer.write(1) packer.write("metadata") + packer.write_map_header(1) + + packer.write("*") packer.write_map_header(3) # TODO: implement our own identity? @@ -77,7 +85,7 @@ def to_msgpack(packer) packer.write("library_version") packer.write(Datadog::CI::VERSION::STRING) - packer.write_array_header(@events.size) + packer.write("events") packer.write(@events) end end diff --git a/sig/datadog/ci/ext/transport.rbs b/sig/datadog/ci/ext/transport.rbs index 9a25cdbc..00aacd2a 100644 --- a/sig/datadog/ci/ext/transport.rbs +++ b/sig/datadog/ci/ext/transport.rbs @@ -8,7 +8,7 @@ module Datadog TEST_VISIBILITY_INTAKE_HOST_PREFIX: "citestcycle-intake" - TEST_VISIBILITY_INTAKE_PATH: "api/v2/citestcycle" + TEST_VISIBILITY_INTAKE_PATH: "/api/v2/citestcycle" CONTENT_TYPE_MESSAGEPACK: "application/msgpack" end From 688eee324d52d5b0b235ebc5de755b2e53529ce5 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 25 Sep 2023 16:51:28 +0200 Subject: [PATCH 26/45] use async writer --- lib/datadog/ci/configuration/components.rb | 15 ++- lib/datadog/ci/workers.rb | 118 +++++++++++++++++++ lib/datadog/ci/writer.rb | 130 +++++++++++++++++++++ 3 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 lib/datadog/ci/workers.rb create mode 100644 lib/datadog/ci/writer.rb diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index b6873793..8bd4813d 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative "../flush" -require_relative "../test_visibility/transport" +require_relative "../writer" module Datadog module CI @@ -17,8 +17,7 @@ def initialize(settings) end def activate_ci!(settings) - agentless_transport = nil - writer_options = settings.ci.writer_options + agentless_writer = nil if settings.ci.agentless_mode_enabled if settings.api_key.nil? @@ -35,12 +34,10 @@ def activate_ci!(settings) settings.ci.enabled = false return else - agentless_transport = Datadog::CI::TestVisibility::Transport.new(api_key: settings.api_key) + agentless_writer = Datadog::CI::Writer.new(api_key: settings.api_key) end end - writer_options[:transport] = agentless_transport if agentless_transport - # Deactivate telemetry settings.telemetry.enabled = false @@ -57,8 +54,10 @@ def activate_ci!(settings) settings.tracing.test_mode.trace_flush = settings.ci.trace_flush \ || CI::Flush::Finished.new - # Pass through any other options - settings.tracing.test_mode.writer_options = writer_options + # # Pass through any other options + # settings.tracing.test_mode.writer_options = writer_options + # Use agentless writer + settings.tracing.writer = agentless_writer if agentless_writer end end end diff --git a/lib/datadog/ci/workers.rb b/lib/datadog/ci/workers.rb new file mode 100644 index 00000000..a3dddd42 --- /dev/null +++ b/lib/datadog/ci/workers.rb @@ -0,0 +1,118 @@ +require "datadog/tracing/buffer" + +module Datadog + module CI + module Workers + # Asynchronous worker that executes a +Send()+ operation after given + # seconds. + class AsyncTransport + DEFAULT_BUFFER_MAX_SIZE = 10000 + DEFAULT_FLUSH_INTERVAL = 0.5 + DEFAULT_TIMEOUT = 5 + BACK_OFF_RATIO = 1.2 + BACK_OFF_MAX = 5 + SHUTDOWN_TIMEOUT = 1000 + + attr_reader \ + :trace_buffer + + def initialize(options = {}) + @transport = options[:transport] + + # Callbacks + @trace_task = options[:on_trace] + + # Intervals + interval = options.fetch(:interval, DEFAULT_FLUSH_INTERVAL) + @flush_interval = interval + @back_off = interval + + # Buffers + buffer_size = options.fetch(:buffer_size, DEFAULT_BUFFER_MAX_SIZE) + @trace_buffer = Datadog::Tracing::TraceBuffer.new(buffer_size) + + # Threading + @shutdown = ConditionVariable.new + @mutex = Mutex.new + @worker = nil + @run = false + end + + # Callback function that process traces and executes the +send_traces()+ method. + def callback_traces + return true if @trace_buffer.empty? + + begin + traces = @trace_buffer.pop + @trace_task.call(traces, @transport) unless @trace_task.nil? || traces.empty? + rescue => e + # ensures that the thread will not die because of an exception. + # TODO[manu]: findout the reason and reschedule the send if it's not + # a fatal exception + Datadog.logger.error( + "Error during traces flush: dropped #{traces.length} items. Cause: #{e} Location: #{Array(e.backtrace).first}" + ) + end + end + + # Start the timer execution. + def start + @mutex.synchronize do + return if @run + + @run = true + Datadog.logger.debug { "Starting thread for: #{self}" } + @worker = Thread.new { perform } + @worker.name = self.class.name unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.3") + + nil + end + end + + # Closes all available queues and waits for the trace buffer to flush + def stop + @mutex.synchronize do + return unless @run + + @trace_buffer.close + @run = false + @shutdown.signal + end + + join + true + end + + # Block until executor shutdown is complete or until timeout seconds have passed. + def join + @worker.join(SHUTDOWN_TIMEOUT) + @transport.flush_sent_tests + end + + # Enqueue an item in the trace internal buffer. This operation is thread-safe + # because uses the +TraceBuffer+ data structure. + def enqueue_trace(trace) + return unless trace && !trace.empty? + + @trace_buffer.push(trace) + end + + private + + alias_method :flush_data, :callback_traces + + def perform + loop do + @back_off = flush_data ? @flush_interval : [@back_off * BACK_OFF_RATIO, BACK_OFF_MAX].min + + @mutex.synchronize do + return if !@run && @trace_buffer.empty? + + @shutdown.wait(@mutex, @back_off) if @run # do not wait when shutting down + end + end + end + end + end + end +end diff --git a/lib/datadog/ci/writer.rb b/lib/datadog/ci/writer.rb new file mode 100644 index 00000000..f43dca06 --- /dev/null +++ b/lib/datadog/ci/writer.rb @@ -0,0 +1,130 @@ +require_relative "workers" +require_relative "test_visibility/transport" + +module Datadog + module CI + # Processor that sends traces and metadata to the agent + class Writer + attr_reader \ + :transport, + :worker + + def initialize(options = {}) + # writer and transport parameters + @buff_size = options.fetch( + :buffer_size, + Datadog::Tracing::Workers::AsyncTransport::DEFAULT_BUFFER_MAX_SIZE + ) + @flush_interval = options.fetch( + :flush_interval, + Datadog::Tracing::Workers::AsyncTransport::DEFAULT_FLUSH_INTERVAL + ) + # transport and buffers + @transport = Datadog::CI::TestVisibility::Transport.new(api_key: options.fetch(:api_key)) + + @traces_flushed = 0 + + # one worker for flushing events + @worker = nil + + # Once stopped, this writer instance cannot be restarted. + # This allow for graceful shutdown, while preventing + # the host application from inadvertently start new + # threads during shutdown. + @stopped = false + end + + # Explicitly starts the {Writer}'s internal worker. + # + # The {Writer} is also automatically started when necessary during calls to {.write}. + def start + return false if @stopped + + pid = Process.pid + return if @worker && pid == @pid + + @pid = pid + + start_worker + true + end + + # spawns a worker for spans; they share the same transport which is thread-safe + # @!visibility private + def start_worker + @trace_handler = ->(items, transport) { send_spans(items, transport) } + @worker = Workers::AsyncTransport.new( + transport: @transport, + buffer_size: @buff_size, + on_trace: @trace_handler, + interval: @flush_interval + ) + + @worker.start + end + + # Gracefully shuts down this writer. + # + # Once stopped methods calls won't fail, but + # no internal work will be performed. + # + # It is not possible to restart a stopped writer instance. + def stop + stop_worker + end + + def stop_worker + @stopped = true + + return if @worker.nil? + + @worker.stop + @worker = nil + + true + end + + private :start_worker, :stop_worker + + # flush spans to the trace-agent, handles spans only + # @!visibility private + def send_spans(traces, transport) + return true if traces.empty? + + # Send traces and get responses + responses = transport.send_traces(traces) + + # Return if server error occurred. + !responses.find(&:server_error?) + end + + # enqueue the trace for submission to the API + def write(trace) + start if @worker.nil? || @pid != Process.pid + + worker_local = @worker + + if worker_local + worker_local.enqueue_trace(trace) + elsif !@stopped + Datadog.logger.debug("Writer either failed to start or was stopped before #write could complete") + end + end + + # stats returns a dictionary of stats about the writer. + def stats + { + traces_flushed: @traces_flushed, + transport: @transport.stats + } + end + + private + + def reset_stats! + @traces_flushed = 0 + @transport.stats.reset! + end + end + end +end From 126fd87ce61e00c9b741ccf7469d7f7146caeada Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 25 Sep 2023 17:23:05 +0200 Subject: [PATCH 27/45] filter invalid events (implement validation later) --- lib/datadog/ci/configuration/components.rb | 12 +++++++++--- lib/datadog/ci/test_visibility/transport.rb | 3 +++ lib/datadog/ci/workers.rb | 1 - 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index 8bd4813d..c4742044 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -17,7 +17,7 @@ def initialize(settings) end def activate_ci!(settings) - agentless_writer = nil + @agentless_writer = nil if settings.ci.agentless_mode_enabled if settings.api_key.nil? @@ -34,7 +34,7 @@ def activate_ci!(settings) settings.ci.enabled = false return else - agentless_writer = Datadog::CI::Writer.new(api_key: settings.api_key) + @gentless_writer = Datadog::CI::Writer.new(api_key: settings.api_key) end end @@ -57,7 +57,13 @@ def activate_ci!(settings) # # Pass through any other options # settings.tracing.test_mode.writer_options = writer_options # Use agentless writer - settings.tracing.writer = agentless_writer if agentless_writer + settings.tracing.writer = @agentless_writer if @agentless_writer + end + + def shutdown!(replacement = nil) + Datadog.logger.info("CI shutdown") + # @agentless_writer.stop if @agentless_writer + super(replacement) end end end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 1ec47ee9..7895a1f1 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -29,6 +29,9 @@ def send_traces(traces) # TODO: batch events in order not to load a lot of them in memory at once events = traces.flat_map { |trace| @serializer.convert_trace_to_serializable_events(trace) } + # move this to validation + events = events.filter { |event| event.start >= 946684800000000000 && event.duration > 0 } + payload = Payload.new(events) encoded_payload = encoder.encode(payload) diff --git a/lib/datadog/ci/workers.rb b/lib/datadog/ci/workers.rb index a3dddd42..2a82151d 100644 --- a/lib/datadog/ci/workers.rb +++ b/lib/datadog/ci/workers.rb @@ -86,7 +86,6 @@ def stop # Block until executor shutdown is complete or until timeout seconds have passed. def join @worker.join(SHUTDOWN_TIMEOUT) - @transport.flush_sent_tests end # Enqueue an item in the trace internal buffer. This operation is thread-safe From 6f14125f3d079b3b71c49c05357be27580e0df0b Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Tue, 26 Sep 2023 14:52:14 +0200 Subject: [PATCH 28/45] use async test mode from ddtrace tracing module --- lib/datadog/ci/configuration/components.rb | 29 ++--- lib/datadog/ci/transport/http.rb | 4 + lib/datadog/ci/workers.rb | 117 ------------------- lib/datadog/ci/writer.rb | 130 --------------------- 4 files changed, 16 insertions(+), 264 deletions(-) delete mode 100644 lib/datadog/ci/workers.rb delete mode 100644 lib/datadog/ci/writer.rb diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index c4742044..6ed69169 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative "../flush" -require_relative "../writer" +require_relative "../test_visibility/transport" module Datadog module CI @@ -17,7 +17,7 @@ def initialize(settings) end def activate_ci!(settings) - @agentless_writer = nil + agentless_transport = nil if settings.ci.agentless_mode_enabled if settings.api_key.nil? @@ -34,7 +34,7 @@ def activate_ci!(settings) settings.ci.enabled = false return else - @gentless_writer = Datadog::CI::Writer.new(api_key: settings.api_key) + agentless_transport = Datadog::CI::TestVisibility::Transport.new(api_key: settings.api_key) end end @@ -44,26 +44,21 @@ def activate_ci!(settings) # Deactivate remote configuration settings.remote.enabled = false - # enable tracing - settings.tracing.enabled = true - # Activate underlying tracing test mode settings.tracing.test_mode.enabled = true # Choose user defined TraceFlush or default to CI TraceFlush - settings.tracing.test_mode.trace_flush = settings.ci.trace_flush \ - || CI::Flush::Finished.new + settings.tracing.test_mode.trace_flush = settings.ci.trace_flush || CI::Flush::Finished.new - # # Pass through any other options - # settings.tracing.test_mode.writer_options = writer_options - # Use agentless writer - settings.tracing.writer = @agentless_writer if @agentless_writer - end + writer_options = settings.ci.writer_options + if agentless_transport + writer_options[:transport] = agentless_transport + writer_options[:shutdown_timeout] = 1000 + + settings.tracing.test_mode.async = true + end - def shutdown!(replacement = nil) - Datadog.logger.info("CI shutdown") - # @agentless_writer.stop if @agentless_writer - super(replacement) + settings.tracing.test_mode.writer_options = writer_options end end end diff --git a/lib/datadog/ci/transport/http.rb b/lib/datadog/ci/transport/http.rb index ba8d3bc9..4dd44172 100644 --- a/lib/datadog/ci/transport/http.rb +++ b/lib/datadog/ci/transport/http.rb @@ -93,6 +93,10 @@ def internal_error? false end + def trace_count + 0 + end + def inspect "#{self.class} ok?:#{ok?} unsupported?:#{unsupported?}, " \ "not_found?:#{not_found?}, client_error?:#{client_error?}, " \ diff --git a/lib/datadog/ci/workers.rb b/lib/datadog/ci/workers.rb deleted file mode 100644 index 2a82151d..00000000 --- a/lib/datadog/ci/workers.rb +++ /dev/null @@ -1,117 +0,0 @@ -require "datadog/tracing/buffer" - -module Datadog - module CI - module Workers - # Asynchronous worker that executes a +Send()+ operation after given - # seconds. - class AsyncTransport - DEFAULT_BUFFER_MAX_SIZE = 10000 - DEFAULT_FLUSH_INTERVAL = 0.5 - DEFAULT_TIMEOUT = 5 - BACK_OFF_RATIO = 1.2 - BACK_OFF_MAX = 5 - SHUTDOWN_TIMEOUT = 1000 - - attr_reader \ - :trace_buffer - - def initialize(options = {}) - @transport = options[:transport] - - # Callbacks - @trace_task = options[:on_trace] - - # Intervals - interval = options.fetch(:interval, DEFAULT_FLUSH_INTERVAL) - @flush_interval = interval - @back_off = interval - - # Buffers - buffer_size = options.fetch(:buffer_size, DEFAULT_BUFFER_MAX_SIZE) - @trace_buffer = Datadog::Tracing::TraceBuffer.new(buffer_size) - - # Threading - @shutdown = ConditionVariable.new - @mutex = Mutex.new - @worker = nil - @run = false - end - - # Callback function that process traces and executes the +send_traces()+ method. - def callback_traces - return true if @trace_buffer.empty? - - begin - traces = @trace_buffer.pop - @trace_task.call(traces, @transport) unless @trace_task.nil? || traces.empty? - rescue => e - # ensures that the thread will not die because of an exception. - # TODO[manu]: findout the reason and reschedule the send if it's not - # a fatal exception - Datadog.logger.error( - "Error during traces flush: dropped #{traces.length} items. Cause: #{e} Location: #{Array(e.backtrace).first}" - ) - end - end - - # Start the timer execution. - def start - @mutex.synchronize do - return if @run - - @run = true - Datadog.logger.debug { "Starting thread for: #{self}" } - @worker = Thread.new { perform } - @worker.name = self.class.name unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.3") - - nil - end - end - - # Closes all available queues and waits for the trace buffer to flush - def stop - @mutex.synchronize do - return unless @run - - @trace_buffer.close - @run = false - @shutdown.signal - end - - join - true - end - - # Block until executor shutdown is complete or until timeout seconds have passed. - def join - @worker.join(SHUTDOWN_TIMEOUT) - end - - # Enqueue an item in the trace internal buffer. This operation is thread-safe - # because uses the +TraceBuffer+ data structure. - def enqueue_trace(trace) - return unless trace && !trace.empty? - - @trace_buffer.push(trace) - end - - private - - alias_method :flush_data, :callback_traces - - def perform - loop do - @back_off = flush_data ? @flush_interval : [@back_off * BACK_OFF_RATIO, BACK_OFF_MAX].min - - @mutex.synchronize do - return if !@run && @trace_buffer.empty? - - @shutdown.wait(@mutex, @back_off) if @run # do not wait when shutting down - end - end - end - end - end - end -end diff --git a/lib/datadog/ci/writer.rb b/lib/datadog/ci/writer.rb deleted file mode 100644 index f43dca06..00000000 --- a/lib/datadog/ci/writer.rb +++ /dev/null @@ -1,130 +0,0 @@ -require_relative "workers" -require_relative "test_visibility/transport" - -module Datadog - module CI - # Processor that sends traces and metadata to the agent - class Writer - attr_reader \ - :transport, - :worker - - def initialize(options = {}) - # writer and transport parameters - @buff_size = options.fetch( - :buffer_size, - Datadog::Tracing::Workers::AsyncTransport::DEFAULT_BUFFER_MAX_SIZE - ) - @flush_interval = options.fetch( - :flush_interval, - Datadog::Tracing::Workers::AsyncTransport::DEFAULT_FLUSH_INTERVAL - ) - # transport and buffers - @transport = Datadog::CI::TestVisibility::Transport.new(api_key: options.fetch(:api_key)) - - @traces_flushed = 0 - - # one worker for flushing events - @worker = nil - - # Once stopped, this writer instance cannot be restarted. - # This allow for graceful shutdown, while preventing - # the host application from inadvertently start new - # threads during shutdown. - @stopped = false - end - - # Explicitly starts the {Writer}'s internal worker. - # - # The {Writer} is also automatically started when necessary during calls to {.write}. - def start - return false if @stopped - - pid = Process.pid - return if @worker && pid == @pid - - @pid = pid - - start_worker - true - end - - # spawns a worker for spans; they share the same transport which is thread-safe - # @!visibility private - def start_worker - @trace_handler = ->(items, transport) { send_spans(items, transport) } - @worker = Workers::AsyncTransport.new( - transport: @transport, - buffer_size: @buff_size, - on_trace: @trace_handler, - interval: @flush_interval - ) - - @worker.start - end - - # Gracefully shuts down this writer. - # - # Once stopped methods calls won't fail, but - # no internal work will be performed. - # - # It is not possible to restart a stopped writer instance. - def stop - stop_worker - end - - def stop_worker - @stopped = true - - return if @worker.nil? - - @worker.stop - @worker = nil - - true - end - - private :start_worker, :stop_worker - - # flush spans to the trace-agent, handles spans only - # @!visibility private - def send_spans(traces, transport) - return true if traces.empty? - - # Send traces and get responses - responses = transport.send_traces(traces) - - # Return if server error occurred. - !responses.find(&:server_error?) - end - - # enqueue the trace for submission to the API - def write(trace) - start if @worker.nil? || @pid != Process.pid - - worker_local = @worker - - if worker_local - worker_local.enqueue_trace(trace) - elsif !@stopped - Datadog.logger.debug("Writer either failed to start or was stopped before #write could complete") - end - end - - # stats returns a dictionary of stats about the writer. - def stats - { - traces_flushed: @traces_flushed, - transport: @transport.stats - } - end - - private - - def reset_stats! - @traces_flushed = 0 - @transport.stats.reset! - end - end - end -end From a3aafba7bedde39f2b62f571669ed62ea84005ff Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Wed, 27 Sep 2023 13:07:00 +0200 Subject: [PATCH 29/45] spec for test visibility transport --- lib/datadog/ci/test_visibility/transport.rb | 1 - .../ci/test_visibility/transport_spec.rb | 94 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 spec/datadog/ci/test_visibility/transport_spec.rb diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 7895a1f1..2f2966fc 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -26,7 +26,6 @@ def initialize(api_key:, site: "datadoghq.com", serializer: Datadog::CI::TestVis def send_traces(traces) # convert traces to events and construct payload # TODO: encode events immediately? - # TODO: batch events in order not to load a lot of them in memory at once events = traces.flat_map { |trace| @serializer.convert_trace_to_serializable_events(trace) } # move this to validation diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb new file mode 100644 index 00000000..4d5462da --- /dev/null +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -0,0 +1,94 @@ +require_relative "../../../../lib/datadog/ci/test_visibility/transport" + +RSpec.describe Datadog::CI::TestVisibility::Transport do + include_context "CI mode activated" do + let(:integration_name) { :rspec } + end + + subject { described_class.new(api_key: api_key, site: site, serializer: serializer) } + + let(:api_key) { "api_key" } + let(:site) { "datad0ghq.com" } + let(:serializer) { Datadog::CI::TestVisibility::Serializers } + + let(:http) { spy(:http) } + + before do + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: "citestcycle-intake.datad0ghq.com", + port: 443 + ).and_return(http) + end + + describe "#send_traces" do + context "with a single trace and a single span" do + before do + produce_test_trace + end + + it "sends correct payload" do + subject.send_traces([trace]) + + expect(http).to have_received(:request) do |args| + expect(args[:path]).to eq("/api/v2/citestcycle") + expect(args[:headers]).to eq({ + "DD-API-KEY" => "api_key", + "Content-Type" => "application/msgpack" + }) + + payload = MessagePack.unpack(args[:payload]) + expect(payload["version"]).to eq(1) + + metadata = payload["metadata"]["*"] + expect(metadata).to include("runtime-id", "language", "library_version") + + events = payload["events"] + expect(events.count).to eq(1) + expect(events.first["content"]["resource"]).to include("calculator_tests") + end + end + end + + context "multiple traces with 2 spans each" do + let(:traces_count) { 2 } + let(:expected_events_count) { 4 } + + before do + 2.times { produce_test_trace(with_http_span: true) } + end + + it "sends event for each of spans" do + subject.send_traces(traces) + + expect(http).to have_received(:request) do |args| + payload = MessagePack.unpack(args[:payload]) + events = payload["events"] + expect(events.count).to eq(expected_events_count) + end + end + + context "when some spans are broken" do + let(:expected_events_count) { 3 } + + before do + http_span = spans.find { |span| span.type == "http" } + http_span.start_time = Time.at(0) + end + + it "filters out invalid events" do + subject.send_traces(traces) + + expect(http).to have_received(:request) do |args| + payload = MessagePack.unpack(args[:payload]) + + events = payload["events"] + expect(events.count).to eq(expected_events_count) + + span_events = events.filter { |e| e["type"] == "span" } + expect(span_events.count).to eq(1) + end + end + end + end + end +end From 20b7a227ae0b775f4848e1a8b8af4a7815bb8a98 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Wed, 27 Sep 2023 13:20:49 +0200 Subject: [PATCH 30/45] empty events list case for transport --- lib/datadog/ci/test_visibility/transport.rb | 5 +++++ spec/datadog/ci/test_visibility/transport_spec.rb | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 2f2966fc..f903332e 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -31,6 +31,11 @@ def send_traces(traces) # move this to validation events = events.filter { |event| event.start >= 946684800000000000 && event.duration > 0 } + if events.empty? + Datadog.logger.debug("[TestVisibility::Transport] empty events list, skipping send") + return [] + end + payload = Payload.new(events) encoded_payload = encoder.encode(payload) diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index 4d5462da..18a284b3 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -90,5 +90,13 @@ end end end + + context "when there are no events" do + it "does not send anything" do + subject.send_traces([]) + + expect(http).not_to have_received(:request) + end + end end end From 097cf4d45f5c3c37bfa4d72bff2529394bdb2dd3 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Wed, 27 Sep 2023 13:40:16 +0200 Subject: [PATCH 31/45] fix payload packing on jruby --- lib/datadog/ci/test_visibility/transport.rb | 7 ++++++- spec/datadog/ci/test_visibility/transport_spec.rb | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index f903332e..c024f811 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -93,7 +93,12 @@ def to_msgpack(packer) packer.write(Datadog::CI::VERSION::STRING) packer.write("events") - packer.write(@events) + # this is required for jruby to pack array correctly + # on CRuby it is enough to call `packer.write(@events)` + packer.write_array_header(@events.size) + @events.each do |event| + packer.write(event) + end end end end diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index 18a284b3..a4714e9f 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -84,6 +84,7 @@ events = payload["events"] expect(events.count).to eq(expected_events_count) + p events span_events = events.filter { |e| e["type"] == "span" } expect(span_events.count).to eq(1) end From d8b473aad2444ad120465282a69800038b41ed90 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Wed, 27 Sep 2023 15:35:47 +0200 Subject: [PATCH 32/45] test components setup --- lib/datadog/ci/configuration/components.rb | 2 +- .../ci/configuration/components_spec.rb | 86 +++++++++++++++---- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index 6ed69169..f9323644 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -53,7 +53,7 @@ def activate_ci!(settings) writer_options = settings.ci.writer_options if agentless_transport writer_options[:transport] = agentless_transport - writer_options[:shutdown_timeout] = 1000 + writer_options[:shutdown_timeout] = 60 settings.tracing.test_mode.async = true end diff --git a/spec/datadog/ci/configuration/components_spec.rb b/spec/datadog/ci/configuration/components_spec.rb index 26efcc97..d47ca7e8 100644 --- a/spec/datadog/ci/configuration/components_spec.rb +++ b/spec/datadog/ci/configuration/components_spec.rb @@ -38,6 +38,14 @@ .to receive(:enabled) .and_return(enabled) + allow(settings.ci) + .to receive(:agentless_mode_enabled) + .and_return(agentless_enabled) + + allow(settings) + .to receive(:api_key) + .and_return(api_key) + # Spy on test mode behavior allow(settings.tracing.test_mode) .to receive(:enabled=) @@ -48,33 +56,79 @@ allow(settings.tracing.test_mode) .to receive(:writer_options=) + allow(settings.tracing.test_mode) + .to receive(:async=) + + allow(settings.ci) + .to receive(:enabled=) + + allow(Datadog.logger) + .to receive(:error) + components end + let(:api_key) { nil } + context "is enabled" do let(:enabled) { true } - it do - expect(settings.tracing.test_mode) - .to have_received(:enabled=) - .with(true) - end - - it do - expect(settings.tracing.test_mode) - .to have_received(:trace_flush=) - .with(settings.ci.trace_flush || kind_of(Datadog::CI::Flush::Finished)) - end - - it do - expect(settings.tracing.test_mode) - .to have_received(:writer_options=) - .with(settings.ci.writer_options) + context "and when #agentless_mode" do + context "is disabled" do + let(:agentless_enabled) { false } + + it do + expect(settings.tracing.test_mode) + .to have_received(:enabled=) + .with(true) + end + + it do + expect(settings.tracing.test_mode) + .to have_received(:trace_flush=) + .with(settings.ci.trace_flush || kind_of(Datadog::CI::Flush::Finished)) + end + + it do + expect(settings.tracing.test_mode) + .to have_received(:writer_options=) + .with(settings.ci.writer_options) + end + end + + context "is enabled" do + let(:agentless_enabled) { true } + + context "when api key is set" do + let(:api_key) { "api_key" } + + it "sets async for test mode and provides transport and shutdown timeout to the write" do + expect(settings.tracing.test_mode) + .to have_received(:async=) + .with(true) + + expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| + expect(options[:transport]).to be_a(Datadog::CI::TestVisibility::Transport) + expect(options[:shutdown_timeout]).to eq(60) + end + end + end + + context "when api key is not set" do + let(:api_key) { nil } + + it "logs an error message and disables CI visibility" do + expect(Datadog.logger).to have_received(:error) + expect(settings.ci).to have_received(:enabled=).with(false) + end + end + end end end context "is disabled" do let(:enabled) { false } + let(:agentless_enabled) { false } it do expect(settings.tracing.test_mode) From 62f1845b070fb5cee71c33850e4cd1a255fb8647 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Wed, 27 Sep 2023 17:05:18 +0200 Subject: [PATCH 33/45] event validation, optimize serializable events creation --- .../ci/test_visibility/serializers/base.rb | 35 +++++++++++--- .../ci/test_visibility/serializers/span.rb | 16 +++++++ .../ci/test_visibility/serializers/test_v1.rb | 16 +++++++ lib/datadog/ci/test_visibility/transport.rb | 35 ++++++++------ .../ci/test_visibility/serializers/base.rbs | 12 +++++ .../ci/test_visibility/serializers/span.rbs | 2 + .../test_visibility/serializers/test_v1.rbs | 2 + sig/datadog/ci/test_visibility/transport.rbs | 2 + .../test_visibility/serializers/span_spec.rb | 14 ++++++ .../serializers/test_v1_spec.rb | 46 +++++++++++++++++++ .../ci/test_visibility/transport_spec.rb | 14 ++++-- .../0/datadog/core/environment/identity.rbs | 10 ++++ 12 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 vendor/rbs/ddtrace/0/datadog/core/environment/identity.rbs diff --git a/lib/datadog/ci/test_visibility/serializers/base.rb b/lib/datadog/ci/test_visibility/serializers/base.rb index 4b6513d5..04e20e72 100644 --- a/lib/datadog/ci/test_visibility/serializers/base.rb +++ b/lib/datadog/ci/test_visibility/serializers/base.rb @@ -5,6 +5,10 @@ module CI module TestVisibility module Serializers class Base + MINIMUM_TIMESTAMP_NANO = 946684800000000000 + MINIMUM_DURATION_NANO = 0 + MAXIMUM_DURATION_NANO = 9223372036854775807 + attr_reader :trace, :span def initialize(trace, span) @@ -34,6 +38,11 @@ def to_msgpack(packer = nil) end end + # validates according to citestcycle json schema + def valid? + required_fields_present? && valid_start_time? && valid_duration? + end + def content_fields [] end @@ -82,11 +91,11 @@ def service end def start - time_nano(@span.start_time) + @start ||= time_nano(@span.start_time) end def duration - duration_nano(@span.duration) + @duration ||= duration_nano(@span.duration) end def meta @@ -113,6 +122,22 @@ def self.calculate_content_map_size(fields_list) private + def valid_start_time? + !start.nil? && start >= MINIMUM_TIMESTAMP_NANO + end + + def valid_duration? + !duration.nil? && duration >= MINIMUM_DURATION_NANO && duration <= MAXIMUM_DURATION_NANO + end + + def required_fields_present? + required_fields.all? { |field| !send(field).nil? } + end + + def required_fields + [] + end + def write_field(packer, field_name, method = nil) method ||= field_name @@ -120,14 +145,12 @@ def write_field(packer, field_name, method = nil) packer.write(send(method)) end - # Used for serialization - # @return [Integer] in nanoseconds since Epoch + # in nanoseconds since Epoch def time_nano(time) time.to_i * 1000000000 + time.nsec end - # Used for serialization - # @return [Integer] in nanoseconds since Epoch + # in nanoseconds def duration_nano(duration) (duration * 1e9).to_i end diff --git a/lib/datadog/ci/test_visibility/serializers/span.rb b/lib/datadog/ci/test_visibility/serializers/span.rb index 83f9b635..5ac657ff 100644 --- a/lib/datadog/ci/test_visibility/serializers/span.rb +++ b/lib/datadog/ci/test_visibility/serializers/span.rb @@ -17,6 +17,16 @@ class Span < Base CONTENT_MAP_SIZE = calculate_content_map_size(CONTENT_FIELDS) + REQUIRED_FIELDS = [ + "trace_id", + "span_id", + "error", + "name", + "resource", + "start", + "duration" + ].freeze + def content_fields CONTENT_FIELDS end @@ -28,6 +38,12 @@ def content_map_size def type "span" end + + private + + def required_fields + REQUIRED_FIELDS + end end end end diff --git a/lib/datadog/ci/test_visibility/serializers/test_v1.rb b/lib/datadog/ci/test_visibility/serializers/test_v1.rb index d8c8d5bb..c2547234 100644 --- a/lib/datadog/ci/test_visibility/serializers/test_v1.rb +++ b/lib/datadog/ci/test_visibility/serializers/test_v1.rb @@ -18,6 +18,16 @@ class TestV1 < Base CONTENT_MAP_SIZE = calculate_content_map_size(CONTENT_FIELDS) + REQUIRED_FIELDS = [ + "trace_id", + "span_id", + "error", + "name", + "resource", + "start", + "duration" + ].freeze + def content_fields CONTENT_FIELDS end @@ -37,6 +47,12 @@ def name def resource "#{@span.get_tag(Ext::Test::TAG_SUITE)}.#{@span.get_tag(Ext::Test::TAG_NAME)}" end + + private + + def required_fields + REQUIRED_FIELDS + end end end end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index c024f811..5677f974 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -2,6 +2,7 @@ require "msgpack" require "datadog/core/encoding" +require "datadog/core/environment/identity" # use it to chunk payloads by size # require "datadog/core/chunker" @@ -24,12 +25,9 @@ def initialize(api_key:, site: "datadoghq.com", serializer: Datadog::CI::TestVis end def send_traces(traces) - # convert traces to events and construct payload - # TODO: encode events immediately? - events = traces.flat_map { |trace| @serializer.convert_trace_to_serializable_events(trace) } + return [] if traces.nil? || traces.empty? - # move this to validation - events = events.filter { |event| event.start >= 946684800000000000 && event.duration > 0 } + events = serialize_traces(traces) if events.empty? Datadog.logger.debug("[TestVisibility::Transport] empty events list, skipping send") @@ -37,7 +35,6 @@ def send_traces(traces) end payload = Payload.new(events) - encoded_payload = encoder.encode(payload) response = @http.request( @@ -55,6 +52,22 @@ def send_traces(traces) private + def serialize_traces(traces) + # TODO: replace map.filter with filter_map when 1.0 is released + traces.flat_map do |trace| + trace.spans.map do |span| + event = @serializer.convert_span_to_serializable_event(trace, span) + + if event.valid? + event + else + Datadog.logger.debug { "Invalid span skipped: #{span}" } + nil + end + end.filter { |event| !event.nil? } + end + end + def encoder Datadog::Core::Encoding::MsgpackEncoder end @@ -79,15 +92,11 @@ def to_msgpack(packer) packer.write("*") packer.write_map_header(3) - # TODO: implement our own identity? - first_event = @events.first - if first_event - packer.write("runtime-id") - packer.write(first_event.runtime_id) - end + packer.write("runtime-id") + packer.write(Datadog::Core::Environment::Identity.id) packer.write("language") - packer.write("ruby") + packer.write(Datadog::Core::Environment::Identity.lang) packer.write("library_version") packer.write(Datadog::CI::VERSION::STRING) diff --git a/sig/datadog/ci/test_visibility/serializers/base.rbs b/sig/datadog/ci/test_visibility/serializers/base.rbs index 04050d25..908b19d6 100644 --- a/sig/datadog/ci/test_visibility/serializers/base.rbs +++ b/sig/datadog/ci/test_visibility/serializers/base.rbs @@ -3,7 +3,13 @@ module Datadog module TestVisibility module Serializers class Base + MINIMUM_TIMESTAMP_NANO: 946684800000000000 + MINIMUM_DURATION_NANO: 0 + MAXIMUM_DURATION_NANO: 9223372036854775807 + @content_fields_count: Integer + @start: Integer + @duration: Integer attr_reader trace: Datadog::Tracing::TraceSegment attr_reader span: Datadog::Tracing::Span @@ -12,6 +18,7 @@ module Datadog def to_msgpack: (?untyped? packer) -> untyped + def valid?: () -> bool def content_fields: () -> ::Array[String | Hash[String, String]] def content_map_size: () -> Integer @@ -49,6 +56,11 @@ module Datadog private + def valid_start_time?: () -> bool + def valid_duration?: () -> bool + def required_fields_present?: () -> bool + def required_fields: () -> Array[String] + def write_field: (untyped packer, String field_name, ?String? method) -> untyped def time_nano: (Time time) -> Integer def duration_nano: (Float duration) -> Integer diff --git a/sig/datadog/ci/test_visibility/serializers/span.rbs b/sig/datadog/ci/test_visibility/serializers/span.rbs index 52184d92..4b1c449c 100644 --- a/sig/datadog/ci/test_visibility/serializers/span.rbs +++ b/sig/datadog/ci/test_visibility/serializers/span.rbs @@ -5,7 +5,9 @@ module Datadog class Span < Base CONTENT_FIELDS: Array[String | Hash[String, String]] CONTENT_MAP_SIZE: Integer + REQUIRED_FIELDS: Array[String] + def required_fields: () -> Array[String] def content_fields: () -> Array[String | Hash[String, String]] def content_map_size: () -> Integer def type: () -> "span" diff --git a/sig/datadog/ci/test_visibility/serializers/test_v1.rbs b/sig/datadog/ci/test_visibility/serializers/test_v1.rbs index 21ee4866..a590d687 100644 --- a/sig/datadog/ci/test_visibility/serializers/test_v1.rbs +++ b/sig/datadog/ci/test_visibility/serializers/test_v1.rbs @@ -5,7 +5,9 @@ module Datadog class TestV1 < Base CONTENT_FIELDS: Array[String | Hash[String, String]] CONTENT_MAP_SIZE: Integer + REQUIRED_FIELDS: Array[String] + def required_fields: () -> Array[String] def content_fields: () -> Array[String | Hash[String, String]] def content_map_size: () -> Integer diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs index 73f4cb15..eb11ed5c 100644 --- a/sig/datadog/ci/test_visibility/transport.rbs +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -12,6 +12,8 @@ module Datadog private + def serialize_traces: (Array[Datadog::Tracing::TraceSegment] traces) -> ::Array[Datadog::CI::TestVisibility::Serializers::Base] + def encoder: () -> singleton(Datadog::Core::Encoding::MsgpackEncoder) class Payload diff --git a/spec/datadog/ci/test_visibility/serializers/span_spec.rb b/spec/datadog/ci/test_visibility/serializers/span_spec.rb index 57abf816..cb3fe02a 100644 --- a/spec/datadog/ci/test_visibility/serializers/span_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/span_spec.rb @@ -50,4 +50,18 @@ end end end + + describe "valid?" do + context "required fields" do + context "when not present" do + before do + produce_test_trace(with_http_span: true) + + tracer_span.name = nil + end + + it { is_expected.not_to be_valid } + end + end + end end diff --git a/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb index aa2ffaed..8d33044b 100644 --- a/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb +++ b/spec/datadog/ci/test_visibility/serializers/test_v1_spec.rb @@ -73,4 +73,50 @@ end end end + + describe "#valid?" do + context "duration" do + before do + produce_test_trace + span.duration = duration + end + + context "when positive number" do + let(:duration) { 42 } + + it { is_expected.to be_valid } + end + + context "when negative number" do + let(:duration) { -1 } + + it { is_expected.not_to be_valid } + end + + context "when too big" do + let(:duration) { Datadog::CI::TestVisibility::Serializers::Base::MAXIMUM_DURATION_NANO + 1 } + + it { is_expected.not_to be_valid } + end + end + + context "start" do + before do + produce_test_trace + span.start_time = start_time + end + + context "when now" do + let(:start_time) { Time.now } + + it { is_expected.to be_valid } + end + + context "when far in the past" do + let(:start_time) { Time.at(0) } + + it { is_expected.not_to be_valid } + end + end + end end diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index a4714e9f..409b9575 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -40,7 +40,8 @@ expect(payload["version"]).to eq(1) metadata = payload["metadata"]["*"] - expect(metadata).to include("runtime-id", "language", "library_version") + expect(metadata).to include("runtime-id", "library_version") + expect(metadata["language"]).to eq("ruby") events = payload["events"] expect(events.count).to eq(1) @@ -84,7 +85,6 @@ events = payload["events"] expect(events.count).to eq(expected_events_count) - p events span_events = events.filter { |e| e["type"] == "span" } expect(span_events.count).to eq(1) end @@ -92,9 +92,15 @@ end end - context "when there are no events" do + context "when all events are invalid" do + before do + produce_test_trace + + span.start_time = Time.at(0) + end + it "does not send anything" do - subject.send_traces([]) + subject.send_traces(traces) expect(http).not_to have_received(:request) end diff --git a/vendor/rbs/ddtrace/0/datadog/core/environment/identity.rbs b/vendor/rbs/ddtrace/0/datadog/core/environment/identity.rbs new file mode 100644 index 00000000..6494a483 --- /dev/null +++ b/vendor/rbs/ddtrace/0/datadog/core/environment/identity.rbs @@ -0,0 +1,10 @@ +module Datadog + module Core + module Environment + module Identity + def self.lang: () -> "ruby" + def self.id: () -> String + end + end + end +end From ac867ce37a5f9688a3b9e744926fc628b522a84f Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 28 Sep 2023 12:29:56 +0200 Subject: [PATCH 34/45] introduce factories concept for serializers --- lib/datadog/ci/test_visibility/serializers.rb | 28 -------------- .../serializers/factories/test_level.rb | 30 +++++++++++++++ lib/datadog/ci/test_visibility/transport.rb | 27 +++++++------ .../ci/test_visibility/serializers.rbs | 10 ----- .../serializers/factories/test_level.rbs | 13 +++++++ sig/datadog/ci/test_visibility/transport.rbs | 4 +- .../serializers/factories/test_level_spec.rb | 21 ++++++++++ .../ci/test_visibility/serializers_spec.rb | 38 ------------------- .../ci/test_visibility/transport_spec.rb | 4 +- 9 files changed, 83 insertions(+), 92 deletions(-) delete mode 100644 lib/datadog/ci/test_visibility/serializers.rb create mode 100644 lib/datadog/ci/test_visibility/serializers/factories/test_level.rb delete mode 100644 sig/datadog/ci/test_visibility/serializers.rbs create mode 100644 sig/datadog/ci/test_visibility/serializers/factories/test_level.rbs create mode 100644 spec/datadog/ci/test_visibility/serializers/factories/test_level_spec.rb delete mode 100644 spec/datadog/ci/test_visibility/serializers_spec.rb diff --git a/lib/datadog/ci/test_visibility/serializers.rb b/lib/datadog/ci/test_visibility/serializers.rb deleted file mode 100644 index 294241ef..00000000 --- a/lib/datadog/ci/test_visibility/serializers.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require_relative "serializers/test_v1" -require_relative "serializers/span" - -module Datadog - module CI - module TestVisibility - module Serializers - module_function - - def convert_trace_to_serializable_events(trace) - trace.spans.map { |span| convert_span_to_serializable_event(trace, span) } - end - - # for test suite visibility we might need to make it configurable - def convert_span_to_serializable_event(trace, span) - case span.type - when Datadog::CI::Ext::AppTypes::TYPE_TEST - Serializers::TestV1.new(trace, span) - else - Serializers::Span.new(trace, span) - end - end - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/serializers/factories/test_level.rb b/lib/datadog/ci/test_visibility/serializers/factories/test_level.rb new file mode 100644 index 00000000..d2cfe1ae --- /dev/null +++ b/lib/datadog/ci/test_visibility/serializers/factories/test_level.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative "../test_v1" +require_relative "../span" + +module Datadog + module CI + module TestVisibility + module Serializers + module Factories + # This factory takes care of creating citestcycle serializers when test-level visibility is enabled + # NOTE: citestcycle is a protocol Datadog uses to submit test execution tracing information to CI visibility + # backend + module TestLevel + module_function + + def serializer(trace, span) + case span.type + when Datadog::CI::Ext::AppTypes::TYPE_TEST + Serializers::TestV1.new(trace, span) + else + Serializers::Span.new(trace, span) + end + end + end + end + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 5677f974..3523af05 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -6,7 +6,7 @@ # use it to chunk payloads by size # require "datadog/core/chunker" -require_relative "serializers" +require_relative "serializers/factories/test_level" require_relative "../ext/transport" require_relative "../transport/http" @@ -14,9 +14,12 @@ module Datadog module CI module TestVisibility class Transport - # TODO: rename Serializers module - def initialize(api_key:, site: "datadoghq.com", serializer: Datadog::CI::TestVisibility::Serializers) - @serializer = serializer + def initialize( + api_key:, + site: "datadoghq.com", + serializers_factory: Datadog::CI::TestVisibility::Serializers::Factories::TestLevel + ) + @serializers_factory = serializers_factory @api_key = api_key @http = Datadog::CI::Transport::HTTP.new( host: "#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{site}", @@ -27,14 +30,14 @@ def initialize(api_key:, site: "datadoghq.com", serializer: Datadog::CI::TestVis def send_traces(traces) return [] if traces.nil? || traces.empty? - events = serialize_traces(traces) + serializable_events = serialize_traces(traces) - if events.empty? - Datadog.logger.debug("[TestVisibility::Transport] empty events list, skipping send") + if serializable_events.empty? + Datadog.logger.debug("[TestVisibility::Transport] empty serializable events list, skipping send") return [] end - payload = Payload.new(events) + payload = Payload.new(serializable_events) encoded_payload = encoder.encode(payload) response = @http.request( @@ -56,15 +59,15 @@ def serialize_traces(traces) # TODO: replace map.filter with filter_map when 1.0 is released traces.flat_map do |trace| trace.spans.map do |span| - event = @serializer.convert_span_to_serializable_event(trace, span) + serializer = @serializers_factory.serializer(trace, span) - if event.valid? - event + if serializer.valid? + serializer else Datadog.logger.debug { "Invalid span skipped: #{span}" } nil end - end.filter { |event| !event.nil? } + end.filter { |serializer| !serializer.nil? } end end diff --git a/sig/datadog/ci/test_visibility/serializers.rbs b/sig/datadog/ci/test_visibility/serializers.rbs deleted file mode 100644 index c8adc17e..00000000 --- a/sig/datadog/ci/test_visibility/serializers.rbs +++ /dev/null @@ -1,10 +0,0 @@ -module Datadog - module CI - module TestVisibility - module Serializers - def self?.convert_trace_to_serializable_events: (Datadog::Tracing::TraceSegment trace) -> untyped - def self?.convert_span_to_serializable_event: (Datadog::Tracing::TraceSegment trace, Datadog::Tracing::Span span) -> untyped - end - end - end -end diff --git a/sig/datadog/ci/test_visibility/serializers/factories/test_level.rbs b/sig/datadog/ci/test_visibility/serializers/factories/test_level.rbs new file mode 100644 index 00000000..ac7b6c13 --- /dev/null +++ b/sig/datadog/ci/test_visibility/serializers/factories/test_level.rbs @@ -0,0 +1,13 @@ +module Datadog + module CI + module TestVisibility + module Serializers + module Factories + module TestLevel + def self?.serializer: (Datadog::Tracing::TraceSegment trace, Datadog::Tracing::Span span) -> Datadog::CI::TestVisibility::Serializers::Base + end + end + end + end + end +end diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs index eb11ed5c..9d66cd03 100644 --- a/sig/datadog/ci/test_visibility/transport.rbs +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -4,9 +4,9 @@ module Datadog class Transport @api_key: String @http: Datadog::CI::Transport::HTTP - @serializer: singleton(Datadog::CI::TestVisibility::Serializers) + @serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel) - def initialize: (api_key: String, ?site: ::String, ?serializer: singleton(Datadog::CI::TestVisibility::Serializers)) -> void + def initialize: (api_key: String, ?site: ::String, ?serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel)) -> void def send_traces: (Array[Datadog::Tracing::TraceSegment] traces) -> ::Array[Datadog::CI::Transport::HTTP::Response] diff --git a/spec/datadog/ci/test_visibility/serializers/factories/test_level_spec.rb b/spec/datadog/ci/test_visibility/serializers/factories/test_level_spec.rb new file mode 100644 index 00000000..72555693 --- /dev/null +++ b/spec/datadog/ci/test_visibility/serializers/factories/test_level_spec.rb @@ -0,0 +1,21 @@ +require_relative "../../../../../../lib/datadog/ci/test_visibility/serializers/factories/test_level" +require_relative "../../../../../../lib/datadog/ci/test_visibility/serializers/test_v1" +require_relative "../../../../../../lib/datadog/ci/recorder" + +RSpec.describe Datadog::CI::TestVisibility::Serializers::Factories::TestLevel do + include_context "CI mode activated" do + let(:integration_name) { :rspec } + end + + subject { described_class.serializer(trace, span) } + + describe ".convert_trace_to_serializable_events" do + context "traced a single test execution with Recorder" do + before do + produce_test_trace + end + + it { is_expected.to be_kind_of(Datadog::CI::TestVisibility::Serializers::TestV1) } + end + end +end diff --git a/spec/datadog/ci/test_visibility/serializers_spec.rb b/spec/datadog/ci/test_visibility/serializers_spec.rb deleted file mode 100644 index c816d6a7..00000000 --- a/spec/datadog/ci/test_visibility/serializers_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require_relative "../../../../lib/datadog/ci/test_visibility/serializers" -require_relative "../../../../lib/datadog/ci/recorder" - -RSpec.describe Datadog::CI::TestVisibility::Serializers do - include_context "CI mode activated" do - let(:integration_name) { :rspec } - end - - subject { described_class.convert_trace_to_serializable_events(trace) } - - describe ".convert_trace_to_serializable_events" do - context "traced a single test execution with Recorder" do - before do - produce_test_trace(with_http_span: true) - end - - it "converts trace to an array of serializable events" do - expect(subject.count).to eq(2) - end - - context "test event is present" do - let(:test_event) { subject.find { |event| event.type == "test" } } - - it "contains test span" do - expect(test_event.span_id).to eq(first_test_span.id) - end - end - - context "span event is present" do - let(:span_event) { subject.find { |event| event.type == "span" } } - - it "contains http span" do - expect(span_event.span_id).to eq(first_other_span.id) - end - end - end - end -end diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index 409b9575..b8614134 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -5,11 +5,11 @@ let(:integration_name) { :rspec } end - subject { described_class.new(api_key: api_key, site: site, serializer: serializer) } + subject { described_class.new(api_key: api_key, site: site, serializers_factory: serializers_factory) } let(:api_key) { "api_key" } let(:site) { "datad0ghq.com" } - let(:serializer) { Datadog::CI::TestVisibility::Serializers } + let(:serializers_factory) { Datadog::CI::TestVisibility::Serializers::Factories::TestLevel } let(:http) { spy(:http) } From 2aba5f3fafa3bbe6609c59452f868cd704bfcf52 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 28 Sep 2023 12:37:10 +0200 Subject: [PATCH 35/45] move Flush to the test_visibility module --- lib/datadog/ci/configuration/components.rb | 4 +- lib/datadog/ci/flush.rb | 38 ------------------ lib/datadog/ci/test_visibility/flush.rb | 40 +++++++++++++++++++ sig/datadog/ci/flush.rbs | 15 ------- sig/datadog/ci/test_visibility/flush.rbs | 17 ++++++++ .../ci/configuration/components_spec.rb | 4 +- .../ci/{ => test_visibility}/flush_spec.rb | 4 +- 7 files changed, 63 insertions(+), 59 deletions(-) delete mode 100644 lib/datadog/ci/flush.rb create mode 100644 lib/datadog/ci/test_visibility/flush.rb delete mode 100644 sig/datadog/ci/flush.rbs create mode 100644 sig/datadog/ci/test_visibility/flush.rbs rename spec/datadog/ci/{ => test_visibility}/flush_spec.rb (92%) diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index f9323644..803e6dbe 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "../flush" +require_relative "../test_visibility/flush" require_relative "../test_visibility/transport" module Datadog @@ -48,7 +48,7 @@ def activate_ci!(settings) settings.tracing.test_mode.enabled = true # Choose user defined TraceFlush or default to CI TraceFlush - settings.tracing.test_mode.trace_flush = settings.ci.trace_flush || CI::Flush::Finished.new + settings.tracing.test_mode.trace_flush = settings.ci.trace_flush || CI::TestVisibility::Flush::Finished.new writer_options = settings.ci.writer_options if agentless_transport diff --git a/lib/datadog/ci/flush.rb b/lib/datadog/ci/flush.rb deleted file mode 100644 index 5fabf41e..00000000 --- a/lib/datadog/ci/flush.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require "datadog/tracing/metadata/ext" -require "datadog/tracing/flush" - -module Datadog - module CI - module Flush - # Common behavior for CI flushing - module Tagging - # Decorate a trace with CI tags - def get_trace(trace_op) - trace = trace_op.flush! - - # Origin tag is required on every span - trace.spans.each do |span| - span.set_tag( - Tracing::Metadata::Ext::Distributed::TAG_ORIGIN, - trace.origin - ) - end - - trace - end - end - - # Consumes only completed traces (where all spans have finished) - class Finished < Tracing::Flush::Finished - prepend Tagging - end - - # Performs partial trace flushing to avoid large traces residing in memory for too long - class Partial < Tracing::Flush::Partial - prepend Tagging - end - end - end -end diff --git a/lib/datadog/ci/test_visibility/flush.rb b/lib/datadog/ci/test_visibility/flush.rb new file mode 100644 index 00000000..36660e7c --- /dev/null +++ b/lib/datadog/ci/test_visibility/flush.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "datadog/tracing/metadata/ext" +require "datadog/tracing/flush" + +module Datadog + module CI + module TestVisibility + module Flush + # Common behavior for CI flushing + module Tagging + # Decorate a trace with CI tags + def get_trace(trace_op) + trace = trace_op.flush! + + # Origin tag is required on every span + trace.spans.each do |span| + span.set_tag( + Tracing::Metadata::Ext::Distributed::TAG_ORIGIN, + trace.origin + ) + end + + trace + end + end + + # Consumes only completed traces (where all spans have finished) + class Finished < Tracing::Flush::Finished + prepend Tagging + end + + # Performs partial trace flushing to avoid large traces residing in memory for too long + class Partial < Tracing::Flush::Partial + prepend Tagging + end + end + end + end +end diff --git a/sig/datadog/ci/flush.rbs b/sig/datadog/ci/flush.rbs deleted file mode 100644 index cc955d6c..00000000 --- a/sig/datadog/ci/flush.rbs +++ /dev/null @@ -1,15 +0,0 @@ -module Datadog - module CI - module Flush - module Tagging - def get_trace: (Datadog::Tracing::TraceOperation trace_op) -> untyped - end - class Finished < Tracing::Flush::Finished - prepend Tagging - end - class Partial < Tracing::Flush::Partial - prepend Tagging - end - end - end -end diff --git a/sig/datadog/ci/test_visibility/flush.rbs b/sig/datadog/ci/test_visibility/flush.rbs new file mode 100644 index 00000000..6d1f153a --- /dev/null +++ b/sig/datadog/ci/test_visibility/flush.rbs @@ -0,0 +1,17 @@ +module Datadog + module CI + module TestVisibility + module Flush + module Tagging + def get_trace: (Datadog::Tracing::TraceOperation trace_op) -> untyped + end + class Finished < Tracing::Flush::Finished + prepend Tagging + end + class Partial < Tracing::Flush::Partial + prepend Tagging + end + end + end + end +end diff --git a/spec/datadog/ci/configuration/components_spec.rb b/spec/datadog/ci/configuration/components_spec.rb index d47ca7e8..7834cdb0 100644 --- a/spec/datadog/ci/configuration/components_spec.rb +++ b/spec/datadog/ci/configuration/components_spec.rb @@ -86,7 +86,7 @@ it do expect(settings.tracing.test_mode) .to have_received(:trace_flush=) - .with(settings.ci.trace_flush || kind_of(Datadog::CI::Flush::Finished)) + .with(settings.ci.trace_flush || kind_of(Datadog::CI::TestVisibility::Flush::Finished)) end it do @@ -108,7 +108,7 @@ .with(true) expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| - expect(options[:transport]).to be_a(Datadog::CI::TestVisibility::Transport) + expect(options[:transport]).to be_kind_of(Datadog::CI::TestVisibility::Transport) expect(options[:shutdown_timeout]).to eq(60) end end diff --git a/spec/datadog/ci/flush_spec.rb b/spec/datadog/ci/test_visibility/flush_spec.rb similarity index 92% rename from spec/datadog/ci/flush_spec.rb rename to spec/datadog/ci/test_visibility/flush_spec.rb index 3de76ccf..034dd006 100644 --- a/spec/datadog/ci/flush_spec.rb +++ b/spec/datadog/ci/test_visibility/flush_spec.rb @@ -33,7 +33,7 @@ end end -RSpec.describe Datadog::CI::Flush::Finished do +RSpec.describe Datadog::CI::TestVisibility::Flush::Finished do subject(:trace_flush) { described_class.new } describe "#consume" do @@ -44,7 +44,7 @@ end end -RSpec.describe Datadog::CI::Flush::Partial do +RSpec.describe Datadog::CI::TestVisibility::Flush::Partial do subject(:trace_flush) { described_class.new(min_spans_before_partial_flush: min_spans_for_partial) } let(:min_spans_for_partial) { 2 } From dd40bb8d34222a33f31504694fe19ecbf1ed137b Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 28 Sep 2023 13:06:26 +0200 Subject: [PATCH 36/45] preparation for enabling chunking: directly encode spans to msgpack --- lib/datadog/ci/test_visibility/transport.rb | 60 ++++++++------------ sig/datadog/ci/test_visibility/transport.rbs | 11 +--- vendor/rbs/msgpack/0/message_pack/packer.rbs | 5 ++ 3 files changed, 31 insertions(+), 45 deletions(-) diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 3523af05..a9391b9e 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -30,15 +30,13 @@ def initialize( def send_traces(traces) return [] if traces.nil? || traces.empty? - serializable_events = serialize_traces(traces) - - if serializable_events.empty? + encoded_events = encode_traces(traces) + if encoded_events.empty? Datadog.logger.debug("[TestVisibility::Transport] empty serializable events list, skipping send") return [] end - payload = Payload.new(serializable_events) - encoded_payload = encoder.encode(payload) + encoded_payload = pack_events(encoded_events) response = @http.request( path: Datadog::CI::Ext::Transport::TEST_VISIBILITY_INTAKE_PATH, @@ -55,19 +53,19 @@ def send_traces(traces) private - def serialize_traces(traces) + def encode_traces(traces) # TODO: replace map.filter with filter_map when 1.0 is released traces.flat_map do |trace| trace.spans.map do |span| serializer = @serializers_factory.serializer(trace, span) if serializer.valid? - serializer + encoder.encode(serializer) else Datadog.logger.debug { "Invalid span skipped: #{span}" } nil end - end.filter { |serializer| !serializer.nil? } + end.filter { |encoded_event| !encoded_event.nil? } end end @@ -75,43 +73,33 @@ def encoder Datadog::Core::Encoding::MsgpackEncoder end - # represents payload with some subset of serializable events to be sent to CI-APP intake - class Payload - def initialize(events) - @events = events - end + def pack_events(encoded_events) + packer = MessagePack::Packer.new - def to_msgpack(packer) - packer ||= MessagePack::Packer.new + packer.write_map_header(3) # Set header with how many elements in the map - packer.write_map_header(3) # Set header with how many elements in the map + packer.write("version") + packer.write(1) - packer.write("version") - packer.write(1) + packer.write("metadata") + packer.write_map_header(1) - packer.write("metadata") - packer.write_map_header(1) + packer.write("*") + packer.write_map_header(3) - packer.write("*") - packer.write_map_header(3) + packer.write("runtime-id") + packer.write(Datadog::Core::Environment::Identity.id) - packer.write("runtime-id") - packer.write(Datadog::Core::Environment::Identity.id) + packer.write("language") + packer.write(Datadog::Core::Environment::Identity.lang) - packer.write("language") - packer.write(Datadog::Core::Environment::Identity.lang) + packer.write("library_version") + packer.write(Datadog::CI::VERSION::STRING) - packer.write("library_version") - packer.write(Datadog::CI::VERSION::STRING) + packer.write("events") + packer.write_array_header(encoded_events.size) - packer.write("events") - # this is required for jruby to pack array correctly - # on CRuby it is enough to call `packer.write(@events)` - packer.write_array_header(@events.size) - @events.each do |event| - packer.write(event) - end - end + (packer.buffer.to_a + encoded_events).join end end end diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs index 9d66cd03..f20185ec 100644 --- a/sig/datadog/ci/test_visibility/transport.rbs +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -12,16 +12,9 @@ module Datadog private - def serialize_traces: (Array[Datadog::Tracing::TraceSegment] traces) -> ::Array[Datadog::CI::TestVisibility::Serializers::Base] - + def pack_events: (Array[String] encoded_events) -> String + def encode_traces: (Array[Datadog::Tracing::TraceSegment] traces) -> ::Array[String] def encoder: () -> singleton(Datadog::Core::Encoding::MsgpackEncoder) - - class Payload - @events: Array[Datadog::CI::TestVisibility::Serializers::Base] - - def initialize: (Array[Datadog::CI::TestVisibility::Serializers::Base] events) -> void - def to_msgpack: (untyped packer) -> untyped - end end end end diff --git a/vendor/rbs/msgpack/0/message_pack/packer.rbs b/vendor/rbs/msgpack/0/message_pack/packer.rbs index 048169f1..e5eaccd4 100644 --- a/vendor/rbs/msgpack/0/message_pack/packer.rbs +++ b/vendor/rbs/msgpack/0/message_pack/packer.rbs @@ -1,9 +1,14 @@ module MessagePack + class Buffer + def to_a: () -> Array[String] + end + class Packer def initialize: () -> void def write: (Object input) -> self def write_map_header: (Integer keys_number) -> self def write_array_header: (Integer keys_number) -> self + def buffer: () -> Buffer end end From 327d1df918561cb4a24fa118b1be19d2ba2f5db0 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 28 Sep 2023 13:35:58 +0200 Subject: [PATCH 37/45] add chunking to make sure request size is not bigger than 4Mb --- lib/datadog/ci/test_visibility/transport.rb | 60 ++++++++++++------- sig/datadog/ci/test_visibility/transport.rbs | 12 +++- .../ci/test_visibility/transport_spec.rb | 23 ++++++- vendor/rbs/ddtrace/0/datadog/core/chunket.rbs | 7 +++ 4 files changed, 79 insertions(+), 23 deletions(-) create mode 100644 vendor/rbs/ddtrace/0/datadog/core/chunket.rbs diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index a9391b9e..c513c161 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -3,8 +3,7 @@ require "msgpack" require "datadog/core/encoding" require "datadog/core/environment/identity" -# use it to chunk payloads by size -# require "datadog/core/chunker" +require "datadog/core/chunker" require_relative "serializers/factories/test_level" require_relative "../ext/transport" @@ -14,13 +13,19 @@ module Datadog module CI module TestVisibility class Transport + # CI test cycle intake's limit is 5.1MB uncompressed + # We will use a bit more conservative value 4MB + DEFAULT_MAX_PAYLOAD_SIZE = 4 * 1024 * 1024 + def initialize( api_key:, site: "datadoghq.com", - serializers_factory: Datadog::CI::TestVisibility::Serializers::Factories::TestLevel + serializers_factory: Datadog::CI::TestVisibility::Serializers::Factories::TestLevel, + max_payload_size: DEFAULT_MAX_PAYLOAD_SIZE ) @serializers_factory = serializers_factory @api_key = api_key + @max_payload_size = max_payload_size @http = Datadog::CI::Transport::HTTP.new( host: "#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{site}", port: 443 @@ -36,9 +41,20 @@ def send_traces(traces) return [] end - encoded_payload = pack_events(encoded_events) + responses = [] + Datadog::Core::Chunker.chunk_by_size(encoded_events, @max_payload_size).map do |chunk| + encoded_payload = pack_events(chunk) + response = send_payload(encoded_payload) + responses << response + end + + responses + end - response = @http.request( + private + + def send_payload(encoded_payload) + @http.request( path: Datadog::CI::Ext::Transport::TEST_VISIBILITY_INTAKE_PATH, payload: encoded_payload, headers: { @@ -46,26 +62,28 @@ def send_traces(traces) Ext::Transport::HEADER_CONTENT_TYPE => Ext::Transport::CONTENT_TYPE_MESSAGEPACK } ) - - # Tracing writers expect an array of responses - [response] end - private - def encode_traces(traces) - # TODO: replace map.filter with filter_map when 1.0 is released traces.flat_map do |trace| - trace.spans.map do |span| - serializer = @serializers_factory.serializer(trace, span) - - if serializer.valid? - encoder.encode(serializer) - else - Datadog.logger.debug { "Invalid span skipped: #{span}" } - nil - end - end.filter { |encoded_event| !encoded_event.nil? } + spans = trace.spans + # TODO: remove condition when 1.0 is released + if spans.respond_to?(:filter_map) + spans.filter_map { |span| encode_span(trace, span) } + else + trace.spans.map { |span| encode_span(trace, span) }.reject(&:nil?) + end + end + end + + def encode_span(trace, span) + serializer = @serializers_factory.serializer(trace, span) + + if serializer.valid? + encoder.encode(serializer) + else + Datadog.logger.debug { "Invalid span skipped: #{span}" } + nil end end diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs index f20185ec..4a7d57f6 100644 --- a/sig/datadog/ci/test_visibility/transport.rbs +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -2,18 +2,28 @@ module Datadog module CI module TestVisibility class Transport + DEFAULT_MAX_PAYLOAD_SIZE: Integer + @api_key: String @http: Datadog::CI::Transport::HTTP @serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel) + @max_payload_size: Integer - def initialize: (api_key: String, ?site: ::String, ?serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel)) -> void + def initialize: ( + api_key: String, + ?site: ::String, + ?serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel), + ?max_payload_size: Integer + ) -> void def send_traces: (Array[Datadog::Tracing::TraceSegment] traces) -> ::Array[Datadog::CI::Transport::HTTP::Response] private + def send_payload: (String encoded_payload) -> Datadog::CI::Transport::HTTP::Response def pack_events: (Array[String] encoded_events) -> String def encode_traces: (Array[Datadog::Tracing::TraceSegment] traces) -> ::Array[String] + def encode_span: (Datadog::Tracing::TraceSegment trace, Datadog::Tracing::Span span) -> String? def encoder: () -> singleton(Datadog::Core::Encoding::MsgpackEncoder) end end diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index b8614134..808bed0b 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -5,11 +5,19 @@ let(:integration_name) { :rspec } end - subject { described_class.new(api_key: api_key, site: site, serializers_factory: serializers_factory) } + subject do + described_class.new( + api_key: api_key, + site: site, + serializers_factory: serializers_factory, + max_payload_size: max_payload_size + ) + end let(:api_key) { "api_key" } let(:site) { "datad0ghq.com" } let(:serializers_factory) { Datadog::CI::TestVisibility::Serializers::Factories::TestLevel } + let(:max_payload_size) { 4 * 1024 * 1024 } let(:http) { spy(:http) } @@ -90,6 +98,19 @@ end end end + + context "when chunking is used" do + # one test event is approximately 1000 bytes currently + # ATTENTION: might break if more data is added to test spans in #produce_test_trace method + let(:max_payload_size) { 2000 } + + it "filters out invalid events" do + responses = subject.send_traces(traces) + + expect(http).to have_received(:request).twice + expect(responses.count).to eq(2) + end + end end context "when all events are invalid" do diff --git a/vendor/rbs/ddtrace/0/datadog/core/chunket.rbs b/vendor/rbs/ddtrace/0/datadog/core/chunket.rbs new file mode 100644 index 00000000..69b01010 --- /dev/null +++ b/vendor/rbs/ddtrace/0/datadog/core/chunket.rbs @@ -0,0 +1,7 @@ +module Datadog + module Core + module Chunker + def self?.chunk_by_size: (untyped list, untyped max_chunk_size) -> untyped + end + end +end From 3272d7ce02808dbdad402abb33a20e9793894453 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 28 Sep 2023 15:42:10 +0200 Subject: [PATCH 38/45] gzip encoding for citestcycle intake requests --- Steepfile | 1 + lib/datadog/ci/ext/transport.rb | 2 ++ lib/datadog/ci/test_visibility/transport.rb | 17 ++++++++-- lib/datadog/ci/transport/gzip.rb | 20 +++++++++++ lib/datadog/ci/transport/http.rb | 23 +++++++++++-- sig/datadog/ci/ext/transport.rbs | 4 +++ sig/datadog/ci/transport/gzip.rbs | 9 +++++ sig/datadog/ci/transport/http.rbs | 3 +- .../ci/test_visibility/transport_spec.rb | 3 +- spec/datadog/ci/transport/gzip_spec.rb | 34 +++++++++++++++++++ spec/datadog/ci/transport/http_spec.rb | 31 +++++++++++++++++ 11 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 lib/datadog/ci/transport/gzip.rb create mode 100644 sig/datadog/ci/transport/gzip.rbs create mode 100644 spec/datadog/ci/transport/gzip_spec.rb diff --git a/Steepfile b/Steepfile index 825e971e..ad68d9c9 100644 --- a/Steepfile +++ b/Steepfile @@ -4,6 +4,7 @@ target :lib do check "lib" ignore "lib/datadog/ci/configuration/settings.rb" + ignore "lib/datadog/ci/transport/gzip.rb" library "pathname" library "json" diff --git a/lib/datadog/ci/ext/transport.rb b/lib/datadog/ci/ext/transport.rb index 231d48b0..ece118eb 100644 --- a/lib/datadog/ci/ext/transport.rb +++ b/lib/datadog/ci/ext/transport.rb @@ -6,11 +6,13 @@ module Ext module Transport HEADER_DD_API_KEY = "DD-API-KEY" HEADER_CONTENT_TYPE = "Content-Type" + HEADER_CONTENT_ENCODING = "Content-Encoding" TEST_VISIBILITY_INTAKE_HOST_PREFIX = "citestcycle-intake" TEST_VISIBILITY_INTAKE_PATH = "/api/v2/citestcycle" CONTENT_TYPE_MESSAGEPACK = "application/msgpack" + CONTENT_ENCODING_GZIP = "gzip" end end end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index c513c161..60142b00 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -28,23 +28,36 @@ def initialize( @max_payload_size = max_payload_size @http = Datadog::CI::Transport::HTTP.new( host: "#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{site}", - port: 443 + port: 443, + compress: true ) end def send_traces(traces) return [] if traces.nil? || traces.empty? + Datadog.logger.debug { "Sending #{traces.count} traces..." } + encoded_events = encode_traces(traces) if encoded_events.empty? - Datadog.logger.debug("[TestVisibility::Transport] empty serializable events list, skipping send") + Datadog.logger.debug("Empty encoded events list, skipping send") return [] end responses = [] Datadog::Core::Chunker.chunk_by_size(encoded_events, @max_payload_size).map do |chunk| encoded_payload = pack_events(chunk) + Datadog.logger.debug do + "Send chunk of #{chunk.count} events; payload size #{encoded_payload.size}" + end + Datadog.logger.debug { encoded_payload } + response = send_payload(encoded_payload) + + Datadog.logger.debug do + "Received server response: #{response.inspect}" + end + responses << response end diff --git a/lib/datadog/ci/transport/gzip.rb b/lib/datadog/ci/transport/gzip.rb new file mode 100644 index 00000000..f8c9aa98 --- /dev/null +++ b/lib/datadog/ci/transport/gzip.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "zlib" +require "stringio" + +module Datadog + module CI + module Transport + module Gzip + module_function + + def compress(input) + gzip_writer = Zlib::GzipWriter.new(StringIO.new) + gzip_writer << input + gzip_writer.close.string + end + end + end + end +end diff --git a/lib/datadog/ci/transport/http.rb b/lib/datadog/ci/transport/http.rb index 4dd44172..a9759b8e 100644 --- a/lib/datadog/ci/transport/http.rb +++ b/lib/datadog/ci/transport/http.rb @@ -2,6 +2,9 @@ require "net/http" +require_relative "gzip" +require_relative "../ext/transport" + module Datadog module CI module Transport @@ -10,20 +13,36 @@ class HTTP :host, :port, :ssl, - :timeout + :timeout, + :compress DEFAULT_TIMEOUT = 30 - def initialize(host:, timeout: DEFAULT_TIMEOUT, port: nil, ssl: true) + def initialize(host:, timeout: DEFAULT_TIMEOUT, port: nil, ssl: true, compress: false) @host = host @port = port @timeout = timeout @ssl = ssl.nil? ? true : ssl + @compress = compress.nil? ? false : compress end def request(path:, payload:, headers:, method: "post") raise "Unknown method #{method}" unless respond_to?(method, true) + if compress + headers[Ext::Transport::HEADER_CONTENT_ENCODING] = Ext::Transport::CONTENT_ENCODING_GZIP + payload = Gzip.compress(payload) + end + + Datadog.logger.debug { "Sending #{method} request" } + Datadog.logger.debug { "host #{host}" } + Datadog.logger.debug { "port #{port}" } + Datadog.logger.debug { "ssl enabled #{ssl}" } + Datadog.logger.debug { "compression enabled #{compress}" } + Datadog.logger.debug { "path #{path}" } + Datadog.logger.debug { "headers #{headers}" } + Datadog.logger.debug { "payload size #{payload.size}" } + send(method, path: path, payload: payload, headers: headers) end diff --git a/sig/datadog/ci/ext/transport.rbs b/sig/datadog/ci/ext/transport.rbs index 00aacd2a..6c40b3b6 100644 --- a/sig/datadog/ci/ext/transport.rbs +++ b/sig/datadog/ci/ext/transport.rbs @@ -6,11 +6,15 @@ module Datadog HEADER_CONTENT_TYPE: "Content-Type" + HEADER_CONTENT_ENCODING: "Content-Encoding" + TEST_VISIBILITY_INTAKE_HOST_PREFIX: "citestcycle-intake" TEST_VISIBILITY_INTAKE_PATH: "/api/v2/citestcycle" CONTENT_TYPE_MESSAGEPACK: "application/msgpack" + + CONTENT_ENCODING_GZIP: "gzip" end end end diff --git a/sig/datadog/ci/transport/gzip.rbs b/sig/datadog/ci/transport/gzip.rbs new file mode 100644 index 00000000..303b96c1 --- /dev/null +++ b/sig/datadog/ci/transport/gzip.rbs @@ -0,0 +1,9 @@ +module Datadog + module CI + module Transport + module Gzip + def self?.compress: (String input) -> String + end + end + end +end diff --git a/sig/datadog/ci/transport/http.rbs b/sig/datadog/ci/transport/http.rbs index dc211264..bab7f203 100644 --- a/sig/datadog/ci/transport/http.rbs +++ b/sig/datadog/ci/transport/http.rbs @@ -6,10 +6,11 @@ module Datadog attr_reader port: Integer? attr_reader ssl: bool attr_reader timeout: Integer + attr_reader compress: bool DEFAULT_TIMEOUT: 30 - def initialize: (host: String, ?port: Integer?, ?ssl: bool, ?timeout: Integer) -> void + def initialize: (host: String, ?port: Integer?, ?ssl: bool, ?timeout: Integer, ?compress: bool) -> void def request: (?method: String, payload: String, headers: Hash[String, String], path: String) -> Response diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index 808bed0b..324f72b5 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -24,7 +24,8 @@ before do expect(Datadog::CI::Transport::HTTP).to receive(:new).with( host: "citestcycle-intake.datad0ghq.com", - port: 443 + port: 443, + compress: true ).and_return(http) end diff --git a/spec/datadog/ci/transport/gzip_spec.rb b/spec/datadog/ci/transport/gzip_spec.rb new file mode 100644 index 00000000..f51ca322 --- /dev/null +++ b/spec/datadog/ci/transport/gzip_spec.rb @@ -0,0 +1,34 @@ +require_relative "../../../../lib/datadog/ci/transport/gzip" + +RSpec.describe Datadog::CI::Transport::Gzip do + subject { described_class.compress(input) } + + describe ".compress" do + let(:input) do + <<-LOREM + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc et est eu dui dignissim tempus. Aliquam + scelerisque posuere odio id sollicitudin. Etiam dolor lorem, interdum sed mollis consectetur, sagittis a massa. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec gravida, libero ac gravida ornare, leo elit + facilisis nunc, in pharetra odio lectus sit amet augue. Cras fermentum interdum tortor, pulvinar laoreet massa + mollis non. Vestibulum pulvinar dolor nec ante facilisis, in scelerisque tortor maximus. + + Cras pellentesque odio at mauris venenatis efficitur. Mauris pretium, est eu convallis sagittis, felis purus ] + ullamcorper turpis, vel hendrerit justo massa at nulla. Maecenas hendrerit ante ligula. Maecenas blandit porta + magna. Proin volutpat vestibulum diam quis malesuada. Aliquam at porttitor turpis. Aliquam ut tellus ultricies, + commodo sapien vel, consequat felis. Aenean velit turpis, rhoncus in ipsum ut, tempor iaculis nisi. Fusce + faucibus consequat blandit. Nam maximus augue quis tellus cursus eleifend. Suspendisse auctor, orci sit amet + vehicula molestie, magna nibh rutrum metus, eget sagittis felis mauris eu quam. Vivamus ut vulputate est. + LOREM + end + + it "compresses" do + expect(input.size).to be > subject.size + end + + it "can be decompressed with gzip" do + Zlib::GzipReader.new(StringIO.new(subject)) do |gzip| + expect(gzip.read).to eq(input) + end + end + end +end diff --git a/spec/datadog/ci/transport/http_spec.rb b/spec/datadog/ci/transport/http_spec.rb index 91749508..09009b2e 100644 --- a/spec/datadog/ci/transport/http_spec.rb +++ b/spec/datadog/ci/transport/http_spec.rb @@ -68,6 +68,22 @@ it { is_expected.to have_attributes(ssl: false) } end end + + context "given a :compress option" do + let(:options) { {compress: compress} } + + context "with nil" do + let(:compress) { nil } + + it { is_expected.to have_attributes(compress: false) } + end + + context "with false" do + let(:compress) { true } + + it { is_expected.to have_attributes(compress: true) } + end + end end describe "#request" do @@ -103,6 +119,21 @@ it { expect { request }.to raise_error("Unknown method delete") } end + + context "when compressing payload" do + let(:headers) { {"Content-Type" => "application/json"} } + let(:expected_headers) { {"Content-Type" => "application/json", "Content-Encoding" => "gzip"} } + let(:options) { {compress: true} } + let(:post_request) { double(:post_request) } + + before do + expect(::Net::HTTP::Post).to receive(:new).with(path, expected_headers).and_return(post_request) + expect(post_request).to receive(:body=).with(Datadog::CI::Transport::Gzip.compress(payload)) + expect(http_connection).to receive(:request).with(post_request).and_return(http_response) + end + + it { expect(request.http_response).to be(http_response) } + end end end From d8cf27fe94266a906a211a32d0dd3888e5625dd4 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 28 Sep 2023 15:53:39 +0200 Subject: [PATCH 39/45] remove headers logging for security reasons; remove payload logs as payload is binary --- lib/datadog/ci/test_visibility/transport.rb | 1 - lib/datadog/ci/transport/http.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 60142b00..a8e73c56 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -50,7 +50,6 @@ def send_traces(traces) Datadog.logger.debug do "Send chunk of #{chunk.count} events; payload size #{encoded_payload.size}" end - Datadog.logger.debug { encoded_payload } response = send_payload(encoded_payload) diff --git a/lib/datadog/ci/transport/http.rb b/lib/datadog/ci/transport/http.rb index a9759b8e..87dc85fa 100644 --- a/lib/datadog/ci/transport/http.rb +++ b/lib/datadog/ci/transport/http.rb @@ -40,7 +40,6 @@ def request(path:, payload:, headers:, method: "post") Datadog.logger.debug { "ssl enabled #{ssl}" } Datadog.logger.debug { "compression enabled #{compress}" } Datadog.logger.debug { "path #{path}" } - Datadog.logger.debug { "headers #{headers}" } Datadog.logger.debug { "payload size #{payload.size}" } send(method, path: path, payload: payload, headers: headers) From d1b14956f43b6d0c7ca934a9a92b302a3ae5a21a Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 29 Sep 2023 10:17:35 +0200 Subject: [PATCH 40/45] agentless mode and additional configuration readme --- README.md | 66 ++++++++++++++++++++ docs/screenshots/test-trace-with-redis.png | Bin 0 -> 78387 bytes lib/datadog/ci/test_visibility/transport.rb | 2 +- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 docs/screenshots/test-trace-with-redis.png diff --git a/README.md b/README.md index 813430af..56ed4e0d 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,72 @@ end | `service_name` | Service name used for `cucumber` instrumentation. | `'cucumber'` | | `operation_name` | Operation name used for `cucumber` instrumentation. Useful if you want rename automatic trace metrics e.g. `trace.#{operation_name}.errors`. | `'cucumber.test'` | +## Agentless mode + +If you are using a cloud CI provider without access to the underlying worker nodes, such as GitHub Actions or CircleCI, configure the library to use the Agentless mode. For this, set the following environment variables: +`DD_CIVISIBILITY_AGENTLESS_ENABLED=true (Required)` and `DD_API_KEY=your_secret_api_key (Required)`. + +Additionally, configure which [Datadog site](https://docs.datadoghq.com/getting_started/site/) you want to send data to: +`DD_SITE=your.datadoghq.com` (datadoghq.com by default). + +Agentless mode can be enabled via `Datadog.configure` (but don't forget to set DD_API_KEY environment variable): + +```ruby +Datadog.configure { |c| c.ci.agentless_mode_enabled = true } +``` + +## Additional configuration + +### Add tracing instrumentations + +It can be useful to have rich tracing information about your tests that includes time spent performing database operations +or other external calls like here: + +![Test trace with redis instrumented](./docs/screenshots/test-trace-with-redis.png) + +In order to achieve this you can configure ddtrace instrumentations in your configure block: + +```ruby +Datadog.configure do |c| + # ... ci configs and instrumentation here ... + c.instrument :redis + c.instrument :pg +end +``` + +...or enable auto instrumentation in your test_helper/spec_helper: + +```ruby +require "ddtrace/auto_instrument" +``` + +Note: in CI mode these traces are going to be submitted to CI visibility backend, +they will **not** be submitted to Datadog APM. + +For the full list of available instrumentations see [ddtrace documentation](https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md) + +### Disabling startup logs + +Startup logs produce a report of tracing state when the application is initially configured. +These logs are activated by default in test mode, if you don't want them you can disable this +via `diagnostics.startup_logs.enabled = false` or `DD_TRACE_STARTUP_LOGS=0`. + +```ruby +Datadog.configure { |c| c.diagnostics.startup_logs.enabled = false } +``` + +### Enabling debug mode + +Switching the library into debug mode will produce verbose, detailed logs about tracing activity, including any suppressed errors. This output can be helpful in identifying errors, confirming trace output, or catching HTTP transport issues. + +You can enable this via `diagnostics.debug = true` or `DD_TRACE_DEBUG`. + +```ruby +Datadog.configure { |c| c.diagnostics.debug = true } +``` + +**We do NOT recommend use of this feature in production or other sensitive environments**, as it can be very verbose under load. It's best to use this in a controlled environment where you can control application load. + ## Contributing See [development guide](/docs/DevelopmentGuide.md), [static typing guide](docs/StaticTypingGuide.md) and [contributing guidelines](/CONTRIBUTING.md). diff --git a/docs/screenshots/test-trace-with-redis.png b/docs/screenshots/test-trace-with-redis.png new file mode 100644 index 0000000000000000000000000000000000000000..a9f1be953b0e4cc98c9239577c67e444167ef74c GIT binary patch literal 78387 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

3WaN0fSh`L3}K$qd4;g-)PsQXp7<*Rf(hC)~R# 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 z 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 Date: Fri, 29 Sep 2023 12:17:59 +0200 Subject: [PATCH 41/45] log when events are too large to be submitted --- lib/datadog/ci/test_visibility/transport.rb | 15 ++++++++++++--- spec/datadog/ci/test_visibility/transport_spec.rb | 12 ++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 8ed984d8..05725dad 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -14,8 +14,8 @@ module CI module TestVisibility class Transport # CI test cycle intake's limit is 5.1MB uncompressed - # We will use a bit more conservative value 4MB - DEFAULT_MAX_PAYLOAD_SIZE = 4 * 1024 * 1024 + # We will use a bit more conservative value 5MB + DEFAULT_MAX_PAYLOAD_SIZE = 5 * 1024 * 1024 def initialize( api_key:, @@ -92,7 +92,16 @@ def encode_span(trace, span) serializer = @serializers_factory.serializer(trace, span) if serializer.valid? - encoder.encode(serializer) + encoded = encoder.encode(serializer) + + if encoded.size > @max_payload_size + # This single event is too large, we can't flush it + Datadog.logger.debug { "Dropping test event. Payload too large: '#{span.inspect}'" } + + return nil + end + + encoded else Datadog.logger.debug { "Invalid span skipped: #{span}" } nil diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index 324f72b5..0c3740a6 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -112,6 +112,18 @@ expect(responses.count).to eq(2) end end + + context "when max_payload-size is too small" do + # one test event is approximately 1000 bytes currently + # ATTENTION: might break if more data is added to test spans in #produce_test_trace method + let(:max_payload_size) { 1 } + + it "does not send events that are larger than max size" do + subject.send_traces(traces) + + expect(http).not_to have_received(:request) + end + end end context "when all events are invalid" do From 6cc85b63f36f841b560852c24c487ec47696a759 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 29 Sep 2023 15:49:16 +0200 Subject: [PATCH 42/45] make agentless intake URL configurable in order to use it in test-environment stand --- lib/datadog/ci/configuration/components.rb | 9 +++- lib/datadog/ci/configuration/settings.rb | 5 +++ lib/datadog/ci/ext/settings.rb | 1 + lib/datadog/ci/test_visibility/transport.rb | 30 ++++++++++---- sig/datadog/ci/ext/settings.rbs | 2 +- sig/datadog/ci/test_visibility/transport.rbs | 7 +++- .../ci/configuration/components_spec.rb | 41 +++++++++++++++++++ .../datadog/ci/configuration/settings_spec.rb | 35 ++++++++++++++++ .../ci/test_visibility/transport_spec.rb | 5 ++- 9 files changed, 122 insertions(+), 13 deletions(-) diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index 803e6dbe..931e929c 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -34,7 +34,14 @@ def activate_ci!(settings) settings.ci.enabled = false return else - agentless_transport = Datadog::CI::TestVisibility::Transport.new(api_key: settings.api_key) + dd_site = settings.site || "datadoghq.com" + agentless_url = settings.ci.agentless_url || + "https://#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{dd_site}:443" + + agentless_transport = Datadog::CI::TestVisibility::Transport.new( + api_key: settings.api_key, + url: agentless_url + ) end end diff --git a/lib/datadog/ci/configuration/settings.rb b/lib/datadog/ci/configuration/settings.rb index bf967ca4..eb92f4b9 100644 --- a/lib/datadog/ci/configuration/settings.rb +++ b/lib/datadog/ci/configuration/settings.rb @@ -29,6 +29,11 @@ def self.add_settings!(base) o.default false end + option :agentless_url do |o| + o.type :string, nilable: true + o.env CI::Ext::Settings::ENV_AGENTLESS_URL + end + define_method(:instrument) do |integration_name, options = {}, &block| return unless enabled diff --git a/lib/datadog/ci/ext/settings.rb b/lib/datadog/ci/ext/settings.rb index 2de83aad..c08a5f56 100644 --- a/lib/datadog/ci/ext/settings.rb +++ b/lib/datadog/ci/ext/settings.rb @@ -7,6 +7,7 @@ module Ext module Settings ENV_MODE_ENABLED = "DD_TRACE_CI_ENABLED" ENV_AGENTLESS_MODE_ENABLED = "DD_CIVISIBILITY_AGENTLESS_ENABLED" + ENV_AGENTLESS_URL = "DD_CIVISIBILITY_AGENTLESS_URL" end end end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index 05725dad..f8668f1b 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require "msgpack" +require "uri" + require "datadog/core/encoding" require "datadog/core/environment/identity" require "datadog/core/chunker" @@ -17,18 +19,29 @@ class Transport # We will use a bit more conservative value 5MB DEFAULT_MAX_PAYLOAD_SIZE = 5 * 1024 * 1024 + attr_reader :serializers_factory, + :api_key, + :max_payload_size, + :http + def initialize( api_key:, - site: "datadoghq.com", + url:, serializers_factory: Datadog::CI::TestVisibility::Serializers::Factories::TestLevel, max_payload_size: DEFAULT_MAX_PAYLOAD_SIZE ) @serializers_factory = serializers_factory @api_key = api_key @max_payload_size = max_payload_size + + uri = URI.parse(url) + + raise "Invalid agentless mode URL: #{url}" if uri.host.nil? + @http = Datadog::CI::Transport::HTTP.new( - host: "#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{site}", - port: 443, + host: uri.host, + port: uri.port, + ssl: uri.scheme == "https" || uri.port == 443, compress: true ) end @@ -45,7 +58,7 @@ def send_traces(traces) end responses = [] - Datadog::Core::Chunker.chunk_by_size(encoded_events, @max_payload_size).map do |chunk| + Datadog::Core::Chunker.chunk_by_size(encoded_events, max_payload_size).map do |chunk| encoded_payload = pack_events(chunk) Datadog.logger.debug do "Send chunk of #{chunk.count} events; payload size #{encoded_payload.size}" @@ -66,11 +79,11 @@ def send_traces(traces) private def send_payload(encoded_payload) - @http.request( + http.request( path: Datadog::CI::Ext::Transport::TEST_VISIBILITY_INTAKE_PATH, payload: encoded_payload, headers: { - Ext::Transport::HEADER_DD_API_KEY => @api_key, + Ext::Transport::HEADER_DD_API_KEY => api_key, Ext::Transport::HEADER_CONTENT_TYPE => Ext::Transport::CONTENT_TYPE_MESSAGEPACK } ) @@ -89,14 +102,15 @@ def encode_traces(traces) end def encode_span(trace, span) - serializer = @serializers_factory.serializer(trace, span) + serializer = serializers_factory.serializer(trace, span) if serializer.valid? encoded = encoder.encode(serializer) - if encoded.size > @max_payload_size + if encoded.size > max_payload_size # This single event is too large, we can't flush it Datadog.logger.debug { "Dropping test event. Payload too large: '#{span.inspect}'" } + Datadog.logger.debug { encoded } return nil end diff --git a/sig/datadog/ci/ext/settings.rbs b/sig/datadog/ci/ext/settings.rbs index afa71d4c..5d76a0cd 100644 --- a/sig/datadog/ci/ext/settings.rbs +++ b/sig/datadog/ci/ext/settings.rbs @@ -4,7 +4,7 @@ module Datadog module Settings ENV_MODE_ENABLED: String ENV_AGENTLESS_MODE_ENABLED: String - ENV_API_KEY: String + ENV_AGENTLESS_URL: String end end end diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs index 4a7d57f6..15be2769 100644 --- a/sig/datadog/ci/test_visibility/transport.rbs +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -4,6 +4,11 @@ module Datadog class Transport DEFAULT_MAX_PAYLOAD_SIZE: Integer + attr_reader serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel) + attr_reader api_key: String + attr_reader http: Datadog::CI::Transport::HTTP + attr_reader max_payload_size: Integer + @api_key: String @http: Datadog::CI::Transport::HTTP @serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel) @@ -11,7 +16,7 @@ module Datadog def initialize: ( api_key: String, - ?site: ::String, + url: ::String, ?serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel), ?max_payload_size: Integer ) -> void diff --git a/spec/datadog/ci/configuration/components_spec.rb b/spec/datadog/ci/configuration/components_spec.rb index 7834cdb0..9f5283d2 100644 --- a/spec/datadog/ci/configuration/components_spec.rb +++ b/spec/datadog/ci/configuration/components_spec.rb @@ -42,6 +42,14 @@ .to receive(:agentless_mode_enabled) .and_return(agentless_enabled) + allow(settings.ci) + .to receive(:agentless_url) + .and_return(agentless_url) + + allow(settings) + .to receive(:site) + .and_return(dd_site) + allow(settings) .to receive(:api_key) .and_return(api_key) @@ -69,6 +77,8 @@ end let(:api_key) { nil } + let(:agentless_url) { nil } + let(:dd_site) { nil } context "is enabled" do let(:enabled) { true } @@ -110,6 +120,37 @@ expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| expect(options[:transport]).to be_kind_of(Datadog::CI::TestVisibility::Transport) expect(options[:shutdown_timeout]).to eq(60) + + http_client = options[:transport].http + expect(http_client.host).to eq("citestcycle-intake.datadoghq.com") + expect(http_client.port).to eq(443) + expect(http_client.ssl).to eq(true) + end + end + + context "when agentless_url is provided" do + let(:agentless_url) { "http://localhost:5555" } + + it "configures transport to use intake URL from settings" do + expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| + http_client = options[:transport].http + expect(http_client.host).to eq("localhost") + expect(http_client.port).to eq(5555) + expect(http_client.ssl).to eq(false) + end + end + end + + context "when dd_site is provided" do + let(:dd_site) { "eu.datadoghq.com" } + + it "construct intake url using provided host" do + expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| + http_client = options[:transport].http + expect(http_client.host).to eq("citestcycle-intake.eu.datadoghq.com") + expect(http_client.port).to eq(443) + expect(http_client.ssl).to eq(true) + end end end end diff --git a/spec/datadog/ci/configuration/settings_spec.rb b/spec/datadog/ci/configuration/settings_spec.rb index 8f6b5152..3ebafdfd 100644 --- a/spec/datadog/ci/configuration/settings_spec.rb +++ b/spec/datadog/ci/configuration/settings_spec.rb @@ -137,6 +137,41 @@ def patcher end end + describe "#agentless_url" do + subject(:agentless_url) { settings.ci.agentless_url } + + it { is_expected.to be nil } + + context "when #{Datadog::CI::Ext::Settings::ENV_AGENTLESS_URL}" do + around do |example| + ClimateControl.modify(Datadog::CI::Ext::Settings::ENV_AGENTLESS_URL => agentless_url) do + example.run + end + end + + context "is not defined" do + let(:agentless_url) { nil } + + it { is_expected.to be nil } + end + + context "is set to some value" do + let(:agentless_url) { "example.com" } + + it { is_expected.to eq agentless_url } + end + end + end + + describe "#agentless_url=" do + it "updates the #enabled setting" do + expect { settings.ci.agentless_url = "example.com" } + .to change { settings.ci.agentless_url } + .from(nil) + .to("example.com") + end + end + describe "#instrument" do let(:integration_name) { :fake } diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index 0c3740a6..89cab458 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -8,14 +8,14 @@ subject do described_class.new( api_key: api_key, - site: site, + url: url, serializers_factory: serializers_factory, max_payload_size: max_payload_size ) end let(:api_key) { "api_key" } - let(:site) { "datad0ghq.com" } + let(:url) { "https://citestcycle-intake.datad0ghq.com:443" } let(:serializers_factory) { Datadog::CI::TestVisibility::Serializers::Factories::TestLevel } let(:max_payload_size) { 4 * 1024 * 1024 } @@ -25,6 +25,7 @@ expect(Datadog::CI::Transport::HTTP).to receive(:new).with( host: "citestcycle-intake.datad0ghq.com", port: 443, + ssl: true, compress: true ).and_return(http) end From 98b0f06146b8e60f72db37abc99d1ab47baf515c Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 2 Oct 2023 13:33:49 +0200 Subject: [PATCH 43/45] cleanup and code review suggestions --- README.md | 10 ++++------ lib/datadog/ci/transport/http.rb | 11 ++++------- spec/datadog/ci/configuration/settings_spec.rb | 3 --- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 56ed4e0d..0a28c209 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ If you are using a cloud CI provider without access to the underlying worker nod Additionally, configure which [Datadog site](https://docs.datadoghq.com/getting_started/site/) you want to send data to: `DD_SITE=your.datadoghq.com` (datadoghq.com by default). -Agentless mode can be enabled via `Datadog.configure` (but don't forget to set DD_API_KEY environment variable): +Agentless mode can also be enabled via `Datadog.configure` (but don't forget to set DD_API_KEY environment variable): ```ruby Datadog.configure { |c| c.ci.agentless_mode_enabled = true } @@ -156,8 +156,8 @@ end require "ddtrace/auto_instrument" ``` -Note: in CI mode these traces are going to be submitted to CI visibility backend, -they will **not** be submitted to Datadog APM. +Note: in CI mode these traces are going to be submitted to CI Visibility, +they will **not** show up in Datadog APM. For the full list of available instrumentations see [ddtrace documentation](https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md) @@ -175,14 +175,12 @@ Datadog.configure { |c| c.diagnostics.startup_logs.enabled = false } Switching the library into debug mode will produce verbose, detailed logs about tracing activity, including any suppressed errors. This output can be helpful in identifying errors, confirming trace output, or catching HTTP transport issues. -You can enable this via `diagnostics.debug = true` or `DD_TRACE_DEBUG`. +You can enable this via `diagnostics.debug = true` or `DD_TRACE_DEBUG=1`. ```ruby Datadog.configure { |c| c.diagnostics.debug = true } ``` -**We do NOT recommend use of this feature in production or other sensitive environments**, as it can be very verbose under load. It's best to use this in a controlled environment where you can control application load. - ## Contributing See [development guide](/docs/DevelopmentGuide.md), [static typing guide](docs/StaticTypingGuide.md) and [contributing guidelines](/CONTRIBUTING.md). diff --git a/lib/datadog/ci/transport/http.rb b/lib/datadog/ci/transport/http.rb index 87dc85fa..a59ffb2f 100644 --- a/lib/datadog/ci/transport/http.rb +++ b/lib/datadog/ci/transport/http.rb @@ -34,13 +34,10 @@ def request(path:, payload:, headers:, method: "post") payload = Gzip.compress(payload) end - Datadog.logger.debug { "Sending #{method} request" } - Datadog.logger.debug { "host #{host}" } - Datadog.logger.debug { "port #{port}" } - Datadog.logger.debug { "ssl enabled #{ssl}" } - Datadog.logger.debug { "compression enabled #{compress}" } - Datadog.logger.debug { "path #{path}" } - Datadog.logger.debug { "payload size #{payload.size}" } + Datadog.logger.debug do + "Sending #{method} request: host=#{host}; port=#{port}; ssl_enabled=#{ssl}; " \ + "compression_enabled=#{compress}; path=#{path}; payload_size=#{payload.size}" + end send(method, path: path, payload: payload, headers: headers) end diff --git a/spec/datadog/ci/configuration/settings_spec.rb b/spec/datadog/ci/configuration/settings_spec.rb index 3ebafdfd..e58fcab7 100644 --- a/spec/datadog/ci/configuration/settings_spec.rb +++ b/spec/datadog/ci/configuration/settings_spec.rb @@ -238,9 +238,6 @@ def patcher end context "when not enabled" do - # before do - # allow(integration.configuration).to receive(:enabled).and_return(false) - # end let(:enabled) { false } it "does not patch the integration" do From d69bfefc9755806a4812e60a7616d5e204b6b5a8 Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Mon, 2 Oct 2023 16:42:19 +0200 Subject: [PATCH 44/45] add env tag to metadata --- lib/datadog/ci/configuration/components.rb | 3 ++- lib/datadog/ci/test_visibility/transport.rb | 13 ++++++++++-- sig/datadog/ci/test_visibility/transport.rbs | 3 +++ .../ci/test_visibility/transport_spec.rb | 20 +++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index 931e929c..fd8c209d 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -40,7 +40,8 @@ def activate_ci!(settings) agentless_transport = Datadog::CI::TestVisibility::Transport.new( api_key: settings.api_key, - url: agentless_url + url: agentless_url, + env: settings.env ) end end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index f8668f1b..472280b8 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -22,17 +22,20 @@ class Transport attr_reader :serializers_factory, :api_key, :max_payload_size, - :http + :http, + :env def initialize( api_key:, url:, + env: nil, serializers_factory: Datadog::CI::TestVisibility::Serializers::Factories::TestLevel, max_payload_size: DEFAULT_MAX_PAYLOAD_SIZE ) @serializers_factory = serializers_factory @api_key = api_key @max_payload_size = max_payload_size + @env = env uri = URI.parse(url) @@ -138,7 +141,13 @@ def pack_events(encoded_events) packer.write_map_header(1) packer.write("*") - packer.write_map_header(3) + metadata_fields_count = env ? 4 : 3 + packer.write_map_header(metadata_fields_count) + + if env + packer.write("env") + packer.write(env) + end packer.write("runtime-id") packer.write(Datadog::Core::Environment::Identity.id) diff --git a/sig/datadog/ci/test_visibility/transport.rbs b/sig/datadog/ci/test_visibility/transport.rbs index 15be2769..c1058380 100644 --- a/sig/datadog/ci/test_visibility/transport.rbs +++ b/sig/datadog/ci/test_visibility/transport.rbs @@ -6,10 +6,12 @@ module Datadog attr_reader serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel) attr_reader api_key: String + attr_reader env: String? attr_reader http: Datadog::CI::Transport::HTTP attr_reader max_payload_size: Integer @api_key: String + @env: String? @http: Datadog::CI::Transport::HTTP @serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel) @max_payload_size: Integer @@ -17,6 +19,7 @@ module Datadog def initialize: ( api_key: String, url: ::String, + ?env: ::String?, ?serializers_factory: singleton(Datadog::CI::TestVisibility::Serializers::Factories::TestLevel), ?max_payload_size: Integer ) -> void diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index 89cab458..a775da46 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -8,6 +8,7 @@ subject do described_class.new( api_key: api_key, + env: env, url: url, serializers_factory: serializers_factory, max_payload_size: max_payload_size @@ -15,6 +16,7 @@ end let(:api_key) { "api_key" } + let(:env) { nil } let(:url) { "https://citestcycle-intake.datad0ghq.com:443" } let(:serializers_factory) { Datadog::CI::TestVisibility::Serializers::Factories::TestLevel } let(:max_payload_size) { 4 * 1024 * 1024 } @@ -60,6 +62,24 @@ end end + context "with env defined" do + let(:env) { "ci" } + before do + produce_test_trace + end + + it "sends correct payload including env" do + subject.send_traces([trace]) + + expect(http).to have_received(:request) do |args| + payload = MessagePack.unpack(args[:payload]) + + metadata = payload["metadata"]["*"] + expect(metadata["env"]).to eq("ci") + end + end + end + context "multiple traces with 2 spans each" do let(:traces_count) { 2 } let(:expected_events_count) { 4 } From db394f03fea45d090661476dd3667b11be2b385b Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Thu, 5 Oct 2023 11:01:40 +0200 Subject: [PATCH 45/45] extract build_agentless_transport method --- lib/datadog/ci/configuration/components.rb | 22 ++++++++++++--------- sig/datadog/ci/configuration/components.rbs | 2 ++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index fd8c209d..6108b48d 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -34,15 +34,7 @@ def activate_ci!(settings) settings.ci.enabled = false return else - dd_site = settings.site || "datadoghq.com" - agentless_url = settings.ci.agentless_url || - "https://#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{dd_site}:443" - - agentless_transport = Datadog::CI::TestVisibility::Transport.new( - api_key: settings.api_key, - url: agentless_url, - env: settings.env - ) + agentless_transport = build_agentless_transport(settings) end end @@ -68,6 +60,18 @@ def activate_ci!(settings) settings.tracing.test_mode.writer_options = writer_options end + + def build_agentless_transport(settings) + dd_site = settings.site || "datadoghq.com" + agentless_url = settings.ci.agentless_url || + "https://#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{dd_site}:443" + + Datadog::CI::TestVisibility::Transport.new( + api_key: settings.api_key, + url: agentless_url, + env: settings.env + ) + end end end end diff --git a/sig/datadog/ci/configuration/components.rbs b/sig/datadog/ci/configuration/components.rbs index c716f604..6d7569e6 100644 --- a/sig/datadog/ci/configuration/components.rbs +++ b/sig/datadog/ci/configuration/components.rbs @@ -5,6 +5,8 @@ module Datadog def initialize: (untyped settings) -> void def activate_ci!: (untyped settings) -> untyped + + def build_agentless_transport: (untyped settings) -> Datadog::CI::TestVisibility::Transport end end end