Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracing without performance: new continue_trace api #2089

Merged
merged 6 commits into from
Aug 31, 2023
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
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 @@
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)

Check warning on line 525 in sentry-ruby/lib/sentry-ruby.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry-ruby.rb#L524-L525

Added lines #L524 - L525 were not covered by tests
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 @@
headers
end

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

Check warning on line 259 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L259

Added line #L259 was not covered by tests

return nil unless configuration.tracing_enabled?

Check warning on line 261 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L261

Added line #L261 was not covered by tests

propagation_context = current_scope.propagation_context
return nil unless propagation_context.incoming_trace

Check warning on line 264 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L263-L264

Added lines #L263 - L264 were not covered by tests

Transaction.new(

Check warning on line 266 in sentry-ruby/lib/sentry/hub.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/hub.rb#L266

Added line #L266 was not covered by tests
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 @@
# @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]

Check warning on line 45 in sentry-ruby/lib/sentry/propagation_context.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/propagation_context.rb#L44-L45

Added lines #L44 - L45 were not covered by tests

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

Check warning on line 48 in sentry-ruby/lib/sentry/propagation_context.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/propagation_context.rb#L47-L48

Added lines #L47 - L48 were not covered by tests

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

Check warning on line 51 in sentry-ruby/lib/sentry/propagation_context.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/propagation_context.rb#L50-L51

Added lines #L50 - L51 were not covered by tests

@baggage = if baggage_header && !baggage_header.empty?
Baggage.from_incoming_header(baggage_header)

Check warning on line 54 in sentry-ruby/lib/sentry/propagation_context.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/propagation_context.rb#L53-L54

Added lines #L53 - L54 were not covered by tests
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({})

Check warning on line 59 in sentry-ruby/lib/sentry/propagation_context.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/propagation_context.rb#L59

Added line #L59 was not covered by tests
end

@baggage.freeze!
@incoming_trace = true

Check warning on line 63 in sentry-ruby/lib/sentry/propagation_context.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/propagation_context.rb#L62-L63

Added lines #L62 - L63 were not covered by tests
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 @@
"#{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 @@
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)

Check warning on line 66 in sentry-ruby/lib/sentry/rack/capture_exceptions.rb

View check run for this annotation

Codecov / codecov/patch

sentry-ruby/lib/sentry/rack/capture_exceptions.rb#L66

Added line #L66 was not covered by tests
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
Loading