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.
Files changed (47) hide show
  1. haiway/__init__.py +24 -18
  2. haiway/context/__init__.py +23 -13
  3. haiway/context/access.py +127 -91
  4. haiway/context/disposables.py +2 -2
  5. haiway/context/identifier.py +4 -5
  6. haiway/context/observability.py +526 -0
  7. haiway/context/state.py +2 -2
  8. haiway/context/tasks.py +1 -3
  9. haiway/context/types.py +2 -2
  10. haiway/helpers/__init__.py +5 -7
  11. haiway/helpers/asynchrony.py +2 -2
  12. haiway/helpers/caching.py +2 -2
  13. haiway/helpers/observability.py +244 -0
  14. haiway/helpers/retries.py +1 -3
  15. haiway/helpers/throttling.py +1 -3
  16. haiway/helpers/timeouted.py +1 -3
  17. haiway/helpers/tracing.py +21 -35
  18. haiway/opentelemetry/__init__.py +3 -0
  19. haiway/opentelemetry/observability.py +452 -0
  20. haiway/state/__init__.py +2 -2
  21. haiway/state/attributes.py +2 -2
  22. haiway/state/path.py +1 -3
  23. haiway/state/requirement.py +1 -3
  24. haiway/state/structure.py +161 -30
  25. haiway/state/validation.py +2 -2
  26. haiway/types/__init__.py +2 -2
  27. haiway/types/default.py +2 -2
  28. haiway/types/frozen.py +1 -3
  29. haiway/types/missing.py +2 -2
  30. haiway/utils/__init__.py +2 -2
  31. haiway/utils/always.py +2 -2
  32. haiway/utils/collections.py +2 -2
  33. haiway/utils/env.py +2 -2
  34. haiway/utils/freezing.py +1 -3
  35. haiway/utils/logs.py +1 -3
  36. haiway/utils/mimic.py +1 -3
  37. haiway/utils/noop.py +2 -2
  38. haiway/utils/queue.py +1 -3
  39. haiway/utils/stream.py +1 -3
  40. {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/METADATA +9 -5
  41. haiway-0.18.1.dist-info/RECORD +44 -0
  42. haiway/context/logging.py +0 -242
  43. haiway/context/metrics.py +0 -176
  44. haiway/helpers/metrics.py +0 -465
  45. haiway-0.17.0.dist-info/RECORD +0 -43
  46. {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/WHEEL +0 -0
  47. {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
@@ -6,9 +6,7 @@ from typing import Any, cast, overload
6
6
  from haiway.context import ctx
7
7
  from haiway.utils import mimic_function
8
8
 
9
- __all__ = [
10
- "retry",
11
- ]
9
+ __all__ = ("retry",)
12
10
 
13
11
 
14
12
  @overload
@@ -11,9 +11,7 @@ from typing import Any, cast, overload
11
11
 
12
12
  from haiway.utils.mimic import mimic_function
13
13
 
14
- __all__ = [
15
- "throttle",
16
- ]
14
+ __all__ = ("throttle",)
17
15
 
18
16
 
19
17
  @overload
@@ -4,9 +4,7 @@ from typing import Any
4
4
 
5
5
  from haiway.utils.mimic import mimic_function
6
6
 
7
- __all__ = [
8
- "timeout",
9
- ]
7
+ __all__ = ("timeout",)
10
8
 
11
9
 
12
10
  def timeout[**Args, Result](
haiway/helpers/tracing.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from asyncio import iscoroutinefunction
2
- from collections.abc import Callable, Coroutine, Mapping, Sequence
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: Any | Missing
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
- ctx.record(ArgumentsTrace.of(*args, **kwargs))
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.record(ResultTrace.of(result))
107
+ ctx.event(ResultTrace.of(result))
127
108
  return result
128
109
 
129
110
  except BaseException as exc:
130
- ctx.record(ResultTrace.of(f"{type(exc)}: {exc}"))
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
- ctx.record(ArgumentsTrace.of(*args, **kwargs))
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.record(ResultTrace.of(result))
138
+ ctx.event(ResultTrace.of(result))
153
139
  return result
154
140
 
155
141
  except BaseException as exc:
156
- ctx.record(ResultTrace.of(f"{type(exc)}: {exc}"))
142
+ ctx.event(ResultTrace.of(f"{type(exc)}: {exc}"))
157
143
  raise exc
158
144
 
159
145
  return mimic_function(
@@ -0,0 +1,3 @@
1
+ from haiway.opentelemetry.observability import OpenTelemetry
2
+
3
+ __all__ = ("OpenTelemetry",)