diff options
Diffstat (limited to 'echo/prometheus/prometheus.go')
-rw-r--r-- | echo/prometheus/prometheus.go | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/echo/prometheus/prometheus.go b/echo/prometheus/prometheus.go new file mode 100644 index 0000000..616e425 --- /dev/null +++ b/echo/prometheus/prometheus.go | |||
@@ -0,0 +1,159 @@ | |||
1 | package prometheus | ||
2 | |||
3 | import ( | ||
4 | "errors" | ||
5 | "net/http" | ||
6 | "strconv" | ||
7 | "time" | ||
8 | |||
9 | "github.com/labstack/echo/v4" | ||
10 | "github.com/labstack/echo/v4/middleware" | ||
11 | "github.com/prometheus/client_golang/prometheus" | ||
12 | "github.com/prometheus/client_golang/prometheus/promauto" | ||
13 | "github.com/prometheus/client_golang/prometheus/promhttp" | ||
14 | ) | ||
15 | |||
16 | type Prometheus struct { | ||
17 | config *PrometheusConfig | ||
18 | requestCount *prometheus.CounterVec | ||
19 | requestDuration *prometheus.HistogramVec | ||
20 | requestSize *prometheus.HistogramVec | ||
21 | responseSize *prometheus.HistogramVec | ||
22 | MetricsHandler echo.HandlerFunc | ||
23 | } | ||
24 | |||
25 | type PrometheusConfig struct { | ||
26 | Skipper middleware.Skipper | ||
27 | ExtractUrl func(c echo.Context) string | ||
28 | ExtractHost func(c echo.Context) string | ||
29 | ContextLabel string // Context var name to use as a prometheus URL label | ||
30 | MetricsPath string | ||
31 | Subsystem string | ||
32 | } | ||
33 | |||
34 | var DefaultPrometheusConfig = &PrometheusConfig{ | ||
35 | Skipper: middleware.DefaultSkipper, | ||
36 | ExtractUrl: func(c echo.Context) string { | ||
37 | return c.Path() | ||
38 | }, | ||
39 | ExtractHost: func(c echo.Context) string { | ||
40 | return c.Request().Host | ||
41 | }, | ||
42 | MetricsPath: "/metrics", | ||
43 | Subsystem: "echo", | ||
44 | } | ||
45 | |||
46 | func NewPrometheus() *Prometheus { | ||
47 | return NewPrometheusWithConfig(DefaultPrometheusConfig) | ||
48 | } | ||
49 | |||
50 | func NewPrometheusWithConfig(c *PrometheusConfig) *Prometheus { | ||
51 | if c.Subsystem == "" { | ||
52 | c.Subsystem = DefaultPrometheusConfig.Subsystem | ||
53 | } | ||
54 | if c.MetricsPath == "" { | ||
55 | c.MetricsPath = DefaultPrometheusConfig.MetricsPath | ||
56 | } | ||
57 | if c.Skipper == nil { | ||
58 | c.Skipper = middleware.DefaultSkipper | ||
59 | } | ||
60 | if c.ExtractUrl == nil { | ||
61 | c.ExtractUrl = DefaultPrometheusConfig.ExtractUrl | ||
62 | } | ||
63 | if c.ExtractHost == nil { | ||
64 | c.ExtractHost = DefaultPrometheusConfig.ExtractHost | ||
65 | } | ||
66 | |||
67 | return &Prometheus{ | ||
68 | config: c, | ||
69 | MetricsHandler: echo.WrapHandler(promhttp.Handler()), | ||
70 | requestCount: promauto.NewCounterVec(prometheus.CounterOpts{ | ||
71 | Subsystem: c.Subsystem, | ||
72 | Name: "requests_total", | ||
73 | Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", | ||
74 | }, []string{"code", "method", "host", "url"}), | ||
75 | requestDuration: promauto.NewHistogramVec(prometheus.HistogramOpts{ | ||
76 | Subsystem: c.Subsystem, | ||
77 | Name: "request_duration_seconds", | ||
78 | Help: "The HTTP request latencies in seconds.", | ||
79 | }, []string{"code", "method", "url"}), | ||
80 | requestSize: promauto.NewHistogramVec(prometheus.HistogramOpts{ | ||
81 | Subsystem: c.Subsystem, | ||
82 | Name: "response_size_bytes", | ||
83 | Help: "The HTTP response sizes in bytes.", | ||
84 | }, []string{"code", "method", "url"}), | ||
85 | responseSize: promauto.NewHistogramVec(prometheus.HistogramOpts{ | ||
86 | Subsystem: c.Subsystem, | ||
87 | Name: "request_size_bytes", | ||
88 | Help: "The HTTP request sizes in bytes.", | ||
89 | }, []string{"code", "method", "url"}), | ||
90 | } | ||
91 | } | ||
92 | |||
93 | func (p *Prometheus) MiddlewareHandler(next echo.HandlerFunc) echo.HandlerFunc { | ||
94 | return func(c echo.Context) error { | ||
95 | if c.Path() == p.config.MetricsPath { | ||
96 | return next(c) | ||
97 | } | ||
98 | |||
99 | if p.config.Skipper(c) { | ||
100 | return next(c) | ||
101 | } | ||
102 | |||
103 | start := time.Now() | ||
104 | reqSz := computeApproximateRequestSize(c.Request()) | ||
105 | err := next(c) | ||
106 | elapsed := float64(time.Since(start)) / float64(time.Second) | ||
107 | |||
108 | status := c.Response().Status | ||
109 | if err != nil { | ||
110 | var httpError *echo.HTTPError | ||
111 | if errors.As(err, &httpError) { | ||
112 | status = httpError.Code | ||
113 | } | ||
114 | if status == 0 || status == http.StatusOK { | ||
115 | status = http.StatusInternalServerError | ||
116 | } | ||
117 | } | ||
118 | |||
119 | url := p.config.ExtractUrl(c) | ||
120 | if len(p.config.ContextLabel) > 0 { | ||
121 | u := c.Get(p.config.ContextLabel) | ||
122 | if u == nil { | ||
123 | u = "unknown" | ||
124 | } | ||
125 | url = u.(string) | ||
126 | } | ||
127 | |||
128 | s := strconv.Itoa(status) | ||
129 | m := c.Request().Method | ||
130 | p.requestDuration.WithLabelValues(s, m, url).Observe(elapsed) | ||
131 | p.requestCount.WithLabelValues(s, m, p.config.ExtractHost(c), url).Inc() | ||
132 | p.requestSize.WithLabelValues(s, m, url).Observe(float64(reqSz)) | ||
133 | p.responseSize.WithLabelValues(s, m, url).Observe(float64(c.Response().Size)) | ||
134 | |||
135 | return err | ||
136 | } | ||
137 | } | ||
138 | |||
139 | func computeApproximateRequestSize(r *http.Request) int { | ||
140 | s := 0 | ||
141 | if r.URL != nil { | ||
142 | s = len(r.URL.Path) | ||
143 | } | ||
144 | |||
145 | s += len(r.Method) | ||
146 | s += len(r.Proto) | ||
147 | for name, values := range r.Header { | ||
148 | s += len(name) | ||
149 | for _, value := range values { | ||
150 | s += len(value) | ||
151 | } | ||
152 | } | ||
153 | s += len(r.Host) | ||
154 | |||
155 | if r.ContentLength != -1 { | ||
156 | s += int(r.ContentLength) | ||
157 | } | ||
158 | return s | ||
159 | } | ||