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  }