Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ output) for plain text. URLs, tokens, and suggested commands remain unstyled.
| `--prometheus-url` | (auto-discover) | Manual Prometheus/VictoriaMetrics URL (skips auto-discovery) |
| `--prometheus-header` | | HTTP header sent with every Prometheus request, format `Key=Value` (repeatable). Required for auth-protected backends. |
| `--prometheus-header-from-env` | | HTTP header sent with every Prometheus request, sourced from an environment variable, format `Key=ENV_VAR` (repeatable). |
| `--opencost-currency` | (auto-detect, then USD) | Override the ISO 4217 currency label for OpenCost values. Radar labels values but does not convert them. |
| `--auth-mode` | `none` | Authentication mode: `none`, `proxy`, or `oidc` ([details](docs/authentication.md)) |
| `--no-mcp` | `false` | Disable MCP server for AI tool integration |
| `--mcp-catalog-stdio` | `false` | Start only the MCP catalog over stdio for registry introspection |
Expand Down Expand Up @@ -428,7 +429,9 @@ See [docs/capacity.md](docs/capacity.md) for the full reference.

### Cost Insights

Track Kubernetes spending with OpenCost integration — no additional configuration needed.
Track Kubernetes spending with OpenCost integration. Radar reads `currencyCode` from a running
OpenCost pricing configuration when available and otherwise uses USD. Override the label in
Settings → Cost, config, CLI, or Helm. Radar does not convert values between currencies.

