diff --git a/internal/qol/measurement.go b/internal/qol/measurement.go index bf16a01..f87ecd5 100644 --- a/internal/qol/measurement.go +++ b/internal/qol/measurement.go @@ -75,7 +75,7 @@ func (r *MeasurementRunner) runMeasurement(upstream string, domains []string, qT defer dnsClient.Close() // Setup output files - jsonPath, pcapPath := GenerateOutputPaths(r.config.OutputDir, upstream, r.config.DNSSEC, r.config.KeepAlive) + csvPath, pcapPath := GenerateOutputPaths(r.config.OutputDir, upstream, r.config.DNSSEC, r.config.KeepAlive) keepAliveStr := "" if r.config.KeepAlive { @@ -83,7 +83,7 @@ func (r *MeasurementRunner) runMeasurement(upstream string, domains []string, qT } fmt.Printf(">>> Measuring %s (dnssec=%v%s) → %s\n", upstream, r.config.DNSSEC, keepAliveStr, - strings.TrimSuffix(strings.TrimSuffix(jsonPath, ".jsonl"), r.config.OutputDir+"/")) + strings.TrimSuffix(strings.TrimSuffix(csvPath, ".csv"), r.config.OutputDir+"/")) // Setup packet capture packetCapture, err := capture.NewPacketCapture(r.config.Interface, pcapPath) @@ -93,7 +93,7 @@ func (r *MeasurementRunner) runMeasurement(upstream string, domains []string, qT defer packetCapture.Close() // Setup results writer - writer, err := results.NewMetricsWriter(jsonPath) + writer, err := results.NewMetricsWriter(csvPath) if err != nil { return err } diff --git a/internal/qol/results/writer.go b/internal/qol/results/writer.go index 8733dd1..f7b3659 100644 --- a/internal/qol/results/writer.go +++ b/internal/qol/results/writer.go @@ -1,9 +1,10 @@ package results import ( - "encoding/json" + "encoding/csv" "fmt" "os" + "strconv" "time" ) @@ -23,29 +24,69 @@ type DNSMetric struct { Error string `json:"error,omitempty"` } -// Rest stays exactly the same type MetricsWriter struct { - encoder *json.Encoder - file *os.File + writer *csv.Writer + file *os.File } func NewMetricsWriter(path string) (*MetricsWriter, error) { file, err := os.Create(path) if err != nil { - return nil, fmt.Errorf("create json output: %w", err) + return nil, fmt.Errorf("create csv output: %w", err) } + writer := csv.NewWriter(file) + + // Write CSV header + header := []string{ + "domain", "query_type", "protocol", "dnssec", "keep_alive", + "dns_server", "timestamp", "duration_ns", "duration_ms", + "request_size_bytes", "response_size_bytes", "response_code", "error", + } + + if err := writer.Write(header); err != nil { + file.Close() + return nil, fmt.Errorf("write csv header: %w", err) + } + + writer.Flush() + return &MetricsWriter{ - encoder: json.NewEncoder(file), - file: file, + writer: writer, + file: file, }, nil } func (mw *MetricsWriter) WriteMetric(metric DNSMetric) error { - return mw.encoder.Encode(metric) + record := []string{ + metric.Domain, + metric.QueryType, + metric.Protocol, + strconv.FormatBool(metric.DNSSEC), + strconv.FormatBool(metric.KeepAlive), + metric.DNSServer, + metric.Timestamp.Format(time.RFC3339), + strconv.FormatInt(metric.Duration, 10), + strconv.FormatFloat(metric.DurationMs, 'f', 3, 64), + strconv.Itoa(metric.RequestSize), + strconv.Itoa(metric.ResponseSize), + metric.ResponseCode, + metric.Error, + } + + err := mw.writer.Write(record) + if err != nil { + return err + } + + mw.writer.Flush() + return mw.writer.Error() } func (mw *MetricsWriter) Close() error { + if mw.writer != nil { + mw.writer.Flush() + } if mw.file != nil { return mw.file.Close() } diff --git a/internal/qol/utils.go b/internal/qol/utils.go index 0d4fee7..5d4cfd7 100644 --- a/internal/qol/utils.go +++ b/internal/qol/utils.go @@ -8,7 +8,7 @@ import ( "time" ) -func GenerateOutputPaths(outputDir, upstream string, dnssec, keepAlive bool) (jsonPath, pcapPath string) { +func GenerateOutputPaths(outputDir, upstream string, dnssec, keepAlive bool) (csvPath, pcapPath string) { proto := DetectProtocol(upstream) serverName := ExtractServerName(upstream) ts := time.Now().Format("20060102_1504") @@ -18,8 +18,8 @@ func GenerateOutputPaths(outputDir, upstream string, dnssec, keepAlive bool) (js base := fmt.Sprintf("%s_%s_dnssec_%s_keepalive_%s_%s", proto, sanitize(serverName), dnssecStr, keepAliveStr, ts) - return filepath.Join(outputDir, base+".jsonl"), - filepath.Join(outputDir, base+".pcap") + return filepath.Join(outputDir, base+".csv"), + filepath.Join(outputDir, base+".pcap") } func sanitize(s string) string {