Skip to content

Commit 88287d1

Browse files
Merge pull request #3180 from newrelic/hybrid-agent-starting-transaction-tests
Enable "Starting transaction tests" in hybrid agent CAT
2 parents a3b316e + 517cd13 commit 88287d1

File tree

10 files changed

+314
-95
lines changed

10 files changed

+314
-95
lines changed

lib/new_relic/agent/opentelemetry/trace/span.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,10 @@ module Agent
77
module OpenTelemetry
88
module Trace
99
class Span < ::OpenTelemetry::Trace::Span
10-
attr_reader :context
10+
attr_accessor :finishable
1111

12-
def initialize(segment:, transaction:)
13-
@context = ::OpenTelemetry::Trace::SpanContext.new(
14-
trace_id: transaction.trace_id,
15-
span_id: segment.guid,
16-
trace_flags: 1
17-
)
12+
def finish(end_timestamp: nil)
13+
finishable&.finish
1814
end
1915
end
2016
end

lib/new_relic/agent/opentelemetry/trace/tracer.rb

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,65 @@ def initialize(name = nil, version = nil)
1212
@version = version || ''
1313
end
1414

15+
def start_span(name, with_parent: nil, attributes: nil, links: nil, start_timestamp: nil, kind: nil)
16+
parent_span_context = ::OpenTelemetry::Trace.current_span(with_parent).context
17+
18+
finishable = if can_start_transaction?(parent_span_context)
19+
return if internal_span_kind_with_invalid_parent?(kind, parent_span_context)
20+
21+
nr_obj = NewRelic::Agent::Tracer.start_transaction_or_segment(name: name, category: :otel)
22+
add_remote_partent_span_context_to_txn(nr_obj, parent_span_context)
23+
nr_obj
24+
else
25+
NewRelic::Agent::Tracer.start_segment(name: name)
26+
end
27+
28+
span = get_span_from_finishable(finishable)
29+
span.finishable = finishable
30+
span
31+
end
32+
1533
def in_span(name, attributes: nil, links: nil, start_timestamp: nil, kind: nil)
16-
case kind
17-
when :internal
18-
begin
19-
return yield unless NewRelic::Agent::Tracer.current_transaction
34+
span = start_span(name, attributes: attributes, links: links, start_timestamp: start_timestamp, kind: kind)
35+
begin
36+
yield
37+
rescue => e
38+
# TODO: Update for segment errors if finishable is a segment
39+
NewRelic::Agent.notice_error(e)
40+
raise
41+
end
42+
ensure
43+
span&.finish
44+
end
2045

21-
segment = NewRelic::Agent::Tracer.start_segment(name: name)
46+
private
2247

23-
NewRelic::Agent::Tracer.capture_segment_error(segment) { yield }
24-
ensure
25-
segment&.finish
26-
end
48+
def get_span_from_finishable(finishable)
49+
case finishable
50+
when NewRelic::Agent::Transaction
51+
finishable.segments.first.instance_variable_get(:@otel_span)
52+
when NewRelic::Agent::Transaction::Segment
53+
finishable.instance_variable_get(:@otel_span)
2754
else
28-
NewRelic::Agent.logger.debug("Span kind: #{kind} is not supported yet")
55+
NewRelic::Agent.logger.warn('Tracer#get_span_from_finishable failed to get span from finishable - finishable is not a transaction or segment')
56+
nil
2957
end
3058
end
59+
60+
def can_start_transaction?(parent_span_context)
61+
parent_span_context.remote? || !parent_span_context.valid?
62+
end
63+
64+
def internal_span_kind_with_invalid_parent?(kind, parent_span_context)
65+
!parent_span_context.valid? && kind == :internal
66+
end
67+
68+
def add_remote_partent_span_context_to_txn(txn, parent_span_context)
69+
return unless txn.is_a?(NewRelic::Agent::Transaction) && parent_span_context.remote?
70+
71+
txn.trace_id = parent_span_context.trace_id
72+
txn.parent_span_id = parent_span_context.span_id
73+
end
3174
end
3275
end
3376
end

