feat(log): Handle multiple log formatter for file
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing

This commit is contained in:
Matthieu 'JP' DERASSE 2022-12-10 14:09:00 +00:00
parent 4e49672758
commit bef0d38f1e
Signed by: mderasse
GPG Key ID: 55141C777B16A705
5 changed files with 98 additions and 69 deletions

View File

@ -4,8 +4,10 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"git.dev.m-and-m.ovh/mderasse/gocommon/convert"
"github.com/asaskevich/govalidator"
"github.com/juju/errors"
)
@ -17,6 +19,7 @@ type ConfigStruct struct {
RotateTime *string `yaml:"rotate_time"`
RotateMaxAge *string `yaml:"rotate_max_age"`
RotateMaxFile *int `yaml:"rotate_max_file"`
OutputFormat *string `yaml:"output_format"`
}
// IsValid will check that the File configuration is valid.
@ -64,5 +67,10 @@ func (c *ConfigStruct) IsValid() error {
return errors.NotValidf("rotate_max_age and rotate_max_file cannot be defined at the same time in File configuration")
}
}
if c.OutputFormat != nil && !OutputFormat(*c.OutputFormat).IsValid() {
return errors.NotValidf("output_format is invalid in File configuration. (Possible Values:Allowed values: %s", strings.Join(convert.StringerSliceToStringSlice(GetListOutputFormat()), ", "))
}
return nil
}

34
log/hooks/file/enum.go Normal file
View File

@ -0,0 +1,34 @@
package file
// OutputFormat Enum
// OutputFormat is the format that will be use to store log in the file.
type OutputFormat string
//nolint:exported // keeping the enum simple and readable.
const (
OutputFormat_TEXT OutputFormat = "TEXT"
OutputFormat_JSON OutputFormat = "JSON"
)
// IsValid check if the gaven OutputFormat is part of the list of handled provider name.
func (e OutputFormat) IsValid() bool {
for _, v := range GetListOutputFormat() {
if e == v {
return true
}
}
return false
}
func (e OutputFormat) String() string {
return string(e)
}
// GetListOutputFormat return a the list of possible OutputFormat.
func GetListOutputFormat() []OutputFormat {
return []OutputFormat{
OutputFormat_TEXT,
OutputFormat_JSON,
}
}

View File

