From db22f4d34488dc46a76d0c6e0a9162953f50b7c4 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Sat, 17 Nov 2018 00:09:53 -0700
Subject: [PATCH] Basic support for metrics

Part of https://github.com/turt2live/matrix-media-repo/issues/65
---
 config.sample.yaml                            | 15 ++++-
 .../api/webserver/route_handler.go            | 32 ++++++++++
 .../api/webserver/webserver.go                | 33 +++++-----
 .../matrix-media-repo/cmd/media_repo/main.go  |  2 +
 .../matrix-media-repo/common/config/config.go | 12 ++++
 .../download_resource_handler.go              |  3 +
 .../previewers/calculated_previewer.go        |  3 +
 .../previewers/opengraph_previewer.go         |  5 +-
 .../thumbnail_resource_handler.go             | 13 ++++
 .../internal_cache/media_cache.go             | 18 ++++++
 .../matrix-media-repo/metrics/metrics.go      | 53 ++++++++++++++++
 .../matrix-media-repo/metrics/webserver.go    | 25 ++++++++
 vendor/manifest                               | 61 +++++++++++++++++++
 13 files changed, 257 insertions(+), 18 deletions(-)
 create mode 100644 src/github.com/turt2live/matrix-media-repo/metrics/metrics.go
 create mode 100644 src/github.com/turt2live/matrix-media-repo/metrics/webserver.go

diff --git a/config.sample.yaml b/config.sample.yaml
index 20fcb118..382b0bcc 100644
--- a/config.sample.yaml
+++ b/config.sample.yaml
@@ -243,4 +243,17 @@ timeouts:
 
   # The maximum amount of time the media repo will spend talking to your configured homeservers.
   # This is usually used to verify a user's identity.
-  clientServerTimeoutSeconds: 30
\ No newline at end of file
+  clientServerTimeoutSeconds: 30
+
+# Prometheus metrics configuration
+# For an example Grafana dashboard, import the following JSON:
+# https://t2bot.io/_matrix/media/r0/download/t2l.io/b89e3f042ff2057abcc470d4366a7977
+metrics:
+  # If true, the bindAddress and port below will serve GET /metrics for Prometheus to scrape.
+  enabled: false
+
+  # The address to listen on. Typically "127.0.0.1" or "0.0.0.0" for all interfaces.
+  bindAddress: "127.0.0.1"
+
+  # The port to listen on. Cannot be the same as the general web server port.
+  port: 9000
diff --git a/src/github.com/turt2live/matrix-media-repo/api/webserver/route_handler.go b/src/github.com/turt2live/matrix-media-repo/api/webserver/route_handler.go
index 83de3b99..83fde20a 100644
--- a/src/github.com/turt2live/matrix-media-repo/api/webserver/route_handler.go
+++ b/src/github.com/turt2live/matrix-media-repo/api/webserver/route_handler.go
@@ -7,19 +7,23 @@ import (
 	"net"
 	"net/http"
 	"net/url"
+	"strconv"
 	"strings"
 
 	"github.com/alioygur/is"
+	"github.com/prometheus/client_golang/prometheus"
 	"github.com/sebest/xff"
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/api"
 	"github.com/turt2live/matrix-media-repo/api/r0"
 	"github.com/turt2live/matrix-media-repo/common"
+	"github.com/turt2live/matrix-media-repo/metrics"
 	"github.com/turt2live/matrix-media-repo/util"
 )
 
 type handler struct {
 	h          func(r *http.Request, entry *logrus.Entry) interface{}
+	action     string
 	reqCounter *requestCounter
 }
 
@@ -64,11 +68,20 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	var res interface{} = api.AuthFailed()
 	if util.IsServerOurs(r.Host) {
 		contextLog.Info("Server is owned by us, processing request")
+		metrics.HttpRequests.With(prometheus.Labels{
+			"host":   r.Host,
+			"action": h.action,
+			"method": r.Method,
+		}).Inc()
 		res = h.h(r, contextLog)
 		if res == nil {
 			res = &api.EmptyResponse{}
 		}
 	} else {
+		metrics.InvalidHttpRequests.With(prometheus.Labels{
+			"action": h.action,
+			"method": r.Method,
+		}).Inc()
 		contextLog.Warn("The server name provided in the Host header is not configured, or the request was made directly to the media repo instead of through your reverse proxy. This request is being rejected.")
 	}
 	if res == nil {
@@ -102,6 +115,12 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		}
 		break
 	case *r0.DownloadMediaResponse:
