Overview
Frugal is a single Go binary that is both an observability server (a live dashboard fed by CloudWatch, EKS, and free native collectors) and a metrics/logs agent (pushes host data from EC2/EKS nodes to a server). It ships as one binary with no external database and no Prometheus — recent data lives in in-memory ring buffers, optionally persisted to SQLite.
The point of the name: frugal prefers free, native endpoints (Redis INFO, OpenSearch stats, RabbitMQ management API, RDS Performance Insights, EKS metrics-server, a host agent) and uses paid CloudWatch only where there's no free alternative — with live cost visibility so you always know what a chart costs.
CloudWatch API ─────┐
Native endpoints ───┤
RDS Perf Insights ──┼─▶ collectors ─▶ ring buffers ─▶ HTTP + SSE ─▶ live dashboard
EKS APIs ───────────┤ (Go) (+ optional SQLite) (embedded HTML)
Agent push (/proc) ─┘
Two parts, one binary
In server mode the process runs as two parts: a web server that comes up immediately from bootstrap config (listen, data_dir, secret_key, auth) and always serves the login + dashboard; and a supervised data-collection service started from the runtime config (AWS/EKS/native targets + credentials). Changing settings tears down and relaunches the collectors without restarting the web server.
Quick start
Run the prebuilt multi-arch image from GHCR — no build, no Go toolchain:
# Docker
docker run -d --name frugal -p 8080:8080 \
-e FRUGAL_SECRET_KEY=$(openssl rand -hex 32) \
-e FRUGAL_DATA_DIR=/data \
-v frugal-data:/data \
ghcr.io/tools-plus/frugal:latest
# …or Docker Compose (docker-compose.prod.yml uses the same image)
FRUGAL_SECRET_KEY=$(openssl rand -hex 32) \
docker compose -f docker-compose.prod.yml up -d
Then open http://localhost:8080, log in as admin / admin, set a new password, and configure what to collect under Admin ▸ Settings.
FRUGAL_SECRET_KEY stable. It encrypts stored credentials; a changed key can't decrypt existing secrets. Images are published for amd64 + arm64 — pin a release with :vX.Y.Z instead of :latest.Install
Docker (recommended)
docker pull ghcr.io/tools-plus/frugal:latest # or :vX.Y.Z
docker run --rm ghcr.io/tools-plus/frugal version # confirm the build
Prebuilt binary
Linux amd64 & arm64 binaries are attached to every release (with sha256):
curl -sSL -o frugal \
https://github.com/tools-plus/frugal/releases/latest/download/frugal-linux-amd64
chmod +x frugal
FRUGAL_SECRET_KEY=$(openssl rand -hex 32) FRUGAL_DATA_DIR=./data ./frugal
From source
Requires Go ≥ 1.24. The server needs CGO_ENABLED=1 for the SQLite driver (the default).
git clone https://github.com/tools-plus/frugal && cd frugal
go build -o frugal ./cmd/frugal
./frugal -config server.json
Run the server in EKS
deploy/k8s.yaml already points at ghcr.io/tools-plus/frugal:latest. Edit it to set the IRSA role ARN (ServiceAccount annotation) and, for persistence, a volume for data_dir. Then:
kubectl apply -f deploy/k8s.yaml
kubectl -n frugal port-forward svc/frugal 8080:80
- IRSA gives the pod read-only AWS access with no long-lived keys — leave AWS keys blank in Settings and the default credential chain is used.
- The Service is
ClusterIPon purpose. There is a built-in login, but keep the dashboard behind port-forward / VPN / an authenticating ingress anyway. - Mount a volume at
data_dirso the control DB and metric history survive restarts. - Requires metrics-server in the cluster for pod/node metrics (if
kubectl top podsworks, you're set).
Agents (EC2 / EKS)
Agents push host CPU/memory/network/load (from /proc) and tail log globs to the server, using the shared ingest token set in Admin ▸ Settings. Agents make zero AWS API calls — collecting host metrics this way is free.
- EC2 / VM:
deploy/frugal-agent.service(systemd unit). - EKS:
deploy/agent-daemonset.yaml(one agent per node; withkube_logs: trueit ships each container's logs from/var/log/containers, so pod logs work even when the server runs outside the cluster).
# minimal agent config, via env
FRUGAL_SERVER_URL=http://frugal.internal:8080 \
FRUGAL_TOKEN=<shared-ingest-token> \
./frugal agent
Configuration
Frugal splits config into two layers:
Bootstrap (server.json / env)
The few things needed before the server can start. These live in server.json, and every key can be overridden by an environment variable (the env wins).
| server.json key | env override | meaning |
|---|---|---|
listen | FRUGAL_LISTEN | bind address (default :8080) |
data_dir | FRUGAL_DATA_DIR | directory for the SQLite databases (enables persistence) |
secret_key | FRUGAL_SECRET_KEY | encrypts stored credentials (AES-256-GCM) |
auth.enabled | FRUGAL_AUTH_ENABLED | require login (default true) |
auth.db_path | FRUGAL_AUTH_DB_PATH | control-DB path (default <data_dir>/auth.db) |
| — | AWS_REGION / AWS_PROFILE | AWS region / shared-config profile |
{
"listen": ":8080",
"data_dir": "./data",
"secret_key": "CHANGE_ME",
"auth": { "enabled": true }
}
Runtime (Admin ▸ Settings)
Everything about what to monitor and how — AWS region/credentials/namespaces/poll intervals, Kubernetes, native targets, retention, the ingest token — is edited in the dashboard, stored encrypted in the control DB, and hot-applied (collectors restart; the web server keeps serving). On first boot with no stored config, frugal seeds the runtime config from any aws/kubernetes/native blocks present in server.json.
secret_key: without it the server still runs and the login works, but credentials can't be stored or used until you set one. Keep it out of source control; prefer the env var or a secret manager in production.IAM permissions
Frugal only ever reads. Attach a policy with these read/list/describe actions to the IAM user whose keys you enter in Settings, or to the IRSA / instance role (see deploy/iam-policy.json):
cloudwatch:ListMetrics, cloudwatch:GetMetricData
ec2:DescribeInstances
rds:DescribeDBInstances, rds:DescribeDBClusters, pi:GetResourceMetrics
elasticache:DescribeCacheClusters, elasticache:DescribeReplicationGroups
es:ListDomainNames, es:DescribeDomains
mq:ListBrokers, mq:DescribeBroker
eks:ListClusters, eks:DescribeCluster
s3:ListAllMyBuckets
elasticloadbalancing:DescribeLoadBalancers, elasticloadbalancing:DescribeTargetGroups
Users & roles
The login is enabled by default. Users, roles, sessions, and the encrypted runtime config all live in the control DB (<data_dir>/auth.db).
- First-time setup seeds admin / admin and forces a password change. Passwords are bcrypt-hashed; login issues an
HttpOnly7-day session cookie. - Roles = a name + a set of services. A scoped role sees only those services, read-only. Built-ins:
admin(manage users/roles + everything) andviewer(all services, read-only). - Create scoped roles (e.g.
db-team→ RDS, DocumentDB, ElastiCache) under Admin ▸ Roles; assign them under Admin ▸ Users. - Service access is enforced server-side on every data path — series list, data/history, pods, logs, and the live SSE stream — so a scoped user can't reach another team's services even via the raw API. Admins bypass the filter.
- Agent push endpoints (
/api/ingest*) use the shared ingest token, not the login.
Data sources & cost
Only CloudWatch GetMetricData is billable — everything else is free. Each series' origin is encoded in its ID prefix, so you always know what a chart costs.
| Source | Prefix | How | Cost |
|---|---|---|---|
| CloudWatch | cw| | ListMetrics + GetMetricData | ~$0.01 / 1k metrics |
| Native — Valkey/ElastiCache | nv| | INFO over Redis protocol | free |
| Native — OpenSearch | nv| | _cluster/health, _nodes/stats | free |
| Native — AmazonMQ (RabbitMQ) | nv| | management HTTP API | free |
| RDS Performance Insights | pi| | pi:GetResourceMetrics | free (7-day) |
| EKS pods / nodes | k8s| | metrics-server (metrics.k8s.io) | free |
| EC2 / host agent | ag| | agent reads /proc, pushes | free |
Keeping the CloudWatch bill down
Cost = metrics × polls. Levers in Admin ▸ Settings ▸ AWS:
- Cost-mode presets — one click sets poll interval + period: frugal (10 min, 5-min resolution), balanced (5 min, default), detailed (1 min). A single call returns every datapoint in its window for one charge, so a longer interval cuts cost without losing resolution — only freshness.
- Live cost estimate — shows the projected monthly CloudWatch spend for your interval, selected services, and the supersede toggle, updating as you change them.
- Native supersedes CloudWatch — drops the paid namespaces a native poller already covers (ElastiCache/OpenSearch/AmazonMQ). Enable only once native pollers are healthy — there's no CloudWatch fallback for a superseded namespace.
- Daily metrics auto-throttled — S3 storage metrics (published once/day) are fetched hourly rather than every poll.
frugal agent never calls CloudWatch and never uses PutMetricData — it reads /proc and pushes to your server. Collecting EC2 host metrics via the agent costs $0, vs. per-read CloudWatch charges for the AWS/EC2 namespace.Services covered
CloudWatch namespaces frugal collects by default: AWS/EC2, AWS/RDS, AWS/DocDB, AWS/ElastiCache, AWS/AmazonMQ, AWS/ES (OpenSearch), AWS/S3, AWS/ApplicationELB, AWS/NetworkELB, AWS/EKS (control plane), ContainerInsights. Native pollers add Valkey/ElastiCache, OpenSearch, and RabbitMQ; RDS Performance Insights adds DB load; the agent adds host metrics + logs.
Select which namespaces to collect (and trim paid ones you don't need) via the services checkboxes in Settings. Empty selection = all defaults.
EKS clusters
Clusters are found from three sources, deduplicated by name:
- Auto-discovery (default) — frugal lists your clusters via
eks:ListClusters+DescribeClusterand mints a token per cluster, so node/pod metrics work straight from AWS credentials — no kubeconfig, no kubectl. Toggle under Settings ▸ Kubernetes. - Uploaded kubeconfig — every context becomes a cluster; EKS exec-auth contexts are tokened automatically.
- Direct API —
api_url+ bearer token entries.
AWS/EKS namespace and appear from credentials alone. Node / pod metrics need a live cluster connection: a reachable API endpoint (public, or in-VPC for private endpoints), the IAM principal mapped in the cluster's access entries / aws-auth, and metrics-server installed. If Nodes shows 0, check those three.Using the dashboard
- Rail (left): pick a service (EKS, EC2, RDS, …) or a host-agent group. Column 2 drills into resources / EKS clusters → control plane, nodes, workloads, pods.
- Time ranges:
12h · 24h · 3d · 7d. CloudWatch history comes from the CloudWatch API; k8s/agent/native history is served from the persisted SQLite store (default retention 7 days). - Drag-to-zoom: drag a horizontal segment on any chart to zoom into that window; double-click to reset. Zooming re-fetches that slice at finer resolution.
- Maximize: the ⤢ button opens a single chart large, with the same ranges + drag-zoom.
- Live: new points stream over SSE. The header shows
<N> series · updated <age>. With CloudWatch as the only source (5-min polls), "updated" ticks up between polls — that's expected. - Logs: live pod-log tails (EKS) and agent-shipped host logs stream to the browser.
CLI
frugal [server] [-config FILE] # web dashboard + collectors (default mode)
frugal agent [-config FILE] # push host metrics + logs to a server
frugal version # print the version
frugal help # full usage, env vars, examples
Run frugal help for the full environment-variable list and examples. Key agent vars: FRUGAL_SERVER_URL, FRUGAL_TOKEN, FRUGAL_AGENT_KUBELOGS, FRUGAL_HOSTNAME.
Security
- The dashboard has no external authentication beyond its built-in login — run it behind a port-forward, VPN, or authenticating ingress. Never expose it directly to the internet.
- Stored credentials (AWS keys, native passwords, ingest token) are encrypted at rest with
FRUGAL_SECRET_KEY(AES-256-GCM). - Prefer IRSA / instance roles over long-lived static keys; frugal needs only read/list/describe.
- Report vulnerabilities privately — see SECURITY.md, not public issues.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| EKS Nodes: 0 (control plane shows) | Node/pod metrics need a live connection: reachable endpoint, access-entry RBAC, and metrics-server. Check kubectl top nodes; grep server logs for k8s: (403 = RBAC, 404 on metrics.k8s.io = no metrics-server, timeout = endpoint unreachable). |
| 0 pts / "updated Xm ago" | Normal when CloudWatch (5-min polls) is the only source — points arrive in bursts. Not a fault; the charts still update. |
| Short range looks sparse, 7d looks dense | CloudWatch has ~1-min granularity with a few-minutes publish lag; coarse buckets on wide ranges hide it. Drag-zoom for detail. |
| No clusters / services at all | Check the AWS region + credentials in Settings, and the server log for aws: collector started and any GetMetricData / AccessDenied errors. |
| "credentials can't be saved" | FRUGAL_SECRET_KEY isn't set — set it (env var) and restart. |
Frugal is licensed under AGPL-3.0. Built by tools-plus. · GitHub