Skip to content

Commit 41d6980

Browse files
feat(traces): wire tracing into the client
Makes tracing usable. Adds the `traces` client option (tracing stays off until it is set), Client.start_span / get_active_span and their posthog module-level counterparts, with the active span scoped per client so two clients never parent to each other's spans. flush() drains spans alongside events within the same budget; shutdown() gives queued spans a final flush of up to 30 s and warns about any it discards; an exit flush bounded by the existing exit deadline covers scripts that never call shutdown(), and a forked child drops the parent's spans. Export failures, limits and the hook are documented on the option. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkZAsCciW4PV8ZdcCHmAbA
1 parent 6ebfcae commit 41d6980

6 files changed

Lines changed: 1057 additions & 11 deletions

File tree

.sampo/changesets/traces-spans.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/posthog: minor
3+
---
4+
5+
Add distributed tracing (alpha): `start_span()` and `get_active_span()` record spans and export them to PostHog as OTLP, with no OpenTelemetry dependency, when the new `traces` client option is set.

posthog/__init__.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from posthog.capture_compression import CaptureCompression as CaptureCompression
1313
from posthog.capture_mode import CaptureMode as CaptureMode
1414
from posthog.client import Client
15+
from posthog.tracing.span import Span
1516
from posthog.async_client import AsyncClient as AsyncClient
1617
from posthog.async_client import AsyncPosthog as AsyncPosthog
1718
from posthog.exception_capture import ExceptionCapture
@@ -340,6 +341,20 @@ def get_tags() -> Dict[str, Any]:
340341
``service_version``, ``environment``, ``flush_interval``, ...). Applied
341342
when ``setup()`` builds the global client, or on a later ``setup()``
342343
call if the metrics API hasn't been used yet.
344+
traces: Config dict for distributed tracing: ``service_name``,
345+
``service_version``, ``environment``, ``resource_attributes``,
346+
``flush_interval`` (5 s), ``max_queue_size`` (2048),
347+
``max_export_batch_size`` (512), ``max_live_spans`` (10000),
348+
``max_span_age`` (3600 s), ``max_attributes_per_span`` (128),
349+
``max_events_per_span`` (128), ``max_attribute_value_length`` (8192).
350+
``before_span_send`` is a callable, or a list run in order,
351+
that receives each finished span as a dict (``trace_id``, ``span_id``
352+
and ``parent_span_id`` are read-only) and returns it, edited, or
353+
``None`` to drop it; a hook that raises drops the span. Tracing is off
354+
until set. Spans export on a background timer even with ``sync_mode``;
355+
serverless handlers should call ``flush()`` before returning. Applied
356+
when ``setup()`` builds the global client, or on a later ``setup()``
357+
call if no span has been started yet.
343358
enable_exception_autocapture: Automatically capture uncaught exceptions.
344359
log_captured_exceptions: Also log exceptions captured by error tracking.
345360
project_root: Root path used to determine in-app exception stack frames.
@@ -396,6 +411,7 @@ def get_tags() -> Dict[str, Any]:
396411
feature_flags_request_max_retries = 1 # type: int
397412
super_properties = None # type: Optional[Dict]
398413
metrics = None # type: Optional[Dict]
414+
traces = None # type: Optional[Dict]
399415
enable_exception_autocapture = False # type: bool
400416
log_captured_exceptions = False # type: bool
401417
# Used to determine in app paths for exception autocapture. Defaults to the current working directory
@@ -1197,6 +1213,83 @@ def join() -> None:
11971213
_proxy("join")
11981214

11991215

1216+
def start_span(
1217+
name: str,
1218+
*,
1219+
kind: Optional[str] = None,
1220+
attributes: Optional[Mapping[str, Any]] = None,
1221+
parent: Union[Span, str, None] = None,
1222+
tracestate: Optional[str] = None,
1223+
start_time: Union[datetime.datetime, float, None] = None,
1224+
) -> Span:
1225+
"""
1226+
Start a span for distributed tracing. Alpha.
1227+
1228+
Returns a span handle. Use it as a context manager to make it the active
1229+
span for the block and end it on exit (recording a raised exception on the
1230+
way out); or call ``end()`` yourself for a span that cannot wrap a block.
1231+
Spans started inside the block nest under it automatically. Always returns
1232+
a usable handle, even when tracing is off, so calling code never branches.
1233+
1234+
Args:
1235+
name: A low-cardinality operation name, e.g. ``GET /users/:id``.
1236+
Variable values belong in attributes, not the name.
1237+
kind: ``internal`` (default), ``server``, ``client``, ``producer`` or
1238+
``consumer``.
1239+
attributes: Initial attributes.
1240+
parent: A span handle, or an inbound W3C ``traceparent`` header value
1241+
to continue a remote trace. Defaults to the active span.
1242+
tracestate: The inbound ``tracestate`` header accompanying a
1243+
``traceparent`` string ``parent``; preserved and propagated.
1244+
start_time: A ``datetime`` or epoch seconds, to backdate the span.
1245+
1246+
Examples:
1247+
```python
1248+
import posthog
1249+
posthog.traces = {"service_name": "checkout-api"}
1250+
1251+
with posthog.start_span("POST /checkout", parent=request.headers.get("traceparent")) as span:
1252+
span.set_attribute("plan", user.plan)
1253+
with posthog.start_span("db.query", kind="client"):
1254+
...
1255+
outgoing_headers = {"traceparent": span.traceparent()}
1256+
```
1257+
1258+
Category:
1259+
Tracing
1260+
"""
1261+
return _proxy(
1262+
"start_span",
1263+
name,
1264+
kind=kind,
1265+
attributes=attributes,
1266+
parent=parent,
1267+
tracestate=tracestate,
1268+
start_time=start_time,
1269+
)
1270+
1271+
1272+
def get_active_span() -> Optional[Span]:
1273+
"""
1274+
The span that is active in the current context, or ``None``. Alpha.
1275+
1276+
Only entering a span (``with posthog.start_span(...) as span:``) makes it
1277+
active; a span started manually is not. Use it to propagate the trace to
1278+
the next service: ``span.traceparent()`` is the header value.
1279+
1280+
Examples:
1281+
```python
1282+
span = posthog.get_active_span()
1283+
if span is not None:
1284+
headers["traceparent"] = span.traceparent()
1285+
```
1286+
1287+
Category:
1288+
Tracing
1289+
"""
1290+
return _proxy("get_active_span")
1291+
1292+
12001293
def shutdown() -> None:
12011294
"""
12021295
Flush all messages and cleanly shutdown the client.
@@ -1259,6 +1352,7 @@ def setup() -> Client:
12591352
feature_flags_request_max_retries=feature_flags_request_max_retries,
12601353
super_properties=super_properties,
12611354
metrics=metrics,
1355+
traces=traces,
12621356
# TODO: Currently this monitoring begins only when the Client is initialised (which happens when you do something with the SDK)
12631357
# This kind of initialisation is very annoying for exception capture. We need to figure out a way around this,
12641358
# or deprecate this proxy option fully (it's already in the process of deprecation, no new clients should be using this method since like 5-6 months)
@@ -1299,6 +1393,8 @@ def setup() -> Client:
12991393
# already forced setup()) still applies until the metrics API is first used.
13001394
if default_client._metrics is None:
13011395
default_client._metrics_config = metrics
1396+
if default_client._traces is None:
1397+
default_client._traces_config = traces
13021398

13031399
return default_client
13041400

0 commit comments

Comments
 (0)