Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

Commit 08bfc50

Browse files
committed
upload-proxy: add basic metrics
1 parent a77e156 commit 08bfc50

6 files changed

Lines changed: 156 additions & 92 deletions

File tree

cmd/upload-proxy/config.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
package main
22

3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/pelletier/go-toml"
8+
)
9+
310
type Config struct {
411
ListenAddress string
512
Sinks []SinkConfig
613
}
14+
15+
func (c *Config) ReadFromFile(path string) error {
16+
data, err := os.ReadFile(path)
17+
if err != nil {
18+
return fmt.Errorf("read: %w", err)
19+
}
20+
err = toml.Unmarshal(data, c)
21+
if err != nil {
22+
return fmt.Errorf("unmarshal: %w", err)
23+
}
24+
return nil
25+
}

cmd/upload-proxy/config.toml

Lines changed: 0 additions & 4 deletions
This file was deleted.

cmd/upload-proxy/main.go

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,38 @@ import (
66
"fmt"
77
"os"
88

9-
"github.com/pelletier/go-toml"
109
"github.com/rs/zerolog"
1110
"github.com/rs/zerolog/log"
1211
"github.com/voc/stream-api/util"
1312
)
1413

14+
func main() {
15+
conf := Config{}
16+
config := flag.String("config", "config.toml", "Set path to proxy config")
17+
debug := flag.Bool("debug", false, "sets log level to debug")
18+
flag.StringVar(&conf.ListenAddress, "addr", ":8080", "Set listen address")
19+
flag.Parse()
20+
ctx, cancel := context.WithCancel(context.Background())
21+
defer cancel()
22+
util.HandleSignal(ctx, cancel)
23+
24+
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
25+
26+
zerolog.SetGlobalLevel(zerolog.InfoLevel)
27+
if *debug {
28+
zerolog.SetGlobalLevel(zerolog.DebugLevel)
29+
}
30+
31+
if err := conf.ReadFromFile(*config); err != nil {
32+
log.Fatal().Err(err).Msg("config parse failed")
33+
}
34+
35+
// Run proxy
36+
if err := run(ctx, &conf); err != nil {
37+
log.Fatal().Err(err).Msg("proxy run failed")
38+
}
39+
}
40+
1541
func run(parentCtx context.Context, conf *Config) error {
1642
ctx, cancel := context.WithCancel(parentCtx)
1743
defer cancel()
@@ -25,7 +51,7 @@ func run(parentCtx context.Context, conf *Config) error {
2551
return fmt.Errorf("sink init failed: %w", err)
2652
}
2753
sinks = append(sinks, sink)
28-
log.Info().Str("sink", sink.url.Host).Str("basePath", sink.url.Path).Msg("added sink")
54+
log.Info().Str("sink", sink.url.Host).Str("basePath", sink.url.Path).Str("authType", string(sink.conf.AuthType)).Msg("added sink")
2955
}
3056
proxy, err := NewProxy(ctx, conf.ListenAddress, sinks)
3157
if err != nil {
@@ -43,35 +69,3 @@ func run(parentCtx context.Context, conf *Config) error {
4369
proxy.Wait()
4470
return nil
4571
}
46-
47-
func main() {
48-
conf := Config{}
49-
config := flag.String("config", "config.toml", "Set path to proxy config")
50-
debug := flag.Bool("debug", false, "sets log level to debug")
51-
flag.StringVar(&conf.ListenAddress, "addr", ":8080", "Set listen address")
52-
flag.Parse()
53-
ctx, cancel := context.WithCancel(context.Background())
54-
defer cancel()
55-
util.HandleSignal(ctx, cancel)
56-
57-
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
58-
59-
zerolog.SetGlobalLevel(zerolog.InfoLevel)
60-
if *debug {
61-
zerolog.SetGlobalLevel(zerolog.DebugLevel)
62-
}
63-
64-
data, err := os.ReadFile(*config)
65-
if err != nil {
66-
log.Fatal().Err(err).Msg("config read failed")
67-
}
68-
err = toml.Unmarshal(data, &conf)
69-
if err != nil {
70-
log.Fatal().Err(err).Msg("config parse failed")
71-
}
72-
73-
// Run proxy
74-
if run(ctx, &conf); err != nil {
75-
log.Fatal().Err(err)
76-
}
77-
}

cmd/upload-proxy/modd.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Auto restart server on file changes
2+
"*.go" {
3+
prep: go build -o upload-proxy .
4+
daemon +sigterm: ./upload-proxy -addr localhost:8081
5+
}

cmd/upload-proxy/proxy.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"github.com/rs/zerolog/log"
2121
)
2222

