github.com/thanos-io/thanos@v0.32.5/pkg/logging/grpc.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package logging 5 6 import ( 7 "fmt" 8 "math/rand" 9 "sort" 10 "strings" 11 "time" 12 13 "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors" 14 grpc_logging "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" 15 "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/tags" 16 "github.com/oklog/ulid" 17 "google.golang.org/grpc/status" 18 "gopkg.in/yaml.v2" 19 ) 20 21 // NewRequestConfig parses the string into a req logging config structure. 22 // Raise an error if unmarshalling is not possible, or values are not valid. 23 func NewRequestConfig(configYAML []byte) (*RequestConfig, error) { 24 reqLogConfig := &RequestConfig{} 25 if err := yaml.UnmarshalStrict(configYAML, reqLogConfig); err != nil { 26 return nil, err 27 } 28 return reqLogConfig, nil 29 } 30 31 // checkOptionsConfigEmpty checks if the OptionsConfig struct is empty and valid. 32 // If invalid combination is present, return an error. 33 func checkOptionsConfigEmpty(optcfg OptionsConfig) (bool, error) { 34 if optcfg.Level == "" && !optcfg.Decision.LogEnd && !optcfg.Decision.LogStart { 35 return true, nil 36 } 37 if optcfg.Level == "" && (optcfg.Decision.LogStart || optcfg.Decision.LogEnd) { 38 return false, fmt.Errorf("level field is empty") 39 } 40 return false, nil 41 } 42 43 // fillGlobalOptionConfig configures all the method to have global config for logging. 44 func fillGlobalOptionConfig(reqLogConfig *RequestConfig, isgRPC bool) (string, bool, bool, error) { 45 globalLevel := "ERROR" 46 globalStart, globalEnd := false, false 47 48 globalOptionConfig := reqLogConfig.Options 49 isEmpty, err := checkOptionsConfigEmpty(globalOptionConfig) 50 51 // If the decision for logging is enabled with empty level field. 52 if err != nil { 53 return "", false, false, err 54 } 55 if !isEmpty { 56 globalLevel = globalOptionConfig.Level 57 globalStart = globalOptionConfig.Decision.LogStart 58 globalEnd = globalOptionConfig.Decision.LogEnd 59 } 60 61 protocolOptionConfig := reqLogConfig.HTTP.Options 62 if isgRPC { 63 // gRPC config overrides the global config. 64 protocolOptionConfig = reqLogConfig.GRPC.Options 65 } 66 67 isEmpty, err = checkOptionsConfigEmpty(protocolOptionConfig) 68 // If the decision for logging is enabled with empty level field. 69 if err != nil { 70 return "", false, false, err 71 } 72 73 if !isEmpty { 74 globalLevel = protocolOptionConfig.Level 75 globalStart = protocolOptionConfig.Decision.LogStart 76 globalEnd = protocolOptionConfig.Decision.LogEnd 77 } 78 return globalLevel, globalStart, globalEnd, nil 79 } 80 81 // getGRPCLoggingOption returns the logging ENUM based on logStart and logEnd values. 82 func getGRPCLoggingOption(logStart, logEnd bool) (grpc_logging.Decision, error) { 83 if !logStart && !logEnd { 84 return grpc_logging.NoLogCall, nil 85 } 86 if !logStart && logEnd { 87 return grpc_logging.LogFinishCall, nil 88 } 89 if logStart && logEnd { 90 return grpc_logging.LogStartAndFinishCall, nil 91 } 92 return -1, fmt.Errorf("log start call is not supported") 93 } 94 95 // validateLevel validates the list of level entries. 96 // Raise an error if empty or log level not in uppercase. 97 func validateLevel(level string) error { 98 if level == "" { 99 return fmt.Errorf("level field in YAML file is empty") 100 } 101 if level == "INFO" || level == "DEBUG" || level == "ERROR" || level == "WARNING" { 102 return nil 103 } 104 return fmt.Errorf("the format of level is invalid. Expected INFO/DEBUG/ERROR/WARNING, got this %v", level) 105 } 106 107 // NewGRPCOption adds in the config options and returns tags for logging middleware. 108 func NewGRPCOption(configYAML []byte) ([]tags.Option, []grpc_logging.Option, error) { 109 110 // Configure tagOpts and logOpts. 111 tagOpts := []tags.Option{ 112 tags.WithFieldExtractor(func(_ string, req interface{}) map[string]string { 113 tagMap := tags.TagBasedRequestFieldExtractor("request-id")("", req) 114 // If a request-id exists for a given request. 115 if tagMap != nil { 116 if _, ok := tagMap["request-id"]; ok { 117 return tagMap 118 } 119 } 120 entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0) 121 reqID := ulid.MustNew(ulid.Timestamp(time.Now()), entropy) 122 tagMap = make(map[string]string) 123 tagMap["request-id"] = reqID.String() 124 return tagMap 125 }), 126 } 127 logOpts := []grpc_logging.Option{ 128 grpc_logging.WithDecider(func(_ string, _ error) grpc_logging.Decision { 129 return grpc_logging.NoLogCall 130 }), 131 grpc_logging.WithLevels(DefaultCodeToLevelGRPC), 132 } 133 134 // Unmarshal YAML. 135 // if req logging is disabled. 136 if len(configYAML) == 0 { 137 return tagOpts, logOpts, nil 138 } 139 140 reqLogConfig, err := NewRequestConfig(configYAML) 141 // If unmarshalling is an issue. 142 if err != nil { 143 return tagOpts, logOpts, err 144 } 145 146 globalLevel, globalStart, globalEnd, err := fillGlobalOptionConfig(reqLogConfig, true) 147 // If global options have invalid entries. 148 if err != nil { 149 return tagOpts, logOpts, err 150 } 151 152 // If the level entry does not matches our entries. 153 if err := validateLevel(globalLevel); err != nil { 154 return tagOpts, logOpts, err 155 } 156 157 // If the combination is valid, use them, otherwise return error. 158 reqLogDecision, err := getGRPCLoggingOption(globalStart, globalEnd) 159 if err != nil { 160 return tagOpts, logOpts, err 161 } 162 163 if len(reqLogConfig.GRPC.Config) == 0 { 164 logOpts = []grpc_logging.Option{ 165 grpc_logging.WithDecider(func(_ string, err error) grpc_logging.Decision { 166 167 runtimeLevel := grpc_logging.DefaultServerCodeToLevel(status.Code(err)) 168 for _, lvl := range MapAllowedLevels[globalLevel] { 169 if string(runtimeLevel) == strings.ToLower(lvl) { 170 return reqLogDecision 171 } 172 } 173 return grpc_logging.NoLogCall 174 }), 175 grpc_logging.WithLevels(DefaultCodeToLevelGRPC), 176 } 177 return tagOpts, logOpts, nil 178 } 179 180 logOpts = []grpc_logging.Option{ 181 grpc_logging.WithLevels(DefaultCodeToLevelGRPC), 182 } 183 184 methodNameSlice := []string{} 185 186 for _, eachConfig := range reqLogConfig.GRPC.Config { 187 eachConfigMethodName := interceptors.FullMethod(eachConfig.Service, eachConfig.Method) 188 methodNameSlice = append(methodNameSlice, eachConfigMethodName) 189 } 190 191 logOpts = append(logOpts, []grpc_logging.Option{ 192 grpc_logging.WithDecider(func(runtimeMethodName string, err error) grpc_logging.Decision { 193 194 idx := sort.SearchStrings(methodNameSlice, runtimeMethodName) 195 if idx < len(methodNameSlice) && methodNameSlice[idx] == runtimeMethodName { 196 runtimeLevel := grpc_logging.DefaultServerCodeToLevel(status.Code(err)) 197 for _, lvl := range MapAllowedLevels[globalLevel] { 198 if string(runtimeLevel) == strings.ToLower(lvl) { 199 return reqLogDecision 200 } 201 } 202 } 203 return grpc_logging.NoLogCall 204 }), 205 }...) 206 return tagOpts, logOpts, nil 207 }