mirror of
https://github.com/ccfos/nightingale.git
synced 2026-03-03 22:48:56 +00:00
Compare commits
448 Commits
password-e
...
refactor-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c74014d53c | ||
|
|
7cc65a2ca7 | ||
|
|
7bb6c6541a | ||
|
|
8b4cfe65e3 | ||
|
|
7227de8c22 | ||
|
|
069e267af8 | ||
|
|
7c5c9a95c3 | ||
|
|
e3da7f344b | ||
|
|
dd741a177f | ||
|
|
4fdd25f020 | ||
|
|
62350bfbc6 | ||
|
|
5ee1baaf07 | ||
|
|
fa12889f06 | ||
|
|
39306a5bf0 | ||
|
|
0aea38e564 | ||
|
|
45e9253b2a | ||
|
|
9385ca9931 | ||
|
|
fdd3d14871 | ||
|
|
e890034c19 | ||
|
|
3aaab9e6ad | ||
|
|
7f7d707cfc | ||
|
|
98402e9f8a | ||
|
|
017094fd78 | ||
|
|
8b6b896362 | ||
|
|
acaa00cfb6 | ||
|
|
87f3d8595d | ||
|
|
42791a374d | ||
|
|
3855c25805 | ||
|
|
10ec0ccbd1 | ||
|
|
94cf304222 | ||
|
|
994de4635a | ||
|
|
9a0013a406 | ||
|
|
6dcd5dd01e | ||
|
|
70126e3aec | ||
|
|
767482d358 | ||
|
|
9a46106cc0 | ||
|
|
da9ea67cee | ||
|
|
c13ecd780b | ||
|
|
cab37c796a | ||
|
|
078578772b | ||
|
|
31883ec844 | ||
|
|
6100cd084a | ||
|
|
b82e260d65 | ||
|
|
3983386af3 | ||
|
|
83f2054062 | ||
|
|
83e0b3cb98 | ||
|
|
f6bfa17e2e | ||
|
|
3d8019b738 | ||
|
|
ee1be71be6 | ||
|
|
7f2fb459bb | ||
|
|
fde6a9c75e | ||
|
|
a2b506e263 | ||
|
|
30024a4951 | ||
|
|
2c3996812a | ||
|
|
51d35900f2 | ||
|
|
852fd2ea6e | ||
|
|
e1a57217ab | ||
|
|
1e7dad1a67 | ||
|
|
534e40ad63 | ||
|
|
15daa3826c | ||
|
|
d5efb5b6d4 | ||
|
|
7ebd776881 | ||
|
|
0e5cda1cee | ||
|
|
64dad19377 | ||
|
|
48f199f8f5 | ||
|
|
f7e4df7415 | ||
|
|
37fe01ab54 | ||
|
|
cbfe661bce | ||
|
|
890c12f0d4 | ||
|
|
643c6c997c | ||
|
|
b201836b40 | ||
|
|
b5eced1540 | ||
|
|
a13004eab7 | ||
|
|
a0c56548e5 | ||
|
|
e3d97386a8 | ||
|
|
051b0ca045 | ||
|
|
2941ced011 | ||
|
|
97d6908edd | ||
|
|
c7117b9461 | ||
|
|
78417b1d5b | ||
|
|
79f3404810 | ||
|
|
81e51c60eb | ||
|
|
af9cd55ca5 | ||
|
|
d4afdb2b6e | ||
|
|
2befc8b0f1 | ||
|
|
14fd2eb26d | ||
|
|
0a938518d7 | ||
|
|
0eed5afa7e | ||
|
|
f82eaf0a1f | ||
|
|
f03278d68d | ||
|
|
7d1e143f60 | ||
|
|
078a0c7b1c | ||
|
|
d9cac65a18 | ||
|
|
dd025ca87c | ||
|
|
04734b8940 | ||
|
|
bf7bcf4196 | ||
|
|
16195abb89 | ||
|
|
3f4891d65d | ||
|
|
102549c6a1 | ||
|
|
5213b1d7f1 | ||
|
|
24de97fb1e | ||
|
|
9c2cf679e0 | ||
|
|
2aa4941010 | ||
|
|
a812f14442 | ||
|
|
4fb7e8e2b5 | ||
|
|
113ad67104 | ||
|
|
49d843540a | ||
|
|
21f0e3310f | ||
|
|
31b3434e87 | ||
|
|
2576a0f815 | ||
|
|
0ac4bc7421 | ||
|
|
95e6ea98f4 | ||
|
|
dc60c74c0d | ||
|
|
a15adc196d | ||
|
|
f89ef04e85 | ||
|
|
f55cd9b32e | ||
|
|
305a898f8b | ||
|
|
60c31d8eb2 | ||
|
|
7da49a8c68 | ||
|
|
65b1410b09 | ||
|
|
3901671c0e | ||
|
|
9c02937e81 | ||
|
|
0a255ee33a | ||
|
|
8dc198b4b1 | ||
|
|
9696f63a71 | ||
|
|
03f56f73b4 | ||
|
|
7b415c91af | ||
|
|
2abf089444 | ||
|
|
e504dab359 | ||
|
|
989ed62e8d | ||
|
|
b7197d10eb | ||
|
|
f4de256388 | ||
|
|
3f5126923f | ||
|
|
5d3e70bc4c | ||
|
|
bb2c5202ad | ||
|
|
3acf3d7bf9 | ||
|
|
a79810b15d | ||
|
|
f61cb532f8 | ||
|
|
34a5a752f4 | ||
|
|
9be3deeebd | ||
|
|
2ceed84120 | ||
|
|
8fbe257090 | ||
|
|
ae35d780c6 | ||
|
|
4d2cdfce53 | ||
|
|
a0e4d0d46e | ||
|
|
dd07d04e2f | ||
|
|
61203e8b75 | ||
|
|
f24bc53c94 | ||
|
|
ef6abe3fdc | ||
|
|
461361d3d0 | ||
|
|
52b3afbd97 | ||
|
|
652439bb85 | ||
|
|
6f0c13d4e7 | ||
|
|
c9f46bad02 | ||
|
|
75146f3626 | ||
|
|
50aafbd73d | ||
|
|
b975cb3c9d | ||
|
|
11deb4ba26 | ||
|
|
ec927297d6 | ||
|
|
f476d7cd63 | ||
|
|
410f3bbceb | ||
|
|
2ad53d6862 | ||
|
|
fc392e4af1 | ||
|
|
9c83c7881a | ||
|
|
f1259d1dff | ||
|
|
d9d59b3205 | ||
|
|
d11cfb0278 | ||
|
|
5adcfc6eaa | ||
|
|
037152ad72 | ||
|
|
2de304d4f2 | ||
|
|
03c56d048f | ||
|
|
1cddb4eca0 | ||
|
|
2dc033944d | ||
|
|
63e6c78e71 | ||
|
|
e1f04eebe7 | ||
|
|
ce17e09f66 | ||
|
|
c98c1d3b90 | ||
|
|
ae3218e6d5 | ||
|
|
7497cc0f28 | ||
|
|
96c4cc7c98 | ||
|
|
1f7314f6b4 | ||
|
|
86d478a0d4 | ||
|
|
b45023630f | ||
|
|
2177049487 | ||
|
|
d3d1e7019f | ||
|
|
f2ad0b9594 | ||
|
|
9c79233b3c | ||
|
|
9ea5de1257 | ||
|
|
3ec97665ac | ||
|
|
bb4eeca2ab | ||
|
|
cc6a5be27f | ||
|
|
630df8a954 | ||
|
|
e28ab6368b | ||
|
|
751c78be4b | ||
|
|
5311bf90d5 | ||
|
|
c464689c6a | ||
|
|
442426be38 | ||
|
|
9a28139d43 | ||
|
|
25b768188f | ||
|
|
b794b62960 | ||
|
|
d7e00a5a49 | ||
|
|
19e6cfe7d2 | ||
|
|
63baa7b6f3 | ||
|
|
407fc90677 | ||
|
|
7da4c99d92 | ||
|
|
6b46e7e83f | ||
|
|
514ccd5f90 | ||
|
|
4565b80717 | ||
|
|
2bac6588c4 | ||
|
|
fc293cb01c | ||
|
|
73f9548242 | ||
|
|
7c91e51c08 | ||
|
|
a4867c406d | ||
|
|
bfea83ae75 | ||
|
|
7a2832c377 | ||
|
|
3f6c54a712 | ||
|
|
1bb590ce6d | ||
|
|
656326458f | ||
|
|
c6ab3ad2b3 | ||
|
|
d050cf72e9 | ||
|
|
084cc1893e | ||
|
|
cd01123b59 | ||
|
|
23ce84d41c | ||
|
|
4764cc2419 | ||
|
|
da66401576 | ||
|
|
0024c9d99c | ||
|
|
96d3b48f10 | ||
|
|
6a0e7a810f | ||
|
|
5b2513b7a1 | ||
|
|
7cec16eaf0 | ||
|
|
17dbb3ec77 | ||
|
|
00822c8404 | ||
|
|
55de30d6c7 | ||
|
|
8b7dbed27e | ||
|
|
71b8fa27d0 | ||
|
|
31174d719e | ||
|
|
5b5bb22ffd | ||
|
|
e98fe9ea2e | ||
|
|
32e9ded393 | ||
|
|
8293ca20be | ||
|
|
6c4ddfc349 | ||
|
|
cd0c478515 | ||
|
|
2cd25ac0e5 | ||
|
|
bb99ba3d1c | ||
|
|
64405dca5d | ||
|
|
69ea9ca8f8 | ||
|
|
41d0f2fcda | ||
|
|
93df1c0fbc | ||
|
|
86e952788d | ||
|
|
e890f2616f | ||
|
|
6c2ee584e5 | ||
|
|
5f07fc3010 | ||
|
|
20fa310ba9 | ||
|
|
0e3b08be9a | ||
|
|
b7d971d7c8 | ||
|
|
4373ae7f0b | ||
|
|
053325a691 | ||
|
|
c54267aa3a | ||
|
|
74dc430886 | ||
|
|
dc79ee4687 | ||
|
|
e154c946e6 | ||
|
|
08bfc0b388 | ||
|
|
5338270aef | ||
|
|
00550ba2c7 | ||
|
|
c58bec23bf | ||
|
|
a5b77be0ab | ||
|
|
f529681c35 | ||
|
|
e3042dd6d5 | ||
|
|
1ebab4fcb0 | ||
|
|
ccf38b6da7 | ||
|
|
9a0a687727 | ||
|
|
d00510978d | ||
|
|
9b478d98fd | ||
|
|
4845ca5bdb | ||
|
|
a844d2b091 | ||
|
|
69ca7f3b93 | ||
|
|
b9c6c33ceb | ||
|
|
5099d3c040 | ||
|
|
e34f8ac701 | ||
|
|
ab82a6f910 | ||
|
|
57f8bd3612 | ||
|
|
8ab96e2cea | ||
|
|
0a2e23c285 | ||
|
|
5c1d4077e2 | ||
|
|
2a46d9f98e | ||
|
|
ce5c213593 | ||
|
|
771a8d121b | ||
|
|
af88b0e283 | ||
|
|
8e5d7f2a5b | ||
|
|
1a22211a5d | ||
|
|
0a0049c6fb | ||
|
|
1b56ebe62e | ||
|
|
a5e92b95b0 | ||
|
|
8e9d06d43e | ||
|
|
ab289de785 | ||
|
|
8667b7743a | ||
|
|
45b9436f69 | ||
|
|
3d03bcf329 | ||
|
|
1851601889 | ||
|
|
fa9745decf | ||
|
|
6f007deeaa | ||
|
|
8fad705065 | ||
|
|
675076779e | ||
|
|
b9e78eee22 | ||
|
|
2219584abb | ||
|
|
ebe31fd6bc | ||
|
|
95ca69e170 | ||
|
|
ef1b5d8d16 | ||
|
|
5b375cf037 | ||
|
|
108b729cae | ||
|
|
a385972fa9 | ||
|
|
98a0a9d94c | ||
|
|
c79eec648d | ||
|
|
603eadd1f2 | ||
|
|
61a2f552be | ||
|
|
e3453328a7 | ||
|
|
4424a6b89c | ||
|
|
9fdb2f0753 | ||
|
|
3d358e367f | ||
|
|
5264874628 | ||
|
|
e0a3ff248c | ||
|
|
1fecf78ede | ||
|
|
839b45904b | ||
|
|
cd0f43f808 | ||
|
|
8047f3deee | ||
|
|
f209ed5bee | ||
|
|
8c61d8c14d | ||
|
|
f7372b1c3b | ||
|
|
a39ced86aa | ||
|
|
f365b7db2a | ||
|
|
7eaec13b6c | ||
|
|
2e824a165e | ||
|
|
f2909b6029 | ||
|
|
a543a5ad09 | ||
|
|
2ee34bf1f9 | ||
|
|
4623622dd0 | ||
|
|
4f259137e5 | ||
|
|
75f1e8a80b | ||
|
|
3648d8dc45 | ||
|
|
8c90d7ab33 | ||
|
|
c6ac3fb959 | ||
|
|
ce854b3166 | ||
|
|
a2be5230fa | ||
|
|
21276a77b6 | ||
|
|
cffd012ec6 | ||
|
|
a9ebdad1cd | ||
|
|
785c577728 | ||
|
|
0e2a66570e | ||
|
|
76583a6227 | ||
|
|
48e0e1a9f8 | ||
|
|
17bb7fa468 | ||
|
|
fc2638680a | ||
|
|
e01a899ae1 | ||
|
|
07c1ef6bd4 | ||
|
|
bfa7059098 | ||
|
|
096a2d3675 | ||
|
|
2232733922 | ||
|
|
b15f638688 | ||
|
|
4f818e3642 | ||
|
|
638c62da2f | ||
|
|
e1a9c995c2 | ||
|
|
1898675075 | ||
|
|
ce7f0272d8 | ||
|
|
93159f07fd | ||
|
|
7d410baa2d | ||
|
|
20b30c3e2c | ||
|
|
8805bf6598 | ||
|
|
fe6a64dae8 | ||
|
|
2c564a2c58 | ||
|
|
ae3c13224d | ||
|
|
9a4015f13f | ||
|
|
274ca09551 | ||
|
|
3d9b4fc14e | ||
|
|
07436a5e0d | ||
|
|
f7b2f1acb9 | ||
|
|
4f4287030a | ||
|
|
e25e712c48 | ||
|
|
66951d7e77 | ||
|
|
f5ff27cd18 | ||
|
|
9e3f6e6285 | ||
|
|
48e3df2cb4 | ||
|
|
ac5d69dba4 | ||
|
|
597351c424 | ||
|
|
1f6b2e341a | ||
|
|
035752ace2 | ||
|
|
60a1437207 | ||
|
|
e31414bc8c | ||
|
|
785a294845 | ||
|
|
98933eee34 | ||
|
|
20905810d7 | ||
|
|
c1bde83639 | ||
|
|
782a0e9616 | ||
|
|
6a3720bc8b | ||
|
|
de252359d6 | ||
|
|
deb313ca3d | ||
|
|
d119de56be | ||
|
|
f05417fa23 | ||
|
|
9ab2eb591f | ||
|
|
3f476d770f | ||
|
|
ced6759686 | ||
|
|
eba3014c59 | ||
|
|
3aeb4e16e9 | ||
|
|
3b62722251 | ||
|
|
fb1cc4868e | ||
|
|
4a0dcf0dbf | ||
|
|
4f913f146e | ||
|
|
533560f432 | ||
|
|
cf7b479a1b | ||
|
|
2e4c29a0de | ||
|
|
6f0ceb94c6 | ||
|
|
800d7ba04b | ||
|
|
fb6a6d2b93 | ||
|
|
cf2b19ae90 | ||
|
|
fb1cc93613 | ||
|
|
c2bba796c2 | ||
|
|
a02bf83842 | ||
|
|
cd9f129e2d | ||
|
|
e85c80bdcf | ||
|
|
7e83e0c482 | ||
|
|
92ac3125f3 | ||
|
|
a61feca369 | ||
|
|
8b0b811919 | ||
|
|
8742526c7f | ||
|
|
ee757cfd92 | ||
|
|
b12cfea379 | ||
|
|
45365e3e03 | ||
|
|
1b676eefd2 | ||
|
|
0092dc44fd | ||
|
|
4941b376f3 | ||
|
|
e46813cd17 | ||
|
|
58ebd224c2 | ||
|
|
95ece6e16f | ||
|
|
b82cbd06fa | ||
|
|
16210892da | ||
|
|
a452d63a56 | ||
|
|
51c7abedd3 | ||
|
|
6d0a2420a8 | ||
|
|
9cf687b73d | ||
|
|
49c9e41df5 | ||
|
|
2ec2e64213 | ||
|
|
867a61c8dc | ||
|
|
12263d1453 | ||
|
|
c0cacb2e64 | ||
|
|
0637b343b1 | ||
|
|
2473e144ef | ||
|
|
00a37d6de7 | ||
|
|
50c664e6bf | ||
|
|
22b7d20455 |
67
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
67
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,67 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Report a bug encountered while running Nightingale
|
||||
labels: ["kind/bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking time to fill out this bug report!
|
||||
The more detailed the form is filled in, the easier the problem will be solved.
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Your config.toml
|
||||
description: Place config in the toml code section. This will be automatically formatted into toml, so no need for backticks.
|
||||
render: toml
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant logs
|
||||
description: categraf | telegraf | n9e | prometheus | chrome request/response ...
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System info
|
||||
description: Include nightingale version, operating system, and other relevant details
|
||||
placeholder: ex. n9e 5.9.2, n9e-fe 5.5.0, categraf 0.1.0, Ubuntu 20.04, Docker 20.10.8
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Describe the steps to reproduce the bug.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Describe what you expected to happen when you performed the above steps.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: Describe what actually happened when you performed the above steps.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional info
|
||||
description: Include gist of relevant config, logs, etc.
|
||||
validations:
|
||||
required: false
|
||||
33
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Bug Report & Usage Question
|
||||
description: Reporting a bug or asking a question about how to use Nightingale
|
||||
labels: []
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
The more detailed the form is filled in, the easier the problem will be solved.
|
||||
提供的信息越详细,问题解决的可能性就越大。另外, 提问之前请先搜索历史 issue (包括 close 的), 以免重复提问。
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question and Steps to reproduce
|
||||
description: Describe your question and steps to reproduce the bug. 描述问题以及复现步骤
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant logs and configurations
|
||||
description: Relevant logs and configurations. 报错日志([查看方法](https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v6/faq/how-to-check-logs/))以及各个相关组件的配置信息
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: Version
|
||||
description: Include nightingale version, operating system, and other relevant details. 请告知夜莺的版本、操作系统的版本、CPU架构等信息
|
||||
validations:
|
||||
required: true
|
||||
|
||||
5
.github/workflows/n9e.yml
vendored
5
.github/workflows/n9e.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
env:
|
||||
GO_VERSION: 1.18
|
||||
GO_VERSION: 1.23
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
@@ -26,7 +26,8 @@ jobs:
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: latest
|
||||
distribution: goreleaser
|
||||
version: '~> v1'
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
*.db
|
||||
*.sw[po]
|
||||
*.tar.gz
|
||||
*.[568vq]
|
||||
|
||||
64
README.md
64
README.md
@@ -28,52 +28,80 @@
|
||||
[English](./README_en.md) | [中文](./README.md)
|
||||
|
||||
## 夜莺 Nightingale 是什么
|
||||
夜莺 Nightingale 是中国计算机学会接受捐赠并托管的第一个开源项目,是一个 All-in-One 的云原生监控工具,集合了 Prometheus 和 Grafana 的优点,你可以在 WebUI 上管理和配置告警策略,也可以对分布在多个 Region 的指标、日志、链路追踪数据进行统一的可视化和分析。夜莺融入了一线互联网公司可观测性最佳实践,沉淀了众多社区专家经验,开箱即用。[了解更多...](https://flashcat.cloud/product/nightingale/)
|
||||
|
||||
夜莺监控是一款开源云原生观测分析工具,采用 All-in-One 的设计理念,集数据采集、可视化、监控告警、数据分析于一体,与云原生生态紧密集成,提供开箱即用的企业级监控分析和告警能力。夜莺于 2020 年 3 月 20 日,在 GitHub 上发布 v1 版本,已累计迭代 100 多个版本。
|
||||
|
||||
夜莺最初由滴滴开发和开源,并于 2022 年 5 月 11 日,捐赠予中国计算机学会开源发展委员会(CCF ODC),为 CCF ODC 成立后接受捐赠的第一个开源项目。夜莺的核心研发团队,也是 Open-Falcon 项目原核心研发人员,从 2014 年(Open-Falcon 是 2014 年开源)算起来,也有 10 年了,只为把监控这个事情做好。
|
||||
|
||||
|
||||
## 快速开始
|
||||
- 👉[文档](https://flashcat.cloud/docs/) | [提问](https://answer.flashcat.cloud/) | [下载](https://flashcat.cloud/download/nightingale/) | [安装](https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v6/install/intro/)
|
||||
- ❤️[报告 Bug](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml)
|
||||
- ℹ️为了提供更快速的访问体验,上述文档和下载站点托管于 [FlashcatCloud](https://flashcat.cloud)
|
||||
- 👉 [文档中心](https://flashcat.cloud/docs/) | [下载中心](https://flashcat.cloud/download/nightingale/)
|
||||
- ❤️ [报告 Bug](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=&projects=&template=question.yml)
|
||||
- ℹ️ 为了提供更快速的访问体验,上述文档和下载站点托管于 [FlashcatCloud](https://flashcat.cloud)
|
||||
- 💡 前后端代码分离,前端代码仓库:[https://github.com/n9e/fe](https://github.com/n9e/fe)
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 对接多种时序库:支持对接 Prometheus、VictoriaMetrics、Thanos、Mimir、M3DB、TDengine 等多种时序库,实现统一告警管理。
|
||||
- 专业告警能力:内置支持多种告警规则,可以扩展支持常见通知媒介,支持告警屏蔽/抑制/聚合/自愈、告警事件管理。
|
||||
- 专业告警能力:内置支持多种告警规则,可以扩展支持常见通知媒介,支持告警屏蔽/抑制/订阅/自愈、告警事件管理。
|
||||
- 高性能可视化引擎:支持多种图表样式,内置众多 Dashboard 模版,也可导入 Grafana 模版,开箱即用,开源协议商业友好。
|
||||
- 支持常见采集器:支持 [Categraf](https://flashcat.cloud/product/categraf)、Telegraf、Grafana-agent、Datadog-agent、各种 Exporter 作为采集器,没有什么数据是不能监控的。
|
||||
- 一体化观测平台:从 V6 版本开始,支持对接 ElasticSearch、Jaeger 数据源,实现日志、链路、指标多维度的统一可观测。
|
||||
- 👀无缝搭配 [Flashduty](https://flashcat.cloud/product/flashcat-duty/):实现告警聚合收敛、认领、升级、排班、IM集成,确保告警处理不遗漏,减少打扰,高效协同。
|
||||
|
||||
|
||||
## 功能演示
|
||||

|
||||
## 截图演示
|
||||
|
||||
## 部署架构
|
||||
<p align=center>中心化部署</p>
|
||||
|
||||

|
||||
你可以在页面的右上角,切换语言和主题,目前我们支持英语、简体中文、繁体中文。
|
||||
|
||||
<p align=center>多机房部署</p>
|
||||

|
||||
|
||||
即时查询,类似 Prometheus 内置的查询分析页面,做 ad-hoc 查询,夜莺做了一些 UI 优化,同时提供了一些内置 promql 指标,让不太了解 promql 的用户也可以快速查询。
|
||||
|
||||

|
||||
|
||||
当然,也可以直接通过指标视图查看,有了指标视图,即时查询基本可以不用了,或者只有高端玩家使用即时查询,普通用户直接通过指标视图查询即可。
|
||||
|
||||

|
||||
|
||||
夜莺内置了常用仪表盘,可以直接导入使用。也可以导入 Grafana 仪表盘,不过只能兼容 Grafana 基本图表,如果已经习惯了 Grafana 建议继续使用 Grafana 看图,把夜莺作为一个告警引擎使用。
|
||||
|
||||

|
||||
|
||||
除了内置的仪表盘,也内置了很多告警规则,开箱即用。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 产品架构
|
||||
|
||||
社区使用夜莺最多的场景就是使用夜莺做告警引擎,对接多套时序库,统一告警规则管理。绘图仍然使用 Grafana 居多。作为一个告警引擎,夜莺的产品架构如下:
|
||||
|
||||

|
||||
|
||||
对于个别边缘机房,如果和中心夜莺服务端网络链路不好,希望提升告警可用性,我们也提供边缘机房告警引擎下沉部署模式,这个模式下,即便网络割裂,告警功能也不受影响。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 交流渠道
|
||||
- 报告Bug,优先推荐提交[夜莺GitHub Issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml)
|
||||
- 推荐完整浏览[夜莺文档站点](https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v7/introduction/),了解更多信息
|
||||
- 推荐搜索关注夜莺公众号,第一时间获取社区动态:`夜莺监控Nightingale`
|
||||
- 日常答疑、技术分享、用户之间的交流,统一使用知识星球,大伙可以免费加入交流,[入口在这里](https://download.flashcat.cloud/ulric/20240319095409.png)
|
||||
- 日常问题交流:
|
||||
- QQ群:730841964
|
||||
- [加入微信群](https://download.flashcat.cloud/ulric/20241022141621.png),如果二维码过期了,可以联系我(我的微信:`picobyte`)拉群,备注: `夜莺互助群`
|
||||
|
||||
## 广受关注
|
||||
[](https://star-history.com/#ccfos/nightingale&Date)
|
||||
|
||||
|
||||
## 社区共建
|
||||
- ❇️请阅读浏览[夜莺开源项目和社区治理架构草案](./doc/community-governance.md),真诚欢迎每一位用户、开发者、公司以及组织,使用夜莺监控、积极反馈 Bug、提交功能需求、分享最佳实践,共建专业、活跃的夜莺开源社区。
|
||||
- 夜莺贡献者❤️
|
||||
- ❇️ 请阅读浏览[夜莺开源项目和社区治理架构草案](./doc/community-governance.md),真诚欢迎每一位用户、开发者、公司以及组织,使用夜莺监控、积极反馈 Bug、提交功能需求、分享最佳实践,共建专业、活跃的夜莺开源社区。
|
||||
- ❤️ 夜莺贡献者
|
||||
<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)
|
||||
|
||||
129
README_en.md
129
README_en.md
@@ -1,104 +1,113 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/ccfos/nightingale">
|
||||
<img src="doc/img/Nightingale_L_V.png" alt="nightingale - cloud native monitoring" width="240" /></a>
|
||||
<img src="doc/img/Nightingale_L_V.png" alt="nightingale - cloud native monitoring" width="100" /></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<b>Open-source Alert Management Expert, an Integrated Observability Platform</b>
|
||||
</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">
|
||||
<a href="https://flashcat.cloud/docs/">
|
||||
<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="GitHub Repo stars" src="https://img.shields.io/github/stars/ccfos/nightingale">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/ccfos/nightingale">
|
||||
<br/><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 latest release" src="https://img.shields.io/github/v/release/ccfos/nightingale"/>
|
||||
<img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue"/>
|
||||
<a href="https://n9e-talk.slack.com/">
|
||||
<img alt="GitHub contributors" src="https://img.shields.io/badge/join%20slack-%23n9e-brightgreen.svg"/></a>
|
||||
<img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue"/>
|
||||
</p>
|
||||
<p align="center">
|
||||
An open-source cloud-native monitoring system that is <b>all-in-one</b> <br/>
|
||||
<b>Out-of-the-box</b>, it integrates data collection, visualization, and monitoring alert <br/>
|
||||
We recommend upgrading your <b>Prometheus + AlertManager + Grafana</b> combination to Nightingale!
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
[English](./README_en.md) | [中文](./README.md)
|
||||
|
||||
## What is Nightingale
|
||||
|
||||
## Highlighted Features
|
||||
Nightingale aims to combine the advantages of Prometheus and Grafana. It manages alert rules and visualizes metrics, logs, traces in a beautiful WebUI.
|
||||
|
||||
- **Out-of-the-box**
|
||||
- Supports multiple deployment methods such as **Docker, Helm Chart, and cloud services**, integrates data collection, monitoring, and alerting into one system, and comes with various monitoring dashboards, quick views, and alert rule templates. **It greatly reduces the construction cost, learning cost, and usage cost of cloud-native monitoring systems**.
|
||||
- **Professional Alerting**
|
||||
- Provides visual alert configuration and management, supports various alert rules, offers the ability to configure silence and subscription rules, supports multiple alert delivery channels, and has features such as alert self-healing and event management.
|
||||
- **Cloud-Native**
|
||||
- Quickly builds an enterprise-level cloud-native monitoring system through a turnkey approach, supports multiple collectors such as [Categraf](https://github.com/flashcatcloud/categraf), Telegraf, and Grafana-agent, supports multiple data sources such as Prometheus, VictoriaMetrics, M3DB, ElasticSearch, and Jaeger, and is compatible with importing Grafana dashboards. **It seamlessly integrates with the cloud-native ecosystem**.
|
||||
- **High Performance and High Availability**
|
||||
- Due to the multi-data-source management engine of Nightingale and its excellent architecture design, and utilizing a high-performance time-series database, it can handle data collection, storage, and alert analysis scenarios with billions of time-series data, saving a lot of costs.
|
||||
- Nightingale components can be horizontally scaled with no single point of failure. It has been deployed in thousands of enterprises and tested in harsh production practices. Many leading Internet companies have used Nightingale for cluster machines with hundreds of nodes, processing billions of time-series data.
|
||||
- **Flexible Extension and Centralized Management**
|
||||
- Nightingale can be deployed on a 1-core 1G cloud host, deployed in a cluster of hundreds of machines, or run in Kubernetes. Time-series databases, alert engines, and other components can also be decentralized to various data centers and regions, balancing edge deployment with centralized management. **It solves the problem of data fragmentation and lack of unified views**.
|
||||
Originally developed and open-sourced by Didi, Nightingale was donated to the China Computer Federation Open Source Development Committee (CCF ODC) on May 11, 2022, becoming the first open-source project accepted by the CCF ODC after its establishment.
|
||||
|
||||
|
||||
#### If you are using Prometheus and have one or more of the following requirement scenarios, it is recommended that you upgrade to Nightingale:
|
||||
## Quick Start
|
||||
|
||||
- Multiple systems such as Prometheus, Alertmanager, Grafana, etc. are fragmented and lack a unified view and cannot be used out of the box;
|
||||
- The way to manage Prometheus and Alertmanager by modifying configuration files has a big learning curve and is difficult to collaborate;
|
||||
- Too much data to scale-up your Prometheus cluster;
|
||||
- Multiple Prometheus clusters running in production environments, which faced high management and usage costs;
|
||||
- 👉 [Documentation](https://flashcat.cloud/docs/) | [Download](https://flashcat.cloud/download/nightingale/)
|
||||
- ❤️ [Report a Bug](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=&projects=&template=question.yml)
|
||||
- ℹ️ For faster access, the above documentation and download sites are hosted on [FlashcatCloud](https://flashcat.cloud).
|
||||
|
||||
#### If you are using Zabbix and have the following scenarios, it is recommended that you upgrade to Nightingale:
|
||||
## Features
|
||||
|
||||
- Monitoring too much data and wanting a better scalable solution;
|
||||
- A high learning curve and a desire for better efficiency of collaborative use in a multi-person, multi-team model;
|
||||
- Microservice and cloud-native architectures with variable monitoring data lifecycles and high monitoring data dimension bases, which are not easily adaptable to the Zabbix data model;
|
||||
- **Integration with Multiple Time-Series Databases:** Supports integration with various time-series databases such as Prometheus, VictoriaMetrics, Thanos, Mimir, M3DB, and TDengine, enabling unified alert management.
|
||||
- **Advanced Alerting Capabilities:** Comes with built-in support for multiple alerting rules, extensible to common notification channels. It also supports alert suppression, silencing, subscription, self-healing, and alert event management.
|
||||
- **High-Performance Visualization Engine:** Offers various chart styles with numerous built-in dashboard templates and the ability to import Grafana templates. Ready to use with a business-friendly open-source license.
|
||||
- **Support for Common Collectors:** Compatible with [Categraf](https://flashcat.cloud/product/categraf), Telegraf, Grafana-agent, Datadog-agent, and various exporters as collectors—there's no data that can't be monitored.
|
||||
- **Seamless Integration with [Flashduty](https://flashcat.cloud/product/flashcat-duty/):** Enables alert aggregation, acknowledgment, escalation, scheduling, and IM integration, ensuring no alerts are missed, reducing unnecessary interruptions, and enhancing efficient collaboration.
|
||||
|
||||
|
||||
#### If you are using [open-falcon](https://github.com/open-falcon/falcon-plus), we recommend you to upgrade to Nightingale:
|
||||
- For more information about open-falcon and Nightingale, please refer to read [Ten features and trends of cloud-native monitoring](https://mp.weixin.qq.com/s?__biz=MzkzNjI5OTM5Nw==&mid=2247483738&idx=1&sn=e8bdbb974a2cd003c1abcc2b5405dd18&chksm=c2a19fb0f5d616a63185cd79277a79a6b80118ef2185890d0683d2bb20451bd9303c78d083c5#rd)。
|
||||
|
||||
## Getting Started
|
||||
|
||||
[https://n9e.github.io/](https://n9e.github.io/)
|
||||
|
||||
## Screenshots
|
||||
|
||||
https://user-images.githubusercontent.com/792850/216888712-2565fcea-9df5-47bd-a49e-d60af9bd76e8.mp4
|
||||
You can switch languages and themes in the top right corner. We now support English, Simplified Chinese, and Traditional Chinese.
|
||||
|
||||

|
||||
|
||||
### Instant Query
|
||||
|
||||
Similar to the built-in query analysis page in Prometheus, Nightingale offers an ad-hoc query feature with UI enhancements. It also provides built-in PromQL metrics, allowing users unfamiliar with PromQL to quickly perform queries.
|
||||
|
||||

|
||||
|
||||
### Metric View
|
||||
|
||||
Alternatively, you can use the Metric View to access data. With this feature, Instant Query becomes less necessary, as it caters more to advanced users. Regular users can easily perform queries using the Metric View.
|
||||
|
||||

|
||||
|
||||
### Built-in Dashboards
|
||||
|
||||
Nightingale includes commonly used dashboards that can be imported and used directly. You can also import Grafana dashboards, although compatibility is limited to basic Grafana charts. If you’re accustomed to Grafana, it’s recommended to continue using it for visualization, with Nightingale serving as an alerting engine.
|
||||
|
||||

|
||||
|
||||
### Built-in Alert Rules
|
||||
|
||||
In addition to the built-in dashboards, Nightingale also comes with numerous alert rules that are ready to use out of the box.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
<img src="doc/img/arch-product.png" width="600">
|
||||
In most community scenarios, Nightingale is primarily used as an alert engine, integrating with multiple time-series databases to unify alert rule management. Grafana remains the preferred tool for visualization. As an alert engine, the product architecture of Nightingale is as follows:
|
||||
|
||||
Nightingale monitoring can receive monitoring data reported by various collectors (such as [Categraf](https://github.com/flashcatcloud/categraf) , telegraf, grafana-agent, Prometheus, etc.) and write them to various popular time-series databases (such as Prometheus, M3DB, VictoriaMetrics, Thanos, TDEngine, etc.). It provides configuration capabilities for alert rules, silence rules, and subscription rules, as well as the ability to view monitoring data. It also provides automatic alarm self-healing mechanisms (such as automatically calling back to a webhook address or executing a script after an alarm is triggered), and the ability to store and manage historical alarm events and view them in groups.
|
||||

|
||||
|
||||
If the performance of a standalone time-series database (such as Prometheus) has bottlenecks or poor disaster recovery, we recommend using [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics). The VictoriaMetrics architecture is relatively simple, has excellent performance, and is easy to deploy and maintain. The architecture diagram is as shown above. For more detailed documentation on VictoriaMetrics, please refer to its [official website](https://victoriametrics.com/).
|
||||
For certain edge data centers with poor network connectivity to the central Nightingale server, we offer a distributed deployment mode for the alert engine. In this mode, even if the network is disconnected, the alerting functionality remains unaffected.
|
||||
|
||||
**We welcome you to participate in the Nightingale open-source project and community in various ways, including but not limited to**:
|
||||
- Adding and improving documentation => [n9e.github.io](https://n9e.github.io/)
|
||||
- Sharing your best practices and experience in using Nightingale monitoring => [Article sharing]((https://n9e.github.io/docs/prologue/share/))
|
||||
- Submitting product suggestions => [github issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md)
|
||||
- Submitting code to make Nightingale monitoring faster, more stable, and easier to use => [github pull request](https://github.com/didi/nightingale/pulls)
|
||||

|
||||
|
||||
|
||||
**Respecting, recognizing, and recording the work of every contributor** is the first guiding principle of the Nightingale open-source community. We advocate effective questioning, which not only respects the developer's time but also contributes to the accumulation of knowledge in the entire community
|
||||
- Before asking a question, please first refer to the [FAQ](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq)
|
||||
- We use [GitHub Discussions](https://github.com/ccfos/nightingale/discussions) as the communication forum. You can search and ask questions here.
|
||||
- We also recommend that you join ours [Slack channel](https://n9e-talk.slack.com/) to exchange experiences with other Nightingale users.
|
||||
## Communication Channels
|
||||
|
||||
|
||||
## Who is using Nightingale
|
||||
You can register your usage and share your experience by posting on **[Who is Using Nightingale](https://github.com/ccfos/nightingale/issues/897)**.
|
||||
- **Report Bugs:** It is highly recommended to submit issues via the [Nightingale GitHub Issue tracker](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml).
|
||||
- **Documentation:** For more information, we recommend thoroughly browsing the [Nightingale Documentation Site](https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v7/introduction/).
|
||||
|
||||
## Stargazers over time
|
||||
[](https://starchart.cc/ccfos/nightingale)
|
||||
|
||||
## Contributors
|
||||
[](https://star-history.com/#ccfos/nightingale&Date)
|
||||
|
||||
## Community Co-Building
|
||||
|
||||
- ❇️ Please read the [Nightingale Open Source Project and Community Governance Draft](./doc/community-governance.md). We sincerely welcome every user, developer, company, and organization to use Nightingale, actively report bugs, submit feature requests, share best practices, and help build a professional and active open-source community.
|
||||
- ❤️ 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)
|
||||
|
||||
@@ -32,6 +32,7 @@ type Alerting struct {
|
||||
Timeout int64
|
||||
TemplatesDir string
|
||||
NotifyConcurrency int
|
||||
WebhookBatchSend bool
|
||||
}
|
||||
|
||||
type CallPlugin struct {
|
||||
@@ -46,13 +47,6 @@ type RedisPub struct {
|
||||
ChannelKey string
|
||||
}
|
||||
|
||||
type Ibex struct {
|
||||
Address string
|
||||
BasicAuthUser string
|
||||
BasicAuthPass string
|
||||
Timeout int64
|
||||
}
|
||||
|
||||
func (a *Alert) PreCheck(configDir string) {
|
||||
if a.Alerting.TemplatesDir == "" {
|
||||
a.Alerting.TemplatesDir = path.Join(configDir, "template")
|
||||
@@ -66,10 +60,6 @@ func (a *Alert) PreCheck(configDir string) {
|
||||
a.Heartbeat.Interval = 1000
|
||||
}
|
||||
|
||||
if a.Heartbeat.EngineName == "" {
|
||||
a.Heartbeat.EngineName = "default"
|
||||
}
|
||||
|
||||
if a.EngineDelay == 0 {
|
||||
a.EngineDelay = 30
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/pconf"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
"github.com/flashcatcloud/ibex/src/cmd/ibex"
|
||||
)
|
||||
|
||||
func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
@@ -40,11 +43,17 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
|
||||
ctx := ctx.NewContext(context.Background(), nil, false, config.CenterApi)
|
||||
|
||||
var redis storage.Redis
|
||||
redis, err = storage.NewRedis(config.Redis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncStats := memsto.NewSyncStats()
|
||||
alertStats := astats.NewSyncStats()
|
||||
|
||||
configCache := memsto.NewConfigCache(ctx, syncStats, nil, "")
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, nil)
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, redis)
|
||||
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
|
||||
alertMuteCache := memsto.NewAlertMuteCache(ctx, syncStats)
|
||||
alertRuleCache := memsto.NewAlertRuleCache(ctx, syncStats)
|
||||
@@ -52,16 +61,25 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
dsCache := memsto.NewDatasourceCache(ctx, syncStats)
|
||||
userCache := memsto.NewUserCache(ctx, syncStats)
|
||||
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
|
||||
taskTplsCache := memsto.NewTaskTplCache(ctx)
|
||||
configCvalCache := memsto.NewCvalCache(ctx, syncStats)
|
||||
|
||||
promClients := prom.NewPromClient(ctx)
|
||||
dispatch.InitRegisterQueryFunc(promClients)
|
||||
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
|
||||
|
||||
externalProcessors := process.NewExternalProcessors()
|
||||
|
||||
Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
|
||||
Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
|
||||
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP,
|
||||
configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
|
||||
rt := router.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
|
||||
|
||||
if config.Ibex.Enable {
|
||||
ibex.ServerStart(false, nil, redis, config.HTTP.APIForService.BasicAuth, config.Alert.Heartbeat, &config.CenterApi, r, nil, config.Ibex, config.HTTP.Port)
|
||||
}
|
||||
|
||||
rt.Config(r)
|
||||
dumper.ConfigRouter(r)
|
||||
|
||||
@@ -74,7 +92,7 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
}
|
||||
|
||||
func Start(alertc aconf.Alert, pushgwc pconf.Pushgw, syncStats *memsto.Stats, alertStats *astats.Stats, externalProcessors *process.ExternalProcessorsType, targetCache *memsto.TargetCacheType, busiGroupCache *memsto.BusiGroupCacheType,
|
||||
alertMuteCache *memsto.AlertMuteCacheType, alertRuleCache *memsto.AlertRuleCacheType, notifyConfigCache *memsto.NotifyConfigCacheType, datasourceCache *memsto.DatasourceCacheType, ctx *ctx.Context,
|
||||
alertMuteCache *memsto.AlertMuteCacheType, alertRuleCache *memsto.AlertRuleCacheType, notifyConfigCache *memsto.NotifyConfigCacheType, taskTplsCache *memsto.TaskTplCache, datasourceCache *memsto.DatasourceCacheType, ctx *ctx.Context,
|
||||
promClients *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType) {
|
||||
alertSubscribeCache := memsto.NewAlertSubscribeCache(ctx, syncStats)
|
||||
recordingRuleCache := memsto.NewRecordingRuleCache(ctx, syncStats)
|
||||
@@ -85,17 +103,17 @@ func Start(alertc aconf.Alert, pushgwc pconf.Pushgw, syncStats *memsto.Stats, al
|
||||
naming := naming.NewNaming(ctx, alertc.Heartbeat, alertStats)
|
||||
|
||||
writers := writer.NewWriters(pushgwc)
|
||||
record.NewScheduler(alertc, recordingRuleCache, promClients, writers, alertStats)
|
||||
record.NewScheduler(alertc, recordingRuleCache, promClients, writers, alertStats, datasourceCache)
|
||||
|
||||
eval.NewScheduler(alertc, externalProcessors, alertRuleCache, targetCache, targetsOfAlertRulesCache,
|
||||
busiGroupCache, alertMuteCache, datasourceCache, promClients, tdendgineClients, naming, ctx, alertStats)
|
||||
|
||||
dp := dispatch.NewDispatch(alertRuleCache, userCache, userGroupCache, alertSubscribeCache, targetCache, notifyConfigCache, alertc.Alerting, ctx, alertStats)
|
||||
consumer := dispatch.NewConsumer(alertc.Alerting, ctx, dp)
|
||||
dp := dispatch.NewDispatch(alertRuleCache, userCache, userGroupCache, alertSubscribeCache, targetCache, notifyConfigCache, taskTplsCache, alertc.Alerting, ctx, alertStats)
|
||||
consumer := dispatch.NewConsumer(alertc.Alerting, ctx, dp, promClients)
|
||||
|
||||
go dp.ReloadTpls()
|
||||
go consumer.LoopConsume()
|
||||
|
||||
go queue.ReportQueueSize(alertStats)
|
||||
go sender.InitEmailSender(notifyConfigCache)
|
||||
go sender.InitEmailSender(ctx, notifyConfigCache)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func NewSyncStats() *Stats {
|
||||
Subsystem: subsystem,
|
||||
Name: "rule_eval_error_total",
|
||||
Help: "Number of rule eval error.",
|
||||
}, []string{"datasource", "stage"})
|
||||
}, []string{"datasource", "stage", "busi_group", "rule_id"})
|
||||
|
||||
CounterQueryDataErrorTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
|
||||
@@ -2,6 +2,7 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
@@ -34,9 +35,9 @@ func MatchGroupsName(groupName string, groupFilter []models.TagFilter) bool {
|
||||
func matchTag(value string, filter models.TagFilter) bool {
|
||||
switch filter.Func {
|
||||
case "==":
|
||||
return filter.Value == value
|
||||
return strings.TrimSpace(filter.Value) == strings.TrimSpace(value)
|
||||
case "!=":
|
||||
return filter.Value != value
|
||||
return strings.TrimSpace(filter.Value) != strings.TrimSpace(value)
|
||||
case "in":
|
||||
_, has := filter.Vset[value]
|
||||
return has
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/common"
|
||||
"github.com/ccfos/nightingale/v6/alert/queue"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
promsdk "github.com/ccfos/nightingale/v6/pkg/prom"
|
||||
"github.com/ccfos/nightingale/v6/pkg/tplx"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/toolkits/pkg/concurrent/semaphore"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
@@ -18,15 +26,29 @@ type Consumer struct {
|
||||
alerting aconf.Alerting
|
||||
ctx *ctx.Context
|
||||
|
||||
dispatch *Dispatch
|
||||
dispatch *Dispatch
|
||||
promClients *prom.PromClientMap
|
||||
}
|
||||
|
||||
func InitRegisterQueryFunc(promClients *prom.PromClientMap) {
|
||||
tplx.RegisterQueryFunc(func(datasourceID int64, promql string) model.Value {
|
||||
if promClients.IsNil(datasourceID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
readerClient := promClients.GetCli(datasourceID)
|
||||
value, _, _ := readerClient.Query(context.Background(), promql, time.Now())
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
// 创建一个 Consumer 实例
|
||||
func NewConsumer(alerting aconf.Alerting, ctx *ctx.Context, dispatch *Dispatch) *Consumer {
|
||||
func NewConsumer(alerting aconf.Alerting, ctx *ctx.Context, dispatch *Dispatch, promClients *prom.PromClientMap) *Consumer {
|
||||
return &Consumer{
|
||||
alerting: alerting,
|
||||
ctx: ctx,
|
||||
dispatch: dispatch,
|
||||
alerting: alerting,
|
||||
ctx: ctx,
|
||||
dispatch: dispatch,
|
||||
promClients: promClients,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,17 +95,19 @@ func (e *Consumer) consumeOne(event *models.AlertCurEvent) {
|
||||
event.RuleName = fmt.Sprintf("failed to parse rule name: %v", err)
|
||||
}
|
||||
|
||||
if err := event.ParseRule("rule_note"); err != nil {
|
||||
logger.Warningf("ruleid:%d failed to parse rule note: %v", event.RuleId, err)
|
||||
event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
|
||||
}
|
||||
|
||||
if err := event.ParseRule("annotations"); err != nil {
|
||||
logger.Warningf("ruleid:%d failed to parse annotations: %v", event.RuleId, err)
|
||||
event.Annotations = fmt.Sprintf("failed to parse annotations: %v", err)
|
||||
event.AnnotationsJSON["error"] = event.Annotations
|
||||
}
|
||||
|
||||
e.queryRecoveryVal(event)
|
||||
|
||||
if err := event.ParseRule("rule_note"); err != nil {
|
||||
logger.Warningf("ruleid:%d failed to parse rule note: %v", event.RuleId, err)
|
||||
event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
|
||||
}
|
||||
|
||||
e.persist(event)
|
||||
|
||||
if event.IsRecovered && event.NotifyRecovered == 0 {
|
||||
@@ -104,7 +128,7 @@ func (e *Consumer) persist(event *models.AlertCurEvent) {
|
||||
event.Id, err = poster.PostByUrlsWithResp[int64](e.ctx, "/v1/n9e/event-persist", event)
|
||||
if err != nil {
|
||||
logger.Errorf("event:%+v persist err:%v", event, err)
|
||||
e.dispatch.Astats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", event.DatasourceId), "persist_event").Inc()
|
||||
e.dispatch.Astats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", event.DatasourceId), "persist_event", event.GroupName, fmt.Sprintf("%v", event.RuleId)).Inc()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -112,6 +136,71 @@ func (e *Consumer) persist(event *models.AlertCurEvent) {
|
||||
err := models.EventPersist(e.ctx, event)
|
||||
if err != nil {
|
||||
logger.Errorf("event%+v persist err:%v", event, err)
|
||||
e.dispatch.Astats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", event.DatasourceId), "persist_event").Inc()
|
||||
e.dispatch.Astats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", event.DatasourceId), "persist_event", event.GroupName, fmt.Sprintf("%v", event.RuleId)).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Consumer) queryRecoveryVal(event *models.AlertCurEvent) {
|
||||
if !event.IsRecovered {
|
||||
return
|
||||
}
|
||||
|
||||
// If the event is a recovery event, execute the recovery_promql query
|
||||
promql, ok := event.AnnotationsJSON["recovery_promql"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
promql = strings.TrimSpace(promql)
|
||||
if promql == "" {
|
||||
logger.Warningf("rule_eval:%s promql is blank", getKey(event))
|
||||
return
|
||||
}
|
||||
|
||||
if e.promClients.IsNil(event.DatasourceId) {
|
||||
logger.Warningf("rule_eval:%s error reader client is nil", getKey(event))
|
||||
return
|
||||
}
|
||||
|
||||
readerClient := e.promClients.GetCli(event.DatasourceId)
|
||||
|
||||
var warnings promsdk.Warnings
|
||||
value, warnings, err := readerClient.Query(e.ctx.Ctx, promql, time.Now())
|
||||
if err != nil {
|
||||
logger.Errorf("rule_eval:%s promql:%s, error:%v", getKey(event), promql, err)
|
||||
event.AnnotationsJSON["recovery_promql_error"] = fmt.Sprintf("promql:%s error:%v", promql, err)
|
||||
|
||||
b, err := json.Marshal(event.AnnotationsJSON)
|
||||
if err != nil {
|
||||
event.AnnotationsJSON = make(map[string]string)
|
||||
event.AnnotationsJSON["error"] = fmt.Sprintf("failed to parse annotations: %v", err)
|
||||
} else {
|
||||
event.Annotations = string(b)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
logger.Errorf("rule_eval:%s promql:%s, warnings:%v", getKey(event), promql, warnings)
|
||||
}
|
||||
|
||||
anomalyPoints := models.ConvertAnomalyPoints(value)
|
||||
if len(anomalyPoints) == 0 {
|
||||
logger.Warningf("rule_eval:%s promql:%s, result is empty", getKey(event), promql)
|
||||
event.AnnotationsJSON["recovery_promql_error"] = fmt.Sprintf("promql:%s error:%s", promql, "result is empty")
|
||||
} else {
|
||||
event.AnnotationsJSON["recovery_value"] = fmt.Sprintf("%v", anomalyPoints[0].Value)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(event.AnnotationsJSON)
|
||||
if err != nil {
|
||||
event.AnnotationsJSON = make(map[string]string)
|
||||
event.AnnotationsJSON["error"] = fmt.Sprintf("failed to parse annotations: %v", err)
|
||||
} else {
|
||||
event.Annotations = string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(event *models.AlertCurEvent) string {
|
||||
return common.RuleKey(event.DatasourceId, event.RuleId)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -26,10 +28,12 @@ type Dispatch struct {
|
||||
alertSubscribeCache *memsto.AlertSubscribeCacheType
|
||||
targetCache *memsto.TargetCacheType
|
||||
notifyConfigCache *memsto.NotifyConfigCacheType
|
||||
taskTplsCache *memsto.TaskTplCache
|
||||
|
||||
alerting aconf.Alerting
|
||||
|
||||
Senders map[string]sender.Sender
|
||||
CallBacks map[string]sender.CallBacker
|
||||
tpls map[string]*template.Template
|
||||
ExtraSenders map[string]sender.Sender
|
||||
BeforeSenderHook func(*models.AlertCurEvent) bool
|
||||
@@ -43,7 +47,7 @@ type Dispatch struct {
|
||||
// 创建一个 Notify 实例
|
||||
func NewDispatch(alertRuleCache *memsto.AlertRuleCacheType, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType,
|
||||
alertSubscribeCache *memsto.AlertSubscribeCacheType, targetCache *memsto.TargetCacheType, notifyConfigCache *memsto.NotifyConfigCacheType,
|
||||
alerting aconf.Alerting, ctx *ctx.Context, astats *astats.Stats) *Dispatch {
|
||||
taskTplsCache *memsto.TaskTplCache, alerting aconf.Alerting, ctx *ctx.Context, astats *astats.Stats) *Dispatch {
|
||||
notify := &Dispatch{
|
||||
alertRuleCache: alertRuleCache,
|
||||
userCache: userCache,
|
||||
@@ -51,6 +55,7 @@ func NewDispatch(alertRuleCache *memsto.AlertRuleCacheType, userCache *memsto.Us
|
||||
alertSubscribeCache: alertSubscribeCache,
|
||||
targetCache: targetCache,
|
||||
notifyConfigCache: notifyConfigCache,
|
||||
taskTplsCache: taskTplsCache,
|
||||
|
||||
alerting: alerting,
|
||||
|
||||
@@ -95,6 +100,21 @@ func (e *Dispatch) relaodTpls() error {
|
||||
models.Mm: sender.NewSender(models.Mm, tmpTpls),
|
||||
models.Telegram: sender.NewSender(models.Telegram, tmpTpls),
|
||||
models.FeishuCard: sender.NewSender(models.FeishuCard, tmpTpls),
|
||||
models.Lark: sender.NewSender(models.Lark, tmpTpls),
|
||||
models.LarkCard: sender.NewSender(models.LarkCard, tmpTpls),
|
||||
}
|
||||
|
||||
// domain -> Callback()
|
||||
callbacks := map[string]sender.CallBacker{
|
||||
models.DingtalkDomain: sender.NewCallBacker(models.DingtalkDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.WecomDomain: sender.NewCallBacker(models.WecomDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.FeishuDomain: sender.NewCallBacker(models.FeishuDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.TelegramDomain: sender.NewCallBacker(models.TelegramDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.FeishuCardDomain: sender.NewCallBacker(models.FeishuCardDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.IbexDomain: sender.NewCallBacker(models.IbexDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.LarkDomain: sender.NewCallBacker(models.LarkDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.DefaultDomain: sender.NewCallBacker(models.DefaultDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.LarkCardDomain: sender.NewCallBacker(models.LarkCardDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
}
|
||||
|
||||
e.RwLock.RLock()
|
||||
@@ -106,6 +126,7 @@ func (e *Dispatch) relaodTpls() error {
|
||||
e.RwLock.Lock()
|
||||
e.tpls = tmpTpls
|
||||
e.Senders = senders
|
||||
e.CallBacks = callbacks
|
||||
e.RwLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -118,6 +139,12 @@ func (e *Dispatch) HandleEventNotify(event *models.AlertCurEvent, isSubscribe bo
|
||||
if rule == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if e.blockEventNotify(rule, event) {
|
||||
logger.Infof("block event notify: rule_id:%d event:%+v", rule.Id, event)
|
||||
return
|
||||
}
|
||||
|
||||
fillUsers(event, e.userCache, e.userGroupCache)
|
||||
|
||||
var (
|
||||
@@ -146,7 +173,7 @@ func (e *Dispatch) HandleEventNotify(event *models.AlertCurEvent, isSubscribe bo
|
||||
}
|
||||
|
||||
// 处理事件发送,这里用一个goroutine处理一个event的所有发送事件
|
||||
go e.Send(rule, event, notifyTarget)
|
||||
go e.Send(rule, event, notifyTarget, isSubscribe)
|
||||
|
||||
// 如果是不是订阅规则出现的event, 则需要处理订阅规则的event
|
||||
if !isSubscribe {
|
||||
@@ -154,6 +181,25 @@ func (e *Dispatch) HandleEventNotify(event *models.AlertCurEvent, isSubscribe bo
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Dispatch) blockEventNotify(rule *models.AlertRule, event *models.AlertCurEvent) bool {
|
||||
ruleType := rule.GetRuleType()
|
||||
|
||||
// 若为机器则先看机器是否删除
|
||||
if ruleType == models.HOST {
|
||||
host, ok := e.targetCache.Get(event.TagsMap["ident"])
|
||||
if !ok || host == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复通知,检测规则配置是否改变
|
||||
// if event.IsRecovered && event.RuleHash != rule.Hash() {
|
||||
// return true
|
||||
// }
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *Dispatch) handleSubs(event *models.AlertCurEvent) {
|
||||
// handle alert subscribes
|
||||
subscribes := make([]*models.AlertSubscribe, 0)
|
||||
@@ -217,11 +263,12 @@ func (e *Dispatch) handleSub(sub *models.AlertSubscribe, event models.AlertCurEv
|
||||
e.HandleEventNotify(&event, true)
|
||||
}
|
||||
|
||||
func (e *Dispatch) Send(rule *models.AlertRule, event *models.AlertCurEvent, notifyTarget *NotifyTarget) {
|
||||
func (e *Dispatch) Send(rule *models.AlertRule, event *models.AlertCurEvent, notifyTarget *NotifyTarget, isSubscribe bool) {
|
||||
needSend := e.BeforeSenderHook(event)
|
||||
if needSend {
|
||||
for channel, uids := range notifyTarget.ToChannelUserMap() {
|
||||
msgCtx := sender.BuildMessageContext(rule, []*models.AlertCurEvent{event}, uids, e.userCache, e.Astats)
|
||||
msgCtx := sender.BuildMessageContext(e.ctx, rule, []*models.AlertCurEvent{event},
|
||||
uids, e.userCache, e.Astats)
|
||||
e.RwLock.RLock()
|
||||
s := e.Senders[channel]
|
||||
e.RwLock.RUnlock()
|
||||
@@ -241,13 +288,102 @@ func (e *Dispatch) Send(rule *models.AlertRule, event *models.AlertCurEvent, not
|
||||
}
|
||||
|
||||
// handle event callbacks
|
||||
sender.SendCallbacks(e.ctx, notifyTarget.ToCallbackList(), event, e.targetCache, e.userCache, e.notifyConfigCache.GetIbex(), e.Astats)
|
||||
e.SendCallbacks(rule, notifyTarget, event)
|
||||
|
||||
// handle global webhooks
|
||||
sender.SendWebhooks(notifyTarget.ToWebhookList(), event, e.Astats)
|
||||
if !event.OverrideGlobalWebhook() {
|
||||
if e.alerting.WebhookBatchSend {
|
||||
sender.BatchSendWebhooks(e.ctx, notifyTarget.ToWebhookMap(), event, e.Astats)
|
||||
} else {
|
||||
sender.SingleSendWebhooks(e.ctx, notifyTarget.ToWebhookMap(), event, e.Astats)
|
||||
}
|
||||
}
|
||||
|
||||
// handle plugin call
|
||||
go sender.MayPluginNotify(e.genNoticeBytes(event), e.notifyConfigCache.GetNotifyScript(), e.Astats)
|
||||
go sender.MayPluginNotify(e.ctx, e.genNoticeBytes(event), e.notifyConfigCache.
|
||||
GetNotifyScript(), e.Astats, event)
|
||||
|
||||
if !isSubscribe {
|
||||
// handle ibex callbacks
|
||||
e.HandleIbex(rule, event)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Dispatch) SendCallbacks(rule *models.AlertRule, notifyTarget *NotifyTarget, event *models.AlertCurEvent) {
|
||||
uids := notifyTarget.ToUidList()
|
||||
urls := notifyTarget.ToCallbackList()
|
||||
whMap := notifyTarget.ToWebhookMap()
|
||||
ogw := event.OverrideGlobalWebhook()
|
||||
for _, urlStr := range urls {
|
||||
if len(urlStr) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cbCtx := sender.BuildCallBackContext(e.ctx, urlStr, rule, []*models.AlertCurEvent{event}, uids, e.userCache, e.alerting.WebhookBatchSend, e.Astats)
|
||||
|
||||
if wh, ok := whMap[cbCtx.CallBackURL]; !ogw && ok && wh.Enable {
|
||||
logger.Debugf("SendCallbacks: webhook[%s] is in global conf.", cbCtx.CallBackURL)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(urlStr, "${ibex}") {
|
||||
e.CallBacks[models.IbexDomain].CallBack(cbCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
if !(strings.HasPrefix(urlStr, "http://") || strings.HasPrefix(urlStr, "https://")) {
|
||||
cbCtx.CallBackURL = "http://" + urlStr
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
logger.Errorf("SendCallbacks: failed to url.Parse(urlStr=%s): %v", urlStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// process feishu card
|
||||
if parsedURL.Host == models.FeishuDomain && parsedURL.Query().Get("card") == "1" {
|
||||
e.CallBacks[models.FeishuCardDomain].CallBack(cbCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
// process lark card
|
||||
if parsedURL.Host == models.LarkDomain && parsedURL.Query().Get("card") == "1" {
|
||||
e.CallBacks[models.LarkCardDomain].CallBack(cbCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
callBacker, ok := e.CallBacks[parsedURL.Host]
|
||||
if ok {
|
||||
callBacker.CallBack(cbCtx)
|
||||
} else {
|
||||
e.CallBacks[models.DefaultDomain].CallBack(cbCtx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Dispatch) HandleIbex(rule *models.AlertRule, event *models.AlertCurEvent) {
|
||||
// 解析 RuleConfig 字段
|
||||
var ruleConfig struct {
|
||||
TaskTpls []*models.Tpl `json:"task_tpls"`
|
||||
}
|
||||
json.Unmarshal([]byte(rule.RuleConfig), &ruleConfig)
|
||||
|
||||
for _, t := range ruleConfig.TaskTpls {
|
||||
if t.TplId == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(t.Host) == 0 {
|
||||
sender.CallIbex(e.ctx, t.TplId, event.TargetIdent,
|
||||
e.taskTplsCache, e.targetCache, e.userCache, event)
|
||||
continue
|
||||
}
|
||||
for _, host := range t.Host {
|
||||
sender.CallIbex(e.ctx, t.TplId, host,
|
||||
e.taskTplsCache, e.targetCache, e.userCache, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Notice struct {
|
||||
|
||||
@@ -76,12 +76,16 @@ func (s *NotifyTarget) ToCallbackList() []string {
|
||||
return callbacks
|
||||
}
|
||||
|
||||
func (s *NotifyTarget) ToWebhookList() []*models.Webhook {
|
||||
webhooks := make([]*models.Webhook, 0, len(s.webhooks))
|
||||
for _, wh := range s.webhooks {
|
||||
webhooks = append(webhooks, wh)
|
||||
func (s *NotifyTarget) ToWebhookMap() map[string]*models.Webhook {
|
||||
return s.webhooks
|
||||
}
|
||||
|
||||
func (s *NotifyTarget) ToUidList() []int64 {
|
||||
uids := make([]int64, 0, len(s.userMap))
|
||||
for uid, _ := range s.userMap {
|
||||
uids = append(uids, uid)
|
||||
}
|
||||
return webhooks
|
||||
return uids
|
||||
}
|
||||
|
||||
// Dispatch 抽象由告警事件到信息接收者的路由策略
|
||||
|
||||
@@ -96,8 +96,7 @@ func (s *Scheduler) syncAlertRules() {
|
||||
|
||||
ruleType := rule.GetRuleType()
|
||||
if rule.IsPrometheusRule() || rule.IsLokiRule() || rule.IsTdengineRule() {
|
||||
datasourceIds := s.promClients.Hit(rule.DatasourceIdsJson)
|
||||
datasourceIds = append(datasourceIds, s.tdengineClients.Hit(rule.DatasourceIdsJson)...)
|
||||
datasourceIds := s.datasourceCache.GetIDsByDsCateAndQueries(rule.Cate, rule.DatasourceQueries)
|
||||
for _, dsId := range datasourceIds {
|
||||
if !naming.DatasourceHashRing.IsHit(strconv.FormatInt(dsId, 10), fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
|
||||
continue
|
||||
@@ -133,7 +132,8 @@ func (s *Scheduler) syncAlertRules() {
|
||||
} else {
|
||||
// 如果 rule 不是通过 prometheus engine 来告警的,则创建为 externalRule
|
||||
// if rule is not processed by prometheus engine, create it as externalRule
|
||||
for _, dsId := range rule.DatasourceIdsJson {
|
||||
dsIds := s.datasourceCache.GetIDsByDsCateAndQueries(rule.Cate, rule.DatasourceQueries)
|
||||
for _, dsId := range dsIds {
|
||||
ds := s.datasourceCache.GetById(dsId)
|
||||
if ds == nil {
|
||||
logger.Debugf("datasource %d not found", dsId)
|
||||
|
||||
1295
alert/eval/eval.go
1295
alert/eval/eval.go
File diff suppressed because it is too large
Load Diff
458
alert/eval/eval_test.go
Normal file
458
alert/eval/eval_test.go
Normal file
@@ -0,0 +1,458 @@
|
||||
package eval
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var (
|
||||
reHashTagIndex1 = map[uint64][][]uint64{
|
||||
1: {
|
||||
{1, 2}, {3, 4},
|
||||
},
|
||||
2: {
|
||||
{5, 6}, {7, 8},
|
||||
},
|
||||
}
|
||||
reHashTagIndex2 = map[uint64][][]uint64{
|
||||
1: {
|
||||
{9, 10}, {11, 12},
|
||||
},
|
||||
3: {
|
||||
{13, 14}, {15, 16},
|
||||
},
|
||||
}
|
||||
seriesTagIndex1 = map[uint64][]uint64{
|
||||
1: {1, 2, 3, 4},
|
||||
2: {5, 6, 7, 8},
|
||||
}
|
||||
seriesTagIndex2 = map[uint64][]uint64{
|
||||
1: {9, 10, 11, 12},
|
||||
3: {13, 14, 15, 16},
|
||||
}
|
||||
)
|
||||
|
||||
func Test_originalJoin(t *testing.T) {
|
||||
type args struct {
|
||||
seriesTagIndex1 map[uint64][]uint64
|
||||
seriesTagIndex2 map[uint64][]uint64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[uint64][]uint64
|
||||
}{
|
||||
{
|
||||
name: "original join",
|
||||
args: args{
|
||||
seriesTagIndex1: map[uint64][]uint64{
|
||||
1: {1, 2, 3, 4},
|
||||
2: {5, 6, 7, 8},
|
||||
},
|
||||
seriesTagIndex2: map[uint64][]uint64{
|
||||
1: {9, 10, 11, 12},
|
||||
3: {13, 14, 15, 16},
|
||||
},
|
||||
},
|
||||
want: map[uint64][]uint64{
|
||||
1: {1, 2, 3, 4, 9, 10, 11, 12},
|
||||
2: {5, 6, 7, 8},
|
||||
3: {13, 14, 15, 16},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := originalJoin(tt.args.seriesTagIndex1, tt.args.seriesTagIndex2); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("originalJoin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_exclude(t *testing.T) {
|
||||
type args struct {
|
||||
reHashTagIndex1 map[uint64][][]uint64
|
||||
reHashTagIndex2 map[uint64][][]uint64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[uint64][]uint64
|
||||
}{
|
||||
{
|
||||
name: "left exclude",
|
||||
args: args{
|
||||
reHashTagIndex1: reHashTagIndex1,
|
||||
reHashTagIndex2: reHashTagIndex2,
|
||||
},
|
||||
want: map[uint64][]uint64{
|
||||
0: {5, 6},
|
||||
1: {7, 8},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "right exclude",
|
||||
args: args{
|
||||
reHashTagIndex1: reHashTagIndex2,
|
||||
reHashTagIndex2: reHashTagIndex1,
|
||||
},
|
||||
want: map[uint64][]uint64{
|
||||
3: {13, 14},
|
||||
4: {15, 16},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := exclude(tt.args.reHashTagIndex1, tt.args.reHashTagIndex2); !allValueDeepEqual(flatten(got), tt.want) {
|
||||
t.Errorf("exclude() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_noneJoin(t *testing.T) {
|
||||
type args struct {
|
||||
seriesTagIndex1 map[uint64][]uint64
|
||||
seriesTagIndex2 map[uint64][]uint64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[uint64][]uint64
|
||||
}{
|
||||
{
|
||||
name: "none join, direct splicing",
|
||||
args: args{
|
||||
seriesTagIndex1: seriesTagIndex1,
|
||||
seriesTagIndex2: seriesTagIndex2,
|
||||
},
|
||||
want: map[uint64][]uint64{
|
||||
0: {1, 2, 3, 4},
|
||||
1: {5, 6, 7, 8},
|
||||
2: {9, 10, 11, 12},
|
||||
3: {13, 14, 15, 16},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := noneJoin(tt.args.seriesTagIndex1, tt.args.seriesTagIndex2); !allValueDeepEqual(got, tt.want) {
|
||||
t.Errorf("noneJoin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cartesianJoin(t *testing.T) {
|
||||
type args struct {
|
||||
seriesTagIndex1 map[uint64][]uint64
|
||||
seriesTagIndex2 map[uint64][]uint64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[uint64][]uint64
|
||||
}{
|
||||
{
|
||||
name: "cartesian join",
|
||||
args: args{
|
||||
seriesTagIndex1: seriesTagIndex1,
|
||||
seriesTagIndex2: seriesTagIndex2,
|
||||
},
|
||||
want: map[uint64][]uint64{
|
||||
0: {1, 2, 3, 4, 9, 10, 11, 12},
|
||||
1: {5, 6, 7, 8, 9, 10, 11, 12},
|
||||
2: {5, 6, 7, 8, 13, 14, 15, 16},
|
||||
3: {1, 2, 3, 4, 13, 14, 15, 16},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := cartesianJoin(tt.args.seriesTagIndex1, tt.args.seriesTagIndex2); !allValueDeepEqual(got, tt.want) {
|
||||
t.Errorf("cartesianJoin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_onJoin(t *testing.T) {
|
||||
type args struct {
|
||||
reHashTagIndex1 map[uint64][][]uint64
|
||||
reHashTagIndex2 map[uint64][][]uint64
|
||||
joinType JoinType
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[uint64][]uint64
|
||||
}{
|
||||
{
|
||||
name: "left join",
|
||||
args: args{
|
||||
reHashTagIndex1: reHashTagIndex1,
|
||||
reHashTagIndex2: reHashTagIndex2,
|
||||
joinType: Left,
|
||||
},
|
||||
want: map[uint64][]uint64{
|
||||
1: {1, 2, 9, 10},
|
||||
2: {3, 4, 9, 10},
|
||||
3: {1, 2, 11, 12},
|
||||
4: {3, 4, 11, 12},
|
||||
5: {5, 6},
|
||||
6: {7, 8},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "right join",
|
||||
args: args{
|
||||
reHashTagIndex1: reHashTagIndex2,
|
||||
reHashTagIndex2: reHashTagIndex1,
|
||||
joinType: Right,
|
||||
},
|
||||
want: map[uint64][]uint64{
|
||||
1: {1, 2, 9, 10},
|
||||
2: {3, 4, 9, 10},
|
||||
3: {1, 2, 11, 12},
|
||||
4: {3, 4, 11, 12},
|
||||
5: {13, 14},
|
||||
6: {15, 16},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "inner join",
|
||||
args: args{
|
||||
reHashTagIndex1: reHashTagIndex1,
|
||||
reHashTagIndex2: reHashTagIndex2,
|
||||
joinType: Inner,
|
||||
},
|
||||
want: map[uint64][]uint64{
|
||||
1: {1, 2, 9, 10},
|
||||
2: {3, 4, 9, 10},
|
||||
3: {1, 2, 11, 12},
|
||||
4: {3, 4, 11, 12},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := onJoin(tt.args.reHashTagIndex1, tt.args.reHashTagIndex2, tt.args.joinType); !allValueDeepEqual(flatten(got), tt.want) {
|
||||
t.Errorf("onJoin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// allValueDeepEqual 判断 map 的 value 是否相同,不考虑 key
|
||||
func allValueDeepEqual(got, want map[uint64][]uint64) bool {
|
||||
if len(got) != len(want) {
|
||||
return false
|
||||
}
|
||||
for _, v1 := range got {
|
||||
curEqual := false
|
||||
slices.Sort(v1)
|
||||
for _, v2 := range want {
|
||||
slices.Sort(v2)
|
||||
if reflect.DeepEqual(v1, v2) {
|
||||
curEqual = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !curEqual {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// allValueDeepEqualOmitOrder 判断两个字符串切片是否相等,不考虑顺序
|
||||
func allValueDeepEqualOmitOrder(got, want []string) bool {
|
||||
if len(got) != len(want) {
|
||||
return false
|
||||
}
|
||||
slices.Sort(got)
|
||||
slices.Sort(want)
|
||||
for i := range got {
|
||||
if got[i] != want[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func Test_removeVal(t *testing.T) {
|
||||
type args struct {
|
||||
promql string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{
|
||||
name: "removeVal1",
|
||||
args: args{
|
||||
promql: "mem{test1=\"$test1\",test2=\"$test2\",test3=\"$test3\"} > $val",
|
||||
},
|
||||
want: "mem{} > $val",
|
||||
},
|
||||
{
|
||||
name: "removeVal2",
|
||||
args: args{
|
||||
promql: "mem{test1=\"test1\",test2=\"$test2\",test3=\"$test3\"} > $val",
|
||||
},
|
||||
want: "mem{test1=\"test1\"} > $val",
|
||||
},
|
||||
{
|
||||
name: "removeVal3",
|
||||
args: args{
|
||||
promql: "mem{test1=\"$test1\",test2=\"test2\",test3=\"$test3\"} > $val",
|
||||
},
|
||||
want: "mem{test2=\"test2\"} > $val",
|
||||
},
|
||||
{
|
||||
name: "removeVal4",
|
||||
args: args{
|
||||
promql: "mem{test1=\"$test1\",test2=\"$test2\",test3=\"test3\"} > $val",
|
||||
},
|
||||
want: "mem{test3=\"test3\"} > $val",
|
||||
},
|
||||
{
|
||||
name: "removeVal5",
|
||||
args: args{
|
||||
promql: "mem{test1=\"$test1\",test2=\"test2\",test3=\"test3\"} > $val",
|
||||
},
|
||||
want: "mem{test2=\"test2\",test3=\"test3\"} > $val",
|
||||
},
|
||||
{
|
||||
name: "removeVal6",
|
||||
args: args{
|
||||
promql: "mem{test1=\"test1\",test2=\"$test2\",test3=\"test3\"} > $val",
|
||||
},
|
||||
want: "mem{test1=\"test1\",test3=\"test3\"} > $val",
|
||||
},
|
||||
{
|
||||
name: "removeVal7",
|
||||
args: args{
|
||||
promql: "mem{test1=\"test1\",test2=\"test2\",test3='$test3'} > $val",
|
||||
},
|
||||
want: "mem{test1=\"test1\",test2=\"test2\"} > $val",
|
||||
},
|
||||
{
|
||||
name: "removeVal8",
|
||||
args: args{
|
||||
promql: "mem{test1=\"test1\",test2=\"test2\",test3=\"test3\"} > $val",
|
||||
},
|
||||
want: "mem{test1=\"test1\",test2=\"test2\",test3=\"test3\"} > $val",
|
||||
},
|
||||
{
|
||||
name: "removeVal9",
|
||||
args: args{
|
||||
promql: "mem{test1=\"$test1\",test2=\"test2\"} > $val1 and mem{test3=\"test3\",test4=\"test4\"} > $val2",
|
||||
},
|
||||
want: "mem{test2=\"test2\"} > $val1 and mem{test3=\"test3\",test4=\"test4\"} > $val2",
|
||||
},
|
||||
{
|
||||
name: "removeVal10",
|
||||
args: args{
|
||||
promql: "mem{test1=\"test1\",test2='$test2'} > $val1 and mem{test3=\"test3\",test4=\"test4\"} > $val2",
|
||||
},
|
||||
want: "mem{test1=\"test1\"} > $val1 and mem{test3=\"test3\",test4=\"test4\"} > $val2",
|
||||
},
|
||||
{
|
||||
name: "removeVal11",
|
||||
args: args{
|
||||
promql: "mem{test1='test1',test2=\"test2\"} > $val1 and mem{test3=\"$test3\",test4=\"test4\"} > $val2",
|
||||
},
|
||||
want: "mem{test1='test1',test2=\"test2\"} > $val1 and mem{test4=\"test4\"} > $val2",
|
||||
},
|
||||
{
|
||||
name: "removeVal12",
|
||||
args: args{
|
||||
promql: "mem{test1=\"test1\",test2=\"test2\"} > $val1 and mem{test3=\"test3\",test4=\"$test4\"} > $val2",
|
||||
},
|
||||
want: "mem{test1=\"test1\",test2=\"test2\"} > $val1 and mem{test3=\"test3\"} > $val2",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := removeVal(tt.args.promql); got != tt.want {
|
||||
t.Errorf("removeVal() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVarMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
promql string
|
||||
want map[string]string
|
||||
}{
|
||||
{
|
||||
name: "单个花括号单个变量",
|
||||
promql: `mem_used_percent{host="$my_host"} > $val`,
|
||||
want: map[string]string{"my_host": "host"},
|
||||
},
|
||||
{
|
||||
name: "单个花括号多个变量",
|
||||
promql: `mem_used_percent{host="$my_host",region="$region",env="prod"} > $val`,
|
||||
want: map[string]string{"my_host": "host", "region": "region"},
|
||||
},
|
||||
{
|
||||
name: "多个花括号多个变量",
|
||||
promql: `sum(rate(mem_used_percent{host="$my_host"})) by (instance) + avg(node_load1{region="$region"}) > $val`,
|
||||
want: map[string]string{"my_host": "host", "region": "region"},
|
||||
},
|
||||
{
|
||||
name: "相同变量出现多次",
|
||||
promql: `sum(rate(mem_used_percent{host="$my_host"})) + avg(node_load1{host="$my_host"}) > $val`,
|
||||
want: map[string]string{"my_host": "host"},
|
||||
},
|
||||
{
|
||||
name: "没有变量",
|
||||
promql: `mem_used_percent{host="localhost",region="cn"} > 80`,
|
||||
want: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "没有花括号",
|
||||
promql: `80 > $val`,
|
||||
want: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "格式不规范的标签",
|
||||
promql: `mem_used_percent{host=$my_host,region = $region} > $val`,
|
||||
want: map[string]string{"my_host": "host", "region": "region"},
|
||||
},
|
||||
{
|
||||
name: "空花括号",
|
||||
promql: `mem_used_percent{} > $val`,
|
||||
want: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "不完整的花括号",
|
||||
promql: `mem_used_percent{host="$my_host"`,
|
||||
want: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "复杂表达式",
|
||||
promql: `sum(rate(http_requests_total{handler="$handler",code="$code"}[5m])) by (handler) / sum(rate(http_requests_total{handler="$handler"}[5m])) by (handler) * 100 > $threshold`,
|
||||
want: map[string]string{"handler": "handler", "code": "code"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ExtractVarMapping(tt.promql)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ExtractVarMapping() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,28 +12,28 @@ import (
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func IsMuted(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, alertMuteCache *memsto.AlertMuteCacheType) bool {
|
||||
func IsMuted(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, alertMuteCache *memsto.AlertMuteCacheType) (bool, string) {
|
||||
if rule.Disabled == 1 {
|
||||
return true
|
||||
return true, "rule disabled"
|
||||
}
|
||||
|
||||
if TimeSpanMuteStrategy(rule, event) {
|
||||
return true
|
||||
return true, "rule is not effective for period of time"
|
||||
}
|
||||
|
||||
if IdentNotExistsMuteStrategy(rule, event, targetCache) {
|
||||
return true
|
||||
return true, "ident not exists mute"
|
||||
}
|
||||
|
||||
if BgNotMatchMuteStrategy(rule, event, targetCache) {
|
||||
return true
|
||||
return true, "bg not match mute"
|
||||
}
|
||||
|
||||
if EventMuteStrategy(event, alertMuteCache) {
|
||||
return true
|
||||
return true, "match mute rule"
|
||||
}
|
||||
|
||||
return false
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// TimeSpanMuteStrategy 根据规则配置的告警生效时间段过滤,如果产生的告警不在规则配置的告警生效时间段内,则不告警,即被mute
|
||||
@@ -114,7 +114,7 @@ func BgNotMatchMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent,
|
||||
target, exists := targetCache.Get(ident)
|
||||
// 对于包含ident的告警事件,check一下ident所属bg和rule所属bg是否相同
|
||||
// 如果告警规则选择了只在本BG生效,那其他BG的机器就不能因此规则产生告警
|
||||
if exists && target.GroupId != rule.GroupId {
|
||||
if exists && !target.MatchGroupId(rule.GroupId) {
|
||||
logger.Debugf("[%s] mute: rule_eval:%d cluster:%s", "BgNotMatchMuteStrategy", rule.Id, event.Cluster)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -67,6 +67,12 @@ func (chr *DatasourceHashRingType) Set(datasourceId string, r *consistent.Consis
|
||||
chr.Rings[datasourceId] = r
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) Del(datasourceId string) {
|
||||
chr.Lock()
|
||||
defer chr.Unlock()
|
||||
delete(chr.Rings, datasourceId)
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) Clear(engineName string) {
|
||||
chr.Lock()
|
||||
defer chr.Unlock()
|
||||
|
||||
@@ -110,7 +110,9 @@ func (n *Naming) heartbeat() error {
|
||||
}
|
||||
}
|
||||
|
||||
newDatasource := make(map[int64]struct{})
|
||||
for i := 0; i < len(datasourceIds); i++ {
|
||||
newDatasource[datasourceIds[i]] = struct{}{}
|
||||
servers, err := n.ActiveServers(datasourceIds[i])
|
||||
if err != nil {
|
||||
logger.Warningf("hearbeat %d get active server err:%v", datasourceIds[i], err)
|
||||
@@ -130,6 +132,13 @@ func (n *Naming) heartbeat() error {
|
||||
localss[datasourceIds[i]] = newss
|
||||
}
|
||||
|
||||
for dsId := range localss {
|
||||
if _, exists := newDatasource[dsId]; !exists {
|
||||
delete(localss, dsId)
|
||||
DatasourceHashRing.Del(fmt.Sprintf("%d", dsId))
|
||||
}
|
||||
}
|
||||
|
||||
// host 告警使用的是 hash ring
|
||||
err = models.AlertingEngineHeartbeatWithCluster(n.ctx, n.heartbeatConfig.Endpoint, n.heartbeatConfig.EngineName, HostDatasource)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,6 +18,10 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/tplx"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
|
||||
"github.com/prometheus/prometheus/prompb"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
@@ -50,10 +54,11 @@ type Processor struct {
|
||||
datasourceId int64
|
||||
EngineName string
|
||||
|
||||
rule *models.AlertRule
|
||||
fires *AlertCurEventMap
|
||||
pendings *AlertCurEventMap
|
||||
inhibit bool
|
||||
rule *models.AlertRule
|
||||
fires *AlertCurEventMap
|
||||
pendings *AlertCurEventMap
|
||||
pendingsUseByRecover *AlertCurEventMap
|
||||
inhibit bool
|
||||
|
||||
tagsMap map[string]string
|
||||
tagsArr []string
|
||||
@@ -74,6 +79,9 @@ type Processor struct {
|
||||
HandleFireEventHook HandleEventFunc
|
||||
HandleRecoverEventHook HandleEventFunc
|
||||
EventMuteHook EventMuteHookFunc
|
||||
|
||||
ScheduleEntry cron.Entry
|
||||
PromEvalInterval int
|
||||
}
|
||||
|
||||
func (p *Processor) Key() string {
|
||||
@@ -85,9 +93,9 @@ func (p *Processor) DatasourceId() int64 {
|
||||
}
|
||||
|
||||
func (p *Processor) Hash() string {
|
||||
return str.MD5(fmt.Sprintf("%d_%d_%s_%d",
|
||||
return str.MD5(fmt.Sprintf("%d_%s_%s_%d",
|
||||
p.rule.Id,
|
||||
p.rule.PromEvalInterval,
|
||||
p.rule.CronPattern,
|
||||
p.rule.RuleConfig,
|
||||
p.datasourceId,
|
||||
))
|
||||
@@ -122,7 +130,7 @@ func NewProcessor(engineName string, rule *models.AlertRule, datasourceId int64,
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inhibit bool) {
|
||||
func (p *Processor) Handle(anomalyPoints []models.AnomalyPoint, from string, inhibit bool) {
|
||||
// 有可能rule的一些配置已经发生变化,比如告警接收人、callbacks等
|
||||
// 这些信息的修改是不会引起worker restart的,但是确实会影响告警处理逻辑
|
||||
// 所以,这里直接从memsto.AlertRuleCache中获取并覆盖
|
||||
@@ -130,10 +138,13 @@ func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inh
|
||||
cachedRule := p.alertRuleCache.Get(p.rule.Id)
|
||||
if cachedRule == nil {
|
||||
logger.Errorf("rule not found %+v", anomalyPoints)
|
||||
p.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", p.DatasourceId()), "handle_event").Inc()
|
||||
p.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", p.DatasourceId()), "handle_event", p.BusiGroupCache.GetNameByBusiGroupId(p.rule.GroupId), fmt.Sprintf("%v", p.rule.Id)).Inc()
|
||||
return
|
||||
}
|
||||
|
||||
// 在 rule 变化之前取到 ruleHash
|
||||
ruleHash := p.rule.Hash()
|
||||
|
||||
p.rule = cachedRule
|
||||
now := time.Now().Unix()
|
||||
alertingKeys := map[string]struct{}{}
|
||||
@@ -141,13 +152,14 @@ func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inh
|
||||
// 根据 event 的 tag 将 events 分组,处理告警抑制的情况
|
||||
eventsMap := make(map[string][]*models.AlertCurEvent)
|
||||
for _, anomalyPoint := range anomalyPoints {
|
||||
event := p.BuildEvent(anomalyPoint, from, now)
|
||||
event := p.BuildEvent(anomalyPoint, from, now, ruleHash)
|
||||
// 如果 event 被 mute 了,本质也是 fire 的状态,这里无论如何都添加到 alertingKeys 中,防止 fire 的事件自动恢复了
|
||||
hash := event.Hash
|
||||
alertingKeys[hash] = struct{}{}
|
||||
if mute.IsMuted(cachedRule, event, p.TargetCache, p.alertMuteCache) {
|
||||
isMuted, detail := mute.IsMuted(cachedRule, event, p.TargetCache, p.alertMuteCache)
|
||||
if isMuted {
|
||||
p.Stats.CounterMuteTotal.WithLabelValues(event.GroupName).Inc()
|
||||
logger.Debugf("rule_eval:%s event:%v is muted", p.Key(), event)
|
||||
logger.Debugf("rule_eval:%s event:%v is muted, detail:%s", p.Key(), event, detail)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -165,10 +177,12 @@ func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inh
|
||||
p.handleEvent(events)
|
||||
}
|
||||
|
||||
p.HandleRecover(alertingKeys, now)
|
||||
if from == "inner" {
|
||||
p.HandleRecover(alertingKeys, now, inhibit)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, now int64) *models.AlertCurEvent {
|
||||
func (p *Processor) BuildEvent(anomalyPoint models.AnomalyPoint, from string, now int64, ruleHash string) *models.AlertCurEvent {
|
||||
p.fillTags(anomalyPoint)
|
||||
p.mayHandleIdent()
|
||||
hash := Hash(p.rule.Id, p.datasourceId, anomalyPoint)
|
||||
@@ -194,6 +208,7 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
|
||||
event.TargetNote = p.targetNote
|
||||
event.TriggerValue = anomalyPoint.ReadableValue()
|
||||
event.TriggerValues = anomalyPoint.Values
|
||||
event.TriggerValuesJson = models.EventTriggerValues{ValuesWithUnit: anomalyPoint.ValuesUnit}
|
||||
event.TagsJSON = p.tagsArr
|
||||
event.Tags = strings.Join(p.tagsArr, ",,")
|
||||
event.IsRecovered = false
|
||||
@@ -206,16 +221,80 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
|
||||
event.Severity = anomalyPoint.Severity
|
||||
event.ExtraConfig = p.rule.ExtraConfigJSON
|
||||
event.PromQl = anomalyPoint.Query
|
||||
event.RecoverConfig = anomalyPoint.RecoverConfig
|
||||
event.RuleHash = ruleHash
|
||||
|
||||
if p.target != "" {
|
||||
if pt, exist := p.TargetCache.Get(p.target); exist {
|
||||
pt.GroupNames = p.BusiGroupCache.GetNamesByBusiGroupIds(pt.GroupIds)
|
||||
event.Target = pt
|
||||
} else {
|
||||
logger.Infof("Target[ident: %s] doesn't exist in cache.", p.target)
|
||||
}
|
||||
}
|
||||
|
||||
if event.TriggerValues != "" && strings.Count(event.TriggerValues, "$") > 1 {
|
||||
// TriggerValues 有多个变量,将多个变量都放到 TriggerValue 中
|
||||
event.TriggerValue = event.TriggerValues
|
||||
}
|
||||
|
||||
if from == "inner" {
|
||||
event.LastEvalTime = now
|
||||
} else {
|
||||
event.LastEvalTime = event.TriggerTime
|
||||
}
|
||||
|
||||
// 生成事件之后,立马进程 relabel 处理
|
||||
Relabel(p.rule, event)
|
||||
return event
|
||||
}
|
||||
|
||||
func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64) {
|
||||
func Relabel(rule *models.AlertRule, event *models.AlertCurEvent) {
|
||||
if rule == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(rule.EventRelabelConfig) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// need to keep the original label
|
||||
event.OriginalTags = event.Tags
|
||||
event.OriginalTagsJSON = make([]string, len(event.TagsJSON))
|
||||
|
||||
labels := make([]prompb.Label, len(event.TagsJSON))
|
||||
for i, tag := range event.TagsJSON {
|
||||
label := strings.SplitN(tag, "=", 2)
|
||||
event.OriginalTagsJSON[i] = tag
|
||||
labels[i] = prompb.Label{Name: label[0], Value: label[1]}
|
||||
}
|
||||
|
||||
for i := 0; i < len(rule.EventRelabelConfig); i++ {
|
||||
if rule.EventRelabelConfig[i].Replacement == "" {
|
||||
rule.EventRelabelConfig[i].Replacement = "$1"
|
||||
}
|
||||
|
||||
if rule.EventRelabelConfig[i].Separator == "" {
|
||||
rule.EventRelabelConfig[i].Separator = ";"
|
||||
}
|
||||
|
||||
if rule.EventRelabelConfig[i].Regex == "" {
|
||||
rule.EventRelabelConfig[i].Regex = "(.*)"
|
||||
}
|
||||
}
|
||||
|
||||
// relabel process
|
||||
relabels := writer.Process(labels, rule.EventRelabelConfig...)
|
||||
event.TagsJSON = make([]string, len(relabels))
|
||||
event.TagsMap = make(map[string]string, len(relabels))
|
||||
for i, label := range relabels {
|
||||
event.TagsJSON[i] = fmt.Sprintf("%s=%s", label.Name, label.Value)
|
||||
event.TagsMap[label.Name] = label.Value
|
||||
}
|
||||
event.Tags = strings.Join(event.TagsJSON, ",,")
|
||||
}
|
||||
|
||||
func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64, inhibit bool) {
|
||||
for _, hash := range p.pendings.Keys() {
|
||||
if _, has := alertingKeys[hash]; has {
|
||||
continue
|
||||
@@ -223,15 +302,59 @@ func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64) {
|
||||
p.pendings.Delete(hash)
|
||||
}
|
||||
|
||||
for hash := range p.fires.GetAll() {
|
||||
hashArr := make([]string, 0, len(alertingKeys))
|
||||
for hash, _ := range p.fires.GetAll() {
|
||||
if _, has := alertingKeys[hash]; has {
|
||||
continue
|
||||
}
|
||||
p.RecoverSingle(hash, now, nil)
|
||||
|
||||
hashArr = append(hashArr, hash)
|
||||
}
|
||||
p.HandleRecoverEvent(hashArr, now, inhibit)
|
||||
|
||||
}
|
||||
|
||||
func (p *Processor) HandleRecoverEvent(hashArr []string, now int64, inhibit bool) {
|
||||
cachedRule := p.rule
|
||||
if cachedRule == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !inhibit {
|
||||
for _, hash := range hashArr {
|
||||
p.RecoverSingle(false, hash, now, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
eventMap := make(map[string]models.AlertCurEvent)
|
||||
for _, hash := range hashArr {
|
||||
event, has := p.fires.Get(hash)
|
||||
if !has {
|
||||
continue
|
||||
}
|
||||
|
||||
e, exists := eventMap[event.Tags]
|
||||
if !exists {
|
||||
eventMap[event.Tags] = *event
|
||||
continue
|
||||
}
|
||||
|
||||
if e.Severity > event.Severity {
|
||||
// hash 对应的恢复事件的被抑制了,把之前的事件删除
|
||||
p.fires.Delete(e.Hash)
|
||||
p.pendings.Delete(e.Hash)
|
||||
models.AlertCurEventDelByHash(p.ctx, e.Hash)
|
||||
eventMap[event.Tags] = *event
|
||||
}
|
||||
}
|
||||
|
||||
for _, event := range eventMap {
|
||||
p.RecoverSingle(false, event.Hash, now, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) RecoverSingle(hash string, now int64, value *string, values ...string) {
|
||||
func (p *Processor) RecoverSingle(byRecover bool, hash string, now int64, value *string, values ...string) {
|
||||
cachedRule := p.rule
|
||||
if cachedRule == nil {
|
||||
return
|
||||
@@ -241,11 +364,28 @@ func (p *Processor) RecoverSingle(hash string, now int64, value *string, values
|
||||
if !has {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果配置了留观时长,就不能立马恢复了
|
||||
if cachedRule.RecoverDuration > 0 && now-event.LastEvalTime < cachedRule.RecoverDuration {
|
||||
if cachedRule.RecoverDuration > 0 {
|
||||
lastPendingEvent, has := p.pendingsUseByRecover.Get(hash)
|
||||
if !has {
|
||||
// 说明没有产生过异常点,就不需要恢复了
|
||||
logger.Debugf("rule_eval:%s event:%v do not has pending event, not recover", p.Key(), event)
|
||||
return
|
||||
}
|
||||
|
||||
if now-lastPendingEvent.LastEvalTime < cachedRule.RecoverDuration {
|
||||
logger.Debugf("rule_eval:%s event:%v not recover", p.Key(), event)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果设置了恢复条件,则不能在此处恢复,必须依靠 recoverPoint 来恢复
|
||||
if event.RecoverConfig.JudgeType != models.Origin && !byRecover {
|
||||
logger.Debugf("rule_eval:%s event:%v not recover", p.Key(), event)
|
||||
return
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
event.TriggerValue = *value
|
||||
if len(values) > 0 {
|
||||
@@ -257,6 +397,7 @@ func (p *Processor) RecoverSingle(hash string, now int64, value *string, values
|
||||
// 我确实无法分辨,是prom中有值但是未满足阈值所以没返回,还是prom中确实丢了一些点导致没有数据可以返回,尴尬
|
||||
p.fires.Delete(hash)
|
||||
p.pendings.Delete(hash)
|
||||
p.pendingsUseByRecover.Delete(hash)
|
||||
|
||||
// 可能是因为调整了promql才恢复的,所以事件里边要体现最新的promql,否则用户会比较困惑
|
||||
// 当然,其实rule的各个字段都可能发生变化了,都更新一下吧
|
||||
@@ -276,6 +417,14 @@ func (p *Processor) handleEvent(events []*models.AlertCurEvent) {
|
||||
if event == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, has := p.pendingsUseByRecover.Get(event.Hash); has {
|
||||
p.pendingsUseByRecover.UpdateLastEvalTime(event.Hash, event.LastEvalTime)
|
||||
} else {
|
||||
p.pendingsUseByRecover.Set(event.Hash, event)
|
||||
}
|
||||
|
||||
event.PromEvalInterval = p.PromEvalInterval
|
||||
if p.rule.PromForDuration == 0 {
|
||||
fireEvents = append(fireEvents, event)
|
||||
if severity > event.Severity {
|
||||
@@ -284,7 +433,7 @@ func (p *Processor) handleEvent(events []*models.AlertCurEvent) {
|
||||
continue
|
||||
}
|
||||
|
||||
var preTriggerTime int64
|
||||
var preTriggerTime int64 // 第一个 pending event 的触发时间
|
||||
preEvent, has := p.pendings.Get(event.Hash)
|
||||
if has {
|
||||
p.pendings.UpdateLastEvalTime(event.Hash, event.LastEvalTime)
|
||||
@@ -370,22 +519,24 @@ func (p *Processor) pushEventToQueue(e *models.AlertCurEvent) {
|
||||
dispatch.LogEvent(e, "push_queue")
|
||||
if !queue.EventQueue.PushFront(e) {
|
||||
logger.Warningf("event_push_queue: queue is full, event:%+v", e)
|
||||
p.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", p.DatasourceId()), "push_event_queue").Inc()
|
||||
p.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", p.DatasourceId()), "push_event_queue", p.BusiGroupCache.GetNameByBusiGroupId(p.rule.GroupId), fmt.Sprintf("%v", p.rule.Id)).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) RecoverAlertCurEventFromDb() {
|
||||
p.pendings = NewAlertCurEventMap(nil)
|
||||
p.pendingsUseByRecover = NewAlertCurEventMap(nil)
|
||||
|
||||
curEvents, err := models.AlertCurEventGetByRuleIdAndDsId(p.ctx, p.rule.Id, p.datasourceId)
|
||||
if err != nil {
|
||||
logger.Errorf("recover event from db for rule:%s failed, err:%s", p.Key(), err)
|
||||
p.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", p.DatasourceId()), "get_recover_event").Inc()
|
||||
p.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", p.DatasourceId()), "get_recover_event", p.BusiGroupCache.GetNameByBusiGroupId(p.rule.GroupId), fmt.Sprintf("%v", p.rule.Id)).Inc()
|
||||
p.fires = NewAlertCurEventMap(nil)
|
||||
return
|
||||
}
|
||||
|
||||
fireMap := make(map[string]*models.AlertCurEvent)
|
||||
pendingsUseByRecoverMap := make(map[string]*models.AlertCurEvent)
|
||||
for _, event := range curEvents {
|
||||
if event.Cate == models.HOST {
|
||||
target, exists := p.TargetCache.Get(event.TargetIdent)
|
||||
@@ -396,13 +547,24 @@ func (p *Processor) RecoverAlertCurEventFromDb() {
|
||||
}
|
||||
|
||||
event.DB2Mem()
|
||||
target, exists := p.TargetCache.Get(event.TargetIdent)
|
||||
if exists {
|
||||
target.GroupNames = p.BusiGroupCache.GetNamesByBusiGroupIds(target.GroupIds)
|
||||
event.Target = target
|
||||
}
|
||||
|
||||
fireMap[event.Hash] = event
|
||||
e := *event
|
||||
pendingsUseByRecoverMap[event.Hash] = &e
|
||||
}
|
||||
|
||||
p.fires = NewAlertCurEventMap(fireMap)
|
||||
|
||||
// 修改告警规则,或者进程重启之后,需要重新加载 pendingsUseByRecover
|
||||
p.pendingsUseByRecover = NewAlertCurEventMap(pendingsUseByRecoverMap)
|
||||
}
|
||||
|
||||
func (p *Processor) fillTags(anomalyPoint common.AnomalyPoint) {
|
||||
func (p *Processor) fillTags(anomalyPoint models.AnomalyPoint) {
|
||||
// handle series tags
|
||||
tagsMap := make(map[string]string)
|
||||
for label, value := range anomalyPoint.Labels {
|
||||
@@ -472,6 +634,12 @@ func (p *Processor) mayHandleGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) DeleteProcessEvent(hash string) {
|
||||
p.fires.Delete(hash)
|
||||
p.pendings.Delete(hash)
|
||||
p.pendingsUseByRecover.Delete(hash)
|
||||
}
|
||||
|
||||
func labelMapToArr(m map[string]string) []string {
|
||||
numLabels := len(m)
|
||||
|
||||
@@ -486,10 +654,10 @@ func labelMapToArr(m map[string]string) []string {
|
||||
return labelStrings
|
||||
}
|
||||
|
||||
func Hash(ruleId, datasourceId int64, vector common.AnomalyPoint) string {
|
||||
func Hash(ruleId, datasourceId int64, vector models.AnomalyPoint) string {
|
||||
return str.MD5(fmt.Sprintf("%d_%s_%d_%d_%s", ruleId, vector.Labels.String(), datasourceId, vector.Severity, vector.Query))
|
||||
}
|
||||
|
||||
func TagHash(vector common.AnomalyPoint) string {
|
||||
func TagHash(vector models.AnomalyPoint) string {
|
||||
return str.MD5(vector.Labels.String())
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/str"
|
||||
@@ -19,19 +20,35 @@ type RecordRuleContext struct {
|
||||
datasourceId int64
|
||||
quit chan struct{}
|
||||
|
||||
scheduler *cron.Cron
|
||||
rule *models.RecordingRule
|
||||
promClients *prom.PromClientMap
|
||||
stats *astats.Stats
|
||||
}
|
||||
|
||||
func NewRecordRuleContext(rule *models.RecordingRule, datasourceId int64, promClients *prom.PromClientMap, writers *writer.WritersType, stats *astats.Stats) *RecordRuleContext {
|
||||
return &RecordRuleContext{
|
||||
rrc := &RecordRuleContext{
|
||||
datasourceId: datasourceId,
|
||||
quit: make(chan struct{}),
|
||||
rule: rule,
|
||||
promClients: promClients,
|
||||
stats: stats,
|
||||
}
|
||||
|
||||
if rule.CronPattern == "" && rule.PromEvalInterval != 0 {
|
||||
rule.CronPattern = fmt.Sprintf("@every %ds", rule.PromEvalInterval)
|
||||
}
|
||||
|
||||
rrc.scheduler = cron.New(cron.WithSeconds())
|
||||
_, err := rrc.scheduler.AddFunc(rule.CronPattern, func() {
|
||||
rrc.Eval()
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("add cron pattern error: %v", err)
|
||||
}
|
||||
|
||||
return rrc
|
||||
}
|
||||
|
||||
func (rrc *RecordRuleContext) Key() string {
|
||||
@@ -39,11 +56,12 @@ func (rrc *RecordRuleContext) Key() string {
|
||||
}
|
||||
|
||||
func (rrc *RecordRuleContext) Hash() string {
|
||||
return str.MD5(fmt.Sprintf("%d_%d_%s_%d",
|
||||
return str.MD5(fmt.Sprintf("%d_%s_%s_%d_%s",
|
||||
rrc.rule.Id,
|
||||
rrc.rule.PromEvalInterval,
|
||||
rrc.rule.CronPattern,
|
||||
rrc.rule.PromQl,
|
||||
rrc.datasourceId,
|
||||
rrc.rule.AppendTags,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -51,23 +69,7 @@ func (rrc *RecordRuleContext) Prepare() {}
|
||||
|
||||
func (rrc *RecordRuleContext) Start() {
|
||||
logger.Infof("eval:%s started", rrc.Key())
|
||||
interval := rrc.rule.PromEvalInterval
|
||||
if interval <= 0 {
|
||||
interval = 10
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-rrc.quit:
|
||||
return
|
||||
case <-ticker.C:
|
||||
rrc.Eval()
|
||||
}
|
||||
}
|
||||
}()
|
||||
rrc.scheduler.Start()
|
||||
}
|
||||
|
||||
func (rrc *RecordRuleContext) Eval() {
|
||||
@@ -109,5 +111,8 @@ func (rrc *RecordRuleContext) Eval() {
|
||||
|
||||
func (rrc *RecordRuleContext) Stop() {
|
||||
logger.Infof("%s stopped", rrc.Key())
|
||||
|
||||
c := rrc.scheduler.Stop()
|
||||
<-c.Done()
|
||||
close(rrc.quit)
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@ type Scheduler struct {
|
||||
writers *writer.WritersType
|
||||
|
||||
stats *astats.Stats
|
||||
|
||||
datasourceCache *memsto.DatasourceCacheType
|
||||
}
|
||||
|
||||
func NewScheduler(aconf aconf.Alert, rrc *memsto.RecordingRuleCacheType, promClients *prom.PromClientMap, writers *writer.WritersType, stats *astats.Stats) *Scheduler {
|
||||
func NewScheduler(aconf aconf.Alert, rrc *memsto.RecordingRuleCacheType, promClients *prom.PromClientMap, writers *writer.WritersType, stats *astats.Stats, datasourceCache *memsto.DatasourceCacheType) *Scheduler {
|
||||
scheduler := &Scheduler{
|
||||
aconf: aconf,
|
||||
recordRules: make(map[string]*RecordRuleContext),
|
||||
@@ -39,6 +41,8 @@ func NewScheduler(aconf aconf.Alert, rrc *memsto.RecordingRuleCacheType, promCli
|
||||
writers: writers,
|
||||
|
||||
stats: stats,
|
||||
|
||||
datasourceCache: datasourceCache,
|
||||
}
|
||||
|
||||
go scheduler.LoopSyncRules(context.Background())
|
||||
@@ -67,7 +71,7 @@ func (s *Scheduler) syncRecordRules() {
|
||||
continue
|
||||
}
|
||||
|
||||
datasourceIds := s.promClients.Hit(rule.DatasourceIdsJson)
|
||||
datasourceIds := s.datasourceCache.GetIDsByDsCateAndQueries("prometheus", rule.DatasourceQueries)
|
||||
for _, dsId := range datasourceIds {
|
||||
if !naming.DatasourceHashRing.IsHit(strconv.FormatInt(dsId, 10), fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
|
||||
continue
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/common"
|
||||
"github.com/ccfos/nightingale/v6/alert/dispatch"
|
||||
"github.com/ccfos/nightingale/v6/alert/mute"
|
||||
"github.com/ccfos/nightingale/v6/alert/naming"
|
||||
@@ -34,7 +33,7 @@ func (rt *Router) pushEventToQueue(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
arr := strings.Split(pair, "=")
|
||||
arr := strings.SplitN(pair, "=", 2)
|
||||
if len(arr) != 2 {
|
||||
continue
|
||||
}
|
||||
@@ -92,7 +91,7 @@ func (rt *Router) eventPersist(c *gin.Context) {
|
||||
|
||||
type eventForm struct {
|
||||
Alert bool `json:"alert"`
|
||||
AnomalyPoints []common.AnomalyPoint `json:"vectors"`
|
||||
AnomalyPoints []models.AnomalyPoint `json:"vectors"`
|
||||
RuleId int64 `json:"rule_id"`
|
||||
DatasourceId int64 `json:"datasource_id"`
|
||||
Inhibit bool `json:"inhibit"`
|
||||
@@ -129,7 +128,7 @@ func (rt *Router) makeEvent(c *gin.Context) {
|
||||
} else {
|
||||
for _, vector := range events[i].AnomalyPoints {
|
||||
readableString := vector.ReadableValue()
|
||||
go ruleWorker.RecoverSingle(process.Hash(events[i].RuleId, events[i].DatasourceId, vector), vector.Timestamp, &readableString)
|
||||
go ruleWorker.RecoverSingle(false, process.Hash(events[i].RuleId, events[i].DatasourceId, vector), vector.Timestamp, &readableString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,182 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ibex"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func SendCallbacks(ctx *ctx.Context, urls []string, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType,
|
||||
ibexConf aconf.Ibex, stats *astats.Stats) {
|
||||
for _, url := range urls {
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
type (
|
||||
// CallBacker 进行回调的接口
|
||||
CallBacker interface {
|
||||
CallBack(ctx CallBackContext)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(url, "${ibex}") {
|
||||
if !event.IsRecovered {
|
||||
handleIbex(ctx, url, event, targetCache, userCache, ibexConf)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// CallBackContext 回调时所需的上下文
|
||||
CallBackContext struct {
|
||||
Ctx *ctx.Context
|
||||
CallBackURL string
|
||||
Users []*models.User
|
||||
Rule *models.AlertRule
|
||||
Events []*models.AlertCurEvent
|
||||
Stats *astats.Stats
|
||||
BatchSend bool
|
||||
}
|
||||
|
||||
if !(strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")) {
|
||||
url = "http://" + url
|
||||
}
|
||||
DefaultCallBacker struct{}
|
||||
)
|
||||
|
||||
stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
|
||||
resp, code, err := poster.PostJSON(url, 5*time.Second, event, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_fail(rule_id=%d url=%s), resp: %s, err: %v, code: %d", event.RuleId, url, string(resp), err, code)
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues("rule_callback").Inc()
|
||||
} else {
|
||||
logger.Infof("event_callback_succ(rule_id=%d url=%s), resp: %s, code: %d", event.RuleId, url, string(resp), code)
|
||||
}
|
||||
func BuildCallBackContext(ctx *ctx.Context, callBackURL string, rule *models.AlertRule, events []*models.AlertCurEvent,
|
||||
uids []int64, userCache *memsto.UserCacheType, batchSend bool, stats *astats.Stats) CallBackContext {
|
||||
users := userCache.GetByUserIds(uids)
|
||||
|
||||
newCallBackUrl, _ := events[0].ParseURL(callBackURL)
|
||||
return CallBackContext{
|
||||
Ctx: ctx,
|
||||
CallBackURL: newCallBackUrl,
|
||||
Rule: rule,
|
||||
Events: events,
|
||||
Users: users,
|
||||
BatchSend: batchSend,
|
||||
Stats: stats,
|
||||
}
|
||||
}
|
||||
|
||||
type TaskForm struct {
|
||||
Title string `json:"title"`
|
||||
Account string `json:"account"`
|
||||
Batch int `json:"batch"`
|
||||
Tolerance int `json:"tolerance"`
|
||||
Timeout int `json:"timeout"`
|
||||
Pause string `json:"pause"`
|
||||
Script string `json:"script"`
|
||||
Args string `json:"args"`
|
||||
Stdin string `json:"stdin"`
|
||||
Action string `json:"action"`
|
||||
Creator string `json:"creator"`
|
||||
Hosts []string `json:"hosts"`
|
||||
func ExtractAtsParams(rawURL string) []string {
|
||||
ans := make([]string, 0, 1)
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
logger.Errorf("ExtractAtsParams(url=%s), err: %v", rawURL, err)
|
||||
return ans
|
||||
}
|
||||
|
||||
queryParams := parsedURL.Query()
|
||||
atParam := queryParams.Get("ats")
|
||||
if atParam == "" {
|
||||
return ans
|
||||
}
|
||||
|
||||
// Split the atParam by comma and return the result as a slice
|
||||
return strings.Split(atParam, ",")
|
||||
}
|
||||
|
||||
func NewCallBacker(
|
||||
key string,
|
||||
targetCache *memsto.TargetCacheType,
|
||||
userCache *memsto.UserCacheType,
|
||||
taskTplCache *memsto.TaskTplCache,
|
||||
tpls map[string]*template.Template,
|
||||
) CallBacker {
|
||||
|
||||
switch key {
|
||||
case models.IbexDomain: // Distribute to Ibex
|
||||
return &IbexCallBacker{
|
||||
targetCache: targetCache,
|
||||
userCache: userCache,
|
||||
taskTplCache: taskTplCache,
|
||||
}
|
||||
case models.DefaultDomain: // default callback
|
||||
return &DefaultCallBacker{}
|
||||
case models.DingtalkDomain:
|
||||
return &DingtalkSender{tpl: tpls[models.Dingtalk]}
|
||||
case models.WecomDomain:
|
||||
return &WecomSender{tpl: tpls[models.Wecom]}
|
||||
case models.FeishuDomain:
|
||||
return &FeishuSender{tpl: tpls[models.Feishu]}
|
||||
case models.FeishuCardDomain:
|
||||
return &FeishuCardSender{tpl: tpls[models.FeishuCard]}
|
||||
//case models.Mm:
|
||||
// return &MmSender{tpl: tpls[models.Mm]}
|
||||
case models.TelegramDomain:
|
||||
return &TelegramSender{tpl: tpls[models.Telegram]}
|
||||
case models.LarkDomain:
|
||||
return &LarkSender{tpl: tpls[models.Lark]}
|
||||
case models.LarkCardDomain:
|
||||
return &LarkCardSender{tpl: tpls[models.LarkCard]}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DefaultCallBacker) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.CallBackURL) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
event := ctx.Events[0]
|
||||
|
||||
if ctx.BatchSend {
|
||||
webhookConf := &models.Webhook{
|
||||
Type: models.RuleCallback,
|
||||
Enable: true,
|
||||
Url: ctx.CallBackURL,
|
||||
Timeout: 5,
|
||||
RetryCount: 3,
|
||||
RetryInterval: 10,
|
||||
Batch: 1000,
|
||||
}
|
||||
|
||||
PushCallbackEvent(ctx.Ctx, webhookConf, event, ctx.Stats)
|
||||
return
|
||||
}
|
||||
|
||||
doSendAndRecord(ctx.Ctx, ctx.CallBackURL, ctx.CallBackURL, event, "callback", ctx.Stats, ctx.Events)
|
||||
}
|
||||
|
||||
func doSendAndRecord(ctx *ctx.Context, url, token string, body interface{}, channel string,
|
||||
stats *astats.Stats, events []*models.AlertCurEvent) {
|
||||
res, err := doSend(url, body, channel, stats)
|
||||
NotifyRecord(ctx, events, channel, token, res, err)
|
||||
}
|
||||
|
||||
func NotifyRecord(ctx *ctx.Context, evts []*models.AlertCurEvent, channel, target, res string, err error) {
|
||||
// 一个通知可能对应多个 event,都需要记录
|
||||
notis := make([]*models.NotificaitonRecord, 0, len(evts))
|
||||
for _, evt := range evts {
|
||||
noti := models.NewNotificationRecord(evt, channel, target)
|
||||
if err != nil {
|
||||
noti.SetStatus(models.NotiStatusFailure)
|
||||
noti.SetDetails(err.Error())
|
||||
} else if res != "" {
|
||||
noti.SetDetails(string(res))
|
||||
}
|
||||
notis = append(notis, noti)
|
||||
}
|
||||
|
||||
if !ctx.IsCenter {
|
||||
_, err := poster.PostByUrlsWithResp[[]int64](ctx, "/v1/n9e/notify-record", notis)
|
||||
if err != nil {
|
||||
logger.Errorf("add notis:%v failed, err: %v", notis, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.DB(ctx).CreateInBatches(notis, 100).Error; err != nil {
|
||||
logger.Errorf("add notis:%v failed, err: %v", notis, err)
|
||||
}
|
||||
}
|
||||
|
||||
func doSend(url string, body interface{}, channel string, stats *astats.Stats) (string, error) {
|
||||
stats.AlertNotifyTotal.WithLabelValues(channel).Inc()
|
||||
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("%s_sender: result=fail url=%s code=%d error=%v req:%v response=%s", channel, url, code, err, body, string(res))
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
return "", err
|
||||
}
|
||||
|
||||
logger.Infof("%s_sender: result=succ url=%s code=%d req:%v response=%s", channel, url, code, body, string(res))
|
||||
return string(res), nil
|
||||
}
|
||||
|
||||
type TaskCreateReply struct {
|
||||
@@ -66,157 +184,26 @@ type TaskCreateReply struct {
|
||||
Dat int64 `json:"dat"` // task.id
|
||||
}
|
||||
|
||||
func handleIbex(ctx *ctx.Context, url string, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType, ibexConf aconf.Ibex) {
|
||||
arr := strings.Split(url, "/")
|
||||
func PushCallbackEvent(ctx *ctx.Context, webhook *models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
|
||||
CallbackEventQueueLock.RLock()
|
||||
queue := CallbackEventQueue[webhook.Url]
|
||||
CallbackEventQueueLock.RUnlock()
|
||||
|
||||
var idstr string
|
||||
var host string
|
||||
|
||||
if len(arr) > 1 {
|
||||
idstr = arr[1]
|
||||
}
|
||||
|
||||
if len(arr) > 2 {
|
||||
host = arr[2]
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(idstr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: failed to parse url: %s", url)
|
||||
return
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
// 用户在callback url中没有传入host,就从event中解析
|
||||
host = event.TargetIdent
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
logger.Error("event_callback_ibex: failed to get host")
|
||||
return
|
||||
}
|
||||
|
||||
tpl, err := models.TaskTplGetById(ctx, id)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: failed to get tpl: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if tpl == nil {
|
||||
logger.Errorf("event_callback_ibex: no such tpl(%d)", id)
|
||||
return
|
||||
}
|
||||
|
||||
// check perm
|
||||
// tpl.GroupId - host - account 三元组校验权限
|
||||
can, err := canDoIbex(ctx, tpl.UpdateBy, tpl, host, targetCache, userCache)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: check perm fail: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !can {
|
||||
logger.Errorf("event_callback_ibex: user(%s) no permission", tpl.UpdateBy)
|
||||
return
|
||||
}
|
||||
|
||||
tagsMap := make(map[string]string)
|
||||
for i := 0; i < len(event.TagsJSON); i++ {
|
||||
pair := strings.TrimSpace(event.TagsJSON[i])
|
||||
if pair == "" {
|
||||
continue
|
||||
if queue == nil {
|
||||
queue = &WebhookQueue{
|
||||
eventQueue: NewSafeEventQueue(QueueMaxSize),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
arr := strings.Split(pair, "=")
|
||||
if len(arr) != 2 {
|
||||
continue
|
||||
}
|
||||
CallbackEventQueueLock.Lock()
|
||||
CallbackEventQueue[webhook.Url] = queue
|
||||
CallbackEventQueueLock.Unlock()
|
||||
|
||||
tagsMap[arr[0]] = arr[1]
|
||||
}
|
||||
// 附加告警级别 告警触发值标签
|
||||
tagsMap["alert_severity"] = strconv.Itoa(event.Severity)
|
||||
tagsMap["alert_trigger_value"] = event.TriggerValue
|
||||
|
||||
tags, err := json.Marshal(tagsMap)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: failed to marshal tags to json: %v", tagsMap)
|
||||
return
|
||||
StartConsumer(ctx, queue, webhook.Batch, webhook, stats)
|
||||
}
|
||||
|
||||
// call ibex
|
||||
in := TaskForm{
|
||||
Title: tpl.Title + " FH: " + host,
|
||||
Account: tpl.Account,
|
||||
Batch: tpl.Batch,
|
||||
Tolerance: tpl.Tolerance,
|
||||
Timeout: tpl.Timeout,
|
||||
Pause: tpl.Pause,
|
||||
Script: tpl.Script,
|
||||
Args: tpl.Args,
|
||||
Stdin: string(tags),
|
||||
Action: "start",
|
||||
Creator: tpl.UpdateBy,
|
||||
Hosts: []string{host},
|
||||
}
|
||||
|
||||
var res TaskCreateReply
|
||||
err = ibex.New(
|
||||
ibexConf.Address,
|
||||
ibexConf.BasicAuthUser,
|
||||
ibexConf.BasicAuthPass,
|
||||
ibexConf.Timeout,
|
||||
).
|
||||
Path("/ibex/v1/tasks").
|
||||
In(in).
|
||||
Out(&res).
|
||||
POST()
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: call ibex fail: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Err != "" {
|
||||
logger.Errorf("event_callback_ibex: call ibex response error: %v", res.Err)
|
||||
return
|
||||
}
|
||||
|
||||
// write db
|
||||
record := models.TaskRecord{
|
||||
Id: res.Dat,
|
||||
EventId: event.Id,
|
||||
GroupId: tpl.GroupId,
|
||||
IbexAddress: ibexConf.Address,
|
||||
IbexAuthUser: ibexConf.BasicAuthUser,
|
||||
IbexAuthPass: ibexConf.BasicAuthPass,
|
||||
Title: in.Title,
|
||||
Account: in.Account,
|
||||
Batch: in.Batch,
|
||||
Tolerance: in.Tolerance,
|
||||
Timeout: in.Timeout,
|
||||
Pause: in.Pause,
|
||||
Script: in.Script,
|
||||
Args: in.Args,
|
||||
CreateAt: time.Now().Unix(),
|
||||
CreateBy: in.Creator,
|
||||
}
|
||||
|
||||
if err = record.Add(ctx); err != nil {
|
||||
logger.Errorf("event_callback_ibex: persist task_record fail: %v", err)
|
||||
succ := queue.eventQueue.Push(event)
|
||||
if !succ {
|
||||
logger.Warningf("Write channel(%s) full, current channel size: %d event:%v", webhook.Url, queue.eventQueue.Len(), event)
|
||||
}
|
||||
}
|
||||
|
||||
func canDoIbex(ctx *ctx.Context, username string, tpl *models.TaskTpl, host string, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType) (bool, error) {
|
||||
user := userCache.GetByUsername(username)
|
||||
if user != nil && user.IsAdmin() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
target, has := targetCache.Get(host)
|
||||
if !has {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return target.GroupId == tpl.GroupId, nil
|
||||
}
|
||||
|
||||
@@ -3,13 +3,8 @@ package sender
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type dingtalkMarkdown struct {
|
||||
@@ -28,6 +23,10 @@ type dingtalk struct {
|
||||
At dingtalkAt `json:"at"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ CallBacker = (*DingtalkSender)(nil)
|
||||
)
|
||||
|
||||
type DingtalkSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
@@ -37,13 +36,13 @@ func (ds *DingtalkSender) Send(ctx MessageContext) {
|
||||
return
|
||||
}
|
||||
|
||||
urls, ats := ds.extract(ctx.Users)
|
||||
urls, ats, tokens := ds.extract(ctx.Users)
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
message := BuildTplMessage(models.Dingtalk, ds.tpl, ctx.Events)
|
||||
|
||||
for _, url := range urls {
|
||||
for i, url := range urls {
|
||||
var body dingtalk
|
||||
// NoAt in url
|
||||
if strings.Contains(url, "noat=1") {
|
||||
@@ -68,14 +67,44 @@ func (ds *DingtalkSender) Send(ctx MessageContext) {
|
||||
}
|
||||
}
|
||||
|
||||
doSend(url, body, models.Dingtalk, ctx.Stats)
|
||||
doSendAndRecord(ctx.Ctx, url, tokens[i], body, models.Dingtalk, ctx.Stats, ctx.Events)
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DingtalkSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
body := dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: ctx.Events[0].RuleName,
|
||||
},
|
||||
}
|
||||
|
||||
ats := ExtractAtsParams(ctx.CallBackURL)
|
||||
message := BuildTplMessage(models.Dingtalk, ds.tpl, ctx.Events)
|
||||
|
||||
if len(ats) > 0 {
|
||||
body.Markdown.Text = message + "\n@" + strings.Join(ats, "@")
|
||||
body.At = dingtalkAt{
|
||||
AtMobiles: ats,
|
||||
IsAtAll: false,
|
||||
}
|
||||
} else {
|
||||
// NoAt in url
|
||||
body.Markdown.Text = message
|
||||
}
|
||||
|
||||
doSendAndRecord(ctx.Ctx, ctx.CallBackURL, ctx.CallBackURL, body, "callback", ctx.Stats, ctx.Events)
|
||||
}
|
||||
|
||||
// extract urls and ats from Users
|
||||
func (ds *DingtalkSender) extract(users []*models.User) ([]string, []string) {
|
||||
func (ds *DingtalkSender) extract(users []*models.User) ([]string, []string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0, len(users))
|
||||
tokens := make([]string, 0, len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if user.Phone != "" {
|
||||
@@ -87,19 +116,8 @@ func (ds *DingtalkSender) extract(users []*models.User) ([]string, []string) {
|
||||
url = "https://oapi.dingtalk.com/robot/send?access_token=" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
|
||||
func doSend(url string, body interface{}, channel string, stats *astats.Stats) {
|
||||
stats.AlertNotifyTotal.WithLabelValues(channel).Inc()
|
||||
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("%s_sender: result=fail url=%s code=%d error=%v req:%v response=%s", channel, url, code, err, body, string(res))
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
} else {
|
||||
logger.Infof("%s_sender: result=succ url=%s code=%d req:%v response=%s", channel, url, code, body, string(res))
|
||||
}
|
||||
return urls, ats, tokens
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
var mailch chan *gomail.Message
|
||||
var mailch chan *EmailContext
|
||||
|
||||
type EmailSender struct {
|
||||
subjectTpl *template.Template
|
||||
@@ -23,6 +24,11 @@ type EmailSender struct {
|
||||
smtp aconf.SMTPConfig
|
||||
}
|
||||
|
||||
type EmailContext struct {
|
||||
events []*models.AlertCurEvent
|
||||
mail *gomail.Message
|
||||
}
|
||||
|
||||
func (es *EmailSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
@@ -36,7 +42,7 @@ func (es *EmailSender) Send(ctx MessageContext) {
|
||||
subject = ctx.Events[0].RuleName
|
||||
}
|
||||
content := BuildTplMessage(models.Email, es.contentTpl, ctx.Events)
|
||||
es.WriteEmail(subject, content, tos)
|
||||
es.WriteEmail(subject, content, tos, ctx.Events)
|
||||
|
||||
ctx.Stats.AlertNotifyTotal.WithLabelValues(models.Email).Add(float64(len(tos)))
|
||||
}
|
||||
@@ -73,7 +79,7 @@ func SendEmail(subject, content string, tos []string, stmp aconf.SMTPConfig) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *EmailSender) WriteEmail(subject, content string, tos []string) {
|
||||
func (es *EmailSender) WriteEmail(subject, content string, tos []string, events []*models.AlertCurEvent) {
|
||||
m := gomail.NewMessage()
|
||||
|
||||
m.SetHeader("From", es.smtp.From)
|
||||
@@ -81,40 +87,45 @@ func (es *EmailSender) WriteEmail(subject, content string, tos []string) {
|
||||
m.SetHeader("Subject", subject)
|
||||
m.SetBody("text/html", content)
|
||||
|
||||
mailch <- m
|
||||
mailch <- &EmailContext{events, m}
|
||||
}
|
||||
|
||||
func dialSmtp(d *gomail.Dialer) gomail.SendCloser {
|
||||
for {
|
||||
if s, err := d.Dial(); err != nil {
|
||||
logger.Errorf("email_sender: failed to dial smtp: %s", err)
|
||||
select {
|
||||
case <-mailQuit:
|
||||
// Note that Sendcloser is not obtained below,
|
||||
// and the outgoing signal (with configuration changes) exits the current dial
|
||||
return nil
|
||||
default:
|
||||
if s, err := d.Dial(); err != nil {
|
||||
logger.Errorf("email_sender: failed to dial smtp: %s", err)
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mailQuit = make(chan struct{})
|
||||
|
||||
func RestartEmailSender(smtp aconf.SMTPConfig) {
|
||||
close(mailQuit)
|
||||
mailQuit = make(chan struct{})
|
||||
startEmailSender(smtp)
|
||||
func RestartEmailSender(ctx *ctx.Context, smtp aconf.SMTPConfig) {
|
||||
// Notify internal start exit
|
||||
mailQuit <- struct{}{}
|
||||
startEmailSender(ctx, smtp)
|
||||
}
|
||||
|
||||
var smtpConfig aconf.SMTPConfig
|
||||
|
||||
func InitEmailSender(ncc *memsto.NotifyConfigCacheType) {
|
||||
mailch = make(chan *gomail.Message, 100000)
|
||||
go updateSmtp(ncc)
|
||||
func InitEmailSender(ctx *ctx.Context, ncc *memsto.NotifyConfigCacheType) {
|
||||
mailch = make(chan *EmailContext, 100000)
|
||||
go updateSmtp(ctx, ncc)
|
||||
smtpConfig = ncc.GetSMTP()
|
||||
startEmailSender(smtpConfig)
|
||||
|
||||
go startEmailSender(ctx, smtpConfig)
|
||||
}
|
||||
|
||||
func updateSmtp(ncc *memsto.NotifyConfigCacheType) {
|
||||
func updateSmtp(ctx *ctx.Context, ncc *memsto.NotifyConfigCacheType) {
|
||||
for {
|
||||
time.Sleep(1 * time.Minute)
|
||||
smtp := ncc.GetSMTP()
|
||||
@@ -122,15 +133,16 @@ func updateSmtp(ncc *memsto.NotifyConfigCacheType) {
|
||||
smtpConfig.Pass != smtp.Pass || smtpConfig.User != smtp.User || smtpConfig.Port != smtp.Port ||
|
||||
smtpConfig.InsecureSkipVerify != smtp.InsecureSkipVerify { //diff
|
||||
smtpConfig = smtp
|
||||
RestartEmailSender(smtp)
|
||||
RestartEmailSender(ctx, smtp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startEmailSender(smtp aconf.SMTPConfig) {
|
||||
func startEmailSender(ctx *ctx.Context, smtp aconf.SMTPConfig) {
|
||||
conf := smtp
|
||||
if conf.Host == "" || conf.Port == 0 {
|
||||
logger.Warning("SMTP configurations invalid")
|
||||
<-mailQuit
|
||||
return
|
||||
}
|
||||
logger.Infof("start email sender... conf.Host:%+v,conf.Port:%+v", conf.Host, conf.Port)
|
||||
@@ -154,9 +166,16 @@ func startEmailSender(smtp aconf.SMTPConfig) {
|
||||
|
||||
if !open {
|
||||
s = dialSmtp(d)
|
||||
if s == nil {
|
||||
// Indicates that the dialing failed and exited the current goroutine directly,
|
||||
// but put the Message back in the mailch
|
||||
mailch <- m
|
||||
return
|
||||
}
|
||||
open = true
|
||||
}
|
||||
if err := gomail.Send(s, m); err != nil {
|
||||
var err error
|
||||
if err = gomail.Send(s, m.mail); err != nil {
|
||||
logger.Errorf("email_sender: failed to send: %s", err)
|
||||
|
||||
// close and retry
|
||||
@@ -165,13 +184,28 @@ func startEmailSender(smtp aconf.SMTPConfig) {
|
||||
}
|
||||
|
||||
s = dialSmtp(d)
|
||||
if s == nil {
|
||||
// Indicates that the dialing failed and exited the current goroutine directly,
|
||||
// but put the Message back in the mailch
|
||||
mailch <- m
|
||||
return
|
||||
}
|
||||
open = true
|
||||
|
||||
if err := gomail.Send(s, m); err != nil {
|
||||
if err = gomail.Send(s, m.mail); err != nil {
|
||||
logger.Errorf("email_sender: failed to retry send: %s", err)
|
||||
}
|
||||
} else {
|
||||
logger.Infof("email_sender: result=succ subject=%v to=%v", m.GetHeader("Subject"), m.GetHeader("To"))
|
||||
logger.Infof("email_sender: result=succ subject=%v to=%v",
|
||||
m.mail.GetHeader("Subject"), m.mail.GetHeader("To"))
|
||||
}
|
||||
|
||||
for _, to := range m.mail.GetHeader("To") {
|
||||
msg := ""
|
||||
if err == nil {
|
||||
msg = "ok"
|
||||
}
|
||||
NotifyRecord(ctx, m.events, models.Email, to, msg, err)
|
||||
}
|
||||
|
||||
size++
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
@@ -22,17 +23,47 @@ type feishu struct {
|
||||
At feishuAt `json:"at"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ CallBacker = (*FeishuSender)(nil)
|
||||
)
|
||||
|
||||
type FeishuSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ats := ExtractAtsParams(ctx.CallBackURL)
|
||||
message := BuildTplMessage(models.Feishu, fs.tpl, ctx.Events)
|
||||
|
||||
if len(ats) > 0 {
|
||||
atTags := ""
|
||||
for _, at := range ats {
|
||||
atTags += fmt.Sprintf("<at user_id=\"%s\"></at> ", at)
|
||||
}
|
||||
message = atTags + message
|
||||
}
|
||||
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
|
||||
doSendAndRecord(ctx.Ctx, ctx.CallBackURL, ctx.CallBackURL, body, "callback", ctx.Stats, ctx.Events)
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls, ats := fs.extract(ctx.Users)
|
||||
urls, ats, tokens := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(models.Feishu, fs.tpl, ctx.Events)
|
||||
for _, url := range urls {
|
||||
for i, url := range urls {
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
@@ -45,13 +76,14 @@ func (fs *FeishuSender) Send(ctx MessageContext) {
|
||||
IsAtAll: false,
|
||||
}
|
||||
}
|
||||
doSend(url, body, models.Feishu, ctx.Stats)
|
||||
doSendAndRecord(ctx.Ctx, url, tokens[i], body, models.Feishu, ctx.Stats, ctx.Events)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) extract(users []*models.User) ([]string, []string) {
|
||||
func (fs *FeishuSender) extract(users []*models.User) ([]string, []string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0, len(users))
|
||||
tokens := make([]string, 0, len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if user.Phone != "" {
|
||||
@@ -63,7 +95,8 @@ func (fs *FeishuSender) extract(users []*models.User) ([]string, []string) {
|
||||
url = "https://open.feishu.cn/open-apis/bot/v2/hook/" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
return urls, ats, tokens
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package sender
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
@@ -55,8 +56,8 @@ const (
|
||||
Triggered = "triggered"
|
||||
)
|
||||
|
||||
var (
|
||||
body = feishuCard{
|
||||
func createFeishuCardBody() feishuCard {
|
||||
return feishuCard{
|
||||
feishu: feishu{Msgtype: "interactive"},
|
||||
Card: Cards{
|
||||
Config: Conf{
|
||||
@@ -89,13 +90,59 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func (fs *FeishuCardSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ats := ExtractAtsParams(ctx.CallBackURL)
|
||||
message := BuildTplMessage(models.FeishuCard, fs.tpl, ctx.Events)
|
||||
|
||||
if len(ats) > 0 {
|
||||
atTags := ""
|
||||
for _, at := range ats {
|
||||
if strings.Contains(at, "@") {
|
||||
atTags += fmt.Sprintf("<at email=\"%s\" ></at>", at)
|
||||
} else {
|
||||
atTags += fmt.Sprintf("<at id=\"%s\" ></at>", at)
|
||||
}
|
||||
}
|
||||
message = atTags + message
|
||||
}
|
||||
|
||||
color := "red"
|
||||
lowerUnicode := strings.ToLower(message)
|
||||
if strings.Count(lowerUnicode, Recovered) > 0 && strings.Count(lowerUnicode, Triggered) > 0 {
|
||||
color = "orange"
|
||||
} else if strings.Count(lowerUnicode, Recovered) > 0 {
|
||||
color = "green"
|
||||
}
|
||||
|
||||
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
|
||||
body := createFeishuCardBody()
|
||||
body.Card.Header.Title.Content = SendTitle
|
||||
body.Card.Header.Template = color
|
||||
body.Card.Elements[0].Text.Content = message
|
||||
body.Card.Elements[2].Elements[0].Content = SendTitle
|
||||
|
||||
// This is to be compatible with the feishucard interface, if with query string parameters, the request will fail
|
||||
// Remove query parameters from the URL,
|
||||
parsedURL, err := url.Parse(ctx.CallBackURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
parsedURL.RawQuery = ""
|
||||
|
||||
doSendAndRecord(ctx.Ctx, parsedURL.String(), parsedURL.String(), body, "callback", ctx.Stats, ctx.Events)
|
||||
}
|
||||
|
||||
func (fs *FeishuCardSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls, _ := fs.extract(ctx.Users)
|
||||
urls, tokens := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(models.FeishuCard, fs.tpl, ctx.Events)
|
||||
color := "red"
|
||||
lowerUnicode := strings.ToLower(message)
|
||||
@@ -106,18 +153,19 @@ func (fs *FeishuCardSender) Send(ctx MessageContext) {
|
||||
}
|
||||
|
||||
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
|
||||
body := createFeishuCardBody()
|
||||
body.Card.Header.Title.Content = SendTitle
|
||||
body.Card.Header.Template = color
|
||||
body.Card.Elements[0].Text.Content = message
|
||||
body.Card.Elements[2].Elements[0].Content = SendTitle
|
||||
for _, url := range urls {
|
||||
doSend(url, body, models.FeishuCard, ctx.Stats)
|
||||
for i, url := range urls {
|
||||
doSendAndRecord(ctx.Ctx, url, tokens[i], body, models.FeishuCard, ctx.Stats, ctx.Events)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FeishuCardSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0)
|
||||
tokens := make([]string, 0, len(users))
|
||||
for i := range users {
|
||||
if token, has := users[i].ExtractToken(models.FeishuCard); has {
|
||||
url := token
|
||||
@@ -125,7 +173,8 @@ func (fs *FeishuCardSender) extract(users []*models.User) ([]string, []string) {
|
||||
url = "https://open.feishu.cn/open-apis/bot/v2/hook/" + strings.TrimSpace(token)
|
||||
}
|
||||
urls = append(urls, url)
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
return urls, tokens
|
||||
}
|
||||
|
||||
276
alert/sender/ibex.go
Normal file
276
alert/sender/ibex.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// @Author: Ciusyan 6/5/24
|
||||
|
||||
package sender
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
imodels "github.com/flashcatcloud/ibex/src/models"
|
||||
"github.com/flashcatcloud/ibex/src/storage"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
_ CallBacker = (*IbexCallBacker)(nil)
|
||||
)
|
||||
|
||||
type IbexCallBacker struct {
|
||||
targetCache *memsto.TargetCacheType
|
||||
userCache *memsto.UserCacheType
|
||||
taskTplCache *memsto.TaskTplCache
|
||||
}
|
||||
|
||||
func (c *IbexCallBacker) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.CallBackURL) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
event := ctx.Events[0]
|
||||
|
||||
if event.IsRecovered {
|
||||
return
|
||||
}
|
||||
|
||||
c.handleIbex(ctx.Ctx, ctx.CallBackURL, event)
|
||||
}
|
||||
|
||||
func (c *IbexCallBacker) handleIbex(ctx *ctx.Context, url string, event *models.AlertCurEvent) {
|
||||
if imodels.DB() == nil && ctx.IsCenter {
|
||||
logger.Warning("event_callback_ibex: db is nil")
|
||||
return
|
||||
}
|
||||
|
||||
arr := strings.Split(url, "/")
|
||||
|
||||
var idstr string
|
||||
var host string
|
||||
|
||||
if len(arr) > 1 {
|
||||
idstr = arr[1]
|
||||
}
|
||||
|
||||
if len(arr) > 2 {
|
||||
host = arr[2]
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(idstr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: failed to parse url: %s", url)
|
||||
return
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
// 用户在callback url中没有传入host,就从event中解析
|
||||
host = event.TargetIdent
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
logger.Error("event_callback_ibex: failed to get host")
|
||||
return
|
||||
}
|
||||
|
||||
CallIbex(ctx, id, host, c.taskTplCache, c.targetCache, c.userCache, event)
|
||||
}
|
||||
|
||||
func CallIbex(ctx *ctx.Context, id int64, host string,
|
||||
taskTplCache *memsto.TaskTplCache, targetCache *memsto.TargetCacheType,
|
||||
userCache *memsto.UserCacheType, event *models.AlertCurEvent) {
|
||||
tpl := taskTplCache.Get(id)
|
||||
if tpl == nil {
|
||||
logger.Errorf("event_callback_ibex: no such tpl(%d)", id)
|
||||
return
|
||||
}
|
||||
// check perm
|
||||
// tpl.GroupId - host - account 三元组校验权限
|
||||
can, err := canDoIbex(tpl.UpdateBy, tpl, host, targetCache, userCache)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: check perm fail: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !can {
|
||||
logger.Errorf("event_callback_ibex: user(%s) no permission", tpl.UpdateBy)
|
||||
return
|
||||
}
|
||||
|
||||
tagsMap := make(map[string]string)
|
||||
for i := 0; i < len(event.TagsJSON); i++ {
|
||||
pair := strings.TrimSpace(event.TagsJSON[i])
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
arr := strings.SplitN(pair, "=", 2)
|
||||
if len(arr) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
tagsMap[arr[0]] = arr[1]
|
||||
}
|
||||
// 附加告警级别 告警触发值标签
|
||||
tagsMap["alert_severity"] = strconv.Itoa(event.Severity)
|
||||
tagsMap["alert_trigger_value"] = event.TriggerValue
|
||||
tagsMap["is_recovered"] = strconv.FormatBool(event.IsRecovered)
|
||||
|
||||
tags, err := json.Marshal(tagsMap)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: failed to marshal tags to json: %v", tagsMap)
|
||||
return
|
||||
}
|
||||
|
||||
// call ibex
|
||||
in := models.TaskForm{
|
||||
Title: tpl.Title + " FH: " + host,
|
||||
Account: tpl.Account,
|
||||
Batch: tpl.Batch,
|
||||
Tolerance: tpl.Tolerance,
|
||||
Timeout: tpl.Timeout,
|
||||
Pause: tpl.Pause,
|
||||
Script: tpl.Script,
|
||||
Args: tpl.Args,
|
||||
Stdin: string(tags),
|
||||
Action: "start",
|
||||
Creator: tpl.UpdateBy,
|
||||
Hosts: []string{host},
|
||||
AlertTriggered: true,
|
||||
}
|
||||
|
||||
id, err = TaskAdd(in, tpl.UpdateBy, ctx.IsCenter)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: call ibex fail: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// write db
|
||||
record := models.TaskRecord{
|
||||
Id: id,
|
||||
EventId: event.Id,
|
||||
GroupId: tpl.GroupId,
|
||||
Title: in.Title,
|
||||
Account: in.Account,
|
||||
Batch: in.Batch,
|
||||
Tolerance: in.Tolerance,
|
||||
Timeout: in.Timeout,
|
||||
Pause: in.Pause,
|
||||
Script: in.Script,
|
||||
Args: in.Args,
|
||||
CreateAt: time.Now().Unix(),
|
||||
CreateBy: in.Creator,
|
||||
}
|
||||
|
||||
if err = record.Add(ctx); err != nil {
|
||||
logger.Errorf("event_callback_ibex: persist task_record fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func canDoIbex(username string, tpl *models.TaskTpl, host string, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType) (bool, error) {
|
||||
user := userCache.GetByUsername(username)
|
||||
if user != nil && user.IsAdmin() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
target, has := targetCache.Get(host)
|
||||
if !has {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return target.MatchGroupId(tpl.GroupId), nil
|
||||
}
|
||||
|
||||
func TaskAdd(f models.TaskForm, authUser string, isCenter bool) (int64, error) {
|
||||
if storage.Cache == nil {
|
||||
logger.Warning("event_callback_ibex: redis cache is nil")
|
||||
return 0, fmt.Errorf("redis cache is nil")
|
||||
}
|
||||
|
||||
hosts := cleanHosts(f.Hosts)
|
||||
if len(hosts) == 0 {
|
||||
return 0, fmt.Errorf("arg(hosts) empty")
|
||||
}
|
||||
|
||||
taskMeta := &imodels.TaskMeta{
|
||||
Title: f.Title,
|
||||
Account: f.Account,
|
||||
Batch: f.Batch,
|
||||
Tolerance: f.Tolerance,
|
||||
Timeout: f.Timeout,
|
||||
Pause: f.Pause,
|
||||
Script: f.Script,
|
||||
Args: f.Args,
|
||||
Stdin: f.Stdin,
|
||||
Creator: f.Creator,
|
||||
}
|
||||
|
||||
err := taskMeta.CleanFields()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
taskMeta.HandleFH(hosts[0])
|
||||
|
||||
// 任务类型分为"告警规则触发"和"n9e center用户下发"两种;
|
||||
// 边缘机房"告警规则触发"的任务不需要规划,并且它可能是失联的,无法使用db资源,所以放入redis缓存中,直接下发给agentd执行
|
||||
if !isCenter && f.AlertTriggered {
|
||||
if err := taskMeta.Create(); err != nil {
|
||||
// 当网络不连通时,生成唯一的id,防止边缘机房中不同任务的id相同;
|
||||
// 方法是,redis自增id去防止同一个机房的不同n9e edge生成的id相同;
|
||||
// 但没法防止不同边缘机房生成同样的id,所以,生成id的数据不会上报存入数据库,只用于闭环执行。
|
||||
taskMeta.Id, err = storage.IdGet()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
taskHost := imodels.TaskHost{
|
||||
Id: taskMeta.Id,
|
||||
Host: hosts[0],
|
||||
Status: "running",
|
||||
}
|
||||
if err = taskHost.Create(); err != nil {
|
||||
logger.Warningf("task_add_fail: authUser=%s title=%s err=%s", authUser, taskMeta.Title, err.Error())
|
||||
}
|
||||
|
||||
// 缓存任务元信息和待下发的任务
|
||||
err = taskMeta.Cache(hosts[0])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
} else {
|
||||
// 如果是中心机房,还是保持之前的逻辑
|
||||
err = taskMeta.Save(hosts, f.Action)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("task_add_succ: authUser=%s title=%s", authUser, taskMeta.Title)
|
||||
return taskMeta.Id, nil
|
||||
}
|
||||
|
||||
func cleanHosts(formHosts []string) []string {
|
||||
cnt := len(formHosts)
|
||||
arr := make([]string, 0, cnt)
|
||||
for i := 0; i < cnt; i++ {
|
||||
item := strings.TrimSpace(formHosts[i])
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(item, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
arr = append(arr, item)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
65
alert/sender/lark.go
Normal file
65
alert/sender/lark.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
var (
|
||||
_ CallBacker = (*LarkSender)(nil)
|
||||
)
|
||||
|
||||
type LarkSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (lk *LarkSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
Text: BuildTplMessage(models.Lark, lk.tpl, ctx.Events),
|
||||
},
|
||||
}
|
||||
|
||||
doSendAndRecord(ctx.Ctx, ctx.CallBackURL, ctx.CallBackURL, body, "callback", ctx.Stats, ctx.Events)
|
||||
}
|
||||
|
||||
func (lk *LarkSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls, tokens := lk.extract(ctx.Users)
|
||||
message := BuildTplMessage(models.Lark, lk.tpl, ctx.Events)
|
||||
for i, url := range urls {
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
doSendAndRecord(ctx.Ctx, url, tokens[i], body, models.Lark, ctx.Stats, ctx.Events)
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LarkSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
tokens := make([]string, 0, len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Lark); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://open.larksuite.com/open-apis/bot/v2/hook/" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return urls, tokens
|
||||
}
|
||||
101
alert/sender/larkcard.go
Normal file
101
alert/sender/larkcard.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
type LarkCardSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (fs *LarkCardSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ats := ExtractAtsParams(ctx.CallBackURL)
|
||||
message := BuildTplMessage(models.LarkCard, fs.tpl, ctx.Events)
|
||||
|
||||
if len(ats) > 0 {
|
||||
atTags := ""
|
||||
for _, at := range ats {
|
||||
if strings.Contains(at, "@") {
|
||||
atTags += fmt.Sprintf("<at email=\"%s\" ></at>", at)
|
||||
} else {
|
||||
atTags += fmt.Sprintf("<at id=\"%s\" ></at>", at)
|
||||
}
|
||||
}
|
||||
message = atTags + message
|
||||
}
|
||||
|
||||
color := "red"
|
||||
lowerUnicode := strings.ToLower(message)
|
||||
if strings.Count(lowerUnicode, Recovered) > 0 && strings.Count(lowerUnicode, Triggered) > 0 {
|
||||
color = "orange"
|
||||
} else if strings.Count(lowerUnicode, Recovered) > 0 {
|
||||
color = "green"
|
||||
}
|
||||
|
||||
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
|
||||
body := createFeishuCardBody()
|
||||
body.Card.Header.Title.Content = SendTitle
|
||||
body.Card.Header.Template = color
|
||||
body.Card.Elements[0].Text.Content = message
|
||||
body.Card.Elements[2].Elements[0].Content = SendTitle
|
||||
|
||||
// This is to be compatible with the Larkcard interface, if with query string parameters, the request will fail
|
||||
// Remove query parameters from the URL,
|
||||
parsedURL, err := url.Parse(ctx.CallBackURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
parsedURL.RawQuery = ""
|
||||
|
||||
doSendAndRecord(ctx.Ctx, ctx.CallBackURL, ctx.CallBackURL, body, "callback", ctx.Stats, ctx.Events)
|
||||
}
|
||||
|
||||
func (fs *LarkCardSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls, tokens := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(models.LarkCard, fs.tpl, ctx.Events)
|
||||
color := "red"
|
||||
lowerUnicode := strings.ToLower(message)
|
||||
if strings.Count(lowerUnicode, Recovered) > 0 && strings.Count(lowerUnicode, Triggered) > 0 {
|
||||
color = "orange"
|
||||
} else if strings.Count(lowerUnicode, Recovered) > 0 {
|
||||
color = "green"
|
||||
}
|
||||
|
||||
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
|
||||
body := createFeishuCardBody()
|
||||
body.Card.Header.Title.Content = SendTitle
|
||||
body.Card.Header.Template = color
|
||||
body.Card.Elements[0].Text.Content = message
|
||||
body.Card.Elements[2].Elements[0].Content = SendTitle
|
||||
for i, url := range urls {
|
||||
doSendAndRecord(ctx.Ctx, url, tokens[i], body, models.LarkCard, ctx.Stats, ctx.Events)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *LarkCardSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
tokens := make([]string, 0)
|
||||
for i := range users {
|
||||
if token, has := users[i].ExtractToken(models.Lark); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://open.larksuite.com/open-apis/bot/v2/hook/" + strings.TrimSpace(token)
|
||||
}
|
||||
urls = append(urls, url)
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return urls, tokens
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
@@ -38,11 +39,24 @@ func (ms *MmSender) Send(ctx MessageContext) {
|
||||
}
|
||||
message := BuildTplMessage(models.Mm, ms.tpl, ctx.Events)
|
||||
|
||||
SendMM(MatterMostMessage{
|
||||
SendMM(ctx.Ctx, MatterMostMessage{
|
||||
Text: message,
|
||||
Tokens: urls,
|
||||
Stats: ctx.Stats,
|
||||
})
|
||||
}, ctx.Events, models.Mm)
|
||||
}
|
||||
|
||||
func (ms *MmSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
message := BuildTplMessage(models.Mm, ms.tpl, ctx.Events)
|
||||
|
||||
SendMM(ctx.Ctx, MatterMostMessage{
|
||||
Text: message,
|
||||
Tokens: []string{ctx.CallBackURL},
|
||||
Stats: ctx.Stats,
|
||||
}, ctx.Events, "callback")
|
||||
}
|
||||
|
||||
func (ms *MmSender) extract(users []*models.User) []string {
|
||||
@@ -55,11 +69,12 @@ func (ms *MmSender) extract(users []*models.User) []string {
|
||||
return tokens
|
||||
}
|
||||
|
||||
func SendMM(message MatterMostMessage) {
|
||||
func SendMM(ctx *ctx.Context, message MatterMostMessage, events []*models.AlertCurEvent, channel string) {
|
||||
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)
|
||||
NotifyRecord(ctx, events, channel, message.Tokens[i], "", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -88,7 +103,7 @@ func SendMM(message MatterMostMessage) {
|
||||
Username: username,
|
||||
Text: txt + message.Text,
|
||||
}
|
||||
doSend(ur, body, models.Mm, message.Stats)
|
||||
doSendAndRecord(ctx, ur, message.Tokens[i], body, channel, message.Stats, events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,30 @@ package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/toolkits/pkg/file"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/sys"
|
||||
)
|
||||
|
||||
func MayPluginNotify(noticeBytes []byte, notifyScript models.NotifyScript, stats *astats.Stats) {
|
||||
func MayPluginNotify(ctx *ctx.Context, noticeBytes []byte, notifyScript models.NotifyScript,
|
||||
stats *astats.Stats, event *models.AlertCurEvent) {
|
||||
if len(noticeBytes) == 0 {
|
||||
return
|
||||
}
|
||||
alertingCallScript(noticeBytes, notifyScript, stats)
|
||||
alertingCallScript(ctx, noticeBytes, notifyScript, stats, event)
|
||||
}
|
||||
|
||||
func alertingCallScript(stdinBytes []byte, notifyScript models.NotifyScript, stats *astats.Stats) {
|
||||
func alertingCallScript(ctx *ctx.Context, stdinBytes []byte, notifyScript models.NotifyScript,
|
||||
stats *astats.Stats, event *models.AlertCurEvent) {
|
||||
// not enable or no notify.py? do nothing
|
||||
config := notifyScript
|
||||
if !config.Enable || config.Content == "" {
|
||||
@@ -81,6 +85,7 @@ func alertingCallScript(stdinBytes []byte, notifyScript models.NotifyScript, sta
|
||||
}
|
||||
|
||||
err, isTimeout := sys.WrapTimeout(cmd, time.Duration(config.Timeout)*time.Second)
|
||||
NotifyRecord(ctx, []*models.AlertCurEvent{event}, channel, cmd.String(), "", buildErr(err, isTimeout))
|
||||
|
||||
if isTimeout {
|
||||
if err == nil {
|
||||
@@ -102,3 +107,11 @@ func alertingCallScript(stdinBytes []byte, notifyScript models.NotifyScript, sta
|
||||
|
||||
logger.Infof("event_script_notify_ok: exec %s output: %s", fpath, buf.String())
|
||||
}
|
||||
|
||||
func buildErr(err error, isTimeout bool) error {
|
||||
if err == nil && !isTimeout {
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("is_timeout: %v, err: %v", isTimeout, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -22,6 +23,7 @@ type (
|
||||
Rule *models.AlertRule
|
||||
Events []*models.AlertCurEvent
|
||||
Stats *astats.Stats
|
||||
Ctx *ctx.Context
|
||||
}
|
||||
)
|
||||
|
||||
@@ -41,17 +43,23 @@ func NewSender(key string, tpls map[string]*template.Template, smtp ...aconf.SMT
|
||||
return &MmSender{tpl: tpls[models.Mm]}
|
||||
case models.Telegram:
|
||||
return &TelegramSender{tpl: tpls[models.Telegram]}
|
||||
case models.Lark:
|
||||
return &LarkSender{tpl: tpls[models.Lark]}
|
||||
case models.LarkCard:
|
||||
return &LarkCardSender{tpl: tpls[models.LarkCard]}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildMessageContext(rule *models.AlertRule, events []*models.AlertCurEvent, uids []int64, userCache *memsto.UserCacheType, stats *astats.Stats) MessageContext {
|
||||
func BuildMessageContext(ctx *ctx.Context, rule *models.AlertRule, events []*models.AlertCurEvent,
|
||||
uids []int64, userCache *memsto.UserCacheType, stats *astats.Stats) MessageContext {
|
||||
users := userCache.GetByUserIds(uids)
|
||||
return MessageContext{
|
||||
Rule: rule,
|
||||
Events: events,
|
||||
Users: users,
|
||||
Stats: stats,
|
||||
Ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
@@ -21,10 +23,27 @@ type telegram struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ CallBacker = (*TelegramSender)(nil)
|
||||
)
|
||||
|
||||
type TelegramSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
message := BuildTplMessage(models.Telegram, ts.tpl, ctx.Events)
|
||||
SendTelegram(ctx.Ctx, TelegramMessage{
|
||||
Text: message,
|
||||
Tokens: []string{ctx.CallBackURL},
|
||||
Stats: ctx.Stats,
|
||||
}, ctx.Events, "callback")
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
@@ -32,11 +51,11 @@ func (ts *TelegramSender) Send(ctx MessageContext) {
|
||||
tokens := ts.extract(ctx.Users)
|
||||
message := BuildTplMessage(models.Telegram, ts.tpl, ctx.Events)
|
||||
|
||||
SendTelegram(TelegramMessage{
|
||||
SendTelegram(ctx.Ctx, TelegramMessage{
|
||||
Text: message,
|
||||
Tokens: tokens,
|
||||
Stats: ctx.Stats,
|
||||
})
|
||||
}, ctx.Events, models.Telegram)
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) extract(users []*models.User) []string {
|
||||
@@ -49,10 +68,11 @@ func (ts *TelegramSender) extract(users []*models.User) []string {
|
||||
return tokens
|
||||
}
|
||||
|
||||
func SendTelegram(message TelegramMessage) {
|
||||
func SendTelegram(ctx *ctx.Context, message TelegramMessage, events []*models.AlertCurEvent, channel string) {
|
||||
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])
|
||||
NotifyRecord(ctx, events, channel, message.Tokens[i], "", errors.New("invalid token"))
|
||||
continue
|
||||
}
|
||||
var url string
|
||||
@@ -73,6 +93,6 @@ func SendTelegram(message TelegramMessage) {
|
||||
Text: message.Text,
|
||||
}
|
||||
|
||||
doSend(url, body, models.Telegram, message.Stats)
|
||||
doSendAndRecord(ctx, url, message.Tokens[i], body, channel, message.Stats, events)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,70 +2,181 @@ package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func SendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
|
||||
for _, conf := range webhooks {
|
||||
if conf.Url == "" || !conf.Enable {
|
||||
continue
|
||||
}
|
||||
bs, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
func sendWebhook(webhook *models.Webhook, event interface{}, stats *astats.Stats) (bool, string, error) {
|
||||
channel := "webhook"
|
||||
if webhook.Type == models.RuleCallback {
|
||||
channel = "callback"
|
||||
}
|
||||
|
||||
bf := bytes.NewBuffer(bs)
|
||||
conf := webhook
|
||||
if conf.Url == "" || !conf.Enable {
|
||||
return false, "", nil
|
||||
}
|
||||
bs, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
logger.Errorf("%s alertingWebhook failed to marshal event:%+v err:%v", channel, event, err)
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", conf.Url, bf)
|
||||
if err != nil {
|
||||
logger.Warning("alertingWebhook failed to new request", err)
|
||||
continue
|
||||
}
|
||||
bf := bytes.NewBuffer(bs)
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if conf.BasicAuthUser != "" && conf.BasicAuthPass != "" {
|
||||
req.SetBasicAuth(conf.BasicAuthUser, conf.BasicAuthPass)
|
||||
}
|
||||
req, err := http.NewRequest("POST", conf.Url, bf)
|
||||
if err != nil {
|
||||
logger.Warningf("%s alertingWebhook failed to new reques event:%s err:%v", channel, string(bs), err)
|
||||
return true, "", err
|
||||
}
|
||||
|
||||
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])
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
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" || conf.Headers[i] == "Host" {
|
||||
req.Host = conf.Headers[i+1]
|
||||
continue
|
||||
}
|
||||
req.Header.Set(conf.Headers[i], conf.Headers[i+1])
|
||||
}
|
||||
}
|
||||
insecureSkipVerify := false
|
||||
if webhook != nil {
|
||||
insecureSkipVerify = webhook.SkipVerify
|
||||
}
|
||||
|
||||
// todo add skip verify
|
||||
client := http.Client{
|
||||
if conf.Client == nil {
|
||||
logger.Warningf("event_%s, event:%s, url: [%s], error: [%s]", channel, string(bs), conf.Url, "client is nil")
|
||||
conf.Client = &http.Client{
|
||||
Timeout: time.Duration(conf.Timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
stats.AlertNotifyTotal.WithLabelValues("webhook").Inc()
|
||||
var resp *http.Response
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues("webhook").Inc()
|
||||
logger.Errorf("event_webhook_fail, ruleId: [%d], eventId: [%d], url: [%s], error: [%s]", event.RuleId, event.Id, conf.Url, err)
|
||||
continue
|
||||
stats.AlertNotifyTotal.WithLabelValues(channel).Inc()
|
||||
var resp *http.Response
|
||||
var body []byte
|
||||
resp, err = conf.Client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
logger.Errorf("event_%s_fail, event:%s, url: [%s], error: [%s]", channel, string(bs), conf.Url, err)
|
||||
return true, "", err
|
||||
}
|
||||
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ = io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
if resp.StatusCode == 429 {
|
||||
logger.Errorf("event_%s_fail, url: %s, response code: %d, body: %s event:%s", channel, conf.Url, resp.StatusCode, string(body), string(bs))
|
||||
return true, string(body), fmt.Errorf("status code is 429")
|
||||
}
|
||||
|
||||
logger.Debugf("event_%s_succ, url: %s, response code: %d, body: %s event:%s", channel, conf.Url, resp.StatusCode, string(body), string(bs))
|
||||
return false, string(body), nil
|
||||
}
|
||||
|
||||
func SingleSendWebhooks(ctx *ctx.Context, webhooks map[string]*models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
|
||||
for _, conf := range webhooks {
|
||||
retryCount := 0
|
||||
for retryCount < 3 {
|
||||
needRetry, res, err := sendWebhook(conf, event, stats)
|
||||
NotifyRecord(ctx, []*models.AlertCurEvent{event}, "webhook", conf.Url, res, err)
|
||||
if !needRetry {
|
||||
break
|
||||
}
|
||||
retryCount++
|
||||
time.Sleep(time.Minute * 1 * time.Duration(retryCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BatchSendWebhooks(ctx *ctx.Context, webhooks map[string]*models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
|
||||
for _, conf := range webhooks {
|
||||
logger.Infof("push event:%+v to queue:%v", event, conf)
|
||||
PushEvent(ctx, conf, event, stats)
|
||||
}
|
||||
}
|
||||
|
||||
var EventQueue = make(map[string]*WebhookQueue)
|
||||
var CallbackEventQueue = make(map[string]*WebhookQueue)
|
||||
var CallbackEventQueueLock sync.RWMutex
|
||||
var EventQueueLock sync.RWMutex
|
||||
|
||||
const QueueMaxSize = 100000
|
||||
|
||||
type WebhookQueue struct {
|
||||
eventQueue *SafeEventQueue
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
func PushEvent(ctx *ctx.Context, webhook *models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
|
||||
EventQueueLock.RLock()
|
||||
queue := EventQueue[webhook.Url]
|
||||
EventQueueLock.RUnlock()
|
||||
|
||||
if queue == nil {
|
||||
queue = &WebhookQueue{
|
||||
eventQueue: NewSafeEventQueue(QueueMaxSize),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
EventQueueLock.Lock()
|
||||
EventQueue[webhook.Url] = queue
|
||||
EventQueueLock.Unlock()
|
||||
|
||||
StartConsumer(ctx, queue, webhook.Batch, webhook, stats)
|
||||
}
|
||||
|
||||
succ := queue.eventQueue.Push(event)
|
||||
if !succ {
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues("push_event_queue").Inc()
|
||||
logger.Warningf("Write channel(%s) full, current channel size: %d event:%v", webhook.Url, queue.eventQueue.Len(), event)
|
||||
}
|
||||
}
|
||||
|
||||
func StartConsumer(ctx *ctx.Context, queue *WebhookQueue, popSize int, webhook *models.Webhook, stats *astats.Stats) {
|
||||
for {
|
||||
select {
|
||||
case <-queue.closeCh:
|
||||
logger.Infof("event queue:%v closed", queue)
|
||||
return
|
||||
default:
|
||||
events := queue.eventQueue.PopN(popSize)
|
||||
if len(events) == 0 {
|
||||
time.Sleep(time.Millisecond * 400)
|
||||
continue
|
||||
}
|
||||
|
||||
retryCount := 0
|
||||
for retryCount < webhook.RetryCount {
|
||||
needRetry, res, err := sendWebhook(webhook, events, stats)
|
||||
go NotifyRecord(ctx, events, "webhook", webhook.Url, res, err)
|
||||
if !needRetry {
|
||||
break
|
||||
}
|
||||
retryCount++
|
||||
time.Sleep(time.Second * time.Duration(webhook.RetryInterval) * time.Duration(retryCount))
|
||||
}
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ = io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
logger.Debugf("event_webhook_succ, url: %s, response code: %d, body: %s event:%+v", conf.Url, resp.StatusCode, string(body), event)
|
||||
}
|
||||
}
|
||||
|
||||
109
alert/sender/webhook_event_queue.go
Normal file
109
alert/sender/webhook_event_queue.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
type SafeEventQueue struct {
|
||||
lock sync.RWMutex
|
||||
maxSize int
|
||||
queueHigh *list.List
|
||||
queueMiddle *list.List
|
||||
queueLow *list.List
|
||||
}
|
||||
|
||||
const (
|
||||
High = 1
|
||||
Middle = 2
|
||||
Low = 3
|
||||
)
|
||||
|
||||
func NewSafeEventQueue(maxSize int) *SafeEventQueue {
|
||||
return &SafeEventQueue{
|
||||
maxSize: maxSize,
|
||||
lock: sync.RWMutex{},
|
||||
queueHigh: list.New(),
|
||||
queueMiddle: list.New(),
|
||||
queueLow: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (spq *SafeEventQueue) Len() int {
|
||||
spq.lock.RLock()
|
||||
defer spq.lock.RUnlock()
|
||||
return spq.queueHigh.Len() + spq.queueMiddle.Len() + spq.queueLow.Len()
|
||||
}
|
||||
|
||||
// len 无锁读取长度,不要在本文件外调用
|
||||
func (spq *SafeEventQueue) len() int {
|
||||
return spq.queueHigh.Len() + spq.queueMiddle.Len() + spq.queueLow.Len()
|
||||
}
|
||||
|
||||
func (spq *SafeEventQueue) Push(event *models.AlertCurEvent) bool {
|
||||
spq.lock.Lock()
|
||||
defer spq.lock.Unlock()
|
||||
|
||||
for spq.len() > spq.maxSize {
|
||||
return false
|
||||
}
|
||||
|
||||
switch event.Severity {
|
||||
case High:
|
||||
spq.queueHigh.PushBack(event)
|
||||
case Middle:
|
||||
spq.queueMiddle.PushBack(event)
|
||||
case Low:
|
||||
spq.queueLow.PushBack(event)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// pop 无锁弹出事件,不要在本文件外调用
|
||||
func (spq *SafeEventQueue) pop() *models.AlertCurEvent {
|
||||
if spq.len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var elem interface{}
|
||||
|
||||
if spq.queueHigh.Len() > 0 {
|
||||
elem = spq.queueHigh.Remove(spq.queueHigh.Front())
|
||||
} else if spq.queueMiddle.Len() > 0 {
|
||||
elem = spq.queueMiddle.Remove(spq.queueMiddle.Front())
|
||||
} else {
|
||||
elem = spq.queueLow.Remove(spq.queueLow.Front())
|
||||
}
|
||||
event, ok := elem.(*models.AlertCurEvent)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func (spq *SafeEventQueue) Pop() *models.AlertCurEvent {
|
||||
spq.lock.Lock()
|
||||
defer spq.lock.Unlock()
|
||||
return spq.pop()
|
||||
}
|
||||
|
||||
func (spq *SafeEventQueue) PopN(n int) []*models.AlertCurEvent {
|
||||
spq.lock.Lock()
|
||||
defer spq.lock.Unlock()
|
||||
|
||||
events := make([]*models.AlertCurEvent, 0, n)
|
||||
count := 0
|
||||
for count < n && spq.len() > 0 {
|
||||
event := spq.pop()
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
count++
|
||||
}
|
||||
return events
|
||||
}
|
||||
157
alert/sender/webhook_event_queue_test.go
Normal file
157
alert/sender/webhook_event_queue_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSafePriorityQueue_ConcurrentPushPop(t *testing.T) {
|
||||
spq := NewSafeEventQueue(100000)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 100
|
||||
numEvents := 1000
|
||||
|
||||
// 并发 Push
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < numEvents; j++ {
|
||||
event := &models.AlertCurEvent{
|
||||
Severity: goroutineID%3 + 1,
|
||||
TriggerTime: time.Now().UnixNano(),
|
||||
}
|
||||
spq.Push(event)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 检查队列长度是否正确
|
||||
expectedLen := numGoroutines * numEvents
|
||||
assert.Equal(t, expectedLen, spq.Len(), "Queue length mismatch after concurrent pushes")
|
||||
|
||||
// 并发 Pop
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
event := spq.Pop()
|
||||
if event == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 最终队列应该为空
|
||||
assert.Equal(t, 0, spq.Len(), "Queue should be empty after concurrent pops")
|
||||
}
|
||||
|
||||
func TestSafePriorityQueue_ConcurrentPopMax(t *testing.T) {
|
||||
spq := NewSafeEventQueue(100000)
|
||||
|
||||
// 添加初始数据
|
||||
for i := 0; i < 1000; i++ {
|
||||
spq.Push(&models.AlertCurEvent{
|
||||
Severity: i%3 + 1,
|
||||
TriggerTime: time.Now().UnixNano(),
|
||||
})
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 10
|
||||
popMax := 100
|
||||
|
||||
// 并发 PopN
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
events := spq.PopN(popMax)
|
||||
assert.LessOrEqual(t, len(events), popMax, "PopN exceeded maximum")
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 检查队列长度是否正确
|
||||
expectedRemaining := 1000 - (numGoroutines * popMax)
|
||||
if expectedRemaining < 0 {
|
||||
expectedRemaining = 0
|
||||
}
|
||||
assert.Equal(t, expectedRemaining, spq.Len(), "Queue length mismatch after concurrent PopN")
|
||||
}
|
||||
|
||||
func TestSafePriorityQueue_ConcurrentPushPopWithDifferentSeverities(t *testing.T) {
|
||||
spq := NewSafeEventQueue(100000)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 50
|
||||
numEvents := 500
|
||||
|
||||
// 并发 Push 不同优先级的事件
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < numEvents; j++ {
|
||||
event := &models.AlertCurEvent{
|
||||
Severity: goroutineID%3 + 1, // 模拟不同的 Severity
|
||||
TriggerTime: time.Now().UnixNano(),
|
||||
}
|
||||
spq.Push(event)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 检查队列长度是否正确
|
||||
expectedLen := numGoroutines * numEvents
|
||||
assert.Equal(t, expectedLen, spq.Len(), "Queue length mismatch after concurrent pushes")
|
||||
|
||||
// 检查事件的顺序是否按照优先级排列
|
||||
var lastEvent *models.AlertCurEvent
|
||||
for spq.Len() > 0 {
|
||||
event := spq.Pop()
|
||||
if lastEvent != nil {
|
||||
assert.LessOrEqual(t, lastEvent.Severity, event.Severity, "Events are not in correct priority order")
|
||||
}
|
||||
lastEvent = event
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafePriorityQueue_ExceedMaxSize(t *testing.T) {
|
||||
spq := NewSafeEventQueue(5)
|
||||
|
||||
// 插入超过最大容量的事件
|
||||
for i := 0; i < 10; i++ {
|
||||
spq.Push(&models.AlertCurEvent{
|
||||
Severity: i % 3,
|
||||
TriggerTime: int64(i),
|
||||
})
|
||||
}
|
||||
|
||||
// 验证队列的长度是否不超过 maxSize
|
||||
assert.LessOrEqual(t, spq.Len(), spq.maxSize)
|
||||
|
||||
// 验证队列中剩余事件的内容
|
||||
expectedEvents := 5
|
||||
if spq.Len() < 5 {
|
||||
expectedEvents = spq.Len()
|
||||
}
|
||||
|
||||
// 检查最后存入的事件是否是按优先级排序
|
||||
for i := 0; i < expectedEvents; i++ {
|
||||
event := spq.Pop()
|
||||
if event != nil {
|
||||
assert.LessOrEqual(t, event.Severity, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
111
alert/sender/webhook_queue.go
Normal file
111
alert/sender/webhook_queue.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
type SafeList struct {
|
||||
sync.RWMutex
|
||||
L *list.List
|
||||
}
|
||||
|
||||
func NewSafeList() *SafeList {
|
||||
return &SafeList{L: list.New()}
|
||||
}
|
||||
|
||||
func (sl *SafeList) PushFront(v interface{}) *list.Element {
|
||||
sl.Lock()
|
||||
e := sl.L.PushFront(v)
|
||||
sl.Unlock()
|
||||
return e
|
||||
}
|
||||
|
||||
func (sl *SafeList) PushFrontBatch(vs []interface{}) {
|
||||
sl.Lock()
|
||||
for _, item := range vs {
|
||||
sl.L.PushFront(item)
|
||||
}
|
||||
sl.Unlock()
|
||||
}
|
||||
|
||||
func (sl *SafeList) PopBack(max int) []*models.AlertCurEvent {
|
||||
sl.Lock()
|
||||
|
||||
count := sl.L.Len()
|
||||
if count == 0 {
|
||||
sl.Unlock()
|
||||
return []*models.AlertCurEvent{}
|
||||
}
|
||||
|
||||
if count > max {
|
||||
count = max
|
||||
}
|
||||
|
||||
items := make([]*models.AlertCurEvent, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
item := sl.L.Remove(sl.L.Back())
|
||||
sample, ok := item.(*models.AlertCurEvent)
|
||||
if ok {
|
||||
items = append(items, sample)
|
||||
}
|
||||
}
|
||||
|
||||
sl.Unlock()
|
||||
return items
|
||||
}
|
||||
|
||||
func (sl *SafeList) RemoveAll() {
|
||||
sl.Lock()
|
||||
sl.L.Init()
|
||||
sl.Unlock()
|
||||
}
|
||||
|
||||
func (sl *SafeList) Len() int {
|
||||
sl.RLock()
|
||||
size := sl.L.Len()
|
||||
sl.RUnlock()
|
||||
return size
|
||||
}
|
||||
|
||||
// SafeList with Limited Size
|
||||
type SafeListLimited struct {
|
||||
maxSize int
|
||||
SL *SafeList
|
||||
}
|
||||
|
||||
func NewSafeListLimited(maxSize int) *SafeListLimited {
|
||||
return &SafeListLimited{SL: NewSafeList(), maxSize: maxSize}
|
||||
}
|
||||
|
||||
func (sll *SafeListLimited) PopBack(max int) []*models.AlertCurEvent {
|
||||
return sll.SL.PopBack(max)
|
||||
}
|
||||
|
||||
func (sll *SafeListLimited) PushFront(v interface{}) bool {
|
||||
if sll.SL.Len() >= sll.maxSize {
|
||||
return false
|
||||
}
|
||||
|
||||
sll.SL.PushFront(v)
|
||||
return true
|
||||
}
|
||||
|
||||
func (sll *SafeListLimited) PushFrontBatch(vs []interface{}) bool {
|
||||
if sll.SL.Len() >= sll.maxSize {
|
||||
return false
|
||||
}
|
||||
|
||||
sll.SL.PushFrontBatch(vs)
|
||||
return true
|
||||
}
|
||||
|
||||
func (sll *SafeListLimited) RemoveAll() {
|
||||
sll.SL.RemoveAll()
|
||||
}
|
||||
|
||||
func (sll *SafeListLimited) Len() int {
|
||||
return sll.SL.Len()
|
||||
}
|
||||
@@ -16,29 +16,50 @@ type wecom struct {
|
||||
Markdown wecomMarkdown `json:"markdown"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ CallBacker = (*WecomSender)(nil)
|
||||
)
|
||||
|
||||
type WecomSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ws *WecomSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
message := BuildTplMessage(models.Wecom, ws.tpl, ctx.Events)
|
||||
body := wecom{
|
||||
Msgtype: "markdown",
|
||||
Markdown: wecomMarkdown{
|
||||
Content: message,
|
||||
},
|
||||
}
|
||||
|
||||
doSendAndRecord(ctx.Ctx, ctx.CallBackURL, ctx.CallBackURL, body, "callback", ctx.Stats, ctx.Events)
|
||||
}
|
||||
|
||||
func (ws *WecomSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls := ws.extract(ctx.Users)
|
||||
urls, tokens := ws.extract(ctx.Users)
|
||||
message := BuildTplMessage(models.Wecom, ws.tpl, ctx.Events)
|
||||
for _, url := range urls {
|
||||
for i, url := range urls {
|
||||
body := wecom{
|
||||
Msgtype: "markdown",
|
||||
Markdown: wecomMarkdown{
|
||||
Content: message,
|
||||
},
|
||||
}
|
||||
doSend(url, body, models.Wecom, ctx.Stats)
|
||||
doSendAndRecord(ctx.Ctx, url, tokens[i], body, models.Wecom, ctx.Stats, ctx.Events)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WecomSender) extract(users []*models.User) []string {
|
||||
func (ws *WecomSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
tokens := make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Wecom); has {
|
||||
url := token
|
||||
@@ -46,7 +67,8 @@ func (ws *WecomSender) extract(users []*models.User) []string {
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return urls
|
||||
return urls, tokens
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ type Center struct {
|
||||
AnonymousAccess AnonymousAccess
|
||||
UseFileAssets bool
|
||||
FlashDuty FlashDuty
|
||||
EventHistoryGroupView bool
|
||||
CleanNotifyRecordDay int
|
||||
MigrateBusiGroupLabel bool
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
@@ -22,8 +25,9 @@ type Plugin struct {
|
||||
}
|
||||
|
||||
type FlashDuty struct {
|
||||
Api string `json:"api"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
Api string
|
||||
Headers map[string]string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type AnonymousAccess struct {
|
||||
|
||||
@@ -18,20 +18,28 @@ var MetricDesc MetricDescType
|
||||
// GetMetricDesc , if metric is not registered, empty string will be returned
|
||||
func GetMetricDesc(lang, metric string) string {
|
||||
var m map[string]string
|
||||
if lang == "zh" {
|
||||
m = MetricDesc.Zh
|
||||
} else {
|
||||
|
||||
switch lang {
|
||||
case "en":
|
||||
m = MetricDesc.En
|
||||
default:
|
||||
m = MetricDesc.Zh
|
||||
}
|
||||
|
||||
if m != nil {
|
||||
if desc, has := m[metric]; has {
|
||||
if desc, ok := m[metric]; ok {
|
||||
return desc
|
||||
}
|
||||
}
|
||||
|
||||
return MetricDesc.CommonDesc[metric]
|
||||
}
|
||||
if MetricDesc.CommonDesc != nil {
|
||||
if desc, ok := MetricDesc.CommonDesc[metric]; ok {
|
||||
return desc
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
func LoadMetricsYaml(configDir, metricsYamlFile string) error {
|
||||
fp := metricsYamlFile
|
||||
if fp == "" {
|
||||
|
||||
@@ -76,7 +76,9 @@ ops:
|
||||
- "/dashboards/add"
|
||||
- "/dashboards/put"
|
||||
- "/dashboards/del"
|
||||
- "/dashboards-built-in"
|
||||
- "/embedded-dashboards/put"
|
||||
- "/embedded-dashboards"
|
||||
- "/public-dashboards"
|
||||
|
||||
- name: alert
|
||||
cname: 告警规则
|
||||
@@ -85,7 +87,7 @@ ops:
|
||||
- "/alert-rules/add"
|
||||
- "/alert-rules/put"
|
||||
- "/alert-rules/del"
|
||||
- "/alert-rules-built-in"
|
||||
|
||||
- name: alert-mutes
|
||||
cname: 告警静默管理
|
||||
ops:
|
||||
@@ -172,6 +174,22 @@ ops:
|
||||
- "/busi-groups/put"
|
||||
- "/busi-groups/del"
|
||||
|
||||
- name: builtin-metrics
|
||||
cname: 指标视图
|
||||
ops:
|
||||
- "/metrics-built-in"
|
||||
- "/builtin-metrics/add"
|
||||
- "/builtin-metrics/put"
|
||||
- "/builtin-metrics/del"
|
||||
|
||||
- name: built-in-components
|
||||
cname: 模版中心
|
||||
ops:
|
||||
- "/built-in-components"
|
||||
- "/built-in-components/add"
|
||||
- "/built-in-components/put"
|
||||
- "/built-in-components/del"
|
||||
|
||||
- name: system
|
||||
cname: 系统信息
|
||||
ops:
|
||||
|
||||
@@ -6,13 +6,18 @@ import (
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/dispatch"
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
alertrt "github.com/ccfos/nightingale/v6/alert/router"
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/center/cconf/rsa"
|
||||
"github.com/ccfos/nightingale/v6/center/cstats"
|
||||
"github.com/ccfos/nightingale/v6/center/integration"
|
||||
"github.com/ccfos/nightingale/v6/center/metas"
|
||||
centerrt "github.com/ccfos/nightingale/v6/center/router"
|
||||
"github.com/ccfos/nightingale/v6/center/sso"
|
||||
"github.com/ccfos/nightingale/v6/conf"
|
||||
"github.com/ccfos/nightingale/v6/cron"
|
||||
"github.com/ccfos/nightingale/v6/dumper"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
@@ -25,13 +30,12 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/pkg/version"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/idents"
|
||||
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
alertrt "github.com/ccfos/nightingale/v6/alert/router"
|
||||
centerrt "github.com/ccfos/nightingale/v6/center/router"
|
||||
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
|
||||
"github.com/flashcatcloud/ibex/src/cmd/ibex"
|
||||
)
|
||||
|
||||
func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
@@ -45,6 +49,10 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
|
||||
cconf.MergeOperationConf()
|
||||
|
||||
if config.Alert.Heartbeat.EngineName == "" {
|
||||
config.Alert.Heartbeat.EngineName = "default"
|
||||
}
|
||||
|
||||
logxClean, err := logx.Init(config.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -60,13 +68,16 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
}
|
||||
ctx := ctx.NewContext(context.Background(), db, true)
|
||||
migrate.Migrate(db)
|
||||
models.InitRoot(ctx)
|
||||
isRootInit := models.InitRoot(ctx)
|
||||
|
||||
config.HTTP.JWTAuth.SigningKey = models.InitJWTSigningKey(ctx)
|
||||
|
||||
err = rsa.InitRSAConfig(ctx, &config.HTTP.RSA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go integration.Init(ctx, config.Center.BuiltinIntegrationsDir)
|
||||
var redis storage.Redis
|
||||
redis, err = storage.NewRedis(config.Redis)
|
||||
if err != nil {
|
||||
@@ -79,8 +90,6 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
syncStats := memsto.NewSyncStats()
|
||||
alertStats := astats.NewSyncStats()
|
||||
|
||||
sso := sso.Init(config.Center, ctx)
|
||||
|
||||
configCache := memsto.NewConfigCache(ctx, syncStats, config.HTTP.RSA.RSAPrivateKey, config.HTTP.RSA.RSAPassWord)
|
||||
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, redis)
|
||||
@@ -90,31 +99,56 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
notifyConfigCache := memsto.NewNotifyConfigCache(ctx, configCache)
|
||||
userCache := memsto.NewUserCache(ctx, syncStats)
|
||||
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
|
||||
taskTplCache := memsto.NewTaskTplCache(ctx)
|
||||
configCvalCache := memsto.NewCvalCache(ctx, syncStats)
|
||||
|
||||
sso := sso.Init(config.Center, ctx, configCache)
|
||||
promClients := prom.NewPromClient(ctx)
|
||||
|
||||
dispatch.InitRegisterQueryFunc(promClients)
|
||||
|
||||
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
|
||||
|
||||
externalProcessors := process.NewExternalProcessors()
|
||||
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
|
||||
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
|
||||
|
||||
writers := writer.NewWriters(config.Pushgw)
|
||||
|
||||
go version.GetGithubVersion()
|
||||
|
||||
go cron.CleanNotifyRecord(ctx, config.Center.CleanNotifyRecordDay)
|
||||
|
||||
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
|
||||
centerRouter := centerrt.New(config.HTTP, config.Center, config.Alert, cconf.Operations, dsCache, notifyConfigCache, promClients, tdengineClients,
|
||||
centerRouter := centerrt.New(config.HTTP, config.Center, config.Alert, config.Ibex,
|
||||
cconf.Operations, dsCache, notifyConfigCache, promClients, tdengineClients,
|
||||
redis, sso, ctx, metas, idents, targetCache, userCache, userGroupCache)
|
||||
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)
|
||||
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
|
||||
go func() {
|
||||
if config.Center.MigrateBusiGroupLabel || models.CanMigrateBg(ctx) {
|
||||
models.MigrateBg(ctx, pushgwRouter.Pushgw.BusiGroupLabelKey)
|
||||
}
|
||||
}()
|
||||
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP, configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
|
||||
|
||||
centerRouter.Config(r)
|
||||
alertrtRouter.Config(r)
|
||||
pushgwRouter.Config(r)
|
||||
dumper.ConfigRouter(r)
|
||||
|
||||
if config.Ibex.Enable {
|
||||
migrate.MigrateIbexTables(db)
|
||||
ibex.ServerStart(true, db, redis, config.HTTP.APIForService.BasicAuth, config.Alert.Heartbeat, &config.CenterApi, r, centerRouter, config.Ibex, config.HTTP.Port)
|
||||
}
|
||||
|
||||
httpClean := httpx.Init(config.HTTP, r)
|
||||
|
||||
fmt.Printf("please view n9e at http://%v:%v\n", config.Alert.Heartbeat.IP, config.HTTP.Port)
|
||||
if isRootInit {
|
||||
fmt.Println("username/password: root/root.2020")
|
||||
}
|
||||
|
||||
return func() {
|
||||
logxClean()
|
||||
httpClean()
|
||||
|
||||
381
center/integration/init.go
Normal file
381
center/integration/init.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/toolkits/pkg/file"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/runner"
|
||||
)
|
||||
|
||||
const SYSTEM = "system"
|
||||
|
||||
func Init(ctx *ctx.Context, builtinIntegrationsDir string) {
|
||||
err := models.InitBuiltinPayloads(ctx)
|
||||
if err != nil {
|
||||
logger.Warning("init old builtinPayloads fail ", err)
|
||||
return
|
||||
}
|
||||
|
||||
fp := builtinIntegrationsDir
|
||||
if fp == "" {
|
||||
fp = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
|
||||
// var fileList []string
|
||||
dirList, err := file.DirsUnder(fp)
|
||||
if err != nil {
|
||||
logger.Warning("read builtin component dir fail ", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, dir := range dirList {
|
||||
// components icon
|
||||
componentDir := fp + "/" + dir
|
||||
component := models.BuiltinComponent{
|
||||
Ident: dir,
|
||||
}
|
||||
|
||||
// get logo name
|
||||
// /api/n9e/integrations/icon/AliYun/aliyun.png
|
||||
files, err := file.FilesUnder(componentDir + "/icon")
|
||||
if err == nil && len(files) > 0 {
|
||||
component.Logo = "/api/n9e/integrations/icon/" + component.Ident + "/" + files[0]
|
||||
} else if err != nil {
|
||||
logger.Warningf("read builtin component icon dir fail %s %v", component.Ident, err)
|
||||
}
|
||||
|
||||
// get description
|
||||
files, err = file.FilesUnder(componentDir + "/markdown")
|
||||
if err == nil && len(files) > 0 {
|
||||
var readmeFile string
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(strings.ToLower(file), "md") {
|
||||
readmeFile = componentDir + "/markdown/" + file
|
||||
break
|
||||
}
|
||||
}
|
||||
if readmeFile != "" {
|
||||
component.Readme, _ = file.ReadString(readmeFile)
|
||||
}
|
||||
} else if err != nil {
|
||||
logger.Warningf("read builtin component markdown dir fail %s %v", component.Ident, err)
|
||||
}
|
||||
|
||||
exists, _ := models.BuiltinComponentExists(ctx, &component)
|
||||
if !exists {
|
||||
err = component.Add(ctx, SYSTEM)
|
||||
if err != nil {
|
||||
logger.Warning("add builtin component fail ", component, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
old, err := models.BuiltinComponentGet(ctx, "ident = ?", component.Ident)
|
||||
if err != nil {
|
||||
logger.Warning("get builtin component fail ", component, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
logger.Warning("get builtin component nil ", component)
|
||||
continue
|
||||
}
|
||||
|
||||
if old.UpdatedBy == SYSTEM {
|
||||
now := time.Now().Unix()
|
||||
old.CreatedAt = now
|
||||
old.UpdatedAt = now
|
||||
old.Readme = component.Readme
|
||||
old.UpdatedBy = SYSTEM
|
||||
|
||||
err = models.DB(ctx).Model(old).Select("*").Updates(old).Error
|
||||
if err != nil {
|
||||
logger.Warning("update builtin component fail ", old, err)
|
||||
}
|
||||
}
|
||||
component.ID = old.ID
|
||||
}
|
||||
|
||||
// delete uuid is emtpy
|
||||
err = models.DB(ctx).Exec("delete from builtin_payloads where uuid = 0 and type != 'collect' and (updated_by = 'system' or updated_by = '')").Error
|
||||
if err != nil {
|
||||
logger.Warning("delete builtin payloads fail ", err)
|
||||
}
|
||||
|
||||
// delete builtin metrics uuid is emtpy
|
||||
err = models.DB(ctx).Exec("delete from builtin_metrics where uuid = 0 and (updated_by = 'system' or updated_by = '')").Error
|
||||
if err != nil {
|
||||
logger.Warning("delete builtin metrics fail ", err)
|
||||
}
|
||||
|
||||
// 删除 uuid%1000 不为 0 uuid > 1000000000000000000 且 type 为 dashboard 的记录
|
||||
err = models.DB(ctx).Exec("delete from builtin_payloads where uuid%1000 != 0 and uuid > 1000000000000000000 and type = 'dashboard' and updated_by = 'system'").Error
|
||||
if err != nil {
|
||||
logger.Warning("delete builtin payloads fail ", err)
|
||||
}
|
||||
|
||||
// alerts
|
||||
files, err = file.FilesUnder(componentDir + "/alerts")
|
||||
if err == nil && len(files) > 0 {
|
||||
for _, f := range files {
|
||||
fp := componentDir + "/alerts/" + f
|
||||
bs, err := file.ReadBytes(fp)
|
||||
if err != nil {
|
||||
logger.Warning("read builtin component alerts file fail ", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
alerts := []models.AlertRule{}
|
||||
err = json.Unmarshal(bs, &alerts)
|
||||
if err != nil {
|
||||
logger.Warning("parse builtin component alerts file fail ", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
newAlerts := []models.AlertRule{}
|
||||
writeAlertFileFlag := false
|
||||
for _, alert := range alerts {
|
||||
if alert.UUID == 0 {
|
||||
writeAlertFileFlag = true
|
||||
alert.UUID = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
newAlerts = append(newAlerts, alert)
|
||||
content, err := json.Marshal(alert)
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin alert fail ", alert, err)
|
||||
continue
|
||||
}
|
||||
|
||||
cate := strings.Replace(f, ".json", "", -1)
|
||||
builtinAlert := models.BuiltinPayload{
|
||||
ComponentID: component.ID,
|
||||
Type: "alert",
|
||||
Cate: cate,
|
||||
Name: alert.Name,
|
||||
Tags: alert.AppendTags,
|
||||
Content: string(content),
|
||||
UUID: alert.UUID,
|
||||
}
|
||||
|
||||
old, err := models.BuiltinPayloadGet(ctx, "uuid = ?", alert.UUID)
|
||||
if err != nil {
|
||||
logger.Warning("get builtin alert fail ", builtinAlert, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
err := builtinAlert.Add(ctx, SYSTEM)
|
||||
if err != nil {
|
||||
logger.Warning("add builtin alert fail ", builtinAlert, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if old.UpdatedBy == SYSTEM {
|
||||
old.ComponentID = component.ID
|
||||
old.Content = string(content)
|
||||
old.Name = alert.Name
|
||||
old.Tags = alert.AppendTags
|
||||
err = models.DB(ctx).Model(old).Select("*").Updates(old).Error
|
||||
if err != nil {
|
||||
logger.Warningf("update builtin alert:%+v fail %v", builtinAlert, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if writeAlertFileFlag {
|
||||
bs, err = json.MarshalIndent(newAlerts, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin alerts fail ", newAlerts, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = file.WriteBytes(fp, bs)
|
||||
if err != nil {
|
||||
logger.Warning("write builtin alerts file fail ", f, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// dashboards
|
||||
files, err = file.FilesUnder(componentDir + "/dashboards")
|
||||
if err == nil && len(files) > 0 {
|
||||
for _, f := range files {
|
||||
fp := componentDir + "/dashboards/" + f
|
||||
bs, err := file.ReadBytes(fp)
|
||||
if err != nil {
|
||||
logger.Warning("read builtin component dashboards file fail ", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
dashboard := BuiltinBoard{}
|
||||
err = json.Unmarshal(bs, &dashboard)
|
||||
if err != nil {
|
||||
logger.Warning("parse builtin component dashboards file fail ", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if dashboard.UUID == 0 {
|
||||
time.Sleep(time.Microsecond)
|
||||
dashboard.UUID = time.Now().UnixMicro()
|
||||
// 补全文件中的 uuid
|
||||
bs, err = json.MarshalIndent(dashboard, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin dashboard fail ", dashboard, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = file.WriteBytes(fp, bs)
|
||||
if err != nil {
|
||||
logger.Warning("write builtin dashboard file fail ", f, err)
|
||||
}
|
||||
}
|
||||
|
||||
content, err := json.Marshal(dashboard)
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin dashboard fail ", dashboard, err)
|
||||
continue
|
||||
}
|
||||
|
||||
builtinDashboard := models.BuiltinPayload{
|
||||
ComponentID: component.ID,
|
||||
Type: "dashboard",
|
||||
Cate: "",
|
||||
Name: dashboard.Name,
|
||||
Tags: dashboard.Tags,
|
||||
Content: string(content),
|
||||
UUID: dashboard.UUID,
|
||||
}
|
||||
|
||||
old, err := models.BuiltinPayloadGet(ctx, "uuid = ?", dashboard.UUID)
|
||||
if err != nil {
|
||||
logger.Warning("get builtin alert fail ", builtinDashboard, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
err := builtinDashboard.Add(ctx, SYSTEM)
|
||||
if err != nil {
|
||||
logger.Warning("add builtin alert fail ", builtinDashboard, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if old.UpdatedBy == SYSTEM {
|
||||
old.ComponentID = component.ID
|
||||
old.Content = string(content)
|
||||
old.Name = dashboard.Name
|
||||
old.Tags = dashboard.Tags
|
||||
err = models.DB(ctx).Model(old).Select("*").Updates(old).Error
|
||||
if err != nil {
|
||||
logger.Warningf("update builtin alert:%+v fail %v", builtinDashboard, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
logger.Warningf("read builtin component dash dir fail %s %v", component.Ident, err)
|
||||
}
|
||||
|
||||
// metrics
|
||||
files, err = file.FilesUnder(componentDir + "/metrics")
|
||||
if err == nil && len(files) > 0 {
|
||||
for _, f := range files {
|
||||
fp := componentDir + "/metrics/" + f
|
||||
bs, err := file.ReadBytes(fp)
|
||||
if err != nil {
|
||||
logger.Warning("read builtin component metrics file fail", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
metrics := []models.BuiltinMetric{}
|
||||
newMetrics := []models.BuiltinMetric{}
|
||||
err = json.Unmarshal(bs, &metrics)
|
||||
if err != nil {
|
||||
logger.Warning("parse builtin component metrics file fail", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
writeMetricFileFlag := false
|
||||
for _, metric := range metrics {
|
||||
if metric.UUID == 0 {
|
||||
writeMetricFileFlag = true
|
||||
metric.UUID = time.Now().UnixNano()
|
||||
}
|
||||
newMetrics = append(newMetrics, metric)
|
||||
|
||||
old, err := models.BuiltinMetricGet(ctx, "uuid = ?", metric.UUID)
|
||||
if err != nil {
|
||||
logger.Warning("get builtin metrics fail ", metric, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
err := metric.Add(ctx, SYSTEM)
|
||||
if err != nil {
|
||||
logger.Warning("add builtin metrics fail ", metric, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if old.UpdatedBy == SYSTEM {
|
||||
old.Collector = metric.Collector
|
||||
old.Typ = metric.Typ
|
||||
old.Name = metric.Name
|
||||
old.Unit = metric.Unit
|
||||
old.Note = metric.Note
|
||||
old.Lang = metric.Lang
|
||||
old.Expression = metric.Expression
|
||||
|
||||
err = models.DB(ctx).Model(old).Select("*").Updates(old).Error
|
||||
if err != nil {
|
||||
logger.Warningf("update builtin metric:%+v fail %v", metric, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if writeMetricFileFlag {
|
||||
bs, err = json.MarshalIndent(newMetrics, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin metrics fail ", newMetrics, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = file.WriteBytes(fp, bs)
|
||||
if err != nil {
|
||||
logger.Warning("write builtin metrics file fail ", f, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else if err != nil {
|
||||
logger.Warningf("read builtin component metrics dir fail %s %v", component.Ident, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BuiltinBoard 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 interface{} `json:"configs" gorm:"-"`
|
||||
Public int `json:"public"` // 0: false, 1: true
|
||||
PublicCate int `json:"public_cate"` // 0: anonymous, 1: login, 2: busi
|
||||
Bgids []int64 `json:"bgids" gorm:"-"`
|
||||
BuiltIn int `json:"built_in"` // 0: false, 1: true
|
||||
Hide int `json:"hide"` // 0: false, 1: true
|
||||
UUID int64 `json:"uuid"`
|
||||
}
|
||||
@@ -13,8 +13,10 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/center/cstats"
|
||||
"github.com/ccfos/nightingale/v6/center/metas"
|
||||
"github.com/ccfos/nightingale/v6/center/sso"
|
||||
"github.com/ccfos/nightingale/v6/conf"
|
||||
_ "github.com/ccfos/nightingale/v6/front/statik"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/aop"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/httpx"
|
||||
@@ -34,6 +36,7 @@ import (
|
||||
type Router struct {
|
||||
HTTP httpx.Config
|
||||
Center cconf.Center
|
||||
Ibex conf.Ibex
|
||||
Alert aconf.Alert
|
||||
Operations cconf.Operation
|
||||
DatasourceCache *memsto.DatasourceCacheType
|
||||
@@ -48,13 +51,20 @@ type Router struct {
|
||||
UserCache *memsto.UserCacheType
|
||||
UserGroupCache *memsto.UserGroupCacheType
|
||||
Ctx *ctx.Context
|
||||
HeartbeatHook HeartbeatHookFunc
|
||||
TargetDeleteHook models.TargetDeleteHookFunc
|
||||
}
|
||||
|
||||
func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType, pc *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, redis storage.Redis, sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, idents *idents.Set, tc *memsto.TargetCacheType, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) *Router {
|
||||
func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, ibex conf.Ibex,
|
||||
operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType,
|
||||
pc *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, redis storage.Redis,
|
||||
sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, idents *idents.Set,
|
||||
tc *memsto.TargetCacheType, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) *Router {
|
||||
return &Router{
|
||||
HTTP: httpConfig,
|
||||
Center: center,
|
||||
Alert: alert,
|
||||
Ibex: ibex,
|
||||
Operations: operations,
|
||||
DatasourceCache: ds,
|
||||
NotifyConfigCache: ncc,
|
||||
@@ -68,9 +78,15 @@ func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, operat
|
||||
UserCache: uc,
|
||||
UserGroupCache: ugc,
|
||||
Ctx: ctx,
|
||||
HeartbeatHook: func(ident string) map[string]interface{} { return nil },
|
||||
TargetDeleteHook: emptyDeleteHook,
|
||||
}
|
||||
}
|
||||
|
||||
func emptyDeleteHook(ctx *ctx.Context, idents []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func stat() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
@@ -91,15 +107,17 @@ func languageDetector(i18NHeaderKey string) gin.HandlerFunc {
|
||||
if headerKey != "" {
|
||||
lang := c.GetHeader(headerKey)
|
||||
if lang != "" {
|
||||
if strings.HasPrefix(lang, "zh") {
|
||||
c.Request.Header.Set("X-Language", "zh")
|
||||
if strings.HasPrefix(lang, "zh_HK") {
|
||||
c.Request.Header.Set("X-Language", "zh_HK")
|
||||
} else if strings.HasPrefix(lang, "zh") {
|
||||
c.Request.Header.Set("X-Language", "zh_CN")
|
||||
} else if strings.HasPrefix(lang, "en") {
|
||||
c.Request.Header.Set("X-Language", "en")
|
||||
} else {
|
||||
c.Request.Header.Set("X-Language", lang)
|
||||
}
|
||||
} else {
|
||||
c.Request.Header.Set("X-Language", "en")
|
||||
c.Request.Header.Set("X-Language", "zh_CN")
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
@@ -112,7 +130,7 @@ func (rt *Router) configNoRoute(r *gin.Engine, fs *http.FileSystem) {
|
||||
suffix := arr[len(arr)-1]
|
||||
|
||||
switch suffix {
|
||||
case "png", "jpeg", "jpg", "svg", "ico", "gif", "css", "js", "html", "htm", "gz", "zip", "map", "ttf":
|
||||
case "png", "jpeg", "jpg", "svg", "ico", "gif", "css", "js", "html", "htm", "gz", "zip", "map", "ttf", "md":
|
||||
if !rt.Center.UseFileAssets {
|
||||
c.FileFromFS(c.Request.URL.Path, *fs)
|
||||
} else {
|
||||
@@ -166,6 +184,7 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.POST("/query-range-batch", rt.promBatchQueryRange)
|
||||
pages.POST("/query-instant-batch", rt.promBatchQueryInstant)
|
||||
pages.GET("/datasource/brief", rt.datasourceBriefs)
|
||||
pages.POST("/datasource/query", rt.datasourceQuery)
|
||||
|
||||
pages.POST("/ds-query", rt.QueryData)
|
||||
pages.POST("/logs-query", rt.QueryLog)
|
||||
@@ -179,6 +198,7 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.POST("/query-range-batch", rt.auth(), rt.promBatchQueryRange)
|
||||
pages.POST("/query-instant-batch", rt.auth(), rt.promBatchQueryInstant)
|
||||
pages.GET("/datasource/brief", rt.auth(), rt.user(), rt.datasourceBriefs)
|
||||
pages.POST("/datasource/query", rt.auth(), rt.user(), rt.datasourceQuery)
|
||||
|
||||
pages.POST("/ds-query", rt.auth(), rt.QueryData)
|
||||
pages.POST("/logs-query", rt.auth(), rt.QueryLog)
|
||||
@@ -229,6 +249,20 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.POST("/metric-views", rt.auth(), rt.user(), rt.metricViewAdd)
|
||||
pages.PUT("/metric-views", rt.auth(), rt.user(), rt.metricViewPut)
|
||||
|
||||
pages.GET("/builtin-metric-filters", rt.auth(), rt.user(), rt.metricFilterGets)
|
||||
pages.DELETE("/builtin-metric-filters", rt.auth(), rt.user(), rt.metricFilterDel)
|
||||
pages.POST("/builtin-metric-filters", rt.auth(), rt.user(), rt.metricFilterAdd)
|
||||
pages.PUT("/builtin-metric-filters", rt.auth(), rt.user(), rt.metricFilterPut)
|
||||
pages.POST("/builtin-metric-promql", rt.auth(), rt.user(), rt.getMetricPromql)
|
||||
|
||||
pages.POST("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics/add"), rt.builtinMetricsAdd)
|
||||
pages.PUT("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics/put"), rt.builtinMetricsPut)
|
||||
pages.DELETE("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics/del"), rt.builtinMetricsDel)
|
||||
pages.GET("/builtin-metrics", rt.auth(), rt.user(), rt.builtinMetricsGets)
|
||||
pages.GET("/builtin-metrics/types", rt.auth(), rt.user(), rt.builtinMetricsTypes)
|
||||
pages.GET("/builtin-metrics/types/default", rt.auth(), rt.user(), rt.builtinMetricsDefaultTypes)
|
||||
pages.GET("/builtin-metrics/collectors", rt.auth(), rt.user(), rt.builtinMetricsCollectors)
|
||||
|
||||
pages.GET("/user-groups", rt.auth(), rt.user(), rt.userGroupGets)
|
||||
pages.POST("/user-groups", rt.auth(), rt.user(), rt.perm("/user-groups/add"), rt.userGroupAdd)
|
||||
pages.GET("/user-group/:id", rt.auth(), rt.user(), rt.userGroupGet)
|
||||
@@ -246,6 +280,7 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.DELETE("/busi-group/:id/members", rt.auth(), rt.user(), rt.perm("/busi-groups/put"), rt.bgrw(), rt.busiGroupMemberDel)
|
||||
pages.DELETE("/busi-group/:id", rt.auth(), rt.user(), rt.perm("/busi-groups/del"), rt.bgrw(), rt.busiGroupDel)
|
||||
pages.GET("/busi-group/:id/perm/:perm", rt.auth(), rt.user(), rt.checkBusiGroupPerm)
|
||||
pages.GET("/busi-groups/tags", rt.auth(), rt.user(), rt.busiGroupsGetTags)
|
||||
|
||||
pages.GET("/targets", rt.auth(), rt.user(), rt.targetGets)
|
||||
pages.GET("/target/extra-meta", rt.auth(), rt.user(), rt.targetExtendInfoByIdent)
|
||||
@@ -255,26 +290,28 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.POST("/targets/tags", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetBindTagsByFE)
|
||||
pages.DELETE("/targets/tags", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetUnbindTagsByFE)
|
||||
pages.PUT("/targets/note", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetUpdateNote)
|
||||
pages.PUT("/targets/bgid", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetUpdateBgid)
|
||||
pages.PUT("/targets/bgids", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetBindBgids)
|
||||
|
||||
pages.POST("/builtin-cate-favorite", rt.auth(), rt.user(), rt.builtinCateFavoriteAdd)
|
||||
pages.DELETE("/builtin-cate-favorite/:name", rt.auth(), rt.user(), rt.builtinCateFavoriteDel)
|
||||
|
||||
pages.GET("/builtin-boards", rt.builtinBoardGets)
|
||||
pages.GET("/builtin-board/:name", rt.builtinBoardGet)
|
||||
pages.GET("/dashboards/builtin/list", rt.builtinBoardGets)
|
||||
pages.GET("/builtin-boards-cates", rt.auth(), rt.user(), rt.builtinBoardCateGets)
|
||||
pages.POST("/builtin-boards-detail", rt.auth(), rt.user(), rt.builtinBoardDetailGets)
|
||||
pages.GET("/integrations/icon/:cate/:name", rt.builtinIcon)
|
||||
pages.GET("/integrations/makedown/:cate", rt.builtinMarkdown)
|
||||
|
||||
// pages.GET("/builtin-boards", rt.builtinBoardGets)
|
||||
// pages.GET("/builtin-board/:name", rt.builtinBoardGet)
|
||||
// pages.GET("/dashboards/builtin/list", rt.builtinBoardGets)
|
||||
// pages.GET("/builtin-boards-cates", rt.auth(), rt.user(), rt.builtinBoardCateGets)
|
||||
// pages.POST("/builtin-boards-detail", rt.auth(), rt.user(), rt.builtinBoardDetailGets)
|
||||
// pages.GET("/integrations/makedown/:cate", rt.builtinMarkdown)
|
||||
|
||||
pages.GET("/busi-groups/public-boards", rt.auth(), rt.user(), rt.perm("/dashboards"), rt.publicBoardGets)
|
||||
pages.GET("/busi-groups/boards", rt.auth(), rt.user(), rt.perm("/dashboards"), rt.boardGetsByGids)
|
||||
pages.GET("/busi-group/:id/boards", rt.auth(), rt.user(), rt.perm("/dashboards"), rt.bgro(), rt.boardGets)
|
||||
pages.POST("/busi-group/:id/boards", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.bgrw(), rt.boardAdd)
|
||||
pages.POST("/busi-group/:id/board/:bid/clone", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.bgrw(), rt.boardClone)
|
||||
pages.POST("/busi-group/:id/boards/clones", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.boardBatchClone)
|
||||
pages.POST("/busi-groups/boards/clones", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.boardBatchClone)
|
||||
|
||||
pages.GET("/boards", rt.auth(), rt.user(), rt.boardGetsByBids)
|
||||
pages.GET("/board/:bid", rt.boardGet)
|
||||
pages.GET("/board/:bid/pure", rt.boardPureGet)
|
||||
pages.PUT("/board/:bid", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.boardPut)
|
||||
@@ -285,19 +322,24 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.GET("/share-charts", rt.chartShareGets)
|
||||
pages.POST("/share-charts", rt.auth(), rt.chartShareAdd)
|
||||
|
||||
pages.GET("/alert-rules/builtin/alerts-cates", rt.auth(), rt.user(), rt.builtinAlertCateGets)
|
||||
pages.GET("/alert-rules/builtin/list", rt.auth(), rt.user(), rt.builtinAlertRules)
|
||||
// pages.GET("/alert-rules/builtin/alerts-cates", rt.auth(), rt.user(), rt.builtinAlertCateGets)
|
||||
// pages.GET("/alert-rules/builtin/list", rt.auth(), rt.user(), rt.builtinAlertRules)
|
||||
pages.GET("/alert-rules/callbacks", rt.auth(), rt.user(), rt.alertRuleCallbacks)
|
||||
|
||||
pages.GET("/busi-groups/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRuleGetsByGids)
|
||||
pages.GET("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRuleGets)
|
||||
pages.POST("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.alertRuleAddByFE)
|
||||
pages.POST("/busi-group/:id/alert-rules/import", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.alertRuleAddByImport)
|
||||
pages.POST("/busi-group/:id/alert-rules/import-prom-rule", rt.auth(),
|
||||
rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.alertRuleAddByImportPromRule)
|
||||
pages.DELETE("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules/del"), rt.bgrw(), rt.alertRuleDel)
|
||||
pages.PUT("/busi-group/:id/alert-rules/fields", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.bgrw(), rt.alertRulePutFields)
|
||||
pages.PUT("/busi-group/:id/alert-rule/:arid", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.alertRulePutByFE)
|
||||
pages.GET("/alert-rule/:arid", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRuleGet)
|
||||
pages.GET("/alert-rule/:arid/pure", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRulePureGet)
|
||||
pages.PUT("/busi-group/alert-rule/validate", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.alertRuleValidation)
|
||||
pages.POST("/relabel-test", rt.auth(), rt.user(), rt.relabelTest)
|
||||
pages.POST("/busi-group/:id/alert-rules/clone", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.cloneToMachine)
|
||||
|
||||
pages.GET("/busi-groups/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGetsByGids)
|
||||
pages.GET("/busi-group/:id/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGets)
|
||||
@@ -326,16 +368,18 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
if rt.Center.AnonymousAccess.AlertDetail {
|
||||
pages.GET("/alert-cur-event/:eid", rt.alertCurEventGet)
|
||||
pages.GET("/alert-his-event/:eid", rt.alertHisEventGet)
|
||||
pages.GET("/event-notify-records/:eid", rt.notificationRecordList)
|
||||
} else {
|
||||
pages.GET("/alert-cur-event/:eid", rt.auth(), rt.alertCurEventGet)
|
||||
pages.GET("/alert-his-event/:eid", rt.auth(), rt.alertHisEventGet)
|
||||
pages.GET("/alert-cur-event/:eid", rt.auth(), rt.user(), rt.alertCurEventGet)
|
||||
pages.GET("/alert-his-event/:eid", rt.auth(), rt.user(), rt.alertHisEventGet)
|
||||
pages.GET("/event-notify-records/:eid", rt.auth(), rt.user(), rt.notificationRecordList)
|
||||
}
|
||||
|
||||
// card logic
|
||||
pages.GET("/alert-cur-events/list", rt.auth(), rt.alertCurEventsList)
|
||||
pages.GET("/alert-cur-events/card", rt.auth(), rt.alertCurEventsCard)
|
||||
pages.GET("/alert-cur-events/list", rt.auth(), rt.user(), rt.alertCurEventsList)
|
||||
pages.GET("/alert-cur-events/card", rt.auth(), rt.user(), rt.alertCurEventsCard)
|
||||
pages.POST("/alert-cur-events/card/details", rt.auth(), rt.alertCurEventsCardDetails)
|
||||
pages.GET("/alert-his-events/list", rt.auth(), rt.alertHisEventsList)
|
||||
pages.GET("/alert-his-events/list", rt.auth(), rt.user(), rt.alertHisEventsList)
|
||||
pages.DELETE("/alert-cur-events", rt.auth(), rt.user(), rt.perm("/alert-cur-events/del"), rt.alertCurEventDel)
|
||||
pages.GET("/alert-cur-events/stats", rt.auth(), rt.alertCurEventsStatistics)
|
||||
|
||||
@@ -356,11 +400,9 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.GET("/busi-groups/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks"), rt.taskGetsByGids)
|
||||
pages.GET("/busi-group/:id/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks"), rt.bgro(), rt.taskGets)
|
||||
pages.POST("/busi-group/:id/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks/add"), rt.bgrw(), rt.taskAdd)
|
||||
pages.GET("/busi-group/:id/task/*url", rt.auth(), rt.user(), rt.perm("/job-tasks"), rt.taskProxy)
|
||||
pages.PUT("/busi-group/:id/task/*url", rt.auth(), rt.user(), rt.perm("/job-tasks/put"), rt.bgrw(), rt.taskProxy)
|
||||
|
||||
pages.GET("/servers", rt.auth(), rt.admin(), rt.serversGet)
|
||||
pages.GET("/server-clusters", rt.auth(), rt.admin(), rt.serverClustersGet)
|
||||
pages.GET("/servers", rt.auth(), rt.user(), rt.serversGet)
|
||||
pages.GET("/server-clusters", rt.auth(), rt.user(), rt.serverClustersGet)
|
||||
|
||||
pages.POST("/datasource/list", rt.auth(), rt.user(), rt.datasourceList)
|
||||
pages.POST("/datasource/plugin/list", rt.auth(), rt.pluginList)
|
||||
@@ -410,6 +452,9 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.PUT("/es-index-pattern", rt.auth(), rt.admin(), rt.esIndexPatternPut)
|
||||
pages.DELETE("/es-index-pattern", rt.auth(), rt.admin(), rt.esIndexPatternDel)
|
||||
|
||||
pages.GET("/embedded-dashboards", rt.auth(), rt.user(), rt.perm("/embedded-dashboards"), rt.embeddedDashboardsGet)
|
||||
pages.PUT("/embedded-dashboards", rt.auth(), rt.user(), rt.perm("/embedded-dashboards/put"), rt.embeddedDashboardsPut)
|
||||
|
||||
pages.GET("/user-variable-configs", rt.auth(), rt.user(), rt.perm("/help/variable-configs"), rt.userVariableConfigGets)
|
||||
pages.POST("/user-variable-config", rt.auth(), rt.user(), rt.perm("/help/variable-configs"), rt.userVariableConfigAdd)
|
||||
pages.PUT("/user-variable-config/:id", rt.auth(), rt.user(), rt.perm("/help/variable-configs"), rt.userVariableConfigPut)
|
||||
@@ -422,6 +467,18 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
// for admin api
|
||||
pages.GET("/user/busi-groups", rt.auth(), rt.admin(), rt.userBusiGroupsGets)
|
||||
|
||||
pages.GET("/builtin-components", rt.auth(), rt.user(), rt.builtinComponentsGets)
|
||||
pages.POST("/builtin-components", rt.auth(), rt.user(), rt.perm("/built-in-components/add"), rt.builtinComponentsAdd)
|
||||
pages.PUT("/builtin-components", rt.auth(), rt.user(), rt.perm("/built-in-components/put"), rt.builtinComponentsPut)
|
||||
pages.DELETE("/builtin-components", rt.auth(), rt.user(), rt.perm("/built-in-components/del"), rt.builtinComponentsDel)
|
||||
|
||||
pages.GET("/builtin-payloads", rt.auth(), rt.user(), rt.builtinPayloadsGets)
|
||||
pages.GET("/builtin-payloads/cates", rt.auth(), rt.user(), rt.builtinPayloadcatesGet)
|
||||
pages.POST("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/add"), rt.builtinPayloadsAdd)
|
||||
pages.GET("/builtin-payload/:id", rt.auth(), rt.user(), rt.perm("/built-in-components"), rt.builtinPayloadGet)
|
||||
pages.PUT("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/put"), rt.builtinPayloadsPut)
|
||||
pages.DELETE("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/del"), rt.builtinPayloadsDel)
|
||||
pages.GET("/builtin-payload", rt.auth(), rt.user(), rt.builtinPayloadsGetByUUIDOrID)
|
||||
}
|
||||
|
||||
r.GET("/api/n9e/versions", func(c *gin.Context) {
|
||||
@@ -455,10 +512,14 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
service.GET("/user-group-members", rt.userGroupMemberGetsByService)
|
||||
|
||||
service.GET("/targets", rt.targetGetsByService)
|
||||
service.GET("/target/extra-meta", rt.targetExtendInfoByIdent)
|
||||
service.POST("/target/list", rt.targetGetsByHostFilter)
|
||||
service.DELETE("/targets", rt.targetDelByService)
|
||||
service.GET("/targets/tags", rt.targetGetTags)
|
||||
service.POST("/targets/tags", rt.targetBindTagsByService)
|
||||
service.DELETE("/targets/tags", rt.targetUnbindTagsByService)
|
||||
service.PUT("/targets/note", rt.targetUpdateNoteByService)
|
||||
service.PUT("/targets/bgid", rt.targetUpdateBgidByService)
|
||||
|
||||
service.POST("/alert-rules", rt.alertRuleAddByService)
|
||||
service.POST("/alert-rule-add", rt.alertRuleAddOneByService)
|
||||
@@ -488,10 +549,13 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
service.GET("/alert-his-event/:eid", rt.alertHisEventGet)
|
||||
|
||||
service.GET("/task-tpl/:tid", rt.taskTplGetByService)
|
||||
service.GET("/task-tpls", rt.taskTplGetsByService)
|
||||
service.GET("/task-tpl/statistics", rt.taskTplStatistics)
|
||||
|
||||
service.GET("/config/:id", rt.configGet)
|
||||
service.GET("/configs", rt.configsGet)
|
||||
service.GET("/config", rt.configGetByKey)
|
||||
service.GET("/all-configs", rt.configGetAll)
|
||||
service.PUT("/configs", rt.configsPut)
|
||||
service.POST("/configs", rt.configsPost)
|
||||
service.DELETE("/configs", rt.configsDel)
|
||||
@@ -509,6 +573,11 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
|
||||
service.GET("/targets-of-alert-rule", rt.targetsOfAlertRule)
|
||||
|
||||
service.POST("/notify-record", rt.notificationRecordAdd)
|
||||
|
||||
service.GET("/alert-cur-events-del-by-hash", rt.alertCurEventDelByHash)
|
||||
|
||||
service.POST("/center/heartbeat", rt.heartbeat)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ func (rt *Router) alertCurEventsCard(c *gin.Context) {
|
||||
stime, etime := getTimeRange(c)
|
||||
severity := ginx.QueryInt(c, "severity", -1)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
busiGroupId := ginx.QueryInt64(c, "bgid", 0)
|
||||
dsIds := queryDatasourceIds(c)
|
||||
rules := parseAggrRules(c)
|
||||
|
||||
@@ -62,8 +61,12 @@ func (rt *Router) alertCurEventsCard(c *gin.Context) {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
bgids, err := GetBusinessGroupIds(c, rt.Ctx, rt.Center.EventHistoryGroupView)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
// 最多获取50000个,获取太多也没啥意义
|
||||
list, err := models.AlertCurEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query, 50000, 0)
|
||||
list, err := models.AlertCurEventsGet(rt.Ctx, prods, bgids, stime, etime, severity, dsIds,
|
||||
cates, 0, query, 50000, 0)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
cardmap := make(map[string]*AlertCard)
|
||||
@@ -142,7 +145,6 @@ func (rt *Router) alertCurEventsList(c *gin.Context) {
|
||||
severity := ginx.QueryInt(c, "severity", -1)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
busiGroupId := ginx.QueryInt64(c, "bgid", 0)
|
||||
dsIds := queryDatasourceIds(c)
|
||||
|
||||
prod := ginx.QueryStr(c, "prods", "")
|
||||
@@ -161,10 +163,17 @@ func (rt *Router) alertCurEventsList(c *gin.Context) {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
total, err := models.AlertCurEventTotal(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query)
|
||||
ruleId := ginx.QueryInt64(c, "rid", 0)
|
||||
|
||||
bgids, err := GetBusinessGroupIds(c, rt.Ctx, rt.Center.EventHistoryGroupView)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertCurEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query, limit, ginx.Offset(c, limit))
|
||||
total, err := models.AlertCurEventTotal(rt.Ctx, prods, bgids, stime, etime, severity, dsIds,
|
||||
cates, ruleId, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertCurEventsGet(rt.Ctx, prods, bgids, stime, etime, severity, dsIds,
|
||||
cates, ruleId, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
@@ -197,7 +206,9 @@ func (rt *Router) checkCurEventBusiGroupRWPermission(c *gin.Context, ids []int64
|
||||
for i := 0; i < len(ids); i++ {
|
||||
event, err := models.AlertCurEventGetById(rt.Ctx, ids[i])
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if event == nil {
|
||||
continue
|
||||
}
|
||||
if _, has := set[event.GroupId]; !has {
|
||||
rt.bgrwCheck(c, event.GroupId)
|
||||
set[event.GroupId] = struct{}{}
|
||||
@@ -214,6 +225,16 @@ func (rt *Router) alertCurEventGet(c *gin.Context) {
|
||||
ginx.Bomb(404, "No such active event")
|
||||
}
|
||||
|
||||
if !rt.Center.AnonymousAccess.AlertDetail && rt.Center.EventHistoryGroupView {
|
||||
rt.bgroCheck(c, event.GroupId)
|
||||
}
|
||||
|
||||
ruleConfig, needReset := models.FillRuleConfigTplName(rt.Ctx, event.RuleConfig)
|
||||
if needReset {
|
||||
event.RuleConfigJson = ruleConfig
|
||||
}
|
||||
|
||||
event.LastEvalTime = event.TriggerTime
|
||||
ginx.NewRender(c).Data(event, nil)
|
||||
}
|
||||
|
||||
@@ -221,3 +242,8 @@ func (rt *Router) alertCurEventsStatistics(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Data(models.AlertCurEventStatistics(rt.Ctx, time.Now()), nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertCurEventDelByHash(c *gin.Context) {
|
||||
hash := ginx.QueryStr(c, "hash")
|
||||
ginx.NewRender(c).Message(models.AlertCurEventDelByHash(rt.Ctx, hash))
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func getTimeRange(c *gin.Context) (stime, etime int64) {
|
||||
@@ -33,7 +36,6 @@ func (rt *Router) alertHisEventsList(c *gin.Context) {
|
||||
recovered := ginx.QueryInt(c, "is_recovered", -1)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
busiGroupId := ginx.QueryInt64(c, "bgid", 0)
|
||||
dsIds := queryDatasourceIds(c)
|
||||
|
||||
prod := ginx.QueryStr(c, "prods", "")
|
||||
@@ -52,10 +54,17 @@ func (rt *Router) alertHisEventsList(c *gin.Context) {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
total, err := models.AlertHisEventTotal(rt.Ctx, prods, busiGroupId, stime, etime, severity, recovered, dsIds, cates, query)
|
||||
ruleId := ginx.QueryInt64(c, "rid", 0)
|
||||
|
||||
bgids, err := GetBusinessGroupIds(c, rt.Ctx, rt.Center.EventHistoryGroupView)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertHisEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, recovered, dsIds, cates, query, limit, ginx.Offset(c, limit))
|
||||
total, err := models.AlertHisEventTotal(rt.Ctx, prods, bgids, stime, etime, severity,
|
||||
recovered, dsIds, cates, ruleId, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertHisEventGets(rt.Ctx, prods, bgids, stime, etime, severity, recovered,
|
||||
dsIds, cates, ruleId, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
@@ -78,5 +87,55 @@ func (rt *Router) alertHisEventGet(c *gin.Context) {
|
||||
ginx.Bomb(404, "No such alert event")
|
||||
}
|
||||
|
||||
if !rt.Center.AnonymousAccess.AlertDetail && rt.Center.EventHistoryGroupView {
|
||||
rt.bgroCheck(c, event.GroupId)
|
||||
}
|
||||
|
||||
ruleConfig, needReset := models.FillRuleConfigTplName(rt.Ctx, event.RuleConfig)
|
||||
if needReset {
|
||||
event.RuleConfigJson = ruleConfig
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(event, err)
|
||||
}
|
||||
|
||||
func GetBusinessGroupIds(c *gin.Context, ctx *ctx.Context, eventHistoryGroupView bool) ([]int64, error) {
|
||||
bgid := ginx.QueryInt64(c, "bgid", 0)
|
||||
var bgids []int64
|
||||
|
||||
if !eventHistoryGroupView || strings.HasPrefix(c.Request.URL.Path, "/v1") {
|
||||
if bgid > 0 {
|
||||
return []int64{bgid}, nil
|
||||
}
|
||||
return bgids, nil
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
if user.IsAdmin() {
|
||||
if bgid > 0 {
|
||||
return []int64{bgid}, nil
|
||||
}
|
||||
return bgids, nil
|
||||
}
|
||||
|
||||
bussGroupIds, err := models.MyBusiGroupIds(ctx, user.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(bussGroupIds) == 0 {
|
||||
// 如果没查到用户属于任何业务组,需要返回一个0,否则会导致查询到全部告警历史
|
||||
return []int64{0}, nil
|
||||
}
|
||||
|
||||
if bgid > 0 && !slices.Contains(bussGroupIds, bgid) {
|
||||
return nil, fmt.Errorf("business group ID not allowed")
|
||||
}
|
||||
|
||||
if bgid > 0 {
|
||||
// Pass filter parameters, priority to use
|
||||
return []int64{bgid}, nil
|
||||
}
|
||||
|
||||
return bussGroupIds, nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/pconf"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/prometheus/prometheus/prompb"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
"github.com/toolkits/pkg/str"
|
||||
@@ -28,6 +37,18 @@ func (rt *Router) alertRuleGets(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(ars, err)
|
||||
}
|
||||
|
||||
func getAlertCueEventTimeRange(c *gin.Context) (stime, etime int64) {
|
||||
stime = ginx.QueryInt64(c, "stime", 0)
|
||||
etime = ginx.QueryInt64(c, "etime", 0)
|
||||
if etime == 0 {
|
||||
etime = time.Now().Unix()
|
||||
}
|
||||
if stime == 0 || stime >= etime {
|
||||
stime = etime - 30*24*int64(time.Hour.Seconds())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleGetsByGids(c *gin.Context) {
|
||||
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
if len(gids) > 0 {
|
||||
@@ -40,15 +61,46 @@ func (rt *Router) alertRuleGetsByGids(c *gin.Context) {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ars, err := models.AlertRuleGetsByBGIds(rt.Ctx, gids)
|
||||
if err == nil {
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
rids := make([]int64, 0, len(ars))
|
||||
names := make([]string, 0, len(ars))
|
||||
for i := 0; i < len(ars); i++ {
|
||||
ars[i].FillNotifyGroups(rt.Ctx, cache)
|
||||
ars[i].FillSeverities()
|
||||
|
||||
if len(ars[i].DatasourceQueries) != 0 {
|
||||
ars[i].DatasourceIdsJson = rt.DatasourceCache.GetIDsByDsCateAndQueries(ars[i].Cate, ars[i].DatasourceQueries)
|
||||
}
|
||||
|
||||
rids = append(rids, ars[i].Id)
|
||||
names = append(names, ars[i].UpdateBy)
|
||||
}
|
||||
|
||||
stime, etime := getAlertCueEventTimeRange(c)
|
||||
cnt := models.AlertCurEventCountByRuleId(rt.Ctx, rids, stime, etime)
|
||||
if cnt != nil {
|
||||
for i := 0; i < len(ars); i++ {
|
||||
ars[i].CurEventCount = cnt[ars[i].Id]
|
||||
}
|
||||
}
|
||||
|
||||
users := models.UserMapGet(rt.Ctx, "username in (?)", names)
|
||||
if users != nil {
|
||||
for i := 0; i < len(ars); i++ {
|
||||
if user, exist := users[ars[i].UpdateBy]; exist {
|
||||
ars[i].UpdateByNickname = user.Nickname
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(ars, err)
|
||||
@@ -76,6 +128,10 @@ func (rt *Router) alertRulesGetByService(c *gin.Context) {
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
for i := 0; i < len(ars); i++ {
|
||||
ars[i].FillNotifyGroups(rt.Ctx, cache)
|
||||
|
||||
if len(ars[i].DatasourceQueries) != 0 {
|
||||
ars[i].DatasourceIdsJson = rt.DatasourceCache.GetIDsByDsCateAndQueries(ars[i].Cate, ars[i].DatasourceQueries)
|
||||
}
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(ars, err)
|
||||
@@ -110,12 +166,48 @@ func (rt *Router) alertRuleAddByImport(c *gin.Context) {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
for i := range lst {
|
||||
if len(lst[i].DatasourceQueries) == 0 {
|
||||
lst[i].DatasourceQueries = []models.DatasourceQuery{
|
||||
models.DataSourceQueryAll,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
reterr := rt.alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language"))
|
||||
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
type promRuleForm struct {
|
||||
Payload string `json:"payload" binding:"required"`
|
||||
DatasourceQueries []models.DatasourceQuery `json:"datasource_queries" binding:"required"`
|
||||
Disabled int `json:"disabled" binding:"gte=0,lte=1"`
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleAddByImportPromRule(c *gin.Context) {
|
||||
var f promRuleForm
|
||||
ginx.Dangerous(c.BindJSON(&f))
|
||||
|
||||
var pr struct {
|
||||
Groups []models.PromRuleGroup `yaml:"groups"`
|
||||
}
|
||||
err := yaml.Unmarshal([]byte(f.Payload), &pr)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, "invalid yaml format, please use the example format. err: %v", err)
|
||||
}
|
||||
|
||||
if len(pr.Groups) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input yaml is empty")
|
||||
}
|
||||
|
||||
lst := models.DealPromGroup(pr.Groups, f.DatasourceQueries, f.Disabled)
|
||||
username := c.MustGet("username").(string)
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
ginx.NewRender(c).Data(rt.alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language")), nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleAddByService(c *gin.Context) {
|
||||
var lst []models.AlertRule
|
||||
ginx.BindJSON(c, &lst)
|
||||
@@ -266,6 +358,43 @@ func (rt *Router) alertRulePutFields(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.Action == "update_triggers" {
|
||||
if triggers, has := f.Fields["triggers"]; has {
|
||||
originRule := ar.RuleConfigJson.(map[string]interface{})
|
||||
originRule["triggers"] = triggers
|
||||
b, err := json.Marshal(originRule)
|
||||
ginx.Dangerous(err)
|
||||
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, map[string]interface{}{"rule_config": string(b)}))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if f.Action == "annotations_add" {
|
||||
if annotations, has := f.Fields["annotations"]; has {
|
||||
annotationsMap := annotations.(map[string]interface{})
|
||||
for k, v := range annotationsMap {
|
||||
ar.AnnotationsJSON[k] = v.(string)
|
||||
}
|
||||
b, err := json.Marshal(ar.AnnotationsJSON)
|
||||
ginx.Dangerous(err)
|
||||
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, map[string]interface{}{"annotations": string(b)}))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if f.Action == "annotations_del" {
|
||||
if annotations, has := f.Fields["annotations"]; has {
|
||||
annotationsKeys := annotations.(map[string]interface{})
|
||||
for key := range annotationsKeys {
|
||||
delete(ar.AnnotationsJSON, key)
|
||||
}
|
||||
b, err := json.Marshal(ar.AnnotationsJSON)
|
||||
ginx.Dangerous(err)
|
||||
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, map[string]interface{}{"annotations": string(b)}))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if f.Action == "callback_add" {
|
||||
// 增加一个 callback 地址
|
||||
if callbacks, has := f.Fields["callbacks"]; has {
|
||||
@@ -286,6 +415,16 @@ func (rt *Router) alertRulePutFields(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if f.Action == "datasource_change" {
|
||||
// 修改数据源
|
||||
if datasourceQueries, has := f.Fields["datasource_queries"]; has {
|
||||
bytes, err := json.Marshal(datasourceQueries)
|
||||
ginx.Dangerous(err)
|
||||
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, map[string]interface{}{"datasource_queries": bytes}))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range f.Fields {
|
||||
ginx.Dangerous(ar.UpdateColumn(rt.Ctx, k, v))
|
||||
}
|
||||
@@ -305,12 +444,30 @@ func (rt *Router) alertRuleGet(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(ar.DatasourceQueries) != 0 {
|
||||
ar.DatasourceIdsJson = rt.DatasourceCache.GetIDsByDsCateAndQueries(ar.Cate, ar.DatasourceQueries)
|
||||
}
|
||||
|
||||
err = ar.FillNotifyGroups(rt.Ctx, make(map[int64]*models.UserGroup))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(ar, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRulePureGet(c *gin.Context) {
|
||||
arid := ginx.UrlParamInt64(c, "arid")
|
||||
|
||||
ar, err := models.AlertRuleGetById(rt.Ctx, arid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if ar == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such AlertRule")
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(ar, err)
|
||||
}
|
||||
|
||||
// pre validation before save rule
|
||||
func (rt *Router) alertRuleValidation(c *gin.Context) {
|
||||
var f models.AlertRule //new
|
||||
@@ -383,3 +540,138 @@ func (rt *Router) alertRuleCallbacks(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Data(callbacks, nil)
|
||||
}
|
||||
|
||||
type alertRuleTestForm struct {
|
||||
Configs []*pconf.RelabelConfig `json:"configs"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (rt *Router) relabelTest(c *gin.Context) {
|
||||
var f alertRuleTestForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Tags) == 0 || len(f.Configs) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "relabel config is empty")
|
||||
}
|
||||
|
||||
labels := make([]prompb.Label, len(f.Tags))
|
||||
for i, tag := range f.Tags {
|
||||
label := strings.SplitN(tag, "=", 2)
|
||||
if len(label) != 2 {
|
||||
ginx.Bomb(http.StatusBadRequest, "tag:%s format error", tag)
|
||||
}
|
||||
|
||||
labels[i] = prompb.Label{Name: label[0], Value: label[1]}
|
||||
}
|
||||
|
||||
for i := 0; i < len(f.Configs); i++ {
|
||||
if f.Configs[i].Replacement == "" {
|
||||
f.Configs[i].Replacement = "$1"
|
||||
}
|
||||
|
||||
if f.Configs[i].Separator == "" {
|
||||
f.Configs[i].Separator = ";"
|
||||
}
|
||||
|
||||
if f.Configs[i].Regex == "" {
|
||||
f.Configs[i].Regex = "(.*)"
|
||||
}
|
||||
}
|
||||
|
||||
relabels := writer.Process(labels, f.Configs...)
|
||||
|
||||
var tags []string
|
||||
for _, label := range relabels {
|
||||
tags = append(tags, fmt.Sprintf("%s=%s", label.Name, label.Value))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(tags, nil)
|
||||
}
|
||||
|
||||
type identListForm struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
IdentList []string `json:"ident_list"`
|
||||
}
|
||||
|
||||
func containsIdentOperator(s string) bool {
|
||||
pattern := `ident\s*(!=|!~|=~)`
|
||||
matched, err := regexp.MatchString(pattern, s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
func (rt *Router) cloneToMachine(c *gin.Context) {
|
||||
var f identListForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.IdentList) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "ident_list is empty")
|
||||
}
|
||||
|
||||
alertRules, err := models.AlertRuleGetsByIds(rt.Ctx, f.Ids)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
re := regexp.MustCompile(`ident\s*=\s*\\".*?\\"`)
|
||||
|
||||
user := c.MustGet("username").(string)
|
||||
now := time.Now().Unix()
|
||||
|
||||
newRules := make([]*models.AlertRule, 0)
|
||||
|
||||
reterr := make(map[string]map[string]string)
|
||||
|
||||
for i := range alertRules {
|
||||
errMsg := make(map[string]string)
|
||||
|
||||
if alertRules[i].Cate != "prometheus" {
|
||||
errMsg["all"] = "Only Prometheus rule can be cloned to machines"
|
||||
reterr[alertRules[i].Name] = errMsg
|
||||
continue
|
||||
}
|
||||
|
||||
if containsIdentOperator(alertRules[i].RuleConfig) {
|
||||
errMsg["all"] = "promql is missing ident"
|
||||
reterr[alertRules[i].Name] = errMsg
|
||||
continue
|
||||
}
|
||||
|
||||
for j := range f.IdentList {
|
||||
alertRules[i].RuleConfig = re.ReplaceAllString(alertRules[i].RuleConfig, fmt.Sprintf(`ident=\"%s\"`, f.IdentList[j]))
|
||||
|
||||
newRule := &models.AlertRule{}
|
||||
if err := copier.Copy(newRule, alertRules[i]); err != nil {
|
||||
errMsg[f.IdentList[j]] = fmt.Sprintf("fail to clone rule, err: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
newRule.Id = 0
|
||||
newRule.Name = alertRules[i].Name + "_" + f.IdentList[j]
|
||||
newRule.CreateBy = user
|
||||
newRule.UpdateBy = user
|
||||
newRule.UpdateAt = now
|
||||
newRule.CreateAt = now
|
||||
newRule.RuleConfig = alertRules[i].RuleConfig
|
||||
|
||||
exist, err := models.AlertRuleExists(rt.Ctx, 0, newRule.GroupId, newRule.Name)
|
||||
if err != nil {
|
||||
errMsg[f.IdentList[j]] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
if exist {
|
||||
errMsg[f.IdentList[j]] = fmt.Sprintf("rule already exists, ruleName: %s", newRule.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
newRules = append(newRules, newRule)
|
||||
}
|
||||
|
||||
if len(errMsg) > 0 {
|
||||
reterr[alertRules[i].Name] = errMsg
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(reterr, models.InsertAlertRule(rt.Ctx, newRules))
|
||||
}
|
||||
|
||||
@@ -42,6 +42,11 @@ func (rt *Router) alertSubscribeGetsByGids(c *gin.Context) {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -93,6 +94,14 @@ func (rt *Router) boardGet(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(board, nil)
|
||||
}
|
||||
|
||||
// 根据 bids 参数,获取多个 board
|
||||
func (rt *Router) boardGetsByBids(c *gin.Context) {
|
||||
bids := str.IdsInt64(ginx.QueryStr(c, "bids", ""), ",")
|
||||
boards, err := models.BoardGetsByBids(rt.Ctx, bids)
|
||||
ginx.Dangerous(err)
|
||||
ginx.NewRender(c).Data(boards, err)
|
||||
}
|
||||
|
||||
func (rt *Router) boardPureGet(c *gin.Context) {
|
||||
board, err := models.BoardGetByID(rt.Ctx, ginx.UrlParamInt64(c, "bid"))
|
||||
ginx.Dangerous(err)
|
||||
@@ -269,6 +278,11 @@ func (rt *Router) boardGetsByGids(c *gin.Context) {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +308,7 @@ func (rt *Router) boardClone(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bo := rt.Board(ginx.UrlParamInt64(c, "bid"))
|
||||
|
||||
newBoard := bo.Clone(me.Username, bo.GroupId)
|
||||
newBoard := bo.Clone(me.Username, bo.GroupId, " Cloned")
|
||||
|
||||
ginx.Dangerous(newBoard.Add(rt.Ctx))
|
||||
|
||||
@@ -311,32 +325,34 @@ func (rt *Router) boardClone(c *gin.Context) {
|
||||
|
||||
type boardsForm struct {
|
||||
BoardIds []int64 `json:"board_ids"`
|
||||
Bgids []int64 `json:"bgids"`
|
||||
}
|
||||
|
||||
func (rt *Router) boardBatchClone(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
rt.bgrwCheck(c, bgid)
|
||||
|
||||
var f boardsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
for _, bgid := range f.Bgids {
|
||||
rt.bgrwCheck(c, bgid)
|
||||
}
|
||||
|
||||
reterr := make(map[string]string, len(f.BoardIds))
|
||||
lang := c.GetHeader("X-Language")
|
||||
for _, bid := range f.BoardIds {
|
||||
bo := rt.Board(bid)
|
||||
|
||||
newBoard := bo.Clone(me.Username, bgid)
|
||||
payload, err := models.BoardPayloadGet(rt.Ctx, bo.Id)
|
||||
if err != nil {
|
||||
reterr[newBoard.Name] = i18n.Sprintf(lang, err.Error())
|
||||
continue
|
||||
}
|
||||
for _, bgid := range f.Bgids {
|
||||
for _, bid := range f.BoardIds {
|
||||
bo := rt.Board(bid)
|
||||
newBoard := bo.Clone(me.Username, bgid, "")
|
||||
payload, err := models.BoardPayloadGet(rt.Ctx, bo.Id)
|
||||
if err != nil {
|
||||
reterr[fmt.Sprintf("%s-%d", newBoard.Name, bgid)] = i18n.Sprintf(lang, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if err = newBoard.AtomicAdd(rt.Ctx, payload); err != nil {
|
||||
reterr[newBoard.Name] = i18n.Sprintf(lang, err.Error())
|
||||
} else {
|
||||
reterr[newBoard.Name] = ""
|
||||
if err = newBoard.AtomicAdd(rt.Ctx, payload); err != nil {
|
||||
reterr[fmt.Sprintf("%s-%d", newBoard.Name, bgid)] = i18n.Sprintf(lang, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
92
center/router/router_builtin_componet.go
Normal file
92
center/router/router_builtin_componet.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const SYSTEM = "system"
|
||||
|
||||
func (rt *Router) builtinComponentsAdd(c *gin.Context) {
|
||||
var lst []models.BuiltinComponent
|
||||
ginx.BindJSON(c, &lst)
|
||||
|
||||
username := Username(c)
|
||||
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
if err := lst[i].Add(rt.Ctx, username); err != nil {
|
||||
reterr[lst[i].Ident] = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinComponentsGets(c *gin.Context) {
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
|
||||
bc, err := models.BuiltinComponentGets(rt.Ctx, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(bc, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinComponentsPut(c *gin.Context) {
|
||||
var req models.BuiltinComponent
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
bc, err := models.BuiltinComponentGet(rt.Ctx, "id = ?", req.ID)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if bc == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such builtin component")
|
||||
return
|
||||
}
|
||||
|
||||
if bc.CreatedBy == SYSTEM {
|
||||
req.Ident = bc.Ident
|
||||
}
|
||||
|
||||
username := Username(c)
|
||||
req.UpdatedBy = username
|
||||
|
||||
err = models.DB(rt.Ctx).Transaction(func(tx *gorm.DB) error {
|
||||
tCtx := &ctx.Context{
|
||||
DB: tx,
|
||||
}
|
||||
|
||||
txErr := models.BuiltinMetricBatchUpdateColumn(tCtx, "typ", bc.Ident, req.Ident, req.UpdatedBy)
|
||||
if txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
|
||||
txErr = bc.Update(tCtx, req)
|
||||
if txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
ginx.NewRender(c).Message(err)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinComponentsDel(c *gin.Context) {
|
||||
var req idsForm
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
req.Verify()
|
||||
|
||||
ginx.NewRender(c).Message(models.BuiltinComponentDels(rt.Ctx, req.Ids))
|
||||
}
|
||||
120
center/router/router_builtin_metric_filter.go
Normal file
120
center/router/router_builtin_metric_filter.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/prom"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) metricFilterGets(c *gin.Context) {
|
||||
lst, err := models.MetricFilterGets(rt.Ctx, "")
|
||||
ginx.Dangerous(err)
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
arr := make([]models.MetricFilter, 0)
|
||||
|
||||
for _, f := range lst {
|
||||
if me.Username == f.CreateBy {
|
||||
arr = append(arr, f)
|
||||
continue
|
||||
}
|
||||
|
||||
if HasPerm(gids, f.GroupsPerm, false) {
|
||||
arr = append(arr, f)
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(arr, err)
|
||||
}
|
||||
|
||||
func (rt *Router) metricFilterAdd(c *gin.Context) {
|
||||
var f models.MetricFilter
|
||||
ginx.BindJSON(c, &f)
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
f.CreateBy = me.Username
|
||||
f.UpdateBy = me.Username
|
||||
ginx.Dangerous(f.Add(rt.Ctx))
|
||||
ginx.NewRender(c).Data(f, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) metricFilterDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
for _, id := range f.Ids {
|
||||
old, err := models.MetricFilterGet(rt.Ctx, id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if me.Username != old.CreateBy {
|
||||
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !HasPerm(gids, old.GroupsPerm, true) {
|
||||
ginx.NewRender(c).Message("no permission")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.MetricFilterDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) metricFilterPut(c *gin.Context) {
|
||||
var f models.MetricFilter
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
old, err := models.MetricFilterGet(rt.Ctx, f.ID)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if me.Username != old.CreateBy {
|
||||
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !HasPerm(gids, old.GroupsPerm, true) {
|
||||
ginx.NewRender(c).Message("no permission")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f.UpdateBy = me.Username
|
||||
ginx.NewRender(c).Message(f.Update(rt.Ctx))
|
||||
}
|
||||
|
||||
type metricPromqlReq struct {
|
||||
LabelFilter string `json:"label_filter"`
|
||||
Promql string `json:"promql"`
|
||||
}
|
||||
|
||||
func (rt *Router) getMetricPromql(c *gin.Context) {
|
||||
var req metricPromqlReq
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
promql := prom.AddLabelToPromQL(req.LabelFilter, req.Promql)
|
||||
ginx.NewRender(c).Data(promql, nil)
|
||||
}
|
||||
|
||||
func HasPerm(gids []int64, gps []models.GroupPerm, checkWrite bool) bool {
|
||||
gmap := make(map[int64]struct{})
|
||||
for _, gp := range gps {
|
||||
if checkWrite && !gp.Write {
|
||||
continue
|
||||
}
|
||||
gmap[gp.Gid] = struct{}{}
|
||||
}
|
||||
|
||||
for _, gid := range gids {
|
||||
if _, ok := gmap[gid]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
116
center/router/router_builtin_metrics.go
Normal file
116
center/router/router_builtin_metrics.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
)
|
||||
|
||||
// single or import
|
||||
func (rt *Router) builtinMetricsAdd(c *gin.Context) {
|
||||
var lst []models.BuiltinMetric
|
||||
ginx.BindJSON(c, &lst)
|
||||
username := Username(c)
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
lang := c.GetHeader("X-Language")
|
||||
if lang == "" {
|
||||
lang = "zh_CN"
|
||||
}
|
||||
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
lst[i].Lang = lang
|
||||
lst[i].UUID = time.Now().UnixNano()
|
||||
if err := lst[i].Add(rt.Ctx, username); err != nil {
|
||||
reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsGets(c *gin.Context) {
|
||||
collector := ginx.QueryStr(c, "collector", "")
|
||||
typ := ginx.QueryStr(c, "typ", "")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
lang := c.GetHeader("X-Language")
|
||||
unit := ginx.QueryStr(c, "unit", "")
|
||||
if lang == "" {
|
||||
lang = "zh_CN"
|
||||
}
|
||||
|
||||
bm, err := models.BuiltinMetricGets(rt.Ctx, lang, collector, typ, query, unit, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
total, err := models.BuiltinMetricCount(rt.Ctx, lang, collector, typ, query, unit)
|
||||
ginx.Dangerous(err)
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"list": bm,
|
||||
"total": total,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsPut(c *gin.Context) {
|
||||
var req models.BuiltinMetric
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
bm, err := models.BuiltinMetricGet(rt.Ctx, "id = ?", req.ID)
|
||||
ginx.Dangerous(err)
|
||||
if bm == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such builtin metric")
|
||||
return
|
||||
}
|
||||
username := Username(c)
|
||||
|
||||
req.UpdatedBy = username
|
||||
ginx.NewRender(c).Message(bm.Update(rt.Ctx, req))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsDel(c *gin.Context) {
|
||||
var req idsForm
|
||||
ginx.BindJSON(c, &req)
|
||||
req.Verify()
|
||||
|
||||
ginx.NewRender(c).Message(models.BuiltinMetricDels(rt.Ctx, req.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsDefaultTypes(c *gin.Context) {
|
||||
lst := []string{
|
||||
"Linux",
|
||||
"cAdvisor",
|
||||
"Ping",
|
||||
"MySQL",
|
||||
"Redis",
|
||||
"Kafka",
|
||||
"Elasticsearch",
|
||||
"PostgreSQL",
|
||||
"MongoDB",
|
||||
"Memcached",
|
||||
}
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsTypes(c *gin.Context) {
|
||||
collector := ginx.QueryStr(c, "collector", "")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
lang := c.GetHeader("X-Language")
|
||||
|
||||
ginx.NewRender(c).Data(models.BuiltinMetricTypes(rt.Ctx, lang, collector, query))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsCollectors(c *gin.Context) {
|
||||
typ := ginx.QueryStr(c, "typ", "")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
lang := c.GetHeader("X-Language")
|
||||
|
||||
ginx.NewRender(c).Data(models.BuiltinMetricCollectors(rt.Ctx, lang, typ, query))
|
||||
}
|
||||
286
center/router/router_builtin_payload.go
Normal file
286
center/router/router_builtin_payload.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
)
|
||||
|
||||
type Board struct {
|
||||
Name string `json:"name"`
|
||||
Tags string `json:"tags"`
|
||||
Configs interface{} `json:"configs"`
|
||||
UUID int64 `json:"uuid"`
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadsAdd(c *gin.Context) {
|
||||
var lst []models.BuiltinPayload
|
||||
ginx.BindJSON(c, &lst)
|
||||
|
||||
username := Username(c)
|
||||
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
if lst[i].Type == "alert" {
|
||||
if strings.HasPrefix(strings.TrimSpace(lst[i].Content), "[") {
|
||||
// 处理多个告警规则模板的情况
|
||||
alertRules := []models.AlertRule{}
|
||||
if err := json.Unmarshal([]byte(lst[i].Content), &alertRules); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
}
|
||||
|
||||
for _, rule := range alertRules {
|
||||
if rule.UUID == 0 {
|
||||
rule.UUID = time.Now().UnixMicro()
|
||||
}
|
||||
|
||||
contentBytes, err := json.Marshal(rule)
|
||||
if err != nil {
|
||||
reterr[rule.Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
bp := models.BuiltinPayload{
|
||||
Type: lst[i].Type,
|
||||
ComponentID: lst[i].ComponentID,
|
||||
Cate: lst[i].Cate,
|
||||
Name: rule.Name,
|
||||
Tags: rule.AppendTags,
|
||||
UUID: rule.UUID,
|
||||
Content: string(contentBytes),
|
||||
CreatedBy: username,
|
||||
UpdatedBy: username,
|
||||
}
|
||||
|
||||
if err := bp.Add(rt.Ctx, username); err != nil {
|
||||
reterr[bp.Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
alertRule := models.AlertRule{}
|
||||
if err := json.Unmarshal([]byte(lst[i].Content), &alertRule); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
if alertRule.UUID == 0 {
|
||||
alertRule.UUID = time.Now().UnixMicro()
|
||||
}
|
||||
|
||||
contentBytes, err := json.Marshal(alertRule)
|
||||
if err != nil {
|
||||
reterr[alertRule.Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
bp := models.BuiltinPayload{
|
||||
Type: lst[i].Type,
|
||||
ComponentID: lst[i].ComponentID,
|
||||
Cate: lst[i].Cate,
|
||||
Name: alertRule.Name,
|
||||
Tags: alertRule.AppendTags,
|
||||
UUID: alertRule.UUID,
|
||||
Content: string(contentBytes),
|
||||
CreatedBy: username,
|
||||
UpdatedBy: username,
|
||||
}
|
||||
|
||||
if err := bp.Add(rt.Ctx, username); err != nil {
|
||||
reterr[bp.Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
} else if lst[i].Type == "dashboard" {
|
||||
if strings.HasPrefix(strings.TrimSpace(lst[i].Content), "[") {
|
||||
// 处理多个告警规则模板的情况
|
||||
dashboards := []Board{}
|
||||
if err := json.Unmarshal([]byte(lst[i].Content), &dashboards); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
}
|
||||
|
||||
for _, dashboard := range dashboards {
|
||||
if dashboard.UUID == 0 {
|
||||
dashboard.UUID = time.Now().UnixMicro()
|
||||
}
|
||||
|
||||
contentBytes, err := json.Marshal(dashboard)
|
||||
if err != nil {
|
||||
reterr[dashboard.Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
bp := models.BuiltinPayload{
|
||||
Type: lst[i].Type,
|
||||
ComponentID: lst[i].ComponentID,
|
||||
Cate: lst[i].Cate,
|
||||
Name: dashboard.Name,
|
||||
Tags: dashboard.Tags,
|
||||
UUID: dashboard.UUID,
|
||||
Content: string(contentBytes),
|
||||
CreatedBy: username,
|
||||
UpdatedBy: username,
|
||||
}
|
||||
|
||||
if err := bp.Add(rt.Ctx, username); err != nil {
|
||||
reterr[bp.Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
dashboard := Board{}
|
||||
if err := json.Unmarshal([]byte(lst[i].Content), &dashboard); err != nil {
|
||||
reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if dashboard.UUID == 0 {
|
||||
dashboard.UUID = time.Now().UnixMicro()
|
||||
}
|
||||
|
||||
contentBytes, err := json.Marshal(dashboard)
|
||||
if err != nil {
|
||||
reterr[dashboard.Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
bp := models.BuiltinPayload{
|
||||
Type: lst[i].Type,
|
||||
ComponentID: lst[i].ComponentID,
|
||||
Cate: lst[i].Cate,
|
||||
Name: dashboard.Name,
|
||||
Tags: dashboard.Tags,
|
||||
UUID: dashboard.UUID,
|
||||
Content: string(contentBytes),
|
||||
CreatedBy: username,
|
||||
UpdatedBy: username,
|
||||
}
|
||||
|
||||
if err := bp.Add(rt.Ctx, username); err != nil {
|
||||
reterr[bp.Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
} else {
|
||||
if lst[i].Type == "collect" {
|
||||
c := make(map[string]interface{})
|
||||
if _, err := toml.Decode(lst[i].Content, &c); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := lst[i].Add(rt.Ctx, username); err != nil {
|
||||
reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadsGets(c *gin.Context) {
|
||||
typ := ginx.QueryStr(c, "type", "")
|
||||
ComponentID := ginx.QueryInt64(c, "component_id", 0)
|
||||
|
||||
cate := ginx.QueryStr(c, "cate", "")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
|
||||
lst, err := models.BuiltinPayloadGets(rt.Ctx, uint64(ComponentID), typ, cate, query)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadcatesGet(c *gin.Context) {
|
||||
typ := ginx.QueryStr(c, "type", "")
|
||||
ComponentID := ginx.QueryInt64(c, "component_id", 0)
|
||||
|
||||
cates, err := models.BuiltinPayloadCates(rt.Ctx, typ, uint64(ComponentID))
|
||||
ginx.NewRender(c).Data(cates, err)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadGet(c *gin.Context) {
|
||||
id := ginx.UrlParamInt64(c, "id")
|
||||
|
||||
bp, err := models.BuiltinPayloadGet(rt.Ctx, "id = ?", id)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if bp == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "builtin payload not found")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(bp, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadsPut(c *gin.Context) {
|
||||
var req models.BuiltinPayload
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
bp, err := models.BuiltinPayloadGet(rt.Ctx, "id = ?", req.ID)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if bp == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such builtin payload")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type == "alert" {
|
||||
alertRule := models.AlertRule{}
|
||||
if err := json.Unmarshal([]byte(req.Content), &alertRule); err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
req.Name = alertRule.Name
|
||||
req.Tags = alertRule.AppendTags
|
||||
} else if req.Type == "dashboard" {
|
||||
dashboard := Board{}
|
||||
if err := json.Unmarshal([]byte(req.Content), &dashboard); err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
req.Name = dashboard.Name
|
||||
req.Tags = dashboard.Tags
|
||||
} else if req.Type == "collect" {
|
||||
c := make(map[string]interface{})
|
||||
if _, err := toml.Decode(req.Content, &c); err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
username := Username(c)
|
||||
req.UpdatedBy = username
|
||||
|
||||
ginx.NewRender(c).Message(bp.Update(rt.Ctx, req))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadsDel(c *gin.Context) {
|
||||
var req idsForm
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
req.Verify()
|
||||
|
||||
ginx.NewRender(c).Message(models.BuiltinPayloadDels(rt.Ctx, req.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadsGetByUUIDOrID(c *gin.Context) {
|
||||
uuid := ginx.QueryInt64(c, "uuid", 0)
|
||||
// 优先以 uuid 为准
|
||||
if uuid != 0 {
|
||||
ginx.NewRender(c).Data(models.BuiltinPayloadGet(rt.Ctx, "uuid = ?", uuid))
|
||||
return
|
||||
}
|
||||
|
||||
id := ginx.QueryInt64(c, "id", 0)
|
||||
ginx.NewRender(c).Data(models.BuiltinPayloadGet(rt.Ctx, "id = ?", id))
|
||||
}
|
||||
@@ -140,3 +140,12 @@ func (rt *Router) busiGroupGet(c *gin.Context) {
|
||||
ginx.Dangerous(bg.FillUserGroups(rt.Ctx))
|
||||
ginx.NewRender(c).Data(bg, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) busiGroupsGetTags(c *gin.Context) {
|
||||
bgids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
targetIdents, err := models.TargetIndentsGetByBgids(rt.Ctx, bgids)
|
||||
ginx.Dangerous(err)
|
||||
tags, err := models.TargetGetTags(rt.Ctx, targetIdents, true, "busigroup")
|
||||
ginx.Dangerous(err)
|
||||
ginx.NewRender(c).Data(tags, nil)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
const EMBEDDEDDASHBOARD = "embedded-dashboards"
|
||||
|
||||
func (rt *Router) configsGet(c *gin.Context) {
|
||||
prefix := ginx.QueryStr(c, "prefix", "")
|
||||
limit := ginx.QueryInt(c, "limit", 10)
|
||||
@@ -21,6 +24,11 @@ func (rt *Router) configGet(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(configs, err)
|
||||
}
|
||||
|
||||
func (rt *Router) configGetAll(c *gin.Context) {
|
||||
config, err := models.ConfigsGetAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(config, err)
|
||||
}
|
||||
|
||||
func (rt *Router) configGetByKey(c *gin.Context) {
|
||||
config, err := models.ConfigsGet(rt.Ctx, ginx.QueryStr(c, "key"))
|
||||
ginx.NewRender(c).Data(config, err)
|
||||
@@ -33,6 +41,18 @@ func (rt *Router) configPutByKey(c *gin.Context) {
|
||||
ginx.NewRender(c).Message(models.ConfigsSetWithUname(rt.Ctx, f.Ckey, f.Cval, username))
|
||||
}
|
||||
|
||||
func (rt *Router) embeddedDashboardsGet(c *gin.Context) {
|
||||
config, err := models.ConfigsGet(rt.Ctx, EMBEDDEDDASHBOARD)
|
||||
ginx.NewRender(c).Data(config, err)
|
||||
}
|
||||
|
||||
func (rt *Router) embeddedDashboardsPut(c *gin.Context) {
|
||||
var f models.Configs
|
||||
ginx.BindJSON(c, &f)
|
||||
username := c.MustGet("username").(string)
|
||||
ginx.NewRender(c).Message(models.ConfigsSetWithUname(rt.Ctx, EMBEDDEDDASHBOARD, f.Cval, username))
|
||||
}
|
||||
|
||||
func (rt *Router) configsDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
@@ -92,10 +92,12 @@ func (rt *Router) datasourceUpsert(c *gin.Context) {
|
||||
var err error
|
||||
var count int64
|
||||
|
||||
err = DatasourceCheck(req)
|
||||
if err != nil {
|
||||
Dangerous(c, err)
|
||||
return
|
||||
if !req.ForceSave {
|
||||
err = DatasourceCheck(req)
|
||||
if err != nil {
|
||||
Dangerous(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
@@ -120,12 +122,14 @@ func (rt *Router) datasourceUpsert(c *gin.Context) {
|
||||
}
|
||||
|
||||
func DatasourceCheck(ds models.Datasource) error {
|
||||
if ds.HTTPJson.Url == "" {
|
||||
return fmt.Errorf("url is empty")
|
||||
}
|
||||
if ds.PluginType != models.ELASTICSEARCH {
|
||||
if ds.HTTPJson.Url == "" {
|
||||
return fmt.Errorf("url is empty")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(ds.HTTPJson.Url, "http") {
|
||||
return fmt.Errorf("url must start with http or https")
|
||||
if !strings.HasPrefix(ds.HTTPJson.Url, "http") {
|
||||
return fmt.Errorf("url must start with http or https")
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
@@ -136,11 +140,11 @@ func DatasourceCheck(ds models.Datasource) error {
|
||||
},
|
||||
}
|
||||
|
||||
fullURL := ds.HTTPJson.Url
|
||||
req, err := http.NewRequest("GET", fullURL, nil)
|
||||
var fullURL string
|
||||
req, err := ds.HTTPJson.NewReq(&fullURL)
|
||||
if err != nil {
|
||||
logger.Errorf("Error creating request: %v", err)
|
||||
return fmt.Errorf("request url:%s failed", fullURL)
|
||||
return fmt.Errorf("request urls:%v failed", ds.HTTPJson.GetUrls())
|
||||
}
|
||||
|
||||
if ds.PluginType == models.PROMETHEUS {
|
||||
@@ -247,3 +251,37 @@ func (rt *Router) getDatasourceIds(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Data(datasourceIds, err)
|
||||
}
|
||||
|
||||
type datasourceQueryForm struct {
|
||||
Cate string `json:"datasource_cate"`
|
||||
DatasourceQueries []models.DatasourceQuery `json:"datasource_queries"`
|
||||
}
|
||||
|
||||
type datasourceQueryResp struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceQuery(c *gin.Context) {
|
||||
var dsf datasourceQueryForm
|
||||
ginx.BindJSON(c, &dsf)
|
||||
datasources, err := models.GetDatasourcesGetsByTypes(rt.Ctx, []string{dsf.Cate})
|
||||
ginx.Dangerous(err)
|
||||
|
||||
nameToID := make(map[string]int64)
|
||||
IDToName := make(map[int64]string)
|
||||
for _, ds := range datasources {
|
||||
nameToID[ds.Name] = ds.Id
|
||||
IDToName[ds.Id] = ds.Name
|
||||
}
|
||||
|
||||
ids := models.GetDatasourceIDsByDatasourceQueries(dsf.DatasourceQueries, IDToName, nameToID)
|
||||
var req []datasourceQueryResp
|
||||
for _, id := range ids {
|
||||
req = append(req, datasourceQueryResp{
|
||||
ID: id,
|
||||
Name: IDToName[id],
|
||||
})
|
||||
}
|
||||
ginx.NewRender(c).Data(req, err)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ibex"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
@@ -48,6 +45,10 @@ func (rt *Router) statistic(c *gin.Context) {
|
||||
statistics, err = models.ConfigsUserVariableStatistics(rt.Ctx)
|
||||
ginx.NewRender(c).Data(statistics, err)
|
||||
return
|
||||
case "cval":
|
||||
statistics, err = models.ConfigCvalStatistics(rt.Ctx)
|
||||
ginx.NewRender(c).Data(statistics, err)
|
||||
return
|
||||
default:
|
||||
ginx.Bomb(http.StatusBadRequest, "invalid name")
|
||||
}
|
||||
@@ -68,6 +69,23 @@ func queryDatasourceIds(c *gin.Context) []int64 {
|
||||
return ids
|
||||
}
|
||||
|
||||
func queryStrListField(c *gin.Context, fieldName string, sep ...string) []string {
|
||||
str := ginx.QueryStr(c, fieldName, "")
|
||||
if str == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
lst := []string{str}
|
||||
for _, s := range sep {
|
||||
var newLst []string
|
||||
for _, str := range lst {
|
||||
newLst = append(newLst, strings.Split(str, s)...)
|
||||
}
|
||||
lst = newLst
|
||||
}
|
||||
return lst
|
||||
}
|
||||
|
||||
type idsForm struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
IsSyncToFlashDuty bool `json:"is_sync_to_flashduty"`
|
||||
@@ -135,31 +153,6 @@ type TaskCreateReply struct {
|
||||
Dat int64 `json:"dat"` // task.id
|
||||
}
|
||||
|
||||
// return task.id, error
|
||||
func TaskCreate(v interface{}, ibexc aconf.Ibex) (int64, error) {
|
||||
var res TaskCreateReply
|
||||
err := ibex.New(
|
||||
ibexc.Address,
|
||||
ibexc.BasicAuthUser,
|
||||
ibexc.BasicAuthPass,
|
||||
ibexc.Timeout,
|
||||
).
|
||||
Path("/ibex/v1/tasks").
|
||||
In(v).
|
||||
Out(&res).
|
||||
POST()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if res.Err != "" {
|
||||
return 0, fmt.Errorf("response.err: %v", res.Err)
|
||||
}
|
||||
|
||||
return res.Dat, nil
|
||||
}
|
||||
|
||||
func Username(c *gin.Context) string {
|
||||
username := c.GetString(gin.AuthUserKey)
|
||||
if username == "" {
|
||||
|
||||
@@ -3,19 +3,35 @@ package router
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/center/metas"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/idents"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type HeartbeatHookFunc func(ident string) map[string]interface{}
|
||||
|
||||
func (rt *Router) heartbeat(c *gin.Context) {
|
||||
req, err := HandleHeartbeat(c, rt.Ctx, rt.Alert.Heartbeat.EngineName, rt.MetaSet, rt.IdentSet, rt.TargetCache)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
m := rt.HeartbeatHook(req.Hostname)
|
||||
ginx.NewRender(c).Data(m, err)
|
||||
}
|
||||
|
||||
func HandleHeartbeat(c *gin.Context, ctx *ctx.Context, engineName string, metaSet *metas.Set, identSet *idents.Set, targetCache *memsto.TargetCacheType) (models.HostMeta, error) {
|
||||
var bs []byte
|
||||
var err error
|
||||
var r *gzip.Reader
|
||||
@@ -24,7 +40,7 @@ func (rt *Router) heartbeat(c *gin.Context) {
|
||||
r, err = gzip.NewReader(c.Request.Body)
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
return req, err
|
||||
}
|
||||
defer r.Close()
|
||||
bs, err = ioutil.ReadAll(r)
|
||||
@@ -32,11 +48,19 @@ func (rt *Router) heartbeat(c *gin.Context) {
|
||||
} else {
|
||||
defer c.Request.Body.Close()
|
||||
bs, err = ioutil.ReadAll(c.Request.Body)
|
||||
ginx.Dangerous(err)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
}
|
||||
|
||||
err = json.Unmarshal(bs, &req)
|
||||
ginx.Dangerous(err)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
if req.Hostname == "" {
|
||||
return req, errors.New("hostname is required")
|
||||
}
|
||||
|
||||
// maybe from pushgw
|
||||
if req.Offset == 0 {
|
||||
@@ -48,51 +72,132 @@ func (rt *Router) heartbeat(c *gin.Context) {
|
||||
}
|
||||
|
||||
if req.EngineName == "" {
|
||||
req.EngineName = rt.Alert.Heartbeat.EngineName
|
||||
req.EngineName = engineName
|
||||
}
|
||||
|
||||
rt.MetaSet.Set(req.Hostname, req)
|
||||
metaSet.Set(req.Hostname, req)
|
||||
var items = make(map[string]struct{})
|
||||
items[req.Hostname] = struct{}{}
|
||||
rt.IdentSet.MSet(items)
|
||||
identSet.MSet(items)
|
||||
|
||||
if target, has := rt.TargetCache.Get(req.Hostname); has && target != nil {
|
||||
gid := ginx.QueryInt64(c, "gid", 0)
|
||||
if target, has := targetCache.Get(req.Hostname); has && target != nil {
|
||||
gidsStr := ginx.QueryStr(c, "gid", "")
|
||||
overwriteGids := ginx.QueryBool(c, "overwrite_gids", false)
|
||||
hostIp := strings.TrimSpace(req.HostIp)
|
||||
gids := strings.Split(gidsStr, ",")
|
||||
|
||||
filed := make(map[string]interface{})
|
||||
if gid != 0 && gid != target.GroupId {
|
||||
filed["group_id"] = gid
|
||||
if overwriteGids {
|
||||
groupIds := make([]int64, 0)
|
||||
for i := range gids {
|
||||
if gids[i] == "" {
|
||||
continue
|
||||
}
|
||||
groupId, err := strconv.ParseInt(gids[i], 10, 64)
|
||||
if err != nil {
|
||||
logger.Warningf("update target:%s group ids failed, err: %v", req.Hostname, err)
|
||||
continue
|
||||
}
|
||||
groupIds = append(groupIds, groupId)
|
||||
}
|
||||
|
||||
err := models.TargetOverrideBgids(ctx, []string{target.Ident}, groupIds, nil)
|
||||
if err != nil {
|
||||
logger.Warningf("update target:%s group ids failed, err: %v", target.Ident, err)
|
||||
}
|
||||
} else if gidsStr != "" {
|
||||
for i := range gids {
|
||||
groupId, err := strconv.ParseInt(gids[i], 10, 64)
|
||||
if err != nil {
|
||||
logger.Warningf("update target:%s group ids failed, err: %v", req.Hostname, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !target.MatchGroupId(groupId) {
|
||||
err := models.TargetBindBgids(ctx, []string{target.Ident}, []int64{groupId}, nil)
|
||||
if err != nil {
|
||||
logger.Warningf("update target:%s group ids failed, err: %v", target.Ident, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newTarget := models.Target{}
|
||||
targetNeedUpdate := false
|
||||
if hostIp != "" && hostIp != target.HostIp {
|
||||
filed["host_ip"] = hostIp
|
||||
newTarget.HostIp = hostIp
|
||||
targetNeedUpdate = true
|
||||
}
|
||||
|
||||
if len(req.GlobalLabels) > 0 {
|
||||
hostTagsMap := target.GetHostTagsMap()
|
||||
hostTagNeedUpdate := false
|
||||
if len(hostTagsMap) != len(req.GlobalLabels) {
|
||||
hostTagNeedUpdate = true
|
||||
} else {
|
||||
for k, v := range req.GlobalLabels {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if tagv, ok := hostTagsMap[k]; !ok || tagv != v {
|
||||
hostTagNeedUpdate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hostTagNeedUpdate {
|
||||
lst := []string{}
|
||||
for k, v := range req.GlobalLabels {
|
||||
lst = append(lst, k+"="+v)
|
||||
}
|
||||
sort.Strings(lst)
|
||||
labels := strings.Join(lst, " ")
|
||||
if target.Tags != labels {
|
||||
filed["tags"] = labels
|
||||
newTarget.HostTags = lst
|
||||
targetNeedUpdate = true
|
||||
}
|
||||
|
||||
userTagsMap := target.GetTagsMap()
|
||||
userTagNeedUpdate := false
|
||||
userTags := []string{}
|
||||
for k, v := range userTagsMap {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := req.GlobalLabels[k]; !ok {
|
||||
userTags = append(userTags, k+"="+v)
|
||||
} else { // 该key在hostTags中已经存在
|
||||
userTagNeedUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
if req.EngineName != "" && req.EngineName != target.EngineName {
|
||||
filed["engine_name"] = req.EngineName
|
||||
if userTagNeedUpdate {
|
||||
newTarget.Tags = strings.Join(userTags, " ") + " "
|
||||
targetNeedUpdate = true
|
||||
}
|
||||
|
||||
if len(filed) > 0 {
|
||||
err := target.UpdateFieldsMap(rt.Ctx, filed)
|
||||
if req.EngineName != "" && req.EngineName != target.EngineName {
|
||||
newTarget.EngineName = req.EngineName
|
||||
targetNeedUpdate = true
|
||||
}
|
||||
|
||||
if req.AgentVersion != "" && req.AgentVersion != target.AgentVersion {
|
||||
newTarget.AgentVersion = req.AgentVersion
|
||||
targetNeedUpdate = true
|
||||
}
|
||||
|
||||
if req.OS != "" && req.OS != target.OS {
|
||||
newTarget.OS = req.OS
|
||||
targetNeedUpdate = true
|
||||
}
|
||||
|
||||
if targetNeedUpdate {
|
||||
err := models.DB(ctx).Model(&target).Updates(newTarget).Error
|
||||
if err != nil {
|
||||
logger.Errorf("update target fields failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
logger.Debugf("heartbeat field:%+v target: %v", filed, *target)
|
||||
logger.Debugf("heartbeat field:%+v target: %v", newTarget, *target)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(err)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@@ -55,12 +55,12 @@ func (rt *Router) loginPost(c *gin.Context) {
|
||||
var err error
|
||||
lc := rt.Sso.LDAP.Copy()
|
||||
if lc.Enable {
|
||||
user, err = ldapx.LdapLogin(rt.Ctx, f.Username, authPassWord, lc.DefaultRoles, lc)
|
||||
user, err = ldapx.LdapLogin(rt.Ctx, f.Username, authPassWord, lc.DefaultRoles, lc.DefaultTeams, lc)
|
||||
if err != nil {
|
||||
logger.Debugf("ldap login failed: %v username: %s", err, f.Username)
|
||||
var errLoginInN9e error
|
||||
// to use n9e as the minimum guarantee for login
|
||||
if user, errLoginInN9e = models.PassLogin(rt.Ctx, f.Username, authPassWord); errLoginInN9e != nil {
|
||||
if user, errLoginInN9e = models.PassLogin(rt.Ctx, rt.Redis, f.Username, authPassWord); errLoginInN9e != nil {
|
||||
ginx.NewRender(c).Message("ldap login failed: %v; n9e login failed: %v", err, errLoginInN9e)
|
||||
return
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func (rt *Router) loginPost(c *gin.Context) {
|
||||
user.RolesLst = strings.Fields(user.Roles)
|
||||
}
|
||||
} else {
|
||||
user, err = models.PassLogin(rt.Ctx, f.Username, authPassWord)
|
||||
user, err = models.PassLogin(rt.Ctx, rt.Redis, f.Username, authPassWord)
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (rt *Router) loginPost(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) logoutPost(c *gin.Context) {
|
||||
logger.Infof("username:%s login from:%s", c.GetString("username"), c.ClientIP())
|
||||
logger.Infof("username:%s logout from:%s", c.GetString("username"), c.ClientIP())
|
||||
metadata, err := rt.extractTokenMetadata(c.Request)
|
||||
if err != nil {
|
||||
ginx.NewRender(c, http.StatusBadRequest).Message("failed to parse jwt token")
|
||||
@@ -262,6 +262,15 @@ func (rt *Router) loginCallback(c *gin.Context) {
|
||||
user.FullSsoFields("oidc", ret.Username, ret.Nickname, ret.Phone, ret.Email, rt.Sso.OIDC.DefaultRoles)
|
||||
// create user from oidc
|
||||
ginx.Dangerous(user.Add(rt.Ctx))
|
||||
|
||||
if len(rt.Sso.OIDC.DefaultTeams) > 0 {
|
||||
for _, gid := range rt.Sso.OIDC.DefaultTeams {
|
||||
err = models.UserGroupMemberAdd(rt.Ctx, gid, user.Id)
|
||||
if err != nil {
|
||||
logger.Errorf("user:%v UserGroupMemberAdd: %s", user, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set user login state
|
||||
|
||||
@@ -33,6 +33,11 @@ func (rt *Router) alertMuteGetsByGids(c *gin.Context) {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +50,8 @@ func (rt *Router) alertMuteGets(c *gin.Context) {
|
||||
prods := strings.Fields(ginx.QueryStr(c, "prods", ""))
|
||||
bgid := ginx.QueryInt64(c, "bgid", -1)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
lst, err := models.AlertMuteGets(rt.Ctx, prods, bgid, query)
|
||||
disabled := ginx.QueryInt(c, "disabled", -1)
|
||||
lst, err := models.AlertMuteGets(rt.Ctx, prods, bgid, disabled, query)
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
@@ -90,7 +96,8 @@ func (rt *Router) alertMuteAddByService(c *gin.Context) {
|
||||
var f models.AlertMute
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
ginx.NewRender(c).Message(f.Add(rt.Ctx))
|
||||
err := f.Add(rt.Ctx)
|
||||
ginx.NewRender(c).Data(f.Id, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertMuteDel(c *gin.Context) {
|
||||
@@ -101,7 +108,7 @@ func (rt *Router) alertMuteDel(c *gin.Context) {
|
||||
ginx.NewRender(c).Message(models.AlertMuteDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
// alertMuteGetById returns the alert mute by ID
|
||||
// alertMuteGet returns the alert mute by ID
|
||||
func (rt *Router) alertMuteGet(c *gin.Context) {
|
||||
amid := ginx.UrlParamInt64(c, "amid")
|
||||
am, err := models.AlertMuteGetById(rt.Ctx, amid)
|
||||
|
||||
@@ -92,6 +92,10 @@ func (rt *Router) jwtAuth() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) Auth() gin.HandlerFunc {
|
||||
return rt.auth()
|
||||
}
|
||||
|
||||
func (rt *Router) auth() gin.HandlerFunc {
|
||||
if rt.HTTP.ProxyAuth.Enable {
|
||||
return rt.proxyAuth()
|
||||
@@ -120,6 +124,10 @@ func (rt *Router) jwtMock() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) User() gin.HandlerFunc {
|
||||
return rt.user()
|
||||
}
|
||||
|
||||
func (rt *Router) user() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userid := c.MustGet("userid").(int64)
|
||||
@@ -135,6 +143,8 @@ func (rt *Router) user() gin.HandlerFunc {
|
||||
|
||||
c.Set("user", user)
|
||||
c.Set("isadmin", user.IsAdmin())
|
||||
// Update user.LastActiveTime
|
||||
rt.UserCache.SetLastActiveTime(user.Id, time.Now().Unix())
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -174,6 +184,10 @@ func (rt *Router) bgro() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// bgrw 逐步要被干掉,不安全
|
||||
func (rt *Router) Bgrw() gin.HandlerFunc {
|
||||
return rt.bgrw()
|
||||
}
|
||||
|
||||
func (rt *Router) bgrw() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
@@ -233,6 +247,10 @@ func (rt *Router) bgroCheck(c *gin.Context, bgid int64) {
|
||||
c.Set("busi_group", bg)
|
||||
}
|
||||
|
||||
func (rt *Router) Perm(operation string) gin.HandlerFunc {
|
||||
return rt.perm(operation)
|
||||
}
|
||||
|
||||
func (rt *Router) perm(operation string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
212
center/router/router_notification_record.go
Normal file
212
center/router/router_notification_record.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type NotificationResponse struct {
|
||||
SubRules []SubRule `json:"sub_rules"`
|
||||
Notifies map[string][]Record `json:"notifies"`
|
||||
}
|
||||
|
||||
type SubRule struct {
|
||||
SubID int64 `json:"sub_id"`
|
||||
Notifies map[string][]Record `json:"notifies"`
|
||||
}
|
||||
|
||||
type Notify struct {
|
||||
Channel string `json:"channel"`
|
||||
Records []Record `json:"records"`
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Target string `json:"target"`
|
||||
Username string `json:"username"`
|
||||
Status int `json:"status"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
// notificationRecordAdd
|
||||
func (rt *Router) notificationRecordAdd(c *gin.Context) {
|
||||
var req []*models.NotificaitonRecord
|
||||
ginx.BindJSON(c, &req)
|
||||
err := models.DB(rt.Ctx).CreateInBatches(req, 100).Error
|
||||
var ids []int64
|
||||
if err == nil {
|
||||
ids = make([]int64, len(req))
|
||||
for i, noti := range req {
|
||||
ids[i] = noti.Id
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(ids, err)
|
||||
}
|
||||
|
||||
func (rt *Router) notificationRecordList(c *gin.Context) {
|
||||
eid := ginx.UrlParamInt64(c, "eid")
|
||||
lst, err := models.NotificaitonRecordsGetByEventId(rt.Ctx, eid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
response := buildNotificationResponse(rt.Ctx, lst)
|
||||
ginx.NewRender(c).Data(response, nil)
|
||||
}
|
||||
|
||||
func buildNotificationResponse(ctx *ctx.Context, nl []*models.NotificaitonRecord) NotificationResponse {
|
||||
response := NotificationResponse{
|
||||
SubRules: []SubRule{},
|
||||
Notifies: make(map[string][]Record),
|
||||
}
|
||||
|
||||
subRuleMap := make(map[int64]*SubRule)
|
||||
|
||||
// Collect all group IDs
|
||||
groupIdSet := make(map[int64]struct{})
|
||||
|
||||
// map[SubId]map[Channel]map[Target]index
|
||||
filter := make(map[int64]map[string]map[string]int)
|
||||
|
||||
for i, n := range nl {
|
||||
// 对相同的 channel-target 进行合并
|
||||
for _, gid := range n.GetGroupIds(ctx) {
|
||||
groupIdSet[gid] = struct{}{}
|
||||
}
|
||||
|
||||
if _, exists := filter[n.SubId]; !exists {
|
||||
filter[n.SubId] = make(map[string]map[string]int)
|
||||
}
|
||||
|
||||
if _, exists := filter[n.SubId][n.Channel]; !exists {
|
||||
filter[n.SubId][n.Channel] = make(map[string]int)
|
||||
}
|
||||
|
||||
idx, exists := filter[n.SubId][n.Channel][n.Target]
|
||||
if !exists {
|
||||
filter[n.SubId][n.Channel][n.Target] = i
|
||||
} else {
|
||||
if nl[idx].Status < n.Status {
|
||||
nl[idx].Status = n.Status
|
||||
}
|
||||
nl[idx].Details = nl[idx].Details + ", " + n.Details
|
||||
nl[i] = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fill usernames only once
|
||||
usernameByTarget := fillUserNames(ctx, groupIdSet)
|
||||
|
||||
for _, n := range nl {
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
m := usernameByTarget[n.Target]
|
||||
usernames := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
usernames = append(usernames, k)
|
||||
}
|
||||
|
||||
if !checkChannel(n.Channel) {
|
||||
// Hide sensitive information
|
||||
n.Target = replaceLastEightChars(n.Target)
|
||||
}
|
||||
record := Record{
|
||||
Target: n.Target,
|
||||
Status: n.Status,
|
||||
Detail: n.Details,
|
||||
}
|
||||
|
||||
record.Username = strings.Join(usernames, ",")
|
||||
|
||||
if n.SubId > 0 {
|
||||
// Handle SubRules
|
||||
subRule, ok := subRuleMap[n.SubId]
|
||||
if !ok {
|
||||
newSubRule := &SubRule{
|
||||
SubID: n.SubId,
|
||||
}
|
||||
newSubRule.Notifies = make(map[string][]Record)
|
||||
newSubRule.Notifies[n.Channel] = []Record{record}
|
||||
|
||||
subRuleMap[n.SubId] = newSubRule
|
||||
} else {
|
||||
if _, exists := subRule.Notifies[n.Channel]; !exists {
|
||||
|
||||
subRule.Notifies[n.Channel] = []Record{record}
|
||||
} else {
|
||||
subRule.Notifies[n.Channel] = append(subRule.Notifies[n.Channel], record)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if response.Notifies == nil {
|
||||
response.Notifies = make(map[string][]Record)
|
||||
}
|
||||
|
||||
if _, exists := response.Notifies[n.Channel]; !exists {
|
||||
response.Notifies[n.Channel] = []Record{record}
|
||||
} else {
|
||||
response.Notifies[n.Channel] = append(response.Notifies[n.Channel], record)
|
||||
}
|
||||
}
|
||||
|
||||
for _, subRule := range subRuleMap {
|
||||
response.SubRules = append(response.SubRules, *subRule)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// check channel is one of the following: tx-sms, tx-voice, ali-sms, ali-voice, email, script
|
||||
func checkChannel(channel string) bool {
|
||||
switch channel {
|
||||
case "tx-sms", "tx-voice", "ali-sms", "ali-voice", "email", "script":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func replaceLastEightChars(s string) string {
|
||||
if len(s) <= 8 {
|
||||
return strings.Repeat("*", len(s))
|
||||
}
|
||||
return s[:len(s)-8] + strings.Repeat("*", 8)
|
||||
}
|
||||
|
||||
func fillUserNames(ctx *ctx.Context, groupIdSet map[int64]struct{}) map[string]map[string]struct{} {
|
||||
userNameByTarget := make(map[string]map[string]struct{})
|
||||
|
||||
gids := make([]int64, 0, len(groupIdSet))
|
||||
for gid := range groupIdSet {
|
||||
gids = append(gids, gid)
|
||||
}
|
||||
|
||||
users, err := models.UsersGetByGroupIds(ctx, gids)
|
||||
if err != nil {
|
||||
logger.Errorf("UsersGetByGroupIds failed, err: %v", err)
|
||||
return userNameByTarget
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
logger.Warningf("user: %s", user.Username)
|
||||
for _, ch := range models.DefaultChannels {
|
||||
target, exist := user.ExtractToken(ch)
|
||||
if exist {
|
||||
if _, ok := userNameByTarget[target]; !ok {
|
||||
userNameByTarget[target] = make(map[string]struct{})
|
||||
}
|
||||
userNameByTarget[target][user.Username] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return userNameByTarget
|
||||
}
|
||||
@@ -90,7 +90,8 @@ func (rt *Router) notifyChannelPuts(c *gin.Context) {
|
||||
var notifyChannels []models.NotifyChannel
|
||||
ginx.BindJSON(c, ¬ifyChannels)
|
||||
|
||||
channels := []string{models.Dingtalk, models.Wecom, models.Feishu, models.Mm, models.Telegram, models.Email}
|
||||
channels := []string{models.Dingtalk, models.Wecom, models.Feishu, models.Mm, models.Telegram,
|
||||
models.Email, models.Lark, models.LarkCard}
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range notifyChannels {
|
||||
@@ -126,7 +127,8 @@ func (rt *Router) notifyContactPuts(c *gin.Context) {
|
||||
var notifyContacts []models.NotifyContact
|
||||
ginx.BindJSON(c, ¬ifyContacts)
|
||||
|
||||
keys := []string{models.DingtalkKey, models.WecomKey, models.FeishuKey, models.MmKey, models.TelegramKey}
|
||||
keys := []string{models.DingtalkKey, models.WecomKey, models.FeishuKey, models.MmKey,
|
||||
models.TelegramKey, models.LarkKey}
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range notifyContacts {
|
||||
@@ -169,10 +171,6 @@ func (rt *Router) notifyConfigPut(c *gin.Context) {
|
||||
var smtp aconf.SMTPConfig
|
||||
err := toml.Unmarshal([]byte(text), &smtp)
|
||||
ginx.Dangerous(err)
|
||||
case models.IBEX:
|
||||
var ibex aconf.Ibex
|
||||
err := toml.Unmarshal([]byte(f.Cval), &ibex)
|
||||
ginx.Dangerous(err)
|
||||
default:
|
||||
ginx.Bomb(200, "key %s can not modify", f.Ckey)
|
||||
}
|
||||
@@ -184,7 +182,7 @@ func (rt *Router) notifyConfigPut(c *gin.Context) {
|
||||
|
||||
smtp, errSmtp := SmtpValidate(text)
|
||||
ginx.Dangerous(errSmtp)
|
||||
go sender.RestartEmailSender(smtp)
|
||||
go sender.RestartEmailSender(rt.Ctx, smtp)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
|
||||
@@ -68,7 +68,7 @@ func (rt *Router) notifyTplUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
// get the count of the same channel and name but different id
|
||||
count, err := models.Count(models.DB(rt.Ctx).Model(&models.NotifyTpl{}).Where("channel = ? or name = ? and id <> ?", f.Channel, f.Name, f.Id))
|
||||
count, err := models.Count(models.DB(rt.Ctx).Model(&models.NotifyTpl{}).Where("(channel = ? or name = ?) and id <> ?", f.Channel, f.Name, f.Id))
|
||||
ginx.Dangerous(err)
|
||||
if count != 0 {
|
||||
ginx.Bomb(200, "Refuse to create duplicate channel or name")
|
||||
@@ -138,7 +138,7 @@ func (rt *Router) notifyTplPreview(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
arr := strings.Split(pair, "=")
|
||||
arr := strings.SplitN(pair, "=", 2)
|
||||
if len(arr) != 2 {
|
||||
continue
|
||||
}
|
||||
@@ -161,7 +161,11 @@ func (rt *Router) notifyTplPreview(c *gin.Context) {
|
||||
func (rt *Router) notifyTplAdd(c *gin.Context) {
|
||||
var f models.NotifyTpl
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Channel = strings.TrimSpace(f.Channel)
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
f.CreateBy = user.Username
|
||||
|
||||
f.Channel = strings.TrimSpace(f.Channel)
|
||||
ginx.Dangerous(templateValidate(f))
|
||||
|
||||
count, err := models.Count(models.DB(rt.Ctx).Model(&models.NotifyTpl{}).Where("channel = ? or name = ?", f.Channel, f.Name))
|
||||
@@ -169,6 +173,8 @@ func (rt *Router) notifyTplAdd(c *gin.Context) {
|
||||
if count != 0 {
|
||||
ginx.Bomb(200, "Refuse to create duplicate channel(unique)")
|
||||
}
|
||||
|
||||
f.CreateAt = time.Now().Unix()
|
||||
ginx.NewRender(c).Message(f.Create(rt.Ctx))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -112,9 +111,9 @@ func (rt *Router) dsProxy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
target, err := url.Parse(ds.HTTPJson.Url)
|
||||
target, err := ds.HTTPJson.ParseUrl()
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "invalid url: %s", ds.HTTPJson.Url)
|
||||
c.String(http.StatusInternalServerError, "invalid urls: %s", ds.HTTPJson.GetUrls())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ package router
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
@@ -32,6 +30,11 @@ func (rt *Router) recordingRuleGetsByGids(c *gin.Context) {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +72,14 @@ func (rt *Router) recordingRuleAddByFE(c *gin.Context) {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
for i := range lst {
|
||||
if len(lst[i].DatasourceQueries) == 0 {
|
||||
lst[i].DatasourceQueries = []models.DatasourceQuery{
|
||||
models.DataSourceQueryAll,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
@@ -132,23 +143,10 @@ func (rt *Router) recordingRulePutFields(c *gin.Context) {
|
||||
f.Fields["update_by"] = c.MustGet("username").(string)
|
||||
f.Fields["update_at"] = time.Now().Unix()
|
||||
|
||||
if _, ok := f.Fields["datasource_ids"]; ok {
|
||||
// datasource_ids = "1 2 3"
|
||||
idsStr := strings.Fields(f.Fields["datasource_ids"].(string))
|
||||
ids := make([]int64, 0)
|
||||
for _, idStr := range idsStr {
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, "datasource_ids error")
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(ids)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, "datasource_ids error")
|
||||
}
|
||||
f.Fields["datasource_ids"] = string(bs)
|
||||
if datasourceQueries, ok := f.Fields["datasource_queries"]; ok {
|
||||
bytes, err := json.Marshal(datasourceQueries)
|
||||
ginx.Dangerous(err)
|
||||
f.Fields["datasource_queries"] = string(bytes)
|
||||
}
|
||||
|
||||
for i := 0; i < len(f.Ids); i++ {
|
||||
|
||||
@@ -2,10 +2,13 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/flashduty"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ormx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/secu"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func (rt *Router) selfProfileGet(c *gin.Context) {
|
||||
@@ -29,6 +32,11 @@ func (rt *Router) selfProfilePut(c *gin.Context) {
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
oldInfo := models.User{
|
||||
Username: user.Username,
|
||||
Phone: user.Phone,
|
||||
Email: user.Email,
|
||||
}
|
||||
user.Nickname = f.Nickname
|
||||
user.Phone = f.Phone
|
||||
user.Email = f.Email
|
||||
@@ -36,6 +44,10 @@ func (rt *Router) selfProfilePut(c *gin.Context) {
|
||||
user.Contacts = f.Contacts
|
||||
user.UpdateBy = user.Username
|
||||
|
||||
if flashduty.NeedSyncUser(rt.Ctx) {
|
||||
flashduty.UpdateUser(rt.Ctx, oldInfo, f.Email, f.Phone)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(user.UpdateAllFields(rt.Ctx))
|
||||
}
|
||||
|
||||
@@ -48,5 +60,25 @@ func (rt *Router) selfPasswordPut(c *gin.Context) {
|
||||
var f selfPasswordForm
|
||||
ginx.BindJSON(c, &f)
|
||||
user := c.MustGet("user").(*models.User)
|
||||
ginx.NewRender(c).Message(user.ChangePassword(rt.Ctx, f.OldPass, f.NewPass))
|
||||
|
||||
newPassWord := f.NewPass
|
||||
oldPassWord := f.OldPass
|
||||
if rt.HTTP.RSA.OpenRSA {
|
||||
var err error
|
||||
newPassWord, err = secu.Decrypt(f.NewPass, rt.HTTP.RSA.RSAPrivateKey, rt.HTTP.RSA.RSAPassWord)
|
||||
if err != nil {
|
||||
logger.Errorf("RSA Decrypt failed: %v username: %s", err, user.Username)
|
||||
ginx.NewRender(c).Message(err)
|
||||
return
|
||||
}
|
||||
|
||||
oldPassWord, err = secu.Decrypt(f.OldPass, rt.HTTP.RSA.RSAPrivateKey, rt.HTTP.RSA.RSAPassWord)
|
||||
if err != nil {
|
||||
logger.Errorf("RSA Decrypt failed: %v username: %s", err, user.Username)
|
||||
ginx.NewRender(c).Message(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(user.ChangePassword(rt.Ctx, oldPassWord, newPassWord))
|
||||
}
|
||||
|
||||
@@ -49,6 +49,11 @@ func (rt *Router) targetGets(c *gin.Context) {
|
||||
downtime := ginx.QueryInt64(c, "downtime", 0)
|
||||
dsIds := queryDatasourceIds(c)
|
||||
|
||||
order := ginx.QueryStr(c, "order", "ident")
|
||||
desc := ginx.QueryBool(c, "desc", false)
|
||||
|
||||
hosts := queryStrListField(c, "hosts", ",", " ", "\n")
|
||||
|
||||
var err error
|
||||
if len(bgids) == 0 {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
@@ -63,12 +68,27 @@ func (rt *Router) targetGets(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
total, err := models.TargetTotal(rt.Ctx, bgids, dsIds, query, downtime)
|
||||
options := []models.BuildTargetWhereOption{
|
||||
models.BuildTargetWhereWithBgids(bgids),
|
||||
models.BuildTargetWhereWithDsIds(dsIds),
|
||||
models.BuildTargetWhereWithQuery(query),
|
||||
models.BuildTargetWhereWithDowntime(downtime),
|
||||
models.BuildTargetWhereWithHosts(hosts),
|
||||
}
|
||||
total, err := models.TargetTotal(rt.Ctx, options...)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.TargetGets(rt.Ctx, bgids, dsIds, query, downtime, limit, ginx.Offset(c, limit))
|
||||
list, err := models.TargetGets(rt.Ctx, limit,
|
||||
ginx.Offset(c, limit), order, desc, options...)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
tgs, err := models.TargetBusiGroupsGetAll(rt.Ctx)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
for _, t := range list {
|
||||
t.GroupIds = tgs[t.Ident]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
now := time.Now()
|
||||
cache := make(map[int64]*models.BusiGroup)
|
||||
@@ -148,205 +168,274 @@ func (rt *Router) targetGetsByService(c *gin.Context) {
|
||||
func (rt *Router) targetGetTags(c *gin.Context) {
|
||||
idents := ginx.QueryStr(c, "idents", "")
|
||||
idents = strings.ReplaceAll(idents, ",", " ")
|
||||
lst, err := models.TargetGetTags(rt.Ctx, strings.Fields(idents))
|
||||
ignoreHostTag := ginx.QueryBool(c, "ignore_host_tag", false)
|
||||
lst, err := models.TargetGetTags(rt.Ctx, strings.Fields(idents), ignoreHostTag, "")
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
type targetTagsForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Tags []string `json:"tags" binding:"required"`
|
||||
Idents []string `json:"idents" binding:"required_without=HostIps"`
|
||||
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
|
||||
Tags []string `json:"tags" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindTagsByFE(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetBindTags(f))
|
||||
ginx.NewRender(c).Data(rt.targetBindTags(f, failedResults))
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindTagsByService(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetBindTags(f))
|
||||
ginx.NewRender(c).Data(rt.targetBindTags(f, failedResults))
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindTags(f targetTagsForm) error {
|
||||
for i := 0; i < len(f.Tags); i++ {
|
||||
arr := strings.Split(f.Tags[i], "=")
|
||||
func (rt *Router) targetBindTags(f targetTagsForm, failedIdents map[string]string) (map[string]string, error) {
|
||||
// 1. Check tags
|
||||
if err := rt.validateTags(f.Tags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Acquire targets by idents
|
||||
targets, err := models.TargetsGetByIdents(rt.Ctx, f.Idents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. Add tags to targets
|
||||
for _, target := range targets {
|
||||
if err = rt.addTagsToTarget(target, f.Tags); err != nil {
|
||||
failedIdents[target.Ident] = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return failedIdents, nil
|
||||
}
|
||||
|
||||
func (rt *Router) validateTags(tags []string) error {
|
||||
for _, tag := range tags {
|
||||
arr := strings.Split(tag, "=")
|
||||
if len(arr) != 2 {
|
||||
return fmt.Errorf("invalid tag(%s)", f.Tags[i])
|
||||
return fmt.Errorf("invalid tag format: %s (expected format: key=value)", tag)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(arr[0]) == "" || strings.TrimSpace(arr[1]) == "" {
|
||||
return fmt.Errorf("invalid tag(%s)", f.Tags[i])
|
||||
key, value := strings.TrimSpace(arr[0]), strings.TrimSpace(arr[1])
|
||||
if key == "" {
|
||||
return fmt.Errorf("invalid tag: key is empty in tag %s", tag)
|
||||
}
|
||||
if value == "" {
|
||||
return fmt.Errorf("invalid tag: value is empty in tag %s", tag)
|
||||
}
|
||||
|
||||
if strings.IndexByte(arr[0], '.') != -1 {
|
||||
return fmt.Errorf("invalid tagkey(%s): cannot contains . ", arr[0])
|
||||
if strings.Contains(key, ".") {
|
||||
return fmt.Errorf("invalid tag key: %s (key cannot contain '.')", key)
|
||||
}
|
||||
|
||||
if strings.IndexByte(arr[0], '-') != -1 {
|
||||
return fmt.Errorf("invalid tagkey(%s): cannot contains -", arr[0])
|
||||
if strings.Contains(key, "-") {
|
||||
return fmt.Errorf("invalid tag key: %s (key cannot contain '-')", key)
|
||||
}
|
||||
|
||||
if !model.LabelNameRE.MatchString(arr[0]) {
|
||||
return fmt.Errorf("invalid tagkey(%s)", arr[0])
|
||||
if !model.LabelNameRE.MatchString(key) {
|
||||
return fmt.Errorf("invalid tag key: %s "+
|
||||
"(key must start with a letter or underscore, followed by letters, digits, or underscores)", key)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(f.Idents); i++ {
|
||||
target, err := models.TargetGetByIdent(rt.Ctx, f.Idents[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 不能有同key的标签,否则附到时序数据上会产生覆盖,让人困惑
|
||||
for j := 0; j < len(f.Tags); j++ {
|
||||
tagkey := strings.Split(f.Tags[j], "=")[0]
|
||||
tagkeyPrefix := tagkey + "="
|
||||
if strings.HasPrefix(target.Tags, tagkeyPrefix) {
|
||||
return fmt.Errorf("duplicate tagkey(%s)", tagkey)
|
||||
}
|
||||
}
|
||||
|
||||
err = target.AddTags(rt.Ctx, f.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *Router) addTagsToTarget(target *models.Target, tags []string) error {
|
||||
hostTagsMap := target.GetHostTagsMap()
|
||||
for _, tag := range tags {
|
||||
tagKey := strings.Split(tag, "=")[0]
|
||||
if _, ok := hostTagsMap[tagKey]; ok ||
|
||||
strings.Contains(target.Tags, tagKey+"=") {
|
||||
return fmt.Errorf("duplicate tagkey(%s)", tagKey)
|
||||
}
|
||||
}
|
||||
|
||||
return target.AddTags(rt.Ctx, tags)
|
||||
}
|
||||
|
||||
func (rt *Router) targetUnbindTagsByFE(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetUnbindTags(f))
|
||||
ginx.NewRender(c).Data(rt.targetUnbindTags(f, failedResults))
|
||||
}
|
||||
|
||||
func (rt *Router) targetUnbindTagsByService(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetUnbindTags(f))
|
||||
ginx.NewRender(c).Data(rt.targetUnbindTags(f, failedResults))
|
||||
}
|
||||
|
||||
func (rt *Router) targetUnbindTags(f targetTagsForm) error {
|
||||
for i := 0; i < len(f.Idents); i++ {
|
||||
target, err := models.TargetGetByIdent(rt.Ctx, f.Idents[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
func (rt *Router) targetUnbindTags(f targetTagsForm, failedIdents map[string]string) (map[string]string, error) {
|
||||
// 1. Acquire targets by idents
|
||||
targets, err := models.TargetsGetByIdents(rt.Ctx, f.Idents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Remove tags from targets
|
||||
for _, target := range targets {
|
||||
err = target.DelTags(rt.Ctx, f.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
failedIdents[target.Ident] = err.Error()
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return failedIdents, nil
|
||||
}
|
||||
|
||||
type targetNoteForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Note string `json:"note"`
|
||||
Idents []string `json:"idents" binding:"required_without=HostIps"`
|
||||
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateNote(c *gin.Context) {
|
||||
var f targetNoteForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateNoteByService(c *gin.Context) {
|
||||
var f targetNoteForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
}
|
||||
|
||||
type targetBgidForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Bgid int64 `json:"bgid"`
|
||||
Idents []string `json:"idents" binding:"required_without=HostIps"`
|
||||
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
|
||||
Bgid int64 `json:"bgid"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateBgid(c *gin.Context) {
|
||||
var f targetBgidForm
|
||||
type targetBgidsForm struct {
|
||||
Idents []string `json:"idents" binding:"required_without=HostIps"`
|
||||
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
|
||||
Bgids []int64 `json:"bgids"`
|
||||
Tags []string `json:"tags"`
|
||||
Action string `json:"action"` // add del reset
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindBgids(c *gin.Context) {
|
||||
var f targetBgidsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
if user.IsAdmin() {
|
||||
ginx.NewRender(c).Message(models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
|
||||
return
|
||||
}
|
||||
|
||||
if f.Bgid > 0 {
|
||||
// 把要操作的机器分成两部分,一部分是bgid为0,需要管理员分配,另一部分bgid>0,说明是业务组内部想调整
|
||||
// 比如原来分配给didiyun的机器,didiyun的管理员想把部分机器调整到didiyun-ceph下
|
||||
// 对于调整的这种情况,当前登录用户要对这批机器有操作权限,同时还要对目标BG有操作权限
|
||||
orphans, err := models.IdentsFilter(rt.Ctx, f.Idents, "group_id = ?", 0)
|
||||
if !user.IsAdmin() {
|
||||
// 普通用户,检查用户是否有权限操作所有请求的业务组
|
||||
existing, _, err := models.SeparateTargetIdents(rt.Ctx, f.Idents)
|
||||
ginx.Dangerous(err)
|
||||
rt.checkTargetPerm(c, existing)
|
||||
|
||||
// 机器里边存在未归组的,登录用户就需要是admin
|
||||
if len(orphans) > 0 && !user.IsAdmin() {
|
||||
can, err := user.CheckPerm(rt.Ctx, "/targets/bind")
|
||||
var groupIds []int64
|
||||
if f.Action == "reset" {
|
||||
// 如果是复写,则需要检查用户是否有权限操作机器之前的业务组
|
||||
bgids, err := models.TargetGroupIdsGetByIdents(rt.Ctx, f.Idents)
|
||||
ginx.Dangerous(err)
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
|
||||
}
|
||||
|
||||
groupIds = append(groupIds, bgids...)
|
||||
}
|
||||
groupIds = append(groupIds, f.Bgids...)
|
||||
|
||||
reBelongs, err := models.IdentsFilter(rt.Ctx, f.Idents, "group_id > ?", 0)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(reBelongs) > 0 {
|
||||
// 对于这些要重新分配的机器,操作者要对这些机器本身有权限,同时要对目标bgid有权限
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
bg := BusiGroup(rt.Ctx, f.Bgid)
|
||||
for _, bgid := range groupIds {
|
||||
bg := BusiGroup(rt.Ctx, bgid)
|
||||
can, err := user.CanDoBusiGroup(rt.Ctx, bg, "rw")
|
||||
ginx.Dangerous(err)
|
||||
|
||||
@@ -354,31 +443,86 @@ func (rt *Router) targetUpdateBgid(c *gin.Context) {
|
||||
ginx.Bomb(http.StatusForbidden, "No permission. You are not admin of BG(%s)", bg.Name)
|
||||
}
|
||||
}
|
||||
} else if f.Bgid == 0 {
|
||||
// 退还机器
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
} else {
|
||||
ginx.Bomb(http.StatusBadRequest, "invalid bgid")
|
||||
|
||||
can, err := user.CheckPerm(rt.Ctx, "/targets/bind")
|
||||
ginx.Dangerous(err)
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
|
||||
switch f.Action {
|
||||
case "add":
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetBindBgids(rt.Ctx, f.Idents, f.Bgids, f.Tags))
|
||||
case "del":
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetUnbindBgids(rt.Ctx, f.Idents, f.Bgids))
|
||||
case "reset":
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetOverrideBgids(rt.Ctx, f.Idents, f.Bgids, f.Tags))
|
||||
default:
|
||||
ginx.Bomb(http.StatusBadRequest, "invalid action")
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateBgidByService(c *gin.Context) {
|
||||
var f targetBgidForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetOverrideBgids(rt.Ctx, f.Idents, []int64{f.Bgid}, nil))
|
||||
}
|
||||
|
||||
type identsForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Idents []string `json:"idents" binding:"required_without=HostIps"`
|
||||
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetDel(c *gin.Context) {
|
||||
var f identsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetDel(rt.Ctx, f.Idents))
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetDel(rt.Ctx, f.Idents, rt.TargetDeleteHook))
|
||||
}
|
||||
|
||||
func (rt *Router) targetDelByService(c *gin.Context) {
|
||||
var f identsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetDel(rt.Ctx, f.Idents, rt.TargetDeleteHook))
|
||||
}
|
||||
|
||||
func (rt *Router) checkTargetPerm(c *gin.Context, idents []string) {
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/sender"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
@@ -54,6 +51,11 @@ func (rt *Router) taskGetsByGids(c *gin.Context) {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,71 +98,6 @@ type taskForm struct {
|
||||
Hosts []string `json:"hosts" binding:"required"`
|
||||
}
|
||||
|
||||
func (f *taskForm) Verify() error {
|
||||
if f.Batch < 0 {
|
||||
return fmt.Errorf("arg(batch) should be nonnegative")
|
||||
}
|
||||
|
||||
if f.Tolerance < 0 {
|
||||
return fmt.Errorf("arg(tolerance) should be nonnegative")
|
||||
}
|
||||
|
||||
if f.Timeout < 0 {
|
||||
return fmt.Errorf("arg(timeout) should be nonnegative")
|
||||
}
|
||||
|
||||
if f.Timeout > 3600*24 {
|
||||
return fmt.Errorf("arg(timeout) longer than one day")
|
||||
}
|
||||
|
||||
if f.Timeout == 0 {
|
||||
f.Timeout = 30
|
||||
}
|
||||
|
||||
f.Pause = strings.Replace(f.Pause, ",", ",", -1)
|
||||
f.Pause = strings.Replace(f.Pause, " ", "", -1)
|
||||
f.Args = strings.Replace(f.Args, ",", ",", -1)
|
||||
|
||||
if f.Title == "" {
|
||||
return fmt.Errorf("arg(title) is required")
|
||||
}
|
||||
|
||||
if str.Dangerous(f.Title) {
|
||||
return fmt.Errorf("arg(title) is dangerous")
|
||||
}
|
||||
|
||||
if f.Script == "" {
|
||||
return fmt.Errorf("arg(script) is required")
|
||||
}
|
||||
f.Script = strings.Replace(f.Script, "\r\n", "\n", -1)
|
||||
|
||||
if str.Dangerous(f.Args) {
|
||||
return fmt.Errorf("arg(args) is dangerous")
|
||||
}
|
||||
|
||||
if str.Dangerous(f.Pause) {
|
||||
return fmt.Errorf("arg(pause) is dangerous")
|
||||
}
|
||||
|
||||
if len(f.Hosts) == 0 {
|
||||
return fmt.Errorf("arg(hosts) empty")
|
||||
}
|
||||
|
||||
if f.Action != "start" && f.Action != "pause" {
|
||||
return fmt.Errorf("arg(action) invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *taskForm) HandleFH(fh string) {
|
||||
i := strings.Index(f.Title, " FH: ")
|
||||
if i > 0 {
|
||||
f.Title = f.Title[:i]
|
||||
}
|
||||
f.Title = f.Title + " FH: " + fh
|
||||
}
|
||||
|
||||
func (rt *Router) taskRecordAdd(c *gin.Context) {
|
||||
var f *models.TaskRecord
|
||||
ginx.BindJSON(c, &f)
|
||||
@@ -168,7 +105,12 @@ func (rt *Router) taskRecordAdd(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) taskAdd(c *gin.Context) {
|
||||
var f taskForm
|
||||
if !rt.Ibex.Enable {
|
||||
ginx.Bomb(400, i18n.Sprintf(c.GetHeader("X-Language"), "This functionality has not been enabled. Please contact the system administrator to activate it."))
|
||||
return
|
||||
}
|
||||
|
||||
var f models.TaskForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
@@ -184,7 +126,7 @@ func (rt *Router) taskAdd(c *gin.Context) {
|
||||
rt.checkTargetPerm(c, f.Hosts)
|
||||
|
||||
// call ibex
|
||||
taskId, err := TaskCreate(f, rt.NotifyConfigCache.GetIbex())
|
||||
taskId, err := sender.TaskAdd(f, user.Username, rt.Ctx.IsCenter)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if taskId <= 0 {
|
||||
@@ -193,65 +135,20 @@ func (rt *Router) taskAdd(c *gin.Context) {
|
||||
|
||||
// write db
|
||||
record := models.TaskRecord{
|
||||
Id: taskId,
|
||||
GroupId: bgid,
|
||||
IbexAddress: rt.NotifyConfigCache.GetIbex().Address,
|
||||
IbexAuthUser: rt.NotifyConfigCache.GetIbex().BasicAuthUser,
|
||||
IbexAuthPass: rt.NotifyConfigCache.GetIbex().BasicAuthPass,
|
||||
Title: f.Title,
|
||||
Account: f.Account,
|
||||
Batch: f.Batch,
|
||||
Tolerance: f.Tolerance,
|
||||
Timeout: f.Timeout,
|
||||
Pause: f.Pause,
|
||||
Script: f.Script,
|
||||
Args: f.Args,
|
||||
CreateAt: time.Now().Unix(),
|
||||
CreateBy: f.Creator,
|
||||
Id: taskId,
|
||||
GroupId: bgid,
|
||||
Title: f.Title,
|
||||
Account: f.Account,
|
||||
Batch: f.Batch,
|
||||
Tolerance: f.Tolerance,
|
||||
Timeout: f.Timeout,
|
||||
Pause: f.Pause,
|
||||
Script: f.Script,
|
||||
Args: f.Args,
|
||||
CreateAt: time.Now().Unix(),
|
||||
CreateBy: f.Creator,
|
||||
}
|
||||
|
||||
err = record.Add(rt.Ctx)
|
||||
ginx.NewRender(c).Data(taskId, err)
|
||||
}
|
||||
|
||||
func (rt *Router) taskProxy(c *gin.Context) {
|
||||
target, err := url.Parse(rt.NotifyConfigCache.GetIbex().Address)
|
||||
if err != nil {
|
||||
ginx.NewRender(c).Message("invalid ibex address: %s", rt.NotifyConfigCache.GetIbex().Address)
|
||||
return
|
||||
}
|
||||
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
|
||||
// fe request e.g. /api/n9e/busi-group/:id/task/*url
|
||||
index := strings.Index(req.URL.Path, "/task/")
|
||||
if index == -1 {
|
||||
panic("url path invalid")
|
||||
}
|
||||
|
||||
req.URL.Path = "/ibex/v1" + req.URL.Path[index:]
|
||||
|
||||
if target.RawQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
|
||||
if rt.NotifyConfigCache.GetIbex().BasicAuthUser != "" {
|
||||
req.SetBasicAuth(rt.NotifyConfigCache.GetIbex().BasicAuthUser, rt.NotifyConfigCache.GetIbex().BasicAuthPass)
|
||||
}
|
||||
}
|
||||
|
||||
errFunc := func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
ginx.NewRender(c, http.StatusBadGateway).Message(err)
|
||||
}
|
||||
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Director: director,
|
||||
ErrorHandler: errFunc,
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
@@ -45,6 +46,11 @@ func (rt *Router) taskTplGetsByGids(c *gin.Context) {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +97,14 @@ func (rt *Router) taskTplGetByService(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(tpl, err)
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplGetsByService(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(models.TaskTplGetAll(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplStatistics(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(models.TaskTplStatistics(rt.Ctx))
|
||||
}
|
||||
|
||||
type taskTplForm struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Batch int `json:"batch"`
|
||||
@@ -105,6 +119,11 @@ type taskTplForm struct {
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplAdd(c *gin.Context) {
|
||||
if !rt.Ibex.Enable {
|
||||
ginx.Bomb(400, i18n.Sprintf(c.GetHeader("X-Language"), "This functionality has not been enabled. Please contact the system administrator to activate it."))
|
||||
return
|
||||
}
|
||||
|
||||
var f taskTplForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
@@ -177,6 +196,13 @@ func (rt *Router) taskTplDel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ids, err := models.GetAlertRuleIdsByTaskId(rt.Ctx, tid)
|
||||
ginx.Dangerous(err)
|
||||
if len(ids) > 0 {
|
||||
ginx.NewRender(c).Message("can't del this task tpl, used by alert rule ids(%v) ", ids)
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(tpl.Del(rt.Ctx))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/flashduty"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ormx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/secu"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func (rt *Router) userBusiGroupsGets(c *gin.Context) {
|
||||
@@ -39,13 +42,17 @@ func (rt *Router) userFindAll(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) userGets(c *gin.Context) {
|
||||
stime, etime := getTimeRange(c)
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
order := ginx.QueryStr(c, "order", "username")
|
||||
desc := ginx.QueryBool(c, "desc", false)
|
||||
|
||||
total, err := models.UserTotal(rt.Ctx, query)
|
||||
go rt.UserCache.UpdateUsersLastActiveTime()
|
||||
total, err := models.UserTotal(rt.Ctx, query, stime, etime)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.UserGets(rt.Ctx, query, limit, ginx.Offset(c, limit))
|
||||
list, err := models.UserGets(rt.Ctx, query, limit, ginx.Offset(c, limit), stime, etime, order, desc)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
@@ -72,7 +79,18 @@ func (rt *Router) userAddPost(c *gin.Context) {
|
||||
var f userAddForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
password, err := models.CryptoPass(rt.Ctx, f.Password)
|
||||
authPassWord := f.Password
|
||||
if rt.HTTP.RSA.OpenRSA {
|
||||
decPassWord, err := secu.Decrypt(f.Password, rt.HTTP.RSA.RSAPrivateKey, rt.HTTP.RSA.RSAPassWord)
|
||||
if err != nil {
|
||||
logger.Errorf("RSA Decrypt failed: %v username: %s", err, f.Username)
|
||||
ginx.NewRender(c).Message(err)
|
||||
return
|
||||
}
|
||||
authPassWord = decPassWord
|
||||
}
|
||||
|
||||
password, err := models.CryptoPass(rt.Ctx, authPassWord)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(f.Roles) == 0 {
|
||||
@@ -94,6 +112,7 @@ func (rt *Router) userAddPost(c *gin.Context) {
|
||||
UpdateBy: username,
|
||||
}
|
||||
|
||||
ginx.Dangerous(u.Verify())
|
||||
ginx.NewRender(c).Message(u.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
@@ -143,6 +162,11 @@ func (rt *Router) userProfilePut(c *gin.Context) {
|
||||
}
|
||||
|
||||
target := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
oldInfo := models.User{
|
||||
Username: target.Username,
|
||||
Phone: target.Phone,
|
||||
Email: target.Email,
|
||||
}
|
||||
target.Nickname = f.Nickname
|
||||
target.Phone = f.Phone
|
||||
target.Email = f.Email
|
||||
@@ -150,6 +174,10 @@ func (rt *Router) userProfilePut(c *gin.Context) {
|
||||
target.Contacts = f.Contacts
|
||||
target.UpdateBy = c.MustGet("username").(string)
|
||||
|
||||
if flashduty.NeedSyncUser(rt.Ctx) {
|
||||
flashduty.UpdateUser(rt.Ctx, oldInfo, f.Email, f.Phone)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(target.UpdateAllFields(rt.Ctx))
|
||||
}
|
||||
|
||||
@@ -163,7 +191,18 @@ func (rt *Router) userPasswordPut(c *gin.Context) {
|
||||
|
||||
target := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
|
||||
cryptoPass, err := models.CryptoPass(rt.Ctx, f.Password)
|
||||
authPassWord := f.Password
|
||||
if rt.HTTP.RSA.OpenRSA {
|
||||
decPassWord, err := secu.Decrypt(f.Password, rt.HTTP.RSA.RSAPrivateKey, rt.HTTP.RSA.RSAPassWord)
|
||||
if err != nil {
|
||||
logger.Errorf("RSA Decrypt failed: %v username: %s", err, target.Username)
|
||||
ginx.NewRender(c).Message(err)
|
||||
return
|
||||
}
|
||||
authPassWord = decPassWord
|
||||
}
|
||||
|
||||
cryptoPass, err := models.CryptoPass(rt.Ctx, authPassWord)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(target.UpdatePassword(rt.Ctx, cryptoPass, c.MustGet("username").(string)))
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
func (rt *Router) checkBusiGroupPerm(c *gin.Context) {
|
||||
@@ -31,8 +32,36 @@ func (rt *Router) userGroupGets(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupGetsByService(c *gin.Context) {
|
||||
lst, err := models.UserGroupGetAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
ids := str.IdsInt64(ginx.QueryStr(c, "ids", ""))
|
||||
|
||||
if len(ids) == 0 {
|
||||
lst, err := models.UserGroupGetAll(rt.Ctx)
|
||||
ginx.Dangerous(err)
|
||||
for i := 0; i < len(lst); i++ {
|
||||
ids, err := models.MemberIds(rt.Ctx, lst[i].Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
lst[i].Users, err = models.UserGetsByIds(rt.Ctx, ids)
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
return
|
||||
}
|
||||
|
||||
lst := make([]models.UserGroup, 0)
|
||||
for _, id := range ids {
|
||||
ug := UserGroup(rt.Ctx, id)
|
||||
|
||||
ids, err := models.MemberIds(rt.Ctx, ug.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ug.Users, err = models.UserGetsByIds(rt.Ctx, ids)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
lst = append(lst, *ug)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
}
|
||||
|
||||
// user group member get by service
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/cas"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ldapx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/oauth2x"
|
||||
"github.com/ccfos/nightingale/v6/pkg/oidcx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/tplx"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type SsoClient struct {
|
||||
OIDC *oidcx.SsoClient
|
||||
LDAP *ldapx.SsoClient
|
||||
CAS *cas.SsoClient
|
||||
OAuth2 *oauth2x.SsoClient
|
||||
LastUpdateTime int64
|
||||
OIDC *oidcx.SsoClient
|
||||
LDAP *ldapx.SsoClient
|
||||
CAS *cas.SsoClient
|
||||
OAuth2 *oauth2x.SsoClient
|
||||
LastUpdateTime int64
|
||||
configCache *memsto.ConfigCache
|
||||
configLastUpdateTime int64
|
||||
}
|
||||
|
||||
const LDAP = `
|
||||
@@ -111,7 +116,7 @@ Phone = 'phone_number'
|
||||
Email = 'email'
|
||||
`
|
||||
|
||||
func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
func Init(center cconf.Center, ctx *ctx.Context, configCache *memsto.ConfigCache) *SsoClient {
|
||||
ssoClient := new(SsoClient)
|
||||
m := make(map[string]string)
|
||||
m["LDAP"] = LDAP
|
||||
@@ -140,6 +145,11 @@ func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
if configCache == nil {
|
||||
log.Fatalln(fmt.Errorf("configCache is nil, sso initialization failed"))
|
||||
}
|
||||
ssoClient.configCache = configCache
|
||||
userVariableMap := configCache.Get()
|
||||
|
||||
configs, err := models.SsoConfigGets(ctx)
|
||||
if err != nil {
|
||||
@@ -147,6 +157,7 @@ func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
cfg.Content = tplx.ReplaceTemplateUseText(cfg.Name, cfg.Content, userVariableMap)
|
||||
switch cfg.Name {
|
||||
case "LDAP":
|
||||
var config ldapx.Config
|
||||
@@ -185,7 +196,7 @@ func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
}
|
||||
}
|
||||
|
||||
ssoClient.SyncSsoUsers(ctx)
|
||||
go ssoClient.SyncSsoUsers(ctx)
|
||||
go ssoClient.Reload(ctx)
|
||||
return ssoClient
|
||||
}
|
||||
@@ -197,7 +208,8 @@ func (s *SsoClient) reload(ctx *ctx.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if lastUpdateTime == s.LastUpdateTime {
|
||||
lastCacheUpdateTime := s.configCache.GetLastUpdateTime()
|
||||
if lastUpdateTime == s.LastUpdateTime && lastCacheUpdateTime == s.configLastUpdateTime {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -205,8 +217,9 @@ func (s *SsoClient) reload(ctx *ctx.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userVariableMap := s.configCache.Get()
|
||||
for _, cfg := range configs {
|
||||
cfg.Content = tplx.ReplaceTemplateUseText(cfg.Name, cfg.Content, userVariableMap)
|
||||
switch cfg.Name {
|
||||
case "LDAP":
|
||||
var config ldapx.Config
|
||||
@@ -250,6 +263,7 @@ func (s *SsoClient) reload(ctx *ctx.Context) error {
|
||||
}
|
||||
|
||||
s.LastUpdateTime = lastUpdateTime
|
||||
s.configLastUpdateTime = lastCacheUpdateTime
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ type ClusterOptions struct {
|
||||
MaxIdleConnsPerHost int
|
||||
}
|
||||
|
||||
func Parse(fpath string, configPtr interface{}) error {
|
||||
func Parse(fpath string, configPtr *Config) error {
|
||||
var (
|
||||
tBuf []byte
|
||||
)
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/dispatch"
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
alertrt "github.com/ccfos/nightingale/v6/alert/router"
|
||||
"github.com/ccfos/nightingale/v6/center/metas"
|
||||
"github.com/ccfos/nightingale/v6/conf"
|
||||
"github.com/ccfos/nightingale/v6/dumper"
|
||||
@@ -17,12 +19,12 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/pkg/logx"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/idents"
|
||||
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
alertrt "github.com/ccfos/nightingale/v6/alert/router"
|
||||
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
|
||||
"github.com/flashcatcloud/ibex/src/cmd/ibex"
|
||||
)
|
||||
|
||||
func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
@@ -42,22 +44,22 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
ctx := ctx.NewContext(context.Background(), nil, false, config.CenterApi)
|
||||
|
||||
var redis storage.Redis
|
||||
if config.Redis.Address != "" {
|
||||
redis, err = storage.NewRedis(config.Redis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
redis, err = storage.NewRedis(config.Redis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncStats := memsto.NewSyncStats()
|
||||
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, nil)
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, redis)
|
||||
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
|
||||
configCvalCache := memsto.NewCvalCache(ctx, syncStats)
|
||||
idents := idents.New(ctx, redis)
|
||||
metas := metas.New(redis)
|
||||
writers := writer.NewWriters(config.Pushgw)
|
||||
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP, configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
|
||||
|
||||
pushgwRouter.Config(r)
|
||||
|
||||
if !config.Alert.Disable {
|
||||
@@ -69,17 +71,25 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
notifyConfigCache := memsto.NewNotifyConfigCache(ctx, configCache)
|
||||
userCache := memsto.NewUserCache(ctx, syncStats)
|
||||
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
|
||||
taskTplsCache := memsto.NewTaskTplCache(ctx)
|
||||
|
||||
promClients := prom.NewPromClient(ctx)
|
||||
|
||||
dispatch.InitRegisterQueryFunc(promClients)
|
||||
|
||||
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
|
||||
externalProcessors := process.NewExternalProcessors()
|
||||
|
||||
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache,
|
||||
alertRuleCache, notifyConfigCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
|
||||
alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
|
||||
|
||||
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
|
||||
|
||||
alertrtRouter.Config(r)
|
||||
|
||||
if config.Ibex.Enable {
|
||||
ibex.ServerStart(false, nil, redis, config.HTTP.APIForService.BasicAuth, config.Alert.Heartbeat, &config.CenterApi, r, nil, config.Ibex, config.HTTP.Port)
|
||||
}
|
||||
}
|
||||
|
||||
dumper.ConfigRouter(r)
|
||||
|
||||
12
conf/conf.go
12
conf/conf.go
@@ -27,6 +27,7 @@ type ConfigType struct {
|
||||
Pushgw pconf.Pushgw
|
||||
Alert aconf.Alert
|
||||
Center cconf.Center
|
||||
Ibex Ibex
|
||||
}
|
||||
|
||||
type CenterApi struct {
|
||||
@@ -40,6 +41,17 @@ type GlobalConfig struct {
|
||||
RunMode string
|
||||
}
|
||||
|
||||
type Ibex struct {
|
||||
Enable bool
|
||||
RPCListen string
|
||||
Output Output
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
ComeFrom string
|
||||
AgtdPort int
|
||||
}
|
||||
|
||||
func InitConfig(configDir, cryptoKey string) (*ConfigType, error) {
|
||||
var config = new(ConfigType)
|
||||
|
||||
|
||||
41
cron/clean_notify_record.go
Normal file
41
cron/clean_notify_record.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func cleanNotifyRecord(ctx *ctx.Context, day int) {
|
||||
lastWeek := time.Now().Unix() - 86400*int64(day)
|
||||
err := models.DB(ctx).Model(&models.NotificaitonRecord{}).Where("created_at < ?", lastWeek).Delete(&models.NotificaitonRecord{}).Error
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to clean notify record: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 每天凌晨1点执行清理任务
|
||||
func CleanNotifyRecord(ctx *ctx.Context, day int) {
|
||||
c := cron.New()
|
||||
if day < 1 {
|
||||
day = 7
|
||||
}
|
||||
|
||||
// 使用cron表达式设置每天凌晨1点执行
|
||||
_, err := c.AddFunc("0 1 * * *", func() {
|
||||
cleanNotifyRecord(ctx, day)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to add clean notify record cron job: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 启动cron任务
|
||||
c.Start()
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
FROM python:3-slim
|
||||
#FROM ubuntu:21.04
|
||||
|
||||
WORKDIR /app
|
||||
ADD n9e /app
|
||||
ADD http://download.flashcat.cloud/wait /wait
|
||||
RUN chmod +x /wait
|
||||
RUN chmod +x n9e
|
||||
|
||||
EXPOSE 17000
|
||||
|
||||
CMD ["/app/n9e", "-h"]
|
||||
@@ -3,7 +3,7 @@ FROM --platform=$TARGETPLATFORM python:3-slim
|
||||
|
||||
WORKDIR /app
|
||||
ADD n9e /app/
|
||||
ADD etc /app/
|
||||
ADD etc /app/etc/
|
||||
ADD integrations /app/integrations/
|
||||
RUN pip install requests
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM --platform=$TARGETPLATFORM python:3-slim
|
||||
|
||||
WORKDIR /app
|
||||
ADD n9e /app/
|
||||
ADD etc /app/
|
||||
ADD etc /app/etc/
|
||||
ADD integrations /app/integrations/
|
||||
|
||||
EXPOSE 17000
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.7"
|
||||
|
||||
networks:
|
||||
nightingale:
|
||||
driver: bridge
|
||||
@@ -69,27 +67,6 @@ services:
|
||||
command:
|
||||
- "--loggerTimezone=Asia/Shanghai"
|
||||
|
||||
ibex:
|
||||
image: flashcatcloud/ibex:v1.2.0
|
||||
container_name: ibex
|
||||
hostname: ibex
|
||||
restart: always
|
||||
environment:
|
||||
GIN_MODE: release
|
||||
TZ: Asia/Shanghai
|
||||
WAIT_HOSTS: mysql:3306
|
||||
volumes:
|
||||
- ./etc-ibex:/app/etc
|
||||
networks:
|
||||
- nightingale
|
||||
ports:
|
||||
- "10090:10090"
|
||||
- "20090:20090"
|
||||
depends_on:
|
||||
- mysql
|
||||
command: >
|
||||
sh -c "/app/ibex server"
|
||||
|
||||
nightingale:
|
||||
image: flashcatcloud/nightingale:latest
|
||||
container_name: nightingale
|
||||
@@ -105,6 +82,7 @@ services:
|
||||
- nightingale
|
||||
ports:
|
||||
- "17000:17000"
|
||||
- "20090:20090"
|
||||
depends_on:
|
||||
- mysql
|
||||
- redis
|
||||
@@ -122,7 +100,7 @@ services:
|
||||
HOST_PROC: /hostfs/proc
|
||||
HOST_SYS: /hostfs/sys
|
||||
HOST_MOUNT_PREFIX: /hostfs
|
||||
WAIT_HOSTS: nightingale:17000, ibex:20090
|
||||
WAIT_HOSTS: nightingale:17000, nightingale:20090
|
||||
volumes:
|
||||
- ./etc-categraf:/etc/categraf/conf
|
||||
- /:/hostfs
|
||||
|
||||
@@ -19,8 +19,8 @@ precision = "ms"
|
||||
# global collect interval
|
||||
interval = 15
|
||||
|
||||
[global.labels]
|
||||
source="categraf"
|
||||
# [global.labels]
|
||||
# source="categraf"
|
||||
# region = "shanghai"
|
||||
# env = "localhost"
|
||||
|
||||
@@ -78,6 +78,6 @@ enable = true
|
||||
## ibex flush interval
|
||||
interval = "1000ms"
|
||||
## n9e ibex server rpc address
|
||||
servers = ["ibex:20090"]
|
||||
servers = ["nightingale:20090"]
|
||||
## temp script dir
|
||||
meta_dir = "./meta"
|
||||
|
||||
42
docker/compose-bridge/etc-categraf/input.mysql/mysql.toml
Normal file
42
docker/compose-bridge/etc-categraf/input.mysql/mysql.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[[instances]]
|
||||
address = "mysql:3306"
|
||||
username = "root"
|
||||
password = "1234"
|
||||
|
||||
# # set tls=custom to enable tls
|
||||
# parameters = "tls=false"
|
||||
|
||||
# extra_status_metrics = true
|
||||
# extra_innodb_metrics = false
|
||||
# gather_processlist_processes_by_state = false
|
||||
# gather_processlist_processes_by_user = false
|
||||
# gather_schema_size = true
|
||||
# gather_table_size = false
|
||||
# gather_system_table_size = false
|
||||
# gather_slave_status = true
|
||||
|
||||
# # timeout
|
||||
# timeout_seconds = 3
|
||||
|
||||
# # interval = global.interval * interval_times
|
||||
# interval_times = 1
|
||||
|
||||
# important! use global unique string to specify instance
|
||||
labels = { instance="docker-compose-mysql" }
|
||||
|
||||
## Optional TLS Config
|
||||
# use_tls = false
|
||||
# tls_min_version = "1.2"
|
||||
# tls_ca = "/etc/categraf/ca.pem"
|
||||
# tls_cert = "/etc/categraf/cert.pem"
|
||||
# tls_key = "/etc/categraf/key.pem"
|
||||
## Use TLS but skip chain & host verification
|
||||
# insecure_skip_verify = true
|
||||
|
||||
#[[instances.queries]]
|
||||
# mesurement = "lock_wait"
|
||||
# metric_fields = [ "total" ]
|
||||
# timeout = "3s"
|
||||
# request = '''
|
||||
#SELECT count(*) as total FROM information_schema.innodb_trx WHERE trx_state='LOCK WAIT'
|
||||
#'''
|
||||
37
docker/compose-bridge/etc-categraf/input.redis/redis.toml
Normal file
37
docker/compose-bridge/etc-categraf/input.redis/redis.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[[instances]]
|
||||
address = "redis:6379"
|
||||
username = ""
|
||||
password = ""
|
||||
# pool_size = 2
|
||||
|
||||
## 是否开启slowlog 收集
|
||||
# gather_slowlog = true
|
||||
## 最多收集少条slowlog
|
||||
# slowlog_max_len = 100
|
||||
## 收集距离现在多少秒以内的slowlog
|
||||
## 注意插件的采集周期,该参数不要小于采集周期,否则会有slowlog查不到
|
||||
# slowlog_time_window=30
|
||||
|
||||
# 指标
|
||||
# redis_slow_log{ident=dev-01 client_addr=127.0.0.1:56364 client_name= cmd="info ALL" log_id=983} 74 (单位微秒)
|
||||
|
||||
# # Optional. Specify redis commands to retrieve values
|
||||
# commands = [
|
||||
# {command = ["get", "sample-key1"], metric = "custom_metric_name1"},
|
||||
# {command = ["get", "sample-key2"], metric = "custom_metric_name2"}
|
||||
# ]
|
||||
|
||||
# # interval = global.interval * interval_times
|
||||
# interval_times = 1
|
||||
|
||||
# important! use global unique string to specify instance
|
||||
labels = { instance="docker-compose-redis" }
|
||||
|
||||
## Optional TLS Config
|
||||
# use_tls = false
|
||||
# tls_min_version = "1.2"
|
||||
# tls_ca = "/etc/categraf/ca.pem"
|
||||
# tls_cert = "/etc/categraf/cert.pem"
|
||||
# tls_key = "/etc/categraf/key.pem"
|
||||
## Use TLS but skip chain & host verification
|
||||
# insecure_skip_verify = true
|
||||
@@ -1,97 +0,0 @@
|
||||
# debug, release
|
||||
RunMode = "release"
|
||||
|
||||
[Log]
|
||||
# log write dir
|
||||
Dir = "logs-server"
|
||||
# log level: DEBUG INFO WARNING ERROR
|
||||
Level = "DEBUG"
|
||||
# stdout, stderr, file
|
||||
Output = "stdout"
|
||||
# # rotate by time
|
||||
# KeepHours: 4
|
||||
# # rotate by size
|
||||
# RotateNum = 3
|
||||
# # unit: MB
|
||||
# RotateSize = 256
|
||||
|
||||
[HTTP]
|
||||
Enable = true
|
||||
# http listening address
|
||||
Host = "0.0.0.0"
|
||||
# http listening port
|
||||
Port = 10090
|
||||
# https cert file path
|
||||
CertFile = ""
|
||||
# https key file path
|
||||
KeyFile = ""
|
||||
# whether print access log
|
||||
PrintAccessLog = true
|
||||
# whether enable pprof
|
||||
PProf = false
|
||||
# http graceful shutdown timeout, unit: s
|
||||
ShutdownTimeout = 30
|
||||
# max content length: 64M
|
||||
MaxContentLength = 67108864
|
||||
# http server read timeout, unit: s
|
||||
ReadTimeout = 20
|
||||
# http server write timeout, unit: s
|
||||
WriteTimeout = 40
|
||||
# http server idle timeout, unit: s
|
||||
IdleTimeout = 120
|
||||
|
||||
[BasicAuth]
|
||||
# using when call apis
|
||||
ibex = "ibex"
|
||||
|
||||
[RPC]
|
||||
Listen = "0.0.0.0:20090"
|
||||
|
||||
[Heartbeat]
|
||||
# auto detect if blank
|
||||
IP = ""
|
||||
# unit: ms
|
||||
Interval = 1000
|
||||
|
||||
[Output]
|
||||
# database | remote
|
||||
ComeFrom = "database"
|
||||
AgtdPort = 2090
|
||||
|
||||
[Gorm]
|
||||
# enable debug mode or not
|
||||
Debug = false
|
||||
# mysql postgres
|
||||
DBType = "mysql"
|
||||
# unit: s
|
||||
MaxLifetime = 7200
|
||||
# max open connections
|
||||
MaxOpenConns = 150
|
||||
# max idle connections
|
||||
MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
|
||||
[MySQL]
|
||||
# mysql address host:port
|
||||
Address = "mysql:3306"
|
||||
# mysql username
|
||||
User = "root"
|
||||
# mysql password
|
||||
Password = "1234"
|
||||
# database name
|
||||
DBName = "ibex"
|
||||
# connection params
|
||||
Parameters = "charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
|
||||
|
||||
[Postgres]
|
||||
# pg address host:port
|
||||
Address = "postgres:5432"
|
||||
# pg user
|
||||
User = "root"
|
||||
# pg password
|
||||
Password = "1234"
|
||||
# database name
|
||||
DBName = "ibex"
|
||||
# ssl mode
|
||||
SSLMode = "disable"
|
||||
@@ -50,13 +50,11 @@ Enable = true
|
||||
# user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
[HTTP.APIForService]
|
||||
Enable = true
|
||||
Enable = false
|
||||
[HTTP.APIForService.BasicAuth]
|
||||
user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
[HTTP.JWTAuth]
|
||||
# signing key
|
||||
SigningKey = "5b94a0fd640fe2765af826acfe42d151"
|
||||
# unit: min
|
||||
AccessExpired = 1500
|
||||
# unit: min
|
||||
@@ -96,8 +94,6 @@ MaxLifetime = 7200
|
||||
MaxOpenConns = 150
|
||||
# max idle connections
|
||||
MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
# enable auto migrate or not
|
||||
# EnableAutoMigrate = false
|
||||
|
||||
@@ -181,3 +177,7 @@ MaxIdleConnsPerHost = 100
|
||||
# Regex = "([^:]+)(?::\\d+)?"
|
||||
# Replacement = "$1:80"
|
||||
# TargetLabel = "__address__"
|
||||
|
||||
[Ibex]
|
||||
Enable = true
|
||||
RPCListen = "0.0.0.0:20090"
|
||||
@@ -42,23 +42,6 @@ services:
|
||||
- "--enable-feature=remote-write-receiver"
|
||||
- "--query.lookback-delta=2m"
|
||||
|
||||
ibex:
|
||||
image: flashcatcloud/ibex:v0.5.0
|
||||
container_name: ibex
|
||||
hostname: ibex
|
||||
restart: always
|
||||
environment:
|
||||
GIN_MODE: release
|
||||
TZ: Asia/Shanghai
|
||||
WAIT_HOSTS: 127.0.0.1:3306
|
||||
volumes:
|
||||
- ./etc-ibex:/app/etc
|
||||
network_mode: host
|
||||
depends_on:
|
||||
- mysql
|
||||
command: >
|
||||
sh -c "/app/ibex server"
|
||||
|
||||
n9e:
|
||||
image: flashcatcloud/nightingale:latest
|
||||
container_name: n9e
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# debug, release
|
||||
RunMode = "release"
|
||||
|
||||
[Log]
|
||||
# log write dir
|
||||
Dir = "logs-server"
|
||||
# log level: DEBUG INFO WARNING ERROR
|
||||
Level = "DEBUG"
|
||||
# stdout, stderr, file
|
||||
Output = "stdout"
|
||||
# # rotate by time
|
||||
# KeepHours: 4
|
||||
# # rotate by size
|
||||
# RotateNum = 3
|
||||
# # unit: MB
|
||||
# RotateSize = 256
|
||||
|
||||
[HTTP]
|
||||
Enable = true
|
||||
# http listening address
|
||||
Host = "0.0.0.0"
|
||||
# http listening port
|
||||
Port = 10090
|
||||
# https cert file path
|
||||
CertFile = ""
|
||||
# https key file path
|
||||
KeyFile = ""
|
||||
# whether print access log
|
||||
PrintAccessLog = true
|
||||
# whether enable pprof
|
||||
PProf = false
|
||||
# http graceful shutdown timeout, unit: s
|
||||
ShutdownTimeout = 30
|
||||
# max content length: 64M
|
||||
MaxContentLength = 67108864
|
||||
# http server read timeout, unit: s
|
||||
ReadTimeout = 20
|
||||
# http server write timeout, unit: s
|
||||
WriteTimeout = 40
|
||||
# http server idle timeout, unit: s
|
||||
IdleTimeout = 120
|
||||
|
||||
[BasicAuth]
|
||||
# using when call apis
|
||||
ibex = "ibex"
|
||||
|
||||
[RPC]
|
||||
Listen = "0.0.0.0:20090"
|
||||
|
||||
[Heartbeat]
|
||||
# auto detect if blank
|
||||
IP = ""
|
||||
# unit: ms
|
||||
Interval = 1000
|
||||
|
||||
[Output]
|
||||
# database | remote
|
||||
ComeFrom = "database"
|
||||
AgtdPort = 2090
|
||||
|
||||
[Gorm]
|
||||
# enable debug mode or not
|
||||
Debug = false
|
||||
# mysql postgres
|
||||
DBType = "mysql"
|
||||
# unit: s
|
||||
MaxLifetime = 7200
|
||||
# max open connections
|
||||
MaxOpenConns = 150
|
||||
# max idle connections
|
||||
MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
|
||||
[MySQL]
|
||||
# mysql address host:port
|
||||
Address = "127.0.0.1:3306"
|
||||
# mysql username
|
||||
User = "root"
|
||||
# mysql password
|
||||
Password = "1234"
|
||||
# database name
|
||||
DBName = "ibex"
|
||||
# connection params
|
||||
Parameters = "charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
|
||||
|
||||
[Postgres]
|
||||
# pg address host:port
|
||||
Address = "127.0.0.1:5432"
|
||||
# pg user
|
||||
User = "root"
|
||||
# pg password
|
||||
Password = "1234"
|
||||
# database name
|
||||
DBName = "ibex"
|
||||
# ssl mode
|
||||
SSLMode = "disable"
|
||||
@@ -50,13 +50,11 @@ Enable = true
|
||||
# user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
[HTTP.APIForService]
|
||||
Enable = true
|
||||
Enable = false
|
||||
[HTTP.APIForService.BasicAuth]
|
||||
user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
[HTTP.JWTAuth]
|
||||
# signing key
|
||||
SigningKey = "5b94a0fd640fe2765af826acfe42d151"
|
||||
# unit: min
|
||||
AccessExpired = 1500
|
||||
# unit: min
|
||||
@@ -97,8 +95,6 @@ MaxLifetime = 7200
|
||||
MaxOpenConns = 150
|
||||
# max idle connections
|
||||
MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
# enable auto migrate or not
|
||||
# EnableAutoMigrate = false
|
||||
|
||||
@@ -182,3 +178,7 @@ MaxIdleConnsPerHost = 100
|
||||
# Regex = "([^:]+)(?::\\d+)?"
|
||||
# Replacement = "$1:80"
|
||||
# TargetLabel = "__address__"
|
||||
|
||||
[Ibex]
|
||||
Enable = true
|
||||
RPCListen = "0.0.0.0:20090"
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
network_mode: host
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
image: prom/prometheus:v2.55.1
|
||||
container_name: prometheus
|
||||
hostname: prometheus
|
||||
restart: always
|
||||
@@ -42,23 +42,6 @@ services:
|
||||
- "--enable-feature=remote-write-receiver"
|
||||
- "--query.lookback-delta=2m"
|
||||
|
||||
ibex:
|
||||
image: flashcatcloud/ibex:v0.5.0
|
||||
container_name: ibex
|
||||
hostname: ibex
|
||||
restart: always
|
||||
environment:
|
||||
GIN_MODE: release
|
||||
TZ: Asia/Shanghai
|
||||
WAIT_HOSTS: 127.0.0.1:3306
|
||||
volumes:
|
||||
- ./etc-ibex:/app/etc
|
||||
network_mode: host
|
||||
depends_on:
|
||||
- mysql
|
||||
command: >
|
||||
sh -c "/app/ibex server"
|
||||
|
||||
n9e:
|
||||
image: flashcatcloud/nightingale:latest
|
||||
container_name: n9e
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# debug, release
|
||||
RunMode = "release"
|
||||
|
||||
[Log]
|
||||
# log write dir
|
||||
Dir = "logs-server"
|
||||
# log level: DEBUG INFO WARNING ERROR
|
||||
Level = "DEBUG"
|
||||
# stdout, stderr, file
|
||||
Output = "stdout"
|
||||
# # rotate by time
|
||||
# KeepHours: 4
|
||||
# # rotate by size
|
||||
# RotateNum = 3
|
||||
# # unit: MB
|
||||
# RotateSize = 256
|
||||
|
||||
[HTTP]
|
||||
Enable = true
|
||||
# http listening address
|
||||
Host = "0.0.0.0"
|
||||
# http listening port
|
||||
Port = 10090
|
||||
# https cert file path
|
||||
CertFile = ""
|
||||
# https key file path
|
||||
KeyFile = ""
|
||||
# whether print access log
|
||||
PrintAccessLog = true
|
||||
# whether enable pprof
|
||||
PProf = false
|
||||
# http graceful shutdown timeout, unit: s
|
||||
ShutdownTimeout = 30
|
||||
# max content length: 64M
|
||||
MaxContentLength = 67108864
|
||||
# http server read timeout, unit: s
|
||||
ReadTimeout = 20
|
||||
# http server write timeout, unit: s
|
||||
WriteTimeout = 40
|
||||
# http server idle timeout, unit: s
|
||||
IdleTimeout = 120
|
||||
|
||||
[BasicAuth]
|
||||
# using when call apis
|
||||
ibex = "ibex"
|
||||
|
||||
[RPC]
|
||||
Listen = "0.0.0.0:20090"
|
||||
|
||||
[Heartbeat]
|
||||
# auto detect if blank
|
||||
IP = ""
|
||||
# unit: ms
|
||||
Interval = 1000
|
||||
|
||||
[Output]
|
||||
# database | remote
|
||||
ComeFrom = "database"
|
||||
AgtdPort = 2090
|
||||
|
||||
[Gorm]
|
||||
# enable debug mode or not
|
||||
Debug = false
|
||||
# mysql postgres
|
||||
DBType = "mysql"
|
||||
# unit: s
|
||||
MaxLifetime = 7200
|
||||
# max open connections
|
||||
MaxOpenConns = 150
|
||||
# max idle connections
|
||||
MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
|
||||
[MySQL]
|
||||
# mysql address host:port
|
||||
Address = "127.0.0.1:3306"
|
||||
# mysql username
|
||||
User = "root"
|
||||
# mysql password
|
||||
Password = "1234"
|
||||
# database name
|
||||
DBName = "ibex"
|
||||
# connection params
|
||||
Parameters = "charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
|
||||
|
||||
[Postgres]
|
||||
# pg address host:port
|
||||
Address = "127.0.0.1:5432"
|
||||
# pg user
|
||||
User = "root"
|
||||
# pg password
|
||||
Password = "1234"
|
||||
# database name
|
||||
DBName = "ibex"
|
||||
# ssl mode
|
||||
SSLMode = "disable"
|
||||
@@ -50,13 +50,11 @@ Enable = true
|
||||
# user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
[HTTP.APIForService]
|
||||
Enable = true
|
||||
Enable = false
|
||||
[HTTP.APIForService.BasicAuth]
|
||||
user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
[HTTP.JWTAuth]
|
||||
# signing key
|
||||
SigningKey = "5b94a0fd640fe2765af826acfe42d151"
|
||||
# unit: min
|
||||
AccessExpired = 1500
|
||||
# unit: min
|
||||
@@ -97,8 +95,6 @@ MaxLifetime = 7200
|
||||
MaxOpenConns = 150
|
||||
# max idle connections
|
||||
MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
# enable auto migrate or not
|
||||
# EnableAutoMigrate = false
|
||||
|
||||
@@ -182,3 +178,7 @@ MaxIdleConnsPerHost = 100
|
||||
# Regex = "([^:]+)(?::\\d+)?"
|
||||
# Replacement = "$1:80"
|
||||
# TargetLabel = "__address__"
|
||||
|
||||
[Ibex]
|
||||
Enable = true
|
||||
RPCListen = "0.0.0.0:20090"
|
||||
@@ -78,6 +78,6 @@ enable = true
|
||||
## ibex flush interval
|
||||
interval = "1000ms"
|
||||
## n9e ibex server rpc address
|
||||
servers = ["ibex:20090"]
|
||||
servers = ["nightingale:20090"]
|
||||
## temp script dir
|
||||
meta_dir = "./meta"
|
||||
|
||||
@@ -51,29 +51,6 @@ services:
|
||||
command:
|
||||
- "--loggerTimezone=Asia/Shanghai"
|
||||
|
||||
ibex:
|
||||
image: flashcatcloud/ibex:v1.2.0
|
||||
container_name: ibex
|
||||
hostname: ibex
|
||||
restart: always
|
||||
environment:
|
||||
GIN_MODE: release
|
||||
TZ: Asia/Shanghai
|
||||
WAIT_HOSTS: postgres:5432
|
||||
ports:
|
||||
- "10090:10090"
|
||||
- "20090:20090"
|
||||
volumes:
|
||||
- ./ibexetc_pg:/app/etc
|
||||
networks:
|
||||
- nightingale
|
||||
depends_on:
|
||||
- postgres
|
||||
links:
|
||||
- postgres:postgres
|
||||
command: >
|
||||
sh -c "/app/ibex server"
|
||||
|
||||
nightingale:
|
||||
image: flashcatcloud/nightingale:latest
|
||||
container_name: nightingale
|
||||
@@ -93,12 +70,10 @@ services:
|
||||
- postgres
|
||||
- redis
|
||||
- victoriametrics
|
||||
- ibex
|
||||
links:
|
||||
- postgres:postgres
|
||||
- redis:redis
|
||||
- victoriametrics:victoriametrics
|
||||
- ibex:ibex
|
||||
command: >
|
||||
sh -c "/app/n9e"
|
||||
|
||||
@@ -112,7 +87,7 @@ services:
|
||||
HOST_PROC: /hostfs/proc
|
||||
HOST_SYS: /hostfs/sys
|
||||
HOST_MOUNT_PREFIX: /hostfs
|
||||
WAIT_HOSTS: nightingale:17000, ibex:20090
|
||||
WAIT_HOSTS: nightingale:17000, nightingale:20090
|
||||
volumes:
|
||||
- ./categraf/conf:/etc/categraf/conf
|
||||
- /:/hostfs
|
||||
@@ -124,7 +99,5 @@ services:
|
||||
- nightingale
|
||||
depends_on:
|
||||
- nightingale
|
||||
- ibex
|
||||
links:
|
||||
- nightingale:nightingale
|
||||
- ibex:ibex
|
||||
- nightingale:nightingale
|
||||
@@ -1,97 +0,0 @@
|
||||
# debug, release
|
||||
RunMode = "release"
|
||||
|
||||
[Log]
|
||||
# log write dir
|
||||
Dir = "logs-server"
|
||||
# log level: DEBUG INFO WARNING ERROR
|
||||
Level = "DEBUG"
|
||||
# stdout, stderr, file
|
||||
Output = "stdout"
|
||||
# # rotate by time
|
||||
# KeepHours: 4
|
||||
# # rotate by size
|
||||
# RotateNum = 3
|
||||
# # unit: MB
|
||||
# RotateSize = 256
|
||||
|
||||
[HTTP]
|
||||
Enable = true
|
||||
# http listening address
|
||||
Host = "0.0.0.0"
|
||||
# http listening port
|
||||
Port = 10090
|
||||
# https cert file path
|
||||
CertFile = ""
|
||||
# https key file path
|
||||
KeyFile = ""
|
||||
# whether print access log
|
||||
PrintAccessLog = true
|
||||
# whether enable pprof
|
||||
PProf = false
|
||||
# http graceful shutdown timeout, unit: s
|
||||
ShutdownTimeout = 30
|
||||
# max content length: 64M
|
||||
MaxContentLength = 67108864
|
||||
# http server read timeout, unit: s
|
||||
ReadTimeout = 20
|
||||
# http server write timeout, unit: s
|
||||
WriteTimeout = 40
|
||||
# http server idle timeout, unit: s
|
||||
IdleTimeout = 120
|
||||
|
||||
[BasicAuth]
|
||||
# using when call apis
|
||||
ibex = "ibex"
|
||||
|
||||
[RPC]
|
||||
Listen = "0.0.0.0:20090"
|
||||
|
||||
[Heartbeat]
|
||||
# auto detect if blank
|
||||
IP = ""
|
||||
# unit: ms
|
||||
Interval = 1000
|
||||
|
||||
[Output]
|
||||
# database | remote
|
||||
ComeFrom = "database"
|
||||
AgtdPort = 2090
|
||||
|
||||
[Gorm]
|
||||
# enable debug mode or not
|
||||
Debug = false
|
||||
# mysql postgres
|
||||
DBType = "postgres"
|
||||
# unit: s
|
||||
MaxLifetime = 7200
|
||||
# max open connections
|
||||
MaxOpenConns = 150
|
||||
# max idle connections
|
||||
MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
|
||||
[MySQL]
|
||||
# mysql address host:port
|
||||
Address = "mysql:3306"
|
||||
# mysql username
|
||||
User = "root"
|
||||
# mysql password
|
||||
Password = "1234"
|
||||
# database name
|
||||
DBName = "ibex"
|
||||
# connection params
|
||||
Parameters = "charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
|
||||
|
||||
[Postgres]
|
||||
# pg address host:port
|
||||
Address = "postgres:5432"
|
||||
# pg user
|
||||
User = "root"
|
||||
# pg password
|
||||
Password = "1234"
|
||||
# database name
|
||||
DBName = "n9e_v6"
|
||||
# ssl mode
|
||||
SSLMode = "disable"
|
||||
@@ -1,26 +1,31 @@
|
||||
CREATE TABLE users (
|
||||
id bigserial,
|
||||
username varchar(64) not null ,
|
||||
nickname varchar(64) not null ,
|
||||
username varchar(64) not null,
|
||||
nickname varchar(64) not null,
|
||||
password varchar(128) not null default '',
|
||||
phone varchar(16) not null default '',
|
||||
email varchar(64) not null default '',
|
||||
portrait varchar(255) not null default '' ,
|
||||
roles varchar(255) not null ,
|
||||
contacts varchar(1024) ,
|
||||
maintainer smallint not null default 0,
|
||||
portrait varchar(255) not null default '',
|
||||
roles varchar(255) not null,
|
||||
contacts varchar(1024),
|
||||
maintainer int not null default 0,
|
||||
belong varchar(16) not null default '',
|
||||
last_active_time bigint 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,
|
||||
update_by varchar(64) not null default '',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (username)
|
||||
) ;
|
||||
COMMENT ON COLUMN users.username IS 'login name, cannot rename';
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN users.id IS 'id';
|
||||
COMMENT ON COLUMN users.username IS 'login name, cannot rename';
|
||||
COMMENT ON COLUMN users.nickname IS 'display name, chinese name';
|
||||
COMMENT ON COLUMN users.portrait IS 'portrait image url';
|
||||
COMMENT ON COLUMN users.roles IS 'Admin | Standard | Guest, split by space';
|
||||
COMMENT ON COLUMN users.contacts IS 'json e.g. {wecom:xx, dingtalk_robot_token:yy}';
|
||||
COMMENT ON COLUMN users.belong IS 'belong';
|
||||
|
||||
insert into users(id, username, nickname, password, roles, create_at, create_by, update_at, update_by) values(1, 'root', '超管', 'root.2020', 'Admin', date_part('epoch',current_timestamp)::int, 'system', date_part('epoch',current_timestamp)::int, 'system');
|
||||
|
||||
@@ -54,9 +59,16 @@ CREATE TABLE configs (
|
||||
id bigserial,
|
||||
ckey varchar(191) not null,
|
||||
cval text not null default '',
|
||||
note varchar(1024) not null default '',
|
||||
external int not null default 0,
|
||||
encrypted int 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,
|
||||
update_by varchar(64) not null default '',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (ckey)
|
||||
) ;
|
||||
);
|
||||
|
||||
CREATE TABLE role (
|
||||
id bigserial,
|
||||
@@ -366,32 +378,37 @@ COMMENT ON COLUMN alert_mute.disabled IS '0:enabled 1:disabled';
|
||||
CREATE TABLE alert_subscribe (
|
||||
id bigserial,
|
||||
name varchar(255) not null default '',
|
||||
disabled smallint not null default 0 ,
|
||||
group_id bigint not null default 0 ,
|
||||
disabled int not null default 0,
|
||||
group_id bigint not null default 0,
|
||||
prod varchar(255) not null default '',
|
||||
cate varchar(128) not null,
|
||||
datasource_ids varchar(255) not null default '' ,
|
||||
datasource_ids varchar(255) not null default '',
|
||||
cluster varchar(128) not null,
|
||||
rule_id bigint not null default 0,
|
||||
severities varchar(32) not null default '',
|
||||
tags varchar(4096) not null default '[]',
|
||||
redefine_severity smallint default 0 ,
|
||||
new_severity smallint not null ,
|
||||
new_severity smallint not null,
|
||||
redefine_channels smallint default 0 ,
|
||||
new_channels varchar(255) not null default '' ,
|
||||
user_group_ids varchar(250) not null ,
|
||||
new_channels varchar(255) not null default '',
|
||||
user_group_ids varchar(250) not null,
|
||||
busi_groups VARCHAR(4096) NOT NULL DEFAULT '[]',
|
||||
note VARCHAR(1024) DEFAULT '',
|
||||
rule_ids VARCHAR(1024) DEFAULT '',
|
||||
webhooks text not null,
|
||||
extra_config text not null,
|
||||
redefine_webhooks smallint default 0,
|
||||
redefine_webhooks int default 0,
|
||||
for_duration bigint 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,
|
||||
update_by varchar(64) not null default '',
|
||||
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);
|
||||
);
|
||||
|
||||
CREATE INDEX ON alert_subscribe (update_at);
|
||||
CREATE INDEX ON alert_subscribe (group_id);
|
||||
|
||||
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.datasource_ids IS 'datasource ids';
|
||||
@@ -401,25 +418,34 @@ COMMENT ON COLUMN alert_subscribe.new_severity IS '0:Emergency 1:Warning 2:Notic
|
||||
COMMENT ON COLUMN alert_subscribe.redefine_channels IS 'is redefine channels?';
|
||||
COMMENT ON COLUMN alert_subscribe.new_channels IS 'split by space: sms voice email dingtalk wecom';
|
||||
COMMENT ON COLUMN alert_subscribe.user_group_ids IS 'split by space 1 34 5, notify cc to user_group_ids';
|
||||
COMMENT ON COLUMN alert_subscribe.note IS 'note';
|
||||
COMMENT ON COLUMN alert_subscribe.rule_ids IS 'rule_ids';
|
||||
COMMENT ON COLUMN alert_subscribe.extra_config IS 'extra_config';
|
||||
|
||||
|
||||
CREATE TABLE target (
|
||||
id bigserial,
|
||||
group_id bigint not null default 0 ,
|
||||
ident varchar(191) not null ,
|
||||
note varchar(255) not null default '' ,
|
||||
tags varchar(512) not null default '' ,
|
||||
group_id bigint not null default 0,
|
||||
ident varchar(191) not null,
|
||||
note varchar(255) not null default '',
|
||||
tags varchar(512) not null default '',
|
||||
host_ip varchar(15) default '',
|
||||
agent_version varchar(255) default '',
|
||||
engine_name varchar(255) default '',
|
||||
update_at bigint not null default 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (ident)
|
||||
) ;
|
||||
CREATE INDEX target_group_id_idx ON target (group_id);
|
||||
);
|
||||
|
||||
CREATE INDEX ON target (group_id);
|
||||
|
||||
COMMENT ON COLUMN target.group_id IS 'busi group id';
|
||||
COMMENT ON COLUMN target.ident IS 'target id';
|
||||
COMMENT ON COLUMN target.note IS 'append to alert event as field';
|
||||
COMMENT ON COLUMN target.tags IS 'append to series data as tags, split by space, append external space at suffix';
|
||||
|
||||
COMMENT ON COLUMN target.host_ip IS 'IPv4 string';
|
||||
COMMENT ON COLUMN target.agent_version IS 'agent version';
|
||||
COMMENT ON COLUMN target.engine_name IS 'engine_name';
|
||||
-- case1: target_idents; case2: target_tags
|
||||
-- CREATE TABLE collect_rule (
|
||||
-- id bigserial,
|
||||
@@ -535,7 +561,7 @@ CREATE TABLE alert_cur_event (
|
||||
target_note varchar(191) not null default '' ,
|
||||
first_trigger_time bigint,
|
||||
trigger_time bigint not null,
|
||||
trigger_value varchar(255) not null,
|
||||
trigger_value varchar(2048) not null,
|
||||
annotations text not null ,
|
||||
rule_config text not null ,
|
||||
tags varchar(1024) not null default '' ,
|
||||
@@ -595,7 +621,7 @@ CREATE TABLE alert_his_event (
|
||||
target_note varchar(191) not null default '' ,
|
||||
first_trigger_time bigint,
|
||||
trigger_time bigint not null,
|
||||
trigger_value varchar(255) not null,
|
||||
trigger_value varchar(2048) not null,
|
||||
recover_time bigint not null default 0,
|
||||
last_eval_time bigint not null default 0 ,
|
||||
tags varchar(1024) not null default '' ,
|
||||
@@ -606,6 +632,7 @@ CREATE TABLE alert_his_event (
|
||||
CREATE INDEX alert_his_event_hash_idx ON alert_his_event (hash);
|
||||
CREATE INDEX alert_his_event_rule_id_idx ON alert_his_event (rule_id);
|
||||
CREATE INDEX alert_his_event_tg_idx ON alert_his_event (trigger_time, group_id);
|
||||
CREATE INDEX alert_his_event_nrn_idx ON alert_his_event (last_eval_time);
|
||||
COMMENT ON COLUMN alert_his_event.group_id IS 'busi group id of rule';
|
||||
COMMENT ON COLUMN alert_his_event.datasource_id IS 'datasource id';
|
||||
COMMENT ON COLUMN alert_his_event.group_name IS 'busi group name';
|
||||
@@ -717,6 +744,7 @@ CREATE TABLE datasource
|
||||
status varchar(255) not null default '',
|
||||
http varchar(4096) not null default '',
|
||||
auth varchar(8192) not null default '',
|
||||
is_default boolean not null default false,
|
||||
created_at bigint not null default 0,
|
||||
created_by varchar(64) not null default '',
|
||||
updated_at bigint not null default 0,
|
||||
@@ -737,17 +765,22 @@ CREATE TABLE notify_tpl (
|
||||
channel varchar(32) not null,
|
||||
name varchar(255) not null,
|
||||
content text not null,
|
||||
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 (channel)
|
||||
) ;
|
||||
);
|
||||
|
||||
CREATE TABLE sso_config (
|
||||
id bigserial,
|
||||
name varchar(191) not null,
|
||||
content text not null,
|
||||
update_at bigint not null default 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (name)
|
||||
) ;
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE es_index_pattern (
|
||||
@@ -765,3 +798,91 @@ CREATE TABLE es_index_pattern (
|
||||
UNIQUE (datasource_id, name)
|
||||
) ;
|
||||
COMMENT ON COLUMN es_index_pattern.datasource_id IS 'datasource id';
|
||||
|
||||
CREATE TABLE builtin_metrics (
|
||||
id bigserial,
|
||||
collector varchar(191) NOT NULL,
|
||||
typ varchar(191) NOT NULL,
|
||||
name varchar(191) NOT NULL,
|
||||
unit varchar(191) NOT NULL,
|
||||
lang varchar(191) NOT NULL DEFAULT '',
|
||||
note varchar(4096) NOT NULL,
|
||||
expression varchar(4096) NOT NULL,
|
||||
created_at bigint NOT NULL DEFAULT 0,
|
||||
created_by varchar(191) NOT NULL DEFAULT '',
|
||||
updated_at bigint NOT NULL DEFAULT 0,
|
||||
updated_by varchar(191) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (lang, collector, typ, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_collector ON builtin_metrics (collector);
|
||||
CREATE INDEX idx_typ ON builtin_metrics (typ);
|
||||
CREATE INDEX idx_name ON builtin_metrics (name);
|
||||
CREATE INDEX idx_lang ON builtin_metrics (lang);
|
||||
|
||||
COMMENT ON COLUMN builtin_metrics.id IS 'unique identifier';
|
||||
COMMENT ON COLUMN builtin_metrics.collector IS 'type of collector';
|
||||
COMMENT ON COLUMN builtin_metrics.typ IS 'type of metric';
|
||||
COMMENT ON COLUMN builtin_metrics.name IS 'name of metric';
|
||||
COMMENT ON COLUMN builtin_metrics.unit IS 'unit of metric';
|
||||
COMMENT ON COLUMN builtin_metrics.lang IS 'language of metric';
|
||||
COMMENT ON COLUMN builtin_metrics.note IS 'description of metric in Chinese';
|
||||
COMMENT ON COLUMN builtin_metrics.expression IS 'expression of metric';
|
||||
COMMENT ON COLUMN builtin_metrics.created_at IS 'create time';
|
||||
COMMENT ON COLUMN builtin_metrics.created_by IS 'creator';
|
||||
COMMENT ON COLUMN builtin_metrics.updated_at IS 'update time';
|
||||
COMMENT ON COLUMN builtin_metrics.updated_by IS 'updater';
|
||||
|
||||
CREATE TABLE metric_filter (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(191) NOT NULL,
|
||||
configs VARCHAR(4096) NOT NULL,
|
||||
groups_perm TEXT,
|
||||
create_at BIGINT NOT NULL DEFAULT 0,
|
||||
create_by VARCHAR(191) NOT NULL DEFAULT '',
|
||||
update_at BIGINT NOT NULL DEFAULT 0,
|
||||
update_by VARCHAR(191) NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE INDEX idx_metric_filter_name ON metric_filter (name);
|
||||
|
||||
CREATE TABLE board_busigroup (
|
||||
busi_group_id BIGINT NOT NULL DEFAULT 0,
|
||||
board_id BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (busi_group_id, board_id)
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE builtin_components (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
ident VARCHAR(191) NOT NULL,
|
||||
logo VARCHAR(191) NOT NULL,
|
||||
readme TEXT NOT NULL,
|
||||
created_at BIGINT NOT NULL DEFAULT 0,
|
||||
created_by VARCHAR(191) NOT NULL DEFAULT '',
|
||||
updated_at BIGINT NOT NULL DEFAULT 0,
|
||||
updated_by VARCHAR(191) NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ident ON builtin_components (ident);
|
||||
|
||||
CREATE TABLE builtin_payloads (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type VARCHAR(191) NOT NULL,
|
||||
uuid BIGINT NOT NULL DEFAULT 0,
|
||||
component VARCHAR(191) NOT NULL,
|
||||
cate VARCHAR(191) NOT NULL,
|
||||
name VARCHAR(191) NOT NULL,
|
||||
tags VARCHAR(191) NOT NULL DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
created_at BIGINT NOT NULL DEFAULT 0,
|
||||
created_by VARCHAR(191) NOT NULL DEFAULT '',
|
||||
updated_at BIGINT NOT NULL DEFAULT 0,
|
||||
updated_by VARCHAR(191) NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE INDEX idx_component ON builtin_payloads (component);
|
||||
CREATE INDEX idx_builtin_payloads_name ON builtin_payloads (name);
|
||||
CREATE INDEX idx_cate ON builtin_payloads (cate);
|
||||
CREATE INDEX idx_type ON builtin_payloads (type);
|
||||
@@ -50,13 +50,11 @@ Enable = true
|
||||
# user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
[HTTP.APIForService]
|
||||
Enable = true
|
||||
Enable = false
|
||||
[HTTP.APIForService.BasicAuth]
|
||||
user001 = "ccc26da7b9aba533cbb263a36c07dcc5"
|
||||
|
||||
[HTTP.JWTAuth]
|
||||
# signing key
|
||||
SigningKey = "5b94a0fd640fe2765af826acfe42d151"
|
||||
# unit: min
|
||||
AccessExpired = 1500
|
||||
# unit: min
|
||||
@@ -92,8 +90,6 @@ MaxLifetime = 7200
|
||||
MaxOpenConns = 150
|
||||
# max idle connections
|
||||
MaxIdleConns = 50
|
||||
# table prefix
|
||||
TablePrefix = ""
|
||||
# enable auto migrate or not
|
||||
# EnableAutoMigrate = false
|
||||
|
||||
@@ -177,3 +173,7 @@ MaxIdleConnsPerHost = 100
|
||||
# Regex = "([^:]+)(?::\\d+)?"
|
||||
# Replacement = "$1:80"
|
||||
# TargetLabel = "__address__"
|
||||
|
||||
[Ibex]
|
||||
Enable = true
|
||||
RPCListen = "0.0.0.0:20090"
|
||||
@@ -1,11 +1,25 @@
|
||||
#### {{if .IsRecovered}}<font color="#008800">S{{.Severity}} - Recovered - {{.RuleName}}</font>{{else}}<font color="#FF0000">S{{.Severity}} - Triggered - {{.RuleName}}</font>{{end}}
|
||||
#### {{if .IsRecovered}}<font color="#008800">💚{{.RuleName}}</font>{{else}}<font color="#FF0000">💔{{.RuleName}}</font>{{end}}
|
||||
|
||||
---
|
||||
|
||||
- **规则标题**: {{.RuleName}}{{if .RuleNote}}
|
||||
- **规则备注**: {{.RuleNote}}{{end}}
|
||||
- **监控指标**: {{.TagsJSON}}
|
||||
- {{if .IsRecovered}}**恢复时间**:{{timeformat .LastEvalTime}}{{else}}**触发时间**: {{timeformat .TriggerTime}}
|
||||
- **触发时值**: {{.TriggerValue}}{{end}}
|
||||
- **发送时间**: {{timestamp}}
|
||||
|
||||
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}
|
||||
- **告警级别**: {{.Severity}}级
|
||||
{{- if .RuleNote}}
|
||||
- **规则备注**: {{.RuleNote}}
|
||||
{{- end}}
|
||||
{{- if not .IsRecovered}}
|
||||
- **当次触发时值**: {{.TriggerValue}}
|
||||
- **当次触发时间**: {{timeformat .TriggerTime}}
|
||||
- **告警持续时长**: {{humanizeDurationInterface $time_duration}}
|
||||
{{- else}}
|
||||
{{- if .AnnotationsJSON.recovery_value}}
|
||||
- **恢复时值**: {{formatDecimal .AnnotationsJSON.recovery_value 4}}
|
||||
{{- end}}
|
||||
- **恢复时间**: {{timeformat .LastEvalTime}}
|
||||
- **告警持续时长**: {{humanizeDurationInterface $time_duration}}
|
||||
{{- end}}
|
||||
- **告警事件标签**:
|
||||
{{- range $key, $val := .TagsMap}}
|
||||
{{- if ne $key "rulename" }}
|
||||
- `{{$key}}`: `{{$val}}`
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user