package conversion import ( "errors" "fmt" "math" "strings" "k8s.io/apimachinery/pkg/conversion" "github.com/grafana/grafana-app-sdk/logging" dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) func getLogger() logging.Logger { return logging.DefaultLogger.With("logger", "dashboard.conversion") } // getErroredSchemaVersionFunc determines the schema version function that errored func getErroredSchemaVersionFunc(err error) string { var migrationErr *schemaversion.MigrationError if errors.As(err, &migrationErr) { return migrationErr.GetFunctionName() } return "" } // getErroredConversionFunc determines the conversion function that errored func getErroredConversionFunc(err error) string { var conversionErr *ConversionError if errors.As(err, &conversionErr) { return conversionErr.GetFunctionName() } var migrationErr *schemaversion.MigrationError if errors.As(err, &migrationErr) { return migrationErr.GetFunctionName() } var dataLossErr *ConversionDataLossError if errors.As(err, &dataLossErr) { return dataLossErr.GetFunctionName() } return "" } // convertAPIVersionToFuncName converts API version to function name format func convertAPIVersionToFuncName(apiVersion string) string { // Convert dashboard.grafana.app/v0alpha1 to v0alpha1 if idx := strings.LastIndex(apiVersion, "/"); idx != -1 { apiVersion = apiVersion[idx+1:] } // Map API versions to function name format switch apiVersion { case "v0alpha1": return "V0" case "v1beta1": return "V1" case "v2alpha1": return "V2alpha1" case "v2beta1": return "V2beta1" default: return apiVersion } } // withConversionMetrics wraps a conversion function with metrics and logging for the overall conversion process // It optionally runs a data loss check function after successful conversion func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversionFunc func(a, b interface{}, scope conversion.Scope) error) func(a, b interface{}, scope conversion.Scope) error { return func(a, b interface{}, scope conversion.Scope) error { // Extract dashboard UID and schema version from source var dashboardUID string var sourceSchemaVersion interface{} var targetSchemaVersion interface{} // Try to extract UID and schema version from source dashboard // Only track schema versions for v0/v1 dashboards (v2+ info is redundant with API version) switch source := a.(type) { case *dashv0.Dashboard: dashboardUID = source.Name if source.Spec.Object != nil { sourceSchemaVersion = schemaversion.GetSchemaVersion(source.Spec.Object) } case *dashv1.Dashboard: dashboardUID = source.Name if source.Spec.Object != nil { sourceSchemaVersion = schemaversion.GetSchemaVersion(source.Spec.Object) } case *dashv2alpha1.Dashboard: dashboardUID = source.Name // Don't track schema version for v2+ (redundant with API version) case *dashv2beta1.Dashboard: dashboardUID = source.Name // Don't track schema version for v2+ (redundant with API version) } // Determine target schema version based on target type // Only for v0/v1 dashboards switch b.(type) { case *dashv0.Dashboard: if sourceSchemaVersion != nil { targetSchemaVersion = sourceSchemaVersion // V0 keeps source schema version } case *dashv1.Dashboard: if sourceSchemaVersion != nil { targetSchemaVersion = schemaversion.LATEST_VERSION // V1 migrates to latest } case *dashv2alpha1.Dashboard: // Don't track schema version for v2+ (redundant with API version) case *dashv2beta1.Dashboard: // Don't track schema version for v2+ (redundant with API version) } // Execute the actual conversion err := conversionFunc(a, b, scope) // If conversion succeeded, run data loss check if err == nil { err = checkConversionDataLoss(sourceVersionAPI, targetVersionAPI, a, b) } // Report conversion-level metrics and logs if err != nil { // Classify error type for metrics errorType := "conversion_error" var migrationErr *schemaversion.MigrationError var minVersionErr *schemaversion.MinimumVersionError var dataLossErr *ConversionDataLossError if errors.As(err, &migrationErr) { errorType = "schema_version_migration_error" } else if errors.As(err, &minVersionErr) { errorType = "schema_minimum_version_error" } else if errors.As(err, &dataLossErr) { errorType = "conversion_data_loss_error" } // Record failure metrics sourceSchemaStr := "" targetSchemaStr := "" if sourceSchemaVersion != nil { sourceSchemaStr = fmt.Sprintf("%v", sourceSchemaVersion) } if targetSchemaVersion != nil { targetSchemaStr = fmt.Sprintf("%v", targetSchemaVersion) } migration.MDashboardConversionFailureTotal.WithLabelValues( sourceVersionAPI, targetVersionAPI, sourceSchemaStr, targetSchemaStr, errorType, ).Inc() // Log failure - use warning for schema_minimum_version_error, error for others // Build base log fields logFields := []interface{}{ "sourceVersionAPI", sourceVersionAPI, "targetVersionAPI", targetVersionAPI, "erroredConversionFunc", getErroredConversionFunc(err), "dashboardUID", dashboardUID, } // Add schema version fields only if we have them (v0/v1 dashboards) if sourceSchemaVersion != nil && targetSchemaVersion != nil { logFields = append(logFields, "sourceSchemaVersion", sourceSchemaVersion, "targetSchemaVersion", targetSchemaVersion, "erroredSchemaVersionFunc", getErroredSchemaVersionFunc(err), ) } // Add data loss specific fields if this is a data loss error if errorType == "conversion_data_loss_error" { sourceStats := collectDashboardStats(a) targetStats := collectDashboardStats(b) // We consider losing data when the target is less than the source logFields = append(logFields, "panelsLost", math.Max(0, float64(sourceStats.panelCount-targetStats.panelCount)), "queriesLost", math.Max(0, float64(sourceStats.queryCount-targetStats.queryCount)), "annotationsLost", math.Max(0, float64(sourceStats.annotationCount-targetStats.annotationCount)), "linksLost", math.Max(0, float64(sourceStats.linkCount-targetStats.linkCount)), "variablesLost", math.Max(0, float64(sourceStats.variableCount-targetStats.variableCount)), ) } // Add remaining fields logFields = append(logFields, "errorType", errorType, "error", err, ) if errorType == "schema_minimum_version_error" { getLogger().Warn("Dashboard conversion failed", logFields...) } else { getLogger().Error("Dashboard conversion failed", logFields...) } } else { // Record success metrics sourceSchemaStr := "" targetSchemaStr := "" if sourceSchemaVersion != nil { sourceSchemaStr = fmt.Sprintf("%v", sourceSchemaVersion) } if targetSchemaVersion != nil { targetSchemaStr = fmt.Sprintf("%v", targetSchemaVersion) } migration.MDashboardConversionSuccessTotal.WithLabelValues( sourceVersionAPI, targetVersionAPI, sourceSchemaStr, targetSchemaStr, ).Inc() // Log success (debug level to avoid spam) // Build base log fields for success successLogFields := []interface{}{ "sourceVersionAPI", sourceVersionAPI, "targetVersionAPI", targetVersionAPI, "dashboardUID", dashboardUID, } // Add schema version fields only if we have them (v0/v1 dashboards) if sourceSchemaVersion != nil && targetSchemaVersion != nil { successLogFields = append(successLogFields, "sourceSchemaVersion", sourceSchemaVersion, "targetSchemaVersion", targetSchemaVersion, ) } getLogger().Debug("Dashboard conversion succeeded", successLogFields...) } return nil } }