Skip to content

Commit 074a348

Browse files
authored
Merge pull request #84 from Icinga/go-yaml-compat
`logging#Config`: Fix `Options` can no longer be parsed by `go-yaml`
2 parents 3b9c90b + 00f1fd4 commit 074a348

File tree

3 files changed

+119
-55
lines changed

3 files changed

+119
-55
lines changed

logging/config.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package logging
22

33
import (
44
"fmt"
5+
"github.com/creasty/defaults"
56
"github.com/pkg/errors"
67
"go.uber.org/zap/zapcore"
78
"os"
@@ -38,39 +39,52 @@ func (o *Options) UnmarshalText(text []byte) error {
3839
return nil
3940
}
4041

42+
// UnmarshalYAML implements yaml.InterfaceUnmarshaler to allow Options to be parsed go-yaml.
43+
func (o *Options) UnmarshalYAML(unmarshal func(any) error) error {
44+
optionsMap := make(map[string]zapcore.Level)
45+
46+
if err := unmarshal(&optionsMap); err != nil {
47+
return err
48+
}
49+
50+
*o = optionsMap
51+
52+
return nil
53+
}
54+
4155
// Config defines Logger configuration.
4256
type Config struct {
4357
// zapcore.Level at 0 is for info level.
4458
Level zapcore.Level `yaml:"level" env:"LEVEL" default:"0"`
4559
Output string `yaml:"output" env:"OUTPUT"`
4660
// Interval for periodic logging.
4761
Interval time.Duration `yaml:"interval" env:"INTERVAL" default:"20s"`
48-
49-
Options `yaml:"options" env:"OPTIONS"`
62+
Options Options `yaml:"options" env:"OPTIONS"`
5063
}
5164

52-
// Validate checks constraints in the configuration and returns an error if they are violated.
53-
// Also configures the log output if it is not configured:
65+
// SetDefaults implements defaults.Setter to configure the log output if it is not set:
5466
// systemd-journald is used when Icinga DB is running under systemd, otherwise stderr.
55-
func (l *Config) Validate() error {
56-
if l.Interval <= 0 {
57-
return errors.New("periodic logging interval must be positive")
58-
}
59-
60-
if l.Output == "" {
67+
func (c *Config) SetDefaults() {
68+
if defaults.CanUpdate(c.Output) {
6169
if _, ok := os.LookupEnv("NOTIFY_SOCKET"); ok {
6270
// When started by systemd, NOTIFY_SOCKET is set by systemd for Type=notify supervised services,
6371
// which is the default setting for the Icinga DB service.
6472
// This assumes that Icinga DB is running under systemd, so set output to systemd-journald.
65-
l.Output = JOURNAL
73+
c.Output = JOURNAL
6674
} else {
6775
// Otherwise set it to console, i.e. write log messages to stderr.
68-
l.Output = CONSOLE
76+
c.Output = CONSOLE
6977
}
7078
}
79+
}
80+
81+
// Validate checks constraints in the configuration and returns an error if they are violated.
82+
func (c *Config) Validate() error {
83+
if c.Interval <= 0 {
84+
return errors.New("periodic logging interval must be positive")
85+
}
7186

72-
// To be on the safe side, always call AssertOutput.
73-
return AssertOutput(l.Output)
87+
return AssertOutput(c.Output)
7488
}
7589

7690
// AssertOutput returns an error if output is not a valid logger output.

logging/config_test.go

Lines changed: 90 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,82 @@
11
package logging
22

33
import (
4+
"fmt"
5+
"github.com/creasty/defaults"
46
"github.com/icinga/icinga-go-library/config"
7+
"github.com/icinga/icinga-go-library/testutils"
58
"github.com/stretchr/testify/require"
69
"go.uber.org/zap/zapcore"
10+
"os"
711
"testing"
812
"time"
913
)
1014

1115
func TestConfig(t *testing.T) {
12-
subtests := []struct {
13-
name string
14-
opts config.EnvOptions
15-
expected Config
16-
error bool
17-
}{
16+
var defaultConfig Config
17+
require.NoError(t, defaults.Set(&defaultConfig), "setting default config")
18+
19+
configTests := []testutils.TestCase[Config, testutils.ConfigTestData]{
20+
{
21+
Name: "Defaults",
22+
Data: testutils.ConfigTestData{
23+
// An empty YAML file causes an error,
24+
// so specify a valid key without a value to trigger fallback to the default.
25+
Yaml: `level:`,
26+
},
27+
Expected: defaultConfig,
28+
},
1829
{
19-
name: "empty",
20-
opts: config.EnvOptions{},
21-
expected: Config{
22-
Output: "console",
23-
Interval: 20 * time.Second,
30+
Name: "periodic logging interval must be positive",
31+
Data: testutils.ConfigTestData{
32+
Yaml: `interval: 0s`,
33+
Env: map[string]string{"INTERVAL": "0s"},
2434
},
35+
Error: testutils.ErrorContains("periodic logging interval must be positive"),
2536
},
2637
{
27-
name: "invalid-output",
28-
opts: config.EnvOptions{Environment: map[string]string{"OUTPUT": "☃"}},
29-
error: true,
38+
Name: "invalid logger output",
39+
Data: testutils.ConfigTestData{
40+
Yaml: `output: invalid`,
41+
Env: map[string]string{"OUTPUT": "invalid"},
42+
},
43+
Error: testutils.ErrorContains("invalid is not a valid logger output"),
3044
},
3145
{
32-
name: "customized",
33-
opts: config.EnvOptions{Environment: map[string]string{
34-
"LEVEL": zapcore.DebugLevel.String(),
35-
"OUTPUT": JOURNAL,
36-
"INTERVAL": "3m14s",
37-
}},
38-
expected: Config{
46+
Name: "Customized",
47+
Data: testutils.ConfigTestData{
48+
Yaml: fmt.Sprintf(
49+
`
50+
level: debug
51+
output: %s
52+
interval: 3m14s`,
53+
JOURNAL,
54+
),
55+
Env: map[string]string{
56+
"LEVEL": zapcore.DebugLevel.String(),
57+
"OUTPUT": JOURNAL,
58+
"INTERVAL": "3m14s",
59+
},
60+
},
61+
Expected: Config{
3962
Level: zapcore.DebugLevel,
4063
Output: JOURNAL,
4164
Interval: 3*time.Minute + 14*time.Second,
4265
},
4366
},
4467
{
45-
name: "options",
46-
opts: config.EnvOptions{Environment: map[string]string{"OPTIONS": "foo:debug,bar:info,buz:panic"}},
47-
expected: Config{
48-
Output: "console",
49-
Interval: 20 * time.Second,
68+
Name: "Options",
69+
Data: testutils.ConfigTestData{
70+
Yaml: `
71+
options:
72+
foo: debug
73+
bar: info
74+
buz: panic`,
75+
Env: map[string]string{"OPTIONS": "foo:debug,bar:info,buz:panic"},
76+
},
77+
Expected: Config{
78+
Output: defaultConfig.Output,
79+
Interval: defaultConfig.Interval,
5080
Options: map[string]zapcore.Level{
5181
"foo": zapcore.DebugLevel,
5282
"bar": zapcore.InfoLevel,
@@ -55,21 +85,41 @@ func TestConfig(t *testing.T) {
5585
},
5686
},
5787
{
58-
name: "options-invalid-levels",
59-
opts: config.EnvOptions{Environment: map[string]string{"OPTIONS": "foo:foo,bar:0"}},
60-
error: true,
88+
Name: "Options with invalid level",
89+
Data: testutils.ConfigTestData{
90+
Yaml: `
91+
options:
92+
foo: foo`,
93+
Env: map[string]string{"OPTIONS": "foo:foo"},
94+
},
95+
Error: testutils.ErrorContains(`unrecognized level: "foo"`),
6196
},
6297
}
6398

64-
for _, test := range subtests {
65-
t.Run(test.name, func(t *testing.T) {
66-
var out Config
67-
if err := config.FromEnv(&out, test.opts); test.error {
68-
require.Error(t, err)
69-
} else {
70-
require.NoError(t, err)
71-
require.Equal(t, test.expected, out)
72-
}
73-
})
74-
}
99+
t.Run("FromEnv", func(t *testing.T) {
100+
for _, tc := range configTests {
101+
t.Run(tc.Name, tc.F(func(data testutils.ConfigTestData) (Config, error) {
102+
var actual Config
103+
104+
err := config.FromEnv(&actual, config.EnvOptions{Environment: data.Env})
105+
106+
return actual, err
107+
}))
108+
}
109+
})
110+
111+
t.Run("FromYAMLFile", func(t *testing.T) {
112+
for _, tc := range configTests {
113+
t.Run(tc.Name+"/FromYAMLFile", tc.F(func(data testutils.ConfigTestData) (Config, error) {
114+
var actual Config
115+
116+
var err error
117+
testutils.WithYAMLFile(t, data.Yaml, func(file *os.File) {
118+
err = config.FromYAMLFile(file.Name(), &actual)
119+
})
120+
121+
return actual, err
122+
}))
123+
}
124+
})
75125
}

testutils/testutils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
// TestCase represents a generic test case structure.
1616
// It is parameterized by T, the type of the expected result, and D, the type of the test data.
1717
// This struct is useful for defining test cases with expected outcomes and associated data.
18-
type TestCase[T comparable, D any] struct {
18+
type TestCase[T any, D any] struct {
1919
// Name is the identifier for the test case, used for reporting purposes.
2020
Name string
2121
// Expected is the anticipated result of the test. It should be left empty if an error is expected.

0 commit comments

Comments
 (0)