未验证 提交 4e6e70c1 编写于 作者: Q qinyening 提交者: GitHub

release v5.0.0-rc1 (#708)

* release v5.0.0-rc1
上级 2ef9a773

要显示的变更太多。

To preserve performance only 1000 of 1000+ files are displayed.
......@@ -23,4 +23,4 @@ jobs:
uses: actions/checkout@v2
- name: Build
run: ./control build
run: ./build.sh
......@@ -26,27 +26,24 @@ _test
/log*
/bin
/out
/meta
/pub
/build
/dist
/etc/*.local.yml
/etc/plugins/*.local.yml
/etc/log/log.test.json
/data*
/tarball
/run
/vendor
/tmp
/pub
.alerts
.idea
.index
.vscode
.DS_Store
.cache-loader
queries.active
/n9e-*
tmp/
main
Makefile
src/modules/monapi/plugins/snmp/
src/modules/monapi/plugins/oracle/
- 官网:[n9e.didiyun.com](https://n9e.didiyun.com/) 右上角切换版本,当下是v4,6月底7月初发布v5,没有上生产的用户建议等v5
- 官网:[n9e.didiyun.com](https://n9e.didiyun.com/) 右上角切换版本
- 招聘:前后端都要,base北京,薪资open,可将简历发至邮箱 `echo cWlueWVuaW5nQGRpZGlnbG9iYWwuY29t | base64 -d` 一起来做开源
- 加群:加入网友互助交流群,和IT同仁一起交流运维监控,扫描如下二维码,加小助手微信,备注“夜莺加群”,小助手会拉你入群
<img src="https://s3-gz01.didistatic.com/n9e-pub/image/n9e-invite.png" width="250" alt="Nightingale"/>
<img src="https://s3-gz01.didistatic.com/n9e-pub/image/n9e-invite.png" width="250" alt="Nightingale"/>
\ No newline at end of file
package alert
import "context"
func Start(ctx context.Context) {
go popEvent()
}
package alert
import (
"bytes"
"encoding/json"
"fmt"
"os/exec"
"sort"
"strconv"
"strings"
"time"
"github.com/didi/nightingale/v5/cache"
"github.com/didi/nightingale/v5/config"
"github.com/didi/nightingale/v5/judge"
"github.com/didi/nightingale/v5/models"
"github.com/toolkits/pkg/concurrent/semaphore"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/net/httplib"
"github.com/toolkits/pkg/sys"
)
func popEvent() {
sema := semaphore.NewSemaphore(config.Config.Alert.NotifyConcurrency)
duration := time.Duration(100) * time.Millisecond
for {
events := judge.EventQueue.PopBackBy(200)
if len(events) < 1 {
time.Sleep(duration)
continue
}
consume(events, sema)
}
}
func consume(events []interface{}, sema *semaphore.Semaphore) {
for i := range events {
if events[i] == nil {
continue
}
event := events[i].(*models.AlertEvent)
alertRule, exists := cache.AlertRules.Get(event.RuleId)
if !exists {
logger.Errorf("event_consume: alert rule not found, event:%+v", event)
continue
}
logger.Debugf("[event_consume_success][type:%v][event:%+v]", event.IsPromePull, event)
if isNoneffective(event, alertRule) {
// 告警规则非生效时段
continue
}
event.RuleName = alertRule.Name
event.RuleNote = alertRule.Note
event.NotifyChannels = alertRule.NotifyChannels
classpaths := cache.ResClasspath.GetValues(event.ResIdent)
event.ResClasspaths = strings.Join(classpaths, " ")
enrichTag(event, alertRule)
if isEventMute(event) {
// 被屏蔽的事件
event.MarkMuted()
if config.Config.Alert.MutedAlertPersist {
err := event.Add()
if err != nil {
logger.Warningf("event_consume: insert muted event err:%v, event:%+v", err, event)
}
}
continue
}
// 操作数据库
persist(event)
// 不管是告警还是恢复,都触发回调,接收端自己处理
if alertRule.Callbacks != "" {
go callback(event, alertRule)
}
uids := genNotifyUserIDs(alertRule)
if len(uids) == 0 {
logger.Warningf("event_consume: notify users not found, event:%+v", event)
continue
}
users := cache.UserCache.GetByIds(uids)
if len(users) == 0 {
logger.Warningf("event_consume: notify users not found, event:%+v", event)
continue
}
alertMsg := AlertMsg{
Event: event,
Rule: alertRule,
Users: users,
}
logger.Infof("event_consume: notify alert:%+v", alertMsg)
sema.Acquire()
go func(alertMsg AlertMsg) {
defer sema.Release()
notify(alertMsg)
}(alertMsg)
}
}
func genNotifyUserIDs(alertRule *models.AlertRule) []int64 {
uidMap := make(map[int64]struct{})
groupIds := strings.Fields(alertRule.NotifyGroups)
for _, groupId := range groupIds {
gid, err := strconv.ParseInt(groupId, 10, 64)
if err != nil {
logger.Warningf("event_consume: strconv groupid(%s) fail: %v", groupId, err)
continue
}
um, exists := cache.UserGroupMember.Get(gid)
if !exists {
continue
}
for uid := range um {
uidMap[uid] = struct{}{}
}
}
userIds := strings.Fields(alertRule.NotifyUsers)
for _, userId := range userIds {
uid, err := strconv.ParseInt(userId, 10, 64)
if err != nil {
logger.Warningf("event_consume: strconv userid(%s) fail: %v", userId, err)
continue
}
uidMap[uid] = struct{}{}
}
uids := make([]int64, 0, len(uidMap))
for uid := range uidMap {
uids = append(uids, uid)
}
return uids
}
// 如果是告警,就存库,如果是恢复,就从未恢复的告警表里删除
func persist(event *models.AlertEvent) {
if event.IsRecov() {
err := event.DelByHashId()
if err != nil {
logger.Warningf("event_consume: delete recovery event err:%v, event:%+v", err, event)
}
} else {
err := event.Add()
if err != nil {
logger.Warningf("event_consume: insert alert event err:%v, event:%+v", err, event)
}
}
}
type AlertMsg struct {
Event *models.AlertEvent `json:"event"`
Rule *models.AlertRule `json:"rule"`
Users []*models.User `json:"users"`
}
func notify(alertMsg AlertMsg) {
//增加并发控制
bs, err := json.Marshal(alertMsg)
if err != nil {
logger.Errorf("notify: marshal alert %+v err:%v", alertMsg, err)
}
fpath := config.Config.Alert.NotifyScriptPath
cmd := exec.Command(fpath)
cmd.Stdin = bytes.NewReader(bs)
// combine stdout and stderr
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
err = cmd.Start()
if err != nil {
logger.Errorf("notify: run cmd err:%v", err)
return
}
err, isTimeout := sys.WrapTimeout(cmd, time.Duration(10)*time.Second)
if isTimeout {
if err == nil {
logger.Errorf("notify: timeout and killed process %s", fpath)
}
if err != nil {
logger.Errorf("notify: kill process %s occur error %v", fpath, err)
}
return
}
if err != nil {
logger.Errorf("notify: exec script %s occur error: %v", fpath, err)
return
}
logger.Infof("notify: exec %s output: %s", fpath, buf.String())
}
func callback(event *models.AlertEvent, alertRule *models.AlertRule) {
urls := strings.Fields(alertRule.Callbacks)
for _, url := range urls {
if url == "" {
continue
}
if !(strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")) {
url = "http://" + url
}
resp, code, err := httplib.PostJSON(url, 5*time.Second, event, map[string]string{})
if err != nil {
logger.Errorf("callback[%s] fail, callback content: %+v, resp: %s, err: %v, code:%d", url, event, string(resp), err, code)
} else {
logger.Infof("callback[%s] succ, callback content: %+v, resp: %s, code:%d", url, event, string(resp), code)
}
}
}
func isNoneffective(event *models.AlertEvent, alertRule *models.AlertRule) bool {
// 生效时间过滤
if alertRule.Status == models.ALERT_RULE_DISABLED {
logger.Debugf("event:%+v alert rule:%+v disable", event, alertRule)
return true
}
tm := time.Unix(event.TriggerTime, 0)
triggerTime := tm.Format("15:04")
triggerWeek := strconv.Itoa(int(tm.Weekday()))
if alertRule.EnableStime <= alertRule.EnableEtime {
if triggerTime < alertRule.EnableStime || triggerTime > alertRule.EnableEtime {
logger.Debugf("event:%+v alert rule:%+v triggerTime Noneffective", event, alertRule)
return true
}
} else {
if triggerTime < alertRule.EnableStime && triggerTime > alertRule.EnableEtime {
logger.Debugf("event:%+v alert rule:%+v triggerTime Noneffective", event, alertRule)
return true
}
}
if !strings.Contains(alertRule.EnableDaysOfWeek, triggerWeek) {
logger.Debugf("event:%+v alert rule:%+v triggerWeek Noneffective", event, alertRule)
return true
}
return false
}
// 事件的tags有多种tags组成:ident作为一个tag,数据本身的tags(前期已经把res的tags也附到数据tags里了)、规则的tags
func enrichTag(event *models.AlertEvent, alertRule *models.AlertRule) {
if event.ResIdent != "" {
event.TagMap["ident"] = event.ResIdent
}
if alertRule.AppendTags != "" {
appendTags := strings.Fields(alertRule.AppendTags)
for _, tag := range appendTags {
arr := strings.Split(tag, "=")
if len(arr) != 2 {
logger.Warningf("alertRule AppendTags:%+v illagel", alertRule.AppendTags)
continue
}
event.TagMap[arr[0]] = arr[1]
}
}
var tagList []string
for key, value := range event.TagMap {
tagList = append(tagList, fmt.Sprintf("%s=%s", key, value))
}
sort.Strings(tagList)
event.Tags = strings.Join(tagList, " ")
}
package alert
import (
"github.com/didi/nightingale/v5/cache"
"github.com/didi/nightingale/v5/models"
"github.com/toolkits/pkg/logger"
)
func isEventMute(event *models.AlertEvent) bool {
historyPoints, err := event.GetHistoryPoints()
if err != nil {
logger.Errorf("get event HistoryPoints:%+v failed, err: %v", event.HistoryPoints, err)
return false
}
// 先去匹配一下metric为空的mute
if matchMute("", event.ResIdent, event.TagMap) {
return true
}
// 如果是与条件,就会有多个metric,任一个匹配了屏蔽规则都算被屏蔽
for i := 0; i < len(historyPoints); i++ {
if matchMute(historyPoints[i].Metric, event.ResIdent, event.TagMap) {
return true
}
}
resAndTags, exists := cache.ResTags.Get(event.ResIdent)
if exists {
if event.TriggerTime > resAndTags.Resource.MuteBtime && event.TriggerTime < resAndTags.Resource.MuteEtime {
return true
}
}
return false
}
func matchMute(metric, ident string, tags map[string]string) bool {
filters, exists := cache.AlertMute.GetByKey(metric)
if !exists {
// 没有屏蔽规则跟这个事件相关
return false
}
// 只要有一个屏蔽规则命中,那这个事件就是被屏蔽了
for _, filter := range filters {
if matchMuteOnce(filter, ident, tags) {
return true
}
}
return false
}
func matchMuteOnce(filter cache.Filter, ident string, tags map[string]string) bool {
if filter.ResReg != nil && !filter.ResReg.MatchString(ident) {
// 比如屏蔽规则配置的是:c3-ceph.*
// 当前事件的资源标识是:c4-ceph01.bj
// 只要有任一点不满足,那这个屏蔽规则也没有继续验证下去的必要
return false
}
// 每个mute中的tags都得出现在event.tags,否则就是不匹配
return mapContains(tags, filter.TagsMap)
}
func mapContains(big, small map[string]string) bool {
for tagk, tagv := range small {
val, exists := big[tagk]
if !exists {
return false
}
if val != tagv {
return false
}
}
return true
}
package backend
import (
"fmt"
"github.com/prometheus/prometheus/promql"
"github.com/didi/nightingale/v5/vos"
"github.com/toolkits/pkg/container/list"
pp "github.com/didi/nightingale/v5/backend/prome"
)
type BackendSection struct {
DataSource string `yaml:"datasource"`
Prometheus pp.PromeSection `yaml:"prometheus"`
}
type DataSource interface {
PushEndpoint
QueryData(inputs vos.DataQueryParam) []*vos.DataQueryResp // 查询一段时间
QueryTagKeys(recv vos.CommonTagQueryParam) *vos.TagKeyQueryResp // 获取标签的names
QueryTagValues(recv vos.CommonTagQueryParam) *vos.TagValueQueryResp // 根据一个label_name获取 values
QueryTagPairs(recv vos.CommonTagQueryParam) *vos.TagPairQueryResp // 根据匹配拿到所有 series 上面三个使用统一的结构体
QueryMetrics(recv vos.MetricQueryParam) *vos.MetricQueryResp // 根据标签查 metric_names
QueryVector(ql string) promql.Vector // prometheus pull alert 所用,其他数据源留空即可
CleanUp() // 数据源退出时需要做的清理工作
}
type PushEndpoint interface {
Push2Queue(items []*vos.MetricPoint)
}
var (
defaultDataSource string
registryDataSources = make(map[string]DataSource)
registryPushEndpoints = make(map[string]PushEndpoint)
)
func Init(cfg BackendSection) {
defaultDataSource = cfg.DataSource
// init prometheus
if cfg.Prometheus.Enable {
promeDs := &pp.PromeDataSource{
Section: cfg.Prometheus,
PushQueue: list.NewSafeListLimited(10240000),
}
promeDs.Init()
RegisterDataSource(cfg.Prometheus.Name, promeDs)
}
}
// get backend datasource
// (pluginId == "" for default datasource)
func GetDataSourceFor(pluginId string) (DataSource, error) {
if pluginId == "" {
pluginId = defaultDataSource
}
if source, exists := registryDataSources[pluginId]; exists {
return source, nil
}
return nil, fmt.Errorf("could not find datasource for plugin: %s", pluginId)
}
func DatasourceCleanUp() {
for _, ds := range registryDataSources {
ds.CleanUp()
}
}
// get all push endpoints
func GetPushEndpoints() ([]PushEndpoint, error) {
if len(registryPushEndpoints) > 0 {
items := make([]PushEndpoint, 0, len(registryPushEndpoints))
for _, value := range registryPushEndpoints {
items = append(items, value)
}
return items, nil
}
return nil, fmt.Errorf("could not find any pushendpoint")
}
func RegisterDataSource(pluginId string, datasource DataSource) {
registryDataSources[pluginId] = datasource
registryPushEndpoints[pluginId] = datasource
}
package backend
import (
"bufio"
"bytes"
"context"
"io"
"io/ioutil"
"net/http"
"regexp"
"time"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/opentracing-contrib/go-stdlib/nethttp"
"github.com/opentracing/opentracing-go"
"github.com/pkg/errors"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/pkg/labels"
"github.com/prometheus/prometheus/prompb"
"github.com/didi/nightingale/v5/vos"
)
var MetricNameRE = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`)
type sample struct {
labels labels.Labels
t int64
v float64
}
func labelsToLabelsProto(labels labels.Labels, buf []prompb.Label) []prompb.Label {
result := buf[:0]
if cap(buf) < len(labels) {
result = make([]prompb.Label, 0, len(labels))
}
for _, l := range labels {
result = append(result, prompb.Label{
Name: l.Name,
Value: l.Value,
})
}
return result
}
func (pd *PromeDataSource) convertOne(item *vos.MetricPoint) (prompb.TimeSeries, error) {
pt := prompb.TimeSeries{}
pt.Samples = []prompb.Sample{{}}
s := sample{}
s.t = item.Time
s.v = item.Value
// name
if !MetricNameRE.MatchString(item.Metric) {
return pt, errors.New("invalid metrics name")
}
nameLs := labels.Label{
Name: LABEL_NAME,
Value: item.Metric,
}
s.labels = append(s.labels, nameLs)
if item.Ident != "" {
identLs := labels.Label{
Name: LABEL_IDENT,
Value: item.Ident,
}
s.labels = append(s.labels, identLs)
}
for k, v := range item.TagsMap {
if model.LabelNameRE.MatchString(k) {
ls := labels.Label{
Name: k,
Value: v,
}
s.labels = append(s.labels, ls)
}
}
pt.Labels = labelsToLabelsProto(s.labels, pt.Labels)
// 时间赋值问题,使用毫秒时间戳
tsMs := time.Unix(s.t, 0).UnixNano() / 1e6
pt.Samples[0].Timestamp = tsMs
pt.Samples[0].Value = s.v
return pt, nil
}
type RecoverableError struct {
error
}
func remoteWritePost(c *HttpClient, req []byte) error {
httpReq, err := http.NewRequest("POST", c.url.String(), bytes.NewReader(req))
if err != nil {
// Errors from NewRequest are from unparsable URLs, so are not
// recoverable.
return err
}
httpReq.Header.Add("Content-Encoding", "snappy")
httpReq.Header.Set("Content-Type", "application/x-protobuf")
httpReq.Header.Set("User-Agent", "n9e-v5")
httpReq.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
httpReq = httpReq.WithContext(ctx)
if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
var ht *nethttp.Tracer
httpReq, ht = nethttp.TraceRequest(
parentSpan.Tracer(),
httpReq,
nethttp.OperationName("Remote Store"),
nethttp.ClientTrace(false),
)
defer ht.Finish()
}
httpResp, err := c.Client.Do(httpReq)
if err != nil {
// Errors from Client.Do are from (for example) network errors, so are
// recoverable.
return RecoverableError{err}
}
defer func() {
io.Copy(ioutil.Discard, httpResp.Body)
httpResp.Body.Close()
}()
if httpResp.StatusCode/100 != 2 {
scanner := bufio.NewScanner(io.LimitReader(httpResp.Body, 512))
line := ""
if scanner.Scan() {
line = scanner.Text()
}
err = errors.Errorf("server returned HTTP status %s: %s", httpResp.Status, line)
}
if httpResp.StatusCode/100 == 5 {
return RecoverableError{err}
}
return err
}
func (pd *PromeDataSource) buildWriteRequest(samples []prompb.TimeSeries) ([]byte, error) {
req := &prompb.WriteRequest{
Timeseries: samples,
Metadata: nil,
}
data, err := proto.Marshal(req)
if err != nil {
return nil, err
}
compressed := snappy.Encode(nil, data)
return compressed, nil
}
package backend
import (
"io/ioutil"
"net/http"
"net/url"
"os"
"time"
"github.com/go-kit/kit/log"
"github.com/prometheus/client_golang/prometheus"
config_util "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promlog"
pc "github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/storage/remote"
"github.com/toolkits/pkg/container/list"
"github.com/toolkits/pkg/logger"
"go.uber.org/atomic"
"github.com/didi/nightingale/v5/vos"
)
const (
DefaultPopNum = 1000
)
type PromeSection struct {
Enable bool `yaml:"enable"`
Name string `yaml:"name"`
Batch int `yaml:"batch"`
MaxRetry int `yaml:"maxRetry"`
LookbackDeltaMinute int `yaml:"lookbackDeltaMinute"`
MaxConcurrentQuery int `yaml:"maxConcurrentQuery"`
MaxSamples int `yaml:"maxSamples"`
MaxFetchAllSeriesLimitMinute int64 `yaml:"maxFetchAllSeriesLimitMinute"`
RemoteWrite []RemoteConfig `yaml:"remoteWrite"`
RemoteRead []RemoteConfig `yaml:"remoteRead"`
}
type RemoteConfig struct {
Name string `yaml:"name"`
Url string `yaml:"url"`
RemoteTimeoutSecond int `yaml:"remoteTimeoutSecond"`
}
type PromeDataSource struct {
Section PromeSection
LocalTmpDir string
// 除了promql的查询,需要后端存储
Queryable storage.SampleAndChunkQueryable
// promql相关查询
QueryEngine *promql.Engine
PushQueue *list.SafeListLimited
WriteTargets []*HttpClient
}
type safePromQLNoStepSubqueryInterval struct {
value atomic.Int64
}
type HttpClient struct {
remoteName string // Used to differentiate clients in metrics.
url *url.URL
Client *http.Client
timeout time.Duration
}
func durationToInt64Millis(d time.Duration) int64 {
return int64(d / time.Millisecond)
}
func (i *safePromQLNoStepSubqueryInterval) Set(ev model.Duration) {
i.value.Store(durationToInt64Millis(time.Duration(ev)))
}
func (i *safePromQLNoStepSubqueryInterval) Get(int64) int64 {
return i.value.Load()
}
func (pd *PromeDataSource) CleanUp() {
err := os.RemoveAll(pd.LocalTmpDir)
logger.Infof("[remove_prome_tmp_dir_err][dir:%+v][err: %v]", pd.LocalTmpDir, err)
}
func (pd *PromeDataSource) Init() {
// 模拟创建本地存储目录
dbDir, err := ioutil.TempDir("", "tsdb-api-ready")
if err != nil {
logger.Errorf("[error_create_local_tsdb_dir][err: %v]", err)
return
}
pd.LocalTmpDir = dbDir
promlogConfig := promlog.Config{}
// 使用本地目录创建remote-storage
remoteS := remote.NewStorage(promlog.New(&promlogConfig), prometheus.DefaultRegisterer, func() (int64, error) {
return 0, nil
}, dbDir, 1*time.Minute, nil)
// ApplyConfig 加载queryables
remoteReadC := make([]*pc.RemoteReadConfig, 0)
for _, u := range pd.Section.RemoteRead {
ur, err := url.Parse(u.Url)
if err != nil {
logger.Errorf("[prome_ds_init_error][parse_url_error][url:%+v][err:%+v]", u.Url, err)
continue
}
remoteReadC = append(remoteReadC,
&pc.RemoteReadConfig{
URL: &config_util.URL{URL: ur},
RemoteTimeout: model.Duration(time.Duration(u.RemoteTimeoutSecond) * time.Second),
ReadRecent: true,
},
)
}
if len(remoteReadC) == 0 {
logger.Errorf("[prome_ds_error_got_zero_remote_read_storage]")
return
}
err = remoteS.ApplyConfig(&pc.Config{RemoteReadConfigs: remoteReadC})
if err != nil {
logger.Errorf("[error_load_remote_read_config][err: %v]", err)
return
}
pLogger := log.NewNopLogger()
noStepSubqueryInterval := &safePromQLNoStepSubqueryInterval{}
queryQueueDir, err := ioutil.TempDir(dbDir, "prom_query_concurrency")
opts := promql.EngineOpts{
Logger: log.With(pLogger, "component", "query engine"),
Reg: prometheus.DefaultRegisterer,
MaxSamples: pd.Section.MaxSamples,
Timeout: 30 * time.Second,
ActiveQueryTracker: promql.NewActiveQueryTracker(queryQueueDir, pd.Section.MaxConcurrentQuery, log.With(pLogger, "component", "activeQueryTracker")),
LookbackDelta: time.Duration(pd.Section.LookbackDeltaMinute) * time.Minute,
NoStepSubqueryIntervalFn: noStepSubqueryInterval.Get,
EnableAtModifier: true,
}
queryEngine := promql.NewEngine(opts)
pd.QueryEngine = queryEngine
pd.Queryable = remoteS
// 初始化writeClients
if len(pd.Section.RemoteWrite) == 0 {
logger.Warningf("[prome_ds_init_with_zero_RemoteWrite_target]")
logger.Infof("[successfully_init_prometheus_datasource][remote_read_num:%+v][remote_write_num:%+v]",
len(pd.Section.RemoteRead),
len(pd.Section.RemoteWrite),
)
return
}
writeTs := make([]*HttpClient, 0)
for _, u := range pd.Section.RemoteWrite {
ur, err := url.Parse(u.Url)
if err != nil {
logger.Errorf("[prome_ds_init_error][parse_url_error][url:%+v][err:%+v]", u.Url, err)
continue
}
writeTs = append(writeTs,
&HttpClient{
remoteName: u.Name,
url: ur,
Client: &http.Client{},
timeout: time.Duration(u.RemoteTimeoutSecond) * time.Second,
})
}
pd.WriteTargets = writeTs
// 开启prometheus 队列消费协程
go pd.remoteWrite()
logger.Infof("[successfully_init_prometheus_datasource][remote_read_num:%+v][remote_write_num:%+v]",
len(remoteReadC),
len(writeTs),
)
}
func (pd *PromeDataSource) Push2Queue(points []*vos.MetricPoint) {
for _, point := range points {
pt, err := pd.convertOne(point)
if err != nil {
logger.Errorf("[prome_convertOne_error][point: %+v][err:%s]", point, err)
continue
}
ok := pd.PushQueue.PushFront(pt)
if !ok {
logger.Errorf("[prome_push_queue_error][point: %+v] ", point)
}
}
}
func (pd *PromeDataSource) remoteWrite() {
batch := pd.Section.Batch // 一次发送,最多batch条数据
if batch <= 0 {
batch = DefaultPopNum
}
for {
items := pd.PushQueue.PopBackBy(batch)
count := len(items)
if count == 0 {
time.Sleep(time.Millisecond * 100)
continue
}
pbItems := make([]prompb.TimeSeries, count)
for i := 0; i < count; i++ {
pbItems[i] = items[i].(prompb.TimeSeries)
}
payload, err := pd.buildWriteRequest(pbItems)
if err != nil {
logger.Errorf("[prome_remote_write_error][pb_marshal_error][items: %+v][pb.err: %v]: ", items, err)
continue
}
pd.processWrite(payload)
}
}
func (pd *PromeDataSource) processWrite(payload []byte) {
retry := pd.Section.MaxRetry
for _, c := range pd.WriteTargets {
newC := c
go func(cc *HttpClient, payload []byte) {
sendOk := false
var err error
for i := 0; i < retry; i++ {
err := remoteWritePost(cc, payload)
if err == nil {
sendOk = true
break
}
err, ok := err.(RecoverableError)
if !ok {
break
}
logger.Warningf("send prome fail: %v", err)
time.Sleep(time.Millisecond * 100)
}
if !sendOk {
logger.Warningf("send prome finally fail: %v", err)
} else {
logger.Infof("send to prome %s ok", cc.url.String())
}
}(newC, payload)
}
}
package backend
import (
"context"
"errors"
"fmt"
"math"
"sort"
"strings"
"time"
"github.com/prometheus/prometheus/pkg/labels"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/storage"
"github.com/toolkits/pkg/logger"
"github.com/didi/nightingale/v5/cache"
"github.com/didi/nightingale/v5/models"
"github.com/didi/nightingale/v5/vos"
)
const (
LABEL_IDENT = "ident"
LABEL_NAME = "__name__"
DEFAULT_QL = `{__name__=~".*a.*|.*e.*"}`
DEFAULT_STEP = 14
)
type commonQueryObj struct {
Idents []string
TagPairs []*vos.TagPair
Metric string
Start int64
End int64
MetricNameExact bool // metric_name精确匹配,在查询看图的时候为true
From string // 调用的来源
}
// 为查询索引或标签相关的转换,大部分都是正则匹配
func convertToPromql(recv *commonQueryObj) string {
qlStr := ""
qlStrFinal := ""
metricName := ""
labelIdent := ""
labelStrSlice := make([]string, 0)
// 匹配metric_name __name__=~"xx.*"
if recv.Metric != "" {
if recv.MetricNameExact {
metricName = fmt.Sprintf(`__name__="%s"`, recv.Metric)
} else {
metricName = fmt.Sprintf(`__name__=~".*%s.*"`, recv.Metric)
}
labelStrSlice = append(labelStrSlice, metricName)
}
// 匹配ident=~"k1.*|k2.*"
for _, i := range recv.Idents {
if i != "" {
labelIdent += fmt.Sprintf(`.*%s.*|`, i)
}
}
labelIdent = strings.TrimRight(labelIdent, "|")
if labelIdent != "" {
labelStrSlice = append(labelStrSlice, fmt.Sprintf(`ident=~"%s"`, labelIdent))
}
// 匹配标签
labelM := make(map[string]string)
for _, i := range recv.TagPairs {
if i.Key == "" {
continue
}
lastStr, _ := labelM[i.Key]
lastStr += fmt.Sprintf(`.*%s.*|`, i.Value)
labelM[i.Key] = lastStr
}
for k, v := range labelM {
thisLabel := strings.TrimRight(v, "|")
labelStrSlice = append(labelStrSlice, fmt.Sprintf(`%s=~"%s"`, k, thisLabel))
}
for _, s := range labelStrSlice {
qlStr += fmt.Sprintf(`%s,`, s)
}
qlStr = strings.TrimRight(qlStr, ",")
qlStrFinal = fmt.Sprintf(`{%s}`, qlStr)
logger.Debugf("[convertToPromql][type=queryLabel][recv:%+v][qlStrFinal:%s]", recv, qlStrFinal)
return qlStrFinal
}
// 查询数据的转换,metrics_name和标签都是精确匹配
func convertToPromqlForQueryData(recv *commonQueryObj) string {
qlStr := ""
qlStrFinal := ""
metricName := ""
labelIdent := ""
labelStrSlice := make([]string, 0)
// 匹配metric_name __name__=~"xx.*"
if recv.Metric != "" {
metricName = fmt.Sprintf(`__name__="%s"`, recv.Metric)
labelStrSlice = append(labelStrSlice, metricName)
}
// 匹配ident=~"k1.*|k2.*"
for _, i := range recv.Idents {
if i != "" {
labelIdent += fmt.Sprintf(`%s|`, i)
}
}
labelIdent = strings.TrimRight(labelIdent, "|")
if labelIdent != "" {
labelStrSlice = append(labelStrSlice, fmt.Sprintf(`ident=~"%s"`, labelIdent))
}
// 匹配标签
labelM := make(map[string]string)
for _, i := range recv.TagPairs {
if i.Key == "" {
continue
}
lastStr, _ := labelM[i.Key]
lastStr += fmt.Sprintf(`%s|`, i.Value)
labelM[i.Key] = lastStr
}
for k, v := range labelM {
thisLabel := strings.TrimRight(v, "|")
labelStrSlice = append(labelStrSlice, fmt.Sprintf(`%s=~"%s"`, k, thisLabel))
}
for _, s := range labelStrSlice {
qlStr += fmt.Sprintf(`%s,`, s)
}
qlStr = strings.TrimRight(qlStr, ",")
qlStrFinal = fmt.Sprintf(`{%s}`, qlStr)
logger.Debugf("[convertToPromql][type=queryData][recv:%+v][qlStrFinal:%s]", recv, qlStrFinal)
return qlStrFinal
}
func parseMatchersParam(matchers []string) ([][]*labels.Matcher, error) {
var matcherSets [][]*labels.Matcher
for _, s := range matchers {
matchers, err := parser.ParseMetricSelector(s)
if err != nil {
return nil, err
}
matcherSets = append(matcherSets, matchers)
}
OUTER:
for _, ms := range matcherSets {
for _, lm := range ms {
if lm != nil && !lm.Matches("") {
continue OUTER
}
}
return nil, errors.New("match[] must contain at least one non-empty matcher")
}
return matcherSets, nil
}
func (pd *PromeDataSource) QueryData(inputs vos.DataQueryParam) []*vos.DataQueryResp {
respD := make([]*vos.DataQueryResp, 0)
for _, input := range inputs.Params {
var qlStrFinal string
if input.PromeQl != "" {
qlStrFinal = input.PromeQl
} else {
if len(input.Idents) == 0 {
for i := range input.TagPairs {
if input.TagPairs[i].Key == "ident" {
input.Idents = append(input.Idents, input.TagPairs[i].Value)
}
}
}
if len(input.Idents) == 0 && input.ClasspathId != 0 {
if input.ClasspathPrefix == 0 {
classpathAndRes, exists := cache.ClasspathRes.Get(input.ClasspathId)
if exists {
input.Idents = classpathAndRes.Res
}
} else {
classpath, err := models.ClasspathGet("id=?", input.ClasspathId)
if err != nil {
continue
}
cps, _ := models.ClasspathGetsByPrefix(classpath.Path)
for _, classpath := range cps {
classpathAndRes, exists := cache.ClasspathRes.Get(classpath.Id)
if exists {
idents := classpathAndRes.Res
input.Idents = append(input.Idents, idents...)
}
}
}
}
cj := &commonQueryObj{
Idents: input.Idents,
TagPairs: input.TagPairs,
Metric: input.Metric,
Start: inputs.Start,
End: inputs.End,
MetricNameExact: true,
}
qlStrFinal = convertToPromqlForQueryData(cj)
}
logger.Debugf("[input:%+v][qlStrFinal:%s]\n", input, qlStrFinal)
// 转化为utc时间
startT := tsToUtcTs(inputs.Start)
endT := tsToUtcTs(inputs.End)
// TODO 前端传入分辨率还是后端计算,grafana和prometheus ui都是前端传入
delta := (inputs.End - inputs.Start) / 3600
if delta <= 0 {
delta = 1
}
resolution := time.Second * time.Duration(delta*DEFAULT_STEP)
q, err := pd.QueryEngine.NewRangeQuery(pd.Queryable, qlStrFinal, startT, endT, resolution)
if err != nil {
logger.Errorf("[prome_query_error][QueryData_error_may_be_parse_ql_error][args:%+v][err:%+v]", input, err)
continue
}
ctx, _ := context.WithTimeout(context.Background(), time.Second*30)
res := q.Exec(ctx)
// TODO 将err返回给前端
if res.Err != nil {
logger.Errorf("[prome_query_error][rangeQuery_exec_error][args:%+v][err:%+v]", input, res.Err)
q.Close()
continue
}
mat, ok := res.Value.(promql.Matrix)
if !ok {
logger.Errorf("[promql.Engine.exec: invalid expression type %q]", res.Value.Type())
q.Close()
continue
}
if res.Err != nil {
logger.Errorf("[prome_query_error][res.Matrix_error][args:%+v][err:%+v]", input, res.Err)
q.Close()
continue
}
for index, m := range mat {
if inputs.Limit > 0 && index+1 > inputs.Limit {
continue
}
tagStr := ""
oneResp := &vos.DataQueryResp{}
ident := m.Metric.Get(LABEL_IDENT)
name := m.Metric.Get(LABEL_NAME)
oneResp.Metric = name
oneResp.Ident = ident
// TODO 去掉point num
pNum := len(m.Points)
for _, p := range m.Points {
tmpP := &vos.Point{
Timestamp: p.T,
Value: vos.JsonFloat(p.V),
}
oneResp.Values = append(oneResp.Values, tmpP)
}
for _, x := range m.Metric {
if x.Name == LABEL_NAME {
continue
}
tagStr += fmt.Sprintf("%s=%s,", x.Name, x.Value)
}
tagStr = strings.TrimRight(tagStr, ",")
oneResp.Tags = tagStr
oneResp.Resolution = delta * DEFAULT_STEP
oneResp.PNum = pNum
respD = append(respD, oneResp)
}
q.Close()
}
return respD
}
func tsToUtcTs(s int64) time.Time {
return time.Unix(s, 0).UTC()
}
func timeParse(ts int64) time.Time {
t := float64(ts)
s, ns := math.Modf(t)
ns = math.Round(ns*1000) / 1000
return time.Unix(int64(s), int64(ns*float64(time.Second))).UTC()
}
func millisecondTs(t time.Time) int64 {
return t.Unix()*1000 + int64(t.Nanosecond())/int64(time.Millisecond)
}
func tsToStr(timestamp int64) string {
timeNow := time.Unix(timestamp, 0)
return timeNow.Format("2006-01-02 15:04:05")
}
func (pd *PromeDataSource) CommonQuerySeries(cj *commonQueryObj) storage.SeriesSet {
qlStrFinal := convertToPromql(cj)
if qlStrFinal == "{}" {
qlStrFinal = DEFAULT_QL
reqMinute := (cj.End - cj.Start) / 60
// 如果前端啥都没传,要限制下查询series的时间范围,防止高基础查询
if reqMinute > pd.Section.MaxFetchAllSeriesLimitMinute {
// 时间超长,用配置文件中的限制一下
now := time.Now().Unix()
cj.End = now
cj.Start = now - pd.Section.MaxFetchAllSeriesLimitMinute*60
logger.Debugf("[CommonQuerySeries.FetchAllSeries.LimitQueryTimeRange][start:%v][end:%v]", cj.Start, cj.End)
}
}
matcherSets, err := parseMatchersParam([]string{qlStrFinal})
if err != nil {
logger.Errorf("[prome_query_error][parse_label_match_error][err:%+v]", err)
return nil
}
now := time.Now().Unix()
if cj.Start == 0 {
cj.Start = now - 60*pd.Section.MaxFetchAllSeriesLimitMinute
}
if cj.End == 0 {
cj.End = now
}
startT := millisecondTs(timeParse(cj.Start))
endT := millisecondTs(timeParse(cj.End))
ctx, _ := context.WithTimeout(context.Background(), time.Second*30)
q, err := pd.Queryable.Querier(ctx, startT, endT)
if err != nil {
logger.Errorf("[prome_query_error][get_querier_errro]")
return nil
}
logger.Debugf("[CommonQuerySeries.Result][from:%s][cj.start_ts:%+v cj.start_str:%+v SelectHints.startT:%+v][cj.end_ts:%+v cj.end_str:%+v SelectHints.endT:%+v][qlStrFinal:%s][cj:%+v]",
cj.From,
cj.Start,
tsToStr(cj.Start),
startT,
cj.End,
tsToStr(cj.End),
endT,
qlStrFinal,
cj,
)
defer q.Close()
hints := &storage.SelectHints{
Start: startT,
End: endT,
Func: "series", // There is no series function, this token is used for lookups that don't need samples.
}
// Get all series which match matchers.
s := q.Select(true, hints, matcherSets[0]...)
return s
}
// 全部转化为 {__name__="a",label_a!="b",label_b=~"d|c",label_c!~"d"}
// 对应prometheus 中的 /api/v1/labels
// TODO 等待prometheus官方对 remote_read label_values 的支持
// Implement: https://github.com/prometheus/prometheus/issues/3351
func (pd *PromeDataSource) QueryTagKeys(recv vos.CommonTagQueryParam) *vos.TagKeyQueryResp {
// TODO 完成标签匹配模式
respD := &vos.TagKeyQueryResp{
Keys: make([]string, 0),
}
labelNamesSet := make(map[string]struct{})
for _, x := range recv.Params {
cj := &commonQueryObj{
Idents: x.Idents,
TagPairs: recv.TagPairs,
Metric: x.Metric,
Start: recv.Start,
End: recv.End,
From: "QueryTagKeys",
}
s := pd.CommonQuerySeries(cj)
if s.Warnings() != nil {
logger.Warningf("[prome_query_error][series_set_iter_error][warning:%+v]", s.Warnings())
}
if err := s.Err(); err != nil {
logger.Errorf("[prome_query_error][series_set_iter_error][err:%+v]", err)
continue
}
for s.Next() {
series := s.At()
for _, lb := range series.Labels() {
if lb.Name == LABEL_NAME {
continue
}
if recv.TagKey != "" {
if !strings.Contains(lb.Name, recv.TagKey) {
continue
}
}
labelNamesSet[lb.Name] = struct{}{}
}
}
}
names := make([]string, 0)
for key := range labelNamesSet {
names = append(names, key)
}
sort.Strings(names)
// 因为map中的key是无序的,必须这样才能稳定输出
if recv.Limit > 0 && len(names) > recv.Limit {
names = names[:recv.Limit]
}
respD.Keys = names
return respD
}
// 对应prometheus 中的 /api/v1/label/<label_name>/values
func (pd *PromeDataSource) QueryTagValues(recv vos.CommonTagQueryParam) *vos.TagValueQueryResp {
labelValuesSet := make(map[string]struct{})
for _, x := range recv.Params {
cj := &commonQueryObj{
Idents: x.Idents,
Metric: x.Metric,
TagPairs: recv.TagPairs,
Start: recv.Start,
End: recv.End,
From: "QueryTagValues",
}
s := pd.CommonQuerySeries(cj)
if s.Warnings() != nil {
logger.Warningf("[prome_query_error][series_set_iter_error][warning:%+v]", s.Warnings())
}
if err := s.Err(); err != nil {
logger.Errorf("[prome_query_error][series_set_iter_error][err:%+v]", err)
continue
}
for s.Next() {
series := s.At()
for _, lb := range series.Labels() {
if lb.Name == recv.TagKey {
if recv.TagValue != "" {
if !strings.Contains(lb.Value, recv.TagValue) {
continue
}
}
labelValuesSet[lb.Value] = struct{}{}
}
}
}
}
vals := make([]string, 0)
for val := range labelValuesSet {
vals = append(vals, val)
}
sort.Strings(vals)
if recv.Limit > 0 && len(vals) > recv.Limit {
vals = vals[:recv.Limit]
}
respD := &vos.TagValueQueryResp{}
respD.Values = vals
return respD
}
// 对应prometheus 中的 /api/v1/label/<label_name>/values label_name == __name__
func (pd *PromeDataSource) QueryMetrics(recv vos.MetricQueryParam) *vos.MetricQueryResp {
cj := &commonQueryObj{
Idents: recv.Idents,
Metric: recv.Metric,
TagPairs: recv.TagPairs,
Start: recv.Start,
End: recv.End,
From: "QueryMetrics",
}
respD := &vos.MetricQueryResp{}
respD.Metrics = make([]string, 0)
s := pd.CommonQuerySeries(cj)
for _, x := range s.Warnings() {
logger.Warningf("[prome_query_error][series_set_iter_error][warning:%+v]\n", x.Error())
}
if err := s.Err(); err != nil {
logger.Errorf("[prome_query_error][series_set_iter_error][err:%+v]", err)
return respD
}
var sets []storage.SeriesSet
sets = append(sets, s)
set := storage.NewMergeSeriesSet(sets, storage.ChainedSeriesMerge)
labelValuesSet := make(map[string]struct{})
//for s.Next() {
for set.Next() {
series := set.At()
for _, lb := range series.Labels() {
if lb.Name == LABEL_NAME {
labelValuesSet[lb.Value] = struct{}{}
}
}
}
vals := make([]string, 0)
for val := range labelValuesSet {
vals = append(vals, val)
}
sort.Strings(vals)
if recv.Limit > 0 && len(vals) > recv.Limit {
vals = vals[:recv.Limit]
}
respD.Metrics = vals
return respD
}
// 对应prometheus 中的 /api/v1/series
func (pd *PromeDataSource) QueryTagPairs(recv vos.CommonTagQueryParam) *vos.TagPairQueryResp {
respD := &vos.TagPairQueryResp{
TagPairs: make([]string, 0),
Idents: make([]string, 0),
}
tps := make(map[string]struct{})
for _, x := range recv.Params {
cj := &commonQueryObj{
Idents: x.Idents,
TagPairs: recv.TagPairs,
Metric: x.Metric,
Start: recv.Start,
End: recv.End,
From: "QueryTagPairs",
}
s := pd.CommonQuerySeries(cj)
if s.Warnings() != nil {
logger.Warningf("[prome_query_error][series_set_iter_error][warning:%+v]", s.Warnings())
}
if err := s.Err(); err != nil {
logger.Errorf("[prome_query_error][series_set_iter_error][err:%+v]", err)
continue
}
var sets []storage.SeriesSet
sets = append(sets, s)
set := storage.NewMergeSeriesSet(sets, storage.ChainedSeriesMerge)
labelIdents := make([]string, 0)
for set.Next() {
series := s.At()
labelsS := series.Labels()
for _, i := range labelsS {
if i.Name == LABEL_NAME {
continue
}
if i.Name == LABEL_IDENT {
labelIdents = append(labelIdents, i.Value)
}
tps[fmt.Sprintf("%s=%s", i.Name, i.Value)] = struct{}{}
}
}
}
newTags := make([]string, 0)
for k := range tps {
newTags = append(newTags, k)
}
sort.Strings(newTags)
if recv.Limit > 0 && len(newTags) > recv.Limit {
newTags = newTags[:recv.Limit]
}
respD.TagPairs = newTags
return respD
}
func (pd *PromeDataSource) QueryVector(ql string) promql.Vector {
t := time.Now()
q, err := pd.QueryEngine.NewInstantQuery(pd.Queryable, ql, t)
if err != nil {
logger.Errorf("[prome_query_error][new_insQuery_error][err:%+v][ql:%+v]", err, ql)
return nil
}
ctx := context.Background()
res := q.Exec(ctx)
if res.Err != nil {
logger.Errorf("[prome_query_error][insQuery_exec_error][err:%+v][ql:%+v]", err, ql)
return nil
}
defer q.Close()
switch v := res.Value.(type) {
case promql.Vector:
return v
case promql.Scalar:
return promql.Vector{promql.Sample{
Point: promql.Point(v),
Metric: labels.Labels{},
}}
default:
logger.Errorf("[prome_query_error][insQuery_res_error rule result is not a vector or scalar][err:%+v][ql:%+v]", err, ql)
return nil
}
}
#!/bin/bash
# release version
version=5.0.0-rc1
export GO111MODULE=on
go build -ldflags "-X main.version=${version}" -o n9e-server main.go
package cache
import (
"regexp"
"sync"
)
type AlertMuteMap struct {
sync.RWMutex
Data map[string][]Filter
}
type Filter struct {
ResReg *regexp.Regexp
TagsMap map[string]string
}
var AlertMute = &AlertMuteMap{Data: make(map[string][]Filter)}
func (a *AlertMuteMap) SetAll(m map[string][]Filter) {
a.Lock()
defer a.Unlock()
a.Data = m
}
func (a *AlertMuteMap) GetByKey(key string) ([]Filter, bool) {
a.RLock()
defer a.RUnlock()
value, exists := a.Data[key]
return value, exists
}
package cache
import (
"sync"
"github.com/didi/nightingale/v5/models"
)
type AlertRulesByMetricCache struct {
sync.RWMutex
Data map[string][]*models.AlertRule // key是metric,便于后续检索
MaxUpdateTs int64 // 从数据库拿到的最大update_at
RuleNum int64 // 从数据库中统计到的行数
LastSync int64 // 保存上次全量同步时间
}
var (
AlertRulesByMetric = &AlertRulesByMetricCache{Data: make(map[string][]*models.AlertRule)}
)
func (a *AlertRulesByMetricCache) GetBy(instance string) []*models.AlertRule {
a.RLock()
defer a.RUnlock()
return a.Data[instance]
}
func (a *AlertRulesByMetricCache) SetAll(alertRulesMap map[string][]*models.AlertRule, lastUpdateTs, ruleNum, lastSync int64) {
a.Lock()
defer a.Unlock()
a.Data = alertRulesMap
a.MaxUpdateTs = lastUpdateTs
a.RuleNum = ruleNum
a.LastSync = lastSync
}
type AlertRulesTotalCache struct {
sync.RWMutex
Data map[int64]*models.AlertRule
}
var AlertRules = &AlertRulesTotalCache{Data: make(map[int64]*models.AlertRule)}
func (a *AlertRulesTotalCache) Get(id int64) (*models.AlertRule, bool) {
a.RLock()
defer a.RUnlock()
alertRule, exists := a.Data[id]
return alertRule, exists
}
func (a *AlertRulesTotalCache) SetAll(alertRulesMap map[int64]*models.AlertRule) {
a.Lock()
defer a.Unlock()
a.Data = alertRulesMap
}
// 获取所有PULL型规则的列表
func (a *AlertRulesTotalCache) Pulls() []*models.AlertRule {
a.RLock()
defer a.RUnlock()
cnt := len(a.Data)
ret := make([]*models.AlertRule, 0, cnt)
for _, rule := range a.Data {
if rule.Type == models.PULL {
ret = append(ret, rule)
}
}
return ret
}
package cache
import (
cmap "github.com/orcaman/concurrent-map"
)
var MetricDescMapper = cmap.New()
package cache
import (
"sync"
)
type ClasspathPrefixMap struct {
sync.RWMutex
Data map[int64][]int64
}
var ClasspathPrefix = &ClasspathPrefixMap{Data: make(map[int64][]int64)}
func (c *ClasspathPrefixMap) Get(id int64) ([]int64, bool) {
c.RLock()
defer c.RUnlock()
ids, exists := c.Data[id]
return ids, exists
}
func (c *ClasspathPrefixMap) SetAll(data map[int64][]int64) {
c.Lock()
defer c.Unlock()
c.Data = data
return
}
package cache
import (
"sync"
"github.com/didi/nightingale/v5/models"
)
type ClasspathResMap struct {
sync.RWMutex
Data map[int64]*ClasspathAndRes
}
type ClasspathAndRes struct {
Res []string
Classpath *models.Classpath
}
// classpath_id -> classpath & res_idents
var ClasspathRes = &ClasspathResMap{Data: make(map[int64]*ClasspathAndRes)}
func (c *ClasspathResMap) Get(id int64) (*ClasspathAndRes, bool) {
c.RLock()
defer c.RUnlock()
resources, exists := c.Data[id]
return resources, exists
}
func (c *ClasspathResMap) SetAll(collectRulesMap map[int64]*ClasspathAndRes) {
c.Lock()
defer c.Unlock()
c.Data = collectRulesMap
}
package cache
import (
"sync"
"github.com/didi/nightingale/v5/models"
)
type CollectRuleOfIdentMap struct {
sync.RWMutex
Data map[string][]*models.CollectRule
}
var CollectRulesOfIdent = &CollectRuleOfIdentMap{Data: make(map[string][]*models.CollectRule)}
func (c *CollectRuleOfIdentMap) GetBy(ident string) []*models.CollectRule {
c.RLock()
defer c.RUnlock()
return c.Data[ident]
}
func (c *CollectRuleOfIdentMap) Set(node string, collectRules []*models.CollectRule) {
c.Lock()
defer c.Unlock()
c.Data[node] = collectRules
}
func (c *CollectRuleOfIdentMap) SetAll(collectRulesMap map[string][]*models.CollectRule) {
c.Lock()
defer c.Unlock()
c.Data = collectRulesMap
}
package cache
import (
"sync"
)
type SafeDoubleMap struct {
sync.RWMutex
M map[string]map[string]struct{}
}
// res_ident -> classpath_path -> struct{}{}
var ResClasspath = &SafeDoubleMap{M: make(map[string]map[string]struct{})}
func (s *SafeDoubleMap) GetKeys() []string {
s.RLock()
defer s.RUnlock()
keys := make([]string, 0, len(s.M))
for key := range s.M {
keys = append(keys, key)
}
return keys
}
func (s *SafeDoubleMap) GetValues(key string) []string {
s.RLock()
defer s.RUnlock()
valueMap, exists := s.M[key]
if !exists {
return []string{}
}
values := make([]string, 0, len(valueMap))
for value := range valueMap {
values = append(values, value)
}
return values
}
func (s *SafeDoubleMap) Exists(key string, value string) bool {
s.RLock()
defer s.RUnlock()
if _, exists := s.M[key]; !exists {
return false
}
if _, exists := s.M[key][value]; !exists {
return false
}
return true
}
func (s *SafeDoubleMap) Set(key string, value string) {
s.Lock()
defer s.Unlock()
if _, exists := s.M[key]; !exists {
s.M[key] = make(map[string]struct{})
}
s.M[key][value] = struct{}{}
}
func (s *SafeDoubleMap) SetAll(data map[string]map[string]struct{}) {
s.Lock()
defer s.Unlock()
s.M = data
}
package cache
import (
"sync"
"github.com/didi/nightingale/v5/models"
)
// resource_ident -> tags_map
// 监控数据上报的时候,要把资源的tags附到指标数据上
type ResTagsMap struct {
sync.RWMutex
Data map[string]ResourceAndTags
}
type ResourceAndTags struct {
Tags map[string]string
Resource models.Resource
}
var ResTags = &ResTagsMap{Data: make(map[string]ResourceAndTags)}
func (r *ResTagsMap) SetAll(m map[string]ResourceAndTags) {
r.Lock()
defer r.Unlock()
r.Data = m
}
func (r *ResTagsMap) Get(key string) (ResourceAndTags, bool) {
r.RLock()
defer r.RUnlock()
value, exists := r.Data[key]
return value, exists
}
package cache
import (
"sync"
"github.com/didi/nightingale/v5/models"
)
type UserMap struct {
sync.RWMutex
Data map[int64]*models.User
}
var UserCache = &UserMap{Data: make(map[int64]*models.User)}
func (s *UserMap) GetBy(id int64) *models.User {
s.RLock()
defer s.RUnlock()
return s.Data[id]
}
func (s *UserMap) GetByIds(ids []int64) []*models.User {
s.RLock()
defer s.RUnlock()
var users []*models.User
for _, id := range ids {
if s.Data[id] == nil {
continue
}
users = append(users, s.Data[id])
}
return users
}
func (s *UserMap) SetAll(users map[int64]*models.User) {
s.Lock()
defer s.Unlock()
s.Data = users
}
package cache
import (
"sync"
)
type UserGroupMemberMap struct {
sync.RWMutex
Data map[int64]map[int64]struct{}
}
// groupid -> userid
var UserGroupMember = &UserGroupMemberMap{Data: make(map[int64]map[int64]struct{})}
func (m *UserGroupMemberMap) Get(id int64) (map[int64]struct{}, bool) {
m.RLock()
defer m.RUnlock()
ids, exists := m.Data[id]
return ids, exists
}
func (m *UserGroupMemberMap) Exists(gid, uid int64) bool {
m.RLock()
defer m.RUnlock()
uidMap, exists := m.Data[gid]
if !exists {
return false
}
_, exists = uidMap[uid]
return exists
}
func (m *UserGroupMemberMap) SetAll(data map[int64]map[int64]struct{}) {
m.Lock()
defer m.Unlock()
m.Data = data
}
......@@ -132,7 +132,7 @@
- 服务端模块合并为一个模块
- agentd和server的调用全部走rpc
重新安装:见 https://n9e.didiyun.com/docs/install/binary/
重新安装:见 https://n9e.didiyun.com/v4/docs/install/
升级方法:
- 使用新的etc替换掉原来的etc
......@@ -145,7 +145,7 @@
升级内容:
- 修复消息通知的问题
重新安装:见 https://n9e.didiyun.com/docs/install/binary/
重新安装:见 https://n9e.didiyun.com/v4/docs/install/
升级方法:
- 将 *.tpl 文件放到 etc/tpl 下
......@@ -157,7 +157,7 @@
- 优化告警接收人补全逻辑
- 增加pospostgresql监控插件
重新安装:见 https://n9e.didiyun.com/docs/install/binary/
重新安装:见 https://n9e.didiyun.com/v4/docs/install/
升级方法:
- 替换n9e-server n9e-prober
......@@ -167,4 +167,11 @@
- 修复nodata恢复告警重复问题
升级方法:
- 替换n9e-server
\ No newline at end of file
- 替换n9e-server
5.0.0-rc1
升级内容:
- 发布v5预览版
部署方式:
- 见文档 https://n9e.didiyun.com/docs/install/
\ No newline at end of file
package config
import (
"bytes"
"fmt"
"net"
"os"
"strings"
"github.com/spf13/viper"
"github.com/toolkits/pkg/file"
"github.com/didi/nightingale/v5/backend"
"github.com/didi/nightingale/v5/models"
"github.com/didi/nightingale/v5/pkg/i18n"
"github.com/didi/nightingale/v5/pkg/iconf"
"github.com/didi/nightingale/v5/pkg/ilog"
)
type ConfigStruct struct {
Logger ilog.Config `yaml:"logger"`
HTTP httpSection `yaml:"http"`
RPC rpcSection `yaml:"rpc"`
LDAP models.LdapSection `yaml:"ldap"`
MySQL models.MysqlSection `yaml:"mysql"`
Heartbeat heartbeatSection `yaml:"heartbeat"`
I18N i18n.Config `yaml:"i18n"`
Judge judgeSection `yaml:"judge"`
Alert alertSection `yaml:"alert"`
Trans transSection `yaml:"trans"`
ContactKeys []contactKey `yaml:"contactKeys"`
NotifyChannels []string `yaml:"notifyChannels"`
}
type alertSection struct {
NotifyScriptPath string `yaml:"notifyScriptPath"`
NotifyConcurrency int `yaml:"notifyConcurrency"`
MutedAlertPersist bool `yaml:"mutedAlertPersist"`
}
type transSection struct {
Enable bool `yaml:"enable"`
Backend backend.BackendSection `yaml:"backend"`
}
type judgeSection struct {
ReadBatch int `yaml:"readBatch"`
ConnTimeout int `yaml:"connTimeout"`
CallTimeout int `yaml:"callTimeout"`
WriterNum int `yaml:"writerNum"`
ConnMax int `yaml:"connMax"`
ConnIdle int `yaml:"connIdle"`
}
type heartbeatSection struct {
IP string `yaml:"ip"`
LocalAddr string `yaml:"-"`
Interval int64 `yaml:"interval"`
}
type httpSection struct {
Mode string `yaml:"mode"`
Access bool `yaml:"access"`
Listen string `yaml:"listen"`
Pprof bool `yaml:"pprof"`
CookieName string `yaml:"cookieName"`
CookieDomain string `yaml:"cookieDomain"`
CookieSecure bool `yaml:"cookieSecure"`
CookieHttpOnly bool `yaml:"cookieHttpOnly"`
CookieMaxAge int `yaml:"cookieMaxAge"`
CookieSecret string `yaml:"cookieSecret"`
CsrfSecret string `yaml:"csrfSecret"`
}
type rpcSection struct {
Listen string `yaml:"listen"`
}
type contactKey struct {
Label string `yaml:"label" json:"label"`
Key string `yaml:"key" json:"key"`
}
var Config *ConfigStruct
func Parse() error {
ymlFile := iconf.GetYmlFile("server")
if ymlFile == "" {
return fmt.Errorf("configuration file of server not found")
}
bs, err := file.ReadBytes(ymlFile)
if err != nil {
return fmt.Errorf("cannot read yml[%s]: %v", ymlFile, err)
}
viper.SetConfigType("yaml")
err = viper.ReadConfig(bytes.NewBuffer(bs))
if err != nil {
return fmt.Errorf("cannot read yml[%s]: %v", ymlFile, err)
}
// default value settings
viper.SetDefault("i18n.lang", "zh")
viper.SetDefault("heartbeat.interval", 1000)
viper.SetDefault("judge.readBatch", 2000)
viper.SetDefault("judge.connTimeout", 2000)
viper.SetDefault("judge.callTimeout", 5000)
viper.SetDefault("judge.writerNum", 256)
viper.SetDefault("judge.connMax", 2560)
viper.SetDefault("judge.connIdle", 256)
viper.SetDefault("alert.notifyScriptPath", "./etc/script/notify.py")
viper.SetDefault("alert.notifyScriptConcurrency", 200)
viper.SetDefault("alert.mutedAlertPersist", true)
viper.SetDefault("trans.backend.prometheus.lookbackDeltaMinute", 2)
viper.SetDefault("trans.backend.prometheus.maxConcurrentQuery", 30)
viper.SetDefault("trans.backend.prometheus.maxSamples", 50000000)
viper.SetDefault("trans.backend.prometheus.maxFetchAllSeriesLimitMinute", 5)
err = viper.Unmarshal(&Config)
if err != nil {
return fmt.Errorf("cannot read yml[%s]: %v", ymlFile, err)
}
fmt.Println("config.file:", ymlFile)
if Config.Heartbeat.IP == "" {
// auto detect
Config.Heartbeat.IP = fmt.Sprint(GetOutboundIP())
if Config.Heartbeat.IP == "" {
fmt.Println("heartbeat ip auto got is blank")
os.Exit(1)
}
port := strings.Split(Config.RPC.Listen, ":")[1]
endpoint := Config.Heartbeat.IP + ":" + port
Config.Heartbeat.LocalAddr = endpoint
}
// 正常情况肯定不是127.0.0.1,但是,如果就是单机部署,并且这个机器没有网络,比如本地调试并且本机没网的时候
// if Config.Heartbeat.IP == "127.0.0.1" {
// fmt.Println("heartbeat ip is 127.0.0.1 and it is useless, so, exit")
// os.Exit(1)
// }
fmt.Println("heartbeat.ip:", Config.Heartbeat.IP)
fmt.Printf("heartbeat.interval: %dms\n", Config.Heartbeat.Interval)
return nil
}
// Get preferred outbound ip of this machine
func GetOutboundIP() net.IP {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
fmt.Println("auto get outbound ip fail:", err)
os.Exit(1)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP
}
package config
// Server周期性去数据库心跳,给自己起的名字
const EndpointName = "server_rpc"
package config
import "github.com/didi/nightingale/v5/pkg/i18n"
var (
dict = map[string]string{
"Login fail, check your username and password": "登录失败,请检查您的用户名和密码",
"Internal server error, try again later please": "系统内部错误,请稍后再试",
"Each user has at most two tokens": "每个用户至多创建两个密钥",
"No such token": "密钥不存在",
"Username is blank": "用户名不能为空",
"Username has invalid characters": "用户名含有非法字符",
"Nickname has invalid characters": "用户昵称含有非法字符",
"Phone invalid": "手机号格式有误",
"Email invalid": "邮箱格式有误",
"Incorrect old password": "旧密码错误",
"Username %s already exists": "用户名(%s)已存在",
"No such user": "用户不存在",
"UserGroup %s already exists": "用户组(%s)已存在",
"Group name has invalid characters": "分组名称含有非法字符",
"Group note has invalid characters": "分组备注含有非法字符",
"No such user group": "用户组不存在",
"Classpath path has invalid characters": "机器分组路径含有非法字符",
"Classpath note has invalid characters": "机器分组路径备注含有非法字符",
"There are still resources under the classpath": "机器分组路径下仍然挂有资源",
"There are still collect rules under the classpath": "机器分组路径下仍然存在采集策略",
"No such classpath": "机器分组路径不存在",
"Classpath %s already exists": "机器分组路径(%s)已存在",
"Preset classpath %s cannot delete": "内置机器分组(%s)不允许删除",
"No such mute config": "此屏蔽配置不存在",
"DashboardGroup name has invalid characters": "大盘分组名称含有非法字符",
"DashboardGroup name is blank": "大盘分组名称为空",
"DashboardGroup %s already exists": "大盘分组(%s)已存在",
"No such dashboard group": "大盘分组不存在",
"Dashboard name has invalid characters": "大盘名称含有非法字符",
"Dashboard %s already exists": "监控大盘(%s)已存在",
"ChartGroup name has invalid characters": "图表分组名称含有非法字符",
"No such dashboard": "监控大盘不存在",
"No such chart group": "图表分组不存在",
"No such chart": "图表不存在",
"There are still dashboards under the group": "分组下面仍然存在监控大盘,请先从组内移出",
"AlertRuleGroup name has invalid characters": "告警规则分组含有非法字符",
"AlertRuleGroup %s already exists": "告警规则分组(%s)已存在",
"There are still alert rules under the group": "分组下面仍然存在告警规则",
"AlertRule name has invalid characters": "告警规则含有非法字符",
"No such alert rule": "告警规则不存在",
"No such alert rule group": "告警规则分组不存在",
"No such alert event": "告警事件不存在",
"No such collect rule": "采集规则不存在",
"Decoded metric description empty": "导入的指标释义列表为空",
"User disabled": "用户已被禁用",
"Tags(%s) invalid": "标签(%s)格式不合法",
"Resource filter(Func:%s)'s param invalid": "资源过滤条件(函数:%s)参数不合法(为空或包含空格都不合法)",
"Tags filter(Func:%s)'s param invalid": "标签过滤条件(函数:%s)参数不合法(为空或包含空格都不合法)",
"Regexp: %s cannot be compiled": "正则表达式(%s)不合法,无法编译",
"AppendTags(%s) invalid": "附件标签(%s)格式不合法",
}
langDict = map[string]map[string]string{
"zh": dict,
}
)
func init() {
i18n.DictRegister(langDict)
}
#!/bin/bash
# release version
version=4.0.3
CWD=$(cd $(dirname $0)/; pwd)
cd $CWD
usage()
{
echo $"Usage: $0 {start|stop|restart|status|build|build_local|pack} <module>"
exit 0
}
start_all()
{
test -x n9e-server && start server
test -x n9e-agentd && start agentd
test -x n9e-prober && start prober
}
start()
{
mod=$1
if [ "x${mod}" = "x" ]; then
usage
return
fi
if [ "x${mod}" = "xall" ]; then
start_all
return
fi
binfile=n9e-${mod}
if [ ! -f $binfile ]; then
echo "file[$binfile] not found"
exit 1
fi
if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -gt 0 ]; then
echo "${mod} already started"
return
fi
mkdir -p logs/$mod
nohup $CWD/$binfile &> logs/${mod}/stdout.log &
for((i=1;i<=15;i++)); do
if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -gt 0 ]; then
echo "${mod} started"
return
fi
sleep 0.2
done
echo "cannot start ${mod}"
exit 1
}
stop_all()
{
test -x n9e-server && stop server
test -x n9e-prober && stop prober
test -x n9e-agentd && stop agentd
}
stop()
{
mod=$1
if [ "x${mod}" = "x" ]; then
usage
return
fi
if [ "x${mod}" = "xall" ]; then
stop_all
return
fi
binfile=n9e-${mod}
if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -eq 0 ]; then
echo "${mod} already stopped"
return
fi
ps aux|grep -v grep|grep -v control|grep "$binfile"|awk '{print $2}'|xargs kill
for((i=1;i<=15;i++)); do
if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -eq 0 ]; then
echo "${mod} stopped"
return
fi
sleep 0.2
done
echo "cannot stop $mod"
exit 1
}
restart()
{
mod=$1
if [ "x${mod}" = "x" ]; then
usage
return
fi
if [ "x${mod}" = "xall" ]; then
stop_all
start_all
return
fi
stop $mod
start $mod
status
}
status()
{
ps aux|grep -v grep|grep "n9e"
}
build_one()
{
mod=$1
echo -n "building ${mod} ... "
go build -ldflags "-X main.version=${version}" -o n9e-${mod} src/modules/${mod}/${mod}.go
echo "done"
}
build_local_one()
{
mod=$1
echo -n "building ${mod} ... "
go build -mod=vendor -ldflags "-X main.version=${version}" -o n9e-${mod} src/modules/${mod}/${mod}.go
echo "done"
}
build()
{
export GO111MODULE=on
mod=$1
if [ "x${mod}" = "x" ]; then
build_one server
build_one agentd
build_one prober
return
fi
build_one $mod
}
build_local()
{
export GO111MODULE=on
mod=$1
if [ "x${mod}" = "x" ]; then
build_local_one server
build_local_one agentd
build_local_one prober
return
fi
build_local_one $mod
}
reload()
{
mod=$1
if [ "x${mod}" = "x" ]; then
echo "arg: <mod> is necessary"
return
fi
build_one $mod
restart $mod
}
pack()
{
clock=$(date +%s)
mkdir -p ~/n9e.bak.$clock
if ls etc/*.local.yml &>/dev/null; then
mv etc/*.local.yml ~/n9e.bak.$clock
fi
tar zcvf n9e-${version}.tar.gz script control sql etc n9e-*
if ls ~/n9e.bak.$clock/*.local.yml &>/dev/null; then
mv ~/n9e.bak.$clock/*.local.yml etc/
fi
rm -rf ~/n9e.bak.$clock
}
exec()
{
params=${@:2}
if [ ${#2} -gt 0 ]; then
for mod in $params
do
mod=${mod#n9e-}
$1 $mod
if [ "x${mod}" = "xall" ]; then
break
fi
done
else
echo "todo $1 at "$(date "+%Y-%m-%d %H:%M:%S")
$1
echo "done $1 at "$(date "+%Y-%m-%d %H:%M:%S")
fi
}
case "$1" in
start)
exec start ${@:2}
;;
stop)
exec stop ${@:2}
;;
restart)
exec restart ${@:2}
;;
status)
status
;;
build)
exec build ${@:2}
;;
build_local)
exec build_local ${@:2}
;;
reload)
exec reload ${@:2}
;;
pack)
exec pack ${@:2}
;;
*)
usage
esac
version: '3.6'
services:
web:
build:
dockerfile: Dockerfile
context: nginx
container_name: nginx-n9e
hostname: nginx-n9e
ports:
- 80:80
restart: unless-stopped
links:
- n9e:n9e
mysql:
image: mysql:5
container_name: mysql
hostname: mysql
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: 1234
# volumes:
# - /usr/local/docker/mysql/data:/var/lib/mysql
redis:
image: redis
container_name: redis
hostname: redis
ports:
- 6379:6379
n9e:
build:
dockerfile: Dockerfile
context: n9e
container_name: n9e
hostname: n9e
ports:
- 8000:8000
- 8002:8002
- 8004:8004
- 8005:8005
- 8006:8006
- 8008:8008
- 8009:8009
- 8010:8010
- 8011:8011
- 8012:8012
- 8013:8013
- 8014:8014
- 8015:8015
- 2080:2080
links:
- mysql:mysql
- redis:redis
depends_on:
- mysql
- redis
FROM centos:7
WORKDIR /home/n9e
RUN set -ex \
&& yum install mysql net-tools wget -y \
&& yum clean all \
&& rm -rf /var/cache/yum
ADD http://116.85.64.82/n9e-3.8.0.tar.gz .
RUN tar xvf n9e-3.8.0.tar.gz \
&& sed -i "s/127.0.0.1:6379/redis:6379/g" `grep -rl "127.0.0.1:6379" ./etc/` \
&& sed -i 's/127.0.0.1/mysql/g' etc/mysql.yml
COPY entrpoint.sh .
ENTRYPOINT ./entrpoint.sh
#!/bin/bash
mysqlRootPassword=1234
until mysql -hmysql -u root -p$mysqlRootPassword -e ";" ; do
echo "Can't connect mysql, retry"
sleep 5
done
mysql -hmysql -uroot -p$mysqlRootPassword < sql/n9e_ams.sql
mysql -hmysql -uroot -p$mysqlRootPassword < sql/n9e_hbs.sql
mysql -hmysql -uroot -p$mysqlRootPassword < sql/n9e_job.sql
mysql -hmysql -uroot -p$mysqlRootPassword < sql/n9e_mon.sql
mysql -hmysql -uroot -p$mysqlRootPassword < sql/n9e_rdb.sql
./control start all
sleep infinity
FROM nginx
WORKDIR /home/n9e
COPY nginx.conf /etc/nginx
ADD http://116.85.64.82/pub.tar.gz .
RUN tar xvf pub.tar.gz
user root;
worker_processes auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 204800;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
use epoll;
worker_connections 204800;
}
http {
log_format main '$server_addr $host $remote_addr [$time_local] $scheme "$request" '
'$status $upstream_status $body_bytes_sent $request_time $upstream_addr $upstream_response_time "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
proxy_connect_timeout 500ms;
proxy_send_timeout 1000ms;
proxy_read_timeout 3000ms;
proxy_buffers 64 8k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 64k;
proxy_redirect off;
proxy_next_upstream error invalid_header timeout http_502 http_504;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 100m;
client_body_buffer_size 512k;
client_body_timeout 180;
client_header_timeout 10;
send_timeout 240;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types application/javascript application/x-javascript text/css text/javascript image/jpeg image/gif image/png;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";
upstream n9e.rdb {
server n9e:8000;
keepalive 60;
}
upstream n9e.ams {
server n9e:8002;
keepalive 60;
}
upstream n9e.job {
server n9e:8004;
keepalive 60;
}
upstream n9e.monapi {
server n9e:8006;
keepalive 60;
}
upstream n9e.transfer {
server n9e:8008;
keepalive 60;
}
upstream n9e.index {
server n9e:8012;
keepalive 60;
}
server {
listen 80 default_server;
server_name n9e.example.com;
root /home/n9e/pub;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location =/ {
rewrite / /mon;
}
location / {
try_files $uri /layout/index.html;
}
location ~ .*(.htm|.html|manifest.json)$ {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
location /api/rdb {
proxy_pass http://n9e.rdb;
}
location /api/ams {
proxy_pass http://n9e.ams;
}
location /api/job {
proxy_pass http://n9e.job;
}
location /api/mon {
proxy_pass http://n9e.monapi;
}
location /api/index {
proxy_pass http://n9e.index;
}
location /api/transfer {
proxy_pass http://n9e.transfer;
}
}
}
## 登陆相关
#### 来源地址限制
IP地址的获取顺序
- http header "X-Forwarded-For"
- http header "X-Real-Ip"
- http request RemoteAddr
nginx 代理配置客户端地址
```
# https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
```
<!DOCTYPE html>
<html>
<head>
<title>Nightingale</title>
</head>
<body>
<h1>Hello, Nightingale</h1>
</body>
</html>
\ No newline at end of file
# 安装 M3db,ETCD
下载安装包 [m3db-install.tar.gz](https://s3-gz01.didistatic.com/n9e-pub/tarball/m3db-install.tar.gz) 
## 配置
#### etcd证书
```shell
# 修改 ./etcd/certs/config/hosts-etcd
# 填写etcd的机器列表
# 生成证书
cd etcd/certs
./reinit-root-ca.sh
./update-etcd-client-certs.sh
./update-etcd-server-certs.sh
```
#### m3db 配置
```shell
# 修改 ./m3db/etc/m3dbnode.yml
db:
config:
service:
etcdClusters:
- zone: embedded
endpoints:
- 10.255.0.146:2379 # 这里需要修改 etcd 节点信息
- 10.255.0.129:2379
```
## 安装部署
```shell
. ./functions.sh
# 设置安装的机器节点,
hosts="{node1} {node2} {node3}"
# 设置好ssh Public key认证后,设置 publick key 用户名
user="dc2-user"
# 同步文件
sync
# 安装
install
# 检查
status
```
---
server:
http: 0.0.0.0:8000
rpc: 0.0.0.0:8001
addresses:
- 127.0.0.1
prober:
http: 0.0.0.0:8023
agentd:
http: 0.0.0.0:2080
\ No newline at end of file
logger:
dir: logs/agentd
level: INFO
keepHours: 24
enable:
mon: true
job: true
report: true
metrics: true
defaultTags:
enable: false
tags:
key1: value1
key2: value2
udp:
enable: false
listen: :788
metrics:
maxProcs: 1
reportIntervalMs: 10
reportTimeoutMs: 2000
reportPacketSize: 100
sendToInfoFile: false
job:
metadir: ./meta
interval: 2
report:
# 调用ams的接口上报数据,需要ams的token
token: ams-builtin-token
# 上报周期,单位是秒
interval: 10
# physical:物理机,virtual:虚拟机,container:容器,switch:交换机
cate: physical
# 使用哪个字段作为唯一KEY,即作为where条件更新对应记录,一般使用sn或ip
uniqkey: ip
# 如果是虚拟机,应该是获取uuid
# curl -s http://169.254.169.254/a/meta-data/instance-id
sn: dmidecode -s system-serial-number | tail -n 1
fields:
cpu: cat /proc/cpuinfo | grep processor | wc -l
mem: cat /proc/meminfo | grep MemTotal | awk '{printf "%.1fGi", $2/1024/1024}'
disk: df -m | grep '/dev/' | grep -v '/var/lib' | grep -v tmpfs | awk '{sum += $2};END{printf "%.1fGi", sum/1024}'
sys:
# timeout in ms
# interval in second
timeout: 5000
interval: 30
ifacePrefix:
- eth
- em
- ens
# 磁盘采集
mountCollect:
# 正则匹配需要采集的挂载卷类型
typePrefix: "^(btrfs|ext2|ext3|ext4|jfs|reiser|xfs|ffs|ufs|jfs|jfs2|vxfs|hfs|ntfs|fat32|zfs|nfs)$"
# 忽略采集以下面开头的挂载目录
ignorePrefix:
- /var/lib
- /run
- /boot
# 仍然采集ignorePrefix中的例外的挂载目录(填写全路径)
exclude: []
ignoreMetrics:
- cpu.core.idle
- cpu.core.util
- cpu.core.sys
- cpu.core.user
- cpu.core.nice
- cpu.core.guest
- cpu.core.irq
- cpu.core.softirq
- cpu.core.iowait
- cpu.core.steal
#ntpServers:
# - ntp1.aliyun.com
[
{
"name": "timewait状态tcp连接超过2万",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 3,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "net.sockets.tcp.timewait",
"params": [],
"threshold": 20000
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": [],
"notify_user": [],
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "内存利用率大于75%",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 2,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "mem.bytes.used.percent",
"params": [],
"threshold": 75
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "机器loadavg大于16",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 2,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "cpu.loadavg.1",
"params": [],
"threshold": 16
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "某磁盘无法正常读写",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 1,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "disk.rw.error",
"params": [],
"threshold": 0
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "监控agent失联",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 1,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": "=",
"func": "nodata",
"metric": "proc.agent.alive",
"params": [],
"threshold": 0
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "磁盘利用率达到85%",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 3,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "disk.bytes.used.percent",
"params": [],
"threshold": 85
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "磁盘利用率达到88%",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 2,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "disk.bytes.used.percent",
"params": [],
"threshold": 88
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "磁盘利用率达到92%",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 1,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "disk.bytes.used.percent",
"params": [],
"threshold": 92
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "端口挂了",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 2,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": "!=",
"func": "all",
"metric": "proc.port.listen",
"params": [],
"threshold": 1
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "网卡入方向丢包",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 2,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "net.in.dropped",
"params": [],
"threshold": 3
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "网卡入方向错包",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 2,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "net.in.errs",
"params": [],
"threshold": 3
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "网卡出方向丢包",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 2,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "net.out.dropped",
"params": [],
"threshold": 3
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "网卡出方向错包",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 2,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "net.out.errs",
"params": [],
"threshold": 3
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "进程总数超过3000",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 1,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": ">",
"func": "all",
"metric": "sys.ps.process.total",
"params": [],
"threshold": 3000
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
},
{
"name": "进程挂了",
"category": 1,
"alert_dur": 60,
"recovery_dur": 0,
"recovery_notify": 1,
"enable_stime": "00:00",
"enable_etime": "23:59",
"priority": 2,
"callback": "",
"need_upgrade": 0,
"runbook": "",
"excl_nid": null,
"nids": null,
"exprs": [
{
"eopt": "<",
"func": "all",
"metric": "proc.num",
"params": [],
"threshold": 1
}
],
"tags": [],
"enable_days_of_week": [
0,
1,
2,
3,
4,
5,
6
],
"converge": [
36000,
1
],
"notify_group": null,
"notify_user": null,
"leaf_nids": null,
"endpoints": null,
"alert_upgrade": {
"users": [],
"groups": [],
"duration": 60,
"level": 1
},
"judge_instance": "",
"work_groups": null
}
]
\ No newline at end of file
- system: 资产管理系统
groups:
- title: 主机设备
ops:
- en: ams_host_mgr_menu
cn: 主机设备管理菜单展示
- en: ams_host_delete
cn: 主机设备删除
- en: ams_host_modify
cn: 主机设备信息修改
- en: ams_host_field_mgr_menu
cn: 主机设备扩展字段菜单展示
- en: ams_host_field_mgr
cn: 主机设备扩展字段管理
# - title: 网络设备
# ops:
# - en: ams_netware_mgr_menu
# cn: 网络设备管理菜单
# - en: ams_netware_modify
# cn: 网络设备信息修改
\ No newline at end of file
# for heartbeat, connected by other modules
ip:
specify: ""
shell: ifconfig `route|grep '^default'|awk '{print $NF}'`|grep inet|awk '{print $2}'|head -n 1|awk -F':' '{print $NF}'
# as identity. equals to endpoint. used by agentd, prober, server
ident:
specify: ""
shell: ifconfig `route|grep '^default'|awk '{print $NF}'`|grep inet|awk '{print $2}'|head -n 1|awk -F':' '{print $NF}'
- system: 用户资源库
groups:
- title: 人员授权
ops:
- en: rdb_perm_grant
cn: 权限分配
- title: 节点类操作
ops:
- en: rdb_node_create
cn: 创建节点
- en: rdb_node_modify
cn: 修改节点
- en: rdb_node_delete
cn: 删除节点
- title: 资源类操作
ops:
- en: rdb_resource_view
cn: 资源信息查看
- en: rdb_resource_bind
cn: 资源挂载节点
- en: rdb_resource_unbind
cn: 资源解挂节点
- en: rdb_resource_modify
cn: 资源信息修改
- system: 任务执行中心
groups:
- title: 任务模板相关
ops:
- en: job_tpl_view
cn: 查看任务模板
- en: job_tpl_create
cn: 创建任务模板
- en: job_tpl_modify
cn: 修改任务模板
- en: job_tpl_delete
cn: 删除任务模板
- title: 任务执行相关
ops:
- en: task_run_use_root_account
cn: 使用root账号运行脚本
- en: task_run_use_gene_account
cn: 使用普通账号运行脚本
- system: 监控告警系统
groups:
- title: 告警策略
ops:
- en: mon_stra_create
cn: 创建告警策略
- en: mon_stra_modify
cn: 修改告警策略
- en: mon_stra_delete
cn: 删除告警策略
- title: 告警屏蔽
ops:
- en: mon_maskconf_create
cn: 创建告警屏蔽
- en: mon_maskconf_modify
cn: 修改告警屏蔽
- en: mon_maskconf_delete
cn: 删除告警屏蔽
- title: 采集策略
ops:
- en: mon_collect_create
cn: 创建采集策略
- en: mon_collect_modify
cn: 修改采集策略
- en: mon_collect_delete
cn: 删除采集策略
- title: 大盘操作
ops:
- en: mon_screen_create
cn: 创建监控大盘
- en: mon_screen_modify
cn: 修改监控大盘
- en: mon_screen_delete
cn: 删除监控大盘
- en: mon_screen_view
cn: 查看监控大盘
# - title: 指标计算
# ops:
# - en: mon_aggr_write
# cn: 指标计算配置权限
- title: 告警历史
ops:
- en: mon_event_write
cn: 告警历史认领、忽略
---
rdb:
addr: "root:1234@tcp(127.0.0.1:3306)/n9e_rdb?charset=utf8&parseTime=True&loc=Asia%2FShanghai"
max: 128
idle: 16
debug: false
ams:
addr: "root:1234@tcp(127.0.0.1:3306)/n9e_ams?charset=utf8&parseTime=True&loc=Asia%2FShanghai"
max: 128
idle: 16
debug: false
job:
addr: "root:1234@tcp(127.0.0.1:3306)/n9e_job?charset=utf8&parseTime=True&loc=Asia%2FShanghai"
max: 128
idle: 16
debug: false
mon:
addr: "root:1234@tcp(127.0.0.1:3306)/n9e_mon?charset=utf8&parseTime=True&loc=Asia%2FShanghai"
max: 128
idle: 16
debug: false
hbs:
addr: "root:1234@tcp(127.0.0.1:3306)/n9e_hbs?charset=utf8&parseTime=True&loc=Asia%2FShanghai"
max: 128
idle: 16
debug: false
\ No newline at end of file
user root;
worker_processes auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 204800;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
use epoll;
worker_connections 204800;
}
http {
log_format main '$server_addr $host $remote_addr [$time_local] $scheme "$request" '
'$status $upstream_status $body_bytes_sent $request_time $upstream_addr $upstream_response_time "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
proxy_connect_timeout 500ms;
proxy_send_timeout 1000ms;
proxy_read_timeout 3000ms;
proxy_buffers 64 8k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 64k;
proxy_redirect off;
proxy_next_upstream error invalid_header timeout http_502 http_504;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 100m;
client_body_buffer_size 512k;
client_body_timeout 180;
client_header_timeout 10;
send_timeout 240;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types application/javascript application/x-javascript text/css text/javascript image/jpeg image/gif image/png;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";
upstream n9e.server {
server 127.0.0.1:8000;
keepalive 60;
}
upstream n9e.index {
server 127.0.0.1:8012;
keepalive 60;
}
server {
listen 80 default_server;
server_name localhost;
root /home/n9e/pub;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location =/ {
rewrite / /mon;
}
location / {
try_files $uri /layout/index.html;
}
location ~ .*(.htm|.html|.json)$ {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
location /api/rdb {
proxy_pass http://n9e.server;
}
location /api/ams {
proxy_pass http://n9e.server;
}
location /api/job {
proxy_pass http://n9e.server;
}
location /api/mon {
proxy_pass http://n9e.server;
}
location /api/index {
proxy_pass http://n9e.server;
}
location /api/transfer {
proxy_pass http://n9e.server;
}
}
}
mode: whitelist # whitelist(default),all
metrics:
- name: dns_query_query_time_ms
- name: dns_query_result_code
- name: dns_query_rcode_value
mode: all # whitelist(default),all
mode: whitelist # whitelist(default),all
metrics:
- name: github_repository_forks
- name: github_repository_networks
- name: github_repository_open_issues
- name: github_repository_size
- name: github_repository_stars
- name: github_repository_subscribers
- name: github_repository_watchers
mode: whitelist # whitelist(default),all
metrics:
- name: http_response_http_response_code
- name: http_response_content_length
- name: http_response_response_string_match
- name: http_response_response_status_code_match
- name: http_response_result_code
- name: http_response_response_time
mode: whitelist # whitelist(default),all
metrics:
- name: mongodb_assert_msg
type: COUNTER
- name: mongodb_assert_regular
type: COUNTER
- name: mongodb_assert_rollovers
type: COUNTER
- name: mongodb_assert_user
type: COUNTER
- name: mongodb_assert_warning
type: COUNTER
- name: mongodb_commands
type: COUNTER
- name: mongodb_count_command_failed
type: COUNTER
- name: mongodb_count_command_total
type: COUNTER
- name: mongodb_connections_available
- name: mongodb_connections_current
- name: mongodb_connections_total_created
type: COUNTER
trash:
- name: mongodb_active_reads
type: COUNTER
- name: mongodb_active_writes
type: COUNTER
- name: mongodb_aggregate_command_failed
type: COUNTER
- name: mongodb_aggregate_command_total
type: COUNTER
- name: mongodb_available_reads
- name: mongodb_available_writes
- name: mongodb_col_stats_avg_obj_size
- name: mongodb_col_stats_count
- name: mongodb_col_stats_ok
- name: mongodb_col_stats_size
- name: mongodb_col_stats_storage_size
- name: mongodb_col_stats_total_index_size
- name: mongodb_commands_per_sec
- name: mongodb_cursor_no_timeout
- name: mongodb_cursor_no_timeout_count
- name: mongodb_cursor_pinned
- name: mongodb_cursor_pinned_count
- name: mongodb_cursor_timed_out
- name: mongodb_cursor_timed_out_count
- name: mongodb_cursor_total
- name: mongodb_cursor_total_count
- name: mongodb_db_stats_avg_obj_size
- name: mongodb_db_stats_collections
- name: mongodb_db_stats_data_size
- name: mongodb_db_stats_index_size
- name: mongodb_db_stats_indexes
- name: mongodb_db_stats_num_extents
- name: mongodb_db_stats_objects
- name: mongodb_db_stats_ok
- name: mongodb_db_stats_storage_size
- name: mongodb_delete_command_failed
type: COUNTER
- name: mongodb_delete_command_total
type: COUNTER
- name: mongodb_deletes
- name: mongodb_deletes_per_sec
- name: mongodb_distinct_command_failed
type: COUNTER
- name: mongodb_distinct_command_total
type: COUNTER
- name: mongodb_document_deleted
- name: mongodb_document_inserted
- name: mongodb_document_returned
- name: mongodb_document_updated
- name: mongodb_find_and_modify_command_failed
type: COUNTER
- name: mongodb_find_and_modify_command_total
type: COUNTER
- name: mongodb_find_command_failed
type: COUNTER
- name: mongodb_find_command_total
type: COUNTER
- name: mongodb_flushes
type: COUNTER
- name: mongodb_flushes_per_sec
- name: mongodb_flushes_total_time_ns
type: COUNTER
- name: mongodb_get_more_command_failed
type: COUNTER
- name: mongodb_get_more_command_total
type: COUNTER
- name: mongodb_getmores
- name: mongodb_getmores_per_sec
- name: mongodb_insert_command_failed
type: COUNTER
- name: mongodb_insert_command_total
type: COUNTER
- name: mongodb_inserts
- name: mongodb_inserts_per_sec
- name: mongodb_jumbo_chunks
- name: mongodb_latency_commands
type: COUNTER
- name: mongodb_latency_commands_count
type: COUNTER
- name: mongodb_latency_reads
- name: mongodb_latency_reads_count
- name: mongodb_latency_writes
- name: mongodb_latency_writes_count
- name: mongodb_net_in_bytes
- name: mongodb_net_in_bytes_count
- name: mongodb_net_out_bytes
- name: mongodb_net_out_bytes_count
- name: mongodb_open_connections
- name: mongodb_operation_scan_and_order
- name: mongodb_operation_write_conflicts
- name: mongodb_page_faults
type: COUNTER
- name: mongodb_percent_cache_dirty
- name: mongodb_percent_cache_used
- name: mongodb_resident_megabytes
- name: mongodb_storage_freelist_search_bucket_exhausted
- name: mongodb_storage_freelist_search_requests
- name: mongodb_storage_freelist_search_scanned
- name: mongodb_tcmalloc_central_cache_free_bytes
- name: mongodb_tcmalloc_current_allocated_bytes
- name: mongodb_tcmalloc_current_total_thread_cache_bytes
- name: mongodb_tcmalloc_heap_size
- name: mongodb_tcmalloc_max_total_thread_cache_bytes
- name: mongodb_tcmalloc_pageheap_commit_count
- name: mongodb_tcmalloc_pageheap_committed_bytes
- name: mongodb_tcmalloc_pageheap_decommit_count
- name: mongodb_tcmalloc_pageheap_free_bytes
- name: mongodb_tcmalloc_pageheap_reserve_count
- name: mongodb_tcmalloc_pageheap_scavenge_count
- name: mongodb_tcmalloc_pageheap_total_commit_bytes
- name: mongodb_tcmalloc_pageheap_total_decommit_bytes
- name: mongodb_tcmalloc_pageheap_total_reserve_bytes
- name: mongodb_tcmalloc_pageheap_unmapped_bytes
- name: mongodb_tcmalloc_spinlock_total_delay_ns
- name: mongodb_tcmalloc_thread_cache_free_bytes
- name: mongodb_tcmalloc_total_free_bytes
- name: mongodb_tcmalloc_transfer_cache_free_bytes
- name: mongodb_total_available
- name: mongodb_total_created
type: COUNTER
- name: mongodb_total_docs_scanned
- name: mongodb_total_in_use
- name: mongodb_total_keys_scanned
- name: mongodb_total_refreshing
- name: mongodb_total_tickets_reads
- name: mongodb_total_tickets_writes
- name: mongodb_ttl_deletes
- name: mongodb_ttl_deletes_per_sec
- name: mongodb_ttl_passes
- name: mongodb_ttl_passes_per_sec
- name: mongodb_update_command_failed
type: COUNTER
- name: mongodb_update_command_total
type: COUNTER
- name: mongodb_updates
- name: mongodb_updates_per_sec
- name: mongodb_uptime_ns
- name: mongodb_vsize_megabytes
- name: mongodb_wtcache_app_threads_page_read_count
type: COUNTER
- name: mongodb_wtcache_app_threads_page_read_time
type: COUNTER
- name: mongodb_wtcache_app_threads_page_write_count
type: COUNTER
- name: mongodb_wtcache_bytes_read_into
- name: mongodb_wtcache_bytes_written_from
- name: mongodb_wtcache_current_bytes
- name: mongodb_wtcache_internal_pages_evicted
- name: mongodb_wtcache_max_bytes_configured
- name: mongodb_wtcache_modified_pages_evicted
- name: mongodb_wtcache_pages_evicted_by_app_thread
- name: mongodb_wtcache_pages_queued_for_eviction
- name: mongodb_wtcache_pages_read_into
- name: mongodb_wtcache_pages_requested_from
- name: mongodb_wtcache_pages_written_from
- name: mongodb_wtcache_server_evicting_pages
- name: mongodb_wtcache_tracked_dirty_bytes
- name: mongodb_wtcache_unmodified_pages_evicted
- name: mongodb_wtcache_worker_thread_evictingpages
此差异已折叠。
mode: whitelist # whitelist(default),all
metrics:
- name: net_response_result_code
- name: net_response_response_time
mode: whitelist # whitelist(default),all
metrics:
- name: nginx_accepts
- name: nginx_active
- name: nginx_handled
- name: nginx_reading
- name: nginx_requests
- name: nginx_waiting
- name: nginx_writing
此差异已折叠。
此差异已折叠。
mode: all # whitelist(default),all
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册