内容总览:
1:组件监控平台和grafana
1:web服务器,接受请求,生成grafana url,并返回对应的grafana面板给用户;通过thanos查询所有集群名,然后把结果返回给前端(重点)
2:部署grafana,很简单,可以百度/chatgpt,故一笔带过;配置grafana,普通用户看到部分变量,管理员看到全部变量(重点)
3:grafana面板配置,包括变量设置,promql编写,仪表盘设置,这里只是简单介绍一下,更多详情请自己百度
备注:我们的组件监控平台是我们自己的网页,grafana是另外部署的,我们的组件平台去grafana请求网址,然后再把获取的页面嵌入到我们自己的监控平台
简介:
我们自己开发了一个服务平台,然后我们需要增加一个组件监控功能,我们决定通过grafana来监控组件,我们需要把grafana面板做到我们的服务平台里。因为我们的组件监控功能是给用户用的,所以用户看到的面板应该只具有查看,不具有编辑功能,所以要实现这样一个功能,需要两个步骤:第一步是返回grafana面板给用户,这个简单,我们直接返回指定grafana面板的url就行,前端再把这个url作为一个iframe嵌入网页就行;第二步是配置grafana,使得管理员和用户看到的面板是不同的,一个具有操作权限,一个只有非管理员权限
1:web服务器返回grafana面板
1:这个简单,服务器内部保存一个map[key]grafana_dashboard_url,然后接收用户参数,首先生成key,从而我们可以知道要返回哪个dashboard,然后是组装grafana_dashboard_url,可以用url参数来设置grafana面板中变量的值,这个组装就是把grafana_dashboard_url和请求中的参数拼接到一起,生成最终的dashboardurl,因为可能有些dashboard变量我们不直接开放给用户而是用另一种形式让用户设置,我们会隐藏掉这个变量,但是因为面板显示结果时又需要这个值,所以我们的做法就是把展示给用户的面板中隐藏掉这个值但是在我们的平台中提供相关设置项,但是把其他变量都暴露给用户(因为不同的面板需要不同的变量,面板的每个变量都在平台中弄一个设置项不灵活也麻烦,直接把大部分变量都暴露给用户,直接由用户去选择,只隐藏少量比较隐私的变量),然后我们的服务器接收到到请求后,就会解析请求,从请求中解析出相关的设置,然后根据这些设置再为该变量生成一个指定的值,然后再拼接成指定的url,然后再返回给前端,前端再去请求对应的grafana面板。由于我们是通过url来返回具有编辑权限和非编辑权限的面板,所以用户可能绕过平台直接伪造url获取具有编辑权限的面板,所以可能有安全问题,不过我们这里不关心,认证和授权则由专门的模块去做比如网关。
2:假设我们隐藏数据源变量,而是给用户提供一个cluster选项,这样用户选择一个集群,前端传一个clusterName给我们,我们服务器内部把clusterName转换成数据源,然后再拼接url,因为我们的集群可能是动态的,比如人增加一个集群,减少一个集群,或者改变集群用途(我们此时相应的要修改集群名字,因为集群名字应该和实际用途相关,比如测试、预发、生产),所以我们不能直接代码里写死,二是通过thanos动态查询,然后再把获取的所有集群名返回给前端,然后用户再从中挑选一个集群名传给我们的(同样,数据源和集群名的关系也可以通过thanos动态查找,这里为了简单就直接写死了)
备注:grafna有个kiosk参数,但是这个参数太简单,不满足我们的要求,有需要者可以直接百度
我们现在来写代码,这里是模拟服务器,也就是略去了收发请求,只写处理函数:
package main
import (
"fmt"
"io/ioutil"
"k8s.io/apimachinery/pkg/util/json"
"net/http"
)
type HttpRequest struct{
//对应面板的key,通过这个key来获取面板url,key=MiddlerwareType_MiddlewareVersion,因为不同版本可能需要不同的面板
MiddlewareType string
MiddlewareVersion string
//集群名,不同的集群可能对应不同的数据源,数据源我们不直接暴露给用户,而是暴露一个集群名给用户
//然后我们服务器内部再根据集群名把对应的数据源参数组装到最终的面板url
ClusterName string
//我们开放给用户的权限为:允许用户查询指定集群中指定命名空间下的所有redis-cluster集群的监控信息
NameSpace string
}
var (
//保存面板url的map,其中key1=type,key2=version
DashBoardMap map[string]map[string]string
//保存集群名和数据源之间的映射,这些数据员就是我们自己在grafana中配置的prometheus数据源(thanos也叫prometheus数据源)
ClusterDataSourceMap map[string]string
//Thanos url,我们可以用来查询ip等信息,后面会介绍
//一个thanos可以聚合多个集群的prometheus,所以一个thanos可以查多个集群的数据
//THANOS_URL="https://localhost:10901/api/v1/query"
//query=count(kube_deployment_annotations)by(clusterName)"
//面板变量包括两种,一种是所有面板都有这个变量,所以我们在所有面板中都把给个变量取一个相同的名字
//比如数据源(datasource)、命名空间(namespace,不同的部门使用不同的namespace)
//grafana规定变量参数前面要加一个"var-"前缀
VAR_DATASOURCE_KEY="var-datasource"
VAR_NAMESPACKE_KEY="var-namespace"
)
func init(){
/*
url参数orgId表示grafana中的用户组,我们可以给不同的组配置不同的权限,给用户配置一个viewer权限,orgId为2
url参数theme参数有dark和light之分,我们在grafana中配置theme=light的样式,比如隐藏部分变量按钮
当用户看的时候就是返回theme=light样式的面板
当管理员看的时候就是返回theme=dark样式的面板
*/
DashBoardMap=map[string]map[string]string{
"redis-cluster":map[string]string{
//假设v1和v2版本面板是通用的,但是v3版本更新了指标系统,所以v3得重新弄一个面板
"v1":"https://localhost:3000/xxxxx/redis-cluster-v1?orgId=2&theme=light",
"v2":"https://localhost:3000/xxxxx/redis-cluster-v1?orgId=2&theme=light",
"v3":"https://localhost:3000/xxxxx/redis-cluster-v3?orgId=2&theme=light",
},
"other-middler-ware":nil,
}
ClusterDataSourceMap=map[string]string{
"cluster-test":"prometheus-test", //测试集群
"cluster-pro":"prometheus-pro", //生产集群
}
}
//没有做错误检查,仅简单演示流程
//构造查询url查询thanos,然后从结果中解析出所有的集群名
func GetAllClusterName()[]string{
//thanos和prometheus api的使用方法基本一样,都是构造一些查询参数,然后直接去指定url查即可
promQl:="count(kube_deployment_annotations)by(clusterName)"
searchUrl:=THANOS_URL+"?query="+promQl
//完整url:https://thanos_query_ip:port/api/v1/query?query=count(kube_deployment_annotations)by(clusterName)
//查询结果是json格式的文件,我们解析这个json文件即可
body:= DoGet(searchUrl)
thanosResult:=&ThanosResponse{}
_ = json.Unmarshal(body, thanosResult)
clusterNames:=[]string{}
//读取响应中的所有clusterName
for idx:=range thanosResult.Data.Result{
labels:=thanosResult.Data.Result[idx].Metric
clusterNames=append(clusterNames,labels.ClusterName)
}
return clusterNames
}
func GenerateUrl(req*HttpRequest)string{
resUrl:=""
//获取面板url
resUrl=DashBoardMap[req.MiddlewareType][req.MiddlewareVersion]
//根据clusterName获取数据源
dataSource:=ClusterDataSourceMap[req.ClusterName]
//获取命名空间
namespace:=req.NameSpace
//填充datasource到url
resUrl=fmt.Sprintf("%s&%s=%s", resUrl, VAR_DATASOURCE_KEY,dataSource)
//填充namespace到rul
resUrl=fmt.Sprintf("%s&%s=%s", resUrl,VAR_NAMESPACKE_KEY,namespace)
return resUrl
}
func main(){
mockReq:=&HttpRequest{
ClusterName:"cluster-test",
NameSpace: "bigdata",
MiddlewareType: "redis-cluster",
MiddlewareVersion: "v1",
}
dashboardUrl:=GenerateUrl(mockReq)
fmt.Println("final grafana dashboard url is :",dashboardUrl)
clusterNames:=GetAllClusterName()
fmt.Println("all cluster is :",clusterNames)
/*控制台会打印如下结果:(查询url和集群名根据实际结果而定)
final grafana dashboard url is : https://localhost:3000/xxxxx/redis-cluster-v1?orgId=2&theme=light&var-datasource=prometheus-test&var-namespace=bigdata
all cluster is : [test pro]
*/
}
//直接请求该url,然后读取body并返回
func DoGet(url string) []byte {
// 发送 GET 请求
resp,_:= http.Get(url)
defer resp.Body.Close()
// 读取响应体
body, _ := ioutil.ReadAll(resp.Body)
return body
}
//下面的所有json字段名必须和返回的json文件中的字段名一致,否则会解析失败
type ThanosResponse struct {
Status Status `json:"status"`
ErrorType string `json:"errorType"`
Error string `json:"error"`
Data Data `json:"data"`
}
type Status string
type Data struct {
ResultType ResultType `json:"resultType"`
Result []Resulte `json:"result"`
}
type ResultType string
type Resulte struct {
Metric ThanosMetricLabels `json:"metric"`
Values []ThanosValue `json:"values"`
}
type ThanosMetricLabels struct {
/*
这个结构体用来表示从thanos查到的指标中的lables,
因为我们目前只需要ClusterName,所以只有一个ClusterName字段
再次声明,该字段后面的json注释中的字段名必须和thanos中的一样,
比如thanos有一个指标a{labelNameA="xxx"},如果我们要获取labelNameA的值,
那么我们就必须把json字段名设置为labelNameA,这里是clusterName
其实不难理解,我们可以用浏览器的ui界面查询,也可以直接用代码查询,
ui界面只不过是把我们的操作转换为代码,然后把自动解析响应并展示,只不过我们这里都用代码,都是我们自己手动解析
*/
ClusterName string `json:"clusterName"`
/*
DataSourceName string `json:"prometheusName"`
我们可以把集群名和数据源的映射关系也写到指标的标签里,这样我们同样可以通过thanos动态获取clusterName和数据源的映射关系
这个字段和clusterName字段一样都需要自己配置,否则是没有的,后续在thanos章节会介绍,其实就是配置一个external字段就可以了
*/
}
type ThanosValue []interface{}
/* thanos返回的数据格式样例:https://thanos_query_ip:port/api/v1/query?query=count(kube_deployment_annotations)by(clusterName)
ui界面:
{clusterName="test"} 200
{clusterName="pro"} 100
json格式:
{
"status": "success",
"data": {
"resultType": "vector",
"result": [{
"metric": {
#"__name__": "xxxxx" #指标名字通过特殊字段__name__来表示,我们这里因为是count计算语句,所有结果中不存在此字段
"clusterName": "test" #指标的标签
},
"value": [1708583625.541, "200"] #前者代表时间,后者代表该条数据的值
}, {
"metric": {
"clusterName": "pro"
},
"value": [1708583625.541, "100"]
}]
}
}
*/
2:部署grafana和配置grafana
一个deployment.yaml+一个config.yaml,重点关注config.yaml中的注释,因为该注释说明了如何根据需要,针对同一个面板返回隐藏了部分变量和显示所有变量的面板,即用户看到部分变量,管理员看到全部变量
grafana.deployment.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
name: grafana
namespace: test-monitoring
labels:
app: grafana
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
volumes:
- name: grafana-pvc #用来保存数据的,保证重启数据还在
persistentVolumeClaim:
claimName: grafana
- name: grafana-config #configmap,重点关注这个configmap
configMap:
name: grafana-config
defaultMode: 420
containers:
- name: grafana
image: 'grafana/grafana:8.3.1'
ports:
- name: http-port
containerPort: 3000 #grafana默认端口
protocol: TCP
resources: {}
volumeMounts:
- name: grafana-pvc
mountPath: /var/lib/grafana
- name: grafana-config
mountPath: /usr/share/grafana/public/views/index.html #我们就是通过修改这个文件来实现返回不同的面板
subPath: index.html
#我们这里就没挂载grafana.ini文件了,不知道为啥我的机器上他已经能嵌入了,不需要再配置了。。如果不能访问再去挂载
imagePullPolicy: IfNotPresent
restartPolicy: Always
securityContext:
runAsUser: 0
grafana.configmap.yaml,我们是通过configmap来覆盖掉grafana自带的文件从而实现自定义配置的功能
kind: ConfigMap
apiVersion: v1
metadata:
name: grafana-config
namespace: test-monitoring
labels:
app: grafana-config
data:
grafana.ini: | #grafana配置文件
allow_embedding = true #允许页面被当做iframe嵌入其他网页,否则我们请求网址的时候会被拒绝
[auth.anonymous]
# enable anonymous access
enabled = true #关闭验证,允许非登录用户查看面板
index.html: |
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/><meta name="viewport" content="width=device-width"/><meta name="theme-color" content="#000"/><title>[[.AppTitle]]</title><base href="[[.AppSubUrl]]/"/><link rel="preload" href="[[.ContentDeliveryURL]]public/fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2" as="font" crossorigin/><link rel="icon" type="image/png" href="[[.FavIcon]]"/><link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]"/><link rel="mask-icon" href="[[.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28"/><link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/grafana.[[ .Theme ]].fab5d6bbd438adca1160.css"/><script nonce="[[.Nonce]]">performance.mark('frontend_boot_css_time_seconds');</script><meta name="apple-mobile-web-app-capable" content="yes"/><meta name="apple-mobile-web-app-status-bar-style" content="black"/><meta name="msapplication-TileColor" content="#2b5797"/><meta name="msapplication-config" content="public/img/browserconfig.xml"/></head><body class="theme-[[ .Theme ]] [[.AppNameBodyClass]]">
<style>
/*
!!!!!重点在这里,重点在这里,重点在这里!!!!!
grafana内置支持两种展示style,.theme-light和.theme-dark,如果是用户查询,我们就返回在url参数里设置theme=light,如果是管理员,我们就显示theme=dark
我们可以通过.theme操控整个grafna面板的通用样式,即修改会应用到所有的监控面板,所以虽然有一定灵活性,但还是有一定局限性。
.theme-light div.submenu-item:nth-child(1) > div:nth-child(1) {display: none}
.theme-light div.submenu-item:nth-child(1) > div:nth-child(2) {display: none}
!!!我们把数据源和命名空间变量分别放到第一和第二的位置,我们这里对于light主题会隐藏掉第一个和第二个变量,而dark主题则依旧显示所有变量
*/
.theme-light .page-toolbar.css-1rf5v84 {display: none} #如果orgId对应admin,那么我们可能对用户展示时还需要隐藏掉编辑按钮,从而禁止用户编辑面板
.theme-light button[aria-label="Toggle menu"]{pointer-events: none} #禁掉相关按钮
.theme-light button[aria-label="切换菜单"]{pointer-events: none} #禁掉相关按钮
.theme-light div.submenu-item:nth-child(1) > div:nth-child(1) {display: none}
...后续省略一大串内容,因为后面的内容我们都没有修改,就是pod里面/usr/share/grafana/public/views/index.html 中的内容,至于怎么获取,我们直接进入grafana pod,然后cat,然后复制即可获取原文件内容,我们再在前面添加我们自定义的配置即可,ini文件可通过同样的套路获取...
3:grafna配置
3.1:变量
变量我们应该按照层层递进的关系,比如数据源、命名空间、redis-exporter、instance四个变量,那么应该是数据源决定命名空间,命名空间决定exporter,exporter决定instance,选定了数据源后则只能查该集群的namespace,选定namespace后则只能查该namespace下的exporter,当exporter选定以后,instance也就确定了(我们目前是把redis-exporter做到了redis-cluster的每一个pod里)
变量名 变量来源
datasource prometheus
namespace label_values(redis_up,namespace)
exporter label_values(redis_up{namespace="$namespace"},job)
instance label_values(redis_up{namespace="$namespace",job="$exporter"},instance)
the_slave_of_master label_values(redis_connected_slave_offset_bytes{namespace="$namespace",job="$exporter",instance="$instance"},slave_ip)
the_master_of_slave label_values(redis_slave_repl_offset{namespace="$namespace",job="$exporter",instance="$instance"},master_host)
3.2:promeQl
//查询指定命名空间下指定exporter对应的指定$instance变量对应的redis节点上的指标
redis_db_keys{namespace="$namespace",job="$exporter",instance="$instance"}
3.3:仪表盘曲线名设置即legend字段设置
{{instance}}:表示引用redis_db_keys中标签instance的值
${instance}:表示引用变量instance的值