Skip to content

Commit

Permalink
[logp] Add typed loggers allowing log entries to go to a different ou…
Browse files Browse the repository at this point in the history
…tput (#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.
  • Loading branch information
belimawr authored Apr 25, 2024
1 parent 873d124 commit f4cb3fd
Show file tree
Hide file tree
Showing 7 changed files with 972 additions and 21 deletions.
24 changes: 24 additions & 0 deletions logp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ func DefaultConfig(environment Environment) Config {
}
}

// DefaultEventConfig returns the default config options for the event logger in
// a given environment the Beat is supposed to be run within.
func DefaultEventConfig(environment Environment) Config {
return Config{
Level: defaultLevel,
ToFiles: true,
ToStderr: false,
Files: FileConfig{
MaxSize: 5 * 1024 * 1024, // 5Mb
MaxBackups: 2,
Permissions: 0600,
Interval: 0,
RotateOnStartup: false,
RedirectStderr: false,
Name: "event-data",
},
Metrics: MetricsConfig{
Enabled: false,
},
environment: environment,
addCaller: true,
}
}

// LogFilename returns the base filename to which logs will be written for
// the "files" log output. If another log output is used, or `logging.files.name`
// is unspecified, then the beat name will be returned.
Expand Down
37 changes: 37 additions & 0 deletions logp/configure/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ func init() {
flag.Var((*environmentVar)(&environment), "environment", "set environment being ran in")
}

func GetEnvironment() logp.Environment {
return environment
}

// Logging builds a logp.Config based on the given common.Config and the specified
// CLI flags.
func Logging(beatName string, cfg *config.C) error {
Expand Down Expand Up @@ -75,6 +79,39 @@ func LoggingWithOutputs(beatName string, cfg *config.C, outputs ...zapcore.Core)
return logp.ConfigureWithOutputs(config, outputs...)
}

// LoggingWithTypedOutputs applies some defaults then calls ConfigureWithTypedOutputs
func LoggingWithTypedOutputs(beatName string, cfg, typedCfg *config.C, logKey, kind string, outputs ...zapcore.Core) error {
config := logp.DefaultConfig(environment)
config.Beat = beatName
if cfg != nil {
if err := cfg.Unpack(&config); err != nil {
return err
}
}

applyFlags(&config)

typedLogpConfig := logp.DefaultEventConfig(environment)
defaultName := typedLogpConfig.Files.Name
typedLogpConfig.Beat = beatName
if typedCfg != nil {
if err := typedCfg.Unpack(&typedLogpConfig); err != nil {
return fmt.Errorf("cannot unpack typed output config: %w", err)
}
}

// Make sure we're always running on the same log level
typedLogpConfig.Level = config.Level
typedLogpConfig.Selectors = config.Selectors

// If the name has not been configured, make it {beatName}-events-data
if typedLogpConfig.Files.Name == defaultName {
typedLogpConfig.Files.Name = beatName + "-events-data"
}

return logp.ConfigureWithTypedOutput(config, typedLogpConfig, logKey, kind, outputs...)
}

