Skip to content
Snippets Groups Projects
Commit db22f4d3 authored by Travis Ralston's avatar Travis Ralston
Browse files
parent bb0511b6
No related branches found
No related tags found
No related merge requests found
Showing
with 257 additions and 18 deletions
......@@ -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
......@@ -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)
......
......@@ -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))
}
......@@ -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
}
......@@ -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,
},
}
}
......@@ -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
}
......@@ -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
}
......
......@@ -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
......
......@@ -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
}
......
......@@ -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
}
......
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)
}
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))
}()
}
......@@ -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",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment