feat(observability): astrolabe_* metrics + traces for the document pipeline

Make per-tier bottlenecks in the document-processing pipeline
(scan -> fetch -> parse -> chunk -> embed -> Qdrant upsert) visible via
metrics, traces, and structured logs. Today the document_processors layer
emits only a logger.info line: no metric, no span, and page counts live only
inside a log string. The single processing-duration histogram is unlabeled and
whole-document, so it cannot isolate parse vs embed vs upsert.

New astrolabe_* metric family (distinct from the mcp_* protocol metrics):
- astrolabe_document_parse_{duration_seconds,total} + pages/chars/bytes counters
  recorded at the ProcessorRegistry.process() boundary (covers all current and
  future processors uniformly)
- astrolabe_document_escalation_total (dormant; tiered-pipeline readiness)
- astrolabe_embedding_{duration_seconds,requests_total,chunks_total,chars_total}
- astrolabe_document_chunks_total, astrolabe_documents_indexed_total{source,status}

Tracing: new document_processor.parse child span + enriched embed/chunk span
attributes (provider/model/batch_size/chunk_count). Structured logs gain a
consistent field vocabulary (doc_id, doc_type, processor, tier, pages, chars,
byte_size, chunks, duration_ms, status) so Loki can aggregate without regex.

Tier-readiness: processor/tier are labels from day one and a tier property is
added to DocumentProcessor, so adding docling/OCR/LLM tiers later is additive
(new label values, never new metrics). Tenant comes from the kube namespace
label; mime_type/model are span attributes only (cardinality). Existing
mcp_vector_sync_*/mcp_qdrant_* are left untouched.

Refs Deck #175 (superset of #173 Phase 2). Dashboard/recording-rules follow-up
tracked on #175 for homelab-argocd.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-02 17:46:20 +02:00
co-authored by Claude Opus 4.8
parent 7e4b83dc94
commit 5d205fcaab
8 changed files with 799 additions and 14 deletions
+44
View File
@@ -949,6 +949,50 @@ class Settings:
return f"simple-{self.simple_embedding_dimension}"
def get_embedding_provider_family(self) -> str:
"""
Get the active dense-embedding provider family (a low-cardinality label).
This is the single source of truth for the ``provider`` metric label and
the ``embedding.provider`` span attribute. It returns the provider
*family* (e.g. "bedrock"), never the model name, to keep metric
cardinality bounded.
Priority mirrors ``get_embedding_model_name`` / ProviderRegistry:
1. Gateway - if EMBEDDING_PROVIDER=gateway (family from the model prefix,
e.g. "mistral/mistral-embed" -> "mistral")
2. Bedrock - if AWS_REGION or BEDROCK_EMBEDDING_MODEL is set
3. OpenAI - if OPENAI_API_KEY is set
4. Mistral - if MISTRAL_API_KEY is set
5. Ollama - if OLLAMA_BASE_URL is set
6. Simple - fallback
Returns:
Provider family: gateway-routed family | bedrock | openai | mistral
| ollama | simple
"""
if self.embedding_provider == "gateway":
model = self.embedding_gateway_model or ""
return model.split("/", 1)[0] if "/" in model else "gateway"
if (
self.aws_region
or self.bedrock_embedding_model
or self.bedrock_generation_model
):
return "bedrock"
if self.openai_api_key:
return "openai"
if self.mistral_api_key:
return "mistral"
if self.ollama_base_url:
return "ollama"
return "simple"
def get_collection_name(self) -> str:
"""
Get Qdrant collection name.