telemetry-dev 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/PKG-INFO +4 -3
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/README.md +3 -2
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/pyproject.toml +1 -1
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/pyproject.toml.orig +1 -1
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_prompts.py +143 -27
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/LICENSE +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/__init__.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_capture.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_client.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_config.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_context.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_export.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_logs.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_metrics.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_observe.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_processor.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_semconv.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_serialize.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/_spans.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/otel.py +0 -0
- {telemetry_dev-0.3.0 → telemetry_dev-0.3.1}/src/telemetry_dev/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telemetry-dev
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: telemetry.dev SDK for Python — OpenTelemetry-native GenAI tracing, logs, and metrics
|
|
5
5
|
Keywords: telemetry,opentelemetry,llm,genai,tracing,observability
|
|
6
6
|
Author: telemetry.dev
|
|
@@ -276,8 +276,9 @@ ran in it, or before it started in the code that started it; with none, it recor
|
|
|
276
276
|
for code that has not compiled in its own task. Model calls are the spans of type `generation`
|
|
277
277
|
(operations `chat`, `text_completion`, and `generate_content`), including the spans of the provider
|
|
278
278
|
integrations: OpenAI, Anthropic, Amazon Bedrock, Google GenAI, LiteLLM, and OpenRouter. Spans from
|
|
279
|
-
other OpenTelemetry tracers do not get the attributes.
|
|
280
|
-
|
|
279
|
+
other OpenTelemetry tracers do not get the attributes. A model call outside `with_prompt` does not
|
|
280
|
+
get the attributes, even when it sends messages from `compile()`; with input capture on, the call
|
|
281
|
+
still records those messages as its input.
|
|
281
282
|
|
|
282
283
|
## Auto-metrics
|
|
283
284
|
|
|
@@ -251,8 +251,9 @@ ran in it, or before it started in the code that started it; with none, it recor
|
|
|
251
251
|
for code that has not compiled in its own task. Model calls are the spans of type `generation`
|
|
252
252
|
(operations `chat`, `text_completion`, and `generate_content`), including the spans of the provider
|
|
253
253
|
integrations: OpenAI, Anthropic, Amazon Bedrock, Google GenAI, LiteLLM, and OpenRouter. Spans from
|
|
254
|
-
other OpenTelemetry tracers do not get the attributes.
|
|
255
|
-
|
|
254
|
+
other OpenTelemetry tracers do not get the attributes. A model call outside `with_prompt` does not
|
|
255
|
+
get the attributes, even when it sends messages from `compile()`; with input capture on, the call
|
|
256
|
+
still records those messages as its input.
|
|
256
257
|
|
|
257
258
|
## Auto-metrics
|
|
258
259
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import copy
|
|
5
6
|
import hashlib
|
|
6
7
|
import json
|
|
7
8
|
import random
|
|
8
9
|
import re
|
|
10
|
+
import socket
|
|
9
11
|
import threading
|
|
10
12
|
import urllib.error
|
|
11
13
|
import urllib.parse
|
|
@@ -274,7 +276,8 @@ def prompt_attributes(
|
|
|
274
276
|
client: Client, context: Context | None, operation: object, capture_input: bool
|
|
275
277
|
) -> dict[str, AttributeValue]:
|
|
276
278
|
"""The td.prompt.* attributes for a span with this operation that starts in ``context``."""
|
|
277
|
-
|
|
279
|
+
# An attribute can be a list, which a frozenset cannot hold.
|
|
280
|
+
if not isinstance(operation, str) or operation not in _MODEL_CALL_OPERATIONS:
|
|
278
281
|
return {}
|
|
279
282
|
scope = otel_context.get_value(_PROMPT_KEY, context) or otel_context.get_value(_PROMPT_KEY)
|
|
280
283
|
if not isinstance(scope, _PromptScope):
|
|
@@ -467,10 +470,9 @@ def _load(
|
|
|
467
470
|
on_error: Callable[[BaseException], None] | None,
|
|
468
471
|
) -> dict[str, Any]:
|
|
469
472
|
"""Return the kept copy of a prompt, and fetch a new copy in a background thread when it is
|
|
470
|
-
older than the TTL. Only a call with no copy waits for a fetch, until the fetch's deadline
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
fetch."""
|
|
473
|
+
older than the TTL. Only a call with no copy waits for a fetch, until the fetch's deadline. A
|
|
474
|
+
failed fetch with no copy fails the calls of the next ``_ERROR_TTL_S`` (or the TTL, if it is
|
|
475
|
+
shorter) without a fetch."""
|
|
474
476
|
with _cache_lock:
|
|
475
477
|
entry = _cache.get(key)
|
|
476
478
|
if entry is None:
|
|
@@ -489,7 +491,7 @@ def _load(
|
|
|
489
491
|
return entry.response
|
|
490
492
|
pending, deadline = entry.pending, entry.deadline
|
|
491
493
|
if pending is not None and not pending.wait(deadline - monotonic()):
|
|
492
|
-
raise
|
|
494
|
+
raise _timeout()
|
|
493
495
|
with _cache_lock:
|
|
494
496
|
response, error = entry.response, entry.error
|
|
495
497
|
if response is not None:
|
|
@@ -548,31 +550,141 @@ class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
|
548
550
|
return None
|
|
549
551
|
|
|
550
552
|
|
|
551
|
-
|
|
553
|
+
class _Deadline:
|
|
554
|
+
"""Ends one prompt fetch after ``_FETCH_TIMEOUT_S``. The timeout of urllib applies to each
|
|
555
|
+
socket operation, so a server that sends a byte now and then could keep a fetch reading forever,
|
|
556
|
+
and each address of a host gets the full timeout to connect. A connection attempt gets only the
|
|
557
|
+
time that is left, and at the deadline this shuts the connection down, which ends a blocked
|
|
558
|
+
read. It cannot stop an address lookup; the system resolver bounds that."""
|
|
559
|
+
|
|
560
|
+
def __init__(self) -> None:
|
|
561
|
+
self.expired = False
|
|
562
|
+
self._end = monotonic() + _FETCH_TIMEOUT_S
|
|
563
|
+
self._lock = threading.Lock()
|
|
564
|
+
self._sockets: list[socket.socket] = []
|
|
565
|
+
self._timer = threading.Timer(_FETCH_TIMEOUT_S, self._expire)
|
|
566
|
+
self._timer.daemon = True
|
|
567
|
+
self._timer.start()
|
|
568
|
+
|
|
569
|
+
def connect(
|
|
570
|
+
self,
|
|
571
|
+
address: tuple[str, int],
|
|
572
|
+
timeout: float | None,
|
|
573
|
+
source_address: tuple[str, int] | None = None,
|
|
574
|
+
) -> socket.socket:
|
|
575
|
+
"""``socket.create_connection``, with each attempt bounded by the deadline."""
|
|
576
|
+
host, port = address
|
|
577
|
+
error: OSError = OSError(f"no address for {host}")
|
|
578
|
+
for family, kind, proto, _, target in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
|
579
|
+
left = self._end - monotonic()
|
|
580
|
+
if left <= 0:
|
|
581
|
+
break
|
|
582
|
+
sock = socket.socket(family, kind, proto)
|
|
583
|
+
try:
|
|
584
|
+
sock.settimeout(left if timeout is None else min(left, timeout))
|
|
585
|
+
if source_address is not None:
|
|
586
|
+
sock.bind(source_address)
|
|
587
|
+
sock.connect(target)
|
|
588
|
+
sock.settimeout(timeout)
|
|
589
|
+
except OSError as exc:
|
|
590
|
+
sock.close()
|
|
591
|
+
error = exc
|
|
592
|
+
continue
|
|
593
|
+
with self._lock:
|
|
594
|
+
if not self.expired:
|
|
595
|
+
try:
|
|
596
|
+
# A TLS wrap takes the file descriptor of sock, so keep a copy to shut down.
|
|
597
|
+
self._sockets.append(sock.dup())
|
|
598
|
+
except OSError:
|
|
599
|
+
sock.close()
|
|
600
|
+
raise
|
|
601
|
+
return sock
|
|
602
|
+
sock.close()
|
|
603
|
+
break
|
|
604
|
+
if self.expired or self._end <= monotonic():
|
|
605
|
+
raise TimeoutError("the prompt fetch passed its deadline")
|
|
606
|
+
raise error
|
|
607
|
+
|
|
608
|
+
def _expire(self) -> None:
|
|
609
|
+
with self._lock:
|
|
610
|
+
self.expired = True
|
|
611
|
+
for sock in self._sockets:
|
|
612
|
+
with contextlib.suppress(OSError):
|
|
613
|
+
sock.shutdown(socket.SHUT_RDWR)
|
|
614
|
+
|
|
615
|
+
def close(self) -> None:
|
|
616
|
+
self._timer.cancel()
|
|
617
|
+
with self._lock:
|
|
618
|
+
for sock in self._sockets:
|
|
619
|
+
sock.close()
|
|
620
|
+
self._sockets.clear()
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class _Request(urllib.request.Request):
|
|
624
|
+
def __init__(self, url: str, headers: dict[str, str], deadline: _Deadline) -> None:
|
|
625
|
+
super().__init__(url, headers=headers, method="GET")
|
|
626
|
+
self.deadline = deadline
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _on_deadline(http_class: Any, request: urllib.request.Request) -> Any:
|
|
630
|
+
"""``http_class``, with the sockets of a ``_Request`` under its deadline."""
|
|
631
|
+
|
|
632
|
+
def connection(host: str, **kwargs: Any) -> Any:
|
|
633
|
+
conn = http_class(host, **kwargs)
|
|
634
|
+
if isinstance(request, _Request):
|
|
635
|
+
conn._create_connection = request.deadline.connect
|
|
636
|
+
return conn
|
|
637
|
+
|
|
638
|
+
return connection
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class _HTTPHandler(urllib.request.HTTPHandler):
|
|
642
|
+
def do_open(self, http_class: Any, req: urllib.request.Request, **http_conn_args: Any) -> Any:
|
|
643
|
+
return super().do_open(_on_deadline(http_class, req), req, **http_conn_args)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class _HTTPSHandler(urllib.request.HTTPSHandler):
|
|
647
|
+
def do_open(self, http_class: Any, req: urllib.request.Request, **http_conn_args: Any) -> Any:
|
|
648
|
+
return super().do_open(_on_deadline(http_class, req), req, **http_conn_args)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
_opener = urllib.request.build_opener(_NoRedirect, _HTTPHandler, _HTTPSHandler)
|
|
552
652
|
|
|
553
653
|
|
|
554
654
|
def _fetch(url: str, headers: dict[str, str]) -> dict[str, Any]:
|
|
555
|
-
|
|
655
|
+
deadline = _Deadline()
|
|
656
|
+
retry_after: str | None = None
|
|
556
657
|
try:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
658
|
+
try:
|
|
659
|
+
with _opener.open(
|
|
660
|
+
_Request(url, headers, deadline), timeout=_FETCH_TIMEOUT_S
|
|
661
|
+
) as response:
|
|
662
|
+
status: int = response.status
|
|
663
|
+
body: bytes = response.read()
|
|
664
|
+
except urllib.error.HTTPError as exc:
|
|
665
|
+
with exc:
|
|
666
|
+
status, body = exc.code, exc.read()
|
|
667
|
+
retry_after = exc.headers.get("Retry-After")
|
|
668
|
+
except Exception as exc:
|
|
669
|
+
if (
|
|
670
|
+
deadline.expired
|
|
671
|
+
or isinstance(exc, TimeoutError)
|
|
672
|
+
or isinstance(getattr(exc, "reason", None), TimeoutError)
|
|
673
|
+
):
|
|
674
|
+
raise _timeout() from exc
|
|
675
|
+
if isinstance(exc, (urllib.error.URLError, OSError)):
|
|
676
|
+
raise PromptError("network_error", "Could not reach telemetry.dev") from exc
|
|
677
|
+
raise
|
|
678
|
+
finally:
|
|
679
|
+
deadline.close()
|
|
680
|
+
# A read that the deadline cut short can end without an error, with part of the body.
|
|
681
|
+
if deadline.expired:
|
|
682
|
+
raise _timeout()
|
|
683
|
+
if not 200 <= status < 300:
|
|
684
|
+
error = _api_error(status, body)
|
|
685
|
+
if status in (429, 503):
|
|
686
|
+
error._retry_after = _retry_after(retry_after) # pyright: ignore[reportPrivateUsage]
|
|
687
|
+
raise error
|
|
576
688
|
try:
|
|
577
689
|
parsed = _parse_response(json.loads(body))
|
|
578
690
|
except ValueError:
|
|
@@ -582,6 +694,10 @@ def _fetch(url: str, headers: dict[str, str]) -> dict[str, Any]:
|
|
|
582
694
|
return parsed
|
|
583
695
|
|
|
584
696
|
|
|
697
|
+
def _timeout() -> PromptError:
|
|
698
|
+
return PromptError("timeout", f"Fetching the prompt took more than {_FETCH_TIMEOUT_S:g} s")
|
|
699
|
+
|
|
700
|
+
|
|
585
701
|
def _api_error(status: int, body: bytes) -> PromptError:
|
|
586
702
|
try:
|
|
587
703
|
error = json.loads(body).get("error")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|