From 1003639e5baad100f09f27e0b4515efbb20d807f Mon Sep 17 00:00:00 2001
From: OutOfEastGate <1946066280@qq.com>
Date: Tue, 25 Apr 2023 16:06:09 +0800
Subject: [PATCH] feat: support for prometheus (#1784)
---
authz/authz.go | 2 +
controllers/prometheus.go | 39 ++++++++
go.mod | 2 +
main.go | 3 +-
object/prometheus.go | 129 +++++++++++++++++++++++++++
routers/prometheus_filter.go | 51 +++++++++++
routers/router.go | 5 +-
web/src/SystemInfo.js | 22 ++++-
web/src/backend/SystemInfo.js | 10 +++
web/src/table/PrometheusInfoTable.js | 82 +++++++++++++++++
10 files changed, 342 insertions(+), 3 deletions(-)
create mode 100644 controllers/prometheus.go
create mode 100644 object/prometheus.go
create mode 100644 routers/prometheus_filter.go
create mode 100644 web/src/table/PrometheusInfoTable.js
diff --git a/authz/authz.go b/authz/authz.go
index 71a6e92b..c2829a1c 100644
--- a/authz/authz.go
+++ b/authz/authz.go
@@ -121,6 +121,8 @@ p, *, *, *, /cas, *, *
p, *, *, *, /api/webauthn, *, *
p, *, *, GET, /api/get-release, *, *
p, *, *, GET, /api/get-default-application, *, *
+p, *, *, GET, /api/get-prometheus-info, *, *
+p, *, *, *, /api/metrics, *, *
`
sa := stringadapter.NewAdapter(ruleText)
diff --git a/controllers/prometheus.go b/controllers/prometheus.go
new file mode 100644
index 00000000..b192f8ef
--- /dev/null
+++ b/controllers/prometheus.go
@@ -0,0 +1,39 @@
+// Copyright 2023 The Casdoor Authors. 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 controllers
+
+import (
+ "github.com/casdoor/casdoor/object"
+)
+
+// GetPrometheusInfo
+// @Title GetPrometheusInfo
+// @Tag Prometheus API
+// @Description get Prometheus Info
+// @Success 200 {object} object.PrometheusInfo The Response object
+// @router /get-prometheus-info [get]
+func (c *ApiController) GetPrometheusInfo() {
+ _, ok := c.RequireAdmin()
+ if !ok {
+ return
+ }
+ prometheusInfo, err := object.GetPrometheusInfo()
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ c.ResponseOk(prometheusInfo)
+}
diff --git a/go.mod b/go.mod
index df8711a4..a2d9526f 100644
--- a/go.mod
+++ b/go.mod
@@ -36,6 +36,8 @@ require (
github.com/markbates/goth v1.75.2
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5
+ github.com/prometheus/client_golang v1.7.0
+ github.com/prometheus/client_model v0.2.0
github.com/qiangmzsx/string-adapter/v2 v2.1.0
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/gosaml2 v0.6.0
diff --git a/main.go b/main.go
index 09b565f1..227954d3 100644
--- a/main.go
+++ b/main.go
@@ -82,6 +82,7 @@ func main() {
logs.SetLogFuncCall(false)
go ldap.StartLdapServer()
+ go object.ClearThroughputPerSecond()
- beego.Run(fmt.Sprintf(":%v", port))
+ beego.RunWithMiddleWares(fmt.Sprintf(":%v", port), routers.PrometheusMiddleWare)
}
diff --git a/object/prometheus.go b/object/prometheus.go
new file mode 100644
index 00000000..59aad42a
--- /dev/null
+++ b/object/prometheus.go
@@ -0,0 +1,129 @@
+// Copyright 2023 The Casdoor Authors. 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 object
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+ "github.com/prometheus/client_model/go"
+)
+
+type PrometheusInfo struct {
+ APIThroughput []GaugeVecInfo `json:"apiThroughput"`
+ APILatency []HistogramVecInfo `json:"apiLatency"`
+ TotalThroughput float64 `json:"totalThroughput"`
+}
+
+type GaugeVecInfo struct {
+ Method string `json:"method"`
+ Name string `json:"name"`
+ Throughput float64 `json:"throughput"`
+}
+
+type HistogramVecInfo struct {
+ Name string `json:"name"`
+ Method string `json:"method"`
+ Count uint64 `json:"count"`
+ Latency string `json:"latency"`
+}
+
+var (
+ APIThroughput = promauto.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "casdoor_api_throughput",
+ Help: "The throughput of each api access",
+ }, []string{"path", "method"})
+
+ APILatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
+ Name: "casdoor_api_latency",
+ Help: "API processing latency in milliseconds",
+ }, []string{"path", "method"})
+
+ CpuUsage = promauto.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "casdoor_cpu_usage",
+ Help: "Casdoor cpu usage",
+ }, []string{"cpuNum"})
+
+ MemoryUsage = promauto.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "casdoor_memory_usage",
+ Help: "Casdoor memory usage in Byte",
+ }, []string{"type"})
+
+ TotalThroughput = promauto.NewGauge(prometheus.GaugeOpts{
+ Name: "casdoor_total_throughput",
+ Help: "The total throughput of casdoor",
+ })
+)
+
+func ClearThroughputPerSecond() {
+ // Clear the throughput every second
+ ticker := time.NewTicker(time.Second)
+ for range ticker.C {
+ APIThroughput.Reset()
+ TotalThroughput.Set(0)
+ }
+}
+
+func GetPrometheusInfo() (*PrometheusInfo, error) {
+ res := &PrometheusInfo{}
+ metricFamilies, err := prometheus.DefaultGatherer.Gather()
+ if err != nil {
+ return nil, err
+ }
+ for _, metricFamily := range metricFamilies {
+ switch metricFamily.GetName() {
+ case "casdoor_api_throughput":
+ res.APIThroughput = getGaugeVecInfo(metricFamily)
+ case "casdoor_api_latency":
+ res.APILatency = getHistogramVecInfo(metricFamily)
+ case "casdoor_total_throughput":
+ res.TotalThroughput = metricFamily.GetMetric()[0].GetGauge().GetValue()
+ }
+ }
+
+ return res, nil
+}
+
+func getHistogramVecInfo(metricFamily *io_prometheus_client.MetricFamily) []HistogramVecInfo {
+ var histogramVecInfos []HistogramVecInfo
+ for _, metric := range metricFamily.GetMetric() {
+ sampleCount := metric.GetHistogram().GetSampleCount()
+ sampleSum := metric.GetHistogram().GetSampleSum()
+ latency := sampleSum / float64(sampleCount)
+ histogramVecInfo := HistogramVecInfo{
+ Method: metric.Label[0].GetValue(),
+ Name: metric.Label[1].GetValue(),
+ Count: sampleCount,
+ Latency: fmt.Sprintf("%.3f", latency),
+ }
+ histogramVecInfos = append(histogramVecInfos, histogramVecInfo)
+ }
+ return histogramVecInfos
+}
+
+func getGaugeVecInfo(metricFamily *io_prometheus_client.MetricFamily) []GaugeVecInfo {
+ var counterVecInfos []GaugeVecInfo
+ for _, metric := range metricFamily.GetMetric() {
+ counterVecInfo := GaugeVecInfo{
+ Method: metric.Label[0].GetValue(),
+ Name: metric.Label[1].GetValue(),
+ Throughput: metric.Gauge.GetValue(),
+ }
+ counterVecInfos = append(counterVecInfos, counterVecInfo)
+ }
+ return counterVecInfos
+}
diff --git a/routers/prometheus_filter.go b/routers/prometheus_filter.go
new file mode 100644
index 00000000..860a33ca
--- /dev/null
+++ b/routers/prometheus_filter.go
@@ -0,0 +1,51 @@
+package routers
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/casdoor/casdoor/object"
+ "github.com/casdoor/casdoor/util"
+)
+
+type PrometheusMiddleWareWrapper struct {
+ handler http.Handler
+}
+
+func PrometheusMiddleWare(h http.Handler) http.Handler {
+ return &PrometheusMiddleWareWrapper{
+ handler: h,
+ }
+}
+
+func (p PrometheusMiddleWareWrapper) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+ method := req.Method
+ endpoint := req.URL.Path
+ if strings.HasPrefix(endpoint, "/api/metrics") {
+ systemInfo, err := util.GetSystemInfo()
+ if err == nil {
+ recordSystemInfo(systemInfo)
+ }
+ p.handler.ServeHTTP(w, req)
+ return
+ }
+
+ if strings.HasPrefix(endpoint, "/api") {
+ start := time.Now()
+ p.handler.ServeHTTP(w, req)
+ latency := time.Since(start).Milliseconds()
+ object.TotalThroughput.Inc()
+ object.APILatency.WithLabelValues(endpoint, method).Observe(float64(latency))
+ object.APIThroughput.WithLabelValues(endpoint, method).Inc()
+ }
+}
+
+func recordSystemInfo(systemInfo *util.SystemInfo) {
+ for i, value := range systemInfo.CpuUsage {
+ object.CpuUsage.WithLabelValues(fmt.Sprintf("%d", i)).Set(value)
+ }
+ object.MemoryUsage.WithLabelValues("memoryUsed").Set(float64(systemInfo.MemoryUsed))
+ object.MemoryUsage.WithLabelValues("memoryTotal").Set(float64(systemInfo.MemoryTotal))
+}
diff --git a/routers/router.go b/routers/router.go
index 24eb160d..4e0baf28 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -21,8 +21,8 @@ package routers
import (
"github.com/beego/beego"
-
"github.com/casdoor/casdoor/controllers"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
)
func init() {
@@ -239,4 +239,7 @@ func initAPI() {
beego.Router("/api/get-system-info", &controllers.ApiController{}, "GET:GetSystemInfo")
beego.Router("/api/get-version-info", &controllers.ApiController{}, "GET:GetVersionInfo")
+ beego.Router("/api/get-prometheus-info", &controllers.ApiController{}, "GET:GetPrometheusInfo")
+
+ beego.Handler("/api/metrics", promhttp.Handler())
}
diff --git a/web/src/SystemInfo.js b/web/src/SystemInfo.js
index 3be02612..3cb808d9 100644
--- a/web/src/SystemInfo.js
+++ b/web/src/SystemInfo.js
@@ -17,6 +17,7 @@ import * as SystemBackend from "./backend/SystemInfo";
import React from "react";
import * as Setting from "./Setting";
import i18next from "i18next";
+import PrometheusInfoTable from "./table/PrometheusInfoTable";
class SystemInfo extends React.Component {
@@ -25,6 +26,7 @@ class SystemInfo extends React.Component {
this.state = {
systemInfo: {cpuUsage: [], memoryUsed: 0, memoryTotal: 0},
versionInfo: {},
+ prometheusInfo: {apiThroughput: [], apiLatency: [], totalThroughput: 0},
intervalId: null,
loading: true,
};
@@ -45,6 +47,11 @@ class SystemInfo extends React.Component {
}).catch(error => {
Setting.showMessage("error", `System info failed to get: ${error}`);
});
+ SystemBackend.getPrometheusInfo().then(res => {
+ this.setState({
+ prometheusInfo: res.data,
+ });
+ });
}, 1000 * 2);
this.setState({intervalId: id});
}).catch(error => {
@@ -80,7 +87,10 @@ class SystemInfo extends React.Component {
;
-
+ const latencyUi = this.state.prometheusInfo.apiLatency.length <= 0 ?