feat(worker): structured logs + metrics + traces for ingest worker

The external split-worker ingest pods (MCP_ROLE=worker / procrastinate) had
no observability: the worker CLI entrypoint never started a Prometheus
metrics server and never configured structured logging, so the pods that do
the real parse/embed/upsert work were invisible to Prometheus and emitted
plain-text logs the platform pipeline couldn't parse.

The always-on API pod bootstraps observability in its lifespan (app.py), but
the worker has its own entrypoint and never went through that path (or
uvicorn's JSON log_config). Add `_init_worker_observability()` mirroring the
API pod: setup_logging (JSON), setup_metrics on METRICS_PORT when
METRICS_ENABLED, and setup_tracing when an OTLP endpoint is configured.
Runs after the INGEST_QUEUE=postgres check so a misconfigured worker fails
fast without binding a metrics port.

This also unblocks the document-pipeline observability shipped in #831
(Deck #175): the astrolabe_* parse/embed/chunk metrics and the
document_processor.parse span are recorded in the shared registry/processor
code the worker executes — they were simply never exposed in external mode
because the worker served no /metrics and set up no tracer.

Deck #310, unblocks #175.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-11 05:23:33 +02:00
co-authored by Claude Opus 4.8
parent 457c115ef4
commit 04bda07de2
2 changed files with 199 additions and 2 deletions
+54 -1
View File
@@ -6,6 +6,7 @@ import click
import uvicorn
from nextcloud_mcp_server.config import (
Settings,
get_database_url,
get_settings,
is_ephemeral_token_db,
@@ -17,7 +18,12 @@ from nextcloud_mcp_server.migrations import (
show_migration_history,
upgrade_database,
)
from nextcloud_mcp_server.observability import get_uvicorn_logging_config
from nextcloud_mcp_server.observability import (
get_uvicorn_logging_config,
setup_logging,
setup_metrics,
setup_tracing,
)
from nextcloud_mcp_server.server import AVAILABLE_APPS
from .app import get_app
@@ -284,6 +290,44 @@ def run(
)
def _init_worker_observability(settings: Settings) -> None:
"""Configure logging, metrics, and tracing for the ingest worker.
Mirrors the observability bootstrap the API pod performs in its lifespan
(``app.py``), but for the standalone ``worker`` entrypoint which never runs
uvicorn. Without this the worker emits plain-text logs and serves no
``/metrics`` endpoint, so the astrolabe_* document-pipeline metrics and the
``document_processor.parse`` spans (recorded in the shared registry/processor
code the worker executes) stay invisible in external split-worker mode
(Deck #310 / #175).
"""
# Structured logging first, so every subsequent startup line is JSON like
# the API's — the worker entrypoint never went through uvicorn's log_config.
setup_logging(
log_format=settings.log_format,
log_level=settings.log_level,
include_trace_context=settings.log_include_trace_context,
)
if settings.metrics_enabled:
setup_metrics(port=settings.metrics_port)
logger.info(
"Prometheus metrics enabled on dedicated port %s", settings.metrics_port
)
if settings.otel_exporter_otlp_endpoint:
setup_tracing(
service_name=settings.otel_service_name,
otlp_endpoint=settings.otel_exporter_otlp_endpoint,
otlp_verify_ssl=settings.otel_exporter_verify_ssl,
sampling_rate=settings.otel_traces_sampler_arg,
)
logger.info(
"OpenTelemetry tracing enabled (endpoint: %s)",
settings.otel_exporter_otlp_endpoint,
)
@click.command()
@click.option(
"--concurrency",
@@ -319,6 +363,15 @@ def worker(concurrency: int | None):
f"resolved INGEST_QUEUE={settings.ingest_queue!r}"
)
# Initialize observability once the config is known to be runnable. The
# always-on API pod does this in its lifespan (app.py); the worker has its
# own entrypoint, so without this it emits plain-text logs and exposes no
# /metrics — leaving the ingest workload (which does the real
# parse/embed/upsert work, and where the astrolabe_* pipeline metrics +
# document_processor.parse spans are recorded) invisible in external
# split-worker mode (Deck #310, unblocks #175).
_init_worker_observability(settings)
from nextcloud_mcp_server.vector.queue.procrastinate import ( # noqa: PLC0415
INGEST_QUEUE_NAME,
apply_ingest_queue_schema,