feat(log): Handle multiple log formatter for file
This commit is contained in:
parent
4e49672758
commit
bef0d38f1e
@ -4,8 +4,10 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.dev.m-and-m.ovh/mderasse/gocommon/convert"
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/juju/errors"
|
"github.com/juju/errors"
|
||||||
)
|
)
|
||||||
@ -17,6 +19,7 @@ type ConfigStruct struct {
|
|||||||
RotateTime *string `yaml:"rotate_time"`
|
RotateTime *string `yaml:"rotate_time"`
|
||||||
RotateMaxAge *string `yaml:"rotate_max_age"`
|
RotateMaxAge *string `yaml:"rotate_max_age"`
|
||||||
RotateMaxFile *int `yaml:"rotate_max_file"`
|
RotateMaxFile *int `yaml:"rotate_max_file"`
|
||||||
|
OutputFormat *string `yaml:"output_format"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid will check that the File configuration is valid.
|
// 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")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
34
log/hooks/file/enum.go
Normal file
34
log/hooks/file/enum.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,9 @@
|
|||||||
package file
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -21,15 +19,18 @@ var BufSize uint = 8192
|
|||||||
// Hook will write logs to a file.
|
// Hook will write logs to a file.
|
||||||
type Hook struct {
|
type Hook struct {
|
||||||
Level logrus.Level
|
Level logrus.Level
|
||||||
w io.Writer
|
|
||||||
buf chan logrus.Entry
|
buf chan logrus.Entry
|
||||||
wg sync.WaitGroup
|
f logrus.Formatter
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
synchronous bool
|
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.
|
// 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 {
|
if w == nil {
|
||||||
logrus.Error("Can't create File Hook with an empty writer")
|
logrus.Error("Can't create File Hook with an empty writer")
|
||||||
return nil
|
return nil
|
||||||
@ -38,6 +39,7 @@ func NewFileHook(w io.Writer) *Hook {
|
|||||||
hook := &Hook{
|
hook := &Hook{
|
||||||
Level: logrus.DebugLevel,
|
Level: logrus.DebugLevel,
|
||||||
synchronous: true,
|
synchronous: true,
|
||||||
|
f: handleFormat(of),
|
||||||
w: w,
|
w: w,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +49,7 @@ func NewFileHook(w io.Writer) *Hook {
|
|||||||
// NewAsyncFileHook creates a hook to be added to an instance of logger.
|
// 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
|
// 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.
|
// before exiting to empty the log queue.
|
||||||
func NewAsyncFileHook(w io.Writer) *Hook {
|
func NewAsyncFileHook(w io.Writer, of OutputFormat) *Hook {
|
||||||
if w == nil {
|
if w == nil {
|
||||||
logrus.Error("Can't create File Hook with an empty writer")
|
logrus.Error("Can't create File Hook with an empty writer")
|
||||||
return nil
|
return nil
|
||||||
@ -56,8 +58,10 @@ func NewAsyncFileHook(w io.Writer) *Hook {
|
|||||||
hook := &Hook{
|
hook := &Hook{
|
||||||
Level: logrus.DebugLevel,
|
Level: logrus.DebugLevel,
|
||||||
buf: make(chan logrus.Entry, BufSize),
|
buf: make(chan logrus.Entry, BufSize),
|
||||||
|
f: handleFormat(of),
|
||||||
w: w,
|
w: w,
|
||||||
}
|
}
|
||||||
|
|
||||||
go hook.fire() // Log in background
|
go hook.fire() // Log in background
|
||||||
|
|
||||||
return hook
|
return hook
|
||||||
@ -76,6 +80,23 @@ func (hook *Hook) Flush() {
|
|||||||
hook.wg.Wait()
|
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.
|
// Fire is called when a log event is fired.
|
||||||
// We assume the entry will be altered by another hook,
|
// We assume the entry will be altered by another hook,
|
||||||
// otherwise we might be logging something wrong to Graylog.
|
// otherwise we might be logging something wrong to Graylog.
|
||||||
@ -109,8 +130,13 @@ func (hook *Hook) writeEntry(entry *logrus.Entry) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
message := constructMessage(entry)
|
message, err := hook.f.Format(entry)
|
||||||
_, err := hook.w.Write([]byte(fmt.Sprintf("%s\n", message)))
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to write in log file. Error: %s\nLog Line: %s", err, message)
|
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
|
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
|
|
||||||
}
|
|
||||||
|
@ -21,6 +21,11 @@ func NewHook(c *ConfigStruct) (logrus.Hook, error) {
|
|||||||
return nil, errors.Trace(err)
|
return nil, errors.Trace(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outputFormat := OutputFormat_TEXT
|
||||||
|
if c.OutputFormat != nil {
|
||||||
|
outputFormat = OutputFormat(*c.OutputFormat)
|
||||||
|
}
|
||||||
|
|
||||||
var w io.Writer
|
var w io.Writer
|
||||||
if c.Rotate {
|
if c.Rotate {
|
||||||
rotateTime, _ := time.ParseDuration(*c.RotateTime)
|
rotateTime, _ := time.ParseDuration(*c.RotateTime)
|
||||||
@ -65,7 +70,7 @@ func NewHook(c *ConfigStruct) (logrus.Hook, error) {
|
|||||||
w = fh
|
w = fh
|
||||||
}
|
}
|
||||||
|
|
||||||
h := NewAsyncFileHook(w)
|
h := NewAsyncFileHook(w, outputFormat)
|
||||||
|
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
36
log/log.go
36
log/log.go
@ -3,7 +3,6 @@ package log
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/juju/errors"
|
"github.com/juju/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -13,43 +12,43 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// InitLog will try to initialize logger by trying to retrieve config from multiple source.
|
// InitLog will try to initialize logger by trying to retrieve config from multiple source.
|
||||||
func InitLog() error {
|
func InitLog() (*logrus.Logger, error) {
|
||||||
|
|
||||||
// loading configuration
|
// loading configuration
|
||||||
c, err := loadConfig()
|
c, err := loadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Trace(err)
|
return nil, errors.Trace(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return initFromSource(c)
|
return initFromSource(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitLogFromCustomVaultSecret will initialize logger with a vault secret.
|
// InitLogFromCustomVaultSecret will initialize logger with a vault secret.
|
||||||
func InitLogFromCustomVaultSecret(secret string) error {
|
func InitLogFromCustomVaultSecret(secret string) (*logrus.Logger, error) {
|
||||||
|
|
||||||
c, err := loadConfigFromVault(secret)
|
c, err := loadConfigFromVault(secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Trace(err)
|
return nil, errors.Trace(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return initFromSource(c)
|
return initFromSource(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitLogFromCustomFile will initialize logger with a config file.
|
// InitLogFromCustomFile will initialize logger with a config file.
|
||||||
func InitLogFromCustomFile(path string) error {
|
func InitLogFromCustomFile(path string) (*logrus.Logger, error) {
|
||||||
|
|
||||||
c, err := loadConfigFromFile(path)
|
c, err := loadConfigFromFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Trace(err)
|
return nil, errors.Trace(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return initFromSource(c)
|
return initFromSource(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initFromSource(c *ConfigStruct) error {
|
func initFromSource(c *ConfigStruct) (*logrus.Logger, error) {
|
||||||
err := c.applyEnv()
|
err := c.applyEnv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Trace(err)
|
return nil, errors.Trace(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.applyDefault()
|
c.applyDefault()
|
||||||
@ -58,16 +57,16 @@ func initFromSource(c *ConfigStruct) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// InitLogFromCustomConfig will initialize logger from a gaven config.
|
// InitLogFromCustomConfig will initialize logger from a gaven config.
|
||||||
func InitLogFromCustomConfig(c *ConfigStruct) error {
|
func InitLogFromCustomConfig(c *ConfigStruct) (*logrus.Logger, error) {
|
||||||
|
|
||||||
err := c.IsValid()
|
err := c.IsValid()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Trace(err)
|
return nil, errors.Trace(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
level, err := logrus.ParseLevel(*c.Level)
|
level, err := logrus.ParseLevel(*c.Level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// init logger
|
// init logger
|
||||||
@ -84,23 +83,20 @@ func InitLogFromCustomConfig(c *ConfigStruct) error {
|
|||||||
case ProviderName_FILE:
|
case ProviderName_FILE:
|
||||||
hook, err := file.NewHook(c.FileConfig)
|
hook, err := file.NewHook(c.FileConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Trace(err)
|
return nil, errors.Trace(err)
|
||||||
}
|
}
|
||||||
log.AddHook(hook)
|
log.AddHook(hook)
|
||||||
case ProviderName_GELF:
|
case ProviderName_GELF:
|
||||||
hook, err := gelf.NewHook(c.GelfConfig)
|
hook, err := gelf.NewHook(c.GelfConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Trace(err)
|
return nil, errors.Trace(err)
|
||||||
}
|
}
|
||||||
log.AddHook(hook)
|
log.AddHook(hook)
|
||||||
case ProviderName_NONE:
|
case ProviderName_NONE:
|
||||||
// Do Nothing
|
fallthrough
|
||||||
default:
|
default:
|
||||||
|
return nil, errors.BadRequestf("Provider is not handled.")
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 500; i++ {
|
return log, nil
|
||||||
log.Infof("Test %d", i)
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user