diff --git a/cmd/config_logging.go b/cmd/config_logging.go index 169830e..dfd6679 100644 --- a/cmd/config_logging.go +++ b/cmd/config_logging.go @@ -19,149 +19,52 @@ type SetNoMetadataFormatter interface { SetNoMetadata(noMetadata bool) } +type OutletCommon struct { + MinLevel logger.Level + Formatter EntryFormatter +} + func parseLogging(i interface{}) (c *LoggingConfig, err error) { c = &LoggingConfig{} - if i == nil { - return c, nil - } - - var asMap struct { - Stdout struct { - Level string - Format string - } - TCP struct { - Level string - Format string - Net string - Address string - RetryInterval string `mapstructure:"retry_interval"` - TLS *struct { - CA string - Cert string - Key string - } - } - 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") - } - c.Outlets = logger.NewOutlets() - if asMap.Stdout.Level != "" { + var asList []interface{} + if err = mapstructure.Decode(i, &asList); err != nil { + return nil, errors.Wrap(err, "mapstructure error") + } + if len(asList) == 0 { + // Default config + out := WriterOutlet{&HumanFormatter{}, os.Stdout} + c.Outlets.Add(out, logger.Warn) + return + } - out := WriterOutlet{ - &HumanFormatter{}, - os.Stdout, - } + var syslogOutlets, stdoutOutlets int + for lei, le := range asList { - level, err := logger.ParseLevel(asMap.Stdout.Level) + outlet, minLevel, err := parseOutlet(le) if err != nil { - return nil, errors.Wrap(err, "cannot parse 'level'") + return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei) + } + var _ logger.Outlet = WriterOutlet{} + var _ logger.Outlet = &SyslogOutlet{} + switch outlet.(type) { + case *SyslogOutlet: + syslogOutlets++ + case WriterOutlet: + stdoutOutlets++ } - if asMap.Stdout.Format != "" { - out.Formatter, err = parseLogFormat(asMap.Stdout.Format) - if err != nil { - return nil, errors.Wrap(err, "cannot parse 'format'") - } - } - - c.Outlets.Add(out, level) + c.Outlets.Add(outlet, minLevel) } - if asMap.TCP.Address != "" { - - out := &TCPOutlet{} - - out.Formatter, err = parseLogFormat(asMap.TCP.Format) - if err != nil { - return nil, errors.Wrap(err, "cannot parse 'format'") - } - - lvl, err := logger.ParseLevel(asMap.TCP.Level) - if err != nil { - return nil, errors.Wrap(err, "cannot parse 'level'") - } - - out.RetryInterval, err = time.ParseDuration(asMap.TCP.RetryInterval) - if err != nil { - return nil, errors.Wrap(err, "cannot parse 'retry_interval'") - } - - out.Net, out.Address = asMap.TCP.Net, asMap.TCP.Address - - if asMap.TCP.TLS != nil { - - cert, err := tls.LoadX509KeyPair(asMap.TCP.TLS.Cert, asMap.TCP.TLS.Key) - if err != nil { - return nil, errors.Wrap(err, "cannot load client cert") - } - - var rootCAs *x509.CertPool - if asMap.TCP.TLS.CA == "" { - if rootCAs, err = x509.SystemCertPool(); err != nil { - return nil, errors.Wrap(err, "cannot open system cert pool") - } - } else { - rootCAs = x509.NewCertPool() - rootCAPEM, err := ioutil.ReadFile(asMap.TCP.TLS.CA) - if err != nil { - return nil, errors.Wrap(err, "cannot load CA cert") - } - if !rootCAs.AppendCertsFromPEM(rootCAPEM) { - return nil, errors.New("cannot parse CA cert") - } - } - if err != nil && asMap.TCP.TLS.CA == "" { - return nil, errors.Wrap(err, "cannot load root ca pool") - } - - out.TLS = &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: rootCAs, - } - - out.TLS.BuildNameToCertificate() - } - - c.Outlets.Add(out, lvl) + if syslogOutlets > 1 { + return nil, errors.Errorf("can only define one 'syslog' outlet") } - - 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) - + if stdoutOutlets > 1 { + return nil, errors.Errorf("can only define one 'stdout' outlet") } return c, nil @@ -189,3 +92,142 @@ func parseLogFormat(i interface{}) (f EntryFormatter, err error) { } } + +func parseOutlet(i interface{}) (o logger.Outlet, level logger.Level, err error) { + + var in struct { + Outlet string + Level string + Format string + } + if err = mapstructure.Decode(i, &in); err != nil { + err = errors.Wrap(err, "mapstructure error") + return + } + if in.Outlet == "" || in.Level == "" || in.Format == "" { + err = errors.Errorf("must specify 'outlet', 'level' and 'format' field") + return + } + + common := &OutletCommon{} + common.MinLevel, err = logger.ParseLevel(in.Level) + if err != nil { + err = errors.Wrap(err, "cannot parse 'level' field") + return + } + common.Formatter, err = parseLogFormat(in.Format) + if err != nil { + err = errors.Wrap(err, "cannot parse") + return + } + + switch in.Outlet { + case "stdout": + o, err = parseStdoutOutlet(i, common) + case "tcp": + o, err = parseTCPOutlet(i, common) + case "syslog": + o, err = parseSyslogOutlet(i, common) + default: + err = errors.Errorf("unknown outlet type '%s'", in.Outlet) + } + return o, common.MinLevel, err + +} + +func parseStdoutOutlet(i interface{}, common *OutletCommon) (WriterOutlet, error) { + return WriterOutlet{ + common.Formatter, + os.Stdout, + }, nil +} + +func parseTCPOutlet(i interface{}, common *OutletCommon) (out *TCPOutlet, err error) { + + out = &TCPOutlet{} + out.Formatter = common.Formatter + + var in struct { + Net string + Address string + RetryInterval string `mapstructure:"retry_interval"` + TLS *struct { + CA string + Cert string + Key string + } + } + if err = mapstructure.Decode(i, &in); err != nil { + return nil, errors.Wrap(err, "mapstructure error") + } + + out.RetryInterval, err = time.ParseDuration(in.RetryInterval) + if err != nil { + return nil, errors.Wrap(err, "cannot parse 'retry_interval'") + } + + out.Net, out.Address = in.Net, in.Address + + if in.TLS != nil { + + cert, err := tls.LoadX509KeyPair(in.TLS.Cert, in.TLS.Key) + if err != nil { + return nil, errors.Wrap(err, "cannot load client cert") + } + + var rootCAs *x509.CertPool + if in.TLS.CA == "" { + if rootCAs, err = x509.SystemCertPool(); err != nil { + return nil, errors.Wrap(err, "cannot open system cert pool") + } + } else { + rootCAs = x509.NewCertPool() + rootCAPEM, err := ioutil.ReadFile(in.TLS.CA) + if err != nil { + return nil, errors.Wrap(err, "cannot load CA cert") + } + if !rootCAs.AppendCertsFromPEM(rootCAPEM) { + return nil, errors.New("cannot parse CA cert") + } + } + if err != nil && in.TLS.CA == "" { + return nil, errors.Wrap(err, "cannot load root ca pool") + } + + out.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: rootCAs, + } + + out.TLS.BuildNameToCertificate() + } + + return + +} + +func parseSyslogOutlet(i interface{}, common *OutletCommon) (out *SyslogOutlet, err error) { + + var in struct { + RetryInterval string `mapstructure:"retry_interval"` + } + if err = mapstructure.Decode(i, &in); err != nil { + return nil, errors.Wrap(err, "mapstructure error") + } + + out = &SyslogOutlet{} + out.Formatter = common.Formatter + 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 in.RetryInterval != "" { + out.RetryInterval, err = time.ParseDuration(in.RetryInterval) + if err != nil { + return nil, errors.Wrap(err, "cannot parse 'retry_interval'") + } + } + + return +} diff --git a/cmd/sampleconf/random/logging.yml b/cmd/sampleconf/random/logging.yml index 94b5953..7981258 100644 --- a/cmd/sampleconf/random/logging.yml +++ b/cmd/sampleconf/random/logging.yml @@ -1,9 +1,11 @@ global: logging: - stdout: + + - outlet: stdout level: warn format: human - tcp: + + - outlet: tcp level: debug format: json net: tcp @@ -13,8 +15,10 @@ global: ca: sampleconf/random/logging/logserver.crt cert: sampleconf/random/logging/client.crt key: sampleconf/random/logging/client.key - syslog: - enable: true + + - outlet: syslog + level: debug format: logfmt + jobs: [] diff --git a/docs/content/configuration/logging.md b/docs/content/configuration/logging.md index d74b0e8..7871004 100644 --- a/docs/content/configuration/logging.md +++ b/docs/content/configuration/logging.md @@ -7,20 +7,39 @@ zrepl uses structured logging to provide users with easily processable log messa ## Configuration -Logging is configured in the `global` section of the [configuration file]({{< relref "install/_index.md#configuration-files" >}}).
+Logging outlets are configured in the `global` section of the [configuration file]({{< relref "install/_index.md#configuration-files" >}}).
Check out {{< sampleconflink "random/logging.yml" >}} for an example on how to configure multiple outlets: + ```yaml global: logging: - OUTLET_TYPE: - PARAM: VAUE - ... - OUTLET_TYPE: - ... -jobs: - ... + + - outlet: OUTLET_TYPE + level: MINIMUM_LEVEL + format: FORMAT + + - outlet: OUTLET_TYPE + level: MINIMUM_LEVEL + format: FORMAT + + ... + +jobs: ... + +``` + +### Default Configuration + +By default, the following logging configuration is used + +```yaml +global: + logging: + + - outlet: "stdout" + level: "warn" + format: "human" ``` -**Note**: Currently, only one instance of an outlet type can be instantiated {{< zrepl-issue 20 >}} {{% notice info %}} Output to **stderr** should always be considered a **critical error**.
@@ -56,30 +75,37 @@ Outlets are ... well ... outlets for log entries into the world. #### **`stdout`** -| Parameter | Default | Description | +| Parameter | Default | Comment | |-----------| --------- | ----------- | -|`level` | *none* | minimum [log level](#levels) | -|`format` | `human` | [format](#formats) | +|`outlet` | *none* | required | +|`level` | *none* | minimum [log level](#levels), required | +|`format` | *none* | output [format](#formats), required | Writes all log entries with minimum level `level` formatted by `format` to stdout. +Can only be specified once. + #### **`syslog`** -| Parameter | Default | Description | +| Parameter | Default | Comment | |-----------| --------- | ----------- | -|`enable` | false | boolean | -|`format` | `human` | [format](#formats) | +|`outlet` | *none* | required | +|`level` | *none* | minimum [log level](#levels), required, usually `debug` | +|`format` | *none* | output [format](#formats), required| |`retry_interval`| 0 | Interval between reconnection attempts to syslog | Writes all log entries formatted by `format` to syslog. On normal setups, you should not need to change the `retry_interval`. +Can only be specified once. + #### **`tcp`** -| Parameter | Default | Description | +| Parameter | Default | Comment | |-----------| --------- | ----------- | -|`level` | *none* | minimum [log level](#levels) | -|`format` | *none* | [format](#formats) | +|`outlet` | *none* | required | +|`level` | *none* | minimum [log level](#levels), required | +|`format` | *none* | output [format](#formats), required | |`net`|*none*|`tcp` in most cases| |`address`|*none*|remote network, e.g. `logs.example.com:10202`| |`retry_interval`|*none*|Interval between reconnection attempts to `address`|