haiway 0.19.4__py3-none-any.whl → 0.20.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.
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from collections.abc import Mapping
3
3
  from typing import Any, ClassVar, Self, cast, final
4
+ from uuid import UUID
4
5
 
5
6
  from opentelemetry import metrics, trace
6
7
  from opentelemetry._logs import get_logger, set_logger_provider
@@ -37,6 +38,14 @@ __all__ = ("OpenTelemetry",)
37
38
 
38
39
 
39
40
  class ScopeStore:
41
+ """
42
+ Internal class for storing and managing OpenTelemetry scope data.
43
+
44
+ This class tracks scope state including its span, meter, logger, and context.
45
+ It manages the lifecycle of OpenTelemetry resources for a specific scope,
46
+ including recording logs, metrics, events, and maintaining the context hierarchy.
47
+ """
48
+
40
49
  __slots__ = (
41
50
  "_completed",
42
51
  "_counters",
@@ -59,6 +68,22 @@ class ScopeStore:
59
68
  meter: Meter,
60
69
  logger: Logger,
61
70
  ) -> None:
71
+ """
72
+ Initialize a new scope store with OpenTelemetry resources.
73
+
74
+ Parameters
75
+ ----------
76
+ identifier : ScopeIdentifier
77
+ The identifier for this scope
78
+ context : Context
79
+ The OpenTelemetry context for this scope
80
+ span : Span
81
+ The OpenTelemetry span for this scope
82
+ meter : Meter
83
+ The OpenTelemetry meter for recording metrics
84
+ logger : Logger
85
+ The OpenTelemetry logger for recording logs
86
+ """
62
87
  self.identifier: ScopeIdentifier = identifier
63
88
  self.nested: list[ScopeStore] = []
64
89
  self._counters: dict[str, Counter] = {}
@@ -75,17 +100,59 @@ class ScopeStore:
75
100
 
76
101
  @property
77
102
  def exited(self) -> bool:
103
+ """
104
+ Check if this scope has been marked as exited.
105
+
106
+ Returns
107
+ -------
108
+ bool
109
+ True if the scope has been exited, False otherwise
110
+ """
78
111
  return self._exited
79
112
 
80
113
  def exit(self) -> None:
114
+ """
115
+ Mark this scope as exited.
116
+
117
+ Raises
118
+ ------
119
+ AssertionError
120
+ If the scope has already been exited
121
+ """
81
122
  assert not self._exited # nosec: B101
82
123
  self._exited = True
83
124
 
84
125
  @property
85
126
  def completed(self) -> bool:
127
+ """
128
+ Check if this scope and all its nested scopes are completed.
129
+
130
+ A scope is considered completed when it has been marked as completed
131
+ and all of its nested scopes are also completed.
132
+
133
+ Returns
134
+ -------
135
+ bool
136
+ True if the scope and all nested scopes are completed
137
+ """
86
138
  return self._completed and all(nested.completed for nested in self.nested)
87
139
 
88
140
  def try_complete(self) -> bool:
141
+ """
142
+ Try to complete this scope if all conditions are met.
143
+
144
+ A scope can be completed if:
145
+ - It has been exited
146
+ - It has not already been completed
147
+ - All nested scopes are completed
148
+
149
+ When completed, the span is ended and the context token is detached.
150
+
151
+ Returns
152
+ -------
153
+ bool
154
+ True if the scope was successfully completed, False otherwise
155
+ """
89
156
  if not self._exited:
90
157
  return False # not elegible for completion yet
91
158
 
@@ -107,6 +174,19 @@ class ScopeStore:
107
174
  /,
108
175
  level: ObservabilityLevel,
109
176
  ) -> None:
177
+ """
178
+ Record a log message with the specified level.
179
+
180
+ Creates a LogRecord with the current span context and scope identifiers,
181
+ and emits it through the OpenTelemetry logger.
182
+
183
+ Parameters
184
+ ----------
185
+ message : str
186
+ The log message to record
187
+ level : ObservabilityLevel
188
+ The severity level of the log
189
+ """
110
190
  span_context: SpanContext = self.span.get_span_context()
111
191
  self.logger.emit(
112
192
  LogRecord(
@@ -116,11 +196,6 @@ class ScopeStore:
116
196
  body=message,
117
197
  severity_text=level.name,
118
198
  severity_number=SEVERITY_MAPPING[level],
119
- attributes={
120
- "context.trace_id": self.identifier.trace_id,
121
- "context.scope_id": self.identifier.scope_id,
122
- "context.parent_id": self.identifier.parent_id,
123
- },
124
199
  )
125
200
  )
126
201
 
@@ -129,6 +204,14 @@ class ScopeStore:
129
204
  exception: BaseException,
130
205
  /,
131
206
  ) -> None:
207
+ """
208
+ Record an exception in the current span.
209
+
210
+ Parameters
211
+ ----------
212
+ exception : BaseException
213
+ The exception to record
214
+ """
132
215
  self.span.record_exception(exception)
133
216
 
134
217
  def record_event(
@@ -138,6 +221,16 @@ class ScopeStore:
138
221
  *,
139
222
  attributes: Mapping[str, ObservabilityAttribute],
140
223
  ) -> None:
224
+ """
225
+ Record an event in the current span.
226
+
227
+ Parameters
228
+ ----------
229
+ event : str
230
+ The name of the event to record
231
+ attributes : Mapping[str, ObservabilityAttribute]
232
+ Attributes to attach to the event
233
+ """
141
234
  self.span.add_event(
142
235
  event,
143
236
  attributes={
@@ -156,6 +249,23 @@ class ScopeStore:
156
249
  unit: str | None,
157
250
  attributes: Mapping[str, ObservabilityAttribute],
158
251
  ) -> None:
252
+ """
253
+ Record a metric with the given name, value, and attributes.
254
+
255
+ Creates a counter if one does not already exist for the metric name,
256
+ and adds the value to it with the provided attributes.
257
+
258
+ Parameters
259
+ ----------
260
+ name : str
261
+ The name of the metric to record
262
+ value : float | int
263
+ The value to add to the metric
264
+ unit : str | None
265
+ The unit of the metric (if any)
266
+ attributes : Mapping[str, ObservabilityAttribute]
267
+ Attributes to attach to the metric
268
+ """
159
269
  if name not in self._counters:
160
270
  self._counters[name] = self.meter.create_counter(
161
271
  name=name,
@@ -165,16 +275,9 @@ class ScopeStore:
165
275
  self._counters[name].add(
166
276
  value,
167
277
  attributes={
168
- **{
169
- "context.trace_id": self.identifier.trace_id,
170
- "context.scope_id": self.identifier.scope_id,
171
- "context.parent_id": self.identifier.parent_id,
172
- },
173
- **{
174
- key: cast(Any, value)
175
- for key, value in attributes.items()
176
- if value is not None and value is not MISSING
177
- },
278
+ key: cast(Any, value)
279
+ for key, value in attributes.items()
280
+ if value is not None and value is not MISSING
178
281
  },
179
282
  )
180
283
 
@@ -183,6 +286,16 @@ class ScopeStore:
183
286
  attributes: Mapping[str, ObservabilityAttribute],
184
287
  /,
185
288
  ) -> None:
289
+ """
290
+ Record attributes in the current span.
291
+
292
+ Sets each attribute on the span, skipping None and MISSING values.
293
+
294
+ Parameters
295
+ ----------
296
+ attributes : Mapping[str, ObservabilityAttribute]
297
+ Attributes to set on the span
298
+ """
186
299
  for name, value in attributes.items():
187
300
  if value is None or value is MISSING:
188
301
  continue
@@ -195,6 +308,17 @@ class ScopeStore:
195
308
 
196
309
  @final
197
310
  class OpenTelemetry:
311
+ """
312
+ Integration with OpenTelemetry for distributed tracing, metrics, and logging.
313
+
314
+ This class provides a bridge between Haiway's observability abstractions and
315
+ the OpenTelemetry SDK, enabling distributed tracing, metrics collection, and
316
+ structured logging with minimal configuration.
317
+
318
+ The class must be configured once at application startup using the configure()
319
+ class method before it can be used.
320
+ """
321
+
198
322
  service: ClassVar[str]
199
323
  environment: ClassVar[str]
200
324
 
@@ -210,6 +334,36 @@ class OpenTelemetry:
210
334
  export_interval_millis: int = 5000,
211
335
  attributes: Mapping[str, Any] | None = None,
212
336
  ) -> type[Self]:
337
+ """
338
+ Configure the OpenTelemetry integration.
339
+
340
+ This method must be called once at application startup to configure the
341
+ OpenTelemetry SDK with the appropriate service information, exporters,
342
+ and resource attributes.
343
+
344
+ Parameters
345
+ ----------
346
+ service : str
347
+ The name of the service
348
+ version : str
349
+ The version of the service
350
+ environment : str
351
+ The deployment environment (e.g., "production", "staging")
352
+ otlp_endpoint : str | None, optional
353
+ The OTLP endpoint URL to export telemetry data to. If None, console
354
+ exporters will be used instead.
355
+ insecure : bool, default=True
356
+ Whether to use insecure connections to the OTLP endpoint
357
+ export_interval_millis : int, default=5000
358
+ How often to export metrics, in milliseconds
359
+ attributes : Mapping[str, Any] | None, optional
360
+ Additional resource attributes to include with all telemetry
361
+
362
+ Returns
363
+ -------
364
+ type[Self]
365
+ The OpenTelemetry class, for method chaining
366
+ """
213
367
  cls.service = service
214
368
  cls.environment = environment
215
369
  # Create shared resource for both metrics and traces
@@ -284,12 +438,64 @@ class OpenTelemetry:
284
438
  cls,
285
439
  level: ObservabilityLevel = ObservabilityLevel.INFO,
286
440
  ) -> Observability:
441
+ """
442
+ Create an Observability implementation using OpenTelemetry.
443
+
444
+ This method creates an Observability implementation that bridges Haiway's
445
+ observability abstractions to OpenTelemetry, allowing transparent usage
446
+ of OpenTelemetry for distributed tracing, metrics, and logging.
447
+
448
+ Parameters
449
+ ----------
450
+ level : ObservabilityLevel, default=ObservabilityLevel.INFO
451
+ The minimum observability level to record
452
+
453
+ Returns
454
+ -------
455
+ Observability
456
+ An Observability implementation that uses OpenTelemetry
457
+
458
+ Notes
459
+ -----
460
+ The OpenTelemetry class must be configured using configure() before
461
+ calling this method.
462
+ """
287
463
  tracer: Tracer = trace.get_tracer(cls.service)
288
464
  meter: Meter | None = None
289
465
  root_scope: ScopeIdentifier | None = None
290
- scopes: dict[str, ScopeStore] = {}
466
+ scopes: dict[UUID, ScopeStore] = {}
291
467
  observed_level: ObservabilityLevel = level
292
468
 
469
+ def trace_identifying(
470
+ scope: ScopeIdentifier,
471
+ /,
472
+ ) -> UUID:
473
+ """
474
+ Get the unique trace identifier for a scope.
475
+
476
+ This function retrieves the OpenTelemetry trace ID for the specified scope
477
+ and converts it to a UUID for compatibility with Haiway's observability system.
478
+
479
+ Parameters
480
+ ----------
481
+ scope: ScopeIdentifier
482
+ The scope identifier to get the trace ID for
483
+
484
+ Returns
485
+ -------
486
+ UUID
487
+ A UUID representation of the OpenTelemetry trace ID
488
+
489
+ Raises
490
+ ------
491
+ AssertionError
492
+ If called outside an initialized scope context
493
+ """
494
+ assert root_scope is not None # nosec: B101
495
+ assert scope.scope_id in scopes # nosec: B101
496
+
497
+ return UUID(int=scopes[scope.scope_id].span.get_span_context().trace_id)
498
+
293
499
  def log_recording(
294
500
  scope: ScopeIdentifier,
295
501
  /,
@@ -298,6 +504,25 @@ class OpenTelemetry:
298
504
  *args: Any,
299
505
  exception: BaseException | None,
300
506
  ) -> None:
507
+ """
508
+ Record a log message using OpenTelemetry logging.
509
+
510
+ Creates a log record with the appropriate severity level and attributes
511
+ based on the current scope context.
512
+
513
+ Parameters
514
+ ----------
515
+ scope: ScopeIdentifier
516
+ The scope identifier the log is associated with
517
+ level: ObservabilityLevel
518
+ The severity level for this log message
519
+ message: str
520
+ The log message text, may contain format placeholders
521
+ *args: Any
522
+ Format arguments for the message
523
+ exception: BaseException | None
524
+ Optional exception to associate with the log
525
+ """
301
526
  assert root_scope is not None # nosec: B101
302
527
  assert scope.scope_id in scopes # nosec: B101
303
528
 
@@ -319,6 +544,23 @@ class OpenTelemetry:
319
544
  event: str,
320
545
  attributes: Mapping[str, ObservabilityAttribute],
321
546
  ) -> None:
547
+ """
548
+ Record an event using OpenTelemetry spans.
549
+
550
+ Creates a span event with the specified name and attributes in the
551
+ current active span for the scope.
552
+
553
+ Parameters
554
+ ----------
555
+ scope: ScopeIdentifier
556
+ The scope identifier the event is associated with
557
+ level: ObservabilityLevel
558
+ The severity level for this event
559
+ event: str
560
+ The name of the event
561
+ attributes: Mapping[str, ObservabilityAttribute]
562
+ Key-value attributes associated with the event
563
+ """
322
564
  assert root_scope is not None # nosec: B101
323
565
  assert scope.scope_id in scopes # nosec: B101
324
566
 
@@ -340,6 +582,27 @@ class OpenTelemetry:
340
582
  unit: str | None,
341
583
  attributes: Mapping[str, ObservabilityAttribute],
342
584
  ) -> None:
585
+ """
586
+ Record a metric using OpenTelemetry metrics.
587
+
588
+ Records a numeric measurement using the appropriate OpenTelemetry
589
+ instrument type based on the metric name and value type.
590
+
591
+ Parameters
592
+ ----------
593
+ scope: ScopeIdentifier
594
+ The scope identifier the metric is associated with
595
+ level: ObservabilityLevel
596
+ The severity level for this metric
597
+ metric: str
598
+ The name of the metric
599
+ value: float | int
600
+ The numeric value of the metric
601
+ unit: str | None
602
+ Optional unit for the metric (e.g., "ms", "bytes")
603
+ attributes: Mapping[str, ObservabilityAttribute]
604
+ Key-value attributes associated with the metric
605
+ """
343
606
  assert root_scope is not None # nosec: B101
344
607
  assert scope.scope_id in scopes # nosec: B101
345
608
 
@@ -359,6 +622,21 @@ class OpenTelemetry:
359
622
  level: ObservabilityLevel,
360
623
  attributes: Mapping[str, ObservabilityAttribute],
361
624
  ) -> None:
625
+ """
626
+ Record standalone attributes using OpenTelemetry span attributes.
627
+
628
+ Records key-value attributes by adding them to the current active span
629
+ for the scope.
630
+
631
+ Parameters
632
+ ----------
633
+ scope: ScopeIdentifier
634
+ The scope identifier the attributes are associated with
635
+ level: ObservabilityLevel
636
+ The severity level for these attributes
637
+ attributes: Mapping[str, ObservabilityAttribute]
638
+ Key-value attributes to record
639
+ """
362
640
  if level < observed_level:
363
641
  return
364
642
 
@@ -371,6 +649,27 @@ class OpenTelemetry:
371
649
  scope: ScopeIdentifier,
372
650
  /,
373
651
  ) -> None:
652
+ """
653
+ Handle scope entry by creating a new OpenTelemetry span.
654
+
655
+ This method is called when a new scope is entered. It creates a new
656
+ OpenTelemetry span for the scope and sets up the appropriate parent-child
657
+ relationships with existing spans.
658
+
659
+ Parameters
660
+ ----------
661
+ scope: ScopeIdentifier
662
+ The identifier for the scope being entered
663
+
664
+ Returns
665
+ -------
666
+ None
667
+
668
+ Notes
669
+ -----
670
+ This method initializes the scopes dictionary entry for the new scope
671
+ and creates meter instruments if this is the first scope entry.
672
+ """
374
673
  assert scope.scope_id not in scopes # nosec: B101
375
674
 
376
675
  nonlocal root_scope
@@ -378,19 +677,18 @@ class OpenTelemetry:
378
677
 
379
678
  scope_store: ScopeStore
380
679
  if root_scope is None:
381
- meter = metrics.get_meter(scope.trace_id)
382
- context: Context = get_current()
680
+ meter = metrics.get_meter(scope.label)
681
+ context: Context = Context(
682
+ **get_current(),
683
+ # trace_id=scope.trace_id,
684
+ # span_id=scope.scope_id,
685
+ )
383
686
  scope_store = ScopeStore(
384
687
  scope,
385
688
  context=context,
386
689
  span=tracer.start_span(
387
690
  name=scope.label,
388
691
  context=context,
389
- attributes={
390
- "context.trace_id": scope.trace_id,
391
- "context.scope_id": scope.scope_id,
392
- "context.parent_id": scope.parent_id,
393
- },
394
692
  ),
395
693
  meter=meter,
396
694
  logger=get_logger(scope.label),
@@ -405,11 +703,6 @@ class OpenTelemetry:
405
703
  span=tracer.start_span(
406
704
  name=scope.label,
407
705
  context=scopes[scope.parent_id].context,
408
- attributes={
409
- "context.trace_id": scope.trace_id,
410
- "context.scope_id": scope.scope_id,
411
- "context.parent_id": scope.parent_id,
412
- },
413
706
  ),
414
707
  meter=meter,
415
708
  logger=get_logger(scope.label),
@@ -424,6 +717,28 @@ class OpenTelemetry:
424
717
  *,
425
718
  exception: BaseException | None,
426
719
  ) -> None:
720
+ """
721
+ Handle scope exit by completing the OpenTelemetry span.
722
+
723
+ This method is called when a scope is exited. It marks the scope as exited,
724
+ attempts to complete it, and ends the associated OpenTelemetry span.
725
+
726
+ Parameters
727
+ ----------
728
+ scope: ScopeIdentifier
729
+ The identifier for the scope being exited
730
+ exception: BaseException | None
731
+ Optional exception that caused the scope to exit
732
+
733
+ Returns
734
+ -------
735
+ None
736
+
737
+ Notes
738
+ -----
739
+ This method ensures proper cleanup of spans, including recording any
740
+ exception that occurred during the scope's execution.
741
+ """
427
742
  nonlocal root_scope
428
743
  nonlocal scopes
429
744
  nonlocal meter
@@ -442,7 +757,7 @@ class OpenTelemetry:
442
757
 
443
758
  # try complete parent scopes
444
759
  if scope != root_scope:
445
- parent_id: str = scope.parent_id
760
+ parent_id: UUID = scope.parent_id
446
761
  while scopes[parent_id].try_complete():
447
762
  if scopes[parent_id].identifier == root_scope:
448
763
  break
@@ -457,6 +772,7 @@ class OpenTelemetry:
457
772
  scopes = {}
458
773
 
459
774
  return Observability(
775
+ trace_identifying=trace_identifying,
460
776
  log_recording=log_recording,
461
777
  event_recording=event_recording,
462
778
  metric_recording=metric_recording,
@@ -472,3 +788,4 @@ SEVERITY_MAPPING = {
472
788
  ObservabilityLevel.WARNING: SeverityNumber.WARN,
473
789
  ObservabilityLevel.ERROR: SeverityNumber.ERROR,
474
790
  }
791
+ """Mapping from Haiway ObservabilityLevel to OpenTelemetry SeverityNumber."""
@@ -34,6 +34,14 @@ __all__ = (
34
34
 
35
35
  @final
36
36
  class AttributeAnnotation:
37
+ """
38
+ Represents a type annotation for a State attribute with additional metadata.
39
+
40
+ This class encapsulates information about a type annotation, including its
41
+ origin type, type arguments, whether it's required, and any extra metadata.
42
+ It's used internally by the State system to track and validate attribute types.
43
+ """
44
+
37
45
  __slots__ = (
38
46
  "arguments",
39
47
  "extra",
@@ -49,6 +57,20 @@ class AttributeAnnotation:
49
57
  required: bool = True,
50
58
  extra: Mapping[str, Any] | None = None,
51
59
  ) -> None:
60
+ """
61
+ Initialize a new attribute annotation.
62
+
63
+ Parameters
64
+ ----------
65
+ origin : Any
66
+ The base type of the annotation (e.g., str, int, List)
67
+ arguments : Sequence[Any] | None
68
+ Type arguments for generic types (e.g., T in List[T])
69
+ required : bool
70
+ Whether this attribute is required (cannot be omitted)
71
+ extra : Mapping[str, Any] | None
72
+ Additional metadata about the annotation
73
+ """
52
74
  self.origin: Any = origin
53
75
  self.arguments: Sequence[Any]
54
76
  if arguments is None:
@@ -71,6 +93,22 @@ class AttributeAnnotation:
71
93
  required: bool,
72
94
  /,
73
95
  ) -> Self:
96
+ """
97
+ Update the required flag for this annotation.
98
+
99
+ The resulting required flag is the logical AND of the current
100
+ flag and the provided value.
101
+
102
+ Parameters
103
+ ----------
104
+ required : bool
105
+ New required flag value to combine with the existing one
106
+
107
+ Returns
108
+ -------
109
+ Self
110
+ This annotation with the updated required flag
111
+ """
74
112
  object.__setattr__(
75
113
  self,
76
114
  "required",
@@ -80,6 +118,17 @@ class AttributeAnnotation:
80
118
  return self
81
119
 
82
120
  def __str__(self) -> str:
121
+ """
122
+ Convert this annotation to a string representation.
123
+
124
+ Returns a readable string representation of the type, including
125
+ its origin type and any type arguments.
126
+
127
+ Returns
128
+ -------
129
+ str
130
+ String representation of this annotation
131
+ """
83
132
  if alias := self.extra.get("TYPE_ALIAS"):
84
133
  return alias
85
134
 
@@ -103,6 +152,29 @@ def attribute_annotations(
103
152
  /,
104
153
  type_parameters: Mapping[str, Any],
105
154
  ) -> Mapping[str, AttributeAnnotation]:
155
+ """
156
+ Extract and process type annotations from a class.
157
+
158
+ This function analyzes a class's type hints and converts them to AttributeAnnotation
159
+ objects, which provide rich type information used by the State system for validation
160
+ and other type-related operations.
161
+
162
+ Parameters
163
+ ----------
164
+ cls : type[Any]
165
+ The class to extract annotations from
166
+ type_parameters : Mapping[str, Any]
167
+ Type parameters to substitute in generic type annotations
168
+
169
+ Returns
170
+ -------
171
+ Mapping[str, AttributeAnnotation]
172
+ A mapping of attribute names to their processed type annotations
173
+
174
+ Notes
175
+ -----
176
+ Private attributes (prefixed with underscore) and ClassVars are ignored.
177
+ """
106
178
  self_annotation = AttributeAnnotation(
107
179
  origin=cls,
108
180
  # ignore arguments here, State (and draive.DataModel) will have them resolved at this stage
@@ -573,6 +645,38 @@ def resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912
573
645
  self_annotation: AttributeAnnotation | None,
574
646
  recursion_guard: MutableMapping[str, AttributeAnnotation],
575
647
  ) -> AttributeAnnotation:
648
+ """
649
+ Resolve a Python type annotation into an AttributeAnnotation object.
650
+
651
+ This function analyzes any Python type annotation and converts it into
652
+ an AttributeAnnotation that captures its structure, including handling
653
+ for special types like unions, optionals, literals, generics, etc.
654
+
655
+ Parameters
656
+ ----------
657
+ annotation : Any
658
+ The type annotation to resolve
659
+ module : str
660
+ The module where the annotation is defined (for resolving ForwardRefs)
661
+ type_parameters : Mapping[str, Any]
662
+ Type parameters to substitute in generic type annotations
663
+ self_annotation : AttributeAnnotation | None
664
+ The annotation for Self references, if available
665
+ recursion_guard : MutableMapping[str, AttributeAnnotation]
666
+ Cache to prevent infinite recursion for recursive types
667
+
668
+ Returns
669
+ -------
670
+ AttributeAnnotation
671
+ A resolved AttributeAnnotation representing the input annotation
672
+
673
+ Raises
674
+ ------
675
+ RuntimeError
676
+ If a Self annotation is used but self_annotation is not provided
677
+ TypeError
678
+ If the annotation is of an unsupported type
679
+ """
576
680
  match get_origin(annotation) or annotation:
577
681
  case types.NoneType | None:
578
682
  return _resolve_none(