Skip to content

Commit d953693

Browse files
authored
Merge pull request #16 from massix/feat/skip-jpeg-images
Automatically skip JPEG images in CLI and Serve modes
2 parents 0d3b5d3 + 78ad2b3 commit d953693

9 files changed

Lines changed: 256 additions & 58 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Gleeter understands the following commands:
5555
* `help`: Prints help information, this is also the default behavior if no arguments are provided.
5656
* `version`: Prints the application version.
5757
* `latest`: Fetches the latest comic from XKCD and displays it in the terminal.
58-
* `random`: Fetches a random comic from XKCD. **Warning**: Some very old comics (IDs ranging from 1 to 120 and possibly more) might not display correctly due to unsupported PNG compression.
58+
* `random`: Fetches a random comic from XKCD and displays it in the terminal. Gleeter will automatically skip the comics using an unsupported format (very old comics were using the JPG extension), so you should always get a valid comic. This is a technical limitation of the Terminal Graphics Protocol.
5959
* `id <number>`: Fetches the comic with the specified ID and displays it in the terminal. Replace `<number>` with the desired comic ID.
6060
* `serve <port> <base_path>`: Starts a web server to serve the comics. `<port>` is optional and defaults to 8080. `<base_path>` is also optional and defaults to "". For example, `gleeter serve 3000 /comics` will start the server on port 3000 and serve the comics under the `/comics` path.
6161

@@ -190,6 +190,8 @@ Example response:
190190

191191
This endpoint can be useful for monitoring the health and performance of the server.
192192

193+
The automatic skipping of unsupported images in random mode also applies when using the `/random` endpoint in serve mode, ensuring that only valid comics are served.
194+
193195
#### Prometheus Metrics
194196

195197
Gleeter exposes Prometheus metrics that can be used to monitor the server's performance and health. These metrics are available at the `/<base_path>/metrics` and **optionally** also at the `/metrics` path (the behavior is configurable via the configuration file).
@@ -205,6 +207,7 @@ The following metrics are exposed:
205207
* `gleeter:cached_elements_total`: A counter of the total number of elements currently stored in the cache.
206208
* `gleeter:memory_used_bytes`: A gauge of the memory used by the Erlang VM in bytes, with labels for different memory slices (e.g., "system", "processes", "ets", "code", "binary", "atom").
207209
* `gleeter:http_invalid_requests_total`: A counter of the total number of invalid HTTP requests.
210+
* `gleeter:jpeg_retried_total`: A counter of the total number of times the server had to retry because it was getting a JPEG image.
208211

209212
These metrics can be used to create dashboards and alerts to monitor the Gleeter server's performance.
210213

docker-test/Gleeter-dashboard.json

