Skip to content

Add CSV Printer to tcping #254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3f7e073
initial setup with new printer and methods
Ilhan-Personal Oct 20, 2024
aca733c
add some testing
Ilhan-Personal Oct 27, 2024
d3e23cf
nits
Ilhan-Personal Oct 27, 2024
ac98cf2
nits
Ilhan-Personal Oct 27, 2024
6d04f04
add tests
Ilhan-Personal Oct 31, 2024
0b00747
add a setup function
Ilhan-Personal Oct 31, 2024
f1b8c4d
add a setup function
Ilhan-Personal Oct 31, 2024
175e486
Update csv_test.go
Ilhan-Personal Oct 31, 2024
120e5a2
Merge branch 'master' into ilu/output-csv
Ilhan-Personal Nov 1, 2024
e26dbd5
Merge branch 'master' into ilu/output-csv
pouriyajamshidi Nov 6, 2024
4c18c0c
write header
Ilhan-Personal Nov 24, 2024
034fa3d
print probes sucess and failures
Ilhan-Personal Nov 30, 2024
9bff8d1
add tests
Ilhan-Personal Dec 1, 2024
f91a153
Merge branch 'master' into ilu/output-csv
Ilhan-Personal Dec 1, 2024
653dcad
Merge branch 'master' into ilu/output-csv
Ilhan-Personal Dec 7, 2024
d11ef3d
nit
Ilhan-Personal Dec 7, 2024
7e5d2a1
satisfy interface
Ilhan-Personal Dec 7, 2024
8f50cba
fix
Ilhan-Personal Dec 7, 2024
0f7794c
add showing local address
Ilhan-Personal Dec 7, 2024
94a6c3b
typecasting
Ilhan-Personal Dec 7, 2024
561722e
fixes
Ilhan-Personal Dec 7, 2024
2317b35
fixes
Ilhan-Personal Dec 7, 2024
e75076b
adjust tests
Ilhan-Personal Dec 7, 2024
5efc9ea
fix tests
Ilhan-Personal Dec 7, 2024
3619ab6
fix types
Ilhan-Personal Dec 8, 2024
5a9ef96
delete output files
Ilhan-Personal Dec 8, 2024
4f5682a
fix tests
Ilhan-Personal Dec 8, 2024
c2f9e0f
address formatting issue in csv
Ilhan-Personal Dec 21, 2024
0860865
nit
Ilhan-Personal Dec 21, 2024
03c44ab
Merge branch 'master' into ilu/output-csv
pouriyajamshidi Dec 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 353 additions & 0 deletions csv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
package main

import (
"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()
}

const (
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
}
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 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()
}

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()
}

func (cp *csvPrinter) printStart(hostname string, port uint16) {
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)
}
}

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)
}
}

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)},
}

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
func (cp *csvPrinter) printTotalDownTime(downtime time.Duration) {}
func (cp *csvPrinter) printVersion() {}
func (cp *csvPrinter) printInfo(format string, args ...any) {}
Loading
Loading