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 }