Skip to content

Commit 284dece

Browse files
committed
review feedback
1 parent 0d64bcb commit 284dece

File tree

5 files changed

+275
-67
lines changed

5 files changed

+275
-67
lines changed

.github/workflows/templates/test.yml.j2

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ jobs:
5656

5757
- name: Install weaver
5858
run: |
59-
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${% raw %}{{ env.WEAVER_VERSION }}{% endraw %}/weaver_Linux_x86_64.tar.gz"
60-
curl -sSL "$WEAVER_URL" | tar -xz -C /tmp weaver
61-
sudo mv /tmp/weaver /usr/local/bin/weaver
59+
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${% raw %}{{ env.WEAVER_VERSION }}{% endraw %}/weaver-x86_64-unknown-linux-gnu.tar.xz"
60+
curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver
61+
sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver
6262
{%- endif %}
6363

6464
- name: Set up Python {{ job_data.python_version }}

.github/workflows/test.yml

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2868,9 +2868,9 @@ jobs:
28682868

28692869
- name: Install weaver
28702870
run: |
2871-
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver_Linux_x86_64.tar.gz"
2872-
curl -sSL "$WEAVER_URL" | tar -xz -C /tmp weaver
2873-
sudo mv /tmp/weaver /usr/local/bin/weaver
2871+
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz"
2872+
curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver
2873+
sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver
28742874
28752875
- name: Set up Python 3.10
28762876
uses: actions/setup-python@v5
@@ -2893,9 +2893,9 @@ jobs:
28932893

28942894
- name: Install weaver
28952895
run: |
2896-
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver_Linux_x86_64.tar.gz"
2897-
curl -sSL "$WEAVER_URL" | tar -xz -C /tmp weaver
2898-
sudo mv /tmp/weaver /usr/local/bin/weaver
2896+
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz"
2897+
curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver
2898+
sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver
28992899
29002900
- name: Set up Python 3.11
29012901
uses: actions/setup-python@v5
@@ -2918,9 +2918,9 @@ jobs:
29182918

29192919
- name: Install weaver
29202920
run: |
2921-
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver_Linux_x86_64.tar.gz"
2922-
curl -sSL "$WEAVER_URL" | tar -xz -C /tmp weaver
2923-
sudo mv /tmp/weaver /usr/local/bin/weaver
2921+
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz"
2922+
curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver
2923+
sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver
29242924
29252925
- name: Set up Python 3.12
29262926
uses: actions/setup-python@v5
@@ -2943,9 +2943,9 @@ jobs:
29432943

29442944
- name: Install weaver
29452945
run: |
2946-
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver_Linux_x86_64.tar.gz"
2947-
curl -sSL "$WEAVER_URL" | tar -xz -C /tmp weaver
2948-
sudo mv /tmp/weaver /usr/local/bin/weaver
2946+
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz"
2947+
curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver
2948+
sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver
29492949
29502950
- name: Set up Python 3.13
29512951
uses: actions/setup-python@v5
@@ -2968,9 +2968,9 @@ jobs:
29682968

29692969
- name: Install weaver
29702970
run: |
2971-
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver_Linux_x86_64.tar.gz"
2972-
curl -sSL "$WEAVER_URL" | tar -xz -C /tmp weaver
2973-
sudo mv /tmp/weaver /usr/local/bin/weaver
2971+
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz"
2972+
curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver
2973+
sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver
29742974
29752975
- name: Set up Python 3.14
29762976
uses: actions/setup-python@v5
@@ -2993,9 +2993,9 @@ jobs:
29932993

29942994
- name: Install weaver
29952995
run: |
2996-
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver_Linux_x86_64.tar.gz"
2997-
curl -sSL "$WEAVER_URL" | tar -xz -C /tmp weaver
2998-
sudo mv /tmp/weaver /usr/local/bin/weaver
2996+
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz"
2997+
curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver
2998+
sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver
29992999
30003000
- name: Set up Python 3.14t
30013001
uses: actions/setup-python@v5
@@ -3018,9 +3018,9 @@ jobs:
30183018

