haiway 0.17.0__py3-none-any.whl → 0.18.1__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.
- haiway/__init__.py +24 -18
- haiway/context/__init__.py +23 -13
- haiway/context/access.py +127 -91
- haiway/context/disposables.py +2 -2
- haiway/context/identifier.py +4 -5
- haiway/context/observability.py +526 -0
- haiway/context/state.py +2 -2
- haiway/context/tasks.py +1 -3
- haiway/context/types.py +2 -2
- haiway/helpers/__init__.py +5 -7
- haiway/helpers/asynchrony.py +2 -2
- haiway/helpers/caching.py +2 -2
- haiway/helpers/observability.py +244 -0
- haiway/helpers/retries.py +1 -3
- haiway/helpers/throttling.py +1 -3
- haiway/helpers/timeouted.py +1 -3
- haiway/helpers/tracing.py +21 -35
- haiway/opentelemetry/__init__.py +3 -0
- haiway/opentelemetry/observability.py +452 -0
- haiway/state/__init__.py +2 -2
- haiway/state/attributes.py +2 -2
- haiway/state/path.py +1 -3
- haiway/state/requirement.py +1 -3
- haiway/state/structure.py +161 -30
- haiway/state/validation.py +2 -2
- haiway/types/__init__.py +2 -2
- haiway/types/default.py +2 -2
- haiway/types/frozen.py +1 -3
- haiway/types/missing.py +2 -2
- haiway/utils/__init__.py +2 -2
- haiway/utils/always.py +2 -2
- haiway/utils/collections.py +2 -2
- haiway/utils/env.py +2 -2
- haiway/utils/freezing.py +1 -3
- haiway/utils/logs.py +1 -3
- haiway/utils/mimic.py +1 -3
- haiway/utils/noop.py +2 -2
- haiway/utils/queue.py +1 -3
- haiway/utils/stream.py +1 -3
- {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/METADATA +9 -5
- haiway-0.18.1.dist-info/RECORD +44 -0
- haiway/context/logging.py +0 -242
- haiway/context/metrics.py +0 -176
- haiway/helpers/metrics.py +0 -465
- haiway-0.17.0.dist-info/RECORD +0 -43
- {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/WHEEL +0 -0
- {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,244 @@
|
|
1
|
+
from collections.abc import Sequence
|
2
|
+
from logging import Logger
|
3
|
+
from time import monotonic
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from haiway.context import Observability, ObservabilityLevel, ScopeIdentifier
|
7
|
+
from haiway.state import State
|
8
|
+
|
9
|
+
__all__ = ("LoggerObservability",)
|
10
|
+
|
11
|
+
|
12
|
+
class ScopeStore:
|
13
|
+
__slots__ = (
|
14
|
+
"_completed",
|
15
|
+
"_exited",
|
16
|
+
"entered",
|
17
|
+
"identifier",
|
18
|
+
"nested",
|
19
|
+
"store",
|
20
|
+
)
|
21
|
+
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
identifier: ScopeIdentifier,
|
25
|
+
/,
|
26
|
+
) -> None:
|
27
|
+
self.identifier: ScopeIdentifier = identifier
|
28
|
+
self.nested: list[ScopeStore] = []
|
29
|
+
self.entered: float = monotonic()
|
30
|
+
self._exited: float | None = None
|
31
|
+
self._completed: float | None = None
|
32
|
+
self.store: list[str] = []
|
33
|
+
|
34
|
+
@property
|
35
|
+
def time(self) -> float:
|
36
|
+
return (self._completed or monotonic()) - self.entered
|
37
|
+
|
38
|
+
@property
|
39
|
+
def exited(self) -> bool:
|
40
|
+
return self._exited is not None
|
41
|
+
|
42
|
+
def exit(self) -> None:
|
43
|
+
assert self._exited is None # nosec: B101
|
44
|
+
self._exited = monotonic()
|
45
|
+
|
46
|
+
@property
|
47
|
+
def completed(self) -> bool:
|
48
|
+
return self._completed is not None and all(nested.completed for nested in self.nested)
|
49
|
+
|
50
|
+
def try_complete(self) -> bool:
|
51
|
+
if self._exited is None:
|
52
|
+
return False # not elegible for completion yet
|
53
|
+
|
54
|
+
if self._completed is not None:
|
55
|
+
return False # already completed
|
56
|
+
|
57
|
+
if not all(nested.completed for nested in self.nested):
|
58
|
+
return False # nested not completed
|
59
|
+
|
60
|
+
self._completed = monotonic()
|
61
|
+
return True # successfully completed
|
62
|
+
|
63
|
+
|
64
|
+
def LoggerObservability( # noqa: C901, PLR0915
|
65
|
+
logger: Logger,
|
66
|
+
/,
|
67
|
+
*,
|
68
|
+
summarize_context: bool = __debug__,
|
69
|
+
) -> Observability:
|
70
|
+
root_scope: ScopeIdentifier | None = None
|
71
|
+
scopes: dict[str, ScopeStore] = {}
|
72
|
+
|
73
|
+
def log_recording(
|
74
|
+
scope: ScopeIdentifier,
|
75
|
+
/,
|
76
|
+
level: ObservabilityLevel,
|
77
|
+
message: str,
|
78
|
+
*args: Any,
|
79
|
+
exception: BaseException | None,
|
80
|
+
**extra: Any,
|
81
|
+
) -> None:
|
82
|
+
assert root_scope is not None # nosec: B101
|
83
|
+
assert scope.scope_id in scopes # nosec: B101
|
84
|
+
|
85
|
+
logger.log(
|
86
|
+
level,
|
87
|
+
f"{scope.unique_name} {message}",
|
88
|
+
*args,
|
89
|
+
exc_info=exception,
|
90
|
+
)
|
91
|
+
|
92
|
+
def event_recording(
|
93
|
+
scope: ScopeIdentifier,
|
94
|
+
/,
|
95
|
+
*,
|
96
|
+
level: ObservabilityLevel,
|
97
|
+
event: State,
|
98
|
+
**extra: Any,
|
99
|
+
) -> None:
|
100
|
+
assert root_scope is not None # nosec: B101
|
101
|
+
assert scope.scope_id in scopes # nosec: B101
|
102
|
+
|
103
|
+
event_str: str = f"Event:\n{event.to_str(pretty=True)}"
|
104
|
+
if summarize_context: # store only for summary
|
105
|
+
scopes[scope.scope_id].store.append(event_str)
|
106
|
+
|
107
|
+
logger.log(
|
108
|
+
level,
|
109
|
+
f"{scope.unique_name} {event_str}",
|
110
|
+
)
|
111
|
+
|
112
|
+
def metric_recording(
|
113
|
+
scope: ScopeIdentifier,
|
114
|
+
/,
|
115
|
+
*,
|
116
|
+
metric: str,
|
117
|
+
value: float | int,
|
118
|
+
unit: str | None,
|
119
|
+
**extra: Any,
|
120
|
+
) -> None:
|
121
|
+
assert root_scope is not None # nosec: B101
|
122
|
+
assert scope.scope_id in scopes # nosec: B101
|
123
|
+
|
124
|
+
metric_str: str = f"Metric: {metric}={value}{unit or ''}"
|
125
|
+
if summarize_context: # store only for summary
|
126
|
+
scopes[scope.scope_id].store.append(metric_str)
|
127
|
+
|
128
|
+
logger.log(
|
129
|
+
ObservabilityLevel.INFO,
|
130
|
+
f"{scope.unique_name} {metric_str}",
|
131
|
+
)
|
132
|
+
|
133
|
+
def attributes_recording(
|
134
|
+
scope: ScopeIdentifier,
|
135
|
+
/,
|
136
|
+
**attributes: Sequence[str | float | int] | str | float | int,
|
137
|
+
) -> None:
|
138
|
+
if not attributes:
|
139
|
+
return
|
140
|
+
|
141
|
+
attributes_str: str = (
|
142
|
+
f"{scope.unique_name} Attributes:"
|
143
|
+
f"\n{'\n'.join([f'{k}: {v}' for k, v in attributes.items()])}"
|
144
|
+
)
|
145
|
+
if summarize_context: # store only for summary
|
146
|
+
scopes[scope.scope_id].store.append(attributes_str)
|
147
|
+
|
148
|
+
logger.log(
|
149
|
+
ObservabilityLevel.INFO,
|
150
|
+
attributes_str,
|
151
|
+
)
|
152
|
+
|
153
|
+
def scope_entering[Metric: State](
|
154
|
+
scope: ScopeIdentifier,
|
155
|
+
/,
|
156
|
+
) -> None:
|
157
|
+
assert scope.scope_id not in scopes # nosec: B101
|
158
|
+
scope_store: ScopeStore = ScopeStore(scope)
|
159
|
+
scopes[scope.scope_id] = scope_store
|
160
|
+
|
161
|
+
logger.log(
|
162
|
+
ObservabilityLevel.INFO,
|
163
|
+
f"{scope.unique_name} Entering scope: {scope.label}",
|
164
|
+
)
|
165
|
+
|
166
|
+
nonlocal root_scope
|
167
|
+
if root_scope is None:
|
168
|
+
root_scope = scope
|
169
|
+
|
170
|
+
else:
|
171
|
+
scopes[scope.parent_id].nested.append(scope_store)
|
172
|
+
|
173
|
+
def scope_exiting[Metric: State](
|
174
|
+
scope: ScopeIdentifier,
|
175
|
+
/,
|
176
|
+
*,
|
177
|
+
exception: BaseException | None,
|
178
|
+
) -> None:
|
179
|
+
nonlocal root_scope
|
180
|
+
nonlocal scopes
|
181
|
+
assert root_scope is not None # nosec: B101
|
182
|
+
assert scope.scope_id in scopes # nosec: B101
|
183
|
+
|
184
|
+
scopes[scope.scope_id].exit()
|
185
|
+
|
186
|
+
if not scopes[scope.scope_id].try_complete():
|
187
|
+
return # not completed yet or already completed
|
188
|
+
|
189
|
+
logger.log(
|
190
|
+
ObservabilityLevel.INFO,
|
191
|
+
f"{scope.unique_name} Exiting scope: {scope.label}",
|
192
|
+
)
|
193
|
+
metric_str: str = f"Metric - scope_time:{scopes[scope.scope_id].time:.3f}s"
|
194
|
+
if summarize_context: # store only for summary
|
195
|
+
scopes[scope.scope_id].store.append(metric_str)
|
196
|
+
|
197
|
+
logger.log(
|
198
|
+
ObservabilityLevel.INFO,
|
199
|
+
f"{scope.unique_name} {metric_str}",
|
200
|
+
)
|
201
|
+
|
202
|
+
# try complete parent scopes
|
203
|
+
parent_id: str = scope.parent_id
|
204
|
+
while scopes[parent_id].try_complete():
|
205
|
+
parent_id = scopes[parent_id].identifier.parent_id
|
206
|
+
|
207
|
+
# check for root completion
|
208
|
+
if scopes[root_scope.scope_id].completed:
|
209
|
+
if summarize_context:
|
210
|
+
logger.log(
|
211
|
+
ObservabilityLevel.DEBUG,
|
212
|
+
f"Observability summary:\n{_tree_summary(scopes[root_scope.scope_id])}",
|
213
|
+
)
|
214
|
+
|
215
|
+
# finished root - cleanup state
|
216
|
+
root_scope = None
|
217
|
+
scopes = {}
|
218
|
+
|
219
|
+
return Observability(
|
220
|
+
log_recording=log_recording,
|
221
|
+
event_recording=event_recording,
|
222
|
+
metric_recording=metric_recording,
|
223
|
+
attributes_recording=attributes_recording,
|
224
|
+
scope_entering=scope_entering,
|
225
|
+
scope_exiting=scope_exiting,
|
226
|
+
)
|
227
|
+
|
228
|
+
|
229
|
+
def _tree_summary(scope_store: ScopeStore) -> str:
|
230
|
+
elements: list[str] = [
|
231
|
+
f"┍━ {scope_store.identifier.label} [{scope_store.identifier.scope_id}]:"
|
232
|
+
]
|
233
|
+
for element in scope_store.store:
|
234
|
+
if not element:
|
235
|
+
continue # skip empty
|
236
|
+
|
237
|
+
elements.append(f"┝ {element.replace('\n', '\n| ')}")
|
238
|
+
|
239
|
+
for nested in scope_store.nested:
|
240
|
+
nested_summary: str = _tree_summary(nested)
|
241
|
+
|
242
|
+
elements.append(f"| {nested_summary.replace('\n', '\n| ')}")
|
243
|
+
|
244
|
+
return "\n".join(elements) + "\n┕━"
|
haiway/helpers/retries.py
CHANGED
haiway/helpers/throttling.py
CHANGED
haiway/helpers/timeouted.py
CHANGED
haiway/helpers/tracing.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from asyncio import iscoroutinefunction
|
2
|
-
from collections.abc import Callable, Coroutine
|
2
|
+
from collections.abc import Callable, Coroutine
|
3
3
|
from typing import Any, Self, cast, overload
|
4
4
|
|
5
5
|
from haiway.context import ctx
|
@@ -7,34 +7,10 @@ from haiway.state import State
|
|
7
7
|
from haiway.types import MISSING, Missing
|
8
8
|
from haiway.utils import mimic_function
|
9
9
|
|
10
|
-
__all__ =
|
11
|
-
"ArgumentsTrace",
|
10
|
+
__all__ = (
|
12
11
|
"ResultTrace",
|
13
12
|
"traced",
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
class ArgumentsTrace(State):
|
18
|
-
if __debug__:
|
19
|
-
|
20
|
-
@classmethod
|
21
|
-
def of(cls, *args: Any, **kwargs: Any) -> Self:
|
22
|
-
return cls(
|
23
|
-
args=args if args else MISSING,
|
24
|
-
kwargs=kwargs if kwargs else MISSING,
|
25
|
-
)
|
26
|
-
|
27
|
-
else: # remove tracing for non debug runs to prevent accidental secret leaks
|
28
|
-
|
29
|
-
@classmethod
|
30
|
-
def of(cls, *args: Any, **kwargs: Any) -> Self:
|
31
|
-
return cls(
|
32
|
-
args=MISSING,
|
33
|
-
kwargs=MISSING,
|
34
|
-
)
|
35
|
-
|
36
|
-
args: Sequence[Any] | Missing
|
37
|
-
kwargs: Mapping[str, Any] | Missing
|
13
|
+
)
|
38
14
|
|
39
15
|
|
40
16
|
class ResultTrace(State):
|
@@ -46,7 +22,7 @@ class ResultTrace(State):
|
|
46
22
|
value: Any,
|
47
23
|
/,
|
48
24
|
) -> Self:
|
49
|
-
return cls(result=value)
|
25
|
+
return cls(result=f"{value}")
|
50
26
|
|
51
27
|
else: # remove tracing for non debug runs to prevent accidental secret leaks
|
52
28
|
|
@@ -58,7 +34,7 @@ class ResultTrace(State):
|
|
58
34
|
) -> Self:
|
59
35
|
return cls(result=MISSING)
|
60
36
|
|
61
|
-
result:
|
37
|
+
result: str | Missing
|
62
38
|
|
63
39
|
|
64
40
|
@overload
|
@@ -120,14 +96,19 @@ def _traced_sync[**Args, Result](
|
|
120
96
|
**kwargs: Args.kwargs,
|
121
97
|
) -> Result:
|
122
98
|
with ctx.scope(label):
|
123
|
-
|
99
|
+
for idx, arg in enumerate(args):
|
100
|
+
ctx.attributes(**{f"[{idx}]": f"{arg}"})
|
101
|
+
|
102
|
+
for key, arg in kwargs.items():
|
103
|
+
ctx.attributes(**{key: f"{arg}"})
|
104
|
+
|
124
105
|
try:
|
125
106
|
result: Result = function(*args, **kwargs)
|
126
|
-
ctx.
|
107
|
+
ctx.event(ResultTrace.of(result))
|
127
108
|
return result
|
128
109
|
|
129
110
|
except BaseException as exc:
|
130
|
-
ctx.
|
111
|
+
ctx.event(ResultTrace.of(f"{type(exc)}: {exc}"))
|
131
112
|
raise exc
|
132
113
|
|
133
114
|
return mimic_function(
|
@@ -146,14 +127,19 @@ def _traced_async[**Args, Result](
|
|
146
127
|
**kwargs: Args.kwargs,
|
147
128
|
) -> Result:
|
148
129
|
with ctx.scope(label):
|
149
|
-
|
130
|
+
for idx, arg in enumerate(args):
|
131
|
+
ctx.attributes(**{f"[{idx}]": f"{arg}"})
|
132
|
+
|
133
|
+
for key, arg in kwargs.items():
|
134
|
+
ctx.attributes(**{key: f"{arg}"})
|
135
|
+
|
150
136
|
try:
|
151
137
|
result: Result = await function(*args, **kwargs)
|
152
|
-
ctx.
|
138
|
+
ctx.event(ResultTrace.of(result))
|
153
139
|
return result
|
154
140
|
|
155
141
|
except BaseException as exc:
|
156
|
-
ctx.
|
142
|
+
ctx.event(ResultTrace.of(f"{type(exc)}: {exc}"))
|
157
143
|
raise exc
|
158
144
|
|
159
145
|
return mimic_function(
|