diff --git a/.travis.yml b/.travis.yml index 422726e47..a3c951961 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: ruby sudo: false rvm: - - 2.0 - - 2.1 - 2.2 - 2.3 - 2.4 diff --git a/Gemfile b/Gemfile index f798f7aa1..bfc17665e 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ group :development do gem 'rmail', '~> 1.1' gem 'redis', '~> 3.2' gem 'logging', '~> 2.2' + gem 'opencensus', '~> 0.3' end platforms :jruby do diff --git a/lib/google/apis/core/http_command.rb b/lib/google/apis/core/http_command.rb index 4f3d5ecb3..a8c4dab75 100644 --- a/lib/google/apis/core/http_command.rb +++ b/lib/google/apis/core/http_command.rb @@ -29,6 +29,13 @@ module Google RETRIABLE_ERRORS = [Google::Apis::ServerError, Google::Apis::RateLimitError, Google::Apis::TransmissionError] + begin + require 'opencensus' + OPENCENSUS_AVAILABLE = true + rescue LoadError + OPENCENSUS_AVAILABLE = false + end + # Request options # @return [Google::Apis::RequestOptions] attr_accessor :options @@ -76,6 +83,7 @@ module Google self.body = body self.query = {} self.params = {} + @opencensus_span = nil end # Execute the command, retrying as necessary @@ -89,6 +97,7 @@ module Google # @raise [Google::Apis::AuthorizationError] Authorization is required def execute(client) prepare! + opencensus_begin_span begin Retriable.retriable tries: options.retries + 1, base_interval: 1, @@ -116,6 +125,8 @@ module Google end end ensure + opencensus_end_span + @http_res = nil release! end @@ -157,7 +168,6 @@ module Google end self.body = '' unless self.body - end # Release any resources used by this command @@ -288,15 +298,15 @@ module Google request_header = header.dup apply_request_options(request_header) - http_res = client.request(method.to_s.upcase, - url.to_s, - query: nil, - body: body, - header: request_header, - follow_redirect: true) - logger.debug { http_res.status } - logger.debug { http_res.inspect } - response = process_response(http_res.status.to_i, http_res.header, http_res.body) + @http_res = client.request(method.to_s.upcase, + url.to_s, + query: nil, + body: body, + header: request_header, + follow_redirect: true) + logger.debug { @http_res.status } + logger.debug { @http_res.inspect } + response = process_response(@http_res.status.to_i, @http_res.header, @http_res.body) success(response) rescue => e logger.debug { sprintf('Caught error %s', e) } @@ -323,10 +333,73 @@ module Google private + def opencensus_begin_span + return unless OPENCENSUS_AVAILABLE && options.use_opencensus + return if @opencensus_span + return unless OpenCensus::Trace.span_context + + @opencensus_span = OpenCensus::Trace.start_span url.path.to_s + @opencensus_span.kind = OpenCensus::Trace::SpanBuilder::CLIENT + @opencensus_span.put_attribute "http.host", url.host.to_s + @opencensus_span.put_attribute "http.method", method.to_s.upcase + @opencensus_span.put_attribute "http.path", url.path.to_s + if body.respond_to? :bytesize + @opencensus_span.put_message_event \ + OpenCensus::Trace::SpanBuilder::SENT, 1, body.bytesize + end + + formatter = OpenCensus::Trace.config.http_formatter + if formatter.respond_to? :header_name + header[formatter.header_name] = formatter.serialize @opencensus_span.context.trace_context + end + rescue StandardError => e + # Log exceptions and continue, so opencensus failures don't cause + # the entire request to fail. + logger.debug { sprintf('Error opening OpenCensus span: %s', e) } + end + + def opencensus_end_span + return unless OPENCENSUS_AVAILABLE + return unless @opencensus_span + return unless OpenCensus::Trace.span_context + + if @http_res.body.respond_to? :bytesize + @opencensus_span.put_message_event \ + OpenCensus::Trace::SpanBuilder::RECEIVED, 1, @http_res.body.bytesize + end + status = @http_res.status.to_i + if status > 0 + @opencensus_span.set_status map_http_status status + @opencensus_span.put_attribute "http.status_code", status + end + + OpenCensus::Trace.end_span @opencensus_span + @opencensus_span = nil + rescue StandardError => e + # Log exceptions and continue, so failures don't cause leaks by + # aborting cleanup. + logger.debug { sprintf('Error finishing OpenCensus span: %s', e) } + end + def form_encoded? @form_encoded end + def map_http_status http_status + case http_status + when 200..399 then 0 # OK + when 400 then 3 # INVALID_ARGUMENT + when 401 then 16 # UNAUTHENTICATED + when 403 then 7 # PERMISSION_DENIED + when 404 then 5 # NOT_FOUND + when 429 then 8 # RESOURCE_EXHAUSTED + when 501 then 12 # UNIMPLEMENTED + when 503 then 14 # UNAVAILABLE + when 504 then 4 # DEADLINE_EXCEEDED + else 2 # UNKNOWN + end + end + def normalize_query_value(v) case v when Array diff --git a/lib/google/apis/options.rb b/lib/google/apis/options.rb index 9b617d483..eaf14f9d0 100644 --- a/lib/google/apis/options.rb +++ b/lib/google/apis/options.rb @@ -32,7 +32,8 @@ module Google :normalize_unicode, :skip_serialization, :skip_deserialization, - :api_format_version) + :api_format_version, + :use_opencensus) # General client options class ClientOptions @@ -73,6 +74,8 @@ module Google # @return [Boolean] True if response should be returned in raw form instead of deserialized. # @!attribute [rw] api_format_version # @return [Fixnum] Version of the error format to request/expect. + # @!attribute [rw] use_opencensus + # @return [Boolean] Whether OpenCensus spans should be generated for requests. Default is true. # Get the default options # @return [Google::Apis::RequestOptions] @@ -101,5 +104,6 @@ module Google RequestOptions.default.skip_serialization = false RequestOptions.default.skip_deserialization = false RequestOptions.default.api_format_version = nil + RequestOptions.default.use_opencensus = true end end diff --git a/spec/google/apis/core/http_command_spec.rb b/spec/google/apis/core/http_command_spec.rb index 5750c4acf..6c6b6c8a4 100644 --- a/spec/google/apis/core/http_command_spec.rb +++ b/spec/google/apis/core/http_command_spec.rb @@ -294,6 +294,71 @@ RSpec.describe Google::Apis::Core::HttpCommand do end end + context('with opencensus integration') do + it 'should create an opencensus 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') + OpenCensus::Trace.start_request_trace do |span_context| + command.execute(client) + spans = span_context.build_contained_spans + expect(spans.size).to eql 1 + span = spans.first + expect(span.name.value).to eql '/zoo/animals' + expect(span.status.code).to eql 0 + attrs = span.attributes + expect(attrs['http.host'].value).to eql 'www.googleapis.com' + expect(attrs['http.method'].value).to eql 'GET' + expect(attrs['http.path'].value).to eql '/zoo/animals' + expect(attrs['http.status_code']).to eql 200 + events = span.time_events + expect(events.size).to eql 2 + expect(events[0].type).to eql :SENT + expect(events[0].uncompressed_size).to eql 0 + expect(events[1].type).to eql :RECEIVED + expect(events[1].uncompressed_size).to eql 11 + end + end + + it 'should create an opencensus 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') + OpenCensus::Trace.start_request_trace do |span_context| + expect { command.execute(client) }.to raise_error(Google::Apis::ClientError) + spans = span_context.build_contained_spans + expect(spans.size).to eql 1 + span = spans.first + expect(span.name.value).to eql '/zoo/animals' + expect(span.status.code).to eql 7 + attrs = span.attributes + expect(attrs['http.host'].value).to eql 'www.googleapis.com' + expect(attrs['http.method'].value).to eql 'GET' + expect(attrs['http.path'].value).to eql '/zoo/animals' + expect(attrs['http.status_code']).to eql 403 + end + end + + it 'should propagate trace context header' do + stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world)) + command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals') + OpenCensus::Trace.start_request_trace do |span_context| + result = command.execute(client) + expect(a_request(:get, 'https://www.googleapis.com/zoo/animals') + .with { |req| !req.headers['Trace-Context'].empty? }).to have_been_made + end + end + + it 'should not create an opencensus span if disabled' 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') + command.options.use_opencensus = false + OpenCensus::Trace.start_request_trace do |span_context| + command.execute(client) + spans = span_context.build_contained_spans + expect(spans.size).to eql 0 + end + end + end if Google::Apis::Core::HttpCommand::OPENCENSUS_AVAILABLE + 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, ''])