Skip to content

Commit 9d37e72

Browse files
committed
py: Add otel module
1 parent 655bb8c commit 9d37e72

File tree

1 file changed

+132
-0
lines changed

1 file changed

+132
-0
lines changed

src/libtmux/otel.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""OpenTelemetry helpers for libtmux (optional)."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
from contextlib import ExitStack
7+
from typing import Any
8+
9+
from ._internal import trace as libtmux_trace
10+
11+
_initialized = False
12+
_tracer = None
13+
_provider = None
14+
15+
DEFAULT_OTLP_ENDPOINT = "http://localhost:4318"
16+
17+
18+
def _env_flag(name: str) -> bool:
19+
raw = os.getenv(name)
20+
if raw is None:
21+
return False
22+
value = raw.strip().lower()
23+
return value in {"1", "true"}
24+
25+
26+
def _otel_enabled() -> bool:
27+
if _env_flag("VIBE_TMUX_OTEL") or _env_flag("LIBTMUX_OTEL"):
28+
return True
29+
return bool(
30+
os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
31+
or os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
32+
)
33+
34+
35+
def _normalize_endpoint(endpoint: str) -> str:
36+
if endpoint.endswith("/v1/traces"):
37+
return endpoint
38+
if endpoint.endswith("/"):
39+
return f"{endpoint}v1/traces"
40+
return f"{endpoint}/v1/traces"
41+
42+
43+
def init_otel() -> None:
44+
global _initialized, _tracer, _provider
45+
if _initialized:
46+
return
47+
_initialized = True
48+
49+
if not _otel_enabled():
50+
return
51+
52+
try:
53+
from opentelemetry import propagate, trace
54+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
55+
OTLPSpanExporter,
56+
)
57+
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_NAMESPACE
58+
from opentelemetry.sdk.trace import TracerProvider
59+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
60+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
61+
from opentelemetry.trace.propagation.tracecontext import (
62+
TraceContextTextMapPropagator,
63+
)
64+
from libtmux.__about__ import __version__
65+
except Exception:
66+
return
67+
68+
endpoint = os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or os.getenv(
69+
"OTEL_EXPORTER_OTLP_ENDPOINT"
70+
)
71+
if endpoint is None and (_env_flag("VIBE_TMUX_OTEL") or _env_flag("LIBTMUX_OTEL")):
72+
endpoint = DEFAULT_OTLP_ENDPOINT
73+
os.environ.setdefault("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint)
74+
if _env_flag("VIBE_TMUX_OTEL") or _env_flag("LIBTMUX_OTEL"):
75+
os.environ.setdefault("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf")
76+
77+
exporter = (
78+
OTLPSpanExporter(endpoint=_normalize_endpoint(endpoint))
79+
if endpoint
80+
else OTLPSpanExporter()
81+
)
82+
83+
resource = Resource.create(
84+
{
85+
SERVICE_NAME: "libtmux",
86+
SERVICE_NAMESPACE: "vibe-tmux",
87+
"service.version": __version__,
88+
}
89+
)
90+
91+
_provider = TracerProvider(resource=resource)
92+
use_batch = _env_flag("VIBE_TMUX_OTEL_BATCH") or _env_flag("LIBTMUX_OTEL_BATCH")
93+
use_sync = _env_flag("VIBE_TMUX_OTEL_SYNC") or _env_flag("LIBTMUX_OTEL_SYNC")
94+
if use_batch or (not use_sync and not _env_flag("VIBE_TMUX_OTEL")):
95+
_provider.add_span_processor(BatchSpanProcessor(exporter))
96+
else:
97+
_provider.add_span_processor(SimpleSpanProcessor(exporter))
98+
trace.set_tracer_provider(_provider)
99+
propagate.set_global_textmap(TraceContextTextMapPropagator())
100+
_tracer = trace.get_tracer("libtmux")
101+
if _provider is not None:
102+
import atexit
103+
104+
atexit.register(_provider.shutdown)
105+
106+
107+
108+
def _normalize_attr(value: Any):
109+
if value is None:
110+
return None
111+
if isinstance(value, (str, bytes, bool, int, float)):
112+
return value
113+
if isinstance(value, (list, tuple)):
114+
items = []
115+
for item in value:
116+
normalized = _normalize_attr(item)
117+
if normalized is None:
118+
continue
119+
items.append(normalized)
120+
return items
121+
return str(value)
122+
123+
def start_span(name: str, attributes: dict[str, Any] | None = None, **fields: Any):
124+
init_otel()
125+
stack = ExitStack()
126+
stack.enter_context(libtmux_trace.span(name, **fields))
127+
if _tracer is None:
128+
return stack
129+
otel_attrs = attributes or fields
130+
filtered = {k: _normalize_attr(v) for k, v in otel_attrs.items() if _normalize_attr(v) is not None}
131+
stack.enter_context(_tracer.start_as_current_span(name, attributes=filtered))
132+
return stack

0 commit comments

Comments
 (0)