Skip to content

Commit f4cb3fd

Browse files
authored
[logp] Add typed loggers allowing log entries to go to a different output (#171)
This commit introduces a typedLogger that allows a log entry to be directed to a different output when a configurable key/value are present in the log fields. This enables Beats and Elastic-Agent to log event data to a separate log file.
1 parent 873d124 commit f4cb3fd

File tree

7 files changed

+972
-21
lines changed

7 files changed

+972
-21
lines changed

logp/config.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,30 @@ func DefaultConfig(environment Environment) Config {
8888
}
8989
}
9090

91+
// DefaultEventConfig returns the default config options for the event logger in
92+
// a given environment the Beat is supposed to be run within.
93+
func DefaultEventConfig(environment Environment) Config {
94+
return Config{
95+
Level: defaultLevel,
96+
ToFiles: true,
97+
ToStderr: false,
98+
Files: FileConfig{
99+
MaxSize: 5 * 1024 * 1024, // 5Mb
100+
MaxBackups: 2,
101+
Permissions: 0600,
102+
Interval: 0,
103+
RotateOnStartup: false,
104+
RedirectStderr: false,
105+
Name: "event-data",
106+
},
107+
Metrics: MetricsConfig{
108+
Enabled: false,
109+
},
110+
environment: environment,
111+
addCaller: true,
112+
}
113+
}
114+
91115
// LogFilename returns the base filename to which logs will be written for
92116
// the "files" log output. If another log output is used, or `logging.files.name`
93117
// is unspecified, then the beat name will be returned.

logp/configure/logging.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ func init() {
4545
flag.Var((*environmentVar)(&environment), "environment", "set environment being ran in")
4646
}
4747

48+
func GetEnvironment() logp.Environment {
49+
return environment
50+
}
51+
4852
// Logging builds a logp.Config based on the given common.Config and the specified
4953
// CLI flags.
5054
func Logging(beatName string, cfg *config.C) error {
@@ -75,6 +79,39 @@ func LoggingWithOutputs(beatName string, cfg *config.C, outputs ...zapcore.Core)
7579
return logp.ConfigureWithOutputs(config, outputs...)
7680
}
7781