30193019
- name: Install weaver
30203020
run: |
3021-
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver_Linux_x86_64.tar.gz"
3022-
curl -sSL "$WEAVER_URL" | tar -xz -C /tmp weaver
3023-
sudo mv /tmp/weaver /usr/local/bin/weaver
3021+
WEAVER_URL="https://github.com/open-telemetry/weaver/releases/download/${{ env.WEAVER_VERSION }}/weaver-x86_64-unknown-linux-gnu.tar.xz"
3022+
curl -sSL "$WEAVER_URL" | tar -xJ -C /tmp weaver-x86_64-unknown-linux-gnu/weaver
3023+
sudo mv /tmp/weaver-x86_64-unknown-linux-gnu/weaver /usr/local/bin/weaver
30243024
30253025
- name: Set up Python pypy-3.10
30263026
uses: actions/setup-python@v5

tests/opentelemetry-test-utils/src/opentelemetry/test/weaver_live_check.py

Lines changed: 179 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import functools
1516
import json
1617
import logging
1718
import os
@@ -81,7 +82,9 @@ def collect(obj: Any) -> None:
8182
{
8283
"id": k[0],
8384
"message": k[1],
84-
"context": k[2],
85+
"context": vs[0].get(
86+
"context"
87+
), # preserve original dict, not JSON string
8588
"signal_name": k[3],
8689
"signal_type": k[4],
8790
"count": len(vs),
@@ -111,10 +114,88 @@ def _format_violations(violations: list) -> str:
111114
return "\n".join(lines)
112115

113116

117+
class LiveCheckError(AssertionError):
118+
"""Raised by :meth:`WeaverLiveCheck.end_and_check` when semconv violations are found.
119+
120+
The full :class:`LiveCheckReport` is attached as :attr:`report` for
121+
structured inspection beyond the human-readable message::
122+
123+
with pytest.raises(LiveCheckError) as exc_info:
124+
weaver.end_and_check()
125+
126+
err = exc_info.value
127+
assert any(
128+
v["id"] == "my_policy_check"
129+
and v["context"]["attribute_name"] == "my.attribute"
130+
for v in err.report.violations
131+
)
132+
"""
133+
134+
def __init__(self, message: str, report: "LiveCheckReport") -> None:
135+
super().__init__(message)
136+
self.report = report
137+
138+
139+
class LiveCheckReport:
140+
"""The result of a weaver live-check run.
141+
142+
Provides structured access to violations and the full raw JSON report.
143+
144+
See https://github.com/open-telemetry/weaver/tree/main/crates/weaver_live_check#output
145+
for the full report structure.
146+
147+
Example — asserting on metrics statistics::
148+
149+
report = weaver.end()
150+
seen = report["statistics"]["seen_registry_metrics"]
151+
assert seen.get("http.server.request.duration") == 1
152+
153+
Example — asserting on violations::
154+
155+
report = weaver.end()
156+
assert any(
157+
v["id"] == "my_policy_check"
158+
and v["context"]["attribute_name"] == "my.attribute"
159+
for v in report.violations
160+
)
161+
"""
162+
163+
def __init__(self, report: dict[str, Any]) -> None:
164+
self._report = report
165+
166+
@functools.cached_property
167+
def violations(self) -> list[dict[str, Any]]:
168+
"""Deduplicated list of semconv violations found in the report.
169+
170+
Each item is a dict with keys: ``id``, ``message``, ``context``
171+
(the raw context dict from weaver, e.g. ``{"attribute_name": "foo"}``),
172+
``signal_name``, ``signal_type``, ``count``.
173+
"""
174+
return _extract_violations(self._report)
175+
176+
def __getitem__(self, key: str) -> Any:
177+
return self._report[key]
178+
179+
def get(self, key: str, default: Any = None) -> Any:
180+
return self._report.get(key, default)
181+
182+
def __contains__(self, key: object) -> bool:
183+
return key in self._report
184+
185+
def __repr__(self) -> str:
186+
n = len(self.violations)
187+
return f"LiveCheckReport({n} violation{'s' if n != 1 else ''})"
188+
189+
190+
# NOTE: WeaverLiveCheck is experimental and its API is subject to change.
114191
class WeaverLiveCheck:
115192
"""Runs ``weaver registry live-check`` as a subprocess and validates
116193
OTLP telemetry against OpenTelemetry semantic conventions.
117194
195+
.. note::
196+
This class is experimental and its API is subject to change without notice.
197+
198+
118199
Requires the ``weaver`` binary on PATH:
119200
https://github.com/open-telemetry/weaver/releases
120201
@@ -128,27 +209,36 @@ def test_my_telemetry(self):
128209
# ... configure provider, emit telemetry ...
129210
provider.force_flush()
130211
131-
# Signals weaver to stop, raises AssertionError listing violations
132-
# if any, or returns the raw JSON report on success.
212+
# Signals weaver to stop, raises LiveCheckError listing violations
213+
# if any, or returns a LiveCheckReport on success.
133214
report = weaver.end_and_check()
134215
# __exit__ calls close(), which is idempotent if end_and_check() was already called
135216
136-
:meth:`end_and_check` returns the raw weaver JSON report when weaver exits
137-
successfully (exit code 0). Use it for custom assertions on the report
138-
content beyond the built-in violation check::
217+
Use :meth:`end` when you need the full :class:`LiveCheckReport`
218+
regardless of whether violations were found — for example, to assert that
219+
specific metrics were observed or to inspect violation fields directly::
220+
221+
with WeaverLiveCheck() as weaver:
222+
# ... configure provider, emit telemetry ...
223+
provider.force_flush()
224+
report = weaver.end()
139225
140-
report = weaver.end_and_check()
141-
# report is the raw JSON dict from weaver; inspect it as needed, e.g.:
142-
self.assertIn("some_signal", str(report))
226+
seen_metrics = report["statistics"]["seen_registry_metrics"]
227+
assert seen_metrics.get("http.server.request.duration") == 1
143228
144229
Lifecycle:
145230
- :meth:`start` — launches weaver and waits for it to become ready.
146231
- :attr:`otlp_endpoint` — gRPC OTLP endpoint to point exporters at.
147-
- :meth:`end_and_check` — signals weaver to stop, collects the report, and
148-
raises :class:`AssertionError` with a human-readable violation list if weaver
149-
exits non-zero. Returns the raw report dict on success.
150-
- :meth:`close` — calls :meth:`end_and_check` then terminates the process.
151-
Idempotent; safe to call even if :meth:`end_and_check` was already called.
232+
- :meth:`end` — signals weaver to stop and always returns a
233+
:class:`LiveCheckReport`. Never raises for semconv violations; use
234+
this when you want to write your own assertions.
235+
- :meth:`end_and_check` — signals weaver to stop and raises
236+
:class:`LiveCheckError` with a human-readable violation list and the
237+
full report attached if weaver exits non-zero. Returns a
238+
:class:`LiveCheckReport` on success.
239+
- :meth:`close` — stops weaver if not already stopped and terminates the
240+
process. Never raises for semconv violations. Idempotent; safe to
241+
call even if :meth:`end_and_check` or :meth:`end` was already called.
152242
"""
153243

154244
def __init__(
@@ -247,25 +337,22 @@ def _wait_for_ready(self, timeout: int = 60) -> None:
247337
def otlp_endpoint(self) -> str:
248338
return f"http://localhost:{self._otlp_port}"
249339

250-
def end_and_check(self, timeout: int = 30) -> dict[str, Any]:
251-
if self._stopped:
252-
logger.warning(
253-
"end_and_check() called after weaver already stopped; returning empty report"
254-
)
255-
return {}
256-
self._stopped = True
340+
def _do_stop(self, timeout: int) -> tuple["LiveCheckReport", int]:
341+
"""POST /stop, wait for the process to exit, return (report, exit_code).
257342
343+
Raises for infrastructure errors (HTTP failure, process communication).
344+
Never raises for semconv violations.
345+
"""
258346
if not self._ready:
259347
raise RuntimeError(
260348
"WeaverLiveCheck process did not start successfully"
261349
)
262-
263350
try:
264351
response = post(
265352
f"http://localhost:{self._admin_port}/stop", timeout=5
266353
)
267354
response.raise_for_status()
268-
report = response.json()
355+
report = LiveCheckReport(response.json())
269356
assert self._process is not None
270357
exit_code = self._process.wait(timeout=timeout)
271358
except Exception as e:
@@ -274,13 +361,58 @@ def end_and_check(self, timeout: int = 30) -> dict[str, Any]:
274361
"Error communicating with weaver: %s, logs: %s", e, logs
275362
)
276363
raise
364+
return report, exit_code
365+
366+
def end(self, timeout: int = 30) -> "LiveCheckReport":
367+
"""Signal weaver to stop and return the full :class:`LiveCheckReport`.
368+
369+
Never raises for semconv violations — use this when you want to write
370+
your own assertions against :attr:`LiveCheckReport.violations` or the
371+
raw report data.
372+
373+
Raises :exc:`RuntimeError` for infrastructure problems (weaver failed
374+
to start, HTTP communication error, etc.).
375+
376+
See https://github.com/open-telemetry/weaver/tree/main/crates/weaver_live_check#output
377+
for the report structure.
378+
"""
379+
if self._stopped:
380+
logger.warning(
381+
"end() called after weaver already stopped; returning empty report"
382+
)
383+
return LiveCheckReport({})
384+
self._stopped = True
385+
report, _ = self._do_stop(timeout)
386+
return report
277387

388+
def end_and_check(self, timeout: int = 30) -> "LiveCheckReport":
389+
"""Signal weaver to stop and assert no semconv violations were found.
390+
391+
Returns the :class:`LiveCheckReport` when weaver exits successfully
392+
(exit code 0).
393+
394+
Does **not** return if weaver exits with a non-zero status — raises
395+
:exc:`LiveCheckError` (a subclass of :exc:`AssertionError`) with a
396+
human-readable list of violations and the full :class:`LiveCheckReport`
397+
attached as :attr:`LiveCheckError.report`.
398+
Use :meth:`end` if you need the report regardless of violations.
399+
400+
Raises :exc:`RuntimeError` for infrastructure problems (weaver failed
401+
to start, HTTP communication error, etc.).
402+
"""
403+
if self._stopped:
404+
logger.warning(
405+
"end_and_check() called after weaver already stopped; returning empty report"
406+
)
407+
return LiveCheckReport({})
408+
self._stopped = True
409+
report, exit_code = self._do_stop(timeout)
278410
if exit_code == 0:
411+
# Success — no violations found, no errors communicating with weaver
279412
return report
280-
281-
violations = _extract_violations(report)
282-
raise AssertionError(
283-
f"Semconv violations found:\n{_format_violations(violations)}"
413+
raise LiveCheckError(
414+
f"Semconv violations found:\n{_format_violations(report.violations)}",
415+
report,
284416
)
285417

286418
def _read_weaver_logs(self) -> str | None:
@@ -296,12 +428,24 @@ def _read_weaver_logs(self) -> str | None:
296428
return None
297429

298430
def close(self) -> None:
299-
try:
300-
self.end_and_check()
301-
finally:
302-
if self._process and self._process.poll() is None:
303-
self._process.terminate()
431+
"""Stop weaver and clean up the process.
432+
433+
If weaver has not been stopped yet, sends the ``/stop`` signal and
434+
waits for the process to exit. Never raises for semconv violations.
435+
Idempotent — safe to call multiple times or after :meth:`end` /
436+
:meth:`end_and_check` has already been called.
437+
"""
438+
if not self._stopped:
439+
self._stopped = True
440+
if self._ready:
304441
try:
305-
self._process.wait(timeout=5)
306-
except subprocess.TimeoutExpired:
307-
self._process.kill()
442+
self._do_stop(timeout=30)
443+
return # process already exited cleanly
444+
except Exception as e:
445+
logger.debug("Error stopping weaver during close: %s", e)
446+
if self._process and self._process.poll() is None:
447+
self._process.terminate()
448+
try:
449+
self._process.wait(timeout=5)
450+
except subprocess.TimeoutExpired:
451+
self._process.kill()

0 commit comments

Comments
 (0)