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  }