lib/new_relic/agent/opentelemetry/transaction_patch.rb

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,16 @@ def initialize(_category, _options)
1313
super
1414
end
1515

16-
def otel_current_span_key
17-
# CURRENT_SPAN_KEY is a private constant
18-
::OpenTelemetry::Trace.const_get(:CURRENT_SPAN_KEY)
19-
end
20-
2116
def set_current_segment(new_segment)
2217
@current_segment_lock.synchronize do
23-
# detach the current token, if one is present
2418
unless opentelemetry_context.empty?
2519
::OpenTelemetry::Context.detach(opentelemetry_context[otel_current_span_key])
2620
end
2721

28-
# create an otel span for the new segment
29-
span = Trace::Span.new(segment: new_segment, transaction: new_segment.transaction)
30-
31-
# set otel's current span to the newly created otel span
22+
span = find_or_create_span(new_segment)
3223
ctx = ::OpenTelemetry::Context.current.set_value(otel_current_span_key, span)
33-
34-
# attach the token generated from updating the current span
3524
token = ::OpenTelemetry::Context.attach(ctx)
3625

37-
# update our context tracking hash to correlate the context token
38-
# with the otel_current_span_key
3926
opentelemetry_context[otel_current_span_key] = token
4027
end
4128

@@ -51,6 +38,32 @@ def remove_current_segment_by_thread_id(id)
5138

5239
super
5340
end
41+
42+
private
43+
44+
def find_or_create_span(segment)
45+
if segment.instance_variable_defined?(:@otel_span)
46+
segment.instance_variable_get(:@otel_span)
47+
else
48+
span = Trace::Span.new(span_context: span_context_from_segment(segment))
49+
segment.instance_variable_set(:@otel_span, span)
50+
span
51+
end
52+
end
53+
54+
def span_context_from_segment(segment)
55+
::OpenTelemetry::Trace::SpanContext.new(
56+
trace_id: segment.transaction.trace_id,
57+
span_id: segment.guid,
58+
trace_flags: ::OpenTelemetry::Trace::TraceFlags::SAMPLED,
59+
remote: false
60+
)
61+
end
62+
63+
def otel_current_span_key
64+
# CURRENT_SPAN_KEY is a private constant
65+
::OpenTelemetry::Trace.const_get(:CURRENT_SPAN_KEY)
66+
end
5467
end
5568
end
5669
end

test/multiverse/suites/hybrid_agent/Envfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ end
88

99
gemfile <<~RB
1010
gem 'opentelemetry-api'
11+
gem 'rack'
1112
RB

test/multiverse/suites/hybrid_agent/commands.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,20 @@ def do_work_in_span(span_name:, span_kind:, &block)
1010
end
1111

1212
def do_work_in_span_with_remote_parent(span_name:, span_kind:, &block)
13-
span = @tracer.start_span(span_name, kind: span_kind)
14-
span.context.instance_variable_set(:@remote, true)
13+
context = OpenTelemetry::Context.current
14+
span_context = OpenTelemetry::Trace::SpanContext.new(
15+
trace_id: 'ba8bc8cc6d062849b0efcf3c169afb5a',
16+
span_id: '6d3efb1b173fecfa',
17+
trace_flags: '01',
18+
remote: true
19+
)
20+
21+
span = OpenTelemetry::Trace.non_recording_span(span_context)
22+
parent_context = OpenTelemetry::Trace.context_with_span(span, parent_context: context)
23+
24+
span = @tracer.start_span(span_name, with_parent: parent_context, kind: span_kind)
1525
yield if block
16-
ensure
17-
span&.finish
26+
span.finish
1827
end
1928