+		metrics.HttpResponses.With(prometheus.Labels{
+			"host":       r.Host,
+			"action":     h.action,
+			"method":     r.Method,
+			"statusCode": strconv.Itoa(http.StatusOK),
+		}).Inc()
 		w.Header().Set("Content-Type", result.ContentType)
 		if result.SizeBytes > 0 {
 			w.Header().Set("Content-Length", fmt.Sprint(result.SizeBytes))
@@ -117,6 +136,12 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		io.Copy(w, result.Data)
 		return // Prevent sending conflicting responses
 	case *r0.IdenticonResponse:
+		metrics.HttpResponses.With(prometheus.Labels{
+			"host":       r.Host,
+			"action":     h.action,
+			"method":     r.Method,
+			"statusCode": strconv.Itoa(http.StatusOK),
+		}).Inc()
 		w.Header().Set("Content-Type", "image/png")
 		io.Copy(w, result.Avatar)
 		return // Prevent sending conflicting responses
@@ -124,6 +149,13 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		break
 	}
 
+	metrics.HttpResponses.With(prometheus.Labels{
+		"host":       r.Host,
+		"action":     h.action,
+		"method":     r.Method,
+		"statusCode": strconv.Itoa(statusCode),
+	}).Inc()
+
 	// Order is important: Set headers before sending responses
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(statusCode)
diff --git a/src/github.com/turt2live/matrix-media-repo/api/webserver/webserver.go b/src/github.com/turt2live/matrix-media-repo/api/webserver/webserver.go
index 6d28bbad..ca549aa8 100644
--- a/src/github.com/turt2live/matrix-media-repo/api/webserver/webserver.go
+++ b/src/github.com/turt2live/matrix-media-repo/api/webserver/webserver.go
@@ -25,18 +25,18 @@ func Init() {
 	rtr := mux.NewRouter()
 	counter := &requestCounter{}
 
-	optionsHandler := handler{api.EmptyResponseHandler, counter}
-	uploadHandler := handler{api.AccessTokenRequiredRoute(r0.UploadMedia), counter}
-	downloadHandler := handler{api.AccessTokenOptionalRoute(r0.DownloadMedia), counter}
-	thumbnailHandler := handler{api.AccessTokenOptionalRoute(r0.ThumbnailMedia), counter}
-	previewUrlHandler := handler{api.AccessTokenRequiredRoute(r0.PreviewUrl), counter}
-	identiconHandler := handler{api.AccessTokenOptionalRoute(r0.Identicon), counter}
-	purgeHandler := handler{api.RepoAdminRoute(custom.PurgeRemoteMedia), counter}
-	quarantineHandler := handler{api.AccessTokenRequiredRoute(custom.QuarantineMedia), counter}
-	quarantineRoomHandler := handler{api.AccessTokenRequiredRoute(custom.QuarantineRoomMedia), counter}
-	localCopyHandler := handler{api.AccessTokenRequiredRoute(unstable.LocalCopy), counter}
-	infoHandler := handler{api.AccessTokenRequiredRoute(unstable.MediaInfo), counter}
-	configHandler := handler{api.AccessTokenRequiredRoute(r0.PublicConfig), counter}
+	optionsHandler := handler{api.EmptyResponseHandler, "options_request", counter}
+	uploadHandler := handler{api.AccessTokenRequiredRoute(r0.UploadMedia), "upload", counter}
+	downloadHandler := handler{api.AccessTokenOptionalRoute(r0.DownloadMedia), "download", counter}
+	thumbnailHandler := handler{api.AccessTokenOptionalRoute(r0.ThumbnailMedia), "thumbnail", counter}
+	previewUrlHandler := handler{api.AccessTokenRequiredRoute(r0.PreviewUrl), "url_preview", counter}
+	identiconHandler := handler{api.AccessTokenOptionalRoute(r0.Identicon), "identicon", counter}
+	purgeHandler := handler{api.RepoAdminRoute(custom.PurgeRemoteMedia), "purge_remote_media", counter}
+	quarantineHandler := handler{api.AccessTokenRequiredRoute(custom.QuarantineMedia), "quarantine_media", counter}
+	quarantineRoomHandler := handler{api.AccessTokenRequiredRoute(custom.QuarantineRoomMedia), "quarantine_room", counter}
+	localCopyHandler := handler{api.AccessTokenRequiredRoute(unstable.LocalCopy), "local_copy", counter}
+	infoHandler := handler{api.AccessTokenRequiredRoute(unstable.MediaInfo), "info", counter}
+	configHandler := handler{api.AccessTokenRequiredRoute(r0.PublicConfig), "config", counter}
 
 	routes := make(map[string]route)
 	versions := []string{"r0", "v1", "unstable"} // r0 is typically clients and v1 is typically servers. v1 is deprecated.
@@ -76,8 +76,8 @@ func Init() {
 		rtr.Handle(routePath+"/", optionsHandler).Methods("OPTIONS")
 	}
 
-	rtr.NotFoundHandler = handler{api.NotFoundHandler, counter}
-	rtr.MethodNotAllowedHandler = handler{api.MethodNotAllowedHandler, counter}
+	rtr.NotFoundHandler = handler{api.NotFoundHandler, "not_found", counter}
+	rtr.MethodNotAllowedHandler = handler{api.MethodNotAllowedHandler, "method_not_allowed", counter}
 
 	var handler http.Handler = rtr
 	if config.Get().RateLimit.Enabled {
@@ -96,8 +96,9 @@ func Init() {
 	}
 
 	address := config.Get().General.BindAddress + ":" + strconv.Itoa(config.Get().General.Port)
-	http.Handle("/", handler)
+	httpMux := http.NewServeMux()
+	httpMux.Handle("/", handler)
 
 	logrus.WithField("address", address).Info("Started up. Listening at http://" + address)
-	http.ListenAndServe(address, nil)
+	logrus.Fatal(http.ListenAndServe(address, httpMux))
 }
diff --git a/src/github.com/turt2live/matrix-media-repo/cmd/media_repo/main.go b/src/github.com/turt2live/matrix-media-repo/cmd/media_repo/main.go
index 582e950b..250d777a 100644
--- a/src/github.com/turt2live/matrix-media-repo/cmd/media_repo/main.go
+++ b/src/github.com/turt2live/matrix-media-repo/cmd/media_repo/main.go
@@ -7,6 +7,7 @@ import (
 	"github.com/turt2live/matrix-media-repo/api/webserver"
 	"github.com/turt2live/matrix-media-repo/common/config"
 	"github.com/turt2live/matrix-media-repo/common/logging"
+	"github.com/turt2live/matrix-media-repo/metrics"
 )
 
 func main() {
@@ -23,5 +24,6 @@ func main() {
 	}
 
 	logrus.Info("Starting media repository...")
+	metrics.Init()
 	webserver.Init() // blocks to listen for requests
 }
diff --git a/src/github.com/turt2live/matrix-media-repo/common/config/config.go b/src/github.com/turt2live/matrix-media-repo/common/config/config.go
index 8fa232cc..bd1d05d5 100644
--- a/src/github.com/turt2live/matrix-media-repo/common/config/config.go
+++ b/src/github.com/turt2live/matrix-media-repo/common/config/config.go
@@ -107,6 +107,12 @@ type TimeoutsConfig struct {
 	ClientServer int `yaml:"clientServerTimeoutSeconds"`
 }
 
+type MetricsConfig struct {
+	Enabled     bool   `yaml:"enabled"`
+	BindAddress string `yaml:"bindAddress"`
+	Port        int    `yaml:"port"`
+}
+
 type MediaRepoConfig struct {
 	General        *GeneralConfig      `yaml:"repo"`
 	Homeservers    []*HomeserverConfig `yaml:"homeservers,flow"`
@@ -120,6 +126,7 @@ type MediaRepoConfig struct {
 	Identicons     *IdenticonsConfig   `yaml:"identicons"`
 	Quarantine     *QuarantineConfig   `yaml:"quarantine"`
 	TimeoutSeconds *TimeoutsConfig     `yaml:"timeouts"`
+	Metrics        *MetricsConfig      `yaml:"metrics"`
 }
 
 var instance *MediaRepoConfig
@@ -275,5 +282,10 @@ func NewDefaultConfig() *MediaRepoConfig {
 			ClientServer: 30,
 			Federation:   120,
 		},
+		Metrics: &MetricsConfig{
+			Enabled:     false,
+			BindAddress: "localhost",
+			Port:        9000,
+		},
 	}
 }
