moat-link-error 0.0.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.
- moat/link/error/__init__.py +19 -0
- moat/link/error/_cfg.yaml +6 -0
- moat/link/error/_impl.py +171 -0
- moat/link/error/_main.py +258 -0
- moat_link_error-0.0.1.dist-info/METADATA +57 -0
- moat_link_error-0.0.1.dist-info/RECORD +9 -0
- moat_link_error-0.0.1.dist-info/WHEEL +5 -0
- moat_link_error-0.0.1.dist-info/licenses/LICENSE.txt +14 -0
- moat_link_error-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error trace monitoring for :mod:`moat.link`.
|
|
3
|
+
|
|
4
|
+
The server strips the backtrace from an error record before storing it.
|
|
5
|
+
The full record is only published, non-retained, to ``run.error.*``. The
|
|
6
|
+
:class:`ErrorMon` daemon in this package subscribes to that topic and
|
|
7
|
+
remembers the first and the last trace per error path, so that
|
|
8
|
+
``moat link error PATH get`` can show them after the fact.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from moat.lib.config import register as _register
|
|
14
|
+
|
|
15
|
+
from ._impl import ErrorMon
|
|
16
|
+
|
|
17
|
+
_register(__name__)
|
|
18
|
+
|
|
19
|
+
__all__ = ["ErrorMon"]
|
moat/link/error/_impl.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MoaT-Link error trace monitor.
|
|
3
|
+
|
|
4
|
+
This daemon watches ``run.error.*`` — the non-retained MQTT topic that
|
|
5
|
+
carries the *full* error record, including transient data like
|
|
6
|
+
backtraces — and remembers the first and the last trace per error path.
|
|
7
|
+
``moat link error PATH get`` retrieves the stored traces via the
|
|
8
|
+
service announced by this daemon.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import anyio
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from moat.util import NotGiven, attrdict
|
|
17
|
+
from moat.lib.path import P, Path
|
|
18
|
+
from moat.lib.priomap import TimerMap
|
|
19
|
+
from moat.lib.rpc import MsgHandler
|
|
20
|
+
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from moat.link.backend import Message
|
|
26
|
+
from moat.link.client import Link
|
|
27
|
+
|
|
28
|
+
from collections.abc import AsyncIterator, Sequence
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
__all__ = ["ErrorMon"]
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
_TRACE_FIELDS = ("bt", "exc", "log")
|
|
36
|
+
"Fields of an error record that describe one occurrence."
|
|
37
|
+
|
|
38
|
+
_ERR_PATH = P(":R.run.error")
|
|
39
|
+
"The topic to watch, relative to the link root."
|
|
40
|
+
|
|
41
|
+
_ID_PATH = P(":R.run.id")
|
|
42
|
+
"The topic where processes announce themselves."
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ErrorMon(MsgHandler):
|
|
46
|
+
"""
|
|
47
|
+
Record transient error data.
|
|
48
|
+
|
|
49
|
+
The server persists error records to ``error.<path>`` but strips the
|
|
50
|
+
backtrace from them. The full record is published, non-retained, to
|
|
51
|
+
``run.error.<path>``. This daemon subscribes to that topic and
|
|
52
|
+
stores, per error path, the first and the last record that carries a
|
|
53
|
+
backtrace.
|
|
54
|
+
|
|
55
|
+
Stored traces are dropped when no new one has arrived for ``max_age``
|
|
56
|
+
seconds (default: a day). Storage is in memory only; restarting the
|
|
57
|
+
daemon loses the traces of errors that don't recur, and an error
|
|
58
|
+
that is active when the daemon starts has no ``first`` trace until a
|
|
59
|
+
new one arrives.
|
|
60
|
+
|
|
61
|
+
Error messages don't say which process sent them, or where it runs.
|
|
62
|
+
The daemon therefore also keeps the ``run.id.<ident>`` announcements
|
|
63
|
+
(host, PID, command line). A stored trace includes the announcement of
|
|
64
|
+
the process that caused it (the message's origin) as ``origin``. The
|
|
65
|
+
announcement disappears when the process ends, but a late error
|
|
66
|
+
message may still arrive, so the daemon keeps it for ``id_keep``
|
|
67
|
+
seconds (default: a minute) afterwards.
|
|
68
|
+
|
|
69
|
+
The daemon announces itself as the ``error.monitor`` service; its
|
|
70
|
+
``get`` command returns the stored data for an error path.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, cfg: attrdict) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Args:
|
|
76
|
+
cfg: The ``link.error.monitor`` sub-configuration.
|
|
77
|
+
"""
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.max_age: float = cfg.get("max_age", 86400)
|
|
80
|
+
self.id_keep: float = cfg.get("id_keep", 60)
|
|
81
|
+
self.traces: dict[Path, dict[str, Any]] = {}
|
|
82
|
+
"The stored trace data, by error path."
|
|
83
|
+
self.ids: dict[str, dict[str, Any]] = {}
|
|
84
|
+
"The announcements of known processes, by ident."
|
|
85
|
+
self._timers: TimerMap[Path | str] = TimerMap()
|
|
86
|
+
"Expiry of traces (keyed by error path) and of ended processes (by ident)."
|
|
87
|
+
|
|
88
|
+
async def run(self, link: Link, evt: anyio.Event | None = None) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Watch ``run.error.*`` and record transient data.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
link: A connected link client.
|
|
94
|
+
evt: Set once the subscription is up.
|
|
95
|
+
"""
|
|
96
|
+
async with (
|
|
97
|
+
anyio.create_task_group() as tg,
|
|
98
|
+
link.monitor(_ID_PATH, subtree=True) as ids,
|
|
99
|
+
link.monitor(_ERR_PATH, subtree=True, retained=False) as mon,
|
|
100
|
+
):
|
|
101
|
+
tg.start_soon(self._expire)
|
|
102
|
+
tg.start_soon(self._watch_ids, ids)
|
|
103
|
+
if evt is not None:
|
|
104
|
+
evt.set()
|
|
105
|
+
async for msg in mon:
|
|
106
|
+
self.record(
|
|
107
|
+
msg.topic[len(_ERR_PATH) :], msg.data, msg.meta.timestamp, msg.meta.origin
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
async def _watch_ids(self, mon: AsyncIterator[Message]) -> None:
|
|
111
|
+
"Track the processes' announcements."
|
|
112
|
+
async for msg in mon:
|
|
113
|
+
ident = msg.topic[-1]
|
|
114
|
+
if not isinstance(ident, str):
|
|
115
|
+
continue
|
|
116
|
+
if msg.data is NotGiven:
|
|
117
|
+
if ident in self.ids:
|
|
118
|
+
self._timers[ident] = self.id_keep
|
|
119
|
+
elif isinstance(msg.data, Mapping):
|
|
120
|
+
self.ids[ident] = dict(msg.data)
|
|
121
|
+
self._timers.pop(ident, None)
|
|
122
|
+
|
|
123
|
+
async def _expire(self) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Drop traces that haven't been refreshed for ``max_age``, and the
|
|
126
|
+
announcements of processes that ended more than ``id_keep`` ago.
|
|
127
|
+
"""
|
|
128
|
+
async for key in self._timers:
|
|
129
|
+
if isinstance(key, Path):
|
|
130
|
+
del self.traces[key]
|
|
131
|
+
else:
|
|
132
|
+
del self.ids[key]
|
|
133
|
+
|
|
134
|
+
def record(self, path: Path, data: Any, ts: float, origin: str | None = None) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Store the transient part of one ``run.error`` message.
|
|
137
|
+
|
|
138
|
+
Messages without a backtrace (counter updates, clearing an error)
|
|
139
|
+
are ignored: the stored record that the server includes with them
|
|
140
|
+
also has ``exc`` and ``log``, which would overwrite the last trace.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
path: The error path, without the ``run.error`` prefix.
|
|
144
|
+
data: The message's payload.
|
|
145
|
+
ts: The message's timestamp.
|
|
146
|
+
origin: The ident of the process that sent the message.
|
|
147
|
+
"""
|
|
148
|
+
if len(path) == 0 or not isinstance(data, Mapping) or "bt" not in data:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
trace = {k: data[k] for k in _TRACE_FIELDS if k in data}
|
|
152
|
+
trace["t"] = ts
|
|
153
|
+
if origin:
|
|
154
|
+
trace["origin"] = {**self.ids.get(origin, {}), "id": origin}
|
|
155
|
+
if (entry := self.traces.get(path)) is None:
|
|
156
|
+
self.traces[path] = {"trace": {"first": trace, "last": trace}, "seen": ts}
|
|
157
|
+
else:
|
|
158
|
+
entry["trace"]["last"] = trace
|
|
159
|
+
entry["seen"] = ts
|
|
160
|
+
self._timers[path] = self.max_age
|
|
161
|
+
|
|
162
|
+
async def cmd_get(self, path: Sequence[str | int]) -> dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Return the stored trace data for this error path, or an empty dict.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
path: The error path elements, without the ``error`` prefix.
|
|
168
|
+
"""
|
|
169
|
+
return self.traces.get(Path.build(path), {})
|
|
170
|
+
|
|
171
|
+
doc_get = "Return the stored first/last trace for an error path, or nothing."
|
moat/link/error/_main.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# command line interface
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import anyio
|
|
5
|
+
from contextlib import nullcontext
|
|
6
|
+
|
|
7
|
+
import asyncclick as click
|
|
8
|
+
|
|
9
|
+
from moat.util import NotGiven, yprint
|
|
10
|
+
from moat.util import time as mtime
|
|
11
|
+
from moat.lib.path import P, Path
|
|
12
|
+
from moat.lib.run import AliasedGroup, attr_args
|
|
13
|
+
from moat.link._data import data_get, node_attr
|
|
14
|
+
from moat.link.announce import announcing
|
|
15
|
+
from moat.link.client import Link
|
|
16
|
+
from moat.link.meta import MsgMeta
|
|
17
|
+
from moat.util.times import humandelta
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.group(cls=AliasedGroup, short_help="Manage error data", invoke_without_command=True) # pylint: disable=undefined-variable
|
|
23
|
+
@click.option("-m", "--meta", is_flag=True, help="include metadata")
|
|
24
|
+
@click.argument("path", type=P, nargs=1)
|
|
25
|
+
@click.pass_context
|
|
26
|
+
async def cli(ctx, path, meta) -> None:
|
|
27
|
+
"""
|
|
28
|
+
This subcommand reads and manipulates errors stored in the MoaT-Link service.
|
|
29
|
+
|
|
30
|
+
The bare command "moat link error monitor" runs the trace monitor
|
|
31
|
+
daemon, which remembers the backtraces that the server doesn't store.
|
|
32
|
+
To address an error named "monitor", add a subcommand, e.g.
|
|
33
|
+
"moat link error monitor get".
|
|
34
|
+
"""
|
|
35
|
+
obj = ctx.obj
|
|
36
|
+
cfg = obj.cfg["link"]
|
|
37
|
+
obj.conn = await ctx.with_async_resource(Link(cfg))
|
|
38
|
+
obj.meta = meta
|
|
39
|
+
if ctx.invoked_subcommand is None and path == P("monitor"):
|
|
40
|
+
await _run_monitor(obj)
|
|
41
|
+
return
|
|
42
|
+
path = P("error") + path
|
|
43
|
+
if ctx.invoked_subcommand is None:
|
|
44
|
+
await data_get(obj.conn, path, meta=obj.meta, out=obj.stdout, recursive=False)
|
|
45
|
+
else:
|
|
46
|
+
obj.path = path
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _run_monitor(obj) -> None:
|
|
50
|
+
"Run the trace monitor and announce its service."
|
|
51
|
+
from ._impl import ErrorMon # noqa: PLC0415
|
|
52
|
+
|
|
53
|
+
mon = ErrorMon(obj.cfg.link.error.monitor)
|
|
54
|
+
async with announcing(
|
|
55
|
+
obj.conn, host=False, name=P("error.monitor"), wait=True, service=mon
|
|
56
|
+
) as ann:
|
|
57
|
+
await mon.run(obj.conn, evt=ann)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _traces(link: Link, path: Path) -> dict[str, Any] | None:
|
|
61
|
+
"""
|
|
62
|
+
Ask the trace monitor for the stored traces of this error.
|
|
63
|
+
|
|
64
|
+
Returns `None` if the monitor isn't running or knows nothing.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
with anyio.fail_after(2):
|
|
68
|
+
srv = await link.get_service(P("error.monitor"))
|
|
69
|
+
res = await srv.get(path[1:])
|
|
70
|
+
except (KeyError, TimeoutError):
|
|
71
|
+
return None
|
|
72
|
+
return dict(res.kw) or None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@cli.command
|
|
76
|
+
@click.pass_obj
|
|
77
|
+
async def get(obj, **k) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Retrieve a MoaT-Link error entry.
|
|
80
|
+
|
|
81
|
+
If you read a sub-tree recursively, be aware that the whole subtree
|
|
82
|
+
will be read before anything is printed. Use the "watch --state" subcommand
|
|
83
|
+
for incremental output.
|
|
84
|
+
|
|
85
|
+
If the trace monitor ("moat link error monitor") is running, the
|
|
86
|
+
first and last backtrace it has seen are added as "trace".
|
|
87
|
+
"""
|
|
88
|
+
res = await data_get(obj.conn, obj.path, meta=obj.meta, out=False, **k)
|
|
89
|
+
if (trace := await _traces(obj.conn, obj.path)) is not None:
|
|
90
|
+
entry = res.get("_") if isinstance(res, dict) else None
|
|
91
|
+
if obj.meta and isinstance(entry, dict):
|
|
92
|
+
entry = entry.get("data")
|
|
93
|
+
if isinstance(entry, dict):
|
|
94
|
+
entry["trace"] = trace["trace"]
|
|
95
|
+
yprint(res, stream=obj.stdout)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cli.command("set", short_help="Update an entry")
|
|
99
|
+
@click.option("-k/-K", "--ok/--bad", is_flag=True, help="Mark as ok / not ok")
|
|
100
|
+
@attr_args
|
|
101
|
+
@click.pass_obj
|
|
102
|
+
async def set_(obj, ok, **kw) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Update some MoaT-Link error entry.
|
|
105
|
+
"""
|
|
106
|
+
if ok is not None:
|
|
107
|
+
kw["eval_"] = (*kw["eval_"], ("ok", repr(ok)))
|
|
108
|
+
res = await node_attr(obj, obj.path, **kw)
|
|
109
|
+
|
|
110
|
+
if obj.meta:
|
|
111
|
+
yprint(res, stream=obj.stdout)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@cli.command(short_help="Delete an entry / subtree")
|
|
115
|
+
@click.option(
|
|
116
|
+
"-b",
|
|
117
|
+
"--before",
|
|
118
|
+
type=float,
|
|
119
|
+
help="Don't delete entries created after this timestamp",
|
|
120
|
+
)
|
|
121
|
+
@click.option("-r", "--recursive", is_flag=True, help="Delete a complete subtree")
|
|
122
|
+
@click.pass_obj
|
|
123
|
+
async def delete(obj, before, recursive) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Delete an entry, or a subtree.
|
|
126
|
+
|
|
127
|
+
You might want to use "--before" flag to ensure that no other change
|
|
128
|
+
arrived after you viewed the data in question.
|
|
129
|
+
|
|
130
|
+
Non-recursively deleting an entry with children works and does *not*
|
|
131
|
+
affect the child entries.
|
|
132
|
+
|
|
133
|
+
The root entry cannot be deleted.
|
|
134
|
+
"""
|
|
135
|
+
args = {}
|
|
136
|
+
if recursive:
|
|
137
|
+
args["rec"] = recursive
|
|
138
|
+
if before:
|
|
139
|
+
args["ts"] = before
|
|
140
|
+
res = await obj.conn.d.delete(obj.path, **args)
|
|
141
|
+
if obj.meta:
|
|
142
|
+
res = dict(data=res[0], meta=MsgMeta.restore(res[1:]).repr())
|
|
143
|
+
else:
|
|
144
|
+
res = res[0]
|
|
145
|
+
yprint(res, stream=obj.stdout)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@cli.command("list")
|
|
149
|
+
@click.pass_obj
|
|
150
|
+
@click.option("-k/-A", "--ok/--all", is_flag=True, help="only list if ok / list all")
|
|
151
|
+
@click.option("-r", "--raw", is_flag=True, help="show age in seconds")
|
|
152
|
+
async def list_(obj, ok, raw) -> None:
|
|
153
|
+
"""
|
|
154
|
+
List all errors with their paths and age.
|
|
155
|
+
|
|
156
|
+
Shows the name and age of each error.
|
|
157
|
+
|
|
158
|
+
The default is to list only open errors.
|
|
159
|
+
"""
|
|
160
|
+
seen = False
|
|
161
|
+
now = mtime.time()
|
|
162
|
+
if ok is None:
|
|
163
|
+
ok = False
|
|
164
|
+
elif ok is False:
|
|
165
|
+
ok = None
|
|
166
|
+
|
|
167
|
+
async with obj.conn.d_watch(
|
|
168
|
+
obj.path,
|
|
169
|
+
state=True,
|
|
170
|
+
meta=True,
|
|
171
|
+
subtree=True,
|
|
172
|
+
) as mon:
|
|
173
|
+
async for pdm in mon:
|
|
174
|
+
if pdm is None:
|
|
175
|
+
break
|
|
176
|
+
p, d, m = pdm
|
|
177
|
+
if ok is not None and d.get("ok", False) != ok:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
print(
|
|
181
|
+
f"{int(now - m.timestamp) if raw else humandelta(now - m.timestamp):10} {p}",
|
|
182
|
+
file=obj.stdout,
|
|
183
|
+
)
|
|
184
|
+
seen = True
|
|
185
|
+
|
|
186
|
+
if not seen:
|
|
187
|
+
print("No errors found.", file=obj.stdout)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@cli.command()
|
|
191
|
+
@click.option("-m", "--mode", type=str, help="Retrieval mode", default="s")
|
|
192
|
+
@click.option("-M", "--mark", is_flag=True, help="Retrieval mode")
|
|
193
|
+
@click.option("-s", "--subtree", is_flag=True, help="Read the whole tree.")
|
|
194
|
+
@click.option("-D", "--add-date", is_flag=True, help="Add *_date entries")
|
|
195
|
+
@click.option("-i", "--ignore", multiple=True, type=P, help="Skip this (sub)tree")
|
|
196
|
+
@click.option("-n", "--min-length", type=int, help="Minimum path length")
|
|
197
|
+
@click.option("-N", "--max-length", type=int, help="Maximum path length")
|
|
198
|
+
@click.option("-a", "--max-age", type=int, help="Skip entries older than N seconds")
|
|
199
|
+
@click.option("-t", "--timeout", type=int, help="Stop reading after N seconds")
|
|
200
|
+
@click.pass_obj
|
|
201
|
+
async def monitor(
|
|
202
|
+
obj,
|
|
203
|
+
mode,
|
|
204
|
+
add_date, # noqa: ARG001
|
|
205
|
+
ignore,
|
|
206
|
+
mark,
|
|
207
|
+
subtree,
|
|
208
|
+
min_length,
|
|
209
|
+
max_length,
|
|
210
|
+
max_age,
|
|
211
|
+
timeout,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Monitor a MoaT-Link subtree.
|
|
214
|
+
|
|
215
|
+
The mode can be:
|
|
216
|
+
* c/current read current data from the server
|
|
217
|
+
* u/update read updates from MQTT
|
|
218
|
+
* s/stream current plus updates
|
|
219
|
+
* m/mqtt subscribe to MQTT stream, including retained data
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
match mode:
|
|
223
|
+
case "c" | "current":
|
|
224
|
+
state = True
|
|
225
|
+
case "u" | "update":
|
|
226
|
+
state = False
|
|
227
|
+
case "s" | "stream":
|
|
228
|
+
state = None
|
|
229
|
+
case "m" | "mqtt":
|
|
230
|
+
state = NotGiven
|
|
231
|
+
case _:
|
|
232
|
+
raise click.UsageError("Mode must be current|update|stream|mqtt")
|
|
233
|
+
if mark and state is not None:
|
|
234
|
+
raise click.UsageError("You can only add a mark in Stream mode")
|
|
235
|
+
|
|
236
|
+
def pm(p) -> bool:
|
|
237
|
+
for ip in ignore:
|
|
238
|
+
if len(p) >= len(ip) and p[: len(ip)] == ip:
|
|
239
|
+
return True
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
with anyio.move_on_after(timeout) if timeout else nullcontext():
|
|
243
|
+
async with obj.conn.d_watch(
|
|
244
|
+
obj.path,
|
|
245
|
+
state=state,
|
|
246
|
+
mark=mark,
|
|
247
|
+
meta=True,
|
|
248
|
+
subtree=subtree,
|
|
249
|
+
max_length=max_length,
|
|
250
|
+
min_length=min_length,
|
|
251
|
+
age=max_age,
|
|
252
|
+
) as mon:
|
|
253
|
+
async for res in mon:
|
|
254
|
+
if res is None:
|
|
255
|
+
res = "*** Snapshot data ends ***" # noqa:PLW2901
|
|
256
|
+
yprint(res, stream=obj.stdout)
|
|
257
|
+
print("---", file=obj.stdout)
|
|
258
|
+
obj.stdout.flush()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moat-link-error
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Error trace monitor for MoaT-Link
|
|
5
|
+
Author-email: Matthias Urlichs <matthias@urlichs.de>
|
|
6
|
+
Project-URL: homepage, https://m-o-a-t.org
|
|
7
|
+
Project-URL: repository, https://github.com/M-o-a-T/moat
|
|
8
|
+
Keywords: MoaT
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Framework :: Trio
|
|
13
|
+
Classifier: Framework :: AnyIO
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Requires-Dist: anyio~=4.2
|
|
19
|
+
Requires-Dist: asyncclick~=8.3
|
|
20
|
+
Requires-Dist: attrs
|
|
21
|
+
Requires-Dist: moat-util~=0.64.0
|
|
22
|
+
Requires-Dist: moat-link~=0.12.0
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Error traces
|
|
26
|
+
|
|
27
|
+
% start synopsis
|
|
28
|
+
% start main
|
|
29
|
+
|
|
30
|
+
Remember the backtraces of MoaT-Link errors.
|
|
31
|
+
|
|
32
|
+
The server stores error records without their backtrace. The full record
|
|
33
|
+
is only published, without retaining it, to `run.error.*`. This package
|
|
34
|
+
adds the `moat link error` command and a daemon (`moat link error monitor`)
|
|
35
|
+
that keeps the first and the last backtrace it sees for each error, for a
|
|
36
|
+
day by default, and hands them to `moat link error PATH get`, along with the host, PID and
|
|
37
|
+
command line of the process that caused the error.
|
|
38
|
+
|
|
39
|
+
% end synopsis
|
|
40
|
+
|
|
41
|
+
The daemon keeps its data in memory. A restart forgets the traces of
|
|
42
|
+
errors that don't recur.
|
|
43
|
+
|
|
44
|
+
% end main
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
Run `moat link error monitor` (the `moat-link-error-monitor` systemd unit
|
|
49
|
+
does that) on one host. Then `moat link error PATH get` adds the stored
|
|
50
|
+
traces to the error's record, as `trace.first` and `trace.last`.
|
|
51
|
+
|
|
52
|
+
How long traces are kept is configured with `link.error.monitor.max_age`.
|
|
53
|
+
See `docs/moat-link/errors.md` for details.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
This project is licensed under the same terms as MoaT.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
moat/link/error/__init__.py,sha256=o10-A8NZviQrmTBAboxqVlU-ZzaY98ds0oGto4H4Q-k,552
|
|
2
|
+
moat/link/error/_cfg.yaml,sha256=mjSs37GsoYPoFe5sDBkdA7zXJPforBBjsNwsDUFFlio,238
|
|
3
|
+
moat/link/error/_impl.py,sha256=DZEWNQVKqXqAJ2qAcT7_m2DpQLiqPnB9Vv9ZkQKTTzc,6346
|
|
4
|
+
moat/link/error/_main.py,sha256=KZxw0WEOglojCnj6HlnFO3iB5NWRVIXujEEcYUfVLiA,8051
|
|
5
|
+
moat_link_error-0.0.1.dist-info/licenses/LICENSE.txt,sha256=L5vKJLVOg5t0CEEPpW9-O_0vzbP0PEjEF06tLvnIDuk,541
|
|
6
|
+
moat_link_error-0.0.1.dist-info/METADATA,sha256=I6Ba59PE4skIlXq3zgSJ7OFA2M9zWRfLRzwUg2wwWg8,1817
|
|
7
|
+
moat_link_error-0.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
moat_link_error-0.0.1.dist-info/top_level.txt,sha256=pcs9fl5w5AB5GVi4SvBqIVmFrkRwQkVw_dEvW0Q0cSA,5
|
|
9
|
+
moat_link_error-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
The code in this repository, and all MoaT submodules it refers to,
|
|
2
|
+
is part of the MoaT project.
|
|
3
|
+
|
|
4
|
+
Unless a submodule's LICENSE.txt states otherwise, all included files are
|
|
5
|
+
licensed under the LGPL V3, as published by the FSF at
|
|
6
|
+
https://www.gnu.org/licenses/lgpl-3.0.html .
|
|
7
|
+
|
|
8
|
+
In addition to the LGPL's terms, the author(s) respectfully ask all users of
|
|
9
|
+
this code to contribute any bug fixes or enhancements. Also, please link back to
|
|
10
|
+
https://M-o-a-T.org.
|
|
11
|
+
|
|
12
|
+
Thank you.
|
|
13
|
+
|
|
14
|
+
Copyright © 2021 ff.: the MoaT contributor(s), as per the git changelog(s).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
moat
|