package middleware import ( "context" "github.com/gogf/gf/net/ghttp" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "gxt-api-frame/library/logger" "io/ioutil" "net/http" "strconv" "time" ) var defaultMetricPath = "/metrics" var reqCnt = &Metric{ ID: "reqCnt", Name: "requests_total", Description: "How many HTTP requests processed, partitioned by status code and HTTP method.", Type: "counter_vec", Args: []string{"code", "method", "host", "url"}} var reqDur = &Metric{ ID: "reqDur", Name: "request_duration_seconds", Description: "The HTTP request latencies in seconds.", Type: "histogram_vec", Args: []string{"code", "method", "url"}, } var resSz = &Metric{ ID: "resSz", Name: "response_size_bytes", Description: "The HTTP response sizes in bytes.", Type: "summary"} var reqSz = &Metric{ ID: "reqSz", Name: "request_size_bytes", Description: "The HTTP request sizes in bytes.", Type: "summary"} var standardMetrics = []*Metric{ reqCnt, reqDur, resSz, reqSz, } type Metric struct { MetricCollector prometheus.Collector ID string Name string Description string Type string Args []string } type RequestCounterURLLabelMappingFn func(c *ghttp.Request) string type Prometheus struct { reqCnt *prometheus.CounterVec reqDur *prometheus.HistogramVec reqSz, resSz prometheus.Summary router *ghttp.Server listenAddress string Ppg PrometheusPushGateway MetricsList []*Metric MetricsPath string ReqCntURLLabelMappingFn RequestCounterURLLabelMappingFn // gin.Context string to use as a prometheus URL label URLLabelFromContext string } type PrometheusPushGateway struct { PushIntervalSeconds time.Duration PushGatewayURL string MetricsURL string Job string } // NewPrometheus generates a new set of metrics with a certain subsystem name func NewPrometheus(subsystem string, customMetricsList ...[]*Metric) *Prometheus { var metricsList []*Metric if len(customMetricsList) > 1 { panic("Too many args. NewPrometheus( string, ).") } else if len(customMetricsList) == 1 { metricsList = customMetricsList[0] } for _, metric := range standardMetrics { metricsList = append(metricsList, metric) } p := &Prometheus{ MetricsList: metricsList, MetricsPath: defaultMetricPath, ReqCntURLLabelMappingFn: func(c *ghttp.Request) string { return c.Request.URL.Path }, } p.registerMetrics(subsystem) return p } // Use adds the middleware to a gin engine. func (p *Prometheus) Use(e *ghttp.Server) { e.Use(p.HandlerFunc()) p.SetMetricsPath(e) } func (p *Prometheus) getMetrics() []byte { response, _ := http.Get(p.Ppg.MetricsURL) defer response.Body.Close() body, _ := ioutil.ReadAll(response.Body) return body } // NewMetric associates prometheus.Collector based on Metric.Type func NewMetric(m *Metric, subsystem string) prometheus.Collector { var metric prometheus.Collector switch m.Type { case "counter_vec": metric = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: subsystem, Name: m.Name, Help: m.Description, }, m.Args, ) case "counter": metric = prometheus.NewCounter( prometheus.CounterOpts{ Subsystem: subsystem, Name: m.Name, Help: m.Description, }, ) case "gauge_vec": metric = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Subsystem: subsystem, Name: m.Name, Help: m.Description, }, m.Args, ) case "gauge": metric = prometheus.NewGauge( prometheus.GaugeOpts{ Subsystem: subsystem, Name: m.Name, Help: m.Description, }, ) case "histogram_vec": metric = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Subsystem: subsystem, Name: m.Name, Help: m.Description, }, m.Args, ) case "histogram": metric = prometheus.NewHistogram( prometheus.HistogramOpts{ Subsystem: subsystem, Name: m.Name, Help: m.Description, }, ) case "summary_vec": metric = prometheus.NewSummaryVec( prometheus.SummaryOpts{ Subsystem: subsystem, Name: m.Name, Help: m.Description, }, m.Args, ) case "summary": metric = prometheus.NewSummary( prometheus.SummaryOpts{ Subsystem: subsystem, Name: m.Name, Help: m.Description, }, ) } return metric } func (p *Prometheus) registerMetrics(subsystem string) { for _, metricDef := range p.MetricsList { metric := NewMetric(metricDef, subsystem) if err := prometheus.Register(metric); err != nil { logger.Errorf(context.Background(), "%s could not be registered in Prometheus", metricDef.Name) } switch metricDef { case reqCnt: p.reqCnt = metric.(*prometheus.CounterVec) case reqDur: p.reqDur = metric.(*prometheus.HistogramVec) case resSz: p.resSz = metric.(prometheus.Summary) case reqSz: p.reqSz = metric.(prometheus.Summary) } metricDef.MetricCollector = metric } } // SetMetricsPath set metrics paths func (p *Prometheus) SetMetricsPath(s *ghttp.Server) { s.BindHandler(p.MetricsPath, prometheusHandler()) } func prometheusHandler() ghttp.HandlerFunc { h := promhttp.Handler() return func(c *ghttp.Request) { h.ServeHTTP(c.Response.Writer, c.Request) } } // HandlerFunc defines handler function for middleware func (p *Prometheus) HandlerFunc() ghttp.HandlerFunc { return func(c *ghttp.Request) { if c.Request.URL.Path == p.MetricsPath || c.Request.URL.Path == "/favicon.ico" { c.Middleware.Next() return } start := time.Now() reqSz := computeApproximateRequestSize(c.Request) c.Middleware.Next() status := strconv.Itoa(c.Response.Status) elapsed := float64(time.Since(start)) / float64(time.Second) resSz := float64(c.Response.BufferLength()) url := p.ReqCntURLLabelMappingFn(c) if len(p.URLLabelFromContext) > 0 { u := c.Get(p.URLLabelFromContext) url = u.(string) } p.reqDur.WithLabelValues(status, c.Request.Method, url).Observe(elapsed) p.reqCnt.WithLabelValues(status, c.Request.Method, c.Request.Host, url).Inc() p.reqSz.Observe(float64(reqSz)) p.resSz.Observe(resSz) } } func computeApproximateRequestSize(r *http.Request) int { s := 0 if r.URL != nil { s = len(r.URL.Path) } s += len(r.Method) s += len(r.Proto) for name, values := range r.Header { s += len(name) for _, value := range values { s += len(value) } } s += len(r.Host) if r.ContentLength != -1 { s += int(r.ContentLength) } return s }