From 1d049ac2f0f4c2fe516636d29572f6818fe2b69b Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Fri, 20 Dec 2019 17:20:22 +0000
Subject: [PATCH] Add Prometheus instrumentation for sso server internals

Metrics cover specifically the authentication workflow.
---
 server/http.go        | 24 ++++++++++++++++++++++++
 server/login/login.go | 19 +++++++++++++++++++
 2 files changed, 43 insertions(+)

diff --git a/server/http.go b/server/http.go
index 7d293c6..f0f5282 100644
--- a/server/http.go
+++ b/server/http.go
@@ -17,6 +17,7 @@ import (
 
 	assetfs "github.com/elazarl/go-bindata-assetfs"
 	"github.com/gorilla/csrf"
+	"github.com/prometheus/client_golang/prometheus"
 	"github.com/rs/cors"
 
 	"git.autistici.org/id/auth"
@@ -219,6 +220,7 @@ func (h *Server) loginCallback(ctx context.Context, username, password string, u
 	// used to authenticate.
 	decrypted, shard, err := h.maybeUnlockKeystore(ctx, username, password, userinfo)
 	if err != nil {
+		keystoreCounter.WithLabelValues("error", shard).Inc()
 		return fmt.Errorf("failed to unlock keystore for user %s: %v", username, err)
 	}
 
@@ -229,6 +231,7 @@ func (h *Server) loginCallback(ctx context.Context, username, password string, u
 			kmsg += fmt.Sprintf(", shard %s", shard)
 		}
 		kmsg += ")"
+		keystoreCounter.WithLabelValues("ok", shard).Inc()
 	}
 	log.Printf("successful login for user %s%s", username, kmsg)
 	return nil
@@ -305,10 +308,12 @@ func (h *Server) handleGrantTicket(w http.ResponseWriter, req *http.Request) {
 	if err != nil {
 		log.Printf("auth error: %v: user=%s service=%s destination=%s nonce=%s groups=%s", err, username, service, destination, nonce, groupsStr)
 		http.Error(w, err.Error(), http.StatusBadRequest)
+		errorsCounter.WithLabelValues(service, "false").Inc()
 		return
 	}
 
 	log.Printf("authorized %s for %s (ttl=%ds)", username, service, int(ttl.Seconds()))
+	grantsCounter.WithLabelValues(service, "false").Inc()
 
 	// Record the service in the session.
 	auth.AddService(service)
@@ -377,13 +382,16 @@ func (h *Server) handleExchange(w http.ResponseWriter, req *http.Request) {
 	case err == ErrUnauthorized:
 		log.Printf("unauthorized exchange request (%s -> %s)", curService, newService)
 		http.Error(w, "Forbidden", http.StatusForbidden)
+		errorsCounter.WithLabelValues(newService, "true").Inc()
 		return
 	case err != nil:
 		log.Printf("exchange error (%s -> %s): %v", curService, newService, err)
 		http.Error(w, err.Error(), http.StatusBadRequest)
+		errorsCounter.WithLabelValues(newService, "true").Inc()
 		return
 	}
 
+	grantsCounter.WithLabelValues(newService, "true").Inc()
 	w.Header().Set("Content-Type", "text/plain")
 	io.WriteString(w, token) // nolint
 }
@@ -472,3 +480,19 @@ func sriIntegrity(uri string) template.HTML {
 	}
 	return template.HTML(fmt.Sprintf(" integrity=\"%s\"", sri))
 }
+
+// Prometheus instrumentation.
+var (
+	grantsCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "sso_grants_total",
+		Help: "Counter of ticket grants by service.",
+	}, []string{"service", "exchange"})
+	errorsCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "sso_grant_errors_total",
+		Help: "Counter of authorization errors by service.",
+	}, []string{"service", "exchange"})
+	keystoreCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "sso_keystore_unlocks_total",
+		Help: "Counter of keystore unlocks.",
+	}, []string{"status", "shard"})
+)
diff --git a/server/login/login.go b/server/login/login.go
index c7162a3..0303021 100644
--- a/server/login/login.go
+++ b/server/login/login.go
@@ -12,6 +12,7 @@ import (
 
 	"git.autistici.org/id/auth"
 	authclient "git.autistici.org/id/auth/client"
+	"github.com/prometheus/client_golang/prometheus"
 	"github.com/tstranex/u2f"
 	"go.opencensus.io/trace"
 
@@ -274,6 +275,7 @@ func (l *Login) handleLogin(w http.ResponseWriter, req *http.Request, sess *logi
 		switch resp.Status {
 		case auth.StatusOK:
 			l.loginOk(w, req, sess, password, resp.UserInfo)
+			loginCounter.WithLabelValues("ok", "password").Inc()
 			return
 		case auth.StatusInsufficientCredentials:
 			sess.Password = password
@@ -290,6 +292,7 @@ func (l *Login) handleLogin(w http.ResponseWriter, req *http.Request, sess *logi
 			return
 		}
 		env["Error"] = true
+		loginCounter.WithLabelValues("error", "password").Inc()
 	}
 
 	l.renderer.Render(w, req, "login_password.html", env)
@@ -323,8 +326,10 @@ func (l *Login) handleLoginOTP(w http.ResponseWriter, req *http.Request, sess *l
 		}
 		if resp.Status == auth.StatusOK {
 			l.loginOk(w, req, sess, sess.Password, resp.UserInfo)
+			loginCounter.WithLabelValues("ok", "otp").Inc()
 			return
 		}
+		loginCounter.WithLabelValues("error", "otp").Inc()
 		env["Error"] = true
 		sess.Failures++
 		if sess.Failures >= maxFailures {
@@ -373,8 +378,10 @@ func (l *Login) handleLoginU2F(w http.ResponseWriter, req *http.Request, sess *l
 		}
 		if resp.Status == auth.StatusOK {
 			l.loginOk(w, req, sess, sess.Password, resp.UserInfo)
+			loginCounter.WithLabelValues("ok", "u2f").Inc()
 			return
 		}
+		loginCounter.WithLabelValues("error", "u2f").Inc()
 		env["Error"] = true
 		sess.Failures++
 		if sess.Failures >= maxFailures {
@@ -419,6 +426,7 @@ func (l *Login) makeAuthRequest(w http.ResponseWriter, req *http.Request, userna
 
 	// Record the authentication response status in the trace.
 	if err != nil {
+		authclientErrors.Inc()
 		span.SetStatus(trace.Status{Code: trace.StatusCodeUnknown, Message: err.Error()})
 	} else if resp.Status == auth.StatusOK {
 		span.SetStatus(trace.Status{Code: trace.StatusCodeOK, Message: "OK"})
@@ -433,3 +441,14 @@ func (l *Login) makeAuthRequest(w http.ResponseWriter, req *http.Request, userna
 func u2fAppIDFromRequest(r *http.Request) string {
 	return fmt.Sprintf("https://%s", r.Host)
 }
+
+var (
+	loginCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "sso_logins_total",
+		Help: "Counter of logins by status and method.",
+	}, []string{"status", "method"})
+	authclientErrors = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "sso_auth_client_errors_total",
+		Help: "Counter for auth_client errors.",
+	})
+)
-- 
GitLab