Skip to content

Commit 0d749a9

Browse files
committed
move span utils to new file
1 parent 73a764c commit 0d749a9

File tree

2 files changed

+146
-111
lines changed

2 files changed

+146
-111
lines changed

util/opentelemetry-util-genai/src/opentelemetry/util/genai/generators.py

Lines changed: 7 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -31,35 +31,28 @@
3131
follow the GenAI semantic conventions.
3232
"""
3333

34-
import json
3534
from contextlib import contextmanager
36-
from dataclasses import asdict, dataclass, field
37-
from typing import Any, Dict, List, Optional
35+
from dataclasses import dataclass, field
36+
from typing import Dict, List, Optional
3837
from uuid import UUID
3938

4039
from opentelemetry import trace
4140
from opentelemetry.semconv._incubating.attributes import (
4241
gen_ai_attributes as GenAI,
4342
)
44-
from opentelemetry.semconv.attributes import (
45-
error_attributes as ErrorAttributes,
46-
)
4743
from opentelemetry.trace import (
4844
Span,
4945
SpanKind,
5046
Tracer,
5147
set_span_in_context,
5248
use_span,
5349
)
54-
from opentelemetry.trace.status import Status, StatusCode
55-
from opentelemetry.util.genai.utils import (
56-
ContentCapturingMode,
57-
get_content_capturing_mode,
58-
is_experimental_mode,
59-
)
60-
from opentelemetry.util.types import AttributeValue
6150

62-
from .types import Error, InputMessage, LLMInvocation, OutputMessage
51+
from .span_utils import (
52+
_apply_error_attributes,
53+
_apply_finish_attributes,
54+
)
55+
from .types import Error, LLMInvocation
6356

6457

6558
@dataclass
@@ -68,103 +61,6 @@ class _SpanState:
6861
children: List[UUID] = field(default_factory=list)
6962

7063

71-
def _apply_common_span_attributes(
72-
span: Span, invocation: LLMInvocation
73-
) -> None:
74-
"""Apply attributes shared by finish() and error() and compute metrics.
75-
76-
Returns (genai_attributes) for use with metrics.
77-
"""
78-
request_model = invocation.attributes.get("request_model")
79-
provider = invocation.attributes.get("provider")
80-
81-
span.set_attribute(
82-
GenAI.GEN_AI_OPERATION_NAME, GenAI.GenAiOperationNameValues.CHAT.value
83-
)
84-
if request_model:
85-
span.set_attribute(GenAI.GEN_AI_REQUEST_MODEL, request_model)
86-
if provider is not None:
87-
# TODO: clean provider name to match GenAiProviderNameValues?
88-
span.set_attribute(GenAI.GEN_AI_PROVIDER_NAME, provider)
89-
90-
finish_reasons: List[str] = []
91-
for gen in invocation.chat_generations:
92-
finish_reasons.append(gen.finish_reason)
93-
if finish_reasons:
94-
span.set_attribute(
95-
GenAI.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons
96-
)
97-
98-
response_model = invocation.attributes.get("response_model_name")
99-
response_id = invocation.attributes.get("response_id")
100-
prompt_tokens = invocation.attributes.get("input_tokens")
101-
completion_tokens = invocation.attributes.get("output_tokens")
102-
_set_response_and_usage_attributes(
103-
span,
104-
response_model,
105-
response_id,
106-
prompt_tokens,
107-
completion_tokens,
108-
)
109-
110-
111-
def _set_response_and_usage_attributes(
112-
span: Span,
113-
response_model: Optional[str],
114-
response_id: Optional[str],
115-
prompt_tokens: Optional[AttributeValue],
116-
completion_tokens: Optional[AttributeValue],
117-
) -> None:
118-
if response_model is not None:
119-
span.set_attribute(GenAI.GEN_AI_RESPONSE_MODEL, response_model)
120-
if response_id is not None:
121-
span.set_attribute(GenAI.GEN_AI_RESPONSE_ID, response_id)
122-
if isinstance(prompt_tokens, (int, float)):
123-
span.set_attribute(GenAI.GEN_AI_USAGE_INPUT_TOKENS, prompt_tokens)
124-
if isinstance(completion_tokens, (int, float)):
125-
span.set_attribute(GenAI.GEN_AI_USAGE_OUTPUT_TOKENS, completion_tokens)
126-
127-
128-
def _maybe_set_span_messages(
129-
span: Span,
130-
input_messages: List[InputMessage],
131-
output_messages: List[OutputMessage],
132-
) -> None:
133-
if not is_experimental_mode() or get_content_capturing_mode() not in (
134-
ContentCapturingMode.SPAN_ONLY,
135-
ContentCapturingMode.SPAN_AND_EVENT,
136-
):
137-
return
138-
message_parts: List[Dict[str, Any]] = [
139-
asdict(message) for message in input_messages
140-
]
141-
if message_parts:
142-
span.set_attribute("gen_ai.input.messages", json.dumps(message_parts))
143-
144-
generation_parts: List[Dict[str, Any]] = [
145-
asdict(generation) for generation in output_messages
146-
]
147-
if generation_parts:
148-
span.set_attribute(
149-
"gen_ai.output.messages", json.dumps(generation_parts)
150-
)
151-
152-
153-
def _apply_finish_attributes(span: Span, invocation: LLMInvocation) -> None:
154-
"""Apply attributes/messages common to finish() paths."""
155-
_apply_common_span_attributes(span, invocation)
156-
_maybe_set_span_messages(
157-
span, invocation.messages, invocation.chat_generations
158-
)
159-
160-
161-
def _apply_error_attributes(span: Span, error: Error) -> None:
162-
"""Apply status and error attributes common to error() paths."""
163-
span.set_status(Status(StatusCode.ERROR, error.message))
164-
if span.is_recording():
165-
span.set_attribute(ErrorAttributes.ERROR_TYPE, error.type.__qualname__)
166-
167-
16864
class BaseTelemetryGenerator:
16965
"""
17066
Abstract base for emitters mapping GenAI types -> OpenTelemetry.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
from dataclasses import asdict
17+
from typing import Any, Dict, List, Optional
18+
19+
from opentelemetry.semconv._incubating.attributes import (
20+
gen_ai_attributes as GenAI,
21+
)
22+
from opentelemetry.semconv.attributes import (
23+
error_attributes as ErrorAttributes,
24+
)
25+
from opentelemetry.trace import (
26+
Span,
27+
)
28+
from opentelemetry.trace.status import Status, StatusCode
29+
from opentelemetry.util.genai.utils import (
30+
ContentCapturingMode,
31+
get_content_capturing_mode,
32+
is_experimental_mode,
33+
)
34+
from opentelemetry.util.types import AttributeValue
35+
36+
from .types import Error, InputMessage, LLMInvocation, OutputMessage
37+
38+
39+
def _apply_common_span_attributes(
40+
span: Span, invocation: LLMInvocation
41+
) -> None:
42+
"""Apply attributes shared by finish() and error() and compute metrics.
43+
44+
Returns (genai_attributes) for use with metrics.
45+
"""
46+
request_model = invocation.attributes.get("request_model")
47+
provider = invocation.attributes.get("provider")
48+
49+
span.set_attribute(
50+
GenAI.GEN_AI_OPERATION_NAME, GenAI.GenAiOperationNameValues.CHAT.value
51+
)
52+
if request_model:
53+
span.set_attribute(GenAI.GEN_AI_REQUEST_MODEL, request_model)
54+
if provider is not None:
55+
# TODO: clean provider name to match GenAiProviderNameValues?
56+
span.set_attribute(GenAI.GEN_AI_PROVIDER_NAME, provider)
57+
58+
finish_reasons: List[str] = []
59+
for gen in invocation.chat_generations:
60+
finish_reasons.append(gen.finish_reason)
61+
if finish_reasons:
62+
span.set_attribute(
63+
GenAI.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons
64+
)
65+
66+
response_model = invocation.attributes.get("response_model_name")
67+
response_id = invocation.attributes.get("response_id")
68+
prompt_tokens = invocation.attributes.get("input_tokens")
69+
completion_tokens = invocation.attributes.get("output_tokens")
70+
_set_response_and_usage_attributes(
71+
span,
72+
response_model,
73+
response_id,
74+
prompt_tokens,
75+
completion_tokens,
76+
)
77+
78+
79+
def _set_response_and_usage_attributes(
80+
span: Span,
81+
response_model: Optional[str],
82+
response_id: Optional[str],
83+
prompt_tokens: Optional[AttributeValue],
84+
completion_tokens: Optional[AttributeValue],
85+
) -> None:
86+
if response_model is not None:
87+
span.set_attribute(GenAI.GEN_AI_RESPONSE_MODEL, response_model)
88+
if response_id is not None:
89+
span.set_attribute(GenAI.GEN_AI_RESPONSE_ID, response_id)
90+
if isinstance(prompt_tokens, (int, float)):
91+
span.set_attribute(GenAI.GEN_AI_USAGE_INPUT_TOKENS, prompt_tokens)
92+
if isinstance(completion_tokens, (int, float)):
93+
span.set_attribute(GenAI.GEN_AI_USAGE_OUTPUT_TOKENS, completion_tokens)
94+
95+
96+
def _maybe_set_span_messages(
97+
span: Span,
98+
input_messages: List[InputMessage],
99+
output_messages: List[OutputMessage],
100+
) -> None:
101+
if not is_experimental_mode() or get_content_capturing_mode() not in (
102+
ContentCapturingMode.SPAN_ONLY,
103+
ContentCapturingMode.SPAN_AND_EVENT,
104+
):
105+
return
106+
message_parts: List[Dict[str, Any]] = [
107+
asdict(message) for message in input_messages
108+
]
109+
if message_parts:
110+
span.set_attribute("gen_ai.input.messages", json.dumps(message_parts))
111+
112+
generation_parts: List[Dict[str, Any]] = [
113+
asdict(generation) for generation in output_messages
114+
]
115+
if generation_parts:
116+
span.set_attribute(
117+
"gen_ai.output.messages", json.dumps(generation_parts)
118+
)
119+
120+
121+
def _apply_finish_attributes(span: Span, invocation: LLMInvocation) -> None:
122+
"""Apply attributes/messages common to finish() paths."""
123+
_apply_common_span_attributes(span, invocation)
124+
_maybe_set_span_messages(
125+
span, invocation.messages, invocation.chat_generations
126+
)
127+
128+
129+
def _apply_error_attributes(span: Span, error: Error) -> None:
130+
"""Apply status and error attributes common to error() paths."""
131+
span.set_status(Status(StatusCode.ERROR, error.message))
132+
if span.is_recording():
133+
span.set_attribute(ErrorAttributes.ERROR_TYPE, error.type.__qualname__)
134+
135+
136+
__all__ = [
137+
"_apply_finish_attributes",
138+
"_apply_error_attributes",
139+
]

0 commit comments

Comments
 (0)