diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md index 39a9aa5eee..cebe7b92e5 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Implement the new semantic convention changes made in https://github.com/open-telemetry/semantic-conventions/pull/2179. +A single event (`gen_ai.client.inference.operation.details`) is used to capture Chat History. This is opt-in, +an environment variable OTEL_SEMCONV_STABILITY_OPT_IN needs to be set to `gen_ai_latest_experimental` to see them ([#3386](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3386)) +- Support CompletionHook for upload to cloud storage. + ## Version 0.3b0 (2025-07-08) - Add automatic instrumentation to tool call functions ([#3446](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3446)) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml index 7b5f585f5e..30695c7c4e 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/pyproject.toml @@ -38,13 +38,15 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~=1.37", - "opentelemetry-instrumentation >=0.52b1, <2", - "opentelemetry-semantic-conventions >=0.52b1, <2" -] + "opentelemetry-instrumentation >=0.58b0, <2", + "opentelemetry-semantic-conventions >=0.58b0, <2", + # TODO https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3786: restrict + # version after the first release + "opentelemetry-util-genai",] [project.optional-dependencies] instruments = [ - "google-genai >= 1.0.0" + "google-genai >= 1.32.0" ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/flags.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/flags.py index 541d9ab48f..6fd404eadf 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/flags.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/flags.py @@ -12,12 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +from os import environ +from typing import Union -_CONTENT_RECORDING_ENV_VAR = ( - "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" +from opentelemetry.instrumentation._semconv import _StabilityMode +from opentelemetry.util.genai.environment_variables import ( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) +from opentelemetry.util.genai.types import ContentCapturingMode +from opentelemetry.util.genai.utils import get_content_capturing_mode -def is_content_recording_enabled(): - return os.getenv(_CONTENT_RECORDING_ENV_VAR, "false").lower() == "true" +def is_content_recording_enabled( + mode: _StabilityMode, +) -> Union[bool, ContentCapturingMode]: + if mode == _StabilityMode.DEFAULT: + capture_content = environ.get( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false" + ) + return capture_content.lower() == "true" + if mode == _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL: + return get_content_capturing_mode() + raise RuntimeError(f"{mode} mode not supported") diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py index 7e85336e56..4598915dc3 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines import copy +import dataclasses import functools import json import logging @@ -21,6 +23,7 @@ from typing import Any, AsyncIterator, Awaitable, Iterator, Optional, Union from google.genai.models import AsyncModels, Models +from google.genai.models import t as transformers from google.genai.types import ( BlockedReason, Candidate, @@ -35,16 +38,36 @@ ) from opentelemetry import trace +from opentelemetry._logs import LogRecord +from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _StabilityMode, +) from opentelemetry.semconv._incubating.attributes import ( code_attributes, gen_ai_attributes, ) from opentelemetry.semconv.attributes import error_attributes +from opentelemetry.trace.span import Span +from opentelemetry.util.genai.completion_hook import CompletionHook +from opentelemetry.util.genai.types import ( + ContentCapturingMode, + InputMessage, + MessagePart, + OutputMessage, +) +from opentelemetry.util.genai.utils import gen_ai_json_dumps from .allowlist_util import AllowList from .custom_semconv import GCP_GENAI_OPERATION_CONFIG from .dict_util import flatten_dict from .flags import is_content_recording_enabled +from .message import ( + to_input_messages, + to_output_messages, + to_system_instructions, +) from .otel_wrapper import OTelWrapper from .tool_call_wrapper import wrapped as wrapped_tool @@ -144,7 +167,9 @@ def _to_dict(value: object): def _add_request_options_to_span( - span, config: Optional[GenerateContentConfigOrDict], allow_list: AllowList + span: Span, + config: Optional[GenerateContentConfigOrDict], + allow_list: AllowList, ): if config is None: return @@ -196,6 +221,31 @@ def _add_request_options_to_span( span.set_attribute(key, value) +def _get_gen_ai_request_attributes( + config: Union[GenerateContentConfigOrDict, None], +) -> dict[str, Any]: + if not config: + return {} + attributes: dict[str, Any] = {} + config = _coerce_config_to_object(config) + if config.seed: + attributes[gen_ai_attributes.GEN_AI_REQUEST_SEED] = config.seed + if config.candidate_count: + attributes[gen_ai_attributes.GEN_AI_REQUEST_CHOICE_COUNT] = ( + config.candidate_count + ) + if config.response_mime_type: + if config.response_mime_type == "text/plain": + attributes[gen_ai_attributes.GEN_AI_OUTPUT_TYPE] = "text" + elif config.response_mime_type == "application/json": + attributes[gen_ai_attributes.GEN_AI_OUTPUT_TYPE] = "json" + else: + attributes[gen_ai_attributes.GEN_AI_OUTPUT_TYPE] = ( + config.response_mime_type + ) + return attributes + + def _get_response_property(response: GenerateContentResponse, path: str): path_segments = path.split(".") current_context = response @@ -232,23 +282,65 @@ def _wrapped_config_with_tools( return result +def _config_to_system_instruction( + config: Union[GenerateContentConfigOrDict, None], +) -> Union[ContentUnion, None]: + if not config: + return None + + if isinstance(config, dict): + return GenerateContentConfig.model_validate(config).system_instruction + return config.system_instruction + + +def _create_completion_details_attributes( + input_messages: list[InputMessage], + output_messages: list[OutputMessage], + system_instructions: list[MessagePart], + as_str: bool = False, +) -> dict[str, Any]: + attributes: dict[str, Any] = { + gen_ai_attributes.GEN_AI_INPUT_MESSAGES: [ + dataclasses.asdict(input_message) + for input_message in input_messages + ], + gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES: [ + dataclasses.asdict(output_message) + for output_message in output_messages + ], + } + if system_instructions: + attributes[gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS] = [ + dataclasses.asdict(sys_instr) for sys_instr in system_instructions + ] + + return attributes + + class _GenerateContentInstrumentationHelper: def __init__( self, models_object: Union[Models, AsyncModels], otel_wrapper: OTelWrapper, model: str, + completion_hook: CompletionHook, generate_content_config_key_allowlist: Optional[AllowList] = None, ): self._start_time = time.time_ns() self._otel_wrapper = otel_wrapper self._genai_system = _determine_genai_system(models_object) self._genai_request_model = model + self.completion_hook = completion_hook self._finish_reasons_set = set() self._error_type = None self._input_tokens = 0 self._output_tokens = 0 - self._content_recording_enabled = is_content_recording_enabled() + self.sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.GEN_AI + ) + self._content_recording_enabled = is_content_recording_enabled( + self.sem_conv_opt_in_mode + ) self._response_index = 0 self._candidate_index = 0 self._generate_content_config_key_allowlist = ( @@ -268,42 +360,50 @@ def wrapped_config( def start_span_as_current_span( self, model_name, function_name, end_on_exit=True - ): + ) -> Span: return self._otel_wrapper.start_as_current_span( f"{_GENERATE_CONTENT_OP_NAME} {model_name}", start_time=self._start_time, attributes={ code_attributes.CODE_FUNCTION_NAME: function_name, - gen_ai_attributes.GEN_AI_SYSTEM: self._genai_system, gen_ai_attributes.GEN_AI_REQUEST_MODEL: self._genai_request_model, gen_ai_attributes.GEN_AI_OPERATION_NAME: _GENERATE_CONTENT_OP_NAME, }, end_on_exit=end_on_exit, ) - def process_request( - self, - contents: Union[ContentListUnion, ContentListUnionDict], - config: Optional[GenerateContentConfigOrDict], + def add_request_options_to_span( + self, config: Optional[GenerateContentConfigOrDict] ): span = trace.get_current_span() _add_request_options_to_span( span, config, self._generate_content_config_key_allowlist ) + + def process_request( + self, + contents: Union[ContentListUnion, ContentListUnionDict], + config: Optional[GenerateContentConfigOrDict], + ): self._maybe_log_system_instruction(config=config) self._maybe_log_user_prompt(contents) def process_response(self, response: GenerateContentResponse): - # TODO: Determine if there are other response properties that - # need to be reflected back into the span attributes. - # - # See also: TODOS.md. - self._update_finish_reasons(response) - self._maybe_update_token_counts(response) - self._maybe_update_error_type(response) + self._update_response(response) self._maybe_log_response(response) self._response_index += 1 + def process_completion( + self, + request: Union[ContentListUnion, ContentListUnionDict], + response: GenerateContentResponse, + config: Optional[GenerateContentConfigOrDict] = None, + ): + self._update_response(response) + self._maybe_log_completion_details( + request, response.candidates or [], config + ) + def process_error(self, e: Exception): self._error_type = str(e.__class__.__name__) @@ -319,10 +419,23 @@ def finalize_processing(self): gen_ai_attributes.GEN_AI_RESPONSE_FINISH_REASONS, sorted(self._finish_reasons_set), ) + if self.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: + span.set_attribute( + gen_ai_attributes.GEN_AI_SYSTEM, self._genai_system + ) self._record_token_usage_metric() self._record_duration_metric() - def _update_finish_reasons(self, response): + def _update_response(self, response: GenerateContentResponse): + # TODO: Determine if there are other response properties that + # need to be reflected back into the span attributes. + # + # See also: TODOS.md. + self._update_finish_reasons(response) + self._maybe_update_token_counts(response) + self._maybe_update_error_type(response) + + def _update_finish_reasons(self, response: GenerateContentResponse): if not response.candidates: return for candidate in response.candidates: @@ -373,6 +486,62 @@ def _maybe_update_error_type(self, response: GenerateContentResponse): block_reason = response.prompt_feedback.block_reason.name.upper() self._error_type = f"BLOCKED_{block_reason}" + def _maybe_log_completion_details( + self, + request: Union[ContentListUnion, ContentListUnionDict], + candidates: list[Candidate], + config: Optional[GenerateContentConfigOrDict] = None, + ): + attributes = _get_gen_ai_request_attributes(config) + system_instructions = [] + if system_content := _config_to_system_instruction(config): + system_instructions = to_system_instructions( + content=transformers.t_contents(system_content)[0] + ) + input_messages = to_input_messages( + contents=transformers.t_contents(request) + ) + output_messages = to_output_messages(candidates=candidates) + + span = trace.get_current_span() + event = LogRecord( + event_name="gen_ai.client.inference.operation.details", + attributes=attributes, + ) + self.completion_hook.on_completion( + inputs=input_messages, + outputs=output_messages, + system_instruction=system_instructions, + span=span, + log_record=event, + ) + completion_details_attributes = _create_completion_details_attributes( + input_messages, + output_messages, + system_instructions, + ) + if self._content_recording_enabled in [ + ContentCapturingMode.EVENT_ONLY, + ContentCapturingMode.SPAN_AND_EVENT, + ]: + event.attributes = { + **(event.attributes or {}), + **completion_details_attributes, + } + self._otel_wrapper.log_completion_details(event=event) + + if self._content_recording_enabled in [ + ContentCapturingMode.SPAN_ONLY, + ContentCapturingMode.SPAN_AND_EVENT, + ]: + span.set_attributes( + { + k: gen_ai_json_dumps(v) + for k, v in completion_details_attributes.items() + } + ) + span.set_attributes(attributes) + def _maybe_log_system_instruction( self, config: Optional[GenerateContentConfigOrDict] = None ): @@ -445,32 +614,6 @@ def _maybe_log_single_user_prompt( body=body, ) - def _maybe_log_response_stats(self, response: GenerateContentResponse): - # TODO: Determine if there is a way that we can log a summary - # of the overall response in a manner that is aligned with - # Semantic Conventions. For example, it would be natural - # to report an event that looks something like: - # - # gen_ai.response.stats { - # response_index: 0, - # candidate_count: 3, - # parts_per_candidate: [ - # 3, - # 1, - # 5 - # ] - # } - # - pass - - def _maybe_log_response_safety_ratings( - self, response: GenerateContentResponse - ): - # TODO: Determine if there is a way that we can log - # the "prompt_feedback". This would be especially useful - # in the case where the response is blocked. - pass - def _maybe_log_response(self, response: GenerateContentResponse): self._maybe_log_response_stats(response) self._maybe_log_response_safety_ratings(response) @@ -526,6 +669,32 @@ def _maybe_log_response_candidate( body=body, ) + def _maybe_log_response_stats(self, response: GenerateContentResponse): + # TODO: Determine if there is a way that we can log a summary + # of the overall response in a manner that is aligned with + # Semantic Conventions. For example, it would be natural + # to report an event that looks something like: + # + # gen_ai.response.stats { + # response_index: 0, + # candidate_count: 3, + # parts_per_candidate: [ + # 3, + # 1, + # 5 + # ] + # } + # + pass + + def _maybe_log_response_safety_ratings( + self, response: GenerateContentResponse + ): + # TODO: Determine if there is a way that we can log + # the "prompt_feedback". This would be especially useful + # in the case where the response is blocked. + pass + def _record_token_usage_metric(self): self._otel_wrapper.token_usage_metric.record( self._input_tokens, @@ -565,6 +734,7 @@ def _record_duration_metric(self): def _create_instrumented_generate_content( snapshot: _MethodsSnapshot, otel_wrapper: OTelWrapper, + completion_hook: CompletionHook, generate_content_config_key_allowlist: Optional[AllowList] = None, ): wrapped_func = snapshot.generate_content @@ -582,12 +752,15 @@ def instrumented_generate_content( self, otel_wrapper, model, + completion_hook, generate_content_config_key_allowlist=generate_content_config_key_allowlist, ) with helper.start_span_as_current_span( model, "google.genai.Models.generate_content" ): - helper.process_request(contents, config) + helper.add_request_options_to_span(config) + if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: + helper.process_request(contents, config) try: response = wrapped_func( self, @@ -596,7 +769,17 @@ def instrumented_generate_content( config=helper.wrapped_config(config), **kwargs, ) - helper.process_response(response) + if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: + helper.process_response(response) + elif ( + helper.sem_conv_opt_in_mode + == _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + ): + helper.process_completion(contents, response, config) + else: + raise ValueError( + f"Sem Conv opt in mode {helper.sem_conv_opt_in_mode} not supported." + ) return response except Exception as error: helper.process_error(error) @@ -610,6 +793,7 @@ def instrumented_generate_content( def _create_instrumented_generate_content_stream( snapshot: _MethodsSnapshot, otel_wrapper: OTelWrapper, + completion_hook: CompletionHook, generate_content_config_key_allowlist: Optional[AllowList] = None, ): wrapped_func = snapshot.generate_content_stream @@ -623,16 +807,20 @@ def instrumented_generate_content_stream( config: Optional[GenerateContentConfigOrDict] = None, **kwargs: Any, ) -> Iterator[GenerateContentResponse]: + candidates: list[Candidate] = [] helper = _GenerateContentInstrumentationHelper( self, otel_wrapper, model, + completion_hook, generate_content_config_key_allowlist=generate_content_config_key_allowlist, ) with helper.start_span_as_current_span( model, "google.genai.Models.generate_content_stream" ): - helper.process_request(contents, config) + helper.add_request_options_to_span(config) + if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: + helper.process_request(contents, config) try: for response in wrapped_func( self, @@ -641,12 +829,27 @@ def instrumented_generate_content_stream( config=helper.wrapped_config(config), **kwargs, ): - helper.process_response(response) + if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: + helper.process_response(response) + elif ( + helper.sem_conv_opt_in_mode + == _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + ): + helper._update_response(response) + if response.candidates: + candidates += response.candidates + else: + raise ValueError( + f"Sem Conv opt in mode {helper.sem_conv_opt_in_mode} not supported." + ) yield response except Exception as error: helper.process_error(error) raise finally: + helper._maybe_log_completion_details( + contents, candidates, config + ) helper.finalize_processing() return instrumented_generate_content_stream @@ -655,6 +858,7 @@ def instrumented_generate_content_stream( def _create_instrumented_async_generate_content( snapshot: _MethodsSnapshot, otel_wrapper: OTelWrapper, + completion_hook: CompletionHook, generate_content_config_key_allowlist: Optional[AllowList] = None, ): wrapped_func = snapshot.async_generate_content @@ -672,12 +876,15 @@ async def instrumented_generate_content( self, otel_wrapper, model, + completion_hook, generate_content_config_key_allowlist=generate_content_config_key_allowlist, ) with helper.start_span_as_current_span( model, "google.genai.AsyncModels.generate_content" ): - helper.process_request(contents, config) + helper.add_request_options_to_span(config) + if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: + helper.process_request(contents, config) try: response = await wrapped_func( self, @@ -686,7 +893,17 @@ async def instrumented_generate_content( config=helper.wrapped_config(config), **kwargs, ) - helper.process_response(response) + if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: + helper.process_response(response) + elif ( + helper.sem_conv_opt_in_mode + == _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + ): + helper.process_completion(contents, response, config) + else: + raise ValueError( + f"Sem Conv opt in mode {helper.sem_conv_opt_in_mode} not supported." + ) return response except Exception as error: helper.process_error(error) @@ -701,6 +918,7 @@ async def instrumented_generate_content( def _create_instrumented_async_generate_content_stream( # type: ignore snapshot: _MethodsSnapshot, otel_wrapper: OTelWrapper, + completion_hook: CompletionHook, generate_content_config_key_allowlist: Optional[AllowList] = None, ): wrapped_func = snapshot.async_generate_content_stream @@ -718,6 +936,7 @@ async def instrumented_generate_content_stream( self, otel_wrapper, model, + completion_hook, generate_content_config_key_allowlist=generate_content_config_key_allowlist, ) with helper.start_span_as_current_span( @@ -725,34 +944,55 @@ async def instrumented_generate_content_stream( "google.genai.AsyncModels.generate_content_stream", end_on_exit=False, ) as span: - helper.process_request(contents, config) - try: - response_async_generator = await wrapped_func( - self, - model=model, - contents=contents, - config=helper.wrapped_config(config), - **kwargs, - ) - except Exception as error: # pylint: disable=broad-exception-caught - helper.process_error(error) - helper.finalize_processing() - with trace.use_span(span, end_on_exit=True): - raise - - async def _response_async_generator_wrapper(): - with trace.use_span(span, end_on_exit=True): - try: - async for response in response_async_generator: - helper.process_response(response) - yield response - except Exception as error: - helper.process_error(error) + helper.add_request_options_to_span(config) + if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT: + helper.process_request(contents, config) + try: + response_async_generator = await wrapped_func( + self, + model=model, + contents=contents, + config=helper.wrapped_config(config), + **kwargs, + ) + except Exception as error: # pylint: disable=broad-exception-caught + helper.process_error(error) + helper.finalize_processing() + with trace.use_span(span, end_on_exit=True): raise - finally: - helper.finalize_processing() - return _response_async_generator_wrapper() + async def _response_async_generator_wrapper(): + candidates: list[Candidate] = [] + with trace.use_span(span, end_on_exit=True): + try: + async for response in response_async_generator: + if ( + helper.sem_conv_opt_in_mode + == _StabilityMode.DEFAULT + ): + helper.process_response(response) + elif ( + helper.sem_conv_opt_in_mode + == _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + ): + helper._update_response(response) + if response.candidates: + candidates += response.candidates + else: + raise ValueError( + f"Sem Conv opt in mode {helper.sem_conv_opt_in_mode} not supported." + ) + yield response + except Exception as error: + helper.process_error(error) + raise + finally: + helper._maybe_log_completion_details( + contents, candidates, config + ) + helper.finalize_processing() + + return _response_async_generator_wrapper() return instrumented_generate_content_stream @@ -764,27 +1004,32 @@ def uninstrument_generate_content(snapshot: object): def instrument_generate_content( otel_wrapper: OTelWrapper, + completion_hook: CompletionHook, generate_content_config_key_allowlist: Optional[AllowList] = None, ) -> object: snapshot = _MethodsSnapshot() Models.generate_content = _create_instrumented_generate_content( snapshot, otel_wrapper, + completion_hook, generate_content_config_key_allowlist=generate_content_config_key_allowlist, ) Models.generate_content_stream = _create_instrumented_generate_content_stream( snapshot, otel_wrapper, + completion_hook, generate_content_config_key_allowlist=generate_content_config_key_allowlist, ) AsyncModels.generate_content = _create_instrumented_async_generate_content( snapshot, otel_wrapper, + completion_hook, generate_content_config_key_allowlist=generate_content_config_key_allowlist, ) AsyncModels.generate_content_stream = _create_instrumented_async_generate_content_stream( snapshot, otel_wrapper, + completion_hook, generate_content_config_key_allowlist=generate_content_config_key_allowlist, ) return snapshot diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/instrumentor.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/instrumentor.py index 0c11419c65..8466128c58 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/instrumentor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/instrumentor.py @@ -18,6 +18,7 @@ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.metrics import get_meter_provider from opentelemetry.trace import get_tracer_provider +from opentelemetry.util.genai.completion_hook import load_completion_hook from .allowlist_util import AllowList from .generate_content import ( @@ -58,8 +59,12 @@ def _instrument(self, **kwargs: Any): logger_provider=logger_provider, meter_provider=meter_provider, ) + completion_hook = ( + kwargs.get("completion_hook") or load_completion_hook() + ) self._generate_content_snapshot = instrument_generate_content( otel_wrapper, + completion_hook, generate_content_config_key_allowlist=self._generate_content_config_key_allowlist, ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py new file mode 100644 index 0000000000..29ef112a6f --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/message.py @@ -0,0 +1,173 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from enum import Enum +from typing import Literal + +from google.genai import types as genai_types + +from opentelemetry.util.genai.types import ( + FinishReason, + InputMessage, + MessagePart, + OutputMessage, + Text, + ToolCall, + ToolCallResponse, +) + + +class Role(str, Enum): + SYSTEM = "system" + USER = "user" + ASSISTANT = "assistant" + TOOL = "tool" + + +@dataclass +class BlobPart: + data: bytes + mime_type: str + type: Literal["blob"] = "blob" + + +@dataclass +class FileDataPart: + mime_type: str + uri: str + type: Literal["file_data"] = "file_data" + + class Config: + extra = "allow" + + +_logger = logging.getLogger(__name__) + + +def to_input_messages( + *, + contents: list[genai_types.Content], +) -> list[InputMessage]: + return [_to_input_message(content) for content in contents] + + +def to_output_messages( + *, + candidates: list[genai_types.Candidate], +) -> list[OutputMessage]: + def content_to_output_message( + candidate: genai_types.Candidate, + ) -> OutputMessage | None: + if not candidate.content: + return None + + message = _to_input_message(candidate.content) + return OutputMessage( + finish_reason=_to_finish_reason(candidate.finish_reason), + role=message.role, + parts=message.parts, + ) + + messages = ( + content_to_output_message(candidate) for candidate in candidates + ) + return [message for message in messages if message is not None] + + +def to_system_instructions( + *, + content: genai_types.Content, +) -> list[MessagePart]: + parts = ( + _to_part(part, idx) for idx, part in enumerate(content.parts or []) + ) + return [part for part in parts if part is not None] + + +def _to_input_message( + content: genai_types.Content, +) -> InputMessage: + parts = ( + _to_part(part, idx) for idx, part in enumerate(content.parts or []) + ) + return InputMessage( + role=_to_role(content.role), + # filter Nones + parts=[part for part in parts if part is not None], + ) + + +def _to_part(part: genai_types.Part, idx: int) -> MessagePart | None: + def tool_call_id(name: str | None) -> str: + if name: + return f"{name}_{idx}" + return f"{idx}" + + if (text := part.text) is not None: + return Text(content=text) + + if data := part.inline_data: + return BlobPart(mime_type=data.mime_type or "", data=data.data or b"") + + if data := part.file_data: + return FileDataPart( + mime_type=data.mime_type or "", uri=data.file_uri or "" + ) + + if call := part.function_call: + return ToolCall( + id=call.id or tool_call_id(call.name), + name=call.name or "", + arguments=call.args, + ) + + if response := part.function_response: + return ToolCallResponse( + id=response.id or tool_call_id(response.name), + response=response.response, + ) + + _logger.info("Unknown part dropped from telemetry %s", part) + return None + + +def _to_role(role: str | None) -> str: + if role == "user": + return Role.USER.value + if role == "model": + return Role.ASSISTANT.value + return "" + + +def _to_finish_reason( + finish_reason: genai_types.FinishReason | None, +) -> FinishReason | str: + if finish_reason is None: + return "" + if ( + finish_reason is genai_types.FinishReason.FINISH_REASON_UNSPECIFIED + or finish_reason is genai_types.FinishReason.OTHER + ): + return "error" + if finish_reason is genai_types.FinishReason.STOP: + return "stop" + if finish_reason is genai_types.FinishReason.MAX_TOKENS: + return "length" + + # If there is no 1:1 mapping to an OTel preferred enum value, use the exact vertex reason + return finish_reason.name diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/otel_wrapper.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/otel_wrapper.py index b0e354ea1b..b82288dec9 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/otel_wrapper.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/otel_wrapper.py @@ -11,14 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import logging +from typing import Any import google.genai -from opentelemetry._logs import LogRecord +from opentelemetry._logs import Logger, LoggerProvider, LogRecord +from opentelemetry.metrics import Meter, MeterProvider from opentelemetry.semconv._incubating.metrics import gen_ai_metrics from opentelemetry.semconv.schemas import Schemas +from opentelemetry.trace import Tracer, TracerProvider from .version import __version__ as _LIBRARY_VERSION @@ -36,7 +40,7 @@ class OTelWrapper: - def __init__(self, tracer, logger, meter): + def __init__(self, tracer: Tracer, logger: Logger, meter: Meter): self._tracer = tracer self._logger = logger self._meter = meter @@ -48,7 +52,11 @@ def __init__(self, tracer, logger, meter): ) @staticmethod - def from_providers(tracer_provider, logger_provider, meter_provider): + def from_providers( + tracer_provider: TracerProvider, + logger_provider: LoggerProvider, + meter_provider: MeterProvider, + ): return OTelWrapper( tracer_provider.get_tracer( _SCOPE_NAME, _LIBRARY_VERSION, _SCHEMA_URL, _SCOPE_ATTRIBUTES @@ -72,23 +80,38 @@ def operation_duration_metric(self): def token_usage_metric(self): return self._token_usage_metric - def log_system_prompt(self, attributes, body): + def log_system_prompt( + self, attributes: dict[str, str], body: dict[str, Any] + ): _logger.debug("Recording system prompt.") event_name = "gen_ai.system.message" self._log_event(event_name, attributes, body) - def log_user_prompt(self, attributes, body): + def log_user_prompt( + self, attributes: dict[str, str], body: dict[str, Any] + ): _logger.debug("Recording user prompt.") event_name = "gen_ai.user.message" self._log_event(event_name, attributes, body) - def log_response_content(self, attributes, body): + def log_response_content( + self, attributes: dict[str, str], body: dict[str, Any] + ): _logger.debug("Recording response.") event_name = "gen_ai.choice" self._log_event(event_name, attributes, body) - def _log_event(self, event_name, attributes, body): + def _log_event( + self, event_name: str, attributes: dict[str, str], body: dict[str, Any] + ): event = LogRecord( event_name=event_name, body=body, attributes=attributes ) self._logger.emit(event) + + def log_completion_details( + self, + event: LogRecord, + ) -> None: + _logger.debug("Recording completion details event.") + self._logger.emit(event) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py index 7b4cc1924a..f4303306e3 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py @@ -24,9 +24,15 @@ ) from opentelemetry import trace +from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _StabilityMode, +) from opentelemetry.semconv._incubating.attributes import ( code_attributes, ) +from opentelemetry.util.genai.types import ContentCapturingMode from .flags import is_content_recording_enabled from .otel_wrapper import OTelWrapper @@ -76,6 +82,21 @@ def _to_otel_attribute(python_value): return json.dumps(otel_value) +def _is_capture_content_enabled() -> bool: + mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.GEN_AI + ) + if mode == _StabilityMode.DEFAULT: + return bool(is_content_recording_enabled(mode)) + if mode == _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL: + capturing_mode = is_content_recording_enabled(mode) + return capturing_mode in [ + ContentCapturingMode.SPAN_ONLY, + ContentCapturingMode.SPAN_AND_EVENT, + ] + raise RuntimeError(f"{mode} mode not supported") + + def _create_function_span_name(wrapped_function): """Constructs the span name for a given local function tool call.""" function_name = wrapped_function.__name__ @@ -115,7 +136,7 @@ def _record_function_call_arguments( otel_wrapper, wrapped_function, function_args, function_kwargs ): """Records the details about a function invocation as span attributes.""" - include_values = is_content_recording_enabled() + include_values = _is_capture_content_enabled() span = trace.get_current_span() signature = inspect.signature(wrapped_function) params = list(signature.parameters.values()) @@ -130,7 +151,7 @@ def _record_function_call_arguments( def _record_function_call_result(otel_wrapper, wrapped_function, result): """Records the details about a function result as span attributes.""" - include_values = is_content_recording_enabled() + include_values = _is_capture_content_enabled() span = trace.get_current_span() span.set_attribute("code.function.return.type", type(result).__name__) if include_values: diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/common/auth.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/common/auth.py index 88831a3e9a..5719ddf0f8 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/common/auth.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/common/auth.py @@ -16,5 +16,9 @@ class FakeCredentials(google.auth.credentials.AnonymousCredentials): + def __init__(self): + self.token = "a" + self._quota_project_id = "a" + def refresh(self, request): pass diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-async].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-async].yaml deleted file mode 100644 index c251cc104b..0000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-async].yaml +++ /dev/null @@ -1,94 +0,0 @@ -interactions: -- request: - body: |- - { - "contents": [ - { - "parts": [ - { - "text": "Create a poem about Open Telemetry." - } - ], - "role": "user" - } - ] - } - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '92' - Content-Type: - - application/json - user-agent: - - google-genai-sdk/1.0.0 gl-python/3.12.8 - x-goog-api-client: - - - x-goog-user-project: - - - method: POST - uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-1.5-flash-002:generateContent - response: - body: - string: |- - { - "candidates": [ - { - "content": { - "role": "model", - "parts": [ - { - "text": "No more dark, inscrutable ways,\nTo trace a request through hazy days.\nOpen Telemetry, a beacon bright,\nIlluminates the path, both day and night.\n\nFrom metrics gathered, a clear display,\nOf latency's dance, and errors' sway.\nTraces unwind, a silken thread,\nShowing the journey, from start to head.\n\nLogs interweave, a richer hue,\nContextual clues, for me and you.\nNo vendor lock-in, a freedom's call,\nTo choose your tools, to stand up tall.\n\nExporters aplenty, a varied choice,\nTo send your data, amplify your voice.\nJaeger, Zipkin, Prometheus' might,\nAll integrate, a glorious sight.\n\nWith spans and attributes, a detailed scene,\nOf how your system works, both sleek and keen.\nPerformance bottlenecks, now laid bare,\nOpen Telemetry, beyond compare.\n\nSo embrace the light, let darkness flee,\nWith Open Telemetry, set your systems free.\nObserve, and learn, and optimize with grace,\nA brighter future, in this digital space.\n" - } - ] - }, - "finishReason": "STOP", - "avgLogprobs": -0.3303731600443522 - } - ], - "usageMetadata": { - "promptTokenCount": 8, - "candidatesTokenCount": 240, - "totalTokenCount": 248, - "promptTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 8 - } - ], - "candidatesTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 240 - } - ] - }, - "modelVersion": "gemini-1.5-flash-002", - "createTime": "2025-03-07T22:19:18.083091Z", - "responseId": "5nDLZ5OJBdyY3NoPiZGx0Ag" - } - headers: - Content-Encoding: - - gzip - Content-Type: - - application/json; charset=UTF-8 - Transfer-Encoding: - - chunked - Vary: - - Origin - - X-Origin - - Referer - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-XSS-Protection: - - '0' - status: - code: 200 - message: OK -version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml deleted file mode 100644 index 3ae84308bf..0000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml +++ /dev/null @@ -1,94 +0,0 @@ -interactions: -- request: - body: |- - { - "contents": [ - { - "parts": [ - { - "text": "Create a poem about Open Telemetry." - } - ], - "role": "user" - } - ] - } - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '92' - Content-Type: - - application/json - user-agent: - - google-genai-sdk/1.0.0 gl-python/3.12.8 - x-goog-api-client: - - - x-goog-user-project: - - - method: POST - uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-1.5-flash-002:generateContent - response: - body: - string: |- - { - "candidates": [ - { - "content": { - "role": "model", - "parts": [ - { - "text": "No more dark logs, a cryptic, hidden trace,\nOf failing systems, lost in time and space.\nOpenTelemetry, a beacon shining bright,\nIlluminating paths, both dark and light.\n\nFrom microservices, a sprawling, tangled mesh,\nTo monolithic beasts, put to the test,\nIt gathers traces, spans, and metrics too,\nA holistic view, for me and you.\n\nWith signals clear, from every single node,\nPerformance bottlenecks, instantly bestowed.\nDistributed tracing, paints a vivid scene,\nWhere latency lurks, and slowdowns intervene.\n\nExporters rise, to send the data forth,\nTo dashboards grand, of proven, measured worth.\nPrometheus, Grafana, Jaeger, fluent streams,\nVisualizing insights, fulfilling data dreams.\n\nFrom Jaeger's diagrams, a branching, flowing art,\nTo Grafana's charts, that play a vital part,\nThe mysteries unravel, hidden deep inside,\nWhere errors slumber, and slow responses hide.\n\nSo hail OpenTelemetry, a gift to all who code,\nA brighter future, on a well-lit road.\nNo more guesswork, no more fruitless chase,\nJust clear observability, in time and space.\n" - } - ] - }, - "finishReason": "STOP", - "avgLogprobs": -0.45532724261283875 - } - ], - "usageMetadata": { - "promptTokenCount": 8, - "candidatesTokenCount": 256, - "totalTokenCount": 264, - "promptTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 8 - } - ], - "candidatesTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 256 - } - ] - }, - "modelVersion": "gemini-1.5-flash-002", - "createTime": "2025-03-07T22:19:15.268428Z", - "responseId": "43DLZ4yxEM6F3NoPzaTkiQU" - } - headers: - Content-Encoding: - - gzip - Content-Type: - - application/json; charset=UTF-8 - Transfer-Encoding: - - chunked - Vary: - - Origin - - X-Origin - - Referer - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-XSS-Protection: - - '0' - status: - code: 200 - message: OK -version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-async-default].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-async-default].yaml new file mode 100644 index 0000000000..9e3f748b60 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-async-default].yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a poem about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '92' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:generateContent + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "In tangled webs where microservices gleam,\nAnd cloud-born systems whisper through a dream,\nA silent plea arises, soft and low:\n\"What truly happens? Where does the data flow?\"\n\nFrom this complex symphony, a standard came,\nTo cast a light upon the hidden game.\nNot tied to one, but open, brave, and free,\n**OpenTelemetry**, for all to see.\n\nIt offers sight, where once was hazy guess,\nThree vital pillars, banishing distress:\n**First, Traces** weave a path, a golden thread,\nFrom first request, through every server led.\nEach tiny hop, a span, a measured beat,\nRevealing journeys, bittersweet and fleet.\n\nThen **Metrics** rise, like numbers in the air,\nA pulse, a count, a burden systems bear.\nCPU hum, or memory's slow creep,\nThe silent sentinels, while data sleeps.\nGauges, counters, histograms defined,\nThe system's health, precisely, you will find.\n\nAnd **Logs**, the whispers from the code's deep heart,\nEach message thrown, a critical new start.\nErrors caught, or triumphs understood,\nThe story told, for bad, or for the good.\nContext rich, linked to the trace it keeps,\nWhere hidden truths, the developer sweeps.\n\nThese three unite, no longer kept apart,\nA common tongue, a truly open art.\nWith SDKs, your code can now converse,\nAnd send its data, 'cross the universe.\nTo any backend, or analytics tool,\nBreaking the vendor's tightly guarded rule.\n\nSo when the bug lurks deep, or users fret,\nOr performance lags, a problem to be met,\n**OpenTelemetry** sheds its guiding light,\nTransforming darkness into knowing sight.\nA tapestry of data, richly spun,\nThe battle for observability won." + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -1.836124080242497 + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 404, + "totalTokenCount": 1958, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 8 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 404 + } + ], + "thoughtsTokenCount": 1546 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-10-10T15:56:32.819932Z", + "responseId": "sCzpaNyFMuLlnvgP3MqN0AQ" + } + headers: + Accept-Ranges: + - none + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 10 Oct 2025 15:56:44 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - X-Origin + - Referer + - Origin,Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml new file mode 100644 index 0000000000..09a94edd30 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a poem about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '92' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:generateContent + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "When systems grow, a tangled, vast domain,\nWhere microservices dance, through sun and rain,\nA shadow falls, where logic hides its plea,\nAnd whispers vanish, lost for all to see.\n\nThen from the cloud, a guiding star takes flight,\nDispelling gloom with its clear, steady light.\nOpenTelemetry, the name we praise,\nUnifying vision through digital haze.\n\nA golden thread, a trace begins to gleam,\nThrough every service, like a waking dream.\nWith parent-child spans, a timeline clear,\nWhat once was hidden, now begins to appear.\n\nThe metrics flow, a system's vital pulse,\nWith counters climbing, no evasive impulse.\nGauges ascend and dip, where loads reside,\nThe heartbeat known, with nowhere left to hide.\n\nAnd logs appear, with context richly cast,\nEach crucial event, for future and for past.\nAttached to spans, a detailed, storied chime,\nUnveiling insights, lost to passing time.\n\nThrough RPC calls, and message queues that bind,\nContext propagates, for every searching mind.\nA hidden thread, connecting all the parts,\nRevealing patterns, igniting thoughtful starts.\n\nNo vendor lock, no proprietary chain,\nYour data streams, through sunshine and through rain.\nTo chosen backends, it will freely roam,\nObservability, now finds its rightful home.\n\nSo hail to Otel, the standard, strong and true,\nBringing clarity, for all our dev crews.\nFrom dark unknowns, to light, a clear display,\nOpenTelemetry illuminates the way." + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -2.441835476585546 + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 339, + "totalTokenCount": 2639, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 8 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 339 + } + ], + "thoughtsTokenCount": 2292 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-10-10T15:56:16.631749Z", + "responseId": "oCzpaMXHJo_B2PgPq7j_8AY" + } + headers: + Accept-Ranges: + - none + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 10 Oct 2025 15:56:30 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - X-Origin + - Referer + - Origin,Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-async].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-async].yaml deleted file mode 100644 index 77e985bf28..0000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-async].yaml +++ /dev/null @@ -1,94 +0,0 @@ -interactions: -- request: - body: |- - { - "contents": [ - { - "parts": [ - { - "text": "Create a poem about Open Telemetry." - } - ], - "role": "user" - } - ] - } - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '92' - Content-Type: - - application/json - user-agent: - - google-genai-sdk/1.0.0 gl-python/3.12.8 - x-goog-api-client: - - - x-goog-user-project: - - - method: POST - uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-1.5-flash-002:generateContent - response: - body: - string: |- - { - "candidates": [ - { - "content": { - "role": "model", - "parts": [ - { - "text": "No more dark, mysterious traces,\nNo more guessing, in empty spaces.\nOpenTelemetry's light now shines,\nIlluminating all our designs.\n\nFrom microservices, small and fleet,\nTo monolithic beasts, hard to beat,\nIt weaves a net, both fine and strong,\nWhere metrics flow, where logs belong.\n\nTraces dance, a vibrant hue,\nShowing journeys, old and new.\nSpans unfold, a story told,\nOf requests handled, brave and bold.\n\nMetrics hum, a steady beat,\nLatency, errors, can't be beat.\nDistribution charts, a clear display,\nGuiding us along the way.\n\nLogs provide a detailed view,\nOf what happened, me and you.\nContext rich, with helpful clues,\nDebugging woes, it quickly subdues.\n\nWith exporters wise, a thoughtful choice,\nTo Prometheus, Jaeger, or Zipkin's voice,\nOur data flows, a precious stream,\nReal-time insights, a waking dream.\n\nSo hail to OpenTelemetry's might,\nBringing clarity to our darkest night.\nObservability's champion, bold and true,\nA brighter future, for me and you.\n" - } - ] - }, - "finishReason": "STOP", - "avgLogprobs": -0.4071464086238575 - } - ], - "usageMetadata": { - "promptTokenCount": 8, - "candidatesTokenCount": 253, - "totalTokenCount": 261, - "promptTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 8 - } - ], - "candidatesTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 253 - } - ] - }, - "modelVersion": "gemini-1.5-flash-002", - "createTime": "2025-03-07T22:19:12.443989Z", - "responseId": "4HDLZ9WMG6SK698Pr5uZ2Qw" - } - headers: - Content-Encoding: - - gzip - Content-Type: - - application/json; charset=UTF-8 - Transfer-Encoding: - - chunked - Vary: - - Origin - - X-Origin - - Referer - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-XSS-Protection: - - '0' - status: - code: 200 - message: OK -version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml deleted file mode 100644 index 7d3d7a56b2..0000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml +++ /dev/null @@ -1,94 +0,0 @@ -interactions: -- request: - body: |- - { - "contents": [ - { - "parts": [ - { - "text": "Create a poem about Open Telemetry." - } - ], - "role": "user" - } - ] - } - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '92' - Content-Type: - - application/json - user-agent: - - google-genai-sdk/1.0.0 gl-python/3.12.8 - x-goog-api-client: - - - x-goog-user-project: - - - method: POST - uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-1.5-flash-002:generateContent - response: - body: - string: |- - { - "candidates": [ - { - "content": { - "role": "model", - "parts": [ - { - "text": "No more dark, mysterious traces,\nOf failing systems, hidden spaces.\nOpen Telemetry's light shines bright,\nGuiding us through the darkest night.\n\nFrom metrics gathered, finely spun,\nTo logs that tell of tasks undone,\nAnd traces linking every call,\nIt answers questions, standing tall.\n\nDistributed systems, complex and vast,\nTheir hidden flaws, no longer cast\nIn shadows deep, beyond our view,\nOpen Telemetry sees them through.\n\nWith spans and attributes, it weaves a tale,\nOf requests flowing, never frail.\nIt pinpoints bottlenecks, slow and grim,\nAnd helps us optimize, system trim.\n\nAcross languages, a common ground,\nWhere data's shared, and insights found.\nExporters whisper, collectors hum,\nA symphony of data, overcome.\n\nSo raise a glass, to this open source,\nA shining beacon, a powerful force.\nOpen Telemetry, a guiding star,\nRevealing secrets, near and far.\n" - } - ] - }, - "finishReason": "STOP", - "avgLogprobs": -0.3586180628193498 - } - ], - "usageMetadata": { - "promptTokenCount": 8, - "candidatesTokenCount": 211, - "totalTokenCount": 219, - "promptTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 8 - } - ], - "candidatesTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 211 - } - ] - }, - "modelVersion": "gemini-1.5-flash-002", - "createTime": "2025-03-07T22:19:09.936326Z", - "responseId": "3XDLZ4aTOZSpnvgPn-e0qQk" - } - headers: - Content-Encoding: - - gzip - Content-Type: - - application/json; charset=UTF-8 - Transfer-Encoding: - - chunked - Vary: - - Origin - - X-Origin - - Referer - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-XSS-Protection: - - '0' - status: - code: 200 - message: OK -version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-async-default].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-async-default].yaml new file mode 100644 index 0000000000..31b149d902 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-async-default].yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a poem about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '92' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:generateContent + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "In tangled webs of code, where logic spins,\nA silent plea for sight, when trouble wins.\nDistributed dreams, where microservices hum,\nCan hide the fault, from whence the failures come.\n\nThen from the void, a beacon starts to gleam,\nOpenTelemetry, a developer's dream.\nA standard hand, a vendor-neutral plea,\nTo weave the fabric of observability.\n\nIt paints the journey, step by fragile step,\nA request's life, where secrets often slept.\nEach function call, a \"span\" upon the thread,\nConnecting dots, where errors might have spread.\n\nThe system's heartbeat, captured, clear, and bright,\nIn counters climbing, through the digital night.\nGauges that measure, histograms that show,\nThe ebb and flow, of where the currents go.\n\nAnd logs, the narratives, of every deed,\nA detailed story, planted like a seed.\nContext attached, for clarity and grace,\nTo pinpoint flaws, and put them in their place.\n\nNo vendor lock, no proprietary chain,\nJust open arms, to ease the developer's pain.\nA unified approach, for all to see,\nThe core of insight, growing strong and free.\n\nThrough careful pipelines, data starts to flow,\nTo trusted backends, where the insights grow.\nNo matter where your services reside,\nIts watchful eye, has nowhere left to hide.\n\nSo hail the standard, comprehensive, vast,\nA future built, where understanding's cast.\nFor complex systems, now a guiding light,\nOpenTelemetry, makes everything bright." + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -1.920527738683364 + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 340, + "totalTokenCount": 2040, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 8 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 340 + } + ], + "thoughtsTokenCount": 1692 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-10-10T15:56:04.095069Z", + "responseId": "lCzpaN3mBYqCnvgP5dCT6AU" + } + headers: + Accept-Ranges: + - none + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 10 Oct 2025 15:56:14 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - X-Origin + - Referer + - Origin,Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml new file mode 100644 index 0000000000..ad43798dca --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a poem about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '92' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:generateContent + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "In systems vast, where services reside,\nA hidden flow, where secrets often hide.\nWhen errors bloom or latency takes hold,\nWe seek the story, yet untold.\n\nThen from the void, a beacon bright appears,\nDispelling shadows, calming all our fears.\n'Tis OpenTelemetry, a guiding hand,\nTo understand the workings of our land.\n\nA standard born, for code to freely speak,\nThe truths our tangled systems always seek.\nWith instrumentation, libraries we weave,\nA common language, all can now believe.\n\nEach call, each task, a journey, swift or slow,\nA **Trace** it paints, to show us where to go.\nWith nested **Spans**, a parent-child embrace,\nWe see the steps, the timing, and the space.\nFrom front-end click to database's deep hum,\nThe full request path, clearly overcome.\n\nThen **Metrics** rise, in streams precise and true,\nCPU, memory, requests old and new.\nCounters climb, and gauges ebb and flow,\nThe system's pulse, a steady, measured glow.\nNo guesswork now, but data, firm and clear,\nPerformance trends, throughout the day and year.\n\nAnd **Logs**, once scattered, now aligned with care,\nContext linking, everywhere, everywhere.\nA unique ID, through layers it will thread,\nConnecting output, words that once were dead.\nFrom single events, a narrative we glean,\nThe bigger picture, beautifully seen.\n\nNo vendor lock, no proprietary chain,\nJust open standards, easing every pain.\nThe Collector waits, a gatherer so grand,\nTo process data, from every digital hand.\nThen send it forth, to chosen backend store,\nFor insights deep, forevermore.\n\nFrom microservices to serverless flight,\nIt brings the dark domains into the light.\nA tapestry woven, of signals three,\nFor clarity and deep observability.\nSo hail to OTel, the unifying way,\nThat turns confusion into clear display." + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -1.8014850572405456 + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 433, + "totalTokenCount": 1918, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 8 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 433 + } + ], + "thoughtsTokenCount": 1477 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-10-10T15:55:50.511529Z", + "responseId": "hizpaKmcH9qs698P85HHgAU" + } + headers: + Accept-Ranges: + - none + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 10 Oct 2025 15:56:02 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - X-Origin + - Referer + - Origin,Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-async].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-async].yaml deleted file mode 100644 index a946911c36..0000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-async].yaml +++ /dev/null @@ -1,97 +0,0 @@ -interactions: -- request: - body: |- - { - "contents": [ - { - "parts": [ - { - "text": "Create a poem about Open Telemetry." - } - ], - "role": "user" - } - ] - } - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '92' - Content-Type: - - application/json - user-agent: - - google-genai-sdk/1.0.0 gl-python/3.12.8 - x-goog-api-client: - - - x-goog-user-project: - - - method: POST - uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-1.5-flash-002:streamGenerateContent?alt=sse - response: - body: - string: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"No\"}]}}],\"usageMetadata\": {},\"modelVersion\": \"gemini-1.5-flash-002\"\ - ,\"createTime\": \"2025-03-07T22:19:29.293930Z\",\"responseId\": \"8XDLZ6r4Efa1-O4PwIHamQ4\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \" longer dark, the tracing's light,\\nOpen Telemetry, shining\ - \ bright\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\"\ - : \"2025-03-07T22:19:29.293930Z\",\"responseId\": \"8XDLZ6r4Efa1-O4PwIHamQ4\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \".\\nA beacon in the coding night,\\nRevealing paths, both\ - \ dark\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"\ - 2025-03-07T22:19:29.293930Z\",\"responseId\": \"8XDLZ6r4Efa1-O4PwIHamQ4\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \" and bright.\\n\\nFrom microservice to sprawling beast,\\\ - nIts watchful eye, a silent priest.\\nObserving calls, both small and vast,\\\ - nPerformance\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\"\ - : \"2025-03-07T22:19:29.293930Z\",\"responseId\": \"8XDLZ6r4Efa1-O4PwIHamQ4\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \" flaws, revealed at last.\\n\\nWith metrics gleaned and logs\ - \ aligned,\\nA clearer picture, you will find.\\nOf latency, and errors dire,\\\ - n\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:29.293930Z\"\ - ,\"responseId\": \"8XDLZ6r4Efa1-O4PwIHamQ4\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"And bottlenecks\ - \ that set afire.\\n\\nIt spans the clouds, a network wide,\\nWhere data streams,\ - \ a surging tide.\\nCollecting traces, rich and deep,\\nWhile slumbering apps\ - \ their secrets keep.\\n\\nJaeger, Zip\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\"\ - ,\"createTime\": \"2025-03-07T22:19:29.293930Z\",\"responseId\": \"8XDLZ6r4Efa1-O4PwIHamQ4\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"kin, the tools it holds,\\nA tapestry of stories told.\\nOf\ - \ requests flowing, swift and free,\\nOr tangled knots, for all to see.\\\ - n\\nSo embrace the power, understand,\\nThe vital role, across the\"}]}}],\"\ - modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:29.293930Z\"\ - ,\"responseId\": \"8XDLZ6r4Efa1-O4PwIHamQ4\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" land.\\nOpen\ - \ Telemetry, a guiding star,\\nTo navigate the digital afar.\\n\"}]},\"finishReason\"\ - : \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 8,\"candidatesTokenCount\"\ - : 212,\"totalTokenCount\": 220,\"promptTokensDetails\": [{\"modality\": \"\ - TEXT\",\"tokenCount\": 8}],\"candidatesTokensDetails\": [{\"modality\": \"\ - TEXT\",\"tokenCount\": 212}]},\"modelVersion\": \"gemini-1.5-flash-002\",\"\ - createTime\": \"2025-03-07T22:19:29.293930Z\",\"responseId\": \"8XDLZ6r4Efa1-O4PwIHamQ4\"\ - }\r\n\r\n" - headers: - Content-Disposition: - - attachment - Content-Type: - - text/event-stream - Transfer-Encoding: - - chunked - Vary: - - Origin - - X-Origin - - Referer - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-XSS-Protection: - - '0' - status: - code: 200 - message: OK -version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml deleted file mode 100644 index 647a76b80a..0000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml +++ /dev/null @@ -1,102 +0,0 @@ -interactions: -- request: - body: |- - { - "contents": [ - { - "parts": [ - { - "text": "Create a poem about Open Telemetry." - } - ], - "role": "user" - } - ] - } - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '92' - Content-Type: - - application/json - user-agent: - - google-genai-sdk/1.0.0 gl-python/3.12.8 - x-goog-api-client: - - - x-goog-user-project: - - - method: POST - uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-1.5-flash-002:streamGenerateContent?alt=sse - response: - body: - string: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"The\"}]}}],\"usageMetadata\": {},\"modelVersion\": \"gemini-1.5-flash-002\"\ - ,\"createTime\": \"2025-03-07T22:19:26.378633Z\",\"responseId\": \"7nDLZ4mOF_Hg-O4P7YfKqQ8\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \" black box whispers, secrets deep,\\nOf failing systems, promises\ - \ to keep.\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\"\ - : \"2025-03-07T22:19:26.378633Z\",\"responseId\": \"7nDLZ4mOF_Hg-O4P7YfKqQ8\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"\\nBut tracing's light, a guiding hand,\\nReveals the path\"\ - }]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:26.378633Z\"\ - ,\"responseId\": \"7nDLZ4mOF_Hg-O4P7YfKqQ8\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \", across the\ - \ land.\\n\\nOpen Telemetry, a beacon bright,\\nIlluminating pathways, day\ - \ and night.\\nFrom spans and traces, stories told,\"}]}}],\"modelVersion\"\ - : \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:26.378633Z\"\ - ,\"responseId\": \"7nDLZ4mOF_Hg-O4P7YfKqQ8\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\\nOf requests\ - \ flowing, brave and bold.\\n\\nThe metrics rise, a vibrant chart,\\nDisplaying\ - \ latency, a work of art.\\nEach request'\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\"\ - ,\"createTime\": \"2025-03-07T22:19:26.378633Z\",\"responseId\": \"7nDLZ4mOF_Hg-O4P7YfKqQ8\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"s journey, clearly shown,\\nWhere bottlenecks slumber, seeds\ - \ are sown.\\n\\nWith logs appended, context clear,\\nThe root of problems,\ - \ drawing near.\\nObservability's embrace, so wide,\\nUnraveling mysteries,\"\ - }]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:26.378633Z\"\ - ,\"responseId\": \"7nDLZ4mOF_Hg-O4P7YfKqQ8\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" deep inside.\\\ - n\\nFrom simple apps to complex weaves,\\nOpen Telemetry's power achieves,\\\ - nA unified vision, strong and true,\\nMonitoring systems, old and new.\\n\\\ - nNo vendor lock-in, free to roam,\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\"\ - ,\"createTime\": \"2025-03-07T22:19:26.378633Z\",\"responseId\": \"7nDLZ4mOF_Hg-O4P7YfKqQ8\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"\\nAcross the clouds, and find your home.\\nA standard rising,\ - \ strong and bold,\\nA future brighter, to behold.\\n\\nSo let the traces\ - \ flow and gleam,\\nOpen Telemetry, a vibrant dream.\\nOf healthy systems,\ - \ running free,\\nFor all to see, for all to be.\"}]}}],\"modelVersion\":\ - \ \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:26.378633Z\"\ - ,\"responseId\": \"7nDLZ4mOF_Hg-O4P7YfKqQ8\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\\n\"}]},\"\ - finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 8,\"\ - candidatesTokenCount\": 258,\"totalTokenCount\": 266,\"promptTokensDetails\"\ - : [{\"modality\": \"TEXT\",\"tokenCount\": 8}],\"candidatesTokensDetails\"\ - : [{\"modality\": \"TEXT\",\"tokenCount\": 258}]},\"modelVersion\": \"gemini-1.5-flash-002\"\ - ,\"createTime\": \"2025-03-07T22:19:26.378633Z\",\"responseId\": \"7nDLZ4mOF_Hg-O4P7YfKqQ8\"\ - }\r\n\r\n" - headers: - Content-Disposition: - - attachment - Content-Type: - - text/event-stream - Transfer-Encoding: - - chunked - Vary: - - Origin - - X-Origin - - Referer - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-XSS-Protection: - - '0' - status: - code: 200 - message: OK -version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-async-default].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-async-default].yaml new file mode 100644 index 0000000000..a26a593eed --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-async-default].yaml @@ -0,0 +1,116 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a poem about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '92' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:streamGenerateContent?alt=sse + response: + body: + string: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \"In tangled webs of cloud and code,\\nWhere microservices softly\ + \ strode,\\nA whispered secret, hard to find,\\nThe journey of a user's mind.\\\ + nA black box hummed, a mystery deep,\\nWhile crucial insights lay asleep.\\\ + n\\nBut then a vision, clear and bright,\\nE\"}]}}],\"usageMetadata\": {\"\ + trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-2.5-flash\",\"createTime\"\ + : \"2025-10-10T15:57:26.830111Z\",\"responseId\": \"5izpaJ_VMqednvgP2KWkqQ8\"\ + }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \"merged to chase away the night.\\n**OpenTelemetry**, a guiding\ + \ star,\\nTo show us where our problems are.\\nA standard banner, raised with\ + \ pride,\\nWhere all observability can ride.\\n\\nThrough **Traces**, golden\ + \ threads unfurl,\\nConnecting paths across the world.\\nFrom start to end,\ + \ a user'\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\"\ + : \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:57:26.830111Z\",\"\ + responseId\": \"5izpaJ_VMqednvgP2KWkqQ8\"}\r\n\r\ndata: {\"candidates\": [{\"\ + content\": {\"role\": \"model\",\"parts\": [{\"text\": \"s plea,\\nA chain\ + \ of **Spans**, for all to see.\\nContext propagated, strong and true,\\nRevealing\ + \ what each service _do_.\\n\\nAnd **Metrics**, count and watch and chart,\\\ + nThe system's pulse, its beating heart.\\nLatency's grace, or error's bite,\\\ + nThrough\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\"\ + : \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:57:26.830111Z\",\"\ + responseId\": \"5izpaJ_VMqednvgP2KWkqQ8\"}\r\n\r\ndata: {\"candidates\": [{\"\ + content\": {\"role\": \"model\",\"parts\": [{\"text\": \" gauges, sums, and\ + \ counters bright.\\nThe CPU's hum, the memory's strain,\\nMade visible, again,\ + \ again.\\n\\nWith **Logs**, the whispered tales are told,\\nEach event, a\ + \ story to unfold.\\nStructured lines, with context deep,\\nThe secrets that\ + \ the services keep.\\nCor\"}]}}],\"usageMetadata\": {\"trafficType\": \"\ + ON_DEMAND\"},\"modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:57:26.830111Z\"\ + ,\"responseId\": \"5izpaJ_VMqednvgP2KWkqQ8\"}\r\n\r\ndata: {\"candidates\"\ + : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"related, timestamped,\ + \ and clear,\\nDispelling doubt, and calming fear.\\n\\nNo vendor lock, no\ + \ walled-off maze,\\nJust open standards, through the haze.\\nA common language,\ + \ strong and free,\\nFor all your signals, *three-in-three*.\\nFrom instrumenting,\ + \ to\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\"\ + : \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:57:26.830111Z\",\"\ + responseId\": \"5izpaJ_VMqednvgP2KWkqQ8\"}\r\n\r\ndata: {\"candidates\": [{\"\ + content\": {\"role\": \"model\",\"parts\": [{\"text\": \" collect and send,\\\ + nA journey without a bitter end.\\n\\nIt gathers data, rich and wide,\\nFrom\ + \ every corner, deep inside.\\nThen sends it forth, for you to glean,\\nThe\ + \ health and state of each machine.\\nTo troubleshoot fast, with knowing eyes,\\\ + nAnd make your complex system rise.\\n\\nSo\"}]}}],\"usageMetadata\": {\"\ + trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-2.5-flash\",\"createTime\"\ + : \"2025-10-10T15:57:26.830111Z\",\"responseId\": \"5izpaJ_VMqednvgP2KWkqQ8\"\ + }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \" hail to OTel, the guiding hand,\\nThat brings observability\ + \ to command.\\nA unified vision, strong and true,\\nFor clarity, for me,\ + \ for you!\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\"\ + : 8,\"candidatesTokenCount\": 437,\"totalTokenCount\": 2472,\"trafficType\"\ + : \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\"\ + : 8}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\"\ + : 437}],\"thoughtsTokenCount\": 2027},\"modelVersion\": \"gemini-2.5-flash\"\ + ,\"createTime\": \"2025-10-10T15:57:26.830111Z\",\"responseId\": \"5izpaJ_VMqednvgP2KWkqQ8\"\ + }\r\n\r\n" + headers: + Content-Disposition: + - attachment + Content-Type: + - text/event-stream + Date: + - Fri, 10 Oct 2025 15:57:39 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml new file mode 100644 index 0000000000..674d9ebc64 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml @@ -0,0 +1,109 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a poem about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '92' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:streamGenerateContent?alt=sse + response: + body: + string: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \"In digital realms, where systems intertwine,\\nA complex dance,\ + \ a labyrinthine design.\\nMicroservices hum, a scattered, vast array,\\nWhere\ + \ issues hide, and secrets hold their sway.\\n\\nBut darkness lifts, a beacon\ + \ takes its stand,\\nA guiding star, across the digital land.\\n**OpenTelemetry**,\"\ + }]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\"\ + : \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:57:12.055169Z\",\"\ + responseId\": \"2CzpaIGvA4C4nvgPk77D6Ak\"}\r\n\r\ndata: {\"candidates\": [{\"\ + content\": {\"role\": \"model\",\"parts\": [{\"text\": \" its noble, open\ + \ name,\\nTo cast out shadows, stoke the insights' flame.\\n\\nIt tracks each\ + \ path, a thread of light so fine,\\nFrom user's click to logic deep inside.\\\ + nWith **Traces** woven, spans that link and blend,\\nThe journey's story,\ + \ from its start to end\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"\ + },\"modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:57:12.055169Z\"\ + ,\"responseId\": \"2CzpaIGvA4C4nvgPk77D6Ak\"}\r\n\r\ndata: {\"candidates\"\ + : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \".\\n\\nThen\ + \ **Metrics** rise, in numbers, stark and clear,\\nThe pulse, the health,\ + \ banishing all fear.\\nCounters grow, gauges shift and fall,\\nA system's\ + \ heartbeat, answering every call.\\n\\nAnd **Logs** emerge, with details\ + \ rich and deep,\\nEvents recorded, secrets they\"}]}}],\"usageMetadata\"\ + : {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-2.5-flash\"\ + ,\"createTime\": \"2025-10-10T15:57:12.055169Z\",\"responseId\": \"2CzpaIGvA4C4nvgPk77D6Ak\"\ + }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \" will keep.\\nContext bound, to trace and metric too,\\nA\ + \ holistic view, for all the world to view.\\n\\nNo vendor chains, no silos,\ + \ built so high,\\nA standard set, beneath the open sky.\\nPortable data,\ + \ free to flow and roam,\\nTo any backend, finding its true\"}]}}],\"usageMetadata\"\ + : {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-2.5-flash\"\ + ,\"createTime\": \"2025-10-10T15:57:12.055169Z\",\"responseId\": \"2CzpaIGvA4C4nvgPk77D6Ak\"\ + }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \" home.\\n\\nThrough SDKs and agents, smart and keen,\\nIt\ + \ weaves its magic, often quite unseen.\\nInstrumentation, gentle, firm, and\ + \ bright,\\nBringing clarity to the darkest night.\\n\\nSo understand your\ + \ code, its ebb and flow,\\nThe why, the how, the seeds that errors sow.\\\ + n**\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\"\ + : \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:57:12.055169Z\",\"\ + responseId\": \"2CzpaIGvA4C4nvgPk77D6Ak\"}\r\n\r\ndata: {\"candidates\": [{\"\ + content\": {\"role\": \"model\",\"parts\": [{\"text\": \"OpenTelemetry**,\ + \ a truth-revealing art,\\nTo build with wisdom, and a confident heart.\"\ + }]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\"\ + : 8,\"candidatesTokenCount\": 354,\"totalTokenCount\": 2064,\"trafficType\"\ + : \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\"\ + : 8}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\"\ + : 354}],\"thoughtsTokenCount\": 1702},\"modelVersion\": \"gemini-2.5-flash\"\ + ,\"createTime\": \"2025-10-10T15:57:12.055169Z\",\"responseId\": \"2CzpaIGvA4C4nvgPk77D6Ak\"\ + }\r\n\r\n" + headers: + Content-Disposition: + - attachment + Content-Type: + - text/event-stream + Date: + - Fri, 10 Oct 2025 15:57:22 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-async].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-async].yaml deleted file mode 100644 index 9a068aae89..0000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-async].yaml +++ /dev/null @@ -1,99 +0,0 @@ -interactions: -- request: - body: |- - { - "contents": [ - { - "parts": [ - { - "text": "Create a poem about Open Telemetry." - } - ], - "role": "user" - } - ] - } - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '92' - Content-Type: - - application/json - user-agent: - - google-genai-sdk/1.0.0 gl-python/3.12.8 - x-goog-api-client: - - - x-goog-user-project: - - - method: POST - uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-1.5-flash-002:streamGenerateContent?alt=sse - response: - body: - string: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"No\"}]}}],\"usageMetadata\": {},\"modelVersion\": \"gemini-1.5-flash-002\"\ - ,\"createTime\": \"2025-03-07T22:19:23.579184Z\",\"responseId\": \"63DLZ_CsI_Hg-O4P7YfKqQ8\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \" more dark logs, a cryptic, silent scream,\\nNo more the hunt\ - \ for\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"\ - 2025-03-07T22:19:23.579184Z\",\"responseId\": \"63DLZ_CsI_Hg-O4P7YfKqQ8\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \" errors, a lost, fading dream.\\nOpen Telemetry, a beacon\ - \ in\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"\ - 2025-03-07T22:19:23.579184Z\",\"responseId\": \"63DLZ_CsI_Hg-O4P7YfKqQ8\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \" the night,\\nShining forth its data, clear and burning bright.\\\ - n\\nFrom traces spanning systems, a flowing, silver thread,\\nMetrics pulse\ - \ and measure,\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\"\ - : \"2025-03-07T22:19:23.579184Z\",\"responseId\": \"63DLZ_CsI_Hg-O4P7YfKqQ8\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \" insights finely spread.\\nLogs enriched with context, a story\ - \ they unfold,\\nOf requests and responses, both brave and bold.\\n\\nObservability's\ - \ promise\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\"\ - : \"2025-03-07T22:19:23.579184Z\",\"responseId\": \"63DLZ_CsI_Hg-O4P7YfKqQ8\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \", a future now at hand,\\nWith vendors interoperable, a collaborative\ - \ band.\\nNo longer vendor lock-in, a restrictive, iron cage,\\nBut freedom\ - \ of selection, turning a new page.\\n\\nFrom microservices humming,\"}]}}],\"\ - modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:23.579184Z\"\ - ,\"responseId\": \"63DLZ_CsI_Hg-O4P7YfKqQ8\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" a symphony\ - \ of calls,\\nTo monolithic giants, answering their thralls,\\nOpen Telemetry\ - \ watches, with keen and watchful eye,\\nDetecting the anomalies, before they\ - \ rise and fly.\\n\\nSo let the data flow freely, a\"}]}}],\"modelVersion\"\ - : \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:23.579184Z\"\ - ,\"responseId\": \"63DLZ_CsI_Hg-O4P7YfKqQ8\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" river strong\ - \ and deep,\\nIts secrets it will whisper, while the systems sleep.\\nOpen\ - \ Telemetry's power, a force that we can wield,\\nTo build more stable systems,\ - \ in the digital field.\\n\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\"\ - : {\"promptTokenCount\": 8,\"candidatesTokenCount\": 238,\"totalTokenCount\"\ - : 246,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 8}],\"\ - candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 238}]},\"\ - modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:23.579184Z\"\ - ,\"responseId\": \"63DLZ_CsI_Hg-O4P7YfKqQ8\"}\r\n\r\n" - headers: - Content-Disposition: - - attachment - Content-Type: - - text/event-stream - Transfer-Encoding: - - chunked - Vary: - - Origin - - X-Origin - - Referer - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-XSS-Protection: - - '0' - status: - code: 200 - message: OK -version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml deleted file mode 100644 index 669f1af93b..0000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-1.5-flash-002-vertexaiapi-sync].yaml +++ /dev/null @@ -1,99 +0,0 @@ -interactions: -- request: - body: |- - { - "contents": [ - { - "parts": [ - { - "text": "Create a poem about Open Telemetry." - } - ], - "role": "user" - } - ] - } - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - Connection: - - keep-alive - Content-Length: - - '92' - Content-Type: - - application/json - user-agent: - - google-genai-sdk/1.0.0 gl-python/3.12.8 - x-goog-api-client: - - - x-goog-user-project: - - - method: POST - uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-1.5-flash-002:streamGenerateContent?alt=sse - response: - body: - string: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"No\"}]}}],\"usageMetadata\": {},\"modelVersion\": \"gemini-1.5-flash-002\"\ - ,\"createTime\": \"2025-03-07T22:19:20.770456Z\",\"responseId\": \"6HDLZ5iDL86F3NoPzaTkiQU\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \" more dark, mysterious traces,\\nNo more guessing, in time\ - \ and spaces.\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\"\ - : \"2025-03-07T22:19:20.770456Z\",\"responseId\": \"6HDLZ5iDL86F3NoPzaTkiQU\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"\\nOpen Telemetry's light shines bright,\\nIlluminating the\ - \ code'\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\":\ - \ \"2025-03-07T22:19:20.770456Z\",\"responseId\": \"6HDLZ5iDL86F3NoPzaTkiQU\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"s dark night.\\n\\nFrom spans and metrics, a story told,\\\ - nOf requests flowing, both brave and bold.\\nTraces weaving, a tapestry grand,\"\ - }]}}],\"modelVersion\": \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:20.770456Z\"\ - ,\"responseId\": \"6HDLZ5iDL86F3NoPzaTkiQU\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\\nShowing\ - \ performance, across the land.\\n\\nLogs and metrics, a perfect blend,\\\ - nInformation's flow, without end.\\nObservability's promise\"}]}}],\"modelVersion\"\ - : \"gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:20.770456Z\"\ - ,\"responseId\": \"6HDLZ5iDL86F3NoPzaTkiQU\"}\r\n\r\ndata: {\"candidates\"\ - : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \", clear and\ - \ true,\\nInsights revealed, for me and you.\\n\\nJaeger, Zipkin, a chorus\ - \ sings,\\nWith exporters ready, for all the things.\\nFrom simple apps to\ - \ systems vast,\\nOpen Telemetry'\"}]}}],\"modelVersion\": \"gemini-1.5-flash-002\"\ - ,\"createTime\": \"2025-03-07T22:19:20.770456Z\",\"responseId\": \"6HDLZ5iDL86F3NoPzaTkiQU\"\ - }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ - : [{\"text\": \"s power will last.\\n\\nNo vendor lock-in, a freedom sweet,\\\ - nOpen source glory, can't be beat.\\nSo let us embrace, this modern way,\\\ - nTo monitor systems, come what may.\\n\\nFrom\"}]}}],\"modelVersion\": \"\ - gemini-1.5-flash-002\",\"createTime\": \"2025-03-07T22:19:20.770456Z\",\"\ - responseId\": \"6HDLZ5iDL86F3NoPzaTkiQU\"}\r\n\r\ndata: {\"candidates\": [{\"\ - content\": {\"role\": \"model\",\"parts\": [{\"text\": \" microservices, small\ - \ and slight,\\nTo monolithic giants, shining bright,\\nOpen Telemetry shows\ - \ the path,\\nTo understand, and fix the wrath,\\nOf latency demons, lurking\ - \ near,\\nBringing clarity, year after year.\\n\"}]},\"finishReason\": \"\ - STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 8,\"candidatesTokenCount\"\ - : 242,\"totalTokenCount\": 250,\"promptTokensDetails\": [{\"modality\": \"\ - TEXT\",\"tokenCount\": 8}],\"candidatesTokensDetails\": [{\"modality\": \"\ - TEXT\",\"tokenCount\": 242}]},\"modelVersion\": \"gemini-1.5-flash-002\",\"\ - createTime\": \"2025-03-07T22:19:20.770456Z\",\"responseId\": \"6HDLZ5iDL86F3NoPzaTkiQU\"\ - }\r\n\r\n" - headers: - Content-Disposition: - - attachment - Content-Type: - - text/event-stream - Transfer-Encoding: - - chunked - Vary: - - Origin - - X-Origin - - Referer - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-XSS-Protection: - - '0' - status: - code: 200 - message: OK -version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-2.5-flash-vertexaiapi-async-default].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-2.5-flash-vertexaiapi-async-default].yaml new file mode 100644 index 0000000000..d66466a25d --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-2.5-flash-vertexaiapi-async-default].yaml @@ -0,0 +1,96 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a poem about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '92' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:streamGenerateContent?alt=sse + response: + body: + string: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \"In digital realms, where tangled systems weave,\\nAnd microservices\ + \ hum, our trust they receive,\\nA complex dance of logic, fast and grand,\\\ + nTo truly see their heart, a guiding hand.\\n\\nFor this, Open Telemetry takes\ + \ its stand,\\nA common tongue across the digital land.\\nWith\"}]}}],\"usageMetadata\"\ + : {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-2.5-flash\"\ + ,\"createTime\": \"2025-10-10T15:57:01.802582Z\",\"responseId\": \"zSzpaJb-MPXZnvgPisGa2A0\"\ + }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \" **Traces**, pathways clear, from start to end they sweep,\\\ + nUnveiling journeys where the secrets sleep.\\n\\nAnd **Metrics** rise, in\ + \ numbers, stark and true,\\nCPU's soft hum, latency's quick, measured queue.\\\ + nWhile **Logs** recount each moment, event, and\"}]}}],\"usageMetadata\":\ + \ {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-2.5-flash\"\ + ,\"createTime\": \"2025-10-10T15:57:01.802582Z\",\"responseId\": \"zSzpaJb-MPXZnvgPisGa2A0\"\ + }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \" plight,\\nIlluminating shadows with their burning light.\\\ + n\\nNo longer bound by proprietary sway,\\nIt gathers data, come what vendor\ + \ may.\\nAn open hand, a standard, strong and free,\\nFor every language,\ + \ for each API.\\n\\nFrom failing links to bottlenecks unseen,\\nIt paints\ + \ the picture, vibrant\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"\ + },\"modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:57:01.802582Z\"\ + ,\"responseId\": \"zSzpaJb-MPXZnvgPisGa2A0\"}\r\n\r\ndata: {\"candidates\"\ + : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" and serene.\\\ + nEmpowering insight, from the core to edge,\\nA clear advantage, every silent\ + \ pledge.\\n\\nSo hail the standard, unifying and bright,\\nThat brings our\ + \ complex systems into light.\\nOpen Telemetry, a bridge across the maze,\\\ + nUnlocking wisdom in these data-driven days.\"}]},\"finishReason\": \"STOP\"\ + }],\"usageMetadata\": {\"promptTokenCount\": 8,\"candidatesTokenCount\": 257,\"\ + totalTokenCount\": 1522,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\"\ + : [{\"modality\": \"TEXT\",\"tokenCount\": 8}],\"candidatesTokensDetails\"\ + : [{\"modality\": \"TEXT\",\"tokenCount\": 257}],\"thoughtsTokenCount\": 1257},\"\ + modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:57:01.802582Z\"\ + ,\"responseId\": \"zSzpaJb-MPXZnvgPisGa2A0\"}\r\n\r\n" + headers: + Content-Disposition: + - attachment + Content-Type: + - text/event-stream + Date: + - Fri, 10 Oct 2025 15:57:09 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml new file mode 100644 index 0000000000..25d9819020 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_streaming[logcontent-gemini-2.5-flash-vertexaiapi-sync-default].yaml @@ -0,0 +1,110 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a poem about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '92' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:streamGenerateContent?alt=sse + response: + body: + string: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \"When systems hum, a silent, complex art,\\nWith hidden errors\ + \ playing their part,\\nA need arose, a vision clear and bold,\\nFor stories\ + \ of our code, bravely told.\\n\\nOpenTelemetry, the name we praise,\\nIlluminating\ + \ all our digital haze.\\nIt brings observability to light,\\nTurning darkness\"\ + }]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\"\ + : \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:56:46.229905Z\",\"\ + responseId\": \"vizpaJGEDvXZnvgPisGa2A0\"}\r\n\r\ndata: {\"candidates\": [{\"\ + content\": {\"role\": \"model\",\"parts\": [{\"text\": \" into vibrant sight.\\\ + n\\nFirst, **Traces** flow, a thread so fine,\\nThrough service calls, a clear\ + \ design.\\nEach span a step, from start to end,\\nA journey mapped, a faithful\ + \ friend.\\nThey show the path, where data goes astray,\\nAnd pinpoint latency,\ + \ come what may.\\n\\n\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"\ + },\"modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:56:46.229905Z\"\ + ,\"responseId\": \"vizpaJGEDvXZnvgPisGa2A0\"}\r\n\r\ndata: {\"candidates\"\ + : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"Then **Metrics**\ + \ rise, in steady stream,\\nOf counters, gauges, like a dream.\\nThey show\ + \ us load, and throughput's grace,\\nThe pulse and rhythm of the place.\\\ + nCPU usage, memory's hold,\\nPerformance tales, in numbers told.\\n\\nWhile\ + \ **Logs** detail, each critical event,\"}]}}],\"usageMetadata\": {\"trafficType\"\ + : \"ON_DEMAND\"},\"modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"\ + 2025-10-10T15:56:46.229905Z\",\"responseId\": \"vizpaJGEDvXZnvgPisGa2A0\"\ + }\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\"\ + : [{\"text\": \"\\nWith context rich, on purpose sent.\\nFor every error,\ + \ warning, or success,\\nThey fill the gaps, with clear address.\\nStructured\ + \ entries, timestamped and deep,\\nSecrets that our applications keep.\\n\\\ + nFrom code embedded, agent deployed,\\nData's collected, gaps avoided.\\nTo\ + \ open standards,\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"\ + },\"modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:56:46.229905Z\"\ + ,\"responseId\": \"vizpaJGEDvXZnvgPisGa2A0\"}\r\n\r\ndata: {\"candidates\"\ + : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" freely bound,\\\ + nNo vendor lock-in can be found.\\nA common language, understood by all,\\\ + nAnswering the complex system's call.\\n\\nFor troubleshooting, deep and wide,\\\ + nWhere hidden problems used to hide,\\nWe find the root, with swift command,\\\ + nAnd build robust systems,\"}]}}],\"usageMetadata\": {\"trafficType\": \"\ + ON_DEMAND\"},\"modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:56:46.229905Z\"\ + ,\"responseId\": \"vizpaJGEDvXZnvgPisGa2A0\"}\r\n\r\ndata: {\"candidates\"\ + : [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" close at hand.\\\ + nA tapestry of insight, woven bright,\\nGuiding our development day and night.\\\ + n\\nA unified vision, strong and free,\\nFor modern systems' destiny.\\nOpenTelemetry,\ + \ a guiding light,\\nMakes complex software shine so bright.\"}]},\"finishReason\"\ + : \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 8,\"candidatesTokenCount\"\ + : 388,\"totalTokenCount\": 2589,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\"\ + : [{\"modality\": \"TEXT\",\"tokenCount\": 8}],\"candidatesTokensDetails\"\ + : [{\"modality\": \"TEXT\",\"tokenCount\": 388}],\"thoughtsTokenCount\": 2193},\"\ + modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"2025-10-10T15:56:46.229905Z\"\ + ,\"responseId\": \"vizpaJGEDvXZnvgPisGa2A0\"}\r\n\r\n" + headers: + Content-Disposition: + - attachment + Content-Type: + - text/event-stream + Date: + - Fri, 10 Oct 2025 15:56:58 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-async-enable_completion_hook-experimental].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-async-enable_completion_hook-experimental].yaml new file mode 100644 index 0000000000..60077ccb2c --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-async-enable_completion_hook-experimental].yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a haiku about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '93' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:generateContent + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "Open data streams,\nMetrics, logs, and traces flow,\nClearly see inside." + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -5.934557172987196 + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 18, + "totalTokenCount": 459, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 8 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 18 + } + ], + "thoughtsTokenCount": 433 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-10-10T16:32:45.350496Z", + "responseId": "LTXpaKCyFdPlnvgPuajSiQQ" + } + headers: + Accept-Ranges: + - none + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 10 Oct 2025 16:32:59 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - X-Origin + - Referer + - Origin,Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-sync-enable_completion_hook-experimental].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-sync-enable_completion_hook-experimental].yaml new file mode 100644 index 0000000000..53b23d8161 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[excludecontent-gemini-2.5-flash-vertexaiapi-sync-enable_completion_hook-experimental].yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a haiku about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '93' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:generateContent + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "Open data streams,\nMetrics, logs, and traces flow,\nClearly see inside." + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -28.687652587890625 + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 20, + "totalTokenCount": 1341, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 8 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 20 + } + ], + "thoughtsTokenCount": 1313 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-10-10T16:32:34.499409Z", + "responseId": "IjXpaNG9HoC4nvgPk77D6Ak" + } + headers: + Accept-Ranges: + - none + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 10 Oct 2025 16:32:42 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - X-Origin + - Referer + - Origin,Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-async-enable_completion_hook-experimental].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-async-enable_completion_hook-experimental].yaml new file mode 100644 index 0000000000..31af66797a --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-async-enable_completion_hook-experimental].yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a haiku about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '93' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:generateContent + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "Open data streams,\nMetrics, logs, and traces flow,\nClearly see inside." + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -6.327557373046875 + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 20, + "totalTokenCount": 475, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 8 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 20 + } + ], + "thoughtsTokenCount": 447 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-10-10T16:32:28.805463Z", + "responseId": "HDXpaNeUMaeBnvgPiJ6P-Ao" + } + headers: + Accept-Ranges: + - none + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 10 Oct 2025 16:32:31 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - X-Origin + - Referer + - Origin,Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-sync-enable_completion_hook-experimental].yaml b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-sync-enable_completion_hook-experimental].yaml new file mode 100644 index 0000000000..1c0a0275cc --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/cassettes/test_upload_hook_non_streaming[logcontent-gemini-2.5-flash-vertexaiapi-sync-enable_completion_hook-experimental].yaml @@ -0,0 +1,102 @@ +interactions: +- request: + body: |- + { + "contents": [ + { + "parts": [ + { + "text": "Create a haiku about Open Telemetry." + } + ], + "role": "user" + } + ] + } + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + content-length: + - '93' + content-type: + - application/json + host: + - us-central1-aiplatform.googleapis.com + user-agent: + - google-genai-sdk/1.32.0 gl-python/3.10.18 + x-goog-api-client: + - + x-goog-user-project: + - + method: POST + uri: https://test-location-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/test-location/publishers/google/models/gemini-2.5-flash:generateContent + response: + body: + string: |- + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "Open data streams,\nMetrics, logs, and traces flow,\nClearly see inside." + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -10.948673672146267 + } + ], + "usageMetadata": { + "promptTokenCount": 8, + "candidatesTokenCount": 18, + "totalTokenCount": 663, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 8 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 18 + } + ], + "thoughtsTokenCount": 637 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-10-10T16:32:22.100847Z", + "responseId": "FjXpaO-TBoC4nvgPk77D6Ak" + } + headers: + Accept-Ranges: + - none + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 10 Oct 2025 16:32:25 GMT + Server: + - scaffolding on HTTPServer2 + Transfer-Encoding: + - chunked + Vary: + - X-Origin + - Referer + - Origin,Accept-Encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/nonstreaming_base.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/nonstreaming_base.py index 39f1dfe927..4ae6f00063 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/nonstreaming_base.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/nonstreaming_base.py @@ -13,8 +13,21 @@ # limitations under the License. import json -import os import unittest +from unittest.mock import patch + +from google.genai.types import GenerateContentConfig + +from opentelemetry._events import Event +from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _StabilityMode, +) +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes, +) +from opentelemetry.util.genai.types import ContentCapturingMode from .base import TestCase @@ -111,10 +124,11 @@ def test_generated_span_counts_tokens(self): self.assertEqual(span.attributes["gen_ai.usage.input_tokens"], 123) self.assertEqual(span.attributes["gen_ai.usage.output_tokens"], 456) + @patch.dict( + "os.environ", + {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true"}, + ) def test_records_system_prompt_as_log(self): - os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = ( - "true" - ) config = {"system_instruction": "foo"} self.configure_valid_response() self.generate_content( @@ -125,10 +139,11 @@ def test_records_system_prompt_as_log(self): self.assertEqual(event_record.attributes["gen_ai.system"], "gemini") self.assertEqual(event_record.body["content"], "foo") + @patch.dict( + "os.environ", + {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "false"}, + ) def test_does_not_record_system_prompt_as_log_if_disabled_by_env(self): - os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = ( - "false" - ) config = {"system_instruction": "foo"} self.configure_valid_response() self.generate_content( @@ -139,20 +154,22 @@ def test_does_not_record_system_prompt_as_log_if_disabled_by_env(self): self.assertEqual(event_record.attributes["gen_ai.system"], "gemini") self.assertEqual(event_record.body["content"], "") + @patch.dict( + "os.environ", + {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true"}, + ) def test_does_not_record_system_prompt_as_log_if_no_system_prompt_present( self, ): - os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = ( - "true" - ) self.configure_valid_response() self.generate_content(model="gemini-2.0-flash", contents="Some input") self.otel.assert_does_not_have_event_named("gen_ai.system.message") + @patch.dict( + "os.environ", + {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true"}, + ) def test_records_user_prompt_as_log(self): - os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = ( - "true" - ) self.configure_valid_response() self.generate_content(model="gemini-2.0-flash", contents="Some input") self.otel.assert_has_event_named("gen_ai.user.message") @@ -160,10 +177,11 @@ def test_records_user_prompt_as_log(self): self.assertEqual(event_record.attributes["gen_ai.system"], "gemini") self.assertEqual(event_record.body["content"], "Some input") + @patch.dict( + "os.environ", + {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "false"}, + ) def test_does_not_record_user_prompt_as_log_if_disabled_by_env(self): - os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = ( - "false" - ) self.configure_valid_response() self.generate_content(model="gemini-2.0-flash", contents="Some input") self.otel.assert_has_event_named("gen_ai.user.message") @@ -171,10 +189,11 @@ def test_does_not_record_user_prompt_as_log_if_disabled_by_env(self): self.assertEqual(event_record.attributes["gen_ai.system"], "gemini") self.assertEqual(event_record.body["content"], "") + @patch.dict( + "os.environ", + {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true"}, + ) def test_records_response_as_log(self): - os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = ( - "true" - ) self.configure_valid_response(text="Some response content") self.generate_content(model="gemini-2.0-flash", contents="Some input") self.otel.assert_has_event_named("gen_ai.choice") @@ -184,10 +203,11 @@ def test_records_response_as_log(self): "Some response content", json.dumps(event_record.body["content"]) ) + @patch.dict( + "os.environ", + {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "false"}, + ) def test_does_not_record_response_as_log_if_disabled_by_env(self): - os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = ( - "false" - ) self.configure_valid_response(text="Some response content") self.generate_content(model="gemini-2.0-flash", contents="Some input") self.otel.assert_has_event_named("gen_ai.choice") @@ -195,6 +215,181 @@ def test_does_not_record_response_as_log_if_disabled_by_env(self): self.assertEqual(event_record.attributes["gen_ai.system"], "gemini") self.assertEqual(event_record.body["content"], "") + def test_new_semconv_record_completion_as_log(self): + for mode in ContentCapturingMode: + patched_environ = patch.dict( + "os.environ", + { + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": mode.name, + "OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental", + }, + ) + patched_otel_mapping = patch.dict( + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING, + { + _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + }, + ) + content = "Some input" + output = "Some response content" + sys_instr = "System instruction" + with self.subTest( + f"mode: {mode}", patched_environ=patched_environ + ): + self.setUp() + with patched_environ, patched_otel_mapping: + self.configure_valid_response(text=output) + self.generate_content( + model="gemini-2.0-flash", + contents=content, + config=GenerateContentConfig( + system_instruction=sys_instr + ), + ) + self.otel.assert_has_event_named( + "gen_ai.client.inference.operation.details" + ) + event = self.otel.get_event_named( + "gen_ai.client.inference.operation.details" + ) + if mode in [ + ContentCapturingMode.NO_CONTENT, + ContentCapturingMode.SPAN_ONLY, + ]: + self.assertNotIn( + gen_ai_attributes.GEN_AI_INPUT_MESSAGES, + event.attributes, + ) + self.assertNotIn( + gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES, + event.attributes, + ) + self.assertNotIn( + gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS, + event.attributes, + ) + else: + attrs = { + gen_ai_attributes.GEN_AI_INPUT_MESSAGES: ( + { + "role": "user", + "parts": ( + {"content": content, "type": "text"}, + ), + }, + ), + gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES: ( + { + "role": "assistant", + "parts": ( + {"content": output, "type": "text"}, + ), + "finish_reason": "", + }, + ), + gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS: ( + {"content": sys_instr, "type": "text"}, + ), + } + expected_event = Event( + "gen_ai.client.inference.operation.details", + attributes=attrs, + ) + self.assertEqual( + event.attributes[ + gen_ai_attributes.GEN_AI_INPUT_MESSAGES + ], + expected_event.attributes[ + gen_ai_attributes.GEN_AI_INPUT_MESSAGES + ], + ) + self.assertEqual( + event.attributes[ + gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES + ], + expected_event.attributes[ + gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES + ], + ) + self.assertEqual( + event.attributes[ + gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS + ], + expected_event.attributes[ + gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS + ], + ) + self.tearDown() + + def test_new_semconv_record_completion_in_span(self): + for mode in ContentCapturingMode: + patched_environ = patch.dict( + "os.environ", + { + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": mode.name, + "OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental", + }, + ) + patched_otel_mapping = patch.dict( + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING, + { + _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + }, + ) + with self.subTest( + f"mode: {mode}", patched_environ=patched_environ + ): + self.setUp() + with patched_environ, patched_otel_mapping: + self.configure_valid_response(text="Some response content") + self.generate_content( + model="gemini-2.0-flash", + contents="Some input", + config=GenerateContentConfig( + system_instruction="System instruction" + ), + ) + span = self.otel.get_span_named( + "generate_content gemini-2.0-flash" + ) + if mode in [ + ContentCapturingMode.SPAN_ONLY, + ContentCapturingMode.SPAN_AND_EVENT, + ]: + self.assertEqual( + span.attributes[ + gen_ai_attributes.GEN_AI_INPUT_MESSAGES + ], + '[{"role":"user","parts":[{"content":"Some input","type":"text"}]}]', + ) + self.assertEqual( + span.attributes[ + gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES + ], + '[{"role":"assistant","parts":[{"content":"Some response content","type":"text"}],"finish_reason":""}]', + ) + self.assertEqual( + span.attributes[ + gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS + ], + '[{"content":"System instruction","type":"text"}]', + ) + else: + self.assertNotIn( + gen_ai_attributes.GEN_AI_INPUT_MESSAGES, + span.attributes, + ) + self.assertNotIn( + gen_ai_attributes.GEN_AI_OUTPUT_MESSAGES, + span.attributes, + ) + self.assertNotIn( + gen_ai_attributes.GEN_AI_SYSTEM_INSTRUCTIONS, + span.attributes, + ) + + self.tearDown() + def test_records_metrics_data(self): self.configure_valid_response() self.generate_content(model="gemini-2.0-flash", contents="Some input") diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_e2e.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_e2e.py index afe4dbfe6b..36802b65dc 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_e2e.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_e2e.py @@ -29,17 +29,30 @@ import os import subprocess import sys +import time +import fsspec import google.auth import google.auth.credentials import google.genai import pytest import yaml +from google.genai import types from vcr.record_mode import RecordMode +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _StabilityMode, +) from opentelemetry.instrumentation.google_genai import ( GoogleGenAiSdkInstrumentor, ) +from opentelemetry.util.genai.environment_variables import ( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, + OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH, +) from ..common.auth import FakeCredentials from ..common.otel_mocker import OTelMocker @@ -308,8 +321,25 @@ def fixture_instrumentor(): return GoogleGenAiSdkInstrumentor() +@pytest.fixture(name="enable_completion_hook") +def fixture_enable_completion_hook(request): + return getattr(request, "param", "default") + + +@pytest.fixture(name="semconv_version") +def fixture_semconv_version(request): + return getattr(request, "param", "default") + + @pytest.fixture(name="internal_instrumentation_setup", autouse=True) -def fixture_setup_instrumentation(instrumentor): +def fixture_setup_instrumentation(instrumentor, enable_completion_hook): + if enable_completion_hook == "enable_completion_hook": + os.environ.update( + { + OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH: "memory://", + "OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK": "upload", + } + ) instrumentor.instrument() yield instrumentor.uninstrument() @@ -328,11 +358,30 @@ def fixture_otel_mocker(): autouse=True, params=["logcontent", "excludecontent"], ) -def fixture_setup_content_recording(request): +def fixture_setup_content_recording(request, semconv_version): enabled = request.param == "logcontent" - os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = str( - enabled - ) + # due to some init weirdness, this needs to be updated manually to work, and later restored, + # otherwise, state of this dict leaks to other tests and breaks them. + orig_dict = _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.copy() + if semconv_version == "experimental": + capture_content = "SPAN_AND_EVENT" if enabled else "NO_CONTENT" + os.environ.update( + { + OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental", + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: capture_content, + } + ) + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.update( + { + _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + } + ) + else: + os.environ[OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT] = str( + enabled + ) + yield + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = orig_dict @pytest.fixture(name="vcr_record_mode") @@ -389,7 +438,14 @@ def fixture_gcloud_api_key(gemini_api_key): @pytest.fixture(name="nonvertex_client_factory") def fixture_nonvertex_client_factory(gemini_api_key): def _factory(): - return google.genai.Client(api_key=gemini_api_key, vertexai=False) + return google.genai.Client( + api_key=gemini_api_key, + vertexai=False, + http_options=types.HttpOptions( + # to prevent compression + headers={"accept-encoding": "identity"} + ), + ) return _factory @@ -404,6 +460,9 @@ def _factory(): project=gcloud_project, location=gcloud_location, credentials=gcloud_credentials, + http_options=types.HttpOptions( + headers={"accept-encoding": "identity"} + ), ) return _factory @@ -435,7 +494,7 @@ def fixture_is_async(request): return request.param == "async" -@pytest.fixture(name="model", params=["gemini-1.5-flash-002"]) +@pytest.fixture(name="model", params=["gemini-2.5-flash"]) def fixture_model(request): return request.param @@ -479,6 +538,7 @@ async def _gather_all(): return _sync_impl +@pytest.mark.parametrize("semconv_version", ["default"], indirect=True) @pytest.mark.vcr def test_non_streaming(generate_content, model, otel_mocker): response = generate_content( @@ -490,6 +550,7 @@ def test_non_streaming(generate_content, model, otel_mocker): otel_mocker.assert_has_span_named(f"generate_content {model}") +@pytest.mark.parametrize("semconv_version", ["default"], indirect=True) @pytest.mark.vcr def test_streaming(generate_content_stream, model, otel_mocker): count = 0 @@ -502,3 +563,59 @@ def test_streaming(generate_content_stream, model, otel_mocker): count += 1 assert count > 0 otel_mocker.assert_has_span_named(f"generate_content {model}") + + +@pytest.mark.parametrize("semconv_version", ["experimental"], indirect=True) +@pytest.mark.parametrize( + "enable_completion_hook", ["enable_completion_hook"], indirect=True +) +@pytest.mark.vcr +def test_upload_hook_non_streaming( + generate_content, model, otel_mocker: OTelMocker +): + expected_input = [ + { + "parts": [ + { + "content": "Create a haiku about Open Telemetry.", + "type": "text", + } + ], + "role": "user", + } + ] + expected_output = [ + { + "role": "assistant", + "parts": [ + { + "content": "Open data streams,\nMetrics, logs, and traces flow,\nClearly see inside.", + "type": "text", + } + ], + "finish_reason": "stop", + } + ] + _ = generate_content( + model=model, contents="Create a haiku about Open Telemetry." + ) + time.sleep(2) + + event = otel_mocker.get_event_named( + "gen_ai.client.inference.operation.details" + ) + assert_fsspec_equal( + event.attributes["gen_ai.input.messages_ref"], expected_input + ) + + span = otel_mocker.get_span_named(f"generate_content {model}") + assert_fsspec_equal( + span.attributes["gen_ai.output.messages_ref"], expected_output + ) + + +def assert_fsspec_equal(path, value): + # Hide this function and its calls from traceback. + __tracebackhide__ = True # pylint: disable=unused-variable + with fsspec.open(path, "r") as file: + assert json.load(file) == value diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py index 7e06422812..2dc0a3d633 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py @@ -16,6 +16,13 @@ import google.genai.types as genai_types +from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _StabilityMode, +) +from opentelemetry.util.genai.types import ContentCapturingMode + from .base import TestCase @@ -275,3 +282,161 @@ def somefunction(x, y=2): self.assertNotIn( "code.function.return.value", generated_span.attributes ) + + def test_new_semconv_tool_calls_record_parameter_values(self): + for mode in ContentCapturingMode: + calls = [] + patched_environ = patch.dict( + "os.environ", + { + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": mode.name, + "OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental", + }, + ) + patched_otel_mapping = patch.dict( + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING, + { + _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + }, + ) + with self.subTest( + f"mode: {mode}", patched_environ=patched_environ + ): + self.setUp() + with patched_environ, patched_otel_mapping: + + def handle(*args, **kwargs): + calls.append((args, kwargs)) # pylint: disable=cell-var-from-loop + return "some result" + + def somefunction(someparam, otherparam=2): + print( + "someparam=%s, otherparam=%s", + someparam, + otherparam, + ) + + self.mock_generate_content.side_effect = handle + self.client.models.generate_content( + model="some-model-name", + contents="Some content", + config={ + "tools": [somefunction], + }, + ) + self.assertEqual(len(calls), 1) + config = calls[0][1]["config"] + tools = config.tools + wrapped_somefunction = tools[0] + wrapped_somefunction(123, otherparam="abc") + self.otel.assert_has_span_named( + "execute_tool somefunction" + ) + generated_span = self.otel.get_span_named( + "execute_tool somefunction" + ) + self.assertEqual( + generated_span.attributes[ + "code.function.parameters.someparam.type" + ], + "int", + ) + self.assertEqual( + generated_span.attributes[ + "code.function.parameters.otherparam.type" + ], + "str", + ) + if mode in [ + ContentCapturingMode.SPAN_ONLY, + ContentCapturingMode.SPAN_AND_EVENT, + ]: + self.assertEqual( + generated_span.attributes[ + "code.function.parameters.someparam.value" + ], + 123, + ) + self.assertEqual( + generated_span.attributes[ + "code.function.parameters.otherparam.value" + ], + "abc", + ) + else: + self.assertNotIn( + "code.function.parameters.someparam.value", + generated_span.attributes, + ) + self.assertNotIn( + "code.function.parameters.otherparam.value", + generated_span.attributes, + ) + self.tearDown() + + def test_new_semconv_tool_calls_record_return_values(self): + for mode in ContentCapturingMode: + calls = [] + patched_environ = patch.dict( + "os.environ", + { + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": mode.name, + "OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental", + }, + ) + patched_otel_mapping = patch.dict( + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING, + { + _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + }, + ) + with self.subTest( + f"mode: {mode}", patched_environ=patched_environ + ): + self.setUp() + with patched_environ, patched_otel_mapping: + + def handle(*args, **kwargs): + calls.append((args, kwargs)) # pylint: disable=cell-var-from-loop + return "some result" + + def somefunction(x, y=2): + return x + y + + self.mock_generate_content.side_effect = handle + self.client.models.generate_content( + model="some-model-name", + contents="Some content", + config={ + "tools": [somefunction], + }, + ) + self.assertEqual(len(calls), 1) + config = calls[0][1]["config"] + tools = config.tools + wrapped_somefunction = tools[0] + wrapped_somefunction(123) + self.otel.assert_has_span_named( + "execute_tool somefunction" + ) + generated_span = self.otel.get_span_named( + "execute_tool somefunction" + ) + self.assertEqual( + generated_span.attributes["code.function.return.type"], + "int", + ) + if mode in [ + ContentCapturingMode.SPAN_ONLY, + ContentCapturingMode.SPAN_AND_EVENT, + ]: + self.assertIn( + "code.function.return.value", + generated_span.attributes, + ) + else: + self.assertNotIn( + "code.function.return.value", + generated_span.attributes, + ) + self.tearDown() diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.latest.txt b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.latest.txt index 32cf3422f5..7c7d511649 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.latest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.latest.txt @@ -41,9 +41,10 @@ pytest-asyncio==0.21.0 pytest-vcr==1.0.2 google-auth==2.38.0 -google-genai==1.0.0 +google-genai==1.32.0 # Install locally from the folder. This path is relative to the # root directory, given invocation from "tox" at root level. -e opentelemetry-instrumentation --e instrumentation-genai/opentelemetry-instrumentation-google-genai \ No newline at end of file +-e instrumentation-genai/opentelemetry-instrumentation-google-genai +-e util/opentelemetry-util-genai[upload] diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt index 39a8a44bc2..72e6bea128 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/requirements.oldest.txt @@ -20,7 +20,7 @@ pytest-asyncio==0.21.0 pytest-vcr==1.0.2 google-auth==2.15.0 -google-genai==1.0.0 +google-genai==1.32.0 opentelemetry-api==1.37.0 opentelemetry-sdk==1.37.0 opentelemetry-semantic-conventions==0.58b0 @@ -29,3 +29,4 @@ opentelemetry-instrumentation==0.58b0 # Install locally from the folder. This path is relative to the # root directory, given invocation from "tox" at root level. -e instrumentation-genai/opentelemetry-instrumentation-google-genai +-e util/opentelemetry-util-genai[upload] diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/utils/test_tool_call_wrapper.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/utils/test_tool_call_wrapper.py index 2c6b4ea2cb..af5dcef29e 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/utils/test_tool_call_wrapper.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/utils/test_tool_call_wrapper.py @@ -19,12 +19,18 @@ from google.genai import types as genai_types from opentelemetry._logs import get_logger_provider +from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _StabilityMode, +) from opentelemetry.instrumentation.google_genai import ( otel_wrapper, tool_call_wrapper, ) from opentelemetry.metrics import get_meter_provider from opentelemetry.trace import get_tracer_provider +from opentelemetry.util.genai.types import ContentCapturingMode from ..common import otel_mocker @@ -278,3 +284,48 @@ def somefunction(arg=None): span.attributes["code.function.parameters.arg.value"], '[123, "abc"]', ) + + def test_handle_with_new_sem_conv(self): + def somefunction(arg=None): + pass + + for mode in ContentCapturingMode: + patched_environ = patch.dict( + "os.environ", + { + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": mode.name, + "OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental", + }, + ) + patched_otel_mapping = patch.dict( + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING, + { + _OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL + }, + ) + with self.subTest( + f"mode: {mode}", patched_environ=patched_environ + ): + self.setUp() + with patched_environ, patched_otel_mapping: + wrapped_somefunction = self.wrap(somefunction) + wrapped_somefunction(12345) + + span = self.otel.get_span_named( + "execute_tool somefunction" + ) + + if mode in [ + ContentCapturingMode.NO_CONTENT, + ContentCapturingMode.EVENT_ONLY, + ]: + self.assertNotIn( + "code.function.parameters.arg.value", + span.attributes, + ) + else: + self.assertIn( + "code.function.parameters.arg.value", + span.attributes, + ) + self.tearDown() diff --git a/uv.lock b/uv.lock index 1a842d365a..5150ff87da 100644 --- a/uv.lock +++ b/uv.lock @@ -1466,7 +1466,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.16.1" +version = "1.42.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1474,12 +1474,13 @@ dependencies = [ { name = "httpx" }, { name = "pydantic" }, { name = "requests" }, + { name = "tenacity" }, { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/1f/1a52736e87b4a22afef615de45e2f509fbfb55c09798620b0c3f394076ef/google_genai-1.16.1.tar.gz", hash = "sha256:4b4ed4ed781a9d61e5ce0fef1486dd3a5d7ff0a73bd76b9633d21e687ab998df", size = 194270 } +sdist = { url = "https://files.pythonhosted.org/packages/18/03/84d04ce446d885eb978abb4b7c785f54a39435f02b182f457a996f5c9eb4/google_genai-1.42.0.tar.gz", hash = "sha256:0cef624c725a358f182e6988632371205bed9be1b1dbcf4296dbbd4eb4a9fb5d", size = 235620 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/31/30caa8d4ae987e47c5250fb6680588733863fd5b39cacb03ba1977c29bde/google_genai-1.16.1-py3-none-any.whl", hash = "sha256:6ae5d24282244f577ca4f0d95c09f75ab29e556602c9d3531b70161e34cd2a39", size = 196327 }, + { url = "https://files.pythonhosted.org/packages/f2/0a/8519cb752c10254899608de5c8cf5ff5ae05260a4ad5db0087fa466ddf46/google_genai-1.42.0-py3-none-any.whl", hash = "sha256:1e45c3ecc630a358c153a08b10d5b03d7c70cf3342fd116ac8a6cc4262cd81e8", size = 236204 }, ] [[package]] @@ -3144,6 +3145,7 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-genai" }, ] [package.optional-dependencies] @@ -3153,10 +3155,11 @@ instruments = [ [package.metadata] requires-dist = [ - { name = "google-genai", marker = "extra == 'instruments'", specifier = ">=1.0.0" }, + { name = "google-genai", marker = "extra == 'instruments'", specifier = ">=1.32.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, + { name = "opentelemetry-util-genai", editable = "util/opentelemetry-util-genai" }, ] provides-extras = ["instruments"]