Skip to content

Commit 50733cc

Browse files
committed
exposed Prometheus metrics with cache statistics
Signed-off-by: Dmitry Shmulevich <dmitry.shmulevich@gmail.com>
1 parent 15442e5 commit 50733cc

File tree

6 files changed

+95
-57
lines changed

6 files changed

+95
-57
lines changed

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

2-
build: pushgateway
3-
go build ./cmd/pushgateway/
2+
build:
3+
CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' ./cmd/pushgateway/
44

55
ingest:
66
curl -X POST -H "Content-Type: text/plain" --data-binary @./examples/data.txt localhost:9753/ingest
@@ -20,4 +20,4 @@ start-prom:
2020
stop-prom:
2121
docker kill prom-srv; docker rm prom-srv
2222

23-
.PHONY: ingest metrics start-redis stop-redis start-prom stop-prom
23+
.PHONY: build ingest metrics start-redis stop-redis start-prom stop-prom

cmd/pushgateway/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func main() {
3434
a.Flag("tls.key", "Path to the server key.").StringVar(&cfg.TLSKeyPath)
3535
a.Flag("tls.cert", "Path to the server certificate.").StringVar(&cfg.TLSCertPath)
3636
a.Flag("metrics.path", "Metrics path.").Short('m').Default("/metrics").StringVar(&cfg.MetricsPath)
37+
a.Flag("telemetry.path", "Telemetry path.").Short('t').Default("/telemetry").StringVar(&cfg.TelemetryPath)
3738
a.Flag("ingest.path", "Ingest path.").Short('i').Default("/ingest").StringVar(&cfg.IngestPath)
3839
a.Flag("redis.endpoint", "Redis endpoint(s).").Default(":6379").StringVar(&cfg.RedisConfig.Endpoint)
3940
a.Flag("redis.expiration", "Redis key/value expiration.").Default("5m").DurationVar(&cfg.RedisConfig.Expiration)
@@ -89,7 +90,7 @@ func main() {
8990
level.Info(logger).Log("msg", "Starting Redis client...")
9091

9192
if err := db.Ping(ctx); err != nil {
92-
level.Error(logger).Log("mgs", "Failed to start Redis", "err", err)
93+
level.Error(logger).Log("mgs", "Failed to start Redis client", "err", err)
9394
return err
9495
}
9596
<-cancel

examples/pushgw.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
port: 9753
22
metrics_path: /metrics
33
ingest_path: /ingest
4+
telemetry_path: /telemetry
45
tls_enabled: false
56
redis:
67
endpoint: "localhost:6379"

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/kr/pretty v0.2.0 // indirect
99
github.com/oklog/run v1.0.0
1010
github.com/pkg/errors v0.9.1
11+
github.com/prometheus/client_golang v1.7.1
1112
github.com/prometheus/common v0.14.0
1213
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
1314
google.golang.org/protobuf v1.24.0 // indirect

pkg/config/config.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import (
99
)
1010

1111
type Config struct {
12-
Port int `yaml:"port"`
13-
MetricsPath string `yaml:"metrics_path"`
14-
IngestPath string `yaml:"ingest_path"`
15-
TLSEnabled bool `yaml:"tls_enabled"`
16-
TLSKeyPath string `yaml:"tls_key_path"`
17-
TLSCertPath string `yaml:"tls_cert_path"`
12+
Port int `yaml:"port"`
13+
MetricsPath string `yaml:"metrics_path"`
14+
TelemetryPath string `yaml:"telemetry_path"`
15+
IngestPath string `yaml:"ingest_path"`
16+
TLSEnabled bool `yaml:"tls_enabled"`
17+
TLSKeyPath string `yaml:"tls_key_path"`
18+
TLSCertPath string `yaml:"tls_cert_path"`
1819

1920
RedisConfig redis.RedisConfig `yaml:"redis"`
2021
}

pkg/metrics/manager.go

Lines changed: 81 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,31 @@ import (
66
"fmt"
77
"net/http"
88
"strings"
9+
"time"
910

1011
"github.com/dmitsh/pushgatewayredis/pkg/config"
1112
"github.com/dmitsh/pushgatewayredis/pkg/redis"
1213
"github.com/go-kit/kit/log"
1314
"github.com/go-kit/kit/log/level"
15+
"github.com/prometheus/client_golang/prometheus"
16+
"github.com/prometheus/client_golang/prometheus/promauto"
17+
"github.com/prometheus/client_golang/prometheus/promhttp"
18+
)
19+
20+
var (
21+
metricsCacheSize = promauto.NewGauge(
22+
prometheus.GaugeOpts{
23+
Name: "metrics_cache_size",
24+
Help: "Number of metrics in cache.",
25+
})
26+
27+
metricsRequestDuration = promauto.NewHistogramVec(
28+
prometheus.HistogramOpts{
29+
Name: "metrics_request_duration_seconds",
30+
Help: "Time (in seconds) spent serving metrics requests.",
31+
Buckets: prometheus.DefBuckets,
32+
},
33+
[]string{"status_code"})
1434
)
1535

1636
type MetricsManager struct {
@@ -26,59 +46,20 @@ func NewMetricsManager(logger log.Logger, cfg *config.Config, db *redis.RedisCli
2646
db: db,
2747
logger: logger,
2848
}
49+
50+
mux := http.NewServeMux()
51+
mux.HandleFunc(cfg.MetricsPath, mm.serveMetricsPath)
52+
mux.HandleFunc(cfg.IngestPath, mm.serveIngestPath)
53+
mux.Handle(cfg.TelemetryPath, promhttp.Handler())
54+
mux.HandleFunc("/", mm.serveDefault)
55+
2956
mm.server = &http.Server{
3057
Addr: fmt.Sprintf(":%d", cfg.Port),
31-
Handler: mm,
58+
Handler: mux,
3259
}
3360
return mm
3461
}
3562

36-
func (mm *MetricsManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
37-
switch r.URL.Path {
38-
case mm.cfg.MetricsPath:
39-
if r.Method != "GET" {
40-
http.Error(w, "Method is not supported.", http.StatusNotFound)
41-
return
42-
}
43-
metrics, err := mm.db.GetAll(r.Context())
44-
if err != nil {
45-
level.Error(mm.logger).Log("msg", "Redis error", "err", err)
46-
http.Error(w, "redis error", http.StatusInternalServerError)
47-
return
48-
}
49-
fmt.Fprintf(w, strings.Join(metrics, "\n"))
50-
case mm.cfg.IngestPath:
51-
if r.Method != "POST" {
52-
http.Error(w, "Method is not supported.", http.StatusNotFound)
53-
return
54-
}
55-
scanner := bufio.NewScanner(r.Body)
56-
keys, vals := []string{}, []string{}
57-
for scanner.Scan() {
58-
line := strings.TrimSpace(scanner.Text())
59-
if strings.HasPrefix(line, "#") {
60-
continue
61-
}
62-
if indx := strings.LastIndex(line, " "); indx != -1 {
63-
keys = append(keys, line[:indx])
64-
vals = append(vals, line[indx:])
65-
}
66-
}
67-
if err := scanner.Err(); err != nil {
68-
level.Error(mm.logger).Log("msg", "Read error", "err", err)
69-
http.Error(w, "can't read body", http.StatusBadRequest)
70-
return
71-
}
72-
if err := mm.db.MSet(r.Context(), keys, vals); err != nil {
73-
level.Error(mm.logger).Log("msg", "Redis error", "err", err)
74-
http.Error(w, "redis error", http.StatusInternalServerError)
75-
return
76-
}
77-
default:
78-
http.Error(w, "404 not found.", http.StatusNotFound)
79-
}
80-
}
81-
8263
func (mm *MetricsManager) Run() error {
8364
if mm.cfg.TLSEnabled {
8465
return mm.server.ListenAndServeTLS(mm.cfg.TLSCertPath, mm.cfg.TLSKeyPath)
@@ -89,3 +70,56 @@ func (mm *MetricsManager) Run() error {
8970
func (mm *MetricsManager) Close(ctx context.Context) error {
9071
return mm.server.Shutdown(ctx)
9172
}
73+
74+
func (mm *MetricsManager) serveMetricsPath(w http.ResponseWriter, r *http.Request) {
75+
start := time.Now()
76+
if r.Method != "GET" {
77+
metricsRequestDuration.WithLabelValues("405").Observe(time.Now().Sub(start).Seconds())
78+
http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed)
79+
return
80+
}
81+
metrics, err := mm.db.GetAll(r.Context())
82+
if err != nil {
83+
level.Error(mm.logger).Log("msg", "Redis error", "err", err)
84+
metricsRequestDuration.WithLabelValues("500").Observe(time.Now().Sub(start).Seconds())
85+
http.Error(w, "Redis error", http.StatusInternalServerError)
86+
return
87+
}
88+
metricsRequestDuration.WithLabelValues("200").Observe(time.Now().Sub(start).Seconds())
89+
metricsCacheSize.Set(float64(len(metrics)))
90+
fmt.Fprintf(w, strings.Join(metrics, "\n"))
91+
}
92+
93+
func (mm *MetricsManager) serveIngestPath(w http.ResponseWriter, r *http.Request) {
94+
if r.Method != "POST" {
95+
http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed)
96+
return
97+
}
98+
scanner := bufio.NewScanner(r.Body)
99+
keys, vals := []string{}, []string{}
100+
for scanner.Scan() {
101+
line := strings.TrimSpace(scanner.Text())
102+
if strings.HasPrefix(line, "#") {
103+
continue
104+
}
105+
if indx := strings.LastIndex(line, " "); indx != -1 {
106+
keys = append(keys, line[:indx])
107+
vals = append(vals, line[indx:])
108+
}
109+
}
110+
if err := scanner.Err(); err != nil {
111+
level.Error(mm.logger).Log("msg", "Read error", "err", err)
112+
http.Error(w, "Cannot read body", http.StatusBadRequest)
113+
return
114+
}
115+
if err := mm.db.MSet(r.Context(), keys, vals); err != nil {
116+
level.Error(mm.logger).Log("msg", "Redis error", "err", err)
117+
http.Error(w, "Redis error", http.StatusInternalServerError)
118+
return
119+
}
120+
}
121+
122+
func (mm *MetricsManager) serveDefault(w http.ResponseWriter, r *http.Request) {
123+
level.Error(mm.logger).Log("msg", "Unsupported path", "url", r.URL.Path)
124+
http.Error(w, "404 page not found.", http.StatusNotFound)
125+
}

0 commit comments

Comments
 (0)