Skip to content

Commit 55cd5a5

Browse files
committed
ai(rules[AGENTS]): Add logging standard conventions
why: Establish structured logging conventions for tmuxp and libtmux, enabling OTel interop, pytest assertions on extra fields, and aggregator-friendly message templates. what: - Add Logging Standards section between Code Style and Doctests - 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 de1aada commit 55cd5a5

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,106 @@ windows:
116116
- **Type imports**: Use `import typing as t` and access via namespace (e.g., `t.Optional`)
117117
- **Development workflow**: Format → Test → Commit → Lint/Type Check → Test → Final Commit
118118

119+
## Logging Standards
120+
121+
These rules guide future logging changes; existing code may not yet conform.
122+
123+
### Logger setup
124+
125+
- Use `logging.getLogger(__name__)` in every module
126+
- Add `NullHandler` in library `__init__.py` files
127+
- Never configure handlers, levels, or formatters in library code — that's the application's job
128+
129+
### Structured context via `extra`
130+
131+
Pass structured data on every log call where useful for filtering, searching, or test assertions.
132+
133+
**Core keys** (stable, scalar, safe at any log level):
134+
135+
| Key | Type | Context |
136+
|-----|------|---------|
137+
| `tmux_cmd` | `str` | tmux command line |
138+
| `tmux_subcommand` | `str` | tmux subcommand (e.g. `new-session`) |
139+
| `tmux_target` | `str` | tmux target specifier (e.g. `mysession:1.2`) |
140+
| `tmux_exit_code` | `int` | tmux process exit code |
141+
| `tmux_session` | `str` | session name |
142+
| `tmux_window` | `str` | window name or index |
143+
| `tmux_pane` | `str` | pane identifier |
144+
| `tmux_config_path` | `str` | workspace config file path |
145+
| `tmux_layout` | `str` | window layout string |
146+
147+
**Heavy/optional keys** (DEBUG only, potentially large):
148+
149+
| Key | Type | Context |
150+
|-----|------|---------|
151+
| `tmux_stdout` | `list[str]` | tmux stdout lines (truncate or cap; `%(tmux_stdout)s` produces repr) |
152+
| `tmux_stderr` | `list[str]` | tmux stderr lines (same caveats) |
153+
154+
Treat established keys as compatibility-sensitive — downstream users may build dashboards and alerts on them. Change deliberately.
155+
156+
### Key naming rules
157+
158+
- `snake_case`, not dotted; `tmux_` prefix
159+
- Prefer stable scalars; avoid ad-hoc objects
160+
- Heavy keys (`tmux_stdout`, `tmux_stderr`) are DEBUG-only; consider companion `tmux_stdout_len` fields or hard truncation (e.g. `stdout[:100]`)
161+
162+
### Lazy formatting
163+
164+
`logger.debug("msg %s", val)` not f-strings. Two rationales:
165+
- Deferred string interpolation: skipped entirely when level is filtered
166+
- Aggregator message template grouping: `"Running %s"` is one signature grouped ×10,000; f-strings make each line unique
167+
168+
When computing `val` itself is expensive, guard with `if logger.isEnabledFor(logging.DEBUG)`.
169+
170+
### stacklevel for wrappers
171+
172+
Increment for each wrapper layer so `%(filename)s:%(lineno)d` and OTel `code.filepath` point to the real caller. Verify whenever call depth changes.
173+
174+
### LoggerAdapter for persistent context
175+
176+
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+.
177+
178+
### Log levels
179+
180+
| Level | Use for | Examples |
181+
|-------|---------|----------|
182+
| `DEBUG` | Internal mechanics, tmux I/O, config expansion | tmux command + stdout, trickle-down steps |
183+
| `INFO` | Session lifecycle, user-visible operations | Session created, window added, workspace loaded |
184+
| `WARNING` | Recoverable issues, deprecation, user-actionable config | Deprecated key, missing optional program |
185+
| `ERROR` | Failures that stop an operation | tmux command failed, config validation error |
186+
187+
Config discovery noise belongs in `DEBUG`; only surprising/user-actionable config issues → `WARNING`.
188+
189+
### Message style
190+
191+
- Lowercase, past tense for events: `"session created"`, `"tmux command failed"`
192+
- No trailing punctuation
193+
- Keep messages short; put details in `extra`, not the message string
194+
195+
### Exception logging
196+
197+
- Use `logger.exception()` only inside `except` blocks when you are **not** re-raising
198+
- Use `logger.error(..., exc_info=True)` when you need the traceback outside an `except` block
199+
- 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
200+
201+
### Testing logs
202+
203+
Assert on `caplog.records` attributes, not string matching on `caplog.text`:
204+
- Scope capture: `caplog.at_level(logging.DEBUG, logger="libtmux.common")`
205+
- Filter records rather than index by position: `[r for r in caplog.records if hasattr(r, "tmux_cmd")]`
206+
- Assert on schema: `record.tmux_exit_code == 0` not `"exit code 0" in caplog.text`
207+
- `caplog.record_tuples` cannot access extra fields — always use `caplog.records`
208+
209+
### Avoid
210+
211+
- f-strings/`.format()` in log calls
212+
- Unguarded logging in hot loops (guard with `isEnabledFor()`)
213+
- Catch-log-reraise without adding new context
214+
- `print()` for diagnostics
215+
- Logging secret env var values (log key names only)
216+
- Non-scalar ad-hoc objects in `extra`
217+
- Requiring custom `extra` fields in format strings without safe defaults (missing keys raise `KeyError`)
218+
119219
## Doctests
120220

121221
**All functions and methods MUST have working doctests.** Doctests serve as both documentation and tests.

0 commit comments

Comments
 (0)