router/core/request_context_fields.go (215 lines of code) (raw):

package core import ( "fmt" "github.com/go-chi/chi/v5/middleware" "github.com/wundergraph/cosmo/router/internal/requestlogger" "github.com/wundergraph/cosmo/router/pkg/config" "github.com/wundergraph/cosmo/router/pkg/logging" "go.uber.org/zap" "go.uber.org/zap/zapcore" "net/http" "strconv" "time" ) // Context field names used to expose information about the operation being executed. const ( ContextFieldOperationName = "operation_name" ContextFieldOperationHash = "operation_hash" ContextFieldOperationType = "operation_type" ContextFieldOperationServices = "operation_service_names" ContextFieldGraphQLErrorCodes = "graphql_error_codes" ContextFieldGraphQLErrorServices = "graphql_error_service_names" ContextFieldOperationParsingTime = "operation_parsing_time" ContextFieldOperationValidationTime = "operation_validation_time" ContextFieldOperationPlanningTime = "operation_planning_time" ContextFieldOperationNormalizationTime = "operation_normalization_time" ContextFieldPersistedOperationSha256 = "persisted_operation_sha256" ContextFieldOperationSha256 = "operation_sha256" ContextFieldResponseErrorMessage = "response_error_message" ContextFieldRequestError = "request_error" ) // Helper functions to create zap fields for custom attributes. func NewExpressionLogField(val any, key string, defaultValue any) zap.Field { // Depending on the condition exprlang will dereference a pointer or a non pointer type // of the error (if an error is existing), thus if the method receiver is of the pointer // type, the Error() wont be printed to the output // By wrapping all errors in a common type we can always unwrap it (some types wont be exported // like errors.joinErrors for example), and ensure its Error() function is then called if assertVal, ok := val.(ExprWrapError); ok { val = &assertVal } if v := val; v != "" { return zap.Any(key, v) } else if defaultValue != "" { return zap.Any(key, defaultValue) } return zap.Skip() } func NewStringLogField(val string, attribute config.CustomAttribute) zap.Field { if v := val; v != "" { return zap.String(attribute.Key, v) } else if attribute.Default != "" { return zap.String(attribute.Key, attribute.Default) } return zap.Skip() } func NewBoolLogField(val bool, attribute config.CustomAttribute) zap.Field { if val { return zap.Bool(attribute.Key, val) } return zap.Skip() } func NewStringSliceLogField(val []string, attribute config.CustomAttribute) zap.Field { if v := val; len(v) > 0 { return zap.Strings(attribute.Key, v) } else if attribute.Default != "" { return zap.String(attribute.Key, attribute.Default) } return zap.Skip() } func NewDurationLogField(val time.Duration, attribute config.CustomAttribute) zap.Field { if v := val; v > 0 { return zap.Duration(attribute.Key, v) } else if attribute.Default != "" { if v, err := strconv.ParseFloat(attribute.Default, 64); err == nil { return zap.Duration(attribute.Key, time.Duration(v)) } } return zap.Skip() } func RouterAccessLogsFieldHandler( logger *zap.Logger, attributes []config.CustomAttribute, exprAttributes []requestlogger.ExpressionAttribute, passedErr any, request *http.Request, responseHeader *http.Header, ) []zapcore.Field { resFields := make([]zapcore.Field, 0, len(attributes)) reqContext, resFields := processRequestIDField(request, resFields) resFields = processCustomAttributes(attributes, responseHeader, resFields, request, reqContext, passedErr) resFields = processExpressionAttributes(logger, exprAttributes, reqContext, resFields) return resFields } func SubgraphAccessLogsFieldHandler( _ *zap.Logger, attributes []config.CustomAttribute, _ []requestlogger.ExpressionAttribute, passedErr any, request *http.Request, responseHeader *http.Header, ) []zapcore.Field { resFields := make([]zapcore.Field, 0, len(attributes)) reqContext, resFields := processRequestIDField(request, resFields) resFields = processCustomAttributes(attributes, responseHeader, resFields, request, reqContext, passedErr) return resFields } func processRequestIDField(request *http.Request, resFields []zapcore.Field) (*requestContext, []zapcore.Field) { var reqContext *requestContext if request == nil { return reqContext, resFields } reqContext = getRequestContext(request.Context()) resFields = append(resFields, logging.WithRequestID(middleware.GetReqID(request.Context()))) return reqContext, resFields } func processExpressionAttributes(logger *zap.Logger, exprAttributes []requestlogger.ExpressionAttribute, reqContext *requestContext, resFields []zapcore.Field) []zapcore.Field { // If the request context was processed as nil (e.g. :- request was nil in the caller) // do not proceed to process exprAttributes if reqContext == nil { return resFields } for _, exprField := range exprAttributes { result, err := reqContext.ResolveAnyExpressionWithWrappedError(exprField.Expr) if err != nil { logger.Error("unable to process expression for access logs", zap.String("fieldKey", exprField.Key), zap.Error(err)) continue } resFields = append(resFields, NewExpressionLogField(result, exprField.Key, exprField.Default)) } return resFields } func processCustomAttributes( attributes []config.CustomAttribute, responseHeader *http.Header, resFields []zapcore.Field, request *http.Request, reqContext *requestContext, passedErr any, ) []zapcore.Field { for _, field := range attributes { if field.ValueFrom != nil && field.ValueFrom.ResponseHeader != "" && responseHeader != nil { resFields = append(resFields, NewStringLogField(responseHeader.Get(field.ValueFrom.ResponseHeader), field)) } else if field.ValueFrom != nil && field.ValueFrom.RequestHeader != "" && request != nil { resFields = append(resFields, NewStringLogField(request.Header.Get(field.ValueFrom.RequestHeader), field)) } else if field.ValueFrom != nil && field.ValueFrom.ContextField != "" { if v := GetLogFieldFromCustomAttribute(field, reqContext, passedErr); v != zap.Skip() { resFields = append(resFields, v) } } else if field.Default != "" { resFields = append(resFields, NewStringLogField(field.Default, field)) } } return resFields } func GetLogFieldFromCustomAttribute(field config.CustomAttribute, req *requestContext, err any) zap.Field { val := getCustomDynamicAttributeValue(field.ValueFrom, req, err) switch v := val.(type) { case string: return NewStringLogField(v, field) case bool: return NewBoolLogField(v, field) case []string: return NewStringSliceLogField(v, field) case time.Duration: return NewDurationLogField(v, field) } return zap.Skip() } func getCustomDynamicAttributeValue( attribute *config.CustomDynamicAttribute, reqContext *requestContext, err any, ) interface{} { if attribute == nil || attribute.ContextField == "" { return "" } if reqContext == nil { // If the request context is nil, we can only return the error state. if attribute.ContextField == ContextFieldRequestError { return err != nil } else if attribute.ContextField == ContextFieldResponseErrorMessage && err != nil { return fmt.Sprintf("%v", err) } return "" } switch attribute.ContextField { case ContextFieldRequestError: return err != nil || reqContext.error != nil case ContextFieldOperationName: return reqContext.operation.Name() case ContextFieldOperationType: return reqContext.operation.Type() case ContextFieldOperationPlanningTime: return reqContext.operation.planningTime case ContextFieldOperationNormalizationTime: return reqContext.operation.normalizationTime case ContextFieldOperationParsingTime: return reqContext.operation.parsingTime case ContextFieldOperationValidationTime: return reqContext.operation.validationTime case ContextFieldOperationSha256: return reqContext.operation.sha256Hash case ContextFieldOperationHash: if reqContext.operation.hash != 0 { return strconv.FormatUint(reqContext.operation.hash, 10) } return reqContext.operation.Hash() case ContextFieldPersistedOperationSha256: return reqContext.operation.persistedID case ContextFieldResponseErrorMessage: if err != nil { return fmt.Sprintf("%v", err) } if reqContext.error != nil { return reqContext.error.Error() } case ContextFieldOperationServices: return reqContext.dataSourceNames case ContextFieldGraphQLErrorServices: return reqContext.graphQLErrorServices case ContextFieldGraphQLErrorCodes: return reqContext.graphQLErrorCodes } return "" }