From 3dbb569a04c83b34a9952e8cbc19969f8b1eb4b9 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:00:46 +0300 Subject: [PATCH 01/22] Instrument Servactory's `call` and `call!` methods with OpenTelemetry **Summary of changes:** - Implemented `Callable` patch to trace `call` and `call!` methods. - Traces span attributes (`code.namespace`, `code.function`, result statuses). - Handles success and failure states, including exceptions. - Integrated result recording for spans, supporting both success and failure cases. - Added methods to track input and output attribute names for spans. - Developed unit tests in `callable_spec.rb`: - Validates span creation, attributes, and status for successful and failing services. - Confirms exception handling and span updates for unexpected errors. --- .../servactory/patches/callable.rb | 128 ++++++++++++++ .../servactory/patches/callable_spec.rb | 164 ++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 lib/opentelemetry/instrumentation/servactory/patches/callable.rb create mode 100644 spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb diff --git a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb new file mode 100644 index 0000000..5a67f4c --- /dev/null +++ b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +module OpenTelemetry + module Instrumentation + module Servactory + module Patches + module Callable + def call(arguments = {}) + service_name = name || "AnonymousService" + attributes = build_span_attributes(service_name, "call") + + tracer.in_span("#{service_name} call", attributes:, kind: :internal) do |span| + result = super + record_result(span, result) + result + end + end + + def call!(arguments = {}) # rubocop:disable Metrics/MethodLength + service_name = name || "AnonymousService" + attributes = build_span_attributes(service_name, "call!") + + span = tracer.start_span("#{service_name} call!", attributes:, kind: :internal) + OpenTelemetry::Trace.with_span(span) do + result = super + mark_span_success(span) + result + rescue StandardError => e + record_exception_on_span(span, e) + raise + ensure + span.finish + end + end + + private + + def build_span_attributes(service_name, method_name) + attributes = { "code.namespace" => service_name, "code.function" => method_name } + append_attribute_names(attributes) + attributes + end + + def append_attribute_names(attributes) + config = OpenTelemetry::Instrumentation::Servactory::Instrumentation.instance.config + + if config[:record_input_names] + names = input_names + attributes["servactory.input_names"] = names unless names.empty? + end + + return unless config[:record_output_names] + + names = output_names + attributes["servactory.output_names"] = names unless names.empty? + end + + def input_names + send(:collection_of_inputs).names.map(&:to_s) + rescue StandardError + [] + end + + def output_names + send(:collection_of_outputs).names.map(&:to_s) + rescue StandardError + [] + end + + def record_result(span, result) + if result.respond_to?(:failure?) && result.failure? + record_failure_on_span(span, result) + span.set_attribute("servactory.result", "failure") + else + mark_span_success(span) + end + end + + def mark_span_success(span) + span.set_attribute("servactory.result", "success") + span.status = OpenTelemetry::Trace::Status.ok + end + + def record_failure_on_span(span, result) + error = result.respond_to?(:error) ? result.error : nil + message = error_message_for(error) + + span.add_event("servactory.failure", attributes: failure_attributes(error)) + span.status = OpenTelemetry::Trace::Status.error(message) + end + + def record_exception_on_span(span, exception) + span.record_exception(exception) + + if exception.respond_to?(:type) + span.set_attribute("servactory.result", "failure") + span.status = OpenTelemetry::Trace::Status.error(error_message_for(exception)) + else + span.set_attribute("servactory.result", "error") + span.status = OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{exception.class}") + end + end + + def error_message_for(error) + return "Service failure" unless error + + if error.respond_to?(:message) + error.message.to_s + else + error.to_s + end + end + + def failure_attributes(error) + attrs = {} + attrs["servactory.failure.type"] = error.type.to_s if error.respond_to?(:type) && error.type + attrs["servactory.failure.message"] = error.message.to_s if error.respond_to?(:message) && error.message + attrs + end + + def tracer + OpenTelemetry::Instrumentation::Servactory::Instrumentation.instance.tracer + end + end + end + end + end +end diff --git a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb new file mode 100644 index 0000000..1735cfc --- /dev/null +++ b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +RSpec.describe OpenTelemetry::Instrumentation::Servactory::Patches::Callable do + let(:exporter) { EXPORTER } + let(:spans) { exporter.finished_spans } + + before do + exporter.reset + end + + describe "#call" do + context "when service succeeds" do + before { SuccessfulService.call(name: "World") } + + it "creates a span with correct name" do + root_span = spans.find { |s| s.name == "SuccessfulService call" } + expect(root_span).not_to be_nil + end + + it "sets success result attribute" do + root_span = spans.find { |s| s.name == "SuccessfulService call" } + expect(root_span.attributes["servactory.result"]).to eq("success") + end + + it "sets OK status" do + root_span = spans.find { |s| s.name == "SuccessfulService call" } + expect(root_span.status.code).to eq(OpenTelemetry::Trace::Status::OK) + end + + it "sets code.namespace attribute" do + root_span = spans.find { |s| s.name == "SuccessfulService call" } + expect(root_span.attributes["code.namespace"]).to eq("SuccessfulService") + end + + it "sets code.function attribute" do + root_span = spans.find { |s| s.name == "SuccessfulService call" } + expect(root_span.attributes["code.function"]).to eq("call") + end + + it "records input names" do + root_span = spans.find { |s| s.name == "SuccessfulService call" } + expect(root_span.attributes["servactory.input_names"]).to eq(["name"]) + end + + it "records output names" do + root_span = spans.find { |s| s.name == "SuccessfulService call" } + expect(root_span.attributes["servactory.output_names"]).to eq(["greeting"]) + end + end + + it "returns the result" do + result = SuccessfulService.call(name: "World") + + expect(result.greeting).to eq("Hello, World!") + end + + context "when service fails" do + before { FailingService.call(should_fail: true) } + + it "sets failure result attribute" do + root_span = spans.find { |s| s.name == "FailingService call" } + expect(root_span.attributes["servactory.result"]).to eq("failure") + end + + it "sets ERROR status" do + root_span = spans.find { |s| s.name == "FailingService call" } + expect(root_span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) + end + + it "adds a failure event" do + root_span = spans.find { |s| s.name == "FailingService call" } + failure_event = root_span.events&.find { |e| e.name == "servactory.failure" } + expect(failure_event).not_to be_nil + end + end + + it "returns the failure result" do + result = FailingService.call(should_fail: true) + + expect(result.failure?).to be true + end + + context "when service does not fail" do + it "sets success result" do + FailingService.call(should_fail: false) + + root_span = spans.find { |s| s.name == "FailingService call" } + expect(root_span.attributes["servactory.result"]).to eq("success") + end + end + end + + describe "#call!" do + context "when service succeeds" do + before { SuccessfulService.call!(name: "World") } + + it "creates a span with correct name" do + root_span = spans.find { |s| s.name == "SuccessfulService call!" } + expect(root_span).not_to be_nil + end + + it "sets success result attribute" do + root_span = spans.find { |s| s.name == "SuccessfulService call!" } + expect(root_span.attributes["servactory.result"]).to eq("success") + end + + it "sets OK status" do + root_span = spans.find { |s| s.name == "SuccessfulService call!" } + expect(root_span.status.code).to eq(OpenTelemetry::Trace::Status::OK) + end + end + + it "returns the result" do + result = SuccessfulService.call!(name: "World") + + expect(result.greeting).to eq("Hello, World!") + end + + context "when service fails with exception" do + before do + FailingService.call!(should_fail: true) + rescue StandardError # rubocop:disable Lint/SuppressedException + end + + it "records the exception" do + root_span = spans.find { |s| s.name == "FailingService call!" } + exception_event = root_span.events&.find { |e| e.name == "exception" } + expect(exception_event).not_to be_nil + end + + it "sets failure result for Servactory failures" do + root_span = spans.find { |s| s.name == "FailingService call!" } + expect(root_span.attributes["servactory.result"]).to eq("failure") + end + + it "sets ERROR status" do + root_span = spans.find { |s| s.name == "FailingService call!" } + expect(root_span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) + end + end + + it "re-raises the exception" do + expect { FailingService.call!(should_fail: true) }.to raise_error(StandardError, /Something went wrong/) + end + + context "when service raises unexpected exception" do + before do + ExceptionService.call! + rescue StandardError # rubocop:disable Lint/SuppressedException + end + + it "records the exception" do + root_span = spans.find { |s| s.name == "ExceptionService call!" } + exception_event = root_span.events&.find { |e| e.name == "exception" } + expect(exception_event).not_to be_nil + end + + it "sets error result" do + root_span = spans.find { |s| s.name == "ExceptionService call!" } + expect(root_span.attributes["servactory.result"]).to eq("error") + end + end + end +end From 82e185f6f5a59045282e0f8beb7cf556513b7ae2 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:00:55 +0300 Subject: [PATCH 02/22] Add Runner patch to instrument Servactory actions with OpenTelemetry **Summary of changes:** - Introduced `Runner` patch to trace Servactory action executions. - Captures spans for Servactory service actions (`call_action`). - Records exceptions, updates span statuses, and ensures span completion. - Added helper methods to create and manage spans: - `start_action_span` initializes spans with service names and attributes (`code.namespace`, `code.function`). - `tracer` retrieves the tracer instance for Servactory instrumentation. --- .../servactory/patches/runner.rb | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lib/opentelemetry/instrumentation/servactory/patches/runner.rb diff --git a/lib/opentelemetry/instrumentation/servactory/patches/runner.rb b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb new file mode 100644 index 0000000..a4a4f97 --- /dev/null +++ b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module OpenTelemetry + module Instrumentation + module Servactory + module Patches + module Runner + private + + def call_action(action) + span = start_action_span(action) + OpenTelemetry::Trace.with_span(span) do + super + rescue StandardError => e + span.record_exception(e) + span.status = OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{e.class}") + raise + ensure + span.finish + end + end + + def start_action_span(action) + service_name = @context.class.name || "AnonymousService" + + tracer.start_span( + "#{service_name} #{action.name}", + attributes: { "code.namespace" => service_name, "code.function" => action.name.to_s }, + kind: :internal + ) + end + + def tracer + OpenTelemetry::Instrumentation::Servactory::Instrumentation.instance.tracer + end + end + end + end + end +end From 9fe244be718dfa502f0ed4ce51ff777da320c515 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:01:19 +0300 Subject: [PATCH 03/22] Add compatibility check and patching logic for Servactory **Summary of changes:** - Introduced `compatible` block to validate minimum required Servactory version (`>= 2.16.0`). - Added configuration options (`trace_actions`, `record_input_names`, `record_output_names`) with default values and validations. - Implemented `present` block to check gem presence using `defined?`. - Added dependency requirements for Callable and Runner patches. - Introduced methods for patching (`patch_callable` and `patch_runner`), enabling instrumentation for Callable and Runner modules. - Updated `install` block to conditionally apply patches based on configuration. --- .../servactory/instrumentation.rb | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/opentelemetry/instrumentation/servactory/instrumentation.rb b/lib/opentelemetry/instrumentation/servactory/instrumentation.rb index ef53bf8..94ca370 100644 --- a/lib/opentelemetry/instrumentation/servactory/instrumentation.rb +++ b/lib/opentelemetry/instrumentation/servactory/instrumentation.rb @@ -4,20 +4,42 @@ module OpenTelemetry module Instrumentation module Servactory class Instrumentation < OpenTelemetry::Instrumentation::Base + instrumentation_version VERSION::STRING + + MINIMUM_VERSION = Gem::Version.new("2.16.0") + install do |_config| require_dependencies + patch_callable + patch_runner if config[:trace_actions] end present do - # TODO: Replace true with a definition check of the gem being instrumented - # Example: `defined?(::Rack)` - true + defined?(::Servactory) end + compatible do + gem_version = Gem::Version.new(::Servactory::VERSION::STRING) + gem_version >= MINIMUM_VERSION + end + + option :trace_actions, default: true, validate: :boolean + option :record_input_names, default: true, validate: :boolean + option :record_output_names, default: true, validate: :boolean + private def require_dependencies - # TODO: Include instrumentation dependencies + require_relative "patches/callable" + require_relative "patches/runner" + end + + def patch_callable + ::Servactory::Context::Callable.prepend(Patches::Callable) + end + + def patch_runner + ::Servactory::Actions::Tools::Runner.prepend(Patches::Runner) end end end From 8c966ef11892cf4fc03879c1e9fd3f333d62a394 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:05:10 +0300 Subject: [PATCH 04/22] Refactor Callable patch to utilize Ruby's forwarding arguments **Summary of changes:** - Updated `call` and `call!` methods to use Ruby's `...` syntax for argument forwarding. - Simplifies parameter handling while maintaining existing functionality. - Preserves span creation and attribute building for OpenTelemetry instrumentation. --- .../instrumentation/servactory/patches/callable.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb index 5a67f4c..7e2c266 100644 --- a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb +++ b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb @@ -5,7 +5,7 @@ module Instrumentation module Servactory module Patches module Callable - def call(arguments = {}) + def call(...) service_name = name || "AnonymousService" attributes = build_span_attributes(service_name, "call") @@ -16,7 +16,7 @@ def call(arguments = {}) end end - def call!(arguments = {}) # rubocop:disable Metrics/MethodLength + def call!(...) # rubocop:disable Metrics/MethodLength service_name = name || "AnonymousService" attributes = build_span_attributes(service_name, "call!") From adc9cfda1a506ef33615042287a0ce94045cf3ed Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:12:53 +0300 Subject: [PATCH 05/22] Update dependencies and Ruby version compatibility **Summary of changes:** - Relaxed `opentelemetry-api` dependency to `~> 1.0` and `opentelemetry-instrumentation-base` to `~> 0.22` for broader compatibility. - Added `opentelemetry-sdk` `~> 1.4` and `servactory` `>= 2.16.0` as development dependencies. - Updated Ruby version to `3.4.8` in `.ruby-version` and `.tool-versions`. - Refreshed `Gemfile.lock` to reflect updated dependencies including: - `activesupport` `8.1.2`, `json` `2.18.1`, and other minor version updates. - Re-bundled with Bundler version `4.0.6`. --- .ruby-version | 2 +- .tool-versions | 2 +- Gemfile.lock | 99 +++++++++++-------- ...lemetry-instrumentation-servactory.gemspec | 6 +- 4 files changed, 64 insertions(+), 45 deletions(-) diff --git a/.ruby-version b/.ruby-version index 4f5e697..7921bd0 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.5 +3.4.8 diff --git a/.tool-versions b/.tool-versions index 27a8619..5876619 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -ruby 3.4.5 +ruby 3.4.8 diff --git a/Gemfile.lock b/Gemfile.lock index ae0f0c3..bb136ca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,20 +2,20 @@ PATH remote: . specs: opentelemetry-instrumentation-servactory (0.1.0) - opentelemetry-api (~> 1.6.0) - opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22) GEM remote: https://rubygems.org/ specs: - activesupport (8.0.2.1) + activesupport (8.1.2) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) @@ -27,52 +27,59 @@ GEM thor (>= 0.14.0) ast (2.4.3) base64 (0.3.0) - benchmark (0.4.1) - bigdecimal (3.2.2) - concurrent-ruby (1.3.5) - connection_pool (2.5.3) + bigdecimal (4.0.1) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) diff-lcs (1.6.2) drb (2.2.3) - i18n (1.14.7) + i18n (1.14.8) concurrent-ruby (~> 1.0) - json (2.13.2) + json (2.18.1) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) - minitest (5.25.5) - opentelemetry-api (1.6.0) - opentelemetry-common (0.22.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (0.23.0) + minitest (6.0.1) + prism (~> 1.5) + opentelemetry-api (1.7.0) + opentelemetry-common (0.23.0) opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (0.25.0) + opentelemetry-api (~> 1.7) opentelemetry-common (~> 0.21) opentelemetry-registry (~> 0.1) opentelemetry-registry (0.4.0) opentelemetry-api (~> 1.1) + opentelemetry-sdk (1.10.0) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-registry (~> 0.2) + opentelemetry-semantic_conventions + opentelemetry-semantic_conventions (1.36.0) + opentelemetry-api (~> 1.0) parallel (1.27.0) - parser (3.3.9.0) + parser (3.3.10.2) ast (~> 2.4.1) racc - prism (1.4.0) + prism (1.9.0) racc (1.8.1) - rack (3.2.0) + rack (3.2.5) rainbow (3.1.1) - rake (13.3.0) - regexp_parser (2.11.2) - rspec (3.13.1) + rake (13.3.1) + regexp_parser (2.11.3) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.5) + rspec-core (3.13.6) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.5) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-support (3.13.5) - rubocop (1.80.0) + rspec-support (3.13.7) + rubocop (1.84.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -80,20 +87,20 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.46.0, < 2.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.46.0) + rubocop-ast (1.49.0) parser (>= 3.3.7.2) - prism (~> 1.4) - rubocop-factory_bot (2.27.1) + prism (~> 1.7) + rubocop-factory_bot (2.28.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) - rubocop-performance (1.25.0) + rubocop-performance (1.26.1) lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.33.3) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.34.3) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) @@ -102,15 +109,20 @@ GEM rubocop-rake (0.7.1) lint_roller (~> 1.1) rubocop (>= 1.72.1) - rubocop-rspec (3.6.0) + rubocop-rspec (3.9.0) lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) - rubocop-rspec_rails (2.31.0) + rubocop (~> 1.81) + rubocop-rspec_rails (2.32.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec (~> 3.5) ruby-progressbar (1.13.0) securerandom (0.4.1) + servactory (3.0.2) + activesupport (>= 5.1) + i18n (>= 1.14) + stroma (>= 0.5, < 1.0) + zeitwerk (>= 2.6) servactory-rubocop (0.9.0) rubocop (>= 1.73) rubocop-factory_bot (>= 2.27) @@ -119,13 +131,16 @@ GEM rubocop-rake (>= 0.7) rubocop-rspec (>= 3.5) rubocop-rspec_rails (>= 2.31) - thor (1.4.0) + stroma (0.5.0) + zeitwerk (>= 2.6) + thor (1.5.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (3.1.5) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) - uri (1.0.3) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + zeitwerk (2.7.5) PLATFORMS aarch64-linux-gnu @@ -143,8 +158,10 @@ PLATFORMS DEPENDENCIES appraisal (>= 2.5) opentelemetry-instrumentation-servactory! + opentelemetry-sdk (~> 1.4) rspec (>= 3.13) + servactory (>= 2.16.0) servactory-rubocop (>= 0.9) BUNDLED WITH - 2.7.1 + 4.0.6 diff --git a/opentelemetry-instrumentation-servactory.gemspec b/opentelemetry-instrumentation-servactory.gemspec index 8d8acbf..edbb1af 100644 --- a/opentelemetry-instrumentation-servactory.gemspec +++ b/opentelemetry-instrumentation-servactory.gemspec @@ -29,10 +29,12 @@ Gem::Specification.new do |spec| spec.required_ruby_version = Gem::Requirement.new(">= 3.2") - spec.add_dependency "opentelemetry-api", "~> 1.6.0" - spec.add_dependency "opentelemetry-instrumentation-base", "~> 0.23.0" + spec.add_dependency "opentelemetry-api", "~> 1.0" + spec.add_dependency "opentelemetry-instrumentation-base", "~> 0.22" spec.add_development_dependency "appraisal", ">= 2.5" + spec.add_development_dependency "opentelemetry-sdk", "~> 1.4" spec.add_development_dependency "rspec", ">= 3.13" + spec.add_development_dependency "servactory", ">= 2.16.0" spec.add_development_dependency "servactory-rubocop", ">= 0.9" end From e3464fe517f4cbe4cb3ab42ea54f502e2c705296 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:15:22 +0300 Subject: [PATCH 06/22] Add RSpec tests for Servactory instrumentation and example services **Summary of changes:** - Added `instrumentation_spec.rb` to validate the behavior of `OpenTelemetry::Instrumentation::Servactory::Instrumentation`: - Ensures the presence of name and version. - Validates default configurations (`trace_actions`, `record_input_names`, `record_output_names`). - Tests compatibility and Servactory presence detection. - Introduced `support/fake_service.rb` with example services for testing: - `SuccessfulService`, `FailingService`, `MultiActionService`, and `ExceptionService`. - Enhanced test coverage with executable service action validations and various edge-case scenarios. --- .../servactory/instrumentation_spec.rb | 46 +++++++++++++++ spec/support/fake_service.rb | 58 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb create mode 100644 spec/support/fake_service.rb diff --git a/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb b/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb new file mode 100644 index 0000000..2883d97 --- /dev/null +++ b/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +RSpec.describe OpenTelemetry::Instrumentation::Servactory::Instrumentation do + let(:instrumentation) { described_class.instance } + + it "has a name" do + expect(instrumentation.name).to eq("OpenTelemetry::Instrumentation::Servactory") + end + + it "has a version" do + expect(instrumentation.version).to eq(OpenTelemetry::Instrumentation::Servactory::VERSION::STRING) + end + + describe "present?" do + it "returns true when Servactory is defined" do + expect(instrumentation).to be_present + end + end + + describe "compatible?" do + it "returns true for supported Servactory versions" do + expect(instrumentation.compatible?).to be true + end + end + + describe "install" do + it "installs with default config" do + expect(instrumentation.install).not_to be_nil + end + + it "has trace_actions enabled by default" do + instrumentation.install + expect(instrumentation.config[:trace_actions]).to be true + end + + it "has record_input_names enabled by default" do + instrumentation.install + expect(instrumentation.config[:record_input_names]).to be true + end + + it "has record_output_names enabled by default" do + instrumentation.install + expect(instrumentation.config[:record_output_names]).to be true + end + end +end diff --git a/spec/support/fake_service.rb b/spec/support/fake_service.rb new file mode 100644 index 0000000..323c905 --- /dev/null +++ b/spec/support/fake_service.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class SuccessfulService < Servactory::Base + input :name, type: String + + output :greeting, type: String + + make :build_greeting + + private + + def build_greeting + outputs.greeting = "Hello, #{inputs.name}!" + end +end + +class FailingService < Servactory::Base + input :should_fail, type: [TrueClass, FalseClass] + + make :maybe_fail + + private + + def maybe_fail + fail!(message: "Something went wrong") if inputs.should_fail + end +end + +class MultiActionService < Servactory::Base + input :value, type: Integer + + internal :temp, type: Integer + + output :result, type: Integer + + make :step_one + make :step_two + + private + + def step_one + internals.temp = inputs.value * 2 + end + + def step_two + outputs.result = internals.temp + 1 + end +end + +class ExceptionService < Servactory::Base + make :blow_up + + private + + def blow_up + raise StandardError, "unexpected error" + end +end From 6589b5f8353d62b9eee39b4a5b5c0fb8d83a160e Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:18:50 +0300 Subject: [PATCH 07/22] Initialize OpenTelemetry SDK and configure RSpec for tracing **Summary of changes:** - Added OpenTelemetry SDK setup with an in-memory span exporter. - Configured RSpec to reset the exporter before each test run. - Required supporting files under the `support` directory automatically. - Set up OpenTelemetry instrumentation and error handling for Servactory. --- spec/spec_helper.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a28810c..4e939be 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true +require "forwardable" +require "servactory" require "opentelemetry/instrumentation/servactory" +require "opentelemetry-sdk" + +Dir[File.join(__dir__, "support", "**", "*.rb")].each { |f| require f } + +EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new RSpec.configure do |config| # Enable flags like --only-failures and --next-failure @@ -17,4 +24,14 @@ # doing truncation. c.max_formatted_output_length = nil end + + config.before do + EXPORTER.reset + end +end + +OpenTelemetry::SDK.configure do |c| + c.error_handler = ->(exception:, message:) { raise(exception || message) } + c.add_span_processor(OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER)) + c.use("OpenTelemetry::Instrumentation::Servactory") end From dfb5244764f298d67942da5a6228d1264347468a Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:21:17 +0300 Subject: [PATCH 08/22] Rename `fake_service.rb` to `example_services.rb` in support folder **Summary of changes:** - Renamed `fake_service.rb` to `example_services.rb` for improved naming clarity. - Adjusted references to the renamed file in the support folder. --- spec/support/{fake_service.rb => example_services.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/support/{fake_service.rb => example_services.rb} (100%) diff --git a/spec/support/fake_service.rb b/spec/support/example_services.rb similarity index 100% rename from spec/support/fake_service.rb rename to spec/support/example_services.rb From 90c8784b19d7c736144b55cf525879b66729c54b Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:28:14 +0300 Subject: [PATCH 09/22] Enhance Servactory instrumentation test coverage **Summary of changes:** - Added unit tests for the `Runner` module in `runner_spec.rb`: - Verifies accurate span creation and attributes for multi-action services. - Validates exception handling and span updates when errors occur. - Refactored `callable_spec.rb` for improved attribute validation: - Consolidated span verification using `subject` and helper variables. - Expanded tests to confirm span attributes, statuses, and exception handling. - Simplified `instrumentation_spec.rb` by adopting one-liners for predicate checks. - Updated `spec_helper.rb` to refine OpenTelemetry SDK initialization: - Improved readability by reordering configuration setup. - Streamlined file requiring and exporter setup. --- .../servactory/instrumentation_spec.rb | 31 +--- .../servactory/patches/callable_spec.rb | 154 +++++++----------- .../servactory/patches/runner_spec.rb | 54 ++++++ .../servactory/version_spec.rb | 2 +- spec/spec_helper.rb | 14 +- 5 files changed, 131 insertions(+), 124 deletions(-) create mode 100644 spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb diff --git a/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb b/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb index 2883d97..22d2fcc 100644 --- a/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe OpenTelemetry::Instrumentation::Servactory::Instrumentation do - let(:instrumentation) { described_class.instance } + subject(:instrumentation) { described_class.instance } it "has a name" do expect(instrumentation.name).to eq("OpenTelemetry::Instrumentation::Servactory") @@ -11,35 +11,20 @@ expect(instrumentation.version).to eq(OpenTelemetry::Instrumentation::Servactory::VERSION::STRING) end - describe "present?" do - it "returns true when Servactory is defined" do - expect(instrumentation).to be_present - end + describe "#present?" do + it { expect(instrumentation).to be_present } end - describe "compatible?" do - it "returns true for supported Servactory versions" do - expect(instrumentation.compatible?).to be true - end + describe "#compatible?" do + it { expect(instrumentation.compatible?).to be true } end - describe "install" do - it "installs with default config" do - expect(instrumentation.install).not_to be_nil - end + describe "#install" do + before { instrumentation.install } - it "has trace_actions enabled by default" do - instrumentation.install + it "sets default config options", :aggregate_failures do expect(instrumentation.config[:trace_actions]).to be true - end - - it "has record_input_names enabled by default" do - instrumentation.install expect(instrumentation.config[:record_input_names]).to be true - end - - it "has record_output_names enabled by default" do - instrumentation.install expect(instrumentation.config[:record_output_names]).to be true end end diff --git a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb index 1735cfc..87a2f2d 100644 --- a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb @@ -1,163 +1,131 @@ # frozen_string_literal: true RSpec.describe OpenTelemetry::Instrumentation::Servactory::Patches::Callable do - let(:exporter) { EXPORTER } - let(:spans) { exporter.finished_spans } - - before do - exporter.reset - end + let(:spans) { EXPORTER.finished_spans } describe "#call" do context "when service succeeds" do - before { SuccessfulService.call(name: "World") } + subject(:result) { SuccessfulService.call(name: "World") } - it "creates a span with correct name" do - root_span = spans.find { |s| s.name == "SuccessfulService call" } - expect(root_span).not_to be_nil - end + let(:span) { spans.find { |s| s.name == "SuccessfulService call" } } - it "sets success result attribute" do - root_span = spans.find { |s| s.name == "SuccessfulService call" } - expect(root_span.attributes["servactory.result"]).to eq("success") - end - - it "sets OK status" do - root_span = spans.find { |s| s.name == "SuccessfulService call" } - expect(root_span.status.code).to eq(OpenTelemetry::Trace::Status::OK) - end + before { result } - it "sets code.namespace attribute" do - root_span = spans.find { |s| s.name == "SuccessfulService call" } - expect(root_span.attributes["code.namespace"]).to eq("SuccessfulService") + it "creates a span" do + expect(span).not_to be_nil end - it "sets code.function attribute" do - root_span = spans.find { |s| s.name == "SuccessfulService call" } - expect(root_span.attributes["code.function"]).to eq("call") + it "sets span attributes", :aggregate_failures do + expect(span.attributes["code.namespace"]).to eq("SuccessfulService") + expect(span.attributes["code.function"]).to eq("call") + expect(span.attributes["servactory.result"]).to eq("success") + expect(span.attributes["servactory.input_names"]).to eq(["name"]) + expect(span.attributes["servactory.output_names"]).to eq(["greeting"]) end - it "records input names" do - root_span = spans.find { |s| s.name == "SuccessfulService call" } - expect(root_span.attributes["servactory.input_names"]).to eq(["name"]) + it "sets OK status" do + expect(span.status.code).to eq(OpenTelemetry::Trace::Status::OK) end - it "records output names" do - root_span = spans.find { |s| s.name == "SuccessfulService call" } - expect(root_span.attributes["servactory.output_names"]).to eq(["greeting"]) + it "does not alter the return value" do + expect(result.greeting).to eq("Hello, World!") end end - it "returns the result" do - result = SuccessfulService.call(name: "World") - - expect(result.greeting).to eq("Hello, World!") - end - context "when service fails" do - before { FailingService.call(should_fail: true) } + subject(:result) { FailingService.call(should_fail: true) } - it "sets failure result attribute" do - root_span = spans.find { |s| s.name == "FailingService call" } - expect(root_span.attributes["servactory.result"]).to eq("failure") - end + let(:span) { spans.find { |s| s.name == "FailingService call" } } + + before { result } - it "sets ERROR status" do - root_span = spans.find { |s| s.name == "FailingService call" } - expect(root_span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) + it "sets span attributes", :aggregate_failures do + expect(span.attributes["servactory.result"]).to eq("failure") + expect(span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) end it "adds a failure event" do - root_span = spans.find { |s| s.name == "FailingService call" } - failure_event = root_span.events&.find { |e| e.name == "servactory.failure" } + failure_event = span.events&.find { |e| e.name == "servactory.failure" } expect(failure_event).not_to be_nil end - end - - it "returns the failure result" do - result = FailingService.call(should_fail: true) - expect(result.failure?).to be true + it "returns a failure result" do + expect(result).to be_failure + end end context "when service does not fail" do - it "sets success result" do - FailingService.call(should_fail: false) + before { FailingService.call(should_fail: false) } - root_span = spans.find { |s| s.name == "FailingService call" } - expect(root_span.attributes["servactory.result"]).to eq("success") + it "sets success result" do + span = spans.find { |s| s.name == "FailingService call" } + expect(span.attributes["servactory.result"]).to eq("success") end end end describe "#call!" do context "when service succeeds" do - before { SuccessfulService.call!(name: "World") } + subject(:result) { SuccessfulService.call!(name: "World") } + + let(:span) { spans.find { |s| s.name == "SuccessfulService call!" } } + + before { result } - it "creates a span with correct name" do - root_span = spans.find { |s| s.name == "SuccessfulService call!" } - expect(root_span).not_to be_nil + it "creates a span" do + expect(span).not_to be_nil end - it "sets success result attribute" do - root_span = spans.find { |s| s.name == "SuccessfulService call!" } - expect(root_span.attributes["servactory.result"]).to eq("success") + it "sets span attributes", :aggregate_failures do + expect(span.attributes["servactory.result"]).to eq("success") + expect(span.status.code).to eq(OpenTelemetry::Trace::Status::OK) end - it "sets OK status" do - root_span = spans.find { |s| s.name == "SuccessfulService call!" } - expect(root_span.status.code).to eq(OpenTelemetry::Trace::Status::OK) + it "does not alter the return value" do + expect(result.greeting).to eq("Hello, World!") end end - it "returns the result" do - result = SuccessfulService.call!(name: "World") - - expect(result.greeting).to eq("Hello, World!") - end + context "when service fails with Servactory failure" do + let(:span) { spans.find { |s| s.name == "FailingService call!" } } - context "when service fails with exception" do before do FailingService.call!(should_fail: true) - rescue StandardError # rubocop:disable Lint/SuppressedException + rescue StandardError + nil end - it "records the exception" do - root_span = spans.find { |s| s.name == "FailingService call!" } - exception_event = root_span.events&.find { |e| e.name == "exception" } + it "records the exception on span" do + exception_event = span.events&.find { |e| e.name == "exception" } expect(exception_event).not_to be_nil end - it "sets failure result for Servactory failures" do - root_span = spans.find { |s| s.name == "FailingService call!" } - expect(root_span.attributes["servactory.result"]).to eq("failure") + it "sets span attributes", :aggregate_failures do + expect(span.attributes["servactory.result"]).to eq("failure") + expect(span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) end - it "sets ERROR status" do - root_span = spans.find { |s| s.name == "FailingService call!" } - expect(root_span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) + it "re-raises the exception" do + expect { FailingService.call!(should_fail: true) }.to raise_error(StandardError, /Something went wrong/) end end - it "re-raises the exception" do - expect { FailingService.call!(should_fail: true) }.to raise_error(StandardError, /Something went wrong/) - end - context "when service raises unexpected exception" do + let(:span) { spans.find { |s| s.name == "ExceptionService call!" } } + before do ExceptionService.call! - rescue StandardError # rubocop:disable Lint/SuppressedException + rescue StandardError + nil end - it "records the exception" do - root_span = spans.find { |s| s.name == "ExceptionService call!" } - exception_event = root_span.events&.find { |e| e.name == "exception" } + it "records the exception on span" do + exception_event = span.events&.find { |e| e.name == "exception" } expect(exception_event).not_to be_nil end it "sets error result" do - root_span = spans.find { |s| s.name == "ExceptionService call!" } - expect(root_span.attributes["servactory.result"]).to eq("error") + expect(span.attributes["servactory.result"]).to eq("error") end end end diff --git a/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb new file mode 100644 index 0000000..839d501 --- /dev/null +++ b/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.describe OpenTelemetry::Instrumentation::Servactory::Patches::Runner do + let(:spans) { EXPORTER.finished_spans } + + describe "#call_action" do + context "when service has multiple actions" do + subject(:result) { MultiActionService.call(value: 5) } + + before { result } + + it "creates child spans for each action", :aggregate_failures do + expect(spans.find { |s| s.name == "MultiActionService step_one" }).not_to be_nil + expect(spans.find { |s| s.name == "MultiActionService step_two" }).not_to be_nil + end + + it "sets span attributes", :aggregate_failures do + span = spans.find { |s| s.name == "MultiActionService step_one" } + expect(span.attributes["code.namespace"]).to eq("MultiActionService") + expect(span.attributes["code.function"]).to eq("step_one") + end + + it "creates action spans as children of the root span" do + root_span = spans.find { |s| s.name == "MultiActionService call" } + action_span = spans.find { |s| s.name == "MultiActionService step_one" } + + expect(action_span.parent_span_id).to eq(root_span.span_id) + end + + it "does not alter the return value" do + expect(result.result).to eq(11) + end + end + + context "when action raises an exception" do + let(:span) { spans.find { |s| s.name == "ExceptionService blow_up" } } + + before do + ExceptionService.call + rescue StandardError + nil + end + + it "records exception on action span" do + exception_event = span.events&.find { |e| e.name == "exception" } + expect(exception_event).not_to be_nil + end + + it "sets ERROR status on action span" do + expect(span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) + end + end + end +end diff --git a/spec/opentelemetry/instrumentation/servactory/version_spec.rb b/spec/opentelemetry/instrumentation/servactory/version_spec.rb index 9c0d0af..5409ac8 100644 --- a/spec/opentelemetry/instrumentation/servactory/version_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/version_spec.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true RSpec.describe OpenTelemetry::Instrumentation::Servactory::VERSION do - it { expect(OpenTelemetry::Instrumentation::Servactory::VERSION::STRING).not_to be_nil } + it { expect(described_class::STRING).not_to be_nil } end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4e939be..7c97eab 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,10 +5,16 @@ require "opentelemetry/instrumentation/servactory" require "opentelemetry-sdk" -Dir[File.join(__dir__, "support", "**", "*.rb")].each { |f| require f } +Dir[File.join(__dir__, "support", "**", "*.rb")].each { |file| require file } EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new +OpenTelemetry::SDK.configure do |c| + c.error_handler = ->(exception:, message:) { raise(exception || message) } + c.add_span_processor(OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER)) + c.use("OpenTelemetry::Instrumentation::Servactory") +end + RSpec.configure do |config| # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" @@ -29,9 +35,3 @@ EXPORTER.reset end end - -OpenTelemetry::SDK.configure do |c| - c.error_handler = ->(exception:, message:) { raise(exception || message) } - c.add_span_processor(OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER)) - c.use("OpenTelemetry::Instrumentation::Servactory") -end From 137c0282ac5b42871cd9d92ffd0aa3b492dbbb38 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:34:12 +0300 Subject: [PATCH 10/22] Relax OpenTelemetry SDK version constraint in development dependencies **Summary of changes:** - Updated `opentelemetry-sdk` development dependency from `~> 1.4` to `>= 1.4` in the gemspec. - Synchronized the dependency update in `Gemfile.lock` for consistency. - Ensures compatibility with future `opentelemetry-sdk` versions while maintaining minimum version requirements. --- Gemfile.lock | 2 +- opentelemetry-instrumentation-servactory.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bb136ca..3910128 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -158,7 +158,7 @@ PLATFORMS DEPENDENCIES appraisal (>= 2.5) opentelemetry-instrumentation-servactory! - opentelemetry-sdk (~> 1.4) + opentelemetry-sdk (>= 1.4) rspec (>= 3.13) servactory (>= 2.16.0) servactory-rubocop (>= 0.9) diff --git a/opentelemetry-instrumentation-servactory.gemspec b/opentelemetry-instrumentation-servactory.gemspec index edbb1af..d0cdd26 100644 --- a/opentelemetry-instrumentation-servactory.gemspec +++ b/opentelemetry-instrumentation-servactory.gemspec @@ -33,7 +33,7 @@ Gem::Specification.new do |spec| spec.add_dependency "opentelemetry-instrumentation-base", "~> 0.22" spec.add_development_dependency "appraisal", ">= 2.5" - spec.add_development_dependency "opentelemetry-sdk", "~> 1.4" + spec.add_development_dependency "opentelemetry-sdk", ">= 1.4" spec.add_development_dependency "rspec", ">= 3.13" spec.add_development_dependency "servactory", ">= 2.16.0" spec.add_development_dependency "servactory-rubocop", ">= 0.9" From b939c05858b7107078e75147ac64cdc158deb461 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:40:41 +0300 Subject: [PATCH 11/22] Document Servactory instrumentation setup and configuration **Summary of changes:** - Added a detailed description of the Servactory instrumentation, its purpose, and functionality. - Documented installation instructions for RubyGems and Bundler. - Introduced a new requirements section specifying required versions for Servactory (`>= 2.16.0`) and Ruby (`>= 3.2`). - Expanded usage examples with configuration options (`trace_actions`, `record_input_names`, `record_output_names`). - Explained the structure and attributes of spans, as well as failure event handling. - Provided example span structures for better clarity on instrumentation behavior. --- README.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3c15fab..e7a7d04 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OpenTelemetry Servactory Instrumentation -Todo: Add a description. +Instrumentation for the [Servactory][servactory-home] service object framework. Automatically traces `call`/`call!` invocations and individual actions with span attributes, failure detection, and error recording. ## How do I get started? @@ -10,7 +10,18 @@ Install the gem using: gem install opentelemetry-instrumentation-servactory ``` -Or, if you use [bundler][bundler-home], include `opentelemetry-instrumentation-servactory` in your `Gemfile`. +Or, if you use [bundler][bundler-home], include `opentelemetry-instrumentation-servactory` in your `Gemfile`: + +```ruby +gem "opentelemetry-instrumentation-servactory" +``` + +## Requirements + +| Requirement | Version | +| --- | --- | +| Servactory | `>= 2.16.0` | +| Ruby | `>= 3.2` | ## Usage @@ -18,11 +29,11 @@ To use the instrumentation, call `use` with the name of the instrumentation: ```ruby OpenTelemetry::SDK.configure do |c| - c.use 'OpenTelemetry::Instrumentation::Servactory' + c.use "OpenTelemetry::Instrumentation::Servactory" end ``` -Alternatively, you can also call `use_all` to install all the available instrumentation. +Alternatively, you can also call `use_all` to install all the available instrumentation: ```ruby OpenTelemetry::SDK.configure do |c| @@ -30,13 +41,60 @@ OpenTelemetry::SDK.configure do |c| end ``` -## Examples +## Configuration Options + +| Option | Default | Type | Description | +| --- | --- | --- | --- | +| `trace_actions` | `true` | Boolean | Create child spans for each `make` action | +| `record_input_names` | `true` | Boolean | Record input names as a span attribute | +| `record_output_names` | `true` | Boolean | Record output names as a span attribute | + +Example with custom configuration: + +```ruby +OpenTelemetry::SDK.configure do |c| + c.use "OpenTelemetry::Instrumentation::Servactory", { + trace_actions: true, + record_input_names: false, + record_output_names: false + } +end +``` + +## Span Structure + +Each `call`/`call!` invocation creates a root span. When `trace_actions` is enabled, each `make` action creates a child span. + +``` +Users::CreateService call (root span) + |-- Users::CreateService validate (child span per action) + |-- Users::CreateService create_user + |-- Users::CreateService send_email +``` + +### Span Attributes + +| Attribute | Type | Description | +| --- | --- | --- | +| `code.namespace` | String | Service class name | +| `code.function` | String | Method name (`call`, `call!`, or action name) | +| `servactory.result` | String | `success`, `failure`, or `error` | +| `servactory.input_names` | Array | Input attribute names (when `record_input_names` is enabled) | +| `servactory.output_names` | Array | Output attribute names (when `record_output_names` is enabled) | + +### Failure Events + +When a service fails via `fail!`, a `servactory.failure` event is added to the span with: -Example usage can be seen in the [`./example/trace_demonstration.rb` file](https://github.com/servactory/opentelemetry-instrumentation-servactory/blob/main/example/trace_demonstration.rb) +| Attribute | Type | Description | +| --- | --- | --- | +| `servactory.failure.type` | String | Failure type (if provided) | +| `servactory.failure.message` | String | Failure message | ## License The `opentelemetry-instrumentation-servactory` gem is distributed under the MIT license. See [LICENSE][license-github] for more information. +[servactory-home]: https://servactory.com [bundler-home]: https://bundler.io [license-github]: https://github.com/servactory/opentelemetry-instrumentation-servactory/blob/main/LICENSE From 56c81fda6bb0b5e59e2a2a0b38b1efcb487e1dfa Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 00:43:39 +0300 Subject: [PATCH 12/22] Format Servactory usage examples for improved readability **Summary of changes:** - Updated code examples in the README to use parentheses for method arguments, enhancing consistency and clarity. - Reformatted multi-line `c.use` blocks for better alignment and visual structure. --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e7a7d04..e49adb2 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ To use the instrumentation, call `use` with the name of the instrumentation: ```ruby OpenTelemetry::SDK.configure do |c| - c.use "OpenTelemetry::Instrumentation::Servactory" + c.use("OpenTelemetry::Instrumentation::Servactory") end ``` @@ -53,11 +53,13 @@ Example with custom configuration: ```ruby OpenTelemetry::SDK.configure do |c| - c.use "OpenTelemetry::Instrumentation::Servactory", { - trace_actions: true, - record_input_names: false, - record_output_names: false - } + c.use( + "OpenTelemetry::Instrumentation::Servactory", { + trace_actions: true, + record_input_names: false, + record_output_names: false + } + ) end ``` From e613b428a1b6f372f77b2d9eccdb0cb8150344b9 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 02:23:18 +0300 Subject: [PATCH 13/22] Relax OpenTelemetry gem version constraints for broader compatibility **Summary of changes:** - Updated `opentelemetry-api` dependency from `~> 1.0` to `>= 1.0`. - Updated `opentelemetry-instrumentation-base` dependency from `~> 0.22` to `>= 0.22`. - Synchronized changes in the `Gemfile.lock` file for consistency. - Ensures better flexibility with future OpenTelemetry versions while retaining minimum compatibility. --- Gemfile.lock | 4 ++-- opentelemetry-instrumentation-servactory.gemspec | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3910128..546e02f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,8 +2,8 @@ PATH remote: . specs: opentelemetry-instrumentation-servactory (0.1.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22) + opentelemetry-api (>= 1.0) + opentelemetry-instrumentation-base (>= 0.22) GEM remote: https://rubygems.org/ diff --git a/opentelemetry-instrumentation-servactory.gemspec b/opentelemetry-instrumentation-servactory.gemspec index d0cdd26..3af32cb 100644 --- a/opentelemetry-instrumentation-servactory.gemspec +++ b/opentelemetry-instrumentation-servactory.gemspec @@ -29,8 +29,8 @@ Gem::Specification.new do |spec| spec.required_ruby_version = Gem::Requirement.new(">= 3.2") - spec.add_dependency "opentelemetry-api", "~> 1.0" - spec.add_dependency "opentelemetry-instrumentation-base", "~> 0.22" + spec.add_dependency "opentelemetry-api", ">= 1.0" + spec.add_dependency "opentelemetry-instrumentation-base", ">= 0.22" spec.add_development_dependency "appraisal", ">= 2.5" spec.add_development_dependency "opentelemetry-sdk", ">= 1.4" From 524959fdb18f50561505d6797fd07e13e23fc587 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 02:26:03 +0300 Subject: [PATCH 14/22] Add Servactory system attributes to spans **Summary of changes:** - Updated `Runner` and `Callable` modules to include `servactory.system` and `servactory.version` attributes in spans. - Extracted span attribute construction logic into dedicated methods for `build_action_attributes` and `build_span_attributes`. - Updated test cases in `runner_spec.rb` and `callable_spec.rb` to validate new attributes. - Extended README documentation to describe new span attributes (`servactory.system`, `servactory.version`). --- README.md | 2 ++ .../servactory/patches/callable.rb | 7 ++++++- .../instrumentation/servactory/patches/runner.rb | 16 +++++++++++----- .../servactory/patches/callable_spec.rb | 2 ++ .../servactory/patches/runner_spec.rb | 2 ++ 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e49adb2..0889b34 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ Users::CreateService call (root span) | --- | --- | --- | | `code.namespace` | String | Service class name | | `code.function` | String | Method name (`call`, `call!`, or action name) | +| `servactory.system` | String | Framework identifier (`servactory`) | +| `servactory.version` | String | Servactory library version | | `servactory.result` | String | `success`, `failure`, or `error` | | `servactory.input_names` | Array | Input attribute names (when `record_input_names` is enabled) | | `servactory.output_names` | Array | Output attribute names (when `record_output_names` is enabled) | diff --git a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb index 7e2c266..1ec1c4b 100644 --- a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb +++ b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb @@ -36,7 +36,12 @@ def call!(...) # rubocop:disable Metrics/MethodLength private def build_span_attributes(service_name, method_name) - attributes = { "code.namespace" => service_name, "code.function" => method_name } + attributes = { + "code.namespace" => service_name, + "code.function" => method_name, + "servactory.system" => "servactory", + "servactory.version" => ::Servactory::VERSION::STRING + } append_attribute_names(attributes) attributes end diff --git a/lib/opentelemetry/instrumentation/servactory/patches/runner.rb b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb index a4a4f97..67baf94 100644 --- a/lib/opentelemetry/instrumentation/servactory/patches/runner.rb +++ b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb @@ -22,12 +22,18 @@ def call_action(action) def start_action_span(action) service_name = @context.class.name || "AnonymousService" + attributes = build_action_attributes(service_name, action.name.to_s) - tracer.start_span( - "#{service_name} #{action.name}", - attributes: { "code.namespace" => service_name, "code.function" => action.name.to_s }, - kind: :internal - ) + tracer.start_span("#{service_name} #{action.name}", attributes:, kind: :internal) + end + + def build_action_attributes(service_name, action_name) + { + "code.namespace" => service_name, + "code.function" => action_name, + "servactory.system" => "servactory", + "servactory.version" => ::Servactory::VERSION::STRING + } end def tracer diff --git a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb index 87a2f2d..3a9a6e3 100644 --- a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb @@ -18,6 +18,8 @@ it "sets span attributes", :aggregate_failures do expect(span.attributes["code.namespace"]).to eq("SuccessfulService") expect(span.attributes["code.function"]).to eq("call") + expect(span.attributes["servactory.system"]).to eq("servactory") + expect(span.attributes["servactory.version"]).to eq(Servactory::VERSION::STRING) expect(span.attributes["servactory.result"]).to eq("success") expect(span.attributes["servactory.input_names"]).to eq(["name"]) expect(span.attributes["servactory.output_names"]).to eq(["greeting"]) diff --git a/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb index 839d501..a98d505 100644 --- a/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb @@ -18,6 +18,8 @@ span = spans.find { |s| s.name == "MultiActionService step_one" } expect(span.attributes["code.namespace"]).to eq("MultiActionService") expect(span.attributes["code.function"]).to eq("step_one") + expect(span.attributes["servactory.system"]).to eq("servactory") + expect(span.attributes["servactory.version"]).to eq(Servactory::VERSION::STRING) end it "creates action spans as children of the root span" do From 6105507d4fb765a95bcaf2d06cbaacb773722fd6 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 13:17:23 +0300 Subject: [PATCH 15/22] Prefix private patch methods with _otel_ - Rename 11 private methods in Patches::Callable to avoid namespace pollution on service class singletons - Rename 3 private methods in Patches::Runner for consistency - Public override methods (call, call!, call_action) unchanged --- .../servactory/patches/callable.rb | 52 +++++++++---------- .../servactory/patches/runner.rb | 12 ++--- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb index 1ec1c4b..c830236 100644 --- a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb +++ b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb @@ -7,26 +7,26 @@ module Patches module Callable def call(...) service_name = name || "AnonymousService" - attributes = build_span_attributes(service_name, "call") + attributes = _otel_build_span_attributes(service_name, "call") - tracer.in_span("#{service_name} call", attributes:, kind: :internal) do |span| + _otel_tracer.in_span("#{service_name} call", attributes:, kind: :internal) do |span| result = super - record_result(span, result) + _otel_record_result(span, result) result end end def call!(...) # rubocop:disable Metrics/MethodLength service_name = name || "AnonymousService" - attributes = build_span_attributes(service_name, "call!") + attributes = _otel_build_span_attributes(service_name, "call!") - span = tracer.start_span("#{service_name} call!", attributes:, kind: :internal) + span = _otel_tracer.start_span("#{service_name} call!", attributes:, kind: :internal) OpenTelemetry::Trace.with_span(span) do result = super - mark_span_success(span) + _otel_mark_span_success(span) result rescue StandardError => e - record_exception_on_span(span, e) + _otel_record_exception_on_span(span, e) raise ensure span.finish @@ -35,78 +35,78 @@ def call!(...) # rubocop:disable Metrics/MethodLength private - def build_span_attributes(service_name, method_name) + def _otel_build_span_attributes(service_name, method_name) attributes = { "code.namespace" => service_name, "code.function" => method_name, "servactory.system" => "servactory", "servactory.version" => ::Servactory::VERSION::STRING } - append_attribute_names(attributes) + _otel_append_attribute_names(attributes) attributes end - def append_attribute_names(attributes) + def _otel_append_attribute_names(attributes) config = OpenTelemetry::Instrumentation::Servactory::Instrumentation.instance.config if config[:record_input_names] - names = input_names + names = _otel_input_names attributes["servactory.input_names"] = names unless names.empty? end return unless config[:record_output_names] - names = output_names + names = _otel_output_names attributes["servactory.output_names"] = names unless names.empty? end - def input_names + def _otel_input_names send(:collection_of_inputs).names.map(&:to_s) rescue StandardError [] end - def output_names + def _otel_output_names send(:collection_of_outputs).names.map(&:to_s) rescue StandardError [] end - def record_result(span, result) + def _otel_record_result(span, result) if result.respond_to?(:failure?) && result.failure? - record_failure_on_span(span, result) + _otel_record_failure_on_span(span, result) span.set_attribute("servactory.result", "failure") else - mark_span_success(span) + _otel_mark_span_success(span) end end - def mark_span_success(span) + def _otel_mark_span_success(span) span.set_attribute("servactory.result", "success") span.status = OpenTelemetry::Trace::Status.ok end - def record_failure_on_span(span, result) + def _otel_record_failure_on_span(span, result) error = result.respond_to?(:error) ? result.error : nil - message = error_message_for(error) + message = _otel_error_message_for(error) - span.add_event("servactory.failure", attributes: failure_attributes(error)) + span.add_event("servactory.failure", attributes: _otel_failure_attributes(error)) span.status = OpenTelemetry::Trace::Status.error(message) end - def record_exception_on_span(span, exception) + def _otel_record_exception_on_span(span, exception) span.record_exception(exception) if exception.respond_to?(:type) span.set_attribute("servactory.result", "failure") - span.status = OpenTelemetry::Trace::Status.error(error_message_for(exception)) + span.status = OpenTelemetry::Trace::Status.error(_otel_error_message_for(exception)) else span.set_attribute("servactory.result", "error") span.status = OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{exception.class}") end end - def error_message_for(error) + def _otel_error_message_for(error) return "Service failure" unless error if error.respond_to?(:message) @@ -116,14 +116,14 @@ def error_message_for(error) end end - def failure_attributes(error) + def _otel_failure_attributes(error) attrs = {} attrs["servactory.failure.type"] = error.type.to_s if error.respond_to?(:type) && error.type attrs["servactory.failure.message"] = error.message.to_s if error.respond_to?(:message) && error.message attrs end - def tracer + def _otel_tracer OpenTelemetry::Instrumentation::Servactory::Instrumentation.instance.tracer end end diff --git a/lib/opentelemetry/instrumentation/servactory/patches/runner.rb b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb index 67baf94..68d304c 100644 --- a/lib/opentelemetry/instrumentation/servactory/patches/runner.rb +++ b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb @@ -8,7 +8,7 @@ module Runner private def call_action(action) - span = start_action_span(action) + span = _otel_start_action_span(action) OpenTelemetry::Trace.with_span(span) do super rescue StandardError => e @@ -20,14 +20,14 @@ def call_action(action) end end - def start_action_span(action) + def _otel_start_action_span(action) service_name = @context.class.name || "AnonymousService" - attributes = build_action_attributes(service_name, action.name.to_s) + attributes = _otel_build_action_attributes(service_name, action.name.to_s) - tracer.start_span("#{service_name} #{action.name}", attributes:, kind: :internal) + _otel_tracer.start_span("#{service_name} #{action.name}", attributes:, kind: :internal) end - def build_action_attributes(service_name, action_name) + def _otel_build_action_attributes(service_name, action_name) { "code.namespace" => service_name, "code.function" => action_name, @@ -36,7 +36,7 @@ def build_action_attributes(service_name, action_name) } end - def tracer + def _otel_tracer OpenTelemetry::Instrumentation::Servactory::Instrumentation.instance.tracer end end From 68440b9e36c621aaf98093734adab3aa5f43cce7 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 13:53:36 +0300 Subject: [PATCH 16/22] Relax version constraints for OpenTelemetry dependencies **Summary of changes:** - Updated `opentelemetry-api` dependency from `>= 1.0` to `~> 1.0`. - Updated `opentelemetry-instrumentation-base` dependency from `>= 0.22` to `~> 0.22`. - Updated `opentelemetry-sdk` development dependency from `>= 1.4` to `~> 1.4`. - Synchronized changes in `Gemfile.lock` for consistency. - Applies stricter version constraints to improve dependency compatibility control. --- Gemfile.lock | 6 +++--- opentelemetry-instrumentation-servactory.gemspec | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 546e02f..bb136ca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,8 +2,8 @@ PATH remote: . specs: opentelemetry-instrumentation-servactory (0.1.0) - opentelemetry-api (>= 1.0) - opentelemetry-instrumentation-base (>= 0.22) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22) GEM remote: https://rubygems.org/ @@ -158,7 +158,7 @@ PLATFORMS DEPENDENCIES appraisal (>= 2.5) opentelemetry-instrumentation-servactory! - opentelemetry-sdk (>= 1.4) + opentelemetry-sdk (~> 1.4) rspec (>= 3.13) servactory (>= 2.16.0) servactory-rubocop (>= 0.9) diff --git a/opentelemetry-instrumentation-servactory.gemspec b/opentelemetry-instrumentation-servactory.gemspec index 3af32cb..edbb1af 100644 --- a/opentelemetry-instrumentation-servactory.gemspec +++ b/opentelemetry-instrumentation-servactory.gemspec @@ -29,11 +29,11 @@ Gem::Specification.new do |spec| spec.required_ruby_version = Gem::Requirement.new(">= 3.2") - spec.add_dependency "opentelemetry-api", ">= 1.0" - spec.add_dependency "opentelemetry-instrumentation-base", ">= 0.22" + spec.add_dependency "opentelemetry-api", "~> 1.0" + spec.add_dependency "opentelemetry-instrumentation-base", "~> 0.22" spec.add_development_dependency "appraisal", ">= 2.5" - spec.add_development_dependency "opentelemetry-sdk", ">= 1.4" + spec.add_development_dependency "opentelemetry-sdk", "~> 1.4" spec.add_development_dependency "rspec", ">= 3.13" spec.add_development_dependency "servactory", ">= 2.16.0" spec.add_development_dependency "servactory-rubocop", ">= 0.9" From e0060d01ba7b50ea8ffb207d1d95326267edfe5d Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 13:55:39 +0300 Subject: [PATCH 17/22] Handle span lifecycle and improve exception handling in Callable **Summary of changes:** - Updated `call` method in `Patches::Callable` to explicitly manage span lifecycle with `span.start`, `with_span`, and `span.finish`. - Added exception handling to record errors on spans using `_otel_record_exception_on_span`. - Enhanced error handling in `_otel_input_names` and `_otel_output_names` to log exceptions via `OpenTelemetry.handle_error`. - Fixed potential nil reference issue in `Patches::Runner` by safely accessing `@context&.class&.name`. - Improved resilience and traceability for Servactory instrumentation. --- .../servactory/patches/callable.rb | 18 +++++++++++++----- .../servactory/patches/runner.rb | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb index c830236..ff352b2 100644 --- a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb +++ b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb @@ -4,15 +4,21 @@ module OpenTelemetry module Instrumentation module Servactory module Patches - module Callable - def call(...) + module Callable # rubocop:disable Metrics/ModuleLength + def call(...) # rubocop:disable Metrics/MethodLength service_name = name || "AnonymousService" attributes = _otel_build_span_attributes(service_name, "call") - _otel_tracer.in_span("#{service_name} call", attributes:, kind: :internal) do |span| + span = _otel_tracer.start_span("#{service_name} call", attributes:, kind: :internal) + OpenTelemetry::Trace.with_span(span) do result = super _otel_record_result(span, result) result + rescue StandardError => e + _otel_record_exception_on_span(span, e) + raise + ensure + span.finish end end @@ -62,13 +68,15 @@ def _otel_append_attribute_names(attributes) def _otel_input_names send(:collection_of_inputs).names.map(&:to_s) - rescue StandardError + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: "Failed to collect Servactory input names") [] end def _otel_output_names send(:collection_of_outputs).names.map(&:to_s) - rescue StandardError + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: "Failed to collect Servactory output names") [] end diff --git a/lib/opentelemetry/instrumentation/servactory/patches/runner.rb b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb index 68d304c..2117390 100644 --- a/lib/opentelemetry/instrumentation/servactory/patches/runner.rb +++ b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb @@ -21,7 +21,7 @@ def call_action(action) end def _otel_start_action_span(action) - service_name = @context.class.name || "AnonymousService" + service_name = @context&.class&.name || "AnonymousService" attributes = _otel_build_action_attributes(service_name, action.name.to_s) _otel_tracer.start_span("#{service_name} #{action.name}", attributes:, kind: :internal) From c675c45bf8d50a501b5b8094a2674b674e882f55 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 13:55:44 +0300 Subject: [PATCH 18/22] Handle exception propagation and span updates in Callable spec **Summary of changes:** - Added tests for handling unexpected exceptions in `Patches::Callable`. - Verified exception logging on spans using `exception` events. - Ensured error results are set as `servactory.result` attribute in spans. - Confirmed exceptions are re-raised after being recorded. - Improved coverage for exception scenarios in Servactory instrumentation. --- .../servactory/patches/callable_spec.rb | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb index 3a9a6e3..b4f38bb 100644 --- a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb @@ -64,6 +64,29 @@ expect(span.attributes["servactory.result"]).to eq("success") end end + + context "when service raises unexpected exception" do + let(:span) { spans.find { |s| s.name == "ExceptionService call" } } + + before do + ExceptionService.call + rescue StandardError + nil + end + + it "records the exception on span" do + exception_event = span.events&.find { |e| e.name == "exception" } + expect(exception_event).not_to be_nil + end + + it "sets error result" do + expect(span.attributes["servactory.result"]).to eq("error") + end + + it "re-raises the exception" do + expect { ExceptionService.call }.to raise_error(StandardError, /unexpected error/) + end + end end describe "#call!" do From e66eb2d931c9368eab393eb46743b92415577667 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 13:57:14 +0300 Subject: [PATCH 19/22] Standardize boolean expectations formatting in Servactory tests **Summary of changes:** - Updated test assertions to use parentheses with boolean values for consistency (e.g., `to be(true)`). - Modified expectations in `#compatible?` and `#install` spec blocks. - Improved readability and alignment with testing conventions. --- .../instrumentation/servactory/instrumentation_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb b/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb index 22d2fcc..94517ca 100644 --- a/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb @@ -16,16 +16,16 @@ end describe "#compatible?" do - it { expect(instrumentation.compatible?).to be true } + it { expect(instrumentation.compatible?).to be(true) } end describe "#install" do before { instrumentation.install } it "sets default config options", :aggregate_failures do - expect(instrumentation.config[:trace_actions]).to be true - expect(instrumentation.config[:record_input_names]).to be true - expect(instrumentation.config[:record_output_names]).to be true + expect(instrumentation.config[:trace_actions]).to be(true) + expect(instrumentation.config[:record_input_names]).to be(true) + expect(instrumentation.config[:record_output_names]).to be(true) end end end From ee729ad737e6c6a81e89203d3988fdea0210e738 Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 14:13:05 +0300 Subject: [PATCH 20/22] Simplify and improve test readability in instrumentation specs **Summary of changes:** - Reformatted test expectations to improve multi-line readability. - Introduced `let` variables (`root_span`, `step_one_span`, `exception_event`, etc.) for clearer span referencing. - Applied consistent indentation and aggregate failures pattern across specs. - Enhanced readability and maintainability for `Patches::Runner`, `Patches::Callable`, and general instrumentation tests. --- .../servactory/instrumentation_spec.rb | 15 ++- .../servactory/patches/callable_spec.rb | 95 ++++++++++++------- .../servactory/patches/runner_spec.rb | 40 +++++--- 3 files changed, 97 insertions(+), 53 deletions(-) diff --git a/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb b/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb index 94517ca..1c24291 100644 --- a/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb @@ -4,11 +4,13 @@ subject(:instrumentation) { described_class.instance } it "has a name" do - expect(instrumentation.name).to eq("OpenTelemetry::Instrumentation::Servactory") + expect(instrumentation.name) + .to eq("OpenTelemetry::Instrumentation::Servactory") end it "has a version" do - expect(instrumentation.version).to eq(OpenTelemetry::Instrumentation::Servactory::VERSION::STRING) + expect(instrumentation.version) + .to eq(OpenTelemetry::Instrumentation::Servactory::VERSION::STRING) end describe "#present?" do @@ -23,9 +25,12 @@ before { instrumentation.install } it "sets default config options", :aggregate_failures do - expect(instrumentation.config[:trace_actions]).to be(true) - expect(instrumentation.config[:record_input_names]).to be(true) - expect(instrumentation.config[:record_output_names]).to be(true) + expect(instrumentation.config[:trace_actions]) + .to be(true) + expect(instrumentation.config[:record_input_names]) + .to be(true) + expect(instrumentation.config[:record_output_names]) + .to be(true) end end end diff --git a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb index b4f38bb..3042ac3 100644 --- a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb @@ -12,25 +12,35 @@ before { result } it "creates a span" do - expect(span).not_to be_nil + expect(span) + .not_to be_nil end it "sets span attributes", :aggregate_failures do - expect(span.attributes["code.namespace"]).to eq("SuccessfulService") - expect(span.attributes["code.function"]).to eq("call") - expect(span.attributes["servactory.system"]).to eq("servactory") - expect(span.attributes["servactory.version"]).to eq(Servactory::VERSION::STRING) - expect(span.attributes["servactory.result"]).to eq("success") - expect(span.attributes["servactory.input_names"]).to eq(["name"]) - expect(span.attributes["servactory.output_names"]).to eq(["greeting"]) + expect(span.attributes["code.namespace"]) + .to eq("SuccessfulService") + expect(span.attributes["code.function"]) + .to eq("call") + expect(span.attributes["servactory.system"]) + .to eq("servactory") + expect(span.attributes["servactory.version"]) + .to eq(Servactory::VERSION::STRING) + expect(span.attributes["servactory.result"]) + .to eq("success") + expect(span.attributes["servactory.input_names"]) + .to eq(["name"]) + expect(span.attributes["servactory.output_names"]) + .to eq(["greeting"]) end it "sets OK status" do - expect(span.status.code).to eq(OpenTelemetry::Trace::Status::OK) + expect(span.status.code) + .to eq(OpenTelemetry::Trace::Status::OK) end it "does not alter the return value" do - expect(result.greeting).to eq("Hello, World!") + expect(result.greeting) + .to eq("Hello, World!") end end @@ -38,35 +48,42 @@ subject(:result) { FailingService.call(should_fail: true) } let(:span) { spans.find { |s| s.name == "FailingService call" } } + let(:failure_event) { span.events&.find { |e| e.name == "servactory.failure" } } before { result } it "sets span attributes", :aggregate_failures do - expect(span.attributes["servactory.result"]).to eq("failure") - expect(span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) + expect(span.attributes["servactory.result"]) + .to eq("failure") + expect(span.status.code) + .to eq(OpenTelemetry::Trace::Status::ERROR) end it "adds a failure event" do - failure_event = span.events&.find { |e| e.name == "servactory.failure" } - expect(failure_event).not_to be_nil + expect(failure_event) + .not_to be_nil end it "returns a failure result" do - expect(result).to be_failure + expect(result) + .to be_failure end end context "when service does not fail" do + let(:span) { spans.find { |s| s.name == "FailingService call" } } + before { FailingService.call(should_fail: false) } it "sets success result" do - span = spans.find { |s| s.name == "FailingService call" } - expect(span.attributes["servactory.result"]).to eq("success") + expect(span.attributes["servactory.result"]) + .to eq("success") end end context "when service raises unexpected exception" do let(:span) { spans.find { |s| s.name == "ExceptionService call" } } + let(:exception_event) { span.events&.find { |e| e.name == "exception" } } before do ExceptionService.call @@ -75,16 +92,18 @@ end it "records the exception on span" do - exception_event = span.events&.find { |e| e.name == "exception" } - expect(exception_event).not_to be_nil + expect(exception_event) + .not_to be_nil end it "sets error result" do - expect(span.attributes["servactory.result"]).to eq("error") + expect(span.attributes["servactory.result"]) + .to eq("error") end it "re-raises the exception" do - expect { ExceptionService.call }.to raise_error(StandardError, /unexpected error/) + expect { ExceptionService.call } + .to raise_error(StandardError, /unexpected error/) end end end @@ -98,21 +117,26 @@ before { result } it "creates a span" do - expect(span).not_to be_nil + expect(span) + .not_to be_nil end it "sets span attributes", :aggregate_failures do - expect(span.attributes["servactory.result"]).to eq("success") - expect(span.status.code).to eq(OpenTelemetry::Trace::Status::OK) + expect(span.attributes["servactory.result"]) + .to eq("success") + expect(span.status.code) + .to eq(OpenTelemetry::Trace::Status::OK) end it "does not alter the return value" do - expect(result.greeting).to eq("Hello, World!") + expect(result.greeting) + .to eq("Hello, World!") end end context "when service fails with Servactory failure" do let(:span) { spans.find { |s| s.name == "FailingService call!" } } + let(:exception_event) { span.events&.find { |e| e.name == "exception" } } before do FailingService.call!(should_fail: true) @@ -121,22 +145,26 @@ end it "records the exception on span" do - exception_event = span.events&.find { |e| e.name == "exception" } - expect(exception_event).not_to be_nil + expect(exception_event) + .not_to be_nil end it "sets span attributes", :aggregate_failures do - expect(span.attributes["servactory.result"]).to eq("failure") - expect(span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) + expect(span.attributes["servactory.result"]) + .to eq("failure") + expect(span.status.code) + .to eq(OpenTelemetry::Trace::Status::ERROR) end it "re-raises the exception" do - expect { FailingService.call!(should_fail: true) }.to raise_error(StandardError, /Something went wrong/) + expect { FailingService.call!(should_fail: true) } + .to raise_error(StandardError, /Something went wrong/) end end context "when service raises unexpected exception" do let(:span) { spans.find { |s| s.name == "ExceptionService call!" } } + let(:exception_event) { span.events&.find { |e| e.name == "exception" } } before do ExceptionService.call! @@ -145,12 +173,13 @@ end it "records the exception on span" do - exception_event = span.events&.find { |e| e.name == "exception" } - expect(exception_event).not_to be_nil + expect(exception_event) + .not_to be_nil end it "sets error result" do - expect(span.attributes["servactory.result"]).to eq("error") + expect(span.attributes["servactory.result"]) + .to eq("error") end end end diff --git a/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb index a98d505..58744c4 100644 --- a/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb @@ -7,35 +7,44 @@ context "when service has multiple actions" do subject(:result) { MultiActionService.call(value: 5) } + let(:root_span) { spans.find { |s| s.name == "MultiActionService call" } } + let(:step_one_span) { spans.find { |s| s.name == "MultiActionService step_one" } } + let(:step_two_span) { spans.find { |s| s.name == "MultiActionService step_two" } } + before { result } it "creates child spans for each action", :aggregate_failures do - expect(spans.find { |s| s.name == "MultiActionService step_one" }).not_to be_nil - expect(spans.find { |s| s.name == "MultiActionService step_two" }).not_to be_nil + expect(step_one_span) + .not_to be_nil + expect(step_two_span) + .not_to be_nil end it "sets span attributes", :aggregate_failures do - span = spans.find { |s| s.name == "MultiActionService step_one" } - expect(span.attributes["code.namespace"]).to eq("MultiActionService") - expect(span.attributes["code.function"]).to eq("step_one") - expect(span.attributes["servactory.system"]).to eq("servactory") - expect(span.attributes["servactory.version"]).to eq(Servactory::VERSION::STRING) + expect(step_one_span.attributes["code.namespace"]) + .to eq("MultiActionService") + expect(step_one_span.attributes["code.function"]) + .to eq("step_one") + expect(step_one_span.attributes["servactory.system"]) + .to eq("servactory") + expect(step_one_span.attributes["servactory.version"]) + .to eq(Servactory::VERSION::STRING) end it "creates action spans as children of the root span" do - root_span = spans.find { |s| s.name == "MultiActionService call" } - action_span = spans.find { |s| s.name == "MultiActionService step_one" } - - expect(action_span.parent_span_id).to eq(root_span.span_id) + expect(step_one_span.parent_span_id) + .to eq(root_span.span_id) end it "does not alter the return value" do - expect(result.result).to eq(11) + expect(result.result) + .to eq(11) end end context "when action raises an exception" do let(:span) { spans.find { |s| s.name == "ExceptionService blow_up" } } + let(:exception_event) { span.events&.find { |e| e.name == "exception" } } before do ExceptionService.call @@ -44,12 +53,13 @@ end it "records exception on action span" do - exception_event = span.events&.find { |e| e.name == "exception" } - expect(exception_event).not_to be_nil + expect(exception_event) + .not_to be_nil end it "sets ERROR status on action span" do - expect(span.status.code).to eq(OpenTelemetry::Trace::Status::ERROR) + expect(span.status.code) + .to eq(OpenTelemetry::Trace::Status::ERROR) end end end From ecd59c26ff84ae64f1981bc7ad2e43f63fadb98e Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 14:16:32 +0300 Subject: [PATCH 21/22] Remove `servactory.system` attribute from spans in instrumentation **Summary of changes:** - Deleted the addition of the `servactory.system` attribute from spans in `Patches::Callable` and `Patches::Runner`. - Updated associated tests in `runner_spec.rb` and `callable_spec.rb` to reflect the removal of the attribute. - Simplified span attribute setup by retaining only relevant attributes (`servactory.version`, etc.). - Improved code and test clarity by eliminating redundant attributes. --- .../instrumentation/servactory/patches/callable.rb | 1 - lib/opentelemetry/instrumentation/servactory/patches/runner.rb | 1 - .../instrumentation/servactory/patches/callable_spec.rb | 2 -- .../instrumentation/servactory/patches/runner_spec.rb | 2 -- 4 files changed, 6 deletions(-) diff --git a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb index ff352b2..a3c477e 100644 --- a/lib/opentelemetry/instrumentation/servactory/patches/callable.rb +++ b/lib/opentelemetry/instrumentation/servactory/patches/callable.rb @@ -45,7 +45,6 @@ def _otel_build_span_attributes(service_name, method_name) attributes = { "code.namespace" => service_name, "code.function" => method_name, - "servactory.system" => "servactory", "servactory.version" => ::Servactory::VERSION::STRING } _otel_append_attribute_names(attributes) diff --git a/lib/opentelemetry/instrumentation/servactory/patches/runner.rb b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb index 2117390..8b21ace 100644 --- a/lib/opentelemetry/instrumentation/servactory/patches/runner.rb +++ b/lib/opentelemetry/instrumentation/servactory/patches/runner.rb @@ -31,7 +31,6 @@ def _otel_build_action_attributes(service_name, action_name) { "code.namespace" => service_name, "code.function" => action_name, - "servactory.system" => "servactory", "servactory.version" => ::Servactory::VERSION::STRING } end diff --git a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb index 3042ac3..fd423b4 100644 --- a/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/patches/callable_spec.rb @@ -21,8 +21,6 @@ .to eq("SuccessfulService") expect(span.attributes["code.function"]) .to eq("call") - expect(span.attributes["servactory.system"]) - .to eq("servactory") expect(span.attributes["servactory.version"]) .to eq(Servactory::VERSION::STRING) expect(span.attributes["servactory.result"]) diff --git a/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb b/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb index 58744c4..878b37c 100644 --- a/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb +++ b/spec/opentelemetry/instrumentation/servactory/patches/runner_spec.rb @@ -25,8 +25,6 @@ .to eq("MultiActionService") expect(step_one_span.attributes["code.function"]) .to eq("step_one") - expect(step_one_span.attributes["servactory.system"]) - .to eq("servactory") expect(step_one_span.attributes["servactory.version"]) .to eq(Servactory::VERSION::STRING) end From e4e269c429fab0e5a117a3cafbbd18e91ba1276e Mon Sep 17 00:00:00 2001 From: Anton Sokolov Date: Sat, 21 Feb 2026 14:19:06 +0300 Subject: [PATCH 22/22] Remove reference to `servactory.system` in README **Summary of changes:** - Deleted the `servactory.system` attribute from the spans table in the README. - Updated documentation to reflect the attribute's removal for consistency with recent code changes. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 0889b34..a4d30bb 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,6 @@ Users::CreateService call (root span) | --- | --- | --- | | `code.namespace` | String | Service class name | | `code.function` | String | Method name (`call`, `call!`, or action name) | -| `servactory.system` | String | Framework identifier (`servactory`) | | `servactory.version` | String | Servactory library version | | `servactory.result` | String | `success`, `failure`, or `error` | | `servactory.input_names` | Array | Input attribute names (when `record_input_names` is enabled) |