Skip to content

Commit 713a4a9

Browse files
committed
✨ add sample with prometheus metrics
0 parents  commit 713a4a9

File tree

17 files changed

+924
-0
lines changed

17 files changed

+924
-0
lines changed

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
bin
2+
mage-tools
3+
gg
4+
mage
5+
Taskfile.yml
6+
.gitignore
7+
.env
8+
prometheus
9+
grafana

.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Output of the go coverage tool, specifically when used with LiteIDE
15+
*.out
16+
17+
# Dependency directories (remove the comment below to include it)
18+
# vendor/
19+
20+
# Go workspace file
21+
go.work
22+
go.work.sum
23+
24+
# env file
25+
.env
26+
gg
27+
mage
28+
bin

Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM golang:1.22.4-alpine AS build
2+
WORKDIR /app
3+
COPY go.mod ./
4+
COPY go.sum ./
5+
RUN go mod download && go mod verify
6+
COPY . ./
7+
RUN go build -o /server ./cmd/main.go
8+
9+
FROM gcr.io/distroless/base-debian11
10+
WORKDIR /app
11+
COPY --from=build /server /server
12+
ENTRYPOINT [ "/server" ]

README.md

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# golang-sample-with-prometheus
2+
3+
This repository is to demo how to use prometheus to collect golang application data
4+
5+
## logic
6+
```golang
7+
package application
8+
9+
import (
10+
"encoding/json"
11+
"math/rand"
12+
"net/http"
13+
"strconv"
14+
"strings"
15+
"time"
16+
17+
"github.com/leetcode-golang-classroom/golang-sample-with-prometheus/internal/config"
18+
"github.com/prometheus/client_golang/prometheus"
19+
"github.com/prometheus/client_golang/prometheus/collectors"
20+
"github.com/prometheus/client_golang/prometheus/promhttp"
21+
)
22+
23+
type Device struct {
24+
ID int `json:"id"`
25+
Mac string `json:"mac"`
26+
Firmware string `json:"firmware"`
27+
}
28+
29+
type registerDevicesHandler struct {
30+
metrics *metrics
31+
}
32+
33+
type metrics struct {
34+
devices prometheus.Gauge
35+
info *prometheus.GaugeVec
36+
upgrades *prometheus.CounterVec
37+
duration *prometheus.HistogramVec
38+
loginDuration prometheus.Summary
39+
}
40+
41+
var devices []Device
42+
var version string
43+
44+
func init() {
45+
version = config.AppConfig.Version
46+
devices = []Device{
47+
{1, "5F-33-CC-1F-43-82", "2.1.6"},
48+
{2, "EF-2B-C4-F5-D6-34", "2.1.6"},
49+
}
50+
}
51+
52+
func (app *App) SetupMetricRoute() {
53+
reg := prometheus.NewRegistry()
54+
reg.MustRegister(collectors.NewGoCollector())
55+
m := NewMetrics(reg)
56+
m.devices.Set(float64(len(devices)))
57+
m.info.With(prometheus.Labels{"version": version}).Set(1)
58+
59+
promHandler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{
60+
Registry: reg,
61+
})
62+
app.metricsRouter.Handle("/metrics", promHandler)
63+
rdh := registerDevicesHandler{metrics: m}
64+
mdh := manageDevicesHandler{metrics: m}
65+
lh := loginHandler{}
66+
mlh := middleware(lh, m)
67+
app.deviceRouter.Handle("/devices", rdh)
68+
app.deviceRouter.Handle("/devices/", mdh)
69+
app.deviceRouter.Handle("/login", mlh)
70+
}
71+
72+
func getDevices(w http.ResponseWriter, _ *http.Request, m *metrics) {
73+
now := time.Now()
74+
b, err := json.Marshal(devices)
75+
if err != nil {
76+
http.Error(w, err.Error(), http.StatusBadRequest)
77+
return
78+
}
79+
sleep(200)
80+
81+
m.duration.With(prometheus.Labels{"method": "GET", "status": "200"}).Observe(time.Since(now).Seconds())
82+
w.Header().Set("Content-Type", "application/json")
83+
w.WriteHeader(http.StatusOK)
84+
w.Write(b)
85+
}
86+
87+
func NewMetrics(reg prometheus.Registerer) *metrics {
88+
m := &metrics{
89+
devices: prometheus.NewGauge(prometheus.GaugeOpts{
90+
Namespace: "myapp",
91+
Name: "connected_devices",
92+
Help: "Number of currently connected devices",
93+
}),
94+
info: prometheus.NewGaugeVec(prometheus.GaugeOpts{
95+
Namespace: "myapp",
96+
Name: "info",
97+
Help: "Information about the My App enviroment",
98+
}, []string{"version"}),
99+
upgrades: prometheus.NewCounterVec(prometheus.CounterOpts{
100+
Namespace: "myapp",
101+
Name: "device_upgrade_total",
102+
Help: "Number of upgraded devices",
103+
}, []string{"type"}),
104+
duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
105+
Namespace: "myapp",
106+
Name: "request_duration_seconds",
107+
Help: "Duration of the request.",
108+
// 4 times larger of apdex score
109+
// Buckets: prometheus.ExponentialBuckets(0.1, 1.5, 5),
110+
// Buckets: prometheus.LinearBuckets(0.1, 5, 5),
111+
Buckets: []float64{0.1, 0.15, 0.2, 0.25, 0.3},
112+
}, []string{"status", "method"}),
113+
loginDuration: prometheus.NewSummary(prometheus.SummaryOpts{
114+
Namespace: "myapp",
115+
Name: "login_request_duration_seconds",
116+
Help: "Duration of the login request",
117+
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
118+
}),
119+
}
120+
reg.MustRegister(m.devices, m.info, m.upgrades, m.duration, m.loginDuration)
121+
return m
122+
}
123+
124+
func (rdh registerDevicesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
125+
switch r.Method {
126+
case "GET":
127+
getDevices(w, r, rdh.metrics)
128+
case "POST":
129+
createDevice(w, r, rdh.metrics)
130+
default:
131+
w.Header().Set("Allow", "GET, POST")
132+
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
133+
}
134+
135+
}
136+
137+
func createDevice(w http.ResponseWriter, r *http.Request, m *metrics) {
138+
var device Device
139+
140+
err := json.NewDecoder(r.Body).Decode(&device)
141+
if err != nil {
142+
http.Error(w, err.Error(), http.StatusBadRequest)
143+
return
144+
}
145+
146+
devices = append(devices, device)
147+
m.devices.Set(float64(len(devices)))
148+
149+
w.WriteHeader(http.StatusCreated)
150+
w.Write([]byte("Device created!"))
151+
}
152+
153+
func upgradeDevice(w http.ResponseWriter, r *http.Request, m *metrics) {
154+
path := strings.TrimPrefix(r.URL.Path, "/devices/")
155+
156+
id, err := strconv.Atoi(path)
157+
if err != nil || id < 1 {
158+
http.NotFound(w, r)
159+
return
160+
}
161+
162+
var device Device
163+
err = json.NewDecoder(r.Body).Decode(&device)
164+
if err != nil {
165+
http.Error(w, err.Error(), http.StatusBadRequest)
166+
return
167+
}
168+
169+
for i := range devices {
170+
if devices[i].ID == id {
171+
devices[i].Firmware = device.Firmware
172+
}
173+
}
174+
175+
sleep(1000)
176+
177+
m.upgrades.With(prometheus.Labels{"type": "router"}).Inc()
178+
w.WriteHeader(http.StatusAccepted)
179+
w.Write([]byte("Upgrading..."))
180+
}
181+
182+
type manageDevicesHandler struct {
183+
metrics *metrics
184+
}
185+
186+
func (mdh manageDevicesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
187+
switch r.Method {
188+
case "PUT":
189+
upgradeDevice(w, r, mdh.metrics)
190+
default:
191+
w.Header().Set("Allow", "PUT")
192+
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
193+
}
194+
}
195+
196+
func sleep(ms int) {
197+
randGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
198+
now := time.Now()
199+
n := randGenerator.Intn(ms + now.Second())
200+
time.Sleep(time.Duration(n) * time.Millisecond)
201+
}
202+
203+
type loginHandler struct{}
204+
205+
func (l loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
206+
sleep(200)
207+
w.Write([]byte("Welcome to the app!"))
208+
}
209+
210+
func middleware(next http.Handler, m *metrics) http.Handler {
211+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
212+
now := time.Now()
213+
next.ServeHTTP(w, r)
214+
m.loginDuration.Observe(time.Since(now).Seconds())
215+
})
216+
}
217+
218+
```
219+
## metric this sample could build
220+
221+
![alt text](metric-sample.png)

