Option C oc_diagnostics
Loading...
Searching...
No Matches
instrumentation_middleware.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_diagnostics/instrumentation_middleware.py
4
5"""
6Instrumentation Middleware
7==========================
8
9Purpose
10-------
11Provide a single entry point (``apply_instrumentation``) for applying
12recursive ID instrumentation to any Dash component tree, and a dispatcher
13patch (``patch_callback_dispatcher``) that automatically instruments all
14callback output trees via Dash's ``callback_map``.
15
16Responsibilities
17----------------
18- ``apply_instrumentation()``: call ``instrument_ids()`` if instrumentation
19 is active; return the component unchanged otherwise.
20- ``patch_callback_dispatcher()``: wrap every callback in
21 ``app.callback_map`` with a thin shim that passes its return value
22 through ``apply_instrumentation()``. Never double-wraps a callback.
23- ``enable_instrumentation_middleware()``: single-call initialization
24 entry point for app setup code; call once after all callbacks are
25 registered.
26- Log diagnostic events through ``_dbg()`` at appropriate levels.
27
28Diagnostics
29-----------
30Domain: INSTRUMENTATION
31Levels:
32 L2 — lifecycle
33 L3 — semantic details
34 L4 — deep tracing
35
36Contracts
37---------
38- ``apply_instrumentation()`` must be a no-op when
39 ``instrumentation_active()`` is ``False``.
40- ``patch_callback_dispatcher()`` must never double-wrap a callback that
41 is already wrapped (guarded by ``_is_instrumentation_wrapper`` flag).
42- Must not modify the component tree when instrumentation is disabled.
43- Requires Dash (``pip install oc_diagnostics[dash]``).
44"""
45
46from dash import Dash
47from oc_diagnostics.id_instrumentation import instrument_ids
49 _dbg,
50 instrumentation_active,
51 instrumentation_verbose,
52)
53
54
55# -------------------------------------------------------------------
56# Public API: apply instrumentation to any component tree
57# -------------------------------------------------------------------
58
59def apply_instrumentation(component):
60 """
61 Apply ID instrumentation to a component tree *if enabled*.
62
63 - No manual recursion (instrument_ids handles that)
64 - No structural ID rewrites
65 - Logs only when instrumentation debug level is high enough
66 """
67
68 if not instrumentation_active():
69 return component
70
71 if instrumentation_verbose():
72 _dbg("INSTRUMENTATION", "middleware", "Applying instrumentation", level=3)
73
74 if isinstance(component, (list, tuple)):
75 return type(component)(instrument_ids(c) for c in component)
76
77 return instrument_ids(component)
78
79
80# -------------------------------------------------------------------
81# Patch Dash callback dispatcher via callback_map
82# -------------------------------------------------------------------
83
85 """
86 Wrap all callbacks in app.callback_map so that their outputs
87 are automatically passed through apply_instrumentation().
88
89 This is the mechanism already proven to work in the main app.
90 """
91
92 callback_map = getattr(app, "callback_map", None)
93
94 if not callback_map:
95 if instrumentation_verbose():
96 _dbg("INSTRUMENTATION", "dispatcher", "No callback_map found", level=2)
97 return
98
99 for output_id, cb_def in callback_map.items():
100 # Dash stores callbacks in a dict under "callback"
101 if "callback" not in cb_def:
102 if instrumentation_verbose():
103 _dbg(
104 "INSTRUMENTATION",
105 "dispatcher",
106 f"Skipping callback without 'callback' key: {output_id}",
107 level=2,
108 )
109 continue
110
111 original = cb_def["callback"]
112
113 # Avoid double wrapping
114 if getattr(original, "_is_instrumentation_wrapper", False):
115 continue
116
117 # The default-argument binding (out_id=output_id) captures the *current*
118 # value of output_id at function-definition time, preventing the classic
119 # late-binding closure bug where all wrappers would share the final value
120 # of the loop variable. See OI-12 in AI_REVIEW_SYNTHESIS V2.md.
121 def make_wrapper(original_callback, out_id=output_id):
122 def wrapper(*args, **kwargs):
123 result = original_callback(*args, **kwargs)
124
125 if not instrumentation_active():
126 return result
127
128 if instrumentation_verbose():
129 _dbg(
130 "INSTRUMENTATION",
131 "dispatcher",
132 f"Instrumenting callback output for {out_id}",
133 level=3,
134 )
135
136 return apply_instrumentation(result)
137
138 wrapper._is_instrumentation_wrapper = True
139 return wrapper
140
141 cb_def["callback"] = make_wrapper(original)
142
143
144# -------------------------------------------------------------------
145# Initialization entry point
146# -------------------------------------------------------------------
147
149 """
150 Call this once after:
151
152 - The app is created
153 - The layout is assigned
154 - All callbacks (including UI event logger) are registered
155
156 This matches the lifecycle that already works in the main app.
157 """
158
159 if instrumentation_verbose():
160 _dbg("INSTRUMENTATION", "init", "Enabling instrumentation middleware", level=3)
161