Skip to content

Commit a446c8a

Browse files
sl0thentr0pyclaude
andcommitted
feat(otlp): Add collector_url option to OTLPIntegration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 763b16a commit a446c8a

File tree

2 files changed

+78
-3
lines changed

2 files changed

+78
-3
lines changed

sentry_sdk/integrations/otlp.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ def otel_propagation_context() -> "Optional[Tuple[str, str]]":
6666
return (format_trace_id(ctx.trace_id), format_span_id(ctx.span_id))
6767

6868

69-
def setup_otlp_traces_exporter(dsn: "Optional[str]" = None) -> None:
69+
def setup_otlp_traces_exporter(
70+
dsn: "Optional[str]" = None, collector_url: "Optional[str]" = None
71+
) -> None:
7072
tracer_provider = get_tracer_provider()
7173

7274
if not isinstance(tracer_provider, TracerProvider):
@@ -76,7 +78,10 @@ def setup_otlp_traces_exporter(dsn: "Optional[str]" = None) -> None:
7678

7779
endpoint = None
7880
headers = None
79-
if dsn:
81+
if collector_url:
82+
endpoint = collector_url
83+
logger.debug(f"[OTLP] Sending traces to collector at {endpoint}")
84+
elif dsn:
8085
auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}")
8186
endpoint = auth.get_api_url(EndpointType.OTLP_TRACES)
8287
headers = {"X-Sentry-Auth": auth.to_header()}
@@ -176,6 +181,8 @@ class OTLPIntegration(Integration):
176181
"""
177182
Automatically setup OTLP ingestion from the DSN.
178183
184+
:param collector_url: URL of your own OpenTelemetry collector, defaults to None.
185+
When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN.
179186
:param setup_otlp_traces_exporter: Automatically configure an Exporter to send OTLP traces from the DSN, defaults to True.
180187
Set to False if using a custom collector or to setup the TracerProvider manually.
181188
:param setup_propagator: Automatically configure the Sentry Propagator for Distributed Tracing, defaults to True.
@@ -188,10 +195,12 @@ class OTLPIntegration(Integration):
188195

189196
def __init__(
190197
self,
198+
collector_url: "Optional[str]" = None,
191199
setup_otlp_traces_exporter: bool = True,
192200
setup_propagator: bool = True,
193201
capture_exceptions: bool = False,
194202
) -> None:
203+
self.collector_url = collector_url
195204
self.setup_otlp_traces_exporter = setup_otlp_traces_exporter
196205
self.setup_propagator = setup_propagator
197206
self.capture_exceptions = capture_exceptions
@@ -207,7 +216,7 @@ def setup_once_with_options(
207216
if self.setup_otlp_traces_exporter:
208217
logger.debug("[OTLP] Setting up OTLP exporter")
209218
dsn: "Optional[str]" = options.get("dsn") if options else None
210-
setup_otlp_traces_exporter(dsn)
219+
setup_otlp_traces_exporter(dsn, collector_url=self.collector_url)
211220

212221
if self.setup_propagator:
213222
logger.debug("[OTLP] Setting up propagator for distributed tracing")

tests/integrations/otlp/test_otlp.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ def mock_otlp_ingest():
3232
url="https://bla.ingest.sentry.io/api/12312012/integration/otlp/v1/traces/",
3333
status=200,
3434
)
35+
responses.add(
36+
responses.POST,
37+
url="https://my-collector.example.com/v1/traces",
38+
status=200,
39+
)
3540

3641
yield
3742

@@ -233,6 +238,67 @@ def test_propagator_inject_continue_trace(sentry_init):
233238
detach(token)
234239

235240

241+
def test_collector_url_sets_endpoint(sentry_init):
242+
sentry_init(
243+
dsn="https://mysecret@bla.ingest.sentry.io/12312012",
244+
integrations=[
245+
OTLPIntegration(collector_url="https://my-collector.example.com/v1/traces")
246+
],
247+
)
248+
249+
tracer_provider = get_tracer_provider()
250+
assert isinstance(tracer_provider, TracerProvider)
251+
252+
(span_processor,) = tracer_provider._active_span_processor._span_processors
253+
assert isinstance(span_processor, BatchSpanProcessor)
254+
255+
exporter = span_processor.span_exporter
256+
assert isinstance(exporter, OTLPSpanExporter)
257+
assert exporter._endpoint == "https://my-collector.example.com/v1/traces"
258+
assert exporter._headers is None or "X-Sentry-Auth" not in exporter._headers
259+
260+
261+
def test_collector_url_takes_precedence_over_dsn(sentry_init):
262+
sentry_init(
263+
dsn="https://mysecret@bla.ingest.sentry.io/12312012",
264+
integrations=[
265+
OTLPIntegration(collector_url="https://my-collector.example.com/v1/traces")
266+
],
267+
)
268+
269+
tracer_provider = get_tracer_provider()
270+
assert isinstance(tracer_provider, TracerProvider)
271+
272+
(span_processor,) = tracer_provider._active_span_processor._span_processors
273+
exporter = span_processor.span_exporter
274+
assert isinstance(exporter, OTLPSpanExporter)
275+
# Should use collector_url, NOT the DSN-derived endpoint
276+
assert exporter._endpoint == "https://my-collector.example.com/v1/traces"
277+
assert (
278+
exporter._endpoint
279+
!= "https://bla.ingest.sentry.io/api/12312012/integration/otlp/v1/traces/"
280+
)
281+
282+
283+
def test_collector_url_none_falls_back_to_dsn(sentry_init):
284+
sentry_init(
285+
dsn="https://mysecret@bla.ingest.sentry.io/12312012",
286+
integrations=[OTLPIntegration(collector_url=None)],
287+
)
288+
289+
tracer_provider = get_tracer_provider()
290+
assert isinstance(tracer_provider, TracerProvider)
291+
292+
(span_processor,) = tracer_provider._active_span_processor._span_processors
293+
exporter = span_processor.span_exporter
294+
assert isinstance(exporter, OTLPSpanExporter)
295+
assert (
296+
exporter._endpoint
297+
== "https://bla.ingest.sentry.io/api/12312012/integration/otlp/v1/traces/"
298+
)
299+
assert "X-Sentry-Auth" in exporter._headers
300+
301+
236302
def test_capture_exceptions_enabled(sentry_init, capture_events):
237303
sentry_init(
238304
dsn="https://mysecret@bla.ingest.sentry.io/12312012",

0 commit comments

Comments
 (0)