1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
13 "github.com/prometheus/client_golang/prometheus"
17 mDownloadSpeed *prometheus.HistogramVec
18 mDownloadBackendSpeed *prometheus.HistogramVec
19 mUploadSpeed *prometheus.HistogramVec
20 mUploadSyncDelay *prometheus.HistogramVec
23 func newMetrics(reg *prometheus.Registry) *metrics {
25 mDownloadSpeed: prometheus.NewHistogramVec(prometheus.HistogramOpts{
28 Name: "download_speed",
29 Help: "Download speed (bytes per second) bucketed by transfer size range",
30 Buckets: []float64{10_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, math.Inf(+1)},
31 }, []string{"size_range"}),
32 mDownloadBackendSpeed: prometheus.NewHistogramVec(prometheus.HistogramOpts{
35 Name: "download_apparent_backend_speed",
36 Help: "Apparent download speed from the backend (bytes per second) when serving file downloads, bucketed by transfer size range (see https://dev.arvados.org/projects/arvados/wiki/WebDAV_performance_metrics for explanation)",
37 Buckets: []float64{10_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, math.Inf(+1)},
38 }, []string{"size_range"}),
39 mUploadSpeed: prometheus.NewHistogramVec(prometheus.HistogramOpts{
43 Help: "Upload speed (bytes per second) bucketed by transfer size range",
44 Buckets: []float64{10_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, math.Inf(+1)},
45 }, []string{"size_range"}),
46 mUploadSyncDelay: prometheus.NewHistogramVec(prometheus.HistogramOpts{
49 Name: "upload_sync_delay_seconds",
50 Help: "Upload sync delay (time from last byte received to HTTP response)",
51 }, []string{"size_range"}),
53 reg.MustRegister(m.mDownloadSpeed)
54 reg.MustRegister(m.mDownloadBackendSpeed)
55 reg.MustRegister(m.mUploadSpeed)
56 reg.MustRegister(m.mUploadSyncDelay)
60 // run handler(w,r) and record upload/download metrics as applicable.
61 func (m *metrics) track(handler http.Handler, w http.ResponseWriter, r *http.Request) {
64 dt := newDownloadTracker(w)
65 handler.ServeHTTP(dt, r)
70 bucket := sizeRange(size)
71 m.mDownloadSpeed.WithLabelValues(bucket).Observe(float64(dt.bytesOut) / time.Since(dt.t0).Seconds())
72 m.mDownloadBackendSpeed.WithLabelValues(bucket).Observe(float64(size) / (dt.backendWait + time.Since(dt.lastByte)).Seconds())
74 ut := newUploadTracker(r)
75 handler.ServeHTTP(w, r)
76 d := ut.lastByte.Sub(ut.t0)
78 // Read() was not called, or did not return
83 bucket := sizeRange(size)
84 m.mUploadSpeed.WithLabelValues(bucket).Observe(float64(ut.bytesIn) / d.Seconds())
85 m.mUploadSyncDelay.WithLabelValues(bucket).Observe(time.Since(ut.lastByte).Seconds())
87 handler.ServeHTTP(w, r)
91 // Assign a sizeRange based on number of bytes transferred (not the
92 // same as file size in the case of a Range request or interrupted
94 func sizeRange(size int64) string {
96 case size < 1_000_000:
98 case size < 10_000_000:
100 case size < 100_000_000:
107 type downloadTracker struct {
111 firstByte time.Time // time of first call to Write
112 lastByte time.Time // time of most recent call to Write
113 bytesOut int64 // bytes sent to client so far
114 backendWait time.Duration // total of intervals between Write calls
117 func newDownloadTracker(w http.ResponseWriter) *downloadTracker {
118 return &downloadTracker{ResponseWriter: w, t0: time.Now()}
121 func (dt *downloadTracker) Write(p []byte) (int, error) {
122 if dt.lastByte.IsZero() {
123 dt.backendWait += time.Since(dt.t0)
125 dt.backendWait += time.Since(dt.lastByte)
127 if dt.firstByte.IsZero() {
128 dt.firstByte = time.Now()
130 n, err := dt.ResponseWriter.Write(p)
131 dt.bytesOut += int64(n)
132 dt.lastByte = time.Now()
136 type uploadTracker struct {
143 func newUploadTracker(r *http.Request) *uploadTracker {
145 ut := &uploadTracker{ReadCloser: r.Body, t0: now}
150 func (ut *uploadTracker) Read(p []byte) (int, error) {
151 n, err := ut.ReadCloser.Read(p)
152 ut.lastByte = time.Now()
153 ut.bytesIn += int64(n)