Skip to content

Commit

Permalink
Add Excon instrumentation (#2383)
Browse files Browse the repository at this point in the history
* Add Excon instrumentation

* Add missing specs

* Add CHANGELOG

* Fix Rubocop

* Fix spec support for Ruby < 3

* Update sentry-ruby/lib/sentry/excon/middleware.rb

Co-authored-by: Peter Solnica <peter@solnica.online>

* Handle case for Excon 1+ with Hash as query

* Borrow Rack::Utils for building nested query

* Add one more specs for more advanced query building in Excon

* Fixes changelog

* Add new specs using the query parameter encoding

---------

Co-authored-by: Peter Solnica <peter@solnica.online>
  • Loading branch information
frederikspang and solnic authored Nov 19, 2024
1 parent b31f0f3 commit a9b3687
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 2 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
### Features

- Add `include_sentry_event` matcher for RSpec [#2424](https://github.com/getsentry/sentry-ruby/pull/2424)
- Add support for Sentry Cache instrumentation, when using Rails.cache ([#2380](https://github.com/getsentry/sentry-ruby/pull/2380))
- Add support for Sentry Cache instrumentation, when using Rails.cache [#2380](https://github.com/getsentry/sentry-ruby/pull/2380)
- Add support for Queue Instrumentation for Sidekiq. [#2403](https://github.com/getsentry/sentry-ruby/pull/2403)
- Add support for string errors in error reporter ([#2464](https://github.com/getsentry/sentry-ruby/pull/2464))
- Reset trace_id and add root transaction for sidekiq-cron [#2446](https://github.com/getsentry/sentry-ruby/pull/2446)
- Add support for Excon HTTP client instrumentation ([#2383](https://github.com/getsentry/sentry-ruby/pull/2383))

Note: MemoryStore and FileStore require Rails 8.0+

Expand Down
1 change: 1 addition & 0 deletions sentry-ruby/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ gem "benchmark-memory"
gem "yard", github: "lsegal/yard"
gem "webrick"
gem "faraday"
gem "excon"

eval_gemfile File.expand_path("../Gemfile", __dir__)
1 change: 1 addition & 0 deletions sentry-ruby/lib/sentry-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -614,3 +614,4 @@ def utc_now
require "sentry/puma"
require "sentry/graphql"
require "sentry/faraday"
require "sentry/excon"
10 changes: 10 additions & 0 deletions sentry-ruby/lib/sentry/excon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

Sentry.register_patch(:excon) do
if defined?(::Excon)
require "sentry/excon/middleware"
if Excon.defaults[:middlewares]
Excon.defaults[:middlewares] << Sentry::Excon::Middleware unless Excon.defaults[:middlewares].include?(Sentry::Excon::Middleware)
end
end
end
77 changes: 77 additions & 0 deletions sentry-ruby/lib/sentry/excon/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

module Sentry
module Excon
OP_NAME = "http.client"

class Middleware < ::Excon::Middleware::Base
def initialize(stack)
super
@instrumenter = Instrumenter.new
end

def request_call(datum)
@instrumenter.start_transaction(datum)
@stack.request_call(datum)
end

def response_call(datum)
@instrumenter.finish_transaction(datum)
@stack.response_call(datum)
end
end

class Instrumenter
SPAN_ORIGIN = "auto.http.excon"
BREADCRUMB_CATEGORY = "http"

include Utils::HttpTracing

def start_transaction(env)
return unless Sentry.initialized?

current_span = Sentry.get_current_scope&.span
@span = current_span&.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN)

request_info = extract_request_info(env)

if propagate_trace?(request_info[:url])
set_propagation_headers(env[:headers])
end
end

def finish_transaction(response)
return unless @span

response_status = response[:response][:status]
request_info = extract_request_info(response)

if record_sentry_breadcrumb?
record_sentry_breadcrumb(request_info, response_status)
end

set_span_info(@span, request_info, response_status)
ensure
@span&.finish
end

private

def extract_request_info(env)
url = env[:scheme] + "://" + env[:hostname] + env[:path]
result = { method: env[:method].to_s.upcase, url: url }

if Sentry.configuration.send_default_pii
result[:query] = env[:query]

# Handle excon 1.0.0+
result[:query] = build_nested_query(result[:query]) unless result[:query].is_a?(String)

result[:body] = env[:body]
end

result
end
end
end
end
21 changes: 20 additions & 1 deletion sentry-ruby/lib/sentry/utils/http_tracing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def record_sentry_breadcrumb(request_info, response_status)
crumb = Sentry::Breadcrumb.new(
level: :info,
category: self.class::BREADCRUMB_CATEGORY,
type: :info,
type: "info",
data: { status: response_status, **request_info }
)

Expand All @@ -36,6 +36,25 @@ def propagate_trace?(url)
Sentry.configuration.propagate_traces &&
Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) }
end

# Kindly borrowed from Rack::Utils
def build_nested_query(value, prefix = nil)
case value
when Array
value.map { |v|
build_nested_query(v, "#{prefix}[]")
}.join("&")
when Hash
value.map { |k, v|
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
}.delete_if(&:empty?).join("&")
when nil
URI.encode_www_form_component(prefix)
else
raise ArgumentError, "value must be a Hash" if prefix.nil?
"#{URI.encode_www_form_component(prefix)}=#{URI.encode_www_form_component(value)}"
end
end
end
end
end
Loading

0 comments on commit a9b3687

Please sign in to comment.