smart-data-engine-sdk 0.1.0.dev0__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.
- sde/__init__.py +226 -0
- sde/canonical.py +141 -0
- sde/capabilities.py +62 -0
- sde/engines/__init__.py +0 -0
- sde/engines/clickhouse.py +689 -0
- sde/engines/orderbook.py +454 -0
- sde/engines/postgres.py +672 -0
- sde/entity.py +170 -0
- sde/errors.py +88 -0
- sde/explain.py +300 -0
- sde/groups.py +97 -0
- sde/hashing.py +242 -0
- sde/infer.py +461 -0
- sde/internal.py +90 -0
- sde/layout.py +660 -0
- sde/logging.py +132 -0
- sde/migration.py +820 -0
- sde/model.py +482 -0
- sde/placement.py +818 -0
- sde/py.typed +0 -0
- sde/routing.py +85 -0
- sde/schema.py +370 -0
- sde/session.py +507 -0
- sde/shapes.py +153 -0
- sde/telemetry.py +736 -0
- sde/testing/__init__.py +14 -0
- sde/testing/loader.py +175 -0
- sde/testing/memory.py +318 -0
- sde/types.py +228 -0
- sde/watermark.py +222 -0
- smart_data_engine_sdk-0.1.0.dev0.dist-info/METADATA +152 -0
- smart_data_engine_sdk-0.1.0.dev0.dist-info/RECORD +35 -0
- smart_data_engine_sdk-0.1.0.dev0.dist-info/WHEEL +4 -0
- smart_data_engine_sdk-0.1.0.dev0.dist-info/licenses/LICENSE +201 -0
- smart_data_engine_sdk-0.1.0.dev0.dist-info/licenses/NOTICE +13 -0
sde/logging.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Structured logging, with one rule: every event has a name from a closed vocabulary.
|
|
2
|
+
|
|
3
|
+
We never have access to a client's machine. When something goes wrong there, the log is the only
|
|
4
|
+
diagnostic we will ever see, so it has to be greppable and stable rather than prose that changes
|
|
5
|
+
with each refactor. Hence ``sde.``-prefixed event names, structured fields, and no interpolated
|
|
6
|
+
values in the event name itself.
|
|
7
|
+
|
|
8
|
+
No dependency on a logging framework, and no configuration required. The library emits through the
|
|
9
|
+
standard :mod:`logging` module under the ``sde`` logger and stays silent unless the application
|
|
10
|
+
configures a handler - a library that prints on import is a library people vendor and patch.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any, Final
|
|
17
|
+
|
|
18
|
+
__all__ = ["EVENTS", "log", "logger"]
|
|
19
|
+
|
|
20
|
+
logger: Final = logging.getLogger("sde")
|
|
21
|
+
|
|
22
|
+
EVENTS: Final[frozenset[str]] = frozenset(
|
|
23
|
+
{
|
|
24
|
+
# Once per process, and the single most useful line here: it names the model_version every
|
|
25
|
+
# other artefact keys on. "Why was my map refused for model version X" is answerable from
|
|
26
|
+
# this line and unanswerable without it.
|
|
27
|
+
"sde.model.built",
|
|
28
|
+
# A map parsed and was accepted. `signed` says which kind of document it was and
|
|
29
|
+
# `forward_only` whether the rollback check applies - fields rather than two event names,
|
|
30
|
+
# so that the account-free mode emits nothing the account mode does not.
|
|
31
|
+
"sde.map.loaded",
|
|
32
|
+
# A map was refused, with the error class and the structural reason. The exception goes to
|
|
33
|
+
# the application; this goes to whoever is looking at why it will not start.
|
|
34
|
+
"sde.map.rejected",
|
|
35
|
+
# A signed map was accepted and is not older than any already applied here. Emitted once
|
|
36
|
+
# per session, so the fields are the ones an operator wants when a start is refused later.
|
|
37
|
+
"sde.map.forward_only",
|
|
38
|
+
# No engine in this map can keep the bookkeeping, so a map that goes backwards cannot be
|
|
39
|
+
# recognised. Not a failure - our own orderbook engine has a fixed schema and nowhere to
|
|
40
|
+
# put it - but it is the one state where a documented protection is genuinely absent.
|
|
41
|
+
"sde.map.rollback_unprotected",
|
|
42
|
+
"sde.route.resolved",
|
|
43
|
+
"sde.route.fallback",
|
|
44
|
+
"sde.schema.applied",
|
|
45
|
+
# extra_columns fires when a table the map describes has columns the map does not
|
|
46
|
+
# name. Allowed rather than refused - a client may have added one outside SDE and
|
|
47
|
+
# writes are unaffected - but logged, because the alternative to refusing is saying
|
|
48
|
+
# nothing, and a schema that has quietly diverged is worth one line.
|
|
49
|
+
"sde.schema.extra_columns",
|
|
50
|
+
"sde.write.failed",
|
|
51
|
+
# A dual-write fan-out did not reach a copy. Not an application failure: the row is in the
|
|
52
|
+
# source, which is authoritative, and VERIFY is the gate that refuses to switch reads while
|
|
53
|
+
# any divergence remains. This is the only place in the library that swallows a write error.
|
|
54
|
+
"sde.migration.divergence",
|
|
55
|
+
# One backfill chunk landed in a copy and the marker moved. Emitted per chunk rather than
|
|
56
|
+
# per run, because a backfill is the one operation here that can take hours and an operator
|
|
57
|
+
# watching it needs to see it move. The marker itself is a row count and never a key value:
|
|
58
|
+
# a log line is the last place a client's own data should turn up.
|
|
59
|
+
"sde.migration.backfill_progress",
|
|
60
|
+
# The orderbook engine only. A write there is invisible to a query until flush(), and
|
|
61
|
+
# flush() in local mode tears the engine down and reopens it - 3.4 ms measured. Reads flush
|
|
62
|
+
# lazily, so this line is where the cost shows up and how many rows it bought.
|
|
63
|
+
"sde.orderbook.flushed",
|
|
64
|
+
# A ClickHouse server whose analyzer does not answer EXPLAIN QUERY TREE, so the table
|
|
65
|
+
# names behind a plan come from EXPLAIN ESTIMATE instead. Not a failure - the plan is
|
|
66
|
+
# returned - but the ReplacingMergeTree finding is a property of a table, so an operator
|
|
67
|
+
# who was expecting one and did not get it should be able to see why.
|
|
68
|
+
"sde.explain.no_query_tree",
|
|
69
|
+
"sde.internal.error",
|
|
70
|
+
# Telemetry. `window_closed` fires when a period ends and its aggregate is buffered for
|
|
71
|
+
# the application to collect; it was called `window_sent` until somebody read the log to
|
|
72
|
+
# check whether this library phones home and found a line saying it had. Nothing here
|
|
73
|
+
# sends anything - there is no channel, no address and no dependency that could open one -
|
|
74
|
+
# so the old name described an event that cannot happen, and a closed vocabulary exists so
|
|
75
|
+
# that an alert built on a name keeps working, not so that a wrong name outlives the
|
|
76
|
+
# reading of it. Renamed while the cost of renaming is zero.
|
|
77
|
+
"sde.telemetry.window_closed",
|
|
78
|
+
# dropped fires when the buffer is full and the oldest window is discarded - telemetry is
|
|
79
|
+
# the thing that gets lost when we run out of room, never an operation.
|
|
80
|
+
"sde.telemetry.dropped",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def log(event: str, /, **fields: Any) -> None:
|
|
86
|
+
"""Emit one structured event.
|
|
87
|
+
|
|
88
|
+
Nothing here can raise. The handler belongs to the client's application: it might write to a
|
|
89
|
+
socket that just closed, or be a custom formatter with a bug in it. A library that fails a
|
|
90
|
+
request because its own log line could not be written has no business being in somebody else's
|
|
91
|
+
process.
|
|
92
|
+
|
|
93
|
+
An unknown event name used to raise, on the argument that it is our programming error and the
|
|
94
|
+
test suite is where it should surface. **Measured, it did not.** The names that go unnoticed
|
|
95
|
+
are the ones on rare paths, and a rare path is precisely where no test goes:
|
|
96
|
+
``sde.explain.no_query_tree`` shipped absent from the vocabulary behind a ``pragma: no cover``,
|
|
97
|
+
and a client on a ClickHouse without the new analyzer got ``ValueError: ... is not a known
|
|
98
|
+
event. Add it to EVENTS ...`` - a note addressed to us, delivered to them, instead of the query
|
|
99
|
+
plan, on the one path written to degrade gracefully.
|
|
100
|
+
|
|
101
|
+
So the guard moved to where it can be total: a static test reads every ``log()`` call site in
|
|
102
|
+
the package and requires the vocabulary and the code to agree in both directions. That fails in
|
|
103
|
+
CI on every path rather than in production on one. Here an unknown name is counted through the
|
|
104
|
+
same registry as a broken handler and nothing is emitted - so the vocabulary stays closed, a
|
|
105
|
+
client can still see that it happened, and it costs them no call.
|
|
106
|
+
"""
|
|
107
|
+
if event not in EVENTS:
|
|
108
|
+
_count("logging.unknown_event")
|
|
109
|
+
return
|
|
110
|
+
# Checked before anything is built. Routing logs on every operation and an unconfigured logger
|
|
111
|
+
# is
|
|
112
|
+
# the normal case in production, so the cost of a log line nobody consumes has to be one
|
|
113
|
+
# comparison rather than a dictionary construction.
|
|
114
|
+
if not logger.isEnabledFor(logging.INFO):
|
|
115
|
+
return
|
|
116
|
+
try:
|
|
117
|
+
logger.info(event, extra={"sde_event": event, "sde_fields": fields})
|
|
118
|
+
except BaseException as exc:
|
|
119
|
+
if isinstance(exc, (KeyboardInterrupt, SystemExit)):
|
|
120
|
+
raise
|
|
121
|
+
_count("logging.emit")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _count(what: str) -> None:
|
|
125
|
+
"""Record one of our own failures without going through :func:`~sde.internal.guard`.
|
|
126
|
+
|
|
127
|
+
guard() reports through log(), so calling it from here would be a loop. Same registry, by hand.
|
|
128
|
+
"""
|
|
129
|
+
from .internal import _failures, _lock
|
|
130
|
+
|
|
131
|
+
with _lock:
|
|
132
|
+
_failures[what] = _failures.get(what, 0) + 1
|