Compare commits

...

106 Commits

Author SHA1 Message Date
Xu Bin
deb11752e5 refactor: webhook send event (#2239) 2024-10-28 11:14:09 +08:00
ulricqin
31b3434e87 Update README.md 2024-10-22 14:19:33 +08:00
ning
2576a0f815 fix: edge get all configs 2024-10-21 19:30:13 +08:00
ning
0ac4bc7421 docs: update linux dashboard tpl 2024-10-21 18:07:52 +08:00
ning
95e6ea98f4 refactor: prom client query api add retry 2024-10-21 17:57:31 +08:00
ning
dc60c74c0d docs: update automq dashboard tpl 2024-10-21 16:50:36 +08:00
ning
a15adc196d docs: update linux dashboard tpl 2024-10-21 16:35:53 +08:00
ning
f89ef04e85 refactor: optimize code robustness 2024-10-21 14:54:48 +08:00
Yening Qin
f55cd9b32e feat: config access log in web (#2227) 2024-10-21 12:11:19 +08:00
Xu Bin
305a898f8b feat: alert recover ckeck (#2226) 2024-10-21 12:07:54 +08:00
Yening Qin
60c31d8eb2 feat: support query set opration (#2225) 2024-10-20 21:18:12 +08:00
ning
7da49a8c68 refactor: update go.mod 2024-10-20 14:04:31 +08:00
flashbo
65b1410b09 refactor: support output logs to one file (#2209) 2024-10-20 14:02:44 +08:00
ning
3901671c0e docs: update n9e.sql 2024-10-18 15:24:33 +08:00
Xu Bin
9c02937e81 refactor: alert mute retain (#2223) 2024-10-18 12:08:31 +08:00
flashbo
0a255ee33a fix: unbind bgids when delete target (#2219) 2024-10-16 10:00:08 +08:00
Xu Bin
8dc198b4b1 fix: smtp update (#2213) 2024-10-12 11:37:14 +08:00
Yening Qin
9696f63a71 rename tpl name 2024-10-11 16:23:57 +08:00
Xu Bin
03f56f73b4 feat: ldap support multi basecn (#2198) 2024-10-08 16:06:21 +08:00
Ulric Qin
7b415c91af update qrcode 2024-10-08 15:40:34 +08:00
flashbo
2abf089444 feat: rule list add user nickname (#2201) 2024-10-08 15:25:25 +08:00
mt
e504dab359 fix: update router_alert_cur_event.go (#2210) 2024-10-03 00:27:31 +08:00
710leo
989ed62e8d refactor: update GetAnomalyPoint 2024-09-29 19:34:25 +08:00
nl594
b7197d10eb docs: add new ipmi dashboard (#2204)
* add new ipmi dashboard

* Update IPMI_by_prometheus.json

---------

Co-authored-by: niulong <niulong@xylink.com>
Co-authored-by: Yening Qin <710leo@gmail.com>
2024-09-29 13:24:56 +08:00
Xu Bin
f4de256388 refactor: target delete hook (#2202) 2024-09-27 15:43:57 +08:00
Xu Bin
3f5126923f feat: get build payload by UUID (#2203) 2024-09-27 15:43:18 +08:00
flashbo
5d3e70bc4c refactor: datasouce support force save (#2200) 2024-09-27 14:40:48 +08:00
710leo
bb2c5202ad Merge branch 'main' of github.com:ccfos/nightingale 2024-09-27 14:26:48 +08:00
710leo
3acf3d7bf9 refactor: migrate target bg 2024-09-27 14:26:35 +08:00
shardingHe
a79810b15d add deployment & statefulset dashboard (#2196)
Co-authored-by: shardingHe <wangzihe@flashcat.cloud>
2024-09-26 17:38:47 +08:00
710leo
f61cb532f8 Merge branch 'main' of github.com:ccfos/nightingale 2024-09-26 15:49:28 +08:00
710leo
34a5a752f4 refactor: update aconf check 2024-09-26 15:49:14 +08:00
Ulric Qin
9be3deeebd update wechat qrcode 2024-09-26 10:14:16 +08:00
710leo
2ceed84120 fix: host alert host filter by busigroup 2024-09-25 15:07:54 +08:00
710leo
8fbe257090 docs: update i18n 2024-09-24 16:27:51 +08:00
710leo
ae35d780c6 refactor: update busigroup del api 2024-09-24 15:49:14 +08:00
710leo
4d2cdfce53 optimize target fill group 2024-09-24 15:29:51 +08:00
710leo
a0e4d0d46e refactor: target bind api 2024-09-24 15:20:21 +08:00
710leo
dd07d04e2f refactor: update target api 2024-09-24 14:37:27 +08:00
710leo
61203e8b75 feat: add boards api 2024-09-24 10:27:43 +08:00
710leo
f24bc53c94 refactor: update target bind group api 2024-09-23 13:13:09 +08:00
710leo
ef6abe3fdc refactor: update target bind api 2024-09-22 23:00:32 +08:00
710leo
461361d3d0 fix: heartbeat api auth check for n9e-edge 2024-09-22 21:05:31 +08:00
710leo
52b3afbd97 fix: recovery event tags map split 2024-09-22 19:14:24 +08:00
710leo
652439bb85 Merge branch 'main' of github.com:ccfos/nightingale 2024-09-22 00:33:40 +08:00
710leo
6f0c13d4e7 fix: edge target cache 2024-09-22 00:33:28 +08:00
ulricqin
c9f46bad02 Remove duplicate fields UseTLS 2024-09-21 20:44:02 +08:00
710leo
75146f3626 docs: add target_busi_group sql 2024-09-20 18:14:15 +08:00
710leo
50aafbd73d refactor: update target query 2024-09-20 18:09:07 +08:00
710leo
b975cb3c9d refactor: update append_labels 2024-09-20 16:24:47 +08:00
flashbo
11deb4ba26 feat: host bind muti group (#2185) 2024-09-19 20:32:08 +08:00
flashbo
ec927297d6 feat:support query alert event by rule id (#2179) 2024-09-19 11:04:14 +08:00
Yening Qin
f476d7cd63 fix: incorrect content in feishucard when sending a large number of messages (#2180) 2024-09-18 18:00:13 +08:00
ulricqin
410f3bbceb Update README.md wechat qrcode 2024-09-18 08:13:42 +08:00
cui fliter
2ad53d6862 refactor: make uids in NotifyTarget (#2169) 2024-09-13 19:26:18 +08:00
710leo
fc392e4af1 docs: update linux metrics tpl 2024-09-13 19:10:33 +08:00
fangpsh
9c83c7881a docs: update oom_kill alert rule tpl (#2170)
Co-authored-by: fangpsh <fangpsh@zego.im>
2024-09-13 19:07:08 +08:00
flashbo
f1259d1dff refactor: alert rule callback url dedup (#2165) 2024-09-13 16:24:04 +08:00
Yening Qin
d9d59b3205 fix: recording rule update (#2168) 2024-09-13 16:20:48 +08:00
Ulric Qin
d11cfb0278 Merge branch 'main' of github.com:ccfos/nightingale 2024-09-09 11:49:37 +08:00
Ulric Qin
5adcfc6eaa update README 2024-09-09 11:49:26 +08:00
710leo
037152ad72 refactor: update alert-cur-event api 2024-09-03 18:17:28 +08:00
Ulric Qin
2de304d4f2 move sqlite.sql to docker dir 2024-09-03 17:51:35 +08:00
Ulric Qin
03c56d048f modify column trigger_value to text 2024-09-03 17:50:13 +08:00
Ulric Qin
1cddb4eca0 Merge branch 'main' of github.com:ccfos/nightingale 2024-09-03 17:46:57 +08:00
Ulric Qin
2dc033944d bugfix InitBuiltinPayloads 2024-09-03 17:46:43 +08:00
flashbo
63e6c78e71 feat: targets support multi idents query (#2119) 2024-09-03 15:32:05 +08:00
Ulric Qin
e1f04eebe7 update README 2024-08-30 17:58:49 +08:00
Yening Qin
ce17e09f66 feat: notify event add target info (#2137)
* fix: tpl center update (#2125)
* put target into alert cur event (#2128)

---------

Co-authored-by: Xu Bin <140785332+Reditiny@users.noreply.github.com>
Co-authored-by: flashbo <36443248+lwb0214@users.noreply.github.com>
2024-08-30 16:28:31 +08:00
710leo
c98c1d3b90 docs: update sql 2024-08-30 16:19:50 +08:00
710leo
ae3218e6d5 fix: target query api 2024-08-30 11:27:16 +08:00
Yening Qin
7497cc0f28 refactor: update alert rule clone api (#2126) 2024-08-28 20:15:25 +08:00
710leo
96c4cc7c98 refactor: import prom rule api 2024-08-28 15:54:39 +08:00
710leo
1f7314f6b4 refactor: sub rule event enable run notify script 2024-08-27 15:48:09 +08:00
yangkaa
86d478a0d4 fix: update notify template failed (#2117)
Signed-off-by: yangk <yangk@goodrain.com>
2024-08-27 11:26:05 +08:00
710leo
b45023630f merge main 2024-08-27 11:14:58 +08:00
710leo
2177049487 refactor: update target tag api 2024-08-27 11:14:20 +08:00
shardingHe
d3d1e7019f docs: update jvm dashboards (SpringBoot Actuator) (#2121) 2024-08-27 10:46:02 +08:00
710leo
f2ad0b9594 refactor: update targets api 2024-08-26 20:34:04 +08:00
Yening Qin
9c79233b3c feat: target add host tags (#2120)
* update target tags (#2091)

---------

Co-authored-by: flashbo <36443248+lwb0214@users.noreply.github.com>
2024-08-26 19:41:38 +08:00
710leo
9ea5de1257 refactor: users get api 2024-08-26 11:56:57 +08:00
ning
3ec97665ac refactor: set e.FirstTriggerTime 2024-08-22 20:18:19 +08:00
ning
bb4eeca2ab code refactor 2024-08-22 15:38:47 +08:00
ning
cc6a5be27f Merge branch 'main' of github.com:ccfos/nightingale 2024-08-22 15:02:32 +08:00
ning
630df8a954 fix: recover event when prom_for_duration is 0 2024-08-22 15:02:26 +08:00
ning
e28ab6368b fix: recover event when prom_for_duration is 0 2024-08-22 14:30:30 +08:00
ulricqin
751c78be4b Update a-n9e.sql 2024-08-22 10:01:04 +08:00
Xu Bin
5311bf90d5 feat: trigger set support operation (#2107) 2024-08-21 20:16:31 +08:00
ning
c464689c6a Merge branch 'main' of github.com:ccfos/nightingale 2024-08-21 19:56:10 +08:00
ning
442426be38 fix: event first trigger time 2024-08-21 19:55:56 +08:00
Yening Qin
9a28139d43 refactor: hostmeta add config (#2112) 2024-08-20 15:26:13 +08:00
710leo
25b768188f refactor: matchTag add strings.TrimSpace 2024-08-19 21:46:22 +08:00
ning
b794b62960 refactor: RecoverAlertCurEventFromDb 2024-08-19 17:33:40 +08:00
flashbo
d7e00a5a49 refactor: import promethues alert rule (improve api) (#2104) 2024-08-16 11:23:53 +08:00
Xu Bin
19e6cfe7d2 feat: generic are supported in alert rule calculation formulas (#2097) 2024-08-15 17:26:24 +08:00
shardingHe
63baa7b6f3 docs: remove process-exporter alerts & dashboards (#2102)
Co-authored-by: shardingHe <wangzihe@flashcat.cloud>
2024-08-15 16:57:41 +08:00
Vicla
407fc90677 update readme_en (#2099) 2024-08-14 12:20:11 +08:00
Dan218
7da4c99d92 feat: Provide Target.AfterFind to automatically calculated fields for Target (#2073) 2024-08-13 19:16:40 +08:00
Xu Bin
6b46e7e83f feat: support clone rules by idents (#2095) 2024-08-13 19:12:31 +08:00
ning
514ccd5f90 Merge branch 'main' of github.com:ccfos/nightingale 2024-08-13 15:11:52 +08:00
ning
4565b80717 fix: edge delete event 2024-08-13 15:11:40 +08:00
Ulric Qin
2bac6588c4 Merge branch 'main' of github.com:ccfos/nightingale 2024-08-13 11:25:42 +08:00
Ulric Qin
fc293cb01c use node_uname_info as dashboard var filter 2024-08-13 11:25:30 +08:00
ning
73f9548242 Merge branch 'main' of github.com:ccfos/nightingale 2024-08-13 10:59:39 +08:00
ning
7c91e51c08 fix: edge record notify 2024-08-13 10:59:17 +08:00
qinguoyi
a4867c406d fix:get configcache return nil need exit (#2094) 2024-08-12 19:52:22 +08:00
103 changed files with 8649 additions and 3250 deletions

View File

@@ -29,15 +29,15 @@
## 夜莺 Nightingale 是什么
夜莺监控是一款开源云原生观测分析工具,采用 All-in-One 的设计理念,集数据采集、可视化、监控告警、数据分析于一体,与云原生生态紧密集成,提供开箱即用的企业级监控分析和告警能力。夜莺于 2020 年 3 月 20 日,在 github 上发布 v1 版本,已累计迭代 100 多个版本。
夜莺监控是一款开源云原生观测分析工具,采用 All-in-One 的设计理念,集数据采集、可视化、监控告警、数据分析于一体,与云原生生态紧密集成,提供开箱即用的企业级监控分析和告警能力。夜莺于 2020 年 3 月 20 日,在 GitHub 上发布 v1 版本,已累计迭代 100 多个版本。
夜莺最初由滴滴开发和开源,并于 2022 年 5 月 11 日捐赠予中国计算机学会开源发展委员会CCF ODC为 CCF ODC 成立后接受捐赠的第一个开源项目。夜莺的核心研发团队,也是 Open-Falcon 项目原核心研发人员,从 2014 年Open-Falcon 是 2014 年开源)算起来,也有 10 年了,只为把监控这个事情做好。
## 快速开始
- 👉[文档中心](https://flashcat.cloud/docs/) | [下载中心](https://flashcat.cloud/download/nightingale/)
- ❤️[报告 Bug](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=&projects=&template=question.yml)
- ℹ️为了提供更快速的访问体验,上述文档和下载站点托管于 [FlashcatCloud](https://flashcat.cloud)
- 👉 [文档中心](https://flashcat.cloud/docs/) | [下载中心](https://flashcat.cloud/download/nightingale/)
- ❤️ [报告 Bug](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=&projects=&template=question.yml)
- 为了提供更快速的访问体验,上述文档和下载站点托管于 [FlashcatCloud](https://flashcat.cloud)
## 功能特点
@@ -50,6 +50,11 @@
## 截图演示
你可以在页面的右上角,切换语言和主题,目前我们支持英语、简体中文、繁体中文。
![语言切换](https://download.flashcat.cloud/ulric/n9e-switch-i18n.png)
即时查询,类似 Prometheus 内置的查询分析页面,做 ad-hoc 查询,夜莺做了一些 UI 优化,同时提供了一些内置 promql 指标,让不太了解 promql 的用户也可以快速查询。
![即时查询](https://download.flashcat.cloud/ulric/20240513103305.png)
@@ -83,15 +88,16 @@
- 报告Bug优先推荐提交[夜莺GitHub Issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml)
- 推荐完整浏览[夜莺文档站点](https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v7/introduction/),了解更多信息
- 推荐搜索关注夜莺公众号,第一时间获取社区动态:`夜莺监控Nightingale`
- 日常问题交流推荐加入[知识星球](https://download.flashcat.cloud/ulric/20240319095409.png),也可以加我微信 `picobyte`,备注:`夜莺加群-<公司>-<姓名>` 拉入微信群,不过研发人员主要是关注 github issue 和星球,微信群关注较少
- 日常问题交流
- QQ群730841964
- [加入微信群](https://download.flashcat.cloud/ulric/20241022141621.png),如果二维码过期了,可以联系我(我的微信:`picobyte`)拉群,备注: `夜莺互助群`
## 广受关注
[![Stargazers over time](https://api.star-history.com/svg?repos=ccfos/nightingale&type=Date)](https://star-history.com/#ccfos/nightingale&Date)
## 社区共建
- ❇️请阅读浏览[夜莺开源项目和社区治理架构草案](./doc/community-governance.md),真诚欢迎每一位用户、开发者、公司以及组织,使用夜莺监控、积极反馈 Bug、提交功能需求、分享最佳实践共建专业、活跃的夜莺开源社区。
- 夜莺贡献者❤️
- ❇️ 请阅读浏览[夜莺开源项目和社区治理架构草案](./doc/community-governance.md),真诚欢迎每一位用户、开发者、公司以及组织,使用夜莺监控、积极反馈 Bug、提交功能需求、分享最佳实践共建专业、活跃的夜莺开源社区。
- ❤️ 夜莺贡献者
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
<img src="https://contrib.rocks/image?repo=ccfos/nightingale" />
</a>

View File

@@ -1,104 +1,113 @@
<p align="center">
<a href="https://github.com/ccfos/nightingale">
<img src="doc/img/Nightingale_L_V.png" alt="nightingale - cloud native monitoring" width="240" /></a>
<img src="doc/img/Nightingale_L_V.png" alt="nightingale - cloud native monitoring" width="100" /></a>
</p>
<p align="center">
<b>Open-source Alert Management Expert, an Integrated Observability Platform</b>
</p>
<p align="center">
<img alt="GitHub latest release" src="https://img.shields.io/github/v/release/ccfos/nightingale"/>
<a href="https://n9e.github.io">
<a href="https://flashcat.cloud/docs/">
<img alt="Docs" src="https://img.shields.io/badge/docs-get%20started-brightgreen"/></a>
<a href="https://hub.docker.com/u/flashcatcloud">
<img alt="Docker pulls" src="https://img.shields.io/docker/pulls/flashcatcloud/nightingale"/></a>
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/ccfos/nightingale">
<img alt="GitHub Repo issues" src="https://img.shields.io/github/issues/ccfos/nightingale">
<img alt="GitHub Repo issues closed" src="https://img.shields.io/github/issues-closed/ccfos/nightingale">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/ccfos/nightingale">
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors-anon/ccfos/nightingale"/></a>
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/ccfos/nightingale">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/ccfos/nightingale">
<br/><img alt="GitHub Repo issues" src="https://img.shields.io/github/issues/ccfos/nightingale">
<img alt="GitHub Repo issues closed" src="https://img.shields.io/github/issues-closed/ccfos/nightingale">
<img alt="GitHub latest release" src="https://img.shields.io/github/v/release/ccfos/nightingale"/>
<img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue"/>
<a href="https://n9e-talk.slack.com/">
<img alt="GitHub contributors" src="https://img.shields.io/badge/join%20slack-%23n9e-brightgreen.svg"/></a>
<img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue"/>
</p>
<p align="center">
An open-source cloud-native monitoring system that is <b>all-in-one</b> <br/>
<b>Out-of-the-box</b>, it integrates data collection, visualization, and monitoring alert <br/>
We recommend upgrading your <b>Prometheus + AlertManager + Grafana</b> combination to Nightingale!
</p>
[English](./README_en.md) | [中文](./README.md)
## What is Nightingale
## Highlighted Features
Nightingale aims to combine the advantages of Prometheus and Grafana. It manages alert rules and visualizes metrics, logs, traces in a beautiful WebUI.
- **Out-of-the-box**
- Supports multiple deployment methods such as **Docker, Helm Chart, and cloud services**, integrates data collection, monitoring, and alerting into one system, and comes with various monitoring dashboards, quick views, and alert rule templates. **It greatly reduces the construction cost, learning cost, and usage cost of cloud-native monitoring systems**.
- **Professional Alerting**
- Provides visual alert configuration and management, supports various alert rules, offers the ability to configure silence and subscription rules, supports multiple alert delivery channels, and has features such as alert self-healing and event management.
- **Cloud-Native**
- Quickly builds an enterprise-level cloud-native monitoring system through a turnkey approach, supports multiple collectors such as [Categraf](https://github.com/flashcatcloud/categraf), Telegraf, and Grafana-agent, supports multiple data sources such as Prometheus, VictoriaMetrics, M3DB, ElasticSearch, and Jaeger, and is compatible with importing Grafana dashboards. **It seamlessly integrates with the cloud-native ecosystem**.
- **High Performance and High Availability**
- Due to the multi-data-source management engine of Nightingale and its excellent architecture design, and utilizing a high-performance time-series database, it can handle data collection, storage, and alert analysis scenarios with billions of time-series data, saving a lot of costs.
- Nightingale components can be horizontally scaled with no single point of failure. It has been deployed in thousands of enterprises and tested in harsh production practices. Many leading Internet companies have used Nightingale for cluster machines with hundreds of nodes, processing billions of time-series data.
- **Flexible Extension and Centralized Management**
- Nightingale can be deployed on a 1-core 1G cloud host, deployed in a cluster of hundreds of machines, or run in Kubernetes. Time-series databases, alert engines, and other components can also be decentralized to various data centers and regions, balancing edge deployment with centralized management. **It solves the problem of data fragmentation and lack of unified views**.
Originally developed and open-sourced by Didi, Nightingale was donated to the China Computer Federation Open Source Development Committee (CCF ODC) on May 11, 2022, becoming the first open-source project accepted by the CCF ODC after its establishment.
#### If you are using Prometheus and have one or more of the following requirement scenarios, it is recommended that you upgrade to Nightingale:
## Quick Start
- Multiple systems such as Prometheus, Alertmanager, Grafana, etc. are fragmented and lack a unified view and cannot be used out of the box;
- The way to manage Prometheus and Alertmanager by modifying configuration files has a big learning curve and is difficult to collaborate;
- Too much data to scale-up your Prometheus cluster;
- Multiple Prometheus clusters running in production environments, which faced high management and usage costs;
- 👉 [Documentation](https://flashcat.cloud/docs/) | [Download](https://flashcat.cloud/download/nightingale/)
- ❤️ [Report a Bug](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=&projects=&template=question.yml)
- For faster access, the above documentation and download sites are hosted on [FlashcatCloud](https://flashcat.cloud).
#### If you are using Zabbix and have the following scenarios, it is recommended that you upgrade to Nightingale:
## Features
- Monitoring too much data and wanting a better scalable solution;
- A high learning curve and a desire for better efficiency of collaborative use in a multi-person, multi-team model;
- Microservice and cloud-native architectures with variable monitoring data lifecycles and high monitoring data dimension bases, which are not easily adaptable to the Zabbix data model;
- **Integration with Multiple Time-Series Databases:** Supports integration with various time-series databases such as Prometheus, VictoriaMetrics, Thanos, Mimir, M3DB, and TDengine, enabling unified alert management.
- **Advanced Alerting Capabilities:** Comes with built-in support for multiple alerting rules, extensible to common notification channels. It also supports alert suppression, silencing, subscription, self-healing, and alert event management.
- **High-Performance Visualization Engine:** Offers various chart styles with numerous built-in dashboard templates and the ability to import Grafana templates. Ready to use with a business-friendly open-source license.
- **Support for Common Collectors:** Compatible with [Categraf](https://flashcat.cloud/product/categraf), Telegraf, Grafana-agent, Datadog-agent, and various exporters as collectors—there's no data that can't be monitored.
- **Seamless Integration with [Flashduty](https://flashcat.cloud/product/flashcat-duty/):** Enables alert aggregation, acknowledgment, escalation, scheduling, and IM integration, ensuring no alerts are missed, reducing unnecessary interruptions, and enhancing efficient collaboration.
#### If you are using [open-falcon](https://github.com/open-falcon/falcon-plus), we recommend you to upgrade to Nightingale
- For more information about open-falcon and Nightingale, please refer to read [Ten features and trends of cloud-native monitoring](https://mp.weixin.qq.com/s?__biz=MzkzNjI5OTM5Nw==&mid=2247483738&idx=1&sn=e8bdbb974a2cd003c1abcc2b5405dd18&chksm=c2a19fb0f5d616a63185cd79277a79a6b80118ef2185890d0683d2bb20451bd9303c78d083c5#rd)。
## Getting Started
[https://n9e.github.io/](https://n9e.github.io/)
## Screenshots
https://user-images.githubusercontent.com/792850/216888712-2565fcea-9df5-47bd-a49e-d60af9bd76e8.mp4
You can switch languages and themes in the top right corner. We now support English, Simplified Chinese, and Traditional Chinese.
![18n switch](https://download.flashcat.cloud/ulric/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)
### 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)
### 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 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)
## Architecture
<img src="doc/img/arch-product.png" width="600">
In most community scenarios, Nightingale is primarily used as an alert engine, integrating with multiple time-series databases to unify alert rule management. Grafana remains the preferred tool for visualization. As an alert engine, the product architecture of Nightingale is as follows:
Nightingale monitoring can receive monitoring data reported by various collectors (such as [Categraf](https://github.com/flashcatcloud/categraf) , telegraf, grafana-agent, Prometheus, etc.) and write them to various popular time-series databases (such as Prometheus, M3DB, VictoriaMetrics, Thanos, TDEngine, etc.). It provides configuration capabilities for alert rules, silence rules, and subscription rules, as well as the ability to view monitoring data. It also provides automatic alarm self-healing mechanisms (such as automatically calling back to a webhook address or executing a script after an alarm is triggered), and the ability to store and manage historical alarm events and view them in groups.
![Product Architecture](https://download.flashcat.cloud/ulric/20240221152601.png)
If the performance of a standalone time-series database (such as Prometheus) has bottlenecks or poor disaster recovery, we recommend using [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics). The VictoriaMetrics architecture is relatively simple, has excellent performance, and is easy to deploy and maintain. The architecture diagram is as shown above. For more detailed documentation on VictoriaMetrics, please refer to its [official website](https://victoriametrics.com/).
For certain edge data centers with poor network connectivity to the central Nightingale server, we offer a distributed deployment mode for the alert engine. In this mode, even if the network is disconnected, the alerting functionality remains unaffected.
**We welcome you to participate in the Nightingale open-source project and community in various ways, including but not limited to**
- Adding and improving documentation => [n9e.github.io](https://n9e.github.io/)
- Sharing your best practices and experience in using Nightingale monitoring => [Article sharing]((https://n9e.github.io/docs/prologue/share/))
- Submitting product suggestions => [github issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md)
- Submitting code to make Nightingale monitoring faster, more stable, and easier to use => [github pull request](https://github.com/didi/nightingale/pulls)
![Edge Deployment Mode](https://download.flashcat.cloud/ulric/20240222102119.png)
**Respecting, recognizing, and recording the work of every contributor** is the first guiding principle of the Nightingale open-source community. We advocate effective questioning, which not only respects the developer's time but also contributes to the accumulation of knowledge in the entire community
- Before asking a question, please first refer to the [FAQ](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq)
- We use [GitHub Discussions](https://github.com/ccfos/nightingale/discussions) as the communication forum. You can search and ask questions here.
- We also recommend that you join ours [Slack channel](https://n9e-talk.slack.com/) to exchange experiences with other Nightingale users.
## Communication Channels
## Who is using Nightingale
You can register your usage and share your experience by posting on **[Who is Using Nightingale](https://github.com/ccfos/nightingale/issues/897)**.
- **Report Bugs:** It is highly recommended to submit issues via the [Nightingale GitHub Issue tracker](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml).
- **Documentation:** For more information, we recommend thoroughly browsing the [Nightingale Documentation Site](https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v7/introduction/).
## Stargazers over time
[![Stargazers over time](https://starchart.cc/ccfos/nightingale.svg)](https://starchart.cc/ccfos/nightingale)
## Contributors
[![Stargazers over time](https://api.star-history.com/svg?repos=ccfos/nightingale&type=Date)](https://star-history.com/#ccfos/nightingale&Date)
## Community Co-Building
- ❇️ Please read the [Nightingale Open Source Project and Community Governance Draft](./doc/community-governance.md). We sincerely welcome every user, developer, company, and organization to use Nightingale, actively report bugs, submit feature requests, share best practices, and help build a professional and active open-source community.
- ❤️ Nightingale Contributors
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
<img src="https://contrib.rocks/image?repo=ccfos/nightingale" />
</a>
## License
[Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)
- [Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)

View File

@@ -60,10 +60,6 @@ func (a *Alert) PreCheck(configDir string) {
a.Heartbeat.Interval = 1000
}
if a.Heartbeat.EngineName == "" {
a.Heartbeat.EngineName = "default"
}
if a.EngineDelay == 0 {
a.EngineDelay = 30
}

View File

@@ -62,6 +62,7 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
userCache := memsto.NewUserCache(ctx, syncStats)
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
taskTplsCache := memsto.NewTaskTplCache(ctx)
configCvalCache := memsto.NewCvalCache(ctx, syncStats)
promClients := prom.NewPromClient(ctx)
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
@@ -70,7 +71,8 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP,
configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
rt := router.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
if config.Ibex.Enable {

View File

@@ -5,18 +5,20 @@ import (
"math"
"strings"
"github.com/ccfos/nightingale/v6/models"
"github.com/prometheus/common/model"
)
type AnomalyPoint struct {
Key string `json:"key"`
Labels model.Metric `json:"labels"`
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value"`
Severity int `json:"severity"`
Triggered bool `json:"triggered"`
Query string `json:"query"`
Values string `json:"values"`
Key string `json:"key"`
Labels model.Metric `json:"labels"`
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value"`
Severity int `json:"severity"`
Triggered bool `json:"triggered"`
Query string `json:"query"`
Values string `json:"values"`
RecoverConfig models.RecoverConfig `json:"recover_config"`
}
func NewAnomalyPoint(key string, labels map[string]string, ts int64, value float64, severity int) AnomalyPoint {

View File

@@ -2,6 +2,7 @@ package common
import (
"fmt"
"strings"
"github.com/ccfos/nightingale/v6/models"
)
@@ -34,9 +35,9 @@ func MatchGroupsName(groupName string, groupFilter []models.TagFilter) bool {
func matchTag(value string, filter models.TagFilter) bool {
switch filter.Func {
case "==":
return filter.Value == value
return strings.TrimSpace(filter.Value) == strings.TrimSpace(value)
case "!=":
return filter.Value != value
return strings.TrimSpace(filter.Value) != strings.TrimSpace(value)
case "in":
_, has := filter.Vset[value]
return has

View File

@@ -272,12 +272,13 @@ func (e *Dispatch) Send(rule *models.AlertRule, event *models.AlertCurEvent, not
sender.SingleSendWebhooks(e.ctx, notifyTarget.ToWebhookList(), event, e.Astats)
}
// handle plugin call
go sender.MayPluginNotify(e.ctx, e.genNoticeBytes(event), e.notifyConfigCache.
GetNotifyScript(), e.Astats, event)
if !isSubscribe {
// handle ibex callbacks
e.HandleIbex(rule, event)
// handle plugin call
go sender.MayPluginNotify(e.ctx, e.genNoticeBytes(event), e.notifyConfigCache.
GetNotifyScript(), e.Astats, event)
}
}
@@ -285,6 +286,7 @@ func (e *Dispatch) SendCallbacks(rule *models.AlertRule, notifyTarget *NotifyTar
uids := notifyTarget.ToUidList()
urls := notifyTarget.ToCallbackList()
whMap := notifyTarget.ToWebhookMap()
for _, urlStr := range urls {
if len(urlStr) == 0 {
continue
@@ -292,6 +294,11 @@ func (e *Dispatch) SendCallbacks(rule *models.AlertRule, notifyTarget *NotifyTar
cbCtx := sender.BuildCallBackContext(e.ctx, urlStr, rule, []*models.AlertCurEvent{event}, uids, e.userCache, e.alerting.WebhookBatchSend, e.Astats)
if wh, ok := whMap[cbCtx.CallBackURL]; ok && wh.Enable {
logger.Debugf("SendCallbacks: webhook[%s] is in global conf.", cbCtx.CallBackURL)
continue
}
if strings.HasPrefix(urlStr, "${ibex}") {
e.CallBacks[models.IbexDomain].CallBack(cbCtx)
continue

View File

@@ -100,8 +100,32 @@ func (s *NotifyTarget) ToWebhookList() []*models.Webhook {
return webhooks
}
func (s *NotifyTarget) ToWebhookMap() map[string]*models.Webhook {
webhookMap := make(map[string]*models.Webhook, len(s.webhooks))
for _, wh := range s.webhooks {
if wh.Batch == 0 {
wh.Batch = 1000
}
if wh.Timeout == 0 {
wh.Timeout = 10
}
if wh.RetryCount == 0 {
wh.RetryCount = 10
}
if wh.RetryInterval == 0 {
wh.RetryInterval = 10
}
webhookMap[wh.Url] = wh
}
return webhookMap
}
func (s *NotifyTarget) ToUidList() []int64 {
uids := make([]int64, len(s.userMap))
uids := make([]int64, 0, len(s.userMap))
for uid, _ := range s.userMap {
uids = append(uids, uid)
}

View File

@@ -3,8 +3,10 @@ package eval
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"reflect"
"sort"
"strings"
"time"
@@ -46,6 +48,14 @@ const (
QUERY_DATA = "query_data"
)
type JoinType string
const (
Left JoinType = "left"
Right JoinType = "right"
Inner JoinType = "inner"
)
func NewAlertRuleWorker(rule *models.AlertRule, datasourceId int64, processor *process.Processor, promClients *prom.PromClientMap, tdengineClients *tdengine.TdengineClientMap, ctx *ctx.Context) *AlertRuleWorker {
arw := &AlertRuleWorker{
datasourceId: datasourceId,
@@ -108,21 +118,29 @@ func (arw *AlertRuleWorker) Eval() {
arw.processor.Stats.CounterRuleEval.WithLabelValues().Inc()
typ := cachedRule.GetRuleType()
var anomalyPoints []common.AnomalyPoint
var recoverPoints []common.AnomalyPoint
var (
anomalyPoints []common.AnomalyPoint
recoverPoints []common.AnomalyPoint
err error
)
switch typ {
case models.PROMETHEUS:
anomalyPoints = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
anomalyPoints, err = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
case models.HOST:
anomalyPoints = arw.GetHostAnomalyPoint(cachedRule.RuleConfig)
anomalyPoints, err = arw.GetHostAnomalyPoint(cachedRule.RuleConfig)
case models.TDENGINE:
anomalyPoints, recoverPoints = arw.GetTdengineAnomalyPoint(cachedRule, arw.processor.DatasourceId())
anomalyPoints, recoverPoints, err = arw.GetTdengineAnomalyPoint(cachedRule, arw.processor.DatasourceId())
case models.LOKI:
anomalyPoints = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
anomalyPoints, err = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
default:
return
}
if err != nil {
logger.Errorf("rule_eval:%s get anomaly point err:%s", arw.Key(), err.Error())
return
}
if arw.processor == nil {
logger.Warningf("rule_eval:%s processor is nil", arw.Key())
return
@@ -152,13 +170,13 @@ func (arw *AlertRuleWorker) Eval() {
now := time.Now().Unix()
for _, point := range pointsMap {
str := fmt.Sprintf("%v", point.Value)
arw.processor.RecoverSingle(process.Hash(cachedRule.Id, arw.processor.DatasourceId(), point), now, &str)
arw.processor.RecoverSingle(true, process.Hash(cachedRule.Id, arw.processor.DatasourceId(), point), now, &str)
}
} else {
now := time.Now().Unix()
for _, point := range recoverPoints {
str := fmt.Sprintf("%v", point.Value)
arw.processor.RecoverSingle(process.Hash(cachedRule.Id, arw.processor.DatasourceId(), point), now, &str)
arw.processor.RecoverSingle(true, process.Hash(cachedRule.Id, arw.processor.DatasourceId(), point), now, &str)
}
}
@@ -170,7 +188,7 @@ func (arw *AlertRuleWorker) Stop() {
close(arw.quit)
}
func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) []common.AnomalyPoint {
func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) ([]common.AnomalyPoint, error) {
var lst []common.AnomalyPoint
var severity int
@@ -178,13 +196,13 @@ func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) []common.Anom
if err := json.Unmarshal([]byte(ruleConfig), &rule); err != nil {
logger.Errorf("rule_eval:%s rule_config:%s, error:%v", arw.Key(), ruleConfig, err)
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
return lst
return lst, err
}
if rule == nil {
logger.Errorf("rule_eval:%s rule_config:%s, error:rule is nil", arw.Key(), ruleConfig)
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
return lst
return lst, errors.New("rule is nil")
}
arw.inhibit = rule.Inhibit
@@ -215,7 +233,7 @@ func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) []common.Anom
logger.Errorf("rule_eval:%s promql:%s, error:%v", arw.Key(), promql, err)
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), QUERY_DATA).Inc()
continue
return lst, err
}
if len(warnings) > 0 {
@@ -232,10 +250,10 @@ func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) []common.Anom
}
lst = append(lst, points...)
}
return lst
return lst, nil
}
func (arw *AlertRuleWorker) GetTdengineAnomalyPoint(rule *models.AlertRule, dsId int64) ([]common.AnomalyPoint, []common.AnomalyPoint) {
func (arw *AlertRuleWorker) GetTdengineAnomalyPoint(rule *models.AlertRule, dsId int64) ([]common.AnomalyPoint, []common.AnomalyPoint, error) {
// 获取查询和规则判断条件
points := []common.AnomalyPoint{}
recoverPoints := []common.AnomalyPoint{}
@@ -243,7 +261,7 @@ func (arw *AlertRuleWorker) GetTdengineAnomalyPoint(rule *models.AlertRule, dsId
if ruleConfig == "" {
logger.Warningf("rule_eval:%d promql is blank", rule.Id)
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
return points, recoverPoints
return points, recoverPoints, errors.New("rule config is nil")
}
var ruleQuery models.RuleQuery
@@ -252,15 +270,18 @@ func (arw *AlertRuleWorker) GetTdengineAnomalyPoint(rule *models.AlertRule, dsId
logger.Warningf("rule_eval:%d promql parse error:%s", rule.Id, err.Error())
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId())).Inc()
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
return points, recoverPoints
return points, recoverPoints, err
}
arw.inhibit = ruleQuery.Inhibit
if len(ruleQuery.Queries) > 0 {
seriesStore := make(map[uint64]models.DataResp)
seriesTagIndex := make(map[uint64][]uint64)
// 将不同查询的 hash 索引分组存放
seriesTagIndexes := make(map[string]map[uint64][]uint64)
for _, query := range ruleQuery.Queries {
seriesTagIndex := make(map[uint64][]uint64)
arw.processor.Stats.CounterQueryDataTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
cli := arw.tdengineClients.GetCli(dsId)
if cli == nil {
@@ -276,21 +297,27 @@ func (arw *AlertRuleWorker) GetTdengineAnomalyPoint(rule *models.AlertRule, dsId
logger.Warningf("rule_eval rid:%d query data error: %v", rule.Id, err)
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), QUERY_DATA).Inc()
continue
return points, recoverPoints, err
}
// 此条日志很重要,是告警判断的现场值
logger.Debugf("rule_eval rid:%d req:%+v resp:%+v", rule.Id, query, series)
MakeSeriesMap(series, seriesTagIndex, seriesStore)
ref, err := GetQueryRef(query)
if err != nil {
logger.Warningf("rule_eval rid:%d query ref error: %v query:%+v", rule.Id, err, query)
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
continue
}
seriesTagIndexes[ref] = seriesTagIndex
}
points, recoverPoints = GetAnomalyPoint(rule.Id, ruleQuery, seriesTagIndex, seriesStore)
points, recoverPoints = GetAnomalyPoint(rule.Id, ruleQuery, seriesTagIndexes, seriesStore)
}
return points, recoverPoints
return points, recoverPoints, nil
}
func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.AnomalyPoint {
func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) ([]common.AnomalyPoint, error) {
var lst []common.AnomalyPoint
var severity int
@@ -298,13 +325,13 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.Anom
if err := json.Unmarshal([]byte(ruleConfig), &rule); err != nil {
logger.Errorf("rule_eval:%s rule_config:%s, error:%v", arw.Key(), ruleConfig, err)
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
return lst
return lst, err
}
if rule == nil {
logger.Errorf("rule_eval:%s rule_config:%s, error:rule is nil", arw.Key(), ruleConfig)
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
return lst
return lst, errors.New("rule is nil")
}
arw.inhibit = rule.Inhibit
@@ -358,11 +385,6 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.Anom
}
m["ident"] = target.Ident
bg := arw.processor.BusiGroupCache.GetByBusiGroupId(target.GroupId)
if bg != nil && bg.LabelEnable == 1 {
m["busigroup"] = bg.LabelValue
}
lst = append(lst, common.NewAnomalyPoint(trigger.Type, m, now, float64(now-target.UpdateAt), trigger.Severity))
}
case "offset":
@@ -411,11 +433,6 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.Anom
}
m["ident"] = host
bg := arw.processor.BusiGroupCache.GetByBusiGroupId(target.GroupId)
if bg != nil && bg.LabelEnable == 1 {
m["busigroup"] = bg.LabelValue
}
lst = append(lst, common.NewAnomalyPoint(trigger.Type, m, now, float64(offset), trigger.Severity))
}
case "pct_target_miss":
@@ -441,20 +458,31 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.Anom
}
}
}
return lst
return lst, nil
}
func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndex map[uint64][]uint64, seriesStore map[uint64]models.DataResp) ([]common.AnomalyPoint, []common.AnomalyPoint) {
func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndexes map[string]map[uint64][]uint64, seriesStore map[uint64]models.DataResp) ([]common.AnomalyPoint, []common.AnomalyPoint) {
points := []common.AnomalyPoint{}
recoverPoints := []common.AnomalyPoint{}
if len(ruleQuery.Triggers) == 0 {
return points, recoverPoints
}
if len(seriesTagIndexes) == 0 {
return points, recoverPoints
}
for _, trigger := range ruleQuery.Triggers {
// seriesTagIndex 的 key 仅做分组使用value 为每组 series 的 hash
seriesTagIndex := ProcessJoins(ruleId, trigger, seriesTagIndexes, seriesStore)
for _, seriesHash := range seriesTagIndex {
sort.Slice(seriesHash, func(i, j int) bool {
return seriesHash[i] < seriesHash[j]
})
m := make(map[string]float64)
m := make(map[string]interface{})
var ts int64
var sample models.DataResp
var value float64
@@ -494,23 +522,37 @@ func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndex ma
}
point := common.AnomalyPoint{
Key: sample.MetricName(),
Labels: sample.Metric,
Timestamp: int64(ts),
Value: value,
Values: values,
Severity: trigger.Severity,
Triggered: isTriggered,
Query: fmt.Sprintf("query:%+v trigger:%+v", ruleQuery.Queries, trigger),
Key: sample.MetricName(),
Labels: sample.Metric,
Timestamp: int64(ts),
Value: value,
Values: values,
Severity: trigger.Severity,
Triggered: isTriggered,
Query: fmt.Sprintf("query:%+v trigger:%+v", ruleQuery.Queries, trigger),
RecoverConfig: trigger.RecoverConfig,
}
if sample.Query != "" {
point.Query = sample.Query
}
// 恢复条件判断经过讨论是只在表达式模式下支持,表达式模式会通过 isTriggered 判断是告警点还是恢复点
// 1. 不设置恢复判断,满足恢复条件产生 recoverPoint 恢复,无数据不产生 anomalyPoint 恢复
// 2. 设置满足条件才恢复,仅可通过产生 recoverPoint 恢复,不能通过不产生 anomalyPoint 恢复
// 3. 设置无数据不恢复,仅可通过产生 recoverPoint 恢复,不产生 anomalyPoint 恢复
if isTriggered {
points = append(points, point)
} else {
switch trigger.RecoverConfig.JudgeType {
case models.Origin:
// 对齐原实现 do nothing
case models.RecoverOnCondition:
// 额外判断恢复条件,满足才恢复
fulfill := parser.Calc(trigger.RecoverConfig.RecoverExp, m)
if !fulfill {
continue
}
}
recoverPoints = append(recoverPoints, point)
}
}
@@ -519,6 +561,144 @@ func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndex ma
return points, recoverPoints
}
func flatten(rehashed map[uint64][][]uint64) map[uint64][]uint64 {
seriesTagIndex := make(map[uint64][]uint64)
var i uint64
for _, HashTagIndex := range rehashed {
for u := range HashTagIndex {
seriesTagIndex[i] = HashTagIndex[u]
i++
}
}
return seriesTagIndex
}
// onJoin 组合两个经过 rehash 之后的集合
// 如查询 A经过 on data_base rehash 分组后
// [[A1{data_base=1, table=alert}A2{data_base=1, table=alert}][A5{data_base=1, table=board}]]
// [[A3{data_base=2, table=board}][A4{data_base=2, table=alert}]]
// 查询 B经过 on data_base rehash 分组后
// [[B1{data_base=1, table=alert}]]
// [[B2{data_base=2, table=alert}]]
// 内联得到
// [[A1{data_base=1, table=alert}A2{data_base=1, table=alert}B1{data_base=1, table=alert}][A5{data_base=1, table=board}[B1{data_base=1, table=alert}]]
// [[A3{data_base=2, table=board}B2{data_base=2, table=alert}][A4{data_base=2, table=alert}B2{data_base=2, table=alert}]]
func onJoin(reHashTagIndex1 map[uint64][][]uint64, reHashTagIndex2 map[uint64][][]uint64, joinType JoinType) map[uint64][][]uint64 {
reHashTagIndex := make(map[uint64][][]uint64)
for rehash := range reHashTagIndex1 {
if _, ok := reHashTagIndex2[rehash]; ok {
// 若有 rehash 相同的记录,两两合并
for i1 := range reHashTagIndex1[rehash] {
for i2 := range reHashTagIndex2[rehash] {
reHashTagIndex[rehash] = append(reHashTagIndex[rehash], mergeNewArray(reHashTagIndex1[rehash][i1], reHashTagIndex2[rehash][i2]))
}
}
} else {
// 合并方式不为 inner 时,需要保留 reHashTagIndex1 中未匹配的记录
if joinType != Inner {
reHashTagIndex[rehash] = reHashTagIndex1[rehash]
}
}
}
return reHashTagIndex
}
// rehashSet 重新 hash 分组
// 如当前查询 A 有五条记录
// A1{data_base=1, table=alert}
// A2{data_base=1, table=alert}
// A3{data_base=2, table=board}
// A4{data_base=2, table=alert}
// A5{data_base=1, table=board}
// 经过预处理(按曲线分组,此步已在进入 GetAnomalyPoint 函数前完成)后,分为 4 组,
// [A1{data_base=1, table=alert}A2{data_base=1, table=alert}]
// [A3{data_base=2, table=board}]
// [A4{data_base=2, table=alert}]
// [A5{data_base=1, table=board}]
// 若 rehashSet 按 data_base 重新分组,此时会得到按 rehash 值分的二维数组,即不会将 rehash 值相同的记录完全合并
// [[A1{data_base=1, table=alert}A2{data_base=1, table=alert}][A5{data_base=1, table=board}]]
// [[A3{data_base=2, table=board}][A4{data_base=2, table=alert}]]
func rehashSet(seriesTagIndex1 map[uint64][]uint64, seriesStore map[uint64]models.DataResp, on []string) map[uint64][][]uint64 {
reHashTagIndex := make(map[uint64][][]uint64)
for _, seriesHashes := range seriesTagIndex1 {
if len(seriesHashes) == 0 {
continue
}
series, exists := seriesStore[seriesHashes[0]]
if !exists {
continue
}
rehash := hash.GetTargetTagHash(series.Metric, on)
if _, ok := reHashTagIndex[rehash]; !ok {
reHashTagIndex[rehash] = make([][]uint64, 0)
}
reHashTagIndex[rehash] = append(reHashTagIndex[rehash], seriesHashes)
}
return reHashTagIndex
}
// 笛卡尔积,查询的结果两两合并
func cartesianJoin(seriesTagIndex1 map[uint64][]uint64, seriesTagIndex2 map[uint64][]uint64) map[uint64][]uint64 {
var index uint64
seriesTagIndex := make(map[uint64][]uint64)
for _, seriesHashes1 := range seriesTagIndex1 {
for _, seriesHashes2 := range seriesTagIndex2 {
seriesTagIndex[index] = mergeNewArray(seriesHashes1, seriesHashes2)
index++
}
}
return seriesTagIndex
}
// noneJoin 直接拼接
func noneJoin(seriesTagIndex1 map[uint64][]uint64, seriesTagIndex2 map[uint64][]uint64) map[uint64][]uint64 {
seriesTagIndex := make(map[uint64][]uint64)
var index uint64
for _, seriesHashes := range seriesTagIndex1 {
seriesTagIndex[index] = seriesHashes
index++
}
for _, seriesHashes := range seriesTagIndex2 {
seriesTagIndex[index] = seriesHashes
index++
}
return seriesTagIndex
}
// originalJoin 原始分组方案key 相同,即标签全部相同分为一组
func originalJoin(seriesTagIndex1 map[uint64][]uint64, seriesTagIndex2 map[uint64][]uint64) map[uint64][]uint64 {
seriesTagIndex := make(map[uint64][]uint64)
for tagHash, seriesHashes := range seriesTagIndex1 {
if _, ok := seriesTagIndex[tagHash]; !ok {
seriesTagIndex[tagHash] = mergeNewArray(seriesHashes)
} else {
seriesTagIndex[tagHash] = append(seriesTagIndex[tagHash], seriesHashes...)
}
}
for tagHash, seriesHashes := range seriesTagIndex2 {
if _, ok := seriesTagIndex[tagHash]; !ok {
seriesTagIndex[tagHash] = mergeNewArray(seriesHashes)
} else {
seriesTagIndex[tagHash] = append(seriesTagIndex[tagHash], seriesHashes...)
}
}
return seriesTagIndex
}
// exclude 左斥,留下在 reHashTagIndex1 中,但不在 reHashTagIndex2 中的记录
func exclude(reHashTagIndex1 map[uint64][][]uint64, reHashTagIndex2 map[uint64][][]uint64) map[uint64][][]uint64 {
reHashTagIndex := make(map[uint64][][]uint64)
for rehash, _ := range reHashTagIndex1 {
if _, ok := reHashTagIndex2[rehash]; !ok {
reHashTagIndex[rehash] = reHashTagIndex1[rehash]
}
}
return reHashTagIndex
}
func MakeSeriesMap(series []models.DataResp, seriesTagIndex map[uint64][]uint64, seriesStore map[uint64]models.DataResp) {
for i := 0; i < len(series); i++ {
serieHash := hash.GetHash(series[i].Metric, series[i].Ref)
@@ -532,3 +712,108 @@ func MakeSeriesMap(series []models.DataResp, seriesTagIndex map[uint64][]uint64,
seriesTagIndex[tagHash] = append(seriesTagIndex[tagHash], serieHash)
}
}
func mergeNewArray(arg ...[]uint64) []uint64 {
res := make([]uint64, 0)
for _, a := range arg {
res = append(res, a...)
}
return res
}
func ProcessJoins(ruleId int64, trigger models.Trigger, seriesTagIndexes map[string]map[uint64][]uint64, seriesStore map[uint64]models.DataResp) map[uint64][]uint64 {
last := make(map[uint64][]uint64)
if len(seriesTagIndexes) == 0 {
return last
}
if len(trigger.Joins) == 0 {
idx := 0
for _, seriesTagIndex := range seriesTagIndexes {
if idx == 0 {
last = seriesTagIndex
} else {
last = originalJoin(last, seriesTagIndex)
}
idx++
}
return last
}
// 有 join 条件,按条件依次合并
if len(seriesTagIndexes) < len(trigger.Joins)+1 {
logger.Errorf("rule_eval rid:%d queries' count: %d not match join condition's count: %d", ruleId, len(seriesTagIndexes), len(trigger.Joins))
return nil
}
last = seriesTagIndexes[trigger.JoinRef]
lastRehashed := rehashSet(last, seriesStore, trigger.Joins[0].On)
for i := range trigger.Joins {
cur := seriesTagIndexes[trigger.Joins[i].Ref]
switch trigger.Joins[i].JoinType {
case "original":
last = originalJoin(last, cur)
case "none":
last = noneJoin(last, cur)
case "cartesian":
last = cartesianJoin(last, cur)
case "inner_join":
curRehashed := rehashSet(cur, seriesStore, trigger.Joins[i].On)
lastRehashed = onJoin(lastRehashed, curRehashed, Inner)
last = flatten(lastRehashed)
case "left_join":
curRehashed := rehashSet(cur, seriesStore, trigger.Joins[i].On)
lastRehashed = onJoin(lastRehashed, curRehashed, Left)
last = flatten(lastRehashed)
case "right_join":
curRehashed := rehashSet(cur, seriesStore, trigger.Joins[i].On)
lastRehashed = onJoin(curRehashed, lastRehashed, Right)
last = flatten(lastRehashed)
case "left_exclude":
curRehashed := rehashSet(cur, seriesStore, trigger.Joins[i].On)
lastRehashed = exclude(lastRehashed, curRehashed)
last = flatten(lastRehashed)
case "right_exclude":
curRehashed := rehashSet(cur, seriesStore, trigger.Joins[i].On)
lastRehashed = exclude(curRehashed, lastRehashed)
last = flatten(lastRehashed)
default:
logger.Warningf("rule_eval rid:%d join type:%s not support", ruleId, trigger.Joins[i].JoinType)
}
}
return last
}
func GetQueryRef(query interface{}) (string, error) {
// 首先检查是否为 map
if m, ok := query.(map[string]interface{}); ok {
if ref, exists := m["ref"]; exists {
if refStr, ok := ref.(string); ok {
return refStr, nil
}
return "", fmt.Errorf("ref 字段不是字符串类型")
}
return "", fmt.Errorf("query 中没有找到 ref 字段")
}
// 如果不是 map则按原来的方式处理结构体
v := reflect.ValueOf(query)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return "", fmt.Errorf("query not a struct or map")
}
refField := v.FieldByName("Ref")
if !refField.IsValid() {
return "", fmt.Errorf("not find ref field")
}
if refField.Kind() != reflect.String {
return "", fmt.Errorf("ref not a string")
}
return refField.String(), nil
}

271
alert/eval/eval_test.go Normal file
View File

@@ -0,0 +1,271 @@
package eval
import (
"reflect"
"testing"
"golang.org/x/exp/slices"
)
var (
reHashTagIndex1 = map[uint64][][]uint64{
1: {
{1, 2}, {3, 4},
},
2: {
{5, 6}, {7, 8},
},
}
reHashTagIndex2 = map[uint64][][]uint64{
1: {
{9, 10}, {11, 12},
},
3: {
{13, 14}, {15, 16},
},
}
seriesTagIndex1 = map[uint64][]uint64{
1: {1, 2, 3, 4},
2: {5, 6, 7, 8},
}
seriesTagIndex2 = map[uint64][]uint64{
1: {9, 10, 11, 12},
3: {13, 14, 15, 16},
}
)
func Test_originalJoin(t *testing.T) {
type args struct {
seriesTagIndex1 map[uint64][]uint64
seriesTagIndex2 map[uint64][]uint64
}
tests := []struct {
name string
args args
want map[uint64][]uint64
}{
{
name: "original join",
args: args{
seriesTagIndex1: map[uint64][]uint64{
1: {1, 2, 3, 4},
2: {5, 6, 7, 8},
},
seriesTagIndex2: map[uint64][]uint64{
1: {9, 10, 11, 12},
3: {13, 14, 15, 16},
},
},
want: map[uint64][]uint64{
1: {1, 2, 3, 4, 9, 10, 11, 12},
2: {5, 6, 7, 8},
3: {13, 14, 15, 16},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := originalJoin(tt.args.seriesTagIndex1, tt.args.seriesTagIndex2); !reflect.DeepEqual(got, tt.want) {
t.Errorf("originalJoin() = %v, want %v", got, tt.want)
}
})
}
}
func Test_exclude(t *testing.T) {
type args struct {
reHashTagIndex1 map[uint64][][]uint64
reHashTagIndex2 map[uint64][][]uint64
}
tests := []struct {
name string
args args
want map[uint64][]uint64
}{
{
name: "left exclude",
args: args{
reHashTagIndex1: reHashTagIndex1,
reHashTagIndex2: reHashTagIndex2,
},
want: map[uint64][]uint64{
0: {5, 6},
1: {7, 8},
},
},
{
name: "right exclude",
args: args{
reHashTagIndex1: reHashTagIndex2,
reHashTagIndex2: reHashTagIndex1,
},
want: map[uint64][]uint64{
3: {13, 14},
4: {15, 16},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := exclude(tt.args.reHashTagIndex1, tt.args.reHashTagIndex2); !allValueDeepEqual(flatten(got), tt.want) {
t.Errorf("exclude() = %v, want %v", got, tt.want)
}
})
}
}
func Test_noneJoin(t *testing.T) {
type args struct {
seriesTagIndex1 map[uint64][]uint64
seriesTagIndex2 map[uint64][]uint64
}
tests := []struct {
name string
args args
want map[uint64][]uint64
}{
{
name: "none join, direct splicing",
args: args{
seriesTagIndex1: seriesTagIndex1,
seriesTagIndex2: seriesTagIndex2,
},
want: map[uint64][]uint64{
0: {1, 2, 3, 4},
1: {5, 6, 7, 8},
2: {9, 10, 11, 12},
3: {13, 14, 15, 16},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := noneJoin(tt.args.seriesTagIndex1, tt.args.seriesTagIndex2); !allValueDeepEqual(got, tt.want) {
t.Errorf("noneJoin() = %v, want %v", got, tt.want)
}
})
}
}
func Test_cartesianJoin(t *testing.T) {
type args struct {
seriesTagIndex1 map[uint64][]uint64
seriesTagIndex2 map[uint64][]uint64
}
tests := []struct {
name string
args args
want map[uint64][]uint64
}{
{
name: "cartesian join",
args: args{
seriesTagIndex1: seriesTagIndex1,
seriesTagIndex2: seriesTagIndex2,
},
want: map[uint64][]uint64{
0: {1, 2, 3, 4, 9, 10, 11, 12},
1: {5, 6, 7, 8, 9, 10, 11, 12},
2: {5, 6, 7, 8, 13, 14, 15, 16},
3: {1, 2, 3, 4, 13, 14, 15, 16},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cartesianJoin(tt.args.seriesTagIndex1, tt.args.seriesTagIndex2); !allValueDeepEqual(got, tt.want) {
t.Errorf("cartesianJoin() = %v, want %v", got, tt.want)
}
})
}
}
func Test_onJoin(t *testing.T) {
type args struct {
reHashTagIndex1 map[uint64][][]uint64
reHashTagIndex2 map[uint64][][]uint64
joinType JoinType
}
tests := []struct {
name string
args args
want map[uint64][]uint64
}{
{
name: "left join",
args: args{
reHashTagIndex1: reHashTagIndex1,
reHashTagIndex2: reHashTagIndex2,
joinType: Left,
},
want: map[uint64][]uint64{
1: {1, 2, 9, 10},
2: {3, 4, 9, 10},
3: {1, 2, 11, 12},
4: {3, 4, 11, 12},
5: {5, 6},
6: {7, 8},
},
},
{
name: "right join",
args: args{
reHashTagIndex1: reHashTagIndex2,
reHashTagIndex2: reHashTagIndex1,
joinType: Right,
},
want: map[uint64][]uint64{
1: {1, 2, 9, 10},
2: {3, 4, 9, 10},
3: {1, 2, 11, 12},
4: {3, 4, 11, 12},
5: {13, 14},
6: {15, 16},
},
},
{
name: "inner join",
args: args{
reHashTagIndex1: reHashTagIndex1,
reHashTagIndex2: reHashTagIndex2,
joinType: Inner,
},
want: map[uint64][]uint64{
1: {1, 2, 9, 10},
2: {3, 4, 9, 10},
3: {1, 2, 11, 12},
4: {3, 4, 11, 12},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := onJoin(tt.args.reHashTagIndex1, tt.args.reHashTagIndex2, tt.args.joinType); !allValueDeepEqual(flatten(got), tt.want) {
t.Errorf("onJoin() = %v, want %v", got, tt.want)
}
})
}
}
// allValueDeepEqual 判断 map 的 value 是否相同,不考虑 key
func allValueDeepEqual(got, want map[uint64][]uint64) bool {
if len(got) != len(want) {
return false
}
for _, v1 := range got {
curEqual := false
slices.Sort(v1)
for _, v2 := range want {
slices.Sort(v2)
if reflect.DeepEqual(v1, v2) {
curEqual = true
break
}
}
if !curEqual {
return false
}
}
return true
}

View File

@@ -114,7 +114,7 @@ func BgNotMatchMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent,
target, exists := targetCache.Get(ident)
// 对于包含ident的告警事件check一下ident所属bg和rule所属bg是否相同
// 如果告警规则选择了只在本BG生效那其他BG的机器就不能因此规则产生告警
if exists && target.GroupId != rule.GroupId {
if exists && !target.MatchGroupId(rule.GroupId) {
logger.Debugf("[%s] mute: rule_eval:%d cluster:%s", "BgNotMatchMuteStrategy", rule.Id, event.Cluster)
return true
}

View File

@@ -170,7 +170,9 @@ func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inh
p.handleEvent(events)
}
p.HandleRecover(alertingKeys, now, inhibit)
if from == "inner" {
p.HandleRecover(alertingKeys, now, inhibit)
}
}
func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, now int64) *models.AlertCurEvent {
@@ -211,6 +213,15 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
event.Severity = anomalyPoint.Severity
event.ExtraConfig = p.rule.ExtraConfigJSON
event.PromQl = anomalyPoint.Query
event.RecoverConfig = anomalyPoint.RecoverConfig
if p.target != "" {
if pt, exist := p.TargetCache.Get(p.target); exist {
event.Target = pt
} else {
logger.Infof("Target[ident: %s] doesn't exist in cache.", p.target)
}
}
if event.TriggerValues != "" && strings.Count(event.TriggerValues, "$") > 1 {
// TriggerValues 有多个变量,将多个变量都放到 TriggerValue 中
@@ -282,7 +293,7 @@ func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64, i
}
hashArr := make([]string, 0, len(alertingKeys))
for hash := range p.fires.GetAll() {
for hash, _ := range p.fires.GetAll() {
if _, has := alertingKeys[hash]; has {
continue
}
@@ -301,7 +312,7 @@ func (p *Processor) HandleRecoverEvent(hashArr []string, now int64, inhibit bool
if !inhibit {
for _, hash := range hashArr {
p.RecoverSingle(hash, now, nil)
p.RecoverSingle(false, hash, now, nil)
}
return
}
@@ -329,11 +340,11 @@ func (p *Processor) HandleRecoverEvent(hashArr []string, now int64, inhibit bool
}
for _, event := range eventMap {
p.RecoverSingle(event.Hash, now, nil)
p.RecoverSingle(false, event.Hash, now, nil)
}
}
func (p *Processor) RecoverSingle(hash string, now int64, value *string, values ...string) {
func (p *Processor) RecoverSingle(byRecover bool, hash string, now int64, value *string, values ...string) {
cachedRule := p.rule
if cachedRule == nil {
return
@@ -359,6 +370,12 @@ func (p *Processor) RecoverSingle(hash string, now int64, value *string, values
}
}
// 如果设置了恢复条件,则不能在此处恢复,必须依靠 recoverPoint 来恢复
if event.RecoverConfig.JudgeType != models.Origin && !byRecover {
logger.Debugf("rule_eval:%s event:%v not recover", p.Key(), event)
return
}
if value != nil {
event.TriggerValue = *value
if len(values) > 0 {
@@ -390,6 +407,13 @@ func (p *Processor) handleEvent(events []*models.AlertCurEvent) {
if event == nil {
continue
}
if _, has := p.pendingsUseByRecover.Get(event.Hash); has {
p.pendingsUseByRecover.UpdateLastEvalTime(event.Hash, event.LastEvalTime)
} else {
p.pendingsUseByRecover.Set(event.Hash, event)
}
if p.rule.PromForDuration == 0 {
fireEvents = append(fireEvents, event)
if severity > event.Severity {
@@ -398,15 +422,13 @@ func (p *Processor) handleEvent(events []*models.AlertCurEvent) {
continue
}
var preTriggerTime int64
var preTriggerTime int64 // 第一个 pending event 的触发时间
preEvent, has := p.pendings.Get(event.Hash)
if has {
p.pendings.UpdateLastEvalTime(event.Hash, event.LastEvalTime)
p.pendingsUseByRecover.UpdateLastEvalTime(event.Hash, event.LastEvalTime)
preTriggerTime = preEvent.TriggerTime
} else {
p.pendings.Set(event.Hash, event)
p.pendingsUseByRecover.Set(event.Hash, event)
preTriggerTime = event.TriggerTime
}
@@ -503,6 +525,7 @@ func (p *Processor) RecoverAlertCurEventFromDb() {
}
fireMap := make(map[string]*models.AlertCurEvent)
pendingsUseByRecoverMap := make(map[string]*models.AlertCurEvent)
for _, event := range curEvents {
if event.Cate == models.HOST {
target, exists := p.TargetCache.Get(event.TargetIdent)
@@ -513,10 +536,20 @@ func (p *Processor) RecoverAlertCurEventFromDb() {
}
event.DB2Mem()
target, exists := p.TargetCache.Get(event.TargetIdent)
if exists {
event.Target = target
}
fireMap[event.Hash] = event
e := *event
pendingsUseByRecoverMap[event.Hash] = &e
}
p.fires = NewAlertCurEventMap(fireMap)
// 修改告警规则,或者进程重启之后,需要重新加载 pendingsUseByRecover
p.pendingsUseByRecover = NewAlertCurEventMap(pendingsUseByRecoverMap)
}
func (p *Processor) fillTags(anomalyPoint common.AnomalyPoint) {

View File

@@ -56,11 +56,12 @@ func (rrc *RecordRuleContext) Key() string {
}
func (rrc *RecordRuleContext) Hash() string {
return str.MD5(fmt.Sprintf("%d_%s_%s_%d",
return str.MD5(fmt.Sprintf("%d_%s_%s_%d_%s",
rrc.rule.Id,
rrc.rule.CronPattern,
rrc.rule.PromQl,
rrc.datasourceId,
rrc.rule.AppendTags,
))
}

View File

@@ -34,7 +34,7 @@ func (rt *Router) pushEventToQueue(c *gin.Context) {
continue
}
arr := strings.Split(pair, "=")
arr := strings.SplitN(pair, "=", 2)
if len(arr) != 2 {
continue
}
@@ -129,7 +129,7 @@ func (rt *Router) makeEvent(c *gin.Context) {
} else {
for _, vector := range events[i].AnomalyPoints {
readableString := vector.ReadableValue()
go ruleWorker.RecoverSingle(process.Hash(events[i].RuleId, events[i].DatasourceId, vector), vector.Timestamp, &readableString)
go ruleWorker.RecoverSingle(false, process.Hash(events[i].RuleId, events[i].DatasourceId, vector), vector.Timestamp, &readableString)
}
}
}

View File

@@ -156,8 +156,16 @@ func NotifyRecord(ctx *ctx.Context, evt *models.AlertCurEvent, channel, target,
noti.SetDetails(string(res))
}
if !ctx.IsCenter {
_, err := poster.PostByUrlsWithResp[int64](ctx, "/v1/n9e/notify-record", noti)
if err != nil {
logger.Errorf("add noti:%v failed, err: %v", noti, err)
}
return
}
if err := noti.Add(ctx); err != nil {
logger.Errorf("Add noti failed, err: %v", err)
logger.Errorf("add noti:%v failed, err: %v", noti, err)
}
}
@@ -187,8 +195,8 @@ func PushCallbackEvent(ctx *ctx.Context, webhook *models.Webhook, event *models.
if queue == nil {
queue = &WebhookQueue{
list: NewSafeListLimited(QueueMaxSize),
closeCh: make(chan struct{}),
eventQueue: NewSafeEventQueue(QueueMaxSize),
closeCh: make(chan struct{}),
}
CallbackEventQueueLock.Lock()
@@ -198,8 +206,8 @@ func PushCallbackEvent(ctx *ctx.Context, webhook *models.Webhook, event *models.
StartConsumer(ctx, queue, webhook.Batch, webhook, stats)
}
succ := queue.list.PushFront(event)
succ := queue.eventQueue.Push(event)
if !succ {
logger.Warningf("Write channel(%s) full, current channel size: %d event:%v", webhook.Url, queue.list.Len(), event)
logger.Warningf("Write channel(%s) full, current channel size: %d event:%v", webhook.Url, queue.eventQueue.Len(), event)
}
}

View File

@@ -123,7 +123,7 @@ func InitEmailSender(ctx *ctx.Context, ncc *memsto.NotifyConfigCacheType) {
mailch = make(chan *EmailContext, 100000)
go updateSmtp(ctx, ncc)
smtpConfig = ncc.GetSMTP()
startEmailSender(ctx, smtpConfig)
go startEmailSender(ctx, smtpConfig)
}
func updateSmtp(ctx *ctx.Context, ncc *memsto.NotifyConfigCacheType) {
@@ -143,6 +143,7 @@ func startEmailSender(ctx *ctx.Context, smtp aconf.SMTPConfig) {
conf := smtp
if conf.Host == "" || conf.Port == 0 {
logger.Warning("SMTP configurations invalid")
<-mailQuit
return
}
logger.Infof("start email sender... conf.Host:%+v,conf.Port:%+v", conf.Host, conf.Port)

View File

@@ -56,8 +56,8 @@ const (
Triggered = "triggered"
)
var (
body = feishuCard{
func createFeishuCardBody() feishuCard {
return feishuCard{
feishu: feishu{Msgtype: "interactive"},
Card: Cards{
Config: Conf{
@@ -90,7 +90,7 @@ var (
},
},
}
)
}
func (fs *FeishuCardSender) CallBack(ctx CallBackContext) {
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
@@ -121,6 +121,7 @@ func (fs *FeishuCardSender) CallBack(ctx CallBackContext) {
}
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
body := createFeishuCardBody()
body.Card.Header.Title.Content = SendTitle
body.Card.Header.Template = color
body.Card.Elements[0].Text.Content = message
@@ -153,6 +154,7 @@ func (fs *FeishuCardSender) Send(ctx MessageContext) {
}
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
body := createFeishuCardBody()
body.Card.Header.Title.Content = SendTitle
body.Card.Header.Template = color
body.Card.Elements[0].Text.Content = message

View File

@@ -108,7 +108,7 @@ func CallIbex(ctx *ctx.Context, id int64, host string,
continue
}
arr := strings.Split(pair, "=")
arr := strings.SplitN(pair, "=", 2)
if len(arr) != 2 {
continue
}
@@ -181,10 +181,15 @@ func canDoIbex(username string, tpl *models.TaskTpl, host string, targetCache *m
return false, nil
}
return target.GroupId == tpl.GroupId, nil
return target.MatchGroupId(tpl.GroupId), nil
}
func TaskAdd(f models.TaskForm, authUser string, isCenter bool) (int64, error) {
if storage.Cache == nil {
logger.Warning("event_callback_ibex: redis cache is nil")
return 0, fmt.Errorf("redis cache is nil")
}
hosts := cleanHosts(f.Hosts)
if len(hosts) == 0 {
return 0, fmt.Errorf("arg(hosts) empty")

View File

@@ -42,6 +42,7 @@ func (fs *LarkCardSender) CallBack(ctx CallBackContext) {
}
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
body := createFeishuCardBody()
body.Card.Header.Title.Content = SendTitle
body.Card.Header.Template = color
body.Card.Elements[0].Text.Content = message
@@ -74,6 +75,7 @@ func (fs *LarkCardSender) Send(ctx MessageContext) {
}
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
body := createFeishuCardBody()
body.Card.Header.Title.Content = SendTitle
body.Card.Header.Template = color
body.Card.Elements[0].Text.Content = message

View File

@@ -121,8 +121,8 @@ var EventQueueLock sync.RWMutex
const QueueMaxSize = 100000
type WebhookQueue struct {
list *SafeListLimited
closeCh chan struct{}
eventQueue *SafeEventQueue
closeCh chan struct{}
}
func PushEvent(ctx *ctx.Context, webhook *models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
@@ -132,8 +132,8 @@ func PushEvent(ctx *ctx.Context, webhook *models.Webhook, event *models.AlertCur
if queue == nil {
queue = &WebhookQueue{
list: NewSafeListLimited(QueueMaxSize),
closeCh: make(chan struct{}),
eventQueue: NewSafeEventQueue(QueueMaxSize),
closeCh: make(chan struct{}),
}
EventQueueLock.Lock()
@@ -143,10 +143,10 @@ func PushEvent(ctx *ctx.Context, webhook *models.Webhook, event *models.AlertCur
StartConsumer(ctx, queue, webhook.Batch, webhook, stats)
}
succ := queue.list.PushFront(event)
succ := queue.eventQueue.Push(event)
if !succ {
stats.AlertNotifyErrorTotal.WithLabelValues("push_event_queue").Inc()
logger.Warningf("Write channel(%s) full, current channel size: %d event:%v", webhook.Url, queue.list.Len(), event)
logger.Warningf("Write channel(%s) full, current channel size: %d event:%v", webhook.Url, queue.eventQueue.Len(), event)
}
}
@@ -157,7 +157,7 @@ func StartConsumer(ctx *ctx.Context, queue *WebhookQueue, popSize int, webhook *
logger.Infof("event queue:%v closed", queue)
return
default:
events := queue.list.PopBack(popSize)
events := queue.eventQueue.PopN(popSize)
if len(events) == 0 {
time.Sleep(time.Millisecond * 400)
continue
@@ -166,10 +166,10 @@ func StartConsumer(ctx *ctx.Context, queue *WebhookQueue, popSize int, webhook *
retryCount := 0
for retryCount < webhook.RetryCount {
needRetry, res, err := sendWebhook(webhook, events, stats)
go RecordEvents(ctx, webhook, events, stats, res, err)
if !needRetry {
break
}
go RecordEvents(ctx, webhook, events, stats, res, err)
retryCount++
time.Sleep(time.Second * time.Duration(webhook.RetryInterval) * time.Duration(retryCount))
}

View File

@@ -0,0 +1,113 @@
package sender
import (
"container/list"
"sync"
"github.com/ccfos/nightingale/v6/models"
)
type SafeEventQueue struct {
lock sync.RWMutex
maxSize int
queueHigh *list.List
queueMiddle *list.List
queueLow *list.List
}
const (
High = 1
Middle = 2
Low = 3
)
func NewSafeEventQueue(maxSize int) *SafeEventQueue {
return &SafeEventQueue{
maxSize: maxSize,
lock: sync.RWMutex{},
queueHigh: list.New(),
queueMiddle: list.New(),
queueLow: list.New(),
}
}
func (spq *SafeEventQueue) Len() int {
spq.lock.RLock()
defer spq.lock.RUnlock()
return spq.queueHigh.Len() + spq.queueMiddle.Len() + spq.queueLow.Len()
}
// len 无锁读取长度,不要在本文件外调用
func (spq *SafeEventQueue) len() int {
return spq.queueHigh.Len() + spq.queueMiddle.Len() + spq.queueLow.Len()
}
func (spq *SafeEventQueue) Push(event *models.AlertCurEvent) bool {
spq.lock.Lock()
defer spq.lock.Unlock()
switch event.Severity {
case High:
spq.queueHigh.PushBack(event)
case Middle:
spq.queueMiddle.PushBack(event)
case Low:
spq.queueLow.PushBack(event)
default:
return false
}
for spq.len() > spq.maxSize {
if spq.queueLow.Len() > 0 {
spq.queueLow.Remove(spq.queueLow.Front())
} else if spq.queueMiddle.Len() > 0 {
spq.queueMiddle.Remove(spq.queueMiddle.Front())
} else {
spq.queueHigh.Remove(spq.queueHigh.Front())
}
}
return true
}
// pop 无锁弹出事件,不要在本文件外调用
func (spq *SafeEventQueue) pop() *models.AlertCurEvent {
if spq.len() == 0 {
return nil
}
var elem interface{}
if spq.queueHigh.Len() > 0 {
elem = spq.queueHigh.Remove(spq.queueHigh.Front())
} else if spq.queueMiddle.Len() > 0 {
elem = spq.queueMiddle.Remove(spq.queueMiddle.Front())
} else {
elem = spq.queueLow.Remove(spq.queueLow.Front())
}
event, ok := elem.(*models.AlertCurEvent)
if !ok {
return nil
}
return event
}
func (spq *SafeEventQueue) Pop() *models.AlertCurEvent {
spq.lock.Lock()
defer spq.lock.Unlock()
return spq.pop()
}
func (spq *SafeEventQueue) PopN(n int) []*models.AlertCurEvent {
spq.lock.Lock()
defer spq.lock.Unlock()
events := make([]*models.AlertCurEvent, 0, n)
count := 0
for count < n && spq.len() > 0 {
event := spq.pop()
if event != nil {
events = append(events, event)
}
count++
}
return events
}

View File

@@ -0,0 +1,157 @@
package sender
import (
"sync"
"testing"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/stretchr/testify/assert"
)
func TestSafePriorityQueue_ConcurrentPushPop(t *testing.T) {
spq := NewSafeEventQueue(100000)
var wg sync.WaitGroup
numGoroutines := 100
numEvents := 1000
// 并发 Push
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < numEvents; j++ {
event := &models.AlertCurEvent{
Severity: goroutineID%3 + 1,
TriggerTime: time.Now().UnixNano(),
}
spq.Push(event)
}
}(i)
}
wg.Wait()
// 检查队列长度是否正确
expectedLen := numGoroutines * numEvents
assert.Equal(t, expectedLen, spq.Len(), "Queue length mismatch after concurrent pushes")
// 并发 Pop
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
for {
event := spq.Pop()
if event == nil {
return
}
}
}()
}
wg.Wait()
// 最终队列应该为空
assert.Equal(t, 0, spq.Len(), "Queue should be empty after concurrent pops")
}
func TestSafePriorityQueue_ConcurrentPopMax(t *testing.T) {
spq := NewSafeEventQueue(100000)
// 添加初始数据
for i := 0; i < 1000; i++ {
spq.Push(&models.AlertCurEvent{
Severity: i%3 + 1,
TriggerTime: time.Now().UnixNano(),
})
}
var wg sync.WaitGroup
numGoroutines := 10
popMax := 100
// 并发 PopN
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
events := spq.PopN(popMax)
assert.LessOrEqual(t, len(events), popMax, "PopN exceeded maximum")
}()
}
wg.Wait()
// 检查队列长度是否正确
expectedRemaining := 1000 - (numGoroutines * popMax)
if expectedRemaining < 0 {
expectedRemaining = 0
}
assert.Equal(t, expectedRemaining, spq.Len(), "Queue length mismatch after concurrent PopN")
}
func TestSafePriorityQueue_ConcurrentPushPopWithDifferentSeverities(t *testing.T) {
spq := NewSafeEventQueue(100000)
var wg sync.WaitGroup
numGoroutines := 50
numEvents := 500
// 并发 Push 不同优先级的事件
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < numEvents; j++ {
event := &models.AlertCurEvent{
Severity: goroutineID%3 + 1, // 模拟不同的 Severity
TriggerTime: time.Now().UnixNano(),
}
spq.Push(event)
}
}(i)
}
wg.Wait()
// 检查队列长度是否正确
expectedLen := numGoroutines * numEvents
assert.Equal(t, expectedLen, spq.Len(), "Queue length mismatch after concurrent pushes")
// 检查事件的顺序是否按照优先级排列
var lastEvent *models.AlertCurEvent
for spq.Len() > 0 {
event := spq.Pop()
if lastEvent != nil {
assert.LessOrEqual(t, lastEvent.Severity, event.Severity, "Events are not in correct priority order")
}
lastEvent = event
}
}
func TestSafePriorityQueue_ExceedMaxSize(t *testing.T) {
spq := NewSafeEventQueue(5)
// 插入超过最大容量的事件
for i := 0; i < 10; i++ {
spq.Push(&models.AlertCurEvent{
Severity: i % 3,
TriggerTime: int64(i),
})
}
// 验证队列的长度是否不超过 maxSize
assert.LessOrEqual(t, spq.Len(), spq.maxSize)
// 验证队列中剩余事件的内容
expectedEvents := 5
if spq.Len() < 5 {
expectedEvents = spq.Len()
}
// 检查最后存入的事件是否是按优先级排序
for i := 0; i < expectedEvents; i++ {
event := spq.Pop()
if event != nil {
assert.LessOrEqual(t, event.Severity, 2)
}
}
}

View File

@@ -14,6 +14,7 @@ type Center struct {
FlashDuty FlashDuty
EventHistoryGroupView bool
CleanNotifyRecordDay int
MigrateBusiGroupLabel bool
}
type Plugin struct {

View File

@@ -72,7 +72,7 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
return nil, err
}
integration.Init(ctx, config.Center.BuiltinIntegrationsDir)
go integration.Init(ctx, config.Center.BuiltinIntegrationsDir)
var redis storage.Redis
redis, err = storage.NewRedis(config.Redis)
if err != nil {
@@ -95,6 +95,7 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
userCache := memsto.NewUserCache(ctx, syncStats)
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
taskTplCache := memsto.NewTaskTplCache(ctx)
configCvalCache := memsto.NewCvalCache(ctx, syncStats)
sso := sso.Init(config.Center, ctx, configCache)
promClients := prom.NewPromClient(ctx)
@@ -110,11 +111,14 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
go cron.CleanNotifyRecord(ctx, config.Center.CleanNotifyRecordDay)
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
centerRouter := centerrt.New(config.HTTP, config.Center, config.Alert, config.Ibex, cconf.Operations, dsCache, notifyConfigCache, promClients, tdengineClients,
centerRouter := centerrt.New(config.HTTP, config.Center, config.Alert, config.Ibex,
cconf.Operations, dsCache, notifyConfigCache, promClients, tdengineClients,
redis, sso, ctx, metas, idents, targetCache, userCache, userGroupCache)
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
go models.MigrateBg(ctx, pushgwRouter.Pushgw.BusiGroupLabelKey)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP, configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
centerRouter.Config(r)
alertrtRouter.Config(r)

View File

@@ -16,6 +16,12 @@ import (
const SYSTEM = "system"
func Init(ctx *ctx.Context, builtinIntegrationsDir string) {
err := models.InitBuiltinPayloads(ctx)
if err != nil {
logger.Warning("init old builtinPayloads fail ", err)
return
}
fp := builtinIntegrationsDir
if fp == "" {
fp = path.Join(runner.Cwd, "integrations")
@@ -92,6 +98,7 @@ func Init(ctx *ctx.Context, builtinIntegrationsDir string) {
logger.Warning("update builtin component fail ", old, err)
}
}
component.ID = old.ID
}
// delete uuid is emtpy
@@ -141,13 +148,13 @@ func Init(ctx *ctx.Context, builtinIntegrationsDir string) {
cate := strings.Replace(f, ".json", "", -1)
builtinAlert := models.BuiltinPayload{
Component: component.Ident,
Type: "alert",
Cate: cate,
Name: alert.Name,
Tags: alert.AppendTags,
Content: string(content),
UUID: alert.UUID,
ComponentID: component.ID,
Type: "alert",
Cate: cate,
Name: alert.Name,
Tags: alert.AppendTags,
Content: string(content),
UUID: alert.UUID,
}
old, err := models.BuiltinPayloadGet(ctx, "uuid = ?", alert.UUID)
@@ -165,6 +172,7 @@ func Init(ctx *ctx.Context, builtinIntegrationsDir string) {
}
if old.UpdatedBy == SYSTEM {
old.ComponentID = component.ID
old.Content = string(content)
old.Name = alert.Name
old.Tags = alert.AppendTags
@@ -231,13 +239,13 @@ func Init(ctx *ctx.Context, builtinIntegrationsDir string) {
}
builtinDashboard := models.BuiltinPayload{
Component: component.Ident,
Type: "dashboard",
Cate: "",
Name: dashboard.Name,
Tags: dashboard.Tags,
Content: string(content),
UUID: dashboard.UUID,
ComponentID: component.ID,
Type: "dashboard",
Cate: "",
Name: dashboard.Name,
Tags: dashboard.Tags,
Content: string(content),
UUID: dashboard.UUID,
}
old, err := models.BuiltinPayloadGet(ctx, "uuid = ?", dashboard.UUID)
@@ -255,6 +263,7 @@ func Init(ctx *ctx.Context, builtinIntegrationsDir string) {
}
if old.UpdatedBy == SYSTEM {
old.ComponentID = component.ID
old.Content = string(content)
old.Name = dashboard.Name
old.Tags = dashboard.Tags

View File

@@ -16,6 +16,7 @@ import (
"github.com/ccfos/nightingale/v6/conf"
_ "github.com/ccfos/nightingale/v6/front/statik"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/aop"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
@@ -51,9 +52,14 @@ type Router struct {
UserGroupCache *memsto.UserGroupCacheType
Ctx *ctx.Context
HeartbeatHook HeartbeatHookFunc
TargetDeleteHook models.TargetDeleteHookFunc
}
func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, ibex conf.Ibex, operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType, pc *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, redis storage.Redis, sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, idents *idents.Set, tc *memsto.TargetCacheType, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) *Router {
func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, ibex conf.Ibex,
operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType,
pc *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, redis storage.Redis,
sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, idents *idents.Set,
tc *memsto.TargetCacheType, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) *Router {
return &Router{
HTTP: httpConfig,
Center: center,
@@ -73,9 +79,14 @@ func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, ibex c
UserGroupCache: ugc,
Ctx: ctx,
HeartbeatHook: func(ident string) map[string]interface{} { return nil },
TargetDeleteHook: emptyDeleteHook,
}
}
func emptyDeleteHook(ctx *ctx.Context, idents []string) error {
return nil
}
func stat() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
@@ -276,7 +287,7 @@ func (rt *Router) Config(r *gin.Engine) {
pages.POST("/targets/tags", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetBindTagsByFE)
pages.DELETE("/targets/tags", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetUnbindTagsByFE)
pages.PUT("/targets/note", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetUpdateNote)
pages.PUT("/targets/bgid", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetUpdateBgid)
pages.PUT("/targets/bgids", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetBindBgids)
pages.POST("/builtin-cate-favorite", rt.auth(), rt.user(), rt.builtinCateFavoriteAdd)
pages.DELETE("/builtin-cate-favorite/:name", rt.auth(), rt.user(), rt.builtinCateFavoriteDel)
@@ -297,6 +308,7 @@ func (rt *Router) Config(r *gin.Engine) {
pages.POST("/busi-group/:id/board/:bid/clone", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.bgrw(), rt.boardClone)
pages.POST("/busi-groups/boards/clones", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.boardBatchClone)
pages.GET("/boards", rt.auth(), rt.user(), rt.boardGetsByBids)
pages.GET("/board/:bid", rt.boardGet)
pages.GET("/board/:bid/pure", rt.boardPureGet)
pages.PUT("/board/:bid", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.boardPut)
@@ -324,6 +336,7 @@ func (rt *Router) Config(r *gin.Engine) {
pages.GET("/alert-rule/:arid/pure", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRulePureGet)
pages.PUT("/busi-group/alert-rule/validate", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.alertRuleValidation)
pages.POST("/relabel-test", rt.auth(), rt.user(), rt.relabelTest)
pages.POST("/busi-group/:id/alert-rules/clone", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.cloneToMachine)
pages.GET("/busi-groups/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGetsByGids)
pages.GET("/busi-group/:id/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGets)
@@ -462,6 +475,7 @@ func (rt *Router) Config(r *gin.Engine) {
pages.GET("/builtin-payload/:id", rt.auth(), rt.user(), rt.perm("/built-in-components"), rt.builtinPayloadGet)
pages.PUT("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/put"), rt.builtinPayloadsPut)
pages.DELETE("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/del"), rt.builtinPayloadsDel)
pages.GET("/builtin-payload", rt.auth(), rt.user(), rt.builtinPayloadsGetByUUIDOrID)
}
r.GET("/api/n9e/versions", func(c *gin.Context) {
@@ -538,6 +552,7 @@ func (rt *Router) Config(r *gin.Engine) {
service.GET("/config/:id", rt.configGet)
service.GET("/configs", rt.configsGet)
service.GET("/config", rt.configGetByKey)
service.GET("/all-configs", rt.configGetAll)
service.PUT("/configs", rt.configsPut)
service.POST("/configs", rt.configsPost)
service.DELETE("/configs", rt.configsDel)
@@ -555,6 +570,11 @@ func (rt *Router) Config(r *gin.Engine) {
service.GET("/targets-of-alert-rule", rt.targetsOfAlertRule)
service.POST("/notify-record", rt.notificationRecordAdd)
service.GET("/alert-cur-events-del-by-hash", rt.alertCurEventDelByHash)
service.POST("/center/heartbeat", rt.heartbeat)
}
}

View File

@@ -65,7 +65,8 @@ func (rt *Router) alertCurEventsCard(c *gin.Context) {
ginx.Dangerous(err)
// 最多获取50000个获取太多也没啥意义
list, err := models.AlertCurEventGets(rt.Ctx, prods, bgids, stime, etime, severity, dsIds, cates, query, 50000, 0)
list, err := models.AlertCurEventsGet(rt.Ctx, prods, bgids, stime, etime, severity, dsIds,
cates, 0, query, 50000, 0)
ginx.Dangerous(err)
cardmap := make(map[string]*AlertCard)
@@ -162,13 +163,17 @@ func (rt *Router) alertCurEventsList(c *gin.Context) {
cates = strings.Split(cate, ",")
}
ruleId := ginx.QueryInt64(c, "rid", 0)
bgids, err := GetBusinessGroupIds(c, rt.Ctx, rt.Center.EventHistoryGroupView)
ginx.Dangerous(err)
total, err := models.AlertCurEventTotal(rt.Ctx, prods, bgids, stime, etime, severity, dsIds, cates, query)
total, err := models.AlertCurEventTotal(rt.Ctx, prods, bgids, stime, etime, severity, dsIds,
cates, ruleId, query)
ginx.Dangerous(err)
list, err := models.AlertCurEventGets(rt.Ctx, prods, bgids, stime, etime, severity, dsIds, cates, query, limit, ginx.Offset(c, limit))
list, err := models.AlertCurEventsGet(rt.Ctx, prods, bgids, stime, etime, severity, dsIds,
cates, ruleId, query, limit, ginx.Offset(c, limit))
ginx.Dangerous(err)
cache := make(map[int64]*models.UserGroup)
@@ -201,7 +206,9 @@ func (rt *Router) checkCurEventBusiGroupRWPermission(c *gin.Context, ids []int64
for i := 0; i < len(ids); i++ {
event, err := models.AlertCurEventGetById(rt.Ctx, ids[i])
ginx.Dangerous(err)
if event == nil {
continue
}
if _, has := set[event.GroupId]; !has {
rt.bgrwCheck(c, event.GroupId)
set[event.GroupId] = struct{}{}
@@ -227,6 +234,7 @@ func (rt *Router) alertCurEventGet(c *gin.Context) {
event.RuleConfigJson = ruleConfig
}
event.LastEvalTime = event.TriggerTime
ginx.NewRender(c).Data(event, nil)
}
@@ -234,3 +242,8 @@ func (rt *Router) alertCurEventsStatistics(c *gin.Context) {
ginx.NewRender(c).Data(models.AlertCurEventStatistics(rt.Ctx, time.Now()), nil)
}
func (rt *Router) alertCurEventDelByHash(c *gin.Context) {
hash := ginx.QueryStr(c, "hash")
ginx.NewRender(c).Message(models.AlertCurEventDelByHash(rt.Ctx, hash))
}

View File

@@ -54,13 +54,17 @@ func (rt *Router) alertHisEventsList(c *gin.Context) {
cates = strings.Split(cate, ",")
}
ruleId := ginx.QueryInt64(c, "rid", 0)
bgids, err := GetBusinessGroupIds(c, rt.Ctx, rt.Center.EventHistoryGroupView)
ginx.Dangerous(err)
total, err := models.AlertHisEventTotal(rt.Ctx, prods, bgids, stime, etime, severity, recovered, dsIds, cates, query)
total, err := models.AlertHisEventTotal(rt.Ctx, prods, bgids, stime, etime, severity,
recovered, dsIds, cates, ruleId, query)
ginx.Dangerous(err)
list, err := models.AlertHisEventGets(rt.Ctx, prods, bgids, stime, etime, severity, recovered, dsIds, cates, query, limit, ginx.Offset(c, limit))
list, err := models.AlertHisEventGets(rt.Ctx, prods, bgids, stime, etime, severity, recovered,
dsIds, cates, ruleId, query, limit, ginx.Offset(c, limit))
ginx.Dangerous(err)
cache := make(map[int64]*models.UserGroup)

View File

@@ -4,15 +4,19 @@ import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v2"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/prometheus/prometheus/prompb"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/i18n"
@@ -33,6 +37,18 @@ func (rt *Router) alertRuleGets(c *gin.Context) {
ginx.NewRender(c).Data(ars, err)
}
func getAlertCueEventTimeRange(c *gin.Context) (stime, etime int64) {
stime = ginx.QueryInt64(c, "stime", 0)
etime = ginx.QueryInt64(c, "etime", 0)
if etime == 0 {
etime = time.Now().Unix()
}
if stime == 0 || stime >= etime {
stime = etime - 30*24*int64(time.Hour.Seconds())
}
return
}
func (rt *Router) alertRuleGetsByGids(c *gin.Context) {
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
if len(gids) > 0 {
@@ -56,9 +72,30 @@ func (rt *Router) alertRuleGetsByGids(c *gin.Context) {
ars, err := models.AlertRuleGetsByBGIds(rt.Ctx, gids)
if err == nil {
cache := make(map[int64]*models.UserGroup)
rids := make([]int64, 0, len(ars))
names := make([]string, 0, len(ars))
for i := 0; i < len(ars); i++ {
ars[i].FillNotifyGroups(rt.Ctx, cache)
ars[i].FillSeverities()
rids = append(rids, ars[i].Id)
names = append(names, ars[i].UpdateBy)
}
stime, etime := getAlertCueEventTimeRange(c)
cnt := models.AlertCurEventCountByRuleId(rt.Ctx, rids, stime, etime)
if cnt != nil {
for i := 0; i < len(ars); i++ {
ars[i].CurEventCount = cnt[ars[i].Id]
}
}
users := models.UserMapGet(rt.Ctx, "username in (?)", names)
if users != nil {
for i := 0; i < len(ars); i++ {
if user, exist := users[ars[i].UpdateBy]; exist {
ars[i].UpdateByNickname = user.Nickname
}
}
}
}
ginx.NewRender(c).Data(ars, err)
@@ -126,23 +163,32 @@ func (rt *Router) alertRuleAddByImport(c *gin.Context) {
ginx.NewRender(c).Data(reterr, nil)
}
func (rt *Router) alertRuleAddByImportPromRule(c *gin.Context) {
username := c.MustGet("username").(string)
type promRuleForm struct {
Payload string `json:"payload" binding:"required"`
DatasourceIds []int64 `json:"datasource_ids" binding:"required"`
Disabled int `json:"disabled" binding:"gte=0,lte=1"`
}
type PromRule struct {
func (rt *Router) alertRuleAddByImportPromRule(c *gin.Context) {
var f promRuleForm
ginx.Dangerous(c.BindJSON(&f))
var pr struct {
Groups []models.PromRuleGroup `yaml:"groups"`
}
var pr PromRule
ginx.Dangerous(c.BindYAML(&pr))
err := yaml.Unmarshal([]byte(f.Payload), &pr)
if err != nil {
ginx.Bomb(http.StatusBadRequest, "invalid yaml format, please use the example format. err: %v", err)
}
if len(pr.Groups) == 0 {
ginx.Bomb(http.StatusBadRequest, "input yaml is empty")
}
lst := models.DealPromGroup(pr.Groups)
lst := models.DealPromGroup(pr.Groups, f.DatasourceIds, f.Disabled)
username := c.MustGet("username").(string)
bgid := ginx.UrlParamInt64(c, "id")
err := rt.alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language"))
ginx.NewRender(c).Data(err, nil)
ginx.NewRender(c).Data(rt.alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language")), nil)
}
func (rt *Router) alertRuleAddByService(c *gin.Context) {
@@ -295,6 +341,17 @@ func (rt *Router) alertRulePutFields(c *gin.Context) {
continue
}
if f.Action == "update_triggers" {
if triggers, has := f.Fields["triggers"]; has {
originRule := ar.RuleConfigJson.(map[string]interface{})
originRule["triggers"] = triggers
b, err := json.Marshal(originRule)
ginx.Dangerous(err)
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, map[string]interface{}{"rule_config": string(b)}))
continue
}
}
if f.Action == "annotations_add" {
if annotations, has := f.Fields["annotations"]; has {
annotationsMap := annotations.(map[string]interface{})
@@ -468,7 +525,7 @@ func (rt *Router) relabelTest(c *gin.Context) {
labels := make([]prompb.Label, len(f.Tags))
for i, tag := range f.Tags {
label := strings.Split(tag, "=")
label := strings.SplitN(tag, "=", 2)
if len(label) != 2 {
ginx.Bomb(http.StatusBadRequest, "tag:%s format error", tag)
}
@@ -499,3 +556,91 @@ func (rt *Router) relabelTest(c *gin.Context) {
ginx.NewRender(c).Data(tags, nil)
}
type identListForm struct {
Ids []int64 `json:"ids"`
IdentList []string `json:"ident_list"`
}
func containsIdentOperator(s string) bool {
pattern := `ident\s*(!=|!~|=~)`
matched, err := regexp.MatchString(pattern, s)
if err != nil {
return false
}
return matched
}
func (rt *Router) cloneToMachine(c *gin.Context) {
var f identListForm
ginx.BindJSON(c, &f)
if len(f.IdentList) == 0 {
ginx.Bomb(http.StatusBadRequest, "ident_list is empty")
}
alertRules, err := models.AlertRuleGetsByIds(rt.Ctx, f.Ids)
ginx.Dangerous(err)
re := regexp.MustCompile(`ident\s*=\s*\\".*?\\"`)
user := c.MustGet("username").(string)
now := time.Now().Unix()
newRules := make([]*models.AlertRule, 0)
reterr := make(map[string]map[string]string)
for i := range alertRules {
errMsg := make(map[string]string)
if alertRules[i].Cate != "prometheus" {
errMsg["all"] = "Only Prometheus rule can be cloned to machines"
reterr[alertRules[i].Name] = errMsg
continue
}
if containsIdentOperator(alertRules[i].RuleConfig) {
errMsg["all"] = "promql is missing ident"
reterr[alertRules[i].Name] = errMsg
continue
}
for j := range f.IdentList {
alertRules[i].RuleConfig = re.ReplaceAllString(alertRules[i].RuleConfig, fmt.Sprintf(`ident=\"%s\"`, f.IdentList[j]))
newRule := &models.AlertRule{}
if err := copier.Copy(newRule, alertRules[i]); err != nil {
errMsg[f.IdentList[j]] = fmt.Sprintf("fail to clone rule, err: %s", err)
continue
}
newRule.Id = 0
newRule.Name = alertRules[i].Name + "_" + f.IdentList[j]
newRule.CreateBy = user
newRule.UpdateBy = user
newRule.UpdateAt = now
newRule.CreateAt = now
newRule.RuleConfig = alertRules[i].RuleConfig
exist, err := models.AlertRuleExists(rt.Ctx, 0, newRule.GroupId, newRule.DatasourceIdsJson, newRule.Name)
if err != nil {
errMsg[f.IdentList[j]] = err.Error()
continue
}
if exist {
errMsg[f.IdentList[j]] = fmt.Sprintf("rule already exists, ruleName: %s", newRule.Name)
continue
}
newRules = append(newRules, newRule)
}
if len(errMsg) > 0 {
reterr[alertRules[i].Name] = errMsg
}
}
ginx.NewRender(c).Data(reterr, models.InsertAlertRule(rt.Ctx, newRules))
}

View File

@@ -94,6 +94,14 @@ func (rt *Router) boardGet(c *gin.Context) {
ginx.NewRender(c).Data(board, nil)
}
// 根据 bids 参数,获取多个 board
func (rt *Router) boardGetsByBids(c *gin.Context) {
bids := str.IdsInt64(ginx.QueryStr(c, "bids", ""), ",")
boards, err := models.BoardGetsByBids(rt.Ctx, bids)
ginx.Dangerous(err)
ginx.NewRender(c).Data(boards, err)
}
func (rt *Router) boardPureGet(c *gin.Context) {
board, err := models.BoardGetByID(rt.Ctx, ginx.UrlParamInt64(c, "bid"))
ginx.Dangerous(err)

View File

@@ -4,10 +4,15 @@ import (
"net/http"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"gorm.io/gorm"
)
const SYSTEM = "system"
func (rt *Router) builtinComponentsAdd(c *gin.Context) {
var lst []models.BuiltinComponent
ginx.BindJSON(c, &lst)
@@ -50,10 +55,31 @@ func (rt *Router) builtinComponentsPut(c *gin.Context) {
return
}
if bc.CreatedBy == SYSTEM {
req.Ident = bc.Ident
}
username := Username(c)
req.UpdatedBy = username
ginx.NewRender(c).Message(bc.Update(rt.Ctx, req))
err = models.DB(rt.Ctx).Transaction(func(tx *gorm.DB) error {
tCtx := &ctx.Context{
DB: tx,
}
txErr := models.BuiltinMetricBatchUpdateColumn(tCtx, "typ", bc.Ident, req.Ident, req.UpdatedBy)
if txErr != nil {
return txErr
}
txErr = bc.Update(tCtx, req)
if txErr != nil {
return txErr
}
return nil
})
ginx.NewRender(c).Message(err)
}
func (rt *Router) builtinComponentsDel(c *gin.Context) {

View File

@@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/BurntSushi/toml"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
@@ -52,15 +53,15 @@ func (rt *Router) builtinPayloadsAdd(c *gin.Context) {
}
bp := models.BuiltinPayload{
Type: lst[i].Type,
Component: lst[i].Component,
Cate: lst[i].Cate,
Name: rule.Name,
Tags: rule.AppendTags,
UUID: rule.UUID,
Content: string(contentBytes),
CreatedBy: username,
UpdatedBy: username,
Type: lst[i].Type,
ComponentID: lst[i].ComponentID,
Cate: lst[i].Cate,
Name: rule.Name,
Tags: rule.AppendTags,
UUID: rule.UUID,
Content: string(contentBytes),
CreatedBy: username,
UpdatedBy: username,
}
if err := bp.Add(rt.Ctx, username); err != nil {
@@ -81,15 +82,15 @@ func (rt *Router) builtinPayloadsAdd(c *gin.Context) {
}
bp := models.BuiltinPayload{
Type: lst[i].Type,
Component: lst[i].Component,
Cate: lst[i].Cate,
Name: alertRule.Name,
Tags: alertRule.AppendTags,
UUID: alertRule.UUID,
Content: lst[i].Content,
CreatedBy: username,
UpdatedBy: username,
Type: lst[i].Type,
ComponentID: lst[i].ComponentID,
Cate: lst[i].Cate,
Name: alertRule.Name,
Tags: alertRule.AppendTags,
UUID: alertRule.UUID,
Content: lst[i].Content,
CreatedBy: username,
UpdatedBy: username,
}
if err := bp.Add(rt.Ctx, username); err != nil {
@@ -115,15 +116,15 @@ func (rt *Router) builtinPayloadsAdd(c *gin.Context) {
}
bp := models.BuiltinPayload{
Type: lst[i].Type,
Component: lst[i].Component,
Cate: lst[i].Cate,
Name: dashboard.Name,
Tags: dashboard.Tags,
UUID: dashboard.UUID,
Content: string(contentBytes),
CreatedBy: username,
UpdatedBy: username,
Type: lst[i].Type,
ComponentID: lst[i].ComponentID,
Cate: lst[i].Cate,
Name: dashboard.Name,
Tags: dashboard.Tags,
UUID: dashboard.UUID,
Content: string(contentBytes),
CreatedBy: username,
UpdatedBy: username,
}
if err := bp.Add(rt.Ctx, username); err != nil {
@@ -144,21 +145,29 @@ func (rt *Router) builtinPayloadsAdd(c *gin.Context) {
}
bp := models.BuiltinPayload{
Type: lst[i].Type,
Component: lst[i].Component,
Cate: lst[i].Cate,
Name: dashboard.Name,
Tags: dashboard.Tags,
UUID: dashboard.UUID,
Content: lst[i].Content,
CreatedBy: username,
UpdatedBy: username,
Type: lst[i].Type,
ComponentID: lst[i].ComponentID,
Cate: lst[i].Cate,
Name: dashboard.Name,
Tags: dashboard.Tags,
UUID: dashboard.UUID,
Content: lst[i].Content,
CreatedBy: username,
UpdatedBy: username,
}
if err := bp.Add(rt.Ctx, username); err != nil {
reterr[bp.Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
}
} else {
if lst[i].Type == "collect" {
c := make(map[string]interface{})
if _, err := toml.Decode(lst[i].Content, &c); err != nil {
reterr[lst[i].Name] = err.Error()
continue
}
}
if err := lst[i].Add(rt.Ctx, username); err != nil {
reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
}
@@ -171,19 +180,20 @@ func (rt *Router) builtinPayloadsAdd(c *gin.Context) {
func (rt *Router) builtinPayloadsGets(c *gin.Context) {
typ := ginx.QueryStr(c, "type", "")
component := ginx.QueryStr(c, "component", "")
ComponentID := ginx.QueryInt64(c, "component_id", 0)
cate := ginx.QueryStr(c, "cate", "")
query := ginx.QueryStr(c, "query", "")
lst, err := models.BuiltinPayloadGets(rt.Ctx, typ, component, cate, query)
lst, err := models.BuiltinPayloadGets(rt.Ctx, uint64(ComponentID), typ, cate, query)
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) builtinPayloadcatesGet(c *gin.Context) {
typ := ginx.QueryStr(c, "type", "")
component := ginx.QueryStr(c, "component", "")
ComponentID := ginx.QueryInt64(c, "component_id", 0)
cates, err := models.BuiltinPayloadCates(rt.Ctx, typ, component)
cates, err := models.BuiltinPayloadCates(rt.Ctx, typ, uint64(ComponentID))
ginx.NewRender(c).Data(cates, err)
}
@@ -229,6 +239,11 @@ func (rt *Router) builtinPayloadsPut(c *gin.Context) {
req.Name = dashboard.Name
req.Tags = dashboard.Tags
} else if req.Type == "collect" {
c := make(map[string]interface{})
if _, err := toml.Decode(req.Content, &c); err != nil {
ginx.Bomb(http.StatusBadRequest, err.Error())
}
}
username := Username(c)
@@ -245,3 +260,15 @@ func (rt *Router) builtinPayloadsDel(c *gin.Context) {
ginx.NewRender(c).Message(models.BuiltinPayloadDels(rt.Ctx, req.Ids))
}
func (rt *Router) builtinPayloadsGetByUUIDOrID(c *gin.Context) {
uuid := ginx.QueryInt64(c, "uuid", 0)
// 优先以 uuid 为准
if uuid != 0 {
ginx.NewRender(c).Data(models.BuiltinPayloadGet(rt.Ctx, "uuid = ?", uuid))
return
}
id := ginx.QueryInt64(c, "id", 0)
ginx.NewRender(c).Data(models.BuiltinPayloadGet(rt.Ctx, "id = ?", id))
}

View File

@@ -24,6 +24,11 @@ func (rt *Router) configGet(c *gin.Context) {
ginx.NewRender(c).Data(configs, err)
}
func (rt *Router) configGetAll(c *gin.Context) {
config, err := models.ConfigsGetAll(rt.Ctx)
ginx.NewRender(c).Data(config, err)
}
func (rt *Router) configGetByKey(c *gin.Context) {
config, err := models.ConfigsGet(rt.Ctx, ginx.QueryStr(c, "key"))
ginx.NewRender(c).Data(config, err)

View File

@@ -92,10 +92,12 @@ func (rt *Router) datasourceUpsert(c *gin.Context) {
var err error
var count int64
err = DatasourceCheck(req)
if err != nil {
Dangerous(c, err)
return
if !req.ForceSave {
err = DatasourceCheck(req)
if err != nil {
Dangerous(c, err)
return
}
}
if req.Id == 0 {

View File

@@ -45,6 +45,10 @@ func (rt *Router) statistic(c *gin.Context) {
statistics, err = models.ConfigsUserVariableStatistics(rt.Ctx)
ginx.NewRender(c).Data(statistics, err)
return
case "cval":
statistics, err = models.ConfigCvalStatistics(rt.Ctx)
ginx.NewRender(c).Data(statistics, err)
return
default:
ginx.Bomb(http.StatusBadRequest, "invalid name")
}
@@ -65,6 +69,23 @@ func queryDatasourceIds(c *gin.Context) []int64 {
return ids
}
func queryStrListField(c *gin.Context, fieldName string, sep ...string) []string {
str := ginx.QueryStr(c, fieldName, "")
if str == "" {
return nil
}
lst := []string{str}
for _, s := range sep {
var newLst []string
for _, str := range lst {
newLst = append(newLst, strings.Split(str, s)...)
}
lst = newLst
}
return lst
}
type idsForm struct {
Ids []int64 `json:"ids"`
IsSyncToFlashDuty bool `json:"is_sync_to_flashduty"`

View File

@@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"io/ioutil"
"sort"
"strconv"
"strings"
"time"
@@ -79,59 +81,122 @@ func HandleHeartbeat(c *gin.Context, ctx *ctx.Context, engineName string, metaSe
identSet.MSet(items)
if target, has := targetCache.Get(req.Hostname); has && target != nil {
gid := ginx.QueryInt64(c, "gid", 0)
gidsStr := ginx.QueryStr(c, "gid", "")
overwriteGids := ginx.QueryBool(c, "overwrite_gids", false)
hostIp := strings.TrimSpace(req.HostIp)
gids := strings.Split(gidsStr, ",")
field := make(map[string]interface{})
if gid != 0 && gid != target.GroupId {
field["group_id"] = gid
if overwriteGids {
groupIds := make([]int64, 0)
for i := range gids {
if gids[i] == "" {
continue
}
groupId, err := strconv.ParseInt(gids[i], 10, 64)
if err != nil {
logger.Warningf("update target:%s group ids failed, err: %v", req.Hostname, err)
continue
}
groupIds = append(groupIds, groupId)
}
err := models.TargetOverrideBgids(ctx, []string{target.Ident}, groupIds)
if err != nil {
logger.Warningf("update target:%s group ids failed, err: %v", target.Ident, err)
}
} else if gidsStr != "" {
for i := range gids {
groupId, err := strconv.ParseInt(gids[i], 10, 64)
if err != nil {
logger.Warningf("update target:%s group ids failed, err: %v", req.Hostname, err)
continue
}
if !target.MatchGroupId(groupId) {
err := models.TargetBindBgids(ctx, []string{target.Ident}, []int64{groupId})
if err != nil {
logger.Warningf("update target:%s group ids failed, err: %v", target.Ident, err)
}
}
}
}
newTarget := models.Target{}
targetNeedUpdate := false
if hostIp != "" && hostIp != target.HostIp {
field["host_ip"] = hostIp
newTarget.HostIp = hostIp
targetNeedUpdate = true
}
tagsMap := target.GetTagsMap()
tagNeedUpdate := false
for k, v := range req.GlobalLabels {
hostTagsMap := target.GetHostTagsMap()
hostTagNeedUpdate := false
if len(hostTagsMap) != len(req.GlobalLabels) {
hostTagNeedUpdate = true
} else {
for k, v := range req.GlobalLabels {
if v == "" {
continue
}
if tagv, ok := hostTagsMap[k]; !ok || tagv != v {
hostTagNeedUpdate = true
break
}
}
}
if hostTagNeedUpdate {
lst := []string{}
for k, v := range req.GlobalLabels {
lst = append(lst, k+"="+v)
}
sort.Strings(lst)
newTarget.HostTags = lst
targetNeedUpdate = true
}
userTagsMap := target.GetTagsMap()
userTagNeedUpdate := false
userTags := []string{}
for k, v := range userTagsMap {
if v == "" {
continue
}
if tagv, ok := tagsMap[k]; !ok || tagv != v {
tagNeedUpdate = true
tagsMap[k] = v
if _, ok := req.GlobalLabels[k]; !ok {
userTags = append(userTags, k+"="+v)
} else { // 该key在hostTags中已经存在
userTagNeedUpdate = true
}
}
if tagNeedUpdate {
lst := []string{}
for k, v := range tagsMap {
lst = append(lst, k+"="+v)
}
labels := strings.Join(lst, " ") + " "
field["tags"] = labels
if userTagNeedUpdate {
newTarget.Tags = strings.Join(userTags, " ") + " "
targetNeedUpdate = true
}
if req.EngineName != "" && req.EngineName != target.EngineName {
field["engine_name"] = req.EngineName
newTarget.EngineName = req.EngineName
targetNeedUpdate = true
}
if req.AgentVersion != "" && req.AgentVersion != target.AgentVersion {
field["agent_version"] = req.AgentVersion
newTarget.AgentVersion = req.AgentVersion
targetNeedUpdate = true
}
if req.OS != "" && req.OS != target.OS {
field["os"] = req.OS
newTarget.OS = req.OS
targetNeedUpdate = true
}
if len(field) > 0 {
err := target.UpdateFieldsMap(ctx, field)
if targetNeedUpdate {
err := models.DB(ctx).Model(&target).Updates(newTarget).Error
if err != nil {
logger.Errorf("update target fields failed, err: %v", err)
}
}
logger.Debugf("heartbeat field:%+v target: %v", field, *target)
logger.Debugf("heartbeat field:%+v target: %v", newTarget, *target)
}
return req, nil

View File

@@ -50,7 +50,8 @@ func (rt *Router) alertMuteGets(c *gin.Context) {
prods := strings.Fields(ginx.QueryStr(c, "prods", ""))
bgid := ginx.QueryInt64(c, "bgid", -1)
query := ginx.QueryStr(c, "query", "")
lst, err := models.AlertMuteGets(rt.Ctx, prods, bgid, query)
disabled := ginx.QueryInt(c, "disabled", -1)
lst, err := models.AlertMuteGets(rt.Ctx, prods, bgid, disabled, query)
ginx.NewRender(c).Data(lst, err)
}

View File

@@ -33,6 +33,15 @@ type Record struct {
Detail string `json:"detail"`
}
// notificationRecordAdd
func (rt *Router) notificationRecordAdd(c *gin.Context) {
var req models.NotificaitonRecord
ginx.BindJSON(c, &req)
err := req.Add(rt.Ctx)
ginx.NewRender(c).Data(req.Id, err)
}
func (rt *Router) notificationRecordList(c *gin.Context) {
eid := ginx.UrlParamInt64(c, "eid")
lst, err := models.NotificaitonRecordsGetByEventId(rt.Ctx, eid)

View File

@@ -68,7 +68,7 @@ func (rt *Router) notifyTplUpdate(c *gin.Context) {
}
// get the count of the same channel and name but different id
count, err := models.Count(models.DB(rt.Ctx).Model(&models.NotifyTpl{}).Where("channel = ? or name = ? and id <> ?", f.Channel, f.Name, f.Id))
count, err := models.Count(models.DB(rt.Ctx).Model(&models.NotifyTpl{}).Where("(channel = ? or name = ?) and id <> ?", f.Channel, f.Name, f.Id))
ginx.Dangerous(err)
if count != 0 {
ginx.Bomb(200, "Refuse to create duplicate channel or name")
@@ -138,7 +138,7 @@ func (rt *Router) notifyTplPreview(c *gin.Context) {
continue
}
arr := strings.Split(pair, "=")
arr := strings.SplitN(pair, "=", 2)
if len(arr) != 2 {
continue
}

View File

@@ -52,6 +52,8 @@ func (rt *Router) targetGets(c *gin.Context) {
order := ginx.QueryStr(c, "order", "ident")
desc := ginx.QueryBool(c, "desc", false)
hosts := queryStrListField(c, "hosts", ",", " ", "\n")
var err error
if len(bgids) == 0 {
user := c.MustGet("user").(*models.User)
@@ -65,11 +67,13 @@ func (rt *Router) targetGets(c *gin.Context) {
bgids = append(bgids, 0)
}
}
options := []models.BuildTargetWhereOption{
models.BuildTargetWhereWithBgids(bgids),
models.BuildTargetWhereWithDsIds(dsIds),
models.BuildTargetWhereWithQuery(query),
models.BuildTargetWhereWithDowntime(downtime),
models.BuildTargetWhereWithHosts(hosts),
}
total, err := models.TargetTotal(rt.Ctx, options...)
ginx.Dangerous(err)
@@ -78,6 +82,13 @@ func (rt *Router) targetGets(c *gin.Context) {
ginx.Offset(c, limit), order, desc, options...)
ginx.Dangerous(err)
tgs, err := models.TargetBusiGroupsGetAll(rt.Ctx)
ginx.Dangerous(err)
for _, t := range list {
t.GroupIds = tgs[t.Ident]
}
if err == nil {
now := time.Now()
cache := make(map[int64]*models.BusiGroup)
@@ -157,7 +168,8 @@ func (rt *Router) targetGetsByService(c *gin.Context) {
func (rt *Router) targetGetTags(c *gin.Context) {
idents := ginx.QueryStr(c, "idents", "")
idents = strings.ReplaceAll(idents, ",", " ")
lst, err := models.TargetGetTags(rt.Ctx, strings.Fields(idents))
ignoreHostTag := ginx.QueryBool(c, "ignore_host_tag", false)
lst, err := models.TargetGetTags(rt.Ctx, strings.Fields(idents), ignoreHostTag)
ginx.NewRender(c).Data(lst, err)
}
@@ -260,9 +272,11 @@ 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 strings.Contains(target.Tags, tagKey+"=") {
if _, ok := hostTagsMap[tagKey]; ok ||
strings.Contains(target.Tags, tagKey+"=") {
return fmt.Errorf("duplicate tagkey(%s)", tagKey)
}
}
@@ -379,8 +393,15 @@ type targetBgidForm struct {
Bgid int64 `json:"bgid"`
}
func (rt *Router) targetUpdateBgid(c *gin.Context) {
var f targetBgidForm
type targetBgidsForm struct {
Idents []string `json:"idents" binding:"required_without=HostIps"`
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
Bgids []int64 `json:"bgids"`
Action string `json:"action"` // add del reset
}
func (rt *Router) targetBindBgids(c *gin.Context) {
var f targetBgidsForm
var err error
var failedResults = make(map[string]string)
ginx.BindJSON(c, &f)
@@ -396,35 +417,24 @@ func (rt *Router) targetUpdateBgid(c *gin.Context) {
}
user := c.MustGet("user").(*models.User)
if user.IsAdmin() {
ginx.NewRender(c).Data(failedResults, models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
return
}
if f.Bgid > 0 {
// 把要操作的机器分成两部分一部分是bgid为0需要管理员分配另一部分bgid>0说明是业务组内部想调整
// 比如原来分配给didiyun的机器didiyun的管理员想把部分机器调整到didiyun-ceph下
// 对于调整的这种情况当前登录用户要对这批机器有操作权限同时还要对目标BG有操作权限
orphans, err := models.IdentsFilter(rt.Ctx, f.Idents, "group_id = ?", 0)
if !user.IsAdmin() {
// 普通用户,检查用户是否有权限操作所有请求的业务组
existing, _, err := models.SeparateTargetIdents(rt.Ctx, f.Idents)
ginx.Dangerous(err)
rt.checkTargetPerm(c, existing)
// 机器里边存在未归组的登录用户就需要是admin
if len(orphans) > 0 && !user.IsAdmin() {
can, err := user.CheckPerm(rt.Ctx, "/targets/bind")
var groupIds []int64
if f.Action == "reset" {
// 如果是复写,则需要检查用户是否有权限操作机器之前的业务组
bgids, err := models.TargetGroupIdsGetByIdents(rt.Ctx, f.Idents)
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
}
groupIds = append(groupIds, bgids...)
}
groupIds = append(groupIds, f.Bgids...)
reBelongs, err := models.IdentsFilter(rt.Ctx, f.Idents, "group_id > ?", 0)
ginx.Dangerous(err)
if len(reBelongs) > 0 {
// 对于这些要重新分配的机器操作者要对这些机器本身有权限同时要对目标bgid有权限
rt.checkTargetPerm(c, f.Idents)
bg := BusiGroup(rt.Ctx, f.Bgid)
for _, bgid := range groupIds {
bg := BusiGroup(rt.Ctx, bgid)
can, err := user.CanDoBusiGroup(rt.Ctx, bg, "rw")
ginx.Dangerous(err)
@@ -432,14 +442,24 @@ func (rt *Router) targetUpdateBgid(c *gin.Context) {
ginx.Bomb(http.StatusForbidden, "No permission. You are not admin of BG(%s)", bg.Name)
}
}
} else if f.Bgid == 0 {
// 退还机器
rt.checkTargetPerm(c, f.Idents)
} else {
ginx.Bomb(http.StatusBadRequest, "invalid bgid")
can, err := user.CheckPerm(rt.Ctx, "/targets/bind")
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
}
}
ginx.NewRender(c).Data(failedResults, models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
switch f.Action {
case "add":
ginx.NewRender(c).Data(failedResults, models.TargetBindBgids(rt.Ctx, f.Idents, f.Bgids))
case "del":
ginx.NewRender(c).Data(failedResults, models.TargetUnbindBgids(rt.Ctx, f.Idents, f.Bgids))
case "reset":
ginx.NewRender(c).Data(failedResults, models.TargetOverrideBgids(rt.Ctx, f.Idents, f.Bgids))
default:
ginx.Bomb(http.StatusBadRequest, "invalid action")
}
}
func (rt *Router) targetUpdateBgidByService(c *gin.Context) {
@@ -458,7 +478,7 @@ func (rt *Router) targetUpdateBgidByService(c *gin.Context) {
ginx.Bomb(http.StatusBadRequest, err.Error())
}
ginx.NewRender(c).Data(failedResults, models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
ginx.NewRender(c).Data(failedResults, models.TargetOverrideBgids(rt.Ctx, f.Idents, []int64{f.Bgid}))
}
type identsForm struct {
@@ -482,7 +502,7 @@ func (rt *Router) targetDel(c *gin.Context) {
ginx.Bomb(http.StatusBadRequest, err.Error())
}
ginx.NewRender(c).Data(failedResults, models.TargetDel(rt.Ctx, f.Idents))
ginx.NewRender(c).Data(failedResults, models.TargetDel(rt.Ctx, f.Idents, rt.TargetDeleteHook))
}
func (rt *Router) targetDelByService(c *gin.Context) {
@@ -501,7 +521,7 @@ func (rt *Router) targetDelByService(c *gin.Context) {
ginx.Bomb(http.StatusBadRequest, err.Error())
}
ginx.NewRender(c).Data(failedResults, models.TargetDel(rt.Ctx, f.Idents))
ginx.NewRender(c).Data(failedResults, models.TargetDel(rt.Ctx, f.Idents, rt.TargetDeleteHook))
}
func (rt *Router) checkTargetPerm(c *gin.Context, idents []string) {

View File

@@ -48,7 +48,7 @@ func (rt *Router) userGets(c *gin.Context) {
order := ginx.QueryStr(c, "order", "username")
desc := ginx.QueryBool(c, "desc", false)
rt.UserCache.UpdateUsersLastActiveTime()
go rt.UserCache.UpdateUsersLastActiveTime()
total, err := models.UserTotal(rt.Ctx, query, stime, etime)
ginx.Dangerous(err)

View File

@@ -1,6 +1,7 @@
package sso
import (
"fmt"
"log"
"time"
@@ -145,7 +146,7 @@ func Init(center cconf.Center, ctx *ctx.Context, configCache *memsto.ConfigCache
}
}
if configCache == nil {
logger.Error("configCache is nil, sso initialization failed")
log.Fatalln(fmt.Errorf("configCache is nil, sso initialization failed"))
}
ssoClient.configCache = configCache
userVariableMap := configCache.Get()

View File

@@ -52,11 +52,13 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
targetCache := memsto.NewTargetCache(ctx, syncStats, redis)
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
configCvalCache := memsto.NewCvalCache(ctx, syncStats)
idents := idents.New(ctx, redis)
metas := metas.New(redis)
writers := writer.NewWriters(config.Pushgw)
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP, configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
pushgwRouter.Config(r)
if !config.Alert.Disable {

View File

@@ -561,7 +561,7 @@ CREATE TABLE alert_cur_event (
target_note varchar(191) not null default '' ,
first_trigger_time bigint,
trigger_time bigint not null,
trigger_value varchar(255) not null,
trigger_value varchar(2048) not null,
annotations text not null ,
rule_config text not null ,
tags varchar(1024) not null default '' ,
@@ -621,7 +621,7 @@ CREATE TABLE alert_his_event (
target_note varchar(191) not null default '' ,
first_trigger_time bigint,
trigger_time bigint not null,
trigger_value varchar(255) not null,
trigger_value varchar(2048) not null,
recover_time bigint not null default 0,
last_eval_time bigint not null default 0 ,
tags varchar(1024) not null default '' ,

View File

@@ -1,6 +1,6 @@
set names utf8mb4;
drop database if exists n9e_v6;
-- drop database if exists n9e_v6;
create database n9e_v6;
use n9e_v6;
@@ -363,6 +363,7 @@ CREATE TABLE `target` (
`ident` varchar(191) not null comment 'target id',
`note` varchar(255) not null default '' comment 'append to alert event as field',
`tags` varchar(512) not null default '' comment 'append to series data as tags, split by space, append external space at suffix',
`host_tags` varchar(512) not null default '' comment 'append to series data as tags, split by space, append external space at suffix',
`host_ip` varchar(15) default '' COMMENT 'IPv4 string',
`agent_version` varchar(255) default '' COMMENT 'agent version',
`engine_name` varchar(255) default '' COMMENT 'engine_name',
@@ -453,7 +454,7 @@ CREATE TABLE `alert_cur_event` (
`target_note` varchar(191) not null default '' comment 'target note',
`first_trigger_time` bigint,
`trigger_time` bigint not null,
`trigger_value` varchar(255) not null,
`trigger_value` text not null,
`annotations` text not null comment 'annotations',
`rule_config` text not null comment 'annotations',
`tags` varchar(1024) not null default '' comment 'merge data_tags rule_tags, split by ,,',
@@ -493,7 +494,7 @@ CREATE TABLE `alert_his_event` (
`target_note` varchar(191) not null default '' comment 'target note',
`first_trigger_time` bigint,
`trigger_time` bigint not null,
`trigger_value` varchar(255) not null,
`trigger_value` text not null,
`recover_time` bigint not null default 0,
`last_eval_time` bigint not null default 0 comment 'for time filter',
`tags` varchar(1024) not null default '' comment 'merge data_tags rule_tags, split by ,,',
@@ -528,6 +529,7 @@ CREATE TABLE `builtin_components` (
CREATE TABLE `builtin_payloads` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '''unique identifier''',
`component_id` bigint(20) NOT NULL DEFAULT 0 COMMENT 'component_id',
`uuid` bigint(20) NOT NULL COMMENT '''uuid of payload''',
`type` varchar(191) NOT NULL COMMENT '''type of payload''',
`component` varchar(191) NOT NULL COMMENT '''component of payload''',
@@ -724,6 +726,15 @@ CREATE TABLE `metric_filter` (
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `target_busi_group` (
`id` bigint NOT NULL AUTO_INCREMENT,
`target_ident` varchar(191) NOT NULL,
`group_id` bigint NOT NULL,
`update_at` bigint NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_target_group` (`target_ident`,`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `task_meta`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT,

View File

@@ -99,4 +99,21 @@ CREATE TABLE notification_record (
`details` VARCHAR(2048),
`created_at` BIGINT NOT NULL,
INDEX idx_evt (event_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/* v7.3.0 2024-08-26 */
ALTER TABLE `target` ADD COLUMN `host_tags` TEXT COMMENT 'global labels set in conf file';
/* v7.3.4 2024-08-28 */
ALTER TABLE `builtin_payloads` ADD COLUMN `component_id` bigint(20) NOT NULL DEFAULT 0 COMMENT 'component_id';
/* v7.4.0 2024-09-20 */
CREATE TABLE `target_busi_group` (
`id` bigint NOT NULL AUTO_INCREMENT,
`target_ident` varchar(191) NOT NULL,
`group_id` bigint NOT NULL,
`update_at` bigint NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_target_group` (`target_ident`,`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -389,7 +389,7 @@ CREATE TABLE `alert_cur_event` (
`target_note` varchar(191) not null default '',
`first_trigger_time` bigint,
`trigger_time` bigint not null,
`trigger_value` varchar(255) not null,
`trigger_value` varchar(2048) not null,
`annotations` text not null,
`rule_config` text not null,
`tags` varchar(1024) not null default ''
@@ -427,7 +427,7 @@ CREATE TABLE `alert_his_event` (
`target_note` varchar(191) not null default '',
`first_trigger_time` bigint,
`trigger_time` bigint not null,
`trigger_value` varchar(255) not null,
`trigger_value` varchar(2048) not null,
`recover_time` bigint not null default 0,
`last_eval_time` bigint not null default 0,
`tags` varchar(1024) not null default '',

File diff suppressed because one or more lines are too long

6
go.mod
View File

@@ -18,6 +18,7 @@ require (
github.com/golang/snappy v0.0.4
github.com/google/uuid v1.3.0
github.com/hashicorp/go-version v1.6.0
github.com/jinzhu/copier v0.4.0
github.com/json-iterator/go v1.1.12
github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7
github.com/mailru/easyjson v0.7.7
@@ -31,8 +32,9 @@ require (
github.com/rakyll/statik v0.1.7
github.com/redis/go-redis/v9 v9.0.2
github.com/spaolacci/murmur3 v1.1.0
github.com/stretchr/testify v1.8.4
github.com/tidwall/gjson v1.14.0
github.com/toolkits/pkg v1.3.6
github.com/toolkits/pkg v1.3.8
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
golang.org/x/oauth2 v0.10.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
@@ -43,6 +45,8 @@ require (
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
)
require github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
require (
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
github.com/beorn7/perks v1.0.1 // indirect

8
go.sum
View File

@@ -160,6 +160,8 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@@ -232,6 +234,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
@@ -286,14 +289,15 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/toolkits/pkg v1.3.6 h1:47e1amsY6mJmcnF3Y2lIpkJXfoYY2RmgI09PtwdAEMU=
github.com/toolkits/pkg v1.3.6/go.mod h1:M9ecwFGW1vxCTUFM9sr2ZjXSKb04N+1sTQ6SA3RNAIU=
github.com/toolkits/pkg v1.3.8 h1:2yamC20c5mHRtbcGiLY99Lm/2mVitFn6onE8KKvMT1o=
github.com/toolkits/pkg v1.3.8/go.mod h1:M9ecwFGW1vxCTUFM9sr2ZjXSKb04N+1sTQ6SA3RNAIU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -584,7 +584,7 @@
"links": [
{
"title": "下钻",
"url": "/dashboards/automq-group-metrics?TSDB=${DS_PROMETHEUS}\u0026cluster_id=${cluster_id}\u0026group_id=${__field.labels.consumer_group}\u0026partition=all\u0026topic=${__field.labels.topic}"
"url": "/built-in-components/dashboard/detail?__uuid__=1717556327172992000&TSDB=${DS_PROMETHEUS}\u0026cluster_id=${cluster_id}\u0026group_id=${__field.labels.consumer_group}\u0026partition=all\u0026topic=${__field.labels.topic}"
}
],
"showHeader": true
@@ -669,7 +669,7 @@
"links": [
{
"title": "下钻",
"url": "/dashboards/automq-topic-metrics?TSDB=${DS_PROMETHEUS}\u0026cluster_id=${cluster_id}\u0026topic=${__field.labels.topic}"
"url": "/built-in-components/dashboard/detail?__uuid__=1717556327174664000&TSDB=${DS_PROMETHEUS}\u0026cluster_id=${cluster_id}\u0026topic=${__field.labels.topic}"
}
],
"showHeader": true
@@ -781,7 +781,7 @@
"links": [
{
"title": "下钻",
"url": "/dashboards/automq-broker-metrics?DS_PROMETHEUS=${DS_PROMETHEUS}\u0026cluster_id=${cluster_id}\u0026node_id=${__field.labels.instance}"
"url": "/built-in-components/dashboard/detail?__uuid__=1717556327159415000&DS_PROMETHEUS=${DS_PROMETHEUS}\u0026cluster_id=${cluster_id}\u0026node_id=${__field.labels.instance}"
}
],
"showHeader": true

View File

@@ -0,0 +1,463 @@
{
"name": "IPMI for Prometheus",
"ident": "",
"configs": {
"version": "2.0.0",
"links": [],
"var": [
{
"name": "node",
"type": "query",
"datasource": {
"cate": "prometheus"
},
"definition": "label_values(ipmi_bmc_info, ident)",
"reg": "",
"multi": false
}
],
"panels": [
{
"type": "gauge",
"id": "f975fded-f57e-4a6e-80b4-50d5be6dd84c",
"layout": {
"h": 7,
"w": 24,
"x": 0,
"y": 0,
"i": "f975fded-f57e-4a6e-80b4-50d5be6dd84c",
"isResizable": true
},
"version": "2.0.0",
"datasourceCate": "prometheus",
"targets": [
{
"refId": "A",
"expr": "ipmi_temperature_celsius{ident='$node'}",
"legend": "{{name}}"
}
],
"transformations": [
{
"id": "organize",
"options": {}
}
],
"name": "Temperatures",
"links": [],
"custom": {
"textMode": "valueAndName",
"calc": "avg"
},
"options": {
"valueMappings": [],
"standardOptions": {
"util": "none"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null,
"type": "base"
},
{
"color": "red",
"value": 80
}
]
}
}
},
{
"type": "timeseries",
"id": "681f1191-4777-4377-8b77-404d9f036406",
"layout": {
"h": 5,
"w": 12,
"x": 0,
"y": 7,
"i": "681f1191-4777-4377-8b77-404d9f036406",
"isResizable": true
},
"version": "2.0.0",
"datasourceCate": "prometheus",
"targets": [
{
"refId": "A",
"expr": "ipmi_power_watts{ident='$node'}",
"legend": "{{name}}"
}
],
"transformations": [
{
"id": "organize",
"options": {}
}
],
"name": "Power",
"links": [],
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {},
"thresholds": {
"steps": [
{
"color": "#634CD9",
"value": null,
"type": "base"
}
]
}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"spanNulls": false,
"lineWidth": 1,
"fillOpacity": 0.5,
"gradientMode": "none",
"stack": "off",
"scaleDistribution": {
"type": "linear"
}
}
},
{
"type": "timeseries",
"id": "feede24c-8296-4127-982e-08cfc4151933",
"layout": {
"h": 5,
"w": 12,
"x": 12,
"y": 7,
"i": "feede24c-8296-4127-982e-08cfc4151933",
"isResizable": true
},
"version": "2.0.0",
"datasourceCate": "prometheus",
"targets": [
{
"refId": "A",
"expr": "ipmi_power_watts{ident='$node'} * 30 * 24 ",
"legend": "{{name}}"
}
],
"transformations": [
{
"id": "organize",
"options": {}
}
],
"name": "Power usage 30d",
"links": [],
"options": {
"tooltip": {
"mode": "all",
"sort": "none"
},
"legend": {
"displayMode": "hidden"
},
"standardOptions": {},
"thresholds": {
"steps": [
{
"color": "#634CD9",
"value": null,
"type": "base"
}
]
}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "smooth",
"spanNulls": false,
"lineWidth": 1,
"fillOpacity": 0.5,
"gradientMode": "none",
"stack": "off",
"scaleDistribution": {
"type": "linear"
}
}
},
{
"type": "timeseries",
"id": "9e11e7f5-ed3c-49eb-8a72-ee76c8700c24",
"layout": {
"h": 7,
"w": 12,
"x": 0,
"y": 12,
"i": "9e11e7f5-ed3c-49eb-8a72-ee76c8700c24",
"isResizable": true
},
"version": "2.0.0",
"datasourceCate": "prometheus",
"targets": [
{
"refId": "A",
"expr": "ipmi_temperature_celsius{ident='$node'}",
"legend": "{{name}}"
}
],
"transformations": [
{
"id": "organize",
"options": {}
}
],
"name": "Temperatures",
"links": [],
"description": "",
"options": {
"tooltip": {
"mode": "multi"
},
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"standardOptions": {
"util": "none"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null,
"type": "base"
},
{
"color": "red",
"value": 80
}
]
}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "linear",
"spanNulls": false,
"lineWidth": 1,
"fillOpacity": 0.5,
"gradientMode": "none",
"stack": "off",
"scaleDistribution": {
"type": "linear"
}
}
},
{
"type": "timeseries",
"id": "95c734f7-26cb-41a7-8376-49332cc220c2",
"layout": {
"h": 7,
"w": 12,
"x": 12,
"y": 12,
"i": "95c734f7-26cb-41a7-8376-49332cc220c2",
"isResizable": true
},
"version": "2.0.0",
"datasourceCate": "prometheus",
"targets": [
{
"refId": "A",
"expr": "ipmi_power_watts{ident='$node'}",
"legend": "{{name}}"
}
],
"transformations": [
{
"id": "organize",
"options": {}
}
],
"name": "Power",
"links": [],
"description": "",
"options": {
"tooltip": {
"mode": "multi"
},
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"standardOptions": {
"util": "none"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null,
"type": "base"
},
{
"color": "red",
"value": 80
}
]
}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "linear",
"spanNulls": false,
"lineWidth": 1,
"fillOpacity": 0.01,
"gradientMode": "none",
"stack": "off",
"scaleDistribution": {
"type": "linear"
}
}
},
{
"type": "timeseries",
"id": "0313f34f-afcf-41e9-8f69-9a3dbd4b2e56",
"layout": {
"h": 7,
"w": 12,
"x": 0,
"y": 19,
"i": "0313f34f-afcf-41e9-8f69-9a3dbd4b2e56",
"isResizable": true
},
"version": "2.0.0",
"datasourceCate": "prometheus",
"targets": [
{
"refId": "A",
"expr": "ipmi_fan_speed_rpm{ident='$node'}",
"legend": "{{name}}"
}
],
"transformations": [
{
"id": "organize",
"options": {}
}
],
"name": "Fans",
"links": [],
"description": "",
"options": {
"tooltip": {
"mode": "multi"
},
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"standardOptions": {
"util": "none"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null,
"type": "base"
},
{
"color": "red",
"value": 80
}
]
}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "linear",
"spanNulls": false,
"lineWidth": 1,
"fillOpacity": 0.5,
"gradientMode": "none",
"stack": "off",
"scaleDistribution": {
"type": "linear"
}
}
},
{
"type": "timeseries",
"id": "29ee004d-a95c-405d-97d1-d715fab4e1de",
"layout": {
"h": 7,
"w": 12,
"x": 12,
"y": 19,
"i": "29ee004d-a95c-405d-97d1-d715fab4e1de",
"isResizable": true
},
"version": "2.0.0",
"datasourceCate": "prometheus",
"targets": [
{
"refId": "A",
"expr": "ipmi_voltage_volts{ident='$node',name!~\"Voltage 1|Voltage 2\"}",
"legend": "{{name}}"
}
],
"transformations": [
{
"id": "organize",
"options": {}
}
],
"name": "Voltages",
"links": [],
"description": "",
"options": {
"tooltip": {
"mode": "multi"
},
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"standardOptions": {
"util": "none"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null,
"type": "base"
},
{
"color": "red",
"value": 80
}
]
}
},
"custom": {
"drawStyle": "lines",
"lineInterpolation": "linear",
"spanNulls": false,
"lineWidth": 1,
"fillOpacity": 0.5,
"gradientMode": "none",
"stack": "off",
"scaleDistribution": {
"type": "linear"
}
}
}
]
},
"uuid": 1727587308068775200
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1425,7 +1425,7 @@
"rule_config": {
"queries": [
{
"prom_ql": "kernel_vmstat_oom_kill != 0",
"prom_ql": "increase(kernel_vmstat_oom_kill[2m]) > 0",
"severity": 2
}
]
@@ -2139,4 +2139,4 @@
"update_by": "",
"uuid": 1717556327737117000
}
]
]

View File

@@ -1,13 +1,6 @@
{
"id": 0,
"group_id": 0,
"name": "机器台账表格视图",
"ident": "",
"tags": "",
"create_at": 0,
"create_by": "",
"update_at": 0,
"update_by": "",
"configs": {
"links": [
{
@@ -28,7 +21,7 @@
"colorRange": [
"thresholds"
],
"detailUrl": "/dashboards-built-in/detail?__built-in-cate=Linux\u0026__built-in-name=Linux%20Host%20by%20Categraf%20v2\u0026ident=${__field.labels.ident}",
"detailUrl": "/built-in-components/dashboard/detail?__uuid__=1717556327744505000&ident=${__field.labels.ident}",
"textMode": "valueAndName",
"valueField": "Value"
},
@@ -98,7 +91,7 @@
"colorRange": [
"thresholds"
],
"detailUrl": "/dashboards-built-in/detail?__built-in-cate=Linux\u0026__built-in-name=Linux%20Host%20by%20Categraf%20v2\u0026ident=${__field.labels.ident}",
"detailUrl": "/built-in-components/dashboard/detail?__uuid__=1717556327744505000&ident=${__field.labels.ident}",
"textMode": "valueAndName",
"valueField": "Value"
},
@@ -171,13 +164,16 @@
"linkMode": "appendLinkColumn",
"links": [
{
"targetBlank": true,
"title": "详情",
"url": "/dashboards-built-in/detail?__built-in-cate=Linux\u0026__built-in-name=Linux%20Host%20by%20Categraf%20v2\u0026ident=${__field.labels.ident}"
"url": "/built-in-components/dashboard/detail?__uuid__=1717556327744505000&ident=${__field.labels.ident}"
}
],
"nowrap": false,
"showHeader": true,
"sortColumn": "ident",
"sortOrder": "ascend"
"sortOrder": "ascend",
"tableLayout": "fixed"
},
"datasourceCate": "prometheus",
"datasourceValue": "${prom}",
@@ -385,10 +381,5 @@
],
"version": "3.0.0"
},
"public": 0,
"public_cate": 0,
"bgids": null,
"built_in": 0,
"hide": 0,
"uuid": 1717556327742611000
}

View File

@@ -2051,7 +2051,7 @@
"cate": "prometheus",
"value": "${prom}"
},
"definition": "label_values(node_cpu_seconds_total, instance)",
"definition": "label_values(node_uname_info, instance)",
"name": "node",
"selected": "$node",
"type": "query"

View File

@@ -252,7 +252,7 @@
"cate": "prometheus",
"value": "${prom}"
},
"definition": "label_values(node_cpu_seconds_total, instance)",
"definition": "label_values(node_uname_info, instance)",
"name": "node",
"selected": "$node",
"type": "query"

View File

@@ -259,11 +259,11 @@
"uuid": 1717556327796195000,
"collector": "Categraf",
"typ": "Linux",
"name": "OOM 次数统计",
"name": "1分钟内 OOM 次数统计",
"unit": "none",
"note": "取自 `/proc/vmstat`,需要较高版本的内核,没记错的话应该是 4.13 以上版本",
"lang": "zh_CN",
"expression": "kernel_vmstat_oom_kill",
"expression": "increase(kernel_vmstat_oom_kill[1m])",
"created_at": 0,
"created_by": "",
"updated_at": 0,
@@ -1334,4 +1334,4 @@
"updated_at": 0,
"updated_by": ""
}
]
]

View File

@@ -1,251 +0,0 @@
[
{
"id": 0,
"group_id": 0,
"cate": "prometheus",
"datasource_ids": [
0
],
"cluster": "",
"name": "Process X high number of open files - exporter",
"note": "",
"prod": "metric",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 2,
"severities": [
2
],
"disabled": 1,
"prom_for_duration": 60,
"prom_ql": "",
"rule_config": {
"algo_params": null,
"inhibit": false,
"prom_ql": "",
"queries": [
{
"prom_ql": "avg by (instance) (namedprocess_namegroup_worst_fd_ratio{groupname=\"X\"}) * 100 \u003e 80",
"severity": 2
}
],
"severity": 0
},
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_stimes": [
"00:00"
],
"enable_etime": "23:59",
"enable_etimes": [
"23:59"
],
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_days_of_weeks": [
[
"1",
"2",
"3",
"4",
"5",
"6",
"0"
]
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_groups_obj": null,
"notify_groups": null,
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=ProcessHighOpenFiles"
],
"annotations": null,
"extra_config": null,
"create_at": 0,
"create_by": "",
"update_at": 0,
"update_by": "",
"uuid": 1717556328248638000
},
{
"id": 0,
"group_id": 0,
"cate": "prometheus",
"datasource_ids": [
0
],
"cluster": "",
"name": "Process X is down - exporter",
"note": "",
"prod": "metric",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 1,
"severities": [
1
],
"disabled": 1,
"prom_for_duration": 0,
"prom_ql": "",
"rule_config": {
"algo_params": null,
"inhibit": false,
"prom_ql": "",
"queries": [
{
"prom_ql": "sum by (instance) (namedprocess_namegroup_num_procs{groupname=\"X\"}) == 0",
"severity": 1
}
],
"severity": 0
},
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_stimes": [
"00:00"
],
"enable_etime": "23:59",
"enable_etimes": [
"23:59"
],
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_days_of_weeks": [
[
"1",
"2",
"3",
"4",
"5",
"6",
"0"
]
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_groups_obj": null,
"notify_groups": null,
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=ProcessNotRunning"
],
"annotations": null,
"extra_config": null,
"create_at": 0,
"create_by": "",
"update_at": 0,
"update_by": "",
"uuid": 1717556328249307000
},
{
"id": 0,
"group_id": 0,
"cate": "prometheus",
"datasource_ids": [
0
],
"cluster": "",
"name": "Process X is restarted - exporter",
"note": "",
"prod": "metric",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 3,
"severities": [
3
],
"disabled": 1,
"prom_for_duration": 0,
"prom_ql": "",
"rule_config": {
"algo_params": null,
"inhibit": false,
"prom_ql": "",
"queries": [
{
"prom_ql": "namedprocess_namegroup_oldest_start_time_seconds{groupname=\"X\"} \u003e time() - 60 ",
"severity": 3
}
],
"severity": 0
},
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_stimes": [
"00:00"
],
"enable_etime": "23:59",
"enable_etimes": [
"23:59"
],
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_days_of_weeks": [
[
"1",
"2",
"3",
"4",
"5",
"6",
"0"
]
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [],
"notify_groups_obj": null,
"notify_groups": null,
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [
"alertname=ProcessRestarted"
],
"annotations": null,
"extra_config": null,
"create_at": 0,
"create_by": "",
"update_at": 0,
"update_by": "",
"uuid": 1717556328249744000
}
]

View File

@@ -1,172 +0,0 @@
[
{
"id": 0,
"group_id": 0,
"cate": "prometheus",
"datasource_ids": [
0
],
"cluster": "",
"name": "process handle limit is too low",
"note": "",
"prod": "metric",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 3,
"severities": [
3
],
"disabled": 1,
"prom_for_duration": 60,
"prom_ql": "",
"rule_config": {
"algo_params": null,
"inhibit": false,
"prom_ql": "",
"queries": [
{
"prom_ql": "procstat_rlimit_num_fds_soft \u003c 2048",
"severity": 3
}
],
"severity": 0
},
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_stimes": [
"00:00"
],
"enable_etime": "23:59",
"enable_etimes": [
"23:59"
],
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_days_of_weeks": [
[
"1",
"2",
"3",
"4",
"5",
"6",
"0"
]
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [
"email",
"dingtalk",
"wecom"
],
"notify_groups_obj": null,
"notify_groups": null,
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [],
"annotations": null,
"extra_config": null,
"create_at": 0,
"create_by": "",
"update_at": 0,
"update_by": "",
"uuid": 1717556328251224000
},
{
"id": 0,
"group_id": 0,
"cate": "prometheus",
"datasource_ids": [
0
],
"cluster": "",
"name": "there is a process count of 0, indicating that a certain process may have crashed",
"note": "",
"prod": "metric",
"algorithm": "",
"algo_params": null,
"delay": 0,
"severity": 1,
"severities": [
1
],
"disabled": 1,
"prom_for_duration": 60,
"prom_ql": "",
"rule_config": {
"algo_params": null,
"inhibit": false,
"prom_ql": "",
"queries": [
{
"prom_ql": "procstat_lookup_count == 0",
"severity": 1
}
],
"severity": 0
},
"prom_eval_interval": 15,
"enable_stime": "00:00",
"enable_stimes": [
"00:00"
],
"enable_etime": "23:59",
"enable_etimes": [
"23:59"
],
"enable_days_of_week": [
"1",
"2",
"3",
"4",
"5",
"6",
"0"
],
"enable_days_of_weeks": [
[
"1",
"2",
"3",
"4",
"5",
"6",
"0"
]
],
"enable_in_bg": 0,
"notify_recovered": 1,
"notify_channels": [
"email",
"dingtalk",
"wecom"
],
"notify_groups_obj": null,
"notify_groups": null,
"notify_repeat_step": 60,
"notify_max_number": 0,
"recover_duration": 0,
"callbacks": [],
"runbook_url": "",
"append_tags": [],
"annotations": null,
"extra_config": null,
"create_at": 0,
"create_by": "",
"update_at": 0,
"update_by": "",
"uuid": 1717556328254260000
}
]

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -2085,7 +2085,6 @@
],
"var": [
{
"defaultValue": 2,
"definition": "prometheus",
"label": "datasource",
"name": "datasource",

150
memsto/config_cval_cache.go Normal file
View File

@@ -0,0 +1,150 @@
package memsto
import (
"encoding/json"
"log"
"sync"
"time"
"github.com/ccfos/nightingale/v6/dumper"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/pkg/errors"
"github.com/toolkits/pkg/logger"
)
type CvalCache struct {
statTotal int64
statLastUpdated int64
ctx *ctx.Context
stats *Stats
mu sync.RWMutex
cvals map[string]string
}
func NewCvalCache(ctx *ctx.Context, stats *Stats) *CvalCache {
cvalCache := &CvalCache{
statTotal: -1,
statLastUpdated: -1,
ctx: ctx,
stats: stats,
cvals: make(map[string]string),
}
cvalCache.initSyncConfigs()
return cvalCache
}
func (c *CvalCache) initSyncConfigs() {
err := c.syncConfigs()
if err != nil {
log.Fatalln("failed to sync configs:", err)
}
go c.loopSyncConfigs()
}
func (c *CvalCache) loopSyncConfigs() {
duration := time.Duration(9000) * time.Millisecond
for {
time.Sleep(duration)
if err := c.syncConfigs(); err != nil {
logger.Warning("failed to sync configs:", err)
}
}
}
func (c *CvalCache) syncConfigs() error {
start := time.Now()
stat, err := models.ConfigCvalStatistics(c.ctx)
if err != nil {
dumper.PutSyncRecord("cvals", start.Unix(), -1, -1, "failed to query statistics: "+err.Error())
return errors.WithMessage(err, "failed to call ConfigCvalStatistics")
}
if !c.statChanged(stat.Total, stat.LastUpdated) {
c.stats.GaugeCronDuration.WithLabelValues("sync_cvals").Set(0)
c.stats.GaugeSyncNumber.WithLabelValues("sync_cvals").Set(0)
dumper.PutSyncRecord("cvals", start.Unix(), -1, -1, "not changed")
return nil
}
cvals, err := models.ConfigsGetAll(c.ctx)
if err != nil {
dumper.PutSyncRecord("cvals", start.Unix(), -1, -1, "failed to query records: "+err.Error())
return errors.WithMessage(err, "failed to call ConfigsGet")
}
c.Set(cvals, stat.Total, stat.LastUpdated)
ms := time.Since(start).Milliseconds()
c.stats.GaugeCronDuration.WithLabelValues("sync_cvals").Set(float64(ms))
c.stats.GaugeSyncNumber.WithLabelValues("sync_cvals").Set(float64(len(c.cvals)))
logger.Infof("timer: sync cvals done, cost: %dms", ms)
dumper.PutSyncRecord("cvals", start.Unix(), ms, len(c.cvals), "success")
return nil
}
func (c *CvalCache) statChanged(total int64, updated int64) bool {
if c.statTotal == total && c.statLastUpdated == updated {
return false
}
return true
}
func (c *CvalCache) Set(cvals []*models.Configs, total int64, updated int64) {
c.mu.Lock()
defer c.mu.Unlock()
c.statTotal = total
c.statLastUpdated = updated
for _, cfg := range cvals {
c.cvals[cfg.Ckey] = cfg.Cval
}
}
func (c *CvalCache) Get(ckey string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cvals[ckey]
}
func (c *CvalCache) GetLastUpdateTime() int64 {
c.mu.RLock()
defer c.mu.RUnlock()
return c.statLastUpdated
}
type SiteInfo struct {
PrintBodyPaths []string `json:"print_body_paths"`
PrintAccessLog bool `json:"print_access_log"`
}
func (c *CvalCache) GetSiteInfo() *SiteInfo {
c.mu.RLock()
defer c.mu.RUnlock()
si := SiteInfo{}
if siteInfoStr := c.Get("site_info"); siteInfoStr != "" {
if err := json.Unmarshal([]byte(siteInfoStr), &si); err != nil {
logger.Errorf("Failed to unmarshal site info: %v", err)
}
}
return &si
}
func (c *CvalCache) PrintBodyPaths() map[string]struct{} {
printBodyPaths := c.GetSiteInfo().PrintBodyPaths
pbp := make(map[string]struct{}, len(printBodyPaths))
for _, p := range printBodyPaths {
pbp[p] = struct{}{}
}
return pbp
}
func (c *CvalCache) PrintAccessLog() bool {
return c.GetSiteInfo().PrintAccessLog
}

View File

@@ -160,8 +160,9 @@ func (tc *TargetCacheType) syncTargets() error {
}
m := make(map[string]*models.Target)
if tc.ctx.IsCenter {
metaMap := tc.GetHostMetas(lst)
metaMap := tc.GetHostMetas(lst)
if len(metaMap) > 0 {
for i := 0; i < len(lst); i++ {
if meta, ok := metaMap[lst[i].Ident]; ok {
lst[i].FillMeta(meta)

View File

@@ -67,6 +67,8 @@ type AlertCurEvent struct {
Claimant string `json:"claimant" gorm:"-"`
SubRuleId int64 `json:"sub_rule_id" gorm:"-"`
ExtraInfo []string `json:"extra_info" gorm:"-"`
Target *Target `json:"target" gorm:"-"`
RecoverConfig RecoverConfig `json:"recover_config" gorm:"-"`
}
func (e *AlertCurEvent) TableName() string {
@@ -341,13 +343,18 @@ func (e *AlertCurEvent) DB2Mem() {
continue
}
arr := strings.Split(pair, "=")
arr := strings.SplitN(pair, "=", 2)
if len(arr) != 2 {
continue
}
e.TagsMap[arr[0]] = arr[1]
}
// 解决之前数据库中 FirstTriggerTime 为 0 的情况
if e.FirstTriggerTime == 0 {
e.FirstTriggerTime = e.TriggerTime
}
}
func FillRuleConfigTplName(ctx *ctx.Context, ruleConfig string) (interface{}, bool) {
@@ -413,7 +420,8 @@ func (e *AlertCurEvent) FillNotifyGroups(ctx *ctx.Context, cache map[int64]*User
return nil
}
func AlertCurEventTotal(ctx *ctx.Context, prods []string, bgids []int64, stime, etime int64, severity int, dsIds []int64, cates []string, query string) (int64, error) {
func AlertCurEventTotal(ctx *ctx.Context, prods []string, bgids []int64, stime, etime int64,
severity int, dsIds []int64, cates []string, ruleId int64, query string) (int64, error) {
session := DB(ctx).Model(&AlertCurEvent{})
if stime != 0 && etime != 0 {
session = session.Where("trigger_time between ? and ?", stime, etime)
@@ -438,6 +446,10 @@ func AlertCurEventTotal(ctx *ctx.Context, prods []string, bgids []int64, stime,
session = session.Where("cate in ?", cates)
}
if ruleId > 0 {
session = session.Where("rule_id = ?", ruleId)
}
if query != "" {
arr := strings.Fields(query)
for i := 0; i < len(arr); i++ {
@@ -449,7 +461,9 @@ func AlertCurEventTotal(ctx *ctx.Context, prods []string, bgids []int64, stime,
return Count(session)
}
func AlertCurEventGets(ctx *ctx.Context, prods []string, bgids []int64, stime, etime int64, severity int, dsIds []int64, cates []string, query string, limit, offset int) ([]AlertCurEvent, error) {
func AlertCurEventsGet(ctx *ctx.Context, prods []string, bgids []int64, stime, etime int64,
severity int, dsIds []int64, cates []string, ruleId int64, query string, limit, offset int) (
[]AlertCurEvent, error) {
session := DB(ctx).Model(&AlertCurEvent{})
if stime != 0 && etime != 0 {
session = session.Where("trigger_time between ? and ?", stime, etime)
@@ -474,6 +488,10 @@ func AlertCurEventGets(ctx *ctx.Context, prods []string, bgids []int64, stime, e
session = session.Where("cate in ?", cates)
}
if ruleId > 0 {
session = session.Where("rule_id = ?", ruleId)
}
if query != "" {
arr := strings.Fields(query)
for i := 0; i < len(arr); i++ {
@@ -494,6 +512,26 @@ func AlertCurEventGets(ctx *ctx.Context, prods []string, bgids []int64, stime, e
return lst, err
}
func AlertCurEventCountByRuleId(ctx *ctx.Context, rids []int64, stime, etime int64) map[int64]int64 {
type Row struct {
RuleId int64
Cnt int64
}
var rows []Row
err := DB(ctx).Model(&AlertCurEvent{}).Select("rule_id, count(*) as cnt").
Where("trigger_time between ? and ?", stime, etime).Group("rule_id").Find(&rows).Error
if err != nil {
logger.Errorf("Failed to count group by rule_id: %v", err)
return nil
}
curEventTotalByRid := make(map[int64]int64, len(rids))
for _, r := range rows {
curEventTotalByRid[r.RuleId] = r.Cnt
}
return curEventTotalByRid
}
func AlertCurEventDel(ctx *ctx.Context, ids []int64) error {
if len(ids) == 0 {
return nil
@@ -503,6 +541,11 @@ func AlertCurEventDel(ctx *ctx.Context, ids []int64) error {
}
func AlertCurEventDelByHash(ctx *ctx.Context, hash string) error {
if !ctx.IsCenter {
_, err := poster.GetByUrls[string](ctx, "/v1/n9e/alert-cur-events-del-by-hash?hash="+hash)
return err
}
return DB(ctx).Where("hash = ?", hash).Delete(&AlertCurEvent{}).Error
}

View File

@@ -121,7 +121,9 @@ func (e *AlertHisEvent) FillNotifyGroups(ctx *ctx.Context, cache map[int64]*User
// }
func AlertHisEventTotal(ctx *ctx.Context, prods []string, bgids []int64, stime, etime int64, severity int, recovered int, dsIds []int64, cates []string, query string) (int64, error) {
func AlertHisEventTotal(
ctx *ctx.Context, prods []string, bgids []int64, stime, etime int64, severity int,
recovered int, dsIds []int64, cates []string, ruleId int64, query string) (int64, error) {
session := DB(ctx).Model(&AlertHisEvent{}).Where("last_eval_time between ? and ?", stime, etime)
if len(prods) > 0 {
@@ -148,6 +150,10 @@ func AlertHisEventTotal(ctx *ctx.Context, prods []string, bgids []int64, stime,
session = session.Where("cate in ?", cates)
}
if ruleId > 0 {
session = session.Where("rule_id = ?", ruleId)
}
if query != "" {
arr := strings.Fields(query)
for i := 0; i < len(arr); i++ {
@@ -159,7 +165,9 @@ func AlertHisEventTotal(ctx *ctx.Context, prods []string, bgids []int64, stime,
return Count(session)
}
func AlertHisEventGets(ctx *ctx.Context, prods []string, bgids []int64, stime, etime int64, severity int, recovered int, dsIds []int64, cates []string, query string, limit, offset int) ([]AlertHisEvent, error) {
func AlertHisEventGets(ctx *ctx.Context, prods []string, bgids []int64, stime, etime int64,
severity int, recovered int, dsIds []int64, cates []string, ruleId int64, query string,
limit, offset int) ([]AlertHisEvent, error) {
session := DB(ctx).Where("last_eval_time between ? and ?", stime, etime)
if len(prods) != 0 {
@@ -186,6 +194,10 @@ func AlertHisEventGets(ctx *ctx.Context, prods []string, bgids []int64, stime, e
session = session.Where("cate in ?", cates)
}
if ruleId > 0 {
session = session.Where("rule_id = ?", ruleId)
}
if query != "" {
arr := strings.Fields(query)
for i := 0; i < len(arr); i++ {

View File

@@ -108,7 +108,7 @@ func AlertMuteGet(ctx *ctx.Context, where string, args ...interface{}) (*AlertMu
return lst[0], err
}
func AlertMuteGets(ctx *ctx.Context, prods []string, bgid int64, query string) (lst []AlertMute, err error) {
func AlertMuteGets(ctx *ctx.Context, prods []string, bgid int64, disabled int, query string) (lst []AlertMute, err error) {
session := DB(ctx)
if bgid != -1 {
@@ -119,6 +119,14 @@ func AlertMuteGets(ctx *ctx.Context, prods []string, bgid int64, query string) (
session = session.Where("prod in (?)", prods)
}
if disabled != -1 {
if disabled == 0 {
session = session.Where("disabled = 0")
} else {
session = session.Where("disabled = 1")
}
}
if query != "" {
arr := strings.Fields(query)
for i := 0; i < len(arr); i++ {
@@ -287,16 +295,9 @@ func AlertMuteStatistics(ctx *ctx.Context) (*Statistics, error) {
return s, err
}
// clean expired first
buf := int64(30)
err := DB(ctx).Where("etime < ? and mute_time_type = 0", time.Now().Unix()-buf).Delete(new(AlertMute)).Error
if err != nil {
return nil, err
}
session := DB(ctx).Model(&AlertMute{}).Select("count(*) as total", "max(update_at) as last_updated")
err = session.Find(&stats).Error
err := session.Find(&stats).Error
if err != nil {
return nil, err
}
@@ -308,7 +309,7 @@ func AlertMuteGetsAll(ctx *ctx.Context) ([]*AlertMute, error) {
// get my cluster's mutes
var lst []*AlertMute
if !ctx.IsCenter {
lst, err := poster.GetByUrls[[]*AlertMute](ctx, "/v1/n9e/alert-mutes")
lst, err := poster.GetByUrls[[]*AlertMute](ctx, "/v1/n9e/alert-mutes?disabled=0")
if err != nil {
return nil, err
}
@@ -318,7 +319,7 @@ func AlertMuteGetsAll(ctx *ctx.Context) ([]*AlertMute, error) {
return lst, err
}
session := DB(ctx).Model(&AlertMute{})
session := DB(ctx).Model(&AlertMute{}).Where("disabled = 0")
err := session.Find(&lst).Error
if err != nil {

View File

@@ -11,6 +11,7 @@ import (
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/str"
@@ -97,6 +98,8 @@ type AlertRule struct {
UpdateAt int64 `json:"update_at"`
UpdateBy string `json:"update_by"`
UUID int64 `json:"uuid" gorm:"-"` // tpl identifier
CurEventCount int64 `json:"cur_event_count" gorm:"-"`
UpdateByNickname string `json:"update_by_nickname" gorm:"-"` // for fe
}
type Tpl struct {
@@ -125,6 +128,19 @@ type PromRuleConfig struct {
AlgoParams interface{} `json:"algo_params"`
}
type RecoverJudge int
const (
Origin RecoverJudge = 0
RecoverWithoutData RecoverJudge = 1
RecoverOnCondition RecoverJudge = 2
)
type RecoverConfig struct {
JudgeType RecoverJudge `json:"judge_type"`
RecoverExp string `json:"recover_exp"`
}
type HostRuleConfig struct {
Queries []HostQuery `json:"queries"`
Triggers []HostTrigger `json:"triggers"`
@@ -132,8 +148,9 @@ type HostRuleConfig struct {
}
type PromQuery struct {
PromQl string `json:"prom_ql"`
Severity int `json:"severity"`
PromQl string `json:"prom_ql"`
Severity int `json:"severity"`
RecoverConfig RecoverConfig `json:"recover_config"`
}
type HostTrigger struct {
@@ -159,6 +176,15 @@ type Trigger struct {
Type string `json:"type,omitempty"`
Duration int `json:"duration,omitempty"`
Percent int `json:"percent,omitempty"`
Joins []Join `json:"joins"`
JoinRef string `json:"join_ref"`
RecoverConfig RecoverConfig `json:"recover_config"`
}
type Join struct {
JoinType string `json:"join_type"`
Ref string `json:"ref"`
On []string `json:"on"`
}
func GetHostsQuery(queries []HostQuery) []map[string]interface{} {
@@ -169,9 +195,10 @@ func GetHostsQuery(queries []HostQuery) []map[string]interface{} {
case "group_ids":
ids := ParseInt64(q.Values)
if q.Op == "==" {
m["group_id in (?)"] = ids
m["target_busi_group.group_id in (?)"] = ids
} else {
m["group_id not in (?)"] = ids
m["target.ident not in (select target_ident "+
"from target_busi_group where group_id in (?))"] = ids
}
case "tags":
lst := []string{}
@@ -185,12 +212,14 @@ func GetHostsQuery(queries []HostQuery) []map[string]interface{} {
blank := " "
for _, tag := range lst {
m["tags like ?"+blank] = "%" + tag + "%"
m["host_tags like ?"+blank] = "%" + tag + "%"
blank += " "
}
} else {
blank := " "
for _, tag := range lst {
m["tags not like ?"+blank] = "%" + tag + "%"
m["host_tags not like ?"+blank] = "%" + tag + "%"
blank += " "
}
}
@@ -809,7 +838,8 @@ func AlertRuleGetsAll(ctx *ctx.Context) ([]*AlertRule, error) {
return lst, nil
}
func AlertRulesGetsBy(ctx *ctx.Context, prods []string, query, algorithm, cluster string, cates []string, disabled int) ([]*AlertRule, error) {
func AlertRulesGetsBy(ctx *ctx.Context, prods []string, query, algorithm, cluster string,
cates []string, disabled int) ([]*AlertRule, error) {
session := DB(ctx)
if len(prods) > 0 {
@@ -1080,3 +1110,19 @@ func GetTargetsOfHostAlertRule(ctx *ctx.Context, engineName string) (map[string]
return m, nil
}
func (ar *AlertRule) Copy(ctx *ctx.Context) (*AlertRule, error) {
newAr := &AlertRule{}
err := copier.Copy(newAr, ar)
if err != nil {
logger.Errorf("copy alert rule failed, %v", err)
}
return newAr, err
}
func InsertAlertRule(ctx *ctx.Context, ars []*AlertRule) error {
if len(ars) == 0 {
return nil
}
return DB(ctx).Create(ars).Error
}

View File

@@ -286,3 +286,53 @@ func BoardSetHide(ctx *ctx.Context, ids []int64) error {
return nil
})
}
func BoardGetsByBids(ctx *ctx.Context, bids []int64) ([]map[string]interface{}, error) {
var boards []Board
err := DB(ctx).Where("id IN ?", bids).Find(&boards).Error
if err != nil {
return nil, err
}
// 收集所有唯一的 group_id
groupIDs := make([]int64, 0)
groupIDSet := make(map[int64]struct{})
for _, board := range boards {
if _, exists := groupIDSet[board.GroupId]; !exists {
groupIDs = append(groupIDs, board.GroupId)
groupIDSet[board.GroupId] = struct{}{}
}
}
// 一次性查询所有需要的 BusiGroup
var busiGroups []BusiGroup
err = DB(ctx).Where("id IN ?", groupIDs).Find(&busiGroups).Error
if err != nil {
return nil, err
}
// 创建 group_id 到 BusiGroup 的映射
groupMap := make(map[int64]BusiGroup)
for _, bg := range busiGroups {
groupMap[bg.Id] = bg
}
result := make([]map[string]interface{}, 0, len(boards))
for _, board := range boards {
busiGroup, exists := groupMap[board.GroupId]
if !exists {
// 处理找不到对应 BusiGroup 的情况
continue
}
item := map[string]interface{}{
"busi_group_name": busiGroup.Name,
"busi_group_id": busiGroup.Id,
"board_id": board.Id,
"board_name": board.Name,
}
result = append(result, item)
}
return result, nil
}

View File

@@ -11,7 +11,7 @@ import (
// BuiltinComponent represents a builtin component along with its metadata.
type BuiltinComponent struct {
ID uint64 `json:"id" gorm:"primaryKey;type:bigint;autoIncrement;comment:'unique identifier'"`
Ident string `json:"ident" gorm:"type:varchar(191);not null;index:idx_ident,sort:asc;comment:'identifier of component'"`
Ident string `json:"ident" gorm:"type:varchar(191);not null;uniqueIndex:idx_ident,sort:asc;comment:'identifier of component'"`
Logo string `json:"logo" gorm:"type:varchar(191);not null;comment:'logo of component'"`
Readme string `json:"readme" gorm:"type:text;not null;comment:'readme of component'"`
CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:'create time'"`

View File

@@ -2,6 +2,7 @@ package models
import (
"errors"
"fmt"
"strings"
"time"
@@ -217,3 +218,10 @@ func BuiltinMetricCollectors(ctx *ctx.Context, lang, typ, query string) ([]strin
err := session.Select("distinct(collector)").Pluck("collector", &collectors).Error
return collectors, err
}
func BuiltinMetricBatchUpdateColumn(ctx *ctx.Context, col, old, new, updatedBy string) error {
if old == new {
return nil
}
return DB(ctx).Model(&BuiltinMetric{}).Where(fmt.Sprintf("%s = ?", col), old).Updates(map[string]interface{}{col: new, "updated_by": updatedBy}).Error
}

View File

@@ -9,18 +9,19 @@ import (
)
type BuiltinPayload struct {
ID int64 `json:"id" gorm:"primaryKey;type:bigint;autoIncrement;comment:'unique identifier'"`
Type string `json:"type" gorm:"type:varchar(191);not null;index:idx_type,sort:asc;comment:'type of payload'"` // Alert Dashboard Collet
Component string `json:"component" gorm:"type:varchar(191);not null;index:idx_component,sort:asc;comment:'component of payload'"` // Host MySQL Redis
Cate string `json:"cate" gorm:"type:varchar(191);not null;comment:'category of payload'"` // categraf_v1 telegraf_v1
Name string `json:"name" gorm:"type:varchar(191);not null;index:idx_buildinpayload_name,sort:asc;comment:'name of payload'"` //
Tags string `json:"tags" gorm:"type:varchar(191);not null;default:'';comment:'tags of payload'"` // {"host":"
Content string `json:"content" gorm:"type:longtext;not null;comment:'content of payload'"`
UUID int64 `json:"uuid" gorm:"type:bigint;not null;index:idx_uuid;comment:'uuid of payload'"`
CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:'create time'"`
CreatedBy string `json:"created_by" gorm:"type:varchar(191);not null;default:'';comment:'creator'"`
UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;not null;default:0;comment:'update time'"`
UpdatedBy string `json:"updated_by" gorm:"type:varchar(191);not null;default:'';comment:'updater'"`
ID int64 `json:"id" gorm:"primaryKey;type:bigint;autoIncrement;comment:'unique identifier'"`
Type string `json:"type" gorm:"type:varchar(191);not null;index:idx_type,sort:asc;comment:'type of payload'"` // Alert Dashboard Collet
Component string `json:"component" gorm:"type:varchar(191);not null;index:idx_component,sort:asc;comment:'component of payload'"` //
ComponentID uint64 `json:"component_id" gorm:"type:bigint;index:idx_component,sort:asc;comment:'component_id of payload'"` // ComponentID which the payload belongs to
Cate string `json:"cate" gorm:"type:varchar(191);not null;comment:'category of payload'"` // categraf_v1 telegraf_v1
Name string `json:"name" gorm:"type:varchar(191);not null;index:idx_buildinpayload_name,sort:asc;comment:'name of payload'"` //
Tags string `json:"tags" gorm:"type:varchar(191);not null;default:'';comment:'tags of payload'"` // {"host":"
Content string `json:"content" gorm:"type:longtext;not null;comment:'content of payload'"`
UUID int64 `json:"uuid" gorm:"type:bigint;not null;index:idx_uuid;comment:'uuid of payload'"`
CreatedAt int64 `json:"created_at" gorm:"type:bigint;not null;default:0;comment:'create time'"`
CreatedBy string `json:"created_by" gorm:"type:varchar(191);not null;default:'';comment:'creator'"`
UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;not null;default:0;comment:'update time'"`
UpdatedBy string `json:"updated_by" gorm:"type:varchar(191);not null;default:'';comment:'updater'"`
}
func (bp *BuiltinPayload) TableName() string {
@@ -33,9 +34,8 @@ func (bp *BuiltinPayload) Verify() error {
return errors.New("type is blank")
}
bp.Component = strings.TrimSpace(bp.Component)
if bp.Component == "" {
return errors.New("component is blank")
if bp.ComponentID == 0 {
return errors.New("component_id is blank")
}
if bp.Name == "" {
@@ -47,7 +47,7 @@ func (bp *BuiltinPayload) Verify() error {
func BuiltinPayloadExists(ctx *ctx.Context, bp *BuiltinPayload) (bool, error) {
var count int64
err := DB(ctx).Model(bp).Where("type = ? AND component = ? AND name = ? AND cate = ?", bp.Type, bp.Component, bp.Name, bp.Cate).Count(&count).Error
err := DB(ctx).Model(bp).Where("type = ? AND component_id = ? AND name = ? AND cate = ?", bp.Type, bp.ComponentID, bp.Name, bp.Cate).Count(&count).Error
if err != nil {
return false, err
}
@@ -78,7 +78,7 @@ func (bp *BuiltinPayload) Update(ctx *ctx.Context, req BuiltinPayload) error {
return err
}
if bp.Type != req.Type || bp.Component != req.Component || bp.Name != req.Name {
if bp.Type != req.Type || bp.ComponentID != req.ComponentID || bp.Name != req.Name {
exists, err := BuiltinPayloadExists(ctx, &req)
if err != nil {
return err
@@ -117,13 +117,13 @@ func BuiltinPayloadGet(ctx *ctx.Context, where string, args ...interface{}) (*Bu
return &bp, nil
}
func BuiltinPayloadGets(ctx *ctx.Context, typ, component, cate, query string) ([]*BuiltinPayload, error) {
func BuiltinPayloadGets(ctx *ctx.Context, componentId uint64, typ, cate, query string) ([]*BuiltinPayload, error) {
session := DB(ctx)
if typ != "" {
session = session.Where("type = ?", typ)
}
if component != "" {
session = session.Where("component = ?", component)
if componentId != 0 {
session = session.Where("component_id = ?", componentId)
}
if cate != "" {
@@ -144,9 +144,9 @@ func BuiltinPayloadGets(ctx *ctx.Context, typ, component, cate, query string) ([
}
// get cates of BuiltinPayload by type and component, return []string
func BuiltinPayloadCates(ctx *ctx.Context, typ, component string) ([]string, error) {
func BuiltinPayloadCates(ctx *ctx.Context, typ string, componentID uint64) ([]string, error) {
var cates []string
err := DB(ctx).Model(new(BuiltinPayload)).Where("type = ? and component = ?", typ, component).Distinct("cate").Pluck("cate", &cates).Error
err := DB(ctx).Model(new(BuiltinPayload)).Where("type = ? and component_id = ?", typ, componentID).Distinct("cate").Pluck("cate", &cates).Error
return cates, err
}
@@ -163,3 +163,37 @@ func BuiltinPayloadComponents(ctx *ctx.Context, typ, cate string) (string, error
}
return components[0], nil
}
// InitBuiltinPayloads 兼容新旧 BuiltinPayload 格式
func InitBuiltinPayloads(ctx *ctx.Context) error {
var lst []*BuiltinPayload
components, err := BuiltinComponentGets(ctx, "")
if err != nil {
return err
}
identToId := make(map[string]uint64)
for _, component := range components {
identToId[component.Ident] = component.ID
}
err = DB(ctx).Where("component_id = 0 or component_id is NULL").Find(&lst).Error
if err != nil {
return err
}
for _, bp := range lst {
componentId, ok := identToId[bp.Component]
if !ok {
continue
}
bp.ComponentID = componentId
}
if len(lst) == 0 {
return nil
}
return DB(ctx).Save(&lst).Error
}

View File

@@ -134,7 +134,7 @@ func (bg *BusiGroup) Del(ctx *ctx.Context) error {
return errors.New("Some alert subscribes still in the BusiGroup")
}
has, err = Exists(DB(ctx).Model(&Target{}).Where("group_id=?", bg.Id))
has, err = Exists(DB(ctx).Model(&TargetBusiGroup{}).Where("group_id=?", bg.Id))
if err != nil {
return err
}

View File

@@ -106,10 +106,8 @@ func InitRSAPassWord(ctx *ctx.Context) (string, error) {
func ConfigsGet(ctx *ctx.Context, ckey string) (string, error) { //select built-in type configs
if !ctx.IsCenter {
if !ctx.IsCenter {
s, err := poster.GetByUrls[string](ctx, "/v1/n9e/config?key="+ckey)
return s, err
}
s, err := poster.GetByUrls[string](ctx, "/v1/n9e/config?key="+ckey)
return s, err
}
var lst []string
@@ -125,6 +123,22 @@ func ConfigsGet(ctx *ctx.Context, ckey string) (string, error) { //select built-
return "", nil
}
func ConfigsGetAll(ctx *ctx.Context) ([]*Configs, error) { // select built-in type configs
if !ctx.IsCenter {
lst, err := poster.GetByUrls[[]*Configs](ctx, "/v1/n9e/all-configs")
return lst, err
}
var lst []*Configs
err := DB(ctx).Model(&Configs{}).Select("ckey, cval").
Where("ckey!='' and external=? ", 0).Find(&lst).Error
if err != nil {
return nil, errors.WithMessage(err, "failed to query configs")
}
return lst, nil
}
func ConfigsSet(ctx *ctx.Context, ckey, cval string) error {
return ConfigsSetWithUname(ctx, ckey, cval, "default")
}
@@ -355,3 +369,19 @@ func ConfigUserVariableGetDecryptMap(context *ctx.Context, privateKey []byte, pa
return ret, nil
}
func ConfigCvalStatistics(context *ctx.Context) (*Statistics, error) {
if !context.IsCenter {
return poster.GetByUrls[*Statistics](context, "/v1/n9e/statistic?name=cval")
}
session := DB(context).Model(&Configs{}).Select("count(*) as total",
"max(update_at) as last_updated").Where("ckey!='' and external=? ", 0) // built-in config
var stats []*Statistics
err := session.Find(&stats).Error
if err != nil {
return nil, err
}
return stats[0], nil
}

View File

@@ -35,6 +35,7 @@ type Datasource struct {
UpdatedBy string `json:"updated_by"`
IsDefault bool `json:"is_default"`
Transport *http.Transport `json:"-" gorm:"-"`
ForceSave bool `json:"force_save" gorm:"-"`
}
type Auth struct {

View File

@@ -17,6 +17,7 @@ type HostMeta struct {
EngineName string `json:"engine_name"`
GlobalLabels map[string]string `json:"global_labels"`
ExtendInfo map[string]interface{} `json:"extend_info"`
Config interface{} `json:"config"`
}
type HostUpdteTime struct {

View File

@@ -28,7 +28,7 @@ func MigrateIbexTables(db *gorm.DB) {
db = db.Set("gorm:table_options", tableOptions)
}
dts := []interface{}{&imodels.TaskMeta{}, &imodels.TaskScheduler{}, &imodels.TaskSchedulerHealth{}, &imodels.TaskHostDoing{}, &imodels.TaskAction{}}
dts := []interface{}{&imodels.TaskMeta{}, &imodels.TaskScheduler{}, &imodels.TaskSchedulerHealth{}, &TaskHostDoing{}, &imodels.TaskAction{}}
for _, dt := range dts {
err := db.AutoMigrate(dt)
if err != nil {
@@ -58,7 +58,8 @@ func MigrateTables(db *gorm.DB) error {
dts := []interface{}{&RecordingRule{}, &AlertRule{}, &AlertSubscribe{}, &AlertMute{},
&TaskRecord{}, &ChartShare{}, &Target{}, &Configs{}, &Datasource{}, &NotifyTpl{},
&Board{}, &BoardBusigroup{}, &Users{}, &SsoConfig{}, &models.BuiltinMetric{},
&models.MetricFilter{}, &models.BuiltinComponent{}, &models.NotificaitonRecord{}}
&models.MetricFilter{}, &models.BuiltinComponent{}, &models.NotificaitonRecord{},
&models.TargetBusiGroup{}}
if !columnHasIndex(db, &AlertHisEvent{}, "original_tags") ||
!columnHasIndex(db, &AlertCurEvent{}, "original_tags") {
@@ -88,7 +89,7 @@ func MigrateTables(db *gorm.DB) error {
for _, dt := range dts {
err := db.AutoMigrate(dt)
if err != nil {
logger.Errorf("failed to migrate table: %v", err)
logger.Errorf("failed to migrate table:%v %v", dt, err)
}
}
@@ -227,10 +228,11 @@ type AlertCurEvent struct {
}
type Target struct {
HostIp string `gorm:"column:host_ip;varchar(15);default:'';comment:IPv4 string;index:idx_host_ip"`
AgentVersion string `gorm:"column:agent_version;varchar(255);default:'';comment:agent version;index:idx_agent_version"`
EngineName string `gorm:"column:engine_name;varchar(255);default:'';comment:engine name;index:idx_engine_name"`
OS string `gorm:"column:os;varchar(31);default:'';comment:os type;index:idx_os"`
HostIp string `gorm:"column:host_ip;type:varchar(15);default:'';comment:IPv4 string;index:idx_host_ip"`
AgentVersion string `gorm:"column:agent_version;type:varchar(255);default:'';comment:agent version;index:idx_agent_version"`
EngineName string `gorm:"column:engine_name;type:varchar(255);default:'';comment:engine name;index:idx_engine_name"`
OS string `gorm:"column:os;type:varchar(31);default:'';comment:os type;index:idx_os"`
HostTags []string `gorm:"column:host_tags;type:text;comment:global labels set in conf file;serializer:json"`
}
type Datasource struct {
@@ -275,5 +277,18 @@ type SsoConfig struct {
}
type BuiltinPayloads struct {
UUID int64 `json:"uuid" gorm:"type:bigint;not null;index:idx_uuid;comment:'uuid of payload'"`
UUID int64 `json:"uuid" gorm:"type:bigint;not null;index:idx_uuid;comment:'uuid of payload'"`
ComponentID int64 `json:"component_id" gorm:"type:bigint;index:idx_component,sort:asc;not null;default:0;comment:'component_id of payload'"`
}
type TaskHostDoing struct {
Id int64 `gorm:"column:id;index;primaryKey:false"`
Host string `gorm:"column:host;size:128;not null;index"`
Clock int64 `gorm:"column:clock;not null;default:0"`
Action string `gorm:"column:action;size:16;not null"`
AlertTriggered bool `gorm:"-"`
}
func (TaskHostDoing) TableName() string {
return "task_host_doing"
}

View File

@@ -25,13 +25,13 @@ type PromRuleGroup struct {
func convertInterval(interval string) int {
duration, err := time.ParseDuration(interval)
if err != nil {
logger.Errorf("Error parsing interval `%s`,err: %v", interval, err)
logger.Errorf("Error parsing interval `%s`, err: %v", interval, err)
return 0
}
return int(duration.Seconds())
}
func ConvertAlert(rule PromRule, interval string) AlertRule {
func ConvertAlert(rule PromRule, interval string, datasouceIds []int64, disabled int) AlertRule {
annotations := rule.Annotations
appendTags := []string{}
severity := 2
@@ -54,14 +54,15 @@ func ConvertAlert(rule PromRule, interval string) AlertRule {
}
return AlertRule{
Name: rule.Alert,
Severity: severity,
Disabled: AlertRuleEnabled,
PromForDuration: convertInterval(rule.For),
PromQl: rule.Expr,
PromEvalInterval: convertInterval(interval),
EnableStimeJSON: "00:00",
EnableEtimeJSON: "23:59",
Name: rule.Alert,
Severity: severity,
DatasourceIdsJson: datasouceIds,
Disabled: disabled,
PromForDuration: convertInterval(rule.For),
PromQl: rule.Expr,
PromEvalInterval: convertInterval(interval),
EnableStimeJSON: "00:00",
EnableEtimeJSON: "23:59",
EnableDaysOfWeekJSON: []string{
"1", "2", "3", "4", "5", "6", "0",
},
@@ -74,7 +75,7 @@ func ConvertAlert(rule PromRule, interval string) AlertRule {
}
}
func DealPromGroup(promRule []PromRuleGroup) []AlertRule {
func DealPromGroup(promRule []PromRuleGroup, dataSourceIds []int64, disabled int) []AlertRule {
var alertRules []AlertRule
for _, group := range promRule {
@@ -84,7 +85,8 @@ func DealPromGroup(promRule []PromRuleGroup) []AlertRule {
}
for _, rule := range group.Rules {
if rule.Alert != "" {
alertRules = append(alertRules, ConvertAlert(rule, interval))
alertRules = append(alertRules,
ConvertAlert(rule, interval, dataSourceIds, disabled))
}
}
}

View File

@@ -21,7 +21,7 @@ func TestConvertAlert(t *testing.T) {
t.Errorf("Failed to Unmarshal, err: %s", err)
}
t.Logf("jobMissing: %+v", jobMissing[0])
convJobMissing := models.ConvertAlert(jobMissing[0], "30s")
convJobMissing := models.ConvertAlert(jobMissing[0], "30s", []int64{1}, 0)
if convJobMissing.PromEvalInterval != 30 {
t.Errorf("PromEvalInterval is expected to be 30, but got %d",
convJobMissing.PromEvalInterval)
@@ -45,7 +45,7 @@ func TestConvertAlert(t *testing.T) {
description: "Prometheus rule evaluation took more time than the scheduled interval. It indicates a slower storage backend access or too complex query.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
`), &ruleEvaluationSlow)
t.Logf("ruleEvaluationSlow: %+v", ruleEvaluationSlow[0])
convRuleEvaluationSlow := models.ConvertAlert(ruleEvaluationSlow[0], "1m")
convRuleEvaluationSlow := models.ConvertAlert(ruleEvaluationSlow[0], "1m", []int64{1}, 0)
if convRuleEvaluationSlow.PromEvalInterval != 60 {
t.Errorf("PromEvalInterval is expected to be 60, but got %d",
convJobMissing.PromEvalInterval)
@@ -69,7 +69,7 @@ func TestConvertAlert(t *testing.T) {
description: "A Prometheus target has disappeared. An exporter might be crashed.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
`), &targetMissing)
t.Logf("targetMissing: %+v", targetMissing[0])
convTargetMissing := models.ConvertAlert(targetMissing[0], "1h")
convTargetMissing := models.ConvertAlert(targetMissing[0], "1h", []int64{1}, 0)
if convTargetMissing.PromEvalInterval != 3600 {
t.Errorf("PromEvalInterval is expected to be 3600, but got %d",
convTargetMissing.PromEvalInterval)

View File

@@ -1,25 +1,30 @@
package models
import (
"log"
"sort"
"strings"
"time"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/poster"
"golang.org/x/exp/slices"
"github.com/pkg/errors"
"github.com/toolkits/pkg/container/set"
"gorm.io/gorm"
)
type TargetDeleteHookFunc func(ctx *ctx.Context, idents []string) error
type Target struct {
Id int64 `json:"id" gorm:"primaryKey"`
GroupId int64 `json:"group_id"`
GroupObj *BusiGroup `json:"group_obj" gorm:"-"`
GroupObjs []*BusiGroup `json:"group_objs" gorm:"-"`
Ident string `json:"ident"`
Note string `json:"note"`
Tags string `json:"-"`
Tags string `json:"-"` // user tags
TagsJSON []string `json:"tags" gorm:"-"`
TagsMap map[string]string `json:"tags_maps" gorm:"-"` // internal use, append tags to series
UpdateAt int64 `json:"update_at"`
@@ -27,6 +32,7 @@ type Target struct {
AgentVersion string `json:"agent_version"`
EngineName string `json:"engine_name"`
OS string `json:"os" gorm:"column:os"`
HostTags []string `json:"host_tags" gorm:"serializer:json"`
UnixTime int64 `json:"unixtime" gorm:"-"`
Offset int64 `json:"offset" gorm:"-"`
@@ -36,6 +42,7 @@ type Target struct {
CpuUtil float64 `json:"cpu_util" gorm:"-"`
Arch string `json:"arch" gorm:"-"`
RemoteAddr string `json:"remote_addr" gorm:"-"`
GroupIds []int64 `json:"group_ids" gorm:"-"`
}
func (t *Target) TableName() string {
@@ -43,26 +50,60 @@ func (t *Target) TableName() string {
}
func (t *Target) FillGroup(ctx *ctx.Context, cache map[int64]*BusiGroup) error {
if t.GroupId <= 0 {
return nil
var err error
if len(t.GroupIds) == 0 {
t.GroupIds, err = TargetGroupIdsGetByIdent(ctx, t.Ident)
if err != nil {
return errors.WithMessage(err, "failed to get target gids")
}
t.GroupObjs = make([]*BusiGroup, 0, len(t.GroupIds))
}
bg, has := cache[t.GroupId]
if has {
t.GroupObj = bg
return nil
for _, gid := range t.GroupIds {
bg, has := cache[gid]
if has && bg != nil {
t.GroupObjs = append(t.GroupObjs, bg)
continue
}
bg, err := BusiGroupGetById(ctx, gid)
if err != nil {
return errors.WithMessage(err, "failed to get busi group")
}
if bg == nil {
continue
}
t.GroupObjs = append(t.GroupObjs, bg)
cache[gid] = bg
}
bg, err := BusiGroupGetById(ctx, t.GroupId)
if err != nil {
return errors.WithMessage(err, "failed to get busi group")
}
t.GroupObj = bg
cache[t.GroupId] = bg
return nil
}
func (t *Target) MatchGroupId(gid ...int64) bool {
for _, tgId := range t.GroupIds {
for _, id := range gid {
if tgId == id {
return true
}
}
}
return false
}
func (t *Target) AfterFind(tx *gorm.DB) (err error) {
delta := time.Now().Unix() - t.UpdateAt
if delta < 60 {
t.TargetUp = 2
} else if delta < 180 {
t.TargetUp = 1
}
t.FillTagsMap()
return
}
func TargetStatistics(ctx *ctx.Context) (*Statistics, error) {
if !ctx.IsCenter {
s, err := poster.GetByUrls[*Statistics](ctx, "/v1/n9e/statistic?name=target")
@@ -78,19 +119,43 @@ func TargetStatistics(ctx *ctx.Context) (*Statistics, error) {
return stats[0], nil
}
func TargetDel(ctx *ctx.Context, idents []string) error {
func TargetDel(ctx *ctx.Context, idents []string, deleteHook TargetDeleteHookFunc) error {
if len(idents) == 0 {
panic("idents empty")
}
return DB(ctx).Where("ident in ?", idents).Delete(new(Target)).Error
return DB(ctx).Transaction(func(tx *gorm.DB) error {
txErr := tx.Where("ident in ?", idents).Delete(new(Target)).Error
if txErr != nil {
return txErr
}
txErr = deleteHook(ctx, idents)
if txErr != nil {
return txErr
}
txErr = TargetDeleteBgids(ctx, idents)
if txErr != nil {
return txErr
}
return nil
})
}
type BuildTargetWhereOption func(session *gorm.DB) *gorm.DB
func BuildTargetWhereWithBgids(bgids []int64) BuildTargetWhereOption {
return func(session *gorm.DB) *gorm.DB {
if len(bgids) > 0 {
session = session.Where("group_id in (?)", bgids)
if len(bgids) == 1 && bgids[0] == 0 {
session = session.Joins("left join target_busi_group on target.ident = " +
"target_busi_group.target_ident").Where("target_busi_group.target_ident is null")
} else if len(bgids) > 0 {
if slices.Contains(bgids, 0) {
session = session.Joins("left join target_busi_group on target.ident = target_busi_group.target_ident").
Where("target_busi_group.target_ident is null OR target_busi_group.group_id in (?)", bgids)
} else {
session = session.Joins("join target_busi_group on target.ident = "+
"target_busi_group.target_ident").Where("target_busi_group.group_id in (?)", bgids)
}
}
return session
}
@@ -105,14 +170,22 @@ func BuildTargetWhereWithDsIds(dsIds []int64) BuildTargetWhereOption {
}
}
func BuildTargetWhereWithHosts(hosts []string) BuildTargetWhereOption {
return func(session *gorm.DB) *gorm.DB {
if len(hosts) > 0 {
session = session.Where("ident in (?) or host_ip in (?)", hosts, hosts)
}
return session
}
}
func BuildTargetWhereWithQuery(query string) BuildTargetWhereOption {
return func(session *gorm.DB) *gorm.DB {
if query != "" {
arr := strings.Fields(query)
for i := 0; i < len(arr); i++ {
q := "%" + arr[i] + "%"
session = session.Where("ident like ? or note like ? or tags like ? "+
"or os like ?", q, q, q, q)
session = session.Where("ident like ? or host_ip like ? or note like ? or tags like ? or host_tags like ? or os like ?", q, q, q, q, q, q)
}
}
return session
@@ -122,18 +195,18 @@ func BuildTargetWhereWithQuery(query string) BuildTargetWhereOption {
func BuildTargetWhereWithDowntime(downtime int64) BuildTargetWhereOption {
return func(session *gorm.DB) *gorm.DB {
if downtime > 0 {
session = session.Where("update_at < ?", time.Now().Unix()-downtime)
session = session.Where("target.update_at < ?", time.Now().Unix()-downtime)
}
return session
}
}
func buildTargetWhere(ctx *ctx.Context, options ...BuildTargetWhereOption) *gorm.DB {
session := DB(ctx).Model(&Target{})
sub := DB(ctx).Model(&Target{}).Distinct("target.ident")
for _, opt := range options {
session = opt(session)
sub = opt(sub)
}
return session
return DB(ctx).Model(&Target{}).Where("ident in (?)", sub)
}
func TargetTotal(ctx *ctx.Context, options ...BuildTargetWhereOption) (int64, error) {
@@ -191,15 +264,18 @@ func MissTargetCountByFilter(ctx *ctx.Context, query []map[string]interface{}, t
}
func TargetFilterQueryBuild(ctx *ctx.Context, query []map[string]interface{}, limit, offset int) *gorm.DB {
session := DB(ctx).Model(&Target{})
sub := DB(ctx).Model(&Target{}).Distinct("target.ident").Joins("left join " +
"target_busi_group on target.ident = target_busi_group.target_ident")
for _, q := range query {
tx := DB(ctx).Model(&Target{})
for k, v := range q {
tx = tx.Or(k, v)
}
session = session.Where(tx)
sub = sub.Where(tx)
}
session := DB(ctx).Model(&Target{}).Where("ident in (?)", sub)
if limit > 0 {
session = session.Limit(limit).Offset(offset)
}
@@ -215,9 +291,20 @@ func TargetGetsAll(ctx *ctx.Context) ([]*Target, error) {
var lst []*Target
err := DB(ctx).Model(&Target{}).Find(&lst).Error
if err != nil {
return lst, err
}
tgs, err := TargetBusiGroupsGetAll(ctx)
if err != nil {
return lst, err
}
for i := 0; i < len(lst); i++ {
lst[i].FillTagsMap()
lst[i].GroupIds = tgs[lst[i].Ident]
}
return lst, err
}
@@ -321,15 +408,15 @@ func TargetsGetIdentsByIdentsAndHostIps(ctx *ctx.Context, idents, hostIps []stri
return inexistence, identSet.ToSlice(), nil
}
func TargetGetTags(ctx *ctx.Context, idents []string) ([]string, error) {
func TargetGetTags(ctx *ctx.Context, idents []string, ignoreHostTag bool) ([]string, error) {
session := DB(ctx).Model(new(Target))
var arr []string
var arr []*Target
if len(idents) > 0 {
session = session.Where("ident in ?", idents)
}
err := session.Select("distinct(tags) as tags").Pluck("tags", &arr).Error
err := session.Select("tags", "host_tags").Find(&arr).Error
if err != nil {
return nil, err
}
@@ -341,10 +428,16 @@ func TargetGetTags(ctx *ctx.Context, idents []string) ([]string, error) {
set := make(map[string]struct{})
for i := 0; i < cnt; i++ {
tags := strings.Fields(arr[i])
tags := strings.Fields(arr[i].Tags)
for j := 0; j < len(tags); j++ {
set[tags[j]] = struct{}{}
}
if !ignoreHostTag {
for _, ht := range arr[i].HostTags {
set[ht] = struct{}{}
}
}
}
cnt = len(set)
@@ -389,7 +482,8 @@ func (t *Target) FillTagsMap() {
t.TagsJSON = strings.Fields(t.Tags)
t.TagsMap = make(map[string]string)
m := make(map[string]string)
for _, item := range t.TagsJSON {
allTags := append(t.TagsJSON, t.HostTags...)
for _, item := range allTags {
arr := strings.Split(item, "=")
if len(arr) != 2 {
continue
@@ -404,6 +498,16 @@ func (t *Target) GetTagsMap() map[string]string {
tagsJSON := strings.Fields(t.Tags)
m := make(map[string]string)
for _, item := range tagsJSON {
if arr := strings.Split(item, "="); len(arr) == 2 {
m[arr[0]] = arr[1]
}
}
return m
}
func (t *Target) GetHostTagsMap() map[string]string {
m := make(map[string]string)
for _, item := range t.HostTags {
arr := strings.Split(item, "=")
if len(arr) != 2 {
continue
@@ -458,3 +562,73 @@ func IdentsFilter(ctx *ctx.Context, idents []string, where string, args ...inter
func (m *Target) UpdateFieldsMap(ctx *ctx.Context, fields map[string]interface{}) error {
return DB(ctx).Model(m).Updates(fields).Error
}
func MigrateBg(ctx *ctx.Context, bgLabelKey string) {
// 1. 判断是否已经完成迁移
var maxGroupId int64
if err := DB(ctx).Model(&Target{}).Select("MAX(group_id)").Scan(&maxGroupId).Error; err != nil {
log.Println("failed to get max group_id from target table, err:", err)
return
}
if maxGroupId == 0 {
log.Println("migration bgid has been completed.")
return
}
err := DoMigrateBg(ctx, bgLabelKey)
if err != nil {
log.Println("failed to migrate bgid, err:", err)
return
}
log.Println("migration bgid has been completed")
}
func DoMigrateBg(ctx *ctx.Context, bgLabelKey string) error {
// 2. 获取全量 target
targets, err := TargetGetsAll(ctx)
if err != nil {
return err
}
// 3. 获取全量 busi_group
bgs, err := BusiGroupGetAll(ctx)
if err != nil {
return err
}
bgById := make(map[int64]*BusiGroup, len(bgs))
for _, bg := range bgs {
bgById[bg.Id] = bg
}
// 4. 如果某 busi_group 有 label将其存至对应的 target tags 中
for _, t := range targets {
if t.GroupId == 0 {
continue
}
err := DB(ctx).Transaction(func(tx *gorm.DB) error {
// 4.1 将 group_id 迁移至关联表
if err := TargetBindBgids(ctx, []string{t.Ident}, []int64{t.GroupId}); err != nil {
return err
}
if err := TargetUpdateBgid(ctx, []string{t.Ident}, 0, false); err != nil {
return err
}
// 4.2 判断该机器是否需要新增 tag
if bg, ok := bgById[t.GroupId]; !ok || bg.LabelEnable == 0 ||
strings.Contains(t.Tags, bgLabelKey+"=") {
return nil
} else {
return t.AddTags(ctx, []string{bgLabelKey + "=" + bg.LabelValue})
}
})
if err != nil {
log.Printf("failed to migrate %v bg, err: %v\n", t.Ident, err)
continue
}
}
return nil
}

158
models/target_busi_group.go Normal file
View File

@@ -0,0 +1,158 @@
package models
import (
"time"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type TargetBusiGroup struct {
Id int64 `json:"id" gorm:"primaryKey;type:bigint;autoIncrement"`
TargetIdent string `json:"target_ident" gorm:"type:varchar(191);not null;index:idx_target_group,unique,priority:1"`
GroupId int64 `json:"group_id" gorm:"type:bigint;not null;index:idx_target_group,unique,priority:2"`
UpdateAt int64 `json:"update_at" gorm:"type:bigint;not null"`
}
func (t *TargetBusiGroup) TableName() string {
return "target_busi_group"
}
func TargetBusiGroupsGetAll(ctx *ctx.Context) (map[string][]int64, error) {
var lst []*TargetBusiGroup
err := DB(ctx).Find(&lst).Error
if err != nil {
return nil, err
}
tgs := make(map[string][]int64)
for _, tg := range lst {
tgs[tg.TargetIdent] = append(tgs[tg.TargetIdent], tg.GroupId)
}
return tgs, nil
}
func TargetGroupIdsGetByIdent(ctx *ctx.Context, ident string) ([]int64, error) {
var lst []*TargetBusiGroup
err := DB(ctx).Where("target_ident = ?", ident).Find(&lst).Error
if err != nil {
return nil, err
}
groupIds := make([]int64, 0, len(lst))
for _, tg := range lst {
groupIds = append(groupIds, tg.GroupId)
}
return groupIds, nil
}
func TargetGroupIdsGetByIdents(ctx *ctx.Context, idents []string) ([]int64, error) {
var groupIds []int64
err := DB(ctx).Model(&TargetBusiGroup{}).
Where("target_ident IN ?", idents).
Distinct().
Pluck("group_id", &groupIds).
Error
if err != nil {
return nil, err
}
return groupIds, nil
}
func TargetBindBgids(ctx *ctx.Context, idents []string, bgids []int64) error {
lst := make([]TargetBusiGroup, 0, len(bgids)*len(idents))
updateAt := time.Now().Unix()
for _, bgid := range bgids {
for _, ident := range idents {
cur := TargetBusiGroup{
TargetIdent: ident,
GroupId: bgid,
UpdateAt: updateAt,
}
lst = append(lst, cur)
}
}
var cl clause.Expression = clause.Insert{Modifier: "ignore"}
switch DB(ctx).Dialector.Name() {
case "sqlite":
cl = clause.Insert{Modifier: "or ignore"}
case "postgres":
cl = clause.OnConflict{DoNothing: true}
}
return DB(ctx).Clauses(cl).CreateInBatches(&lst, 10).Error
}
func TargetUnbindBgids(ctx *ctx.Context, idents []string, bgids []int64) error {
return DB(ctx).Where("target_ident in ? and group_id in ?",
idents, bgids).Delete(&TargetBusiGroup{}).Error
}
func TargetDeleteBgids(ctx *ctx.Context, idents []string) error {
return DB(ctx).Where("target_ident in ?", idents).Delete(&TargetBusiGroup{}).Error
}
func TargetOverrideBgids(ctx *ctx.Context, idents []string, bgids []int64) error {
return DB(ctx).Transaction(func(tx *gorm.DB) error {
// 先删除旧的关联
if err := tx.Where("target_ident IN ?", idents).Delete(&TargetBusiGroup{}).Error; err != nil {
return err
}
// 准备新的关联数据
lst := make([]TargetBusiGroup, 0, len(bgids)*len(idents))
updateAt := time.Now().Unix()
for _, ident := range idents {
for _, bgid := range bgids {
cur := TargetBusiGroup{
TargetIdent: ident,
GroupId: bgid,
UpdateAt: updateAt,
}
lst = append(lst, cur)
}
}
if len(lst) == 0 {
return nil
}
// 添加新的关联
var cl clause.Expression = clause.Insert{Modifier: "ignore"}
switch tx.Dialector.Name() {
case "sqlite":
cl = clause.Insert{Modifier: "or ignore"}
case "postgres":
cl = clause.OnConflict{DoNothing: true}
}
return tx.Clauses(cl).CreateInBatches(&lst, 10).Error
})
}
func SeparateTargetIdents(ctx *ctx.Context, idents []string) (existing, nonExisting []string, err error) {
existingMap := make(map[string]bool)
// 查询已存在的 idents 并直接填充 map
err = DB(ctx).Model(&TargetBusiGroup{}).
Where("target_ident IN ?", idents).
Distinct().
Pluck("target_ident", &existing).
Error
if err != nil {
return nil, nil, err
}
for _, ident := range existing {
existingMap[ident] = true
}
// 分离不存在的 idents
for _, ident := range idents {
if !existingMap[ident] {
nonExisting = append(nonExisting, ident)
}
}
return
}

View File

@@ -296,6 +296,34 @@ func UserGet(ctx *ctx.Context, where string, args ...interface{}) (*User, error)
return lst[0], nil
}
func UsersGet(ctx *ctx.Context, where string, args ...interface{}) ([]*User, error) {
var lst []*User
err := DB(ctx).Where(where, args...).Find(&lst).Error
if err != nil {
return nil, err
}
for _, user := range lst {
user.RolesLst = strings.Fields(user.Roles)
user.Admin = user.IsAdmin()
}
return lst, nil
}
func UserMapGet(ctx *ctx.Context, where string, args ...interface{}) map[string]*User {
lst, err := UsersGet(ctx, where, args...)
if err != nil {
logger.Errorf("UsersGet err: %v", err)
return nil
}
um := make(map[string]*User, len(lst))
for _, user := range lst {
um[user.Username] = user
}
return um
}
func UserGetByUsername(ctx *ctx.Context, username string) (*User, error) {
return UserGet(ctx, "username=?", username)
}
@@ -704,7 +732,10 @@ func (u *User) NopriIdents(ctx *ctx.Context, idents []string) ([]string, error)
}
var allowedIdents []string
err = DB(ctx).Model(&Target{}).Where("group_id in ?", bgids).Pluck("ident", &allowedIdents).Error
sub := DB(ctx).Model(&Target{}).Distinct("target.ident").
Joins("join target_busi_group on target.ident = target_busi_group.target_ident").
Where("target_busi_group.group_id in (?)", bgids)
err = DB(ctx).Model(&Target{}).Where("ident in (?)", sub).Pluck("ident", &allowedIdents).Error
if err != nil {
return []string{}, err
}
@@ -736,7 +767,11 @@ func (u *User) BusiGroups(ctx *ctx.Context, limit int, query string, all ...bool
return lst, nil
}
err = DB(ctx).Order("name").Limit(limit).Where("id=?", t.GroupId).Find(&lst).Error
t.GroupIds, err = TargetGroupIdsGetByIdent(ctx, t.Ident)
if err != nil {
return nil, err
}
err = DB(ctx).Order("name").Limit(limit).Where("id in ?", t.GroupIds).Find(&lst).Error
}
return lst, err
@@ -768,8 +803,12 @@ func (u *User) BusiGroups(ctx *ctx.Context, limit int, query string, all ...bool
return lst, err
}
if t != nil && slice.ContainsInt64(busiGroupIds, t.GroupId) {
err = DB(ctx).Order("name").Limit(limit).Where("id=?", t.GroupId).Find(&lst).Error
t.GroupIds, err = TargetGroupIdsGetByIdent(ctx, t.Ident)
if err != nil {
return nil, err
}
if t != nil && t.MatchGroupId(busiGroupIds...) {
err = DB(ctx).Order("name").Limit(limit).Where("id in ?", t.GroupIds).Find(&lst).Error
}
}

View File

@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -41,14 +42,21 @@ type LoggerConfig struct {
// Output is a writer where logs are written.
// Optional. Default value is gin.DefaultWriter.
Output io.Writer
PrintBody bool
Output io.Writer
PrintAccessLog func() bool
PrintBodyPaths func() map[string]struct{}
// SkipPaths is a url path array which logs are not written.
// Optional.
SkipPaths []string
}
func (c *LoggerConfig) ContainsPath(path string) bool {
path = strings.Split(path, "?")[0]
_, exist := c.PrintBodyPaths()[path]
return exist
}
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
type LogFormatter func(params LogFormatterParams) string
@@ -255,6 +263,11 @@ func LoggerWithConfig(conf LoggerConfig) gin.HandlerFunc {
}
return func(c *gin.Context) {
if !conf.PrintAccessLog() {
c.Next()
return
}
// Start timer
start := time.Now()
path := c.Request.URL.Path
@@ -271,7 +284,7 @@ func LoggerWithConfig(conf LoggerConfig) gin.HandlerFunc {
}
c.Writer = bodyWriter
if conf.PrintBody {
if conf.ContainsPath(c.Request.RequestURI) {
buf, _ := io.ReadAll(c.Request.Body)
rdr1 = io.NopCloser(bytes.NewBuffer(buf))
rdr2 = io.NopCloser(bytes.NewBuffer(buf))
@@ -309,7 +322,7 @@ func LoggerWithConfig(conf LoggerConfig) gin.HandlerFunc {
// fmt.Fprint(out, formatter(param))
logger.Info(formatter(param))
if conf.PrintBody {
if conf.ContainsPath(c.Request.RequestURI) {
respBody := readBody(bytes.NewReader(bodyWriter.body.Bytes()), c.Writer.Header().Get("Content-Encoding"))
reqBody := readBody(rdr1, c.Request.Header.Get("Content-Encoding"))
logger.Debugf("path:%s req body:%s resp:%s", path, reqBody, respBody)

View File

@@ -2,6 +2,7 @@ package hash
import (
"sort"
"strings"
prommodel "github.com/prometheus/common/model"
"github.com/spaolacci/murmur3"
@@ -53,3 +54,14 @@ func GetTagHash(m prommodel.Metric) uint64 {
return murmur3.Sum64([]byte(str))
}
func GetTargetTagHash(m prommodel.Metric, target []string) uint64 {
builder := strings.Builder{}
for _, k := range target {
builder.WriteString("/")
builder.WriteString(k)
builder.WriteString("/")
builder.WriteString(string(m[prommodel.LabelName(k)]))
}
return murmur3.Sum64([]byte(builder.String()))
}

View File

@@ -70,10 +70,12 @@ type JWTAuth struct {
RedisKeyPrefix string
}
func GinEngine(mode string, cfg Config) *gin.Engine {
func GinEngine(mode string, cfg Config, printBodyPaths func() map[string]struct{},
printAccessLog func() bool) *gin.Engine {
gin.SetMode(mode)
loggerMid := aop.Logger(aop.LoggerConfig{PrintBody: cfg.PrintBody})
loggerMid := aop.Logger(aop.LoggerConfig{PrintAccessLog: printAccessLog,
PrintBodyPaths: printBodyPaths})
recoveryMid := aop.Recovery()
if strings.ToLower(mode) == "release" {
@@ -84,10 +86,7 @@ func GinEngine(mode string, cfg Config) *gin.Engine {
r.Use(recoveryMid)
// whether print access log
if cfg.PrintAccessLog {
r.Use(loggerMid)
}
r.Use(loggerMid)
if cfg.PProf {
pprof.Register(r, "/api/debug/pprof")

View File

@@ -104,6 +104,56 @@ var I18N = `
"builtin metric already exists":"内置指标已存在",
"AlertRule already exists":"告警规则已存在",
"This functionality has not been enabled. Please contact the system administrator to activate it.":"此功能尚未启用。请联系系统管理员启用"
},
"ja_JP": {
"Username or password invalid": "ユーザー名またはパスワードが無効です",
"incorrect verification code": "認証コードが正しくありません",
"roles empty": "役割を空にすることはできません",
"Username already exists": "このユーザー名は既に存在します。別のユーザー名を使用してください",
"failed to count user-groups": "データの検証に失敗しました。もう一度お試しください",
"UserGroup already exists": "グループ名は既に存在します。別の名前を使用してください",
"members empty": "メンバーを空にすることはできません",
"At least one team have rw permission": "少なくとも1つのチームに読み書き権限が必要です",
"Failed to create BusiGroup(%s)": "[%s]の作成に失敗しました。もう一度お試しください",
"business group id invalid": "ビジネスグループIDが正しくありません",
"idents empty": "監視対象を空にすることはできません",
"invalid tag(%s)": "タグ[%s]が無効です",
"invalid tagkey(%s): cannot contains . ": "タグキー[%s]にドット(.)を含めることはできません",
"invalid tagkey(%s): cannot contains _ ": "タグキー[%s]にアンダースコア(_)を含めることはできません",
"invalid tagkey(%s)": "タグキー[%s]が無効です",
"duplicate tagkey(%s)": "タグキー(%s)が重複しています",
"name is empty": "名前を空にすることはできません",
"Ident duplicate": "ダッシュボードの一意の識別子が既に存在します",
"No such dashboard": "ダッシュボードが存在しません",
"Name has invalid characters": "名前に無効な文字が含まれています",
"Name is blank": "名前を空白にすることはできません",
"forbidden": "権限がありません",
"builtin alerts is empty, file: %s": "ビルトインアラートテンプレートが空です %s",
"input json is empty": "提出内容を空にすることはできません",
"fields empty": "選択フィールドを空にすることはできません",
"No such AlertRule": "そのようなアラートルールはありません",
"GroupId(%d) invalid": "ビジネスグループIDが無効です",
"No such recording rule": "そのような記録ルールはありません",
"tags is blank": "タグを空白にすることはできません",
"oops... etime(%d) <= btime(%d)": "開始時間は終了時間より大きくすることはできません",
"group_id invalid": "ビジネスグループが無効です",
"No such AlertMute": "そのようなアラートミュートルールはありません",
"rule_id and tags are both blank": "アラートルールとタグを同時に空にすることはできません",
"rule is blank": "ルールを空にすることはできません",
"rule invalid": "ルールが無効です。正しいかどうか確認してください",
"unsupported field: %s": "フィールド %s はサポートされていません",
"arg(batch) should be nonnegative": "batchは負の数にできません",
"arg(tolerance) should be nonnegative": "toleranceは負の数にできません",
"arg(timeout) should be nonnegative": "timeoutは負の数にできません",
"arg(timeout) longer than five days": "timeoutは5日を超えることはできません",
"arg(title) is required": "titleは必須項目です",
"created task.id is zero": "作成されたタスクIDがゼロです",
"invalid ibex address: %s": "ibex %s のアドレスが無効です",
"url path invalid": "URLパスが無効です",
"no such server": "そのようなインスタンスはありません",
"admin role can not be modified": "管理者ロールは変更できません",
"builtin payload already exists": "ビルトインテンプレートは既に存在します",
"This functionality has not been enabled. Please contact the system administrator to activate it.": "この機能はまだ有効になっていません。システム管理者に連絡して有効にしてください"
}
}
`

View File

@@ -43,6 +43,7 @@ type SsoClient struct {
Host string
Port int
BaseDn string
BaseDns []string
BindUser string
BindPass string
SyncAdd bool
@@ -131,6 +132,8 @@ func (s *SsoClient) Reload(cf Config) {
if s.SyncInterval > 0 {
s.Ticker.Reset(s.SyncInterval * time.Second)
}
s.BaseDns = strings.Split(s.BaseDn, "|")
}
func (s *SsoClient) Copy() *SsoClient {
@@ -158,27 +161,40 @@ func (s *SsoClient) LoginCheck(user, pass string) (*ldap.SearchResult, error) {
}
defer conn.Close()
sr, err := lc.ldapReq(conn, lc.AuthFilter, user)
srs, err := lc.ldapReq(conn, lc.AuthFilter, user)
if err != nil {
return nil, fmt.Errorf("ldap.error: ldap search fail: %v", err)
}
if len(sr.Entries) == 0 {
var sr *ldap.SearchResult
for i := range srs {
if srs[i] == nil || len(srs[i].Entries) == 0 {
continue
}
// 多个 dn 中,账号的唯一性由 LDAP 保证
if len(srs[i].Entries) > 1 {
return nil, fmt.Errorf("ldap.error: search user(%s), multi entries found", user)
}
sr = srs[i]
if err := conn.Bind(srs[i].Entries[0].DN, pass); err != nil {
return nil, fmt.Errorf("username or password invalid")
}
for _, info := range srs[i].Entries[0].Attributes {
logger.Infof("ldap.info: user(%s) info: %+v", user, info)
}
break
}
if sr == nil {
return nil, fmt.Errorf("username or password invalid")
}
if len(sr.Entries) > 1 {
return nil, fmt.Errorf("ldap.error: search user(%s), multi entries found", user)
}
if err := conn.Bind(sr.Entries[0].DN, pass); err != nil {
return nil, fmt.Errorf("username or password invalid")
}
for _, info := range sr.Entries[0].Attributes {
logger.Infof("ldap.info: user(%s) info: %+v", user, info)
}
return sr, nil
}
@@ -218,21 +234,26 @@ func (s *SsoClient) newLdapConn() (*ldap.Conn, error) {
return conn, nil
}
func (s *SsoClient) ldapReq(conn *ldap.Conn, filter string, values ...interface{}) (*ldap.SearchResult, error) {
searchRequest := ldap.NewSearchRequest(
s.BaseDn, // The base dn to search
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf(filter, values...), // The filter to apply
s.genLdapAttributeSearchList(), // A list attributes to retrieve
nil,
)
func (s *SsoClient) ldapReq(conn *ldap.Conn, filter string, values ...interface{}) ([]*ldap.SearchResult, error) {
srs := make([]*ldap.SearchResult, 0, len(s.BaseDns))
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, fmt.Errorf("ldap.error: ldap search fail: %v", err)
for i := range s.BaseDns {
searchRequest := ldap.NewSearchRequest(
strings.TrimSpace(s.BaseDns[i]), // The base dn to search
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf(filter, values...), // The filter to apply
s.genLdapAttributeSearchList(), // A list attributes to retrieve
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
logger.Errorf("ldap.error: ldap search fail: %v", err)
continue
}
srs = append(srs, sr)
}
return sr, nil
return srs, nil
}
// GetUserRolesAndTeams Gets the roles and teams of the user
@@ -302,6 +323,7 @@ func LdapLogin(ctx *ctx.Context, username, pass string, defaultRoles []string, d
if err != nil {
return nil, err
}
// copy attributes from ldap
ldap.RLock()
attrs := ldap.Attributes

View File

@@ -82,29 +82,35 @@ func (s *SsoClient) UserGetAll() (map[string]*models.User, error) {
}
defer conn.Close()
sr, err := lc.ldapReq(conn, lc.UserFilter)
srs, err := lc.ldapReq(conn, lc.UserFilter)
if err != nil {
return nil, fmt.Errorf("ldap.error: ldap search fail: %v", err)
}
res := make(map[string]*models.User, len(sr.Entries))
for _, entry := range sr.Entries {
attrs := lc.Attributes
username := entry.GetAttributeValue(attrs.Username)
nickname := entry.GetAttributeValue(attrs.Nickname)
email := entry.GetAttributeValue(attrs.Email)
phone := entry.GetAttributeValue(attrs.Phone)
res := make(map[string]*models.User)
// Gets the roles and teams for this entry
roleTeamMapping := lc.GetUserRolesAndTeams(entry)
if len(roleTeamMapping.Roles) == 0 {
// No role mapping is configured, the configured default role is used
roleTeamMapping.Roles = lc.DefaultRoles
for i := range srs {
if srs[i] == nil {
continue
}
user := new(models.User)
user.FullSsoFieldsWithTeams("ldap", username, nickname, phone, email, roleTeamMapping.Roles, roleTeamMapping.Teams)
for _, entry := range srs[i].Entries {
attrs := lc.Attributes
username := entry.GetAttributeValue(attrs.Username)
nickname := entry.GetAttributeValue(attrs.Nickname)
email := entry.GetAttributeValue(attrs.Email)
phone := entry.GetAttributeValue(attrs.Phone)
res[entry.GetAttributeValue(attrs.Username)] = user
// Gets the roles and teams for this entry
roleTeamMapping := lc.GetUserRolesAndTeams(entry)
if len(roleTeamMapping.Roles) == 0 {
// No role mapping is configured, the configured default role is used
roleTeamMapping.Roles = lc.DefaultRoles
}
user := new(models.User)
user.FullSsoFieldsWithTeams("ldap", username, nickname, phone, email, roleTeamMapping.Roles, roleTeamMapping.Teams)
res[entry.GetAttributeValue(attrs.Username)] = user
}
}
return res, nil
@@ -172,13 +178,20 @@ func (s *SsoClient) UserExist(username string) (bool, error) {
}
defer conn.Close()
sr, err := lc.ldapReq(conn, "(&(%s=%s))", lc.Attributes.Username, username)
srs, err := lc.ldapReq(conn, "(&(%s=%s))", lc.Attributes.Username, username)
if err != nil {
return false, err
}
if len(sr.Entries) > 0 {
return true, nil
for i := range srs {
if srs[i] == nil {
continue
}
if len(srs[i].Entries) > 0 {
return true, nil
}
}
return false, nil

View File

@@ -8,12 +8,13 @@ import (
)
type Config struct {
Dir string
Level string
Output string
KeepHours uint
RotateNum int
RotateSize uint64
Dir string
Level string
Output string
KeepHours uint
RotateNum int
RotateSize uint64
OutputToOneFile bool
}
func Init(c Config) (func(), error) {
@@ -35,6 +36,7 @@ func Init(c Config) (func(), error) {
} else {
return nil, errors.New("KeepHours and Rotatenum both are 0")
}
lb.OutputToOneFile(c.OutputToOneFile)
logger.SetLogging(c.Level, lb)
}

View File

@@ -208,7 +208,7 @@ func (s *SsoClient) exchangeUser(code string) (*CallbackOutput, error) {
if err != nil {
return nil, fmt.Errorf("failed to exchange token: %s", err)
}
userInfo, err := s.getUserInfo(s.UserInfoAddr, oauth2Token.AccessToken, s.TranTokenMethod)
userInfo, err := s.getUserInfo(s.Config.ClientID, s.UserInfoAddr, oauth2Token.AccessToken, s.TranTokenMethod)
if err != nil {
logger.Errorf("failed to get user info: %s", err)
return nil, fmt.Errorf("failed to get user info: %s", err)
@@ -223,10 +223,10 @@ func (s *SsoClient) exchangeUser(code string) (*CallbackOutput, error) {
}, nil
}
func (s *SsoClient) getUserInfo(UserInfoAddr, accessToken string, TranTokenMethod string) ([]byte, error) {
func (s *SsoClient) getUserInfo(ClientId, UserInfoAddr, accessToken string, TranTokenMethod string) ([]byte, error) {
var req *http.Request
if TranTokenMethod == "formdata" {
body := bytes.NewBuffer([]byte("access_token=" + accessToken))
body := bytes.NewBuffer([]byte("access_token=" + accessToken + "&client_id=" + ClientId))
r, err := http.NewRequest("POST", UserInfoAddr, body)
if err != nil {
return nil, err
@@ -234,7 +234,7 @@ func (s *SsoClient) getUserInfo(UserInfoAddr, accessToken string, TranTokenMetho
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req = r
} else if TranTokenMethod == "querystring" {
r, err := http.NewRequest("GET", UserInfoAddr+"?access_token="+accessToken, nil)
r, err := http.NewRequest("GET", UserInfoAddr+"?access_token="+accessToken+"&client_id="+ClientId, nil)
if err != nil {
return nil, err
}
@@ -246,6 +246,7 @@ func (s *SsoClient) getUserInfo(UserInfoAddr, accessToken string, TranTokenMetho
return nil, err
}
r.Header.Add("Authorization", "Bearer "+accessToken)
r.Header.Add("client_id", ClientId)
req = r
}

View File

@@ -8,12 +8,21 @@ import (
"github.com/toolkits/pkg/logger"
)
func MathCalc(s string, data map[string]float64) (float64, error) {
m := make(map[string]float64)
var defaultFuncMap = map[string]interface{}{
"between": between,
}
func MathCalc(s string, data map[string]interface{}) (float64, error) {
m := make(map[string]interface{})
for k, v := range data {
m[cleanStr(k)] = v
}
for k, v := range defaultFuncMap {
m[k] = v
}
// 表达式要求类型一致,否则此处编译会报错
program, err := expr.Compile(cleanStr(s), expr.Env(m))
if err != nil {
return 0, err
@@ -32,12 +41,14 @@ func MathCalc(s string, data map[string]float64) (float64, error) {
} else {
return 0, nil
}
} else if result, ok := output.(int); ok {
return float64(result), nil
} else {
return 0, nil
}
}
func Calc(s string, data map[string]float64) bool {
func Calc(s string, data map[string]interface{}) bool {
v, err := MathCalc(s, data)
if err != nil {
logger.Errorf("Calc exp:%s data:%v error: %v", s, data, err)
@@ -57,3 +68,32 @@ func replaceDollarSigns(s string) string {
re := regexp.MustCompile(`\$([A-Z])\.`)
return re.ReplaceAllString(s, "${1}_")
}
// 自定义 expr 函数
// between 函数,判断 target 是否在 arr[0] 和 arr[1] 之间
func between(target float64, arr []interface{}) bool {
if len(arr) != 2 {
return false
}
var min, max float64
switch arr[0].(type) {
case float64:
min = arr[0].(float64)
case int:
min = float64(arr[0].(int))
default:
return false
}
switch arr[1].(type) {
case float64:
max = arr[1].(float64)
case int:
max = float64(arr[1].(int))
default:
return false
}
return target >= min && target <= max
}

View File

@@ -8,140 +8,140 @@ func TestMathCalc(t *testing.T) {
tests := []struct {
name string
expr string
data map[string]float64
data map[string]interface{}
expected float64
wantErr bool
}{
{
name: "Add and Subtract",
expr: "一个 + $.B - $.C",
data: map[string]float64{"一个": 1, "$.B": 2, "$.C": 3},
data: map[string]interface{}{"一个": 1, "$.B": 2, "$.C": 3},
expected: 0,
wantErr: false,
},
{
name: "Multiply and Divide",
expr: "($A.err_count >0&& $A.err_count <=3)||($B.err_count>0 && $B.err_count <=5)",
data: map[string]float64{"$A.err_count": 4, "$B.err_count": 2},
data: map[string]interface{}{"$A.err_count": 4, "$B.err_count": 2},
expected: 1,
wantErr: false,
},
{
name: "Subtract and Add",
expr: "$.C - $.D + $.A",
data: map[string]float64{"$.A": 5, "$.C": 3, "$.D": 2},
data: map[string]interface{}{"$.A": 5, "$.C": 3, "$.D": 2},
expected: 6,
wantErr: false,
},
{
name: "Divide and Multiply",
expr: "$.B / $.C * $.D",
data: map[string]float64{"$.B": 6, "$.C": 2, "$.D": 3},
data: map[string]interface{}{"$.B": 6, "$.C": 2, "$.D": 3},
expected: 9,
wantErr: false,
},
{
name: "Divide and Multiply",
expr: "$.B / $.C * $.D",
data: map[string]float64{"$.B": 6, "$.C": 2, "$.D": 3},
data: map[string]interface{}{"$.B": 6, "$.C": 2, "$.D": 3},
expected: 9,
wantErr: false,
},
{
name: "Multiply and Add",
expr: "$.A * $.B + $.C",
data: map[string]float64{"$.A": 2, "$.B": 3, "$.C": 4},
data: map[string]interface{}{"$.A": 2, "$.B": 3, "$.C": 4},
expected: 10,
wantErr: false,
},
{
name: "Subtract and Divide",
expr: "$.D - $.A / $.B",
data: map[string]float64{"$.D": 10, "$.A": 4, "$.B": 2},
data: map[string]interface{}{"$.D": 10, "$.A": 4, "$.B": 2},
expected: 8,
wantErr: false,
},
{
name: "Add, Subtract and Subtract",
expr: "$.C + $.D - $.A",
data: map[string]float64{"$.C": 3, "$.D": 4, "$.A": 5},
data: map[string]interface{}{"$.C": 3, "$.D": 4, "$.A": 5},
expected: 2,
wantErr: false,
},
{
name: "Multiply and Subtract",
expr: "$.B * $.A - $.D",
data: map[string]float64{"$.B": 2, "$.A": 3, "$.D": 4},
data: map[string]interface{}{"$.B": 2, "$.A": 3, "$.D": 4},
expected: 2,
wantErr: false,
},
{
name: "Divide and Add",
expr: "$.A / $.B + $.C",
data: map[string]float64{"$.A": 4, "$.B": 2, "$.C": 3},
data: map[string]interface{}{"$.A": 4, "$.B": 2, "$.C": 3},
expected: 5,
wantErr: false,
},
{
name: "Add and Multiply",
expr: "$.D + $.A * $.B",
data: map[string]float64{"$.D": 1, "$.A": 2, "$.B": 3},
data: map[string]interface{}{"$.D": 1, "$.A": 2, "$.B": 3},
expected: 7,
wantErr: false,
},
{
name: "Divide and Add with Parentheses",
expr: "($A / $B) + ($C * $D)",
data: map[string]float64{"$A": 4, "$B": 2, "$C": 1, "$D": 3},
data: map[string]interface{}{"$A": 4, "$B": 2, "$C": 1, "$D": 3},
expected: 5.0,
wantErr: false,
},
{
name: "Divide with Parentheses",
expr: "($.A - $.B) / ($.C + $.D)",
data: map[string]float64{"$.A": 6, "$.B": 2, "$.C": 3, "$.D": 1},
data: map[string]interface{}{"$.A": 6, "$.B": 2, "$.C": 3, "$.D": 1},
expected: 1.0,
wantErr: false,
},
{
name: "Add and Multiply with Parentheses",
expr: "($.A + $.B) * ($.C - $.D)",
data: map[string]float64{"$.A": 8, "$.B": 2, "$.C": 4, "$.D": 2},
data: map[string]interface{}{"$.A": 8, "$.B": 2, "$.C": 4, "$.D": 2},
expected: 20,
wantErr: false,
},
{
name: "Divide and Multiply with Parentheses",
expr: "($.A * $.B) / ($.C - $.D)",
data: map[string]float64{"$.A": 8, "$.B": 2, "$.C": 4, "$.D": 2},
data: map[string]interface{}{"$.A": 8, "$.B": 2, "$.C": 4, "$.D": 2},
expected: 8,
wantErr: false,
},
{
name: "Add and Divide with Parentheses",
expr: "$.A + ($.B * $.C) / $.D",
data: map[string]float64{"$.A": 1, "$.B": 2, "$.C": 3, "$.D": 4},
data: map[string]interface{}{"$.A": 1, "$.B": 2, "$.C": 3, "$.D": 4},
expected: 2.5,
wantErr: false,
},
{
name: "Subtract and Multiply with Parentheses",
expr: "($.A + $.B) - ($.C * $.D)",
data: map[string]float64{"$.A": 5, "$.B": 2, "$.C": 3, "$.D": 1},
data: map[string]interface{}{"$.A": 5, "$.B": 2, "$.C": 3, "$.D": 1},
expected: 4,
wantErr: false,
},
{
name: "Multiply and Divide with Parentheses",
expr: "$.A / ($.B - $.C) * $.D",
data: map[string]float64{"$.A": 4, "$.B": 3, "$.C": 2, "$.D": 5},
data: map[string]interface{}{"$.A": 4, "$.B": 3, "$.C": 2, "$.D": 5},
expected: 20.0,
wantErr: false,
},
{
name: "Multiply and Divide with Parentheses 2",
expr: "($.A - $.B) * ($.C / $.D)",
data: map[string]float64{"$.A": 3, "$.B": 1, "$.C": 2, "$.D": 4},
data: map[string]interface{}{"$.A": 3, "$.B": 1, "$.C": 2, "$.D": 4},
expected: 1.0,
wantErr: false,
},
@@ -149,73 +149,80 @@ func TestMathCalc(t *testing.T) {
{
name: "Complex expression",
expr: "$.A/$.B*$.D",
data: map[string]float64{"$.A": 1, "$.B": 2, "$.C": 3, "$.D": 4},
data: map[string]interface{}{"$.A": 1, "$.B": 2, "$.C": 3, "$.D": 4},
expected: 2,
wantErr: false,
},
{
name: "Complex expression",
expr: "$.A/$.B*$.C",
data: map[string]float64{"$.A": 2, "$.B": 2, "$.C": 2},
data: map[string]interface{}{"$.A": 2, "$.B": 2, "$.C": 2},
expected: 2,
wantErr: false,
},
{
name: "Complex expression",
expr: "$.A/($.B*$.C)",
data: map[string]float64{"$.A": 2, "$.B": 2, "$.C": 2},
data: map[string]interface{}{"$.A": 2, "$.B": 2, "$.C": 2},
expected: 0.5,
wantErr: false,
},
{
name: "Addition",
expr: "$.A + $.B",
data: map[string]float64{"$.A": 2, "$.B": 3},
data: map[string]interface{}{"$.A": 2, "$.B": 3},
expected: 5,
wantErr: false,
},
{
name: "Subtraction",
expr: "$.A - $.B",
data: map[string]float64{"$.A": 5, "$.B": 3},
data: map[string]interface{}{"$.A": 5, "$.B": 3},
expected: 2,
wantErr: false,
},
{
name: "Multiplication",
expr: "$.A * $.B",
data: map[string]float64{"$.A": 4, "$.B": 3},
data: map[string]interface{}{"$.A": 4, "$.B": 3},
expected: 12,
wantErr: false,
},
{
name: "Division",
expr: "$.A / $.B",
data: map[string]float64{"$.A": 10, "$.B": 2},
data: map[string]interface{}{"$.A": 10, "$.B": 2},
expected: 5,
wantErr: false,
},
{
name: "Mixed operations",
expr: "($.A + $.B) * ($.C - $.D)",
data: map[string]float64{"$.A": 1, "$.B": 2, "$.C": 5, "$.D": 3},
data: map[string]interface{}{"$.A": 1, "$.B": 2, "$.C": 5, "$.D": 3},
expected: 6, // Corrected from 9 to 6
wantErr: false,
},
{
name: "Division by zero",
expr: "( $D/$A >= 0.1 || $D/$B <= 0.5 ) && $C >= 1000",
data: map[string]float64{"$A": 428382, "$B": 250218, "$C": 305578, "$D": 325028},
expected: 1,
wantErr: true,
},
{
name: "Parentheses",
expr: "($.A + $.B) / ($.C - $.D)",
data: map[string]float64{"$.A": 6, "$.B": 4, "$.C": 10, "$.D": 2},
data: map[string]interface{}{"$.A": 6, "$.B": 4, "$.C": 10, "$.D": 2},
expected: 1.25, // Corrected from 2.5 to 1.25
wantErr: false,
},
{
name: "Add and Multiply with Parentheses for float64 and int",
expr: "($.A + $.B) * ($.C - $.D)",
data: map[string]interface{}{"$.A": 8.0, "$.B": 2.0, "$.C": 4.0, "$.D": 2},
expected: 20,
wantErr: false,
},
{
name: "Divide and Multiply with Parentheses for float64 and int",
expr: "($.A * $.B) / ($.C - $.D)",
data: map[string]interface{}{"$.A": 8, "$.B": 2, "$.C": 4.0, "$.D": 2},
expected: 8,
wantErr: false,
},
}
for _, tc := range tests {
@@ -248,172 +255,269 @@ func TestCalc(t *testing.T) {
tests := []struct {
name string
expr string
data map[string]float64
data map[string]interface{}
expected bool
}{
{
name: "Greater than - true",
expr: "$.A > $.B",
data: map[string]float64{"$.A": 5, "$.B": 3},
data: map[string]interface{}{"$.A": 5, "$.B": 3},
expected: true,
},
{
name: "Multiply and Subtract with Parentheses",
expr: "$A.yesterday_rate > 0.1 && $A.last_week_rate>0.1 or ($A.今天 >300 || $A.昨天>300 || $A.上周今天 > 300)",
data: map[string]float64{"$A.yesterday_rate": 0.1, "$A.last_week_rate": 2, "$A.今天": 200.4, "$A.昨天": 200.4, "$A.上周今天": 200.4},
data: map[string]interface{}{"$A.yesterday_rate": 0.1, "$A.last_week_rate": 2, "$A.今天": 200.4, "$A.昨天": 200.4, "$A.上周今天": 200.4},
expected: false,
},
{
name: "Count Greater Than Zero with Code",
expr: "$A.count > 0",
data: map[string]float64{"$A.count": 197, "$A.code": 30000},
data: map[string]interface{}{"$A.count": 197, "$A.code": 30000},
expected: true,
},
{
name: "Today, Yesterday, and Lastweek Rate Comparison",
expr: "$A.todayRate<0.3 && $A.yesterdayRate<0.3 && $A.lastweekRate<0.3",
data: map[string]float64{"$A.todayRate": 1.1, "$A.yesterdayRate": 0.8, "$A.lastweekRate": 1.2},
data: map[string]interface{}{"$A.todayRate": 1.1, "$A.yesterdayRate": 0.8, "$A.lastweekRate": 1.2},
expected: false,
},
{
name: "Today, Yesterday, and Lastweek Rate Low Threshold",
expr: "$A.todayRate<0.1 && $A.yesterdayRate<0.1 && $A.lastweekRate<0.1",
data: map[string]float64{"$A.todayRate": 0.9, "$A.yesterdayRate": 0.8, "$A.lastweekRate": 0.9},
data: map[string]interface{}{"$A.todayRate": 0.9, "$A.yesterdayRate": 0.8, "$A.lastweekRate": 0.9},
expected: false,
},
{
name: "Agent Specific Today, Yesterday, and Lastweek Rate Comparison",
expr: "$A.agent == 11 && $A.todayRate<0.3 && $A.yesterdayRate<0.3 && $A.lastweekRate<0.3",
data: map[string]float64{"$A.agent": 11, "$A.todayRate": 0.9, "$A.yesterdayRate": 0.9, "$A.lastweekRate": 1},
data: map[string]interface{}{"$A.agent": 11, "$A.todayRate": 0.9, "$A.yesterdayRate": 0.9, "$A.lastweekRate": 1},
expected: false,
},
{
name: "Today, Yesterday, and Lastweek Rate Below 0.1 - Case 1",
expr: "$A<0.1 && $A.yesterdayRate<0.1 && $A.lastweekRate<0.1",
data: map[string]float64{"$A": 0.8, "$A.yesterdayRate": 0.9, "$A.lastweekRate": 0.9},
data: map[string]interface{}{"$A": 0.8, "$A.yesterdayRate": 0.9, "$A.lastweekRate": 0.9},
expected: false,
},
{
name: "Today, Yesterday, and Lastweek Rate Below 0.1 - Case 2",
expr: "$A.today_rate<0.1 && $A.yesterday_rate<0.1 && $A.lastweek_rate<0.1",
data: map[string]float64{"$A.today_rate": 0.9, "$A.yesterday_rate": 0.9, "$A.lastweek_rate": 0.9},
data: map[string]interface{}{"$A.today_rate": 0.9, "$A.yesterday_rate": 0.9, "$A.lastweek_rate": 0.9},
expected: false,
},
{
name: "Today, Yesterday, and Lastweek Rate Below 0.1 - Case 3",
expr: "$B.today_rate<0.1 && $A.yesterday_rate<0.1 && $A.lastweek_rate<0.1",
data: map[string]float64{"$B.today_rate": 0.5, "$A.yesterday_rate": 0.9, "$A.lastweek_rate": 0.8},
data: map[string]interface{}{"$B.today_rate": 0.5, "$A.yesterday_rate": 0.9, "$A.lastweek_rate": 0.8},
expected: false,
},
{
name: "Yesterday and Byesterday Rates Logical Conditions - Case 1",
expr: "($A.yesterday_rate > 2 && $A.byesterday_rate > 2) or ($A.yesterday_rate <= 0.7 && $A.byesterday_rate <= 0.7)",
data: map[string]float64{"$A.yesterday_rate": 3, "$A.byesterday_rate": 3},
data: map[string]interface{}{"$A.yesterday_rate": 3, "$A.byesterday_rate": 3},
expected: true,
},
{
name: "Yesterday and Byesterday Rates Higher Thresholds - Case 1",
expr: "($A.yesterday_rate > 1.5 && $A.byesterday_rate > 1.5) or ($A.yesterday_rate <= 0.8 && $A.byesterday_rate <= 0.8)",
data: map[string]float64{"$A.yesterday_rate": 1.08, "$A.byesterday_rate": 1.02},
data: map[string]interface{}{"$A.yesterday_rate": 1.08, "$A.byesterday_rate": 1.02},
expected: false,
},
{
name: "Greater than - false",
expr: "($A.yesterday_rate > 1.0 && $A.byesterday_rate > 1.0 ) or ($A.yesterday_rate <= 0.9 && $A.byesterday_rate <= 0.9)",
data: map[string]float64{"$A.byesterday_rate": 0.33, "$A.yesterday_rate": 2},
data: map[string]interface{}{"$A.byesterday_rate": 0.33, "$A.yesterday_rate": 2},
expected: false,
},
{
name: "Less than - true",
expr: "$A.count > 100 or $A.count2 > -3",
data: map[string]float64{"$A.count": 5, "$A.count2": -1, "$.D": 2},
data: map[string]interface{}{"$A.count": 5, "$A.count2": -1, "$.D": 2},
expected: true,
},
{
name: "Less than - false",
expr: "$.A < $.B/$.B*4",
data: map[string]float64{"$.A": 5, "$.B": 3},
data: map[string]interface{}{"$.A": 5, "$.B": 3},
expected: false,
},
{
name: "Greater than or equal - true",
expr: "$.A >= $.B",
data: map[string]float64{"$.A": 3, "$.B": 3},
data: map[string]interface{}{"$.A": 3, "$.B": 3},
expected: true,
},
{
name: "Less than or equal - true",
expr: "$.A <= $.B",
data: map[string]float64{"$.A": 2, "$.B": 2},
data: map[string]interface{}{"$.A": 2, "$.B": 2},
expected: true,
},
{
name: "Not equal - true",
expr: "$.A != $.B",
data: map[string]float64{"$.A": 3, "$.B": 2},
data: map[string]interface{}{"$.A": 3, "$.B": 2},
expected: true,
},
{
name: "Not equal - false",
expr: "$.A != $.B",
data: map[string]float64{"$.A": 2, "$.B": 2},
data: map[string]interface{}{"$.A": 2, "$.B": 2},
expected: false,
},
{
name: "Addition resulting in true",
expr: "$.A + $.B > $.C",
data: map[string]float64{"$.A": 3, "$.B": 2, "$.C": 4},
data: map[string]interface{}{"$.A": 3, "$.B": 2, "$.C": 4},
expected: true,
},
{
name: "Subtraction resulting in false",
expr: "$.A - $.B < $.C",
data: map[string]float64{"$.A": 1, "$.B": 3, "$.C": 1},
data: map[string]interface{}{"$.A": 1, "$.B": 3, "$.C": 1},
expected: true,
},
{
name: "Multiplication resulting in true",
expr: "$.A * $.B > $.C",
data: map[string]float64{"$.A": 2, "$.B": 3, "$.C": 5},
data: map[string]interface{}{"$.A": 2, "$.B": 3, "$.C": 5},
expected: true,
},
{
name: "Division resulting in false",
expr: "$.A / $.B*$.C < $.C",
data: map[string]float64{"$.A": 4, "$.B": 2, "$.C": 2},
data: map[string]interface{}{"$.A": 4, "$.B": 2, "$.C": 2},
expected: false,
},
{
name: "Addition with parentheses resulting in true",
expr: "($.A + $.B) > $.C && $.A >0",
data: map[string]float64{"$.A": 1, "$.B": 4, "$.C": 4},
data: map[string]interface{}{"$.A": 1, "$.B": 4, "$.C": 4},
expected: true,
},
{
name: "Addition with parentheses resulting in true",
expr: "($.A + $.B) > $.C || $.A < 0",
data: map[string]float64{"$.A": 1, "$.B": 4, "$.C": 4},
data: map[string]interface{}{"$.A": 1, "$.B": 4, "$.C": 4},
expected: true,
},
{
name: "Complex expression with parentheses resulting in false",
expr: "($.A + $.B) * $.C < $.D",
data: map[string]float64{"$.A": 1, "$.B": 2, "$.C": 3, "$.D": 10},
data: map[string]interface{}{"$.A": 1, "$.B": 2, "$.C": 3, "$.D": 10},
expected: true,
},
{
name: "Nested parentheses resulting in true",
expr: "($.A + ($.B - $.C)) * $.D > $.E",
data: map[string]float64{"$.A": 2, "$.B": 5, "$.C": 2, "$.D": 2, "$.E": 8},
data: map[string]interface{}{"$.A": 2, "$.B": 5, "$.C": 2, "$.D": 2, "$.E": 8},
expected: true,
},
{
name: "Division with parentheses resulting in false",
expr: " ( true || false ) && true",
data: map[string]float64{"$A": 673601, "$A.": 673601, "$B": 250218, "$C": 456513, "$C.": 456513, "$D": 456513, "$D.": 456513},
data: map[string]interface{}{"$A": 673601, "$A.": 673601, "$B": 250218, "$C": 456513, "$C.": 456513, "$D": 456513, "$D.": 456513},
expected: true,
},
// $A:673601.5 $A.:673601.5 $B:361520 $B.:361520 $C:456513 $C.:456513 $D:422634 $D.:422634]
{
name: "Greater than or equal for string - true",
expr: "$.A >= $.B",
data: map[string]interface{}{"$.A": "123", "$.B": "123"},
expected: true,
},
{
name: "Less than or equal - true",
expr: "$.A <= $.B",
data: map[string]interface{}{"$.A": "abc", "$.B": "abc"},
expected: true,
},
{
name: "Not equal - true",
expr: "$.A != $.B",
data: map[string]interface{}{"$.A": "abcde", "$.B": "abcdf"},
expected: true,
},
{
name: "Not equal - false",
expr: "$.A != $.B",
data: map[string]interface{}{"$.A": "!@#$qwer1234", "$.B": "!@#$qwer1234"},
expected: false,
},
{
name: "In operation for string resulting in false",
expr: `$.A in ["admin", "moderator"]`,
data: map[string]interface{}{"$.A": "admin1"},
expected: false,
},
{
name: "In operation for string resulting in true",
expr: `$.A in ["admin", "moderator"]`,
data: map[string]interface{}{"$.A": "admin"},
expected: true,
},
{
name: "In operation for int resulting in false",
expr: `$.A not in [1, 2, 3]`,
data: map[string]interface{}{"$.A": 2},
expected: false,
},
{
name: "In operation for int resulting in true",
expr: `$.A not in [1, 2, 3]`,
data: map[string]interface{}{"$.A": 5},
expected: true,
},
{
name: "Contains operation resulting in true",
expr: `$.A contains $.B`,
data: map[string]interface{}{"$.A": "hello world", "$.B": "world"},
expected: true,
},
{
name: "Contains operation resulting in false",
expr: `$.A contains $.B`,
data: map[string]interface{}{"$.A": "hello world", "$.B": "go"},
expected: false,
},
{
name: "Contains operation resulting in false",
expr: `$.A not contains $.B`,
data: map[string]interface{}{"$.A": "hello world", "$.B": "world"},
expected: false,
},
{
name: "Contains operation resulting in true",
expr: `$.A not contains $.B`,
data: map[string]interface{}{"$.A": "hello world", "$.B": "go"},
expected: true,
},
{
name: "regex operation resulting in true",
expr: `$.A matches $.B`,
data: map[string]interface{}{"$.A": "123", "$.B": "^[0-9]+$"},
expected: true,
},
{
name: "regex operation resulting in false",
expr: `$.A matches $.B`,
data: map[string]interface{}{"$.A": "abc", "$.B": "^[0-9]+$"},
expected: false,
},
{
name: "between function resulting in true",
expr: `between($.A, [100,200])`,
data: map[string]interface{}{"$.A": 155.0},
expected: true,
},
{
name: "between function resulting in false",
expr: `not between($.A, [100.3,200.3])`,
data: map[string]interface{}{"$.A": 155.1},
expected: false,
},
}
for _, tc := range tests {

View File

@@ -697,6 +697,18 @@ func (h *httpAPI) LabelValues(ctx context.Context, label string, matchs []string
}
func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, Warnings, error) {
for i := 0; i < 3; i++ {
value, warnings, err := h.query(ctx, query, ts)
if err == nil {
return value, warnings, nil
}
time.Sleep(100 * time.Millisecond)
}
return nil, nil, errors.New("query failed")
}
func (h *httpAPI) query(ctx context.Context, query string, ts time.Time) (model.Value, Warnings, error) {
u := h.client.URL(epQuery, nil)
q := u.Query()

View File

@@ -48,10 +48,11 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
busiGroupCache := memsto.NewBusiGroupCache(ctx, stats)
targetCache := memsto.NewTargetCache(ctx, stats, nil)
configCvalCache := memsto.NewCvalCache(ctx, stats)
writers := writer.NewWriters(config.Pushgw)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP, configCvalCache.PrintBodyPaths, configCvalCache.PrintAccessLog)
rt := router.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)
rt.Config(r)

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