|
7 | 7 | from urllib.parse import parse_qs, urlparse |
8 | 8 |
|
9 | 9 | from opentelemetry import context |
10 | | -from opentelemetry.sdk.trace import ReadableSpan, Span |
| 10 | +from opentelemetry.sdk.trace import Event, ReadableSpan, Span |
11 | 11 | from opentelemetry.sdk.util.instrumentation import InstrumentationScope |
12 | 12 | from opentelemetry.trace import SpanKind, Status, StatusCode |
13 | 13 |
|
|
26 | 26 | from ..scrubbing import BaseScrubber |
27 | 27 | from ..utils import ( |
28 | 28 | ReadableSpanDict, |
| 29 | + handle_internal_errors, |
29 | 30 | is_asgi_send_receive_span_name, |
30 | 31 | is_instrumentation_suppressed, |
31 | 32 | span_to_dict, |
@@ -72,15 +73,18 @@ def on_start( |
72 | 73 | super().on_start(span, parent_context) |
73 | 74 |
|
74 | 75 | def on_end(self, span: ReadableSpan) -> None: |
75 | | - span_dict = span_to_dict(span) |
76 | | - _tweak_asgi_send_receive_spans(span_dict) |
77 | | - _tweak_sqlalchemy_connect_spans(span_dict) |
78 | | - _tweak_http_spans(span_dict) |
79 | | - _summarize_db_statement(span_dict) |
80 | | - _set_error_level_and_status(span_dict) |
81 | | - _transform_langchain_span(span_dict) |
82 | | - self.scrubber.scrub_span(span_dict) |
83 | | - span = ReadableSpan(**span_dict) |
| 76 | + with handle_internal_errors: |
| 77 | + span_dict = span_to_dict(span) |
| 78 | + _tweak_asgi_send_receive_spans(span_dict) |
| 79 | + _tweak_sqlalchemy_connect_spans(span_dict) |
| 80 | + _tweak_http_spans(span_dict) |
| 81 | + _summarize_db_statement(span_dict) |
| 82 | + _set_error_level_and_status(span_dict) |
| 83 | + _transform_langchain_span(span_dict) |
| 84 | + _transform_google_genai_span(span_dict) |
| 85 | + _default_gen_ai_response_model(span_dict) |
| 86 | + self.scrubber.scrub_span(span_dict) |
| 87 | + span = ReadableSpan(**span_dict) |
84 | 88 | super().on_end(span) |
85 | 89 |
|
86 | 90 |
|
@@ -404,3 +408,38 @@ def _transform_langchain_message(old_message: dict[str, Any]) -> dict[str, Any]: |
404 | 408 | if 'tool_call_id' in result: |
405 | 409 | result['id'] = result.pop('tool_call_id') |
406 | 410 | return result |
| 411 | + |
| 412 | + |
| 413 | +def _default_gen_ai_response_model(span: ReadableSpanDict): |
| 414 | + attrs = span['attributes'] |
| 415 | + if 'gen_ai.request.model' in attrs and 'gen_ai.response.model' not in attrs: |
| 416 | + span['attributes'] = { |
| 417 | + **attrs, |
| 418 | + 'gen_ai.response.model': attrs['gen_ai.request.model'], |
| 419 | + } |
| 420 | + |
| 421 | + |
| 422 | +def _transform_google_genai_span(span: ReadableSpanDict): |
| 423 | + scope = span['instrumentation_scope'] |
| 424 | + if not (scope and scope.name == 'opentelemetry.instrumentation.google_genai'): |
| 425 | + return |
| 426 | + |
| 427 | + new_events: list[Event] = [] |
| 428 | + events_attr: list[dict[str, Any]] = [] |
| 429 | + for event in span['events']: |
| 430 | + if not ( |
| 431 | + event.name.startswith('gen_ai.') |
| 432 | + and event.attributes |
| 433 | + and isinstance(event_attrs_string := event.attributes.get('event_body'), str) |
| 434 | + ): # pragma: no cover |
| 435 | + new_events.append(event) |
| 436 | + continue |
| 437 | + event_attrs: dict[str, Any] = json.loads(event_attrs_string) |
| 438 | + events_attr.append(event_attrs) |
| 439 | + span['attributes'] = { |
| 440 | + **span['attributes'], |
| 441 | + 'events': json.dumps(events_attr), |
| 442 | + 'gen_ai.operation.name': 'chat', |
| 443 | + ATTRIBUTES_JSON_SCHEMA_KEY: attributes_json_schema(JsonSchemaProperties({'events': {'type': 'array'}})), |
| 444 | + } |
| 445 | + span['events'] = new_events |
0 commit comments