Loading lib/labkit/covered_experience.rb +18 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ require 'labkit/covered_experience/error' require 'labkit/covered_experience/experience' require 'labkit/covered_experience/null' require 'labkit/covered_experience/registry' require 'labkit/logging/json_logger' module Labkit # Labkit::CoveredExperience namespace module. Loading @@ -12,7 +13,24 @@ module Labkit # specific events or activities within the application that are measured # and reported for performance monitoring and analysis. module CoveredExperience # Configuration class for CoveredExperience class Configuration attr_accessor :logger def initialize @logger = Labkit::Logging::JsonLogger.new($stdout) end end class << self def configuration @configuration ||= Configuration.new end def configure yield(configuration) if block_given? end def registry @registry ||= Registry.new end Loading lib/labkit/covered_experience/README.md +14 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,20 @@ This module covers the definition for Covered Experiences, as described in the [ ## Configuration ### Logger Configuration By default, `Labkit::CoveredExperience` uses `Labkit::Logging::JsonLogger.new($stdout)` for logging. You can configure a custom logger: ```ruby Labkit::CoveredExperience.configure do |config| config.logger = Labkit::Logging::JsonLogger.new($stdout) end ``` This configuration affects all Covered Experience instances and their logging output. ### Covered Experience Definitions Covered experience definitions will be lazy loaded from the default directory (`config/covered_experiences`). Create a new covered experience file in the registry directory, e.g. config/covered_experiences/merge_request_creation.yaml Loading lib/labkit/covered_experience/experience.rb +1 −2 Original line number Diff line number Diff line Loading @@ -2,7 +2,6 @@ require 'labkit/context' require 'labkit/covered_experience/error' require 'labkit/logging/json_logger' module Labkit module CoveredExperience Loading Loading @@ -186,7 +185,7 @@ module Labkit end def logger @logger ||= Labkit::Logging::JsonLogger.new($stdout) Labkit::CoveredExperience.configuration.logger end end end Loading lib/labkit/covered_experience/registry.rb +1 −2 Original line number Diff line number Diff line Loading @@ -4,7 +4,6 @@ require 'forwardable' require 'json-schema' require 'pathname' require 'yaml' require 'labkit/logging/json_logger' module Labkit module CoveredExperience Loading Loading @@ -99,7 +98,7 @@ module Labkit end def logger @logger ||= Labkit::Logging::JsonLogger.new($stdout) Labkit::CoveredExperience.configuration.logger end end end Loading lib/labkit/rspec/matchers/covered_experience_matchers.rb +38 −26 Original line number Diff line number Diff line # frozen_string_literal: true require 'json' # RSpec matchers for testing Labkit CoveredExperience functionality # # This file must be explicitly required in your test setup: Loading Loading @@ -32,27 +34,43 @@ module Labkit end def logger ::RSpec.current_example.metadata[:logger] ||= begin instance = Labkit::Logging::JsonLogger.new($stdout) allow(Labkit::Logging::JsonLogger).to receive(:new).with($stdout).and_return(instance) # rubocop:disable CodeReuse/ActiveRecord -- false positive instance end Labkit::CoveredExperience.configuration.logger end def allow_logger_call(covered_experience_id, checkpoint_type, extra = {}) return unless logger def expect_log_for(covered_experience_id, event_type, extra_args = {}) # Get the current logger's output (assumes StringIO for testing) log_output = logger.instance_variable_get(:@logdev).dev raise 'Logger does not using StringIO for test' unless log_output.respond_to?(:string) log_content = log_output.string expected_log_data = { **extra, checkpoint: checkpoint_type, # always present values expected_attrs = { checkpoint: event_type, **attributes(covered_experience_id), start_time: be_a(Time), elapsed: be_a(Numeric), urgency_threshold_in_seconds: be_a(Numeric) **extra_args } allow(logger).to receive(:info).with(hash_including(expected_log_data)).and_call_original # rubocop:disable CodeReuse/ActiveRecord -- false positive # Parse each log line and check if the log line with the set of expected attributes # is present and matches the expected attributes log_content.split("\n").any? do |line| next if line.strip.empty? begin log_entry = JSON.parse(line, symbolize_names: true) expected_attrs.all? do |key, value| case value when Class log_entry[key].is_a?(value) when Regexp log_entry[key]&.match?(value) else log_entry[key] == value end end rescue JSON::ParserError false end end end end end Loading Loading @@ -81,15 +99,13 @@ RSpec::Matchers.define :start_covered_experience do |covered_experience_id| checkpoint_before = checkpoint_counter&.get(labels.merge(checkpoint: "start")).to_i allow_logger_call(covered_experience_id, "start") actual.call checkpoint_after = checkpoint_counter&.get(labels.merge(checkpoint: "start")).to_i @checkpoint_change = checkpoint_after - checkpoint_before @checkpoint_change == 1 @checkpoint_change == 1 && expect_log_for(covered_experience_id, "start") end failure_message do Loading Loading @@ -120,14 +136,12 @@ RSpec::Matchers.define :checkpoint_covered_experience do |covered_experience_id| checkpoint_before = checkpoint_counter&.get(labels.merge(checkpoint: "intermediate")).to_i allow_logger_call(covered_experience_id, "intermediate", checkpoint_time: be_a(Time)) actual.call checkpoint_after = checkpoint_counter&.get(labels.merge(checkpoint: "intermediate")).to_i @checkpoint_change = checkpoint_after - checkpoint_before @checkpoint_change == 1 @checkpoint_change == 1 && expect_log_for(covered_experience_id, "intermediate") end failure_message do Loading Loading @@ -182,10 +196,6 @@ RSpec::Matchers.define :complete_covered_experience do |covered_experience_id, e total_before = total_counter&.get(labels.merge(error: error)).to_i apdex_before = apdex_counter&.get(labels.merge(success: success)).to_i attrs = { end_time: be_a(Time) } attrs.merge!(error: true, error_message: be_a(String)) if error allow_logger_call(covered_experience_id, "end", attrs) actual.call checkpoint_after = checkpoint_counter&.get(labels.merge(checkpoint: "end")).to_i Loading @@ -195,7 +205,9 @@ RSpec::Matchers.define :complete_covered_experience do |covered_experience_id, e @total_change = total_after - total_before @apdex_change = apdex_after - apdex_before @checkpoint_change == 1 && @total_change == 1 && @apdex_change == (error ? 0 : 1) expected_log_attrs = error ? { error: true } : {} @checkpoint_change == 1 && @total_change == 1 && @apdex_change == (error ? 0 : 1) && expect_log_for(covered_experience_id, "end", expected_log_attrs) end failure_message do Loading Loading
lib/labkit/covered_experience.rb +18 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ require 'labkit/covered_experience/error' require 'labkit/covered_experience/experience' require 'labkit/covered_experience/null' require 'labkit/covered_experience/registry' require 'labkit/logging/json_logger' module Labkit # Labkit::CoveredExperience namespace module. Loading @@ -12,7 +13,24 @@ module Labkit # specific events or activities within the application that are measured # and reported for performance monitoring and analysis. module CoveredExperience # Configuration class for CoveredExperience class Configuration attr_accessor :logger def initialize @logger = Labkit::Logging::JsonLogger.new($stdout) end end class << self def configuration @configuration ||= Configuration.new end def configure yield(configuration) if block_given? end def registry @registry ||= Registry.new end Loading
lib/labkit/covered_experience/README.md +14 −0 Original line number Diff line number Diff line Loading @@ -4,6 +4,20 @@ This module covers the definition for Covered Experiences, as described in the [ ## Configuration ### Logger Configuration By default, `Labkit::CoveredExperience` uses `Labkit::Logging::JsonLogger.new($stdout)` for logging. You can configure a custom logger: ```ruby Labkit::CoveredExperience.configure do |config| config.logger = Labkit::Logging::JsonLogger.new($stdout) end ``` This configuration affects all Covered Experience instances and their logging output. ### Covered Experience Definitions Covered experience definitions will be lazy loaded from the default directory (`config/covered_experiences`). Create a new covered experience file in the registry directory, e.g. config/covered_experiences/merge_request_creation.yaml Loading
lib/labkit/covered_experience/experience.rb +1 −2 Original line number Diff line number Diff line Loading @@ -2,7 +2,6 @@ require 'labkit/context' require 'labkit/covered_experience/error' require 'labkit/logging/json_logger' module Labkit module CoveredExperience Loading Loading @@ -186,7 +185,7 @@ module Labkit end def logger @logger ||= Labkit::Logging::JsonLogger.new($stdout) Labkit::CoveredExperience.configuration.logger end end end Loading
lib/labkit/covered_experience/registry.rb +1 −2 Original line number Diff line number Diff line Loading @@ -4,7 +4,6 @@ require 'forwardable' require 'json-schema' require 'pathname' require 'yaml' require 'labkit/logging/json_logger' module Labkit module CoveredExperience Loading Loading @@ -99,7 +98,7 @@ module Labkit end def logger @logger ||= Labkit::Logging::JsonLogger.new($stdout) Labkit::CoveredExperience.configuration.logger end end end Loading
lib/labkit/rspec/matchers/covered_experience_matchers.rb +38 −26 Original line number Diff line number Diff line # frozen_string_literal: true require 'json' # RSpec matchers for testing Labkit CoveredExperience functionality # # This file must be explicitly required in your test setup: Loading Loading @@ -32,27 +34,43 @@ module Labkit end def logger ::RSpec.current_example.metadata[:logger] ||= begin instance = Labkit::Logging::JsonLogger.new($stdout) allow(Labkit::Logging::JsonLogger).to receive(:new).with($stdout).and_return(instance) # rubocop:disable CodeReuse/ActiveRecord -- false positive instance end Labkit::CoveredExperience.configuration.logger end def allow_logger_call(covered_experience_id, checkpoint_type, extra = {}) return unless logger def expect_log_for(covered_experience_id, event_type, extra_args = {}) # Get the current logger's output (assumes StringIO for testing) log_output = logger.instance_variable_get(:@logdev).dev raise 'Logger does not using StringIO for test' unless log_output.respond_to?(:string) log_content = log_output.string expected_log_data = { **extra, checkpoint: checkpoint_type, # always present values expected_attrs = { checkpoint: event_type, **attributes(covered_experience_id), start_time: be_a(Time), elapsed: be_a(Numeric), urgency_threshold_in_seconds: be_a(Numeric) **extra_args } allow(logger).to receive(:info).with(hash_including(expected_log_data)).and_call_original # rubocop:disable CodeReuse/ActiveRecord -- false positive # Parse each log line and check if the log line with the set of expected attributes # is present and matches the expected attributes log_content.split("\n").any? do |line| next if line.strip.empty? begin log_entry = JSON.parse(line, symbolize_names: true) expected_attrs.all? do |key, value| case value when Class log_entry[key].is_a?(value) when Regexp log_entry[key]&.match?(value) else log_entry[key] == value end end rescue JSON::ParserError false end end end end end Loading Loading @@ -81,15 +99,13 @@ RSpec::Matchers.define :start_covered_experience do |covered_experience_id| checkpoint_before = checkpoint_counter&.get(labels.merge(checkpoint: "start")).to_i allow_logger_call(covered_experience_id, "start") actual.call checkpoint_after = checkpoint_counter&.get(labels.merge(checkpoint: "start")).to_i @checkpoint_change = checkpoint_after - checkpoint_before @checkpoint_change == 1 @checkpoint_change == 1 && expect_log_for(covered_experience_id, "start") end failure_message do Loading Loading @@ -120,14 +136,12 @@ RSpec::Matchers.define :checkpoint_covered_experience do |covered_experience_id| checkpoint_before = checkpoint_counter&.get(labels.merge(checkpoint: "intermediate")).to_i allow_logger_call(covered_experience_id, "intermediate", checkpoint_time: be_a(Time)) actual.call checkpoint_after = checkpoint_counter&.get(labels.merge(checkpoint: "intermediate")).to_i @checkpoint_change = checkpoint_after - checkpoint_before @checkpoint_change == 1 @checkpoint_change == 1 && expect_log_for(covered_experience_id, "intermediate") end failure_message do Loading Loading @@ -182,10 +196,6 @@ RSpec::Matchers.define :complete_covered_experience do |covered_experience_id, e total_before = total_counter&.get(labels.merge(error: error)).to_i apdex_before = apdex_counter&.get(labels.merge(success: success)).to_i attrs = { end_time: be_a(Time) } attrs.merge!(error: true, error_message: be_a(String)) if error allow_logger_call(covered_experience_id, "end", attrs) actual.call checkpoint_after = checkpoint_counter&.get(labels.merge(checkpoint: "end")).to_i Loading @@ -195,7 +205,9 @@ RSpec::Matchers.define :complete_covered_experience do |covered_experience_id, e @total_change = total_after - total_before @apdex_change = apdex_after - apdex_before @checkpoint_change == 1 && @total_change == 1 && @apdex_change == (error ? 0 : 1) expected_log_attrs = error ? { error: true } : {} @checkpoint_change == 1 && @total_change == 1 && @apdex_change == (error ? 0 : 1) && expect_log_for(covered_experience_id, "end", expected_log_attrs) end failure_message do Loading