Compare commits

..

2 Commits

Author SHA1 Message Date
yimeng
7584a14390 修改前端制成品包 (#903) 2022-04-07 13:10:42 +08:00
Ulric Qin
09e6fcc1de add etc/*.local.conf to .gitignore 2022-04-06 11:18:40 +08:00
5817 changed files with 1415006 additions and 79542 deletions

19
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,19 @@
---
name: Bug Report
about: Report a bug encountered while operating Nightingale
labels: kind/bug
---
**What happened**:
**What you expected to happen**:
**How to reproduce it (as minimally and precisely as possible)**:
**Anything else we need to know?**:
**Environment**:
- Nightingale Version (`./n9e-rdb -v`):
- OS Version (`uname -a`):
- Logs (stdout/error/warning/http.request/http.response):
- Others:

View File

@@ -1,67 +0,0 @@
name: Bug Report
description: Report a bug encountered while running Nightingale
labels: ["kind/bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking time to fill out this bug report!
The more detailed the form is filled in, the easier the problem will be solved.
- type: textarea
id: config
attributes:
label: Relevant server.conf | webapi.conf
description: Place config in the toml code section. This will be automatically formatted into toml, so no need for backticks.
render: toml
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant logs
description: categraf | telegraf | server | webapi | prometheus | chrome request/response ...
render: text
validations:
required: true
- type: input
id: system-info
attributes:
label: System info
description: Include nightingale version, operating system, and other relevant details
placeholder: ex. n9e 5.9.2, n9e-fe 5.5.0, categraf 0.1.0, Ubuntu 20.04, Docker 20.10.8
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Describe the steps to reproduce the bug.
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: Describe what you expected to happen when you performed the above steps.
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
description: Describe what actually happened when you performed the above steps.
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional info
description: Include gist of relevant config, logs, etc.
validations:
required: false

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Nightingale docs
url: https://n9e.github.io/
about: You may want to read through the document before asking questions.
- name: Nightingale community
url: https://n9e.didiyun.com/community/
about: List of communication channels for the Nightingale community.

View File

@@ -1,32 +1,26 @@
name: Release
name: Go
on:
push:
tags:
- 'v*'
env:
GO_VERSION: 1.18
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
goreleaser:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Go Environment
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Build
run: ./control build

32
.gitignore vendored
View File

@@ -26,32 +26,38 @@ _test
/log*
/bin
/out
/meta
/pub
/build
/dist
/etc/*.local.yml
/etc/*.local.conf
/etc/plugins/*.local.yml
/etc/log/log.test.json
/data*
/tarball
/run
/vendor
/tmp
/pub
/n9e
/docker/pub
/docker/n9e
/docker/mysqldata
/docker/experience_pg_vm/pgdata
/etc.local*
.alerts
.idea
.index
.vscode
.DS_Store
.cache-loader
.payload
queries.active
/n9e-*
n9e.sql
/src/modules/agent/agent
/src/modules/ams/ams
/src/modules/index/index
/src/modules/job/job
/src/modules/judge/judge
/src/modules/monapi/monapi
/src/modules/rdb/rdb
/src/modules/transfer/transfer
/src/modules/tsdb/tsdb
tmp/
main
Makefile
src/modules/monapi/plugins/snmp/
src/modules/monapi/plugins/oracle/

View File

@@ -1,138 +0,0 @@
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
- go install github.com/rakyll/statik
snapshot:
name_template: '{{ .Tag }}'
checksum:
name_template: 'checksums.txt'
changelog:
skip: true
builds:
- id: build
hooks:
pre:
- ./fe.sh
main: ./cmd/center/
binary: n9e
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
ldflags:
- -s -w
- -X github.com/ccfos/nightingale/v6/pkg/version.Version={{ .Tag }}-{{.Commit}}
- id: build-cli
main: ./cmd/cli/
binary: n9e-cli
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
ldflags:
- -s -w
- -X github.com/ccfos/nightingale/v6/pkg/version.Version={{ .Tag }}-{{.Commit}}
- id: build-alert
main: ./cmd/alert/
binary: n9e-alert
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
ldflags:
- -s -w
- -X github.com/ccfos/nightingale/v6/pkg/version.Version={{ .Tag }}-{{.Commit}}
- id: build-pushgw
main: ./cmd/pushgw/
binary: n9e-pushgw
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
ldflags:
- -s -w
- -X github.com/ccfos/nightingale/v6/pkg/version.Version={{ .Tag }}-{{.Commit}}
archives:
- id: n9e
builds:
- build
- build-cli
- build-alert
- build-pushgw
format: tar.gz
format_overrides:
- goos: windows
format: zip
name_template: "n9e-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
wrap_in_directory: false
files:
- docker/*
- etc/*
- pub/*
- integrations/*
- cli/*
- n9e.sql
release:
github:
owner: ccfos
name: nightingale
name_template: "v{{ .Version }}"
dockers:
- image_templates:
- flashcatcloud/nightingale:{{ .Version }}-amd64
goos: linux
goarch: amd64
ids:
- build
dockerfile: docker/Dockerfile.goreleaser
extra_files:
- pub
- etc
- integrations
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- image_templates:
- flashcatcloud/nightingale:{{ .Version }}-arm64v8
goos: linux
goarch: arm64
ids:
- build
dockerfile: docker/Dockerfile.goreleaser.arm64
extra_files:
- pub
- etc
- integrations
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
docker_manifests:
- name_template: flashcatcloud/nightingale:{{ .Version }}
image_templates:
- flashcatcloud/nightingale:{{ .Version }}-amd64
- flashcatcloud/nightingale:{{ .Version }}-arm64v8
- name_template: flashcatcloud/nightingale:latest
image_templates:
- flashcatcloud/nightingale:{{ .Version }}-amd64
- flashcatcloud/nightingale:{{ .Version }}-arm64v8

View File

@@ -430,4 +430,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.

View File

@@ -1,38 +0,0 @@
.PHONY: prebuild start build
ROOT:=$(shell pwd -P)
GIT_COMMIT:=$(shell git --work-tree ${ROOT} rev-parse 'HEAD^{commit}')
_GIT_VERSION:=$(shell git --work-tree ${ROOT} describe --tags --abbrev=14 "${GIT_COMMIT}^{commit}" 2>/dev/null)
TAG=$(shell echo "${_GIT_VERSION}" | awk -F"-" '{print $$1}')
RELEASE_VERSION:="$(TAG)-$(GIT_COMMIT)"
prebuild:
echo "begin download and embed the front-end file..."
sh fe.sh
echo "front-end file download and embedding completed."
all: build
build:
go build -ldflags "-w -s -X github.com/ccfos/nightingale/v6/pkg/version.Version=$(RELEASE_VERSION)" -o n9e ./cmd/center/main.go
build-alert:
go build -ldflags "-w -s -X github.com/ccfos/nightingale/v6/pkg/version.Version=$(RELEASE_VERSION)" -o n9e-alert ./cmd/alert/main.go
build-pushgw:
go build -ldflags "-w -s -X github.com/ccfos/nightingale/v6/pkg/version.Version=$(RELEASE_VERSION)" -o n9e-pushgw ./cmd/pushgw/main.go
build-cli:
go build -ldflags "-w -s -X github.com/ccfos/nightingale/v6/pkg/version.Version=$(RELEASE_VERSION)" -o n9e-cli ./cmd/cli/main.go
run:
nohup ./n9e > n9e.log 2>&1 &
run_alert:
nohup ./n9e-alert > n9e-alert.log 2>&1 &
run_pushgw:
nohup ./n9e-pushgw > n9e-pushgw.log 2>&1 &
release:
goreleaser --skip-validate --skip-publish --snapshot

View File

@@ -1,68 +1,26 @@
<p align="center">
<a href="https://github.com/ccfos/nightingale">
<img src="doc/img/nightingale_logo_h.png" alt="nightingale - cloud native monitoring" width="240" /></a>
</p>
<img src="https://s3-gz01.didistatic.com/n9e-pub/logo/nightingale-logo.png" width="200" alt="Nightingale"/>
<p align="center">
<img alt="GitHub latest release" src="https://img.shields.io/github/v/release/ccfos/nightingale"/>
<a href="https://n9e.github.io">
<img alt="Docs" src="https://img.shields.io/badge/docs-get%20started-brightgreen"/></a>
<a href="https://hub.docker.com/u/flashcatcloud">
<img alt="Docker pulls" src="https://img.shields.io/docker/pulls/flashcatcloud/nightingale"/></a>
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/ccfos/nightingale">
<img alt="GitHub Repo issues" src="https://img.shields.io/github/issues/ccfos/nightingale">
<img alt="GitHub Repo issues closed" src="https://img.shields.io/github/issues-closed/ccfos/nightingale">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/ccfos/nightingale">
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors-anon/ccfos/nightingale"/></a>
<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">
告警管理专家,一体化开源观测平台!
</p>
---
[English](./README_en.md) | [中文](./README.md)
### 夜莺简介
## 资料
夜莺是一套分布式高可用的运维监控系统最大的特点是混合云支持既可以支持传统物理机虚拟机的场景也可以支持K8S容器的场景。同时夜莺也不只是监控还有一部分CMDB的能力、自动化运维的能力很多公司都基于夜莺开发自己公司的运维平台。开源的这部分功能模块也是商业版本的一部分所以可靠性有保障、会持续维护诸君可放心使用。效果图如下
- 文档:[https://flashcat.cloud/docs/](https://flashcat.cloud/docs/)
- 论坛提问:[https://answer.flashcat.cloud/](https://answer.flashcat.cloud/)
- 报Bug[https://github.com/ccfos/nightingale/issues](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml)
- 商业版本:[企业版](https://mp.weixin.qq.com/s/FOwnnGPkRao2ZDV574EHrw) | [专业版](https://mp.weixin.qq.com/s/uM2a8QUDJEYwdBpjkbQDxA) 感兴趣请 [联系我们交流试用](https://flashcat.cloud/contact/)
![Nightingale](https://s3-gz01.didistatic.com/n9e-pub/image/snapshot/n9e-screen.png)
## 功能和特点
### OCE认证
- **统一接入各种时序库**:支持对接 Prometheus、VictoriaMetrics、Thanos、Mimir、M3DB 等多种时序库,实现统一告警管理
- **专业告警能力**:内置支持多种告警规则,可以扩展支持所有通知媒介,支持告警屏蔽、告警抑制、告警自愈、告警事件管理
- **无缝搭配 [FlashDuty](https://flashcat.cloud/product/flashcat-duty/)**实现告警聚合收敛、认领、升级、排班、IM集成确保告警处理不遗漏减少打扰更好协同
- **支持所有常见采集器**:支持 categraf、telegraf、grafana-agent、datadog-agent、给类 exporter 作为采集器,没有什么数据是不能监控的
- **统一的观测平台**:从 v6 版本开始,支持接入 ElasticSearch、Jaeger 数据源,逐步实现日志、链路、指标的一体化观测
OCE是一个认证机制和交流平台为夜莺生产用户量身打造我们会为OCE企业提供更好的技术支持比如专属的技术沙龙、企业一对一的交流机会、专属的答疑群等如果贵司已将夜莺上了生产[快来加入吧](https://v.didi.cn/qAA1kY)
## 产品示意图
### 文档资料
https://user-images.githubusercontent.com/792850/216888712-2565fcea-9df5-47bd-a49e-d60af9bd76e8.mp4
- **文档手册** [https://n9e.didiyun.com/](https://n9e.didiyun.com/) 欢迎大家一起完善
- **视频教程**[https://space.bilibili.com/442531657](https://space.bilibili.com/442531657) 欢迎大家关注
- **音频答疑**[https://www.ximalaya.com/keji/45095827/](https://www.ximalaya.com/keji/45095827/) 欢迎大家关注
- **二次开发**[https://xie.infoq.cn/article/30d37e98fbe52ff2a79fc04b4](https://xie.infoq.cn/article/30d37e98fbe52ff2a79fc04b4) 欢迎大家共建
### 交流互助
## 加入交流群
欢迎加入 QQ 交流群群号479290895也可以扫下方二维码加入微信交流群
<img src="doc/img/wecom.png" width="240">
## Stargazers over time
[![Stargazers over time](https://starchart.cc/ccfos/nightingale.svg)](https://starchart.cc/ccfos/nightingale)
## Contributors
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
<img src="https://contrib.rocks/image?repo=ccfos/nightingale" />
</a>
## License
[Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)
## 社区管理
[夜莺开源项目和社区治理架构(草案)](./doc/community-governance.md)
关注公众号 Obsuite(官方公众号) 回复 "夜莺加群"
<img src="https://s3-gz01.didistatic.com/n9e-pub/image/qrcode_obsuite.jpg" width="250" alt="Nightingale"/>

View File

@@ -1,104 +0,0 @@
<p align="center">
<a href="https://github.com/ccfos/nightingale">
<img src="doc/img/nightingale_logo_h.png" alt="nightingale - cloud native monitoring" width="240" /></a>
</p>
<p align="center">
<img alt="GitHub latest release" src="https://img.shields.io/github/v/release/ccfos/nightingale"/>
<a href="https://n9e.github.io">
<img alt="Docs" src="https://img.shields.io/badge/docs-get%20started-brightgreen"/></a>
<a href="https://hub.docker.com/u/flashcatcloud">
<img alt="Docker pulls" src="https://img.shields.io/docker/pulls/flashcatcloud/nightingale"/></a>
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/ccfos/nightingale">
<img alt="GitHub Repo issues" src="https://img.shields.io/github/issues/ccfos/nightingale">
<img alt="GitHub Repo issues closed" src="https://img.shields.io/github/issues-closed/ccfos/nightingale">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/ccfos/nightingale">
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors-anon/ccfos/nightingale"/></a>
<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.md) | [中文](./README_ZH.md)
## Highlighted Features
- **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**.
#### If you are using Prometheus and have one or more of the following requirement scenarios, it is recommended that you upgrade to Nightingale:
- 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;
#### If you are using Zabbix and have the following scenarios, it is recommended that you upgrade to Nightingale:
- 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;
#### 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
[English Doc](https://n9e.github.io/) | [中文文档](http://n9e.flashcat.cloud/)
## Screenshots
https://user-images.githubusercontent.com/792850/216888712-2565fcea-9df5-47bd-a49e-d60af9bd76e8.mp4
## Architecture
<img src="doc/img/arch-product.png" width="600">
Nightingale monitoring can receive monitoring data reported by various collectors (such as [Categraf](https://github.com/flashcatcloud/categraf) , telegraf, grafana-agent, Prometheus, etc.) and write them to various popular time-series databases (such as Prometheus, M3DB, VictoriaMetrics, Thanos, TDEngine, etc.). It provides configuration capabilities for alert rules, silence rules, and subscription rules, as well as the ability to view monitoring data. It also provides automatic alarm self-healing mechanisms (such as automatically calling back to a webhook address or executing a script after an alarm is triggered), and the ability to store and manage historical alarm events and view them in groups.
If the performance of a standalone time-series database (such as Prometheus) has bottlenecks or poor disaster recovery, we recommend using [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics). The VictoriaMetrics architecture is relatively simple, has excellent performance, and is easy to deploy and maintain. The architecture diagram is as shown above. For more detailed documentation on VictoriaMetrics, please refer to its [official website](https://victoriametrics.com/).
**We welcome you to participate in the Nightingale open-source project and community in various ways, including but not limited to**
- Adding and improving documentation => [n9e.github.io](https://n9e.github.io/)
- Sharing your best practices and experience in using Nightingale monitoring => [Article sharing]((https://n9e.github.io/docs/prologue/share/))
- Submitting product suggestions => [github issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md)
- Submitting code to make Nightingale monitoring faster, more stable, and easier to use => [github pull request](https://github.com/didi/nightingale/pulls)
**Respecting, recognizing, and recording the work of every contributor** is the first guiding principle of the Nightingale open-source community. We advocate effective questioning, which not only respects the developer's time but also contributes to the accumulation of knowledge in the entire community
- Before asking a question, please first refer to the [FAQ](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq)
- We use [GitHub Discussions](https://github.com/ccfos/nightingale/discussions) as the communication forum. You can search and ask questions here.
- We also recommend that you join ours [Slack channel](https://n9e-talk.slack.com/) to exchange experiences with other Nightingale users.
## 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)**.
## Stargazers over time
[![Stargazers over time](https://starchart.cc/ccfos/nightingale.svg)](https://starchart.cc/ccfos/nightingale)
## Contributors
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
<img src="https://contrib.rocks/image?repo=ccfos/nightingale" />
</a>
## License
[Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)

View File

@@ -1,73 +0,0 @@
package aconf
import (
"path"
"github.com/toolkits/pkg/runner"
)
type Alert struct {
EngineDelay int64
Heartbeat HeartbeatConfig
Alerting Alerting
}
type SMTPConfig struct {
Host string
Port int
User string
Pass string
From string
InsecureSkipVerify bool
Batch int
}
type HeartbeatConfig struct {
IP string
Interval int64
Endpoint string
EngineName string
}
type Alerting struct {
Timeout int64
TemplatesDir string
NotifyConcurrency int
}
type CallPlugin struct {
Enable bool
PluginPath string
Caller string
}
type RedisPub struct {
Enable bool
ChannelPrefix string
ChannelKey string
}
type Ibex struct {
Address string
BasicAuthUser string
BasicAuthPass string
Timeout int64
}
func (a *Alert) PreCheck() {
if a.Alerting.TemplatesDir == "" {
a.Alerting.TemplatesDir = path.Join(runner.Cwd, "etc", "template")
}
if a.Alerting.NotifyConcurrency == 0 {
a.Alerting.NotifyConcurrency = 10
}
if a.Heartbeat.Interval == 0 {
a.Heartbeat.Interval = 1000
}
if a.Heartbeat.EngineName == "" {
a.Heartbeat.EngineName = "default"
}
}

View File

@@ -1,93 +0,0 @@
package alert
import (
"context"
"fmt"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/dispatch"
"github.com/ccfos/nightingale/v6/alert/eval"
"github.com/ccfos/nightingale/v6/alert/naming"
"github.com/ccfos/nightingale/v6/alert/process"
"github.com/ccfos/nightingale/v6/alert/queue"
"github.com/ccfos/nightingale/v6/alert/record"
"github.com/ccfos/nightingale/v6/alert/router"
"github.com/ccfos/nightingale/v6/alert/sender"
"github.com/ccfos/nightingale/v6/conf"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/pkg/logx"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/ccfos/nightingale/v6/pushgw/writer"
)
func Initialize(configDir string, cryptoKey string) (func(), error) {
config, err := conf.InitConfig(configDir, cryptoKey)
if err != nil {
return nil, fmt.Errorf("failed to init config: %v", err)
}
logxClean, err := logx.Init(config.Log)
if err != nil {
return nil, err
}
ctx := ctx.NewContext(context.Background(), nil, false, config.CenterApi)
syncStats := memsto.NewSyncStats()
alertStats := astats.NewSyncStats()
targetCache := memsto.NewTargetCache(ctx, syncStats, nil)
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
alertMuteCache := memsto.NewAlertMuteCache(ctx, syncStats)
alertRuleCache := memsto.NewAlertRuleCache(ctx, syncStats)
notifyConfigCache := memsto.NewNotifyConfigCache(ctx)
dsCache := memsto.NewDatasourceCache(ctx, syncStats)
promClients := prom.NewPromClient(ctx, config.Alert.Heartbeat)
externalProcessors := process.NewExternalProcessors()
Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, dsCache, ctx, promClients)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
rt := router.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
rt.Config(r)
httpClean := httpx.Init(config.HTTP, r)
return func() {
logxClean()
httpClean()
}, nil
}
func Start(alertc aconf.Alert, pushgwc pconf.Pushgw, syncStats *memsto.Stats, alertStats *astats.Stats, externalProcessors *process.ExternalProcessorsType, targetCache *memsto.TargetCacheType, busiGroupCache *memsto.BusiGroupCacheType,
alertMuteCache *memsto.AlertMuteCacheType, alertRuleCache *memsto.AlertRuleCacheType, notifyConfigCache *memsto.NotifyConfigCacheType, datasourceCache *memsto.DatasourceCacheType, ctx *ctx.Context, promClients *prom.PromClientMap) {
userCache := memsto.NewUserCache(ctx, syncStats)
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
alertSubscribeCache := memsto.NewAlertSubscribeCache(ctx, syncStats)
recordingRuleCache := memsto.NewRecordingRuleCache(ctx, syncStats)
go models.InitNotifyConfig(ctx, alertc.Alerting.TemplatesDir)
naming := naming.NewNaming(ctx, alertc.Heartbeat)
writers := writer.NewWriters(pushgwc)
record.NewScheduler(alertc, recordingRuleCache, promClients, writers, alertStats)
eval.NewScheduler(alertc, externalProcessors, alertRuleCache, targetCache, busiGroupCache, alertMuteCache, datasourceCache, promClients, naming, ctx, alertStats)
dp := dispatch.NewDispatch(alertRuleCache, userCache, userGroupCache, alertSubscribeCache, targetCache, notifyConfigCache, alertc.Alerting, ctx)
consumer := dispatch.NewConsumer(alertc.Alerting, ctx, dp)
go dp.ReloadTpls()
go consumer.LoopConsume()
go queue.ReportQueueSize(alertStats)
go sender.StartEmailSender(notifyConfigCache.GetSMTP()) // todo
}

View File

@@ -1,93 +0,0 @@
package astats
import (
"github.com/prometheus/client_golang/prometheus"
)
const (
namespace = "n9e"
subsystem = "alert"
)
type Stats struct {
CounterSampleTotal *prometheus.CounterVec
CounterAlertsTotal *prometheus.CounterVec
GaugeAlertQueueSize prometheus.Gauge
GaugeSampleQueueSize *prometheus.GaugeVec
RequestDuration *prometheus.HistogramVec
ForwardDuration *prometheus.HistogramVec
}
func NewSyncStats() *Stats {
// 从各个接收接口接收到的监控数据总量
CounterSampleTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "samples_received_total",
Help: "Total number samples received.",
}, []string{"cluster", "channel"})
// 产生的告警总量
CounterAlertsTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "alerts_total",
Help: "Total number alert events.",
}, []string{"cluster"})
// 内存中的告警事件队列的长度
GaugeAlertQueueSize := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "alert_queue_size",
Help: "The size of alert queue.",
})
// 数据转发队列,各个队列的长度
GaugeSampleQueueSize := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "sample_queue_size",
Help: "The size of sample queue.",
}, []string{"cluster", "channel_number"})
// 一些重要的请求,比如接收数据的请求,应该统计一下延迟情况
RequestDuration := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Buckets: []float64{.01, .1, 1},
Name: "http_request_duration_seconds",
Help: "HTTP request latencies in seconds.",
}, []string{"code", "path", "method"},
)
// 发往后端TSDB延迟如何
ForwardDuration := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Buckets: []float64{.1, 1, 10},
Name: "forward_duration_seconds",
Help: "Forward samples to TSDB. latencies in seconds.",
}, []string{"cluster", "channel_number"},
)
prometheus.MustRegister(
CounterSampleTotal,
CounterAlertsTotal,
GaugeAlertQueueSize,
GaugeSampleQueueSize,
RequestDuration,
ForwardDuration,
)
return &Stats{
CounterSampleTotal: CounterSampleTotal,
CounterAlertsTotal: CounterAlertsTotal,
GaugeAlertQueueSize: GaugeAlertQueueSize,
GaugeSampleQueueSize: GaugeSampleQueueSize,
RequestDuration: RequestDuration,
ForwardDuration: ForwardDuration,
}
}

View File

@@ -1,110 +0,0 @@
package common
import (
"fmt"
"math"
"strings"
"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"`
}
func NewAnomalyPoint(key string, labels map[string]string, ts int64, value float64, severity int) AnomalyPoint {
anomalyPointLabels := make(model.Metric)
for k, v := range labels {
anomalyPointLabels[model.LabelName(k)] = model.LabelValue(v)
}
anomalyPointLabels[model.MetricNameLabel] = model.LabelValue(key)
return AnomalyPoint{
Key: key,
Labels: anomalyPointLabels,
Timestamp: ts,
Value: value,
Severity: severity,
}
}
func (v *AnomalyPoint) ReadableValue() string {
ret := fmt.Sprintf("%.5f", v.Value)
ret = strings.TrimRight(ret, "0")
return strings.TrimRight(ret, ".")
}
func ConvertAnomalyPoints(value model.Value) (lst []AnomalyPoint) {
if value == nil {
return
}
switch value.Type() {
case model.ValVector:
items, ok := value.(model.Vector)
if !ok {
return
}
for _, item := range items {
if math.IsNaN(float64(item.Value)) {
continue
}
lst = append(lst, AnomalyPoint{
Key: item.Metric.String(),
Timestamp: item.Timestamp.Unix(),
Value: float64(item.Value),
Labels: item.Metric,
})
}
case model.ValMatrix:
items, ok := value.(model.Matrix)
if !ok {
return
}
for _, item := range items {
if len(item.Values) == 0 {
return
}
last := item.Values[len(item.Values)-1]
if math.IsNaN(float64(last.Value)) {
continue
}
lst = append(lst, AnomalyPoint{
Key: item.Metric.String(),
Labels: item.Metric,
Timestamp: last.Timestamp.Unix(),
Value: float64(last.Value),
})
}
case model.ValScalar:
item, ok := value.(*model.Scalar)
if !ok {
return
}
if math.IsNaN(float64(item.Value)) {
return
}
lst = append(lst, AnomalyPoint{
Key: "{}",
Timestamp: item.Timestamp.Unix(),
Value: float64(item.Value),
Labels: model.Metric{},
})
default:
return
}
return
}

View File

@@ -1,45 +0,0 @@
package common
import (
"fmt"
"github.com/ccfos/nightingale/v6/models"
)
func RuleKey(datasourceId, id int64) string {
return fmt.Sprintf("alert-%d-%d", datasourceId, id)
}
func MatchTags(eventTagsMap map[string]string, itags []models.TagFilter) bool {
for _, filter := range itags {
value, has := eventTagsMap[filter.Key]
if !has {
return false
}
if !matchTag(value, filter) {
return false
}
}
return true
}
func matchTag(value string, filter models.TagFilter) bool {
switch filter.Func {
case "==":
return filter.Value == value
case "!=":
return filter.Value != value
case "in":
_, has := filter.Vset[value]
return has
case "not in":
_, has := filter.Vset[value]
return !has
case "=~":
return filter.Regexp.MatchString(value)
case "!~":
return !filter.Regexp.MatchString(value)
}
// unexpect func
return false
}

View File

@@ -1,99 +0,0 @@
package dispatch
import (
"fmt"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/queue"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/toolkits/pkg/concurrent/semaphore"
"github.com/toolkits/pkg/logger"
)
type Consumer struct {
alerting aconf.Alerting
ctx *ctx.Context
dispatch *Dispatch
}
// 创建一个 Consumer 实例
func NewConsumer(alerting aconf.Alerting, ctx *ctx.Context, dispatch *Dispatch) *Consumer {
return &Consumer{
alerting: alerting,
ctx: ctx,
dispatch: dispatch,
}
}
func (e *Consumer) LoopConsume() {
sema := semaphore.NewSemaphore(e.alerting.NotifyConcurrency)
duration := time.Duration(100) * time.Millisecond
for {
events := queue.EventQueue.PopBackBy(100)
if len(events) == 0 {
time.Sleep(duration)
continue
}
e.consume(events, sema)
}
}
func (e *Consumer) consume(events []interface{}, sema *semaphore.Semaphore) {
for i := range events {
if events[i] == nil {
continue
}
event := events[i].(*models.AlertCurEvent)
sema.Acquire()
go func(event *models.AlertCurEvent) {
defer sema.Release()
e.consumeOne(event)
}(event)
}
}
func (e *Consumer) consumeOne(event *models.AlertCurEvent) {
LogEvent(event, "consume")
if err := event.ParseRule("rule_name"); err != nil {
event.RuleName = fmt.Sprintf("failed to parse rule name: %v", err)
}
if err := event.ParseRule("rule_note"); err != nil {
event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
}
if err := event.ParseRule("annotations"); err != nil {
event.Annotations = fmt.Sprintf("failed to parse rule note: %v", err)
}
e.persist(event)
if event.IsRecovered && event.NotifyRecovered == 0 {
return
}
e.dispatch.HandleEventNotify(event, false)
}
func (e *Consumer) persist(event *models.AlertCurEvent) {
if !e.ctx.IsCenter {
event.DB2FE()
err := poster.PostByUrls(e.ctx, "/v1/n9e/event-persist", event)
if err != nil {
logger.Errorf("event%+v persist err:%v", event, err)
}
return
}
err := models.EventPersist(e.ctx, event)
if err != nil {
logger.Errorf("event%+v persist err:%v", event, err)
}
}

View File

@@ -1,271 +0,0 @@
package dispatch
import (
"bytes"
"encoding/json"
"html/template"
"strconv"
"sync"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/common"
"github.com/ccfos/nightingale/v6/alert/sender"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/toolkits/pkg/logger"
)
type Dispatch struct {
alertRuleCache *memsto.AlertRuleCacheType
userCache *memsto.UserCacheType
userGroupCache *memsto.UserGroupCacheType
alertSubscribeCache *memsto.AlertSubscribeCacheType
targetCache *memsto.TargetCacheType
notifyConfigCache *memsto.NotifyConfigCacheType
alerting aconf.Alerting
senders map[string]sender.Sender
tpls map[string]*template.Template
ExtraSenders map[string]sender.Sender
ctx *ctx.Context
RwLock sync.RWMutex
}
// 创建一个 Notify 实例
func NewDispatch(alertRuleCache *memsto.AlertRuleCacheType, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType,
alertSubscribeCache *memsto.AlertSubscribeCacheType, targetCache *memsto.TargetCacheType, notifyConfigCache *memsto.NotifyConfigCacheType,
alerting aconf.Alerting, ctx *ctx.Context) *Dispatch {
notify := &Dispatch{
alertRuleCache: alertRuleCache,
userCache: userCache,
userGroupCache: userGroupCache,
alertSubscribeCache: alertSubscribeCache,
targetCache: targetCache,
notifyConfigCache: notifyConfigCache,
alerting: alerting,
senders: make(map[string]sender.Sender),
tpls: make(map[string]*template.Template),
ExtraSenders: make(map[string]sender.Sender),
ctx: ctx,
}
return notify
}
func (e *Dispatch) ReloadTpls() error {
err := e.relaodTpls()
if err != nil {
logger.Error("failed to reload tpls: %v", err)
}
duration := time.Duration(9000) * time.Millisecond
for {
time.Sleep(duration)
if err := e.relaodTpls(); err != nil {
logger.Warning("failed to reload tpls:", err)
}
}
}
func (e *Dispatch) relaodTpls() error {
tmpTpls, err := models.ListTpls(e.ctx)
if err != nil {
return err
}
smtp := e.notifyConfigCache.GetSMTP()
senders := map[string]sender.Sender{
models.Email: sender.NewSender(models.Email, tmpTpls, smtp),
models.Dingtalk: sender.NewSender(models.Dingtalk, tmpTpls, smtp),
models.Wecom: sender.NewSender(models.Wecom, tmpTpls, smtp),
models.Feishu: sender.NewSender(models.Feishu, tmpTpls, smtp),
models.Mm: sender.NewSender(models.Mm, tmpTpls, smtp),
models.Telegram: sender.NewSender(models.Telegram, tmpTpls, smtp),
}
e.RwLock.RLock()
for channel, sender := range e.ExtraSenders {
senders[channel] = sender
}
e.RwLock.RUnlock()
e.RwLock.Lock()
e.tpls = tmpTpls
e.senders = senders
e.RwLock.Unlock()
return nil
}
// HandleEventNotify 处理event事件的主逻辑
// event: 告警/恢复事件
// isSubscribe: 告警事件是否由subscribe的配置产生
func (e *Dispatch) HandleEventNotify(event *models.AlertCurEvent, isSubscribe bool) {
rule := e.alertRuleCache.Get(event.RuleId)
if rule == nil {
return
}
fillUsers(event, e.userCache, e.userGroupCache)
var (
// 处理事件到 notifyTarget 关系,处理的notifyTarget用OrMerge进行合并
handlers []NotifyTargetDispatch
// 额外去掉一些订阅,处理的notifyTarget用AndMerge进行合并, 如设置 channel=false,合并后不通过这个channel发送
// 如果实现了相关 Dispatch,可以添加到interceptors中
interceptorHandlers []NotifyTargetDispatch
)
if isSubscribe {
handlers = []NotifyTargetDispatch{NotifyGroupDispatch, EventCallbacksDispatch}
} else {
handlers = []NotifyTargetDispatch{NotifyGroupDispatch, GlobalWebhookDispatch, EventCallbacksDispatch}
}
notifyTarget := NewNotifyTarget()
// 处理订阅关系使用OrMerge
for _, handler := range handlers {
notifyTarget.OrMerge(handler(rule, event, notifyTarget, e))
}
// 处理移除订阅关系的逻辑,比如员工离职,临时静默某个通道的策略等
for _, handler := range interceptorHandlers {
notifyTarget.AndMerge(handler(rule, event, notifyTarget, e))
}
// 处理事件发送,这里用一个goroutine处理一个event的所有发送事件
go e.Send(rule, event, notifyTarget, isSubscribe)
// 如果是不是订阅规则出现的event, 则需要处理订阅规则的event
if !isSubscribe {
e.handleSubs(event)
}
}
func (e *Dispatch) handleSubs(event *models.AlertCurEvent) {
// handle alert subscribes
subscribes := make([]*models.AlertSubscribe, 0)
// rule specific subscribes
if subs, has := e.alertSubscribeCache.Get(event.RuleId); has {
subscribes = append(subscribes, subs...)
}
// global subscribes
if subs, has := e.alertSubscribeCache.Get(0); has {
subscribes = append(subscribes, subs...)
}
for _, sub := range subscribes {
e.handleSub(sub, *event)
}
}
// handleSub 处理订阅规则的event,注意这里event要使用值传递,因为后面会修改event的状态
func (e *Dispatch) handleSub(sub *models.AlertSubscribe, event models.AlertCurEvent) {
if sub.IsDisabled() || !sub.MatchCluster(event.DatasourceId) {
return
}
if !common.MatchTags(event.TagsMap, sub.ITags) {
return
}
if sub.ForDuration > (event.TriggerTime - event.FirstTriggerTime) {
return
}
sub.ModifyEvent(&event)
LogEvent(&event, "subscribe")
e.HandleEventNotify(&event, true)
}
func (e *Dispatch) Send(rule *models.AlertRule, event *models.AlertCurEvent, notifyTarget *NotifyTarget, isSubscribe bool) {
for channel, uids := range notifyTarget.ToChannelUserMap() {
ctx := sender.BuildMessageContext(rule, event, uids, e.userCache)
e.RwLock.RLock()
s := e.senders[channel]
e.RwLock.RUnlock()
if s == nil {
logger.Debugf("no sender for channel: %s", channel)
continue
}
logger.Debugf("send event: %s, channel: %s", event.Hash, channel)
for i := 0; i < len(ctx.Users); i++ {
logger.Debug("send event to user: ", ctx.Users[i])
}
s.Send(ctx)
}
// handle event callbacks
sender.SendCallbacks(e.ctx, notifyTarget.ToCallbackList(), event, e.targetCache, e.userCache, e.notifyConfigCache.GetIbex())
// handle global webhooks
sender.SendWebhooks(notifyTarget.ToWebhookList(), event)
// handle plugin call
go sender.MayPluginNotify(e.genNoticeBytes(event), e.notifyConfigCache.GetNotifyScript())
}
type Notice struct {
Event *models.AlertCurEvent `json:"event"`
Tpls map[string]string `json:"tpls"`
}
func (e *Dispatch) genNoticeBytes(event *models.AlertCurEvent) []byte {
// build notice body with templates
ntpls := make(map[string]string)
e.RwLock.RLock()
defer e.RwLock.RUnlock()
for filename, tpl := range e.tpls {
var body bytes.Buffer
if err := tpl.Execute(&body, event); err != nil {
ntpls[filename] = err.Error()
} else {
ntpls[filename] = body.String()
}
}
notice := Notice{Event: event, Tpls: ntpls}
stdinBytes, err := json.Marshal(notice)
if err != nil {
logger.Errorf("event_notify: failed to marshal notice: %v", err)
return nil
}
return stdinBytes
}
// for alerting
func fillUsers(ce *models.AlertCurEvent, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) {
gids := make([]int64, 0, len(ce.NotifyGroupsJSON))
for i := 0; i < len(ce.NotifyGroupsJSON); i++ {
gid, err := strconv.ParseInt(ce.NotifyGroupsJSON[i], 10, 64)
if err != nil {
continue
}
gids = append(gids, gid)
}
ce.NotifyGroupsObj = ugc.GetByUserGroupIds(gids)
uids := make(map[int64]struct{})
for i := 0; i < len(ce.NotifyGroupsObj); i++ {
ug := ce.NotifyGroupsObj[i]
for j := 0; j < len(ug.UserIds); j++ {
uids[ug.UserIds[j]] = struct{}{}
}
}
ce.NotifyUsersObj = uc.GetByUserIds(mapKeys(uids))
}
func mapKeys(m map[int64]struct{}) []int64 {
lst := make([]int64, 0, len(m))
for k := range m {
lst = append(lst, k)
}
return lst
}

View File

@@ -1,32 +0,0 @@
package dispatch
import (
"github.com/ccfos/nightingale/v6/models"
"github.com/toolkits/pkg/logger"
)
func LogEvent(event *models.AlertCurEvent, location string, err ...error) {
status := "triggered"
if event.IsRecovered {
status = "recovered"
}
message := ""
if len(err) > 0 && err[0] != nil {
message = "error_message: " + err[0].Error()
}
logger.Infof(
"event(%s %s) %s: rule_id=%d cluster:%s %v%s@%d %s",
event.Hash,
status,
location,
event.RuleId,
event.Cluster,
event.TagsJSON,
event.TriggerValue,
event.TriggerTime,
message,
)
}

View File

@@ -1,33 +0,0 @@
package dispatch
// NotifyChannels channelKey -> bool
type NotifyChannels map[string]bool
func NewNotifyChannels(channels []string) NotifyChannels {
nc := make(NotifyChannels)
for _, ch := range channels {
nc[ch] = true
}
return nc
}
func (nc NotifyChannels) OrMerge(other NotifyChannels) {
nc.merge(other, func(a, b bool) bool { return a || b })
}
func (nc NotifyChannels) AndMerge(other NotifyChannels) {
nc.merge(other, func(a, b bool) bool { return a && b })
}
func (nc NotifyChannels) merge(other NotifyChannels, f func(bool, bool) bool) {
if other == nil {
return
}
for k, v := range other {
if curV, has := nc[k]; has {
nc[k] = f(curV, v)
} else {
nc[k] = v
}
}
}

View File

@@ -1,134 +0,0 @@
package dispatch
import (
"strconv"
"github.com/ccfos/nightingale/v6/models"
)
// NotifyTarget 维护所有需要发送的目标 用户-通道/回调/钩子信息,用map维护的数据结构具有去重功能
type NotifyTarget struct {
userMap map[int64]NotifyChannels
webhooks map[string]*models.Webhook
callbacks map[string]struct{}
}
func NewNotifyTarget() *NotifyTarget {
return &NotifyTarget{
userMap: make(map[int64]NotifyChannels),
webhooks: make(map[string]*models.Webhook),
callbacks: make(map[string]struct{}),
}
}
// OrMerge 将 channelMap 按照 or 的方式合并,方便实现多种组合的策略,比如根据某个 tag 进行路由等
func (s *NotifyTarget) OrMerge(other *NotifyTarget) {
s.merge(other, NotifyChannels.OrMerge)
}
// AndMerge 将 channelMap 中的 bool 值按照 and 的逻辑进行合并,可以单独将人/通道维度的通知移除
// 常用的场景有:
// 1. 人员离职了不需要发送告警了
// 2. 某个告警通道进行维护,暂时不需要发送告警了
// 3. 业务值班的重定向逻辑,将高等级的告警额外发送给应急人员等
// 可以结合业务需求自己实现router
func (s *NotifyTarget) AndMerge(other *NotifyTarget) {
s.merge(other, NotifyChannels.AndMerge)
}
func (s *NotifyTarget) merge(other *NotifyTarget, f func(NotifyChannels, NotifyChannels)) {
if other == nil {
return
}
for k, v := range other.userMap {
if curV, has := s.userMap[k]; has {
f(curV, v)
} else {
s.userMap[k] = v
}
}
for k, v := range other.webhooks {
s.webhooks[k] = v
}
for k, v := range other.callbacks {
s.callbacks[k] = v
}
}
// ToChannelUserMap userMap(map[uid][channel]bool) 转换为 map[channel][]uid 的结构
func (s *NotifyTarget) ToChannelUserMap() map[string][]int64 {
m := make(map[string][]int64)
for uid, nc := range s.userMap {
for ch, send := range nc {
if send {
m[ch] = append(m[ch], uid)
}
}
}
return m
}
func (s *NotifyTarget) ToCallbackList() []string {
callbacks := make([]string, 0, len(s.callbacks))
for cb := range s.callbacks {
callbacks = append(callbacks, cb)
}
return callbacks
}
func (s *NotifyTarget) ToWebhookList() []*models.Webhook {
webhooks := make([]*models.Webhook, 0, len(s.webhooks))
for _, wh := range s.webhooks {
webhooks = append(webhooks, wh)
}
return webhooks
}
// Dispatch 抽象由告警事件到信息接收者的路由策略
// rule: 告警规则
// event: 告警事件
// prev: 前一次路由结果, Dispatch 的实现可以直接修改 prev, 也可以返回一个新的 NotifyTarget 用于 AndMerge/OrMerge
type NotifyTargetDispatch func(rule *models.AlertRule, event *models.AlertCurEvent, prev *NotifyTarget, dispatch *Dispatch) *NotifyTarget
// GroupDispatch 处理告警规则的组订阅关系
func NotifyGroupDispatch(rule *models.AlertRule, event *models.AlertCurEvent, prev *NotifyTarget, dispatch *Dispatch) *NotifyTarget {
groupIds := make([]int64, 0, len(event.NotifyGroupsJSON))
for _, groupId := range event.NotifyGroupsJSON {
gid, err := strconv.ParseInt(groupId, 10, 64)
if err != nil {
continue
}
groupIds = append(groupIds, gid)
}
groups := dispatch.userGroupCache.GetByUserGroupIds(groupIds)
NotifyTarget := NewNotifyTarget()
for _, group := range groups {
for _, userId := range group.UserIds {
NotifyTarget.userMap[userId] = NewNotifyChannels(event.NotifyChannelsJSON)
}
}
return NotifyTarget
}
func GlobalWebhookDispatch(rule *models.AlertRule, event *models.AlertCurEvent, prev *NotifyTarget, dispatch *Dispatch) *NotifyTarget {
webhooks := dispatch.notifyConfigCache.GetWebhooks()
NotifyTarget := NewNotifyTarget()
for _, webhook := range webhooks {
if !webhook.Enable {
continue
}
NotifyTarget.webhooks[webhook.Url] = webhook
}
return NotifyTarget
}
func EventCallbacksDispatch(rule *models.AlertRule, event *models.AlertCurEvent, prev *NotifyTarget, dispatch *Dispatch) *NotifyTarget {
for _, c := range event.CallbacksJSON {
if c == "" {
continue
}
prev.callbacks[c] = struct{}{}
}
return nil
}

View File

@@ -1,172 +0,0 @@
package eval
import (
"context"
"fmt"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/naming"
"github.com/ccfos/nightingale/v6/alert/process"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/prom"
"github.com/toolkits/pkg/logger"
)
type Scheduler struct {
// key: hash
alertRules map[string]*AlertRuleWorker
ExternalProcessors *process.ExternalProcessorsType
aconf aconf.Alert
alertRuleCache *memsto.AlertRuleCacheType
targetCache *memsto.TargetCacheType
busiGroupCache *memsto.BusiGroupCacheType
alertMuteCache *memsto.AlertMuteCacheType
datasourceCache *memsto.DatasourceCacheType
promClients *prom.PromClientMap
naming *naming.Naming
ctx *ctx.Context
stats *astats.Stats
}
func NewScheduler(aconf aconf.Alert, externalProcessors *process.ExternalProcessorsType, arc *memsto.AlertRuleCacheType, targetCache *memsto.TargetCacheType,
busiGroupCache *memsto.BusiGroupCacheType, alertMuteCache *memsto.AlertMuteCacheType, datasourceCache *memsto.DatasourceCacheType, promClients *prom.PromClientMap, naming *naming.Naming,
ctx *ctx.Context, stats *astats.Stats) *Scheduler {
scheduler := &Scheduler{
aconf: aconf,
alertRules: make(map[string]*AlertRuleWorker),
ExternalProcessors: externalProcessors,
alertRuleCache: arc,
targetCache: targetCache,
busiGroupCache: busiGroupCache,
alertMuteCache: alertMuteCache,
datasourceCache: datasourceCache,
promClients: promClients,
naming: naming,
ctx: ctx,
stats: stats,
}
go scheduler.LoopSyncRules(context.Background())
return scheduler
}
func (s *Scheduler) LoopSyncRules(ctx context.Context) {
time.Sleep(time.Duration(s.aconf.EngineDelay) * time.Second)
duration := 9000 * time.Millisecond
for {
select {
case <-ctx.Done():
return
case <-time.After(duration):
s.syncAlertRules()
}
}
}
func (s *Scheduler) syncAlertRules() {
ids := s.alertRuleCache.GetRuleIds()
alertRuleWorkers := make(map[string]*AlertRuleWorker)
externalRuleWorkers := make(map[string]*process.Processor)
for _, id := range ids {
rule := s.alertRuleCache.Get(id)
if rule == nil {
continue
}
if rule.IsPrometheusRule() {
datasourceIds := s.promClients.Hit(rule.DatasourceIdsJson)
for _, dsId := range datasourceIds {
if !naming.DatasourceHashRing.IsHit(dsId, fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
continue
}
ds := s.datasourceCache.GetById(dsId)
if ds == nil {
logger.Debugf("datasource %d not found", dsId)
continue
}
if ds.Status != "enabled" {
logger.Debugf("datasource %d status is %s", dsId, ds.Status)
continue
}
processor := process.NewProcessor(rule, dsId, s.alertRuleCache, s.targetCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.promClients, s.ctx, s.stats)
alertRule := NewAlertRuleWorker(rule, dsId, processor, s.promClients, s.ctx)
alertRuleWorkers[alertRule.Hash()] = alertRule
}
} else if rule.IsHostRule() && s.ctx.IsCenter {
// all host rule will be processed by center instance
if !naming.DatasourceHashRing.IsHit(naming.HostDatasource, fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
continue
}
processor := process.NewProcessor(rule, 0, s.alertRuleCache, s.targetCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.promClients, s.ctx, s.stats)
alertRule := NewAlertRuleWorker(rule, 0, processor, s.promClients, s.ctx)
alertRuleWorkers[alertRule.Hash()] = alertRule
} else {
// 如果 rule 不是通过 prometheus engine 来告警的,则创建为 externalRule
// if rule is not processed by prometheus engine, create it as externalRule
for _, dsId := range rule.DatasourceIdsJson {
ds := s.datasourceCache.GetById(dsId)
if ds == nil {
logger.Debugf("datasource %d not found", dsId)
continue
}
if ds.Status != "enabled" {
logger.Debugf("datasource %d status is %s", dsId, ds.Status)
continue
}
processor := process.NewProcessor(rule, dsId, s.alertRuleCache, s.targetCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.promClients, s.ctx, s.stats)
externalRuleWorkers[processor.Key()] = processor
}
}
}
for hash, rule := range alertRuleWorkers {
if _, has := s.alertRules[hash]; !has {
rule.Prepare()
rule.Start()
s.alertRules[hash] = rule
}
}
for hash, rule := range s.alertRules {
if _, has := alertRuleWorkers[hash]; !has {
rule.Stop()
delete(s.alertRules, hash)
}
}
s.ExternalProcessors.ExternalLock.Lock()
for key, processor := range externalRuleWorkers {
if curProcessor, has := s.ExternalProcessors.Processors[key]; has {
// rule存在,且hash一致,认为没有变更,这里可以根据需求单独实现一个关联数据更多的hash函数
if processor.Hash() == curProcessor.Hash() {
continue
}
}
// 现有规则中没有rule以及有rule但hash不一致的场景需要触发rule的update
processor.RecoverAlertCurEventFromDb()
s.ExternalProcessors.Processors[key] = processor
}
for key := range s.ExternalProcessors.Processors {
if _, has := externalRuleWorkers[key]; !has {
delete(s.ExternalProcessors.Processors, key)
}
}
s.ExternalProcessors.ExternalLock.Unlock()
}

View File

@@ -1,268 +0,0 @@
package eval
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/ccfos/nightingale/v6/alert/common"
"github.com/ccfos/nightingale/v6/alert/process"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
promsdk "github.com/ccfos/nightingale/v6/pkg/prom"
"github.com/ccfos/nightingale/v6/prom"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/str"
)
type AlertRuleWorker struct {
datasourceId int64
quit chan struct{}
inhibit bool
severity int
rule *models.AlertRule
processor *process.Processor
promClients *prom.PromClientMap
ctx *ctx.Context
}
func NewAlertRuleWorker(rule *models.AlertRule, datasourceId int64, processor *process.Processor, promClients *prom.PromClientMap, ctx *ctx.Context) *AlertRuleWorker {
arw := &AlertRuleWorker{
datasourceId: datasourceId,
quit: make(chan struct{}),
rule: rule,
processor: processor,
promClients: promClients,
ctx: ctx,
}
return arw
}
func (arw *AlertRuleWorker) Key() string {
return common.RuleKey(arw.datasourceId, arw.rule.Id)
}
func (arw *AlertRuleWorker) Hash() string {
return str.MD5(fmt.Sprintf("%d_%d_%s_%d",
arw.rule.Id,
arw.rule.PromEvalInterval,
arw.rule.RuleConfig,
arw.datasourceId,
))
}
func (arw *AlertRuleWorker) Prepare() {
arw.processor.RecoverAlertCurEventFromDb()
}
func (arw *AlertRuleWorker) Start() {
logger.Infof("eval:%s started", arw.Key())
interval := arw.rule.PromEvalInterval
if interval <= 0 {
interval = 10
}
go func() {
for {
select {
case <-arw.quit:
return
default:
arw.Eval()
time.Sleep(time.Duration(interval) * time.Second)
}
}
}()
}
func (arw *AlertRuleWorker) Eval() {
cachedRule := arw.rule
if cachedRule == nil {
//logger.Errorf("rule_eval:%s rule not found", arw.Key())
return
}
typ := cachedRule.GetRuleType()
var lst []common.AnomalyPoint
switch typ {
case models.PROMETHEUS:
lst = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
case models.HOST:
lst = arw.GetHostAnomalyPoint(cachedRule.RuleConfig)
default:
return
}
if arw.processor == nil {
logger.Warningf("rule_eval:%s processor is nil", arw.Key())
return
}
arw.processor.Handle(lst, "inner", arw.inhibit)
}
func (arw *AlertRuleWorker) Stop() {
logger.Infof("rule_eval %s stopped", arw.Key())
close(arw.quit)
}
func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) []common.AnomalyPoint {
var lst []common.AnomalyPoint
var severity int
var rule *models.PromRuleConfig
if err := json.Unmarshal([]byte(ruleConfig), &rule); err != nil {
logger.Errorf("rule_eval:%s rule_config:%s, error:%v", arw.Key(), ruleConfig, err)
return lst
}
if rule == nil {
logger.Errorf("rule_eval:%s rule_config:%s, error:rule is nil", arw.Key(), ruleConfig)
return lst
}
arw.inhibit = rule.Inhibit
for _, query := range rule.Queries {
if query.Severity < severity {
arw.severity = query.Severity
}
promql := strings.TrimSpace(query.PromQl)
if promql == "" {
logger.Errorf("rule_eval:%s promql is blank", arw.Key())
continue
}
if arw.promClients.IsNil(arw.datasourceId) {
logger.Errorf("rule_eval:%s error reader client is nil", arw.Key())
continue
}
readerClient := arw.promClients.GetCli(arw.datasourceId)
var warnings promsdk.Warnings
value, warnings, err := readerClient.Query(context.Background(), promql, time.Now())
if err != nil {
logger.Errorf("rule_eval:%s promql:%s, error:%v", arw.Key(), promql, err)
continue
}
if len(warnings) > 0 {
logger.Errorf("rule_eval:%s promql:%s, warnings:%v", arw.Key(), promql, warnings)
continue
}
logger.Debugf("rule_eval:%s query:%+v, value:%v", arw.Key(), query, value)
points := common.ConvertAnomalyPoints(value)
for i := 0; i < len(points); i++ {
points[i].Severity = query.Severity
}
lst = append(lst, points...)
}
return lst
}
func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.AnomalyPoint {
var lst []common.AnomalyPoint
var severity int
var rule *models.HostRuleConfig
if err := json.Unmarshal([]byte(ruleConfig), &rule); err != nil {
logger.Errorf("rule_eval:%s rule_config:%s, error:%v", arw.Key(), ruleConfig, err)
return lst
}
if rule == nil {
logger.Errorf("rule_eval:%s rule_config:%s, error:rule is nil", arw.Key(), ruleConfig)
return lst
}
arw.inhibit = rule.Inhibit
now := time.Now().Unix()
for _, trigger := range rule.Triggers {
if trigger.Severity < severity {
arw.severity = trigger.Severity
}
query := models.GetHostsQuery(rule.Queries)
switch trigger.Type {
case "target_miss":
t := now - int64(trigger.Duration)
targets, err := models.MissTargetGetsByFilter(arw.ctx, query, t)
if err != nil {
logger.Errorf("rule_eval:%s query:%v, error:%v", arw.Key(), query, err)
continue
}
for _, target := range targets {
m := make(map[string]string)
target.FillTagsMap()
for k, v := range target.TagsMap {
m[k] = v
}
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":
targets, err := models.TargetGetsByFilter(arw.ctx, query, 0, 0)
if err != nil {
logger.Errorf("rule_eval:%s query:%v, error:%v", arw.Key(), query, err)
continue
}
var targetMap = make(map[string]*models.Target)
for _, target := range targets {
targetMap[target.Ident] = target
}
hostOffsetMap := arw.processor.TargetCache.GetOffsetHost(targets, now, int64(trigger.Duration))
for host, offset := range hostOffsetMap {
m := make(map[string]string)
target, exists := targetMap[host]
if exists {
target.FillTagsMap()
for k, v := range target.TagsMap {
m[k] = v
}
}
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":
t := now - int64(trigger.Duration)
count, err := models.MissTargetCountByFilter(arw.ctx, query, t)
if err != nil {
logger.Errorf("rule_eval:%s query:%v, error:%v", arw.Key(), query, err)
continue
}
total, err := models.TargetCountByFilter(arw.ctx, query)
if err != nil {
logger.Errorf("rule_eval:%s query:%v, error:%v", arw.Key(), query, err)
continue
}
pct := float64(count) / float64(total) * 100
if pct >= float64(trigger.Percent) {
lst = append(lst, common.NewAnomalyPoint(trigger.Type, nil, now, pct, trigger.Severity))
}
}
}
return lst
}

View File

@@ -1,178 +0,0 @@
package mute
import (
"strconv"
"strings"
"time"
"github.com/ccfos/nightingale/v6/alert/common"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/toolkits/pkg/logger"
)
func IsMuted(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, alertMuteCache *memsto.AlertMuteCacheType) bool {
if TimeNonEffectiveMuteStrategy(rule, event) {
return true
}
if IdentNotExistsMuteStrategy(rule, event, targetCache) {
return true
}
if BgNotMatchMuteStrategy(rule, event, targetCache) {
return true
}
if EventMuteStrategy(event, alertMuteCache) {
return true
}
return false
}
// TimeNonEffectiveMuteStrategy 根据规则配置的告警时间过滤,如果产生的告警不在规则配置的告警时间内,则不告警
func TimeNonEffectiveMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent) bool {
if rule.Disabled == 1 {
return true
}
tm := time.Unix(event.TriggerTime, 0)
triggerTime := tm.Format("15:04")
triggerWeek := strconv.Itoa(int(tm.Weekday()))
enableStime := strings.Fields(rule.EnableStime)
enableEtime := strings.Fields(rule.EnableEtime)
enableDaysOfWeek := strings.Split(rule.EnableDaysOfWeek, ";")
length := len(enableDaysOfWeek)
// enableStime,enableEtime,enableDaysOfWeek三者长度肯定相同这里循环一个即可
for i := 0; i < length; i++ {
enableDaysOfWeek[i] = strings.Replace(enableDaysOfWeek[i], "7", "0", 1)
if !strings.Contains(enableDaysOfWeek[i], triggerWeek) {
continue
}
if enableStime[i] <= enableEtime[i] {
if triggerTime < enableStime[i] || triggerTime > enableEtime[i] {
continue
}
} else {
if triggerTime < enableStime[i] && triggerTime > enableEtime[i] {
continue
}
}
// 到这里说明当前时刻在告警规则的某组生效时间范围内,直接返回 false
return false
}
return true
}
// IdentNotExistsMuteStrategy 根据ident是否存在过滤,如果ident不存在,则target_up的告警直接过滤掉
func IdentNotExistsMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType) bool {
ident, has := event.TagsMap["ident"]
if !has {
return false
}
_, exists := targetCache.Get(ident)
// 如果是target_up的告警,且ident已经不存在了,直接过滤掉
// 这里的判断有点太粗暴了,但是目前没有更好的办法
if !exists && strings.Contains(rule.PromQl, "target_up") {
logger.Debugf("[%s] mute: rule_eval:%d cluster:%s ident:%s", "IdentNotExistsMuteStrategy", rule.Id, event.Cluster, ident)
return true
}
return false
}
// BgNotMatchMuteStrategy 当规则开启只在bg内部告警时,对于非bg内部的机器过滤
func BgNotMatchMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType) bool {
// 没有开启BG内部告警,直接不过滤
if rule.EnableInBG == 0 {
return false
}
ident, has := event.TagsMap["ident"]
if !has {
return false
}
target, exists := targetCache.Get(ident)
// 对于包含ident的告警事件check一下ident所属bg和rule所属bg是否相同
// 如果告警规则选择了只在本BG生效那其他BG的机器就不能因此规则产生告警
if exists && target.GroupId != rule.GroupId {
logger.Debugf("[%s] mute: rule_eval:%d cluster:%s", "BgNotMatchMuteStrategy", rule.Id, event.Cluster)
return true
}
return false
}
func EventMuteStrategy(event *models.AlertCurEvent, alertMuteCache *memsto.AlertMuteCacheType) bool {
mutes, has := alertMuteCache.Gets(event.GroupId)
if !has || len(mutes) == 0 {
return false
}
for i := 0; i < len(mutes); i++ {
if matchMute(event, mutes[i]) {
return true
}
}
return false
}
// matchMute 如果传入了clock这个可选参数就表示使用这个clock表示的时间否则就从event的字段中取TriggerTime
func matchMute(event *models.AlertCurEvent, mute *models.AlertMute, clock ...int64) bool {
if mute.Disabled == 1 {
return false
}
ts := event.TriggerTime
if len(clock) > 0 {
ts = clock[0]
}
// 如果不是全局的,判断 匹配的 datasource id
if !(len(mute.DatasourceIdsJson) != 0 && mute.DatasourceIdsJson[0] == 0) && event.DatasourceId != 0 {
idm := make(map[int64]struct{}, len(mute.DatasourceIdsJson))
for i := 0; i < len(mute.DatasourceIdsJson); i++ {
idm[mute.DatasourceIdsJson[i]] = struct{}{}
}
// 判断 event.datasourceId 是否包含在 idm 中
if _, has := idm[event.DatasourceId]; !has {
return false
}
}
var matchTime bool
if mute.MuteTimeType == models.TimeRange {
if ts < mute.Btime || ts > mute.Etime {
return false
}
matchTime = true
} else if mute.MuteTimeType == models.Periodic {
tm := time.Unix(event.TriggerTime, 0)
triggerTime := tm.Format("15:04")
triggerWeek := strconv.Itoa(int(tm.Weekday()))
for i := 0; i < len(mute.PeriodicMutesJson); i++ {
if strings.Contains(mute.PeriodicMutesJson[i].EnableDaysOfWeek, triggerWeek) {
if mute.PeriodicMutesJson[i].EnableStime <= mute.PeriodicMutesJson[i].EnableEtime {
if triggerTime >= mute.PeriodicMutesJson[i].EnableStime && triggerTime < mute.PeriodicMutesJson[i].EnableEtime {
matchTime = true
break
}
} else {
if triggerTime < mute.PeriodicMutesJson[i].EnableStime || triggerTime >= mute.PeriodicMutesJson[i].EnableEtime {
matchTime = true
break
}
}
}
}
}
if !matchTime {
return false
}
return common.MatchTags(event.TagsMap, mute.ITags)
}

View File

@@ -1,70 +0,0 @@
package naming
import (
"errors"
"sync"
"github.com/toolkits/pkg/consistent"
"github.com/toolkits/pkg/logger"
)
const NodeReplicas = 500
type DatasourceHashRingType struct {
sync.RWMutex
Rings map[int64]*consistent.Consistent
}
// for alert_rule sharding
var HostDatasource int64 = 100000
var DatasourceHashRing = DatasourceHashRingType{Rings: make(map[int64]*consistent.Consistent)}
func NewConsistentHashRing(replicas int32, nodes []string) *consistent.Consistent {
ret := consistent.New()
ret.NumberOfReplicas = int(replicas)
for i := 0; i < len(nodes); i++ {
ret.Add(nodes[i])
}
return ret
}
func RebuildConsistentHashRing(datasourceId int64, nodes []string) {
r := consistent.New()
r.NumberOfReplicas = NodeReplicas
for i := 0; i < len(nodes); i++ {
r.Add(nodes[i])
}
DatasourceHashRing.Set(datasourceId, r)
logger.Infof("hash ring %d rebuild %+v", datasourceId, r.Members())
}
func (chr *DatasourceHashRingType) GetNode(datasourceId int64, pk string) (string, error) {
chr.Lock()
defer chr.Unlock()
_, exists := chr.Rings[datasourceId]
if !exists {
chr.Rings[datasourceId] = NewConsistentHashRing(int32(NodeReplicas), []string{})
}
return chr.Rings[datasourceId].Get(pk)
}
func (chr *DatasourceHashRingType) IsHit(datasourceId int64, pk string, currentNode string) bool {
node, err := chr.GetNode(datasourceId, pk)
if err != nil {
if errors.Is(err, consistent.ErrEmptyCircle) {
logger.Debugf("rule id:%s is not work, datasource id:%d is not assigned to active alert engine", pk, datasourceId)
} else {
logger.Debugf("rule id:%s is not work, datasource id:%d failed to get node from hashring:%v", pk, datasourceId, err)
}
return false
}
return node == currentNode
}
func (chr *DatasourceHashRingType) Set(datasourceId int64, r *consistent.Consistent) {
chr.Lock()
defer chr.Unlock()
chr.Rings[datasourceId] = r
}

View File

@@ -1,159 +0,0 @@
package naming
import (
"fmt"
"sort"
"strings"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/toolkits/pkg/logger"
)
type Naming struct {
ctx *ctx.Context
heartbeatConfig aconf.HeartbeatConfig
}
func NewNaming(ctx *ctx.Context, heartbeat aconf.HeartbeatConfig) *Naming {
naming := &Naming{
ctx: ctx,
heartbeatConfig: heartbeat,
}
naming.Heartbeats()
return naming
}
// local servers
var localss map[int64]string
func (n *Naming) Heartbeats() error {
localss = make(map[int64]string)
if err := n.heartbeat(); err != nil {
fmt.Println("failed to heartbeat:", err)
return err
}
go n.loopHeartbeat()
go n.loopDeleteInactiveInstances()
return nil
}
func (n *Naming) loopDeleteInactiveInstances() {
if !n.ctx.IsCenter {
return
}
interval := time.Duration(10) * time.Minute
for {
time.Sleep(interval)
n.DeleteInactiveInstances()
}
}
func (n *Naming) DeleteInactiveInstances() {
err := models.DB(n.ctx).Where("clock < ?", time.Now().Unix()-600).Delete(new(models.AlertingEngines)).Error
if err != nil {
logger.Errorf("delete inactive instances err:%v", err)
}
}
func (n *Naming) loopHeartbeat() {
interval := time.Duration(n.heartbeatConfig.Interval) * time.Millisecond
for {
time.Sleep(interval)
if err := n.heartbeat(); err != nil {
logger.Warning(err)
}
}
}
func (n *Naming) heartbeat() error {
var datasourceIds []int64
var err error
// 在页面上维护实例和集群的对应关系
datasourceIds, err = models.GetDatasourceIdsByEngineName(n.ctx, n.heartbeatConfig.EngineName)
if err != nil {
return err
}
if len(datasourceIds) == 0 {
err := models.AlertingEngineHeartbeatWithCluster(n.ctx, n.heartbeatConfig.Endpoint, n.heartbeatConfig.EngineName, 0)
if err != nil {
logger.Warningf("heartbeat with cluster %s err:%v", "", err)
}
} else {
for i := 0; i < len(datasourceIds); i++ {
err := models.AlertingEngineHeartbeatWithCluster(n.ctx, n.heartbeatConfig.Endpoint, n.heartbeatConfig.EngineName, datasourceIds[i])
if err != nil {
logger.Warningf("heartbeat with cluster %d err:%v", datasourceIds[i], err)
}
}
}
for i := 0; i < len(datasourceIds); i++ {
servers, err := n.ActiveServers(datasourceIds[i])
if err != nil {
logger.Warningf("hearbeat %d get active server err:%v", datasourceIds[i], err)
continue
}
sort.Strings(servers)
newss := strings.Join(servers, " ")
oldss, exists := localss[datasourceIds[i]]
if exists && oldss == newss {
continue
}
RebuildConsistentHashRing(datasourceIds[i], servers)
localss[datasourceIds[i]] = newss
}
if n.ctx.IsCenter {
// 如果是中心节点,还需要处理 host 类型的告警规则host 类型告警规则,和数据源无关,想复用下数据源的 hash ring想用一个虚假的数据源 id 来处理
// if is center node, we need to handle host type alerting rules, host type alerting rules are not related to datasource, we want to reuse the hash ring of datasource, we want to use a fake datasource id to handle it
err := models.AlertingEngineHeartbeatWithCluster(n.ctx, n.heartbeatConfig.Endpoint, n.heartbeatConfig.EngineName, HostDatasource)
if err != nil {
logger.Warningf("heartbeat with cluster %s err:%v", "", err)
}
servers, err := n.ActiveServers(HostDatasource)
if err != nil {
logger.Warningf("hearbeat %d get active server err:%v", HostDatasource, err)
return nil
}
sort.Strings(servers)
newss := strings.Join(servers, " ")
oldss, exists := localss[HostDatasource]
if exists && oldss == newss {
return nil
}
RebuildConsistentHashRing(HostDatasource, servers)
localss[HostDatasource] = newss
}
return nil
}
func (n *Naming) ActiveServers(datasourceId int64) ([]string, error) {
if datasourceId == -1 {
return nil, fmt.Errorf("cluster is empty")
}
if !n.ctx.IsCenter {
lst, err := poster.GetByUrls[[]string](n.ctx, "/v1/n9e/servers-active?dsid="+fmt.Sprintf("%d", datasourceId))
return lst, err
}
// 30秒内有心跳就认为是活的
return models.AlertingEngineGetsInstances(n.ctx, "datasource_id = ? and clock > ?", datasourceId, time.Now().Unix()-30)
}

View File

@@ -1,74 +0,0 @@
package process
import (
"sync"
"github.com/ccfos/nightingale/v6/models"
)
type AlertCurEventMap struct {
sync.RWMutex
Data map[string]*models.AlertCurEvent
}
func NewAlertCurEventMap(data map[string]*models.AlertCurEvent) *AlertCurEventMap {
if data == nil {
return &AlertCurEventMap{
Data: make(map[string]*models.AlertCurEvent),
}
}
return &AlertCurEventMap{
Data: data,
}
}
func (a *AlertCurEventMap) SetAll(data map[string]*models.AlertCurEvent) {
a.Lock()
defer a.Unlock()
a.Data = data
}
func (a *AlertCurEventMap) Set(key string, value *models.AlertCurEvent) {
a.Lock()
defer a.Unlock()
a.Data[key] = value
}
func (a *AlertCurEventMap) Get(key string) (*models.AlertCurEvent, bool) {
a.RLock()
defer a.RUnlock()
event, exists := a.Data[key]
return event, exists
}
func (a *AlertCurEventMap) UpdateLastEvalTime(key string, lastEvalTime int64) {
a.Lock()
defer a.Unlock()
event, exists := a.Data[key]
if !exists {
return
}
event.LastEvalTime = lastEvalTime
}
func (a *AlertCurEventMap) Delete(key string) {
a.Lock()
defer a.Unlock()
delete(a.Data, key)
}
func (a *AlertCurEventMap) Keys() []string {
a.RLock()
defer a.RUnlock()
keys := make([]string, 0, len(a.Data))
for k := range a.Data {
keys = append(keys, k)
}
return keys
}
func (a *AlertCurEventMap) GetAll() map[string]*models.AlertCurEvent {
a.RLock()
defer a.RUnlock()
return a.Data
}

View File

@@ -1,440 +0,0 @@
package process
import (
"bytes"
"fmt"
"html/template"
"sort"
"strings"
"sync"
"time"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/common"
"github.com/ccfos/nightingale/v6/alert/dispatch"
"github.com/ccfos/nightingale/v6/alert/mute"
"github.com/ccfos/nightingale/v6/alert/queue"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/tplx"
"github.com/ccfos/nightingale/v6/prom"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/str"
)
type ExternalProcessorsType struct {
ExternalLock sync.RWMutex
Processors map[string]*Processor
}
var ExternalProcessors ExternalProcessorsType
func NewExternalProcessors() *ExternalProcessorsType {
return &ExternalProcessorsType{
Processors: make(map[string]*Processor),
}
}
func (e *ExternalProcessorsType) GetExternalAlertRule(datasourceId, id int64) (*Processor, bool) {
e.ExternalLock.RLock()
defer e.ExternalLock.RUnlock()
processor, has := e.Processors[common.RuleKey(datasourceId, id)]
return processor, has
}
type Processor struct {
datasourceId int64
rule *models.AlertRule
fires *AlertCurEventMap
pendings *AlertCurEventMap
inhibit bool
tagsMap map[string]string
tagsArr []string
target string
targetNote string
groupName string
atertRuleCache *memsto.AlertRuleCacheType
TargetCache *memsto.TargetCacheType
BusiGroupCache *memsto.BusiGroupCacheType
alertMuteCache *memsto.AlertMuteCacheType
datasourceCache *memsto.DatasourceCacheType
promClients *prom.PromClientMap
ctx *ctx.Context
stats *astats.Stats
}
func (p *Processor) Key() string {
return common.RuleKey(p.datasourceId, p.rule.Id)
}
func (p *Processor) DatasourceId() int64 {
return p.datasourceId
}
func (p *Processor) Hash() string {
return str.MD5(fmt.Sprintf("%d_%d_%s_%d",
p.rule.Id,
p.rule.PromEvalInterval,
p.rule.RuleConfig,
p.datasourceId,
))
}
func NewProcessor(rule *models.AlertRule, datasourceId int64, atertRuleCache *memsto.AlertRuleCacheType, targetCache *memsto.TargetCacheType,
busiGroupCache *memsto.BusiGroupCacheType, alertMuteCache *memsto.AlertMuteCacheType, datasourceCache *memsto.DatasourceCacheType, promClients *prom.PromClientMap, ctx *ctx.Context,
stats *astats.Stats) *Processor {
p := &Processor{
datasourceId: datasourceId,
rule: rule,
TargetCache: targetCache,
BusiGroupCache: busiGroupCache,
alertMuteCache: alertMuteCache,
atertRuleCache: atertRuleCache,
datasourceCache: datasourceCache,
promClients: promClients,
ctx: ctx,
stats: stats,
}
p.mayHandleGroup()
return p
}
func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inhibit bool) {
// 有可能rule的一些配置已经发生变化比如告警接收人、callbacks等
// 这些信息的修改是不会引起worker restart的但是确实会影响告警处理逻辑
// 所以这里直接从memsto.AlertRuleCache中获取并覆盖
p.inhibit = inhibit
cachedRule := p.atertRuleCache.Get(p.rule.Id)
if cachedRule == nil {
logger.Errorf("rule not found %+v", anomalyPoints)
return
}
p.rule = cachedRule
now := time.Now().Unix()
alertingKeys := map[string]struct{}{}
// 根据 event 的 tag 将 events 分组,处理告警抑制的情况
eventsMap := make(map[string][]*models.AlertCurEvent)
for _, anomalyPoint := range anomalyPoints {
event := p.BuildEvent(anomalyPoint, from, now)
// 如果 event 被 mute 了,本质也是 fire 的状态,这里无论如何都添加到 alertingKeys 中,防止 fire 的事件自动恢复了
hash := event.Hash
alertingKeys[hash] = struct{}{}
if mute.IsMuted(cachedRule, event, p.TargetCache, p.alertMuteCache) {
logger.Debugf("rule_eval:%s event:%v is muted", p.Key(), event)
continue
}
tagHash := TagHash(anomalyPoint)
eventsMap[tagHash] = append(eventsMap[tagHash], event)
}
for _, events := range eventsMap {
p.handleEvent(events)
}
p.HandleRecover(alertingKeys, now)
}
func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, now int64) *models.AlertCurEvent {
p.fillTags(anomalyPoint)
p.mayHandleIdent()
hash := Hash(p.rule.Id, p.datasourceId, anomalyPoint)
ds := p.datasourceCache.GetById(p.datasourceId)
var dsName string
if ds != nil {
dsName = ds.Name
}
event := p.rule.GenerateNewEvent(p.ctx)
event.TriggerTime = anomalyPoint.Timestamp
event.TagsMap = p.tagsMap
event.DatasourceId = p.datasourceId
event.Cluster = dsName
event.Hash = hash
event.TargetIdent = p.target
event.TargetNote = p.targetNote
event.TriggerValue = anomalyPoint.ReadableValue()
event.TagsJSON = p.tagsArr
event.GroupName = p.groupName
event.Tags = strings.Join(p.tagsArr, ",,")
event.IsRecovered = false
event.Callbacks = p.rule.Callbacks
event.CallbacksJSON = p.rule.CallbacksJSON
event.Annotations = p.rule.Annotations
event.AnnotationsJSON = make(map[string]string)
event.RuleConfig = p.rule.RuleConfig
event.RuleConfigJson = p.rule.RuleConfigJson
event.Severity = anomalyPoint.Severity
if from == "inner" {
event.LastEvalTime = now
} else {
event.LastEvalTime = event.TriggerTime
}
return event
}
func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64) {
for _, hash := range p.pendings.Keys() {
if _, has := alertingKeys[hash]; has {
continue
}
p.pendings.Delete(hash)
}
for hash := range p.fires.GetAll() {
if _, has := alertingKeys[hash]; has {
continue
}
p.RecoverSingle(hash, now, nil)
}
}
func (p *Processor) RecoverSingle(hash string, now int64, value *string) {
cachedRule := p.rule
if cachedRule == nil {
return
}
event, has := p.fires.Get(hash)
if !has {
return
}
// 如果配置了留观时长,就不能立马恢复了
if cachedRule.RecoverDuration > 0 && now-event.LastEvalTime < cachedRule.RecoverDuration {
logger.Debugf("rule_eval:%s event:%v not recover", p.Key(), event)
return
}
if value != nil {
event.TriggerValue = *value
}
// 没查到触发阈值的vector姑且就认为这个vector的值恢复了
// 我确实无法分辨是prom中有值但是未满足阈值所以没返回还是prom中确实丢了一些点导致没有数据可以返回尴尬
p.fires.Delete(hash)
p.pendings.Delete(hash)
// 可能是因为调整了promql才恢复的所以事件里边要体现最新的promql否则用户会比较困惑
// 当然其实rule的各个字段都可能发生变化了都更新一下吧
cachedRule.UpdateEvent(event)
event.IsRecovered = true
event.LastEvalTime = now
p.pushEventToQueue(event)
}
func (p *Processor) handleEvent(events []*models.AlertCurEvent) {
var fireEvents []*models.AlertCurEvent
// severity 初始为 4, 一定为遇到比自己优先级高的事件
severity := 4
for _, event := range events {
if event == nil {
continue
}
if p.rule.PromForDuration == 0 {
fireEvents = append(fireEvents, event)
if severity > event.Severity {
severity = event.Severity
}
continue
}
var preTriggerTime int64
preEvent, has := p.pendings.Get(event.Hash)
if has {
p.pendings.UpdateLastEvalTime(event.Hash, event.LastEvalTime)
preTriggerTime = preEvent.TriggerTime
} else {
p.pendings.Set(event.Hash, event)
preTriggerTime = event.TriggerTime
}
if event.LastEvalTime-preTriggerTime+int64(event.PromEvalInterval) >= int64(p.rule.PromForDuration) {
fireEvents = append(fireEvents, event)
if severity > event.Severity {
severity = event.Severity
}
continue
}
}
p.inhibitEvent(fireEvents, severity)
}
func (p *Processor) inhibitEvent(events []*models.AlertCurEvent, highSeverity int) {
for _, event := range events {
if p.inhibit && event.Severity > highSeverity {
logger.Debugf("rule_eval:%s event:%+v inhibit highSeverity:%d", p.Key(), event, highSeverity)
continue
}
p.fireEvent(event)
}
}
func (p *Processor) fireEvent(event *models.AlertCurEvent) {
// As p.rule maybe outdated, use rule from cache
cachedRule := p.rule
if cachedRule == nil {
return
}
logger.Debugf("rule_eval:%s event:%+v fire", p.Key(), event)
if fired, has := p.fires.Get(event.Hash); has {
p.fires.UpdateLastEvalTime(event.Hash, event.LastEvalTime)
if cachedRule.NotifyRepeatStep == 0 {
logger.Debugf("rule_eval:%s event:%+v repeat is zero nothing to do", p.Key(), event)
// 说明不想重复通知那就直接返回了nothing to do
// do not need to send alert again
return
}
// 之前发送过告警了,这次是否要继续发送,要看是否过了通道静默时间
if event.LastEvalTime > fired.LastSentTime+int64(cachedRule.NotifyRepeatStep)*60 {
if cachedRule.NotifyMaxNumber == 0 {
// 最大可以发送次数如果是0表示不想限制最大发送次数一直发即可
event.NotifyCurNumber = fired.NotifyCurNumber + 1
event.FirstTriggerTime = fired.FirstTriggerTime
p.pushEventToQueue(event)
} else {
// 有最大发送次数的限制,就要看已经发了几次了,是否达到了最大发送次数
if fired.NotifyCurNumber >= cachedRule.NotifyMaxNumber {
logger.Debugf("rule_eval:%s event:%+v reach max number", p.Key(), event)
return
} else {
event.NotifyCurNumber = fired.NotifyCurNumber + 1
event.FirstTriggerTime = fired.FirstTriggerTime
p.pushEventToQueue(event)
}
}
}
} else {
event.NotifyCurNumber = 1
event.FirstTriggerTime = event.TriggerTime
p.pushEventToQueue(event)
}
}
func (p *Processor) pushEventToQueue(e *models.AlertCurEvent) {
if !e.IsRecovered {
e.LastSentTime = e.LastEvalTime
p.fires.Set(e.Hash, e)
}
p.stats.CounterAlertsTotal.WithLabelValues(fmt.Sprintf("%d", e.DatasourceId)).Inc()
dispatch.LogEvent(e, "push_queue")
if !queue.EventQueue.PushFront(e) {
logger.Warningf("event_push_queue: queue is full, event:%+v", e)
}
}
func (p *Processor) RecoverAlertCurEventFromDb() {
p.pendings = NewAlertCurEventMap(nil)
curEvents, err := models.AlertCurEventGetByRuleIdAndDsId(p.ctx, p.rule.Id, p.datasourceId)
if err != nil {
logger.Errorf("recover event from db for rule:%s failed, err:%s", p.Key(), err)
p.fires = NewAlertCurEventMap(nil)
return
}
fireMap := make(map[string]*models.AlertCurEvent)
for _, event := range curEvents {
event.DB2Mem()
fireMap[event.Hash] = event
}
p.fires = NewAlertCurEventMap(fireMap)
}
func (p *Processor) fillTags(anomalyPoint common.AnomalyPoint) {
// handle series tags
tagsMap := make(map[string]string)
for label, value := range anomalyPoint.Labels {
tagsMap[string(label)] = string(value)
}
var e = &models.AlertCurEvent{
TagsMap: tagsMap,
}
// handle rule tags
for _, tag := range p.rule.AppendTagsJSON {
arr := strings.SplitN(tag, "=", 2)
var defs = []string{
"{{$labels := .TagsMap}}",
"{{$value := .TriggerValue}}",
}
tagValue := arr[1]
text := strings.Join(append(defs, tagValue), "")
t, err := template.New(fmt.Sprint(p.rule.Id)).Funcs(template.FuncMap(tplx.TemplateFuncMap)).Parse(text)
if err != nil {
tagValue = fmt.Sprintf("parse tag value failed, err:%s", err)
tagsMap[arr[0]] = tagValue
continue
}
var body bytes.Buffer
err = t.Execute(&body, e)
if err != nil {
tagValue = fmt.Sprintf("parse tag value failed, err:%s", err)
tagsMap[arr[0]] = tagValue
continue
}
tagsMap[arr[0]] = body.String()
}
tagsMap["rulename"] = p.rule.Name
p.tagsMap = tagsMap
// handle tagsArr
p.tagsArr = labelMapToArr(tagsMap)
}
func (p *Processor) mayHandleIdent() {
// handle ident
if ident, has := p.tagsMap["ident"]; has {
if target, exists := p.TargetCache.Get(ident); exists {
p.target = target.Ident
p.targetNote = target.Note
}
}
}
func (p *Processor) mayHandleGroup() {
// handle bg
bg := p.BusiGroupCache.GetByBusiGroupId(p.rule.GroupId)
if bg != nil {
p.groupName = bg.Name
}
}
func labelMapToArr(m map[string]string) []string {
numLabels := len(m)
labelStrings := make([]string, 0, numLabels)
for label, value := range m {
labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", label, value))
}
if numLabels > 1 {
sort.Strings(labelStrings)
}
return labelStrings
}
func Hash(ruleId, datasourceId int64, vector common.AnomalyPoint) string {
return str.MD5(fmt.Sprintf("%d_%s_%d_%d", ruleId, vector.Labels.String(), datasourceId, vector.Severity))
}
func TagHash(vector common.AnomalyPoint) string {
return str.MD5(vector.Labels.String())
}

View File

@@ -1,18 +0,0 @@
package queue
import (
"time"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/toolkits/pkg/container/list"
)
var EventQueue = list.NewSafeListLimited(10000000)
func ReportQueueSize(stats *astats.Stats) {
for {
time.Sleep(time.Second)
stats.GaugeAlertQueueSize.Set(float64(EventQueue.Len()))
}
}

View File

@@ -1,102 +0,0 @@
package record
import (
"context"
"fmt"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/str"
)
type RecordRuleContext struct {
datasourceId int64
quit chan struct{}
rule *models.RecordingRule
// writers *writer.WritersType
promClients *prom.PromClientMap
}
func NewRecordRuleContext(rule *models.RecordingRule, datasourceId int64, promClients *prom.PromClientMap, writers *writer.WritersType) *RecordRuleContext {
return &RecordRuleContext{
datasourceId: datasourceId,
quit: make(chan struct{}),
rule: rule,
promClients: promClients,
//writers: writers,
}
}
func (rrc *RecordRuleContext) Key() string {
return fmt.Sprintf("record-%d-%d", rrc.datasourceId, rrc.rule.Id)
}
func (rrc *RecordRuleContext) Hash() string {
return str.MD5(fmt.Sprintf("%d_%d_%s_%d",
rrc.rule.Id,
rrc.rule.PromEvalInterval,
rrc.rule.PromQl,
rrc.datasourceId,
))
}
func (rrc *RecordRuleContext) Prepare() {}
func (rrc *RecordRuleContext) Start() {
logger.Infof("eval:%s started", rrc.Key())
interval := rrc.rule.PromEvalInterval
if interval <= 0 {
interval = 10
}
go func() {
for {
select {
case <-rrc.quit:
return
default:
rrc.Eval()
time.Sleep(time.Duration(interval) * time.Second)
}
}
}()
}
func (rrc *RecordRuleContext) Eval() {
promql := strings.TrimSpace(rrc.rule.PromQl)
if promql == "" {
logger.Errorf("eval:%s promql is blank", rrc.Key())
return
}
if rrc.promClients.IsNil(rrc.datasourceId) {
logger.Errorf("eval:%s reader client is nil", rrc.Key())
return
}
value, warnings, err := rrc.promClients.GetCli(rrc.datasourceId).Query(context.Background(), promql, time.Now())
if err != nil {
logger.Errorf("eval:%s promql:%s, error:%v", rrc.Key(), promql, err)
return
}
if len(warnings) > 0 {
logger.Errorf("eval:%s promql:%s, warnings:%v", rrc.Key(), promql, warnings)
return
}
ts := ConvertToTimeSeries(value, rrc.rule)
if len(ts) != 0 {
rrc.promClients.GetWriterCli(rrc.datasourceId).Write(ts)
}
}
func (rrc *RecordRuleContext) Stop() {
logger.Infof("%s stopped", rrc.Key())
close(rrc.quit)
}

View File

@@ -1,122 +0,0 @@
package record
import (
"math"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/prompb"
)
const (
LabelName = "__name__"
)
func ConvertToTimeSeries(value model.Value, rule *models.RecordingRule) (lst []*prompb.TimeSeries) {
switch value.Type() {
case model.ValVector:
items, ok := value.(model.Vector)
if !ok {
return
}
for _, item := range items {
if math.IsNaN(float64(item.Value)) {
continue
}
s := prompb.Sample{}
s.Timestamp = time.Unix(item.Timestamp.Unix(), 0).UnixNano() / 1e6
s.Value = float64(item.Value)
l := labelsToLabelsProto(item.Metric, rule)
lst = append(lst, &prompb.TimeSeries{
Labels: l,
Samples: []prompb.Sample{s},
})
}
case model.ValMatrix:
items, ok := value.(model.Matrix)
if !ok {
return
}
for _, item := range items {
if len(item.Values) == 0 {
return
}
last := item.Values[len(item.Values)-1]
if math.IsNaN(float64(last.Value)) {
continue
}
l := labelsToLabelsProto(item.Metric, rule)
var slst []prompb.Sample
for _, v := range item.Values {
if math.IsNaN(float64(v.Value)) {
continue
}
slst = append(slst, prompb.Sample{
Timestamp: time.Unix(v.Timestamp.Unix(), 0).UnixNano() / 1e6,
Value: float64(v.Value),
})
}
lst = append(lst, &prompb.TimeSeries{
Labels: l,
Samples: slst,
})
}
case model.ValScalar:
item, ok := value.(*model.Scalar)
if !ok {
return
}
if math.IsNaN(float64(item.Value)) {
return
}
lst = append(lst, &prompb.TimeSeries{
Labels: nil,
Samples: []prompb.Sample{{Value: float64(item.Value), Timestamp: time.Unix(item.Timestamp.Unix(), 0).UnixNano() / 1e6}},
})
default:
return
}
return
}
func labelsToLabelsProto(labels model.Metric, rule *models.RecordingRule) (result []*prompb.Label) {
//name
nameLs := &prompb.Label{
Name: LabelName,
Value: rule.Name,
}
result = append(result, nameLs)
for k, v := range labels {
if k == LabelName {
continue
}
if model.LabelNameRE.MatchString(string(k)) {
result = append(result, &prompb.Label{
Name: string(k),
Value: string(v),
})
}
}
if len(rule.AppendTagsJSON) != 0 {
for _, v := range rule.AppendTagsJSON {
index := strings.Index(v, "=")
if model.LabelNameRE.MatchString(v[:index]) {
result = append(result, &prompb.Label{
Name: v[:index],
Value: v[index+1:],
})
}
}
}
return result
}

View File

@@ -1,94 +0,0 @@
package record
import (
"context"
"fmt"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/naming"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/pushgw/writer"
)
type Scheduler struct {
// key: hash
recordRules map[string]*RecordRuleContext
aconf aconf.Alert
recordingRuleCache *memsto.RecordingRuleCacheType
promClients *prom.PromClientMap
writers *writer.WritersType
stats *astats.Stats
}
func NewScheduler(aconf aconf.Alert, rrc *memsto.RecordingRuleCacheType, promClients *prom.PromClientMap, writers *writer.WritersType, stats *astats.Stats) *Scheduler {
scheduler := &Scheduler{
aconf: aconf,
recordRules: make(map[string]*RecordRuleContext),
recordingRuleCache: rrc,
promClients: promClients,
writers: writers,
stats: stats,
}
go scheduler.LoopSyncRules(context.Background())
return scheduler
}
func (s *Scheduler) LoopSyncRules(ctx context.Context) {
time.Sleep(time.Duration(s.aconf.EngineDelay) * time.Second)
duration := 9000 * time.Millisecond
for {
select {
case <-ctx.Done():
return
case <-time.After(duration):
s.syncRecordRules()
}
}
}
func (s *Scheduler) syncRecordRules() {
ids := s.recordingRuleCache.GetRuleIds()
recordRules := make(map[string]*RecordRuleContext)
for _, id := range ids {
rule := s.recordingRuleCache.Get(id)
if rule == nil {
continue
}
datasourceIds := s.promClients.Hit(rule.DatasourceIdsJson)
for _, dsId := range datasourceIds {
if !naming.DatasourceHashRing.IsHit(dsId, fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
continue
}
recordRule := NewRecordRuleContext(rule, dsId, s.promClients, s.writers)
recordRules[recordRule.Hash()] = recordRule
}
}
for hash, rule := range recordRules {
if _, has := s.recordRules[hash]; !has {
rule.Prepare()
rule.Start()
s.recordRules[hash] = rule
}
}
for hash, rule := range s.recordRules {
if _, has := recordRules[hash]; !has {
rule.Stop()
delete(s.recordRules, hash)
}
}
}

View File

@@ -1,79 +0,0 @@
package router
import (
"net/http"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/process"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/gin-gonic/gin"
)
type Router struct {
HTTP httpx.Config
Alert aconf.Alert
AlertMuteCache *memsto.AlertMuteCacheType
TargetCache *memsto.TargetCacheType
BusiGroupCache *memsto.BusiGroupCacheType
AlertStats *astats.Stats
Ctx *ctx.Context
ExternalProcessors *process.ExternalProcessorsType
}
func New(httpConfig httpx.Config, alert aconf.Alert, amc *memsto.AlertMuteCacheType, tc *memsto.TargetCacheType, bgc *memsto.BusiGroupCacheType,
astats *astats.Stats, ctx *ctx.Context, externalProcessors *process.ExternalProcessorsType) *Router {
return &Router{
HTTP: httpConfig,
Alert: alert,
AlertMuteCache: amc,
TargetCache: tc,
BusiGroupCache: bgc,
AlertStats: astats,
Ctx: ctx,
ExternalProcessors: externalProcessors,
}
}
func (rt *Router) Config(r *gin.Engine) {
if !rt.HTTP.APIForService.Enable {
return
}
service := r.Group("/v1/n9e")
if len(rt.HTTP.APIForService.BasicAuth) > 0 {
service.Use(gin.BasicAuth(rt.HTTP.APIForService.BasicAuth))
}
service.POST("/event", rt.pushEventToQueue)
service.POST("/event-persist", rt.eventPersist)
service.POST("/make-event", rt.makeEvent)
}
func Render(c *gin.Context, data, msg interface{}) {
if msg == nil {
if data == nil {
data = struct{}{}
}
c.JSON(http.StatusOK, gin.H{"data": data, "error": ""})
} else {
c.JSON(http.StatusOK, gin.H{"error": gin.H{"message": msg}})
}
}
func Dangerous(c *gin.Context, v interface{}, code ...int) {
if v == nil {
return
}
switch t := v.(type) {
case string:
if t != "" {
c.JSON(http.StatusOK, gin.H{"error": gin.H{"message": v}})
}
case error:
c.JSON(http.StatusOK, gin.H{"error": gin.H{"message": t.Error()}})
}
}

View File

@@ -1,148 +0,0 @@
package router
import (
"fmt"
"strings"
"time"
"github.com/ccfos/nightingale/v6/alert/common"
"github.com/ccfos/nightingale/v6/alert/dispatch"
"github.com/ccfos/nightingale/v6/alert/mute"
"github.com/ccfos/nightingale/v6/alert/naming"
"github.com/ccfos/nightingale/v6/alert/process"
"github.com/ccfos/nightingale/v6/alert/queue"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
func (rt *Router) pushEventToQueue(c *gin.Context) {
var event *models.AlertCurEvent
ginx.BindJSON(c, &event)
if event.RuleId == 0 {
ginx.Bomb(200, "event is illegal")
}
event.TagsMap = make(map[string]string)
for i := 0; i < len(event.TagsJSON); i++ {
pair := strings.TrimSpace(event.TagsJSON[i])
if pair == "" {
continue
}
arr := strings.Split(pair, "=")
if len(arr) != 2 {
continue
}
event.TagsMap[arr[0]] = arr[1]
}
if mute.EventMuteStrategy(event, rt.AlertMuteCache) {
logger.Infof("event_muted: rule_id=%d %s", event.RuleId, event.Hash)
ginx.NewRender(c).Message(nil)
return
}
if err := event.ParseRule("rule_name"); err != nil {
event.RuleName = fmt.Sprintf("failed to parse rule name: %v", err)
}
if err := event.ParseRule("rule_note"); err != nil {
event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
}
if err := event.ParseRule("annotations"); err != nil {
event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
}
// 如果 rule_note 中有 ; 前缀,则使用 rule_note 替换 tags 中的内容
if strings.HasPrefix(event.RuleNote, ";") {
event.RuleNote = strings.TrimPrefix(event.RuleNote, ";")
event.Tags = strings.ReplaceAll(event.RuleNote, " ", ",,")
event.TagsJSON = strings.Split(event.Tags, ",,")
} else {
event.Tags = strings.Join(event.TagsJSON, ",,")
}
event.Callbacks = strings.Join(event.CallbacksJSON, " ")
event.NotifyChannels = strings.Join(event.NotifyChannelsJSON, " ")
event.NotifyGroups = strings.Join(event.NotifyGroupsJSON, " ")
rt.AlertStats.CounterAlertsTotal.WithLabelValues(event.Cluster).Inc()
dispatch.LogEvent(event, "http_push_queue")
if !queue.EventQueue.PushFront(event) {
msg := fmt.Sprintf("event:%+v push_queue err: queue is full", event)
ginx.Bomb(200, msg)
logger.Warningf(msg)
}
ginx.NewRender(c).Message(nil)
}
func (rt *Router) eventPersist(c *gin.Context) {
var event *models.AlertCurEvent
ginx.BindJSON(c, &event)
event.FE2DB()
ginx.NewRender(c).Message(models.EventPersist(rt.Ctx, event))
}
type eventForm struct {
Alert bool `json:"alert"`
AnomalyPoints []common.AnomalyPoint `json:"vectors"`
RuleId int64 `json:"rule_id"`
DatasourceId int64 `json:"datasource_id"`
Inhibit bool `json:"inhibit"`
}
func (rt *Router) makeEvent(c *gin.Context) {
var events []*eventForm
ginx.BindJSON(c, &events)
//now := time.Now().Unix()
for i := 0; i < len(events); i++ {
node, err := naming.DatasourceHashRing.GetNode(events[i].DatasourceId, fmt.Sprintf("%d", events[i].RuleId))
if err != nil {
logger.Warningf("event:%+v get node err:%v", events[i], err)
ginx.Bomb(200, "event node not exists")
}
if node != rt.Alert.Heartbeat.Endpoint {
err := forwardEvent(events[i], node)
if err != nil {
logger.Warningf("event:%+v forward err:%v", events[i], err)
ginx.Bomb(200, "event forward error")
}
continue
}
ruleWorker, exists := rt.ExternalProcessors.GetExternalAlertRule(events[i].DatasourceId, events[i].RuleId)
logger.Debugf("handle event:%+v exists:%v", events[i], exists)
if !exists {
ginx.Bomb(200, "rule not exists")
}
if events[i].Alert {
go ruleWorker.Handle(events[i].AnomalyPoints, "http", events[i].Inhibit)
} 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)
}
}
}
ginx.NewRender(c).Message(nil)
}
// event 不归本实例处理,转发给对应的实例
func forwardEvent(event *eventForm, instance string) error {
ur := fmt.Sprintf("http://%s/v1/n9e/make-event", instance)
res, code, err := poster.PostJSON(ur, time.Second*5, []*eventForm{event}, 3)
if err != nil {
return err
}
logger.Infof("forward event: result=succ url=%s code=%d event:%v response=%s", ur, code, event, string(res))
return nil
}

View File

@@ -1,191 +0,0 @@
package sender
import (
"strconv"
"strings"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/ibex"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/toolkits/pkg/logger"
)
func SendCallbacks(ctx *ctx.Context, urls []string, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType, ibexConf aconf.Ibex) {
for _, url := range urls {
if url == "" {
continue
}
if strings.HasPrefix(url, "${ibex}") {
if !event.IsRecovered {
handleIbex(ctx, url, event, targetCache, userCache, ibexConf)
}
continue
}
if !(strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")) {
url = "http://" + url
}
resp, code, err := poster.PostJSON(url, 5*time.Second, event, 3)
if err != nil {
logger.Errorf("event_callback_fail(rule_id=%d url=%s), resp: %s, err: %v, code: %d", event.RuleId, url, string(resp), err, code)
} else {
logger.Infof("event_callback_succ(rule_id=%d url=%s), resp: %s, code: %d", event.RuleId, url, string(resp), code)
}
}
}
type TaskForm struct {
Title string `json:"title"`
Account string `json:"account"`
Batch int `json:"batch"`
Tolerance int `json:"tolerance"`
Timeout int `json:"timeout"`
Pause string `json:"pause"`
Script string `json:"script"`
Args string `json:"args"`
Action string `json:"action"`
Creator string `json:"creator"`
Hosts []string `json:"hosts"`
}
type TaskCreateReply struct {
Err string `json:"err"`
Dat int64 `json:"dat"` // task.id
}
func handleIbex(ctx *ctx.Context, url string, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType, ibexConf aconf.Ibex) {
arr := strings.Split(url, "/")
var idstr string
var host string
if len(arr) > 1 {
idstr = arr[1]
}
if len(arr) > 2 {
host = arr[2]
}
id, err := strconv.ParseInt(idstr, 10, 64)
if err != nil {
logger.Errorf("event_callback_ibex: failed to parse url: %s", url)
return
}
if host == "" {
// 用户在callback url中没有传入host就从event中解析
host = event.TargetIdent
}
if host == "" {
logger.Error("event_callback_ibex: failed to get host")
return
}
tpl, err := models.TaskTplGet(ctx, "id = ?", id)
if err != nil {
logger.Errorf("event_callback_ibex: failed to get tpl: %v", err)
return
}
if tpl == nil {
logger.Errorf("event_callback_ibex: no such tpl(%d)", id)
return
}
// check perm
// tpl.GroupId - host - account 三元组校验权限
can, err := canDoIbex(ctx, tpl.UpdateBy, tpl, host, targetCache, userCache)
if err != nil {
logger.Errorf("event_callback_ibex: check perm fail: %v", err)
return
}
if !can {
logger.Errorf("event_callback_ibex: user(%s) no permission", tpl.UpdateBy)
return
}
// call ibex
in := TaskForm{
Title: tpl.Title + " FH: " + host,
Account: tpl.Account,
Batch: tpl.Batch,
Tolerance: tpl.Tolerance,
Timeout: tpl.Timeout,
Pause: tpl.Pause,
Script: tpl.Script,
Args: tpl.Args,
Action: "start",
Creator: tpl.UpdateBy,
Hosts: []string{host},
}
var res TaskCreateReply
err = ibex.New(
ibexConf.Address,
ibexConf.BasicAuthUser,
ibexConf.BasicAuthPass,
ibexConf.Timeout,
).
Path("/ibex/v1/tasks").
In(in).
Out(&res).
POST()
if err != nil {
logger.Errorf("event_callback_ibex: call ibex fail: %v", err)
return
}
if res.Err != "" {
logger.Errorf("event_callback_ibex: call ibex response error: %v", res.Err)
return
}
// write db
record := models.TaskRecord{
Id: res.Dat,
EventId: event.Id,
GroupId: tpl.GroupId,
IbexAddress: ibexConf.Address,
IbexAuthUser: ibexConf.BasicAuthUser,
IbexAuthPass: ibexConf.BasicAuthPass,
Title: in.Title,
Account: in.Account,
Batch: in.Batch,
Tolerance: in.Tolerance,
Timeout: in.Timeout,
Pause: in.Pause,
Script: in.Script,
Args: in.Args,
CreateAt: time.Now().Unix(),
CreateBy: in.Creator,
}
if err = record.Add(ctx); err != nil {
logger.Errorf("event_callback_ibex: persist task_record fail: %v", err)
}
}
func canDoIbex(ctx *ctx.Context, username string, tpl *models.TaskTpl, host string, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType) (bool, error) {
user := userCache.GetByUsername(username)
if user != nil && user.IsAdmin() {
return true, nil
}
target, has := targetCache.Get(host)
if !has {
return false, nil
}
return target.GroupId == tpl.GroupId, nil
}

View File

@@ -1,100 +0,0 @@
package sender
import (
"html/template"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/toolkits/pkg/logger"
)
type dingtalkMarkdown struct {
Title string `json:"title"`
Text string `json:"text"`
}
type dingtalkAt struct {
AtMobiles []string `json:"atMobiles"`
IsAtAll bool `json:"isAtAll"`
}
type dingtalk struct {
Msgtype string `json:"msgtype"`
Markdown dingtalkMarkdown `json:"markdown"`
At dingtalkAt `json:"at"`
}
type DingtalkSender struct {
tpl *template.Template
}
func (ds *DingtalkSender) Send(ctx MessageContext) {
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
return
}
urls, ats := ds.extract(ctx.Users)
if len(urls) == 0 {
return
}
message := BuildTplMessage(ds.tpl, ctx.Event)
for _, url := range urls {
var body dingtalk
// NoAt in url
if strings.Contains(url, "noat=1") {
body = dingtalk{
Msgtype: "markdown",
Markdown: dingtalkMarkdown{
Title: ctx.Event.RuleName,
Text: message,
},
}
} else {
body = dingtalk{
Msgtype: "markdown",
Markdown: dingtalkMarkdown{
Title: ctx.Event.RuleName,
Text: message + "\n" + strings.Join(ats, " "),
},
At: dingtalkAt{
AtMobiles: ats,
IsAtAll: false,
},
}
}
ds.doSend(url, body)
}
}
// extract urls and ats from Users
func (ds *DingtalkSender) extract(users []*models.User) ([]string, []string) {
urls := make([]string, 0, len(users))
ats := make([]string, 0, len(users))
for _, user := range users {
if user.Phone != "" {
ats = append(ats, "@"+user.Phone)
}
if token, has := user.ExtractToken(models.Dingtalk); has {
url := token
if !strings.HasPrefix(token, "https://") {
url = "https://oapi.dingtalk.com/robot/send?access_token=" + token
}
urls = append(urls, url)
}
}
return urls, ats
}
func (ds *DingtalkSender) doSend(url string, body dingtalk) {
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
if err != nil {
logger.Errorf("dingtalk_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
} else {
logger.Infof("dingtalk_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
}
}

View File

@@ -1,173 +0,0 @@
package sender
import (
"crypto/tls"
"html/template"
"time"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/models"
"github.com/toolkits/pkg/logger"
"gopkg.in/gomail.v2"
)
var mailch chan *gomail.Message
type EmailSender struct {
subjectTpl *template.Template
contentTpl *template.Template
smtp aconf.SMTPConfig
}
func (es *EmailSender) Send(ctx MessageContext) {
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
return
}
tos := extract(ctx.Users)
var subject string
if es.subjectTpl != nil {
subject = BuildTplMessage(es.subjectTpl, ctx.Event)
} else {
subject = ctx.Event.RuleName
}
content := BuildTplMessage(es.contentTpl, ctx.Event)
es.WriteEmail(subject, content, tos)
}
func extract(users []*models.User) []string {
tos := make([]string, 0, len(users))
for _, u := range users {
if u.Email != "" {
tos = append(tos, u.Email)
}
}
return tos
}
func (es *EmailSender) SendEmail(subject, content string, tos []string, stmp aconf.SMTPConfig) {
conf := stmp
d := gomail.NewDialer(conf.Host, conf.Port, conf.User, conf.Pass)
if conf.InsecureSkipVerify {
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
m := gomail.NewMessage()
m.SetHeader("From", stmp.From)
m.SetHeader("To", tos...)
m.SetHeader("Subject", subject)
m.SetBody("text/html", content)
err := d.DialAndSend(m)
if err != nil {
logger.Errorf("email_sender: failed to send: %v", err)
}
}
func (es *EmailSender) WriteEmail(subject, content string, tos []string) {
m := gomail.NewMessage()
m.SetHeader("From", es.smtp.From)
m.SetHeader("To", tos...)
m.SetHeader("Subject", subject)
m.SetBody("text/html", content)
mailch <- m
}
func dialSmtp(d *gomail.Dialer) gomail.SendCloser {
for {
if s, err := d.Dial(); err != nil {
logger.Errorf("email_sender: failed to dial smtp: %s", err)
time.Sleep(time.Second)
continue
} else {
return s
}
}
}
var mailQuit = make(chan struct{})
func RestartEmailSender(smtp aconf.SMTPConfig) {
close(mailQuit)
mailQuit = make(chan struct{})
StartEmailSender(smtp)
}
func StartEmailSender(smtp aconf.SMTPConfig) {
mailch = make(chan *gomail.Message, 100000)
conf := smtp
if conf.Host == "" || conf.Port == 0 {
logger.Warning("SMTP configurations invalid")
return
}
logger.Infof("start email sender... %+v", conf)
d := gomail.NewDialer(conf.Host, conf.Port, conf.User, conf.Pass)
if conf.InsecureSkipVerify {
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
var s gomail.SendCloser
var open bool
var size int
for {
select {
case <-mailQuit:
return
case m, ok := <-mailch:
if !ok {
return
}
if !open {
s = dialSmtp(d)
open = true
}
if err := gomail.Send(s, m); err != nil {
logger.Errorf("email_sender: failed to send: %s", err)
// close and retry
if err := s.Close(); err != nil {
logger.Warningf("email_sender: failed to close smtp connection: %s", err)
}
s = dialSmtp(d)
open = true
if err := gomail.Send(s, m); err != nil {
logger.Errorf("email_sender: failed to retry send: %s", err)
}
} else {
logger.Infof("email_sender: result=succ subject=%v to=%v", m.GetHeader("Subject"), m.GetHeader("To"))
}
size++
if size >= conf.Batch {
if err := s.Close(); err != nil {
logger.Warningf("email_sender: failed to close smtp connection: %s", err)
}
open = false
size = 0
}
// Close the connection to the SMTP server if no email was sent in
// the last 30 seconds.
case <-time.After(30 * time.Second):
if open {
if err := s.Close(); err != nil {
logger.Warningf("email_sender: failed to close smtp connection: %s", err)
}
open = false
}
}
}
}

View File

@@ -1,82 +0,0 @@
package sender
import (
"html/template"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/toolkits/pkg/logger"
)
type feishuContent struct {
Text string `json:"text"`
}
type feishuAt struct {
AtMobiles []string `json:"atMobiles"`
IsAtAll bool `json:"isAtAll"`
}
type feishu struct {
Msgtype string `json:"msg_type"`
Content feishuContent `json:"content"`
At feishuAt `json:"at"`
}
type FeishuSender struct {
tpl *template.Template
}
func (fs *FeishuSender) Send(ctx MessageContext) {
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
return
}
urls, ats := fs.extract(ctx.Users)
message := BuildTplMessage(fs.tpl, ctx.Event)
for _, url := range urls {
body := feishu{
Msgtype: "text",
Content: feishuContent{
Text: message,
},
}
if !strings.Contains(url, "noat=1") {
body.At = feishuAt{
AtMobiles: ats,
IsAtAll: false,
}
}
fs.doSend(url, body)
}
}
func (fs *FeishuSender) extract(users []*models.User) ([]string, []string) {
urls := make([]string, 0, len(users))
ats := make([]string, 0, len(users))
for _, user := range users {
if user.Phone != "" {
ats = append(ats, user.Phone)
}
if token, has := user.ExtractToken(models.Feishu); has {
url := token
if !strings.HasPrefix(token, "https://") {
url = "https://open.feishu.cn/open-apis/bot/v2/hook/" + token
}
urls = append(urls, url)
}
}
return urls, ats
}
func (fs *FeishuSender) doSend(url string, body feishu) {
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
if err != nil {
logger.Errorf("feishu_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
} else {
logger.Infof("feishu_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
}
}

View File

@@ -1,107 +0,0 @@
package sender
import (
"html/template"
"net/url"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/toolkits/pkg/logger"
)
type MatterMostMessage struct {
Text string
Tokens []string
}
type mm struct {
Channel string `json:"channel"`
Username string `json:"username"`
Text string `json:"text"`
}
type MmSender struct {
tpl *template.Template
}
func (ms *MmSender) Send(ctx MessageContext) {
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
return
}
urls := ms.extract(ctx.Users)
if len(urls) == 0 {
return
}
message := BuildTplMessage(ms.tpl, ctx.Event)
SendMM(MatterMostMessage{
Text: message,
Tokens: urls,
})
}
func (ms *MmSender) extract(users []*models.User) []string {
tokens := make([]string, 0, len(users))
for _, user := range users {
if token, has := user.ExtractToken(models.Mm); has {
tokens = append(tokens, token)
}
}
return tokens
}
func SendMM(message MatterMostMessage) {
for i := 0; i < len(message.Tokens); i++ {
u, err := url.Parse(message.Tokens[i])
if err != nil {
logger.Errorf("mm_sender: failed to parse error=%v", err)
continue
}
v, err := url.ParseQuery(u.RawQuery)
if err != nil {
logger.Errorf("mm_sender: failed to parse query error=%v", err)
}
channels := v["channel"] // do not get
txt := ""
atuser := v["atuser"]
if len(atuser) != 0 {
txt = strings.Join(MapStrToStr(atuser, func(u string) string {
return "@" + u
}), ",") + "\n"
}
username := v.Get("username")
if err != nil {
logger.Errorf("mm_sender: failed to parse error=%v", err)
}
// simple concatenating
ur := u.Scheme + "://" + u.Host + u.Path
for _, channel := range channels {
body := mm{
Channel: channel,
Username: username,
Text: txt + message.Text,
}
res, code, err := poster.PostJSON(ur, time.Second*5, body, 3)
if err != nil {
logger.Errorf("mm_sender: result=fail url=%s code=%d error=%v response=%s", ur, code, err, string(res))
} else {
logger.Infof("mm_sender: result=succ url=%s code=%d response=%s", ur, code, string(res))
}
}
}
}
func MapStrToStr(arr []string, fn func(s string) string) []string {
var newArray = []string{}
for _, it := range arr {
newArray = append(newArray, fn(it))
}
return newArray
}

View File

@@ -1,97 +0,0 @@
package sender
import (
"bytes"
"os"
"os/exec"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/toolkits/pkg/file"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/sys"
)
func MayPluginNotify(noticeBytes []byte, notifyScript models.NotifyScript) {
if len(noticeBytes) == 0 {
return
}
alertingCallScript(noticeBytes, notifyScript)
}
func alertingCallScript(stdinBytes []byte, notifyScript models.NotifyScript) {
// not enable or no notify.py? do nothing
config := notifyScript
if !config.Enable || config.Content == "" {
return
}
fpath := ".notify_scriptt"
if config.Type == 1 {
fpath = config.Content
} else {
rewrite := true
if file.IsExist(fpath) {
oldContent, err := file.ToString(fpath)
if err != nil {
logger.Errorf("event_script_notify_fail: read script file err: %v", err)
return
}
if oldContent == config.Content {
rewrite = false
}
}
if rewrite {
_, err := file.WriteString(fpath, config.Content)
if err != nil {
logger.Errorf("event_script_notify_fail: write script file err: %v", err)
return
}
err = os.Chmod(fpath, 0777)
if err != nil {
logger.Errorf("event_script_notify_fail: chmod script file err: %v", err)
return
}
}
fpath = "./" + fpath
}
cmd := exec.Command(fpath)
cmd.Stdin = bytes.NewReader(stdinBytes)
// combine stdout and stderr
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
err := startCmd(cmd)
if err != nil {
logger.Errorf("event_script_notify_fail: run cmd err: %v", err)
return
}
err, isTimeout := sys.WrapTimeout(cmd, time.Duration(config.Timeout)*time.Second)
if isTimeout {
if err == nil {
logger.Errorf("event_script_notify_fail: timeout and killed process %s", fpath)
}
if err != nil {
logger.Errorf("event_script_notify_fail: kill process %s occur error %v", fpath, err)
}
return
}
if err != nil {
logger.Errorf("event_script_notify_fail: exec script %s occur error: %v, output: %s", fpath, err, buf.String())
return
}
logger.Infof("event_script_notify_ok: exec %s output: %s", fpath, buf.String())
}

View File

@@ -1,14 +0,0 @@
//go:build !windows
// +build !windows
package sender
import (
"os/exec"
"syscall"
)
func startCmd(c *exec.Cmd) error {
c.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
return c.Start()
}

View File

@@ -1,7 +0,0 @@
package sender
import "os/exec"
func startCmd(c *exec.Cmd) error {
return c.Start()
}

View File

@@ -1,63 +0,0 @@
package sender
import (
"bytes"
"html/template"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
)
type (
// Sender 发送消息通知的接口
Sender interface {
Send(ctx MessageContext)
}
// MessageContext 一个event所生成的告警通知的上下文
MessageContext struct {
Users []*models.User
Rule *models.AlertRule
Event *models.AlertCurEvent
}
)
func NewSender(key string, tpls map[string]*template.Template, smtp aconf.SMTPConfig) Sender {
switch key {
case models.Dingtalk:
return &DingtalkSender{tpl: tpls[models.Dingtalk]}
case models.Wecom:
return &WecomSender{tpl: tpls[models.Wecom]}
case models.Feishu:
return &FeishuSender{tpl: tpls[models.Feishu]}
case models.Email:
return &EmailSender{subjectTpl: tpls["mailsubject"], contentTpl: tpls[models.Email], smtp: smtp}
case models.Mm:
return &MmSender{tpl: tpls[models.Mm]}
case models.Telegram:
return &TelegramSender{tpl: tpls[models.Telegram]}
}
return nil
}
func BuildMessageContext(rule *models.AlertRule, event *models.AlertCurEvent, uids []int64, userCache *memsto.UserCacheType) MessageContext {
users := userCache.GetByUserIds(uids)
return MessageContext{
Rule: rule,
Event: event,
Users: users,
}
}
func BuildTplMessage(tpl *template.Template, event *models.AlertCurEvent) string {
if tpl == nil {
return "tpl for current sender not found, please check configuration"
}
var body bytes.Buffer
if err := tpl.Execute(&body, event); err != nil {
return err.Error()
}
return body.String()
}

View File

@@ -1,82 +0,0 @@
package sender
import (
"html/template"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/toolkits/pkg/logger"
)
type TelegramMessage struct {
Text string
Tokens []string
}
type telegram struct {
ParseMode string `json:"parse_mode"`
Text string `json:"text"`
}
type TelegramSender struct {
tpl *template.Template
}
func (ts *TelegramSender) Send(ctx MessageContext) {
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
return
}
tokens := ts.extract(ctx.Users)
message := BuildTplMessage(ts.tpl, ctx.Event)
SendTelegram(TelegramMessage{
Text: message,
Tokens: tokens,
})
}
func (ts *TelegramSender) extract(users []*models.User) []string {
tokens := make([]string, 0, len(users))
for _, user := range users {
if token, has := user.ExtractToken(models.Telegram); has {
tokens = append(tokens, token)
}
}
return tokens
}
func SendTelegram(message TelegramMessage) {
for i := 0; i < len(message.Tokens); i++ {
if !strings.Contains(message.Tokens[i], "/") && !strings.HasPrefix(message.Tokens[i], "https://") {
logger.Errorf("telegram_sender: result=fail invalid token=%s", message.Tokens[i])
continue
}
var url string
if strings.HasPrefix(message.Tokens[i], "https://") {
url = message.Tokens[i]
} else {
array := strings.Split(message.Tokens[i], "/")
if len(array) != 2 {
logger.Errorf("telegram_sender: result=fail invalid token=%s", message.Tokens[i])
continue
}
botToken := array[0]
chatId := array[1]
url = "https://api.telegram.org/bot" + botToken + "/sendMessage?chat_id=" + chatId
}
body := telegram{
ParseMode: "markdown",
Text: message.Text,
}
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
if err != nil {
logger.Errorf("telegram_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
} else {
logger.Infof("telegram_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
}
}
}

View File

@@ -1,68 +0,0 @@
package sender
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/toolkits/pkg/logger"
)
func SendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent) {
for _, conf := range webhooks {
if conf.Url == "" || !conf.Enable {
continue
}
bs, err := json.Marshal(event)
if err != nil {
continue
}
bf := bytes.NewBuffer(bs)
req, err := http.NewRequest("POST", conf.Url, bf)
if err != nil {
logger.Warning("alertingWebhook failed to new request", err)
continue
}
req.Header.Set("Content-Type", "application/json")
if conf.BasicAuthUser != "" && conf.BasicAuthPass != "" {
req.SetBasicAuth(conf.BasicAuthUser, conf.BasicAuthPass)
}
if len(conf.Headers) > 0 && len(conf.Headers)%2 == 0 {
for i := 0; i < len(conf.Headers); i += 2 {
if conf.Headers[i] == "host" {
req.Host = conf.Headers[i+1]
continue
}
req.Header.Set(conf.Headers[i], conf.Headers[i+1])
}
}
// todo add skip verify
client := http.Client{
Timeout: time.Duration(conf.Timeout) * time.Second,
}
var resp *http.Response
resp, err = client.Do(req)
if err != nil {
logger.Errorf("event_webhook_fail, ruleId: [%d], eventId: [%d], url: [%s], error: [%s]", event.RuleId, event.Id, conf.Url, err)
continue
}
var body []byte
if resp.Body != nil {
defer resp.Body.Close()
body, _ = ioutil.ReadAll(resp.Body)
}
logger.Debugf("event_webhook_succ, url: %s, response code: %d, body: %s", conf.Url, resp.StatusCode, string(body))
}
}

View File

@@ -1,65 +0,0 @@
package sender
import (
"html/template"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/toolkits/pkg/logger"
)
type wecomMarkdown struct {
Content string `json:"content"`
}
type wecom struct {
Msgtype string `json:"msgtype"`
Markdown wecomMarkdown `json:"markdown"`
}
type WecomSender struct {
tpl *template.Template
}
func (ws *WecomSender) Send(ctx MessageContext) {
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
return
}
urls := ws.extract(ctx.Users)
message := BuildTplMessage(ws.tpl, ctx.Event)
for _, url := range urls {
body := wecom{
Msgtype: "markdown",
Markdown: wecomMarkdown{
Content: message,
},
}
ws.doSend(url, body)
}
}
func (ws *WecomSender) extract(users []*models.User) []string {
urls := make([]string, 0, len(users))
for _, user := range users {
if token, has := user.ExtractToken(models.Wecom); has {
url := token
if !strings.HasPrefix(token, "https://") {
url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + token
}
urls = append(urls, url)
}
}
return urls
}
func (ws *WecomSender) doSend(url string, body wecom) {
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
if err != nil {
logger.Errorf("wecom_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
} else {
logger.Infof("wecom_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
}
}

View File

@@ -1,29 +0,0 @@
package cconf
type Center struct {
Plugins []Plugin
MetricsYamlFile string
OpsYamlFile string
BuiltinIntegrationsDir string
I18NHeaderKey string
MetricDesc MetricDescType
AnonymousAccess AnonymousAccess
}
type Plugin struct {
Id int64 `json:"id"`
Category string `json:"category"`
Type string `json:"plugin_type"`
TypeName string `json:"plugin_type_name"`
}
type AnonymousAccess struct {
PromQuerier bool
AlertDetail bool
}
func (c *Center) PreCheck() {
if len(c.Plugins) == 0 {
c.Plugins = Plugins
}
}

View File

@@ -1,60 +0,0 @@
package cconf
const EVENT_EXAMPLE = `
{
"id": 1000000,
"cate": "prometheus",
"datasource_id": 1,
"group_id": 1,
"group_name": "Default Busi Group",
"hash": "2cb966f9ba1cdc7af94c3796e855955a",
"rule_id": 23,
"rule_name": "测试告警",
"rule_note": "测试告警",
"rule_prod": "metric",
"rule_config": {
"queries": [
{
"key": "all_hosts",
"op": "==",
"values": []
}
],
"triggers": [
{
"duration": 3,
"percent": 10,
"severity": 3,
"type": "pct_target_miss"
}
]
},
"prom_for_duration": 60,
"prom_eval_interval": 30,
"callbacks": ["https://n9e.github.io"],
"notify_recovered": 1,
"notify_channels": ["dingtalk"],
"notify_groups": [],
"notify_groups_obj": null,
"target_ident": "host01",
"target_note": "机器备注",
"trigger_time": 1677229517,
"trigger_value": "2273533952",
"tags": [
"__name__=disk_free",
"dc=qcloud-dev",
"device=vda1",
"fstype=ext4",
"ident=tt-fc-dev00.nj"
],
"is_recovered": false,
"notify_users_obj": null,
"last_eval_time": 1677229517,
"last_sent_time": 1677229517,
"notify_cur_number": 1,
"first_trigger_time": 1677229517,
"annotations": {
"summary": "测试告警"
}
}
`

View File

@@ -1,44 +0,0 @@
package cconf
import (
"path"
"github.com/toolkits/pkg/file"
)
// metricDesc , As load map happens before read map, there is no necessary to use concurrent map for metric desc store
type MetricDescType struct {
CommonDesc map[string]string `yaml:",inline" json:"common"`
Zh map[string]string `yaml:"zh" json:"zh"`
En map[string]string `yaml:"en" json:"en"`
}
var MetricDesc MetricDescType
// GetMetricDesc , if metric is not registered, empty string will be returned
func GetMetricDesc(lang, metric string) string {
var m map[string]string
if lang == "zh" {
m = MetricDesc.Zh
} else {
m = MetricDesc.En
}
if m != nil {
if desc, has := m[metric]; has {
return desc
}
}
return MetricDesc.CommonDesc[metric]
}
func LoadMetricsYaml(configDir, metricsYamlFile string) error {
fp := metricsYamlFile
if fp == "" {
fp = path.Join(configDir, "metrics.yaml")
}
if !file.IsExist(fp) {
return nil
}
return file.ReadYaml(fp, &MetricDesc)
}

View File

@@ -1,38 +0,0 @@
package cconf
import (
"path"
"github.com/toolkits/pkg/file"
)
var Operations = Operation{}
type Operation struct {
Ops []Ops `yaml:"ops"`
}
type Ops struct {
Name string `yaml:"name" json:"name"`
Cname string `yaml:"cname" json:"cname"`
Ops []string `yaml:"ops" json:"ops"`
}
func LoadOpsYaml(configDir string, opsYamlFile string) error {
fp := opsYamlFile
if fp == "" {
fp = path.Join(configDir, "ops.yaml")
}
if !file.IsExist(fp) {
return nil
}
return file.ReadYaml(fp, &Operations)
}
func GetAllOps(ops []Ops) []string {
var ret []string
for _, op := range ops {
ret = append(ret, op.Ops...)
}
return ret
}

View File

@@ -1,22 +0,0 @@
package cconf
var Plugins = []Plugin{
{
Id: 1,
Category: "timeseries",
Type: "prometheus",
TypeName: "Prometheus Like",
},
{
Id: 2,
Category: "logging",
Type: "elasticsearch",
TypeName: "Elasticsearch",
},
{
Id: 3,
Category: "logging",
Type: "jaeger",
TypeName: "Jaeger",
},
}

View File

@@ -1,96 +0,0 @@
package center
import (
"context"
"fmt"
"github.com/ccfos/nightingale/v6/alert"
"github.com/ccfos/nightingale/v6/alert/astats"
"github.com/ccfos/nightingale/v6/alert/process"
"github.com/ccfos/nightingale/v6/center/cconf"
"github.com/ccfos/nightingale/v6/center/metas"
"github.com/ccfos/nightingale/v6/center/sso"
"github.com/ccfos/nightingale/v6/conf"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/pkg/i18nx"
"github.com/ccfos/nightingale/v6/pkg/logx"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/pushgw/idents"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/ccfos/nightingale/v6/storage"
alertrt "github.com/ccfos/nightingale/v6/alert/router"
centerrt "github.com/ccfos/nightingale/v6/center/router"
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
)
func Initialize(configDir string, cryptoKey string) (func(), error) {
config, err := conf.InitConfig(configDir, cryptoKey)
if err != nil {
return nil, fmt.Errorf("failed to init config: %v", err)
}
cconf.LoadMetricsYaml(configDir, config.Center.MetricsYamlFile)
cconf.LoadOpsYaml(configDir, config.Center.OpsYamlFile)
logxClean, err := logx.Init(config.Log)
if err != nil {
return nil, err
}
i18nx.Init()
db, err := storage.New(config.DB)
if err != nil {
return nil, err
}
ctx := ctx.NewContext(context.Background(), db, true)
models.InitRoot(ctx)
redis, err := storage.NewRedis(config.Redis)
if err != nil {
return nil, err
}
metas := metas.New(redis)
idents := idents.New(ctx)
syncStats := memsto.NewSyncStats()
alertStats := astats.NewSyncStats()
sso := sso.Init(config.Center, ctx)
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
targetCache := memsto.NewTargetCache(ctx, syncStats, redis)
dsCache := memsto.NewDatasourceCache(ctx, syncStats)
alertMuteCache := memsto.NewAlertMuteCache(ctx, syncStats)
alertRuleCache := memsto.NewAlertRuleCache(ctx, syncStats)
notifyConfigCache := memsto.NewNotifyConfigCache(ctx)
promClients := prom.NewPromClient(ctx, config.Alert.Heartbeat)
externalProcessors := process.NewExternalProcessors()
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, dsCache, ctx, promClients)
writers := writer.NewWriters(config.Pushgw)
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
centerRouter := centerrt.New(config.HTTP, config.Center, cconf.Operations, dsCache, notifyConfigCache, promClients, redis, sso, ctx, metas, targetCache)
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, targetCache, busiGroupCache, idents, writers, ctx)
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
centerRouter.Config(r)
alertrtRouter.Config(r)
pushgwRouter.Config(r)
httpClean := httpx.Init(config.HTTP, r)
return func() {
logxClean()
httpClean()
}, nil
}

View File

@@ -1,53 +0,0 @@
package cstats
import (
"time"
"github.com/prometheus/client_golang/prometheus"
)
const Service = "n9e-center"
var (
labels = []string{"service", "code", "path", "method"}
uptime = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "uptime",
Help: "HTTP service uptime.",
}, []string{"service"},
)
RequestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_request_count_total",
Help: "Total number of HTTP requests made.",
}, labels,
)
RequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Buckets: []float64{.01, .1, 1, 10},
Name: "http_request_duration_seconds",
Help: "HTTP request latencies in seconds.",
}, labels,
)
)
func Init() {
// Register the summary and the histogram with Prometheus's default registry.
prometheus.MustRegister(
uptime,
RequestCounter,
RequestDuration,
)
go recordUptime()
}
// recordUptime increases service uptime per second.
func recordUptime() {
for range time.Tick(time.Second) {
uptime.WithLabelValues(Service).Inc()
}
}

View File

@@ -1,104 +0,0 @@
package metas
import (
"context"
"sync"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/storage"
"github.com/toolkits/pkg/logger"
)
type Set struct {
sync.RWMutex
items map[string]models.HostMeta
redis storage.Redis
}
func New(redis storage.Redis) *Set {
set := &Set{
items: make(map[string]models.HostMeta),
redis: redis,
}
set.Init()
return set
}
func (s *Set) Init() {
go s.LoopPersist()
}
func (s *Set) MSet(items map[string]models.HostMeta) {
s.Lock()
defer s.Unlock()
for ident, meta := range items {
s.items[ident] = meta
}
}
func (s *Set) Set(ident string, meta models.HostMeta) {
s.Lock()
defer s.Unlock()
s.items[ident] = meta
}
func (s *Set) LoopPersist() {
for {
time.Sleep(time.Second)
s.persist()
}
}
func (s *Set) persist() {
var items map[string]models.HostMeta
s.Lock()
if len(s.items) == 0 {
s.Unlock()
return
}
items = s.items
s.items = make(map[string]models.HostMeta)
s.Unlock()
s.updateMeta(items)
}
func (s *Set) updateMeta(items map[string]models.HostMeta) {
m := make(map[string]models.HostMeta, 100)
num := 0
for _, meta := range items {
m[meta.Hostname] = meta
num++
if num == 100 {
if err := s.updateTargets(m); err != nil {
logger.Errorf("failed to update targets: %v", err)
}
m = make(map[string]models.HostMeta, 100)
num = 0
}
}
if err := s.updateTargets(m); err != nil {
logger.Errorf("failed to update targets: %v", err)
}
}
func (s *Set) updateTargets(m map[string]models.HostMeta) error {
count := int64(len(m))
if count == 0 {
return nil
}
newMap := make(map[string]interface{}, count)
for ident, meta := range m {
newMap[models.WrapIdent(ident)] = meta
}
err := storage.MSet(context.Background(), s.redis, newMap)
return err
}

View File

@@ -1,428 +0,0 @@
package router
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/ccfos/nightingale/v6/center/cconf"
"github.com/ccfos/nightingale/v6/center/cstats"
"github.com/ccfos/nightingale/v6/center/metas"
"github.com/ccfos/nightingale/v6/center/sso"
_ "github.com/ccfos/nightingale/v6/front/statik"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/pkg/aop"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/prom"
"github.com/ccfos/nightingale/v6/storage"
"github.com/gin-gonic/gin"
"github.com/rakyll/statik/fs"
"github.com/toolkits/pkg/logger"
)
type Router struct {
HTTP httpx.Config
Center cconf.Center
Operations cconf.Operation
DatasourceCache *memsto.DatasourceCacheType
NotifyConfigCache *memsto.NotifyConfigCacheType
PromClients *prom.PromClientMap
Redis storage.Redis
MetaSet *metas.Set
TargetCache *memsto.TargetCacheType
Sso *sso.SsoClient
Ctx *ctx.Context
}
func New(httpConfig httpx.Config, center cconf.Center, operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType,
pc *prom.PromClientMap, redis storage.Redis, sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, tc *memsto.TargetCacheType) *Router {
return &Router{
HTTP: httpConfig,
Center: center,
Operations: operations,
DatasourceCache: ds,
NotifyConfigCache: ncc,
PromClients: pc,
Redis: redis,
MetaSet: metaSet,
TargetCache: tc,
Sso: sso,
Ctx: ctx,
}
}
func stat() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
code := fmt.Sprintf("%d", c.Writer.Status())
method := c.Request.Method
labels := []string{cstats.Service, code, c.FullPath(), method}
cstats.RequestCounter.WithLabelValues(labels...).Inc()
cstats.RequestDuration.WithLabelValues(labels...).Observe(float64(time.Since(start).Seconds()))
}
}
func languageDetector(i18NHeaderKey string) gin.HandlerFunc {
headerKey := i18NHeaderKey
return func(c *gin.Context) {
if headerKey != "" {
lang := c.GetHeader(headerKey)
if lang != "" {
if strings.HasPrefix(lang, "zh") {
c.Request.Header.Set("X-Language", "zh")
} else if strings.HasPrefix(lang, "en") {
c.Request.Header.Set("X-Language", "en")
} else {
c.Request.Header.Set("X-Language", lang)
}
} else {
c.Request.Header.Set("X-Language", "en")
}
}
c.Next()
}
}
func (rt *Router) configNoRoute(r *gin.Engine, fs *http.FileSystem) {
r.NoRoute(func(c *gin.Context) {
arr := strings.Split(c.Request.URL.Path, ".")
suffix := arr[len(arr)-1]
switch suffix {
case "png", "jpeg", "jpg", "svg", "ico", "gif", "css", "js", "html", "htm", "gz", "zip", "map":
c.FileFromFS(c.Request.URL.Path, *fs)
default:
c.FileFromFS("/", *fs)
}
})
}
func (rt *Router) Config(r *gin.Engine) {
r.Use(stat())
r.Use(languageDetector(rt.Center.I18NHeaderKey))
r.Use(aop.Recovery())
statikFS, err := fs.New()
if err != nil {
logger.Errorf("cannot create statik fs: %v", err)
}
r.StaticFS("/pub", statikFS)
pagesPrefix := "/api/n9e"
pages := r.Group(pagesPrefix)
{
if rt.Center.AnonymousAccess.PromQuerier {
pages.Any("/proxy/:id/*url", rt.dsProxy)
pages.POST("/query-range-batch", rt.promBatchQueryRange)
pages.POST("/query-instant-batch", rt.promBatchQueryInstant)
pages.GET("/datasource/brief", rt.datasourceBriefs)
} else {
pages.Any("/proxy/:id/*url", rt.auth(), rt.dsProxy)
pages.POST("/query-range-batch", rt.auth(), rt.promBatchQueryRange)
pages.POST("/query-instant-batch", rt.auth(), rt.promBatchQueryInstant)
pages.GET("/datasource/brief", rt.auth(), rt.datasourceBriefs)
}
pages.POST("/auth/login", rt.jwtMock(), rt.loginPost)
pages.POST("/auth/logout", rt.jwtMock(), rt.logoutPost)
pages.POST("/auth/refresh", rt.jwtMock(), rt.refreshPost)
pages.GET("/auth/sso-config", rt.ssoConfigNameGet)
pages.GET("/auth/redirect", rt.loginRedirect)
pages.GET("/auth/redirect/cas", rt.loginRedirectCas)
pages.GET("/auth/redirect/oauth", rt.loginRedirectOAuth)
pages.GET("/auth/callback", rt.loginCallback)
pages.GET("/auth/callback/cas", rt.loginCallbackCas)
pages.GET("/auth/callback/oauth", rt.loginCallbackOAuth)
pages.GET("/auth/perms", rt.allPerms)
pages.GET("/metrics/desc", rt.metricsDescGetFile)
pages.POST("/metrics/desc", rt.metricsDescGetMap)
pages.GET("/notify-channels", rt.notifyChannelsGets)
pages.GET("/contact-keys", rt.contactKeysGets)
pages.GET("/self/perms", rt.auth(), rt.user(), rt.permsGets)
pages.GET("/self/profile", rt.auth(), rt.user(), rt.selfProfileGet)
pages.PUT("/self/profile", rt.auth(), rt.user(), rt.selfProfilePut)
pages.PUT("/self/password", rt.auth(), rt.user(), rt.selfPasswordPut)
pages.GET("/users", rt.auth(), rt.user(), rt.perm("/users"), rt.userGets)
pages.POST("/users", rt.auth(), rt.admin(), rt.userAddPost)
pages.GET("/user/:id/profile", rt.auth(), rt.userProfileGet)
pages.PUT("/user/:id/profile", rt.auth(), rt.admin(), rt.userProfilePut)
pages.PUT("/user/:id/password", rt.auth(), rt.admin(), rt.userPasswordPut)
pages.DELETE("/user/:id", rt.auth(), rt.admin(), rt.userDel)
pages.GET("/metric-views", rt.auth(), rt.metricViewGets)
pages.DELETE("/metric-views", rt.auth(), rt.user(), rt.metricViewDel)
pages.POST("/metric-views", rt.auth(), rt.user(), rt.metricViewAdd)
pages.PUT("/metric-views", rt.auth(), rt.user(), rt.metricViewPut)
pages.GET("/user-groups", rt.auth(), rt.user(), rt.userGroupGets)
pages.POST("/user-groups", rt.auth(), rt.user(), rt.perm("/user-groups/add"), rt.userGroupAdd)
pages.GET("/user-group/:id", rt.auth(), rt.user(), rt.userGroupGet)
pages.PUT("/user-group/:id", rt.auth(), rt.user(), rt.perm("/user-groups/put"), rt.userGroupWrite(), rt.userGroupPut)
pages.DELETE("/user-group/:id", rt.auth(), rt.user(), rt.perm("/user-groups/del"), rt.userGroupWrite(), rt.userGroupDel)
pages.POST("/user-group/:id/members", rt.auth(), rt.user(), rt.perm("/user-groups/put"), rt.userGroupWrite(), rt.userGroupMemberAdd)
pages.DELETE("/user-group/:id/members", rt.auth(), rt.user(), rt.perm("/user-groups/put"), rt.userGroupWrite(), rt.userGroupMemberDel)
pages.GET("/busi-groups", rt.auth(), rt.user(), rt.busiGroupGets)
pages.POST("/busi-groups", rt.auth(), rt.user(), rt.perm("/busi-groups/add"), rt.busiGroupAdd)
pages.GET("/busi-groups/alertings", rt.auth(), rt.busiGroupAlertingsGets)
pages.GET("/busi-group/:id", rt.auth(), rt.user(), rt.bgro(), rt.busiGroupGet)
pages.PUT("/busi-group/:id", rt.auth(), rt.user(), rt.perm("/busi-groups/put"), rt.bgrw(), rt.busiGroupPut)
pages.POST("/busi-group/:id/members", rt.auth(), rt.user(), rt.perm("/busi-groups/put"), rt.bgrw(), rt.busiGroupMemberAdd)
pages.DELETE("/busi-group/:id/members", rt.auth(), rt.user(), rt.perm("/busi-groups/put"), rt.bgrw(), rt.busiGroupMemberDel)
pages.DELETE("/busi-group/:id", rt.auth(), rt.user(), rt.perm("/busi-groups/del"), rt.bgrw(), rt.busiGroupDel)
pages.GET("/busi-group/:id/perm/:perm", rt.auth(), rt.user(), rt.checkBusiGroupPerm)
pages.GET("/targets", rt.auth(), rt.user(), rt.targetGets)
pages.POST("/target/list", rt.auth(), rt.user(), rt.targetGetsByHostFilter)
pages.DELETE("/targets", rt.auth(), rt.user(), rt.perm("/targets/del"), rt.targetDel)
pages.GET("/targets/tags", rt.auth(), rt.user(), rt.targetGetTags)
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.POST("/builtin-cate-favorite", rt.auth(), rt.user(), rt.builtinCateFavoriteAdd)
pages.DELETE("/builtin-cate-favorite/:name", rt.auth(), rt.user(), rt.builtinCateFavoriteDel)
pages.GET("/builtin-boards", rt.builtinBoardGets)
pages.GET("/builtin-board/:name", rt.builtinBoardGet)
pages.GET("/dashboards/builtin/list", rt.builtinBoardGets)
pages.GET("/builtin-boards-cates", rt.auth(), rt.user(), rt.builtinBoardCateGets)
pages.POST("/builtin-boards-detail", rt.auth(), rt.user(), rt.builtinBoardDetailGets)
pages.GET("/integrations/icon/:cate/:name", rt.builtinIcon)
pages.GET("/busi-group/:id/boards", rt.auth(), rt.user(), rt.perm("/dashboards"), rt.bgro(), rt.boardGets)
pages.POST("/busi-group/:id/boards", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.bgrw(), rt.boardAdd)
pages.POST("/busi-group/:id/board/:bid/clone", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.bgrw(), rt.boardClone)
pages.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)
pages.PUT("/board/:bid/configs", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.boardPutConfigs)
pages.PUT("/board/:bid/public", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.boardPutPublic)
pages.DELETE("/boards", rt.auth(), rt.user(), rt.perm("/dashboards/del"), rt.boardDel)
pages.GET("/share-charts", rt.chartShareGets)
pages.POST("/share-charts", rt.auth(), rt.chartShareAdd)
pages.GET("/alert-rules/builtin/alerts-cates", rt.auth(), rt.user(), rt.builtinAlertCateGets)
pages.GET("/alert-rules/builtin/list", rt.auth(), rt.user(), rt.builtinAlertRules)
pages.GET("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRuleGets)
pages.POST("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.alertRuleAddByFE)
pages.POST("/busi-group/:id/alert-rules/import", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.alertRuleAddByImport)
pages.DELETE("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules/del"), rt.bgrw(), rt.alertRuleDel)
pages.PUT("/busi-group/:id/alert-rules/fields", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.bgrw(), rt.alertRulePutFields)
pages.PUT("/busi-group/:id/alert-rule/:arid", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.alertRulePutByFE)
pages.GET("/alert-rule/:arid", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRuleGet)
pages.GET("/busi-group/:id/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGets)
pages.POST("/busi-group/:id/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules/add"), rt.bgrw(), rt.recordingRuleAddByFE)
pages.DELETE("/busi-group/:id/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules/del"), rt.bgrw(), rt.recordingRuleDel)
pages.PUT("/busi-group/:id/recording-rule/:rrid", rt.auth(), rt.user(), rt.perm("/recording-rules/put"), rt.bgrw(), rt.recordingRulePutByFE)
pages.GET("/recording-rule/:rrid", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGet)
pages.PUT("/busi-group/:id/recording-rules/fields", rt.auth(), rt.user(), rt.perm("/recording-rules/put"), rt.recordingRulePutFields)
pages.GET("/busi-group/:id/alert-mutes", rt.auth(), rt.user(), rt.perm("/alert-mutes"), rt.bgro(), rt.alertMuteGetsByBG)
pages.POST("/busi-group/:id/alert-mutes", rt.auth(), rt.user(), rt.perm("/alert-mutes/add"), rt.bgrw(), rt.alertMuteAdd)
pages.DELETE("/busi-group/:id/alert-mutes", rt.auth(), rt.user(), rt.perm("/alert-mutes/del"), rt.bgrw(), rt.alertMuteDel)
pages.PUT("/busi-group/:id/alert-mute/:amid", rt.auth(), rt.user(), rt.perm("/alert-mutes/put"), rt.alertMutePutByFE)
pages.PUT("/busi-group/:id/alert-mutes/fields", rt.auth(), rt.user(), rt.perm("/alert-mutes/put"), rt.bgrw(), rt.alertMutePutFields)
pages.GET("/busi-group/:id/alert-subscribes", rt.auth(), rt.user(), rt.perm("/alert-subscribes"), rt.bgro(), rt.alertSubscribeGets)
pages.GET("/alert-subscribe/:sid", rt.auth(), rt.user(), rt.perm("/alert-subscribes"), rt.alertSubscribeGet)
pages.POST("/busi-group/:id/alert-subscribes", rt.auth(), rt.user(), rt.perm("/alert-subscribes/add"), rt.bgrw(), rt.alertSubscribeAdd)
pages.PUT("/busi-group/:id/alert-subscribes", rt.auth(), rt.user(), rt.perm("/alert-subscribes/put"), rt.bgrw(), rt.alertSubscribePut)
pages.DELETE("/busi-group/:id/alert-subscribes", rt.auth(), rt.user(), rt.perm("/alert-subscribes/del"), rt.bgrw(), rt.alertSubscribeDel)
if rt.Center.AnonymousAccess.AlertDetail {
pages.GET("/alert-cur-event/:eid", rt.alertCurEventGet)
pages.GET("/alert-his-event/:eid", rt.alertHisEventGet)
} else {
pages.GET("/alert-cur-event/:eid", rt.auth(), rt.alertCurEventGet)
pages.GET("/alert-his-event/:eid", rt.auth(), rt.alertHisEventGet)
}
// card logic
pages.GET("/alert-cur-events/list", rt.auth(), rt.alertCurEventsList)
pages.GET("/alert-cur-events/card", rt.auth(), rt.alertCurEventsCard)
pages.POST("/alert-cur-events/card/details", rt.auth(), rt.alertCurEventsCardDetails)
pages.GET("/alert-his-events/list", rt.auth(), rt.alertHisEventsList)
pages.DELETE("/alert-cur-events", rt.auth(), rt.user(), rt.perm("/alert-cur-events/del"), rt.alertCurEventDel)
pages.GET("/alert-aggr-views", rt.auth(), rt.alertAggrViewGets)
pages.DELETE("/alert-aggr-views", rt.auth(), rt.user(), rt.alertAggrViewDel)
pages.POST("/alert-aggr-views", rt.auth(), rt.user(), rt.alertAggrViewAdd)
pages.PUT("/alert-aggr-views", rt.auth(), rt.user(), rt.alertAggrViewPut)
pages.GET("/busi-group/:id/task-tpls", rt.auth(), rt.user(), rt.perm("/job-tpls"), rt.bgro(), rt.taskTplGets)
pages.POST("/busi-group/:id/task-tpls", rt.auth(), rt.user(), rt.perm("/job-tpls/add"), rt.bgrw(), rt.taskTplAdd)
pages.DELETE("/busi-group/:id/task-tpl/:tid", rt.auth(), rt.user(), rt.perm("/job-tpls/del"), rt.bgrw(), rt.taskTplDel)
pages.POST("/busi-group/:id/task-tpls/tags", rt.auth(), rt.user(), rt.perm("/job-tpls/put"), rt.bgrw(), rt.taskTplBindTags)
pages.DELETE("/busi-group/:id/task-tpls/tags", rt.auth(), rt.user(), rt.perm("/job-tpls/put"), rt.bgrw(), rt.taskTplUnbindTags)
pages.GET("/busi-group/:id/task-tpl/:tid", rt.auth(), rt.user(), rt.perm("/job-tpls"), rt.bgro(), rt.taskTplGet)
pages.PUT("/busi-group/:id/task-tpl/:tid", rt.auth(), rt.user(), rt.perm("/job-tpls/put"), rt.bgrw(), rt.taskTplPut)
pages.GET("/busi-group/:id/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks"), rt.bgro(), rt.taskGets)
pages.POST("/busi-group/:id/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks/add"), rt.bgrw(), rt.taskAdd)
pages.GET("/busi-group/:id/task/*url", rt.auth(), rt.user(), rt.perm("/job-tasks"), rt.taskProxy)
pages.PUT("/busi-group/:id/task/*url", rt.auth(), rt.user(), rt.perm("/job-tasks/put"), rt.bgrw(), rt.taskProxy)
pages.GET("/servers", rt.auth(), rt.admin(), rt.serversGet)
pages.GET("/server-clusters", rt.auth(), rt.admin(), rt.serverClustersGet)
pages.POST("/datasource/list", rt.auth(), rt.datasourceList)
pages.POST("/datasource/plugin/list", rt.auth(), rt.pluginList)
pages.POST("/datasource/upsert", rt.auth(), rt.admin(), rt.datasourceUpsert)
pages.POST("/datasource/desc", rt.auth(), rt.admin(), rt.datasourceGet)
pages.POST("/datasource/status/update", rt.auth(), rt.admin(), rt.datasourceUpdataStatus)
pages.DELETE("/datasource/", rt.auth(), rt.admin(), rt.datasourceDel)
pages.GET("/roles", rt.auth(), rt.admin(), rt.roleGets)
pages.POST("/roles", rt.auth(), rt.admin(), rt.roleAdd)
pages.PUT("/roles", rt.auth(), rt.admin(), rt.rolePut)
pages.DELETE("/role/:id", rt.auth(), rt.admin(), rt.roleDel)
pages.GET("/role/:id/ops", rt.auth(), rt.admin(), rt.operationOfRole)
pages.PUT("/role/:id/ops", rt.auth(), rt.admin(), rt.roleBindOperation)
pages.GET("/operation", rt.operations)
pages.GET("/notify-tpls", rt.auth(), rt.admin(), rt.notifyTplGets)
pages.PUT("/notify-tpl/content", rt.auth(), rt.admin(), rt.notifyTplUpdateContent)
pages.PUT("/notify-tpl", rt.auth(), rt.admin(), rt.notifyTplUpdate)
pages.POST("/notify-tpl/preview", rt.auth(), rt.admin(), rt.notifyTplPreview)
pages.GET("/sso-configs", rt.auth(), rt.admin(), rt.ssoConfigGets)
pages.PUT("/sso-config", rt.auth(), rt.admin(), rt.ssoConfigUpdate)
pages.GET("/webhooks", rt.auth(), rt.admin(), rt.webhookGets)
pages.PUT("/webhooks", rt.auth(), rt.admin(), rt.webhookPuts)
pages.GET("/notify-script", rt.auth(), rt.admin(), rt.notifyScriptGet)
pages.PUT("/notify-script", rt.auth(), rt.admin(), rt.notifyScriptPut)
pages.GET("/notify-channel", rt.auth(), rt.admin(), rt.notifyChannelGets)
pages.PUT("/notify-channel", rt.auth(), rt.admin(), rt.notifyChannelPuts)
pages.GET("/notify-contact", rt.auth(), rt.admin(), rt.notifyContactGets)
pages.PUT("/notify-contact", rt.auth(), rt.admin(), rt.notifyContactPuts)
pages.GET("/notify-config", rt.auth(), rt.admin(), rt.notifyConfigGet)
pages.PUT("/notify-config", rt.auth(), rt.admin(), rt.notifyConfigPut)
}
if rt.HTTP.APIForService.Enable {
service := r.Group("/v1/n9e")
if len(rt.HTTP.APIForService.BasicAuth) > 0 {
service.Use(gin.BasicAuth(rt.HTTP.APIForService.BasicAuth))
}
{
service.Any("/prometheus/*url", rt.dsProxy)
service.POST("/users", rt.userAddPost)
service.GET("/users", rt.userFindAll)
service.GET("/user-groups", rt.userGroupGetsByService)
service.GET("/user-group-members", rt.userGroupMemberGetsByService)
service.GET("/targets", rt.targetGetsByService)
service.GET("/targets/tags", rt.targetGetTags)
service.POST("/targets/tags", rt.targetBindTagsByService)
service.DELETE("/targets/tags", rt.targetUnbindTagsByService)
service.PUT("/targets/note", rt.targetUpdateNoteByService)
service.POST("/alert-rules", rt.alertRuleAddByService)
service.DELETE("/alert-rules", rt.alertRuleDelByService)
service.PUT("/alert-rule/:arid", rt.alertRulePutByService)
service.GET("/alert-rule/:arid", rt.alertRuleGet)
service.GET("/alert-rules", rt.alertRulesGetByService)
service.GET("/alert-subscribes", rt.alertSubscribeGetsByService)
service.GET("/busi-groups", rt.busiGroupGetsByService)
service.GET("/datasources", rt.datasourceGetsByService)
service.GET("/datasource-ids", rt.getDatasourceIds)
service.POST("/server-heartbeat", rt.serverHeartbeat)
service.GET("/servers-active", rt.serversActive)
service.GET("/recording-rules", rt.recordingRuleGetsByService)
service.GET("/alert-mutes", rt.alertMuteGets)
service.POST("/alert-mutes", rt.alertMuteAddByService)
service.DELETE("/alert-mutes", rt.alertMuteDel)
service.GET("/alert-cur-events", rt.alertCurEventsList)
service.GET("/alert-cur-events-get-by-rid", rt.alertCurEventsGetByRid)
service.GET("/alert-his-events", rt.alertHisEventsList)
service.GET("/alert-his-event/:eid", rt.alertHisEventGet)
service.GET("/config/:id", rt.configGet)
service.GET("/configs", rt.configsGet)
service.GET("/config", rt.configGetByKey)
service.PUT("/configs", rt.configsPut)
service.POST("/configs", rt.configsPost)
service.DELETE("/configs", rt.configsDel)
service.POST("/conf-prop/encrypt", rt.confPropEncrypt)
service.POST("/conf-prop/decrypt", rt.confPropDecrypt)
service.GET("/statistic", rt.statistic)
service.GET("/notify-tpls", rt.notifyTplGets)
service.POST("/task-record-add", rt.taskRecordAdd)
}
}
if rt.HTTP.APIForAgent.Enable {
heartbeat := r.Group("/v1/n9e")
{
if len(rt.HTTP.APIForAgent.BasicAuth) > 0 {
heartbeat.Use(gin.BasicAuth(rt.HTTP.APIForAgent.BasicAuth))
}
heartbeat.POST("/heartbeat", rt.heartbeat)
}
}
rt.configNoRoute(r, &statikFS)
}
func Render(c *gin.Context, data, msg interface{}) {
if msg == nil {
if data == nil {
data = struct{}{}
}
c.JSON(http.StatusOK, gin.H{"data": data, "error": ""})
} else {
c.JSON(http.StatusOK, gin.H{"error": gin.H{"message": msg}})
}
}
func Dangerous(c *gin.Context, v interface{}, code ...int) {
if v == nil {
return
}
switch t := v.(type) {
case string:
if t != "" {
c.JSON(http.StatusOK, gin.H{"error": v})
}
case error:
c.JSON(http.StatusOK, gin.H{"error": t.Error()})
}
}

View File

@@ -1,74 +0,0 @@
package router
import (
"net/http"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
// no param
func (rt *Router) alertAggrViewGets(c *gin.Context) {
lst, err := models.AlertAggrViewGets(rt.Ctx, c.MustGet("userid"))
ginx.NewRender(c).Data(lst, err)
}
// body: name, rule, cate
func (rt *Router) alertAggrViewAdd(c *gin.Context) {
var f models.AlertAggrView
ginx.BindJSON(c, &f)
me := c.MustGet("user").(*models.User)
if !me.IsAdmin() {
// 管理员可以选择当前这个视图是公开呢,还是私有,普通用户的话就只能是私有的
f.Cate = 1
}
f.Id = 0
f.CreateBy = me.Id
ginx.Dangerous(f.Add(rt.Ctx))
ginx.NewRender(c).Data(f, nil)
}
// body: ids
func (rt *Router) alertAggrViewDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
me := c.MustGet("user").(*models.User)
if me.IsAdmin() {
ginx.NewRender(c).Message(models.AlertAggrViewDel(rt.Ctx, f.Ids))
} else {
ginx.NewRender(c).Message(models.AlertAggrViewDel(rt.Ctx, f.Ids, me.Id))
}
}
// body: id, name, rule, cate
func (rt *Router) alertAggrViewPut(c *gin.Context) {
var f models.AlertAggrView
ginx.BindJSON(c, &f)
view, err := models.AlertAggrViewGet(rt.Ctx, "id = ?", f.Id)
ginx.Dangerous(err)
if view == nil {
ginx.NewRender(c).Message("no such item(id: %d)", f.Id)
return
}
me := c.MustGet("user").(*models.User)
if !me.IsAdmin() {
f.Cate = 1
if view.CreateBy != me.Id {
ginx.NewRender(c, http.StatusForbidden).Message("forbidden")
return
}
}
ginx.NewRender(c).Message(view.Update(rt.Ctx, f.Name, f.Rule, f.Cate, me.Id))
}

View File

@@ -1,210 +0,0 @@
package router
import (
"net/http"
"sort"
"strings"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func parseAggrRules(c *gin.Context) []*models.AggrRule {
aggrRules := strings.Split(ginx.QueryStr(c, "rule", ""), "::") // e.g. field:group_name::field:severity::tagkey:ident
if len(aggrRules) == 0 {
ginx.Bomb(http.StatusBadRequest, "rule empty")
}
rules := make([]*models.AggrRule, len(aggrRules))
for i := 0; i < len(aggrRules); i++ {
pair := strings.Split(aggrRules[i], ":")
if len(pair) != 2 {
ginx.Bomb(http.StatusBadRequest, "rule invalid")
}
if !(pair[0] == "field" || pair[0] == "tagkey") {
ginx.Bomb(http.StatusBadRequest, "rule invalid")
}
rules[i] = &models.AggrRule{
Type: pair[0],
Value: pair[1],
}
}
return rules
}
func (rt *Router) alertCurEventsCard(c *gin.Context) {
stime, etime := getTimeRange(c)
severity := ginx.QueryInt(c, "severity", -1)
query := ginx.QueryStr(c, "query", "")
busiGroupId := ginx.QueryInt64(c, "bgid", 0)
dsIds := queryDatasourceIds(c)
rules := parseAggrRules(c)
prod := ginx.QueryStr(c, "prods", "")
if prod == "" {
prod = ginx.QueryStr(c, "rule_prods", "")
}
prods := []string{}
if prod != "" {
prods = strings.Split(prod, ",")
}
cate := ginx.QueryStr(c, "cate", "$all")
cates := []string{}
if cate != "$all" {
cates = strings.Split(cate, ",")
}
// 最多获取50000个获取太多也没啥意义
list, err := models.AlertCurEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query, 50000, 0)
ginx.Dangerous(err)
cardmap := make(map[string]*AlertCard)
for _, event := range list {
title := event.GenCardTitle(rules)
if _, has := cardmap[title]; has {
cardmap[title].Total++
cardmap[title].EventIds = append(cardmap[title].EventIds, event.Id)
if event.Severity < cardmap[title].Severity {
cardmap[title].Severity = event.Severity
}
} else {
cardmap[title] = &AlertCard{
Total: 1,
EventIds: []int64{event.Id},
Title: title,
Severity: event.Severity,
}
}
}
titles := make([]string, 0, len(cardmap))
for title := range cardmap {
titles = append(titles, title)
}
sort.Strings(titles)
cards := make([]*AlertCard, len(titles))
for i := 0; i < len(titles); i++ {
cards[i] = cardmap[titles[i]]
}
sort.SliceStable(cards, func(i, j int) bool {
if cards[i].Severity != cards[j].Severity {
return cards[i].Severity < cards[j].Severity
}
return cards[i].Total > cards[j].Total
})
ginx.NewRender(c).Data(cards, nil)
}
type AlertCard struct {
Title string `json:"title"`
Total int `json:"total"`
EventIds []int64 `json:"event_ids"`
Severity int `json:"severity"`
}
func (rt *Router) alertCurEventsCardDetails(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
list, err := models.AlertCurEventGetByIds(rt.Ctx, f.Ids)
if err == nil {
cache := make(map[int64]*models.UserGroup)
for i := 0; i < len(list); i++ {
list[i].FillNotifyGroups(rt.Ctx, cache)
}
}
ginx.NewRender(c).Data(list, err)
}
// alertCurEventsGetByRid
func (rt *Router) alertCurEventsGetByRid(c *gin.Context) {
rid := ginx.QueryInt64(c, "rid")
dsId := ginx.QueryInt64(c, "dsid")
ginx.NewRender(c).Data(models.AlertCurEventGetByRuleIdAndDsId(rt.Ctx, rid, dsId))
}
// 列表方式,拉取活跃告警
func (rt *Router) alertCurEventsList(c *gin.Context) {
stime, etime := getTimeRange(c)
severity := ginx.QueryInt(c, "severity", -1)
query := ginx.QueryStr(c, "query", "")
limit := ginx.QueryInt(c, "limit", 20)
busiGroupId := ginx.QueryInt64(c, "bgid", 0)
dsIds := queryDatasourceIds(c)
prod := ginx.QueryStr(c, "prods", "")
if prod == "" {
prod = ginx.QueryStr(c, "rule_prods", "")
}
prods := []string{}
if prod != "" {
prods = strings.Split(prod, ",")
}
cate := ginx.QueryStr(c, "cate", "$all")
cates := []string{}
if cate != "$all" {
cates = strings.Split(cate, ",")
}
total, err := models.AlertCurEventTotal(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query)
ginx.Dangerous(err)
list, err := models.AlertCurEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query, limit, ginx.Offset(c, limit))
ginx.Dangerous(err)
cache := make(map[int64]*models.UserGroup)
for i := 0; i < len(list); i++ {
list[i].FillNotifyGroups(rt.Ctx, cache)
}
ginx.NewRender(c).Data(gin.H{
"list": list,
"total": total,
}, nil)
}
func (rt *Router) alertCurEventDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
set := make(map[int64]struct{})
for i := 0; i < len(f.Ids); i++ {
event, err := models.AlertCurEventGetById(rt.Ctx, f.Ids[i])
ginx.Dangerous(err)
if _, has := set[event.GroupId]; !has {
rt.bgrwCheck(c, event.GroupId)
set[event.GroupId] = struct{}{}
}
}
ginx.NewRender(c).Message(models.AlertCurEventDel(rt.Ctx, f.Ids))
}
func (rt *Router) alertCurEventGet(c *gin.Context) {
eid := ginx.UrlParamInt64(c, "eid")
event, err := models.AlertCurEventGetById(rt.Ctx, eid)
ginx.Dangerous(err)
if event == nil {
ginx.Bomb(404, "No such active event")
}
ginx.NewRender(c).Data(event, nil)
}

View File

@@ -1,82 +0,0 @@
package router
import (
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func getTimeRange(c *gin.Context) (stime, etime int64) {
stime = ginx.QueryInt64(c, "stime", 0)
etime = ginx.QueryInt64(c, "etime", 0)
hours := ginx.QueryInt64(c, "hours", 0)
now := time.Now().Unix()
if hours != 0 {
stime = now - 3600*hours
etime = now + 3600*24
}
if stime != 0 && etime == 0 {
etime = now + 3600*24
}
return
}
func (rt *Router) alertHisEventsList(c *gin.Context) {
stime, etime := getTimeRange(c)
severity := ginx.QueryInt(c, "severity", -1)
recovered := ginx.QueryInt(c, "is_recovered", -1)
query := ginx.QueryStr(c, "query", "")
limit := ginx.QueryInt(c, "limit", 20)
busiGroupId := ginx.QueryInt64(c, "bgid", 0)
dsIds := queryDatasourceIds(c)
prod := ginx.QueryStr(c, "prods", "")
if prod == "" {
prod = ginx.QueryStr(c, "rule_prods", "")
}
prods := []string{}
if prod != "" {
prods = strings.Split(prod, ",")
}
cate := ginx.QueryStr(c, "cate", "$all")
cates := []string{}
if cate != "$all" {
cates = strings.Split(cate, ",")
}
total, err := models.AlertHisEventTotal(rt.Ctx, prods, busiGroupId, stime, etime, severity, recovered, dsIds, cates, query)
ginx.Dangerous(err)
list, err := models.AlertHisEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, recovered, dsIds, cates, query, limit, ginx.Offset(c, limit))
ginx.Dangerous(err)
cache := make(map[int64]*models.UserGroup)
for i := 0; i < len(list); i++ {
list[i].FillNotifyGroups(rt.Ctx, cache)
}
ginx.NewRender(c).Data(gin.H{
"list": list,
"total": total,
}, nil)
}
func (rt *Router) alertHisEventGet(c *gin.Context) {
eid := ginx.UrlParamInt64(c, "eid")
event, err := models.AlertHisEventGetById(rt.Ctx, eid)
ginx.Dangerous(err)
if event == nil {
ginx.Bomb(404, "No such alert event")
}
ginx.NewRender(c).Data(event, err)
}

View File

@@ -1,273 +0,0 @@
package router
import (
"net/http"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/i18n"
)
// Return all, front-end search and paging
func (rt *Router) alertRuleGets(c *gin.Context) {
busiGroupId := ginx.UrlParamInt64(c, "id")
ars, err := models.AlertRuleGets(rt.Ctx, busiGroupId)
if err == nil {
cache := make(map[int64]*models.UserGroup)
for i := 0; i < len(ars); i++ {
ars[i].FillNotifyGroups(rt.Ctx, cache)
ars[i].FillSeverities()
}
}
ginx.NewRender(c).Data(ars, err)
}
func (rt *Router) alertRulesGetByService(c *gin.Context) {
prods := []string{}
prodStr := ginx.QueryStr(c, "prods", "")
if prodStr != "" {
prods = strings.Split(ginx.QueryStr(c, "prods", ""), ",")
}
query := ginx.QueryStr(c, "query", "")
algorithm := ginx.QueryStr(c, "algorithm", "")
cluster := ginx.QueryStr(c, "cluster", "")
cate := ginx.QueryStr(c, "cate", "$all")
cates := []string{}
if cate != "$all" {
cates = strings.Split(cate, ",")
}
disabled := ginx.QueryInt(c, "disabled", -1)
ars, err := models.AlertRulesGetsBy(rt.Ctx, prods, query, algorithm, cluster, cates, disabled)
if err == nil {
cache := make(map[int64]*models.UserGroup)
for i := 0; i < len(ars); i++ {
ars[i].FillNotifyGroups(rt.Ctx, cache)
}
}
ginx.NewRender(c).Data(ars, err)
}
// single or import
func (rt *Router) alertRuleAddByFE(c *gin.Context) {
username := c.MustGet("username").(string)
var lst []models.AlertRule
ginx.BindJSON(c, &lst)
count := len(lst)
if count == 0 {
ginx.Bomb(http.StatusBadRequest, "input json is empty")
}
bgid := ginx.UrlParamInt64(c, "id")
reterr := rt.alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language"))
ginx.NewRender(c).Data(reterr, nil)
}
func (rt *Router) alertRuleAddByImport(c *gin.Context) {
username := c.MustGet("username").(string)
var lst []models.AlertRule
ginx.BindJSON(c, &lst)
count := len(lst)
if count == 0 {
ginx.Bomb(http.StatusBadRequest, "input json is empty")
}
bgid := ginx.UrlParamInt64(c, "id")
reterr := rt.alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language"))
ginx.NewRender(c).Data(reterr, nil)
}
func (rt *Router) alertRuleAddByService(c *gin.Context) {
var lst []models.AlertRule
ginx.BindJSON(c, &lst)
count := len(lst)
if count == 0 {
ginx.Bomb(http.StatusBadRequest, "input json is empty")
}
reterr := rt.alertRuleAddForService(lst, "")
ginx.NewRender(c).Data(reterr, nil)
}
func (rt *Router) alertRuleAddForService(lst []models.AlertRule, username string) map[string]string {
count := len(lst)
// alert rule name -> error string
reterr := make(map[string]string)
for i := 0; i < count; i++ {
lst[i].Id = 0
if username != "" {
lst[i].CreateBy = username
lst[i].UpdateBy = username
}
if err := lst[i].FE2DB(); err != nil {
reterr[lst[i].Name] = err.Error()
continue
}
if err := lst[i].Add(rt.Ctx); err != nil {
reterr[lst[i].Name] = err.Error()
} else {
reterr[lst[i].Name] = ""
}
}
return reterr
}
func (rt *Router) alertRuleAdd(lst []models.AlertRule, username string, bgid int64, lang string) map[string]string {
count := len(lst)
// alert rule name -> error string
reterr := make(map[string]string)
for i := 0; i < count; i++ {
lst[i].Id = 0
lst[i].GroupId = bgid
if username != "" {
lst[i].CreateBy = username
lst[i].UpdateBy = username
}
if err := lst[i].FE2DB(); err != nil {
reterr[lst[i].Name] = i18n.Sprintf(lang, err.Error())
continue
}
if err := lst[i].Add(rt.Ctx); err != nil {
reterr[lst[i].Name] = i18n.Sprintf(lang, err.Error())
} else {
reterr[lst[i].Name] = ""
}
}
return reterr
}
func (rt *Router) alertRuleDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
// param(busiGroupId) for protect
ginx.NewRender(c).Message(models.AlertRuleDels(rt.Ctx, f.Ids, ginx.UrlParamInt64(c, "id")))
}
func (rt *Router) alertRuleDelByService(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
ginx.NewRender(c).Message(models.AlertRuleDels(rt.Ctx, f.Ids))
}
func (rt *Router) alertRulePutByFE(c *gin.Context) {
var f models.AlertRule
ginx.BindJSON(c, &f)
arid := ginx.UrlParamInt64(c, "arid")
ar, err := models.AlertRuleGetById(rt.Ctx, arid)
ginx.Dangerous(err)
if ar == nil {
ginx.NewRender(c, http.StatusNotFound).Message("No such AlertRule")
return
}
rt.bgrwCheck(c, ar.GroupId)
f.UpdateBy = c.MustGet("username").(string)
ginx.NewRender(c).Message(ar.Update(rt.Ctx, f))
}
func (rt *Router) alertRulePutByService(c *gin.Context) {
var f models.AlertRule
ginx.BindJSON(c, &f)
arid := ginx.UrlParamInt64(c, "arid")
ar, err := models.AlertRuleGetById(rt.Ctx, arid)
ginx.Dangerous(err)
if ar == nil {
ginx.NewRender(c, http.StatusNotFound).Message("No such AlertRule")
return
}
ginx.NewRender(c).Message(ar.Update(rt.Ctx, f))
}
type alertRuleFieldForm struct {
Ids []int64 `json:"ids"`
Fields map[string]interface{} `json:"fields"`
Action string `json:"action"`
}
// update one field: cluster note severity disabled prom_eval_interval prom_for_duration notify_channels notify_groups notify_recovered notify_repeat_step callbacks runbook_url append_tags
func (rt *Router) alertRulePutFields(c *gin.Context) {
var f alertRuleFieldForm
ginx.BindJSON(c, &f)
if len(f.Fields) == 0 {
ginx.Bomb(http.StatusBadRequest, "fields empty")
}
f.Fields["update_by"] = c.MustGet("username").(string)
f.Fields["update_at"] = time.Now().Unix()
for i := 0; i < len(f.Ids); i++ {
ar, err := models.AlertRuleGetById(rt.Ctx, f.Ids[i])
ginx.Dangerous(err)
if ar == nil {
continue
}
if f.Action == "callback_add" {
// 增加一个 callback 地址
if callbacks, has := f.Fields["callbacks"]; has {
callback := callbacks.(string)
if !strings.Contains(ar.Callbacks, callback) {
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, map[string]interface{}{"callbacks": ar.Callbacks + " " + callback}))
continue
}
}
}
if f.Action == "callback_del" {
// 删除一个 callback 地址
if callbacks, has := f.Fields["callbacks"]; has {
callback := callbacks.(string)
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, map[string]interface{}{"callbacks": strings.ReplaceAll(ar.Callbacks, callback, "")}))
continue
}
}
for k, v := range f.Fields {
ginx.Dangerous(ar.UpdateColumn(rt.Ctx, k, v))
}
}
ginx.NewRender(c).Message(nil)
}
func (rt *Router) alertRuleGet(c *gin.Context) {
arid := ginx.UrlParamInt64(c, "arid")
ar, err := models.AlertRuleGetById(rt.Ctx, arid)
ginx.Dangerous(err)
if ar == nil {
ginx.NewRender(c, http.StatusNotFound).Message("No such AlertRule")
return
}
err = ar.FillNotifyGroups(rt.Ctx, make(map[int64]*models.UserGroup))
ginx.Dangerous(err)
ginx.NewRender(c).Data(ar, err)
}

View File

@@ -1,117 +0,0 @@
package router
import (
"net/http"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
// Return all, front-end search and paging
func (rt *Router) alertSubscribeGets(c *gin.Context) {
bgid := ginx.UrlParamInt64(c, "id")
lst, err := models.AlertSubscribeGets(rt.Ctx, bgid)
if err == nil {
ugcache := make(map[int64]*models.UserGroup)
for i := 0; i < len(lst); i++ {
ginx.Dangerous(lst[i].FillUserGroups(rt.Ctx, ugcache))
}
rulecache := make(map[int64]string)
for i := 0; i < len(lst); i++ {
ginx.Dangerous(lst[i].FillRuleName(rt.Ctx, rulecache))
}
for i := 0; i < len(lst); i++ {
ginx.Dangerous(lst[i].FillDatasourceIds(rt.Ctx))
}
}
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) alertSubscribeGet(c *gin.Context) {
subid := ginx.UrlParamInt64(c, "sid")
sub, err := models.AlertSubscribeGet(rt.Ctx, "id=?", subid)
ginx.Dangerous(err)
if sub == nil {
ginx.NewRender(c, 404).Message("No such alert subscribe")
return
}
ugcache := make(map[int64]*models.UserGroup)
ginx.Dangerous(sub.FillUserGroups(rt.Ctx, ugcache))
rulecache := make(map[int64]string)
ginx.Dangerous(sub.FillRuleName(rt.Ctx, rulecache))
ginx.Dangerous(sub.FillDatasourceIds(rt.Ctx))
ginx.Dangerous(sub.DB2FE())
ginx.NewRender(c).Data(sub, nil)
}
func (rt *Router) alertSubscribeAdd(c *gin.Context) {
var f models.AlertSubscribe
ginx.BindJSON(c, &f)
username := c.MustGet("username").(string)
f.CreateBy = username
f.UpdateBy = username
f.GroupId = ginx.UrlParamInt64(c, "id")
if f.GroupId <= 0 {
ginx.Bomb(http.StatusBadRequest, "group_id invalid")
}
ginx.NewRender(c).Message(f.Add(rt.Ctx))
}
func (rt *Router) alertSubscribePut(c *gin.Context) {
var fs []models.AlertSubscribe
ginx.BindJSON(c, &fs)
timestamp := time.Now().Unix()
username := c.MustGet("username").(string)
for i := 0; i < len(fs); i++ {
fs[i].UpdateBy = username
fs[i].UpdateAt = timestamp
ginx.Dangerous(fs[i].Update(
rt.Ctx,
"name",
"disabled",
"cluster",
"rule_id",
"tags",
"redefine_severity",
"new_severity",
"redefine_channels",
"new_channels",
"user_group_ids",
"update_at",
"update_by",
"webhooks",
"for_duration",
"redefine_webhooks",
"datasource_ids",
))
}
ginx.NewRender(c).Message(nil)
}
func (rt *Router) alertSubscribeDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
ginx.NewRender(c).Message(models.AlertSubscribeDel(rt.Ctx, f.Ids))
}
func (rt *Router) alertSubscribeGetsByService(c *gin.Context) {
lst, err := models.AlertSubscribeGetsByService(rt.Ctx)
ginx.NewRender(c).Data(lst, err)
}

View File

@@ -1,237 +0,0 @@
package router
import (
"net/http"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/toolkits/pkg/ginx"
)
type boardForm struct {
Name string `json:"name"`
Ident string `json:"ident"`
Tags string `json:"tags"`
Configs string `json:"configs"`
Public int `json:"public"`
}
func (rt *Router) boardAdd(c *gin.Context) {
var f boardForm
ginx.BindJSON(c, &f)
me := c.MustGet("user").(*models.User)
board := &models.Board{
GroupId: ginx.UrlParamInt64(c, "id"),
Name: f.Name,
Ident: f.Ident,
Tags: f.Tags,
Configs: f.Configs,
CreateBy: me.Username,
UpdateBy: me.Username,
}
err := board.Add(rt.Ctx)
ginx.Dangerous(err)
if f.Configs != "" {
ginx.Dangerous(models.BoardPayloadSave(rt.Ctx, board.Id, f.Configs))
}
ginx.NewRender(c).Data(board, nil)
}
func (rt *Router) boardGet(c *gin.Context) {
bid := ginx.UrlParamStr(c, "bid")
board, err := models.BoardGet(rt.Ctx, "id = ? or ident = ?", bid, bid)
ginx.Dangerous(err)
if board == nil {
ginx.Bomb(http.StatusNotFound, "No such dashboard")
}
if board.Public == 0 {
rt.auth()(c)
rt.user()(c)
me := c.MustGet("user").(*models.User)
if !me.IsAdmin() {
// check permission
rt.bgroCheck(c, board.GroupId)
}
}
ginx.NewRender(c).Data(board, nil)
}
func (rt *Router) boardPureGet(c *gin.Context) {
board, err := models.BoardGetByID(rt.Ctx, ginx.UrlParamInt64(c, "bid"))
ginx.Dangerous(err)
if board == nil {
ginx.Bomb(http.StatusNotFound, "No such dashboard")
}
ginx.NewRender(c).Data(board, nil)
}
// bgrwCheck
func (rt *Router) boardDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
for i := 0; i < len(f.Ids); i++ {
bid := f.Ids[i]
board, err := models.BoardGet(rt.Ctx, "id = ?", bid)
ginx.Dangerous(err)
if board == nil {
continue
}
me := c.MustGet("user").(*models.User)
if !me.IsAdmin() {
// check permission
rt.bgrwCheck(c, board.GroupId)
}
ginx.Dangerous(board.Del(rt.Ctx))
}
ginx.NewRender(c).Message(nil)
}
func (rt *Router) Board(id int64) *models.Board {
obj, err := models.BoardGet(rt.Ctx, "id=?", id)
ginx.Dangerous(err)
if obj == nil {
ginx.Bomb(http.StatusNotFound, "No such dashboard")
}
return obj
}
// bgrwCheck
func (rt *Router) boardPut(c *gin.Context) {
var f boardForm
ginx.BindJSON(c, &f)
me := c.MustGet("user").(*models.User)
bo := rt.Board(ginx.UrlParamInt64(c, "bid"))
if !me.IsAdmin() {
// check permission
rt.bgrwCheck(c, bo.GroupId)
}
can, err := bo.CanRenameIdent(rt.Ctx, f.Ident)
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusOK, "Ident duplicate")
}
bo.Name = f.Name
bo.Ident = f.Ident
bo.Tags = f.Tags
bo.UpdateBy = me.Username
bo.UpdateAt = time.Now().Unix()
err = bo.Update(rt.Ctx, "name", "ident", "tags", "update_by", "update_at")
ginx.NewRender(c).Data(bo, err)
}
// bgrwCheck
func (rt *Router) boardPutConfigs(c *gin.Context) {
var f boardForm
ginx.BindJSON(c, &f)
me := c.MustGet("user").(*models.User)
bid := ginx.UrlParamStr(c, "bid")
bo, err := models.BoardGet(rt.Ctx, "id = ? or ident = ?", bid, bid)
ginx.Dangerous(err)
if bo == nil {
ginx.Bomb(http.StatusNotFound, "No such dashboard")
}
// check permission
if !me.IsAdmin() {
rt.bgrwCheck(c, bo.GroupId)
}
bo.UpdateBy = me.Username
bo.UpdateAt = time.Now().Unix()
ginx.Dangerous(bo.Update(rt.Ctx, "update_by", "update_at"))
bo.Configs = f.Configs
ginx.Dangerous(models.BoardPayloadSave(rt.Ctx, bo.Id, f.Configs))
ginx.NewRender(c).Data(bo, nil)
}
// bgrwCheck
func (rt *Router) boardPutPublic(c *gin.Context) {
var f boardForm
ginx.BindJSON(c, &f)
me := c.MustGet("user").(*models.User)
bo := rt.Board(ginx.UrlParamInt64(c, "bid"))
// check permission
if !me.IsAdmin() {
rt.bgrwCheck(c, bo.GroupId)
}
bo.Public = f.Public
bo.UpdateBy = me.Username
bo.UpdateAt = time.Now().Unix()
err := bo.Update(rt.Ctx, "public", "update_by", "update_at")
ginx.NewRender(c).Data(bo, err)
}
func (rt *Router) boardGets(c *gin.Context) {
bgid := ginx.UrlParamInt64(c, "id")
query := ginx.QueryStr(c, "query", "")
boards, err := models.BoardGetsByGroupId(rt.Ctx, bgid, query)
ginx.NewRender(c).Data(boards, err)
}
func (rt *Router) boardClone(c *gin.Context) {
me := c.MustGet("user").(*models.User)
bo := rt.Board(ginx.UrlParamInt64(c, "bid"))
newBoard := &models.Board{
Name: bo.Name + " Copy",
Tags: bo.Tags,
GroupId: bo.GroupId,
CreateBy: me.Username,
UpdateBy: me.Username,
}
if bo.Ident != "" {
newBoard.Ident = uuid.NewString()
}
ginx.Dangerous(newBoard.Add(rt.Ctx))
// clone payload
payload, err := models.BoardPayloadGet(rt.Ctx, bo.Id)
ginx.Dangerous(err)
if payload != "" {
ginx.Dangerous(models.BoardPayloadSave(rt.Ctx, newBoard.Id, payload))
}
ginx.NewRender(c).Message(nil)
}

View File

@@ -1,311 +0,0 @@
package router
import (
"encoding/json"
"fmt"
"net/http"
"path"
"strings"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/file"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/runner"
)
// 创建 builtin_cate
func (rt *Router) builtinCateFavoriteAdd(c *gin.Context) {
var f models.BuiltinCate
ginx.BindJSON(c, &f)
if f.Name == "" {
ginx.Bomb(http.StatusBadRequest, "name is empty")
}
me := c.MustGet("user").(*models.User)
f.UserId = me.Id
ginx.NewRender(c).Message(f.Create(rt.Ctx))
}
// 删除 builtin_cate
func (rt *Router) builtinCateFavoriteDel(c *gin.Context) {
name := ginx.UrlParamStr(c, "name")
me := c.MustGet("user").(*models.User)
ginx.NewRender(c).Message(models.BuiltinCateDelete(rt.Ctx, name, me.Id))
}
type Payload struct {
Cate string `json:"cate"`
Fname string `json:"fname"`
Name string `json:"name"`
Configs interface{} `json:"configs"`
Tags string `json:"tags"`
}
type BoardCate struct {
Name string `json:"name"`
IconUrl string `json:"icon_url"`
Boards []Payload `json:"boards"`
Favorite bool `json:"favorite"`
}
func (rt *Router) builtinBoardDetailGets(c *gin.Context) {
var payload Payload
ginx.BindJSON(c, &payload)
fp := rt.Center.BuiltinIntegrationsDir
if fp == "" {
fp = path.Join(runner.Cwd, "integrations")
}
fn := fp + "/" + payload.Cate + "/dashboards/" + payload.Fname
content, err := file.ReadBytes(fn)
ginx.Dangerous(err)
err = json.Unmarshal(content, &payload)
ginx.NewRender(c).Data(payload, err)
}
func (rt *Router) builtinBoardCateGets(c *gin.Context) {
fp := rt.Center.BuiltinIntegrationsDir
if fp == "" {
fp = path.Join(runner.Cwd, "integrations")
}
me := c.MustGet("user").(*models.User)
buildinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
if err != nil {
logger.Warningf("get builtin favorites fail: %v", err)
}
var boardCates []BoardCate
dirList, err := file.DirsUnder(fp)
ginx.Dangerous(err)
for _, dir := range dirList {
var boardCate BoardCate
boardCate.Name = dir
files, err := file.FilesUnder(fp + "/" + dir + "/dashboards")
ginx.Dangerous(err)
var boards []Payload
for _, f := range files {
fn := fp + "/" + dir + "/dashboards/" + f
content, err := file.ReadBytes(fn)
if err != nil {
logger.Warningf("add board fail: %v", err)
continue
}
var payload Payload
err = json.Unmarshal(content, &payload)
if err != nil {
logger.Warningf("add board:%s fail: %v", fn, err)
continue
}
payload.Cate = dir
payload.Fname = f
payload.Configs = ""
boards = append(boards, payload)
}
boardCate.Boards = boards
if _, ok := buildinFavoritesMap[dir]; ok {
boardCate.Favorite = true
}
iconFiles, _ := file.FilesUnder(fp + "/" + dir + "/icon")
if len(iconFiles) > 0 {
boardCate.IconUrl = fmt.Sprintf("/api/n9e/integrations/icon/%s/%s", dir, iconFiles[0])
}
boardCates = append(boardCates, boardCate)
}
ginx.NewRender(c).Data(boardCates, nil)
}
func (rt *Router) builtinBoardGets(c *gin.Context) {
fp := rt.Center.BuiltinIntegrationsDir
if fp == "" {
fp = path.Join(runner.Cwd, "integrations")
}
var fileList []string
dirList, err := file.DirsUnder(fp)
ginx.Dangerous(err)
for _, dir := range dirList {
files, err := file.FilesUnder(fp + "/" + dir + "/dashboards")
ginx.Dangerous(err)
fileList = append(fileList, files...)
}
names := make([]string, 0, len(fileList))
for _, f := range fileList {
if !strings.HasSuffix(f, ".json") {
continue
}
name := strings.TrimSuffix(f, ".json")
names = append(names, name)
}
ginx.NewRender(c).Data(names, nil)
}
type AlertCate struct {
Name string `json:"name"`
IconUrl string `json:"icon_url"`
AlertRules []models.AlertRule `json:"alert_rules"`
Favorite bool `json:"favorite"`
}
func (rt *Router) builtinAlertCateGets(c *gin.Context) {
fp := rt.Center.BuiltinIntegrationsDir
if fp == "" {
fp = path.Join(runner.Cwd, "integrations")
}
me := c.MustGet("user").(*models.User)
buildinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
if err != nil {
logger.Warningf("get builtin favorites fail: %v", err)
}
var alertCates []AlertCate
dirList, err := file.DirsUnder(fp)
ginx.Dangerous(err)
for _, dir := range dirList {
var alertCate AlertCate
alertCate.Name = dir
files, err := file.FilesUnder(fp + "/" + dir + "/alerts")
ginx.Dangerous(err)
var alertRules []models.AlertRule
for _, f := range files {
fn := fp + "/" + dir + "/alerts/" + f
content, err := file.ReadBytes(fn)
if err != nil {
logger.Warningf("add board fail: %v", err)
continue
}
var ars []models.AlertRule
err = json.Unmarshal(content, &ars)
if err != nil {
logger.Warningf("add board:%s fail: %v", fn, err)
continue
}
alertRules = append(alertRules, ars...)
}
alertCate.AlertRules = alertRules
iconFiles, _ := file.FilesUnder(fp + "/" + dir + "/icon")
if len(iconFiles) > 0 {
alertCate.IconUrl = fmt.Sprintf("/api/n9e/integrations/icon/%s/%s", dir, iconFiles[0])
}
if _, ok := buildinFavoritesMap[dir]; ok {
alertCate.Favorite = true
}
alertCates = append(alertCates, alertCate)
}
ginx.NewRender(c).Data(alertCates, nil)
}
type builtinAlertRulesList struct {
Name string `json:"name"`
IconUrl string `json:"icon_url"`
AlertRules map[string][]models.AlertRule `json:"alert_rules"`
Favorite bool `json:"favorite"`
}
func (rt *Router) builtinAlertRules(c *gin.Context) {
fp := rt.Center.BuiltinIntegrationsDir
if fp == "" {
fp = path.Join(runner.Cwd, "integrations")
}
me := c.MustGet("user").(*models.User)
buildinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
if err != nil {
logger.Warningf("get builtin favorites fail: %v", err)
}
var alertCates []builtinAlertRulesList
dirList, err := file.DirsUnder(fp)
ginx.Dangerous(err)
for _, dir := range dirList {
var alertCate builtinAlertRulesList
alertCate.Name = dir
files, err := file.FilesUnder(fp + "/" + dir + "/alerts")
ginx.Dangerous(err)
alertRules := make(map[string][]models.AlertRule)
for _, f := range files {
fn := fp + "/" + dir + "/alerts/" + f
content, err := file.ReadBytes(fn)
if err != nil {
logger.Warningf("add board fail: %v", err)
continue
}
var ars []models.AlertRule
err = json.Unmarshal(content, &ars)
if err != nil {
logger.Warningf("add board:%s fail: %v", fn, err)
continue
}
alertRules[strings.TrimSuffix(f, ".json")] = ars
}
alertCate.AlertRules = alertRules
iconFiles, _ := file.FilesUnder(fp + "/" + dir + "/icon")
if len(iconFiles) > 0 {
alertCate.IconUrl = fmt.Sprintf("/api/n9e/integrations/icon/%s/%s", dir, iconFiles[0])
}
if _, ok := buildinFavoritesMap[dir]; ok {
alertCate.Favorite = true
}
alertCates = append(alertCates, alertCate)
}
ginx.NewRender(c).Data(alertCates, nil)
}
// read the json file content
func (rt *Router) builtinBoardGet(c *gin.Context) {
name := ginx.UrlParamStr(c, "name")
dirpath := rt.Center.BuiltinIntegrationsDir
if dirpath == "" {
dirpath = path.Join(runner.Cwd, "integrations")
}
dirList, err := file.DirsUnder(dirpath)
ginx.Dangerous(err)
for _, dir := range dirList {
jsonFile := dirpath + "/" + dir + "/dashboards/" + name + ".json"
if file.IsExist(jsonFile) {
body, err := file.ReadString(jsonFile)
ginx.NewRender(c).Data(body, err)
return
}
}
ginx.Bomb(http.StatusBadRequest, "%s not found", name)
}
func (rt *Router) builtinIcon(c *gin.Context) {
fp := rt.Center.BuiltinIntegrationsDir
if fp == "" {
fp = path.Join(runner.Cwd, "integrations")
}
cate := ginx.UrlParamStr(c, "cate")
iconPath := fp + "/" + cate + "/icon/" + ginx.UrlParamStr(c, "name")
c.File(path.Join(iconPath))
}

View File

@@ -1,142 +0,0 @@
package router
import (
"net/http"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/str"
)
type busiGroupForm struct {
Name string `json:"name" binding:"required"`
LabelEnable int `json:"label_enable"`
LabelValue string `json:"label_value"`
Members []models.BusiGroupMember `json:"members"`
}
func (rt *Router) busiGroupAdd(c *gin.Context) {
var f busiGroupForm
ginx.BindJSON(c, &f)
if len(f.Members) == 0 {
ginx.Bomb(http.StatusBadRequest, "members empty")
}
rwhas := false
for i := 0; i < len(f.Members); i++ {
if f.Members[i].PermFlag == "rw" {
rwhas = true
break
}
}
if !rwhas {
ginx.Bomb(http.StatusBadRequest, "At least one team have rw permission")
}
username := c.MustGet("username").(string)
ginx.Dangerous(models.BusiGroupAdd(rt.Ctx, f.Name, f.LabelEnable, f.LabelValue, f.Members, username))
// 如果创建成功拿着name去查应该可以查到
newbg, err := models.BusiGroupGet(rt.Ctx, "name=?", f.Name)
ginx.Dangerous(err)
if newbg == nil {
ginx.NewRender(c).Message("Failed to create BusiGroup(%s)", f.Name)
return
}
ginx.NewRender(c).Data(newbg.Id, nil)
}
func (rt *Router) busiGroupPut(c *gin.Context) {
var f busiGroupForm
ginx.BindJSON(c, &f)
username := c.MustGet("username").(string)
targetbg := c.MustGet("busi_group").(*models.BusiGroup)
ginx.NewRender(c).Message(targetbg.Update(rt.Ctx, f.Name, f.LabelEnable, f.LabelValue, username))
}
func (rt *Router) busiGroupMemberAdd(c *gin.Context) {
var members []models.BusiGroupMember
ginx.BindJSON(c, &members)
username := c.MustGet("username").(string)
targetbg := c.MustGet("busi_group").(*models.BusiGroup)
for i := 0; i < len(members); i++ {
if members[i].BusiGroupId != targetbg.Id {
ginx.Bomb(http.StatusBadRequest, "business group id invalid")
}
}
ginx.NewRender(c).Message(targetbg.AddMembers(rt.Ctx, members, username))
}
func (rt *Router) busiGroupMemberDel(c *gin.Context) {
var members []models.BusiGroupMember
ginx.BindJSON(c, &members)
username := c.MustGet("username").(string)
targetbg := c.MustGet("busi_group").(*models.BusiGroup)
for i := 0; i < len(members); i++ {
if members[i].BusiGroupId != targetbg.Id {
ginx.Bomb(http.StatusBadRequest, "business group id invalid")
}
}
ginx.NewRender(c).Message(targetbg.DelMembers(rt.Ctx, members, username))
}
func (rt *Router) busiGroupDel(c *gin.Context) {
username := c.MustGet("username").(string)
targetbg := c.MustGet("busi_group").(*models.BusiGroup)
err := targetbg.Del(rt.Ctx)
if err != nil {
logger.Infof("busi_group_delete fail: operator=%s, group_name=%s error=%v", username, targetbg.Name, err)
} else {
logger.Infof("busi_group_delete succ: operator=%s, group_name=%s", username, targetbg.Name)
}
ginx.NewRender(c).Message(err)
}
// 我是超管、或者我是业务组成员
func (rt *Router) busiGroupGets(c *gin.Context) {
limit := ginx.QueryInt(c, "limit", defaultLimit)
query := ginx.QueryStr(c, "query", "")
all := ginx.QueryBool(c, "all", false)
me := c.MustGet("user").(*models.User)
lst, err := me.BusiGroups(rt.Ctx, limit, query, all)
if len(lst) == 0 {
lst = []models.BusiGroup{}
}
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) busiGroupGetsByService(c *gin.Context) {
lst, err := models.BusiGroupGetAll(rt.Ctx)
ginx.NewRender(c).Data(lst, err)
}
// 这个接口只有在活跃告警页面才调用获取各个BG的活跃告警数量
func (rt *Router) busiGroupAlertingsGets(c *gin.Context) {
ids := ginx.QueryStr(c, "ids", "")
ret, err := models.AlertNumbers(rt.Ctx, str.IdsInt64(ids))
ginx.NewRender(c).Data(ret, err)
}
func (rt *Router) busiGroupGet(c *gin.Context) {
bg := BusiGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
ginx.Dangerous(bg.FillUserGroups(rt.Ctx))
ginx.NewRender(c).Data(bg, nil)
}

View File

@@ -1,45 +0,0 @@
package router
import (
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/str"
)
func (rt *Router) chartShareGets(c *gin.Context) {
ids := ginx.QueryStr(c, "ids", "")
lst, err := models.ChartShareGetsByIds(rt.Ctx, str.IdsInt64(ids, ","))
ginx.NewRender(c).Data(lst, err)
}
type chartShareForm struct {
DatasourceId int64 `json:"datasource_id"`
Configs string `json:"configs"`
}
func (rt *Router) chartShareAdd(c *gin.Context) {
username := c.MustGet("username").(string)
var forms []chartShareForm
ginx.BindJSON(c, &forms)
ids := []int64{}
now := time.Now().Unix()
for _, f := range forms {
chart := models.ChartShare{
DatasourceId: f.DatasourceId,
Configs: f.Configs,
CreateBy: username,
CreateAt: now,
}
ginx.Dangerous(chart.Add(rt.Ctx))
ids = append(ids, chart.Id)
}
ginx.NewRender(c).Data(ids, nil)
}

View File

@@ -1,64 +0,0 @@
package router
import (
"encoding/json"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) notifyChannelsGets(c *gin.Context) {
var labelAndKeys []models.LabelAndKey
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYCHANNEL)
ginx.Dangerous(err)
if cval == "" {
ginx.NewRender(c).Data(labelAndKeys, nil)
return
}
var notifyChannels []models.NotifyChannel
err = json.Unmarshal([]byte(cval), &notifyChannels)
ginx.Dangerous(err)
for _, v := range notifyChannels {
if v.Hide {
continue
}
var labelAndKey models.LabelAndKey
labelAndKey.Label = v.Name
labelAndKey.Key = v.Ident
labelAndKeys = append(labelAndKeys, labelAndKey)
}
ginx.NewRender(c).Data(labelAndKeys, nil)
}
func (rt *Router) contactKeysGets(c *gin.Context) {
var labelAndKeys []models.LabelAndKey
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYCONTACT)
ginx.Dangerous(err)
if cval == "" {
ginx.NewRender(c).Data(labelAndKeys, nil)
return
}
var notifyContacts []models.NotifyContact
err = json.Unmarshal([]byte(cval), &notifyContacts)
ginx.Dangerous(err)
for _, v := range notifyContacts {
if v.Hide {
continue
}
var labelAndKey models.LabelAndKey
labelAndKey.Label = v.Name
labelAndKey.Key = v.Ident
labelAndKeys = append(labelAndKeys, labelAndKey)
}
ginx.NewRender(c).Data(labelAndKeys, nil)
}

View File

@@ -1,54 +0,0 @@
package router
import (
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) configsGet(c *gin.Context) {
prefix := ginx.QueryStr(c, "prefix", "")
limit := ginx.QueryInt(c, "limit", 10)
configs, err := models.ConfigsGets(rt.Ctx, prefix, limit, ginx.Offset(c, limit))
ginx.NewRender(c).Data(configs, err)
}
func (rt *Router) configGet(c *gin.Context) {
id := ginx.UrlParamInt64(c, "id")
configs, err := models.ConfigGet(rt.Ctx, id)
ginx.NewRender(c).Data(configs, err)
}
func (rt *Router) configGetByKey(c *gin.Context) {
config, err := models.ConfigsGet(rt.Ctx, ginx.QueryStr(c, "key"))
ginx.NewRender(c).Data(config, err)
}
func (rt *Router) configsDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
ginx.NewRender(c).Message(models.ConfigsDel(rt.Ctx, f.Ids))
}
func (rt *Router) configsPut(c *gin.Context) {
var arr []models.Configs
ginx.BindJSON(c, &arr)
for i := 0; i < len(arr); i++ {
ginx.Dangerous(arr[i].Update(rt.Ctx))
}
ginx.NewRender(c).Message(nil)
}
func (rt *Router) configsPost(c *gin.Context) {
var arr []models.Configs
ginx.BindJSON(c, &arr)
for i := 0; i < len(arr); i++ {
ginx.Dangerous(arr[i].Add(rt.Ctx))
}
ginx.NewRender(c).Message(nil)
}

View File

@@ -1,63 +0,0 @@
package router
import (
"github.com/ccfos/nightingale/v6/pkg/secu"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
type confPropCrypto struct {
Data string `json:"data" binding:"required"`
Key string `json:"key" binding:"required"`
}
func (rt *Router) confPropEncrypt(c *gin.Context) {
var f confPropCrypto
ginx.BindJSON(c, &f)
k := len(f.Key)
switch k {
default:
c.String(400, "The key length should be 16, 24 or 32")
return
case 16, 24, 32:
break
}
s, err := secu.DealWithEncrypt(f.Data, f.Key)
if err != nil {
c.String(500, err.Error())
}
c.JSON(200, gin.H{
"src": f.Data,
"key": f.Key,
"encrypt": s,
})
}
func (rt *Router) confPropDecrypt(c *gin.Context) {
var f confPropCrypto
ginx.BindJSON(c, &f)
k := len(f.Key)
switch k {
default:
c.String(400, "The key length should be 16, 24 or 32")
return
case 16, 24, 32:
break
}
s, err := secu.DealWithDecrypt(f.Data, f.Key)
if err != nil {
c.String(500, err.Error())
}
c.JSON(200, gin.H{
"src": f.Data,
"key": f.Key,
"decrypt": s,
})
}

View File

@@ -1,19 +0,0 @@
package router
type ChartPure struct {
Configs string `json:"configs"`
Weight int `json:"weight"`
}
type ChartGroupPure struct {
Name string `json:"name"`
Weight int `json:"weight"`
Charts []ChartPure `json:"charts"`
}
type DashboardPure struct {
Name string `json:"name"`
Tags string `json:"tags"`
Configs string `json:"configs"`
ChartGroups []ChartGroupPure `json:"chart_groups"`
}

View File

@@ -1,198 +0,0 @@
package router
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
func (rt *Router) pluginList(c *gin.Context) {
Render(c, rt.Center.Plugins, nil)
}
type listReq struct {
Name string `json:"name"`
Type string `json:"plugin_type"`
Category string `json:"category"`
}
func (rt *Router) datasourceList(c *gin.Context) {
var req listReq
ginx.BindJSON(c, &req)
typ := req.Type
category := req.Category
name := req.Name
list, err := models.GetDatasourcesGetsBy(rt.Ctx, typ, category, name, "")
Render(c, list, err)
}
func (rt *Router) datasourceGetsByService(c *gin.Context) {
typ := ginx.QueryStr(c, "typ", "")
lst, err := models.GetDatasourcesGetsBy(rt.Ctx, typ, "", "", "")
ginx.NewRender(c).Data(lst, err)
}
type datasourceBrief struct {
Id int64 `json:"id"`
Name string `json:"name"`
PluginType string `json:"plugin_type"`
}
func (rt *Router) datasourceBriefs(c *gin.Context) {
var dss []datasourceBrief
list, err := models.GetDatasourcesGetsBy(rt.Ctx, "", "", "", "")
ginx.Dangerous(err)
for i := range list {
dss = append(dss, datasourceBrief{
Id: list[i].Id,
Name: list[i].Name,
PluginType: list[i].PluginType,
})
}
ginx.NewRender(c).Data(dss, err)
}
func (rt *Router) datasourceUpsert(c *gin.Context) {
var req models.Datasource
ginx.BindJSON(c, &req)
username := Username(c)
req.UpdatedBy = username
var err error
var count int64
err = DatasourceCheck(req)
if err != nil {
Dangerous(c, err)
return
}
if req.Id == 0 {
req.CreatedBy = username
req.Status = "enabled"
count, err = models.GetDatasourcesCountBy(rt.Ctx, "", "", req.Name)
if err != nil {
Render(c, nil, err)
return
}
if count > 0 {
Render(c, nil, "name already exists")
return
}
err = req.Add(rt.Ctx)
} else {
err = req.Update(rt.Ctx, "name", "description", "cluster_name", "settings", "http", "auth", "updated_by", "updated_at")
}
Render(c, nil, err)
}
func DatasourceCheck(ds models.Datasource) error {
if ds.HTTPJson.Url == "" {
return fmt.Errorf("url is empty")
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: ds.HTTPJson.TLS.SkipTlsVerify,
},
},
}
fullURL := ds.HTTPJson.Url
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
logger.Errorf("Error creating request: %v", err)
return fmt.Errorf("request url:%s failed", fullURL)
}
if ds.PluginType == models.PROMETHEUS {
subPath := "/api/v1/query"
query := url.Values{}
if strings.Contains(fullURL, "loki") {
subPath = "/api/v1/labels"
query.Add("start", "1")
query.Add("end", "2")
} else {
query.Add("query", "1+1")
}
fullURL = fmt.Sprintf("%s%s?%s", ds.HTTPJson.Url, subPath, query.Encode())
req, err = http.NewRequest("POST", fullURL, nil)
if err != nil {
logger.Errorf("Error creating request: %v", err)
return fmt.Errorf("request url:%s failed", fullURL)
}
}
if ds.AuthJson.BasicAuthUser != "" {
req.SetBasicAuth(ds.AuthJson.BasicAuthUser, ds.AuthJson.BasicAuthPassword)
}
for k, v := range ds.HTTPJson.Headers {
req.Header.Set(k, v)
}
resp, err := client.Do(req)
if err != nil {
logger.Errorf("Error making request: %v\n", err)
return fmt.Errorf("request url:%s failed", fullURL)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
logger.Errorf("Error making request: %v\n", resp.StatusCode)
return fmt.Errorf("request url:%s failed code:%d", fullURL, resp.StatusCode)
}
return nil
}
func (rt *Router) datasourceGet(c *gin.Context) {
var req models.Datasource
ginx.BindJSON(c, &req)
err := req.Get(rt.Ctx)
Render(c, req, err)
}
func (rt *Router) datasourceUpdataStatus(c *gin.Context) {
var req models.Datasource
ginx.BindJSON(c, &req)
username := Username(c)
req.UpdatedBy = username
err := req.Update(rt.Ctx, "status", "updated_by", "updated_at")
Render(c, req, err)
}
func (rt *Router) datasourceDel(c *gin.Context) {
var ids []int64
ginx.BindJSON(c, &ids)
err := models.DatasourceDel(rt.Ctx, ids)
Render(c, nil, err)
}
func (rt *Router) getDatasourceIds(c *gin.Context) {
name := ginx.QueryStr(c, "name")
datasourceIds, err := models.GetDatasourceIdsByEngineName(rt.Ctx, name)
ginx.NewRender(c).Data(datasourceIds, err)
}
func Username(c *gin.Context) string {
return c.MustGet("username").(string)
}

View File

@@ -1,156 +0,0 @@
package router
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/ibex"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
const defaultLimit = 300
func (rt *Router) statistic(c *gin.Context) {
name := ginx.QueryStr(c, "name")
var model interface{}
var err error
var statistics *models.Statistics
switch name {
case "alert_mute":
model = models.AlertMute{}
case "alert_rule":
model = models.AlertRule{}
case "alert_subscribe":
model = models.AlertSubscribe{}
case "busi_group":
model = models.BusiGroup{}
case "recording_rule":
model = models.RecordingRule{}
case "target":
model = models.Target{}
case "user":
model = models.User{}
case "user_group":
model = models.UserGroup{}
case "datasource":
// datasource update_at is different from others
statistics, err = models.DatasourceStatistics(rt.Ctx)
ginx.NewRender(c).Data(statistics, err)
return
default:
ginx.Bomb(http.StatusBadRequest, "invalid name")
}
statistics, err = models.StatisticsGet(rt.Ctx, model)
ginx.NewRender(c).Data(statistics, err)
}
func queryDatasourceIds(c *gin.Context) []int64 {
datasourceIds := ginx.QueryStr(c, "datasource_ids", "")
datasourceIds = strings.ReplaceAll(datasourceIds, ",", " ")
idsStr := strings.Fields(datasourceIds)
ids := make([]int64, len(idsStr))
for i, idStr := range idsStr {
id, _ := strconv.ParseInt(idStr, 10, 64)
ids[i] = id
}
return ids
}
type idsForm struct {
Ids []int64 `json:"ids"`
}
func (f idsForm) Verify() {
if len(f.Ids) == 0 {
ginx.Bomb(http.StatusBadRequest, "ids empty")
}
}
func User(ctx *ctx.Context, id int64) *models.User {
obj, err := models.UserGetById(ctx, id)
ginx.Dangerous(err)
if obj == nil {
ginx.Bomb(http.StatusNotFound, "No such user")
}
return obj
}
func UserGroup(ctx *ctx.Context, id int64) *models.UserGroup {
obj, err := models.UserGroupGetById(ctx, id)
ginx.Dangerous(err)
if obj == nil {
ginx.Bomb(http.StatusNotFound, "No such UserGroup")
}
return obj
}
func BusiGroup(ctx *ctx.Context, id int64) *models.BusiGroup {
obj, err := models.BusiGroupGetById(ctx, id)
ginx.Dangerous(err)
if obj == nil {
ginx.Bomb(http.StatusNotFound, "No such BusiGroup")
}
return obj
}
func Dashboard(ctx *ctx.Context, id int64) *models.Dashboard {
obj, err := models.DashboardGet(ctx, "id=?", id)
ginx.Dangerous(err)
if obj == nil {
ginx.Bomb(http.StatusNotFound, "No such dashboard")
}
return obj
}
type DoneIdsReply struct {
Err string `json:"err"`
Dat struct {
List []int64 `json:"list"`
} `json:"dat"`
}
type TaskCreateReply struct {
Err string `json:"err"`
Dat int64 `json:"dat"` // task.id
}
// return task.id, error
func TaskCreate(v interface{}, ibexc aconf.Ibex) (int64, error) {
var res TaskCreateReply
err := ibex.New(
ibexc.Address,
ibexc.BasicAuthUser,
ibexc.BasicAuthPass,
ibexc.Timeout,
).
Path("/ibex/v1/tasks").
In(v).
Out(&res).
POST()
if err != nil {
return 0, err
}
if res.Err != "" {
return 0, fmt.Errorf("response.err: %v", res.Err)
}
return res.Dat, nil
}

View File

@@ -1,52 +0,0 @@
package router
import (
"compress/gzip"
"encoding/json"
"io/ioutil"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) heartbeat(c *gin.Context) {
var bs []byte
var err error
var r *gzip.Reader
var req models.HostMeta
if c.GetHeader("Content-Encoding") == "gzip" {
r, err = gzip.NewReader(c.Request.Body)
if err != nil {
c.String(400, err.Error())
return
}
defer r.Close()
bs, err = ioutil.ReadAll(r)
ginx.Dangerous(err)
} else {
defer c.Request.Body.Close()
bs, err = ioutil.ReadAll(c.Request.Body)
ginx.Dangerous(err)
}
err = json.Unmarshal(bs, &req)
ginx.Dangerous(err)
req.Offset = (time.Now().UnixMilli() - req.UnixTime)
req.RemoteAddr = c.ClientIP()
rt.MetaSet.Set(req.Hostname, req)
gid := ginx.QueryInt64(c, "gid", 0)
if gid != 0 {
target, has := rt.TargetCache.Get(req.Hostname)
if has && target.GroupId != gid {
err = models.TargetUpdateBgid(rt.Ctx, []string{req.Hostname}, gid, false)
}
}
ginx.NewRender(c).Message(err)
}

View File

@@ -1,539 +0,0 @@
package router
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/cas"
"github.com/ccfos/nightingale/v6/pkg/ldapx"
"github.com/ccfos/nightingale/v6/pkg/oauth2x"
"github.com/ccfos/nightingale/v6/pkg/oidcx"
"github.com/pelletier/go-toml/v2"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
type loginForm struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func (rt *Router) loginPost(c *gin.Context) {
var f loginForm
ginx.BindJSON(c, &f)
user, err := models.PassLogin(rt.Ctx, f.Username, f.Password)
if err != nil {
// pass validate fail, try ldap
if rt.Sso.LDAP.Enable {
roles := strings.Join(rt.Sso.LDAP.DefaultRoles, " ")
user, err = models.LdapLogin(rt.Ctx, f.Username, f.Password, roles, rt.Sso.LDAP)
if err != nil {
logger.Debugf("ldap login failed: %v username: %s", err, f.Username)
ginx.NewRender(c).Message(err)
return
}
user.RolesLst = strings.Fields(user.Roles)
} else {
ginx.NewRender(c).Message(err)
return
}
}
if user == nil {
// Theoretically impossible
ginx.NewRender(c).Message("Username or password invalid")
return
}
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
ginx.Dangerous(err)
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
ginx.NewRender(c).Data(gin.H{
"user": user,
"access_token": ts.AccessToken,
"refresh_token": ts.RefreshToken,
}, nil)
}
func (rt *Router) logoutPost(c *gin.Context) {
metadata, err := rt.extractTokenMetadata(c.Request)
if err != nil {
ginx.NewRender(c, http.StatusBadRequest).Message("failed to parse jwt token")
return
}
delErr := rt.deleteTokens(c.Request.Context(), metadata)
if delErr != nil {
ginx.NewRender(c).Message(http.StatusText(http.StatusInternalServerError))
return
}
ginx.NewRender(c).Message("")
}
type refreshForm struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
func (rt *Router) refreshPost(c *gin.Context) {
var f refreshForm
ginx.BindJSON(c, &f)
// verify the token
token, err := jwt.Parse(f.RefreshToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected jwt signing method: %v", token.Header["alg"])
}
return []byte(rt.HTTP.JWTAuth.SigningKey), nil
})
// if there is an error, the token must have expired
if err != nil {
// redirect to login page
ginx.NewRender(c, http.StatusUnauthorized).Message("refresh token expired")
return
}
// Since token is valid, get the uuid:
claims, ok := token.Claims.(jwt.MapClaims) //the token claims should conform to MapClaims
if ok && token.Valid {
refreshUuid, ok := claims["refresh_uuid"].(string) //convert the interface to string
if !ok {
// Theoretically impossible
ginx.NewRender(c, http.StatusUnauthorized).Message("failed to parse refresh_uuid from jwt")
return
}
userIdentity, ok := claims["user_identity"].(string)
if !ok {
// Theoretically impossible
ginx.NewRender(c, http.StatusUnauthorized).Message("failed to parse user_identity from jwt")
return
}
userid, err := strconv.ParseInt(strings.Split(userIdentity, "-")[0], 10, 64)
if err != nil {
ginx.NewRender(c, http.StatusUnauthorized).Message("failed to parse user_identity from jwt")
return
}
u, err := models.UserGetById(rt.Ctx, userid)
if err != nil {
ginx.NewRender(c, http.StatusInternalServerError).Message("failed to query user by id")
return
}
if u == nil {
// user already deleted
ginx.NewRender(c, http.StatusUnauthorized).Message("user already deleted")
return
}
// Delete the previous Refresh Token
err = rt.deleteAuth(c.Request.Context(), refreshUuid)
if err != nil {
ginx.NewRender(c, http.StatusUnauthorized).Message(http.StatusText(http.StatusInternalServerError))
return
}
// Delete previous Access Token
rt.deleteAuth(c.Request.Context(), strings.Split(refreshUuid, "++")[0])
// Create new pairs of refresh and access tokens
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
ginx.Dangerous(err)
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
ginx.NewRender(c).Data(gin.H{
"access_token": ts.AccessToken,
"refresh_token": ts.RefreshToken,
}, nil)
} else {
// redirect to login page
ginx.NewRender(c, http.StatusUnauthorized).Message("refresh token expired")
}
}
func (rt *Router) loginRedirect(c *gin.Context) {
redirect := ginx.QueryStr(c, "redirect", "/")
v, exists := c.Get("userid")
if exists {
userid := v.(int64)
user, err := models.UserGetById(rt.Ctx, userid)
ginx.Dangerous(err)
if user == nil {
ginx.Bomb(200, "user not found")
}
if user.Username != "" { // already login
ginx.NewRender(c).Data(redirect, nil)
return
}
}
if !rt.Sso.OIDC.Enable {
ginx.NewRender(c).Data("", nil)
return
}
redirect, err := rt.Sso.OIDC.Authorize(rt.Redis, redirect)
ginx.Dangerous(err)
ginx.NewRender(c).Data(redirect, err)
}
type CallbackOutput struct {
Redirect string `json:"redirect"`
User *models.User `json:"user"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
func (rt *Router) loginCallback(c *gin.Context) {
code := ginx.QueryStr(c, "code", "")
state := ginx.QueryStr(c, "state", "")
ret, err := rt.Sso.OIDC.Callback(rt.Redis, c.Request.Context(), code, state)
if err != nil {
logger.Debugf("sso.callback() get ret %+v error %v", ret, err)
ginx.NewRender(c).Data(CallbackOutput{}, err)
return
}
user, err := models.UserGet(rt.Ctx, "username=?", ret.Username)
ginx.Dangerous(err)
if user != nil {
if rt.Sso.OIDC.CoverAttributes {
if ret.Nickname != "" {
user.Nickname = ret.Nickname
}
if ret.Email != "" {
user.Email = ret.Email
}
if ret.Phone != "" {
user.Phone = ret.Phone
}
user.UpdateAt = time.Now().Unix()
user.Update(rt.Ctx, "email", "nickname", "phone", "update_at")
}
} else {
now := time.Now().Unix()
user = &models.User{
Username: ret.Username,
Password: "******",
Nickname: ret.Nickname,
Phone: ret.Phone,
Email: ret.Email,
Portrait: "",
Roles: strings.Join(rt.Sso.OIDC.DefaultRoles, " "),
RolesLst: rt.Sso.OIDC.DefaultRoles,
Contacts: []byte("{}"),
CreateAt: now,
UpdateAt: now,
CreateBy: "oidc",
UpdateBy: "oidc",
}
// create user from oidc
ginx.Dangerous(user.Add(rt.Ctx))
}
// set user login state
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
ginx.Dangerous(err)
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
redirect := "/"
if ret.Redirect != "/login" {
redirect = ret.Redirect
}
ginx.NewRender(c).Data(CallbackOutput{
Redirect: redirect,
User: user,
AccessToken: ts.AccessToken,
RefreshToken: ts.RefreshToken,
}, nil)
}
type RedirectOutput struct {
Redirect string `json:"redirect"`
State string `json:"state"`
}
func (rt *Router) loginRedirectCas(c *gin.Context) {
redirect := ginx.QueryStr(c, "redirect", "/")
v, exists := c.Get("userid")
if exists {
userid := v.(int64)
user, err := models.UserGetById(rt.Ctx, userid)
ginx.Dangerous(err)
if user == nil {
ginx.Bomb(200, "user not found")
}
if user.Username != "" { // already login
ginx.NewRender(c).Data(redirect, nil)
return
}
}
if !rt.Sso.CAS.Enable {
logger.Error("cas is not enable")
ginx.NewRender(c).Data("", nil)
return
}
redirect, state, err := rt.Sso.CAS.Authorize(rt.Redis, redirect)
ginx.Dangerous(err)
ginx.NewRender(c).Data(RedirectOutput{
Redirect: redirect,
State: state,
}, err)
}
func (rt *Router) loginCallbackCas(c *gin.Context) {
ticket := ginx.QueryStr(c, "ticket", "")
state := ginx.QueryStr(c, "state", "")
ret, err := rt.Sso.CAS.ValidateServiceTicket(c.Request.Context(), ticket, state, rt.Redis)
if err != nil {
logger.Errorf("ValidateServiceTicket: %s", err)
ginx.NewRender(c).Data("", err)
return
}
user, err := models.UserGet(rt.Ctx, "username=?", ret.Username)
if err != nil {
logger.Errorf("UserGet: %s", err)
}
ginx.Dangerous(err)
if user != nil {
if rt.Sso.CAS.CoverAttributes {
if ret.Nickname != "" {
user.Nickname = ret.Nickname
}
if ret.Email != "" {
user.Email = ret.Email
}
if ret.Phone != "" {
user.Phone = ret.Phone
}
user.UpdateAt = time.Now().Unix()
ginx.Dangerous(user.Update(rt.Ctx, "email", "nickname", "phone", "update_at"))
}
} else {
now := time.Now().Unix()
user = &models.User{
Username: ret.Username,
Password: "******",
Nickname: ret.Nickname,
Portrait: "",
Roles: strings.Join(rt.Sso.CAS.DefaultRoles, " "),
RolesLst: rt.Sso.CAS.DefaultRoles,
Contacts: []byte("{}"),
Phone: ret.Phone,
Email: ret.Email,
CreateAt: now,
UpdateAt: now,
CreateBy: "CAS",
UpdateBy: "CAS",
}
// create user from cas
ginx.Dangerous(user.Add(rt.Ctx))
}
// set user login state
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
if err != nil {
logger.Errorf("createTokens: %s", err)
}
ginx.Dangerous(err)
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
redirect := "/"
if ret.Redirect != "/login" {
redirect = ret.Redirect
}
ginx.NewRender(c).Data(CallbackOutput{
Redirect: redirect,
User: user,
AccessToken: ts.AccessToken,
RefreshToken: ts.RefreshToken,
}, nil)
}
func (rt *Router) loginRedirectOAuth(c *gin.Context) {
redirect := ginx.QueryStr(c, "redirect", "/")
v, exists := c.Get("userid")
if exists {
userid := v.(int64)
user, err := models.UserGetById(rt.Ctx, userid)
ginx.Dangerous(err)
if user == nil {
ginx.Bomb(200, "user not found")
}
if user.Username != "" { // already login
ginx.NewRender(c).Data(redirect, nil)
return
}
}
if !rt.Sso.OAuth2.Enable {
ginx.NewRender(c).Data("", nil)
return
}
redirect, err := rt.Sso.OAuth2.Authorize(rt.Redis, redirect)
ginx.Dangerous(err)
ginx.NewRender(c).Data(redirect, err)
}
func (rt *Router) loginCallbackOAuth(c *gin.Context) {
code := ginx.QueryStr(c, "code", "")
state := ginx.QueryStr(c, "state", "")
ret, err := rt.Sso.OAuth2.Callback(rt.Redis, c.Request.Context(), code, state)
if err != nil {
logger.Debugf("sso.callback() get ret %+v error %v", ret, err)
ginx.NewRender(c).Data(CallbackOutput{}, err)
return
}
user, err := models.UserGet(rt.Ctx, "username=?", ret.Username)
ginx.Dangerous(err)
if user != nil {
if rt.Sso.OAuth2.CoverAttributes {
if ret.Nickname != "" {
user.Nickname = ret.Nickname
}
if ret.Email != "" {
user.Email = ret.Email
}
if ret.Phone != "" {
user.Phone = ret.Phone
}
user.UpdateAt = time.Now().Unix()
user.Update(rt.Ctx, "email", "nickname", "phone", "update_at")
}
} else {
now := time.Now().Unix()
user = &models.User{
Username: ret.Username,
Password: "******",
Nickname: ret.Nickname,
Phone: ret.Phone,
Email: ret.Email,
Portrait: "",
Roles: strings.Join(rt.Sso.OAuth2.DefaultRoles, " "),
RolesLst: rt.Sso.OAuth2.DefaultRoles,
Contacts: []byte("{}"),
CreateAt: now,
UpdateAt: now,
CreateBy: "oauth2",
UpdateBy: "oauth2",
}
// create user from oidc
ginx.Dangerous(user.Add(rt.Ctx))
}
// set user login state
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
ginx.Dangerous(err)
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
redirect := "/"
if ret.Redirect != "/login" {
redirect = ret.Redirect
}
ginx.NewRender(c).Data(CallbackOutput{
Redirect: redirect,
User: user,
AccessToken: ts.AccessToken,
RefreshToken: ts.RefreshToken,
}, nil)
}
type SsoConfigOutput struct {
OidcDisplayName string `json:"oidcDisplayName"`
CasDisplayName string `json:"casDisplayName"`
OauthDisplayName string `json:"oauthDisplayName"`
}
func (rt *Router) ssoConfigNameGet(c *gin.Context) {
ginx.NewRender(c).Data(SsoConfigOutput{
OidcDisplayName: rt.Sso.OIDC.GetDisplayName(),
CasDisplayName: rt.Sso.CAS.GetDisplayName(),
OauthDisplayName: rt.Sso.OAuth2.GetDisplayName(),
}, nil)
}
func (rt *Router) ssoConfigGets(c *gin.Context) {
ginx.NewRender(c).Data(models.SsoConfigGets(rt.Ctx))
}
func (rt *Router) ssoConfigUpdate(c *gin.Context) {
var f models.SsoConfig
ginx.BindJSON(c, &f)
err := f.Update(rt.Ctx)
ginx.Dangerous(err)
switch f.Name {
case "LDAP":
var config ldapx.Config
err := toml.Unmarshal([]byte(f.Content), &config)
ginx.Dangerous(err)
rt.Sso.LDAP.Reload(config)
case "OIDC":
var config oidcx.Config
err := toml.Unmarshal([]byte(f.Content), &config)
ginx.Dangerous(err)
err = rt.Sso.OIDC.Reload(config)
ginx.Dangerous(err)
case "CAS":
var config cas.Config
err := toml.Unmarshal([]byte(f.Content), &config)
ginx.Dangerous(err)
rt.Sso.CAS.Reload(config)
case "OAuth2":
var config oauth2x.Config
err := toml.Unmarshal([]byte(f.Content), &config)
ginx.Dangerous(err)
rt.Sso.OAuth2.Reload(config)
}
ginx.NewRender(c).Message(nil)
}

View File

@@ -1,97 +0,0 @@
package router
import (
"github.com/ccfos/nightingale/v6/center/cconf"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) metricsDescGetFile(c *gin.Context) {
c.JSON(200, rt.Center.MetricDesc)
}
// 前端传过来一个metric数组后端去查询有没有对应的释义返回map
func (rt *Router) metricsDescGetMap(c *gin.Context) {
var arr []string
ginx.BindJSON(c, &arr)
ret := make(map[string]string)
for _, key := range arr {
ret[key] = cconf.GetMetricDesc(c.GetHeader("X-Language"), key)
}
ginx.NewRender(c).Data(ret, nil)
}
// 页面功能暂时先不要了,直接通过配置文件来维护
// func metricDescriptionGets(c *gin.Context) {
// limit := ginx.QueryInt(c, "limit", 20)
// query := ginx.QueryStr(c, "query", "")
// total, err := models.MetricDescriptionTotal(query)
// ginx.Dangerous(err)
// list, err := models.MetricDescriptionGets(query, limit, ginx.Offset(c, limit))
// ginx.Dangerous(err)
// ginx.NewRender(c).Data(gin.H{
// "list": list,
// "total": total,
// }, nil)
// }
// type metricDescriptionAddForm struct {
// Data string `json:"data"`
// }
// func metricDescriptionAdd(c *gin.Context) {
// var f metricDescriptionAddForm
// ginx.BindJSON(c, &f)
// var metricDescriptions []models.MetricDescription
// lines := strings.Split(f.Data, "\n")
// for _, md := range lines {
// arr := strings.SplitN(md, ":", 2)
// if len(arr) != 2 {
// ginx.Bomb(200, "metric description %s is illegal", md)
// }
// m := models.MetricDescription{
// Metric: arr[0],
// Description: arr[1],
// }
// metricDescriptions = append(metricDescriptions, m)
// }
// if len(metricDescriptions) == 0 {
// ginx.Bomb(http.StatusBadRequest, "Decoded metric description empty")
// }
// ginx.NewRender(c).Message(models.MetricDescriptionUpdate(metricDescriptions))
// }
// func metricDescriptionDel(c *gin.Context) {
// var f idsForm
// ginx.BindJSON(c, &f)
// f.Verify()
// ginx.NewRender(c).Message(models.MetricDescriptionDel(f.Ids))
// }
// type metricDescriptionForm struct {
// Description string `json:"description"`
// }
// func metricDescriptionPut(c *gin.Context) {
// var f metricDescriptionForm
// ginx.BindJSON(c, &f)
// md, err := models.MetricDescriptionGet("id=?", ginx.UrlParamInt64(c, "id"))
// ginx.Dangerous(err)
// if md == nil {
// ginx.Bomb(200, "No such metric description")
// }
// ginx.NewRender(c).Message(md.Update(f.Description, time.Now().Unix()))
// }

View File

@@ -1,76 +0,0 @@
package router
import (
"net/http"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
// no param
func (rt *Router) metricViewGets(c *gin.Context) {
lst, err := models.MetricViewGets(rt.Ctx, c.MustGet("userid"))
ginx.NewRender(c).Data(lst, err)
}
// body: name, configs, cate
func (rt *Router) metricViewAdd(c *gin.Context) {
var f models.MetricView
ginx.BindJSON(c, &f)
me := c.MustGet("user").(*models.User)
if !me.IsAdmin() {
// 管理员可以选择当前这个视图是公开呢,还是私有,普通用户的话就只能是私有的
f.Cate = 1
}
f.Id = 0
f.CreateBy = me.Id
ginx.Dangerous(f.Add(rt.Ctx))
ginx.NewRender(c).Data(f, nil)
}
// body: ids
func (rt *Router) metricViewDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
me := c.MustGet("user").(*models.User)
if me.IsAdmin() {
ginx.NewRender(c).Message(models.MetricViewDel(rt.Ctx, f.Ids))
} else {
ginx.NewRender(c).Message(models.MetricViewDel(rt.Ctx, f.Ids, me.Id))
}
}
// body: id, name, configs, cate
func (rt *Router) metricViewPut(c *gin.Context) {
var f models.MetricView
ginx.BindJSON(c, &f)
view, err := models.MetricViewGet(rt.Ctx, "id = ?", f.Id)
ginx.Dangerous(err)
if view == nil {
ginx.NewRender(c).Message("no such item(id: %d)", f.Id)
return
}
me := c.MustGet("user").(*models.User)
if !me.IsAdmin() {
f.Cate = 1
// 如果是普通用户,只能修改自己的
if view.CreateBy != me.Id {
ginx.NewRender(c, http.StatusForbidden).Message("forbidden")
return
}
}
ginx.NewRender(c).Message(view.Update(rt.Ctx, f.Name, f.Configs, f.Cate, me.Id))
}

View File

@@ -1,105 +0,0 @@
package router
import (
"net/http"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
// Return all, front-end search and paging
func (rt *Router) alertMuteGetsByBG(c *gin.Context) {
bgid := ginx.UrlParamInt64(c, "id")
lst, err := models.AlertMuteGetsByBG(rt.Ctx, bgid)
ginx.NewRender(c).Data(lst, err)
}
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)
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) alertMuteAdd(c *gin.Context) {
var f models.AlertMute
ginx.BindJSON(c, &f)
username := c.MustGet("username").(string)
f.CreateBy = username
f.GroupId = ginx.UrlParamInt64(c, "id")
ginx.NewRender(c).Message(f.Add(rt.Ctx))
}
func (rt *Router) alertMuteAddByService(c *gin.Context) {
var f models.AlertMute
ginx.BindJSON(c, &f)
ginx.NewRender(c).Message(f.Add(rt.Ctx))
}
func (rt *Router) alertMuteDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
ginx.NewRender(c).Message(models.AlertMuteDel(rt.Ctx, f.Ids))
}
func (rt *Router) alertMutePutByFE(c *gin.Context) {
var f models.AlertMute
ginx.BindJSON(c, &f)
amid := ginx.UrlParamInt64(c, "amid")
am, err := models.AlertMuteGetById(rt.Ctx, amid)
ginx.Dangerous(err)
if am == nil {
ginx.NewRender(c, http.StatusNotFound).Message("No such AlertMute")
return
}
rt.bgrwCheck(c, am.GroupId)
f.UpdateBy = c.MustGet("username").(string)
ginx.NewRender(c).Message(am.Update(rt.Ctx, f))
}
type alertMuteFieldForm struct {
Ids []int64 `json:"ids"`
Fields map[string]interface{} `json:"fields"`
}
func (rt *Router) alertMutePutFields(c *gin.Context) {
var f alertMuteFieldForm
ginx.BindJSON(c, &f)
if len(f.Fields) == 0 {
ginx.Bomb(http.StatusBadRequest, "fields empty")
}
f.Fields["update_by"] = c.MustGet("username").(string)
f.Fields["update_at"] = time.Now().Unix()
for i := 0; i < len(f.Ids); i++ {
am, err := models.AlertMuteGetById(rt.Ctx, f.Ids[i])
ginx.Dangerous(err)
if am == nil {
continue
}
am.FE2DB()
ginx.Dangerous(am.UpdateFieldsMap(rt.Ctx, f.Fields))
}
ginx.NewRender(c).Message(nil)
}

View File

@@ -1,422 +0,0 @@
package router
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/google/uuid"
"github.com/toolkits/pkg/ginx"
)
type AccessDetails struct {
AccessUuid string
UserIdentity string
}
func (rt *Router) handleProxyUser(c *gin.Context) *models.User {
headerUserNameKey := rt.HTTP.ProxyAuth.HeaderUserNameKey
username := c.GetHeader(headerUserNameKey)
if username == "" {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
user, err := models.UserGetByUsername(rt.Ctx, username)
if err != nil {
ginx.Bomb(http.StatusInternalServerError, err.Error())
}
if user == nil {
now := time.Now().Unix()
user = &models.User{
Username: username,
Nickname: username,
Roles: strings.Join(rt.HTTP.ProxyAuth.DefaultRoles, " "),
CreateAt: now,
UpdateAt: now,
CreateBy: "system",
UpdateBy: "system",
}
err = user.Add(rt.Ctx)
if err != nil {
ginx.Bomb(http.StatusInternalServerError, err.Error())
}
}
return user
}
func (rt *Router) proxyAuth() gin.HandlerFunc {
return func(c *gin.Context) {
user := rt.handleProxyUser(c)
c.Set("userid", user.Id)
c.Set("username", user.Username)
c.Next()
}
}
func (rt *Router) jwtAuth() gin.HandlerFunc {
return func(c *gin.Context) {
metadata, err := rt.extractTokenMetadata(c.Request)
if err != nil {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
userIdentity, err := rt.fetchAuth(c.Request.Context(), metadata.AccessUuid)
if err != nil {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
// ${userid}-${username}
arr := strings.SplitN(userIdentity, "-", 2)
if len(arr) != 2 {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
userid, err := strconv.ParseInt(arr[0], 10, 64)
if err != nil {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
c.Set("userid", userid)
c.Set("username", arr[1])
c.Next()
}
}
func (rt *Router) auth() gin.HandlerFunc {
if rt.HTTP.ProxyAuth.Enable {
return rt.proxyAuth()
} else {
return rt.jwtAuth()
}
}
// if proxy auth is enabled, mock jwt login/logout/refresh request
func (rt *Router) jwtMock() gin.HandlerFunc {
return func(c *gin.Context) {
if !rt.HTTP.ProxyAuth.Enable {
c.Next()
return
}
if strings.Contains(c.FullPath(), "logout") {
ginx.Bomb(http.StatusBadRequest, "logout is not supported when proxy auth is enabled")
}
user := rt.handleProxyUser(c)
ginx.NewRender(c).Data(gin.H{
"user": user,
"access_token": "",
"refresh_token": "",
}, nil)
c.Abort()
}
}
func (rt *Router) user() gin.HandlerFunc {
return func(c *gin.Context) {
userid := c.MustGet("userid").(int64)
user, err := models.UserGetById(rt.Ctx, userid)
if err != nil {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
if user == nil {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
c.Set("user", user)
c.Set("isadmin", user.IsAdmin())
c.Next()
}
}
func (rt *Router) userGroupWrite() gin.HandlerFunc {
return func(c *gin.Context) {
me := c.MustGet("user").(*models.User)
ug := UserGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
can, err := me.CanModifyUserGroup(rt.Ctx, ug)
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "forbidden")
}
c.Set("user_group", ug)
c.Next()
}
}
func (rt *Router) bgro() gin.HandlerFunc {
return func(c *gin.Context) {
me := c.MustGet("user").(*models.User)
bg := BusiGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
can, err := me.CanDoBusiGroup(rt.Ctx, bg)
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "forbidden")
}
c.Set("busi_group", bg)
c.Next()
}
}
// bgrw 逐步要被干掉,不安全
func (rt *Router) bgrw() gin.HandlerFunc {
return func(c *gin.Context) {
me := c.MustGet("user").(*models.User)
bg := BusiGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
can, err := me.CanDoBusiGroup(rt.Ctx, bg, "rw")
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "forbidden")
}
c.Set("busi_group", bg)
c.Next()
}
}
// bgrwCheck 要逐渐替换掉bgrw方法更安全
func (rt *Router) bgrwCheck(c *gin.Context, bgid int64) {
me := c.MustGet("user").(*models.User)
bg := BusiGroup(rt.Ctx, bgid)
can, err := me.CanDoBusiGroup(rt.Ctx, bg, "rw")
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "forbidden")
}
c.Set("busi_group", bg)
}
func (rt *Router) bgrwChecks(c *gin.Context, bgids []int64) {
set := make(map[int64]struct{})
for i := 0; i < len(bgids); i++ {
if _, has := set[bgids[i]]; has {
continue
}
rt.bgrwCheck(c, bgids[i])
set[bgids[i]] = struct{}{}
}
}
func (rt *Router) bgroCheck(c *gin.Context, bgid int64) {
me := c.MustGet("user").(*models.User)
bg := BusiGroup(rt.Ctx, bgid)
can, err := me.CanDoBusiGroup(rt.Ctx, bg)
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "forbidden")
}
c.Set("busi_group", bg)
}
func (rt *Router) perm(operation string) gin.HandlerFunc {
return func(c *gin.Context) {
me := c.MustGet("user").(*models.User)
can, err := me.CheckPerm(rt.Ctx, operation)
ginx.Dangerous(err)
if !can {
ginx.Bomb(http.StatusForbidden, "forbidden")
}
c.Next()
}
}
func (rt *Router) admin() gin.HandlerFunc {
return func(c *gin.Context) {
userid := c.MustGet("userid").(int64)
user, err := models.UserGetById(rt.Ctx, userid)
if err != nil {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
if user == nil {
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
}
roles := strings.Fields(user.Roles)
found := false
for i := 0; i < len(roles); i++ {
if roles[i] == models.AdminRole {
found = true
break
}
}
if !found {
ginx.Bomb(http.StatusForbidden, "forbidden")
}
c.Set("user", user)
c.Next()
}
}
func (rt *Router) extractTokenMetadata(r *http.Request) (*AccessDetails, error) {
token, err := rt.verifyToken(rt.HTTP.JWTAuth.SigningKey, rt.extractToken(r))
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
accessUuid, ok := claims["access_uuid"].(string)
if !ok {
return nil, errors.New("failed to parse access_uuid from jwt")
}
return &AccessDetails{
AccessUuid: accessUuid,
UserIdentity: claims["user_identity"].(string),
}, nil
}
return nil, err
}
func (rt *Router) extractToken(r *http.Request) string {
tok := r.Header.Get("Authorization")
if len(tok) > 6 && strings.ToUpper(tok[0:7]) == "BEARER " {
return tok[7:]
}
return ""
}
func (rt *Router) createAuth(ctx context.Context, userIdentity string, td *TokenDetails) error {
at := time.Unix(td.AtExpires, 0)
rte := time.Unix(td.RtExpires, 0)
now := time.Now()
errAccess := rt.Redis.Set(ctx, rt.wrapJwtKey(td.AccessUuid), userIdentity, at.Sub(now)).Err()
if errAccess != nil {
return errAccess
}
errRefresh := rt.Redis.Set(ctx, rt.wrapJwtKey(td.RefreshUuid), userIdentity, rte.Sub(now)).Err()
if errRefresh != nil {
return errRefresh
}
return nil
}
func (rt *Router) fetchAuth(ctx context.Context, givenUuid string) (string, error) {
return rt.Redis.Get(ctx, rt.wrapJwtKey(givenUuid)).Result()
}
func (rt *Router) deleteAuth(ctx context.Context, givenUuid string) error {
return rt.Redis.Del(ctx, rt.wrapJwtKey(givenUuid)).Err()
}
func (rt *Router) deleteTokens(ctx context.Context, authD *AccessDetails) error {
// get the refresh uuid
refreshUuid := authD.AccessUuid + "++" + authD.UserIdentity
// delete access token
err := rt.Redis.Del(ctx, rt.wrapJwtKey(authD.AccessUuid)).Err()
if err != nil {
return err
}
// delete refresh token
err = rt.Redis.Del(ctx, rt.wrapJwtKey(refreshUuid)).Err()
if err != nil {
return err
}
return nil
}
func (rt *Router) wrapJwtKey(key string) string {
return rt.HTTP.JWTAuth.RedisKeyPrefix + key
}
type TokenDetails struct {
AccessToken string
RefreshToken string
AccessUuid string
RefreshUuid string
AtExpires int64
RtExpires int64
}
func (rt *Router) createTokens(signingKey, userIdentity string) (*TokenDetails, error) {
td := &TokenDetails{}
td.AtExpires = time.Now().Add(time.Minute * time.Duration(rt.HTTP.JWTAuth.AccessExpired)).Unix()
td.AccessUuid = uuid.NewString()
td.RtExpires = time.Now().Add(time.Minute * time.Duration(rt.HTTP.JWTAuth.RefreshExpired)).Unix()
td.RefreshUuid = td.AccessUuid + "++" + userIdentity
var err error
// Creating Access Token
atClaims := jwt.MapClaims{}
atClaims["authorized"] = true
atClaims["access_uuid"] = td.AccessUuid
atClaims["user_identity"] = userIdentity
atClaims["exp"] = td.AtExpires
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
td.AccessToken, err = at.SignedString([]byte(signingKey))
if err != nil {
return nil, err
}
// Creating Refresh Token
rtClaims := jwt.MapClaims{}
rtClaims["refresh_uuid"] = td.RefreshUuid
rtClaims["user_identity"] = userIdentity
rtClaims["exp"] = td.RtExpires
jrt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims)
td.RefreshToken, err = jrt.SignedString([]byte(signingKey))
if err != nil {
return nil, err
}
return td, nil
}
func (rt *Router) verifyToken(signingKey, tokenString string) (*jwt.Token, error) {
if tokenString == "" {
return nil, fmt.Errorf("bearer token not found")
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected jwt signing method: %v", token.Header["alg"])
}
return []byte(signingKey), nil
})
if err != nil {
return nil, err
}
return token, nil
}

View File

@@ -1,193 +0,0 @@
package router
import (
"encoding/json"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/alert/sender"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/pelletier/go-toml/v2"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) webhookGets(c *gin.Context) {
var webhooks []models.Webhook
cval, err := models.ConfigsGet(rt.Ctx, models.WEBHOOKKEY)
ginx.Dangerous(err)
if cval == "" {
ginx.NewRender(c).Data(webhooks, nil)
return
}
err = json.Unmarshal([]byte(cval), &webhooks)
ginx.NewRender(c).Data(webhooks, err)
}
func (rt *Router) webhookPuts(c *gin.Context) {
var webhooks []models.Webhook
ginx.BindJSON(c, &webhooks)
for i := 0; i < len(webhooks); i++ {
webhooks[i].Headers = []string{}
if len(webhooks[i].HeaderMap) > 0 {
for k, v := range webhooks[i].HeaderMap {
webhooks[i].Headers = append(webhooks[i].Headers, k)
webhooks[i].Headers = append(webhooks[i].Headers, v)
}
}
}
data, err := json.Marshal(webhooks)
ginx.Dangerous(err)
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.WEBHOOKKEY, string(data)))
}
func (rt *Router) notifyScriptGet(c *gin.Context) {
var notifyScript models.NotifyScript
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYSCRIPT)
ginx.Dangerous(err)
if cval == "" {
ginx.NewRender(c).Data(notifyScript, nil)
return
}
err = json.Unmarshal([]byte(cval), &notifyScript)
ginx.NewRender(c).Data(notifyScript, err)
}
func (rt *Router) notifyScriptPut(c *gin.Context) {
var notifyScript models.NotifyScript
ginx.BindJSON(c, &notifyScript)
data, err := json.Marshal(notifyScript)
ginx.Dangerous(err)
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.NOTIFYSCRIPT, string(data)))
}
func (rt *Router) notifyChannelGets(c *gin.Context) {
var notifyChannels []models.NotifyChannel
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYCHANNEL)
ginx.Dangerous(err)
if cval == "" {
ginx.NewRender(c).Data(notifyChannels, nil)
return
}
err = json.Unmarshal([]byte(cval), &notifyChannels)
ginx.NewRender(c).Data(notifyChannels, err)
}
func (rt *Router) notifyChannelPuts(c *gin.Context) {
var notifyChannels []models.NotifyChannel
ginx.BindJSON(c, &notifyChannels)
channels := []string{models.Dingtalk, models.Wecom, models.Feishu, models.Mm, models.Telegram, models.Email}
m := make(map[string]struct{})
for _, v := range notifyChannels {
m[v.Ident] = struct{}{}
}
for _, v := range channels {
if _, ok := m[v]; !ok {
ginx.Bomb(200, "channel %s ident can not modify", v)
}
}
data, err := json.Marshal(notifyChannels)
ginx.Dangerous(err)
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.NOTIFYCHANNEL, string(data)))
}
func (rt *Router) notifyContactGets(c *gin.Context) {
var notifyContacts []models.NotifyContact
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYCONTACT)
ginx.Dangerous(err)
if cval == "" {
ginx.NewRender(c).Data(notifyContacts, nil)
return
}
err = json.Unmarshal([]byte(cval), &notifyContacts)
ginx.NewRender(c).Data(notifyContacts, err)
}
func (rt *Router) notifyContactPuts(c *gin.Context) {
var notifyContacts []models.NotifyContact
ginx.BindJSON(c, &notifyContacts)
keys := []string{models.DingtalkKey, models.WecomKey, models.FeishuKey, models.MmKey, models.TelegramKey}
m := make(map[string]struct{})
for _, v := range notifyContacts {
m[v.Ident] = struct{}{}
}
for _, v := range keys {
if _, ok := m[v]; !ok {
ginx.Bomb(200, "contact %s ident can not modify", v)
}
}
data, err := json.Marshal(notifyContacts)
ginx.Dangerous(err)
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.NOTIFYCONTACT, string(data)))
}
func (rt *Router) notifyConfigGet(c *gin.Context) {
key := ginx.QueryStr(c, "ckey")
cval, err := models.ConfigsGet(rt.Ctx, key)
if cval == "" {
switch key {
case models.IBEX:
cval = memsto.DefaultIbex
case models.SMTP:
cval = memsto.DefaultSMTP
}
}
ginx.NewRender(c).Data(cval, err)
}
func (rt *Router) notifyConfigPut(c *gin.Context) {
var f models.Configs
ginx.BindJSON(c, &f)
switch f.Ckey {
case models.SMTP:
var smtp aconf.SMTPConfig
err := toml.Unmarshal([]byte(f.Cval), &smtp)
ginx.Dangerous(err)
case models.IBEX:
var ibex aconf.Ibex
err := toml.Unmarshal([]byte(f.Cval), &ibex)
ginx.Dangerous(err)
default:
ginx.Bomb(200, "key %s can not modify", f.Ckey)
}
err := models.ConfigsSet(rt.Ctx, f.Ckey, f.Cval)
if err != nil {
ginx.Bomb(200, err.Error())
}
if f.Ckey == models.SMTP {
// 重置邮件发送器
var smtp aconf.SMTPConfig
err := toml.Unmarshal([]byte(f.Cval), &smtp)
ginx.Dangerous(err)
if smtp.Host == "" || smtp.Port == 0 {
ginx.Bomb(200, "smtp host or port can not be empty")
}
go sender.RestartEmailSender(smtp)
}
ginx.NewRender(c).Message(nil)
}

View File

@@ -1,108 +0,0 @@
package router
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"strings"
"github.com/ccfos/nightingale/v6/center/cconf"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/tplx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) notifyTplGets(c *gin.Context) {
lst, err := models.NotifyTplGets(rt.Ctx)
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) notifyTplUpdateContent(c *gin.Context) {
var f models.NotifyTpl
ginx.BindJSON(c, &f)
if err := templateValidate(f); err != nil {
ginx.NewRender(c).Message(err.Error())
return
}
ginx.NewRender(c).Message(f.UpdateContent(rt.Ctx))
}
func (rt *Router) notifyTplUpdate(c *gin.Context) {
var f models.NotifyTpl
ginx.BindJSON(c, &f)
if err := templateValidate(f); err != nil {
ginx.NewRender(c).Message(err.Error())
return
}
ginx.NewRender(c).Message(f.Update(rt.Ctx))
}
func templateValidate(f models.NotifyTpl) error {
if f.Content == "" {
return nil
}
var defs = []string{
"{{$labels := .TagsMap}}",
"{{$value := .TriggerValue}}",
}
text := strings.Join(append(defs, f.Content), "")
if _, err := template.New(f.Channel).Funcs(tplx.TemplateFuncMap).Parse(text); err != nil {
return fmt.Errorf("notify template verify illegal:%s", err.Error())
}
return nil
}
func (rt *Router) notifyTplPreview(c *gin.Context) {
var event models.AlertCurEvent
err := json.Unmarshal([]byte(cconf.EVENT_EXAMPLE), &event)
if err != nil {
ginx.NewRender(c).Message(err.Error())
return
}
var f models.NotifyTpl
ginx.BindJSON(c, &f)
var defs = []string{
"{{$labels := .TagsMap}}",
"{{$value := .TriggerValue}}",
}
text := strings.Join(append(defs, f.Content), "")
tpl, err := template.New(f.Channel).Funcs(tplx.TemplateFuncMap).Parse(text)
ginx.Dangerous(err)
event.TagsMap = make(map[string]string)
for i := 0; i < len(event.TagsJSON); i++ {
pair := strings.TrimSpace(event.TagsJSON[i])
if pair == "" {
continue
}
arr := strings.Split(pair, "=")
if len(arr) != 2 {
continue
}
event.TagsMap[arr[0]] = arr[1]
}
var body bytes.Buffer
var ret string
if err := tpl.Execute(&body, event); err != nil {
ret = err.Error()
} else {
ret = body.String()
}
ginx.NewRender(c).Data(ret, nil)
}

View File

@@ -1,159 +0,0 @@
package router
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
pkgprom "github.com/ccfos/nightingale/v6/pkg/prom"
"github.com/gin-gonic/gin"
"github.com/prometheus/common/model"
"github.com/toolkits/pkg/ginx"
)
type queryFormItem struct {
Start int64 `json:"start" binding:"required"`
End int64 `json:"end" binding:"required"`
Step int64 `json:"step" binding:"required"`
Query string `json:"query" binding:"required"`
}
type batchQueryForm struct {
DatasourceId int64 `json:"datasource_id" binding:"required"`
Queries []queryFormItem `json:"queries" binding:"required"`
}
func (rt *Router) promBatchQueryRange(c *gin.Context) {
var f batchQueryForm
ginx.Dangerous(c.BindJSON(&f))
cli := rt.PromClients.GetCli(f.DatasourceId)
var lst []model.Value
for _, item := range f.Queries {
r := pkgprom.Range{
Start: time.Unix(item.Start, 0),
End: time.Unix(item.End, 0),
Step: time.Duration(item.Step) * time.Second,
}
resp, _, err := cli.QueryRange(context.Background(), item.Query, r)
ginx.Dangerous(err)
lst = append(lst, resp)
}
ginx.NewRender(c).Data(lst, nil)
}
type batchInstantForm struct {
DatasourceId int64 `json:"datasource_id" binding:"required"`
Queries []InstantFormItem `json:"queries" binding:"required"`
}
type InstantFormItem struct {
Time int64 `json:"time" binding:"required"`
Query string `json:"query" binding:"required"`
}
func (rt *Router) promBatchQueryInstant(c *gin.Context) {
var f batchInstantForm
ginx.Dangerous(c.BindJSON(&f))
cli := rt.PromClients.GetCli(f.DatasourceId)
var lst []model.Value
for _, item := range f.Queries {
resp, _, err := cli.Query(context.Background(), item.Query, time.Unix(item.Time, 0))
ginx.Dangerous(err)
lst = append(lst, resp)
}
ginx.NewRender(c).Data(lst, nil)
}
func (rt *Router) dsProxy(c *gin.Context) {
dsId := ginx.UrlParamInt64(c, "id")
ds := rt.DatasourceCache.GetById(dsId)
if ds == nil {
c.String(http.StatusBadRequest, "no such datasource")
return
}
target, err := url.Parse(ds.HTTPJson.Url)
if err != nil {
c.String(http.StatusInternalServerError, "invalid url: %s", ds.HTTPJson.Url)
return
}
director := func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.Host = target.Host
req.Header.Set("Host", target.Host)
// fe request e.g. /api/n9e/proxy/:id/*
arr := strings.Split(req.URL.Path, "/")
if len(arr) < 6 {
c.String(http.StatusBadRequest, "invalid url path")
return
}
req.URL.Path = strings.TrimRight(target.Path, "/") + "/" + strings.Join(arr[5:], "/")
if target.RawQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
if ds.AuthJson.BasicAuthUser != "" {
req.SetBasicAuth(ds.AuthJson.BasicAuthUser, ds.AuthJson.BasicAuthPassword)
}
headerCount := len(ds.HTTPJson.Headers)
if headerCount > 0 {
for key, value := range ds.HTTPJson.Headers {
req.Header.Set(key, value)
if key == "Host" {
req.Host = value
}
}
}
}
errFunc := func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusBadGateway)
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: ds.HTTPJson.TLS.SkipTlsVerify},
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: time.Duration(ds.HTTPJson.DialTimeout) * time.Millisecond,
}).DialContext,
ResponseHeaderTimeout: time.Duration(ds.HTTPJson.Timeout) * time.Millisecond,
MaxIdleConnsPerHost: ds.HTTPJson.MaxIdleConnsPerHost,
}
proxy := &httputil.ReverseProxy{
Director: director,
Transport: transport,
ErrorHandler: errFunc,
}
proxy.ServeHTTP(c.Writer, c.Request)
}

View File

@@ -1,146 +0,0 @@
package router
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) recordingRuleGets(c *gin.Context) {
busiGroupId := ginx.UrlParamInt64(c, "id")
ars, err := models.RecordingRuleGets(rt.Ctx, busiGroupId)
ginx.NewRender(c).Data(ars, err)
}
func (rt *Router) recordingRuleGetsByService(c *gin.Context) {
ars, err := models.RecordingRuleEnabledGets(rt.Ctx)
ginx.NewRender(c).Data(ars, err)
}
func (rt *Router) recordingRuleGet(c *gin.Context) {
rrid := ginx.UrlParamInt64(c, "rrid")
ar, err := models.RecordingRuleGetById(rt.Ctx, rrid)
ginx.Dangerous(err)
if ar == nil {
ginx.NewRender(c, http.StatusNotFound).Message("No such recording rule")
return
}
ginx.NewRender(c).Data(ar, err)
}
func (rt *Router) recordingRuleAddByFE(c *gin.Context) {
username := c.MustGet("username").(string)
var lst []models.RecordingRule
ginx.BindJSON(c, &lst)
count := len(lst)
if count == 0 {
ginx.Bomb(http.StatusBadRequest, "input json is empty")
}
bgid := ginx.UrlParamInt64(c, "id")
reterr := make(map[string]string)
for i := 0; i < count; i++ {
lst[i].Id = 0
lst[i].GroupId = bgid
lst[i].CreateBy = username
lst[i].UpdateBy = username
lst[i].FE2DB()
if err := lst[i].Add(rt.Ctx); err != nil {
reterr[lst[i].Name] = err.Error()
} else {
reterr[lst[i].Name] = ""
}
}
ginx.NewRender(c).Data(reterr, nil)
}
func (rt *Router) recordingRulePutByFE(c *gin.Context) {
var f models.RecordingRule
ginx.BindJSON(c, &f)
rrid := ginx.UrlParamInt64(c, "rrid")
ar, err := models.RecordingRuleGetById(rt.Ctx, rrid)
ginx.Dangerous(err)
if ar == nil {
ginx.NewRender(c, http.StatusNotFound).Message("No such recording rule")
return
}
rt.bgrwCheck(c, ar.GroupId)
f.UpdateBy = c.MustGet("username").(string)
ginx.NewRender(c).Message(ar.Update(rt.Ctx, f))
}
func (rt *Router) recordingRuleDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
ginx.NewRender(c).Message(models.RecordingRuleDels(rt.Ctx, f.Ids, ginx.UrlParamInt64(c, "id")))
}
type recordRuleFieldForm struct {
Ids []int64 `json:"ids"`
Fields map[string]interface{} `json:"fields"`
}
func (rt *Router) recordingRulePutFields(c *gin.Context) {
var f recordRuleFieldForm
ginx.BindJSON(c, &f)
if len(f.Fields) == 0 {
ginx.Bomb(http.StatusBadRequest, "fields empty")
}
f.Fields["update_by"] = c.MustGet("username").(string)
f.Fields["update_at"] = time.Now().Unix()
if _, ok := f.Fields["datasource_ids"]; ok {
// datasource_ids = "1 2 3"
idsStr := strings.Fields(f.Fields["datasource_ids"].(string))
ids := make([]int64, 0)
for _, idStr := range idsStr {
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
ginx.Bomb(http.StatusBadRequest, "datasource_ids error")
}
ids = append(ids, id)
}
bs, err := json.Marshal(ids)
if err != nil {
ginx.Bomb(http.StatusBadRequest, "datasource_ids error")
}
f.Fields["datasource_ids"] = string(bs)
}
for i := 0; i < len(f.Ids); i++ {
ar, err := models.RecordingRuleGetById(rt.Ctx, f.Ids[i])
ginx.Dangerous(err)
if ar == nil {
continue
}
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, f.Fields))
}
ginx.NewRender(c).Message(nil)
}

View File

@@ -1,100 +0,0 @@
package router
import (
"net/http"
"strings"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) rolesGets(c *gin.Context) {
lst, err := models.RoleGetsAll(rt.Ctx)
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) permsGets(c *gin.Context) {
user := c.MustGet("user").(*models.User)
lst, err := models.OperationsOfRole(rt.Ctx, strings.Fields(user.Roles))
ginx.NewRender(c).Data(lst, err)
}
// 创建角色
func (rt *Router) roleAdd(c *gin.Context) {
var f models.Role
ginx.BindJSON(c, &f)
err := f.Add(rt.Ctx)
ginx.NewRender(c).Message(err)
}
// 更新角色
func (rt *Router) rolePut(c *gin.Context) {
var f models.Role
ginx.BindJSON(c, &f)
oldRule, err := models.RoleGet(rt.Ctx, "id=?", f.Id)
ginx.Dangerous(err)
if oldRule == nil {
ginx.Bomb(http.StatusOK, "role not found")
}
if oldRule.Name == "Admin" {
ginx.Bomb(http.StatusOK, "admin role can not be modified")
}
if oldRule.Name != f.Name {
// name changed, check duplication
num, err := models.RoleCount(rt.Ctx, "name=? and id<>?", f.Name, oldRule.Id)
ginx.Dangerous(err)
if num > 0 {
ginx.Bomb(http.StatusOK, "role name already exists")
}
}
oldRule.Name = f.Name
oldRule.Note = f.Note
ginx.NewRender(c).Message(oldRule.Update(rt.Ctx, "name", "note"))
}
func (rt *Router) roleDel(c *gin.Context) {
id := ginx.UrlParamInt64(c, "id")
target, err := models.RoleGet(rt.Ctx, "id=?", id)
ginx.Dangerous(err)
if target.Name == "Admin" {
ginx.Bomb(http.StatusOK, "admin role can not be modified")
}
if target == nil {
ginx.NewRender(c).Message(nil)
return
}
ginx.NewRender(c).Message(target.Del(rt.Ctx))
}
// 角色列表
func (rt *Router) roleGets(c *gin.Context) {
lst, err := models.RoleGetsAll(rt.Ctx)
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) allPerms(c *gin.Context) {
roles, err := models.RoleGetsAll(rt.Ctx)
ginx.Dangerous(err)
m := make(map[string][]string)
for _, r := range roles {
lst, err := models.OperationsOfRole(rt.Ctx, strings.Fields(r.Name))
if err != nil {
continue
}
m[r.Name] = lst
}
ginx.NewRender(c).Data(m, err)
}

View File

@@ -1,43 +0,0 @@
package router
import (
"net/http"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) operationOfRole(c *gin.Context) {
id := ginx.UrlParamInt64(c, "id")
role, err := models.RoleGet(rt.Ctx, "id=?", id)
ginx.Dangerous(err)
if role == nil {
ginx.Bomb(http.StatusOK, "role not found")
}
ops, err := models.OperationsOfRole(rt.Ctx, []string{role.Name})
ginx.NewRender(c).Data(ops, err)
}
func (rt *Router) roleBindOperation(c *gin.Context) {
id := ginx.UrlParamInt64(c, "id")
role, err := models.RoleGet(rt.Ctx, "id=?", id)
ginx.Dangerous(err)
if role == nil {
ginx.Bomb(http.StatusOK, "role not found")
}
if role.Name == "Admin" {
ginx.Bomb(http.StatusOK, "admin role can not be modified")
}
var ops []string
ginx.BindJSON(c, &ops)
ginx.NewRender(c).Message(models.RoleOperationBind(rt.Ctx, role.Name, ops))
}
func (rt *Router) operations(c *gin.Context) {
ginx.NewRender(c).Data(rt.Operations.Ops, nil)
}

View File

@@ -1,52 +0,0 @@
package router
import (
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ormx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) selfProfileGet(c *gin.Context) {
user := c.MustGet("user").(*models.User)
if user.IsAdmin() {
user.Admin = true
}
ginx.NewRender(c).Data(user, nil)
}
type selfProfileForm struct {
Nickname string `json:"nickname"`
Phone string `json:"phone"`
Email string `json:"email"`
Portrait string `json:"portrait"`
Contacts ormx.JSONObj `json:"contacts"`
}
func (rt *Router) selfProfilePut(c *gin.Context) {
var f selfProfileForm
ginx.BindJSON(c, &f)
user := c.MustGet("user").(*models.User)
user.Nickname = f.Nickname
user.Phone = f.Phone
user.Email = f.Email
user.Portrait = f.Portrait
user.Contacts = f.Contacts
user.UpdateBy = user.Username
ginx.NewRender(c).Message(user.UpdateAllFields(rt.Ctx))
}
type selfPasswordForm struct {
OldPass string `json:"oldpass" binding:"required"`
NewPass string `json:"newpass" binding:"required"`
}
func (rt *Router) selfPasswordPut(c *gin.Context) {
var f selfPasswordForm
ginx.BindJSON(c, &f)
user := c.MustGet("user").(*models.User)
ginx.NewRender(c).Message(user.ChangePassword(rt.Ctx, f.OldPass, f.NewPass))
}

View File

@@ -1,34 +0,0 @@
package router
import (
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) serversGet(c *gin.Context) {
list, err := models.AlertingEngineGets(rt.Ctx, "")
ginx.NewRender(c).Data(list, err)
}
func (rt *Router) serverClustersGet(c *gin.Context) {
list, err := models.AlertingEngineGetsClusters(rt.Ctx, "")
ginx.NewRender(c).Data(list, err)
}
func (rt *Router) serverHeartbeat(c *gin.Context) {
var req models.HeartbeatInfo
ginx.BindJSON(c, &req)
err := models.AlertingEngineHeartbeatWithCluster(rt.Ctx, req.Instance, req.EngineCluster, req.DatasourceId)
ginx.NewRender(c).Message(err)
}
func (rt *Router) serversActive(c *gin.Context) {
datasourceId := ginx.QueryInt64(c, "dsid")
servers, err := models.AlertingEngineGetsInstances(rt.Ctx, "datasource_id = ? and clock > ?", datasourceId, time.Now().Unix()-30)
ginx.NewRender(c).Data(servers, err)
}

View File

@@ -1,349 +0,0 @@
package router
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/storage"
"github.com/gin-gonic/gin"
"github.com/prometheus/common/model"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
type TargetQuery struct {
Filters []models.HostQuery `json:"queries"`
P int `json:"p"`
Limit int `json:"limit"`
}
func (rt *Router) targetGetsByHostFilter(c *gin.Context) {
var f TargetQuery
ginx.BindJSON(c, &f)
query := models.GetHostsQuery(f.Filters)
hosts, err := models.TargetGetsByFilter(rt.Ctx, query, f.Limit, (f.P-1)*f.Limit)
ginx.Dangerous(err)
total, err := models.TargetCountByFilter(rt.Ctx, query)
ginx.Dangerous(err)
ginx.NewRender(c).Data(gin.H{
"list": hosts,
"total": total,
}, nil)
}
func (rt *Router) targetGets(c *gin.Context) {
bgid := ginx.QueryInt64(c, "bgid", -1)
query := ginx.QueryStr(c, "query", "")
limit := ginx.QueryInt(c, "limit", 30)
dsIds := queryDatasourceIds(c)
total, err := models.TargetTotal(rt.Ctx, bgid, dsIds, query)
ginx.Dangerous(err)
list, err := models.TargetGets(rt.Ctx, bgid, dsIds, query, limit, ginx.Offset(c, limit))
ginx.Dangerous(err)
if err == nil {
now := time.Now()
cache := make(map[int64]*models.BusiGroup)
var keys []string
for i := 0; i < len(list); i++ {
ginx.Dangerous(list[i].FillGroup(rt.Ctx, cache))
keys = append(keys, models.WrapIdent(list[i].Ident))
}
if len(keys) > 0 {
metaMap := make(map[string]*models.HostMeta)
vals := storage.MGet(context.Background(), rt.Redis, keys)
for _, value := range vals {
var meta models.HostMeta
if value == nil {
continue
}
err := json.Unmarshal(value, &meta)
if err != nil {
logger.Warningf("unmarshal %v host meta failed: %v", value, err)
continue
}
metaMap[meta.Hostname] = &meta
}
for i := 0; i < len(list); i++ {
if now.Unix()-list[i].UpdateAt < 120 {
list[i].TargetUp = 1
}
if meta, ok := metaMap[list[i].Ident]; ok {
list[i].FillMeta(meta)
} else {
// 未上报过元数据的主机cpuNum默认为-1, 用于前端展示 unknown
list[i].CpuNum = -1
}
}
}
}
ginx.NewRender(c).Data(gin.H{
"list": list,
"total": total,
}, nil)
}
func (rt *Router) targetGetsByService(c *gin.Context) {
lst, err := models.TargetGetsAll(rt.Ctx)
ginx.NewRender(c).Data(lst, err)
}
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))
ginx.NewRender(c).Data(lst, err)
}
type targetTagsForm struct {
Idents []string `json:"idents" binding:"required"`
Tags []string `json:"tags" binding:"required"`
}
func (rt *Router) targetBindTagsByFE(c *gin.Context) {
var f targetTagsForm
ginx.BindJSON(c, &f)
if len(f.Idents) == 0 {
ginx.Bomb(http.StatusBadRequest, "idents empty")
}
rt.checkTargetPerm(c, f.Idents)
ginx.NewRender(c).Message(rt.targetBindTags(f))
}
func (rt *Router) targetBindTagsByService(c *gin.Context) {
var f targetTagsForm
ginx.BindJSON(c, &f)
if len(f.Idents) == 0 {
ginx.Bomb(http.StatusBadRequest, "idents empty")
}
ginx.NewRender(c).Message(rt.targetBindTags(f))
}
func (rt *Router) targetBindTags(f targetTagsForm) error {
for i := 0; i < len(f.Tags); i++ {
arr := strings.Split(f.Tags[i], "=")
if len(arr) != 2 {
return fmt.Errorf("invalid tag(%s)", f.Tags[i])
}
if strings.TrimSpace(arr[0]) == "" || strings.TrimSpace(arr[1]) == "" {
return fmt.Errorf("invalid tag(%s)", f.Tags[i])
}
if strings.IndexByte(arr[0], '.') != -1 {
return fmt.Errorf("invalid tagkey(%s): cannot contains . ", arr[0])
}
if strings.IndexByte(arr[0], '-') != -1 {
return fmt.Errorf("invalid tagkey(%s): cannot contains -", arr[0])
}
if !model.LabelNameRE.MatchString(arr[0]) {
return fmt.Errorf("invalid tagkey(%s)", arr[0])
}
}
for i := 0; i < len(f.Idents); i++ {
target, err := models.TargetGetByIdent(rt.Ctx, f.Idents[i])
if err != nil {
return err
}
if target == nil {
continue
}
// 不能有同key的标签否则附到时序数据上会产生覆盖让人困惑
for j := 0; j < len(f.Tags); j++ {
tagkey := strings.Split(f.Tags[j], "=")[0]
tagkeyPrefix := tagkey + "="
if strings.HasPrefix(target.Tags, tagkeyPrefix) {
return fmt.Errorf("duplicate tagkey(%s)", tagkey)
}
}
err = target.AddTags(rt.Ctx, f.Tags)
if err != nil {
return err
}
}
return nil
}
func (rt *Router) targetUnbindTagsByFE(c *gin.Context) {
var f targetTagsForm
ginx.BindJSON(c, &f)
if len(f.Idents) == 0 {
ginx.Bomb(http.StatusBadRequest, "idents empty")
}
rt.checkTargetPerm(c, f.Idents)
ginx.NewRender(c).Message(rt.targetUnbindTags(f))
}
func (rt *Router) targetUnbindTagsByService(c *gin.Context) {
var f targetTagsForm
ginx.BindJSON(c, &f)
if len(f.Idents) == 0 {
ginx.Bomb(http.StatusBadRequest, "idents empty")
}
ginx.NewRender(c).Message(rt.targetUnbindTags(f))
}
func (rt *Router) targetUnbindTags(f targetTagsForm) error {
for i := 0; i < len(f.Idents); i++ {
target, err := models.TargetGetByIdent(rt.Ctx, f.Idents[i])
if err != nil {
return err
}
if target == nil {
continue
}
err = target.DelTags(rt.Ctx, f.Tags)
if err != nil {
return err
}
}
return nil
}
type targetNoteForm struct {
Idents []string `json:"idents" binding:"required"`
Note string `json:"note"`
}
func (rt *Router) targetUpdateNote(c *gin.Context) {
var f targetNoteForm
ginx.BindJSON(c, &f)
if len(f.Idents) == 0 {
ginx.Bomb(http.StatusBadRequest, "idents empty")
}
rt.checkTargetPerm(c, f.Idents)
ginx.NewRender(c).Message(models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
}
func (rt *Router) targetUpdateNoteByService(c *gin.Context) {
var f targetNoteForm
ginx.BindJSON(c, &f)
if len(f.Idents) == 0 {
ginx.Bomb(http.StatusBadRequest, "idents empty")
}
ginx.NewRender(c).Message(models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
}
type targetBgidForm struct {
Idents []string `json:"idents" binding:"required"`
Bgid int64 `json:"bgid"`
}
func (rt *Router) targetUpdateBgid(c *gin.Context) {
var f targetBgidForm
ginx.BindJSON(c, &f)
if len(f.Idents) == 0 {
ginx.Bomb(http.StatusBadRequest, "idents empty")
}
user := c.MustGet("user").(*models.User)
if user.IsAdmin() {
ginx.NewRender(c).Message(models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
return
}
if f.Bgid > 0 {
// 把要操作的机器分成两部分一部分是bgid为0需要管理员分配另一部分bgid>0说明是业务组内部想调整
// 比如原来分配给didiyun的机器didiyun的管理员想把部分机器调整到didiyun-ceph下
// 对于调整的这种情况当前登录用户要对这批机器有操作权限同时还要对目标BG有操作权限
orphans, err := models.IdentsFilter(rt.Ctx, f.Idents, "group_id = ?", 0)
ginx.Dangerous(err)
// 机器里边存在未归组的登录用户就需要是admin
if len(orphans) > 0 && !user.IsAdmin() {
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
}
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)
can, err := user.CanDoBusiGroup(rt.Ctx, bg, "rw")
ginx.Dangerous(err)
if !can {
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")
}
ginx.NewRender(c).Message(models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
}
type identsForm struct {
Idents []string `json:"idents" binding:"required"`
}
func (rt *Router) targetDel(c *gin.Context) {
var f identsForm
ginx.BindJSON(c, &f)
if len(f.Idents) == 0 {
ginx.Bomb(http.StatusBadRequest, "idents empty")
}
rt.checkTargetPerm(c, f.Idents)
ginx.NewRender(c).Message(models.TargetDel(rt.Ctx, f.Idents))
}
func (rt *Router) checkTargetPerm(c *gin.Context, idents []string) {
user := c.MustGet("user").(*models.User)
nopri, err := user.NopriIdents(rt.Ctx, idents)
ginx.Dangerous(err)
if len(nopri) > 0 {
ginx.Bomb(http.StatusForbidden, "No permission to operate the targets: %s", strings.Join(nopri, ", "))
}
}

View File

@@ -1,216 +0,0 @@
package router
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/str"
)
func (rt *Router) taskGets(c *gin.Context) {
bgid := ginx.UrlParamInt64(c, "id")
mine := ginx.QueryBool(c, "mine", false)
days := ginx.QueryInt64(c, "days", 7)
limit := ginx.QueryInt(c, "limit", 20)
query := ginx.QueryStr(c, "query", "")
user := c.MustGet("user").(*models.User)
creator := ""
if mine {
creator = user.Username
}
beginTime := time.Now().Unix() - days*24*3600
total, err := models.TaskRecordTotal(rt.Ctx, bgid, beginTime, creator, query)
ginx.Dangerous(err)
list, err := models.TaskRecordGets(rt.Ctx, bgid, beginTime, creator, query, limit, ginx.Offset(c, limit))
ginx.Dangerous(err)
ginx.NewRender(c).Data(gin.H{
"total": total,
"list": list,
}, nil)
}
type taskForm struct {
Title string `json:"title" binding:"required"`
Account string `json:"account" binding:"required"`
Batch int `json:"batch"`
Tolerance int `json:"tolerance"`
Timeout int `json:"timeout"`
Pause string `json:"pause"`
Script string `json:"script" binding:"required"`
Args string `json:"args"`
Action string `json:"action" binding:"required"`
Creator string `json:"creator"`
Hosts []string `json:"hosts" binding:"required"`
}
func (f *taskForm) Verify() error {
if f.Batch < 0 {
return fmt.Errorf("arg(batch) should be nonnegative")
}
if f.Tolerance < 0 {
return fmt.Errorf("arg(tolerance) should be nonnegative")
}
if f.Timeout < 0 {
return fmt.Errorf("arg(timeout) should be nonnegative")
}
if f.Timeout > 3600*24 {
return fmt.Errorf("arg(timeout) longer than one day")
}
if f.Timeout == 0 {
f.Timeout = 30
}
f.Pause = strings.Replace(f.Pause, "", ",", -1)
f.Pause = strings.Replace(f.Pause, " ", "", -1)
f.Args = strings.Replace(f.Args, "", ",", -1)
if f.Title == "" {
return fmt.Errorf("arg(title) is required")
}
if str.Dangerous(f.Title) {
return fmt.Errorf("arg(title) is dangerous")
}
if f.Script == "" {
return fmt.Errorf("arg(script) is required")
}
if str.Dangerous(f.Args) {
return fmt.Errorf("arg(args) is dangerous")
}
if str.Dangerous(f.Pause) {
return fmt.Errorf("arg(pause) is dangerous")
}
if len(f.Hosts) == 0 {
return fmt.Errorf("arg(hosts) empty")
}
if f.Action != "start" && f.Action != "pause" {
return fmt.Errorf("arg(action) invalid")
}
return nil
}
func (f *taskForm) HandleFH(fh string) {
i := strings.Index(f.Title, " FH: ")
if i > 0 {
f.Title = f.Title[:i]
}
f.Title = f.Title + " FH: " + fh
}
func (rt *Router) taskRecordAdd(c *gin.Context) {
var f *models.TaskRecord
ginx.BindJSON(c, &f)
ginx.NewRender(c).Message(f.Add(rt.Ctx))
}
func (rt *Router) taskAdd(c *gin.Context) {
var f taskForm
ginx.BindJSON(c, &f)
bgid := ginx.UrlParamInt64(c, "id")
user := c.MustGet("user").(*models.User)
f.Creator = user.Username
err := f.Verify()
ginx.Dangerous(err)
f.HandleFH(f.Hosts[0])
// check permission
rt.checkTargetPerm(c, f.Hosts)
// call ibex
taskId, err := TaskCreate(f, rt.NotifyConfigCache.GetIbex())
ginx.Dangerous(err)
if taskId <= 0 {
ginx.Dangerous("created task.id is zero")
}
// write db
record := models.TaskRecord{
Id: taskId,
GroupId: bgid,
IbexAddress: rt.NotifyConfigCache.GetIbex().Address,
IbexAuthUser: rt.NotifyConfigCache.GetIbex().BasicAuthUser,
IbexAuthPass: rt.NotifyConfigCache.GetIbex().BasicAuthPass,
Title: f.Title,
Account: f.Account,
Batch: f.Batch,
Tolerance: f.Tolerance,
Timeout: f.Timeout,
Pause: f.Pause,
Script: f.Script,
Args: f.Args,
CreateAt: time.Now().Unix(),
CreateBy: f.Creator,
}
err = record.Add(rt.Ctx)
ginx.NewRender(c).Data(taskId, err)
}
func (rt *Router) taskProxy(c *gin.Context) {
target, err := url.Parse(rt.NotifyConfigCache.GetIbex().Address)
if err != nil {
ginx.NewRender(c).Message("invalid ibex address: %s", rt.NotifyConfigCache.GetIbex().Address)
return
}
director := func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
// fe request e.g. /api/n9e/busi-group/:id/task/*url
index := strings.Index(req.URL.Path, "/task/")
if index == -1 {
panic("url path invalid")
}
req.URL.Path = "/ibex/v1" + req.URL.Path[index:]
if target.RawQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
}
if rt.NotifyConfigCache.GetIbex().BasicAuthUser != "" {
req.SetBasicAuth(rt.NotifyConfigCache.GetIbex().BasicAuthUser, rt.NotifyConfigCache.GetIbex().BasicAuthPass)
}
}
errFunc := func(w http.ResponseWriter, r *http.Request, err error) {
ginx.NewRender(c, http.StatusBadGateway).Message(err)
}
proxy := &httputil.ReverseProxy{
Director: director,
ErrorHandler: errFunc,
}
proxy.ServeHTTP(c.Writer, c.Request)
}

View File

@@ -1,214 +0,0 @@
package router
import (
"net/http"
"sort"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/str"
)
func (rt *Router) taskTplGets(c *gin.Context) {
query := ginx.QueryStr(c, "query", "")
limit := ginx.QueryInt(c, "limit", 20)
groupId := ginx.UrlParamInt64(c, "id")
total, err := models.TaskTplTotal(rt.Ctx, groupId, query)
ginx.Dangerous(err)
list, err := models.TaskTplGets(rt.Ctx, groupId, query, limit, ginx.Offset(c, limit))
ginx.Dangerous(err)
ginx.NewRender(c).Data(gin.H{
"total": total,
"list": list,
}, nil)
}
func (rt *Router) taskTplGet(c *gin.Context) {
tid := ginx.UrlParamInt64(c, "tid")
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", tid)
ginx.Dangerous(err)
if tpl == nil {
ginx.Bomb(404, "no such task template")
}
hosts, err := tpl.Hosts(rt.Ctx)
ginx.NewRender(c).Data(gin.H{
"tpl": tpl,
"hosts": hosts,
}, err)
}
type taskTplForm struct {
Title string `json:"title" binding:"required"`
Batch int `json:"batch"`
Tolerance int `json:"tolerance"`
Timeout int `json:"timeout"`
Pause string `json:"pause"`
Script string `json:"script"`
Args string `json:"args"`
Tags []string `json:"tags"`
Account string `json:"account"`
Hosts []string `json:"hosts"`
}
func (rt *Router) taskTplAdd(c *gin.Context) {
var f taskTplForm
ginx.BindJSON(c, &f)
user := c.MustGet("user").(*models.User)
now := time.Now().Unix()
sort.Strings(f.Tags)
tpl := &models.TaskTpl{
GroupId: ginx.UrlParamInt64(c, "id"),
Title: f.Title,
Batch: f.Batch,
Tolerance: f.Tolerance,
Timeout: f.Timeout,
Pause: f.Pause,
Script: f.Script,
Args: f.Args,
Tags: strings.Join(f.Tags, " ") + " ",
Account: f.Account,
CreateBy: user.Username,
UpdateBy: user.Username,
CreateAt: now,
UpdateAt: now,
}
ginx.NewRender(c).Message(tpl.Save(rt.Ctx, f.Hosts))
}
func (rt *Router) taskTplPut(c *gin.Context) {
tid := ginx.UrlParamInt64(c, "tid")
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", tid)
ginx.Dangerous(err)
if tpl == nil {
ginx.NewRender(c).Message("no such task template")
return
}
user := c.MustGet("user").(*models.User)
var f taskTplForm
ginx.BindJSON(c, &f)
sort.Strings(f.Tags)
tpl.Title = f.Title
tpl.Batch = f.Batch
tpl.Tolerance = f.Tolerance
tpl.Timeout = f.Timeout
tpl.Pause = f.Pause
tpl.Script = f.Script
tpl.Args = f.Args
tpl.Tags = strings.Join(f.Tags, " ") + " "
tpl.Account = f.Account
tpl.UpdateBy = user.Username
tpl.UpdateAt = time.Now().Unix()
ginx.NewRender(c).Message(tpl.Update(rt.Ctx, f.Hosts))
}
func (rt *Router) taskTplDel(c *gin.Context) {
tid := ginx.UrlParamInt64(c, "tid")
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", tid)
ginx.Dangerous(err)
if tpl == nil {
ginx.NewRender(c).Message(nil)
return
}
ginx.NewRender(c).Message(tpl.Del(rt.Ctx))
}
type tplTagsForm struct {
Ids []int64 `json:"ids" binding:"required"`
Tags []string `json:"tags" binding:"required"`
}
func (f *tplTagsForm) Verify() {
if len(f.Ids) == 0 {
ginx.Bomb(http.StatusBadRequest, "arg(ids) empty")
}
if len(f.Tags) == 0 {
ginx.Bomb(http.StatusBadRequest, "arg(tags) empty")
}
newTags := make([]string, 0, len(f.Tags))
for i := 0; i < len(f.Tags); i++ {
tag := strings.TrimSpace(f.Tags[i])
if tag == "" {
continue
}
if str.Dangerous(tag) {
ginx.Bomb(http.StatusBadRequest, "arg(tags) invalid")
}
newTags = append(newTags, tag)
}
f.Tags = newTags
if len(f.Tags) == 0 {
ginx.Bomb(http.StatusBadRequest, "arg(tags) empty")
}
}
func (rt *Router) taskTplBindTags(c *gin.Context) {
var f tplTagsForm
ginx.BindJSON(c, &f)
f.Verify()
username := c.MustGet("username").(string)
for i := 0; i < len(f.Ids); i++ {
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", f.Ids[i])
ginx.Dangerous(err)
if tpl == nil {
continue
}
ginx.Dangerous(tpl.AddTags(rt.Ctx, f.Tags, username))
}
ginx.NewRender(c).Message(nil)
}
func (rt *Router) taskTplUnbindTags(c *gin.Context) {
var f tplTagsForm
ginx.BindJSON(c, &f)
f.Verify()
username := c.MustGet("username").(string)
for i := 0; i < len(f.Ids); i++ {
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", f.Ids[i])
ginx.Dangerous(err)
if tpl == nil {
continue
}
ginx.Dangerous(tpl.DelTags(rt.Ctx, f.Tags, username))
}
ginx.NewRender(c).Message(nil)
}

View File

@@ -1,137 +0,0 @@
package router
import (
"net/http"
"strings"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ormx"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
)
func (rt *Router) userFindAll(c *gin.Context) {
list, err := models.UserGetAll(rt.Ctx)
ginx.NewRender(c).Data(list, err)
}
func (rt *Router) userGets(c *gin.Context) {
limit := ginx.QueryInt(c, "limit", 20)
query := ginx.QueryStr(c, "query", "")
total, err := models.UserTotal(rt.Ctx, query)
ginx.Dangerous(err)
list, err := models.UserGets(rt.Ctx, query, limit, ginx.Offset(c, limit))
ginx.Dangerous(err)
user := c.MustGet("user").(*models.User)
ginx.NewRender(c).Data(gin.H{
"list": list,
"total": total,
"admin": user.IsAdmin(),
}, nil)
}
type userAddForm struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Nickname string `json:"nickname"`
Phone string `json:"phone"`
Email string `json:"email"`
Portrait string `json:"portrait"`
Roles []string `json:"roles" binding:"required"`
Contacts ormx.JSONObj `json:"contacts"`
}
func (rt *Router) userAddPost(c *gin.Context) {
var f userAddForm
ginx.BindJSON(c, &f)
password, err := models.CryptoPass(rt.Ctx, f.Password)
ginx.Dangerous(err)
if len(f.Roles) == 0 {
ginx.Bomb(http.StatusBadRequest, "roles empty")
}
user := c.MustGet("user").(*models.User)
u := models.User{
Username: f.Username,
Password: password,
Nickname: f.Nickname,
Phone: f.Phone,
Email: f.Email,
Portrait: f.Portrait,
Roles: strings.Join(f.Roles, " "),
Contacts: f.Contacts,
CreateBy: user.Username,
UpdateBy: user.Username,
}
ginx.NewRender(c).Message(u.Add(rt.Ctx))
}
func (rt *Router) userProfileGet(c *gin.Context) {
user := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
ginx.NewRender(c).Data(user, nil)
}
type userProfileForm struct {
Nickname string `json:"nickname"`
Phone string `json:"phone"`
Email string `json:"email"`
Roles []string `json:"roles"`
Contacts ormx.JSONObj `json:"contacts"`
}
func (rt *Router) userProfilePut(c *gin.Context) {
var f userProfileForm
ginx.BindJSON(c, &f)
if len(f.Roles) == 0 {
ginx.Bomb(http.StatusBadRequest, "roles empty")
}
target := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
target.Nickname = f.Nickname
target.Phone = f.Phone
target.Email = f.Email
target.Roles = strings.Join(f.Roles, " ")
target.Contacts = f.Contacts
target.UpdateBy = c.MustGet("username").(string)
ginx.NewRender(c).Message(target.UpdateAllFields(rt.Ctx))
}
type userPasswordForm struct {
Password string `json:"password" binding:"required"`
}
func (rt *Router) userPasswordPut(c *gin.Context) {
var f userPasswordForm
ginx.BindJSON(c, &f)
target := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
cryptoPass, err := models.CryptoPass(rt.Ctx, f.Password)
ginx.Dangerous(err)
ginx.NewRender(c).Message(target.UpdatePassword(rt.Ctx, cryptoPass, c.MustGet("username").(string)))
}
func (rt *Router) userDel(c *gin.Context) {
id := ginx.UrlParamInt64(c, "id")
target, err := models.UserGetById(rt.Ctx, id)
ginx.Dangerous(err)
if target == nil {
ginx.NewRender(c).Message(nil)
return
}
ginx.NewRender(c).Message(target.Del(rt.Ctx))
}

View File

@@ -1,150 +0,0 @@
package router
import (
"net/http"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
func (rt *Router) checkBusiGroupPerm(c *gin.Context) {
me := c.MustGet("user").(*models.User)
bg := BusiGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
can, err := me.CanDoBusiGroup(rt.Ctx, bg, ginx.UrlParamStr(c, "perm"))
ginx.NewRender(c).Data(can, err)
}
func (rt *Router) userGroupGets(c *gin.Context) {
limit := ginx.QueryInt(c, "limit", 1500)
query := ginx.QueryStr(c, "query", "")
me := c.MustGet("user").(*models.User)
lst, err := me.UserGroups(rt.Ctx, limit, query)
ginx.NewRender(c).Data(lst, err)
}
func (rt *Router) userGroupGetsByService(c *gin.Context) {
lst, err := models.UserGroupGetAll(rt.Ctx)
ginx.NewRender(c).Data(lst, err)
}
// user group member get by service
func (rt *Router) userGroupMemberGetsByService(c *gin.Context) {
members, err := models.UserGroupMemberGetAll(rt.Ctx)
ginx.NewRender(c).Data(members, err)
}
type userGroupForm struct {
Name string `json:"name" binding:"required"`
Note string `json:"note"`
}
func (rt *Router) userGroupAdd(c *gin.Context) {
var f userGroupForm
ginx.BindJSON(c, &f)
me := c.MustGet("user").(*models.User)
ug := models.UserGroup{
Name: f.Name,
Note: f.Note,
CreateBy: me.Username,
UpdateBy: me.Username,
}
err := ug.Add(rt.Ctx)
if err == nil {
// Even failure is not a big deal
models.UserGroupMemberAdd(rt.Ctx, ug.Id, me.Id)
}
ginx.NewRender(c).Data(ug.Id, err)
}
func (rt *Router) userGroupPut(c *gin.Context) {
var f userGroupForm
ginx.BindJSON(c, &f)
me := c.MustGet("user").(*models.User)
ug := c.MustGet("user_group").(*models.UserGroup)
if ug.Name != f.Name {
// name changed, check duplication
num, err := models.UserGroupCount(rt.Ctx, "name=? and id<>?", f.Name, ug.Id)
ginx.Dangerous(err)
if num > 0 {
ginx.Bomb(http.StatusOK, "UserGroup already exists")
}
}
ug.Name = f.Name
ug.Note = f.Note
ug.UpdateBy = me.Username
ug.UpdateAt = time.Now().Unix()
ginx.NewRender(c).Message(ug.Update(rt.Ctx, "Name", "Note", "UpdateAt", "UpdateBy"))
}
// Return all members, front-end search and paging
func (rt *Router) userGroupGet(c *gin.Context) {
ug := UserGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
ids, err := models.MemberIds(rt.Ctx, ug.Id)
ginx.Dangerous(err)
logger.Info("userGroupGet", ids)
users, err := models.UserGetsByIds(rt.Ctx, ids)
ginx.NewRender(c).Data(gin.H{
"users": users,
"user_group": ug,
}, err)
}
func (rt *Router) userGroupDel(c *gin.Context) {
ug := c.MustGet("user_group").(*models.UserGroup)
ginx.NewRender(c).Message(ug.Del(rt.Ctx))
}
func (rt *Router) userGroupMemberAdd(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
me := c.MustGet("user").(*models.User)
ug := c.MustGet("user_group").(*models.UserGroup)
err := ug.AddMembers(rt.Ctx, f.Ids)
if err == nil {
ug.UpdateAt = time.Now().Unix()
ug.UpdateBy = me.Username
ug.Update(rt.Ctx, "UpdateAt", "UpdateBy")
}
ginx.NewRender(c).Message(err)
}
func (rt *Router) userGroupMemberDel(c *gin.Context) {
var f idsForm
ginx.BindJSON(c, &f)
f.Verify()
me := c.MustGet("user").(*models.User)
ug := c.MustGet("user_group").(*models.UserGroup)
err := ug.DelMembers(rt.Ctx, f.Ids)
if err == nil {
ug.UpdateAt = time.Now().Unix()
ug.UpdateBy = me.Username
ug.Update(rt.Ctx, "UpdateAt", "UpdateBy")
}
ginx.NewRender(c).Message(err)
}

View File

@@ -1,169 +0,0 @@
package sso
import (
"log"
"github.com/BurntSushi/toml"
"github.com/ccfos/nightingale/v6/center/cconf"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/cas"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/ldapx"
"github.com/ccfos/nightingale/v6/pkg/oauth2x"
"github.com/ccfos/nightingale/v6/pkg/oidcx"
"github.com/toolkits/pkg/logger"
)
type SsoClient struct {
OIDC *oidcx.SsoClient
LDAP *ldapx.SsoClient
CAS *cas.SsoClient
OAuth2 *oauth2x.SsoClient
}
const LDAP = `
Enable = false
Host = 'ldap.example.org'
Port = 389
BaseDn = 'dc=example,dc=org'
BindUser = 'cn=manager,dc=example,dc=org'
BindPass = '*******'
AuthFilter = '(&(uid=%s))'
CoverAttributes = true
TLS = false
StartTLS = true
DefaultRoles = ['Standard']
[Attributes]
Nickname = 'cn'
Phone = 'mobile'
Email = 'mail'
`
const OAuth2 = `
Enable = false
DisplayName = 'OAuth2登录'
RedirectURL = 'http://127.0.0.1:18000/callback/oauth'
SsoAddr = 'https://sso.example.com/oauth2/authorize'
TokenAddr = 'https://sso.example.com/oauth2/token'
UserInfoAddr = 'https://api.example.com/api/v1/user/info'
TranTokenMethod = 'header'
ClientId = ''
ClientSecret = ''
CoverAttributes = true
DefaultRoles = ['Standard']
UserinfoIsArray = false
UserinfoPrefix = 'data'
Scopes = ['profile', 'email', 'phone']
[Attributes]
Username = 'username'
Nickname = 'nickname'
Phone = 'phone_number'
Email = 'email'
`
const CAS = `
Enable = false
SsoAddr = 'https://cas.example.com/cas/'
RedirectURL = 'http://127.0.0.1:18000/callback/cas'
DisplayName = 'CAS登录'
CoverAttributes = false
DefaultRoles = ['Standard']
[Attributes]
Nickname = 'nickname'
Phone = 'phone_number'
Email = 'email'
`
const OIDC = `
Enable = false
DisplayName = 'OIDC登录'
RedirectURL = 'http://n9e.com/callback'
SsoAddr = 'http://sso.example.org'
ClientId = ''
ClientSecret = ''
CoverAttributes = true
DefaultRoles = ['Standard']
[Attributes]
Nickname = 'nickname'
Phone = 'phone_number'
Email = 'email'
`
func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
ssoClient := new(SsoClient)
m := make(map[string]string)
m["LDAP"] = LDAP
m["CAS"] = CAS
m["OIDC"] = OIDC
m["OAuth2"] = OAuth2
for name, config := range m {
count, err := models.SsoConfigCountByName(ctx, name)
if err != nil {
logger.Error(err)
continue
}
if count > 0 {
continue
}
ssoConfig := models.SsoConfig{
Name: name,
Content: config,
}
err = ssoConfig.Create(ctx)
if err != nil {
log.Fatalln(err)
}
}
configs, err := models.SsoConfigGets(ctx)
if err != nil {
log.Fatalln(err)
}
for _, cfg := range configs {
switch cfg.Name {
case "LDAP":
var config ldapx.Config
err := toml.Unmarshal([]byte(cfg.Content), &config)
if err != nil {
log.Fatalln("init ldap failed", err)
}
ssoClient.LDAP = ldapx.New(config)
case "OIDC":
var config oidcx.Config
err := toml.Unmarshal([]byte(cfg.Content), &config)
if err != nil {
log.Fatalln("init oidc failed:", err)
}
oidcClient, err := oidcx.New(config)
if err != nil {
logger.Error("init oidc failed:", err)
} else {
ssoClient.OIDC = oidcClient
}
case "CAS":
var config cas.Config
err := toml.Unmarshal([]byte(cfg.Content), &config)
if err != nil {
log.Fatalln("init cas failed:", err)
}
ssoClient.CAS = cas.New(config)
case "OAuth2":
var config oauth2x.Config
err := toml.Unmarshal([]byte(cfg.Content), &config)
if err != nil {
log.Fatalln("init oauth2 failed:", err)
}
ssoClient.OAuth2 = oauth2x.New(config)
}
}
return ssoClient
}

127
changelog Normal file
View File

@@ -0,0 +1,127 @@
3.1.1
影响模块n9e-job
更新内容job模块之前给监控用的callback地址method误设置为了get是不对的改成了post
3.1.2
影响模块n9e-rdb
更新内容:子节点修改的时候,不允许修改为租户节点
3.1.3
影响模块n9e-monapi
更新内容对于P2、P3的告警会发送重复的两条
3.1.4
影响模块n9e-index n9e-judge n9e-monapi n9e-rdb n9e-transfer n9e-tsdb
更新内容把hbs的逻辑从monapi挪到rdb拆分监控的权限点
3.1.5
影响模块n9e-monapi
更新内容清理策略的时候会空指针node删除了策略还在此时会复现
3.1.6
影响模块n9e-ams etc/gop.yml
更新内容主机设备增加了扩展字段的管理用于维护一些位置信息、过保信息增加了新的sqlsql/n9e_ams_3.1.6.sql
3.2.0
影响模块n9e-agent etc/agent.yml
更新内容agent支持metrics指标采集能力这个版本是为商业版本服务的开源用户无需更新
3.3.0
影响模块n9e-rdb n9e-transfer n9e-judge n9e-ams n9e-monapi sql/n9e_rdb_3.3.0.sql etc/*.tpl
更新内容增强安全性密码复杂度、cookie处理优化等支持M3DB作为存储后端如果要尝试M3需要修改transfer、monapi配置文件修复告警引擎与条件串数的问题为主机设备增加自定义字段的能力
3.3.1
影响模块n9e-job n9e-rdb n9e-agent n9e-ams n9e-judge
更新内容修复job模块的一个调度bugrdb支持根据org搜索useragent在fields变化时及时感知fields和host扩展字段联动解决上个版本引入的judge处理nodata的问题
3.4.0
升级内容:
- 增强了安全性引入了session机制写入cookie的内容从user.uuid变更为随机session.id
- 修复部分sql注入漏洞
- 告警引擎函数优化all、c_avg_rate_abs等
- 告警消息内容优化可以展示设备名称和设备备注感谢冯骐的PR
- 增加了大盘导入导出功能
升级方法:
- 除了agent、tsdb、index的二进制不用升级其他所有模块的二进制都要升级
- job ams monapi rdb 四个模块的配置文件中的cookieName全部换成ecmc-sid
- rdb的配置文件发生了较大变化需要对照升级
- sql目录下有几个3.4.0的sql需要导入
3.4.1
升级内容:
- 修复日志监控采集策略配置了tag但是无法编辑的问题
升级方法:
- 更新monapi的二进制即可
3.5.0
升级内容:
- 引入了组件监控模块prober内置了mysql、redis、mongo监控采集能力
- 引入了内置监控大盘和内置告警策略,可以在任意节点一键导入内置大盘和策略
升级方法:
- n9e-monapi n9e-rdb n9e-transfer n9e-ams n9e-job 的二进制要升级
- n9e-agent也可以升级解决了进程监控的性能问题如果不在意可以不升级
- n9e-prober 模块需要新部署
- sql目录下有个3.5.0的sql patch文件需要导入
- etc目录下新增了screen、alert两个目录需要拷贝到生产环境
- etc目录下新增了plugins目录需要随着prober模块走
- etc/address.yml里增加prober的配置
3.5.1
升级内容:
- monapi里的alarmEnabled默认值设置为true
- agent进程采集忽略EOF日志
- agent增加一个接口获取endpoint
- agent日志监控支持一种新的日志时间格式
- 修复组件监控调整采集频率不生效的问题
升级方法:
- 替换n9e-monapi n9e-prober n9e-agent二进制升级pub下的前端资源文件
3.5.2
升级内容:
- prober模板支持匿名结构体结构体嵌套
- prober插件添加了对TLS的支持
- 修复prober上报没有port的问题
升级方法:
- 替换n9e-prober n9e-monapi二进制升级pub下的前端资源文件
3.6.0
升级内容:
- prober模块支持nginx、elasticsearch、prometheus的监控采集prometheus转换时姑且干掉了 Histogram 和 Summary
- 告警消息中节点挂载关系做了去重处理
升级方法:
- 替换n9e-prober n9e-monapi二进制
3.7.0
升级内容:
- 调整session清理频率
- 新增zookeeper、tengine、rabbitmq、haproxy、ping、telnet相关采集工具
- bugfix集群部署的时候多个redis实例judge只能识别最后一个实例的问题
升级方法:
- sql/n9e_rdb-v3.7.0.sql 有个新的表结构,需要导入一下
- 替换n9e-rdb n9e-prober n9e-judge n9e-monapi二进制前端没有升级
- 将etc/plugins里zookeeper.yml,tengine.yml等新增的yml文件复制到配置文件里
3.7.1
升级内容:
- prober采集增加dryrun测试方法可以测试是否真的能采集到数据
- 增加dns_query插件对dns做监控
- 内置大盘增加n9e内置模块大盘
- 如果存储使用m3支持在transfer配置一次查询每条线最多返回的原始点数
- 日志监控可以把最后一条日志放到extra字段报警的时候可以展示需要升级n9e-agent n9e-monapi
- 修复agent对进程监控采集的bug进程cpu使用采集的不准确
- 修改告警策略配置多个团队的时候不生效的问题
- monapi支持一个新的timestamp格式
升级方法:
- sql/n9e_mon-v3.7.1.sql变更了表结构需要执行一下
- 将etc/plugins里的dns_query.yml放到生产环境的etc/plugins目录下
- 将etc/screen/n9e_modules放到生产环境的etc/screen目录下
- 替换n9e-rdb n9e-prober n9e-monapi n9e-transfer n9e-agent二进制
3.8.0
升级内容:
- monapi优化告警策略中用户信息补全逻辑
- rdb新增接口,查询项目下用户拥有的资源权限点
- transfer查询索引接口支持指定时间范围
- prober去掉组件采集默认的白名单设置
升级方法:
- 替换n9e-rdb n9e-prober n9e-monapi n9e-transfer二进制

View File

@@ -1,9 +0,0 @@
package cli
import (
"github.com/ccfos/nightingale/v6/cli/upgrade"
)
func Upgrade(configFile string) error {
return upgrade.Upgrade(configFile)
}

View File

@@ -1,63 +0,0 @@
package upgrade
import (
"bytes"
"path"
"github.com/ccfos/nightingale/v6/pkg/cfg"
"github.com/ccfos/nightingale/v6/pkg/ormx"
"github.com/ccfos/nightingale/v6/pkg/tlsx"
"github.com/koding/multiconfig"
)
type Config struct {
DB ormx.DBConfig
Clusters []ClusterOptions
}
type ClusterOptions struct {
Name string
Prom string
BasicAuthUser string
BasicAuthPass string
Headers []string
Timeout int64
DialTimeout int64
UseTLS bool
tlsx.ClientConfig
MaxIdleConnsPerHost int
}
func Parse(fpath string, configPtr interface{}) error {
var (
tBuf []byte
)
loaders := []multiconfig.Loader{
&multiconfig.TagLoader{},
&multiconfig.EnvironmentLoader{},
}
s := cfg.NewFileScanner()
s.Read(path.Join(fpath))
tBuf = append(tBuf, s.Data()...)
tBuf = append(tBuf, []byte("\n")...)
if s.Err() != nil {
return s.Err()
}
if len(tBuf) != 0 {
loaders = append(loaders, &multiconfig.TOMLLoader{Reader: bytes.NewReader(tBuf)})
}
m := multiconfig.DefaultLoader{
Loader: multiconfig.MultiLoader(loaders...),
Validator: multiconfig.MultiValidator(&multiconfig.RequiredValidator{}),
}
return m.Load(configPtr)
}

View File

@@ -1,21 +0,0 @@
# v5 升级 v6 手册
0. 操作之前,记得备注下数据库!
1. 需要先将你正在使用的夜莺数据源表结构更新到和 v5.15.0 一致,[release](https://github.com/ccfos/nightingale/releases) 页面有每个版本表结构的更新说明,可以根据你正在使用的版本,按照说明,逐个执行的更新表结构的语句
2. 解压 n9e 安装包,导入 upgrade.sql 到 n9e_v5 数据库
```
mysql -h 127.0.0.1 -u root -p1234 < cli/upgrade/upgrade.sql
```
3. 执行 n9e-cli 完成数据库表结构升级, webapi.conf 为 v5 版本 n9e-webapi 正在使用的配置文件
```
./n9e-cli --upgrade --config webapi.conf
```
4. 修改 n9e 配置文件中的数据库为 n9e_v5启动 n9e 进程
```
nohup ./n9e &> n9e.log &
```
5. n9e 监听的端口为 17000需要将之前的 web 端口和数据上报的端口,都调整为 17000

View File

@@ -1,117 +0,0 @@
package upgrade
import (
"context"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/storage"
"github.com/toolkits/pkg/logger"
)
func Upgrade(configFile string) error {
var config Config
Parse(configFile, &config)
db, err := storage.New(config.DB)
if err != nil {
return err
}
ctx := ctx.NewContext(context.Background(), db, false)
for _, cluster := range config.Clusters {
count, err := models.GetDatasourcesCountBy(ctx, "", "", cluster.Name)
if err != nil {
logger.Errorf("get datasource %s count error: %v", cluster.Name, err)
continue
}
if count > 0 {
continue
}
header := make(map[string]string)
headerCount := len(cluster.Headers)
if headerCount > 0 && headerCount%2 == 0 {
for i := 0; i < len(cluster.Headers); i += 2 {
header[cluster.Headers[i]] = cluster.Headers[i+1]
}
}
authJosn := models.Auth{
BasicAuthUser: cluster.BasicAuthUser,
BasicAuthPassword: cluster.BasicAuthPass,
}
httpJson := models.HTTP{
Timeout: cluster.Timeout,
DialTimeout: cluster.DialTimeout,
TLS: models.TLS{
SkipTlsVerify: cluster.UseTLS,
},
MaxIdleConnsPerHost: cluster.MaxIdleConnsPerHost,
Url: cluster.Prom,
Headers: header,
}
datasrouce := models.Datasource{
PluginId: 1,
PluginType: "prometheus",
PluginTypeName: "Prometheus Like",
Name: cluster.Name,
HTTPJson: httpJson,
AuthJson: authJosn,
ClusterName: "default",
Status: "enabled",
}
err = datasrouce.Add(ctx)
if err != nil {
logger.Errorf("add datasource %s error: %v", cluster.Name, err)
}
}
datasources, err := models.GetDatasources(ctx)
if err != nil {
return err
}
m := make(map[string]models.Datasource)
for i := 0; i < len(datasources); i++ {
m[datasources[i].Name] = datasources[i]
}
err = models.AlertRuleUpgradeToV6(ctx, m)
if err != nil {
return err
}
// alert mute
err = models.AlertMuteUpgradeToV6(ctx, m)
if err != nil {
return err
}
// alert subscribe
err = models.AlertSubscribeUpgradeToV6(ctx, m)
if err != nil {
return err
}
// recoding rule
err = models.RecordingRuleUpgradeToV6(ctx, m)
if err != nil {
return err
}
// alert cur event
err = models.AlertCurEventUpgradeToV6(ctx, m)
if err != nil {
return err
}
// alert his event
err = models.AlertHisEventUpgradeToV6(ctx, m)
if err != nil {
return err
}
return nil
}

View File

@@ -1,91 +0,0 @@
use n9e_v5;
insert into `role_operation`(role_name, operation) values('Guest', '/log/explorer');
insert into `role_operation`(role_name, operation) values('Guest', '/trace/explorer');
insert into `role_operation`(role_name, operation) values('Standard', '/log/explorer');
insert into `role_operation`(role_name, operation) values('Standard', '/trace/explorer');
insert into `role_operation`(role_name, operation) values('Standard', '/alert-rules-built-in');
insert into `role_operation`(role_name, operation) values('Standard', '/dashboards-built-in');
insert into `role_operation`(role_name, operation) values('Standard', '/trace/dependencies');
alter table `board` add built_in tinyint(1) not null default 0 comment '0:false 1:true';
alter table `board` add hide tinyint(1) not null default 0 comment '0:false 1:true';
alter table `chart_share` add datasource_id bigint unsigned not null default 0;
alter table `alert_rule` add datasource_ids varchar(255) not null default '';
alter table `alert_rule` add rule_config text not null comment 'rule_config';
alter table `alert_rule` add annotations text not null comment 'annotations';
alter table `alert_mute` add datasource_ids varchar(255) not null default '';
alter table `alert_mute` add periodic_mutes varchar(4096) not null default '[]';
alter table `alert_mute` add mute_time_type tinyint(1) not null default 0;
alter table `alert_subscribe` add datasource_ids varchar(255) not null default '';
alter table `alert_subscribe` add prod varchar(255) not null default '';
alter table `alert_subscribe` add webhooks text;
alter table `alert_subscribe` add redefine_webhooks tinyint(1) default 0;
alter table `alert_subscribe` add for_duration bigint not null default 0;
alter table `recording_rule` add datasource_ids varchar(255) default '';
alter table `target` modify cluster varchar(128) not null default '';
alter table `alert_cur_event` add datasource_id bigint unsigned not null default 0;
alter table `alert_cur_event` add annotations text not null comment 'annotations';
alter table `alert_cur_event` add rule_config text not null comment 'rule_config';
alter table `alert_his_event` add datasource_id bigint unsigned not null default 0;
alter table `alert_his_event` add annotations text not null comment 'annotations';
alter table `alert_his_event` add rule_config text not null comment 'rule_config';
alter table `alerting_engines` add datasource_id bigint unsigned not null default 0;
alter table `alerting_engines` change cluster engine_cluster varchar(128) not null default '' comment 'n9e engine cluster';
alter table `task_record` add event_id bigint not null comment 'event id' default 0;
CREATE TABLE `datasource`
(
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) not null default '',
`description` varchar(255) not null default '',
`category` varchar(255) not null default '',
`plugin_id` int unsigned not null default 0,
`plugin_type` varchar(255) not null default '',
`plugin_type_name` varchar(255) not null default '',
`cluster_name` varchar(255) not null default '',
`settings` text not null,
`status` varchar(255) not null default '',
`http` varchar(4096) not null default '',
`auth` varchar(8192) not null default '',
`created_at` bigint not null default 0,
`created_by` varchar(64) not null default '',
`updated_at` bigint not null default 0,
`updated_by` varchar(64) not null default '',
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `builtin_cate` (
`id` bigint unsigned not null auto_increment,
`name` varchar(191) not null,
`user_id` bigint not null default 0,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `notify_tpl` (
`id` bigint unsigned not null auto_increment,
`channel` varchar(32) not null,
`name` varchar(255) not null,
`content` text not null,
PRIMARY KEY (`id`),
UNIQUE KEY (`channel`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `sso_config` (
`id` bigint unsigned not null auto_increment,
`name` varchar(191) not null,
`content` text not null,
PRIMARY KEY (`id`),
UNIQUE KEY (`name`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@@ -1,69 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/ccfos/nightingale/v6/alert"
"github.com/ccfos/nightingale/v6/pkg/osx"
"github.com/ccfos/nightingale/v6/pkg/version"
"github.com/toolkits/pkg/runner"
)
var (
showVersion = flag.Bool("version", false, "Show version.")
configDir = flag.String("configs", osx.GetEnv("N9E_CONFIGS", "etc"), "Specify configuration directory.(env:N9E_CONFIGS)")
cryptoKey = flag.String("crypto-key", "", "Specify the secret key for configuration file field encryption.")
)
func main() {
flag.Parse()
if *showVersion {
fmt.Println(version.Version)
os.Exit(0)
}
printEnv()
cleanFunc, err := alert.Initialize(*configDir, *cryptoKey)
if err != nil {
log.Fatalln("failed to initialize:", err)
}
code := 1
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
EXIT:
for {
sig := <-sc
fmt.Println("received signal:", sig.String())
switch sig {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
code = 0
break EXIT
case syscall.SIGHUP:
// reload configuration?
default:
break EXIT
}
}
cleanFunc()
fmt.Println("process exited")
os.Exit(code)
}
func printEnv() {
runner.Init()
fmt.Println("runner.cwd:", runner.Cwd)
fmt.Println("runner.hostname:", runner.Hostname)
fmt.Println("runner.fd_limits:", runner.FdLimits())
fmt.Println("runner.vm_limits:", runner.VMLimits())
}

View File

@@ -1,69 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/ccfos/nightingale/v6/center"
"github.com/ccfos/nightingale/v6/pkg/osx"
"github.com/ccfos/nightingale/v6/pkg/version"
"github.com/toolkits/pkg/runner"
)
var (
showVersion = flag.Bool("version", false, "Show version.")
configDir = flag.String("configs", osx.GetEnv("N9E_CONFIGS", "etc"), "Specify configuration directory.(env:N9E_CONFIGS)")
cryptoKey = flag.String("crypto-key", "", "Specify the secret key for configuration file field encryption.")
)
func main() {
flag.Parse()
if *showVersion {
fmt.Println(version.Version)
os.Exit(0)
}
printEnv()
cleanFunc, err := center.Initialize(*configDir, *cryptoKey)
if err != nil {
log.Fatalln("failed to initialize:", err)
}
code := 1
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
EXIT:
for {
sig := <-sc
fmt.Println("received signal:", sig.String())
switch sig {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
code = 0
break EXIT
case syscall.SIGHUP:
// reload configuration?
default:
break EXIT
}
}
cleanFunc()
fmt.Println("process exited")
os.Exit(code)
}
func printEnv() {
runner.Init()
fmt.Println("runner.cwd:", runner.Cwd)
fmt.Println("runner.hostname:", runner.Hostname)
fmt.Println("runner.fd_limits:", runner.FdLimits())
fmt.Println("runner.vm_limits:", runner.VMLimits())
}

View File

@@ -1,40 +0,0 @@
package main
import (
"flag"
"fmt"
"os"
"github.com/ccfos/nightingale/v6/cli"
"github.com/ccfos/nightingale/v6/pkg/version"
)
var (
upgrade = flag.Bool("upgrade", false, "Upgrade the database.")
showVersion = flag.Bool("version", false, "Show version.")
configFile = flag.String("config", "", "Specify webapi.conf of v5.x version")
)
func main() {
flag.Parse()
if *showVersion {
fmt.Println(version.Version)
os.Exit(0)
}
if *upgrade {
if *configFile == "" {
fmt.Println("Please specify the configuration directory.")
os.Exit(1)
}
err := cli.Upgrade(*configFile)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Print("Upgrade successfully.")
os.Exit(0)
}
}

View File

@@ -1,69 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/ccfos/nightingale/v6/pkg/osx"
"github.com/ccfos/nightingale/v6/pkg/version"
"github.com/ccfos/nightingale/v6/pushgw"
"github.com/toolkits/pkg/runner"
)
var (
showVersion = flag.Bool("version", false, "Show version.")
configDir = flag.String("configs", osx.GetEnv("N9E_CONFIGS", "etc"), "Specify configuration directory.(env:N9E_CONFIGS)")
cryptoKey = flag.String("crypto-key", "", "Specify the secret key for configuration file field encryption.")
)
func main() {
flag.Parse()
if *showVersion {
fmt.Println(version.Version)
os.Exit(0)
}
printEnv()
cleanFunc, err := pushgw.Initialize(*configDir, *cryptoKey)
if err != nil {
log.Fatalln("failed to initialize:", err)
}
code := 1
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
EXIT:
for {
sig := <-sc
fmt.Println("received signal:", sig.String())
switch sig {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
code = 0
break EXIT
case syscall.SIGHUP:
// reload configuration?
default:
break EXIT
}
}
cleanFunc()
fmt.Println("process exited")
os.Exit(code)
}
func printEnv() {
runner.Init()
fmt.Println("runner.cwd:", runner.Cwd)
fmt.Println("runner.hostname:", runner.Hostname)
fmt.Println("runner.fd_limits:", runner.FdLimits())
fmt.Println("runner.vm_limits:", runner.VMLimits())
}

View File

@@ -1,93 +0,0 @@
package conf
import (
"fmt"
"net"
"os"
"strings"
"github.com/ccfos/nightingale/v6/alert/aconf"
"github.com/ccfos/nightingale/v6/center/cconf"
"github.com/ccfos/nightingale/v6/pkg/cfg"
"github.com/ccfos/nightingale/v6/pkg/httpx"
"github.com/ccfos/nightingale/v6/pkg/logx"
"github.com/ccfos/nightingale/v6/pkg/ormx"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/ccfos/nightingale/v6/storage"
"github.com/gin-gonic/gin"
)
type ConfigType struct {
Global GlobalConfig
Log logx.Config
HTTP httpx.Config
DB ormx.DBConfig
Redis storage.RedisConfig
CenterApi CenterApi
Pushgw pconf.Pushgw
Alert aconf.Alert
Center cconf.Center
}
type CenterApi struct {
Addrs []string
BasicAuth gin.Accounts
}
type GlobalConfig struct {
RunMode string
}
func InitConfig(configDir, cryptoKey string) (*ConfigType, error) {
var config = new(ConfigType)
if err := cfg.LoadConfigByDir(configDir, config); err != nil {
return nil, fmt.Errorf("failed to load configs of directory: %s error: %s", configDir, err)
}
config.Pushgw.PreCheck()
config.Alert.PreCheck()
config.Center.PreCheck()
err := decryptConfig(config, cryptoKey)
if err != nil {
return nil, err
}
if config.Alert.Heartbeat.IP == "" {
// auto detect
config.Alert.Heartbeat.IP = fmt.Sprint(GetOutboundIP())
if config.Alert.Heartbeat.IP == "" {
hostname, err := os.Hostname()
if err != nil {
fmt.Println("failed to get hostname:", err)
os.Exit(1)
}
if strings.Contains(hostname, "localhost") {
fmt.Println("Warning! hostname contains substring localhost, setting a more unique hostname is recommended")
}
config.Alert.Heartbeat.IP = hostname
}
}
config.Alert.Heartbeat.Endpoint = fmt.Sprintf("%s:%d", config.Alert.Heartbeat.IP, config.HTTP.Port)
return config, nil
}
func GetOutboundIP() net.IP {
conn, err := net.Dial("udp", "223.5.5.5:80")
if err != nil {
fmt.Println("auto get outbound ip fail:", err)
return []byte{}
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP
}

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