Skip to content

Commit 6fcd92a

Browse files
committed
ai(rules[AGENTS]): Add logging standard conventions
why: Establish structured logging conventions for libtmux, enabling OTel interop, pytest assertions on extra fields, and aggregator-friendly message templates. what: - Add Logging Standards section between Doctests and Git Commit Standards - Define tmux_ prefixed key vocabulary (core + heavy/optional) - Document lazy formatting, stacklevel, LoggerAdapter patterns - Specify log levels, message style, exception logging rules - Add testing guidance (caplog.records over caplog.text)
1 parent 236dcc1 commit 6fcd92a

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,102 @@ Window(@... ...:my_window, Session($... ...))
282282
3. **Keep doctests simple and focused** on demonstrating usage
283283
4. **Add blank lines between test sections** for improved readability
284284

285+
### Logging Standards
286+
287+
These rules guide future logging changes; existing code may not yet conform.
288+
289+
#### Logger setup
290+
291+
- Use `logging.getLogger(__name__)` in every module
292+
- Add `NullHandler` in library `__init__.py` files
293+
- Never configure handlers, levels, or formatters in library code — that's the application's job
294+
295+
#### Structured context via `extra`
296+
297+
Pass structured data on every log call where useful for filtering, searching, or test assertions.
298+
299+
**Core keys** (stable, scalar, safe at any log level):
300+
301+
| Key | Type | Context |
302+
|-----|------|---------|
303+
| `tmux_cmd` | `str` | tmux command line |
304+
| `tmux_subcommand` | `str` | tmux subcommand (e.g. `new-session`) |
305+
| `tmux_target` | `str` | tmux target specifier (e.g. `mysession:1.2`) |
306+
| `tmux_exit_code` | `int` | tmux process exit code |
307+
| `tmux_session` | `str` | session name |
308+
| `tmux_window` | `str` | window name or index |
309+
| `tmux_pane` | `str` | pane identifier |
310+
311+
**Heavy/optional keys** (DEBUG only, potentially large):
312+
313+
| Key | Type | Context |
314+
|-----|------|---------|
315+
| `tmux_stdout` | `list[str]` | tmux stdout lines (truncate or cap; `%(tmux_stdout)s` produces repr) |
316+
| `tmux_stderr` | `list[str]` | tmux stderr lines (same caveats) |
317+
318+
Treat established keys as compatibility-sensitive — downstream users may build dashboards and alerts on them. Change deliberately.
319+
320+
#### Key naming rules
321+
322+
- `snake_case`, not dotted; `tmux_` prefix
323+
- Prefer stable scalars; avoid ad-hoc objects
324+
- Heavy keys (`tmux_stdout`, `tmux_stderr`) are DEBUG-only; consider companion `tmux_stdout_len` fields or hard truncation (e.g. `stdout[:100]`)
325+
326+
#### Lazy formatting
327+
328+
`logger.debug("msg %s", val)` not f-strings. Two rationales:
329+
- Deferred string interpolation: skipped entirely when level is filtered
330+
- Aggregator message template grouping: `"Running %s"` is one signature grouped ×10,000; f-strings make each line unique
331+
332+
When computing `val` itself is expensive, guard with `if logger.isEnabledFor(logging.DEBUG)`.
333+
334+
#### stacklevel for wrappers
335+
336+
Increment for each wrapper layer so `%(filename)s:%(lineno)d` and OTel `code.filepath` point to the real caller. Verify whenever call depth changes.
337+
338+
#### LoggerAdapter for persistent context
339+
340+
For objects with stable identity (Session, Window, Pane), use `LoggerAdapter` to avoid repeating the same `extra` on every call. Lead with the portable pattern (override `process()` to merge); `merge_extra=True` simplifies this on Python 3.13+.
341+
342+
#### Log levels
343+
344+
| Level | Use for | Examples |
345+
|-------|---------|----------|
346+
| `DEBUG` | Internal mechanics, tmux I/O | tmux command + stdout, format queries |
347+
| `INFO` | Object lifecycle, user-visible operations | Session created, window added |
348+
| `WARNING` | Recoverable issues, deprecation | Deprecated method, missing optional program |
349+
| `ERROR` | Failures that stop an operation | tmux command failed, invalid target |
350+
351+
#### Message style
352+
353+
- Lowercase, past tense for events: `"session created"`, `"tmux command failed"`
354+
- No trailing punctuation
355+
- Keep messages short; put details in `extra`, not the message string
356+
357+
#### Exception logging
358+
359+
- Use `logger.exception()` only inside `except` blocks when you are **not** re-raising
360+
- Use `logger.error(..., exc_info=True)` when you need the traceback outside an `except` block
361+
- Avoid `logger.exception()` followed by `raise` — this duplicates the traceback. Either add context via `extra` that would otherwise be lost, or let the exception propagate
362+
363+
#### Testing logs
364+
365+
Assert on `caplog.records` attributes, not string matching on `caplog.text`:
366+
- Scope capture: `caplog.at_level(logging.DEBUG, logger="libtmux.common")`
367+
- Filter records rather than index by position: `[r for r in caplog.records if hasattr(r, "tmux_cmd")]`
368+
- Assert on schema: `record.tmux_exit_code == 0` not `"exit code 0" in caplog.text`
369+
- `caplog.record_tuples` cannot access extra fields — always use `caplog.records`
370+
371+
#### Avoid
372+
373+
- f-strings/`.format()` in log calls
374+
- Unguarded logging in hot loops (guard with `isEnabledFor()`)
375+
- Catch-log-reraise without adding new context
376+
- `print()` for diagnostics
377+
- Logging secret env var values (log key names only)
378+
- Non-scalar ad-hoc objects in `extra`
379+
- Requiring custom `extra` fields in format strings without safe defaults (missing keys raise `KeyError`)
380+
285381
### Git Commit Standards
286382

287383
Format commit messages as:

0 commit comments

Comments
 (0)