haiway 0.18.2__py3-none-any.whl → 0.19.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.
haiway/helpers/tracing.py CHANGED
@@ -1,40 +1,14 @@
1
1
  from asyncio import iscoroutinefunction
2
2
  from collections.abc import Callable, Coroutine
3
- from typing import Any, Self, cast, overload
3
+ from typing import Any, cast, overload
4
4
 
5
5
  from haiway.context import ctx
6
- from haiway.state import State
7
- from haiway.types import MISSING, Missing
6
+ from haiway.context.observability import ObservabilityLevel
7
+ from haiway.types import MISSING
8
8
  from haiway.utils import mimic_function
9
+ from haiway.utils.formatting import format_str
9
10
 
10
- __all__ = (
11
- "ResultTrace",
12
- "traced",
13
- )
14
-
15
-
16
- class ResultTrace(State):
17
- if __debug__:
18
-
19
- @classmethod
20
- def of(
21
- cls,
22
- value: Any,
23
- /,
24
- ) -> Self:
25
- return cls(result=f"{value}")
26
-
27
- else: # remove tracing for non debug runs to prevent accidental secret leaks
28
-
29
- @classmethod
30
- def of(
31
- cls,
32
- value: Any,
33
- /,
34
- ) -> Self:
35
- return cls(result=MISSING)
36
-
37
- result: str | Missing
11
+ __all__ = ("traced",)
38
12
 
39
13
 
40
14
  @overload
@@ -47,6 +21,7 @@ def traced[**Args, Result](
47
21
  @overload
48
22
  def traced[**Args, Result](
49
23
  *,
24
+ level: ObservabilityLevel = ObservabilityLevel.DEBUG,
50
25
  label: str,
51
26
  ) -> Callable[[Callable[Args, Result]], Callable[Args, Result]]: ...
52
27
 
@@ -55,6 +30,7 @@ def traced[**Args, Result](
55
30
  function: Callable[Args, Result] | None = None,
56
31
  /,
57
32
  *,
33
+ level: ObservabilityLevel = ObservabilityLevel.DEBUG,
58
34
  label: str | None = None,
59
35
  ) -> Callable[[Callable[Args, Result]], Callable[Args, Result]] | Callable[Args, Result]:
60
36
  def wrap(
@@ -67,6 +43,7 @@ def traced[**Args, Result](
67
43
  _traced_async(
68
44
  wrapped,
69
45
  label=label or wrapped.__name__,
46
+ level=level,
70
47
  ),
71
48
  )
72
49
 
@@ -74,6 +51,7 @@ def traced[**Args, Result](
74
51
  return _traced_sync(
75
52
  wrapped,
76
53
  label=label or wrapped.__name__,
54
+ level=level,
77
55
  )
78
56
 
79
57
  else: # do not trace on non debug runs
@@ -90,24 +68,39 @@ def _traced_sync[**Args, Result](
90
68
  function: Callable[Args, Result],
91
69
  /,
92
70
  label: str,
71
+ level: ObservabilityLevel,
93
72
  ) -> Callable[Args, Result]:
94
73
  def traced(
95
74
  *args: Args.args,
96
75
  **kwargs: Args.kwargs,
97
76
  ) -> Result:
98
77
  with ctx.scope(label):
99
- ctx.attributes(
100
- **{f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING}
78
+ ctx.record(
79
+ level,
80
+ attributes={
81
+ f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING
82
+ },
83
+ )
84
+ ctx.record(
85
+ level,
86
+ attributes={key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING},
101
87
  )
102
- ctx.attributes(**{key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING})
103
88
 
104
89
  try:
105
90
  result: Result = function(*args, **kwargs)
106
- ctx.event(ResultTrace.of(result))
91
+ ctx.record(
92
+ level,
93
+ event="result",
94
+ attributes={"value": format_str(result)},
95
+ )
107
96
  return result
108
97
 
109
98
  except BaseException as exc:
110
- ctx.event(ResultTrace.of(f"{type(exc)}: {exc}"))
99
+ ctx.record(
100
+ level,
101
+ event="result",
102
+ attributes={"error": f"{type(exc)}: {exc}"},
103
+ )
111
104
  raise exc
112
105
 
113
106
  return mimic_function(
@@ -120,25 +113,39 @@ def _traced_async[**Args, Result](
120
113
  function: Callable[Args, Coroutine[Any, Any, Result]],
121
114
  /,
122
115
  label: str,
116
+ level: ObservabilityLevel,
123
117
  ) -> Callable[Args, Coroutine[Any, Any, Result]]:
124
118
  async def traced(
125
119
  *args: Args.args,
126
120
  **kwargs: Args.kwargs,
127
121
  ) -> Result:
128
122
  with ctx.scope(label):
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}"})
123
+ ctx.record(
124
+ level,
125
+ attributes={
126
+ f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING
127
+ },
128
+ )
129
+ ctx.record(
130
+ level,
131
+ attributes={key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING},
132
+ )
134
133
 
135
134
  try:
136
135
  result: Result = await function(*args, **kwargs)
137
- ctx.event(ResultTrace.of(result))
136
+ ctx.record(
137
+ level,
138
+ event="result",
139
+ attributes={"value": format_str(result)},
140
+ )
138
141
  return result
139
142
 
140
143
  except BaseException as exc:
141
- ctx.event(ResultTrace.of(f"{type(exc)}: {exc}"))
144
+ ctx.record(
145
+ level,
146
+ event="result",
147
+ attributes={"error": f"{type(exc)}: {exc}"},
148
+ )
142
149
  raise exc
143
150
 
144
151
  return mimic_function(
@@ -29,6 +29,7 @@ from opentelemetry.sdk.resources import Resource
29
29
  from opentelemetry.sdk.trace import TracerProvider
30
30
  from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter, SpanExporter
31
31
  from opentelemetry.trace import Span, StatusCode, Tracer
32
+ from opentelemetry.trace.span import SpanContext
32
33
 
33
34
  from haiway.context import Observability, ObservabilityLevel, ScopeIdentifier
34
35
  from haiway.context.observability import ObservabilityAttribute
@@ -44,9 +45,9 @@ class ScopeStore:
44
45
  "_counters",
45
46
  "_exited",
46
47
  "_histograms",
48
+ "_span_context",
47
49
  "identifier",
48
50
  "logger",
49
- "logger_2",
50
51
  "meter",
51
52
  "nested",
52
53
  "span",
@@ -67,6 +68,7 @@ class ScopeStore:
67
68
  self._exited: bool = False
68
69
  self._completed: bool = False
69
70
  self.span: Span = span
71
+ self._span_context: SpanContext = span.get_span_context()
70
72
  self.meter: Meter = meter
71
73
  self.logger: Logger = logger
72
74
 
@@ -104,16 +106,16 @@ class ScopeStore:
104
106
  ) -> None:
105
107
  self.logger.emit(
106
108
  LogRecord(
107
- span_id=self.span.get_span_context().span_id,
108
- trace_id=self.span.get_span_context().trace_id,
109
- trace_flags=self.span.get_span_context().trace_flags,
109
+ span_id=self._span_context.span_id,
110
+ trace_id=self._span_context.trace_id,
111
+ trace_flags=self._span_context.trace_flags,
110
112
  body=message,
111
113
  severity_text=level.name,
112
114
  severity_number=SEVERITY_MAPPING[level],
113
115
  attributes={
114
- "trace_id": self.identifier.trace_id,
115
- "scope_id": self.identifier.scope_id,
116
- "parent_id": self.identifier.parent_id,
116
+ "context.trace_id": self.identifier.trace_id,
117
+ "context.scope_id": self.identifier.scope_id,
118
+ "context.parent_id": self.identifier.parent_id,
117
119
  },
118
120
  )
119
121
  )
@@ -127,12 +129,18 @@ class ScopeStore:
127
129
 
128
130
  def record_event(
129
131
  self,
130
- event: State,
132
+ event: str,
131
133
  /,
134
+ *,
135
+ attributes: Mapping[str, ObservabilityAttribute],
132
136
  ) -> None:
133
137
  self.span.add_event(
134
- str(type(event).__name__),
135
- attributes=event.to_mapping(recursive=True),
138
+ event,
139
+ attributes={
140
+ key: cast(Any, value)
141
+ for key, value in attributes.items()
142
+ if value is not None and value is not MISSING
143
+ },
136
144
  )
137
145
 
138
146
  def record_metric(
@@ -142,13 +150,8 @@ class ScopeStore:
142
150
  *,
143
151
  value: float | int,
144
152
  unit: str | None,
153
+ attributes: Mapping[str, ObservabilityAttribute],
145
154
  ) -> None:
146
- attributes: Mapping[str, Any] = {
147
- "trace_id": self.identifier.trace_id,
148
- "scope_id": self.identifier.scope_id,
149
- "parent_id": self.identifier.parent_id,
150
- }
151
-
152
155
  if name not in self._counters:
153
156
  self._counters[name] = self.meter.create_counter(
154
157
  name=name,
@@ -157,7 +160,18 @@ class ScopeStore:
157
160
 
158
161
  self._counters[name].add(
159
162
  value,
160
- attributes=attributes,
163
+ attributes={
164
+ **{
165
+ "context.trace_id": self.identifier.trace_id,
166
+ "context.scope_id": self.identifier.scope_id,
167
+ "context.parent_id": self.identifier.parent_id,
168
+ },
169
+ **{
170
+ key: cast(Any, value)
171
+ for key, value in attributes.items()
172
+ if value is not None and value is not MISSING
173
+ },
174
+ },
161
175
  )
162
176
 
163
177
  def record_attribute(
@@ -193,8 +207,8 @@ class OpenTelemetry:
193
207
  {
194
208
  "service.name": service,
195
209
  "service.version": version,
196
- "deployment.environment": environment,
197
210
  "service.pid": os.getpid(),
211
+ "deployment.environment": environment,
198
212
  **(attributes if attributes is not None else {}),
199
213
  },
200
214
  )
@@ -273,7 +287,6 @@ class OpenTelemetry:
273
287
  message: str,
274
288
  *args: Any,
275
289
  exception: BaseException | None,
276
- **extra: Any,
277
290
  ) -> None:
278
291
  assert root_scope is not None # nosec: B101
279
292
  assert scope.scope_id in scopes # nosec: B101
@@ -291,10 +304,10 @@ class OpenTelemetry:
291
304
  def event_recording(
292
305
  scope: ScopeIdentifier,
293
306
  /,
294
- *,
295
307
  level: ObservabilityLevel,
296
- event: State,
297
- **extra: Any,
308
+ *,
309
+ event: str,
310
+ attributes: Mapping[str, ObservabilityAttribute],
298
311
  ) -> None:
299
312
  assert root_scope is not None # nosec: B101
300
313
  assert scope.scope_id in scopes # nosec: B101
@@ -302,16 +315,20 @@ class OpenTelemetry:
302
315
  if level < observed_level:
303
316
  return
304
317
 
305
- scopes[scope.scope_id].record_event(event)
318
+ scopes[scope.scope_id].record_event(
319
+ event,
320
+ attributes=attributes,
321
+ )
306
322
 
307
323
  def metric_recording(
308
324
  scope: ScopeIdentifier,
309
325
  /,
326
+ level: ObservabilityLevel,
310
327
  *,
311
328
  metric: str,
312
329
  value: float | int,
313
330
  unit: str | None,
314
- **extra: Any,
331
+ attributes: Mapping[str, ObservabilityAttribute],
315
332
  ) -> None:
316
333
  assert root_scope is not None # nosec: B101
317
334
  assert scope.scope_id in scopes # nosec: B101
@@ -323,13 +340,18 @@ class OpenTelemetry:
323
340
  metric,
324
341
  value=value,
325
342
  unit=unit,
343
+ attributes=attributes,
326
344
  )
327
345
 
328
346
  def attributes_recording(
329
347
  scope: ScopeIdentifier,
330
348
  /,
331
- **attributes: ObservabilityAttribute,
349
+ level: ObservabilityLevel,
350
+ attributes: Mapping[str, ObservabilityAttribute],
332
351
  ) -> None:
352
+ if level < observed_level:
353
+ return
354
+
333
355
  if not attributes:
334
356
  return
335
357
 
@@ -363,9 +385,9 @@ class OpenTelemetry:
363
385
  parent_id=scope.parent_id,
364
386
  ),
365
387
  attributes={
366
- "trace_id": scope.trace_id,
367
- "scope_id": scope.scope_id,
368
- "parent_id": scope.parent_id,
388
+ "context.trace_id": scope.trace_id,
389
+ "context.scope_id": scope.scope_id,
390
+ "context.parent_id": scope.parent_id,
369
391
  },
370
392
  ),
371
393
  meter=meter,
@@ -390,9 +412,9 @@ class OpenTelemetry:
390
412
  ),
391
413
  ),
392
414
  attributes={
393
- "trace_id": scope.trace_id,
394
- "scope_id": scope.scope_id,
395
- "parent_id": scope.parent_id,
415
+ "context.trace_id": scope.trace_id,
416
+ "context.scope_id": scope.scope_id,
417
+ "context.parent_id": scope.parent_id,
396
418
  },
397
419
  ),
398
420
  meter=meter,
haiway/state/structure.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import typing
2
- from collections.abc import ItemsView, Mapping, Sequence
2
+ from collections.abc import Mapping
3
3
  from types import EllipsisType, GenericAlias
4
4
  from typing import (
5
5
  Any,
@@ -366,15 +366,8 @@ class State(metaclass=StateMeta):
366
366
  ) -> Self:
367
367
  return self.__replace__(**kwargs)
368
368
 
369
- def to_str(
370
- self,
371
- pretty: bool = False,
372
- ) -> str:
373
- if pretty:
374
- return _state_str(self)
375
-
376
- else:
377
- return self.__str__()
369
+ def to_str(self) -> str:
370
+ return self.__str__()
378
371
 
379
372
  def to_mapping(
380
373
  self,
@@ -448,144 +441,3 @@ class State(metaclass=StateMeta):
448
441
  **kwargs,
449
442
  }
450
443
  )
451
-
452
-
453
- def _attribute_str(
454
- *,
455
- key: str,
456
- value: str,
457
- ) -> str:
458
- return f"┝ {key}: {value}"
459
-
460
-
461
- def _element_str(
462
- *,
463
- key: Any,
464
- value: Any,
465
- ) -> str:
466
- return f"[{key}]: {value}"
467
-
468
-
469
- def _state_str(
470
- state: State,
471
- /,
472
- ) -> str:
473
- variables: ItemsView[str, Any] = vars(state).items()
474
-
475
- parts: list[str] = [f"┍━ {type(state).__name__}:"]
476
- for key, value in variables:
477
- value_string: str | None = _value_str(value)
478
-
479
- if value_string:
480
- parts.append(
481
- _attribute_str(
482
- key=key,
483
- value=value_string,
484
- )
485
- )
486
-
487
- else:
488
- continue # skip empty elements
489
-
490
- if parts:
491
- return "\n".join(parts) + "\n┕━"
492
-
493
- else:
494
- return "╍"
495
-
496
-
497
- def _mapping_str(
498
- dictionary: Mapping[Any, Any],
499
- /,
500
- ) -> str | None:
501
- elements: ItemsView[Any, Any] = dictionary.items()
502
-
503
- parts: list[str] = []
504
- for key, value in elements:
505
- value_string: str | None = _value_str(value)
506
-
507
- if value_string:
508
- parts.append(
509
- _element_str(
510
- key=key,
511
- value=value_string,
512
- )
513
- )
514
-
515
- else:
516
- continue # skip empty elements
517
-
518
- if parts:
519
- return "\n| " + "\n".join(parts).replace("\n", "\n| ")
520
-
521
- else:
522
- return None
523
-
524
-
525
- def _sequence_str(
526
- sequence: Sequence[Any],
527
- /,
528
- ) -> str | None:
529
- parts: list[str] = []
530
- for idx, element in enumerate(sequence):
531
- element_string: str | None = _value_str(element)
532
-
533
- if element_string:
534
- parts.append(
535
- _element_str(
536
- key=idx,
537
- value=element_string,
538
- )
539
- )
540
-
541
- else:
542
- continue # skip empty elements
543
-
544
- if parts:
545
- return "\n| " + "\n".join(parts).replace("\n", "\n| ")
546
-
547
- else:
548
- return None
549
-
550
-
551
- def _raw_value_str(
552
- value: Any,
553
- /,
554
- ) -> str | None:
555
- if value is MISSING:
556
- return None # skip missing
557
-
558
- else:
559
- return str(value).strip().replace("\n", "\n| ")
560
-
561
-
562
- def _value_str( # noqa: PLR0911
563
- value: Any,
564
- /,
565
- ) -> str | None:
566
- # check for string
567
- if isinstance(value, str):
568
- if "\n" in value:
569
- return f'"""\n{value}\n"""'.replace("\n", "\n| ")
570
-
571
- else:
572
- return f'"{value}"'
573
-
574
- # check for bytes
575
- elif isinstance(value, bytes):
576
- return f'b"{value}"'
577
-
578
- # try unpack state
579
- elif isinstance(value, State):
580
- return _state_str(value)
581
-
582
- # try unpack mapping
583
- elif isinstance(value, Mapping):
584
- return _mapping_str(value)
585
-
586
- # try unpack sequence
587
- elif isinstance(value, Sequence):
588
- return _sequence_str(value)
589
-
590
- else: # fallback to other
591
- return _raw_value_str(value)
haiway/utils/__init__.py CHANGED
@@ -8,6 +8,7 @@ from haiway.utils.env import (
8
8
  getenv_str,
9
9
  load_env,
10
10
  )
11
+ from haiway.utils.formatting import format_str
11
12
  from haiway.utils.freezing import freeze
12
13
  from haiway.utils.logs import setup_logging
13
14
  from haiway.utils.mimic import mimic_function
@@ -25,6 +26,7 @@ __all__ = (
25
26
  "as_tuple",
26
27
  "async_always",
27
28
  "async_noop",
29
+ "format_str",
28
30
  "freeze",
29
31
  "getenv_base64",
30
32
  "getenv_bool",
@@ -0,0 +1,151 @@
1
+ from collections.abc import ItemsView, Mapping, Sequence
2
+ from typing import Any
3
+
4
+ from haiway.types.missing import MISSING
5
+
6
+ __all__ = ("format_str",)
7
+
8
+
9
+ def format_str( # noqa: PLR0911
10
+ value: Any,
11
+ /,
12
+ ) -> str:
13
+ # check for string
14
+ if isinstance(value, str):
15
+ if "\n" in value:
16
+ return f'"""\n{value.replace("\n", "\n ")}\n"""'
17
+
18
+ else:
19
+ return f'"{value}"'
20
+
21
+ # check for bytes
22
+ elif isinstance(value, bytes):
23
+ return f"b'{value}'"
24
+
25
+ # try unpack mapping
26
+ elif isinstance(value, Mapping):
27
+ return _mapping_str(value)
28
+
29
+ # try unpack sequence
30
+ elif isinstance(value, Sequence):
31
+ return _sequence_str(value)
32
+
33
+ elif value is MISSING:
34
+ return ""
35
+
36
+ else: # fallback to object
37
+ return _object_str(value)
38
+
39
+
40
+ def _attribute_str(
41
+ *,
42
+ key: str,
43
+ value: str,
44
+ ) -> str:
45
+ if "\n" in value:
46
+ formatted_value: str = value.replace("\n", "\n| ")
47
+ return f"┝ {key}:\n{formatted_value}"
48
+
49
+ else:
50
+ return f"┝ {key}: {value}"
51
+
52
+
53
+ def _element_str(
54
+ *,
55
+ key: Any,
56
+ value: Any,
57
+ ) -> str:
58
+ if "\n" in value:
59
+ formatted_value: str = value.replace("\n", "\n ")
60
+ return f"[{key}]:\n{formatted_value}"
61
+
62
+ else:
63
+ return f"[{key}]: {value}"
64
+
65
+
66
+ def _object_str(
67
+ other: object,
68
+ /,
69
+ ) -> str:
70
+ if not hasattr(other, "__dict__"):
71
+ return str(other)
72
+
73
+ variables: ItemsView[str, Any] = vars(other).items()
74
+
75
+ parts: list[str] = [f"┍━ {type(other).__name__}:"]
76
+ for key, value in variables:
77
+ if key.startswith("_"):
78
+ continue # skip private and dunder
79
+
80
+ value_string: str = format_str(value)
81
+
82
+ if value_string:
83
+ parts.append(
84
+ _attribute_str(
85
+ key=key,
86
+ value=value_string,
87
+ )
88
+ )
89
+
90
+ else:
91
+ continue # skip empty elements
92
+
93
+ if parts:
94
+ return "\n".join(parts) + "\n┕━"
95
+
96
+ else:
97
+ return ""
98
+
99
+
100
+ def _mapping_str(
101
+ mapping: Mapping[Any, Any],
102
+ /,
103
+ ) -> str:
104
+ items: ItemsView[Any, Any] = mapping.items()
105
+
106
+ parts: list[str] = []
107
+ for key, value in items:
108
+ value_string: str = format_str(value)
109
+
110
+ if value_string:
111
+ parts.append(
112
+ _element_str(
113
+ key=key,
114
+ value=value_string,
115
+ )
116
+ )
117
+
118
+ else:
119
+ continue # skip empty items
120
+
121
+ if parts:
122
+ return "{\n " + "\n".join(parts) + "\n}"
123
+
124
+ else:
125
+ return "{}"
126
+
127
+
128
+ def _sequence_str(
129
+ sequence: Sequence[Any],
130
+ /,
131
+ ) -> str:
132
+ parts: list[str] = []
133
+ for idx, element in enumerate(sequence):
134
+ element_string: str = format_str(element)
135
+
136
+ if element_string:
137
+ parts.append(
138
+ _element_str(
139
+ key=idx,
140
+ value=element_string,
141
+ )
142
+ )
143
+
144
+ else:
145
+ continue # skip empty elements
146
+
147
+ if parts:
148
+ return "[\n " + "\n".join(parts) + "\n]"
149
+
150
+ else:
151
+ return "[]"