open-falcon-alarm 代码分析

总结:alarm消费由judge产生的redis报警事件,根据优先级高低是否做合并,发往不同的报警通道

高优先级报警比如p0: judge产生报警事件-->写入redis event:p0队列 -->alarm消费-->获取发送对象并处理调用回调函数(如果有)-->根据策略不同生成不同通道的报警(im,sms,mail,phone)等-->写入redis各个通道的发送队列 /im /sms /mail /phone -->发送报警的worker取出报警发送

低优先级报警比如p4: judge产生报警事件-->写入redis event:p4队列 -->alarm消费-->获取发送对象并处理调用回调函数(如果有)-->根据策略不同生成不同通道的合并(im,sms,mail,phone)事件写入合并队列(来自于配置文件中的 /queue/user/im)等-->由不通通道的合并函数处理,合并报警生成dashboard链接调用dashboard的api写入falcon_portal.alert_link表中供用户日后查看原始信息-->写入redis各个通道的发送队列 /im /sms /mail /phone -->发送报警的worker取出报警发送

下面具体看下代码

1.main函数中的核心就是这几个goroutine了

        //消费报警事件
    go cron.ReadHighEvent()
    go cron.ReadLowEvent()
    //合并低优先报警
    go cron.CombineSms()
    go cron.CombineMail()
    go cron.CombineIM()
    //发送真实报警
    go cron.ConsumeIM()
    go cron.ConsumeSms()
    go cron.ConsumeMail()
    go cron.ConsumePhone()
    go cron.CleanExpiredEvent()

2.ReadHighEvent 和 ReadLowEvent的区别就是consume时分优先级

func ReadHighEvent() {
    queues := g.Config().Redis.HighQueues
    if len(queues) == 0 {
        return
    }

    for {
        /*brpop 多个队列的1条返回event
        1.传入的是包含多个高优先级的队列的列表比如[p0,p1,p2]
        那么总是先pop完event:p0的队列,然后才是p1 ,p2(这里我进行过实测)
        2.单纯的popevent速度是很快的,但是每次循环里都有下面的consume,如果
        consume速度慢的话会直接影响整体的pop速度,我观察过再没加goroutine之前
        pop速度大概5条/s ,如果报警过多会有堆积现象,之前看到会有4个小时左右的延迟
        */
        event, err := popEvent(queues)
        if err != nil {
            time.Sleep(time.Second)
            continue
        }
        //这里的consume其实和popevent已经没关系了,所以异步执行,但是可能会产生过多的goroutine
        go consume(event, true)
    }
}

3.消费报警事件函数 consume

func consume(event *cmodel.Event, isHigh bool) {
    actionId := event.ActionId()
    if actionId <= 0 {
        return
    }
        /*这里通过 event中的actionid 拿到 action
        就是拿到这个 报警组的名字 是否有回调等信息
        */
    action := api.GetAction(actionId)
    if action == nil {
        return
    }
        //有回调的话处理下http get调用相应的回调函数,会把报警的信息作为参数带上
    if action.Callback == 1 {
        HandleCallback(event, action)
    }

    if isHigh {
        consumeHighEvents(event, action)
    } else {
        consumeLowEvents(event, action)
    }
}

4.下面分别看下高低优先级的consume函数

// 高优先级的不做报警合并
func consumeHighEvents(event *cmodel.Event, action *api.Action) {
    //如果报警没有接收组,那么直接返回了
    if action.Uic == "" {
        return
    }

    phones, mails, ims := api.ParseTeams(action.Uic)
        log.Infof("api.ParseTeams--phones, mails, ims,action.uic",phones, mails, ims,action.Uic)
    //生成报警内容,这里可以为不同通道的报警做定制
    smsContent := GenerateSmsContent(event)
    mailContent := GenerateMailContent(event)
    //imContent := GenerateIMContent(event)
        phoneContent := GeneratePhoneContent(event)


    /* 这里根据报警的级别可以做通道的定制
    如<=P2 才发送短信 =p9 电话报警等等
    下面的redi.wirtesms等方法就是将报警内容lpush到不通通道的发送队列中
    */
    if event.Priority() < 3 {
        redi.WriteSms(phones, smsContent)
    }
        //p9 电话报警
        if event.Priority() ==9 {
        redi.WriteSms(phones, smsContent)
        redi.WritePhone(phones, phoneContent)
    }
    redi.WriteIM(mails, mailContent)
    redi.WriteMail(mails, smsContent, mailContent)

}
// 低优先级的做报警合并
func consumeLowEvents(event *cmodel.Event, action *api.Action) {
    if action.Uic == "" {
        return
    }

    // <=P2 才发送短信
        //parseuser函数将event转换为合并消息 写入中间队列
    if event.Priority() < 3 {
        ParseUserSms(event, action)
    }
        
    ParseUserIm(event, action)
    ParseUserMail(event, action)
}

