fix(observability): address second review round

- Failed deletes no longer bump astrolabe_documents_indexed_total: the outer
  except in process_document now gates doc_type on operation != "delete", so a
  delete error is counted as processed-error but not as an indexing event.
  Added test_failed_delete_is_processed_but_not_indexed.
- registry parse span: pass record_exception=True explicitly (matches
  instrument_tool) and add a structured logger.warning on the parse-error path
  (processor/tier/byte_size/duration_ms) for a Loki-aggregatable failed-parse
  signal.
- test_error_does_not_increment_throughput: snapshot-before/delta pattern
  instead of absolute 0.0 (counters are global singletons).
- config: document the deliberate gateway asymmetry between
  get_embedding_model_name() (no gateway branch) and
  get_embedding_provider_family() (short-circuits on gateway).
- Cleanup in touched scope: narrow `except (HTTPStatusError, Exception)` to
  `except Exception` (drop now-unused import); convert registry signatures from
  Optional[...] to `... | None`.

Refs Deck #175, PR #831.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-02 21:09:50 +02:00
co-authored by Claude Opus 4.8
parent 2e49e442f4
commit 68c9e20636
4 changed files with 80 additions and 16 deletions
@@ -3,7 +3,7 @@
import logging
import time
from collections.abc import Awaitable, Callable
from typing import Any, Optional
from typing import Any
from nextcloud_mcp_server.observability.metrics import record_document_parse
from nextcloud_mcp_server.observability.tracing import trace_operation
@@ -72,7 +72,7 @@ class ProcessorRegistry:
len(processor.supported_mime_types),
)
def get_processor(self, name: str) -> Optional[DocumentProcessor]:
def get_processor(self, name: str) -> DocumentProcessor | None:
"""Get a processor by name.
Args:
@@ -85,7 +85,7 @@ class ProcessorRegistry:
return self._processors[name][0]
return None
def find_processor(self, content_type: str) -> Optional[DocumentProcessor]:
def find_processor(self, content_type: str) -> DocumentProcessor | None:
"""Find the first processor that supports the given MIME type.
Processors are checked in priority order (highest priority first).
@@ -117,12 +117,12 @@ class ProcessorRegistry:
self,
content: bytes,
content_type: str,
filename: Optional[str] = None,
processor_name: Optional[str] = None,
options: Optional[dict[str, Any]] = None,
progress_callback: Optional[
Callable[[float, Optional[float], Optional[str]], Awaitable[None]]
] = None,
filename: str | None = None,
processor_name: str | None = None,
options: dict[str, Any] | None = None,
progress_callback: (
Callable[[float, float | None, str | None], Awaitable[None]] | None
) = None,
) -> ProcessingResult:
"""Process a document using available processors.
@@ -183,6 +183,7 @@ class ProcessorRegistry:
"byte_size": byte_size,
"escalated": False,
},
record_exception=True,
) as span:
try:
result = await processor.process(
@@ -197,6 +198,22 @@ class ProcessorRegistry:
byte_size=byte_size,
status="error",
)
# Structured error signal for Loki (the processor logs the
# traceback; this adds the aggregatable fields). The span
# records the exception itself via record_exception=True.
logger.warning(
"Parse failed for %s with '%s' after %.2fs",
filename or "<bytes>",
processor.name,
duration,
extra={
"processor": processor.name,
"tier": tier,
"byte_size": byte_size,
"duration_ms": round(duration * 1000, 1),
"status": "error",
},
)
raise
duration = time.time() - start_time