diff --git a/cmd/config_logging.go b/cmd/config_logging.go index 9579778..92cae32 100644 --- a/cmd/config_logging.go +++ b/cmd/config_logging.go @@ -12,6 +12,10 @@ type LoggingConfig struct { Outlets logger.Outlets } +type SetNoMetadataFormatter interface { + SetNoMetadata(noMetadata bool) +} + func parseLogging(i interface{}) (c *LoggingConfig, err error) { c = &LoggingConfig{} @@ -31,6 +35,11 @@ func parseLogging(i interface{}) (c *LoggingConfig, err error) { Address string RetryInterval string `mapstructure:"retry_interval"` } + Syslog struct { + Enable bool + Format string + RetryInterval string `mapstructure:"retry_interval"` + } } if err = mapstructure.Decode(i, &asMap); err != nil { return nil, errors.Wrap(err, "mapstructure error") @@ -41,19 +50,19 @@ func parseLogging(i interface{}) (c *LoggingConfig, err error) { if asMap.Stdout.Level != "" { out := WriterOutlet{ - HumanFormatter{}, + &HumanFormatter{}, os.Stdout, } level, err := logger.ParseLevel(asMap.Stdout.Level) if err != nil { - return nil, errors.Wrap(err, "cannot parse stdout log level") + return nil, errors.Wrap(err, "cannot parse 'level'") } if asMap.Stdout.Format != "" { out.Formatter, err = parseLogFormat(asMap.Stdout.Format) if err != nil { - return nil, errors.Wrap(err, "cannot parse log format") + return nil, errors.Wrap(err, "cannot parse 'format'") } } @@ -67,7 +76,7 @@ func parseLogging(i interface{}) (c *LoggingConfig, err error) { out.Formatter, err = parseLogFormat(asMap.TCP.Format) if err != nil { - return nil, errors.Wrap(err, "cannot parse log 'format'") + return nil, errors.Wrap(err, "cannot parse 'format'") } lvl, err := logger.ParseLevel(asMap.TCP.Level) @@ -85,6 +94,34 @@ func parseLogging(i interface{}) (c *LoggingConfig, err error) { c.Outlets.Add(out, lvl) } + if asMap.Syslog.Enable { + + out := &SyslogOutlet{} + + out.Formatter = &HumanFormatter{} + if asMap.Syslog.Format != "" { + out.Formatter, err = parseLogFormat(asMap.Syslog.Format) + if err != nil { + return nil, errors.Wrap(err, "cannot parse 'format'") + } + } + + if f, ok := out.Formatter.(SetNoMetadataFormatter); ok { + f.SetNoMetadata(true) + } + + out.RetryInterval = 0 // default to 0 as we assume local syslog will just work + if asMap.Syslog.RetryInterval != "" { + out.RetryInterval, err = time.ParseDuration(asMap.Syslog.RetryInterval) + if err != nil { + return nil, errors.Wrap(err, "cannot parse 'retry_interval'") + } + } + + c.Outlets.Add(out, logger.Debug) + + } + return c, nil } @@ -100,7 +137,7 @@ func parseLogFormat(i interface{}) (f EntryFormatter, err error) { switch is { case "human": - return HumanFormatter{}, nil + return &HumanFormatter{}, nil case "json": return &JSONFormatter{}, nil default: diff --git a/cmd/logging_formatters.go b/cmd/logging_formatters.go index 23f29be..3f0bf16 100644 --- a/cmd/logging_formatters.go +++ b/cmd/logging_formatters.go @@ -36,13 +36,23 @@ func (f NoFormatter) Format(e *logger.Entry) ([]byte, error) { return []byte(e.Message), nil } -type HumanFormatter struct{} +type HumanFormatter struct { + NoMetadata bool +} -func (f HumanFormatter) Format(e *logger.Entry) (out []byte, err error) { +var _ SetNoMetadataFormatter = &HumanFormatter{} + +func (f *HumanFormatter) SetNoMetadata(noMetadata bool) { + f.NoMetadata = noMetadata +} + +func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) { var line bytes.Buffer - fmt.Fprintf(&line, "[%s]", e.Level.Short()) + if !f.NoMetadata { + fmt.Fprintf(&line, "[%s]", e.Level.Short()) + } prefixFields := []string{logJobField, logTaskField, logFSField} prefixed := make(map[string]bool, len(prefixFields)+2) @@ -69,7 +79,10 @@ func (f HumanFormatter) Format(e *logger.Entry) (out []byte, err error) { prefixed[logIncFromField], prefixed[logIncToField] = true, true } - fmt.Fprintf(&line, ": %s", e.Message) + if line.Len() > 0 { + fmt.Fprint(&line, ": ") + } + fmt.Fprint(&line, e.Message) for field, value := range e.Fields { @@ -88,7 +101,7 @@ func (f HumanFormatter) Format(e *logger.Entry) (out []byte, err error) { type JSONFormatter struct{} -func (f JSONFormatter) Format(e *logger.Entry) ([]byte, error) { +func (f *JSONFormatter) Format(e *logger.Entry) ([]byte, error) { data := make(logger.Fields, len(e.Fields)+3) for k, v := range e.Fields { switch v := v.(type) { diff --git a/cmd/logging_outlets.go b/cmd/logging_outlets.go index e0d0d26..b329bca 100644 --- a/cmd/logging_outlets.go +++ b/cmd/logging_outlets.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" "github.com/zrepl/zrepl/logger" "io" + "log/syslog" "net" "time" ) @@ -61,3 +62,47 @@ func (h *TCPOutlet) WriteEntry(ctx context.Context, e logger.Entry) error { return nil } + +type SyslogOutlet struct { + Formatter EntryFormatter + RetryInterval time.Duration + writer *syslog.Writer + lastConnectAttempt time.Time +} + +func (o *SyslogOutlet) WriteEntry(ctx context.Context, entry logger.Entry) error { + + bytes, err := o.Formatter.Format(&entry) + if err != nil { + return err + } + + s := string(bytes) + + if o.writer == nil { + now := time.Now() + if now.Sub(o.lastConnectAttempt) < o.RetryInterval { + return nil // not an error toward logger + } + o.writer, err = syslog.New(syslog.LOG_LOCAL0, "zrepl") + o.lastConnectAttempt = time.Now() + if err != nil { + o.writer = nil + return err + } + } + + switch entry.Level { + case logger.Debug: + return o.writer.Debug(s) + case logger.Info: + return o.writer.Info(s) + case logger.Warn: + return o.writer.Warning(s) + case logger.Error: + return o.writer.Err(s) + default: + return o.writer.Err(s) // write as error as reaching this case is in fact an error + } + +} diff --git a/cmd/test.go b/cmd/test.go index aae9330..3a46b49 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -66,7 +66,7 @@ func init() { func testCmdGlobalInit(cmd *cobra.Command, args []string) { out := logger.NewOutlets() - out.Add(WriterOutlet{NoFormatter{}, os.Stdout}, logger.Info) + out.Add(WriterOutlet{&NoFormatter{}, os.Stdout}, logger.Info) log := logger.NewLogger(out, 1*time.Second) testCmdGlobal.log = log