github.com/observiq/bindplane-agent@v1.51.0/internal/logging/config.go (about) 1 // Copyright observIQ, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package logging parses and applies logging configuration 16 package logging 17 18 import ( 19 "errors" 20 "fmt" 21 "os" 22 "path/filepath" 23 24 "go.uber.org/zap" 25 "go.uber.org/zap/zapcore" 26 "gopkg.in/natefinch/lumberjack.v2" 27 "gopkg.in/yaml.v3" 28 ) 29 30 // DefaultConfigPath is the relative path to the default logging.yaml 31 const DefaultConfigPath = "./logging.yaml" 32 33 const ( 34 // fileOutput is an output option for logging to a file. 35 fileOutput string = "file" 36 37 // stdOutput is an output option for logging to stdout. 38 stdOutput string = "stdout" 39 ) 40 41 // LoggerConfig is the configuration of a logger. 42 type LoggerConfig struct { 43 Output string `yaml:"output"` 44 Level zapcore.Level `yaml:"level"` 45 File *lumberjack.Logger `yaml:"file,omitempty"` 46 } 47 48 // NewLoggerConfig returns a logger config. 49 // If configPath is not set, stdout logging will be enabled, and a default 50 // configuration will be written to ./logging.yaml 51 func NewLoggerConfig(configPath string) (*LoggerConfig, error) { 52 // No logger path specified, we'll assume the default path. 53 if configPath == DefaultConfigPath { 54 // If the file doesn't exist, we will create the config with the default parameters. 55 if _, err := os.Stat(DefaultConfigPath); errors.Is(err, os.ErrNotExist) { 56 defaultConf := defaultConfig() 57 if err := writeConfig(defaultConf, DefaultConfigPath); err != nil { 58 return nil, fmt.Errorf("failed to write default configuration: %w", err) 59 } 60 return defaultConf, nil 61 } else if err != nil { 62 return nil, fmt.Errorf("failed to stat config: %w", err) 63 } 64 // The config already exists; We should continue and read it like any other config. 65 } 66 67 cleanPath := filepath.Clean(configPath) 68 69 // conf will start as the default config; any unspecified values in the config 70 // will default to the values in the default config. 71 conf := defaultConfig() 72 73 confBytes, err := os.ReadFile(cleanPath) 74 if err != nil { 75 return nil, fmt.Errorf("failed to read config: %w", err) 76 } 77 78 if err := yaml.Unmarshal(confBytes, conf); err != nil { 79 return nil, fmt.Errorf("failed to unmarshal config: %w", err) 80 } 81 82 if conf.File != nil { 83 // Expand optional environment variables in file path 84 conf.File.Filename = os.ExpandEnv(conf.File.Filename) 85 } 86 87 return conf, nil 88 } 89 90 // Options returns the LoggerConfig's zap logging options. 91 func (l *LoggerConfig) Options() ([]zap.Option, error) { 92 core, err := l.core() 93 if err != nil { 94 return nil, err 95 } 96 97 opt := zap.WrapCore(func(_ zapcore.Core) zapcore.Core { 98 return core 99 }) 100 101 return []zap.Option{opt}, nil 102 } 103 104 // core returns the logging core specified in the config. 105 // An unknown output will return a nop core. 106 func (l *LoggerConfig) core() (zapcore.Core, error) { 107 switch l.Output { 108 case fileOutput: 109 return zapcore.NewCore(newEncoder(), zapcore.AddSync(l.File), l.Level), nil 110 case stdOutput: 111 return zapcore.NewCore(newEncoder(), zapcore.Lock(os.Stdout), l.Level), nil 112 default: 113 return nil, fmt.Errorf("unrecognized output type: %s", l.Output) 114 } 115 } 116 117 func newEncoder() zapcore.Encoder { 118 encoderConfig := zap.NewProductionEncoderConfig() 119 encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 120 return zapcore.NewJSONEncoder(encoderConfig) 121 } 122 123 // defaultConfig returns a new instance of the default logging configuration 124 func defaultConfig() *LoggerConfig { 125 return &LoggerConfig{ 126 Output: stdOutput, 127 Level: zap.InfoLevel, 128 } 129 } 130 131 // writeConfig writes the given configuration to the specified location. 132 func writeConfig(config *LoggerConfig, outLocation string) error { 133 configBytes, err := yaml.Marshal(config) 134 if err != nil { 135 return fmt.Errorf("failed to marshal: %w", err) 136 } 137 138 if err = os.WriteFile(outLocation, configBytes, 0600); err != nil { 139 return fmt.Errorf("failed to write config file: %w", err) 140 } 141 142 return nil 143 }