commit ba9d5318ac2a0b2137bd03c2a9a9bcc1ecca253c Author: nxshock Date: Thu May 12 19:22:55 2022 +0500 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b735ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..01aa971 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 nxshock + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..70990b9 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# logwriter + +Simple Go library for SystemD's journalctl like log formatting. + +Provides time prefix generation for each line of provided messages. + +## Usage example + +```go + import "github.com/nxshock/logwriter" + + // Create new writer that writes result to stdout + writer := logwriter.New(os.Stdout) + + // Set custom time format and timezone if needed + writer.TimeFormat = "02.01.06 15:04:05" + writer.TimeZone = time.UTC + + writer.Print("hello world") + // result: + // 02.01.06 15:04:05 hello world + + writer.Write([]byte("line 1\nline 2\nline 3")) + // result: + // 02.01.06 15:04:05 line 1 + // 02.01.06 15:04:05 line 2 + // 02.01.06 15:04:05 line 3 + + writer.Print("hello ") + writer.Print("world") + // result: + // 02.01.06 15:04:05 hello world + + writer.Close() + // writes final \n if not written before +``` diff --git a/defaults.go b/defaults.go new file mode 100644 index 0000000..7aa7787 --- /dev/null +++ b/defaults.go @@ -0,0 +1,13 @@ +package logwriter + +import ( + "time" +) + +const ( + defaultTimeFormat = "02.01.06 15:04:05" +) + +var ( + defaultTimeZone = time.Local +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0479b1c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/nxshock/logwriter + +go 1.18 diff --git a/logwriter.go b/logwriter.go new file mode 100644 index 0000000..94316b8 --- /dev/null +++ b/logwriter.go @@ -0,0 +1,147 @@ +package logwriter + +import ( + "bufio" + "bytes" + "fmt" + "io" + "time" +) + +type LogWriter struct { + TimeFormat string + TimeZone *time.Location + writer io.Writer + + newLine bool +} + +// New created new LogWriter. +func New(w io.Writer) *LogWriter { + lw := &LogWriter{ + TimeFormat: defaultTimeFormat, + TimeZone: defaultTimeZone, + writer: w, + newLine: true} + + return lw +} + +// Println formats using the default formats for its operands and writes to writer. +// Spaces are always added between operands and a newline is appended. +// It returns the number of bytes written and any write error encountered. +func (lw *LogWriter) Println(a ...any) (n int, err error) { + s := fmt.Sprintln(a...) + + n, err = lw.Write([]byte(s)) + if err != nil { + return n, err + } + + lw.newLine = true + + return n, nil +} + +// Print formats using the default formats for its operands and writes to writer. +// Spaces are added between operands when neither is a string. +// It returns the number of bytes written and any write error encountered. +func (lw *LogWriter) Print(a ...any) (n int, err error) { + s := fmt.Sprint(a...) + + n, err = lw.Write([]byte(s)) + if err != nil { + return n, err + } + + if s[len(s)-1] == '\n' { + lw.newLine = true + } else { + lw.newLine = false + } + + return n, nil +} + +// Printf formats according to a format specifier and writes to writer. +// It returns the number of bytes written and any write error encountered. +func (lw *LogWriter) Printf(format string, a ...any) (n int, err error) { + s := fmt.Sprintf(format, a...) + + n, err = lw.Write([]byte(s)) + if err != nil { + return n, err + } + + if s[len(s)-1] == '\n' { + lw.newLine = true + } else { + lw.newLine = false + } + + return n, nil +} + +// Write writes len(p) bytes from p to the writer. +// It returns the number of bytes written from p (0 <= n <= len(p)) +// and any error encountered that caused the write to stop early. +func (lw *LogWriter) Write(p []byte) (n int, err error) { + r := bufio.NewReader(bytes.NewReader(p)) + + for { + line, err := r.ReadString('\n') + if err == io.EOF && len(line) > 0 { + nn, err := io.WriteString(lw.writer, lw.prefix()+" "+line) + n += nn + if err != nil { + return n, err + } + + if line[len(line)-1] == '\n' { + lw.newLine = true // TODO: uncovered or unused? + } else { + lw.newLine = false + } + break + } + if err == io.EOF { + break + } + + if lw.newLine { + nn, err := io.WriteString(lw.writer, lw.prefix()+" "+line) + n += nn + if err != nil { + return n, err + } + } else { + nn, err := io.WriteString(lw.writer, line) + n += nn + if err != nil { + return n, err + } + lw.newLine = true + } + } + + return n, nil +} + +func (lw *LogWriter) Close() error { + if lw.newLine { + return nil + } + + _, err := io.WriteString(lw, "\n") + if err != nil { + return err + } + + lw.newLine = true + + return nil +} + +func (lw *LogWriter) prefix() string { + return fmt.Sprintf("%s", time.Now().Format(lw.TimeFormat)) +} diff --git a/logwriter_test.go b/logwriter_test.go new file mode 100644 index 0000000..8c7714a --- /dev/null +++ b/logwriter_test.go @@ -0,0 +1,160 @@ +package logwriter + +import ( + "bytes" + "testing" +) + +func TestBasicPrint(t *testing.T) { + buf := new(bytes.Buffer) + + lw := New(buf) + lw.TimeFormat = "-" + + n, err := lw.Print("text") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if n != 6 { + t.Errorf("expected 6, got %d", n) + } + if lw.newLine { + t.Error("newLine must be false") + } +} + +func TestBasicPrintln(t *testing.T) { + buf := new(bytes.Buffer) + + lw := New(buf) + lw.TimeFormat = "-" + + n, err := lw.Println("text") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if n != 7 { + t.Errorf("expected 7, got %d", n) + } + if !lw.newLine { + t.Error("newLine must be true") + } +} + +func TestBasicPrintf(t *testing.T) { + buf := new(bytes.Buffer) + + lw := New(buf) + lw.TimeFormat = "-" + + n, err := lw.Printf("%s %d", "string", 1) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if n != 10 { + t.Errorf("expected 10, got %d", n) + } + if lw.newLine { + t.Error("newLine must be false") + } +} + +func TestWriteWithEndLine(t *testing.T) { + buf := new(bytes.Buffer) + + lw := New(buf) + lw.TimeFormat = "-" + + n, err := lw.Write([]byte("text\n")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if n != 7 { + t.Errorf("expected 7, got %d", n) + } + if !lw.newLine { + t.Error("newLine must be true") + } +} + +func TestWriteWithoutEndLine(t *testing.T) { + buf := new(bytes.Buffer) + + lw := New(buf) + lw.TimeFormat = "-" + + n, err := lw.Write([]byte("text")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if n != 6 { + t.Errorf("expected 6, got %d", n) + } + if lw.newLine { + t.Error("newLine must be false") + } +} + +func TestClose(t *testing.T) { + buf := new(bytes.Buffer) + + lw := New(buf) + lw.TimeFormat = "-" + + _, err := lw.Write([]byte("text")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + err = lw.Close() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !lw.newLine { + t.Error("newLine must be true") + } + + if buf.String() != "- text\n" { + t.Errorf(`expected "- text\n", got "%s"`, buf.String()) + } +} + +func TestWriteMultipleLines(t *testing.T) { + buf := new(bytes.Buffer) + + lw := New(buf) + lw.TimeFormat = "-" + + n, err := lw.Write([]byte("line1\nline2\nline3")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if n != 23 { + t.Errorf("expected 23, got %d", n) + } + if lw.newLine { + t.Error("newLine must be false") + } +} + +func TestPrintMultipleLines(t *testing.T) { + buf := new(bytes.Buffer) + + lw := New(buf) + lw.TimeFormat = "-" + + n, err := lw.Print("line1\nline2\nline3") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if n != 23 { + t.Errorf("expected 23, got %d", n) + } + if lw.newLine { + t.Error("newLine must be false") + } + if buf.String() != "- line1\n- line2\n- line3" { + t.Errorf("wrong output:\n%s", buf.String()) + } +}