Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ def index
def connect_trace
transaction = Sentry.get_current_scope.get_transaction
# see the sinatra example under the `sentry-ruby` folder
uri = URI("http://localhost:4567/connect_trace")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri.request_uri)
request["SENTRY_TRACE"] = transaction.to_sentry_trace
response = http.request(request)
response = Net::HTTP.get_response(URI("http://localhost:4567/connect_trace"))

render plain: response.code
end
Expand Down
14 changes: 14 additions & 0 deletions sentry-ruby/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

## Unreleased

### Features

- Implement sentry-trace propagation [#1446](https://github.com/getsentry/sentry-ruby/pull/1446)

The SDK will insert the `sentry-trace` to outgoing requests made with `Net::HTTP`. Its value would look like `d827317d25d5420aa3aa97a0257db998-57757614642bdff5-1`.

If the receiver service also uses Sentry and the SDK supports performance monitoring, its tracing event will be connected with the sender application's.

Example:

<img width="1283" alt="connect sentry trace" src="https://user-images.githubusercontent.com/5079556/118963250-d7b40980-b998-11eb-9de4-598d1b220137.png">

This feature is activated by default. But users can use the new `config.propagate_traces` config option to disable it.

### Bug Fixes

- Allow toggling background sending on the fly [#1447](https://github.com/getsentry/sentry-ruby/pull/1447)
Expand Down
2 changes: 2 additions & 0 deletions sentry-ruby/lib/sentry-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ module Sentry

LOGGER_PROGNAME = "sentry".freeze

SENTRY_TRACE_HEADER_NAME = "sentry-trace".freeze

THREAD_LOCAL = :sentry_hub

def self.sdk_meta
Expand Down
8 changes: 8 additions & 0 deletions sentry-ruby/lib/sentry/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ def send_event(event, hint = nil)
raise
end

def generate_sentry_trace(span)
return unless configuration.propagate_traces

trace = span.to_sentry_trace
log_debug("[Tracing] Adding #{SENTRY_TRACE_HEADER_NAME} header to outgoing request: #{trace}")
trace
end

private

def dispatch_background_event(event, hint)
Expand Down
4 changes: 4 additions & 0 deletions sentry-ruby/lib/sentry/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ class Configuration
# Set automatically for Rails.
attr_reader :project_root

# Insert sentry-trace to outgoing requests' headers
attr_accessor :propagate_traces

# Array of rack env parameters to be included in the event sent to sentry.
attr_accessor :rack_env_whitelist

Expand Down Expand Up @@ -190,6 +193,7 @@ def initialize
self.linecache = ::Sentry::LineCache.new
self.logger = ::Sentry::Logger.new(STDOUT)
self.project_root = Dir.pwd
self.propagate_traces = true

self.release = detect_release
self.sample_rate = 1.0
Expand Down
36 changes: 36 additions & 0 deletions sentry-ruby/lib/sentry/net/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,36 @@ module Net
module HTTP
OP_NAME = "net.http"

# To explain how the entire thing works, we need to know how the original Net::HTTP#request works
# Here's part of its definition. As you can see, it usually calls itself inside a #start block
#
# ```
# def request(req, body = nil, &block)
# unless started?
# start {
# req['connection'] ||= 'close'
# return request(req, body, &block) # <- request will be called for the second time from the first call
# }
# end
# # .....
# end
# ```
#
# So when the entire flow looks like this:
#
# 1. #request is called.
# - But because the request hasn't started yet, it calls #start (which then calls #do_start)
# - At this moment @sentry_span is still nil, so #set_sentry_trace_header returns early
# 2. #do_start then creates a new Span and assigns it to @sentry_span
# 3. #request is called for the second time.
# - This time @sentry_span should present. So #set_sentry_trace_header will set the sentry-trace header on the request object
# 4. Once the request finished, it
# - Records a breadcrumb if http_logger is set
# - Finishes the Span inside @sentry_span and clears the instance variable
#
def request(req, body = nil, &block)
set_sentry_trace_header(req)
Comment thread
rhcarvalho marked this conversation as resolved.

super.tap do |res|
record_sentry_breadcrumb(req, res)
record_sentry_span(req, res)
Expand All @@ -26,6 +55,13 @@ def do_finish

private

def set_sentry_trace_header(req)
return unless @sentry_span

trace = Sentry.get_current_client.generate_sentry_trace(@sentry_span)
req[SENTRY_TRACE_HEADER_NAME] = trace if trace
end

def record_sentry_breadcrumb(req, res)
if Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
return if from_sentry_sdk?
Expand Down
30 changes: 30 additions & 0 deletions sentry-ruby/spec/sentry/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,34 @@ module ExcTag; end
end
end
end

describe "#generate_sentry_trace" do
let(:string_io) { StringIO.new }
let(:logger) do
::Logger.new(string_io)
end

before do
configuration.logger = logger
end

let(:span) { Sentry::Span.new }

it "generates the trace with given span and logs correct message" do
expect(subject.generate_sentry_trace(span)).to eq(span.to_sentry_trace)
expect(string_io.string).to match(
/\[Tracing\] Adding sentry-trace header to outgoing request: #{span.to_sentry_trace}/
)
end

context "with config.propagate_traces = false" do
before do
configuration.propagate_traces = false
end

it "returns nil" do
expect(subject.generate_sentry_trace(span)).to eq(nil)
end
end
end
end
5 changes: 5 additions & 0 deletions sentry-ruby/spec/sentry/hub_spec.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
require 'spec_helper'

RSpec.describe Sentry::Hub do
let(:string_io) { StringIO.new }
let(:logger) do
::Logger.new(string_io)
end
let(:configuration) do
config = Sentry::Configuration.new
config.dsn = DUMMY_DSN
config.transport.transport_class = Sentry::DummyTransport
config.background_worker_threads = 0
config.logger = logger
config
end
let(:client) { Sentry::Client.new(configuration) }
Expand Down
45 changes: 45 additions & 0 deletions sentry-ruby/spec/sentry/net/http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,51 @@ def stub_normal_response(code: "200")
expect(request_span.data).to eq({ status: 200 })
end

it "adds sentry-trace header to the request header" do
stub_normal_response

uri = URI("http://example.com/path")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri.request_uri)

transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)

response = http.request(request)

expect(response.code).to eq("200")
expect(string_io.string).to match(
/\[Tracing\] Adding sentry-trace header to outgoing request:/
)
request_span = transaction.span_recorder.spans.last
expect(request["sentry-trace"]).to eq(request_span.to_sentry_trace)
end

context "with config.propagate_trace = false" do
before do
Sentry.configuration.propagate_traces = false
end

it "doesn't add the sentry-trace header to outgoing requests" do
stub_normal_response

uri = URI("http://example.com/path")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri.request_uri)

transaction = Sentry.start_transaction
Sentry.get_current_scope.set_span(transaction)

response = http.request(request)

expect(response.code).to eq("200")
expect(string_io.string).not_to match(
/Adding sentry-trace header to outgoing request:/
)
expect(request.key?("sentry-trace")).to eq(false)
end
end

it "doesn't record span for the SDK's request" do
stub_sentry_response

Expand Down