diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a23c3e4..efd9b4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,9 @@ jobs: - name: Ruff format check run: uv run ruff format --check . + - name: RAG001 — logging policy check + run: uv run python scripts/check_logging.py + - name: Mypy (strict) run: uv run mypy packages/ apps/gateway/ diff --git a/docs/README.md b/docs/README.md index c0619a0..7ef32d9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ | File | Description | |------|-------------| +| [logging-policy.md](guides/logging-policy.md) | RAG001 policy: structured-logging requirement, allowlist, and how to extend it | | [ragctl-quickstart.md](guides/ragctl-quickstart.md) | Five-minute tour of the `ragctl` CLI | ## adr/ diff --git a/docs/guides/logging-policy.md b/docs/guides/logging-policy.md new file mode 100644 index 0000000..438a0ec --- /dev/null +++ b/docs/guides/logging-policy.md @@ -0,0 +1,64 @@ +# Logging policy (RAG001) + +## Overview + +Every log record emitted by AgentContextOS must conform to the 7-field JSON +schema enforced by `rag_observability.logging`. To guarantee this, the project +bans direct use of `logging.getLogger()` in application code — a pre-commit +hook (`scripts/check_logging.py`) called **RAG001** fails the build if it is +called outside an explicit allowlist. + +## Usage + +Use the structured logger in all new code: + +```python +from rag_observability.logging import get_logger + +_log = get_logger(__name__) +_log.info("event.name", extra={"rag_tenant_id": tenant_id, ...}) +``` + +`rag_core.logging` re-exports the same symbols as a backwards-compatibility +shim — either import path is fine. + +Run the check manually: + +```bash +task lint:logging # via Taskfile +uv run python scripts/check_logging.py # direct +``` + +The hook also runs automatically through pre-commit on every staged commit, +and via the `lint-test` job in [.github/workflows/ci.yml](../../.github/workflows/ci.yml) +on every push and pull request — a non-zero exit fails the build on +ubuntu-22.04, macos-14, and windows-latest. + +## Internals + +The check scans every tracked `.py` file under the repo root with the regex +`\blogging\.getLogger\s*\(` and reports each match as a `RAG001` violation. +Files in `_SKIP_DIRS` (`.venv`, `__pycache__`, `.git`, `.mypy_cache`, +`.ruff_cache`, `node_modules`) are skipped wholesale. Files in `_ALLOWLIST` +are permitted to call `logging.getLogger()` directly. + +### Allowlist + +| Path | Reason | +|------|--------| +| `packages/observability/src/rag_observability/logging.py` | The structured-logger implementation itself. | +| `packages/core/src/rag_core/logging.py` | Backwards-compat shim that re-exports the observability logger. | +| `packages/core/src/rag_core/audit.py` | Cannot import the structured logger without creating a circular import: `rag_observability` depends on `rag_core.types`, so any path from `rag_core.audit` → `rag_observability` → `rag_core` is a cycle. | +| `tests/logs/conftest.py` | Test fixtures that intentionally manipulate the `rag` logger hierarchy to assert schema conformance. | +| `scripts/check_logging.py` | This script — its pattern string contains the banned call as a literal. | + +## Extension points + +If you need to add a new allowlisted file, prefer fixing the underlying cause +instead — a circular import almost always points to a layering mistake, and +test scaffolding can usually live under `tests/logs/conftest.py`. When a new +entry is genuinely required: + +1. Add the path to `_ALLOWLIST` in [scripts/check_logging.py](../../scripts/check_logging.py). +2. Add a one-line comment above the entry explaining why the exception is necessary. +3. Update the allowlist table above in the same PR. diff --git a/scripts/check_logging.py b/scripts/check_logging.py index 45bd132..e98da23 100644 --- a/scripts/check_logging.py +++ b/scripts/check_logging.py @@ -27,6 +27,10 @@ Path("packages/observability/src/rag_observability/logging.py"), # Shim re-export — delegates to rag_observability.logging. Path("packages/core/src/rag_core/logging.py"), + # rag_core.audit cannot import rag_observability.logging (or the + # rag_core.logging shim that re-exports it) because rag_observability + # depends on rag_core.types — doing so would create a circular import. + Path("packages/core/src/rag_core/audit.py"), # Test infrastructure that intentionally manipulates the rag logger hierarchy. Path("tests/logs/conftest.py"), # This script — pattern string contains the banned call as a literal.