@@ -181,3 +181,145 @@ def _assert_metric_scope_schema_urls(
181181 self .assertEqual (
182182 scope_metric .scope .schema_url , expected_schema_url
183183 )
184+
185+ def test_stop_embedding_records_duration_and_tokens (self ) -> None :
186+ """Verify embedding invocations record duration and input token metrics."""
187+ handler = TelemetryHandler (
188+ tracer_provider = self .tracer_provider ,
189+ meter_provider = self .meter_provider ,
190+ )
191+ # Patch default_timer during start to ensure monotonic_start_s
192+ with patch ("timeit.default_timer" , return_value = 1000.0 ):
193+ invocation = handler .start_embedding (
194+ "embed-prov" , request_model = "embed-model"
195+ )
196+ invocation .input_tokens = 100
197+
198+ # Simulate 1.5 seconds of elapsed monotonic time
199+ with patch ("timeit.default_timer" , return_value = 1001.5 ):
200+ invocation .stop ()
201+
202+ self ._assert_metric_scope_schema_urls (_DEFAULT_SCHEMA_URL )
203+ metrics = self ._harvest_metrics ()
204+
205+ # Duration should be recorded
206+ self .assertIn ("gen_ai.client.operation.duration" , metrics )
207+ duration_points = metrics ["gen_ai.client.operation.duration" ]
208+ self .assertEqual (len (duration_points ), 1 )
209+ duration_point = duration_points [0 ]
210+ self .assertEqual (
211+ duration_point .attributes [GenAI .GEN_AI_OPERATION_NAME ],
212+ GenAI .GenAiOperationNameValues .EMBEDDINGS .value ,
213+ )
214+ self .assertEqual (
215+ duration_point .attributes [GenAI .GEN_AI_REQUEST_MODEL ],
216+ "embed-model" ,
217+ )
218+ self .assertEqual (
219+ duration_point .attributes [GenAI .GEN_AI_PROVIDER_NAME ], "embed-prov"
220+ )
221+ self .assertAlmostEqual (duration_point .sum , 1.5 , places = 3 )
222+
223+ # Token metrics should be recorded for embedding (input only)
224+ self .assertIn ("gen_ai.client.token.usage" , metrics )
225+ token_points = metrics ["gen_ai.client.token.usage" ]
226+ self .assertEqual (len (token_points ), 1 ) # Only input tokens
227+ token_point = token_points [0 ]
228+ self .assertEqual (
229+ token_point .attributes [GenAI .GEN_AI_TOKEN_TYPE ],
230+ GenAI .GenAiTokenTypeValues .INPUT .value ,
231+ )
232+ self .assertAlmostEqual (token_point .sum , 100.0 , places = 3 )
233+
234+ def test_stop_embedding_records_duration_with_additional_attributes (
235+ self ,
236+ ) -> None :
237+ """Verify embedding metrics include server and custom attributes."""
238+ handler = TelemetryHandler (
239+ tracer_provider = self .tracer_provider ,
240+ meter_provider = self .meter_provider ,
241+ )
242+ invocation = handler .start_embedding (
243+ "embed-prov" ,
244+ request_model = "embed-model" ,
245+ server_address = "embed.server.com" ,
246+ server_port = 8080 ,
247+ )
248+ invocation .metric_attributes = {"custom.embed.attr" : "embed_value" }
249+ invocation .response_model_name = "embed-response-model"
250+ invocation .stop ()
251+
252+ self ._assert_metric_scope_schema_urls (_DEFAULT_SCHEMA_URL )
253+ metrics = self ._harvest_metrics ()
254+
255+ self .assertIn ("gen_ai.client.operation.duration" , metrics )
256+ duration_points = metrics ["gen_ai.client.operation.duration" ]
257+ self .assertEqual (len (duration_points ), 1 )
258+ duration_point = duration_points [0 ]
259+
260+ self .assertEqual (
261+ duration_point .attributes ["server.address" ], "embed.server.com"
262+ )
263+ self .assertEqual (duration_point .attributes ["server.port" ], 8080 )
264+ self .assertEqual (
265+ duration_point .attributes ["custom.embed.attr" ], "embed_value"
266+ )
267+ self .assertEqual (
268+ duration_point .attributes [GenAI .GEN_AI_RESPONSE_MODEL ],
269+ "embed-response-model" ,
270+ )
271+
272+ def test_fail_embedding_records_error_and_duration (self ) -> None :
273+ """Verify embedding failure records error type and duration."""
274+ handler = TelemetryHandler (
275+ tracer_provider = self .tracer_provider ,
276+ meter_provider = self .meter_provider ,
277+ )
278+ with patch ("timeit.default_timer" , return_value = 3000.0 ):
279+ invocation = handler .start_embedding (
280+ "embed-prov" , request_model = "embed-err-model"
281+ )
282+
283+ error = Error (message = "embedding failed" , type = RuntimeError )
284+ with patch ("timeit.default_timer" , return_value = 3002.5 ):
285+ invocation .fail (error )
286+
287+ self ._assert_metric_scope_schema_urls (_DEFAULT_SCHEMA_URL )
288+ metrics = self ._harvest_metrics ()
289+
290+ self .assertIn ("gen_ai.client.operation.duration" , metrics )
291+ duration_points = metrics ["gen_ai.client.operation.duration" ]
292+ self .assertEqual (len (duration_points ), 1 )
293+ duration_point = duration_points [0 ]
294+
295+ self .assertEqual (
296+ duration_point .attributes .get ("error.type" ), "RuntimeError"
297+ )
298+ self .assertEqual (
299+ duration_point .attributes .get (GenAI .GEN_AI_REQUEST_MODEL ),
300+ "embed-err-model" ,
301+ )
302+ self .assertAlmostEqual (duration_point .sum , 2.5 , places = 3 )
303+
304+ # Token metrics should NOT be recorded when input_tokens is not set
305+ self .assertNotIn ("gen_ai.client.token.usage" , metrics )
306+
307+ def test_stop_embedding_without_tokens (self ) -> None :
308+ """Verify embedding without input_tokens does not record token metrics."""
309+ handler = TelemetryHandler (
310+ tracer_provider = self .tracer_provider ,
311+ meter_provider = self .meter_provider ,
312+ )
313+ invocation = handler .start_embedding (
314+ "embed-prov" , request_model = "embed-model"
315+ )
316+ # input_tokens is not set
317+ invocation .stop ()
318+
319+ metrics = self ._harvest_metrics ()
320+
321+ # Duration should be recorded
322+ self .assertIn ("gen_ai.client.operation.duration" , metrics )
323+
324+ # Token metrics should NOT be recorded when input_tokens is not set
325+ self .assertNotIn ("gen_ai.client.token.usage" , metrics )
0 commit comments