From 9683dfa6f7106c2a4d82da9258bc42abba701114 Mon Sep 17 00:00:00 2001 From: Robert Laurin Date: Tue, 23 Mar 2021 10:20:03 -0500 Subject: [PATCH 1/2] feat: add opentelemetry instrumentation --- google-apis-core/Gemfile | 1 + google-apis-core/google-apis-core.gemspec | 1 + .../lib/google/apis/core/http_command.rb | 43 ++++++++++ .../lib/google/apis/core/opentelemetry.rb | 26 ++++++ .../google/apis/core/http_command_spec.rb | 83 +++++++++++++++++++ 5 files changed, 154 insertions(+) create mode 100644 google-apis-core/lib/google/apis/core/opentelemetry.rb diff --git a/google-apis-core/Gemfile b/google-apis-core/Gemfile index 4a0c5da3fcd..c0cffc3fba0 100644 --- a/google-apis-core/Gemfile +++ b/google-apis-core/Gemfile @@ -20,6 +20,7 @@ group :development do gem 'redis', '~> 3.2' gem 'logging', '~> 2.2' gem 'opencensus', '~> 0.4' + gem 'opentelemetry-sdk', '~> 0.15.0' gem 'httparty' end diff --git a/google-apis-core/google-apis-core.gemspec b/google-apis-core/google-apis-core.gemspec index 8d866ac3ff9..c494d9f01bc 100644 --- a/google-apis-core/google-apis-core.gemspec +++ b/google-apis-core/google-apis-core.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.required_ruby_version = '>= 2.5' + gem.add_runtime_dependency "opentelemetry-api", '~> 0.15.0' gem.add_runtime_dependency "representable", "~> 3.0" gem.add_runtime_dependency "retriable", ">= 2.0", "< 4.0" gem.add_runtime_dependency "addressable", "~> 2.5", ">= 2.5.1" diff --git a/google-apis-core/lib/google/apis/core/http_command.rb b/google-apis-core/lib/google/apis/core/http_command.rb index 62429b6aeae..bb2d7a2960f 100644 --- a/google-apis-core/lib/google/apis/core/http_command.rb +++ b/google-apis-core/lib/google/apis/core/http_command.rb @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'google/apis/core/opentelemetry' require 'addressable/uri' require 'addressable/template' require 'google/apis/options' @@ -97,6 +98,7 @@ def initialize(method, url, body: nil) # @raise [Google::Apis::AuthorizationError] Authorization is required def execute(client) prepare! + opentelemetry_begin_span opencensus_begin_span begin Retriable.retriable tries: options.retries + 1, @@ -125,6 +127,7 @@ def execute(client) end end ensure + opentelemetry_end_span opencensus_end_span @http_res = nil release! @@ -355,7 +358,46 @@ def safe_response_representation http_res http_res.inspect end + def opentelemetry_begin_span + return unless Google::Apis::Core::OpenTelemetry.instance.installed? + return if @opentelemetry_span + + attributes = { + 'http.host' => url.host.to_s, + 'http.method' => method.to_s, + 'http.target' => url.path.to_s, + 'peer.service' => url.host.to_s + } + + @opentelemetry_span = Google::Apis::Core::OpenTelemetry.instance.tracer.start_span(url.host.to_s, attributes: attributes) + rescue StandardError => e + # Log exceptions and continue, so OpenTelemetry failures don't cause + # the entire request to fail. + logger.debug("Error opening OpenTelemetry span: #{e}") + end + + def opentelemetry_end_span + return unless Google::Apis::Core::OpenTelemetry.instance.installed? + return unless @opentelemetry_span + + if @http_res + status_code = @http_res.status.to_i + @opentelemetry_span.set_attribute('http.status_code', status_code) + @opentelemetry_span.status = ::OpenTelemetry::Trace::Status.http_to_status( + status_code + ) + end + + @opentelemetry_span.finish + @opentelemetry_span = nil + rescue StandardError => e + # Log exceptions and continue, so failures don't cause leaks by + # aborting cleanup. + logger.debug("Error finishing OpenTelemetry span: #{e}") + end + def opencensus_begin_span + return if Google::Apis::Core::OpenTelemetry.instance.installed? return unless OPENCENSUS_AVAILABLE && options.use_opencensus return if @opencensus_span return unless OpenCensus::Trace.span_context @@ -381,6 +423,7 @@ def opencensus_begin_span end def opencensus_end_span + return if Google::Apis::Core::OpenTelemetry.instance.installed? return unless OPENCENSUS_AVAILABLE return unless @opencensus_span return unless OpenCensus::Trace.span_context diff --git a/google-apis-core/lib/google/apis/core/opentelemetry.rb b/google-apis-core/lib/google/apis/core/opentelemetry.rb new file mode 100644 index 00000000000..dc34e34f335 --- /dev/null +++ b/google-apis-core/lib/google/apis/core/opentelemetry.rb @@ -0,0 +1,26 @@ +# Copyright 2020 Google LLC +# +# 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. + +require 'opentelemetry-api' + +module Google + module Apis + module Core + class OpenTelemetry < OpenTelemetry::Instrumentation::Base + install { true } + present { true } + end + end + end +end diff --git a/google-apis-core/spec/google/apis/core/http_command_spec.rb b/google-apis-core/spec/google/apis/core/http_command_spec.rb index e3f3830595d..19b2dce2800 100644 --- a/google-apis-core/spec/google/apis/core/http_command_spec.rb +++ b/google-apis-core/spec/google/apis/core/http_command_spec.rb @@ -14,6 +14,14 @@ require 'spec_helper' require 'google/apis/core/http_command' +require 'opentelemetry-sdk' + +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 module Google module Apis @@ -392,8 +400,83 @@ class DecryptResponse expect(spans.size).to eql 0 end end + + it 'should not attempt to create an opentelemetry span' do + stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, '']) + command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') + + expect(Google::Apis::Core::OpenTelemetry.instance.tracer).not_to receive(:start_span) + command.execute(client) + end end if Google::Apis::Core::HttpCommand::OPENCENSUS_AVAILABLE + context('with opentelemetry integration installed') do + let(:instrumentation) { Google::Apis::Core::OpenTelemetry.instance } + let(:span) { EXPORTER.finished_spans.first } + + before do + EXPORTER.reset + instrumentation.install + end + + after { instrumentation.instance_variable_set(:@installed, false) } + + it 'should not attempt to create an opencesus span' do + stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, ''], body: "Hello world") + command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') + command.execute(client) + + expect(Google::Apis::Core::HttpCommand::OPENCENSUS_AVAILABLE).to be(true) + expect(OpenCensus::Trace).not_to receive(:start_span) + expect(OpenCensus::Trace).not_to receive(:end) + command.execute(client) + end + + it 'should create an OpenTelemetry span for a successful call' do + stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, ''], body: "Hello world") + command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') + command.execute(client) + + expect(span.name).to eq('www.googleapis.com') + expect(span.status).to be_ok + expect(span.instrumentation_library.name).to eq('Google::Apis::Core') + expect(span.instrumentation_library.version).to eq(Google::Apis::Core::VERSION) + expect(span.attributes).to include( + 'http.host' => 'www.googleapis.com', + 'http.method' => 'get', + 'http.target' => '/zoo/animals', + 'peer.service' => 'www.googleapis.com', + 'http.status_code' => 200 + ) + end + + it 'should create an OpenTelemetry span for a call failure' do + stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [403, '']) + command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') + expect { command.execute(client) }.to raise_error(Google::Apis::ClientError) + + expect(span.name).to eq('www.googleapis.com') + expect(span.status).not_to be_ok + expect(span.attributes).to include( + 'http.host' => 'www.googleapis.com', + 'http.method' => 'get', + 'http.target' => '/zoo/animals', + 'peer.service' => 'www.googleapis.com', + 'http.status_code' => 403 + ) + end + + it 'should not create an OpenTelemetry span if not installed' do + instrumentation.instance_variable_set(:@installed, false) + stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, '']) + command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') + + expect(instrumentation.tracer).not_to receive(:start_span) + command.execute(client) + expect(span).to be_nil + end + end + it 'should send repeated query parameters' do stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=1&a=2&a=3') .to_return(status: [200, '']) From db6abc9867d3b4e9c7bdad0464b429433343bb80 Mon Sep 17 00:00:00 2001 From: Robert Laurin Date: Fri, 28 May 2021 10:42:40 -0500 Subject: [PATCH 2/2] remove otel instrumentation base dependency --- google-apis-core/Gemfile | 2 +- google-apis-core/google-apis-core.gemspec | 2 +- .../lib/google/apis/core/http_command.rb | 76 +++++-------------- .../lib/google/apis/core/opentelemetry.rb | 26 ------- .../google/apis/core/http_command_spec.rb | 71 ++++------------- 5 files changed, 34 insertions(+), 143 deletions(-) delete mode 100644 google-apis-core/lib/google/apis/core/opentelemetry.rb diff --git a/google-apis-core/Gemfile b/google-apis-core/Gemfile index c0cffc3fba0..133d29027f3 100644 --- a/google-apis-core/Gemfile +++ b/google-apis-core/Gemfile @@ -20,7 +20,7 @@ group :development do gem 'redis', '~> 3.2' gem 'logging', '~> 2.2' gem 'opencensus', '~> 0.4' - gem 'opentelemetry-sdk', '~> 0.15.0' + gem 'opentelemetry-sdk' gem 'httparty' end diff --git a/google-apis-core/google-apis-core.gemspec b/google-apis-core/google-apis-core.gemspec index c494d9f01bc..52dd659eea9 100644 --- a/google-apis-core/google-apis-core.gemspec +++ b/google-apis-core/google-apis-core.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.required_ruby_version = '>= 2.5' - gem.add_runtime_dependency "opentelemetry-api", '~> 0.15.0' + gem.add_runtime_dependency "opentelemetry-api", '~> 1.0.0.rc1' gem.add_runtime_dependency "representable", "~> 3.0" gem.add_runtime_dependency "retriable", ">= 2.0", "< 4.0" gem.add_runtime_dependency "addressable", "~> 2.5", ">= 2.5.1" diff --git a/google-apis-core/lib/google/apis/core/http_command.rb b/google-apis-core/lib/google/apis/core/http_command.rb index bb2d7a2960f..17c6dff6ae7 100644 --- a/google-apis-core/lib/google/apis/core/http_command.rb +++ b/google-apis-core/lib/google/apis/core/http_command.rb @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'google/apis/core/opentelemetry' +require 'opentelemetry' require 'addressable/uri' require 'addressable/template' require 'google/apis/options' @@ -28,6 +28,7 @@ module Core class HttpCommand include Logging + OTEL_TRACER = OpenTelemetry.tracer_provider.tracer('Google::Apis::Core::HttpCommand', Google::Apis::Core::VERSION) RETRIABLE_ERRORS = [Google::Apis::ServerError, Google::Apis::RateLimitError, Google::Apis::TransmissionError] begin @@ -98,23 +99,25 @@ def initialize(method, url, body: nil) # @raise [Google::Apis::AuthorizationError] Authorization is required def execute(client) prepare! - opentelemetry_begin_span opencensus_begin_span begin - Retriable.retriable tries: options.retries + 1, - base_interval: 1, - multiplier: 2, - on: RETRIABLE_ERRORS do |try| - # This 2nd level retriable only catches auth errors, and supports 1 retry, which allows - # auth to be re-attempted without having to retry all sorts of other failures like - # NotFound, etc - auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1) - Retriable.retriable tries: auth_tries, - on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError], - on_retry: proc { |*| refresh_authorization } do - execute_once(client).tap do |result| - if block_given? - yield result, nil + url_host = url.host.to_s + OTEL_TRACER.in_span(url_host) do + Retriable.retriable tries: options.retries + 1, + base_interval: 1, + multiplier: 2, + on: RETRIABLE_ERRORS do |try| + # This 2nd level retriable only catches auth errors, and supports 1 retry, which allows + # auth to be re-attempted without having to retry all sorts of other failures like + # NotFound, etc + auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1) + Retriable.retriable tries: auth_tries, + on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError], + on_retry: proc { |*| refresh_authorization } do + execute_once(client).tap do |result| + if block_given? + yield result, nil + end end end end @@ -127,7 +130,6 @@ def execute(client) end end ensure - opentelemetry_end_span opencensus_end_span @http_res = nil release! @@ -358,46 +360,7 @@ def safe_response_representation http_res http_res.inspect end - def opentelemetry_begin_span - return unless Google::Apis::Core::OpenTelemetry.instance.installed? - return if @opentelemetry_span - - attributes = { - 'http.host' => url.host.to_s, - 'http.method' => method.to_s, - 'http.target' => url.path.to_s, - 'peer.service' => url.host.to_s - } - - @opentelemetry_span = Google::Apis::Core::OpenTelemetry.instance.tracer.start_span(url.host.to_s, attributes: attributes) - rescue StandardError => e - # Log exceptions and continue, so OpenTelemetry failures don't cause - # the entire request to fail. - logger.debug("Error opening OpenTelemetry span: #{e}") - end - - def opentelemetry_end_span - return unless Google::Apis::Core::OpenTelemetry.instance.installed? - return unless @opentelemetry_span - - if @http_res - status_code = @http_res.status.to_i - @opentelemetry_span.set_attribute('http.status_code', status_code) - @opentelemetry_span.status = ::OpenTelemetry::Trace::Status.http_to_status( - status_code - ) - end - - @opentelemetry_span.finish - @opentelemetry_span = nil - rescue StandardError => e - # Log exceptions and continue, so failures don't cause leaks by - # aborting cleanup. - logger.debug("Error finishing OpenTelemetry span: #{e}") - end - def opencensus_begin_span - return if Google::Apis::Core::OpenTelemetry.instance.installed? return unless OPENCENSUS_AVAILABLE && options.use_opencensus return if @opencensus_span return unless OpenCensus::Trace.span_context @@ -423,7 +386,6 @@ def opencensus_begin_span end def opencensus_end_span - return if Google::Apis::Core::OpenTelemetry.instance.installed? return unless OPENCENSUS_AVAILABLE return unless @opencensus_span return unless OpenCensus::Trace.span_context diff --git a/google-apis-core/lib/google/apis/core/opentelemetry.rb b/google-apis-core/lib/google/apis/core/opentelemetry.rb deleted file mode 100644 index dc34e34f335..00000000000 --- a/google-apis-core/lib/google/apis/core/opentelemetry.rb +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2020 Google LLC -# -# 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. - -require 'opentelemetry-api' - -module Google - module Apis - module Core - class OpenTelemetry < OpenTelemetry::Instrumentation::Base - install { true } - present { true } - end - end - end -end diff --git a/google-apis-core/spec/google/apis/core/http_command_spec.rb b/google-apis-core/spec/google/apis/core/http_command_spec.rb index 19b2dce2800..94d5f7d913f 100644 --- a/google-apis-core/spec/google/apis/core/http_command_spec.rb +++ b/google-apis-core/spec/google/apis/core/http_command_spec.rb @@ -14,14 +14,6 @@ require 'spec_helper' require 'google/apis/core/http_command' -require 'opentelemetry-sdk' - -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 module Google module Apis @@ -400,37 +392,21 @@ class DecryptResponse expect(spans.size).to eql 0 end end - - it 'should not attempt to create an opentelemetry span' do - stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, '']) - command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') - - expect(Google::Apis::Core::OpenTelemetry.instance.tracer).not_to receive(:start_span) - command.execute(client) - end end if Google::Apis::Core::HttpCommand::OPENCENSUS_AVAILABLE - context('with opentelemetry integration installed') do - let(:instrumentation) { Google::Apis::Core::OpenTelemetry.instance } - let(:span) { EXPORTER.finished_spans.first } + context('with opentelemetry sdk configured') do + require 'opentelemetry-sdk' - before do - EXPORTER.reset - instrumentation.install - end + EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new + SPAN_PROCESSOR = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER) - after { instrumentation.instance_variable_set(:@installed, false) } + OpenTelemetry::SDK.configure do |c| + c.add_span_processor SPAN_PROCESSOR + end - it 'should not attempt to create an opencesus span' do - stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, ''], body: "Hello world") - command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') - command.execute(client) + let(:span) { EXPORTER.finished_spans.first } - expect(Google::Apis::Core::HttpCommand::OPENCENSUS_AVAILABLE).to be(true) - expect(OpenCensus::Trace).not_to receive(:start_span) - expect(OpenCensus::Trace).not_to receive(:end) - command.execute(client) - end + before { EXPORTER.reset } it 'should create an OpenTelemetry span for a successful call' do stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, ''], body: "Hello world") @@ -439,15 +415,8 @@ class DecryptResponse expect(span.name).to eq('www.googleapis.com') expect(span.status).to be_ok - expect(span.instrumentation_library.name).to eq('Google::Apis::Core') + expect(span.instrumentation_library.name).to eq('Google::Apis::Core::HttpCommand') expect(span.instrumentation_library.version).to eq(Google::Apis::Core::VERSION) - expect(span.attributes).to include( - 'http.host' => 'www.googleapis.com', - 'http.method' => 'get', - 'http.target' => '/zoo/animals', - 'peer.service' => 'www.googleapis.com', - 'http.status_code' => 200 - ) end it 'should create an OpenTelemetry span for a call failure' do @@ -457,23 +426,9 @@ class DecryptResponse expect(span.name).to eq('www.googleapis.com') expect(span.status).not_to be_ok - expect(span.attributes).to include( - 'http.host' => 'www.googleapis.com', - 'http.method' => 'get', - 'http.target' => '/zoo/animals', - 'peer.service' => 'www.googleapis.com', - 'http.status_code' => 403 - ) - end - - it 'should not create an OpenTelemetry span if not installed' do - instrumentation.instance_variable_set(:@installed, false) - stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, '']) - command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') - - expect(instrumentation.tracer).not_to receive(:start_span) - command.execute(client) - expect(span).to be_nil + expect(span.events.first.attributes['exception.type']).to eq('Google::Apis::ClientError') + expect(span.events.first.attributes['exception.message']).to eq('Invalid request') + expect(span.events.first.attributes['exception.stacktrace']).not_to be_empty end end