Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .env.example.observability
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# OpenTelemetry env vars (PR-001 default)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The file .env.example.observability is placed in the project root. According to the Repository Style Guide (Section 1: File Placement & Organization), the project root must only contain specific allowed files, and .env.example.observability is not on that list. Please merge these environment variables into the existing .env.example file in the root to keep the root clean and adhere to the organization rules.

References
  1. The Project Root must only contain specific allowed configuration, dependency, documentation, and CI/CD files. Loose environment files other than .env.example are not permitted. (link)

# Leave OTEL_EXPORTER_OTLP_ENDPOINT unset to disable span export (default).
# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
# OTEL_EXPORTER_OTLP_PROTOCOL=http/json
# OTEL_EXPORTER_OTLP_HEADERS=x-api-key=secret,x-tenant=42
# OTEL_TRACES_SAMPLER_ARG=1
# OTEL_BSP_MAX_QUEUE_SIZE=2048
# OTEL_BSP_SCHEDULE_DELAY=5000
# OTEL_SERVICE_NAME=omniroute
# OTEL_RESOURCE_ATTRIBUTES=deployment.region=us-east-1,deployment.environment=production
# OTEL_SDK_DISABLED=false

# LiveWS LAN allow-list (PR #4877 — already merged)
# Comma-separated host list. Loopback is always allowed.
# LIVE_WS_ALLOWED_HOSTS=100.96.135.160,192.168.1.20

# Authz LAN recognition (PR #4876 — closed by design)
# Tailscale CGNAT (100.64.0.0/10) is private-LAN by default.
# TAILSCALE_CIDR=disabled # opt out
# Comma-separated extra IPv4 CIDRs for LAN overlays (ZeroTier, Nebula, ...).
# PRIVATE_LAN_CIDR=10.241.0.0/16,100.64.0.0/10

# Structured logger
OMNIROUTE_LOG_LEVEL=info
# OMNIROUTE_LOG_FORMAT=pretty

# Misc
DEPLOYMENT_ENVIRONMENT=development
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts"

test-vitest:
name: Vitest (MCP / autoCombo / UI components)
Expand Down Expand Up @@ -661,7 +661,7 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts"

node-26-compat-build:
name: Node 26 Compatibility Build
Expand Down Expand Up @@ -714,7 +714,7 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts"

test-coverage-shard:
name: Coverage Shard (${{ matrix.shard }}/8)
Expand Down Expand Up @@ -755,7 +755,7 @@ jobs:
--exclude=tests/** \
--exclude=**/*.test.* \
node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts"
- name: Upload raw shard coverage
if: always()
uses: actions/upload-artifact@v7
Expand Down
100 changes: 100 additions & 0 deletions docs/observability/01-quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# OmniRoute Observability — Quickstart

This document describes the operator-facing observability stack that ships
with OmniRoute v3.8.34. Three subsystems:

1. **OpenTelemetry traces** (`src/lib/observability/otel.ts`) — OTLP/HTTP
exporter, per-request span emission, idempotent bootstrap.
2. **Prometheus `/metrics`** (`src/lib/observability/metrics.ts`) —
RED-method HTTP counter/histogram, provider-attempt counters, quota
gauges, cache hit/miss counters. Cardinality-capped label allow-list.
3. **Structured logger** (`src/lib/observability/logger.ts`) — pino-style
NDJSON with `AsyncLocalStorage` request-scoped context, trace/span
correlation, optional pretty format for dev.

All three are **off by default** (zero-dep, zero-cost). Operators opt in
via env vars (see below) — when disabled, every function returns a
no-op stub in O(1) with no allocations on the hot path.

## OpenTelemetry

| Variable | Default | Purpose |
|----------|---------|---------|
| `OTEL_SDK_DISABLED` | `false` | Set to `1` / `true` / `yes` / `on` to force-disable. |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | _unset_ | Collector URL, e.g. `http://otel-collector:4318`. |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | _unset_ | Override traces-only endpoint. |
| `OTEL_EXPORTER_OTLP_HEADERS` | _unset_ | Comma-separated `k=v,k=v` headers (auth tokens). |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/json` | We implement HTTP/JSON; `grpc` falls back with a warning. |
| `OTEL_TRACES_SAMPLER_ARG` | `1` | Sample ratio `[0,1]`. |
| `OTEL_BSP_MAX_QUEUE_SIZE` | `2048` | Spans buffered between flushes. |
| `OTEL_BSP_SCHEDULE_DELAY` | `5000` | Flush interval (ms). |
| `OTEL_SERVICE_NAME` / `OMNIROUTE_SERVICE_NAME` | `omniroute` | `service.name` resource attribute. |
| `OTEL_RESOURCE_ATTRIBUTES` | _unset_ | Comma-separated `k=v,k=v` extras (e.g. `deployment.region=us-east-1`). |
| `DEPLOYMENT_ENVIRONMENT` | auto-detect | `deployment.environment` resource attribute. |

Bootstrap once at startup (already wired in `src/instrumentation-node.ts`):

```ts
import { initTelemetry, shutdownTelemetry } from "@/lib/observability";
await initTelemetry();