diff --git a/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_resource_handler.go b/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_resource_handler.go
index 39305ce1..7b76345e 100644
--- a/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_resource_handler.go
+++ b/src/github.com/turt2live/matrix-media-repo/controllers/download_controller/download_resource_handler.go
@@ -11,11 +11,13 @@ import (
 
 	"github.com/djherbis/stream"
 	"github.com/patrickmn/go-cache"
+	"github.com/prometheus/client_golang/prometheus"
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common"
 	"github.com/turt2live/matrix-media-repo/common/config"
 	"github.com/turt2live/matrix-media-repo/controllers/upload_controller"
 	"github.com/turt2live/matrix-media-repo/matrix"
+	"github.com/turt2live/matrix-media-repo/metrics"
 	"github.com/turt2live/matrix-media-repo/types"
 	"github.com/turt2live/matrix-media-repo/util"
 	"github.com/turt2live/matrix-media-repo/util/resource_handler"
@@ -251,5 +253,6 @@ func DownloadRemoteMediaDirect(server string, mediaId string, log *logrus.Entry)
 	}
 
 	log.Info("Persisting downloaded media")
+	metrics.MediaDownloaded.With(prometheus.Labels{"origin": server}).Inc()
 	return request, nil
 }
diff --git a/src/github.com/turt2live/matrix-media-repo/controllers/preview_controller/previewers/calculated_previewer.go b/src/github.com/turt2live/matrix-media-repo/controllers/preview_controller/previewers/calculated_previewer.go
index d5022828..02194661 100644
--- a/src/github.com/turt2live/matrix-media-repo/controllers/preview_controller/previewers/calculated_previewer.go
+++ b/src/github.com/turt2live/matrix-media-repo/controllers/preview_controller/previewers/calculated_previewer.go
@@ -10,10 +10,12 @@ import (
 	"strconv"
 	"time"
 
+	"github.com/prometheus/client_golang/prometheus"
 	"github.com/ryanuber/go-glob"
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common"
 	"github.com/turt2live/matrix-media-repo/common/config"
+	"github.com/turt2live/matrix-media-repo/metrics"
 	"github.com/turt2live/matrix-media-repo/util"
 )
 
