Skip to content

Commit db6cedf

Browse files
authored
Add sentry_opentelemetry_trace_url_template (#121)
* Add trace URL support to Sentry events with configurable template * Refactor Sentry tests to use mock.patch and improve trace URL logic * Add test for preserving existing contexts in Sentry trace URL injection * Add Sentry trace URL template parameter and documentation * Enable OpenTelemetry log traces and fix Sentry trace URL integration * Refactor Sentry trace URL injection and improve test coverage * Move trace URL to event extra data instead of contexts * Simplify Sentry trace URL addition logic and improve tests * Refactor Sentry trace URL generation with constants * Update Sentry trace URL template documentation with example * 1 * Remove unnecessary trace logging and fix import order * 1
1 parent 057c180 commit db6cedf

File tree

3 files changed

+89
-4
lines changed

3 files changed

+89
-4
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,12 +288,27 @@ class YourSettings(BaseServiceSettings):
288288
sentry_integrations: list[Integration] = []
289289
sentry_additional_params: dict[str, typing.Any] = {}
290290
sentry_tags: dict[str, str] | None = None
291+
sentry_opentelemetry_trace_url_template: str | None = None
291292

292293
... # Other settings here
293294
```
294295

295296
These settings are subsequently passed to the [sentry-sdk](https://pypi.org/project/sentry-sdk/) package, finalizing your Sentry integration.
296297

298+
Parameter descriptions:
299+
300+
- `service_environment` - The environment name for Sentry events.
301+
- `sentry_dsn` - The Data Source Name for your Sentry project.
302+
- `sentry_traces_sample_rate` - The rate at which traces are sampled (via Sentry Tracing, not OpenTelemetry).
303+
- `sentry_sample_rate` - The rate at which transactions are sampled.
304+
- `sentry_max_breadcrumbs` - The maximum number of breadcrumbs to keep.
305+
- `sentry_max_value_length` - The maximum length of values in Sentry events.
306+
- `sentry_attach_stacktrace` - Whether to attach stacktraces to messages.
307+
- `sentry_integrations` - A list of Sentry integrations to enable.
308+
- `sentry_additional_params` - Additional parameters to pass to Sentry SDK.
309+
- `sentry_tags` - Tags to apply to all Sentry events.
310+
- `sentry_opentelemetry_trace_url_template` - Template for OpenTelemetry trace URLs to add to Sentry events (example: `"https://example.com/traces/{trace_id}"`).
311+
297312
### [Prometheus](https://prometheus.io/)
298313

299314
Prometheus integration presents a challenge because the underlying libraries for `FastAPI`, `Litestar` and `FastStream` differ significantly, making it impossible to unify them under a single interface. As a result, the Prometheus settings for `FastAPI`, `Litestar` and `FastStream` must be configured separately.

microbootstrap/instruments/sentry_instrument.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
import contextlib
3+
import functools
34
import typing
45

56
import orjson
@@ -24,6 +25,7 @@ class SentryConfig(BaseInstrumentConfig):
2425
sentry_additional_params: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
2526
sentry_tags: dict[str, str] | None = None
2627
sentry_before_send: typing.Callable[[typing.Any, typing.Any], typing.Any | None] | None = None
28+
sentry_opentelemetry_trace_url_template: str | None = None
2729

2830

2931
IGNORED_STRUCTLOG_ATTRIBUTES: typing.Final = frozenset({"event", "level", "logger", "tracing", "timestamp"})
@@ -58,6 +60,18 @@ def enrich_sentry_event_from_structlog_log(event: sentry_types.Event, _hint: sen
5860
return event
5961

6062

63+
SENTRY_EXTRA_OTEL_TRACE_ID_KEY: typing.Final = "otelTraceID"
64+
SENTRY_EXTRA_OTEL_TRACE_URL_KEY: typing.Final = "otelTraceURL"
65+
66+
67+
def add_trace_url_to_event(
68+
trace_link_template: str, event: sentry_types.Event, _hint: sentry_types.Hint
69+
) -> sentry_types.Event:
70+
if trace_link_template and (trace_id := event.get("extra", {}).get(SENTRY_EXTRA_OTEL_TRACE_ID_KEY)):
71+
event["extra"][SENTRY_EXTRA_OTEL_TRACE_URL_KEY] = trace_link_template.replace("{trace_id}", str(trace_id))
72+
return event
73+
74+
6175
def wrap_before_send_callbacks(*callbacks: sentry_types.EventProcessor | None) -> sentry_types.EventProcessor:
6276
def run_before_send(event: sentry_types.Event, hint: sentry_types.Hint) -> sentry_types.Event | None:
6377
for callback in callbacks:
@@ -89,7 +103,13 @@ def bootstrap(self) -> None:
89103
max_value_length=self.instrument_config.sentry_max_value_length,
90104
attach_stacktrace=self.instrument_config.sentry_attach_stacktrace,
91105
before_send=wrap_before_send_callbacks(
92-
enrich_sentry_event_from_structlog_log, self.instrument_config.sentry_before_send
106+
enrich_sentry_event_from_structlog_log,
107+
functools.partial(
108+
add_trace_url_to_event, self.instrument_config.sentry_opentelemetry_trace_url_template
109+
)
110+
if self.instrument_config.sentry_opentelemetry_trace_url_template
111+
else None,
112+
self.instrument_config.sentry_before_send,
93113
),
94114
integrations=self.instrument_config.sentry_integrations,
95115
**self.instrument_config.sentry_additional_params,

tests/instruments/test_sentry.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22
import copy
33
import typing
44
from unittest import mock
5-
from unittest.mock import patch
65

76
import litestar
87
import pytest
98
from litestar.testing import TestClient as LitestarTestClient
109

1110
from microbootstrap.bootstrappers.litestar import LitestarSentryInstrument
12-
from microbootstrap.instruments.sentry_instrument import SentryInstrument, enrich_sentry_event_from_structlog_log
11+
from microbootstrap.instruments.sentry_instrument import (
12+
SENTRY_EXTRA_OTEL_TRACE_ID_KEY,
13+
SENTRY_EXTRA_OTEL_TRACE_URL_KEY,
14+
SentryInstrument,
15+
add_trace_url_to_event,
16+
enrich_sentry_event_from_structlog_log,
17+
)
1318

1419

1520
if typing.TYPE_CHECKING:
21+
import faker
1622
from sentry_sdk import _types as sentry_types
1723

1824
from microbootstrap import SentryConfig
@@ -61,7 +67,7 @@ async def error_handler() -> None:
6167

6268
sentry_instrument.bootstrap()
6369
litestar_application: typing.Final = litestar.Litestar(route_handlers=[error_handler])
64-
with patch("sentry_sdk.Scope.capture_event") as mock_capture_event:
70+
with mock.patch("sentry_sdk.Scope.capture_event") as mock_capture_event:
6571
with LitestarTestClient(app=litestar_application) as test_client:
6672
test_client.get("/test-error-handler")
6773

@@ -110,3 +116,47 @@ def test_skip(self, event: sentry_types.Event) -> None:
110116
)
111117
def test_modify(self, event_before: sentry_types.Event, event_after: sentry_types.Event) -> None:
112118
assert enrich_sentry_event_from_structlog_log(event_before, mock.Mock()) == event_after
119+
120+
121+
TRACE_URL_TEMPLATE = "https://example.com/traces/{trace_id}"
122+
123+
124+
class TestSentryAddTraceUrlToEvent:
125+
def test_add_trace_url_with_trace_id(self, faker: faker.Faker) -> None:
126+
trace_id = faker.pystr()
127+
event: sentry_types.Event = {"extra": {SENTRY_EXTRA_OTEL_TRACE_ID_KEY: trace_id}}
128+
129+
result = add_trace_url_to_event(TRACE_URL_TEMPLATE, event, mock.Mock())
130+
131+
assert result["extra"][SENTRY_EXTRA_OTEL_TRACE_URL_KEY] == f"https://example.com/traces/{trace_id}"
132+
133+
@pytest.mark.parametrize(
134+
"event",
135+
[
136+
{},
137+
{"extra": {}},
138+
{"extra": {"other_field": "value"}},
139+
{"extra": {SENTRY_EXTRA_OTEL_TRACE_ID_KEY: None}},
140+
{"extra": {SENTRY_EXTRA_OTEL_TRACE_ID_KEY: ""}},
141+
],
142+
)
143+
def test_add_trace_url_without_trace_id(self, event: sentry_types.Event) -> None:
144+
result = add_trace_url_to_event(TRACE_URL_TEMPLATE, event, mock.Mock())
145+
146+
assert SENTRY_EXTRA_OTEL_TRACE_URL_KEY not in result.get("extra", {})
147+
148+
def test_add_trace_url_empty_template(self, faker: faker.Faker) -> None:
149+
event: sentry_types.Event = {"extra": {SENTRY_EXTRA_OTEL_TRACE_ID_KEY: faker.pystr()}}
150+
151+
result = add_trace_url_to_event("", event, mock.Mock())
152+
153+
assert SENTRY_EXTRA_OTEL_TRACE_URL_KEY not in result["extra"]
154+
155+
@pytest.mark.parametrize("event", [{}, {"contexts": {}}])
156+
def test_add_trace_url_creates_contexts(self, faker: faker.Faker, event: sentry_types.Event) -> None:
157+
event["extra"] = {SENTRY_EXTRA_OTEL_TRACE_ID_KEY: faker.pystr()}
158+
159+
result = add_trace_url_to_event(TRACE_URL_TEMPLATE, event, mock.Mock())
160+
161+
assert SENTRY_EXTRA_OTEL_TRACE_URL_KEY in result["extra"]
162+
assert SENTRY_EXTRA_OTEL_TRACE_ID_KEY in result["extra"]

0 commit comments

Comments
 (0)