From ed1e756871afc148615291d0fa1cd0026d9b9821 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Tue, 3 Sep 2024 14:18:13 -0400 Subject: [PATCH] Nh-90184: add feature - add_tracer on function --- .rubocop.yml | 3 + lib/solarwinds_apm/api.rb | 2 + .../api/custom_instrumentation.rb | 31 ++++++ test/api/custom_instrumentation_test.rb | 102 ++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 lib/solarwinds_apm/api/custom_instrumentation.rb create mode 100644 test/api/custom_instrumentation_test.rb diff --git a/.rubocop.yml b/.rubocop.yml index bf12bb0..00fd503 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -48,3 +48,6 @@ Metrics/BlockNesting: Exclude: - 'lib/solarwinds_apm.rb' - 'lib/solarwinds_apm/support.rb' +Lint/ConstantDefinitionInBlock: + Exclude: + - 'test/api/custom_instrumentation_test.rb' diff --git a/lib/solarwinds_apm/api.rb b/lib/solarwinds_apm/api.rb index 882118c..ffae6ee 100644 --- a/lib/solarwinds_apm/api.rb +++ b/lib/solarwinds_apm/api.rb @@ -11,6 +11,7 @@ require_relative 'api/tracing' require_relative 'api/opentelemetry' require_relative 'api/custom_metrics' +require_relative 'api/custom_instrumentation' module SolarWindsAPM module API @@ -19,5 +20,6 @@ module API extend SolarWindsAPM::API::Tracing extend SolarWindsAPM::API::OpenTelemetry extend SolarWindsAPM::API::CustomMetrics + extend SolarWindsAPM::API::Tracer end end diff --git a/lib/solarwinds_apm/api/custom_instrumentation.rb b/lib/solarwinds_apm/api/custom_instrumentation.rb new file mode 100644 index 0000000..2a86b7c --- /dev/null +++ b/lib/solarwinds_apm/api/custom_instrumentation.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# © 2024 SolarWinds Worldwide, LLC. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +module SolarWindsAPM + module API + module Tracer + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def add_tracer(method_name, span_name = nil, options = {}) + span_name = name.nil? ? "#{to_s.split(':').last&.tr('>', '')}/#{__method__}" : "#{name}/#{__method__}" if span_name.nil? + + original_method = instance_method(method_name) + + define_method(method_name) do |*args, &block| + SolarWindsAPM::API.in_span(span_name, **options) do |_span| + original_method.bind_call(self, *args, &block) + end + end + end + end + end + end +end diff --git a/test/api/custom_instrumentation_test.rb b/test/api/custom_instrumentation_test.rb new file mode 100644 index 0000000..8614fb1 --- /dev/null +++ b/test/api/custom_instrumentation_test.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +# Copyright (c) 2023 SolarWinds, LLC. +# All rights reserved. + +require 'minitest_helper' +require './lib/solarwinds_apm/api' + +describe 'SolarWinds Custom Instrumentation Test' do + let(:sdk) { OpenTelemetry::SDK } + let(:exporter) { sdk::Trace::Export::InMemorySpanExporter.new } + let(:span_processor) { sdk::Trace::Export::SimpleSpanProcessor.new(exporter) } + let(:provider) do + OpenTelemetry.tracer_provider = sdk::Trace::TracerProvider.new.tap do |provider| + provider.add_span_processor(span_processor) + end + end + let(:tracer) { provider.tracer(__FILE__, sdk::VERSION) } + let(:parent_context) { OpenTelemetry::Context.empty } + let(:finished_spans) { exporter.finished_spans } + + before do + ENV['OTEL_SERVICE_NAME'] = __FILE__ + end + + after do + ENV.delete('OTEL_SERVICE_NAME') + end + + it 'test_custom_instrumentation_simple_case' do + class MyClass + include SolarWindsAPM::API::Tracer + def new_method(param1, param2) + param1 + param2 + end + + add_tracer :new_method + end + + OpenTelemetry::Context.with_current(parent_context) do + tracer.in_span('root') do + my_class = MyClass.new + my_class.new_method(1, 2) + end + end + + skip if finished_spans.empty? + + _(finished_spans.size).must_equal(2) + _(finished_spans[0].name).must_equal('MyClass/add_tracer') + _(finished_spans[0].kind).must_equal(:internal) + end + + it 'test_custom_instrumentation_simple_case_with_custom_name_and_options' do + class MyClass + include SolarWindsAPM::API::Tracer + def new_method(param1, param2) + param1 + param2 + end + + add_tracer :new_method, 'custom_name', { attributes: { 'foo' => 'bar' }, kind: :consumer } + end + + OpenTelemetry::Context.with_current(parent_context) do + tracer.in_span('root') do + my_class = MyClass.new + my_class.new_method(1, 2) + end + end + + skip if finished_spans.empty? + + _(finished_spans[0].name).must_equal('custom_name') + _(finished_spans[0].attributes['foo']).must_equal('bar') + _(finished_spans[0].kind).must_equal(:consumer) + end + + it 'test_custom_instrumentation_instance_method' do + class MyClass + def self.new_method(param1, param2) + param1 + param2 + end + + class << self + include SolarWindsAPM::API::Tracer + add_tracer :new_method, 'custom_name', { attributes: { 'foo' => 'bar' }, kind: :unknown } + end + end + + OpenTelemetry::Context.with_current(parent_context) do + tracer.in_span('root') do + MyClass.new_method(1, 2) + end + end + + skip if finished_spans.empty? + + _(finished_spans[0].name).must_equal('custom_name') + _(finished_spans[0].attributes['foo']).must_equal('bar') + _(finished_spans[0].kind).must_equal(:unknown) + end +end