haiway 0.17.0__py3-none-any.whl → 0.18.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/__init__.py +24 -18
- haiway/context/__init__.py +23 -13
- haiway/context/access.py +127 -91
- haiway/context/disposables.py +2 -2
- haiway/context/identifier.py +4 -5
- haiway/context/observability.py +526 -0
- haiway/context/state.py +2 -2
- haiway/context/tasks.py +1 -3
- haiway/context/types.py +2 -2
- haiway/helpers/__init__.py +5 -7
- haiway/helpers/asynchrony.py +2 -2
- haiway/helpers/caching.py +2 -2
- haiway/helpers/observability.py +244 -0
- haiway/helpers/retries.py +1 -3
- haiway/helpers/throttling.py +1 -3
- haiway/helpers/timeouted.py +1 -3
- haiway/helpers/tracing.py +21 -35
- haiway/opentelemetry/__init__.py +3 -0
- haiway/opentelemetry/observability.py +452 -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.17.0.dist-info → haiway-0.18.1.dist-info}/METADATA +9 -5
- haiway-0.18.1.dist-info/RECORD +44 -0
- haiway/context/logging.py +0 -242
- haiway/context/metrics.py +0 -176
- haiway/helpers/metrics.py +0 -465
- haiway-0.17.0.dist-info/RECORD +0 -43
- {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/WHEEL +0 -0
- {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,452 @@
|
|
1
|
+
import os
|
2
|
+
from collections.abc import Mapping
|
3
|
+
from typing import Any, Self, final
|
4
|
+
|
5
|
+
from opentelemetry import metrics, trace
|
6
|
+
from opentelemetry._logs import get_logger, set_logger_provider
|
7
|
+
from opentelemetry._logs._internal import Logger
|
8
|
+
from opentelemetry._logs.severity import SeverityNumber
|
9
|
+
from opentelemetry.context import Context
|
10
|
+
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
11
|
+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
12
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
13
|
+
from opentelemetry.metrics._internal import Meter
|
14
|
+
from opentelemetry.metrics._internal.instrument import Counter, Histogram
|
15
|
+
from opentelemetry.sdk._logs import LoggerProvider
|
16
|
+
from opentelemetry.sdk._logs._internal import LogRecord
|
17
|
+
from opentelemetry.sdk._logs._internal.export import (
|
18
|
+
BatchLogRecordProcessor,
|
19
|
+
ConsoleLogExporter,
|
20
|
+
LogExporter,
|
21
|
+
)
|
22
|
+
from opentelemetry.sdk.metrics import MeterProvider as SdkMeterProvider
|
23
|
+
from opentelemetry.sdk.metrics._internal.export import MetricExporter
|
24
|
+
from opentelemetry.sdk.metrics.export import (
|
25
|
+
ConsoleMetricExporter,
|
26
|
+
PeriodicExportingMetricReader,
|
27
|
+
)
|
28
|
+
from opentelemetry.sdk.resources import Resource
|
29
|
+
from opentelemetry.sdk.trace import TracerProvider
|
30
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter, SpanExporter
|
31
|
+
from opentelemetry.trace import Span, StatusCode, Tracer
|
32
|
+
|
33
|
+
from haiway.context import Observability, ObservabilityLevel, ScopeIdentifier
|
34
|
+
|
35
|
+
###
|
36
|
+
from haiway.context.observability import ObservabilityAttribute
|
37
|
+
from haiway.state import State
|
38
|
+
|
39
|
+
__all__ = ("OpenTelemetry",)
|
40
|
+
|
41
|
+
|
42
|
+
class ScopeStore:
|
43
|
+
__slots__ = (
|
44
|
+
"_completed",
|
45
|
+
"_counters",
|
46
|
+
"_exited",
|
47
|
+
"_histograms",
|
48
|
+
"identifier",
|
49
|
+
"logger",
|
50
|
+
"logger_2",
|
51
|
+
"meter",
|
52
|
+
"nested",
|
53
|
+
"span",
|
54
|
+
)
|
55
|
+
|
56
|
+
def __init__(
|
57
|
+
self,
|
58
|
+
identifier: ScopeIdentifier,
|
59
|
+
/,
|
60
|
+
span: Span,
|
61
|
+
meter: Meter,
|
62
|
+
logger: Logger,
|
63
|
+
) -> None:
|
64
|
+
self.identifier: ScopeIdentifier = identifier
|
65
|
+
self.nested: list[ScopeStore] = []
|
66
|
+
self._counters: dict[str, Counter] = {}
|
67
|
+
self._histograms: dict[str, Histogram] = {}
|
68
|
+
self._exited: bool = False
|
69
|
+
self._completed: bool = False
|
70
|
+
self.span: Span = span
|
71
|
+
self.meter: Meter = meter
|
72
|
+
self.logger: Logger = logger
|
73
|
+
|
74
|
+
@property
|
75
|
+
def exited(self) -> bool:
|
76
|
+
return self._exited
|
77
|
+
|
78
|
+
def exit(self) -> None:
|
79
|
+
assert not self._exited # nosec: B101
|
80
|
+
self._exited = True
|
81
|
+
|
82
|
+
@property
|
83
|
+
def completed(self) -> bool:
|
84
|
+
return self._completed and all(nested.completed for nested in self.nested)
|
85
|
+
|
86
|
+
def try_complete(self) -> bool:
|
87
|
+
if not self._exited:
|
88
|
+
return False # not elegible for completion yet
|
89
|
+
|
90
|
+
if self._completed:
|
91
|
+
return False # already completed
|
92
|
+
|
93
|
+
if not all(nested.completed for nested in self.nested):
|
94
|
+
return False # nested not completed
|
95
|
+
|
96
|
+
self._completed = True
|
97
|
+
self.span.end()
|
98
|
+
return True # successfully completed
|
99
|
+
|
100
|
+
def record_log(
|
101
|
+
self,
|
102
|
+
message: str,
|
103
|
+
/,
|
104
|
+
level: ObservabilityLevel,
|
105
|
+
) -> None:
|
106
|
+
self.logger.emit(
|
107
|
+
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,
|
111
|
+
body=message,
|
112
|
+
severity_text=level.name,
|
113
|
+
severity_number=SEVERITY_MAPPING[level],
|
114
|
+
attributes={
|
115
|
+
"trace_id": self.identifier.trace_id,
|
116
|
+
"scope_id": self.identifier.scope_id,
|
117
|
+
"parent_id": self.identifier.parent_id,
|
118
|
+
},
|
119
|
+
)
|
120
|
+
)
|
121
|
+
|
122
|
+
def record_exception(
|
123
|
+
self,
|
124
|
+
exception: BaseException,
|
125
|
+
/,
|
126
|
+
) -> None:
|
127
|
+
self.span.record_exception(exception)
|
128
|
+
|
129
|
+
def record_event(
|
130
|
+
self,
|
131
|
+
event: State,
|
132
|
+
/,
|
133
|
+
) -> None:
|
134
|
+
self.span.add_event(
|
135
|
+
str(type(event).__name__),
|
136
|
+
attributes=event.to_mapping(recursive=True),
|
137
|
+
)
|
138
|
+
|
139
|
+
def record_metric(
|
140
|
+
self,
|
141
|
+
name: str,
|
142
|
+
/,
|
143
|
+
*,
|
144
|
+
value: float | int,
|
145
|
+
unit: str | None,
|
146
|
+
) -> 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
|
+
if name not in self._counters:
|
154
|
+
self._counters[name] = self.meter.create_counter(
|
155
|
+
name=name,
|
156
|
+
unit=unit or "",
|
157
|
+
)
|
158
|
+
|
159
|
+
self._counters[name].add(
|
160
|
+
value,
|
161
|
+
attributes=attributes,
|
162
|
+
)
|
163
|
+
|
164
|
+
def record_attribute(
|
165
|
+
self,
|
166
|
+
name: str,
|
167
|
+
/,
|
168
|
+
*,
|
169
|
+
value: ObservabilityAttribute,
|
170
|
+
) -> None:
|
171
|
+
self.span.set_attribute(
|
172
|
+
name,
|
173
|
+
value=value,
|
174
|
+
)
|
175
|
+
|
176
|
+
|
177
|
+
@final
|
178
|
+
class OpenTelemetry:
|
179
|
+
@classmethod
|
180
|
+
def configure(
|
181
|
+
cls,
|
182
|
+
*,
|
183
|
+
service: str,
|
184
|
+
version: str,
|
185
|
+
environment: str,
|
186
|
+
otlp_endpoint: str | None = None,
|
187
|
+
insecure: bool = True,
|
188
|
+
export_interval_millis: int = 10000,
|
189
|
+
attributes: Mapping[str, Any] | None = None,
|
190
|
+
) -> type[Self]:
|
191
|
+
# Create shared resource for both metrics and traces
|
192
|
+
resource: Resource = Resource.create(
|
193
|
+
{
|
194
|
+
"service.name": service,
|
195
|
+
"service.version": version,
|
196
|
+
"deployment.environment": environment,
|
197
|
+
"service.pid": os.getpid(),
|
198
|
+
**(attributes if attributes is not None else {}),
|
199
|
+
},
|
200
|
+
)
|
201
|
+
|
202
|
+
logs_exporter: LogExporter
|
203
|
+
span_exporter: SpanExporter
|
204
|
+
metric_exporter: MetricExporter
|
205
|
+
|
206
|
+
if otlp_endpoint:
|
207
|
+
logs_exporter = OTLPLogExporter(
|
208
|
+
endpoint=otlp_endpoint,
|
209
|
+
insecure=insecure,
|
210
|
+
)
|
211
|
+
span_exporter = OTLPSpanExporter(
|
212
|
+
endpoint=otlp_endpoint,
|
213
|
+
insecure=insecure,
|
214
|
+
)
|
215
|
+
metric_exporter = OTLPMetricExporter(
|
216
|
+
endpoint=otlp_endpoint,
|
217
|
+
insecure=insecure,
|
218
|
+
)
|
219
|
+
|
220
|
+
else:
|
221
|
+
logs_exporter = ConsoleLogExporter()
|
222
|
+
span_exporter = ConsoleSpanExporter()
|
223
|
+
metric_exporter = ConsoleMetricExporter()
|
224
|
+
|
225
|
+
# Set up logger provider
|
226
|
+
logger_provider: LoggerProvider = LoggerProvider(
|
227
|
+
resource=resource,
|
228
|
+
shutdown_on_exit=True,
|
229
|
+
)
|
230
|
+
log_processor: BatchLogRecordProcessor = BatchLogRecordProcessor(logs_exporter)
|
231
|
+
logger_provider.add_log_record_processor(log_processor)
|
232
|
+
set_logger_provider(logger_provider)
|
233
|
+
|
234
|
+
# Set up metrics provider
|
235
|
+
meter_provider: SdkMeterProvider = SdkMeterProvider(
|
236
|
+
resource=resource,
|
237
|
+
metric_readers=[
|
238
|
+
PeriodicExportingMetricReader(
|
239
|
+
metric_exporter,
|
240
|
+
export_interval_millis=export_interval_millis,
|
241
|
+
)
|
242
|
+
],
|
243
|
+
shutdown_on_exit=True,
|
244
|
+
)
|
245
|
+
metrics.set_meter_provider(meter_provider)
|
246
|
+
|
247
|
+
# Set up trace provider
|
248
|
+
tracer_provider: TracerProvider = TracerProvider(
|
249
|
+
resource=resource,
|
250
|
+
shutdown_on_exit=True,
|
251
|
+
)
|
252
|
+
span_processor: BatchSpanProcessor = BatchSpanProcessor(span_exporter)
|
253
|
+
tracer_provider.add_span_processor(span_processor)
|
254
|
+
trace.set_tracer_provider(tracer_provider)
|
255
|
+
|
256
|
+
return cls
|
257
|
+
|
258
|
+
@classmethod
|
259
|
+
def observability( # noqa: C901, PLR0915
|
260
|
+
cls,
|
261
|
+
level: ObservabilityLevel = ObservabilityLevel.INFO,
|
262
|
+
) -> Observability:
|
263
|
+
tracer: Tracer | None = None
|
264
|
+
meter: Meter | None = None
|
265
|
+
root_scope: ScopeIdentifier | None = None
|
266
|
+
scopes: dict[str, ScopeStore] = {}
|
267
|
+
observed_level: ObservabilityLevel = level
|
268
|
+
|
269
|
+
def log_recording(
|
270
|
+
scope: ScopeIdentifier,
|
271
|
+
/,
|
272
|
+
level: ObservabilityLevel,
|
273
|
+
message: str,
|
274
|
+
*args: Any,
|
275
|
+
exception: BaseException | None,
|
276
|
+
**extra: Any,
|
277
|
+
) -> None:
|
278
|
+
assert root_scope is not None # nosec: B101
|
279
|
+
assert scope.scope_id in scopes # nosec: B101
|
280
|
+
|
281
|
+
if level < observed_level:
|
282
|
+
return
|
283
|
+
|
284
|
+
scopes[scope.scope_id].record_log(
|
285
|
+
message % args,
|
286
|
+
level=level,
|
287
|
+
)
|
288
|
+
if exception is not None:
|
289
|
+
scopes[scope.scope_id].record_exception(exception)
|
290
|
+
|
291
|
+
def event_recording(
|
292
|
+
scope: ScopeIdentifier,
|
293
|
+
/,
|
294
|
+
*,
|
295
|
+
level: ObservabilityLevel,
|
296
|
+
event: State,
|
297
|
+
**extra: Any,
|
298
|
+
) -> None:
|
299
|
+
assert root_scope is not None # nosec: B101
|
300
|
+
assert scope.scope_id in scopes # nosec: B101
|
301
|
+
|
302
|
+
if level < observed_level:
|
303
|
+
return
|
304
|
+
|
305
|
+
scopes[scope.scope_id].record_event(event)
|
306
|
+
|
307
|
+
def metric_recording(
|
308
|
+
scope: ScopeIdentifier,
|
309
|
+
/,
|
310
|
+
*,
|
311
|
+
metric: str,
|
312
|
+
value: float | int,
|
313
|
+
unit: str | None,
|
314
|
+
**extra: Any,
|
315
|
+
) -> None:
|
316
|
+
assert root_scope is not None # nosec: B101
|
317
|
+
assert scope.scope_id in scopes # nosec: B101
|
318
|
+
|
319
|
+
if level < observed_level:
|
320
|
+
return
|
321
|
+
|
322
|
+
scopes[scope.scope_id].record_metric(
|
323
|
+
metric,
|
324
|
+
value=value,
|
325
|
+
unit=unit,
|
326
|
+
)
|
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
|
+
|
342
|
+
def scope_entering[Metric: State](
|
343
|
+
scope: ScopeIdentifier,
|
344
|
+
/,
|
345
|
+
) -> None:
|
346
|
+
nonlocal tracer
|
347
|
+
assert scope.scope_id not in scopes # nosec: B101
|
348
|
+
|
349
|
+
nonlocal root_scope
|
350
|
+
nonlocal meter
|
351
|
+
|
352
|
+
scope_store: ScopeStore
|
353
|
+
if root_scope is None:
|
354
|
+
tracer = trace.get_tracer(scope.trace_id)
|
355
|
+
meter = metrics.get_meter(scope.trace_id)
|
356
|
+
scope_store = ScopeStore(
|
357
|
+
scope,
|
358
|
+
span=tracer.start_span(
|
359
|
+
name=scope.label,
|
360
|
+
context=Context(
|
361
|
+
trace_id=scope.trace_id,
|
362
|
+
scope_id=scope.scope_id,
|
363
|
+
parent_id=scope.parent_id,
|
364
|
+
),
|
365
|
+
attributes={
|
366
|
+
"trace_id": scope.trace_id,
|
367
|
+
"scope_id": scope.scope_id,
|
368
|
+
"parent_id": scope.parent_id,
|
369
|
+
},
|
370
|
+
),
|
371
|
+
meter=meter,
|
372
|
+
logger=get_logger(scope.label),
|
373
|
+
)
|
374
|
+
root_scope = scope
|
375
|
+
|
376
|
+
else:
|
377
|
+
assert tracer is not None # nosec: B101
|
378
|
+
assert meter is not None # nosec: B101
|
379
|
+
|
380
|
+
scope_store = ScopeStore(
|
381
|
+
scope,
|
382
|
+
span=tracer.start_span(
|
383
|
+
name=scope.label,
|
384
|
+
context=trace.set_span_in_context(
|
385
|
+
scopes[scope.parent_id].span,
|
386
|
+
Context(
|
387
|
+
trace_id=scope.trace_id,
|
388
|
+
scope_id=scope.scope_id,
|
389
|
+
parent_id=scope.parent_id,
|
390
|
+
),
|
391
|
+
),
|
392
|
+
attributes={
|
393
|
+
"trace_id": scope.trace_id,
|
394
|
+
"scope_id": scope.scope_id,
|
395
|
+
"parent_id": scope.parent_id,
|
396
|
+
},
|
397
|
+
),
|
398
|
+
meter=meter,
|
399
|
+
logger=get_logger(scope.label),
|
400
|
+
)
|
401
|
+
scopes[scope.parent_id].nested.append(scope_store)
|
402
|
+
|
403
|
+
scopes[scope.scope_id] = scope_store
|
404
|
+
|
405
|
+
def scope_exiting[Metric: State](
|
406
|
+
scope: ScopeIdentifier,
|
407
|
+
/,
|
408
|
+
*,
|
409
|
+
exception: BaseException | None,
|
410
|
+
) -> None:
|
411
|
+
nonlocal root_scope
|
412
|
+
nonlocal scopes
|
413
|
+
assert root_scope is not None # nosec: B101
|
414
|
+
assert scope.scope_id in scopes # nosec: B101
|
415
|
+
|
416
|
+
scopes[scope.scope_id].exit()
|
417
|
+
if exception is not None:
|
418
|
+
scopes[scope.scope_id].span.set_status(status=StatusCode.ERROR)
|
419
|
+
|
420
|
+
else:
|
421
|
+
scopes[scope.scope_id].span.set_status(status=StatusCode.OK)
|
422
|
+
|
423
|
+
if not scopes[scope.scope_id].try_complete():
|
424
|
+
return # not completed yet or already completed
|
425
|
+
|
426
|
+
# try complete parent scopes
|
427
|
+
parent_id: str = scope.parent_id
|
428
|
+
while scopes[parent_id].try_complete():
|
429
|
+
parent_id = scopes[parent_id].identifier.parent_id
|
430
|
+
|
431
|
+
# check for root completion
|
432
|
+
if scopes[root_scope.scope_id].completed:
|
433
|
+
# finished root - cleanup state
|
434
|
+
root_scope = None
|
435
|
+
scopes = {}
|
436
|
+
|
437
|
+
return Observability(
|
438
|
+
log_recording=log_recording,
|
439
|
+
event_recording=event_recording,
|
440
|
+
metric_recording=metric_recording,
|
441
|
+
attributes_recording=attributes_recording,
|
442
|
+
scope_entering=scope_entering,
|
443
|
+
scope_exiting=scope_exiting,
|
444
|
+
)
|
445
|
+
|
446
|
+
|
447
|
+
SEVERITY_MAPPING = {
|
448
|
+
ObservabilityLevel.DEBUG: SeverityNumber.DEBUG,
|
449
|
+
ObservabilityLevel.INFO: SeverityNumber.INFO,
|
450
|
+
ObservabilityLevel.WARNING: SeverityNumber.WARN,
|
451
|
+
ObservabilityLevel.ERROR: SeverityNumber.ERROR,
|
452
|
+
}
|
haiway/state/__init__.py
CHANGED
@@ -3,10 +3,10 @@ from haiway.state.path import AttributePath
|
|
3
3
|
from haiway.state.requirement import AttributeRequirement
|
4
4
|
from haiway.state.structure import State
|
5
5
|
|
6
|
-
__all__ =
|
6
|
+
__all__ = (
|
7
7
|
"AttributeAnnotation",
|
8
8
|
"AttributePath",
|
9
9
|
"AttributeRequirement",
|
10
10
|
"State",
|
11
11
|
"attribute_annotations",
|
12
|
-
|
12
|
+
)
|
haiway/state/attributes.py
CHANGED
@@ -25,11 +25,11 @@ from typing import (
|
|
25
25
|
from haiway import types as haiway_types
|
26
26
|
from haiway.types import MISSING, Missing
|
27
27
|
|
28
|
-
__all__ =
|
28
|
+
__all__ = (
|
29
29
|
"AttributeAnnotation",
|
30
30
|
"attribute_annotations",
|
31
31
|
"resolve_attribute_annotation",
|
32
|
-
|
32
|
+
)
|
33
33
|
|
34
34
|
|
35
35
|
@final
|
haiway/state/path.py
CHANGED
@@ -10,9 +10,7 @@ from typing import Any, TypeAliasType, final, get_args, get_origin, overload
|
|
10
10
|
|
11
11
|
from haiway.types import MISSING, Missing, not_missing
|
12
12
|
|
13
|
-
__all__ =
|
14
|
-
"AttributePath",
|
15
|
-
]
|
13
|
+
__all__ = ("AttributePath",)
|
16
14
|
|
17
15
|
|
18
16
|
class AttributePathComponent(ABC):
|