Skip to content

Commit d27ef1b

Browse files
committed
Merge branch 'release-2.10.0'
Wercker fixes Fixing tests with a small sleep to sync state Add release pipeline (let's see if it works) Fixed some whitespace and names Prometheus metrics for some level of stat tracking (needs more work) UUID implementation and fallback upon ratelimt
2 parents 26dfd05 + 27efa32 commit d27ef1b

10 files changed

+260
-38
lines changed

AUTHORS

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ If you've made a contribution, please feel free to add your name below:
22

33
Luke Strickland (clone1018) <luke@axxim.net>
44
Luke Granger-Brown (lukegb) <minotar@lukegb.com>
5-
Luke Handle (LukeHandle) <luke.handle@googlemail.com>
5+
Luke Hanley (LukeHandle) <luke.handle@googlemail.com>
66
Connor Peet (connor4312) <connor@peet.io>

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ RUN apk --no-cache add ca-certificates
1212
COPY --from=builder /go/bin/imgd /imgd
1313
COPY config.example.gcfg /config.gcfg
1414
ENTRYPOINT ./imgd
15-
LABEL Name=imgd Version=2.9.5
15+
LABEL Name=imgd Version=2.10.0
1616
EXPOSE 8000

cache_off.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func (c *CacheOff) has(username string) bool {
1818

1919
// Should never be called.
2020
func (c *CacheOff) pull(username string) minecraft.Skin {
21-
char, _ := minecraft.FetchSkinForChar()
21+
char, _ := minecraft.FetchSkinForSteve()
2222
return char
2323
}
2424

cache_redis.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func (c *CacheRedis) has(username string) bool {
8989
// What to do when failing to pull a skin from redis
9090
func (c *CacheRedis) pullFailed(username string) minecraft.Skin {
9191
c.remove(username)
92-
char, _ := minecraft.FetchSkinForChar()
92+
char, _ := minecraft.FetchSkinForSteve()
9393
return char
9494
}
9595

@@ -174,19 +174,20 @@ func (c *CacheRedis) memory() uint64 {
174174
}
175175

176176
func getSkinFromReply(resp *redis.Reply) (minecraft.Skin, error) {
177+
skin := &minecraft.Skin{}
177178
respBytes, respErr := resp.Bytes()
178179
if respErr != nil {
179180
return minecraft.Skin{}, respErr
180181
}
181182

182183
imgBuf := bytes.NewReader(respBytes)
183184

184-
skin, skinErr := minecraft.DecodeSkin(imgBuf)
185+
skinErr := skin.Decode(imgBuf)
185186
if skinErr != nil {
186187
return minecraft.Skin{}, skinErr
187188
}
188189

189-
return skin, nil
190+
return *skin, nil
190191
}
191192

192193
// Parses a reply from redis INFO into a nice map.

http.go

+69-16
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"strconv"
77
"strings"
88

9+
"github.com/prometheus/client_golang/prometheus"
10+
"github.com/prometheus/client_golang/prometheus/promhttp"
11+
912
"github.com/gorilla/mux"
1013
"github.com/minotar/minecraft"
1114
)
@@ -16,12 +19,20 @@ type Router struct {
1619

1720
// Middleware function to manipulate our request and response.
1821
func imgdHandler(router http.Handler) http.Handler {
19-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22+
return metricChain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2023
w.Header().Set("Access-Control-Allow-Origin", "*")
2124
w.Header().Set("Access-Control-Allow-Methods", "GET")
2225
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding")
2326
router.ServeHTTP(w, r)
24-
})
27+
}))
28+
}
29+
30+
func metricChain(router http.Handler) http.Handler {
31+
return promhttp.InstrumentHandlerInFlight(inFlightGauge,
32+
promhttp.InstrumentHandlerDuration(requestDuration,
33+
promhttp.InstrumentHandlerResponseSize(responseSize, router),
34+
),
35+
)
2536
}
2637

2738
type NotFoundHandler struct{}
@@ -138,7 +149,9 @@ func (router *Router) Serve(resource string) {
138149
return
139150
}
140151

152+
processingTimer := prometheus.NewTimer(processingDuration.WithLabelValues(resource))
141153
err := router.ResolveMethod(skin, resource)(int(width))
154+
processingTimer.ObserveDuration()
142155
if err != nil {
143156
w.WriteHeader(http.StatusInternalServerError)
144157
fmt.Fprintf(w, "500 internal server error")
@@ -177,6 +190,8 @@ func (router *Router) Bind() {
177190
log.Infof("%s %s 200", r.RemoteAddr, r.RequestURI)
178191
})
179192

