Skip to content

Commit

Permalink
Tracing without performance: new continue_trace api (#2089)
Browse files Browse the repository at this point in the history
  • Loading branch information
sl0thentr0py committed Aug 31, 2023
1 parent 94ce420 commit 143bdce
Show file tree
Hide file tree
Showing 17 changed files with 308 additions and 60 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
```
- Tracing without Performance
- Implement `PropagationContext` on `Scope` and add `Sentry.get_trace_propagation_headers` API [#2084](https://github.com/getsentry/sentry-ruby/pull/2084)
- Implement `Sentry.continue_trace` API [#2089](https://github.com/getsentry/sentry-ruby/pull/2089)

The SDK now supports connecting arbitrary events (Errors / Transactions / Replays) across distributed services and not just Transactions.
To continue an incoming trace starting with this version of the SDK, use `Sentry.continue_trace` as follows.
Expand Down
5 changes: 1 addition & 4 deletions sentry-rails/lib/sentry/rails/action_cable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,8 @@ def capture(connection, transaction_name:, extra_context: nil, &block)
end

def start_transaction(env, scope)
sentry_trace = env["HTTP_SENTRY_TRACE"]
baggage = env["HTTP_BAGGAGE"]

options = { name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME }
transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, baggage: baggage, **options) if sentry_trace
transaction = Sentry.continue_trace(env, **options)
Sentry.start_transaction(transaction: transaction, **options)
end

Expand Down
5 changes: 1 addition & 4 deletions sentry-rails/lib/sentry/rails/capture_exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,13 @@ def capture_exception(exception, env)
end

def start_transaction(env, scope)
sentry_trace = env["HTTP_SENTRY_TRACE"]
baggage = env["HTTP_BAGGAGE"]

options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op }

if @assets_regexp && scope.transaction_name.match?(@assets_regexp)
options.merge!(sampled: false)
end

transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, baggage: baggage, **options) if sentry_trace
transaction = Sentry.continue_trace(env, **options)
Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
end

Expand Down
9 changes: 9 additions & 0 deletions sentry-ruby/lib/sentry-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,15 @@ def get_trace_propagation_headers
get_current_hub.get_trace_propagation_headers
end

# Continue an incoming trace from a rack env like hash.
#
# @param env [Hash]
# @return [Transaction, nil]
def continue_trace(env, **options)
return nil unless initialized?
get_current_hub.continue_trace(env, **options)
end

##### Helpers #####

# @!visibility private
Expand Down
18 changes: 18 additions & 0 deletions sentry-ruby/lib/sentry/hub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,24 @@ def get_trace_propagation_headers
headers
end

def continue_trace(env, **options)
configure_scope { |s| s.generate_propagation_context(env) }

return nil unless configuration.tracing_enabled?

propagation_context = current_scope.propagation_context
return nil unless propagation_context.incoming_trace

Transaction.new(
hub: self,
trace_id: propagation_context.trace_id,
parent_span_id: propagation_context.parent_span_id,
parent_sampled: propagation_context.parent_sampled,
baggage: propagation_context.baggage,
**options
)
end

private

def current_layer
Expand Down
71 changes: 65 additions & 6 deletions sentry-ruby/lib/sentry/propagation_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

module Sentry
class PropagationContext
SENTRY_TRACE_REGEXP = Regexp.new(
"^[ \t]*" + # whitespace
"([0-9a-f]{32})?" + # trace_id
"-?([0-9a-f]{16})?" + # span_id
"-?([01])?" + # sampled
"[ \t]*$" # whitespace
)

# An uuid that can be used to identify a trace.
# @return [String]
Expand All @@ -13,15 +20,67 @@ class PropagationContext
# @return [String]
attr_reader :span_id
# Span parent's span_id.
# @return [String]
# @return [String, nil]
attr_reader :parent_span_id
# The sampling decision of the parent transaction.
# @return [Boolean, nil]
attr_reader :parent_sampled
# Is there an incoming trace or not?
# @return [Boolean]
attr_reader :incoming_trace
# This is only for accessing the current baggage variable.
# Please use the #get_baggage method for interfacing outside this class.
# @return [Baggage, nil]
attr_reader :baggage

def initialize(scope)
def initialize(scope, env = nil)
@scope = scope
@trace_id = SecureRandom.uuid.delete("-")
@span_id = SecureRandom.uuid.delete("-").slice(0, 16)
@parent_span_id = nil
@parent_sampled = nil
@baggage = nil
@incoming_trace = false

if env
sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
baggage_header = env["HTTP_BAGGAGE"] || env[BAGGAGE_HEADER_NAME]

if sentry_trace_header
sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)

if sentry_trace_data
@trace_id, @parent_span_id, @parent_sampled = sentry_trace_data

@baggage = if baggage_header && !baggage_header.empty?
Baggage.from_incoming_header(baggage_header)
else
# If there's an incoming sentry-trace but no incoming baggage header,
# for instance in traces coming from older SDKs,
# baggage will be empty and frozen and won't be populated as head SDK.
Baggage.new({})
end

@baggage.freeze!
@incoming_trace = true
end
end
end

@trace_id ||= SecureRandom.uuid.delete("-")
@span_id = SecureRandom.uuid.delete("-").slice(0, 16)
end

# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
#
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
# @return [Array, nil]
def self.extract_sentry_trace(sentry_trace)
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
return nil if match.nil?

trace_id, parent_span_id, sampled_flag = match[1..3]
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"

[trace_id, parent_span_id, parent_sampled]
end

# Returns the trace context that can be used to embed in an Event.
Expand All @@ -40,8 +99,8 @@ def get_traceparent
"#{trace_id}-#{span_id}"
end

# Returns the W3C baggage header from the propagation context.
# @return [String, nil]
# Returns the Baggage from the propagation context or populates as head SDK if empty.
# @return [Baggage, nil]
def get_baggage
populate_head_baggage if @baggage.nil? || @baggage.mutable
@baggage
Expand Down
5 changes: 1 addition & 4 deletions sentry-ruby/lib/sentry/rack/capture_exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,8 @@ def capture_exception(exception, env)
end

def start_transaction(env, scope)
sentry_trace = env["HTTP_SENTRY_TRACE"]
baggage = env["HTTP_BAGGAGE"]

options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op }
transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, baggage: baggage, **options) if sentry_trace
transaction = Sentry.continue_trace(env, **options)
Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
end

Expand Down
11 changes: 7 additions & 4 deletions sentry-ruby/lib/sentry/scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,13 @@ def add_event_processor(&block)
@event_processors << block
end

# Generate a new propagation context either from the incoming env headers or from scratch.
# @param env [Hash, nil]
# @return [void]
def generate_propagation_context(env = nil)
@propagation_context = PropagationContext.new(self, env)
end

protected

# for duplicating scopes internally
Expand Down Expand Up @@ -307,10 +314,6 @@ def set_new_breadcrumb_buffer
@breadcrumbs = BreadcrumbBuffer.new(@max_breadcrumbs)
end

def generate_propagation_context
@propagation_context = PropagationContext.new(self)
end

class << self
# @return [Hash]
def os_context
Expand Down
25 changes: 8 additions & 17 deletions sentry-ruby/lib/sentry/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@

require "sentry/baggage"
require "sentry/profiler"
require "sentry/propagation_context"

module Sentry
class Transaction < Span
SENTRY_TRACE_REGEXP = Regexp.new(
"^[ \t]*" + # whitespace
"([0-9a-f]{32})?" + # trace_id
"-?([0-9a-f]{16})?" + # span_id
"-?([01])?" + # sampled
"[ \t]*$" # whitespace
)
# @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead.
SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP

UNLABELD_NAME = "<unlabeled transaction>".freeze
MESSAGE_PREFIX = "[Tracing]"

Expand Down Expand Up @@ -92,6 +89,8 @@ def initialize(
init_span_recorder
end

# @deprecated use Sentry.continue_trace instead.
#
# Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request).
#
# The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`.
Expand Down Expand Up @@ -132,18 +131,10 @@ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_h
)
end

# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
#
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
# @deprecated Use Sentry::PropagationContext.extract_sentry_trace instead.
# @return [Array, nil]
def self.extract_sentry_trace(sentry_trace)
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
return nil if match.nil?

trace_id, parent_span_id, sampled_flag = match[1..3]
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"

[trace_id, parent_span_id, parent_sampled]
PropagationContext.extract_sentry_trace(sentry_trace)
end

# @return [Hash]
Expand Down
4 changes: 2 additions & 2 deletions sentry-ruby/spec/sentry/net/http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
"sentry-user_id=Am%C3%A9lie, "\
"other-vendor-value-2=foo;bar;"

transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, baggage: baggage)
transaction = Sentry.continue_trace({ "sentry-trace" => sentry_trace, "baggage" => baggage })
Sentry.get_current_scope.set_span(transaction)

response = http.request(request)
Expand Down Expand Up @@ -190,7 +190,7 @@
"sentry-user_id=Am%C3%A9lie, "\
"other-vendor-value-2=foo;bar;"

transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, baggage: baggage)
transaction = Sentry.continue_trace({ "sentry-trace" => sentry_trace, "baggage" => baggage })
Sentry.get_current_scope.set_span(transaction)

response = http.request(request)
Expand Down
75 changes: 74 additions & 1 deletion sentry-ruby/spec/sentry/propagation_context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,83 @@
let(:subject) { described_class.new(scope) }

