fleetmesh-node 0.2.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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
node/app/views.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Views the CLI and the control socket share: a peer's grants and windows.
|
|
2
|
+
|
|
3
|
+
Separate because `meshnode peer` reads them straight from the files when no
|
|
4
|
+
daemon is running, and must not import the node to do so.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Mapping, Optional, TypedDict
|
|
10
|
+
|
|
11
|
+
from fleetmesh_core.accounting import accounting
|
|
12
|
+
from fleetmesh_core.wire.wire_types import GrantRow, HistoryRow, PendingRow
|
|
13
|
+
from ..storage import grants as grants_mod
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GrantView(TypedDict, total=False):
|
|
17
|
+
peer: str
|
|
18
|
+
issued: Optional[GrantRow]
|
|
19
|
+
received: Optional[GrantRow]
|
|
20
|
+
issued_history: list[HistoryRow]
|
|
21
|
+
received_history: list[HistoryRow]
|
|
22
|
+
pending: Optional[PendingRow]
|
|
23
|
+
seed_applied: Optional[tuple[str, int]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WindowView(TypedDict, total=False):
|
|
27
|
+
epoch: int
|
|
28
|
+
closed: int
|
|
29
|
+
severity: Optional[str]
|
|
30
|
+
measured: dict[str, int]
|
|
31
|
+
claimed: Optional[dict[str, int]]
|
|
32
|
+
receipts_in: int
|
|
33
|
+
my_usage: dict[str, int]
|
|
34
|
+
my_receipts_sent: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _live_peers(grants: grants_mod.GrantStore) -> list[str]:
|
|
38
|
+
"""GrantStore.peers() as a plain list of pubkeys. The store's own
|
|
39
|
+
annotation is unreadable to the checker because GrantStore.list shadows
|
|
40
|
+
the builtin in its class body (node/grants.py, not owned by this change)."""
|
|
41
|
+
raw: Any = grants.peers()
|
|
42
|
+
return [str(p) for p in raw]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def grant_view(grants: grants_mod.GrantStore, pk: str) -> GrantView:
|
|
46
|
+
"""Both grants with a peer, their history, the pending tier and the seed.
|
|
47
|
+
The same structure whether read from the files or asked of the daemon."""
|
|
48
|
+
return {
|
|
49
|
+
'peer': pk,
|
|
50
|
+
'issued': grants.get(pk, grants_mod.ISSUED),
|
|
51
|
+
'issued_history': grants.history(pk, grants_mod.ISSUED),
|
|
52
|
+
'received': grants.get(pk, grants_mod.RECEIVED),
|
|
53
|
+
'received_history': grants.history(pk, grants_mod.RECEIVED),
|
|
54
|
+
'pending': grants.pending(pk),
|
|
55
|
+
'seed_applied': grants.seed_applied(pk),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def windows_view(source: Any, pk: str, last: int = 0) -> list[WindowView]:
|
|
60
|
+
"""The windows this node priced for a peer, from an accounts database:
|
|
61
|
+
measured against claimed in the issuer frame, receipts in and sent.
|
|
62
|
+
|
|
63
|
+
`source` is an AccountStore (its `_all` takes the store lock and closes
|
|
64
|
+
the cursor before letting go - the daemon path) or a bare read-only
|
|
65
|
+
sqlite3 connection (the CLI reading the file beside a stopped or running
|
|
66
|
+
node). Either way every statement is finished and its cursor closed
|
|
67
|
+
before the next; `.fetchone()` on its own leaves the statement live to be
|
|
68
|
+
finalised by whichever thread the collector happens to run in.
|
|
69
|
+
"""
|
|
70
|
+
def rows(q: str, args: tuple[Any, ...]) -> list[Any]:
|
|
71
|
+
if hasattr(source, '_all'):
|
|
72
|
+
return list(source._all(q, args))
|
|
73
|
+
cur = source.execute(q, args)
|
|
74
|
+
try:
|
|
75
|
+
return cur.fetchall()
|
|
76
|
+
finally:
|
|
77
|
+
cur.close()
|
|
78
|
+
|
|
79
|
+
def one(q: str, args: tuple[Any, ...]) -> Any:
|
|
80
|
+
got = rows(q, args)
|
|
81
|
+
return got[0] if got else None
|
|
82
|
+
|
|
83
|
+
def col(q: str, args: tuple[Any, ...]) -> list[Any]:
|
|
84
|
+
return [r[0] for r in rows(q, args)]
|
|
85
|
+
|
|
86
|
+
epochs = sorted(set(col('SELECT epoch FROM meters WHERE peer=?', (pk,)))
|
|
87
|
+
| set(col('SELECT epoch FROM evaluated WHERE peer=?', (pk,))))
|
|
88
|
+
epochs = epochs[-last:] if last else epochs
|
|
89
|
+
out: list[WindowView] = []
|
|
90
|
+
for e in epochs:
|
|
91
|
+
sev = one('SELECT severity FROM evaluated WHERE peer=? AND epoch=?', (pk, e))
|
|
92
|
+
by = one("SELECT counters FROM meters WHERE peer=? AND epoch=? AND side='by'", (pk, e))
|
|
93
|
+
me = one("SELECT counters FROM meters WHERE peer=? AND epoch=? AND side='of_me'", (pk, e))
|
|
94
|
+
n_in = one('SELECT count(*) FROM receipts WHERE peer=? AND epoch=?', (pk, e))[0]
|
|
95
|
+
latest = one('SELECT body FROM receipts WHERE peer=? AND epoch=? ORDER BY seq DESC LIMIT 1', (pk, e))
|
|
96
|
+
chain = one('SELECT state FROM chain WHERE peer=? AND epoch=?', (pk, e))
|
|
97
|
+
out.append({'epoch': e, 'closed': accounting.window_bounds(e, 3600)[1],
|
|
98
|
+
'severity': (sev[0] or None) if sev else 'open',
|
|
99
|
+
'measured': json.loads(by[0]) if by else {},
|
|
100
|
+
'claimed': json.loads(latest[0])['counters'] if latest else None,
|
|
101
|
+
'receipts_in': n_in, 'my_usage': json.loads(me[0]) if me else {},
|
|
102
|
+
'my_receipts_sent': json.loads(chain[0])['seq'] if chain else 0})
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
PLANES: tuple[tuple[str, str, tuple[str, ...]], ...] = (
|
|
107
|
+
('relays', 'link', ('relays',)),
|
|
108
|
+
('dropped', 'link', ('dropped',)),
|
|
109
|
+
('recent_faults', 'link', ('recent_faults',)),
|
|
110
|
+
('publish_refused_in_window', 'link', ('publish_refused_in_window',)),
|
|
111
|
+
('cooling', 'link', ('cooling',)),
|
|
112
|
+
('inbox', 'link', ('inbox',)),
|
|
113
|
+
('wraps_in', 'link', ('wraps_in',)),
|
|
114
|
+
('wraps_seen_again', 'link', ('wraps_seen_again',)),
|
|
115
|
+
('unfinished_envelopes', 'link', ('unfinished_envelopes',)),
|
|
116
|
+
('awaiting_ack', 'link', ('awaiting_ack',)),
|
|
117
|
+
('unacknowledged', 'link', ('unacknowledged',)),
|
|
118
|
+
('sessions_open', 'sync', ('open',)),
|
|
119
|
+
('sessions_refused', 'sync', ('refused',)),
|
|
120
|
+
('sync_unsent', 'sync', ('unsent',)),
|
|
121
|
+
('malformed_sync', 'sync', ('malformed',)),
|
|
122
|
+
('budget_rejected', 'budget', ('rejected',)),
|
|
123
|
+
('receipts_refused', 'budget', ('receipts_refused',)),
|
|
124
|
+
('malformed_complaints', 'reporter', ('malformed',)),
|
|
125
|
+
('peering_refused', 'peering', ('refusals', 'recent')),
|
|
126
|
+
('throttled', 'scheduler', ('throttled',)),
|
|
127
|
+
('capacity', 'capacity', ()),
|
|
128
|
+
('strain', 'strain', ()),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def node_health(app: Any) -> Mapping[str, Any]:
|
|
133
|
+
"""Assemble the operator health report across all compartments and node planes."""
|
|
134
|
+
report = app.subsystems.report() if app._built else {}
|
|
135
|
+
|
|
136
|
+
def plane(name: str, path: tuple[str, ...]) -> Any:
|
|
137
|
+
d: Any = report.get(name)
|
|
138
|
+
if d is None:
|
|
139
|
+
return None
|
|
140
|
+
if not path:
|
|
141
|
+
return {k: v for k, v in d.items() if k != 'refusals'}
|
|
142
|
+
for key in path:
|
|
143
|
+
d = d.get(key) if isinstance(d, dict) else None
|
|
144
|
+
return d
|
|
145
|
+
|
|
146
|
+
h = {key: plane(name, path) for key, name, path in app.PLANES}
|
|
147
|
+
h.update({
|
|
148
|
+
'rejected': dict(app.rejected),
|
|
149
|
+
'handler_failures': app.counts['handler failed'],
|
|
150
|
+
'tick_failures': app.counts['tick failed'],
|
|
151
|
+
'close_failures': app.counts['close failed'],
|
|
152
|
+
'last_tick_error': app._last_tick_error,
|
|
153
|
+
'config_error': app._config_error,
|
|
154
|
+
'subsystems': report,
|
|
155
|
+
'control': app.control.state() if getattr(app, 'control', None) is not None else None,
|
|
156
|
+
'subsystem_trouble': app.subsystems.trouble() if app._built else [],
|
|
157
|
+
'subsystem_notes': app.subsystems.notes() if app._built else [],
|
|
158
|
+
})
|
|
159
|
+
return h
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def node_status(app: Any) -> Mapping[str, Any]:
|
|
163
|
+
"""Assemble the complete status.json view for a running or stopped NodeApp."""
|
|
164
|
+
from .. import release as release_mod
|
|
165
|
+
from ..interfaces import EvaluationRow, GrantStatusRow, PendingGrantRow, RepricedRow
|
|
166
|
+
|
|
167
|
+
h = app.health()
|
|
168
|
+
subs = h['subsystems']
|
|
169
|
+
|
|
170
|
+
def has(name: str, key: str, default: int = 0) -> Any:
|
|
171
|
+
return (subs.get(name) or {}).get(key, default)
|
|
172
|
+
|
|
173
|
+
face_url = app.face.url if app.face is not None else (app.host.url if getattr(app, 'host', None) is not None else '')
|
|
174
|
+
st = {
|
|
175
|
+
'did': app.ks.did,
|
|
176
|
+
'class': app.cfg['class'],
|
|
177
|
+
'face': face_url,
|
|
178
|
+
'release': release_mod.running().as_dict(),
|
|
179
|
+
'relays': app.relays,
|
|
180
|
+
'descriptor': app.descriptor['id'] if app.descriptor else None,
|
|
181
|
+
'peers': _live_peers(app.grants),
|
|
182
|
+
'grants': [GrantStatusRow(peer=g['peer'][:16], direction=g['direction'], tier=g['payload'].get('tier'),
|
|
183
|
+
status=g['status']) for g in app.grants.list()],
|
|
184
|
+
'evaluations': [EvaluationRow(peer=r.peer[:16], epoch=r.epoch, severity=r.severity,
|
|
185
|
+
state=r.state) for r in app.budget.evaluations[-10:]] if app._built else [],
|
|
186
|
+
'repriced': [RepricedRow(peer=r.peer[:16], epoch=r.epoch, was=r.was,
|
|
187
|
+
severity=r.severity, remedy_cancelled=r.remedy_cancelled)
|
|
188
|
+
for r in app.budget.repriced[-5:]] if app._built else [],
|
|
189
|
+
'pending_grants': [PendingGrantRow(peer=p[:16], tier=pend['payload'].get('tier'),
|
|
190
|
+
from_epoch=pend['decided_epoch'] + 1)
|
|
191
|
+
for p in _live_peers(app.grants)
|
|
192
|
+
if (pend := app.grants.pending(p)) is not None],
|
|
193
|
+
'refused_sessions': has('sync', 'refused'),
|
|
194
|
+
'reports_published': has('reporter', 'published'),
|
|
195
|
+
'pending_findings': has('reporter', 'pending'),
|
|
196
|
+
'events_stored': app.store.count(),
|
|
197
|
+
'at': app.clock(),
|
|
198
|
+
'health': h,
|
|
199
|
+
}
|
|
200
|
+
if app._last_tick:
|
|
201
|
+
st['last_tick'] = [list(map(str, d)) for d in app._last_tick]
|
|
202
|
+
return st
|
|
203
|
+
|
|
204
|
+
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""What a compartment says, declared where the compartment is.
|
|
2
|
+
|
|
3
|
+
A package rather than a root module: node/*.py is held to a monotone
|
|
4
|
+
baseline (fleetmesh.org/scripts/check-root.py) so the flat root stops
|
|
5
|
+
growing, and this is new surface rather than one of the dozen things
|
|
6
|
+
everything leans on.
|
|
7
|
+
|
|
8
|
+
A node has two ways of saying something happened and they are not connected.
|
|
9
|
+
`journal.line` prints a timestamped prose line to stderr - 45 call sites, each
|
|
10
|
+
formatting its own text, no levels and no channel. `NodeEventBus.emit` carries
|
|
11
|
+
a structured event through `WIRE` to the control socket's `Tap` - 6 call
|
|
12
|
+
sites, seven topic names, all of them declared in eventbus.py rather than by
|
|
13
|
+
the compartment that emits them.
|
|
14
|
+
|
|
15
|
+
So a compartment chooses a path by hand, and the two carry different events.
|
|
16
|
+
`pulled` is printed and invisible to the socket; `session.completed` is
|
|
17
|
+
emitted and absent from the log. An operator reading `journalctl` and a
|
|
18
|
+
dashboard reading the socket are looking at different nodes.
|
|
19
|
+
|
|
20
|
+
A `Channel` is one event declared once, in the compartment that raises it,
|
|
21
|
+
with both renderings attached: `wire` for a reader, `say` for a person. A
|
|
22
|
+
compartment emits once. `Log` subscribes to every declared channel and fans
|
|
23
|
+
out to the backends an operator configured. Nothing picks a path.
|
|
24
|
+
|
|
25
|
+
That is the same move `Parameter` makes for config and the same reason: two
|
|
26
|
+
expressions of one fact, kept in step by hand, is how a node comes to tell an
|
|
27
|
+
operator something other than what it did.
|
|
28
|
+
|
|
29
|
+
Backends are additive and each is cheap:
|
|
30
|
+
|
|
31
|
+
console journal.line, the existing stderr format, unchanged
|
|
32
|
+
tap the existing control-socket ring buffer
|
|
33
|
+
file newline-delimited JSON at a configured path
|
|
34
|
+
|
|
35
|
+
Standard library only, and the console backend keeps `journal.line`'s format
|
|
36
|
+
exactly so no operator's eye or log scraper has to relearn anything.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import dataclasses
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import threading
|
|
44
|
+
from typing import Any, Callable, Iterable, Mapping, Optional, Sequence, TypedDict, Union
|
|
45
|
+
|
|
46
|
+
from . import bus, journal
|
|
47
|
+
from .journal import line, short
|
|
48
|
+
from .bus import (
|
|
49
|
+
TOPIC_WINDOW_EVALUATED, TOPIC_WINDOW_REPRICED, TOPIC_SESSION_COMPLETED,
|
|
50
|
+
TOPIC_GRANT_ISSUED, TOPIC_PEER_CONNECTED, TOPIC_REPORT_ADMITTED,
|
|
51
|
+
TOPIC_META_DROPPED, EventHandler, WireFn, WIRE, register_wire, to_wire,
|
|
52
|
+
Subscriber, Tap, NodeEventBus,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Levels. Deliberately three, matching the vocabulary the node already uses in
|
|
56
|
+
# `trouble()` and `notes()` rather than importing syslog's eight, none of which
|
|
57
|
+
# this tree has ever distinguished.
|
|
58
|
+
INFO, NOTE, TROUBLE = 'info', 'note', 'trouble'
|
|
59
|
+
LEVELS: tuple[str, ...] = (INFO, NOTE, TROUBLE)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Record(TypedDict):
|
|
63
|
+
"""One event as a backend receives it."""
|
|
64
|
+
channel: str
|
|
65
|
+
level: str
|
|
66
|
+
at: int
|
|
67
|
+
payload: Mapping[str, Any]
|
|
68
|
+
said: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ChannelRecord(TypedDict, total=False):
|
|
72
|
+
"""A channel as `meshnode channels` and the docs read it.
|
|
73
|
+
|
|
74
|
+
Named records rather than loose mappings, for the reason check-dicts.py
|
|
75
|
+
enforces it: a row an operator reads by field name is the case that rule
|
|
76
|
+
exists for. `compartment` is absent until `every_channel` adds it.
|
|
77
|
+
"""
|
|
78
|
+
name: str
|
|
79
|
+
description: str
|
|
80
|
+
level: str
|
|
81
|
+
tags: list[str]
|
|
82
|
+
compartment: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class LogState(TypedDict):
|
|
86
|
+
"""What the log itself is doing, for the health block."""
|
|
87
|
+
backends: list[str]
|
|
88
|
+
channels: int
|
|
89
|
+
undeclared: dict[str, int]
|
|
90
|
+
failures: dict[str, int]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclasses.dataclass(frozen=True)
|
|
94
|
+
class Channel:
|
|
95
|
+
"""One thing a compartment can say, with both of its renderings.
|
|
96
|
+
|
|
97
|
+
`wire` is the structured payload a reader gets, which is what `WIRE` in
|
|
98
|
+
eventbus.py already holds per topic. `say` is the line a person reads,
|
|
99
|
+
which is what the 45 `journal.line` call sites each build inline. Keeping
|
|
100
|
+
them together is the point: an event cannot now exist in one and not the
|
|
101
|
+
other.
|
|
102
|
+
"""
|
|
103
|
+
name: str
|
|
104
|
+
description: str
|
|
105
|
+
level: str = INFO
|
|
106
|
+
wire: Optional[Callable[..., Mapping[str, Any]]] = None
|
|
107
|
+
# The human line. A format string is preferred and a callable is the
|
|
108
|
+
# fallback: a string is data, so it can be rendered into the docs,
|
|
109
|
+
# checked against the payload it formats, and changed without touching
|
|
110
|
+
# code. A callable is for a line that needs a conditional, which most do
|
|
111
|
+
# not.
|
|
112
|
+
say: Union[str, Callable[..., str], None] = None
|
|
113
|
+
tags: tuple[str, ...] = ()
|
|
114
|
+
|
|
115
|
+
def payload(self, *args: Any, **kw: Any) -> Mapping[str, Any]:
|
|
116
|
+
if self.wire is None:
|
|
117
|
+
return {'args': [journal.short(a) for a in args], **kw}
|
|
118
|
+
return self.wire(*args, **kw)
|
|
119
|
+
|
|
120
|
+
def said(self, *args: Any, **kw: Any) -> str:
|
|
121
|
+
"""The human line, from the format string or the callable.
|
|
122
|
+
|
|
123
|
+
A format string is rendered against the wire payload, so the two
|
|
124
|
+
renderings cannot describe different fields. A field the payload does
|
|
125
|
+
not carry is reported in the line rather than raised: a log must not
|
|
126
|
+
be able to fail the thing it is logging.
|
|
127
|
+
"""
|
|
128
|
+
if self.say is None:
|
|
129
|
+
return '%s %s' % (self.name, ' '.join(journal.short(a) for a in args))
|
|
130
|
+
if isinstance(self.say, str):
|
|
131
|
+
payload = self.payload(*args, **kw)
|
|
132
|
+
try:
|
|
133
|
+
return self.say.format(**payload)
|
|
134
|
+
except (KeyError, IndexError, ValueError) as e:
|
|
135
|
+
return '%s (unformattable line: %s) %s' % (self.name, e, dict(payload))
|
|
136
|
+
return self.say(*args, **kw)
|
|
137
|
+
|
|
138
|
+
def fields(self) -> tuple[str, ...]:
|
|
139
|
+
"""The payload fields the format string names, for the docs and for a
|
|
140
|
+
check that a line and its payload agree."""
|
|
141
|
+
if not isinstance(self.say, str):
|
|
142
|
+
return ()
|
|
143
|
+
import string # noqa: PLC0415
|
|
144
|
+
return tuple(f for _lit, f, _spec, _c in string.Formatter().parse(self.say) if f)
|
|
145
|
+
|
|
146
|
+
def describe(self) -> ChannelRecord:
|
|
147
|
+
return {'name': self.name, 'description': self.description,
|
|
148
|
+
'level': self.level, 'tags': list(self.tags)}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def declared(obj: Any) -> tuple[Channel, ...]:
|
|
152
|
+
"""The channels a compartment declares, or () for one not yet converted."""
|
|
153
|
+
return tuple(getattr(obj, 'CHANNELS', ()) or ())
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def every_channel(compartments: Sequence[Any]) -> list[ChannelRecord]:
|
|
157
|
+
"""Every declared channel, for `meshnode channels` and the docs.
|
|
158
|
+
|
|
159
|
+
The companion of `every_parameter`: one list of what a node can say, as
|
|
160
|
+
that one is a list of what it can be told.
|
|
161
|
+
"""
|
|
162
|
+
out: list[ChannelRecord] = []
|
|
163
|
+
for c in compartments:
|
|
164
|
+
for ch in declared(c):
|
|
165
|
+
row = ch.describe()
|
|
166
|
+
# A compartment names itself `name`; a config block names itself
|
|
167
|
+
# `key`. Both declare channels, so both spellings are read rather
|
|
168
|
+
# than one of them silently falling back to the class name.
|
|
169
|
+
row['compartment'] = (getattr(c, 'name', '') or getattr(c, 'key', '')
|
|
170
|
+
or getattr(c, '__name__', str(c)))
|
|
171
|
+
out.append(row)
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------- backends
|
|
176
|
+
|
|
177
|
+
class Backend:
|
|
178
|
+
"""Somewhere a record goes. Subclasses implement `write`."""
|
|
179
|
+
name: str = 'backend'
|
|
180
|
+
|
|
181
|
+
def write(self, rec: Record) -> None:
|
|
182
|
+
raise NotImplementedError
|
|
183
|
+
|
|
184
|
+
def close(self) -> None:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ConsoleBackend(Backend):
|
|
189
|
+
"""`journal.line`, byte for byte as before.
|
|
190
|
+
|
|
191
|
+
The format is not re-invented: a node runs under systemd, an operator's
|
|
192
|
+
eye is trained on these lines and so is anything grepping them.
|
|
193
|
+
"""
|
|
194
|
+
name = 'console'
|
|
195
|
+
|
|
196
|
+
def __init__(self, min_level: str = INFO) -> None:
|
|
197
|
+
self.floor: int = LEVELS.index(min_level) if min_level in LEVELS else 0
|
|
198
|
+
|
|
199
|
+
def write(self, rec: Record) -> None:
|
|
200
|
+
if LEVELS.index(rec['level']) < self.floor:
|
|
201
|
+
return
|
|
202
|
+
journal.line(rec['said'])
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TapBackend(Backend):
|
|
206
|
+
"""The control socket's ring buffer, which already exists.
|
|
207
|
+
|
|
208
|
+
Wrapping it rather than replacing it: the Tap holds the sequence numbers
|
|
209
|
+
and the bounded per-subscriber queues, and a reader that drains it today
|
|
210
|
+
keeps working.
|
|
211
|
+
"""
|
|
212
|
+
name = 'tap'
|
|
213
|
+
|
|
214
|
+
def __init__(self, tap: Any) -> None:
|
|
215
|
+
self.tap = tap
|
|
216
|
+
|
|
217
|
+
def write(self, rec: Record) -> None:
|
|
218
|
+
if self.tap is not None:
|
|
219
|
+
self.tap.record(rec['channel'], (), dict(rec['payload']))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class FileBackend(Backend):
|
|
223
|
+
"""Newline-delimited JSON at a path, one record per line.
|
|
224
|
+
|
|
225
|
+
Opened append-only and flushed per record, because a log that loses its
|
|
226
|
+
tail on a crash loses exactly the part being asked about.
|
|
227
|
+
"""
|
|
228
|
+
name = 'file'
|
|
229
|
+
|
|
230
|
+
def __init__(self, path: str, min_level: str = INFO) -> None:
|
|
231
|
+
self.path = path
|
|
232
|
+
self.floor: int = LEVELS.index(min_level) if min_level in LEVELS else 0
|
|
233
|
+
self._lock = threading.Lock()
|
|
234
|
+
d = os.path.dirname(os.path.abspath(path))
|
|
235
|
+
if d:
|
|
236
|
+
os.makedirs(d, exist_ok=True)
|
|
237
|
+
|
|
238
|
+
def write(self, rec: Record) -> None:
|
|
239
|
+
if LEVELS.index(rec['level']) < self.floor:
|
|
240
|
+
return
|
|
241
|
+
with self._lock:
|
|
242
|
+
with open(self.path, 'a', encoding='utf-8') as fh:
|
|
243
|
+
fh.write(json.dumps(rec, separators=(',', ':'), default=str) + '\n')
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class MemoryBackend(Backend):
|
|
247
|
+
"""Keeps records in a list. For tests, and for asking what just happened."""
|
|
248
|
+
name = 'memory'
|
|
249
|
+
|
|
250
|
+
def __init__(self, cap: int = 1000) -> None:
|
|
251
|
+
self.records: list[Record] = []
|
|
252
|
+
self.cap = cap
|
|
253
|
+
|
|
254
|
+
def write(self, rec: Record) -> None:
|
|
255
|
+
self.records.append(rec)
|
|
256
|
+
if len(self.records) > self.cap:
|
|
257
|
+
del self.records[0]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# -------------------------------------------------------------------- the log
|
|
261
|
+
|
|
262
|
+
class Log:
|
|
263
|
+
"""The one place a declared channel reaches a backend.
|
|
264
|
+
|
|
265
|
+
A compartment calls `log.say(channel, ...)` once. Where that goes is the
|
|
266
|
+
operator's configuration, not the compartment's decision, which is the
|
|
267
|
+
whole point: before this, choosing `journal.line` meant the socket never
|
|
268
|
+
saw it and choosing `emit` meant the journal never did.
|
|
269
|
+
|
|
270
|
+
A backend that raises is isolated and the others still run - the same rule
|
|
271
|
+
`NodeEventBus.emit` already applies to handlers, for the same reason: a
|
|
272
|
+
log that can take the node down is worse than no log.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(self, backends: Iterable[Backend] = (),
|
|
276
|
+
clock: Optional[Callable[[], int]] = None,
|
|
277
|
+
bus: Any = None, monitors: Any = None) -> None:
|
|
278
|
+
self.backends: list[Backend] = list(backends)
|
|
279
|
+
self.clock: Callable[[], int] = clock or (lambda: int(__import__('time').time()))
|
|
280
|
+
self.bus = bus
|
|
281
|
+
# The metrics registry, when the node has one. A counter declared
|
|
282
|
+
# against a channel is fed from here, so one declaration drives both
|
|
283
|
+
# the line and the count and the two cannot disagree about how many
|
|
284
|
+
# there were. See node/metrics.
|
|
285
|
+
self.monitors = monitors
|
|
286
|
+
self.registry: dict[str, Channel] = {}
|
|
287
|
+
self.unknown: dict[str, int] = {}
|
|
288
|
+
self.failures: dict[str, int] = {}
|
|
289
|
+
|
|
290
|
+
def register(self, channels: Iterable[Channel]) -> None:
|
|
291
|
+
"""Declare channels. Called once per compartment at assembly."""
|
|
292
|
+
for ch in channels:
|
|
293
|
+
self.registry[ch.name] = ch
|
|
294
|
+
|
|
295
|
+
def register_from(self, compartments: Iterable[Any]) -> None:
|
|
296
|
+
for c in compartments:
|
|
297
|
+
self.register(declared(c))
|
|
298
|
+
|
|
299
|
+
def say(self, name: str, *args: Any, **kw: Any) -> Record:
|
|
300
|
+
"""Raise `name`, rendering it once for every backend.
|
|
301
|
+
|
|
302
|
+
An unregistered channel is still carried, and counted. Dropping it
|
|
303
|
+
would make a compartment's new event silently invisible, which is the
|
|
304
|
+
failure this file exists to remove; counting it is how the gap gets
|
|
305
|
+
noticed.
|
|
306
|
+
"""
|
|
307
|
+
ch = self.registry.get(name)
|
|
308
|
+
if ch is None:
|
|
309
|
+
self.unknown[name] = self.unknown.get(name, 0) + 1
|
|
310
|
+
ch = Channel(name=name, description='(undeclared)')
|
|
311
|
+
# Rendering is the caller's arguments meeting the channel's own
|
|
312
|
+
# functions, so it can fail - a raise site passing the wrong number of
|
|
313
|
+
# them is the obvious way. It must not fail the caller: a log that can
|
|
314
|
+
# take down the thing it is logging is worse than no log, and the
|
|
315
|
+
# moment it does so is the moment something was already going wrong.
|
|
316
|
+
# The record still goes out, carrying what the failure was.
|
|
317
|
+
try:
|
|
318
|
+
payload: Mapping[str, Any] = ch.payload(*args, **kw)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
self.failures['payload'] = self.failures.get('payload', 0) + 1
|
|
321
|
+
payload = {'error': '%s: %s' % (type(e).__name__, e),
|
|
322
|
+
'args': [journal.short(a) for a in args]}
|
|
323
|
+
try:
|
|
324
|
+
said = ch.said(*args, **kw)
|
|
325
|
+
except Exception as e:
|
|
326
|
+
self.failures['said'] = self.failures.get('said', 0) + 1
|
|
327
|
+
said = '%s (unsayable: %s: %s)' % (ch.name, type(e).__name__, e)
|
|
328
|
+
rec: Record = {'channel': ch.name, 'level': ch.level, 'at': self.clock(),
|
|
329
|
+
'payload': payload, 'said': said}
|
|
330
|
+
for b in self.backends:
|
|
331
|
+
try:
|
|
332
|
+
b.write(rec)
|
|
333
|
+
except Exception:
|
|
334
|
+
self.failures[b.name] = self.failures.get(b.name, 0) + 1
|
|
335
|
+
if self.monitors is not None:
|
|
336
|
+
try:
|
|
337
|
+
self.monitors.observe(ch.name)
|
|
338
|
+
except Exception:
|
|
339
|
+
self.failures['monitors'] = self.failures.get('monitors', 0) + 1
|
|
340
|
+
if self.bus is not None:
|
|
341
|
+
# The in-process bus keeps working for anything that subscribed to
|
|
342
|
+
# a topic by name, so converting a compartment does not unsubscribe
|
|
343
|
+
# whatever was already listening.
|
|
344
|
+
try:
|
|
345
|
+
self.bus.emit(ch.name, **dict(rec['payload']))
|
|
346
|
+
except Exception:
|
|
347
|
+
self.failures['bus'] = self.failures.get('bus', 0) + 1
|
|
348
|
+
return rec
|
|
349
|
+
|
|
350
|
+
def state(self) -> LogState:
|
|
351
|
+
"""What the log itself is doing, for the health block.
|
|
352
|
+
|
|
353
|
+
`undeclared` and `failures` are reported rather than kept quiet: a log
|
|
354
|
+
dropping records, or carrying channels nobody declared, is the kind of
|
|
355
|
+
thing that is invisible precisely when it matters.
|
|
356
|
+
"""
|
|
357
|
+
out: LogState = {'backends': [b.name for b in self.backends],
|
|
358
|
+
'channels': len(self.registry),
|
|
359
|
+
'undeclared': dict(self.unknown),
|
|
360
|
+
'failures': dict(self.failures)}
|
|
361
|
+
return out
|
|
362
|
+
|
|
363
|
+
def close(self) -> None:
|
|
364
|
+
for b in self.backends:
|
|
365
|
+
try:
|
|
366
|
+
b.close()
|
|
367
|
+
except Exception:
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
__all__ = ['Channel', 'ChannelRecord', 'Record', 'LogState', 'Log', 'Backend',
|
|
372
|
+
'ConsoleBackend', 'TapBackend',
|
|
373
|
+
'FileBackend', 'MemoryBackend', 'declared', 'every_channel',
|
|
374
|
+
'INFO', 'NOTE', 'TROUBLE', 'LEVELS',
|
|
375
|
+
'TOPIC_WINDOW_EVALUATED', 'TOPIC_WINDOW_REPRICED', 'TOPIC_SESSION_COMPLETED',
|
|
376
|
+
'TOPIC_GRANT_ISSUED', 'TOPIC_PEER_CONNECTED', 'TOPIC_REPORT_ADMITTED',
|
|
377
|
+
'TOPIC_META_DROPPED', 'EventHandler', 'WireFn', 'WIRE', 'register_wire', 'to_wire',
|
|
378
|
+
'Subscriber', 'Tap', 'NodeEventBus', 'bus', 'journal', 'line', 'short']
|