mirror of
https://github.com/ccfos/nightingale.git
synced 2026-03-03 06:29:16 +00:00
Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11152b447d | ||
|
|
9ffaefead2 | ||
|
|
307306ca8a | ||
|
|
5b5ef8346b | ||
|
|
b7cc7d6065 | ||
|
|
c77c9de5a7 | ||
|
|
52e0ba2eae | ||
|
|
9957c4e42a | ||
|
|
68ea466f59 | ||
|
|
41adc6f586 | ||
|
|
aa6daffe7b | ||
|
|
f0c28bb271 | ||
|
|
707a35bc06 | ||
|
|
88f1645d3a | ||
|
|
d8255d0cd3 | ||
|
|
fcc26f6410 | ||
|
|
6ff112f5da | ||
|
|
87899cbedb | ||
|
|
d4257d11f2 | ||
|
|
00b9c31f29 | ||
|
|
1c8c6b92a9 | ||
|
|
9c9fe800e4 | ||
|
|
9aeeaa191e | ||
|
|
e69112958b | ||
|
|
6d8317927e | ||
|
|
072f1bd51f | ||
|
|
25dbc62ff4 | ||
|
|
b233067789 | ||
|
|
d531178c9b | ||
|
|
174df1495c | ||
|
|
ffe423148d | ||
|
|
926559c9a7 | ||
|
|
136642f126 | ||
|
|
a054828fcc | ||
|
|
e46e946689 | ||
|
|
cf083c543b | ||
|
|
2e1508fdd3 | ||
|
|
954543a5b2 | ||
|
|
71a402c33c | ||
|
|
e30a5a316f | ||
|
|
0c9b7de391 | ||
|
|
063b6f63df | ||
|
|
44b780093a | ||
|
|
780ad19dd9 | ||
|
|
c6d133772a | ||
|
|
c5bb8a4a13 | ||
|
|
06c1664577 | ||
|
|
96a4c1ebfa | ||
|
|
b0c05368f7 | ||
|
|
eebf2cff49 | ||
|
|
30d021bc19 | ||
|
|
b4ea395fe3 | ||
|
|
9f4d1a1ea7 | ||
|
|
ed06da90d9 | ||
|
|
9461b549d2 | ||
|
|
3b1b595461 | ||
|
|
4257de69fd | ||
|
|
ddc86f20ee | ||
|
|
bf27162a9b | ||
|
|
f8ac0a9b4a | ||
|
|
7a190b152c | ||
|
|
99fbdae121 | ||
|
|
aa26ddfb48 | ||
|
|
ba5aba9cdf | ||
|
|
3400803672 | ||
|
|
f11377b289 | ||
|
|
1165312532 | ||
|
|
8a145d5ba2 | ||
|
|
352415662a | ||
|
|
65d8f80637 | ||
|
|
b3700c7251 | ||
|
|
106a8e490a | ||
|
|
5332f797a6 | ||
|
|
aff0dbfea1 | ||
|
|
da5dd683d6 | ||
|
|
15892d6e57 | ||
|
|
fbff60eefb | ||
|
|
62867ddbf2 | ||
|
|
5d4acb6cc3 | ||
|
|
b893483d26 | ||
|
|
4130a5df02 | ||
|
|
445d03e096 | ||
|
|
577c402a5b | ||
|
|
40bbbfd475 | ||
|
|
0d05ad85f2 | ||
|
|
e70622d18c | ||
|
|
562f98ddaf | ||
|
|
ee07969c8a | ||
|
|
5b0e24cd40 | ||
|
|
78b2e54910 | ||
|
|
2e64c83632 | ||
|
|
537d5d2386 | ||
|
|
86899b8c48 | ||
|
|
fcc45ebf2a | ||
|
|
95727e9c00 | ||
|
|
3a3ad5d9d9 | ||
|
|
7209da192f | ||
|
|
98f3508424 | ||
|
|
c33900ee1b | ||
|
|
a2490104b9 | ||
|
|
1a25c3804e | ||
|
|
23eb766c14 | ||
|
|
a7bad003f5 | ||
|
|
e4e48cfda0 | ||
|
|
d0ce4c25e5 | ||
|
|
01aea821b9 | ||
|
|
bdc1c1c60b | ||
|
|
09f37b8076 | ||
|
|
fc4c4b96bf | ||
|
|
5c60c2c85e | ||
|
|
1e9bd900e9 | ||
|
|
1ca000af2c | ||
|
|
81fade557b | ||
|
|
b82f646636 | ||
|
|
26a3d2dafa | ||
|
|
5e931ebe8e | ||
|
|
8c45479c02 | ||
|
|
940313bd4e | ||
|
|
5057cd0ae6 | ||
|
|
a4be2c73ac | ||
|
|
a38e50d6b8 | ||
|
|
89f66dd5d1 | ||
|
|
3963470603 | ||
|
|
640b6e6825 | ||
|
|
e7d2c45f9d | ||
|
|
80ee54898a | ||
|
|
fe68cebbf9 | ||
|
|
c1fec215a9 | ||
|
|
388228a631 | ||
|
|
b4ddd03691 | ||
|
|
b92e4abf86 | ||
|
|
a1c458b764 | ||
|
|
acb4b8e33e | ||
|
|
54eab51e54 | ||
|
|
be89fde030 | ||
|
|
37711ea6b2 | ||
|
|
3b5c8d8357 | ||
|
|
635369e3fd | ||
|
|
6c2c945bd9 | ||
|
|
48d24c79d6 | ||
|
|
c6a1761a7b | ||
|
|
23d7e5a7de | ||
|
|
b1b2c7d6b0 | ||
|
|
f34c3c6a2c | ||
|
|
454dc7f983 | ||
|
|
c1e92b56b9 | ||
|
|
fd93fd7182 | ||
|
|
1a446f0749 | ||
|
|
f18ed76593 | ||
|
|
9b3a9f29d9 | ||
|
|
49965fd5d5 | ||
|
|
a248e054fa | ||
|
|
bbb35d36be | ||
|
|
fd3e51cbb1 | ||
|
|
bd0480216c | ||
|
|
2c963258cf | ||
|
|
b4f267fb01 | ||
|
|
ea46401db2 | ||
|
|
58e777eb00 | ||
|
|
04a9161f75 | ||
|
|
1ed8f38833 | ||
|
|
bb17751a81 | ||
|
|
a8dcb1fe83 | ||
|
|
1ea30e03a4 | ||
|
|
ba0eafa065 | ||
|
|
c78c8d07f2 | ||
|
|
8fe9e57c03 | ||
|
|
64646d2ace | ||
|
|
e747e73145 | ||
|
|
896f85efdf | ||
|
|
77e4499a32 | ||
|
|
7c351e09e5 | ||
|
|
14ad3b1b0a | ||
|
|
184867d07c | ||
|
|
3476b95b35 | ||
|
|
76e105c93a | ||
|
|
39705787c9 | ||
|
|
293680a9cd | ||
|
|
05005357fb | ||
|
|
ba7ff133e6 | ||
|
|
0bd7ba9549 | ||
|
|
17c7361620 | ||
|
|
c45cbd02cc | ||
|
|
04cb501ab4 | ||
|
|
ba6f089c78 | ||
|
|
ab0cb6fc47 | ||
|
|
2847a315b1 | ||
|
|
65439df7fb | ||
|
|
b6436b09ce | ||
|
|
92354d5765 | ||
|
|
05651ad744 | ||
|
|
b7ff82d722 | ||
|
|
a285966560 | ||
|
|
538880b0e0 | ||
|
|
299270f74e | ||
|
|
9c69362650 | ||
|
|
d508aef7e5 | ||
|
|
616674b643 | ||
|
|
94847d9059 | ||
|
|
cbd416495c | ||
|
|
cc32194fb6 | ||
|
|
f5e2b43526 | ||
|
|
5bc8f0b9b1 | ||
|
|
7359a69223 | ||
|
|
04d64d09d7 | ||
|
|
43343182e4 | ||
|
|
072ab98fcf | ||
|
|
35ef6b9265 | ||
|
|
eaa53f2533 | ||
|
|
de322c4daf | ||
|
|
936c751a93 | ||
|
|
796a7014a1 | ||
|
|
f4368302ea | ||
|
|
01e611a9f9 |
44
.github/workflows/n9e.yml
vendored
44
.github/workflows/n9e.yml
vendored
@@ -1,26 +1,32 @@
|
||||
name: Go
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
env:
|
||||
GO_VERSION: 1.18
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.17
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: make
|
||||
- name: Checkout Source Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go Environment
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
88
.goreleaser.yaml
Normal file
88
.goreleaser.yaml
Normal file
@@ -0,0 +1,88 @@
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
|
||||
snapshot:
|
||||
name_template: '{{ .Tag }}'
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
changelog:
|
||||
skip: true
|
||||
|
||||
builds:
|
||||
- id: build
|
||||
hooks:
|
||||
pre:
|
||||
- ./fe.sh
|
||||
main: ./src/
|
||||
binary: n9e
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/didi/nightingale/v5/src/pkg/version.VERSION={{ .Tag }}-{{.Commit}}
|
||||
|
||||
archives:
|
||||
- id: n9e
|
||||
builds:
|
||||
- build
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: "n9e-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
|
||||
wrap_in_directory: false
|
||||
files:
|
||||
- docker/*
|
||||
- etc/*
|
||||
- pub/*
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: ccfos
|
||||
name: nightingale
|
||||
name_template: "v{{ .Version }}"
|
||||
|
||||
dockers:
|
||||
- image_templates:
|
||||
- flashcatcloud/nightingale:{{ .Version }}-amd64
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
ids:
|
||||
- build
|
||||
dockerfile: docker/Dockerfile.goreleaser
|
||||
extra_files:
|
||||
- pub
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- image_templates:
|
||||
- flashcatcloud/nightingale:{{ .Version }}-arm64v8
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
ids:
|
||||
- build
|
||||
dockerfile: docker/Dockerfile.goreleaser
|
||||
extra_files:
|
||||
- pub
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
|
||||
docker_manifests:
|
||||
|
||||
- name_template: flashcatcloud/nightingale:{{ .Version }}
|
||||
image_templates:
|
||||
- flashcatcloud/nightingale:{{ .Version }}-amd64
|
||||
- flashcatcloud/nightingale:{{ .Version }}-arm64v8
|
||||
|
||||
- name_template: flashcatcloud/nightingale:latest
|
||||
image_templates:
|
||||
- flashcatcloud/nightingale:{{ .Version }}-amd64
|
||||
- flashcatcloud/nightingale:{{ .Version }}-arm64v8
|
||||
10
Makefile
10
Makefile
@@ -2,10 +2,15 @@
|
||||
|
||||
NOW = $(shell date -u '+%Y%m%d%I%M%S')
|
||||
|
||||
RELEASE_VERSION = 5.9.4
|
||||
|
||||
APP = n9e
|
||||
SERVER_BIN = $(APP)
|
||||
ROOT:=$(shell pwd -P)
|
||||
GIT_COMMIT:=$(shell git --work-tree ${ROOT} rev-parse 'HEAD^{commit}')
|
||||
_GIT_VERSION:=$(shell git --work-tree ${ROOT} describe --tags --abbrev=14 "${GIT_COMMIT}^{commit}" 2>/dev/null)
|
||||
TAG=$(shell echo "${_GIT_VERSION}" | awk -F"-" '{print $$1}')
|
||||
RELEASE_VERSION:="$(TAG)-$(GIT_COMMIT)"
|
||||
|
||||
# RELEASE_ROOT = release
|
||||
# RELEASE_SERVER = release/${APP}
|
||||
# GIT_COUNT = $(shell git rev-list --all --count)
|
||||
@@ -17,6 +22,9 @@ all: build
|
||||
build:
|
||||
go build -ldflags "-w -s -X github.com/didi/nightingale/v5/src/pkg/version.VERSION=$(RELEASE_VERSION)" -o $(SERVER_BIN) ./src
|
||||
|
||||
build-linux:
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "-w -s -X github.com/didi/nightingale/v5/src/pkg/version.VERSION=$(RELEASE_VERSION)" -o $(SERVER_BIN) ./src
|
||||
|
||||
# start:
|
||||
# @go run -ldflags "-X main.VERSION=$(RELEASE_TAG)" ./cmd/${APP}/main.go web -c ./configs/config.toml -m ./configs/model.conf --menu ./configs/menu.yaml
|
||||
run_webapi:
|
||||
|
||||
138
README.md
138
README.md
@@ -1,102 +1,128 @@
|
||||
<img src="doc/img/ccf-n9e.png" width="240">
|
||||
<p align="center">
|
||||
<a href="https://github.com/ccfos/nightingale">
|
||||
<img src="doc/img/nightingale_logo_h.png" alt="nightingale - cloud native monitoring" width="240" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="GitHub latest release" src="https://img.shields.io/github/v/release/ccfos/nightingale"/>
|
||||
<a href="https://n9e.github.io">
|
||||
<img alt="Docs" src="https://img.shields.io/badge/docs-get%20started-brightgreen"/></a>
|
||||
<a href="https://hub.docker.com/u/flashcatcloud">
|
||||
<img alt="Docker pulls" src="https://img.shields.io/docker/pulls/flashcatcloud/nightingale"/></a>
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/ccfos/nightingale">
|
||||
<img alt="GitHub Repo issues" src="https://img.shields.io/github/issues/ccfos/nightingale">
|
||||
<img alt="GitHub Repo issues closed" src="https://img.shields.io/github/issues-closed/ccfos/nightingale">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/ccfos/nightingale">
|
||||
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
|
||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors-anon/ccfos/nightingale"/></a>
|
||||
<img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue"/>
|
||||
</p>
|
||||
<p align="center">
|
||||
<b>All-in-one</b> 的开源云原生监控系统 <br/>
|
||||
<b>开箱即用</b>,集数据采集、可视化、监控告警于一体 <br/>
|
||||
推荐升级您的 <b>Prometheus + AlertManager + Grafana</b> 组合方案到夜莺!
|
||||
</p>
|
||||
|
||||
[English](./README_EN.md) | [中文](./README.md)
|
||||
|
||||
> 夜莺是一款开源的云原生监控系统,采用 All-In-One 的设计,提供企业级的功能特性,开箱即用的产品体验。推荐升级您的 Prometheus + AlertManager + Grafana 组合方案到夜莺。
|
||||
|
||||
**夜莺监控具有以下特点:**
|
||||
|
||||
#### 1. 开箱即用
|
||||
支持 Docker、Helm Chart 等多种部署方式,内置多种监控大盘、快捷视图、告警规则模板,导入即可快速使用,活跃、专业的社区用户也在持续迭代和沉淀更多的最佳实践于产品中;
|
||||
|
||||
#### 2. 兼容并包
|
||||
支持 [Categraf](https://github.com/flashcatcloud/categraf)、Telegraf、Grafana-agent 等多种采集器,支持 Prometheus、VictoriaMetrics、M3DB 等各种时序数据库,支持对接 Grafana,与云原生生态无缝集成;
|
||||
|
||||
#### 3. 开放社区
|
||||
托管于[中国计算机学会开源发展委员会](https://www.ccf.org.cn/kyfzwyh/),有[快猫星云](https://flashcat.cloud)的持续投入,和数千名社区用户的积极参与,以及夜莺监控项目清晰明确的定位,都保证了夜莺开源社区健康、长久的发展;
|
||||
|
||||
#### 4. 高性能
|
||||
得益于夜莺的多数据源管理引擎,和夜莺引擎侧优秀的架构设计,借助于高性能时序库,可以满足数亿时间线的采集、存储、告警分析场景,节省大量成本;
|
||||
|
||||
#### 5. 高可用
|
||||
夜莺监控组件均可水平扩展,无单点,已在上千家企业部署落地,经受了严苛的生产实践检验。众多互联网头部公司,夜莺集群机器达百台,处理十亿级时间线,重度使用夜莺监控;
|
||||
|
||||
#### 6. 灵活扩展
|
||||
夜莺监控,可部署在1核1G的云主机,可在上百台机器部署集群,可运行在K8s中;也可将时序库、告警引擎等组件下沉到各机房、各region,兼顾边缘部署和中心化管理;
|
||||
|
||||
|
||||
#### 如果您在使用 Prometheus 过程中,有以下的一个或者多个需求场景,推荐您升级到夜莺:
|
||||
## Highlighted Features
|
||||
|
||||
- **开箱即用**
|
||||
- 支持 Docker、Helm Chart、云服务等多种部署方式,集数据采集、监控告警、可视化为一体,内置多种监控仪表盘、快捷视图、告警规则模板,导入即可快速使用,**大幅降低云原生监控系统的建设成本、学习成本、使用成本**;
|
||||
- **专业告警**
|
||||
- 可视化的告警配置和管理,支持丰富的告警规则,提供屏蔽规则、订阅规则的配置能力,支持告警多种送达渠道,支持告警自愈、告警事件管理等;
|
||||
- **云原生**
|
||||
- 以交钥匙的方式快速构建企业级的云原生监控体系,支持 [Categraf](https://github.com/flashcatcloud/categraf)、Telegraf、Grafana-agent 等多种采集器,支持 Prometheus、VictoriaMetrics、M3DB、ElasticSearch 等多种数据库,兼容支持导入 Grafana 仪表盘,**与云原生生态无缝集成**;
|
||||
- **高性能 高可用**
|
||||
- 得益于夜莺的多数据源管理引擎,和夜莺引擎侧优秀的架构设计,借助于高性能时序库,可以满足数亿时间线的采集、存储、告警分析场景,节省大量成本;
|
||||
- 夜莺监控组件均可水平扩展,无单点,已在上千家企业部署落地,经受了严苛的生产实践检验。众多互联网头部公司,夜莺集群机器达百台,处理数亿级时间线,重度使用夜莺监控;
|
||||
- **灵活扩展 中心化管理**
|
||||
- 夜莺监控,可部署在 1 核 1G 的云主机,可在上百台机器集群化部署,可运行在 K8s 中;也可将时序库、告警引擎等组件下沉到各机房、各 Region,兼顾边缘部署和中心化统一管理,**解决数据割裂,缺乏统一视图的难题**;
|
||||
- **开放社区**
|
||||
- 托管于[中国计算机学会开源发展委员会](https://www.ccf.org.cn/kyfzwyh/),有[快猫星云](https://flashcat.cloud)和众多公司的持续投入,和数千名社区用户的积极参与,以及夜莺监控项目清晰明确的定位,都保证了夜莺开源社区健康、长久的发展。活跃、专业的社区用户也在持续迭代和沉淀更多的最佳实践于产品中;
|
||||
|
||||
**如果您在使用 Prometheus 过程中,有以下的一个或者多个需求场景,推荐您无缝升级到夜莺**:
|
||||
|
||||
- Prometheus、Alertmanager、Grafana 等多个系统较为割裂,缺乏统一视图,无法开箱即用;
|
||||
- 通过修改配置文件来管理 Prometheus、Alertmanager 的方式,学习曲线大,协同有难度;
|
||||
- 数据量过大而无法扩展您的 Prometheus 集群;
|
||||
- 生产环境运行多套 Prometheus 集群,面临管理和使用成本高的问题;
|
||||
|
||||
#### 如果您在使用 Zabbix,有以下的场景,推荐您升级到夜莺:
|
||||
**如果您在使用 Zabbix,有以下的场景,推荐您升级到夜莺**:
|
||||
|
||||
- 监控的数据量太大,希望有更好的扩展解决方案;
|
||||
- 学习曲线高,多人多团队模式下,希望有更好的协同使用效率;
|
||||
- 微服务和云原生架构下,监控数据的生命周期多变、监控数据维度基数高,Zabbix 数据模型不易适配;
|
||||
|
||||
#### 如果您在使用 [open-falcon](https://github.com/open-falcon/falcon-plus),我们更推荐您升级到夜莺:
|
||||
- 关于open-falcon和夜莺的详细介绍,请参考阅读[《云原生监控的十个特点和趋势》](https://mp.weixin.qq.com/s?__biz=MzkzNjI5OTM5Nw==&mid=2247483738&idx=1&sn=e8bdbb974a2cd003c1abcc2b5405dd18&chksm=c2a19fb0f5d616a63185cd79277a79a6b80118ef2185890d0683d2bb20451bd9303c78d083c5#rd)。
|
||||
> 了解更多Zabbix和夜莺监控的对比,推荐您进一步阅读[《Zabbix 和夜莺监控选型对比》](https://flashcat.cloud/blog/zabbx-vs-nightingale/)
|
||||
|
||||
#### 我们推荐您使用 [Categraf](https://github.com/flashcatcloud/categraf) 作为首选的监控数据采集器:
|
||||
- Categraf 是夜莺监控的默认采集器,采用开放插件机制和 all-in-one 的设计,同时支持 metric、log、trace、event 的采集。Categraf 不仅可以采集 CPU、内存、网络等系统层面的指标,也集成了众多开源组件的采集能力,支持K8s生态。Categraf 内置了对应的仪表盘和告警规则,开箱即用。
|
||||
**如果您在使用 [Open-Falcon](https://github.com/open-falcon/falcon-plus),我们推荐您升级到夜莺:**
|
||||
|
||||
- 关于 Open-Falcon 和夜莺的详细介绍,请参考阅读:[《云原生监控的十个特点和趋势》](http://flashcat.cloud/blog/10-trends-of-cloudnative-monitoring/)
|
||||
|
||||
**我们推荐您使用 [Categraf](https://github.com/flashcatcloud/categraf) 作为首选的监控数据采集器**:
|
||||
|
||||
- [Categraf](https://github.com/flashcatcloud/categraf) 是夜莺监控的默认采集器,采用开放插件机制和 All-in-one 的设计理念,同时支持 metric、log、trace、event 的采集。Categraf 不仅可以采集 CPU、内存、网络等系统层面的指标,也集成了众多开源组件的采集能力,支持K8s生态。Categraf 内置了对应的仪表盘和告警规则,开箱即用。
|
||||
|
||||
|
||||
## 资料链接
|
||||
## Getting Started
|
||||
|
||||
- [快速安装](https://mp.weixin.qq.com/s/iEC4pfL1TgjMDOWYh8H-FA)
|
||||
- [详细文档](https://n9e.github.io/)
|
||||
- [社区分享](https://n9e.github.io/docs/prologue/share/)
|
||||
[国外文档](https://n9e.github.io/) | [国内文档](http://n9e.flashcat.cloud/)
|
||||
|
||||
## 产品演示
|
||||
## Screenshots
|
||||
|
||||
<img src="doc/img/intro.gif" width="680">
|
||||
https://user-images.githubusercontent.com/792850/216888712-2565fcea-9df5-47bd-a49e-d60af9bd76e8.mp4
|
||||
|
||||
## 架构介绍
|
||||
## Architecture
|
||||
|
||||
<img src="doc/img/arch-product.png" width="680">
|
||||
<img src="doc/img/arch-product.png" width="600">
|
||||
|
||||
Nightingale 可以接收各种采集器上报的监控数据(比如 [Categraf](https://github.com/flashcatcloud/categraf)、telegraf、grafana-agent、Prometheus),并写入多种流行的时序数据库中(可以支持Prometheus、M3DB、VictoriaMetrics、Thanos、TDEngine等),提供告警规则、屏蔽规则、订阅规则的配置能力,提供监控数据的查看能力,提供告警自愈机制(告警触发之后自动回调某个webhook地址或者执行某个脚本),提供历史告警事件的存储管理、分组查看的能力。
|
||||
夜莺监控可以接收各种采集器上报的监控数据(比如 [Categraf](https://github.com/flashcatcloud/categraf)、telegraf、grafana-agent、Prometheus),并写入多种流行的时序数据库中(可以支持Prometheus、M3DB、VictoriaMetrics、Thanos、TDEngine等),提供告警规则、屏蔽规则、订阅规则的配置能力,提供监控数据的查看能力,提供告警自愈机制(告警触发之后自动回调某个webhook地址或者执行某个脚本),提供历史告警事件的存储管理、分组查看的能力。
|
||||
|
||||
|
||||
<img src="doc/img/arch-system.png" width="680">
|
||||
<img src="doc/img/arch-system.png" width="600">
|
||||
|
||||
夜莺 v5 版本的设计非常简单,核心是 server 和 webapi 两个模块,webapi 无状态,放到中心端,承接前端请求,将用户配置写入数据库;server 是告警引擎和数据转发模块,一般随着时序库走,一个时序库就对应一套 server,每套 server 可以只用一个实例,也可以多个实例组成集群,server 可以接收 Categraf、Telegraf、Grafana-Agent、Datadog-Agent、Falcon-Plugins 上报的数据,写入后端时序库,周期性从数据库同步告警规则,然后查询时序库做告警判断。每套 server 依赖一个 redis。
|
||||
|
||||
|
||||
<img src="doc/img/install-vm.png" width="680">
|
||||
<img src="doc/img/install-vm.png" width="600">
|
||||
|
||||
如果单机版本的 Prometheus 性能不够或容灾较差,我们推荐使用 [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics),VictoriaMetrics 架构较为简单,性能优异,易于部署和运维,架构图如上。VictoriaMetrics 更详尽的文档,还请参考其[官网](https://victoriametrics.com/)。
|
||||
如果单机版本的时序数据库(比如 Prometheus) 性能有瓶颈或容灾较差,我们推荐使用 [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics),VictoriaMetrics 架构较为简单,性能优异,易于部署和运维,架构图如上。VictoriaMetrics 更详尽的文档,还请参考其[官网](https://victoriametrics.com/)。
|
||||
|
||||
|
||||
## 如何参与
|
||||
## Community
|
||||
|
||||
开源项目要更有生命力,离不开开放的治理架构和源源不断的开发者和用户共同参与,我们致力于建立开放、中立的开源治理架构,吸纳更多来自企业、高校等各方面对云原生监控感兴趣、有热情的计算机专业人士,打造专业、有活力的开发者社区。关于《夜莺开源项目和社区治理架构(草案)》,请查阅 [doc/community-governance.md](./doc/community-governance.md).
|
||||
开源项目要更有生命力,离不开开放的治理架构和源源不断的开发者和用户共同参与,我们致力于建立开放、中立的开源治理架构,吸纳更多来自企业、高校等各方面对云原生监控感兴趣、有热情的开发者,一起打造有活力的夜莺开源社区。关于《夜莺开源项目和社区治理架构(草案)》,请查阅 [COMMUNITY GOVERNANCE](./doc/community-governance.md).
|
||||
|
||||
**我们欢迎您以各种方式参与到夜莺开源项目和开源社区中来,工作包括不限于**:
|
||||
- 补充和完善文档 => [n9e.github.io](https://n9e.github.io/);
|
||||
- 分享您在使用夜莺监控过程中的最佳实践和经验心得 => [文章分享](https://n9e.github.io/docs/prologue/share/);
|
||||
- 提交产品建议 =》 [github issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md);
|
||||
- 提交代码,让夜莺监控更快、更稳、更好用 => [github pull request](https://github.com/didi/nightingale/pulls);
|
||||
|
||||
- 补充和完善文档 => [n9e.github.io](https://n9e.github.io/)
|
||||
- 分享您在使用夜莺监控过程中的最佳实践和经验心得 => [文章分享](https://n9e.github.io/docs/prologue/share/)
|
||||
- 提交产品建议 =》 [github issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md)
|
||||
- 提交代码,让夜莺监控更快、更稳、更好用 => [github pull request](https://github.com/didi/nightingale/pulls)
|
||||
|
||||
**尊重、认可和记录每一位贡献者的工作**是夜莺开源社区的第一指导原则,我们提倡**高效的提问**,这既是对开发者时间的尊重,也是对整个社区知识沉淀的贡献:
|
||||
1. 提问之前请先查阅 [FAQ](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq) ;
|
||||
2. 提问之前请先搜索 [github issue](https://github.com/ccfos/nightingale/issues);
|
||||
3. 我们优先推荐通过提交 github issue 来提问,如果[有问题点击这里](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&template=bug_report.yml) | [有需求建议点击这里](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md);
|
||||
4. 最后,我们推荐你加入微信群,针对相关开放式问题,相互交流咨询 (请先加好友:[UlricGO](https://www.gitlink.org.cn/UlricQin/gist/tree/master/self.jpeg) 备注:夜莺加群+姓名+公司,交流群里会有开发者团队和专业、热心的群友回答问题);
|
||||
- 提问之前请先查阅 [FAQ](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq)
|
||||
- 我们使用[GitHub Discussions](https://github.com/ccfos/nightingale/discussions)作为交流论坛,有问题可以到这里搜索、提问
|
||||
- 我们也推荐你加入微信群,和其他夜莺用户交流经验 (请先加好友:[picobyte](https://www.gitlink.org.cn/UlricQin/gist/tree/master/self.jpeg) 备注:夜莺加群+姓名+公司)
|
||||
|
||||
|
||||
## 联系我们
|
||||
- 推荐您关注夜莺监控公众号,及时获取相关产品和社区动态
|
||||
## Who is using Nightingale
|
||||
|
||||
<img src="doc/img/n9e-vx-new.png" width="180">
|
||||
您可以通过在 **[Who is Using Nightingale](https://github.com/ccfos/nightingale/issues/897)** 登记您的使用情况,分享您的使用经验。
|
||||
|
||||
## Stargazers over time
|
||||
[](https://starchart.cc/ccfos/nightingale)
|
||||
|
||||
## Contributors
|
||||
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ccfos/nightingale" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
- [Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)
|
||||
[Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)
|
||||
|
||||
## Contact Us
|
||||
推荐您关注夜莺监控公众号,及时获取相关产品和社区动态:
|
||||
|
||||
<img src="doc/img/n9e-vx-new.png" width="120">
|
||||
|
||||
7
doc/active-contributors.md
Normal file
7
doc/active-contributors.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Active Contributors
|
||||
|
||||
- [xiaoziv](https://github.com/xiaoziv)
|
||||
- [tanxiao1990](https://github.com/tanxiao1990)
|
||||
- [bbaobelief](https://github.com/bbaobelief)
|
||||
- [freedomkk-qfeng](https://github.com/freedomkk-qfeng)
|
||||
- [lsy1990](https://github.com/lsy1990)
|
||||
5
doc/committers.md
Normal file
5
doc/committers.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## Committers
|
||||
|
||||
- [YeningQin](https://github.com/710leo)
|
||||
- [FeiKong](https://github.com/kongfei605)
|
||||
- [XiaqingDai](https://github.com/jsers)
|
||||
@@ -1,52 +1,80 @@
|
||||
# 夜莺开源项目和社区治理架构(草案)
|
||||
[夜莺监控](https://github.com/ccfos/nightingale "夜莺监控")是一款开源云原生监控系统,由滴滴设计开发,2020 年 3 月份开源之后,凭借其优秀的产品设计、灵活性架构和明确清晰的定位,夜莺监控快速发展为国内最活跃的企业级云原生监控方案。[截止当前](具体指2022年8月 "截止当前"),在 [Github](https://github.com/ccfos/nightingale "Github") 上已经迭代发布了 **70** 多个版本,获得了 **5K** 多个 Star,**80** 多位代码贡献者。快速的迭代,也让夜莺监控的用户群越来越大,涉及各行各业。
|
||||
|
||||
#### 用户(User)
|
||||
更进一步,夜莺监控于 2022 年 5 月 11 日,正式捐赠予中国计算机学会开源发展委员会 [CCF ODC](https://www.ccf.org.cn/kyfzwyh/ "CCF ODC"),为 CCF ODC 成立后接受捐赠的第一个开源项目。
|
||||
|
||||
>欢迎任何个人、公司以及组织,使用 Nightingale,并积极的反馈 bug、提交功能需求、以及相互帮助,我们推荐使用 github issue 来跟踪 bug 和管理需求。
|
||||
开源项目要更有生命力,离不开开放的治理架构和源源不断的开发者共同参与。夜莺监控项目加入 CCF 开源大家庭后,能在计算机学会的支持和带动下,进一步结合云原生、可观测、国产化等多个技术发展的需求,建立开放、中立的开源治理架构,打造更专业、有活力的开发者社区。
|
||||
|
||||
#### 贡献者(Contributer)
|
||||
**今天,我们郑重发布夜莺监控开源社区治理架构,并公示相关的任命和社区荣誉,期待开源的道路上,一起同行。**
|
||||
|
||||
>欢迎每一位用户,包括但不限于以下列方式参与到 Nightingale 开源项目并做出贡献:
|
||||
>1. 在 [github issue](https://github.com/ccfos/nightingale/issues) 中积极参与讨论;
|
||||
>2. 提交代码补丁;
|
||||
>3. 修订、补充和完善文档;
|
||||
>4. 提交建议 / 批评;
|
||||
# 夜莺监控开源社区架构
|
||||
|
||||
#### 提交者(Committer)
|
||||
### User|用户
|
||||
|
||||
>Committer 是指拥有 Nightingale 代码仓库写操作权限的贡献者,而且他们也签署了 Nightingale 项目贡献者许可协议(CLA),他们拥有 ccf.org.cn 为后缀的邮箱地址。原则上 Committer 能够自主决策某个代码补丁是否可以合入到 Nightingale 代码仓库,但是项目管委会拥有最终的决策权。
|
||||
> 欢迎任何个人、公司以及组织,使用夜莺监控,并积极的反馈 bug、提交功能需求、以及相互帮助,我们推荐使用 [Github Issue](https://github.com/ccfos/nightingale/issues "Github Issue") 来跟踪 bug 和管理需求。
|
||||
|
||||
#### 项目管委会成员(PMC Member)
|
||||
社区用户,可以通过在 **[Who is Using Nightingale](https://github.com/ccfos/nightingale/issues/897 "Who is Using Nightingale")** 登记您的使用情况,并分享您使用夜莺监控的经验,将会自动进入 **[END USERS](https://github.com/ccfos/nightingale/blob/main/doc/end-users.md "END USERS")** 文件列表,并获得社区的 **VIP Support**。
|
||||
|
||||
> 项目管委会成员,从贡献者或者 Committer 中选举产生,他们拥有 Nightingale 代码仓库的写操作权限,拥有 ccf.org.cn 为后缀的邮箱地址,拥有 Nightingale 社区相关事务的投票权、以及提名 Committer 候选人的权利。 项目管委会作为一个实体,为整个项目的发展全权负责。
|
||||
### Contributor|贡献者
|
||||
|
||||
#### 项目管委会主席(PMC Chair)
|
||||
> 欢迎每一位用户,包括但不限于以下方式参与到夜莺开源社区并做出贡献:
|
||||
|
||||
> 项目管委会主席采用任命制,由 [CCF ODC](https://www.ccf.org.cn/kyfzwyh/) 从项目管委会成员中任命产生。项目管委会作为一个统一的实体,来管理和领导 Nightingale 项目。管委会主席是 CCF ODC 和项目管委会之间的沟通桥梁,履行特定的项目管理职责。
|
||||
1. 在 [Github Issue](https://github.com/ccfos/nightingale/issues "Github Issue") 中积极参与讨论,参与社区活动;
|
||||
1. 提交代码补丁;
|
||||
1. 翻译、修订、补充和完善[文档](https://n9e.github.io "文档");
|
||||
1. 分享夜莺监控的使用经验,积极布道;
|
||||
1. 提交建议 / 批评;
|
||||
|
||||
# 沟通机制(Communication)
|
||||
年度累计向 [CCFOS/NIGHTINGALE](https://github.com/ccfos/nightingale "CCFOS/NIGHTINGALE") 提交 **5** 个PR(被合并),或者因为其他贡献被**项目管委会**一致认可,将会自动进入到 **[ACTIVE CONTRIBUTORS](https://github.com/ccfos/nightingale/blob/main/doc/active-contributors.md "ACTIVE CONTRIBUTORS")** 列表,并获得夜莺开源社区颁发的证书,享有夜莺开源社区一定的权益和福利。
|
||||
|
||||
所有向 [CCFOS/NIGHTINGALE](https://github.com/ccfos/nightingale "CCFOS/NIGHTINGALE") 提交过PR(被合并),或者做出过重要贡献的 Contributor,都会被永久记载于 [CONTRIBUTORS](https://github.com/ccfos/nightingale/blob/main/doc/contributors.md "CONTRIBUTORS") 列表。
|
||||
|
||||
### Committer|提交者
|
||||
|
||||
> Committer 是指拥有 [CCFOS/NIGHTINGALE](https://github.com/ccfos/nightingale "CCFOS/NIGHTINGALE") 代码仓库写操作权限的贡献者。原则上 Committer 能够自主决策某个代码补丁是否可以合入到夜莺代码仓库,但是项目管委会拥有最终的决策权。
|
||||
|
||||
Committer 承担以下一个或多个职责:
|
||||
- 积极回应 Issues;
|
||||
- Review PRs;
|
||||
- 参加开发者例行会议,积极讨论项目规划和技术方案;
|
||||
- 代表夜莺开源社区出席相关技术会议并做演讲;
|
||||
|
||||
Committer 记录并公示于 **[COMMITTERS](https://github.com/ccfos/nightingale/blob/main/doc/committers.md "COMMITTERS")** 列表,并获得夜莺开源社区颁发的证书,以及享有夜莺开源社区的各种权益和福利。
|
||||
|
||||
|
||||
### PMC|项目管委会
|
||||
|
||||
> PMC(项目管委会)作为一个实体,来管理和领导夜莺项目,为整个项目的发展全权负责。项目管委会相关内容记录并公示于文件[PMC](https://github.com/ccfos/nightingale/blob/main/doc/pmc.md "PMC").
|
||||
|
||||
- 项目管委会成员(PMC Member),从 Contributor 或者 Committer 中选举产生,他们拥有 [CCFOS/NIGHTINGALE](https://github.com/ccfos/nightingale "CCFOS/NIGHTINGALE") 代码仓库的写操作权限,拥有 Nightingale 社区相关事务的投票权、以及提名 Committer 候选人的权利。
|
||||
- 项目管委会主席(PMC Chair),从项目管委会成员中投票产生。管委会主席是 **[CCF ODC](https://www.ccf.org.cn/kyfzwyh/ "CCF ODC")** 和项目管委会之间的沟通桥梁,履行特定的项目管理职责。
|
||||
|
||||
## Communication|沟通机制
|
||||
1. 我们推荐使用邮件列表来反馈建议(待发布);
|
||||
2. 我们推荐使用 [github issue](https://github.com/ccfos/nightingale/issues) 跟踪 bug 和管理需求;
|
||||
3. 我们推荐使用 [github milestone](https://github.com/ccfos/nightingale/milestones) 来管理项目进度和规划;
|
||||
4. 我们推荐使用腾讯会议来定期召开项目例会;
|
||||
2. 我们推荐使用 [Github Issue](https://github.com/ccfos/nightingale/issues "Github Issue") 跟踪 bug 和管理需求;
|
||||
3. 我们推荐使用 [Github Milestone](https://github.com/ccfos/nightingale/milestones "Github Milestone") 来管理项目进度和规划;
|
||||
4. 我们推荐使用腾讯会议来定期召开项目例会(会议 ID 待发布);
|
||||
|
||||
# 文档(Documentation)
|
||||
1. 我们推荐使用 [github pages](https://n9e.github.io) 来沉淀文档;
|
||||
2. 我们推荐使用 [gitlink wiki](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq) 来沉淀FAQ;
|
||||
## Documentation|文档
|
||||
1. 我们推荐使用 [Github Pages](https://n9e.github.io "Github Pages") 来沉淀文档;
|
||||
2. 我们推荐使用 [Gitlink Wiki](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq "Gitlink Wiki") 来沉淀 FAQ;
|
||||
|
||||
|
||||
# 运营机制(Operation)
|
||||
## Operation|运营机制
|
||||
|
||||
1. 我们定期组织用户、贡献者、项目管委会成员之间的沟通会议,讨论项目开发的目标、方案、进度,以及讨论相关需求的合理性、优先级等议题;
|
||||
2. 我们定期组织 meetup (线上&线下),创造良好的用户交流分享环境,并沉淀相关内容到文档站点;
|
||||
3. 我们定期组织 Nightingale 开发者大会,分享 best user story、同步年度开发目标和计划、讨论新技术方向等;
|
||||
3. 我们定期组织夜莺开发者大会,分享 [best user story](https://n9e.github.io/docs/prologue/share/ "best user story")、同步年度开发目标和计划、讨论新技术方向等;
|
||||
|
||||
# 社区指导原则(Philosophy)
|
||||
- 尊重、认可和记录每一位贡献者的工作;
|
||||
## Philosophy|社区指导原则
|
||||
|
||||
>尊重、认可和记录每一位贡献者的工作。
|
||||
|
||||
## 关于提问的原则
|
||||
|
||||
# 关于提问的原则
|
||||
按照**尊重、认可、记录每一位贡献者的工作**原则,我们提倡**高效的提问**,这既是对开发者时间的尊重,也是对整个社区的知识沉淀的贡献:
|
||||
|
||||
1. 提问之前请先查阅 [FAQ](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq) ;
|
||||
2. 提问之前请先搜索 [github issue](https://github.com/ccfos/nightingale/issues);
|
||||
3. 我们优先推荐通过提交 github issue 来提问,如果[有问题点击这里](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&template=bug_report.yml) | [有需求建议点击这里](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md);
|
||||
4. 最后,我们推荐你加入微信群,针对相关开放式问题,相互交流咨询 (请先加好友:[UlricGO](https://www.gitlink.org.cn/UlricQin/gist/tree/master/self.jpeg) 备注:夜莺加群+姓名+公司,交流群里会有开发者团队和专业、热心的群友回答问题);
|
||||
1. 提问之前请先查阅 [FAQ](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq "FAQ") ;
|
||||
2. 提问之前请先搜索 [Github Issues](https://github.com/ccfos/nightingale/issues "Github Issue");
|
||||
3. 我们优先推荐通过提交 [Github Issue](https://github.com/ccfos/nightingale/issues "Github Issue") 来提问,如果[有问题点击这里](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&template=bug_report.yml "有问题点击这里") | [有需求建议点击这里](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md "有需求建议点击这里");
|
||||
|
||||
最后,我们推荐你加入微信群,针对相关开放式问题,相互交流咨询 (请先加好友:[UlricGO](https://www.gitlink.org.cn/UlricQin/gist/tree/master/self.jpeg "UlricGO") 备注:夜莺加群+姓名+公司,交流群里会有开发者团队和专业、热心的群友回答问题)。
|
||||
|
||||
5
doc/contributors.md
Normal file
5
doc/contributors.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ccfos/nightingale" />
|
||||
</a>
|
||||
5
doc/end-users.md
Normal file
5
doc/end-users.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## End Users
|
||||
|
||||
- [中移动](https://github.com/ccfos/nightingale/issues/897#issuecomment-1086573166)
|
||||
- [inke](https://github.com/ccfos/nightingale/issues/897#issuecomment-1099840636)
|
||||
- [方正证券](https://github.com/ccfos/nightingale/issues/897#issuecomment-1110492461)
|
||||
BIN
doc/img/ccf-logo.png
Normal file
BIN
doc/img/ccf-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
BIN
doc/img/nightingale_logo_h.png
Normal file
BIN
doc/img/nightingale_logo_h.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
BIN
doc/img/nightingale_logo_v.png
Normal file
BIN
doc/img/nightingale_logo_v.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
7
doc/pmc.md
Normal file
7
doc/pmc.md
Normal file
@@ -0,0 +1,7 @@
|
||||
### PMC Chair
|
||||
- [laiwei](https://github.com/laiwei)
|
||||
|
||||
### PMC Co-Chair
|
||||
- [UlricQin](https://github.com/UlricQin)
|
||||
|
||||
### PMC Member
|
||||
234
doc/server-dash.json
Normal file
234
doc/server-dash.json
Normal file
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"name": "夜莺大盘",
|
||||
"tags": "",
|
||||
"configs": {
|
||||
"var": [],
|
||||
"panels": [
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "rate(n9e_server_samples_received_total[1m])"
|
||||
}
|
||||
],
|
||||
"name": "每秒接收的数据点个数",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "none"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "off"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"i": "53fcb9dc-23f9-41e0-bc5e-121eed14c3a4",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "53fcb9dc-23f9-41e0-bc5e-121eed14c3a4"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "rate(n9e_server_alerts_total[10m])"
|
||||
}
|
||||
],
|
||||
"name": "每秒产生的告警事件个数",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "none"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "off"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
"i": "47fc6252-9cc8-4b53-8e27-0c5c59a47269",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "f70dcb8b-b58b-4ef9-9e48-f230d9e17140"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "n9e_server_alert_queue_size"
|
||||
}
|
||||
],
|
||||
"name": "告警事件内存队列长度",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "none"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "off"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 4,
|
||||
"i": "ad1af16c-de0c-45f4-8875-cea4e85d51d0",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "caf23e58-d907-42b0-9ed6-722c8c6f3c5f"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "n9e_server_http_request_duration_seconds_sum/n9e_server_http_request_duration_seconds_count"
|
||||
}
|
||||
],
|
||||
"name": "数据接收接口平均响应时间(单位:秒)",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "desc"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "noraml"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 4,
|
||||
"i": "64c3abc2-404c-4462-a82f-c109a21dac91",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "6b8d2db1-efca-4b9e-b429-57a9d2272bc5"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "n9e_server_sample_queue_size"
|
||||
}
|
||||
],
|
||||
"name": "内存数据队列长度",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "desc"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "off"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8,
|
||||
"i": "1c7da942-58c2-40dc-b42f-983e4a35b89b",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "bd41677d-40d3-482e-bb6e-fbd25df46d87"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "avg(n9e_server_forward_duration_seconds_sum/n9e_server_forward_duration_seconds_count)"
|
||||
}
|
||||
],
|
||||
"name": "数据发往TSDB平均耗时(单位:秒)",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "desc"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {
|
||||
"decimals": 8
|
||||
},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "noraml"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8,
|
||||
"i": "eed94a0b-954f-48ac-82e5-a2eada1c8a3d",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "c8642e72-f384-46a5-8410-1e6be2953c3c"
|
||||
}
|
||||
],
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM python:2
|
||||
FROM python:2.7.8-slim
|
||||
#FROM python:2
|
||||
#FROM ubuntu:21.04
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
14
docker/Dockerfile.goreleaser
Normal file
14
docker/Dockerfile.goreleaser
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM --platform=$BUILDPLATFORM python:2.7.8-slim
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
ADD n9e /app
|
||||
ADD http://download.flashcat.cloud/wait /wait
|
||||
RUN mkdir -p /app/pub && chmod +x /wait
|
||||
ADD pub /app/pub/
|
||||
RUN chmod +x n9e
|
||||
|
||||
EXPOSE 19000
|
||||
EXPOSE 18000
|
||||
|
||||
CMD ["/app/n9e", "-h"]
|
||||
@@ -43,3 +43,9 @@ basic_auth_pass = ""
|
||||
timeout = 5000
|
||||
dial_timeout = 2500
|
||||
max_idle_conns_per_host = 100
|
||||
|
||||
[http]
|
||||
enable = false
|
||||
address = ":9100"
|
||||
print_access = false
|
||||
run_mode = "release"
|
||||
|
||||
@@ -6,6 +6,7 @@ networks:
|
||||
|
||||
services:
|
||||
mysql:
|
||||
platform: linux/x86_64
|
||||
image: "mysql:5.7"
|
||||
container_name: mysql
|
||||
hostname: mysql
|
||||
@@ -35,7 +36,7 @@ services:
|
||||
- nightingale
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
image: prom/prometheus:v2.37.5
|
||||
container_name: prometheus
|
||||
hostname: prometheus
|
||||
restart: always
|
||||
@@ -52,7 +53,7 @@ services:
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--web.console.libraries=/usr/share/prometheus/console_libraries"
|
||||
- "--web.console.templates=/usr/share/prometheus/consoles"
|
||||
- "--enable-feature=remote-write-receiver"
|
||||
- "--web.enable-remote-write-receiver"
|
||||
- "--query.lookback-delta=2m"
|
||||
|
||||
ibex:
|
||||
@@ -79,7 +80,7 @@ services:
|
||||
sh -c "/wait && /app/ibex server"
|
||||
|
||||
nwebapi:
|
||||
image: ulric2019/nightingale:5.9.4
|
||||
image: flashcatcloud/nightingale:latest
|
||||
container_name: nwebapi
|
||||
hostname: nwebapi
|
||||
restart: always
|
||||
@@ -107,7 +108,7 @@ services:
|
||||
sh -c "/wait && /app/n9e webapi"
|
||||
|
||||
nserver:
|
||||
image: ulric2019/nightingale:5.9.4
|
||||
image: flashcatcloud/nightingale:latest
|
||||
container_name: nserver
|
||||
hostname: nserver
|
||||
restart: always
|
||||
@@ -135,7 +136,7 @@ services:
|
||||
sh -c "/wait && /app/n9e server"
|
||||
|
||||
categraf:
|
||||
image: "flashcatcloud/categraf:v0.1.9"
|
||||
image: "flashcatcloud/categraf:latest"
|
||||
container_name: "categraf"
|
||||
hostname: "categraf01"
|
||||
restart: always
|
||||
@@ -149,7 +150,7 @@ services:
|
||||
- /:/hostfs
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- "8094:8094/tcp"
|
||||
- "9100:9100/tcp"
|
||||
networks:
|
||||
- nightingale
|
||||
depends_on:
|
||||
|
||||
@@ -41,10 +41,12 @@ CREATE TABLE `user_group` (
|
||||
insert into user_group(id, name, create_at, create_by, update_at, update_by) values(1, 'demo-root-group', unix_timestamp(now()), 'root', unix_timestamp(now()), 'root');
|
||||
|
||||
CREATE TABLE `user_group_member` (
|
||||
`id` bigint unsigned not null auto_increment,
|
||||
`group_id` bigint unsigned not null,
|
||||
`user_id` bigint unsigned not null,
|
||||
KEY (`group_id`),
|
||||
KEY (`user_id`)
|
||||
KEY (`user_id`),
|
||||
PRIMARY KEY(`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
insert into user_group_member(group_id, user_id) values(1, 1);
|
||||
@@ -52,7 +54,7 @@ insert into user_group_member(group_id, user_id) values(1, 1);
|
||||
CREATE TABLE `configs` (
|
||||
`id` bigint unsigned not null auto_increment,
|
||||
`ckey` varchar(191) not null,
|
||||
`cval` varchar(1024) not null default '',
|
||||
`cval` varchar(4096) not null default '',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY (`ckey`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -70,10 +72,12 @@ insert into `role`(name, note) values('Standard', 'Ordinary user role');
|
||||
insert into `role`(name, note) values('Guest', 'Readonly user role');
|
||||
|
||||
CREATE TABLE `role_operation`(
|
||||
`id` bigint unsigned not null auto_increment,
|
||||
`role_name` varchar(128) not null,
|
||||
`operation` varchar(191) not null,
|
||||
KEY (`role_name`),
|
||||
KEY (`operation`)
|
||||
KEY (`operation`),
|
||||
PRIMARY KEY(`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
-- Admin is special, who has no concrete operation but can do anything.
|
||||
@@ -161,13 +165,16 @@ CREATE TABLE `board` (
|
||||
`id` bigint unsigned not null auto_increment,
|
||||
`group_id` bigint not null default 0 comment 'busi group id',
|
||||
`name` varchar(191) not null,
|
||||
`ident` varchar(200) not null default '',
|
||||
`tags` varchar(255) not null comment 'split by space',
|
||||
`public` tinyint(1) not null default 0 comment '0:false 1:true',
|
||||
`create_at` bigint not null default 0,
|
||||
`create_by` varchar(64) not null default '',
|
||||
`update_at` bigint not null default 0,
|
||||
`update_by` varchar(64) not null default '',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY (`group_id`, `name`)
|
||||
UNIQUE KEY (`group_id`, `name`),
|
||||
KEY(`ident`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
-- for dashboard new version
|
||||
@@ -226,6 +233,7 @@ CREATE TABLE `chart_share` (
|
||||
CREATE TABLE `alert_rule` (
|
||||
`id` bigint unsigned not null auto_increment,
|
||||
`group_id` bigint not null default 0 comment 'busi group id',
|
||||
`cate` varchar(128) not null,
|
||||
`cluster` varchar(128) not null,
|
||||
`name` varchar(255) not null,
|
||||
`note` varchar(1024) not null default '',
|
||||
@@ -238,9 +246,9 @@ CREATE TABLE `alert_rule` (
|
||||
`prom_for_duration` int not null comment 'prometheus for, unit:s',
|
||||
`prom_ql` text not null comment 'promql',
|
||||
`prom_eval_interval` int not null comment 'evaluate interval',
|
||||
`enable_stime` char(5) not null default '00:00',
|
||||
`enable_etime` char(5) not null default '23:59',
|
||||
`enable_days_of_week` varchar(32) not null default '' comment 'split by space: 0 1 2 3 4 5 6',
|
||||
`enable_stime` char(255) not null default '00:00',
|
||||
`enable_etime` char(255) not null default '23:59',
|
||||
`enable_days_of_week` varchar(255) not null default '' comment 'eg: "0 1 2 3 4 5 6 ; 0 1 2"',
|
||||
`enable_in_bg` tinyint(1) not null default 0 comment '1: only this bg 0: global',
|
||||
`notify_recovered` tinyint(1) not null comment 'whether notify when recovery',
|
||||
`notify_channels` varchar(255) not null default '' comment 'split by space: sms voice email dingtalk wecom',
|
||||
@@ -264,13 +272,18 @@ CREATE TABLE `alert_mute` (
|
||||
`id` bigint unsigned not null auto_increment,
|
||||
`group_id` bigint not null default 0 comment 'busi group id',
|
||||
`prod` varchar(255) not null default '',
|
||||
`note` varchar(1024) not null default '',
|
||||
`cate` varchar(128) not null,
|
||||
`cluster` varchar(128) not null,
|
||||
`tags` varchar(4096) not null default '' comment 'json,map,tagkey->regexp|value',
|
||||
`cause` varchar(255) not null default '',
|
||||
`btime` bigint not null default 0 comment 'begin time',
|
||||
`etime` bigint not null default 0 comment 'end time',
|
||||
`disabled` tinyint(1) not null default 0 comment '0:enabled 1:disabled',
|
||||
`create_at` bigint not null default 0,
|
||||
`create_by` varchar(64) not null default '',
|
||||
`update_at` bigint not null default 0,
|
||||
`update_by` varchar(64) not null default '',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY (`create_at`),
|
||||
KEY (`group_id`)
|
||||
@@ -278,7 +291,10 @@ CREATE TABLE `alert_mute` (
|
||||
|
||||
CREATE TABLE `alert_subscribe` (
|
||||
`id` bigint unsigned not null auto_increment,
|
||||
`name` varchar(255) not null default '',
|
||||
`disabled` tinyint(1) not null default 0 comment '0:enabled 1:disabled',
|
||||
`group_id` bigint not null default 0 comment 'busi group id',
|
||||
`cate` varchar(128) not null,
|
||||
`cluster` varchar(128) not null,
|
||||
`rule_id` bigint not null default 0,
|
||||
`tags` varchar(4096) not null default '' comment 'json,map,tagkey->regexp|value',
|
||||
@@ -350,7 +366,7 @@ CREATE TABLE `recording_rule` (
|
||||
`cluster` varchar(128) not null,
|
||||
`name` varchar(255) not null comment 'new metric name',
|
||||
`note` varchar(255) not null comment 'rule note',
|
||||
`disabled` tinyint(1) not null comment '0:enabled 1:disabled',
|
||||
`disabled` tinyint(1) not null default 0 comment '0:enabled 1:disabled',
|
||||
`prom_ql` varchar(8192) not null comment 'promql',
|
||||
`prom_eval_interval` int not null comment 'evaluate interval',
|
||||
`append_tags` varchar(255) default '' comment 'split by space: service=n9e mod=api',
|
||||
@@ -380,6 +396,7 @@ insert into alert_aggr_view(name, rule, cate) values('By RuleName', 'field:rule_
|
||||
|
||||
CREATE TABLE `alert_cur_event` (
|
||||
`id` bigint unsigned not null comment 'use alert_his_event.id',
|
||||
`cate` varchar(128) not null,
|
||||
`cluster` varchar(128) not null,
|
||||
`group_id` bigint unsigned not null comment 'busi group id of rule',
|
||||
`group_name` varchar(255) not null default '' comment 'busi group name',
|
||||
@@ -402,6 +419,7 @@ CREATE TABLE `alert_cur_event` (
|
||||
`notify_cur_number` int not null default 0 comment '',
|
||||
`target_ident` varchar(191) not null default '' comment 'target ident, also in tags',
|
||||
`target_note` varchar(191) not null default '' comment 'target note',
|
||||
`first_trigger_time` bigint,
|
||||
`trigger_time` bigint not null,
|
||||
`trigger_value` varchar(255) not null,
|
||||
`tags` varchar(1024) not null default '' comment 'merge data_tags rule_tags, split by ,,',
|
||||
@@ -415,6 +433,7 @@ CREATE TABLE `alert_cur_event` (
|
||||
CREATE TABLE `alert_his_event` (
|
||||
`id` bigint unsigned not null AUTO_INCREMENT,
|
||||
`is_recovered` tinyint(1) not null,
|
||||
`cate` varchar(128) not null,
|
||||
`cluster` varchar(128) not null,
|
||||
`group_id` bigint unsigned not null comment 'busi group id of rule',
|
||||
`group_name` varchar(255) not null default '' comment 'busi group name',
|
||||
@@ -436,6 +455,7 @@ CREATE TABLE `alert_his_event` (
|
||||
`notify_cur_number` int not null default 0 comment '',
|
||||
`target_ident` varchar(191) not null default '' comment 'target ident, also in tags',
|
||||
`target_note` varchar(191) not null default '' comment 'target note',
|
||||
`first_trigger_time` bigint,
|
||||
`trigger_time` bigint not null,
|
||||
`trigger_value` varchar(255) not null,
|
||||
`recover_time` bigint not null default 0,
|
||||
@@ -498,3 +518,12 @@ CREATE TABLE `task_record`
|
||||
KEY (`create_at`, `group_id`),
|
||||
KEY (`create_by`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
CREATE TABLE `alerting_engines`
|
||||
(
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`instance` varchar(128) not null default '' comment 'instance identification, e.g. 10.9.0.9:9090',
|
||||
`cluster` varchar(128) not null default '' comment 'target reader cluster',
|
||||
`clock` bigint not null,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -43,9 +43,11 @@ CREATE INDEX user_group_update_at_idx ON user_group (update_at);
|
||||
insert into user_group(id, name, create_at, create_by, update_at, update_by) values(1, 'demo-root-group', date_part('epoch',current_timestamp)::int, 'root', date_part('epoch',current_timestamp)::int, 'root');
|
||||
|
||||
CREATE TABLE user_group_member (
|
||||
id bigserial,
|
||||
group_id bigint not null,
|
||||
user_id bigint not null
|
||||
) ;
|
||||
ALTER TABLE user_group_member ADD CONSTRAINT user_group_member_pk PRIMARY KEY (id);
|
||||
CREATE INDEX user_group_member_group_id_idx ON user_group_member (group_id);
|
||||
CREATE INDEX user_group_member_user_id_idx ON user_group_member (user_id);
|
||||
|
||||
@@ -54,7 +56,7 @@ insert into user_group_member(group_id, user_id) values(1, 1);
|
||||
CREATE TABLE configs (
|
||||
id bigserial,
|
||||
ckey varchar(191) not null,
|
||||
cval varchar(1024) not null default ''
|
||||
cval varchar(4096) not null default ''
|
||||
) ;
|
||||
ALTER TABLE configs ADD CONSTRAINT configs_pk PRIMARY KEY (id);
|
||||
ALTER TABLE configs ADD CONSTRAINT configs_un UNIQUE (ckey);
|
||||
@@ -72,9 +74,11 @@ insert into role(name, note) values('Standard', 'Ordinary user role');
|
||||
insert into role(name, note) values('Guest', 'Readonly user role');
|
||||
|
||||
CREATE TABLE role_operation(
|
||||
id bigserial,
|
||||
role_name varchar(128) not null,
|
||||
operation varchar(191) not null
|
||||
) ;
|
||||
ALTER TABLE role_operation ADD CONSTRAINT role_operation_pk PRIMARY KEY (id);
|
||||
CREATE INDEX role_operation_role_name_idx ON role_operation (role_name);
|
||||
CREATE INDEX role_operation_operation_idx ON role_operation (operation);
|
||||
|
||||
@@ -126,7 +130,36 @@ insert into role_operation(role_name, operation) values('Standard', '/job-tpls/d
|
||||
insert into role_operation(role_name, operation) values('Standard', '/job-tasks');
|
||||
insert into role_operation(role_name, operation) values('Standard', '/job-tasks/add');
|
||||
insert into role_operation(role_name, operation) values('Standard', '/job-tasks/put');
|
||||
insert into role_operation(role_name, operation) values('Standard', '/recording-rules');
|
||||
insert into role_operation(role_name, operation) values('Standard', '/recording-rules/add');
|
||||
insert into role_operation(role_name, operation) values('Standard', '/recording-rules/put');
|
||||
insert into role_operation(role_name, operation) values('Standard', '/recording-rules/del');
|
||||
|
||||
CREATE TABLE recording_rule (
|
||||
id bigserial NOT NULL,
|
||||
group_id bigint not null default 0,
|
||||
cluster varchar(128) not null,
|
||||
name varchar(255) not null,
|
||||
note varchar(255) not null,
|
||||
disabled smallint not null,
|
||||
prom_ql varchar(8192) not null,
|
||||
prom_eval_interval int not null,
|
||||
append_tags varchar(255) default '',
|
||||
create_at bigint default 0,
|
||||
create_by varchar(64) default '',
|
||||
update_at bigint default 0,
|
||||
update_by varchar(64) default '',
|
||||
CONSTRAINT recording_rule_pk PRIMARY KEY (id)
|
||||
) ;
|
||||
CREATE INDEX recording_rule_group_id_idx ON recording_rule (group_id);
|
||||
CREATE INDEX recording_rule_update_at_idx ON recording_rule (update_at);
|
||||
COMMENT ON COLUMN recording_rule.group_id IS 'group_id';
|
||||
COMMENT ON COLUMN recording_rule.name IS 'new metric name';
|
||||
COMMENT ON COLUMN recording_rule.note IS 'rule note';
|
||||
COMMENT ON COLUMN recording_rule.disabled IS '0:enabled 1:disabled';
|
||||
COMMENT ON COLUMN recording_rule.prom_ql IS 'promql';
|
||||
COMMENT ON COLUMN recording_rule.prom_eval_interval IS 'evaluate interval';
|
||||
COMMENT ON COLUMN recording_rule.append_tags IS 'split by space: service=n9e mod=api';
|
||||
|
||||
-- for alert_rule | collect_rule | mute | dashboard grouping
|
||||
CREATE TABLE busi_group (
|
||||
@@ -165,7 +198,9 @@ CREATE TABLE board (
|
||||
id bigserial not null ,
|
||||
group_id bigint not null default 0 ,
|
||||
name varchar(191) not null,
|
||||
ident varchar(200) not null default '',
|
||||
tags varchar(255) not null ,
|
||||
public smallint not null default 0,
|
||||
create_at bigint not null default 0,
|
||||
create_by varchar(64) not null default '',
|
||||
update_at bigint not null default 0,
|
||||
@@ -175,6 +210,8 @@ ALTER TABLE board ADD CONSTRAINT board_pk PRIMARY KEY (id);
|
||||
ALTER TABLE board ADD CONSTRAINT board_un UNIQUE (group_id,"name");
|
||||
COMMENT ON COLUMN board.group_id IS 'busi group id';
|
||||
COMMENT ON COLUMN board.tags IS 'split by space';
|
||||
COMMENT ON COLUMN board.public IS '0:false 1:true';
|
||||
CREATE INDEX board_ident_idx ON board (ident);
|
||||
|
||||
-- for dashboard new version
|
||||
CREATE TABLE board_payload (
|
||||
@@ -230,6 +267,7 @@ CREATE INDEX chart_share_create_at_idx ON chart_share (create_at);
|
||||
CREATE TABLE alert_rule (
|
||||
id bigserial NOT NULL,
|
||||
group_id int8 NOT NULL DEFAULT 0,
|
||||
cate varchar(128) not null default '' ,
|
||||
"cluster" varchar(128) NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
note varchar(1024) NOT NULL,
|
||||
@@ -285,14 +323,19 @@ COMMENT ON COLUMN alert_rule.append_tags IS 'split by space: service=n9e mod=api
|
||||
CREATE TABLE alert_mute (
|
||||
id bigserial,
|
||||
group_id bigint not null default 0 ,
|
||||
cate varchar(128) not null default '' ,
|
||||
prod varchar(255) NOT NULL DEFAULT '' ,
|
||||
note varchar(1024) not null default '',
|
||||
cluster varchar(128) not null,
|
||||
tags varchar(4096) not null default '' ,
|
||||
cause varchar(255) not null default '',
|
||||
btime bigint not null default 0 ,
|
||||
etime bigint not null default 0 ,
|
||||
disabled smallint not null default 0 ,
|
||||
create_at bigint not null default 0,
|
||||
create_by varchar(64) not null default ''
|
||||
create_by varchar(64) not null default '',
|
||||
update_at bigint not null default 0,
|
||||
update_by varchar(64) not null default ''
|
||||
) ;
|
||||
ALTER TABLE alert_mute ADD CONSTRAINT alert_mute_pk PRIMARY KEY (id);
|
||||
CREATE INDEX alert_mute_create_at_idx ON alert_mute (create_at);
|
||||
@@ -301,10 +344,14 @@ COMMENT ON COLUMN alert_mute.group_id IS 'busi group id';
|
||||
COMMENT ON COLUMN alert_mute.tags IS 'json,map,tagkey->regexp|value';
|
||||
COMMENT ON COLUMN alert_mute.btime IS 'begin time';
|
||||
COMMENT ON COLUMN alert_mute.etime IS 'end time';
|
||||
COMMENT ON COLUMN alert_mute.disabled IS '0:enabled 1:disabled';
|
||||
|
||||
CREATE TABLE alert_subscribe (
|
||||
id bigserial,
|
||||
"name" varchar(255) NOT NULL default '',
|
||||
disabled int2 NOT NULL default 0 ,
|
||||
group_id bigint not null default 0 ,
|
||||
cate varchar(128) not null default '' ,
|
||||
cluster varchar(128) not null,
|
||||
rule_id bigint not null default 0,
|
||||
tags jsonb not null ,
|
||||
@@ -321,6 +368,7 @@ CREATE TABLE alert_subscribe (
|
||||
ALTER TABLE alert_subscribe ADD CONSTRAINT alert_subscribe_pk PRIMARY KEY (id);
|
||||
CREATE INDEX alert_subscribe_group_id_idx ON alert_subscribe (group_id);
|
||||
CREATE INDEX alert_subscribe_update_at_idx ON alert_subscribe (update_at);
|
||||
COMMENT ON COLUMN alert_subscribe.disabled IS '0:enabled 1:disabled';
|
||||
COMMENT ON COLUMN alert_subscribe.group_id IS 'busi group id';
|
||||
COMMENT ON COLUMN alert_subscribe.tags IS 'json,map,tagkey->regexp|value';
|
||||
COMMENT ON COLUMN alert_subscribe.redefine_severity IS 'is redefine severity?';
|
||||
@@ -387,6 +435,7 @@ insert into alert_aggr_view(name, rule, cate) values('By RuleName', 'field:rule_
|
||||
|
||||
CREATE TABLE alert_cur_event (
|
||||
id bigserial NOT NULL,
|
||||
cate varchar(128) not null default '' ,
|
||||
"cluster" varchar(128) NOT NULL,
|
||||
group_id int8 NOT NULL,
|
||||
group_name varchar(255) NOT NULL DEFAULT ''::character varying,
|
||||
@@ -407,6 +456,7 @@ CREATE TABLE alert_cur_event (
|
||||
notify_cur_number int4 not null default 0,
|
||||
target_ident varchar(191) NOT NULL DEFAULT ''::character varying,
|
||||
target_note varchar(191) NOT NULL DEFAULT ''::character varying,
|
||||
first_trigger_time int8,
|
||||
trigger_time int8 NOT NULL,
|
||||
trigger_value varchar(255) NOT NULL,
|
||||
tags varchar(1024) NOT NULL DEFAULT ''::character varying,
|
||||
@@ -439,6 +489,7 @@ COMMENT ON COLUMN alert_cur_event.tags IS 'merge data_tags rule_tags, split by ,
|
||||
CREATE TABLE alert_his_event (
|
||||
id bigserial NOT NULL,
|
||||
is_recovered int2 NOT NULL,
|
||||
cate varchar(128) not null default '' ,
|
||||
"cluster" varchar(128) NOT NULL,
|
||||
group_id int8 NOT NULL,
|
||||
group_name varchar(255) NOT NULL DEFAULT ''::character varying,
|
||||
@@ -458,6 +509,7 @@ CREATE TABLE alert_his_event (
|
||||
notify_cur_number int4 not null default 0,
|
||||
target_ident varchar(191) NOT NULL DEFAULT ''::character varying,
|
||||
target_note varchar(191) NOT NULL DEFAULT ''::character varying,
|
||||
first_trigger_time int8,
|
||||
trigger_time int8 NOT NULL,
|
||||
trigger_value varchar(255) NOT NULL,
|
||||
recover_time int8 NOT NULL DEFAULT 0,
|
||||
@@ -549,3 +601,14 @@ CREATE INDEX task_record_create_by_idx ON task_record (create_by);
|
||||
|
||||
COMMENT ON COLUMN task_record.id IS 'ibex task id';
|
||||
COMMENT ON COLUMN task_record.group_id IS 'busi group id';
|
||||
|
||||
CREATE TABLE alerting_engines
|
||||
(
|
||||
id bigserial NOT NULL,
|
||||
instance varchar(128) not null default '',
|
||||
cluster varchar(128) not null default '',
|
||||
clock bigint not null
|
||||
) ;
|
||||
ALTER TABLE alerting_engines ADD CONSTRAINT alerting_engines_pk PRIMARY KEY (id);
|
||||
COMMENT ON COLUMN alerting_engines.instance IS 'instance identification, e.g. 10.9.0.9:9090';
|
||||
COMMENT ON COLUMN alerting_engines.cluster IS 'target reader cluster';
|
||||
|
||||
@@ -23,6 +23,11 @@ class Sender(object):
|
||||
def send_feishu(cls, payload):
|
||||
# already done in go code
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send_mm(cls, payload):
|
||||
# already done in go code
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send_sms(cls, payload):
|
||||
|
||||
@@ -29,6 +29,7 @@ func (n *N9EPlugin) Notify(bs []byte) {
|
||||
"dingtalk_robot_token",
|
||||
"wecom_robot_token",
|
||||
"feishu_robot_token",
|
||||
"telegram_robot_token",
|
||||
}
|
||||
for _, ch := range channels {
|
||||
if ret := gjson.GetBytes(bs, ch); ret.Exists() {
|
||||
|
||||
@@ -9,7 +9,12 @@ ClusterName = "Default"
|
||||
BusiGroupLabelKey = "busigroup"
|
||||
|
||||
# sleep x seconds, then start judge engine
|
||||
EngineDelay = 120
|
||||
EngineDelay = 60
|
||||
|
||||
DisableUsageReport = true
|
||||
|
||||
# config | database
|
||||
ReaderFrom = "config"
|
||||
|
||||
[Log]
|
||||
# log write dir
|
||||
@@ -68,10 +73,12 @@ InsecureSkipVerify = true
|
||||
Batch = 5
|
||||
|
||||
[Alerting]
|
||||
# timeout settings, unit: ms, default: 30000ms
|
||||
Timeout=30000
|
||||
TemplatesDir = "./etc/template"
|
||||
NotifyConcurrency = 10
|
||||
# use builtin go code notify
|
||||
NotifyBuiltinChannels = ["email", "dingtalk", "wecom", "feishu"]
|
||||
NotifyBuiltinChannels = ["email", "dingtalk", "wecom", "feishu", "feishucard","mm", "telegram"]
|
||||
|
||||
[Alerting.CallScript]
|
||||
# built in sending capability in go code
|
||||
@@ -83,7 +90,8 @@ ScriptPath = "./etc/script/notify.py"
|
||||
Enable = false
|
||||
# use a plugin via `go build -buildmode=plugin -o notify.so`
|
||||
PluginPath = "./etc/script/notify.so"
|
||||
Caller = "n9eCaller"
|
||||
# The first letter must be capitalized to be exported
|
||||
Caller = "N9eCaller"
|
||||
|
||||
[Alerting.RedisPub]
|
||||
Enable = false
|
||||
@@ -101,7 +109,7 @@ Headers = ["Content-Type", "application/json", "X-From", "N9E"]
|
||||
[NoData]
|
||||
Metric = "target_up"
|
||||
# unit: second
|
||||
Interval = 15
|
||||
Interval = 120
|
||||
|
||||
[Ibex]
|
||||
# callback: ${ibex}/${tplid}/${host}
|
||||
@@ -136,7 +144,7 @@ MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
# enable auto migrate or not
|
||||
EnableAutoMigrate = false
|
||||
# EnableAutoMigrate = false
|
||||
|
||||
[Reader]
|
||||
# prometheus base url
|
||||
@@ -147,23 +155,18 @@ BasicAuthUser = ""
|
||||
BasicAuthPass = ""
|
||||
# timeout settings, unit: ms
|
||||
Timeout = 30000
|
||||
DialTimeout = 10000
|
||||
TLSHandshakeTimeout = 30000
|
||||
ExpectContinueTimeout = 1000
|
||||
IdleConnTimeout = 90000
|
||||
# time duration, unit: ms
|
||||
KeepAlive = 30000
|
||||
MaxConnsPerHost = 0
|
||||
MaxIdleConns = 100
|
||||
MaxIdleConnsPerHost = 10
|
||||
DialTimeout = 3000
|
||||
MaxIdleConnsPerHost = 100
|
||||
|
||||
[WriterOpt]
|
||||
# queue channel count
|
||||
QueueCount = 1000
|
||||
# queue max size
|
||||
QueueMaxSize = 10000000
|
||||
QueueMaxSize = 1000000
|
||||
# once pop samples number from queue
|
||||
QueuePopSize = 2000
|
||||
# unit: ms
|
||||
SleepInterval = 50
|
||||
QueuePopSize = 1000
|
||||
# metric or ident
|
||||
ShardingKey = "ident"
|
||||
|
||||
[[Writers]]
|
||||
Url = "http://prometheus:9090/api/v1/write"
|
||||
@@ -172,8 +175,8 @@ BasicAuthUser = ""
|
||||
# Basic auth password
|
||||
BasicAuthPass = ""
|
||||
# timeout settings, unit: ms
|
||||
Timeout = 30000
|
||||
DialTimeout = 10000
|
||||
Timeout = 10000
|
||||
DialTimeout = 3000
|
||||
TLSHandshakeTimeout = 30000
|
||||
ExpectContinueTimeout = 1000
|
||||
IdleConnTimeout = 90000
|
||||
@@ -182,6 +185,12 @@ KeepAlive = 30000
|
||||
MaxConnsPerHost = 0
|
||||
MaxIdleConns = 100
|
||||
MaxIdleConnsPerHost = 100
|
||||
# [[Writers.WriteRelabels]]
|
||||
# Action = "replace"
|
||||
# SourceLabels = ["__address__"]
|
||||
# Regex = "([^:]+)(?::\\d+)?"
|
||||
# Replacement = "$1:80"
|
||||
# TargetLabel = "__address__"
|
||||
|
||||
# [[Writers]]
|
||||
# Url = "http://m3db:7201/api/v1/prom/remote/write"
|
||||
|
||||
15
docker/n9eetc/template/feishucard.tpl
Normal file
15
docker/n9eetc/template/feishucard.tpl
Normal file
@@ -0,0 +1,15 @@
|
||||
{{ if .IsRecovered }}
|
||||
**告警集群:** {{.Cluster}}
|
||||
**级别状态:** S{{.Severity}} Recovered
|
||||
**告警名称:** {{.RuleName}}
|
||||
**恢复时间:** {{timeformat .LastEvalTime}}
|
||||
**告警描述:** **服务已恢复**
|
||||
{{- else }}
|
||||
**告警集群:** {{.Cluster}}
|
||||
**级别状态:** S{{.Severity}} Triggered
|
||||
**告警名称:** {{.RuleName}}
|
||||
**触发时间:** {{timeformat .TriggerTime}}
|
||||
**发送时间:** {{timestamp}}
|
||||
**触发时值:** {{.TriggerValue}}
|
||||
{{if .RuleNote }}**告警描述:** **{{.RuleNote}}**{{end}}
|
||||
{{- end -}}
|
||||
7
docker/n9eetc/template/telegram.tpl
Normal file
7
docker/n9eetc/template/telegram.tpl
Normal file
@@ -0,0 +1,7 @@
|
||||
**级别状态**: {{if .IsRecovered}}<font color="info">S{{.Severity}} Recovered</font>{{else}}<font color="warning">S{{.Severity}} Triggered</font>{{end}}
|
||||
**规则标题**: {{.RuleName}}{{if .RuleNote}}
|
||||
**规则备注**: {{.RuleNote}}{{end}}
|
||||
**监控指标**: {{.TagsJSON}}
|
||||
{{if .IsRecovered}}**恢复时间**:{{timeformat .LastEvalTime}}{{else}}**触发时间**: {{timeformat .TriggerTime}}
|
||||
**触发时值**: {{.TriggerValue}}{{end}}
|
||||
**发送时间**: {{timestamp}}
|
||||
@@ -4,12 +4,21 @@ RunMode = "release"
|
||||
# # custom i18n dict config
|
||||
# I18N = "./etc/i18n.json"
|
||||
|
||||
# # custom i18n request header key
|
||||
# I18NHeaderKey = "X-Language"
|
||||
|
||||
# metrics descriptions
|
||||
MetricsYamlFile = "./etc/metrics.yaml"
|
||||
|
||||
BuiltinAlertsDir = "./etc/alerts"
|
||||
BuiltinDashboardsDir = "./etc/dashboards"
|
||||
|
||||
# config | api
|
||||
ClustersFrom = "config"
|
||||
|
||||
# using when ClustersFrom = "api"
|
||||
ClustersFromAPIs = []
|
||||
|
||||
[[NotifyChannels]]
|
||||
Label = "邮箱"
|
||||
# do not change Key
|
||||
@@ -30,6 +39,21 @@ Label = "飞书机器人"
|
||||
# do not change Key
|
||||
Key = "feishu"
|
||||
|
||||
[[NotifyChannels]]
|
||||
Label = "飞书机器人消息卡片"
|
||||
# do not change Key
|
||||
Key = "feishucard"
|
||||
|
||||
[[NotifyChannels]]
|
||||
Label = "mm bot"
|
||||
# do not change Key
|
||||
Key = "mm"
|
||||
|
||||
[[NotifyChannels]]
|
||||
Label = "telegram机器人"
|
||||
# do not change Key
|
||||
Key = "telegram"
|
||||
|
||||
[[ContactKeys]]
|
||||
Label = "Wecom Robot Token"
|
||||
# do not change Key
|
||||
@@ -45,6 +69,16 @@ Label = "Feishu Robot Token"
|
||||
# do not change Key
|
||||
Key = "feishu_robot_token"
|
||||
|
||||
[[ContactKeys]]
|
||||
Label = "MatterMost Webhook URL"
|
||||
# do not change Key
|
||||
Key = "mm_webhook_url"
|
||||
|
||||
[[ContactKeys]]
|
||||
Label = "Telegram Robot Token"
|
||||
# do not change Key
|
||||
Key = "telegram_robot_token"
|
||||
|
||||
[Log]
|
||||
# log write dir
|
||||
Dir = "logs"
|
||||
@@ -92,6 +126,13 @@ AccessExpired = 1500
|
||||
RefreshExpired = 10080
|
||||
RedisKeyPrefix = "/jwt/"
|
||||
|
||||
[ProxyAuth]
|
||||
# if proxy auth enabled, jwt auth is disabled
|
||||
Enable = false
|
||||
# username key in http proxy header
|
||||
HeaderUserNameKey = "X-User-Name"
|
||||
DefaultRoles = ["Standard"]
|
||||
|
||||
[BasicAuth]
|
||||
user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
@@ -121,6 +162,20 @@ Nickname = "cn"
|
||||
Phone = "mobile"
|
||||
Email = "mail"
|
||||
|
||||
[OIDC]
|
||||
Enable = false
|
||||
RedirectURL = "http://n9e.com/callback"
|
||||
SsoAddr = "http://sso.example.org"
|
||||
ClientId = ""
|
||||
ClientSecret = ""
|
||||
CoverAttributes = true
|
||||
DefaultRoles = ["Standard"]
|
||||
|
||||
[OIDC.Attributes]
|
||||
Nickname = "nickname"
|
||||
Phone = "phone_number"
|
||||
Email = "email"
|
||||
|
||||
[Redis]
|
||||
# address, ip:port
|
||||
Address = "redis:6379"
|
||||
@@ -145,7 +200,7 @@ MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
# enable auto migrate or not
|
||||
EnableAutoMigrate = false
|
||||
# EnableAutoMigrate = false
|
||||
|
||||
[[Clusters]]
|
||||
# Prometheus cluster name
|
||||
@@ -158,14 +213,7 @@ BasicAuthUser = ""
|
||||
BasicAuthPass = ""
|
||||
# timeout settings, unit: ms
|
||||
Timeout = 30000
|
||||
DialTimeout = 10000
|
||||
TLSHandshakeTimeout = 30000
|
||||
ExpectContinueTimeout = 1000
|
||||
IdleConnTimeout = 90000
|
||||
# time duration, unit: ms
|
||||
KeepAlive = 30000
|
||||
MaxConnsPerHost = 0
|
||||
MaxIdleConns = 100
|
||||
DialTimeout = 3000
|
||||
MaxIdleConnsPerHost = 100
|
||||
|
||||
[Ibex]
|
||||
@@ -174,4 +222,10 @@ Address = "http://ibex:10090"
|
||||
BasicAuthUser = "ibex"
|
||||
BasicAuthPass = "ibex"
|
||||
# unit: ms
|
||||
Timeout = 3000
|
||||
Timeout = 3000
|
||||
|
||||
[TargetMetrics]
|
||||
TargetUp = '''max(max_over_time(target_up{ident=~"(%s)"}[%dm])) by (ident)'''
|
||||
LoadPerCore = '''max(max_over_time(system_load_norm_1{ident=~"(%s)"}[%dm])) by (ident)'''
|
||||
MemUtil = '''100-max(max_over_time(mem_available_percent{ident=~"(%s)"}[%dm])) by (ident)'''
|
||||
DiskUtil = '''max(max_over_time(disk_used_percent{ident=~"(%s)", path="/"}[%dm])) by (ident)'''
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
232
etc/dashboards/n9e_server.json
Normal file
232
etc/dashboards/n9e_server.json
Normal file
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"name": "夜莺大盘",
|
||||
"tags": "",
|
||||
"configs": {
|
||||
"var": [],
|
||||
"panels": [
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "rate(n9e_server_samples_received_total[1m])"
|
||||
}
|
||||
],
|
||||
"name": "每秒接收的数据点个数",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "none"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "off"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"i": "53fcb9dc-23f9-41e0-bc5e-121eed14c3a4",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "53fcb9dc-23f9-41e0-bc5e-121eed14c3a4"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "rate(n9e_server_alerts_total[10m])"
|
||||
}
|
||||
],
|
||||
"name": "每秒产生的告警事件个数",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "none"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "off"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
"i": "47fc6252-9cc8-4b53-8e27-0c5c59a47269",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "f70dcb8b-b58b-4ef9-9e48-f230d9e17140"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "n9e_server_alert_queue_size"
|
||||
}
|
||||
],
|
||||
"name": "告警事件内存队列长度",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "none"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "off"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 4,
|
||||
"i": "ad1af16c-de0c-45f4-8875-cea4e85d51d0",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "caf23e58-d907-42b0-9ed6-722c8c6f3c5f"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "n9e_server_http_request_duration_seconds_sum/n9e_server_http_request_duration_seconds_count"
|
||||
}
|
||||
],
|
||||
"name": "数据接收接口平均响应时间(单位:秒)",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "desc"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "noraml"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 4,
|
||||
"i": "64c3abc2-404c-4462-a82f-c109a21dac91",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "6b8d2db1-efca-4b9e-b429-57a9d2272bc5"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "n9e_server_sample_queue_size"
|
||||
}
|
||||
],
|
||||
"name": "内存数据队列长度",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "desc"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "off"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8,
|
||||
"i": "1c7da942-58c2-40dc-b42f-983e4a35b89b",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "bd41677d-40d3-482e-bb6e-fbd25df46d87"
|
||||
},
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"expr": "avg(n9e_server_forward_duration_seconds_sum/n9e_server_forward_duration_seconds_count)"
|
||||
}
|
||||
],
|
||||
"name": "数据发往TSDB平均耗时(单位:秒)",
|
||||
"options": {
|
||||
"tooltip": {
|
||||
"mode": "all",
|
||||
"sort": "desc"
|
||||
},
|
||||
"legend": {
|
||||
"displayMode": "hidden"
|
||||
},
|
||||
"standardOptions": {},
|
||||
"thresholds": {}
|
||||
},
|
||||
"custom": {
|
||||
"drawStyle": "lines",
|
||||
"lineInterpolation": "smooth",
|
||||
"fillOpacity": 0.5,
|
||||
"stack": "noraml"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"type": "timeseries",
|
||||
"layout": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8,
|
||||
"i": "eed94a0b-954f-48ac-82e5-a2eada1c8a3d",
|
||||
"isResizable": true
|
||||
},
|
||||
"id": "c8642e72-f384-46a5-8410-1e6be2953c3c"
|
||||
}
|
||||
],
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
2430
etc/dashboards/vmware_by_vsphere-monitor.json
Normal file
2430
etc/dashboards/vmware_by_vsphere-monitor.json
Normal file
File diff suppressed because it is too large
Load Diff
500
etc/metrics.yaml
500
etc/metrics.yaml
@@ -1,131 +1,387 @@
|
||||
cpu_usage_idle: CPU空闲率(单位:%)
|
||||
cpu_usage_active: CPU使用率(单位:%)
|
||||
cpu_usage_system: CPU内核态时间占比(单位:%)
|
||||
cpu_usage_user: CPU用户态时间占比(单位:%)
|
||||
cpu_usage_nice: 低优先级用户态CPU时间占比,也就是进程nice值被调整为1-19之间的CPU时间。这里注意,nice可取值范围是-20到19,数值越大,优先级反而越低(单位:%)
|
||||
cpu_usage_iowait: CPU等待I/O的时间占比(单位:%)
|
||||
cpu_usage_irq: CPU处理硬中断的时间占比(单位:%)
|
||||
cpu_usage_softirq: CPU处理软中断的时间占比(单位:%)
|
||||
cpu_usage_steal: 在虚拟机环境下有该指标,表示CPU被其他虚拟机争用的时间占比,超过20就表示争抢严重(单位:%)
|
||||
cpu_usage_guest: 通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的CPU时间占比(单位:%)
|
||||
cpu_usage_guest_nice: 以低优先级运行虚拟机的时间占比(单位:%)
|
||||
zh:
|
||||
ip_conntrack_count: 连接跟踪表条目总数(单位:int, count)
|
||||
ip_conntrack_max: 连接跟踪表最大容量(单位:int, size)
|
||||
cpu_usage_idle: CPU空闲率(单位:%)
|
||||
cpu_usage_active: CPU使用率(单位:%)
|
||||
cpu_usage_system: CPU内核态时间占比(单位:%)
|
||||
cpu_usage_user: CPU用户态时间占比(单位:%)
|
||||
cpu_usage_nice: 低优先级用户态CPU时间占比,也就是进程nice值被调整为1-19之间的CPU时间。这里注意,nice可取值范围是-20到19,数值越大,优先级反而越低(单位:%)
|
||||
cpu_usage_iowait: CPU等待I/O的时间占比(单位:%)
|
||||
cpu_usage_irq: CPU处理硬中断的时间占比(单位:%)
|
||||
cpu_usage_softirq: CPU处理软中断的时间占比(单位:%)
|
||||
cpu_usage_steal: 在虚拟机环境下有该指标,表示CPU被其他虚拟机争用的时间占比,超过20就表示争抢严重(单位:%)
|
||||
cpu_usage_guest: 通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的CPU时间占比(单位:%)
|
||||
cpu_usage_guest_nice: 以低优先级运行虚拟机的时间占比(单位:%)
|
||||
|
||||
disk_free: 硬盘分区剩余量(单位:byte)
|
||||
disk_used: 硬盘分区使用量(单位:byte)
|
||||
disk_used_percent: 硬盘分区使用率(单位:%)
|
||||
disk_total: 硬盘分区总量(单位:byte)
|
||||
disk_inodes_free: 硬盘分区inode剩余量
|
||||
disk_inodes_used: 硬盘分区inode使用量
|
||||
disk_inodes_total: 硬盘分区inode总量
|
||||
disk_free: 硬盘分区剩余量(单位:byte)
|
||||
disk_used: 硬盘分区使用量(单位:byte)
|
||||
disk_used_percent: 硬盘分区使用率(单位:%)
|
||||
disk_total: 硬盘分区总量(单位:byte)
|
||||
disk_inodes_free: 硬盘分区inode剩余量
|
||||
disk_inodes_used: 硬盘分区inode使用量
|
||||
disk_inodes_total: 硬盘分区inode总量
|
||||
|
||||
diskio_io_time: 从设备视角来看I/O请求总时间,队列中有I/O请求就计数(单位:毫秒),counter类型,需要用函数求rate才有使用价值
|
||||
diskio_iops_in_progress: 已经分配给设备驱动且尚未完成的IO请求,不包含在队列中但尚未分配给设备驱动的IO请求,gauge类型
|
||||
diskio_merged_reads: 相邻读请求merge读的次数,counter类型
|
||||
diskio_merged_writes: 相邻写请求merge写的次数,counter类型
|
||||
diskio_read_bytes: 读取的byte数量,counter类型,需要用函数求rate才有使用价值
|
||||
diskio_read_time: 读请求总时间(单位:毫秒),counter类型,需要用函数求rate才有使用价值
|
||||
diskio_reads: 读请求次数,counter类型,需要用函数求rate才有使用价值
|
||||
diskio_weighted_io_time: 从I/O请求视角来看I/O等待总时间,如果同时有多个I/O请求,时间会叠加(单位:毫秒)
|
||||
diskio_write_bytes: 写入的byte数量,counter类型,需要用函数求rate才有使用价值
|
||||
diskio_write_time: 写请求总时间(单位:毫秒),counter类型,需要用函数求rate才有使用价值
|
||||
diskio_writes: 写请求次数,counter类型,需要用函数求rate才有使用价值
|
||||
diskio_io_time: 从设备视角来看I/O请求总时间,队列中有I/O请求就计数(单位:毫秒),counter类型,需要用函数求rate才有使用价值
|
||||
diskio_iops_in_progress: 已经分配给设备驱动且尚未完成的IO请求,不包含在队列中但尚未分配给设备驱动的IO请求,gauge类型
|
||||
diskio_merged_reads: 相邻读请求merge读的次数,counter类型
|
||||
diskio_merged_writes: 相邻写请求merge写的次数,counter类型
|
||||
diskio_read_bytes: 读取的byte数量,counter类型,需要用函数求rate才有使用价值
|
||||
diskio_read_time: 读请求总时间(单位:毫秒),counter类型,需要用函数求rate才有使用价值
|
||||
diskio_reads: 读请求次数,counter类型,需要用函数求rate才有使用价值
|
||||
diskio_weighted_io_time: 从I/O请求视角来看I/O等待总时间,如果同时有多个I/O请求,时间会叠加(单位:毫秒)
|
||||
diskio_write_bytes: 写入的byte数量,counter类型,需要用函数求rate才有使用价值
|
||||
diskio_write_time: 写请求总时间(单位:毫秒),counter类型,需要用函数求rate才有使用价值
|
||||
diskio_writes: 写请求次数,counter类型,需要用函数求rate才有使用价值
|
||||
|
||||
kernel_boot_time: 内核启动时间
|
||||
kernel_context_switches: 内核上下文切换次数
|
||||
kernel_entropy_avail: linux系统内部的熵池
|
||||
kernel_interrupts: 内核中断次数
|
||||
kernel_processes_forked: fork的进程数
|
||||
kernel_boot_time: 内核启动时间
|
||||
kernel_context_switches: 内核上下文切换次数
|
||||
kernel_entropy_avail: linux系统内部的熵池
|
||||
kernel_interrupts: 内核中断次数
|
||||
kernel_processes_forked: fork的进程数
|
||||
|
||||
mem_active: 活跃使用的内存总数(包括cache和buffer内存)
|
||||
mem_available: 应用程序可用内存数
|
||||
mem_available_percent: 内存剩余百分比(0~100)
|
||||
mem_buffered: 用来给文件做缓冲大小
|
||||
mem_cached: 被高速缓冲存储器(cache memory)用的内存的大小(等于 diskcache minus SwapCache )
|
||||
mem_commit_limit: 根据超额分配比率('vm.overcommit_ratio'),这是当前在系统上分配可用的内存总量,这个限制只是在模式2('vm.overcommit_memory')时启用
|
||||
mem_committed_as: 目前在系统上分配的内存量。是所有进程申请的内存的总和
|
||||
mem_dirty: 等待被写回到磁盘的内存大小
|
||||
mem_free: 空闲内存数
|
||||
mem_high_free: 未被使用的高位内存大小
|
||||
mem_high_total: 高位内存总大小(Highmem是指所有内存高于860MB的物理内存,Highmem区域供用户程序使用,或用于页面缓存。该区域不是直接映射到内核空间。内核必须使用不同的手法使用该段内存)
|
||||
mem_huge_page_size: 每个大页的大小
|
||||
mem_huge_pages_free: 池中尚未分配的 HugePages 数量
|
||||
mem_huge_pages_total: 预留HugePages的总个数
|
||||
mem_inactive: 空闲的内存数(包括free和avalible的内存)
|
||||
mem_low_free: 未被使用的低位大小
|
||||
mem_low_total: 低位内存总大小,低位可以达到高位内存一样的作用,而且它还能够被内核用来记录一些自己的数据结构
|
||||
mem_mapped: 设备和文件等映射的大小
|
||||
mem_page_tables: 管理内存分页页面的索引表的大小
|
||||
mem_shared: 多个进程共享的内存总额
|
||||
mem_slab: 内核数据结构缓存的大小,可以减少申请和释放内存带来的消耗
|
||||
mem_sreclaimable: 可收回Slab的大小
|
||||
mem_sunreclaim: 不可收回Slab的大小(SUnreclaim+SReclaimable=Slab)
|
||||
mem_swap_cached: 被高速缓冲存储器(cache memory)用的交换空间的大小,已经被交换出来的内存,但仍然被存放在swapfile中。用来在需要的时候很快的被替换而不需要再次打开I/O端口
|
||||
mem_swap_free: 未被使用交换空间的大小
|
||||
mem_swap_total: 交换空间的总大小
|
||||
mem_total: 内存总数
|
||||
mem_used: 已用内存数
|
||||
mem_used_percent: 已用内存数百分比(0~100)
|
||||
mem_vmalloc_chunk: 最大的连续未被使用的vmalloc区域
|
||||
mem_vmalloc_totalL: 可以vmalloc虚拟内存大小
|
||||
mem_vmalloc_used: vmalloc已使用的虚拟内存大小
|
||||
mem_write_back: 正在被写回到磁盘的内存大小
|
||||
mem_write_back_tmp: FUSE用于临时写回缓冲区的内存
|
||||
mem_active: 活跃使用的内存总数(包括cache和buffer内存)
|
||||
mem_available: 应用程序可用内存数
|
||||
mem_available_percent: 内存剩余百分比(0~100)
|
||||
mem_buffered: 用来给文件做缓冲大小
|
||||
mem_cached: 被高速缓冲存储器(cache memory)用的内存的大小(等于 diskcache minus SwapCache )
|
||||
mem_commit_limit: 根据超额分配比率('vm.overcommit_ratio'),这是当前在系统上分配可用的内存总量,这个限制只是在模式2('vm.overcommit_memory')时启用
|
||||
mem_committed_as: 目前在系统上分配的内存量。是所有进程申请的内存的总和
|
||||
mem_dirty: 等待被写回到磁盘的内存大小
|
||||
mem_free: 空闲内存数
|
||||
mem_high_free: 未被使用的高位内存大小
|
||||
mem_high_total: 高位内存总大小(Highmem是指所有内存高于860MB的物理内存,Highmem区域供用户程序使用,或用于页面缓存。该区域不是直接映射到内核空间。内核必须使用不同的手法使用该段内存)
|
||||
mem_huge_page_size: 每个大页的大小
|
||||
mem_huge_pages_free: 池中尚未分配的 HugePages 数量
|
||||
mem_huge_pages_total: 预留HugePages的总个数
|
||||
mem_inactive: 空闲的内存数(包括free和avalible的内存)
|
||||
mem_low_free: 未被使用的低位大小
|
||||
mem_low_total: 低位内存总大小,低位可以达到高位内存一样的作用,而且它还能够被内核用来记录一些自己的数据结构
|
||||
mem_mapped: 设备和文件等映射的大小
|
||||
mem_page_tables: 管理内存分页页面的索引表的大小
|
||||
mem_shared: 多个进程共享的内存总额
|
||||
mem_slab: 内核数据结构缓存的大小,可以减少申请和释放内存带来的消耗
|
||||
mem_sreclaimable: 可收回Slab的大小
|
||||
mem_sunreclaim: 不可收回Slab的大小(SUnreclaim+SReclaimable=Slab)
|
||||
mem_swap_cached: 被高速缓冲存储器(cache memory)用的交换空间的大小,已经被交换出来的内存,但仍然被存放在swapfile中。用来在需要的时候很快的被替换而不需要再次打开I/O端口
|
||||
mem_swap_free: 未被使用交换空间的大小
|
||||
mem_swap_total: 交换空间的总大小
|
||||
mem_total: 内存总数
|
||||
mem_used: 已用内存数
|
||||
mem_used_percent: 已用内存数百分比(0~100)
|
||||
mem_vmalloc_chunk: 最大的连续未被使用的vmalloc区域
|
||||
mem_vmalloc_totalL: 可以vmalloc虚拟内存大小
|
||||
mem_vmalloc_used: vmalloc已使用的虚拟内存大小
|
||||
mem_write_back: 正在被写回到磁盘的内存大小
|
||||
mem_write_back_tmp: FUSE用于临时写回缓冲区的内存
|
||||
|
||||
net_bytes_recv: 网卡收包总数(bytes)
|
||||
net_bytes_sent: 网卡发包总数(bytes)
|
||||
net_drop_in: 网卡收丢包数量
|
||||
net_drop_out: 网卡发丢包数量
|
||||
net_err_in: 网卡收包错误数量
|
||||
net_err_out: 网卡发包错误数量
|
||||
net_packets_recv: 网卡收包数量
|
||||
net_packets_sent: 网卡发包数量
|
||||
net_bytes_recv: 网卡收包总数(bytes)
|
||||
net_bytes_sent: 网卡发包总数(bytes)
|
||||
net_drop_in: 网卡收丢包数量
|
||||
net_drop_out: 网卡发丢包数量
|
||||
net_err_in: 网卡收包错误数量
|
||||
net_err_out: 网卡发包错误数量
|
||||
net_packets_recv: 网卡收包数量
|
||||
net_packets_sent: 网卡发包数量
|
||||
|
||||
netstat_tcp_established: ESTABLISHED状态的网络链接数
|
||||
netstat_tcp_fin_wait1: FIN_WAIT1状态的网络链接数
|
||||
netstat_tcp_fin_wait2: FIN_WAIT2状态的网络链接数
|
||||
netstat_tcp_last_ack: LAST_ACK状态的网络链接数
|
||||
netstat_tcp_listen: LISTEN状态的网络链接数
|
||||
netstat_tcp_syn_recv: SYN_RECV状态的网络链接数
|
||||
netstat_tcp_syn_sent: SYN_SENT状态的网络链接数
|
||||
netstat_tcp_time_wait: TIME_WAIT状态的网络链接数
|
||||
netstat_udp_socket: UDP状态的网络链接数
|
||||
netstat_tcp_established: ESTABLISHED状态的网络链接数
|
||||
netstat_tcp_fin_wait1: FIN_WAIT1状态的网络链接数
|
||||
netstat_tcp_fin_wait2: FIN_WAIT2状态的网络链接数
|
||||
netstat_tcp_last_ack: LAST_ACK状态的网络链接数
|
||||
netstat_tcp_listen: LISTEN状态的网络链接数
|
||||
netstat_tcp_syn_recv: SYN_RECV状态的网络链接数
|
||||
netstat_tcp_syn_sent: SYN_SENT状态的网络链接数
|
||||
netstat_tcp_time_wait: TIME_WAIT状态的网络链接数
|
||||
netstat_udp_socket: UDP状态的网络链接数
|
||||
|
||||
processes_blocked: 不可中断的睡眠状态下的进程数('U','D','L')
|
||||
processes_dead: 回收中的进程数('X')
|
||||
processes_idle: 挂起的空闲进程数('I')
|
||||
processes_paging: 分页进程数('P')
|
||||
processes_running: 运行中的进程数('R')
|
||||
processes_sleeping: 可中断进程数('S')
|
||||
processes_stopped: 暂停状态进程数('T')
|
||||
processes_total: 总进程数
|
||||
processes_total_threads: 总线程数
|
||||
processes_unknown: 未知状态进程数
|
||||
processes_zombies: 僵尸态进程数('Z')
|
||||
#[ping]
|
||||
ping_percent_packet_loss: ping数据包丢失百分比(%)
|
||||
ping_result_code: ping返回码('0','1')
|
||||
|
||||
swap_used_percent: Swap空间换出数据量
|
||||
processes_blocked: 不可中断的睡眠状态下的进程数('U','D','L')
|
||||
processes_dead: 回收中的进程数('X')
|
||||
processes_idle: 挂起的空闲进程数('I')
|
||||
processes_paging: 分页进程数('P')
|
||||
processes_running: 运行中的进程数('R')
|
||||
processes_sleeping: 可中断进程数('S')
|
||||
processes_stopped: 暂停状态进程数('T')
|
||||
processes_total: 总进程数
|
||||
processes_total_threads: 总线程数
|
||||
processes_unknown: 未知状态进程数
|
||||
processes_zombies: 僵尸态进程数('Z')
|
||||
|
||||
system_load1: 1分钟平均load值
|
||||
system_load5: 5分钟平均load值
|
||||
system_load15: 15分钟平均load值
|
||||
system_n_users: 用户数
|
||||
system_n_cpus: CPU核数
|
||||
system_uptime: 系统启动时间
|
||||
swap_used_percent: Swap空间换出数据量
|
||||
|
||||
nginx_accepts: 自nginx启动起,与客户端建立过得连接总数
|
||||
nginx_active: 当前nginx正在处理的活动连接数,等于Reading/Writing/Waiting总和
|
||||
nginx_handled: 自nginx启动起,处理过的客户端连接总数
|
||||
nginx_reading: 正在读取HTTP请求头部的连接总数
|
||||
nginx_requests: 自nginx启动起,处理过的客户端请求总数,由于存在HTTP Krrp-Alive请求,该值会大于handled值
|
||||
nginx_upstream_check_fall: upstream_check模块检测到后端失败的次数
|
||||
nginx_upstream_check_rise: upstream_check模块对后端的检测次数
|
||||
nginx_upstream_check_status_code: 后端upstream的状态,up为1,down为0
|
||||
nginx_waiting: 开启 keep-alive 的情况下,这个值等于 active – (reading+writing), 意思就是 Nginx 已经处理完正在等候下一次请求指令的驻留连接
|
||||
nginx_writing: 正在向客户端发送响应的连接总数
|
||||
system_load1: 1分钟平均load值
|
||||
system_load5: 5分钟平均load值
|
||||
system_load15: 15分钟平均load值
|
||||
system_n_users: 用户数
|
||||
system_n_cpus: CPU核数
|
||||
system_uptime: 系统启动时间
|
||||
|
||||
http_response_content_length: HTTP消息实体的传输长度
|
||||
http_response_http_response_code: http响应状态码
|
||||
http_response_response_time: http响应用时
|
||||
http_response_result_code: url探测结果0为正常否则url无法访问
|
||||
nginx_accepts: 自nginx启动起,与客户端建立过得连接总数
|
||||
nginx_active: 当前nginx正在处理的活动连接数,等于Reading/Writing/Waiting总和
|
||||
nginx_handled: 自nginx启动起,处理过的客户端连接总数
|
||||
nginx_reading: 正在读取HTTP请求头部的连接总数
|
||||
nginx_requests: 自nginx启动起,处理过的客户端请求总数,由于存在HTTP Krrp-Alive请求,该值会大于handled值
|
||||
nginx_upstream_check_fall: upstream_check模块检测到后端失败的次数
|
||||
nginx_upstream_check_rise: upstream_check模块对后端的检测次数
|
||||
nginx_upstream_check_status_code: 后端upstream的状态,up为1,down为0
|
||||
nginx_waiting: 开启 keep-alive 的情况下,这个值等于 active – (reading+writing), 意思就是 Nginx 已经处理完正在等候下一次请求指令的驻留连接
|
||||
nginx_writing: 正在向客户端发送响应的连接总数
|
||||
|
||||
http_response_content_length: HTTP消息实体的传输长度
|
||||
http_response_http_response_code: http响应状态码
|
||||
http_response_response_time: http响应用时
|
||||
http_response_result_code: url探测结果0为正常否则url无法访问
|
||||
|
||||
# [aws cloudwatch rds]
|
||||
cloudwatch_aws_rds_bin_log_disk_usage_average: rds 磁盘使用平均值
|
||||
cloudwatch_aws_rds_bin_log_disk_usage_maximum: rds 磁盘使用量最大值
|
||||
cloudwatch_aws_rds_bin_log_disk_usage_minimum: rds binlog 磁盘使用量最低
|
||||
cloudwatch_aws_rds_bin_log_disk_usage_sample_count: rds binlog 磁盘使用情况样本计数
|
||||
cloudwatch_aws_rds_bin_log_disk_usage_sum: rds binlog 磁盘使用总和
|
||||
cloudwatch_aws_rds_burst_balance_average: rds 突发余额平均值
|
||||
cloudwatch_aws_rds_burst_balance_maximum: rds 突发余额最大值
|
||||
cloudwatch_aws_rds_burst_balance_minimum: rds 突发余额最低
|
||||
cloudwatch_aws_rds_burst_balance_sample_count: rds 突发平衡样本计数
|
||||
cloudwatch_aws_rds_burst_balance_sum: rds 突发余额总和
|
||||
cloudwatch_aws_rds_cpu_utilization_average: rds cpu 利用率平均值
|
||||
cloudwatch_aws_rds_cpu_utilization_maximum: rds cpu 利用率最大值
|
||||
cloudwatch_aws_rds_cpu_utilization_minimum: rds cpu 利用率最低
|
||||
cloudwatch_aws_rds_cpu_utilization_sample_count: rds cpu 利用率样本计数
|
||||
cloudwatch_aws_rds_cpu_utilization_sum: rds cpu 利用率总和
|
||||
cloudwatch_aws_rds_database_connections_average: rds 数据库连接平均值
|
||||
cloudwatch_aws_rds_database_connections_maximum: rds 数据库连接数最大值
|
||||
cloudwatch_aws_rds_database_connections_minimum: rds 数据库连接最小
|
||||
cloudwatch_aws_rds_database_connections_sample_count: rds 数据库连接样本数
|
||||
cloudwatch_aws_rds_database_connections_sum: rds 数据库连接总和
|
||||
cloudwatch_aws_rds_db_load_average: rds db 平均负载
|
||||
cloudwatch_aws_rds_db_load_cpu_average: rds db 负载 cpu 平均值
|
||||
cloudwatch_aws_rds_db_load_cpu_maximum: rds db 负载 cpu 最大值
|
||||
cloudwatch_aws_rds_db_load_cpu_minimum: rds db 负载 cpu 最小值
|
||||
cloudwatch_aws_rds_db_load_cpu_sample_count: rds db 加载 CPU 样本数
|
||||
cloudwatch_aws_rds_db_load_cpu_sum: rds db 加载cpu总和
|
||||
cloudwatch_aws_rds_db_load_maximum: rds 数据库负载最大值
|
||||
cloudwatch_aws_rds_db_load_minimum: rds 数据库负载最小值
|
||||
cloudwatch_aws_rds_db_load_non_cpu_average: rds 加载非 CPU 平均值
|
||||
cloudwatch_aws_rds_db_load_non_cpu_maximum: rds 加载非 cpu 最大值
|
||||
cloudwatch_aws_rds_db_load_non_cpu_minimum: rds 加载非 cpu 最小值
|
||||
cloudwatch_aws_rds_db_load_non_cpu_sample_count: rds 加载非 cpu 样本计数
|
||||
cloudwatch_aws_rds_db_load_non_cpu_sum: rds 加载非cpu总和
|
||||
cloudwatch_aws_rds_db_load_sample_count: rds db 加载样本计数
|
||||
cloudwatch_aws_rds_db_load_sum: rds db 负载总和
|
||||
cloudwatch_aws_rds_disk_queue_depth_average: rds 磁盘队列深度平均值
|
||||
cloudwatch_aws_rds_disk_queue_depth_maximum: rds 磁盘队列深度最大值
|
||||
cloudwatch_aws_rds_disk_queue_depth_minimum: rds 磁盘队列深度最小值
|
||||
cloudwatch_aws_rds_disk_queue_depth_sample_count: rds 磁盘队列深度样本计数
|
||||
cloudwatch_aws_rds_disk_queue_depth_sum: rds 磁盘队列深度总和
|
||||
cloudwatch_aws_rds_ebs_byte_balance__average: rds ebs 字节余额平均值
|
||||
cloudwatch_aws_rds_ebs_byte_balance__maximum: rds ebs 字节余额最大值
|
||||
cloudwatch_aws_rds_ebs_byte_balance__minimum: rds ebs 字节余额最低
|
||||
cloudwatch_aws_rds_ebs_byte_balance__sample_count: rds ebs 字节余额样本数
|
||||
cloudwatch_aws_rds_ebs_byte_balance__sum: rds ebs 字节余额总和
|
||||
cloudwatch_aws_rds_ebsio_balance__average: rds ebsio 余额平均值
|
||||
cloudwatch_aws_rds_ebsio_balance__maximum: rds ebsio 余额最大值
|
||||
cloudwatch_aws_rds_ebsio_balance__minimum: rds ebsio 余额最低
|
||||
cloudwatch_aws_rds_ebsio_balance__sample_count: rds ebsio 平衡样本计数
|
||||
cloudwatch_aws_rds_ebsio_balance__sum: rds ebsio 余额总和
|
||||
cloudwatch_aws_rds_free_storage_space_average: rds 免费存储空间平均
|
||||
cloudwatch_aws_rds_free_storage_space_maximum: rds 最大可用存储空间
|
||||
cloudwatch_aws_rds_free_storage_space_minimum: rds 最低可用存储空间
|
||||
cloudwatch_aws_rds_free_storage_space_sample_count: rds 可用存储空间样本数
|
||||
cloudwatch_aws_rds_free_storage_space_sum: rds 免费存储空间总和
|
||||
cloudwatch_aws_rds_freeable_memory_average: rds 可用内存平均值
|
||||
cloudwatch_aws_rds_freeable_memory_maximum: rds 最大可用内存
|
||||
cloudwatch_aws_rds_freeable_memory_minimum: rds 最小可用内存
|
||||
cloudwatch_aws_rds_freeable_memory_sample_count: rds 可释放内存样本数
|
||||
cloudwatch_aws_rds_freeable_memory_sum: rds 可释放内存总和
|
||||
cloudwatch_aws_rds_lvm_read_iops_average: rds lvm 读取 iops 平均值
|
||||
cloudwatch_aws_rds_lvm_read_iops_maximum: rds lvm 读取 iops 最大值
|
||||
cloudwatch_aws_rds_lvm_read_iops_minimum: rds lvm 读取 iops 最低
|
||||
cloudwatch_aws_rds_lvm_read_iops_sample_count: rds lvm 读取 iops 样本计数
|
||||
cloudwatch_aws_rds_lvm_read_iops_sum: rds lvm 读取 iops 总和
|
||||
cloudwatch_aws_rds_lvm_write_iops_average: rds lvm 写入 iops 平均值
|
||||
cloudwatch_aws_rds_lvm_write_iops_maximum: rds lvm 写入 iops 最大值
|
||||
cloudwatch_aws_rds_lvm_write_iops_minimum: rds lvm 写入 iops 最低
|
||||
cloudwatch_aws_rds_lvm_write_iops_sample_count: rds lvm 写入 iops 样本计数
|
||||
cloudwatch_aws_rds_lvm_write_iops_sum: rds lvm 写入 iops 总和
|
||||
cloudwatch_aws_rds_network_receive_throughput_average: rds 网络接收吞吐量平均
|
||||
cloudwatch_aws_rds_network_receive_throughput_maximum: rds 网络接收吞吐量最大值
|
||||
cloudwatch_aws_rds_network_receive_throughput_minimum: rds 网络接收吞吐量最小值
|
||||
cloudwatch_aws_rds_network_receive_throughput_sample_count: rds 网络接收吞吐量样本计数
|
||||
cloudwatch_aws_rds_network_receive_throughput_sum: rds 网络接收吞吐量总和
|
||||
cloudwatch_aws_rds_network_transmit_throughput_average: rds 网络传输吞吐量平均值
|
||||
cloudwatch_aws_rds_network_transmit_throughput_maximum: rds 网络传输吞吐量最大
|
||||
cloudwatch_aws_rds_network_transmit_throughput_minimum: rds 网络传输吞吐量最小值
|
||||
cloudwatch_aws_rds_network_transmit_throughput_sample_count: rds 网络传输吞吐量样本计数
|
||||
cloudwatch_aws_rds_network_transmit_throughput_sum: rds 网络传输吞吐量总和
|
||||
cloudwatch_aws_rds_read_iops_average: rds 读取 iops 平均值
|
||||
cloudwatch_aws_rds_read_iops_maximum: rds 最大读取 iops
|
||||
cloudwatch_aws_rds_read_iops_minimum: rds 读取 iops 最低
|
||||
cloudwatch_aws_rds_read_iops_sample_count: rds 读取 iops 样本计数
|
||||
cloudwatch_aws_rds_read_iops_sum: rds 读取 iops 总和
|
||||
cloudwatch_aws_rds_read_latency_average: rds 读取延迟平均值
|
||||
cloudwatch_aws_rds_read_latency_maximum: rds 读取延迟最大值
|
||||
cloudwatch_aws_rds_read_latency_minimum: rds 最小读取延迟
|
||||
cloudwatch_aws_rds_read_latency_sample_count: rds 读取延迟样本计数
|
||||
cloudwatch_aws_rds_read_latency_sum: rds 读取延迟总和
|
||||
cloudwatch_aws_rds_read_throughput_average: rds 读取吞吐量平均值
|
||||
cloudwatch_aws_rds_read_throughput_maximum: rds 最大读取吞吐量
|
||||
cloudwatch_aws_rds_read_throughput_minimum: rds 最小读取吞吐量
|
||||
cloudwatch_aws_rds_read_throughput_sample_count: rds 读取吞吐量样本计数
|
||||
cloudwatch_aws_rds_read_throughput_sum: rds 读取吞吐量总和
|
||||
cloudwatch_aws_rds_swap_usage_average: rds 交换使用平均值
|
||||
cloudwatch_aws_rds_swap_usage_maximum: rds 交换使用最大值
|
||||
cloudwatch_aws_rds_swap_usage_minimum: rds 交换使用量最低
|
||||
cloudwatch_aws_rds_swap_usage_sample_count: rds 交换使用示例计数
|
||||
cloudwatch_aws_rds_swap_usage_sum: rds 交换使用总和
|
||||
cloudwatch_aws_rds_write_iops_average: rds 写入 iops 平均值
|
||||
cloudwatch_aws_rds_write_iops_maximum: rds 写入 iops 最大值
|
||||
cloudwatch_aws_rds_write_iops_minimum: rds 写入 iops 最低
|
||||
cloudwatch_aws_rds_write_iops_sample_count: rds 写入 iops 样本计数
|
||||
cloudwatch_aws_rds_write_iops_sum: rds 写入 iops 总和
|
||||
cloudwatch_aws_rds_write_latency_average: rds 写入延迟平均值
|
||||
cloudwatch_aws_rds_write_latency_maximum: rds 最大写入延迟
|
||||
cloudwatch_aws_rds_write_latency_minimum: rds 写入延迟最小值
|
||||
cloudwatch_aws_rds_write_latency_sample_count: rds 写入延迟样本计数
|
||||
cloudwatch_aws_rds_write_latency_sum: rds 写入延迟总和
|
||||
cloudwatch_aws_rds_write_throughput_average: rds 写入吞吐量平均值
|
||||
cloudwatch_aws_rds_write_throughput_maximum: rds 最大写入吞吐量
|
||||
cloudwatch_aws_rds_write_throughput_minimum: rds 写入吞吐量最小值
|
||||
cloudwatch_aws_rds_write_throughput_sample_count: rds 写入吞吐量样本计数
|
||||
cloudwatch_aws_rds_write_throughput_sum: rds 写入吞吐量总和
|
||||
|
||||
en:
|
||||
ip_conntrack_count: the number of entries in the conntrack table(unit:int, count)
|
||||
ip_conntrack_max: the max capacity of the conntrack table(unit:int, size)
|
||||
cpu_usage_idle: "CPU idle rate(unit:%)"
|
||||
cpu_usage_active: "CPU usage rate(unit:%)"
|
||||
cpu_usage_system: "CPU kernel state time proportion(unit:%)"
|
||||
cpu_usage_user: "CPU user attitude time proportion(unit:%)"
|
||||
cpu_usage_nice: "The proportion of low priority CPU time, that is, the process NICE value is adjusted to the CPU time between 1-19. Note here that the value range of NICE is -20 to 19, the larger the value, the lower the priority, the lower the priority(unit:%)"
|
||||
cpu_usage_iowait: "CPU waiting for I/O time proportion(unit:%)"
|
||||
cpu_usage_irq: "CPU processing hard interrupt time proportion(unit:%)"
|
||||
cpu_usage_softirq: "CPU processing soft interrupt time proportion(unit:%)"
|
||||
cpu_usage_steal: "In the virtual machine environment, there is this indicator, which means that the CPU is used by other virtual machines for the proportion of time.(unit:%)"
|
||||
cpu_usage_guest: "The time to run other operating systems by virtualization, that is, the proportion of CPU time running the virtual machine(unit:%)"
|
||||
cpu_usage_guest_nice: "The proportion of time to run the virtual machine at low priority(unit:%)"
|
||||
|
||||
disk_free: "The remaining amount of the hard disk partition (unit: byte)"
|
||||
disk_used: "Hard disk partitional use (unit: byte)"
|
||||
disk_used_percent: "Hard disk partitional use rate (unit:%)"
|
||||
disk_total: "Total amount of hard disk partition (unit: byte)"
|
||||
disk_inodes_free: "Hard disk partition INODE remaining amount"
|
||||
disk_inodes_used: "Hard disk partition INODE usage amount"
|
||||
disk_inodes_total: "The total amount of hard disk partition INODE"
|
||||
|
||||
diskio_io_time: "From the perspective of the device perspective, the total time of I/O request, the I/O request in the queue is count (unit: millisecond), the counter type, you need to use the function to find the value"
|
||||
diskio_iops_in_progress: "IO requests that have been assigned to device -driven and have not yet been completed, not included in the queue but not yet assigned to the device -driven IO request, Gauge type"
|
||||
diskio_merged_reads: "The number of times of adjacent reading request Merge, the counter type"
|
||||
diskio_merged_writes: "The number of times the request Merge writes, the counter type"
|
||||
diskio_read_bytes: "The number of byte reads, the counter type, you need to use the function to find the Rate to use the value"
|
||||
diskio_read_time: "The total time of reading request (unit: millisecond), the counter type, you need to use the function to find the Rate to have the value of use"
|
||||
diskio_reads: "Read the number of requests, the counter type, you need to use the function to find the Rate to use the value"
|
||||
diskio_weighted_io_time: "From the perspective of the I/O request perspective, I/O wait for the total time. If there are multiple I/O requests at the same time, the time will be superimposed (unit: millisecond)"
|
||||
diskio_write_bytes: "The number of bytes written, the counter type, you need to use the function to find the Rate to use the value"
|
||||
diskio_write_time: "The total time of the request (unit: millisecond), the counter type, you need to use the function to find the rate to have the value of use"
|
||||
diskio_writes: "Write the number of requests, the counter type, you need to use the function to find the rate to use value"
|
||||
|
||||
kernel_boot_time: "Kernel startup time"
|
||||
kernel_context_switches: "Number of kernel context switching times"
|
||||
kernel_entropy_avail: "Entropy pool inside the Linux system"
|
||||
kernel_interrupts: "Number of kernel interruption"
|
||||
kernel_processes_forked: "ForK's process number"
|
||||
|
||||
mem_active: "The total number of memory (including Cache and BUFFER memory)"
|
||||
mem_available: "Application can use memory numbers"
|
||||
mem_available_percent: "Memory remaining percentage (0 ~ 100)"
|
||||
mem_buffered: "Used to make buffer size for the file"
|
||||
mem_cached: "The size of the memory used by the cache memory (equal to diskcache minus Swap Cache )"
|
||||
mem_commit_limit: "According to the over allocation ratio ('vm.overCommit _ Ratio'), this is the current total memory that can be allocated on the system."
|
||||
mem_committed_as: "Currently allocated on the system. It is the sum of the memory of all process applications"
|
||||
mem_dirty: "Waiting to be written back to the memory size of the disk"
|
||||
mem_free: "Senior memory number"
|
||||
mem_high_free: "Unused high memory size"
|
||||
mem_high_total: "The total memory size of the high memory (Highmem refers to all the physical memory that is higher than 860 MB of memory, the HighMem area is used for user programs, or for page cache. This area is not directly mapped to the kernel space. The kernels must use different methods to use this section of memory. )"
|
||||
mem_huge_page_size: "The size of each big page"
|
||||
mem_huge_pages_free: "The number of Huge Pages in the pool that have not been allocated"
|
||||
mem_huge_pages_total: "Reserve the total number of Huge Pages"
|
||||
mem_inactive: "Free memory (including the memory of free and avalible)"
|
||||
mem_low_free: "Unused low size"
|
||||
mem_low_total: "The total size of the low memory memory can achieve the same role of high memory, and it can be used by the kernel to record some of its own data structure"
|
||||
mem_mapped: "The size of the mapping of equipment and files"
|
||||
mem_page_tables: "The size of the index table of the management of the memory paging page"
|
||||
mem_shared: "The total memory shared by multiple processes"
|
||||
mem_slab: "The size of the kernel data structure cache can reduce the consumption of application and release memory"
|
||||
mem_sreclaimable: "The size of the SLAB can be recovered"
|
||||
mem_sunreclaim: "The size of the SLAB cannot be recovered(SUnreclaim+SReclaimable=Slab)"
|
||||
mem_swap_cached: "The size of the swap space used by the cache memory (cache memory), the memory that has been swapped out, but is still stored in the swapfile. Used to be quickly replaced when needed without opening the I/O port again"
|
||||
mem_swap_free: "The size of the switching space is not used"
|
||||
mem_swap_total: "The total size of the exchange space"
|
||||
mem_total: "Total memory"
|
||||
mem_used: "Memory number"
|
||||
mem_used_percent: "The memory has been used by several percentage (0 ~ 100)"
|
||||
mem_vmalloc_chunk: "The largest continuous unused vmalloc area"
|
||||
mem_vmalloc_totalL: "You can vmalloc virtual memory size"
|
||||
mem_vmalloc_used: "Vmalloc's virtual memory size"
|
||||
mem_write_back: "The memory size of the disk is being written back to the disk"
|
||||
mem_write_back_tmp: "Fuse is used to temporarily write back the memory of the buffer area"
|
||||
|
||||
net_bytes_recv: "The total number of packaging of the network card (bytes)"
|
||||
net_bytes_sent: "Total number of network cards (bytes)"
|
||||
net_drop_in: "The number of packets for network cards"
|
||||
net_drop_out: "The number of packets issued by the network card"
|
||||
net_err_in: "The number of incorrect packets of the network card"
|
||||
net_err_out: "Number of incorrect number of network cards"
|
||||
net_packets_recv: "Net card collection quantity"
|
||||
net_packets_sent: "Number of network card issuance"
|
||||
|
||||
netstat_tcp_established: "ESTABLISHED status network link number"
|
||||
netstat_tcp_fin_wait1: "FIN _ WAIT1 status network link number"
|
||||
netstat_tcp_fin_wait2: "FIN _ WAIT2 status number of network links"
|
||||
netstat_tcp_last_ack: "LAST_ ACK status number of network links"
|
||||
netstat_tcp_listen: "Number of network links in Listen status"
|
||||
netstat_tcp_syn_recv: "SYN _ RECV status number of network links"
|
||||
netstat_tcp_syn_sent: "SYN _ SENT status number of network links"
|
||||
netstat_tcp_time_wait: "Time _ WAIT status network link number"
|
||||
netstat_udp_socket: "Number of network links in UDP status"
|
||||
|
||||
processes_blocked: "The number of processes in the unreprudible sleep state('U','D','L')"
|
||||
processes_dead: "Number of processes in recycling('X')"
|
||||
processes_idle: "Number of idle processes hanging('I')"
|
||||
processes_paging: "Number of paging processes('P')"
|
||||
processes_running: "Number of processes during operation('R')"
|
||||
processes_sleeping: "Can interrupt the number of processes('S')"
|
||||
processes_stopped: "Pushing status process number('T')"
|
||||
processes_total: "Total process number"
|
||||
processes_total_threads: "Number of threads"
|
||||
processes_unknown: "Unknown status process number"
|
||||
processes_zombies: "Number of zombies('Z')"
|
||||
|
||||
swap_used_percent: "SWAP space replace the data volume"
|
||||
|
||||
system_load1: "1 minute average load value"
|
||||
system_load5: "5 minutes average load value"
|
||||
system_load15: "15 minutes average load value"
|
||||
system_n_users: "User number"
|
||||
system_n_cpus: "CPU nuclear number"
|
||||
system_uptime: "System startup time"
|
||||
|
||||
nginx_accepts: "Since Nginx started, the total number of connections has been established with the client"
|
||||
nginx_active: "The current number of activity connections that Nginx is being processed is equal to Reading/Writing/Waiting"
|
||||
nginx_handled: "Starting from Nginx, the total number of client connections that have been processed"
|
||||
nginx_reading: "Reading the total number of connections on the http request header"
|
||||
nginx_requests: "Since nginx is started, the total number of client requests processed, due to the existence of HTTP Krrp - Alive requests, this value will be greater than the handled value"
|
||||
nginx_upstream_check_fall: "UPStream_CHECK module detects the number of back -end failures"
|
||||
nginx_upstream_check_rise: "UPSTREAM _ Check module to detect the number of back -end"
|
||||
nginx_upstream_check_status_code: "The state of the backstream is 1, and the down is 0"
|
||||
nginx_waiting: "When keep-alive is enabled, this value is equal to active – (reading+writing), which means that Nginx has processed the resident connection that is waiting for the next request command"
|
||||
nginx_writing: "The total number of connections to send a response to the client"
|
||||
|
||||
http_response_content_length: "HTTP message entity transmission length"
|
||||
http_response_http_response_code: "http response status code"
|
||||
http_response_response_time: "When http ring application"
|
||||
http_response_result_code: "URL detection result 0 is normal, otherwise the URL cannot be accessed"
|
||||
|
||||
# [mysqld_exporter]
|
||||
mysql_global_status_uptime: The number of seconds that the server has been up.(Gauge)
|
||||
@@ -237,7 +493,7 @@ redis_last_key_groups_scrape_duration_milliseconds: Duration of the last key gro
|
||||
redis_last_slow_execution_duration_seconds: The amount of time needed for last slow execution, in seconds.
|
||||
redis_latest_fork_seconds: The amount of time needed for last fork, in seconds.
|
||||
redis_lazyfree_pending_objects: The number of objects waiting to be freed (as a result of calling UNLINK, or FLUSHDB and FLUSHALL with the ASYNC option).
|
||||
redis_master_repl_offset: The server's current replication offset.
|
||||
redis_master_repl_offset: The server's current replication offset.
|
||||
redis_mem_clients_normal: Memory used by normal clients.(Gauge)
|
||||
redis_mem_clients_slaves: Memory used by replica clients - Starting Redis 7.0, replica buffers share memory with the replication backlog, so this field can show 0 when replicas don't trigger an increase of memory usage.
|
||||
redis_mem_fragmentation_bytes: Delta between used_memory_rss and used_memory. Note that when the total fragmentation bytes is low (few megabytes), a high ratio (e.g. 1.5 and above) is not an indication of an issue.
|
||||
@@ -370,8 +626,6 @@ node_load15: cpu load 15m
|
||||
|
||||
# MEM
|
||||
# 内核态
|
||||
# 用户追踪已从交换区获取但尚未修改的页面的内存
|
||||
node_memory_SwapCached_bytes: Memory that keeps track of pages that have been fetched from swap but not yet been modified
|
||||
# 内核用于缓存数据结构供自己使用的内存
|
||||
node_memory_Slab_bytes: Memory used by the kernel to cache data structures for its own use
|
||||
# slab中可回收的部分
|
||||
@@ -433,7 +687,7 @@ node_memory_SwapTotal_bytes: Memory information field SwapTotal_bytes
|
||||
node_memory_SwapFree_bytes: Memory information field SwapFree_bytes
|
||||
|
||||
# DISK
|
||||
node_filesystem_files_free: Filesystem space available to non-root users in byte
|
||||
node_filesystem_avail_bytes: Filesystem space available to non-root users in byte
|
||||
node_filesystem_free_bytes: Filesystem free space in bytes
|
||||
node_filesystem_size_bytes: Filesystem size in bytes
|
||||
node_filesystem_files_free: Filesystem total free file nodes
|
||||
@@ -479,7 +733,7 @@ kafka_consumer_lag_millis: Current approximation of consumer lag for a ConsumerG
|
||||
kafka_topic_partition_under_replicated_partition: 1 if Topic/Partition is under Replicated
|
||||
|
||||
# [zookeeper_exporter]
|
||||
zk_znode_count: The total count of znodes stored
|
||||
zk_znode_count: The total count of znodes stored
|
||||
zk_ephemerals_count: The number of Ephemerals nodes
|
||||
zk_watch_count: The number of watchers setup over Zookeeper nodes.
|
||||
zk_approximate_data_size: Size of data in bytes that a zookeeper server has in its data tree
|
||||
@@ -491,4 +745,4 @@ zk_open_file_descriptor_count: Number of file descriptors that a zookeeper serve
|
||||
zk_max_file_descriptor_count: Maximum number of file descriptors that a zookeeper server can open
|
||||
zk_avg_latency: Average time in milliseconds for requests to be processed
|
||||
zk_min_latency: Maximum time in milliseconds for a request to be processed
|
||||
zk_max_latency: Minimum time in milliseconds for a request to be processed
|
||||
zk_max_latency: Minimum time in milliseconds for a request to be processed
|
||||
|
||||
@@ -24,6 +24,11 @@ class Sender(object):
|
||||
# already done in go code
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send_mm(cls, payload):
|
||||
# already done in go code
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send_sms(cls, payload):
|
||||
users = payload.get('event').get("notify_users_obj")
|
||||
|
||||
@@ -7,12 +7,6 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// the caller can be called for alerting notify by complete this interface
|
||||
type inter interface {
|
||||
Descript() string
|
||||
Notify([]byte)
|
||||
}
|
||||
|
||||
// N9E complete
|
||||
type N9EPlugin struct {
|
||||
Name string
|
||||
@@ -29,6 +23,7 @@ func (n *N9EPlugin) Notify(bs []byte) {
|
||||
"dingtalk_robot_token",
|
||||
"wecom_robot_token",
|
||||
"feishu_robot_token",
|
||||
"telegram_robot_token",
|
||||
}
|
||||
for _, ch := range channels {
|
||||
if ret := gjson.GetBytes(bs, ch); ret.Exists() {
|
||||
@@ -37,9 +32,16 @@ func (n *N9EPlugin) Notify(bs []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *N9EPlugin) NotifyMaintainer(bs []byte) {
|
||||
fmt.Println("do something... begin")
|
||||
result := string(bs)
|
||||
fmt.Println(result)
|
||||
fmt.Println("do something... end")
|
||||
}
|
||||
|
||||
// will be loaded for alertingCall , The first letter must be capitalized to be exported
|
||||
var N9eCaller = N9EPlugin{
|
||||
Name: "n9e",
|
||||
Description: "演示告警通过动态链接库方式通知",
|
||||
Name: "N9EPlugin",
|
||||
Description: "Notify by lib",
|
||||
BuildAt: time.Now().Local().Format("2006/01/02 15:04:05"),
|
||||
}
|
||||
|
||||
92
etc/script/notify_feishu.py
Executable file
92
etc/script/notify_feishu.py
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
|
||||
class Sender(object):
|
||||
@classmethod
|
||||
def send_email(cls, payload):
|
||||
# already done in go code
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send_wecom(cls, payload):
|
||||
# already done in go code
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send_dingtalk(cls, payload):
|
||||
# already done in go code
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send_ifeishu(cls, payload):
|
||||
users = payload.get('event').get("notify_users_obj")
|
||||
tokens = {}
|
||||
phones = {}
|
||||
|
||||
for u in users:
|
||||
if u.get("phone"):
|
||||
phones[u.get("phone")] = 1
|
||||
|
||||
contacts = u.get("contacts")
|
||||
if contacts.get("feishu_robot_token", ""):
|
||||
tokens[contacts.get("feishu_robot_token", "")] = 1
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json;charset=utf-8",
|
||||
"Host": "open.feishu.cn"
|
||||
}
|
||||
|
||||
for t in tokens:
|
||||
url = "https://open.feishu.cn/open-apis/bot/v2/hook/{}".format(t)
|
||||
body = {
|
||||
"msg_type": "text",
|
||||
"content": {
|
||||
"text": payload.get('tpls').get("feishu.tpl", "feishu.tpl not found")
|
||||
},
|
||||
"at": {
|
||||
"atMobiles": phones.keys(),
|
||||
"isAtAll": False
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, data=json.dumps(body))
|
||||
print(f"notify_feishu: token={t} status_code={response.status_code} response_text={response.text}")
|
||||
|
||||
@classmethod
|
||||
def send_mm(cls, payload):
|
||||
# already done in go code
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send_sms(cls, payload):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def send_voice(cls, payload):
|
||||
pass
|
||||
|
||||
def main():
|
||||
payload = json.load(sys.stdin)
|
||||
with open(".payload", 'w') as f:
|
||||
f.write(json.dumps(payload, indent=4))
|
||||
for ch in payload.get('event').get('notify_channels'):
|
||||
send_func_name = "send_{}".format(ch.strip())
|
||||
if not hasattr(Sender, send_func_name):
|
||||
print("function: {} not found", send_func_name)
|
||||
continue
|
||||
send_func = getattr(Sender, send_func_name)
|
||||
send_func(payload)
|
||||
|
||||
def hello():
|
||||
print("hello nightingale")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) == 1:
|
||||
main()
|
||||
elif sys.argv[1] == "hello":
|
||||
hello()
|
||||
else:
|
||||
print("I am confused")
|
||||
193
etc/script/rule_converter.py
Normal file
193
etc/script/rule_converter.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import json
|
||||
import yaml
|
||||
|
||||
'''
|
||||
将promtheus/vmalert的rule转换为n9e中的rule
|
||||
支持k8s的rule configmap
|
||||
'''
|
||||
|
||||
rule_file = 'rules.yaml'
|
||||
|
||||
|
||||
def convert_interval(interval):
|
||||
if interval.endswith('s') or interval.endswith('S'):
|
||||
return int(interval[:-1])
|
||||
if interval.endswith('m') or interval.endswith('M'):
|
||||
return int(interval[:-1]) * 60
|
||||
if interval.endswith('h') or interval.endswith('H'):
|
||||
return int(interval[:-1]) * 60 * 60
|
||||
if interval.endswith('d') or interval.endswith('D'):
|
||||
return int(interval[:-1]) * 60 * 60 * 24
|
||||
return int(interval)
|
||||
|
||||
|
||||
def convert_alert(rule, interval):
|
||||
name = rule['alert']
|
||||
prom_ql = rule['expr']
|
||||
if 'for' in rule:
|
||||
prom_for_duration = convert_interval(rule['for'])
|
||||
else:
|
||||
prom_for_duration = 0
|
||||
|
||||
prom_eval_interval = convert_interval(interval)
|
||||
note = ''
|
||||
if 'annotations' in rule:
|
||||
for v in rule['annotations'].values():
|
||||
note = v
|
||||
break
|
||||
|
||||
append_tags = []
|
||||
severity = 2
|
||||
if 'labels' in rule:
|
||||
for k, v in rule['labels'].items():
|
||||
if k != 'severity':
|
||||
append_tags.append('{}={}'.format(k, v))
|
||||
continue
|
||||
if v == 'critical':
|
||||
severity = 1
|
||||
elif v == 'info':
|
||||
severity = 3
|
||||
# elif v == 'warning':
|
||||
# severity = 2
|
||||
|
||||
|
||||
n9e_alert_rule = {
|
||||
"name": name,
|
||||
"note": note,
|
||||
"severity": severity,
|
||||
"disabled": 0,
|
||||
"prom_for_duration": prom_for_duration,
|
||||
"prom_ql": prom_ql,
|
||||
"prom_eval_interval": prom_eval_interval,
|
||||
"enable_stime": "00:00",
|
||||
"enable_etime": "23:59",
|
||||
"enable_days_of_week": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"0"
|
||||
],
|
||||
"enable_in_bg": 0,
|
||||
"notify_recovered": 1,
|
||||
"notify_channels": [],
|
||||
"notify_repeat_step": 60,
|
||||
"recover_duration": 0,
|
||||
"callbacks": [],
|
||||
"runbook_url": "",
|
||||
"append_tags": append_tags
|
||||
}
|
||||
return n9e_alert_rule
|
||||
|
||||
|
||||
def convert_record(rule, interval):
|
||||
name = rule['record']
|
||||
prom_ql = rule['expr']
|
||||
prom_eval_interval = convert_interval(interval)
|
||||
note = ''
|
||||
append_tags = []
|
||||
if 'labels' in rule:
|
||||
for k, v in rule['labels'].items():
|
||||
append_tags.append('{}={}'.format(k, v))
|
||||
|
||||
n9e_record_rule = {
|
||||
"name": name,
|
||||
"note": note,
|
||||
"disabled": 0,
|
||||
"prom_ql": prom_ql,
|
||||
"prom_eval_interval": prom_eval_interval,
|
||||
"append_tags": append_tags
|
||||
}
|
||||
return n9e_record_rule
|
||||
|
||||
|
||||
'''
|
||||
example of rule group file
|
||||
---
|
||||
groups:
|
||||
- name: example
|
||||
rules:
|
||||
- alert: HighRequestLatency
|
||||
expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5
|
||||
for: 10m
|
||||
labels:
|
||||
severity: page
|
||||
annotations:
|
||||
summary: High request latency
|
||||
'''
|
||||
def deal_group(group):
|
||||
"""
|
||||
parse single prometheus/vmalert rule group
|
||||
"""
|
||||
alert_rules = []
|
||||
record_rules = []
|
||||
|
||||
for rule_segment in group['groups']:
|
||||
if 'interval' in rule_segment:
|
||||
interval = rule_segment['interval']
|
||||
else:
|
||||
interval = '15s'
|
||||
for rule in rule_segment['rules']:
|
||||
if 'alert' in rule:
|
||||
alert_rules.append(convert_alert(rule, interval))
|
||||
else:
|
||||
record_rules.append(convert_record(rule, interval))
|
||||
|
||||
return alert_rules, record_rules
|
||||
|
||||
|
||||
'''
|
||||
example of k8s rule configmap
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: rulefiles-0
|
||||
data:
|
||||
etcdrules.yaml: |
|
||||
groups:
|
||||
- name: etcd
|
||||
rules:
|
||||
- alert: etcdInsufficientMembers
|
||||
annotations:
|
||||
message: 'etcd cluster "{{ $labels.job }}": insufficient members ({{ $value}}).'
|
||||
expr: sum(up{job=~".*etcd.*"} == bool 1) by (job) < ((count(up{job=~".*etcd.*"})
|
||||
by (job) + 1) / 2)
|
||||
for: 3m
|
||||
labels:
|
||||
severity: critical
|
||||
'''
|
||||
def deal_configmap(rule_configmap):
|
||||
"""
|
||||
parse rule configmap from k8s
|
||||
"""
|
||||
all_record_rules = []
|
||||
all_alert_rules = []
|
||||
for _, rule_group_str in rule_configmap['data'].items():
|
||||
rule_group = yaml.load(rule_group_str, Loader=yaml.FullLoader)
|
||||
alert_rules, record_rules = deal_group(rule_group)
|
||||
all_alert_rules.extend(alert_rules)
|
||||
all_record_rules.extend(record_rules)
|
||||
|
||||
return all_alert_rules, all_record_rules
|
||||
|
||||
|
||||
def main():
|
||||
with open(rule_file, 'r') as f:
|
||||
rule_config = yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
# 如果文件是k8s中的configmap,使用下面的方法
|
||||
# alert_rules, record_rules = deal_configmap(rule_config)
|
||||
alert_rules, record_rules = deal_group(rule_config)
|
||||
|
||||
with open("alert-rules.json", 'w') as fw:
|
||||
json.dump(alert_rules, fw, indent=2, ensure_ascii=False)
|
||||
|
||||
with open("record-rules.json", 'w') as fw:
|
||||
json.dump(record_rules, fw, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -9,9 +9,15 @@ ClusterName = "Default"
|
||||
BusiGroupLabelKey = "busigroup"
|
||||
|
||||
# sleep x seconds, then start judge engine
|
||||
EngineDelay = 120
|
||||
EngineDelay = 30
|
||||
|
||||
DisableUsageReport = false
|
||||
DisableUsageReport = true
|
||||
|
||||
# config | database
|
||||
ReaderFrom = "config"
|
||||
|
||||
# if true, target tags can rewrite labels defined in categraf config file
|
||||
LabelRewrite = false
|
||||
|
||||
[Log]
|
||||
# log write dir
|
||||
@@ -70,10 +76,12 @@ InsecureSkipVerify = true
|
||||
Batch = 5
|
||||
|
||||
[Alerting]
|
||||
# timeout settings, unit: ms, default: 30000ms
|
||||
Timeout=30000
|
||||
TemplatesDir = "./etc/template"
|
||||
NotifyConcurrency = 10
|
||||
# use builtin go code notify
|
||||
NotifyBuiltinChannels = ["email", "dingtalk", "wecom", "feishu"]
|
||||
NotifyBuiltinChannels = ["email", "dingtalk", "wecom", "feishu", "feishucard","mm", "telegram"]
|
||||
|
||||
[Alerting.CallScript]
|
||||
# built in sending capability in go code
|
||||
@@ -104,7 +112,7 @@ Headers = ["Content-Type", "application/json", "X-From", "N9E"]
|
||||
[NoData]
|
||||
Metric = "target_up"
|
||||
# unit: second
|
||||
Interval = 15
|
||||
Interval = 120
|
||||
|
||||
[Ibex]
|
||||
# callback: ${ibex}/${tplid}/${host}
|
||||
@@ -127,6 +135,8 @@ Address = "127.0.0.1:6379"
|
||||
RedisType = "standalone"
|
||||
# Mastername for sentinel type
|
||||
# MasterName = "mymaster"
|
||||
# SentinelUsername = ""
|
||||
# SentinelPassword = ""
|
||||
|
||||
[DB]
|
||||
# postgres: host=%s port=%s user=%s dbname=%s password=%s sslmode=%s
|
||||
@@ -155,24 +165,31 @@ BasicAuthUser = ""
|
||||
BasicAuthPass = ""
|
||||
# timeout settings, unit: ms
|
||||
Timeout = 30000
|
||||
DialTimeout = 10000
|
||||
TLSHandshakeTimeout = 30000
|
||||
ExpectContinueTimeout = 1000
|
||||
IdleConnTimeout = 90000
|
||||
# time duration, unit: ms
|
||||
KeepAlive = 30000
|
||||
MaxConnsPerHost = 0
|
||||
MaxIdleConns = 100
|
||||
MaxIdleConnsPerHost = 10
|
||||
DialTimeout = 3000
|
||||
MaxIdleConnsPerHost = 100
|
||||
|
||||
# [[Readers]]
|
||||
# ClusterName = "Default"
|
||||
# prometheus base url
|
||||
# Url = "http://127.0.0.1:9090"
|
||||
# Basic auth username
|
||||
# BasicAuthUser = ""
|
||||
# Basic auth password
|
||||
# BasicAuthPass = ""
|
||||
# timeout settings, unit: ms
|
||||
# Timeout = 30000
|
||||
# DialTimeout = 3000
|
||||
# MaxIdleConnsPerHost = 100
|
||||
|
||||
[WriterOpt]
|
||||
# queue channel count
|
||||
QueueCount = 100
|
||||
QueueCount = 1000
|
||||
# queue max size
|
||||
QueueMaxSize = 200000
|
||||
QueueMaxSize = 1000000
|
||||
# once pop samples number from queue
|
||||
QueuePopSize = 2000
|
||||
QueuePopSize = 1000
|
||||
# metric or ident
|
||||
ShardingKey = "ident"
|
||||
|
||||
[[Writers]]
|
||||
Url = "http://127.0.0.1:9090/api/v1/write"
|
||||
@@ -181,6 +198,7 @@ BasicAuthUser = ""
|
||||
# Basic auth password
|
||||
BasicAuthPass = ""
|
||||
# timeout settings, unit: ms
|
||||
Headers = ["X-From", "n9e"]
|
||||
Timeout = 10000
|
||||
DialTimeout = 3000
|
||||
TLSHandshakeTimeout = 30000
|
||||
@@ -191,6 +209,12 @@ KeepAlive = 30000
|
||||
MaxConnsPerHost = 0
|
||||
MaxIdleConns = 100
|
||||
MaxIdleConnsPerHost = 100
|
||||
# [[Writers.WriteRelabels]]
|
||||
# Action = "replace"
|
||||
# SourceLabels = ["__address__"]
|
||||
# Regex = "([^:]+)(?::\\d+)?"
|
||||
# Replacement = "$1:80"
|
||||
# TargetLabel = "__address__"
|
||||
|
||||
# [[Writers]]
|
||||
# Url = "http://127.0.0.1:7201/api/v1/prom/remote/write"
|
||||
|
||||
26
etc/template/README.md
Normal file
26
etc/template/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 告警消息模版文件
|
||||
|
||||
模版中可以使用的变量参考`AlertCurEvent`对象
|
||||
模版语法如何使用可以参考[html/template](https://pkg.go.dev/html/template)
|
||||
|
||||
## 如何在告警模版中添加监控详情url
|
||||
|
||||
假设web的地址是http://127.0.0.1:18000/, 实际使用时用web地址替换该地址
|
||||
|
||||
在监控模版中添加以下行:
|
||||
|
||||
* dingtalk / wecom / feishu
|
||||
```markdown
|
||||
[监控详情](http://127.0.0.1:18000/metric/explorer?promql={{ .PromQl | escape }})
|
||||
```
|
||||
|
||||
* mailbody
|
||||
|
||||
```html
|
||||
<tr>
|
||||
<th>监控详情:</th>
|
||||
<td>
|
||||
<a href="http://127.0.0.1:18000/metric/explorer?promql={{ .PromQl | escape }}" target="_blank">点击查看</a>
|
||||
</td>
|
||||
</tr>
|
||||
```
|
||||
15
etc/template/feishucard.tpl
Normal file
15
etc/template/feishucard.tpl
Normal file
@@ -0,0 +1,15 @@
|
||||
{{ if .IsRecovered }}
|
||||
**告警集群:** {{.Cluster}}
|
||||
**级别状态:** S{{.Severity}} Recovered
|
||||
**告警名称:** {{.RuleName}}
|
||||
**恢复时间:** {{timeformat .LastEvalTime}}
|
||||
**告警描述:** **服务已恢复**
|
||||
{{- else }}
|
||||
**告警集群:** {{.Cluster}}
|
||||
**级别状态:** S{{.Severity}} Triggered
|
||||
**告警名称:** {{.RuleName}}
|
||||
**触发时间:** {{timeformat .TriggerTime}}
|
||||
**发送时间:** {{timestamp}}
|
||||
**触发时值:** {{.TriggerValue}}
|
||||
{{if .RuleNote }}**告警描述:** **{{.RuleNote}}**{{end}}
|
||||
{{- end -}}
|
||||
7
etc/template/mm.tpl
Normal file
7
etc/template/mm.tpl
Normal file
@@ -0,0 +1,7 @@
|
||||
级别状态: S{{.Severity}} {{if .IsRecovered}}Recovered{{else}}Triggered{{end}}
|
||||
规则名称: {{.RuleName}}{{if .RuleNote}}
|
||||
规则备注: {{.RuleNote}}{{end}}
|
||||
监控指标: {{.TagsJSON}}
|
||||
{{if .IsRecovered}}恢复时间:{{timeformat .LastEvalTime}}{{else}}触发时间: {{timeformat .TriggerTime}}
|
||||
触发时值: {{.TriggerValue}}{{end}}
|
||||
发送时间: {{timestamp}}
|
||||
9
etc/template/telegram.tpl
Normal file
9
etc/template/telegram.tpl
Normal file
@@ -0,0 +1,9 @@
|
||||
**级别状态**: {{if .IsRecovered}}<font color="info">S{{.Severity}} Recovered</font>{{else}}<font color="warning">S{{.Severity}} Triggered</font>{{end}}
|
||||
**规则标题**: {{.RuleName}}{{if .RuleNote}}
|
||||
**规则备注**: {{.RuleNote}}{{end}}{{if .TargetIdent}}
|
||||
**监控对象**: {{.TargetIdent}}{{end}}
|
||||
**监控指标**: {{.TagsJSON}}{{if not .IsRecovered}}
|
||||
**触发时值**: {{.TriggerValue}}{{end}}
|
||||
{{if .IsRecovered}}**恢复时间**: {{timeformat .LastEvalTime}}{{else}}**首次触发时间**: {{timeformat .FirstTriggerTime}}{{end}}
|
||||
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}**持续时长**: {{humanizeDurationInterface $time_duration}}
|
||||
**发送时间**: {{timestamp}}
|
||||
@@ -1,7 +1,9 @@
|
||||
**级别状态**: {{if .IsRecovered}}<font color="info">S{{.Severity}} Recovered</font>{{else}}<font color="warning">S{{.Severity}} Triggered</font>{{end}}
|
||||
**规则标题**: {{.RuleName}}{{if .RuleNote}}
|
||||
**规则备注**: {{.RuleNote}}{{end}}
|
||||
**监控指标**: {{.TagsJSON}}
|
||||
{{if .IsRecovered}}**恢复时间**:{{timeformat .LastEvalTime}}{{else}}**触发时间**: {{timeformat .TriggerTime}}
|
||||
**规则备注**: {{.RuleNote}}{{end}}{{if .TargetIdent}}
|
||||
**监控对象**: {{.TargetIdent}}{{end}}
|
||||
**监控指标**: {{.TagsJSON}}{{if not .IsRecovered}}
|
||||
**触发时值**: {{.TriggerValue}}{{end}}
|
||||
{{if .IsRecovered}}**恢复时间**: {{timeformat .LastEvalTime}}{{else}}**首次触发时间**: {{timeformat .FirstTriggerTime}}{{end}}
|
||||
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}**持续时长**: {{humanizeDurationInterface $time_duration}}
|
||||
**发送时间**: {{timestamp}}
|
||||
@@ -4,6 +4,9 @@ RunMode = "release"
|
||||
# # custom i18n dict config
|
||||
# I18N = "./etc/i18n.json"
|
||||
|
||||
# # custom i18n request header key
|
||||
# I18NHeaderKey = "X-Language"
|
||||
|
||||
# metrics descriptions
|
||||
MetricsYamlFile = "./etc/metrics.yaml"
|
||||
|
||||
@@ -36,6 +39,21 @@ Label = "飞书机器人"
|
||||
# do not change Key
|
||||
Key = "feishu"
|
||||
|
||||
[[NotifyChannels]]
|
||||
Label = "飞书机器人消息卡片"
|
||||
# do not change Key
|
||||
Key = "feishucard"
|
||||
|
||||
[[NotifyChannels]]
|
||||
Label = "mm bot"
|
||||
# do not change Key
|
||||
Key = "mm"
|
||||
|
||||
[[NotifyChannels]]
|
||||
Label = "telegram机器人"
|
||||
# do not change Key
|
||||
Key = "telegram"
|
||||
|
||||
[[ContactKeys]]
|
||||
Label = "Wecom Robot Token"
|
||||
# do not change Key
|
||||
@@ -51,6 +69,16 @@ Label = "Feishu Robot Token"
|
||||
# do not change Key
|
||||
Key = "feishu_robot_token"
|
||||
|
||||
[[ContactKeys]]
|
||||
Label = "MatterMost Webhook URL"
|
||||
# do not change Key
|
||||
Key = "mm_webhook_url"
|
||||
|
||||
[[ContactKeys]]
|
||||
Label = "Telegram Robot Token"
|
||||
# do not change Key
|
||||
Key = "telegram_robot_token"
|
||||
|
||||
[Log]
|
||||
# log write dir
|
||||
Dir = "logs"
|
||||
@@ -98,6 +126,13 @@ AccessExpired = 1500
|
||||
RefreshExpired = 10080
|
||||
RedisKeyPrefix = "/jwt/"
|
||||
|
||||
[ProxyAuth]
|
||||
# if proxy auth enabled, jwt auth is disabled
|
||||
Enable = false
|
||||
# username key in http proxy header
|
||||
HeaderUserNameKey = "X-User-Name"
|
||||
DefaultRoles = ["Standard"]
|
||||
|
||||
[BasicAuth]
|
||||
user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
@@ -129,6 +164,7 @@ Email = "mail"
|
||||
|
||||
[OIDC]
|
||||
Enable = false
|
||||
DisplayName = "OIDC登录"
|
||||
RedirectURL = "http://n9e.com/callback"
|
||||
SsoAddr = "http://sso.example.org"
|
||||
ClientId = ""
|
||||
@@ -141,6 +177,54 @@ Nickname = "nickname"
|
||||
Phone = "phone_number"
|
||||
Email = "email"
|
||||
|
||||
[CAS]
|
||||
Enable = false
|
||||
DisplayName = "CAS登录"
|
||||
SsoAddr = "https://cas.example.com/cas/"
|
||||
RedirectURL = "http://127.0.0.1:18000/callback/cas"
|
||||
CoverAttributes = false
|
||||
# cas user default roles
|
||||
DefaultRoles = ["Standard"]
|
||||
|
||||
[CAS.Attributes]
|
||||
Nickname = "nickname"
|
||||
Phone = "phone_number"
|
||||
Email = "email"
|
||||
|
||||
[OAuth]
|
||||
Enable = false
|
||||
DisplayName = "OAuth2登录"
|
||||
RedirectURL = "http://127.0.0.1:18000/callback/oauth"
|
||||
SsoAddr = "https://sso.example.com/oauth2/authorize"
|
||||
TokenAddr = "https://sso.example.com/oauth2/token"
|
||||
UserInfoAddr = "https://api.example.com/api/v1/user/info"
|
||||
# "header" "querystring" "formdata"
|
||||
TranTokenMethod = "header"
|
||||
ClientId = ""
|
||||
ClientSecret = ""
|
||||
CoverAttributes = true
|
||||
DefaultRoles = ["Standard"]
|
||||
UserinfoIsArray = false
|
||||
UserinfoPrefix = "data"
|
||||
Scopes = ["profile", "email", "phone"]
|
||||
|
||||
[OAuth.Attributes]
|
||||
# Username must be defined
|
||||
Username = "username"
|
||||
Nickname = "nickname"
|
||||
Phone = "phone_number"
|
||||
Email = "email"
|
||||
|
||||
# example
|
||||
# # nested : UserinfoIsArray=false, UserinfoPrefix="data"
|
||||
# # {"data":{"username":"123456","nickname":"姓名"},"code":0,"message":"ok"}
|
||||
# # nested and array : UserinfoIsArray=true, UserinfoPrefix="data"
|
||||
# # {"data":[{"username":"123456","nickname":"姓名"}],"code":0,"message":"ok"}
|
||||
# # flat : UserinfoIsArray=false, UserinfoPrefix=""
|
||||
# # {"username":"123456","nickname":"姓名"}
|
||||
# # flat and array : UserinfoIsArray=true, UserinfoPrefix=""
|
||||
# # [{"username":"123456","nickname":"姓名"}]
|
||||
|
||||
[Redis]
|
||||
# address, ip:port or ip1:port,ip2:port for cluster and sentinel(SentinelAddrs)
|
||||
Address = "127.0.0.1:6379"
|
||||
@@ -153,6 +237,8 @@ Address = "127.0.0.1:6379"
|
||||
RedisType = "standalone"
|
||||
# Mastername for sentinel type
|
||||
# MasterName = "mymaster"
|
||||
# SentinelUsername = ""
|
||||
# SentinelPassword = ""
|
||||
|
||||
[DB]
|
||||
DSN="root:1234@tcp(127.0.0.1:3306)/n9e_v5?charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
|
||||
@@ -184,6 +270,7 @@ BasicAuthPass = ""
|
||||
Timeout = 30000
|
||||
DialTimeout = 3000
|
||||
MaxIdleConnsPerHost = 100
|
||||
Headers = ["X-From", "n9e"]
|
||||
|
||||
[Ibex]
|
||||
Address = "http://127.0.0.1:10090"
|
||||
@@ -191,4 +278,10 @@ Address = "http://127.0.0.1:10090"
|
||||
BasicAuthUser = "ibex"
|
||||
BasicAuthPass = "ibex"
|
||||
# unit: ms
|
||||
Timeout = 3000
|
||||
Timeout = 3000
|
||||
|
||||
[TargetMetrics]
|
||||
TargetUp = '''max(max_over_time(target_up{ident=~"(%s)"}[%dm])) by (ident)'''
|
||||
LoadPerCore = '''max(max_over_time(system_load_norm_1{ident=~"(%s)"}[%dm])) by (ident)'''
|
||||
MemUtil = '''100-max(max_over_time(mem_available_percent{ident=~"(%s)"}[%dm])) by (ident)'''
|
||||
DiskUtil = '''max(max_over_time(disk_used_percent{ident=~"(%s)", path="/"}[%dm])) by (ident)'''
|
||||
|
||||
8
fe.sh
Executable file
8
fe.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
TAG=$(curl -sX GET https://api.github.com/repos/n9e/fe-v5/releases/latest | awk '/tag_name/{print $4;exit}' FS='[""]')
|
||||
VERSION=$(echo $TAG | sed 's/v//g')
|
||||
|
||||
curl -o n9e-fe-${VERSION}.tar.gz -L https://github.com/n9e/fe-v5/releases/download/${TAG}/n9e-fe-${VERSION}.tar.gz
|
||||
|
||||
tar zxvf n9e-fe-${VERSION}.tar.gz
|
||||
28
go.mod
28
go.mod
@@ -6,9 +6,9 @@ require (
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/gin-contrib/pprof v1.3.0
|
||||
github.com/gin-gonic/gin v1.7.4
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/go-ldap/ldap/v3 v3.4.1
|
||||
github.com/go-redis/redis/v8 v8.11.3
|
||||
github.com/go-redis/redis/v9 v9.0.0-rc.1
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/golang/protobuf v1.5.2
|
||||
@@ -16,16 +16,17 @@ require (
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/prometheus/common v0.26.0
|
||||
github.com/prometheus/client_golang v1.12.2
|
||||
github.com/prometheus/common v0.32.1
|
||||
github.com/prometheus/prometheus v2.5.0+incompatible
|
||||
github.com/tidwall/gjson v1.14.0
|
||||
github.com/toolkits/pkg v1.2.9
|
||||
github.com/toolkits/pkg v1.3.2
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gorm.io/driver/mysql v1.1.2
|
||||
gorm.io/driver/postgres v1.1.1
|
||||
@@ -36,7 +37,7 @@ require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fatih/camelcase v1.0.0 // indirect
|
||||
@@ -58,13 +59,14 @@ require (
|
||||
github.com/jackc/pgx/v4 v4.13.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
@@ -72,11 +74,11 @@ require (
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
go.uber.org/automaxprocs v1.4.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/appengine v1.4.0 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211007155348-82e027067bd4 // indirect
|
||||
google.golang.org/grpc v1.41.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
|
||||
322
go.sum
322
go.sum
@@ -1,9 +1,41 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
@@ -19,8 +51,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -53,9 +89,7 @@ github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8
|
||||
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/garyburd/redigo v1.6.2/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
|
||||
@@ -63,10 +97,14 @@ github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwf
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
@@ -84,12 +122,11 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCxgt8=
|
||||
github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc=
|
||||
github.com/go-redis/redis/v9 v9.0.0-rc.1 h1:/+bS+yeUnanqAbuD3QwlejzQZ+4eqgfUtFTG4b+QnXs=
|
||||
github.com/go-redis/redis/v9 v9.0.0-rc.1/go.mod h1:8et+z03j0l8N+DvsVnclzjf3Dl/pFHgRk+2Ct1qw66A=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -98,11 +135,22 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
@@ -116,23 +164,39 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
@@ -182,6 +246,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -189,6 +255,8 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
@@ -212,6 +280,8 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
@@ -229,17 +299,9 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
|
||||
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/gomega v1.21.1 h1:OB/euWYIExnPBohllTicTHmGTrMaqJ67nIu80j0/uEM=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -253,8 +315,9 @@ github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQ
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -262,13 +325,15 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/prometheus v2.5.0+incompatible h1:7QPitgO2kOFG8ecuRn9O/4L9+10He72rVRJvMXrE9Hg=
|
||||
github.com/prometheus/prometheus v2.5.0+incompatible/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s=
|
||||
github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY=
|
||||
@@ -300,24 +365,31 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
|
||||
github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/toolkits/pkg v1.2.9 h1:zGlrJDl+2sMBoxBRIoMtAwvKmW5wctuji2+qHCecMKk=
|
||||
github.com/toolkits/pkg v1.2.9/go.mod h1:ZUsQAOoaR99PSbes+RXSirvwmtd6+XIUvizCmrjfUYc=
|
||||
github.com/toolkits/pkg v1.3.2 h1:elEW//SWOO956RQymAwcxBHGBKhrvCUAfXDo8wAkmJs=
|
||||
github.com/toolkits/pkg v1.3.2/go.mod h1:PvTBg/UxazPgBz6VaCM7FM7kJldjfVrsuN6k4HT/VuY=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
@@ -336,6 +408,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@@ -343,125 +416,274 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20211007155348-82e027067bd4 h1:YXPV/eKW0ZWRdB5tyI6aPoaa2Wxb4OSlFrTREMdwn64=
|
||||
google.golang.org/genproto v0.0.0-20211007155348-82e027067bd4/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
@@ -475,6 +697,7 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
@@ -488,14 +711,12 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -505,8 +726,8 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gorm.io/driver/mysql v1.1.2 h1:OofcyE2lga734MxwcCW9uB4mWNXMr50uaGRVwQL2B0M=
|
||||
gorm.io/driver/mysql v1.1.2/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM=
|
||||
gorm.io/driver/postgres v1.1.1 h1:tWLmqYCyaoh89fi7DhM6QggujrOnmfo3H98AzgNAAu0=
|
||||
@@ -515,5 +736,12 @@ gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
gorm.io/gorm v1.21.15 h1:gAyaDoPw0lCyrSFWhBlahbUA1U4P5RViC1uIqoB+1Rk=
|
||||
gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
16
src/main.go
16
src/main.go
@@ -34,6 +34,11 @@ func newWebapiCmd() *cli.Command {
|
||||
Aliases: []string{"c"},
|
||||
Usage: "specify configuration file(.json,.yaml,.toml)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "key",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "specify the secret key for configuration file field encryption",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
printEnv()
|
||||
@@ -43,6 +48,9 @@ func newWebapiCmd() *cli.Command {
|
||||
opts = append(opts, webapi.SetConfigFile(c.String("conf")))
|
||||
}
|
||||
opts = append(opts, webapi.SetVersion(version.VERSION))
|
||||
if c.String("key") != "" {
|
||||
opts = append(opts, webapi.SetKey(c.String("key")))
|
||||
}
|
||||
|
||||
webapi.Run(opts...)
|
||||
return nil
|
||||
@@ -60,6 +68,11 @@ func newServerCmd() *cli.Command {
|
||||
Aliases: []string{"c"},
|
||||
Usage: "specify configuration file(.json,.yaml,.toml)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "key",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "specify the secret key for configuration file field encryption",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
printEnv()
|
||||
@@ -69,6 +82,9 @@ func newServerCmd() *cli.Command {
|
||||
opts = append(opts, server.SetConfigFile(c.String("conf")))
|
||||
}
|
||||
opts = append(opts, server.SetVersion(version.VERSION))
|
||||
if c.String("key") != "" {
|
||||
opts = append(opts, server.SetKey(c.String("key")))
|
||||
}
|
||||
|
||||
server.Run(opts...)
|
||||
return nil
|
||||
|
||||
@@ -3,15 +3,16 @@ package models
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/pkg/tplx"
|
||||
)
|
||||
|
||||
type AlertCurEvent struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
Cate string `json:"cate"`
|
||||
Cluster string `json:"cluster"`
|
||||
GroupId int64 `json:"group_id"` // busi group id
|
||||
GroupName string `json:"group_name"` // busi group name
|
||||
@@ -46,6 +47,7 @@ type AlertCurEvent struct {
|
||||
LastEvalTime int64 `json:"last_eval_time" gorm:"-"` // for notify.py 上次计算的时间
|
||||
LastSentTime int64 `json:"last_sent_time" gorm:"-"` // 上次发送时间
|
||||
NotifyCurNumber int `json:"notify_cur_number"` // notify: current number
|
||||
FirstTriggerTime int64 `json:"first_trigger_time"` // 连续告警的首次告警时间
|
||||
}
|
||||
|
||||
func (e *AlertCurEvent) TableName() string {
|
||||
@@ -61,10 +63,11 @@ type AggrRule struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (e *AlertCurEvent) ParseRuleNote() error {
|
||||
e.RuleNote = strings.TrimSpace(e.RuleNote)
|
||||
func (e *AlertCurEvent) ParseRule(field string) error {
|
||||
f := e.GetField(field)
|
||||
f = strings.TrimSpace(f)
|
||||
|
||||
if e.RuleNote == "" {
|
||||
if f == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -73,8 +76,8 @@ func (e *AlertCurEvent) ParseRuleNote() error {
|
||||
"{{$value := .TriggerValue}}",
|
||||
}
|
||||
|
||||
text := strings.Join(append(defs, e.RuleNote), "")
|
||||
t, err := template.New(fmt.Sprint(e.RuleId)).Funcs(tplx.TemplateFuncMap).Parse(text)
|
||||
text := strings.Join(append(defs, f), "")
|
||||
t, err := template.New(fmt.Sprint(e.RuleId)).Funcs(template.FuncMap(tplx.TemplateFuncMap)).Parse(text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -85,7 +88,13 @@ func (e *AlertCurEvent) ParseRuleNote() error {
|
||||
return err
|
||||
}
|
||||
|
||||
e.RuleNote = body.String()
|
||||
if field == "rule_name" {
|
||||
e.RuleName = body.String()
|
||||
}
|
||||
|
||||
if field == "rule_note" {
|
||||
e.RuleNote = body.String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -131,6 +140,8 @@ func (e *AlertCurEvent) GetField(field string) string {
|
||||
return fmt.Sprint(e.RuleId)
|
||||
case "rule_name":
|
||||
return e.RuleName
|
||||
case "rule_note":
|
||||
return e.RuleNote
|
||||
case "severity":
|
||||
return fmt.Sprint(e.Severity)
|
||||
case "runbook_url":
|
||||
@@ -154,6 +165,7 @@ func (e *AlertCurEvent) ToHis() *AlertHisEvent {
|
||||
|
||||
return &AlertHisEvent{
|
||||
IsRecovered: isRecovered,
|
||||
Cate: e.Cate,
|
||||
Cluster: e.Cluster,
|
||||
GroupId: e.GroupId,
|
||||
GroupName: e.GroupName,
|
||||
@@ -180,6 +192,7 @@ func (e *AlertCurEvent) ToHis() *AlertHisEvent {
|
||||
RecoverTime: recoverTime,
|
||||
LastEvalTime: e.LastEvalTime,
|
||||
NotifyCurNumber: e.NotifyCurNumber,
|
||||
FirstTriggerTime: e.FirstTriggerTime,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +260,7 @@ func (e *AlertCurEvent) FillNotifyGroups(cache map[int64]*UserGroup) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func AlertCurEventTotal(prod string, bgid, stime, etime int64, severity int, clusters []string, query string) (int64, error) {
|
||||
func AlertCurEventTotal(prod string, bgid, stime, etime int64, severity int, clusters, cates []string, query string) (int64, error) {
|
||||
session := DB().Model(&AlertCurEvent{}).Where("trigger_time between ? and ? and rule_prod = ?", stime, etime, prod)
|
||||
|
||||
if bgid > 0 {
|
||||
@@ -262,6 +275,10 @@ func AlertCurEventTotal(prod string, bgid, stime, etime int64, severity int, clu
|
||||
session = session.Where("cluster in ?", clusters)
|
||||
}
|
||||
|
||||
if len(cates) > 0 {
|
||||
session = session.Where("cate in ?", cates)
|
||||
}
|
||||
|
||||
if query != "" {
|
||||
arr := strings.Fields(query)
|
||||
for i := 0; i < len(arr); i++ {
|
||||
@@ -273,7 +290,7 @@ func AlertCurEventTotal(prod string, bgid, stime, etime int64, severity int, clu
|
||||
return Count(session)
|
||||
}
|
||||
|
||||
func AlertCurEventGets(prod string, bgid, stime, etime int64, severity int, clusters []string, query string, limit, offset int) ([]AlertCurEvent, error) {
|
||||
func AlertCurEventGets(prod string, bgid, stime, etime int64, severity int, clusters, cates []string, query string, limit, offset int) ([]AlertCurEvent, error) {
|
||||
session := DB().Where("trigger_time between ? and ? and rule_prod = ?", stime, etime, prod)
|
||||
|
||||
if bgid > 0 {
|
||||
@@ -288,6 +305,10 @@ func AlertCurEventGets(prod string, bgid, stime, etime int64, severity int, clus
|
||||
session = session.Where("cluster in ?", clusters)
|
||||
}
|
||||
|
||||
if len(cates) > 0 {
|
||||
session = session.Where("cate in ?", cates)
|
||||
}
|
||||
|
||||
if query != "" {
|
||||
arr := strings.Fields(query)
|
||||
for i := 0; i < len(arr); i++ {
|
||||
@@ -399,9 +420,9 @@ func AlertCurEventGetByIds(ids []int64) ([]*AlertCurEvent, error) {
|
||||
return lst, err
|
||||
}
|
||||
|
||||
func AlertCurEventGetByRule(ruleId int64) ([]*AlertCurEvent, error) {
|
||||
func AlertCurEventGetByRuleIdAndCluster(ruleId int64, cluster string) ([]*AlertCurEvent, error) {
|
||||
var lst []*AlertCurEvent
|
||||
err := DB().Where("rule_id=?", ruleId).Find(&lst).Error
|
||||
err := DB().Where("rule_id=? and cluster=?", ruleId, cluster).Find(&lst).Error
|
||||
return lst, err
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
type AlertHisEvent struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
Cate string `json:"cate"`
|
||||
IsRecovered int `json:"is_recovered"`
|
||||
Cluster string `json:"cluster"`
|
||||
GroupId int64 `json:"group_id"`
|
||||
@@ -38,7 +39,8 @@ type AlertHisEvent struct {
|
||||
LastEvalTime int64 `json:"last_eval_time"`
|
||||
Tags string `json:"-"`
|
||||
TagsJSON []string `json:"tags" gorm:"-"`
|
||||
NotifyCurNumber int `json:"notify_cur_number"` // notify: current number
|
||||
NotifyCurNumber int `json:"notify_cur_number"` // notify: current number
|
||||
FirstTriggerTime int64 `json:"first_trigger_time"` // 连续告警的首次告警时间
|
||||
}
|
||||
|
||||
func (e *AlertHisEvent) TableName() string {
|
||||
@@ -90,7 +92,7 @@ func (e *AlertHisEvent) FillNotifyGroups(cache map[int64]*UserGroup) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func AlertHisEventTotal(prod string, bgid, stime, etime int64, severity int, recovered int, clusters []string, query string) (int64, error) {
|
||||
func AlertHisEventTotal(prod string, bgid, stime, etime int64, severity int, recovered int, clusters, cates []string, query string) (int64, error) {
|
||||
session := DB().Model(&AlertHisEvent{}).Where("last_eval_time between ? and ? and rule_prod = ?", stime, etime, prod)
|
||||
|
||||
if bgid > 0 {
|
||||
@@ -109,6 +111,10 @@ func AlertHisEventTotal(prod string, bgid, stime, etime int64, severity int, rec
|
||||
session = session.Where("cluster in ?", clusters)
|
||||
}
|
||||
|
||||
if len(cates) > 0 {
|
||||
session = session.Where("cate in ?", cates)
|
||||
}
|
||||
|
||||
if query != "" {
|
||||
arr := strings.Fields(query)
|
||||
for i := 0; i < len(arr); i++ {
|
||||
@@ -120,7 +126,7 @@ func AlertHisEventTotal(prod string, bgid, stime, etime int64, severity int, rec
|
||||
return Count(session)
|
||||
}
|
||||
|
||||
func AlertHisEventGets(prod string, bgid, stime, etime int64, severity int, recovered int, clusters []string, query string, limit, offset int) ([]AlertHisEvent, error) {
|
||||
func AlertHisEventGets(prod string, bgid, stime, etime int64, severity int, recovered int, clusters, cates []string, query string, limit, offset int) ([]AlertHisEvent, error) {
|
||||
session := DB().Where("last_eval_time between ? and ? and rule_prod = ?", stime, etime, prod)
|
||||
|
||||
if bgid > 0 {
|
||||
@@ -139,6 +145,10 @@ func AlertHisEventGets(prod string, bgid, stime, etime int64, severity int, reco
|
||||
session = session.Where("cluster in ?", clusters)
|
||||
}
|
||||
|
||||
if len(cates) > 0 {
|
||||
session = session.Where("cate in ?", cates)
|
||||
}
|
||||
|
||||
if query != "" {
|
||||
arr := strings.Fields(query)
|
||||
for i := 0; i < len(arr); i++ {
|
||||
|
||||
@@ -13,23 +13,28 @@ import (
|
||||
|
||||
type TagFilter struct {
|
||||
Key string `json:"key"` // tag key
|
||||
Func string `json:"func"` // == | =~ | in
|
||||
Func string `json:"func"` // `==` | `=~` | `in` | `!=` | `!~` | `not in`
|
||||
Value string `json:"value"` // tag value
|
||||
Regexp *regexp.Regexp // parse value to regexp if func = '=~'
|
||||
Vset map[string]struct{} // parse value to regexp if func = 'in'
|
||||
Regexp *regexp.Regexp // parse value to regexp if func = '=~' or '!~'
|
||||
Vset map[string]struct{} // parse value to regexp if func = 'in' or 'not in'
|
||||
}
|
||||
|
||||
type AlertMute struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
GroupId int64 `json:"group_id"`
|
||||
Prod string `json:"prod"` // product empty means n9e
|
||||
Cluster string `json:"cluster"`
|
||||
Note string `json:"note"`
|
||||
Cate string `json:"cate"`
|
||||
Prod string `json:"prod"` // product empty means n9e
|
||||
Cluster string `json:"cluster"` // take effect by clusters, seperated by space
|
||||
Tags ormx.JSONArr `json:"tags"`
|
||||
Cause string `json:"cause"`
|
||||
Btime int64 `json:"btime"`
|
||||
Etime int64 `json:"etime"`
|
||||
Disabled int `json:"disabled"` // 0: enabled, 1: disabled
|
||||
CreateBy string `json:"create_by"`
|
||||
UpdateBy string `json:"update_by"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
ITags []TagFilter `json:"-" gorm:"-"` // inner tags
|
||||
}
|
||||
|
||||
@@ -37,6 +42,24 @@ func (m *AlertMute) TableName() string {
|
||||
return "alert_mute"
|
||||
}
|
||||
|
||||
func AlertMuteGetById(id int64) (*AlertMute, error) {
|
||||
return AlertMuteGet("id=?", id)
|
||||
}
|
||||
|
||||
func AlertMuteGet(where string, args ...interface{}) (*AlertMute, error) {
|
||||
var lst []*AlertMute
|
||||
err := DB().Where(where, args...).Find(&lst).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(lst) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return lst[0], nil
|
||||
}
|
||||
|
||||
func AlertMuteGets(prods []string, bgid int64, query string) (lst []AlertMute, err error) {
|
||||
session := DB().Where("group_id = ? and prod in (?)", bgid, prods)
|
||||
|
||||
@@ -44,7 +67,7 @@ func AlertMuteGets(prods []string, bgid int64, query string) (lst []AlertMute, e
|
||||
arr := strings.Fields(query)
|
||||
for i := 0; i < len(arr); i++ {
|
||||
qarg := "%" + arr[i] + "%"
|
||||
session = session.Where("cause like ?", qarg, qarg)
|
||||
session = session.Where("cause like ?", qarg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +89,12 @@ func (m *AlertMute) Verify() error {
|
||||
return errors.New("cluster invalid")
|
||||
}
|
||||
|
||||
if IsClusterAll(m.Cluster) {
|
||||
m.Cluster = ClusterAll
|
||||
}
|
||||
|
||||
if m.Etime <= m.Btime {
|
||||
return fmt.Errorf("Oops... etime(%d) <= btime(%d)", m.Etime, m.Btime)
|
||||
return fmt.Errorf("oops... etime(%d) <= btime(%d)", m.Etime, m.Btime)
|
||||
}
|
||||
|
||||
if err := m.Parse(); err != nil {
|
||||
@@ -88,12 +115,12 @@ func (m *AlertMute) Parse() error {
|
||||
}
|
||||
|
||||
for i := 0; i < len(m.ITags); i++ {
|
||||
if m.ITags[i].Func == "=~" {
|
||||
if m.ITags[i].Func == "=~" || m.ITags[i].Func == "!~" {
|
||||
m.ITags[i].Regexp, err = regexp.Compile(m.ITags[i].Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if m.ITags[i].Func == "in" {
|
||||
} else if m.ITags[i].Func == "in" || m.ITags[i].Func == "not in" {
|
||||
arr := strings.Fields(m.ITags[i].Value)
|
||||
m.ITags[i].Vset = make(map[string]struct{})
|
||||
for j := 0; j < len(arr); j++ {
|
||||
@@ -109,10 +136,31 @@ func (m *AlertMute) Add() error {
|
||||
if err := m.Verify(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.CreateAt = time.Now().Unix()
|
||||
now := time.Now().Unix()
|
||||
m.CreateAt = now
|
||||
m.UpdateAt = now
|
||||
return Insert(m)
|
||||
}
|
||||
|
||||
func (m *AlertMute) Update(arm AlertMute) error {
|
||||
|
||||
arm.Id = m.Id
|
||||
arm.GroupId = m.GroupId
|
||||
arm.CreateAt = m.CreateAt
|
||||
arm.CreateBy = m.CreateBy
|
||||
arm.UpdateAt = time.Now().Unix()
|
||||
|
||||
err := arm.Verify()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return DB().Model(m).Select("*").Updates(arm).Error
|
||||
}
|
||||
|
||||
func (m *AlertMute) UpdateFieldsMap(fields map[string]interface{}) error {
|
||||
return DB().Model(m).Updates(fields).Error
|
||||
}
|
||||
|
||||
func AlertMuteDel(ids []int64) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
@@ -121,13 +169,20 @@ func AlertMuteDel(ids []int64) error {
|
||||
}
|
||||
|
||||
func AlertMuteStatistics(cluster string) (*Statistics, error) {
|
||||
session := DB().Model(&AlertMute{}).Select("count(*) as total", "max(create_at) as last_updated")
|
||||
// clean expired first
|
||||
buf := int64(30)
|
||||
err := DB().Where("etime < ?", time.Now().Unix()-buf).Delete(new(AlertMute)).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session := DB().Model(&AlertMute{}).Select("count(*) as total", "max(update_at) as last_updated")
|
||||
if cluster != "" {
|
||||
session = session.Where("cluster = ?", cluster)
|
||||
session = session.Where("(cluster like ? or cluster = ?)", "%"+cluster+"%", ClusterAll)
|
||||
}
|
||||
|
||||
var stats []*Statistics
|
||||
err := session.Find(&stats).Error
|
||||
err = session.Find(&stats).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -136,20 +191,27 @@ func AlertMuteStatistics(cluster string) (*Statistics, error) {
|
||||
}
|
||||
|
||||
func AlertMuteGetsByCluster(cluster string) ([]*AlertMute, error) {
|
||||
// clean expired first
|
||||
buf := int64(30)
|
||||
err := DB().Where("etime < ?", time.Now().Unix()+buf).Delete(new(AlertMute)).Error
|
||||
// get my cluster's mutes
|
||||
session := DB().Model(&AlertMute{})
|
||||
if cluster != "" {
|
||||
session = session.Where("(cluster like ? or cluster = ?)", "%"+cluster+"%", ClusterAll)
|
||||
}
|
||||
|
||||
var lst []*AlertMute
|
||||
var mlst []*AlertMute
|
||||
err := session.Find(&lst).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get my cluster's mutes
|
||||
session := DB().Model(&AlertMute{})
|
||||
if cluster != "" {
|
||||
session = session.Where("cluster = ?", cluster)
|
||||
if cluster == "" {
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
var lst []*AlertMute
|
||||
err = session.Find(&lst).Error
|
||||
return lst, err
|
||||
for _, m := range lst {
|
||||
if MatchCluster(m.Cluster, cluster) {
|
||||
mlst = append(mlst, m)
|
||||
}
|
||||
}
|
||||
return mlst, err
|
||||
}
|
||||
|
||||
@@ -14,44 +14,50 @@ import (
|
||||
)
|
||||
|
||||
type AlertRule struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
GroupId int64 `json:"group_id"` // busi group id
|
||||
Cluster string `json:"cluster"` // take effect by cluster
|
||||
Name string `json:"name"` // rule name
|
||||
Note string `json:"note"` // will sent in notify
|
||||
Prod string `json:"prod"` // product empty means n9e
|
||||
Algorithm string `json:"algorithm"` // algorithm (''|holtwinters), empty means threshold
|
||||
AlgoParams string `json:"-" gorm:"algo_params"` // params algorithm need
|
||||
AlgoParamsJson interface{} `json:"algo_params" gorm:"-"` //
|
||||
Delay int `json:"delay"` // Time (in seconds) to delay evaluation
|
||||
Severity int `json:"severity"` // 1: Emergency 2: Warning 3: Notice
|
||||
Disabled int `json:"disabled"` // 0: enabled, 1: disabled
|
||||
PromForDuration int `json:"prom_for_duration"` // prometheus for, unit:s
|
||||
PromQl string `json:"prom_ql"` // just one ql
|
||||
PromEvalInterval int `json:"prom_eval_interval"` // unit:s
|
||||
EnableStime string `json:"enable_stime"` // e.g. 00:00
|
||||
EnableEtime string `json:"enable_etime"` // e.g. 23:59
|
||||
EnableDaysOfWeek string `json:"-"` // split by space: 0 1 2 3 4 5 6
|
||||
EnableDaysOfWeekJSON []string `json:"enable_days_of_week" gorm:"-"` // for fe
|
||||
EnableInBG int `json:"enable_in_bg"` // 0: global 1: enable one busi-group
|
||||
NotifyRecovered int `json:"notify_recovered"` // whether notify when recovery
|
||||
NotifyChannels string `json:"-"` // split by space: sms voice email dingtalk wecom
|
||||
NotifyChannelsJSON []string `json:"notify_channels" gorm:"-"` // for fe
|
||||
NotifyGroups string `json:"-"` // split by space: 233 43
|
||||
NotifyGroupsObj []UserGroup `json:"notify_groups_obj" gorm:"-"` // for fe
|
||||
NotifyGroupsJSON []string `json:"notify_groups" gorm:"-"` // for fe
|
||||
NotifyRepeatStep int `json:"notify_repeat_step"` // notify repeat interval, unit: min
|
||||
NotifyMaxNumber int `json:"notify_max_number"` // notify: max number
|
||||
RecoverDuration int64 `json:"recover_duration"` // unit: s
|
||||
Callbacks string `json:"-"` // split by space: http://a.com/api/x http://a.com/api/y'
|
||||
CallbacksJSON []string `json:"callbacks" gorm:"-"` // for fe
|
||||
RunbookUrl string `json:"runbook_url"` // sop url
|
||||
AppendTags string `json:"-"` // split by space: service=n9e mod=api
|
||||
AppendTagsJSON []string `json:"append_tags" gorm:"-"` // for fe
|
||||
CreateAt int64 `json:"create_at"`
|
||||
CreateBy string `json:"create_by"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
UpdateBy string `json:"update_by"`
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
GroupId int64 `json:"group_id"` // busi group id
|
||||
Cate string `json:"cate"` // alert rule cate (prometheus|elasticsearch)
|
||||
Cluster string `json:"cluster"` // take effect by clusters, seperated by space
|
||||
Name string `json:"name"` // rule name
|
||||
Note string `json:"note"` // will sent in notify
|
||||
Prod string `json:"prod"` // product empty means n9e
|
||||
Algorithm string `json:"algorithm"` // algorithm (''|holtwinters), empty means threshold
|
||||
AlgoParams string `json:"-" gorm:"algo_params"` // params algorithm need
|
||||
AlgoParamsJson interface{} `json:"algo_params" gorm:"-"` //
|
||||
Delay int `json:"delay"` // Time (in seconds) to delay evaluation
|
||||
Severity int `json:"severity"` // 1: Emergency 2: Warning 3: Notice
|
||||
Disabled int `json:"disabled"` // 0: enabled, 1: disabled
|
||||
PromForDuration int `json:"prom_for_duration"` // prometheus for, unit:s
|
||||
PromQl string `json:"prom_ql"` // just one ql
|
||||
PromEvalInterval int `json:"prom_eval_interval"` // unit:s
|
||||
EnableStime string `json:"-"` // split by space: "00:00 10:00 12:00"
|
||||
EnableStimeJSON string `json:"enable_stime" gorm:"-"` // for fe
|
||||
EnableStimesJSON []string `json:"enable_stimes" gorm:"-"` // for fe
|
||||
EnableEtime string `json:"-"` // split by space: "00:00 10:00 12:00"
|
||||
EnableEtimeJSON string `json:"enable_etime" gorm:"-"` // for fe
|
||||
EnableEtimesJSON []string `json:"enable_etimes" gorm:"-"` // for fe
|
||||
EnableDaysOfWeek string `json:"-"` // eg: "0 1 2 3 4 5 6 ; 0 1 2"
|
||||
EnableDaysOfWeekJSON []string `json:"enable_days_of_week" gorm:"-"` // for fe
|
||||
EnableDaysOfWeeksJSON [][]string `json:"enable_days_of_weeks" gorm:"-"` // for fe
|
||||
EnableInBG int `json:"enable_in_bg"` // 0: global 1: enable one busi-group
|
||||
NotifyRecovered int `json:"notify_recovered"` // whether notify when recovery
|
||||
NotifyChannels string `json:"-"` // split by space: sms voice email dingtalk wecom
|
||||
NotifyChannelsJSON []string `json:"notify_channels" gorm:"-"` // for fe
|
||||
NotifyGroups string `json:"-"` // split by space: 233 43
|
||||
NotifyGroupsObj []UserGroup `json:"notify_groups_obj" gorm:"-"` // for fe
|
||||
NotifyGroupsJSON []string `json:"notify_groups" gorm:"-"` // for fe
|
||||
NotifyRepeatStep int `json:"notify_repeat_step"` // notify repeat interval, unit: min
|
||||
NotifyMaxNumber int `json:"notify_max_number"` // notify: max number
|
||||
RecoverDuration int64 `json:"recover_duration"` // unit: s
|
||||
Callbacks string `json:"-"` // split by space: http://a.com/api/x http://a.com/api/y'
|
||||
CallbacksJSON []string `json:"callbacks" gorm:"-"` // for fe
|
||||
RunbookUrl string `json:"runbook_url"` // sop url
|
||||
AppendTags string `json:"-"` // split by space: service=n9e mod=api
|
||||
AppendTagsJSON []string `json:"append_tags" gorm:"-"` // for fe
|
||||
CreateAt int64 `json:"create_at"`
|
||||
CreateBy string `json:"create_by"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
UpdateBy string `json:"update_by"`
|
||||
}
|
||||
|
||||
func (ar *AlertRule) TableName() string {
|
||||
@@ -67,6 +73,10 @@ func (ar *AlertRule) Verify() error {
|
||||
return errors.New("cluster is blank")
|
||||
}
|
||||
|
||||
if IsClusterAll(ar.Cluster) {
|
||||
ar.Cluster = ClusterAll
|
||||
}
|
||||
|
||||
if str.Dangerous(ar.Name) {
|
||||
return errors.New("Name has invalid characters")
|
||||
}
|
||||
@@ -124,7 +134,7 @@ func (ar *AlertRule) Add() error {
|
||||
return err
|
||||
}
|
||||
|
||||
exists, err := AlertRuleExists("group_id=? and cluster=? and name=?", ar.GroupId, ar.Cluster, ar.Name)
|
||||
exists, err := AlertRuleExists(0, ar.GroupId, ar.Cluster, ar.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -142,7 +152,7 @@ func (ar *AlertRule) Add() error {
|
||||
|
||||
func (ar *AlertRule) Update(arf AlertRule) error {
|
||||
if ar.Name != arf.Name {
|
||||
exists, err := AlertRuleExists("group_id=? and cluster=? and name=? and id <> ?", ar.GroupId, ar.Cluster, arf.Name, ar.Id)
|
||||
exists, err := AlertRuleExists(ar.Id, ar.GroupId, ar.Cluster, arf.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -219,7 +229,30 @@ func (ar *AlertRule) FillNotifyGroups(cache map[int64]*UserGroup) error {
|
||||
}
|
||||
|
||||
func (ar *AlertRule) FE2DB() error {
|
||||
ar.EnableDaysOfWeek = strings.Join(ar.EnableDaysOfWeekJSON, " ")
|
||||
if len(ar.EnableStimesJSON) > 0 {
|
||||
ar.EnableStime = strings.Join(ar.EnableStimesJSON, " ")
|
||||
ar.EnableEtime = strings.Join(ar.EnableEtimesJSON, " ")
|
||||
} else {
|
||||
ar.EnableStime = ar.EnableStimeJSON
|
||||
ar.EnableEtime = ar.EnableEtimeJSON
|
||||
}
|
||||
|
||||
if len(ar.EnableDaysOfWeeksJSON) > 0 {
|
||||
for i := 0; i < len(ar.EnableDaysOfWeeksJSON); i++ {
|
||||
if len(ar.EnableDaysOfWeeksJSON) == 1 {
|
||||
ar.EnableDaysOfWeek = strings.Join(ar.EnableDaysOfWeeksJSON[i], " ")
|
||||
} else {
|
||||
if i == len(ar.EnableDaysOfWeeksJSON)-1 {
|
||||
ar.EnableDaysOfWeek += strings.Join(ar.EnableDaysOfWeeksJSON[i], " ")
|
||||
} else {
|
||||
ar.EnableDaysOfWeek += strings.Join(ar.EnableDaysOfWeeksJSON[i], " ") + ";"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ar.EnableDaysOfWeek = strings.Join(ar.EnableDaysOfWeekJSON, " ")
|
||||
}
|
||||
|
||||
ar.NotifyChannels = strings.Join(ar.NotifyChannelsJSON, " ")
|
||||
ar.NotifyGroups = strings.Join(ar.NotifyGroupsJSON, " ")
|
||||
ar.Callbacks = strings.Join(ar.CallbacksJSON, " ")
|
||||
@@ -234,7 +267,21 @@ func (ar *AlertRule) FE2DB() error {
|
||||
}
|
||||
|
||||
func (ar *AlertRule) DB2FE() {
|
||||
ar.EnableDaysOfWeekJSON = strings.Fields(ar.EnableDaysOfWeek)
|
||||
ar.EnableStimesJSON = strings.Fields(ar.EnableStime)
|
||||
ar.EnableEtimesJSON = strings.Fields(ar.EnableEtime)
|
||||
if len(ar.EnableEtimesJSON) > 0 {
|
||||
ar.EnableStimeJSON = ar.EnableStimesJSON[0]
|
||||
ar.EnableEtimeJSON = ar.EnableEtimesJSON[0]
|
||||
}
|
||||
|
||||
cache := strings.Split(ar.EnableDaysOfWeek, ";")
|
||||
for i := 0; i < len(cache); i++ {
|
||||
ar.EnableDaysOfWeeksJSON = append(ar.EnableDaysOfWeeksJSON, strings.Fields(cache[i]))
|
||||
}
|
||||
if len(ar.EnableDaysOfWeeksJSON) > 0 {
|
||||
ar.EnableDaysOfWeekJSON = ar.EnableDaysOfWeeksJSON[0]
|
||||
}
|
||||
|
||||
ar.NotifyChannelsJSON = strings.Fields(ar.NotifyChannels)
|
||||
ar.NotifyGroupsJSON = strings.Fields(ar.NotifyGroups)
|
||||
ar.CallbacksJSON = strings.Fields(ar.Callbacks)
|
||||
@@ -262,8 +309,25 @@ func AlertRuleDels(ids []int64, bgid ...int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func AlertRuleExists(where string, args ...interface{}) (bool, error) {
|
||||
return Exists(DB().Model(&AlertRule{}).Where(where, args...))
|
||||
func AlertRuleExists(id, groupId int64, cluster, name string) (bool, error) {
|
||||
session := DB().Where("id <> ? and group_id = ? and name = ?", id, groupId, name)
|
||||
|
||||
var lst []AlertRule
|
||||
err := session.Find(&lst).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(lst) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// match cluster
|
||||
for _, r := range lst {
|
||||
if MatchCluster(r.Cluster, cluster) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func AlertRuleGets(groupId int64) ([]AlertRule, error) {
|
||||
@@ -284,22 +348,39 @@ func AlertRuleGetsByCluster(cluster string) ([]*AlertRule, error) {
|
||||
session := DB().Where("disabled = ? and prod = ?", 0, "")
|
||||
|
||||
if cluster != "" {
|
||||
session = session.Where("cluster = ?", cluster)
|
||||
session = session.Where("(cluster like ? or cluster = ?)", "%"+cluster+"%", ClusterAll)
|
||||
}
|
||||
|
||||
var lst []*AlertRule
|
||||
err := session.Find(&lst).Error
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
return lst, err
|
||||
}
|
||||
|
||||
if len(lst) == 0 {
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
if cluster == "" {
|
||||
for i := 0; i < len(lst); i++ {
|
||||
lst[i].DB2FE()
|
||||
}
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
return lst, err
|
||||
lr := make([]*AlertRule, 0, len(lst))
|
||||
for _, r := range lst {
|
||||
if MatchCluster(r.Cluster, cluster) {
|
||||
r.DB2FE()
|
||||
lr = append(lr, r)
|
||||
}
|
||||
}
|
||||
|
||||
return lr, err
|
||||
}
|
||||
|
||||
func AlertRulesGetsBy(prods []string, query string) ([]*AlertRule, error) {
|
||||
session := DB().Where("disabled = ? and prod in (?)", 0, prods)
|
||||
func AlertRulesGetsBy(prods []string, query, algorithm, cluster string, cates []string, disabled int) ([]*AlertRule, error) {
|
||||
session := DB().Where("prod in (?)", prods)
|
||||
|
||||
if query != "" {
|
||||
arr := strings.Fields(query)
|
||||
@@ -309,6 +390,22 @@ func AlertRulesGetsBy(prods []string, query string) ([]*AlertRule, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if algorithm != "" {
|
||||
session = session.Where("algorithm = ?", algorithm)
|
||||
}
|
||||
|
||||
if cluster != "" {
|
||||
session = session.Where("cluster like ?", "%"+cluster+"%")
|
||||
}
|
||||
|
||||
if len(cates) != 0 {
|
||||
session = session.Where("cate in (?)", cates)
|
||||
}
|
||||
|
||||
if disabled != -1 {
|
||||
session = session.Where("disabled = ?", disabled)
|
||||
}
|
||||
|
||||
var lst []*AlertRule
|
||||
err := session.Find(&lst).Error
|
||||
if err == nil {
|
||||
@@ -358,7 +455,8 @@ func AlertRuleStatistics(cluster string) (*Statistics, error) {
|
||||
session := DB().Model(&AlertRule{}).Select("count(*) as total", "max(update_at) as last_updated").Where("disabled = ? and prod = ?", 0, "")
|
||||
|
||||
if cluster != "" {
|
||||
session = session.Where("cluster = ?", cluster)
|
||||
// 简略的判断,当一个clustername是另一个clustername的substring的时候,会出现stats与预期不符,不影响使用
|
||||
session = session.Where("(cluster like ? or cluster = ?)", "%"+cluster+"%", ClusterAll)
|
||||
}
|
||||
|
||||
var stats []*Statistics
|
||||
@@ -369,3 +467,38 @@ func AlertRuleStatistics(cluster string) (*Statistics, error) {
|
||||
|
||||
return stats[0], nil
|
||||
}
|
||||
|
||||
func (ar *AlertRule) IsPrometheusRule() bool {
|
||||
return ar.Algorithm == "" && (ar.Cate == "" || strings.ToLower(ar.Cate) == "prometheus")
|
||||
}
|
||||
|
||||
func (ar *AlertRule) GenerateNewEvent() *AlertCurEvent {
|
||||
event := &AlertCurEvent{}
|
||||
ar.UpdateEvent(event)
|
||||
return event
|
||||
}
|
||||
|
||||
func (ar *AlertRule) UpdateEvent(event *AlertCurEvent) {
|
||||
if event == nil {
|
||||
return
|
||||
}
|
||||
event.GroupId = ar.GroupId
|
||||
event.Cate = ar.Cate
|
||||
event.RuleId = ar.Id
|
||||
event.RuleName = ar.Name
|
||||
event.RuleNote = ar.Note
|
||||
event.RuleProd = ar.Prod
|
||||
event.RuleAlgo = ar.Algorithm
|
||||
event.Severity = ar.Severity
|
||||
event.PromForDuration = ar.PromForDuration
|
||||
event.PromQl = ar.PromQl
|
||||
event.PromEvalInterval = ar.PromEvalInterval
|
||||
event.Callbacks = ar.Callbacks
|
||||
event.CallbacksJSON = ar.CallbacksJSON
|
||||
event.RunbookUrl = ar.RunbookUrl
|
||||
event.NotifyRecovered = ar.NotifyRecovered
|
||||
event.NotifyChannels = ar.NotifyChannels
|
||||
event.NotifyChannelsJSON = ar.NotifyChannelsJSON
|
||||
event.NotifyGroups = ar.NotifyGroups
|
||||
event.NotifyGroupsJSON = ar.NotifyGroupsJSON
|
||||
}
|
||||
|
||||
@@ -13,8 +13,11 @@ import (
|
||||
|
||||
type AlertSubscribe struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name"` // AlertSubscribe name
|
||||
Disabled int `json:"disabled"` // 0: enabled, 1: disabled
|
||||
GroupId int64 `json:"group_id"`
|
||||
Cluster string `json:"cluster"`
|
||||
Cate string `json:"cate"`
|
||||
Cluster string `json:"cluster"` // take effect by clusters, seperated by space
|
||||
RuleId int64 `json:"rule_id"`
|
||||
RuleName string `json:"rule_name" gorm:"-"` // for fe
|
||||
Tags ormx.JSONArr `json:"tags"`
|
||||
@@ -54,11 +57,19 @@ func AlertSubscribeGet(where string, args ...interface{}) (*AlertSubscribe, erro
|
||||
return lst[0], nil
|
||||
}
|
||||
|
||||
func (s *AlertSubscribe) IsDisabled() bool {
|
||||
return s.Disabled == 1
|
||||
}
|
||||
|
||||
func (s *AlertSubscribe) Verify() error {
|
||||
if s.Cluster == "" {
|
||||
return errors.New("cluster invalid")
|
||||
}
|
||||
|
||||
if IsClusterAll(s.Cluster) {
|
||||
s.Cluster = ClusterAll
|
||||
}
|
||||
|
||||
if err := s.Parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -84,12 +95,12 @@ func (s *AlertSubscribe) Parse() error {
|
||||
}
|
||||
|
||||
for i := 0; i < len(s.ITags); i++ {
|
||||
if s.ITags[i].Func == "=~" {
|
||||
if s.ITags[i].Func == "=~" || s.ITags[i].Func == "!~" {
|
||||
s.ITags[i].Regexp, err = regexp.Compile(s.ITags[i].Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if s.ITags[i].Func == "in" {
|
||||
} else if s.ITags[i].Func == "in" || s.ITags[i].Func == "not in" {
|
||||
arr := strings.Fields(s.ITags[i].Value)
|
||||
s.ITags[i].Vset = make(map[string]struct{})
|
||||
for j := 0; j < len(arr); j++ {
|
||||
@@ -202,7 +213,7 @@ func AlertSubscribeStatistics(cluster string) (*Statistics, error) {
|
||||
session := DB().Model(&AlertSubscribe{}).Select("count(*) as total", "max(update_at) as last_updated")
|
||||
|
||||
if cluster != "" {
|
||||
session = session.Where("cluster = ?", cluster)
|
||||
session = session.Where("(cluster like ? or cluster = ?)", "%"+cluster+"%", ClusterAll)
|
||||
}
|
||||
|
||||
var stats []*Statistics
|
||||
@@ -218,10 +229,51 @@ func AlertSubscribeGetsByCluster(cluster string) ([]*AlertSubscribe, error) {
|
||||
// get my cluster's subscribes
|
||||
session := DB().Model(&AlertSubscribe{})
|
||||
if cluster != "" {
|
||||
session = session.Where("cluster = ?", cluster)
|
||||
session = session.Where("(cluster like ? or cluster = ?)", "%"+cluster+"%", ClusterAll)
|
||||
}
|
||||
|
||||
var lst []*AlertSubscribe
|
||||
var slst []*AlertSubscribe
|
||||
err := session.Find(&lst).Error
|
||||
return lst, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cluster == "" {
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
for _, s := range lst {
|
||||
if MatchCluster(s.Cluster, cluster) {
|
||||
slst = append(slst, s)
|
||||
}
|
||||
}
|
||||
return slst, err
|
||||
}
|
||||
|
||||
func (s *AlertSubscribe) MatchCluster(cluster string) bool {
|
||||
if s.Cluster == ClusterAll {
|
||||
return true
|
||||
}
|
||||
clusters := strings.Fields(s.Cluster)
|
||||
for _, c := range clusters {
|
||||
if c == cluster {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *AlertSubscribe) ModifyEvent(event *AlertCurEvent) {
|
||||
if s.RedefineSeverity == 1 {
|
||||
event.Severity = s.NewSeverity
|
||||
}
|
||||
|
||||
if s.RedefineChannels == 1 {
|
||||
event.NotifyChannels = s.NewChannels
|
||||
event.NotifyChannelsJSON = strings.Fields(s.NewChannels)
|
||||
}
|
||||
|
||||
event.NotifyGroups = s.UserGroupIds
|
||||
event.NotifyGroupsJSON = strings.Fields(s.UserGroupIds)
|
||||
}
|
||||
|
||||
144
src/models/alerting_engine.go
Normal file
144
src/models/alerting_engine.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AlertingEngines struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
Instance string `json:"instance"`
|
||||
Cluster string `json:"cluster"` // reader cluster
|
||||
Clock int64 `json:"clock"`
|
||||
}
|
||||
|
||||
func (e *AlertingEngines) TableName() string {
|
||||
return "alerting_engines"
|
||||
}
|
||||
|
||||
// UpdateCluster 页面上用户会给各个n9e-server分配要关联的目标集群是什么
|
||||
func (e *AlertingEngines) UpdateCluster(c string) error {
|
||||
count, err := Count(DB().Model(&AlertingEngines{}).Where("id<>? and instance=? and cluster=?", e.Id, e.Instance, c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return fmt.Errorf("instance %s and cluster %s already exists", e.Instance, c)
|
||||
}
|
||||
|
||||
e.Cluster = c
|
||||
return DB().Model(e).Select("cluster").Updates(e).Error
|
||||
}
|
||||
|
||||
func AlertingEngineAdd(instance, cluster string) error {
|
||||
count, err := Count(DB().Model(&AlertingEngines{}).Where("instance=? and cluster=?", instance, cluster))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return fmt.Errorf("instance %s and cluster %s already exists", instance, cluster)
|
||||
}
|
||||
|
||||
err = DB().Create(&AlertingEngines{
|
||||
Instance: instance,
|
||||
Cluster: cluster,
|
||||
Clock: time.Now().Unix(),
|
||||
}).Error
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func AlertingEngineDel(ids []int64) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return DB().Where("id in ?", ids).Delete(new(AlertingEngines)).Error
|
||||
}
|
||||
|
||||
// AlertingEngineGetCluster 根据实例名获取对应的集群名字
|
||||
func AlertingEngineGetClusters(instance string) ([]string, error) {
|
||||
var objs []AlertingEngines
|
||||
err := DB().Where("instance=?", instance).Find(&objs).Error
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
if len(objs) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
var clusters []string
|
||||
for i := 0; i < len(objs); i++ {
|
||||
clusters = append(clusters, objs[i].Cluster)
|
||||
}
|
||||
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
// AlertingEngineGets 拉取列表数据,用户要在页面上看到所有 n9e-server 实例列表,然后为其分配 cluster
|
||||
func AlertingEngineGets(where string, args ...interface{}) ([]*AlertingEngines, error) {
|
||||
var objs []*AlertingEngines
|
||||
var err error
|
||||
session := DB().Order("instance")
|
||||
if where == "" {
|
||||
err = session.Find(&objs).Error
|
||||
} else {
|
||||
err = session.Where(where, args...).Find(&objs).Error
|
||||
}
|
||||
return objs, err
|
||||
}
|
||||
|
||||
func AlertingEngineGet(where string, args ...interface{}) (*AlertingEngines, error) {
|
||||
lst, err := AlertingEngineGets(where, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(lst) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return lst[0], nil
|
||||
}
|
||||
|
||||
func AlertingEngineGetsInstances(where string, args ...interface{}) ([]string, error) {
|
||||
var arr []string
|
||||
var err error
|
||||
session := DB().Model(new(AlertingEngines)).Order("instance")
|
||||
if where == "" {
|
||||
err = session.Pluck("instance", &arr).Error
|
||||
} else {
|
||||
err = session.Where(where, args...).Pluck("instance", &arr).Error
|
||||
}
|
||||
return arr, err
|
||||
}
|
||||
|
||||
func AlertingEngineHeartbeatWithCluster(instance, cluster string) error {
|
||||
var total int64
|
||||
err := DB().Model(new(AlertingEngines)).Where("instance=? and cluster=?", instance, cluster).Count(&total).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
// insert
|
||||
err = DB().Create(&AlertingEngines{
|
||||
Instance: instance,
|
||||
Cluster: cluster,
|
||||
Clock: time.Now().Unix(),
|
||||
}).Error
|
||||
} else {
|
||||
// updates
|
||||
fields := map[string]interface{}{"clock": time.Now().Unix()}
|
||||
err = DB().Model(new(AlertingEngines)).Where("instance=? and cluster=?", instance, cluster).Updates(fields).Error
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func AlertingEngineHeartbeat(instance string) error {
|
||||
fields := map[string]interface{}{"clock": time.Now().Unix()}
|
||||
err := DB().Model(new(AlertingEngines)).Where("instance=?", instance).Updates(fields).Error
|
||||
return err
|
||||
}
|
||||
@@ -13,12 +13,14 @@ type Board struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
GroupId int64 `json:"group_id"`
|
||||
Name string `json:"name"`
|
||||
Ident string `json:"ident"`
|
||||
Tags string `json:"tags"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
CreateBy string `json:"create_by"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
UpdateBy string `json:"update_by"`
|
||||
Configs string `json:"configs" gorm:"-"`
|
||||
Public int `json:"public"` // 0: false, 1: true
|
||||
}
|
||||
|
||||
func (b *Board) TableName() string {
|
||||
@@ -37,11 +39,36 @@ func (b *Board) Verify() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Board) CanRenameIdent(ident string) (bool, error) {
|
||||
if ident == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
cnt, err := Count(DB().Model(b).Where("ident=? and id <> ?", ident, b.Id))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return cnt == 0, nil
|
||||
}
|
||||
|
||||
func (b *Board) Add() error {
|
||||
if err := b.Verify(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.Ident != "" {
|
||||
// ident duplicate check
|
||||
cnt, err := Count(DB().Model(b).Where("ident=?", b.Ident))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt > 0 {
|
||||
return errors.New("Ident duplicate")
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
b.CreateAt = now
|
||||
b.UpdateAt = now
|
||||
@@ -71,6 +98,20 @@ func (b *Board) Del() error {
|
||||
})
|
||||
}
|
||||
|
||||
func BoardGetByID(id int64) (*Board, error) {
|
||||
var lst []*Board
|
||||
err := DB().Where("id = ?", id).Find(&lst).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(lst) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return lst[0], nil
|
||||
}
|
||||
|
||||
// BoardGet for detail page
|
||||
func BoardGet(where string, args ...interface{}) (*Board, error) {
|
||||
var lst []*Board
|
||||
|
||||
@@ -119,7 +119,7 @@ func (bg *BusiGroup) Del() error {
|
||||
return errors.New("Some targets still in the BusiGroup")
|
||||
}
|
||||
|
||||
has, err = Exists(DB().Model(&Dashboard{}).Where("group_id=?", bg.Id))
|
||||
has, err = Exists(DB().Model(&Board{}).Where("group_id=?", bg.Id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/toolkits/pkg/str"
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -9,6 +11,9 @@ import (
|
||||
|
||||
const AdminRole = "Admin"
|
||||
|
||||
// if rule's cluster field contains `ClusterAll`, means it take effect in all clusters
|
||||
const ClusterAll = "$all"
|
||||
|
||||
func DB() *gorm.DB {
|
||||
return storage.DB
|
||||
}
|
||||
@@ -42,3 +47,26 @@ type Statistics struct {
|
||||
Total int64 `gorm:"total"`
|
||||
LastUpdated int64 `gorm:"last_updated"`
|
||||
}
|
||||
|
||||
func MatchCluster(ruleCluster, targetCluster string) bool {
|
||||
if targetCluster == ClusterAll {
|
||||
return true
|
||||
}
|
||||
clusters := strings.Fields(ruleCluster)
|
||||
for _, c := range clusters {
|
||||
if c == ClusterAll || c == targetCluster {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsClusterAll(ruleCluster string) bool {
|
||||
clusters := strings.Fields(ruleCluster)
|
||||
for _, c := range clusters {
|
||||
if c == ClusterAll {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -74,7 +74,62 @@ func ConfigsSet(ckey, cval string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func ConfigsGets(ckeys []string) (map[string]string, error) {
|
||||
func ConfigGet(id int64) (*Configs, error) {
|
||||
var objs []*Configs
|
||||
err := DB().Where("id=?", id).Find(&objs).Error
|
||||
|
||||
if len(objs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return objs[0], err
|
||||
}
|
||||
|
||||
func ConfigsGets(prefix string, limit, offset int) ([]*Configs, error) {
|
||||
var objs []*Configs
|
||||
session := DB()
|
||||
if prefix != "" {
|
||||
session = session.Where("ckey like ?", prefix+"%")
|
||||
}
|
||||
|
||||
err := session.Order("id desc").Limit(limit).Offset(offset).Find(&objs).Error
|
||||
return objs, err
|
||||
}
|
||||
|
||||
func (c *Configs) Add() error {
|
||||
num, err := Count(DB().Model(&Configs{}).Where("ckey=?", c.Ckey))
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to count configs")
|
||||
}
|
||||
if num > 0 {
|
||||
return errors.WithMessage(err, "key is exists")
|
||||
}
|
||||
|
||||
// insert
|
||||
err = DB().Create(&Configs{
|
||||
Ckey: c.Ckey,
|
||||
Cval: c.Cval,
|
||||
}).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Configs) Update() error {
|
||||
num, err := Count(DB().Model(&Configs{}).Where("id<>? and ckey=?", c.Id, c.Ckey))
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to count configs")
|
||||
}
|
||||
if num > 0 {
|
||||
return errors.WithMessage(err, "key is exists")
|
||||
}
|
||||
|
||||
err = DB().Model(&Configs{}).Where("id=?", c.Id).Updates(c).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func ConfigsDel(ids []int64) error {
|
||||
return DB().Where("id in ?", ids).Delete(&Configs{}).Error
|
||||
}
|
||||
|
||||
func ConfigsGetsByKey(ckeys []string) (map[string]string, error) {
|
||||
var objs []Configs
|
||||
err := DB().Where("ckey in ?", ckeys).Find(&objs).Error
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type RecordingRule struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
GroupId int64 `json:"group_id"` // busi group id
|
||||
Cluster string `json:"cluster"` // take effect by cluster
|
||||
Cluster string `json:"cluster"` // take effect by cluster, seperated by space
|
||||
Name string `json:"name"` // new metric name
|
||||
Note string `json:"note"` // note
|
||||
Disabled int `json:"disabled"` // 0: enabled, 1: disabled
|
||||
@@ -40,6 +40,7 @@ func (re *RecordingRule) DB2FE() {
|
||||
//re.ClusterJSON = strings.Fields(re.Cluster)
|
||||
re.AppendTagsJSON = strings.Fields(re.AppendTags)
|
||||
}
|
||||
|
||||
func (re *RecordingRule) Verify() error {
|
||||
if re.GroupId < 0 {
|
||||
return fmt.Errorf("GroupId(%d) invalid", re.GroupId)
|
||||
@@ -49,6 +50,10 @@ func (re *RecordingRule) Verify() error {
|
||||
return errors.New("cluster is blank")
|
||||
}
|
||||
|
||||
if IsClusterAll(re.Cluster) {
|
||||
re.Cluster = ClusterAll
|
||||
}
|
||||
|
||||
if !model.MetricNameRE.MatchString(re.Name) {
|
||||
return errors.New("Name has invalid chreacters")
|
||||
}
|
||||
@@ -78,14 +83,15 @@ func (re *RecordingRule) Add() error {
|
||||
return err
|
||||
}
|
||||
|
||||
exists, err := RecordingRuleExists("group_id=? and cluster=? and name=?", re.GroupId, re.Cluster, re.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
return errors.New("RecordingRule already exists")
|
||||
}
|
||||
// 由于实际场景中会出现name重复的recording rule,所以不需要检查重复
|
||||
//exists, err := RecordingRuleExists(0, re.GroupId, re.Cluster, re.Name)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//
|
||||
//if exists {
|
||||
// return errors.New("RecordingRule already exists")
|
||||
//}
|
||||
|
||||
now := time.Now().Unix()
|
||||
re.CreateAt = now
|
||||
@@ -95,15 +101,16 @@ func (re *RecordingRule) Add() error {
|
||||
}
|
||||
|
||||
func (re *RecordingRule) Update(ref RecordingRule) error {
|
||||
if re.Name != ref.Name {
|
||||
exists, err := RecordingRuleExists("group_id=? and cluster=? and name=? and id <> ?", re.GroupId, re.Cluster, ref.Name, re.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return errors.New("RecordingRule already exists")
|
||||
}
|
||||
}
|
||||
// 由于实际场景中会出现name重复的recording rule,所以不需要检查重复
|
||||
//if re.Name != ref.Name {
|
||||
// exists, err := RecordingRuleExists(re.Id, re.GroupId, re.Cluster, ref.Name)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if exists {
|
||||
// return errors.New("RecordingRule already exists")
|
||||
// }
|
||||
//}
|
||||
|
||||
ref.FE2DB()
|
||||
ref.Id = re.Id
|
||||
@@ -133,9 +140,27 @@ func RecordingRuleDels(ids []int64, groupId int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func RecordingRuleExists(where string, regs ...interface{}) (bool, error) {
|
||||
return Exists(DB().Model(&RecordingRule{}).Where(where, regs...))
|
||||
func RecordingRuleExists(id, groupId int64, cluster, name string) (bool, error) {
|
||||
session := DB().Where("id <> ? and group_id = ? and name =? ", id, groupId, name)
|
||||
|
||||
var lst []RecordingRule
|
||||
err := session.Find(&lst).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(lst) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// match cluster
|
||||
for _, r := range lst {
|
||||
if MatchCluster(r.Cluster, cluster) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func RecordingRuleGets(groupId int64) ([]RecordingRule, error) {
|
||||
session := DB().Where("group_id=?", groupId).Order("name")
|
||||
|
||||
@@ -171,26 +196,45 @@ func RecordingRuleGetById(id int64) (*RecordingRule, error) {
|
||||
}
|
||||
|
||||
func RecordingRuleGetsByCluster(cluster string) ([]*RecordingRule, error) {
|
||||
session := DB()
|
||||
session := DB().Where("disabled = ?", 0)
|
||||
|
||||
if cluster != "" {
|
||||
session = session.Where("cluster = ?", cluster)
|
||||
session = session.Where("(cluster like ? or cluster = ?)", "%"+cluster+"%", ClusterAll)
|
||||
}
|
||||
|
||||
var lst []*RecordingRule
|
||||
err := session.Find(&lst).Error
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
return lst, err
|
||||
}
|
||||
|
||||
if len(lst) == 0 {
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
if cluster == "" {
|
||||
for i := 0; i < len(lst); i++ {
|
||||
lst[i].DB2FE()
|
||||
}
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
return lst, err
|
||||
lr := make([]*RecordingRule, 0, len(lst))
|
||||
for _, r := range lst {
|
||||
if MatchCluster(r.Cluster, cluster) {
|
||||
r.DB2FE()
|
||||
lr = append(lr, r)
|
||||
}
|
||||
}
|
||||
|
||||
return lr, err
|
||||
}
|
||||
|
||||
func RecordingRuleStatistics(cluster string) (*Statistics, error) {
|
||||
session := DB().Model(&RecordingRule{}).Select("count(*) as total", "max(update_at) as last_updated")
|
||||
if cluster != "" {
|
||||
session = session.Where("cluster = ?", cluster)
|
||||
// 简略的判断,当一个clustername是另一个clustername的substring的时候,会出现stats与预期不符,不影响使用
|
||||
session = session.Where("(cluster like ? or cluster = ?)", "%"+cluster+"%", ClusterAll)
|
||||
}
|
||||
|
||||
var stats []*Statistics
|
||||
|
||||
198
src/models/relabel.go
Normal file
198
src/models/relabel.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/prompb"
|
||||
)
|
||||
|
||||
const (
|
||||
Replace Action = "replace"
|
||||
Keep Action = "keep"
|
||||
Drop Action = "drop"
|
||||
HashMod Action = "hashmod"
|
||||
LabelMap Action = "labelmap"
|
||||
LabelDrop Action = "labeldrop"
|
||||
LabelKeep Action = "labelkeep"
|
||||
Lowercase Action = "lowercase"
|
||||
Uppercase Action = "uppercase"
|
||||
)
|
||||
|
||||
type Action string
|
||||
|
||||
type Regexp struct {
|
||||
*regexp.Regexp
|
||||
}
|
||||
|
||||
type RelabelConfig struct {
|
||||
SourceLabels model.LabelNames
|
||||
Separator string
|
||||
Regex interface{}
|
||||
Modulus uint64
|
||||
TargetLabel string
|
||||
Replacement string
|
||||
Action Action
|
||||
}
|
||||
|
||||
func Process(labels []*prompb.Label, cfgs ...*RelabelConfig) []*prompb.Label {
|
||||
for _, cfg := range cfgs {
|
||||
labels = relabel(labels, cfg)
|
||||
if labels == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
func getValue(ls []*prompb.Label, name model.LabelName) string {
|
||||
for _, l := range ls {
|
||||
if l.Name == string(name) {
|
||||
return l.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type LabelBuilder struct {
|
||||
LabelSet map[string]string
|
||||
}
|
||||
|
||||
func newBuilder(ls []*prompb.Label) *LabelBuilder {
|
||||
lset := make(map[string]string, len(ls))
|
||||
for _, l := range ls {
|
||||
lset[l.Name] = l.Value
|
||||
}
|
||||
return &LabelBuilder{LabelSet: lset}
|
||||
}
|
||||
|
||||
func (l *LabelBuilder) set(k, v string) *LabelBuilder {
|
||||
if v == "" {
|
||||
return l.del(k)
|
||||
}
|
||||
|
||||
l.LabelSet[k] = v
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *LabelBuilder) del(ns ...string) *LabelBuilder {
|
||||
for _, n := range ns {
|
||||
delete(l.LabelSet, n)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *LabelBuilder) labels() []*prompb.Label {
|
||||
ls := make([]*prompb.Label, 0, len(l.LabelSet))
|
||||
if len(l.LabelSet) == 0 {
|
||||
return ls
|
||||
}
|
||||
|
||||
for k, v := range l.LabelSet {
|
||||
ls = append(ls, &prompb.Label{
|
||||
Name: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(ls, func(i, j int) bool {
|
||||
return ls[i].Name > ls[j].Name
|
||||
})
|
||||
return ls
|
||||
}
|
||||
|
||||
func relabel(lset []*prompb.Label, cfg *RelabelConfig) []*prompb.Label {
|
||||
values := make([]string, 0, len(cfg.SourceLabels))
|
||||
for _, ln := range cfg.SourceLabels {
|
||||
values = append(values, getValue(lset, ln))
|
||||
}
|
||||
|
||||
regx := cfg.Regex.(Regexp)
|
||||
|
||||
val := strings.Join(values, cfg.Separator)
|
||||
lb := newBuilder(lset)
|
||||
switch cfg.Action {
|
||||
case Drop:
|
||||
if regx.MatchString(val) {
|
||||
return nil
|
||||
}
|
||||
case Keep:
|
||||
if !regx.MatchString(val) {
|
||||
return nil
|
||||
}
|
||||
case Replace:
|
||||
indexes := regx.FindStringSubmatchIndex(val)
|
||||
if indexes == nil {
|
||||
break
|
||||
}
|
||||
target := model.LabelName(regx.ExpandString([]byte{}, cfg.TargetLabel, val, indexes))
|
||||
if !target.IsValid() {
|
||||
lb.del(cfg.TargetLabel)
|
||||
break
|
||||
}
|
||||
res := regx.ExpandString([]byte{}, cfg.Replacement, val, indexes)
|
||||
if len(res) == 0 {
|
||||
lb.del(cfg.TargetLabel)
|
||||
break
|
||||
}
|
||||
lb.set(string(target), string(res))
|
||||
case Lowercase:
|
||||
lb.set(cfg.TargetLabel, strings.ToLower(val))
|
||||
case Uppercase:
|
||||
lb.set(cfg.TargetLabel, strings.ToUpper(val))
|
||||
case HashMod:
|
||||
mod := sum64(md5.Sum([]byte(val))) % cfg.Modulus
|
||||
lb.set(cfg.TargetLabel, fmt.Sprintf("%d", mod))
|
||||
case LabelMap:
|
||||
for _, l := range lset {
|
||||
if regx.MatchString(l.Name) {
|
||||
res := regx.ReplaceAllString(l.Name, cfg.Replacement)
|
||||
lb.set(res, l.Value)
|
||||
}
|
||||
}
|
||||
case LabelDrop:
|
||||
for _, l := range lset {
|
||||
if regx.MatchString(l.Name) {
|
||||
lb.del(l.Name)
|
||||
}
|
||||
}
|
||||
case LabelKeep:
|
||||
for _, l := range lset {
|
||||
if !regx.MatchString(l.Name) {
|
||||
lb.del(l.Name)
|
||||
}
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("relabel: unknown relabel action type %q", cfg.Action))
|
||||
}
|
||||
|
||||
return lb.labels()
|
||||
}
|
||||
|
||||
func sum64(hash [md5.Size]byte) uint64 {
|
||||
var s uint64
|
||||
|
||||
for i, b := range hash {
|
||||
shift := uint64((md5.Size - i - 1) * 8)
|
||||
|
||||
s |= uint64(b) << shift
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func NewRegexp(s string) (Regexp, error) {
|
||||
regex, err := regexp.Compile("^(?:" + s + ")$")
|
||||
return Regexp{Regexp: regex}, err
|
||||
}
|
||||
|
||||
func MustNewRegexp(s string) Regexp {
|
||||
re, err := NewRegexp(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return re
|
||||
}
|
||||
@@ -20,6 +20,11 @@ type Target struct {
|
||||
TagsJSON []string `json:"tags" gorm:"-"`
|
||||
TagsMap map[string]string `json:"-" gorm:"-"` // internal use, append tags to series
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
|
||||
TargetUp float64 `json:"target_up" gorm:"-"`
|
||||
LoadPerCore float64 `json:"load_per_core" gorm:"-"`
|
||||
MemUtil float64 `json:"mem_util" gorm:"-"`
|
||||
DiskUtil float64 `json:"disk_util" gorm:"-"`
|
||||
}
|
||||
|
||||
func (t *Target) TableName() string {
|
||||
@@ -111,6 +116,10 @@ func buildTargetWhere(bgid int64, clusters []string, query string) *gorm.DB {
|
||||
return session
|
||||
}
|
||||
|
||||
func TargetTotalCount() (int64, error) {
|
||||
return Count(DB().Model(new(Target)))
|
||||
}
|
||||
|
||||
func TargetTotal(bgid int64, clusters []string, query string) (int64, error) {
|
||||
return Count(buildTargetWhere(bgid, clusters, query))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/slice"
|
||||
"github.com/toolkits/pkg/str"
|
||||
"gorm.io/gorm"
|
||||
@@ -16,6 +18,22 @@ import (
|
||||
"github.com/didi/nightingale/v5/src/webapi/config"
|
||||
)
|
||||
|
||||
const (
|
||||
Dingtalk = "dingtalk"
|
||||
Wecom = "wecom"
|
||||
Feishu = "feishu"
|
||||
FeishuCard = "feishucard"
|
||||
Mm = "mm"
|
||||
Telegram = "telegram"
|
||||
Email = "email"
|
||||
|
||||
DingtalkKey = "dingtalk_robot_token"
|
||||
WecomKey = "wecom_robot_token"
|
||||
FeishuKey = "feishu_robot_token"
|
||||
MmKey = "mm_webhook_url"
|
||||
TelegramKey = "telegram_robot_token"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
Username string `json:"username"`
|
||||
@@ -95,7 +113,7 @@ func (u *User) Update(selectField interface{}, selectFields ...interface{}) erro
|
||||
return err
|
||||
}
|
||||
|
||||
return DB().Model(u).Select(selectField, selectFields).Updates(u).Error
|
||||
return DB().Model(u).Select(selectField, selectFields...).Updates(u).Error
|
||||
}
|
||||
|
||||
func (u *User) UpdateAllFields() error {
|
||||
@@ -450,6 +468,25 @@ func (u *User) BusiGroups(limit int, query string, all ...bool) ([]BusiGroup, er
|
||||
var lst []BusiGroup
|
||||
if u.IsAdmin() || (len(all) > 0 && all[0]) {
|
||||
err := session.Where("name like ?", "%"+query+"%").Find(&lst).Error
|
||||
if err != nil {
|
||||
return lst, err
|
||||
}
|
||||
|
||||
if len(lst) == 0 && len(query) > 0 {
|
||||
// 隐藏功能,一般人不告诉,哈哈。query可能是给的ident,所以上面的sql没有查到,当做ident来查一下试试
|
||||
var t *Target
|
||||
t, err = TargetGet("ident=?", query)
|
||||
if err != nil {
|
||||
return lst, err
|
||||
}
|
||||
|
||||
if t == nil {
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
err = DB().Order("name").Limit(limit).Where("id=?", t.GroupId).Find(&lst).Error
|
||||
}
|
||||
|
||||
return lst, err
|
||||
}
|
||||
|
||||
@@ -468,6 +505,22 @@ func (u *User) BusiGroups(limit int, query string, all ...bool) ([]BusiGroup, er
|
||||
}
|
||||
|
||||
err = session.Where("id in ?", busiGroupIds).Where("name like ?", "%"+query+"%").Find(&lst).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(lst) == 0 && len(query) > 0 {
|
||||
var t *Target
|
||||
t, err = TargetGet("ident=?", query)
|
||||
if err != nil {
|
||||
return lst, err
|
||||
}
|
||||
|
||||
if slice.ContainsInt64(busiGroupIds, t.GroupId) {
|
||||
err = DB().Order("name").Limit(limit).Where("id=?", t.GroupId).Find(&lst).Error
|
||||
}
|
||||
}
|
||||
|
||||
return lst, err
|
||||
}
|
||||
|
||||
@@ -477,6 +530,23 @@ func (u *User) UserGroups(limit int, query string) ([]UserGroup, error) {
|
||||
var lst []UserGroup
|
||||
if u.IsAdmin() {
|
||||
err := session.Where("name like ?", "%"+query+"%").Find(&lst).Error
|
||||
if err != nil {
|
||||
return lst, err
|
||||
}
|
||||
|
||||
if len(lst) == 0 && len(query) > 0 {
|
||||
// 隐藏功能,一般人不告诉,哈哈。query可能是给的用户名,所以上面的sql没有查到,当做user来查一下试试
|
||||
user, err := UserGetByUsername(query)
|
||||
if user == nil {
|
||||
return lst, err
|
||||
}
|
||||
var ids []int64
|
||||
ids, err = MyGroupIds(user.Id)
|
||||
if err != nil || len(ids) == 0 {
|
||||
return lst, err
|
||||
}
|
||||
lst, err = UserGroupGetByIds(ids)
|
||||
}
|
||||
return lst, err
|
||||
}
|
||||
|
||||
@@ -494,3 +564,57 @@ func (u *User) UserGroups(limit int, query string) ([]UserGroup, error) {
|
||||
err = session.Where("name like ?", "%"+query+"%").Find(&lst).Error
|
||||
return lst, err
|
||||
}
|
||||
|
||||
func (u *User) ExtractToken(key string) (string, bool) {
|
||||
bs, err := u.Contacts.MarshalJSON()
|
||||
if err != nil {
|
||||
logger.Errorf("handle_notice: failed to marshal contacts: %v", err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
switch key {
|
||||
case Dingtalk:
|
||||
ret := gjson.GetBytes(bs, DingtalkKey)
|
||||
return ret.String(), ret.Exists()
|
||||
case Wecom:
|
||||
ret := gjson.GetBytes(bs, WecomKey)
|
||||
return ret.String(), ret.Exists()
|
||||
case Feishu:
|
||||
ret := gjson.GetBytes(bs, FeishuKey)
|
||||
return ret.String(), ret.Exists()
|
||||
case FeishuCard:
|
||||
ret := gjson.GetBytes(bs, FeishuKey)
|
||||
return ret.String(), ret.Exists()
|
||||
case Mm:
|
||||
ret := gjson.GetBytes(bs, MmKey)
|
||||
return ret.String(), ret.Exists()
|
||||
case Telegram:
|
||||
ret := gjson.GetBytes(bs, TelegramKey)
|
||||
return ret.String(), ret.Exists()
|
||||
case Email:
|
||||
return u.Email, u.Email != ""
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) ExtractAllToken() map[string]string {
|
||||
ret := make(map[string]string)
|
||||
if u.Email != "" {
|
||||
ret[Email] = u.Email
|
||||
}
|
||||
|
||||
bs, err := u.Contacts.MarshalJSON()
|
||||
if err != nil {
|
||||
logger.Errorf("handle_notice: failed to marshal contacts: %v", err)
|
||||
return ret
|
||||
}
|
||||
|
||||
ret[Dingtalk] = gjson.GetBytes(bs, DingtalkKey).String()
|
||||
ret[Wecom] = gjson.GetBytes(bs, WecomKey).String()
|
||||
ret[Feishu] = gjson.GetBytes(bs, FeishuKey).String()
|
||||
ret[FeishuCard] = gjson.GetBytes(bs, FeishuKey).String()
|
||||
ret[Mm] = gjson.GetBytes(bs, MmKey).String()
|
||||
ret[Telegram] = gjson.GetBytes(bs, TelegramKey).String()
|
||||
return ret
|
||||
}
|
||||
|
||||
9
src/notifier/notifier.go
Normal file
9
src/notifier/notifier.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package notifier
|
||||
|
||||
type Notifier interface {
|
||||
Descript() string
|
||||
Notify([]byte)
|
||||
NotifyMaintainer([]byte)
|
||||
}
|
||||
|
||||
var Instance Notifier
|
||||
169
src/pkg/cas/cas.go
Normal file
169
src/pkg/cas/cas.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package cas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/storage"
|
||||
"github.com/google/uuid"
|
||||
"github.com/toolkits/pkg/cas"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Enable bool
|
||||
SsoAddr string
|
||||
LoginPath string
|
||||
RedirectURL string
|
||||
DisplayName string
|
||||
CoverAttributes bool
|
||||
Attributes struct {
|
||||
Nickname string
|
||||
Phone string
|
||||
Email string
|
||||
}
|
||||
DefaultRoles []string
|
||||
}
|
||||
|
||||
type ssoClient struct {
|
||||
config Config
|
||||
ssoAddr string
|
||||
callbackAddr string
|
||||
displayName string
|
||||
attributes struct {
|
||||
nickname string
|
||||
phone string
|
||||
email string
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
cli ssoClient
|
||||
)
|
||||
|
||||
func Init(cf Config) {
|
||||
if !cf.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
cli = ssoClient{}
|
||||
cli.config = cf
|
||||
cli.ssoAddr = cf.SsoAddr
|
||||
cli.callbackAddr = cf.RedirectURL
|
||||
cli.displayName = cf.DisplayName
|
||||
cli.attributes.nickname = cf.Attributes.Nickname
|
||||
cli.attributes.phone = cf.Attributes.Phone
|
||||
cli.attributes.email = cf.Attributes.Email
|
||||
}
|
||||
|
||||
func GetDisplayName() string {
|
||||
return cli.displayName
|
||||
}
|
||||
|
||||
// Authorize return the cas authorize location and state
|
||||
func Authorize(redirect string) (string, string, error) {
|
||||
state := uuid.New().String()
|
||||
ctx := context.Background()
|
||||
err := storage.Redis.Set(ctx, wrapStateKey(state), redirect, time.Duration(300*time.Second)).Err()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return cli.genRedirectURL(state), state, nil
|
||||
}
|
||||
|
||||
func fetchRedirect(ctx context.Context, state string) (string, error) {
|
||||
return storage.Redis.Get(ctx, wrapStateKey(state)).Result()
|
||||
}
|
||||
|
||||
func deleteRedirect(ctx context.Context, state string) error {
|
||||
return storage.Redis.Del(ctx, wrapStateKey(state)).Err()
|
||||
}
|
||||
|
||||
func wrapStateKey(key string) string {
|
||||
return "n9e_cas_" + key
|
||||
}
|
||||
|
||||
func (cli *ssoClient) genRedirectURL(state string) string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
ssoAddr, err := url.Parse(cli.config.SsoAddr)
|
||||
if cli.config.LoginPath == "" {
|
||||
if strings.Contains(cli.config.SsoAddr, "p3") {
|
||||
ssoAddr.Path = "login"
|
||||
} else {
|
||||
ssoAddr.Path = "cas/login"
|
||||
}
|
||||
} else {
|
||||
ssoAddr.Path = cli.config.LoginPath
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
buf.WriteString(ssoAddr.String())
|
||||
v := url.Values{
|
||||
"service": {cli.callbackAddr},
|
||||
}
|
||||
if strings.Contains(cli.ssoAddr, "?") {
|
||||
buf.WriteByte('&')
|
||||
} else {
|
||||
buf.WriteByte('?')
|
||||
}
|
||||
buf.WriteString(v.Encode())
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type CallbackOutput struct {
|
||||
Redirect string `json:"redirect"`
|
||||
Msg string `json:"msg"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Phone string `yaml:"phone"`
|
||||
Email string `yaml:"email"`
|
||||
}
|
||||
|
||||
func ValidateServiceTicket(ctx context.Context, ticket, state string) (ret *CallbackOutput, err error) {
|
||||
casUrl, err := url.Parse(cli.config.SsoAddr)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
serviceUrl, err := url.Parse(cli.callbackAddr)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
resOptions := &cas.RestOptions{
|
||||
CasURL: casUrl,
|
||||
ServiceURL: serviceUrl,
|
||||
}
|
||||
resCli := cas.NewRestClient(resOptions)
|
||||
authRet, err := resCli.ValidateServiceTicket(cas.ServiceTicket(ticket))
|
||||
if err != nil {
|
||||
logger.Errorf("Ticket Validating Failed: %s", err)
|
||||
return
|
||||
}
|
||||
ret = &CallbackOutput{}
|
||||
ret.Username = authRet.User
|
||||
ret.Nickname = authRet.Attributes.Get(cli.attributes.nickname)
|
||||
logger.Debugf("CAS Authentication Response's Attributes--[Nickname]: %s", ret.Nickname)
|
||||
ret.Email = authRet.Attributes.Get(cli.attributes.email)
|
||||
logger.Debugf("CAS Authentication Response's Attributes--[Email]: %s", ret.Email)
|
||||
ret.Phone = authRet.Attributes.Get(cli.attributes.phone)
|
||||
logger.Debugf("CAS Authentication Response's Attributes--[Phone]: %s", ret.Phone)
|
||||
ret.Redirect, err = fetchRedirect(ctx, state)
|
||||
if err != nil {
|
||||
logger.Debugf("get redirect err:%s state:%s", state, err)
|
||||
}
|
||||
err = deleteRedirect(ctx, state)
|
||||
if err != nil {
|
||||
logger.Debugf("delete redirect err:%s state:%s", state, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
225
src/pkg/oauth2x/oauth2x.go
Normal file
225
src/pkg/oauth2x/oauth2x.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package oauth2x
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/storage"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"github.com/google/uuid"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type ssoClient struct {
|
||||
config oauth2.Config
|
||||
ssoAddr string
|
||||
userInfoAddr string
|
||||
TranTokenMethod string
|
||||
callbackAddr string
|
||||
displayName string
|
||||
coverAttributes bool
|
||||
attributes struct {
|
||||
username string
|
||||
nickname string
|
||||
phone string
|
||||
email string
|
||||
}
|
||||
userinfoIsArray bool
|
||||
userinfoPrefix string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Enable bool
|
||||
DisplayName string
|
||||
RedirectURL string
|
||||
SsoAddr string
|
||||
TokenAddr string
|
||||
UserInfoAddr string
|
||||
TranTokenMethod string
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
CoverAttributes bool
|
||||
Attributes struct {
|
||||
Username string
|
||||
Nickname string
|
||||
Phone string
|
||||
Email string
|
||||
}
|
||||
DefaultRoles []string
|
||||
UserinfoIsArray bool
|
||||
UserinfoPrefix string
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
var (
|
||||
cli ssoClient
|
||||
)
|
||||
|
||||
func Init(cf Config) {
|
||||
if !cf.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
cli.ssoAddr = cf.SsoAddr
|
||||
cli.userInfoAddr = cf.UserInfoAddr
|
||||
cli.TranTokenMethod = cf.TranTokenMethod
|
||||
cli.callbackAddr = cf.RedirectURL
|
||||
cli.displayName = cf.DisplayName
|
||||
cli.coverAttributes = cf.CoverAttributes
|
||||
cli.attributes.username = cf.Attributes.Username
|
||||
cli.attributes.nickname = cf.Attributes.Nickname
|
||||
cli.attributes.phone = cf.Attributes.Phone
|
||||
cli.attributes.email = cf.Attributes.Email
|
||||
cli.userinfoIsArray = cf.UserinfoIsArray
|
||||
cli.userinfoPrefix = cf.UserinfoPrefix
|
||||
|
||||
cli.config = oauth2.Config{
|
||||
ClientID: cf.ClientId,
|
||||
ClientSecret: cf.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: cf.SsoAddr,
|
||||
TokenURL: cf.TokenAddr,
|
||||
},
|
||||
RedirectURL: cf.RedirectURL,
|
||||
Scopes: cf.Scopes,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDisplayName() string {
|
||||
return cli.displayName
|
||||
}
|
||||
|
||||
func wrapStateKey(key string) string {
|
||||
return "n9e_oauth_" + key
|
||||
}
|
||||
|
||||
// Authorize return the sso authorize location with state
|
||||
func Authorize(redirect string) (string, error) {
|
||||
state := uuid.New().String()
|
||||
ctx := context.Background()
|
||||
|
||||
err := storage.Redis.Set(ctx, wrapStateKey(state), redirect, time.Duration(300*time.Second)).Err()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return cli.config.AuthCodeURL(state), nil
|
||||
}
|
||||
|
||||
func fetchRedirect(ctx context.Context, state string) (string, error) {
|
||||
return storage.Redis.Get(ctx, wrapStateKey(state)).Result()
|
||||
}
|
||||
|
||||
func deleteRedirect(ctx context.Context, state string) error {
|
||||
return storage.Redis.Del(ctx, wrapStateKey(state)).Err()
|
||||
}
|
||||
|
||||
// Callback 用 code 兑换 accessToken 以及 用户信息
|
||||
func Callback(ctx context.Context, code, state string) (*CallbackOutput, error) {
|
||||
ret, err := exchangeUser(code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ilegal user:%v", err)
|
||||
}
|
||||
ret.Redirect, err = fetchRedirect(ctx, state)
|
||||
if err != nil {
|
||||
logger.Errorf("get redirect err:%v code:%s state:%s", code, state, err)
|
||||
}
|
||||
|
||||
err = deleteRedirect(ctx, state)
|
||||
if err != nil {
|
||||
logger.Errorf("delete redirect err:%v code:%s state:%s", code, state, err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
type CallbackOutput struct {
|
||||
Redirect string `json:"redirect"`
|
||||
Msg string `json:"msg"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Phone string `yaml:"phone"`
|
||||
Email string `yaml:"email"`
|
||||
}
|
||||
|
||||
func exchangeUser(code string) (*CallbackOutput, error) {
|
||||
ctx := context.Background()
|
||||
oauth2Token, err := cli.config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to exchange token: %s", err)
|
||||
}
|
||||
|
||||
userInfo, err := getUserInfo(cli.userInfoAddr, oauth2Token.AccessToken, cli.TranTokenMethod)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to get user info: %s", err)
|
||||
return nil, fmt.Errorf("failed to get user info: %s", err)
|
||||
}
|
||||
|
||||
return &CallbackOutput{
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
Username: getUserinfoField(userInfo, cli.userinfoIsArray, cli.userinfoPrefix, cli.attributes.username),
|
||||
Nickname: getUserinfoField(userInfo, cli.userinfoIsArray, cli.userinfoPrefix, cli.attributes.nickname),
|
||||
Phone: getUserinfoField(userInfo, cli.userinfoIsArray, cli.userinfoPrefix, cli.attributes.phone),
|
||||
Email: getUserinfoField(userInfo, cli.userinfoIsArray, cli.userinfoPrefix, cli.attributes.email),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getUserInfo(userInfoAddr, accessToken string, TranTokenMethod string) ([]byte, error) {
|
||||
var req *http.Request
|
||||
if TranTokenMethod == "formdata" {
|
||||
body := bytes.NewBuffer([]byte("access_token=" + accessToken))
|
||||
r, err := http.NewRequest("POST", userInfoAddr, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req = r
|
||||
} else if TranTokenMethod == "querystring" {
|
||||
r, err := http.NewRequest("GET", userInfoAddr+"?access_token="+accessToken, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Add("Authorization", "Bearer "+accessToken)
|
||||
req = r
|
||||
} else {
|
||||
r, err := http.NewRequest("GET", userInfoAddr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Add("Authorization", "Bearer "+accessToken)
|
||||
req = r
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return body, err
|
||||
}
|
||||
|
||||
func getUserinfoField(input []byte, isArray bool, prefix, field string) string {
|
||||
if prefix == "" {
|
||||
if isArray {
|
||||
return jsoniter.Get(input, 0).Get(field).ToString()
|
||||
} else {
|
||||
return jsoniter.Get(input, field).ToString()
|
||||
}
|
||||
} else {
|
||||
if isArray {
|
||||
return jsoniter.Get(input, prefix, 0).Get(field).ToString()
|
||||
} else {
|
||||
return jsoniter.Get(input, prefix).Get(field).ToString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type ssoClient struct {
|
||||
ssoAddr string
|
||||
callbackAddr string
|
||||
coverAttributes bool
|
||||
displayName string
|
||||
attributes struct {
|
||||
username string
|
||||
nickname string
|
||||
@@ -30,6 +31,7 @@ type ssoClient struct {
|
||||
|
||||
type Config struct {
|
||||
Enable bool
|
||||
DisplayName string
|
||||
RedirectURL string
|
||||
SsoAddr string
|
||||
ClientId string
|
||||
@@ -59,6 +61,7 @@ func Init(cf Config) {
|
||||
cli.attributes.nickname = cf.Attributes.Nickname
|
||||
cli.attributes.phone = cf.Attributes.Phone
|
||||
cli.attributes.email = cf.Attributes.Email
|
||||
cli.displayName = cf.DisplayName
|
||||
provider, err := oidc.NewProvider(context.Background(), cf.SsoAddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -77,6 +80,10 @@ func Init(cf Config) {
|
||||
}
|
||||
}
|
||||
|
||||
func GetDisplayName() string {
|
||||
return cli.displayName
|
||||
}
|
||||
|
||||
func wrapStateKey(key string) string {
|
||||
return "n9e_oidc_" + key
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func PostJSON(url string, timeout time.Duration, v interface{}) (response []byte, code int, err error) {
|
||||
func PostJSON(url string, timeout time.Duration, v interface{}, retries ...int) (response []byte, code int, err error) {
|
||||
var bs []byte
|
||||
|
||||
bs, err = json.Marshal(v)
|
||||
@@ -26,7 +28,29 @@ func PostJSON(url string, timeout time.Duration, v interface{}) (response []byte
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
var resp *http.Response
|
||||
resp, err = client.Do(req)
|
||||
|
||||
if len(retries) > 0 {
|
||||
for i := 0; i < retries[0]; i++ {
|
||||
resp, err = client.Do(req)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
tryagain := ""
|
||||
if i+1 < retries[0] {
|
||||
tryagain = " try again"
|
||||
}
|
||||
|
||||
logger.Warningf("failed to curl %s error: %s"+tryagain, url, err)
|
||||
|
||||
if i+1 < retries[0] {
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resp, err = client.Do(req)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
// Package v1 provides bindings to the Prometheus HTTP API v1:
|
||||
// http://prometheus.io/docs/querying/api/
|
||||
package reader
|
||||
package prom
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -558,10 +558,11 @@ func (qr *queryResult) UnmarshalJSON(b []byte) error {
|
||||
// NewAPI returns a new API for the client.
|
||||
//
|
||||
// It is safe to use the returned API from multiple goroutines.
|
||||
func NewAPI(c api.Client) API {
|
||||
func NewAPI(c api.Client, opt ClientOptions) API {
|
||||
return &httpAPI{
|
||||
client: &apiClientImpl{
|
||||
client: c,
|
||||
opt: opt,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -891,6 +892,7 @@ type apiClient interface {
|
||||
|
||||
type apiClientImpl struct {
|
||||
client api.Client
|
||||
opt ClientOptions
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
@@ -921,16 +923,16 @@ func (h *apiClientImpl) URL(ep string, args map[string]string) *url.URL {
|
||||
}
|
||||
|
||||
func (h *apiClientImpl) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, Warnings, error) {
|
||||
if Reader.Opts.BasicAuthUser != "" && Reader.Opts.BasicAuthPass != "" {
|
||||
req.SetBasicAuth(Reader.Opts.BasicAuthUser, Reader.Opts.BasicAuthPass)
|
||||
if h.opt.BasicAuthUser != "" && h.opt.BasicAuthPass != "" {
|
||||
req.SetBasicAuth(h.opt.BasicAuthUser, h.opt.BasicAuthPass)
|
||||
}
|
||||
|
||||
headerCount := len(Reader.Opts.Headers)
|
||||
headerCount := len(h.opt.Headers)
|
||||
if headerCount > 0 && headerCount%2 == 0 {
|
||||
for i := 0; i < len(Reader.Opts.Headers); i += 2 {
|
||||
req.Header.Add(Reader.Opts.Headers[i], Reader.Opts.Headers[i+1])
|
||||
if Reader.Opts.Headers[i] == "Host" {
|
||||
req.Host = Reader.Opts.Headers[i+1]
|
||||
for i := 0; i < len(h.opt.Headers); i += 2 {
|
||||
req.Header.Add(h.opt.Headers[i], h.opt.Headers[i+1])
|
||||
if h.opt.Headers[i] == "Host" {
|
||||
req.Host = h.opt.Headers[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/pkg/prom/client_option.go
Normal file
7
src/pkg/prom/client_option.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package prom
|
||||
|
||||
type ClientOptions struct {
|
||||
BasicAuthUser string
|
||||
BasicAuthPass string
|
||||
Headers []string
|
||||
}
|
||||
100
src/pkg/secu/aes.go
Normal file
100
src/pkg/secu/aes.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package secu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BASE64StdEncode base64编码
|
||||
func BASE64StdEncode(src []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(src)
|
||||
}
|
||||
|
||||
// BASE64StdDecode base64解码
|
||||
func BASE64StdDecode(src string) ([]byte, error) {
|
||||
dst, err := base64.StdEncoding.DecodeString(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
func PKCS7UnPadding(originData []byte) []byte {
|
||||
length := len(originData)
|
||||
unpadding := int(originData[length-1])
|
||||
return originData[:(length - unpadding)]
|
||||
}
|
||||
|
||||
//AES加密
|
||||
func AesEncrypt(origData, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//加密块填充
|
||||
blockSize := block.BlockSize()
|
||||
padOrigData := PKCS7Padding(origData, blockSize)
|
||||
//初始化CBC加密
|
||||
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
||||
crypted := make([]byte, len(padOrigData))
|
||||
//加密
|
||||
blockMode.CryptBlocks(crypted, padOrigData)
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
//AES解密
|
||||
func AesDecrypt(crypted, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
|
||||
origData := make([]byte, len(crypted))
|
||||
//解密
|
||||
blockMode.CryptBlocks(origData, crypted)
|
||||
//去除填充
|
||||
origData = PKCS7UnPadding(origData)
|
||||
return origData, nil
|
||||
}
|
||||
|
||||
// 针对配置文件属性进行解密处理
|
||||
func DealWithDecrypt(src string, key string) (string, error) {
|
||||
//如果是{{cipher}}前缀,则代表是加密过的属性,先解密
|
||||
if strings.HasPrefix(src, "{{cipher}}") {
|
||||
data := src[10:]
|
||||
decodeData, err := BASE64StdDecode(data)
|
||||
if err != nil {
|
||||
return src, err
|
||||
}
|
||||
//解密
|
||||
origin, err := AesDecrypt(decodeData, []byte(key))
|
||||
if err != nil {
|
||||
return src, err
|
||||
}
|
||||
//返回明文
|
||||
return string(origin), nil
|
||||
} else {
|
||||
return src, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 针对配置文件属性进行加密处理
|
||||
func DealWithEncrypt(src string, key string) (string, error) {
|
||||
encrypted, err := AesEncrypt([]byte(src), []byte(key))
|
||||
if err != nil {
|
||||
return src, err
|
||||
}
|
||||
|
||||
data := BASE64StdEncode(encrypted)
|
||||
return "{{cipher}}" + data, nil
|
||||
}
|
||||
@@ -12,25 +12,26 @@ import (
|
||||
|
||||
// ClientConfig represents the standard client TLS config.
|
||||
type ClientConfig struct {
|
||||
TLSCA string
|
||||
TLSCert string
|
||||
TLSKey string
|
||||
TLSKeyPwd string
|
||||
InsecureSkipVerify bool
|
||||
ServerName string
|
||||
TLSMinVersion string
|
||||
TLSCA string `toml:"tls_ca"`
|
||||
TLSCert string `toml:"tls_cert"`
|
||||
TLSKey string `toml:"tls_key"`
|
||||
TLSKeyPwd string `toml:"tls_key_pwd"`
|
||||
InsecureSkipVerify bool `toml:"insecure_skip_verify"`
|
||||
ServerName string `toml:"tls_server_name"`
|
||||
TLSMinVersion string `toml:"tls_min_version"`
|
||||
TLSMaxVersion string `toml:"tls_max_version"`
|
||||
}
|
||||
|
||||
// ServerConfig represents the standard server TLS config.
|
||||
type ServerConfig struct {
|
||||
TLSCert string
|
||||
TLSKey string
|
||||
TLSKeyPwd string
|
||||
TLSAllowedCACerts []string
|
||||
TLSCipherSuites []string
|
||||
TLSMinVersion string
|
||||
TLSMaxVersion string
|
||||
TLSAllowedDNSNames []string
|
||||
TLSCert string `toml:"tls_cert"`
|
||||
TLSKey string `toml:"tls_key"`
|
||||
TLSKeyPwd string `toml:"tls_key_pwd"`
|
||||
TLSAllowedCACerts []string `toml:"tls_allowed_cacerts"`
|
||||
TLSCipherSuites []string `toml:"tls_cipher_suites"`
|
||||
TLSMinVersion string `toml:"tls_min_version"`
|
||||
TLSMaxVersion string `toml:"tls_max_version"`
|
||||
TLSAllowedDNSNames []string `toml:"tls_allowed_dns_names"`
|
||||
}
|
||||
|
||||
// TLSConfig returns a tls.Config, may be nil without error if TLS is not
|
||||
@@ -70,6 +71,16 @@ func (c *ClientConfig) TLSConfig() (*tls.Config, error) {
|
||||
tlsConfig.MinVersion = tls.VersionTLS13
|
||||
}
|
||||
|
||||
if c.TLSMaxVersion == "1.0" {
|
||||
tlsConfig.MaxVersion = tls.VersionTLS10
|
||||
} else if c.TLSMaxVersion == "1.1" {
|
||||
tlsConfig.MaxVersion = tls.VersionTLS11
|
||||
} else if c.TLSMaxVersion == "1.2" {
|
||||
tlsConfig.MaxVersion = tls.VersionTLS12
|
||||
} else if c.TLSMaxVersion == "1.3" {
|
||||
tlsConfig.MaxVersion = tls.VersionTLS13
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -33,6 +34,10 @@ func Timestamp(pattern ...string) string {
|
||||
return time.Now().Format(defp)
|
||||
}
|
||||
|
||||
func Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func Args(args ...interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for i, a := range args {
|
||||
@@ -95,11 +100,27 @@ func Humanize1024(s string) string {
|
||||
return fmt.Sprintf("%.4g%s", v, prefix)
|
||||
}
|
||||
|
||||
func ToString(v interface{}) string {
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
func HumanizeDuration(s string) string {
|
||||
v, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return HumanizeDurationFloat64(v)
|
||||
}
|
||||
|
||||
func HumanizeDurationInterface(i interface{}) string {
|
||||
f, err := ToFloat64(i)
|
||||
if err != nil {
|
||||
return ToString(i)
|
||||
}
|
||||
return HumanizeDurationFloat64(f)
|
||||
}
|
||||
|
||||
func HumanizeDurationFloat64(v float64) string {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return fmt.Sprintf("%.4g", v)
|
||||
}
|
||||
@@ -155,3 +176,179 @@ func HumanizePercentageH(s string) string {
|
||||
}
|
||||
return fmt.Sprintf("%.2f%%", v)
|
||||
}
|
||||
|
||||
// Add returns the sum of a and b.
|
||||
func Add(a, b interface{}) (interface{}, error) {
|
||||
av := reflect.ValueOf(a)
|
||||
bv := reflect.ValueOf(b)
|
||||
|
||||
switch av.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return av.Int() + bv.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Int() + int64(bv.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(av.Int()) + bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("add: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return int64(av.Uint()) + bv.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Uint() + bv.Uint(), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(av.Uint()) + bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("add: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return av.Float() + float64(bv.Int()), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Float() + float64(bv.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return av.Float() + bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("add: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("add: unknown type for %q (%T)", av, a)
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract returns the difference of b from a.
|
||||
func Subtract(a, b interface{}) (interface{}, error) {
|
||||
av := reflect.ValueOf(a)
|
||||
bv := reflect.ValueOf(b)
|
||||
|
||||
switch av.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return av.Int() - bv.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Int() - int64(bv.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(av.Int()) - bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("subtract: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return int64(av.Uint()) - bv.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Uint() - bv.Uint(), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(av.Uint()) - bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("subtract: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return av.Float() - float64(bv.Int()), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Float() - float64(bv.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return av.Float() - bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("subtract: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("subtract: unknown type for %q (%T)", av, a)
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply returns the product of a and b.
|
||||
func Multiply(a, b interface{}) (interface{}, error) {
|
||||
av := reflect.ValueOf(a)
|
||||
bv := reflect.ValueOf(b)
|
||||
|
||||
switch av.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return av.Int() * bv.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Int() * int64(bv.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(av.Int()) * bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("multiply: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return int64(av.Uint()) * bv.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Uint() * bv.Uint(), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(av.Uint()) * bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("multiply: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return av.Float() * float64(bv.Int()), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Float() * float64(bv.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return av.Float() * bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("multiply: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("multiply: unknown type for %q (%T)", av, a)
|
||||
}
|
||||
}
|
||||
|
||||
// Divide returns the division of b from a.
|
||||
func Divide(a, b interface{}) (interface{}, error) {
|
||||
av := reflect.ValueOf(a)
|
||||
bv := reflect.ValueOf(b)
|
||||
|
||||
switch av.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return av.Int() / bv.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Int() / int64(bv.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(av.Int()) / bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("divide: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return int64(av.Uint()) / bv.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Uint() / bv.Uint(), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return float64(av.Uint()) / bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("divide: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return av.Float() / float64(bv.Int()), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return av.Float() / float64(bv.Uint()), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return av.Float() / bv.Float(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("divide: unknown type for %q (%T)", bv, b)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("divide: unknown type for %q (%T)", av, a)
|
||||
}
|
||||
}
|
||||
|
||||
73
src/pkg/tplx/conv.go
Normal file
73
src/pkg/tplx/conv.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package tplx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ToFloat64 convert interface to float64
|
||||
func ToFloat64(val interface{}) (float64, error) {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// try int
|
||||
if i, err := strconv.ParseInt(v, 0, 64); err == nil {
|
||||
return float64(i), nil
|
||||
}
|
||||
|
||||
// try bool
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err == nil {
|
||||
if b {
|
||||
return 1, nil
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
if v == "Yes" || v == "yes" || v == "YES" || v == "Y" || v == "ON" || v == "on" || v == "On" || v == "ok" || v == "up" {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
if v == "No" || v == "no" || v == "NO" || v == "N" || v == "OFF" || v == "off" || v == "Off" || v == "fail" || v == "err" || v == "down" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unparseable value %v", v)
|
||||
case float64:
|
||||
return v, nil
|
||||
case uint64:
|
||||
return float64(v), nil
|
||||
case uint32:
|
||||
return float64(v), nil
|
||||
case uint16:
|
||||
return float64(v), nil
|
||||
case uint8:
|
||||
return float64(v), nil
|
||||
case uint:
|
||||
return float64(v), nil
|
||||
case int64:
|
||||
return float64(v), nil
|
||||
case int32:
|
||||
return float64(v), nil
|
||||
case int16:
|
||||
return float64(v), nil
|
||||
case int8:
|
||||
return float64(v), nil
|
||||
case bool:
|
||||
if v {
|
||||
return 1, nil
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
case int:
|
||||
return float64(v), nil
|
||||
case float32:
|
||||
return float64(v), nil
|
||||
default:
|
||||
return strconv.ParseFloat(fmt.Sprint(v), 64)
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,33 @@ package tplx
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var TemplateFuncMap = template.FuncMap{
|
||||
"unescaped": Unescaped,
|
||||
"urlconvert": Urlconvert,
|
||||
"timeformat": Timeformat,
|
||||
"timestamp": Timestamp,
|
||||
"args": Args,
|
||||
"reReplaceAll": ReReplaceAll,
|
||||
"match": regexp.MatchString,
|
||||
"toUpper": strings.ToUpper,
|
||||
"toLower": strings.ToLower,
|
||||
"contains": strings.Contains,
|
||||
"humanize": Humanize,
|
||||
"humanize1024": Humanize1024,
|
||||
"humanizeDuration": HumanizeDuration,
|
||||
"humanizePercentage": HumanizePercentage,
|
||||
"humanizePercentageH": HumanizePercentageH,
|
||||
"escape": url.PathEscape,
|
||||
"unescaped": Unescaped,
|
||||
"urlconvert": Urlconvert,
|
||||
"timeformat": Timeformat,
|
||||
"timestamp": Timestamp,
|
||||
"args": Args,
|
||||
"reReplaceAll": ReReplaceAll,
|
||||
"match": regexp.MatchString,
|
||||
"toUpper": strings.ToUpper,
|
||||
"toLower": strings.ToLower,
|
||||
"contains": strings.Contains,
|
||||
"humanize": Humanize,
|
||||
"humanize1024": Humanize1024,
|
||||
"humanizeDuration": HumanizeDuration,
|
||||
"humanizeDurationInterface": HumanizeDurationInterface,
|
||||
"humanizePercentage": HumanizePercentage,
|
||||
"humanizePercentageH": HumanizePercentageH,
|
||||
"add": Add,
|
||||
"sub": Subtract,
|
||||
"mul": Multiply,
|
||||
"div": Divide,
|
||||
"now": Now,
|
||||
"toString": ToString,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package conv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
@@ -13,6 +15,12 @@ type Vector struct {
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
func (v *Vector) ReadableValue() string {
|
||||
ret := fmt.Sprintf("%.5f", v.Value)
|
||||
ret = strings.TrimRight(ret, "0")
|
||||
return strings.TrimRight(ret, ".")
|
||||
}
|
||||
|
||||
func ConvertVectors(value model.Value) (lst []Vector) {
|
||||
if value == nil {
|
||||
return
|
||||
|
||||
@@ -96,6 +96,9 @@ func labelsToLabelsProto(labels model.Metric, rule *models.RecordingRule) (resul
|
||||
}
|
||||
result = append(result, nameLs)
|
||||
for k, v := range labels {
|
||||
if k == LabelName {
|
||||
continue
|
||||
}
|
||||
if model.LabelNameRE.MatchString(string(k)) {
|
||||
result = append(result, &prompb.Label{
|
||||
Name: string(k),
|
||||
|
||||
@@ -12,13 +12,31 @@ func AppendLabels(pt *prompb.TimeSeries, target *models.Target) {
|
||||
return
|
||||
}
|
||||
|
||||
labelKeys := make(map[string]int)
|
||||
for j := 0; j < len(pt.Labels); j++ {
|
||||
labelKeys[pt.Labels[j].Name] = j
|
||||
}
|
||||
|
||||
for key, value := range target.TagsMap {
|
||||
if index, has := labelKeys[key]; has {
|
||||
// overwrite labels
|
||||
if config.C.LabelRewrite {
|
||||
pt.Labels[index].Value = value
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
pt.Labels = append(pt.Labels, &prompb.Label{
|
||||
Name: key,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
// e.g. busigroup=cloud
|
||||
if _, has := labelKeys[config.C.BusiGroupLabelKey]; has {
|
||||
return
|
||||
}
|
||||
// 将业务组名称作为tag附加到数据上
|
||||
if target.GroupId > 0 && len(config.C.BusiGroupLabelKey) > 0 {
|
||||
bg := memsto.BusiGroupCache.GetByBusiGroupId(target.GroupId)
|
||||
if bg == nil {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package engine
|
||||
package sender
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
@@ -14,8 +14,7 @@ import (
|
||||
"github.com/didi/nightingale/v5/src/server/memsto"
|
||||
)
|
||||
|
||||
func callback(event *models.AlertCurEvent) {
|
||||
urls := strings.Fields(event.Callbacks)
|
||||
func SendCallbacks(urls []string, event *models.AlertCurEvent) {
|
||||
for _, url := range urls {
|
||||
if url == "" {
|
||||
continue
|
||||
@@ -32,7 +31,7 @@ func callback(event *models.AlertCurEvent) {
|
||||
url = "http://" + url
|
||||
}
|
||||
|
||||
resp, code, err := poster.PostJSON(url, 5*time.Second, event)
|
||||
resp, code, err := poster.PostJSON(url, 5*time.Second, event, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback(rule_id=%d url=%s) fail, resp: %s, err: %v, code: %d", event.RuleId, url, string(resp), err, code)
|
||||
} else {
|
||||
@@ -1,20 +1,15 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/pkg/poster"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type DingtalkMessage struct {
|
||||
Title string
|
||||
Text string
|
||||
AtMobiles []string
|
||||
Tokens []string
|
||||
}
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/pkg/poster"
|
||||
)
|
||||
|
||||
type dingtalkMarkdown struct {
|
||||
Title string `json:"title"`
|
||||
@@ -32,45 +27,91 @@ type dingtalk struct {
|
||||
At dingtalkAt `json:"at"`
|
||||
}
|
||||
|
||||
func SendDingtalk(message DingtalkMessage) {
|
||||
ats := make([]string, len(message.AtMobiles))
|
||||
for i := 0; i < len(message.AtMobiles); i++ {
|
||||
ats[i] = "@" + message.AtMobiles[i]
|
||||
type DingtalkSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ds *DingtalkSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < len(message.Tokens); i++ {
|
||||
u, err := url.Parse(message.Tokens[i])
|
||||
if err != nil {
|
||||
logger.Errorf("dingtalk_sender: failed to parse error=%v", err)
|
||||
}
|
||||
urls, ats := ds.extract(ctx.Users)
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
message := BuildTplMessage(ds.tpl, ctx.Event)
|
||||
|
||||
v, err := url.ParseQuery(u.RawQuery)
|
||||
if err != nil {
|
||||
logger.Errorf("dingtalk_sender: failed to parse query error=%v", err)
|
||||
}
|
||||
|
||||
ur := "https://oapi.dingtalk.com/robot/send?access_token=" + u.Path
|
||||
body := dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: message.Title,
|
||||
Text: message.Text,
|
||||
},
|
||||
}
|
||||
|
||||
if v.Get("noat") != "1" {
|
||||
body.Markdown.Text = message.Text + " " + strings.Join(ats, " ")
|
||||
body.At = dingtalkAt{
|
||||
AtMobiles: message.AtMobiles,
|
||||
IsAtAll: false,
|
||||
for _, url := range urls {
|
||||
var body dingtalk
|
||||
// NoAt in url
|
||||
if strings.Contains(url, "noat=1") {
|
||||
body = dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: ctx.Rule.Name,
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
body = dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: ctx.Rule.Name,
|
||||
Text: message + " " + strings.Join(ats, " "),
|
||||
},
|
||||
At: dingtalkAt{
|
||||
AtMobiles: ats,
|
||||
IsAtAll: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
res, code, err := poster.PostJSON(ur, time.Second*5, body)
|
||||
if err != nil {
|
||||
logger.Errorf("dingtalk_sender: result=fail url=%s code=%d error=%v response=%s", ur, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("dingtalk_sender: result=succ url=%s code=%d response=%s", ur, code, string(res))
|
||||
}
|
||||
ds.doSend(url, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DingtalkSender) SendRaw(users []*models.User, title, message string) {
|
||||
if len(users) == 0 {
|
||||
return
|
||||
}
|
||||
urls, _ := ds.extract(users)
|
||||
body := dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: title,
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
for _, url := range urls {
|
||||
ds.doSend(url, body)
|
||||
}
|
||||
}
|
||||
|
||||
// extract urls and ats from Users
|
||||
func (ds *DingtalkSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0, len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if user.Phone != "" {
|
||||
ats = append(ats, "@"+user.Phone)
|
||||
}
|
||||
if token, has := user.ExtractToken(models.Dingtalk); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") {
|
||||
url = "https://oapi.dingtalk.com/robot/send?access_token=" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
|
||||
func (ds *DingtalkSender) doSend(url string, body dingtalk) {
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("dingtalk_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("dingtalk_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,54 @@ package sender
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/server/config"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"gopkg.in/gomail.v2"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/server/config"
|
||||
)
|
||||
|
||||
var mailch chan *gomail.Message
|
||||
|
||||
type EmailSender struct {
|
||||
subjectTpl *template.Template
|
||||
contentTpl *template.Template
|
||||
}
|
||||
|
||||
func (es *EmailSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
return
|
||||
}
|
||||
tos := es.extract(ctx.Users)
|
||||
var subject string
|
||||
|
||||
if es.subjectTpl != nil {
|
||||
subject = BuildTplMessage(es.subjectTpl, ctx.Event)
|
||||
} else {
|
||||
subject = ctx.Rule.Name
|
||||
}
|
||||
content := BuildTplMessage(es.contentTpl, ctx.Event)
|
||||
WriteEmail(subject, content, tos)
|
||||
}
|
||||
|
||||
func (es *EmailSender) SendRaw(users []*models.User, title, message string) {
|
||||
tos := es.extract(users)
|
||||
WriteEmail(title, message, tos)
|
||||
}
|
||||
|
||||
func (es *EmailSender) extract(users []*models.User) []string {
|
||||
tos := make([]string, 0, len(users))
|
||||
for _, u := range users {
|
||||
if u.Email != "" {
|
||||
tos = append(tos, u.Email)
|
||||
}
|
||||
}
|
||||
return tos
|
||||
}
|
||||
|
||||
func SendEmail(subject, content string, tos []string) {
|
||||
conf := config.C.SMTP
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/pkg/poster"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type FeishuMessage struct {
|
||||
Text string
|
||||
AtMobiles []string
|
||||
Tokens []string
|
||||
}
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/pkg/poster"
|
||||
)
|
||||
|
||||
type feishuContent struct {
|
||||
Text string `json:"text"`
|
||||
@@ -28,25 +26,73 @@ type feishu struct {
|
||||
At feishuAt `json:"at"`
|
||||
}
|
||||
|
||||
func SendFeishu(message FeishuMessage) {
|
||||
for i := 0; i < len(message.Tokens); i++ {
|
||||
url := "https://open.feishu.cn/open-apis/bot/v2/hook/" + message.Tokens[i]
|
||||
type FeishuSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
return
|
||||
}
|
||||
urls, ats := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(fs.tpl, ctx.Event)
|
||||
for _, url := range urls {
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
Text: message.Text,
|
||||
Text: message,
|
||||
},
|
||||
At: feishuAt{
|
||||
AtMobiles: message.AtMobiles,
|
||||
}
|
||||
if !strings.Contains(url, "noat=1") {
|
||||
body.At = feishuAt{
|
||||
AtMobiles: ats,
|
||||
IsAtAll: false,
|
||||
},
|
||||
}
|
||||
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body)
|
||||
if err != nil {
|
||||
logger.Errorf("feishu_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("feishu_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
}
|
||||
fs.doSend(url, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) SendRaw(users []*models.User, title, message string) {
|
||||
if len(users) == 0 {
|
||||
return
|
||||
}
|
||||
urls, _ := fs.extract(users)
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
for _, url := range urls {
|
||||
fs.doSend(url, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0, len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if user.Phone != "" {
|
||||
ats = append(ats, user.Phone)
|
||||
}
|
||||
if token, has := user.ExtractToken(models.Feishu); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") {
|
||||
url = "https://open.feishu.cn/open-apis/bot/v2/hook/" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) doSend(url string, body feishu) {
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("feishu_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("feishu_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
}
|
||||
|
||||
161
src/server/common/sender/feishu_card.go
Normal file
161
src/server/common/sender/feishu_card.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/pkg/poster"
|
||||
)
|
||||
|
||||
type feishuCardContent struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
WideScreenMode bool `json:"wide_screen_mode"`
|
||||
EnableForward bool `json:"enable_forward"`
|
||||
}
|
||||
|
||||
type Te struct {
|
||||
Content string `json:"content"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
type Element struct {
|
||||
Tag string `json:"tag"`
|
||||
Text Te `json:"text"`
|
||||
Content string `json:"content"`
|
||||
Elements []Element `json:"elements"`
|
||||
}
|
||||
|
||||
type Titles struct {
|
||||
Content string `json:"content"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
type Headers struct {
|
||||
Title Titles `json:"title"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
type Cards struct {
|
||||
Config Conf `json:"config"`
|
||||
Elements []Element `json:"elements"`
|
||||
Header Headers `json:"header"`
|
||||
}
|
||||
|
||||
type feishuCard struct {
|
||||
Msgtype string `json:"msg_type"`
|
||||
Content feishuCardContent `json:"content"`
|
||||
Card Cards `json:"card"`
|
||||
}
|
||||
|
||||
type FeishuCardSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (fs *FeishuCardSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
return
|
||||
}
|
||||
urls, _ := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(fs.tpl, ctx.Event)
|
||||
for _, url := range urls {
|
||||
var color string
|
||||
if strings.Count(message, "Recovered") > 0 && strings.Count(message, "Triggered") > 0 {
|
||||
color = "orange"
|
||||
} else if strings.Count(message, "Recovered") > 0 {
|
||||
color = "green"
|
||||
} else {
|
||||
color = "red"
|
||||
}
|
||||
SendTitle := fmt.Sprintf("🔔 [告警提醒] - %s", ctx.Rule.Name)
|
||||
body := feishuCard{
|
||||
Msgtype: "interactive",
|
||||
Card: Cards{
|
||||
Config: Conf{
|
||||
WideScreenMode: true,
|
||||
EnableForward: true,
|
||||
},
|
||||
Header: Headers{
|
||||
Title: Titles{
|
||||
Content: SendTitle,
|
||||
Tag: "plain_text",
|
||||
},
|
||||
Template: color,
|
||||
},
|
||||
Elements: []Element{
|
||||
Element{
|
||||
Tag: "div",
|
||||
Text: Te{
|
||||
Content: message,
|
||||
Tag: "lark_md",
|
||||
},
|
||||
},
|
||||
{
|
||||
Tag: "hr",
|
||||
},
|
||||
{
|
||||
Tag: "note",
|
||||
Elements: []Element{
|
||||
{
|
||||
Content: SendTitle,
|
||||
Tag: "lark_md",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
fs.doSend(url, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FeishuCardSender) SendRaw(users []*models.User, title, message string) {
|
||||
if len(users) == 0 {
|
||||
return
|
||||
}
|
||||
urls, _ := fs.extract(users)
|
||||
body := feishuCard{
|
||||
Msgtype: "text",
|
||||
Content: feishuCardContent{
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
for _, url := range urls {
|
||||
fs.doSend(url, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FeishuCardSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0, len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if user.Phone != "" {
|
||||
ats = append(ats, user.Phone)
|
||||
}
|
||||
if token, has := user.ExtractToken(models.FeishuCard); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") {
|
||||
url = "https://open.feishu.cn/open-apis/bot/v2/hook/" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
|
||||
func (fs *FeishuCardSender) doSend(url string, body feishuCard) {
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("feishu_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("feishu_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
}
|
||||
117
src/server/common/sender/mm.go
Normal file
117
src/server/common/sender/mm.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/pkg/poster"
|
||||
)
|
||||
|
||||
type MatterMostMessage struct {
|
||||
Text string
|
||||
Tokens []string
|
||||
}
|
||||
|
||||
type mm struct {
|
||||
Channel string `json:"channel"`
|
||||
Username string `json:"username"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type MmSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ms *MmSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
return
|
||||
}
|
||||
|
||||
urls := ms.extract(ctx.Users)
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
message := BuildTplMessage(ms.tpl, ctx.Event)
|
||||
|
||||
SendMM(MatterMostMessage{
|
||||
Text: message,
|
||||
Tokens: urls,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MmSender) SendRaw(users []*models.User, title, message string) {
|
||||
urls := ms.extract(users)
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
SendMM(MatterMostMessage{
|
||||
Text: message,
|
||||
Tokens: urls,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MmSender) extract(users []*models.User) []string {
|
||||
tokens := make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Mm); has {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
func SendMM(message MatterMostMessage) {
|
||||
for i := 0; i < len(message.Tokens); i++ {
|
||||
u, err := url.Parse(message.Tokens[i])
|
||||
if err != nil {
|
||||
logger.Errorf("mm_sender: failed to parse error=%v", err)
|
||||
}
|
||||
|
||||
v, err := url.ParseQuery(u.RawQuery)
|
||||
if err != nil {
|
||||
logger.Errorf("mm_sender: failed to parse query error=%v", err)
|
||||
}
|
||||
|
||||
channels := v["channel"] // do not get
|
||||
txt := ""
|
||||
atuser := v["atuser"]
|
||||
if len(atuser) != 0 {
|
||||
txt = strings.Join(MapStrToStr(atuser, func(u string) string {
|
||||
return "@" + u
|
||||
}), ",") + "\n"
|
||||
}
|
||||
username := v.Get("username")
|
||||
if err != nil {
|
||||
logger.Errorf("mm_sender: failed to parse error=%v", err)
|
||||
}
|
||||
// simple concatenating
|
||||
ur := u.Scheme + "://" + u.Host + u.Path
|
||||
for _, channel := range channels {
|
||||
body := mm{
|
||||
Channel: channel,
|
||||
Username: username,
|
||||
Text: txt + message.Text,
|
||||
}
|
||||
|
||||
res, code, err := poster.PostJSON(ur, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("mm_sender: result=fail url=%s code=%d error=%v response=%s", ur, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("mm_sender: result=succ url=%s code=%d response=%s", ur, code, string(res))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MapStrToStr(arr []string, fn func(s string) string) []string {
|
||||
var newArray = []string{}
|
||||
for _, it := range arr {
|
||||
newArray = append(newArray, fn(it))
|
||||
}
|
||||
return newArray
|
||||
}
|
||||
77
src/server/common/sender/plugin.go
Normal file
77
src/server/common/sender/plugin.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/notifier"
|
||||
"github.com/didi/nightingale/v5/src/pkg/sys"
|
||||
"github.com/didi/nightingale/v5/src/server/config"
|
||||
)
|
||||
|
||||
func MayPluginNotify(noticeBytes []byte) {
|
||||
if len(noticeBytes) == 0 {
|
||||
return
|
||||
}
|
||||
alertingCallPlugin(noticeBytes)
|
||||
alertingCallScript(noticeBytes)
|
||||
}
|
||||
|
||||
func alertingCallScript(stdinBytes []byte) {
|
||||
// not enable or no notify.py? do nothing
|
||||
if !config.C.Alerting.CallScript.Enable || config.C.Alerting.CallScript.ScriptPath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
fpath := config.C.Alerting.CallScript.ScriptPath
|
||||
cmd := exec.Command(fpath)
|
||||
cmd.Stdin = bytes.NewReader(stdinBytes)
|
||||
|
||||
// combine stdout and stderr
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &buf
|
||||
|
||||
err := startCmd(cmd)
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: run cmd err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err, isTimeout := sys.WrapTimeout(cmd, time.Duration(config.C.Alerting.Timeout)*time.Millisecond)
|
||||
|
||||
if isTimeout {
|
||||
if err == nil {
|
||||
logger.Errorf("event_notify: timeout and killed process %s", fpath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: kill process %s occur error %v", fpath, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: exec script %s occur error: %v, output: %s", fpath, err, buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("event_notify: exec %s output: %s", fpath, buf.String())
|
||||
}
|
||||
|
||||
// call notify.so via golang plugin build
|
||||
// ig. etc/script/notify/notify.so
|
||||
func alertingCallPlugin(stdinBytes []byte) {
|
||||
if !config.C.Alerting.CallPlugin.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("alertingCallPlugin begin")
|
||||
logger.Debugf("payload:", string(stdinBytes))
|
||||
notifier.Instance.Notify(stdinBytes)
|
||||
logger.Debugf("alertingCallPlugin done")
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package engine
|
||||
package sender
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
@@ -1,4 +1,4 @@
|
||||
package engine
|
||||
package sender
|
||||
|
||||
import "os/exec"
|
||||
|
||||
26
src/server/common/sender/redis_pub.go
Normal file
26
src/server/common/sender/redis_pub.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/server/config"
|
||||
"github.com/didi/nightingale/v5/src/storage"
|
||||
)
|
||||
|
||||
func PublishToRedis(clusterName string, bs []byte) {
|
||||
if len(bs) == 0 {
|
||||
return
|
||||
}
|
||||
if !config.C.Alerting.RedisPub.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
// pub all alerts to redis
|
||||
channelKey := config.C.Alerting.RedisPub.ChannelPrefix + clusterName
|
||||
err := storage.Redis.Publish(context.Background(), channelKey, bs).Err()
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: redis publish %s err: %v", channelKey, err)
|
||||
}
|
||||
}
|
||||
73
src/server/common/sender/sender.go
Normal file
73
src/server/common/sender/sender.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
|
||||
"github.com/toolkits/pkg/slice"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/server/config"
|
||||
"github.com/didi/nightingale/v5/src/server/memsto"
|
||||
)
|
||||
|
||||
type (
|
||||
// Sender 发送消息通知的接口
|
||||
Sender interface {
|
||||
Send(ctx MessageContext)
|
||||
|
||||
// SendRaw 发送原始消息,目前在notifyMaintainer时使用
|
||||
SendRaw(users []*models.User, title, message string)
|
||||
}
|
||||
|
||||
// MessageContext 一个event所生成的告警通知的上下文
|
||||
MessageContext struct {
|
||||
Users []*models.User
|
||||
Rule *models.AlertRule
|
||||
Event *models.AlertCurEvent
|
||||
}
|
||||
)
|
||||
|
||||
func NewSender(key string, tpls map[string]*template.Template) Sender {
|
||||
if !slice.ContainsString(config.C.Alerting.NotifyBuiltinChannels, key) {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch key {
|
||||
case models.Dingtalk:
|
||||
return &DingtalkSender{tpl: tpls["dingtalk.tpl"]}
|
||||
case models.Wecom:
|
||||
return &WecomSender{tpl: tpls["wecom.tpl"]}
|
||||
case models.Feishu:
|
||||
return &FeishuSender{tpl: tpls["feishu.tpl"]}
|
||||
case models.FeishuCard:
|
||||
return &FeishuCardSender{tpl: tpls["feishucard.tpl"]}
|
||||
case models.Email:
|
||||
return &EmailSender{subjectTpl: tpls["subject.tpl"], contentTpl: tpls["mailbody.tpl"]}
|
||||
case models.Mm:
|
||||
return &MmSender{tpl: tpls["mm.tpl"]}
|
||||
case models.Telegram:
|
||||
return &TelegramSender{tpl: tpls["telegram.tpl"]}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildMessageContext(rule *models.AlertRule, event *models.AlertCurEvent, uids []int64) MessageContext {
|
||||
users := memsto.UserCache.GetByUserIds(uids)
|
||||
return MessageContext{
|
||||
Rule: rule,
|
||||
Event: event,
|
||||
Users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildTplMessage(tpl *template.Template, event *models.AlertCurEvent) string {
|
||||
if tpl == nil {
|
||||
return "tpl for current sender not found, please check configuration"
|
||||
}
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, event); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return body.String()
|
||||
}
|
||||
90
src/server/common/sender/telegram.go
Normal file
90
src/server/common/sender/telegram.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/pkg/poster"
|
||||
)
|
||||
|
||||
type TelegramMessage struct {
|
||||
Text string
|
||||
Tokens []string
|
||||
}
|
||||
|
||||
type telegram struct {
|
||||
ParseMode string `json:"parse_mode"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type TelegramSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
return
|
||||
}
|
||||
tokens := ts.extract(ctx.Users)
|
||||
message := BuildTplMessage(ts.tpl, ctx.Event)
|
||||
|
||||
SendTelegram(TelegramMessage{
|
||||
Text: message,
|
||||
Tokens: tokens,
|
||||
})
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) SendRaw(users []*models.User, title, message string) {
|
||||
tokens := ts.extract(users)
|
||||
SendTelegram(TelegramMessage{
|
||||
Text: message,
|
||||
Tokens: tokens,
|
||||
})
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) extract(users []*models.User) []string {
|
||||
tokens := make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Telegram); has {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
func SendTelegram(message TelegramMessage) {
|
||||
for i := 0; i < len(message.Tokens); i++ {
|
||||
if !strings.Contains(message.Tokens[i], "/") && !strings.HasPrefix(message.Tokens[i], "https://") {
|
||||
logger.Errorf("telegram_sender: result=fail invalid token=%s", message.Tokens[i])
|
||||
continue
|
||||
}
|
||||
var url string
|
||||
if strings.HasPrefix(message.Tokens[i], "https://") {
|
||||
url = message.Tokens[i]
|
||||
} else {
|
||||
array := strings.Split(message.Tokens[i], "/")
|
||||
if len(array) != 2 {
|
||||
logger.Errorf("telegram_sender: result=fail invalid token=%s", message.Tokens[i])
|
||||
continue
|
||||
}
|
||||
botToken := array[0]
|
||||
chatId := array[1]
|
||||
url = "https://api.telegram.org/bot" + botToken + "/sendMessage?chat_id=" + chatId
|
||||
}
|
||||
body := telegram{
|
||||
ParseMode: "markdown",
|
||||
Text: message.Text,
|
||||
}
|
||||
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("telegram_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("telegram_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/server/common/sender/webhook.go
Normal file
66
src/server/common/sender/webhook.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/server/config"
|
||||
)
|
||||
|
||||
func SendWebhooks(webhooks []config.Webhook, event *models.AlertCurEvent) {
|
||||
for _, conf := range webhooks {
|
||||
if conf.Url == "" || !conf.Enable {
|
||||
continue
|
||||
}
|
||||
bs, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bf := bytes.NewBuffer(bs)
|
||||
|
||||
req, err := http.NewRequest("POST", conf.Url, bf)
|
||||
if err != nil {
|
||||
logger.Warning("alertingWebhook failed to new request", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if conf.BasicAuthUser != "" && conf.BasicAuthPass != "" {
|
||||
req.SetBasicAuth(conf.BasicAuthUser, conf.BasicAuthPass)
|
||||
}
|
||||
|
||||
if len(conf.Headers) > 0 && len(conf.Headers)%2 == 0 {
|
||||
for i := 0; i < len(conf.Headers); i += 2 {
|
||||
if conf.Headers[i] == "host" {
|
||||
req.Host = conf.Headers[i+1]
|
||||
continue
|
||||
}
|
||||
req.Header.Set(conf.Headers[i], conf.Headers[i+1])
|
||||
}
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: conf.TimeoutDuration,
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
logger.Warningf("WebhookCallError, ruleId: [%d], eventId: [%d], url: [%s], error: [%s]", event.RuleId, event.Id, conf.Url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ = ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
logger.Debugf("alertingWebhook done, url: %s, response code: %d, body: %s", conf.Url, resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/pkg/poster"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type WecomMessage struct {
|
||||
Text string
|
||||
Tokens []string
|
||||
}
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/pkg/poster"
|
||||
)
|
||||
|
||||
type wecomMarkdown struct {
|
||||
Content string `json:"content"`
|
||||
@@ -21,21 +20,59 @@ type wecom struct {
|
||||
Markdown wecomMarkdown `json:"markdown"`
|
||||
}
|
||||
|
||||
func SendWecom(message WecomMessage) {
|
||||
for i := 0; i < len(message.Tokens); i++ {
|
||||
url := "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + message.Tokens[i]
|
||||
type WecomSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ws *WecomSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
return
|
||||
}
|
||||
urls := ws.extract(ctx.Users)
|
||||
message := BuildTplMessage(ws.tpl, ctx.Event)
|
||||
for _, url := range urls {
|
||||
body := wecom{
|
||||
Msgtype: "markdown",
|
||||
Markdown: wecomMarkdown{
|
||||
Content: message.Text,
|
||||
Content: message,
|
||||
},
|
||||
}
|
||||
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body)
|
||||
if err != nil {
|
||||
logger.Errorf("wecom_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("wecom_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
ws.doSend(url, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WecomSender) SendRaw(users []*models.User, title, message string) {
|
||||
urls := ws.extract(users)
|
||||
for _, url := range urls {
|
||||
body := wecom{
|
||||
Msgtype: "markdown",
|
||||
Markdown: wecomMarkdown{
|
||||
Content: message,
|
||||
},
|
||||
}
|
||||
ws.doSend(url, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WecomSender) extract(users []*models.User) []string {
|
||||
urls := make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Wecom); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") {
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func (ws *WecomSender) doSend(url string, body wecom) {
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("wecom_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("wecom_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,30 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"plugin"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/koding/multiconfig"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/toolkits/pkg/file"
|
||||
"github.com/toolkits/pkg/runner"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/notifier"
|
||||
"github.com/didi/nightingale/v5/src/pkg/httpx"
|
||||
"github.com/didi/nightingale/v5/src/pkg/logx"
|
||||
"github.com/didi/nightingale/v5/src/pkg/ormx"
|
||||
"github.com/didi/nightingale/v5/src/pkg/secu"
|
||||
"github.com/didi/nightingale/v5/src/pkg/tplx"
|
||||
"github.com/didi/nightingale/v5/src/storage"
|
||||
)
|
||||
|
||||
@@ -22,7 +34,68 @@ var (
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func MustLoad(fpaths ...string) {
|
||||
func DealConfigCrypto(key string) {
|
||||
decryptDsn, err := secu.DealWithDecrypt(C.DB.DSN, key)
|
||||
if err != nil {
|
||||
fmt.Println("failed to decrypt the db dsn", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
C.DB.DSN = decryptDsn
|
||||
|
||||
decryptRedisPwd, err := secu.DealWithDecrypt(C.Redis.Password, key)
|
||||
if err != nil {
|
||||
fmt.Println("failed to decrypt the redis password", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
C.Redis.Password = decryptRedisPwd
|
||||
|
||||
decryptSmtpPwd, err := secu.DealWithDecrypt(C.SMTP.Pass, key)
|
||||
if err != nil {
|
||||
fmt.Println("failed to decrypt the smtp password", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
C.SMTP.Pass = decryptSmtpPwd
|
||||
|
||||
decryptHookPwd, err := secu.DealWithDecrypt(C.Alerting.Webhook.BasicAuthPass, key)
|
||||
if err != nil {
|
||||
fmt.Println("failed to decrypt the alert webhook password", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
C.Alerting.Webhook.BasicAuthPass = decryptHookPwd
|
||||
|
||||
decryptIbexPwd, err := secu.DealWithDecrypt(C.Ibex.BasicAuthPass, key)
|
||||
if err != nil {
|
||||
fmt.Println("failed to decrypt the ibex password", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
C.Ibex.BasicAuthPass = decryptIbexPwd
|
||||
|
||||
if len(C.Readers) == 0 {
|
||||
C.Reader.ClusterName = C.ClusterName
|
||||
C.Readers = append(C.Readers, C.Reader)
|
||||
}
|
||||
|
||||
for index, v := range C.Readers {
|
||||
decryptReaderPwd, err := secu.DealWithDecrypt(v.BasicAuthPass, key)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to decrypt the reader password: %s , error: %s", v.BasicAuthPass, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
C.Readers[index].BasicAuthPass = decryptReaderPwd
|
||||
}
|
||||
|
||||
for index, v := range C.Writers {
|
||||
decryptWriterPwd, err := secu.DealWithDecrypt(v.BasicAuthPass, key)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to decrypt the writer password: %s , error: %s", v.BasicAuthPass, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
C.Writers[index].BasicAuthPass = decryptWriterPwd
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func MustLoad(key string, fpaths ...string) {
|
||||
once.Do(func() {
|
||||
loaders := []multiconfig.Loader{
|
||||
&multiconfig.TagLoader{},
|
||||
@@ -61,10 +134,21 @@ func MustLoad(fpaths ...string) {
|
||||
}
|
||||
m.MustLoad(C)
|
||||
|
||||
DealConfigCrypto(key)
|
||||
|
||||
if C.EngineDelay == 0 {
|
||||
C.EngineDelay = 120
|
||||
}
|
||||
|
||||
if C.ReaderFrom == "" {
|
||||
C.ReaderFrom = "config"
|
||||
}
|
||||
|
||||
if C.ReaderFrom == "config" && C.ClusterName == "" {
|
||||
fmt.Println("configuration ClusterName is blank")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if C.Heartbeat.IP == "" {
|
||||
// auto detect
|
||||
// C.Heartbeat.IP = fmt.Sprint(GetOutboundIP())
|
||||
@@ -76,7 +160,11 @@ func MustLoad(fpaths ...string) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
C.Heartbeat.IP = hostname + "+" + fmt.Sprint(os.Getpid())
|
||||
if strings.Contains(hostname, "localhost") {
|
||||
fmt.Println("Warning! hostname contains substring localhost, setting a more unique hostname is recommended")
|
||||
}
|
||||
|
||||
C.Heartbeat.IP = hostname
|
||||
|
||||
// if C.Heartbeat.IP == "" {
|
||||
// fmt.Println("heartbeat ip auto got is blank")
|
||||
@@ -85,23 +173,11 @@ func MustLoad(fpaths ...string) {
|
||||
}
|
||||
|
||||
C.Heartbeat.Endpoint = fmt.Sprintf("%s:%d", C.Heartbeat.IP, C.HTTP.Port)
|
||||
C.Alerting.RedisPub.ChannelKey = C.Alerting.RedisPub.ChannelPrefix + C.ClusterName
|
||||
|
||||
if C.Alerting.Webhook.Enable {
|
||||
if C.Alerting.Webhook.Timeout == "" {
|
||||
C.Alerting.Webhook.TimeoutDuration = time.Second * 5
|
||||
} else {
|
||||
dur, err := time.ParseDuration(C.Alerting.Webhook.Timeout)
|
||||
if err != nil {
|
||||
fmt.Println("failed to parse Alerting.Webhook.Timeout")
|
||||
os.Exit(1)
|
||||
}
|
||||
C.Alerting.Webhook.TimeoutDuration = dur
|
||||
}
|
||||
}
|
||||
C.Alerting.check()
|
||||
|
||||
if C.WriterOpt.QueueMaxSize <= 0 {
|
||||
C.WriterOpt.QueueMaxSize = 100000
|
||||
C.WriterOpt.QueueMaxSize = 10000000
|
||||
}
|
||||
|
||||
if C.WriterOpt.QueuePopSize <= 0 {
|
||||
@@ -109,7 +185,42 @@ func MustLoad(fpaths ...string) {
|
||||
}
|
||||
|
||||
if C.WriterOpt.QueueCount <= 0 {
|
||||
C.WriterOpt.QueueCount = 100
|
||||
C.WriterOpt.QueueCount = 1000
|
||||
}
|
||||
|
||||
if C.WriterOpt.ShardingKey == "" {
|
||||
C.WriterOpt.ShardingKey = "ident"
|
||||
}
|
||||
|
||||
for i, write := range C.Writers {
|
||||
if C.Writers[i].ClusterName == "" {
|
||||
C.Writers[i].ClusterName = C.ClusterName
|
||||
}
|
||||
|
||||
for _, relabel := range write.WriteRelabels {
|
||||
regex, ok := relabel.Regex.(string)
|
||||
if !ok {
|
||||
log.Println("Regex field must be a string")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if regex == "" {
|
||||
regex = "(.*)"
|
||||
}
|
||||
relabel.Regex = models.MustNewRegexp(regex)
|
||||
|
||||
if relabel.Separator == "" {
|
||||
relabel.Separator = ";"
|
||||
}
|
||||
|
||||
if relabel.Action == "" {
|
||||
relabel.Action = "replace"
|
||||
}
|
||||
|
||||
if relabel.Replacement == "" {
|
||||
relabel.Replacement = "$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("heartbeat.ip:", C.Heartbeat.IP)
|
||||
@@ -119,11 +230,13 @@ func MustLoad(fpaths ...string) {
|
||||
|
||||
type Config struct {
|
||||
RunMode string
|
||||
ClusterName string
|
||||
ClusterName string // 监控对象上报时,指定的集群名称
|
||||
BusiGroupLabelKey string
|
||||
AnomalyDataApi []string
|
||||
EngineDelay int64
|
||||
DisableUsageReport bool
|
||||
ReaderFrom string
|
||||
LabelRewrite bool
|
||||
ForceUseServerTS bool
|
||||
Log logx.Config
|
||||
HTTP httpx.Config
|
||||
BasicAuth gin.Accounts
|
||||
@@ -135,30 +248,13 @@ type Config struct {
|
||||
DB ormx.DBConfig
|
||||
WriterOpt WriterGlobalOpt
|
||||
Writers []WriterOptions
|
||||
Reader ReaderOptions
|
||||
Reader PromOption
|
||||
Readers []PromOption
|
||||
Ibex Ibex
|
||||
}
|
||||
|
||||
type ReaderOptions struct {
|
||||
Url string
|
||||
BasicAuthUser string
|
||||
BasicAuthPass string
|
||||
|
||||
Timeout int64
|
||||
DialTimeout int64
|
||||
TLSHandshakeTimeout int64
|
||||
ExpectContinueTimeout int64
|
||||
IdleConnTimeout int64
|
||||
KeepAlive int64
|
||||
|
||||
MaxConnsPerHost int
|
||||
MaxIdleConns int
|
||||
MaxIdleConnsPerHost int
|
||||
|
||||
Headers []string
|
||||
}
|
||||
|
||||
type WriterOptions struct {
|
||||
ClusterName string
|
||||
Url string
|
||||
BasicAuthUser string
|
||||
BasicAuthPass string
|
||||
@@ -175,12 +271,15 @@ type WriterOptions struct {
|
||||
MaxIdleConnsPerHost int
|
||||
|
||||
Headers []string
|
||||
|
||||
WriteRelabels []*models.RelabelConfig
|
||||
}
|
||||
|
||||
type WriterGlobalOpt struct {
|
||||
QueueCount int
|
||||
QueueMaxSize int
|
||||
QueuePopSize int
|
||||
ShardingKey string
|
||||
}
|
||||
|
||||
type HeartbeatConfig struct {
|
||||
@@ -200,6 +299,7 @@ type SMTPConfig struct {
|
||||
}
|
||||
|
||||
type Alerting struct {
|
||||
Timeout int64
|
||||
TemplatesDir string
|
||||
NotifyConcurrency int
|
||||
NotifyBuiltinChannels []string
|
||||
@@ -209,6 +309,89 @@ type Alerting struct {
|
||||
Webhook Webhook
|
||||
}
|
||||
|
||||
func (a *Alerting) check() {
|
||||
if a.Webhook.Enable {
|
||||
if a.Webhook.Timeout == "" {
|
||||
a.Webhook.TimeoutDuration = time.Second * 5
|
||||
} else {
|
||||
dur, err := time.ParseDuration(C.Alerting.Webhook.Timeout)
|
||||
if err != nil {
|
||||
fmt.Println("failed to parse Alerting.Webhook.Timeout")
|
||||
os.Exit(1)
|
||||
}
|
||||
a.Webhook.TimeoutDuration = dur
|
||||
}
|
||||
}
|
||||
|
||||
if a.CallPlugin.Enable {
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Println("notify plugin on unsupported os:", runtime.GOOS)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
p, err := plugin.Open(a.CallPlugin.PluginPath)
|
||||
if err != nil {
|
||||
fmt.Println("failed to load plugin:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
caller, err := p.Lookup(a.CallPlugin.Caller)
|
||||
if err != nil {
|
||||
fmt.Println("failed to lookup plugin Caller:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ins, ok := caller.(notifier.Notifier)
|
||||
if !ok {
|
||||
log.Println("notifier interface not implemented")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
notifier.Instance = ins
|
||||
}
|
||||
|
||||
if a.TemplatesDir == "" {
|
||||
a.TemplatesDir = path.Join(runner.Cwd, "etc", "template")
|
||||
}
|
||||
|
||||
if a.Timeout == 0 {
|
||||
a.Timeout = 30000
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Alerting) ListTpls() (map[string]*template.Template, error) {
|
||||
filenames, err := file.FilesUnder(a.TemplatesDir)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to exec FilesUnder")
|
||||
}
|
||||
|
||||
if len(filenames) == 0 {
|
||||
return nil, errors.New("no tpl files under " + a.TemplatesDir)
|
||||
}
|
||||
|
||||
tplFiles := make([]string, 0, len(filenames))
|
||||
for i := 0; i < len(filenames); i++ {
|
||||
if strings.HasSuffix(filenames[i], ".tpl") {
|
||||
tplFiles = append(tplFiles, filenames[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(tplFiles) == 0 {
|
||||
return nil, errors.New("no tpl files under " + a.TemplatesDir)
|
||||
}
|
||||
|
||||
tpls := make(map[string]*template.Template)
|
||||
for _, tplFile := range tplFiles {
|
||||
tplpath := path.Join(a.TemplatesDir, tplFile)
|
||||
tpl, err := template.New(tplFile).Funcs(tplx.TemplateFuncMap).ParseFiles(tplpath)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to parse tpl: "+tplpath)
|
||||
}
|
||||
tpls[tplFile] = tpl
|
||||
}
|
||||
return tpls, nil
|
||||
}
|
||||
|
||||
type CallScript struct {
|
||||
Enable bool
|
||||
ScriptPath string
|
||||
@@ -254,7 +437,7 @@ func (c *Config) IsDebugMode() bool {
|
||||
|
||||
// Get preferred outbound ip of this machine
|
||||
func GetOutboundIP() net.IP {
|
||||
conn, err := net.Dial("udp", "8.8.8.8:80")
|
||||
conn, err := net.Dial("udp", "223.5.5.5:80")
|
||||
if err != nil {
|
||||
fmt.Println("auto get outbound ip fail:", err)
|
||||
os.Exit(1)
|
||||
|
||||
92
src/server/config/prom_client.go
Normal file
92
src/server/config/prom_client.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/pkg/prom"
|
||||
)
|
||||
|
||||
type PromClientMap struct {
|
||||
sync.RWMutex
|
||||
Clients map[string]prom.API
|
||||
}
|
||||
|
||||
var ReaderClients = &PromClientMap{Clients: make(map[string]prom.API)}
|
||||
|
||||
func (pc *PromClientMap) Set(clusterName string, c prom.API) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
pc.Lock()
|
||||
defer pc.Unlock()
|
||||
pc.Clients[clusterName] = c
|
||||
}
|
||||
|
||||
func (pc *PromClientMap) GetClusterNames() []string {
|
||||
pc.RLock()
|
||||
defer pc.RUnlock()
|
||||
var clusterNames []string
|
||||
for k := range pc.Clients {
|
||||
clusterNames = append(clusterNames, k)
|
||||
}
|
||||
|
||||
return clusterNames
|
||||
}
|
||||
|
||||
func (pc *PromClientMap) GetCli(cluster string) prom.API {
|
||||
pc.RLock()
|
||||
defer pc.RUnlock()
|
||||
c := pc.Clients[cluster]
|
||||
return c
|
||||
}
|
||||
|
||||
func (pc *PromClientMap) IsNil(cluster string) bool {
|
||||
pc.RLock()
|
||||
defer pc.RUnlock()
|
||||
|
||||
c, exists := pc.Clients[cluster]
|
||||
if !exists {
|
||||
return true
|
||||
}
|
||||
|
||||
return c == nil
|
||||
}
|
||||
|
||||
// Hit 根据当前有效的cluster和规则的cluster配置计算有效的cluster列表
|
||||
func (pc *PromClientMap) Hit(cluster string) []string {
|
||||
pc.RLock()
|
||||
defer pc.RUnlock()
|
||||
clusters := make([]string, 0, len(pc.Clients))
|
||||
if cluster == models.ClusterAll {
|
||||
for c := range pc.Clients {
|
||||
clusters = append(clusters, c)
|
||||
}
|
||||
return clusters
|
||||
}
|
||||
|
||||
ruleClusters := strings.Fields(cluster)
|
||||
for c := range pc.Clients {
|
||||
for _, rc := range ruleClusters {
|
||||
if rc == c {
|
||||
clusters = append(clusters, c)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return clusters
|
||||
}
|
||||
|
||||
func (pc *PromClientMap) Reset() {
|
||||
pc.Lock()
|
||||
defer pc.Unlock()
|
||||
|
||||
pc.Clients = make(map[string]prom.API)
|
||||
}
|
||||
|
||||
func (pc *PromClientMap) Del(cluster string) {
|
||||
pc.Lock()
|
||||
defer pc.Unlock()
|
||||
delete(pc.Clients, cluster)
|
||||
}
|
||||
89
src/server/config/prom_option.go
Normal file
89
src/server/config/prom_option.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/pkg/tls"
|
||||
)
|
||||
|
||||
type PromOption struct {
|
||||
ClusterName string
|
||||
Url string
|
||||
BasicAuthUser string
|
||||
BasicAuthPass string
|
||||
|
||||
Timeout int64
|
||||
DialTimeout int64
|
||||
|
||||
UseTLS bool
|
||||
tls.ClientConfig
|
||||
|
||||
MaxIdleConnsPerHost int
|
||||
|
||||
Headers []string
|
||||
}
|
||||
|
||||
func (po *PromOption) Equal(target PromOption) bool {
|
||||
if po.Url != target.Url {
|
||||
return false
|
||||
}
|
||||
|
||||
if po.BasicAuthUser != target.BasicAuthUser {
|
||||
return false
|
||||
}
|
||||
|
||||
if po.BasicAuthPass != target.BasicAuthPass {
|
||||
return false
|
||||
}
|
||||
|
||||
if po.Timeout != target.Timeout {
|
||||
return false
|
||||
}
|
||||
|
||||
if po.DialTimeout != target.DialTimeout {
|
||||
return false
|
||||
}
|
||||
|
||||
if po.MaxIdleConnsPerHost != target.MaxIdleConnsPerHost {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(po.Headers) != len(target.Headers) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(po.Headers); i++ {
|
||||
if po.Headers[i] != target.Headers[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type PromOptionsStruct struct {
|
||||
Data map[string]PromOption
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (pos *PromOptionsStruct) Set(clusterName string, po PromOption) {
|
||||
pos.Lock()
|
||||
pos.Data[clusterName] = po
|
||||
pos.Unlock()
|
||||
}
|
||||
|
||||
func (pos *PromOptionsStruct) Del(clusterName string) {
|
||||
pos.Lock()
|
||||
delete(pos.Data, clusterName)
|
||||
pos.Unlock()
|
||||
}
|
||||
|
||||
func (pos *PromOptionsStruct) Get(clusterName string) (PromOption, bool) {
|
||||
pos.RLock()
|
||||
defer pos.RUnlock()
|
||||
ret, has := pos.Data[clusterName]
|
||||
return ret, has
|
||||
}
|
||||
|
||||
// Data key is cluster name
|
||||
var PromOptions = &PromOptionsStruct{Data: make(map[string]PromOption)}
|
||||
173
src/server/config/reader.go
Normal file
173
src/server/config/reader.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/pkg/prom"
|
||||
"github.com/prometheus/client_golang/api"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func InitReader() error {
|
||||
rf := strings.ToLower(strings.TrimSpace(C.ReaderFrom))
|
||||
if rf == "" || rf == "config" {
|
||||
if len(C.Readers) == 0 {
|
||||
C.Reader.ClusterName = C.ClusterName
|
||||
C.Readers = append(C.Readers, C.Reader)
|
||||
}
|
||||
|
||||
for _, reader := range C.Readers {
|
||||
err := setClientFromPromOption(reader.ClusterName, reader)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to setClientFromPromOption: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if rf == "database" {
|
||||
return initFromDatabase()
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid configuration ReaderFrom: %s", rf)
|
||||
}
|
||||
|
||||
func initFromDatabase() error {
|
||||
go func() {
|
||||
for {
|
||||
loadFromDatabase()
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadFromDatabase() {
|
||||
clusters, err := models.AlertingEngineGetClusters(C.Heartbeat.Endpoint)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to get current cluster, error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(clusters) == 0 {
|
||||
ReaderClients.Reset()
|
||||
logger.Warning("no datasource binded to me")
|
||||
return
|
||||
}
|
||||
|
||||
newCluster := make(map[string]struct{})
|
||||
for _, cluster := range clusters {
|
||||
newCluster[cluster] = struct{}{}
|
||||
ckey := "prom." + cluster + ".option"
|
||||
cval, err := models.ConfigsGet(ckey)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to get ckey: %s, error: %v", ckey, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if cval == "" {
|
||||
logger.Debugf("ckey: %s is empty", ckey)
|
||||
continue
|
||||
}
|
||||
|
||||
var po PromOption
|
||||
err = json.Unmarshal([]byte(cval), &po)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to unmarshal PromOption: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if ReaderClients.IsNil(cluster) {
|
||||
// first time
|
||||
if err = setClientFromPromOption(cluster, po); err != nil {
|
||||
logger.Errorf("failed to setClientFromPromOption: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("setClientFromPromOption success: ", cluster)
|
||||
PromOptions.Set(cluster, po)
|
||||
continue
|
||||
}
|
||||
|
||||
localPo, has := PromOptions.Get(cluster)
|
||||
if !has || !localPo.Equal(po) {
|
||||
if err = setClientFromPromOption(cluster, po); err != nil {
|
||||
logger.Errorf("failed to setClientFromPromOption: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
PromOptions.Set(cluster, po)
|
||||
}
|
||||
}
|
||||
|
||||
// delete useless cluster
|
||||
oldClusters := ReaderClients.GetClusterNames()
|
||||
for _, oldCluster := range oldClusters {
|
||||
if _, has := newCluster[oldCluster]; !has {
|
||||
ReaderClients.Del(oldCluster)
|
||||
PromOptions.Del(oldCluster)
|
||||
logger.Info("delete cluster: ", oldCluster)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newClientFromPromOption(po PromOption) (api.Client, error) {
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: time.Duration(po.DialTimeout) * time.Millisecond,
|
||||
}).DialContext,
|
||||
ResponseHeaderTimeout: time.Duration(po.Timeout) * time.Millisecond,
|
||||
MaxIdleConnsPerHost: po.MaxIdleConnsPerHost,
|
||||
}
|
||||
|
||||
if po.UseTLS {
|
||||
tlsConfig, err := po.TLSConfig()
|
||||
if err != nil {
|
||||
logger.Errorf("new cluster %s fail: %v", po.Url, err)
|
||||
return nil, err
|
||||
}
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
return api.NewClient(api.Config{
|
||||
Address: po.Url,
|
||||
RoundTripper: transport,
|
||||
})
|
||||
}
|
||||
|
||||
func setClientFromPromOption(clusterName string, po PromOption) error {
|
||||
if clusterName == "" {
|
||||
return fmt.Errorf("argument clusterName is blank")
|
||||
}
|
||||
|
||||
if po.Url == "" {
|
||||
return fmt.Errorf("prometheus url is blank")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(po.Url, "https") {
|
||||
po.UseTLS = true
|
||||
po.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
cli, err := newClientFromPromOption(po)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to newClientFromPromOption: %v", err)
|
||||
}
|
||||
|
||||
logger.Debugf("setClientFromPromOption: %s, %+v", clusterName, po)
|
||||
ReaderClients.Set(clusterName, prom.NewAPI(cli, prom.ClientOptions{
|
||||
BasicAuthUser: po.BasicAuthUser,
|
||||
BasicAuthPass: po.BasicAuthPass,
|
||||
Headers: po.Headers,
|
||||
}))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -45,7 +45,11 @@ func consume(events []interface{}, sema *semaphore.Semaphore) {
|
||||
func consumeOne(event *models.AlertCurEvent) {
|
||||
LogEvent(event, "consume")
|
||||
|
||||
if err := event.ParseRuleNote(); err != nil {
|
||||
if err := event.ParseRule("rule_name"); err != nil {
|
||||
event.RuleName = fmt.Sprintf("failed to parse rule name: %v", err)
|
||||
}
|
||||
|
||||
if err := event.ParseRule("rule_note"); err != nil {
|
||||
event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
|
||||
}
|
||||
|
||||
@@ -55,9 +59,7 @@ func consumeOne(event *models.AlertCurEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
fillUsers(event)
|
||||
callback(event)
|
||||
notify(event)
|
||||
HandleEventNotify(event, false)
|
||||
}
|
||||
|
||||
func persist(event *models.AlertCurEvent) {
|
||||
@@ -72,9 +74,10 @@ func persist(event *models.AlertCurEvent) {
|
||||
// 不管是告警还是恢复,全量告警里都要记录
|
||||
if err := his.Add(); err != nil {
|
||||
logger.Errorf(
|
||||
"event_persist_his_fail: %v rule_id=%d hash=%s tags=%v timestamp=%d value=%s",
|
||||
"event_persist_his_fail: %v rule_id=%d cluster:%s hash=%s tags=%v timestamp=%d value=%s",
|
||||
err,
|
||||
event.RuleId,
|
||||
event.Cluster,
|
||||
event.Hash,
|
||||
event.TagsJSON,
|
||||
event.TriggerTime,
|
||||
@@ -97,9 +100,10 @@ func persist(event *models.AlertCurEvent) {
|
||||
if event.Id > 0 {
|
||||
if err := event.Add(); err != nil {
|
||||
logger.Errorf(
|
||||
"event_persist_cur_fail: %v rule_id=%d hash=%s tags=%v timestamp=%d value=%s",
|
||||
"event_persist_cur_fail: %v rule_id=%d cluster:%s hash=%s tags=%v timestamp=%d value=%s",
|
||||
err,
|
||||
event.RuleId,
|
||||
event.Cluster,
|
||||
event.Hash,
|
||||
event.TagsJSON,
|
||||
event.TriggerTime,
|
||||
@@ -122,9 +126,10 @@ func persist(event *models.AlertCurEvent) {
|
||||
if event.Id > 0 {
|
||||
if err := event.Add(); err != nil {
|
||||
logger.Errorf(
|
||||
"event_persist_cur_fail: %v rule_id=%d hash=%s tags=%v timestamp=%d value=%s",
|
||||
"event_persist_cur_fail: %v rule_id=%d cluster:%s hash=%s tags=%v timestamp=%d value=%s",
|
||||
err,
|
||||
event.RuleId,
|
||||
event.Cluster,
|
||||
event.Hash,
|
||||
event.TagsJSON,
|
||||
event.TriggerTime,
|
||||
@@ -142,7 +147,6 @@ func fillUsers(e *models.AlertCurEvent) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
gids = append(gids, gid)
|
||||
}
|
||||
|
||||
@@ -166,11 +170,3 @@ func mapKeys(m map[int64]struct{}) []int64 {
|
||||
}
|
||||
return lst
|
||||
}
|
||||
|
||||
func StringSetKeys(m map[string]struct{}) []string {
|
||||
lst := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
lst = append(lst, k)
|
||||
}
|
||||
return lst
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
)
|
||||
|
||||
func isNoneffective(timestamp int64, alertRule *models.AlertRule) bool {
|
||||
if alertRule.Disabled == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
tm := time.Unix(timestamp, 0)
|
||||
triggerTime := tm.Format("15:04")
|
||||
triggerWeek := strconv.Itoa(int(tm.Weekday()))
|
||||
|
||||
if alertRule.EnableStime <= alertRule.EnableEtime {
|
||||
if triggerTime < alertRule.EnableStime || triggerTime > alertRule.EnableEtime {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if triggerTime < alertRule.EnableStime && triggerTime > alertRule.EnableEtime {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
alertRule.EnableDaysOfWeek = strings.Replace(alertRule.EnableDaysOfWeek, "7", "0", 1)
|
||||
|
||||
return !strings.Contains(alertRule.EnableDaysOfWeek, triggerWeek)
|
||||
}
|
||||
@@ -2,15 +2,20 @@ package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/server/common/sender"
|
||||
"github.com/didi/nightingale/v5/src/server/config"
|
||||
promstat "github.com/didi/nightingale/v5/src/server/stat"
|
||||
"github.com/toolkits/pkg/container/list"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
var EventQueue = list.NewSafeListLimited(10000000)
|
||||
|
||||
func Start(ctx context.Context) error {
|
||||
err := initTpls()
|
||||
err := reloadTpls()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -18,19 +23,38 @@ func Start(ctx context.Context) error {
|
||||
// start loop consumer
|
||||
go loopConsume(ctx)
|
||||
|
||||
// filter my rules and start worker
|
||||
go loopFilterRules(ctx)
|
||||
go ruleHolder.LoopSyncRules(ctx)
|
||||
|
||||
go reportQueueSize()
|
||||
|
||||
go sender.StartEmailSender()
|
||||
|
||||
go initReporter(func(em map[ErrorType]uint64) {
|
||||
if len(em) == 0 {
|
||||
return
|
||||
}
|
||||
title := fmt.Sprintf("server %s has some errors, please check server logs for detail", config.C.Heartbeat.IP)
|
||||
msg := ""
|
||||
for k, v := range em {
|
||||
msg += fmt.Sprintf("error: %s, count: %d\n", k, v)
|
||||
}
|
||||
notifyToMaintainer(title, msg)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Reload() {
|
||||
err := reloadTpls()
|
||||
if err != nil {
|
||||
logger.Error("engine reload err:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func reportQueueSize() {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
promstat.GaugeAlertQueueSize.WithLabelValues(config.C.ClusterName).Set(float64(EventQueue.Len()))
|
||||
|
||||
promstat.GaugeAlertQueueSize.Set(float64(EventQueue.Len()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
)
|
||||
|
||||
func LogEvent(event *models.AlertCurEvent, location string, err ...error) {
|
||||
@@ -17,11 +18,12 @@ func LogEvent(event *models.AlertCurEvent, location string, err ...error) {
|
||||
}
|
||||
|
||||
logger.Infof(
|
||||
"event(%s %s) %s: rule_id=%d %v%s@%d %s",
|
||||
"event(%s %s) %s: rule_id=%d cluster:%s %v%s@%d %s",
|
||||
event.Hash,
|
||||
status,
|
||||
location,
|
||||
event.RuleId,
|
||||
event.Cluster,
|
||||
event.TagsJSON,
|
||||
event.TriggerValue,
|
||||
event.TriggerTime,
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/server/memsto"
|
||||
)
|
||||
|
||||
// 如果传入了clock这个可选参数,就表示使用这个clock表示的时间,否则就从event的字段中取TriggerTime
|
||||
func isMuted(event *models.AlertCurEvent, clock ...int64) bool {
|
||||
mutes, has := memsto.AlertMuteCache.Gets(event.GroupId)
|
||||
if !has || len(mutes) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(mutes); i++ {
|
||||
if matchMute(event, mutes[i], clock...) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func matchMute(event *models.AlertCurEvent, mute *models.AlertMute, clock ...int64) bool {
|
||||
ts := event.TriggerTime
|
||||
if len(clock) > 0 {
|
||||
ts = clock[0]
|
||||
}
|
||||
|
||||
if ts < mute.Btime || ts > mute.Etime {
|
||||
return false
|
||||
}
|
||||
tg := event.TagsMap
|
||||
tg["rulename"] = event.RuleName
|
||||
return matchTags(tg, mute.ITags)
|
||||
}
|
||||
|
||||
func matchTags(eventTagsMap map[string]string, itags []models.TagFilter) bool {
|
||||
for i := 0; i < len(itags); i++ {
|
||||
filter := itags[i]
|
||||
value, exists := eventTagsMap[filter.Key]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
if filter.Func == "==" {
|
||||
// ==
|
||||
if filter.Value != value {
|
||||
return false
|
||||
}
|
||||
} else if filter.Func == "in" {
|
||||
// in
|
||||
if _, has := filter.Vset[value]; !has {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// =~
|
||||
if !filter.Regexp.MatchString(value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
185
src/server/engine/mute_strategy.go
Normal file
185
src/server/engine/mute_strategy.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/server/memsto"
|
||||
)
|
||||
|
||||
type MuteStrategyFunc func(rule *models.AlertRule, event *models.AlertCurEvent) bool
|
||||
|
||||
var AlertMuteStrategies = []MuteStrategyFunc{
|
||||
TimeNonEffectiveMuteStrategy,
|
||||
IdentNotExistsMuteStrategy,
|
||||
BgNotMatchMuteStrategy,
|
||||
EventMuteStrategy,
|
||||
}
|
||||
|
||||
func IsMuted(rule *models.AlertRule, event *models.AlertCurEvent) bool {
|
||||
for _, strategyFunc := range AlertMuteStrategies {
|
||||
if strategyFunc(rule, event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TimeNonEffectiveMuteStrategy 根据规则配置的告警时间过滤,如果产生的告警不在规则配置的告警时间内,则不告警
|
||||
func TimeNonEffectiveMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent) bool {
|
||||
if rule.Disabled == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
tm := time.Unix(event.TriggerTime, 0)
|
||||
triggerTime := tm.Format("15:04")
|
||||
triggerWeek := strconv.Itoa(int(tm.Weekday()))
|
||||
|
||||
enableStime := strings.Fields(rule.EnableStime)
|
||||
enableEtime := strings.Fields(rule.EnableEtime)
|
||||
enableDaysOfWeek := strings.Split(rule.EnableDaysOfWeek, ";")
|
||||
length := len(enableDaysOfWeek)
|
||||
// enableStime,enableEtime,enableDaysOfWeek三者长度肯定相同,这里循环一个即可
|
||||
for i := 0; i < length; i++ {
|
||||
enableDaysOfWeek[i] = strings.Replace(enableDaysOfWeek[i], "7", "0", 1)
|
||||
if !strings.Contains(enableDaysOfWeek[i], triggerWeek) {
|
||||
continue
|
||||
}
|
||||
if enableStime[i] <= enableEtime[i] {
|
||||
if triggerTime < enableStime[i] || triggerTime > enableEtime[i] {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if triggerTime < enableStime[i] && triggerTime > enableEtime[i] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 到这里说明当前时刻在告警规则的某组生效时间范围内,直接返回 false
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IdentNotExistsMuteStrategy 根据ident是否存在过滤,如果ident不存在,则target_up的告警直接过滤掉
|
||||
func IdentNotExistsMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent) bool {
|
||||
ident, has := event.TagsMap["ident"]
|
||||
if !has {
|
||||
return false
|
||||
}
|
||||
_, exists := memsto.TargetCache.Get(ident)
|
||||
// 如果是target_up的告警,且ident已经不存在了,直接过滤掉
|
||||
// 这里的判断有点太粗暴了,但是目前没有更好的办法
|
||||
if !exists && strings.Contains(rule.PromQl, "target_up") {
|
||||
logger.Debugf("[%s] mute: rule_eval:%d cluster:%s ident:%s", "IdentNotExistsMuteStrategy", rule.Id, event.Cluster, ident)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BgNotMatchMuteStrategy 当规则开启只在bg内部告警时,对于非bg内部的机器过滤
|
||||
func BgNotMatchMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent) bool {
|
||||
// 没有开启BG内部告警,直接不过滤
|
||||
if rule.EnableInBG == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
ident, has := event.TagsMap["ident"]
|
||||
if !has {
|
||||
return false
|
||||
}
|
||||
|
||||
target, exists := memsto.TargetCache.Get(ident)
|
||||
// 对于包含ident的告警事件,check一下ident所属bg和rule所属bg是否相同
|
||||
// 如果告警规则选择了只在本BG生效,那其他BG的机器就不能因此规则产生告警
|
||||
if exists && target.GroupId != rule.GroupId {
|
||||
logger.Debugf("[%s] mute: rule_eval:%d cluster:%s", "BgNotMatchMuteStrategy", rule.Id, event.Cluster)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func EventMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent) bool {
|
||||
mutes, has := memsto.AlertMuteCache.Gets(event.GroupId)
|
||||
if !has || len(mutes) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(mutes); i++ {
|
||||
if matchMute(event, mutes[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchMute 如果传入了clock这个可选参数,就表示使用这个clock表示的时间,否则就从event的字段中取TriggerTime
|
||||
func matchMute(event *models.AlertCurEvent, mute *models.AlertMute, clock ...int64) bool {
|
||||
if mute.Disabled == 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
ts := event.TriggerTime
|
||||
if len(clock) > 0 {
|
||||
ts = clock[0]
|
||||
}
|
||||
|
||||
// 如果不是全局的,判断 cluster
|
||||
if mute.Cluster != models.ClusterAll {
|
||||
// mute.Cluster 是一个字符串,可能是多个cluster的组合,比如"cluster1 cluster2"
|
||||
clusters := strings.Fields(mute.Cluster)
|
||||
cm := make(map[string]struct{}, len(clusters))
|
||||
for i := 0; i < len(clusters); i++ {
|
||||
cm[clusters[i]] = struct{}{}
|
||||
}
|
||||
|
||||
// 判断event.Cluster是否包含在cm中
|
||||
if _, has := cm[event.Cluster]; !has {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if ts < mute.Btime || ts > mute.Etime {
|
||||
return false
|
||||
}
|
||||
|
||||
return matchTags(event.TagsMap, mute.ITags)
|
||||
}
|
||||
|
||||
func matchTag(value string, filter models.TagFilter) bool {
|
||||
switch filter.Func {
|
||||
case "==":
|
||||
return filter.Value == value
|
||||
case "!=":
|
||||
return filter.Value != value
|
||||
case "in":
|
||||
_, has := filter.Vset[value]
|
||||
return has
|
||||
case "not in":
|
||||
_, has := filter.Vset[value]
|
||||
return !has
|
||||
case "=~":
|
||||
return filter.Regexp.MatchString(value)
|
||||
case "!~":
|
||||
return !filter.Regexp.MatchString(value)
|
||||
}
|
||||
// unexpect func
|
||||
return false
|
||||
}
|
||||
|
||||
func matchTags(eventTagsMap map[string]string, itags []models.TagFilter) bool {
|
||||
for _, filter := range itags {
|
||||
value, has := eventTagsMap[filter.Key]
|
||||
if !has {
|
||||
return false
|
||||
}
|
||||
if !matchTag(value, filter) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -2,83 +2,169 @@ package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"path"
|
||||
"plugin"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/toolkits/pkg/file"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/runner"
|
||||
"github.com/toolkits/pkg/slice"
|
||||
"sync"
|
||||
|
||||
"github.com/didi/nightingale/v5/src/models"
|
||||
"github.com/didi/nightingale/v5/src/pkg/sys"
|
||||
"github.com/didi/nightingale/v5/src/pkg/tplx"
|
||||
"github.com/didi/nightingale/v5/src/server/common/sender"
|
||||
"github.com/didi/nightingale/v5/src/server/config"
|
||||
"github.com/didi/nightingale/v5/src/server/memsto"
|
||||
"github.com/didi/nightingale/v5/src/storage"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
var tpls = make(map[string]*template.Template)
|
||||
var (
|
||||
rwLock sync.RWMutex
|
||||
tpls map[string]*template.Template
|
||||
Senders map[string]sender.Sender
|
||||
|
||||
func initTpls() error {
|
||||
if config.C.Alerting.TemplatesDir == "" {
|
||||
config.C.Alerting.TemplatesDir = path.Join(runner.Cwd, "etc", "template")
|
||||
}
|
||||
// 处理事件到subscription关系,处理的subscription用OrMerge进行合并
|
||||
routers = []Router{GroupRouter, GlobalWebhookRouter, EventCallbacksRouter}
|
||||
// 额外去掉一些订阅,处理的subscription用AndMerge进行合并, 如设置 channel=false,合并后不通过这个channel发送
|
||||
// 如果实现了相关Router,可以添加到interceptors中
|
||||
interceptors []Router
|
||||
|
||||
filenames, err := file.FilesUnder(config.C.Alerting.TemplatesDir)
|
||||
// 额外的订阅event逻辑处理
|
||||
subscribeRouters = []Router{GroupRouter}
|
||||
subscribeInterceptors []Router
|
||||
)
|
||||
|
||||
func reloadTpls() error {
|
||||
tmpTpls, err := config.C.Alerting.ListTpls()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to exec FilesUnder")
|
||||
return err
|
||||
}
|
||||
|
||||
if len(filenames) == 0 {
|
||||
return errors.New("no tpl files under " + config.C.Alerting.TemplatesDir)
|
||||
}
|
||||
|
||||
tplFiles := make([]string, 0, len(filenames))
|
||||
for i := 0; i < len(filenames); i++ {
|
||||
if strings.HasSuffix(filenames[i], ".tpl") {
|
||||
tplFiles = append(tplFiles, filenames[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(tplFiles) == 0 {
|
||||
return errors.New("no tpl files under " + config.C.Alerting.TemplatesDir)
|
||||
}
|
||||
|
||||
for i := 0; i < len(tplFiles); i++ {
|
||||
tplpath := path.Join(config.C.Alerting.TemplatesDir, tplFiles[i])
|
||||
|
||||
tpl, err := template.New(tplFiles[i]).Funcs(tplx.TemplateFuncMap).ParseFiles(tplpath)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to parse tpl: "+tplpath)
|
||||
}
|
||||
|
||||
tpls[tplFiles[i]] = tpl
|
||||
senders := map[string]sender.Sender{
|
||||
models.Email: sender.NewSender(models.Email, tmpTpls),
|
||||
models.Dingtalk: sender.NewSender(models.Dingtalk, tmpTpls),
|
||||
models.Wecom: sender.NewSender(models.Wecom, tmpTpls),
|
||||
models.Feishu: sender.NewSender(models.Feishu, tmpTpls),
|
||||
models.FeishuCard: sender.NewSender(models.FeishuCard, tmpTpls),
|
||||
models.Mm: sender.NewSender(models.Mm, tmpTpls),
|
||||
models.Telegram: sender.NewSender(models.Telegram, tmpTpls),
|
||||
}
|
||||
|
||||
rwLock.Lock()
|
||||
tpls = tmpTpls
|
||||
Senders = senders
|
||||
rwLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleEventNotify 处理event事件的主逻辑
|
||||
// event: 告警/恢复事件
|
||||
// isSubscribe: 告警事件是否由subscribe的配置产生
|
||||
func HandleEventNotify(event *models.AlertCurEvent, isSubscribe bool) {
|
||||
rule := memsto.AlertRuleCache.Get(event.RuleId)
|
||||
if rule == nil {
|
||||
return
|
||||
}
|
||||
fillUsers(event)
|
||||
|
||||
var (
|
||||
handlers []Router
|
||||
interceptorHandlers []Router
|
||||
)
|
||||
if isSubscribe {
|
||||
handlers = subscribeRouters
|
||||
interceptorHandlers = subscribeInterceptors
|
||||
} else {
|
||||
handlers = routers
|
||||
interceptorHandlers = interceptors
|
||||
}
|
||||
|
||||
subscription := NewSubscription()
|
||||
// 处理订阅关系使用OrMerge
|
||||
for _, handler := range handlers {
|
||||
subscription.OrMerge(handler(rule, event, subscription))
|
||||
}
|
||||
|
||||
// 处理移除订阅关系的逻辑,比如员工离职,临时静默某个通道的策略等
|
||||
for _, handler := range interceptorHandlers {
|
||||
subscription.AndMerge(handler(rule, event, subscription))
|
||||
}
|
||||
|
||||
// 处理事件发送,这里用一个goroutine处理一个event的所有发送事件
|
||||
go Send(rule, event, subscription, isSubscribe)
|
||||
|
||||
// 如果是不是订阅规则出现的event,则需要处理订阅规则的event
|
||||
if !isSubscribe {
|
||||
handleSubs(event)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSubs(event *models.AlertCurEvent) {
|
||||
// handle alert subscribes
|
||||
subscribes := make([]*models.AlertSubscribe, 0)
|
||||
// rule specific subscribes
|
||||
if subs, has := memsto.AlertSubscribeCache.Get(event.RuleId); has {
|
||||
subscribes = append(subscribes, subs...)
|
||||
}
|
||||
// global subscribes
|
||||
if subs, has := memsto.AlertSubscribeCache.Get(0); has {
|
||||
subscribes = append(subscribes, subs...)
|
||||
}
|
||||
for _, sub := range subscribes {
|
||||
handleSub(sub, *event)
|
||||
}
|
||||
}
|
||||
|
||||
// handleSub 处理订阅规则的event,注意这里event要使用值传递,因为后面会修改event的状态
|
||||
func handleSub(sub *models.AlertSubscribe, event models.AlertCurEvent) {
|
||||
if sub.IsDisabled() || !sub.MatchCluster(event.Cluster) {
|
||||
return
|
||||
}
|
||||
if !matchTags(event.TagsMap, sub.ITags) {
|
||||
return
|
||||
}
|
||||
|
||||
sub.ModifyEvent(&event)
|
||||
LogEvent(&event, "subscribe")
|
||||
HandleEventNotify(&event, true)
|
||||
}
|
||||
|
||||
func Send(rule *models.AlertRule, event *models.AlertCurEvent, subscription *Subscription, isSubscribe bool) {
|
||||
for channel, uids := range subscription.ToChannelUserMap() {
|
||||
ctx := sender.BuildMessageContext(rule, event, uids)
|
||||
rwLock.RLock()
|
||||
s := Senders[channel]
|
||||
rwLock.RUnlock()
|
||||
if s == nil {
|
||||
logger.Warningf("no sender for channel: %s", channel)
|
||||
continue
|
||||
}
|
||||
s.Send(ctx)
|
||||
}
|
||||
|
||||
// handle event callbacks
|
||||
sender.SendCallbacks(subscription.ToCallbackList(), event)
|
||||
|
||||
// handle global webhooks
|
||||
sender.SendWebhooks(subscription.ToWebhookList(), event)
|
||||
|
||||
noticeBytes := genNoticeBytes(event)
|
||||
|
||||
// handle plugin call
|
||||
go sender.MayPluginNotify(noticeBytes)
|
||||
|
||||
if !isSubscribe {
|
||||
// handle redis pub
|
||||
sender.PublishToRedis(event.Cluster, noticeBytes)
|
||||
}
|
||||
}
|
||||
|
||||
type Notice struct {
|
||||
Event *models.AlertCurEvent `json:"event"`
|
||||
Tpls map[string]string `json:"tpls"`
|
||||
}
|
||||
|
||||
func genNotice(event *models.AlertCurEvent) Notice {
|
||||
func genNoticeBytes(event *models.AlertCurEvent) []byte {
|
||||
// build notice body with templates
|
||||
ntpls := make(map[string]string)
|
||||
|
||||
rwLock.RLock()
|
||||
defer rwLock.RUnlock()
|
||||
for filename, tpl := range tpls {
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, event); err != nil {
|
||||
@@ -88,348 +174,12 @@ func genNotice(event *models.AlertCurEvent) Notice {
|
||||
}
|
||||
}
|
||||
|
||||
return Notice{Event: event, Tpls: ntpls}
|
||||
}
|
||||
|
||||
func alertingRedisPub(bs []byte) {
|
||||
// pub all alerts to redis
|
||||
if config.C.Alerting.RedisPub.Enable {
|
||||
err := storage.Redis.Publish(context.Background(), config.C.Alerting.RedisPub.ChannelKey, bs).Err()
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: redis publish %s err: %v", config.C.Alerting.RedisPub.ChannelKey, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleNotice(notice Notice, bs []byte) {
|
||||
alertingCallScript(bs)
|
||||
|
||||
alertingCallPlugin(bs)
|
||||
|
||||
if len(config.C.Alerting.NotifyBuiltinChannels) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
emailset := make(map[string]struct{})
|
||||
phoneset := make(map[string]struct{})
|
||||
wecomset := make(map[string]struct{})
|
||||
dingtalkset := make(map[string]struct{})
|
||||
feishuset := make(map[string]struct{})
|
||||
|
||||
for _, user := range notice.Event.NotifyUsersObj {
|
||||
if user.Email != "" {
|
||||
emailset[user.Email] = struct{}{}
|
||||
}
|
||||
|
||||
if user.Phone != "" {
|
||||
phoneset[user.Phone] = struct{}{}
|
||||
}
|
||||
|
||||
bs, err := user.Contacts.MarshalJSON()
|
||||
if err != nil {
|
||||
logger.Errorf("handle_notice: failed to marshal contacts: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
ret := gjson.GetBytes(bs, "dingtalk_robot_token")
|
||||
if ret.Exists() {
|
||||
dingtalkset[ret.String()] = struct{}{}
|
||||
}
|
||||
|
||||
ret = gjson.GetBytes(bs, "wecom_robot_token")
|
||||
if ret.Exists() {
|
||||
wecomset[ret.String()] = struct{}{}
|
||||
}
|
||||
|
||||
ret = gjson.GetBytes(bs, "feishu_robot_token")
|
||||
if ret.Exists() {
|
||||
feishuset[ret.String()] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
phones := StringSetKeys(phoneset)
|
||||
|
||||
for _, ch := range notice.Event.NotifyChannelsJSON {
|
||||
switch ch {
|
||||
case "email":
|
||||
if len(emailset) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !slice.ContainsString(config.C.Alerting.NotifyBuiltinChannels, "email") {
|
||||
continue
|
||||
}
|
||||
|
||||
subject, has := notice.Tpls["subject.tpl"]
|
||||
if !has {
|
||||
subject = "subject.tpl not found"
|
||||
}
|
||||
|
||||
content, has := notice.Tpls["mailbody.tpl"]
|
||||
if !has {
|
||||
content = "mailbody.tpl not found"
|
||||
}
|
||||
|
||||
sender.WriteEmail(subject, content, StringSetKeys(emailset))
|
||||
case "dingtalk":
|
||||
if len(dingtalkset) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !slice.ContainsString(config.C.Alerting.NotifyBuiltinChannels, "dingtalk") {
|
||||
continue
|
||||
}
|
||||
|
||||
content, has := notice.Tpls["dingtalk.tpl"]
|
||||
if !has {
|
||||
content = "dingtalk.tpl not found"
|
||||
}
|
||||
|
||||
sender.SendDingtalk(sender.DingtalkMessage{
|
||||
Title: notice.Event.RuleName,
|
||||
Text: content,
|
||||
AtMobiles: phones,
|
||||
Tokens: StringSetKeys(dingtalkset),
|
||||
})
|
||||
case "wecom":
|
||||
if len(wecomset) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !slice.ContainsString(config.C.Alerting.NotifyBuiltinChannels, "wecom") {
|
||||
continue
|
||||
}
|
||||
|
||||
content, has := notice.Tpls["wecom.tpl"]
|
||||
if !has {
|
||||
content = "wecom.tpl not found"
|
||||
}
|
||||
sender.SendWecom(sender.WecomMessage{
|
||||
Text: content,
|
||||
Tokens: StringSetKeys(wecomset),
|
||||
})
|
||||
case "feishu":
|
||||
if len(feishuset) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !slice.ContainsString(config.C.Alerting.NotifyBuiltinChannels, "feishu") {
|
||||
continue
|
||||
}
|
||||
|
||||
content, has := notice.Tpls["feishu.tpl"]
|
||||
if !has {
|
||||
content = "feishu.tpl not found"
|
||||
}
|
||||
sender.SendFeishu(sender.FeishuMessage{
|
||||
Text: content,
|
||||
AtMobiles: phones,
|
||||
Tokens: StringSetKeys(feishuset),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func notify(event *models.AlertCurEvent) {
|
||||
LogEvent(event, "notify")
|
||||
|
||||
notice := genNotice(event)
|
||||
notice := Notice{Event: event, Tpls: ntpls}
|
||||
stdinBytes, err := json.Marshal(notice)
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: failed to marshal notice: %v", err)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
alertingRedisPub(stdinBytes)
|
||||
alertingWebhook(event)
|
||||
|
||||
handleNotice(notice, stdinBytes)
|
||||
|
||||
// handle alert subscribes
|
||||
subs, has := memsto.AlertSubscribeCache.Get(event.RuleId)
|
||||
if has {
|
||||
handleSubscribes(*event, subs)
|
||||
}
|
||||
|
||||
subs, has = memsto.AlertSubscribeCache.Get(0)
|
||||
if has {
|
||||
handleSubscribes(*event, subs)
|
||||
}
|
||||
}
|
||||
|
||||
func alertingWebhook(event *models.AlertCurEvent) {
|
||||
conf := config.C.Alerting.Webhook
|
||||
|
||||
if !conf.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
if conf.Url == "" {
|
||||
return
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bf := bytes.NewBuffer(bs)
|
||||
|
||||
req, err := http.NewRequest("POST", conf.Url, bf)
|
||||
if err != nil {
|
||||
logger.Warning("alertingWebhook failed to new request", err)
|
||||
return
|
||||
}
|
||||
|
||||
if conf.BasicAuthUser != "" && conf.BasicAuthPass != "" {
|
||||
req.SetBasicAuth(conf.BasicAuthUser, conf.BasicAuthPass)
|
||||
}
|
||||
|
||||
if len(conf.Headers) > 0 && len(conf.Headers)%2 == 0 {
|
||||
for i := 0; i < len(conf.Headers); i += 2 {
|
||||
req.Header.Set(conf.Headers[i], conf.Headers[i+1])
|
||||
}
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: conf.TimeoutDuration,
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
logger.Warning("alertingWebhook failed to call url, error: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ = ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
logger.Debugf("alertingWebhook done, url: %s, response code: %d, body: %s", conf.Url, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
func handleSubscribes(event models.AlertCurEvent, subs []*models.AlertSubscribe) {
|
||||
for i := 0; i < len(subs); i++ {
|
||||
handleSubscribe(event, subs[i])
|
||||
}
|
||||
}
|
||||
|
||||
func handleSubscribe(event models.AlertCurEvent, sub *models.AlertSubscribe) {
|
||||
if !matchTags(event.TagsMap, sub.ITags) {
|
||||
return
|
||||
}
|
||||
|
||||
if sub.RedefineSeverity == 1 {
|
||||
event.Severity = sub.NewSeverity
|
||||
}
|
||||
|
||||
if sub.RedefineChannels == 1 {
|
||||
event.NotifyChannels = sub.NewChannels
|
||||
event.NotifyChannelsJSON = strings.Fields(sub.NewChannels)
|
||||
}
|
||||
|
||||
event.NotifyGroups = sub.UserGroupIds
|
||||
event.NotifyGroupsJSON = strings.Fields(sub.UserGroupIds)
|
||||
if len(event.NotifyGroupsJSON) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
LogEvent(&event, "subscribe")
|
||||
|
||||
fillUsers(&event)
|
||||
|
||||
notice := genNotice(&event)
|
||||
stdinBytes, err := json.Marshal(notice)
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: failed to marshal notice: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
handleNotice(notice, stdinBytes)
|
||||
}
|
||||
|
||||
func alertingCallScript(stdinBytes []byte) {
|
||||
if !config.C.Alerting.CallScript.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
// no notify.py? do nothing
|
||||
if config.C.Alerting.CallScript.ScriptPath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
fpath := config.C.Alerting.CallScript.ScriptPath
|
||||
cmd := exec.Command(fpath)
|
||||
cmd.Stdin = bytes.NewReader(stdinBytes)
|
||||
|
||||
// combine stdout and stderr
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &buf
|
||||
|
||||
err := startCmd(cmd)
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: run cmd err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err, isTimeout := sys.WrapTimeout(cmd, time.Duration(30)*time.Second)
|
||||
|
||||
if isTimeout {
|
||||
if err == nil {
|
||||
logger.Errorf("event_notify: timeout and killed process %s", fpath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: kill process %s occur error %v", fpath, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: exec script %s occur error: %v, output: %s", fpath, err, buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("event_notify: exec %s output: %s", fpath, buf.String())
|
||||
}
|
||||
|
||||
type Notifier interface {
|
||||
Descript() string
|
||||
Notify([]byte)
|
||||
}
|
||||
|
||||
// call notify.so via golang plugin build
|
||||
// ig. etc/script/notify/notify.so
|
||||
func alertingCallPlugin(stdinBytes []byte) {
|
||||
if !config.C.Alerting.CallPlugin.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
logger.Errorf("call notify plugin on unsupported os: %s", runtime.GOOS)
|
||||
return
|
||||
}
|
||||
|
||||
p, err := plugin.Open(config.C.Alerting.CallPlugin.PluginPath)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to open notify plugin: %v", err)
|
||||
return
|
||||
}
|
||||
caller, err := p.Lookup(config.C.Alerting.CallPlugin.Caller)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to load caller: %v", err)
|
||||
return
|
||||
}
|
||||
notifier, ok := caller.(Notifier)
|
||||
if !ok {
|
||||
logger.Errorf("notifier interface not implemented): %v", err)
|
||||
return
|
||||
}
|
||||
notifier.Notify(stdinBytes)
|
||||
logger.Debugf("alertingCallPlugin done. %s", notifier.Descript())
|
||||
return stdinBytes
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user