From 962175cb22868009e120ddf898ffdc9358371da9 Mon Sep 17 00:00:00 2001 From: AndrewI26 Date: Sun, 22 Dec 2024 17:02:49 -0500 Subject: [PATCH] added generic writer --- canlink/README.md | 18 ++- canlink/canlink_test.go | 18 ++- canlink/tracer.go | 28 ++--- canlink/tracewriters/README.md | 41 ------- canlink/tracewriters/asciiwriter.go | 97 ----------------- canlink/tracewriters/jsonwriter.go | 103 ------------------ canlink/writer/README.md | 45 ++++++++ canlink/{tracewriters => writer}/constants.go | 2 +- canlink/writer/converttostring.go | 61 +++++++++++ .../timestampedframe.go | 2 +- .../{tracewriters => writer}/tracewriters.go | 4 +- .../tracewriters_test.go | 39 +++++-- canlink/{tracewriters => writer}/utils.go | 2 +- canlink/writer/writer.go | 69 ++++++++++++ 14 files changed, 255 insertions(+), 274 deletions(-) delete mode 100644 canlink/tracewriters/README.md delete mode 100644 canlink/tracewriters/asciiwriter.go delete mode 100644 canlink/tracewriters/jsonwriter.go create mode 100644 canlink/writer/README.md rename canlink/{tracewriters => writer}/constants.go (95%) create mode 100644 canlink/writer/converttostring.go rename canlink/{tracewriters => writer}/timestampedframe.go (83%) rename canlink/{tracewriters => writer}/tracewriters.go (72%) rename canlink/{tracewriters => writer}/tracewriters_test.go (65%) rename canlink/{tracewriters => writer}/utils.go (96%) create mode 100644 canlink/writer/writer.go diff --git a/canlink/README.md b/canlink/README.md index 97e342d8..d11bdadc 100644 --- a/canlink/README.md +++ b/canlink/README.md @@ -11,18 +11,32 @@ during a test and dumps it into a file. ### Usage -1) Create an instance of Tracer with the _NewTracer()_ function. +1) Create an instance of Tracer with the `NewTracer()` function. A can interface, directory and logger must be provided as arguments. -Functional options are available of type _TracerOption_ if required. +Functional options are available of type `TracerOption` if required. ```go func main() { + writers := make([]writer.WriterIface, 0) + jsonWriter := writer.NewWriter( + logger, + ".jsonl", + ) + asciiWriter := writer.NewWriter( + logger, + ".asc", + ) + writers = append(writers, jsonWriter) + writers = append(writers, asciiWriter) + tracer := canlink.NewTracer( "can0", "/opt/traces", logger, + connection, canlink.WithBusName("PT"), canlink.WithTimeout(5*time.Minute)) + canlink.WithWriters(writers) } ``` 2) Open the Tracer using _Open()_ diff --git a/canlink/canlink_test.go b/canlink/canlink_test.go index 69f99877..0d049ee6 100644 --- a/canlink/canlink_test.go +++ b/canlink/canlink_test.go @@ -8,7 +8,7 @@ import ( "go.einride.tech/can/pkg/socketcan" "go.uber.org/zap" - "github.com/macformula/hil/canlink/tracewriters" + "github.com/macformula/hil/canlink/writer" ) const ( @@ -48,9 +48,17 @@ func setup(t *testing.T) (*Tracer, *zap.Logger, func(*testing.T, *Tracer, *zap.L t.Fatalf("Failed to create socket can connection. Error: %v", err) } - writers := make([]tracewriters.TraceWriter, 0) - writers = append(writers, tracewriters.NewAsciiWriter(logger)) - writers = append(writers, tracewriters.NewJsonWriter(logger)) + writers := make([]writer.WriterIface, 0) + jsonWriter := writer.NewWriter( + logger, + ".jsonl", + ) + asciiWriter := writer.NewWriter( + logger, + ".asc", + ) + writers = append(writers, jsonWriter) + writers = append(writers, asciiWriter) tracer := NewTracer( _canIface, @@ -58,7 +66,7 @@ func setup(t *testing.T) (*Tracer, *zap.Logger, func(*testing.T, *Tracer, *zap.L logger, conn, WithBusName(_busName), - WithTraceWriters(writers)) + WithWriters(writers)) err = tracer.Open(ctx) if err != nil { diff --git a/canlink/tracer.go b/canlink/tracer.go index eedc25e3..3b91104c 100644 --- a/canlink/tracer.go +++ b/canlink/tracer.go @@ -7,7 +7,7 @@ import ( "time" "github.com/macformula/hil/utils" - "github.com/macformula/hil/canlink/tracewriters" + "github.com/macformula/hil/canlink/writer" "github.com/pkg/errors" "go.einride.tech/can/pkg/socketcan" "go.uber.org/zap" @@ -36,14 +36,14 @@ type TracerOption func(*Tracer) type Tracer struct { l *zap.Logger stop chan struct{} - frameCh chan tracewriters.TimestampedFrame - cachedData []tracewriters.TimestampedFrame + frameCh chan writer.TimestampedFrame + cachedData []writer.TimestampedFrame err *utils.ResettableError isRunning bool receiver *socketcan.Receiver traceDir string - traceWriters []tracewriters.TraceWriter + writers []writer.WriterIface canInterface string timeout time.Duration @@ -61,7 +61,7 @@ func NewTracer( tracer := &Tracer{ l: l.Named(_loggerName), - cachedData: []tracewriters.TimestampedFrame{}, + cachedData: []writer.TimestampedFrame{}, err: utils.NewResettaleError(), timeout: _defaultTimeout, canInterface: canInterface, @@ -91,10 +91,10 @@ func WithBusName(name string) TracerOption { } } -// WithFiles sets different filetypes the tracer can dump CAN data to -func WithTraceWriters(traceWriters []tracewriters.TraceWriter) TracerOption { +// WithWriters sets the slice of writers which manage trace files +func WithWriters(writer []writer.WriterIface) TracerOption { return func(t *Tracer) { - t.traceWriters = traceWriters + t.writers = writer } } @@ -107,7 +107,7 @@ func (t *Tracer) Open(ctx context.Context) error { t.l.Info("canlink receiver created") // IMPORTANT: frameCh must be open before isRunning is set - t.frameCh = make(chan tracewriters.TimestampedFrame, _frameBufferLength) + t.frameCh = make(chan writer.TimestampedFrame, _frameBufferLength) go t.fetchData(ctx) @@ -121,7 +121,7 @@ func (t *Tracer) StartTrace(ctx context.Context) error { t.stop = make(chan struct{}) - for _, writer := range t.traceWriters { + for _, writer := range t.writers { err := writer.CreateTraceFile(t.traceDir, t.busName) if err != nil { return errors.Wrap(err, "create trace file") @@ -169,7 +169,7 @@ func (t *Tracer) Close() error { return errors.Wrap(err, "close socketcan receiver") } - for _, writer := range t.traceWriters { + for _, writer := range t.writers { err = writer.CloseTraceFile() if err != nil { return errors.Wrap(err, "close trace file") @@ -187,7 +187,7 @@ func (t *Tracer) Error() error { // fetchData fetches CAN frames from the socket and sends them over a buffered channel func (t *Tracer) fetchData(ctx context.Context) { - timeFrame := tracewriters.TimestampedFrame{} + timeFrame := writer.TimestampedFrame{} for t.receiver.Receive() { select { @@ -234,8 +234,8 @@ func (t *Tracer) receiveData(ctx context.Context) { } } -func (t *Tracer) writeFrameToFile(frame *tracewriters.TimestampedFrame) error { - for _, writer := range t.traceWriters { +func (t *Tracer) writeFrameToFile(frame *writer.TimestampedFrame) error { + for _, writer := range t.writers { err := writer.WriteFrameToFile(frame) if err != nil { return errors.Wrap(err, "write frame to file") diff --git a/canlink/tracewriters/README.md b/canlink/tracewriters/README.md deleted file mode 100644 index 4b27c2bb..00000000 --- a/canlink/tracewriters/README.md +++ /dev/null @@ -1,41 +0,0 @@ -tracewriters -====================== - -`tracewriters` contains structs to create, write to, and close a trace file of a specific format. -Currently supported writers are `JsonWriter` and `AsciiWriter`. - -### Usage - -__NOTE: Usage is the same for both JsonWriter and AsciiWriter but shown for only JsonWriter.__ - -1) Create a JsonWriter instance with _NewJsonWriter()_ function. -A logger must be provided as an argument. - - ```go - func main() { - jsonWriter := NewJsonWriter(logger) - } - ``` -2) Initialize the trace file with _CreateTraceFile()_. -A trace directory and bus name are passed as arguments. - - ```go - func main() { - jsonWriter.CreateTraceFile(traceDir, busName) - } - ``` -3) Write to the trace file with _WriteFrameToFile()_, passing in a frame to write. - - - ```go - func main() { - jsonWriter.WriteFrameToFile(timestampedFrame) - } - ``` -4) Once the trace has ended, close the file with _CloseTraceFile()_. - - ```go - func main() { - jsonWriter.CloseTraceFile() - } - ``` \ No newline at end of file diff --git a/canlink/tracewriters/asciiwriter.go b/canlink/tracewriters/asciiwriter.go deleted file mode 100644 index 5b5d89c5..00000000 --- a/canlink/tracewriters/asciiwriter.go +++ /dev/null @@ -1,97 +0,0 @@ -package tracewriters - -import ( - "os" - "fmt" - "strconv" - "strings" - - "go.uber.org/zap" - "github.com/pkg/errors" -) - -// Responsible for managing the ascii trace file -type AsciiWriter struct { - traceFile *os.File - l *zap.Logger -} - -func NewAsciiWriter(l *zap.Logger) *AsciiWriter { - return &AsciiWriter{ - l: l, - traceFile: nil, - } -} - -func (w *AsciiWriter) CreateTraceFile(traceDir string, busName string) error { - file, err := createEmptyTraceFile(traceDir, busName, "asc") - w.traceFile = file - if err != nil { - w.l.Info("cannot create trace file") - return errors.Wrap(err, "creating trace file") - } - - return nil -} - -func (w *AsciiWriter) WriteFrameToFile(frame *TimestampedFrame) error { - line := w.convertToAscii(frame) - - _, err := w.traceFile.WriteString(line) - if err != nil { - w.l.Info("cannot write to file") - return errors.Wrap(err, "writing to trace file") - } - - _, err = w.traceFile.WriteString("\n") - if err != nil { - w.l.Info("cannot write to file") - return errors.Wrap(err, "writing to trace file") - } - - return nil -} - -// Converts timestamped frames into strings for file writing -func (w *AsciiWriter) convertToAscii(timestampedFrame *TimestampedFrame) string { - var builder strings.Builder - - _, err := builder.WriteString(timestampedFrame.Time.Format(_messageTimeFormat)) - if err != nil { - w.l.Error(err.Error()) - } - - _, err = builder.WriteString(" " + strconv.FormatUint(uint64(timestampedFrame.Frame.ID), _decimal)) - if err != nil { - w.l.Error(err.Error()) - } - - _, err = builder.WriteString(" Rx") - if err != nil { - w.l.Error(err.Error()) - } - - _, err = builder.WriteString(" " + strconv.FormatUint(uint64(timestampedFrame.Frame.Length), _decimal)) - if err != nil { - w.l.Error(err.Error()) - } - - for i := uint8(0); i < timestampedFrame.Frame.Length; i++ { - builder.WriteString(" " + fmt.Sprintf("%02X", timestampedFrame.Frame.Data[i])) - if err != nil { - w.l.Error(err.Error()) - } - } - - return builder.String() -} - -func (w *AsciiWriter) CloseTraceFile() error { - err := w.traceFile.Close() - if err != nil { - w.l.Error(err.Error()) - return errors.Wrap(err, "closing trace file") - } - - return nil -} \ No newline at end of file diff --git a/canlink/tracewriters/jsonwriter.go b/canlink/tracewriters/jsonwriter.go deleted file mode 100644 index 047edce9..00000000 --- a/canlink/tracewriters/jsonwriter.go +++ /dev/null @@ -1,103 +0,0 @@ -package tracewriters - -import ( - "os" - "strconv" - - "encoding/json" - - "go.uber.org/zap" - "github.com/pkg/errors" -) - -// Responsible for managing the json trace file -type JsonWriter struct { - traceFile *os.File - l *zap.Logger -} - -func NewJsonWriter(l *zap.Logger) *JsonWriter { - return &JsonWriter{ - l: l, - traceFile: nil, - } -} - -func (w *JsonWriter) CreateTraceFile(traceDir string, busName string) error { - file, err := createEmptyTraceFile(traceDir, busName, "json") - - w.traceFile = file - if err != nil { - w.l.Info("cannot create trace file") - return errors.Wrap(err, "creating trace file") - } - - // Initializes the trace file with a "[" to ensure the json array is started - _, err = file.WriteString("[\n") - if err != nil { - w.l.Info("cannot write to file") - return errors.Wrap(err, "initialiing json file") - } - - return nil -} - -func (w *JsonWriter) WriteFrameToFile(frame *TimestampedFrame) error { - line := w.convertToJson(frame) - - _, err := w.traceFile.WriteString(line) - if err != nil { - w.l.Info("cannot write to file") - return errors.Wrap(err, "writing to trace file") - } - - _, err = w.traceFile.WriteString("\n") - if err != nil { - w.l.Info("cannot write to file") - return errors.Wrap(err, "writing to trace file") - } - - return nil -} - -func (w *JsonWriter) convertToJson(timestampedFrame *TimestampedFrame) string { - jsonObject := map[string]interface{}{ - "time": timestampedFrame.Time.Format(_messageTimeFormat), - "id": strconv.FormatUint(uint64(timestampedFrame.Frame.ID), _decimal), - "frameLength": strconv.FormatUint(uint64(timestampedFrame.Frame.Length), _decimal), - "bytes": timestampedFrame.Frame.Data, - } - - jsonData, err := json.Marshal(jsonObject) - if err != nil { - w.l.Error(err.Error()) - } - - return string(jsonData) + "," -} - -func (w *JsonWriter) CloseTraceFile() error { - // this is nessesary to prevent trailing commas in the json - info, _ := w.traceFile.Stat() - fileLength := info.Size() - - closingBracket := []byte("\n]") - - // this checks if no messages have been written into the file - if fileLength <= 2 { - w.traceFile.WriteString(string(closingBracket)) - return nil - } - - // otherwise we need to remove the trailing comma in the file - w.traceFile.WriteAt(closingBracket, fileLength - 2) - - - err := w.traceFile.Close() - if err != nil { - w.l.Error(err.Error()) - return errors.Wrap(err, "closing trace file") - } - - return nil -} \ No newline at end of file diff --git a/canlink/writer/README.md b/canlink/writer/README.md new file mode 100644 index 00000000..1bb00df9 --- /dev/null +++ b/canlink/writer/README.md @@ -0,0 +1,45 @@ +writer +====================== + +`Writer` is a struct which manages the creation, writing to, and closing of a trace file. +Currently supported formats are ascii and jsonl. Each writer instance manages its own trace file. Multiple instances can be created if multiple trace files are desired. + +### Usage + +1) Create a Writer instance with `NewWriter()`. A logger and a file extension are passed in as arguments. + +__NOTE: The only currently supported file extensions are `.asc` and `.jsonl`__ + + ```go + func main() { + writer := NewWriter( + logger, + ".jsonl", + ) + } + ``` + +2) Initialize the trace file with `CreateTraceFile()`. +A trace directory and bus name are passed as arguments. + + ```go + func main() { + writer.CreateTraceFile(traceDir, busName) + } + ``` +3) Write to the trace file with `WriteFrameToFile()`, passing in a frame to write. + + + ```go + func main() { + writer.WriteFrameToFile(timestampedFrame) + } + ``` +4) Once the trace has ended, close the file with `CloseTraceFile()`. + + ```go + func main() { + writer.CloseTraceFile() + } + ``` + diff --git a/canlink/tracewriters/constants.go b/canlink/writer/constants.go similarity index 95% rename from canlink/tracewriters/constants.go rename to canlink/writer/constants.go index 8f0ed747..03540a5c 100644 --- a/canlink/tracewriters/constants.go +++ b/canlink/writer/constants.go @@ -1,4 +1,4 @@ -package tracewriters +package writer const ( _decimal = 10 diff --git a/canlink/writer/converttostring.go b/canlink/writer/converttostring.go new file mode 100644 index 00000000..d83d7ab2 --- /dev/null +++ b/canlink/writer/converttostring.go @@ -0,0 +1,61 @@ +package writer + +import ( + "strings" + "fmt" + "strconv" + + "encoding/json" + "go.uber.org/zap" +) + +// Converts timestamped frames into strings for file writing +func convertToAscii(l *zap.Logger, timestampedFrame *TimestampedFrame) string { + var builder strings.Builder + + _, err := builder.WriteString(timestampedFrame.Time.Format(_messageTimeFormat)) + if err != nil { + l.Error(err.Error()) + } + + _, err = builder.WriteString(" " + strconv.FormatUint(uint64(timestampedFrame.Frame.ID), _decimal)) + if err != nil { + l.Error(err.Error()) + } + + _, err = builder.WriteString(" Rx") + if err != nil { + l.Error(err.Error()) + } + + _, err = builder.WriteString(" " + strconv.FormatUint(uint64(timestampedFrame.Frame.Length), _decimal)) + if err != nil { + l.Error(err.Error()) + } + + for i := uint8(0); i < timestampedFrame.Frame.Length; i++ { + builder.WriteString(" " + fmt.Sprintf("%02X", timestampedFrame.Frame.Data[i])) + if err != nil { + l.Error(err.Error()) + } + } + + return builder.String() +} + +// Converts timestamped frames into strings for file writing +func convertToJson(l *zap.Logger, timestampedFrame *TimestampedFrame) string { + jsonObject := map[string]interface{}{ + "time": timestampedFrame.Time.Format(_messageTimeFormat), + "id": strconv.FormatUint(uint64(timestampedFrame.Frame.ID), _decimal), + "frameLength": strconv.FormatUint(uint64(timestampedFrame.Frame.Length), _decimal), + "bytes": timestampedFrame.Frame.Data, + } + + jsonData, err := json.Marshal(jsonObject) + if err != nil { + l.Error(err.Error()) + } + + return string(jsonData) +} \ No newline at end of file diff --git a/canlink/tracewriters/timestampedframe.go b/canlink/writer/timestampedframe.go similarity index 83% rename from canlink/tracewriters/timestampedframe.go rename to canlink/writer/timestampedframe.go index 4520260a..0eb53f62 100644 --- a/canlink/tracewriters/timestampedframe.go +++ b/canlink/writer/timestampedframe.go @@ -1,4 +1,4 @@ -package tracewriters +package writer import ( "go.einride.tech/can" diff --git a/canlink/tracewriters/tracewriters.go b/canlink/writer/tracewriters.go similarity index 72% rename from canlink/tracewriters/tracewriters.go rename to canlink/writer/tracewriters.go index fa2a7d87..1d6faa3c 100644 --- a/canlink/tracewriters/tracewriters.go +++ b/canlink/writer/tracewriters.go @@ -1,6 +1,6 @@ -package tracewriters +package writer -type TraceWriter interface { +type WriterIface interface { CreateTraceFile(traceDir string, busName string) error WriteFrameToFile(frame *TimestampedFrame) error CloseTraceFile() error diff --git a/canlink/tracewriters/tracewriters_test.go b/canlink/writer/tracewriters_test.go similarity index 65% rename from canlink/tracewriters/tracewriters_test.go rename to canlink/writer/tracewriters_test.go index 20fbd035..b4f41a5a 100644 --- a/canlink/tracewriters/tracewriters_test.go +++ b/canlink/writer/tracewriters_test.go @@ -1,15 +1,28 @@ -package tracewriters +package writer import ( "testing" "time" "go.einride.tech/can" + "go.uber.org/zap" ) // Tests a if a simple timestamped frame is converted to json with proper formatting func TestJsonConvertToString(t *testing.T) { - jsonWriter := JsonWriter{} + loggerConfig := zap.NewDevelopmentConfig() + loggerConfig.OutputPaths = []string{"stdout"} + loggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel) + logger, err := loggerConfig.Build() + + if err != nil { + t.Errorf("failed to build logger") + } + + writer := NewWriter( + logger, + ".jsonl", + ) time := time.Date(2024, time.November, 27, 10, 30, 45, 0, time.Local) timestampedFrame := TimestampedFrame{ @@ -21,8 +34,8 @@ func TestJsonConvertToString(t *testing.T) { Time: time, } - output := jsonWriter.convertToJson(×tampedFrame) - expected := `{"bytes":[17,34,51,68,85,102,119,136],"frameLength":"8","id":"12","time":"10:30:45.0000"},` + output := writer.convertToString(logger, ×tampedFrame) + expected := `{"bytes":[17,34,51,68,85,102,119,136],"frameLength":"8","id":"12","time":"10:30:45.0000"}` if output != expected { t.Fatalf(` @@ -34,14 +47,26 @@ func TestJsonConvertToString(t *testing.T) { }, Time: time, }) = - %v; want + %v want %v`, output, expected) } } // Tests a if a simple timestamped frame is converted to ascii with proper format func TestAsciiConvertToString(t *testing.T) { - asciiWriter := AsciiWriter{} + loggerConfig := zap.NewDevelopmentConfig() + loggerConfig.OutputPaths = []string{"stdout"} + loggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel) + logger, err := loggerConfig.Build() + + if err != nil { + t.Errorf("failed to build logger") + } + + writer := NewWriter( + logger, + ".asc", + ) time := time.Date(2024, time.November, 27, 10, 30, 45, 0, time.Local) @@ -54,7 +79,7 @@ func TestAsciiConvertToString(t *testing.T) { Time: time, } - output := asciiWriter.convertToAscii(×tampedFrame) + output := writer.convertToString(logger, ×tampedFrame) expected := `10:30:45.0000 12 Rx 8 11 22 33 44 55 66 77 88` if output != expected { diff --git a/canlink/tracewriters/utils.go b/canlink/writer/utils.go similarity index 96% rename from canlink/tracewriters/utils.go rename to canlink/writer/utils.go index 9d2d47a4..7e72e7c9 100644 --- a/canlink/tracewriters/utils.go +++ b/canlink/writer/utils.go @@ -1,4 +1,4 @@ -package tracewriters +package writer import ( "fmt" diff --git a/canlink/writer/writer.go b/canlink/writer/writer.go new file mode 100644 index 00000000..07c14568 --- /dev/null +++ b/canlink/writer/writer.go @@ -0,0 +1,69 @@ +package writer + +import ( + "os" + + "go.uber.org/zap" + "github.com/pkg/errors" +) + +// Responsible for managing the trace file +type Writer struct { + traceFile *os.File + fileExtention string + convertToString func(*zap.Logger, *TimestampedFrame) string + l *zap.Logger +} + +func NewWriter(l *zap.Logger, fileExtention string) *Writer { + convertToStringMapping := map[string]func(*zap.Logger, *TimestampedFrame) string{ + ".jsonl": convertToJson, + ".asc": convertToAscii, + } + return &Writer{ + l: l, + fileExtention: fileExtention, + convertToString: convertToStringMapping[fileExtention], + traceFile: nil, + } +} + +func (w *Writer) CreateTraceFile(traceDir string, busName string) error { + file, err := createEmptyTraceFile(traceDir, busName, w.fileExtention) + w.traceFile = file + if err != nil { + w.l.Info("cannot create trace file") + return errors.Wrap(err, "creating trace file") + } + + return nil +} + +func (w *Writer) WriteFrameToFile(frame *TimestampedFrame) error { + line := w.convertToString(w.l, frame) + + _, err := w.traceFile.WriteString(line) + if err != nil { + w.l.Info("cannot write to file") + return errors.Wrap(err, "writing to trace file") + } + + _, err = w.traceFile.WriteString("\n") + if err != nil { + w.l.Info("cannot write to file") + return errors.Wrap(err, "writing to trace file") + } + + return nil +} + + +func (w *Writer) CloseTraceFile() error { + err := w.traceFile.Close() + if err != nil { + w.l.Error(err.Error()) + return errors.Wrap(err, "closing trace file") + } + + return nil +} \ No newline at end of file