Task 6.1.1: Get the source code
git clone https://github.com/acend/prometheus-training-go-instrumentation.git
Change into the freshly cloned git repository
cd prometheus-training-go-instrumentation
TODO: create Dockerfile
This application creates a simple webserver which listens at http://localhost:8083 and returns the string Prometheus Training after a random interval between 0 and 1000 milliseconds. You can test the app by building it as a docker image using docker build -t prometheus-training-go-instrumentation:local . and then running the built image using docker run --rm -p 8083:8083 prometheus-training-go-instrumentation:local. You can then (in a separate terminal) issue a request using the command curl http://localhost:8083.
Task 6.1.2: Instrument the Go example app
The Prometheus Go client library provides an easy way to add prometheus metrics to our own applications. In this task we will extend a simple Go application to expose these metrics.
Task description:
- Look at the described steps and try to understand them. As you are missing the needed tooling on the webshell, you will not be able to execute them yourself.
- You will be provided docker commands so you can build and test the added functionality
In order to collect the metrics and use the promhttp.Handler function we first need to download the prometheus/promhttp library:
go get github.com/prometheus/client_golang/prometheus/promhttp
Next we replace the line // insert prometheus library here in the main.go file with the following line:
"github.com/prometheus/client_golang/prometheus/promhttp"
Finally we need to add a handler for the /metrics endpoint. We replace the line // insert additional handlers here in the main.go file with the following line:
http.Handle("/metrics", promhttp.Handler())
The main.go of our example app now looks like this:
package main
import (
"fmt"
"net/http"
"math/rand"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
http.HandleFunc("/", ServeHandler)
http.Handle("/metrics", promhttp.Handler())
err := http.ListenAndServe(":8083", nil)
if err != nil {
fmt.Println(err)
}
}
func ServeHandler(w http.ResponseWriter, r *http.Request) {
rand.Seed(time.Now().UnixNano())
n := rand.Intn(1000)
time.Sleep(time.Duration(n)*time.Millisecond)
fmt.Fprintf(w, "Prometheus Training")
}
You can now rebuild and rerun the application. For this, switch into the solution folder and run docker build -t prometheus-training-go-instrumentation:local . and docker run --rm -p 8083:8083 prometheus-training-go-instrumentation:local.
Using a second terminal window we can now query the /metrics endpoint of our app and should receive a number of Prometheus metrics
curl localhost:8083/metrics
Expected result:
curl localhost:8083/metrics
# HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
# TYPE go_gc_cycles_automatic_gc_cycles_total counter
go_gc_cycles_automatic_gc_cycles_total 0
# HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
# TYPE go_gc_cycles_forced_gc_cycles_total counter
go_gc_cycles_forced_gc_cycles_total 0
# HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
# TYPE go_gc_cycles_total_gc_cycles_total counter
go_gc_cycles_total_gc_cycles_total 0
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
...
Task 6.1.3: Scrape config
TODO: prep image and create deployment + SM here
Task description:
- Configure Prometheus to scrape the metrics endpoint of the Go Application
Hints
Add a new Job to the scrape_configs in your /etc/prometheus/prometheus.yml:
scrape_configs:
...
- job_name: "go_app"
metrics_path: "/metrics"
static_configs:
- targets:
- "localhost:8083"
...
Note that you can always validate the configuration with:
promtool check config /etc/prometheus/prometheus.yml
Do not forget to reload or restart the Prometheus server.
Task 6.1.4: Metric names
Study the following metrics and decide if the metric name is ok
http_requests{handler="/", status="200"}
http_request_200_count{handler="/"}
go_memstats_heap_inuse_megabytes{instance="localhost:9090",job="prometheus"}
prometheus_build_info{branch="HEAD",goversion="go1.19.1",instance="localhost:9090",job="prometheus",revision="6d7f26c46ff70286944991f95d791dff03174eea",version="2.39.0"}
prometheus_config_last_reload_success_timestamp{instance="localhost:9090",job="prometheus"}
prometheus_tsdb_lowest_timestamp_minutes{instance="localhost:9090",job="prometheus"}
Hints
-
The
_totalsuffix should be appended, sohttp_requests_total{handler="/", status="200"}is better. -
There are two issues in
http_request_200_count{handler="/"}: The_countsuffix is foreseen for histograms, counters can be suffixed with_total. Second, status information should not be part of the metric name, a label{status="200"}is the better option. -
The base unit is
bytesnotmegabytes, sogo_memstats_heap_inuse_bytesis correct. -
Everything is ok with
prometheus_build_infoand its labels. It’s a good practice to export such base information with a gauge. -
In
prometheus_config_last_reload_success_timestamp, the base unit is missing, correct isprometheus_config_last_reload_success_timestamp_seconds. -
The base unit is
secondsfor timestamps, soprometheus_tsdb_lowest_timestamp_secondsis correct.
Task 6.1.5: Metric names (optional)
What kind of risk do you have, when you see such a metric
http_requests_total{path="/etc/passwd", status="404"} 1
Hints
There is no potential security vulnerability from exposing the /etc/passwd path, which seems to be handled appropriately in this case: no password is revealed.
From a Prometheus point of view, however, there is the risk of a DDoS attack: An attacker could easily make requests to paths which obviously don’t exist. As every request and therefore path is registered with a label, many new time series are created which could lead to a cardinality explosion and finally to out-of-memory errors.
It’s hard to recover from that!
For this case, it’s better just to count the 404 requests and to lookup the paths in the log files.
http_requests_total{status="404"} 15
Task 6.1.6: Custom metric (optional)
TODO: prepare custom metric lab (if wanted)
Task description:
- Create a custom histogram metric with the name
training_app_request_duration_secondswhich measures the time (per request) used by theServeHandlerhandler - Make a couple of requests to the example application
- Calculate the 85th percentile of the request duration of the example app
Hints
Download the following two prometheus libraries and add them to the imports in the main.go file:
- github.com/prometheus/client_golang/prometheus/promauto
- github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promauto \
github.com/prometheus/client_golang/prometheus
Add the imports to main.go (only the relevant section is shown in the example below)
...
import (
...
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus"
...
Add a new variable for the histogram metric to the main.go file (only the relevant section is shown in the example below):
...
var (
responseLatency = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "training_app_request_duration_seconds",
Help: "example app response latencies",
})
)
func main() {
...
Extend the existing ServeHandler function to measure the time between the start end the end of the function and update the histogram accordingly:
main.go (only the relevant section is shown in the example below)
...
func ServeHandler(w http.ResponseWriter, r *http.Request) {
requestReceived := time.Now()
rand.Seed(time.Now().UnixNano())
n := rand.Intn(1000)
time.Sleep(time.Duration(n)*time.Millisecond)
fmt.Fprintf(w, "Prometheus Training")
responseLatency.Observe(time.Since(requestReceived).Seconds())
}
By now your main.go file should look like this:
package main
import (
"fmt"
"net/http"
"math/rand"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus"
)
var (
responseLatency = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "training_app_request_duration_seconds",
Help: "example app response latencies",
})
)
func main() {
http.HandleFunc("/", ServeHandler)
http.Handle("/metrics", promhttp.Handler())
err := http.ListenAndServe(":8083", nil)
if err != nil {
fmt.Println(err)
}
}
func ServeHandler(w http.ResponseWriter, r *http.Request) {
requestReceived := time.Now()
rand.Seed(time.Now().UnixNano())
n := rand.Intn(1000)
time.Sleep(time.Duration(n)*time.Millisecond)
fmt.Fprintf(w, "Prometheus Training")
responseLatency.Observe(time.Since(requestReceived).Seconds())
}
Next we have to build and run the app again:
go run main.go
Now let’s create a couple of requests to our new endpoint. Make sure to run those commands from a second terminal window, while the Go application is still running.
for i in {1..100}; do (curl http://localhost:8083/ -o /dev/null -s &) done
Verify the Prometheus metrics endpoint and look for a metric with the name training_app_request_duration_seconds_bucket
curl http://localhost:8083/metrics
Expected result:
...
# HELP training_app_request_duration_seconds example app response latencies
# TYPE training_app_request_duration_seconds histogram
training_app_request_duration_seconds_bucket{le="0.005"} 0
training_app_request_duration_seconds_bucket{le="0.01"} 1
training_app_request_duration_seconds_bucket{le="0.025"} 1
training_app_request_duration_seconds_bucket{le="0.05"} 4
training_app_request_duration_seconds_bucket{le="0.1"} 10
training_app_request_duration_seconds_bucket{le="0.25"} 24
training_app_request_duration_seconds_bucket{le="0.5"} 55
training_app_request_duration_seconds_bucket{le="1"} 100
training_app_request_duration_seconds_bucket{le="2.5"} 100
training_app_request_duration_seconds_bucket{le="5"} 100
training_app_request_duration_seconds_bucket{le="10"} 100
training_app_request_duration_seconds_bucket{le="+Inf"} 100
training_app_request_duration_seconds_sum 48.73099980000001
training_app_request_duration_seconds_count 100
...
Let us now calculate the 85th percentile by navigating to the Prometheus Web Console by entering the following query in the Prometheus expression browser:
histogram_quantile(0.85, training_app_request_duration_seconds_bucket)
The result describes the number of seconds it took to process 85% of all requests (15% of the requests were slower than the calculated result).