apps/dashboard/pkg/migration/README.md
This document describes the Grafana dashboard migration system, focusing on conversion-level practices including metrics, logging, and testing infrastructure for API version conversions. For schema version migration implementation details, see the SchemaVersion Migration Guide.
The Grafana dashboard migration system operates across three main conversion layers:
v0alpha1 (Legacy JSON) → v1beta1 (Migrated JSON) → v2alpha1/v2beta1 (Structured)
When converting dashboards from v0/v1 to v2, panels with Angular types require special handling. The autoMigrateFrom field is used in v0 and v1 to indicate the panel was migrated from a deprecated plugin type, and the target plugin contains migration logic to transform the original panel's options and field configurations.
Panel plugins define their own migration logic via plugin.onPanelTypeChanged(). This migration runs in the frontend when a panel type changes, transforming old options/fieldConfig to the new format. Examples:
singlestat.format: "short" → stat.fieldConfig.defaults.unit: "short"graph.legend.show: true → timeseries.options.legend.showLegend: trueThe v2 schema doesn't include autoMigrateFrom as a typed field, so we need a mechanism to preserve the original panel data for the frontend to run these plugin migrations.
__angularMigration Temporary DataThe backend v1 → v2 conversion preserves the original panel data in a temporary field within vizConfig.spec.options. This works for any Angular panel, not just specific panel types:
{
"vizConfig": {
"kind": "stat",
"spec": {
"options": {
"__angularMigration": {
"autoMigrateFrom": "singlestat",
"originalPanel": {
"type": "singlestat",
"format": "short",
"colorBackground": true,
"sparkline": { "show": true },
"fieldConfig": { ... },
"options": { ... }
}
}
}
}
}
}
Backend (v1 → v2): The conversion detects panels that need Angular migration in two ways:
autoMigrateFrom is already set on the panel (from v0 → v1 migration) - panel type already convertedIn the second case, the conversion also transforms the panel type (e.g., singlestat → stat) and sets autoMigrateFrom. This replicates the same logic as the v0 → v1 migration.
The entire original panel is stored under options.__angularMigration for the frontend to run plugin-specific migrations.
Frontend (v2 load): When building a VizPanel from v2 data:
__angularMigration from options_UNSAFE_customMigrationHandlerPlugin load (VizPanel activation): When the plugin loads, the migration handler calls plugin.onPanelTypeChanged() with the original panel data, allowing the plugin's own migration code to run
autoMigrateFrom, not limited to specific panel typesoriginalPanel contains the complete panel data to ensure no information is lostautoMigrateFrom is not set.| File | Purpose |
|---|---|
apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go | Injects __angularMigration during v1 → v2 |
public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts | Extracts and consumes __angularMigration |
public/app/features/dashboard-scene/serialization/angularMigration.ts | Creates migration handler for v2 path |
The system supports conversions between all dashboard API versions:
| From ↓ / To → | v0alpha1 | v1beta1 | v2alpha1 | v2beta1 |
|---|---|---|---|---|
| v0alpha1 | ✓ | ✓ | ✓ | ✓ |
| v1beta1 | ✓ | ✓ | ✓ | ✓ |
| v2alpha1 | ✓ | ✓ | ✓ | ✓ |
| v2beta1 | ✓ | ✓ | ✓ | ✓ |
Each conversion path is automatically instrumented with metrics and logging.
The supported dashboard API versions are:
dashboard.grafana.app/v0alpha1 - Legacy JSON dashboard formatdashboard.grafana.app/v1beta1 - Migrated JSON dashboard formatdashboard.grafana.app/v2alpha1 - New structured dashboard formatdashboard.grafana.app/v2beta1 - Enhanced structured dashboard formatSchema versions (v13-v42) apply only to v0alpha1 and v1beta1 dashboards:
For detailed information about creating schema version migrations, see the SchemaVersion Migration Guide.
The implementation includes comprehensive test coverage for conversion-level operations:
Both the migration and conversion test suites produce golden files -- JSON snapshots of their output. These files are not tracked in git; only their SHA-256 checksums are committed (in testdata/golden_checksums.json within each package). Tracking checksums instead of full files keeps the repository small (the golden files total hundreds of megabytes) and avoids noisy diffs when migration logic changes, while still catching unintended output changes. The golden files are generated on demand by running the Go tests, and in CI they are built as artifacts so the frontend parity tests can consume them.
The shared checksum logic lives in testutil/golden.go (ChecksumStore). Each package wires it via TestMain in its own golden_test.go.
Generating golden files locally:
# From apps/dashboard/
make generate-golden-files
This runs the migration tests first (which produce testdata/output/ and testdata/dev-dashboards-output/), then the conversion tests (which produce conversion/testdata/output/ and conversion/testdata/migrated_dashboards_output/).
Updating checksums after changing migrations or conversions:
# From apps/dashboard/
make regenerate-golden-checksums
This sets REGENERATE_CHECKSUMS=true and re-runs both test suites so the golden_checksums.json files are updated with the new hashes. Commit the updated checksum files.
How validation works at test time:
REGENERATE_CHECKSUMS=true), it records the new hash instead.The backend conversion tests validate API version conversions and metrics instrumentation:
Test execution:
# All backend conversion tests
go test ./apps/dashboard/pkg/migration/conversion/... -v
# Metrics validation tests
go test ./apps/dashboard/pkg/migration/... -run TestSchemaMigrationMetrics
These tests ensure that backend (Go) and frontend (TypeScript) conversions produce identical outputs. This is critical because:
The frontend tests read golden files generated by the backend tests. In CI, a dedicated generate-golden-files job runs the Go tests and uploads the output as a build artifact, which the frontend test jobs download before running.
Why normalize through Scene?
Both backend and frontend outputs are passed through the same Scene load/save cycle before comparison. This normalization:
Test locations:
| Test File | Purpose |
|---|---|
public/app/features/dashboard-scene/serialization/transformSaveModelV1ToV2.test.ts | v1beta1 -> v2beta1 conversion parity |
public/app/features/dashboard-scene/serialization/transformSaveModelV2ToV1.test.ts | v2beta1 -> v1beta1/v0alpha1 conversion parity |
public/app/features/dashboard/state/DashboardMigratorToBackend.test.ts | Schema version migration parity |
Test execution:
# V1 to V2 conversion parity tests
yarn test transformSaveModelV1ToV2.test.ts
# V2 to V1 conversion parity tests
yarn test transformSaveModelV2ToV1.test.ts
# Schema migration parity tests
yarn test DashboardMigratorToBackend.test.ts
Test approach (v1 -> v2):
v1beta1 -> Go conversion -> v2beta1 -> Scene -> normalized outputv1beta1 -> Scene -> v2beta1 -> Scene -> normalized outputapps/dashboard/pkg/migration/conversion/testdata/ and migrated dashboardsTest approach (v2 -> v1/v0):
v2beta1 -> Go conversion -> v1beta1/v0alpha1 -> Scene -> normalized outputv2beta1 -> Scene -> v1beta1 -> Scene -> normalized outputapps/dashboard/pkg/migration/conversion/testdata/For schema version migration testing details, see the SchemaVersion Migration Guide.
The dashboard migration system provides comprehensive observability through metrics, logging, and error classification to monitor conversion operations.
The dashboard migration system now provides comprehensive observability through:
Metric Name: grafana_dashboard_migration_conversion_success_total
Type: Counter
Description: Total number of successful dashboard conversions
Labels:
source_version_api - Source API version (e.g., "dashboard.grafana.app/v0alpha1")target_version_api - Target API version (e.g., "dashboard.grafana.app/v1beta1")source_schema_version - Source schema version (e.g., "16") - only for v0/v1 dashboardstarget_schema_version - Target schema version (e.g., "41") - only for v0/v1 dashboardsExample:
grafana_dashboard_migration_conversion_success_total{
source_version_api="dashboard.grafana.app/v0alpha1",
target_version_api="dashboard.grafana.app/v1beta1",
source_schema_version="16",
target_schema_version="41"
} 1250
Metric Name: grafana_dashboard_migration_conversion_failure_total
Type: Counter
Description: Total number of failed dashboard conversions
Labels:
source_version_api - Source API versiontarget_version_api - Target API versionsource_schema_version - Source schema version (only for v0/v1 dashboards)target_schema_version - Target schema version (only for v0/v1 dashboards)error_type - Classification of the error (see Error Types section)Example:
grafana_dashboard_migration_conversion_failure_total{
source_version_api="dashboard.grafana.app/v0alpha1",
target_version_api="dashboard.grafana.app/v1beta1",
source_schema_version="14",
target_schema_version="41",
error_type="schema_version_migration_error"
} 42
The error_type label classifies failures into four categories:
conversion_errorschema_version_migration_errorschema_minimum_version_errorconversion_data_loss_errorAll migration logs use structured logging with consistent field names:
Base Fields (always present):
sourceVersionAPI - Source API versiontargetVersionAPI - Target API versiondashboardUID - Unique identifier of the dashboard being convertedSchema Version Fields (v0/v1 dashboards only):
sourceSchemaVersion - Source schema version numbertargetSchemaVersion - Target schema version numbererroredSchemaVersionFunc - Name of the schema migration function that failed (on error)Error Fields (failures only):
errorType - Same classification as metrics error_type labelerroredConversionFunc - Name of the conversion function that failederror - The actual error messageData Loss Fields (conversion_data_loss_error only):
panelsLost - Number of panels lostqueriesLost - Number of queries lostannotationsLost - Number of annotations lostlinksLost - Number of links lostvariablesLost - Number of template variables lost{
"level": "debug",
"msg": "Dashboard conversion succeeded",
"sourceVersionAPI": "dashboard.grafana.app/v0alpha1",
"targetVersionAPI": "dashboard.grafana.app/v1beta1",
"dashboardUID": "abc123",
"sourceSchemaVersion": 16,
"targetSchemaVersion": 41
}
{
"level": "error",
"msg": "Dashboard conversion failed",
"sourceVersionAPI": "dashboard.grafana.app/v0alpha1",
"targetVersionAPI": "dashboard.grafana.app/v1beta1",
"erroredConversionFunc": "Convert_V0_to_V1",
"dashboardUID": "abc123",
"sourceSchemaVersion": 16,
"targetSchemaVersion": 41,
"erroredSchemaVersionFunc": "V24",
"errorType": "schema_version_migration_error",
"error": "migration failed: table panel plugin not found"
}
{
"level": "warn",
"msg": "Dashboard conversion failed",
"sourceVersionAPI": "dashboard.grafana.app/v0alpha1",
"targetVersionAPI": "dashboard.grafana.app/v1beta1",
"erroredConversionFunc": "Convert_V0_to_V1",
"dashboardUID": "def456",
"sourceSchemaVersion": 10,
"targetSchemaVersion": 41,
"erroredSchemaVersionFunc": "",
"errorType": "schema_minimum_version_error",
"error": "dashboard schema version 10 cannot be migrated"
}
{
"level": "error",
"msg": "Dashboard conversion failed",
"sourceVersionAPI": "dashboard.grafana.app/v1beta1",
"targetVersionAPI": "dashboard.grafana.app/v2alpha1",
"erroredConversionFunc": "V1beta1_to_V2alpha1",
"dashboardUID": "abc123",
"sourceSchemaVersion": 42,
"targetSchemaVersion": 42,
"panelsLost": 0,
"queriesLost": 2,
"annotationsLost": 0,
"linksLost": 0,
"variablesLost": 0,
"errorType": "conversion_data_loss_error",
"error": "data loss detected: query count decreased from 7 to 5"
}
Automatic Runtime Checks:
Every conversion automatically detects data loss by comparing:
templating.list in v0/v1, variables in v2)Detection Logic:
conversion_data_loss_errorTesting:
Run comprehensive data loss tests on all conversion test files:
# Test all conversions for data loss
go test ./apps/dashboard/pkg/migration/conversion/... -run TestDataLossDetectionOnAllInputFiles -v
# Test shows detailed panel/query analysis when loss is detected
Implementation: See conversion/conversion_data_loss_detection.go and conversion/README.md for details.
All dashboard conversions are automatically instrumented via the normalizeConversion wrapper function, which provides:
storedVersion is preserved through multi-step conversionsThe normalizeConversion wrapper uses the DashboardConversion interface, which all dashboard API versions implement to provide consistent conversion behavior:
// All conversion functions are wrapped with normalizeConversion
// This ensures consistent status handling, metrics, and error management
s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil),
normalizeConversion(dashv0.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error {
return Convert_V0_to_V1beta1(a.(*dashv0.Dashboard), b.(*dashv1.Dashboard), scope)
}))
Custom error types provide structured error information:
// Schema migration errors
type MigrationError struct {
msg string
targetVersion int
currentVersion int
functionName string
}
// API conversion errors
type ConversionError struct {
msg string
functionName string
currentAPIVersion string
targetAPIVersion string
}
// Data loss errors are detected when dashboard components (panels, queries, annotations, links, variables)
// are lost during conversion
type ConversionDataLossError struct {
functionName string // Function where data loss was detected (e.g., "V1_to_V2alpha1")
message string // Detailed error message with loss statistics
sourceAPIVersion string // Source API version (e.g., "dashboard.grafana.app/v1beta1")
targetAPIVersion string // Target API version (e.g., "dashboard.grafana.app/v2alpha1")
}
Metrics must be registered with Prometheus during service initialization:
import "github.com/grafana/grafana/apps/dashboard/pkg/migration"
// Register metrics with Prometheus
migration.RegisterMetrics(prometheusRegistry)
The following metrics are available after registration:
// Success counter
migration.MDashboardConversionSuccessTotal
// Failure counter
migration.MDashboardConversionFailureTotal