Skip to content

Commit

Permalink
Add new middleware for monitoring gin http routes (#171)
Browse files Browse the repository at this point in the history
* modify metrics to send dynamic label values

* added path, and http method label
  • Loading branch information
DeimonDB authored Oct 7, 2022
1 parent f6eb879 commit 8d0ce7c
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 24 deletions.
53 changes: 29 additions & 24 deletions metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ const (
type Collectors map[string]prometheus.Collector

type PerformanceMetric interface {
Start() time.Time
Duration(start time.Time)
Success()
Failure()
Start(labelValues ...string) time.Time
Duration(start time.Time, labelValues ...string)
Success(labelValues ...string)
Failure(labelValues ...string)
}

type performanceMetric struct {
Expand All @@ -29,32 +29,37 @@ type performanceMetric struct {
executionFailedTotal *prometheus.CounterVec
}

func NewPerformanceMetric(namespace string, labels prometheus.Labels, reg prometheus.Registerer) PerformanceMetric {
func NewPerformanceMetric(
namespace string,
labelNames []string,
staticLabels prometheus.Labels,
reg prometheus.Registerer,
) PerformanceMetric {
executionStarted := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Name: executionStartedKey,
Help: "Last Unix time when execution started.",
}, nil)
}, labelNames)

executionDurationSeconds := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Name: executionDurationSecondsKey,
Help: "Duration of the executions.",
}, nil)
}, labelNames)

executionSucceededTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Name: executionSucceededTotalKey,
Help: "Total number of the executions which succeeded.",
}, nil)
}, labelNames)

executionFailedTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Name: executionFailedTotalKey,
Help: "Total number of the executions which failed.",
}, nil)
}, labelNames)

Register(labels, reg, executionStarted, executionDurationSeconds, executionSucceededTotal, executionFailedTotal)
Register(staticLabels, reg, executionStarted, executionDurationSeconds, executionSucceededTotal, executionFailedTotal)

return &performanceMetric{
executionStarted: executionStarted,
Expand All @@ -64,33 +69,33 @@ func NewPerformanceMetric(namespace string, labels prometheus.Labels, reg promet
}
}

func (m *performanceMetric) Start() time.Time {
func (m *performanceMetric) Start(labelValues ...string) time.Time {
start := time.Now()
m.executionStarted.WithLabelValues().SetToCurrentTime()
m.executionStarted.WithLabelValues(labelValues...).SetToCurrentTime()
return start
}

func (m *performanceMetric) Duration(start time.Time) {
func (m *performanceMetric) Duration(start time.Time, labelValues ...string) {
duration := time.Since(start)
m.executionDurationSeconds.WithLabelValues().Observe(duration.Seconds())
m.executionDurationSeconds.WithLabelValues(labelValues...).Observe(duration.Seconds())
}

func (m *performanceMetric) Success() {
m.executionSucceededTotal.WithLabelValues().Inc()
m.executionFailedTotal.WithLabelValues().Add(0)
func (m *performanceMetric) Success(labelValues ...string) {
m.executionSucceededTotal.WithLabelValues(labelValues...).Inc()
m.executionFailedTotal.WithLabelValues(labelValues...).Add(0)
}

func (m *performanceMetric) Failure() {
m.executionFailedTotal.WithLabelValues().Inc()
m.executionSucceededTotal.WithLabelValues().Add(0)
func (m *performanceMetric) Failure(labelValues ...string) {
m.executionFailedTotal.WithLabelValues(labelValues...).Inc()
m.executionSucceededTotal.WithLabelValues(labelValues...).Add(0)
}

type NullablePerformanceMetric struct{}

func (NullablePerformanceMetric) Start() time.Time {
func (NullablePerformanceMetric) Start(_ ...string) time.Time {
// NullablePerformanceMetric is a no-op, so returning empty value
return time.Time{}
}
func (NullablePerformanceMetric) Duration(_ time.Time) {}
func (NullablePerformanceMetric) Success() {}
func (NullablePerformanceMetric) Failure() {}
func (NullablePerformanceMetric) Duration(_ time.Time, _ ...string) {}
func (NullablePerformanceMetric) Success(_ ...string) {}
func (NullablePerformanceMetric) Failure(_ ...string) {}
42 changes: 42 additions & 0 deletions middleware/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package middleware

import (
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"

"github.com/trustwallet/go-libs/metrics"
)

const labelPath = "path"
const labelMethod = "method"

func MetricsMiddleware(namespace string, labels prometheus.Labels, reg prometheus.Registerer) gin.HandlerFunc {
perfMetric := metrics.NewPerformanceMetric(namespace, []string{labelPath, labelMethod}, labels, reg)
return func(c *gin.Context) {
path := c.FullPath()
method := c.Request.Method

// route not found, call next and immediately return
if path == "" {
c.Next()
return
}

labelValues := []string{path, method}

startTime := perfMetric.Start(labelValues...)
c.Next()
perfMetric.Duration(startTime, labelValues...)

statusCode := c.Writer.Status()
if successfulHttpStatusCode(statusCode) {
perfMetric.Success(labelValues...)
} else {
perfMetric.Failure(labelValues...)
}
}
}

func successfulHttpStatusCode(statusCode int) bool {
return 200 <= statusCode && statusCode < 300
}
75 changes: 75 additions & 0 deletions middleware/metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package middleware

import (
"errors"
"net/http"
"testing"

"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)

func TestMetricsMiddleware(t *testing.T) {
r := prometheus.NewRegistry()
router := gin.New()
router.Use(MetricsMiddleware("", nil, r))

successGroup := router.Group("/success")
successGroup.GET("/:test", func(c *gin.Context) {
c.JSON(http.StatusOK, struct{}{})
})

successGroup.GET("", func(c *gin.Context) {
c.JSON(http.StatusOK, struct{}{})
})

router.GET("/error", func(c *gin.Context) {
_ = c.AbortWithError(http.StatusInternalServerError, errors.New("oops error"))
})

// 2 successes, 1 errors
_ = performRequest("GET", "/success?haha=1&hoho=2", router)
_ = performRequest("GET", "/error?hehe=1&huhu=3", router)
_ = performRequest("GET", "/success/hihi", router)

metricFamilies, err := r.Gather()
require.NoError(t, err)

const executionFailedTotal = "execution_failed_total"
const executionSucceededTotal = "execution_succeeded_total"

// metricFamily.Name --> label --> counter value
expected := map[string]map[string]int{
executionSucceededTotal: {
"/success": 1,
"/success/:test": 1,
"/error": 0,
},
executionFailedTotal: {
"/success": 0,
"/success/:test": 0,
"/error": 1,
},
}

for _, metricFamily := range metricFamilies {
expectedLabelCounterMap, ok := expected[*metricFamily.Name]
if !ok {
continue
}

require.Len(t, metricFamily.Metric, len(expectedLabelCounterMap))
for _, metric := range metricFamily.Metric {
require.Len(t, metric.Label, 2)
var chosenLabelIdx = -1
for idx, label := range metric.Label {
if *label.Name == labelPath {
chosenLabelIdx = idx
}
}
require.NotEqual(t, -1, chosenLabelIdx)
require.Equal(t, float64(expectedLabelCounterMap[*metric.Label[chosenLabelIdx].Value]), *metric.Counter.Value)
}
}
}

0 comments on commit 8d0ce7c

Please sign in to comment.