1
0
mirror of https://github.com/bitwarden/server synced 2025-12-25 12:43:14 +00:00
Files
server/util/Seeder/Migration/Reporters/VerificationReporter.cs
2025-11-07 15:57:00 -08:00

300 lines
12 KiB
C#

using Bit.Seeder.Migration.Models;
using Microsoft.Extensions.Logging;
namespace Bit.Seeder.Migration.Reporters;
public class VerificationReporter(ILogger<VerificationReporter> logger)
{
private readonly ILogger<VerificationReporter> _logger = logger;
private readonly List<TableVerificationStats> _tableStats = [];
// ANSI color codes for console output
private const string ColorGreen = "\x1b[32m";
private const string ColorRed = "\x1b[31m";
private const string ColorYellow = "\x1b[33m";
private const string ColorBlue = "\x1b[34m";
private const string ColorBold = "\x1b[1m";
private const string ColorReset = "\x1b[0m";
// Separator constants for logging
private const string Separator = "================================================================================";
private const string ShortSeparator = "----------------------------------------";
public void StartVerification()
{
_tableStats.Clear();
Console.WriteLine(Separator);
Console.WriteLine($"{ColorBold}Starting Import Verification{ColorReset}");
Console.WriteLine(Separator);
}
public void VerifyTable(
string tableName,
string destinationTable,
int csvRowCount,
int databaseRowCount,
string? errorMessage = null)
{
var status = DetermineStatus(csvRowCount, databaseRowCount, errorMessage);
var stats = new TableVerificationStats
{
TableName = tableName,
DestinationTable = destinationTable,
CsvRowCount = csvRowCount,
DatabaseRowCount = databaseRowCount,
Status = status,
ErrorMessage = errorMessage
};
_tableStats.Add(stats);
// Log verification result
var statusColor = status switch
{
VerificationStatus.Verified => ColorGreen,
VerificationStatus.Mismatch => ColorRed,
VerificationStatus.Missing => ColorYellow,
VerificationStatus.Error => ColorRed,
_ => ColorReset
};
var statusSymbol = status switch
{
VerificationStatus.Verified => "✓",
VerificationStatus.Mismatch => "✗",
VerificationStatus.Missing => "?",
VerificationStatus.Error => "!",
_ => "?"
};
Console.WriteLine($"\n{ColorBlue}[TABLE]{ColorReset} {ColorBold}{tableName}{ColorReset} -> {destinationTable}");
Console.WriteLine($"{statusColor}{statusSymbol} Status:{ColorReset} {status}");
Console.WriteLine($"CSV rows: {csvRowCount:N0}");
Console.WriteLine($"Database rows: {databaseRowCount:N0}");
if (stats.RowDifference != 0)
{
var diffColor = stats.RowDifference > 0 ? ColorGreen : ColorRed;
Console.WriteLine($"Difference: {diffColor}{stats.RowDifference:+#;-#;0}{ColorReset}");
}
if (!string.IsNullOrEmpty(errorMessage))
{
Console.WriteLine($"{ColorRed}Error: {errorMessage}{ColorReset}");
}
}
public void FinishVerification()
{
PrintVerificationReport();
}
public VerificationSummaryStats GetSummaryStats()
{
return new VerificationSummaryStats
{
TotalTables = _tableStats.Count,
VerifiedTables = _tableStats.Count(t => t.Status == VerificationStatus.Verified),
MismatchedTables = _tableStats.Count(t => t.Status == VerificationStatus.Mismatch),
MissingTables = _tableStats.Count(t => t.Status == VerificationStatus.Missing),
ErrorTables = _tableStats.Count(t => t.Status == VerificationStatus.Error)
};
}
public List<TableVerificationStats> GetTableStats() => _tableStats.ToList();
public void PrintVerificationReport()
{
var summary = GetSummaryStats();
Console.WriteLine($"\n{Separator}");
Console.WriteLine($"{ColorBold}Verification Summary Report{ColorReset}");
Console.WriteLine(Separator);
// Overall statistics
Console.WriteLine($"\n{ColorBold}Overall Statistics:{ColorReset}");
Console.WriteLine($" Total tables: {summary.TotalTables}");
Console.WriteLine($" {ColorGreen}✓ Verified:{ColorReset} {summary.VerifiedTables}");
if (summary.MismatchedTables > 0)
Console.WriteLine($" {ColorRed}✗ Mismatched:{ColorReset} {summary.MismatchedTables}");
if (summary.MissingTables > 0)
Console.WriteLine($" {ColorYellow}? Missing:{ColorReset} {summary.MissingTables}");
if (summary.ErrorTables > 0)
Console.WriteLine($" {ColorRed}! Errors:{ColorReset} {summary.ErrorTables}");
Console.WriteLine($" Success rate: {summary.SuccessRate:F1}%");
// Per-table details
if (_tableStats.Count > 0)
{
// Calculate dynamic column widths based on actual data
var maxTableNameLength = _tableStats.Max(t => t.TableName.Length);
var tableColumnWidth = Math.Max(30, maxTableNameLength + 2); // Minimum 30, add 2 for padding
// Calculate max text lengths for numeric columns
var maxCsvTextLength = _tableStats.Max(t => $"{t.CsvRowCount:N0}".Length);
var maxDbTextLength = _tableStats.Max(t => $"{t.DatabaseRowCount:N0}".Length);
var csvColumnWidth = Math.Max(10, maxCsvTextLength + 2); // Minimum 10, add 2 for padding
var dbColumnWidth = Math.Max(10, maxDbTextLength + 2); // Minimum 10, add 2 for padding
// Calculate total width for dynamic separator
// tableColumnWidth + space + 12 (status) + space + csvColumnWidth + space + dbColumnWidth + space + 10 (diff)
var totalWidth = tableColumnWidth + 1 + 12 + 1 + csvColumnWidth + 1 + dbColumnWidth + 1 + 10;
var dynamicSeparator = new string('=', totalWidth);
Console.WriteLine($"\n{ColorBold}Per-Table Details:{ColorReset}");
Console.WriteLine(dynamicSeparator);
Console.WriteLine($"{"Table".PadRight(tableColumnWidth)} {"Status".PadRight(12)} {"CSV Rows".PadLeft(csvColumnWidth)} {"DB Rows".PadLeft(dbColumnWidth)} {"Diff",10}");
Console.WriteLine(dynamicSeparator);
foreach (var stats in _tableStats.OrderBy(t => t.TableName))
{
var statusColor = stats.Status switch
{
VerificationStatus.Verified => ColorGreen,
VerificationStatus.Mismatch => ColorRed,
VerificationStatus.Missing => ColorYellow,
VerificationStatus.Error => ColorRed,
_ => ColorReset
};
var statusText = $"{statusColor}{stats.Status.ToString().PadRight(12)}{ColorReset}";
var csvText = $"{stats.CsvRowCount:N0}";
var dbText = $"{stats.DatabaseRowCount:N0}";
var diffText = stats.RowDifference != 0
? $"{(stats.RowDifference > 0 ? ColorGreen : ColorRed)}{stats.RowDifference:+#;-#;0}{ColorReset}"
: "0";
Console.WriteLine($"{stats.TableName.PadRight(tableColumnWidth)} {statusText} {csvText.PadLeft(csvColumnWidth)} {dbText.PadLeft(dbColumnWidth)} {diffText,10}");
if (!string.IsNullOrEmpty(stats.ErrorMessage))
{
Console.WriteLine($" {ColorRed}→ {stats.ErrorMessage}{ColorReset}");
}
}
Console.WriteLine(dynamicSeparator);
}
// Problem tables
var problemTables = _tableStats
.Where(t => t.Status != VerificationStatus.Verified)
.ToList();
if (problemTables.Count > 0)
{
Console.WriteLine($"\n{ColorRed}{ColorBold}Tables Needing Attention:{ColorReset}");
foreach (var problem in problemTables)
{
var issueType = problem.Status switch
{
VerificationStatus.Mismatch => "Row count mismatch",
VerificationStatus.Missing => "CSV file not found",
VerificationStatus.Error => "Verification error",
_ => "Unknown issue"
};
Console.WriteLine($" • {problem.TableName}: {issueType}");
if (problem.Status == VerificationStatus.Mismatch)
{
Console.WriteLine($" Expected: {problem.CsvRowCount:N0}, Found: {problem.DatabaseRowCount:N0}");
}
if (!string.IsNullOrEmpty(problem.ErrorMessage))
{
Console.WriteLine($" Error: {problem.ErrorMessage}");
}
}
}
Console.WriteLine($"\n{Separator}");
// Final status
if (summary.MismatchedTables == 0 && summary.ErrorTables == 0 && summary.MissingTables == 0)
{
Console.WriteLine($"{ColorGreen}{ColorBold}✓ All tables verified successfully!{ColorReset}");
}
else
{
var problemCount = summary.MismatchedTables + summary.ErrorTables + summary.MissingTables;
Console.WriteLine($"{ColorRed}{ColorBold}✗ Verification completed with {problemCount} issue(s){ColorReset}");
}
Console.WriteLine($"{Separator}\n");
}
public void ExportReport(string filePath)
{
try
{
using var writer = new StreamWriter(filePath);
var summary = GetSummaryStats();
writer.WriteLine("Database Verification Report");
writer.WriteLine($"Generated: {DateTime.Now}");
writer.WriteLine(new string('=', 80));
writer.WriteLine();
writer.WriteLine("Overall Statistics:");
writer.WriteLine($" Total tables: {summary.TotalTables}");
writer.WriteLine($" Verified: {summary.VerifiedTables}");
writer.WriteLine($" Mismatched: {summary.MismatchedTables}");
writer.WriteLine($" Missing: {summary.MissingTables}");
writer.WriteLine($" Errors: {summary.ErrorTables}");
writer.WriteLine($" Success rate: {summary.SuccessRate:F1}%");
writer.WriteLine();
// Calculate dynamic column width based on longest table name
var maxTableNameLength = _tableStats.Max(t => t.TableName.Length);
var tableColumnWidth = Math.Max(30, maxTableNameLength + 2); // Minimum 30, add 2 for padding
writer.WriteLine("Per-Table Details:");
writer.WriteLine(new string('-', 80));
writer.WriteLine($"{"Table".PadRight(tableColumnWidth)} {"Status",-12} {"CSV Rows",12} {"DB Rows",12} {"Diff",10}");
writer.WriteLine(new string('-', 80));
foreach (var stats in _tableStats.OrderBy(t => t.TableName))
{
var csvText = $"{stats.CsvRowCount:N0}";
var dbText = $"{stats.DatabaseRowCount:N0}";
var diffText = stats.RowDifference != 0 ? $"{stats.RowDifference:+#;-#;0}" : "0";
writer.WriteLine($"{stats.TableName.PadRight(tableColumnWidth)} {stats.Status,-12} {csvText,12} {dbText,12} {diffText,10}");
if (!string.IsNullOrEmpty(stats.ErrorMessage))
{
writer.WriteLine($" Error: {stats.ErrorMessage}");
}
}
writer.WriteLine(new string('-', 80));
_logger.LogInformation("Verification report exported to: {FilePath}", filePath);
}
catch (Exception ex)
{
_logger.LogError("Failed to export report: {Message}", ex.Message);
}
}
private static VerificationStatus DetermineStatus(int csvRowCount, int databaseRowCount, string? errorMessage)
{
if (!string.IsNullOrEmpty(errorMessage))
return VerificationStatus.Error;
if (csvRowCount < 0)
return VerificationStatus.Missing;
if (csvRowCount == databaseRowCount)
return VerificationStatus.Verified;
return VerificationStatus.Mismatch;
}
}