diff --git a/README.md b/README.md index aa306be..aa11c90 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,12 @@ terminal graphical visualization time. | Encoder | Configuration | ns/op | B/op | allocs/op | |---------------------|--------------------|-------|------|-----------| -| **Default Encoder** | All Properties OFF | 484.0 | 80 | 1 | -| | All Properties ON | 540.1 | 104 | 2 | -| **JSON Encoder** | All Properties OFF | 507.3 | 80 | 1 | -| | All Properties ON | 553.6 | 104 | 2 | -| **YAML Encoder** | All Properties OFF | 531.3 | 80 | 1 | -| | All Properties ON | 588.2 | 104 | 2 | +| **Default Encoder** | All Properties OFF | 490.3 | 80 | 1 | +| | All Properties ON | 511.2 | 104 | 1 | +| **JSON Encoder** | All Properties OFF | 513.3 | 80 | 1 | +| | All Properties ON | 536.5 | 104 | 1 | +| **YAML Encoder** | All Properties OFF | 535.3 | 80 | 1 | +| | All Properties ON | 557.1 | 104 | 1 | ## 🤝 Contributing diff --git a/internal/services/colors_printer.go b/internal/services/colors_printer.go deleted file mode 100644 index d071cff..0000000 --- a/internal/services/colors_printer.go +++ /dev/null @@ -1,34 +0,0 @@ -package services - -import ( - c "github.com/pho3b/tiny-logger/logs/colors" - "github.com/pho3b/tiny-logger/logs/log_level" -) - -type ColorsPrinter struct { -} - -// RetrieveColorsFromLogLevel returns an array of colors as strings to be used in log output based on a given log level. -// if enableColors is false, it returns an array of empty strings. -func (d *ColorsPrinter) RetrieveColorsFromLogLevel(enableColors bool, logLevelInt int8) []c.Color { - var res = []c.Color{"", ""} - - if enableColors { - switch logLevelInt { - case log_level.FatalErrorLvl: - res[0] = c.Magenta - case log_level.ErrorLvl: - res[0] = c.Red - case log_level.WarnLvl: - res[0] = c.Yellow - case log_level.InfoLvl: - res[0] = c.Cyan - case log_level.DebugLvl: - res[0] = c.Gray - } - - res[1] = c.Reset - } - - return res -} diff --git a/internal/services/datetime_printer.go b/internal/services/datetime_printer.go index 1ef9d97..66c415a 100644 --- a/internal/services/datetime_printer.go +++ b/internal/services/datetime_printer.go @@ -21,112 +21,108 @@ var timeFormat = map[s.DateTimeFormat]string{ s.JP: "03:04:05 PM", } +var ( + dateTimePrinterInstance *DateTimePrinter + dateTimePrinterOnce sync.Once +) + type DateTimePrinter struct { - timeNow func() time.Time // Function to get current time, allows mocking for tests - currentFormat atomic.Value - currentDate atomic.Value - currentTime atomic.Value - currentUnix atomic.Value - dateOnce sync.Once - timeOnce sync.Once - unixOnce sync.Once + timeNow func() time.Time + cachedDates [3]atomic.Value + cachedTimes [3]atomic.Value + currentUnix atomic.Value } -// RetrieveDateTime returns the current date, time, and combined/unix string based on the configuration. -// Returns: (dateString, timeString, combinedOrUnixString). -// If the format is UnixTimestamp, the timestamp is returned as the third value, ignoring the boolean flags. -// Otherwise, 'addDate' and 'addTime' control which components are generated. -func (d *DateTimePrinter) RetrieveDateTime(addDate, addTime bool) (string, string, string) { - var dateRes, timeRes string - currentFmt := d.currentFormat.Load().(s.DateTimeFormat) - - if currentFmt == s.UnixTimestamp && (addDate || addTime) { - d.unixOnce.Do(func() { - d.currentUnix.Store(strconv.FormatInt(d.timeNow().Unix(), 10)) - go d.updateCurrentUnixEverySecond() - }) - +// RetrieveDateTime now accepts the desired format +func (d *DateTimePrinter) RetrieveDateTime(fmt s.DateTimeFormat, addDate, addTime bool) (string, string, string) { + if fmt == s.UnixTimestamp { return "", "", d.currentUnix.Load().(string) } - if addDate { - d.dateOnce.Do(func() { - d.currentDate.Store(d.timeNow().Format(dateFormat[currentFmt])) - go d.updateCurrentDateEveryDay() - }) + var dateRes, timeRes string - dateRes = d.currentDate.Load().(string) + if addDate { + dateRes = d.cachedDates[fmt].Load().(string) } if addTime { - d.timeOnce.Do(func() { - d.currentTime.Store(d.timeNow().Format(timeFormat[currentFmt])) - go d.updateCurrentTimeEverySecond() - }) - - timeRes = d.currentTime.Load().(string) - } - - if addDate && addTime { - return "", "", dateRes + " " + timeRes + timeRes = d.cachedTimes[fmt].Load().(string) } return dateRes, timeRes, "" } -// UpdateDateTimeFormat updates the DateTimePrinter's currentFormat property and updates the currentDate and -// currentTime properties accordingly. -func (d *DateTimePrinter) UpdateDateTimeFormat(format s.DateTimeFormat) { +// init initializes the current timestamp and cached formatted strings, +// then starts background goroutines to keep them updated. +func (d *DateTimePrinter) init() { now := d.timeNow() + d.currentUnix.Store(strconv.FormatInt(now.Unix(), 10)) + + for i := 0; i < 3; i++ { + fmt := s.DateTimeFormat(i) + d.cachedDates[i].Store(now.Format(dateFormat[fmt])) + d.cachedTimes[i].Store(now.Format(timeFormat[fmt])) + } - d.currentFormat.Store(format) - d.currentDate.Store(now.Format(dateFormat[format])) - d.currentTime.Store(now.Format(timeFormat[format])) + go d.loopUpdateDate() + go d.loopUpdateTime() } -// updateCurrentDateEveryDay synchronizes with the system clock and updates the DateTimePrinter's -// currentDate property every midnight. -func (d *DateTimePrinter) updateCurrentDateEveryDay() { +// loopUpdateTime updates all time formats, the unix timestamp every second, +// and refreshes the date format if the day has changed. +func (d *DateTimePrinter) loopUpdateTime() { + // Initialize lastDay with a value that forces an update on the first iteration + lastDay := -1 + for { now := d.timeNow() - currentFmt := d.currentFormat.Load().(s.DateTimeFormat) - d.currentDate.Store(now.Format(dateFormat[currentFmt])) - // computing next midnight in local time zone - nextMidnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) - time.Sleep(time.Until(nextMidnight)) - } -} + // 1. Update Time Formats (Always) + for i := 0; i < 3; i++ { + fmt := s.DateTimeFormat(i) + d.cachedTimes[i].Store(now.Format(timeFormat[fmt])) + } -// updateCurrentTimeEverySecond synchronizes with the system clock and updates the DateTimePrinter's -// currentTime property every full second. -func (d *DateTimePrinter) updateCurrentTimeEverySecond() { - for { - now := d.timeNow() - currentFmt := d.currentFormat.Load().(s.DateTimeFormat) - d.currentTime.Store(now.Format(timeFormat[currentFmt])) + // 2. Update Unix Timestamp (Always) + d.currentUnix.Store(strconv.FormatInt(now.Unix(), 10)) + + // 3. Update Date Formats (Only if day changed) + if currentDay := now.Day(); currentDay != lastDay { + for i := 0; i < 3; i++ { + fmt := s.DateTimeFormat(i) + d.cachedDates[i].Store(now.Format(dateFormat[fmt])) + } + + lastDay = currentDay + } nextSecond := now.Truncate(time.Second).Add(time.Second) time.Sleep(time.Until(nextSecond)) } } -// updateCurrentUnixEverySecond synchronizes with the system clock and updates the DateTimePrinter's -// currentTime property every full second. -func (d *DateTimePrinter) updateCurrentUnixEverySecond() { +// loopUpdateDate updates all date formats every 10 mins +func (d *DateTimePrinter) loopUpdateDate() { for { now := d.timeNow() - d.currentUnix.Store(strconv.FormatInt(now.Unix(), 10)) - nextSecond := now.Truncate(time.Second).Add(time.Second) - time.Sleep(time.Until(nextSecond)) + for i := 0; i < 3; i++ { + fmt := s.DateTimeFormat(i) + d.cachedDates[i].Store(now.Format(dateFormat[fmt])) + } + + time.Sleep(time.Minute * 10) } } -// NewDateTimePrinter initializes DateTimePrinter with the default timeNow function. -func NewDateTimePrinter() *DateTimePrinter { - dateTimePrinter := &DateTimePrinter{timeNow: time.Now} - dateTimePrinter.currentFormat.Store(s.IT) +// GetDateTimePrinter returns the singleton instance. +func GetDateTimePrinter() *DateTimePrinter { + dateTimePrinterOnce.Do( + func() { + dateTimePrinterInstance = &DateTimePrinter{timeNow: time.Now} + dateTimePrinterInstance.init() + }, + ) - return dateTimePrinter + return dateTimePrinterInstance } diff --git a/internal/services/datetime_printer_test.go b/internal/services/datetime_printer_test.go index 55235e1..6cf8a70 100644 --- a/internal/services/datetime_printer_test.go +++ b/internal/services/datetime_printer_test.go @@ -15,34 +15,32 @@ func TestDateTimePrinter_PrintDateTime(t *testing.T) { return time.Date(2023, time.November, 1, 15, 30, 45, 0, time.UTC) }, } + dateTimePrinter.init() t.Run("Return both date and time", func(t *testing.T) { - dateTimePrinter.UpdateDateTimeFormat(shared.IT) - dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(true, true) - assert.Empty(t, dateRes) - assert.Empty(t, timeRes) - assert.Equal(t, "01/11/2023 15:30:45", dateTimeRes) + dateRes, timeRes, unixTs := dateTimePrinter.RetrieveDateTime(shared.IT, true, true) + assert.Empty(t, unixTs) + assert.NotEmpty(t, dateRes) + assert.NotEmpty(t, timeRes) + assert.Equal(t, "01/11/2023 15:30:45", dateRes+" "+timeRes) }) t.Run("Return date only", func(t *testing.T) { - dateTimePrinter.UpdateDateTimeFormat(shared.IT) - dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(true, false) + dateRes, timeRes, unixTs := dateTimePrinter.RetrieveDateTime(shared.IT, true, false) assert.Equal(t, "01/11/2023", dateRes) assert.Equal(t, "", timeRes) - assert.Equal(t, "", dateTimeRes) + assert.Equal(t, "", unixTs) }) t.Run("Return time only", func(t *testing.T) { - dateTimePrinter.UpdateDateTimeFormat(shared.IT) - dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(false, true) + dateRes, timeRes, unixTs := dateTimePrinter.RetrieveDateTime(shared.IT, false, true) assert.Equal(t, "", dateRes) assert.Equal(t, "15:30:45", timeRes) - assert.Equal(t, "", dateTimeRes) + assert.Equal(t, "", unixTs) }) t.Run("Return empty string when both flags are false", func(t *testing.T) { - dateTimePrinter.UpdateDateTimeFormat(shared.IT) - dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(false, false) + dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(shared.IT, false, false) assert.Equal(t, "", dateRes) assert.Equal(t, "", timeRes) assert.Equal(t, "", dateTimeRes) @@ -60,6 +58,7 @@ func TestDateTimePrinter_Formats(t *testing.T) { return fixedFutureTime }, } + dateTimePrinter.init() tests := []struct { name string @@ -93,18 +92,18 @@ func TestDateTimePrinter_Formats(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - dateTimePrinter.UpdateDateTimeFormat(tt.format) - dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(true, true) - assert.Empty(t, dateRes) - assert.Empty(t, timeRes) - assert.Equal(t, tt.wantCombined, dateTimeRes) + dateRes, timeRes, unixTs := dateTimePrinter.RetrieveDateTime(tt.format, true, true) + assert.NotEmpty(t, dateRes) + assert.NotEmpty(t, timeRes) + assert.Empty(t, unixTs) + assert.Equal(t, tt.wantCombined, dateRes+" "+timeRes) // Also test individual components - d, tRes, _ := dateTimePrinter.RetrieveDateTime(true, false) + d, tRes, _ := dateTimePrinter.RetrieveDateTime(tt.format, true, false) assert.Equal(t, tt.wantDate, d) assert.Empty(t, tRes) - d, tRes, _ = dateTimePrinter.RetrieveDateTime(false, true) + d, tRes, _ = dateTimePrinter.RetrieveDateTime(tt.format, false, true) assert.Empty(t, d) assert.Equal(t, tt.wantTime, tRes) }) @@ -121,25 +120,24 @@ func TestDateTimePrinter_UnixTimestamp(t *testing.T) { return fixedFutureTime }, } - - dateTimePrinter.UpdateDateTimeFormat(shared.UnixTimestamp) + dateTimePrinter.init() t.Run("Return unix timestamp combined", func(t *testing.T) { - dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(true, true) + dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(shared.UnixTimestamp, true, true) assert.Empty(t, dateRes) assert.Empty(t, timeRes) assert.Equal(t, expectedUnix, dateTimeRes) }) t.Run("Return unix timestamp with date flag only", func(t *testing.T) { - dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(true, false) + dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(shared.UnixTimestamp, true, false) assert.Empty(t, dateRes) assert.Empty(t, timeRes) assert.Equal(t, expectedUnix, dateTimeRes) }) t.Run("Return unix timestamp with time flag only", func(t *testing.T) { - dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(false, true) + dateRes, timeRes, dateTimeRes := dateTimePrinter.RetrieveDateTime(shared.UnixTimestamp, false, true) assert.Empty(t, dateRes) assert.Empty(t, timeRes) assert.Equal(t, expectedUnix, dateTimeRes) @@ -147,22 +145,21 @@ func TestDateTimePrinter_UnixTimestamp(t *testing.T) { } func TestNewDateTimePrinter(t *testing.T) { - assert.NotNil(t, NewDateTimePrinter()) - assert.IsType(t, &DateTimePrinter{}, NewDateTimePrinter()) + assert.NotNil(t, GetDateTimePrinter()) + assert.IsType(t, &DateTimePrinter{}, GetDateTimePrinter()) } func TestDateTimePrinter_FullSecondUpdate(t *testing.T) { - dateTimePrinter := NewDateTimePrinter() + dateTimePrinter := GetDateTimePrinter() t.Run("Return both date and time", func(t *testing.T) { - dateTimePrinter.UpdateDateTimeFormat(shared.IT) - dateRes, timeRes, _ := dateTimePrinter.RetrieveDateTime(true, true) - assert.Empty(t, dateRes) - assert.Empty(t, timeRes) + dateRes, timeRes, _ := dateTimePrinter.RetrieveDateTime(shared.IT, true, true) + assert.NotEmpty(t, dateRes) + assert.NotEmpty(t, timeRes) - prevTime := dateTimePrinter.currentTime.Load() + prevTime := dateTimePrinter.cachedTimes[shared.IT] time.Sleep(2 * time.Second) - currTime := dateTimePrinter.currentTime.Load() + currTime := dateTimePrinter.cachedTimes[shared.IT] assert.NotEqual(t, prevTime, currTime, "The current %s time should have changed from previous time", currTime, prevTime) diff --git a/internal/services/json_marshaler.go b/internal/services/json_marshaler.go index 227881f..b0646a2 100644 --- a/internal/services/json_marshaler.go +++ b/internal/services/json_marshaler.go @@ -4,20 +4,17 @@ import ( "bytes" "fmt" "strconv" - - s "github.com/pho3b/tiny-logger/shared" ) // JsonLogEntry represents a structured log entry that can be marshaled to JSON format. // All fields except Message are optional and will be omitted if empty. type JsonLogEntry struct { - Level string `json:"level,omitempty"` - Date string `json:"date,omitempty"` - Time string `json:"time,omitempty"` - DateTime string `json:"datetime,omitempty"` - Message string `json:"msg"` - DateTimeFormat s.DateTimeFormat `json:"dateTimeFormat,omitempty"` - Extras []any `json:"extras,omitempty"` + Level string `json:"level,omitempty"` + Date string `json:"date,omitempty"` + Time string `json:"time,omitempty"` + Message string `json:"msg"` + UnixTS string `json:"unixTimestamp,omitempty"` + Extras []any `json:"extras,omitempty"` } // JsonMarshaler provides custom JSON marshaling functionality optimized for log entries. @@ -31,7 +28,7 @@ func (j *JsonMarshaler) MarshalInto(buf *bytes.Buffer, logEntry JsonLogEntry) { buf.Grow(jsonCharOverhead + (averageExtraLen * extrasLen)) buf.WriteByte('{') - j.writeLogEntryProperties(buf, logEntry.Level, logEntry.Date, logEntry.Time, logEntry.DateTime, logEntry.DateTimeFormat) + j.writeLogEntryProperties(buf, logEntry.Level, logEntry.Date, logEntry.Time, logEntry.UnixTS) buf.WriteString("\"msg\":\"") buf.WriteString(logEntry.Message) @@ -89,13 +86,13 @@ func (j *JsonMarshaler) writeValue(buf *bytes.Buffer, v any, isKey bool) { buf.WriteByte('"') } case int: - buf.WriteString(strconv.Itoa(val)) + buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(val), 10)) case int64: - buf.WriteString(strconv.FormatInt(val, 10)) + buf.Write(strconv.AppendInt(buf.AvailableBuffer(), val, 10)) case float64: - buf.WriteString(strconv.FormatFloat(val, 'f', -1, 64)) + buf.Write(strconv.AppendFloat(buf.AvailableBuffer(), val, 'f', -1, 64)) case bool: - buf.WriteString(strconv.FormatBool(val)) + buf.Write(strconv.AppendBool(buf.AvailableBuffer(), val)) default: if isKey { buf.WriteString(fmt.Sprint(val)) @@ -114,8 +111,7 @@ func (j *JsonMarshaler) writeLogEntryProperties( level string, date string, time string, - dateTime string, - dateTimeFormat s.DateTimeFormat, + unixTS string, ) { if level != "" { buf.WriteString("\"level\":\"") @@ -124,14 +120,20 @@ func (j *JsonMarshaler) writeLogEntryProperties( buf.WriteByte(',') } - if dateTime != "" || (date != "" && time != "") { - if dateTimeFormat == s.UnixTimestamp { - buf.WriteString("\"ts\":\"") - } else { - buf.WriteString("\"datetime\":\"") - } + if unixTS != "" { + buf.WriteString("\"ts\":\"") + buf.WriteString(unixTS) + buf.WriteByte('"') + buf.WriteByte(',') + + return + } - buf.WriteString(dateTime) + if date != "" && time != "" { + buf.WriteString("\"datetime\":\"") + buf.WriteString(date) + buf.WriteByte(' ') + buf.WriteString(time) buf.WriteByte('"') buf.WriteByte(',') @@ -152,3 +154,7 @@ func (j *JsonMarshaler) writeLogEntryProperties( buf.WriteByte(',') } } + +func NewJsonMarshaler() JsonMarshaler { + return JsonMarshaler{} +} diff --git a/internal/services/json_mashaler_test.go b/internal/services/json_mashaler_test.go index f8bdcc5..ee66347 100644 --- a/internal/services/json_mashaler_test.go +++ b/internal/services/json_mashaler_test.go @@ -46,17 +46,16 @@ func TestJsonMarshaler_Marshal_AllFields(t *testing.T) { buf := &bytes.Buffer{} m := &JsonMarshaler{} entry := JsonLogEntry{ - Level: "info", - Date: "2025-06-14", - Time: "20:15:30", - DateTime: "2025-06-14T20:15:30", - Message: "all systems go", - DateTimeFormat: shared.IT, + Level: "info", + Date: "2025-06-14", + Time: "20:15:30", + Message: "all systems go", + UnixTS: "", } m.MarshalInto(buf, entry) got := buf.String() - want := `{"level":"info","datetime":"2025-06-14T20:15:30","msg":"all systems go"}` + want := `{"level":"info","datetime":"2025-06-14 20:15:30","msg":"all systems go"}` if got != want { t.Errorf("Marshal() = %q, want %q", got, want) } @@ -66,12 +65,11 @@ func TestJsonMarshaler_Marshal_WithExtras(t *testing.T) { buf := &bytes.Buffer{} m := &JsonMarshaler{} entry := JsonLogEntry{ - Level: "INFO", - Date: "", - Time: "", - DateTime: "20/06/2025 08:11:06", - Message: "all systems go", - Extras: []any{"bool", true, "int", 3, "float", 4.3, "arr", []int{1, 2, 3}, "rune", 'A', "string", "ciaooo", "null"}, + Level: "INFO", + Date: "20/06/2025", + Time: "08:11:06", + Message: "all systems go", + Extras: []any{"bool", true, "int", 3, "float", 4.3, "arr", []int{1, 2, 3}, "rune", 'A', "string", "ciaooo", "null"}, } m.MarshalInto(buf, entry) @@ -87,12 +85,11 @@ func TestJsonMarshaler_Unmarshal_Std_Marshal_Result(t *testing.T) { buf := &bytes.Buffer{} m := JsonMarshaler{} entry := JsonLogEntry{ - Level: "INFO", - Date: "", - Time: "", - DateTime: "20/06/2025 08:11:06", - Message: "all systems go", - Extras: []any{"bool", true, "int", 3, "float", 4.3, "arr", []int{1, 2, 3}, "rune", 'A', "string", "ciaooo", "null"}, + Level: "INFO", + Date: "20/06/2025", + Time: "08:11:06", + Message: "all systems go", + Extras: []any{"bool", true, "int", 3, "float", 4.3, "arr", []int{1, 2, 3}, "rune", 'A', "string", "ciaooo", "null"}, } m.MarshalInto(buf, entry) @@ -103,10 +100,9 @@ func TestJsonMarshaler_Marshal_UnixTimestamp(t *testing.T) { buf := &bytes.Buffer{} m := &JsonMarshaler{} entry := JsonLogEntry{ - Level: "info", - DateTime: "1700000000", - Message: "unix time", - DateTimeFormat: shared.UnixTimestamp, + Level: "info", + Message: "unix time", + UnixTS: "1700000000", } m.MarshalInto(buf, entry) diff --git a/internal/services/printer.go b/internal/services/printer.go new file mode 100644 index 0000000..af2e219 --- /dev/null +++ b/internal/services/printer.go @@ -0,0 +1,66 @@ +package services + +import ( + "bytes" + "os" + + c "github.com/pho3b/tiny-logger/logs/colors" + "github.com/pho3b/tiny-logger/logs/log_level" + s "github.com/pho3b/tiny-logger/shared" +) + +type Printer struct { +} + +// PrintLog prints the given msgBuffer to the given outputType (stdout or stderr). +// If 'file' is not nil AND (outType == FileOutput), the message is written to the file. +func (p *Printer) PrintLog(outType s.OutputType, msgBuffer *bytes.Buffer, file *os.File) { + var err error + + switch outType { + case s.StdOutput: + _, err = os.Stdout.Write(msgBuffer.Bytes()) + case s.StdErrOutput: + _, err = os.Stderr.Write(msgBuffer.Bytes()) + case s.FileOutput: + if file == nil { + _, _ = os.Stderr.Write([]byte("tiny-logger-err: given out file is nil")) + return + } + + _, err = file.Write(msgBuffer.Bytes()) + } + + if err != nil { + _, _ = os.Stderr.Write([]byte("tiny-logger-err: " + err.Error() + "\n")) + } +} + +// RetrieveColorsFromLogLevel returns an array of colors as strings to be used in log output based on a given log level. +// if enableColors is false, it returns an array of empty strings. +func (p *Printer) RetrieveColorsFromLogLevel(enableColors bool, logLevelInt int8) []c.Color { + var res = []c.Color{"", ""} + + if enableColors { + switch logLevelInt { + case log_level.FatalErrorLvl: + res[0] = c.Magenta + case log_level.ErrorLvl: + res[0] = c.Red + case log_level.WarnLvl: + res[0] = c.Yellow + case log_level.InfoLvl: + res[0] = c.Cyan + case log_level.DebugLvl: + res[0] = c.Gray + } + + res[1] = c.Reset + } + + return res +} + +func NewPrinter() Printer { + return Printer{} +} diff --git a/internal/services/color_printer_test.go b/internal/services/printer_test.go similarity index 54% rename from internal/services/color_printer_test.go rename to internal/services/printer_test.go index 1428b58..0c6a2a6 100644 --- a/internal/services/color_printer_test.go +++ b/internal/services/printer_test.go @@ -1,15 +1,84 @@ package services import ( - "github.com/pho3b/tiny-logger/logs/log_level" + "bytes" + "os" "testing" + "github.com/pho3b/tiny-logger/logs/log_level" + s "github.com/pho3b/tiny-logger/shared" + "github.com/pho3b/tiny-logger/test" + c "github.com/pho3b/tiny-logger/logs/colors" "github.com/stretchr/testify/assert" ) +func TestPrinter_PrintLog_Stdout(t *testing.T) { + p := NewPrinter() + buf := bytes.NewBufferString("hello stdout") + + output := test.CaptureOutput(func() { + p.PrintLog(s.StdOutput, buf, nil) + }) + + assert.Equal(t, "hello stdout", output) +} + +func TestPrinter_PrintLog_Stderr(t *testing.T) { + p := NewPrinter() + buf := bytes.NewBufferString("hello stderr") + + output := test.CaptureErrorOutput(func() { + p.PrintLog(s.StdErrOutput, buf, nil) + }) + + assert.Equal(t, "hello stderr", output) +} + +func TestPrinter_PrintLog_FileOutput_WritesToFile(t *testing.T) { + p := NewPrinter() + buf := bytes.NewBufferString("file log") + + tmpFile, err := os.CreateTemp("", "printer-log-*") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + p.PrintLog(s.FileOutput, buf, tmpFile) + + content, err := os.ReadFile(tmpFile.Name()) + assert.NoError(t, err) + assert.Equal(t, "file log", string(content)) +} + +func TestPrinter_PrintLog_FileOutput_NilFile_WritesErrorToStderr(t *testing.T) { + p := NewPrinter() + buf := bytes.NewBufferString("ignored") + + output := test.CaptureErrorOutput(func() { + p.PrintLog(s.FileOutput, buf, nil) + }) + + assert.Contains(t, output, "tiny-logger-err: given out file is nil") +} + +func TestPrinter_PrintLog_WriteError_LogsToStderr(t *testing.T) { + p := NewPrinter() + buf := bytes.NewBufferString("data") + + // Create an invalid *os.File to trigger a write error. + // The fd value here is intentionally bogus. + badFile := os.NewFile(^uintptr(0), "bad") + + output := test.CaptureErrorOutput(func() { + p.PrintLog(s.FileOutput, buf, badFile) + }) + + assert.Contains(t, output, "tiny-logger-err:") +} + func TestPrintColors_EnableColorsTrue(t *testing.T) { - printer := ColorsPrinter{} + printer := Printer{} result := printer.RetrieveColorsFromLogLevel(true, log_level.FatalErrorLvl) assert.Equal(t, c.Magenta, result[0], "Expected first element to be the provided color") @@ -33,7 +102,7 @@ func TestPrintColors_EnableColorsTrue(t *testing.T) { } func TestPrintColors_EnableColorsFalse(t *testing.T) { - printer := ColorsPrinter{} + printer := Printer{} result := printer.RetrieveColorsFromLogLevel(false, log_level.DebugLvl) assert.Equal(t, c.Color(""), result[0], "Expected first element to be an empty string when colors are disabled") diff --git a/internal/services/yaml_marshaler.go b/internal/services/yaml_marshaler.go index 8fc6ddb..8cc8ced 100644 --- a/internal/services/yaml_marshaler.go +++ b/internal/services/yaml_marshaler.go @@ -4,20 +4,17 @@ import ( "bytes" "fmt" "strconv" - - s "github.com/pho3b/tiny-logger/shared" ) // YamlLogEntry represents a structured log entry that can be marshaled to YAML format. // All fields except Message are optional and will be omitted if empty. type YamlLogEntry struct { - Level string `yaml:"level,omitempty"` - Date string `yaml:"date,omitempty"` - Time string `yaml:"time,omitempty"` - DateTime string `yaml:"datetime,omitempty"` - DateTimeFormat s.DateTimeFormat `yaml:"dateTimeFormat,omitempty"` - Message string `yaml:"msg"` - Extras []any `yaml:"extras,omitempty"` + Level string `yaml:"level,omitempty"` + Date string `yaml:"date,omitempty"` + Time string `yaml:"time,omitempty"` + UnixTS string `yaml:"unixTimestamp,omitempty"` + Message string `yaml:"msg"` + Extras []any `yaml:"extras,omitempty"` } // YamlMarshaler provides custom YAML marshaling functionality optimized for log entries. @@ -31,7 +28,7 @@ func (y *YamlMarshaler) MarshalInto(buf *bytes.Buffer, logEntry YamlLogEntry) { extrasLen := len(logEntry.Extras) buf.Grow(yamlCharOverhead + (averageExtraLen * extrasLen)) - y.writeLogEntryProperties(buf, logEntry.Level, logEntry.Date, logEntry.Time, logEntry.DateTime, logEntry.DateTimeFormat) + y.writeLogEntryProperties(buf, logEntry.Level, logEntry.Date, logEntry.Time, logEntry.UnixTS) buf.WriteString("msg: ") buf.WriteString(logEntry.Message) @@ -81,13 +78,13 @@ func (y *YamlMarshaler) writeStr(buf *bytes.Buffer, v any, isKey bool) { buf.WriteByte('"') } case int: - buf.WriteString(strconv.Itoa(val)) + buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(val), 10)) case int64: - buf.WriteString(strconv.FormatInt(val, 10)) + buf.Write(strconv.AppendInt(buf.AvailableBuffer(), val, 10)) case float64: - buf.WriteString(strconv.FormatFloat(val, 'f', -1, 64)) + buf.Write(strconv.AppendFloat(buf.AvailableBuffer(), val, 'f', -1, 64)) case bool: - buf.WriteString(strconv.FormatBool(val)) + buf.Write(strconv.AppendBool(buf.AvailableBuffer(), val)) default: // Check if the string representation needs quotes str := fmt.Sprint(val) @@ -109,8 +106,7 @@ func (y *YamlMarshaler) writeLogEntryProperties( level string, date string, time string, - dateTime string, - dateTimeFormat s.DateTimeFormat, + unixTS string, ) { if level != "" { buf.WriteString("level: ") @@ -118,14 +114,19 @@ func (y *YamlMarshaler) writeLogEntryProperties( buf.WriteByte('\n') } - if dateTime != "" || (date != "" && time != "") { - if dateTimeFormat == s.UnixTimestamp { - buf.WriteString("ts: ") - } else { - buf.WriteString("datetime: ") - } + if unixTS != "" { + buf.WriteString("ts: ") + buf.WriteString(unixTS) + buf.WriteByte('\n') - buf.WriteString(dateTime) + return + } + + if date != "" && time != "" { + buf.WriteString("datetime: ") + buf.WriteString(date) + buf.WriteByte(' ') + buf.WriteString(time) buf.WriteByte('\n') return diff --git a/internal/services/yaml_marshaler_test.go b/internal/services/yaml_marshaler_test.go index 3472686..3fac4f5 100644 --- a/internal/services/yaml_marshaler_test.go +++ b/internal/services/yaml_marshaler_test.go @@ -3,8 +3,6 @@ package services import ( "bytes" "testing" - - "github.com/pho3b/tiny-logger/shared" ) func TestYamlMarshaler_Marshal(t *testing.T) { @@ -31,13 +29,13 @@ func TestYamlMarshaler_Marshal(t *testing.T) { { name: "full log entry", entry: YamlLogEntry{ - Level: "DEBUG", - Date: "2024-03-21", - Time: "15:04:05", - DateTime: "2024-03-21T15:04:05", - Message: "full test message", + Level: "DEBUG", + Date: "2024-03-21", + Time: "15:04:05", + UnixTS: "", + Message: "full test message", }, - expected: "level: DEBUG\ndatetime: 2024-03-21T15:04:05\nmsg: full test message\n", + expected: "level: DEBUG\ndatetime: 2024-03-21 15:04:05\nmsg: full test message\n", }, { name: "with simple extras", @@ -166,10 +164,9 @@ func TestYamlMarshaler_Marshal_UnixTimestamp(t *testing.T) { buf := &bytes.Buffer{} m := NewYamlMarshaler() entry := YamlLogEntry{ - Level: "debug", - DateTime: "1715421234", // Unix timestamp string - Message: "unix timestamp test", - DateTimeFormat: shared.UnixTimestamp, + Level: "debug", + UnixTS: "1715421234", // Unix timestamp string + Message: "unix timestamp test", } // Pass pointer to m because MarshalInto is defined on *YamlMarshaler diff --git a/logs/encoders/base.go b/logs/encoders/base.go index eb77e37..d673227 100644 --- a/logs/encoders/base.go +++ b/logs/encoders/base.go @@ -3,7 +3,6 @@ package encoders import ( "bytes" "fmt" - "os" "strconv" "sync" @@ -36,13 +35,13 @@ func (b *baseEncoder) castAndConcatenateInto(buf *bytes.Buffer, args ...any) { case rune: buf.WriteRune(v) case int: - buf.WriteString(strconv.Itoa(v)) + buf.Write(strconv.AppendInt(buf.AvailableBuffer(), int64(v), 10)) case int64: - buf.WriteString(strconv.FormatInt(v, 10)) + buf.Write(strconv.AppendInt(buf.AvailableBuffer(), v, 10)) case float64: - buf.WriteString(strconv.FormatFloat(v, 'f', -1, 64)) + buf.Write(strconv.AppendFloat(buf.AvailableBuffer(), v, 'f', -1, 64)) case bool: - buf.WriteString(strconv.FormatBool(v)) + buf.Write(strconv.AppendBool(buf.AvailableBuffer(), v)) case fmt.Stringer: buf.WriteString(v.String()) case error: @@ -79,30 +78,6 @@ func (b *baseEncoder) castToString(arg any) string { } } -// printLog prints the given msgBuffer to the given outputType (stdout or stderr). -// If 'file' is not nil, the message is written to the file. -func (b *baseEncoder) printLog(outType s.OutputType, msgBuffer *bytes.Buffer, file *os.File) { - var err error - - switch outType { - case s.StdOutput: - _, err = os.Stdout.Write(msgBuffer.Bytes()) - case s.StdErrOutput: - _, err = os.Stderr.Write(msgBuffer.Bytes()) - case s.FileOutput: - if file == nil { - _, _ = os.Stderr.Write([]byte("tiny-logger-err: given out file is nil")) - return - } - - _, err = file.Write(msgBuffer.Bytes()) - } - - if err != nil { - _, _ = os.Stderr.Write([]byte("tiny-logger-err: " + err.Error() + "\n")) - } -} - // getBuffer returns a new bytes buffer from the pool. // If the pool is empty, a new buffer is created. func (b *baseEncoder) getBuffer() *bytes.Buffer { diff --git a/logs/encoders/base_test.go b/logs/encoders/base_test.go index 26c84e4..6925561 100644 --- a/logs/encoders/base_test.go +++ b/logs/encoders/base_test.go @@ -3,10 +3,10 @@ package encoders import ( "bytes" "errors" - "os" "sync" "testing" + "github.com/pho3b/tiny-logger/internal/services" s "github.com/pho3b/tiny-logger/shared" "github.com/stretchr/testify/assert" ) @@ -87,13 +87,13 @@ func TestBuildMsgWithCastAndConcatenateInto(t *testing.T) { } func TestBaseEncoder_GetType(t *testing.T) { - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) assert.Equal(t, s.DefaultEncoderType, encoder.GetType()) - jsonEncoder := NewJSONEncoder() + jsonEncoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) assert.Equal(t, s.JsonEncoderType, jsonEncoder.GetType()) - yamlEncoder := NewYAMLEncoder() + yamlEncoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) assert.Equal(t, s.YamlEncoderType, yamlEncoder.GetType()) baseEncoder := newBaseEncoder() @@ -111,37 +111,3 @@ func newBaseEncoder() *baseEncoder { return encoder } - -// captureOutput redirects os.Stdout to capture the output of the function f -func captureOutput(f func()) string { - r, w, _ := os.Pipe() - defer r.Close() - - origStdout := os.Stdout - os.Stdout = w - - f() - w.Close() - os.Stdout = origStdout - - var buf bytes.Buffer - _, _ = buf.ReadFrom(r) - return buf.String() -} - -// captureErrorOutput redirects os.Stderr to capture the output of the function f -func captureErrorOutput(f func()) string { - r, w, _ := os.Pipe() - defer r.Close() - - origStderr := os.Stderr - os.Stderr = w - - f() - w.Close() - os.Stderr = origStderr - - var buf bytes.Buffer - _, _ = buf.ReadFrom(r) - return buf.String() -} diff --git a/logs/encoders/default.go b/logs/encoders/default.go index 7f80498..aac47f8 100644 --- a/logs/encoders/default.go +++ b/logs/encoders/default.go @@ -13,7 +13,7 @@ import ( type DefaultEncoder struct { baseEncoder dateTimeFormat s.DateTimeFormat - ColorsPrinter services.ColorsPrinter + printer services.Printer DateTimePrinter *services.DateTimePrinter } @@ -34,43 +34,40 @@ func (d *DefaultEncoder) Log( tEnabled, logger.GetColorsEnabled(), logger.GetShowLogLevel(), + logger.GetDateTimeFormat(), args..., ) msgBuffer.WriteByte('\n') - d.printLog(outType, msgBuffer, logger.GetLogFile()) + d.printer.PrintLog(outType, msgBuffer, logger.GetLogFile()) d.putBuffer(msgBuffer) } // Color formats and prints a colored Log message using the specified color. func (d *DefaultEncoder) Color(logger s.LoggerConfigsInterface, color c.Color, args ...any) { if len(args) > 0 { + dEnabled, tEnabled := logger.GetDateTimeEnabled() msgBuffer := d.getBuffer() msgBuffer.WriteString(color.String()) d.composeMsgInto( msgBuffer, ll.InfoLvlName, + dEnabled, + tEnabled, false, false, - false, - false, + logger.GetDateTimeFormat(), args..., ) msgBuffer.WriteString(c.Reset.String()) msgBuffer.WriteByte('\n') - d.printLog(s.StdOutput, msgBuffer, logger.GetLogFile()) + d.printer.PrintLog(s.StdOutput, msgBuffer, logger.GetLogFile()) d.putBuffer(msgBuffer) } } -// SetDateTimeFormat updates the date and time format used by the encoder's DateTimePrinter. -// This method triggers an immediate update of the cached date and time strings to match the new format. -func (d *DefaultEncoder) SetDateTimeFormat(format s.DateTimeFormat) { - d.DateTimePrinter.UpdateDateTimeFormat(format) -} - // composeMsgInto formats and writes the given 'msg' into the given buffer. func (d *DefaultEncoder) composeMsgInto( buf *bytes.Buffer, @@ -79,12 +76,13 @@ func (d *DefaultEncoder) composeMsgInto( timeEnabled bool, headerColorEnabled bool, showLogLevel bool, + dateTimeFormat s.DateTimeFormat, args ...any, ) { buf.Grow(len(args)*averageWordLen + defaultCharOverhead) isDateOrTimeEnabled := dateEnabled || timeEnabled - colors := d.ColorsPrinter.RetrieveColorsFromLogLevel(headerColorEnabled, ll.LogLvlNameToInt[logLevel]) + colors := d.printer.RetrieveColorsFromLogLevel(headerColorEnabled, ll.LogLvlNameToInt[logLevel]) buf.WriteString(string(colors[0])) if showLogLevel { @@ -96,8 +94,8 @@ func (d *DefaultEncoder) composeMsgInto( } if isDateOrTimeEnabled { - dateStr, timeStr, dateTimeStr := d.DateTimePrinter.RetrieveDateTime(dateEnabled, timeEnabled) - d.addFormattedDateTime(buf, dateStr, timeStr, dateTimeStr) + dateStr, timeStr, unixTs := d.DateTimePrinter.RetrieveDateTime(dateTimeFormat, dateEnabled, timeEnabled) + d.addFormattedDateTime(buf, dateStr, timeStr, unixTs) } if showLogLevel || isDateOrTimeEnabled { @@ -109,35 +107,37 @@ func (d *DefaultEncoder) composeMsgInto( d.castAndConcatenateInto(buf, args...) } -// addFormattedDateTime correctly formats the dateTime string, adding and removing square brackets -// and white spaces as needed. -// While formatting, it adds the dateTime string to the given buffer. -func (d *DefaultEncoder) addFormattedDateTime(buf *bytes.Buffer, dateStr, timeStr, dateTimeStr string) { - if dateStr == "" && timeStr == "" && dateTimeStr == "" { +// addFormattedDateTime formats and adds the date and time strings enclosed in square brackets to the given buffer. +func (d *DefaultEncoder) addFormattedDateTime(buf *bytes.Buffer, dateStr, timeStr, unixTs string) { + if unixTs != "" { + buf.WriteByte('[') + buf.WriteString(unixTs) + buf.WriteByte(']') + return } - buf.Grow(averageWordLen) - buf.WriteByte('[') - - if dateTimeStr != "" { - buf.WriteString(dateTimeStr) - } else { - buf.WriteString(dateStr) + if dateStr == "" && timeStr == "" { + return + } - if dateStr != "" && timeStr != "" { - buf.WriteByte(' ') - } + buf.WriteByte('[') + buf.WriteString(dateStr) - buf.WriteString(timeStr) + if dateStr != "" && timeStr != "" { + buf.WriteByte(' ') } + buf.WriteString(timeStr) buf.WriteByte(']') } // NewDefaultEncoder initializes and returns a new DefaultEncoder instance. -func NewDefaultEncoder() *DefaultEncoder { - encoder := &DefaultEncoder{DateTimePrinter: services.NewDateTimePrinter(), ColorsPrinter: services.ColorsPrinter{}} +func NewDefaultEncoder( + printer services.Printer, + dateTimePrinter *services.DateTimePrinter, +) *DefaultEncoder { + encoder := &DefaultEncoder{DateTimePrinter: dateTimePrinter, printer: printer} encoder.encoderType = s.DefaultEncoderType encoder.bufferSyncPool = sync.Pool{ New: func() any { diff --git a/logs/encoders/default_test.go b/logs/encoders/default_test.go index 806a469..c47d764 100644 --- a/logs/encoders/default_test.go +++ b/logs/encoders/default_test.go @@ -6,6 +6,7 @@ import ( "os/exec" "testing" + "github.com/pho3b/tiny-logger/internal/services" "github.com/pho3b/tiny-logger/logs/colors" ll "github.com/pho3b/tiny-logger/logs/log_level" s "github.com/pho3b/tiny-logger/shared" @@ -14,10 +15,10 @@ import ( ) func TestLogDebug(t *testing.T) { - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, s.StdOutput, "Test debug message") }) @@ -26,10 +27,10 @@ func TestLogDebug(t *testing.T) { } func TestLogInfo(t *testing.T) { - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.InfoLvlName, s.StdOutput, "Test info message") }) @@ -38,10 +39,10 @@ func TestLogInfo(t *testing.T) { } func TestLogWarn(t *testing.T) { - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, s.StdOutput, "Test warning message") }) @@ -50,10 +51,10 @@ func TestLogWarn(t *testing.T) { } func TestLogError(t *testing.T) { - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureErrorOutput(func() { + output := test.CaptureErrorOutput(func() { encoder.Log(loggerConfig, ll.ErrorLvlName, s.StdErrOutput, "Test error message") }) @@ -62,7 +63,7 @@ func TestLogError(t *testing.T) { } func TestLogFatalError(t *testing.T) { - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} if os.Getenv("BE_CRASHER") == "1" { @@ -79,7 +80,7 @@ func TestLogFatalError(t *testing.T) { func TestFormatDateTimeString(t *testing.T) { b := bytes.NewBuffer([]byte{}) - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) encoder.addFormattedDateTime(b, "dateTest", "timeTest", "") assert.Contains(t, b.String(), "[") @@ -104,10 +105,10 @@ func TestFormatDateTimeString(t *testing.T) { } func TestShowLogLevel(t *testing.T) { - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, s.StdOutput, "Test my-test message") }) @@ -116,7 +117,7 @@ func TestShowLogLevel(t *testing.T) { loggerConfig = &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ShowLogLevel: false} - output = captureOutput(func() { + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, s.StdOutput, "Test my-test message") }) @@ -125,19 +126,19 @@ func TestShowLogLevel(t *testing.T) { } func TestCheckColorsInTheOutput(t *testing.T) { - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: false, TimeEnabled: false, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, s.StdOutput, "Test msg") }) + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, s.StdOutput, "Test msg") }) assert.Contains(t, output, colors.Gray.String()) - output = captureOutput(func() { encoder.Log(loggerConfig, ll.InfoLvlName, s.StdOutput, "Test my-test message") }) + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.InfoLvlName, s.StdOutput, "Test my-test message") }) assert.Contains(t, output, colors.Cyan.String()) - output = captureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, s.StdOutput, "Test my-test message") }) + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, s.StdOutput, "Test my-test message") }) assert.Contains(t, output, colors.Yellow.String()) - output = captureErrorOutput(func() { encoder.Log(loggerConfig, ll.ErrorLvlName, s.StdErrOutput, "Test my-test message") }) + output = test.CaptureErrorOutput(func() { encoder.Log(loggerConfig, ll.ErrorLvlName, s.StdErrOutput, "Test my-test message") }) assert.Contains(t, output, colors.Red.String()) } @@ -145,7 +146,7 @@ func TestDefaultEncoder_Color(t *testing.T) { var output string testLog := "my testing Log" originalStdOut := os.Stdout - encoder := NewDefaultEncoder() + encoder := NewDefaultEncoder(services.NewPrinter(), services.GetDateTimePrinter()) lConfig := test.LoggerConfigMock{ DateEnabled: false, TimeEnabled: false, @@ -153,16 +154,16 @@ func TestDefaultEncoder_Color(t *testing.T) { ShowLogLevel: false, } - output = captureOutput(func() { encoder.Color(&lConfig, colors.Magenta, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Magenta, testLog) }) assert.Contains(t, output, colors.Magenta.String()+testLog) - output = captureOutput(func() { encoder.Color(&lConfig, colors.Cyan, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Cyan, testLog) }) assert.Contains(t, output, colors.Cyan.String()+testLog+colors.Reset.String()) - output = captureOutput(func() { encoder.Color(&lConfig, colors.Gray, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Gray, testLog) }) assert.Contains(t, output, colors.Gray.String()+testLog+colors.Reset.String()) - output = captureOutput(func() { encoder.Color(&lConfig, colors.Blue, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Blue, testLog) }) assert.Contains(t, output, colors.Blue.String()+testLog+colors.Reset.String()) os.Stdout = originalStdOut diff --git a/logs/encoders/json.go b/logs/encoders/json.go index 82c8b0e..9a8e3f9 100644 --- a/logs/encoders/json.go +++ b/logs/encoders/json.go @@ -14,6 +14,7 @@ type JSONEncoder struct { baseEncoder DateTimePrinter *services.DateTimePrinter jsonMarshaler services.JsonMarshaler + printer services.Printer } // Log formats and prints a log message to the given output type. @@ -40,7 +41,7 @@ func (j *JSONEncoder) Log( ) msgBuffer.WriteByte('\n') - j.printLog(outType, msgBuffer, logger.GetLogFile()) + j.printer.PrintLog(outType, msgBuffer, logger.GetLogFile()) j.putBuffer(msgBuffer) } @@ -65,17 +66,11 @@ func (j *JSONEncoder) Color(logger s.LoggerConfigsInterface, color c.Color, args msgBuffer.WriteString(c.Reset.String()) msgBuffer.WriteByte('\n') - j.printLog(s.StdOutput, msgBuffer, logger.GetLogFile()) + j.printer.PrintLog(s.StdOutput, msgBuffer, logger.GetLogFile()) j.putBuffer(msgBuffer) } } -// SetDateTimeFormat updates the date and time format used by the encoder's DateTimePrinter. -// This method triggers an immediate update of the cached date and time strings to match the new format. -func (j *JSONEncoder) SetDateTimeFormat(format s.DateTimeFormat) { - j.DateTimePrinter.UpdateDateTimeFormat(format) -} - // composeMsgInto formats and writes the given 'msg' into the given buffer. func (j *JSONEncoder) composeMsgInto( buf *bytes.Buffer, @@ -89,7 +84,7 @@ func (j *JSONEncoder) composeMsgInto( extras ...any, ) { buf.Grow((averageWordLen * len(extras)) + len(msg) + 60) - dateStr, timeStr, dateTimeStr := j.DateTimePrinter.RetrieveDateTime(dateEnabled, timeEnabled) + dateStr, timeStr, unixTs := j.DateTimePrinter.RetrieveDateTime(dateTimeFormat, dateEnabled, timeEnabled) if !showLogLevel { logLevel = "" @@ -98,20 +93,23 @@ func (j *JSONEncoder) composeMsgInto( jsonMarshaler.MarshalInto( buf, services.JsonLogEntry{ - Level: logLevel.String(), - Date: dateStr, - DateTime: dateTimeStr, - Time: timeStr, - DateTimeFormat: dateTimeFormat, - Message: msg, - Extras: extras, + Level: logLevel.String(), + Date: dateStr, + Time: timeStr, + UnixTS: unixTs, + Message: msg, + Extras: extras, }, ) } // NewJSONEncoder initializes and returns a new JSONEncoder instance. -func NewJSONEncoder() *JSONEncoder { - encoder := &JSONEncoder{DateTimePrinter: services.NewDateTimePrinter(), jsonMarshaler: services.JsonMarshaler{}} +func NewJSONEncoder( + printer services.Printer, + jsonMarshaler services.JsonMarshaler, + dateTimePrinter *services.DateTimePrinter, +) *JSONEncoder { + encoder := &JSONEncoder{DateTimePrinter: dateTimePrinter, jsonMarshaler: jsonMarshaler, printer: printer} encoder.encoderType = s.JsonEncoderType encoder.bufferSyncPool = sync.Pool{ New: func() any { diff --git a/logs/encoders/json_test.go b/logs/encoders/json_test.go index f621708..08189f0 100644 --- a/logs/encoders/json_test.go +++ b/logs/encoders/json_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/pho3b/tiny-logger/internal/services" "github.com/pho3b/tiny-logger/logs/colors" ll "github.com/pho3b/tiny-logger/logs/log_level" "github.com/pho3b/tiny-logger/shared" @@ -24,10 +25,10 @@ func decodeLogEntry(t *testing.T, logOutput string) shared.JsonLog { } func TestJSONEncoder_LogDebug(t *testing.T) { - encoder := NewJSONEncoder() + encoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, shared.StdOutput, "Test debug message") }) @@ -37,10 +38,10 @@ func TestJSONEncoder_LogDebug(t *testing.T) { } func TestJSONEncoder_LogInfo(t *testing.T) { - encoder := NewJSONEncoder() + encoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.InfoLvlName, shared.StdOutput, "Test info message") }) @@ -50,10 +51,10 @@ func TestJSONEncoder_LogInfo(t *testing.T) { } func TestJSONEncoder_LogInfoWithExtras(t *testing.T) { - encoder := NewJSONEncoder() + encoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.InfoLvlName, shared.StdOutput, "Test info message") }) @@ -62,7 +63,7 @@ func TestJSONEncoder_LogInfoWithExtras(t *testing.T) { assert.Equal(t, "Test info message", entry.Message) assert.IsType(t, make(map[string]any), entry.Extras) - output = captureOutput(func() { + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.InfoLvlName, shared.StdOutput, "Test info message with extras", "Location", "Italy", "Weather", "sunny", "Mood") }) @@ -75,10 +76,10 @@ func TestJSONEncoder_LogInfoWithExtras(t *testing.T) { } func TestJSONEncoder_LogWarn(t *testing.T) { - encoder := NewJSONEncoder() + encoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, shared.StdOutput, "Test warning message") }) @@ -88,10 +89,10 @@ func TestJSONEncoder_LogWarn(t *testing.T) { } func TestJSONEncoder_LogError(t *testing.T) { - encoder := NewJSONEncoder() + encoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureErrorOutput(func() { + output := test.CaptureErrorOutput(func() { encoder.Log(loggerConfig, ll.ErrorLvlName, shared.StdErrOutput, "Test error message") }) @@ -101,7 +102,7 @@ func TestJSONEncoder_LogError(t *testing.T) { } func TestJSONEncoder_LogFatalError(t *testing.T) { - encoder := NewJSONEncoder() + encoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} if os.Getenv("BE_CRASHER") == "1" { @@ -117,9 +118,9 @@ func TestJSONEncoder_LogFatalError(t *testing.T) { } func TestJSONEncoder_DateTime(t *testing.T) { - encoder := NewJSONEncoder() + encoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, shared.StdOutput, "Test msg") }) @@ -130,7 +131,7 @@ func TestJSONEncoder_DateTime(t *testing.T) { assert.NotEmpty(t, entry.Time) loggerConfig = &test.LoggerConfigMock{DateEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output = captureOutput(func() { + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, shared.StdOutput, "Test msg") }) @@ -141,7 +142,7 @@ func TestJSONEncoder_DateTime(t *testing.T) { assert.NotEmpty(t, entry.Date) loggerConfig = &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output = captureOutput(func() { + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, shared.StdOutput, "Test msg") }) @@ -153,10 +154,10 @@ func TestJSONEncoder_DateTime(t *testing.T) { } func TestJSONEncoder_ExtraMessages(t *testing.T) { - jsonEncoder := NewJSONEncoder() + jsonEncoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) lConfig := &test.LoggerConfigMock{DateEnabled: false, TimeEnabled: false, ColorsEnabled: false, ShowLogLevel: false} - output := captureOutput(func() { + output := test.CaptureOutput(func() { jsonEncoder.Log(lConfig, ll.InfoLvlName, shared.StdOutput, "test", "user", "alice", "ip", "192.168.1.1") }) entry := decodeLogEntry(t, output) @@ -164,14 +165,14 @@ func TestJSONEncoder_ExtraMessages(t *testing.T) { assert.NotNil(t, entry.Extras["ip"]) assert.Len(t, entry.Extras, 2) - output = captureOutput(func() { + output = test.CaptureOutput(func() { jsonEncoder.Log(lConfig, ll.InfoLvlName, shared.StdOutput, "test", "user", "alice", "ip") }) entry = decodeLogEntry(t, output) assert.Nil(t, entry.Extras["ip"]) assert.Len(t, entry.Extras, 2) - output = captureOutput(func() { + output = test.CaptureOutput(func() { jsonEncoder.Log(lConfig, ll.InfoLvlName, shared.StdOutput, "test", "user", "alice", "ip", "192.168.1.1", "city", "paris", "pass") }) entry = decodeLogEntry(t, output) @@ -183,10 +184,10 @@ func TestJSONEncoder_ExtraMessages(t *testing.T) { } func TestJSONEncoder_ShowLogLevelLt(t *testing.T) { - encoder := NewJSONEncoder() + encoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, shared.StdOutput, "Test debug message") }) @@ -196,7 +197,7 @@ func TestJSONEncoder_ShowLogLevelLt(t *testing.T) { loggerConfig = &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: false} - output = captureOutput(func() { + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, shared.StdOutput, "Test debug message") }) @@ -209,7 +210,7 @@ func TestJSONEncoder_Color(t *testing.T) { testLog := "my testing Log" originalStdOut := os.Stdout - encoder := NewJSONEncoder() + encoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) lConfig := test.LoggerConfigMock{ DateEnabled: false, TimeEnabled: false, @@ -217,25 +218,25 @@ func TestJSONEncoder_Color(t *testing.T) { ShowLogLevel: false, } - output = captureOutput(func() { encoder.Color(&lConfig, colors.Magenta, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Magenta, testLog) }) assert.Contains(t, output, colors.Magenta.String()) assert.Contains(t, output, testLog) assert.NotContains(t, output, time.Now().Format("02/01/2006")) assert.Contains(t, output, colors.Reset.String()) lConfig.DateEnabled = true - output = captureOutput(func() { encoder.Color(&lConfig, colors.Cyan, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Cyan, testLog) }) assert.Contains(t, output, colors.Cyan.String()) assert.Contains(t, output, time.Now().Format("02/01/2006")) assert.Contains(t, output, testLog) assert.Contains(t, output, colors.Reset.String()) - output = captureOutput(func() { encoder.Color(&lConfig, colors.Gray, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Gray, testLog) }) assert.Contains(t, output, colors.Gray.String()) assert.Contains(t, output, testLog) assert.Contains(t, output, colors.Reset.String()) - output = captureOutput(func() { encoder.Color(&lConfig, colors.Blue, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Blue, testLog) }) assert.Contains(t, output, colors.Blue.String()) assert.Contains(t, output, testLog) assert.Contains(t, output, colors.Reset.String()) @@ -248,7 +249,7 @@ func TestJSONEncoder_ValidJSONOutput(t *testing.T) { originalStdOut := os.Stdout testLog := "my testing Log" - jsonEncoder := NewJSONEncoder() + jsonEncoder := NewJSONEncoder(services.NewPrinter(), services.NewJsonMarshaler(), services.GetDateTimePrinter()) lConfig := &test.LoggerConfigMock{ DateEnabled: false, TimeEnabled: false, @@ -256,21 +257,21 @@ func TestJSONEncoder_ValidJSONOutput(t *testing.T) { ShowLogLevel: false, } - jsonMsg = captureOutput( + jsonMsg = test.CaptureOutput( func() { jsonEncoder.Log(lConfig, ll.DebugLvlName, shared.StdOutput, testLog, "id", 3) }, ) assert.NoError(t, json.Unmarshal([]byte(jsonMsg), &shared.JsonLog{})) - jsonMsg = captureOutput( + jsonMsg = test.CaptureOutput( func() { jsonEncoder.Log(lConfig, ll.DebugLvlName, shared.StdOutput, testLog, "id", 3, 34, []string{"test", "test2"}) }, ) assert.NoError(t, json.Unmarshal([]byte(jsonMsg), &shared.JsonLog{})) - jsonMsg = captureOutput(func() { + jsonMsg = test.CaptureOutput(func() { jsonEncoder.Log(lConfig, ll.DebugLvlName, shared.StdOutput, testLog, "id", 3, 34, []string{"test", "test2"}, []string{"k", "k2"}, 2.3, 'f', 'A') }) assert.NoError(t, json.Unmarshal([]byte(jsonMsg), &shared.JsonLog{})) diff --git a/logs/encoders/yaml.go b/logs/encoders/yaml.go index f8d224b..109712c 100644 --- a/logs/encoders/yaml.go +++ b/logs/encoders/yaml.go @@ -14,6 +14,7 @@ type YAMLEncoder struct { baseEncoder DateTimePrinter *services.DateTimePrinter yamlMarshaler services.YamlMarshaler + printer services.Printer } // Log formats and prints a log message to the given output type. @@ -40,7 +41,7 @@ func (y *YAMLEncoder) Log( ) msgBuffer.WriteByte('\n') - y.printLog(outType, msgBuffer, logger.GetLogFile()) + y.printer.PrintLog(outType, msgBuffer, logger.GetLogFile()) y.putBuffer(msgBuffer) } @@ -65,17 +66,11 @@ func (y *YAMLEncoder) Color(logger s.LoggerConfigsInterface, color c.Color, args msgBuffer.WriteString(c.Reset.String()) msgBuffer.WriteByte('\n') - y.printLog(s.StdOutput, msgBuffer, logger.GetLogFile()) + y.printer.PrintLog(s.StdOutput, msgBuffer, logger.GetLogFile()) y.putBuffer(msgBuffer) } } -// SetDateTimeFormat updates the date and time format used by the encoder's DateTimePrinter. -// This method triggers an immediate update of the cached date and time strings to match the new format. -func (y *YAMLEncoder) SetDateTimeFormat(format s.DateTimeFormat) { - y.DateTimePrinter.UpdateDateTimeFormat(format) -} - // composeMsgInto formats and writes the given 'msg' into the given buffer. func (y *YAMLEncoder) composeMsgInto( buf *bytes.Buffer, @@ -89,7 +84,7 @@ func (y *YAMLEncoder) composeMsgInto( extras ...any, ) { buf.Grow((averageWordLen * len(extras)) + len(msg) + 60) - date, time, dateTime := y.DateTimePrinter.RetrieveDateTime(dateEnabled, timeEnabled) + date, time, unixTs := y.DateTimePrinter.RetrieveDateTime(dateTimeFormat, dateEnabled, timeEnabled) if !showLogLevel { logLevel = "" @@ -98,20 +93,23 @@ func (y *YAMLEncoder) composeMsgInto( yamlMarshaler.MarshalInto( buf, services.YamlLogEntry{ - Level: logLevel.String(), - Date: date, - Time: time, - DateTime: dateTime, - DateTimeFormat: dateTimeFormat, - Message: msg, - Extras: extras, + Level: logLevel.String(), + Date: date, + Time: time, + UnixTS: unixTs, + Message: msg, + Extras: extras, }, ) } // NewYAMLEncoder initializes and returns a new YAMLEncoder instance. -func NewYAMLEncoder() *YAMLEncoder { - encoder := &YAMLEncoder{DateTimePrinter: services.NewDateTimePrinter(), yamlMarshaler: services.NewYamlMarshaler()} +func NewYAMLEncoder( + printer services.Printer, + yamlMarshaler services.YamlMarshaler, + dateTimePrinter *services.DateTimePrinter, +) *YAMLEncoder { + encoder := &YAMLEncoder{DateTimePrinter: dateTimePrinter, yamlMarshaler: yamlMarshaler, printer: printer} encoder.encoderType = s.YamlEncoderType encoder.bufferSyncPool = sync.Pool{ New: func() any { diff --git a/logs/encoders/yaml_test.go b/logs/encoders/yaml_test.go index d1141fb..dbabc4c 100644 --- a/logs/encoders/yaml_test.go +++ b/logs/encoders/yaml_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/pho3b/tiny-logger/internal/services" "github.com/pho3b/tiny-logger/logs/colors" ll "github.com/pho3b/tiny-logger/logs/log_level" "github.com/pho3b/tiny-logger/shared" @@ -23,10 +24,10 @@ func decodeYamlLogEntry(t *testing.T, logOutput string) shared.YamlLog { } func TestYAMLEncoder_LogDebug(t *testing.T) { - encoder := NewYAMLEncoder() + encoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, shared.StdOutput, "Test debug message") }) @@ -36,10 +37,10 @@ func TestYAMLEncoder_LogDebug(t *testing.T) { } func TestYAMLEncoder_LogInfo(t *testing.T) { - encoder := NewYAMLEncoder() + encoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.InfoLvlName, shared.StdOutput, "Test info message") }) @@ -49,10 +50,10 @@ func TestYAMLEncoder_LogInfo(t *testing.T) { } func TestYAMLEncoder_LogInfoWithExtras(t *testing.T) { - encoder := NewYAMLEncoder() + encoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.InfoLvlName, shared.StdOutput, "Test info message") }) @@ -61,7 +62,7 @@ func TestYAMLEncoder_LogInfoWithExtras(t *testing.T) { assert.Equal(t, "Test info message", entry.Message) assert.IsType(t, make(map[string]any), entry.Extras) - output = captureOutput(func() { + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.InfoLvlName, shared.StdOutput, "Test info message with extras", "Location", "Italy", "Weather", "sunny", "Mood") }) @@ -74,10 +75,10 @@ func TestYAMLEncoder_LogInfoWithExtras(t *testing.T) { } func TestYAMLEncoder_LogWarn(t *testing.T) { - encoder := NewYAMLEncoder() + encoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, shared.StdOutput, "Test warning message") }) @@ -87,10 +88,10 @@ func TestYAMLEncoder_LogWarn(t *testing.T) { } func TestYAMLEncoder_LogError(t *testing.T) { - encoder := NewYAMLEncoder() + encoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureErrorOutput(func() { + output := test.CaptureErrorOutput(func() { encoder.Log(loggerConfig, ll.ErrorLvlName, shared.StdErrOutput, "Test error message") }) @@ -100,7 +101,7 @@ func TestYAMLEncoder_LogError(t *testing.T) { } func TestYAMLEncoder_LogFatalError(t *testing.T) { - encoder := NewYAMLEncoder() + encoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} if os.Getenv("BE_CRASHER") == "1" { @@ -116,9 +117,9 @@ func TestYAMLEncoder_LogFatalError(t *testing.T) { } func TestYAMLEncoder_DateTime(t *testing.T) { - encoder := NewYAMLEncoder() + encoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, shared.StdOutput, "Test msg") }) @@ -129,7 +130,7 @@ func TestYAMLEncoder_DateTime(t *testing.T) { assert.NotEmpty(t, entry.Time) loggerConfig = &test.LoggerConfigMock{DateEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output = captureOutput(func() { + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, shared.StdOutput, "Test msg") }) @@ -140,7 +141,7 @@ func TestYAMLEncoder_DateTime(t *testing.T) { assert.NotEmpty(t, entry.Date) loggerConfig = &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output = captureOutput(func() { + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.WarnLvlName, shared.StdOutput, "Test msg") }) @@ -152,10 +153,10 @@ func TestYAMLEncoder_DateTime(t *testing.T) { } func TestYAMLEncoder_ExtraMessages(t *testing.T) { - yamlEncoder := NewYAMLEncoder() + yamlEncoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) lConfig := &test.LoggerConfigMock{DateEnabled: false, TimeEnabled: false, ColorsEnabled: false, ShowLogLevel: false} - output := captureOutput(func() { + output := test.CaptureOutput(func() { yamlEncoder.Log(lConfig, ll.InfoLvlName, shared.StdOutput, "test", "user", "alice", "ip", "192.168.1.1") }) entry := decodeYamlLogEntry(t, output) @@ -163,14 +164,14 @@ func TestYAMLEncoder_ExtraMessages(t *testing.T) { assert.NotNil(t, entry.Extras["ip"]) assert.Len(t, entry.Extras, 2) - output = captureOutput(func() { + output = test.CaptureOutput(func() { yamlEncoder.Log(lConfig, ll.InfoLvlName, shared.StdOutput, "test", "user", "alice", "ip") }) entry = decodeYamlLogEntry(t, output) assert.Nil(t, entry.Extras["ip"]) assert.Len(t, entry.Extras, 2) - output = captureOutput(func() { + output = test.CaptureOutput(func() { yamlEncoder.Log(lConfig, ll.InfoLvlName, shared.StdOutput, "test", "user", "alice", "ip", "192.168.1.1", "city", "paris", "pass") }) entry = decodeYamlLogEntry(t, output) @@ -182,10 +183,10 @@ func TestYAMLEncoder_ExtraMessages(t *testing.T) { } func TestYAMLEncoder_ShowLogLevelLt(t *testing.T) { - encoder := NewYAMLEncoder() + encoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) loggerConfig := &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: true} - output := captureOutput(func() { + output := test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, shared.StdOutput, "Test debug message") }) @@ -195,7 +196,7 @@ func TestYAMLEncoder_ShowLogLevelLt(t *testing.T) { loggerConfig = &test.LoggerConfigMock{DateEnabled: true, TimeEnabled: true, ColorsEnabled: true, ShowLogLevel: false} - output = captureOutput(func() { + output = test.CaptureOutput(func() { encoder.Log(loggerConfig, ll.DebugLvlName, shared.StdOutput, "Test debug message") }) @@ -207,7 +208,7 @@ func TestYAMLEncoder_Color(t *testing.T) { var output string testLog := "my testing Log" originalStdOut := os.Stdout - encoder := NewYAMLEncoder() + encoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) lConfig := test.LoggerConfigMock{ DateEnabled: false, TimeEnabled: false, @@ -215,25 +216,25 @@ func TestYAMLEncoder_Color(t *testing.T) { ShowLogLevel: false, } - output = captureOutput(func() { encoder.Color(&lConfig, colors.Magenta, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Magenta, testLog) }) assert.Contains(t, output, colors.Magenta.String()) assert.Contains(t, output, testLog) assert.NotContains(t, output, time.Now().Format("02/01/2006")) assert.Contains(t, output, colors.Reset.String()) lConfig.DateEnabled = true - output = captureOutput(func() { encoder.Color(&lConfig, colors.Cyan, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Cyan, testLog) }) assert.Contains(t, output, colors.Cyan.String()) assert.Contains(t, output, time.Now().Format("02/01/2006")) assert.Contains(t, output, testLog) assert.Contains(t, output, colors.Reset.String()) - output = captureOutput(func() { encoder.Color(&lConfig, colors.Gray, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Gray, testLog) }) assert.Contains(t, output, colors.Gray.String()) assert.Contains(t, output, testLog) assert.Contains(t, output, colors.Reset.String()) - output = captureOutput(func() { encoder.Color(&lConfig, colors.Blue, testLog) }) + output = test.CaptureOutput(func() { encoder.Color(&lConfig, colors.Blue, testLog) }) assert.Contains(t, output, colors.Blue.String()) assert.Contains(t, output, testLog) assert.Contains(t, output, colors.Reset.String()) @@ -246,7 +247,7 @@ func TestYAMLEncoder_ValidYAMLOutput(t *testing.T) { originalStdOut := os.Stdout testLog := "my testing Log" - yamlEncoder := NewYAMLEncoder() + yamlEncoder := NewYAMLEncoder(services.NewPrinter(), services.NewYamlMarshaler(), services.GetDateTimePrinter()) lConfig := &test.LoggerConfigMock{ DateEnabled: false, TimeEnabled: false, @@ -254,21 +255,21 @@ func TestYAMLEncoder_ValidYAMLOutput(t *testing.T) { ShowLogLevel: false, } - yamlMsg = captureOutput( + yamlMsg = test.CaptureOutput( func() { yamlEncoder.Log(lConfig, ll.DebugLvlName, shared.StdOutput, testLog, "id", 3) }, ) assert.NoError(t, yaml.Unmarshal([]byte(yamlMsg), &shared.YamlLog{})) - yamlMsg = captureOutput( + yamlMsg = test.CaptureOutput( func() { yamlEncoder.Log(lConfig, ll.DebugLvlName, shared.StdOutput, testLog, "id", 3, 34, []string{"test", "test2"}) }, ) assert.NoError(t, yaml.Unmarshal([]byte(yamlMsg), &shared.YamlLog{})) - yamlMsg = captureOutput(func() { + yamlMsg = test.CaptureOutput(func() { yamlEncoder.Log(lConfig, ll.DebugLvlName, shared.StdOutput, testLog, "id", 3, 34, []string{"test", "test2"}, []string{"k", "k2"}, 2.3, 'f', 'A') }) assert.NoError(t, yaml.Unmarshal([]byte(yamlMsg), &shared.YamlLog{})) diff --git a/logs/logger.go b/logs/logger.go index 916be5b..e68755b 100644 --- a/logs/logger.go +++ b/logs/logger.go @@ -3,6 +3,7 @@ package logs import ( "os" + "github.com/pho3b/tiny-logger/internal/services" "github.com/pho3b/tiny-logger/logs/colors" "github.com/pho3b/tiny-logger/logs/encoders" ll "github.com/pho3b/tiny-logger/logs/log_level" @@ -10,14 +11,16 @@ import ( ) type Logger struct { - dateEnabled bool - timeEnabled bool - colorsEnabled bool - showLogLevel bool - encoder s.EncoderInterface - logLvl ll.LogLevel - outFile *os.File - dateTimeFormat s.DateTimeFormat + dateEnabled bool + timeEnabled bool + colorsEnabled bool + showLogLevel bool + encoder s.EncoderInterface + logLvl ll.LogLevel + outFile *os.File + dateTimeFormat s.DateTimeFormat + printer services.Printer + dateTimePrinter *services.DateTimePrinter } // Debug logs a debug-level message if the logger's log level allows it. @@ -51,7 +54,7 @@ func (l *Logger) Error(args ...any) { // FatalError logs a fatal error message and terminates the application only if any given args is not NIl, // otherwise the method does nothing. func (l *Logger) FatalError(args ...any) { - if l.logLvl.Lvl >= ll.ErrorLvl && len(args) > 0 && !l.areAllNil(args...) { + if len(args) > 0 && !l.areAllNil(args...) { l.encoder.Log(l, ll.FatalErrorLvlName, l.checkOutFile(s.StdErrOutput), args...) os.Exit(1) } @@ -152,14 +155,13 @@ func (l *Logger) GetEncoderType() s.EncoderType { func (l *Logger) SetEncoder(encoderType s.EncoderType) *Logger { switch encoderType { case s.DefaultEncoderType: - l.encoder = encoders.NewDefaultEncoder() + l.encoder = encoders.NewDefaultEncoder(l.printer, l.dateTimePrinter) case s.JsonEncoderType: - l.encoder = encoders.NewJSONEncoder() + l.encoder = encoders.NewJSONEncoder(l.printer, services.NewJsonMarshaler(), l.dateTimePrinter) case s.YamlEncoderType: - l.encoder = encoders.NewYAMLEncoder() + l.encoder = encoders.NewYAMLEncoder(l.printer, services.NewYamlMarshaler(), l.dateTimePrinter) } - l.encoder.SetDateTimeFormat(l.dateTimeFormat) return l } @@ -204,7 +206,6 @@ func (l *Logger) GetDateTimeFormat() s.DateTimeFormat { // SetDateTimeFormat sets the DateTimeFormat of the logger. func (l *Logger) SetDateTimeFormat(format s.DateTimeFormat) *Logger { l.dateTimeFormat = format - l.encoder.SetDateTimeFormat(l.dateTimeFormat) return l } @@ -233,6 +234,8 @@ func (l *Logger) checkOutFile(outType s.OutputType) s.OutputType { func NewLogger() *Logger { logger := &Logger{showLogLevel: true, dateTimeFormat: s.IT} logger.SetLogLvlEnvVariable(ll.DefaultEnvLogLvlVar) + logger.printer = services.NewPrinter() + logger.dateTimePrinter = services.GetDateTimePrinter() logger.SetEncoder(s.DefaultEncoderType) return logger diff --git a/logs/logger_test.go b/logs/logger_test.go index d8d81db..6a95677 100644 --- a/logs/logger_test.go +++ b/logs/logger_test.go @@ -12,6 +12,7 @@ import ( "github.com/pho3b/tiny-logger/logs/colors" "github.com/pho3b/tiny-logger/logs/log_level" "github.com/pho3b/tiny-logger/shared" + "github.com/pho3b/tiny-logger/test" "github.com/stretchr/testify/assert" ) @@ -224,16 +225,16 @@ func TestLogger_Color(t *testing.T) { originalStdOut := os.Stdout logger := NewLogger() - output = captureOutput(func() { logger.Color(colors.Magenta, testLog) }) + output = test.CaptureOutput(func() { logger.Color(colors.Magenta, testLog) }) assert.Contains(t, output, colors.Magenta.String()+testLog) - output = captureOutput(func() { logger.Color(colors.Cyan, testLog) }) + output = test.CaptureOutput(func() { logger.Color(colors.Cyan, testLog) }) assert.Contains(t, output, colors.Cyan.String()+testLog+colors.Reset.String()) - output = captureOutput(func() { logger.Color(colors.Gray, testLog) }) + output = test.CaptureOutput(func() { logger.Color(colors.Gray, testLog) }) assert.Contains(t, output, colors.Gray.String()+testLog+colors.Reset.String()) - output = captureOutput(func() { logger.Color(colors.Blue, testLog) }) + output = test.CaptureOutput(func() { logger.Color(colors.Blue, testLog) }) assert.Contains(t, output, colors.Blue.String()+testLog+colors.Reset.String()) os.Stdout = originalStdOut @@ -244,13 +245,13 @@ func TestLogger_ShowLogLevel(t *testing.T) { ShowLogLevel(true). EnableColors(false) - output := captureOutput(func() { + output := test.CaptureOutput(func() { logger.Info("my testing log") }) assert.Contains(t, output, "INFO: my testing log") logger.ShowLogLevel(false) - output = captureOutput(func() { + output = test.CaptureOutput(func() { logger.Info("my testing log") }) assert.NotContains(t, output, "INFO: my testing log") @@ -275,14 +276,14 @@ func TestLogger_CorrectLogsFormattingDefaultEncoder(t *testing.T) { logger := NewLogger().SetEncoder(shared.DefaultEncoderType).AddDateTime(true).ShowLogLevel(true) re := regexp.MustCompile(`^([A-Z]+) \[(\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2})\]: (.+)\n?$`) - outMsg := captureOutput(func() { logger.Debug("testing log") }) + outMsg := test.CaptureOutput(func() { logger.Debug("testing log") }) matches := re.FindStringSubmatch(outMsg) assert.Equal(t, "DEBUG", matches[1]) assert.NotNil(t, matches[2]) assert.Equal(t, "testing log", matches[3]) - outMsg = captureOutput(func() { + outMsg = test.CaptureOutput(func() { logger.Warn("testing log") }) @@ -291,7 +292,7 @@ func TestLogger_CorrectLogsFormattingDefaultEncoder(t *testing.T) { assert.NotNil(t, matches[2]) assert.Equal(t, "testing log", matches[3]) - outMsg = captureErrorOutput(func() { + outMsg = test.CaptureErrorOutput(func() { logger.Error("testing log") }) @@ -300,7 +301,7 @@ func TestLogger_CorrectLogsFormattingDefaultEncoder(t *testing.T) { assert.NotNil(t, matches[2]) assert.Equal(t, "testing log", matches[3]) - outMsg = captureOutput(func() { + outMsg = test.CaptureOutput(func() { logger.Info("testing log") }) @@ -387,7 +388,7 @@ func TestLogger_SetLogFile_ExistingFile(t *testing.T) { func TestLogger_SetLogFile_Nil(t *testing.T) { logger := NewLogger() - warnOut := captureOutput(func() { logger.SetLogFile(nil) }) + warnOut := test.CaptureOutput(func() { logger.SetLogFile(nil) }) assert.Equal(t, "WARN: the given log file is nil, skipping logs redirection\n", warnOut) assert.Nil(t, logger.outFile) assert.Nil(t, logger.GetLogFile()) @@ -416,7 +417,7 @@ func TestLogger_CloseLogFile_NoFileSet(t *testing.T) { logger := NewLogger() // Test closing when no file is set - should log warning and not crash - output := captureOutput(func() { + output := test.CaptureOutput(func() { logger.CloseLogFile() }) @@ -472,40 +473,6 @@ func TestLogger_LogsRedirectedToFile(t *testing.T) { assert.Contains(t, contentStr, "error message") } -// captureOutput redirects os.Stdout to capture the output of the function f -func captureOutput(f func()) string { - r, w, _ := os.Pipe() - defer r.Close() - - origStdout := os.Stdout - os.Stdout = w - - f() - w.Close() - os.Stdout = origStdout - - var buf bytes.Buffer - _, _ = buf.ReadFrom(r) - return buf.String() -} - -// captureErrorOutput redirects os.Stderr to capture the output of the function f -func captureErrorOutput(f func()) string { - r, w, _ := os.Pipe() - defer r.Close() - - origStderr := os.Stderr - os.Stderr = w - - f() - w.Close() - os.Stderr = origStderr - - var buf bytes.Buffer - _, _ = buf.ReadFrom(r) - return buf.String() -} - func createMockOutFile(fileName string) *os.File { file, err := os.OpenFile(fmt.Sprintf("./%s", fileName), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { diff --git a/makefile b/makefile index 4b0e0c9..854cc38 100644 --- a/makefile +++ b/makefile @@ -12,4 +12,4 @@ test-coverage: # Run benchmark tests test-benchmark: - CGO_ENABLED=1 GOARCH=${ARCH} go test ./test/benchmark_test.go -bench=. -benchmem -benchtime=5s -cpu=8 | grep /op + CGO_ENABLED=1 GOARCH=${ARCH} go test ./test/benchmark_test.go -bench=. -benchmem -benchtime=8s -cpu=8 | grep /op diff --git a/shared/interfaces.go b/shared/interfaces.go index 06d8853..eae90c6 100644 --- a/shared/interfaces.go +++ b/shared/interfaces.go @@ -30,5 +30,4 @@ type EncoderInterface interface { Log(logger LoggerConfigsInterface, lvl log_level.LogLvlName, outType OutputType, args ...any) Color(lConfigs LoggerConfigsInterface, color colors.Color, args ...any) GetType() EncoderType - SetDateTimeFormat(format DateTimeFormat) } diff --git a/shared/models.go b/shared/models.go index 2bbf47f..2574430 100644 --- a/shared/models.go +++ b/shared/models.go @@ -1,6 +1,6 @@ package shared -// JsonLog represents the structure of a JSON log and can be used to marshal JSON logs. +// JsonLog represents the structure of a JSON log and can be used to Unmarshal JSON logEntries. type JsonLog struct { Level string `json:"level,omitempty"` Date string `json:"date,omitempty"` @@ -10,7 +10,7 @@ type JsonLog struct { Extras map[string]any `json:"extras,omitempty"` } -// YamlLog represents the structure of a YAML log and can be used to marshal YAML logs. +// YamlLog represents the structure of a YAML log and can be used to Unmarshal YAML log entries. type YamlLog struct { Level string `yaml:"level,omitempty"` Date string `yaml:"date,omitempty"` diff --git a/test/functions.go b/test/functions.go new file mode 100644 index 0000000..41969c4 --- /dev/null +++ b/test/functions.go @@ -0,0 +1,40 @@ +package test + +import ( + "bytes" + "os" +) + +// CaptureOutput redirects os.Stdout to capture the output of the function f +func CaptureOutput(f func()) string { + r, w, _ := os.Pipe() + defer r.Close() + + origStdout := os.Stdout + os.Stdout = w + + f() + w.Close() + os.Stdout = origStdout + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + return buf.String() +} + +// CaptureErrorOutput redirects os.Stderr to capture the output of the function f +func CaptureErrorOutput(f func()) string { + r, w, _ := os.Pipe() + defer r.Close() + + origStderr := os.Stderr + os.Stderr = w + + f() + w.Close() + os.Stderr = origStderr + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + return buf.String() +}