add syslog outlet

This commit is contained in:
Christian Schwarz 2017-09-24 02:05:41 +02:00
parent e0e362c4ff
commit c4c38d5b23
4 changed files with 106 additions and 11 deletions

View File

@ -12,6 +12,10 @@ type LoggingConfig struct {
Outlets logger.Outlets Outlets logger.Outlets
} }
type SetNoMetadataFormatter interface {
SetNoMetadata(noMetadata bool)
}
func parseLogging(i interface{}) (c *LoggingConfig, err error) { func parseLogging(i interface{}) (c *LoggingConfig, err error) {
c = &LoggingConfig{} c = &LoggingConfig{}
@ -31,6 +35,11 @@ func parseLogging(i interface{}) (c *LoggingConfig, err error) {
Address string Address string
RetryInterval string `mapstructure:"retry_interval"` RetryInterval string `mapstructure:"retry_interval"`
} }
Syslog struct {
Enable bool
Format string
RetryInterval string `mapstructure:"retry_interval"`
}
} }
if err = mapstructure.Decode(i, &asMap); err != nil { if err = mapstructure.Decode(i, &asMap); err != nil {
return nil, errors.Wrap(err, "mapstructure error") return nil, errors.Wrap(err, "mapstructure error")
@ -41,19 +50,19 @@ func parseLogging(i interface{}) (c *LoggingConfig, err error) {
if asMap.Stdout.Level != "" { if asMap.Stdout.Level != "" {
out := WriterOutlet{ out := WriterOutlet{
HumanFormatter{}, &HumanFormatter{},
os.Stdout, os.Stdout,
} }
level, err := logger.ParseLevel(asMap.Stdout.Level) level, err := logger.ParseLevel(asMap.Stdout.Level)
if err != nil { 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 != "" { if asMap.Stdout.Format != "" {
out.Formatter, err = parseLogFormat(asMap.Stdout.Format) out.Formatter, err = parseLogFormat(asMap.Stdout.Format)
if err != nil { 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) out.Formatter, err = parseLogFormat(asMap.TCP.Format)
if err != nil { 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) lvl, err := logger.ParseLevel(asMap.TCP.Level)
@ -85,6 +94,34 @@ func parseLogging(i interface{}) (c *LoggingConfig, err error) {
c.Outlets.Add(out, lvl) 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 return c, nil
} }
@ -100,7 +137,7 @@ func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
switch is { switch is {
case "human": case "human":
return HumanFormatter{}, nil return &HumanFormatter{}, nil
case "json": case "json":
return &JSONFormatter{}, nil return &JSONFormatter{}, nil
default: default:

View File

@ -36,13 +36,23 @@ func (f NoFormatter) Format(e *logger.Entry) ([]byte, error) {
return []byte(e.Message), nil 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 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} prefixFields := []string{logJobField, logTaskField, logFSField}
prefixed := make(map[string]bool, len(prefixFields)+2) 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 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 { for field, value := range e.Fields {
@ -88,7 +101,7 @@ func (f HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {
type JSONFormatter struct{} 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) data := make(logger.Fields, len(e.Fields)+3)
for k, v := range e.Fields { for k, v := range e.Fields {
switch v := v.(type) { switch v := v.(type) {

View File

@ -5,6 +5,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
"io" "io"
"log/syslog"
"net" "net"
"time" "time"
) )
@ -61,3 +62,47 @@ func (h *TCPOutlet) WriteEntry(ctx context.Context, e logger.Entry) error {
return nil 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
}
}

View File

@ -66,7 +66,7 @@ func init() {
func testCmdGlobalInit(cmd *cobra.Command, args []string) { func testCmdGlobalInit(cmd *cobra.Command, args []string) {
out := logger.NewOutlets() 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) log := logger.NewLogger(out, 1*time.Second)
testCmdGlobal.log = log testCmdGlobal.log = log