diff --git a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go index 269fb51bd70..6409fe71360 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go @@ -1,6 +1,7 @@ package conversion import ( + "encoding/json" "errors" "fmt" @@ -82,6 +83,17 @@ type dashboardStats struct { annotationCount int linkCount int variableCount int + jsonSize int +} + +// getJSONSize returns the size of an object when serialized to JSON +// This is a generic way to detect content loss without knowing the specific structure +func getJSONSize(v interface{}) int { + data, err := json.Marshal(v) + if err != nil { + return 0 + } + return len(data) } // countPanelsV0V1 counts panels in v0alpha1 or v1beta1 dashboard spec (unstructured JSON) @@ -238,6 +250,7 @@ func collectStatsV0V1(spec map[string]interface{}) dashboardStats { annotationCount: countAnnotationsV0V1(spec), linkCount: countLinksV0V1(spec), variableCount: countVariablesV0V1(spec), + jsonSize: getJSONSize(spec), } } @@ -289,6 +302,7 @@ func collectStatsV2alpha1(spec dashv2alpha1.DashboardSpec) dashboardStats { annotationCount: countAnnotationsV2(spec.Annotations), linkCount: countLinksV2(spec.Links), variableCount: countVariablesV2(spec.Variables), + jsonSize: getJSONSize(spec), } } @@ -340,6 +354,7 @@ func collectStatsV2beta1(spec dashv2beta1.DashboardSpec) dashboardStats { annotationCount: countAnnotationsV2beta1(spec.Annotations), linkCount: countLinksV2beta1(spec.Links), variableCount: countVariablesV2beta1(spec.Variables), + jsonSize: getJSONSize(spec), } } @@ -400,6 +415,20 @@ func detectConversionDataLoss(sourceStats, targetStats dashboardStats, sourceFun )) } + // JSON size: detect significant decrease (>20%) which may indicate content loss + // This catches cases like text panel content being replaced with defaults + if sourceStats.jsonSize > 0 && targetStats.jsonSize > 0 { + decreasePercent := float64(sourceStats.jsonSize-targetStats.jsonSize) / float64(sourceStats.jsonSize) * 100 + if decreasePercent > 20 { + errors = append(errors, fmt.Sprintf( + "JSON size decreased significantly: source=%d bytes, target=%d bytes (%.1f%% decrease, possible content loss)", + sourceStats.jsonSize, + targetStats.jsonSize, + decreasePercent, + )) + } + } + if len(errors) > 0 { errorMsg := fmt.Sprintf("%v", errors) // Note: sourceAPIVersion and targetAPIVersion are passed from checkConversionDataLoss diff --git a/apps/dashboard/pkg/migration/conversion/metrics.go b/apps/dashboard/pkg/migration/conversion/metrics.go index 9cbdec193e8..bb853f599dd 100644 --- a/apps/dashboard/pkg/migration/conversion/metrics.go +++ b/apps/dashboard/pkg/migration/conversion/metrics.go @@ -189,6 +189,8 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion "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)), + "sourceJSONSize", sourceStats.jsonSize, + "targetJSONSize", targetStats.jsonSize, ) } @@ -222,11 +224,19 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion ).Inc() // Log success (debug level to avoid spam) + // Collect stats for logging + sourceStats := collectDashboardStats(a) + targetStats := collectDashboardStats(b) + // Build base log fields for success successLogFields := []interface{}{ "sourceVersionAPI", sourceVersionAPI, "targetVersionAPI", targetVersionAPI, "dashboardUID", dashboardUID, + "panelCount", targetStats.panelCount, + "queryCount", targetStats.queryCount, + "sourceJsonSize", sourceStats.jsonSize, + "targetJsonSize", targetStats.jsonSize, } // Add schema version fields only if we have them (v0/v1 dashboards)