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.
This commit is contained in:
2026-06-22 15:09:59 +02:00
parent cd309ee290
commit f53e1a3a5a
4 changed files with 225 additions and 1 deletions
+22
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import logging
from typing import Any
from aegis_gitea_mcp.gitea_client import (
@@ -10,6 +11,7 @@ from aegis_gitea_mcp.gitea_client import (
GiteaClient,
GiteaError,
)
from aegis_gitea_mcp.logging_utils import log_event, log_nullable_field
from aegis_gitea_mcp.response_limits import limit_items, limit_text
from aegis_gitea_mcp.tools.arguments import (
CommitDiffArgs,
@@ -38,6 +40,8 @@ from aegis_gitea_mcp.tools.arguments import (
SearchCodeArgs,
)
logger = logging.getLogger(__name__)
async def search_code_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
"""Search repository code and return bounded result snippets."""
@@ -265,8 +269,26 @@ async def list_issues_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dic
async def get_issue_tool(gitea: GiteaClient, arguments: dict[str, Any]) -> dict[str, Any]:
"""Get issue details."""
parsed = IssueArgs.model_validate(arguments)
log_event(
logger,
logging.DEBUG,
"get_issue.start",
owner=parsed.owner,
repo=parsed.repo,
issue_number=parsed.issue_number,
)
try:
issue = await gitea.get_issue(parsed.owner, parsed.repo, parsed.issue_number)
log_event(
logger,
logging.DEBUG,
"get_issue.payload_shape",
top_level_keys=sorted(issue.keys()) if issue else None,
)
# Surface nullable collections that previously broke parsing (see #13).
log_nullable_field(logger, "get_issue.field_check", "labels", issue.get("labels"))
log_nullable_field(logger, "get_issue.field_check", "assignees", issue.get("assignees"))
log_nullable_field(logger, "get_issue.field_check", "user", issue.get("user"))
return {
"number": issue.get("number", 0),
"title": limit_text(str(issue.get("title", ""))),