Skip to content

Commit e56a8af

Browse files
authored
Merge branch 'main' into fix-flask-active-requests-gauge-leak
2 parents efb677f + b8ca943 commit e56a8af

5 files changed

Lines changed: 225 additions & 16 deletions

File tree

.github/workflows/check-links.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: check-links
2+
on:
3+
push:
4+
branches: [ main ]
5+
paths:
6+
- '**/*.md'
7+
- '**/*.rst'
8+
- '.github/workflows/check-links.yml'
9+
- '.github/workflows/check_links_config.json'
10+
pull_request:
11+
paths:
12+
- '**/*.md'
13+
- '**/*.rst'
14+
- '.github/workflows/check-links.yml'
15+
- '.github/workflows/check_links_config.json'
16+
17+
permissions:
18+
contents: read
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
22+
cancel-in-progress: true
23+
24+
jobs:
25+
check-links:
26+
runs-on: ubuntu-latest
27+
if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'otelbot[bot]' }}
28+
timeout-minutes: 15
29+
steps:
30+
- name: Checkout Repo
31+
uses: actions/checkout@v6
32+
33+
- name: Get changed markdown files
34+
id: changed-files
35+
uses: tj-actions/changed-files@v46
36+
with:
37+
files: |
38+
**/*.md
39+
**/*.rst
40+
41+
- name: Install markdown-link-check
42+
if: steps.changed-files.outputs.any_changed == 'true'
43+
run: npm install -g markdown-link-check@v3.12.2
44+
45+
- name: Run markdown-link-check
46+
if: steps.changed-files.outputs.any_changed == 'true'
47+
run: |
48+
markdown-link-check \
49+
--verbose \
50+
--config .github/workflows/check_links_config.json \
51+
${{ steps.changed-files.outputs.all_changed_files }} \
52+
|| { echo "Check that anchor links are lowercase"; exit 1; }
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"ignorePatterns": [
3+
{
4+
"pattern": "http(s)?://\\d+\\.\\d+\\.\\d+\\.\\d+"
5+
},
6+
{
7+
"pattern": "http(s)?://localhost"
8+
},
9+
{
10+
"pattern": "http(s)?://example.com"
11+
}
12+
],
13+
"aliveStatusCodes": [429, 200]
14+
}

util/opentelemetry-util-genai/CHANGELOG.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Add metrics support for EmbeddingInvocation
11+
([#4377](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4377))
1012
- Add support for workflow in genAI utils handler.
11-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4366](#4366))
13+
([#4366](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4366))
1214
- Enrich ToolCall type, breaking change: usage of ToolCall class renamed to ToolCallRequest
1315
([#4218](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4218))
1416
- Add EmbeddingInvocation span lifecycle support
15-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4219](#4219))
17+
([#4219](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4219))
1618
- Populate schema_url on metrics
1719
([#4320](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4320))
1820
- Add workflow invocation type to genAI utils
19-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4310](#4310))
21+
([#4310](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4310))
2022
- Check if upload works at startup in initializer of the `UploadCompletionHook`, instead
21-
of repeatedly failing on every upload ([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4390](#4390)).
23+
of repeatedly failing on every upload ([#4390](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4390)).
2224
- Refactor public API: add factory methods (`start_inference`, `start_embedding`, `start_tool`, `start_workflow`) and invocation-owned lifecycle (`invocation.stop()` / `invocation.fail(exc)`); rename `LLMInvocation``InferenceInvocation` and `ToolCall``ToolInvocation`. Existing usages remain fully functional via deprecated aliases.
2325
([#4391](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4391))
2426

@@ -32,28 +34,28 @@ of repeatedly failing on every upload ([https://github.com/open-telemetry/opente
3234
- Log error when `fsspec` fails to be imported instead of silently failing ([#4037](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4037)).
3335
- Minor change to check LRU cache in Completion Hook before acquiring semaphore/thread ([#3907](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3907)).
3436
- Add environment variable for genai upload hook queue size
35-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3943](#3943))
37+
([#3943](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3943))
3638
- Add more Semconv attributes to LLMInvocation spans.
37-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3862](#3862))
39+
([#3862](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3862))
3840
- Limit the upload hook thread pool to 64 workers
39-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3944](#3944))
41+
([#3944](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3944))
4042
- Add metrics to LLMInvocation traces
41-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3891](#3891))
43+
([#3891](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3891))
4244
- Add parent class genAI invocation
43-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3889](#3889))
45+
([#3889](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3889))
4446

4547
## Version 0.2b0 (2025-10-14)
4648

4749
- Add jsonlines support to fsspec uploader
48-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3791](#3791))
50+
([#3791](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3791))
4951
- Rename "fsspec_upload" entry point and classes to more generic "upload"
50-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3798](#3798))
52+
([#3798](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3798))
5153
- Record content-type and use canonical paths in fsspec genai uploader
52-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3795](#3795))
54+
([#3795](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3795))
5355
- Make inputs / outputs / system instructions optional params to `on_completion`,
54-
([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3802](#3802)).
56+
([#3802](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3802)).
5557
- Use a SHA256 hash of the system instructions as it's upload filename, and check
56-
if the file exists before re-uploading it, ([https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3814](#3814)).
58+
if the file exists before re-uploading it, ([#3814](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3814)).
5759

5860
## Version 0.1b0 (2025-09-25)
5961

util/opentelemetry-util-genai/src/opentelemetry/util/genai/_embedding_invocation.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,4 @@ def _apply_finish(self, error: Error | None = None) -> None:
120120
self._apply_error_attributes(error)
121121
attributes.update(self.attributes)
122122
self.span.set_attributes(attributes)
123-
# Metrics recorder currently supports InferenceInvocation fields only.
124-
# No-op until dedicated embedding metric support is added.
123+
self._metrics_recorder.record(self)

util/opentelemetry-util-genai/tests/test_handler_metrics.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)