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

Fix deferred tracing decisions for twp #3802

Merged
merged 1 commit into from
Nov 19, 2024
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
11 changes: 10 additions & 1 deletion sentry_sdk/integrations/opentelemetry/sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ def get_parent_sampled(parent_context, trace_id):
# Only inherit sample rate if `traceId` is the same
if is_span_context_valid and parent_context.trace_id == trace_id:
# this is getSamplingDecision in JS
# if there was no sampling flag, defer the decision
dsc_sampled = parent_context.trace_state.get(TRACESTATE_SAMPLED_KEY)
if dsc_sampled == "deferred":
return None

if parent_context.trace_flags.sampled is not None:
return parent_context.trace_flags.sampled

dsc_sampled = parent_context.trace_state.get(TRACESTATE_SAMPLED_KEY)
if dsc_sampled == "true":
return True
elif dsc_sampled == "false":
Expand All @@ -53,6 +57,8 @@ def dropped_result(parent_span_context, attributes, sample_rate=None):

if TRACESTATE_SAMPLED_KEY not in trace_state:
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, "false")
elif trace_state.get(TRACESTATE_SAMPLED_KEY) == "deferred":
trace_state = trace_state.update(TRACESTATE_SAMPLED_KEY, "false")

if sample_rate and TRACESTATE_SAMPLE_RATE_KEY not in trace_state:
trace_state = trace_state.add(TRACESTATE_SAMPLE_RATE_KEY, str(sample_rate))
Expand Down Expand Up @@ -88,6 +94,9 @@ def sampled_result(span_context, attributes, sample_rate):

if TRACESTATE_SAMPLED_KEY not in trace_state:
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, "true")
elif trace_state.get(TRACESTATE_SAMPLED_KEY) == "deferred":
trace_state = trace_state.update(TRACESTATE_SAMPLED_KEY, "true")

if TRACESTATE_SAMPLE_RATE_KEY not in trace_state:
trace_state = trace_state.add(TRACESTATE_SAMPLE_RATE_KEY, str(sample_rate))

Expand Down
11 changes: 9 additions & 2 deletions sentry_sdk/integrations/opentelemetry/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
SpanContext,
NonRecordingSpan,
TraceFlags,
TraceState,
use_span,
)

Expand All @@ -14,6 +15,7 @@
SENTRY_FORK_ISOLATION_SCOPE_KEY,
SENTRY_USE_CURRENT_SCOPE_KEY,
SENTRY_USE_ISOLATION_SCOPE_KEY,
TRACESTATE_SAMPLED_KEY,
)
from sentry_sdk.integrations.opentelemetry.utils import trace_state_from_baggage
from sentry_sdk.scope import Scope, ScopeType
Expand Down Expand Up @@ -96,10 +98,15 @@ def _incoming_otel_span_context(self):
else TraceFlags.DEFAULT
)

# TODO-neel-potel do we need parent and sampled like JS?
trace_state = None
if self._propagation_context.baggage:
trace_state = trace_state_from_baggage(self._propagation_context.baggage)
else:
trace_state = TraceState()

# for twp to work, we also need to consider deferred sampling when the sampling
# flag is not present, so the above TraceFlags are not sufficient
if self._propagation_context.parent_sampled is None:
trace_state = trace_state.add(TRACESTATE_SAMPLED_KEY, "deferred")

