|
| 1 | +--- |
| 2 | +title: Building the application |
| 3 | +linkTitle: Understand the application |
| 4 | +weight: 10 # |
| 5 | +keywords: go, golang, prometheus, grafana, containerize, monitor |
| 6 | +description: Learn how to create a Golang server to register metrics with Prometheus. |
| 7 | +--- |
| 8 | + |
| 9 | +## Prerequisites |
| 10 | + |
| 11 | +* You have a [Git client](https://git-scm.com/downloads). The examples in this section use a command-line based Git client, but you can use any client. |
| 12 | + |
| 13 | +You will be creating a Golang server with some endpoints to simulate a real-world application. Then you will expose metrics from the server using Prometheus. |
| 14 | + |
| 15 | +## Getting the sample application |
| 16 | + |
| 17 | +Clone the sample application to use with this guide. Open a terminal, change |
| 18 | +directory to a directory that you want to work in, and run the following |
| 19 | +command to clone the repository: |
| 20 | + |
| 21 | +```console |
| 22 | +$ git clone https://github.com/dockersamples/go-prometheus-monitoring.git |
| 23 | +``` |
| 24 | + |
| 25 | +Once you cloned you will see the following content structure inside `go-prometheus-monitoring` directory, |
| 26 | + |
| 27 | +```text |
| 28 | +go-prometheus-monitoring |
| 29 | +├── CONTRIBUTING.md |
| 30 | +├── Docker |
| 31 | +│ ├── grafana.yml |
| 32 | +│ └── prometheus.yml |
| 33 | +├── dashboard.json |
| 34 | +├── Dockerfile |
| 35 | +├── LICENSE |
| 36 | +├── README.md |
| 37 | +├── compose.yaml |
| 38 | +├── go.mod |
| 39 | +├── go.sum |
| 40 | +└── main.go |
| 41 | +``` |
| 42 | + |
| 43 | +- **main.go** - The entry point of the application. |
| 44 | +- **go.mod and go.sum** - Go module files. |
| 45 | +- **Dockerfile** - Dockerfile used to build the app. |
| 46 | +- **Docker/** - Contains the Docker Compose configuration files for Grafana and Prometheus. |
| 47 | +- **compose.yaml** - Compose file to launch everything (Golang app, Prometheus, and Grafana). |
| 48 | +- **dashboard.json** - Grafana dashboard configuration file. |
| 49 | +- **Dockerfile** - Dockerfile used to build the Golang app. |
| 50 | +- **compose.yaml** - Docker Compose file to launch everything (Golang app, Prometheus, and Grafana). |
| 51 | +- Other files are for licensing and documentation purposes. |
| 52 | + |
| 53 | +## Understanding the application |
| 54 | + |
| 55 | +The following is the complete logic of the application you will find in `main.go`. |
| 56 | + |
| 57 | +```go |
| 58 | +package main |
| 59 | + |
| 60 | +import ( |
| 61 | + "strconv" |
| 62 | + |
| 63 | + "github.com/gin-gonic/gin" |
| 64 | + "github.com/prometheus/client_golang/prometheus" |
| 65 | + "github.com/prometheus/client_golang/prometheus/promhttp" |
| 66 | +) |
| 67 | + |
| 68 | +// Define metrics |
| 69 | +var ( |
| 70 | + HttpRequestTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ |
| 71 | + Name: "api_http_request_total", |
| 72 | + Help: "Total number of requests processed by the API", |
| 73 | + }, []string{"path", "status"}) |
| 74 | + |
| 75 | + HttpRequestErrorTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ |
| 76 | + Name: "api_http_request_error_total", |
| 77 | + Help: "Total number of errors returned by the API", |
| 78 | + }, []string{"path", "status"}) |
| 79 | +) |
| 80 | + |
| 81 | +// Custom registry (without default Go metrics) |
| 82 | +var customRegistry = prometheus.NewRegistry() |
| 83 | + |
| 84 | +// Register metrics with custom registry |
| 85 | +func init() { |
| 86 | + customRegistry.MustRegister(HttpRequestTotal, HttpRequestErrorTotal) |
| 87 | +} |
| 88 | + |
| 89 | +func main() { |
| 90 | + router := gin.Default() |
| 91 | + |
| 92 | + // Register /metrics before middleware |
| 93 | + router.GET("/metrics", PrometheusHandler()) |
| 94 | + |
| 95 | + router.Use(RequestMetricsMiddleware()) |
| 96 | + router.GET("/health", func(c *gin.Context) { |
| 97 | + c.JSON(200, gin.H{ |
| 98 | + "message": "Up and running!", |
| 99 | + }) |
| 100 | + }) |
| 101 | + router.GET("/v1/users", func(c *gin.Context) { |
| 102 | + c.JSON(200, gin.H{ |
| 103 | + "message": "Hello from /v1/users", |
| 104 | + }) |
| 105 | + }) |
| 106 | + |
| 107 | + router.Run(":8000") |
| 108 | +} |
| 109 | + |
| 110 | +// Custom metrics handler with custom registry |
| 111 | +func PrometheusHandler() gin.HandlerFunc { |
| 112 | + h := promhttp.HandlerFor(customRegistry, promhttp.HandlerOpts{}) |
| 113 | + return func(c *gin.Context) { |
| 114 | + h.ServeHTTP(c.Writer, c.Request) |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +// Middleware to record incoming requests metrics |
| 119 | +func RequestMetricsMiddleware() gin.HandlerFunc { |
| 120 | + return func(c *gin.Context) { |
| 121 | + path := c.Request.URL.Path |
| 122 | + c.Next() |
| 123 | + status := c.Writer.Status() |
| 124 | + if status < 400 { |
| 125 | + HttpRequestTotal.WithLabelValues(path, strconv.Itoa(status)).Inc() |
| 126 | + } else { |
| 127 | + HttpRequestErrorTotal.WithLabelValues(path, strconv.Itoa(status)).Inc() |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +In this part of the code, you have imported the required packages `gin`, `prometheus`, and `promhttp`. Then you have defined a couple of variables, `HttpRequestTotal` and `HttpRequestErrorTotal` are Prometheus counter metrics, and `customRegistry` is a custom registry that will be used to register these metrics. The name of the metric is a string that you can use to identify the metric. The help string is a string that will be shown when you query the `/metrics` endpoint to understand the metric. The reason you are using the custom registry is so avoid the default Go metrics that are registered by default by the Prometheus client. Then using the `init` function you are registering the metrics with the custom registry. |
| 134 | + |
| 135 | +```go |
| 136 | +import ( |
| 137 | + "strconv" |
| 138 | + |
| 139 | + "github.com/gin-gonic/gin" |
| 140 | + "github.com/prometheus/client_golang/prometheus" |
| 141 | + "github.com/prometheus/client_golang/prometheus/promhttp" |
| 142 | +) |
| 143 | + |
| 144 | +// Define metrics |
| 145 | +var ( |
| 146 | + HttpRequestTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ |
| 147 | + Name: "api_http_request_total", |
| 148 | + Help: "Total number of requests processed by the API", |
| 149 | + }, []string{"path", "status"}) |
| 150 | + |
| 151 | + HttpRequestErrorTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ |
| 152 | + Name: "api_http_request_error_total", |
| 153 | + Help: "Total number of errors returned by the API", |
| 154 | + }, []string{"path", "status"}) |
| 155 | +) |
| 156 | + |
| 157 | +// Custom registry (without default Go metrics) |
| 158 | +var customRegistry = prometheus.NewRegistry() |
| 159 | + |
| 160 | +// Register metrics with custom registry |
| 161 | +func init() { |
| 162 | + customRegistry.MustRegister(HttpRequestTotal, HttpRequestErrorTotal) |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +In the `main` function, you have created a new instance of the `gin` framework and created three routes. You can see the health endpoint that is on path `/health` that will return a JSON with `{"message": "Up and running!"}` and the `/v1/users` endpoint that will return a JSON with `{"message": "Hello from /v1/users"}`. The third route is for the `/metrics` endpoint that will return the metrics in the Prometheus format. Then you have `RequestMetricsMiddleware` middleware, it will be called for every request made to the API. It will record the incoming requests metrics like status codes and paths. Finally, you are running the gin application on port 8000. |
| 167 | + |
| 168 | +```golang |
| 169 | +func main() { |
| 170 | + router := gin.Default() |
| 171 | + |
| 172 | + // Register /metrics before middleware |
| 173 | + router.GET("/metrics", PrometheusHandler()) |
| 174 | + |
| 175 | + router.Use(RequestMetricsMiddleware()) |
| 176 | + router.GET("/health", func(c *gin.Context) { |
| 177 | + c.JSON(200, gin.H{ |
| 178 | + "message": "Up and running!", |
| 179 | + }) |
| 180 | + }) |
| 181 | + router.GET("/v1/users", func(c *gin.Context) { |
| 182 | + c.JSON(200, gin.H{ |
| 183 | + "message": "Hello from /v1/users", |
| 184 | + }) |
| 185 | + }) |
| 186 | + |
| 187 | + router.Run(":8000") |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +Now comes the middleware function `RequestMetricsMiddleware`. This function is called for every request made to the API. It increments the `HttpRequestTotal` counter (different counter for different paths and status codes) if the status code is less than or equal to 400. If the status code is greater than 400, it increments the `HttpRequestErrorTotal` counter (different counter for different paths and status codes). The `PrometheusHandler` function is the custom handler that will be called for the `/metrics` endpoint. It will return the metrics in the Prometheus format. |
| 192 | + |
| 193 | +```golang |
| 194 | +// Custom metrics handler with custom registry |
| 195 | +func PrometheusHandler() gin.HandlerFunc { |
| 196 | + h := promhttp.HandlerFor(customRegistry, promhttp.HandlerOpts{}) |
| 197 | + return func(c *gin.Context) { |
| 198 | + h.ServeHTTP(c.Writer, c.Request) |
| 199 | + } |
| 200 | +} |
| 201 | + |
| 202 | +// Middleware to record incoming requests metrics |
| 203 | +func RequestMetricsMiddleware() gin.HandlerFunc { |
| 204 | + return func(c *gin.Context) { |
| 205 | + path := c.Request.URL.Path |
| 206 | + c.Next() |
| 207 | + status := c.Writer.Status() |
| 208 | + if status < 400 { |
| 209 | + HttpRequestTotal.WithLabelValues(path, strconv.Itoa(status)).Inc() |
| 210 | + } else { |
| 211 | + HttpRequestErrorTotal.WithLabelValues(path, strconv.Itoa(status)).Inc() |
| 212 | + } |
| 213 | + } |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +That's it, this was the complete gist of the application. Now it's time to run and test if the app is registering metrics correctly. |
| 218 | + |
| 219 | +## Running the application |
| 220 | + |
| 221 | +Make sure you are still inside `go-prometheus-monitoring` directory in the terminal, and run the following command. Install the dependencies by running `go mod tidy` and then build and run the application by running `go run main.go`. Then visit `http://localhost:8000/health` or `http://localhost:8000/v1/users`. You should see the output `{"message": "Up and running!"}` or `{"message": "Hello from /v1/users"}`. If you are able to see this then your app is successfully up and running. |
| 222 | + |
| 223 | + |
| 224 | +Now, check your application's metrics by accessing the `/metrics` endpoint. |
| 225 | +Open `http://localhost:8000/metrics` in your browser. You should see similar output to the following. |
| 226 | + |
| 227 | +```sh |
| 228 | +# HELP api_http_request_error_total Total number of errors returned by the API |
| 229 | +# TYPE api_http_request_error_total counter |
| 230 | +api_http_request_error_total{path="/",status="404"} 1 |
| 231 | +api_http_request_error_total{path="//v1/users",status="404"} 1 |
| 232 | +api_http_request_error_total{path="/favicon.ico",status="404"} 1 |
| 233 | +# HELP api_http_request_total Total number of requests processed by the API |
| 234 | +# TYPE api_http_request_total counter |
| 235 | +api_http_request_total{path="/health",status="200"} 2 |
| 236 | +api_http_request_total{path="/v1/users",status="200"} 1 |
| 237 | +``` |
| 238 | + |
| 239 | +In the terminal, press `ctrl` + `c` to stop the application. |
| 240 | + |
| 241 | +> [!Note] |
| 242 | +> If you don't want to run the application locally, and want to run it in a Docker container, skip to next page where you create a Dockerfile and containerize the application. |
| 243 | +
|
| 244 | +## Summary |
| 245 | + |
| 246 | +In this section, you learned how to create a Golang app to register metrics with Prometheus. By implementing middleware functions, you were able to increment the counters based on the request path and status codes. |
| 247 | + |
| 248 | +## Next steps |
| 249 | + |
| 250 | +In the next section, you'll learn how to containerize your application. |
0 commit comments