diff --git a/internal/mocks/metrics/Recorder.go b/internal/mocks/metrics/Recorder.go index 2e04256..1afc69a 100644 --- a/internal/mocks/metrics/Recorder.go +++ b/internal/mocks/metrics/Recorder.go @@ -16,6 +16,10 @@ type Recorder struct { mock.Mock } +func (_m *Recorder) ObserveHTTPRequestSize(ctx context.Context, props metrics.HTTPReqProperties, sizeBytes int64) { + _m.Called(ctx, props, sizeBytes) +} // end ObserveHTTPRequestSize() + // AddInflightRequests provides a mock function with given fields: ctx, props, quantity func (_m *Recorder) AddInflightRequests(ctx context.Context, props metrics.HTTPProperties, quantity int) { _m.Called(ctx, props, quantity) diff --git a/internal/mocks/middleware/Reporter.go b/internal/mocks/middleware/Reporter.go index 7ed1742..531abb4 100644 --- a/internal/mocks/middleware/Reporter.go +++ b/internal/mocks/middleware/Reporter.go @@ -13,6 +13,28 @@ type Reporter struct { mock.Mock } +func (_m *Reporter) URI() []byte { + ret := _m.Called() + var r0 []byte + if rf, ok := ret.Get(0).(func() []byte); ok { + r0 = rf() + } else { + r0 = ret.Get(0).([]byte) + } // end if + return r0 +} // end URI() + +func (_m *Reporter) BytesReceived() int64 { + ret := _m.Called() + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } // end if + return r0 +} // end BytesReceived() + // BytesWritten provides a mock function with given fields: func (_m *Reporter) BytesWritten() int64 { ret := _m.Called() diff --git a/metrics/metrics.go b/metrics/metrics.go index f4fa46a..b408eff 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -8,6 +8,7 @@ import ( // HTTPReqProperties are the metric properties for the metrics based // on client request. type HTTPReqProperties struct { + URI []byte // Service is the service that has served the request. Service string // ID is the id of the request handler. @@ -30,6 +31,7 @@ type HTTPProperties struct { // Interface has the required methods to be used with the HTTP // middlewares. type Recorder interface { + ObserveHTTPRequestSize(ctx context.Context, props HTTPReqProperties, sizeBytes int64) // ObserveHTTPRequestDuration measures the duration of an HTTP request. ObserveHTTPRequestDuration(ctx context.Context, props HTTPReqProperties, duration time.Duration) // ObserveHTTPResponseSize measures the size of an HTTP response in bytes. @@ -44,6 +46,7 @@ const Dummy = dummy(0) type dummy int +func (dummy) ObserveHTTPRequestSize(context.Context, HTTPReqProperties, int64) {} // end ObserveHTTPRequestSize() func (dummy) ObserveHTTPRequestDuration(_ context.Context, _ HTTPReqProperties, _ time.Duration) {} func (dummy) ObserveHTTPResponseSize(_ context.Context, _ HTTPReqProperties, _ int64) {} func (dummy) AddInflightRequests(_ context.Context, _ HTTPProperties, _ int) {} diff --git a/metrics/opencensus/opencensus.go b/metrics/opencensus/opencensus.go index 3590ae6..0ce80a3 100644 --- a/metrics/opencensus/opencensus.go +++ b/metrics/opencensus/opencensus.go @@ -74,9 +74,10 @@ type recorder struct { serviceKey tag.Key // Measures. - latencySecs *stats.Float64Measure - sizeBytes *stats.Int64Measure - inflightCount *stats.Int64Measure + latencySecs *stats.Float64Measure + requestSizeBytes *stats.Int64Measure + responseSizeBytes *stats.Int64Measure + inflightCount *stats.Int64Measure } // NewRecorder returns a new Recorder that uses OpenCensus stats @@ -133,7 +134,8 @@ func (r *recorder) createMeasurements() { "http_request_duration_seconds", "The latency of the HTTP requests", "s") - r.sizeBytes = stats.Int64( + r.requestSizeBytes = stats.Int64("http_request_size_bytes", "The size of the HTTP requests", stats.UnitBytes) + r.responseSizeBytes = stats.Int64( "http_response_size_bytes", "The size of the HTTP responses", stats.UnitBytes) @@ -153,11 +155,18 @@ func (r recorder) registerViews(cfg Config) error { Measure: r.latencySecs, Aggregation: view.Distribution(cfg.DurationBuckets...), } - sizeView := &view.View{ + reqSizeView := &view.View{ + Name: r.requestSizeBytes.Name(), + Description: r.requestSizeBytes.Description(), + TagKeys: []tag.Key{r.serviceKey, r.handlerKey, r.methodKey, r.codeKey}, + Measure: r.requestSizeBytes, + Aggregation: view.Distribution(cfg.SizeBuckets...), + } + repSizeView := &view.View{ Name: "http_response_size_bytes", Description: "The size of the HTTP responses", TagKeys: []tag.Key{r.serviceKey, r.handlerKey, r.methodKey, r.codeKey}, - Measure: r.sizeBytes, + Measure: r.responseSizeBytes, Aggregation: view.Distribution(cfg.SizeBuckets...), } inflightView := &view.View{ @@ -170,10 +179,10 @@ func (r recorder) registerViews(cfg Config) error { // Do we need to unregister the same views before registering. if cfg.UnregisterViewsBeforeRegister { - view.Unregister(durationView, sizeView, inflightView) + view.Unregister(durationView, reqSizeView, repSizeView, inflightView) } - err := view.Register(durationView, sizeView, inflightView) + err := view.Register(durationView, reqSizeView, repSizeView, inflightView) if err != nil { return err } @@ -181,6 +190,11 @@ func (r recorder) registerViews(cfg Config) error { return nil } +func (r recorder) ObserveHTTPRequestSize(ctx context.Context, p metrics.HTTPReqProperties, sizeBytes int64) { + ctx = r.ctxWithTagFromHTTPReqProperties(ctx, p) + stats.Record(ctx, r.requestSizeBytes.M(sizeBytes)) +} // end ObserveHTTPRequestSize() + func (r recorder) ObserveHTTPRequestDuration(ctx context.Context, p metrics.HTTPReqProperties, duration time.Duration) { ctx = r.ctxWithTagFromHTTPReqProperties(ctx, p) stats.Record(ctx, r.latencySecs.M(duration.Seconds())) @@ -188,7 +202,7 @@ func (r recorder) ObserveHTTPRequestDuration(ctx context.Context, p metrics.HTTP func (r recorder) ObserveHTTPResponseSize(ctx context.Context, p metrics.HTTPReqProperties, sizeBytes int64) { ctx = r.ctxWithTagFromHTTPReqProperties(ctx, p) - stats.Record(ctx, r.sizeBytes.M(sizeBytes)) + stats.Record(ctx, r.responseSizeBytes.M(sizeBytes)) } func (r recorder) AddInflightRequests(ctx context.Context, p metrics.HTTPProperties, quantity int) { diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go index 41e95b5..197c27d 100644 --- a/metrics/prometheus/prometheus.go +++ b/metrics/prometheus/prometheus.go @@ -64,6 +64,7 @@ func (c *Config) defaults() { type recorder struct { httpRequestDurHistogram *prometheus.HistogramVec + httpRequestSizeHistogram *prometheus.HistogramVec httpResponseSizeHistogram *prometheus.HistogramVec httpRequestsInflight *prometheus.GaugeVec } @@ -82,6 +83,14 @@ func NewRecorder(cfg Config) metrics.Recorder { Buckets: cfg.DurationBuckets, }, []string{cfg.ServiceLabel, cfg.HandlerIDLabel, cfg.MethodLabel, cfg.StatusCodeLabel}), + httpRequestSizeHistogram: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: cfg.Prefix, + Subsystem: "http", + Name: "request_size_bytes", + Help: "The size of the HTTP requests.", + Buckets: cfg.SizeBuckets, + }, []string{cfg.ServiceLabel, cfg.HandlerIDLabel, cfg.MethodLabel, cfg.StatusCodeLabel}), + httpResponseSizeHistogram: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: cfg.Prefix, Subsystem: "http", @@ -99,6 +108,7 @@ func NewRecorder(cfg Config) metrics.Recorder { } cfg.Registry.MustRegister( + r.httpRequestSizeHistogram, r.httpRequestDurHistogram, r.httpResponseSizeHistogram, r.httpRequestsInflight, @@ -107,6 +117,10 @@ func NewRecorder(cfg Config) metrics.Recorder { return r } +func (r recorder) ObserveHTTPRequestSize(_ context.Context, p metrics.HTTPReqProperties, sizeBytes int64) { + r.httpRequestSizeHistogram.WithLabelValues(p.Service, p.ID, p.Method, p.Code).Observe(float64(sizeBytes)) +} // end ObserveHTTPRequestSize() + func (r recorder) ObserveHTTPRequestDuration(_ context.Context, p metrics.HTTPReqProperties, duration time.Duration) { r.httpRequestDurHistogram.WithLabelValues(p.Service, p.ID, p.Method, p.Code).Observe(duration.Seconds()) } diff --git a/middleware/echo/echo.go b/middleware/echo/echo.go index 62d75bd..e1cbeec 100644 --- a/middleware/echo/echo.go +++ b/middleware/echo/echo.go @@ -26,6 +26,17 @@ type reporter struct { c echo.Context } +func (r *reporter) URI() []byte { + u := r.c.Request().URL + if u != nil { + b, _ := u.MarshalBinary() + return b + } // end if + return nil +} // end URI() + +func (r *reporter) BytesReceived() int64 { return r.c.Request().ContentLength } // end BytesReceived() + func (r *reporter) Method() string { return r.c.Request().Method } func (r *reporter) Context() context.Context { return r.c.Request().Context() } diff --git a/middleware/fasthttp/fasthttp.go b/middleware/fasthttp/fasthttp.go index a7ce124..5c1ad81 100644 --- a/middleware/fasthttp/fasthttp.go +++ b/middleware/fasthttp/fasthttp.go @@ -21,6 +21,18 @@ type reporter struct { c *fasthttp.RequestCtx } +func (r reporter) URI() []byte { + uri := r.c.Request.URI() + if uri != nil { + return uri.FullURI() + } // end if + return nil +} // end URI() + +func (r reporter) BytesReceived() int64 { + return int64(len(r.c.Request.Body())) +} // end BytesReceived() + func (r reporter) Method() string { return string(r.c.Method()) } diff --git a/middleware/gin/gin.go b/middleware/gin/gin.go index b106c7e..12664ca 100644 --- a/middleware/gin/gin.go +++ b/middleware/gin/gin.go @@ -23,6 +23,16 @@ type reporter struct { c *gin.Context } +func (r *reporter) URI() []byte { + if r.c.Request.URL != nil { + b, _ := r.c.Request.URL.MarshalBinary() + return b + } // end if + return nil +} // end URI() + +func (r *reporter) BytesReceived() int64 { return r.c.Request.ContentLength } // end BytesReceived() + func (r *reporter) Method() string { return r.c.Request.Method } func (r *reporter) Context() context.Context { return r.c.Request.Context() } diff --git a/middleware/gorestful/gorestful.go b/middleware/gorestful/gorestful.go index 4cfe76e..cadbc3b 100644 --- a/middleware/gorestful/gorestful.go +++ b/middleware/gorestful/gorestful.go @@ -24,6 +24,16 @@ type reporter struct { resp *gorestful.Response } +func (r *reporter) URI() []byte { + if r.req.Request.URL != nil { + b, _ := r.req.Request.URL.MarshalBinary() + return b + } // end if + return nil +} // end URI() + +func (r *reporter) BytesReceived() int64 { return r.req.Request.ContentLength } // end BytesReceived() + func (r *reporter) Method() string { return r.req.Request.Method } func (r *reporter) Context() context.Context { return r.req.Request.Context() } diff --git a/middleware/iris/iris.go b/middleware/iris/iris.go index 001199d..798b185 100644 --- a/middleware/iris/iris.go +++ b/middleware/iris/iris.go @@ -23,6 +23,17 @@ type reporter struct { ctx iris.Context } +func (r *reporter) URI() []byte { + u := r.ctx.Request().URL + if u != nil { + b, _ := u.MarshalBinary() + return b + } // end if + return nil +} // end URI() + +func (r *reporter) BytesReceived() int64 { return r.ctx.Request().ContentLength } // end BytesReceived() + func (r *reporter) Method() string { return r.ctx.Method() } func (r *reporter) Context() context.Context { return r.ctx.Request().Context() } diff --git a/middleware/middleware.go b/middleware/middleware.go index 351a0d4..c29b456 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -106,6 +106,12 @@ func (m Middleware) Measure(handlerID string, reporter Reporter, next func()) { // Start the timer and when finishing measure the duration. start := time.Now() + props := metrics.HTTPReqProperties{ + URI: reporter.URI(), + Service: m.service, + ID: hid, + Method: reporter.Method(), + } defer func() { _, shouldIgnore := m.ignoredPaths[reporter.URLPath()] if shouldIgnore { @@ -123,13 +129,7 @@ func (m Middleware) Measure(handlerID string, reporter Reporter, next func()) { } else { code = strconv.Itoa(reporter.StatusCode()) } - - props := metrics.HTTPReqProperties{ - Service: m.service, - ID: hid, - Method: reporter.Method(), - Code: code, - } + props.Code = code m.recorder.ObserveHTTPRequestDuration(ctx, props, duration) // Measure size of response if required. @@ -137,6 +137,7 @@ func (m Middleware) Measure(handlerID string, reporter Reporter, next func()) { m.recorder.ObserveHTTPResponseSize(ctx, props, reporter.BytesWritten()) } }() + m.recorder.ObserveHTTPRequestSize(ctx, props, reporter.BytesReceived()) // Call the wrapped logic. next() @@ -145,9 +146,11 @@ func (m Middleware) Measure(handlerID string, reporter Reporter, next func()) { // Reporter knows how to report the data to the Middleware so it can measure the // different framework/libraries. type Reporter interface { + URI() []byte Method() string Context() context.Context URLPath() string StatusCode() int + BytesReceived() int64 BytesWritten() int64 } diff --git a/middleware/std/std.go b/middleware/std/std.go index 5532f5e..56367af 100644 --- a/middleware/std/std.go +++ b/middleware/std/std.go @@ -42,6 +42,16 @@ type stdReporter struct { r *http.Request } +func (s *stdReporter) URI() []byte { + if s.r.URL != nil { + b, _ := s.r.URL.MarshalBinary() + return b + } // end if + return nil +} // end URI() + +func (s *stdReporter) BytesReceived() int64 { return s.r.ContentLength } // end BytesReceived() + func (s *stdReporter) Method() string { return s.r.Method } func (s *stdReporter) Context() context.Context { return s.r.Context() }