// Copyright © 2023 OpenIM. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ginprometheus // //import ( // "bytes" // "fmt" // "io" // "net/http" // "os" // "strconv" // "time" // // "github.com/gin-gonic/gin" // "github.com/prometheus/client_golang/prometheus" // "github.com/prometheus/client_golang/prometheus/promhttp" //) // //var defaultMetricPath = "/metrics" // //// counter, counter_vec, gauge, gauge_vec, //// histogram, histogram_vec, summary, summary_vec. //var ( // reqCounter = &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", "handler", "host", "url"}} // // reqDuration = &Metric{ // ID: "reqDur", // Name: "request_duration_seconds", // Description: "The HTTP request latencies in seconds.", // Type: "histogram_vec", // Args: []string{"code", "method", "url"}, // } // // resSize = &Metric{ // ID: "resSz", // Name: "response_size_bytes", // Description: "The HTTP response sizes in bytes.", // Type: "summary"} // // reqSize = &Metric{ // ID: "reqSz", // Name: "request_size_bytes", // Description: "The HTTP request sizes in bytes.", // Type: "summary"} // // standardMetrics = []*Metric{ // reqCounter, // reqDuration, // resSize, // reqSize, // } //) // ///* //RequestCounterURLLabelMappingFn is a function which can be supplied to the middleware to control //the cardinality of the request counter's "url" label, which might be required in some contexts. //For instance, if for a "/customer/:name" route you don't want to generate a time series for every //possible customer name, you could use this function: // // func(c *gin.Context) string { // url := c.Request.URL.Path // for _, p := range c.Params { // if p.Key == "name" { // url = strings.Replace(url, p.Value, ":name", 1) // break // } // } // return url // } // //which would map "/customer/alice" and "/customer/bob" to their template "/customer/:name". //*/ //type RequestCounterURLLabelMappingFn func(c *gin.Context) string // //// Metric is a definition for the name, description, type, ID, and //// prometheus.Collector type (i.e. CounterVec, Summary, etc) of each metric. //type Metric struct { // MetricCollector prometheus.Collector // ID string // Name string // Description string // Type string // Args []string //} // //// Prometheus contains the metrics gathered by the instance and its path. //type Prometheus struct { // reqCnt *prometheus.CounterVec // reqDur *prometheus.HistogramVec // reqSz, resSz prometheus.Summary // router *gin.Engine // listenAddress string // Ppg PrometheusPushGateway // // MetricsList []*Metric // MetricsPath string // // ReqCntURLLabelMappingFn RequestCounterURLLabelMappingFn // // // gin.Context string to use as a prometheus URL label // URLLabelFromContext string //} // //// PrometheusPushGateway contains the configuration for pushing to a Prometheus pushgateway (optional). //type PrometheusPushGateway struct { // // // Push interval in seconds // PushIntervalSeconds time.Duration // // // Push Gateway URL in format http://domain:port // // where JOBNAME can be any string of your choice // PushGatewayURL string // // // Local metrics URL where metrics are fetched from, this could be omitted in the future // // if implemented using prometheus common/expfmt instead // MetricsURL string // // // pushgateway job name, defaults to "gin" // Job string //} // //// NewPrometheus generates a new set of metrics with a certain subsystem name. //func NewPrometheus(subsystem string, customMetricsList ...[]*Metric) *Prometheus { // if subsystem == "" { // subsystem = "app" // } // // var metricsList []*Metric // // if len(customMetricsList) > 1 { // panic("Too many args. NewPrometheus( string, ).") // } else if len(customMetricsList) == 1 { // metricsList = customMetricsList[0] // } // metricsList = append(metricsList, standardMetrics...) // // p := &Prometheus{ // MetricsList: metricsList, // MetricsPath: defaultMetricPath, // ReqCntURLLabelMappingFn: func(c *gin.Context) string { // return c.FullPath() // e.g. /user/:id , /user/:id/info // }, // } // // p.registerMetrics(subsystem) // // return p //} // //// SetPushGateway sends metrics to a remote pushgateway exposed on pushGatewayURL //// every pushIntervalSeconds. Metrics are fetched from metricsURL. //func (p *Prometheus) SetPushGateway(pushGatewayURL, metricsURL string, pushIntervalSeconds time.Duration) { // p.Ppg.PushGatewayURL = pushGatewayURL // p.Ppg.MetricsURL = metricsURL // p.Ppg.PushIntervalSeconds = pushIntervalSeconds // p.startPushTicker() //} // //// SetPushGatewayJob job name, defaults to "gin". //func (p *Prometheus) SetPushGatewayJob(j string) { // p.Ppg.Job = j //} // //// SetListenAddress for exposing metrics on address. If not set, it will be exposed at the //// same address of the gin engine that is being used. //func (p *Prometheus) SetListenAddress(address string) { // p.listenAddress = address // if p.listenAddress != "" { // p.router = gin.Default() // } //} // //// SetListenAddressWithRouter for using a separate router to expose metrics. (this keeps things like GET /metrics out of //// your content's access log). //func (p *Prometheus) SetListenAddressWithRouter(listenAddress string, r *gin.Engine) { // p.listenAddress = listenAddress // if len(p.listenAddress) > 0 { // p.router = r // } //} // //// SetMetricsPath set metrics paths. //func (p *Prometheus) SetMetricsPath(e *gin.Engine) error { // // if p.listenAddress != "" { // p.router.GET(p.MetricsPath, prometheusHandler()) // return p.runServer() // } else { // e.GET(p.MetricsPath, prometheusHandler()) // return nil // } //} // //// SetMetricsPathWithAuth set metrics paths with authentication. //func (p *Prometheus) SetMetricsPathWithAuth(e *gin.Engine, accounts gin.Accounts) error { // // if p.listenAddress != "" { // p.router.GET(p.MetricsPath, gin.BasicAuth(accounts), prometheusHandler()) // return p.runServer() // } else { // e.GET(p.MetricsPath, gin.BasicAuth(accounts), prometheusHandler()) // return nil // } // //} // //func (p *Prometheus) runServer() error { // return p.router.Run(p.listenAddress) //} // //func (p *Prometheus) getMetrics() []byte { // response, err := http.Get(p.Ppg.MetricsURL) // if err != nil { // return nil // } // // defer response.Body.Close() // // body, _ := io.ReadAll(response.Body) // return body //} // //var hostname, _ = os.Hostname() // //func (p *Prometheus) getPushGatewayURL() string { // if p.Ppg.Job == "" { // p.Ppg.Job = "gin" // } // return p.Ppg.PushGatewayURL + "/metrics/job/" + p.Ppg.Job + "/instance/" + hostname //} // //func (p *Prometheus) sendMetricsToPushGateway(metrics []byte) { // req, err := http.NewRequest("POST", p.getPushGatewayURL(), bytes.NewBuffer(metrics)) // if err != nil { // return // } // // client := &http.Client{} // resp, err := client.Do(req) // if err != nil { // fmt.Println("Error sending to push gateway error:", err.Error()) // } // // resp.Body.Close() //} // //func (p *Prometheus) startPushTicker() { // ticker := time.NewTicker(time.Second * p.Ppg.PushIntervalSeconds) // go func() { // for range ticker.C { // p.sendMetricsToPushGateway(p.getMetrics()) // } // }() //} // //// 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 { // fmt.Println("could not be registered in Prometheus,metricDef.Name:", metricDef.Name, " error:", err.Error()) // } // // switch metricDef { // case reqCounter: // p.reqCnt = metric.(*prometheus.CounterVec) // case reqDuration: // p.reqDur = metric.(*prometheus.HistogramVec) // case resSize: // p.resSz = metric.(prometheus.Summary) // case reqSize: // p.reqSz = metric.(prometheus.Summary) // } // metricDef.MetricCollector = metric // } //} // //// Use adds the middleware to a gin engine. //func (p *Prometheus) Use(e *gin.Engine) error { // e.Use(p.HandlerFunc()) // return p.SetMetricsPath(e) //} // //// UseWithAuth adds the middleware to a gin engine with BasicAuth. //func (p *Prometheus) UseWithAuth(e *gin.Engine, accounts gin.Accounts) error { // e.Use(p.HandlerFunc()) // return p.SetMetricsPathWithAuth(e, accounts) //} // //// HandlerFunc defines handler function for middleware. //func (p *Prometheus) HandlerFunc() gin.HandlerFunc { // return func(c *gin.Context) { // if c.Request.URL.Path == p.MetricsPath { // c.Next() // return // } // // start := time.Now() // reqSz := computeApproximateRequestSize(c.Request) // // c.Next() // // status := strconv.Itoa(c.Writer.Status()) // elapsed := float64(time.Since(start)) / float64(time.Second) // resSz := float64(c.Writer.Size()) // // url := p.ReqCntURLLabelMappingFn(c) // if len(p.URLLabelFromContext) > 0 { // u, found := c.Get(p.URLLabelFromContext) // if !found { // u = "unknown" // } // url = u.(string) // } // p.reqDur.WithLabelValues(status, c.Request.Method, url).Observe(elapsed) // p.reqCnt.WithLabelValues(status, c.Request.Method, c.HandlerName(), c.Request.Host, url).Inc() // p.reqSz.Observe(float64(reqSz)) // p.resSz.Observe(resSz) // } //} // //func prometheusHandler() gin.HandlerFunc { // h := promhttp.Handler() // return func(c *gin.Context) { // h.ServeHTTP(c.Writer, c.Request) // } //} // //func computeApproximateRequestSize(r *http.Request) int { // var s int // 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) // // // r.FormData and r.MultipartForm are assumed to be included in r.URL. // // if r.ContentLength != -1 { // s += int(r.ContentLength) // } // return s //}