aboutsummaryrefslogtreecommitdiff
path: root/echo/prometheus/prometheus.go
diff options
context:
space:
mode:
Diffstat (limited to 'echo/prometheus/prometheus.go')
-rw-r--r--echo/prometheus/prometheus.go159
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 @@
1package prometheus
2
3import (
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
16type 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
25type 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
34var 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
46func NewPrometheus() *Prometheus {
47 return NewPrometheusWithConfig(DefaultPrometheusConfig)
48}
49
50func 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
93func (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
139func 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}