Metadata-Version: 2.4
Name: demystify-platform-observability
Version: 0.2.0
Summary: Shared metrics + dashboard for Demystify services (Python mirror): canonical RED metric names, a prometheus-client registry with RED helpers, a framework-neutral /metrics handler, an offline HTML dashboard, and a span->metrics bridge.
Project-URL: Homepage, https://github.com/demystify-systems/ai-services-tools/tree/main/packages/platform-observability
Project-URL: Repository, https://github.com/demystify-systems/ai-services-tools
Author: Demystify Systems
License: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: prometheus-client<1,>=0.20
Provides-Extra: dev
Requires-Dist: httpx<1,>=0.27; extra == 'dev'
Requires-Dist: mypy<2,>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest<9,>=8; extra == 'dev'
Requires-Dist: ruff<0.9,>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# platform-observability — shared metrics + dashboard

`@demystify/platform-observability` (TS) · `demystify-platform-observability` (Py) · **v0.2.0** · MIT

## What it is / when to use it

The single source of truth for **RED metrics** across all six Demystify services.
Canonical `dmstfy_*` metric names, a metrics registry with RED helpers, a
framework-neutral `/metrics` handler, an offline HTML dashboard, and a bridge that
turns a module's existing span instrumentation into duration metrics for free.

Leaf package: depends only on `prom-client` (TS) / `prometheus-client` (Py). Zero
dependency on any module. Keyless, offline, deterministic.

## Install standalone

```
pnpm add @demystify/platform-observability        # TS
uv add demystify-platform-observability           # Py
```

## Canonical metric names

Defined once, shared by both languages (`METRIC_NAMES` / fixture-locked):

| name | type | labels |
|---|---|---|
| `dmstfy_http_requests_total` | counter | method, route, status, tenant |
| `dmstfy_http_request_duration_seconds` | histogram | method, route, status, tenant |
| `dmstfy_provider_calls_total` | counter | provider, model, status |
| `dmstfy_provider_call_duration_seconds` | histogram | provider, model, status |
| `dmstfy_provider_cost_micro_usd_total` | counter | provider, model |
| `dmstfy_queue_depth` | gauge | queue |
| `dmstfy_span_duration_seconds` | histogram | span, module |

Durations are seconds; cost is integer µUSD (CONVENTIONS.md §2). Buckets:
`[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]`.

## SDK usage

```ts
import {
  createMetrics, mountMetricsHono, renderDashboardHtml, spanExporterToMetrics,
} from "@demystify/platform-observability";

const metrics = createMetrics();
metrics.httpRequest("GET", "/v1/chat", 200, tenantId, 12.4);         // RED helper
metrics.providerCall("openai", "gpt-4o", "ok", 210, 1500);          // count+dur+cost
metrics.queueDepth("gateway", 7);

mountMetricsHono(app, metrics);                    // GET /metrics (Prometheus text)
setSpanExporter(spanExporterToMetrics(metrics));   // spans -> duration histogram
const html = renderDashboardHtml(metrics.snapshot());
```

```python
from demystify_platform_observability import (
    create_metrics, mount_metrics_fastapi, render_dashboard_html, span_exporter_to_metrics,
)

metrics = create_metrics()
metrics.http_request("GET", "/v1/chat", 200, tenant_id, 12.4)
metrics.provider_call("openai", "gpt-4o", "ok", 210, 1500)
metrics.queue_depth("gateway", 7)

mount_metrics_fastapi(app, metrics)                 # GET /metrics
set_span_exporter(span_exporter_to_metrics(metrics))
html = render_dashboard_html(metrics.snapshot())
```

## Mounting `/metrics`

- **Hono:** `mountMetricsHono(app, metrics, "/metrics")`.
- **FastAPI:** `mount_metrics_fastapi(app, metrics, "/metrics")`.
- **Anything else:** `metricsHandler(metrics)` / `metrics_handler(metrics)` returns
  `{status, contentType, body}` — wire it to your framework.

## Span → metrics bridge

`spanExporterToMetrics(metrics)` returns an object matching the Wave-7
`SpanExporter` shape (`export(span)` in TS; a `Callable[[Span], None]` in Py). Pass
it to a module's `setSpanExporter(...)` / `set_span_exporter(...)` and every
finished span becomes an observation on `dmstfy_span_duration_seconds`. Off by
default (no exporter registered = zero cost), exactly like §14.3.

## Architecture & ports/adapters

| seam | implementation | notes |
|---|---|---|
| metric store | private `prom-client` / `prometheus-client` registry per `Metrics` | isolated; no global default registry |
| snapshot | deterministic in-memory tally | ordering-stable; powers dashboard + parity |
| exposition | `metricsText()` / `metrics_text()` | Prometheus v0.0.4 text |

## Testing

`make -C packages/platform-observability setup lint test build`. Unit tests only
(offline). Parity: TS and Py replay the same `fixtures/snapshot.json` and must
produce byte-identical snapshots; metric names locked to `fixtures/metric-names.json`.

## v1 scope

RED + cost + queue-depth + span-duration. No exemplars, no push-gateway, no
histogram-quantile helpers (scrape + PromQL do that). Real Prometheus scrape is
the standard opt-in path; nothing here requires it to run.
