diff --git a/cmd/cli/app/history/history_list.go b/cmd/cli/app/history/history_list.go index 42d49422d3..8e979c03a0 100644 --- a/cmd/cli/app/history/history_list.go +++ b/cmd/cli/app/history/history_list.go @@ -22,9 +22,11 @@ import ( "strings" "time" + "github.com/google/uuid" "github.com/spf13/cobra" "github.com/spf13/viper" "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/timestamppb" "github.com/stacklok/minder/cmd/cli/app" @@ -116,7 +118,12 @@ func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc req.To = timestamppb.New(to) } - resp, err := client.ListEvaluationHistory(ctx, req) + rID := uuid.New().String() + md := metadata.Pairs("request-id", rID) + ctx = metadata.NewOutgoingContext(ctx, md) + + var header, trailer metadata.MD + resp, err := client.ListEvaluationHistory(ctx, req, grpc.Header(&header), grpc.Trailer(&trailer)) if err != nil { return cli.MessageAndError("Error getting profile status", err) } diff --git a/internal/controlplane/server.go b/internal/controlplane/server.go index c9a172eeab..5c32bd640a 100644 --- a/internal/controlplane/server.go +++ b/internal/controlplane/server.go @@ -247,6 +247,7 @@ func (s *Server) StartGRPCServer(ctx context.Context) error { // TODO: this has no test coverage! util.SanitizingInterceptor(), logger.Interceptor(s.cfg.LoggingConfig), + logger.RequestIDInterceptor(), TokenValidationInterceptor, EntityContextProjectInterceptor, ProjectAuthorizationInterceptor, diff --git a/internal/logger/logging_interceptor.go b/internal/logger/logging_interceptor.go index ad76d42631..e1c6a61bcd 100644 --- a/internal/logger/logging_interceptor.go +++ b/internal/logger/logging_interceptor.go @@ -21,6 +21,7 @@ import ( "path" "time" + "github.com/google/uuid" "github.com/rs/zerolog" "google.golang.org/grpc" "google.golang.org/grpc/metadata" @@ -101,3 +102,45 @@ func Interceptor(cfg config.LoggingConfig) grpc.UnaryServerInterceptor { return resp, err } } + +// RequestIDInterceptor traces request ids. +// +// It tries to use the request id from the request context, creating a +// new one if that is missing. It also sends back in the trailer the +// request id, ensuring that the client receives it. +func RequestIDInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + rID := maybeGetRequestID(ctx) + ctx = zerolog.Ctx(ctx).With().Str("request_id", rID).Logger().WithContext(ctx) + + resp, err := handler(ctx, req) + + if err := grpc.SetTrailer(ctx, metadata.Pairs("request-id", rID)); err != nil { + zerolog.Ctx(ctx).Trace().Err(err).Msg("unable to attach request id to trailer") + } + + return resp, err + } +} + +func maybeGetRequestID(ctx context.Context) string { + var rID string + if md, ok := metadata.FromIncomingContext(ctx); ok { + if rIDs, ok := md["request-id"]; ok { + if len(rIDs) != 0 { + rID = rIDs[0] + } + } + } + + if rID == "" { + return uuid.New().String() + } + + if _, err := uuid.Parse(rID); err != nil { + zerolog.Ctx(ctx).Trace().Err(err).Msg("request id is not valid uuid") + return uuid.New().String() + } + + return rID +}