From b9df39e1cd6678dbb9be55fc910703fa0911a1f6 Mon Sep 17 00:00:00 2001 From: Grant Bourque Date: Fri, 28 Feb 2020 13:46:11 -0600 Subject: [PATCH] Add `Net:HTTP` instrumentation adapter (#187) Add `Net::HTTP` instrumentation adapter based on other HTTP client adapters and the related `ddtrace` code. --- .circleci/config.yml | 8 ++ Rakefile | 6 + adapters/net_http/.rubocop.yml | 27 +++++ adapters/net_http/Gemfile | 15 +++ adapters/net_http/Rakefile | 28 +++++ adapters/net_http/example/Gemfile | 7 ++ adapters/net_http/example/net_http.rb | 13 ++ .../lib/opentelemetry-adapters-net-http.rb | 7 ++ .../net_http/lib/opentelemetry/adapters.rb | 16 +++ .../lib/opentelemetry/adapters/net/http.rb | 20 ++++ .../adapters/net/http/adapter.rb | 36 ++++++ .../net/http/patches/instrumentation.rb | 62 ++++++++++ .../adapters/net/http/version.rb | 15 +++ .../opentelemetry-adapters-net-http.gemspec | 39 ++++++ adapters/net_http/test/.rubocop.yml | 4 + .../adapters/net/http/adapter_test.rb | 111 ++++++++++++++++++ adapters/net_http/test/test_helper.rb | 20 ++++ docker-compose.yml | 4 + 18 files changed, 438 insertions(+) create mode 100644 adapters/net_http/.rubocop.yml create mode 100644 adapters/net_http/Gemfile create mode 100644 adapters/net_http/Rakefile create mode 100644 adapters/net_http/example/Gemfile create mode 100644 adapters/net_http/example/net_http.rb create mode 100644 adapters/net_http/lib/opentelemetry-adapters-net-http.rb create mode 100644 adapters/net_http/lib/opentelemetry/adapters.rb create mode 100644 adapters/net_http/lib/opentelemetry/adapters/net/http.rb create mode 100644 adapters/net_http/lib/opentelemetry/adapters/net/http/adapter.rb create mode 100644 adapters/net_http/lib/opentelemetry/adapters/net/http/patches/instrumentation.rb create mode 100644 adapters/net_http/lib/opentelemetry/adapters/net/http/version.rb create mode 100644 adapters/net_http/opentelemetry-adapters-net-http.gemspec create mode 100644 adapters/net_http/test/.rubocop.yml create mode 100644 adapters/net_http/test/opentelemetry/adapters/net/http/adapter_test.rb create mode 100644 adapters/net_http/test/test_helper.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index 86535b9b9..0f643a82f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,6 +68,14 @@ commands: bundle install --jobs=3 --retry=3 bundle exec appraisal install bundle exec appraisal rake test + - run: + name: Bundle + CI (Adapters - Net::HTTP) + command: | + cd adapters/net_http + gem uninstall -aIx bundler + gem install --no-document bundler -v '~> 2.0.2' + bundle install --jobs=3 --retry=3 + bundle exec rake test - run: name: Bundle + CI (Adapters - Redis) command: | diff --git a/Rakefile b/Rakefile index 64947204a..a3f524caf 100644 --- a/Rakefile +++ b/Rakefile @@ -69,6 +69,12 @@ GEM_INFO = { OpenTelemetry::Adapters::Faraday::VERSION } }, + "opentelemetry-adapters-net_http" => { + version_getter: ->() { + require './lib/opentelemetry/adapters/net/http/version.rb' + OpenTelemetry::Adapters::Net::HTTP::VERSION + } + }, "opentelemetry-adapters-redis" => { version_getter: ->() { require './lib/opentelemetry/adapters/redis/version.rb' diff --git a/adapters/net_http/.rubocop.yml b/adapters/net_http/.rubocop.yml new file mode 100644 index 000000000..f38ba33ef --- /dev/null +++ b/adapters/net_http/.rubocop.yml @@ -0,0 +1,27 @@ +AllCops: + TargetRubyVersion: '2.4.0' + +Bundler/OrderedGems: + Exclude: + - gemfiles/**/* +Lint/UnusedMethodArgument: + Enabled: false +Metrics/AbcSize: + Max: 18 +Metrics/LineLength: + Enabled: false +Metrics/MethodLength: + Max: 20 +Metrics/ParameterLists: + Enabled: false +Naming/FileName: + Exclude: + - "lib/opentelemetry-adapters-net-http.rb" +Style/FrozenStringLiteralComment: + Exclude: + - gemfiles/**/* +Style/ModuleFunction: + Enabled: false +Style/StringLiterals: + Exclude: + - gemfiles/**/* diff --git a/adapters/net_http/Gemfile b/adapters/net_http/Gemfile new file mode 100644 index 000000000..8af0cd4fc --- /dev/null +++ b/adapters/net_http/Gemfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gemspec + +gem 'opentelemetry-api', path: '../../api' + +group :test do + gem 'opentelemetry-sdk', path: '../../sdk' +end diff --git a/adapters/net_http/Rakefile b/adapters/net_http/Rakefile new file mode 100644 index 000000000..197514f1d --- /dev/null +++ b/adapters/net_http/Rakefile @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'yard' +require 'rubocop/rake_task' + +RuboCop::RakeTask.new + +Rake::TestTask.new :test do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +YARD::Rake::YardocTask.new do |t| + t.stats_options = ['--list-undoc'] +end + +if RUBY_ENGINE == 'truffleruby' + task default: %i[test] +else + task default: %i[test rubocop yard] +end diff --git a/adapters/net_http/example/Gemfile b/adapters/net_http/example/Gemfile new file mode 100644 index 000000000..623b4b831 --- /dev/null +++ b/adapters/net_http/example/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'opentelemetry-adapters-net-http', path: '../../../adapters/net_http' +gem 'opentelemetry-api', path: '../../../api' +gem 'opentelemetry-sdk', path: '../../../sdk' diff --git a/adapters/net_http/example/net_http.rb b/adapters/net_http/example/net_http.rb new file mode 100644 index 000000000..65120e54e --- /dev/null +++ b/adapters/net_http/example/net_http.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rubygems' +require 'bundler/setup' +require 'net/http' + +Bundler.require + +OpenTelemetry::SDK.configure do |c| + c.use 'OpenTelemetry::Adapters::Net::HTTP' +end + +Net::HTTP.get(URI('http://example.com')) diff --git a/adapters/net_http/lib/opentelemetry-adapters-net-http.rb b/adapters/net_http/lib/opentelemetry-adapters-net-http.rb new file mode 100644 index 000000000..a2f3a3444 --- /dev/null +++ b/adapters/net_http/lib/opentelemetry-adapters-net-http.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require_relative './opentelemetry/adapters' diff --git a/adapters/net_http/lib/opentelemetry/adapters.rb b/adapters/net_http/lib/opentelemetry/adapters.rb new file mode 100644 index 000000000..d7bf2bace --- /dev/null +++ b/adapters/net_http/lib/opentelemetry/adapters.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + # "Instrumentation adapters" are specified by + # https://github.com/open-telemetry/opentelemetry-specification/blob/57714f7547fe4dcb342ad0ad10a80d86118431c7/specification/overview.md#instrumentation-adapters + # + # Adapters should be able to handle the case when the library is not installed on a user's system. + module Adapters + end +end + +require_relative './adapters/net/http' diff --git a/adapters/net_http/lib/opentelemetry/adapters/net/http.rb b/adapters/net_http/lib/opentelemetry/adapters/net/http.rb new file mode 100644 index 000000000..c2941ffb7 --- /dev/null +++ b/adapters/net_http/lib/opentelemetry/adapters/net/http.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry' + +module OpenTelemetry + module Adapters + module Net + # Contains the OpenTelemetry adapter for the Net::HTTP gem + module HTTP + end + end + end +end + +require_relative './http/adapter' +require_relative './http/version' diff --git a/adapters/net_http/lib/opentelemetry/adapters/net/http/adapter.rb b/adapters/net_http/lib/opentelemetry/adapters/net/http/adapter.rb new file mode 100644 index 000000000..4d66c242d --- /dev/null +++ b/adapters/net_http/lib/opentelemetry/adapters/net/http/adapter.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Adapters + module Net + module HTTP + # The Adapter class contains logic to detect and install the Net::HTTP + # instrumentation adapter + class Adapter < OpenTelemetry::Instrumentation::Adapter + install do |_config| + require_dependencies + patch + end + + present do + defined?(::Net::HTTP) + end + + private + + def require_dependencies + require_relative 'patches/instrumentation' + end + + def patch + ::Net::HTTP.prepend(Patches::Instrumentation) + end + end + end + end + end +end diff --git a/adapters/net_http/lib/opentelemetry/adapters/net/http/patches/instrumentation.rb b/adapters/net_http/lib/opentelemetry/adapters/net/http/patches/instrumentation.rb new file mode 100644 index 000000000..b5444c3ea --- /dev/null +++ b/adapters/net_http/lib/opentelemetry/adapters/net/http/patches/instrumentation.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Adapters + module Net + module HTTP + module Patches + # Module to prepend to Net::HTTP for instrumentation + module Instrumentation + HTTP_METHODS_TO_SPAN_NAMES = Hash.new { |h, k| h[k] = "HTTP #{k}" } + USE_SSL_TO_SCHEME = { false => 'http', true => 'https' }.freeze + + def request(req, body = nil, &block) + # Do not trace recursive call for starting the connection + return super(req, body, &block) unless started? + + tracer.in_span( + HTTP_METHODS_TO_SPAN_NAMES[req.method], + attributes: { + 'component' => 'http', + 'http.method' => req.method, + 'http.scheme' => USE_SSL_TO_SCHEME[use_ssl?], + 'http.target' => req.path, + 'peer.hostname' => @address, + 'peer.port' => @port + }, + kind: :client + ) do |span| + OpenTelemetry.propagation.inject(req) + + super(req, body, &block).tap do |response| + annotate_span_with_response!(span, response) + end + end + end + + private + + def annotate_span_with_response!(span, response) + return unless response&.code + + status_code = response.code.to_i + + span.set_attribute('http.status_code', status_code) + span.status = OpenTelemetry::Trace::Status.http_to_status( + status_code + ) + end + + def tracer + Net::HTTP::Adapter.instance.tracer + end + end + end + end + end + end +end diff --git a/adapters/net_http/lib/opentelemetry/adapters/net/http/version.rb b/adapters/net_http/lib/opentelemetry/adapters/net/http/version.rb new file mode 100644 index 000000000..e61d06795 --- /dev/null +++ b/adapters/net_http/lib/opentelemetry/adapters/net/http/version.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Adapters + module Net + module HTTP + VERSION = '0.0.0' + end + end + end +end diff --git a/adapters/net_http/opentelemetry-adapters-net-http.gemspec b/adapters/net_http/opentelemetry-adapters-net-http.gemspec new file mode 100644 index 000000000..9909552d1 --- /dev/null +++ b/adapters/net_http/opentelemetry-adapters-net-http.gemspec @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'opentelemetry/adapters/net/http/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-adapters-net-http' + spec.version = OpenTelemetry::Adapters::Net::HTTP::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'Net::HTTP instrumentation adapter for the OpenTelemetry framework' + spec.description = 'Net::HTTP instrumentation adapter for the OpenTelemetry framework' + spec.homepage = 'https://github.com/open-telemetry/opentelemetry-ruby' + spec.license = 'Apache-2.0' + + spec.files = ::Dir.glob('lib/**/*.rb') + + ::Dir.glob('*.md') + + ['LICENSE'] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 2.4.0' + + spec.add_dependency 'opentelemetry-api', '~> 0.0' + + spec.add_development_dependency 'bundler', '>= 1.17' + spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'opentelemetry-sdk', '~> 0.0' + spec.add_development_dependency 'rake', '~> 13.0.1' + spec.add_development_dependency 'rubocop', '~> 0.73.0' + spec.add_development_dependency 'simplecov', '~> 0.17.1' + spec.add_development_dependency 'webmock', '~> 3.7.6' + spec.add_development_dependency 'yard', '~> 0.9' + spec.add_development_dependency 'yard-doctest', '~> 0.1.6' +end diff --git a/adapters/net_http/test/.rubocop.yml b/adapters/net_http/test/.rubocop.yml new file mode 100644 index 000000000..dd9425858 --- /dev/null +++ b/adapters/net_http/test/.rubocop.yml @@ -0,0 +1,4 @@ +inherit_from: ../.rubocop.yml + +Metrics/BlockLength: + Enabled: false diff --git a/adapters/net_http/test/opentelemetry/adapters/net/http/adapter_test.rb b/adapters/net_http/test/opentelemetry/adapters/net/http/adapter_test.rb new file mode 100644 index 000000000..8b3586a46 --- /dev/null +++ b/adapters/net_http/test/opentelemetry/adapters/net/http/adapter_test.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../../lib/opentelemetry/adapters/net/http' +require_relative '../../../../../lib/opentelemetry/adapters/net/http/patches/instrumentation' + +describe OpenTelemetry::Adapters::Net::HTTP::Adapter do + let(:adapter) { OpenTelemetry::Adapters::Net::HTTP::Adapter.instance } + let(:exporter) { EXPORTER } + let(:span) { exporter.finished_spans.first } + + before do + exporter.reset + stub_request(:get, 'http://example.com/success').to_return(status: 200) + stub_request(:post, 'http://example.com/failure').to_return(status: 500) + stub_request(:get, 'https://example.com/timeout').to_timeout + + # these are currently empty, but this will future proof the test + @orig_injectors = OpenTelemetry.propagation.http_injectors + OpenTelemetry.propagation.http_injectors = [ + OpenTelemetry::Trace::Propagation.http_trace_context_injector + ] + end + + after do + # Force re-install of instrumentation + adapter.instance_variable_set(:@installed, false) + + OpenTelemetry.propagation.http_injectors = @orig_injectors + end + + describe 'tracing' do + before do + adapter.install + end + + it 'before request' do + _(exporter.finished_spans.size).must_equal 0 + end + + it 'after request with success code' do + ::Net::HTTP.get('example.com', '/success') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP GET' + _(span.attributes['component']).must_equal 'http' + _(span.attributes['http.method']).must_equal 'GET' + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['http.status_code']).must_equal 200 + _(span.attributes['http.target']).must_equal '/success' + _(span.attributes['peer.hostname']).must_equal 'example.com' + _(span.attributes['peer.port']).must_equal 80 + assert_requested( + :get, + 'http://example.com/success', + headers: { 'Traceparent' => "00-#{span.trace_id}-#{span.span_id}-01" } + ) + end + + it 'after request with failure code' do + ::Net::HTTP.post(URI('http://example.com/failure'), 'q' => 'ruby') + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP POST' + _(span.attributes['component']).must_equal 'http' + _(span.attributes['http.method']).must_equal 'POST' + _(span.attributes['http.scheme']).must_equal 'http' + _(span.attributes['http.status_code']).must_equal 500 + _(span.attributes['http.target']).must_equal '/failure' + _(span.attributes['peer.hostname']).must_equal 'example.com' + _(span.attributes['peer.port']).must_equal 80 + assert_requested( + :post, + 'http://example.com/failure', + headers: { 'Traceparent' => "00-#{span.trace_id}-#{span.span_id}-01" } + ) + end + + it 'after request timeout' do + expect do + ::Net::HTTP.get(URI('https://example.com/timeout')) + end.must_raise Net::OpenTimeout + + _(exporter.finished_spans.size).must_equal 1 + _(span.name).must_equal 'HTTP GET' + _(span.attributes['component']).must_equal 'http' + _(span.attributes['http.method']).must_equal 'GET' + _(span.attributes['http.scheme']).must_equal 'https' + _(span.attributes['http.status_code']).must_be_nil + _(span.attributes['http.target']).must_equal '/timeout' + _(span.attributes['peer.hostname']).must_equal 'example.com' + _(span.attributes['peer.port']).must_equal 443 + _(span.status.canonical_code).must_equal( + OpenTelemetry::Trace::Status::UNKNOWN_ERROR + ) + _(span.status.description).must_equal( + 'Unhandled exception of type: Net::OpenTimeout' + ) + assert_requested( + :get, + 'https://example.com/timeout', + headers: { 'Traceparent' => "00-#{span.trace_id}-#{span.span_id}-01" } + ) + end + end +end diff --git a/adapters/net_http/test/test_helper.rb b/adapters/net_http/test/test_helper.rb new file mode 100644 index 000000000..f0f156489 --- /dev/null +++ b/adapters/net_http/test/test_helper.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Copyright 2020 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'net/http' + +require 'opentelemetry/sdk' + +require 'minitest/autorun' +require 'webmock/minitest' + +# 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.add_span_processor span_processor +end diff --git a/docker-compose.yml b/docker-compose.yml index 62ae9663d..8b0f6e1e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,10 @@ services: <<: *base working_dir: /app/adapters/faraday/example + ex-adapter-net-http: + <<: *base + working_dir: /app/adapters/net_http/example + ex-adapter-redis: <<: *base environment: