haiway 0.18.1__py3-none-any.whl → 0.19.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/helpers/tracing.py CHANGED
@@ -1,40 +1,13 @@
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.types import MISSING
8
7
  from haiway.utils import mimic_function
8
+ from haiway.utils.formatting import format_str
9
9
 
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
10
+ __all__ = ("traced",)
38
11
 
39
12
 
40
13
  @overload
@@ -96,19 +69,28 @@ def _traced_sync[**Args, Result](
96
69
  **kwargs: Args.kwargs,
97
70
  ) -> Result:
98
71
  with ctx.scope(label):
99
- for idx, arg in enumerate(args):
100
- ctx.attributes(**{f"[{idx}]": f"{arg}"})
101
-
102
- for key, arg in kwargs.items():
103
- ctx.attributes(**{key: f"{arg}"})
72
+ ctx.record(
73
+ attributes={
74
+ f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING
75
+ }
76
+ )
77
+ ctx.record(
78
+ attributes={key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING}
79
+ )
104
80
 
105
81
  try:
106
82
  result: Result = function(*args, **kwargs)
107
- ctx.event(ResultTrace.of(result))
83
+ ctx.record(
84
+ event="result",
85
+ attributes={"value": format_str(result)},
86
+ )
108
87
  return result
109
88
 
110
89
  except BaseException as exc:
111
- ctx.event(ResultTrace.of(f"{type(exc)}: {exc}"))
90
+ ctx.record(
91
+ event="result",
92
+ attributes={"error": f"{type(exc)}: {exc}"},
93
+ )
112
94
  raise exc
113
95
 
114
96
  return mimic_function(
@@ -127,19 +109,28 @@ def _traced_async[**Args, Result](
127
109
  **kwargs: Args.kwargs,
128
110
  ) -> Result:
129
111
  with ctx.scope(label):
130
- for idx, arg in enumerate(args):
131
- ctx.attributes(**{f"[{idx}]": f"{arg}"})
132
-
133
- for key, arg in kwargs.items():
134
- ctx.attributes(**{key: f"{arg}"})
112
+ ctx.record(
113
+ attributes={
114
+ f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING
115
+ }
116
+ )
117
+ ctx.record(
118
+ attributes={key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING}
119
+ )
135
120
 
136
121
  try:
137
122
  result: Result = await function(*args, **kwargs)
138
- ctx.event(ResultTrace.of(result))
123
+ ctx.record(
124
+ event="result",
125
+ attributes={"value": format_str(result)},
126
+ )
139
127
  return result
140
128
 
141
129
  except BaseException as exc:
142
- ctx.event(ResultTrace.of(f"{type(exc)}: {exc}"))
130
+ ctx.record(
131
+ event="result",
132
+ attributes={"error": f"{type(exc)}: {exc}"},
133
+ )
143
134
  raise exc
144
135
 
145
136
  return mimic_function(
@@ -1,6 +1,6 @@
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
5
  from opentelemetry import metrics, trace
6
6
  from opentelemetry._logs import get_logger, set_logger_provider
@@ -29,12 +29,12 @@ 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
- ###
36
35
  from haiway.context.observability import ObservabilityAttribute
37
36
  from haiway.state import State
37
+ from haiway.types import MISSING
38
38
 
39
39
  __all__ = ("OpenTelemetry",)
40
40
 
@@ -45,9 +45,9 @@ class ScopeStore:
45
45
  "_counters",
46
46
  "_exited",
47
47
  "_histograms",
48
+ "_span_context",
48
49
  "identifier",
49
50
  "logger",
50
- "logger_2",
51
51
  "meter",
52
52
  "nested",
53
53
  "span",
@@ -68,6 +68,7 @@ class ScopeStore:
68
68
  self._exited: bool = False
69
69
  self._completed: bool = False
70
70
  self.span: Span = span
71
+ self._span_context: SpanContext = span.get_span_context()
71
72
  self.meter: Meter = meter
72
73
  self.logger: Logger = logger
73
74
 
@@ -105,16 +106,16 @@ class ScopeStore:
105
106
  ) -> None:
106
107
  self.logger.emit(
107
108
  LogRecord(
108
- span_id=self.span.get_span_context().span_id,
109
- trace_id=self.span.get_span_context().trace_id,
110
- 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,
111
112
  body=message,
112
113
  severity_text=level.name,
113
114
  severity_number=SEVERITY_MAPPING[level],
114
115
  attributes={
115
- "trace_id": self.identifier.trace_id,
116
- "scope_id": self.identifier.scope_id,
117
- "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,
118
119
  },
119
120
  )
120
121
  )
@@ -128,12 +129,18 @@ class ScopeStore:
128
129
 
129
130
  def record_event(
130
131
  self,
131
- event: State,
132
+ event: str,
132
133
  /,
134
+ *,
135
+ attributes: Mapping[str, ObservabilityAttribute],
133
136
  ) -> None:
134
137
  self.span.add_event(
135
- str(type(event).__name__),
136
- 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
+ },
137
144
  )
138
145
 
139
146
  def record_metric(
@@ -143,13 +150,8 @@ class ScopeStore:
143
150
  *,
144
151
  value: float | int,
145
152
  unit: str | None,
153
+ attributes: Mapping[str, ObservabilityAttribute],
146
154
  ) -> None:
147
- attributes: Mapping[str, Any] = {
148
- "trace_id": self.identifier.trace_id,
149
- "scope_id": self.identifier.scope_id,
150
- "parent_id": self.identifier.parent_id,
151
- }
152
-
153
155
  if name not in self._counters:
154
156
  self._counters[name] = self.meter.create_counter(
155
157
  name=name,
@@ -158,7 +160,18 @@ class ScopeStore:
158
160
 
159
161
  self._counters[name].add(
160
162
  value,
161
- 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
+ },
162
175
  )
163
176
 
164
177
  def record_attribute(
@@ -168,10 +181,11 @@ class ScopeStore:
168
181
  *,
169
182
  value: ObservabilityAttribute,
170
183
  ) -> None:
171
- self.span.set_attribute(
172
- name,
173
- value=value,
174
- )
184
+ if value is not None and value is not MISSING:
185
+ self.span.set_attribute(
186
+ name,
187
+ value=cast(Any, value),
188
+ )
175
189
 
176
190
 
177
191
  @final
@@ -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,148 @@
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
+ variables: ItemsView[str, Any] = vars(other).items()
71
+
72
+ parts: list[str] = [f"┍━ {type(other).__name__}:"]
73
+ for key, value in variables:
74
+ if key.startswith("_"):
75
+ continue # skip private and dunder
76
+
77
+ value_string: str = format_str(value)
78
+
79
+ if value_string:
80
+ parts.append(
81
+ _attribute_str(
82
+ key=key,
83
+ value=value_string,
84
+ )
85
+ )
86
+
87
+ else:
88
+ continue # skip empty elements
89
+
90
+ if parts:
91
+ return "\n".join(parts) + "\n┕━"
92
+
93
+ else:
94
+ return ""
95
+
96
+
97
+ def _mapping_str(
98
+ mapping: Mapping[Any, Any],
99
+ /,
100
+ ) -> str:
101
+ items: ItemsView[Any, Any] = mapping.items()
102
+
103
+ parts: list[str] = []
104
+ for key, value in items:
105
+ value_string: str = format_str(value)
106
+
107
+ if value_string:
108
+ parts.append(
109
+ _element_str(
110
+ key=key,
111
+ value=value_string,
112
+ )
113
+ )
114
+
115
+ else:
116
+ continue # skip empty items
117
+
118
+ if parts:
119
+ return "{\n " + "\n".join(parts) + "\n}"
120
+
121
+ else:
122
+ return "{}"
123
+
124
+
125
+ def _sequence_str(
126
+ sequence: Sequence[Any],
127
+ /,
128
+ ) -> str:
129
+ parts: list[str] = []
130
+ for idx, element in enumerate(sequence):
131
+ element_string: str = format_str(element)
132
+
133
+ if element_string:
134
+ parts.append(
135
+ _element_str(
136
+ key=idx,
137
+ value=element_string,
138
+ )
139
+ )
140
+
141
+ else:
142
+ continue # skip empty elements
143
+
144
+ if parts:
145
+ return "[\n " + "\n".join(parts) + "\n]"
146
+
147
+ else:
148
+ return "[]"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.18.1
3
+ Version: 0.19.0
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