Telemetry¶
KDBL Context Lake (K-Lake) exposes Prometheus-format metrics, structured JSON logs, and standard Kubernetes health endpoints. Together they give you everything you need to alert on, dashboard, and debug a running deployment.
All of it is local. "Telemetry" here means what K-Lake exposes for you to scrape and read inside your own environment — it is not reported to KDBL. K-Lake has no call-home: no metrics, no logs, no usage counts and no crash reports are sent anywhere, and there is no address in the product that uploads to us. The only way anything reaches KDBL support is a diagnostic pack you generate, inspect, and send yourself.
Health endpoints¶
Every K-Lake service exposes:
| Path | Purpose |
|---|---|
/healthz |
Liveness. 200 if the process is up. Wire to your liveness probe. |
/readyz |
Readiness. 200 only if dependencies (database, mounts) are reachable. Wire to your readiness probe. |
Use /readyz for traffic gating; use /healthz only to restart wedged processes.
Metrics endpoint¶
Each service exposes Prometheus text-format metrics at /metrics on a dedicated port:
| Service | Port | Notes |
|---|---|---|
| API | 9100 |
|
| Worker | 9200 |
crawl / listing |
| Worker (stats sidecar) | 9201 |
statistics rollup |
| Metadata enrichment | 9102 |
optional metadata enrichment |
| Extractor | 9200 |
content extraction (kdbl_extract_*) |
| Extractor (engine) | 9101 |
content-extraction engine metrics |
All metrics share the kdbl_ prefix.
Wiring it to Prometheus¶
There is nothing to "turn on" inside K-Lake — every service always exposes
/metrics. You just need Prometheus to scrape it. How depends on your setup:
-
Plain Prometheus (annotation-based discovery): the deployments already carry
prometheus.io/scrape: "true"/prometheus.io/port/prometheus.io/pathpod annotations, so a Prometheus configured with the standardkubernetes-podsscrape job picks them up automatically — no extra config. -
Prometheus Operator / kube-prometheus-stack (the common case): the pod annotations are ignored. Scraping is driven by
ServiceMonitorobjects, so each component needs one pointing at a Service with a named telemetry port. Ready-made ServiceMonitors for every component (API, workers, metadata enrichment, and the extractor deployments) ship with the deployment manifests — apply them into the same namespace. A minimal one looks like:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: kdbl-extractor
namespace: kdbl
spec:
selector:
matchLabels:
app.kubernetes.io/name: kdbl-extractor
endpoints:
- port: telemetry # the named Service port, not the number
path: /metrics
interval: 10s
If your kube-prometheus-stack restricts ServiceMonitor discovery by label (
serviceMonitorSelector), add the release label it expects — e.g.release: <your-stack-release>— to each ServiceMonitor'smetadata.labels.
Key metrics¶
Throughput¶
| Metric | Type | Labels | What it tells you |
|---|---|---|---|
kdbl_files_written_total |
counter | backend, source_id |
Files persisted to the metadata store |
kdbl_files_inserted_total |
counter | backend, source_id |
New files added |
kdbl_files_updated_total |
counter | backend, source_id |
Existing files with changed metadata |
kdbl_files_unchanged_total |
counter | backend, source_id |
Files seen but already up-to-date |
kdbl_bytes_indexed_total |
counter | backend, source_id |
Total bytes covered by indexed files |
Queue depth¶
| Metric | Type | Labels | What it tells you |
|---|---|---|---|
kdbl_queue_depth |
gauge | state (pending, running, done, failed) |
Current size of each queue partition. Watch pending to know when to add workers. |
kdbl_subtree_backlog |
gauge | state (pending, running) |
Backlog of in-progress crawl work as a deep share fans out. pending is the signal for queue-depth autoscaling — it grows when a large crawl outpaces the current workers. |
kdbl_tenant_queue_depth |
gauge | tenant (tenant id), state (pending, running) |
The busiest tenants' queued and in-flight work across every queue, refreshed every 30 seconds. Only the top KDBL_TENANT_QUEUE_GAUGE_TOP tenants (default 20; 0 turns it off) get a series, so a deployment with thousands of tenants stays cheap to scrape. On a deployment hosting many customers, this answers "whose crawl is filling the queue?" |
kdbl_inflight_tasks |
gauge | worker_id |
Per-worker concurrency utilization |
kdbl_tasks_total |
counter | protocol, outcome |
Tasks completed, broken down by result |
Latency¶
| Metric | Type | Labels | What it tells you |
|---|---|---|---|
kdbl_list_seconds |
histogram | protocol |
Time to list one directory / prefix from a source |
kdbl_sink_write_seconds |
histogram | backend |
Time to persist one batch to the metadata store |
kdbl_meta_fetch_seconds |
histogram | protocol |
Time to gather optional enrichments per file |
Metadata enrichment¶
| Metric | Type | Labels | What it tells you |
|---|---|---|---|
kdbl_meta_files_written |
counter | backend, source_id |
Files with enrichment recorded |
kdbl_meta_tasks_total |
counter | protocol, result (ok, failed, skipped, parked) |
Enrichment task outcomes |
kdbl_meta_queue_depth |
gauge | state |
Enrichment queue size |
Content extraction¶
Emitted by the extractor deployments (scraped via their ServiceMonitors —
see above). The compute metrics are how you size GPU vs CPU extractor pools.
Track throughput as pages per second — rate(kdbl_extract_pages_total[5m])
— which is a truer measure of extractor work than files/second (a 400-page
report and a one-page memo are very different jobs) and is what the console's
extraction view shows.
| Metric | Type | Labels | What it tells you |
|---|---|---|---|
kdbl_extract_tasks_total |
counter | protocol, outcome (ok, failed, skipped, unchanged), plugin_version |
Extraction tasks processed, by result. unchanged = the extractor's per-task dedup guard skipped an already-extracted file. |
kdbl_extract_skipped_unchanged_total |
counter | source_id |
Files dropped from the extract enqueue because they were already extracted at their current version. This is the recrawl-dedup payoff — rises on a recrawl of an unchanged source; near-zero means files are changing (or the dedup isn't matching). |
kdbl_extract_seconds |
histogram | protocol |
End-to-end extraction latency per file |
kdbl_extract_plugin_seconds |
histogram | plugin_version |
Plugin-measured compute time per file (the GPU/CPU work itself) |
kdbl_extract_pages_total |
counter | protocol, plugin_version |
Pages/segments the plugin processed |
kdbl_extract_ocr_total |
counter | plugin_version |
Extractions where the plugin ran OCR (the heavy GPU path) |
kdbl_extract_chunks_written_total |
counter | protocol |
Searchable content chunks persisted |
kdbl_extract_bytes_total |
counter | protocol |
Source bytes streamed to the extractor |
kdbl_extract_queue_depth |
gauge | state (pending, running) |
Content-extraction queue depth. The extractor HPA scales off pending. |
Retention¶
| Metric | Type | Labels | What it tells you |
|---|---|---|---|
kdbl_retention_runs_total |
counter | outcome |
Retention sweeps completed |
kdbl_retention_rows_deleted_total |
counter | tenant |
Rows removed by retention |
Per-tenant limits¶
Emitted by the API on a deployment hosting many customers, where each tenant has its own request rate and concurrency shares (see Hosting many customers).
| Metric | Type | Labels | What it tells you |
|---|---|---|---|
kdbl_tenant_limit_total |
counter | limit (rate, mcp, search, rerank), outcome (admitted, rejected, degraded) |
Every per-tenant limit decision. rejected is a 429 (REST) or a tenant-limit error (MCP); degraded is a search served without reranking because no rerank slot came free in time. |
kdbl_tenant_limit_rejected_total |
counter | tenant, limit |
The same refusals by tenant id — which customer is hitting its limit. A series exists only for a tenant that has been refused. |
Background index builds and the console certificate¶
| Metric | Type | Labels | What it tells you |
|---|---|---|---|
kdbl_deferred_indexes_incomplete |
gauge | — | Indexes an upgrade added that are still being built in the background. 0 once every one is complete. The worker re-checks every few minutes and finishes them without a restart; kdbl-control status --include-cluster lists any still building. |
kdbl_deferred_index_leaves |
gauge | index, state |
Per index, how many of its parts are built, so a slow build shows its progress. |
kdbl_console_tls_expires_timestamp_seconds |
gauge | — | When the console's HTTPS certificate expires (Unix time); 0 when none is installed. |
The chart's alert rules include KdblConsoleCertificateExpiring (warning,
under 30 days left) and KdblConsoleCertificateExpiringSoon (critical, under
7 days). Renew under Settings › Security › HTTPS certificate; while it is
close to expiry the console's command palette lists it under Needs
attention (type !).
MCP metrics and alerts¶
Emitted by the API on its telemetry port (9100).
| Metric | What it tells you |
|---|---|
kdbl_mcp_tool_calls_total{tool,status} |
Tool calls (and resource reads) by outcome: ok, error, not_found, denied |
kdbl_mcp_tool_seconds{tool} |
Tool latency |
kdbl_mcp_auth_failures_total{reason} |
Requests refused before authentication completed: no_token (the normal discovery probe every client opens with), invalid_token, origin_forbidden, tenant_suspended, server_error, cluster_admin_refused, scope_refused, throttled |
kdbl_mcp_rate_limited_total{limit} |
Refused by the per-user rate (principal) or by the failed sign-in throttle, per address (auth_ip) or per token (auth_token) |
kdbl_mcp_scope_grace_total |
Requests accepted on an unscoped personal access token during this release's grace period — re-mint those tokens with the mcp scope |
kdbl_mcp_shed_total |
Tool calls shed at the MCP concurrency limit |
kdbl_mcp_oauth_tokens_issued_total, kdbl_mcp_oauth_errors_total{error} |
The built-in sign-in server's token and client-registration endpoints |
kdbl_mcp_oauth_refresh_reuse_total |
A rotated refresh token was replayed — the token-theft signal; each one revokes that token family |
kdbl_mcp_cimd_fetch_total{outcome} |
Client metadata document fetches, including refused_address (a client id whose host resolves to a private address) |
cloudflared_tunnel_ha_connections |
Live edge connections per remote-access connector (tunnel on) |
With monitoring.rules.enabled: true (needs the Prometheus Operator), the chart ships these alerts in the kdbl.mcp group:
| Alert | Fires when | Severity |
|---|---|---|
KdblMcpAuthFailureSpike |
More failed sign-ins a minute than one address's budget (mcp.abuse.authFailPerMinute, 300) for 10 minutes |
warning |
KdblMcpAddressThrottled |
Any address used up its failed sign-in budget | info |
KdblMcpTokenThrottled |
A client keeps retrying a token that is throttled on its own budget, for 30 minutes | info |
KdblMcpScopeGraceInUse |
Unscoped personal access tokens were used on MCP in the last hour (re-mint them before the grace ends) | info |
KdblMcpRefreshTokenReuse |
A rotated refresh token was replayed | warning |
KdblMcpCallerRateLimited |
A caller is being held to its request rate for 15 minutes | warning |
KdblMcpToolErrorRate |
More than 20% of tool calls fail for 15 minutes | warning |
KdblMcpShedding |
Tool calls are shed at the concurrency limit for 10 minutes | warning |
KdblMcpSlowTools |
Tool p95 over 10 s for 15 minutes | info |
KdblMcpTunnelDown |
A running connector holds no edge connection for 5 minutes (tunnel on) | critical |
The limits these watch are described in Exposing MCP to the internet. Per-caller detail — who called which tool, from where, returning how much — is in the MCP audit trail.
Logs¶
All services log JSON to stdout. Configure the verbosity with the RUST_LOG environment variable, set in each component's ConfigMap:
Each log line carries (where applicable):
tenant_id— owning tenantsource_id— source the work was fortask_id— work-unit identifierworker_id— emitting worker podoutcome— final result tag
Ship logs into whatever aggregation stack you use (Loki, Elasticsearch, CloudWatch). Filtering by tenant_id or source_id is the fastest way to scope an investigation.
Suggested alerts¶
Starting points for production alerting:
| Alert | Condition |
|---|---|
| Queue building | kdbl_queue_depth{state="pending"} rising for >15 minutes and worker CPU at limit |
| Failed tasks rising | rate(kdbl_tasks_total{outcome="failed"}[5m]) > 0.1 |
| Sink writes slow | histogram_quantile(0.95, rate(kdbl_sink_write_seconds_bucket[5m])) > 1 |
| Readiness flapping | kube_pod_status_ready{condition="false"} for any K-Lake pod |
| Source unhealthy | last_error field returned by /api/sources/:id/health is non-null for >1 hour |
| A tenant is being limited | sum by (tenant, limit) (rate(kdbl_tenant_limit_rejected_total[5m])) > 0 for >15 minutes — raise that tenant's limit, or add API replicas |
Dashboarding¶
The basic operator dashboard is just three panels:
kdbl_queue_depthper state, stackedrate(kdbl_files_written_total[1m])persource_id, top 10histogram_quantile(0.95, rate(kdbl_sink_write_seconds_bucket[5m]))perbackend
Build out from there based on the sources and SLOs that matter to your tenants.