193+
router.Mux.Handle("/metrics", promhttp.Handler())
194+
180195
router.Mux.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) {
181196
w.Header().Set("Content-Type", "application/json")
182197
w.Write(stats.ToJSON())
@@ -191,32 +206,70 @@ func (router *Router) Bind() {
191206

192207
func fetchSkin(username string) *mcSkin {
193208
if username == "char" || username == "MHF_Steve" {
194-
skin, _ := minecraft.FetchSkinForChar()
209+
skin, _ := minecraft.FetchSkinForSteve()
195210
return &mcSkin{Skin: skin}
196211
}
197212

213+
hasTimer := prometheus.NewTimer(cacheDuration.WithLabelValues("has"))
198214
if cache.has(strings.ToLower(username)) {
215+
hasTimer.ObserveDuration()
216+
pullTimer := prometheus.NewTimer(cacheDuration.WithLabelValues("pull"))
217+
defer pullTimer.ObserveDuration()
199218
stats.HitCache()
200219
return &mcSkin{Processed: nil, Skin: cache.pull(strings.ToLower(username))}
201220
}
221+
hasTimer.ObserveDuration()
222+
stats.MissCache()
202223

203-
skin, err := minecraft.FetchSkinFromMojang(username)
204-
if err != nil {
205-
log.Debugf("Failed Skin Mojang: %s (%s)", username, err.Error())
206-
// Let's fallback to S3 and try and serve at least an old skin...
207-
skin, err = minecraft.FetchSkinFromS3(username)
208-
if err != nil {
209-
log.Debugf("Failed Skin S3: %s (%s)", username, err.Error())
210-
// Well, looks like they don't exist after all.
211-
skin, _ = minecraft.FetchSkinForChar()
212-
stats.Errored("FallbackSteve")
224+
// Everyone loves nested if statements, right?
225+
var skin minecraft.Skin
226+
stats.APIRequested("GetUUID")
227+
uuid, err := minecraft.NormalizePlayerForUUID(username)
228+
if err != nil && err.Error() == "unable to GetAPIProfile: user not found" {
229+
log.Debugf("Failed UUID lookup: %s (%s)", username, err.Error())
230+
skin, _ = minecraft.FetchSkinForSteve()
231+
stats.Errored("UnknownUser")
232+
// Don't return yet to ensure we cache the failure
233+
} else {
234+
var catchErr error
235+
// Either no error, or there is one (eg. rate limit or network etc.), but they do possibly still exist
236+
if err != nil && err.Error() == "unable to GetAPIProfile: rate limited" {
237+
log.Noticef("Failed UUID lookup: %s (%s)", username, err.Error())
238+
stats.Errored("LookupUUIDRateLimit")
239+
catchErr = err
240+
} else if err != nil {
241+
// Other generic issues with looking up UUID, but still worth trying S3
242+
log.Infof("Failed UUID lookup: %s (%s)", username, err.Error())
243+
stats.Errored("LookupUUID")
244+
catchErr = err
213245
} else {
214-
stats.Errored("FallbackUsernameS3")
246+
// We have a UUID, so let's get a skin!
247+
sPTimer := prometheus.NewTimer(getDuration.WithLabelValues("SessionProfile"))
248+
skin, catchErr = minecraft.FetchSkinUUID(uuid)
249+
sPTimer.ObserveDuration()
250+
if catchErr != nil {
251+
log.Noticef("Failed Skin SessionProfile: %s (%s)", username, catchErr.Error())
252+
stats.Errored("SkinSessionProfile")
253+
}
254+
}
255+
if catchErr != nil {
256+
// Let's fallback to S3 and try and serve at least an old skin...
257+
s3Timer := prometheus.NewTimer(getDuration.WithLabelValues("S3"))
258+
skin, err = minecraft.FetchSkinUsernameS3(username)
259+
s3Timer.ObserveDuration()
260+
if err != nil {
261+
log.Debugf("Failed Skin S3: %s (%s)", username, err.Error())
262+
// Well, looks like they don't exist after all.
263+
skin, _ = minecraft.FetchSkinForSteve()
264+
stats.Errored("FallbackSteve")
265+
} else {
266+
stats.Errored("FallbackUsernameS3")
267+
}
215268
}
216269
}
217270

218-
stats.MissCache()
271+
addTimer := prometheus.NewTimer(cacheDuration.WithLabelValues("add"))
219272
cache.add(strings.ToLower(username), skin)
220-
273+
addTimer.ObserveDuration()
221274
return &mcSkin{Processed: nil, Skin: skin}
222275
}

main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const (
1616
MinWidth = uint(8)
1717
MaxWidth = uint(300)
1818

19-
ImgdVersion = "2.9.5"
19+
ImgdVersion = "2.10.0"
2020
)
2121

2222
var (

metrics.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package main
2+
3+
import "github.com/prometheus/client_golang/prometheus"
4+
5+
const namespace = "imgd"
6+
7+
var (
8+
inFlightGauge = prometheus.NewGauge(prometheus.GaugeOpts{
9+
Namespace: namespace,
10+
Subsystem: "http",
11+
Name: "in_flight_requests",
12+
Help: "A gauge of requests currently being served.",
13+
})
14+
15+
requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
16+
Namespace: namespace,
17+
Subsystem: "http",
18+
Name: "request_duration_seconds",
19+
Help: "Histogram of the time (in seconds) each HTTP request took.",
20+
Buckets: []float64{.001, .005, 0.0075, .01, .025, .1, .5, 1, 5},
21+
}, []string{"code"})
22+
23+
responseSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
24+
Namespace: namespace,
25+
Subsystem: "http",
26+
Name: "response_size_bytes",
27+
Help: "A histogram of response sizes (in bytes) for requests.",
28+
Buckets: []float64{100, 500, 1000, 2500, 5000},
29+
}, []string{})
30+
31+
processingDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
32+
Namespace: namespace,
33+
Subsystem: "image",
34+
Name: "processing_duration_seconds",
35+
Help: "Histogram of the time (in seconds) image processing took.",
36+
Buckets: []float64{.00025, .0005, 0.001, 0.0025, .005},
37+
}, []string{"resource"})
38+
39+
getDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
40+
Namespace: namespace,
41+
Subsystem: "texture",
42+
Name: "get_duration_seconds",
43+
Help: "Histogram of the time (in seconds) each texture GET took.",
44+
Buckets: []float64{.1, .25, .5, 1},
45+
}, []string{"source"})
46+
47+
cacheDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
48+
Namespace: namespace,
49+
Subsystem: "cache",
50+
Name: "operation_duration_seconds",
51+
Help: "Histogram of the time (in seconds) each cache operation took.",
52+
Buckets: []float64{.0005, .001, 0.0025, .005, 0.0075, 0.01, 0.1},
53+
}, []string{"operation"})
54+
55+
errorCounter = prometheus.NewCounterVec(
56+
prometheus.CounterOpts{
57+
Namespace: namespace,
58+
Subsystem: "status",
59+
Name: "errors",
60+
Help: "Error events",
61+
},
62+
[]string{"event"},
63+
)
64+
65+
cacheCounter = prometheus.NewCounterVec(
66+
prometheus.CounterOpts{
67+
Namespace: namespace,
68+
Subsystem: "status",
69+
Name: "cache",
70+
Help: "Cache status",
71+
},
72+
[]string{"status"},
73+
)
74+
75+
requestCounter = prometheus.NewCounterVec(
76+
prometheus.CounterOpts{
77+
Namespace: namespace,
78+
Subsystem: "status",
79+
Name: "requests",
80+
Help: "Resource requests",
81+
},
82+
[]string{"resource"},
83+
)
84+
85+
apiCounter = prometheus.NewCounterVec(
86+
prometheus.CounterOpts{
87+
Namespace: namespace,
88+
Subsystem: "status",
89+
Name: "apirequests",
90+
Help: "Requests to external APIs",
91+
},
92+
[]string{"call"},
93+
)
94+
95+
// Latency on Get (source of skin) :tick:
96+
// Total latency for HTTP request (response code) :tick:
97+
// Latency on cache (has, puul or add) :tick:
98+
// Gauge for cache hit, miss :tick:
99+
// Gauge for request (type) :tick:
100+
// Latency for processing (type) :tick:
101+
)
102+
103+
func init() {
104+
prometheus.MustRegister(inFlightGauge)
105+
prometheus.MustRegister(requestDuration)
106+
prometheus.MustRegister(responseSize)
107+
prometheus.MustRegister(processingDuration)
108+
prometheus.MustRegister(getDuration)
109+
prometheus.MustRegister(cacheDuration)
110+
prometheus.MustRegister(errorCounter)
111+
prometheus.MustRegister(cacheCounter)
112+
prometheus.MustRegister(requestCounter)
113+
prometheus.MustRegister(apiCounter)
114+
}

0 commit comments

Comments
 (0)