func applyFlags(cfg *logp.Config) {
if toStderr {
cfg.ToStderr = true
Expand Down
93 changes: 78 additions & 15 deletions logp/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"errors"
"flag"
"fmt"
"io/ioutil"
"io"
golog "log"
"os"
"path/filepath"
Expand Down Expand Up @@ -68,36 +68,33 @@ func Configure(cfg Config) error {
return ConfigureWithOutputs(cfg)
}

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

level = zap.NewAtomicLevelAt(cfg.Level.ZapLevel())
level = zap.NewAtomicLevelAt(defaultLoggerCfg.Level.ZapLevel())
// Build a single output (stderr has priority if more than one are enabled).
if cfg.toObserver {
if defaultLoggerCfg.toObserver {
sink, observedLogs = observer.New(level)
} else {
sink, err = createLogOutput(cfg, level)
sink, err = createLogOutput(defaultLoggerCfg, level)
}
if err != nil {
return fmt.Errorf("failed to build log output: %w", err)
return nil, level, nil, nil, fmt.Errorf("failed to build log output: %w", err)
}

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

// Enabled selectors when debug is enabled.
selectors := make(map[string]struct{}, len(cfg.Selectors))
if cfg.Level.Enabled(DebugLevel) && len(cfg.Selectors) > 0 {
for _, sel := range cfg.Selectors {
selectors := make(map[string]struct{}, len(defaultLoggerCfg.Selectors))
if defaultLoggerCfg.Level.Enabled(DebugLevel) && len(defaultLoggerCfg.Selectors) > 0 {
for _, sel := range defaultLoggerCfg.Selectors {
selectors[strings.TrimSpace(sel)] = struct{}{}
}

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

sink = newMultiCore(append(outputs, sink)...)
root := zap.New(sink, makeOptions(cfg)...)

return sink, level, observedLogs, selectors, err
}

// ConfigureWithOutputs configures the global logger to use an output created
// from `defaultLoggerCfg` and all the outputs passed by `outputs`.
// This function needs to be exported because it's used by `logp/configure`
func ConfigureWithOutputs(defaultLoggerCfg Config, outputs ...zapcore.Core) error {
sink, level, observedLogs, selectors, err := createSink(defaultLoggerCfg, outputs...)
if err != nil {
return err
}
root := zap.New(sink, makeOptions(defaultLoggerCfg)...)
storeLogger(&coreLogger{
selectors: selectors,
rootLogger: root,
globalLogger: root.WithOptions(zap.AddCallerSkip(1)),
logger: newLogger(root, ""),
level: level,
observedLogs: observedLogs,
})
return nil
}

// ConfigureWithTypedOutput configures the global logger to use typed outputs.
//
// If a log entry matches the defined key/value, this entry is logged using the
// core generated from `typedLoggerCfg`, otherwise it will be logged by all
// cores in `outputs` and the one generated from `defaultLoggerCfg`.
// Arguments:
// - `defaultLoggerCfg` is used to create a new core that will be the default
// output from the logger
// - `typedLoggerCfg` is used to create a new output that will only be used
// when the log entry matches `entry[logKey] = kind`
// - `key` is the key the typed logger will look at
// - `value` is the value compared against the `logKey` entry
// - `outputs` is a list of cores that will be added together with the core
// generated by `defaultLoggerCfg` as the default output for the loggger.
//
// If `defaultLoggerCfg.toObserver` is true, then `typedLoggerCfg` is ignored
// and a single sink is used so all logs can be observed.
func ConfigureWithTypedOutput(defaultLoggerCfg, typedLoggerCfg Config, key, value string, outputs ...zapcore.Core) error {
sink, level, observedLogs, selectors, err := createSink(defaultLoggerCfg, outputs...)
if err != nil {
return err
}

var typedCore zapcore.Core
if defaultLoggerCfg.toObserver {
typedCore = sink
} else {
typedCore, err = createLogOutput(typedLoggerCfg, level)
}
if err != nil {
return fmt.Errorf("could not create typed logger output: %w", err)
}

sink = &typedLoggerCore{
defaultCore: sink,
typedCore: typedCore,
key: key,
value: value,
}

sink = selectiveWrapper(sink, selectors)

root := zap.New(sink, makeOptions(defaultLoggerCfg)...)
storeLogger(&coreLogger{
selectors: selectors,
rootLogger: root,
Expand Down Expand Up @@ -215,7 +278,7 @@ func makeStderrOutput(cfg Config, enab zapcore.LevelEnabler) (zapcore.Core, erro
}

func makeDiscardOutput(cfg Config, enab zapcore.LevelEnabler) (zapcore.Core, error) {
discard := zapcore.AddSync(ioutil.Discard)
discard := zapcore.AddSync(io.Discard)
return newCore(buildEncoder(cfg), discard, enab), nil
}

Expand Down
Loading

0 comments on commit f4cb3fd

Please sign in to comment.