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  }