From 3f7e0738f15661c6d3974b75e84a15036b3a42e8 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Mon, 21 Oct 2024 00:59:05 +0530 Subject: [PATCH 01/25] initial setup with new printer and methods --- csv.go | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++++ csv_test.go | 1 + tcping.go | 14 ++++- 3 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 csv.go create mode 100644 csv_test.go diff --git a/csv.go b/csv.go new file mode 100644 index 0000000..f5c6728 --- /dev/null +++ b/csv.go @@ -0,0 +1,177 @@ +package main + +import ( + "encoding/csv" + "fmt" + "os" + "strconv" + "time" +) + +type csvPrinter struct { + writer *csv.Writer + file *os.File + filename string + headerDone bool +} + +const ( + csvTimeFormat = "2006-01-02 15:04:05.000" +) + +func newCSVPrinter(filename string, args []string) (*csvPrinter, error) { + file, err := os.Create(filename) + if err != nil { + return nil, fmt.Errorf("error creating CSV file: %w", err) + } + + writer := csv.NewWriter(file) + return &csvPrinter{ + writer: writer, + file: file, + filename: filename, + headerDone: false, + }, nil +} + +func (cp *csvPrinter) writeHeader() error { + header := []string{ + "Event Type", "Timestamp", "Address", "Hostname", "Port", + "Hostname Resolve Retries", "Total Successful Probes", "Total Unsuccessful Probes", + "Never Succeed Probe", "Never Failed Probe", "Last Successful Probe", + "Last Unsuccessful Probe", "Total Packets", "Total Packet Loss", + "Total Uptime", "Total Downtime", "Longest Uptime", "Longest Uptime Start", + "Longest Uptime End", "Longest Downtime", "Longest Downtime Start", + "Longest Downtime End", "Latency Min", "Latency Avg", "Latency Max", + "Start Time", "End Time", "Total Duration", + } + return cp.writer.Write(header) +} + +func (cp *csvPrinter) saveStats(tcping tcping) error { + if !cp.headerDone { + if err := cp.writeHeader(); err != nil { + return fmt.Errorf("error writing CSV header: %w", err) + } + cp.headerDone = true + } + + totalPackets := tcping.totalSuccessfulProbes + tcping.totalUnsuccessfulProbes + packetLoss := float64(tcping.totalUnsuccessfulProbes) / float64(totalPackets) * 100 + + lastSuccessfulProbe := tcping.lastSuccessfulProbe.Format(csvTimeFormat) + lastUnsuccessfulProbe := tcping.lastUnsuccessfulProbe.Format(csvTimeFormat) + if tcping.lastSuccessfulProbe.IsZero() { + lastSuccessfulProbe = "" + } + if tcping.lastUnsuccessfulProbe.IsZero() { + lastUnsuccessfulProbe = "" + } + + longestUptimeDuration := tcping.longestUptime.duration.String() + longestUptimeStart := tcping.longestUptime.start.Format(csvTimeFormat) + longestUptimeEnd := tcping.longestUptime.end.Format(csvTimeFormat) + longestDowntimeDuration := tcping.longestDowntime.duration.String() + longestDowntimeStart := tcping.longestDowntime.start.Format(csvTimeFormat) + longestDowntimeEnd := tcping.longestDowntime.end.Format(csvTimeFormat) + + totalDuration := time.Since(tcping.startTime).String() + if !tcping.endTime.IsZero() { + totalDuration = tcping.endTime.Sub(tcping.startTime).String() + } + + row := []string{ + "statistics", + time.Now().Format(csvTimeFormat), + tcping.userInput.ip.String(), + tcping.userInput.hostname, + strconv.Itoa(int(tcping.userInput.port)), + strconv.Itoa(int(tcping.retriedHostnameLookups)), + strconv.Itoa(int(tcping.totalSuccessfulProbes)), + strconv.Itoa(int(tcping.totalUnsuccessfulProbes)), + strconv.FormatBool(tcping.lastSuccessfulProbe.IsZero()), + strconv.FormatBool(tcping.lastUnsuccessfulProbe.IsZero()), + lastSuccessfulProbe, + lastUnsuccessfulProbe, + strconv.Itoa(int(totalPackets)), + fmt.Sprintf("%.2f", packetLoss), + tcping.totalUptime.String(), + tcping.totalDowntime.String(), + longestUptimeDuration, + longestUptimeStart, + longestUptimeEnd, + longestDowntimeDuration, + longestDowntimeStart, + longestDowntimeEnd, + fmt.Sprintf("%.3f", tcping.rttResults.min), + fmt.Sprintf("%.3f", tcping.rttResults.average), + fmt.Sprintf("%.3f", tcping.rttResults.max), + tcping.startTime.Format(csvTimeFormat), + tcping.endTime.Format(csvTimeFormat), + totalDuration, + } + + return cp.writer.Write(row) +} + +func (cp *csvPrinter) saveHostNameChange(h []hostnameChange) error { + for _, host := range h { + if host.Addr.String() == "" { + continue + } + row := []string{ + "hostname change", + host.When.Format(csvTimeFormat), + host.Addr.String(), + "", // Empty fields for consistency with stats rows + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", + } + if err := cp.writer.Write(row); err != nil { + return err + } + } + return nil +} + +func (cp *csvPrinter) printStart(hostname string, port uint16) { + fmt.Printf("TCPinging %s on port %d\n", hostname, port) +} + +func (cp *csvPrinter) printStatistics(tcping tcping) { + err := cp.saveStats(tcping) + if err != nil { + cp.printError("\nError while writing stats to the CSV file %q\nerr: %s", cp.filename, err) + } + + if !tcping.endTime.IsZero() { + err = cp.saveHostNameChange(tcping.hostnameChanges) + if err != nil { + cp.printError("\nError while writing hostname changes to the CSV file %q\nerr: %s", cp.filename, err) + } + } + + cp.writer.Flush() + if err := cp.writer.Error(); err != nil { + cp.printError("Error flushing CSV writer: %v", err) + } + + fmt.Printf("\nStatistics for %q have been saved to %q\n", tcping.userInput.hostname, cp.filename) +} + +func (cp *csvPrinter) printError(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(1) +} + +func (cp *csvPrinter) Close() error { + cp.writer.Flush() + return cp.file.Close() +} + +// Satisfying the "printer" interface. +func (db *csvPrinter) printProbeSuccess(hostname, ip string, port uint16, streak uint, rtt float32) {} +func (db *csvPrinter) printProbeFail(hostname, ip string, port uint16, streak uint) {} +func (db *csvPrinter) printRetryingToResolve(hostname string) {} +func (db *csvPrinter) printTotalDownTime(downtime time.Duration) {} +func (db *csvPrinter) printVersion() {} +func (db *csvPrinter) printInfo(format string, args ...any) {} diff --git a/csv_test.go b/csv_test.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/csv_test.go @@ -0,0 +1 @@ +package main diff --git a/tcping.go b/tcping.go index 799b9bb..f13bcc8 100644 --- a/tcping.go +++ b/tcping.go @@ -18,7 +18,7 @@ import ( "github.com/google/go-github/v45/github" ) -var version = "" // set at compile time +var version = "" // set at compile time const ( owner = "pouriyajamshidi" @@ -228,7 +228,7 @@ func usage() { } // setPrinter selects the printer -func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, timeStamp *bool, outputDb *string, args []string) { +func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, timeStamp *bool, outputDb, outputCSV *string, args []string) { if *prettyJSON && !*outputJSON { colorRed("--pretty has no effect without the -j flag.") usage() @@ -237,6 +237,13 @@ func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, timeStamp *bool, o tcping.printer = newJSONPrinter(*prettyJSON) } else if *outputDb != "" { tcping.printer = newDB(*outputDb, args) + } else if *outputCSV != "" { + var err error + tcping.printer, err = newCSVPrinter(*outputCSV, args) + if err != nil { + tcping.printError("Failed to create CSV file: %s", err) + os.Exit(1) + } } else { tcping.printer = newPlanePrinter(timeStamp) } @@ -324,6 +331,7 @@ func processUserInput(tcping *tcping) { outputJSON := flag.Bool("j", false, "output in JSON format.") prettyJSON := flag.Bool("pretty", false, "use indentation when using json output format. No effect without the '-j' flag.") showTimestamp := flag.Bool("D", false, "show timestamp in output.") + saveToCSV := flag.String("csv", "", "path and file name to store tcping output to CSV file.") showVer := flag.Bool("v", false, "show version.") checkUpdates := flag.Bool("u", false, "check for updates and exit.") secondsBetweenProbes := flag.Float64("i", 1, "interval between sending probes. Real number allowed with dot as a decimal separator. The default is one second") @@ -343,7 +351,7 @@ func processUserInput(tcping *tcping) { // we need to set printers first, because they're used for // error reporting and other output. - setPrinter(tcping, outputJSON, prettyJSON, showTimestamp, outputDB, args) + setPrinter(tcping, outputJSON, prettyJSON, showTimestamp, outputDB, saveToCSV, args) // Handle -v flag if *showVer { From aca733cb419a58a3f47deed3a715b01820ec71b3 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Mon, 28 Oct 2024 01:56:42 +0530 Subject: [PATCH 02/25] add some testing --- csv_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ google.com | 0 output.csv | 3 +++ test.csvs | 0 4 files changed, 46 insertions(+) create mode 100644 google.com create mode 100644 output.csv create mode 100644 test.csvs diff --git a/csv_test.go b/csv_test.go index 06ab7d0..59413ff 100644 --- a/csv_test.go +++ b/csv_test.go @@ -1 +1,44 @@ package main + +import ( + "os" + "testing" +) + +func TestNewCSVPrinter(t *testing.T) { + args := []string{"localhost", "8001"} + filename := "test.csv" + + cp, err := newCSVPrinter(filename, args) + if err != nil { + t.Fatalf("error creating CSV printer: %v", err) + } + + // Ensure file is closed even if the test fails + defer func() { + if cp != nil && cp.file != nil { + cp.file.Close() + } + // Clean up the created file + os.Remove(filename) + }() + + // Check if file was created + if _, err := os.Stat(filename); os.IsNotExist(err) { + t.Errorf("file %s was not created", filename) + } + + if cp.filename != filename { + t.Errorf("expected filename %q, got %q", filename, cp.filename) + } + + if cp.headerDone { + t.Errorf("expected headerDone to be false, got true") + } + + // Check if writer is initialized + if cp.writer == nil { + t.Errorf("CSV writer was not initialized") + } + +} diff --git a/google.com b/google.com new file mode 100644 index 0000000..e69de29 diff --git a/output.csv b/output.csv new file mode 100644 index 0000000..b8f8300 --- /dev/null +++ b/output.csv @@ -0,0 +1,3 @@ +Event Type,Timestamp,Address,Hostname,Port,Hostname Resolve Retries,Total Successful Probes,Total Unsuccessful Probes,Never Succeed Probe,Never Failed Probe,Last Successful Probe,Last Unsuccessful Probe,Total Packets,Total Packet Loss,Total Uptime,Total Downtime,Longest Uptime,Longest Uptime Start,Longest Uptime End,Longest Downtime,Longest Downtime Start,Longest Downtime End,Latency Min,Latency Avg,Latency Max,Start Time,End Time,Total Duration +statistics,2024-10-28 01:08:51.062,142.250.192.46,google.com,80,0,123,5,false,false,2024-10-28 01:08:50.645,2024-10-28 01:06:55.644,128,3.91,2m3s,5s,1m54.417312208s,2024-10-28 01:06:56.644,2024-10-28 01:08:51.061,4.999738708s,2024-10-28 01:06:51.644,2024-10-28 01:06:56.644,21.771,46.909,806.623,2024-10-28 01:06:43.643,2024-10-28 01:08:51.062,2m7.417743708s +hostname change,2024-10-28 01:06:43.643,142.250.192.46,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/test.csvs b/test.csvs new file mode 100644 index 0000000..e69de29 From d3e23cf13121e535b3b69cf9549b862f81a3e344 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Mon, 28 Oct 2024 01:58:55 +0530 Subject: [PATCH 03/25] nits --- google.com | 0 output.csv | 3 --- 2 files changed, 3 deletions(-) delete mode 100644 google.com delete mode 100644 output.csv diff --git a/google.com b/google.com deleted file mode 100644 index e69de29..0000000 diff --git a/output.csv b/output.csv deleted file mode 100644 index b8f8300..0000000 --- a/output.csv +++ /dev/null @@ -1,3 +0,0 @@ -Event Type,Timestamp,Address,Hostname,Port,Hostname Resolve Retries,Total Successful Probes,Total Unsuccessful Probes,Never Succeed Probe,Never Failed Probe,Last Successful Probe,Last Unsuccessful Probe,Total Packets,Total Packet Loss,Total Uptime,Total Downtime,Longest Uptime,Longest Uptime Start,Longest Uptime End,Longest Downtime,Longest Downtime Start,Longest Downtime End,Latency Min,Latency Avg,Latency Max,Start Time,End Time,Total Duration -statistics,2024-10-28 01:08:51.062,142.250.192.46,google.com,80,0,123,5,false,false,2024-10-28 01:08:50.645,2024-10-28 01:06:55.644,128,3.91,2m3s,5s,1m54.417312208s,2024-10-28 01:06:56.644,2024-10-28 01:08:51.061,4.999738708s,2024-10-28 01:06:51.644,2024-10-28 01:06:56.644,21.771,46.909,806.623,2024-10-28 01:06:43.643,2024-10-28 01:08:51.062,2m7.417743708s -hostname change,2024-10-28 01:06:43.643,142.250.192.46,,,,,,,,,,,,,,,,,,,,,,,,, From ac98cf2c4bfae8a311f0bc7ebe4683b4623f3fa6 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Mon, 28 Oct 2024 01:59:12 +0530 Subject: [PATCH 04/25] nits --- test.csvs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test.csvs diff --git a/test.csvs b/test.csvs deleted file mode 100644 index e69de29..0000000 From 6d04f04fbc4d07bb895b41c42a4afee62772a53c Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Thu, 31 Oct 2024 15:56:59 +0530 Subject: [PATCH 05/25] add tests --- csv.go | 5 +- csv_test.go | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 222 insertions(+), 5 deletions(-) diff --git a/csv.go b/csv.go index f5c6728..04bf5d2 100644 --- a/csv.go +++ b/csv.go @@ -6,6 +6,8 @@ import ( "os" "strconv" "time" + + "github.com/davecgh/go-spew/spew" ) type csvPrinter struct { @@ -116,7 +118,7 @@ func (cp *csvPrinter) saveStats(tcping tcping) error { func (cp *csvPrinter) saveHostNameChange(h []hostnameChange) error { for _, host := range h { - if host.Addr.String() == "" { + if !host.Addr.IsValid() { continue } row := []string{ @@ -126,6 +128,7 @@ func (cp *csvPrinter) saveHostNameChange(h []hostnameChange) error { "", // Empty fields for consistency with stats rows "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", } + spew.Dump(row) if err := cp.writer.Write(row); err != nil { return err } diff --git a/csv_test.go b/csv_test.go index 59413ff..3c842ca 100644 --- a/csv_test.go +++ b/csv_test.go @@ -1,8 +1,12 @@ package main import ( + "encoding/csv" + "net/netip" "os" + "reflect" "testing" + "time" ) func TestNewCSVPrinter(t *testing.T) { @@ -14,16 +18,13 @@ func TestNewCSVPrinter(t *testing.T) { t.Fatalf("error creating CSV printer: %v", err) } - // Ensure file is closed even if the test fails defer func() { if cp != nil && cp.file != nil { cp.file.Close() } - // Clean up the created file os.Remove(filename) }() - // Check if file was created if _, err := os.Stat(filename); os.IsNotExist(err) { t.Errorf("file %s was not created", filename) } @@ -36,9 +37,222 @@ func TestNewCSVPrinter(t *testing.T) { t.Errorf("expected headerDone to be false, got true") } - // Check if writer is initialized if cp.writer == nil { t.Errorf("CSV writer was not initialized") } } + +func TestCSVPrinterWriteHeader(t *testing.T) { + tempFile, err := os.CreateTemp("", "test_csv_*.csv") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + + cp := &csvPrinter{ + file: tempFile, + writer: csv.NewWriter(tempFile), + } + + err = cp.writeHeader() + if err != nil { + t.Fatalf("writeHeader() failed: %v", err) + } + + cp.writer.Flush() + if err := cp.writer.Error(); err != nil { + t.Fatalf("Error flushing CSV writer: %v", err) + } + + tempFile.Seek(0, 0) + reader := csv.NewReader(tempFile) + header, err := reader.Read() + if err != nil { + t.Fatalf("Failed to read CSV header: %v", err) + } + + expectedHeader := []string{ + "Event Type", "Timestamp", "Address", "Hostname", "Port", + "Hostname Resolve Retries", "Total Successful Probes", "Total Unsuccessful Probes", + "Never Succeed Probe", "Never Failed Probe", "Last Successful Probe", + "Last Unsuccessful Probe", "Total Packets", "Total Packet Loss", + "Total Uptime", "Total Downtime", "Longest Uptime", "Longest Uptime Start", + "Longest Uptime End", "Longest Downtime", "Longest Downtime Start", + "Longest Downtime End", "Latency Min", "Latency Avg", "Latency Max", + "Start Time", "End Time", "Total Duration", + } + + if !reflect.DeepEqual(header, expectedHeader) { + t.Errorf("Header mismatch.\nExpected: %v\nGot: %v", expectedHeader, header) + } +} + +func TestCSVPrinterSaveStats(t *testing.T) { + tempFile, err := os.CreateTemp("", "test_csv_*.csv") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + defer tempFile.Close() + + cp := &csvPrinter{ + file: tempFile, + writer: csv.NewWriter(tempFile), + headerDone: false, + } + + now := time.Now() + sampleTcping := tcping{ + userInput: userInput{ + ip: netip.MustParseAddr("192.168.1.1"), + hostname: "example.com", + port: 80, + }, + startTime: now.Add(-time.Hour), + endTime: now, + lastSuccessfulProbe: now.Add(-time.Minute), + lastUnsuccessfulProbe: now, + longestUptime: longestTime{duration: 50 * time.Second, start: now.Add(-time.Hour), end: now.Add(-59 * time.Minute)}, + longestDowntime: longestTime{duration: 3 * time.Second, start: now.Add(-30 * time.Minute), end: now.Add(-29*time.Minute - 57*time.Second)}, + totalUptime: 55 * time.Minute, + totalDowntime: 5 * time.Minute, + totalSuccessfulProbes: 95, + totalUnsuccessfulProbes: 5, + retriedHostnameLookups: 2, + rttResults: rttResult{min: 10.5, max: 20.1, average: 15.3, hasResults: true}, + } + + err = cp.saveStats(sampleTcping) + if err != nil { + t.Fatalf("saveStats() failed: %v", err) + } + + cp.writer.Flush() + if err := cp.writer.Error(); err != nil { + t.Fatalf("Error flushing CSV writer: %v", err) + } + + tempFile.Seek(0, 0) + reader := csv.NewReader(tempFile) + + header, err := reader.Read() + if err != nil { + t.Fatalf("Failed to read CSV header: %v", err) + } + expectedHeader := []string{ + "Event Type", "Timestamp", "Address", "Hostname", "Port", + "Hostname Resolve Retries", "Total Successful Probes", "Total Unsuccessful Probes", + "Never Succeed Probe", "Never Failed Probe", "Last Successful Probe", + "Last Unsuccessful Probe", "Total Packets", "Total Packet Loss", + "Total Uptime", "Total Downtime", "Longest Uptime", "Longest Uptime Start", + "Longest Uptime End", "Longest Downtime", "Longest Downtime Start", + "Longest Downtime End", "Latency Min", "Latency Avg", "Latency Max", + "Start Time", "End Time", "Total Duration", + } + if !reflect.DeepEqual(header, expectedHeader) { + t.Errorf("Header mismatch.\nExpected: %v\nGot: %v", expectedHeader, header) + } + + row, err := reader.Read() + if err != nil { + t.Fatalf("Failed to read CSV data row: %v", err) + } + + if row[0] != "statistics" { + t.Errorf("Expected event type 'statistics', got '%s'", row[0]) + } + if row[2] != "192.168.1.1" { + t.Errorf("Expected IP '192.168.1.1', got '%s'", row[2]) + } + if row[3] != "example.com" { + t.Errorf("Expected hostname 'example.com', got '%s'", row[3]) + } + if row[4] != "80" { + t.Errorf("Expected port '80', got '%s'", row[4]) + } + if row[6] != "95" { + t.Errorf("Expected 95 successful probes, got '%s'", row[6]) + } + if row[7] != "5" { + t.Errorf("Expected 5 unsuccessful probes, got '%s'", row[7]) + } +} + +func TestCSVPrinterSaveHostNameChange(t *testing.T) { + tempFile, err := os.CreateTemp("", "test_csv_*.csv") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + defer tempFile.Close() + + testCases := []struct { + name string + changes []hostnameChange + expected int + }{ + { + name: "Empty slice", + changes: []hostnameChange{}, + expected: 0, + }, + { + name: "Valid entries", + changes: []hostnameChange{ + {Addr: netip.MustParseAddr("192.168.1.1"), When: time.Now()}, + {Addr: netip.MustParseAddr("192.168.1.2"), When: time.Now().Add(time.Hour)}, + }, + expected: 2, + }, + { + name: "Mixed valid and invalid entries", + changes: []hostnameChange{ + {Addr: netip.MustParseAddr("192.168.1.1"), When: time.Now()}, + {Addr: netip.Addr{}, When: time.Now()}, + {Addr: netip.MustParseAddr("192.168.1.2"), When: time.Now().Add(time.Hour)}, + }, + expected: 2, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Reset file for each test case + tempFile.Truncate(0) + tempFile.Seek(0, 0) + + cp := &csvPrinter{ + file: tempFile, + writer: csv.NewWriter(tempFile), + } + + err := cp.saveHostNameChange(tc.changes) + if err != nil { + t.Fatalf("saveHostNameChange failed: %v", err) + } + + cp.writer.Flush() + tempFile.Seek(0, 0) + + reader := csv.NewReader(tempFile) + rows, err := reader.ReadAll() + if err != nil { + t.Fatalf("Failed to read CSV: %v", err) + } + + if len(rows) != tc.expected { + t.Errorf("Expected %d rows, got %d", tc.expected, len(rows)) + } + + for _, row := range rows { + if row[0] != "hostname change" { + t.Errorf("Expected 'hostname change', got %s", row[0]) + } + if row[2] == "" { + t.Errorf("IP address should not be empty") + } + } + }) + } +} From 0b00747c2b66e81719cea6e13878f10f4be31e34 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Thu, 31 Oct 2024 16:21:54 +0530 Subject: [PATCH 06/25] add a setup function --- csv_test.go | 86 ++++++++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/csv_test.go b/csv_test.go index 3c842ca..369d666 100644 --- a/csv_test.go +++ b/csv_test.go @@ -9,6 +9,27 @@ import ( "time" ) +// setupTempCSVFile creates a temporary CSV file and returns a csvPrinter and a cleanup function +func setupTempCSVFile(t *testing.T) (*csvPrinter, func()) { + t.Helper() + tempFile, err := os.CreateTemp("", "test_csv_*.csv") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + + cp := &csvPrinter{ + file: tempFile, + writer: csv.NewWriter(tempFile), + } + + cleanup := func() { + tempFile.Close() + os.Remove(tempFile.Name()) + } + + return cp, cleanup +} + func TestNewCSVPrinter(t *testing.T) { args := []string{"localhost", "8001"} filename := "test.csv" @@ -40,22 +61,13 @@ func TestNewCSVPrinter(t *testing.T) { if cp.writer == nil { t.Errorf("CSV writer was not initialized") } - } func TestCSVPrinterWriteHeader(t *testing.T) { - tempFile, err := os.CreateTemp("", "test_csv_*.csv") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(tempFile.Name()) + cp, cleanup := setupTempCSVFile(t) + defer cleanup() - cp := &csvPrinter{ - file: tempFile, - writer: csv.NewWriter(tempFile), - } - - err = cp.writeHeader() + err := cp.writeHeader() if err != nil { t.Fatalf("writeHeader() failed: %v", err) } @@ -65,8 +77,8 @@ func TestCSVPrinterWriteHeader(t *testing.T) { t.Fatalf("Error flushing CSV writer: %v", err) } - tempFile.Seek(0, 0) - reader := csv.NewReader(tempFile) + cp.file.Seek(0, 0) + reader := csv.NewReader(cp.file) header, err := reader.Read() if err != nil { t.Fatalf("Failed to read CSV header: %v", err) @@ -89,18 +101,8 @@ func TestCSVPrinterWriteHeader(t *testing.T) { } func TestCSVPrinterSaveStats(t *testing.T) { - tempFile, err := os.CreateTemp("", "test_csv_*.csv") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(tempFile.Name()) - defer tempFile.Close() - - cp := &csvPrinter{ - file: tempFile, - writer: csv.NewWriter(tempFile), - headerDone: false, - } + cp, cleanup := setupTempCSVFile(t) + defer cleanup() now := time.Now() sampleTcping := tcping{ @@ -123,7 +125,7 @@ func TestCSVPrinterSaveStats(t *testing.T) { rttResults: rttResult{min: 10.5, max: 20.1, average: 15.3, hasResults: true}, } - err = cp.saveStats(sampleTcping) + err := cp.saveStats(sampleTcping) if err != nil { t.Fatalf("saveStats() failed: %v", err) } @@ -133,8 +135,8 @@ func TestCSVPrinterSaveStats(t *testing.T) { t.Fatalf("Error flushing CSV writer: %v", err) } - tempFile.Seek(0, 0) - reader := csv.NewReader(tempFile) + cp.file.Seek(0, 0) + reader := csv.NewReader(cp.file) header, err := reader.Read() if err != nil { @@ -178,14 +180,9 @@ func TestCSVPrinterSaveStats(t *testing.T) { t.Errorf("Expected 5 unsuccessful probes, got '%s'", row[7]) } } - func TestCSVPrinterSaveHostNameChange(t *testing.T) { - tempFile, err := os.CreateTemp("", "test_csv_*.csv") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(tempFile.Name()) - defer tempFile.Close() + cp, cleanup := setupTempCSVFile(t) + defer cleanup() testCases := []struct { name string @@ -219,13 +216,8 @@ func TestCSVPrinterSaveHostNameChange(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Reset file for each test case - tempFile.Truncate(0) - tempFile.Seek(0, 0) - - cp := &csvPrinter{ - file: tempFile, - writer: csv.NewWriter(tempFile), - } + cp.file.Truncate(0) + cp.file.Seek(0, 0) err := cp.saveHostNameChange(tc.changes) if err != nil { @@ -233,9 +225,9 @@ func TestCSVPrinterSaveHostNameChange(t *testing.T) { } cp.writer.Flush() - tempFile.Seek(0, 0) + cp.file.Seek(0, 0) - reader := csv.NewReader(tempFile) + reader := csv.NewReader(cp.file) rows, err := reader.ReadAll() if err != nil { t.Fatalf("Failed to read CSV: %v", err) @@ -249,8 +241,8 @@ func TestCSVPrinterSaveHostNameChange(t *testing.T) { if row[0] != "hostname change" { t.Errorf("Expected 'hostname change', got %s", row[0]) } - if row[2] == "" { - t.Errorf("IP address should not be empty") + if row[2] == "invalid IP" { + t.Errorf("Invalid IP should not be written to CSV") } } }) From f1b8c4de1a5e2979a286efe3cb61441d1016e83d Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Thu, 31 Oct 2024 16:25:35 +0530 Subject: [PATCH 07/25] add a setup function --- csv_test.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/csv_test.go b/csv_test.go index 369d666..21e9b14 100644 --- a/csv_test.go +++ b/csv_test.go @@ -23,8 +23,16 @@ func setupTempCSVFile(t *testing.T) (*csvPrinter, func()) { } cleanup := func() { - tempFile.Close() - os.Remove(tempFile.Name()) + cp.writer.Flush() + if err := cp.writer.Error(); err != nil { + t.Errorf("Error flushing CSV writer: %v", err) + } + if err := cp.file.Close(); err != nil { + t.Errorf("Error closing file: %v", err) + } + if err := os.Remove(cp.file.Name()); err != nil { + t.Errorf("Error removing temp file: %v", err) + } } return cp, cleanup From 175e48668230f2185ce8d2c82c49d21f94d7b77f Mon Sep 17 00:00:00 2001 From: Syed Shahidh Ilhan F <62804977+SYSHIL@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:26:58 +0530 Subject: [PATCH 08/25] Update csv_test.go --- csv_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/csv_test.go b/csv_test.go index 21e9b14..7bd0ac6 100644 --- a/csv_test.go +++ b/csv_test.go @@ -9,7 +9,6 @@ import ( "time" ) -// setupTempCSVFile creates a temporary CSV file and returns a csvPrinter and a cleanup function func setupTempCSVFile(t *testing.T) (*csvPrinter, func()) { t.Helper() tempFile, err := os.CreateTemp("", "test_csv_*.csv") From 4c18c0cc46b28052fc671bb13250122ea3ede5ec Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sun, 24 Nov 2024 13:12:04 +0530 Subject: [PATCH 09/25] write header --- csv.go | 132 ++---------------------------------- csv_test.go | 189 ---------------------------------------------------- go.mod | 2 +- 3 files changed, 6 insertions(+), 317 deletions(-) diff --git a/csv.go b/csv.go index 04bf5d2..24c52dc 100644 --- a/csv.go +++ b/csv.go @@ -4,10 +4,7 @@ import ( "encoding/csv" "fmt" "os" - "strconv" "time" - - "github.com/davecgh/go-spew/spew" ) type csvPrinter struct { @@ -38,137 +35,18 @@ func newCSVPrinter(filename string, args []string) (*csvPrinter, error) { func (cp *csvPrinter) writeHeader() error { header := []string{ - "Event Type", "Timestamp", "Address", "Hostname", "Port", - "Hostname Resolve Retries", "Total Successful Probes", "Total Unsuccessful Probes", - "Never Succeed Probe", "Never Failed Probe", "Last Successful Probe", - "Last Unsuccessful Probe", "Total Packets", "Total Packet Loss", - "Total Uptime", "Total Downtime", "Longest Uptime", "Longest Uptime Start", - "Longest Uptime End", "Longest Downtime", "Longest Downtime Start", - "Longest Downtime End", "Latency Min", "Latency Avg", "Latency Max", - "Start Time", "End Time", "Total Duration", + "probe status", "hostname", "ip", "port", "TCP_conn", "time", } return cp.writer.Write(header) } -func (cp *csvPrinter) saveStats(tcping tcping) error { - if !cp.headerDone { - if err := cp.writeHeader(); err != nil { - return fmt.Errorf("error writing CSV header: %w", err) - } - cp.headerDone = true - } - - totalPackets := tcping.totalSuccessfulProbes + tcping.totalUnsuccessfulProbes - packetLoss := float64(tcping.totalUnsuccessfulProbes) / float64(totalPackets) * 100 - - lastSuccessfulProbe := tcping.lastSuccessfulProbe.Format(csvTimeFormat) - lastUnsuccessfulProbe := tcping.lastUnsuccessfulProbe.Format(csvTimeFormat) - if tcping.lastSuccessfulProbe.IsZero() { - lastSuccessfulProbe = "" - } - if tcping.lastUnsuccessfulProbe.IsZero() { - lastUnsuccessfulProbe = "" - } - - longestUptimeDuration := tcping.longestUptime.duration.String() - longestUptimeStart := tcping.longestUptime.start.Format(csvTimeFormat) - longestUptimeEnd := tcping.longestUptime.end.Format(csvTimeFormat) - longestDowntimeDuration := tcping.longestDowntime.duration.String() - longestDowntimeStart := tcping.longestDowntime.start.Format(csvTimeFormat) - longestDowntimeEnd := tcping.longestDowntime.end.Format(csvTimeFormat) - - totalDuration := time.Since(tcping.startTime).String() - if !tcping.endTime.IsZero() { - totalDuration = tcping.endTime.Sub(tcping.startTime).String() - } - - row := []string{ - "statistics", - time.Now().Format(csvTimeFormat), - tcping.userInput.ip.String(), - tcping.userInput.hostname, - strconv.Itoa(int(tcping.userInput.port)), - strconv.Itoa(int(tcping.retriedHostnameLookups)), - strconv.Itoa(int(tcping.totalSuccessfulProbes)), - strconv.Itoa(int(tcping.totalUnsuccessfulProbes)), - strconv.FormatBool(tcping.lastSuccessfulProbe.IsZero()), - strconv.FormatBool(tcping.lastUnsuccessfulProbe.IsZero()), - lastSuccessfulProbe, - lastUnsuccessfulProbe, - strconv.Itoa(int(totalPackets)), - fmt.Sprintf("%.2f", packetLoss), - tcping.totalUptime.String(), - tcping.totalDowntime.String(), - longestUptimeDuration, - longestUptimeStart, - longestUptimeEnd, - longestDowntimeDuration, - longestDowntimeStart, - longestDowntimeEnd, - fmt.Sprintf("%.3f", tcping.rttResults.min), - fmt.Sprintf("%.3f", tcping.rttResults.average), - fmt.Sprintf("%.3f", tcping.rttResults.max), - tcping.startTime.Format(csvTimeFormat), - tcping.endTime.Format(csvTimeFormat), - totalDuration, - } - - return cp.writer.Write(row) -} - -func (cp *csvPrinter) saveHostNameChange(h []hostnameChange) error { - for _, host := range h { - if !host.Addr.IsValid() { - continue - } - row := []string{ - "hostname change", - host.When.Format(csvTimeFormat), - host.Addr.String(), - "", // Empty fields for consistency with stats rows - "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", - } - spew.Dump(row) - if err := cp.writer.Write(row); err != nil { - return err - } - } - return nil -} - -func (cp *csvPrinter) printStart(hostname string, port uint16) { - fmt.Printf("TCPinging %s on port %d\n", hostname, port) -} - -func (cp *csvPrinter) printStatistics(tcping tcping) { - err := cp.saveStats(tcping) - if err != nil { - cp.printError("\nError while writing stats to the CSV file %q\nerr: %s", cp.filename, err) - } - - if !tcping.endTime.IsZero() { - err = cp.saveHostNameChange(tcping.hostnameChanges) - if err != nil { - cp.printError("\nError while writing hostname changes to the CSV file %q\nerr: %s", cp.filename, err) - } - } - - cp.writer.Flush() - if err := cp.writer.Error(); err != nil { - cp.printError("Error flushing CSV writer: %v", err) - } - - fmt.Printf("\nStatistics for %q have been saved to %q\n", tcping.userInput.hostname, cp.filename) -} - -func (cp *csvPrinter) printError(format string, args ...interface{}) { - fmt.Fprintf(os.Stderr, format+"\n", args...) - os.Exit(1) +func (cp *csvPrinter) writeRecord(record []string) error { + return cp.writer.Write(record) } -func (cp *csvPrinter) Close() error { +func (cp *csvPrinter) close() { cp.writer.Flush() - return cp.file.Close() + cp.file.Close() } // Satisfying the "printer" interface. diff --git a/csv_test.go b/csv_test.go index 7bd0ac6..75cb4e7 100644 --- a/csv_test.go +++ b/csv_test.go @@ -2,11 +2,8 @@ package main import ( "encoding/csv" - "net/netip" "os" - "reflect" "testing" - "time" ) func setupTempCSVFile(t *testing.T) (*csvPrinter, func()) { @@ -69,189 +66,3 @@ func TestNewCSVPrinter(t *testing.T) { t.Errorf("CSV writer was not initialized") } } - -func TestCSVPrinterWriteHeader(t *testing.T) { - cp, cleanup := setupTempCSVFile(t) - defer cleanup() - - err := cp.writeHeader() - if err != nil { - t.Fatalf("writeHeader() failed: %v", err) - } - - cp.writer.Flush() - if err := cp.writer.Error(); err != nil { - t.Fatalf("Error flushing CSV writer: %v", err) - } - - cp.file.Seek(0, 0) - reader := csv.NewReader(cp.file) - header, err := reader.Read() - if err != nil { - t.Fatalf("Failed to read CSV header: %v", err) - } - - expectedHeader := []string{ - "Event Type", "Timestamp", "Address", "Hostname", "Port", - "Hostname Resolve Retries", "Total Successful Probes", "Total Unsuccessful Probes", - "Never Succeed Probe", "Never Failed Probe", "Last Successful Probe", - "Last Unsuccessful Probe", "Total Packets", "Total Packet Loss", - "Total Uptime", "Total Downtime", "Longest Uptime", "Longest Uptime Start", - "Longest Uptime End", "Longest Downtime", "Longest Downtime Start", - "Longest Downtime End", "Latency Min", "Latency Avg", "Latency Max", - "Start Time", "End Time", "Total Duration", - } - - if !reflect.DeepEqual(header, expectedHeader) { - t.Errorf("Header mismatch.\nExpected: %v\nGot: %v", expectedHeader, header) - } -} - -func TestCSVPrinterSaveStats(t *testing.T) { - cp, cleanup := setupTempCSVFile(t) - defer cleanup() - - now := time.Now() - sampleTcping := tcping{ - userInput: userInput{ - ip: netip.MustParseAddr("192.168.1.1"), - hostname: "example.com", - port: 80, - }, - startTime: now.Add(-time.Hour), - endTime: now, - lastSuccessfulProbe: now.Add(-time.Minute), - lastUnsuccessfulProbe: now, - longestUptime: longestTime{duration: 50 * time.Second, start: now.Add(-time.Hour), end: now.Add(-59 * time.Minute)}, - longestDowntime: longestTime{duration: 3 * time.Second, start: now.Add(-30 * time.Minute), end: now.Add(-29*time.Minute - 57*time.Second)}, - totalUptime: 55 * time.Minute, - totalDowntime: 5 * time.Minute, - totalSuccessfulProbes: 95, - totalUnsuccessfulProbes: 5, - retriedHostnameLookups: 2, - rttResults: rttResult{min: 10.5, max: 20.1, average: 15.3, hasResults: true}, - } - - err := cp.saveStats(sampleTcping) - if err != nil { - t.Fatalf("saveStats() failed: %v", err) - } - - cp.writer.Flush() - if err := cp.writer.Error(); err != nil { - t.Fatalf("Error flushing CSV writer: %v", err) - } - - cp.file.Seek(0, 0) - reader := csv.NewReader(cp.file) - - header, err := reader.Read() - if err != nil { - t.Fatalf("Failed to read CSV header: %v", err) - } - expectedHeader := []string{ - "Event Type", "Timestamp", "Address", "Hostname", "Port", - "Hostname Resolve Retries", "Total Successful Probes", "Total Unsuccessful Probes", - "Never Succeed Probe", "Never Failed Probe", "Last Successful Probe", - "Last Unsuccessful Probe", "Total Packets", "Total Packet Loss", - "Total Uptime", "Total Downtime", "Longest Uptime", "Longest Uptime Start", - "Longest Uptime End", "Longest Downtime", "Longest Downtime Start", - "Longest Downtime End", "Latency Min", "Latency Avg", "Latency Max", - "Start Time", "End Time", "Total Duration", - } - if !reflect.DeepEqual(header, expectedHeader) { - t.Errorf("Header mismatch.\nExpected: %v\nGot: %v", expectedHeader, header) - } - - row, err := reader.Read() - if err != nil { - t.Fatalf("Failed to read CSV data row: %v", err) - } - - if row[0] != "statistics" { - t.Errorf("Expected event type 'statistics', got '%s'", row[0]) - } - if row[2] != "192.168.1.1" { - t.Errorf("Expected IP '192.168.1.1', got '%s'", row[2]) - } - if row[3] != "example.com" { - t.Errorf("Expected hostname 'example.com', got '%s'", row[3]) - } - if row[4] != "80" { - t.Errorf("Expected port '80', got '%s'", row[4]) - } - if row[6] != "95" { - t.Errorf("Expected 95 successful probes, got '%s'", row[6]) - } - if row[7] != "5" { - t.Errorf("Expected 5 unsuccessful probes, got '%s'", row[7]) - } -} -func TestCSVPrinterSaveHostNameChange(t *testing.T) { - cp, cleanup := setupTempCSVFile(t) - defer cleanup() - - testCases := []struct { - name string - changes []hostnameChange - expected int - }{ - { - name: "Empty slice", - changes: []hostnameChange{}, - expected: 0, - }, - { - name: "Valid entries", - changes: []hostnameChange{ - {Addr: netip.MustParseAddr("192.168.1.1"), When: time.Now()}, - {Addr: netip.MustParseAddr("192.168.1.2"), When: time.Now().Add(time.Hour)}, - }, - expected: 2, - }, - { - name: "Mixed valid and invalid entries", - changes: []hostnameChange{ - {Addr: netip.MustParseAddr("192.168.1.1"), When: time.Now()}, - {Addr: netip.Addr{}, When: time.Now()}, - {Addr: netip.MustParseAddr("192.168.1.2"), When: time.Now().Add(time.Hour)}, - }, - expected: 2, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Reset file for each test case - cp.file.Truncate(0) - cp.file.Seek(0, 0) - - err := cp.saveHostNameChange(tc.changes) - if err != nil { - t.Fatalf("saveHostNameChange failed: %v", err) - } - - cp.writer.Flush() - cp.file.Seek(0, 0) - - reader := csv.NewReader(cp.file) - rows, err := reader.ReadAll() - if err != nil { - t.Fatalf("Failed to read CSV: %v", err) - } - - if len(rows) != tc.expected { - t.Errorf("Expected %d rows, got %d", tc.expected, len(rows)) - } - - for _, row := range rows { - if row[0] != "hostname change" { - t.Errorf("Expected 'hostname change', got %s", row[0]) - } - if row[2] == "invalid IP" { - t.Errorf("Invalid IP should not be written to CSV") - } - } - }) - } -} diff --git a/go.mod b/go.mod index 31031f6..109ffcc 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/pouriyajamshidi/tcping/v2 go 1.23.1 require ( + github.com/davecgh/go-spew v1.1.1 github.com/google/go-github/v45 v45.2.0 github.com/gookit/color v1.5.4 github.com/stretchr/testify v1.9.0 @@ -10,7 +11,6 @@ require ( ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect From 034fa3d1f0de513d77c2cc075a0be7e1a81b9bba Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 30 Nov 2024 19:36:48 +0530 Subject: [PATCH 10/25] print probes sucess and failures --- csv.go | 41 ++++++++++++++++++++++++++++------------- go.mod | 2 +- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/csv.go b/csv.go index 24c52dc..c19c30d 100644 --- a/csv.go +++ b/csv.go @@ -33,13 +33,6 @@ func newCSVPrinter(filename string, args []string) (*csvPrinter, error) { }, nil } -func (cp *csvPrinter) writeHeader() error { - header := []string{ - "probe status", "hostname", "ip", "port", "TCP_conn", "time", - } - return cp.writer.Write(header) -} - func (cp *csvPrinter) writeRecord(record []string) error { return cp.writer.Write(record) } @@ -49,10 +42,32 @@ func (cp *csvPrinter) close() { cp.file.Close() } +func (cp *csvPrinter) printError(format string, args ...any) { + colorRed("Error: "+format, args...) +} + +func (cp *csvPrinter) printStart(hostname string, port uint16) { + header := []string{ + "status", "hostname", "ip", "port", "TCP_conn", "time", + } + cp.writer.Write(header) +} + +func (cp *csvPrinter) printProbeSuccess(hostname, ip string, port uint16, streak uint, rtt float32) { + cp.writeRecord([]string{ + "reply", hostname, ip, fmt.Sprint(port), fmt.Sprint(streak), fmt.Sprintf("%.3f", rtt), + }) +} + +func (cp *csvPrinter) printProbeFail(hostname, ip string, port uint16, streak uint) { + cp.writeRecord([]string{ + "no reply", hostname, ip, fmt.Sprint(port), fmt.Sprint(streak), "", + }) +} + // Satisfying the "printer" interface. -func (db *csvPrinter) printProbeSuccess(hostname, ip string, port uint16, streak uint, rtt float32) {} -func (db *csvPrinter) printProbeFail(hostname, ip string, port uint16, streak uint) {} -func (db *csvPrinter) printRetryingToResolve(hostname string) {} -func (db *csvPrinter) printTotalDownTime(downtime time.Duration) {} -func (db *csvPrinter) printVersion() {} -func (db *csvPrinter) printInfo(format string, args ...any) {} +func (cp *csvPrinter) printStatistics(s tcping) {} +func (cp *csvPrinter) printRetryingToResolve(hostname string) {} +func (cp *csvPrinter) printTotalDownTime(downtime time.Duration) {} +func (cp *csvPrinter) printVersion() {} +func (cp *csvPrinter) printInfo(format string, args ...any) {} diff --git a/go.mod b/go.mod index 109ffcc..31031f6 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/pouriyajamshidi/tcping/v2 go 1.23.1 require ( - github.com/davecgh/go-spew v1.1.1 github.com/google/go-github/v45 v45.2.0 github.com/gookit/color v1.5.4 github.com/stretchr/testify v1.9.0 @@ -11,6 +10,7 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect From 9bff8d100743d621d8011ea301ec05fc0be7651b Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sun, 1 Dec 2024 21:09:16 +0530 Subject: [PATCH 11/25] add tests --- csv.go | 321 ++++++++++++++++++++++++++++++++++++++++++++++------ csv_test.go | 141 +++++++++++++++-------- tcping.go | 13 ++- 3 files changed, 394 insertions(+), 81 deletions(-) diff --git a/csv.go b/csv.go index c19c30d..a2d1e92 100644 --- a/csv.go +++ b/csv.go @@ -3,71 +3,326 @@ package main import ( "encoding/csv" "fmt" + "math" "os" "time" ) type csvPrinter struct { - writer *csv.Writer - file *os.File - filename string - headerDone bool + writer *csv.Writer + file *os.File + dataFilename string + headerDone bool + showTimestamp bool + statsWriter *csv.Writer + statsFile *os.File + statsFilename string + statsHeaderDone bool + cleanup func() } const ( csvTimeFormat = "2006-01-02 15:04:05.000" ) -func newCSVPrinter(filename string, args []string) (*csvPrinter, error) { - file, err := os.Create(filename) +const ( + colStatus = "Status" + colTimestamp = "Timestamp" + colHostname = "Hostname" + colIP = "IP" + colPort = "Port" + colTCPConn = "TCP_Conn" + colLatency = "Latency(ms)" +) + +func newCSVPrinter(dataFilename string, showTimestamp bool) (*csvPrinter, error) { + // Open the data file with the os.O_TRUNC flag to truncate it + file, err := os.OpenFile(dataFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { - return nil, fmt.Errorf("error creating CSV file: %w", err) + return nil, fmt.Errorf("error creating data CSV file: %w", err) } - writer := csv.NewWriter(file) - return &csvPrinter{ - writer: writer, - file: file, - filename: filename, - headerDone: false, - }, nil -} + // Append _stats before the .csv extension + statsFilename := dataFilename + if len(dataFilename) > 4 && dataFilename[len(dataFilename)-4:] == ".csv" { + statsFilename = dataFilename[:len(dataFilename)-4] + "_stats.csv" + } else { + statsFilename = dataFilename + "_stats" + } -func (cp *csvPrinter) writeRecord(record []string) error { - return cp.writer.Write(record) + cp := &csvPrinter{ + writer: csv.NewWriter(file), + file: file, + dataFilename: dataFilename, + statsFilename: statsFilename, + showTimestamp: showTimestamp, + } + + cp.cleanup = func() { + if cp.writer != nil { + cp.writer.Flush() + } + if cp.file != nil { + cp.file.Close() + } + if cp.statsWriter != nil { + cp.statsWriter.Flush() + } + if cp.statsFile != nil { + cp.statsFile.Close() + } + } + + return cp, nil } -func (cp *csvPrinter) close() { +func (cp *csvPrinter) writeHeader() error { + headers := []string{ + colStatus, + colHostname, + colIP, + colPort, + colTCPConn, + colLatency, + } + + if cp.showTimestamp { + headers = append(headers, colTimestamp) + } + + if err := cp.writer.Write(headers); err != nil { + return fmt.Errorf("failed to write headers: %w", err) + } + cp.writer.Flush() - cp.file.Close() + return cp.writer.Error() } -func (cp *csvPrinter) printError(format string, args ...any) { - colorRed("Error: "+format, args...) +func (cp *csvPrinter) writeRecord(record []string) error { + if _, err := os.Stat(cp.dataFilename); os.IsNotExist(err) { + file, err := os.OpenFile(cp.dataFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("failed to recreate data CSV file: %w", err) + } + cp.file = file + cp.writer = csv.NewWriter(file) + cp.headerDone = false + } + + if !cp.headerDone { + if err := cp.writeHeader(); err != nil { + return err + } + cp.headerDone = true + } + + if cp.showTimestamp { + record = append(record, time.Now().Format(csvTimeFormat)) + } + + if err := cp.writer.Write(record); err != nil { + return fmt.Errorf("failed to write record: %w", err) + } + + cp.writer.Flush() + return cp.writer.Error() } func (cp *csvPrinter) printStart(hostname string, port uint16) { - header := []string{ - "status", "hostname", "ip", "port", "TCP_conn", "time", - } - cp.writer.Write(header) + fmt.Printf("TCPing results being written to: %s\n", cp.dataFilename) } func (cp *csvPrinter) printProbeSuccess(hostname, ip string, port uint16, streak uint, rtt float32) { - cp.writeRecord([]string{ - "reply", hostname, ip, fmt.Sprint(port), fmt.Sprint(streak), fmt.Sprintf("%.3f", rtt), - }) + record := []string{ + "Success", + hostname, + ip, + fmt.Sprint(port), + fmt.Sprint(streak), + fmt.Sprintf("%.3f", rtt), + } + + if err := cp.writeRecord(record); err != nil { + cp.printError("Failed to write success record: %v", err) + } } func (cp *csvPrinter) printProbeFail(hostname, ip string, port uint16, streak uint) { - cp.writeRecord([]string{ - "no reply", hostname, ip, fmt.Sprint(port), fmt.Sprint(streak), "", + record := []string{ + "Failed", + hostname, + ip, + fmt.Sprint(port), + fmt.Sprint(streak), + "-", + } + + if err := cp.writeRecord(record); err != nil { + cp.printError("Failed to write failure record: %v", err) + } +} + +func (cp *csvPrinter) printRetryingToResolve(hostname string) { + record := []string{ + "Resolving", + hostname, + "-", + "-", + "-", + "-", + } + + if err := cp.writeRecord(record); err != nil { + cp.printError("Failed to write resolve record: %v", err) + } +} + +func (cp *csvPrinter) printError(format string, args ...any) { + fmt.Fprintf(os.Stderr, "CSV Error: "+format+"\n", args...) +} + +func (cp *csvPrinter) writeStatsHeader() error { + headers := []string{ + "Metric", + "Value", + } + + if err := cp.statsWriter.Write(headers); err != nil { + return fmt.Errorf("failed to write statistics headers: %w", err) + } + + cp.statsWriter.Flush() + return cp.statsWriter.Error() +} + +func (cp *csvPrinter) writeStatsRecord(record []string) error { + if _, err := os.Stat(cp.statsFilename); os.IsNotExist(err) { + statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("failed to recreate statistics CSV file: %w", err) + } + cp.statsFile = statsFile + cp.statsWriter = csv.NewWriter(statsFile) + cp.statsHeaderDone = false + } + + // Write header if not done + if !cp.statsHeaderDone { + if err := cp.writeStatsHeader(); err != nil { + return err + } + cp.statsHeaderDone = true + } + + if err := cp.statsWriter.Write(record); err != nil { + return fmt.Errorf("failed to write statistics record: %w", err) + } + + cp.statsWriter.Flush() + return cp.statsWriter.Error() +} + +func (cp *csvPrinter) printStatistics(t tcping) { + // Initialize stats file if not already done + if cp.statsFile == nil { + statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_TRUNC, 0644) + if err != nil { + cp.printError("Failed to create statistics CSV file: %v", err) + return + } + cp.statsFile = statsFile + cp.statsWriter = csv.NewWriter(statsFile) + cp.statsHeaderDone = false + } + + totalPackets := t.totalSuccessfulProbes + t.totalUnsuccessfulProbes + packetLoss := (float32(t.totalUnsuccessfulProbes) / float32(totalPackets)) * 100 + if math.IsNaN(float64(packetLoss)) { + packetLoss = 0 + } + + // Collect statistics data + timestamp := time.Now().Format(time.RFC3339) + statistics := [][]string{ + {"Timestamp", timestamp}, + {"Total Packets", fmt.Sprint(totalPackets)}, + {"Successful Probes", fmt.Sprint(t.totalSuccessfulProbes)}, + {"Unsuccessful Probes", fmt.Sprint(t.totalUnsuccessfulProbes)}, + {"Packet Loss", fmt.Sprintf("%.2f%%", packetLoss)}, + {"Last Successful Probe", t.lastSuccessfulProbe.Format(timeFormat)}, + {"Last Unsuccessful Probe", t.lastUnsuccessfulProbe.Format(timeFormat)}, + {"Total Uptime", durationToString(t.totalUptime)}, + {"Total Downtime", durationToString(t.totalDowntime)}, + } + + if t.longestUptime.duration != 0 { + statistics = append(statistics, []string{ + "Longest Uptime", durationToString(t.longestUptime.duration), + "From", t.longestUptime.start.Format(timeFormat), + "To", t.longestUptime.end.Format(timeFormat), + }) + } + + if t.longestDowntime.duration != 0 { + statistics = append(statistics, []string{ + "Longest Downtime", durationToString(t.longestDowntime.duration), + "From", t.longestDowntime.start.Format(timeFormat), + "To", t.longestDowntime.end.Format(timeFormat), + }) + } + + if !t.destIsIP { + statistics = append(statistics, []string{ + "Retried Hostname Lookups", fmt.Sprint(t.retriedHostnameLookups), + }) + + if len(t.hostnameChanges) >= 2 { + for i := 0; i < len(t.hostnameChanges)-1; i++ { + statistics = append(statistics, []string{ + "IP Change", t.hostnameChanges[i].Addr.String(), + "To", t.hostnameChanges[i+1].Addr.String(), + "At", t.hostnameChanges[i+1].When.Format(timeFormat), + }) + } + } + } + + if t.rttResults.hasResults { + statistics = append(statistics, []string{ + "RTT Min", fmt.Sprintf("%.3f ms", t.rttResults.min), + "RTT Avg", fmt.Sprintf("%.3f ms", t.rttResults.average), + "RTT Max", fmt.Sprintf("%.3f ms", t.rttResults.max), + }) + } + + statistics = append(statistics, []string{ + "TCPing Started At", t.startTime.Format(timeFormat), + }) + + if !t.endTime.IsZero() { + statistics = append(statistics, []string{ + "TCPing Ended At", t.endTime.Format(timeFormat), + }) + } + + durationTime := time.Time{}.Add(t.totalDowntime + t.totalUptime) + statistics = append(statistics, []string{ + "Duration (HH:MM:SS)", durationTime.Format(hourFormat), }) + + // Write statistics to CSV + for _, record := range statistics { + if err := cp.writeStatsRecord(record); err != nil { + cp.printError("Failed to write statistics record: %v", err) + return + } + } + + // Print the message only if statistics are written + fmt.Printf("TCPing statistics written to: %s\n", cp.statsFilename) } -// Satisfying the "printer" interface. -func (cp *csvPrinter) printStatistics(s tcping) {} -func (cp *csvPrinter) printRetryingToResolve(hostname string) {} +// Satisfying remaining printer interface methods func (cp *csvPrinter) printTotalDownTime(downtime time.Duration) {} func (cp *csvPrinter) printVersion() {} func (cp *csvPrinter) printInfo(format string, args ...any) {} diff --git a/csv_test.go b/csv_test.go index 75cb4e7..476f19d 100644 --- a/csv_test.go +++ b/csv_test.go @@ -4,65 +4,116 @@ import ( "encoding/csv" "os" "testing" + "time" + + "github.com/stretchr/testify/assert" ) -func setupTempCSVFile(t *testing.T) (*csvPrinter, func()) { - t.Helper() - tempFile, err := os.CreateTemp("", "test_csv_*.csv") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } +func TestNewCSVPrinter(t *testing.T) { + dataFilename := "test_data.csv" + showTimestamp := true + + cp, err := newCSVPrinter(dataFilename, showTimestamp) + assert.NoError(t, err) + assert.NotNil(t, cp) + assert.Equal(t, dataFilename, cp.dataFilename) + assert.Equal(t, dataFilename[:len(dataFilename)-4]+"_stats.csv", cp.statsFilename) + + cp.cleanup() + os.Remove(dataFilename) + os.Remove(cp.statsFilename) +} + +func TestWriteRecord(t *testing.T) { + dataFilename := "test_data.csv" + showTimestamp := false + + cp, err := newCSVPrinter(dataFilename, showTimestamp) + assert.NoError(t, err) + assert.NotNil(t, cp) + + record := []string{"Success", "hostname", "127.0.0.1", "80", "1", "10.123"} + err = cp.writeRecord(record) + assert.NoError(t, err) + + // Verify the record is written + file, err := os.Open(dataFilename) + assert.NoError(t, err) + defer file.Close() + + reader := csv.NewReader(file) + headers, err := reader.Read() + assert.NoError(t, err) + assert.Equal(t, []string{"Status", "Hostname", "IP", "Port", "TCP_Conn", "Latency(ms)"}, headers) + + readRecord, err := reader.Read() + assert.NoError(t, err) + assert.Equal(t, record, readRecord) + + // Cleanup + cp.cleanup() + os.Remove(dataFilename) + os.Remove(cp.statsFilename) +} - cp := &csvPrinter{ - file: tempFile, - writer: csv.NewWriter(tempFile), +func TestWriteStatistics(t *testing.T) { + dataFilename := "test_data.csv" + showTimestamp := true + + cp, err := newCSVPrinter(dataFilename, showTimestamp) + assert.NoError(t, err) + assert.NotNil(t, cp) + + tcping := tcping{ + totalSuccessfulProbes: 1, + totalUnsuccessfulProbes: 0, + lastSuccessfulProbe: time.Now(), + startTime: time.Now(), } - cleanup := func() { - cp.writer.Flush() - if err := cp.writer.Error(); err != nil { - t.Errorf("Error flushing CSV writer: %v", err) - } - if err := cp.file.Close(); err != nil { - t.Errorf("Error closing file: %v", err) - } - if err := os.Remove(cp.file.Name()); err != nil { - t.Errorf("Error removing temp file: %v", err) + cp.printStatistics(tcping) + + statsFile, err := os.Open(cp.statsFilename) + assert.NoError(t, err) + defer statsFile.Close() + + reader := csv.NewReader(statsFile) + headers, err := reader.Read() + assert.NoError(t, err) + assert.Equal(t, []string{"Metric", "Value"}, headers) + + for { + record, err := reader.Read() + if err != nil { + break } + assert.NotEmpty(t, record) } - return cp, cleanup + cp.cleanup() + os.Remove(dataFilename) + os.Remove(cp.statsFilename) } -func TestNewCSVPrinter(t *testing.T) { - args := []string{"localhost", "8001"} - filename := "test.csv" +func TestCleanup(t *testing.T) { + dataFilename := "test_data.csv" + showTimestamp := true - cp, err := newCSVPrinter(filename, args) - if err != nil { - t.Fatalf("error creating CSV printer: %v", err) - } + cp, err := newCSVPrinter(dataFilename, showTimestamp) + assert.NoError(t, err) + assert.NotNil(t, cp) - defer func() { - if cp != nil && cp.file != nil { - cp.file.Close() - } - os.Remove(filename) - }() + os.Create(dataFilename) + os.Create(cp.statsFilename) - if _, err := os.Stat(filename); os.IsNotExist(err) { - t.Errorf("file %s was not created", filename) - } + cp.cleanup() - if cp.filename != filename { - t.Errorf("expected filename %q, got %q", filename, cp.filename) - } + _, err = os.Stat(dataFilename) + assert.NoError(t, err) - if cp.headerDone { - t.Errorf("expected headerDone to be false, got true") - } + _, err = os.Stat(cp.statsFilename) + assert.NoError(t, err) - if cp.writer == nil { - t.Errorf("CSV writer was not initialized") - } + os.Remove(dataFilename) + os.Remove(cp.statsFilename) } diff --git a/tcping.go b/tcping.go index f13bcc8..5886b9e 100644 --- a/tcping.go +++ b/tcping.go @@ -197,11 +197,16 @@ func shutdown(tcping *tcping) { tcping.endTime = time.Now() tcping.printStats() - // if the printer type is `database`, close the it before exiting + // if the printer type is `database`, close it before exiting if db, ok := tcping.printer.(*database); ok { db.conn.Close() } + // if the printer type is `csvPrinter`, call the cleanup function before exiting + if cp, ok := tcping.printer.(*csvPrinter); ok { + cp.cleanup() + } + os.Exit(0) } @@ -239,7 +244,7 @@ func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, timeStamp *bool, o tcping.printer = newDB(*outputDb, args) } else if *outputCSV != "" { var err error - tcping.printer, err = newCSVPrinter(*outputCSV, args) + tcping.printer, err = newCSVPrinter(*outputCSV, *timeStamp) if err != nil { tcping.printError("Failed to create CSV file: %s", err) os.Exit(1) @@ -331,7 +336,7 @@ func processUserInput(tcping *tcping) { outputJSON := flag.Bool("j", false, "output in JSON format.") prettyJSON := flag.Bool("pretty", false, "use indentation when using json output format. No effect without the '-j' flag.") showTimestamp := flag.Bool("D", false, "show timestamp in output.") - saveToCSV := flag.String("csv", "", "path and file name to store tcping output to CSV file.") + saveToCSV := flag.String("csv", "", "path and file name to store tcping output to CSV file...If user prompts for stats, it will be saved to a file with the same name but _stats appended.") showVer := flag.Bool("v", false, "show version.") checkUpdates := flag.Bool("u", false, "check for updates and exit.") secondsBetweenProbes := flag.Float64("i", 1, "interval between sending probes. Real number allowed with dot as a decimal separator. The default is one second") @@ -422,6 +427,8 @@ func permuteArgs(args []string) { fallthrough case "i": fallthrough + case "csv": + fallthrough case "r": /* out of index */ if len(args) <= i+1 { From d11ef3d0969f113869c04008e0d0aad7426f5ae9 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 7 Dec 2024 21:35:10 +0530 Subject: [PATCH 12/25] nit --- tcping.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tcping.go b/tcping.go index b8162b0..15d6685 100644 --- a/tcping.go +++ b/tcping.go @@ -235,7 +235,6 @@ func usage() { } // setPrinter selects the printer -func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, timeStamp *bool, outputDb, outputCSV *string, args []string) { func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, noColor *bool, timeStamp *bool, outputDb *string, args []string) { if *prettyJSON && !*outputJSON { colorRed("--pretty has no effect without the -j flag.") From 7e5d2a108485a4bd85cf1e89494f27ac8c2b5c16 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 7 Dec 2024 21:53:25 +0530 Subject: [PATCH 13/25] satisfy interface --- csv.go | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/csv.go b/csv.go index a2d1e92..99130c1 100644 --- a/csv.go +++ b/csv.go @@ -134,7 +134,7 @@ func (cp *csvPrinter) printStart(hostname string, port uint16) { func (cp *csvPrinter) printProbeSuccess(hostname, ip string, port uint16, streak uint, rtt float32) { record := []string{ - "Success", + "Reply", hostname, ip, fmt.Sprint(port), @@ -147,14 +147,26 @@ func (cp *csvPrinter) printProbeSuccess(hostname, ip string, port uint16, streak } } -func (cp *csvPrinter) printProbeFail(hostname, ip string, port uint16, streak uint) { - record := []string{ - "Failed", - hostname, - ip, - fmt.Sprint(port), - fmt.Sprint(streak), - "-", +func (cp *csvPrinter) printProbeFail(userInput userInput, streak uint) { + if userInput.hostname == ""{ + record := []string{ + "No reply", + "-", + ip, + fmt.Sprint(userInput.port), + fmt.Sprint(streak), + "-", + } + } + else { + record := []string{ + "No reply", + userInput.hostname, + userInput.ip, + fmt.Sprint(userInput.port), + fmt.Sprint(streak), + "-", + } } if err := cp.writeRecord(record); err != nil { From 8f50cbaddd85787adf16e1ab84cee608fc3efc6f Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 7 Dec 2024 23:11:50 +0530 Subject: [PATCH 14/25] fix --- csv.go | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/csv.go b/csv.go index 99130c1..fb8a955 100644 --- a/csv.go +++ b/csv.go @@ -133,6 +133,10 @@ func (cp *csvPrinter) printStart(hostname string, port uint16) { } func (cp *csvPrinter) printProbeSuccess(hostname, ip string, port uint16, streak uint, rtt float32) { + hostname := userInput.hostname + if hostname == "" { + hostname = "-" + } record := []string{ "Reply", hostname, @@ -148,30 +152,23 @@ func (cp *csvPrinter) printProbeSuccess(hostname, ip string, port uint16, streak } func (cp *csvPrinter) printProbeFail(userInput userInput, streak uint) { - if userInput.hostname == ""{ - record := []string{ - "No reply", - "-", - ip, - fmt.Sprint(userInput.port), - fmt.Sprint(streak), - "-", - } - } - else { - record := []string{ - "No reply", - userInput.hostname, - userInput.ip, - fmt.Sprint(userInput.port), - fmt.Sprint(streak), - "-", - } - } - - if err := cp.writeRecord(record); err != nil { - cp.printError("Failed to write failure record: %v", err) - } + hostname := userInput.hostname + if hostname == "" { + hostname = "-" + } + + record := []string{ + "No reply", + hostname, + userInput.ip, + fmt.Sprint(userInput.port), + fmt.Sprint(streak), + "-", + } + + if err := cp.writeRecord(record); err != nil { + cp.printError("Failed to write failure record: %v", err) + } } func (cp *csvPrinter) printRetryingToResolve(hostname string) { From 0f7794c7551f2cae3c046f0fde0a05b0af732b11 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 7 Dec 2024 23:33:18 +0530 Subject: [PATCH 15/25] add showing local address --- csv.go | 544 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 278 insertions(+), 266 deletions(-) diff --git a/csv.go b/csv.go index fb8a955..be2a2be 100644 --- a/csv.go +++ b/csv.go @@ -1,154 +1,166 @@ package main import ( - "encoding/csv" - "fmt" - "math" - "os" - "time" + "encoding/csv" + "fmt" + "math" + "os" + "time" ) type csvPrinter struct { - writer *csv.Writer - file *os.File - dataFilename string - headerDone bool - showTimestamp bool - statsWriter *csv.Writer - statsFile *os.File - statsFilename string - statsHeaderDone bool - cleanup func() + writer *csv.Writer + file *os.File + dataFilename string + headerDone bool + showTimestamp bool + showLocalAddress bool + statsWriter *csv.Writer + statsFile *os.File + statsFilename string + statsHeaderDone bool + cleanup func() } const ( - csvTimeFormat = "2006-01-02 15:04:05.000" + csvTimeFormat = "2006-01-02 15:04:05.000" ) const ( - colStatus = "Status" - colTimestamp = "Timestamp" - colHostname = "Hostname" - colIP = "IP" - colPort = "Port" - colTCPConn = "TCP_Conn" - colLatency = "Latency(ms)" + colStatus = "Status" + colTimestamp = "Timestamp" + colHostname = "Hostname" + colIP = "IP" + colPort = "Port" + colTCPConn = "TCP_Conn" + colLatency = "Latency(ms)" + colLocalAddress = "Local Address" ) -func newCSVPrinter(dataFilename string, showTimestamp bool) (*csvPrinter, error) { - // Open the data file with the os.O_TRUNC flag to truncate it - file, err := os.OpenFile(dataFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return nil, fmt.Errorf("error creating data CSV file: %w", err) - } - - // Append _stats before the .csv extension - statsFilename := dataFilename - if len(dataFilename) > 4 && dataFilename[len(dataFilename)-4:] == ".csv" { - statsFilename = dataFilename[:len(dataFilename)-4] + "_stats.csv" - } else { - statsFilename = dataFilename + "_stats" - } - - cp := &csvPrinter{ - writer: csv.NewWriter(file), - file: file, - dataFilename: dataFilename, - statsFilename: statsFilename, - showTimestamp: showTimestamp, - } - - cp.cleanup = func() { - if cp.writer != nil { - cp.writer.Flush() - } - if cp.file != nil { - cp.file.Close() - } - if cp.statsWriter != nil { - cp.statsWriter.Flush() - } - if cp.statsFile != nil { - cp.statsFile.Close() - } - } - - return cp, nil +func newCSVPrinter(dataFilename string, showTimestamp bool, showLocalAddress bool) (*csvPrinter, error) { + // Open the data file with the os.O_TRUNC flag to truncate it + file, err := os.OpenFile(dataFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return nil, fmt.Errorf("error creating data CSV file: %w", err) + } + + // Append _stats before the .csv extension + statsFilename := dataFilename + if len(dataFilename) > 4 && dataFilename[len(dataFilename)-4:] == ".csv" { + statsFilename = dataFilename[:len(dataFilename)-4] + "_stats.csv" + } else { + statsFilename = dataFilename + "_stats" + } + + cp := &csvPrinter{ + writer: csv.NewWriter(file), + file: file, + dataFilename: dataFilename, + statsFilename: statsFilename, + showTimestamp: showTimestamp, + showLocalAddress: showLocalAddress, + } + + cp.cleanup = func() { + if cp.writer != nil { + cp.writer.Flush() + } + if cp.file != nil { + cp.file.Close() + } + if cp.statsWriter != nil { + cp.statsWriter.Flush() + } + if cp.statsFile != nil { + cp.statsFile.Close() + } + } + + return cp, nil } func (cp *csvPrinter) writeHeader() error { - headers := []string{ - colStatus, - colHostname, - colIP, - colPort, - colTCPConn, - colLatency, - } - - if cp.showTimestamp { - headers = append(headers, colTimestamp) - } - - if err := cp.writer.Write(headers); err != nil { - return fmt.Errorf("failed to write headers: %w", err) - } - - cp.writer.Flush() - return cp.writer.Error() + headers := []string{ + colStatus, + colHostname, + colIP, + colPort, + colTCPConn, + colLatency, + } + + if cp.showLocalAddress { + headers = append(headers, colLocalAddress) + } + + if cp.showTimestamp { + headers = append(headers, colTimestamp) + } + + if err := cp.writer.Write(headers); err != nil { + return fmt.Errorf("failed to write headers: %w", err) + } + + cp.writer.Flush() + return cp.writer.Error() } func (cp *csvPrinter) writeRecord(record []string) error { - if _, err := os.Stat(cp.dataFilename); os.IsNotExist(err) { - file, err := os.OpenFile(cp.dataFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return fmt.Errorf("failed to recreate data CSV file: %w", err) - } - cp.file = file - cp.writer = csv.NewWriter(file) - cp.headerDone = false - } - - if !cp.headerDone { - if err := cp.writeHeader(); err != nil { - return err - } - cp.headerDone = true - } - - if cp.showTimestamp { - record = append(record, time.Now().Format(csvTimeFormat)) - } - - if err := cp.writer.Write(record); err != nil { - return fmt.Errorf("failed to write record: %w", err) - } - - cp.writer.Flush() - return cp.writer.Error() + if _, err := os.Stat(cp.dataFilename); os.IsNotExist(err) { + file, err := os.OpenFile(cp.dataFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("failed to recreate data CSV file: %w", err) + } + cp.file = file + cp.writer = csv.NewWriter(file) + cp.headerDone = false + } + + if !cp.headerDone { + if err := cp.writeHeader(); err != nil { + return err + } + cp.headerDone = true + } + + if cp.showTimestamp { + record = append(record, time.Now().Format(csvTimeFormat)) + } + + if err := cp.writer.Write(record); err != nil { + return fmt.Errorf("failed to write record: %w", err) + } + + cp.writer.Flush() + return cp.writer.Error() } func (cp *csvPrinter) printStart(hostname string, port uint16) { - fmt.Printf("TCPing results being written to: %s\n", cp.dataFilename) + fmt.Printf("TCPing results being written to: %s\n", cp.dataFilename) } -func (cp *csvPrinter) printProbeSuccess(hostname, ip string, port uint16, streak uint, rtt float32) { - hostname := userInput.hostname +func (cp *csvPrinter) printProbeSuccess(localAddr string, userInput userInput, streak uint, rtt float32) { + hostname := userInput.hostname if hostname == "" { hostname = "-" } - record := []string{ - "Reply", - hostname, - ip, - fmt.Sprint(port), - fmt.Sprint(streak), - fmt.Sprintf("%.3f", rtt), - } - - if err := cp.writeRecord(record); err != nil { - cp.printError("Failed to write success record: %v", err) - } + + record := []string{ + "Reply", + hostname, + userInput.ip, + fmt.Sprint(userInput.port), + fmt.Sprint(streak), + fmt.Sprintf("%.3f", rtt), + } + + if cp.showLocalAddress { + record = append(record, localAddr) + } + + if err := cp.writeRecord(record); err != nil { + cp.printError("Failed to write success record: %v", err) + } } func (cp *csvPrinter) printProbeFail(userInput userInput, streak uint) { @@ -172,163 +184,163 @@ func (cp *csvPrinter) printProbeFail(userInput userInput, streak uint) { } func (cp *csvPrinter) printRetryingToResolve(hostname string) { - record := []string{ - "Resolving", - hostname, - "-", - "-", - "-", - "-", - } - - if err := cp.writeRecord(record); err != nil { - cp.printError("Failed to write resolve record: %v", err) - } + record := []string{ + "Resolving", + hostname, + "-", + "-", + "-", + "-", + } + + if err := cp.writeRecord(record); err != nil { + cp.printError("Failed to write resolve record: %v", err) + } } func (cp *csvPrinter) printError(format string, args ...any) { - fmt.Fprintf(os.Stderr, "CSV Error: "+format+"\n", args...) + fmt.Fprintf(os.Stderr, "CSV Error: "+format+"\n", args...) } func (cp *csvPrinter) writeStatsHeader() error { - headers := []string{ - "Metric", - "Value", - } + headers := []string{ + "Metric", + "Value", + } - if err := cp.statsWriter.Write(headers); err != nil { - return fmt.Errorf("failed to write statistics headers: %w", err) - } + if err := cp.statsWriter.Write(headers); err != nil { + return fmt.Errorf("failed to write statistics headers: %w", err) + } - cp.statsWriter.Flush() - return cp.statsWriter.Error() + cp.statsWriter.Flush() + return cp.statsWriter.Error() } func (cp *csvPrinter) writeStatsRecord(record []string) error { - if _, err := os.Stat(cp.statsFilename); os.IsNotExist(err) { - statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return fmt.Errorf("failed to recreate statistics CSV file: %w", err) - } - cp.statsFile = statsFile - cp.statsWriter = csv.NewWriter(statsFile) - cp.statsHeaderDone = false - } - - // Write header if not done - if !cp.statsHeaderDone { - if err := cp.writeStatsHeader(); err != nil { - return err - } - cp.statsHeaderDone = true - } - - if err := cp.statsWriter.Write(record); err != nil { - return fmt.Errorf("failed to write statistics record: %w", err) - } - - cp.statsWriter.Flush() - return cp.statsWriter.Error() + if _, err := os.Stat(cp.statsFilename); os.IsNotExist(err) { + statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("failed to recreate statistics CSV file: %w", err) + } + cp.statsFile = statsFile + cp.statsWriter = csv.NewWriter(statsFile) + cp.statsHeaderDone = false + } + + // Write header if not done + if !cp.statsHeaderDone { + if err := cp.writeStatsHeader(); err != nil { + return err + } + cp.statsHeaderDone = true + } + + if err := cp.statsWriter.Write(record); err != nil { + return fmt.Errorf("failed to write statistics record: %w", err) + } + + cp.statsWriter.Flush() + return cp.statsWriter.Error() } func (cp *csvPrinter) printStatistics(t tcping) { - // Initialize stats file if not already done - if cp.statsFile == nil { - statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_TRUNC, 0644) - if err != nil { - cp.printError("Failed to create statistics CSV file: %v", err) - return - } - cp.statsFile = statsFile - cp.statsWriter = csv.NewWriter(statsFile) - cp.statsHeaderDone = false - } - - totalPackets := t.totalSuccessfulProbes + t.totalUnsuccessfulProbes - packetLoss := (float32(t.totalUnsuccessfulProbes) / float32(totalPackets)) * 100 - if math.IsNaN(float64(packetLoss)) { - packetLoss = 0 - } - - // Collect statistics data - timestamp := time.Now().Format(time.RFC3339) - statistics := [][]string{ - {"Timestamp", timestamp}, - {"Total Packets", fmt.Sprint(totalPackets)}, - {"Successful Probes", fmt.Sprint(t.totalSuccessfulProbes)}, - {"Unsuccessful Probes", fmt.Sprint(t.totalUnsuccessfulProbes)}, - {"Packet Loss", fmt.Sprintf("%.2f%%", packetLoss)}, - {"Last Successful Probe", t.lastSuccessfulProbe.Format(timeFormat)}, - {"Last Unsuccessful Probe", t.lastUnsuccessfulProbe.Format(timeFormat)}, - {"Total Uptime", durationToString(t.totalUptime)}, - {"Total Downtime", durationToString(t.totalDowntime)}, - } - - if t.longestUptime.duration != 0 { - statistics = append(statistics, []string{ - "Longest Uptime", durationToString(t.longestUptime.duration), - "From", t.longestUptime.start.Format(timeFormat), - "To", t.longestUptime.end.Format(timeFormat), - }) - } - - if t.longestDowntime.duration != 0 { - statistics = append(statistics, []string{ - "Longest Downtime", durationToString(t.longestDowntime.duration), - "From", t.longestDowntime.start.Format(timeFormat), - "To", t.longestDowntime.end.Format(timeFormat), - }) - } - - if !t.destIsIP { - statistics = append(statistics, []string{ - "Retried Hostname Lookups", fmt.Sprint(t.retriedHostnameLookups), - }) - - if len(t.hostnameChanges) >= 2 { - for i := 0; i < len(t.hostnameChanges)-1; i++ { - statistics = append(statistics, []string{ - "IP Change", t.hostnameChanges[i].Addr.String(), - "To", t.hostnameChanges[i+1].Addr.String(), - "At", t.hostnameChanges[i+1].When.Format(timeFormat), - }) - } - } - } - - if t.rttResults.hasResults { - statistics = append(statistics, []string{ - "RTT Min", fmt.Sprintf("%.3f ms", t.rttResults.min), - "RTT Avg", fmt.Sprintf("%.3f ms", t.rttResults.average), - "RTT Max", fmt.Sprintf("%.3f ms", t.rttResults.max), - }) - } - - statistics = append(statistics, []string{ - "TCPing Started At", t.startTime.Format(timeFormat), - }) - - if !t.endTime.IsZero() { - statistics = append(statistics, []string{ - "TCPing Ended At", t.endTime.Format(timeFormat), - }) - } - - durationTime := time.Time{}.Add(t.totalDowntime + t.totalUptime) - statistics = append(statistics, []string{ - "Duration (HH:MM:SS)", durationTime.Format(hourFormat), - }) - - // Write statistics to CSV - for _, record := range statistics { - if err := cp.writeStatsRecord(record); err != nil { - cp.printError("Failed to write statistics record: %v", err) - return - } - } - - // Print the message only if statistics are written - fmt.Printf("TCPing statistics written to: %s\n", cp.statsFilename) + // Initialize stats file if not already done + if cp.statsFile == nil { + statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_TRUNC, 0644) + if err != nil { + cp.printError("Failed to create statistics CSV file: %v", err) + return + } + cp.statsFile = statsFile + cp.statsWriter = csv.NewWriter(statsFile) + cp.statsHeaderDone = false + } + + totalPackets := t.totalSuccessfulProbes + t.totalUnsuccessfulProbes + packetLoss := (float32(t.totalUnsuccessfulProbes) / float32(totalPackets)) * 100 + if math.IsNaN(float64(packetLoss)) { + packetLoss = 0 + } + + // Collect statistics data + timestamp := time.Now().Format(time.RFC3339) + statistics := [][]string{ + {"Timestamp", timestamp}, + {"Total Packets", fmt.Sprint(totalPackets)}, + {"Successful Probes", fmt.Sprint(t.totalSuccessfulProbes)}, + {"Unsuccessful Probes", fmt.Sprint(t.totalUnsuccessfulProbes)}, + {"Packet Loss", fmt.Sprintf("%.2f%%", packetLoss)}, + {"Last Successful Probe", t.lastSuccessfulProbe.Format(timeFormat)}, + {"Last Unsuccessful Probe", t.lastUnsuccessfulProbe.Format(timeFormat)}, + {"Total Uptime", durationToString(t.totalUptime)}, + {"Total Downtime", durationToString(t.totalDowntime)}, + } + + if t.longestUptime.duration != 0 { + statistics = append(statistics, []string{ + "Longest Uptime", durationToString(t.longestUptime.duration), + "From", t.longestUptime.start.Format(timeFormat), + "To", t.longestUptime.end.Format(timeFormat), + }) + } + + if t.longestDowntime.duration != 0 { + statistics = append(statistics, []string{ + "Longest Downtime", durationToString(t.longestDowntime.duration), + "From", t.longestDowntime.start.Format(timeFormat), + "To", t.longestDowntime.end.Format(timeFormat), + }) + } + + if !t.destIsIP { + statistics = append(statistics, []string{ + "Retried Hostname Lookups", fmt.Sprint(t.retriedHostnameLookups), + }) + + if len(t.hostnameChanges) >= 2 { + for i := 0; i < len(t.hostnameChanges)-1; i++ { + statistics = append(statistics, []string{ + "IP Change", t.hostnameChanges[i].Addr.String(), + "To", t.hostnameChanges[i+1].Addr.String(), + "At", t.hostnameChanges[i+1].When.Format(timeFormat), + }) + } + } + } + + if t.rttResults.hasResults { + statistics = append(statistics, []string{ + "RTT Min", fmt.Sprintf("%.3f ms", t.rttResults.min), + "RTT Avg", fmt.Sprintf("%.3f ms", t.rttResults.average), + "RTT Max", fmt.Sprintf("%.3f ms", t.rttResults.max), + }) + } + + statistics = append(statistics, []string{ + "TCPing Started At", t.startTime.Format(timeFormat), + }) + + if !t.endTime.IsZero() { + statistics = append(statistics, []string{ + "TCPing Ended At", t.endTime.Format(timeFormat), + }) + } + + durationTime := time.Time{}.Add(t.totalDowntime + t.totalUptime) + statistics = append(statistics, []string{ + "Duration (HH:MM:SS)", durationTime.Format(hourFormat), + }) + + // Write statistics to CSV + for _, record := range statistics { + if err := cp.writeStatsRecord(record); err != nil { + cp.printError("Failed to write statistics record: %v", err) + return + } + } + + // Print the message only if statistics are written + fmt.Printf("TCPing statistics written to: %s\n", cp.statsFilename) } // Satisfying remaining printer interface methods From 94a6c3b6b79a73cc6d63a7c3b49817d234799759 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 7 Dec 2024 23:38:25 +0530 Subject: [PATCH 16/25] typecasting --- csv.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csv.go b/csv.go index be2a2be..a8bbf42 100644 --- a/csv.go +++ b/csv.go @@ -148,7 +148,7 @@ func (cp *csvPrinter) printProbeSuccess(localAddr string, userInput userInput, s record := []string{ "Reply", hostname, - userInput.ip, + userInput.ip.String(), fmt.Sprint(userInput.port), fmt.Sprint(streak), fmt.Sprintf("%.3f", rtt), @@ -172,7 +172,7 @@ func (cp *csvPrinter) printProbeFail(userInput userInput, streak uint) { record := []string{ "No reply", hostname, - userInput.ip, + userInput.ip.String(), fmt.Sprint(userInput.port), fmt.Sprint(streak), "-", From 561722e145981cc2db00be0a2f5b4b9e2294ac2d Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 7 Dec 2024 23:45:18 +0530 Subject: [PATCH 17/25] fixes --- csv.go | 21 ++++++++++++++------- tcping.go | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/csv.go b/csv.go index a8bbf42..7ed3876 100644 --- a/csv.go +++ b/csv.go @@ -37,20 +37,26 @@ const ( colLocalAddress = "Local Address" ) + +func ensureCSVExtension(filename string) string { + if len(filename) > 4 && filename[len(filename)-4:] == ".csv" { + return filename + } + return filename + ".csv" +} + func newCSVPrinter(dataFilename string, showTimestamp bool, showLocalAddress bool) (*csvPrinter, error) { + // Ensure .csv extension for dataFilename + dataFilename = ensureCSVExtension(dataFilename) + // Open the data file with the os.O_TRUNC flag to truncate it file, err := os.OpenFile(dataFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return nil, fmt.Errorf("error creating data CSV file: %w", err) } - // Append _stats before the .csv extension - statsFilename := dataFilename - if len(dataFilename) > 4 && dataFilename[len(dataFilename)-4:] == ".csv" { - statsFilename = dataFilename[:len(dataFilename)-4] + "_stats.csv" - } else { - statsFilename = dataFilename + "_stats" - } + // Append _stats before the .csv extension for statsFilename + statsFilename := dataFilename[:len(dataFilename)-4] + "_stats.csv" cp := &csvPrinter{ writer: csv.NewWriter(file), @@ -79,6 +85,7 @@ func newCSVPrinter(dataFilename string, showTimestamp bool, showLocalAddress boo return cp, nil } + func (cp *csvPrinter) writeHeader() error { headers := []string{ colStatus, diff --git a/tcping.go b/tcping.go index 15d6685..7434b2d 100644 --- a/tcping.go +++ b/tcping.go @@ -235,7 +235,7 @@ func usage() { } // setPrinter selects the printer -func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, noColor *bool, timeStamp *bool, outputDb *string, args []string) { +func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, noColor *bool, timeStamp *bool, outputDb *string, outputCSV *string, args []string) { if *prettyJSON && !*outputJSON { colorRed("--pretty has no effect without the -j flag.") usage() From 2317b355f21272781fd6787ce8c38e73edaa5f6e Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 7 Dec 2024 23:55:37 +0530 Subject: [PATCH 18/25] fixes --- tcping.go | 4 ++-- test.csv | 39 +++++++++++++++++++++++++++++++++++++++ test_stats.csv | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 test.csv create mode 100644 test_stats.csv diff --git a/tcping.go b/tcping.go index 7434b2d..aeb99b2 100644 --- a/tcping.go +++ b/tcping.go @@ -246,7 +246,7 @@ func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, noColor *bool, tim tcping.printer = newDB(*outputDb, args) } else if *outputCSV != "" { var err error - tcping.printer, err = newCSVPrinter(*outputCSV, *timeStamp) + tcping.printer, err = newCSVPrinter(*outputCSV, *timeStamp, tcping.userInput.showLocalAddress) if err != nil { tcping.printError("Failed to create CSV file: %s", err) os.Exit(1) @@ -364,7 +364,7 @@ func processUserInput(tcping *tcping) { // we need to set printers first, because they're used for // error reporting and other output. - setPrinter(tcping, outputJSON, prettyJSON, showTimestamp, outputDB, saveToCSV, args) + setPrinter(tcping, outputJSON, prettyJSON, noColor, showTimestamp, outputDB, saveToCSV, args) // Handle -v flag if *showVer { diff --git a/test.csv b/test.csv new file mode 100644 index 0000000..047edd8 --- /dev/null +++ b/test.csv @@ -0,0 +1,39 @@ +Status,Hostname,IP,Port,TCP_Conn,Latency(ms) +Reply,google.com,142.250.74.174,80,1,306.725 +Reply,google.com,142.250.74.174,80,2,330.389 +Reply,google.com,142.250.74.174,80,3,354.380 +Reply,google.com,142.250.74.174,80,4,293.406 +Reply,google.com,142.250.74.174,80,5,402.546 +Reply,google.com,142.250.74.174,80,6,324.038 +Reply,google.com,142.250.74.174,80,7,347.952 +Reply,google.com,142.250.74.174,80,8,279.303 +Reply,google.com,142.250.74.174,80,9,293.731 +Reply,google.com,142.250.74.174,80,10,287.733 +Reply,google.com,142.250.74.174,80,11,294.353 +Reply,google.com,142.250.74.174,80,12,278.473 +Reply,google.com,142.250.74.174,80,13,389.935 +Reply,google.com,142.250.74.174,80,14,311.674 +Reply,google.com,142.250.74.174,80,15,335.196 +Reply,google.com,142.250.74.174,80,16,279.873 +Reply,google.com,142.250.74.174,80,17,384.850 +Reply,google.com,142.250.74.174,80,18,279.908 +Reply,google.com,142.250.74.174,80,19,329.757 +Reply,google.com,142.250.74.174,80,20,353.001 +Reply,google.com,142.250.74.174,80,21,377.345 +Reply,google.com,142.250.74.174,80,22,279.068 +Reply,google.com,142.250.74.174,80,23,289.550 +Reply,google.com,142.250.74.174,80,24,290.410 +Reply,google.com,142.250.74.174,80,25,291.619 +Reply,google.com,142.250.74.174,80,26,288.640 +Reply,google.com,142.250.74.174,80,27,292.917 +Reply,google.com,142.250.74.174,80,28,287.585 +Reply,google.com,142.250.74.174,80,29,284.054 +Reply,google.com,142.250.74.174,80,30,295.039 +Reply,google.com,142.250.74.174,80,31,309.916 +Reply,google.com,142.250.74.174,80,32,288.380 +Reply,google.com,142.250.74.174,80,33,357.919 +Reply,google.com,142.250.74.174,80,34,381.970 +Reply,google.com,142.250.74.174,80,35,360.870 +Reply,google.com,142.250.74.174,80,36,328.463 +Reply,google.com,142.250.74.174,80,37,352.036 +Reply,google.com,142.250.74.174,80,38,285.379 diff --git a/test_stats.csv b/test_stats.csv new file mode 100644 index 0000000..6d5d1c9 --- /dev/null +++ b/test_stats.csv @@ -0,0 +1,44 @@ +Metric,Value +Timestamp,2024-12-07T23:55:00+05:30 +Total Packets,3 +Successful Probes,3 +Unsuccessful Probes,0 +Packet Loss,0.00% +Last Successful Probe,2024-12-07 23:54:59 +Last Unsuccessful Probe,0001-01-01 00:00:00 +Total Uptime,3 seconds +Total Downtime,0 second +Longest Uptime,3 seconds,From,2024-12-07 23:54:57,To,2024-12-07 23:55:00 +Retried Hostname Lookups,0 +RTT Min,306.725 ms,RTT Avg,330.498 ms,RTT Max,354.380 ms +TCPing Started At,2024-12-07 23:54:57 +Duration (HH:MM:SS),00:00:03 +Timestamp,2024-12-07T23:55:10+05:30 +Total Packets,13 +Successful Probes,13 +Unsuccessful Probes,0 +Packet Loss,0.00% +Last Successful Probe,2024-12-07 23:55:09 +Last Unsuccessful Probe,0001-01-01 00:00:00 +Total Uptime,13 seconds +Total Downtime,0 second +Longest Uptime,13 seconds,From,2024-12-07 23:54:57,To,2024-12-07 23:55:10 +Retried Hostname Lookups,0 +RTT Min,278.473 ms,RTT Avg,321.766 ms,RTT Max,402.546 ms +TCPing Started At,2024-12-07 23:54:57 +Duration (HH:MM:SS),00:00:13 +Timestamp,2024-12-07T23:55:35+05:30 +Total Packets,38 +Successful Probes,38 +Unsuccessful Probes,0 +Packet Loss,0.00% +Last Successful Probe,2024-12-07 23:55:34 +Last Unsuccessful Probe,0001-01-01 00:00:00 +Total Uptime,38 seconds +Total Downtime,0 second +Longest Uptime,38 seconds,From,2024-12-07 23:54:57,To,2024-12-07 23:55:35 +Retried Hostname Lookups,0 +RTT Min,278.473 ms,RTT Avg,318.379 ms,RTT Max,402.546 ms +TCPing Started At,2024-12-07 23:54:57 +TCPing Ended At,2024-12-07 23:55:35 +Duration (HH:MM:SS),00:00:38 From e75076bddf891ef9d79025cc05838cfbc96549b2 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sun, 8 Dec 2024 00:57:19 +0530 Subject: [PATCH 19/25] adjust tests --- csv_test.go | 196 +++++++++++++++++++++++++++------------------------- 1 file changed, 100 insertions(+), 96 deletions(-) diff --git a/csv_test.go b/csv_test.go index 476f19d..819156c 100644 --- a/csv_test.go +++ b/csv_test.go @@ -1,119 +1,123 @@ package main import ( - "encoding/csv" - "os" - "testing" - "time" + "encoding/csv" + "os" + "testing" + "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/assert" ) func TestNewCSVPrinter(t *testing.T) { - dataFilename := "test_data.csv" - showTimestamp := true - - cp, err := newCSVPrinter(dataFilename, showTimestamp) - assert.NoError(t, err) - assert.NotNil(t, cp) - assert.Equal(t, dataFilename, cp.dataFilename) - assert.Equal(t, dataFilename[:len(dataFilename)-4]+"_stats.csv", cp.statsFilename) - - cp.cleanup() - os.Remove(dataFilename) - os.Remove(cp.statsFilename) + dataFilename := "test_data.csv" + showTimestamp := true + showLocalAddress := true + + cp, err := newCSVPrinter(dataFilename, showTimestamp, showLocalAddress) + assert.NoError(t, err) + assert.NotNil(t, cp) + assert.Equal(t, dataFilename, cp.dataFilename) + assert.Equal(t, dataFilename[:len(dataFilename)-4]+"_stats.csv", cp.statsFilename) + + cp.cleanup() + os.Remove(dataFilename) + os.Remove(cp.statsFilename) } func TestWriteRecord(t *testing.T) { - dataFilename := "test_data.csv" - showTimestamp := false - - cp, err := newCSVPrinter(dataFilename, showTimestamp) - assert.NoError(t, err) - assert.NotNil(t, cp) - - record := []string{"Success", "hostname", "127.0.0.1", "80", "1", "10.123"} - err = cp.writeRecord(record) - assert.NoError(t, err) - - // Verify the record is written - file, err := os.Open(dataFilename) - assert.NoError(t, err) - defer file.Close() - - reader := csv.NewReader(file) - headers, err := reader.Read() - assert.NoError(t, err) - assert.Equal(t, []string{"Status", "Hostname", "IP", "Port", "TCP_Conn", "Latency(ms)"}, headers) - - readRecord, err := reader.Read() - assert.NoError(t, err) - assert.Equal(t, record, readRecord) - - // Cleanup - cp.cleanup() - os.Remove(dataFilename) - os.Remove(cp.statsFilename) + dataFilename := "test_data.csv" + showTimestamp := false + showLocalAddress := true + + cp, err := newCSVPrinter(dataFilename, showTimestamp, showLocalAddress) + assert.NoError(t, err) + assert.NotNil(t, cp) + + record := []string{"Success", "hostname", "127.0.0.1", "80", "1", "10.123", "localAddr"} + err = cp.writeRecord(record) + assert.NoError(t, err) + + // Verify the record is written + file, err := os.Open(dataFilename) + assert.NoError(t, err) + defer file.Close() + + reader := csv.NewReader(file) + headers, err := reader.Read() + assert.NoError(t, err) + assert.Equal(t, []string{"Status", "Hostname", "IP", "Port", "TCP_Conn", "Latency(ms)", "Local Address"}, headers) + + readRecord, err := reader.Read() + assert.NoError(t, err) + assert.Equal(t, record, readRecord) + + // Cleanup + cp.cleanup() + os.Remove(dataFilename) + os.Remove(cp.statsFilename) } func TestWriteStatistics(t *testing.T) { - dataFilename := "test_data.csv" - showTimestamp := true - - cp, err := newCSVPrinter(dataFilename, showTimestamp) - assert.NoError(t, err) - assert.NotNil(t, cp) - - tcping := tcping{ - totalSuccessfulProbes: 1, - totalUnsuccessfulProbes: 0, - lastSuccessfulProbe: time.Now(), - startTime: time.Now(), - } - - cp.printStatistics(tcping) - - statsFile, err := os.Open(cp.statsFilename) - assert.NoError(t, err) - defer statsFile.Close() - - reader := csv.NewReader(statsFile) - headers, err := reader.Read() - assert.NoError(t, err) - assert.Equal(t, []string{"Metric", "Value"}, headers) - - for { - record, err := reader.Read() - if err != nil { - break - } - assert.NotEmpty(t, record) - } - - cp.cleanup() - os.Remove(dataFilename) - os.Remove(cp.statsFilename) + dataFilename := "test_data.csv" + showTimestamp := true + showLocalAddress := false + + cp, err := newCSVPrinter(dataFilename, showTimestamp, showLocalAddress) + assert.NoError(t, err) + assert.NotNil(t, cp) + + tcping := tcping{ + totalSuccessfulProbes: 1, + totalUnsuccessfulProbes: 0, + lastSuccessfulProbe: time.Now(), + startTime: time.Now(), + } + + cp.printStatistics(tcping) + + statsFile, err := os.Open(cp.statsFilename) + assert.NoError(t, err) + defer statsFile.Close() + + reader := csv.NewReader(statsFile) + headers, err := reader.Read() + assert.NoError(t, err) + assert.Equal(t, []string{"Metric", "Value"}, headers) + + for { + record, err := reader.Read() + if err != nil { + break + } + assert.NotEmpty(t, record) + } + + cp.cleanup() + os.Remove(dataFilename) + os.Remove(cp.statsFilename) } func TestCleanup(t *testing.T) { - dataFilename := "test_data.csv" - showTimestamp := true + dataFilename := "test_data.csv" + showTimestamp := true + showLocalAddress := false - cp, err := newCSVPrinter(dataFilename, showTimestamp) - assert.NoError(t, err) - assert.NotNil(t, cp) + cp, err := newCSVPrinter(dataFilename, showTimestamp, showLocalAddress) + assert.NoError(t, err) + assert.NotNil(t, cp) - os.Create(dataFilename) - os.Create(cp.statsFilename) + os.Create(dataFilename) + os.Create(cp.statsFilename) - cp.cleanup() + cp.cleanup() - _, err = os.Stat(dataFilename) - assert.NoError(t, err) + _, err = os.Stat(dataFilename) + assert.NoError(t, err) - _, err = os.Stat(cp.statsFilename) - assert.NoError(t, err) + _, err = os.Stat(cp.statsFilename) + assert.NoError(t, err) - os.Remove(dataFilename) - os.Remove(cp.statsFilename) + os.Remove(dataFilename) + os.Remove(cp.statsFilename) } From 5efc9ea9441594ad64ae105c1fbe0e282308548c Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sun, 8 Dec 2024 01:06:34 +0530 Subject: [PATCH 20/25] fix tests --- csv_test.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/csv_test.go b/csv_test.go index 819156c..683af92 100644 --- a/csv_test.go +++ b/csv_test.go @@ -97,7 +97,6 @@ func TestWriteStatistics(t *testing.T) { os.Remove(dataFilename) os.Remove(cp.statsFilename) } - func TestCleanup(t *testing.T) { dataFilename := "test_data.csv" showTimestamp := true @@ -107,17 +106,26 @@ func TestCleanup(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, cp) - os.Create(dataFilename) - os.Create(cp.statsFilename) + // Call printStatistics to ensure the stats file is created + tcping := tcping{ + totalSuccessfulProbes: 1, + totalUnsuccessfulProbes: 0, + lastSuccessfulProbe: time.Now(), + startTime: time.Now(), + } + cp.printStatistics(tcping) + // Perform cleanup cp.cleanup() + // Verify files are closed and flushed _, err = os.Stat(dataFilename) assert.NoError(t, err) _, err = os.Stat(cp.statsFilename) assert.NoError(t, err) + // Cleanup files os.Remove(dataFilename) os.Remove(cp.statsFilename) } From 3619ab61851dd3214dde7357ad1f2b9ac542738d Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sun, 8 Dec 2024 11:52:30 +0530 Subject: [PATCH 21/25] fix types --- csv.go | 20 ++++++++------------ result.csv | 25 +++++++++++++++++++++++++ result_stats.csv | 16 ++++++++++++++++ tcping.go | 9 +++++---- test.csv | 42 +++--------------------------------------- test_stats.csv | 48 ++++++++++-------------------------------------- 6 files changed, 67 insertions(+), 93 deletions(-) create mode 100644 result.csv create mode 100644 result_stats.csv diff --git a/csv.go b/csv.go index 7ed3876..a9b8836 100644 --- a/csv.go +++ b/csv.go @@ -13,8 +13,8 @@ type csvPrinter struct { file *os.File dataFilename string headerDone bool - showTimestamp bool - showLocalAddress bool + showTimestamp *bool + showLocalAddress *bool statsWriter *csv.Writer statsFile *os.File statsFilename string @@ -22,9 +22,6 @@ type csvPrinter struct { cleanup func() } -const ( - csvTimeFormat = "2006-01-02 15:04:05.000" -) const ( colStatus = "Status" @@ -45,7 +42,7 @@ func ensureCSVExtension(filename string) string { return filename + ".csv" } -func newCSVPrinter(dataFilename string, showTimestamp bool, showLocalAddress bool) (*csvPrinter, error) { +func newCSVPrinter(dataFilename string, showTimestamp *bool, showLocalAddress *bool) (*csvPrinter, error) { // Ensure .csv extension for dataFilename dataFilename = ensureCSVExtension(dataFilename) @@ -57,7 +54,6 @@ func newCSVPrinter(dataFilename string, showTimestamp bool, showLocalAddress boo // Append _stats before the .csv extension for statsFilename statsFilename := dataFilename[:len(dataFilename)-4] + "_stats.csv" - cp := &csvPrinter{ writer: csv.NewWriter(file), file: file, @@ -96,11 +92,11 @@ func (cp *csvPrinter) writeHeader() error { colLatency, } - if cp.showLocalAddress { + if *cp.showLocalAddress { headers = append(headers, colLocalAddress) } - if cp.showTimestamp { + if *cp.showTimestamp { headers = append(headers, colTimestamp) } @@ -130,8 +126,8 @@ func (cp *csvPrinter) writeRecord(record []string) error { cp.headerDone = true } - if cp.showTimestamp { - record = append(record, time.Now().Format(csvTimeFormat)) + if *cp.showTimestamp { + record = append(record, time.Now().Format(timeFormat)) } if err := cp.writer.Write(record); err != nil { @@ -161,7 +157,7 @@ func (cp *csvPrinter) printProbeSuccess(localAddr string, userInput userInput, s fmt.Sprintf("%.3f", rtt), } - if cp.showLocalAddress { + if *cp.showLocalAddress { record = append(record, localAddr) } diff --git a/result.csv b/result.csv new file mode 100644 index 0000000..d0f66e3 --- /dev/null +++ b/result.csv @@ -0,0 +1,25 @@ +Status,Hostname,IP,Port,TCP_Conn,Latency(ms),Timestamp +Reply,google.com,216.58.207.206,80,1,293.349,2024-12-08 01:14:32 +Reply,google.com,216.58.207.206,80,2,283.458,2024-12-08 01:14:33 +Reply,google.com,216.58.207.206,80,3,286.398,2024-12-08 01:14:34 +Reply,google.com,216.58.207.206,80,4,277.211,2024-12-08 01:14:35 +Reply,google.com,216.58.207.206,80,5,335.059,2024-12-08 01:14:37 +Reply,google.com,216.58.207.206,80,6,288.685,2024-12-08 01:14:37 +Reply,google.com,216.58.207.206,80,7,278.510,2024-12-08 01:14:38 +Reply,google.com,216.58.207.206,80,8,295.153,2024-12-08 01:14:39 +Reply,google.com,216.58.207.206,80,9,298.399,2024-12-08 01:14:40 +Reply,google.com,216.58.207.206,80,10,280.830,2024-12-08 01:14:41 +Reply,google.com,216.58.207.206,80,11,376.692,2024-12-08 01:14:43 +Reply,google.com,216.58.207.206,80,12,288.564,2024-12-08 01:14:43 +Reply,google.com,216.58.207.206,80,13,322.513,2024-12-08 01:14:45 +Reply,google.com,216.58.207.206,80,14,277.871,2024-12-08 01:14:45 +Reply,google.com,216.58.207.206,80,15,289.862,2024-12-08 01:14:46 +Reply,google.com,216.58.207.206,80,16,295.957,2024-12-08 01:14:47 +Reply,google.com,216.58.207.206,80,17,296.148,2024-12-08 01:14:48 +Reply,google.com,216.58.207.206,80,18,287.333,2024-12-08 01:14:49 +Reply,google.com,216.58.207.206,80,19,279.460,2024-12-08 01:14:50 +Reply,google.com,216.58.207.206,80,20,297.491,2024-12-08 01:14:51 +Reply,google.com,216.58.207.206,80,21,284.533,2024-12-08 01:14:52 +Reply,google.com,216.58.207.206,80,22,294.538,2024-12-08 01:14:53 +Reply,google.com,216.58.207.206,80,23,358.669,2024-12-08 01:14:55 +Reply,google.com,216.58.207.206,80,24,289.248,2024-12-08 01:14:55 diff --git a/result_stats.csv b/result_stats.csv new file mode 100644 index 0000000..7e4041c --- /dev/null +++ b/result_stats.csv @@ -0,0 +1,16 @@ +Metric,Value +Timestamp,2024-12-08T01:14:56+05:30 +Total Packets,24 +Successful Probes,24 +Unsuccessful Probes,0 +Packet Loss,0.00% +Last Successful Probe,2024-12-08 01:14:55 +Last Unsuccessful Probe,0001-01-01 00:00:00 +Total Uptime,24 seconds +Total Downtime,0 second +Longest Uptime,24 seconds,From,2024-12-08 01:14:32,To,2024-12-08 01:14:56 +Retried Hostname Lookups,0 +RTT Min,277.211 ms,RTT Avg,298.164 ms,RTT Max,376.692 ms +TCPing Started At,2024-12-08 01:14:32 +TCPing Ended At,2024-12-08 01:14:56 +Duration (HH:MM:SS),00:00:24 diff --git a/tcping.go b/tcping.go index aeb99b2..ff29c4a 100644 --- a/tcping.go +++ b/tcping.go @@ -14,7 +14,7 @@ import ( "strings" "syscall" "time" - + "fmt" "github.com/google/go-github/v45/github" ) @@ -235,7 +235,7 @@ func usage() { } // setPrinter selects the printer -func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, noColor *bool, timeStamp *bool, outputDb *string, outputCSV *string, args []string) { +func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, noColor *bool, timeStamp *bool, localAddress *bool, outputDb *string, outputCSV *string, args []string) { if *prettyJSON && !*outputJSON { colorRed("--pretty has no effect without the -j flag.") usage() @@ -246,7 +246,7 @@ func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, noColor *bool, tim tcping.printer = newDB(*outputDb, args) } else if *outputCSV != "" { var err error - tcping.printer, err = newCSVPrinter(*outputCSV, *timeStamp, tcping.userInput.showLocalAddress) + tcping.printer, err = newCSVPrinter(*outputCSV, timeStamp, localAddress) if err != nil { tcping.printError("Failed to create CSV file: %s", err) os.Exit(1) @@ -354,6 +354,7 @@ func processUserInput(tcping *tcping) { showFailuresOnly := flag.Bool("show-failures-only", false, "Show only the failed probes.") showHelp := flag.Bool("h", false, "show help message.") + flag.CommandLine.Usage = usage permuteArgs(os.Args[1:]) @@ -364,7 +365,7 @@ func processUserInput(tcping *tcping) { // we need to set printers first, because they're used for // error reporting and other output. - setPrinter(tcping, outputJSON, prettyJSON, noColor, showTimestamp, outputDB, saveToCSV, args) + setPrinter(tcping, outputJSON, prettyJSON, noColor, showTimestamp, showLocalAddress, outputDB, saveToCSV, args) // Handle -v flag if *showVer { diff --git a/test.csv b/test.csv index 047edd8..87a3e27 100644 --- a/test.csv +++ b/test.csv @@ -1,39 +1,3 @@ -Status,Hostname,IP,Port,TCP_Conn,Latency(ms) -Reply,google.com,142.250.74.174,80,1,306.725 -Reply,google.com,142.250.74.174,80,2,330.389 -Reply,google.com,142.250.74.174,80,3,354.380 -Reply,google.com,142.250.74.174,80,4,293.406 -Reply,google.com,142.250.74.174,80,5,402.546 -Reply,google.com,142.250.74.174,80,6,324.038 -Reply,google.com,142.250.74.174,80,7,347.952 -Reply,google.com,142.250.74.174,80,8,279.303 -Reply,google.com,142.250.74.174,80,9,293.731 -Reply,google.com,142.250.74.174,80,10,287.733 -Reply,google.com,142.250.74.174,80,11,294.353 -Reply,google.com,142.250.74.174,80,12,278.473 -Reply,google.com,142.250.74.174,80,13,389.935 -Reply,google.com,142.250.74.174,80,14,311.674 -Reply,google.com,142.250.74.174,80,15,335.196 -Reply,google.com,142.250.74.174,80,16,279.873 -Reply,google.com,142.250.74.174,80,17,384.850 -Reply,google.com,142.250.74.174,80,18,279.908 -Reply,google.com,142.250.74.174,80,19,329.757 -Reply,google.com,142.250.74.174,80,20,353.001 -Reply,google.com,142.250.74.174,80,21,377.345 -Reply,google.com,142.250.74.174,80,22,279.068 -Reply,google.com,142.250.74.174,80,23,289.550 -Reply,google.com,142.250.74.174,80,24,290.410 -Reply,google.com,142.250.74.174,80,25,291.619 -Reply,google.com,142.250.74.174,80,26,288.640 -Reply,google.com,142.250.74.174,80,27,292.917 -Reply,google.com,142.250.74.174,80,28,287.585 -Reply,google.com,142.250.74.174,80,29,284.054 -Reply,google.com,142.250.74.174,80,30,295.039 -Reply,google.com,142.250.74.174,80,31,309.916 -Reply,google.com,142.250.74.174,80,32,288.380 -Reply,google.com,142.250.74.174,80,33,357.919 -Reply,google.com,142.250.74.174,80,34,381.970 -Reply,google.com,142.250.74.174,80,35,360.870 -Reply,google.com,142.250.74.174,80,36,328.463 -Reply,google.com,142.250.74.174,80,37,352.036 -Reply,google.com,142.250.74.174,80,38,285.379 +Status,Hostname,IP,Port,TCP_Conn,Latency(ms),Local Address,Timestamp +Reply,google.com,142.250.183.46,80,1,26.628,192.168.1.11:49985,2024-12-08 11:52:13 +Reply,google.com,142.250.183.46,80,2,34.806,192.168.1.11:49986,2024-12-08 11:52:14 diff --git a/test_stats.csv b/test_stats.csv index 6d5d1c9..103c2e1 100644 --- a/test_stats.csv +++ b/test_stats.csv @@ -1,44 +1,16 @@ Metric,Value -Timestamp,2024-12-07T23:55:00+05:30 -Total Packets,3 -Successful Probes,3 +Timestamp,2024-12-08T11:52:14+05:30 +Total Packets,2 +Successful Probes,2 Unsuccessful Probes,0 Packet Loss,0.00% -Last Successful Probe,2024-12-07 23:54:59 +Last Successful Probe,2024-12-08 11:52:14 Last Unsuccessful Probe,0001-01-01 00:00:00 -Total Uptime,3 seconds +Total Uptime,2 seconds Total Downtime,0 second -Longest Uptime,3 seconds,From,2024-12-07 23:54:57,To,2024-12-07 23:55:00 +Longest Uptime,2 seconds,From,2024-12-08 11:52:13,To,2024-12-08 11:52:14 Retried Hostname Lookups,0 -RTT Min,306.725 ms,RTT Avg,330.498 ms,RTT Max,354.380 ms -TCPing Started At,2024-12-07 23:54:57 -Duration (HH:MM:SS),00:00:03 -Timestamp,2024-12-07T23:55:10+05:30 -Total Packets,13 -Successful Probes,13 -Unsuccessful Probes,0 -Packet Loss,0.00% -Last Successful Probe,2024-12-07 23:55:09 -Last Unsuccessful Probe,0001-01-01 00:00:00 -Total Uptime,13 seconds -Total Downtime,0 second -Longest Uptime,13 seconds,From,2024-12-07 23:54:57,To,2024-12-07 23:55:10 -Retried Hostname Lookups,0 -RTT Min,278.473 ms,RTT Avg,321.766 ms,RTT Max,402.546 ms -TCPing Started At,2024-12-07 23:54:57 -Duration (HH:MM:SS),00:00:13 -Timestamp,2024-12-07T23:55:35+05:30 -Total Packets,38 -Successful Probes,38 -Unsuccessful Probes,0 -Packet Loss,0.00% -Last Successful Probe,2024-12-07 23:55:34 -Last Unsuccessful Probe,0001-01-01 00:00:00 -Total Uptime,38 seconds -Total Downtime,0 second -Longest Uptime,38 seconds,From,2024-12-07 23:54:57,To,2024-12-07 23:55:35 -Retried Hostname Lookups,0 -RTT Min,278.473 ms,RTT Avg,318.379 ms,RTT Max,402.546 ms -TCPing Started At,2024-12-07 23:54:57 -TCPing Ended At,2024-12-07 23:55:35 -Duration (HH:MM:SS),00:00:38 +RTT Min,26.628 ms,RTT Avg,30.717 ms,RTT Max,34.806 ms +TCPing Started At,2024-12-08 11:52:13 +TCPing Ended At,2024-12-08 11:52:14 +Duration (HH:MM:SS),00:00:02 From 5a9ef968c76470c25b8e2fa437d9d3e74fa50d01 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sun, 8 Dec 2024 12:20:23 +0530 Subject: [PATCH 22/25] delete output files --- result.csv | 25 ------------------------- result_stats.csv | 16 ---------------- tcping.go | 1 - test.csv | 3 --- test_stats.csv | 16 ---------------- 5 files changed, 61 deletions(-) delete mode 100644 result.csv delete mode 100644 result_stats.csv delete mode 100644 test.csv delete mode 100644 test_stats.csv diff --git a/result.csv b/result.csv deleted file mode 100644 index d0f66e3..0000000 --- a/result.csv +++ /dev/null @@ -1,25 +0,0 @@ -Status,Hostname,IP,Port,TCP_Conn,Latency(ms),Timestamp -Reply,google.com,216.58.207.206,80,1,293.349,2024-12-08 01:14:32 -Reply,google.com,216.58.207.206,80,2,283.458,2024-12-08 01:14:33 -Reply,google.com,216.58.207.206,80,3,286.398,2024-12-08 01:14:34 -Reply,google.com,216.58.207.206,80,4,277.211,2024-12-08 01:14:35 -Reply,google.com,216.58.207.206,80,5,335.059,2024-12-08 01:14:37 -Reply,google.com,216.58.207.206,80,6,288.685,2024-12-08 01:14:37 -Reply,google.com,216.58.207.206,80,7,278.510,2024-12-08 01:14:38 -Reply,google.com,216.58.207.206,80,8,295.153,2024-12-08 01:14:39 -Reply,google.com,216.58.207.206,80,9,298.399,2024-12-08 01:14:40 -Reply,google.com,216.58.207.206,80,10,280.830,2024-12-08 01:14:41 -Reply,google.com,216.58.207.206,80,11,376.692,2024-12-08 01:14:43 -Reply,google.com,216.58.207.206,80,12,288.564,2024-12-08 01:14:43 -Reply,google.com,216.58.207.206,80,13,322.513,2024-12-08 01:14:45 -Reply,google.com,216.58.207.206,80,14,277.871,2024-12-08 01:14:45 -Reply,google.com,216.58.207.206,80,15,289.862,2024-12-08 01:14:46 -Reply,google.com,216.58.207.206,80,16,295.957,2024-12-08 01:14:47 -Reply,google.com,216.58.207.206,80,17,296.148,2024-12-08 01:14:48 -Reply,google.com,216.58.207.206,80,18,287.333,2024-12-08 01:14:49 -Reply,google.com,216.58.207.206,80,19,279.460,2024-12-08 01:14:50 -Reply,google.com,216.58.207.206,80,20,297.491,2024-12-08 01:14:51 -Reply,google.com,216.58.207.206,80,21,284.533,2024-12-08 01:14:52 -Reply,google.com,216.58.207.206,80,22,294.538,2024-12-08 01:14:53 -Reply,google.com,216.58.207.206,80,23,358.669,2024-12-08 01:14:55 -Reply,google.com,216.58.207.206,80,24,289.248,2024-12-08 01:14:55 diff --git a/result_stats.csv b/result_stats.csv deleted file mode 100644 index 7e4041c..0000000 --- a/result_stats.csv +++ /dev/null @@ -1,16 +0,0 @@ -Metric,Value -Timestamp,2024-12-08T01:14:56+05:30 -Total Packets,24 -Successful Probes,24 -Unsuccessful Probes,0 -Packet Loss,0.00% -Last Successful Probe,2024-12-08 01:14:55 -Last Unsuccessful Probe,0001-01-01 00:00:00 -Total Uptime,24 seconds -Total Downtime,0 second -Longest Uptime,24 seconds,From,2024-12-08 01:14:32,To,2024-12-08 01:14:56 -Retried Hostname Lookups,0 -RTT Min,277.211 ms,RTT Avg,298.164 ms,RTT Max,376.692 ms -TCPing Started At,2024-12-08 01:14:32 -TCPing Ended At,2024-12-08 01:14:56 -Duration (HH:MM:SS),00:00:24 diff --git a/tcping.go b/tcping.go index ff29c4a..2db4a14 100644 --- a/tcping.go +++ b/tcping.go @@ -14,7 +14,6 @@ import ( "strings" "syscall" "time" - "fmt" "github.com/google/go-github/v45/github" ) diff --git a/test.csv b/test.csv deleted file mode 100644 index 87a3e27..0000000 --- a/test.csv +++ /dev/null @@ -1,3 +0,0 @@ -Status,Hostname,IP,Port,TCP_Conn,Latency(ms),Local Address,Timestamp -Reply,google.com,142.250.183.46,80,1,26.628,192.168.1.11:49985,2024-12-08 11:52:13 -Reply,google.com,142.250.183.46,80,2,34.806,192.168.1.11:49986,2024-12-08 11:52:14 diff --git a/test_stats.csv b/test_stats.csv deleted file mode 100644 index 103c2e1..0000000 --- a/test_stats.csv +++ /dev/null @@ -1,16 +0,0 @@ -Metric,Value -Timestamp,2024-12-08T11:52:14+05:30 -Total Packets,2 -Successful Probes,2 -Unsuccessful Probes,0 -Packet Loss,0.00% -Last Successful Probe,2024-12-08 11:52:14 -Last Unsuccessful Probe,0001-01-01 00:00:00 -Total Uptime,2 seconds -Total Downtime,0 second -Longest Uptime,2 seconds,From,2024-12-08 11:52:13,To,2024-12-08 11:52:14 -Retried Hostname Lookups,0 -RTT Min,26.628 ms,RTT Avg,30.717 ms,RTT Max,34.806 ms -TCPing Started At,2024-12-08 11:52:13 -TCPing Ended At,2024-12-08 11:52:14 -Duration (HH:MM:SS),00:00:02 From 4f5682a303a8414fe176460ead0fc4f24b8b68fd Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sun, 8 Dec 2024 12:25:19 +0530 Subject: [PATCH 23/25] fix tests --- csv_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/csv_test.go b/csv_test.go index 683af92..0650078 100644 --- a/csv_test.go +++ b/csv_test.go @@ -14,7 +14,7 @@ func TestNewCSVPrinter(t *testing.T) { showTimestamp := true showLocalAddress := true - cp, err := newCSVPrinter(dataFilename, showTimestamp, showLocalAddress) + cp, err := newCSVPrinter(dataFilename, &showTimestamp, &showLocalAddress) assert.NoError(t, err) assert.NotNil(t, cp) assert.Equal(t, dataFilename, cp.dataFilename) @@ -30,7 +30,7 @@ func TestWriteRecord(t *testing.T) { showTimestamp := false showLocalAddress := true - cp, err := newCSVPrinter(dataFilename, showTimestamp, showLocalAddress) + cp, err := newCSVPrinter(dataFilename, &showTimestamp, &showLocalAddress) assert.NoError(t, err) assert.NotNil(t, cp) @@ -63,7 +63,7 @@ func TestWriteStatistics(t *testing.T) { showTimestamp := true showLocalAddress := false - cp, err := newCSVPrinter(dataFilename, showTimestamp, showLocalAddress) + cp, err := newCSVPrinter(dataFilename, &showTimestamp, &showLocalAddress) assert.NoError(t, err) assert.NotNil(t, cp) @@ -97,12 +97,13 @@ func TestWriteStatistics(t *testing.T) { os.Remove(dataFilename) os.Remove(cp.statsFilename) } + func TestCleanup(t *testing.T) { dataFilename := "test_data.csv" showTimestamp := true showLocalAddress := false - cp, err := newCSVPrinter(dataFilename, showTimestamp, showLocalAddress) + cp, err := newCSVPrinter(dataFilename, &showTimestamp, &showLocalAddress) assert.NoError(t, err) assert.NotNil(t, cp) From c2f9e0f4bb65468fa8deffffc11ba50aec350355 Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 21 Dec 2024 12:57:28 +0530 Subject: [PATCH 24/25] address formatting issue in csv --- csv.go | 591 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 296 insertions(+), 295 deletions(-) diff --git a/csv.go b/csv.go index a9b8836..36bd8cc 100644 --- a/csv.go +++ b/csv.go @@ -1,40 +1,38 @@ package main import ( - "encoding/csv" - "fmt" - "math" - "os" - "time" + "encoding/csv" + "fmt" + "math" + "os" + "time" ) type csvPrinter struct { - writer *csv.Writer - file *os.File - dataFilename string - headerDone bool - showTimestamp *bool - showLocalAddress *bool - statsWriter *csv.Writer - statsFile *os.File - statsFilename string - statsHeaderDone bool - cleanup func() + writer *csv.Writer + file *os.File + dataFilename string + headerDone bool + showTimestamp *bool + showLocalAddress *bool + statsWriter *csv.Writer + statsFile *os.File + statsFilename string + statsHeaderDone bool + cleanup func() } - const ( - colStatus = "Status" - colTimestamp = "Timestamp" - colHostname = "Hostname" - colIP = "IP" - colPort = "Port" - colTCPConn = "TCP_Conn" - colLatency = "Latency(ms)" - colLocalAddress = "Local Address" + colStatus = "Status" + colTimestamp = "Timestamp" + colHostname = "Hostname" + colIP = "IP" + colPort = "Port" + colTCPConn = "TCP_Conn" + colLatency = "Latency(ms)" + colLocalAddress = "Local Address" ) - func ensureCSVExtension(filename string) string { if len(filename) > 4 && filename[len(filename)-4:] == ".csv" { return filename @@ -43,307 +41,310 @@ func ensureCSVExtension(filename string) string { } func newCSVPrinter(dataFilename string, showTimestamp *bool, showLocalAddress *bool) (*csvPrinter, error) { - // Ensure .csv extension for dataFilename - dataFilename = ensureCSVExtension(dataFilename) - - // Open the data file with the os.O_TRUNC flag to truncate it - file, err := os.OpenFile(dataFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return nil, fmt.Errorf("error creating data CSV file: %w", err) - } - - // Append _stats before the .csv extension for statsFilename - statsFilename := dataFilename[:len(dataFilename)-4] + "_stats.csv" - cp := &csvPrinter{ - writer: csv.NewWriter(file), - file: file, - dataFilename: dataFilename, - statsFilename: statsFilename, - showTimestamp: showTimestamp, - showLocalAddress: showLocalAddress, - } - - cp.cleanup = func() { - if cp.writer != nil { - cp.writer.Flush() - } - if cp.file != nil { - cp.file.Close() - } - if cp.statsWriter != nil { - cp.statsWriter.Flush() - } - if cp.statsFile != nil { - cp.statsFile.Close() - } - } - - return cp, nil -} + // Ensure .csv extension for dataFilename + dataFilename = ensureCSVExtension(dataFilename) + + // Open the data file with the os.O_TRUNC flag to truncate it + file, err := os.OpenFile(dataFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return nil, fmt.Errorf("error creating data CSV file: %w", err) + } + // Append _stats before the .csv extension for statsFilename + statsFilename := dataFilename[:len(dataFilename)-4] + "_stats.csv" + cp := &csvPrinter{ + writer: csv.NewWriter(file), + file: file, + dataFilename: dataFilename, + statsFilename: statsFilename, + showTimestamp: showTimestamp, + showLocalAddress: showLocalAddress, + } + + cp.cleanup = func() { + if cp.writer != nil { + cp.writer.Flush() + } + if cp.file != nil { + cp.file.Close() + } + if cp.statsWriter != nil { + cp.statsWriter.Flush() + } + if cp.statsFile != nil { + cp.statsFile.Close() + } + } + + return cp, nil +} func (cp *csvPrinter) writeHeader() error { - headers := []string{ - colStatus, - colHostname, - colIP, - colPort, - colTCPConn, - colLatency, - } - - if *cp.showLocalAddress { - headers = append(headers, colLocalAddress) - } - - if *cp.showTimestamp { - headers = append(headers, colTimestamp) - } - - if err := cp.writer.Write(headers); err != nil { - return fmt.Errorf("failed to write headers: %w", err) - } - - cp.writer.Flush() - return cp.writer.Error() + headers := []string{ + colStatus, + colHostname, + colIP, + colPort, + colTCPConn, + colLatency, + } + + if *cp.showLocalAddress { + headers = append(headers, colLocalAddress) + } + + if *cp.showTimestamp { + headers = append(headers, colTimestamp) + } + + if err := cp.writer.Write(headers); err != nil { + return fmt.Errorf("failed to write headers: %w", err) + } + + cp.writer.Flush() + return cp.writer.Error() } func (cp *csvPrinter) writeRecord(record []string) error { - if _, err := os.Stat(cp.dataFilename); os.IsNotExist(err) { - file, err := os.OpenFile(cp.dataFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return fmt.Errorf("failed to recreate data CSV file: %w", err) - } - cp.file = file - cp.writer = csv.NewWriter(file) - cp.headerDone = false - } - - if !cp.headerDone { - if err := cp.writeHeader(); err != nil { - return err - } - cp.headerDone = true - } - - if *cp.showTimestamp { - record = append(record, time.Now().Format(timeFormat)) - } - - if err := cp.writer.Write(record); err != nil { - return fmt.Errorf("failed to write record: %w", err) - } - - cp.writer.Flush() - return cp.writer.Error() + if _, err := os.Stat(cp.dataFilename); os.IsNotExist(err) { + file, err := os.OpenFile(cp.dataFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("failed to recreate data CSV file: %w", err) + } + cp.file = file + cp.writer = csv.NewWriter(file) + cp.headerDone = false + } + + if !cp.headerDone { + if err := cp.writeHeader(); err != nil { + return err + } + cp.headerDone = true + } + + if *cp.showTimestamp { + record = append(record, time.Now().Format(timeFormat)) + } + + if err := cp.writer.Write(record); err != nil { + return fmt.Errorf("failed to write record: %w", err) + } + + cp.writer.Flush() + return cp.writer.Error() } func (cp *csvPrinter) printStart(hostname string, port uint16) { - fmt.Printf("TCPing results being written to: %s\n", cp.dataFilename) + fmt.Printf("TCPing results being written to: %s\n", cp.dataFilename) } func (cp *csvPrinter) printProbeSuccess(localAddr string, userInput userInput, streak uint, rtt float32) { - hostname := userInput.hostname - if hostname == "" { - hostname = "-" - } - - record := []string{ - "Reply", - hostname, - userInput.ip.String(), - fmt.Sprint(userInput.port), - fmt.Sprint(streak), - fmt.Sprintf("%.3f", rtt), - } - - if *cp.showLocalAddress { - record = append(record, localAddr) - } - - if err := cp.writeRecord(record); err != nil { - cp.printError("Failed to write success record: %v", err) - } + hostname := userInput.hostname + if hostname == "" { + hostname = "-" + } + + record := []string{ + "Reply", + hostname, + userInput.ip.String(), + fmt.Sprint(userInput.port), + fmt.Sprint(streak), + fmt.Sprintf("%.3f", rtt), + } + + if *cp.showLocalAddress { + record = append(record, localAddr) + } + + if err := cp.writeRecord(record); err != nil { + cp.printError("Failed to write success record: %v", err) + } } func (cp *csvPrinter) printProbeFail(userInput userInput, streak uint) { - hostname := userInput.hostname - if hostname == "" { - hostname = "-" - } - - record := []string{ - "No reply", - hostname, - userInput.ip.String(), - fmt.Sprint(userInput.port), - fmt.Sprint(streak), - "-", - } - - if err := cp.writeRecord(record); err != nil { - cp.printError("Failed to write failure record: %v", err) - } + hostname := userInput.hostname + if hostname == "" { + hostname = "-" + } + + record := []string{ + "No reply", + hostname, + userInput.ip.String(), + fmt.Sprint(userInput.port), + fmt.Sprint(streak), + "-", + } + + if err := cp.writeRecord(record); err != nil { + cp.printError("Failed to write failure record: %v", err) + } } func (cp *csvPrinter) printRetryingToResolve(hostname string) { - record := []string{ - "Resolving", - hostname, - "-", - "-", - "-", - "-", - } - - if err := cp.writeRecord(record); err != nil { - cp.printError("Failed to write resolve record: %v", err) - } + record := []string{ + "Resolving", + hostname, + "-", + "-", + "-", + "-", + } + + if err := cp.writeRecord(record); err != nil { + cp.printError("Failed to write resolve record: %v", err) + } } func (cp *csvPrinter) printError(format string, args ...any) { - fmt.Fprintf(os.Stderr, "CSV Error: "+format+"\n", args...) + fmt.Fprintf(os.Stderr, "CSV Error: "+format+"\n", args...) } func (cp *csvPrinter) writeStatsHeader() error { - headers := []string{ - "Metric", - "Value", - } + + headers := []string{ + "Metric", + "Value", + } - if err := cp.statsWriter.Write(headers); err != nil { - return fmt.Errorf("failed to write statistics headers: %w", err) - } + if err := cp.statsWriter.Write(headers); err != nil { + return fmt.Errorf("failed to write statistics headers: %w", err) + } - cp.statsWriter.Flush() - return cp.statsWriter.Error() + cp.statsWriter.Flush() + return cp.statsWriter.Error() } func (cp *csvPrinter) writeStatsRecord(record []string) error { - if _, err := os.Stat(cp.statsFilename); os.IsNotExist(err) { - statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return fmt.Errorf("failed to recreate statistics CSV file: %w", err) - } - cp.statsFile = statsFile - cp.statsWriter = csv.NewWriter(statsFile) - cp.statsHeaderDone = false - } - - // Write header if not done - if !cp.statsHeaderDone { - if err := cp.writeStatsHeader(); err != nil { - return err - } - cp.statsHeaderDone = true - } - - if err := cp.statsWriter.Write(record); err != nil { - return fmt.Errorf("failed to write statistics record: %w", err) - } - - cp.statsWriter.Flush() - return cp.statsWriter.Error() + if _, err := os.Stat(cp.statsFilename); os.IsNotExist(err) { + statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("failed to recreate statistics CSV file: %w", err) + } + cp.statsFile = statsFile + cp.statsWriter = csv.NewWriter(statsFile) + cp.statsHeaderDone = false + } + + // Write header if not done + if !cp.statsHeaderDone { + if err := cp.writeStatsHeader(); err != nil { + return err + } + cp.statsHeaderDone = true + } + + if err := cp.statsWriter.Write(record); err != nil { + return fmt.Errorf("failed to write statistics record: %w", err) + } + + cp.statsWriter.Flush() + return cp.statsWriter.Error() } func (cp *csvPrinter) printStatistics(t tcping) { - // Initialize stats file if not already done - if cp.statsFile == nil { - statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_TRUNC, 0644) - if err != nil { - cp.printError("Failed to create statistics CSV file: %v", err) - return - } - cp.statsFile = statsFile - cp.statsWriter = csv.NewWriter(statsFile) - cp.statsHeaderDone = false - } - - totalPackets := t.totalSuccessfulProbes + t.totalUnsuccessfulProbes - packetLoss := (float32(t.totalUnsuccessfulProbes) / float32(totalPackets)) * 100 - if math.IsNaN(float64(packetLoss)) { - packetLoss = 0 - } - - // Collect statistics data - timestamp := time.Now().Format(time.RFC3339) - statistics := [][]string{ - {"Timestamp", timestamp}, - {"Total Packets", fmt.Sprint(totalPackets)}, - {"Successful Probes", fmt.Sprint(t.totalSuccessfulProbes)}, - {"Unsuccessful Probes", fmt.Sprint(t.totalUnsuccessfulProbes)}, - {"Packet Loss", fmt.Sprintf("%.2f%%", packetLoss)}, - {"Last Successful Probe", t.lastSuccessfulProbe.Format(timeFormat)}, - {"Last Unsuccessful Probe", t.lastUnsuccessfulProbe.Format(timeFormat)}, - {"Total Uptime", durationToString(t.totalUptime)}, - {"Total Downtime", durationToString(t.totalDowntime)}, - } - - if t.longestUptime.duration != 0 { - statistics = append(statistics, []string{ - "Longest Uptime", durationToString(t.longestUptime.duration), - "From", t.longestUptime.start.Format(timeFormat), - "To", t.longestUptime.end.Format(timeFormat), - }) - } - - if t.longestDowntime.duration != 0 { - statistics = append(statistics, []string{ - "Longest Downtime", durationToString(t.longestDowntime.duration), - "From", t.longestDowntime.start.Format(timeFormat), - "To", t.longestDowntime.end.Format(timeFormat), - }) - } - - if !t.destIsIP { - statistics = append(statistics, []string{ - "Retried Hostname Lookups", fmt.Sprint(t.retriedHostnameLookups), - }) - - if len(t.hostnameChanges) >= 2 { - for i := 0; i < len(t.hostnameChanges)-1; i++ { - statistics = append(statistics, []string{ - "IP Change", t.hostnameChanges[i].Addr.String(), - "To", t.hostnameChanges[i+1].Addr.String(), - "At", t.hostnameChanges[i+1].When.Format(timeFormat), - }) - } - } - } - - if t.rttResults.hasResults { - statistics = append(statistics, []string{ - "RTT Min", fmt.Sprintf("%.3f ms", t.rttResults.min), - "RTT Avg", fmt.Sprintf("%.3f ms", t.rttResults.average), - "RTT Max", fmt.Sprintf("%.3f ms", t.rttResults.max), - }) - } - - statistics = append(statistics, []string{ - "TCPing Started At", t.startTime.Format(timeFormat), - }) - - if !t.endTime.IsZero() { - statistics = append(statistics, []string{ - "TCPing Ended At", t.endTime.Format(timeFormat), - }) - } - - durationTime := time.Time{}.Add(t.totalDowntime + t.totalUptime) - statistics = append(statistics, []string{ - "Duration (HH:MM:SS)", durationTime.Format(hourFormat), - }) - - // Write statistics to CSV - for _, record := range statistics { - if err := cp.writeStatsRecord(record); err != nil { - cp.printError("Failed to write statistics record: %v", err) - return - } - } - - // Print the message only if statistics are written - fmt.Printf("TCPing statistics written to: %s\n", cp.statsFilename) + // Initialize stats file if not already done + if cp.statsFile == nil { + statsFile, err := os.OpenFile(cp.statsFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_TRUNC, 0644) + if err != nil { + cp.printError("Failed to create statistics CSV file: %v", err) + return + } + cp.statsFile = statsFile + cp.statsWriter = csv.NewWriter(statsFile) + cp.statsHeaderDone = false + } + + totalPackets := t.totalSuccessfulProbes + t.totalUnsuccessfulProbes + packetLoss := (float32(t.totalUnsuccessfulProbes) / float32(totalPackets)) * 100 + if math.IsNaN(float64(packetLoss)) { + packetLoss = 0 + } + + // Collect statistics data + timestamp := time.Now().Format(time.RFC3339) + statistics := [][]string{ + {"Timestamp", timestamp}, + {"Total Packets", fmt.Sprint(totalPackets)}, + {"Successful Probes", fmt.Sprint(t.totalSuccessfulProbes)}, + {"Unsuccessful Probes", fmt.Sprint(t.totalUnsuccessfulProbes)}, + {"Packet Loss", fmt.Sprintf("%.2f%%", packetLoss)}, + } + + if t.lastSuccessfulProbe.IsZero() { + statistics = append(statistics, []string{"Last Successful Probe", "Never succeeded"}) + } else { + statistics = append(statistics, []string{"Last Successful Probe", t.lastSuccessfulProbe.Format(timeFormat)}) + } + + if t.lastUnsuccessfulProbe.IsZero() { + statistics = append(statistics, []string{"Last Unsuccessful Probe", "Never failed"}) + } else { + statistics = append(statistics, []string{"Last Unsuccessful Probe", t.lastUnsuccessfulProbe.Format(timeFormat)}) + } + + statistics = append(statistics, []string{"Total Uptime", durationToString(t.totalUptime)}) + statistics = append(statistics, []string{"Total Downtime", durationToString(t.totalDowntime)}) + + if t.longestUptime.duration != 0 { + statistics = append(statistics, + []string{"Longest Uptime Duration", durationToString(t.longestUptime.duration)}, + []string{"Longest Uptime From", t.longestUptime.start.Format(timeFormat)}, + []string{"Longest Uptime To", t.longestUptime.end.Format(timeFormat)}, + ) + } + + if t.longestDowntime.duration != 0 { + statistics = append(statistics, + []string{"Longest Downtime Duration", durationToString(t.longestDowntime.duration)}, + []string{"Longest Downtime From", t.longestDowntime.start.Format(timeFormat)}, + []string{"Longest Downtime To", t.longestDowntime.end.Format(timeFormat)}, + ) + } + + if !t.destIsIP { + statistics = append(statistics, []string{"Retried Hostname Lookups", fmt.Sprint(t.retriedHostnameLookups)}) + + if len(t.hostnameChanges) >= 2 { + for i := 0; i < len(t.hostnameChanges)-1; i++ { + statistics = append(statistics, + []string{"IP Change", t.hostnameChanges[i].Addr.String()}, + []string{"To", t.hostnameChanges[i+1].Addr.String()}, + []string{"At", t.hostnameChanges[i+1].When.Format(timeFormat)}, + ) + } + } + } + + if t.rttResults.hasResults { + statistics = append(statistics, + []string{"RTT Min", fmt.Sprintf("%.3f ms", t.rttResults.min)}, + []string{"RTT Avg", fmt.Sprintf("%.3f ms", t.rttResults.average)}, + []string{"RTT Max", fmt.Sprintf("%.3f ms", t.rttResults.max)}, + ) + } + + statistics = append(statistics, []string{"TCPing Started At", t.startTime.Format(timeFormat)}) + + if !t.endTime.IsZero() { + statistics = append(statistics, []string{"TCPing Ended At", t.endTime.Format(timeFormat)}) + } + + durationTime := time.Time{}.Add(t.totalDowntime + t.totalUptime) + statistics = append(statistics, []string{"Duration (HH:MM:SS)", durationTime.Format(hourFormat)}) + + // Write statistics to CSV + for _, record := range statistics { + if err := cp.writeStatsRecord(record); err != nil { + cp.printError("Failed to write statistics record: %v", err) + return + } + } + + // Print the message only if statistics are written + fmt.Printf("TCPing statistics written to: %s\n", cp.statsFilename) } // Satisfying remaining printer interface methods From 0860865bd459663d15a556e05d13beeaeed35b1b Mon Sep 17 00:00:00 2001 From: SYSHIL Date: Sat, 21 Dec 2024 16:34:03 +0530 Subject: [PATCH 25/25] nit --- tcping.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcping.go b/tcping.go index 2db4a14..020edf7 100644 --- a/tcping.go +++ b/tcping.go @@ -250,7 +250,7 @@ func setPrinter(tcping *tcping, outputJSON, prettyJSON *bool, noColor *bool, tim tcping.printError("Failed to create CSV file: %s", err) os.Exit(1) } - } else if *noColor == true { + } else if *noColor { tcping.printer = newPlanePrinter(timeStamp) } else { tcping.printer = newColorPrinter(timeStamp)