mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-25 07:20:35 +01:00
Compare commits
17 Commits
nv/sql-das
...
nv/integra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e626b14582 | ||
|
|
3fba8c4ab2 | ||
|
|
e8ad0e5912 | ||
|
|
c9fe9ce0e8 | ||
|
|
c46246aa68 | ||
|
|
2cb1a1f215 | ||
|
|
a0e9ee87ae | ||
|
|
3190a9cf02 | ||
|
|
ae9e351905 | ||
|
|
6979c7a5eb | ||
|
|
85aae6e79b | ||
|
|
1e5654d646 | ||
|
|
99b13f04bf | ||
|
|
37b3bfd045 | ||
|
|
ddbf3e1d60 | ||
|
|
418c446f99 | ||
|
|
34c4b385a4 |
211
cmd/dashboardmigrateintegrations/main.go
Normal file
211
cmd/dashboardmigrateintegrations/main.go
Normal file
@@ -0,0 +1,211 @@
|
||||
// Command dashboardmigrateintegrations runs the v1→v2 dashboard migration over
|
||||
// every bundled integration dashboard in this repo — the built-in integrations
|
||||
// under pkg/query-service/app/integrations/builtin_integrations and the cloud
|
||||
// integrations under pkg/modules/cloudintegration/.../fs/definitions — to surface
|
||||
// conversion/validation gaps and rewrite the dashboards to the v2 schema.
|
||||
//
|
||||
// It mirrors the production pipeline (module.ConvertAllV1ToV2): run the v4→v5
|
||||
// widget-query migration in place, then StorableDashboard.ConvertV1ToV2, then
|
||||
// PostableDashboardV2.Validate.
|
||||
//
|
||||
// Unlike dashboardmigraterepo (which reviews an external checkout via -out), these
|
||||
// dashboards live in this repo, so migrated output overwrites each file in place by
|
||||
// default — review the result with `git diff`. Pass -out to mirror the migrated
|
||||
// copies into a separate directory instead, leaving the repo untouched.
|
||||
//
|
||||
// Only files matching <root>/.../assets/dashboards/*.json count as dashboards;
|
||||
// integration.json, config, and other assets are skipped, as are the frozen
|
||||
// pkg/sqlmigration snapshot copies (they live outside the scanned roots).
|
||||
//
|
||||
// Throwaway tooling for the schema migration; not part of the build.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/dashboardmigrateintegrations # overwrite in place
|
||||
// go run ./cmd/dashboardmigrateintegrations -out /tmp/v2 # review copies, repo untouched
|
||||
// go run ./cmd/dashboardmigrateintegrations -only redis # just the redis dashboards
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tagtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// integrationDashboardRoots are the two live sources of bundled integration
|
||||
// dashboards, relative to -in. The pkg/sqlmigration snapshot copies are
|
||||
// deliberately excluded — they are frozen inputs to one-time DB migrations.
|
||||
var integrationDashboardRoots = []string{
|
||||
"pkg/query-service/app/integrations/builtin_integrations",
|
||||
"pkg/modules/cloudintegration/implcloudintegration/fs/definitions",
|
||||
}
|
||||
|
||||
type outcome struct {
|
||||
relPath string
|
||||
status string // ok | skipped-v2 | convert-failed | validate-failed | read-failed | write-failed
|
||||
detail string
|
||||
}
|
||||
|
||||
func main() {
|
||||
inDir := flag.String("in", ".", "repo root to scan for integration dashboards")
|
||||
outDir := flag.String("out", "", "directory to write migrated v2 JSON (mirrors -in layout); empty = overwrite each dashboard in place")
|
||||
only := flag.String("only", "", "restrict to dashboards whose path contains this substring (e.g. redis or aws/rds); empty = all")
|
||||
flag.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
// nil duplicate-key lists == the create path / ConvertAllV1ToV2 wiring.
|
||||
migrator := transition.NewDashboardMigrateV5(slog.New(slog.NewTextHandler(os.Stderr, nil)), nil, nil)
|
||||
|
||||
var outcomes []outcome
|
||||
for _, root := range integrationDashboardRoots {
|
||||
walkRoot := filepath.Join(*inDir, root)
|
||||
err := filepath.WalkDir(walkRoot, func(path string, dirEntry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dirEntry.IsDir() || !isDashboardFile(path) {
|
||||
return nil
|
||||
}
|
||||
// rel is always computed against -in so -out mirrors the repo layout.
|
||||
rel, _ := filepath.Rel(*inDir, path)
|
||||
if *only != "" && !strings.Contains(rel, *only) {
|
||||
return nil
|
||||
}
|
||||
outcomes = append(outcomes, migrateOne(ctx, migrator, path, rel, *outDir))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "walk %s failed: %v\n", walkRoot, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
report(outcomes)
|
||||
}
|
||||
|
||||
// isDashboardFile picks out only the dashboard JSONs — those under an
|
||||
// assets/dashboards/ directory — leaving integration.json, config, and other
|
||||
// per-integration assets untouched.
|
||||
func isDashboardFile(path string) bool {
|
||||
return strings.HasSuffix(path, ".json") &&
|
||||
strings.Contains(filepath.ToSlash(path), "/assets/dashboards/")
|
||||
}
|
||||
|
||||
func migrateOne(ctx context.Context, migrator interface {
|
||||
Migrate(context.Context, map[string]any) bool
|
||||
}, path, rel, outDir string) outcome {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return outcome{rel, "read-failed", err.Error()}
|
||||
}
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
return outcome{rel, "read-failed", err.Error()}
|
||||
}
|
||||
|
||||
storable := dashboardtypes.StorableDashboard{
|
||||
Data: dashboardtypes.StorableDashboardData(data),
|
||||
OrgID: valuer.GenerateUUID(),
|
||||
}
|
||||
storable.ID = valuer.GenerateUUID()
|
||||
|
||||
if storable.IsV2() {
|
||||
return outcome{rel, "skipped-v2", "already v2 schema"}
|
||||
}
|
||||
|
||||
// v1→v2 assumes v5-shaped widget queries; run v4→v5 in place first.
|
||||
migrator.Migrate(ctx, storable.Data)
|
||||
|
||||
v2, err := storable.ConvertV1ToV2()
|
||||
if err != nil {
|
||||
return outcome{rel, "convert-failed", err.Error()}
|
||||
}
|
||||
|
||||
out, err := marshalPostableV2(v2)
|
||||
if err != nil {
|
||||
return outcome{rel, "convert-failed", err.Error()}
|
||||
}
|
||||
|
||||
// Validate exactly as the import API does: unmarshal the JSON back. This both
|
||||
// populates common.JSONRef.Path (json:"-", set only on decode — an in-memory
|
||||
// Spec.Validate() would spuriously fail panel-ref checks) and runs the full
|
||||
// PostableDashboardV2.Validate (DisallowUnknownFields + spec validation).
|
||||
var roundTrip dashboardtypes.PostableDashboardV2
|
||||
if err := json.Unmarshal(out, &roundTrip); err != nil {
|
||||
return outcome{rel, "validate-failed", err.Error()}
|
||||
}
|
||||
|
||||
// Overwrite in place by default; mirror into -out (same layout, same name) when set.
|
||||
dst := path
|
||||
if outDir != "" {
|
||||
dst = filepath.Join(outDir, rel)
|
||||
}
|
||||
if err := writeFile(out, dst); err != nil {
|
||||
return outcome{rel, "write-failed", err.Error()}
|
||||
}
|
||||
return outcome{rel, "ok", ""}
|
||||
}
|
||||
|
||||
// marshalPostableV2 renders the PostableDashboardV2 form (schemaVersion, image,
|
||||
// tags, spec) — the shape the provisioning path decodes.
|
||||
//
|
||||
// generateName is set (and name left empty) so the committed files stay
|
||||
// deterministic — no baked-in random suffix that would churn on every re-run —
|
||||
// and each install generates a fresh per-org dashboard name from spec.display.name.
|
||||
func marshalPostableV2(v2 *dashboardtypes.DashboardV2) ([]byte, error) {
|
||||
postable := dashboardtypes.PostableDashboardV2{
|
||||
DashboardV2MetadataBase: v2.DashboardV2MetadataBase,
|
||||
GenerateName: true,
|
||||
Tags: tagtypes.NewPostableTagsFromTags(v2.Tags),
|
||||
Spec: v2.Spec,
|
||||
}
|
||||
return json.MarshalIndent(postable, "", " ")
|
||||
}
|
||||
|
||||
func writeFile(out []byte, dst string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, out, 0o644)
|
||||
}
|
||||
|
||||
func report(outcomes []outcome) {
|
||||
sort.Slice(outcomes, func(i, j int) bool { return outcomes[i].relPath < outcomes[j].relPath })
|
||||
|
||||
counts := map[string]int{}
|
||||
for _, o := range outcomes {
|
||||
counts[o.status]++
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== %d dashboards ===\n", len(outcomes))
|
||||
for _, status := range []string{"ok", "skipped-v2", "convert-failed", "validate-failed", "read-failed", "write-failed"} {
|
||||
if counts[status] > 0 {
|
||||
fmt.Printf(" %-16s %d\n", status, counts[status])
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== failures ===\n")
|
||||
any := false
|
||||
for _, o := range outcomes {
|
||||
if o.status == "ok" || o.status == "skipped-v2" {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
fmt.Printf("\n[%s] %s\n %s\n", o.status, o.relPath, o.detail)
|
||||
}
|
||||
if !any {
|
||||
fmt.Println(" none")
|
||||
}
|
||||
}
|
||||
@@ -551,12 +551,12 @@ func (module *module) provisionDashboards(ctx context.Context, orgID valuer.UUID
|
||||
continue
|
||||
}
|
||||
|
||||
createdDashboard, err := module.dashboardModule.Create(ctx, orgID, createdBy, creator, dashboardtypes.SourceIntegration, dashboardtypes.PostableDashboard(dashboard.Definition))
|
||||
createdDashboard, err := module.dashboardModule.CreateV2(ctx, orgID, createdBy, creator, dashboardtypes.SourceIntegration, dashboard.Definition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
integrationDashboard := cloudintegrationtypes.NewStorableIntegrationDashboard(createdDashboard.ID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
|
||||
integrationDashboard := cloudintegrationtypes.NewStorableIntegrationDashboard(createdDashboard.ID.StringValue(), cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
|
||||
if err := module.store.CreateIntegrationDashboard(ctx, integrationDashboard); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -151,20 +151,15 @@ func readBuiltInIntegration(dirpath string) (
|
||||
|
||||
func validateIntegration(i IntegrationDetails) error {
|
||||
// Validate dashboard data
|
||||
seenDashboardIds := map[string]interface{}{}
|
||||
seenDashboardIds := map[string]struct{}{}
|
||||
for _, dd := range i.Assets.Dashboards {
|
||||
did, exists := dd["id"]
|
||||
if !exists {
|
||||
return fmt.Errorf("id is required. not specified in dashboard titled %v", dd["title"])
|
||||
if dd.ID == "" {
|
||||
return fmt.Errorf("id is required for dashboard %q in integration %s", dd.Definition.Spec.Display.Name, i.Id)
|
||||
}
|
||||
dashboardId, ok := did.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("id must be string in dashboard titled %v", dd["title"])
|
||||
if _, seen := seenDashboardIds[dd.ID]; seen {
|
||||
return fmt.Errorf("multiple dashboards found with id %s", dd.ID)
|
||||
}
|
||||
if _, seen := seenDashboardIds[dashboardId]; seen {
|
||||
return fmt.Errorf("multiple dashboards found with id %s", dashboardId)
|
||||
}
|
||||
seenDashboardIds[dashboardId] = nil
|
||||
seenDashboardIds[dd.ID] = struct{}{}
|
||||
}
|
||||
|
||||
// TODO(Raj): Validate all parts of plugged in integrations
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,10 @@
|
||||
"pipelines": []
|
||||
},
|
||||
"dashboards": [
|
||||
"file://assets/dashboards/overview.json"
|
||||
{
|
||||
"id": "overview",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
],
|
||||
"alerts": []
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -31,8 +31,14 @@
|
||||
"pipelines": []
|
||||
},
|
||||
"dashboards": [
|
||||
"file://assets/dashboards/overview.json",
|
||||
"file://assets/dashboards/db_metrics_overview.json"
|
||||
{
|
||||
"id": "overview",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
},
|
||||
{
|
||||
"id": "db_metrics_overview",
|
||||
"definition": "file://assets/dashboards/db_metrics_overview.json"
|
||||
}
|
||||
],
|
||||
"alerts": []
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -31,8 +31,14 @@
|
||||
"pipelines": []
|
||||
},
|
||||
"dashboards": [
|
||||
"file://assets/dashboards/overview.json",
|
||||
"file://assets/dashboards/db_metrics_overview.json"
|
||||
{
|
||||
"id": "overview",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
},
|
||||
{
|
||||
"id": "db_metrics_overview",
|
||||
"definition": "file://assets/dashboards/db_metrics_overview.json"
|
||||
}
|
||||
],
|
||||
"alerts": []
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,10 @@
|
||||
"pipelines": []
|
||||
},
|
||||
"dashboards": [
|
||||
"file://assets/dashboards/overview.json"
|
||||
{
|
||||
"id": "overview",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
],
|
||||
"alerts": []
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,10 @@
|
||||
"pipelines": []
|
||||
},
|
||||
"dashboards": [
|
||||
"file://assets/dashboards/overview.json"
|
||||
{
|
||||
"id": "overview",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
],
|
||||
"alerts": []
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,10 @@
|
||||
"pipelines": []
|
||||
},
|
||||
"dashboards": [
|
||||
"file://assets/dashboards/overview.json"
|
||||
{
|
||||
"id": "overview",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
],
|
||||
"alerts": []
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,10 @@
|
||||
"pipelines": []
|
||||
},
|
||||
"dashboards": [
|
||||
"file://assets/dashboards/overview.json"
|
||||
{
|
||||
"id": "overview",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
],
|
||||
"alerts": []
|
||||
},
|
||||
|
||||
@@ -34,12 +34,21 @@ type IntegrationSummary struct {
|
||||
}
|
||||
|
||||
type IntegrationAssets struct {
|
||||
Logs LogsAssets `json:"logs"`
|
||||
Dashboards []dashboardtypes.StorableDashboardData `json:"dashboards"`
|
||||
Logs LogsAssets `json:"logs"`
|
||||
Dashboards []Dashboard `json:"dashboards"`
|
||||
|
||||
Alerts []ruletypes.PostableRule `json:"alerts"`
|
||||
}
|
||||
|
||||
// Dashboard pairs a built-in dashboard's stable id with its v2 definition. The id
|
||||
// lives in integration.json alongside the definition file reference (mirroring the
|
||||
// cloud integration model) and drives the install slug, so it stays stable even
|
||||
// though the v2 definition blob no longer carries a top-level id.
|
||||
type Dashboard struct {
|
||||
ID string `json:"id"`
|
||||
Definition dashboardtypes.PostableDashboardV2 `json:"definition"`
|
||||
}
|
||||
|
||||
type LogsAssets struct {
|
||||
Pipelines []pipelinetypes.PostablePipeline `json:"pipelines"`
|
||||
}
|
||||
@@ -417,23 +426,22 @@ func (m *Manager) provisionDashboards(
|
||||
) error {
|
||||
bareIntegrationID := strings.TrimPrefix(integrationID, "builtin-")
|
||||
for _, dd := range integration.Assets.Dashboards {
|
||||
dashID, _ := dd["id"].(string)
|
||||
if dashID == "" {
|
||||
if dd.ID == "" {
|
||||
continue
|
||||
}
|
||||
slug := cloudintegrationtypes.InstalledIntegrationDashboardSlug(bareIntegrationID, dashID)
|
||||
slug := cloudintegrationtypes.InstalledIntegrationDashboardSlug(bareIntegrationID, dd.ID)
|
||||
|
||||
existing, err := m.installedIntegrationsRepo.getIntegrationDashboardBySlug(ctx, orgID.StringValue(), slug)
|
||||
if err == nil && existing != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
createdDashboard, err := m.dashboardModule.Create(ctx, orgID, createdBy, creator, dashboardtypes.SourceIntegration, dashboardtypes.PostableDashboard(dd))
|
||||
createdDashboard, err := m.dashboardModule.CreateV2(ctx, orgID, createdBy, creator, dashboardtypes.SourceIntegration, dd.Definition)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create dashboard for slug %s: %w", slug, err)
|
||||
}
|
||||
|
||||
row := cloudintegrationtypes.NewStorableIntegrationDashboard(createdDashboard.ID, cloudintegrationtypes.IntegrationDashboardInstalledIntegrationProvider, slug)
|
||||
row := cloudintegrationtypes.NewStorableIntegrationDashboard(createdDashboard.ID.StringValue(), cloudintegrationtypes.IntegrationDashboardInstalledIntegrationProvider, slug)
|
||||
if err := m.installedIntegrationsRepo.createIntegrationDashboard(ctx, row); err != nil {
|
||||
return fmt.Errorf("could not create integration_dashboard row for slug %s: %w", slug, err)
|
||||
}
|
||||
|
||||
@@ -222,7 +222,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddTagUniqueIndexFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddTelemetryTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddTagRelationRankFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewMigrateDashboardsV1ToV2Factory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tag/impltag"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type migrateDashboardsV1ToV2 struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
settings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewMigrateDashboardsV1ToV2Factory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("migrate_dashboards_v1_to_v2"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &migrateDashboardsV1ToV2{sqlstore: sqlstore, sqlschema: sqlschema, settings: ps}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *migrateDashboardsV1ToV2) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
// Up converts every v1 dashboard in every org to the v2 schema in place. It
|
||||
// delegates to the dashboard module's ConvertAllV1ToV2, which — per dashboard,
|
||||
// in its own transaction — runs the v4→v5 query migration, converts to v2,
|
||||
// overwrites the stored data, and syncs tags. Dashboards already in v2 are
|
||||
// skipped and per-dashboard conversion failures are recorded rather than
|
||||
// aborting the run, so one irrecoverably malformed dashboard can't wedge startup.
|
||||
func (migration *migrateDashboardsV1ToV2) Up(ctx context.Context, db *bun.DB) error {
|
||||
var orgIDs []string
|
||||
if err := db.NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dashboardModule := impldashboard.NewModule(
|
||||
impldashboard.NewStore(migration.sqlstore),
|
||||
migration.settings,
|
||||
nil, // analytics, orgGetter, and queryParser are unused on the conversion path
|
||||
nil,
|
||||
nil,
|
||||
impltag.NewModule(impltag.NewStore(migration.sqlstore)),
|
||||
)
|
||||
// ConvertAllV1ToV2 is temporary scaffolding kept off the core Module interface,
|
||||
// so reach it via the V1ToV2Migrator capability.
|
||||
migrator, ok := dashboardModule.(dashboard.V1ToV2Migrator)
|
||||
if !ok {
|
||||
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "dashboard module does not support v1 to v2 conversion")
|
||||
}
|
||||
|
||||
logger := migration.settings.Logger
|
||||
for _, id := range orgIDs {
|
||||
orgID, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := migrator.ConvertAllV1ToV2(ctx, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.InfoContext(ctx, "converted dashboards from v1 to v2",
|
||||
slog.String("org_id", id),
|
||||
slog.Int("total", result.Total),
|
||||
slog.Int("migrated", result.Migrated),
|
||||
slog.Int("skipped", result.Skipped),
|
||||
slog.Int("failed", result.Failed),
|
||||
)
|
||||
for _, item := range result.Results {
|
||||
if item.Status == "failed" {
|
||||
logger.WarnContext(ctx, "failed to convert dashboard from v1 to v2",
|
||||
slog.String("org_id", id),
|
||||
slog.String("dashboard_id", item.ID),
|
||||
slog.String("error", item.Error),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every dashboard now has a name (migrated, or backfilled from the v1 title), so a
|
||||
// v2 dashboard's (org_id, name) can be made unique.
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
sqls := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{
|
||||
TableName: "dashboard",
|
||||
ColumnNames: []sqlschema.ColumnName{"org_id", "name"},
|
||||
})
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *migrateDashboardsV1ToV2) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -125,10 +125,10 @@ type CollectedMetric struct {
|
||||
// This is used to show available pre-made dashboards for a service,
|
||||
// hence has additional fields like id, title and description.
|
||||
type Dashboard struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Definition dashboardtypes.StorableDashboardData `json:"definition,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Definition dashboardtypes.PostableDashboardV2 `json:"definition"`
|
||||
}
|
||||
|
||||
type ServiceDashboard struct {
|
||||
|
||||
Reference in New Issue
Block a user