// Copyright 2019 Yunion // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package notifiers import ( "fmt" "golang.org/x/sync/errgroup" "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/onecloud/pkg/apis/monitor" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/monitor/alerting" "yunion.io/x/onecloud/pkg/monitor/notifydrivers/feishu" ) func init() { alerting.RegisterNotifier(&alerting.NotifierPlugin{ Type: monitor.AlertNotificationTypeFeishu, Factory: newFeishuNotifier, ValidateCreateData: func(cred mcclient.IIdentityProvider, input monitor.NotificationCreateInput) (monitor.NotificationCreateInput, error) { settings := new(monitor.NotificationSettingFeishu) if err := input.Settings.Unmarshal(settings); err != nil { return input, errors.Wrap(err, "unmarshal setting") } if settings.AppId == "" { return input, httperrors.NewInputParameterError("app_id is empty") } if settings.AppSecret == "" { return input, httperrors.NewInputParameterError("app_secret is empty") } _, err := feishu.NewTenant(settings.AppId, settings.AppSecret) if err != nil { return input, httperrors.NewGeneralError(errors.Wrap(err, "test connection")) } input.Settings = jsonutils.Marshal(settings) return input, nil }, }) } type FeishuNotifier struct { NotifierBase // Settings *monitor.NotificationSettingFeishu Client *feishu.Tenant ChatIds []string } func newFeishuNotifier(config alerting.NotificationConfig) (alerting.Notifier, error) { settings := new(monitor.NotificationSettingFeishu) if err := config.Settings.Unmarshal(settings); err != nil { return nil, errors.Wrap(err, "unmarshal setting") } cli, err := feishu.NewTenant(settings.AppId, settings.AppSecret) if err != nil { return nil, err } ret, err := cli.ChatList(0, "") if err != nil { return nil, err } chatIds := make([]string, 0) for _, obj := range ret.Data.Groups { chatIds = append(chatIds, obj.ChatId) } return &FeishuNotifier{ NotifierBase: NewNotifierBase(config), Client: cli, ChatIds: chatIds, }, nil } func (fs *FeishuNotifier) Notify(ctx *alerting.EvalContext, _ jsonutils.JSONObject) error { log.Infof("Sending alert notification to feishu") errGrp := errgroup.Group{} for _, cId := range fs.ChatIds { errGrp.Go(func() error { msg, err := fs.genCard(ctx, cId) if err != nil { return err } if _, err := fs.Client.SendMessage(*msg); err != nil { log.Errorf("--feishu send msg error: %s, error: %v", jsonutils.Marshal(msg), err) return err } log.Errorf("--feishu send msg: %s", jsonutils.Marshal(msg)) return nil }) } return errGrp.Wait() } func (fs *FeishuNotifier) getCommonInfoMod(config monitor.NotificationTemplateConfig) feishu.CardElement { fields := []*feishu.CardElementField{ feishu.NewCardElementTextField(false, fmt.Sprintf("**时间:** %s", config.StartTime)), feishu.NewCardElementTextField(false, fmt.Sprintf("**级别:** %s", config.Level)), } if config.Reason != "" { fields = append(fields, feishu.NewCardElementTextField(false, fmt.Sprintf("**原因:** %s", config.Reason))) } elem := feishu.CardElement{ Tag: feishu.TagDiv, Fields: fields, } return elem } func (fs *FeishuNotifier) getMetricElem(idx int, m monitor.EvalMatch) *feishu.CardElement { var val string if m.Value == nil { val = "NaN" } else { val = fmt.Sprintf("%.2f", *m.Value) } elem := feishu.CardElement{ Tag: feishu.TagDiv, Fields: []*feishu.CardElementField{ feishu.NewCardElementTextField(false, fmt.Sprintf("**指标 %d:** %s", idx, m.Metric)), feishu.NewCardElementTextField(false, fmt.Sprintf("**当前值:** %s", val)), feishu.NewCardElementTextField(true, fmt.Sprintf("**触发条件:**\n%s", m.Condition)), }, } return &elem } func (fs *FeishuNotifier) getMetricTagElem(m monitor.EvalMatch) *feishu.CardElement { inElems := make([]*feishu.CardElement, 0) for val, key := range m.Tags { inElems = append(inElems, feishu.NewCardElementText(fmt.Sprintf("%s: %s", val, key))) } elem := feishu.CardElement{ Tag: feishu.TagNote, Elements: inElems, } return &elem } func (fs *FeishuNotifier) getMetricsMod(config monitor.NotificationTemplateConfig) []*feishu.CardElement { inElems := make([]*feishu.CardElement, 0) for idx, m := range config.Matches { hrE := feishu.NewCardElementHR() mE := fs.getMetricElem(idx+1, *m) mTE := fs.getMetricTagElem(*m) inElems = append(inElems, hrE, mE, mTE) } return inElems } func (fs *FeishuNotifier) genCard(ctx *alerting.EvalContext, chatId string) (*feishu.MsgReq, error) { config := GetNotifyTemplateConfig(ctx, false, ctx.EvalMatches) commonElem := fs.getCommonInfoMod(config) msElems := fs.getMetricsMod(config) // 消息卡片: https://open.feishu.cn/document/ukTMukTMukTM/uYTNwUjL2UDM14iN1ATN msg := &feishu.MsgReq{ ChatId: chatId, MsgType: feishu.MsgTypeInteractive, Card: &feishu.Card{ Config: &feishu.CardConfig{WideScreenMode: false}, CardLink: nil, Header: &feishu.CardHeader{ Title: &feishu.CardHeaderTitle{ Tag: feishu.TagPlainText, Content: config.Title, }, }, Elements: []interface{}{ commonElem, }, }, } for _, elem := range msElems { msg.Card.Elements = append(msg.Card.Elements, elem) } return msg, nil } func (fs *FeishuNotifier) genMsg(ctx *alerting.EvalContext, chatId string) (*feishu.MsgReq, error) { config := GetNotifyTemplateConfig(ctx, false, ctx.EvalMatches) // 富文本: https://open.feishu.cn/document/ukTMukTMukTM/uMDMxEjLzATMx4yMwETM return &feishu.MsgReq{ ChatId: chatId, MsgType: feishu.MsgTypePost, Content: &feishu.MsgContent{ Post: &feishu.MsgPost{ ZhCn: &feishu.MsgPostValue{ Title: config.Title, Content: []interface{}{ []interface{}{ feishu.MsgPostContentText{ Tag: "text", UnEscape: true, Text: "first line", }, }, }, }, }, }, }, nil }