knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/logging/config.go (about)

     1  /*
     2  Copyright 2018 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package logging
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  	"strings"
    26  
    27  	"github.com/blendle/zapdriver"
    28  	"go.uber.org/zap"
    29  	"go.uber.org/zap/zapcore"
    30  	corev1 "k8s.io/api/core/v1"
    31  
    32  	"knative.dev/pkg/changeset"
    33  	"knative.dev/pkg/logging/logkey"
    34  )
    35  
    36  const (
    37  	configMapNameEnv   = "CONFIG_LOGGING_NAME"
    38  	loggerConfigKey    = "zap-logger-config"
    39  	fallbackLoggerName = "fallback-logger"
    40  )
    41  
    42  var (
    43  	errEmptyLoggerConfig     = errors.New("empty logger configuration")
    44  	errEmptyJSONLogginString = errors.New("json logging string is empty")
    45  )
    46  
    47  // NewLogger creates a logger with the supplied configuration.
    48  // In addition to the logger, it returns AtomicLevel that can
    49  // be used to change the logging level at runtime.
    50  // If configuration is empty, a fallback configuration is used.
    51  // If configuration cannot be used to instantiate a logger,
    52  // the same fallback configuration is used.
    53  func NewLogger(configJSON string, levelOverride string, opts ...zap.Option) (*zap.SugaredLogger, zap.AtomicLevel) {
    54  	logger, atomicLevel, err := newLoggerFromConfig(configJSON, levelOverride, opts)
    55  	if err == nil {
    56  		return enrichLoggerWithCommitID(logger), atomicLevel
    57  	}
    58  
    59  	loggingCfg := stackdriverConfig()
    60  
    61  	if levelOverride != "" {
    62  		if level, err := levelFromString(levelOverride); err == nil {
    63  			loggingCfg.Level = zap.NewAtomicLevelAt(*level)
    64  		}
    65  	}
    66  
    67  	logger, err = loggingCfg.Build(opts...)
    68  	if err != nil {
    69  		panic(err)
    70  	}
    71  
    72  	slogger := enrichLoggerWithCommitID(logger.Named(fallbackLoggerName))
    73  	slogger.Warnw("Failed to parse logging config - using default zap production config", zap.Error(err))
    74  	return slogger, loggingCfg.Level
    75  }
    76  
    77  func enrichLoggerWithCommitID(logger *zap.Logger) *zap.SugaredLogger {
    78  	revision := changeset.Get()
    79  	if revision == changeset.Unknown {
    80  		logger.Info("Unable to read vcs.revision from binary")
    81  		return logger.Sugar()
    82  	}
    83  
    84  	// Enrich logs with the components git revision.
    85  	return logger.With(zap.String(logkey.Commit, revision)).Sugar()
    86  }
    87  
    88  // NewLoggerFromConfig creates a logger using the provided Config
    89  func NewLoggerFromConfig(config *Config, name string, opts ...zap.Option) (*zap.SugaredLogger, zap.AtomicLevel) {
    90  	var componentLvl string
    91  	if lvl, defined := config.LoggingLevel[name]; defined {
    92  		componentLvl = lvl.String()
    93  	}
    94  
    95  	logger, level := NewLogger(config.LoggingConfig, componentLvl, opts...)
    96  	return logger.Named(name), level
    97  }
    98  
    99  func newLoggerFromConfig(configJSON, levelOverride string, opts []zap.Option) (*zap.Logger, zap.AtomicLevel, error) {
   100  	loggingCfg, err := zapConfigFromJSON(configJSON)
   101  	if err != nil {
   102  		return nil, zap.AtomicLevel{}, err
   103  	}
   104  
   105  	if levelOverride != "" {
   106  		if level, err := levelFromString(levelOverride); err == nil {
   107  			loggingCfg.Level = zap.NewAtomicLevelAt(*level)
   108  		}
   109  	}
   110  
   111  	logger, err := loggingCfg.Build(opts...)
   112  	if err != nil {
   113  		return nil, zap.AtomicLevel{}, err
   114  	}
   115  
   116  	logger.Debug("Successfully created the logger.")
   117  	logger.Debug("Logging level set to: " + loggingCfg.Level.String())
   118  	return logger, loggingCfg.Level, nil
   119  }
   120  
   121  func zapConfigFromJSON(configJSON string) (*zap.Config, error) {
   122  	loggingCfg := stackdriverConfig()
   123  
   124  	if configJSON != "" {
   125  		if err := json.Unmarshal([]byte(configJSON), &loggingCfg); err != nil {
   126  			return nil, err
   127  		}
   128  	}
   129  	return &loggingCfg, nil
   130  }
   131  
   132  // Config contains the configuration defined in the logging ConfigMap.
   133  // +k8s:deepcopy-gen=true
   134  type Config struct {
   135  	LoggingConfig string
   136  	LoggingLevel  map[string]zapcore.Level
   137  }
   138  
   139  type lcfg struct{}
   140  
   141  // WithConfig associates a logging configuration with the context.
   142  func WithConfig(ctx context.Context, cfg *Config) context.Context {
   143  	return context.WithValue(ctx, lcfg{}, cfg)
   144  }
   145  
   146  // GetConfig gets the logging config from the provided context.
   147  func GetConfig(ctx context.Context) *Config {
   148  	untyped := ctx.Value(lcfg{})
   149  	if untyped == nil {
   150  		return nil
   151  	}
   152  	return untyped.(*Config)
   153  }
   154  
   155  func defaultConfig() *Config {
   156  	return &Config{
   157  		LoggingLevel: make(map[string]zapcore.Level),
   158  	}
   159  }
   160  
   161  // NewConfigFromMap creates a LoggingConfig from the supplied map,
   162  // expecting the given list of components.
   163  func NewConfigFromMap(data map[string]string) (*Config, error) {
   164  	lc := defaultConfig()
   165  	if zlc, ok := data[loggerConfigKey]; ok {
   166  		lc.LoggingConfig = zlc
   167  	}
   168  
   169  	for k, v := range data {
   170  		if component := strings.TrimPrefix(k, "loglevel."); component != k && component != "" {
   171  			if len(v) > 0 {
   172  				level, err := levelFromString(v)
   173  				if err != nil {
   174  					return nil, err
   175  				}
   176  				lc.LoggingLevel[component] = *level
   177  			}
   178  		}
   179  	}
   180  	return lc, nil
   181  }
   182  
   183  // NewConfigFromConfigMap creates a Config from the supplied ConfigMap,
   184  // expecting the given list of components.
   185  func NewConfigFromConfigMap(configMap *corev1.ConfigMap) (*Config, error) {
   186  	return NewConfigFromMap(configMap.Data)
   187  }
   188  
   189  func levelFromString(level string) (*zapcore.Level, error) {
   190  	var zapLevel zapcore.Level
   191  	if err := zapLevel.UnmarshalText([]byte(level)); err != nil {
   192  		return nil, fmt.Errorf("invalid logging level: %v", level)
   193  	}
   194  	return &zapLevel, nil
   195  }
   196  
   197  // UpdateLevelFromConfigMap returns a helper func that can be used to update the logging level
   198  // when a config map is updated
   199  func UpdateLevelFromConfigMap(logger *zap.SugaredLogger, atomicLevel zap.AtomicLevel,
   200  	levelKey string,
   201  ) func(configMap *corev1.ConfigMap) {
   202  	return func(configMap *corev1.ConfigMap) {
   203  		config, err := NewConfigFromConfigMap(configMap)
   204  		if err != nil {
   205  			logger.Errorw("Failed to parse the logging configmap. Previous config map will be used.", zap.Error(err))
   206  			return
   207  		}
   208  
   209  		level, defined := config.LoggingLevel[levelKey]
   210  		if !defined {
   211  			// reset to global level
   212  			loggingCfg, err := zapConfigFromJSON(config.LoggingConfig)
   213  			switch {
   214  			case errors.Is(err, errEmptyLoggerConfig):
   215  				level = zap.NewAtomicLevel().Level()
   216  			case err != nil:
   217  				logger.Errorw("Failed to parse logger configuration. Previous log level retained for "+levelKey,
   218  					zap.Error(err))
   219  				return
   220  			default:
   221  				level = loggingCfg.Level.Level()
   222  			}
   223  		}
   224  
   225  		if atomicLevel.Level() != level {
   226  			logger.Infof("Updating logging level for %v from %v to %v.", levelKey, atomicLevel.Level(), level)
   227  			atomicLevel.SetLevel(level)
   228  		}
   229  	}
   230  }
   231  
   232  // ConfigMapName gets the name of the logging ConfigMap
   233  func ConfigMapName() string {
   234  	if cm := os.Getenv(configMapNameEnv); cm != "" {
   235  		return cm
   236  	}
   237  	return "config-logging"
   238  }
   239  
   240  // JSONToConfig converts a JSON string of a Config.
   241  // Always returns a non-nil Config.
   242  func JSONToConfig(jsonCfg string) (*Config, error) {
   243  	if jsonCfg == "" {
   244  		return nil, errEmptyJSONLogginString
   245  	}
   246  
   247  	var configMap map[string]string
   248  	if err := json.Unmarshal([]byte(jsonCfg), &configMap); err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	cfg, err := NewConfigFromMap(configMap)
   253  	if err != nil {
   254  		// Get the default config from logging package.
   255  		return NewConfigFromConfigMap(nil)
   256  	}
   257  	return cfg, nil
   258  }
   259  
   260  // ConfigToJSON  converts a Config to a JSON string.
   261  func ConfigToJSON(cfg *Config) (string, error) {
   262  	if cfg == nil || cfg.LoggingConfig == "" {
   263  		return "", nil
   264  	}
   265  
   266  	jsonCfg, err := json.Marshal(map[string]string{
   267  		loggerConfigKey: cfg.LoggingConfig,
   268  	})
   269  	return string(jsonCfg), err
   270  }
   271  
   272  func stackdriverConfig() zap.Config {
   273  	cfg := zapdriver.NewProductionConfig()
   274  	cfg.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder
   275  	return cfg
   276  }