Skip to content

feat(otel,ai): TimescaleDB OTEL metrics + streaming agentic AI chat, API CLI tool, trace UI, alarms - #173

Merged
dviejokfs merged 6 commits into
mainfrom
feat/otel-metrics-timescaledb
Jun 30, 2026
Merged

dviejokfs merged 6 commits into
mainfrom
feat/otel-metrics-timescaledb

Conversation

@dviejokfs

Copy link
Copy Markdown
Contributor

What

Brings the default TimescaleDB OTEL-metrics backend to full-fidelity parity with the ClickHouse backend from #158. It now persists and queries the complete decoded metric record, with efficient day-one indexing.

ClickHouse is opt-in (TEMPS_CLICKHOUSE_*); TimescaleDB is what every default self-host runs — so this closes the fidelity gap for the common path (histograms, exponential histograms, summaries, exemplars, temporality).

Changes

  • Write (batch_insert_metrics): store all 31 columns — start_time, temporality, is_monotonic, flags, description, exponential-histogram fields, summary_quantiles, and exemplars (JSONB) — as bound params.
  • Read (query_metrics): temporality-aware histogram reconstruction (delta = element-wise bucket-count sum via WITH ORDINALITY; cumulative = latest snapshot per series via rn=1), NULL-safe group_by join (IS NOT DISTINCT FROM), and Prometheus-style histogram quantile interpolation (p50/p95/p99) instead of a mean fallback.
  • Migration m20260629_000001_otel_metrics_full_fidelity: 14 nullable columns + composite btree (project_id, metric_name, service_name, timestamp DESC) + GIN (attributes jsonb_path_ops). Idempotent, reversible, verified safe on compressed hypertables.
  • Reads deliberately stay on the raw hypertable, NOT the otel_metrics_1min/1hr continuous aggregates: the CAGGs lag materialization and would silently drop recent buckets from charts. Raw + the composite index + chunk pruning is correct and fast.

Security

The SQL builder is injection-safe: all user values are bound $N params; the only interpolated tokens are a clamped quantile float and an allowlisted bucket-interval integer (reviewed by the security-auditor agent — verdict: injection-safe).

Tests

  • 4 unit tests for histogram_quantile (interpolation, +Inf overflow bucket, empty/zero).
  • 11 integration tests against a real TimescaleDB container (skip-graceful when Docker is unavailable, per repo policy): aggregations, label filter, group_by, histogram summary, delta element-wise sum, cumulative latest-snapshot, quantile interpolation, NULL-label group_by, rate-per-sec, compressed-chunk DDL safety, bad-label rejection.
  • All green locally (25 integration + 331 unit), clippy clean.

E2E

Verified live end-to-end against the Observability Starter example app: real OTLP metrics → ingested into TimescaleDB (full-fidelity write confirmed) → rendered in the Observe metrics explorer, with histogram p50/p95/p99 bucket interpolation confirmed in the UI.

…arity