82+
// LoggingWithTypedOutputs applies some defaults then calls ConfigureWithTypedOutputs
83+
func LoggingWithTypedOutputs(beatName string, cfg, typedCfg *config.C, logKey, kind string, outputs ...zapcore.Core) error {
84+
config := logp.DefaultConfig(environment)
85+
config.Beat = beatName
86+
if cfg != nil {
87+
if err := cfg.Unpack(&config); err != nil {
88+
return err
89+
}
90+
}
91+
92+
applyFlags(&config)
93+
94+
typedLogpConfig := logp.DefaultEventConfig(environment)
95+
defaultName := typedLogpConfig.Files.Name
96+
typedLogpConfig.Beat = beatName
97+
if typedCfg != nil {
98+
if err := typedCfg.Unpack(&typedLogpConfig); err != nil {
99+
return fmt.Errorf("cannot unpack typed output config: %w", err)
100+
}
101+
}
102+
103+
// Make sure we're always running on the same log level
104+
typedLogpConfig.Level = config.Level
105+
typedLogpConfig.Selectors = config.Selectors
106+
107+
// If the name has not been configured, make it {beatName}-events-data
108+
if typedLogpConfig.Files.Name == defaultName {
109+
typedLogpConfig.Files.Name = beatName + "-events-data"
110+
}
111+
112+
return logp.ConfigureWithTypedOutput(config, typedLogpConfig, logKey, kind, outputs...)
113+
}
114+
78115
func applyFlags(cfg *logp.Config) {
79116
if toStderr {
80117
cfg.ToStderr = true

logp/core.go

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
"errors"
2222
"flag"
2323
"fmt"
24-
"io/ioutil"
24+
"io"
2525
golog "log"
2626
"os"
2727
"path/filepath"
@@ -68,36 +68,33 @@ func Configure(cfg Config) error {
6868
return ConfigureWithOutputs(cfg)
6969
}
7070

71-
// ConfigureWithOutputs XXX: is used by elastic-agent only (See file: x-pack/elastic-agent/pkg/core/logger/logger.go).
72-
// The agent requires that the output specified in the config object is configured and merged with the
73-
// logging outputs given.
74-
func ConfigureWithOutputs(cfg Config, outputs ...zapcore.Core) error {
71+
func createSink(defaultLoggerCfg Config, outputs ...zapcore.Core) (zapcore.Core, zap.AtomicLevel, *observer.ObservedLogs, map[string]struct{}, error) {
7572
var (
7673
sink zapcore.Core
7774
observedLogs *observer.ObservedLogs
7875
err error
7976
level zap.AtomicLevel
8077
)
8178

82-
level = zap.NewAtomicLevelAt(cfg.Level.ZapLevel())
79+
level = zap.NewAtomicLevelAt(defaultLoggerCfg.Level.ZapLevel())
8380
// Build a single output (stderr has priority if more than one are enabled).
84-
if cfg.toObserver {
81+
if defaultLoggerCfg.toObserver {
8582
sink, observedLogs = observer.New(level)
8683
} else {
87-
sink, err = createLogOutput(cfg, level)
84+
sink, err = createLogOutput(defaultLoggerCfg, level)
8885
}
8986
if err != nil {
90-
return fmt.Errorf("failed to build log output: %w", err)
87+
return nil, level, nil, nil, fmt.Errorf("failed to build log output: %w", err)
9188
}
9289

9390
// Default logger is always discard, debug level below will
9491
// possibly re-enable it.
95-
golog.SetOutput(ioutil.Discard)
92+
golog.SetOutput(io.Discard)
9693

9794
// Enabled selectors when debug is enabled.
98-
selectors := make(map[string]struct{}, len(cfg.Selectors))
99-
if cfg.Level.Enabled(DebugLevel) && len(cfg.Selectors) > 0 {
100-
for _, sel := range cfg.Selectors {
95+
selectors := make(map[string]struct{}, len(defaultLoggerCfg.Selectors))
96+
if defaultLoggerCfg.Level.Enabled(DebugLevel) && len(defaultLoggerCfg.Selectors) > 0 {
97+
for _, sel := range defaultLoggerCfg.Selectors {
10198
selectors[strings.TrimSpace(sel)] = struct{}{}
10299
}
103100

@@ -118,7 +115,73 @@ func ConfigureWithOutputs(cfg Config, outputs ...zapcore.Core) error {
118115
}
119116

120117
sink = newMultiCore(append(outputs, sink)...)
121-
root := zap.New(sink, makeOptions(cfg)...)
118+
119+
return sink, level, observedLogs, selectors, err
120+
}
121+
122+
// ConfigureWithOutputs configures the global logger to use an output created
123+
// from `defaultLoggerCfg` and all the outputs passed by `outputs`.
124+
// This function needs to be exported because it's used by `logp/configure`
125+
func ConfigureWithOutputs(defaultLoggerCfg Config, outputs ...zapcore.Core) error {
126+
sink, level, observedLogs, selectors, err := createSink(defaultLoggerCfg, outputs...)
127+
if err != nil {
128+
return err
129+
}
130+
root := zap.New(sink, makeOptions(defaultLoggerCfg)...)
131+
storeLogger(&coreLogger{
132+
selectors: selectors,
133+
rootLogger: root,
134+
globalLogger: root.WithOptions(zap.AddCallerSkip(1)),
135+
logger: newLogger(root, ""),
136+
level: level,
137+
observedLogs: observedLogs,
138+
})
139+
return nil
140+
}
141+
142+
// ConfigureWithTypedOutput configures the global logger to use typed outputs.
143+
//
144+
// If a log entry matches the defined key/value, this entry is logged using the
145+
// core generated from `typedLoggerCfg`, otherwise it will be logged by all
146+
// cores in `outputs` and the one generated from `defaultLoggerCfg`.
147+
// Arguments:
148+
// - `defaultLoggerCfg` is used to create a new core that will be the default
149+
// output from the logger
150+
// - `typedLoggerCfg` is used to create a new output that will only be used
151+
// when the log entry matches `entry[logKey] = kind`
152+
// - `key` is the key the typed logger will look at
153+
// - `value` is the value compared against the `logKey` entry
154+
// - `outputs` is a list of cores that will be added together with the core
155+
// generated by `defaultLoggerCfg` as the default output for the loggger.
156+
//
157+
// If `defaultLoggerCfg.toObserver` is true, then `typedLoggerCfg` is ignored
158+
// and a single sink is used so all logs can be observed.
159+
func ConfigureWithTypedOutput(defaultLoggerCfg, typedLoggerCfg Config, key, value string, outputs ...zapcore.Core) error {
160+
sink, level, observedLogs, selectors, err := createSink(defaultLoggerCfg, outputs...)
161+
if err != nil {
162+
return err
163+
}
164+
165+
var typedCore zapcore.Core
166+
if defaultLoggerCfg.toObserver {
167+
typedCore = sink
168+
} else {
169+
typedCore, err = createLogOutput(typedLoggerCfg, level)
170+
}
171+
if err != nil {
172+
return fmt.Errorf("could not create typed logger output: %w", err)
173+
}
174+
175+
sink = &typedLoggerCore{
176+
defaultCore: sink,
177+
typedCore: typedCore,
178+
key: key,
179+
value: value,
180+
}
181+
182+
sink = selectiveWrapper(sink, selectors)
183+
184+
root := zap.New(sink, makeOptions(defaultLoggerCfg)...)
122185
storeLogger(&coreLogger{
123186
selectors: selectors,
124187
rootLogger: root,
@@ -215,7 +278,7 @@ func makeStderrOutput(cfg Config, enab zapcore.LevelEnabler) (zapcore.Core, erro
215278
}
216279

217280
func makeDiscardOutput(cfg Config, enab zapcore.LevelEnabler) (zapcore.Core, error) {
218-
discard := zapcore.AddSync(ioutil.Discard)
281+
discard := zapcore.AddSync(io.Discard)
219282
return newCore(buildEncoder(cfg), discard, enab), nil
220283
}
221284

0 commit comments

Comments
 (0)