23+
const MaxFileSize = 50 * 1024 * 1024 // 50 MB
24+
2325
type Proxy struct {
2426
sinks []*Sink
2527
errors chan error
@@ -118,10 +120,10 @@ func getDeadline(path string) time.Time {
118120
ext := filepath.Ext(path)
119121
now := time.Now()
120122
switch ext {
121-
case ".m3u8":
122-
fallthrough
123-
case ".mpd":
123+
case ".m3u8", ".mpd":
124124
return now.Add(time.Second * 6)
125+
case ".jpg", ".jpeg":
126+
return now.Add(time.Second * 20)
125127
default:
126128
return now.Add(time.Second * 60)
127129
}
@@ -170,7 +172,7 @@ func (p *Proxy) HandleUpload(w http.ResponseWriter, r *http.Request) {
170172
log.Debug().Str("method", r.Method).Str("url", r.URL.Path).Msg("handle")
171173
uploadStart := time.Now()
172174
var b bytes.Buffer
173-
_, err := b.ReadFrom(r.Body)
175+
size, err := b.ReadFrom(http.MaxBytesReader(w, r.Body, MaxFileSize))
174176
if err != nil {
175177
log.Error().Err(err).Msg("read body")
176178
w.WriteHeader(500)
@@ -183,8 +185,8 @@ func (p *Proxy) HandleUpload(w http.ResponseWriter, r *http.Request) {
183185
return io.NopCloser(bytes.NewReader(b.Bytes())), nil
184186
}
185187
for _, sink := range p.sinks {
186-
req := r.Clone(context.Background())
187-
req.ContentLength = r.ContentLength
188+
req := r.Clone(p.ctx)
189+
req.ContentLength = size
188190
req.GetBody = getBody
189191
req.Body, _ = getBody()
190192
sink.handle(req, getDeadline(r.URL.Path))

cmd/upload-proxy/sink.go

Lines changed: 96 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ func NewSink(conf SinkConfig) (*Sink, error) {
5252
if conf.QueueSize == 0 {
5353
conf.QueueSize = DefaultQueueSize
5454
}
55-
55+
if conf.Username != "" && (conf.AuthType != AuthTypeBasic && conf.AuthType != AuthTypeDigest) {
56+
conf.AuthType = AuthTypeBasic
57+
}
5658
url, err := url.Parse(conf.Address)
5759
if err != nil {
5860
return nil, fmt.Errorf("invalid sink address: %w", err)
@@ -109,7 +111,8 @@ outer:
109111
default:
110112
// drop front of queue
111113
<-sink.queue
112-
log.Info().Str("sink", sink.url.Host).Msg("queue overflow")
114+
sink.metrics.totalNumDropped.Inc()
115+
log.Warn().Str("sink", sink.url.Host).Msg("queue full, dropping request")
113116
continue
114117
}
115118
}
@@ -126,71 +129,113 @@ func (sink *Sink) work(ctx context.Context, client *http.Client) {
126129
case <-ctx.Done():
127130
return
128131
case entry := <-sink.queue:
129-
retry:
130-
for {
131-
select {
132-
case <-entry.Request.Context().Done():
133-
log.Warn().Str("sink", sink.url.Host).Msg("discarding timed out request")
134-
break retry
135-
default:
136-
}
137-
res, err := client.Do(entry.Request)
138-
if err != nil {
139-
log.Error().Str("sink", sink.url.Host).Err(err).Msg("sink error")
140-
break retry
141-
}
142-
res.Body.Close()
143-
if res.StatusCode < 200 || res.StatusCode > 299 {
144-
log.Warn().
145-
Str("sink", sink.url.Host).
146-
Str("method", entry.Request.Method).
147-
Str("path", entry.Request.URL.Path).
148-
Str("status", res.Status).
149-
Msg("upload failed")
150-
151-
// get new body
152-
entry.Request.Body, _ = entry.Request.GetBody()
153-
154-
// retry if we have space in queue
155-
time.Sleep(time.Second)
156-
continue
157-
}
158-
// success
159-
sink.metrics.totalNumUploaded.Inc()
160-
sink.metrics.totalUploadDelay.Add(time.Since(entry.QueuedAt).Seconds())
161-
break
132+
sink.upload(ctx, entry, client)
133+
}
134+
}
135+
}
136+
137+
func (sink *Sink) upload(ctx context.Context, entry SinkEntry, client *http.Client) {
138+
for {
139+
if entry.Deadline.Before(time.Now()) {
140+
log.Warn().Str("sink", sink.url.Host).Msg("discarding timed out request")
141+
sink.metrics.totalNumDropped.Inc()
142+
return
143+
}
144+
queuedFor := time.Since(entry.QueuedAt)
145+
uploadStart := time.Now()
146+
res, err := client.Do(entry.Request)
147+
if err != nil {
148+
log.Error().Str("sink", sink.url.Host).Err(err).Msg("sink error")
149+
sink.metrics.totalNumDropped.Inc()
150+
return
151+
}
152+
_ = res.Body.Close()
153+
if res.StatusCode < 200 || res.StatusCode > 299 {
154+
log.Warn().
155+
Str("sink", sink.url.Host).
156+
Str("method", entry.Request.Method).
157+
Str("path", entry.Request.URL.Path).
158+
Str("status", res.Status).
159+
Msg("upload failed")
160+
sink.metrics.totalTransientErrors.Inc()
161+
162+
// get new body
163+
entry.Request.Body, _ = entry.Request.GetBody()
164+
165+
// drop if queue is almost full
166+
if len(sink.queue) > cap(sink.queue)-2 {
167+
log.Error().Str("sink", sink.url.Host).Err(err).Msg("queue full, dropping request")
168+
sink.metrics.totalNumDropped.Inc()
169+
return
170+
}
171+
// retry after short delay
172+
select {
173+
case <-ctx.Done():
174+
return
175+
case <-time.After(time.Second):
162176
}
177+
continue
163178
}
179+
// success
180+
sink.metrics.totalNumUploaded.Inc()
181+
sink.metrics.totalQueueDelay.Add(queuedFor.Seconds())
182+
sink.metrics.totalUploadDuration.Add(time.Since(uploadStart).Seconds())
183+
sink.metrics.totalBytesUploaded.Add(float64(entry.Request.ContentLength))
184+
return
164185
}
165186
}
166187

167188
type SinkMetrics struct {
168-
reg prometheus.Registerer
169-
totalNumUploaded prometheus.Counter
170-
totalUploadDelay prometheus.Counter
171-
queueLength prometheus.Gauge
189+
reg prometheus.Registerer
190+
totalNumUploaded prometheus.Counter
191+
totalNumDropped prometheus.Counter
192+
totalQueueDelay prometheus.Counter
193+
totalUploadDuration prometheus.Counter
194+
totalBytesUploaded prometheus.Counter
195+
totalTransientErrors prometheus.Counter
172196
}
173197

174198
func NewSinkMetrics(reg prometheus.Registerer, cfg SinkConfig) *SinkMetrics {
175199
return &SinkMetrics{
176200
reg: reg,
177201
totalNumUploaded: promauto.With(reg).NewCounter(prometheus.CounterOpts{
178202
Name: "upload_proxy_sink_uploaded_total",
179-
Help: "Total number of uploaded requests",
203+
Help: "Number of uploaded requests",
204+
ConstLabels: prometheus.Labels{
205+
"sink": cfg.Address,
206+
},
207+
}),
208+
totalNumDropped: promauto.With(reg).NewCounter(prometheus.CounterOpts{
209+
Name: "upload_proxy_sink_dropped_total",
210+
Help: "Number of dropped requests due to error, timeout or full queue",
211+
ConstLabels: prometheus.Labels{
212+
"sink": cfg.Address,
213+
},
214+
}),
215+
totalQueueDelay: promauto.With(reg).NewCounter(prometheus.CounterOpts{
216+
Name: "upload_proxy_sink_queue_delay_total",
217+
Help: "Queue delay in seconds",
218+
ConstLabels: prometheus.Labels{
219+
"sink": cfg.Address,
220+
},
221+
}),
222+
totalUploadDuration: promauto.With(reg).NewCounter(prometheus.CounterOpts{
223+
Name: "upload_proxy_sink_upload_duration_total",
224+
Help: "Upload duration in seconds",
180225
ConstLabels: prometheus.Labels{
181226
"sink": cfg.Address,
182227
},
183228
}),
184-
totalUploadDelay: promauto.With(reg).NewCounter(prometheus.CounterOpts{
185-
Name: "upload_proxy_sink_upload_delay_total",
186-
Help: "Total upload delay in seconds",
229+
totalBytesUploaded: promauto.With(reg).NewCounter(prometheus.CounterOpts{
230+
Name: "upload_proxy_sink_bytes_uploaded_total",
231+
Help: "Number of bytes uploaded",
187232
ConstLabels: prometheus.Labels{
188233
"sink": cfg.Address,
189234
},
190235
}),
191-
queueLength: promauto.With(reg).NewGauge(prometheus.GaugeOpts{
192-
Name: "upload_proxy_sink_queue_length",
193-
Help: "Number of entries in the sink queue",
236+
totalTransientErrors: promauto.With(reg).NewCounter(prometheus.CounterOpts{
237+
Name: "upload_proxy_sink_transient_errors_total",
238+
Help: "Number of transient errors",
194239
ConstLabels: prometheus.Labels{
195240
"sink": cfg.Address,
196241
},
@@ -200,6 +245,9 @@ func NewSinkMetrics(reg prometheus.Registerer, cfg SinkConfig) *SinkMetrics {
200245

201246
func (m *SinkMetrics) deregister() {
202247
m.reg.Unregister(m.totalNumUploaded)
203-
m.reg.Unregister(m.totalUploadDelay)
204-
m.reg.Unregister(m.queueLength)
248+
m.reg.Unregister(m.totalNumDropped)
249+
m.reg.Unregister(m.totalQueueDelay)
250+
m.reg.Unregister(m.totalUploadDuration)
251+
m.reg.Unregister(m.totalBytesUploaded)
252+
m.reg.Unregister(m.totalTransientErrors)
205253
}

0 commit comments

Comments
 (0)