1717from dataclasses import asdict
1818from typing import Any
1919
20+ from opentelemetry ._logs import Logger , LogRecord
2021from opentelemetry .semconv ._incubating .attributes import (
2122 gen_ai_attributes as GenAI ,
2223)
3132 Error ,
3233 InputMessage ,
3334 LLMInvocation ,
35+ MessagePart ,
3436 OutputMessage ,
3537)
3638from opentelemetry .util .genai .utils import (
4143)
4244
4345
44- def _apply_common_span_attributes (
45- span : Span , invocation : LLMInvocation
46- ) -> None :
47- """Apply attributes shared by finish() and error() and compute metrics .
46+ def _get_llm_common_attributes (
47+ invocation : LLMInvocation ,
48+ ) -> dict [ str , Any ] :
49+ """Get common LLM attributes shared by finish() and error() paths .
4850
49- Returns (genai_attributes) for use with metrics .
51+ Returns a dictionary of attributes .
5052 """
51- span .update_name (
52- f"{ GenAI .GenAiOperationNameValues .CHAT .value } { invocation .request_model } " .strip ()
53- )
54- span .set_attribute (
55- GenAI .GEN_AI_OPERATION_NAME , GenAI .GenAiOperationNameValues .CHAT .value
53+ attributes : dict [str , Any ] = {}
54+ attributes [GenAI .GEN_AI_OPERATION_NAME ] = (
55+ GenAI .GenAiOperationNameValues .CHAT .value
5656 )
5757 if invocation .request_model :
58- span .set_attribute (
59- GenAI .GEN_AI_REQUEST_MODEL , invocation .request_model
60- )
58+ attributes [GenAI .GEN_AI_REQUEST_MODEL ] = invocation .request_model
6159 if invocation .provider is not None :
6260 # TODO: clean provider name to match GenAiProviderNameValues?
63- span .set_attribute (GenAI .GEN_AI_PROVIDER_NAME , invocation .provider )
61+ attributes [GenAI .GEN_AI_PROVIDER_NAME ] = invocation .provider
62+ return attributes
63+
6464
65- _apply_response_attributes (span , invocation )
65+ def _get_llm_span_name (invocation : LLMInvocation ) -> str :
66+ """Get the span name for an LLM invocation."""
67+ return f"{ GenAI .GenAiOperationNameValues .CHAT .value } { invocation .request_model } " .strip ()
6668
6769
68- def _maybe_set_span_messages (
69- span : Span ,
70+ def _get_llm_messages_attributes_for_span (
7071 input_messages : list [InputMessage ],
7172 output_messages : list [OutputMessage ],
72- ) -> None :
73+ system_instruction : list [MessagePart ] | None = None ,
74+ ) -> dict [str , Any ]:
75+ """Get message attributes formatted for span (JSON string format).
76+
77+ Returns empty dict if not in experimental mode or content capturing is disabled.
78+ """
79+ attributes : dict [str , Any ] = {}
7380 if not is_experimental_mode () or get_content_capturing_mode () not in (
7481 ContentCapturingMode .SPAN_ONLY ,
7582 ContentCapturingMode .SPAN_AND_EVENT ,
7683 ):
77- return
84+ return attributes
7885 if input_messages :
79- span .set_attribute (
80- GenAI .GEN_AI_INPUT_MESSAGES ,
81- gen_ai_json_dumps ([asdict (message ) for message in input_messages ]),
86+ attributes [GenAI .GEN_AI_INPUT_MESSAGES ] = gen_ai_json_dumps (
87+ [asdict (message ) for message in input_messages ]
8288 )
8389 if output_messages :
84- span .set_attribute (
85- GenAI .GEN_AI_OUTPUT_MESSAGES ,
86- gen_ai_json_dumps (
87- [asdict (message ) for message in output_messages ]
88- ),
90+ attributes [GenAI .GEN_AI_OUTPUT_MESSAGES ] = gen_ai_json_dumps (
91+ [asdict (message ) for message in output_messages ]
92+ )
93+ if system_instruction :
94+ attributes [GenAI .GEN_AI_SYSTEM_INSTRUCTIONS ] = gen_ai_json_dumps (
95+ [asdict (part ) for part in system_instruction ]
8996 )
97+ return attributes
9098
9199
92- def _apply_finish_attributes (span : Span , invocation : LLMInvocation ) -> None :
100+ def _get_llm_messages_attributes_for_event (
101+ input_messages : list [InputMessage ],
102+ output_messages : list [OutputMessage ],
103+ system_instruction : list [MessagePart ] | None = None ,
104+ ) -> dict [str , Any ]:
105+ """Get message attributes formatted for event (structured format).
106+
107+ Returns empty dict if not in experimental mode or content capturing is disabled.
108+ """
109+ attributes : dict [str , Any ] = {}
110+ if not is_experimental_mode () or get_content_capturing_mode () not in (
111+ ContentCapturingMode .EVENT_ONLY ,
112+ ContentCapturingMode .SPAN_AND_EVENT ,
113+ ):
114+ return attributes
115+ if input_messages :
116+ attributes [GenAI .GEN_AI_INPUT_MESSAGES ] = [
117+ asdict (message ) for message in input_messages
118+ ]
119+ if output_messages :
120+ attributes [GenAI .GEN_AI_OUTPUT_MESSAGES ] = [
121+ asdict (message ) for message in output_messages
122+ ]
123+ if system_instruction :
124+ attributes [GenAI .GEN_AI_SYSTEM_INSTRUCTIONS ] = [
125+ asdict (part ) for part in system_instruction
126+ ]
127+ return attributes
128+
129+
130+ def _maybe_emit_llm_event (
131+ logger : Logger | None ,
132+ invocation : LLMInvocation ,
133+ error : Error | None = None ,
134+ ) -> None :
135+ """Emit a gen_ai.client.inference.operation.details event to the logger.
136+
137+ This function creates a LogRecord event following the semantic convention
138+ for gen_ai.client.inference.operation.details as specified in the GenAI
139+ event semantic conventions.
140+ """
141+ if not is_experimental_mode () or get_content_capturing_mode () not in (
142+ ContentCapturingMode .EVENT_ONLY ,
143+ ContentCapturingMode .SPAN_AND_EVENT ,
144+ ):
145+ return
146+
147+ if logger is None :
148+ return
149+
150+ # Build event attributes by reusing the attribute getter functions
151+ attributes : dict [str , Any ] = {}
152+ attributes .update (_get_llm_common_attributes (invocation ))
153+ attributes .update (_get_llm_request_attributes (invocation ))
154+ attributes .update (_get_llm_response_attributes (invocation ))
155+ attributes .update (
156+ _get_llm_messages_attributes_for_event (
157+ invocation .input_messages ,
158+ invocation .output_messages ,
159+ invocation .system_instruction ,
160+ )
161+ )
162+
163+ # Add error.type if operation ended in error
164+ if error is not None :
165+ attributes [ErrorAttributes .ERROR_TYPE ] = error .type .__qualname__
166+
167+ # Create and emit the event
168+ event = LogRecord (
169+ event_name = "gen_ai.client.inference.operation.details" ,
170+ attributes = attributes ,
171+ )
172+ logger .emit (event )
173+
174+
175+ def _apply_llm_finish_attributes (
176+ span : Span , invocation : LLMInvocation
177+ ) -> None :
93178 """Apply attributes/messages common to finish() paths."""
94- _apply_common_span_attributes (span , invocation )
95- _maybe_set_span_messages (
96- span , invocation .input_messages , invocation .output_messages
179+ # Update span name
180+ span .update_name (_get_llm_span_name (invocation ))
181+
182+ # Build all attributes by reusing the attribute getter functions
183+ attributes : dict [str , Any ] = {}
184+ attributes .update (_get_llm_common_attributes (invocation ))
185+ attributes .update (_get_llm_request_attributes (invocation ))
186+ attributes .update (_get_llm_response_attributes (invocation ))
187+ attributes .update (
188+ _get_llm_messages_attributes_for_span (
189+ invocation .input_messages ,
190+ invocation .output_messages ,
191+ invocation .system_instruction ,
192+ )
97193 )
98- _apply_request_attributes (span , invocation )
99- _apply_response_attributes (span , invocation )
100- span .set_attributes (invocation .attributes )
194+ attributes .update (invocation .attributes )
195+
196+ # Set all attributes on the span
197+ if attributes :
198+ span .set_attributes (attributes )
101199
102200
103201def _apply_error_attributes (span : Span , error : Error ) -> None :
@@ -107,8 +205,10 @@ def _apply_error_attributes(span: Span, error: Error) -> None:
107205 span .set_attribute (ErrorAttributes .ERROR_TYPE , error .type .__qualname__ )
108206
109207
110- def _apply_request_attributes (span : Span , invocation : LLMInvocation ) -> None :
111- """Attach GenAI request semantic convention attributes to the span."""
208+ def _get_llm_request_attributes (
209+ invocation : LLMInvocation ,
210+ ) -> dict [str , Any ]:
211+ """Get GenAI request semantic convention attributes."""
112212 attributes : dict [str , Any ] = {}
113213 if invocation .temperature is not None :
114214 attributes [GenAI .GEN_AI_REQUEST_TEMPERATURE ] = invocation .temperature
@@ -130,12 +230,13 @@ def _apply_request_attributes(span: Span, invocation: LLMInvocation) -> None:
130230 )
131231 if invocation .seed is not None :
132232 attributes [GenAI .GEN_AI_REQUEST_SEED ] = invocation .seed
133- if attributes :
134- span .set_attributes (attributes )
233+ return attributes
135234
136235
137- def _apply_response_attributes (span : Span , invocation : LLMInvocation ) -> None :
138- """Attach GenAI response semantic convention attributes to the span."""
236+ def _get_llm_response_attributes (
237+ invocation : LLMInvocation ,
238+ ) -> dict [str , Any ]:
239+ """Get GenAI response semantic convention attributes."""
139240 attributes : dict [str , Any ] = {}
140241
141242 finish_reasons : list [str ] | None
@@ -169,13 +270,15 @@ def _apply_response_attributes(span: Span, invocation: LLMInvocation) -> None:
169270 if invocation .output_tokens is not None :
170271 attributes [GenAI .GEN_AI_USAGE_OUTPUT_TOKENS ] = invocation .output_tokens
171272
172- if attributes :
173- span .set_attributes (attributes )
273+ return attributes
174274
175275
176276__all__ = [
177- "_apply_finish_attributes " ,
277+ "_apply_llm_finish_attributes " ,
178278 "_apply_error_attributes" ,
179- "_apply_request_attributes" ,
180- "_apply_response_attributes" ,
279+ "_get_llm_common_attributes" ,
280+ "_get_llm_request_attributes" ,
281+ "_get_llm_response_attributes" ,
282+ "_get_llm_span_name" ,
283+ "_maybe_emit_llm_event" ,
181284]
0 commit comments