{
  "markdown": "# BetterDB Monitor\n\n[![Docker Pulls](https://img.shields.io/docker/pulls/betterdb/monitor)](https://hub.docker.com/r/betterdb/monitor)\n[![Docker Image Version](https://img.shields.io/docker/v/betterdb/monitor?sort=semver&label=docker)](https://hub.docker.com/r/betterdb/monitor/tags)\n[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/betterdb-monitor)](https://artifacthub.io/packages/search?repo=betterdb-monitor)\n[![npm](https://img.shields.io/npm/v/%40betterdb%2Fmonitor?label=npm)](https://www.npmjs.com/package/@betterdb/monitor)\n[![npm downloads](https://img.shields.io/npm/dm/%40betterdb%2Fmonitor)](https://www.npmjs.com/package/@betterdb/monitor)\n[![API Tests](https://github.com/betterdb-inc/monitor/actions/workflows/api-tests.yml/badge.svg)](https://github.com/betterdb-inc/monitor/actions/workflows/api-tests.yml)\n[![License](https://img.shields.io/badge/license-MIT%20%2B%20Commercial-blue)](LICENSE)\n[![Valkey](https://img.shields.io/badge/Valkey-8.x%20native-6a5acd)](https://valkey.io)\n[![Redis](https://img.shields.io/badge/Redis-6%2B%20compatible-d82c20)](https://redis.io)\n\n**The monitoring layer that Valkey deserves.**\n\nBetterDB persists what Valkey throws away - slowlogs, command patterns, client activity, anomaly signals - so you can debug what happened at 3am, not just what's happening now. Built for Valkey 8.x with native support for COMMANDLOG, CLUSTER SLOT-STATS, and per-thread I/O metrics. Redis 6+ compatible for everything else.\n\n[Website](https://betterdb.com) | [Docker Hub](https://hub.docker.com/r/betterdb/monitor) | [npm](https://www.npmjs.com/package/@betterdb/monitor) | [Documentation](https://docs.betterdb.com) | [Blog](https://betterdb.com/blog)\n\nBetterDB is built by [BetterDB Inc.](https://betterdb.com), a public benefit company operating under the [OCV Open Charter](https://github.com/OpenCoreVentures/ocv-public-benefit-company).\n\n![BetterDB Monitor - Key Analytics with per-type key size distribution histograms](docs/assets/readme-hero.png)\n\n## Quick Start (Docker)\n\n```bash\ndocker run -d --name betterdb -p 3001:3001 betterdb/monitor:latest\n```\n\nPoint your browser to `http://localhost:3001`. To monitor a specific instance:\n\n```bash\ndocker run -d \\\n  --name betterdb \\\n  -p 3001:3001 \\\n  -e DB_HOST=your-valkey-host \\\n  -e DB_PORT=6379 \\\n  -e DB_PASSWORD=your-password \\\n  betterdb/monitor:latest\n```\n\n> **Connecting to a database on your host machine?** Inside the container\n> `localhost` is the container itself, not your host — so use\n> `host.docker.internal` as the database host. On **Docker Desktop\n> (macOS/Windows)** it works out of the box; on **Linux** add\n> `--add-host=host.docker.internal:host-gateway` to the `docker run` command so\n> the name resolves. The dashboard's one-click \"connect to local instance\"\n> button auto-detects this and pre-fills the right host for you.\n\nTwo image variants are published, both multi-arch (`linux/amd64`, `linux/arm64`):\n\n| Tag | What it is |\n|-----|------------|\n| `latest`, `X.Y.Z-no-ai` | Default image - every monitoring feature included, without the dependencies for the experimental local-LLM AI Helper |\n| `X.Y.Z` | Adds the experimental AI Helper (bring your own Ollama; disabled by default via `AI_ENABLED`) |\n\nSee [Docker Production Deployment](#docker-production-deployment) for persistent storage, custom ports, licensing, and air-gapped setups.\n\n## Quick Start (Kubernetes / Helm)\n\n```bash\nhelm repo add betterdb https://docs.betterdb.com/charts\nhelm repo update\nhelm install betterdb-monitor betterdb/betterdb-monitor \\\n  --namespace betterdb --create-namespace \\\n  --set db.host=my-valkey.default.svc.cluster.local \\\n  --set db.password=yourpassword\n```\n\nThen `kubectl port-forward -n betterdb svc/betterdb-monitor 3001:3001` and open `http://localhost:3001`, or enable the chart's ingress. PostgreSQL-backed history, bring-your-own Secrets, and air-gapped licensing are all covered in the [Kubernetes guide](https://docs.betterdb.com/kubernetes) and the [chart README](charts/betterdb-monitor/README.md).\n\n## Quick Start (CLI)\n\nRun BetterDB Monitor without Docker:\n\n```bash\nnpx @betterdb/monitor\n```\n\nOn first run, an interactive setup wizard guides you through database connection, storage backend (SQLite, PostgreSQL, or in-memory), and server settings. Configuration is saved to `~/.betterdb/config.json`.\n\n```bash\nnpm install -g @betterdb/monitor   # global install\nbetterdb --setup                   # re-run setup wizard\nbetterdb --port 8080               # override server port\nbetterdb --db-host 1.2.3.4         # override database host\nbetterdb --help                    # all options\n```\n\nRequires Node.js >= 20.0.0 and a Valkey or Redis instance to monitor. For SQLite storage, also `npm install -g better-sqlite3`.\n\n## What You Get\n\n### See everything, keep everything\n\n- **Historical analytics** - query slowlogs, command patterns, client activity, and latency across any time range. The data that used to disappear after a log rotation.\n- **COMMANDLOG support** - Valkey 8.1+ exclusive. Large requests and large replies, not just the slow ones.\n- **MONITOR capture sessions** - record real traffic on demand: live tail, filter, replay, export to JSON/CSV, and cross-reference against connection history.\n- **Hot key tracking** - top keys by access frequency with rank movement over time. Key Analytics (Pro, free in early access) adds type, TTL, and size distributions from live sampling.\n- **Cluster visibility** - topology graphs, SLOT-STATS heatmaps, per-slot CPU and key distribution.\n- **CPU & I/O thread metrics** - per-thread visibility that no Redis tool can provide.\n- **Client analytics** - see exactly which service is responsible for what, attributed by client name and pattern.\n- **ACL audit trail** - track who accessed what, persisted for compliance and post-incident debugging.\n\n### Understand and act\n\n- **Anomaly detection** (Pro, free in early access) - automatic baseline learning with correlated events and plain-English diagnoses. 20+ detectors, no manual thresholds.\n- **Capacity forecasting** - projected time-to-ceiling for memory, ops/sec, CPU, and fragmentation.\n- **Webhooks** - HMAC-signed alert deliveries with retries and a full delivery log.\n- **Live migration** - move between Redis and Valkey with a three-phase analysis, execution, and validation workflow.\n\n### Built for the AI era\n\n- **Vector search observability** - FT.SEARCH ops/sec and latency with per-index health for [valkey-search](https://github.com/valkey-io/valkey-search) and RediSearch. See [docs/vector-ai](docs/vector-ai/README.md).\n- **Inference latency** - p50/p95/p99 per index, with SLA breach alerts (Pro, free in early access).\n- **Semantic cache intelligence** (Pro, free in early access) - hit-rate health, similarity-threshold recommendations, and an approve/reject proposal workflow. Agent memory observability included.\n- **AI traces** - OTLP span waterfalls from your AI application, correlated with the live Valkey state underneath each request. See [docs/opentelemetry.md](docs/opentelemetry.md).\n\n### Plugs into everything\n\n- **MCP server** - 60 tools for Claude Code, Cursor, or any MCP client via [`@betterdb/mcp`](packages/mcp).\n- **Prometheus endpoint** - 100+ `betterdb_*` metrics. See [docs/prometheus-metrics.md](docs/prometheus-metrics.md).\n- **OpenTelemetry** - ingest OTLP traces, and mirror metrics and events to any OTLP backend. See [docs/opentelemetry.md](docs/opentelemetry.md).\n- **REST API** - everything in the UI is an API call, documented via OpenAPI.\n\n## Access Your Data Your Way\n\n| Interface | Details |\n|-----------|---------|\n| Web UI | `http://localhost:3001` |\n| MCP server | `npx @betterdb/mcp` (stdio) - create a token under Settings → MCP Tokens |\n| Prometheus | `http://localhost:3001/api/prometheus/metrics` |\n| REST API (OpenAPI) | `http://localhost:3001/docs` |\n| Health check | `http://localhost:3001/api/health` |\n\n> **Note**: In production builds (Docker, CLI) API routes are served under the `/api` prefix. In local development (`pnpm dev`) there is no prefix - e.g. `http://localhost:3001/health`.\n\n## Supported Databases\n\n| Database | Minimum Version | Supported Features |\n|----------|----------------|-------------------|\n| **Valkey** | 8.0+ | All features including COMMANDLOG (8.1+) and CLUSTER SLOT-STATS |\n| **Redis** | 6+ | All features except the Valkey-exclusive COMMANDLOG and CLUSTER SLOT-STATS |\n\nThe backend uses a unified adapter over the wire-compatible `iovalkey` client and auto-detects Valkey vs Redis from the `INFO` response (`DB_TYPE=auto`). Capabilities like COMMANDLOG and SLOT-STATS are detected per version, and the UI gracefully degrades when a feature isn't available.\n\nManaged services are supported too - guides for AWS ElastiCache, MemoryDB, Redis Cloud, and Upstash live in [docs/providers](docs/providers/), and [`@betterdb/agent`](packages/agent) reaches VPC-only instances over an outbound WebSocket.\n\n## Docker Production Deployment\n\nThe Docker image contains the monitoring application (backend + frontend). It requires:\n1. A Valkey/Redis instance to monitor\n2. A PostgreSQL instance for data persistence (or use memory storage)\n\n### Run with PostgreSQL Storage\n\n```bash\ndocker run -d \\\n  --name betterdb-monitor \\\n  -p 3001:3001 \\\n  -e DB_HOST=your-valkey-host \\\n  -e DB_PORT=6379 \\\n  -e DB_PASSWORD=your-password \\\n  -e STORAGE_TYPE=postgres \\\n  -e STORAGE_URL=postgresql://user:pass@postgres-host:5432/dbname \\\n  betterdb/monitor\n```\n\n### Run on Custom Port\n\nSet the `PORT` environment variable and match the `-p` mapping:\n\n```bash\ndocker run -d \\\n  --name betterdb-monitor \\\n  -p 8080:8080 \\\n  -e PORT=8080 \\\n  -e DB_HOST=your-valkey-host \\\n  betterdb/monitor\n```\n\n### Run with Host Network (Access localhost services)\n\nIf your Valkey and PostgreSQL are running on the same host:\n\n```bash\ndocker run -d \\\n  --name betterdb-monitor \\\n  --network host \\\n  -e DB_HOST=localhost \\\n  -e DB_PORT=6380 \\\n  -e DB_PASSWORD=devpassword \\\n  -e STORAGE_TYPE=postgres \\\n  -e STORAGE_URL=postgresql://dev:devpass@localhost:5432/postgres \\\n  betterdb/monitor\n```\n\n### Environment Variables\n\n| Variable | Required | Default | Description |\n|----------|----------|---------|-------------|\n| `DB_HOST` | Yes | `localhost` | Valkey/Redis host to monitor |\n| `DB_PORT` | No | `6379` | Valkey/Redis port |\n| `DB_PASSWORD` | No | - | Valkey/Redis password |\n| `DB_USERNAME` | No | `default` | Valkey/Redis ACL username |\n| `DB_TYPE` | No | `auto` | Database type: `auto`, `valkey`, or `redis` |\n| `STORAGE_TYPE` | No | `memory` | Storage backend: `memory` or `postgres` |\n| `STORAGE_URL` | Conditional | - | PostgreSQL connection URL (required if `STORAGE_TYPE=postgres`) |\n| `PORT` | No | `3001` | Application HTTP port |\n| `NODE_ENV` | No | `production` | Node environment |\n| `ANOMALY_DETECTION_ENABLED` | No | `true` | Enable anomaly detection |\n| `ANOMALY_PROMETHEUS_INTERVAL_MS` | No | `30000` | Prometheus summary update interval (ms) |\n| `BETTERDB_LICENSE_KEY` | No | - | Online license key (Pro/Enterprise), validated over the network |\n| `BETTERDB_OFFLINE_LICENSE_FILE` | No | - | Path to a signed offline license `.jwt` for **air-gapped** hosts (see below) |\n| `BETTERDB_OFFLINE_LICENSE` | No | - | Offline license token as an inline JWT string |\n| `BETTERDB_DATA_DIR` | No | `/app/data` | Directory for persisted license state (mount a writable volume) |\n| `ENCRYPTION_KEY` | No | - | Key (min 16 chars) used to envelope-encrypt stored connection passwords and SSH tunnel secrets at rest. Without it, secrets are stored in plaintext |\n| `BETTERDB_SSH_KEY_DIR` | No | - | Directory that server-side SSH private keys must live in. Enables the \"server file path\" key source for [SSH tunnels](#ssh-tunnels); a connection's key path must resolve inside it. Unset disables file-based keys (inline pasted keys still work) |\n| `BETTERDB_TELEMETRY` | No | `true` | Set `false` to disable anonymous telemetry |\n\nFull reference, including AI, webhook tuning, and health-gate thresholds: [docs/configuration.md](docs/configuration.md). For OTLP trace ingest and metrics/event export, see [docs/opentelemetry.md](docs/opentelemetry.md).\n\n### SSH Tunnels\n\nConnections can reach a database through an SSH bastion/jump host instead of connecting directly — useful for Valkey/Redis in a private subnet, ElastiCache, or MemoryDB. Enable **Connect via SSH tunnel** when adding a connection and provide the SSH host, port, and username. A single hop is supported.\n\nAuthentication is either a password or a private key. Private keys come from one of two sources:\n\n- **Paste key** (inline): the PEM key content is submitted with the connection. It is stored encrypted at rest **only when `ENCRYPTION_KEY` is set** (envelope encryption); without that key it is stored in plaintext, like connection passwords. Works everywhere, including managed/cloud deployments.\n- **Server file path**: the key already lives on the monitor server's filesystem and is referenced by path. This requires setting the `BETTERDB_SSH_KEY_DIR` environment variable to the directory holding the allowed keys, and the referenced path must resolve inside it, so the API can never be coerced into reading arbitrary files. Leave `BETTERDB_SSH_KEY_DIR` unset to disable this option.\n\nOptionally pin the SSH server's **host key fingerprint** (`SHA256:...`) on the connection; when set, the tunnel is refused unless the server presents a matching key, preventing man-in-the-middle attacks on the bastion path. Left blank, the server identity is not verified (a warning is logged).\n\nThe tunnel forwards to the database over `127.0.0.1`; when TLS is enabled the certificate is still validated against the real database hostname. Set `ENCRYPTION_KEY` so SSH passwords, key passphrases, and inline keys are encrypted at rest.\n\n**Known limitation — cluster/Sentinel topologies:** only the connection you configure is tunnelled. Cluster and Sentinel monitoring fan out to the other nodes using the addresses those nodes advertise (`CLUSTER NODES` / Sentinel), and those per-node connections are made directly, not through the tunnel. If the other nodes are only reachable via the bastion (e.g. ElastiCache/MemoryDB in a private subnet), per-node views will be unavailable. Use SSH tunnels for single-node/primary monitoring, or place the monitor where it can reach the cluster nodes directly.\n\n### Licensing & Air-Gapped Support\n\nBetterDB Monitor unlocks Pro/Enterprise features in one of two ways, depending on\nwhether the host has internet access:\n\n- **Online license key** - set `BETTERDB_LICENSE_KEY`. The monitor validates it\n  against `betterdb.com` and caches a locally-verified **signed token**, so your\n  tier keeps working through short outages and restarts.\n- **Offline / air-gapped license token** - for hosts with **no internet access at\n  all** (see below).\n\n#### How air-gapped licensing works\n\nEvery entitlement is a **signed RS256 JWT**. The monitor verifies it **locally**\nagainst public keys embedded in the image - it never has to reach a license server\nto trust a token. So an air-gapped host can run paid tiers with zero connectivity:\n\n1. On an internet-connected machine, sign in at\n   [betterdb.com/account/licenses](https://www.betterdb.com/account/licenses) and\n   **download your offline license token** (`.jwt`, Pro/Enterprise). It contains no\n   secrets and can't be tampered with - any edit breaks the signature.\n2. Transfer it to the air-gapped host however you like (USB, config management, a\n   Docker/Kubernetes secret mount).\n3. Provide it via `BETTERDB_OFFLINE_LICENSE_FILE` (path), `BETTERDB_OFFLINE_LICENSE`\n   (inline string), or paste it in the UI under **Settings → License → \"Air-gapped\n   environment? Activate an offline license.\"**\n\nWhen an offline token is configured and **no** `BETTERDB_LICENSE_KEY` is set, the\nmonitor makes **zero outbound requests** - license checks, telemetry, and update\npings are all disabled. It runs the granted tier until the token expires (perpetual\nlicenses re-download yearly), then reverts to Community.\n\n```bash\n# fully offline - no network required\ndocker volume create betterdb-data\ndocker run --rm -v betterdb-data:/d alpine chown 1001:1001 /d   # volume writable by UID 1001 (one-time)\n\ndocker run -d --name betterdb-monitor -p 3001:3001 \\\n  -e DB_HOST=your-valkey-host -e DB_PORT=6379 -e DB_PASSWORD=your-password \\\n  -v /path/to/betterdb-license.jwt:/run/secrets/betterdb-license.jwt:ro \\\n  -e BETTERDB_OFFLINE_LICENSE_FILE=/run/secrets/betterdb-license.jwt \\\n  -v betterdb-data:/app/data \\\n  betterdb/monitor\n```\n\nVerify with `GET /api/license/status` → `source: offline-token`, `mode: offline`,\n`airGapped: true`.\n\n> **Persistence:** mount a writable volume at `/app/data` so the offline license and\n> the online outage-grace token survive restarts. The container runs as **UID 1001**,\n> so a freshly-created volume must be `chown`ed to it (shown above) - otherwise\n> persistence fails with `EACCES … license.jwt`.\n\nFor the full flow, verification precedence, and key-rotation runbook see\n**[Offline & Air-Gapped Licenses](docs/offline-licenses.md)** and the\n**[Configuration reference](docs/configuration.md#license-configuration)**.\n\n### Docker Image Details\n\n- **Base Image**: `node:20-alpine`\n- **Compressed size**: ~360MB (`latest` / `-no-ai`) / ~640MB (versioned image with the experimental AI Helper's local-LLM dependencies)\n- **Platforms**: `linux/amd64`, `linux/arm64`\n- **Contains**: Backend API + Frontend static files (served by Fastify)\n- **Excluded**: SQLite support (use PostgreSQL or Memory storage)\n\n### Container Operations\n\n```bash\ndocker logs -f betterdb-monitor        # follow logs\ndocker stop betterdb-monitor           # stop\ndocker rm betterdb-monitor             # remove\n```\n\n## Storage Backends\n\nBetterDB Monitor persists audit trail, analytics, captures, and anomaly data to one of four backends:\n\n| Backend | Use case | Notes |\n|---------|----------|-------|\n| `memory` | Testing, ephemeral environments | Default in Docker; all data lost on restart |\n| `postgres` | Production | `STORAGE_TYPE=postgres` + `STORAGE_URL=postgresql://user:pass@host:port/db` |\n| `turso` | Production / serverless SQLite | `STORAGE_TYPE=turso` + `STORAGE_URL=libsql://...` + `STORAGE_AUTH_TOKEN`; works in Docker |\n| `sqlite` | Local development / CLI | Native module stripped from the `latest` Docker image; `STORAGE_SQLITE_FILEPATH` optional |\n\n## Prometheus Metrics\n\nMetrics are exposed at `GET /api/prometheus/metrics` in Prometheus text format: ACL audit, client connections, slowlog/commandlog patterns, memory, throughput, keyspace, replication, cluster slot stats, and Node.js runtime metrics - all prefixed `betterdb_`.\n\n```yaml\nscrape_configs:\n  - job_name: 'betterdb-monitor'\n    metrics_path: '/api/prometheus/metrics'\n    static_configs:\n      - targets: ['your-monitor-host:3001']\n```\n\nFull metric reference: [docs/prometheus-metrics.md](docs/prometheus-metrics.md) and [docs/prometheus-integration.md](docs/prometheus-integration.md).\n\n## Development\n\n### Project Structure\n\n```\nbetterdb-monitor/\n├── apps/\n│   ├── api/                 # NestJS backend (Fastify)\n│   └── web/                 # React frontend (Vite)\n├── packages/                # Published packages (see below)\n├── docs/                    # Documentation site (Jekyll)\n├── docker-compose.yml       # Local Valkey (port 6380) and Redis (port 6382) for testing\n└── package.json             # Workspace root\n```\n\n### Packages\n\nThis monorepo ships several standalone packages. See [`packages/`](packages/) for the full list.\n\n| Package | Language | Registry |\n|---|---|---|\n| [`@betterdb/monitor`](packages/cli) | TypeScript | [npm](https://www.npmjs.com/package/@betterdb/monitor) |\n| [`@betterdb/mcp`](packages/mcp) | TypeScript | [npm](https://www.npmjs.com/package/@betterdb/mcp) |\n| [`@betterdb/agent`](packages/agent) | TypeScript | [npm](https://www.npmjs.com/package/@betterdb/agent) |\n| [`@betterdb/semantic-cache`](packages/semantic-cache) | TypeScript | [npm](https://www.npmjs.com/package/@betterdb/semantic-cache) |\n| [`betterdb-semantic-cache`](packages/semantic-cache-py) | Python | [PyPI](https://pypi.org/project/betterdb-semantic-cache/) |\n| [`@betterdb/agent-cache`](packages/agent-cache) | TypeScript | [npm](https://www.npmjs.com/package/@betterdb/agent-cache) |\n| [`betterdb-agent-cache`](packages/agent-cache-py) | Python | [PyPI](https://pypi.org/project/betterdb-agent-cache/) |\n| [`cache-benchmark`](packages/cache-benchmark) | Python | Replay harness for benchmarking semantic caches |\n\n### Tech Stack\n\n- **Backend**: NestJS with Fastify adapter, `iovalkey` for Valkey/Redis connections, TypeScript strict mode. Port **3001**.\n- **Frontend**: React + TypeScript, Vite, TailwindCSS, Recharts. Dev server on port **5173**.\n- **Monorepo**: pnpm workspaces + Turborepo.\n\n### Local Setup\n\nPrerequisites: Node.js >= 20.0.0, pnpm >= 9.0.0, Docker.\n\n```bash\npnpm install\ncp .env.example .env\npnpm docker:dev        # local Valkey (6380) and Redis (6382)\npnpm dev               # web on :5173, api on :3001\n```\n\nTo connect to Redis instead of Valkey, set `DB_PORT=6382` in `.env`.\n\n```bash\npnpm dev:api           # API only\npnpm dev:web           # frontend only\npnpm docker:dev:down   # stop local databases\npnpm build             # production build\npnpm test              # API tests\n```\n\nDocker image builds:\n\n```bash\npnpm docker:build      # local build\npnpm docker:publish    # multi-arch build & push (requires buildx)\n```\n\n### Adding New Features\n\n1. Add new endpoints in `apps/api/src/`\n2. Add corresponding API calls in `apps/web/src/api/`\n3. Add shared types in `packages/shared/src/types/`\n\n### Code Style\n\n- TypeScript strict mode, explicit return types, no `any`\n- ESLint + Prettier configured\n\n## License\n\n- Content under `docs/` is licensed under CC BY-SA 4.0.\n- Content under `proprietary/` is covered by a commercial license (see `proprietary/LICENSE`). These features are free during early access.\n- Everything else is [MIT](LICENSE).\n",
  "bytes": 22055,
  "sha": "774b0e5ea3e8af4b5308891fb15a9c4bddf57010b91f3cf339b8ff4231fe6830",
  "repo_slug": "betterdb-inc/monitor",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_betterdb_inc_monitor_c73b8746/readme"
}