Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
67ce692
Added the metrics support for google generativeai
adharshctr Dec 8, 2025
53ced7a
Collect tokens from response
adharshctr Dec 8, 2025
3ec203e
Done linting
adharshctr Dec 8, 2025
012121e
updated the wrapper
adharshctr Dec 8, 2025
28f61d1
Address the suggestion
adharshctr Dec 8, 2025
98655d7
Address the suggestion
adharshctr Dec 8, 2025
ff67a81
Changed the failing testcase
adharshctr Dec 9, 2025
c6d2e17
updated confest file
adharshctr Dec 9, 2025
e524d2b
Added testcases for client
adharshctr Dec 9, 2025
1a178c7
Added testcases for metrics
adharshctr Dec 9, 2025
e57ec68
Recorded the tests
adharshctr Dec 9, 2025
bbdaa9e
Addressed the suggestion
adharshctr Dec 9, 2025
b20e9e7
Merge branch 'main' into google-metrics-support
adharshctr Dec 9, 2025
8e70176
Updated the suggestion
adharshctr Dec 9, 2025
1dcce3e
Done the suggested change
adharshctr Jan 6, 2026
e3bbde2
Done linting
adharshctr Jan 6, 2026
1e4f103
Merge branch 'main' into google-metrics-support
adharshctr Jan 6, 2026
851e4e0
Addressed the review comments
adharshctr Jan 12, 2026
6d33235
Updated the tests
adharshctr Jan 12, 2026
f07f311
Updated the review comments
adharshctr Jan 13, 2026
c19901c
Updated the suggestions
adharshctr Jan 13, 2026
53da2c5
Updated the suggestions
adharshctr Jan 13, 2026
4d38f98
Merge branch 'main' into google-metrics-support
adharshctr Jan 13, 2026
2ca4a4b
Merge branch 'main' into google-metrics-support
adharshctr Jan 18, 2026
e130e02
Refactor import statements in test_generate_content.py
adharshctr Jan 18, 2026
11afbef
Resolve conflit
adharshctr Jan 18, 2026
23c27a0
Done the suggested changes
adharshctr Jan 19, 2026
75b7d47
Done the suggested changes
adharshctr Jan 19, 2026
cabc120
Merge branch 'main' into google-metrics-support
adharshctr Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""OpenTelemetry Google Generative AI API instrumentation"""

import logging
import os
import time
import types
from typing import Collection

Expand Down Expand Up @@ -32,7 +34,9 @@
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
LLMRequestTypeValues,
SpanAttributes,
Meters
)
from opentelemetry.metrics import Meter, get_meter
from opentelemetry.trace import SpanKind, get_tracer
from wrapt import wrap_function_wrapper

Expand Down Expand Up @@ -79,6 +83,7 @@ def _build_from_streaming_response(
response: GenerateContentResponse,
llm_model,
event_logger,
token_histogram,
):
complete_response = ""
last_chunk = None
Expand All @@ -93,12 +98,12 @@ def _build_from_streaming_response(
emit_choice_events(response, event_logger)
else:
set_response_attributes(span, complete_response, llm_model)
set_model_response_attributes(span, last_chunk or response, llm_model)
set_model_response_attributes(span, last_chunk or response, llm_model, token_histogram)
span.end()


async def _abuild_from_streaming_response(
span, response: GenerateContentResponse, llm_model, event_logger
span, response: GenerateContentResponse, llm_model, event_logger, token_histogram
):
complete_response = ""
last_chunk = None
Expand All @@ -113,7 +118,7 @@ async def _abuild_from_streaming_response(
emit_choice_events(response, event_logger)
else:
set_response_attributes(span, complete_response, llm_model)
set_model_response_attributes(span, last_chunk if last_chunk else response, llm_model)
set_model_response_attributes(span, last_chunk if last_chunk else response, llm_model, token_histogram)
span.end()


Expand All @@ -128,21 +133,31 @@ def _handle_request(span, args, kwargs, llm_model, event_logger):


@dont_throw
def _handle_response(span, response, llm_model, event_logger):
def _handle_response(span, response, llm_model, event_logger, token_histogram):
if should_emit_events() and event_logger:
emit_choice_events(response, event_logger)
else:
set_response_attributes(span, response, llm_model)

set_model_response_attributes(span, response, llm_model)
set_model_response_attributes(span, response, llm_model, token_histogram)


def _with_tracer_wrapper(func):
"""Helper for providing tracer for wrapper functions."""

def _with_tracer(tracer, event_logger, to_wrap):
def _with_tracer(tracer, event_logger, to_wrap, token_histogram, duration_histogram):
def wrapper(wrapped, instance, args, kwargs):
return func(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs)
return func(
tracer,
event_logger,
to_wrap,
token_histogram,
duration_histogram,
wrapped,
instance,
args,
kwargs,
)

return wrapper

Expand All @@ -154,10 +169,13 @@ async def _awrap(
tracer,
event_logger,
to_wrap,
token_histogram,
duration_histogram,
wrapped,
instance,
args,
kwargs,

):
"""Instruments and calls every function defined in TO_WRAP."""
if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
Expand Down Expand Up @@ -186,22 +204,31 @@ async def _awrap(
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
},
)

start_time = time.time()
_handle_request(span, args, kwargs, llm_model, event_logger)

response = await wrapped(*args, **kwargs)

if duration_histogram:
duration = time.time() - start_time
duration_histogram.record(
duration,
attributes={
GenAIAttributes.GEN_AI_SYSTEM: "Google",
GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model
},
)
if response:
if is_streaming_response(response):
return _build_from_streaming_response(
span, response, llm_model, event_logger
span, response, llm_model, event_logger, token_histogram
)
elif is_async_streaming_response(response):
return _abuild_from_streaming_response(
span, response, llm_model, event_logger
span, response, llm_model, event_logger, token_histogram
)
else:
_handle_response(span, response, llm_model, event_logger)
_handle_response(span, response, llm_model, event_logger, token_histogram)

span.end()
return response
Expand All @@ -212,10 +239,13 @@ def _wrap(
tracer,
event_logger,
to_wrap,
token_histogram,
duration_histogram,
wrapped,
instance,
args,
kwargs,

):
"""Instruments and calls every function defined in TO_WRAP."""
if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
Expand Down Expand Up @@ -245,26 +275,56 @@ def _wrap(
},
)

start_time = time.time()
_handle_request(span, args, kwargs, llm_model, event_logger)

response = wrapped(*args, **kwargs)

if duration_histogram:
duration = time.time() - start_time
duration_histogram.record(
duration,
attributes={
GenAIAttributes.GEN_AI_SYSTEM: "Google",
GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model
},
)
if response:
if is_streaming_response(response):
return _build_from_streaming_response(
span, response, llm_model, event_logger
span, response, llm_model, event_logger, token_histogram
)
elif is_async_streaming_response(response):
return _abuild_from_streaming_response(
span, response, llm_model, event_logger
span, response, llm_model, event_logger, token_histogram
)
else:
_handle_response(span, response, llm_model, event_logger)
_handle_response(span, response, llm_model, event_logger, token_histogram)

span.end()
return response


def is_metrics_enabled() -> bool:
return (os.getenv("TRACELOOP_METRICS_ENABLED") or "true").lower() == "true"


def _create_metrics(meter: Meter):
token_histogram = meter.create_histogram(
name=Meters.LLM_TOKEN_USAGE,
unit="token",
description="Measures number of input and output tokens used",
)

duration_histogram = meter.create_histogram(
name=Meters.LLM_OPERATION_DURATION,
unit="s",
description="GenAI operation duration",
)

return token_histogram, duration_histogram


class GoogleGenerativeAiInstrumentor(BaseInstrumentor):
"""An instrumentor for Google Generative AI's client library."""

Expand All @@ -285,6 +345,12 @@ def _instrument(self, **kwargs):
tracer_provider = kwargs.get("tracer_provider")
tracer = get_tracer(__name__, __version__, tracer_provider)

meter_provider = kwargs.get("meter_provider")
meter = get_meter(__name__, __version__, meter_provider)

if is_metrics_enabled():
token_histogram, duration_histogram = _create_metrics(meter)

event_logger = None
if not Config.use_legacy_attributes:
logger_provider = kwargs.get("logger_provider")
Expand All @@ -297,14 +363,24 @@ def _instrument(self, **kwargs):
wrap_object = wrapped_method.get("object")
wrap_method = wrapped_method.get("method")

wrapper_args = (
tracer,
event_logger,
wrapped_method,
token_histogram,
duration_histogram,
)

wrapper = (
_awrap(*wrapper_args)
if wrap_object == "AsyncModels"
else _wrap(*wrapper_args)
)

wrap_function_wrapper(
wrap_package,
f"{wrap_object}.{wrap_method}",
(
_awrap(tracer, event_logger, wrapped_method)
if wrap_object == "AsyncModels"
else _wrap(tracer, event_logger, wrapped_method)
),
wrapper,
)

def _uninstrument(self, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ def set_response_attributes(span, response, llm_model):
)


def set_model_response_attributes(span, response, llm_model):
def set_model_response_attributes(span, response, llm_model, token_histogram):
if not span.is_recording():
return

Expand All @@ -469,4 +469,22 @@ def set_model_response_attributes(span, response, llm_model):
response.usage_metadata.prompt_token_count,
)

if token_histogram:
token_histogram.record(
response.usage_metadata.prompt_token_count,
attributes={
GenAIAttributes.GEN_AI_SYSTEM: "Google",
GenAIAttributes.GEN_AI_TOKEN_TYPE: "input",
GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model,
}
)
token_histogram.record(
response.usage_metadata.candidates_token_count,
attributes={
GenAIAttributes.GEN_AI_SYSTEM: "Google",
GenAIAttributes.GEN_AI_TOKEN_TYPE: "output",
GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model,
},
)

span.set_status(Status(StatusCode.OK))