Skip to content

Commit 1fe612b

Browse files
committed
Add a Connect toolbar item linking to the request's trace
When plain.toolbar is installed and connect is exporting, a Trace button links the current request to its `/t/<trace_id>` page in Plain Cloud (or shows "Not sampled" when the trace was dropped). Adds the CONNECT_DASHBOARD_URL setting for the dashboard base URL, and a shared current_trace() helper used by both the toolbar item and pageviews.
1 parent 66d2cdc commit 1fe612b

3 files changed

Lines changed: 46 additions & 28 deletions

File tree

plain-connect/plain/connect/templates.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
from urllib.parse import quote
55

66
from jinja2.runtime import Context
7-
from opentelemetry import trace
87

98
from plain.runtime import settings
109
from plain.templates import register_template_extension, register_template_global
1110
from plain.templates.jinja.extensions import InclusionTagExtension
1211

1312
from .identity import encrypt_identity, sign_render_token
13+
from .tracing import current_trace
1414

1515
if TYPE_CHECKING:
1616
from plain.http import Request
@@ -43,7 +43,7 @@ def get_context(
4343
"connect_pageviews_identity": _identity_token(request, secret)
4444
if token
4545
else "",
46-
"connect_pageviews_trace_id": _current_trace_id() if token else "",
46+
"connect_pageviews_trace_id": current_trace().trace_id if token else "",
4747
"connect_pageviews_route": _current_route(request) if token else "",
4848
}
4949

@@ -89,10 +89,3 @@ def _identity_token(request: Request | None, secret: str) -> str:
8989
if user is None:
9090
return ""
9191
return encrypt_identity(user.id, secret)
92-
93-
94-
def _current_trace_id() -> str:
95-
span_context = trace.get_current_span().get_span_context()
96-
if not span_context.trace_id:
97-
return ""
98-
return format(span_context.trace_id, "032x")

plain-connect/plain/connect/toolbar.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,21 @@
22

33
from typing import Any
44

5-
from opentelemetry import trace
6-
from opentelemetry.trace import format_trace_id
7-
85
from plain.runtime import settings
96
from plain.toolbar import ToolbarItem, register_toolbar_item
107

8+
from .tracing import current_trace
9+
1110

1211
@register_toolbar_item
1312
class ConnectToolbarItem(ToolbarItem):
1413
"""Links the current request to its exported trace in Plain Cloud.
1514
1615
This module is only imported when plain.toolbar is installed (it is
17-
autodiscovered by the toolbar package), so the import above is safe
18-
without a guard. The item is a button-only toolbar entry — no panel
19-
that points at the `/t/<trace_id>` short URL on the dashboard, which
20-
resolves the trace back to its app and redirects.
16+
autodiscovered by the toolbar package), so the `plain.toolbar` import
17+
above is safe without a guard. The item is a button-only toolbar entry —
18+
no panel — that points at the `/t/<trace_id>` short URL on the dashboard,
19+
which resolves the trace back to its app and redirects.
2120
"""
2221

2322
name = "Connect"
@@ -30,19 +29,11 @@ def is_enabled(self) -> bool:
3029
def get_template_context(self) -> dict[str, Any]:
3130
context = super().get_template_context()
3231

33-
span_context = trace.get_current_span().get_span_context()
34-
if not span_context.is_valid:
35-
context["sampled"] = None
36-
context["trace_url"] = ""
37-
return context
38-
39-
# The sampled flag reflects the final sampling decision, so a
40-
# sub-1.0 CONNECT_TRACE_SAMPLE_RATE is accounted for here for free.
41-
context["sampled"] = span_context.trace_flags.sampled
42-
if span_context.trace_flags.sampled:
32+
trace = current_trace()
33+
context["sampled"] = trace.sampled
34+
if trace.sampled:
4335
dashboard_url = str(settings.CONNECT_DASHBOARD_URL).rstrip("/")
44-
trace_id = format_trace_id(span_context.trace_id)
45-
context["trace_url"] = f"{dashboard_url}/t/{trace_id}"
36+
context["trace_url"] = f"{dashboard_url}/t/{trace.trace_id}"
4637
else:
4738
context["trace_url"] = ""
4839

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
from opentelemetry import trace
6+
from opentelemetry.trace import format_trace_id
7+
8+
9+
@dataclass(frozen=True)
10+
class CurrentTrace:
11+
"""The active request's OpenTelemetry trace, as connect needs it.
12+
13+
`trace_id` is the 32-char hex id, or "" when there is no valid trace —
14+
connect only installs an SDK tracer provider when export is configured,
15+
so without a token requests get no-op spans with an all-zero id.
16+
17+
`sampled` is the final sampling decision (so a sub-1.0
18+
CONNECT_TRACE_SAMPLE_RATE is accounted for here for free), or None when
19+
there is no valid trace.
20+
"""
21+
22+
trace_id: str
23+
sampled: bool | None
24+
25+
26+
def current_trace() -> CurrentTrace:
27+
"""Read the active span's trace id and sampling decision."""
28+
span_context = trace.get_current_span().get_span_context()
29+
if not span_context.is_valid:
30+
return CurrentTrace(trace_id="", sampled=None)
31+
return CurrentTrace(
32+
trace_id=format_trace_id(span_context.trace_id),
33+
sampled=span_context.trace_flags.sampled,
34+
)

0 commit comments

Comments
 (0)