Skip to content

Commit a9b3687

Browse files
Add Excon instrumentation (#2383)
* 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 <[email protected]> * 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 <[email protected]>
1 parent b31f0f3 commit a9b3687

File tree

8 files changed

+372
-2
lines changed

8 files changed

+372
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
### Features
44

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

1112
Note: MemoryStore and FileStore require Rails 8.0+
1213

sentry-ruby/Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ gem "benchmark-memory"
2828
gem "yard", github: "lsegal/yard"
2929
gem "webrick"
3030
gem "faraday"
31+
gem "excon"
3132

3233
eval_gemfile File.expand_path("../Gemfile", __dir__)

sentry-ruby/lib/sentry-ruby.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,3 +614,4 @@ def utc_now
614614
require "sentry/puma"
615615
require "sentry/graphql"
616616
require "sentry/faraday"
617+
require "sentry/excon"

sentry-ruby/lib/sentry/excon.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
Sentry.register_patch(:excon) do
4+
if defined?(::Excon)
5+
require "sentry/excon/middleware"
6+
if Excon.defaults[:middlewares]
7+
Excon.defaults[:middlewares] << Sentry::Excon::Middleware unless Excon.defaults[:middlewares].include?(Sentry::Excon::Middleware)
8+
end
9+
end
10+
end
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
module Sentry
4+
module Excon
5+
OP_NAME = "http.client"
6+
7+
class Middleware < ::Excon::Middleware::Base
8+
def initialize(stack)
9+
super
10+
@instrumenter = Instrumenter.new
11+
end
12+
13+
def request_call(datum)
14+
@instrumenter.start_transaction(datum)
15+
@stack.request_call(datum)
16+
end
17+
18+
def response_call(datum)
19+
@instrumenter.finish_transaction(datum)
20+
@stack.response_call(datum)
21+
end
22+
end
23+
24+
class Instrumenter
25+
SPAN_ORIGIN = "auto.http.excon"
26+
BREADCRUMB_CATEGORY = "http"
27+
28+
include Utils::HttpTracing
29+
30+
def start_transaction(env)
31+
return unless Sentry.initialized?
32+
33+
current_span = Sentry.get_current_scope&.span
34+
@span = current_span&.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN)
35+
36+
request_info = extract_request_info(env)
37+
38+
if propagate_trace?(request_info[:url])
39+
set_propagation_headers(env[:headers])
40+
end
41+
end
42+
43+
def finish_transaction(response)
44+
return unless @span
45+
46+
response_status = response[:response][:status]
47+
request_info = extract_request_info(response)
48+
49+
if record_sentry_breadcrumb?
50+
record_sentry_breadcrumb(request_info, response_status)
51+
end
52+
53+
set_span_info(@span, request_info, response_status)
54+
ensure
55+
@span&.finish
56+
end
57+
58+
private
59+
60+
def extract_request_info(env)
61+
url = env[:scheme] + "://" + env[:hostname] + env[:path]
62+
result = { method: env[:method].to_s.upcase, url: url }
63+
64+
if Sentry.configuration.send_default_pii
65+
result[:query] = env[:query]
66+
67+
# Handle excon 1.0.0+
68+
result[:query] = build_nested_query(result[:query]) unless result[:query].is_a?(String)
69+
70+
result[:body] = env[:body]
71+
end
72+
73+
result
74+
end
75+
end
76+
end
77+
end

sentry-ruby/lib/sentry/utils/http_tracing.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def record_sentry_breadcrumb(request_info, response_status)
1919
crumb = Sentry::Breadcrumb.new(
2020
level: :info,
2121
category: self.class::BREADCRUMB_CATEGORY,
22-
type: :info,
22+
type: "info",
2323
data: { status: response_status, **request_info }
2424
)
2525

@@ -36,6 +36,25 @@ def propagate_trace?(url)
3636
Sentry.configuration.propagate_traces &&
3737
Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) }
3838
end
39+
40+
# Kindly borrowed from Rack::Utils
41+
def build_nested_query(value, prefix = nil)
42+
case value
43+
when Array
44+
value.map { |v|
45+
build_nested_query(v, "#{prefix}[]")
46+
}.join("&")
47+
when Hash
48+
value.map { |k, v|
49+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
50+
}.delete_if(&:empty?).join("&")
51+
when nil
52+
URI.encode_www_form_component(prefix)
53+
else
54+
raise ArgumentError, "value must be a Hash" if prefix.nil?
55+
"#{URI.encode_www_form_component(prefix)}=#{URI.encode_www_form_component(value)}"
56+
end
57+
end
3958
end
4059
end
4160
end

0 commit comments

Comments
 (0)