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 ? : + ; + const throughputUi = this.state.prometheusInfo.apiLatency.length <= 0 ? : + ; const link = this.state.versionInfo?.version !== "" ? `https://github.com/casdoor/casdoor/releases/tag/${this.state.versionInfo?.version}` : ""; let versionText = this.state.versionInfo?.version !== "" ? this.state.versionInfo?.version : i18next.t("system:Unknown version"); if (this.state.versionInfo?.commitOffset > 0) { @@ -103,6 +113,16 @@ class SystemInfo extends React.Component { {this.state.loading ? : memUi} + + + {this.state.loading ? : latencyUi} + + + + + {this.state.loading ? : throughputUi} + + diff --git a/web/src/backend/SystemInfo.js b/web/src/backend/SystemInfo.js index e6f871be..a0bdaee9 100644 --- a/web/src/backend/SystemInfo.js +++ b/web/src/backend/SystemInfo.js @@ -33,3 +33,13 @@ export function getVersionInfo() { }, }).then(res => res.json()); } + +export function getPrometheusInfo() { + return fetch(`${Setting.ServerUrl}/api/get-prometheus-info `, { + method: "GET", + credentials: "include", + headers: { + "Accept-Language": Setting.getAcceptLanguage(), + }, + }).then(res => res.json()); +} diff --git a/web/src/table/PrometheusInfoTable.js b/web/src/table/PrometheusInfoTable.js new file mode 100644 index 00000000..6e13c307 --- /dev/null +++ b/web/src/table/PrometheusInfoTable.js @@ -0,0 +1,82 @@ +// 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. + +import React from "react"; +import {Table} from "antd"; + +class PrometheusInfoTable extends React.Component { + constructor(props) { + super(props); + this.state = { + table: props.table, + }; + } + render() { + const latencyColumns = [ + { + title: "Name", + dataIndex: "name", + key: "name", + }, + { + title: "Method", + dataIndex: "method", + key: "method", + }, + { + title: "Count", + dataIndex: "count", + key: "count", + }, + { + title: "Latency(ms)", + dataIndex: "latency", + key: "latency", + }, + ]; + const throughputColumns = [ + { + title: "Name", + dataIndex: "name", + key: "name", + }, + { + title: "Method", + dataIndex: "method", + key: "method", + }, + { + title: "Throughput", + dataIndex: "throughput", + key: "throughput", + }, + ]; + if (this.state.table === "latency") { + return ( +
+ + + ); + } else if (this.state.table === "throughput") { + return ( +
+ Total Throughput: {this.props.prometheusInfo.totalThroughput} +
+ + ); + } + } +} + +export default PrometheusInfoTable;