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