This commit is contained in:
Karan 2026-10-31 14:38:50 +08:00 • committed by GitHub
commit 23604ea472
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 40 additions and 3 deletions

View file

@ -1747,7 +1747,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
self._token_usage_histogram.record(usage.get("completion_tokens", 0), attributes=out_attrs)
cost: Final = kwargs.get("response_cost")
if self._cost_histogram and cost:
if self._cost_histogram and cost is not None:
self._cost_histogram.record(cost, attributes=common_attrs)
# Record latency metrics (TTFT, TPOT, and Total Generation Time)

View file

@ -234,7 +234,7 @@ class GenAIMetricRecorder:
self._record_token_usage(response_obj, common_attrs)
cost: Final = kwargs.get("response_cost")
if cost:
if cost is not None:
self._metrics.token_cost.record(cost, attributes=common_attrs)
self._record_time_to_first_token(kwargs, common_attrs)

View file

@ -193,6 +193,26 @@ def test_all_six_metrics_emitted_when_enabled():
assert cost_points[0].sum == pytest.approx(RESPONSE_COST)
@pytest.mark.parametrize("response_cost", [0, 0.0123, None])
def test_cost_metric_records_known_values_including_zero(response_cost: float | None):
reader = InMemoryMetricReader()
logger = _logger(reader, enable_metrics=True)
kwargs, response_obj, start, end = _build_call()
kwargs["response_cost"] = response_cost
asyncio.run(logger.async_log_success_event(kwargs, response_obj, start, end))
metrics = _metrics_by_name(reader)
cost_points = metrics.get(TOKEN_COST, [])
if response_cost is None:
assert cost_points == []
return
assert len(cost_points) == 1
assert cost_points[0].sum == pytest.approx(response_cost)
assert cost_points[0].count == 1
assert cost_points[0].attributes == metrics[OPERATION_DURATION][0].attributes
def test_time_to_first_token_is_streaming_only():
"""time_to_first_token is gated on streaming: a non-streaming call emits the
other five metrics but never that one."""

View file

@ -6255,7 +6255,7 @@ class TestOpenTelemetryMetricAttributeFiltering(unittest.TestCase):
response_obj = json.load(f)
return kwargs, response_obj
def _record(self, attributes):
def _record(self, attributes, response_cost: float | None = None):
"""Run a real success hook with metrics enabled and return the reader."""
metric_reader = InMemoryMetricReader()
meter_provider = MeterProvider(metric_readers=[metric_reader])
@ -6271,11 +6271,28 @@ class TestOpenTelemetryMetricAttributeFiltering(unittest.TestCase):
otel.tracer = tracer_provider.get_tracer(__name__)
kwargs, response_obj = self._load_fixtures()
if response_cost is not None:
kwargs["response_cost"] = response_cost
start = datetime.utcnow()
end = start + timedelta(seconds=1)
otel._handle_success(kwargs, response_obj, start, end)
return metric_reader
def test_zero_cost_is_recorded(self):
reader = self._record(None, response_cost=0)
data = reader.get_metrics_data()
cost_metrics = tuple(
metric
for resource_metrics in data.resource_metrics
for scope_metrics in resource_metrics.scope_metrics
for metric in scope_metrics.metrics
if metric.name == "gen_ai.usage.cost"
)
self.assertEqual(len(cost_metrics), 1)
self.assertEqual(cost_metrics[0].data.data_points[0].sum, 0)
self.assertEqual(cost_metrics[0].data.data_points[0].count, 1)
def _keysets(self, reader, metric_name):
"""Attribute-key sets, one per recorded data point of `metric_name`."""
deadline = time.time() + self.POLL_TIMEOUT