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  }