github.com/minio/console@v1.4.1/pkg/logger/logger.go (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2022 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 package logger 18 19 import ( 20 "context" 21 "crypto/tls" 22 "encoding/hex" 23 "errors" 24 "fmt" 25 "go/build" 26 "net/http" 27 "path/filepath" 28 "reflect" 29 "runtime" 30 "strings" 31 "syscall" 32 "time" 33 34 "github.com/minio/pkg/v3/env" 35 36 "github.com/minio/console/pkg" 37 "github.com/minio/pkg/v3/certs" 38 39 "github.com/minio/console/pkg/logger/config" 40 "github.com/minio/console/pkg/logger/message/log" 41 "github.com/minio/highwayhash" 42 "github.com/minio/minio-go/v7/pkg/set" 43 ) 44 45 // HighwayHash key for logging in anonymous mode 46 var magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0") 47 48 // Disable disables all logging, false by default. (used for "go test") 49 var Disable = false 50 51 // Level type 52 type Level int8 53 54 // Enumerated level types 55 const ( 56 InformationLvl Level = iota + 1 57 ErrorLvl 58 FatalLvl 59 ) 60 61 var trimStrings []string 62 63 var matchingFuncNames = [...]string{ 64 "http.HandlerFunc.ServeHTTP", 65 "cmd.serverMain", 66 "cmd.StartGateway", 67 // add more here .. 68 } 69 70 func (level Level) String() string { 71 var lvlStr string 72 switch level { 73 case InformationLvl: 74 lvlStr = "INFO" 75 case ErrorLvl: 76 lvlStr = "ERROR" 77 case FatalLvl: 78 lvlStr = "FATAL" 79 } 80 return lvlStr 81 } 82 83 // quietFlag: Hide startup messages if enabled 84 // jsonFlag: Display in JSON format, if enabled 85 var ( 86 quietFlag, jsonFlag, anonFlag bool 87 // Custom function to format errors 88 errorFmtFunc func(string, error, bool) string 89 ) 90 91 // EnableQuiet - turns quiet option on. 92 func EnableQuiet() { 93 quietFlag = true 94 } 95 96 // EnableJSON - outputs logs in json format. 97 func EnableJSON() { 98 jsonFlag = true 99 quietFlag = true 100 } 101 102 // EnableAnonymous - turns anonymous flag 103 // to avoid printing sensitive information. 104 func EnableAnonymous() { 105 anonFlag = true 106 } 107 108 // IsAnonymous - returns true if anonFlag is true 109 func IsAnonymous() bool { 110 return anonFlag 111 } 112 113 // IsJSON - returns true if jsonFlag is true 114 func IsJSON() bool { 115 return jsonFlag 116 } 117 118 // IsQuiet - returns true if quietFlag is true 119 func IsQuiet() bool { 120 return quietFlag 121 } 122 123 // RegisterError registers the specified rendering function. This latter 124 // will be called for a pretty rendering of fatal errors. 125 func RegisterError(f func(string, error, bool) string) { 126 errorFmtFunc = f 127 } 128 129 // Remove any duplicates and return unique entries. 130 func uniqueEntries(paths []string) []string { 131 m := make(set.StringSet) 132 for _, p := range paths { 133 if !m.Contains(p) { 134 m.Add(p) 135 } 136 } 137 return m.ToSlice() 138 } 139 140 // Init sets the trimStrings to possible GOPATHs 141 // and GOROOT directories. Also append github.com/minio/minio 142 // This is done to clean up the filename, when stack trace is 143 // displayed when an errors happens. 144 func Init(goPath, goRoot string) { 145 var goPathList []string 146 var goRootList []string 147 var defaultgoPathList []string 148 var defaultgoRootList []string 149 pathSeperator := ":" 150 // Add all possible GOPATH paths into trimStrings 151 // Split GOPATH depending on the OS type 152 if runtime.GOOS == "windows" { 153 pathSeperator = ";" 154 } 155 156 goPathList = strings.Split(goPath, pathSeperator) 157 goRootList = strings.Split(goRoot, pathSeperator) 158 defaultgoPathList = strings.Split(build.Default.GOPATH, pathSeperator) 159 defaultgoRootList = strings.Split(build.Default.GOROOT, pathSeperator) 160 161 // Add trim string "{GOROOT}/src/" into trimStrings 162 trimStrings = []string{filepath.Join(runtime.GOROOT(), "src") + string(filepath.Separator)} 163 164 // Add all possible path from GOPATH=path1:path2...:pathN 165 // as "{path#}/src/" into trimStrings 166 for _, goPathString := range goPathList { 167 trimStrings = append(trimStrings, filepath.Join(goPathString, "src")+string(filepath.Separator)) 168 } 169 170 for _, goRootString := range goRootList { 171 trimStrings = append(trimStrings, filepath.Join(goRootString, "src")+string(filepath.Separator)) 172 } 173 174 for _, defaultgoPathString := range defaultgoPathList { 175 trimStrings = append(trimStrings, filepath.Join(defaultgoPathString, "src")+string(filepath.Separator)) 176 } 177 178 for _, defaultgoRootString := range defaultgoRootList { 179 trimStrings = append(trimStrings, filepath.Join(defaultgoRootString, "src")+string(filepath.Separator)) 180 } 181 182 // Remove duplicate entries. 183 trimStrings = uniqueEntries(trimStrings) 184 185 // Add "github.com/minio/minio" as the last to cover 186 // paths like "{GOROOT}/src/github.com/minio/minio" 187 // and "{GOPATH}/src/github.com/minio/minio" 188 trimStrings = append(trimStrings, filepath.Join("github.com", "minio", "minio")+string(filepath.Separator)) 189 } 190 191 func trimTrace(f string) string { 192 for _, trimString := range trimStrings { 193 f = strings.TrimPrefix(filepath.ToSlash(f), filepath.ToSlash(trimString)) 194 } 195 return filepath.FromSlash(f) 196 } 197 198 func getSource(level int) string { 199 pc, file, lineNumber, ok := runtime.Caller(level) 200 if ok { 201 // Clean up the common prefixes 202 file = trimTrace(file) 203 _, funcName := filepath.Split(runtime.FuncForPC(pc).Name()) 204 return fmt.Sprintf("%v:%v:%v()", file, lineNumber, funcName) 205 } 206 return "" 207 } 208 209 // getTrace method - creates and returns stack trace 210 func getTrace(traceLevel int) []string { 211 var trace []string 212 pc, file, lineNumber, ok := runtime.Caller(traceLevel) 213 214 for ok && file != "" { 215 // Clean up the common prefixes 216 file = trimTrace(file) 217 // Get the function name 218 _, funcName := filepath.Split(runtime.FuncForPC(pc).Name()) 219 // Skip duplicate traces that start with file name, "<autogenerated>" 220 // and also skip traces with function name that starts with "runtime." 221 if !strings.HasPrefix(file, "<autogenerated>") && 222 !strings.HasPrefix(funcName, "runtime.") { 223 // Form and append a line of stack trace into a 224 // collection, 'trace', to build full stack trace 225 trace = append(trace, fmt.Sprintf("%v:%v:%v()", file, lineNumber, funcName)) 226 227 // Ignore trace logs beyond the following conditions 228 for _, name := range matchingFuncNames { 229 if funcName == name { 230 return trace 231 } 232 } 233 } 234 traceLevel++ 235 // Read stack trace information from PC 236 pc, file, lineNumber, ok = runtime.Caller(traceLevel) 237 } 238 return trace 239 } 240 241 // Return the highway hash of the passed string 242 func hashString(input string) string { 243 hh, _ := highwayhash.New(magicHighwayHash256Key) 244 hh.Write([]byte(input)) 245 return hex.EncodeToString(hh.Sum(nil)) 246 } 247 248 // Kind specifies the kind of errors log 249 type Kind string 250 251 const ( 252 // Minio errors 253 Minio Kind = "CONSOLE" 254 // All errors 255 All Kind = "ALL" 256 ) 257 258 // LogAlwaysIf prints a detailed errors message during 259 // the execution of the server. 260 func LogAlwaysIf(ctx context.Context, err error, errKind ...interface{}) { 261 if err == nil { 262 return 263 } 264 265 logIf(ctx, err, errKind...) 266 } 267 268 // LogIf prints a detailed errors message during 269 // the execution of the server 270 func LogIf(ctx context.Context, err error, errKind ...interface{}) { 271 if err == nil { 272 return 273 } 274 275 if errors.Is(err, context.Canceled) { 276 return 277 } 278 logIf(ctx, err, errKind...) 279 } 280 281 // logIf prints a detailed errors message during 282 // the execution of the server. 283 func logIf(ctx context.Context, err error, errKind ...interface{}) { 284 if Disable { 285 return 286 } 287 logKind := string(Minio) 288 if len(errKind) > 0 { 289 if ek, ok := errKind[0].(Kind); ok { 290 logKind = string(ek) 291 } 292 } 293 req := GetReqInfo(ctx) 294 295 if req == nil { 296 req = &ReqInfo{API: "SYSTEM"} 297 } 298 299 kv := req.GetTags() 300 tags := make(map[string]interface{}, len(kv)) 301 for _, entry := range kv { 302 tags[entry.Key] = entry.Val 303 } 304 305 // Get full stack trace 306 trace := getTrace(3) 307 308 // Get the cause for the Error 309 message := fmt.Sprintf("%v (%T)", err, err) 310 if req.DeploymentID == "" { 311 req.DeploymentID = GetGlobalDeploymentID() 312 } 313 314 entry := log.Entry{ 315 DeploymentID: req.DeploymentID, 316 Level: ErrorLvl.String(), 317 LogKind: logKind, 318 RemoteHost: req.RemoteHost, 319 Host: req.Host, 320 RequestID: req.RequestID, 321 SessionID: req.SessionID, 322 UserAgent: req.UserAgent, 323 Time: time.Now().UTC(), 324 Trace: &log.Trace{ 325 Message: message, 326 Source: trace, 327 Variables: tags, 328 }, 329 } 330 331 if anonFlag { 332 entry.SessionID = hashString(entry.SessionID) 333 entry.RemoteHost = hashString(entry.RemoteHost) 334 entry.Trace.Message = reflect.TypeOf(err).String() 335 entry.Trace.Variables = make(map[string]interface{}) 336 } 337 338 // Iterate over all logger targets to send the log entry 339 for _, t := range SystemTargets() { 340 if err := t.Send(entry, entry.LogKind); err != nil { 341 if consoleTgt != nil { 342 entry.Trace.Message = fmt.Sprintf("event(%#v) was not sent to Logger target (%#v): %#v", entry, t, err) 343 consoleTgt.Send(entry, entry.LogKind) 344 } 345 } 346 } 347 } 348 349 // ErrCritical is the value panic'd whenever CriticalIf is called. 350 var ErrCritical struct{} 351 352 // CriticalIf logs the provided errors on the console. It fails the 353 // current go-routine by causing a `panic(ErrCritical)`. 354 func CriticalIf(ctx context.Context, err error, errKind ...interface{}) { 355 if err != nil { 356 LogIf(ctx, err, errKind...) 357 panic(ErrCritical) 358 } 359 } 360 361 // FatalIf is similar to Fatal() but it ignores passed nil errors 362 func FatalIf(err error, msg string, data ...interface{}) { 363 if err == nil { 364 return 365 } 366 fatal(err, msg, data...) 367 } 368 369 func applyDynamicConfigForSubSys(ctx context.Context, transport *http.Transport, subSys string) error { 370 switch subSys { 371 case config.LoggerWebhookSubSys: 372 loggerCfg, err := LookupConfigForSubSys(config.LoggerWebhookSubSys) 373 if err != nil { 374 LogIf(ctx, fmt.Errorf("unable to load logger webhook config: %w", err)) 375 return err 376 } 377 userAgent := getUserAgent() 378 for n, l := range loggerCfg.HTTP { 379 if l.Enabled { 380 l.LogOnce = LogOnceIf 381 l.UserAgent = userAgent 382 l.Transport = NewHTTPTransportWithClientCerts(transport, l.ClientCert, l.ClientKey) 383 loggerCfg.HTTP[n] = l 384 } 385 } 386 err = UpdateSystemTargets(loggerCfg) 387 if err != nil { 388 LogIf(ctx, fmt.Errorf("unable to update logger webhook config: %w", err)) 389 return err 390 } 391 case config.AuditWebhookSubSys: 392 loggerCfg, err := LookupConfigForSubSys(config.AuditWebhookSubSys) 393 if err != nil { 394 LogIf(ctx, fmt.Errorf("unable to load audit webhook config: %w", err)) 395 return err 396 } 397 userAgent := getUserAgent() 398 for n, l := range loggerCfg.AuditWebhook { 399 if l.Enabled { 400 l.LogOnce = LogOnceIf 401 l.UserAgent = userAgent 402 l.Transport = NewHTTPTransportWithClientCerts(transport, l.ClientCert, l.ClientKey) 403 loggerCfg.AuditWebhook[n] = l 404 } 405 } 406 407 err = UpdateAuditWebhookTargets(loggerCfg) 408 if err != nil { 409 LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %w", err)) 410 return err 411 } 412 } 413 return nil 414 } 415 416 // InitializeLogger : 417 func InitializeLogger(ctx context.Context, transport *http.Transport) error { 418 err := applyDynamicConfigForSubSys(ctx, transport, config.LoggerWebhookSubSys) 419 if err != nil { 420 return err 421 } 422 err = applyDynamicConfigForSubSys(ctx, transport, config.AuditWebhookSubSys) 423 if err != nil { 424 return err 425 } 426 427 if enable, _ := config.ParseBool(env.Get(EnvLoggerJSONEnable, "")); enable { 428 EnableJSON() 429 } 430 if enable, _ := config.ParseBool(env.Get(EnvLoggerAnonymousEnable, "")); enable { 431 EnableAnonymous() 432 } 433 if enable, _ := config.ParseBool(env.Get(EnvLoggerQuietEnable, "")); enable { 434 EnableQuiet() 435 } 436 437 return nil 438 } 439 440 func getUserAgent() string { 441 userAgentParts := []string{} 442 // Helper function to concisely append a pair of strings to a 443 // the user-agent slice. 444 uaAppend := func(p, q string) { 445 userAgentParts = append(userAgentParts, p, q) 446 } 447 uaAppend("Console (", runtime.GOOS) 448 uaAppend("; ", runtime.GOARCH) 449 uaAppend(") Console/", pkg.Version) 450 uaAppend(" Console/", pkg.ReleaseTag) 451 uaAppend(" Console/", pkg.CommitID) 452 453 return strings.Join(userAgentParts, "") 454 } 455 456 // NewHTTPTransportWithClientCerts returns a new http configuration 457 // used while communicating with the cloud backends. 458 func NewHTTPTransportWithClientCerts(parentTransport *http.Transport, clientCert, clientKey string) *http.Transport { 459 transport := parentTransport.Clone() 460 if clientCert != "" && clientKey != "" { 461 ctx, cancel := context.WithCancel(context.Background()) 462 defer cancel() 463 c, err := certs.NewManager(ctx, clientCert, clientKey, tls.LoadX509KeyPair) 464 if err != nil { 465 LogIf(ctx, fmt.Errorf("failed to load client key and cert, please check your endpoint configuration: %s", 466 err.Error())) 467 } 468 if c != nil { 469 c.UpdateReloadDuration(10 * time.Second) 470 c.ReloadOnSignal(syscall.SIGHUP) // allow reloads upon SIGHUP 471 transport.TLSClientConfig.GetClientCertificate = c.GetClientCertificate 472 } 473 } 474 return transport 475 }