Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ Style/Documentation:
RSpec/SpecFilePathFormat:
CustomTransform:
OpenTelemetry: opentelemetry

Lint/EmptyBlock:
Exclude:
- 'Appraisals'
24 changes: 16 additions & 8 deletions Appraisals
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
# frozen_string_literal: true

appraise "rails-5.1"
appraise "rails-5.1" do
end

appraise "rails-5.2"
appraise "rails-5.2" do
end

appraise "rails-6.0"
appraise "rails-6.0" do
end

appraise "rails-6.1"
appraise "rails-6.1" do
end

appraise "rails-7.0"
appraise "rails-7.0" do
end

appraise "rails-7.1"
appraise "rails-7.1" do
end

appraise "rails-7.2"
appraise "rails-7.2" do
end

appraise "rails-8.0"
appraise "rails-8.0" do
end
18 changes: 18 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ GEM
lint_roller (1.1.0)
logger (1.7.0)
minitest (5.25.5)
mutex_m (0.3.0)
opentelemetry-api (1.6.0)
opentelemetry-common (0.22.0)
opentelemetry-api (~> 1.0)
Expand All @@ -49,6 +50,13 @@ GEM
opentelemetry-registry (~> 0.1)
opentelemetry-registry (0.4.0)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.8.1)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.11.0)
opentelemetry-api (~> 1.0)
parallel (1.27.0)
parser (3.3.9.0)
ast (~> 2.4.1)
Expand Down Expand Up @@ -111,6 +119,13 @@ GEM
rubocop-rspec (~> 3.5)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
servactory (2.16.0)
activesupport (>= 5.1, < 8.1)
base64 (>= 0.2)
bigdecimal (>= 3.1)
i18n (>= 1.14)
mutex_m (>= 0.3)
zeitwerk (>= 2.6)
servactory-rubocop (0.9.0)
rubocop (>= 1.73)
rubocop-factory_bot (>= 2.27)
Expand All @@ -126,6 +141,7 @@ GEM
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
zeitwerk (2.7.3)

PLATFORMS
aarch64-linux-gnu
Expand All @@ -143,7 +159,9 @@ PLATFORMS
DEPENDENCIES
appraisal (>= 2.5)
opentelemetry-instrumentation-servactory!
opentelemetry-sdk (~> 1.1)
rspec (>= 3.13)
servactory (>= 2.16.0)
servactory-rubocop (>= 0.9)

BUNDLED WITH
Expand Down
41 changes: 37 additions & 4 deletions lib/opentelemetry/instrumentation/servactory/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,53 @@ module OpenTelemetry
module Instrumentation
module Servactory
class Instrumentation < OpenTelemetry::Instrumentation::Base
MINIMUM_VERSION = Gem::Version.new("2.16.0")

install do |_config|
require_dependencies
install_patches
end

# TODO: constantize base_class from config[:base_class]
option :base_class, default: nil, validate: ->(value) { value.present? && value.is_a?(Class) }

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.new(::Servactory::VERSION::STRING) >= MINIMUM_VERSION
end

private

def require_dependencies
# TODO: Include instrumentation dependencies
require_relative "patches/extension"
end

def install_patches
base_class = prepare_base_class
return if base_class.blank?

base_class.include(
::Servactory::DSL.with_extensions(Patches::Extension)
)
end

def prepare_base_class
# TODO: maybe constantize base_class from config[:base_class]?
base_class = config[:base_class]

if base_class.blank?
OpenTelemetry.logger.error(
"Instrumentation Servactory configuration option :base_class value=#{base_class.inspect} " \
"failed validation, installation aborted."
)

return
end

base_class
end
end
end
Expand Down
52 changes: 52 additions & 0 deletions lib/opentelemetry/instrumentation/servactory/patches/extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module OpenTelemetry
module Instrumentation
module Servactory
module Patches
module Extension
def self.included(base)
class << base
prepend ClassMethods
end
end

module ClassMethods
def call(...) # rubocop:disable Metrics/MethodLength
tracer
.in_span(
name,
attributes: {
"service" => name,
"method" => __method__.to_s,
"type" => ::Servactory.name.underscore,
"version" => ::Servactory::VERSION::STRING
}
) do
super
end
end

def call!(...) # rubocop:disable Metrics/MethodLength
tracer.in_span(
name,
attributes: {
"service" => name,
"method" => __method__.to_s,
"type" => ::Servactory.name.underscore,
"version" => ::Servactory::VERSION::STRING
}
) do
super
end
end

def tracer
Servactory::Instrumentation.instance.tracer
end
end
end
end
end
end
end
2 changes: 2 additions & 0 deletions opentelemetry-instrumentation-servactory.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Gem::Specification.new do |spec|
spec.add_dependency "opentelemetry-instrumentation-base", "~> 0.23.0"

