Compare commits

...

202 Commits

Author SHA1 Message Date
ning
445b6b2e41 refactor: build event tags 2025-05-28 16:10:22 +08:00
ning
fcf07df35f refactor: datasource init add recover 2025-05-28 14:13:54 +08:00
ning
196e5241bf refactor: handle ibex 2025-05-28 13:13:09 +08:00
ning
0d7ee0cfd9 fix: ibex after event relabel 2025-05-28 12:24:48 +08:00
ning
aed6927d7e refactor: update send duty 2025-05-27 15:50:26 +08:00
ning
61b9ebb1e6 Merge branch 'release-16' of github.com:ccfos/nightingale into release-16 2025-05-26 19:57:42 +08:00
ning
da27ca06d6 refactor: update relabel processor 2025-05-26 19:57:00 +08:00
shardingHe
776f100eb9 docs: add config for ntp (#2691) 2025-05-21 16:25:05 +08:00
Yening Qin
ac8443fd66 feat: add event pipeline (#2680) 2025-05-15 20:17:27 +08:00
Yening Qin
751d2e8601 fix: alert rule verify (#2667) 2025-05-13 18:46:55 +08:00
Yening Qin
87855cb184 fix: default ds id update (#2665) 2025-05-13 15:44:52 +08:00
Yening Qin
9ddeefea9b update k8s dashboards (#2662) 2025-05-13 14:56:36 +08:00
Yening Qin
6b566f5a18 add offset, index-pattern-ops, log-query-api (#2656) 2025-05-12 15:38:19 +08:00
smx_Morgan
a828603406 Fix: Fixed the issue of group synchronization flashduty (#2628) 2025-04-25 15:34:08 +08:00
Yening Qin
c5c4e00ab8 refactor: change query api (#2626) 2025-04-24 14:20:36 +08:00
ning
770e15db39 refactor: datasource model add identifier 2025-04-23 16:13:36 +08:00
ning
5096117b45 refactor: update api for agent auth 2025-04-23 15:53:40 +08:00
Yening Qin
dd3b68e4ab refactor: change notify channel api auth 2025-04-23 15:47:20 +08:00
Yening Qin
85947c08a8 refactor: sync user info to duty (#2615) 2025-04-18 17:07:54 +08:00
Ulric Qin
3f3c815171 set QueueWaterMark to 0.1 2025-04-17 19:30:21 +08:00
smx_Morgan
08f82e899a feat: support user get by emails and phones (#2613) 2025-04-16 19:07:07 +08:00
bowen
043628d4eb feat: add usernames query param for /api/n9e/users 2025-04-16 12:10:39 +08:00
smx_Morgan
ba33512d22 fix: prevent incorrect board matches caused by implicit type casting 2025-04-15 13:21:58 +08:00
YR Chen
a7cf658c1d refactor: allow valid Go template as rule name (#2549)
* fix: allow valid Go template as rule name

* feat: add back `str.Dangerous` check for texts in template

* Update models/alert_rule.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Yening Qin <710leo@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-14 18:28:42 +08:00
smx_Morgan
b62e6fda04 feat: return value add "avtivate" when get alert-mute (#2599) 2025-04-14 18:09:32 +08:00
Yening Qin
6243f9a05c refactor: update es event index (#2602) 2025-04-14 14:46:15 +08:00
Yening Qin
e8962b5646 refactor: change query tpl func (#2597) 2025-04-11 16:10:25 +08:00
ning
97a4ee2764 es_index_pattern add note 2025-04-10 17:28:06 +08:00
rechardwang
2fdb80f314 docs: add chinese verion rabbitmq dashboard 2025-04-08 22:07:24 +08:00
710leo
c0ab672cf7 change createTokens 2025-04-03 00:19:33 +08:00
Yening Qin
7664c15121 refactor: change get ids (#2583) 2025-04-02 18:50:01 +08:00
Ulric Qin
4059a2022c add logic: SingleLogin 2025-04-02 15:10:31 +08:00
smx_Morgan
e7263680a8 refactor: targets get api 2025-04-01 20:56:40 +08:00
alingse
4a67f7a108 fix: call errors.WithMessage with a nil value error err after check another error decryptErr (#2572) 2025-04-01 17:33:25 +08:00
ning
04ca6c5fd5 fix: auto migrate add cross_cluster_enabled of es_index_pattern 2025-03-27 15:45:47 +08:00
Ulric Qin
747211c78f fix builtin node-exporter-alerting-rules 2025-03-27 08:35:56 +08:00
ning
bf54fac1e8 refactor: change send script notify log 2025-03-26 16:09:35 +08:00
ning
76117ae440 refactor: change send script notify log 2025-03-26 16:00:02 +08:00
Yening Qin
9ad02075c6 refactor: es query add IgnoreUnavailable(true) (#2566) 2025-03-26 11:05:45 +08:00
ning
6d27ff673f docs: add migrate sql 2025-03-25 15:15:53 +08:00
kugarocks
ee4e2b3f7d chore: delete rule worker severity var (#2539) 2025-03-21 18:54:26 +08:00
smx_Morgan
e6de301c65 feat: verify host before add task (#2544)
Co-authored-by: Yening Qin <710leo@gmail.com>
2025-03-21 18:48:06 +08:00
Ulric Qin
d4f5871fba Merge branch 'main' of github.com:ccfos/nightingale 2025-03-21 18:39:05 +08:00
Ulric Qin
c2e61f3741 delete no use configurations in docker directory 2025-03-21 18:38:46 +08:00
Yening Qin
d26df3b331 refactor: es query add log (#2560) 2025-03-21 18:37:45 +08:00
Yening Qin
391c674d21 refactor: add metrics and change crontab (#2559) 2025-03-21 16:29:08 +08:00
Ulric Qin
b95457ee9c update n9e-v8 dashboard 2025-03-20 19:06:43 +08:00
Ulric Qin
09179b004c refactor pushgw stats 2025-03-20 18:54:23 +08:00
Ulric Qin
274de9b994 delete no use code 2025-03-20 18:42:06 +08:00
Ulric Qin
7fcb9f7e4a add n9e.v8 dashboard 2025-03-20 18:38:06 +08:00
Ulric Qin
06ca3c2579 refactor default settings of QueueNumber 2025-03-20 17:25:17 +08:00
Ulric Qin
68509a9ed4 refactor default settings of QueueNumber 2025-03-20 17:20:35 +08:00
Ulric Qin
ea88def18c refactor configurations 2025-03-20 17:16:52 +08:00
Ulric Qin
a22fded16f refactor pushgw queue logic 2025-03-20 17:12:13 +08:00
Ulric Qin
490dc62dad fix: init pushgw http transport 2025-03-20 13:43:58 +08:00
Ulric Qin
47dbe5f2e2 Merge branch 'main' of github.com:ccfos/nightingale 2025-03-20 11:41:46 +08:00
Ulric Qin
596ee8b26d refactor writer http transport initilization 2025-03-20 11:41:19 +08:00
Yening Qin
677bf50293 refactor: change migrate bg (#2550) 2025-03-18 23:16:03 +08:00
ning
99cc397290 fix: email notify channel send muti people 2025-03-17 15:42:09 +08:00
ning
938299a539 fix: edge panic where query with var 2025-03-17 15:20:07 +08:00
ning
f44964c876 refactor: change notify rule test api 2025-03-17 14:45:36 +08:00
ning
f284baf139 feat: add callback notify channel 2025-03-17 14:37:06 +08:00
Yening Qin
17495c8e01 feat: add slack notify channel (#2543) 2025-03-17 12:22:17 +08:00
ning
58100f9924 Merge branch 'main' of github.com:ccfos/nightingale 2025-03-14 17:37:11 +08:00
ning
13a7d64499 docs: init sql add index 2025-03-14 17:36:58 +08:00
kugarocks
94102e8fbc typo: rename relaodTpls to reloadTpls (#2541) 2025-03-14 17:18:07 +08:00
smx_Morgan
2d6e066d54 feat: allow clone alert rules (#2542) 2025-03-14 17:06:46 +08:00
Ulric Qin
a553aa5f78 refactor pushgw: add more queue for metric prefix 2025-03-14 17:03:50 +08:00
ning
4a50ae9ef1 add comment 2025-03-14 12:27:13 +08:00
ning
a86f5d7996 refactor: change queue full log 2025-03-14 11:54:25 +08:00
小炒肉
728af57d8e add feishuapp notify channel (#2537)
Co-authored-by: zhihuanzhu <zhihuanzhu@deeproute.ai>
Co-authored-by: Yening Qin <710leo@gmail.com>
2025-03-13 19:47:38 +08:00
ning
5c02fc64b8 fix: get user contacts 2025-03-13 12:15:52 +08:00
ning
d890476e5a docs: update go.mod 2025-03-13 10:29:15 +08:00
ning
c2af8b1064 refactor: delete script stdin json.sendto 2025-03-12 20:08:28 +08:00
ning
e64629dafd Merge branch 'main' of github.com:ccfos/nightingale 2025-03-12 19:57:41 +08:00
ning
9bcddf3457 fix ali sms notify 2025-03-12 19:57:27 +08:00
ningblue
2ea820645a docs: update migrate.sql (#2535) 2025-03-12 16:36:45 +08:00
Ulric Qin
70b7ed35b4 Merge branch 'main' of github.com:ccfos/nightingale 2025-03-12 12:23:42 +08:00
Ulric Qin
b4603dc012 code refactor 2025-03-12 12:23:28 +08:00
ning
9360433f96 change notify channel init 2025-03-11 21:02:06 +08:00
ning
3346a4aa29 update notify channel init 2025-03-11 19:29:29 +08:00
ning
7c7a560c55 add git commit -m 2025-03-11 18:18:39 +08:00
ning
88c5a7bbef add channel idents list api 2025-03-11 18:14:46 +08:00
ning
76654b64e7 refactor: add notify_channel_ident 2025-03-11 18:04:32 +08:00
ning
4648b16106 chore: ignore front/statik/statik.go 2025-03-11 16:56:34 +08:00
ning
0bec5b55c5 change user.ExtractToken 2025-03-11 16:49:24 +08:00
ning
3744e396c6 refactor: notify test api 2025-03-11 15:30:02 +08:00
ning
947365c5f3 refactor: change msg tpl 2025-03-11 15:27:52 +08:00
ning
71f8d6b1cb refactor: send http 2025-03-11 15:13:12 +08:00
ning
15a263f525 refactor: notify test api 2025-03-11 12:21:20 +08:00
ning
f3cc0e5b57 fix: load event from db 2025-03-11 11:30:19 +08:00
ning
6e15c88e26 refactor: change feishu tpl 2025-03-11 10:40:50 +08:00
710leo
ed37299118 refactor: send script 2025-03-11 00:19:03 +08:00
710leo
ec7c72d68c add jsonMarshal tpl func 2025-03-11 00:15:37 +08:00
710leo
20e986091b refactor: tpl init 2025-03-11 00:02:55 +08:00
710leo
f78e92f253 refactor: NotifyContact api 2025-03-10 23:46:58 +08:00
ning
94d6c3a075 fix: aliyun sms and voice send 2025-03-10 23:19:01 +08:00
ning
b830622cbf fix: notify rule get user group 2025-03-10 19:59:11 +08:00
ning
ba63f512c3 refactor: send script 2025-03-10 19:39:12 +08:00
ning
c3db7d0d51 refactor: migrate and alert_subscribe notify rule 2025-03-10 16:43:50 +08:00
ning
c0d0d48a83 fix: get notify rule server api 2025-03-10 13:16:40 +08:00
ning
e22103ff7f refactor: change msg tpl 2025-03-09 23:55:02 +08:00
ning
31362e41d5 refactor: notify test 2025-03-08 19:40:27 +08:00
Yening Qin
00b502579d feat: add Discord notifications (#2519)
Co-authored-by: smx_Morgan <86641888+smx-Morgan@users.noreply.github.com>
2025-03-08 19:13:05 +08:00
Yening Qin
52d032b6f5 refactor: optimize dingtalk html (#2522) 2025-03-08 19:09:10 +08:00
Yening Qin
9026736acb refactor: support dingtalk ats by phone (#2521) 2025-03-08 12:41:27 +08:00
NinaLua
8ceea820db chore: change some comments (#2517) 2025-03-07 19:05:35 +08:00
Yening Qin
0686ea4fe7 fix add self metric (#2515) 2025-03-07 14:38:37 +08:00
Yening Qin
d1ea3ed450 refactor: change notify tpl init (#2513) 2025-03-07 12:29:31 +08:00
Yening Qin
0c6558f92f change notify channel init (#2511) 2025-03-07 11:54:10 +08:00
Yening Qin
446da9b8cb refactor: add self metrics (#2509) 2025-03-07 10:43:10 +08:00
710leo
8612a53ded refactor: event notify 2025-03-05 21:13:07 +08:00
Yening Qin
52b7890eac refactor: update notify channel api (#2508) 2025-03-05 18:03:06 +08:00
Yening Qin
0166405069 add contact perm (#2506) 2025-03-05 11:28:43 +08:00
Yening Qin
863b2f6659 refactor: change some noitfy api (#2505) 2025-03-04 20:35:31 +08:00
ning
e39cdabd8d refactor notify test api 2025-03-04 19:01:17 +08:00
ning
a5b4b09619 add service api 2025-03-04 17:49:56 +08:00
ning
8690a28619 refactor: notify rule add api 2025-03-04 16:37:34 +08:00
ning
0142cc36e6 refactor: notify rule api perm 2025-03-04 15:42:38 +08:00
ning
1fbe0889f6 refactor: index_pattern query 2025-03-04 14:46:32 +08:00
Yening Qin
f384a9a235 alert rule support index pattern (#2491) 2025-03-04 14:32:27 +08:00
ning
2d21249856 update ops 2025-03-04 14:28:04 +08:00
ning
69e58f53f3 refactor: alert subscribe api 2025-03-04 14:09:02 +08:00
Yening Qin
ab41eb58fa refactor notify rule (#2501) 2025-03-03 20:50:46 +08:00
Yening Qin
7fd415d7f7 feat: support notify rule (#2500)
Co-authored-by: flashbo <36443248+lwb0214@users.noreply.github.com>
Co-authored-by: Xu Bin <140785332+Reditiny@users.noreply.github.com>
2025-03-03 19:59:01 +08:00
smx_Morgan
f7401b7b40 refactor: read config disable_integration_init to decide skip init integration (#2492) 2025-02-26 19:22:49 +08:00
ning
ef0430052a refactor: ensure proper UTF-8 character boundaries when truncating script output 2025-02-25 12:10:13 +08:00
ning
ab49b13596 fix: add recording rule 2025-02-25 11:20:01 +08:00
Yening Qin
b727c36b2a refactor: async alert notification record (#2474) (#2482)
Co-authored-by: smx_Morgan <86641888+smx-Morgan@users.noreply.github.com>
2025-02-24 14:13:12 +08:00
smx_Morgan
154c44b63e feat: support recording script alert results (#2469) 2025-02-18 13:26:42 +08:00
ning
91a8afbf1c docs: update config.toml add token auth 2025-02-14 15:00:20 +08:00
Ulric Qin
a7207cf4e1 set ibex.Enable to true by default 2025-02-13 16:36:10 +08:00
Yening Qin
bd6d1cf88d feat: support push data to kafka (#2463)
Co-authored-by: flashbo <36443248+lwb0214@users.noreply.github.com>
2025-02-12 15:47:00 +08:00
ning
12382b3b0e refactor: change new user token 2025-02-11 15:30:25 +08:00
ning
4803fa628b refactor: add user token api 2025-02-11 14:54:27 +08:00
ning
992f62cbf5 refactor: add token last used time 2025-02-11 10:58:28 +08:00
ning
3cb6d65bd1 refactor: add token last used time 2025-02-10 20:41:45 +08:00
ning
a0ec09669f refactor: target delete api 2025-02-10 17:36:29 +08:00
ning
82855d9c68 refactor: optimize poster pkg 2025-02-08 18:30:39 +08:00
ning
56d3031a6e refactor: change alert rule api 2025-02-08 14:46:02 +08:00
ning
22e9c99e46 refactor: add log 2025-02-08 14:32:48 +08:00
ning
200117b8b2 refactor: add log 2025-02-08 14:21:33 +08:00
ulricqin
836caabee8 Update host_generic_categraf_all.json 2025-02-08 11:54:11 +08:00
ulricqin
65ddd8c724 Update host_generic_categraf_all.json 2025-02-08 11:46:02 +08:00
ning
bb7556c75a docs: add migrate sql 2025-02-07 17:11:11 +08:00
ning
b83f118f1b refactor: delete user token 2025-02-06 20:15:10 +08:00
ning
9e0f0581d6 refactor: user token sync 2025-02-06 16:40:14 +08:00
Yening Qin
250c737174 feat: api support token auth (#2453)
Co-authored-by: Xu Bin <140785332+Reditiny@users.noreply.github.com>
2025-02-06 16:01:57 +08:00
Xu Bin
cdf8140e3c fix: alert check when var contains .* 2025-02-05 20:02:27 +08:00
flashbo
f8d7e84ca0 refactor: query target (#2424) 2025-02-05 19:40:58 +08:00
ulricqin
542a98e708 Update README_en.md 2025-01-25 22:07:18 +08:00
ulricqin
fb8ee0be72 Update README_en.md 2025-01-25 22:06:36 +08:00
ulricqin
a4e9349dfd Update LICENSE COPYRIGHT 2025-01-25 21:49:49 +08:00
ning
8df3ff0f03 update ops desc 2025-01-24 17:08:58 +08:00
ning
a5d38d63ca update ops 2025-01-24 17:01:16 +08:00
ning
9cf147faf1 Merge branch 'main' of github.com:ccfos/nightingale 2025-01-23 18:11:32 +08:00
ning
0dd3d0e29d fix: es query delay 2025-01-23 18:11:18 +08:00
VicLai
9e95ab951a Update README.md 2025-01-23 16:18:13 +08:00
ning
2482ef45fb docs: remove dash tpl 2025-01-23 15:39:55 +08:00
ning
d33f1f1bdb docs: update ops 2025-01-22 15:03:53 +08:00
Yening Qin
0a9439446f refactor: optimize ops (#2447)
Co-authored-by: Xu Bin <140785332+Reditiny@users.noreply.github.com>
2025-01-21 20:50:30 +08:00
Yening Qin
8d4137c5bb feat: es support nodata alert (#2445) 2025-01-21 18:01:50 +08:00
ulricqin
caabbba251 更新 README.md 2025-01-21 09:17:58 +08:00
ulricqin
3d21a5c426 更新 README.md 2025-01-21 08:48:15 +08:00
ning
e928363e5d docs: rename es metrics cate 2025-01-20 18:09:25 +08:00
Ulric Qin
6879181f00 refactor linux dashboards 2025-01-17 16:39:23 +08:00
Ulric Qin
a8808c5262 update img url 2025-01-17 11:32:11 +08:00
Yening Qin
9253145aad refactor: optimize recv and push data (#2437) 2025-01-16 17:23:14 +08:00
ulricqin
1968e13da6 更新 README.md 2025-01-15 08:04:50 +08:00
ulricqin
88d075ba13 更新 README.md 2025-01-14 21:33:49 +08:00
Yening Qin
562da5a73f refactor: data recv and push to queue (#2427) 2025-01-14 12:08:03 +08:00
Yening Qin
9780e1ee8f refactor: change metric type filter (#2425) 2025-01-10 17:32:22 +08:00
Yening Qin
db050ec781 filter metric type (#2423) 2025-01-10 15:11:41 +08:00
ning
6a31521b62 fix: es group by int 2025-01-09 12:09:48 +08:00
ning
61512857a5 refactor: builtin_components 2025-01-08 20:48:37 +08:00
ning
cb56037ef8 refactor: change eval sleep 2025-01-08 19:40:04 +08:00
ning
2ebd64dfa0 Merge branch 'main' of github.com:ccfos/nightingale 2025-01-08 11:28:58 +08:00
ning
4d2ffdf096 refactor: change builtin component model 2025-01-08 11:24:50 +08:00
kongfei605
1915701ce0 chore: default interval for cloudwatch collecting (#2418) 2025-01-07 10:35:53 +08:00
kongfei605
7fd9cd5a3d chore: update tpl and readme for cloudwatch (#2417) 2025-01-06 19:13:45 +08:00
ning
0e2f386419 refactor: change tdengine 2025-01-06 17:29:58 +08:00
ning
b96b08fb9e refactor: change tdengine 2025-01-06 17:22:22 +08:00
ning
eebd1021de refactor: change tdengine 2025-01-06 17:04:46 +08:00
ning
ef61a4cfa7 refactor: change tdengine 2025-01-06 16:50:29 +08:00
ning
2563d2891d feat: add cron pattern validation for alert rule 2025-01-06 11:45:43 +08:00
ning
6ae8ef0d9f feat: add random delay before starting alert rule worker 2025-01-06 11:21:26 +08:00
Yening Qin
38adbefe9c feat: dashboard support add annotations (#2416) 2025-01-05 17:46:17 +08:00
Yening Qin
3f5e0c056d feat: support elasticsearch alert (#2400)
Co-authored-by: Xu Bin <140785332+Reditiny@users.noreply.github.com>
2025-01-05 17:44:17 +08:00
flashbo
b0131a3799 feat: support setting builtin component to disabled (#2406) 2025-01-02 11:11:54 +08:00
Yening Qin
cbb03a7c63 refactor: optimize enum type for alert rule with var
Co-authored-by: Xu Bin <140785332+Reditiny@users.noreply.github.com>
2024-12-28 22:18:39 +08:00
ning
080d412124 docs: add sql 2024-12-26 14:29:50 +08:00
ning
752e02f32d docs: add sql 2024-12-26 14:26:15 +08:00
CRISPpp
e05d59d72a refactor: update target update group part (#2388) 2024-12-25 15:35:19 +08:00
Yening Qin
854e30551a fix: docker compose of postgres init error (#2370) (#2392)
Co-authored-by: CRISPpp <78430796+CRISPpp@users.noreply.github.com>
2024-12-24 15:55:03 +08:00
Yening Qin
0b6dc5beba refactor get user from context (#2391)
Co-authored-by: flashbo <36443248+lwb0214@users.noreply.github.com>
2024-12-24 15:52:55 +08:00
ning
8685a95fa5 Merge branch 'main' of github.com:ccfos/nightingale 2024-12-24 15:40:22 +08:00
ning
7ca7fd8d66 refactor: event set AnnotationsJSON 2024-12-24 15:40:08 +08:00
Yening Qin
1b5dc81b6c fix: the dedup logic when adding tags to target (#2386)
Co-authored-by: flashbo <36443248+lwb0214@users.noreply.github.com>
2024-12-24 11:48:58 +08:00
Ulric Qin
04495f0892 set ignore_host to true 2024-12-20 18:10:48 +08:00
Yening Qin
8158ce1b90 refactor: global webhook add env proxy (#2375)
Co-authored-by: Xu Bin <140785332+Reditiny@users.noreply.github.com>
2024-12-20 14:21:47 +08:00
Yening Qin
a43952e168 refactor: es_index_pattern add cross_cluster_enabled (#2372) 2024-12-19 14:12:27 +08:00
Yening Qin
5702fc81d0 refactor: group delete check (#2368)
Co-authored-by: Xu Bin <140785332+Reditiny@users.noreply.github.com>
2024-12-18 17:00:18 +08:00
202 changed files with 21236 additions and 3274 deletions

4
.gitignore vendored
View File

@@ -65,3 +65,7 @@ queries.active
/n9e-*
n9e.sql
!/datasource
.env.json

634
LICENSE
View File

@@ -1,433 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2017 Beijing Didi Infinity Technology and Development Co.,Ltd. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright CCF ODC.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -29,9 +29,13 @@
## 夜莺 Nightingale 是什么
夜莺监控是一款开源云原生观测分析工具,采用 All-in-One 的设计理念,集数据采集、可视化、监控告警、数据分析于一体,与云原生生态紧密集成,提供开箱即用的企业级监控分析和告警能力。夜莺于 2020 年 3 月 20 日,在 GitHub 上发布 v1 版本,已累计迭代 100 多个版本
> 夜莺 Nightingale 是什么,解决什么问题?以大家都很熟悉的 Grafana 做个类比Grafana 擅长对接各种各样的数据源,然后提供灵活、强大、好看的可视化面板。夜莺则擅长对接各种多样的数据源,提供灵活、强大、高效的监控告警管理能力。从发展路径和定位来说,夜莺和 Grafana 很像,可以总结为一句话:可视化就用 Grafana监控告警就找夜莺
>
> 在可视化领域Grafana 是毫无争议的领导者Grafana 在影响力、装机量、用户群、开发者数量等各个维度的数字上相比夜莺都是追赶的榜样。巨无霸往往都是从一个切入点打开局面的Grafana Labs 有了在可视化领域 Grafana 这个王牌,逐步扩展到整个可观测性方向,比如 Logging 维度有 LokiTracing 维度有 TempoProfiling 维度有收购来的 PyroscopeOn-call 维度有同样是收购来的 Grafana-OnCall 项目,还有时序数据库 Mimir、eBPF 采集器 Beyla、OpenTelemetry 采集器 Alloy、前端监控 SDK Faro最终构成了一个完整的可观测性工具矩阵但整个飞轮都是从 Grafana 项目开始转动起来的。
>
>夜莺,则是从监控告警这个切入点打开局面,也逐步横向做了相应扩展,比如夜莺也自研了可视化面板,如果你想有一个 all-in-one 的监控告警+可视化的工具,那么用夜莺也是正确的选择;比如 OnCall 方向,夜莺可以和 [Flashduty SaaS](https://flashcat.cloud/product/flashcat-duty/) 服务无缝的集成;在采集器方向,夜莺有配套的 [Categraf](https://flashcat.cloud/product/categraf),可以一个采集器中管理所有的 exporter并同时支持指标和日志的采集极大减轻工程师维护的采集器数量和工作量这个点太痛了你可能也遇到过业务团队吐槽采集器数量比业务应用进程数量还多的窘况吧
夜莺最初由滴滴开发和开源,并于 2022 年 5 月 11 日捐赠予中国计算机学会开源发展委员会CCF ODC为 CCF ODC 成立后接受捐赠的第一个开源项目。夜莺的核心研发团队,也是 Open-Falcon 项目原核心研发人员,从 2014 年Open-Falcon 是 2014 年开源)算起来,也有 10 年了,只为把监控这个事情做好
夜莺 Nightingale 作为一款开源云原生监控工具,最初由滴滴开发和开源,并于 2022 年 5 月 11 日捐赠予中国计算机学会开源发展委员会CCF ODC为 CCF ODC 成立后接受捐赠的第一个开源项目。在 GitHub 上有超过 10000 颗星,是广受关注和使用的开源监控工具。夜莺的核心研发团队,也是 Open-Falcon 项目原核心研发人员,从 2014 年Open-Falcon 是 2014 年开源)算起来,也有 10 年了,只为把监控做到极致
## 快速开始
@@ -42,7 +46,8 @@
## 功能特点
- 对接多种时序库:支持对接 Prometheus、VictoriaMetrics、Thanos、Mimir、M3DB、TDengine 等多种时序库,实现统一告警管理
- 对接多种时序库,实现统一监控告警管理:支持对接的时序库包括 Prometheus、VictoriaMetrics、Thanos、Mimir、M3DB、TDengine 等。
- 对接日志库,实现针对日志的监控告警:支持对接的日志库包括 ElasticSearch、Loki 等。
- 专业告警能力:内置支持多种告警规则,可以扩展支持常见通知媒介,支持告警屏蔽/抑制/订阅/自愈、告警事件管理。
- 高性能可视化引擎:支持多种图表样式,内置众多 Dashboard 模版,也可导入 Grafana 模版,开箱即用,开源协议商业友好。
- 支持常见采集器:支持 [Categraf](https://flashcat.cloud/product/categraf)、Telegraf、Grafana-agent、Datadog-agent、各种 Exporter 作为采集器,没有什么数据是不能监控的。
@@ -54,23 +59,23 @@
你可以在页面的右上角,切换语言和主题,目前我们支持英语、简体中文、繁体中文。
![语言切换](https://download.flashcat.cloud/ulric/n9e-switch-i18n.png)
![语言切换](doc/img/readme/n9e-switch-i18n.png)
即时查询,类似 Prometheus 内置的查询分析页面,做 ad-hoc 查询,夜莺做了一些 UI 优化,同时提供了一些内置 promql 指标,让不太了解 promql 的用户也可以快速查询。
![即时查询](https://download.flashcat.cloud/ulric/20240513103305.png)
![即时查询](doc/img/readme/20240513103305.png)
当然,也可以直接通过指标视图查看,有了指标视图,即时查询基本可以不用了,或者只有高端玩家使用即时查询,普通用户直接通过指标视图查询即可。
![指标视图](https://download.flashcat.cloud/ulric/20240513103530.png)
![指标视图](doc/img/readme/20240513103530.png)
夜莺内置了常用仪表盘,可以直接导入使用。也可以导入 Grafana 仪表盘,不过只能兼容 Grafana 基本图表,如果已经习惯了 Grafana 建议继续使用 Grafana 看图,把夜莺作为一个告警引擎使用。
![内置仪表盘](https://download.flashcat.cloud/ulric/20240513103628.png)
![内置仪表盘](doc/img/readme/20240513103628.png)
除了内置的仪表盘,也内置了很多告警规则,开箱即用。
![内置告警规则](https://download.flashcat.cloud/ulric/20240513103825.png)
![内置告警规则](doc/img/readme/20240513103825.png)
@@ -78,20 +83,17 @@
社区使用夜莺最多的场景就是使用夜莺做告警引擎,对接多套时序库,统一告警规则管理。绘图仍然使用 Grafana 居多。作为一个告警引擎,夜莺的产品架构如下:
![产品架构](https://download.flashcat.cloud/ulric/20240221152601.png)
![产品架构](doc/img/readme/20240221152601.png)
对于个别边缘机房,如果和中心夜莺服务端网络链路不好,希望提升告警可用性,我们也提供边缘机房告警引擎下沉部署模式,这个模式下,即便网络割裂,告警功能也不受影响。
![边缘部署模式](https://download.flashcat.cloud/ulric/20240222102119.png)
![边缘部署模式](doc/img/readme/20240222102119.png)
## 交流渠道
- 报告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`
- 日常问题交流:
- QQ群730841964
- [加入微信群](https://download.flashcat.cloud/ulric/20241022141621.png),如果二维码过期了,可以联系我(我的微信:`picobyte`)拉群,备注: `夜莺互助群`
- 关注[这个公众号](https://gitlink.org.cn/UlricQin)了解更多夜莺动态和知识
- 加我微信:`picobyte`(我已关闭好友验证)拉入微信群,备注:`夜莺互助群`
## 广受关注
[![Stargazers over time](https://api.star-history.com/svg?repos=ccfos/nightingale&type=Date)](https://star-history.com/#ccfos/nightingale&Date)

View File

@@ -29,7 +29,7 @@
## What is Nightingale
Nightingale aims to combine the advantages of Prometheus and Grafana. It manages alert rules and visualizes metrics, logs, traces in a beautiful WebUI.
Nightingale is an open-source project focused on alerting. Similar to Grafana's data source integration approach, Nightingale also connects with various existing data sources. However, while Grafana focuses on visualization, Nightingale focuses on alerting engines.
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.
@@ -53,31 +53,31 @@ Originally developed and open-sourced by Didi, Nightingale was donated to the Ch
You can switch languages and themes in the top right corner. We now support English, Simplified Chinese, and Traditional Chinese.
![18n switch](https://download.flashcat.cloud/ulric/n9e-switch-i18n.png)
![18n switch](doc/img/readme/n9e-switch-i18n.png)
### 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.
![Instant Query](https://download.flashcat.cloud/ulric/20240513103305.png)
![Instant Query](doc/img/readme/20240513103305.png)
### 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.
![Metric View](https://download.flashcat.cloud/ulric/20240513103530.png)
![Metric View](doc/img/readme/20240513103530.png)
### 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 youre accustomed to Grafana, its recommended to continue using it for visualization, with Nightingale serving as an alerting engine.
![Built-in Dashboards](https://download.flashcat.cloud/ulric/20240513103628.png)
![Built-in Dashboards](doc/img/readme/20240513103628.png)
### 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.
![Built-in Alert Rules](https://download.flashcat.cloud/ulric/20240513103825.png)
![Built-in Alert Rules](doc/img/readme/20240513103825.png)
@@ -85,11 +85,11 @@ In addition to the built-in dashboards, Nightingale also comes with numerous ale
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:
![Product Architecture](https://download.flashcat.cloud/ulric/20240221152601.png)
![Product Architecture](doc/img/readme/20240221152601.png)
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.
![Edge Deployment Mode](https://download.flashcat.cloud/ulric/20240222102119.png)
![Edge Deployment Mode](doc/img/readme/20240222102119.png)
## Communication Channels

View File

@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/dispatch"
@@ -21,12 +23,11 @@ import (
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/pkg/logx"
"github.com/ccfos/nightingale/v6/pkg/macros"
"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"
)
@@ -63,14 +64,18 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
taskTplsCache := memsto.NewTaskTplCache(ctx)
configCvalCache := memsto.NewCvalCache(ctx, syncStats)
notifyRuleCache := memsto.NewNotifyRuleCache(ctx, syncStats)
notifyChannelCache := memsto.NewNotifyChannelCache(ctx, syncStats)
messageTemplateCache := memsto.NewMessageTemplateCache(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, taskTplsCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
macros.RegisterMacro(macros.MacroInVain)
dscache.Init(ctx, false)
Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, userCache, userGroupCache, notifyRuleCache, notifyChannelCache, messageTemplateCache)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP,
configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
@@ -93,12 +98,14 @@ 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, taskTplsCache *memsto.TaskTplCache, datasourceCache *memsto.DatasourceCacheType, ctx *ctx.Context,
promClients *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType) {
promClients *prom.PromClientMap, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType, notifyRuleCache *memsto.NotifyRuleCacheType, notifyChannelCache *memsto.NotifyChannelCacheType, messageTemplateCache *memsto.MessageTemplateCacheType) {
alertSubscribeCache := memsto.NewAlertSubscribeCache(ctx, syncStats)
recordingRuleCache := memsto.NewRecordingRuleCache(ctx, syncStats)
targetsOfAlertRulesCache := memsto.NewTargetOfAlertRuleCache(ctx, alertc.Heartbeat.EngineName, syncStats)
go models.InitNotifyConfig(ctx, alertc.Alerting.TemplatesDir)
go models.InitNotifyChannel(ctx)
go models.InitMessageTemplate(ctx)
naming := naming.NewNaming(ctx, alertc.Heartbeat, alertStats)
@@ -106,14 +113,20 @@ func Start(alertc aconf.Alert, pushgwc pconf.Pushgw, syncStats *memsto.Stats, al
record.NewScheduler(alertc, recordingRuleCache, promClients, writers, alertStats, datasourceCache)
eval.NewScheduler(alertc, externalProcessors, alertRuleCache, targetCache, targetsOfAlertRulesCache,
busiGroupCache, alertMuteCache, datasourceCache, promClients, tdendgineClients, naming, ctx, alertStats)
busiGroupCache, alertMuteCache, datasourceCache, promClients, naming, ctx, alertStats)
dp := dispatch.NewDispatch(alertRuleCache, userCache, userGroupCache, alertSubscribeCache, targetCache, notifyConfigCache, taskTplsCache, alertc.Alerting, ctx, alertStats)
eventProcessorCache := memsto.NewEventProcessorCache(ctx, syncStats)
dp := dispatch.NewDispatch(alertRuleCache, userCache, userGroupCache, alertSubscribeCache, targetCache, notifyConfigCache, taskTplsCache, notifyRuleCache, notifyChannelCache, messageTemplateCache, eventProcessorCache, alertc.Alerting, ctx, alertStats)
consumer := dispatch.NewConsumer(alertc.Alerting, ctx, dp, promClients)
notifyRecordComsumer := sender.NewNotifyRecordConsumer(ctx)
go dp.ReloadTpls()
go consumer.LoopConsume()
go notifyRecordComsumer.LoopConsume()
go queue.ReportQueueSize(alertStats)
go sender.ReportNotifyRecordQueueSize(alertStats)
go sender.InitEmailSender(ctx, notifyConfigCache)
}

View File

@@ -17,12 +17,15 @@ type Stats struct {
CounterRuleEval *prometheus.CounterVec
CounterQueryDataErrorTotal *prometheus.CounterVec
CounterQueryDataTotal *prometheus.CounterVec
CounterVarFillingQuery *prometheus.CounterVec
CounterRecordEval *prometheus.CounterVec
CounterRecordEvalErrorTotal *prometheus.CounterVec
CounterMuteTotal *prometheus.CounterVec
CounterRuleEvalErrorTotal *prometheus.CounterVec
CounterHeartbeatErrorTotal *prometheus.CounterVec
CounterSubEventTotal *prometheus.CounterVec
GaugeQuerySeriesCount *prometheus.GaugeVec
GaugeNotifyRecordQueueSize prometheus.Gauge
}
func NewSyncStats() *Stats {
@@ -52,7 +55,7 @@ func NewSyncStats() *Stats {
Subsystem: subsystem,
Name: "query_data_total",
Help: "Number of rule eval query data.",
}, []string{"datasource"})
}, []string{"datasource", "rule_id"})
CounterRecordEval := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
@@ -103,7 +106,7 @@ func NewSyncStats() *Stats {
Subsystem: subsystem,
Name: "mute_total",
Help: "Number of mute.",
}, []string{"group"})
}, []string{"group", "rule_id", "mute_rule_id", "datasource_id"})
CounterSubEventTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
@@ -119,6 +122,27 @@ func NewSyncStats() *Stats {
Help: "Number of heartbeat error.",
}, []string{})
GaugeQuerySeriesCount := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "eval_query_series_count",
Help: "Number of series retrieved from data source after query.",
}, []string{"rule_id", "datasource_id", "ref"})
// 通知记录队列的长度
GaugeNotifyRecordQueueSize := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "notify_record_queue_size",
Help: "The size of notify record queue.",
})
CounterVarFillingQuery := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "var_filling_query_total",
Help: "Number of var filling query.",
}, []string{"rule_id", "datasource_id", "ref", "typ"})
prometheus.MustRegister(
CounterAlertsTotal,
GaugeAlertQueueSize,
@@ -133,6 +157,9 @@ func NewSyncStats() *Stats {
CounterRuleEvalErrorTotal,
CounterHeartbeatErrorTotal,
CounterSubEventTotal,
GaugeQuerySeriesCount,
GaugeNotifyRecordQueueSize,
CounterVarFillingQuery,
)
return &Stats{
@@ -149,5 +176,8 @@ func NewSyncStats() *Stats {
CounterRuleEvalErrorTotal: CounterRuleEvalErrorTotal,
CounterHeartbeatErrorTotal: CounterHeartbeatErrorTotal,
CounterSubEventTotal: CounterSubEventTotal,
GaugeQuerySeriesCount: GaugeQuerySeriesCount,
GaugeNotifyRecordQueueSize: GaugeNotifyRecordQueueSize,
CounterVarFillingQuery: CounterVarFillingQuery,
}
}

View File

@@ -3,6 +3,8 @@ package dispatch
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
"net/url"
"strconv"
@@ -13,6 +15,8 @@ import (
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/common"
"github.com/ccfos/nightingale/v6/alert/pipeline"
"github.com/ccfos/nightingale/v6/alert/pipeline/processor/relabel"
"github.com/ccfos/nightingale/v6/alert/sender"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
@@ -30,6 +34,11 @@ type Dispatch struct {
notifyConfigCache *memsto.NotifyConfigCacheType
taskTplsCache *memsto.TaskTplCache
notifyRuleCache *memsto.NotifyRuleCacheType
notifyChannelCache *memsto.NotifyChannelCacheType
messageTemplateCache *memsto.MessageTemplateCacheType
eventProcessorCache *memsto.EventProcessorCacheType
alerting aconf.Alerting
Senders map[string]sender.Sender
@@ -47,15 +56,20 @@ type Dispatch struct {
// 创建一个 Notify 实例
func NewDispatch(alertRuleCache *memsto.AlertRuleCacheType, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType,
alertSubscribeCache *memsto.AlertSubscribeCacheType, targetCache *memsto.TargetCacheType, notifyConfigCache *memsto.NotifyConfigCacheType,
taskTplsCache *memsto.TaskTplCache, alerting aconf.Alerting, ctx *ctx.Context, astats *astats.Stats) *Dispatch {
taskTplsCache *memsto.TaskTplCache, notifyRuleCache *memsto.NotifyRuleCacheType, notifyChannelCache *memsto.NotifyChannelCacheType,
messageTemplateCache *memsto.MessageTemplateCacheType, eventProcessorCache *memsto.EventProcessorCacheType, alerting aconf.Alerting, ctx *ctx.Context, astats *astats.Stats) *Dispatch {
notify := &Dispatch{
alertRuleCache: alertRuleCache,
userCache: userCache,
userGroupCache: userGroupCache,
alertSubscribeCache: alertSubscribeCache,
targetCache: targetCache,
notifyConfigCache: notifyConfigCache,
taskTplsCache: taskTplsCache,
alertRuleCache: alertRuleCache,
userCache: userCache,
userGroupCache: userGroupCache,
alertSubscribeCache: alertSubscribeCache,
targetCache: targetCache,
notifyConfigCache: notifyConfigCache,
taskTplsCache: taskTplsCache,
notifyRuleCache: notifyRuleCache,
notifyChannelCache: notifyChannelCache,
messageTemplateCache: messageTemplateCache,
eventProcessorCache: eventProcessorCache,
alerting: alerting,
@@ -67,11 +81,14 @@ func NewDispatch(alertRuleCache *memsto.AlertRuleCacheType, userCache *memsto.Us
ctx: ctx,
Astats: astats,
}
relabel.Init()
return notify
}
func (e *Dispatch) ReloadTpls() error {
err := e.relaodTpls()
err := e.reloadTpls()
if err != nil {
logger.Errorf("failed to reload tpls: %v", err)
}
@@ -79,13 +96,13 @@ func (e *Dispatch) ReloadTpls() error {
duration := time.Duration(9000) * time.Millisecond
for {
time.Sleep(duration)
if err := e.relaodTpls(); err != nil {
if err := e.reloadTpls(); err != nil {
logger.Warning("failed to reload tpls:", err)
}
}
}
func (e *Dispatch) relaodTpls() error {
func (e *Dispatch) reloadTpls() error {
tmpTpls, err := models.ListTpls(e.ctx)
if err != nil {
return err
@@ -131,6 +148,356 @@ func (e *Dispatch) relaodTpls() error {
return nil
}
func (e *Dispatch) HandleEventWithNotifyRule(eventOrigin *models.AlertCurEvent) {
if len(eventOrigin.NotifyRuleIDs) > 0 {
for _, notifyRuleId := range eventOrigin.NotifyRuleIDs {
// 深拷贝新的 event避免并发修改 event 冲突
eventCopy := eventOrigin.DeepCopy()
logger.Infof("notify rule ids: %v, event: %+v", notifyRuleId, eventCopy)
notifyRule := e.notifyRuleCache.Get(notifyRuleId)
if notifyRule == nil {
continue
}
if !notifyRule.Enable {
continue
}
var processors []pipeline.Processor
for _, pipelineConfig := range notifyRule.PipelineConfigs {
if !pipelineConfig.Enable {
continue
}
eventPipeline := e.eventProcessorCache.Get(pipelineConfig.PipelineId)
if eventPipeline == nil {
logger.Warningf("notify_id: %d, event:%+v, processor not found", notifyRuleId, eventCopy)
continue
}
if !pipelineApplicable(eventPipeline, eventCopy) {
logger.Debugf("notify_id: %d, event:%+v, pipeline_id: %d, not applicable", notifyRuleId, eventCopy, pipelineConfig.PipelineId)
continue
}
for _, p := range eventPipeline.Processors {
processor, err := pipeline.GetProcessorByType(p.Typ, p.Config)
if err != nil {
logger.Warningf("notify_id: %d, event:%+v, processor:%+v type not found", notifyRuleId, eventCopy, p)
continue
}
processors = append(processors, processor)
}
}
for _, processor := range processors {
logger.Infof("before processor notify_id: %d, event:%+v, processor:%+v", notifyRuleId, eventCopy, processor)
processor.Process(e.ctx, eventCopy)
logger.Infof("after processor notify_id: %d, event:%+v, processor:%+v", notifyRuleId, eventCopy, processor)
if eventCopy == nil {
logger.Warningf("notify_id: %d, event:%+v, processor:%+v, event is nil", notifyRuleId, eventCopy, processor)
break
}
}
// notify
for i := range notifyRule.NotifyConfigs {
if !NotifyRuleApplicable(&notifyRule.NotifyConfigs[i], eventCopy) {
continue
}
notifyChannel := e.notifyChannelCache.Get(notifyRule.NotifyConfigs[i].ChannelID)
messageTemplate := e.messageTemplateCache.Get(notifyRule.NotifyConfigs[i].TemplateID)
if notifyChannel == nil {
sender.NotifyRecord(e.ctx, []*models.AlertCurEvent{eventCopy}, notifyRuleId, fmt.Sprintf("notify_channel_id:%d", notifyRule.NotifyConfigs[i].ChannelID), "", "", errors.New("notify_channel not found"))
logger.Warningf("notify_id: %d, event:%+v, channel_id:%d, template_id: %d, notify_channel not found", notifyRuleId, eventCopy, notifyRule.NotifyConfigs[i].ChannelID, notifyRule.NotifyConfigs[i].TemplateID)
continue
}
if notifyChannel.RequestType != "flashduty" && messageTemplate == nil {
logger.Warningf("notify_id: %d, channel_name: %v, event:%+v, template_id: %d, message_template not found", notifyRuleId, notifyChannel.Ident, eventCopy, notifyRule.NotifyConfigs[i].TemplateID)
sender.NotifyRecord(e.ctx, []*models.AlertCurEvent{eventCopy}, notifyRuleId, notifyChannel.Name, "", "", errors.New("message_template not found"))
continue
}
// todo go send
// todo 聚合 event
go e.sendV2([]*models.AlertCurEvent{eventCopy}, notifyRuleId, &notifyRule.NotifyConfigs[i], notifyChannel, messageTemplate)
}
}
}
}
func pipelineApplicable(pipeline *models.EventPipeline, event *models.AlertCurEvent) bool {
if pipeline == nil {
return true
}
if !pipeline.FilterEnable {
return true
}
tagMatch := true
if len(pipeline.LabelFilters) > 0 {
for i := range pipeline.LabelFilters {
if pipeline.LabelFilters[i].Func == "" {
pipeline.LabelFilters[i].Func = pipeline.LabelFilters[i].Op
}
}
tagFilters, err := models.ParseTagFilter(pipeline.LabelFilters)
if err != nil {
logger.Errorf("pipeline applicable failed to parse tag filter: %v event:%+v pipeline:%+v", err, event, pipeline)
return false
}
tagMatch = common.MatchTags(event.TagsMap, tagFilters)
}
attributesMatch := true
if len(pipeline.AttrFilters) > 0 {
tagFilters, err := models.ParseTagFilter(pipeline.AttrFilters)
if err != nil {
logger.Errorf("pipeline applicable failed to parse tag filter: %v event:%+v pipeline:%+v err:%v", tagFilters, event, pipeline, err)
return false
}
attributesMatch = common.MatchTags(event.JsonTagsAndValue(), tagFilters)
}
return tagMatch && attributesMatch
}
func NotifyRuleApplicable(notifyConfig *models.NotifyConfig, event *models.AlertCurEvent) bool {
tm := time.Unix(event.TriggerTime, 0)
triggerTime := tm.Format("15:04")
triggerWeek := int(tm.Weekday())
timeMatch := false
if len(notifyConfig.TimeRanges) == 0 {
timeMatch = true
}
for j := range notifyConfig.TimeRanges {
if timeMatch {
break
}
enableStime := notifyConfig.TimeRanges[j].Start
enableEtime := notifyConfig.TimeRanges[j].End
enableDaysOfWeek := notifyConfig.TimeRanges[j].Week
length := len(enableDaysOfWeek)
// enableStime,enableEtime,enableDaysOfWeek三者长度肯定相同这里循环一个即可
for i := 0; i < length; i++ {
if enableDaysOfWeek[i] != triggerWeek {
continue
}
if enableStime < enableEtime {
if enableEtime == "23:59" {
// 02:00-23:59这种情况做个特殊处理相当于左闭右闭区间了
if triggerTime < enableStime {
// mute, 即没生效
continue
}
} else {
// 02:00-04:00 或者 02:00-24:00
if triggerTime < enableStime || triggerTime >= enableEtime {
// mute, 即没生效
continue
}
}
} else if enableStime > enableEtime {
// 21:00-09:00
if triggerTime < enableStime && triggerTime >= enableEtime {
// mute, 即没生效
continue
}
}
// 到这里说明当前时刻在告警规则的某组生效时间范围内,即没有 mute直接返回 false
timeMatch = true
break
}
}
severityMatch := false
for i := range notifyConfig.Severities {
if notifyConfig.Severities[i] == event.Severity {
severityMatch = true
}
}
tagMatch := true
if len(notifyConfig.LabelKeys) > 0 {
for i := range notifyConfig.LabelKeys {
if notifyConfig.LabelKeys[i].Func == "" {
notifyConfig.LabelKeys[i].Func = notifyConfig.LabelKeys[i].Op
}
}
tagFilters, err := models.ParseTagFilter(notifyConfig.LabelKeys)
if err != nil {
logger.Errorf("notify send failed to parse tag filter: %v event:%+v notify_config:%+v", err, event, notifyConfig)
return false
}
tagMatch = common.MatchTags(event.TagsMap, tagFilters)
}
attributesMatch := true
if len(notifyConfig.Attributes) > 0 {
tagFilters, err := models.ParseTagFilter(notifyConfig.Attributes)
if err != nil {
logger.Errorf("notify send failed to parse tag filter: %v event:%+v notify_config:%+v err:%v", tagFilters, event, notifyConfig, err)
return false
}
attributesMatch = common.MatchTags(event.JsonTagsAndValue(), tagFilters)
}
logger.Infof("notify send timeMatch:%v severityMatch:%v tagMatch:%v attributesMatch:%v event:%+v notify_config:%+v", timeMatch, severityMatch, tagMatch, attributesMatch, event, notifyConfig)
return timeMatch && severityMatch && tagMatch && attributesMatch
}
func GetNotifyConfigParams(notifyConfig *models.NotifyConfig, contactKey string, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType) ([]string, []int64, map[string]string) {
customParams := make(map[string]string)
var flashDutyChannelIDs []int64
var userInfoParams models.CustomParams
for key, value := range notifyConfig.Params {
switch key {
case "user_ids", "user_group_ids", "ids":
if data, err := json.Marshal(value); err == nil {
var ids []int64
if json.Unmarshal(data, &ids) == nil {
if key == "user_ids" {
userInfoParams.UserIDs = ids
} else if key == "user_group_ids" {
userInfoParams.UserGroupIDs = ids
} else if key == "ids" {
flashDutyChannelIDs = ids
}
}
}
default:
customParams[key] = value.(string)
}
}
if len(userInfoParams.UserIDs) == 0 && len(userInfoParams.UserGroupIDs) == 0 {
return []string{}, flashDutyChannelIDs, customParams
}
userIds := make([]int64, 0)
userIds = append(userIds, userInfoParams.UserIDs...)
if len(userInfoParams.UserGroupIDs) > 0 {
userGroups := userGroupCache.GetByUserGroupIds(userInfoParams.UserGroupIDs)
for _, userGroup := range userGroups {
userIds = append(userIds, userGroup.UserIds...)
}
}
users := userCache.GetByUserIds(userIds)
visited := make(map[int64]bool)
sendtos := make([]string, 0)
for _, user := range users {
if visited[user.Id] {
continue
}
var sendto string
if contactKey == "phone" {
sendto = user.Phone
} else if contactKey == "email" {
sendto = user.Email
} else {
sendto, _ = user.ExtractToken(contactKey)
}
if sendto == "" {
continue
}
sendtos = append(sendtos, sendto)
visited[user.Id] = true
}
return sendtos, flashDutyChannelIDs, customParams
}
func (e *Dispatch) sendV2(events []*models.AlertCurEvent, notifyRuleId int64, notifyConfig *models.NotifyConfig, notifyChannel *models.NotifyChannelConfig, messageTemplate *models.MessageTemplate) {
if len(events) == 0 {
logger.Errorf("notify_id: %d events is empty", notifyRuleId)
return
}
tplContent := make(map[string]interface{})
if notifyChannel.RequestType != "flashduty" {
tplContent = messageTemplate.RenderEvent(events)
}
var contactKey string
if notifyChannel.ParamConfig != nil && notifyChannel.ParamConfig.UserInfo != nil {
contactKey = notifyChannel.ParamConfig.UserInfo.ContactKey
}
sendtos, flashDutyChannelIDs, customParams := GetNotifyConfigParams(notifyConfig, contactKey, e.userCache, e.userGroupCache)
e.Astats.GaugeNotifyRecordQueueSize.Inc()
defer e.Astats.GaugeNotifyRecordQueueSize.Dec()
switch notifyChannel.RequestType {
case "flashduty":
if len(flashDutyChannelIDs) == 0 {
flashDutyChannelIDs = []int64{0} // 如果 flashduty 通道没有配置,则使用 0, 给 SendFlashDuty 判断使用, 不给 flashduty 传 channel_id 参数
}
for i := range flashDutyChannelIDs {
respBody, err := notifyChannel.SendFlashDuty(events, flashDutyChannelIDs[i], e.notifyChannelCache.GetHttpClient(notifyChannel.ID))
logger.Infof("notify_id: %d, channel_name: %v, event:%+v, IntegrationUrl: %v dutychannel_id: %v, respBody: %v, err: %v", notifyRuleId, notifyChannel.Name, events[0], notifyChannel.RequestConfig.FlashDutyRequestConfig.IntegrationUrl, flashDutyChannelIDs[i], respBody, err)
sender.NotifyRecord(e.ctx, events, notifyRuleId, notifyChannel.Name, strconv.FormatInt(flashDutyChannelIDs[i], 10), respBody, err)
}
return
case "http":
if e.notifyChannelCache.HttpConcurrencyAdd(notifyChannel.ID) {
defer e.notifyChannelCache.HttpConcurrencyDone(notifyChannel.ID)
}
if notifyChannel.RequestConfig == nil {
logger.Warningf("notify_id: %d, channel_name: %v, event:%+v, request config not found", notifyRuleId, notifyChannel.Name, events[0])
}
if notifyChannel.RequestConfig.HTTPRequestConfig == nil {
logger.Warningf("notify_id: %d, channel_name: %v, event:%+v, http request config not found", notifyRuleId, notifyChannel.Name, events[0])
}
if NeedBatchContacts(notifyChannel.RequestConfig.HTTPRequestConfig) || len(sendtos) == 0 {
resp, err := notifyChannel.SendHTTP(events, tplContent, customParams, sendtos, e.notifyChannelCache.GetHttpClient(notifyChannel.ID))
logger.Infof("notify_id: %d, channel_name: %v, event:%+v, tplContent:%s, customParams:%v, userInfo:%+v, respBody: %v, err: %v", notifyRuleId, notifyChannel.Name, events[0], tplContent, customParams, sendtos, resp, err)
sender.NotifyRecord(e.ctx, events, notifyRuleId, notifyChannel.Name, getSendTarget(customParams, sendtos), resp, err)
} else {
for i := range sendtos {
resp, err := notifyChannel.SendHTTP(events, tplContent, customParams, []string{sendtos[i]}, e.notifyChannelCache.GetHttpClient(notifyChannel.ID))
logger.Infof("notify_id: %d, channel_name: %v, event:%+v, tplContent:%s, customParams:%v, userInfo:%+v, respBody: %v, err: %v", notifyRuleId, notifyChannel.Name, events[0], tplContent, customParams, sendtos[i], resp, err)
sender.NotifyRecord(e.ctx, events, notifyRuleId, notifyChannel.Name, getSendTarget(customParams, []string{sendtos[i]}), resp, err)
}
}
case "smtp":
notifyChannel.SendEmail(notifyRuleId, events, tplContent, sendtos, e.notifyChannelCache.GetSmtpClient(notifyChannel.ID))
case "script":
target, res, err := notifyChannel.SendScript(events, tplContent, customParams, sendtos)
logger.Infof("notify_id: %d, channel_name: %v, event:%+v, tplContent:%s, customParams:%v, target:%s, res:%s, err:%v", notifyRuleId, notifyChannel.Name, events[0], tplContent, customParams, target, res, err)
sender.NotifyRecord(e.ctx, events, notifyRuleId, notifyChannel.Name, target, res, err)
default:
logger.Warningf("notify_id: %d, channel_name: %v, event:%+v send type not found", notifyRuleId, notifyChannel.Name, events[0])
}
}
func NeedBatchContacts(requestConfig *models.HTTPRequestConfig) bool {
b, _ := json.Marshal(requestConfig)
return strings.Contains(string(b), "$sendtos")
}
// HandleEventNotify 处理event事件的主逻辑
// event: 告警/恢复事件
// isSubscribe: 告警事件是否由subscribe的配置产生
@@ -172,7 +539,7 @@ func (e *Dispatch) HandleEventNotify(event *models.AlertCurEvent, isSubscribe bo
notifyTarget.AndMerge(handler(rule, event, notifyTarget, e))
}
// 处理事件发送,这里用一个goroutine处理一个event的所有发送事件
go e.HandleEventWithNotifyRule(event)
go e.Send(rule, event, notifyTarget, isSubscribe)
// 如果是不是订阅规则出现的event, 则需要处理订阅规则的event
@@ -369,6 +736,11 @@ func (e *Dispatch) HandleIbex(rule *models.AlertRule, event *models.AlertCurEven
}
json.Unmarshal([]byte(rule.RuleConfig), &ruleConfig)
if event.IsRecovered {
// 恢复事件不需要走故障自愈的逻辑
return
}
for _, t := range ruleConfig.TaskTpls {
if t.TplId == 0 {
continue
@@ -447,3 +819,22 @@ func mapKeys(m map[int64]struct{}) []int64 {
}
return lst
}
func getSendTarget(customParams map[string]string, sendtos []string) string {
if len(customParams) == 0 {
return strings.Join(sendtos, ",")
}
values := make([]string, 0)
for _, value := range customParams {
runes := []rune(value)
if len(runes) <= 4 {
values = append(values, value)
} else {
maskedValue := string(runes[:len(runes)-4]) + "****"
values = append(values, maskedValue)
}
}
return strings.Join(values, ",")
}

View File

@@ -18,12 +18,13 @@ func LogEvent(event *models.AlertCurEvent, location string, err ...error) {
}
logger.Infof(
"event(%s %s) %s: rule_id=%d sub_id:%d cluster:%s %v%s@%d %s",
"event(%s %s) %s: rule_id=%d sub_id:%d notify_rule_ids:%v cluster:%s %v%s@%d %s",
event.Hash,
status,
location,
event.RuleId,
event.SubRuleId,
event.NotifyRuleIDs,
event.Cluster,
event.TagsJSON,
event.TriggerValue,

View File

@@ -10,11 +10,10 @@ import (
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/naming"
"github.com/ccfos/nightingale/v6/alert/process"
"github.com/ccfos/nightingale/v6/datasource/commons/eslike"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/tdengine"
"github.com/toolkits/pkg/logger"
)
@@ -33,8 +32,7 @@ type Scheduler struct {
alertMuteCache *memsto.AlertMuteCacheType
datasourceCache *memsto.DatasourceCacheType
promClients *prom.PromClientMap
tdengineClients *tdengine.TdengineClientMap
promClients *prom.PromClientMap
naming *naming.Naming
@@ -45,7 +43,7 @@ type Scheduler struct {
func NewScheduler(aconf aconf.Alert, externalProcessors *process.ExternalProcessorsType, arc *memsto.AlertRuleCacheType,
targetCache *memsto.TargetCacheType, toarc *memsto.TargetsOfAlertRuleCacheType,
busiGroupCache *memsto.BusiGroupCacheType, alertMuteCache *memsto.AlertMuteCacheType, datasourceCache *memsto.DatasourceCacheType,
promClients *prom.PromClientMap, tdengineClients *tdengine.TdengineClientMap, naming *naming.Naming, ctx *ctx.Context, stats *astats.Stats) *Scheduler {
promClients *prom.PromClientMap, naming *naming.Naming, ctx *ctx.Context, stats *astats.Stats) *Scheduler {
scheduler := &Scheduler{
aconf: aconf,
alertRules: make(map[string]*AlertRuleWorker),
@@ -59,13 +57,13 @@ func NewScheduler(aconf aconf.Alert, externalProcessors *process.ExternalProcess
alertMuteCache: alertMuteCache,
datasourceCache: datasourceCache,
promClients: promClients,
tdengineClients: tdengineClients,
naming: naming,
promClients: promClients,
naming: naming,
ctx: ctx,
stats: stats,
}
eslike.SetEsIndexPatternCacheType(memsto.NewEsIndexPatternCacheType(ctx))
go scheduler.LoopSyncRules(context.Background())
return scheduler
@@ -95,7 +93,7 @@ func (s *Scheduler) syncAlertRules() {
}
ruleType := rule.GetRuleType()
if rule.IsPrometheusRule() || rule.IsLokiRule() || rule.IsTdengineRule() {
if rule.IsPrometheusRule() || rule.IsLokiRule() || rule.IsTdengineRule() || rule.IsClickHouseRule() || rule.IsElasticSearch() {
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) {
@@ -118,7 +116,7 @@ func (s *Scheduler) syncAlertRules() {
}
processor := process.NewProcessor(s.aconf.Heartbeat.EngineName, rule, dsId, s.alertRuleCache, s.targetCache, s.targetsOfAlertRuleCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.ctx, s.stats)
alertRule := NewAlertRuleWorker(rule, dsId, processor, s.promClients, s.tdengineClients, s.ctx)
alertRule := NewAlertRuleWorker(rule, dsId, processor, s.promClients, s.ctx)
alertRuleWorkers[alertRule.Hash()] = alertRule
}
} else if rule.IsHostRule() {
@@ -127,7 +125,7 @@ func (s *Scheduler) syncAlertRules() {
continue
}
processor := process.NewProcessor(s.aconf.Heartbeat.EngineName, rule, 0, s.alertRuleCache, s.targetCache, s.targetsOfAlertRuleCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.ctx, s.stats)
alertRule := NewAlertRuleWorker(rule, 0, processor, s.promClients, s.tdengineClients, s.ctx)
alertRule := NewAlertRuleWorker(rule, 0, processor, s.promClients, s.ctx)
alertRuleWorkers[alertRule.Hash()] = alertRule
} else {
// 如果 rule 不是通过 prometheus engine 来告警的,则创建为 externalRule
@@ -153,6 +151,7 @@ func (s *Scheduler) syncAlertRules() {
for hash, rule := range alertRuleWorkers {
if _, has := s.alertRules[hash]; !has {
rule.Prepare()
time.Sleep(time.Duration(20) * time.Millisecond)
rule.Start()
s.alertRules[hash] = rule
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,28 +12,29 @@ import (
"github.com/toolkits/pkg/logger"
)
func IsMuted(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, alertMuteCache *memsto.AlertMuteCacheType) (bool, string) {
func IsMuted(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, alertMuteCache *memsto.AlertMuteCacheType) (bool, string, int64) {
if rule.Disabled == 1 {
return true, "rule disabled"
return true, "rule disabled", 0
}
if TimeSpanMuteStrategy(rule, event) {
return true, "rule is not effective for period of time"
return true, "rule is not effective for period of time", 0
}
if IdentNotExistsMuteStrategy(rule, event, targetCache) {
return true, "ident not exists mute"
return true, "ident not exists mute", 0
}
if BgNotMatchMuteStrategy(rule, event, targetCache) {
return true, "bg not match mute"
return true, "bg not match mute", 0
}
if EventMuteStrategy(event, alertMuteCache) {
return true, "match mute rule"
hit, muteId := EventMuteStrategy(event, alertMuteCache)
if hit {
return true, "match mute rule", muteId
}
return false, ""
return false, "", 0
}
// TimeSpanMuteStrategy 根据规则配置的告警生效时间段过滤,如果产生的告警不在规则配置的告警生效时间段内,则不告警,即被mute
@@ -121,19 +122,19 @@ func BgNotMatchMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent,
return false
}
func EventMuteStrategy(event *models.AlertCurEvent, alertMuteCache *memsto.AlertMuteCacheType) bool {
func EventMuteStrategy(event *models.AlertCurEvent, alertMuteCache *memsto.AlertMuteCacheType) (bool, int64) {
mutes, has := alertMuteCache.Gets(event.GroupId)
if !has || len(mutes) == 0 {
return false
return false, 0
}
for i := 0; i < len(mutes); i++ {
if matchMute(event, mutes[i]) {
return true
return true, mutes[i].Id
}
}
return false
return false, 0
}
// matchMute 如果传入了clock这个可选参数就表示使用这个clock表示的时间否则就从event的字段中取TriggerTime
@@ -141,10 +142,7 @@ func matchMute(event *models.AlertCurEvent, mute *models.AlertMute, clock ...int
if mute.Disabled == 1 {
return false
}
ts := event.TriggerTime
if len(clock) > 0 {
ts = clock[0]
}
// 如果不是全局的,判断 匹配的 datasource id
if len(mute.DatasourceIdsJson) != 0 && mute.DatasourceIdsJson[0] != 0 && event.DatasourceId != 0 {
@@ -159,37 +157,21 @@ func matchMute(event *models.AlertCurEvent, mute *models.AlertMute, clock ...int
}
}
var matchTime bool
if mute.MuteTimeType == models.TimeRange {
if ts < mute.Btime || ts > mute.Etime {
if !mute.IsWithinTimeRange(event.TriggerTime) {
return false
}
matchTime = true
} else if mute.MuteTimeType == models.Periodic {
tm := time.Unix(event.TriggerTime, 0)
triggerTime := tm.Format("15:04")
triggerWeek := strconv.Itoa(int(tm.Weekday()))
for i := 0; i < len(mute.PeriodicMutesJson); i++ {
if strings.Contains(mute.PeriodicMutesJson[i].EnableDaysOfWeek, triggerWeek) {
if mute.PeriodicMutesJson[i].EnableStime == mute.PeriodicMutesJson[i].EnableEtime || (mute.PeriodicMutesJson[i].EnableStime == "00:00" && mute.PeriodicMutesJson[i].EnableEtime == "23:59") {
matchTime = true
break
} else if mute.PeriodicMutesJson[i].EnableStime < mute.PeriodicMutesJson[i].EnableEtime {
if triggerTime >= mute.PeriodicMutesJson[i].EnableStime && triggerTime < mute.PeriodicMutesJson[i].EnableEtime {
matchTime = true
break
}
} else {
if triggerTime >= mute.PeriodicMutesJson[i].EnableStime || triggerTime < mute.PeriodicMutesJson[i].EnableEtime {
matchTime = true
break
}
}
}
ts := event.TriggerTime
if len(clock) > 0 {
ts = clock[0]
}
}
if !matchTime {
if !mute.IsWithinPeriodicMute(ts) {
return false
}
} else {
logger.Warningf("mute time type invalid, %d", mute.MuteTimeType)
return false
}

View File

@@ -0,0 +1,12 @@
package pipeline
import (
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
)
func Pipeline(ctx *ctx.Context, event *models.AlertCurEvent, processors []Processor) {
for _, processor := range processors {
processor.Process(ctx, event)
}
}

View File

@@ -0,0 +1,52 @@
package pipeline
import (
"fmt"
"strings"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
)
// Processor 是处理器接口,所有处理器类型都需要实现此接口
type Processor interface {
Init(settings interface{}) (Processor, error) // 初始化配置
Process(ctx *ctx.Context, event *models.AlertCurEvent) // 处理告警事件
}
// NewProcessorFn 创建处理器的函数类型
type NewProcessorFn func(settings interface{}) (Processor, error)
// 处理器注册表,存储各种类型处理器的构造函数
var processorRegister = map[string]NewProcessorFn{}
// // ProcessorTypes 存储所有支持的处理器类型
// var Processors map[int64]models.Processor
// func init() {
// Processors = make(map[int64]models.Processor)
// }
// RegisterProcessor 注册处理器类型
func RegisterProcessor(typ string, p Processor) {
if _, found := processorRegister[typ]; found {
return
}
processorRegister[typ] = p.Init
}
// GetProcessorByType 根据类型获取处理器实例
func GetProcessorByType(typ string, settings interface{}) (Processor, error) {
typ = strings.TrimSpace(typ)
fn, found := processorRegister[typ]
if !found {
return nil, fmt.Errorf("processor type %s not found", typ)
}
processor, err := fn(settings)
if err != nil {
return nil, err
}
return processor, nil
}

View File

@@ -0,0 +1,124 @@
package relabel
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"github.com/ccfos/nightingale/v6/alert/pipeline"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/prompb"
)
// RelabelConfig
type RelabelConfig struct {
SourceLabels []string `json:"source_labels"`
Separator string `json:"separator"`
Regex string `json:"regex"`
RegexCompiled *regexp.Regexp
If string `json:"if"`
IfRegex *regexp.Regexp
Modulus uint64 `json:"modulus"`
TargetLabel string `json:"target_label"`
Replacement string `json:"replacement"`
Action string `json:"action"`
}
func Init() {
}
func init() {
pipeline.RegisterProcessor("relabel", &RelabelConfig{})
}
func (r *RelabelConfig) Init(settings interface{}) (pipeline.Processor, error) {
b, err := json.Marshal(settings)
if err != nil {
return nil, err
}
newProcessor := &RelabelConfig{}
err = json.Unmarshal(b, &newProcessor)
if err != nil {
return nil, err
}
return newProcessor, nil
}
const (
REPLACE_DOT = "___"
)
func (r *RelabelConfig) Process(ctx *ctx.Context, event *models.AlertCurEvent) {
sourceLabels := make([]model.LabelName, len(r.SourceLabels))
for i := range r.SourceLabels {
sourceLabels[i] = model.LabelName(strings.ReplaceAll(r.SourceLabels[i], ".", REPLACE_DOT))
}
relabelConfigs := []*pconf.RelabelConfig{
{
SourceLabels: sourceLabels,
Separator: r.Separator,
Regex: r.Regex,
RegexCompiled: r.RegexCompiled,
If: r.If,
IfRegex: r.IfRegex,
Modulus: r.Modulus,
TargetLabel: r.TargetLabel,
Replacement: r.Replacement,
Action: r.Action,
},
}
EventRelabel(event, relabelConfigs)
}
func EventRelabel(event *models.AlertCurEvent, relabelConfigs []*pconf.RelabelConfig) {
labels := make([]prompb.Label, len(event.TagsJSON))
event.OriginalTagsJSON = make([]string, len(event.TagsJSON))
for i, tag := range event.TagsJSON {
label := strings.SplitN(tag, "=", 2)
if len(label) != 2 {
continue
}
event.OriginalTagsJSON[i] = tag
label[0] = strings.ReplaceAll(string(label[0]), ".", REPLACE_DOT)
labels[i] = prompb.Label{Name: label[0], Value: label[1]}
}
for i := 0; i < len(relabelConfigs); i++ {
if relabelConfigs[i].Replacement == "" {
relabelConfigs[i].Replacement = "$1"
}
if relabelConfigs[i].Separator == "" {
relabelConfigs[i].Separator = ";"
}
if relabelConfigs[i].Regex == "" {
relabelConfigs[i].Regex = "(.*)"
}
for j := range relabelConfigs[i].SourceLabels {
relabelConfigs[i].SourceLabels[j] = model.LabelName(strings.ReplaceAll(string(relabelConfigs[i].SourceLabels[j]), ".", REPLACE_DOT))
}
}
gotLabels := writer.Process(labels, relabelConfigs...)
event.TagsJSON = make([]string, len(gotLabels))
event.TagsMap = make(map[string]string, len(gotLabels))
for i, label := range gotLabels {
label.Name = strings.ReplaceAll(string(label.Name), REPLACE_DOT, ".")
event.TagsJSON[i] = fmt.Sprintf("%s=%s", label.Name, label.Value)
event.TagsMap[label.Name] = label.Value
}
event.Tags = strings.Join(event.TagsJSON, ",,")
}

View File

@@ -2,6 +2,7 @@ package process
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"sort"
@@ -13,14 +14,13 @@ import (
"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/pipeline/processor/relabel"
"github.com/ccfos/nightingale/v6/alert/queue"
"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/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"
@@ -60,11 +60,9 @@ type Processor struct {
pendingsUseByRecover *AlertCurEventMap
inhibit bool
tagsMap map[string]string
tagsArr []string
target string
targetNote string
groupName string
tagsMap map[string]string
tagsArr []string
groupName string
alertRuleCache *memsto.AlertRuleCacheType
TargetCache *memsto.TargetCacheType
@@ -153,19 +151,30 @@ func (p *Processor) Handle(anomalyPoints []models.AnomalyPoint, from string, inh
eventsMap := make(map[string][]*models.AlertCurEvent)
for _, anomalyPoint := range anomalyPoints {
event := p.BuildEvent(anomalyPoint, from, now, ruleHash)
event.NotifyRuleIDs = cachedRule.NotifyRuleIds
// 如果 event 被 mute 了,本质也是 fire 的状态,这里无论如何都添加到 alertingKeys 中,防止 fire 的事件自动恢复了
hash := event.Hash
alertingKeys[hash] = struct{}{}
isMuted, detail := mute.IsMuted(cachedRule, event, p.TargetCache, p.alertMuteCache)
isMuted, detail, muteId := 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, detail:%s", p.Key(), event, detail)
p.Stats.CounterMuteTotal.WithLabelValues(
fmt.Sprintf("%v", event.GroupName),
fmt.Sprintf("%v", p.rule.Id),
fmt.Sprintf("%v", muteId),
fmt.Sprintf("%v", p.datasourceId),
).Inc()
continue
}
if p.EventMuteHook(event) {
p.Stats.CounterMuteTotal.WithLabelValues(event.GroupName).Inc()
logger.Debugf("rule_eval:%s event:%v is muted by hook", p.Key(), event)
p.Stats.CounterMuteTotal.WithLabelValues(
fmt.Sprintf("%v", event.GroupName),
fmt.Sprintf("%v", p.rule.Id),
fmt.Sprintf("%v", 0),
fmt.Sprintf("%v", p.datasourceId),
).Inc()
continue
}
@@ -184,7 +193,7 @@ func (p *Processor) Handle(anomalyPoints []models.AnomalyPoint, from string, inh
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)
ds := p.datasourceCache.GetById(p.datasourceId)
var dsName string
@@ -204,8 +213,6 @@ func (p *Processor) BuildEvent(anomalyPoint models.AnomalyPoint, from string, no
event.DatasourceId = p.datasourceId
event.Cluster = dsName
event.Hash = hash
event.TargetIdent = p.target
event.TargetNote = p.targetNote
event.TriggerValue = anomalyPoint.ReadableValue()
event.TriggerValues = anomalyPoint.Values
event.TriggerValuesJson = models.EventTriggerValues{ValuesWithUnit: anomalyPoint.ValuesUnit}
@@ -215,7 +222,6 @@ func (p *Processor) BuildEvent(anomalyPoint models.AnomalyPoint, from string, no
event.Callbacks = p.rule.Callbacks
event.CallbacksJSON = p.rule.CallbacksJSON
event.Annotations = p.rule.Annotations
event.AnnotationsJSON = make(map[string]string)
event.RuleConfig = p.rule.RuleConfig
event.RuleConfigJson = p.rule.RuleConfigJson
event.Severity = anomalyPoint.Severity
@@ -224,13 +230,18 @@ func (p *Processor) BuildEvent(anomalyPoint models.AnomalyPoint, from string, no
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 anomalyPoint.TriggerType == models.TriggerTypeNodata {
event.TriggerValue = "nodata"
ruleConfig := models.RuleQuery{}
json.Unmarshal([]byte(p.rule.RuleConfig), &ruleConfig)
ruleConfig.TriggerType = anomalyPoint.TriggerType
b, _ := json.Marshal(ruleConfig)
event.RuleConfig = string(b)
}
if err := json.Unmarshal([]byte(p.rule.Annotations), &event.AnnotationsJSON); err != nil {
event.AnnotationsJSON = make(map[string]string) // 解析失败时使用空 map
logger.Warningf("unmarshal annotations json failed: %v, rule: %d", err, p.rule.Id)
}
if event.TriggerValues != "" && strings.Count(event.TriggerValues, "$") > 1 {
@@ -246,6 +257,19 @@ func (p *Processor) BuildEvent(anomalyPoint models.AnomalyPoint, from string, no
// 生成事件之后,立马进程 relabel 处理
Relabel(p.rule, event)
// 放到 Relabel(p.rule, event) 下面,为了处理 relabel 之后,标签里才出现 ident 的情况
p.mayHandleIdent(event)
if event.TargetIdent != "" {
if pt, exist := p.TargetCache.Get(event.TargetIdent); exist {
pt.GroupNames = p.BusiGroupCache.GetNamesByBusiGroupIds(pt.GroupIds)
event.Target = pt
} else {
logger.Infof("fill event target error, ident: %s doesn't exist in cache.", event.TargetIdent)
}
}
return event
}
@@ -254,44 +278,15 @@ func Relabel(rule *models.AlertRule, event *models.AlertCurEvent) {
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]}
if len(rule.EventRelabelConfig) == 0 {
return
}
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, ",,")
relabel.EventRelabel(event, rule.EventRelabelConfig)
}
func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64, inhibit bool) {
@@ -411,8 +406,8 @@ func (p *Processor) RecoverSingle(byRecover bool, hash string, now int64, value
func (p *Processor) handleEvent(events []*models.AlertCurEvent) {
var fireEvents []*models.AlertCurEvent
// severity 初始为 4, 一定为遇到比自己优先级高的事件
severity := 4
// severity 初始为最低优先级, 一定为遇到比自己优先级高的事件
severity := models.SeverityLowest
for _, event := range events {
if event == nil {
continue
@@ -538,6 +533,12 @@ func (p *Processor) RecoverAlertCurEventFromDb() {
fireMap := make(map[string]*models.AlertCurEvent)
pendingsUseByRecoverMap := make(map[string]*models.AlertCurEvent)
for _, event := range curEvents {
alertRule := p.alertRuleCache.Get(event.RuleId)
if alertRule == nil {
continue
}
event.NotifyRuleIDs = alertRule.NotifyRuleIds
if event.Cate == models.HOST {
target, exists := p.TargetCache.Get(event.TargetIdent)
if exists && target.EngineName != p.EngineName && !(p.ctx.IsCenter && target.EngineName == "") {
@@ -610,19 +611,19 @@ func (p *Processor) fillTags(anomalyPoint models.AnomalyPoint) {
p.tagsArr = labelMapToArr(tagsMap)
}
func (p *Processor) mayHandleIdent() {
func (p *Processor) mayHandleIdent(event *models.AlertCurEvent) {
// handle ident
if ident, has := p.tagsMap["ident"]; has {
if ident, has := event.TagsMap["ident"]; has {
if target, exists := p.TargetCache.Get(ident); exists {
p.target = target.Ident
p.targetNote = target.Note
event.TargetIdent = target.Ident
event.TargetNote = target.Note
} else {
p.target = ident
p.targetNote = ""
event.TargetIdent = ident
event.TargetNote = ""
}
} else {
p.target = ""
p.targetNote = ""
event.TargetIdent = ""
event.TargetNote = ""
}
}

View File

@@ -39,7 +39,7 @@ func NewRecordRuleContext(rule *models.RecordingRule, datasourceId int64, promCl
rule.CronPattern = fmt.Sprintf("@every %ds", rule.PromEvalInterval)
}
rrc.scheduler = cron.New(cron.WithSeconds())
rrc.scheduler = cron.New(cron.WithSeconds(), cron.WithChain(cron.SkipIfStillRunning(cron.DefaultLogger)))
_, err := rrc.scheduler.AddFunc(rule.CronPattern, func() {
rrc.Eval()
})

View File

@@ -40,8 +40,8 @@ func (rt *Router) pushEventToQueue(c *gin.Context) {
event.TagsMap[arr[0]] = arr[1]
}
if mute.EventMuteStrategy(event, rt.AlertMuteCache) {
hit, _ := mute.EventMuteStrategy(event, rt.AlertMuteCache)
if hit {
logger.Infof("event_muted: rule_id=%d %s", event.RuleId, event.Hash)
ginx.NewRender(c).Message(nil)
return

View File

@@ -135,14 +135,14 @@ func (c *DefaultCallBacker) CallBack(ctx CallBackContext) {
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)
NotifyRecord(ctx, events, 0, channel, token, res, err)
}
func NotifyRecord(ctx *ctx.Context, evts []*models.AlertCurEvent, channel, target, res string, err error) {
func NotifyRecord(ctx *ctx.Context, evts []*models.AlertCurEvent, notifyRuleID int64, channel, target, res string, err error) {
// 一个通知可能对应多个 event都需要记录
notis := make([]*models.NotificaitonRecord, 0, len(evts))
for _, evt := range evts {
noti := models.NewNotificationRecord(evt, channel, target)
noti := models.NewNotificationRecord(evt, notifyRuleID, channel, target)
if err != nil {
noti.SetStatus(models.NotiStatusFailure)
noti.SetDetails(err.Error())
@@ -153,16 +153,14 @@ func NotifyRecord(ctx *ctx.Context, evts []*models.AlertCurEvent, channel, targe
}
if !ctx.IsCenter {
_, err := poster.PostByUrlsWithResp[[]int64](ctx, "/v1/n9e/notify-record", notis)
err := poster.PostByUrls(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)
}
PushNotifyRecords(notis)
}
func doSend(url string, body interface{}, channel string, stats *astats.Stats) (string, error) {

View File

@@ -205,7 +205,7 @@ func startEmailSender(ctx *ctx.Context, smtp aconf.SMTPConfig) {
if err == nil {
msg = "ok"
}
NotifyRecord(ctx, m.events, models.Email, to, msg, err)
NotifyRecord(ctx, m.events, 0, models.Email, to, msg, err)
}
size++

View File

@@ -30,12 +30,14 @@ type IbexCallBacker struct {
func (c *IbexCallBacker) CallBack(ctx CallBackContext) {
if len(ctx.CallBackURL) == 0 || len(ctx.Events) == 0 {
logger.Warningf("event_callback_ibex: url or events is empty, url: %s, events: %+v", ctx.CallBackURL, ctx.Events)
return
}
event := ctx.Events[0]
if event.IsRecovered {
logger.Infof("event_callback_ibex: event is recovered, event: %+v", event)
return
}
@@ -43,8 +45,9 @@ func (c *IbexCallBacker) CallBack(ctx CallBackContext) {
}
func (c *IbexCallBacker) handleIbex(ctx *ctx.Context, url string, event *models.AlertCurEvent) {
logger.Infof("event_callback_ibex: url: %s, event: %+v", url, event)
if imodels.DB() == nil && ctx.IsCenter {
logger.Warning("event_callback_ibex: db is nil")
logger.Warningf("event_callback_ibex: db is nil, event: %+v", event)
return
}
@@ -63,17 +66,23 @@ func (c *IbexCallBacker) handleIbex(ctx *ctx.Context, url string, event *models.
id, err := strconv.ParseInt(idstr, 10, 64)
if err != nil {
logger.Errorf("event_callback_ibex: failed to parse url: %s", url)
logger.Errorf("event_callback_ibex: failed to parse url: %s event: %+v", url, event)
return
}
if host == "" {
// 用户在callback url中没有传入host就从event中解析
host = event.TargetIdent
if host == "" {
if ident, has := event.TagsMap["ident"]; has {
host = ident
}
}
}
if host == "" {
logger.Error("event_callback_ibex: failed to get host")
logger.Errorf("event_callback_ibex: failed to get host, id: %d, event: %+v", id, event)
return
}
@@ -83,21 +92,23 @@ func (c *IbexCallBacker) handleIbex(ctx *ctx.Context, url string, event *models.
func CallIbex(ctx *ctx.Context, id int64, host string,
taskTplCache *memsto.TaskTplCache, targetCache *memsto.TargetCacheType,
userCache *memsto.UserCacheType, event *models.AlertCurEvent) {
logger.Infof("event_callback_ibex: id: %d, host: %s, event: %+v", id, host, event)
tpl := taskTplCache.Get(id)
if tpl == nil {
logger.Errorf("event_callback_ibex: no such tpl(%d)", id)
logger.Errorf("event_callback_ibex: no such tpl(%d), event: %+v", id, event)
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)
logger.Errorf("event_callback_ibex: check perm fail: %v, event: %+v", err, event)
return
}
if !can {
logger.Errorf("event_callback_ibex: user(%s) no permission", tpl.UpdateBy)
logger.Errorf("event_callback_ibex: user(%s) no permission, event: %+v", tpl.UpdateBy, event)
return
}
@@ -122,7 +133,7 @@ func CallIbex(ctx *ctx.Context, id int64, host string,
tags, err := json.Marshal(tagsMap)
if err != nil {
logger.Errorf("event_callback_ibex: failed to marshal tags to json: %v", tagsMap)
logger.Errorf("event_callback_ibex: failed to marshal tags to json: %v, event: %+v", tagsMap, event)
return
}
@@ -145,7 +156,7 @@ func CallIbex(ctx *ctx.Context, id int64, host string,
id, err = TaskAdd(in, tpl.UpdateBy, ctx.IsCenter)
if err != nil {
logger.Errorf("event_callback_ibex: call ibex fail: %v", err)
logger.Errorf("event_callback_ibex: call ibex fail: %v, event: %+v", err, event)
return
}
@@ -167,7 +178,7 @@ func CallIbex(ctx *ctx.Context, id int64, host string,
}
if err = record.Add(ctx); err != nil {
logger.Errorf("event_callback_ibex: persist task_record fail: %v", err)
logger.Errorf("event_callback_ibex: persist task_record fail: %v, event: %+v", err, event)
}
}
@@ -187,7 +198,7 @@ func canDoIbex(username string, tpl *models.TaskTpl, host string, targetCache *m
func TaskAdd(f models.TaskForm, authUser string, isCenter bool) (int64, error) {
if storage.Cache == nil {
logger.Warning("event_callback_ibex: redis cache is nil")
logger.Warningf("event_callback_ibex: redis cache is nil, task: %+v", f)
return 0, fmt.Errorf("redis cache is nil")
}

View File

@@ -74,7 +74,7 @@ func SendMM(ctx *ctx.Context, message MatterMostMessage, events []*models.AlertC
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)
NotifyRecord(ctx, events, 0, channel, message.Tokens[i], "", err)
continue
}

View File

@@ -0,0 +1,75 @@
package sender
import (
"errors"
"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/container/list"
"github.com/toolkits/pkg/logger"
)
// 通知记录队列,最大长度 1000000
var NotifyRecordQueue = list.NewSafeListLimited(1000000)
// 每秒上报通知记录队列大小
func ReportNotifyRecordQueueSize(stats *astats.Stats) {
for {
time.Sleep(time.Second)
stats.GaugeNotifyRecordQueueSize.Set(float64(NotifyRecordQueue.Len()))
}
}
// 推送通知记录到队列
// 若队列满 则返回 error
func PushNotifyRecords(records []*models.NotificaitonRecord) error {
for _, record := range records {
if ok := NotifyRecordQueue.PushFront(record); !ok {
logger.Warningf("notify record queue is full, record: %+v", record)
return errors.New("notify record queue is full")
}
}
return nil
}
type NotifyRecordConsumer struct {
ctx *ctx.Context
}
func NewNotifyRecordConsumer(ctx *ctx.Context) *NotifyRecordConsumer {
return &NotifyRecordConsumer{
ctx: ctx,
}
}
// 消费通知记录队列 每 100ms 检测一次队列是否为空
func (c *NotifyRecordConsumer) LoopConsume() {
duration := time.Duration(100) * time.Millisecond
for {
// 无论队列是否为空 都需要等待
time.Sleep(duration)
inotis := NotifyRecordQueue.PopBackBy(100)
if len(inotis) == 0 {
continue
}
// 类型转换,不然 CreateInBatches 会报错
notis := make([]*models.NotificaitonRecord, 0, len(inotis))
for _, inoti := range inotis {
notis = append(notis, inoti.(*models.NotificaitonRecord))
}
c.consume(notis)
}
}
func (c *NotifyRecordConsumer) consume(notis []*models.NotificaitonRecord) {
if err := models.DB(c.ctx).CreateInBatches(notis, 100).Error; err != nil {
logger.Errorf("add notis:%v failed, err: %v", notis, err)
}
}

View File

@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"time"
"unicode/utf8"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/models"
@@ -85,7 +86,25 @@ func alertingCallScript(ctx *ctx.Context, stdinBytes []byte, notifyScript models
}
err, isTimeout := sys.WrapTimeout(cmd, time.Duration(config.Timeout)*time.Second)
NotifyRecord(ctx, []*models.AlertCurEvent{event}, channel, cmd.String(), "", buildErr(err, isTimeout))
res := buf.String()
// 截断超出长度的输出
if len(res) > 512 {
// 确保在有效的UTF-8字符边界处截断
validLen := 0
for i := 0; i < 512 && i < len(res); {
_, size := utf8.DecodeRuneInString(res[i:])
if i+size > 512 {
break
}
i += size
validLen = i
}
res = res[:validLen] + "..."
}
NotifyRecord(ctx, []*models.AlertCurEvent{event}, 0, channel, cmd.String(), res, buildErr(err, isTimeout))
if isTimeout {
if err == nil {
@@ -100,12 +119,12 @@ func alertingCallScript(ctx *ctx.Context, stdinBytes []byte, notifyScript models
}
if err != nil {
logger.Errorf("event_script_notify_fail: exec script %s occur error: %v, output: %s", fpath, err, buf.String())
logger.Errorf("event_script_notify_fail: exec script %s occur error: %v, output: %s", fpath, err, res)
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
return
}
logger.Infof("event_script_notify_ok: exec %s output: %s", fpath, buf.String())
logger.Infof("event_script_notify_ok: exec %s output: %s", fpath, res)
}
func buildErr(err error, isTimeout bool) error {

View File

@@ -72,7 +72,7 @@ func SendTelegram(ctx *ctx.Context, message TelegramMessage, events []*models.Al
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"))
NotifyRecord(ctx, events, 0, channel, message.Tokens[i], "", errors.New("invalid token"))
continue
}
var url string

View File

@@ -100,7 +100,7 @@ func SingleSendWebhooks(ctx *ctx.Context, webhooks map[string]*models.Webhook, e
retryCount := 0
for retryCount < 3 {
needRetry, res, err := sendWebhook(conf, event, stats)
NotifyRecord(ctx, []*models.AlertCurEvent{event}, "webhook", conf.Url, res, err)
NotifyRecord(ctx, []*models.AlertCurEvent{event}, 0, "webhook", conf.Url, res, err)
if !needRetry {
break
}
@@ -170,7 +170,7 @@ func StartConsumer(ctx *ctx.Context, queue *WebhookQueue, popSize int, webhook *
retryCount := 0
for retryCount < webhook.RetryCount {
needRetry, res, err := sendWebhook(webhook, events, stats)
go NotifyRecord(ctx, events, "webhook", webhook.Url, res, err)
go NotifyRecord(ctx, events, 0, "webhook", webhook.Url, res, err)
if !needRetry {
break
}

View File

@@ -15,9 +15,25 @@ type Operation struct {
}
type Ops struct {
Name string `yaml:"name" json:"name"`
Cname string `yaml:"cname" json:"cname"`
Ops []string `yaml:"ops" json:"ops"`
Name string `yaml:"name" json:"name"`
Cname string `yaml:"cname" json:"cname"`
Ops []SingleOp `yaml:"ops" json:"ops"`
}
// SingleOp Name 为 op 名称Cname 为展示名称,默认英文
type SingleOp struct {
Name string `yaml:"name" json:"name"`
Cname string `yaml:"cname" json:"cname"`
}
func TransformNames(name []string, nameToName map[string]string) []string {
var ret []string
for _, n := range name {
if v, has := nameToName[n]; has {
ret = append(ret, v)
}
}
return ret
}
func LoadOpsYaml(configDir string, opsYamlFile string) error {
@@ -39,8 +55,8 @@ func LoadOpsYaml(configDir string, opsYamlFile string) error {
return file.ReadYaml(fp, &Operations)
}
func GetAllOps(ops []Ops) []string {
var ret []string
func GetAllOps(ops []Ops) []SingleOp {
var ret []SingleOp
for _, op := range ops {
ret = append(ret, op.Ops...)
}
@@ -48,7 +64,7 @@ func GetAllOps(ops []Ops) []string {
}
func MergeOperationConf() error {
opsBuiltIn := Operation{}
var opsBuiltIn Operation
err := yaml.Unmarshal([]byte(builtInOps), &opsBuiltIn)
if err != nil {
return fmt.Errorf("cannot parse builtInOps: %s", err.Error())
@@ -70,137 +86,272 @@ const (
builtInOps = `
ops:
- name: dashboards
cname: 仪表盘
cname: Dashboards
ops:
- "/dashboards"
- "/dashboards/add"
- "/dashboards/put"
- "/dashboards/del"
- "/embedded-dashboards/put"
- "/embedded-dashboards"
- "/public-dashboards"
- name: alert
cname: 告警规则
ops:
- "/alert-rules"
- "/alert-rules/add"
- "/alert-rules/put"
- "/alert-rules/del"
- name: alert-mutes
cname: 告警静默管理
ops:
- "/alert-mutes"
- "/alert-mutes/add"
- "/alert-mutes/put"
- "/alert-mutes/del"
- name: alert-subscribes
cname: 告警订阅管理
ops:
- "/alert-subscribes"
- "/alert-subscribes/add"
- "/alert-subscribes/put"
- "/alert-subscribes/del"
- name: alert-events
cname: 告警事件管理
ops:
- "/alert-cur-events"
- "/alert-cur-events/del"
- "/alert-his-events"
- name: recording-rules
cname: 记录规则管理
ops:
- "/recording-rules"
- "/recording-rules/add"
- "/recording-rules/put"
- "/recording-rules/del"
- name: "/dashboards"
cname: View Dashboards
- name: "/dashboards/add"
cname: Add Dashboard
- name: "/dashboards/put"
cname: Modify Dashboard
- name: "/dashboards/del"
cname: Delete Dashboard
- name: "/embedded-dashboards/put"
cname: Modify Embedded Dashboard
- name: "/embedded-dashboards"
cname: View Embedded Dashboard
- name: "/public-dashboards"
cname: View Public Dashboard
- name: metric
cname: 时序指标
cname: Time Series Metrics
ops:
- "/metric/explorer"
- "/object/explorer"
- name: log
cname: 日志分析
ops:
- "/log/explorer"
- "/log/index-patterns"
- name: targets
cname: 基础设施
ops:
- "/targets"
- "/targets/add"
- "/targets/put"
- "/targets/del"
- "/targets/bind"
- name: job
cname: 任务管理
ops:
- "/job-tpls"
- "/job-tpls/add"
- "/job-tpls/put"
- "/job-tpls/del"
- "/job-tasks"
- "/job-tasks/add"
- "/job-tasks/put"
- "/ibex-settings"
- name: user
cname: 用户管理
ops:
- "/users"
- "/user-groups"
- "/user-groups/add"
- "/user-groups/put"
- "/user-groups/del"
- name: permissions
cname: 权限管理
ops:
- "/permissions"
- name: busi-groups
cname: 业务分组管理
ops:
- "/busi-groups"
- "/busi-groups/add"
- "/busi-groups/put"
- "/busi-groups/del"
- name: "/metric/explorer"
cname: View Metric Data
- name: "/object/explorer"
cname: View Object Data
- name: builtin-metrics
cname: 指标视图
cname: Metric Views
ops:
- "/metrics-built-in"
- "/builtin-metrics/add"
- "/builtin-metrics/put"
- "/builtin-metrics/del"
- name: "/metrics-built-in"
cname: View Built-in Metrics
- name: "/builtin-metrics/add"
cname: Add Built-in Metric
- name: "/builtin-metrics/put"
cname: Modify Built-in Metric
- name: "/builtin-metrics/del"
cname: Delete Built-in Metric
- name: recording-rules
cname: Recording Rule Management
ops:
- name: "/recording-rules"
cname: View Recording Rules
- name: "/recording-rules/add"
cname: Add Recording Rule
- name: "/recording-rules/put"
cname: Modify Recording Rule
- name: "/recording-rules/del"
cname: Delete Recording Rule
- name: log
cname: Log Analysis
ops:
- name: "/log/explorer"
cname: View Logs
- name: "/log/index-patterns"
cname: View Index Patterns
- name: "/log/index-patterns/add"
cname: Add Index Pattern
- name: "/log/index-patterns/put"
cname: Modify Index Pattern
- name: "/log/index-patterns/del"
cname: Delete Index Pattern
- name: alert
cname: Alert Rules
ops:
- name: "/alert-rules"
cname: View Alert Rules
- name: "/alert-rules/add"
cname: Add Alert Rule
- name: "/alert-rules/put"
cname: Modify Alert Rule
- name: "/alert-rules/del"
cname: Delete Alert Rule
- name: alert-mutes
cname: Alert Silence Management
ops:
- name: "/alert-mutes"
cname: View Alert Silences
- name: "/alert-mutes/add"
cname: Add Alert Silence
- name: "/alert-mutes/put"
cname: Modify Alert Silence
- name: "/alert-mutes/del"
cname: Delete Alert Silence
- name: alert-subscribes
cname: Alert Subscription Management
ops:
- name: "/alert-subscribes"
cname: View Alert Subscriptions
- name: "/alert-subscribes/add"
cname: Add Alert Subscription
- name: "/alert-subscribes/put"
cname: Modify Alert Subscription
- name: "/alert-subscribes/del"
cname: Delete Alert Subscription
- name: alert-events
cname: Alert Event Management
ops:
- name: "/alert-cur-events"
cname: View Current Alerts
- name: "/alert-cur-events/del"
cname: Delete Current Alert
- name: "/alert-his-events"
cname: View Historical Alerts
- name: notification
cname: Alert Notification
ops:
- name: "/help/notification-settings"
cname: View Notification Settings
- name: "/help/notification-tpls"
cname: View Notification Templates
- name: job
cname: Task Management
ops:
- name: "/job-tpls"
cname: View Task Templates
- name: "/job-tpls/add"
cname: Add Task Template
- name: "/job-tpls/put"
cname: Modify Task Template
- name: "/job-tpls/del"
cname: Delete Task Template
- name: "/job-tasks"
cname: View Task Instances
- name: "/job-tasks/add"
cname: Add Task Instance
- name: "/job-tasks/put"
cname: Modify Task Instance
- name: targets
cname: Infrastructure
ops:
- name: "/targets"
cname: View Objects
- name: "/targets/add"
cname: Add Object
- name: "/targets/put"
cname: Modify Object
- name: "/targets/del"
cname: Delete Object
- name: "/targets/bind"
cname: Bind Object
- name: user
cname: User Management
ops:
- name: "/users"
cname: View User List
- name: "/user-groups"
cname: View User Groups
- name: "/user-groups/add"
cname: Add User Group
- name: "/user-groups/put"
cname: Modify User Group
- name: "/user-groups/del"
cname: Delete User Group
- name: busi-groups
cname: Business Group Management
ops:
- name: "/busi-groups"
cname: View Business Groups
- name: "/busi-groups/add"
cname: Add Business Group
- name: "/busi-groups/put"
cname: Modify Business Group
- name: "/busi-groups/del"
cname: Delete Business Group
- name: permissions
cname: Permission Management
ops:
- name: "/permissions"
cname: View Permission Settings
- name: contacts
cname: User Contact Management
ops:
- name: "/contacts"
cname: User Contact Management
- name: built-in-components
cname: 模版中心
cname: Template Center
ops:
- "/built-in-components"
- "/built-in-components/add"
- "/built-in-components/put"
- "/built-in-components/del"
- name: "/built-in-components"
cname: View Built-in Components
- name: "/built-in-components/add"
cname: Add Built-in Component
- name: "/built-in-components/put"
cname: Modify Built-in Component
- name: "/built-in-components/del"
cname: Delete Built-in Component
- name: datasource
cname: Data Source Management
ops:
- name: "/help/source"
cname: View Data Source Configuration
- name: system
cname: 系统信息
cname: System Information
ops:
- "/help/variable-configs"
- "/help/version"
- "/help/servers"
- "/help/source"
- "/help/sso"
- "/help/notification-tpls"
- "/help/notification-settings"
- "/help/migrate"
- "/site-settings"
- name: "/help/variable-configs"
cname: View Variable Configuration
- name: "/help/version"
cname: View Version Information
- name: "/help/servers"
cname: View Server Information
- name: "/help/sso"
cname: View SSO Configuration
- name: "/site-settings"
cname: View Site Settings
- name: message-templates
cname: Message Templates
ops:
- name: "/notification-templates"
cname: View Message Templates
- name: "/notification-templates/add"
cname: Add Message Templates
- name: "/notification-templates/put"
cname: Modify Message Templates
- name: "/notification-templates/del"
cname: Delete Message Templates
- name: notify-rules
cname: Notify Rules
ops:
- name: "/notification-rules"
cname: View Notify Rules
- name: "/notification-rules/add"
cname: Add Notify Rules
- name: "/notification-rules/put"
cname: Modify Notify Rules
- name: "/notification-rules/del"
cname: Delete Notify Rules
- name: event-pipelines
cname: Event Pipelines
ops:
- name: "/event-pipelines"
cname: View Event Pipelines
- name: "/event-pipelines/add"
cname: Add Event Pipeline
- name: "/event-pipelines/put"
cname: Modify Event Pipeline
- name: "/event-pipelines/del"
cname: Delete Event Pipeline
- name: notify-channels
cname: Notify Channels
ops:
- name: "/notification-channels"
cname: View Notify Channels
- name: "/notification-channels/add"
cname: Add Notify Channels
- name: "/notification-channels/put"
cname: Modify Notify Channels
- name: "/notification-channels/del"
cname: Delete Notify Channels
`
)

View File

@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/alert"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/dispatch"
@@ -27,14 +29,13 @@ import (
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/pkg/i18nx"
"github.com/ccfos/nightingale/v6/pkg/logx"
"github.com/ccfos/nightingale/v6/pkg/macros"
"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"
"github.com/flashcatcloud/ibex/src/cmd/ibex"
)
@@ -90,6 +91,10 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
syncStats := memsto.NewSyncStats()
alertStats := astats.NewSyncStats()
if config.Center.MigrateBusiGroupLabel || models.CanMigrateBg(ctx) {
models.MigrateBg(ctx, config.Pushgw.BusiGroupLabelKey)
}
configCache := memsto.NewConfigCache(ctx, syncStats, config.HTTP.RSA.RSAPrivateKey, config.HTTP.RSA.RSAPassWord)
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
targetCache := memsto.NewTargetCache(ctx, syncStats, redis)
@@ -101,16 +106,21 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
taskTplCache := memsto.NewTaskTplCache(ctx)
configCvalCache := memsto.NewCvalCache(ctx, syncStats)
notifyRuleCache := memsto.NewNotifyRuleCache(ctx, syncStats)
notifyChannelCache := memsto.NewNotifyChannelCache(ctx, syncStats)
messageTemplateCache := memsto.NewMessageTemplateCache(ctx, syncStats)
userTokenCache := memsto.NewUserTokenCache(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, taskTplCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
macros.RegisterMacro(macros.MacroInVain)
dscache.Init(ctx, false)
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplCache, dsCache, ctx, promClients, userCache, userGroupCache, notifyRuleCache, notifyChannelCache, messageTemplateCache)
writers := writer.NewWriters(config.Pushgw)
@@ -120,16 +130,10 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
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)
cconf.Operations, dsCache, notifyConfigCache, promClients,
redis, sso, ctx, metas, idents, targetCache, userCache, userGroupCache, userTokenCache)
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)
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)

View File

@@ -22,6 +22,14 @@ func Init(ctx *ctx.Context, builtinIntegrationsDir string) {
return
}
if res, err := models.ConfigsSelectByCkey(ctx, "disable_integration_init"); err != nil {
logger.Error("fail to get value 'disable_integration_init' from configs", err)
return
} else if len(res) != 0 {
logger.Info("disable_integration_init is set, skip integration init")
return
}
fp := builtinIntegrationsDir
if fp == "" {
fp = path.Join(runner.Cwd, "integrations")

View File

@@ -24,7 +24,7 @@ import (
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/pushgw/idents"
"github.com/ccfos/nightingale/v6/storage"
"github.com/ccfos/nightingale/v6/tdengine"
"gorm.io/gorm"
"github.com/gin-gonic/gin"
"github.com/rakyll/statik/fs"
@@ -42,7 +42,6 @@ type Router struct {
DatasourceCache *memsto.DatasourceCacheType
NotifyConfigCache *memsto.NotifyConfigCacheType
PromClients *prom.PromClientMap
TdendgineClients *tdengine.TdengineClientMap
Redis storage.Redis
MetaSet *metas.Set
IdentSet *idents.Set
@@ -50,43 +49,43 @@ type Router struct {
Sso *sso.SsoClient
UserCache *memsto.UserCacheType
UserGroupCache *memsto.UserGroupCacheType
UserTokenCache *memsto.UserTokenCacheType
Ctx *ctx.Context
HeartbeatHook HeartbeatHookFunc
TargetDeleteHook models.TargetDeleteHookFunc
HeartbeatHook HeartbeatHookFunc
TargetDeleteHook models.TargetDeleteHookFunc
AlertRuleModifyHook AlertRuleModifyHookFunc
}
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,
pc *prom.PromClientMap, 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 {
tc *memsto.TargetCacheType, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType, utc *memsto.UserTokenCacheType) *Router {
return &Router{
HTTP: httpConfig,
Center: center,
Alert: alert,
Ibex: ibex,
Operations: operations,
DatasourceCache: ds,
NotifyConfigCache: ncc,
PromClients: pc,
TdendgineClients: tdendgineClients,
Redis: redis,
MetaSet: metaSet,
IdentSet: idents,
TargetCache: tc,
Sso: sso,
UserCache: uc,
UserGroupCache: ugc,
Ctx: ctx,
HeartbeatHook: func(ident string) map[string]interface{} { return nil },
TargetDeleteHook: emptyDeleteHook,
HTTP: httpConfig,
Center: center,
Alert: alert,
Ibex: ibex,
Operations: operations,
DatasourceCache: ds,
NotifyConfigCache: ncc,
PromClients: pc,
Redis: redis,
MetaSet: metaSet,
IdentSet: idents,
TargetCache: tc,
Sso: sso,
UserCache: uc,
UserGroupCache: ugc,
UserTokenCache: utc,
Ctx: ctx,
HeartbeatHook: func(ident string) map[string]interface{} { return nil },
TargetDeleteHook: func(tx *gorm.DB, idents []string) error { return nil },
AlertRuleModifyHook: func(ar *models.AlertRule) {},
}
}
func emptyDeleteHook(ctx *ctx.Context, idents []string) error {
return nil
}
func stat() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
@@ -187,12 +186,24 @@ func (rt *Router) Config(r *gin.Engine) {
pages.POST("/datasource/query", rt.datasourceQuery)
pages.POST("/ds-query", rt.QueryData)
pages.POST("/logs-query", rt.QueryLog)
pages.POST("/logs-query", rt.QueryLogV2)
pages.POST("/tdengine-databases", rt.tdengineDatabases)
pages.POST("/tdengine-tables", rt.tdengineTables)
pages.POST("/tdengine-columns", rt.tdengineColumns)
pages.POST("/log-query-batch", rt.QueryLogBatch)
// 数据库元数据接口
pages.POST("/db-databases", rt.ShowDatabases)
pages.POST("/db-tables", rt.ShowTables)
pages.POST("/db-desc-table", rt.DescribeTable)
// es 专用接口
pages.POST("/indices", rt.auth(), rt.user(), rt.QueryIndices)
pages.POST("/es-variable", rt.auth(), rt.user(), rt.QueryESVariable)
pages.POST("/fields", rt.auth(), rt.user(), rt.QueryFields)
pages.POST("/log-query", rt.auth(), rt.user(), rt.QueryLog)
} else {
pages.Any("/proxy/:id/*url", rt.auth(), rt.dsProxy)
pages.POST("/query-range-batch", rt.auth(), rt.promBatchQueryRange)
@@ -201,11 +212,24 @@ func (rt *Router) Config(r *gin.Engine) {
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)
pages.POST("/logs-query", rt.auth(), rt.QueryLogV2)
pages.POST("/tdengine-databases", rt.auth(), rt.tdengineDatabases)
pages.POST("/tdengine-tables", rt.auth(), rt.tdengineTables)
pages.POST("/tdengine-columns", rt.auth(), rt.tdengineColumns)
pages.POST("/log-query-batch", rt.auth(), rt.user(), rt.QueryLogBatch)
// 数据库元数据接口
pages.POST("/db-databases", rt.auth(), rt.user(), rt.ShowDatabases)
pages.POST("/db-tables", rt.auth(), rt.user(), rt.ShowTables)
pages.POST("/db-desc-table", rt.auth(), rt.user(), rt.DescribeTable)
// es 专用接口
pages.POST("/indices", rt.auth(), rt.user(), rt.QueryIndices)
pages.POST("/es-variable", rt.QueryESVariable)
pages.POST("/fields", rt.QueryFields)
pages.POST("/log-query", rt.QueryLog)
}
pages.GET("/sql-template", rt.QuerySqlTemplate)
@@ -236,6 +260,9 @@ func (rt *Router) Config(r *gin.Engine) {
pages.GET("/self/profile", rt.auth(), rt.user(), rt.selfProfileGet)
pages.PUT("/self/profile", rt.auth(), rt.user(), rt.selfProfilePut)
pages.PUT("/self/password", rt.auth(), rt.user(), rt.selfPasswordPut)
pages.GET("/self/token", rt.auth(), rt.user(), rt.getToken)
pages.POST("/self/token", rt.auth(), rt.user(), rt.addToken)
pages.DELETE("/self/token/:id", rt.auth(), rt.user(), rt.deleteToken)
pages.GET("/users", rt.auth(), rt.user(), rt.perm("/users"), rt.userGets)
pages.POST("/users", rt.auth(), rt.admin(), rt.userAddPost)
@@ -322,6 +349,11 @@ func (rt *Router) Config(r *gin.Engine) {
pages.GET("/share-charts", rt.chartShareGets)
pages.POST("/share-charts", rt.auth(), rt.chartShareAdd)
pages.POST("/dashboard-annotations", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.dashAnnotationAdd)
pages.GET("/dashboard-annotations", rt.dashAnnotationGets)
pages.PUT("/dashboard-annotation/:id", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.dashAnnotationPut)
pages.DELETE("/dashboard-annotation/:id", rt.auth(), rt.user(), rt.perm("/dashboards/del"), rt.dashAnnotationDel)
// 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)
@@ -340,6 +372,7 @@ func (rt *Router) Config(r *gin.Engine) {
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.POST("/busi-groups/alert-rules/clones", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.batchAlertRuleClone)
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)
@@ -448,9 +481,9 @@ func (rt *Router) Config(r *gin.Engine) {
pages.GET("/es-index-pattern", rt.auth(), rt.esIndexPatternGet)
pages.GET("/es-index-pattern-list", rt.auth(), rt.esIndexPatternGetList)
pages.POST("/es-index-pattern", rt.auth(), rt.admin(), rt.esIndexPatternAdd)
pages.PUT("/es-index-pattern", rt.auth(), rt.admin(), rt.esIndexPatternPut)
pages.DELETE("/es-index-pattern", rt.auth(), rt.admin(), rt.esIndexPatternDel)
pages.POST("/es-index-pattern", rt.auth(), rt.user(), rt.perm("/log/index-patterns/add"), rt.esIndexPatternAdd)
pages.PUT("/es-index-pattern", rt.auth(), rt.user(), rt.perm("/log/index-patterns/put"), rt.esIndexPatternPut)
pages.DELETE("/es-index-pattern", rt.auth(), rt.user(), rt.perm("/log/index-patterns/del"), 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)
@@ -479,6 +512,41 @@ func (rt *Router) Config(r *gin.Engine) {
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)
pages.POST("/message-templates", rt.auth(), rt.user(), rt.perm("/notification-templates/add"), rt.messageTemplatesAdd)
pages.DELETE("/message-templates", rt.auth(), rt.user(), rt.perm("/notification-templates/del"), rt.messageTemplatesDel)
pages.PUT("/message-template/:id", rt.auth(), rt.user(), rt.perm("/notification-templates/put"), rt.messageTemplatePut)
pages.GET("/message-template/:id", rt.auth(), rt.user(), rt.perm("/notification-templates"), rt.messageTemplateGet)
pages.GET("/message-templates", rt.auth(), rt.user(), rt.messageTemplatesGet)
pages.POST("/events-message", rt.auth(), rt.user(), rt.eventsMessage)
pages.POST("/notify-rules", rt.auth(), rt.user(), rt.perm("/notification-rules/add"), rt.notifyRulesAdd)
pages.DELETE("/notify-rules", rt.auth(), rt.user(), rt.perm("/notification-rules/del"), rt.notifyRulesDel)
pages.PUT("/notify-rule/:id", rt.auth(), rt.user(), rt.perm("/notification-rules/put"), rt.notifyRulePut)
pages.GET("/notify-rule/:id", rt.auth(), rt.user(), rt.perm("/notification-rules"), rt.notifyRuleGet)
pages.GET("/notify-rules", rt.auth(), rt.user(), rt.perm("/notification-rules"), rt.notifyRulesGet)
pages.POST("/notify-rule/test", rt.auth(), rt.user(), rt.perm("/notification-rules"), rt.notifyTest)
pages.GET("/notify-rule/custom-params", rt.auth(), rt.user(), rt.perm("/notification-rules"), rt.notifyRuleCustomParamsGet)
pages.POST("/notify-rule/event-pipelines-tryrun", rt.auth(), rt.user(), rt.perm("/notification-rules/add"), rt.tryRunEventProcessorByNotifyRule)
// 事件Pipeline相关路由
pages.GET("/event-pipelines", rt.auth(), rt.user(), rt.perm("/event-pipelines"), rt.eventPipelinesList)
pages.POST("/event-pipeline", rt.auth(), rt.user(), rt.perm("/event-pipelines/add"), rt.addEventPipeline)
pages.PUT("/event-pipeline", rt.auth(), rt.user(), rt.perm("/event-pipelines/put"), rt.updateEventPipeline)
pages.GET("/event-pipeline/:id", rt.auth(), rt.user(), rt.perm("/event-pipelines"), rt.getEventPipeline)
pages.DELETE("/event-pipelines", rt.auth(), rt.user(), rt.perm("/event-pipelines/del"), rt.deleteEventPipelines)
pages.POST("/event-pipeline-tryrun", rt.auth(), rt.user(), rt.perm("/event-pipelines"), rt.tryRunEventPipeline)
pages.POST("/event-processor-tryrun", rt.auth(), rt.user(), rt.perm("/event-pipelines"), rt.tryRunEventProcessor)
pages.POST("/notify-channel-configs", rt.auth(), rt.user(), rt.perm("/notification-channels/add"), rt.notifyChannelsAdd)
pages.DELETE("/notify-channel-configs", rt.auth(), rt.user(), rt.perm("/notification-channels/del"), rt.notifyChannelsDel)
pages.PUT("/notify-channel-config/:id", rt.auth(), rt.user(), rt.perm("/notification-channels/put"), rt.notifyChannelPut)
pages.GET("/notify-channel-config/:id", rt.auth(), rt.user(), rt.perm("/notification-channels"), rt.notifyChannelGet)
pages.GET("/notify-channel-configs", rt.auth(), rt.user(), rt.perm("/notification-channels"), rt.notifyChannelsGet)
pages.GET("/simplified-notify-channel-configs", rt.notifyChannelsGetForNormalUser)
pages.GET("/flashduty-channel-list/:id", rt.auth(), rt.user(), rt.flashDutyNotifyChannelsGet)
pages.GET("/notify-channel-config", rt.auth(), rt.user(), rt.notifyChannelGetBy)
pages.GET("/notify-channel-config/idents", rt.notifyChannelIdentsGet)
}
r.GET("/api/n9e/versions", func(c *gin.Context) {
@@ -521,6 +589,8 @@ func (rt *Router) Config(r *gin.Engine) {
service.PUT("/targets/note", rt.targetUpdateNoteByService)
service.PUT("/targets/bgid", rt.targetUpdateBgidByService)
service.POST("/targets-of-host-query", rt.targetsOfHostQuery)
service.POST("/alert-rules", rt.alertRuleAddByService)
service.POST("/alert-rule-add", rt.alertRuleAddOneByService)
service.DELETE("/alert-rules", rt.alertRuleDelByService)
@@ -578,6 +648,16 @@ func (rt *Router) Config(r *gin.Engine) {
service.GET("/alert-cur-events-del-by-hash", rt.alertCurEventDelByHash)
service.POST("/center/heartbeat", rt.heartbeat)
service.GET("/es-index-pattern-list", rt.esIndexPatternGetList)
service.GET("/notify-rules", rt.notifyRulesGetByService)
service.GET("/notify-channels", rt.notifyChannelConfigGets)
service.GET("/message-templates", rt.messageTemplateGets)
service.GET("/event-pipelines", rt.eventPipelinesListByService)
}
}

View File

@@ -12,6 +12,7 @@ import (
"gopkg.in/yaml.v2"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/ccfos/nightingale/v6/pushgw/writer"
@@ -20,9 +21,10 @@ import (
"github.com/prometheus/prometheus/prompb"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/i18n"
"github.com/toolkits/pkg/str"
)
type AlertRuleModifyHookFunc func(ar *models.AlertRule)
// Return all, front-end search and paging
func (rt *Router) alertRuleGets(c *gin.Context) {
busiGroupId := ginx.UrlParamInt64(c, "id")
@@ -50,7 +52,7 @@ func getAlertCueEventTimeRange(c *gin.Context) (stime, etime int64) {
}
func (rt *Router) alertRuleGetsByGids(c *gin.Context) {
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
gids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "gids", ""), ",")
if len(gids) > 0 {
for _, gid := range gids {
rt.bgroCheck(c, gid)
@@ -426,7 +428,16 @@ func (rt *Router) alertRulePutFields(c *gin.Context) {
}
for k, v := range f.Fields {
ginx.Dangerous(ar.UpdateColumn(rt.Ctx, k, v))
// 检查 v 是否为各种切片类型
switch v.(type) {
case []interface{}, []int64, []int, []string:
// 将切片转换为 JSON 字符串
bytes, err := json.Marshal(v)
ginx.Dangerous(err)
ginx.Dangerous(ar.UpdateColumn(rt.Ctx, k, string(bytes)))
default:
ginx.Dangerous(ar.UpdateColumn(rt.Ctx, k, v))
}
}
}
@@ -451,6 +462,7 @@ func (rt *Router) alertRuleGet(c *gin.Context) {
err = ar.FillNotifyGroups(rt.Ctx, make(map[int64]*models.UserGroup))
ginx.Dangerous(err)
rt.AlertRuleModifyHook(ar)
ginx.NewRender(c).Data(ar, err)
}
@@ -675,3 +687,49 @@ func (rt *Router) cloneToMachine(c *gin.Context) {
ginx.NewRender(c).Data(reterr, models.InsertAlertRule(rt.Ctx, newRules))
}
type alertBatchCloneForm struct {
RuleIds []int64 `json:"rule_ids"`
Bgids []int64 `json:"bgids"`
}
// 批量克隆告警规则
func (rt *Router) batchAlertRuleClone(c *gin.Context) {
me := c.MustGet("user").(*models.User)
var f alertBatchCloneForm
ginx.BindJSON(c, &f)
// 校验 bgids 操作权限
for _, bgid := range f.Bgids {
rt.bgrwCheck(c, bgid)
}
reterr := make(map[string]string, len(f.RuleIds))
lang := c.GetHeader("X-Language")
for _, arid := range f.RuleIds {
ar, err := models.AlertRuleGetById(rt.Ctx, arid)
for _, bgid := range f.Bgids {
// 为了让 bgid 和 arid 对应,将上面的 err 放到这里处理
if err != nil {
reterr[fmt.Sprintf("%d-%d", arid, bgid)] = i18n.Sprintf(lang, err.Error())
continue
}
if ar == nil {
reterr[fmt.Sprintf("%d-%d", arid, bgid)] = i18n.Sprintf(lang, "alert rule not found")
continue
}
newAr := ar.Clone(me.Username, bgid)
err = newAr.Add(rt.Ctx)
if err != nil {
reterr[fmt.Sprintf("%d-%d", arid, bgid)] = i18n.Sprintf(lang, err.Error())
continue
}
}
}
ginx.NewRender(c).Data(reterr, nil)
}

View File

@@ -5,10 +5,10 @@ import (
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/str"
)
// Return all, front-end search and paging
@@ -31,7 +31,7 @@ func (rt *Router) alertSubscribeGets(c *gin.Context) {
}
func (rt *Router) alertSubscribeGetsByGids(c *gin.Context) {
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
gids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "gids", ""), ",")
if len(gids) > 0 {
for _, gid := range gids {
rt.bgroCheck(c, gid)
@@ -141,6 +141,7 @@ func (rt *Router) alertSubscribePut(c *gin.Context) {
"extra_config",
"busi_groups",
"note",
"notify_rule_ids",
))
}

View File

@@ -6,11 +6,11 @@ import (
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/i18n"
"github.com/toolkits/pkg/str"
)
type boardForm struct {
@@ -51,9 +51,14 @@ func (rt *Router) boardAdd(c *gin.Context) {
func (rt *Router) boardGet(c *gin.Context) {
bid := ginx.UrlParamStr(c, "bid")
board, err := models.BoardGet(rt.Ctx, "id = ? or ident = ?", bid, bid)
board, err := models.BoardGet(rt.Ctx, "ident = ?", bid)
ginx.Dangerous(err)
if board == nil {
board, err = models.BoardGet(rt.Ctx, "id = ?", bid)
ginx.Dangerous(err)
}
if board == nil {
ginx.Bomb(http.StatusNotFound, "No such dashboard")
}
@@ -96,7 +101,7 @@ func (rt *Router) boardGet(c *gin.Context) {
// 根据 bids 参数,获取多个 board
func (rt *Router) boardGetsByBids(c *gin.Context) {
bids := str.IdsInt64(ginx.QueryStr(c, "bids", ""), ",")
bids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "bids", ""), ",")
boards, err := models.BoardGetsByBids(rt.Ctx, bids)
ginx.Dangerous(err)
ginx.NewRender(c).Data(boards, err)
@@ -265,7 +270,7 @@ func (rt *Router) publicBoardGets(c *gin.Context) {
}
func (rt *Router) boardGetsByGids(c *gin.Context) {
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
gids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "gids", ""), ",")
query := ginx.QueryStr(c, "query", "")
if len(gids) > 0 {

View File

@@ -36,8 +36,9 @@ func (rt *Router) builtinComponentsAdd(c *gin.Context) {
func (rt *Router) builtinComponentsGets(c *gin.Context) {
query := ginx.QueryStr(c, "query", "")
disabled := ginx.QueryInt(c, "disabled", -1)
bc, err := models.BuiltinComponentGets(rt.Ctx, query)
bc, err := models.BuiltinComponentGets(rt.Ctx, query, disabled)
ginx.Dangerous(err)
ginx.NewRender(c).Data(bc, nil)

View File

@@ -102,9 +102,29 @@ func (rt *Router) builtinMetricsDefaultTypes(c *gin.Context) {
func (rt *Router) builtinMetricsTypes(c *gin.Context) {
collector := ginx.QueryStr(c, "collector", "")
query := ginx.QueryStr(c, "query", "")
disabled := ginx.QueryInt(c, "disabled", -1)
lang := c.GetHeader("X-Language")
ginx.NewRender(c).Data(models.BuiltinMetricTypes(rt.Ctx, lang, collector, query))
metricTypeList, err := models.BuiltinMetricTypes(rt.Ctx, lang, collector, query)
ginx.Dangerous(err)
componentList, err := models.BuiltinComponentGets(rt.Ctx, "", disabled)
ginx.Dangerous(err)
// 创建一个 map 来存储 componentList 中的类型
componentTypes := make(map[string]struct{})
for _, comp := range componentList {
componentTypes[comp.Ident] = struct{}{}
}
filteredMetricTypeList := make([]string, 0)
for _, metricType := range metricTypeList {
if _, exists := componentTypes[metricType]; exists {
filteredMetricTypeList = append(filteredMetricTypeList, metricType)
}
}
ginx.NewRender(c).Data(filteredMetricTypeList, nil)
}
func (rt *Router) builtinMetricsCollectors(c *gin.Context) {

View File

@@ -4,11 +4,11 @@ import (
"net/http"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/str"
)
type busiGroupForm struct {
@@ -131,7 +131,7 @@ func (rt *Router) busiGroupGetsByService(c *gin.Context) {
// 这个接口只有在活跃告警页面才调用获取各个BG的活跃告警数量
func (rt *Router) busiGroupAlertingsGets(c *gin.Context) {
ids := ginx.QueryStr(c, "ids", "")
ret, err := models.AlertNumbers(rt.Ctx, str.IdsInt64(ids))
ret, err := models.AlertNumbers(rt.Ctx, strx.IdsInt64ForAPI(ids))
ginx.NewRender(c).Data(ret, err)
}
@@ -142,7 +142,7 @@ func (rt *Router) busiGroupGet(c *gin.Context) {
}
func (rt *Router) busiGroupsGetTags(c *gin.Context) {
bgids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
bgids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "gids", ""), ",")
targetIdents, err := models.TargetIndentsGetByBgids(rt.Ctx, bgids)
ginx.Dangerous(err)
tags, err := models.TargetGetTags(rt.Ctx, targetIdents, true, "busigroup")

View File

@@ -4,15 +4,15 @@ import (
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/str"
)
func (rt *Router) chartShareGets(c *gin.Context) {
ids := ginx.QueryStr(c, "ids", "")
lst, err := models.ChartShareGetsByIds(rt.Ctx, str.IdsInt64(ids, ","))
lst, err := models.ChartShareGetsByIds(rt.Ctx, strx.IdsInt64ForAPI(ids, ","))
ginx.NewRender(c).Data(lst, err)
}

View File

@@ -0,0 +1,99 @@
package router
import (
"fmt"
"net/http"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func checkAnnotationPermission(c *gin.Context, ctx *ctx.Context, dashboardId int64) {
dashboard, err := models.BoardGetByID(ctx, dashboardId)
if err != nil {
ginx.Bomb(http.StatusInternalServerError, "failed to get dashboard: %v", err)
}
if dashboard == nil {
ginx.Bomb(http.StatusNotFound, "dashboard not found")
}
bg := BusiGroup(ctx, dashboard.GroupId)
me := c.MustGet("user").(*models.User)
can, err := me.CanDoBusiGroup(ctx, bg, "rw")
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "forbidden")
}
}
func (rt *Router) dashAnnotationAdd(c *gin.Context) {
var f models.DashAnnotation
ginx.BindJSON(c, &f)
username := c.MustGet("username").(string)
now := time.Now().Unix()
checkAnnotationPermission(c, rt.Ctx, f.DashboardId)
f.CreateBy = username
f.CreateAt = now
f.UpdateBy = username
f.UpdateAt = now
ginx.NewRender(c).Data(f.Id, f.Add(rt.Ctx))
}
func (rt *Router) dashAnnotationGets(c *gin.Context) {
dashboardId := ginx.QueryInt64(c, "dashboard_id")
from := ginx.QueryInt64(c, "from")
to := ginx.QueryInt64(c, "to")
limit := ginx.QueryInt(c, "limit", 100)
lst, err := models.DashAnnotationGets(rt.Ctx, dashboardId, from, to, limit)
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) dashAnnotationPut(c *gin.Context) {
var f models.DashAnnotation
ginx.BindJSON(c, &f)
id := ginx.UrlParamInt64(c, "id")
annotation, err := getAnnotationById(rt.Ctx, id)
ginx.Dangerous(err)
checkAnnotationPermission(c, rt.Ctx, annotation.DashboardId)
f.Id = id
f.UpdateAt = time.Now().Unix()
f.UpdateBy = c.MustGet("username").(string)
ginx.NewRender(c).Message(f.Update(rt.Ctx))
}
func (rt *Router) dashAnnotationDel(c *gin.Context) {
id := ginx.UrlParamInt64(c, "id")
annotation, err := getAnnotationById(rt.Ctx, id)
ginx.Dangerous(err)
checkAnnotationPermission(c, rt.Ctx, annotation.DashboardId)
ginx.NewRender(c).Message(models.DashAnnotationDel(rt.Ctx, id))
}
// 可以提取获取注释的通用方法
func getAnnotationById(ctx *ctx.Context, id int64) (*models.DashAnnotation, error) {
annotation, err := models.DashAnnotationGet(ctx, "id=?", id)
if err != nil {
return nil, err
}
if annotation == nil {
return nil, fmt.Errorf("annotation not found")
}
return annotation, nil
}

View File

@@ -57,15 +57,21 @@ func (rt *Router) datasourceBriefs(c *gin.Context) {
for _, item := range list {
item.AuthJson.BasicAuthPassword = ""
if item.PluginType != models.PROMETHEUS {
item.SettingsJson = nil
} else {
if item.PluginType == models.PROMETHEUS {
for k, v := range item.SettingsJson {
if strings.HasPrefix(k, "prometheus.") {
item.SettingsJson[strings.TrimPrefix(k, "prometheus.")] = v
delete(item.SettingsJson, k)
}
}
} else if item.PluginType == "cloudwatch" {
for k := range item.SettingsJson {
if !strings.Contains(k, "region") {
delete(item.SettingsJson, k)
}
}
} else {
item.SettingsJson = nil
}
dss = append(dss, item)
}
@@ -93,10 +99,12 @@ func (rt *Router) datasourceUpsert(c *gin.Context) {
var count int64
if !req.ForceSave {
err = DatasourceCheck(req)
if err != nil {
Dangerous(c, err)
return
if req.PluginType == models.PROMETHEUS || req.PluginType == models.LOKI || req.PluginType == models.TDENGINE {
err = DatasourceCheck(req)
if err != nil {
Dangerous(c, err)
return
}
}
}
@@ -115,14 +123,14 @@ func (rt *Router) datasourceUpsert(c *gin.Context) {
}
err = req.Add(rt.Ctx)
} else {
err = req.Update(rt.Ctx, "name", "description", "cluster_name", "settings", "http", "auth", "updated_by", "updated_at", "is_default")
err = req.Update(rt.Ctx, "name", "identifier", "description", "cluster_name", "settings", "http", "auth", "updated_by", "updated_at", "is_default")
}
Render(c, nil, err)
}
func DatasourceCheck(ds models.Datasource) error {
if ds.PluginType != models.ELASTICSEARCH {
if ds.PluginType == models.PROMETHEUS || ds.PluginType == models.LOKI || ds.PluginType == models.TDENGINE {
if ds.HTTPJson.Url == "" {
return fmt.Errorf("url is empty")
}

View File

@@ -0,0 +1,101 @@
package router
import (
"context"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/dskit/types"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
func (rt *Router) ShowDatabases(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
var databases []string
var err error
type DatabaseShower interface {
ShowDatabases(context.Context) ([]string, error)
}
switch plug.(type) {
case DatabaseShower:
databases, err = plug.(DatabaseShower).ShowDatabases(c.Request.Context())
ginx.Dangerous(err)
default:
ginx.Bomb(200, "datasource not exists")
}
if len(databases) == 0 {
databases = make([]string, 0)
}
ginx.NewRender(c).Data(databases, nil)
}
func (rt *Router) ShowTables(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
// 只接受一个入参
tables := make([]string, 0)
var err error
type TableShower interface {
ShowTables(ctx context.Context, database string) ([]string, error)
}
switch plug.(type) {
case TableShower:
if len(f.Querys) > 0 {
database, ok := f.Querys[0].(string)
if ok {
tables, err = plug.(TableShower).ShowTables(c.Request.Context(), database)
}
}
default:
ginx.Bomb(200, "datasource not exists")
}
ginx.NewRender(c).Data(tables, err)
}
func (rt *Router) DescribeTable(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
// 只接受一个入参
columns := make([]*types.ColumnProperty, 0)
var err error
type TableDescriber interface {
DescribeTable(context.Context, interface{}) ([]*types.ColumnProperty, error)
}
switch plug.(type) {
case TableDescriber:
client := plug.(TableDescriber)
if len(f.Querys) > 0 {
columns, err = client.DescribeTable(c.Request.Context(), f.Querys[0])
}
default:
ginx.Bomb(200, "datasource not exists")
}
ginx.NewRender(c).Data(columns, err)
}

View File

@@ -0,0 +1,77 @@
package router
import (
"github.com/ccfos/nightingale/v6/datasource/es"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
type IndexReq struct {
Cate string `json:"cate"`
DatasourceId int64 `json:"datasource_id"`
Index string `json:"index"`
}
type FieldValueReq struct {
Cate string `json:"cate"`
DatasourceId int64 `json:"datasource_id"`
Index string `json:"index"`
Query FieldObj `json:"query"`
}
type FieldObj struct {
Find string `json:"find"`
Field string `json:"field"`
Query string `json:"query"`
}
func (rt *Router) QueryIndices(c *gin.Context) {
var f IndexReq
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
indices, err := plug.(*es.Elasticsearch).QueryIndices()
ginx.Dangerous(err)
ginx.NewRender(c).Data(indices, nil)
}
func (rt *Router) QueryFields(c *gin.Context) {
var f IndexReq
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
fields, err := plug.(*es.Elasticsearch).QueryFields([]string{f.Index})
ginx.Dangerous(err)
ginx.NewRender(c).Data(fields, nil)
}
func (rt *Router) QueryESVariable(c *gin.Context) {
var f FieldValueReq
ginx.BindJSON(c, &f)
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
fields, err := plug.(*es.Elasticsearch).QueryFieldValue([]string{f.Index}, f.Query.Field, f.Query.Query)
ginx.Dangerous(err)
ginx.NewRender(c).Data(fields, nil)
}

View File

@@ -0,0 +1,217 @@
package router
import (
"net/http"
"time"
"github.com/ccfos/nightingale/v6/alert/pipeline"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
// 获取事件Pipeline列表
func (rt *Router) eventPipelinesList(c *gin.Context) {
me := c.MustGet("user").(*models.User)
pipelines, err := models.ListEventPipelines(rt.Ctx)
ginx.Dangerous(err)
allTids := make([]int64, 0)
for _, pipeline := range pipelines {
allTids = append(allTids, pipeline.TeamIds...)
}
ugMap, err := models.UserGroupIdAndNameMap(rt.Ctx, allTids)
ginx.Dangerous(err)
for _, pipeline := range pipelines {
for _, tid := range pipeline.TeamIds {
pipeline.TeamNames = append(pipeline.TeamNames, ugMap[tid])
}
}
gids, err := models.MyGroupIdsMap(rt.Ctx, me.Id)
ginx.Dangerous(err)
if me.IsAdmin() {
ginx.NewRender(c).Data(pipelines, nil)
return
}
res := make([]*models.EventPipeline, 0)
for _, pipeline := range pipelines {
for _, tid := range pipeline.TeamIds {
if _, ok := gids[tid]; ok {
res = append(res, pipeline)
break
}
}
}
ginx.NewRender(c).Data(res, nil)
}
// 获取单个事件Pipeline详情
func (rt *Router) getEventPipeline(c *gin.Context) {
me := c.MustGet("user").(*models.User)
id := ginx.UrlParamInt64(c, "id")
pipeline, err := models.GetEventPipeline(rt.Ctx, id)
ginx.Dangerous(err)
ginx.Dangerous(me.CheckGroupPermission(rt.Ctx, pipeline.TeamIds))
err = pipeline.FillTeamNames(rt.Ctx)
ginx.Dangerous(err)
ginx.NewRender(c).Data(pipeline, nil)
}
// 创建事件Pipeline
func (rt *Router) addEventPipeline(c *gin.Context) {
var pipeline models.EventPipeline
ginx.BindJSON(c, &pipeline)
user := c.MustGet("user").(*models.User)
now := time.Now().Unix()
pipeline.CreateBy = user.Username
pipeline.CreateAt = now
pipeline.UpdateAt = now
pipeline.UpdateBy = user.Username
err := pipeline.Verify()
if err != nil {
ginx.Bomb(http.StatusBadRequest, err.Error())
}
ginx.Dangerous(user.CheckGroupPermission(rt.Ctx, pipeline.TeamIds))
err = models.CreateEventPipeline(rt.Ctx, &pipeline)
ginx.NewRender(c).Message(err)
}
// 更新事件Pipeline
func (rt *Router) updateEventPipeline(c *gin.Context) {
var f models.EventPipeline
ginx.BindJSON(c, &f)
me := c.MustGet("user").(*models.User)
f.UpdateBy = me.Username
f.UpdateAt = time.Now().Unix()
pipeline, err := models.GetEventPipeline(rt.Ctx, f.ID)
if err != nil {
ginx.Bomb(http.StatusNotFound, "No such event pipeline")
}
ginx.Dangerous(me.CheckGroupPermission(rt.Ctx, pipeline.TeamIds))
ginx.NewRender(c).Message(pipeline.Update(rt.Ctx, &f))
}
// 删除事件Pipeline
func (rt *Router) deleteEventPipelines(c *gin.Context) {
var f struct {
Ids []int64 `json:"ids"`
}
ginx.BindJSON(c, &f)
if len(f.Ids) == 0 {
ginx.Bomb(http.StatusBadRequest, "ids required")
}
me := c.MustGet("user").(*models.User)
for _, id := range f.Ids {
pipeline, err := models.GetEventPipeline(rt.Ctx, id)
ginx.Dangerous(err)
ginx.Dangerous(me.CheckGroupPermission(rt.Ctx, pipeline.TeamIds))
}
err := models.DeleteEventPipelines(rt.Ctx, f.Ids)
ginx.NewRender(c).Message(err)
}
// 测试事件Pipeline
func (rt *Router) tryRunEventPipeline(c *gin.Context) {
var f struct {
EventId int64 `json:"event_id"`
PipelineConfig models.EventPipeline `json:"pipeline_config"`
}
ginx.BindJSON(c, &f)
hisEvent, err := models.AlertHisEventGetById(rt.Ctx, f.EventId)
if err != nil || hisEvent == nil {
ginx.Bomb(http.StatusBadRequest, "event not found")
}
event := hisEvent.ToCur()
for _, p := range f.PipelineConfig.Processors {
processor, err := pipeline.GetProcessorByType(p.Typ, p.Config)
if err != nil {
ginx.Bomb(http.StatusBadRequest, "processor %+v type not found, error: %s", p, err.Error())
}
processor.Process(rt.Ctx, event)
}
ginx.NewRender(c).Data(event, nil)
}
// 测试事件处理器
func (rt *Router) tryRunEventProcessor(c *gin.Context) {
var f struct {
EventId int64 `json:"event_id"`
ProcessorConfig models.Processor `json:"processor_config"`
}
ginx.BindJSON(c, &f)
hisEvent, err := models.AlertHisEventGetById(rt.Ctx, f.EventId)
if err != nil || hisEvent == nil {
ginx.Bomb(http.StatusBadRequest, "event not found")
}
event := hisEvent.ToCur()
processor, err := pipeline.GetProcessorByType(f.ProcessorConfig.Typ, f.ProcessorConfig.Config)
if err != nil {
ginx.Bomb(http.StatusBadRequest, "processor type: %s, error: %s", f.ProcessorConfig.Typ, err.Error())
}
processor.Process(rt.Ctx, event)
ginx.NewRender(c).Data(event, nil)
}
func (rt *Router) tryRunEventProcessorByNotifyRule(c *gin.Context) {
var f struct {
EventId int64 `json:"event_id"`
PipelineConfigs []models.PipelineConfig `json:"pipeline_configs"`
}
ginx.BindJSON(c, &f)
hisEvent, err := models.AlertHisEventGetById(rt.Ctx, f.EventId)
if err != nil || hisEvent == nil {
ginx.Bomb(http.StatusBadRequest, "event not found")
}
event := hisEvent.ToCur()
pids := make([]int64, 0)
for _, pc := range f.PipelineConfigs {
if pc.Enable {
pids = append(pids, pc.PipelineId)
}
}
pipelines, err := models.GetEventPipelinesByIds(rt.Ctx, pids)
if err != nil {
ginx.Bomb(http.StatusBadRequest, "processors not found")
}
for _, pl := range pipelines {
for _, p := range pl.Processors {
processor, err := pipeline.GetProcessorByType(p.Typ, p.Config)
if err != nil {
ginx.Bomb(http.StatusBadRequest, "processor %+v type not found", p)
}
processor.Process(rt.Ctx, event)
}
}
ginx.NewRender(c).Data(event, nil)
}
func (rt *Router) eventPipelinesListByService(c *gin.Context) {
pipelines, err := models.ListEventPipelines(rt.Ctx)
ginx.NewRender(c).Data(pipelines, err)
}

View File

@@ -36,6 +36,14 @@ func (rt *Router) statistic(c *gin.Context) {
model = models.User{}
case "user_group":
model = models.UserGroup{}
case "notify_rule":
model = models.NotifyRule{}
case "notify_channel":
model = models.NotifyChannel{}
case "event_pipeline":
statistics, err = models.EventPipelineStatistics(rt.Ctx)
ginx.NewRender(c).Data(statistics, err)
return
case "datasource":
// datasource update_at is different from others
statistics, err = models.DatasourceStatistics(rt.Ctx)
@@ -49,6 +57,10 @@ func (rt *Router) statistic(c *gin.Context) {
statistics, err = models.ConfigCvalStatistics(rt.Ctx)
ginx.NewRender(c).Data(statistics, err)
return
case "message_template":
statistics, err = models.MessageTemplateStatistics(rt.Ctx)
ginx.NewRender(c).Data(statistics, err)
return
default:
ginx.Bomb(http.StatusBadRequest, "invalid name")
}

View File

@@ -152,6 +152,13 @@ func (rt *Router) refreshPost(c *gin.Context) {
return
}
// 看这个 token 是否还存在 redis 中
val, err := rt.fetchAuth(c.Request.Context(), refreshUuid)
if err != nil || val == "" {
ginx.NewRender(c, http.StatusUnauthorized).Message("refresh token expired")
return
}
userIdentity, ok := claims["user_identity"].(string)
if !ok {
// Theoretically impossible

View File

@@ -0,0 +1,214 @@
package router
import (
"bytes"
"fmt"
"html/template"
"net/http"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/slice"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/ccfos/nightingale/v6/pkg/tplx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) messageTemplatesAdd(c *gin.Context) {
var lst []*models.MessageTemplate
ginx.BindJSON(c, &lst)
if len(lst) == 0 {
ginx.Bomb(http.StatusBadRequest, "input json is empty")
}
me := c.MustGet("user").(*models.User)
isAdmin := me.IsAdmin()
idents := make([]string, 0, len(lst))
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
now := time.Now().Unix()
for _, tpl := range lst {
ginx.Dangerous(tpl.Verify())
if !isAdmin && !slice.HaveIntersection(gids, tpl.UserGroupIds) {
ginx.Bomb(http.StatusForbidden, "no permission")
}
idents = append(idents, tpl.Ident)
tpl.CreateBy = me.Username
tpl.CreateAt = now
tpl.UpdateBy = me.Username
tpl.UpdateAt = now
}
lstWithSameId, err := models.MessageTemplatesGet(rt.Ctx, "ident IN ?", idents)
ginx.Dangerous(err)
if len(lstWithSameId) > 0 {
ginx.Bomb(http.StatusBadRequest, "ident already exists")
}
ids := make([]int64, 0, len(lst))
for _, tpl := range lst {
err := models.Insert(rt.Ctx, tpl)
ginx.Dangerous(err)
ids = append(ids, tpl.ID)
}
ginx.NewRender(c).Data(ids, nil)
}
func (rt *Router) messageTemplatesDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
lst, err := models.MessageTemplatesGet(rt.Ctx, "id in (?)", f.Ids)
ginx.Dangerous(err)
notifyRuleIds, err := models.UsedByNotifyRule(rt.Ctx, models.MsgTplList(lst))
ginx.Dangerous(err)
if len(notifyRuleIds) > 0 {
ginx.NewRender(c).Message(fmt.Errorf("used by notify rule: %v", notifyRuleIds))
return
}
if me := c.MustGet("user").(*models.User); !me.IsAdmin() {
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
for _, t := range lst {
if !slice.HaveIntersection[int64](gids, t.UserGroupIds) {
ginx.Bomb(http.StatusForbidden, "no permission")
}
}
}
ginx.NewRender(c).Message(models.DB(rt.Ctx).Delete(
&models.MessageTemplate{}, "id in (?)", f.Ids).Error)
}
func (rt *Router) messageTemplatePut(c *gin.Context) {
var f models.MessageTemplate
ginx.BindJSON(c, &f)
mt, err := models.MessageTemplateGet(rt.Ctx, "id <> ? and ident = ?", ginx.UrlParamInt64(c, "id"), f.Ident)
ginx.Dangerous(err)
if mt != nil {
ginx.Bomb(http.StatusBadRequest, "message template ident already exists")
}
mt, err = models.MessageTemplateGet(rt.Ctx, "id = ?", ginx.UrlParamInt64(c, "id"))
ginx.Dangerous(err)
if mt == nil {
ginx.Bomb(http.StatusNotFound, "message template not found")
}
me := c.MustGet("user").(*models.User)
if !me.IsAdmin() {
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
if !slice.HaveIntersection[int64](gids, mt.UserGroupIds) {
ginx.Bomb(http.StatusForbidden, "no permission")
}
}
f.UpdateBy = me.Username
ginx.NewRender(c).Message(mt.Update(rt.Ctx, f))
}
func (rt *Router) messageTemplateGet(c *gin.Context) {
me := c.MustGet("user").(*models.User)
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
tid := ginx.UrlParamInt64(c, "id")
mt, err := models.MessageTemplateGet(rt.Ctx, "id = ?", tid)
ginx.Dangerous(err)
if mt == nil {
ginx.Bomb(http.StatusNotFound, "message template not found")
}
if mt.Private == 1 && !slice.HaveIntersection[int64](gids, mt.UserGroupIds) {
ginx.Bomb(http.StatusForbidden, "no permission")
}
ginx.NewRender(c).Data(mt, nil)
}
func (rt *Router) messageTemplatesGet(c *gin.Context) {
var notifyChannelIdents []string
if tmp := ginx.QueryStr(c, "notify_channel_idents", ""); tmp != "" {
notifyChannelIdents = strings.Split(tmp, ",")
}
notifyChannelIds := strx.IdsInt64ForAPI(ginx.QueryStr(c, "notify_channel_ids", ""))
if len(notifyChannelIds) > 0 {
ginx.Dangerous(models.DB(rt.Ctx).Model(models.NotifyChannelConfig{}).
Where("id in (?)", notifyChannelIds).Pluck("ident", &notifyChannelIdents).Error)
}
me := c.MustGet("user").(*models.User)
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
lst, err := models.MessageTemplatesGetBy(rt.Ctx, notifyChannelIdents)
ginx.Dangerous(err)
if me.IsAdmin() {
ginx.NewRender(c).Data(lst, nil)
return
}
res := make([]*models.MessageTemplate, 0)
for _, t := range lst {
if slice.HaveIntersection[int64](gids, t.UserGroupIds) || t.Private == 0 {
res = append(res, t)
}
}
ginx.NewRender(c).Data(res, nil)
}
type evtMsgReq struct {
EventIds []int64 `json:"event_ids"`
Tpl struct {
Content map[string]string `json:"content"`
} `json:"tpl"`
}
func (rt *Router) eventsMessage(c *gin.Context) {
var req evtMsgReq
ginx.BindJSON(c, &req)
hisEvents, err := models.AlertHisEventGetByIds(rt.Ctx, req.EventIds)
ginx.Dangerous(err)
if len(hisEvents) == 0 {
ginx.Bomb(http.StatusBadRequest, "event not found")
}
ginx.Dangerous(err)
events := make([]*models.AlertCurEvent, len(hisEvents))
for i, he := range hisEvents {
events[i] = he.ToCur()
}
var defs = []string{
"{{$events := .}}",
"{{$event := index . 0}}",
}
ret := make(map[string]string, len(req.Tpl.Content))
for k, v := range req.Tpl.Content {
text := strings.Join(append(defs, v), "")
tpl, err := template.New(k).Funcs(tplx.TemplateFuncMap).Parse(text)
if err != nil {
ret[k] = err.Error()
continue
}
var buf bytes.Buffer
err = tpl.Execute(&buf, events)
if err != nil {
ret[k] = err.Error()
continue
}
ret[k] = buf.String()
}
ginx.NewRender(c).Data(ret, nil)
}

View File

@@ -7,10 +7,10 @@ import (
"github.com/ccfos/nightingale/v6/alert/common"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/str"
)
// Return all, front-end search and paging
@@ -22,7 +22,7 @@ func (rt *Router) alertMuteGetsByBG(c *gin.Context) {
}
func (rt *Router) alertMuteGetsByGids(c *gin.Context) {
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
gids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "gids", ""), ",")
if len(gids) > 0 {
for _, gid := range gids {
rt.bgroCheck(c, gid)

View File

@@ -17,6 +17,10 @@ import (
"github.com/toolkits/pkg/ginx"
)
const (
DefaultTokenKey = "X-User-Token"
)
type AccessDetails struct {
AccessUuid string
UserIdentity string
@@ -62,8 +66,29 @@ func (rt *Router) proxyAuth() gin.HandlerFunc {
}
}
func (rt *Router) jwtAuth() gin.HandlerFunc {
// tokenAuth 支持两种方式的认证,固定 token 和 jwt token
// 因为不太好区分用户使用哪个方式,所以两种方式放在一个中间件里
func (rt *Router) tokenAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 先验证固定 token
if rt.HTTP.TokenAuth.Enable {
tokenKey := rt.HTTP.TokenAuth.HeaderUserTokenKey
if tokenKey == "" {
tokenKey = DefaultTokenKey
}
token := c.GetHeader(tokenKey)
if token != "" {
user := rt.UserTokenCache.GetByToken(token)
if user != nil && user.Username != "" {
c.Set("userid", user.Id)
c.Set("username", user.Username)
c.Next()
return
}
}
}
// 再验证 jwt token
metadata, err := rt.extractTokenMetadata(c.Request)
if err != nil {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
@@ -100,7 +125,7 @@ func (rt *Router) auth() gin.HandlerFunc {
if rt.HTTP.ProxyAuth.Enable {
return rt.proxyAuth()
} else {
return rt.jwtAuth()
return rt.tokenAuth()
}
}
@@ -130,9 +155,9 @@ func (rt *Router) User() gin.HandlerFunc {
func (rt *Router) user() gin.HandlerFunc {
return func(c *gin.Context) {
userid := c.MustGet("userid").(int64)
username := c.MustGet("username").(string)
user, err := models.UserGetById(rt.Ctx, userid)
user, err := models.UserGetByUsername(rt.Ctx, username)
if err != nil {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
@@ -310,6 +335,12 @@ func (rt *Router) extractTokenMetadata(r *http.Request) (*AccessDetails, error)
return nil, errors.New("failed to parse access_uuid from jwt")
}
// accessUuid 在 redis 里存在才放行
val, err := rt.fetchAuth(r.Context(), accessUuid)
if err != nil || val == "" {
return nil, errors.New("unauthorized")
}
return &AccessDetails{
AccessUuid: accessUuid,
UserIdentity: claims["user_identity"].(string),
@@ -330,18 +361,43 @@ func (rt *Router) extractToken(r *http.Request) string {
}
func (rt *Router) createAuth(ctx context.Context, userIdentity string, td *TokenDetails) error {
username := strings.Split(userIdentity, "-")[1]
// 如果只能有一个账号登录,那么就删除之前的 token
if rt.HTTP.JWTAuth.SingleLogin {
delKeys, err := rt.Redis.SMembers(ctx, rt.wrapJwtKey(username)).Result()
if err != nil {
return err
}
if len(delKeys) > 0 {
errDel := rt.Redis.Del(ctx, delKeys...).Err()
if errDel != nil {
return errDel
}
}
if errDel := rt.Redis.Del(ctx, rt.wrapJwtKey(username)).Err(); errDel != nil {
return errDel
}
}
at := time.Unix(td.AtExpires, 0)
rte := time.Unix(td.RtExpires, 0)
now := time.Now()
errAccess := rt.Redis.Set(ctx, rt.wrapJwtKey(td.AccessUuid), userIdentity, at.Sub(now)).Err()
if errAccess != nil {
return errAccess
if err := rt.Redis.Set(ctx, rt.wrapJwtKey(td.AccessUuid), userIdentity, at.Sub(now)).Err(); err != nil {
return err
}
errRefresh := rt.Redis.Set(ctx, rt.wrapJwtKey(td.RefreshUuid), userIdentity, rte.Sub(now)).Err()
if errRefresh != nil {
return errRefresh
if err := rt.Redis.Set(ctx, rt.wrapJwtKey(td.RefreshUuid), userIdentity, rte.Sub(now)).Err(); err != nil {
return err
}
if rt.HTTP.JWTAuth.SingleLogin {
if err := rt.Redis.SAdd(ctx, rt.wrapJwtKey(username), rt.wrapJwtKey(td.AccessUuid), rt.wrapJwtKey(td.RefreshUuid)).Err(); err != nil {
return err
}
}
return nil

View File

@@ -3,6 +3,7 @@ package router
import (
"strings"
"github.com/ccfos/nightingale/v6/alert/sender"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
@@ -17,36 +18,27 @@ type NotificationResponse struct {
}
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"`
SubID int64 `json:"sub_id"`
NotifyRuleId int64 `json:"notify_rule_id"`
Notifies map[string][]Record `json:"notifies"`
}
type Record struct {
Target string `json:"target"`
Username string `json:"username"`
Status int `json:"status"`
Detail string `json:"detail"`
NotifyRuleId int64 `json:"notify_rule_id"`
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
}
}
err := sender.PushNotifyRecords(req)
ginx.Dangerous(err, 429)
ginx.NewRender(c).Data(ids, err)
ginx.NewRender(c).Data(nil, err)
}
func (rt *Router) notificationRecordList(c *gin.Context) {
@@ -118,9 +110,10 @@ func buildNotificationResponse(ctx *ctx.Context, nl []*models.NotificaitonRecord
n.Target = replaceLastEightChars(n.Target)
}
record := Record{
Target: n.Target,
Status: n.Status,
Detail: n.Details,
Target: n.Target,
Status: n.Status,
Detail: n.Details,
NotifyRuleId: n.NotifyRuleID,
}
record.Username = strings.Join(usernames, ",")
@@ -130,7 +123,8 @@ func buildNotificationResponse(ctx *ctx.Context, nl []*models.NotificaitonRecord
subRule, ok := subRuleMap[n.SubId]
if !ok {
newSubRule := &SubRule{
SubID: n.SubId,
NotifyRuleId: n.NotifyRuleID,
SubID: n.SubId,
}
newSubRule.Notifies = make(map[string][]Record)
newSubRule.Notifies[n.Channel] = []Record{record}

View File

@@ -0,0 +1,268 @@
package router
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) notifyChannelsAdd(c *gin.Context) {
me := c.MustGet("user").(*models.User)
var lst []*models.NotifyChannelConfig
ginx.BindJSON(c, &lst)
if len(lst) == 0 {
ginx.Bomb(http.StatusBadRequest, "input json is empty")
}
names := make([]string, 0, len(lst))
for i := range lst {
ginx.Dangerous(lst[i].Verify())
names = append(names, lst[i].Name)
lst[i].CreateBy = me.Username
lst[i].CreateAt = time.Now().Unix()
lst[i].UpdateBy = me.Username
lst[i].UpdateAt = time.Now().Unix()
}
lstWithSameName, err := models.NotifyChannelsGet(rt.Ctx, "name IN ?", names)
ginx.Dangerous(err)
if len(lstWithSameName) > 0 {
ginx.Bomb(http.StatusBadRequest, "name already exists")
}
ids := make([]int64, 0, len(lst))
for _, item := range lst {
err := models.Insert(rt.Ctx, item)
ginx.Dangerous(err)
ids = append(ids, item.ID)
}
ginx.NewRender(c).Data(ids, nil)
}
func (rt *Router) notifyChannelsDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
lst, err := models.NotifyChannelsGet(rt.Ctx, "id in (?)", f.Ids)
ginx.Dangerous(err)
notifyRuleIds, err := models.UsedByNotifyRule(rt.Ctx, models.NotiChList(lst))
ginx.Dangerous(err)
if len(notifyRuleIds) > 0 {
ginx.NewRender(c).Message(fmt.Errorf("used by notify rule: %v", notifyRuleIds))
return
}
ginx.NewRender(c).Message(models.DB(rt.Ctx).
Delete(&models.NotifyChannelConfig{}, "id in (?)", f.Ids).Error)
}
func (rt *Router) notifyChannelPut(c *gin.Context) {
me := c.MustGet("user").(*models.User)
var f models.NotifyChannelConfig
ginx.BindJSON(c, &f)
lstWithSameName, err := models.NotifyChannelsGet(rt.Ctx, "name = ? and id <> ?", f.Name, f.ID)
ginx.Dangerous(err)
if len(lstWithSameName) > 0 {
ginx.Bomb(http.StatusBadRequest, "name already exists")
}
nc, err := models.NotifyChannelGet(rt.Ctx, "id = ?", ginx.UrlParamInt64(c, "id"))
ginx.Dangerous(err)
if nc == nil {
ginx.Bomb(http.StatusNotFound, "notify channel not found")
}
f.UpdateBy = me.Username
ginx.NewRender(c).Message(nc.Update(rt.Ctx, f))
}
func (rt *Router) notifyChannelGet(c *gin.Context) {
cid := ginx.UrlParamInt64(c, "id")
nc, err := models.NotifyChannelGet(rt.Ctx, "id = ?", cid)
ginx.Dangerous(err)
if nc == nil {
ginx.Bomb(http.StatusNotFound, "notify channel not found")
}
ginx.NewRender(c).Data(nc, nil)
}
func (rt *Router) notifyChannelGetBy(c *gin.Context) {
ident := ginx.QueryStr(c, "ident")
nc, err := models.NotifyChannelGet(rt.Ctx, "ident = ?", ident)
ginx.Dangerous(err)
if nc == nil {
ginx.Bomb(http.StatusNotFound, "notify channel not found")
}
nc.ParamConfig = &models.NotifyParamConfig{}
nc.RequestConfig = &models.RequestConfig{}
ginx.NewRender(c).Data(nc, nil)
}
func (rt *Router) notifyChannelsGet(c *gin.Context) {
lst, err := models.NotifyChannelsGet(rt.Ctx, "", nil)
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) notifyChannelsGetForNormalUser(c *gin.Context) {
lst, err := models.NotifyChannelsGet(rt.Ctx, "")
ginx.Dangerous(err)
newLst := make([]*models.NotifyChannelConfig, 0, len(lst))
for _, c := range lst {
newLst = append(newLst, &models.NotifyChannelConfig{
ID: c.ID,
Name: c.Name,
Ident: c.Ident,
Enable: c.Enable,
RequestType: c.RequestType,
ParamConfig: c.ParamConfig,
})
}
ginx.NewRender(c).Data(newLst, nil)
}
func (rt *Router) notifyChannelIdentsGet(c *gin.Context) {
// 获取所有通知渠道
channels, err := models.NotifyChannelsGet(rt.Ctx, "", nil)
ginx.Dangerous(err)
// ident 去重
idents := make(map[string]struct{})
for _, channel := range channels {
if channel.Ident != "" {
idents[channel.Ident] = struct{}{}
}
}
lst := make([]string, 0, len(idents))
for ident := range idents {
lst = append(lst, ident)
}
sort.Strings(lst)
ginx.NewRender(c).Data(lst, nil)
}
type flushDutyChannelsResponse struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
Data struct {
Items []struct {
ChannelID int `json:"channel_id"`
ChannelName string `json:"channel_name"`
Status string `json:"status"`
} `json:"items"`
Total int `json:"total"`
} `json:"data"`
}
func (rt *Router) flashDutyNotifyChannelsGet(c *gin.Context) {
cid := ginx.UrlParamInt64(c, "id")
nc, err := models.NotifyChannelGet(rt.Ctx, "id = ?", cid)
ginx.Dangerous(err)
if nc == nil {
ginx.Bomb(http.StatusNotFound, "notify channel not found")
}
configs, err := models.ConfigsSelectByCkey(rt.Ctx, "flashduty_app_key")
if err != nil {
ginx.Bomb(http.StatusInternalServerError, "failed to get flashduty app key")
}
jsonData := []byte("{}")
if len(configs) > 0 {
me := c.MustGet("user").(*models.User)
jsonData = []byte(fmt.Sprintf(`{"member_name":"%s","email":"%s","phone":"%s"}`, me.Username, me.Email, me.Phone))
}
items, err := getFlashDutyChannels(nc.RequestConfig.FlashDutyRequestConfig.IntegrationUrl, jsonData)
ginx.Dangerous(err)
ginx.NewRender(c).Data(items, nil)
}
// getFlashDutyChannels 从FlashDuty API获取频道列表
func getFlashDutyChannels(integrationUrl string, jsonData []byte) ([]struct {
ChannelID int `json:"channel_id"`
ChannelName string `json:"channel_name"`
Status string `json:"status"`
}, error) {
// 解析URL提取baseUrl和参数
baseUrl, integrationKey, err := parseIntegrationUrl(integrationUrl)
if err != nil {
return nil, err
}
if integrationKey == "" {
return nil, fmt.Errorf("integration_key not found in URL")
}
// 构建新的API URL保持原始路径
url := fmt.Sprintf("%s/channel/list-by-integration?integration_key=%s", baseUrl, integrationKey)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
httpResp, err := (&http.Client{}).Do(req)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
var res flushDutyChannelsResponse
if err := json.Unmarshal(body, &res); err != nil {
return nil, err
}
if res.Error.Message != "" {
return nil, fmt.Errorf(res.Error.Message)
}
return res.Data.Items, nil
}
// parseIntegrationUrl 从URL中提取baseUrl和参数
func parseIntegrationUrl(urlStr string) (baseUrl string, integrationKey string, err error) {
// 解析URL
parsedUrl, err := url.Parse(urlStr)
if err != nil {
return "", "", err
}
host := fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host)
// 提取查询参数
queryParams := parsedUrl.Query()
integrationKey = queryParams.Get("integration_key")
return host, integrationKey, nil
}

View File

@@ -0,0 +1,17 @@
package router
import (
"fmt"
"testing"
)
func TestGetFlashDutyChannels(t *testing.T) {
// 构造测试数据
integrationUrl := "https://api.flashcat.cloud/event/push/alert/n9e?integration_key=xxx"
jsonData := []byte(`{}`)
// 调用被测试的函数
channels, err := getFlashDutyChannels(integrationUrl, jsonData)
fmt.Println(channels, err)
}

View File

@@ -111,7 +111,7 @@ func (rt *Router) notifyChannelPuts(c *gin.Context) {
}
func (rt *Router) notifyContactGets(c *gin.Context) {
var notifyContacts []models.NotifyContact
notifyContacts := []models.NotifyContact{}
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYCONTACT)
ginx.Dangerous(err)
if cval == "" {
@@ -120,6 +120,7 @@ func (rt *Router) notifyContactGets(c *gin.Context) {
}
err = json.Unmarshal([]byte(cval), &notifyContacts)
ginx.NewRender(c).Data(notifyContacts, err)
}
@@ -127,20 +128,6 @@ func (rt *Router) notifyContactPuts(c *gin.Context) {
var notifyContacts []models.NotifyContact
ginx.BindJSON(c, &notifyContacts)
keys := []string{models.DingtalkKey, models.WecomKey, models.FeishuKey, models.MmKey,
models.TelegramKey, models.LarkKey}
m := make(map[string]struct{})
for _, v := range notifyContacts {
m[v.Ident] = struct{}{}
}
for _, v := range keys {
if _, ok := m[v]; !ok {
ginx.Bomb(200, "contact %s ident can not modify", v)
}
}
data, err := json.Marshal(notifyContacts)
ginx.Dangerous(err)
username := c.MustGet("username").(string)
@@ -226,3 +213,14 @@ func (rt *Router) attemptSendEmail(c *gin.Context) {
ginx.NewRender(c).Message(sender.SendEmail("Email test", "email content", []string{f.Email}, smtp))
}
func (rt *Router) notifyChannelConfigGets(c *gin.Context) {
id := ginx.QueryInt64(c, "id", 0)
name := ginx.QueryStr(c, "name", "")
ident := ginx.QueryStr(c, "ident", "")
eabled := ginx.QueryInt(c, "eabled", -1)
notifyChannels, err := models.NotifyChannelGets(rt.Ctx, id, name, ident, eabled)
ginx.NewRender(c).Data(notifyChannels, err)
}

View File

@@ -0,0 +1,321 @@
package router
import (
"fmt"
"net/http"
"time"
"github.com/ccfos/nightingale/v6/alert/dispatch"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/slice"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
func (rt *Router) notifyRulesAdd(c *gin.Context) {
var lst []*models.NotifyRule
ginx.BindJSON(c, &lst)
if len(lst) == 0 {
ginx.Bomb(http.StatusBadRequest, "input json is empty")
}
me := c.MustGet("user").(*models.User)
isAdmin := me.IsAdmin()
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
now := time.Now().Unix()
for _, nr := range lst {
ginx.Dangerous(nr.Verify())
if !isAdmin && !slice.HaveIntersection(gids, nr.UserGroupIds) {
ginx.Bomb(http.StatusForbidden, "no permission")
}
nr.CreateBy = me.Username
nr.CreateAt = now
nr.UpdateBy = me.Username
nr.UpdateAt = now
err := models.Insert(rt.Ctx, nr)
ginx.Dangerous(err)
}
ginx.NewRender(c).Data(lst, nil)
}
func (rt *Router) notifyRulesDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
if me := c.MustGet("user").(*models.User); !me.IsAdmin() {
lst, err := models.NotifyRulesGet(rt.Ctx, "id in (?)", f.Ids)
ginx.Dangerous(err)
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
for _, t := range lst {
if !slice.HaveIntersection[int64](gids, t.UserGroupIds) {
ginx.Bomb(http.StatusForbidden, "no permission")
}
}
}
ginx.NewRender(c).Message(models.DB(rt.Ctx).
Delete(&models.NotifyRule{}, "id in (?)", f.Ids).Error)
}
func (rt *Router) notifyRulePut(c *gin.Context) {
var f models.NotifyRule
ginx.BindJSON(c, &f)
nr, err := models.NotifyRuleGet(rt.Ctx, "id = ?", ginx.UrlParamInt64(c, "id"))
ginx.Dangerous(err)
if nr == nil {
ginx.Bomb(http.StatusNotFound, "notify rule not found")
}
me := c.MustGet("user").(*models.User)
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
if !slice.HaveIntersection[int64](gids, nr.UserGroupIds) && !me.IsAdmin() {
ginx.Bomb(http.StatusForbidden, "no permission")
}
f.UpdateBy = me.Username
ginx.NewRender(c).Message(nr.Update(rt.Ctx, f))
}
func (rt *Router) notifyRuleGet(c *gin.Context) {
me := c.MustGet("user").(*models.User)
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
tid := ginx.UrlParamInt64(c, "id")
nr, err := models.NotifyRuleGet(rt.Ctx, "id = ?", tid)
ginx.Dangerous(err)
if nr == nil {
ginx.Bomb(http.StatusNotFound, "notify rule not found")
}
if !slice.HaveIntersection[int64](gids, nr.UserGroupIds) && !me.IsAdmin() {
ginx.Bomb(http.StatusForbidden, "no permission")
}
ginx.NewRender(c).Data(nr, nil)
}
func (rt *Router) notifyRulesGetByService(c *gin.Context) {
ginx.NewRender(c).Data(models.NotifyRulesGet(rt.Ctx, "enable = ?", true))
}
func (rt *Router) notifyRulesGet(c *gin.Context) {
me := c.MustGet("user").(*models.User)
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
lst, err := models.NotifyRulesGet(rt.Ctx, "", nil)
ginx.Dangerous(err)
if me.IsAdmin() {
ginx.NewRender(c).Data(lst, nil)
return
}
res := make([]*models.NotifyRule, 0)
for _, nr := range lst {
if slice.HaveIntersection[int64](gids, nr.UserGroupIds) {
res = append(res, nr)
}
}
ginx.NewRender(c).Data(res, nil)
}
type NotifyTestForm struct {
EventIDs []int64 `json:"event_ids" binding:"required"`
NotifyConfig models.NotifyConfig `json:"notify_config" binding:"required"`
}
func (rt *Router) notifyTest(c *gin.Context) {
var f NotifyTestForm
ginx.BindJSON(c, &f)
hisEvents, err := models.AlertHisEventGetByIds(rt.Ctx, f.EventIDs)
ginx.Dangerous(err)
if len(hisEvents) == 0 {
ginx.Bomb(http.StatusBadRequest, "event not found")
}
ginx.Dangerous(err)
events := []*models.AlertCurEvent{}
for _, he := range hisEvents {
event := he.ToCur()
event.SetTagsMap()
if dispatch.NotifyRuleApplicable(&f.NotifyConfig, event) {
events = append(events, event)
}
}
if len(events) == 0 {
ginx.Bomb(http.StatusBadRequest, "not events applicable")
}
notifyChannels, err := models.NotifyChannelGets(rt.Ctx, f.NotifyConfig.ChannelID, "", "", -1)
ginx.Dangerous(err)
if len(notifyChannels) == 0 {
ginx.Bomb(http.StatusBadRequest, "notify channel not found")
}
notifyChannel := notifyChannels[0]
if !notifyChannel.Enable {
ginx.Bomb(http.StatusBadRequest, "notify channel not enabled, please enable it first")
}
tplContent := make(map[string]interface{})
if notifyChannel.RequestType != "flashtudy" {
messageTemplates, err := models.MessageTemplateGets(rt.Ctx, f.NotifyConfig.TemplateID, "", "")
ginx.Dangerous(err)
if len(messageTemplates) == 0 {
ginx.Bomb(http.StatusBadRequest, "message template not found")
}
tplContent = messageTemplates[0].RenderEvent(events)
}
var contactKey string
if notifyChannel.ParamConfig != nil && notifyChannel.ParamConfig.UserInfo != nil {
contactKey = notifyChannel.ParamConfig.UserInfo.ContactKey
}
sendtos, flashDutyChannelIDs, customParams := dispatch.GetNotifyConfigParams(&f.NotifyConfig, contactKey, rt.UserCache, rt.UserGroupCache)
var resp string
switch notifyChannel.RequestType {
case "flashduty":
client, err := models.GetHTTPClient(notifyChannel)
ginx.Dangerous(err)
for i := range flashDutyChannelIDs {
resp, err = notifyChannel.SendFlashDuty(events, flashDutyChannelIDs[i], client)
if err != nil {
break
}
}
logger.Infof("channel_name: %v, event:%+v, tplContent:%s, customParams:%v, respBody: %v, err: %v", notifyChannel.Name, events[0], tplContent, customParams, resp, err)
ginx.NewRender(c).Data(resp, err)
case "http":
client, err := models.GetHTTPClient(notifyChannel)
ginx.Dangerous(err)
if notifyChannel.RequestConfig == nil {
ginx.Bomb(http.StatusBadRequest, "request config not found")
}
if notifyChannel.RequestConfig.HTTPRequestConfig == nil {
ginx.Bomb(http.StatusBadRequest, "http request config not found")
}
if dispatch.NeedBatchContacts(notifyChannel.RequestConfig.HTTPRequestConfig) || len(sendtos) == 0 {
resp, err = notifyChannel.SendHTTP(events, tplContent, customParams, sendtos, client)
logger.Infof("channel_name: %v, event:%+v, sendtos:%+v, tplContent:%s, customParams:%v, respBody: %v, err: %v", notifyChannel.Name, events[0], sendtos, tplContent, customParams, resp, err)
if err != nil {
logger.Errorf("failed to send http notify: %v", err)
}
ginx.NewRender(c).Data(resp, err)
} else {
for i := range sendtos {
resp, err = notifyChannel.SendHTTP(events, tplContent, customParams, []string{sendtos[i]}, client)
logger.Infof("channel_name: %v, event:%+v, tplContent:%s, customParams:%v, sendto:%+v, respBody: %v, err: %v", notifyChannel.Name, events[0], tplContent, customParams, sendtos[i], resp, err)
if err != nil {
logger.Errorf("failed to send http notify: %v", err)
ginx.NewRender(c).Message(err)
return
}
}
ginx.NewRender(c).Message(err)
}
case "smtp":
err := notifyChannel.SendEmailNow(events, tplContent, sendtos)
ginx.NewRender(c).Message(err)
case "script":
resp, _, err := notifyChannel.SendScript(events, tplContent, customParams, sendtos)
logger.Infof("channel_name: %v, event:%+v, tplContent:%s, customParams:%v, respBody: %v, err: %v", notifyChannel.Name, events[0], tplContent, customParams, resp, err)
ginx.NewRender(c).Data(resp, err)
default:
logger.Errorf("unsupported request type: %v", notifyChannel.RequestType)
ginx.NewRender(c).Message(errors.New("unsupported request type"))
}
}
type paramList struct {
Name string `json:"name"`
CName string `json:"cname"`
Value interface{} `json:"value"`
}
func (rt *Router) notifyRuleCustomParamsGet(c *gin.Context) {
notifyChannelID := ginx.QueryInt64(c, "notify_channel_id")
me := c.MustGet("user").(*models.User)
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
ginx.Dangerous(err)
notifyChannel, err := models.NotifyChannelGet(rt.Ctx, "id=?", notifyChannelID)
ginx.Dangerous(err)
keyMap := make(map[string]string)
if notifyChannel == nil {
ginx.NewRender(c).Data([][]paramList{}, nil)
return
}
if notifyChannel.ParamConfig == nil {
ginx.NewRender(c).Data([][]paramList{}, nil)
return
}
for _, param := range notifyChannel.ParamConfig.Custom.Params {
keyMap[param.Key] = param.CName
}
lst, err := models.NotifyRulesGet(rt.Ctx, "", nil)
ginx.Dangerous(err)
res := make([][]paramList, 0)
filter := make(map[string]struct{})
for _, nr := range lst {
if !slice.HaveIntersection[int64](gids, nr.UserGroupIds) {
continue
}
for _, nc := range nr.NotifyConfigs {
if nc.ChannelID != notifyChannelID {
continue
}
list := make([]paramList, 0)
filterKey := ""
for key, value := range nc.Params {
// 找到在通知媒介中的自定义变量配置项,进行 cname 转换
cname, exsits := keyMap[key]
if exsits {
list = append(list, paramList{
Name: key,
CName: cname,
Value: value,
})
}
filterKey += fmt.Sprintf("%s:%s,", key, value)
}
if _, ok := filter[filterKey]; ok {
continue
}
filter[filterKey] = struct{}{}
res = append(res, list)
}
}
ginx.NewRender(c).Data(res, nil)
}

View File

@@ -162,10 +162,10 @@ func (rt *Router) notifyTplAdd(c *gin.Context) {
var f models.NotifyTpl
ginx.BindJSON(c, &f)
user := c.MustGet("user").(*models.User)
f.CreateBy = user.Username
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))
@@ -174,7 +174,7 @@ func (rt *Router) notifyTplAdd(c *gin.Context) {
ginx.Bomb(200, "Refuse to create duplicate channel(unique)")
}
f.CreateAt = time.Now().Unix()
f.CreateAt = time.Now().Unix()
ginx.NewRender(c).Message(f.Create(rt.Ctx))
}
@@ -193,3 +193,13 @@ func (rt *Router) notifyTplDel(c *gin.Context) {
ginx.NewRender(c).Message(f.NotifyTplDelete(rt.Ctx, id))
}
func (rt *Router) messageTemplateGets(c *gin.Context) {
id := ginx.QueryInt64(c, "id", 0)
name := ginx.QueryStr(c, "name", "")
ident := ginx.QueryStr(c, "ident", "")
tpls, err := models.MessageTemplateGets(rt.Ctx, id, name, ident)
ginx.NewRender(c).Data(tpls, err)
}

View File

@@ -0,0 +1,266 @@
package router
import (
"fmt"
"sort"
"sync"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
func CheckDsPerm(c *gin.Context, dsId int64, cate string, q interface{}) bool {
// todo: 后续需要根据 cate 判断是否需要权限
return true
}
type QueryFrom struct {
Queries []Query `json:"queries"`
Exps []Exp `json:"exps"`
}
type Query struct {
Ref string `json:"ref"`
Did int64 `json:"ds_id"`
DsCate string `json:"ds_cate"`
Query interface{} `json:"query"`
}
type Exp struct {
Exp string `json:"exp"`
Ref string `json:"ref"`
}
type LogResp struct {
Total int64 `json:"total"`
List []interface{} `json:"list"`
}
func QueryLogBatchConcurrently(anonymousAccess bool, ctx *gin.Context, f QueryFrom) (LogResp, error) {
var resp LogResp
var mu sync.Mutex
var wg sync.WaitGroup
var errs []error
for _, q := range f.Queries {
if !anonymousAccess && !CheckDsPerm(ctx, q.Did, q.DsCate, q) {
return LogResp{}, fmt.Errorf("no permission")
}
plug, exists := dscache.DsCache.Get(q.DsCate, q.Did)
if !exists {
logger.Warningf("cluster:%d not exists query:%+v", q.Did, q)
return LogResp{}, fmt.Errorf("cluster not exists")
}
wg.Add(1)
go func(query Query) {
defer wg.Done()
data, total, err := plug.QueryLog(ctx.Request.Context(), query.Query)
mu.Lock()
defer mu.Unlock()
if err != nil {
errMsg := fmt.Sprintf("query data error: %v query:%v\n ", err, query)
logger.Warningf(errMsg)
errs = append(errs, err)
return
}
m := make(map[string]interface{})
m["ref"] = query.Ref
m["ds_id"] = query.Did
m["ds_cate"] = query.DsCate
m["data"] = data
resp.List = append(resp.List, m)
resp.Total += total
}(q)
}
wg.Wait()
if len(errs) > 0 {
return LogResp{}, errs[0]
}
if len(resp.List) == 0 {
return LogResp{}, fmt.Errorf("no data")
}
return resp, nil
}
func (rt *Router) QueryLogBatch(c *gin.Context) {
var f QueryFrom
ginx.BindJSON(c, &f)
resp, err := QueryLogBatchConcurrently(rt.Center.AnonymousAccess.PromQuerier, c, f)
if err != nil {
ginx.Bomb(200, "err:%v", err)
}
ginx.NewRender(c).Data(resp, nil)
}
func QueryDataConcurrently(anonymousAccess bool, ctx *gin.Context, f models.QueryParam) ([]models.DataResp, error) {
var resp []models.DataResp
var mu sync.Mutex
var wg sync.WaitGroup
var errs []error
for _, q := range f.Querys {
if !anonymousAccess && !CheckDsPerm(ctx, f.DatasourceId, f.Cate, q) {
return nil, fmt.Errorf("no permission")
}
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
return nil, fmt.Errorf("cluster not exists")
}
wg.Add(1)
go func(query interface{}) {
defer wg.Done()
datas, err := plug.QueryData(ctx.Request.Context(), query)
if err != nil {
logger.Warningf("query data error: req:%+v err:%v", query, err)
mu.Lock()
errs = append(errs, err)
mu.Unlock()
return
}
logger.Debugf("query data: req:%+v resp:%+v", query, datas)
mu.Lock()
resp = append(resp, datas...)
mu.Unlock()
}(q)
}
wg.Wait()
if len(errs) > 0 {
return nil, errs[0]
}
// 面向API的统一处理
// 按照 .Metric 排序
// 确保仪表盘中相同图例的曲线颜色相同
if len(resp) > 1 {
sort.Slice(resp, func(i, j int) bool {
if resp[i].Metric != nil && resp[j].Metric != nil {
return resp[i].Metric.String() < resp[j].Metric.String()
}
return false
})
}
return resp, nil
}
func (rt *Router) QueryData(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
resp, err := QueryDataConcurrently(rt.Center.AnonymousAccess.PromQuerier, c, f)
if err != nil {
ginx.Bomb(200, "err:%v", err)
}
ginx.NewRender(c).Data(resp, nil)
}
// QueryLogConcurrently 并发查询日志
func QueryLogConcurrently(anonymousAccess bool, ctx *gin.Context, f models.QueryParam) (LogResp, error) {
var resp LogResp
var mu sync.Mutex
var wg sync.WaitGroup
var errs []error
for _, q := range f.Querys {
if !anonymousAccess && !CheckDsPerm(ctx, f.DatasourceId, f.Cate, q) {
return LogResp{}, fmt.Errorf("no permission")
}
plug, exists := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists query:%+v", f.DatasourceId, f)
return LogResp{}, fmt.Errorf("cluster not exists")
}
wg.Add(1)
go func(query interface{}) {
defer wg.Done()
data, total, err := plug.QueryLog(ctx.Request.Context(), query)
logger.Debugf("query log: req:%+v resp:%+v", query, data)
if err != nil {
errMsg := fmt.Sprintf("query data error: %v query:%v\n ", err, query)
logger.Warningf(errMsg)
mu.Lock()
errs = append(errs, err)
mu.Unlock()
return
}
mu.Lock()
resp.List = append(resp.List, data...)
resp.Total += total
mu.Unlock()
}(q)
}
wg.Wait()
if len(errs) > 0 {
return LogResp{}, errs[0]
}
if len(resp.List) == 0 {
return LogResp{}, fmt.Errorf("no data")
}
return resp, nil
}
func (rt *Router) QueryLogV2(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
resp, err := QueryLogConcurrently(rt.Center.AnonymousAccess.PromQuerier, c, f)
ginx.NewRender(c).Data(resp, err)
}
func (rt *Router) QueryLog(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
var resp []interface{}
for _, q := range f.Querys {
if !rt.Center.AnonymousAccess.PromQuerier && !CheckDsPerm(c, f.DatasourceId, f.Cate, q) {
ginx.Bomb(200, "no permission")
}
plug, exists := dscache.DsCache.Get("elasticsearch", f.DatasourceId)
if !exists {
logger.Warningf("cluster:%d not exists", f.DatasourceId)
ginx.Bomb(200, "cluster not exists")
}
data, _, err := plug.QueryLog(c.Request.Context(), q)
if err != nil {
logger.Warningf("query data error: %v", err)
ginx.Bomb(200, "err:%v", err)
continue
}
resp = append(resp, data...)
}
ginx.NewRender(c).Data(resp, nil)
}

View File

@@ -6,10 +6,10 @@ import (
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/str"
)
func (rt *Router) recordingRuleGets(c *gin.Context) {
@@ -19,7 +19,7 @@ func (rt *Router) recordingRuleGets(c *gin.Context) {
}
func (rt *Router) recordingRuleGetsByGids(c *gin.Context) {
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
gids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "gids", ""), ",")
if len(gids) > 0 {
for _, gid := range gids {
rt.bgroCheck(c, gid)

View File

@@ -21,7 +21,9 @@ func (rt *Router) permsGets(c *gin.Context) {
if user.IsAdmin() {
var lst []string
for _, ops := range cconf.Operations.Ops {
lst = append(lst, ops.Ops...)
for _, op := range ops.Ops {
lst = append(lst, op.Name)
}
}
ginx.NewRender(c).Data(lst, nil)
return

View File

@@ -11,24 +11,32 @@ import (
)
func (rt *Router) operationOfRole(c *gin.Context) {
var (
role *models.Role
err error
res []string
roleOperations []string
)
id := ginx.UrlParamInt64(c, "id")
role, err := models.RoleGet(rt.Ctx, "id=?", id)
role, err = models.RoleGet(rt.Ctx, "id=?", id)
ginx.Dangerous(err)
if role == nil {
ginx.Bomb(http.StatusOK, "role not found")
}
if role.Name == "Admin" {
var lst []string
for _, ops := range cconf.Operations.Ops {
lst = append(lst, ops.Ops...)
for i := range ops.Ops {
res = append(res, ops.Ops[i].Name)
}
}
ginx.NewRender(c).Data(lst, nil)
return
} else {
roleOperations, err = models.OperationsOfRole(rt.Ctx, []string{role.Name})
res = roleOperations
}
ops, err := models.OperationsOfRole(rt.Ctx, []string{role.Name})
ginx.NewRender(c).Data(ops, err)
ginx.NewRender(c).Data(res, err)
}
func (rt *Router) roleBindOperation(c *gin.Context) {
@@ -52,9 +60,19 @@ func (rt *Router) roleBindOperation(c *gin.Context) {
func (rt *Router) operations(c *gin.Context) {
var ops []cconf.Ops
for _, v := range rt.Operations.Ops {
v.Cname = i18n.Sprintf(c.GetHeader("X-Language"), v.Cname)
ops = append(ops, v)
newOp := cconf.Ops{
Name: v.Name,
Cname: i18n.Sprintf(c.GetHeader("X-Language"), v.Cname),
Ops: []cconf.SingleOp{},
}
for i := range v.Ops {
op := cconf.SingleOp{
Name: v.Ops[i].Name,
Cname: i18n.Sprintf(c.GetHeader("X-Language"), v.Ops[i].Cname),
}
newOp.Ops = append(newOp.Ops, op)
}
ops = append(ops, newOp)
}
ginx.NewRender(c).Data(ops, nil)
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/ccfos/nightingale/v6/pkg/flashduty"
"github.com/ccfos/nightingale/v6/pkg/ormx"
"github.com/ccfos/nightingale/v6/pkg/secu"
"github.com/google/uuid"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
@@ -82,3 +83,48 @@ func (rt *Router) selfPasswordPut(c *gin.Context) {
ginx.NewRender(c).Message(user.ChangePassword(rt.Ctx, oldPassWord, newPassWord))
}
type tokenForm struct {
TokenName string `json:"token_name"`
Token string `json:"token"`
}
func (rt *Router) getToken(c *gin.Context) {
username := c.MustGet("username").(string)
tokens, err := models.GetTokensByUsername(rt.Ctx, username)
ginx.NewRender(c).Data(tokens, err)
}
func (rt *Router) addToken(c *gin.Context) {
var f tokenForm
ginx.BindJSON(c, &f)
username := c.MustGet("username").(string)
tokens, err := models.GetTokensByUsername(rt.Ctx, username)
ginx.Dangerous(err)
for _, token := range tokens {
if token.TokenName == f.TokenName {
ginx.NewRender(c).Message("token name already exists")
return
}
}
token, err := models.AddToken(rt.Ctx, username, uuid.New().String(), f.TokenName)
ginx.NewRender(c).Data(token, err)
}
func (rt *Router) deleteToken(c *gin.Context) {
id := ginx.UrlParamInt64(c, "id")
username := c.MustGet("username").(string)
tokenCount, err := models.CountToken(rt.Ctx, username)
ginx.Dangerous(err)
if tokenCount <= 1 {
ginx.NewRender(c).Message("cannot delete the last token")
return
}
ginx.NewRender(c).Message(models.DeleteToken(rt.Ctx, id))
}

View File

@@ -9,13 +9,14 @@ import (
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/ccfos/nightingale/v6/storage"
"github.com/gin-gonic/gin"
"github.com/prometheus/common/model"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/str"
)
type TargetQuery struct {
@@ -43,7 +44,7 @@ func (rt *Router) targetGetsByHostFilter(c *gin.Context) {
}
func (rt *Router) targetGets(c *gin.Context) {
bgids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
bgids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "gids", ""), ",")
query := ginx.QueryStr(c, "query", "")
limit := ginx.QueryInt(c, "limit", 30)
downtime := ginx.QueryInt64(c, "downtime", 0)
@@ -55,7 +56,11 @@ func (rt *Router) targetGets(c *gin.Context) {
hosts := queryStrListField(c, "hosts", ",", " ", "\n")
var err error
if len(bgids) == 0 {
if len(bgids) > 0 {
for _, gid := range bgids {
rt.bgroCheck(c, gid)
}
} else {
user := c.MustGet("user").(*models.User)
if !user.IsAdmin() {
// 如果是非 admin 用户,全部对象的情况,找到用户有权限的业务组
@@ -272,11 +277,9 @@ func (rt *Router) validateTags(tags []string) error {
}
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+"=") {
if _, exist := target.TagsMap[tagKey]; exist {
return fmt.Errorf("duplicate tagkey(%s)", tagKey)
}
}
@@ -401,6 +404,21 @@ type targetBgidsForm struct {
Action string `json:"action"` // add del reset
}
func haveNeverGroupedIdent(ctx *ctx.Context, idents []string) (bool, error) {
for _, ident := range idents {
bgids, err := models.TargetGroupIdsGetByIdent(ctx, ident)
if err != nil {
return false, err
}
if len(bgids) <= 0 {
return true, nil
}
}
return false, nil
}
func (rt *Router) targetBindBgids(c *gin.Context) {
var f targetBgidsForm
var err error
@@ -443,11 +461,15 @@ func (rt *Router) targetBindBgids(c *gin.Context) {
ginx.Bomb(http.StatusForbidden, "No permission. You are not admin of BG(%s)", bg.Name)
}
}
isNeverGrouped, checkErr := haveNeverGroupedIdent(rt.Ctx, f.Idents)
ginx.Dangerous(checkErr)
can, err := user.CheckPerm(rt.Ctx, "/targets/bind")
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
if isNeverGrouped {
can, err := user.CheckPerm(rt.Ctx, "/targets/bind")
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
}
}
}
@@ -552,3 +574,27 @@ func (rt *Router) targetsOfAlertRule(c *gin.Context) {
ginx.NewRender(c).Data(ret, err)
}
func (rt *Router) checkTargetsExistByIndent(idents []string) {
existingIdents, err := models.TargetNoExistIdents(rt.Ctx, idents)
ginx.Dangerous(err)
if len(existingIdents) > 0 {
ginx.Bomb(http.StatusBadRequest, "targets not exist: %s", strings.Join(existingIdents, ","))
}
}
func (rt *Router) targetsOfHostQuery(c *gin.Context) {
var queries []models.HostQuery
ginx.BindJSON(c, &queries)
hostsQuery := models.GetHostsQuery(queries)
session := models.TargetFilterQueryBuild(rt.Ctx, hostsQuery, 0, 0)
var lst []*models.Target
err := session.Find(&lst).Error
if err != nil {
ginx.Bomb(http.StatusInternalServerError, err.Error())
}
ginx.NewRender(c).Data(lst, nil)
}

View File

@@ -5,11 +5,11 @@ import (
"github.com/ccfos/nightingale/v6/alert/sender"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/i18n"
"github.com/toolkits/pkg/str"
)
func (rt *Router) taskGets(c *gin.Context) {
@@ -40,7 +40,7 @@ func (rt *Router) taskGets(c *gin.Context) {
}
func (rt *Router) taskGetsByGids(c *gin.Context) {
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
gids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "gids", ""), ",")
if len(gids) > 0 {
for _, gid := range gids {
rt.bgroCheck(c, gid)
@@ -117,6 +117,8 @@ func (rt *Router) taskAdd(c *gin.Context) {
user := c.MustGet("user").(*models.User)
f.Creator = user.Username
rt.checkTargetsExistByIndent(f.Hosts)
err := f.Verify()
ginx.Dangerous(err)

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/strx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
@@ -35,7 +36,7 @@ func (rt *Router) taskTplGetsByGids(c *gin.Context) {
query := ginx.QueryStr(c, "query", "")
limit := ginx.QueryInt(c, "limit", 20)
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
gids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "gids", ""), ",")
if len(gids) > 0 {
for _, gid := range gids {
rt.bgroCheck(c, gid)
@@ -130,6 +131,8 @@ func (rt *Router) taskTplAdd(c *gin.Context) {
user := c.MustGet("user").(*models.User)
now := time.Now().Unix()
rt.checkTargetsExistByIndent(f.Hosts)
sort.Strings(f.Tags)
tpl := &models.TaskTpl{
@@ -168,6 +171,8 @@ func (rt *Router) taskTplPut(c *gin.Context) {
var f taskTplForm
ginx.BindJSON(c, &f)
rt.checkTargetsExistByIndent(f.Hosts)
sort.Strings(f.Tags)
tpl.Title = f.Title

View File

@@ -1,14 +1,14 @@
package router
import (
"net/http"
"fmt"
"github.com/ccfos/nightingale/v6/center/cconf"
"github.com/ccfos/nightingale/v6/datasource/tdengine"
"github.com/ccfos/nightingale/v6/dscache"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
"net/http"
)
type databasesQueryForm struct {
@@ -20,13 +20,13 @@ func (rt *Router) tdengineDatabases(c *gin.Context) {
var f databasesQueryForm
ginx.BindJSON(c, &f)
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
if tdClient == nil {
datasource, hit := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if _, ok := datasource.(*tdengine.TDengine); !hit || !ok {
ginx.NewRender(c, http.StatusNotFound).Message("No such datasource")
return
}
databases, err := tdClient.GetDatabases()
databases, err := datasource.(*tdengine.TDengine).ShowDatabases(rt.Ctx.Ctx)
ginx.NewRender(c).Data(databases, err)
}
@@ -37,18 +37,29 @@ type tablesQueryForm struct {
IsStable bool `json:"is_stable"`
}
type Column struct {
Name string `json:"name"`
Type string `json:"type"`
Size int `json:"size"`
}
// get tdengine tables
func (rt *Router) tdengineTables(c *gin.Context) {
var f tablesQueryForm
ginx.BindJSON(c, &f)
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
if tdClient == nil {
datasource, hit := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if _, ok := datasource.(*tdengine.TDengine); !hit || !ok {
ginx.NewRender(c, http.StatusNotFound).Message("No such datasource")
return
}
tables, err := tdClient.GetTables(f.Database, f.IsStable)
database := fmt.Sprintf("%s.tables", f.Database)
if f.IsStable {
database = fmt.Sprintf("%s.stables", f.Database)
}
tables, err := datasource.(*tdengine.TDengine).ShowTables(rt.Ctx.Ctx, database)
ginx.NewRender(c).Data(tables, err)
}
@@ -59,50 +70,31 @@ type columnsQueryForm struct {
Table string `json:"table"`
}
// get tdengine columns
func (rt *Router) tdengineColumns(c *gin.Context) {
var f columnsQueryForm
ginx.BindJSON(c, &f)
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
if tdClient == nil {
datasource, hit := dscache.DsCache.Get(f.Cate, f.DatasourceId)
if _, ok := datasource.(*tdengine.TDengine); !hit || !ok {
ginx.NewRender(c, http.StatusNotFound).Message("No such datasource")
return
}
columns, err := tdClient.GetColumns(f.Database, f.Table)
ginx.NewRender(c).Data(columns, err)
}
func (rt *Router) QueryData(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
var resp []models.DataResp
var err error
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
for _, q := range f.Querys {
datas, err := tdClient.Query(q)
ginx.Dangerous(err)
resp = append(resp, datas...)
query := map[string]string{
"database": f.Database,
"table": f.Table,
}
ginx.NewRender(c).Data(resp, err)
}
func (rt *Router) QueryLog(c *gin.Context) {
var f models.QueryParam
ginx.BindJSON(c, &f)
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
if len(f.Querys) == 0 {
ginx.Bomb(200, "querys is empty")
return
columns, err := datasource.(*tdengine.TDengine).DescribeTable(rt.Ctx.Ctx, query)
// 对齐前端,后续可以将 tdEngine 的查数据的接口都统一
tdColumns := make([]Column, len(columns))
for i, column := range columns {
tdColumns[i] = Column{
Name: column.Field,
Type: column.Type,
}
}
data, err := tdClient.QueryLog(f.Querys[0])
logger.Debugf("tdengine query:%s result: %+v", f.Querys[0], data)
ginx.NewRender(c).Data(data, err)
ginx.NewRender(c).Data(tdColumns, err)
}
// query sql template

View File

@@ -47,12 +47,27 @@ func (rt *Router) userGets(c *gin.Context) {
query := ginx.QueryStr(c, "query", "")
order := ginx.QueryStr(c, "order", "username")
desc := ginx.QueryBool(c, "desc", false)
usernames := strings.Split(ginx.QueryStr(c, "usernames", ""), ",")
phones := strings.Split(ginx.QueryStr(c, "phones", ""), ",")
emails := strings.Split(ginx.QueryStr(c, "emails", ""), ",")
if len(usernames) == 1 && usernames[0] == "" {
usernames = []string{}
}
if len(phones) == 1 && phones[0] == "" {
phones = []string{}
}
if len(emails) == 1 && emails[0] == "" {
emails = []string{}
}
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), stime, etime, order, desc)
list, err := models.UserGets(rt.Ctx, query, limit, ginx.Offset(c, limit), stime, etime, order, desc, usernames, phones, emails)
ginx.Dangerous(err)
user := c.MustGet("user").(*models.User)

View File

@@ -6,11 +6,11 @@ import (
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/flashduty"
"github.com/ccfos/nightingale/v6/pkg/strx"
"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) {
@@ -32,7 +32,7 @@ func (rt *Router) userGroupGets(c *gin.Context) {
}
func (rt *Router) userGroupGetsByService(c *gin.Context) {
ids := str.IdsInt64(ginx.QueryStr(c, "ids", ""))
ids := strx.IdsInt64ForAPI(ginx.QueryStr(c, "ids", ""))
if len(ids) == 0 {
lst, err := models.UserGroupGetAll(rt.Ctx)
@@ -111,7 +111,6 @@ func (rt *Router) userGroupPut(c *gin.Context) {
me := c.MustGet("user").(*models.User)
ug := c.MustGet("user_group").(*models.UserGroup)
oldUGName := ug.Name
if ug.Name != f.Name {
// name changed, check duplication
@@ -130,7 +129,7 @@ func (rt *Router) userGroupPut(c *gin.Context) {
if f.IsSyncToFlashDuty || flashduty.NeedSyncTeam(rt.Ctx) {
ugs, err := flashduty.NewUserGroupSyncer(rt.Ctx, ug)
ginx.Dangerous(err)
err = ugs.SyncUGPut(oldUGName)
err = ugs.SyncUGPut()
ginx.Dangerous(err)
}
ginx.NewRender(c).Message(ug.Update(rt.Ctx, "Name", "Note", "UpdateAt", "UpdateBy"))
@@ -159,8 +158,11 @@ func (rt *Router) userGroupDel(c *gin.Context) {
if isSyncToFlashDuty || flashduty.NeedSyncTeam(rt.Ctx) {
ugs, err := flashduty.NewUserGroupSyncer(rt.Ctx, ug)
ginx.Dangerous(err)
err = ugs.SyncUGDel(ug.Name)
ginx.Dangerous(err)
err = ugs.SyncUGDel()
// 如果team 在 duty 被引用或者已经删除,会报错,可以忽略报错
if err != nil {
logger.Warningf("failed to sync user group %s to flashduty's team: %v", ug.Name, err)
}
}
ginx.NewRender(c).Message(ug.Del(rt.Ctx))

View File

@@ -12,18 +12,18 @@ import (
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/dscache"
"github.com/ccfos/nightingale/v6/dumper"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/pkg/logx"
"github.com/ccfos/nightingale/v6/pkg/macros"
"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"
"github.com/flashcatcloud/ibex/src/cmd/ibex"
)
@@ -61,6 +61,8 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
r := httpx.GinEngine(config.Global.RunMode, config.HTTP, configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
pushgwRouter.Config(r)
macros.RegisterMacro(macros.MacroInVain)
dscache.Init(ctx, false)
if !config.Alert.Disable {
configCache := memsto.NewConfigCache(ctx, syncStats, nil, "")
@@ -72,16 +74,18 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
userCache := memsto.NewUserCache(ctx, syncStats)
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
taskTplsCache := memsto.NewTaskTplCache(ctx)
notifyRuleCache := memsto.NewNotifyRuleCache(ctx, syncStats)
notifyChannelCache := memsto.NewNotifyChannelCache(ctx, syncStats)
messageTemplateCache := memsto.NewMessageTemplateCache(ctx, syncStats)
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, taskTplsCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, userCache, userGroupCache, notifyRuleCache, notifyChannelCache, messageTemplateCache)
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)

241
datasource/ck/clickhouse.go Normal file
View File

@@ -0,0 +1,241 @@
package ck
import (
"context"
"fmt"
"strings"
"github.com/ccfos/nightingale/v6/datasource"
ck "github.com/ccfos/nightingale/v6/dskit/clickhouse"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/macros"
"github.com/mitchellh/mapstructure"
"github.com/toolkits/pkg/logger"
)
const (
CKType = "ck"
TimeFieldFormatEpochMilli = "epoch_millis"
TimeFieldFormatEpochSecond = "epoch_second"
DefaultLimit = 500
)
var (
ckPrivBanned = []string{
"INSERT",
"CREATE",
"DROP",
"DELETE",
"UPDATE",
"ALL",
}
ckBannedOp = map[string]struct{}{
"CREATE": {},
"INSERT": {},
"ALTER": {},
"REVOKE": {},
"DROP": {},
"RENAME": {},
"ATTACH": {},
"DETACH": {},
"OPTIMIZE": {},
"TRUNCATE": {},
"SET": {},
}
)
func init() {
datasource.RegisterDatasource(CKType, new(Clickhouse))
}
type CKShard struct {
Addr string `json:"ck.addr" mapstructure:"ck.addr"`
User string `json:"ck.user" mapstructure:"ck.user"`
Password string `json:"ck.password" mapstructure:"ck.password"`
Database string `json:"ck.db" mapstructure:"ck.db"`
IsEncrypted bool `json:"ck.is_encrypt" mapstructure:"ck.is_encrypt"`
}
type QueryParam struct {
Limit int `json:"limit" mapstructure:"limit"`
Sql string `json:"sql" mapstructure:"sql"`
Ref string `json:"ref" mapstructure:"ref"`
From int64 `json:"from" mapstructure:"from"`
To int64 `json:"to" mapstructure:"to"`
TimeField string `json:"time_field" mapstructure:"time_field"`
TimeFormat string `json:"time_format" mapstructure:"time_format"`
Keys datasource.Keys `json:"keys" mapstructure:"keys"`
Database string `json:"database" mapstructure:"database"`
Table string `json:"table" mapstructure:"table"`
}
type Clickhouse struct {
ck.Clickhouse `json:",inline" mapstructure:",squash"`
}
func (c *Clickhouse) Init(settings map[string]interface{}) (datasource.Datasource, error) {
newest := new(Clickhouse)
err := mapstructure.Decode(settings, newest)
return newest, err
}
func (c *Clickhouse) InitClient() error {
return c.InitCli()
}
func (c *Clickhouse) Validate(ctx context.Context) error {
if len(c.Nodes) == 0 {
return fmt.Errorf("ck shard is invalid, please check datasource setting")
}
addr := c.Nodes[0]
if len(strings.Trim(c.User, " ")) == 0 {
return fmt.Errorf("ck shard user is invalid, please check datasource setting")
}
if len(strings.Trim(addr, " ")) == 0 {
return fmt.Errorf("ck shard addr is invalid, please check datasource setting")
}
// if len(strings.Trim(shard.Password, " ")) == 0 {
// return fmt.Errorf("ck shard password is empty, please check datasource setting or set password for user")
// }
return nil
}
// Equal compares whether two objects are the same, used for caching
func (c *Clickhouse) Equal(p datasource.Datasource) bool {
plg, ok := p.(*Clickhouse)
if !ok {
logger.Errorf("unexpected plugin type, expected is ck")
return false
}
// only compare first shard
if len(c.Nodes) == 0 {
logger.Errorf("ck shard is empty")
return false
}
addr := c.Nodes[0]
if len(plg.Nodes) == 0 {
logger.Errorf("new ck plugin obj shard is empty")
return false
}
newAddr := plg.Nodes[0]
if c.User != plg.User {
return false
}
if addr != newAddr {
return false
}
if c.Password != plg.Password {
return false
}
return true
}
func (c *Clickhouse) MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return nil, nil
}
func (c *Clickhouse) MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return nil, nil
}
func (c *Clickhouse) QueryMapData(ctx context.Context, query interface{}) ([]map[string]string, error) {
return nil, nil
}
func (c *Clickhouse) QueryData(ctx context.Context, query interface{}) ([]models.DataResp, error) {
ckQueryParam := new(ck.QueryParam)
if err := mapstructure.Decode(query, ckQueryParam); err != nil {
return nil, err
}
if strings.Contains(ckQueryParam.Sql, "$__") {
var err error
ckQueryParam.Sql, err = macros.Macro(ckQueryParam.Sql, ckQueryParam.From, ckQueryParam.To)
if err != nil {
return nil, err
}
}
if ckQueryParam.Keys.ValueKey == "" {
return nil, fmt.Errorf("valueKey is required")
}
rows, err := c.QueryTimeseries(ctx, ckQueryParam)
if err != nil {
logger.Warningf("query:%+v get data err:%v", ckQueryParam, err)
return nil, err
}
if err != nil {
logger.Warningf("query:%+v get data err:%v", ckQueryParam, err)
return []models.DataResp{}, err
}
data := make([]models.DataResp, 0)
for i := range rows {
data = append(data, models.DataResp{
Ref: ckQueryParam.Ref,
Metric: rows[i].Metric,
Values: rows[i].Values,
})
}
return data, nil
}
func (c *Clickhouse) QueryLog(ctx context.Context, query interface{}) ([]interface{}, int64, error) {
ckQueryParam := new(QueryParam)
if err := mapstructure.Decode(query, ckQueryParam); err != nil {
return nil, 0, err
}
if strings.Contains(ckQueryParam.Sql, "$__") {
var err error
ckQueryParam.Sql, err = macros.Macro(ckQueryParam.Sql, ckQueryParam.From, ckQueryParam.To)
if err != nil {
return nil, 0, err
}
}
rows, err := c.Query(ctx, ckQueryParam)
if err != nil {
logger.Warningf("query:%+v get data err:%v", ckQueryParam, err)
return nil, 0, err
}
limit := getLimit(len(rows), ckQueryParam.Limit)
logs := make([]interface{}, 0)
for i := 0; i < limit; i++ {
logs = append(logs, rows[i])
}
return logs, int64(limit), nil
}
func getLimit(rowLen, pLimit int) int {
limit := DefaultLimit
if pLimit > 0 {
limit = pLimit
}
if rowLen > limit {
return limit
}
return rowLen
}

View File

@@ -0,0 +1,653 @@
package eslike
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/araddon/dateparse"
"github.com/bitly/go-simplejson"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/mitchellh/mapstructure"
"github.com/olivere/elastic/v7"
"github.com/prometheus/common/model"
"github.com/toolkits/pkg/logger"
)
type Query struct {
Ref string `json:"ref" mapstructure:"ref"`
IndexType string `json:"index_type" mapstructure:"index_type"` // 普通索引:index 索引模式:index_pattern
Index string `json:"index" mapstructure:"index"`
IndexPatternId int64 `json:"index_pattern" mapstructure:"index_pattern"`
Filter string `json:"filter" mapstructure:"filter"`
Offset int64 `json:"offset" mapstructure:"offset"`
MetricAggr MetricAggr `json:"value" mapstructure:"value"`
GroupBy []GroupBy `json:"group_by" mapstructure:"group_by"`
DateField string `json:"date_field" mapstructure:"date_field"`
Interval int64 `json:"interval" mapstructure:"interval"`
Start int64 `json:"start" mapstructure:"start"`
End int64 `json:"end" mapstructure:"end"`
P int `json:"page" mapstructure:"page"` // 页码
Limit int `json:"limit" mapstructure:"limit"` // 每页个数
Ascending bool `json:"ascending" mapstructure:"ascending"` // 按照DataField排序
Timeout int `json:"timeout" mapstructure:"timeout"`
MaxShard int `json:"max_shard" mapstructure:"max_shard"`
}
type MetricAggr struct {
Field string `json:"field" mapstructure:"field"`
Func string `json:"func" mapstructure:"func"`
Ref string `json:"ref" mapstructure:"ref"` // 变量名A B C
}
type GroupBy struct {
Cate GroupByCate `json:"cate" mapstructure:"cate"` // 分组类型
Field string `json:"field" mapstructure:"field"`
MinDocCount int64 `json:"min_doc_count" mapstructure:"min_doc_count"`
Order string `json:"order" mapstructure:"order"`
OrderBy string `json:"order_by" mapstructure:"order_by"`
Size int `json:"size" mapstructure:"size"`
Params []Param `json:"params" mapstructure:"params"` // 类型是 filter 时使用
Interval int64 `json:"interval" mapstructure:"interval"` // 分组间隔
}
type SearchFunc func(ctx context.Context, indices []string, source interface{}, timeout int, maxShard int) (*elastic.SearchResult, error)
type QueryFieldsFunc func(indices []string) ([]string, error)
// 分组类型
type GroupByCate string
const (
Filters GroupByCate = "filters"
Histgram GroupByCate = "histgram"
Terms GroupByCate = "terms"
)
// 参数
type Param struct {
Alias string `json:"alias,omitempty"` // 别名a=b的形式filter 特有
Query string `json:"query,omitempty"` // 查询条件filter 特有
}
type MetricPtr struct {
Data map[string][][]float64
}
func IterGetMap(m, ret map[string]interface{}, prefixKey string) {
for k, v := range m {
switch v.(type) {
case map[string]interface{}:
var key string
if prefixKey != "" {
key = fmt.Sprintf("%s.%s", prefixKey, k)
} else {
key = k
}
IterGetMap(v.(map[string]interface{}), ret, key)
default:
ret[prefixKey+"."+k] = []interface{}{v}
}
}
}
func TransferData(metric, ref string, m map[string][][]float64) []models.DataResp {
var datas []models.DataResp
for k, v := range m {
data := models.DataResp{
Ref: ref,
Metric: make(model.Metric),
Labels: k,
Values: v,
}
data.Metric["__name__"] = model.LabelValue(metric)
labels := strings.Split(k, "--")
for _, label := range labels {
arr := strings.SplitN(label, "=", 2)
if len(arr) == 2 {
data.Metric[model.LabelName(arr[0])] = model.LabelValue(arr[1])
}
}
datas = append(datas, data)
}
for i := 0; i < len(datas); i++ {
for k, v := range datas[i].Metric {
if k == "__name__" {
datas[i].Metric[k] = model.LabelValue(ref) + "_" + v
}
}
}
return datas
}
func GetQueryString(filter string, q *elastic.RangeQuery) *elastic.BoolQuery {
var queryString *elastic.BoolQuery
if filter != "" {
if strings.Contains(filter, ":") || strings.Contains(filter, "AND") || strings.Contains(filter, "OR") || strings.Contains(filter, "NOT") {
queryString = elastic.NewBoolQuery().Must(elastic.NewQueryStringQuery(filter)).Filter(q)
} else {
queryString = elastic.NewBoolQuery().Filter(elastic.NewMultiMatchQuery(filter).Lenient(true).Type("phrase")).Filter(q)
}
} else {
queryString = elastic.NewBoolQuery().Should(q)
}
return queryString
}
func getUnixTs(timeStr string) int64 {
ts, err := strconv.ParseInt(timeStr, 10, 64)
if err == nil {
return ts
}
parsedTime, err := dateparse.ParseAny(timeStr)
if err != nil {
logger.Error("failed to ParseAny: ", err)
return 0
}
return parsedTime.UnixMilli()
}
func GetBuckts(labelKey string, keys []string, arr []interface{}, metrics *MetricPtr, labels string, ts int64, f string) {
var err error
bucketsKey := ""
if len(keys) > 0 {
bucketsKey = keys[0]
}
newlabels := ""
for i := 0; i < len(arr); i++ {
tmp := arr[i].(map[string]interface{})
keyAsString, getTs := tmp["key_as_string"]
if getTs {
ts = getUnixTs(keyAsString.(string))
}
keyValue := tmp["key"]
switch keyValue.(type) {
case json.Number, string:
if !getTs {
if labels != "" {
newlabels = fmt.Sprintf("%s--%s=%v", labels, labelKey, keyValue)
} else {
newlabels = fmt.Sprintf("%s=%v", labelKey, keyValue)
}
}
default:
continue
}
var finalValue float64
if len(keys) == 0 { // 计算 doc_count 的情况
count := tmp["doc_count"]
finalValue, err = count.(json.Number).Float64()
if err != nil {
logger.Warningf("labelKey:%s get value error:%v", labelKey, err)
}
newValues := []float64{float64(ts / 1000), finalValue}
metrics.Data[newlabels] = append(metrics.Data[newlabels], newValues)
continue
}
innerBuckets, exists := tmp[bucketsKey]
if !exists {
continue
}
nextBucketsArr, exists := innerBuckets.(map[string]interface{})["buckets"]
if exists {
if len(keys[1:]) >= 1 {
GetBuckts(bucketsKey, keys[1:], nextBucketsArr.([]interface{}), metrics, newlabels, ts, f)
} else {
GetBuckts(bucketsKey, []string{}, nextBucketsArr.([]interface{}), metrics, newlabels, ts, f)
}
} else {
// doc_count
if f == "count" || f == "nodata" {
count := tmp["doc_count"]
finalValue, err = count.(json.Number).Float64()
if err != nil {
logger.Warningf("get %v value error:%v", count, err)
}
} else {
values, exists := innerBuckets.(map[string]interface{})["value"]
if exists {
switch values.(type) {
case json.Number:
value, err := values.(json.Number).Float64()
if err != nil {
logger.Warningf("labelKey:%s get value error:%v", labelKey, err)
}
finalValue = value
}
} else {
switch values.(type) {
case map[string]interface{}:
var err error
values := innerBuckets.(map[string]interface{})["values"]
for _, v := range values.(map[string]interface{}) {
finalValue, err = v.(json.Number).Float64()
if err != nil {
logger.Warningf("labelKey:%s get value error:%v", labelKey, err)
}
}
default:
values := innerBuckets.(map[string]interface{})["values"]
for _, v := range values.(map[string]interface{}) {
// Todo 修复 v is nil 导致 panic 情况
finalValue, err = v.(json.Number).Float64()
if err != nil {
logger.Warningf("labelKey:%s get value error:%v", labelKey, err)
}
}
}
}
}
if _, exists := metrics.Data[newlabels]; !exists {
metrics.Data[newlabels] = [][]float64{}
}
newValues := []float64{float64(ts / 1000), finalValue}
metrics.Data[newlabels] = append(metrics.Data[newlabels], newValues)
}
}
}
func MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
param := new(Query)
if err := mapstructure.Decode(query, param); err != nil {
return nil, err
}
for i := 0; i < len(eventTags); i++ {
eventTags[i] = strings.Replace(eventTags[i], "=", ":", 1)
}
if len(eventTags) > 0 {
if param.Filter == "" {
param.Filter = strings.Join(eventTags, " AND ")
} else {
param.Filter = param.Filter + " AND " + strings.Join(eventTags, " AND ")
}
}
param.Start = start
param.End = end
return param, nil
}
func MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
param := new(Query)
if err := mapstructure.Decode(query, param); err != nil {
return nil, err
}
for i := 0; i < len(eventTags); i++ {
eventTags[i] = strings.Replace(eventTags[i], "=", ":", 1)
}
if len(eventTags) > 0 {
if param.Filter == "" {
param.Filter = strings.Join(eventTags, " AND ")
} else {
param.Filter = param.Filter + " AND " + strings.Join(eventTags, " AND ")
}
}
param.Start = start
param.End = end
return param, nil
}
var esIndexPatternCache *memsto.EsIndexPatternCacheType
func SetEsIndexPatternCacheType(c *memsto.EsIndexPatternCacheType) {
esIndexPatternCache = c
}
func GetEsIndexPatternCacheType() *memsto.EsIndexPatternCacheType {
return esIndexPatternCache
}
func QueryData(ctx context.Context, queryParam interface{}, cliTimeout int64, version string, search SearchFunc) ([]models.DataResp, error) {
param := new(Query)
if err := mapstructure.Decode(queryParam, param); err != nil {
return nil, err
}
if param.Timeout == 0 {
param.Timeout = int(cliTimeout) / 1000
}
if param.Interval == 0 {
param.Interval = 60
}
if param.MaxShard < 1 {
param.MaxShard = 5
}
if param.DateField == "" {
param.DateField = "@timestamp"
}
var indexArr []string
if param.IndexType == "index_pattern" {
if ip, ok := GetEsIndexPatternCacheType().Get(param.IndexPatternId); ok {
param.DateField = ip.TimeField
indexArr = []string{ip.Name}
param.Index = ip.Name
} else {
return nil, fmt.Errorf("index pattern:%d not found", param.IndexPatternId)
}
} else {
indexArr = strings.Split(param.Index, ",")
}
q := elastic.NewRangeQuery(param.DateField)
now := time.Now().Unix()
var start, end int64
if param.End != 0 && param.Start != 0 {
end = param.End - param.End%param.Interval
start = param.Start - param.Start%param.Interval
} else {
end = now
start = end - param.Interval
}
delay, ok := ctx.Value("delay").(int64)
if ok && delay != 0 {
end = end - delay
start = start - delay
}
if param.Offset > 0 {
end = end - param.Offset
start = start - param.Offset
}
q.Gte(time.Unix(start, 0).UnixMilli())
q.Lte(time.Unix(end, 0).UnixMilli())
q.Format("epoch_millis")
field := param.MetricAggr.Field
groupBys := param.GroupBy
queryString := GetQueryString(param.Filter, q)
var aggr elastic.Aggregation
switch param.MetricAggr.Func {
case "avg":
aggr = elastic.NewAvgAggregation().Field(field)
case "max":
aggr = elastic.NewMaxAggregation().Field(field)
case "min":
aggr = elastic.NewMinAggregation().Field(field)
case "sum":
aggr = elastic.NewSumAggregation().Field(field)
case "count":
aggr = elastic.NewValueCountAggregation().Field(field)
case "p90":
aggr = elastic.NewPercentilesAggregation().Percentiles(90).Field(field)
case "p95":
aggr = elastic.NewPercentilesAggregation().Percentiles(95).Field(field)
case "p99":
aggr = elastic.NewPercentilesAggregation().Percentiles(99).Field(field)
case "median":
aggr = elastic.NewPercentilesAggregation().Percentiles(50).Field(field)
default:
return nil, fmt.Errorf("func %s not support", param.MetricAggr.Func)
}
tsAggr := elastic.NewDateHistogramAggregation().
Field(param.DateField).
MinDocCount(1)
if strings.HasPrefix(version, "7") {
tsAggr.FixedInterval(fmt.Sprintf("%ds", param.Interval))
} else {
// 兼容 7.0 以下的版本
// OpenSearch 也使用这个字段
tsAggr.Interval(fmt.Sprintf("%ds", param.Interval))
}
// group by
var groupByAggregation elastic.Aggregation
if len(groupBys) > 0 {
groupBy := groupBys[0]
if groupBy.MinDocCount == 0 {
groupBy.MinDocCount = 1
}
if groupBy.Size == 0 {
groupBy.Size = 300
}
switch groupBy.Cate {
case Terms:
if param.MetricAggr.Func != "count" {
groupByAggregation = elastic.NewTermsAggregation().Field(groupBy.Field).SubAggregation(field, aggr).OrderByKeyDesc().Size(groupBy.Size).MinDocCount(int(groupBy.MinDocCount))
} else {
groupByAggregation = elastic.NewTermsAggregation().Field(groupBy.Field).OrderByKeyDesc().Size(groupBy.Size).MinDocCount(int(groupBy.MinDocCount))
}
case Histgram:
if param.MetricAggr.Func != "count" {
groupByAggregation = elastic.NewHistogramAggregation().Field(groupBy.Field).Interval(float64(groupBy.Interval)).SubAggregation(field, aggr)
} else {
groupByAggregation = elastic.NewHistogramAggregation().Field(groupBy.Field).Interval(float64(groupBy.Interval))
}
case Filters:
for _, filterParam := range groupBy.Params {
if param.MetricAggr.Func != "count" {
groupByAggregation = elastic.NewFilterAggregation().Filter(elastic.NewTermQuery(filterParam.Query, "true")).SubAggregation(field, aggr)
} else {
groupByAggregation = elastic.NewFilterAggregation().Filter(elastic.NewTermQuery(filterParam.Query, "true"))
}
}
}
for i := 1; i < len(groupBys); i++ {
groupBy := groupBys[i]
if groupBy.MinDocCount == 0 {
groupBy.MinDocCount = 1
}
if groupBy.Size == 0 {
groupBy.Size = 300
}
switch groupBy.Cate {
case Terms:
groupByAggregation = elastic.NewTermsAggregation().Field(groupBy.Field).SubAggregation(groupBys[i-1].Field, groupByAggregation).OrderByKeyDesc().Size(groupBy.Size).MinDocCount(int(groupBy.MinDocCount))
case Histgram:
groupByAggregation = elastic.NewHistogramAggregation().Field(groupBy.Field).Interval(float64(groupBy.Interval)).SubAggregation(groupBys[i-1].Field, groupByAggregation)
case Filters:
for _, filterParam := range groupBy.Params {
groupByAggregation = elastic.NewFilterAggregation().Filter(elastic.NewTermQuery(filterParam.Query, "true")).SubAggregation(groupBys[i-1].Field, groupByAggregation)
}
}
}
tsAggr.SubAggregation(groupBys[len(groupBys)-1].Field, groupByAggregation)
} else if param.MetricAggr.Func != "count" {
tsAggr.SubAggregation(field, aggr)
}
source, _ := queryString.Source()
b, _ := json.Marshal(source)
logger.Debugf("query_data q:%+v indexArr:%+v tsAggr:%+v query_string:%s", param, indexArr, tsAggr, string(b))
searchSource := elastic.NewSearchSource().
Query(queryString).
Aggregation("ts", tsAggr)
searchSourceString, err := searchSource.Source()
if err != nil {
logger.Warningf("query_data searchSource:%s to string error:%v", searchSourceString, err)
}
jsonSearchSource, err := json.Marshal(searchSourceString)
if err != nil {
logger.Warningf("query_data searchSource:%s to json error:%v", searchSourceString, err)
}
result, err := search(ctx, indexArr, searchSource, param.Timeout, param.MaxShard)
if err != nil {
logger.Warningf("query_data searchSource:%s query_data error:%v", searchSourceString, err)
return nil, err
}
logger.Debugf("query_data searchSource:%s resp:%s", string(jsonSearchSource), string(result.Aggregations["ts"]))
js, err := simplejson.NewJson(result.Aggregations["ts"])
if err != nil {
return nil, err
}
bucketsData, err := js.Get("buckets").Array()
if err != nil {
return nil, err
}
var keys []string
for i := len(groupBys) - 1; i >= 0; i-- {
keys = append(keys, groupBys[i].Field)
}
if param.MetricAggr.Func != "count" {
keys = append(keys, field)
}
metrics := &MetricPtr{Data: make(map[string][][]float64)}
GetBuckts("", keys, bucketsData, metrics, "", 0, param.MetricAggr.Func)
items, err := TransferData(fmt.Sprintf("%s_%s", field, param.MetricAggr.Func), param.Ref, metrics.Data), nil
var m map[string]interface{}
bs, _ := json.Marshal(queryParam)
json.Unmarshal(bs, &m)
m["index"] = param.Index
for i := range items {
items[i].Query = fmt.Sprintf("%+v", m)
}
return items, nil
}
func HitFilter(typ string) bool {
switch typ {
case "keyword", "date", "long", "integer", "short", "byte", "double", "float", "half_float", "scaled_float", "unsigned_long":
return false
default:
return true
}
}
func QueryLog(ctx context.Context, queryParam interface{}, timeout int64, version string, maxShard int, search SearchFunc) ([]interface{}, int64, error) {
param := new(Query)
if err := mapstructure.Decode(queryParam, param); err != nil {
return nil, 0, err
}
if param.Timeout == 0 {
param.Timeout = int(timeout)
}
var indexArr []string
if param.IndexType == "index_pattern" {
if ip, ok := GetEsIndexPatternCacheType().Get(param.IndexPatternId); ok {
param.DateField = ip.TimeField
indexArr = []string{ip.Name}
} else {
return nil, 0, fmt.Errorf("index pattern:%d not found", param.IndexPatternId)
}
} else {
indexArr = strings.Split(param.Index, ",")
}
now := time.Now().Unix()
var start, end int64
if param.End != 0 && param.Start != 0 {
end = param.End - param.End%param.Interval
start = param.Start - param.Start%param.Interval
} else {
end = now
start = end - param.Interval
}
q := elastic.NewRangeQuery(param.DateField)
q.Gte(time.Unix(start, 0).UnixMilli())
q.Lte(time.Unix(end, 0).UnixMilli())
q.Format("epoch_millis")
queryString := GetQueryString(param.Filter, q)
if param.Limit <= 0 {
param.Limit = 10
}
if param.MaxShard < 1 {
param.MaxShard = maxShard
}
source := elastic.NewSearchSource().
TrackTotalHits(true).
Query(queryString).
From(param.P).
Size(param.Limit).
Sort(param.DateField, param.Ascending)
result, err := search(ctx, indexArr, source, param.Timeout, param.MaxShard)
if err != nil {
logger.Warningf("query data error:%v", err)
return nil, 0, err
}
total := result.TotalHits()
var ret []interface{}
b, _ := json.Marshal(source)
logger.Debugf("query data result query source:%s len:%d total:%d", string(b), len(result.Hits.Hits), total)
resultBytes, _ := json.Marshal(result)
logger.Debugf("query data result query source:%s result:%s", string(b), string(resultBytes))
if strings.HasPrefix(version, "6") {
for i := 0; i < len(result.Hits.Hits); i++ {
var x map[string]interface{}
err := json.Unmarshal(result.Hits.Hits[i].Source, &x)
if err != nil {
logger.Warningf("Unmarshal soruce error:%v", err)
continue
}
if result.Hits.Hits[i].Fields == nil {
result.Hits.Hits[i].Fields = make(map[string]interface{})
}
IterGetMap(x, result.Hits.Hits[i].Fields, "")
ret = append(ret, result.Hits.Hits[i])
}
} else {
for _, hit := range result.Hits.Hits {
ret = append(ret, hit)
}
}
return ret, total, nil
}

115
datasource/datasource.go Normal file
View File

@@ -0,0 +1,115 @@
package datasource
import (
"context"
"fmt"
"strings"
"github.com/ccfos/nightingale/v6/models"
)
type DatasourceType struct {
Id int64 `json:"id"`
Category string `json:"category"`
PluginType string `json:"type"`
PluginTypeName string `json:"type_name"`
}
type Keys struct {
ValueKey string `json:"valueKey" mapstructure:"valueKey"` // 多个用空格分隔
LabelKey string `json:"labelKey" mapstructure:"labelKey"` // 多个用空格分隔
TimeKey string `json:"timeKey" mapstructure:"timeKey"`
TimeFormat string `json:"timeFormat" mapstructure:"timeFormat"`
}
var DatasourceTypes map[int64]DatasourceType
func init() {
DatasourceTypes = make(map[int64]DatasourceType)
DatasourceTypes[1] = DatasourceType{
Id: 1,
Category: "timeseries",
PluginType: "prometheus",
PluginTypeName: "Prometheus Like",
}
DatasourceTypes[2] = DatasourceType{
Id: 2,
Category: "logging",
PluginType: "elasticsearch",
PluginTypeName: "Elasticsearch",
}
DatasourceTypes[3] = DatasourceType{
Id: 3,
Category: "logging",
PluginType: "aliyun-sls",
PluginTypeName: "SLS",
}
DatasourceTypes[4] = DatasourceType{
Id: 4,
Category: "timeseries",
PluginType: "ck",
PluginTypeName: "ClickHouse",
}
}
type NewDatasrouceFn func(settings map[string]interface{}) (Datasource, error)
var datasourceRegister = map[string]NewDatasrouceFn{}
type Datasource interface {
Init(settings map[string]interface{}) (Datasource, error) // 初始化配置
InitClient() error // 初始化客户端
Validate(ctx context.Context) error // 参数验证
Equal(p Datasource) bool // 验证是否相等
MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error)
MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error)
QueryData(ctx context.Context, query interface{}) ([]models.DataResp, error)
QueryLog(ctx context.Context, query interface{}) ([]interface{}, int64, error)
// 在生成告警事件时,会调用该方法,用于获取额外的数据
QueryMapData(ctx context.Context, query interface{}) ([]map[string]string, error)
}
func RegisterDatasource(typ string, p Datasource) {
if _, found := datasourceRegister[typ]; found {
return
}
datasourceRegister[typ] = p.Init
}
func GetDatasourceByType(typ string, settings map[string]interface{}) (Datasource, error) {
typ = strings.ReplaceAll(typ, ".logging", "")
fn, found := datasourceRegister[typ]
if !found {
return nil, fmt.Errorf("plugin type %s not found", typ)
}
plug, err := fn(settings)
if err != nil {
return nil, err
}
return plug, nil
}
type DatasourceInfo struct {
Id int64 `json:"id"`
Name string `json:"name"`
Identifier string `json:"identifier"`
Description string `json:"description"`
ClusterName string `json:"cluster_name"`
Category string `json:"category"`
PluginId int64 `json:"plugin_id"`
Type string `json:"plugin_type"`
PluginTypeName string `json:"plugin_type_name"`
Settings map[string]interface{} `json:"settings"`
HTTPJson models.HTTP `json:"http"`
AuthJson models.Auth `json:"auth"`
Status string `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
IsDefault bool `json:"is_default"`
}

409
datasource/es/es.go Normal file
View File

@@ -0,0 +1,409 @@
package es
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"reflect"
"sort"
"strings"
"time"
"github.com/ccfos/nightingale/v6/datasource"
"github.com/ccfos/nightingale/v6/datasource/commons/eslike"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/tlsx"
"github.com/mitchellh/mapstructure"
"github.com/olivere/elastic/v7"
"github.com/toolkits/pkg/logger"
)
const (
ESType = "elasticsearch"
)
type Elasticsearch struct {
Addr string `json:"es.addr" mapstructure:"es.addr"`
Nodes []string `json:"es.nodes" mapstructure:"es.nodes"`
Timeout int64 `json:"es.timeout" mapstructure:"es.timeout"` // millis
Basic BasicAuth `json:"es.basic" mapstructure:"es.basic"`
TLS TLS `json:"es.tls" mapstructure:"es.tls"`
Version string `json:"es.version" mapstructure:"es.version"`
Headers map[string]string `json:"es.headers" mapstructure:"es.headers"`
MinInterval int `json:"es.min_interval" mapstructure:"es.min_interval"` // seconds
MaxShard int `json:"es.max_shard" mapstructure:"es.max_shard"`
ClusterName string `json:"es.cluster_name" mapstructure:"es.cluster_name"`
EnableWrite bool `json:"es.enable_write" mapstructure:"es.enable_write"` // 允许写操作
Client *elastic.Client `json:"es.client" mapstructure:"es.client"`
}
type TLS struct {
SkipTlsVerify bool `json:"es.tls.skip_tls_verify" mapstructure:"es.tls.skip_tls_verify"`
}
type BasicAuth struct {
Enable bool `json:"es.auth.enable" mapstructure:"es.auth.enable"`
Username string `json:"es.user" mapstructure:"es.user"`
Password string `json:"es.password" mapstructure:"es.password"`
}
func init() {
datasource.RegisterDatasource(ESType, new(Elasticsearch))
}
func (e *Elasticsearch) Init(settings map[string]interface{}) (datasource.Datasource, error) {
newest := new(Elasticsearch)
err := mapstructure.Decode(settings, newest)
return newest, err
}
func (e *Elasticsearch) InitClient() error {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: time.Duration(e.Timeout) * time.Millisecond,
}).DialContext,
ResponseHeaderTimeout: time.Duration(e.Timeout) * time.Millisecond,
}
if len(e.Nodes) > 0 {
e.Addr = e.Nodes[0]
}
if strings.Contains(e.Addr, "https") {
tlsConfig := tlsx.ClientConfig{
InsecureSkipVerify: e.TLS.SkipTlsVerify,
UseTLS: true,
}
cfg, err := tlsConfig.TLSConfig()
if err != nil {
return err
}
transport.TLSClientConfig = cfg
}
var err error
options := []elastic.ClientOptionFunc{
elastic.SetURL(e.Nodes...),
}
if e.Basic.Username != "" {
options = append(options, elastic.SetBasicAuth(e.Basic.Username, e.Basic.Password))
}
headers := http.Header{}
for k, v := range e.Headers {
headers[k] = []string{v}
}
options = append(options, elastic.SetHeaders(headers))
options = append(options, elastic.SetHttpClient(&http.Client{Transport: transport}))
options = append(options, elastic.SetSniff(false))
options = append(options, elastic.SetHealthcheck(false))
e.Client, err = elastic.NewClient(options...)
return err
}
func (e *Elasticsearch) Equal(other datasource.Datasource) bool {
sort.Strings(e.Nodes)
sort.Strings(other.(*Elasticsearch).Nodes)
if strings.Join(e.Nodes, ",") != strings.Join(other.(*Elasticsearch).Nodes, ",") {
return false
}
if e.Basic.Username != other.(*Elasticsearch).Basic.Username {
return false
}
if e.Basic.Password != other.(*Elasticsearch).Basic.Password {
return false
}
if e.TLS.SkipTlsVerify != other.(*Elasticsearch).TLS.SkipTlsVerify {
return false
}
if e.EnableWrite != other.(*Elasticsearch).EnableWrite {
return false
}
if !reflect.DeepEqual(e.Headers, other.(*Elasticsearch).Headers) {
return false
}
return true
}
func (e *Elasticsearch) Validate(ctx context.Context) (err error) {
if len(e.Nodes) == 0 {
return fmt.Errorf("need a valid addr")
}
for _, addr := range e.Nodes {
_, err = url.Parse(addr)
if err != nil {
return fmt.Errorf("parse addr error: %v", err)
}
}
if e.Basic.Enable && (len(e.Basic.Username) == 0 || len(e.Basic.Password) == 0) {
return fmt.Errorf("need a valid user, password")
}
if e.MaxShard == 0 {
e.MaxShard = 5
}
if e.MinInterval < 10 {
e.MinInterval = 10
}
if e.Timeout == 0 {
e.Timeout = 60000
}
if !strings.HasPrefix(e.Version, "6") && !strings.HasPrefix(e.Version, "7") {
return fmt.Errorf("version must be 6.0+ or 7.0+")
}
return nil
}
func (e *Elasticsearch) MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return eslike.MakeLogQuery(ctx, query, eventTags, start, end)
}
func (e *Elasticsearch) MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return eslike.MakeTSQuery(ctx, query, eventTags, start, end)
}
func (e *Elasticsearch) QueryData(ctx context.Context, queryParam interface{}) ([]models.DataResp, error) {
search := func(ctx context.Context, indices []string, source interface{}, timeout int, maxShard int) (*elastic.SearchResult, error) {
return e.Client.Search().
Index(indices...).
IgnoreUnavailable(true).
Source(source).
Timeout(fmt.Sprintf("%ds", timeout)).
MaxConcurrentShardRequests(maxShard).
Do(ctx)
}
return eslike.QueryData(ctx, queryParam, e.Timeout, e.Version, search)
}
func (e *Elasticsearch) QueryIndices() ([]string, error) {
result, err := e.Client.IndexNames()
return result, err
}
func (e *Elasticsearch) QueryFields(indexs []string) ([]string, error) {
var fields []string
result, err := elastic.NewGetFieldMappingService(e.Client).Index(indexs...).IgnoreUnavailable(true).Do(context.Background())
if err != nil {
return fields, err
}
fieldMap := make(map[string]struct{})
for _, indexMap := range result {
if m, exists := indexMap.(map[string]interface{})["mappings"]; exists {
for k, v := range m.(map[string]interface{}) {
// 兼容 es6 版本
if k == "doc" && strings.HasPrefix(e.Version, "6") {
// if k == "doc" {
for kk, vv := range v.(map[string]interface{}) {
typ := getFieldType(kk, vv.(map[string]interface{}))
if eslike.HitFilter(typ) {
continue
}
if _, exsits := fieldMap[kk]; !exsits {
fieldMap[kk] = struct{}{}
fields = append(fields, kk)
}
}
} else {
// es7 版本
typ := getFieldType(k, v.(map[string]interface{}))
if eslike.HitFilter(typ) {
continue
}
if _, exsits := fieldMap[k]; !exsits {
fieldMap[k] = struct{}{}
fields = append(fields, k)
}
}
}
}
}
sort.Strings(fields)
return fields, nil
}
func (e *Elasticsearch) QueryLog(ctx context.Context, queryParam interface{}) ([]interface{}, int64, error) {
search := func(ctx context.Context, indices []string, source interface{}, timeout int, maxShard int) (*elastic.SearchResult, error) {
// 应该是之前为了获取 fields 字段,做的这个兼容
// fields, err := e.QueryFields(indices)
// if err != nil {
// logger.Warningf("query data error:%v", err)
// return nil, err
// }
// if source != nil && strings.HasPrefix(e.Version, "7") {
// source = source.(*elastic.SearchSource).DocvalueFields(fields...)
// }
return e.Client.Search().
Index(indices...).
IgnoreUnavailable(true).
MaxConcurrentShardRequests(maxShard).
Source(source).
Timeout(fmt.Sprintf("%ds", timeout)).
Do(ctx)
}
return eslike.QueryLog(ctx, queryParam, e.Timeout, e.Version, e.MaxShard, search)
}
func (e *Elasticsearch) QueryFieldValue(indexs []string, field string, query string) ([]string, error) {
var values []string
search := e.Client.Search().
IgnoreUnavailable(true).
Index(indexs...).
Size(0)
if query != "" {
search = search.Query(elastic.NewBoolQuery().Must(elastic.NewQueryStringQuery(query)))
}
search = search.Aggregation("distinct", elastic.NewTermsAggregation().Field(field).Size(10000))
result, err := search.Do(context.Background())
if err != nil {
return values, err
}
agg, found := result.Aggregations.Terms("distinct")
if !found {
return values, nil
}
for _, bucket := range agg.Buckets {
values = append(values, bucket.Key.(string))
}
return values, nil
}
func (e *Elasticsearch) Test(ctx context.Context) (err error) {
err = e.Validate(ctx)
if err != nil {
return err
}
if e.Addr == "" {
return fmt.Errorf("addr is invalid")
}
if e.Version == "7.10+" {
options := []elastic.ClientOptionFunc{
elastic.SetURL(e.Addr),
}
if e.Basic.Enable {
options = append(options, elastic.SetBasicAuth(e.Basic.Username, e.Basic.Password))
}
client, err := elastic.NewClient(options...)
if err != nil {
return fmt.Errorf("config is invalid:%v", err)
}
_, err = client.ElasticsearchVersion(e.Addr)
if err != nil {
return fmt.Errorf("config is invalid:%v", err)
}
} else {
return fmt.Errorf("version must be 7.10+")
}
return nil
}
func getFieldType(key string, m map[string]interface{}) string {
if innerMap, exists := m["mapping"]; exists {
if innerM, exists := innerMap.(map[string]interface{})[key]; exists {
if typ, exists := innerM.(map[string]interface{})["type"]; exists {
return typ.(string)
}
} else {
arr := strings.Split(key, ".")
if innerM, exists := innerMap.(map[string]interface{})[arr[len(arr)-1]]; exists {
if typ, exists := innerM.(map[string]interface{})["type"]; exists {
return typ.(string)
}
}
}
}
return ""
}
func (e *Elasticsearch) QueryMapData(ctx context.Context, query interface{}) ([]map[string]string, error) {
search := func(ctx context.Context, indices []string, source interface{}, timeout int, maxShard int) (*elastic.SearchResult, error) {
return e.Client.Search().
Index(indices...).
IgnoreUnavailable(true).
Source(source).
Timeout(fmt.Sprintf("%ds", timeout)).
Do(ctx)
}
param := new(eslike.Query)
if err := mapstructure.Decode(query, param); err != nil {
return nil, err
}
// 扩大查询范围, 解决上一次查询消耗时间太多,导致本次查询时间范围起止时间,滞后问题
param.Interval += 30
res, _, err := eslike.QueryLog(ctx, param, e.Timeout, e.Version, e.MaxShard, search)
if err != nil {
return nil, err
}
var result []map[string]string
for _, item := range res {
logger.Debugf("query:%v item:%v", query, item)
if itemMap, ok := item.(*elastic.SearchHit); ok {
mItem := make(map[string]string)
// 遍历 fields 字段的每个键值对
sourceMap := make(map[string]interface{})
err := json.Unmarshal(itemMap.Source, &sourceMap)
if err != nil {
logger.Warningf("unmarshal source%s error:%v", string(itemMap.Source), err)
continue
}
for k, v := range sourceMap {
mItem[k] = fmt.Sprintf("%v", v)
}
// 将处理好的 map 添加到 m 切片中
result = append(result, mItem)
// 只取第一条数据
break
}
}
return result, nil
}

15
datasource/prom/prom.go Normal file
View File

@@ -0,0 +1,15 @@
package prom
type Prometheus struct {
PrometheusAddr string `json:"prometheus.addr"`
PrometheusBasic struct {
PrometheusUser string `json:"prometheus.user"`
PrometheusPass string `json:"prometheus.password"`
} `json:"prometheus.basic"`
Headers map[string]string `json:"prometheus.headers"`
PrometheusTimeout int64 `json:"prometheus.timeout"`
ClusterName string `json:"prometheus.cluster_name"`
WriteAddr string `json:"prometheus.write_addr"`
TsdbType string `json:"prometheus.tsdb_type"`
InternalAddr string `json:"prometheus.internal_addr"`
}

View File

@@ -1,24 +1,32 @@
package tdengine
import (
"encoding/base64"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/prometheus/common/model"
"github.com/toolkits/pkg/logger"
"github.com/ccfos/nightingale/v6/datasource"
td "github.com/ccfos/nightingale/v6/dskit/tdengine"
"github.com/ccfos/nightingale/v6/models"
"github.com/mitchellh/mapstructure"
)
const (
TDEngineType = "tdengine"
)
type TDengine struct {
td.Tdengine `json:",inline" mapstructure:",squash"`
}
type TdengineQuery struct {
From string `json:"from"`
Interval int64 `json:"interval"`
@@ -34,162 +42,94 @@ type Keys struct {
TimeFormat string `json:"timeFormat"`
}
type TdengineClientMap struct {
sync.RWMutex
ctx *ctx.Context
heartbeat aconf.HeartbeatConfig
ReaderClients map[int64]*tdengineClient
func init() {
datasource.RegisterDatasource(TDEngineType, new(TDengine))
}
func (pc *TdengineClientMap) Set(datasourceId int64, r *tdengineClient) {
if r == nil {
return
}
pc.Lock()
defer pc.Unlock()
pc.ReaderClients[datasourceId] = r
func (td *TDengine) Init(settings map[string]interface{}) (datasource.Datasource, error) {
newest := new(TDengine)
err := mapstructure.Decode(settings, newest)
return newest, err
}
func (pc *TdengineClientMap) GetDatasourceIds() []int64 {
pc.RLock()
defer pc.RUnlock()
var datasourceIds []int64
for k := range pc.ReaderClients {
datasourceIds = append(datasourceIds, k)
func (td *TDengine) InitClient() error {
td.InitCli()
return nil
}
func (td *TDengine) Equal(other datasource.Datasource) bool {
otherTD, ok := other.(*TDengine)
if !ok {
return false
}
return datasourceIds
}
func (pc *TdengineClientMap) GetCli(datasourceId int64) *tdengineClient {
pc.RLock()
defer pc.RUnlock()
c := pc.ReaderClients[datasourceId]
return c
}
func (pc *TdengineClientMap) IsNil(datasourceId int64) bool {
pc.RLock()
defer pc.RUnlock()
c, exists := pc.ReaderClients[datasourceId]
if !exists {
return true
if td.Addr != otherTD.Addr {
return false
}
return c == nil
}
func (pc *TdengineClientMap) Reset() {
pc.Lock()
defer pc.Unlock()
pc.ReaderClients = make(map[int64]*tdengineClient)
}
func (pc *TdengineClientMap) Del(datasourceId int64) {
pc.Lock()
defer pc.Unlock()
delete(pc.ReaderClients, datasourceId)
}
type tdengineClient struct {
url string
client *http.Client
header map[string][]string
}
func newTdengine(po TdengineOption) *tdengineClient {
tc := &tdengineClient{
url: po.Url,
}
tc.client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: true,
},
}
tc.header = map[string][]string{
"Connection": {"keep-alive"},
}
for _, v := range po.Headers {
kv := strings.Split(v, ":")
if len(kv) != 2 {
continue
if td.Basic != nil && otherTD.Basic != nil {
if td.Basic.User != otherTD.Basic.User {
return false
}
tc.header[kv[0]] = []string{kv[1]}
}
if po.BasicAuthUser != "" {
basic := base64.StdEncoding.EncodeToString([]byte(po.BasicAuthUser + ":" + po.BasicAuthPass))
tc.header["Authorization"] = []string{fmt.Sprintf("Basic %s", basic)}
}
return tc
}
type APIResponse struct {
Code int `json:"code"`
ColumnMeta [][]interface{} `json:"column_meta"`
Data [][]interface{} `json:"data"`
Rows int `json:"rows"`
}
func (tc *tdengineClient) QueryTable(query string) (APIResponse, error) {
var apiResp APIResponse
req, err := http.NewRequest("POST", tc.url+"/rest/sql", strings.NewReader(query))
if err != nil {
return apiResp, err
}
for k, v := range tc.header {
req.Header[k] = v
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := tc.client.Do(req)
if err != nil {
return apiResp, err
}
defer resp.Body.Close()
// 限制响应体大小为10MB
maxSize := int64(10 * 1024 * 1024) // 10MB
limitedReader := http.MaxBytesReader(nil, resp.Body, maxSize)
if resp.StatusCode != http.StatusOK {
return apiResp, fmt.Errorf("HTTP error, status: %s", resp.Status)
}
err = json.NewDecoder(limitedReader).Decode(&apiResp)
if err != nil {
if strings.Contains(err.Error(), "http: request body too large") {
return apiResp, fmt.Errorf("response body exceeds 10MB limit")
if td.Basic.Password != otherTD.Basic.Password {
return false
}
return apiResp, err
}
return apiResp, nil
if td.Token != otherTD.Token {
return false
}
if td.Timeout != otherTD.Timeout {
return false
}
if td.DialTimeout != otherTD.DialTimeout {
return false
}
if td.MaxIdleConnsPerHost != otherTD.MaxIdleConnsPerHost {
return false
}
if len(td.Headers) != len(otherTD.Headers) {
return false
}
for k, v := range td.Headers {
if otherV, ok := otherTD.Headers[k]; !ok || v != otherV {
return false
}
}
return true
}
func (tc *tdengineClient) QueryLog(query interface{}) (APIResponse, error) {
b, err := json.Marshal(query)
func (td *TDengine) Validate(ctx context.Context) (err error) {
return nil
}
func (td *TDengine) MakeLogQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return nil, nil
}
func (td *TDengine) MakeTSQuery(ctx context.Context, query interface{}, eventTags []string, start, end int64) (interface{}, error) {
return nil, nil
}
func (td *TDengine) QueryData(ctx context.Context, queryParam interface{}) ([]models.DataResp, error) {
return td.Query(queryParam, 0)
}
func (td *TDengine) QueryLog(ctx context.Context, queryParam interface{}) ([]interface{}, int64, error) {
b, err := json.Marshal(queryParam)
if err != nil {
return APIResponse{}, err
return nil, 0, err
}
var q TdengineQuery
err = json.Unmarshal(b, &q)
if err != nil {
return APIResponse{}, err
return nil, 0, err
}
if q.Interval == 0 {
@@ -218,135 +158,19 @@ func (tc *tdengineClient) QueryLog(query interface{}) (APIResponse, error) {
q.Query = q.Query + " limit 200"
}
data, err := tc.QueryTable(q.Query)
data, err := td.QueryTable(q.Query)
if err != nil {
return data, err
return nil, 0, err
}
return TimeFormat(data, q.Keys.TimeFormat), err
return ConvertToTable(data), int64(len(data.Data)), nil
}
func TimeFormat(src APIResponse, timeFormat string) APIResponse {
if timeFormat == "" {
return src
}
tsIdx := -1
for colIndex, colData := range src.ColumnMeta {
// 类型参考 https://docs.taosdata.com/taos-sql/data-type/
// 处理v2版本数字类型和v3版本字符串类型
switch t := colData[1].(type) {
case float64:
// v2版本数字类型映射
if int(t) == 9 { // TIMESTAMP type in v2
tsIdx = colIndex
break
}
case string:
// v3版本直接使用字符串类型
if t == "TIMESTAMP" {
tsIdx = colIndex
break
}
default:
logger.Warningf("unexpected column type: %v", colData[1])
continue
}
}
if tsIdx == -1 {
return src
}
for i := range src.Data {
var t time.Time
var err error
switch tsVal := src.Data[i][tsIdx].(type) {
case string:
// 尝试解析不同格式的时间字符串
t, err = parseTimeString(tsVal)
if err != nil {
logger.Warningf("parse timestamp string failed: %v, value: %v", err, tsVal)
continue
}
default:
logger.Warningf("unexpected timestamp type: %T, value: %v", tsVal, tsVal)
continue
}
src.Data[i][tsIdx] = t.In(time.Local).Format(timeFormat)
}
return src
func (td *TDengine) QueryMapData(ctx context.Context, query interface{}) ([]map[string]string, error) {
return nil, nil
}
func parseTimeString(ts string) (time.Time, error) {
// 尝试不同的时间格式
formats := []string{
// 标准格式
time.Layout, // "01/02 03:04:05PM '06 -0700"
time.ANSIC, // "Mon Jan _2 15:04:05 2006"
time.UnixDate, // "Mon Jan _2 15:04:05 MST 2006"
time.RubyDate, // "Mon Jan 02 15:04:05 -0700 2006"
time.RFC822, // "02 Jan 06 15:04 MST"
time.RFC822Z, // "02 Jan 06 15:04 -0700"
time.RFC850, // "Monday, 02-Jan-06 15:04:05 MST"
time.RFC1123, // "Mon, 02 Jan 2006 15:04:05 MST"
time.RFC1123Z, // "Mon, 02 Jan 2006 15:04:05 -0700"
time.RFC3339, // "2006-01-02T15:04:05Z07:00"
time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00"
time.Kitchen, // "3:04PM"
// 实用时间戳格式
time.Stamp, // "Jan _2 15:04:05"
time.StampMilli, // "Jan _2 15:04:05.000"
time.StampMicro, // "Jan _2 15:04:05.000000"
time.StampNano, // "Jan _2 15:04:05.000000000"
time.DateTime, // "2006-01-02 15:04:05"
time.DateOnly, // "2006-01-02"
time.TimeOnly, // "15:04:05"
// 常用自定义格式
"2006-01-02T15:04:05", // 无时区的ISO格式
"2006-01-02T15:04:05.000Z",
"2006-01-02T15:04:05Z",
"2006-01-02 15:04:05.999999999", // 纳秒
"2006-01-02 15:04:05.999999", // 微秒
"2006-01-02 15:04:05.999", // 毫秒
"2006/01/02",
"20060102",
"01/02/2006",
"2006年01月02日",
"2006年01月02日 15:04:05",
}
var lastErr error
for _, format := range formats {
t, err := time.Parse(format, ts)
if err == nil {
return t, nil
}
lastErr = err
}
// 尝试解析 Unix 时间戳
if timestamp, err := strconv.ParseInt(ts, 10, 64); err == nil {
switch len(ts) {
case 10: // 秒
return time.Unix(timestamp, 0), nil
case 13: // 毫秒
return time.Unix(timestamp/1000, (timestamp%1000)*1000000), nil
case 16: // 微秒
return time.Unix(timestamp/1000000, (timestamp%1000000)*1000), nil
case 19: // 纳秒
return time.Unix(timestamp/1000000000, timestamp%1000000000), nil
}
}
return time.Time{}, fmt.Errorf("failed to parse time with any format: %v", lastErr)
}
func (tc *tdengineClient) Query(query interface{}, delay ...int) ([]models.DataResp, error) {
func (td *TDengine) Query(query interface{}, delay ...int) ([]models.DataResp, error) {
b, err := json.Marshal(query)
if err != nil {
return nil, err
@@ -384,7 +208,7 @@ func (tc *tdengineClient) Query(query interface{}, delay ...int) ([]models.DataR
q.Query = strings.ReplaceAll(q.Query, key, val)
}
data, err := tc.QueryTable(q.Query)
data, err := td.QueryTable(q.Query)
if err != nil {
return nil, err
}
@@ -393,154 +217,7 @@ func (tc *tdengineClient) Query(query interface{}, delay ...int) ([]models.DataR
return ConvertToTStData(data, q.Keys, q.Ref)
}
// get tdendgine databases
func (tc *tdengineClient) GetDatabases() ([]string, error) {
var databases []string
data, err := tc.QueryTable("show databases")
if err != nil {
return databases, err
}
for _, row := range data.Data {
databases = append(databases, row[0].(string))
}
return databases, nil
}
// get tdendgine tables by database
func (tc *tdengineClient) GetTables(database string, isStable bool) ([]string, error) {
var tables []string
sql := fmt.Sprintf("show %s.tables", database)
if isStable {
sql = fmt.Sprintf("show %s.stables", database)
}
data, err := tc.QueryTable(sql)
if err != nil {
return tables, err
}
for _, row := range data.Data {
tables = append(tables, row[0].(string))
}
return tables, nil
}
type Column struct {
Name string `json:"name"`
Type string `json:"type"`
Size int `json:"size"`
}
func (tc *tdengineClient) GetColumns(database, table string) ([]Column, error) {
var columns []Column
sql := fmt.Sprintf("select * from %s.%s limit 1", database, table)
data, err := tc.QueryTable(sql)
if err != nil {
return columns, err
}
for _, row := range data.ColumnMeta {
var colType string
switch t := row[1].(type) {
case float64:
// v2版本数字类型映射
switch int(t) {
case 1:
colType = "BOOL"
case 2:
colType = "TINYINT"
case 3:
colType = "SMALLINT"
case 4:
colType = "INT"
case 5:
colType = "BIGINT"
case 6:
colType = "FLOAT"
case 7:
colType = "DOUBLE"
case 8:
colType = "BINARY"
case 9:
colType = "TIMESTAMP"
case 10:
colType = "NCHAR"
default:
colType = "UNKNOWN"
}
case string:
// v3版本直接使用字符串类型
colType = t
default:
logger.Warningf("unexpected column type format: %v", row[1])
colType = "UNKNOWN"
}
column := Column{
Name: row[0].(string),
Type: colType,
Size: int(row[2].(float64)),
}
columns = append(columns, column)
}
return columns, nil
}
// {
// "code": 0,
// "column_meta": [
// ["ts", "TIMESTAMP", 8],
// ["count", "BIGINT", 8],
// ["endpoint", "VARCHAR", 45],
// ["status_code", "INT", 4],
// ["client_ip", "VARCHAR", 40],
// ["request_method", "VARCHAR", 15],
// ["request_uri", "VARCHAR", 128]
// ],
// "data": [
// [
// "2022-06-29T05:50:55.401Z",
// 2,
// "LAPTOP-NNKFTLTG:6041",
// 200,
// "172.23.208.1",
// "POST",
// "/rest/sql"
// ],
// [
// "2022-06-29T05:52:16.603Z",
// 1,
// "LAPTOP-NNKFTLTG:6041",
// 200,
// "172.23.208.1",
// "POST",
// "/rest/sql"
// ]
// ],
// "rows": 2
// }
// {
// "dat": [
// {
// "ref": "",
// "metric": {
// "__name__": "count",
// "host":"host1"
// },
// "values": [
// [
// 1693219500,
// 12
// ]
// ]
// }
// ],
// "err": ""
// }
func ConvertToTStData(src APIResponse, key Keys, ref string) ([]models.DataResp, error) {
func ConvertToTStData(src td.APIResponse, key Keys, ref string) ([]models.DataResp, error) {
metricIdxMap := make(map[string]int)
labelIdxMap := make(map[string]int)
@@ -702,3 +379,82 @@ func interfaceToFloat64(input interface{}) (float64, error) {
return 0, fmt.Errorf("unsupported type: %T", input)
}
}
func parseTimeString(ts string) (time.Time, error) {
// 尝试不同的时间格式
formats := []string{
// 标准格式
time.Layout, // "01/02 03:04:05PM '06 -0700"
time.ANSIC, // "Mon Jan _2 15:04:05 2006"
time.UnixDate, // "Mon Jan _2 15:04:05 MST 2006"
time.RubyDate, // "Mon Jan 02 15:04:05 -0700 2006"
time.RFC822, // "02 Jan 06 15:04 MST"
time.RFC822Z, // "02 Jan 06 15:04 -0700"
time.RFC850, // "Monday, 02-Jan-06 15:04:05 MST"
time.RFC1123, // "Mon, 02 Jan 2006 15:04:05 MST"
time.RFC1123Z, // "Mon, 02 Jan 2006 15:04:05 -0700"
time.RFC3339, // "2006-01-02T15:04:05Z07:00"
time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00"
time.Kitchen, // "3:04PM"
// 实用时间戳格式
time.Stamp, // "Jan _2 15:04:05"
time.StampMilli, // "Jan _2 15:04:05.000"
time.StampMicro, // "Jan _2 15:04:05.000000"
time.StampNano, // "Jan _2 15:04:05.000000000"
time.DateTime, // "2006-01-02 15:04:05"
time.DateOnly, // "2006-01-02"
time.TimeOnly, // "15:04:05"
// 常用自定义格式
"2006-01-02T15:04:05", // 无时区的ISO格式
"2006-01-02T15:04:05.000Z",
"2006-01-02T15:04:05Z",
"2006-01-02 15:04:05.999999999", // 纳秒
"2006-01-02 15:04:05.999999", // 微秒
"2006-01-02 15:04:05.999", // 毫秒
"2006/01/02",
"20060102",
"01/02/2006",
"2006年01月02日",
"2006年01月02日 15:04:05",
}
var lastErr error
for _, format := range formats {
t, err := time.Parse(format, ts)
if err == nil {
return t, nil
}
lastErr = err
}
// 尝试解析 Unix 时间戳
if timestamp, err := strconv.ParseInt(ts, 10, 64); err == nil {
switch len(ts) {
case 10: // 秒
return time.Unix(timestamp, 0), nil
case 13: // 毫秒
return time.Unix(timestamp/1000, (timestamp%1000)*1000000), nil
case 16: // 微秒
return time.Unix(timestamp/1000000, (timestamp%1000000)*1000), nil
case 19: // 纳秒
return time.Unix(timestamp/1000000000, timestamp%1000000000), nil
}
}
return time.Time{}, fmt.Errorf("failed to parse time with any format: %v", lastErr)
}
func ConvertToTable(src td.APIResponse) []interface{} {
var resp []interface{}
for i := range src.Data {
cur := make(map[string]interface{})
for j := range src.Data[i] {
cur[src.ColumnMeta[j][0].(string)] = src.Data[i][j]
}
resp = append(resp, cur)
}
return resp
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

View File

@@ -5,7 +5,7 @@ RunMode = "release"
# log write dir
Dir = "logs"
# log level: DEBUG INFO WARNING ERROR
Level = "DEBUG"
Level = "INFO"
# stdout, stderr, file
Output = "stdout"
# # rotate by time
@@ -61,6 +61,10 @@ AccessExpired = 1500
RefreshExpired = 10080
RedisKeyPrefix = "/jwt/"
[HTTP.TokenAuth]
Enable = false
HeaderUserTokenKey = "X-User-Token"
[HTTP.ProxyAuth]
# if proxy auth enabled, jwt auth is disabled
Enable = false
@@ -71,15 +75,6 @@ DefaultRoles = ["Standard"]
[HTTP.RSA]
# open RSA
OpenRSA = false
# Before replacing the key file, make sure that there are no encrypted variables in the database "configs".
# It is recommended to decrypt and remove all encrypted values from the database before replacing the key file.
# This will prevent any potential issues with accessing or decrypting the variables using the new key file.
# RSA public key (auto carete)
RSAPublicKeyPath = "etc/rsa/public.pem"
# RSA private key (auto carete)
RSAPrivateKeyPath = "etc/rsa/private.pem"
# RSA private key password
RSAPassWord = "n9e@n9e!"
[DB]
# postgres: DSN="host=127.0.0.1 port=5432 user=root dbname=n9e_v6 password=1234 sslmode=disable"
@@ -134,8 +129,6 @@ AlertDetail = false
[Pushgw]
# use target labels in database instead of in series
LabelRewrite = true
# # default busigroup key name
# BusiGroupLabelKey = "busigroup"
ForceUseServerTS = true
# [Pushgw.DebugSample]

View File

@@ -5,7 +5,7 @@ RunMode = "release"
# log write dir
Dir = "logs"
# log level: DEBUG INFO WARNING ERROR
Level = "DEBUG"
Level = "INFO"
# stdout, stderr, file
Output = "file"
# # rotate by time
@@ -71,15 +71,6 @@ DefaultRoles = ["Standard"]
[HTTP.RSA]
# open RSA
OpenRSA = false
# Before replacing the key file, make sure that there are no encrypted variables in the database "configs".
# It is recommended to decrypt and remove all encrypted values from the database before replacing the key file.
# This will prevent any potential issues with accessing or decrypting the variables using the new key file.
# RSA public key (auto carete)
RSAPublicKeyPath = "etc/rsa/public.pem"
# RSA private key (auto carete)
RSAPrivateKeyPath = "etc/rsa/private.pem"
# RSA private key password
RSAPassWord = "n9e@n9e!"
[DB]
# postgres: host=%s port=%s user=%s dbname=%s password=%s sslmode=%s
@@ -135,8 +126,6 @@ AlertDetail = true
[Pushgw]
# use target labels in database instead of in series
LabelRewrite = true
# # default busigroup key name
# BusiGroupLabelKey = "busigroup"
ForceUseServerTS = true
# [Pushgw.DebugSample]

View File

@@ -5,7 +5,7 @@ RunMode = "release"
# log write dir
Dir = "logs"
# log level: DEBUG INFO WARNING ERROR
Level = "DEBUG"
Level = "INFO"
# stdout, stderr, file
Output = "stdout"
# # rotate by time
@@ -71,15 +71,6 @@ DefaultRoles = ["Standard"]
[HTTP.RSA]
# open RSA
OpenRSA = false
# Before replacing the key file, make sure that there are no encrypted variables in the database "configs".
# It is recommended to decrypt and remove all encrypted values from the database before replacing the key file.
# This will prevent any potential issues with accessing or decrypting the variables using the new key file.
# RSA public key (auto carete)
RSAPublicKeyPath = "etc/rsa/public.pem"
# RSA private key (auto carete)
RSAPrivateKeyPath = "etc/rsa/private.pem"
# RSA private key password
RSAPassWord = "n9e@n9e!"
[DB]
# postgres: host=%s port=%s user=%s dbname=%s password=%s sslmode=%s
@@ -135,8 +126,6 @@ AlertDetail = true
[Pushgw]
# use target labels in database instead of in series
LabelRewrite = true
# # default busigroup key name
# BusiGroupLabelKey = "busigroup"
ForceUseServerTS = true
# [Pushgw.DebugSample]

View File

@@ -790,6 +790,7 @@ CREATE TABLE es_index_pattern (
time_field varchar(128) not null default '@timestamp',
allow_hide_system_indices smallint not null default 0,
fields_format varchar(4096) not null default '',
cross_cluster_enabled int not null default 0,
create_at bigint default '0',
create_by varchar(64) default '',
update_at bigint default '0',
@@ -859,6 +860,7 @@ CREATE TABLE builtin_components (
ident VARCHAR(191) NOT NULL,
logo VARCHAR(191) NOT NULL,
readme TEXT NOT NULL,
disabled INT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
created_by VARCHAR(191) NOT NULL DEFAULT '',
updated_at BIGINT NOT NULL DEFAULT 0,
@@ -885,4 +887,20 @@ CREATE TABLE builtin_payloads (
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);
CREATE INDEX idx_type ON builtin_payloads (type);
CREATE TABLE dash_annotation (
id bigserial PRIMARY KEY,
dashboard_id bigint not null,
panel_id varchar(191) not null,
tags text,
description text,
config text,
time_start bigint not null default 0,
time_end 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 ''
);

View File

@@ -5,7 +5,7 @@ RunMode = "release"
# log write dir
Dir = "logs"
# log level: DEBUG INFO WARNING ERROR
Level = "DEBUG"
Level = "INFO"
# stdout, stderr, file
Output = "stdout"
# # rotate by time
@@ -130,8 +130,6 @@ AlertDetail = true
[Pushgw]
# use target labels in database instead of in series
LabelRewrite = true
# # default busigroup key name
# BusiGroupLabelKey = "busigroup"
ForceUseServerTS = true
# [Pushgw.DebugSample]

View File

@@ -1,25 +0,0 @@
#### {{if .IsRecovered}}<font color="#008800">💚{{.RuleName}}</font>{{else}}<font color="#FF0000">💔{{.RuleName}}</font>{{end}}
---
{{$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}}

View File

@@ -1,224 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>夜莺告警通知</title>
<style type="text/css">
.wrapper {
background-color: #f8f8f8;
padding: 15px;
height: 100%;
}
.main {
width: 600px;
padding: 30px;
margin: 0 auto;
background-color: #fff;
font-size: 12px;
font-family: verdana,'Microsoft YaHei',Consolas,'Deja Vu Sans Mono','Bitstream Vera Sans Mono';
}
header {
border-radius: 2px 2px 0 0;
}
header .title {
font-size: 14px;
color: #333333;
margin: 0;
}
header .sub-desc {
color: #333;
font-size: 14px;
margin-top: 6px;
margin-bottom: 0;
}
hr {
margin: 20px 0;
height: 0;
border: none;
border-top: 1px solid #e5e5e5;
}
em {
font-weight: 600;
}
table {
margin: 20px 0;
width: 100%;
}
table tbody tr{
font-weight: 200;
font-size: 12px;
color: #666;
height: 32px;
}
.succ {
background-color: green;
color: #fff;
}
.fail {
background-color: red;
color: #fff;
}
.succ th, .succ td, .fail th, .fail td {
color: #fff;
}
table tbody tr th {
width: 80px;
text-align: right;
}
.text-right {
text-align: right;
}
.body {
margin-top: 24px;
}
.body-text {
color: #666666;
-webkit-font-smoothing: antialiased;
}
.body-extra {
-webkit-font-smoothing: antialiased;
}
.body-extra.text-right a {
text-decoration: none;
color: #333;
}
.body-extra.text-right a:hover {
color: #666;
}
.button {
width: 200px;
height: 50px;
margin-top: 20px;
text-align: center;
border-radius: 2px;
background: #2D77EE;
line-height: 50px;
font-size: 20px;
color: #FFFFFF;
cursor: pointer;
}
.button:hover {
background: rgb(25, 115, 255);
border-color: rgb(25, 115, 255);
color: #fff;
}
footer {
margin-top: 10px;
text-align: right;
}
.footer-logo {
text-align: right;
}
.footer-logo-image {
width: 108px;
height: 27px;
margin-right: 10px;
}
.copyright {
margin-top: 10px;
font-size: 12px;
text-align: right;
color: #999;
-webkit-font-smoothing: antialiased;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="main">
<header>
<h3 class="title">{{.RuleName}}</h3>
<p class="sub-desc"></p>
</header>
<hr>
<div class="body">
<table cellspacing="0" cellpadding="0" border="0">
<tbody>
{{if .IsRecovered}}
<tr class="succ">
<th>级别状态:</th>
<td>S{{.Severity}} Recovered</td>
</tr>
{{else}}
<tr class="fail">
<th>级别状态:</th>
<td>S{{.Severity}} Triggered</td>
</tr>
{{end}}
<tr>
<th>策略备注:</th>
<td>{{.RuleNote}}</td>
</tr>
<tr>
<th>设备备注:</th>
<td>{{.TargetNote}}</td>
</tr>
{{if not .IsRecovered}}
<tr>
<th>触发时值:</th>
<td>{{.TriggerValue}}</td>
</tr>
{{end}}
{{if .TargetIdent}}
<tr>
<th>监控对象:</th>
<td>{{.TargetIdent}}</td>
</tr>
{{end}}
<tr>
<th>监控指标:</th>
<td>{{.TagsJSON}}</td>
</tr>
{{if .IsRecovered}}
<tr>
<th>恢复时间:</th>
<td>{{timeformat .LastEvalTime}}</td>
</tr>
{{else}}
<tr>
<th>触发时间:</th>
<td>
{{timeformat .TriggerTime}}
</td>
</tr>
{{end}}
<tr>
<th>发送时间:</th>
<td>
{{timestamp}}
</td>
</tr>
<tr>
<th>PromQL</th>
<td>
{{.PromQl}}
</td>
</tr>
</tbody>
</table>
<hr>
<footer>
<div class="copyright" style="font-style: italic">
报警太多?使用 <a href="https://flashcat.cloud/product/flashduty/" target="_blank">FlashDuty</a> 做告警聚合降噪、排班OnCall
</div>
</footer>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,7 +0,0 @@
级别状态: S{{.Severity}} {{if .IsRecovered}}Recovered{{else}}Triggered{{end}}
规则名称: {{.RuleName}}{{if .RuleNote}}
规则备注: {{.RuleNote}}{{end}}
监控指标: {{.TagsJSON}}
{{if .IsRecovered}}恢复时间:{{timeformat .LastEvalTime}}{{else}}触发时间: {{timeformat .TriggerTime}}
触发时值: {{.TriggerValue}}{{end}}
发送时间: {{timestamp}}

View File

@@ -1,7 +0,0 @@
级别状态: S{{.Severity}} {{if .IsRecovered}}Recovered{{else}}Triggered{{end}}
规则名称: {{.RuleName}}{{if .RuleNote}}
规则备注: {{.RuleNote}}{{end}}
监控指标: {{.TagsJSON}}
{{if .IsRecovered}}恢复时间:{{timeformat .LastEvalTime}}{{else}}触发时间: {{timeformat .TriggerTime}}
触发时值: {{.TriggerValue}}{{end}}
发送时间: {{timestamp}}

View File

@@ -1 +0,0 @@
{{if .IsRecovered}}Recovered{{else}}Triggered{{end}}: {{.RuleName}} {{.TagsJSON}}

View File

@@ -1,7 +0,0 @@
**级别状态**: {{if .IsRecovered}}<font color="info">S{{.Severity}} Recovered</font>{{else}}<font color="warning">S{{.Severity}} Triggered</font>{{end}}
**规则标题**: {{.RuleName}}{{if .RuleNote}}
**规则备注**: {{.RuleNote}}{{end}}
**监控指标**: {{.TagsJSON}}
{{if .IsRecovered}}**恢复时间**{{timeformat .LastEvalTime}}{{else}}**触发时间**: {{timeformat .TriggerTime}}
**触发时值**: {{.TriggerValue}}{{end}}
**发送时间**: {{timestamp}}

View File

@@ -1,7 +0,0 @@
**级别状态**: {{if .IsRecovered}}<font color="info">S{{.Severity}} Recovered</font>{{else}}<font color="warning">S{{.Severity}} Triggered</font>{{end}}
**规则标题**: {{.RuleName}}{{if .RuleNote}}
**规则备注**: {{.RuleNote}}{{end}}
**监控指标**: {{.TagsJSON}}
{{if .IsRecovered}}**恢复时间**{{timeformat .LastEvalTime}}{{else}}**触发时间**: {{timeformat .TriggerTime}}
**触发时值**: {{.TriggerValue}}{{end}}
**发送时间**: {{timestamp}}

View File

@@ -291,6 +291,8 @@ CREATE TABLE `alert_rule` (
`append_tags` varchar(255) not null default '' comment 'split by space: service=n9e mod=api',
`annotations` text not null comment 'annotations',
`extra_config` text,
`notify_rule_ids` varchar(1024) DEFAULT '',
`notify_version` int DEFAULT 0,
`create_at` bigint not null default 0,
`create_by` varchar(64) not null default '',
`update_at` bigint not null default 0,
@@ -351,6 +353,8 @@ CREATE TABLE `alert_subscribe` (
`extra_config` text,
`redefine_webhooks` tinyint(1) default 0,
`for_duration` bigint not null default 0,
`notify_rule_ids` varchar(1024) DEFAULT '',
`notify_version` int DEFAULT 0,
`create_at` bigint not null default 0,
`create_by` varchar(64) not null default '',
`update_at` bigint not null default 0,
@@ -531,11 +535,11 @@ CREATE TABLE `builtin_components` (
`created_by` varchar(191) NOT NULL DEFAULT '' COMMENT '''creator''',
`updated_at` bigint NOT NULL DEFAULT 0 COMMENT '''update time''',
`updated_by` varchar(191) NOT NULL DEFAULT '' COMMENT '''updater''',
`disabled` int NOT NULL DEFAULT 0 COMMENT '''is disabled or not''',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_ident` (`ident`) USING BTREE
KEY (`ident`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `builtin_payloads` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '''unique identifier''',
`component_id` bigint NOT NULL DEFAULT 0 COMMENT '''component_id of payload''',
@@ -560,6 +564,7 @@ CREATE TABLE `builtin_payloads` (
CREATE TABLE notification_record (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`notify_rule_id` BIGINT NOT NULL DEFAULT 0,
`event_id` bigint NOT NULL COMMENT 'event history id',
`sub_id` bigint COMMENT 'subscribed rule id',
`channel` varchar(255) NOT NULL COMMENT 'notification channel name',
@@ -631,13 +636,16 @@ CREATE TABLE `alerting_engines`
`datasource_id` bigint not null default 0 comment 'datasource id',
`engine_cluster` varchar(128) not null default '' comment 'n9e-alert cluster',
`clock` bigint not null,
PRIMARY KEY (`id`)
PRIMARY KEY (`id`),
INDEX `idx_inst` (`instance`),
INDEX `idx_clock` (`clock`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `datasource`
(
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) not null default '',
`identifier` varchar(255) not null default '',
`description` varchar(255) not null default '',
`category` varchar(255) not null default '',
`plugin_id` int unsigned not null default 0,
@@ -693,6 +701,8 @@ CREATE TABLE `es_index_pattern` (
`time_field` varchar(128) not null default '@timestamp',
`allow_hide_system_indices` tinyint(1) not null default 0,
`fields_format` varchar(4096) not null default '',
`cross_cluster_enabled` int not null default 0,
`note` varchar(1024) not null default '',
`create_at` bigint default '0',
`create_by` varchar(64) default '',
`update_at` bigint default '0',
@@ -701,6 +711,7 @@ CREATE TABLE `es_index_pattern` (
UNIQUE KEY (`datasource_id`, `name`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `builtin_metrics` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'unique identifier',
`collector` varchar(191) NOT NULL COMMENT '''type of collector''',
@@ -717,6 +728,7 @@ CREATE TABLE `builtin_metrics` (
`uuid` bigint NOT NULL DEFAULT 0 COMMENT '''uuid''',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_collector_typ_name` (`lang`,`collector`, `typ`, `name`),
INDEX `idx_uuid` (`uuid`),
INDEX `idx_collector` (`collector`),
INDEX `idx_typ` (`typ`),
INDEX `idx_builtinmetric_name` (`name` ASC),
@@ -745,6 +757,99 @@ CREATE TABLE `target_busi_group` (
UNIQUE KEY `idx_target_group` (`target_ident`,`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `dash_annotation` (
`id` bigint unsigned not null auto_increment,
`dashboard_id` bigint not null comment 'dashboard id',
`panel_id` varchar(191) not null comment 'panel id',
`tags` text comment 'tags array json string',
`description` text comment 'annotation description',
`config` text comment 'annotation config',
`time_start` bigint not null default 0 comment 'start timestamp',
`time_end` bigint not null default 0 comment 'end timestamp',
`create_at` bigint not null default 0 comment 'create time',
`create_by` varchar(64) not null default '' comment 'creator',
`update_at` bigint not null default 0 comment 'update time',
`update_by` varchar(64) not null default '' comment 'updater',
PRIMARY KEY (`id`),
KEY `idx_dashboard_id` (`dashboard_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `user_token` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL DEFAULT '',
`token_name` varchar(255) NOT NULL DEFAULT '',
`token` varchar(255) NOT NULL DEFAULT '',
`create_at` bigint NOT NULL DEFAULT 0,
`last_used` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `notify_rule` (
`id` bigint unsigned not null auto_increment,
`name` varchar(255) not null,
`description` text,
`enable` tinyint(1) not null default 0,
`user_group_ids` varchar(255) not null default '',
`notify_configs` text,
`pipeline_configs` text,
`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`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `notify_channel` (
`id` bigint unsigned not null auto_increment,
`name` varchar(255) not null,
`ident` varchar(255) not null,
`description` text,
`enable` tinyint(1) not null default 0,
`param_config` text,
`request_type` varchar(50) not null,
`request_config` text,
`weight` 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`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `message_template` (
`id` bigint unsigned not null auto_increment,
`name` varchar(64) not null,
`ident` varchar(64) not null,
`content` text,
`user_group_ids` varchar(64),
`notify_channel_ident` varchar(64) not null default '',
`private` int not null default 0,
`weight` 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`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `event_pipeline` (
`id` bigint unsigned not null auto_increment,
`name` varchar(128) not null,
`team_ids` text,
`description` varchar(255) not null default '',
`filter_enable` tinyint(1) not null default 0,
`label_filters` text,
`attribute_filters` text,
`processors` text,
`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`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `task_meta`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
@@ -2101,4 +2206,4 @@ CREATE TABLE task_host_99
UNIQUE KEY `idx_id_host` (`id`, `host`),
PRIMARY KEY (`ii`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
DEFAULT CHARSET = utf8mb4;

View File

@@ -118,6 +118,10 @@ CREATE TABLE `target_busi_group` (
UNIQUE KEY `idx_target_group` (`target_ident`,`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/* v7.7.0 2024-11-13 */
ALTER TABLE `recording_rule` ADD COLUMN `datasource_queries` TEXT;
ALTER TABLE `alert_rule` ADD COLUMN `datasource_queries` TEXT;
/* v7.7.2 2024-12-02 */
ALTER TABLE alert_subscribe MODIFY COLUMN rule_ids varchar(1024);
ALTER TABLE alert_subscribe MODIFY COLUMN busi_groups varchar(4096);
@@ -125,3 +129,119 @@ ALTER TABLE alert_subscribe MODIFY COLUMN busi_groups varchar(4096);
/* v8.0.0-beta.1 2024-12-13 */
ALTER TABLE `alert_rule` ADD COLUMN `cron_pattern` VARCHAR(64);
ALTER TABLE `builtin_components` MODIFY COLUMN `logo` mediumtext COMMENT '''logo of component''';
/* v8.0.0-beta.2 2024-12-26 */
ALTER TABLE `es_index_pattern` ADD COLUMN `cross_cluster_enabled` int not null default 0;
/* v8.0.0-beta.3 2025-01-03 */
ALTER TABLE `builtin_components` ADD COLUMN `disabled` INT NOT NULL DEFAULT 0 COMMENT 'is disabled or not';
CREATE TABLE `dash_annotation` (
`id` bigint unsigned not null auto_increment,
`dashboard_id` bigint not null comment 'dashboard id',
`panel_id` varchar(191) not null comment 'panel id',
`tags` text comment 'tags array json string',
`description` text comment 'annotation description',
`config` text comment 'annotation config',
`time_start` bigint not null default 0 comment 'start timestamp',
`time_end` bigint not null default 0 comment 'end timestamp',
`create_at` bigint not null default 0 comment 'create time',
`create_by` varchar(64) not null default '' comment 'creator',
`update_at` bigint not null default 0 comment 'update time',
`update_by` varchar(64) not null default '' comment 'updater',
PRIMARY KEY (`id`),
KEY `idx_dashboard_id` (`dashboard_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/* v8.0.0-beta.5 2025-02-05 */
CREATE TABLE `user_token` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL DEFAULT '',
`token_name` varchar(255) NOT NULL DEFAULT '',
`token` varchar(255) NOT NULL DEFAULT '',
`create_at` bigint NOT NULL DEFAULT 0,
`last_used` bigint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/* v8.0.0-beta.7 2025-03-01 */
CREATE TABLE `notify_rule` (
`id` bigint unsigned not null auto_increment,
`name` varchar(255) not null,
`description` text,
`enable` tinyint(1) not null default 0,
`user_group_ids` varchar(255) not null default '',
`notify_configs` text,
`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`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `notify_channel` (
`id` bigint unsigned not null auto_increment,
`name` varchar(255) not null,
`ident` varchar(255) not null,
`description` text,
`enable` tinyint(1) not null default 0,
`param_config` text,
`request_type` varchar(50) not null,
`request_config` text,
`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`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `message_template` (
`id` bigint unsigned not null auto_increment,
`name` varchar(64) not null,
`ident` varchar(64) not null,
`content` text,
`user_group_ids` varchar(64),
`notify_channel_ident` varchar(64) not null default '',
`private` 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`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
ALTER TABLE `alert_rule` ADD COLUMN `notify_rule_ids` varchar(1024) DEFAULT '';
ALTER TABLE `alert_rule` ADD COLUMN `notify_version` int DEFAULT 0;
ALTER TABLE `alert_subscribe` ADD COLUMN `notify_rule_ids` varchar(1024) DEFAULT '';
ALTER TABLE `alert_subscribe` ADD COLUMN `notify_version` int DEFAULT 0;
ALTER TABLE `notification_record` ADD COLUMN `notify_rule_id` BIGINT NOT NULL DEFAULT 0;
/* v8.0.0-beta.9 2025-03-17 */
ALTER TABLE `message_template` ADD COLUMN `weight` int not null default 0;
ALTER TABLE `notify_channel` ADD COLUMN `weight` int not null default 0;
/* v8.0.0-beta.11 2025-04-10 */
ALTER TABLE `es_index_pattern` ADD COLUMN `note` varchar(1024) not null default '';
ALTER TABLE `datasource` ADD COLUMN `identifier` varchar(255) not null default '';
/* v8.0.0-beta.11 2025-05-15 */
ALTER TABLE `notify_rule` ADD COLUMN `pipeline_configs` text;
CREATE TABLE `event_pipeline` (
`id` bigint unsigned not null auto_increment,
`name` varchar(128) not null,
`team_ids` text,
`description` varchar(255) not null default '',
`filter_enable` tinyint(1) not null default 0,
`label_filters` text,
`attribute_filters` text,
`processors` text,
`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`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@@ -630,6 +630,7 @@ CREATE TABLE `es_index_pattern` (
`time_field` varchar(128) not null default '@timestamp',
`allow_hide_system_indices` tinyint(1) not null default 0,
`fields_format` varchar(4096) not null default '',
`cross_cluster_enabled` int not null default 0,
`create_at` bigint default '0',
`create_by` varchar(64) default '',
`update_at` bigint default '0',
@@ -683,6 +684,22 @@ CREATE TABLE `target_busi_group` (
CREATE UNIQUE INDEX idx_target_busi_group ON target_busi_group (target_ident, group_id);
CREATE TABLE `dash_annotation` (
`id` integer primary key autoincrement,
`dashboard_id` bigint not null,
`panel_id` varchar(191) not null,
`tags` text,
`description` text,
`config` text,
`time_start` bigint not null default 0,
`time_end` 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 ''
);
CREATE TABLE `task_meta`
(
`id` integer primary key autoincrement,

59
dscache/cache.go Normal file
View File

@@ -0,0 +1,59 @@
package dscache
import (
"sync"
"github.com/ccfos/nightingale/v6/datasource"
"github.com/toolkits/pkg/logger"
)
type Cache struct {
datas map[string]map[int64]datasource.Datasource
mutex *sync.RWMutex
}
var DsCache = Cache{
datas: make(map[string]map[int64]datasource.Datasource),
mutex: new(sync.RWMutex),
}
func (cs *Cache) Put(cate string, dsId int64, ds datasource.Datasource) {
cs.mutex.Lock()
if _, found := cs.datas[cate]; !found {
cs.datas[cate] = make(map[int64]datasource.Datasource)
}
if _, found := cs.datas[cate][dsId]; found {
if cs.datas[cate][dsId].Equal(ds) {
cs.mutex.Unlock()
return
}
}
cs.mutex.Unlock()
// InitClient() 在用户配置错误或远端不可用时, 会非常耗时, mutex被长期持有, 导致Get()会超时
err := ds.InitClient()
if err != nil {
logger.Errorf("init plugin:%s %d %+v client fail: %v", cate, dsId, ds, err)
return
}
logger.Debugf("init plugin:%s %d %+v client success", cate, dsId, ds)
cs.mutex.Lock()
cs.datas[cate][dsId] = ds
cs.mutex.Unlock()
}
func (cs *Cache) Get(cate string, dsId int64) (datasource.Datasource, bool) {
cs.mutex.RLock()
defer cs.mutex.RUnlock()
if _, found := cs.datas[cate]; !found {
return nil, false
}
if _, found := cs.datas[cate][dsId]; !found {
return nil, false
}
return cs.datas[cate][dsId], true
}

205
dscache/sync.go Normal file
View File

@@ -0,0 +1,205 @@
package dscache
import (
"context"
"strings"
"sync/atomic"
"time"
"github.com/ccfos/nightingale/v6/datasource"
_ "github.com/ccfos/nightingale/v6/datasource/ck"
"github.com/ccfos/nightingale/v6/datasource/es"
"github.com/ccfos/nightingale/v6/dskit/tdengine"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/toolkits/pkg/logger"
)
var FromAPIHook func()
func Init(ctx *ctx.Context, fromAPI bool) {
go getDatasourcesFromDBLoop(ctx, fromAPI)
}
type ListInput struct {
Page int `json:"p"`
Limit int `json:"limit"`
Category string `json:"category"`
PluginType string `json:"plugin_type"` // promethues
Status string `json:"status"`
}
type DSReply struct {
RequestID string `json:"request_id"`
Data struct {
Items []datasource.DatasourceInfo `json:"items"`
} `json:"data"`
}
type DSReplyEncrypt struct {
RequestID string `json:"request_id"`
Data string `json:"data"`
}
var PromDefaultDatasourceId int64
func getDatasourcesFromDBLoop(ctx *ctx.Context, fromAPI bool) {
for {
if !fromAPI {
foundDefaultDatasource := false
items, err := models.GetDatasources(ctx)
if err != nil {
logger.Errorf("get datasource from database fail: %v", err)
//stat.CounterExternalErrorTotal.WithLabelValues("db", "get_cluster").Inc()
time.Sleep(time.Second * 2)
continue
}
var dss []datasource.DatasourceInfo
for _, item := range items {
if item.PluginType == "prometheus" && item.IsDefault {
atomic.StoreInt64(&PromDefaultDatasourceId, item.Id)
foundDefaultDatasource = true
}
logger.Debugf("get datasource: %+v", item)
ds := datasource.DatasourceInfo{
Id: item.Id,
Name: item.Name,
Description: item.Description,
Category: item.Category,
PluginId: item.PluginId,
Type: item.PluginType,
PluginTypeName: item.PluginTypeName,
Settings: item.SettingsJson,
HTTPJson: item.HTTPJson,
AuthJson: item.AuthJson,
Status: item.Status,
IsDefault: item.IsDefault,
}
if item.PluginType == "elasticsearch" {
esN9eToDatasourceInfo(&ds, item)
} else if item.PluginType == "opensearch" {
osN9eToDatasourceInfo(&ds, item)
} else if item.PluginType == "tdengine" {
tdN9eToDatasourceInfo(&ds, item)
} else {
ds.Settings = make(map[string]interface{})
for k, v := range item.SettingsJson {
ds.Settings[k] = v
}
}
dss = append(dss, ds)
}
if !foundDefaultDatasource && atomic.LoadInt64(&PromDefaultDatasourceId) != 0 {
logger.Debugf("no default datasource found")
atomic.StoreInt64(&PromDefaultDatasourceId, 0)
}
PutDatasources(dss)
} else {
FromAPIHook()
}
time.Sleep(time.Second * 2)
}
}
func tdN9eToDatasourceInfo(ds *datasource.DatasourceInfo, item models.Datasource) {
ds.Settings = make(map[string]interface{})
ds.Settings["tdengine.cluster_name"] = item.Name
ds.Settings["tdengine.addr"] = item.HTTPJson.Url
ds.Settings["tdengine.timeout"] = item.HTTPJson.Timeout
ds.Settings["tdengine.dial_timeout"] = item.HTTPJson.DialTimeout
ds.Settings["tdengine.max_idle_conns_per_host"] = item.HTTPJson.MaxIdleConnsPerHost
ds.Settings["tdengine.headers"] = item.HTTPJson.Headers
ds.Settings["tdengine.basic"] = tdengine.TDengineBasicAuth{
User: item.AuthJson.BasicAuthUser,
Password: item.AuthJson.BasicAuthPassword,
}
}
func esN9eToDatasourceInfo(ds *datasource.DatasourceInfo, item models.Datasource) {
ds.Settings = make(map[string]interface{})
ds.Settings["es.nodes"] = []string{item.HTTPJson.Url}
if len(item.HTTPJson.Urls) > 0 {
ds.Settings["es.nodes"] = item.HTTPJson.Urls
}
ds.Settings["es.timeout"] = item.HTTPJson.Timeout
ds.Settings["es.basic"] = es.BasicAuth{
Username: item.AuthJson.BasicAuthUser,
Password: item.AuthJson.BasicAuthPassword,
}
ds.Settings["es.tls"] = es.TLS{
SkipTlsVerify: item.HTTPJson.TLS.SkipTlsVerify,
}
ds.Settings["es.version"] = item.SettingsJson["version"]
ds.Settings["es.headers"] = item.HTTPJson.Headers
ds.Settings["es.min_interval"] = item.SettingsJson["min_interval"]
ds.Settings["es.max_shard"] = item.SettingsJson["max_shard"]
ds.Settings["es.enable_write"] = item.SettingsJson["enable_write"]
}
// for opensearch
func osN9eToDatasourceInfo(ds *datasource.DatasourceInfo, item models.Datasource) {
ds.Settings = make(map[string]interface{})
ds.Settings["os.nodes"] = []string{item.HTTPJson.Url}
ds.Settings["os.timeout"] = item.HTTPJson.Timeout
ds.Settings["os.basic"] = es.BasicAuth{
Username: item.AuthJson.BasicAuthUser,
Password: item.AuthJson.BasicAuthPassword,
}
ds.Settings["os.tls"] = es.TLS{
SkipTlsVerify: item.HTTPJson.TLS.SkipTlsVerify,
}
ds.Settings["os.version"] = item.SettingsJson["version"]
ds.Settings["os.headers"] = item.HTTPJson.Headers
ds.Settings["os.min_interval"] = item.SettingsJson["min_interval"]
ds.Settings["os.max_shard"] = item.SettingsJson["max_shard"]
}
func PutDatasources(items []datasource.DatasourceInfo) {
ids := make([]int64, 0)
for _, item := range items {
if item.Type == "prometheus" {
continue
}
if item.Type == "loki" {
continue
}
if item.Name == "" {
logger.Warningf("cluster name is empty, ignore %+v", item)
continue
}
typ := strings.ReplaceAll(item.Type, ".logging", "")
ds, err := datasource.GetDatasourceByType(typ, item.Settings)
if err != nil {
logger.Warningf("get plugin:%+v fail: %v", item, err)
continue
}
err = ds.Validate(context.Background())
if err != nil {
logger.Warningf("get plugin:%+v fail: %v", item, err)
continue
}
ids = append(ids, item.Id)
// 异步初始化 client 不然数据源同步的会很慢
go func() {
defer func() {
if r := recover(); r != nil {
logger.Errorf("panic in datasource item: %+v panic:%v", item, r)
}
}()
DsCache.Put(typ, item.Id, ds)
}()
}
logger.Debugf("get plugin by type success Ids:%v", ids)
}

View File

@@ -0,0 +1,303 @@
package clickhouse
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/ccfos/nightingale/v6/dskit/sqlbase"
"github.com/ccfos/nightingale/v6/dskit/types"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/mitchellh/mapstructure"
"github.com/toolkits/pkg/net/httplib"
ckDriver "gorm.io/driver/clickhouse"
"gorm.io/gorm"
)
const (
ckDataSource = "clickhouse://%s:%s@%s?read_timeout=10s"
DefaultLimit = 500
)
type Clickhouse struct {
Nodes []string `json:"ck.nodes" mapstructure:"ck.nodes"`
User string `json:"ck.user" mapstructure:"ck.user"`
Password string `json:"ck.password" mapstructure:"ck.password"`
Timeout int `json:"ck.timeout" mapstructure:"ck.timeout"`
MaxQueryRows int `json:"ck.max_query_rows" mapstructure:"ck.max_query_rows"`
Client *gorm.DB `json:"-"`
ClientByHTTP *sql.DB `json:"-"`
}
func (c *Clickhouse) InitCli() error {
if c.MaxQueryRows == 0 {
c.MaxQueryRows = DefaultLimit
}
if len(c.Nodes) == 0 {
return fmt.Errorf("not found ck shard, please check datasource config")
}
addr := c.Nodes[0]
url := addr
if !strings.HasPrefix(url, "http://") {
url = "http://" + url
}
resp, err := httplib.Get(url).SetTimeout(time.Second * 1).Response()
// 忽略HTTP Code错误, 因为可能不是HTTP协议
if err != nil {
return err
}
defer resp.Body.Close()
// HTTP 协议
if resp.StatusCode == 200 {
jsonBytes, _ := io.ReadAll(resp.Body)
if len(jsonBytes) > 0 && strings.Contains(strings.ToLower(string(jsonBytes)), "ok.") {
ckconn := clickhouse.OpenDB(&clickhouse.Options{
Addr: []string{addr},
Auth: clickhouse.Auth{
Username: c.User,
Password: c.Password,
},
Settings: clickhouse.Settings{
"max_execution_time": 60,
},
DialTimeout: 10 * time.Second,
Protocol: clickhouse.HTTP,
})
if ckconn == nil {
return errors.New("db conn failed")
}
c.ClientByHTTP = ckconn
return nil
}
}
db, err := gorm.Open(
ckDriver.New(
ckDriver.Config{
DSN: fmt.Sprintf(ckDataSource,
c.User, c.Password, addr),
DisableDatetimePrecision: true,
DontSupportRenameColumn: true,
SkipInitializeWithVersion: false,
}),
)
if err != nil {
return err
}
c.Client = db
return nil
}
const (
ShowDatabases = "SHOW DATABASES"
ShowTables = "SELECT name FROM system.tables WHERE database = '%s'"
DescTable = "SELECT name,type FROM system.columns WHERE database='%s' AND table = '%s';"
)
func (c *Clickhouse) QueryRows(ctx context.Context, query string) (*sql.Rows, error) {
var (
rows *sql.Rows
err error
)
if c.ClientByHTTP != nil {
rows, err = c.ClientByHTTP.Query(query)
if err != nil {
return nil, err
}
} else if c.Client != nil {
rows, err = c.Client.Raw(query).Rows()
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("clickhouse client is nil")
}
return rows, nil
}
// ShowDatabases lists all databases in Clickhouse
func (c *Clickhouse) ShowDatabases(ctx context.Context) ([]string, error) {
var (
res []string
)
rows, err := c.QueryRows(ctx, ShowDatabases)
if err != nil {
return nil, err
}
for rows.Next() {
var r string
if err := rows.Scan(&r); err != nil {
return nil, err
}
res = append(res, r)
}
return res, nil
}
// ShowTables lists all tables in a given database
func (c *Clickhouse) ShowTables(ctx context.Context, database string) ([]string, error) {
var (
res []string
)
showTables := fmt.Sprintf(ShowTables, database)
rows, err := c.QueryRows(ctx, showTables)
if err != nil {
return nil, err
}
for rows.Next() {
var r string
if err := rows.Scan(&r); err != nil {
return nil, err
}
res = append(res, r)
}
return res, nil
}
// DescribeTable describes the schema of a specified table in Clickhouse
func (c *Clickhouse) DescribeTable(ctx context.Context, query interface{}) ([]*types.ColumnProperty, error) {
var (
ret []*types.ColumnProperty
)
ckQueryParam := new(QueryParam)
if err := mapstructure.Decode(query, ckQueryParam); err != nil {
return nil, err
}
descTable := fmt.Sprintf(DescTable, ckQueryParam.Database, ckQueryParam.Table)
rows, err := c.QueryRows(ctx, descTable)
if err != nil {
return nil, err
}
for rows.Next() {
var column types.ColumnProperty
if err := rows.Scan(&column.Field, &column.Type); err != nil {
return nil, err
}
ret = append(ret, &column)
}
return ret, nil
}
func (c *Clickhouse) ExecQueryBySqlDB(ctx context.Context, sql string) ([]map[string]interface{}, error) {
rows, err := c.QueryRows(ctx, sql)
if err != nil {
return nil, err
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return nil, err
}
var results []map[string]interface{}
for rows.Next() {
columnValues := make([]interface{}, len(columns))
columnPointers := make([]interface{}, len(columns))
for i := range columnValues {
columnPointers[i] = &columnValues[i]
}
if err := rows.Scan(columnPointers...); err != nil {
continue
}
rowMap := make(map[string]interface{})
for i, colName := range columns {
val := columnValues[i]
bytes, ok := val.([]byte)
if ok {
rowMap[colName] = string(bytes)
} else {
rowMap[colName] = val
}
}
results = append(results, rowMap)
}
return results, nil
}
func (c *Clickhouse) Query(ctx context.Context, query interface{}) ([]map[string]interface{}, error) {
ckQuery := new(QueryParam)
if err := mapstructure.Decode(query, ckQuery); err != nil {
return nil, err
}
// 校验SQL的合法性, 过滤掉 write请求
sqlItem := strings.Split(strings.ToUpper(ckQuery.Sql), " ")
for _, item := range sqlItem {
if _, ok := ckBannedOp[item]; ok {
return nil, fmt.Errorf("operation %s is forbid, only read db, please check your sql", item)
}
}
// 检查匹配数据长度,防止数据量过大
err := c.CheckMaxQueryRows(ctx, ckQuery.Sql)
if err != nil {
return nil, err
}
dbRows := make([]map[string]interface{}, 0)
if c.ClientByHTTP != nil {
dbRows, err = c.ExecQueryBySqlDB(ctx, ckQuery.Sql)
} else {
err = c.Client.Raw(ckQuery.Sql).Find(&dbRows).Error
}
if err != nil {
return nil, fmt.Errorf("fetch data failed, sql is %s, err is %s", ckQuery.Sql, err.Error())
}
return dbRows, nil
}
func (c *Clickhouse) CheckMaxQueryRows(ctx context.Context, sql string) error {
subSql := strings.ReplaceAll(sql, ";", "")
subSql = fmt.Sprintf("SELECT COUNT(*) as count FROM (%s) AS subquery;", subSql)
dbRows, err := c.ExecQueryBySqlDB(ctx, subSql)
if err != nil {
return fmt.Errorf("fetch data failed, sql is %s, err is %s", subSql, err.Error())
}
if len(dbRows) > 0 {
if count, exists := dbRows[0]["count"]; exists {
v, err := sqlbase.ParseFloat64Value(count)
if err != nil {
return err
}
if v > float64(c.MaxQueryRows) {
return fmt.Errorf("query result rows count %d exceeds the maximum limit %d", int(v), c.MaxQueryRows)
}
}
}
return nil
}

View File

@@ -0,0 +1,43 @@
package clickhouse
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/ccfos/nightingale/v6/dskit/types"
)
func Test_Timeseries(t *testing.T) {
ck := &Clickhouse{
Nodes: []string{"127.0.0.1:8123"},
User: "default",
Password: "123456",
}
err := ck.InitCli()
if err != nil {
t.Fatal(err)
}
data, err := ck.QueryTimeseries(context.TODO(), &QueryParam{
Sql: `select * from default.student limit 20`,
From: time.Now().Unix() - 300,
To: time.Now().Unix(),
TimeField: "created_at",
TimeFormat: "datetime",
Keys: types.Keys{
LabelKey: "age",
},
})
if err != nil {
t.Fatal(err)
}
bs, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
fmt.Println(string(bs))
}

View File

@@ -0,0 +1,58 @@
package clickhouse
import (
"context"
"fmt"
"github.com/ccfos/nightingale/v6/dskit/sqlbase"
"github.com/ccfos/nightingale/v6/dskit/types"
)
const (
TimeFieldFormatEpochMilli = "epoch_millis"
TimeFieldFormatEpochSecond = "epoch_second"
)
// 时序数据相关的API
type QueryParam struct {
Limit int `json:"limit" mapstructure:"limit"`
Sql string `json:"sql" mapstructure:"sql"`
Ref string `json:"ref" mapstructure:"ref"`
From int64 `json:"from" mapstructure:"from"`
To int64 `json:"to" mapstructure:"to"`
TimeField string `json:"time_field" mapstructure:"time_field"`
TimeFormat string `json:"time_format" mapstructure:"time_format"`
Keys types.Keys `json:"keys" mapstructure:"keys"`
Database string `json:"database" mapstructure:"database"`
Table string `json:"table" mapstructure:"table"`
}
var (
ckBannedOp = map[string]struct{}{
"CREATE": {},
"INSERT": {},
"ALTER": {},
"REVOKE": {},
"DROP": {},
"RENAME": {},
"ATTACH": {},
"DETACH": {},
"OPTIMIZE": {},
"TRUNCATE": {},
"SET": {},
}
)
func (c *Clickhouse) QueryTimeseries(ctx context.Context, query *QueryParam) ([]types.MetricValues, error) {
if query.Keys.ValueKey == "" {
return nil, fmt.Errorf("valueKey is required")
}
rows, err := c.Query(ctx, query)
if err != nil {
return nil, err
}
// 构造成时续数据
return sqlbase.FormatMetricValues(query.Keys, rows, true), nil
}

236
dskit/sqlbase/base.go Normal file
View File

@@ -0,0 +1,236 @@
// @Author: Ciusyan 5/19/24
package sqlbase
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/ccfos/nightingale/v6/dskit/types"
"gorm.io/gorm"
)
// NewDB creates a new Gorm DB instance based on the provided gorm.Dialector and configures the connection pool
func NewDB(ctx context.Context, dialector gorm.Dialector, maxIdleConns, maxOpenConns int, connMaxLifetime time.Duration) (*gorm.DB, error) {
// Create a new Gorm DB instance
db, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
return nil, err
}
// Configure the connection pool
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(maxIdleConns)
sqlDB.SetMaxOpenConns(maxOpenConns)
sqlDB.SetConnMaxLifetime(connMaxLifetime)
return db.WithContext(ctx), sqlDB.Ping()
}
// ShowTables retrieves a list of all tables in the specified database
func ShowTables(ctx context.Context, db *gorm.DB, query string) ([]string, error) {
var tables []string
rows, err := db.WithContext(ctx).Raw(query).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return nil, err
}
tables = append(tables, table)
}
return tables, nil
}
// ShowDatabases retrieves a list of all databases in the connected database server
func ShowDatabases(ctx context.Context, db *gorm.DB, query string) ([]string, error) {
var databases []string
rows, err := db.WithContext(ctx).Raw(query).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var database string
if err := rows.Scan(&database); err != nil {
return nil, err
}
databases = append(databases, database)
}
return databases, nil
}
// DescTable describes the schema of a specified table in MySQL or PostgreSQL
func DescTable(ctx context.Context, db *gorm.DB, query string) ([]*types.ColumnProperty, error) {
rows, err := db.WithContext(ctx).Raw(query).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
var columns []*types.ColumnProperty
for rows.Next() {
var (
field string
typ string
null string
key sql.NullString
defaultValue sql.NullString
extra sql.NullString
)
switch db.Dialector.Name() {
case "mysql":
if err := rows.Scan(&field, &typ, &null, &key, &defaultValue, &extra); err != nil {
continue
}
case "postgres", "sqlserver":
if err := rows.Scan(&field, &typ, &null, &defaultValue); err != nil {
continue
}
case "oracle":
if err := rows.Scan(&field, &typ, &null); err != nil {
continue
}
}
// Convert the database-specific type to internal type
type2, indexable := convertDBType(db.Dialector.Name(), typ)
columns = append(columns, &types.ColumnProperty{
Field: field,
Type: typ,
Type2: type2,
Indexable: indexable,
})
}
return columns, nil
}
// ExecQuery executes the specified query and returns the result rows
func ExecQuery(ctx context.Context, db *gorm.DB, sql string) ([]map[string]interface{}, error) {
rows, err := db.WithContext(ctx).Raw(sql).Rows()
if err != nil {
return nil, err
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return nil, err
}
var results []map[string]interface{}
for rows.Next() {
columnValues := make([]interface{}, len(columns))
columnPointers := make([]interface{}, len(columns))
for i := range columnValues {
columnPointers[i] = &columnValues[i]
}
if err := rows.Scan(columnPointers...); err != nil {
continue
}
rowMap := make(map[string]interface{})
for i, colName := range columns {
val := columnValues[i]
bytes, ok := val.([]byte)
if ok {
rowMap[colName] = string(bytes)
} else {
rowMap[colName] = val
}
}
results = append(results, rowMap)
}
return results, nil
}
// SelectRows selects rows from a specified table based on a given query
func SelectRows(ctx context.Context, db *gorm.DB, table, query string) ([]map[string]interface{}, error) {
sql := fmt.Sprintf("SELECT * FROM %s", table)
if query != "" {
sql += " WHERE " + query
}
return ExecQuery(ctx, db, sql)
}
// convertDBType converts MySQL or PostgreSQL data types to custom internal types and determines if they are indexable
func convertDBType(dialect, dbType string) (string, bool) {
typ := strings.ToLower(dbType)
// Common type conversions
switch {
case strings.HasPrefix(typ, "int"), strings.HasPrefix(typ, "tinyint"),
strings.HasPrefix(typ, "smallint"), strings.HasPrefix(typ, "mediumint"),
strings.HasPrefix(typ, "bigint"), strings.HasPrefix(typ, "serial"),
strings.HasPrefix(typ, "bigserial"):
return types.LogExtractValueTypeLong, true
case strings.HasPrefix(typ, "varchar"), strings.HasPrefix(typ, "text"),
strings.HasPrefix(typ, "char"), strings.HasPrefix(typ, "tinytext"),
strings.HasPrefix(typ, "mediumtext"), strings.HasPrefix(typ, "longtext"),
strings.HasPrefix(typ, "character varying"), strings.HasPrefix(typ, "nvarchar"),
strings.HasPrefix(typ, "nchar"):
return types.LogExtractValueTypeText, true
case strings.HasPrefix(typ, "float"), strings.HasPrefix(typ, "double"),
strings.HasPrefix(typ, "decimal"), strings.HasPrefix(typ, "numeric"),
strings.HasPrefix(typ, "real"), strings.HasPrefix(typ, "double precision"):
return types.LogExtractValueTypeFloat, true
case strings.HasPrefix(typ, "date"), strings.HasPrefix(typ, "datetime"),
strings.HasPrefix(typ, "timestamp"), strings.HasPrefix(typ, "timestamptz"),
strings.HasPrefix(typ, "time"), strings.HasPrefix(typ, "smalldatetime"):
return types.LogExtractValueTypeDate, false
case strings.HasPrefix(typ, "boolean"), strings.HasPrefix(typ, "bit"):
return types.LogExtractValueTypeBool, false
}
// Specific type conversions for MySQL
if dialect == "mysql" {
switch {
default:
return typ, false
}
}
// Specific type conversions for PostgreSQL
if dialect == "postgres" {
switch {
default:
return typ, false
}
}
if dialect == "oracle" {
switch {
default:
return typ, false
}
}
// Can continue to add specific 'dialect' type ...
return typ, false
}

250
dskit/sqlbase/timeseries.go Normal file
View File

@@ -0,0 +1,250 @@
// @Author: Ciusyan 5/20/24
package sqlbase
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/ccfos/nightingale/v6/dskit/types"
"github.com/prometheus/common/model"
"gorm.io/gorm"
)
type QueryParam struct {
Sql string `json:"sql"`
Keys types.Keys `json:"keys" mapstructure:"keys"`
}
var (
BannedOp = map[string]struct{}{
"CREATE": {},
"INSERT": {},
"UPDATE": {},
"DELETE": {},
"ALTER": {},
"REVOKE": {},
"DROP": {},
"RENAME": {},
"TRUNCATE": {},
"SET": {},
}
)
// Query executes a given SQL query and returns the results
func Query(ctx context.Context, db *gorm.DB, query *QueryParam) ([]map[string]interface{}, error) {
// Validate SQL to prevent write operations if needed
sqlItem := strings.Split(strings.ToUpper(query.Sql), " ")
for _, item := range sqlItem {
if _, ok := BannedOp[item]; ok {
return nil, fmt.Errorf("operation %s is forbidden, only read operations are allowed, please check your SQL", item)
}
}
return ExecQuery(ctx, db, query.Sql)
}
// QueryTimeseries executes a time series data query using the given parameters
func QueryTimeseries(ctx context.Context, db *gorm.DB, query *QueryParam, ignoreDefault ...bool) ([]types.MetricValues, error) {
rows, err := Query(ctx, db, query)
if err != nil {
return nil, err
}
return FormatMetricValues(query.Keys, rows, ignoreDefault...), nil
}
func FormatMetricValues(keys types.Keys, rows []map[string]interface{}, ignoreDefault ...bool) []types.MetricValues {
ignore := false
if len(ignoreDefault) > 0 {
ignore = ignoreDefault[0]
}
keyMap := make(map[string]string)
for _, valueMetric := range strings.Split(keys.ValueKey, " ") {
keyMap[valueMetric] = "value"
}
for _, labelMetric := range strings.Split(keys.LabelKey, " ") {
keyMap[labelMetric] = "label"
}
if keys.TimeKey == "" {
keys.TimeKey = "time"
}
if len(keys.TimeKey) > 0 {
keyMap[keys.TimeKey] = "time"
}
var dataResps []types.MetricValues
dataMap := make(map[string]*types.MetricValues)
for _, row := range rows {
labels := make(map[string]string)
metricValue := make(map[string]float64)
metricTs := make(map[string]float64)
// Process each column based on its designated role (value, label, time)
for k, v := range row {
switch keyMap[k] {
case "value":
val, err := ParseFloat64Value(v)
if err != nil {
continue
}
metricValue[k] = val
case "label":
labels[k] = fmt.Sprintf("%v", v)
case "time":
ts, err := ParseTime(v, keys.TimeFormat)
if err != nil {
continue
}
metricTs[k] = float64(ts.Unix())
default:
// Default to labels for any unrecognized columns
if !ignore {
labels[k] = fmt.Sprintf("%v", v)
}
}
}
// Compile and store the metric values
for metricName, value := range metricValue {
metrics := make(model.Metric)
var labelsStr []string
for k1, v1 := range labels {
metrics[model.LabelName(k1)] = model.LabelValue(v1)
labelsStr = append(labelsStr, fmt.Sprintf("%s=%s", k1, v1))
}
metrics["__name__"] = model.LabelValue(metricName)
labelsStr = append(labelsStr, fmt.Sprintf("__name__=%s", metricName))
// Hash the labels to use as a key
sort.Strings(labelsStr)
labelsStrHash := fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(labelsStr, ","))))
// Append new values to the existing metric, if present
ts, exists := metricTs[keys.TimeKey]
if !exists {
ts = float64(time.Now().Unix()) // Default to current time if not specified
}
valuePair := []float64{ts, value}
if existing, ok := dataMap[labelsStrHash]; ok {
existing.Values = append(existing.Values, valuePair)
} else {
dataResp := types.MetricValues{
Metric: metrics,
Values: [][]float64{valuePair},
}
dataMap[labelsStrHash] = &dataResp
}
}
}
// Convert the map to a slice for the response
for _, v := range dataMap {
sort.Slice(v.Values, func(i, j int) bool { return v.Values[i][0] < v.Values[j][0] }) // Sort by timestamp
dataResps = append(dataResps, *v)
}
return dataResps
}
// ParseFloat64Value attempts to convert an interface{} to float64 using reflection
func ParseFloat64Value(val interface{}) (float64, error) {
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.Float64, reflect.Float32:
return v.Float(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(v.Int()), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(v.Uint()), nil
case reflect.String:
return strconv.ParseFloat(v.String(), 64)
case reflect.Slice:
if v.Type().Elem().Kind() == reflect.Uint8 {
return strconv.ParseFloat(string(v.Bytes()), 64)
}
case reflect.Interface:
return ParseFloat64Value(v.Interface())
case reflect.Ptr:
if !v.IsNil() {
return ParseFloat64Value(v.Elem().Interface())
}
case reflect.Struct:
if num, ok := val.(json.Number); ok {
return num.Float64()
}
}
return 0, fmt.Errorf("cannot convert type %T to float64", val)
}
// ParseTime attempts to parse a time value from an interface{} using a specified format
func ParseTime(val interface{}, format string) (time.Time, error) {
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.String:
str := v.String()
return parseTimeFromString(str, format)
case reflect.Slice:
if v.Type().Elem().Kind() == reflect.Uint8 {
str := string(v.Bytes())
return parseTimeFromString(str, format)
}
case reflect.Int, reflect.Int64:
return time.Unix(v.Int(), 0), nil
case reflect.Float64:
return time.Unix(int64(v.Float()), 0), nil
case reflect.Interface:
return ParseTime(v.Interface(), format)
case reflect.Ptr:
if !v.IsNil() {
return ParseTime(v.Elem().Interface(), format)
}
case reflect.Struct:
if t, ok := val.(time.Time); ok {
return t, nil
}
}
return time.Time{}, fmt.Errorf("invalid time value type: %v", val)
}
func parseTimeFromString(str, format string) (time.Time, error) {
// If a custom time format is provided, use it to parse the string
if format != "" {
parsedTime, err := time.Parse(format, str)
if err == nil {
return parsedTime, nil
}
return time.Time{}, fmt.Errorf("failed to parse time '%s' with format '%s': %v", str, format, err)
}
// Try to parse the string as RFC3339, RFC3339Nano, or Unix timestamp
if parsedTime, err := time.Parse(time.RFC3339, str); err == nil {
return parsedTime, nil
}
if parsedTime, err := time.Parse(time.RFC3339Nano, str); err == nil {
return parsedTime, nil
}
if timestamp, err := strconv.ParseInt(str, 10, 64); err == nil {
return time.Unix(timestamp, 0), nil
}
if timestamp, err := strconv.ParseFloat(str, 64); err == nil {
return time.Unix(int64(timestamp), 0), nil
}
return time.Time{}, fmt.Errorf("failed to parse time '%s'", str)
}

View File

@@ -0,0 +1,149 @@
// @Author: Ciusyan 5/17/24
package sqlbase
import (
"encoding/json"
"testing"
"time"
"github.com/ccfos/nightingale/v6/dskit/types"
)
func TestFormatMetricValues(t *testing.T) {
tests := []struct {
name string
keys types.Keys
rows []map[string]interface{}
want []types.MetricValues
}{
{
name: "cases1",
keys: types.Keys{
ValueKey: "grade a_grade",
LabelKey: "id student_name",
TimeKey: "update_time",
TimeFormat: "2006-01-02 15:04:05",
},
rows: []map[string]interface{}{
{
"id": "10007",
"grade": 20003,
"student_name": "邵子韬",
"a_grade": 69,
"update_time": "2024-05-14 10:00:00",
},
{
"id": "10007",
"grade": 20003,
"student_name": "邵子韬",
"a_grade": 69,
"update_time": "2024-05-14 10:05:00",
},
{
"id": "10007",
"grade": 20003,
"student_name": "邵子韬",
"a_grade": 69,
"update_time": "2024-05-14 10:10:00",
},
{
"id": "10008",
"grade": 20004,
"student_name": "Ciusyan",
"a_grade": 100,
"update_time": "2024-05-14 12:00:00",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FormatMetricValues(tt.keys, tt.rows)
for _, g := range got {
t.Log(g)
}
})
}
}
func TestParseFloat64Value(t *testing.T) {
ptr := func(val float64) *float64 {
return &val
}
tests := []struct {
name string
input interface{}
want float64
wantErr bool
}{
{"float64", 1.23, 1.23, false},
{"float32", float32(1.23), float64(float32(1.23)), false},
{"int", 123, 123, false},
{"int64", int64(123), 123, false},
{"uint", uint(123), 123, false},
{"uint64", uint64(123), 123, false},
{"string", "1.23", 1.23, false},
{"[]byte", []byte("1.23"), 1.23, false},
{"json.Number", json.Number("1.23"), 1.23, false},
{"interface", interface{}(1.23), 1.23, false},
{"pointer", ptr(1.23), 1.23, false},
{"invalid string", "abc", 0, true},
{"invalid type", struct{}{}, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseFloat64Value(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseFloat64Value() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseFloat64Value() = %v, want %v", got, tt.want)
}
})
}
}
func TestParseTime(t *testing.T) {
ptrTime := func(t time.Time) *time.Time {
return &t
}
tests := []struct {
name string
input interface{}
format string
want time.Time
wantErr bool
}{
{"RFC3339", "2024-05-14T12:34:56Z", "", time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC), false},
{"RFC3339Nano", "2024-05-14T12:34:56.789Z", "", time.Date(2024, 5, 14, 12, 34, 56, 789000000, time.UTC), false},
{"Unix timestamp int", int64(1715642135), "", time.Unix(1715642135, 0), false},
{"Unix timestamp float64", 1715642135.0, "", time.Unix(int64(1715642135), 0), false},
{"custom format", "14/05/2024", "02/01/2006", time.Date(2024, 5, 14, 0, 0, 0, 0, time.UTC), false},
{"slice", []byte("2024-05-14T12:34:56Z"), "", time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC), false},
{"interface", interface{}("2024-05-14T12:34:56Z"), "", time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC), false},
{"pointer", ptrTime(time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC)), "", time.Date(2024, 5, 14, 12, 34, 56, 0, time.UTC), false},
{"invalid format", "14-05-2024", "02/01/2006", time.Time{}, true},
{"invalid type", struct{}{}, "", time.Time{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseTime(tt.input, tt.format)
if (err != nil) != tt.wantErr {
t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !got.Equal(tt.want) {
t.Errorf("ParseTime() = %v, want %v", got, tt.want)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More