下面以ParseUserMail为例

func ParseUserMail(event *cmodel.Event, action *api.Action) {
    //api根据报警组获取组里人
    userMap := api.GetUsers(action.Uic)

    metric := event.Metric()
    subject := GenerateSmsContent(event)
    content := GenerateMailContent(event)
    status := event.Status
    priority := event.Priority()

    queue := g.Config().Redis.UserMailQueue

    rc := g.RedisConnPool.Get()
    defer rc.Close()
       //遍历usermap 生成报警中间态消息并LPUSH写入中间队列
    for _, user := range userMap {
        dto := MailDto{
            Priority: priority,
            Metric:   metric,
            Subject:  subject,
            Content:  content,
            Email:    user.Email,
            Status:   status,
        }
        bs, err := json.Marshal(dto)
        if err != nil {
            log.Error("json marshal MailDto fail:", err)
            continue
        }

        _, err = rc.Do("LPUSH", queue, string(bs))
        if err != nil {
            log.Error("LPUSH redis", queue, "fail:", err, "dto:", string(bs))
        }
    }
}

此时低优先级的报警存在于配置文件中的中间队列名称的redis队列中 /queue/user/mail

5.报警合并函数

func CombineSms() {
    for {
        // 每分钟读取处理一次
        time.Sleep(time.Minute)
        combineSms()
    }
}


func combineIM() {
    //从中间队列中pop出要合并的报警
    dtos := popAllImDto()
    count := len(dtos)
    if count == 0 {
        return
    }

    dtoMap := make(map[string][]*ImDto)
    for i := 0; i < count; i++ {
        //根据报警的metirc priority status 和接收人作为key合并报警为列表
        key := fmt.Sprintf("%d%s%s%s", dtos[i].Priority, dtos[i].Status, dtos[i].IM, dtos[i].Metric)
        if _, ok := dtoMap[key]; ok {
            dtoMap[key] = append(dtoMap[key], dtos[i])
        } else {
            dtoMap[key] = []*ImDto{dtos[i]}
        }
    }

    for _, arr := range dtoMap {
        size := len(arr)
        //如果合并后的报警只有一条直接写入redis发送队列
        if size == 1 {
            redi.WriteIM([]string{arr[0].IM}, arr[0].Content)
            continue
        }

        // 把多个im内容写入数据库,只给用户提供一个链接
        contentArr := make([]string, size)
        for i := 0; i < size; i++ {
            contentArr[i] = arr[i].Content
        }
        content := strings.Join(contentArr, ",,")

        first := arr[0].Content
        t := strings.Split(first, "][")
        eg := ""
        if len(t) >= 3 {
            eg = t[2]
        }
                //调用dashboard的api将合并后的信息写入falcon_portal.alert_link表
        path, err := api.LinkToSMS(content)
        chat := ""
        if err != nil || path == "" {
            chat = fmt.Sprintf("[P%d][%s] %d %s.  e.g. %s. detail in email", arr[0].Priority, arr[0].Status, size, arr[0].Metric, eg)
            log.Error("create short link fail", err)
        } else {
            //生成一个汇总信息 展示:metric status link的url
            chat = fmt.Sprintf("[P%d][%s] %d %s e.g. %s %s/portal/links/%s ",
                arr[0].Priority, arr[0].Status, size, arr[0].Metric, eg, g.Config().Api.Dashboard, path)
            log.Debugf("combined im is:%s", chat)
        }
        if  arr[0].IM==""{
            email:= fmt.Sprintf("%s@bytedance.com",arr[0].Name)
            redi.WriteIM([]string{email}, chat)
        }else{
            redi.WriteIM([]string{arr[0].IM}, chat)
        }

    }
}

6.最后看下报警发送函数

func ConsumeIM() {
    for {
        //rpop出所有的报警信息到一个slice中
        L := redi.PopAllIM()
        if len(L) == 0 {
            time.Sleep(time.Millisecond * 200)
            continue
        }
        SendIMList(L)
    }
}

func SendIMList(L []*model.IM) {
    for _, im := range L {
        /*
        1.IMWorkerChan是带缓冲的chan,chan的长度意思就是同时可以多少个send作业
        2.向im发送workerchan中写入1说明可以发送一条
        3.如果队列没满,是不会阻塞在这里的,否则会阻塞
        */
        IMWorkerChan <- 1
        go SendIM(im)
    }
}

func SendIM(im *model.IM) {
    /*
    1.这里使用defer的逻辑是先发送后读取chan
    2.因为如果先读取意味着又有一个work可以开始和逻辑相反
    3.下面就是自己定制的发送方式了
    */
    defer func() {
        <-IMWorkerChan
    }()
    if im.Tos==""{
        log.Errorf("content_tos_empty_error %s ",im.Content)
        return
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345