- Cluster hourly and projected monthly cost, top namespaces by spend
- Cost trend charts with 6h/24h/7d range selector
Expand Down
7 changes: 7 additions & 0 deletions cmd/desktop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func main() {
timelineRetention := flag.Duration("timeline-retention", fileCfg.TimelineRetentionOr(7*24*time.Hour), "How long to retain timeline events when --timeline-storage=sqlite (e.g. 168h, 720h). 0 disables age-based cleanup.")
timelineMaxSize := flag.String("timeline-max-size", fileCfg.TimelineMaxSizeOr("1Gi"), "Maximum SQLite timeline storage size before pruning oldest events (e.g. 800Mi, 8Gi). 0 disables size-based pruning.")
prometheusURL := flag.String("prometheus-url", fileCfg.PrometheusURL, "Manual Prometheus/VictoriaMetrics URL (skips auto-discovery)")
openCostCurrency := flag.String("opencost-currency", fileCfg.OpenCostCurrency, "Override the ISO 4217 currency label for OpenCost values (empty: auto-detect, then USD)")
flag.Parse()

if *showVersion {
Expand Down Expand Up @@ -120,6 +121,11 @@ func main() {
log.Printf("ERROR: invalid --timeline-max-size %q: %v", *timelineMaxSize, err)
os.Exit(1)
}
normalizedOpenCostCurrency, err := config.NormalizeOpenCostCurrency(*openCostCurrency)
if err != nil {
log.Printf("ERROR: invalid --opencost-currency %q: %v", *openCostCurrency, err)
os.Exit(1)
}
resolvedPrometheusHeaders, err := app.ResolvePrometheusHeaders(fileCfg.PrometheusHeaders, fileCfg.PrometheusHeadersFromEnv)
if err != nil {
log.Printf("ERROR: invalid Prometheus header configuration: %v", err)
Expand Down Expand Up @@ -163,6 +169,7 @@ func main() {
TimelineRetention: *timelineRetention,
TimelineMaxSizeBytes: timelineMaxSizeBytes,
PrometheusURL: *prometheusURL,
OpenCostCurrency: normalizedOpenCostCurrency,
PrometheusHeaders: resolvedPrometheusHeaders,
PrometheusHeadersFromEnv: fileCfg.PrometheusHeadersFromEnv,
Version: version,
Expand Down
6 changes: 6 additions & 0 deletions cmd/explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func main() {
aiHistory := flag.Bool("ai-history", fileCfg.AIHistoryOr(true), "Persist AI investigations (transcripts + verdicts) to ~/.radar/ai-runs.db so they survive restarts")
// Traffic/metrics options
prometheusURL := flag.String("prometheus-url", fileCfg.PrometheusURL, "Manual Prometheus/VictoriaMetrics URL (skips auto-discovery)")
openCostCurrency := flag.String("opencost-currency", fileCfg.OpenCostCurrency, "Override the ISO 4217 currency label for OpenCost values (empty: auto-detect, then USD)")
// --prometheus-header Key=Value, repeatable. Defaults populated from
// config file; any --prometheus-header flag replaces the file value rather
// than merging — matches kubectl semantics (file is the default, CLI wins).
Expand Down Expand Up @@ -270,6 +271,10 @@ func main() {
if err != nil {
log.Fatalf("Invalid --timeline-max-size %q: %v", *timelineMaxSize, err)
}
normalizedOpenCostCurrency, err := config.NormalizeOpenCostCurrency(*openCostCurrency)
if err != nil {
log.Fatalf("Invalid --opencost-currency %q: %v", *openCostCurrency, err)
}
noMCPFlagSet := false
namespaceFlagSet := false
namespacesFlagSet := false
Expand Down Expand Up @@ -349,6 +354,7 @@ func main() {
TimelineRetention: *timelineRetention,
TimelineMaxSizeBytes: timelineMaxSizeBytes,
PrometheusURL: *prometheusURL,
OpenCostCurrency: normalizedOpenCostCurrency,
PrometheusHeaders: resolvedPrometheusHeaders,
PrometheusHeadersFromEnv: promHeadersFromEnv.value(),
BeylaJobSelector: *beylaJobSelector,
Expand Down
1 change: 1 addition & 0 deletions deploy/helm/radar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ lands in the Helm release state. Rotation requires a pod restart. See
| `timeline.retention` | SQLite retention (Go duration; `0` disables) | `168h` |
| `timeline.maxSize` | SQLite max DB + WAL size before oldest events are pruned (`0` disables) | `800Mi` |
| `persistence.enabled` | Enable PVC for SQLite | `false` |
| `cost.currency` | Optional ISO 4217 override for OpenCost values; empty auto-detects, then uses USD | `""` |
| `traffic.prometheusUrl` | Manual Prometheus/VictoriaMetrics URL (skips auto-discovery) | `""` |
| `traffic.prometheusHeaders` | HTTP headers sent with every Prometheus request (auth-protected backends) | `{}` |
| `traffic.prometheusHeadersFromEnv` | Prometheus headers sourced from environment variables, for secret-backed auth headers | `{}` |
Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/radar/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ spec:
{{- if .Values.traffic.prometheusUrl }}
- --prometheus-url={{ .Values.traffic.prometheusUrl }}
{{- end }}
{{- if .Values.cost.currency }}
- --opencost-currency={{ .Values.cost.currency }}
{{- end }}
{{- range $k, $v := .Values.traffic.prometheusHeaders }}
- {{ printf "--prometheus-header=%s=%s" $k $v | quote }}
{{- end }}
Expand Down
11 changes: 11 additions & 0 deletions deploy/helm/radar/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,17 @@
"enabled": { "type": "boolean" }
}
},
"cost": {
"type": "object",
"additionalProperties": false,
"properties": {
"currency": {
"type": "string",
"pattern": "^$|^[A-Za-z]{3}$",
"description": "Optional ISO 4217 override for OpenCost values; empty auto-detects from a running OpenCost pricing config, then uses USD."
}
}
},
"traffic": {
"type": "object",
"additionalProperties": true,
Expand Down
6 changes: 6 additions & 0 deletions deploy/helm/radar/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,12 @@ mcp:
# Set to false to disable the MCP server (useful when deployed behind authentication)
enabled: true

# OpenCost display configuration
cost:
# Optional ISO 4217 override for OpenCost values. Empty detects currencyCode
# from a running OpenCost pricing config when available, then uses USD.
currency: ""

# Traffic source configuration
traffic:
# Manual Prometheus/VictoriaMetrics URL (bypasses auto-discovery)
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Persistent defaults for CLI flags. CLI flags always override these values. Manag
"timelineMaxSize": "0",
"historyLimit": 10000,
"prometheusUrl": "",
"opencostCurrency": "",
"prometheusHeaders": {},
"mcp": true,
"debugImage": ""
Expand All @@ -68,6 +69,7 @@ All fields are optional — omitted fields use built-in defaults.
| `timelineMaxSize` | Max SQLite DB + WAL size before pruning oldest events (`0` disables) |
| `historyLimit` | Max timeline events to retain |
| `prometheusUrl` | Manual Prometheus/VictoriaMetrics URL — skips auto-discovery. Useful when Prometheus is not in the same cluster or uses a non-standard service name. |
| `opencostCurrency` | Optional ISO 4217 override for values produced by OpenCost. Empty detects `currencyCode` from a running OpenCost pricing configuration when Radar auto-discovers cluster Prometheus, then falls back to `USD`. Radar labels values but does not convert them. Equivalent CLI: `--opencost-currency`; an explicit CLI value remains authoritative after restart. |
| `prometheusHeaders` | HTTP headers sent with every Prometheus request. Required for auth-protected backends — e.g. `{"X-Scope-OrgID": "my-org"}`. Equivalent CLI: `--prometheus-header Key=Value` (repeatable). Stored in plain text in `config.json` — protect the file accordingly. |
| `argoCdUrl` | Manual argocd-server URL for the Argo CD API integration — skips auto-discovery. |
| `argoCdToken` | Argo CD API token (get-only account recommended). Stored in plain text — the file is written `0600`; the token is redacted from `GET /api/config`. |
Expand Down
4 changes: 3 additions & 1 deletion docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,9 @@ PolicyReport findings are policy posture, not live operational failure, so they

[OpenCost](https://www.opencost.io/) is a CNCF tool for Kubernetes cost monitoring, exposing cloud provider pricing and workload resource allocation as Prometheus metrics.

Radar discovers if OpenCost metrics are available in the already-discovered Prometheus. If OpenCost is installed and scraping into Prometheus, cost data appears automatically with no additional configuration. The integration is passive and read-only.
Radar discovers if OpenCost metrics are available in the already-discovered Prometheus. If OpenCost is installed and scraping into Prometheus, cost data appears automatically. The integration is passive and read-only.

OpenCost's Prometheus metrics contain numeric values but no currency metadata. When Radar auto-discovers Prometheus in the connected cluster, it looks for `currencyCode` in the pricing ConfigMap referenced by a running OpenCost deployment. If that evidence is unavailable or ambiguous, Radar uses USD. Radar skips cluster inference for a manually configured Prometheus URL because it may serve another cluster. Override the label in Settings → Cost or `opencostCurrency` (CLI: `--opencost-currency`; Helm: `cost.currency`). CLI and Helm overrides remain authoritative after restart. Radar labels the values but does not convert them.

### What Radar Shows

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.0
google.golang.org/grpc v1.83.0
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
helm.sh/helm/v3 v3.21.3
Expand Down Expand Up @@ -157,7 +158,6 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect
Expand Down
4 changes: 4 additions & 0 deletions internal/app/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type AppConfig struct {
TimelineRetention time.Duration
TimelineMaxSizeBytes int64
PrometheusURL string
OpenCostCurrency string
PrometheusHeaders map[string]string
PrometheusHeadersFromEnv map[string]string
BeylaJobSelector string
Expand Down Expand Up @@ -275,6 +276,7 @@ func CreateServer(cfg AppConfig) *server.Server {
TimelineMaxSize: fmt.Sprintf("%d", cfg.TimelineMaxSizeBytes),
HistoryLimit: cfg.HistoryLimit,
PrometheusURL: cfg.PrometheusURL,
OpenCostCurrency: cfg.OpenCostCurrency,
PrometheusHeaders: cfg.PrometheusHeaders,
PrometheusHeadersFromEnv: cfg.PrometheusHeadersFromEnv,
DebugImage: cfg.DebugImage,
Expand All @@ -292,6 +294,7 @@ func CreateServer(cfg AppConfig) *server.Server {
StaticFS: static.FS,
StaticRoot: "dist",
EffectiveConfig: effectiveCfg,
OpenCostCurrency: cfg.OpenCostCurrency,
DiagConfig: &server.DiagConfig{
Port: cfg.Port,
DevMode: cfg.DevMode,
Expand All @@ -300,6 +303,7 @@ func CreateServer(cfg AppConfig) *server.Server {
HistoryLimit: cfg.HistoryLimit,
DebugEvents: cfg.DebugEvents,
MCPEnabled: cfg.MCPEnabled,
OpenCostCurrency: cfg.OpenCostCurrency,
HasPrometheusURL: cfg.PrometheusURL != "",
HasPrometheusHeaders: len(cfg.PrometheusHeaders) > 0,
},
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Config struct {
TimelineMaxSize string `json:"timelineMaxSize,omitempty"` // Byte size (e.g. "800Mi", "8Gi"); "0" disables
HistoryLimit int `json:"historyLimit,omitempty"`
PrometheusURL string `json:"prometheusUrl,omitempty"`
OpenCostCurrency string `json:"opencostCurrency,omitempty"`
// PrometheusHeaders are sent with every request to the Prometheus API.
// Required for auth-protected backends (Bearer tokens, X-Scope-OrgID, etc.).
// Stored in plain text in ~/.radar/config.json — protect the file accordingly.
Expand Down
23 changes: 23 additions & 0 deletions internal/config/opencost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

import (
"fmt"
"strings"

"golang.org/x/text/currency"
)

func NormalizeOpenCostCurrency(raw string) (string, error) {
code := strings.ToUpper(strings.TrimSpace(raw))
if code == "" {
return "", nil
}
if code == "XXX" || code == "XTS" {
return "", fmt.Errorf("must be a monetary ISO 4217 currency code")
}
unit, err := currency.ParseISO(code)
if err != nil {
return "", fmt.Errorf("must be a recognized ISO 4217 currency code: %w", err)
}
return unit.String(), nil
}
31 changes: 31 additions & 0 deletions internal/config/opencost_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package config

import "testing"

func TestNormalizeOpenCostCurrency(t *testing.T) {
tests := []struct {
name string
raw string
want string
wantErr bool
}{
{name: "auto", want: ""},
{name: "trim and uppercase", raw: " gbp ", want: "GBP"},
{name: "zero decimal currency", raw: "jpy", want: "JPY"},
{name: "unknown", raw: "ZZZ", wantErr: true},
{name: "malformed", raw: "EURO", wantErr: true},
{name: "no currency", raw: "XXX", wantErr: true},
{name: "testing code", raw: "XTS", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NormalizeOpenCostCurrency(tt.raw)
if (err != nil) != tt.wantErr {
t.Fatalf("NormalizeOpenCostCurrency(%q) error = %v, wantErr %v", tt.raw, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("NormalizeOpenCostCurrency(%q) = %q, want %q", tt.raw, got, tt.want)
}
})
}
}
Loading
Loading