From cf7fd73006f4b9a3afc44d7b6d7b2a61428f8d48 Mon Sep 17 00:00:00 2001
From: Tomasz Maczukin <tomasz@maczukin.pl>
Date: Tue, 13 Dec 2016 14:24:41 +0100
Subject: [PATCH] Move metrics from git-http.go to response-writer.go

---
 internal/git/git-http.go         |  81 +++++++-----------------
 internal/git/response-writter.go | 103 +++++++++++++++++++++++++++++++
 internal/helper/logging.go       |   2 +-
 internal/sendfile/sendfile.go    |   4 +-
 4 files changed, 130 insertions(+), 60 deletions(-)
 create mode 100644 internal/git/response-writter.go

diff --git a/internal/git/git-http.go b/internal/git/git-http.go
index 48da4457450e0..03211e4651c50 100644
--- a/internal/git/git-http.go
+++ b/internal/git/git-http.go
@@ -16,35 +16,10 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/prometheus/client_golang/prometheus"
-
 	"gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
 	"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
 )
 
-var (
-	gitHTTPRequests = prometheus.NewCounterVec(
-		prometheus.CounterOpts{
-			Name: "gitlab_workhorse_git_http_requests",
-			Help: "How many Git HTTP requests have been processed by gitlab-workhorse, partitioned by request type and agent.",
-		},
-		[]string{"request_type", "agent"},
-	)
-
-	gitHTTPBytes = prometheus.NewCounterVec(
-		prometheus.CounterOpts{
-			Name: "gitlab_workhorse_git_http_bytes",
-			Help: "How many Git HTTP bytes have been sent by gitlab-workhorse, partitioned by request type, agent and direction.",
-		},
-		[]string{"request_type", "agent", "direction"},
-	)
-)
-
-func init() {
-	prometheus.MustRegister(gitHTTPRequests)
-	prometheus.MustRegister(gitHTTPBytes)
-}
-
 func GetInfoRefs(a *api.API) http.Handler {
 	return repoPreAuthorizeHandler(a, handleGetInfoRefs)
 }
@@ -63,17 +38,6 @@ func looksLikeRepo(p string) bool {
 	return true
 }
 
-func getRequestAgent(r *http.Request) string {
-	u, _, ok := r.BasicAuth()
-	if ok && u == "gitlab-ci-token" {
-		return "gitlab-ci"
-	} else if ok {
-		return "logged"
-	} else {
-		return "anonymous"
-	}
-}
-
 func repoPreAuthorizeHandler(myAPI *api.API, handleFunc api.HandleFunc) http.Handler {
 	return myAPI.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) {
 		if a.RepoPath == "" {
@@ -90,21 +54,18 @@ func repoPreAuthorizeHandler(myAPI *api.API, handleFunc api.HandleFunc) http.Han
 	}, "")
 }
 
-func handleGetInfoRefs(w http.ResponseWriter, r *http.Request, a *api.Response) {
-	rpc := r.URL.Query().Get("service")
+func handleGetInfoRefs(rw http.ResponseWriter, r *http.Request, a *api.Response) {
+	w := NewGitHttpResponseWriter(rw)
+	// Log 0 bytes in because we ignore the request body (and there usually is none anyway).
+	defer w.Log(r, 0)
+
+	rpc := getService(r)
 	if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") {
 		// The 'dumb' Git HTTP protocol is not supported
 		http.Error(w, "Not Found", 404)
 		return
 	}
 
-	requestType := "get-info-refs"
-	gitHTTPRequests.WithLabelValues(requestType, getRequestAgent(r)).Inc()
-	var writtenOut int64
-	defer func() {
-		gitHTTPBytes.WithLabelValues(requestType, getRequestAgent(r), "out").Add(float64(writtenOut))
-	}()
-
 	// Prepare our Git subprocess
 	cmd := gitCommand(a.GL_ID, "git", subCommand(rpc), "--stateless-rpc", "--advertise-refs", a.RepoPath)
 	stdout, err := cmd.StdoutPipe()
@@ -131,7 +92,7 @@ func handleGetInfoRefs(w http.ResponseWriter, r *http.Request, a *api.Response)
 		helper.LogError(r, fmt.Errorf("handleGetInfoRefs: pktFlush: %v", err))
 		return
 	}