spec.add_development_dependency "appraisal", ">= 2.5"
spec.add_development_dependency "opentelemetry-sdk", "~> 1.1"
spec.add_development_dependency "rspec", ">= 3.13"
spec.add_development_dependency "servactory", ">= 2.16.0"
spec.add_development_dependency "servactory-rubocop", ">= 0.9"
end
149 changes: 149 additions & 0 deletions spec/opentelemetry/instrumentation/servactory/instrumentation_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# frozen_string_literal: true

RSpec.describe OpenTelemetry::Instrumentation::Servactory::Instrumentation do
let(:instrumentation) { described_class.instance }

describe "fields" do
it "has #name" do
expect(instrumentation.name).to(eq("OpenTelemetry::Instrumentation::Servactory"))
end

it "has #version" do
expect(instrumentation.version::STRING).to(eq("0.1.0"))
end
end

describe "#install" do
let(:config) { { base_class: } }

before do
skip "#{Servactory::VERSION} is not supported" unless instrumentation.compatible?
allow(OpenTelemetry.logger).to(receive(:error))
end

after do
# Force re-install of instrumentation
instrumentation.instance_variable_set(:@installed, false)
instrumentation.instance_variable_set(:@config, {})
end

context "with valid config" do
let(:base_class) { Class.new }

it "accepts argument" do
expect(instrumentation.install(config)).to(be_truthy)
end

it "does not warn about config" do
instrumentation.install(config)

expect(OpenTelemetry.logger).not_to(
have_received(:error)
)
end
end

context "with invalid config" do
let(:base_class) { nil }

it "warns about nil base class" do
instrumentation.install(config)

expect(OpenTelemetry.logger).to(
have_received(:error).with(
"Instrumentation Servactory configuration option :base_class value=nil " \
"failed validation, installation aborted."
)
)
end
end
end

describe "span creation" do
let(:exporter) { EXPORTER }
let(:spans) { exporter.finished_spans }
let(:root_span) { spans.find { |s| s.parent_span_id == OpenTelemetry::Trace::INVALID_SPAN_ID } }

let(:dummy_service) do
Class.new(Servactory::Base) do
def self.name = "DummyService"

make :step

def step; end
end
end

before do
dummy_service
exporter.reset
instrumentation.install(config)
end

after do
instrumentation.instance_variable_set(:@installed, false)
instrumentation.instance_variable_set(:@config, {})
end

context "with invalid config" do
let(:config) { { base_class: nil } }

shared_examples "expected span creation skip" do |call_strategy:|
let(:method_name) { call_strategy }
let(:service_call) { dummy_service.public_send(method_name, {}) }

it "does not create spans" do
expect { service_call }.not_to(
change { exporter.finished_spans.size }
)
end
end

it_behaves_like "expected span creation skip", call_strategy: "call"
it_behaves_like "expected span creation skip", call_strategy: "call!"
end

context "with valid config" do
let(:config) { { base_class: dummy_service } }

shared_examples "successful span creation" do |call_strategy:|
let(:method_name) { call_strategy }
let(:service_call) { dummy_service.public_send(method_name, {}) }

it "creates correct count of spans" do
expect { service_call }.to(
change { exporter.finished_spans.size }.by(1)
)
end

shared_examples "correct span attributes" do
before { service_call }

it "creates correct spans" do
expect(target_span).to(
have_attributes(
name: "DummyService",
kind: :internal,
attributes: {
"service" => "DummyService",
"method" => method_name,
"type" => "servactory",
"version" => Servactory::VERSION::STRING
}
)
)
end
end

describe "root span" do
it_behaves_like "correct span attributes" do
let(:target_span) { root_span }
end
end
end

it_behaves_like "successful span creation", call_strategy: "call"
it_behaves_like "successful span creation", call_strategy: "call!"
end
end
end
11 changes: 11 additions & 0 deletions spec/otel_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

# global opentelemetry-sdk setup:
EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new
span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER)

OpenTelemetry::SDK.configure do |c|
c.error_handler = ->(exception:, message:) { raise(exception || message) }
c.logger = Logger.new($stderr, level: ENV.fetch("OTEL_LOG_LEVEL", "warn").to_sym)
c.add_span_processor span_processor
end
18 changes: 18 additions & 0 deletions spec/rspec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"

# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!

config.expect_with :rspec do |c|
c.syntax = :expect

# Configures the maximum character length that RSpec will print while
# formatting an object. You can set length to nil to prevent RSpec from
# doing truncation.
c.max_formatted_output_length = nil
end
end
Loading