@@ -56,6 +58,7 @@ func GenerateCalculatedPreview(urlStr string, log *logrus.Entry) (PreviewResult,
 		result.Image = img
 	}
 
+	metrics.UrlPreviewsGenerated.With(prometheus.Labels{"type":"calculated"}).Inc()
 	return *result, nil
 }
 
diff --git a/src/github.com/turt2live/matrix-media-repo/controllers/preview_controller/previewers/opengraph_previewer.go b/src/github.com/turt2live/matrix-media-repo/controllers/preview_controller/previewers/opengraph_previewer.go
index 4c95daea..59616c29 100644
--- a/src/github.com/turt2live/matrix-media-repo/controllers/preview_controller/previewers/opengraph_previewer.go
+++ b/src/github.com/turt2live/matrix-media-repo/controllers/preview_controller/previewers/opengraph_previewer.go
@@ -15,10 +15,12 @@ import (
 
 	"github.com/PuerkitoBio/goquery"
 	"github.com/dyatlov/go-opengraph/opengraph"
+	"github.com/prometheus/client_golang/prometheus"
 	"github.com/ryanuber/go-glob"
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common"
 	"github.com/turt2live/matrix-media-repo/common/config"
+	"github.com/turt2live/matrix-media-repo/metrics"
 )
 
 var ogSupportedTypes = []string{"text/*"}
