6.5 KiB
Schema eventi del EventLogger
EventLogger espone due sink paralleli attivabili via __init__:
- JSONL (
--log-json PATH) — uno{"ts":..., "kind":..., "data":{...}}per riga. - Markdown (
--log-md PATH) — heading## HH:MM:SS.mmm — kindseguito da un body specifico delkind.
Entrambi flush() dopo ogni evento (sub‑os buffer vuoto) → adatto a
tail -f.
Timestamp
- JSONL: ISO‑8601 UTC con precisione millisecondo +
Zfinale (es."2026-07-05T12:34:56.789Z"). - MD: solo l'ora,
HH:MM:SS.mmm.
Entrambi calcolati da datetime.now(timezone.utc) non da time.time()
(locale‑indipendente).
Tipi di evento
session_start (1 per sessione)
{"kind": "session_start", "data": {"host": "localhost", "port": 6666, "user": ""}}
MD: header # MUD log — <ts> con host, port, user poi ---.
session_end (1 per sessione)
{"kind": "session_end", "data": {"reason": "done"}}
Possibili reason: done, quit.
disconnect
{"kind": "disconnect", "data": {"reason": "local_close"}}
Emesso quando conn.close() viene chiamato explicitamente
(--quit, EOF, ecc.).
text
{"kind": "text", "data": {"raw": "Inn Hall\n\x1b[36mCobbles.\x1b[0m\n[ Exits: n ]\n"}}
Chunk ANSI grezzo dal server, subito dopo la IAC strip. Nel MD:
blocco ``` con testo privato di ANSI via strip_ansi.
Attenzione: ANSI con byte non validi vengono preservati nel
rawJSONL ma al massimo appaiono come "?" nel markdown. La redazione non si applica al text (se il server ti invia una password in chiaro la vedrai).
room_enter
{"kind": "room_enter", "data": {"id": "vnum12345", "name": "Inn Hall", "exits": ["n", "e"]}}
id:
vnum{int(num)}se GMCPRoom.Info.num/vnum/idera numerico.- altrimenti
name.lower()(case‑insensitive identity).
MD: lista bullet id / name / exits.
gmcp
{"kind": "gmcp", "data": {"module": "Char.Vitals", "payload": {"hp": 100, "max_hp": 120}}}
payload è il JSON decodificato (potrebbe essere null se il modulo
veniva emesso senza body). MD: blocco ```json con payload ordinato
per chiave + indentazione.
vitals
{"kind": "vitals", "data": {"hp": 100, "max_hp": 120, "sp": 50, "max_sp": 60, "mv": 80, "max_mv": 100}}
Solo i campi effettivamente presenti nel payload GMCP. MD:
HP 100/120 — SP 50/60 — MV 80/100.
line_sent
{"kind": "line_sent", "data": {"text": "north", "redacted": false}}
{"kind": "line_sent", "data": {"text": "***", "redacted": true}}
redacted=true ⇒ la text diventa "***" (la stringa originale
viene scartata). MD: > … *(redacted)* se redatto, altrimenti > ….
local_cmd
{"kind": "local_cmd", "data": {"cmd": "map", "args": [], "redacted": false}}
{"kind": "local_cmd", "data": {"cmd": "password", "args": ["***"], "redacted": true}}
Se redacted=true, args è collassato a una lista di "***" di pari
lunghezza all'originale (anche password multi‑parola).
subneg (forward hook diagnostico)
{"kind": "subneg", "data": {"opt": "NAWS", "bytes": 4}}
{"kind": "subneg", "data": {"opt": "TTYPE", "bytes": 0}}
Emesso da cli.py per ogni subnegoziazione telnet non‑GMCP
ricevuta. opt è il nome umano (ECHO/SUPPRESS_GO_AHEAD/
TTYPE/NAWS/GMCP o opt_<num> se sconosciuto) da _OPT_NAMES,
bytes è la lunghezza del payload. Utile in tail -f per
diagnosticare la negoziazione delle opzioni senza dover attivare
--log DEBUG.
MD: - **opt**: NAWS + - **bytes**: 4.
login
{"kind": "login", "data": {"phase": "user_sent", "value": "alice", "redact": false}}
{"kind": "login", "data": {"phase": "password_sent", "value": "***", "redact": true}}
{"kind": "login", "data": {"phase": "password_prompt", "value": "", "redact": true}}
phase ∈ {user_sent, password_prompt, password_sent}.
Default redact=True ⇒ value registrato come "***". Passa
redact=False esplicitamente per i nomi account (devono essere
tracciabili per leggere i log).
MD: **{phase}** → \{value}`` (con back‑tick escape).
Esempio di sessione (estratti)
$ jq -c '.' < session.jsonl
{"ts":"2026-07-05T12:34:56.789Z","kind":"session_start","data":{"host":"localhost","port":6666,"user":"zork"}}
{"ts":"2026-07-05T12:34:57.012Z","kind":"text","data":{"raw":"Welcome to Dead Souls.\n"}}
{"ts":"2026-07-05T12:34:57.234Z","kind":"text","data":{"raw":"What name do you wish?\n"}}
{"ts":"2026-07-05T12:34:57.235Z","kind":"line_sent","data":{"text":"zork","redacted":false}}
{"ts":"2026-07-05T12:34:57.236Z","kind":"login","data":{"phase":"user_sent","value":"zork","redact":false}}
{"ts":"2026-07-05T12:34:58.001Z","kind":"text","data":{"raw":"Password:\n"}}
{"ts":"2026-07-05T12:34:58.002Z","kind":"login","data":{"phase":"password_prompt","value":"","redact":true}}
{"ts":"2026-07-05T12:35:00.500Z","kind":"line_sent","data":{"text":"***","redacted":true}}
{"ts":"2026-07-05T12:35:00.501Z","kind":"login","data":{"phase":"password_sent","value":"***","redact":true}}
{"ts":"2026-07-05T12:35:01.123Z","kind":"gmcp","data":{"module":"Room.Info","payload":{"num":12345,"name":"Town Square","exits":{"n":12346}}}}
{"ts":"2026-07-05T12:35:01.124Z","kind":"room_enter","data":{"id":"vnum12345","name":"Town Square","exits":["n"]}}
{"ts":"2026-07-05T12:35:30.000Z","kind":"session_end","data":{"reason":"quit"}}
Helpers di analisi (jq)
# solo stanze visitate
jq -c 'select(.kind=="room_enter")' session.jsonl
# vitals (ultimo snapshot)
jq -c 'select(.kind=="vitals")' session.jsonl | tail -1
# righe inviate (escludendo redacted)
jq -c 'select(.kind=="line_sent" and .data.redacted==false)' session.jsonl
# tutti i comandi locali slash
jq -c 'select(.kind=="local_cmd")' session.jsonl
Idempotenza
EventLogger.close() può essere chiamato più volte senza effetti
collaterali. Nel finally di cli._run, viene chiamato esattamente
una volta anche se l'utente fa Ctrl‑C due volte.
Note di privacy
- Il client non logga la password in chiaro. Tutti i punti di
emissione relativi a credenziali sono protetti da
redacted=True. textchunk può contenere qualsiasi cosa il server ti invii. Se il MUD fa echo della tua password (raro, perché WILL ECHO è attivo), apparirà nel log.gmcp.payloadè decodificato; se un modulo MUD contiene segreti, li vedrai nel log.- Per ambienti condivisi si consiglia
--log-jsonsu FS cifrato e/o log ruotato con uno scheduler esterno (es.logrotate).