Skip to content

Commit fed7bd4

Browse files
author
Chojan Shang
committed
feat: intro telemetry
Signed-off-by: Chojan Shang <chojan.shang@vesoft.com>
1 parent 0da09a3 commit fed7bd4

File tree

4 files changed

+308
-28
lines changed

4 files changed

+308
-28
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ dev = [
4343
"python-dotenv>=1.1.1",
4444
]
4545

46+
[project.optional-dependencies]
47+
logfire = ["logfire>=0.14"]
48+
4649
[build-system]
4750
requires = ["hatchling"]
4851
build-backend = "hatchling.build"

src/acp/connection.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
SenderFactory,
2626
TaskSupervisor,
2727
)
28+
from .telemetry import span_context
2829

2930
JsonValue = Any
3031
MethodHandler = Callable[[str, JsonValue | None, bool], Awaitable[JsonValue | None]]
@@ -135,35 +136,41 @@ async def _process_message(self, message: dict[str, Any]) -> None:
135136

136137
async def _run_request(self, message: dict[str, Any]) -> Any:
137138
payload: dict[str, Any] = {"jsonrpc": "2.0", "id": message["id"]}
138-
try:
139-
result = await self._handler(message["method"], message.get("params"), False)
140-
if isinstance(result, BaseModel):
141-
result = result.model_dump()
142-
payload["result"] = result if result is not None else None
143-
await self._sender.send(payload)
144-
return payload.get("result")
145-
except RequestError as exc:
146-
payload["error"] = exc.to_error_obj()
147-
await self._sender.send(payload)
148-
raise
149-
except ValidationError as exc:
150-
err = RequestError.invalid_params({"errors": exc.errors()})
151-
payload["error"] = err.to_error_obj()
152-
await self._sender.send(payload)
153-
raise err from None
154-
except Exception as exc:
139+
method = message["method"]
140+
with span_context(
141+
"acp.request",
142+
attributes={"method": method},
143+
):
155144
try:
156-
data = json.loads(str(exc))
157-
except Exception:
158-
data = {"details": str(exc)}
159-
err = RequestError.internal_error(data)
160-
payload["error"] = err.to_error_obj()
161-
await self._sender.send(payload)
162-
raise err from None
145+
result = await self._handler(method, message.get("params"), False)
146+
if isinstance(result, BaseModel):
147+
result = result.model_dump()
148+
payload["result"] = result if result is not None else None
149+
await self._sender.send(payload)
150+
return payload.get("result")
151+
except RequestError as exc:
152+
payload["error"] = exc.to_error_obj()
153+
await self._sender.send(payload)
154+
raise
155+
except ValidationError as exc:
156+
err = RequestError.invalid_params({"errors": exc.errors()})
157+
payload["error"] = err.to_error_obj()
158+
await self._sender.send(payload)
159+
raise err from None
160+
except Exception as exc:
161+
try:
162+
data = json.loads(str(exc))
163+
except Exception:
164+
data = {"details": str(exc)}
165+
err = RequestError.internal_error(data)
166+
payload["error"] = err.to_error_obj()
167+
await self._sender.send(payload)
168+
raise err from None
163169

164170
async def _run_notification(self, message: dict[str, Any]) -> None:
165-
with contextlib.suppress(Exception):
166-
await self._handler(message["method"], message.get("params"), True)
171+
method = message["method"]
172+
with span_context("acp.notification", attributes={"method": method}), contextlib.suppress(Exception):
173+
await self._handler(method, message.get("params"), True)
167174

168175
async def _handle_response(self, message: dict[str, Any]) -> None:
169176
request_id = message["id"]

src/acp/telemetry.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from collections.abc import Mapping
5+
from contextlib import AbstractContextManager, ExitStack, nullcontext
6+
from typing import Any
7+
8+
try:
9+
from logfire import span as logfire_span
10+
except Exception: # pragma: no cover - logfire is optional
11+
logfire_span = None # type: ignore[assignment]
12+
else: # pragma: no cover - optional
13+
os.environ.setdefault("LOGFIRE_IGNORE_NO_CONFIG", "1")
14+
15+
try: # pragma: no cover - opentelemetry is optional
16+
from opentelemetry.trace import get_tracer as otel_get_tracer
17+
except Exception: # pragma: no cover - opentelemetry is optional
18+
otel_get_tracer = None # type: ignore[assignment]
19+
20+
DEFAULT_TAGS = ["acp"]
21+
TRACER = otel_get_tracer(__name__) if otel_get_tracer else None
22+
23+
24+
def _start_tracer_span(name: str, *, attributes: Mapping[str, Any] | None = None) -> AbstractContextManager[Any]:
25+
if TRACER is None:
26+
return nullcontext()
27+
attrs = dict(attributes or {})
28+
return TRACER.start_as_current_span(name, attributes=attrs)
29+
30+
31+
def span_context(name: str, *, attributes: Mapping[str, Any] | None = None) -> AbstractContextManager[None]:
32+
if logfire_span is None and TRACER is None:
33+
return nullcontext()
34+
stack = ExitStack()
35+
attrs: dict[str, Any] = {"logfire.tags": DEFAULT_TAGS}
36+
if attributes:
37+
attrs.update(attributes)
38+
if logfire_span is not None:
39+
stack.enter_context(logfire_span(name, attributes=attrs))
40+
stack.enter_context(_start_tracer_span(name, attributes=attributes))
41+
return stack

0 commit comments

Comments
 (0)