Skip to content

Commit

Permalink
feat: add net http instrumentation hooks config (#62)
Browse files Browse the repository at this point in the history
* Add request and response hooks

* docs

* lint fixes

* Added test for invalid hooks

* pr comments

* verify exception handled

* fix tests

* lint ignore

* check is hook is nil outside safe_execute
  • Loading branch information
nozik authored Jun 27, 2022
1 parent 4db6509 commit d9842bf
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ instrumentation/active_record/db
**/vendor/**/*

# IDE Settings
/.idea/
**/.idea/

# rbenv configuration
.ruby-version
Expand Down
15 changes: 15 additions & 0 deletions instrumentation/net_http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ OpenTelemetry::SDK.configure do |c|
end
```

### Configuration options

```ruby
OpenTelemetry::SDK.configure do |c|
c.use 'OpenTelemetry::Instrumentation::Net::HTTP', {
request_hook: lambda { |span, request, request_body|
# Extract custom attributes from request
},
response_hook: lambda { |span, response|
# Extract custom attributes from response
}
}
end
```

## Example

An example of usage can be seen in [`example/net_http.rb`](https://github.com/open-telemetry/opentelemetry-ruby-contrib/blob/main/instrumentation/net_http/example/net_http.rb).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
defined?(::Net::HTTP)
end

option :request_hook, default: nil, validate: :callable
option :response_hook, default: nil, validate: :callable

private

def require_dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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)
def request(req, body = nil, &block) # rubocop:disable Metrics/AbcSize
# Do not trace recursive call for starting the connection
return super(req, body, &block) unless started?

Expand All @@ -32,6 +32,8 @@ def request(req, body = nil, &block)
kind: :client
) do |span|
OpenTelemetry.propagation.inject(req)
request_hook = instrumentation_config[:request_hook]
safe_execute_hook(request_hook, span, req, body) unless request_hook.nil?

super(req, body, &block).tap do |response|
annotate_span_with_response!(span, response)
Expand All @@ -41,6 +43,16 @@ def request(req, body = nil, &block)

private

def instrumentation_config
Net::HTTP::Instrumentation.instance.config
end

def safe_execute_hook(hook, *args)
hook.call(*args)
rescue StandardError => e
OpenTelemetry.handle_error(exception: e)
end

def connect
if proxy?
conn_address = proxy_address
Expand All @@ -67,6 +79,9 @@ def annotate_span_with_response!(span, response)

span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, status_code)
span.status = OpenTelemetry::Trace::Status.error unless (100..399).include?(status_code.to_i)

response_hook = instrumentation_config[:response_hook]
safe_execute_hook(response_hook, span, response) unless response_hook.nil?
end

def tracer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,110 @@
headers: { 'Traceparent' => "00-#{span.hex_trace_id}-#{span.hex_span_id}-01" }
)
end

describe 'hooks' do
let(:response_body) { 'abcd1234' }
let(:headers_attribute) { 'headers' }
let(:response_body_attribute) { 'response_body' }

before do
stub_request(:get, 'http://example.com/body').to_return(status: 200, body: response_body)
end

describe 'valid hooks' do
before do
instrumentation.instance_variable_set(:@installed, false)
config = {
request_hook: lambda do |span, request, _request_body|
headers = {}
request.each_header do |k, v|
headers[k] = v
end
span.set_attribute(headers_attribute, headers.to_json)
end,
response_hook: lambda do |span, response|
span.set_attribute(response_body_attribute, response.body)
end
}

instrumentation.install(config)
end

it 'collects data in request hook' do
::Net::HTTP.get('example.com', '/body')
_(exporter.finished_spans.size).must_equal 1
_(span.name).must_equal 'HTTP GET'
_(span.attributes['http.method']).must_equal 'GET'
headers = span.attributes[headers_attribute]
_(headers).wont_be_nil
parsed_headers = JSON.parse(headers)
_(parsed_headers['traceparent']).wont_be_nil
_(span.attributes[response_body_attribute]).must_equal response_body
end
end

describe 'invalid hook - wrong number of args' do
let(:received_exceptions) { [] }

before do
instrumentation.instance_variable_set(:@installed, false)
config = {
request_hook: ->(_span) { nil },
response_hook: ->(_span) { nil }
}

instrumentation.install(config)
OpenTelemetry.error_handler = lambda do |exception: nil, message: nil| # rubocop:disable Lint/UnusedBlockArgument
received_exceptions << exception
end
end

after do
OpenTelemetry.error_handler = nil
end

it 'should not fail the instrumentation' do
::Net::HTTP.get('example.com', '/body')
_(exporter.finished_spans.size).must_equal 1
_(span.name).must_equal 'HTTP GET'
_(span.attributes['http.method']).must_equal 'GET'
error_messages = received_exceptions.map(&:message)
_(error_messages.all? { |em| em.start_with?('wrong number of arguments') }).must_equal true
end
end

describe 'invalid hooks - throws an error' do
let(:error1) { 'err1' }
let(:error2) { 'err2' }
let(:received_exceptions) { [] }

before do
instrumentation.instance_variable_set(:@installed, false)
config = {
request_hook: ->(_span, _request, _request_body) { raise StandardError, error1 },
response_hook: ->(_span, _response) { raise StandardError, error2 }
}

instrumentation.install(config)
OpenTelemetry.error_handler = lambda do |exception: nil, message: nil| # rubocop:disable Lint/UnusedBlockArgument
received_exceptions << exception
end
end

after do
OpenTelemetry.error_handler = nil
end

it 'should not fail the instrumentation' do
::Net::HTTP.get('example.com', '/body')
_(exporter.finished_spans.size).must_equal 1
_(span.name).must_equal 'HTTP GET'
_(span.attributes['http.method']).must_equal 'GET'
error_messages = received_exceptions.map(&:message)
_(error_messages).must_equal([error1, error2])
end
end
end
end

describe '#connect' do
Expand Down

0 comments on commit d9842bf

Please sign in to comment.