Convert Grafana dashboards to SUSE Observability (StackState) dashboard YAML — with an interactive wizard or non-interactive CLI.
- Web UI — run
odyssey serverfor a browser-based dashboard conversion experience - Interactive wizard — run
odysseywith no arguments for a guided terminal experience - Non-interactive CLI —
odyssey convertandodyssey checkfor scripting and CI - Handles all common Grafana JSON layouts:
panels[],rows[], nested panels,targets[].expr, andoptions.queries[] - Sanitises Grafana-specific PromQL: template variables, time-range variables, uppercase functions
- Configurable interval — replace
$__rate_interval,$__interval,$__range_swith your chosen duration (default 5m; use--intervalor web UI) - Variable bake-in — Grafana template variables (e.g.
$namespace,$job) are parsed fromtemplating.listand their default values are baked into queries. Override via--variable namespace=prod(CLI) or the web UI - Auto-detects metric namespace prefixes (e.g.
pg_up→postgresql_pg_up) and rewrites queries - Merges multiple Grafana JSON files into a single STS dashboard
checkmode prints a per-panel metric-availability report
go install github.com/aeltai/odyssey@latestOr build from source:
git clone https://github.com/aeltai/odyssey.git
cd odyssey
make build # produces ./odysseyodyssey server
# → http://localhost:3000Upload Grafana JSON files, configure your STS connection, preview panel analysis, and download the converted YAML — all from your browser.
odysseyThe wizard will guide you through:
- Selecting a Grafana dashboard JSON file
- Connecting to your SUSE Observability instance
- Reviewing metric availability
- Configuring the output dashboard
- Generating YAML and optionally applying it
# Check which panels would have data
odyssey check postgres-dashboard.json
# Convert a dashboard
odyssey convert -o postgres.sts.yaml postgres-dashboard.json
# Include all panels even if metrics are missing
odyssey convert --include-missing -o full.sts.yaml dashboard.json
# Bake namespace and job variables into queries
odyssey convert -v namespace=prod -v job=api-server -o filtered.sts.yaml dashboard.json
# Merge multiple dashboards
odyssey convert --name "All Services" -o merged.sts.yaml nginx.json mysql.json
# Apply the result
sts dashboard apply --file postgres.sts.yamlLaunches the interactive wizard.
odyssey convert [flags] <dashboard.json> [more.json ...]
| Flag | Default | Description |
|---|---|---|
-o, --output |
<input>.sts.yaml |
Output YAML file path |
--name |
from Grafana title | Dashboard name in STS |
--interval |
5m |
PromQL interval for rate/irate (e.g. 1m, 5m, 15m, 1h) |
-v, --variable |
(none) | Bake variable value into queries (repeatable, e.g. -v namespace=prod) |
--include-missing |
false |
Include panels with missing metrics |
--rewrite-metrics |
true |
Rewrite queries with detected prefix |
--metric-prefix |
auto-detected | Override the namespace prefix |
--id |
0 (create new) |
Existing STS dashboard ID for updates |
--sts-url |
SUSE Observability base URL | |
--sts-token |
SUSE Observability API token |
odyssey check [flags] <dashboard.json> [more.json ...]
| Flag | Default | Description |
|---|---|---|
--sts-url |
SUSE Observability base URL | |
--sts-token |
SUSE Observability API token | |
--interval |
5m |
PromQL interval for rate/irate (e.g. 1m, 5m, 15m, 1h) |
-v, --variable |
(none) | Bake variable value into queries (repeatable, e.g. -v namespace=prod) |
odyssey server [flags]
| Flag | Default | Description |
|---|---|---|
-p, --port |
3000 |
Port to listen on |
Starts the web UI. The frontend is embedded in the binary — no Node.js required at runtime.
Print the version and exit.
The STS connection is resolved in priority order:
| Source | URL | Token |
|---|---|---|
| CLI flags | --sts-url |
--sts-token |
| Environment | STS_URL |
STS_API_TOKEN |
| sts CLI config | ~/.config/stackstate-cli/config.yaml |
(same) |
┌─────────────────┐ ┌──────────────┐ ┌────────────────┐
│ Grafana JSON(s) │───>│ Parse panels │───>│ Sanitise PromQL│
└─────────────────┘ └──────────────┘ └───────┬────────┘
│
┌──────────────┐ ┌───────▼────────┐
│ STS Prom API │───>│ Match metrics │
└──────────────┘ └───────┬────────┘
│
┌──────────────┐ ┌───────▼────────┐
│ YAML output │<───│ Build dashboard│
└──────────────┘ └────────────────┘
- Parse — Walk all Grafana panels (including rows, nested panels) and extract every PromQL expression
- Sanitise — Bake Grafana template-variable values into label selectors (from
templating.listdefaults or--variableoverrides), remove remaining variable references, replace$__rate_interval/$__interval/$__range_swith your chosen interval (default 5m), lowercase function names - Extract — Use the Prometheus PromQL parser to identify all metric names
- Fetch — Call
GET {url}/prometheus/api/v1/label/__name__/valuesto get available STS metrics - Match — Check each panel's metrics with prefix-aware suffix matching
- Rewrite — Update metric names in queries to use the detected agent prefix
- Build — Generate STS dashboard YAML (
dashboard.spec.layouts+dashboard.spec.panels)
| Grafana pattern | Replacement |
|---|---|
$__rate_interval, $__interval, $__range |
Your chosen interval (default 5m) |
$__range_s |
Seconds derived from interval (e.g. 300 for 5m) |
label="$variable" (with override) |
label="value" — baked from Grafana default or --variable |
label="$variable" (no override) |
(removed) |
label=~"$variable" (with override) |
label=~"value" |
label=~"$variable" (no override) |
(removed) |
SUM(...), AVG(...) |
sum(...), avg(...) |
Use --interval 1m (CLI) or the interval selector (web UI) to change the default. Use -v namespace=prod to bake variable values into queries.
Dashboards in examples/ have been deployed to a live SUSE Observability instance and verified end-to-end.
| Dashboard | Grafana ID | Panels | Verified | Notes |
|---|---|---|---|---|
| PostgreSQL | 9628 | 40 (34 live) | Yes | Prefix postgresql auto-rewritten |
| NGINX Ingress | 12708 | 8 (8 live) | Yes | 100% coverage |
| MySQL Overview | 14031 | 25 (21 live) | Yes | 4 use deprecated MySQL 8.0 metrics |
| Kubewarden | 15760 | 24 | Yes | Requires Kubewarden metrics |
The full test suite validates parsing, sanitisation, and YAML generation against the top 47 Grafana dashboards (1,551 panels total). Run with make test-integration.
make test # run all tests with race detector
make test-integration # download 47 dashboards and run integration tests
make lint # go vet
make build # build binary
make install # go install
make clean # remove binaryodyssey/
├── main.go # Entry point
├── cmd/
│ ├── root.go # Cobra root command
│ ├── server.go # Web UI server (embedded Vue)
│ ├── convert.go # Non-interactive convert
│ ├── check.go # Non-interactive check
│ ├── interactive.go # Interactive wizard (huh)
│ ├── exec.go # Command execution helper
│ └── dist/ # Built frontend (auto-generated)
├── web/ # Vue 3 + Vite + Tailwind frontend
│ ├── src/
│ │ ├── App.vue # Main app with wizard stepper
│ │ └── components/
│ │ ├── StepUpload.vue # Drag & drop file upload
│ │ ├── StepConfig.vue # STS connection + options
│ │ ├── StepResults.vue # Panel analysis & metrics
│ │ └── StepOutput.vue # YAML preview & download
│ ├── vite.config.js
│ └── package.json
├── internal/
│ ├── api/
│ │ └── handler.go # HTTP API handlers
│ ├── engine/
│ │ └── engine.go # Shared conversion logic
│ ├── integration_test.go # 47-dashboard integration tests
│ ├── grafana/
│ │ ├── parse.go # Grafana JSON parser
│ │ └── parse_test.go
│ ├── promql/
│ │ ├── metrics.go # PromQL metric extraction
│ │ └── metrics_test.go
│ └── sts/
│ ├── client.go # STS API client + MetricIndex
│ ├── client_test.go
│ ├── dashboard.go # STS YAML builder
│ ├── dashboard_test.go
│ ├── sanitize.go # PromQL sanitisation
│ └── sanitize_test.go
├── examples/
│ ├── grafana-json/ # Source Grafana dashboards
│ └── verified/ # Converted STS YAML (ready to apply)
├── testdata/dashboards/ # 47 dashboards (downloaded via script)
├── scripts/download-testdata.sh
├── Makefile
├── go.mod / go.sum
└── .gitignore
Apache License 2.0