@ -1,11 +1,9 @@
package file
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
@ -21,15 +19,18 @@ var BufSize uint = 8192
// Hook will write logs to a file.
type Hook struct {
Level logrus.Level
w io.Writer
buf chan logrus.Entry
wg sync.WaitGroup
f logrus.Formatter
mu sync.RWMutex
synchronous bool
w io.Writer
wg sync.WaitGroup
}
// XXX: Maybe just take a formatter in input
// NewFileHook creates a hook to be added to an instance of logger.
func NewFileHook(w io.Writer) *Hook {
func NewFileHook(w io.Writer, of OutputFormat) *Hook {
if w == nil {
logrus.Error("Can't create File Hook with an empty writer")
return nil
@ -38,6 +39,7 @@ func NewFileHook(w io.Writer) *Hook {
hook := &Hook{
Level: logrus.DebugLevel,
synchronous: true,
f: handleFormat(of),
w: w,
}
@ -47,7 +49,7 @@ func NewFileHook(w io.Writer) *Hook {
// NewAsyncFileHook creates a hook to be added to an instance of logger.
// The hook created will be asynchronous, and it's the responsibility of the user to call the Flush method
// before exiting to empty the log queue.
func NewAsyncFileHook(w io.Writer) *Hook {
func NewAsyncFileHook(w io.Writer, of OutputFormat) *Hook {
if w == nil {
logrus.Error("Can't create File Hook with an empty writer")
return nil
@ -56,8 +58,10 @@ func NewAsyncFileHook(w io.Writer) *Hook {
hook := &Hook{
Level: logrus.DebugLevel,
buf: make(chan logrus.Entry, BufSize),
f: handleFormat(of),
w: w,
}
go hook.fire() // Log in background
return hook
@ -76,6 +80,23 @@ func (hook *Hook) Flush() {
hook.wg.Wait()
}
// handleFormat will take a OutputFormat and will transform it in a formatter.
func handleFormat(of OutputFormat) logrus.Formatter {
if of == OutputFormat_JSON {
return &logrus.JSONFormatter{
PrettyPrint: false,
TimestampFormat: time.RFC3339Nano,
}
}
return &logrus.TextFormatter{
DisableColors: true,
TimestampFormat: time.RFC3339Nano,
QuoteEmptyFields: true,
}
}
// Fire is called when a log event is fired.
// We assume the entry will be altered by another hook,
// otherwise we might be logging something wrong to Graylog.
@ -109,8 +130,13 @@ func (hook *Hook) writeEntry(entry *logrus.Entry) {
return
}
message := constructMessage(entry)
_, err := hook.w.Write([]byte(fmt.Sprintf("%s\n", message)))
message, err := hook.f.Format(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to format log line. Error: %s\n", err)
return
}
_, err = hook.w.Write(message)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write in log file. Error: %s\nLog Line: %s", err, message)
}
@ -126,43 +152,3 @@ func (hook *Hook) Levels() []logrus.Level {
}
return levels
}
func constructMessage(entry *logrus.Entry) string {
var messageParts []string
// handle time
messageParts = append(messageParts, fmt.Sprintf("time=\"%s\"", entry.Time.UTC().Format(time.RFC3339)))
// handle log level
messageParts = append(messageParts, fmt.Sprintf("level=%s", entry.Level.String()))
// handle actual message
messageParts = append(messageParts, fmt.Sprintf("msg=\"%s\"", string(bytes.TrimSpace([]byte(entry.Message)))))
// add extra data except err_*
for k, v := range entry.Data {
if !strings.HasPrefix(k, "err_") {
messageParts = append(messageParts, fmt.Sprintf("%s=\"%s\"", k, v))
}
}
message := strings.Join(messageParts, " ")
// handle error stack
// XXX: Is that really how it work ?? I doubt but let see
if _, exist := entry.Data["err_code"]; exist {
// get error infos
//ns := entry.Data["err_ns"]
ctx := entry.Data["err_ctx"]
//id := entry.Data["err_id"]
// get stack and clean it a bit
tSt := entry.Data["err_stack"]
st := strings.Replace(tSt.(string), "\n", "\n\t\t", -1)
message = fmt.Sprintf("%s\nerror:\n\tcontext:%s\n\tstacktrace:\n\t\t%s", message, ctx, st)
}
return message
}

View File

@ -21,6 +21,11 @@ func NewHook(c *ConfigStruct) (logrus.Hook, error) {
return nil, errors.Trace(err)
}
outputFormat := OutputFormat_TEXT
if c.OutputFormat != nil {
outputFormat = OutputFormat(*c.OutputFormat)
}
var w io.Writer
if c.Rotate {
rotateTime, _ := time.ParseDuration(*c.RotateTime)
@ -65,7 +70,7 @@ func NewHook(c *ConfigStruct) (logrus.Hook, error) {
w = fh
}
h := NewAsyncFileHook(w)
h := NewAsyncFileHook(w, outputFormat)
return h, nil
}

View File

@ -3,7 +3,6 @@ package log
import (
"io"
"os"
"time"
"github.com/juju/errors"
"github.com/sirupsen/logrus"
@ -13,43 +12,43 @@ import (
)
// InitLog will try to initialize logger by trying to retrieve config from multiple source.
func InitLog() error {
func InitLog() (*logrus.Logger, error) {
// loading configuration
c, err := loadConfig()
if err != nil {
return errors.Trace(err)
return nil, errors.Trace(err)
}
return initFromSource(c)
}
// InitLogFromCustomVaultSecret will initialize logger with a vault secret.
func InitLogFromCustomVaultSecret(secret string) error {
func InitLogFromCustomVaultSecret(secret string) (*logrus.Logger, error) {
c, err := loadConfigFromVault(secret)
if err != nil {
return errors.Trace(err)
return nil, errors.Trace(err)
}
return initFromSource(c)
}
// InitLogFromCustomFile will initialize logger with a config file.
func InitLogFromCustomFile(path string) error {
func InitLogFromCustomFile(path string) (*logrus.Logger, error) {
c, err := loadConfigFromFile(path)
if err != nil {
return errors.Trace(err)
return nil, errors.Trace(err)
}
return initFromSource(c)
}
func initFromSource(c *ConfigStruct) error {
func initFromSource(c *ConfigStruct) (*logrus.Logger, error) {
err := c.applyEnv()
if err != nil {
return errors.Trace(err)
return nil, errors.Trace(err)
}
c.applyDefault()
@ -58,16 +57,16 @@ func initFromSource(c *ConfigStruct) error {
}
// InitLogFromCustomConfig will initialize logger from a gaven config.
func InitLogFromCustomConfig(c *ConfigStruct) error {
func InitLogFromCustomConfig(c *ConfigStruct) (*logrus.Logger, error) {
err := c.IsValid()
if err != nil {
return errors.Trace(err)
return nil, errors.Trace(err)
}
level, err := logrus.ParseLevel(*c.Level)
if err != nil {
return err
return nil, err
}
// init logger
@ -84,23 +83,20 @@ func InitLogFromCustomConfig(c *ConfigStruct) error {
case ProviderName_FILE:
hook, err := file.NewHook(c.FileConfig)
if err != nil {
return errors.Trace(err)
return nil, errors.Trace(err)
}
log.AddHook(hook)
case ProviderName_GELF:
hook, err := gelf.NewHook(c.GelfConfig)
if err != nil {
return errors.Trace(err)
return nil, errors.Trace(err)
}
log.AddHook(hook)
case ProviderName_NONE:
// Do Nothing
fallthrough
default:
return nil, errors.BadRequestf("Provider is not handled.")
}
for i := 0; i < 500; i++ {
log.Infof("Test %d", i)
time.Sleep(1 * time.Second)
}
return nil
return log, nil
}