github.com/network-quality/goresponsiveness@v0.0.0-20240129151524-343954285090/datalogger/logger.go (about) 1 /* 2 * This file is part of Go Responsiveness. 3 * 4 * Go Responsiveness is free software: you can redistribute it and/or modify it under 5 * the terms of the GNU General Public License as published by the Free Software Foundation, 6 * either version 2 of the License, or (at your option) any later version. 7 * Go Responsiveness is distributed in the hope that it will be useful, but WITHOUT ANY 8 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 9 * PARTICULAR PURPOSE. See the GNU General Public License for more details. 10 * 11 * You should have received a copy of the GNU General Public License along 12 * with Go Responsiveness. If not, see <https://www.gnu.org/licenses/>. 13 */ 14 15 package datalogger 16 17 import ( 18 "fmt" 19 "io" 20 "os" 21 "reflect" 22 "sync" 23 24 "github.com/network-quality/goresponsiveness/utilities" 25 ) 26 27 type DataLogger[T any] interface { 28 LogRecord(record T) 29 Export() bool 30 Close() bool 31 } 32 33 type CSVDataLogger[T any] struct { 34 mut *sync.Mutex 35 recordCount int 36 data []T 37 isOpen bool 38 destination io.WriteCloser 39 } 40 41 type NullDataLogger[T any] struct{} 42 43 func CreateNullDataLogger[T any]() DataLogger[T] { 44 return &NullDataLogger[T]{} 45 } 46 47 func (_ *NullDataLogger[T]) LogRecord(_ T) {} 48 func (_ *NullDataLogger[T]) Export() bool { return true } 49 func (_ *NullDataLogger[T]) Close() bool { return true } 50 51 func CreateCSVDataLogger[T any](filename string) (DataLogger[T], error) { 52 data := make([]T, 0) 53 destination, err := os.Create(filename) 54 if err != nil { 55 return &CSVDataLogger[T]{&sync.Mutex{}, 0, data, true, destination}, err 56 } 57 58 result := CSVDataLogger[T]{&sync.Mutex{}, 0, data, true, destination} 59 return &result, nil 60 } 61 62 func (logger *CSVDataLogger[T]) LogRecord(record T) { 63 logger.mut.Lock() 64 defer logger.mut.Unlock() 65 logger.recordCount += 1 66 logger.data = append(logger.data, record) 67 } 68 69 func doCustomFormatting(value reflect.Value, tag reflect.StructTag) (string, error) { 70 if utilities.IsInterfaceNil(value) { 71 return "", fmt.Errorf("Cannot format an empty interface value") 72 } 73 formatMethodName, success := tag.Lookup("Formatter") 74 if !success { 75 return "", fmt.Errorf("Could not find the formatter name") 76 } 77 formatMethodArgument, success := tag.Lookup("FormatterArgument") 78 if !success { 79 formatMethodArgument = "" 80 } 81 82 formatMethod := value.MethodByName(formatMethodName) 83 if formatMethod == reflect.ValueOf(0) { 84 return "", fmt.Errorf( 85 "Type %v does not support a method named %v", 86 value.Type(), 87 formatMethodName, 88 ) 89 } 90 var formatMethodArgumentUsable []reflect.Value = make([]reflect.Value, 0) 91 if formatMethodArgument != "" { 92 formatMethodArgumentUsable = append( 93 formatMethodArgumentUsable, 94 reflect.ValueOf(formatMethodArgument), 95 ) 96 } 97 result := formatMethod.Call(formatMethodArgumentUsable) 98 if len(result) == 1 { 99 return fmt.Sprintf("%v", result[0]), nil 100 } 101 return "", fmt.Errorf("Too many results returned by the format method's invocation.") 102 } 103 104 func (logger *CSVDataLogger[T]) Export() bool { 105 logger.mut.Lock() 106 defer logger.mut.Unlock() 107 if !logger.isOpen { 108 return false 109 } 110 111 toOmit := make([]int, 0) 112 visibleFields := reflect.VisibleFields(reflect.TypeOf((*T)(nil)).Elem()) 113 for i, v := range visibleFields { 114 description, success := v.Tag.Lookup("Description") 115 columnName := v.Name 116 if success { 117 if description == "[OMIT]" { 118 toOmit = append(toOmit, i) 119 continue 120 } 121 columnName = description 122 } 123 logger.destination.Write([]byte(fmt.Sprintf("%s, ", columnName))) 124 } 125 logger.destination.Write([]byte("\n")) 126 127 // Remove the Omitted fields 128 for _, i := range toOmit { 129 visibleFields = append(visibleFields[:i], visibleFields[i+1:]...) 130 } 131 132 for _, d := range logger.data { 133 for _, v := range visibleFields { 134 data := reflect.ValueOf(d) 135 toWrite := data.FieldByIndex(v.Index) 136 if formattedToWrite, err := doCustomFormatting(toWrite, v.Tag); err == nil { 137 logger.destination.Write([]byte(fmt.Sprintf("%s,", formattedToWrite))) 138 } else { 139 logger.destination.Write([]byte(fmt.Sprintf("%v, ", toWrite))) 140 } 141 } 142 logger.destination.Write([]byte("\n")) 143 } 144 return true 145 } 146 147 func (logger *CSVDataLogger[T]) Close() bool { 148 logger.mut.Lock() 149 defer logger.mut.Unlock() 150 if !logger.isOpen { 151 return false 152 } 153 logger.destination.Close() 154 logger.isOpen = false 155 return true 156 }