diff --git a/api/server/gin_middlewares.go b/api/server/gin_middlewares.go index c2e51577d..e0d39828b 100644 --- a/api/server/gin_middlewares.go +++ b/api/server/gin_middlewares.go @@ -14,8 +14,11 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "go.opencensus.io/stats" "go.opencensus.io/tag" "go.opencensus.io/trace" + "strconv" + "time" ) func optionalCorsWrap(r *gin.Engine) { @@ -75,6 +78,54 @@ func traceWrap(c *gin.Context) { c.Next() } +func apiMetricsWrap(s *Server) { + + measure := func(engine *gin.Engine) func(*gin.Context) { + var routes gin.RoutesInfo + return func(c *gin.Context) { + if routes == nil { + routes = engine.Routes() + } + start := time.Now() + // get the handler url, example: /v1/apps/:app + url := "" + for _, r := range routes { + if r.Handler == c.HandlerName() { + url = r.Path + break + } + } + + ctx, err := tag.New(c.Request.Context(), + tag.Upsert(pathKey, url), + tag.Upsert(methodKey, c.Request.Method), + ) + if err != nil { + logrus.Fatal(err) + } + stats.Record(ctx, apiRequestCount.M(1)) + c.Next() + + status := strconv.Itoa(c.Writer.Status()) + ctx, err = tag.New(ctx, + tag.Upsert(statusKey, status), + ) + if err != nil { + logrus.Fatal(err) + } + stats.Record(ctx, apiLatency.M(float64(time.Since(start))/float64(time.Millisecond))) + } + } + + r := s.Router + r.Use(measure(r)) + if s.webListenPort != s.adminListenPort { + a := s.AdminRouter + a.Use(measure(a)) + } + +} + func panicWrap(c *gin.Context) { defer func(c *gin.Context) { if rec := recover(); rec != nil { diff --git a/api/server/server.go b/api/server/server.go index 9b7a9d367..1d194e059 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -537,6 +537,7 @@ func New(ctx context.Context, opts ...ServerOption) *Server { setMachineID() s.Router.Use(loggerWrap, traceWrap, panicWrap) // TODO should be opts optionalCorsWrap(s.Router) // TODO should be an opt + apiMetricsWrap(s) s.bindHandlers(ctx) s.appListeners = new(appListeners) @@ -572,6 +573,7 @@ func WithPrometheus() ServerOption { } s.promExporter = exporter view.RegisterExporter(exporter) + registerViews() return nil } } diff --git a/api/server/stats.go b/api/server/stats.go new file mode 100644 index 000000000..084537b08 --- /dev/null +++ b/api/server/stats.go @@ -0,0 +1,68 @@ +package server + +import ( + "github.com/sirupsen/logrus" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +var ( + apiRequestCount = stats.Int64("api/request_count", "Number of API requests", stats.UnitDimensionless) + apiLatency = stats.Float64("api/latency", "API latency", stats.UnitMilliseconds) +) + +var ( + pathKey = makeKey("path") + methodKey = makeKey("method") + statusKey = makeKey("status") +) + +var ( + defaultLatencyDistribution = view.Distribution(0, 1, 2, 3, 4, 5, 6, 8, 10, 13, 16, 20, 25, 30, 40, 50, 65, 80, 100, 130, 160, 200, 250, 300, 400, 500, 650, 800, 1000, 2000, 5000, 10000, 20000, 50000, 100000) +) + +var ( + ApiRequestCountView = &view.View{ + Name: "api/request_count", + Description: "Count of API requests started", + Measure: apiRequestCount, + TagKeys: []tag.Key{pathKey, methodKey}, + Aggregation: view.Count(), + } + + ApiResponseCountView = &view.View{ + Name: "api/response_count", + Description: "API response count", + TagKeys: []tag.Key{pathKey, methodKey, statusKey}, + Measure: apiLatency, + Aggregation: view.Count(), + } + + ApiLatencyView = &view.View{ + Name: "api/latency", + Description: "Latency distribution of API requests", + Measure: apiLatency, + TagKeys: []tag.Key{pathKey, methodKey, statusKey}, + Aggregation: defaultLatencyDistribution, + } +) + +func registerViews() { + err := view.Register( + ApiRequestCountView, + ApiResponseCountView, + ApiLatencyView, + ) + if err != nil { + logrus.WithError(err).Fatal("cannot register view") + } +} + +func makeKey(name string) tag.Key { + key, err := tag.NewKey(name) + if err != nil { + logrus.Fatal(err) + } + return key +}