@@ -89,6 +91,7 @@ func GenerateOpenGraphPreview(urlStr string, log *logrus.Entry) (PreviewResult,
 		graph.Image = img
 	}
 
+	metrics.UrlPreviewsGenerated.With(prometheus.Labels{"type": "opengraph"}).Inc()
 	return *graph, nil
 }
 
@@ -125,7 +128,7 @@ func doHttpGet(urlStr string, log *logrus.Entry) (*http.Response, error) {
 			Timeout: time.Duration(config.Get().TimeoutSeconds.UrlPreviews) * time.Second,
 		}
 	}
-	
+
 	req, err := http.NewRequest("GET", urlStr, nil)
 	if err != nil {
 		return nil, err
diff --git a/src/github.com/turt2live/matrix-media-repo/controllers/thumbnail_controller/thumbnail_resource_handler.go b/src/github.com/turt2live/matrix-media-repo/controllers/thumbnail_controller/thumbnail_resource_handler.go
index 1bac90f4..f5efaebd 100644
--- a/src/github.com/turt2live/matrix-media-repo/controllers/thumbnail_controller/thumbnail_resource_handler.go
+++ b/src/github.com/turt2live/matrix-media-repo/controllers/thumbnail_controller/thumbnail_resource_handler.go
@@ -12,11 +12,14 @@ import (
 	"os"
 	"os/exec"
 	"path"
+	"strconv"
 	"sync"
 
 	"github.com/disintegration/imaging"
+	"github.com/prometheus/client_golang/prometheus"
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common/config"
+	"github.com/turt2live/matrix-media-repo/metrics"
 	"github.com/turt2live/matrix-media-repo/storage"
 	"github.com/turt2live/matrix-media-repo/types"
 	"github.com/turt2live/matrix-media-repo/util"
@@ -158,6 +161,14 @@ func GenerateThumbnail(media *types.Media, width int, height int, method string,
 		log.Info("Aspect ratio is the same, converting method to 'scale'")
 	}
 
+	metric := metrics.ThumbnailsGenerated.With(prometheus.Labels{
+		"width":    strconv.Itoa(width),
+		"height":   strconv.Itoa(height),
+		"method":   method,
+		"animated": strconv.FormatBool(animated),
+		"origin":   media.Origin,
+	})
+
 	thumb := &GeneratedThumbnail{
 		Animated: animated,
 	}
@@ -175,6 +186,7 @@ func GenerateThumbnail(media *types.Media, width int, height int, method string,
 			thumb.SizeBytes = media.SizeBytes
 			thumb.Sha256Hash = media.Sha256Hash
 			log.Warn("Image too small, returning raw image")
+			metric.Inc()
 			return thumb, nil
 		}
 	}
@@ -292,6 +304,7 @@ func GenerateThumbnail(media *types.Media, width int, height int, method string,
 	thumb.SizeBytes = fileSize
 	thumb.Sha256Hash = hash
 
+	metric.Inc()
 	return thumb, nil
 }
 
diff --git a/src/github.com/turt2live/matrix-media-repo/internal_cache/media_cache.go b/src/github.com/turt2live/matrix-media-repo/internal_cache/media_cache.go
index a614d24e..cfd84062 100644
--- a/src/github.com/turt2live/matrix-media-repo/internal_cache/media_cache.go
+++ b/src/github.com/turt2live/matrix-media-repo/internal_cache/media_cache.go
@@ -10,8 +10,10 @@ import (
 	"time"
 
 	"github.com/patrickmn/go-cache"
+	"github.com/prometheus/client_golang/prometheus"
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common/config"
+	"github.com/turt2live/matrix-media-repo/metrics"
 	"github.com/turt2live/matrix-media-repo/storage"
 	"github.com/turt2live/matrix-media-repo/types"
 	"github.com/turt2live/matrix-media-repo/util"
@@ -90,6 +92,7 @@ func (c *MediaCache) IncrementDownloads(fileHash string) {
 
 func (c *MediaCache) GetMedia(media *types.Media, log *logrus.Entry) (*cachedFile, error) {
 	if !c.enabled {
+		metrics.CacheMisses.With(prometheus.Labels{"cache": "media"}).Inc()
 		return nil, nil
 	}
 
@@ -111,6 +114,7 @@ func (c *MediaCache) GetMedia(media *types.Media, log *logrus.Entry) (*cachedFil
 
 func (c *MediaCache) GetThumbnail(thumbnail *types.Thumbnail, log *logrus.Entry) (*cachedFile, error) {
 	if !c.enabled {
+		metrics.CacheMisses.With(prometheus.Labels{"cache": "media"}).Inc()
 		return nil, nil
 	}
 
@@ -140,6 +144,8 @@ func (c *MediaCache) updateItemInCache(recordId string, mediaSize int64, cacheFn
 	// The cached bytes will leave memory over time
 	if found && !enoughDownloads {
 		log.Info("Removing media from cache because it does not have enough downloads")
+		metrics.CacheMisses.With(prometheus.Labels{"cache": "media"}).Inc()
+		metrics.CacheEvictions.With(prometheus.Labels{"cache": "media", "reason": "not_enough_downloads"}).Inc()
 		c.cache.Delete(recordId)
 		c.flagEvicted(recordId)
 		return nil, nil
@@ -155,6 +161,7 @@ func (c *MediaCache) updateItemInCache(recordId string, mediaSize int64, cacheFn
 		// Don't bother checking for space if it won't fit anyways
 		if mediaSize > maxSpace {
 			log.Warn("Media too large to cache")
+			metrics.CacheMisses.With(prometheus.Labels{"cache": "media"}).Inc()
 			return nil, nil
 		}
 
@@ -168,6 +175,8 @@ func (c *MediaCache) updateItemInCache(recordId string, mediaSize int64, cacheFn
 			if err != nil {
 				return nil, err
 			}
+			metrics.CacheNumItems.With(prometheus.Labels{"cache": "media"}).Inc()
+			metrics.CacheNumBytes.With(prometheus.Labels{"cache": "media"}).Set(float64(c.size))
 			c.cache.Set(recordId, cachedItem, cache.DefaultExpiration)
 		}
 
@@ -187,6 +196,9 @@ func (c *MediaCache) updateItemInCache(recordId string, mediaSize int64, cacheFn
 			if err != nil {
 				return nil, err
 			}
+			metrics.CacheHits.With(prometheus.Labels{"cache": "media"}).Inc()
+			metrics.CacheNumItems.With(prometheus.Labels{"cache": "media"}).Inc()
+			metrics.CacheNumBytes.With(prometheus.Labels{"cache": "media"}).Set(float64(c.size))
 			c.cache.Set(recordId, cachedItem, cache.DefaultExpiration)
 			return cachedItem, nil
 		}
@@ -197,8 +209,10 @@ func (c *MediaCache) updateItemInCache(recordId string, mediaSize int64, cacheFn
 
 	// By now the media should be in the correct state (cached or not)
 	if found {
+		metrics.CacheHits.With(prometheus.Labels{"cache": "media"}).Inc()
 		return item.(*cachedFile), nil
 	}
+	metrics.CacheMisses.With(prometheus.Labels{"cache": "media"}).Inc()
 	return nil, nil
 }
 
@@ -249,8 +263,12 @@ func (c *MediaCache) clearSpace(neededBytes int64, withDownloadsLessThan int, wi
 		toRemove := e.Value.(*removable)
 		c.cache.Delete(toRemove.cacheKey)
 		c.flagEvicted(toRemove.recordId)
+		metrics.CacheEvictions.With(prometheus.Labels{"cache": "media", "reason": "need_space"}).Inc()
+		metrics.CacheNumItems.With(prometheus.Labels{"cache": "media"}).Dec()
 	}
 
+	c.size -= preppedSpace
+	metrics.CacheNumBytes.With(prometheus.Labels{"cache": "media"}).Set(float64(c.size))
 	return preppedSpace
 }
 
diff --git a/src/github.com/turt2live/matrix-media-repo/metrics/metrics.go b/src/github.com/turt2live/matrix-media-repo/metrics/metrics.go
new file mode 100644
index 00000000..e607a8a7
--- /dev/null
+++ b/src/github.com/turt2live/matrix-media-repo/metrics/metrics.go
@@ -0,0 +1,53 @@
+package metrics
+
+import (
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+var HttpRequests = prometheus.NewCounterVec(prometheus.CounterOpts{
+	Name: "media_http_requests_total",
+}, []string{"host", "action", "method"})
+var InvalidHttpRequests = prometheus.NewCounterVec(prometheus.CounterOpts{
+	Name: "media_invalid_http_requests_total",
+}, []string{"action", "method"})
+var HttpResponses = prometheus.NewCounterVec(prometheus.CounterOpts{
+	Name: "media_http_responses_total",
+}, []string{"host", "action", "method", "statusCode"})
+var CacheHits = prometheus.NewCounterVec(prometheus.CounterOpts{
+	Name: "media_cache_hits_total",
+}, []string{"cache"})
+var CacheMisses = prometheus.NewCounterVec(prometheus.CounterOpts{
+	Name: "media_cache_misses_total",
+}, []string{"cache"})
+var CacheEvictions = prometheus.NewCounterVec(prometheus.CounterOpts{
+	Name: "media_cache_evictions_total",
+}, []string{"cache","reason"})
+var CacheNumItems = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+	Name: "media_cache_num_items",
+}, []string{"cache"})
+var CacheNumBytes = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+	Name: "media_cache_num_bytes_used",
+}, []string{"cache"})
+var ThumbnailsGenerated = prometheus.NewCounterVec(prometheus.CounterOpts{
+	Name: "media_thumbnails_generated_total",
+}, []string{"width","height","method","animated","origin"})
+var MediaDownloaded = prometheus.NewCounterVec(prometheus.CounterOpts{
+	Name: "media_downloaded_total",
+}, []string{"origin"})
+var UrlPreviewsGenerated = prometheus.NewCounterVec(prometheus.CounterOpts{
+	Name: "media_url_previews_generated_total",
+}, []string{"type"})
+
+func init() {
+	prometheus.MustRegister(HttpRequests)
+	prometheus.MustRegister(InvalidHttpRequests)
+	prometheus.MustRegister(HttpResponses)
+	prometheus.MustRegister(CacheHits)
+	prometheus.MustRegister(CacheMisses)
+	prometheus.MustRegister(CacheEvictions)
+	prometheus.MustRegister(CacheNumItems)
+	prometheus.MustRegister(CacheNumBytes)
+	prometheus.MustRegister(ThumbnailsGenerated)
+	prometheus.MustRegister(MediaDownloaded)
+	prometheus.MustRegister(UrlPreviewsGenerated)
+}
diff --git a/src/github.com/turt2live/matrix-media-repo/metrics/webserver.go b/src/github.com/turt2live/matrix-media-repo/metrics/webserver.go
new file mode 100644
index 00000000..cdfdf4d3
--- /dev/null
+++ b/src/github.com/turt2live/matrix-media-repo/metrics/webserver.go
@@ -0,0 +1,25 @@
+package metrics
+
+import (
+	"net/http"
+	"strconv"
+
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"github.com/sirupsen/logrus"
+	"github.com/turt2live/matrix-media-repo/common/config"
+)
+
+func Init() {
+	if !config.Get().Metrics.Enabled {
+		logrus.Info("Metrics disabled")
+		return
+	}
+	rtr := http.NewServeMux()
+	rtr.Handle("/metrics", promhttp.Handler())
+	rtr.Handle("/_media/metrics", promhttp.Handler())
+	go func() {
+		address := config.Get().Metrics.BindAddress + ":" + strconv.Itoa(config.Get().Metrics.Port)
+		logrus.WithField("address", address).Info("Started metrics listener. Listening at http://" + address)
+		logrus.Fatal(http.ListenAndServe(address, rtr))
+	}()
+}
diff --git a/vendor/manifest b/vendor/manifest
index 5b50fea7..0fb7d376 100644
--- a/vendor/manifest
+++ b/vendor/manifest
@@ -31,6 +31,13 @@
 			"revision": "349dd0209470eabd9514242c688c403c0926d266",
 			"branch": "master"
 		},
+		{
+			"importpath": "github.com/beorn7/perks/quantile",
+			"repository": "https://github.com/beorn7/perks",
+			"revision": "3a771d992973f24aa725d07868b467d1ddfceafb",
+			"branch": "master",
+			"path": "/quantile"
+		},
 		{
 			"importpath": "github.com/cenk/backoff",
 			"repository": "https://github.com/cenk/backoff",
@@ -100,6 +107,13 @@
 			"branch": "master",
 			"path": "/truetype"
 		},
+		{
+			"importpath": "github.com/golang/protobuf/proto",
+			"repository": "https://github.com/golang/protobuf",
+			"revision": "52132540909e117f2b98b0694383dc0ab1e1deca",
+			"branch": "master",
+			"path": "/proto"
+		},
 		{
 			"importpath": "github.com/gorilla/mux",
 			"repository": "https://github.com/gorilla/mux",
@@ -142,6 +156,13 @@
 			"revision": "b609790bd85edf8e9ab7e0f8912750a786177bcf",
 			"branch": "master"
 		},
+		{
+			"importpath": "github.com/matttproud/golang_protobuf_extensions/pbutil",
+			"repository": "https://github.com/matttproud/golang_protobuf_extensions",
+			"revision": "c12348ce28de40eed0136aa2b644d0ee0650e56c",
+			"branch": "master",
+			"path": "/pbutil"
+		},
 		{
 			"importpath": "github.com/olebedev/emitter",
 			"repository": "https://github.com/olebedev/emitter",
@@ -160,6 +181,46 @@
 			"revision": "f15c970de5b76fac0b59abb32d62c17cc7bed265",
 			"branch": "master"
 		},
+		{
+			"importpath": "github.com/prometheus/client_golang",
+			"repository": "https://github.com/prometheus/client_golang",
+			"revision": "3fb53dff765f8a3e0f9d8b1d5b86d4f8c4eb3a09",
+			"branch": "master"
+		},
+		{
+			"importpath": "github.com/prometheus/client_model/go",
+			"repository": "https://github.com/prometheus/client_model",
+			"revision": "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f",
+			"branch": "master",
+			"path": "/go"
+		},
+		{
+			"importpath": "github.com/prometheus/common/expfmt",
+			"repository": "https://github.com/prometheus/common",
+			"revision": "1f2c4f3cd6db5fd6f68f36af6b6d5d936fd93c4e",
+			"branch": "master",
+			"path": "/expfmt"
+		},
+		{
+			"importpath": "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg",
+			"repository": "https://github.com/prometheus/common",
+			"revision": "1f2c4f3cd6db5fd6f68f36af6b6d5d936fd93c4e",
+			"branch": "master",
+			"path": "/internal/bitbucket.org/ww/goautoneg"
+		},
+		{
+			"importpath": "github.com/prometheus/common/model",
+			"repository": "https://github.com/prometheus/common",
+			"revision": "1f2c4f3cd6db5fd6f68f36af6b6d5d936fd93c4e",
+			"branch": "master",
+			"path": "/model"
+		},
+		{
+			"importpath": "github.com/prometheus/procfs",
+			"repository": "https://github.com/prometheus/procfs",
+			"revision": "185b4288413d2a0dd0806f78c90dde719829e5ae",
+			"branch": "master"
+		},
 		{
 			"importpath": "github.com/rifflock/lfshook",
 			"repository": "https://github.com/rifflock/lfshook",
-- 
GitLab