Compare commits

..

1 Commits

Author SHA1 Message Date
Latte f53e1a3a5a feat: add structured logging helpers and instrument get_issue (#14)
docker / test (pull_request) Successful in 29s
test / test (push) Successful in 38s
docker / lint (pull_request) Successful in 39s
lint / lint (push) Successful in 39s
docker / docker-test (pull_request) Successful in 12s
docker / docker-publish (pull_request) Has been skipped
lint / lint (pull_request) Successful in 28s
test / test (pull_request) Successful in 22s
Adds reusable, secret-safe logging helpers to `logging_utils`:
- `log_event(logger, level, event, **context)` emits a named event with a
  sanitized `context` mapping (sensitive keys masked as `***`).
- `log_nullable_field(...)` records whether a parsed field is None plus its
  runtime type, without dumping its contents.
- `sanitize_context(...)` is the shared masking primitive.

The JSON formatter now serializes a record's `context` into the payload.

`get_issue_tool` is instrumented at DEBUG (`get_issue.start`,
`get_issue.payload_shape`, `get_issue.field_check` for labels/assignees/user)
so the nullable-field parsing that caused #13 is diagnosable going forward.

Adds tests for the helpers, the formatter, and the get_issue instrumentation,
and documents the pattern in docs/observability.md.
2026-06-22 15:40:36 +02:00
2 changed files with 13 additions and 4 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ class JsonLogFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str: def format(self, record: logging.LogRecord) -> str:
"""Serialize a log record to JSON.""" """Serialize a log record to JSON."""
payload = { payload: dict[str, Any] = {
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname, "level": record.levelname,
"logger": record.name, "logger": record.name,
+12 -3
View File
@@ -7,6 +7,7 @@ import logging
import pytest import pytest
from aegis_gitea_mcp.config import reset_settings
from aegis_gitea_mcp.logging_utils import ( from aegis_gitea_mcp.logging_utils import (
JsonLogFormatter, JsonLogFormatter,
log_event, log_event,
@@ -18,6 +19,16 @@ from aegis_gitea_mcp.tools.read_tools import get_issue_tool
READ_TOOLS_LOGGER = "aegis_gitea_mcp.tools.read_tools" READ_TOOLS_LOGGER = "aegis_gitea_mcp.tools.read_tools"
@pytest.fixture(autouse=True)
def tool_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Provide minimal settings environment for response limit helpers."""
reset_settings()
monkeypatch.setenv("GITEA_URL", "https://gitea.example.com")
monkeypatch.setenv("GITEA_TOKEN", "test-token")
monkeypatch.setenv("MCP_API_KEYS", "a" * 64)
monkeypatch.setenv("ENVIRONMENT", "test")
def test_sanitize_context_masks_sensitive_keys() -> None: def test_sanitize_context_masks_sensitive_keys() -> None:
"""Sensitive keys are masked case-insensitively; others pass through.""" """Sensitive keys are masked case-insensitively; others pass through."""
cleaned = sanitize_context( cleaned = sanitize_context(
@@ -100,9 +111,7 @@ async def test_get_issue_tool_emits_debug_events(caplog: pytest.LogCaptureFixtur
assert "get_issue.start" in events assert "get_issue.start" in events
assert "get_issue.payload_shape" in events assert "get_issue.payload_shape" in events
field_checks = [ field_checks = [r.context for r in caplog.records if r.getMessage() == "get_issue.field_check"]
r.context for r in caplog.records if r.getMessage() == "get_issue.field_check"
]
assert {"field": "labels", "is_none": True, "value_type": None} in field_checks assert {"field": "labels", "is_none": True, "value_type": None} in field_checks
assert {"field": "user", "is_none": True, "value_type": None} in field_checks assert {"field": "user", "is_none": True, "value_type": None} in field_checks