Taskfile.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
version: '3'
2+
3+
dotenv: ['.env']
4+
5+
tasks:
6+
default:
7+
cmds:
8+
- echo "PORT=$PORT"
9+
silent: true
10+
11+
# build:
12+
# cmds:
13+
# - CGO_ENABLED=0 GOOS=linux go build -o bin/main cmd/main.go
14+
# silent: true
15+
# run:
16+
# cmds:
17+
# - ./bin/main
18+
# deps:
19+
# - build
20+
# silent: true
21+
22+
build-mage:
23+
cmds:
24+
- CGO_ENABLED=0 GOOS=linux go build -o ./mage mage-tools/mage.go
25+
silent: true
26+
27+
build-gg:
28+
cmds:
29+
- ./mage -d mage-tools -compile ../gg
30+
deps:
31+
- build-mage
32+
silent: true
33+
34+
coverage:
35+
cmds:
36+
- go test -v -cover ./...
37+
silent: true
38+
test:
39+
cmds:
40+
- go test -v ./...
41+
silent: true
42+

cmd/main.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"os"
7+
"os/signal"
8+
"syscall"
9+
10+
"github.com/leetcode-golang-classroom/golang-sample-with-prometheus/internal/application"
11+
"github.com/leetcode-golang-classroom/golang-sample-with-prometheus/internal/config"
12+
)
13+
14+
func main() {
15+
// 建立 application instance
16+
app := application.New(config.AppConfig)
17+
// 設定中斷訊號監聽
18+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt,
19+
syscall.SIGTERM, syscall.SIGINT)
20+
defer cancel()
21+
err := app.Start(ctx)
22+
if err != nil {
23+
log.Println("failed to start app:", err)
24+
}
25+
}

docker-compose.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
services:
2+
myapp:
3+
container_name: myapp
4+
build:
5+
context: .
6+
ports:
7+
- 8080:8080
8+
- 8081:8081
9+
environment:
10+
- VERSION=2.10.5
11+
- PORT=8080
12+
- DEVICE_PORT=8081
13+
14+
prometheus:
15+
image: prom/prometheus:v2.40.4
16+
ports:
17+
- 9090:9090
18+
volumes:
19+
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
20+
21+
grafana:
22+
image: grafana/grafana:9.3.0
23+
ports:
24+
- 3000:3000
25+
environment:
26+
- GF_SECURITY_ADMIN_USER=admin
27+
- GF_SECURITY_ADMIN_PASSWORD=devops123
28+
volumes:
29+
- ./grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
30+
- grafana:/var/lib/grafana
31+
32+
volumes:
33+
grafana:

0 commit comments

Comments
 (0)