Skip to content

Commit fb7b731

Browse files
committed
distributed tracing support with the parent id set before
1 parent b7e1679 commit fb7b731

File tree

2 files changed

+87
-5
lines changed

2 files changed

+87
-5
lines changed

lib/sentry/opentelemetry/span_processor.ex

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ if Code.ensure_loaded?(OpenTelemetry) do
1515
alias Sentry.{Transaction, OpenTelemetry.SpanStorage, OpenTelemetry.SpanRecord}
1616
alias Sentry.Interfaces.Span
1717

18-
# This can be a no-op since we can postpone inserting the span into storage until on_end
18+
# This can be a no-op since we can postpone inserting the span into storage until on_end
1919
@impl :otel_span_processor
2020
def on_start(_ctx, otel_span, _config) do
2121
otel_span
2222
end
2323

24-
@impl :otel_span_processor
24+
@impl :otel_span_processor
2525
def on_end(otel_span, _config) do
2626
span_record = SpanRecord.new(otel_span)
2727

2828
SpanStorage.store_span(span_record)
2929

30-
if span_record.parent_span_id == nil do
30+
# Check if this is a root span (no parent) or a transaction root (HTTP request span)
31+
is_transaction_root = span_record.parent_span_id == nil or is_http_request_span?(span_record)
32+
33+
if is_transaction_root do
3134
child_span_records = SpanStorage.get_child_spans(span_record.span_id)
3235
transaction = build_transaction(span_record, child_span_records)
3336

@@ -60,7 +63,20 @@ if Code.ensure_loaded?(OpenTelemetry) do
6063
:ok
6164
end
6265

63-
defp build_transaction(root_span_record, child_span_records) do
66+
# Helper function to detect if a span represents an HTTP request that should be treated as a transaction root
67+
defp is_http_request_span?(%{attributes: attributes, name: name}) do
68+
# Check if this is an HTTP request span (has HTTP method and is likely a server span)
69+
has_http_method = Map.has_key?(attributes, to_string(HTTPAttributes.http_request_method()))
70+
has_http_route = Map.has_key?(attributes, "http.route")
71+
has_url_path = Map.has_key?(attributes, to_string(URLAttributes.url_path()))
72+
73+
# Check if the name looks like an HTTP endpoint
74+
name_looks_like_http = String.contains?(name, ["/", "POST", "GET", "PUT", "DELETE", "PATCH"])
75+
76+
(has_http_method and (has_http_route or has_url_path)) or name_looks_like_http
77+
end
78+
79+
defp build_transaction(root_span_record, child_span_records) do
6480
root_span = build_span(root_span_record)
6581
child_spans = Enum.map(child_span_records, &build_span(&1))
6682

@@ -77,7 +93,7 @@ if Code.ensure_loaded?(OpenTelemetry) do
7793
})
7894
end
7995

80-
defp transaction_name(
96+
defp transaction_name(
8197
%{attributes: %{unquote(to_string(MessagingAttributes.messaging_system())) => :oban}} =
8298
span_record
8399
) do

test/sentry/opentelemetry/span_processor_test.exs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,72 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do
234234
assert length(transaction.spans) == 0
235235
assert transaction.transaction == "child_instrumented_function_standalone"
236236
end
237+
238+
@tag span_storage: true
239+
test "treats HTTP request spans as transaction roots even with external parents" do
240+
put_test_config(environment_name: "test", traces_sample_rate: 1.0)
241+
242+
Sentry.Test.start_collecting_sentry_reports()
243+
244+
# Simulate an HTTP request span with external parent (like from browser tracing)
245+
require OpenTelemetry.Tracer, as: Tracer
246+
require OpenTelemetry.SemConv.Incubating.HTTPAttributes, as: HTTPAttributes
247+
require OpenTelemetry.SemConv.Incubating.URLAttributes, as: URLAttributes
248+
249+
# Create a span with HTTP attributes and an external parent span ID
250+
external_parent_span_id = "b943d7459127970c"
251+
252+
# Start a span that simulates an HTTP request from an external trace
253+
Tracer.with_span "POST /api/v1alpha", %{
254+
attributes: %{
255+
HTTPAttributes.http_request_method() => :POST,
256+
URLAttributes.url_path() => "/api/v1alpha",
257+
"http.route" => "/api/v1alpha",
258+
"server.address" => "localhost",
259+
"server.port" => 4000
260+
},
261+
parent: {:span_context, :undefined, external_parent_span_id, :undefined, :undefined, :undefined, :undefined, :undefined, :undefined, :undefined}
262+
} do
263+
# Simulate child spans (database queries, etc.) with proper DB attributes
264+
Tracer.with_span "matrix_data.repo.query", %{
265+
attributes: %{
266+
"db.system" => :postgresql,
267+
"db.statement" => "SELECT * FROM users"
268+
}
269+
} do
270+
Process.sleep(10)
271+
end
272+
273+
Tracer.with_span "matrix_data.repo.query:agents", %{
274+
attributes: %{
275+
"db.system" => :postgresql,
276+
"db.statement" => "INSERT INTO agents (...) VALUES (...)"
277+
}
278+
} do
279+
Process.sleep(10)
280+
end
281+
end
282+
283+
# Should capture the HTTP request span as a transaction root despite having an external parent
284+
assert [%Sentry.Transaction{} = transaction] = Sentry.Test.pop_sentry_transactions()
285+
286+
# Verify transaction properties
287+
assert transaction.transaction == "POST /api/v1alpha"
288+
assert transaction.transaction_info == %{source: :custom}
289+
assert length(transaction.spans) == 2
290+
291+
# Verify child spans are properly included - they should have "db" operation
292+
span_names = Enum.map(transaction.spans, & &1.op) |> Enum.sort()
293+
expected_names = ["db", "db"]
294+
assert span_names == expected_names
295+
296+
# Verify all spans share the same trace ID
297+
trace_id = transaction.contexts.trace.trace_id
298+
Enum.each(transaction.spans, fn span ->
299+
assert span.trace_id == trace_id
300+
end)
301+
end
302+
237303

238304
@tag span_storage: true
239305
test "concurrent traces maintain independent sampling decisions" do

0 commit comments

Comments
 (0)