encore-dev 2.0.0a7__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.
- encore/__init__.py +43 -0
- encore/cron/__init__.py +50 -0
- encore/internal/__init__.py +1 -0
- encore/internal/_loader.py +79 -0
- encore/internal/_native.pyi +734 -0
- encore/internal/lazy.py +35 -0
- encore/internal/runtime.py +133 -0
- encore/internal/startup.py +24 -0
- encore/logs/__init__.py +19 -0
- encore/logs/_console.py +155 -0
- encore/logs/_runtime.py +56 -0
- encore/meta/__init__.py +228 -0
- encore/metrics/__init__.py +390 -0
- encore/pubsub/__init__.py +425 -0
- encore/py.typed +0 -0
- encore/secrets/__init__.py +139 -0
- encore/shutdown/__init__.py +164 -0
- encore/storage/__init__.py +5 -0
- encore/storage/objects/__init__.py +396 -0
- encore/storage/postgres/__init__.py +737 -0
- encore/storage/redis/__init__.py +200 -0
- encore/storage/redis/_base.py +42 -0
- encore/storage/redis/_responses.py +150 -0
- encore/storage/redis/_values.py +189 -0
- encore/storage/redis/bitmap.py +142 -0
- encore/storage/redis/generic.py +367 -0
- encore/storage/redis/geo.py +508 -0
- encore/storage/redis/hash.py +504 -0
- encore/storage/redis/json.py +614 -0
- encore/storage/redis/list.py +511 -0
- encore/storage/redis/pubsub.py +276 -0
- encore/storage/redis/set.py +570 -0
- encore/storage/redis/sorted_set.py +753 -0
- encore/storage/redis/stream.py +773 -0
- encore/storage/redis/string.py +654 -0
- encore/tracing/__init__.py +19 -0
- encore/tracing/_context.py +27 -0
- encore/tracing/_encode.py +351 -0
- encore/tracing/_provider.py +62 -0
- encore/tracing/_span.py +226 -0
- encore/tracing/_tracer.py +93 -0
- encore_dev-2.0.0a7.dist-info/METADATA +791 -0
- encore_dev-2.0.0a7.dist-info/RECORD +44 -0
- encore_dev-2.0.0a7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Application metrics, declared as module-level variables.
|
|
2
|
+
|
|
3
|
+
Typical usage example:
|
|
4
|
+
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
from encore import metrics
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResponseLabels(TypedDict):
|
|
11
|
+
method: str
|
|
12
|
+
status: int
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
requests = metrics.Counter("requests_total")
|
|
16
|
+
queue_depth = metrics.Gauge("queue_depth")
|
|
17
|
+
responses = metrics.CounterGroup[ResponseLabels]("responses_total")
|
|
18
|
+
latency = metrics.Histogram("request_duration", unit="seconds")
|
|
19
|
+
|
|
20
|
+
requests.increment()
|
|
21
|
+
queue_depth.set(3)
|
|
22
|
+
responses.with_labels({"method": "GET", "status": 200}).increment()
|
|
23
|
+
latency.observe(0.025)
|
|
24
|
+
|
|
25
|
+
Declare metrics at module scope and update them with increment(), set(), or
|
|
26
|
+
observe().
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Mapping
|
|
32
|
+
from dataclasses import fields, is_dataclass
|
|
33
|
+
import math
|
|
34
|
+
from threading import Lock
|
|
35
|
+
from typing import Callable, Generic, Union, cast, get_args, overload
|
|
36
|
+
|
|
37
|
+
from typing_extensions import TypeVar
|
|
38
|
+
|
|
39
|
+
from ..internal.lazy import Lazy
|
|
40
|
+
from ..internal.runtime import _native, runtime
|
|
41
|
+
|
|
42
|
+
LabelValue = Union[str, int, float, bool]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _label_pairs(labels: object) -> tuple[tuple[str, str], ...]:
|
|
46
|
+
if isinstance(labels, Mapping):
|
|
47
|
+
items = labels.items()
|
|
48
|
+
elif is_dataclass(labels) and not isinstance(labels, type):
|
|
49
|
+
items = ((field.name, getattr(labels, field.name)) for field in fields(labels))
|
|
50
|
+
else:
|
|
51
|
+
raise TypeError("labels must be a mapping or a dataclass instance")
|
|
52
|
+
pairs = []
|
|
53
|
+
for key, value in items:
|
|
54
|
+
if not isinstance(key, str):
|
|
55
|
+
raise TypeError("label names must be strings")
|
|
56
|
+
if isinstance(value, bool):
|
|
57
|
+
text = "true" if value else "false"
|
|
58
|
+
elif isinstance(value, float):
|
|
59
|
+
if not math.isfinite(value):
|
|
60
|
+
raise ValueError("numeric labels must be finite")
|
|
61
|
+
text = str(math.floor(value))
|
|
62
|
+
elif isinstance(value, (str, int)):
|
|
63
|
+
text = str(value)
|
|
64
|
+
else:
|
|
65
|
+
raise TypeError("label values must be strings, booleans, or finite numbers")
|
|
66
|
+
pairs.append((key, text))
|
|
67
|
+
return tuple(sorted(pairs))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _MetricBinding:
|
|
71
|
+
def __init__(
|
|
72
|
+
self, name: str, kind: _native.MetricType | Callable[[], _native.MetricType]
|
|
73
|
+
) -> None:
|
|
74
|
+
self.name = name
|
|
75
|
+
self._kind = kind
|
|
76
|
+
self.labels: tuple[tuple[str, str], ...] = ()
|
|
77
|
+
self._handle = Lazy(self._resolve)
|
|
78
|
+
|
|
79
|
+
def get(self) -> tuple[_native.MetricsRegistry, int]:
|
|
80
|
+
return self._handle.get()
|
|
81
|
+
|
|
82
|
+
def _resolve(self) -> tuple[_native.MetricsRegistry, int]:
|
|
83
|
+
registry = runtime().get_metrics_registry()
|
|
84
|
+
kind = self._kind() if callable(self._kind) else self._kind
|
|
85
|
+
slot = registry.allocate_slot(self.name, list(self.labels), kind)
|
|
86
|
+
return registry, slot
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_N = TypeVar("_N", int, float, default=int)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _counter_type(instance: object, index: int) -> type[int] | type[float]:
|
|
93
|
+
"""Resolve a constructor's numeric type after generic metadata is attached."""
|
|
94
|
+
args = get_args(getattr(instance, "__orig_class__", None))
|
|
95
|
+
numeric = args[index] if len(args) > index else int
|
|
96
|
+
if numeric is not int and numeric is not float:
|
|
97
|
+
raise TypeError("counter numeric type must be int or float")
|
|
98
|
+
return numeric
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Counter(Generic[_N]):
|
|
102
|
+
"""A nondecreasing integer or floating-point counter.
|
|
103
|
+
|
|
104
|
+
Counter(name) and Counter[int](name) use unsigned 64-bit integers and wrap
|
|
105
|
+
on overflow. Counter[float](name) uses double precision and rejects updates
|
|
106
|
+
that would overflow. Float totals may lose precision, including for integers
|
|
107
|
+
above 2**53. A variable annotation alone does not select the runtime type.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, name: str) -> None:
|
|
111
|
+
"""Declare a counter at module scope.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
name: The metric name used when exporting measurements.
|
|
115
|
+
"""
|
|
116
|
+
self._numeric = Lazy(lambda: _counter_type(self, 0))
|
|
117
|
+
self._binding = _MetricBinding(
|
|
118
|
+
name,
|
|
119
|
+
lambda: (
|
|
120
|
+
_native.MetricType.FloatCounter
|
|
121
|
+
if self._numeric.get() is float
|
|
122
|
+
else _native.MetricType.Counter
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def name(self) -> str:
|
|
128
|
+
"""The declared metric name."""
|
|
129
|
+
return self._binding.name
|
|
130
|
+
|
|
131
|
+
@overload
|
|
132
|
+
def increment(self) -> None: ...
|
|
133
|
+
|
|
134
|
+
@overload
|
|
135
|
+
def increment(self, value: _N) -> None: ...
|
|
136
|
+
|
|
137
|
+
def increment(self, value: int | float = 1) -> None:
|
|
138
|
+
"""Add a nonnegative amount to the counter.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
value: Defaults to 1. Integer counters require an integer from 0
|
|
142
|
+
through 2**64 - 1. Float counters accept finite, nonnegative
|
|
143
|
+
integers or floats.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
TypeError: The value or constructor's numeric type is unsupported.
|
|
147
|
+
OverflowError: An integer increment is outside the unsigned range.
|
|
148
|
+
ValueError: A float increment is negative or nonfinite, its total
|
|
149
|
+
would overflow, or the series already has a different type.
|
|
150
|
+
"""
|
|
151
|
+
registry, slot = self._binding.get()
|
|
152
|
+
if self._numeric.get() is float:
|
|
153
|
+
registry.increment_float(slot, value)
|
|
154
|
+
else:
|
|
155
|
+
registry.increment(slot, cast(int, value))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Gauge:
|
|
159
|
+
"""A gauge that can go up and down."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, name: str) -> None:
|
|
162
|
+
"""Declare a gauge at module scope.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
name: The metric name used when exporting measurements.
|
|
166
|
+
"""
|
|
167
|
+
self._binding = _MetricBinding(name, _native.MetricType.Gauge)
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def name(self) -> str:
|
|
171
|
+
"""The declared metric name."""
|
|
172
|
+
return self._binding.name
|
|
173
|
+
|
|
174
|
+
def set(self, value: float) -> None:
|
|
175
|
+
"""Set the gauge's current measurement.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
value: A finite number.
|
|
179
|
+
"""
|
|
180
|
+
registry, slot = self._binding.get()
|
|
181
|
+
registry.set(slot, value)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Histogram:
|
|
185
|
+
"""A distribution of observations recorded in automatic exponential buckets.
|
|
186
|
+
|
|
187
|
+
Use for measurements such as durations or sizes. The runtime tracks count,
|
|
188
|
+
sum, and buckets without retaining individual observations. Buckets grow by
|
|
189
|
+
at most 10% by default and support positive, negative, and zero values.
|
|
190
|
+
|
|
191
|
+
Prometheus remote write exports the bucket distribution. Other exporters
|
|
192
|
+
currently export only the count and sum, which cannot provide percentiles.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(self, name: str, *, unit: str | None = None) -> None:
|
|
196
|
+
"""Declare a histogram at module scope.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
name: The metric name as a string literal.
|
|
200
|
+
unit: The measurement unit as a string literal, such as "seconds"
|
|
201
|
+
or "bytes", or None if unspecified. Encore reads this metadata
|
|
202
|
+
from the declaration; it does not convert observations.
|
|
203
|
+
"""
|
|
204
|
+
self._binding = _MetricBinding(name, _native.MetricType.Histogram)
|
|
205
|
+
self._unit = unit
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def name(self) -> str:
|
|
209
|
+
"""The declared metric name."""
|
|
210
|
+
return self._binding.name
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def unit(self) -> str | None:
|
|
214
|
+
"""The declared measurement unit, or None if unspecified."""
|
|
215
|
+
return self._unit
|
|
216
|
+
|
|
217
|
+
def observe(self, value: float) -> None:
|
|
218
|
+
"""Record one measurement in the declared unit.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
value: The measurement to record. Prefer finite values. As in the
|
|
222
|
+
Rust SDK, NaN increments the count and makes the sum NaN but
|
|
223
|
+
enters no bucket; infinities enter buckets of their own.
|
|
224
|
+
"""
|
|
225
|
+
registry, slot = self._binding.get()
|
|
226
|
+
registry.observe(slot, value)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
_M = TypeVar("_M")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class _LabelCache(Generic[_M]):
|
|
233
|
+
def __init__(self, factory: Callable[[tuple[tuple[str, str], ...]], _M]) -> None:
|
|
234
|
+
self._factory = factory
|
|
235
|
+
self._cache: dict[tuple[tuple[str, str], ...], _M] = {}
|
|
236
|
+
self._lock = Lock()
|
|
237
|
+
|
|
238
|
+
def get(self, labels: object) -> _M:
|
|
239
|
+
pairs = _label_pairs(labels)
|
|
240
|
+
with self._lock:
|
|
241
|
+
metric = self._cache.get(pairs)
|
|
242
|
+
if metric is None:
|
|
243
|
+
metric = self._factory(pairs)
|
|
244
|
+
self._cache[pairs] = metric
|
|
245
|
+
return metric
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
_L = TypeVar("_L")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class CounterGroup(Generic[_L, _N]):
|
|
252
|
+
"""A counter partitioned by labels.
|
|
253
|
+
|
|
254
|
+
Use a dataclass or TypedDict as the label type. CounterGroup[Labels](name)
|
|
255
|
+
uses integer counters; CounterGroup[Labels, float](name) uses float counters.
|
|
256
|
+
Select the numeric type in the constructor, not only a variable annotation.
|
|
257
|
+
Label values must be strings, booleans, or finite numbers.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def __init__(self, name: str) -> None:
|
|
261
|
+
"""Declare a counter group at module scope.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name: The metric name used when exporting measurements.
|
|
265
|
+
"""
|
|
266
|
+
self._name = name
|
|
267
|
+
self._cache = _LabelCache(self._create_counter)
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def name(self) -> str:
|
|
271
|
+
"""The declared metric name."""
|
|
272
|
+
return self._name
|
|
273
|
+
|
|
274
|
+
def with_labels(self, labels: _L) -> Counter[_N]:
|
|
275
|
+
"""Bind labels and reuse the counter for those values.
|
|
276
|
+
|
|
277
|
+
Numeric labels are rounded down to integers; booleans become "true"
|
|
278
|
+
or "false". Label names are normalized to snake_case. The service
|
|
279
|
+
label is added automatically and cannot be supplied explicitly.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
labels: A dataclass instance or mapping with string, boolean, or
|
|
283
|
+
finite numeric values. Dataclass fields are read without
|
|
284
|
+
recursively converting nested objects.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
The cached counter for this combination of label values.
|
|
288
|
+
"""
|
|
289
|
+
return self._cache.get(labels)
|
|
290
|
+
|
|
291
|
+
def _create_counter(self, pairs: tuple[tuple[str, str], ...]) -> Counter[_N]:
|
|
292
|
+
counter = (
|
|
293
|
+
Counter[float](self.name)
|
|
294
|
+
if _counter_type(self, 1) is float
|
|
295
|
+
else Counter[int](self.name)
|
|
296
|
+
)
|
|
297
|
+
counter._binding.labels = pairs
|
|
298
|
+
return cast("Counter[_N]", counter)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class GaugeGroup(Generic[_L]):
|
|
302
|
+
"""A float gauge partitioned by dataclass or mapping labels."""
|
|
303
|
+
|
|
304
|
+
def __init__(self, name: str) -> None:
|
|
305
|
+
"""Declare a gauge group at module scope.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
name: The metric name used when exporting measurements.
|
|
309
|
+
"""
|
|
310
|
+
self._name = name
|
|
311
|
+
self._cache = _LabelCache(self._create_gauge)
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def name(self) -> str:
|
|
315
|
+
"""The declared metric name."""
|
|
316
|
+
return self._name
|
|
317
|
+
|
|
318
|
+
def with_labels(self, labels: _L) -> Gauge:
|
|
319
|
+
"""Bind labels and reuse the gauge for those values.
|
|
320
|
+
|
|
321
|
+
Numeric labels are rounded down to integers; booleans become "true"
|
|
322
|
+
or "false". Label names are normalized to snake_case. The service
|
|
323
|
+
label is added automatically and cannot be supplied explicitly.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
labels: A dataclass instance or mapping with string, boolean, or
|
|
327
|
+
finite numeric values.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
The cached gauge for this combination of label values.
|
|
331
|
+
"""
|
|
332
|
+
return self._cache.get(labels)
|
|
333
|
+
|
|
334
|
+
def _create_gauge(self, pairs: tuple[tuple[str, str], ...]) -> Gauge:
|
|
335
|
+
gauge = Gauge(self.name)
|
|
336
|
+
gauge._binding.labels = pairs
|
|
337
|
+
return gauge
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class HistogramGroup(Generic[_L]):
|
|
341
|
+
"""A separate histogram for each combination of label values.
|
|
342
|
+
|
|
343
|
+
Use a dataclass or TypedDict as the label type to check names and values.
|
|
344
|
+
Every series uses the group's unit and the automatic buckets of Histogram.
|
|
345
|
+
Series remain in memory for the process lifetime, so prefer labels with a
|
|
346
|
+
small set of possible values, such as operation names, over request IDs.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
def __init__(self, name: str, *, unit: str | None = None) -> None:
|
|
350
|
+
"""Declare a histogram group at module scope.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
name: The metric name as a string literal.
|
|
354
|
+
unit: The measurement unit as a string literal, such as "seconds"
|
|
355
|
+
or "bytes", or None if unspecified. Values are not converted.
|
|
356
|
+
"""
|
|
357
|
+
self._name = name
|
|
358
|
+
self._unit = unit
|
|
359
|
+
self._cache = _LabelCache(self._create_histogram)
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def name(self) -> str:
|
|
363
|
+
"""The declared metric name."""
|
|
364
|
+
return self._name
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def unit(self) -> str | None:
|
|
368
|
+
"""The declared measurement unit, or None if unspecified."""
|
|
369
|
+
return self._unit
|
|
370
|
+
|
|
371
|
+
def with_labels(self, labels: _L) -> Histogram:
|
|
372
|
+
"""Bind labels and reuse the histogram for those values.
|
|
373
|
+
|
|
374
|
+
Numeric labels are rounded down to integers; booleans become "true"
|
|
375
|
+
or "false". Label names are normalized to snake_case. The service
|
|
376
|
+
label is added automatically and cannot be supplied explicitly.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
labels: A dataclass instance or mapping with string, boolean, or
|
|
380
|
+
finite numeric values.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
The cached histogram for this combination of label values.
|
|
384
|
+
"""
|
|
385
|
+
return self._cache.get(labels)
|
|
386
|
+
|
|
387
|
+
def _create_histogram(self, pairs: tuple[tuple[str, str], ...]) -> Histogram:
|
|
388
|
+
histogram = Histogram(self.name, unit=self.unit)
|
|
389
|
+
histogram._binding.labels = pairs
|
|
390
|
+
return histogram
|