|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -import base64 |
4 | 3 | import os |
5 | | -from typing import TYPE_CHECKING, Any |
| 4 | +from typing import TYPE_CHECKING |
6 | 5 |
|
7 | 6 | import httpx # noqa: TC002 |
8 | 7 | from any_llm_platform_client import ( |
9 | 8 | AnyLLMPlatformClient, # noqa: TC002 |
10 | 9 | ) |
| 10 | +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter |
| 11 | +from opentelemetry.sdk.resources import Resource |
| 12 | +from opentelemetry.sdk.trace import TracerProvider |
| 13 | +from opentelemetry.trace import SpanKind |
| 14 | +from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExporter |
11 | 15 |
|
12 | 16 | from any_llm import __version__ |
13 | 17 |
|
|
23 | 27 | ANY_LLM_PLATFORM_TRACE_URL = f"{_trace_base_url}{TRACE_API_PATH}" |
24 | 28 |
|
25 | 29 |
|
26 | | -def _generate_trace_ids() -> tuple[str, str]: |
27 | | - trace_id = base64.b64encode(os.urandom(16)).decode("ascii") |
28 | | - span_id = base64.b64encode(os.urandom(8)).decode("ascii") |
29 | | - return trace_id, span_id |
30 | | - |
31 | | - |
32 | | -def _attribute_value(value: Any) -> dict[str, Any]: |
33 | | - if isinstance(value, bool): |
34 | | - return {"boolValue": value} |
35 | | - if isinstance(value, int): |
36 | | - return {"intValue": str(value)} |
37 | | - if isinstance(value, float): |
38 | | - return {"doubleValue": value} |
39 | | - return {"stringValue": str(value)} |
| 30 | +def _build_span_exporter(access_token: str) -> SpanExporter: |
| 31 | + return OTLPSpanExporter( |
| 32 | + endpoint=ANY_LLM_PLATFORM_TRACE_URL, |
| 33 | + headers={ |
| 34 | + "Authorization": f"Bearer {access_token}", |
| 35 | + "User-Agent": f"python-any-llm/{__version__}", |
| 36 | + }, |
| 37 | + ) |
40 | 38 |
|
41 | 39 |
|
42 | | -def _append_attribute(attributes: list[dict[str, Any]], key: str, value: Any) -> None: |
43 | | - if value is None: |
44 | | - return |
45 | | - attributes.append({"key": key, "value": _attribute_value(value)}) |
| 40 | +def _build_tracer_provider(access_token: str) -> TracerProvider: |
| 41 | + provider = TracerProvider(resource=Resource.create({"service.name": "any-llm"})) |
| 42 | + provider.add_span_processor(SimpleSpanProcessor(_build_span_exporter(access_token))) |
| 43 | + return provider |
46 | 44 |
|
47 | 45 |
|
48 | 46 | async def export_completion_trace( |
@@ -72,70 +70,46 @@ async def export_completion_trace( |
72 | 70 | """ |
73 | 71 | access_token = await platform_client._aensure_valid_token(any_llm_key) |
74 | 72 |
|
75 | | - attributes: list[dict[str, Any]] = [] |
76 | | - _append_attribute(attributes, "gen_ai.provider.name", provider) |
77 | | - _append_attribute(attributes, "gen_ai.request.model", request_model) |
| 73 | + provider_instance = _build_tracer_provider(access_token) |
| 74 | + tracer = provider_instance.get_tracer("any-llm", __version__) |
| 75 | + |
| 76 | + span = tracer.start_span("llm.request", kind=SpanKind.CLIENT, start_time=start_time_ns) |
| 77 | + |
| 78 | + span.set_attribute("gen_ai.provider.name", provider) |
| 79 | + span.set_attribute("gen_ai.request.model", request_model) |
78 | 80 |
|
79 | 81 | if completion is not None: |
80 | | - _append_attribute(attributes, "gen_ai.response.model", completion.model) |
| 82 | + span.set_attribute("gen_ai.response.model", completion.model) |
81 | 83 | usage = completion.usage |
82 | 84 | if usage is not None: |
83 | | - _append_attribute(attributes, "gen_ai.usage.input_tokens", usage.prompt_tokens) |
84 | | - _append_attribute(attributes, "gen_ai.usage.output_tokens", usage.completion_tokens) |
85 | | - |
86 | | - _append_attribute(attributes, "gen_ai.conversation.id", conversation_id) |
87 | | - _append_attribute(attributes, "anyllm.client_name", client_name) |
88 | | - _append_attribute(attributes, "anyllm.session_label", session_label) |
89 | | - |
90 | | - _append_attribute(attributes, "anyllm.performance.time_to_first_token_ms", time_to_first_token_ms) |
91 | | - _append_attribute(attributes, "anyllm.performance.time_to_last_token_ms", time_to_last_token_ms) |
92 | | - _append_attribute(attributes, "anyllm.performance.total_duration_ms", total_duration_ms) |
93 | | - _append_attribute(attributes, "anyllm.performance.tokens_per_second", tokens_per_second) |
94 | | - _append_attribute(attributes, "anyllm.performance.chunks_received", chunks_received) |
95 | | - _append_attribute(attributes, "anyllm.performance.avg_chunk_size", avg_chunk_size) |
96 | | - _append_attribute( |
97 | | - attributes, |
98 | | - "anyllm.performance.inter_chunk_latency_variance_ms", |
99 | | - inter_chunk_latency_variance_ms, |
100 | | - ) |
101 | | - |
102 | | - trace_id, span_id = _generate_trace_ids() |
103 | | - |
104 | | - payload = { |
105 | | - "resourceSpans": [ |
106 | | - { |
107 | | - "resource": { |
108 | | - "attributes": [ |
109 | | - {"key": "service.name", "value": {"stringValue": "any-llm"}}, |
110 | | - ] |
111 | | - }, |
112 | | - "scopeSpans": [ |
113 | | - { |
114 | | - "scope": {"name": "any-llm", "version": __version__}, |
115 | | - "spans": [ |
116 | | - { |
117 | | - "traceId": trace_id, |
118 | | - "spanId": span_id, |
119 | | - "name": "llm.request", |
120 | | - "kind": "SPAN_KIND_CLIENT", |
121 | | - "startTimeUnixNano": str(start_time_ns), |
122 | | - "endTimeUnixNano": str(end_time_ns), |
123 | | - "attributes": attributes, |
124 | | - } |
125 | | - ], |
126 | | - } |
127 | | - ], |
128 | | - } |
129 | | - ] |
130 | | - } |
131 | | - |
132 | | - response = await client.post( |
133 | | - ANY_LLM_PLATFORM_TRACE_URL, |
134 | | - json=payload, |
135 | | - headers={ |
136 | | - "Authorization": f"Bearer {access_token}", |
137 | | - "User-Agent": f"python-any-llm/{__version__}", |
138 | | - "Content-Type": "application/json", |
139 | | - }, |
140 | | - ) |
141 | | - response.raise_for_status() |
| 85 | + span.set_attribute("gen_ai.usage.input_tokens", usage.prompt_tokens) |
| 86 | + span.set_attribute("gen_ai.usage.output_tokens", usage.completion_tokens) |
| 87 | + |
| 88 | + if conversation_id is not None: |
| 89 | + span.set_attribute("gen_ai.conversation.id", conversation_id) |
| 90 | + if client_name is not None: |
| 91 | + span.set_attribute("anyllm.client_name", client_name) |
| 92 | + if session_label is not None: |
| 93 | + span.set_attribute("anyllm.session_label", session_label) |
| 94 | + |
| 95 | + if time_to_first_token_ms is not None: |
| 96 | + span.set_attribute("anyllm.performance.time_to_first_token_ms", time_to_first_token_ms) |
| 97 | + if time_to_last_token_ms is not None: |
| 98 | + span.set_attribute("anyllm.performance.time_to_last_token_ms", time_to_last_token_ms) |
| 99 | + if total_duration_ms is not None: |
| 100 | + span.set_attribute("anyllm.performance.total_duration_ms", total_duration_ms) |
| 101 | + if tokens_per_second is not None: |
| 102 | + span.set_attribute("anyllm.performance.tokens_per_second", tokens_per_second) |
| 103 | + if chunks_received is not None: |
| 104 | + span.set_attribute("anyllm.performance.chunks_received", chunks_received) |
| 105 | + if avg_chunk_size is not None: |
| 106 | + span.set_attribute("anyllm.performance.avg_chunk_size", avg_chunk_size) |
| 107 | + if inter_chunk_latency_variance_ms is not None: |
| 108 | + span.set_attribute( |
| 109 | + "anyllm.performance.inter_chunk_latency_variance_ms", |
| 110 | + inter_chunk_latency_variance_ms, |
| 111 | + ) |
| 112 | + |
| 113 | + span.end(end_time=end_time_ns) |
| 114 | + provider_instance.force_flush() |
| 115 | + provider_instance.shutdown() |
0 commit comments