haiway 0.16.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.
- haiway/__init__.py +18 -18
- haiway/context/__init__.py +19 -15
- haiway/context/access.py +92 -144
- haiway/context/disposables.py +2 -2
- haiway/context/identifier.py +4 -5
- haiway/context/observability.py +452 -0
- haiway/context/state.py +2 -2
- haiway/context/tasks.py +1 -3
- haiway/context/types.py +2 -2
- haiway/helpers/__init__.py +7 -6
- haiway/helpers/asynchrony.py +2 -2
- haiway/helpers/caching.py +2 -2
- haiway/helpers/observability.py +219 -0
- haiway/helpers/retries.py +1 -3
- haiway/helpers/throttling.py +1 -3
- haiway/helpers/timeouted.py +1 -3
- haiway/helpers/tracing.py +25 -17
- haiway/opentelemetry/__init__.py +3 -0
- haiway/opentelemetry/observability.py +420 -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.16.0.dist-info → haiway-0.18.0.dist-info}/METADATA +9 -5
- haiway-0.18.0.dist-info/RECORD +44 -0
- haiway/context/logging.py +0 -242
- haiway/context/metrics.py +0 -214
- haiway/helpers/metrics.py +0 -501
- haiway-0.16.0.dist-info/RECORD +0 -43
- {haiway-0.16.0.dist-info → haiway-0.18.0.dist-info}/WHEEL +0 -0
- {haiway-0.16.0.dist-info → haiway-0.18.0.dist-info}/licenses/LICENSE +0 -0
haiway/context/metrics.py
DELETED
@@ -1,214 +0,0 @@
|
|
1
|
-
from contextvars import ContextVar, Token
|
2
|
-
from types import TracebackType
|
3
|
-
from typing import Any, Protocol, Self, final, runtime_checkable
|
4
|
-
|
5
|
-
from haiway.context.identifier import ScopeIdentifier
|
6
|
-
from haiway.context.logging import LoggerContext
|
7
|
-
from haiway.state import State
|
8
|
-
|
9
|
-
__all__ = [
|
10
|
-
"MetricsContext",
|
11
|
-
"MetricsHandler",
|
12
|
-
"MetricsReading",
|
13
|
-
"MetricsRecording",
|
14
|
-
"MetricsScopeEntering",
|
15
|
-
"MetricsScopeExiting",
|
16
|
-
]
|
17
|
-
|
18
|
-
|
19
|
-
@runtime_checkable
|
20
|
-
class MetricsRecording(Protocol):
|
21
|
-
def __call__(
|
22
|
-
self,
|
23
|
-
scope: ScopeIdentifier,
|
24
|
-
/,
|
25
|
-
metric: State,
|
26
|
-
) -> None: ...
|
27
|
-
|
28
|
-
|
29
|
-
@runtime_checkable
|
30
|
-
class MetricsReading(Protocol):
|
31
|
-
async def __call__[Metric: State](
|
32
|
-
self,
|
33
|
-
scope: ScopeIdentifier,
|
34
|
-
/,
|
35
|
-
*,
|
36
|
-
metric: type[Metric],
|
37
|
-
merged: bool,
|
38
|
-
) -> Metric | None: ...
|
39
|
-
|
40
|
-
|
41
|
-
@runtime_checkable
|
42
|
-
class MetricsScopeEntering(Protocol):
|
43
|
-
def __call__[Metric: State](
|
44
|
-
self,
|
45
|
-
scope: ScopeIdentifier,
|
46
|
-
/,
|
47
|
-
) -> None: ...
|
48
|
-
|
49
|
-
|
50
|
-
@runtime_checkable
|
51
|
-
class MetricsScopeExiting(Protocol):
|
52
|
-
def __call__[Metric: State](
|
53
|
-
self,
|
54
|
-
scope: ScopeIdentifier,
|
55
|
-
/,
|
56
|
-
) -> None: ...
|
57
|
-
|
58
|
-
|
59
|
-
class MetricsHandler(State):
|
60
|
-
record: MetricsRecording
|
61
|
-
read: MetricsReading
|
62
|
-
enter_scope: MetricsScopeEntering
|
63
|
-
exit_scope: MetricsScopeExiting
|
64
|
-
|
65
|
-
|
66
|
-
@final
|
67
|
-
class MetricsContext:
|
68
|
-
_context = ContextVar[Self]("MetricsContext")
|
69
|
-
|
70
|
-
@classmethod
|
71
|
-
def scope(
|
72
|
-
cls,
|
73
|
-
scope: ScopeIdentifier,
|
74
|
-
/,
|
75
|
-
*,
|
76
|
-
metrics: MetricsHandler | None,
|
77
|
-
) -> Self:
|
78
|
-
current: Self
|
79
|
-
try: # check for current scope
|
80
|
-
current = cls._context.get()
|
81
|
-
|
82
|
-
except LookupError:
|
83
|
-
# create root scope when missing
|
84
|
-
return cls(
|
85
|
-
scope=scope,
|
86
|
-
metrics=metrics,
|
87
|
-
)
|
88
|
-
|
89
|
-
# create nested scope otherwise
|
90
|
-
return cls(
|
91
|
-
scope=scope,
|
92
|
-
metrics=metrics or current._metrics,
|
93
|
-
)
|
94
|
-
|
95
|
-
@classmethod
|
96
|
-
def record(
|
97
|
-
cls,
|
98
|
-
metric: State,
|
99
|
-
/,
|
100
|
-
) -> None:
|
101
|
-
try: # catch exceptions - we don't wan't to blow up on metrics
|
102
|
-
metrics: Self = cls._context.get()
|
103
|
-
|
104
|
-
if metrics._metrics is not None:
|
105
|
-
metrics._metrics.record(
|
106
|
-
metrics._scope,
|
107
|
-
metric,
|
108
|
-
)
|
109
|
-
|
110
|
-
except Exception as exc:
|
111
|
-
LoggerContext.log_error(
|
112
|
-
"Failed to record metric: %s",
|
113
|
-
type(metric).__qualname__,
|
114
|
-
exception=exc,
|
115
|
-
)
|
116
|
-
|
117
|
-
@classmethod
|
118
|
-
async def read[Metric: State](
|
119
|
-
cls,
|
120
|
-
metric: type[Metric],
|
121
|
-
/,
|
122
|
-
merged: bool,
|
123
|
-
) -> Metric | None:
|
124
|
-
try: # catch exceptions - we don't wan't to blow up on metrics
|
125
|
-
metrics: Self = cls._context.get()
|
126
|
-
|
127
|
-
if metrics._metrics is not None:
|
128
|
-
return await metrics._metrics.read(
|
129
|
-
metrics._scope,
|
130
|
-
metric=metric,
|
131
|
-
merged=merged,
|
132
|
-
)
|
133
|
-
|
134
|
-
except Exception as exc:
|
135
|
-
LoggerContext.log_error(
|
136
|
-
"Failed to read metric: %s",
|
137
|
-
metric.__qualname__,
|
138
|
-
exception=exc,
|
139
|
-
)
|
140
|
-
|
141
|
-
__slots__ = (
|
142
|
-
"_metrics",
|
143
|
-
"_scope",
|
144
|
-
"_token",
|
145
|
-
)
|
146
|
-
|
147
|
-
def __init__(
|
148
|
-
self,
|
149
|
-
scope: ScopeIdentifier,
|
150
|
-
metrics: MetricsHandler | None,
|
151
|
-
) -> None:
|
152
|
-
self._scope: ScopeIdentifier
|
153
|
-
object.__setattr__(
|
154
|
-
self,
|
155
|
-
"_scope",
|
156
|
-
scope,
|
157
|
-
)
|
158
|
-
self._metrics: MetricsHandler | None
|
159
|
-
object.__setattr__(
|
160
|
-
self,
|
161
|
-
"_metrics",
|
162
|
-
metrics,
|
163
|
-
)
|
164
|
-
self._token: Token[MetricsContext] | None
|
165
|
-
object.__setattr__(
|
166
|
-
self,
|
167
|
-
"_token",
|
168
|
-
None,
|
169
|
-
)
|
170
|
-
|
171
|
-
def __setattr__(
|
172
|
-
self,
|
173
|
-
name: str,
|
174
|
-
value: Any,
|
175
|
-
) -> Any:
|
176
|
-
raise AttributeError(
|
177
|
-
f"Can't modify immutable {self.__class__.__qualname__},"
|
178
|
-
f" attribute - '{name}' cannot be modified"
|
179
|
-
)
|
180
|
-
|
181
|
-
def __delattr__(
|
182
|
-
self,
|
183
|
-
name: str,
|
184
|
-
) -> None:
|
185
|
-
raise AttributeError(
|
186
|
-
f"Can't modify immutable {self.__class__.__qualname__},"
|
187
|
-
f" attribute - '{name}' cannot be deleted"
|
188
|
-
)
|
189
|
-
|
190
|
-
def __enter__(self) -> None:
|
191
|
-
assert self._token is None, "Context reentrance is not allowed" # nosec: B101
|
192
|
-
object.__setattr__(
|
193
|
-
self,
|
194
|
-
"_token",
|
195
|
-
MetricsContext._context.set(self),
|
196
|
-
)
|
197
|
-
if self._metrics is not None:
|
198
|
-
self._metrics.enter_scope(self._scope)
|
199
|
-
|
200
|
-
def __exit__(
|
201
|
-
self,
|
202
|
-
exc_type: type[BaseException] | None,
|
203
|
-
exc_val: BaseException | None,
|
204
|
-
exc_tb: TracebackType | None,
|
205
|
-
) -> None:
|
206
|
-
assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
|
207
|
-
MetricsContext._context.reset(self._token)
|
208
|
-
object.__setattr__(
|
209
|
-
self,
|
210
|
-
"_token",
|
211
|
-
None,
|
212
|
-
)
|
213
|
-
if self._metrics is not None:
|
214
|
-
self._metrics.exit_scope(self._scope)
|
haiway/helpers/metrics.py
DELETED
@@ -1,501 +0,0 @@
|
|
1
|
-
from collections.abc import Sequence
|
2
|
-
from itertools import chain
|
3
|
-
from time import monotonic
|
4
|
-
from typing import Any, Self, cast, final, overload
|
5
|
-
|
6
|
-
from haiway.context import MetricsHandler, ScopeIdentifier, ctx
|
7
|
-
from haiway.state import State
|
8
|
-
from haiway.types import MISSING
|
9
|
-
|
10
|
-
__all_ = [
|
11
|
-
"MetricsLogger",
|
12
|
-
"MetricsHolder",
|
13
|
-
]
|
14
|
-
|
15
|
-
|
16
|
-
class MetricsScopeStore:
|
17
|
-
__slots__ = (
|
18
|
-
"allow_exit",
|
19
|
-
"entered",
|
20
|
-
"exited",
|
21
|
-
"identifier",
|
22
|
-
"metrics",
|
23
|
-
"nested",
|
24
|
-
)
|
25
|
-
|
26
|
-
def __init__(
|
27
|
-
self,
|
28
|
-
identifier: ScopeIdentifier,
|
29
|
-
/,
|
30
|
-
) -> None:
|
31
|
-
self.identifier: ScopeIdentifier = identifier
|
32
|
-
self.entered: float = monotonic()
|
33
|
-
self.metrics: dict[type[State], State] = {}
|
34
|
-
self.exited: float | None = None
|
35
|
-
self.allow_exit: bool = False
|
36
|
-
self.nested: list[MetricsScopeStore] = []
|
37
|
-
|
38
|
-
@property
|
39
|
-
def time(self) -> float:
|
40
|
-
return (self.exited or monotonic()) - self.entered
|
41
|
-
|
42
|
-
@property
|
43
|
-
def finished(self) -> float:
|
44
|
-
return self.exited is not None and all(nested.finished for nested in self.nested)
|
45
|
-
|
46
|
-
@overload
|
47
|
-
def merged[Metric: State](
|
48
|
-
self,
|
49
|
-
) -> Sequence[State]: ...
|
50
|
-
|
51
|
-
@overload
|
52
|
-
def merged[Metric: State](
|
53
|
-
self,
|
54
|
-
metric: type[Metric],
|
55
|
-
) -> Metric | None: ...
|
56
|
-
|
57
|
-
def merged[Metric: State](
|
58
|
-
self,
|
59
|
-
metric: type[Metric] | None = None,
|
60
|
-
) -> Sequence[State] | Metric | None:
|
61
|
-
if metric is None:
|
62
|
-
merged_metrics: dict[type[State], State] = dict(self.metrics)
|
63
|
-
for nested in chain.from_iterable(nested.merged() for nested in self.nested):
|
64
|
-
metric_type: type[State] = type(nested)
|
65
|
-
current: State | None = merged_metrics.get(metric_type)
|
66
|
-
|
67
|
-
if current is None:
|
68
|
-
merged_metrics[metric_type] = nested
|
69
|
-
continue # keep going
|
70
|
-
|
71
|
-
if hasattr(current, "__add__"):
|
72
|
-
merged_metrics[metric_type] = current.__add__(nested) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
|
73
|
-
assert isinstance(merged_metrics[metric_type], State) # nosec: B101
|
74
|
-
continue # keep going
|
75
|
-
|
76
|
-
break # we have multiple value without a way to merge
|
77
|
-
|
78
|
-
return tuple(merged_metrics.values())
|
79
|
-
|
80
|
-
else:
|
81
|
-
merged_metric: State | None = self.metrics.get(metric)
|
82
|
-
for nested in self.nested:
|
83
|
-
nested_metric: Metric | None = nested.merged(metric)
|
84
|
-
if nested_metric is None:
|
85
|
-
continue # skip missing
|
86
|
-
|
87
|
-
if merged_metric is None:
|
88
|
-
merged_metric = nested_metric
|
89
|
-
continue # keep going
|
90
|
-
|
91
|
-
if hasattr(merged_metric, "__add__"):
|
92
|
-
merged_metric = merged_metric.__add__(nested_metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
|
93
|
-
assert isinstance(merged_metric, metric) # nosec: B101
|
94
|
-
continue # keep going
|
95
|
-
|
96
|
-
break # we have multiple value without a way to merge
|
97
|
-
|
98
|
-
return cast(Metric | None, merged_metric)
|
99
|
-
|
100
|
-
|
101
|
-
@final
|
102
|
-
class MetricsHolder:
|
103
|
-
@classmethod
|
104
|
-
def handler(cls) -> MetricsHandler:
|
105
|
-
store_handler: Self = cls()
|
106
|
-
return MetricsHandler(
|
107
|
-
record=store_handler.record,
|
108
|
-
read=store_handler.read,
|
109
|
-
enter_scope=store_handler.enter_scope,
|
110
|
-
exit_scope=store_handler.exit_scope,
|
111
|
-
)
|
112
|
-
|
113
|
-
__slots__ = (
|
114
|
-
"root_scope",
|
115
|
-
"scopes",
|
116
|
-
)
|
117
|
-
|
118
|
-
def __init__(self) -> None:
|
119
|
-
self.root_scope: ScopeIdentifier | None = None
|
120
|
-
self.scopes: dict[str, MetricsScopeStore] = {}
|
121
|
-
|
122
|
-
def record(
|
123
|
-
self,
|
124
|
-
scope: ScopeIdentifier,
|
125
|
-
/,
|
126
|
-
metric: State,
|
127
|
-
) -> None:
|
128
|
-
assert self.root_scope is not None # nosec: B101
|
129
|
-
assert scope.scope_id in self.scopes # nosec: B101
|
130
|
-
|
131
|
-
metric_type: type[State] = type(metric)
|
132
|
-
metrics: dict[type[State], State] = self.scopes[scope.scope_id].metrics
|
133
|
-
if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
|
134
|
-
metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
|
135
|
-
|
136
|
-
metrics[type(metric)] = metric
|
137
|
-
|
138
|
-
async def read[Metric: State](
|
139
|
-
self,
|
140
|
-
scope: ScopeIdentifier,
|
141
|
-
/,
|
142
|
-
*,
|
143
|
-
metric: type[Metric],
|
144
|
-
merged: bool,
|
145
|
-
) -> Metric | None:
|
146
|
-
assert self.root_scope is not None # nosec: B101
|
147
|
-
assert scope.scope_id in self.scopes # nosec: B101
|
148
|
-
|
149
|
-
if merged:
|
150
|
-
return self.scopes[scope.scope_id].merged(metric)
|
151
|
-
|
152
|
-
else:
|
153
|
-
return cast(Metric | None, self.scopes[scope.scope_id].metrics.get(metric))
|
154
|
-
|
155
|
-
def enter_scope[Metric: State](
|
156
|
-
self,
|
157
|
-
scope: ScopeIdentifier,
|
158
|
-
/,
|
159
|
-
) -> None:
|
160
|
-
assert scope.scope_id not in self.scopes # nosec: B101
|
161
|
-
scope_metrics = MetricsScopeStore(scope)
|
162
|
-
self.scopes[scope.scope_id] = scope_metrics
|
163
|
-
|
164
|
-
if self.root_scope is None:
|
165
|
-
self.root_scope = scope
|
166
|
-
|
167
|
-
else:
|
168
|
-
for key in self.scopes.keys():
|
169
|
-
if key == scope.parent_id:
|
170
|
-
self.scopes[key].nested.append(scope_metrics)
|
171
|
-
return
|
172
|
-
|
173
|
-
ctx.log_debug(
|
174
|
-
"Attempting to enter nested scope metrics without entering its parent first"
|
175
|
-
)
|
176
|
-
|
177
|
-
def exit_scope[Metric: State](
|
178
|
-
self,
|
179
|
-
scope: ScopeIdentifier,
|
180
|
-
/,
|
181
|
-
) -> None:
|
182
|
-
assert self.root_scope is not None # nosec: B101
|
183
|
-
assert scope.scope_id in self.scopes # nosec: B101
|
184
|
-
|
185
|
-
self.scopes[scope.scope_id].allow_exit = True
|
186
|
-
|
187
|
-
if not all(nested.exited for nested in self.scopes[scope.scope_id].nested):
|
188
|
-
return # not completed yet
|
189
|
-
|
190
|
-
self.scopes[scope.scope_id].exited = monotonic()
|
191
|
-
|
192
|
-
if scope != self.root_scope and self.scopes[scope.parent_id].allow_exit:
|
193
|
-
self.exit_scope(self.scopes[scope.parent_id].identifier)
|
194
|
-
|
195
|
-
|
196
|
-
@final
|
197
|
-
class MetricsLogger:
|
198
|
-
@classmethod
|
199
|
-
def handler(
|
200
|
-
cls,
|
201
|
-
items_limit: int | None = None,
|
202
|
-
redact_content: bool = False,
|
203
|
-
) -> MetricsHandler:
|
204
|
-
logger_handler: Self = cls(
|
205
|
-
items_limit=items_limit,
|
206
|
-
redact_content=redact_content,
|
207
|
-
)
|
208
|
-
return MetricsHandler(
|
209
|
-
record=logger_handler.record,
|
210
|
-
read=logger_handler.read,
|
211
|
-
enter_scope=logger_handler.enter_scope,
|
212
|
-
exit_scope=logger_handler.exit_scope,
|
213
|
-
)
|
214
|
-
|
215
|
-
__slots__ = (
|
216
|
-
"items_limit",
|
217
|
-
"redact_content",
|
218
|
-
"root_scope",
|
219
|
-
"scopes",
|
220
|
-
)
|
221
|
-
|
222
|
-
def __init__(
|
223
|
-
self,
|
224
|
-
items_limit: int | None,
|
225
|
-
redact_content: bool,
|
226
|
-
) -> None:
|
227
|
-
self.root_scope: ScopeIdentifier | None = None
|
228
|
-
self.scopes: dict[str, MetricsScopeStore] = {}
|
229
|
-
self.items_limit: int | None = items_limit
|
230
|
-
self.redact_content: bool = redact_content
|
231
|
-
|
232
|
-
def record(
|
233
|
-
self,
|
234
|
-
scope: ScopeIdentifier,
|
235
|
-
/,
|
236
|
-
metric: State,
|
237
|
-
) -> None:
|
238
|
-
assert self.root_scope is not None # nosec: B101
|
239
|
-
assert scope.scope_id in self.scopes # nosec: B101
|
240
|
-
|
241
|
-
metric_type: type[State] = type(metric)
|
242
|
-
metrics: dict[type[State], State] = self.scopes[scope.scope_id].metrics
|
243
|
-
if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
|
244
|
-
metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
|
245
|
-
|
246
|
-
metrics[type(metric)] = metric
|
247
|
-
if log := _state_log(
|
248
|
-
metric,
|
249
|
-
list_items_limit=self.items_limit,
|
250
|
-
redact_content=self.redact_content,
|
251
|
-
):
|
252
|
-
ctx.log_debug(f"Recorded metric:\n⎡ {type(metric).__qualname__}:{log}\n⌊")
|
253
|
-
|
254
|
-
async def read[Metric: State](
|
255
|
-
self,
|
256
|
-
scope: ScopeIdentifier,
|
257
|
-
/,
|
258
|
-
*,
|
259
|
-
metric: type[Metric],
|
260
|
-
merged: bool,
|
261
|
-
) -> Metric | None:
|
262
|
-
assert self.root_scope is not None # nosec: B101
|
263
|
-
assert scope.scope_id in self.scopes # nosec: B101
|
264
|
-
|
265
|
-
if merged:
|
266
|
-
return self.scopes[scope.scope_id].merged(metric)
|
267
|
-
|
268
|
-
else:
|
269
|
-
return cast(Metric | None, self.scopes[scope.scope_id].metrics.get(metric))
|
270
|
-
|
271
|
-
def enter_scope[Metric: State](
|
272
|
-
self,
|
273
|
-
scope: ScopeIdentifier,
|
274
|
-
/,
|
275
|
-
) -> None:
|
276
|
-
assert scope.scope_id not in self.scopes # nosec: B101
|
277
|
-
scope_metrics = MetricsScopeStore(scope)
|
278
|
-
self.scopes[scope.scope_id] = scope_metrics
|
279
|
-
|
280
|
-
if self.root_scope is None:
|
281
|
-
self.root_scope = scope
|
282
|
-
|
283
|
-
else:
|
284
|
-
for key in self.scopes.keys():
|
285
|
-
if key == scope.parent_id:
|
286
|
-
self.scopes[key].nested.append(scope_metrics)
|
287
|
-
return
|
288
|
-
|
289
|
-
ctx.log_debug(
|
290
|
-
"Attempting to enter nested scope metrics without entering its parent first"
|
291
|
-
)
|
292
|
-
|
293
|
-
def exit_scope[Metric: State](
|
294
|
-
self,
|
295
|
-
scope: ScopeIdentifier,
|
296
|
-
/,
|
297
|
-
) -> None:
|
298
|
-
assert self.root_scope is not None # nosec: B101
|
299
|
-
assert scope.scope_id in self.scopes # nosec: B101
|
300
|
-
|
301
|
-
self.scopes[scope.scope_id].allow_exit = True
|
302
|
-
|
303
|
-
if not all(nested.exited for nested in self.scopes[scope.scope_id].nested):
|
304
|
-
return # not completed yet
|
305
|
-
|
306
|
-
self.scopes[scope.scope_id].exited = monotonic()
|
307
|
-
|
308
|
-
if scope != self.root_scope and self.scopes[scope.parent_id].allow_exit:
|
309
|
-
self.exit_scope(self.scopes[scope.parent_id].identifier)
|
310
|
-
|
311
|
-
elif scope == self.root_scope and self.scopes[self.root_scope.scope_id].finished:
|
312
|
-
if log := _tree_log(
|
313
|
-
self.scopes[scope.scope_id],
|
314
|
-
list_items_limit=self.items_limit,
|
315
|
-
redact_content=self.redact_content,
|
316
|
-
):
|
317
|
-
ctx.log_debug(f"Metrics summary:\n{log}")
|
318
|
-
|
319
|
-
|
320
|
-
def _tree_log(
|
321
|
-
metrics: MetricsScopeStore,
|
322
|
-
list_items_limit: int | None,
|
323
|
-
redact_content: bool,
|
324
|
-
) -> str:
|
325
|
-
log: str = (
|
326
|
-
f"⎡ @{metrics.identifier.label} [{metrics.identifier.scope_id}]({metrics.time:.2f}s):"
|
327
|
-
)
|
328
|
-
|
329
|
-
for metric in metrics.merged():
|
330
|
-
if type(metric) not in metrics.metrics:
|
331
|
-
continue # skip metrics not available in this scope
|
332
|
-
|
333
|
-
metric_log: str = ""
|
334
|
-
for key, value in vars(metric).items():
|
335
|
-
if value_log := _value_log(
|
336
|
-
value,
|
337
|
-
list_items_limit=list_items_limit,
|
338
|
-
redact_content=redact_content,
|
339
|
-
):
|
340
|
-
metric_log += f"\n├ {key}: {value_log}"
|
341
|
-
|
342
|
-
else:
|
343
|
-
continue # skip empty values
|
344
|
-
|
345
|
-
if not metric_log:
|
346
|
-
continue # skip empty logs
|
347
|
-
|
348
|
-
log += f"\n⎡ •{type(metric).__qualname__}:{metric_log.replace('\n', '\n| ')}\n⌊"
|
349
|
-
|
350
|
-
for nested in metrics.nested:
|
351
|
-
nested_log: str = _tree_log(
|
352
|
-
nested,
|
353
|
-
list_items_limit=list_items_limit,
|
354
|
-
redact_content=redact_content,
|
355
|
-
)
|
356
|
-
|
357
|
-
log += f"\n\n{nested_log}"
|
358
|
-
|
359
|
-
return log.strip().replace("\n", "\n| ") + "\n⌊"
|
360
|
-
|
361
|
-
|
362
|
-
def _state_log(
|
363
|
-
value: State,
|
364
|
-
/,
|
365
|
-
list_items_limit: int | None,
|
366
|
-
redact_content: bool,
|
367
|
-
) -> str | None:
|
368
|
-
state_log: str = ""
|
369
|
-
for key, element in vars(value).items():
|
370
|
-
element_log: str | None = _value_log(
|
371
|
-
element,
|
372
|
-
list_items_limit=list_items_limit,
|
373
|
-
redact_content=redact_content,
|
374
|
-
)
|
375
|
-
|
376
|
-
if element_log:
|
377
|
-
state_log += f"\n├ {key}: {element_log}"
|
378
|
-
|
379
|
-
else:
|
380
|
-
continue # skip empty logs
|
381
|
-
|
382
|
-
if state_log:
|
383
|
-
return state_log
|
384
|
-
|
385
|
-
else:
|
386
|
-
return None # skip empty logs
|
387
|
-
|
388
|
-
|
389
|
-
def _dict_log(
|
390
|
-
value: dict[Any, Any],
|
391
|
-
/,
|
392
|
-
list_items_limit: int | None,
|
393
|
-
redact_content: bool,
|
394
|
-
) -> str | None:
|
395
|
-
dict_log: str = ""
|
396
|
-
for key, element in value.items():
|
397
|
-
element_log: str | None = _value_log(
|
398
|
-
element,
|
399
|
-
list_items_limit=list_items_limit,
|
400
|
-
redact_content=redact_content,
|
401
|
-
)
|
402
|
-
if element_log:
|
403
|
-
dict_log += f"\n[{key}]: {element_log}"
|
404
|
-
|
405
|
-
else:
|
406
|
-
continue # skip empty logs
|
407
|
-
|
408
|
-
if dict_log:
|
409
|
-
return dict_log.replace("\n", "\n| ")
|
410
|
-
|
411
|
-
else:
|
412
|
-
return None # skip empty logs
|
413
|
-
|
414
|
-
|
415
|
-
def _list_log(
|
416
|
-
value: list[Any],
|
417
|
-
/,
|
418
|
-
list_items_limit: int | None,
|
419
|
-
redact_content: bool,
|
420
|
-
) -> str | None:
|
421
|
-
list_log: str = ""
|
422
|
-
enumerated: list[tuple[int, Any]] = list(enumerate(value))
|
423
|
-
if list_items_limit:
|
424
|
-
if list_items_limit > 0:
|
425
|
-
enumerated = enumerated[:list_items_limit]
|
426
|
-
|
427
|
-
else:
|
428
|
-
enumerated = enumerated[list_items_limit:]
|
429
|
-
|
430
|
-
for idx, element in enumerated:
|
431
|
-
element_log: str | None = _value_log(
|
432
|
-
element,
|
433
|
-
list_items_limit=list_items_limit,
|
434
|
-
redact_content=redact_content,
|
435
|
-
)
|
436
|
-
if element_log:
|
437
|
-
list_log += f"\n[{idx}] {element_log}"
|
438
|
-
|
439
|
-
else:
|
440
|
-
continue # skip empty logs
|
441
|
-
|
442
|
-
if list_log:
|
443
|
-
return list_log.replace("\n", "\n| ")
|
444
|
-
|
445
|
-
else:
|
446
|
-
return None # skip empty logs
|
447
|
-
|
448
|
-
|
449
|
-
def _raw_value_log(
|
450
|
-
value: Any,
|
451
|
-
/,
|
452
|
-
redact_content: bool,
|
453
|
-
) -> str | None:
|
454
|
-
if value is MISSING:
|
455
|
-
return None # skip missing
|
456
|
-
|
457
|
-
if redact_content:
|
458
|
-
return "[redacted]"
|
459
|
-
|
460
|
-
elif isinstance(value, str):
|
461
|
-
return f'"{value}"'.replace("\n", "\n| ")
|
462
|
-
|
463
|
-
else:
|
464
|
-
return str(value).strip().replace("\n", "\n| ")
|
465
|
-
|
466
|
-
|
467
|
-
def _value_log(
|
468
|
-
value: Any,
|
469
|
-
/,
|
470
|
-
list_items_limit: int | None,
|
471
|
-
redact_content: bool,
|
472
|
-
) -> str | None:
|
473
|
-
# try unpack dicts
|
474
|
-
if isinstance(value, dict):
|
475
|
-
return _dict_log(
|
476
|
-
cast(dict[Any, Any], value),
|
477
|
-
list_items_limit=list_items_limit,
|
478
|
-
redact_content=redact_content,
|
479
|
-
)
|
480
|
-
|
481
|
-
# try unpack lists
|
482
|
-
elif isinstance(value, list):
|
483
|
-
return _list_log(
|
484
|
-
cast(list[Any], value),
|
485
|
-
list_items_limit=list_items_limit,
|
486
|
-
redact_content=redact_content,
|
487
|
-
)
|
488
|
-
|
489
|
-
# try unpack state
|
490
|
-
elif isinstance(value, State):
|
491
|
-
return _state_log(
|
492
|
-
value,
|
493
|
-
list_items_limit=list_items_limit,
|
494
|
-
redact_content=redact_content,
|
495
|
-
)
|
496
|
-
|
497
|
-
else:
|
498
|
-
return _raw_value_log(
|
499
|
-
value,
|
500
|
-
redact_content=redact_content,
|
501
|
-
)
|