haiway 0.18.0__py3-none-any.whl → 0.18.2__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 CHANGED
@@ -4,6 +4,8 @@ from haiway.context import (
4
4
  MissingContext,
5
5
  MissingState,
6
6
  Observability,
7
+ ObservabilityAttribute,
8
+ ObservabilityAttributesRecording,
7
9
  ObservabilityContext,
8
10
  ObservabilityEventRecording,
9
11
  ObservabilityLevel,
@@ -17,7 +19,7 @@ from haiway.context import (
17
19
  ctx,
18
20
  )
19
21
  from haiway.helpers import (
20
- ArgumentsTrace,
22
+ LoggerObservability,
21
23
  ResultTrace,
22
24
  asynchronous,
23
25
  cache,
@@ -63,7 +65,6 @@ from haiway.utils import (
63
65
 
64
66
  __all__ = (
65
67
  "MISSING",
66
- "ArgumentsTrace",
67
68
  "AsyncQueue",
68
69
  "AsyncStream",
69
70
  "AttributePath",
@@ -72,10 +73,13 @@ __all__ = (
72
73
  "DefaultValue",
73
74
  "Disposable",
74
75
  "Disposables",
76
+ "LoggerObservability",
75
77
  "Missing",
76
78
  "MissingContext",
77
79
  "MissingState",
78
80
  "Observability",
81
+ "ObservabilityAttribute",
82
+ "ObservabilityAttributesRecording",
79
83
  "ObservabilityContext",
80
84
  "ObservabilityEventRecording",
81
85
  "ObservabilityLevel",
@@ -3,6 +3,8 @@ from haiway.context.disposables import Disposable, Disposables
3
3
  from haiway.context.identifier import ScopeIdentifier
4
4
  from haiway.context.observability import (
5
5
  Observability,
6
+ ObservabilityAttribute,
7
+ ObservabilityAttributesRecording,
6
8
  ObservabilityContext,
7
9
  ObservabilityEventRecording,
8
10
  ObservabilityLevel,
@@ -20,6 +22,8 @@ __all__ = (
20
22
  "MissingContext",
21
23
  "MissingState",
22
24
  "Observability",
25
+ "ObservabilityAttribute",
26
+ "ObservabilityAttributesRecording",
23
27
  "ObservabilityContext",
24
28
  "ObservabilityEventRecording",
25
29
  "ObservabilityLevel",
haiway/context/access.py CHANGED
@@ -18,7 +18,12 @@ from typing import Any, final, overload
18
18
 
19
19
  from haiway.context.disposables import Disposable, Disposables
20
20
  from haiway.context.identifier import ScopeIdentifier
21
- from haiway.context.observability import Observability, ObservabilityContext, ObservabilityLevel
21
+ from haiway.context.observability import (
22
+ Observability,
23
+ ObservabilityAttribute,
24
+ ObservabilityContext,
25
+ ObservabilityLevel,
26
+ )
22
27
  from haiway.context.state import ScopeState, StateContext
23
28
  from haiway.context.tasks import TaskGroupContext
24
29
  from haiway.state import State
@@ -462,6 +467,7 @@ class ctx:
462
467
  /,
463
468
  *args: Any,
464
469
  exception: BaseException | None = None,
470
+ **extra: Any,
465
471
  ) -> None:
466
472
  """
467
473
  Log using ERROR level within current scope context. When there is no current scope\
@@ -488,6 +494,7 @@ class ctx:
488
494
  message,
489
495
  *args,
490
496
  exception=exception,
497
+ **extra,
491
498
  )
492
499
 
493
500
  @staticmethod
@@ -496,6 +503,7 @@ class ctx:
496
503
  /,
497
504
  *args: Any,
498
505
  exception: Exception | None = None,
506
+ **extra: Any,
499
507
  ) -> None:
500
508
  """
501
509
  Log using WARNING level within current scope context. When there is no current scope\
@@ -522,6 +530,7 @@ class ctx:
522
530
  message,
523
531
  *args,
524
532
  exception=exception,
533
+ **extra,
525
534
  )
526
535
 
527
536
  @staticmethod
@@ -529,6 +538,7 @@ class ctx:
529
538
  message: str,
530
539
  /,
531
540
  *args: Any,
541
+ **extra: Any,
532
542
  ) -> None:
533
543
  """
534
544
  Log using INFO level within current scope context. When there is no current scope\
@@ -552,6 +562,7 @@ class ctx:
552
562
  message,
553
563
  *args,
554
564
  exception=None,
565
+ **extra,
555
566
  )
556
567
 
557
568
  @staticmethod
@@ -560,6 +571,7 @@ class ctx:
560
571
  /,
561
572
  *args: Any,
562
573
  exception: Exception | None = None,
574
+ **extra: Any,
563
575
  ) -> None:
564
576
  """
565
577
  Log using DEBUG level within current scope context. When there is no current scope\
@@ -582,10 +594,7 @@ class ctx:
582
594
  """
583
595
 
584
596
  ObservabilityContext.record_log(
585
- ObservabilityLevel.DEBUG,
586
- message,
587
- *args,
588
- exception=exception,
597
+ ObservabilityLevel.DEBUG, message, *args, exception=exception, **extra
589
598
  )
590
599
 
591
600
  @staticmethod
@@ -594,6 +603,7 @@ class ctx:
594
603
  /,
595
604
  *,
596
605
  level: ObservabilityLevel = ObservabilityLevel.INFO,
606
+ **extra: Any,
597
607
  ) -> None:
598
608
  """
599
609
  Record event within current scope context.
@@ -611,6 +621,7 @@ class ctx:
611
621
  ObservabilityContext.record_event(
612
622
  event,
613
623
  level=level,
624
+ **extra,
614
625
  )
615
626
 
616
627
  @staticmethod
@@ -620,6 +631,7 @@ class ctx:
620
631
  *,
621
632
  value: float | int,
622
633
  unit: str | None = None,
634
+ **extra: Any,
623
635
  ) -> None:
624
636
  """
625
637
  Record metric within current scope context.
@@ -642,4 +654,24 @@ class ctx:
642
654
  metric,
643
655
  value=value,
644
656
  unit=unit,
657
+ **extra,
658
+ )
659
+
660
+ @staticmethod
661
+ def attributes(**attributes: ObservabilityAttribute) -> None:
662
+ """
663
+ Record attributes within current scope context.
664
+
665
+ Parameters
666
+ ----------
667
+ **attributes: ObservabilityAttribute,
668
+ attributes to be recorded within current context.
669
+
670
+ Returns
671
+ -------
672
+ None
673
+ """
674
+
675
+ ObservabilityContext.record_attributes(
676
+ **attributes,
645
677
  )
@@ -1,3 +1,4 @@
1
+ from collections.abc import Sequence
1
2
  from contextvars import ContextVar, Token
2
3
  from enum import IntEnum
3
4
  from logging import DEBUG as DEBUG_LOGGING
@@ -9,9 +10,8 @@ from types import TracebackType
9
10
  from typing import Any, Final, Protocol, Self, final, runtime_checkable
10
11
 
11
12
  from haiway.context.identifier import ScopeIdentifier
12
-
13
- # from haiway.context.logging import LoggerContext
14
13
  from haiway.state import State
14
+ from haiway.types import MISSING, Missing
15
15
 
16
16
  __all__ = (
17
17
  "DEBUG",
@@ -19,6 +19,8 @@ __all__ = (
19
19
  "INFO",
20
20
  "WARNING",
21
21
  "Observability",
22
+ "ObservabilityAttribute",
23
+ "ObservabilityAttributesRecording",
22
24
  "ObservabilityContext",
23
25
  "ObservabilityEventRecording",
24
26
  "ObservabilityLevel",
@@ -42,6 +44,19 @@ WARNING: Final[int] = ObservabilityLevel.WARNING
42
44
  INFO: Final[int] = ObservabilityLevel.INFO
43
45
  DEBUG: Final[int] = ObservabilityLevel.DEBUG
44
46
 
47
+ type ObservabilityAttribute = (
48
+ Sequence[str]
49
+ | Sequence[float]
50
+ | Sequence[int]
51
+ | Sequence[bool]
52
+ | str
53
+ | float
54
+ | int
55
+ | bool
56
+ | None
57
+ | Missing
58
+ )
59
+
45
60
 
46
61
  @runtime_checkable
47
62
  class ObservabilityLogRecording(Protocol):
@@ -53,6 +68,7 @@ class ObservabilityLogRecording(Protocol):
53
68
  message: str,
54
69
  *args: Any,
55
70
  exception: BaseException | None,
71
+ **extra: Any,
56
72
  ) -> None: ...
57
73
 
58
74
 
@@ -65,6 +81,7 @@ class ObservabilityEventRecording(Protocol):
65
81
  *,
66
82
  level: ObservabilityLevel,
67
83
  event: State,
84
+ **extra: Any,
68
85
  ) -> None: ...
69
86
 
70
87
 
@@ -78,6 +95,17 @@ class ObservabilityMetricRecording(Protocol):
78
95
  metric: str,
79
96
  value: float | int,
80
97
  unit: str | None,
98
+ **extra: Any,
99
+ ) -> None: ...
100
+
101
+
102
+ @runtime_checkable
103
+ class ObservabilityAttributesRecording(Protocol):
104
+ def __call__(
105
+ self,
106
+ scope: ScopeIdentifier,
107
+ /,
108
+ **attributes: ObservabilityAttribute,
81
109
  ) -> None: ...
82
110
 
83
111
 
@@ -103,6 +131,7 @@ class ObservabilityScopeExiting(Protocol):
103
131
 
104
132
  class Observability: # avoiding State inheritance to prevent propagation as scope state
105
133
  __slots__ = (
134
+ "attributes_recording",
106
135
  "event_recording",
107
136
  "log_recording",
108
137
  "metric_recording",
@@ -115,6 +144,7 @@ class Observability: # avoiding State inheritance to prevent propagation as sco
115
144
  log_recording: ObservabilityLogRecording,
116
145
  metric_recording: ObservabilityMetricRecording,
117
146
  event_recording: ObservabilityEventRecording,
147
+ attributes_recording: ObservabilityAttributesRecording,
118
148
  scope_entering: ObservabilityScopeEntering,
119
149
  scope_exiting: ObservabilityScopeExiting,
120
150
  ) -> None:
@@ -136,6 +166,13 @@ class Observability: # avoiding State inheritance to prevent propagation as sco
136
166
  "event_recording",
137
167
  event_recording,
138
168
  )
169
+ self.attributes_recording: ObservabilityAttributesRecording
170
+ object.__setattr__(
171
+ self,
172
+ "attributes_recording",
173
+ attributes_recording,
174
+ )
175
+
139
176
  self.scope_entering: ObservabilityScopeEntering
140
177
  object.__setattr__(
141
178
  self,
@@ -180,6 +217,7 @@ def _logger_observability(
180
217
  message: str,
181
218
  *args: Any,
182
219
  exception: BaseException | None,
220
+ **extra: Any,
183
221
  ) -> None:
184
222
  logger.log(
185
223
  level,
@@ -194,6 +232,7 @@ def _logger_observability(
194
232
  *,
195
233
  level: ObservabilityLevel,
196
234
  event: State,
235
+ **extra: Any,
197
236
  ) -> None:
198
237
  logger.log(
199
238
  level,
@@ -207,10 +246,25 @@ def _logger_observability(
207
246
  metric: str,
208
247
  value: float | int,
209
248
  unit: str | None,
249
+ **extra: Any,
210
250
  ) -> None:
211
251
  logger.log(
212
252
  INFO,
213
- f"{scope.unique_name} Recorded metric - {metric}:{value}{unit or ''}",
253
+ f"{scope.unique_name} Recorded metric: {metric}={value}{unit or ''}",
254
+ )
255
+
256
+ def attributes_recording(
257
+ scope: ScopeIdentifier,
258
+ /,
259
+ **attributes: ObservabilityAttribute,
260
+ ) -> None:
261
+ if not attributes:
262
+ return
263
+
264
+ logger.log(
265
+ INFO,
266
+ f"{scope.unique_name} Recorded attributes:"
267
+ f"\n{'\n'.join(f'{k}: {v}' for k, v in attributes.items() if v is not None and v is not MISSING)}", # noqa: E501
214
268
  )
215
269
 
216
270
  def scope_entering[Metric: State](
@@ -238,6 +292,7 @@ def _logger_observability(
238
292
  log_recording=log_recording,
239
293
  event_recording=event_recording,
240
294
  metric_recording=metric_recording,
295
+ attributes_recording=attributes_recording,
241
296
  scope_entering=scope_entering,
242
297
  scope_exiting=scope_exiting,
243
298
  )
@@ -302,6 +357,7 @@ class ObservabilityContext:
302
357
  /,
303
358
  *args: Any,
304
359
  exception: BaseException | None,
360
+ **extra: Any,
305
361
  ) -> None:
306
362
  try: # catch exceptions - we don't wan't to blow up on observability
307
363
  context: Self = cls._context.get()
@@ -313,6 +369,7 @@ class ObservabilityContext:
313
369
  message,
314
370
  *args,
315
371
  exception=exception,
372
+ **extra,
316
373
  )
317
374
 
318
375
  except LookupError:
@@ -330,6 +387,7 @@ class ObservabilityContext:
330
387
  /,
331
388
  *,
332
389
  level: ObservabilityLevel,
390
+ **extra: Any,
333
391
  ) -> None:
334
392
  try: # catch exceptions - we don't wan't to blow up on observability
335
393
  context: Self = cls._context.get()
@@ -339,6 +397,7 @@ class ObservabilityContext:
339
397
  context._scope,
340
398
  level=level,
341
399
  event=event,
400
+ **extra,
342
401
  )
343
402
 
344
403
  except Exception as exc:
@@ -356,6 +415,7 @@ class ObservabilityContext:
356
415
  *,
357
416
  value: float | int,
358
417
  unit: str | None,
418
+ **extra: Any,
359
419
  ) -> None:
360
420
  try: # catch exceptions - we don't wan't to blow up on observability
361
421
  context: Self = cls._context.get()
@@ -366,6 +426,7 @@ class ObservabilityContext:
366
426
  metric=metric,
367
427
  value=value,
368
428
  unit=unit,
429
+ **extra,
369
430
  )
370
431
 
371
432
  except Exception as exc:
@@ -375,6 +436,27 @@ class ObservabilityContext:
375
436
  exception=exc,
376
437
  )
377
438
 
439
+ @classmethod
440
+ def record_attributes(
441
+ cls,
442
+ **attributes: ObservabilityAttribute,
443
+ ) -> None:
444
+ try: # catch exceptions - we don't wan't to blow up on observability
445
+ context: Self = cls._context.get()
446
+
447
+ if context.observability is not None:
448
+ context.observability.attributes_recording(
449
+ context._scope,
450
+ **attributes,
451
+ )
452
+
453
+ except Exception as exc:
454
+ cls.record_log(
455
+ ERROR,
456
+ f"Failed to record attributes: {attributes}",
457
+ exception=exc,
458
+ )
459
+
378
460
  __slots__ = (
379
461
  "_scope",
380
462
  "_token",
@@ -1,19 +1,16 @@
1
1
  from haiway.helpers.asynchrony import asynchronous, wrap_async
2
2
  from haiway.helpers.caching import CacheMakeKey, CacheRead, CacheWrite, cache
3
+ from haiway.helpers.observability import LoggerObservability
3
4
  from haiway.helpers.retries import retry
4
5
  from haiway.helpers.throttling import throttle
5
6
  from haiway.helpers.timeouted import timeout
6
- from haiway.helpers.tracing import (
7
- ArgumentsTrace,
8
- ResultTrace,
9
- traced,
10
- )
7
+ from haiway.helpers.tracing import ResultTrace, traced
11
8
 
12
9
  __all__ = (
13
- "ArgumentsTrace",
14
10
  "CacheMakeKey",
15
11
  "CacheRead",
16
12
  "CacheWrite",
13
+ "LoggerObservability",
17
14
  "ResultTrace",
18
15
  "asynchronous",
19
16
  "cache",
@@ -3,7 +3,9 @@ from time import monotonic
3
3
  from typing import Any
4
4
 
5
5
  from haiway.context import Observability, ObservabilityLevel, ScopeIdentifier
6
+ from haiway.context.observability import ObservabilityAttribute
6
7
  from haiway.state import State
8
+ from haiway.types import MISSING
7
9
 
8
10
  __all__ = ("LoggerObservability",)
9
11
 
@@ -60,7 +62,7 @@ class ScopeStore:
60
62
  return True # successfully completed
61
63
 
62
64
 
63
- def LoggerObservability( # noqa: C901
65
+ def LoggerObservability( # noqa: C901, PLR0915
64
66
  logger: Logger,
65
67
  /,
66
68
  *,
@@ -76,6 +78,7 @@ def LoggerObservability( # noqa: C901
76
78
  message: str,
77
79
  *args: Any,
78
80
  exception: BaseException | None,
81
+ **extra: Any,
79
82
  ) -> None:
80
83
  assert root_scope is not None # nosec: B101
81
84
  assert scope.scope_id in scopes # nosec: B101
@@ -93,6 +96,7 @@ def LoggerObservability( # noqa: C901
93
96
  *,
94
97
  level: ObservabilityLevel,
95
98
  event: State,
99
+ **extra: Any,
96
100
  ) -> None:
97
101
  assert root_scope is not None # nosec: B101
98
102
  assert scope.scope_id in scopes # nosec: B101
@@ -113,11 +117,12 @@ def LoggerObservability( # noqa: C901
113
117
  metric: str,
114
118
  value: float | int,
115
119
  unit: str | None,
120
+ **extra: Any,
116
121
  ) -> None:
117
122
  assert root_scope is not None # nosec: B101
118
123
  assert scope.scope_id in scopes # nosec: B101
119
124
 
120
- metric_str: str = f"Metric - {metric}:{value}{unit or ''}"
125
+ metric_str: str = f"Metric: {metric}={value}{unit or ''}"
121
126
  if summarize_context: # store only for summary
122
127
  scopes[scope.scope_id].store.append(metric_str)
123
128
 
@@ -126,6 +131,26 @@ def LoggerObservability( # noqa: C901
126
131
  f"{scope.unique_name} {metric_str}",
127
132
  )
128
133
 
134
+ def attributes_recording(
135
+ scope: ScopeIdentifier,
136
+ /,
137
+ **attributes: ObservabilityAttribute,
138
+ ) -> None:
139
+ if not attributes:
140
+ return
141
+
142
+ attributes_str: str = (
143
+ f"{scope.unique_name} Attributes:"
144
+ f"\n{'\n'.join(f'{k}: {v}' for k, v in attributes.items() if v is not None and v is not MISSING)}" # noqa: E501
145
+ )
146
+ if summarize_context: # store only for summary
147
+ scopes[scope.scope_id].store.append(attributes_str)
148
+
149
+ logger.log(
150
+ ObservabilityLevel.INFO,
151
+ attributes_str,
152
+ )
153
+
129
154
  def scope_entering[Metric: State](
130
155
  scope: ScopeIdentifier,
131
156
  /,
@@ -196,6 +221,7 @@ def LoggerObservability( # noqa: C901
196
221
  log_recording=log_recording,
197
222
  event_recording=event_recording,
198
223
  metric_recording=metric_recording,
224
+ attributes_recording=attributes_recording,
199
225
  scope_entering=scope_entering,
200
226
  scope_exiting=scope_exiting,
201
227
  )
haiway/helpers/tracing.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from asyncio import iscoroutinefunction
2
- from collections.abc import Callable, Coroutine, 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
@@ -8,43 +8,11 @@ from haiway.types import MISSING, Missing
8
8
  from haiway.utils import mimic_function
9
9
 
10
10
  __all__ = (
11
- "ArgumentsTrace",
12
11
  "ResultTrace",
13
12
  "traced",
14
13
  )
15
14
 
16
15
 
17
- class ArgumentsTrace(State):
18
- if __debug__:
19
-
20
- @classmethod
21
- def of(
22
- cls,
23
- *args: Any,
24
- **kwargs: Any,
25
- ) -> Self:
26
- return cls(
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,
29
- )
30
-
31
- else: # remove tracing for non debug runs to prevent accidental secret leaks
32
-
33
- @classmethod
34
- def of(
35
- cls,
36
- *args: Any,
37
- **kwargs: Any,
38
- ) -> Self:
39
- return cls(
40
- args=MISSING,
41
- kwargs=MISSING,
42
- )
43
-
44
- args: Sequence[str] | Missing
45
- kwargs: Sequence[str] | Missing
46
-
47
-
48
16
  class ResultTrace(State):
49
17
  if __debug__:
50
18
 
@@ -128,7 +96,11 @@ def _traced_sync[**Args, Result](
128
96
  **kwargs: Args.kwargs,
129
97
  ) -> Result:
130
98
  with ctx.scope(label):
131
- ctx.event(ArgumentsTrace.of(*args, **kwargs))
99
+ ctx.attributes(
100
+ **{f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING}
101
+ )
102
+ ctx.attributes(**{key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING})
103
+
132
104
  try:
133
105
  result: Result = function(*args, **kwargs)
134
106
  ctx.event(ResultTrace.of(result))
@@ -154,7 +126,12 @@ def _traced_async[**Args, Result](
154
126
  **kwargs: Args.kwargs,
155
127
  ) -> Result:
156
128
  with ctx.scope(label):
157
- ctx.event(ArgumentsTrace.of(*args, **kwargs))
129
+ for idx, arg in enumerate(args):
130
+ ctx.attributes(**{f"[{idx}]": f"{arg}"})
131
+
132
+ for key, arg in kwargs.items():
133
+ ctx.attributes(**{key: f"{arg}"})
134
+
158
135
  try:
159
136
  result: Result = await function(*args, **kwargs)
160
137
  ctx.event(ResultTrace.of(result))
@@ -1,8 +1,7 @@
1
1
  import os
2
2
  from collections.abc import Mapping
3
- from typing import Any, Self, final
3
+ from typing import Any, Self, cast, final
4
4
 
5
- ###
6
5
  from opentelemetry import metrics, trace
7
6
  from opentelemetry._logs import get_logger, set_logger_provider
8
7
  from opentelemetry._logs._internal import Logger
@@ -32,7 +31,9 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExport
32
31
  from opentelemetry.trace import Span, StatusCode, Tracer
33
32
 
34
33
  from haiway.context import Observability, ObservabilityLevel, ScopeIdentifier
34
+ from haiway.context.observability import ObservabilityAttribute
35
35
  from haiway.state import State
36
+ from haiway.types import MISSING
36
37
 
37
38
  __all__ = ("OpenTelemetry",)
38
39
 
@@ -159,6 +160,19 @@ class ScopeStore:
159
160
  attributes=attributes,
160
161
  )
161
162
 
163
+ def record_attribute(
164
+ self,
165
+ name: str,
166
+ /,
167
+ *,
168
+ value: ObservabilityAttribute,
169
+ ) -> None:
170
+ if value is not None and value is not MISSING:
171
+ self.span.set_attribute(
172
+ name,
173
+ value=cast(Any, value),
174
+ )
175
+
162
176
 
163
177
  @final
164
178
  class OpenTelemetry:
@@ -259,6 +273,7 @@ class OpenTelemetry:
259
273
  message: str,
260
274
  *args: Any,
261
275
  exception: BaseException | None,
276
+ **extra: Any,
262
277
  ) -> None:
263
278
  assert root_scope is not None # nosec: B101
264
279
  assert scope.scope_id in scopes # nosec: B101
@@ -279,6 +294,7 @@ class OpenTelemetry:
279
294
  *,
280
295
  level: ObservabilityLevel,
281
296
  event: State,
297
+ **extra: Any,
282
298
  ) -> None:
283
299
  assert root_scope is not None # nosec: B101
284
300
  assert scope.scope_id in scopes # nosec: B101
@@ -295,6 +311,7 @@ class OpenTelemetry:
295
311
  metric: str,
296
312
  value: float | int,
297
313
  unit: str | None,
314
+ **extra: Any,
298
315
  ) -> None:
299
316
  assert root_scope is not None # nosec: B101
300
317
  assert scope.scope_id in scopes # nosec: B101
@@ -308,6 +325,20 @@ class OpenTelemetry:
308
325
  unit=unit,
309
326
  )
310
327
 
328
+ def attributes_recording(
329
+ scope: ScopeIdentifier,
330
+ /,
331
+ **attributes: ObservabilityAttribute,
332
+ ) -> None:
333
+ if not attributes:
334
+ return
335
+
336
+ for attribute, value in attributes.items():
337
+ scopes[scope.scope_id].record_attribute(
338
+ attribute,
339
+ value=value,
340
+ )
341
+
311
342
  def scope_entering[Metric: State](
312
343
  scope: ScopeIdentifier,
313
344
  /,
@@ -407,6 +438,7 @@ class OpenTelemetry:
407
438
  log_recording=log_recording,
408
439
  event_recording=event_recording,
409
440
  metric_recording=metric_recording,
441
+ attributes_recording=attributes_recording,
410
442
  scope_entering=scope_entering,
411
443
  scope_exiting=scope_exiting,
412
444
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.18.0
3
+ Version: 0.18.2
4
4
  Summary: Framework for dependency injection and state management within structured concurrency model.
5
5
  Project-URL: Homepage, https://miquido.com
6
6
  Project-URL: Repository, https://github.com/miquido/haiway.git
@@ -1,23 +1,23 @@
1
- haiway/__init__.py,sha256=0ShohKDFyEyUpBpSfJWDFGopBWOdhdWyX3wC9tgmkVU,2243
1
+ haiway/__init__.py,sha256=GAukz1qQxO0mhvxSXdabC0JYiWw4nGKIGnmuImtDWdQ,2389
2
2
  haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- haiway/context/__init__.py,sha256=hPQ7e-4515zD7vSEtSvzZpZ6rAqGceyE3jeVH3eE80M,966
4
- haiway/context/access.py,sha256=Oxl5Hs6jDvD6DMt8Im8MpIXswXaFW5PGWqGF9rsryGE,17951
3
+ haiway/context/__init__.py,sha256=eoxqxUmFtkWLhc_gH6tqax9m90tSDie-jiP1BHruTdk,1102
4
+ haiway/context/access.py,sha256=49yzFt9yJU6p0pDE3jV_ZIvmgSk6_czzOOT2gijSJk8,18648
5
5
  haiway/context/disposables.py,sha256=xE8RZYsYgXiOZY_TjWR7UiPG6dirna6y2LBZvMwTkIs,2588
6
6
  haiway/context/identifier.py,sha256=i6nO-tozps7iDnpS5Se7CRch7hh6z2akjZthutxHte8,3943
7
- haiway/context/observability.py,sha256=rkq2YYMSQE3ryXdX_OvUxsWP9G4B6wHJM0_PIgURwo4,11666
7
+ haiway/context/observability.py,sha256=jgmRP2iKu3AF2UbAh6o3w7wZK9Qrr8bsKlUF4eyOZh4,13831
8
8
  haiway/context/state.py,sha256=0oq7ctNO0urJd7rVzwwNtgpguoXuI6Tp1exfCsxrS2M,5981
9
9
  haiway/context/tasks.py,sha256=QOxFdjmMp4IYff0ihHElKLCQrcVksSJmxqTlOKfoH4o,2907
10
10
  haiway/context/types.py,sha256=WulPvpqUbI1vYyny-s2NItldDnk3zh1O-n_hGibFZRY,142
11
- haiway/helpers/__init__.py,sha256=bVOCI0RHh6JNY_B8aJyw-fh1pDD10WnCPR8nVGX4NA0,582
11
+ haiway/helpers/__init__.py,sha256=awAEVFv9_talefS6ymMbofDXigB-Klsxkj2uth8la-g,615
12
12
  haiway/helpers/asynchrony.py,sha256=k_A0yCWUKSFfzYZ8WvqK4wqTMljv6ykMivmERrDLHIU,6266
13
13
  haiway/helpers/caching.py,sha256=4WX2Md5AOncduYB_RLLENI2s9C2zD5kNJgKZjMzPIGY,13257
14
- haiway/helpers/observability.py,sha256=PdlTR8OGxbkWcVcnTAhpr2OzvgM2Oig1B0KhFKvQjLI,6241
14
+ haiway/helpers/observability.py,sha256=Su9C4H_w8RuAXM0S24Yv6UDU5A6jnhZgwYhc9UNTL84,7075
15
15
  haiway/helpers/retries.py,sha256=unssUKBDOENvquh6R4Ud65TuSKl4mTHgZ5N_b7mAYa4,7533
16
16
  haiway/helpers/throttling.py,sha256=U6HJvSzffw47730VeiXxXSW4VVxpDx48k0oIAOpL-O4,4115
17
17
  haiway/helpers/timeouted.py,sha256=_M8diuD_GN49pl5KQA5fMKn4iUHsUuhkDSatAwWXiK8,3331
18
- haiway/helpers/tracing.py,sha256=UpwhNEoTefy1oplz_tqMFrN2AcmPRkKU7UYVENVykM0,4161
18
+ haiway/helpers/tracing.py,sha256=v4vRpkXKV5AEdrnvQ3MGsaXHKKqXvwQ0V5xWXOg6RrU,3692
19
19
  haiway/opentelemetry/__init__.py,sha256=TV-1C14mDAtcHhFZ29ActFQdrGH6x5KuGV9w-JlKYJg,91
20
- haiway/opentelemetry/observability.py,sha256=4c22MqSTBbYoadXTBbbBJTPik1GQ7LjjpMz2qeOKXf4,13439
20
+ haiway/opentelemetry/observability.py,sha256=71jDpoxwye-_dEqSxRd_g6Z9_8fz0dYUOwdfOHla1uk,14380
21
21
  haiway/state/__init__.py,sha256=AaMqlMhO4zKS_XNevy3A7BHh5PxmguA-Sk_FnaNDY1Q,355
22
22
  haiway/state/attributes.py,sha256=p6jUBzg62bOl0zAYTCa7NIllsaNY2Kt68IooQ9tb-y8,23311
23
23
  haiway/state/path.py,sha256=-IpbUpF2QHWg3hEITkWYHJ6ZPoRVixu-SOSuWk-bbBY,21318
@@ -38,7 +38,7 @@ haiway/utils/mimic.py,sha256=L5AS4WEL2aPMZAQZlvLvRzHl0cipI7ivky60_eL4iwY,1822
38
38
  haiway/utils/noop.py,sha256=f54PSLHGEwCQNYXQHkPAW5NDE-tk5yjzkNL1pZj0TJQ,344
39
39
  haiway/utils/queue.py,sha256=YTvCn3wgSwLJiLqolMx44sa3304Xkv3tJG77gvfWnZs,4114
40
40
  haiway/utils/stream.py,sha256=Mjhy2S-ZDR1g_NsgS_nuBA8AgVbhrGXKvG3wjJ5mCJQ,2826
41
- haiway-0.18.0.dist-info/METADATA,sha256=HhXh_Dn5VRN-pA94h4iBM3TB9ugkqKIUrbwCWZ6sob4,4527
42
- haiway-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
- haiway-0.18.0.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
44
- haiway-0.18.0.dist-info/RECORD,,
41
+ haiway-0.18.2.dist-info/METADATA,sha256=Sf5zs_gFdyGBy5MtZQyM68b4zTnvuf9M2otV7_3ricM,4527
42
+ haiway-0.18.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
+ haiway-0.18.2.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
44
+ haiway-0.18.2.dist-info/RECORD,,