package grpc import ( "context" "log/slog" "time" "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/logger" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" ) // LoggingUnaryInterceptor logs one completion record for each unary gRPC call. func LoggingUnaryInterceptor(log *slog.Logger) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { method, ok := grpc.Method(ctx) if !ok { method = info.FullMethod } reqLog := requestLogger(ctx, log, method) ctx = logger.WithLogger(ctx, reqLog) start := time.Now() resp, err := handler(ctx, req) logCompletion(reqLog, "grpc call completed", status.Code(err), time.Since(start), err) return resp, err } } // LoggingStreamInterceptor logs stream start and completion with request-scoped fields. func LoggingStreamInterceptor(log *slog.Logger) grpc.StreamServerInterceptor { return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { reqLog := requestLogger(ss.Context(), log, info.FullMethod) reqLog.Info("grpc stream started") wrapped := &loggingServerStream{ ServerStream: ss, ctx: logger.WithLogger(ss.Context(), reqLog), } start := time.Now() err := handler(srv, wrapped) logCompletion(reqLog, "grpc stream completed", status.Code(err), time.Since(start), err) return err } } type loggingServerStream struct { grpc.ServerStream ctx context.Context } // Context returns the request-scoped context with the derived logger attached. func (s *loggingServerStream) Context() context.Context { return s.ctx } // requestLogger derives a child logger so every downstream component sees the // same gRPC method and peer metadata through context propagation. func requestLogger(ctx context.Context, log *slog.Logger, method string) *slog.Logger { peerAddr := "" if p, ok := peer.FromContext(ctx); ok && p.Addr != nil { peerAddr = p.Addr.String() } return log.With("grpc.method", method, "grpc.peer", peerAddr) } // logCompletion keeps severity consistent with gRPC status semantics so // expected client-facing errors do not look like infrastructure failures. func logCompletion(log *slog.Logger, msg string, code codes.Code, duration time.Duration, err error) { attrs := []any{ "duration_ms", duration.Milliseconds(), "grpc.code", code.String(), } if err != nil { attrs = append(attrs, "error", err.Error()) } switch code { case codes.OK: log.Info(msg, attrs...) case codes.NotFound, codes.InvalidArgument, codes.Unimplemented: log.Warn(msg, attrs...) default: log.Error(msg, attrs...) } }