describe "#initialize" do
it "generates correct attributes" do
it "generates correct attributes without env" do
expect(subject.trace_id.length).to eq(32)
expect(subject.span_id.length).to eq(16)
expect(subject.parent_span_id).to be_nil
expect(subject.parent_sampled).to be_nil
expect(subject.baggage).to be_nil
expect(subject.incoming_trace).to eq(false)
end

it "generates correct attributes when incoming sentry-trace and baggage" do
env = {
"sentry-trace" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a",
"baggage" => "other-vendor-value-1=foo;bar;baz, "\
"sentry-trace_id=771a43a4192642f0b136d5159a501700, "\
"sentry-public_key=49d0f7386ad645858ae85020e393bef3, "\
"sentry-sample_rate=0.01337, "\
"sentry-user_id=Am%C3%A9lie, "\
"other-vendor-value-2=foo;bar;"
}

subject = described_class.new(scope, env)
expect(subject.trace_id).to eq("771a43a4192642f0b136d5159a501700")
expect(subject.span_id.length).to eq(16)
expect(subject.parent_span_id).to eq("7c51afd529da4a2a")
expect(subject.parent_sampled).to eq(nil)
expect(subject.incoming_trace).to eq(true)
expect(subject.baggage).to be_a(Sentry::Baggage)
expect(subject.baggage.mutable).to eq(false)
expect(subject.baggage.items).to eq({
"public_key"=>"49d0f7386ad645858ae85020e393bef3",
"sample_rate"=>"0.01337",
"trace_id"=>"771a43a4192642f0b136d5159a501700",
"user_id"=>"Amélie"
})
end

it "generates correct attributes when incoming HTTP_SENTRY_TRACE and HTTP_BAGGAGE" do
env = {
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a",
"HTTP_BAGGAGE" => "other-vendor-value-1=foo;bar;baz, "\
"sentry-trace_id=771a43a4192642f0b136d5159a501700, "\
"sentry-public_key=49d0f7386ad645858ae85020e393bef3, "\
"sentry-sample_rate=0.01337, "\
"sentry-user_id=Am%C3%A9lie, "\
"other-vendor-value-2=foo;bar;"
}

subject = described_class.new(scope, env)
expect(subject.trace_id).to eq("771a43a4192642f0b136d5159a501700")
expect(subject.span_id.length).to eq(16)
expect(subject.parent_span_id).to eq("7c51afd529da4a2a")
expect(subject.parent_sampled).to eq(nil)
expect(subject.incoming_trace).to eq(true)
expect(subject.baggage).to be_a(Sentry::Baggage)
expect(subject.baggage.mutable).to eq(false)
expect(subject.baggage.items).to eq({
"public_key"=>"49d0f7386ad645858ae85020e393bef3",
"sample_rate"=>"0.01337",
"trace_id"=>"771a43a4192642f0b136d5159a501700",
"user_id"=>"Amélie"
})
end

it "generates correct attributes when incoming sentry-trace only (from older SDKs)" do
env = {
"sentry-trace" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a"
}

subject = described_class.new(scope, env)
expect(subject.trace_id).to eq("771a43a4192642f0b136d5159a501700")
expect(subject.span_id.length).to eq(16)
expect(subject.parent_span_id).to eq("7c51afd529da4a2a")
expect(subject.parent_sampled).to eq(nil)
expect(subject.incoming_trace).to eq(true)
expect(subject.baggage).to be_a(Sentry::Baggage)
expect(subject.baggage.mutable).to eq(false)
expect(subject.baggage.items).to eq({})
end
end

Expand Down
Loading

0 comments on commit 143bdce

Please sign in to comment.