2029
def do_work_in_span_with_inbound_context(span_name:, span_kind:, trace_id_in_header:,

test/multiverse/suites/hybrid_agent/hybrid_agent_test.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ def teardown
3232
# until the full suite can be run on the CI
3333
def focus_tests
3434
%w[
35-
does_not_create_segment_without_a_transaction
3635
creates_opentelemetry_segment_in_a_transaction
3736
creates_new_relic_span_as_child_of_opentelemetry_span
37+
does_not_create_segment_without_a_transaction
38+
starting_transaction_tests
3839
]
3940
end
4041

@@ -51,7 +52,7 @@ def focus_tests
5152
parse_operation(o)
5253
end
5354

54-
harvest_and_verify_agent_output(test_case['agentOutput'])
55+
verify_agent_output(test_case['agentOutput'])
5556
else
5657
skip('marked pending by exclusion from #focus_tests')
5758
end

test/multiverse/suites/hybrid_agent/parsing_helpers.rb

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,6 @@
33
# frozen_string_literal: true
44

55
module ParsingHelpers
6-
def harvest_and_verify_agent_output(agent_output)
7-
txns = harvest_transaction_events![1]
8-
spans = harvest_span_events![1]
9-
10-
verify_agent_output(txns, agent_output, 'transactions')
11-
verify_agent_output(spans, agent_output, 'spans')
12-
end
13-
146
def parse_operation(operation)
157
command = parse_command(operation['command'])
168
parameters = parse_parameters(operation['parameters'])
@@ -123,47 +115,48 @@ def matches_assertion(parameters)
123115
assert_equal expected, actual, "Expected #{object} to equal #{expected}"
124116
end
125117

126-
def verify_agent_output(harvest, output, type)
127-
puts "Agent Output: #{type}: #{output[type]}" if ENV['ENABLE_OUTPUT']
118+
def verify_agent_output(output)
119+
verify_transaction_output(output['transactions'])
120+
verify_span_output(output['spans'])
121+
end
128122

129-
if output[type].empty?
130-
assert_empty harvest, "Agent output expected no #{type}. Found: #{harvest}"
131-
else
132-
outputs = prepare_keys(output[type])
133-
events = harvest.flatten.select { |a| a.key?('type') }
123+
def verify_transaction_output(output)
124+
txns = harvest_transaction_events![1].flatten.map { |t| t['name'] }.compact
134125

135-
events.each_with_index do |event, i|
136-
output = outputs[i]
137-
if output && event
138-
msg = "Agent output for #{type.capitalize} wasn't found in the harvest." \
139-
"\nHarvest: #{event}\nAgent output: #{output}"
126+
assert_equal output.length, txns.length, 'Wrong number of transactions found'
140127

141-
if output['parent_name']
142-
result = events.find { |e| e['guid'] == event['parentId'] }.dig('name')
128+
output.each do |txn|
129+
assert_includes(txns, txn['name'], "Transaction name #{txn['name']} missing from output")
130+
end
131+
end
143132

144-
assert_equal output['parent_name'], result, msg
145-
output.delete('parent_name')
146-
end
133+
def verify_span_output(output)
134+
spans = harvest_span_events![1].flatten.reject { |h| h.empty? }
147135

148-
assert event >= output, msg
149-
end
136+
output.each do |expected|
137+
actual = spans.find { |s| s['name'] == expected['name'] }
138+
139+
if expected['category']
140+
assert_equal expected['category'], actual['category'], 'Unexpected category'
141+
end
142+
143+
if expected['entryPoint']
144+
assert_equal expected['entryPoint'], actual['nr.entryPoint'], 'Unexpected entryPoint'
145+
end
146+
147+
expected['attributes']&.each do |expected_key, expected_value|
148+
assert_equal expected_value, actual['attributes'][expected_key], 'Unexpected attribute'
149+
end
150+
151+
if expected['parentName']
152+
result = spans.find { |s| s['guid'] == actual['parentId'] }.dig('name')
153+
154+
assert_equal expected['parentName'], result, 'Unexpected parent name'
150155
end
151156
end
152157
end
153158

154159
def snake_sub_downcase(key)
155160
key.gsub(/(.)([A-Z])/, '\1_\2').downcase
156161
end
157-
158-
def prepare_keys(output)
159-
output.map do |o|
160-
o.transform_keys do |k|
161-
if k == 'entryPoint'
162-
k = 'nr.entryPoint'
163-
else
164-
snake_sub_downcase(k)
165-
end
166-
end
167-
end
168-
end
169162
end

test/multiverse/suites/hybrid_agent/span_test.rb

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,34 @@ module Agent
77
module OpenTelemetry
88
module Trace
99
class SpanTest < Minitest::Test
10-
Segment = Struct.new(:guid)
10+
Segment = Struct.new(:guid, :transaction)
1111
Transaction = Struct.new(:trace_id)
1212

13-
def teardown
14-
NewRelic::Agent.instance.transaction_event_aggregator.reset!
15-
NewRelic::Agent.instance.span_event_aggregator.reset!
13+
def test_finish_does_not_fail_if_no_finishable_present
14+
span = NewRelic::Agent::OpenTelemetry::Trace::Span.new
15+
16+
assert_nil span.finishable
17+
assert_nil span.finish
1618
end
1719

18-
def test_span_has_context
19-
segment = Segment.new('123')
20-
transaction = Transaction.new('456')
20+
def test_finishable_can_finish_transactions
21+
txn = NewRelic::Agent::Tracer.start_transaction_or_segment(category: :web, name: 'test')
22+
span = NewRelic::Agent::OpenTelemetry::Trace::Span.new
23+
span.finishable = txn
24+
span.finish
25+
26+
assert_predicate span.finishable, :finished?
27+
assert_predicate txn, :finished?
28+
end
2129

22-
span = Span.new(segment: segment, transaction: transaction)
30+
def test_finishable_can_finish_segments
31+
segment = NewRelic::Agent::Transaction::Segment.new
32+
span = NewRelic::Agent::OpenTelemetry::Trace::Span.new
33+
span.finishable = segment
34+
span.finish
2335

24-
assert_equal '123', span.context.span_id
25-
assert_equal '456', span.context.trace_id
26-
assert_equal 1, span.context.trace_flags
36+
assert_predicate span.finishable, :finished?
37+
assert_predicate segment, :finished?
2738
end
2839
end
2940
end

test/multiverse/suites/hybrid_agent/tracer_test.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,6 @@ def teardown
1616
NewRelic::Agent.instance.span_event_aggregator.reset!
1717
end
1818

19-
def test_in_span_logs_when_span_kind_unknown
20-
NewRelic::Agent.stub(:logger, NewRelic::Agent::MemoryLogger.new) do
21-
@tracer.in_span('fruit', kind: :mango) { 'yep' }
22-
23-
assert_logged(/Span kind: mango is not supported yet/)
24-
end
25-
end
26-
2719
def test_in_span_creates_segment_when_span_kind_internal
2820
txn = in_transaction do
2921
@tracer.in_span('fruit', kind: :internal) { 'seeds' }
@@ -47,6 +39,17 @@ def test_in_span_captures_error_when_span_kind_internal
4739
assert_transaction_noticed_error txn, 'RuntimeError'
4840
end
4941

42+
def test_start_span_assigns_finishable_to_transaction
43+
otel_span = @tracer.start_span('otel_api_span')
44+
otel_finishable = otel_span.finishable
45+
46+
assert_instance_of NewRelic::Agent::Transaction, otel_finishable, "OTel span's finishable should be an NR Transaction"
47+
48+
otel_span.finish
49+
50+
assert_predicate otel_finishable, :finished?, 'OTel span should finish NR transaction'
51+
end
52+
5053
private
5154

5255
def assert_logged(expected)

0 commit comments

Comments
 (0)