config: logging: defaults + definition as list

* Stdout logger as default logger
* Clearer keyword / value separation
* Allows multiple outlet definitions

BREAK CONFIG

fixes #20
fixes #19
This commit is contained in:
Christian Schwarz 2017-10-05 13:31:16 +02:00
parent 2764c95952
commit b95260f4b5
3 changed files with 223 additions and 151 deletions

View File

@ -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
}

View File

@ -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: []

View File

@ -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" >}}).<br/>
Logging outlets are configured in the `global` section of the [configuration file]({{< relref "install/_index.md#configuration-files" >}}).<br />
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**.<br />
@ -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`|