Skip to content

Commit

Permalink
Simplified logic for migration complexity explanation summary in html…
Browse files Browse the repository at this point in the history
… report

- moved the html template code for summary to the parent template file
- passing combined struct(report + explanation summary) to the html template
  • Loading branch information
sanyamsinghal committed Jan 28, 2025
1 parent 396bece commit 9f09326
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 126 deletions.
49 changes: 18 additions & 31 deletions yb-voyager/cmd/assessMigrationCommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,6 @@ func packAndSendAssessMigrationPayload(status string, errMsg string) {
}),
}

// we will build this twice for json and html reports both, at the time of report generation.
// whatever happens later will be stored in the struct's field. so to be on safer side, we will build it again here as per required format.
explanation, err := buildMigrationComplexityExplanation(source.DBType, assessmentReport, "")
if err != nil {
log.Errorf("failed to build migration complexity explanation for callhome assessment payload: %v", err)
}

var obfuscatedIssues []callhome.AssessmentIssueCallhome
for _, issue := range assessmentReport.Issues {
obfuscatedIssue := callhome.AssessmentIssueCallhome{
Expand All @@ -195,7 +188,7 @@ func packAndSendAssessMigrationPayload(status string, errMsg string) {
PayloadVersion: callhome.ASSESS_MIGRATION_CALLHOME_PAYLOAD_VERSION,
TargetDBVersion: assessmentReport.TargetDBVersion,
MigrationComplexity: assessmentReport.MigrationComplexity,
MigrationComplexityExplanation: explanation,
MigrationComplexityExplanation: assessmentReport.MigrationComplexityExplanation,
SchemaSummary: callhome.MarshalledJsonString(schemaSummaryCopy),
Issues: callhome.MarshalledJsonString(obfuscatedIssues),
Error: callhome.SanitizeErrorMsg(errMsg),
Expand All @@ -206,7 +199,7 @@ func packAndSendAssessMigrationPayload(status string, errMsg string) {
}

payload.PhasePayload = callhome.MarshalledJsonString(assessPayload)
err = callhome.SendPayload(&payload)
err := callhome.SendPayload(&payload)
if err == nil && (status == COMPLETE || status == ERROR) {
callHomeErrorOrCompletePayloadSent = true
}
Expand Down Expand Up @@ -463,19 +456,13 @@ func createMigrationAssessmentCompletedEvent() *cp.MigrationAssessmentCompletedE
}

assessmentIssues := convertAssessmentIssueToYugabyteDAssessmentIssue(assessmentReport)
// we will build this twice for json and html reports both, at the time of report generation.
// whatever happens later will be stored in the struct's field. so to be on safer side, we will build it again here as per required format.
explanation, err := buildMigrationComplexityExplanation(source.DBType, assessmentReport, "")
if err != nil {
log.Errorf("failed to build migration complexity explanation for yugabyted assessment payload: %v", err)
}

payload := AssessMigrationPayload{
PayloadVersion: ASSESS_MIGRATION_YBD_PAYLOAD_VERSION,
VoyagerVersion: assessmentReport.VoyagerVersion,
TargetDBVersion: assessmentReport.TargetDBVersion,
MigrationComplexity: assessmentReport.MigrationComplexity,
MigrationComplexityExplanation: explanation,
MigrationComplexityExplanation: assessmentReport.MigrationComplexityExplanation,
SchemaSummary: assessmentReport.SchemaSummary,
AssessmentIssues: assessmentIssues,
SourceSizeDetails: SourceDBSizeDetails{
Expand Down Expand Up @@ -828,7 +815,10 @@ func generateAssessmentReport() (err error) {
addMigrationCaveatsToAssessmentReport(unsupportedDataTypesForLiveMigration, unsupportedDataTypesForLiveMigrationWithFForFB)

// calculating migration complexity after collecting all assessment issues
assessmentReport.MigrationComplexity = calculateMigrationComplexity(source.DBType, schemaDir, assessmentReport)
complexity, explanation := calculateMigrationComplexityAndExplanation(source.DBType, schemaDir, assessmentReport)
log.Infof("migration complexity: %q and explanation: %q", complexity, explanation)
assessmentReport.MigrationComplexity = complexity
assessmentReport.MigrationComplexityExplanation = explanation

assessmentReport.Sizing = migassessment.SizingReport
assessmentReport.TableIndexStats, err = assessmentDB.FetchAllStats()
Expand Down Expand Up @@ -1565,13 +1555,6 @@ func generateAssessmentReportJson(reportDir string) error {
jsonReportFilePath := filepath.Join(reportDir, fmt.Sprintf("%s%s", ASSESSMENT_FILE_NAME, JSON_EXTENSION))
log.Infof("writing assessment report to file: %s", jsonReportFilePath)

var err error
assessmentReport.MigrationComplexityExplanation, err = buildMigrationComplexityExplanation(source.DBType, assessmentReport, "")
if err != nil {
return fmt.Errorf("unable to build migration complexity explanation for json report: %w", err)
}
log.Infof("migration complexity explanation: %q", assessmentReport.MigrationComplexityExplanation)

strReport, err := json.MarshalIndent(assessmentReport, "", "\t")
if err != nil {
return fmt.Errorf("failed to marshal the assessment report: %w", err)
Expand All @@ -1590,12 +1573,6 @@ func generateAssessmentReportHtml(reportDir string) error {
htmlReportFilePath := filepath.Join(reportDir, fmt.Sprintf("%s%s", ASSESSMENT_FILE_NAME, HTML_EXTENSION))
log.Infof("writing assessment report to file: %s", htmlReportFilePath)

var err error
assessmentReport.MigrationComplexityExplanation, err = buildMigrationComplexityExplanation(source.DBType, assessmentReport, "html")
if err != nil {
return fmt.Errorf("unable to build migration complexity explanation for html report: %w", err)
}

file, err := os.Create(htmlReportFilePath)
if err != nil {
return fmt.Errorf("failed to create file for %q: %w", filepath.Base(htmlReportFilePath), err)
Expand Down Expand Up @@ -1625,7 +1602,17 @@ func generateAssessmentReportHtml(reportDir string) error {
// marking this as empty to not display this in html report for PG
assessmentReport.SchemaSummary.SchemaNames = []string{}
}
err = tmpl.Execute(file, assessmentReport)

type CombinedStruct struct {
AssessmentReport
MigrationComplexityCategorySummary []MigrationComplexityCategorySummary
}
combined := CombinedStruct{
AssessmentReport: assessmentReport,
MigrationComplexityCategorySummary: buildCategorySummary(source.DBType, assessmentReport.Issues),
}

err = tmpl.Execute(file, combined)
if err != nil {
return fmt.Errorf("failed to render the assessment report: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion yb-voyager/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -1158,7 +1158,7 @@ type AssessmentIssueYugabyteD struct {
Type string `json:"Type"` // Ex: GIN_INDEXES, SECURITY_INVOKER_VIEWS, STORED_GENERATED_COLUMNS
Name string `json:"Name"` // Ex: GIN Indexes, Security Invoker Views, Stored Generated Columns
Description string `json:"Description"` // description based on type/name
Impact string `json:"Impact"` // // Level-1, Level-2, Level-3 (no default: need to be assigned for each issue)
Impact string `json:"Impact"` // Level-1, Level-2, Level-3 (no default: need to be assigned for each issue)
ObjectType string `json:"ObjectType"` // For datatype category, ObjectType will be datatype (for eg "geometry")
ObjectName string `json:"ObjectName"` // Fully qualified object name(empty if NA, eg UQC)
SqlStatement string `json:"SqlStatement"` // DDL or DML(UQC)
Expand Down
103 changes: 11 additions & 92 deletions yb-voyager/cmd/migration_complexity.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@ limitations under the License.
package cmd

import (
"bytes"
"encoding/csv"
"fmt"
"math"
"os"
"path/filepath"
"strings"
"text/template"

"github.com/samber/lo"
log "github.com/sirupsen/logrus"
Expand All @@ -45,9 +43,9 @@ var (
)

// Migration complexity calculation based on the detected assessment issues
func calculateMigrationComplexity(sourceDBType string, schemaDirectory string, assessmentReport AssessmentReport) string {
func calculateMigrationComplexityAndExplanation(sourceDBType string, schemaDirectory string, assessmentReport AssessmentReport) (string, string) {
if sourceDBType != ORACLE && sourceDBType != POSTGRESQL {
return NOT_AVAILABLE
return NOT_AVAILABLE, ""
}

log.Infof("calculating migration complexity for %s...", sourceDBType)
Expand All @@ -56,19 +54,19 @@ func calculateMigrationComplexity(sourceDBType string, schemaDirectory string, a
migrationComplexity, err := calculateMigrationComplexityForOracle(schemaDirectory)
if err != nil {
log.Errorf("failed to get migration complexity for oracle: %v", err)
return NOT_AVAILABLE
return NOT_AVAILABLE, ""
}
return migrationComplexity
return migrationComplexity, ""
case POSTGRESQL:
return calculateMigrationComplexityForPG(assessmentReport)
return calculateMigrationComplexityAndExplanationForPG(assessmentReport)
default:
panic(fmt.Sprintf("unsupported source db type '%s' for migration complexity", sourceDBType))
}
}

func calculateMigrationComplexityForPG(assessmentReport AssessmentReport) string {
func calculateMigrationComplexityAndExplanationForPG(assessmentReport AssessmentReport) (string, string) {
if assessmentReport.MigrationComplexity != "" {
return assessmentReport.MigrationComplexity
return assessmentReport.MigrationComplexity, assessmentReport.MigrationComplexityExplanation
}

counts := lo.CountValuesBy(assessmentReport.Issues, func(issue AssessmentIssue) string {
Expand Down Expand Up @@ -97,7 +95,7 @@ func calculateMigrationComplexityForPG(assessmentReport AssessmentReport) string
}

migrationComplexityRationale = buildRationale(finalComplexity, l1IssueCount, l2IssueCount, l3IssueCount)
return finalComplexity
return finalComplexity, migrationComplexityRationale
}

// This is a temporary logic to get migration complexity for oracle based on the migration level from ora2pg report.
Expand Down Expand Up @@ -212,92 +210,12 @@ func getComplexityForLevel(level string, count int) string {
}

// ======================================= Migration Complexity Explanation ==========================================

// TODO: discuss if the html should be in main report or here
const explainTemplateHTML = `
{{- if .Summaries }}
<p>Below is a breakdown of the issues detected in different categories for each impact level.</p>
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
<thead>
<tr>
<th>Category</th>
<th>Level 1</th>
<th>Level 2</th>
<th>Level 3</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{{- range .Summaries }}
<tr>
<td>{{ .Category }}</td>
<td>{{ index .ImpactCounts "LEVEL_1" }}</td>
<td>{{ index .ImpactCounts "LEVEL_2" }}</td>
<td>{{ index .ImpactCounts "LEVEL_3" }}</td>
<td>{{ .TotalIssueCount }}</td>
</tr>
{{- end }}
</tbody>
</table>
{{- end }}
<p>
<strong>Complexity:</strong> {{ .Complexity }}</br>
<strong>Reasoning:</strong> {{ .ComplexityRationale }}
</p>
<p>
<strong>Impact Levels:</strong></br>
Level 1: Resolutions are available with minimal effort.<br/>
Level 2: Resolutions are available requiring moderate effort.<br/>
Level 3: Resolutions may not be available or are complex.
</p>
`

const explainTemplateText = `{{ .ComplexityRationale }}`

type MigrationComplexityExplanationData struct {
Summaries []MigrationComplexityCategorySummary
Complexity string
ComplexityRationale string // short reasoning or explanation text
}

type MigrationComplexityCategorySummary struct {
Category string
TotalIssueCount int
ImpactCounts map[string]int // e.g. {"Level-1": 3, "Level-2": 5, "Level-3": 2}
}

func buildMigrationComplexityExplanation(sourceDBType string, assessmentReport AssessmentReport, reportFormat string) (string, error) {
if sourceDBType != POSTGRESQL {
return "", nil
}

var explanation MigrationComplexityExplanationData
explanation.Complexity = assessmentReport.MigrationComplexity
explanation.ComplexityRationale = migrationComplexityRationale

explanation.Summaries = buildCategorySummary(assessmentReport.Issues)

var tmpl *template.Template
var err error
if reportFormat == "html" {
tmpl, err = template.New("Explain").Parse(explainTemplateHTML)
} else {
tmpl, err = template.New("Explain").Parse(explainTemplateText)
}

if err != nil {
return "", fmt.Errorf("failed creating the explanation template: %w", err)
}

var buf bytes.Buffer
if err := tmpl.Execute(&buf, explanation); err != nil {
return "", fmt.Errorf("failed executing the template with data: %w", err)
}
return buf.String(), nil
}

func buildRationale(finalComplexity string, l1Count int, l2Count int, l3Count int) string {
switch finalComplexity {
case constants.MIGRATION_COMPLEXITY_HIGH:
Expand All @@ -310,10 +228,11 @@ func buildRationale(finalComplexity string, l1Count int, l2Count int, l3Count in
return ""
}

func buildCategorySummary(issues []AssessmentIssue) []MigrationComplexityCategorySummary {
func buildCategorySummary(sourceDBType string, issues []AssessmentIssue) []MigrationComplexityCategorySummary {
if len(issues) == 0 {
return nil

} else if sourceDBType != POSTGRESQL {
return nil
}

summaryMap := make(map[string]*MigrationComplexityCategorySummary)
Expand Down
40 changes: 38 additions & 2 deletions yb-voyager/cmd/templates/migration_assessment_report.template
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,44 @@

{{if and (ne .MigrationComplexity "NOT AVAILABLE") (ne (len .MigrationComplexityExplanation) 0)}}
<h2>Migration Complexity Explanation</h2>
<p>{{ .MigrationComplexityExplanation }}</p>
{{end}}
{{- if .MigrationComplexityCategorySummary }}
<p>Below is a breakdown of the issues detected in different categories for each impact level.</p>
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">
<thead>
<tr>
<th>Category</th>
<th>Level 1</th>
<th>Level 2</th>
<th>Level 3</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{{ range $idx, $summary := .MigrationComplexityCategorySummary }}
<tr>
<td>{{ $summary.Category }}</td>
<td>{{ index $summary.ImpactCounts "LEVEL_1" }}</td>
<td>{{ index $summary.ImpactCounts "LEVEL_2" }}</td>
<td>{{ index $summary.ImpactCounts "LEVEL_3" }}</td>
<td>{{ $summary.TotalIssueCount }}</td>
</tr>
{{- end }}
</tbody>
</table>
{{- end }}

<p>
<strong>Complexity:</strong> {{ .MigrationComplexity }}</br>
<strong>Reasoning:</strong> {{ .MigrationComplexityExplanation }}
</p>

<p>
<strong>Impact Levels:</strong></br>
Level 1: Resolutions are available with minimal effort.<br/>
Level 2: Resolutions are available requiring moderate effort.<br/>
Level 3: Resolutions may not be available or are complex.
</p>
{{end}}

<h2>Assessment Issues</h2>
{{ if .Issues }}
Expand Down

0 comments on commit 9f09326

Please sign in to comment.