Bring the default TimescaleDB OTEL-metrics backend to parity with the
ClickHouse backend (PR #158): persist and query the full decoded metric
record, with efficient day-one indexing.

- batch_insert_metrics: store all 31 columns (start_time, temporality,
  is_monotonic, flags, description, exponential-histogram fields, summary
  quantiles, exemplars) as bound params.
- query_metrics: temporality-aware histogram reconstruction (delta =
  element-wise bucket-count sum via WITH ORDINALITY; cumulative = latest
  snapshot per series), NULL-safe group_by join (IS NOT DISTINCT FROM), and
  Prometheus-style histogram quantile interpolation (p50/p95/p99) instead of
  a mean fallback.
- Migration m20260629_000001: nullable columns + composite
  (project_id, metric_name, service_name, timestamp DESC) btree + GIN
  (attributes jsonb_path_ops) indexes; idempotent, reversible, verified safe
  on compressed hypertables.
- Reads stay on the raw hypertable, NOT the 1min/1hr continuous aggregates:
  the CAGGs lag materialization and would silently drop recent buckets from
  charts. Raw + composite index + chunk pruning is correct and fast.

Tests: 4 unit (histogram_quantile) + 11 integration vs real TimescaleDB
(aggregations, label filter, group_by, histogram summary, delta element-wise
sum, cumulative latest snapshot, quantile interpolation, NULL-label group_by,
rate-per-sec, compressed-chunk DDL safety, bad-label rejection).
…, mobile fixes

AI chat (temps-ai-chat / temps-ai-gateway / temps-ai):
- single-pass streaming with inline tool calls via AiService::chat_stream_turn
  (gateway accumulates OpenAI tool_call deltas by index); replaces the
  non-streaming gather loop so prose streams token-by-token with live tool cards
- stop/cancel: a dropped SSE connection halts generation (drops the AI stream)
  and persists the partial turn
- ordered render parts (text/tool segments) persisted so reload == live
- temps-ai-api-tools: read-only `temps` virtual CLI over the platform API
  (--help discovery, auto project scope); structured {operation,status,data} result
- add temps_ai* crates to the tracing EnvFilter allowlist

Chat UI (web):
- token streaming, interleaved tool cards, syntax-highlighted args/results,
  soft line-breaks, clickable links (new tab), Stop button
- draft + active-chat persistence, project-aware "new chat", page context,
  favicon + context-type avatar in the chat header

Observability:
- trace detail: per-span logs/events, service-coloured split waterfall with a
  wide side detail (focused drawer on mobile); logs<->trace correlation
- alarms UI + unified alarm/log (ADR-024/025); AI provider default model
- otel: fixed LogRecord.flags wire type; store all log severities

Mobile/shell: dvh app shell, body scroll lock, viewport fixes
Review fixes for #173:
- security: add project_scope_guard! to list_metric_label_keys /
  list_metric_label_values so a project-scoped deployment token can't enumerate
  another project's metric labels (no-op for user/API-key/session auth)
- validate_label_key now returns OtelError::Validation (HTTP 400) instead of
  Storage (500) — a malformed client label key is a client error
- docs: update temps-ai-chat api_tools module doc to the shipped single `temps`
  CLI tool (was describing the removed search_api/describe_api/call_api trio)
Resolves the 2 generated-SDK merge conflicts by regenerating from the merged
backend so the client has both main's and this branch's endpoints (alarms,
OTel metric label discovery, AI provider default_model, chat message parts).
@dviejokfs dviejokfs changed the title feat(otel): full-fidelity TimescaleDB OTEL-metrics storage (ClickHouse parity) feat(otel,ai): TimescaleDB OTEL metrics + streaming agentic AI chat, API CLI tool, trace UI, alarms Jun 30, 2026
@dviejokfs
dviejokfs merged commit f0ced47 into main Jun 30, 2026
11 of 12 checks passed
dviejokfs added a commit that referenced this pull request Jun 30, 2026
CI runs clippy across the whole workspace with -D warnings. A pre-existing
unnecessary_sort_by lint in temps-otel (merged from main via #173) failed the
advisory clippy job on this branch. Replace the manual comparator closure with
sort_by_key. No behaviour change.
dviejokfs added a commit that referenced this pull request Jun 30, 2026
…B load (#174)

* perf(proxy): cache IP block-list and geolocation to cut per-request DB load

The proxy issued a Postgres query on every request from two hot-path spots,
saturating the connection pool under load (~114 busy connections at 1k req/s):

- IpAccessControlService::is_blocked ran a COUNT(*) against ip_access_control
  in early_request_filter for every request.
- IpAddressService::get_or_create_ip queried ip_geolocations per unique IP
  from the batch-log enricher and the visitor manager.

Eliminate both from the request path:

- Hold an in-memory ArcSwap<Vec<IpNetwork>> snapshot of block rules, refreshed
  every 30s by a background task and after each write. is_blocked now does a
  lock-free CIDR-containment scan with no DB query (IpNetwork::contains mirrors
  the prior $ip <<= ip_address). Unparsable client IPs fail open as before.
- Add a moka cache (IP -> IpAddressInfo, 100k cap, 6h TTL) in IpAddressService
  so repeat IPs resolve from memory; only cache misses touch the DB. Benefits
  the batch writer and the visitor manager alike.

Per-request proxy DB queries drop from ~2 to ~0. Adds unit tests for the
in-memory block matching.

* docs(changelog): note proxy per-request DB query elimination (#174)

* fix(otel): use sort_by_key to satisfy clippy -D warnings

CI runs clippy across the whole workspace with -D warnings. A pre-existing
unnecessary_sort_by lint in temps-otel (merged from main via #173) failed the
advisory clippy job on this branch. Replace the manual comparator closure with
sort_by_key. No behaviour change.

* fix(ai-api-tools): use sort_by_key to satisfy clippy -D warnings

Another pre-existing unnecessary_sort_by lint (from main) that the whole-workspace
clippy -D warnings CI job flags. Replace the reverse comparator closure with
sort_by_key + Reverse. No behaviour change.
@dviejokfs
dviejokfs deleted the feat/otel-metrics-timescaledb branch July 1, 2026 00:07
dviejokfs added a commit that referenced this pull request Jul 26, 2026
…with (#442)

Closes 11 of the 14 open Dependabot alerts; the remaining 3 are
upstream-blocked and re-verified unreachable (documented in Cargo.toml).

Fixed:
- next 16.2.6 -> 16.2.11 in examples/nextjs/basic (alerts #175-#183, 9
  alerts: SSRF in Server Actions on custom servers, SSRF via rewrite
  destination hostname, middleware/proxy bypass with Turbopack + single
  locale, DoS in Server Actions, cache confusion on invalid-UTF-8 and
  on request bodies, unbounded Server Action payload on Edge, image
  optimization SVG DoS, and unauthenticated Server Function endpoint
  disclosure). Verified: tsc --noEmit clean and `next build` succeeds.
- setuptools 82.0.0 -> 83.0.0 in sdks/python/uv.lock (alert #174,
  MANIFEST.in sdist exclusion bypass via NFC/NFD collision on APFS/HFS+).
- serde_with 3.20.0 -> 3.21.0, pulling serde_with_macros 3.21.0 (alert
  #173, KeyValueMap serialize panic on empty sequence/map entries).
  Reached via cloudflare 0.14 -> temps-dns.

Not fixable, re-verified unreachable (triage refreshed in Cargo.toml):
- protobuf 2.28.0 (#4): pingora-core 0.8.1 declares prometheus = "0.13"
  with default features, and `protobuf` is a DEFAULT feature of
  prometheus 0.13 -- cargo features are additive, so it cannot be
  disabled without vendoring a pingora fork. No ProtobufEncoder /
  TextEncoder / protobuf:: / PrometheusServer reference exists under
  crates/, so the 2.x parser is never fed input.
- opentelemetry_sdk 0.30.0 (#151): pinned via the getsentry/relay git
  rev. The bug is in the W3C BaggagePropagator; no case-insensitive
  match for `baggage` exists under crates/.
- remove_dir_all 0.5.3 (#1): nixpacks 1.41.0 (still crates.io latest)
  uses deprecated tempdir, which hard-requires remove_dir_all ^0.5. The
  TOCTOU branch only runs when out_dir is None, and
  nixpacks_preset.rs:508 remains the only out_dir site, passing Some(..).
- serde_with 2.3.3 also remains via nixpacks and has no fix (2.3.3 is
  the last 2.x; the fix landed only in 3.21.0). KeyValueMap does exist
  in 2.3.3, but nixpacks uses serde_with with default-features = false,
  features = ["macros"], solely for skip_serializing_none.

Cargo.lock also re-unifies two entries that any re-resolve surfaces, both
within their declared ranges and not regressions: pingora-lru moves to
hashbrown 0.14.5 (its req is "0", and ahash/allocator-api2 are 0.14.5
defaults), and iana-time-zone moves to windows-core 0.57.0 (req is
">=0.56, <=0.62"; a cfg(target_os = "windows")-only dep, collapsing a
duplicate copy).

Verified: cargo check --lib --workspace clean (0 errors); cargo audit
vulnerability count unchanged at the 3 known upstream-blocked entries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant