diff options
author | Mike Crute <mike@crute.us> | 2021-11-12 20:54:39 -0800 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2021-11-12 20:54:39 -0800 |
commit | 4d8fde6a8882e63e8dfafdf9f62c73b7b1036ebf (patch) | |
tree | 3e2380dc9bbae828fb0a6f72aae964742d24d953 | |
parent | 6961a3dfea279d8ff636ca27e1c6b52f65130992 (diff) | |
download | golib-4d8fde6a8882e63e8dfafdf9f62c73b7b1036ebf.tar.bz2 golib-4d8fde6a8882e63e8dfafdf9f62c73b7b1036ebf.tar.xz golib-4d8fde6a8882e63e8dfafdf9f62c73b7b1036ebf.zip |
echo: add prometheus and multiple middlewarev0.3.0echo/v0.4.0
- Integrate prometheus
- Integrate CORS
- Access control prometheus by IP
- Fix redirectors to consider port
- Fix redirectors to not redirect IP addresses
- Use body limit e.Use(middleware.BodyLimit("2M"))
-rw-r--r-- | echo/echo_default.go | 177 | ||||
-rw-r--r-- | echo/middleware/ip_filter.go | 39 | ||||
-rw-r--r-- | echo/middleware/redirect.go | 75 | ||||
-rw-r--r-- | echo/prometheus/prometheus.go | 16 |
4 files changed, 244 insertions, 63 deletions
diff --git a/echo/echo_default.go b/echo/echo_default.go index ddaab44..92a5cfd 100644 --- a/echo/echo_default.go +++ b/echo/echo_default.go | |||
@@ -6,13 +6,17 @@ import ( | |||
6 | "fmt" | 6 | "fmt" |
7 | "html/template" | 7 | "html/template" |
8 | "io/fs" | 8 | "io/fs" |
9 | "net" | ||
9 | "net/http" | 10 | "net/http" |
10 | "path" | 11 | "path" |
12 | "strconv" | ||
11 | "sync" | 13 | "sync" |
12 | 14 | ||
13 | gltls "code.crute.us/mcrute/golib/crypto/tls" | 15 | gltls "code.crute.us/mcrute/golib/crypto/tls" |
14 | glmw "code.crute.us/mcrute/golib/echo/middleware" | 16 | glmw "code.crute.us/mcrute/golib/echo/middleware" |
15 | "code.crute.us/mcrute/golib/echo/prometheus" | 17 | "code.crute.us/mcrute/golib/echo/prometheus" |
18 | glnet "code.crute.us/mcrute/golib/net" | ||
19 | glservice "code.crute.us/mcrute/golib/service" | ||
16 | 20 | ||
17 | "github.com/labstack/echo/v4" | 21 | "github.com/labstack/echo/v4" |
18 | "github.com/labstack/echo/v4/middleware" | 22 | "github.com/labstack/echo/v4/middleware" |
@@ -22,21 +26,23 @@ import ( | |||
22 | // Docs: https://echo.labstack.com/guide/ | 26 | // Docs: https://echo.labstack.com/guide/ |
23 | 27 | ||
24 | // TODO: | 28 | // TODO: |
25 | // - Intgrate prometheus | ||
26 | // - Integrate CORS | ||
27 | // - Access control prometheus by IP | ||
28 | // - Fix redirectors to consider port | ||
29 | // - Fix redirectors to not redirect IP addresses | ||
30 | // - Integrate CSRF | 29 | // - Integrate CSRF |
31 | // - Integrate session | 30 | // - Integrate session |
32 | // - Use bodylimit e.Use(middleware.BodyLimit("2M")) | 31 | // - Enable auto cert management by passing hostnames |
32 | |||
33 | const ( | ||
34 | defaultBodySizeLimit = "10M" | ||
35 | ) | ||
33 | 36 | ||
34 | type EchoConfig struct { | 37 | type EchoConfig struct { |
35 | Debug bool | 38 | Debug bool |
36 | BindAddress string | 39 | Hostnames []string |
37 | BindTLSAddress string | 40 | BindAddresses []string |
41 | BindTLSAddresses []string | ||
38 | TLSCacheDir string | 42 | TLSCacheDir string |
43 | BodySizeLimit string | ||
39 | TrustedProxyIPRanges []string | 44 | TrustedProxyIPRanges []string |
45 | ManagementIPRanges []string | ||
40 | EmbeddedTemplates fs.FS | 46 | EmbeddedTemplates fs.FS |
41 | DiskTemplates fs.FS | 47 | DiskTemplates fs.FS |
42 | TemplateGlob *string | 48 | TemplateGlob *string |
@@ -44,14 +50,16 @@ type EchoConfig struct { | |||
44 | CombinedHostLogFile string | 50 | CombinedHostLogFile string |
45 | RedirectToWWW bool | 51 | RedirectToWWW bool |
46 | ContentSecurityPolicy *glmw.ContentSecurityPolicyConfig | 52 | ContentSecurityPolicy *glmw.ContentSecurityPolicyConfig |
53 | DisablePrometheus bool | ||
47 | PrometheusConfig *prometheus.PrometheusConfig | 54 | PrometheusConfig *prometheus.PrometheusConfig |
55 | CORSConfig *middleware.CORSConfig | ||
48 | } | 56 | } |
49 | 57 | ||
50 | type EchoWrapper struct { | 58 | type EchoWrapper struct { |
51 | *echo.Echo | 59 | *echo.Echo |
52 | tlsServer http.Server | 60 | servers []*http.Server |
61 | tlsServers []*http.Server | ||
53 | templateFS fs.FS | 62 | templateFS fs.FS |
54 | bindAddress string | ||
55 | ocspErrors chan gltls.OcspError | 63 | ocspErrors chan gltls.OcspError |
56 | ocspManager *gltls.OcspManager | 64 | ocspManager *gltls.OcspManager |
57 | initDone bool | 65 | initDone bool |
@@ -79,45 +87,56 @@ func (w *EchoWrapper) RunCertificateManager(ctx context.Context, wg *sync.WaitGr | |||
79 | return w.ocspManager.Run(ctx, wg) | 87 | return w.ocspManager.Run(ctx, wg) |
80 | } | 88 | } |
81 | 89 | ||
82 | func (w *EchoWrapper) run(ctx context.Context, wg *sync.WaitGroup, f func() error, sf func(context.Context) error) error { | 90 | func (w *EchoWrapper) makeServerJob(s *http.Server, echoInit bool) glservice.RunnerFunc { |
83 | if !w.initDone { | 91 | return func(ctx context.Context, wg *sync.WaitGroup) error { |
84 | return fmt.Errorf("Echo is not initialized. Call Init()") | 92 | if !w.initDone { |
85 | } | 93 | return fmt.Errorf("Echo is not initialized. Call Init()") |
94 | } | ||
86 | 95 | ||
87 | wg.Add(1) | 96 | wg.Add(1) |
88 | defer wg.Done() | 97 | defer wg.Done() |
89 | 98 | ||
90 | err := make(chan error) | 99 | w.Logger.Infof("Starting server with address: %s", s.Addr) |
91 | go func() { err <- f() }() | ||
92 | select { | ||
93 | case e := <-err: | ||
94 | return e | ||
95 | default: | ||
96 | } | ||
97 | 100 | ||
98 | select { | 101 | err := make(chan error) |
99 | case <-ctx.Done(): | 102 | go func() { |
100 | w.Logger.Info("Shutting down web server") | 103 | if s.TLSConfig == nil && echoInit { |
101 | return sf(ctx) | 104 | err <- w.Echo.StartServer(s) |
105 | } else if s.TLSConfig == nil && !echoInit { | ||
106 | err <- s.ListenAndServe() | ||
107 | } else { | ||
108 | err <- s.ListenAndServeTLS("", "") | ||
109 | } | ||
110 | }() | ||
111 | select { | ||
112 | case e := <-err: | ||
113 | return e | ||
114 | default: | ||
115 | } | ||
116 | |||
117 | select { | ||
118 | case <-ctx.Done(): | ||
119 | w.Logger.Info("Shutting down web server") | ||
120 | return s.Shutdown(ctx) | ||
121 | } | ||
102 | } | 122 | } |
103 | } | 123 | } |
104 | 124 | ||
105 | func (w *EchoWrapper) Serve(ctx context.Context, wg *sync.WaitGroup) error { | 125 | func (w *EchoWrapper) MakeServerJobs() []glservice.RunnerFunc { |
106 | return w.run( | 126 | out := []glservice.RunnerFunc{} |
107 | ctx, | ||
108 | wg, | ||
109 | func() error { return w.Echo.Start(w.bindAddress) }, | ||
110 | func(ctx context.Context) error { return w.Echo.Shutdown(ctx) }, | ||
111 | ) | ||
112 | } | ||
113 | 127 | ||
114 | func (w *EchoWrapper) ServeTLS(ctx context.Context, wg *sync.WaitGroup) error { | 128 | for i, s := range w.servers { |
115 | return w.run( | 129 | // The first http (not https) server should do an echo.StartServer to |
116 | ctx, | 130 | // configure some internal echo state and print the banner (if |
117 | wg, | 131 | // configured). |
118 | func() error { return w.tlsServer.ListenAndServeTLS("", "") }, | 132 | out = append(out, w.makeServerJob(s, i == 0)) |
119 | func(ctx context.Context) error { return w.tlsServer.Shutdown(ctx) }, | 133 | } |
120 | ) | 134 | |
135 | for _, s := range w.tlsServers { | ||
136 | out = append(out, w.makeServerJob(s, false)) | ||
137 | } | ||
138 | |||
139 | return out | ||
121 | } | 140 | } |
122 | 141 | ||
123 | func (w *EchoWrapper) GetTemplateFS() fs.FS { | 142 | func (w *EchoWrapper) GetTemplateFS() fs.FS { |
@@ -183,25 +202,55 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) { | |||
183 | Errors: cmek, | 202 | Errors: cmek, |
184 | } | 203 | } |
185 | 204 | ||
186 | ts := http.Server{ | 205 | servers := make([]*http.Server, len(c.BindAddresses)) |
187 | Addr: c.BindTLSAddress, | 206 | for i, a := range c.BindAddresses { |
188 | TLSConfig: &tls.Config{ | 207 | servers[i] = &http.Server{ |
189 | MinVersion: tls.VersionTLS12, | 208 | Addr: a, |
190 | GetCertificate: cm.GetCertificate, | 209 | Handler: e, |
191 | }, | 210 | } |
192 | Handler: e, | 211 | } |
212 | |||
213 | tlsServers := make([]*http.Server, len(c.BindTLSAddresses)) | ||
214 | for i, a := range c.BindTLSAddresses { | ||
215 | tlsServers[i] = &http.Server{ | ||
216 | Addr: a, | ||
217 | TLSConfig: &tls.Config{ | ||
218 | MinVersion: tls.VersionTLS12, | ||
219 | GetCertificate: cm.GetCertificate, | ||
220 | }, | ||
221 | Handler: e, | ||
222 | } | ||
193 | } | 223 | } |
194 | 224 | ||
195 | e.Use(middleware.Logger()) | 225 | e.Use(middleware.Logger()) |
196 | e.Use(glmw.Recover()) | 226 | e.Use(glmw.Recover()) |
197 | e.Use(middleware.HTTPSRedirect()) | 227 | |
228 | if c.BodySizeLimit == "" { | ||
229 | e.Use(middleware.BodyLimit(defaultBodySizeLimit)) | ||
230 | } else if c.BodySizeLimit != "0" { | ||
231 | e.Use(middleware.BodyLimit(c.BodySizeLimit)) | ||
232 | } | ||
233 | |||
234 | _, tlsPort, err := net.SplitHostPort(tlsServers[0].Addr) | ||
235 | if err != nil { | ||
236 | return nil, fmt.Errorf("Unable to split TLS addr and port: %w", err) | ||
237 | } | ||
238 | tlsPortI, err := strconv.Atoi(tlsPort) | ||
239 | if err != nil { | ||
240 | return nil, fmt.Errorf("Unable to convert TLS port to int: %w", err) | ||
241 | } | ||
242 | e.Use(glmw.HTTPSRedirectWithConfig(glmw.HTTPSRedirectConfig{Port: tlsPortI})) | ||
243 | |||
198 | if c.RedirectToWWW { | 244 | if c.RedirectToWWW { |
199 | e.Use(middleware.WWWRedirect()) | 245 | e.Use(middleware.WWWRedirect()) |
200 | } | 246 | } |
247 | |||
201 | e.Use(middleware.Decompress()) | 248 | e.Use(middleware.Decompress()) |
202 | e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ | 249 | e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ |
203 | // TODO: This mw causes prometheus responses to show up compressed for | 250 | // TODO: This mw causes prometheus responses to show up compressed for |
204 | // browsers. Why? | 251 | // browsers. Why? |
252 | // | ||
253 | // Also, this path should use the config path if we keep it | ||
205 | Skipper: func(c echo.Context) bool { | 254 | Skipper: func(c echo.Context) bool { |
206 | if c.Path() == "/metrics" { | 255 | if c.Path() == "/metrics" { |
207 | return true | 256 | return true |
@@ -210,24 +259,42 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) { | |||
210 | }, | 259 | }, |
211 | Level: 5, | 260 | Level: 5, |
212 | })) | 261 | })) |
262 | |||
213 | e.Use(glmw.StrictSecure()) | 263 | e.Use(glmw.StrictSecure()) |
214 | 264 | ||
265 | if c.CORSConfig != nil { | ||
266 | e.Use(middleware.CORSWithConfig(*c.CORSConfig)) | ||
267 | } else { | ||
268 | e.Use(middleware.CORS()) | ||
269 | } | ||
270 | |||
215 | if c.ContentSecurityPolicy != nil { | 271 | if c.ContentSecurityPolicy != nil { |
216 | e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy)) | 272 | e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy)) |
217 | } else { | 273 | } else { |
218 | return nil, fmt.Errorf("ContentSecurityPolicy is required") | 274 | return nil, fmt.Errorf("ContentSecurityPolicy is required") |
219 | } | 275 | } |
220 | 276 | ||
221 | if c.PrometheusConfig != nil { | 277 | if !c.DisablePrometheus { |
222 | prom := prometheus.NewPrometheusWithConfig(c.PrometheusConfig) | 278 | mips, err := glnet.ParseCIDRSlice(c.ManagementIPRanges) |
279 | if err != nil { | ||
280 | return nil, err | ||
281 | } | ||
282 | |||
283 | var prom *prometheus.Prometheus | ||
284 | if c.PrometheusConfig != nil { | ||
285 | prom = prometheus.NewPrometheusWithConfig(c.PrometheusConfig) | ||
286 | } else { | ||
287 | prom = prometheus.NewPrometheus() | ||
288 | } | ||
289 | |||
223 | e.Use(prom.MiddlewareHandler) | 290 | e.Use(prom.MiddlewareHandler) |
224 | e.GET(c.PrometheusConfig.MetricsPath, prom.MetricsHandler) | 291 | e.GET(prom.Config.MetricsPath, prom.MetricsHandler, glmw.NewIPFilter(mips)) |
225 | } | 292 | } |
226 | 293 | ||
227 | return &EchoWrapper{ | 294 | return &EchoWrapper{ |
228 | Echo: e, | 295 | Echo: e, |
229 | tlsServer: ts, | 296 | servers: servers, |
230 | bindAddress: c.BindAddress, | 297 | tlsServers: tlsServers, |
231 | ocspErrors: cmek, | 298 | ocspErrors: cmek, |
232 | ocspManager: cm, | 299 | ocspManager: cm, |
233 | templateFS: templates, | 300 | templateFS: templates, |
diff --git a/echo/middleware/ip_filter.go b/echo/middleware/ip_filter.go new file mode 100644 index 0000000..007791e --- /dev/null +++ b/echo/middleware/ip_filter.go | |||
@@ -0,0 +1,39 @@ | |||
1 | package middleware | ||
2 | |||
3 | import ( | ||
4 | "net" | ||
5 | |||
6 | "github.com/labstack/echo/v4" | ||
7 | ) | ||
8 | |||
9 | func NewIPFilter(allowedRanges []*net.IPNet) echo.MiddlewareFunc { | ||
10 | return func(next echo.HandlerFunc) echo.HandlerFunc { | ||
11 | return func(c echo.Context) error { | ||
12 | if allowedRanges == nil { | ||
13 | c.Logger().Error("No allowed IPs configured for filter") | ||
14 | return echo.ErrNotFound | ||
15 | } | ||
16 | |||
17 | ip := net.ParseIP(c.RealIP()) | ||
18 | if ip == nil { | ||
19 | c.Logger().Error("Unable to parse IP in IPFilter") | ||
20 | return echo.ErrNotFound | ||
21 | } | ||
22 | |||
23 | found := false | ||
24 | for _, ipnet := range allowedRanges { | ||
25 | if ipnet.Contains(ip) { | ||
26 | found = true | ||
27 | break | ||
28 | } | ||
29 | } | ||
30 | |||
31 | if !found { | ||
32 | c.Logger().Errorf("IP %s not in range for filter", c.RealIP()) | ||
33 | return echo.ErrNotFound | ||
34 | } | ||
35 | |||
36 | return next(c) | ||
37 | } | ||
38 | } | ||
39 | } | ||
diff --git a/echo/middleware/redirect.go b/echo/middleware/redirect.go new file mode 100644 index 0000000..134bfee --- /dev/null +++ b/echo/middleware/redirect.go | |||
@@ -0,0 +1,75 @@ | |||
1 | package middleware | ||
2 | |||
3 | /* | ||
4 | HTTP to HTTPS Redirect Middleware | ||
5 | |||
6 | This is a duplicate of existing functionality in Echo because the Echo default | ||
7 | middleware doesn't support redirecting to a different HTTPS port which is | ||
8 | needed in dev environments and some prod environments where the server runs on | ||
9 | an off-port. | ||
10 | */ | ||
11 | |||
12 | import ( | ||
13 | "fmt" | ||
14 | "net" | ||
15 | "net/http" | ||
16 | |||
17 | "github.com/labstack/echo/v4" | ||
18 | "github.com/labstack/echo/v4/middleware" | ||
19 | ) | ||
20 | |||
21 | type HTTPSRedirectConfig struct { | ||
22 | Skipper middleware.Skipper | ||
23 | Port int | ||
24 | Code int | ||
25 | } | ||
26 | |||
27 | var DefaultHTTPSRedirectConfig = HTTPSRedirectConfig{ | ||
28 | Skipper: middleware.DefaultSkipper, | ||
29 | Port: 443, | ||
30 | Code: http.StatusMovedPermanently, | ||
31 | } | ||
32 | |||
33 | func HTTPSRedirect() echo.MiddlewareFunc { | ||
34 | return HTTPSRedirectWithConfig(DefaultHTTPSRedirectConfig) | ||
35 | } | ||
36 | |||
37 | func HTTPSRedirectWithConfig(config HTTPSRedirectConfig) echo.MiddlewareFunc { | ||
38 | if config.Skipper == nil { | ||
39 | config.Skipper = DefaultHTTPSRedirectConfig.Skipper | ||
40 | } | ||
41 | if config.Code == 0 { | ||
42 | config.Code = DefaultHTTPSRedirectConfig.Code | ||
43 | } | ||
44 | if config.Port == 0 { | ||
45 | config.Port = DefaultHTTPSRedirectConfig.Port | ||
46 | } | ||
47 | |||
48 | return func(next echo.HandlerFunc) echo.HandlerFunc { | ||
49 | return func(c echo.Context) error { | ||
50 | if config.Skipper(c) || c.Scheme() == "https" { | ||
51 | return next(c) | ||
52 | } | ||
53 | |||
54 | var err error | ||
55 | req := c.Request() | ||
56 | |||
57 | host := req.URL.Host | ||
58 | if host == "" { | ||
59 | host, _, err = net.SplitHostPort(req.Host) | ||
60 | if err != nil { | ||
61 | return echo.ErrBadRequest | ||
62 | } | ||
63 | } | ||
64 | |||
65 | // Browers assume 443 if it's an https request, otherwise the port | ||
66 | // needs to be specified in the URL | ||
67 | redir := fmt.Sprintf("https://%s%s", host, req.RequestURI) | ||
68 | if config.Port != 443 { | ||
69 | redir = fmt.Sprintf("https://%s:%d%s", host, config.Port, req.RequestURI) | ||
70 | } | ||
71 | |||
72 | return c.Redirect(http.StatusMovedPermanently, redir) | ||
73 | } | ||
74 | } | ||
75 | } | ||
diff --git a/echo/prometheus/prometheus.go b/echo/prometheus/prometheus.go index 616e425..2fbf252 100644 --- a/echo/prometheus/prometheus.go +++ b/echo/prometheus/prometheus.go | |||
@@ -14,7 +14,7 @@ import ( | |||
14 | ) | 14 | ) |
15 | 15 | ||
16 | type Prometheus struct { | 16 | type Prometheus struct { |
17 | config *PrometheusConfig | 17 | Config *PrometheusConfig |
18 | requestCount *prometheus.CounterVec | 18 | requestCount *prometheus.CounterVec |
19 | requestDuration *prometheus.HistogramVec | 19 | requestDuration *prometheus.HistogramVec |
20 | requestSize *prometheus.HistogramVec | 20 | requestSize *prometheus.HistogramVec |
@@ -65,7 +65,7 @@ func NewPrometheusWithConfig(c *PrometheusConfig) *Prometheus { | |||
65 | } | 65 | } |
66 | 66 | ||
67 | return &Prometheus{ | 67 | return &Prometheus{ |
68 | config: c, | 68 | Config: c, |
69 | MetricsHandler: echo.WrapHandler(promhttp.Handler()), | 69 | MetricsHandler: echo.WrapHandler(promhttp.Handler()), |
70 | requestCount: promauto.NewCounterVec(prometheus.CounterOpts{ | 70 | requestCount: promauto.NewCounterVec(prometheus.CounterOpts{ |
71 | Subsystem: c.Subsystem, | 71 | Subsystem: c.Subsystem, |
@@ -92,11 +92,11 @@ func NewPrometheusWithConfig(c *PrometheusConfig) *Prometheus { | |||
92 | 92 | ||
93 | func (p *Prometheus) MiddlewareHandler(next echo.HandlerFunc) echo.HandlerFunc { | 93 | func (p *Prometheus) MiddlewareHandler(next echo.HandlerFunc) echo.HandlerFunc { |
94 | return func(c echo.Context) error { | 94 | return func(c echo.Context) error { |
95 | if c.Path() == p.config.MetricsPath { | 95 | if c.Path() == p.Config.MetricsPath { |
96 | return next(c) | 96 | return next(c) |
97 | } | 97 | } |
98 | 98 | ||
99 | if p.config.Skipper(c) { | 99 | if p.Config.Skipper(c) { |
100 | return next(c) | 100 | return next(c) |
101 | } | 101 | } |
102 | 102 | ||
@@ -116,9 +116,9 @@ func (p *Prometheus) MiddlewareHandler(next echo.HandlerFunc) echo.HandlerFunc { | |||
116 | } | 116 | } |
117 | } | 117 | } |
118 | 118 | ||
119 | url := p.config.ExtractUrl(c) | 119 | url := p.Config.ExtractUrl(c) |
120 | if len(p.config.ContextLabel) > 0 { | 120 | if len(p.Config.ContextLabel) > 0 { |
121 | u := c.Get(p.config.ContextLabel) | 121 | u := c.Get(p.Config.ContextLabel) |
122 | if u == nil { | 122 | if u == nil { |
123 | u = "unknown" | 123 | u = "unknown" |
124 | } | 124 | } |
@@ -128,7 +128,7 @@ func (p *Prometheus) MiddlewareHandler(next echo.HandlerFunc) echo.HandlerFunc { | |||
128 | s := strconv.Itoa(status) | 128 | s := strconv.Itoa(status) |
129 | m := c.Request().Method | 129 | m := c.Request().Method |
130 | p.requestDuration.WithLabelValues(s, m, url).Observe(elapsed) | 130 | p.requestDuration.WithLabelValues(s, m, url).Observe(elapsed) |
131 | p.requestCount.WithLabelValues(s, m, p.config.ExtractHost(c), url).Inc() | 131 | p.requestCount.WithLabelValues(s, m, p.Config.ExtractHost(c), url).Inc() |
132 | p.requestSize.WithLabelValues(s, m, url).Observe(float64(reqSz)) | 132 | p.requestSize.WithLabelValues(s, m, url).Observe(float64(reqSz)) |
133 | p.responseSize.WithLabelValues(s, m, url).Observe(float64(c.Response().Size)) | 133 | p.responseSize.WithLabelValues(s, m, url).Observe(float64(c.Response().Size)) |
134 | 134 | ||