Lines changed: 122 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,15 @@
140140
"pluginVersion": "12.0.2",
141141
"targets": [
142142
{
143+
"datasource": {
144+
"type": "prometheus",
145+
"uid": "${DS_PROMETHEUS}"
146+
},
143147
"editorMode": "code",
144148
"expr": "sum (gleeter:processed_queries_total) by (method)",
145149
"legendFormat": "__auto",
146150
"range": true,
147-
"refId": "A",
148-
"datasource": {
149-
"type": "prometheus",
150-
"uid": "${DS_PROMETHEUS}"
151-
}
151+
"refId": "A"
152152
}
153153
],
154154
"title": "Total processed queries",
@@ -201,15 +201,15 @@
201201
"pluginVersion": "12.0.2",
202202
"targets": [
203203
{
204+
"datasource": {
205+
"type": "prometheus",
206+
"uid": "${DS_PROMETHEUS}"
207+
},
204208
"editorMode": "code",
205209
"expr": "sum (gleeter:cached_elements_total) by (instance)",
206210
"legendFormat": "__auto",
207211
"range": true,
208-
"refId": "A",
209-
"datasource": {
210-
"type": "prometheus",
211-
"uid": "${DS_PROMETHEUS}"
212-
}
212+
"refId": "A"
213213
}
214214
],
215215
"title": "Cached elements",
@@ -377,7 +377,7 @@
377377
},
378378
"gridPos": {
379379
"h": 9,
380-
"w": 24,
380+
"w": 17,
381381
"x": 0,
382382
"y": 17
383383
},
@@ -398,18 +398,116 @@
398398
"pluginVersion": "12.0.2",
399399
"targets": [
400400
{
401+
"datasource": {
402+
"type": "prometheus",
403+
"uid": "${DS_PROMETHEUS}"
404+
},
401405
"editorMode": "code",
402406
"expr": "sum (rate(gleeter:processed_queries_total[10m])) by (method)",
403407
"legendFormat": "__auto",
404408
"range": true,
409+
"refId": "A"
410+
}
411+
],
412+
"title": "Processed queries per second",
413+
"type": "timeseries"
414+
},
415+
{
416+
"datasource": {
417+
"type": "prometheus",
418+
"uid": "${DS_PROMETHEUS}"
419+
},
420+
"fieldConfig": {
421+
"defaults": {
422+
"color": {
423+
"mode": "palette-classic"
424+
},
425+
"custom": {
426+
"axisBorderShow": false,
427+
"axisCenteredZero": false,
428+
"axisColorMode": "text",
429+
"axisLabel": "",
430+
"axisPlacement": "auto",
431+
"barAlignment": 0,
432+
"barWidthFactor": 0.6,
433+
"drawStyle": "line",
434+
"fillOpacity": 0,
435+
"gradientMode": "none",
436+
"hideFrom": {
437+
"legend": false,
438+
"tooltip": false,
439+
"viz": false
440+
},
441+
"insertNulls": false,
442+
"lineInterpolation": "linear",
443+
"lineWidth": 1,
444+
"pointSize": 5,
445+
"scaleDistribution": {
446+
"type": "linear"
447+
},
448+
"showPoints": "auto",
449+
"spanNulls": false,
450+
"stacking": {
451+
"group": "A",
452+
"mode": "none"
453+
},
454+
"thresholdsStyle": {
455+
"mode": "off"
456+
}
457+
},
458+
"mappings": [],
459+
"thresholds": {
460+
"mode": "absolute",
461+
"steps": [
462+
{
463+
"color": "green"
464+
},
465+
{
466+
"color": "red",
467+
"value": 80
468+
}
469+
]
470+
}
471+
},
472+
"overrides": []
473+
},
474+
"gridPos": {
475+
"h": 9,
476+
"w": 7,
477+
"x": 17,
478+
"y": 17
479+
},
480+
"id": 7,
481+
"options": {
482+
"legend": {
483+
"calcs": [
484+
"last"
485+
],
486+
"displayMode": "list",
487+
"placement": "bottom",
488+
"showLegend": true
489+
},
490+
"tooltip": {
491+
"hideZeros": false,
492+
"mode": "single",
493+
"sort": "none"
494+
}
495+
},
496+
"pluginVersion": "12.0.2",
497+
"targets": [
498+
{
499+
"editorMode": "code",
500+
"expr": "gleeter:jpeg_retried_total",
501+
"legendFormat": "Retried",
502+
"range": true,
405503
"refId": "A",
406504
"datasource": {
407505
"type": "prometheus",
408506
"uid": "${DS_PROMETHEUS}"
409507
}
410508
}
411509
],
412-
"title": "Processed queries per second",
510+
"title": "Retried JPEG",
413511
"type": "timeseries"
414512
},
415513
{
@@ -495,15 +593,15 @@
495593
"pluginVersion": "12.0.2",
496594
"targets": [
497595
{
596+
"datasource": {
597+
"type": "prometheus",
598+
"uid": "${DS_PROMETHEUS}"
599+
},
498600
"editorMode": "code",
499601
"expr": "sum (rate(gleeter:http_request_duration_seconds_sum[5m]) / rate(gleeter:http_request_duration_seconds_count[5m])) by (method)",
500602
"legendFormat": "__auto",
501603
"range": true,
502-
"refId": "A",
503-
"datasource": {
504-
"type": "prometheus",
505-
"uid": "${DS_PROMETHEUS}"
506-
}
604+
"refId": "A"
507605
}
508606
],
509607
"title": "Average request duration",
@@ -591,15 +689,15 @@
591689
"pluginVersion": "12.0.2",
592690
"targets": [
593691
{
692+
"datasource": {
693+
"type": "prometheus",
694+
"uid": "${DS_PROMETHEUS}"
695+
},
594696
"editorMode": "code",
595697
"expr": "gleeter:cached_elements_total",
596698
"legendFormat": "Total cached elements",
597699
"range": true,
598-
"refId": "Total Elements",
599-
"datasource": {
600-
"type": "prometheus",
601-
"uid": "${DS_PROMETHEUS}"
602-
}
700+
"refId": "Total Elements"
603701
},
604702
{
605703
"datasource": {
@@ -646,6 +744,6 @@
646744
"timezone": "browser",
647745
"title": "Gleeter",
648746
"uid": "22949757-c3b8-4009-bc64-5d8ef0ca9e33",
649-
"version": 5,
747+
"version": 2,
650748
"weekStart": ""
651-
}
749+
}

docker-test/docker-compose.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,7 @@ services:
3838
- target: 9999
3939
published: 8080
4040
protocol: tcp
41+
environment:
42+
GLEETER_DEBUG: "1"
4143
networks: [internal]
4244
command: ["serve", "9999", "comics"]

src/gleeter.gleam

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import gleam/io
55
import gleam/list
66
import gleam/option
77
import gleam/result
8+
import gleam/uri
89
import gleeter/application_behavior
910
import gleeter/cache
1011
import gleeter/config
1112
import gleeter/debug.{debug_print}
1213
import gleeter/graphics
1314
import gleeter/png
1415
import gleeter/serve
16+
import gleeter/utils
1517
import gleeter/version
1618
import gleeter/xkcd
1719

@@ -36,7 +38,10 @@ fn print_version() -> Result(Nil, Nil) {
3638
}
3739

3840
// Get a comic from the cache or fetch it from the APIs and then store in the cache
39-
fn get_comic(cache: cache.Cache, id: Int) -> Result(cache.ComicWithData, Nil) {
41+
fn get_comic_from_cache(
42+
cache: cache.Cache,
43+
id: Int,
44+
) -> Result(cache.ComicWithData, Nil) {
4045
case cache.get_comic(cache, id) {
4146
option.Some(cd) -> Ok(cd)
4247
option.None -> {
@@ -71,26 +76,39 @@ fn get_comic(cache: cache.Cache, id: Int) -> Result(cache.ComicWithData, Nil) {
7176
}
7277
}
7378

74-
fn print_comic(
75-
cache: cache.Cache,
76-
config: config.Configuration,
79+
fn get_comic(
7780
in: PrintComic,
78-
) -> Result(Nil, Nil) {
79-
use cached_comic <- result.try(case in {
80-
Latest -> get_comic(cache, 0)
81+
cache: cache.Cache,
82+
) -> Result(cache.ComicWithData, Nil) {
83+
case in {
84+
Latest -> get_comic_from_cache(cache, 0)
85+
ID(id) -> get_comic_from_cache(cache, id)
8186
Random -> {
8287
use xkcd.Xkcd(number: highest, ..) <- result.try(
8388
xkcd.get_latest() |> result.map_error(print_api_error),
8489
)
85-
let random_comic = case config.random_start {
86-
option.Some(start) -> int.random(highest - start) + start
87-
option.None -> int.random(highest)
90+
let random_comic = int.random(highest)
91+
use cache.ComicWithData(xkcd.Xkcd(img_url:, ..), ..) as r <- result.try(
92+
get_comic_from_cache(cache, random_comic),
93+
)
94+
let uri_string = uri.to_string(img_url)
95+
case utils.is_jpeg(uri_string) {
96+
False -> Ok(r)
97+
True -> {
98+
debug_print("Skipping JPEG comic: " <> uri_string)
99+
get_comic(in, cache)
100+
}
88101
}
89-
get_comic(cache, random_comic)
90102
}
91-
ID(id) -> get_comic(cache, id)
92-
})
103+
}
104+
}
93105

106+
fn print_comic(
107+
cache: cache.Cache,
108+
config: config.Configuration,
109+
in: PrintComic,
110+
) -> Result(Nil, Nil) {
111+
use cached_comic <- result.try(get_comic(in, cache))
94112
let cache.ComicWithData(xkcd, body, raw_data) = cached_comic
95113
use image_size <- result.try(png.get_image_size(raw_data))
96114
let terminal_size = graphics.get_terminal_size()

src/gleeter/metrics.gleam

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const memory_used = "gleeter:memory_used_bytes"
2424

2525
const invalid_requests = "gleeter:http_invalid_requests_total"
2626

27+
const jpeg_retried = "gleeter:jpeg_retried_total"
28+
2729
type Memory {
2830
Processes(m: Int)
2931
ProcessesUsed(m: Int)
@@ -52,6 +54,7 @@ pub fn init() -> Nil {
5254
counter.new(invalid_requests, "Number of invalid HTTP requests")
5355
let assert Ok(_) =
5456
gauge.new(memory_used, "Memory used in bytes by the Erlang VM")
57+
let assert Ok(_) = counter.new(jpeg_retried, "Number of JPEG retries")
5558

5659
let buckets =
5760
set.from_list([
@@ -115,6 +118,11 @@ pub fn increment_cache_miss(method: String) -> Nil {
115118
Nil
116119
}
117120

121+
pub fn increment_jpeg_retries() -> Nil {
122+
let assert Ok(_) = counter.increment(jpeg_retried, dict.new())
123+
Nil
124+
}
125+
118126
pub fn update_memory() -> Nil {
119127
list.each(memory(), fn(m) {
120128
let #(label, memory) = case m {

0 commit comments

Comments
 (0)