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`|