nullrun 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ """
2
+ Centralised error handling for auto-instrumentation patchers.
3
+
4
+ Sprint 2.9 (B47): pre-fix, the auto-instrumentation modules had
5
+ 25+ instances of ``try/except Exception: pass # pragma: no cover``
6
+ scattered across ``auto.py``, ``auto_requests.py``, ``autogen.py``,
7
+ ``crewai.py``, ``llama_index.py``. If a patch failed in production
8
+ (typically because the vendored SDK changed a method signature),
9
+ the SDK would silently degrade and the user would have no idea
10
+ why their costs were no longer being tracked.
11
+
12
+ The fix: every patch call goes through ``safe_patch()`` which:
13
+ - Returns ``True``/``False`` based on patch outcome.
14
+ - Logs at WARNING with the patch name + the actual exception
15
+ (so a SRE can grep for ``Auto-instrumentation patch X failed``
16
+ and see WHY each patch broke).
17
+ - Treats ``ImportError`` (optional dep not installed) as a
18
+ normal, expected event — DEBUG level, not WARNING.
19
+
20
+ Usage:
21
+
22
+ from nullrun.instrumentation._safe_patch import safe_patch
23
+
24
+ # In auto_instrument:
25
+ paths = [
26
+ safe_patch("httpx", lambda: patch_httpx(runtime)),
27
+ safe_patch("langchain", lambda: patch_langchain_callback(runtime)),
28
+ ...
29
+ ]
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import logging
34
+ from collections.abc import Callable
35
+ from typing import TypeAlias
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # The result type produced by individual patchers. Most return
40
+ # ``bool`` (True if the patch was installed, False if the vendor
41
+ # class wasn't found). Some return ``None`` (e.g. if they early-
42
+ # exit on a missing optional dependency).
43
+ PatchResult: TypeAlias = bool | None
44
+
45
+
46
+ def safe_patch(name: str, patch_fn: Callable[[], PatchResult]) -> bool:
47
+ """Run an auto-instrumentation patch with centralised error handling.
48
+
49
+ The 25+ scattered ``try/except`` blocks in the auto-instrumentation
50
+ modules all shared the same contract:
51
+ 1. ``ImportError`` means the optional dep isn't installed —
52
+ not actionable, just skip.
53
+ 2. Any other ``Exception`` is a real patch failure that the
54
+ operator needs to know about.
55
+
56
+ ``safe_patch()`` captures both cases and logs at the right
57
+ level, returning a single boolean so the caller can count
58
+ successful patches without dealing with try/except itself.
59
+
60
+ Args:
61
+ name: Human-readable patch name (e.g. ``"httpx"``,
62
+ ``"langchain_callback"``). Used in the log line so
63
+ an operator can grep their logs.
64
+ patch_fn: Zero-arg callable that performs the patch and
65
+ returns ``True`` on success, ``False`` on benign
66
+ no-op (e.g. vendor class not found), or ``None``
67
+ (treated as success).
68
+
69
+ Returns:
70
+ ``True`` if the patch was applied (or had nothing to do),
71
+ ``False`` if the patch failed.
72
+ """
73
+ try:
74
+ result = patch_fn()
75
+ # ``None`` is treated as "patch did its job, nothing more
76
+ # to report" — distinct from ``False`` which means "I tried
77
+ # but the vendor class wasn't installed".
78
+ return bool(result) if result is not None else True
79
+ except ImportError as e:
80
+ # Optional dependency not installed (e.g. ``crewai`` is
81
+ # in extras but the user didn't install it). Normal,
82
+ # expected case — DEBUG level so it doesn't pollute
83
+ # production logs.
84
+ logger.debug("Skipped %s patch: optional dependency not installed (%s)", name, e)
85
+ return False
86
+ except Exception as e:
87
+ # Real failure. The vendor SDK probably changed a method
88
+ # signature, or the runtime environment is in an
89
+ # unexpected state. Log at WARNING with enough context
90
+ # to investigate — but don't crash the SDK init.
91
+ logger.warning(
92
+ "Auto-instrumentation patch %s failed: %s: %s. "
93
+ "This is a silent cost-tracking gap — please report "
94
+ "this log line.",
95
+ name,
96
+ type(e).__name__,
97
+ e,
98
+ )
99
+ return False