diff --git a/csv_writer.go b/csv_writer.go new file mode 100644 index 0000000..f0e1e89 --- /dev/null +++ b/csv_writer.go @@ -0,0 +1,36 @@ +package tracelog + +import ( + "encoding/csv" + "fmt" + "io" + "sync" +) + +type csvWriter struct { + lock sync.Mutex + out io.Writer + fields []string +} + +func (csvWriter *csvWriter) Log(fields Fields) { + vals := getValuesFromMap(fields, csvWriter.fields) + stringVals := make([]string, len(vals)) + for i := range vals { + stringVals[i] = fmt.Sprint(vals[i]) + } + csvWriter.lock.Lock() + defer csvWriter.lock.Unlock() + w := csv.NewWriter(csvWriter.out) + err := w.Write(stringVals) + if err == nil { + w.Flush() + } +} + +func NewCsvWriter(out io.Writer, fields []string) LoggerWriter { + return &csvWriter{ + out: out, + fields: fields, + } +} diff --git a/error_logger.go b/error_logger.go deleted file mode 100644 index 0abdf3b..0000000 --- a/error_logger.go +++ /dev/null @@ -1,56 +0,0 @@ -package tracelog - -import ( - "io" - "log" -) - -type errorLogger struct { - *log.Logger -} - -func NewErrorLogger(out io.Writer, prefix string) *errorLogger { - return &errorLogger{log.New(out, prefix, timeFlags)} -} - -func (logger *errorLogger) PanicError(err error) { - logger.Panicf(GetErrorFormatter(), err) -} - -func (logger *errorLogger) PanicfOnError(format string, err error) { - if err != nil { - logger.Panicf(format, err) - } -} - -func (logger *errorLogger) PanicOnError(err error) { - if err != nil { - logger.PanicError(err) - } -} - -func (logger *errorLogger) FatalError(err error) { - logger.Fatalf(GetErrorFormatter(), err) -} - -func (logger *errorLogger) FatalfOnError(format string, err error) { - if err != nil { - logger.Fatalf(format, err) - } -} - -func (logger *errorLogger) FatalOnError(err error) { - if err != nil { - logger.FatalError(err) - } -} - -func (logger *errorLogger) PrintError(err error) { - logger.Printf(GetErrorFormatter()+"\n", err) -} - -func (logger *errorLogger) PrintOnError(err error) { - if err != nil { - logger.PrintError(err) - } -} diff --git a/json_writer.go b/json_writer.go new file mode 100644 index 0000000..4362562 --- /dev/null +++ b/json_writer.go @@ -0,0 +1,28 @@ +package tracelog + +import ( + "encoding/json" + "fmt" + "io" + "sync" +) + +type jsonWriter struct { + lock sync.Mutex + out io.Writer +} + +func (jsonWriter *jsonWriter) Log(fields Fields) { + jsonWriter.lock.Lock() + defer jsonWriter.lock.Unlock() + data, err := json.MarshalIndent(fields, "", " ") + if err == nil { + _, _ = fmt.Fprint(jsonWriter.out, string(data)) + } +} + +func NewJsonWriter(out io.Writer) LoggerWriter { + return &jsonWriter{ + out: out, + } +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..64e43aa --- /dev/null +++ b/logger.go @@ -0,0 +1,127 @@ +package tracelog + +import ( + "fmt" + "os" +) + +type LoggerType string + +const ( + InfoLoggerType LoggerType = "INFO" + WarningLoggerType LoggerType = "WARNING" + ErrorLoggerType LoggerType = "ERROR" + DebugLoggerType LoggerType = "DEBUG" +) + +type Fields map[string]interface{} +type FieldValues func() Fields + +type Logger struct { + loggerWriter LoggerWriter + fieldValues FieldValues +} + +func NewLogger(fieldValues FieldValues, loggerWriter LoggerWriter) *Logger { + return &Logger{ + loggerWriter: loggerWriter, + fieldValues: fieldValues, + } +} + +func (logger *Logger) Log(v ...interface{}) { + fields := logger.fieldValues() + fields["message"] = fmt.Sprint(v...) + logger.loggerWriter.Log(fields) +} + +func (logger *Logger) Logf(format string, v ...interface{}) { + logger.Log(fmt.Sprintf(format, v...)) +} + +func (logger *Logger) Fatalln(v ...interface{}) { + logger.Log(fmt.Sprintln(v...)) + os.Exit(1) +} + +func (logger *Logger) Fatalf(format string, v ...interface{}) { + logger.Logf(format, v...) + os.Exit(1) +} + +func (logger *Logger) Fatal(v ...interface{}) { + logger.Log(v...) + os.Exit(1) +} + +func (logger *Logger) Panicln(v ...interface{}) { + s := fmt.Sprintln(v...) + logger.Log(s) + panic(s) +} + +func (logger *Logger) Panicf(format string, v ...interface{}) { + s := fmt.Sprintf(format, v...) + logger.Log(s) + panic(s) +} + +func (logger *Logger) Panic(v ...interface{}) { + s := fmt.Sprint(v...) + logger.Log(s) + panic(s) +} + +func (logger *Logger) Println(v ...interface{}) { + logger.Log(fmt.Sprintln(v...)) +} + +func (logger *Logger) Printf(format string, v ...interface{}) { + logger.Logf(format, v...) +} + +func (logger *Logger) Print(v ...interface{}) { + logger.Log(v...) +} + +func (logger *Logger) PanicError(err error) { + logger.Panic(err) +} + +func (logger *Logger) PanicfOnError(format string, err error) { + if err != nil { + logger.Panicf(format, err) + } +} + +func (logger *Logger) PanicOnError(err error) { + if err != nil { + logger.PanicError(err) + } +} + +func (logger *Logger) FatalError(err error) { + logger.Fatal(err) +} + +func (logger *Logger) FatalfOnError(format string, err error) { + if err != nil { + logger.Fatalf(format, err) + } +} + +func (logger *Logger) FatalOnError(err error) { + if err != nil { + logger.FatalError(err) + } +} + +func (logger *Logger) PrintError(err error) { + logger.Println(err) +} + +func (logger *Logger) PrintOnError(err error) { + if err != nil { + logger.PrintError(err) + } +} diff --git a/logger_test.go b/logger_test.go new file mode 100644 index 0000000..ea47e66 --- /dev/null +++ b/logger_test.go @@ -0,0 +1,153 @@ +package tracelog + +import ( + "bytes" + "fmt" + "os" + "testing" + "time" +) + +var TestBasicFormat = "%s [%d] %s: %s" +var TestBasicFields = []string{"time", "pid", "level", "message"} + +func GetFieldValuesForTest(loggerType LoggerType) func() Fields { + return func() Fields { + now := time.Now().UTC() + fields := Fields{ + "time": now.Format("2006-01-02 03:04:05 UTC"), + "pid": os.Getpid(), + "level": loggerType, + } + + return fields + } +} + +func TestLogger_PrintWith(t *testing.T) { + message := "test" + logLevel := InfoLoggerType + now := time.Now().UTC().Format("2006-01-02 03:04:05 UTC") + buffer := bytes.Buffer{} + tests := []struct { + name string + input string + want string + infoLogger *Logger + }{ + { + "TextWriter", + message, + fmt.Sprintf("%s [%d] %s: %s", now, os.Getpid(), logLevel, message), + NewLogger(GetFieldValuesForTest(logLevel), NewTextWriter(&buffer, TestBasicFormat, TestBasicFields)), + }, + { + "JsonWriter", + message, + fmt.Sprintf("{\n \"level\": \"%s\",\n \"message\": \"%s\",\n \"pid\": %d,\n \"time\": \"%s\"\n}", logLevel, message, os.Getpid(), now), + NewLogger(GetFieldValuesForTest(logLevel), NewJsonWriter(&buffer)), + }, + { + "CsvWriter", + message, + fmt.Sprintf("%s,%d,%s,%s\n", now, os.Getpid(), logLevel, message), + NewLogger(GetFieldValuesForTest(logLevel), NewCsvWriter(&buffer, TestBasicFields)), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.infoLogger.Print(tt.input) + if tt.want != buffer.String() { + t.Errorf("\\nOutput should be:\\n %s, \\ngot: \\n%s\"", tt.want, buffer.String()) + } + buffer.Reset() + }) + } +} + +func TestLogger_PrintlnWith(t *testing.T) { + message := "test" + logLevel := InfoLoggerType + now := time.Now().UTC().Format("2006-01-02 03:04:05 UTC") + buffer := bytes.Buffer{} + tests := []struct { + name string + input string + want string + infoLogger *Logger + }{ + { + "TextWriter", + message, + fmt.Sprintf("%s [%d] %s: %s%s", now, os.Getpid(), logLevel, message, "\n"), + NewLogger(GetFieldValuesForTest(logLevel), NewTextWriter(&buffer, TestBasicFormat, TestBasicFields)), + }, + { + "JsonWriter", + message, + fmt.Sprintf("{\n \"level\": \"%s\",\n \"message\": \"%s\\n\",\n \"pid\": %d,\n \"time\": \"%s\"\n}", logLevel, message, os.Getpid(), now), + NewLogger(GetFieldValuesForTest(logLevel), NewJsonWriter(&buffer)), + }, + { + "CsvWriter", + message, + fmt.Sprintf("%s,%d,%s,\"%s\n\"\n", now, os.Getpid(), logLevel, message), + NewLogger(GetFieldValuesForTest(logLevel), NewCsvWriter(&buffer, TestBasicFields)), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.infoLogger.Println(tt.input) + if tt.want != buffer.String() { + t.Errorf("\\nOutput should be:\\n %s, \\ngot: \\n%s\"", tt.want, buffer.String()) + } + buffer.Reset() + }) + } +} + +func TestLogger_PrintfWith(t *testing.T) { + format := "%s %s" + messages := []string{"kek", "test"} + logLevel := InfoLoggerType + now := time.Now().UTC().Format("2006-01-02 03:04:05 UTC") + buffer := bytes.Buffer{} + tests := []struct { + name string + format string + input []string + want string + infoLogger *Logger + }{ + { + "TextWriter", + format, + messages, + fmt.Sprintf("%s [%d] %s: %s %s", now, os.Getpid(), logLevel, "kek", "test"), + NewLogger(GetFieldValuesForTest(logLevel), NewTextWriter(&buffer, TestBasicFormat, TestBasicFields)), + }, + { + "JsonWriter", + format, + messages, + fmt.Sprintf("{\n \"level\": \"%s\",\n \"message\": \"%s\",\n \"pid\": %d,\n \"time\": \"%s\"\n}", logLevel, "kek test", os.Getpid(), now), + NewLogger(GetFieldValuesForTest(logLevel), NewJsonWriter(&buffer)), + }, + { + "CsvWriter", + format, + messages, + fmt.Sprintf("%s,%d,%s,%s\n", now, os.Getpid(), logLevel, "kek test"), + NewLogger(GetFieldValuesForTest(logLevel), NewCsvWriter(&buffer, TestBasicFields)), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.infoLogger.Printf(tt.format, tt.input[0], tt.input[1]) + if tt.want != buffer.String() { + t.Errorf("\\nOutput should be:\\n %s, \\ngot: \\n%s\"", tt.want, buffer.String()) + } + buffer.Reset() + }) + } +} diff --git a/logger_writer.go b/logger_writer.go new file mode 100644 index 0000000..ef11c67 --- /dev/null +++ b/logger_writer.go @@ -0,0 +1,5 @@ +package tracelog + +type LoggerWriter interface { + Log(Fields) +} diff --git a/logging.go b/logging.go index 74b8290..c966ae7 100644 --- a/logging.go +++ b/logging.go @@ -2,24 +2,30 @@ package tracelog import ( "fmt" + "github.com/pkg/errors" + "io" "io/ioutil" - "log" "os" - - "github.com/pkg/errors" + "strings" ) const ( NormalLogLevel = "NORMAL" DevelLogLevel = "DEVEL" ErrorLogLevel = "ERROR" - timeFlags = log.LstdFlags | log.Lmicroseconds ) -var InfoLogger = NewErrorLogger(os.Stdout, "INFO: ") -var WarningLogger = NewErrorLogger(os.Stdout, "WARNING: ") -var ErrorLogger = NewErrorLogger(os.Stderr, "ERROR: ") -var DebugLogger = NewErrorLogger(ioutil.Discard, "DEBUG: ") +const ( + CsvPg = "CSV PG" + JsonPg = "JSON PG" + TextPg = "TEXT PG" + TextWalg = "TEXT WALG" +) + +var InfoLogger = NewLogger(GetFieldValuesForWalg(InfoLoggerType), WalgDefaultWriter) +var WarningLogger = NewLogger(GetFieldValuesForWalg(WarningLoggerType), WalgDefaultWriter) +var ErrorLogger = NewLogger(GetFieldValuesForWalg(ErrorLoggerType), WalgDefaultWriter) +var DebugLogger = NewLogger(GetFieldValuesForWalg(DebugLoggerType), NewTextWriter(ioutil.Discard, WalgTextFormatForDebug, WalgTextFormatFields)) var LogLevels = []string{NormalLogLevel, DevelLogLevel, ErrorLogLevel} var logLevel = NormalLogLevel @@ -29,15 +35,40 @@ var logLevelFormatters = map[string]string{ DevelLogLevel: "%+v", } -func setupLoggers() { +var LogWriters = []string{CsvPg, JsonPg, TextPg, TextWalg} +var logWriter = TextWalg + +type GetWriter func(out io.Writer) LoggerWriter +type GetFieldValues func(loggerType LoggerType) func() Fields + +func setupLoggersDependingOnWriter() { + if logWriter == TextWalg { + setupLoggers(GetWalgWriter, GetFieldValuesForWalg) + } else if logWriter == JsonPg { + setupLoggers(NewJsonWriter, GetFieldValuesForPg) + } else if logWriter == CsvPg { + setupLoggers(GetPgCsvWriter, GetFieldValuesForPg) + } else if logWriter == TextPg { + setupLoggers(GetPgTextWriter, GetFieldValuesForPg) + } +} + +func setupLoggers(writer GetWriter, getFieldValues GetFieldValues) { if logLevel == NormalLogLevel { - DebugLogger = NewErrorLogger(ioutil.Discard, "DEBUG: ") + DebugLogger = NewLogger(getFieldValues(DebugLoggerType), writer(ioutil.Discard)) } else if logLevel == ErrorLogLevel { - DebugLogger = NewErrorLogger(ioutil.Discard, "DEBUG: ") - InfoLogger = NewErrorLogger(ioutil.Discard, "INFO: ") - WarningLogger = NewErrorLogger(ioutil.Discard, "WARNING: ") + ErrorLogger = NewLogger(getFieldValues(ErrorLoggerType), writer(os.Stderr)) + DebugLogger = NewLogger(getFieldValues(DebugLoggerType), writer(ioutil.Discard)) + InfoLogger = NewLogger(getFieldValues(InfoLoggerType), writer(ioutil.Discard)) + WarningLogger = NewLogger(getFieldValues(WarningLoggerType), writer(ioutil.Discard)) } else { - DebugLogger = NewErrorLogger(os.Stdout, "DEBUG: ") + DebugLogger = NewLogger(getFieldValues(DebugLoggerType), writer(os.Stdout)) + } + + if logLevel == NormalLogLevel || logLevel == DevelLogLevel { + InfoLogger = NewLogger(getFieldValues(InfoLoggerType), writer(os.Stderr)) + WarningLogger = NewLogger(getFieldValues(WarningLoggerType), writer(os.Stderr)) + ErrorLogger = NewLogger(getFieldValues(ErrorLoggerType), writer(os.Stderr)) } } @@ -45,8 +76,16 @@ type LogLevelError struct { error } -func NewLogLevelError() LogLevelError { - return LogLevelError{errors.Errorf("got incorrect log level: '%s', expected one of: '%v'", logLevel, LogLevels)} +func NewLogLevelError(incorrectLogLevel string) LogLevelError { + return LogLevelError{errors.Errorf("got incorrect log level: '%s', expected one of: '%v'", incorrectLogLevel, strings.Join(LogLevels[:], ", "))} +} + +type LogWriterError struct { + error +} + +func NewLogWriterError(incorrectLogWriter string) LogWriterError { + return LogWriterError{errors.Errorf("got incorrect log writer: '%s', expected one of: '%v'", incorrectLogWriter, strings.Join(LogWriters[:], ", "))} } func (err LogLevelError) Error() string { @@ -65,10 +104,26 @@ func UpdateLogLevel(newLevel string) error { } } if !isCorrect { - return NewLogLevelError() + return NewLogLevelError(newLevel) } logLevel = newLevel - setupLoggers() + setupLoggersDependingOnWriter() + return nil +} + +func UpdateLogWriter(newWriter string) error { + isCorrect := false + for _, logWriter := range LogWriters { + if newWriter == logWriter { + isCorrect = true + } + } + if !isCorrect { + return NewLogWriterError(newWriter) + } + + logWriter = newWriter + setupLoggersDependingOnWriter() return nil } diff --git a/postgres_logger.go b/postgres_logger.go new file mode 100644 index 0000000..9e6d2c9 --- /dev/null +++ b/postgres_logger.go @@ -0,0 +1,31 @@ +package tracelog + +import ( + "io" + "os" + "time" +) + +var PgFormat = "%s [%d] %s: %s" +var PgFields = []string{"timestamp", "pid", "error_severity", "message"} + +func GetFieldValuesForPg(loggerType LoggerType) func() Fields { + return func() Fields { + now := time.Now().UTC() + fields := Fields{ + "timestamp": now.Format("2006-01-02 03:04:05.000 UTC"), + "pid": os.Getpid(), + "error_severity": loggerType, + } + + return fields + } +} + +func GetPgTextWriter(out io.Writer) LoggerWriter { + return NewTextWriter(out, PgFormat, PgFields) +} + +func GetPgCsvWriter(out io.Writer) LoggerWriter { + return NewCsvWriter(out, PgFields) +} diff --git a/text_writer.go b/text_writer.go new file mode 100644 index 0000000..cf8cbc6 --- /dev/null +++ b/text_writer.go @@ -0,0 +1,29 @@ +package tracelog + +import ( + "fmt" + "io" + "sync" +) + +type textWriter struct { + lock sync.Mutex + out io.Writer + format string + fields []string +} + +func (textWriter *textWriter) Log(fields Fields) { + vals := getValuesFromMap(fields, textWriter.fields) + textWriter.lock.Lock() + defer textWriter.lock.Unlock() + _, _ = fmt.Fprintf(textWriter.out, textWriter.format, vals...) +} + +func NewTextWriter(out io.Writer, format string, fields []string) LoggerWriter { + return &textWriter{ + out: out, + format: format, + fields: fields, + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..a8534fa --- /dev/null +++ b/utils.go @@ -0,0 +1,14 @@ +package tracelog + +func getValuesFromMap(fieldsMap Fields, fields []string) []interface{} { + vals := make([]interface{}, len(fields)) + for i, field := range fields { + var ok bool + vals[i], ok = fieldsMap[field] + if !ok { + vals[i] = "???" + } + } + + return vals +} diff --git a/walg_logger.go b/walg_logger.go new file mode 100644 index 0000000..e275423 --- /dev/null +++ b/walg_logger.go @@ -0,0 +1,28 @@ +package tracelog + +import ( + "io" + "os" + "time" +) + +var WalgTextFormat = "%s: %s %v" +var WalgTextFormatForDebug = "%s: %s %+v" +var WalgTextFormatFields = []string{"level", "time", "message"} +var WalgDefaultWriter = NewTextWriter(os.Stderr, WalgTextFormat, WalgTextFormatFields) + +func GetFieldValuesForWalg(loggerType LoggerType) func() Fields { + return func() Fields { + now := time.Now() + fields := Fields{ + "level": loggerType, + "time": now.Format("2006/01/02 03:04:05.000000"), + } + + return fields + } +} + +func GetWalgWriter(out io.Writer) LoggerWriter { + return NewTextWriter(out, WalgTextFormat, WalgTextFormatFields) +}