process.on("SIGTERM", () => {
void shutdownTelemetry().finally(() => process.exit(0));
});
```

Spans:

```ts
import { getTracer } from "@/lib/observability";

const tracer = getTracer("my-component");
await tracer.withSpan("upstream.call", async (span) => {
span.setAttribute("provider", "openai");
span.setAttribute("model", "gpt-4o");
// throws? → status=ERROR + exception event auto-recorded
});
```

## Prometheus /metrics

Endpoint: `GET /api/system/metrics` (added in PR-001).

| Metric | Type | Labels |
|--------|------|--------|
| `omniroute_http_requests_total` | counter | `route`, `method`, `status` |
| `omniroute_http_request_duration_seconds` | histogram | `route`, `method`, `status` |
| `omniroute_provider_upstream_attempts_total` | counter | `provider`, `model`, `outcome` |
| `omniroute_provider_upstream_duration_seconds` | histogram | `provider`, `model`, `outcome` |
| `omniroute_quota_remaining` | gauge | `provider`, `model` |
| `omniroute_quota_limit` | gauge | `provider`, `model` |
| `omniroute_cache_hits_total` | counter | `layer` |
| `omniroute_cache_misses_total` | counter | `layer` |

Cardinality cap: each family has a fixed label allow-list
(`src/lib/observability/metrics.ts :: LABEL_ALLOWLIST`). Unrecognised
labels are dropped at write time with a `console.warn` so operators see
the mismatch.

## Structured logger

| Variable | Default | Purpose |
|----------|---------|---------|
| `OMNIROUTE_LOG_LEVEL` | `info` | `trace` / `debug` / `info` / `warn` / `error` / `fatal`. |
| `OMNIROUTE_LOG_FORMAT` | `json` | `json` (NDJSON) or `pretty` (human-readable). |

Usage:

```ts
import { log, withLogContext } from "@/lib/observability/logger";

withLogContext({ tenantId: "t-1", requestId: req.id }, () => {
log.info("combo_created", { comboId, tierCount });
log.error("upstream_failed", err, { provider, model });
});
```

When an OTel span is active in the current async scope, every log
record automatically carries `traceId` and `spanId`. Logs at `warn` /
`error` / `fatal` go to **stderr**; everything else to **stdout** (so
container log shippers can split severity streams).
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@
"electron:build:mac": "npm run build && cd electron && npm run build:mac",
"electron:build:linux": "npm run build && cd electron && npm run build:linux",
"electron:smoke:packaged": "node scripts/dev/smoke-electron-packaged.mjs",
"test": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=20 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit:shard": "concurrently --kill-others-on-fail -n s1,s2 \"npm:test:unit:shard:1\" \"npm:test:unit:shard:2\"",
"test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:plan3": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/plan3-p0.test.ts",
"test:fixes": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/fixes-p1.test.ts",
"test:security": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/security-fase01.test.ts",
Expand Down Expand Up @@ -178,7 +178,7 @@
"test:mutation": "stryker run",
"test:ecosystem": "node scripts/dev/run-ecosystem-tests.mjs",
"test:system": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 tests/e2e/system-failover.test.ts",
"test:coverage": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:coverage": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts\"",
"test:coverage:legacy": "c8 --output-dir coverage --exclude=open-sse --check-coverage --lines 50 --functions 50 --branches 50 node --import tsx --test tests/unit/*.test.ts",
"coverage:report": "cross-env NODE_OPTIONS=--max-old-space-size=8192 c8 report --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov",
"coverage:summary": "node scripts/check/test-report-summary.mjs --input coverage/coverage-summary.json --output coverage/coverage-report.md",
Expand Down
2 changes: 1 addition & 1 deletion scripts/check/check-test-discovery.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const COLLECTORS = [
// "vitest" e explodem no node runner). Subdir novo: adicione aqui E nos scripts
// (o drift-check + o gate de órfãos forçam a manutenção em sincronia).
{
glob: "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts",
glob: "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,observability,runtime,security,services,settings,shared,ui}/**/*.test.ts",
sources: ["package.json", ".github/workflows/ci.yml"],
},
// Node native runner — test:integration (top-level only; tests/integration/services/ NÃO roda)
Expand Down
Loading
Loading