fix: metric cardinality et al (#315)

Carlos Alexandro Becker created

Change summary

server/cmd/cmd.go     | 23 +++++++++++--
server/ssh/session.go | 32 +++++++++++++------
server/ssh/ssh.go     | 73 ++++++++++++++++++++++++++++++++------------
3 files changed, 94 insertions(+), 34 deletions(-)

Detailed changes

server/cmd/cmd.go 🔗

@@ -15,13 +15,20 @@ import (
 	"github.com/charmbracelet/soft-serve/server/utils"
 	"github.com/charmbracelet/ssh"
 	"github.com/charmbracelet/wish"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
 	"github.com/spf13/cobra"
 )
 
-var (
-	// sessionCtxKey is the key for the session in the context.
-	sessionCtxKey = &struct{ string }{"session"}
-)
+// sessionCtxKey is the key for the session in the context.
+var sessionCtxKey = &struct{ string }{"session"}
+
+var cliCommandCounter = promauto.NewCounterVec(prometheus.CounterOpts{
+	Namespace: "soft_serve",
+	Subsystem: "cli",
+	Name:      "commands_total",
+	Help:      "Total times each command was called",
+}, []string{"command"})
 
 var templateFuncs = template.FuncMap{
 	"trim":                    strings.TrimSpace,
@@ -75,8 +82,16 @@ func rpad(s string, padding int) string {
 	return fmt.Sprintf(template, s)
 }
 
+func cmdName(args []string) string {
+	if len(args) == 0 {
+		return ""
+	}
+	return args[0]
+}
+
 // rootCommand is the root command for the server.
 func rootCommand(cfg *config.Config, s ssh.Session) *cobra.Command {
+	cliCommandCounter.WithLabelValues(cmdName(s.Command())).Inc()
 	rootCmd := &cobra.Command{
 		Short:        "Soft Serve is a self-hostable Git server for the command line.",
 		SilenceUsage: true,

server/ssh/session.go 🔗

@@ -2,6 +2,7 @@ package ssh
 
 import (
 	"strings"
+	"time"
 
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/log"
@@ -19,19 +20,23 @@ import (
 	"github.com/prometheus/client_golang/prometheus/promauto"
 )
 
-var (
-	tuiSessionCounter = promauto.NewCounterVec(prometheus.CounterOpts{
-		Namespace: "soft_serve",
-		Subsystem: "ssh",
-		Name:      "tui_session_total",
-		Help:      "The total number of TUI sessions",
-	}, []string{"key", "user", "repo", "term"})
-)
+var tuiSessionCounter = promauto.NewCounterVec(prometheus.CounterOpts{
+	Namespace: "soft_serve",
+	Subsystem: "ssh",
+	Name:      "tui_session_total",
+	Help:      "The total number of TUI sessions",
+}, []string{"repo", "term"})
+
+var tuiSessionDuration = promauto.NewCounterVec(prometheus.CounterOpts{
+	Namespace: "soft_serve",
+	Subsystem: "ssh",
+	Name:      "tui_session_seconds_total",
+	Help:      "The total number of TUI sessions",
+}, []string{"repo", "term"})
 
 // SessionHandler is the soft-serve bubbletea ssh session handler.
 func SessionHandler(cfg *config.Config) bm.ProgramHandler {
 	return func(s ssh.Session) *tea.Program {
-		ak := backend.MarshalAuthorizedKey(s.PublicKey())
 		pty, _, active := s.Pty()
 		if !active {
 			return nil
@@ -61,9 +66,16 @@ func SessionHandler(cfg *config.Config) bm.ProgramHandler {
 			tea.WithAltScreen(),
 			tea.WithoutCatchPanics(),
 			tea.WithMouseCellMotion(),
+			tea.WithContext(ctx),
 		)
 
-		tuiSessionCounter.WithLabelValues(ak, s.User(), initialRepo, pty.Term).Inc()
+		tuiSessionCounter.WithLabelValues(initialRepo, pty.Term).Inc()
+
+		start := time.Now()
+		go func() {
+			<-ctx.Done()
+			tuiSessionDuration.WithLabelValues(initialRepo, pty.Term).Add(time.Since(start).Seconds())
+		}()
 
 		return p
 	}

server/ssh/ssh.go 🔗

@@ -35,42 +35,63 @@ var (
 		Subsystem: "ssh",
 		Name:      "public_key_auth_total",
 		Help:      "The total number of public key auth requests",
-	}, []string{"key", "user", "allowed"})
+	}, []string{"allowed"})
 
 	keyboardInteractiveCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 		Namespace: "soft_serve",
 		Subsystem: "ssh",
 		Name:      "keyboard_interactive_auth_total",
 		Help:      "The total number of keyboard interactive auth requests",
-	}, []string{"user", "allowed"})
+	}, []string{"allowed"})
 
 	uploadPackCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 		Namespace: "soft_serve",
-		Subsystem: "ssh",
-		Name:      "git_upload_pack_total",
+		Subsystem: "git",
+		Name:      "upload_pack_total",
 		Help:      "The total number of git-upload-pack requests",
-	}, []string{"key", "user", "repo"})
+	}, []string{"repo"})
 
 	receivePackCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 		Namespace: "soft_serve",
-		Subsystem: "ssh",
-		Name:      "git_receive_pack_total",
+		Subsystem: "git",
+		Name:      "receive_pack_total",
 		Help:      "The total number of git-receive-pack requests",
-	}, []string{"key", "user", "repo"})
+	}, []string{"repo"})
 
 	uploadArchiveCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 		Namespace: "soft_serve",
-		Subsystem: "ssh",
-		Name:      "git_upload_archive_total",
+		Subsystem: "git",
+		Name:      "upload_archive_total",
 		Help:      "The total number of git-upload-archive requests",
-	}, []string{"key", "user", "repo"})
+	}, []string{"repo"})
+
+	uploadPackSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
+		Namespace: "soft_serve",
+		Subsystem: "git",
+		Name:      "upload_pack_seconds_total",
+		Help:      "The total time spent on git-upload-pack requests",
+	}, []string{"repo"})
+
+	receivePackSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
+		Namespace: "soft_serve",
+		Subsystem: "git",
+		Name:      "receive_pack_seconds_total",
+		Help:      "The total time spent on git-receive-pack requests",
+	}, []string{"repo"})
+
+	uploadArchiveSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
+		Namespace: "soft_serve",
+		Subsystem: "git",
+		Name:      "upload_archive_seconds_total",
+		Help:      "The total time spent on git-upload-archive requests",
+	}, []string{"repo"})
 
 	createRepoCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 		Namespace: "soft_serve",
 		Subsystem: "ssh",
 		Name:      "create_repo_total",
 		Help:      "The total number of create repo requests",
-	}, []string{"key", "user", "repo"})
+	}, []string{"repo"})
 )
 
 // SSHServer is a SSH server that implements the git protocol.
@@ -168,7 +189,7 @@ func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) (allowed
 
 	ak := backend.MarshalAuthorizedKey(pk)
 	defer func(allowed *bool) {
-		publicKeyCounter.WithLabelValues(ak, ctx.User(), strconv.FormatBool(*allowed)).Inc()
+		publicKeyCounter.WithLabelValues(strconv.FormatBool(*allowed)).Inc()
 	}(&allowed)
 
 	ac := s.cfg.Backend.AccessLevelByPublicKey("", pk)
@@ -181,7 +202,7 @@ func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) (allowed
 // This is used after all public key authentication has failed.
 func (s *SSHServer) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.KeyboardInteractiveChallenge) bool {
 	ac := s.cfg.Backend.AllowKeyless()
-	keyboardInteractiveCounter.WithLabelValues(ctx.User(), strconv.FormatBool(ac)).Inc()
+	keyboardInteractiveCounter.WithLabelValues(strconv.FormatBool(ac)).Inc()
 	return ac
 }
 
@@ -194,6 +215,7 @@ func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 	return func(sh ssh.Handler) ssh.Handler {
 		return func(s ssh.Session) {
 			func() {
+				start := time.Now()
 				cmd := s.Command()
 				ctx := s.Context()
 				be := ss.be.WithContext(ctx)
@@ -224,6 +246,10 @@ func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 					repoDir := filepath.Join(reposDir, repo)
 					switch gc {
 					case git.ReceivePackBin:
+						receivePackCounter.WithLabelValues(name).Inc()
+						defer func() {
+							receivePackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
+						}()
 						if access < backend.ReadWriteAccess {
 							sshFatal(s, git.ErrNotAuthed)
 							return
@@ -234,12 +260,11 @@ func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 								sshFatal(s, err)
 								return
 							}
-							createRepoCounter.WithLabelValues(ak, s.User(), name).Inc()
+							createRepoCounter.WithLabelValues(name).Inc()
 						}
 						if err := git.ReceivePack(s.Context(), s, s, s.Stderr(), repoDir, envs...); err != nil {
 							sshFatal(s, git.ErrSystemMalfunction)
 						}
-						receivePackCounter.WithLabelValues(ak, s.User(), name).Inc()
 						return
 					case git.UploadPackBin, git.UploadArchiveBin:
 						if access < backend.ReadOnlyAccess {
@@ -248,10 +273,19 @@ func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 						}
 
 						gitPack := git.UploadPack
-						counter := uploadPackCounter
-						if gc == git.UploadArchiveBin {
+						switch gc {
+						case git.UploadArchiveBin:
 							gitPack = git.UploadArchive
-							counter = uploadArchiveCounter
+							uploadArchiveCounter.WithLabelValues(name).Inc()
+							defer func() {
+								uploadArchiveSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
+							}()
+						default:
+							uploadPackCounter.WithLabelValues(name).Inc()
+							defer func() {
+								uploadPackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
+							}()
+
 						}
 
 						err := gitPack(ctx, s, s, s.Stderr(), repoDir, envs...)
@@ -261,7 +295,6 @@ func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
 							sshFatal(s, git.ErrSystemMalfunction)
 						}
 
-						counter.WithLabelValues(ak, s.User(), name).Inc()
 					}
 				}
 			}()