a615b7ebfd
Closes leeworks-agents/api-company#5 (docs-site Astro scaffold) Closes leeworks-agents/api-company#9 (metrics instrumentation standard) Closes leeworks-agents/api-company#10 (Gitea Actions openapi aggregation pipeline) Closes leeworks-agents/api-company#11 (docs-site Flux HelmRelease) Closes leeworks-agents/api-company#12 (SEO blog posts x3) Closes leeworks-agents/api-company#13 (legal docs ToS/Privacy/AUP) Closes leeworks-agents/api-company#14 (DNS documentation) ## Changes ### docs/legal/ - terms-of-service.md — API usage, liability, account termination, governing law - privacy-policy.md — request log retention (90d), no PII sold, data sharing - acceptable-use-policy.md — rate limit abuse, scraping prohibition, resale ban ### docs/metrics-standard.md - Defines api_requests_total, api_response_duration_seconds, api_data_freshness_seconds - Fastify (TypeScript) and FastAPI (Python) reference middleware implementations - Prometheus scrape config and Grafana dashboard guidance ### docs/registry.md - Decision: use Gitea built-in container registry (no new infra) - Image naming convention, auth, Kubernetes imagePullSecrets, ingress config ### docs/dns.md - Required A records for all 6 subdomains - cert-manager ClusterIssuer and Ingress TLS examples - Verification commands and human-operator action items ### docs-site/ - Astro 4 + MDX + sitemap scaffold - Base layout with nav linking all APIs, blog, RapidAPI, status - Landing page with API cards - Per-API Redoc viewer pages (zip-enrichment, holidays, air-quality) - Blog index + 3 SEO blog posts (~1000 words each with JSON-LD) - Dockerfile (multi-stage: node build + nginx serve) - nginx.conf with gzip, caching, health endpoint ### flux/ - gitea-runner/: gitea-act-runner HelmRelease (org-scope, dind) - monitoring/: kube-prometheus-stack + Gatus HelmReleases - Prometheus with pod annotation scraping - Grafana at grafana.leeworks.dev with persistence - Gatus status page at status.leeworks.dev, 90-day retention - docs-site/: Deployment + Service + Ingress via raw chart - api-company-source/: GitRepository + Kustomization reference manifests - kustomization.yaml: root kustomize entry point (build validated) ### .gitea/workflows/build-docs.yaml - Aggregates openapi.yaml from zip-enrichment, holidays, air-quality repos - Builds Astro docs-site - Pushes image to registry.leeworks.dev/leeworks-agents/docs-site - Triggered on push to main, schedule daily 02:00 UTC, workflow_dispatch
310 lines
8.8 KiB
Markdown
310 lines
8.8 KiB
Markdown
# API Metrics Instrumentation Standard
|
|
|
|
**Version:** 1.0
|
|
**Date:** 2026-05-24
|
|
**Applies to:** All leeworks.dev API services (zip-enrichment, holidays, air-quality)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
Every API service MUST expose Prometheus-compatible metrics at `GET /metrics`. This document defines the required metrics, label conventions, and provides reference middleware implementations for both Fastify (Node.js) and FastAPI (Python).
|
|
|
|
---
|
|
|
|
## Required Metrics
|
|
|
|
### 1. `api_requests_total`
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Type** | Counter |
|
|
| **Description** | Total number of HTTP requests received |
|
|
| **Labels** | `api`, `route`, `method`, `status` |
|
|
|
|
**Label values:**
|
|
- `api`: one of `zip-enrichment`, `holidays`, `air-quality`
|
|
- `route`: the matched route pattern, e.g. `/v1/lookup`, `/v1/holidays`
|
|
- `method`: HTTP method, e.g. `GET`, `POST`
|
|
- `status`: HTTP status code as string, e.g. `200`, `404`, `429`, `403`
|
|
|
|
**Example:**
|
|
```
|
|
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="200"} 1234
|
|
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="429"} 12
|
|
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="403"} 3
|
|
```
|
|
|
|
---
|
|
|
|
### 2. `api_response_duration_seconds`
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Type** | Histogram |
|
|
| **Description** | HTTP response latency in seconds |
|
|
| **Labels** | `api`, `route` |
|
|
| **Buckets** | `0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5` |
|
|
|
|
**Example:**
|
|
```
|
|
api_response_duration_seconds_bucket{api="holidays",route="/v1/holidays",le="0.05"} 800
|
|
api_response_duration_seconds_bucket{api="holidays",route="/v1/holidays",le="0.1"} 990
|
|
api_response_duration_seconds_sum{api="holidays",route="/v1/holidays"} 45.2
|
|
api_response_duration_seconds_count{api="holidays",route="/v1/holidays"} 1000
|
|
```
|
|
|
|
---
|
|
|
|
### 3. `api_data_freshness_seconds`
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Type** | Gauge |
|
|
| **Description** | Seconds since the local dataset was last seeded/refreshed |
|
|
| **Labels** | `api`, `dataset` |
|
|
| **Unit** | Seconds (Unix timestamp diff: `now - last_seed_time`) |
|
|
|
|
**Label values:**
|
|
- `dataset`: a descriptive name for the dataset, e.g. `zip_codes`, `us_holidays`, `aqi_readings`
|
|
|
|
**Example:**
|
|
```
|
|
api_data_freshness_seconds{api="air-quality",dataset="aqi_readings"} 86400
|
|
api_data_freshness_seconds{api="zip-enrichment",dataset="zip_codes"} 2592000
|
|
```
|
|
|
|
A value of `0` means freshly seeded; values growing toward `2592000` (30 days) are expected for monthly re-seed schedules.
|
|
|
|
---
|
|
|
|
## Reference Implementations
|
|
|
|
### Fastify (Node.js/TypeScript)
|
|
|
|
Install dependencies:
|
|
```bash
|
|
npm install prom-client
|
|
```
|
|
|
|
**`src/metrics.ts`:**
|
|
```typescript
|
|
import { Registry, Counter, Histogram, Gauge } from 'prom-client';
|
|
|
|
export const register = new Registry();
|
|
|
|
export const requestsTotal = new Counter({
|
|
name: 'api_requests_total',
|
|
help: 'Total number of HTTP requests received',
|
|
labelNames: ['api', 'route', 'method', 'status'],
|
|
registers: [register],
|
|
});
|
|
|
|
export const responseDuration = new Histogram({
|
|
name: 'api_response_duration_seconds',
|
|
help: 'HTTP response latency in seconds',
|
|
labelNames: ['api', 'route'],
|
|
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
|
|
registers: [register],
|
|
});
|
|
|
|
export const dataFreshness = new Gauge({
|
|
name: 'api_data_freshness_seconds',
|
|
help: 'Seconds since the local dataset was last seeded',
|
|
labelNames: ['api', 'dataset'],
|
|
registers: [register],
|
|
});
|
|
```
|
|
|
|
**`src/metricsMiddleware.ts`:**
|
|
```typescript
|
|
import { FastifyPluginAsync } from 'fastify';
|
|
import { register, requestsTotal, responseDuration } from './metrics';
|
|
|
|
const API_NAME = process.env.API_NAME ?? 'unknown'; // set per service
|
|
|
|
export const metricsPlugin: FastifyPluginAsync = async (fastify) => {
|
|
// Expose /metrics endpoint
|
|
fastify.get('/metrics', async (_req, reply) => {
|
|
reply.header('Content-Type', register.contentType);
|
|
return register.metrics();
|
|
});
|
|
|
|
// Instrument all routes
|
|
fastify.addHook('onRequest', async (request, _reply) => {
|
|
(request as any)._startTime = process.hrtime.bigint();
|
|
});
|
|
|
|
fastify.addHook('onResponse', async (request, reply) => {
|
|
const startTime = (request as any)._startTime as bigint;
|
|
const durationMs = Number(process.hrtime.bigint() - startTime) / 1e6;
|
|
const route = request.routerPath ?? request.url;
|
|
|
|
requestsTotal.labels(API_NAME, route, request.method, String(reply.statusCode)).inc();
|
|
responseDuration.labels(API_NAME, route).observe(durationMs / 1000);
|
|
});
|
|
};
|
|
```
|
|
|
|
**Register in main:**
|
|
```typescript
|
|
import { metricsPlugin } from './metricsMiddleware';
|
|
await fastify.register(metricsPlugin);
|
|
```
|
|
|
|
**Update data freshness gauge (call after each seed):**
|
|
```typescript
|
|
import { dataFreshness } from './metrics';
|
|
// Call this after each DB seed completes:
|
|
dataFreshness.labels('zip-enrichment', 'zip_codes').set(0);
|
|
// Or set it to seconds since last seed on startup:
|
|
dataFreshness.labels('zip-enrichment', 'zip_codes').set(secondsSinceLastSeed);
|
|
```
|
|
|
|
---
|
|
|
|
### FastAPI (Python)
|
|
|
|
Install dependencies:
|
|
```bash
|
|
pip install prometheus-client starlette
|
|
```
|
|
|
|
**`metrics.py`:**
|
|
```python
|
|
from prometheus_client import Counter, Histogram, Gauge, REGISTRY, CollectorRegistry
|
|
|
|
registry = CollectorRegistry()
|
|
|
|
requests_total = Counter(
|
|
'api_requests_total',
|
|
'Total number of HTTP requests received',
|
|
['api', 'route', 'method', 'status'],
|
|
registry=registry,
|
|
)
|
|
|
|
response_duration = Histogram(
|
|
'api_response_duration_seconds',
|
|
'HTTP response latency in seconds',
|
|
['api', 'route'],
|
|
buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
|
|
registry=registry,
|
|
)
|
|
|
|
data_freshness = Gauge(
|
|
'api_data_freshness_seconds',
|
|
'Seconds since the local dataset was last seeded',
|
|
['api', 'dataset'],
|
|
registry=registry,
|
|
)
|
|
```
|
|
|
|
**`metrics_middleware.py`:**
|
|
```python
|
|
import time
|
|
import os
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response
|
|
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
|
|
from metrics import requests_total, response_duration, registry
|
|
|
|
API_NAME = os.getenv("API_NAME", "unknown")
|
|
|
|
|
|
class MetricsMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
start = time.time()
|
|
response = await call_next(request)
|
|
duration = time.time() - start
|
|
|
|
route = request.url.path
|
|
requests_total.labels(
|
|
api=API_NAME,
|
|
route=route,
|
|
method=request.method,
|
|
status=str(response.status_code),
|
|
).inc()
|
|
response_duration.labels(api=API_NAME, route=route).observe(duration)
|
|
|
|
return response
|
|
|
|
|
|
async def metrics_endpoint(request: Request):
|
|
return Response(
|
|
generate_latest(registry),
|
|
media_type=CONTENT_TYPE_LATEST,
|
|
)
|
|
```
|
|
|
|
**Register in FastAPI app:**
|
|
```python
|
|
from fastapi import FastAPI
|
|
from starlette.routing import Route
|
|
from metrics_middleware import MetricsMiddleware, metrics_endpoint
|
|
|
|
app = FastAPI()
|
|
app.add_middleware(MetricsMiddleware)
|
|
app.add_route("/metrics", metrics_endpoint)
|
|
```
|
|
|
|
---
|
|
|
|
## Prometheus Scrape Configuration
|
|
|
|
Add to your Prometheus `scrape_configs` (or ServiceMonitor for kube-prometheus-stack):
|
|
|
|
```yaml
|
|
# prometheus-additional-scrapes.yaml
|
|
- job_name: 'leeworks-apis'
|
|
kubernetes_sd_configs:
|
|
- role: pod
|
|
relabel_configs:
|
|
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
|
|
action: keep
|
|
regex: "true"
|
|
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
|
|
action: replace
|
|
target_label: __metrics_path__
|
|
regex: (.+)
|
|
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
|
|
action: replace
|
|
regex: ([^:]+)(?::\d+)?;(\d+)
|
|
replacement: $1:$2
|
|
target_label: __address__
|
|
```
|
|
|
|
Add annotations to each API pod:
|
|
```yaml
|
|
annotations:
|
|
prometheus.io/scrape: "true"
|
|
prometheus.io/port: "3000" # or 8000 for FastAPI
|
|
prometheus.io/path: "/metrics"
|
|
```
|
|
|
|
---
|
|
|
|
## Grafana Dashboard
|
|
|
|
A reference dashboard JSON is available at `docs/grafana-api-dashboard.json` (TBD — will be committed once Grafana is deployed per issue #7).
|
|
|
|
Key panels to include:
|
|
1. Request rate by API and status (`rate(api_requests_total[5m])`)
|
|
2. P50/P95/P99 latency (`histogram_quantile(0.99, rate(api_response_duration_seconds_bucket[5m]))`)
|
|
3. Error rate = non-2xx / total requests
|
|
4. Data freshness gauge per API
|
|
5. Request volume heatmap
|
|
|
|
---
|
|
|
|
## Compliance Checklist
|
|
|
|
Before marking an API server PR as ready:
|
|
|
|
- [ ] `GET /metrics` returns `text/plain; version=0.0.4; charset=utf-8`
|
|
- [ ] `api_requests_total` increments on every request with correct labels
|
|
- [ ] `api_response_duration_seconds` has observations on every request
|
|
- [ ] `api_data_freshness_seconds` is set on startup and after each seed
|
|
- [ ] Pod annotations for Prometheus scraping are present in the Helm chart values
|
|
- [ ] `API_NAME` env var is set correctly per deployment
|