span_context = SpanContext(
trace_id=int(self._propagation_context.trace_id, 16), # type: ignore
Expand Down
2 changes: 2 additions & 0 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,8 @@ def __exit__(self, ty, value, tb):
# type: (Optional[Any], Optional[Any], Optional[Any]) -> None
if value is not None:
self.set_status(SPANSTATUS.INTERNAL_ERROR)
else:
self.set_status(SPANSTATUS.OK)

self.finish()
context.detach(self._ctx_token)
Expand Down
6 changes: 2 additions & 4 deletions tests/integrations/opentelemetry/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def test_transaction_name_span_description_compat(

events = capture_events()

with sentry_sdk.start_transaction(
with sentry_sdk.start_span(
name="trx-name",
op="trx-op",
) as trx:
Expand All @@ -33,13 +33,12 @@ def test_transaction_name_span_description_compat(
assert spn.__class__.__name__ == "POTelSpan"
assert spn.op == "span-op"
assert spn.description == "span-desc"
assert spn.name is None
assert spn.name == "span-desc"

assert spn._otel_span is not None
assert spn._otel_span.name == "span-desc"
assert spn._otel_span.attributes["sentry.op"] == "span-op"
assert spn._otel_span.attributes["sentry.description"] == "span-desc"
assert "sentry.name" not in spn._otel_span.attributes

transaction = events[0]
assert transaction["transaction"] == "trx-name"
Expand All @@ -53,4 +52,3 @@ def test_transaction_name_span_description_compat(
assert span["op"] == "span-op"
assert span["data"]["sentry.op"] == "span-op"
assert span["data"]["sentry.description"] == "span-desc"
assert "sentry.name" not in span["data"]
47 changes: 0 additions & 47 deletions tests/integrations/opentelemetry/test_experimental.py

This file was deleted.

36 changes: 19 additions & 17 deletions tests/integrations/opentelemetry/test_potel.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import pytest

from opentelemetry import trace

import sentry_sdk
from tests.conftest import ApproxDict


tracer = trace.get_tracer(__name__)
Expand Down Expand Up @@ -43,7 +43,6 @@ def test_root_span_transaction_payload_started_with_otel_only(capture_envelopes)
assert "span_id" in trace_context
assert trace_context["origin"] == "manual"
assert trace_context["op"] == "request"
assert trace_context["status"] == "ok"

assert payload["spans"] == []

Expand All @@ -63,7 +62,6 @@ def test_child_span_payload_started_with_otel_only(capture_envelopes):
assert span["op"] == "db"
assert span["description"] == "db"
assert span["origin"] == "manual"
assert span["status"] == "ok"
assert span["span_id"] is not None
assert span["trace_id"] == payload["contexts"]["trace"]["trace_id"]
assert span["parent_span_id"] == payload["contexts"]["trace"]["span_id"]
Expand Down Expand Up @@ -222,8 +220,8 @@ def test_span_attributes_in_data_started_with_otel(capture_envelopes):
(item,) = envelope.items
payload = item.payload.json

assert payload["contexts"]["trace"]["data"] == {"foo": "bar", "baz": 42}
assert payload["spans"][0]["data"] == {"abc": 99, "def": "moo"}
assert payload["contexts"]["trace"]["data"] == ApproxDict({"foo": "bar", "baz": 42})
assert payload["spans"][0]["data"] == ApproxDict({"abc": 99, "def": "moo"})


def test_span_data_started_with_sentry(capture_envelopes):
Expand All @@ -238,18 +236,22 @@ def test_span_data_started_with_sentry(capture_envelopes):
(item,) = envelope.items
payload = item.payload.json

assert payload["contexts"]["trace"]["data"] == {
"foo": "bar",
"sentry.origin": "manual",
"sentry.description": "request",
"sentry.op": "http",
}
assert payload["spans"][0]["data"] == {
"baz": 42,
"sentry.origin": "manual",
"sentry.description": "statement",
"sentry.op": "db",
}
assert payload["contexts"]["trace"]["data"] == ApproxDict(
{
"foo": "bar",
"sentry.origin": "manual",
"sentry.description": "request",
"sentry.op": "http",
}
)
assert payload["spans"][0]["data"] == ApproxDict(
{
"baz": 42,
"sentry.origin": "manual",
"sentry.description": "statement",
"sentry.op": "db",
}
)


def test_transaction_tags_started_with_otel(capture_envelopes):
Expand Down
85 changes: 36 additions & 49 deletions tests/integrations/opentelemetry/test_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,13 @@ def test_sampling_traces_sample_rate_50(sentry_init, capture_envelopes):

envelopes = capture_envelopes()

with mock.patch(
"sentry_sdk.integrations.opentelemetry.sampler.random", return_value=0.2
): # drop
with mock.patch("random.random", return_value=0.2): # drop
with sentry_sdk.start_span(description="request a"):
with sentry_sdk.start_span(description="cache a"):
with sentry_sdk.start_span(description="db a"):
...

with mock.patch(
"sentry_sdk.integrations.opentelemetry.sampler.random", return_value=0.7
): # keep
with mock.patch("random.random", return_value=0.7): # keep
with sentry_sdk.start_span(description="request b"):
with sentry_sdk.start_span(description="cache b"):
with sentry_sdk.start_span(description="db b"):
Expand All @@ -101,46 +97,34 @@ def test_sampling_traces_sample_rate_50(sentry_init, capture_envelopes):
def test_sampling_traces_sampler(sentry_init, capture_envelopes):
def keep_only_a(sampling_context):
if " a" in sampling_context["transaction_context"]["name"]:
return 0.05
return 1
else:
return 0

sentry_init(
traces_sample_rate=1.0,
traces_sampler=keep_only_a,
)
sentry_init(traces_sampler=keep_only_a)

envelopes = capture_envelopes()

# Make sure random() always returns the same values
with mock.patch(
"sentry_sdk.integrations.opentelemetry.sampler.random",
side_effect=[0.04 for _ in range(12)],
):

with sentry_sdk.start_span(description="request a"): # keep
with sentry_sdk.start_span(description="cache a"): # keep
with sentry_sdk.start_span(description="db a"): # keep
...
# children inherit from root spans
with sentry_sdk.start_span(description="request a"): # keep
with sentry_sdk.start_span(description="cache a"):
with sentry_sdk.start_span(description="db a"):
...

with sentry_sdk.start_span(description="request b"): # drop
with sentry_sdk.start_span(description="cache b"): # drop
with sentry_sdk.start_span(description="db b"): # drop
...
with sentry_sdk.start_span(description="request b"): # drop
with sentry_sdk.start_span(description="cache b"):
with sentry_sdk.start_span(description="db b"):
...

with sentry_sdk.start_span(description="request c"): # drop
with sentry_sdk.start_span(
description="cache a c"
): # keep (but trx dropped, so not collected)
with sentry_sdk.start_span(
description="db a c"
): # keep (but trx dropped, so not collected)
...
with sentry_sdk.start_span(description="request c"): # drop
with sentry_sdk.start_span(description="cache a c"):
with sentry_sdk.start_span(description="db a c"):
...

with sentry_sdk.start_span(description="new a c"): # keep
with sentry_sdk.start_span(description="cache c"): # drop
with sentry_sdk.start_span(description="db c"): # drop
...
with sentry_sdk.start_span(description="new a c"): # keep
with sentry_sdk.start_span(description="cache c"):
with sentry_sdk.start_span(description="db c"):
...

assert len(envelopes) == 2
(envelope1, envelope2) = envelopes
Expand All @@ -150,7 +134,7 @@ def keep_only_a(sampling_context):
assert transaction1["transaction"] == "request a"
assert len(transaction1["spans"]) == 2
assert transaction2["transaction"] == "new a c"
assert len(transaction2["spans"]) == 0
assert len(transaction2["spans"]) == 2


def test_sampling_traces_sampler_boolean(sentry_init, capture_envelopes):
Expand All @@ -168,21 +152,21 @@ def keep_only_a(sampling_context):
envelopes = capture_envelopes()

with sentry_sdk.start_span(description="request a"): # keep
with sentry_sdk.start_span(description="cache a"): # keep
with sentry_sdk.start_span(description="db X"): # drop
with sentry_sdk.start_span(description="cache a"):
with sentry_sdk.start_span(description="db X"):
...

with sentry_sdk.start_span(description="request b"): # drop
with sentry_sdk.start_span(description="cache b"): # drop
with sentry_sdk.start_span(description="db b"): # drop
with sentry_sdk.start_span(description="cache b"):
with sentry_sdk.start_span(description="db b"):
...

assert len(envelopes) == 1
(envelope,) = envelopes
transaction = envelope.items[0].payload.json

assert transaction["transaction"] == "request a"
assert len(transaction["spans"]) == 1
assert len(transaction["spans"]) == 2


@pytest.mark.parametrize(
Expand Down Expand Up @@ -237,21 +221,24 @@ def test_sampling_parent_sampled(


@pytest.mark.parametrize(
"traces_sample_rate, expected_num_of_envelopes",
"traces_sample_rate, upstream_sampled, expected_num_of_envelopes",
[
# special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=None will be used)
(-1, 0),
(-1, 0, 0),
# traces_sample_rate=None means do not create new traces, and also do not continue incoming traces. So, no envelopes at all.
(None, 0),
(None, 1, 0),
# traces_sample_rate=0 means do not create new traces (0% of the requests), but continue incoming traces. So envelopes will be created only if there is an incoming trace.
(0, 0),
(0, 0, 0),
(0, 1, 1),
# traces_sample_rate=1 means create new traces for 100% of requests (and also continue incoming traces, of course).
(1, 1),
(1, 0, 0),
(1, 1, 1),
],
)
def test_sampling_parent_dropped(
sentry_init,
traces_sample_rate,
upstream_sampled,
expected_num_of_envelopes,
capture_envelopes,
):
Expand All @@ -265,7 +252,7 @@ def test_sampling_parent_dropped(

# The upstream service has dropped the request
headers = {
"sentry-trace": "771a43a4192642f0b136d5159a501700-1234567890abcdef-0",
"sentry-trace": f"771a43a4192642f0b136d5159a501700-1234567890abcdef-{upstream_sampled}",
}
with sentry_sdk.continue_trace(headers):
with sentry_sdk.start_span(description="request a"):
Expand Down
Loading