-	if writtenOut, err = io.Copy(w, stdout); err != nil {
+	if _, err := io.Copy(w, stdout); err != nil {
 		helper.LogError(
 			r,
 			&copyError{fmt.Errorf("handleGetInfoRefs: copy output of %v: %v", cmd.Args, err)},
@@ -144,27 +105,24 @@ func handleGetInfoRefs(w http.ResponseWriter, r *http.Request, a *api.Response)
 	}
 }
 
-func handlePostRPC(w http.ResponseWriter, r *http.Request, a *api.Response) {
+func handlePostRPC(rw http.ResponseWriter, r *http.Request, a *api.Response) {
 	var err error
 	var body io.Reader
 	var isShallowClone bool
+	var writtenIn int64
+
+	w := NewGitHttpResponseWriter(rw)
+	defer func() {
+		w.Log(r, writtenIn)
+	}()
 
-	// Get Git action from URL
-	action := filepath.Base(r.URL.Path)
+	action := getService(r)
 	if !(action == "git-upload-pack" || action == "git-receive-pack") {
 		// The 'dumb' Git HTTP protocol is not supported
 		helper.Fail500(w, r, fmt.Errorf("handlePostRPC: unsupported action: %s", r.URL.Path))
 		return
 	}
 
-	requestType := "post-" + action
-	gitHTTPRequests.WithLabelValues(requestType, getRequestAgent(r)).Inc()
-	var writtenIn, writtenOut int64
-	defer func() {
-		gitHTTPBytes.WithLabelValues(requestType, getRequestAgent(r), "in").Add(float64(writtenIn))
-		gitHTTPBytes.WithLabelValues(requestType, getRequestAgent(r), "out").Add(float64(writtenOut))
-	}()
-
 	if action == "git-upload-pack" {
 		buffer := &bytes.Buffer{}
 		// Only sniff on the first 4096 bytes: we assume that if we find no
@@ -220,7 +178,7 @@ func handlePostRPC(w http.ResponseWriter, r *http.Request, a *api.Response) {
 	w.WriteHeader(200) // Don't bother with HTTP 500 from this point on, just return
 
 	// This io.Copy may take a long time, both for Git push and pull.
-	if writtenOut, err = io.Copy(w, stdout); err != nil {
+	if _, err := io.Copy(w, stdout); err != nil {
 		helper.LogError(
 			r,
 			&copyError{fmt.Errorf("handlePostRPC: copy output of %v: %v", cmd.Args, err)},
@@ -233,6 +191,13 @@ func handlePostRPC(w http.ResponseWriter, r *http.Request, a *api.Response) {
 	}
 }
 
+func getService(r *http.Request) string {
+	if r.Method == "GET" {
+		return r.URL.Query().Get("service")
+	}
+	return filepath.Base(r.URL.Path)
+}
+
 func isExitError(err error) bool {
 	_, ok := err.(*exec.ExitError)
 	return ok
diff --git a/internal/git/response-writter.go b/internal/git/response-writter.go
new file mode 100644
index 0000000000000..bca2e73ef6c5d
--- /dev/null
+++ b/internal/git/response-writter.go
@@ -0,0 +1,103 @@
+package git
+
+import (
+	"net/http"
+	"strconv"
+
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+const (
+	directionIn  = "in"
+	directionOut = "out"
+)
+
+var (
+	gitHTTPSessionsActive = prometheus.NewGauge(prometheus.GaugeOpts{
+		Name: "gitlab_workhorse_git_http_sessions_active",
+		Help: "Number of Git HTTP request-response cycles currently being handled by gitlab-workhorse.",
+	})
+
+	gitHTTPRequests = prometheus.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "gitlab_workhorse_git_http_requests",
+			Help: "How many Git HTTP requests have been processed by gitlab-workhorse, partitioned by request type and agent.",
+		},
+		[]string{"method", "code", "service", "agent"},
+	)
+
+	gitHTTPBytes = prometheus.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "gitlab_workhorse_git_http_bytes",
+			Help: "How many Git HTTP bytes have been sent by gitlab-workhorse, partitioned by request type, agent and direction.",
+		},
+		[]string{"method", "code", "service", "agent", "direction"},
+	)
+)
+
+func init() {
+	prometheus.MustRegister(gitHTTPSessionsActive)
+	prometheus.MustRegister(gitHTTPRequests)
+	prometheus.MustRegister(gitHTTPBytes)
+}
+
+type GitHttpResponseWriter struct {
+	rw      http.ResponseWriter
+	status  int
+	written int64
+}
+
+func NewGitHttpResponseWriter(rw http.ResponseWriter) *GitHttpResponseWriter {
+	gitHTTPSessionsActive.Inc()
+	return &GitHttpResponseWriter{
+		rw: rw,
+	}
+}
+
+func (w *GitHttpResponseWriter) Header() http.Header {
+	return w.rw.Header()
+}
+
+func (w *GitHttpResponseWriter) Write(data []byte) (n int, err error) {
+	if w.status == 0 {
+		w.WriteHeader(http.StatusOK)
+	}
+
+	n, err = w.rw.Write(data)
+	w.written += int64(n)
+	return n, err
+}
+
+func (w *GitHttpResponseWriter) WriteHeader(status int) {
+	if w.status != 0 {
+		return
+	}
+
+	w.status = status
+	w.rw.WriteHeader(status)
+}
+
+func (w *GitHttpResponseWriter) Log(r *http.Request, writtenIn int64) {
+	service := getService(r)
+	agent := getRequestAgent(r)
+
+	gitHTTPSessionsActive.Dec()
+	gitHTTPRequests.WithLabelValues(r.Method, strconv.Itoa(w.status), service, agent).Inc()
+	gitHTTPBytes.WithLabelValues(r.Method, strconv.Itoa(w.status), service, agent, directionIn).
+		Add(float64(writtenIn))
+	gitHTTPBytes.WithLabelValues(r.Method, strconv.Itoa(w.status), service, agent, directionOut).
+		Add(float64(w.written))
+}
+
+func getRequestAgent(r *http.Request) string {
+	u, _, ok := r.BasicAuth()
+	if !ok {
+		return "anonymous"
+	}
+
+	if u == "gitlab-ci-token" {
+		return "gitlab-ci"
+	}
+
+	return "logged"
+}
diff --git a/internal/helper/logging.go b/internal/helper/logging.go
index def28ebe9b006..e10b41ab447a1 100644
--- a/internal/helper/logging.go
+++ b/internal/helper/logging.go
@@ -68,7 +68,7 @@ func (l *LoggingResponseWriter) Write(data []byte) (n int, err error) {
 	}
 	n, err = l.rw.Write(data)
 	l.written += int64(n)
-	return
+	return n, err
 }
 
 func (l *LoggingResponseWriter) WriteHeader(status int) {
diff --git a/internal/sendfile/sendfile.go b/internal/sendfile/sendfile.go
index 2fde6db9096f5..d8d69abcbeb1b 100644
--- a/internal/sendfile/sendfile.go
+++ b/internal/sendfile/sendfile.go
@@ -34,6 +34,8 @@ var (
 		},
 		[]string{"type"},
 	)
+
+	artifactsSendFile = regexp.MustCompile("builds/[0-9]+/artifacts")
 )
 
 type sendFileResponseWriter struct {
@@ -118,7 +120,7 @@ func sendFileFromDisk(w http.ResponseWriter, r *http.Request, file string) {
 func countSendFileMetrics(size int64, r *http.Request) {
 	var requestType string
 	switch {
-	case regexp.MustCompile("builds/[0-9]+/artifacts").MatchString(r.RequestURI):
+	case artifactsSendFile.MatchString(r.RequestURI):
 		requestType = "artifacts"
 	default:
 		requestType = "other"
-- 
GitLab