using System.Globalization; namespace Azaion.Missions.E2E.Helpers; /// /// Appends one row per NFT-PERF / NFT-RES-LIM scenario to a side-channel /// CSV referenced by an environment variable. The Reporting.Cli converter /// only knows about compile-time [Trait] data — runtime measurements /// (P50/P95, MAX_FD, P95_RSS_MiB, etc.) need this separate file so /// deployment planning + trend dashboards can read them. /// /// /// File schema (idempotent header written on first append): /// Timestamp,Category,Scenario,Result,Traces,ErrorMessage /// The Traces column carries the dynamic key=value pairs the spec requires /// (e.g., "AC-3.6; P50_MS=23.4; P95_MS=41.8"); the recorder just /// joins them with semicolons — callers compose the right shape. /// public sealed class MetricCsvRecorder { private readonly string? _path; private static readonly object Lock = new(); /// name of the env var that carries the target CSV path /// (e.g., PERF_RESULTS_FILE for NFT-PERF, RESLIM_RESULTS_FILE /// for NFT-RES-LIM). When the env var is missing or whitespace, every /// call is a no-op — the recorder is intentionally /// silent inside the standard CI run. public MetricCsvRecorder(string envVar) { var v = Environment.GetEnvironmentVariable(envVar); _path = string.IsNullOrWhiteSpace(v) ? null : v; } public bool IsEnabled => _path is not null; public void Record(string category, string scenario, string result, string traces, string? errorMessage = null) { if (_path is null) return; lock (Lock) { var dir = Path.GetDirectoryName(_path); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); var newFile = !File.Exists(_path); using var sw = new StreamWriter(_path, append: true); if (newFile) sw.WriteLine("Timestamp,Category,Scenario,Result,Traces,ErrorMessage"); sw.WriteLine( $"{DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}," + $"{Csv(category)},{Csv(scenario)},{Csv(result)},{Csv(traces)},{Csv(errorMessage ?? "")}"); } } private static string Csv(string value) => value.Contains(',') || value.Contains('"') || value.Contains('\n') ? "\"" + value.Replace("\"", "\"\"", StringComparison.Ordinal) + "\"" : value; }