通知服务 - Nebula.Metis
通知服务主要解决以下问题:
- 应用程序与消息发送过程解耦:程序不需要关注消息发给谁,以及用什么方式发送
- 应用程序与消息内容编排解耦:程序只管提交消息数据,最终发出的消息格式由模板决定
- 消息的发送方式灵活可配置:消息可以用短信发出,或者邮件,或者聊天工具发出
- 发送方式多样性支持:目前已支持 12 种发送渠道,3类消息格式
3类消息格式:
- 文本消息(含模板,Markdown)
- 文件消息
- 图片消息
发送渠道:
可参考本文示例中的SendConfig参数,
- 它是一个数组,表示可以包含多个发送目标
- Mode:发送方式
- Mode=1: 邮件
- Mode=101: 企业微信-群-webhook
- Mode=102: 企业微信-个人
- Mode=103: 企业微信-群
- Mode=201: 钉钉-群-webhook
- Mode=202: 钉钉-个人
- Mode=203: 钉钉-群
- Mode=301: 飞书-群-webhook
- Mode=302: 飞书-个人
- Mode=303: 飞书-群
- Mode=401: POST URL
- Mode=501: 短信(阿里云短信)
- Target:发送目标,数组类型,范围:
- 邮箱
- Im用户名
- Im群ID
- ImHook配置,格式:ImType=xxx;WebHookUrl=xxxxxxxx;SignKey=xxxx
- URL
- 手机号
- AuthConf: 登录认证配置名称,【可选参数】
- 连接参数需要事先在配置服务中注册
- 格式:ImType=WxWork;AppId=xxxxxxxxxxxx;AppSecret=xxxxxxxxxxxxxxxx;AgentId=xxxxxx
- 格式:ImType=DingDing;AppId=xxxxxxxxxxxxxx;AppSecret=xxxxxxxxxxxxxxxx;AgentId=xxxxx
- 格式:ImType=FeiShu;AppId=xxxxxxxxx;AppSecret=xxxxxxxxxxxxxxxxxxx;AgentId=0
- 仅在部分需要认证的场景中使用
- 如果不指定,使用以下默认值:
- IM场景:"ImApp.Auth.Config"
- 邮件:"Nebula_Metis_Email"
- 连接参数需要事先在配置服务中注册
发送【文本】消息
POST http://xxxxxxxx/v20/api/metis/notify2/text.svc?tenantId={TenantId}&sender={AppName}&sendId={SendId} HTTP/1.1
Content-Type: application/json
{
"Text": "xxxxxxxxxx消息正文内容xxxxxxxxxxx",
"IsMarkdown": 0,
"MaxRetryCount": 2,
"Expiration": 60,
"Subject": "message title",
"SendConfig": [
{
"Mode": 102, "Target": [ "liqf01", "yangmc", "fangw" ], "AuthConf": "WxWork.AppAuth.Config_124"
},
{
"Mode": 103, "Target": [ "7f0f8881a37f4be49be51f3f8b4e42c4" ], "AuthConf": "WxWork.AppAuth.Config_124"
}
]
}
参数说明:
- TenantId: 租户ID,如果消息不涉及租户可指定为 "NULL"
- AppName:应用名称
- SendId:一个业务ID号
- Text: 消息正文内容,可以是一个普通的字符串,或者是 markdown
- IsMarkdown: 文本内容是否为 markdown,目前仅供一些IM渠道使用
- MaxRetryCount: 最大重试次数,【可选参数】
- Expiration: 过期时间,单位:秒,【可选参数】
- Subject:消息标题。某些场景下使用,例如:邮件
- SendConfig: 发送渠道参数
- 此处列出的部分参数,在下面的场景中出现时,将不再重复说明。
- 除明确说明是 可选参数 之外,其它都是 必选参数
C#代码示例
await new MetisClient("message title")
.SetTenantId("my57a04574bf635")
.SetSendConfig(MetisSendConfig.Text001())
.SetRetry(2, 600)
.GetTextRequest("xxxxxxxxxx消息正文内容xxxxxxxxxxx")
.SendAsync();
发送【模板】消息
POST http://xxxxxxxx/v20/api/metis/notify2/text.svc?tenantId={TenantId}&sender={AppName}&sendId={SendId} HTTP/1.1
Content-Type: application/json
{
"Text": "{\"tenant_name\":\"XX租户\",\"last_week_result\":99,\"slow_pages\":19,\"unreslove_bugs\":66,\"fault_ticket_rate\":0.85,\"fault_ticket_cnt\":12,\"fault_ticket_times\":7,\"update_fault_tickets\":77,\"timeout_bugs\":13,\"urgent_bug_count\":9,\"health_score\":120}",
"Template": "# **当日拉闸指标计数更新**
> 租户名称:{{data.tenant_name}}
> 最近七天突破底线次数:{{if ((string.to_int data.last_week_result)>0)}}<font color=\"warning\">{{data.last_week_result}}</font>{{else}}<font color=\"info\">{{data.last_week_result}}</font>{{end}}次
> 最近一天拉闸指标如下:
> 关键用户近七天平均慢次数(底线小于等于5个):{{if ((string.to_float data.slow_pages)>5)}}<font color=\"warning\">{{data.slow_pages}}</font>{{else}}<font color=\"info\">{{data.slow_pages}}</font>{{end}}
> 整体BUG数(底线小于等于20个):{{if ((string.to_int data.unreslove_bugs)>20)}}<font color=\"warning\">{{data.unreslove_bugs}}</font>{{else}}<font color=\"info\">{{data.unreslove_bugs}}</font>{{end}}
> 故障类事件占比:{{if (data.fault_ticket_rate>0.10)}}<font color=\"warning\">{{ 100*(string.to_float data.fault_ticket_rate) }}%</font>{{else}}<font color=\"info\">{{ 100*(string.to_float data.fault_ticket_rate) }}%</font>{{end}},故障工单数(底线工单数大于5时,故障类事件占比小于等于10%):{{if ((string.to_int data.fault_ticket_cnt)>5)}}<font color=\"warning\">{{data.fault_ticket_cnt}}</font>{{else}}<font color=\"info\">{{data.fault_ticket_cnt}}</font>{{end}},连续突破底线天数:{{if ((string.to_int data.fault_ticket_times)>1)}}<font color=\"warning\">{{data.fault_ticket_times}}</font>{{else}}<font color=\"info\">{{data.fault_ticket_times}}</font>{{end}}
> 更新引发事件量(底线小于30个):{{if ((string.to_int data.update_fault_tickets)>30)}}<font color=\"warning\">{{data.update_fault_tickets}}</font>{{else}}<font color=\"info\">{{data.update_fault_tickets}}</font>{{end}}
> 超时未解决BUG(底线小于5个):{{if ((string.to_int data.timeout_bugs)>4)}}<font color=\"warning\">{{data.timeout_bugs}}</font>{{else}}<font color=\"info\">{{data.timeout_bugs}}</font>{{end}}
> P1级别BUG类事件单数(底线小于等于2个):{{if ((string.to_int data.urgent_bug_count)>2)}}<font color=\"warning\">{{data.urgent_bug_count}}</font>{{else}}<font color=\"info\">{{data.urgent_bug_count}}</font>{{end}}
> 最近一天三高一关键告警得分如下:
> 系统健康度得分(底线大于等于80分):{{if ((string.to_int data.health_score)<80)}}<font color=\"warning\">{{data.health_score}}</font>{{else}}<font color=\"info\">{{data.health_score}}</font>{{end}}",
"IsMarkdown": 1,
"MaxRetryCount": 2,
"Expiration": 600,
"Subject": "message title",
"SendConfig": [
{
"Mode": 102, "Target": [ "liqf01", "yangmc", "fangw" ], "AuthConf": "WxWork.AppAuth.Config_124"
}
]
}
参数说明:
- Text: 用于绑定模板所需的数据
- Template: 消息模板,如果不使用模板,可以不指定。
C#代码示例
object data = new {
tenant_name = "XX租户",
last_week_result = 99,
slow_pages = 19,
unreslove_bugs = 66,
fault_ticket_rate = 0.85,
fault_ticket_cnt = 12,
fault_ticket_times = 7,
update_fault_tickets = 77,
timeout_bugs = 13,
urgent_bug_count = 9,
health_score = 120
};
await new MetisClient("message title")
.SetTenantId("my57a04574bf635")
.SetSendConfig(MetisSendConfig.Xmsg001())
.SetRetry(2, 600)
.GetTextRequest(data.ToJson(), MetisSendConfig.Template2, true)
.SendAsync();
发送【文件】消息
POST http://xxxxxxxx/v20/api/metis/notify2/file.svc?tenantId={TenantId}&sender={AppName}&sendId={SendId} HTTP/1.1
Content-Type: application/json
{
"FileBody": "......file-base64-string.....or...URL......",
"FileName": "logs.txt",
"MaxRetryCount": 2,
"Expiration": 600,
"Subject": "test send notify/file",
"SendConfig": [
{
"Mode": 102, "Target": [ "liqf01", "yangmc", "fangw" ], "AuthConf": "WxWork.AppAuth.Config_124"
}
]
}
参数说明:
- Filebody: 文件二进制内容做Base64编码后的结果,或者一个可下载的URL
- Filename: IM工具中显示的文件名
C#代码示例
string fileBodyBase64 = "......file-base64-string.....or...URL......";
string filename = "logs.txt";
await new MetisClient("test send notify/file")
.SetTenantId("my57a04574bf635")
.SetSendConfig(MetisSendConfig.FileImg_001())
.SetRetry(2, 600)
.GetFileRequest(fileBodyBase64, filename)
.SendAsync();
发送【图片】消息
POST http://xxxxxxxx/v20/api/metis/notify2/image.svc?tenantId={TenantId}&sender={AppName}&sendId={SendId} HTTP/1.1
Content-Type: application/json
{
"FileBody": "......image-base64-string.....or...URL......",
"MaxRetryCount": 2,
"Expiration": 600,
"Subject": "test send notify/image",
"SendConfig": [
{
"Mode": 102, "Target": [ "liqf01", "yangmc", "fangw" ], "AuthConf": "WxWork.AppAuth.Config_124"
}
]
}
C#代码示例
string imageBase64OrUrl = "......image-base64-string.....or...URL......";
await new MetisClient("test send notify/image")
.SetTenantId("my57a04574bf635")
.SetSendConfig(MetisSendConfig.FileImg_001())
.SetRetry(2, 600)
.GetImageRequest(imageBase64OrUrl)
.SendAsync();
用短信方式,发送验证码
POST http://xxxxxxxx/v20/api/metis/notify2/text.svc?tenantId={TenantId}&sender={AppName}&sendId={SendId} HTTP/1.1
Content-Type: application/json
{
"Text": "{\"code\":\"c8ceb8\"}",
"MaxRetryCount": 2,
"Expiration": 600,
"Subject": "test send notify/sms",
"SendConfig": [
{
"Mode": 501, "Target": [ "15871353763" ], "Options": "SignName=明源云运维;TemplateCode=SMS_229636351"
}
]
}
参数说明:
- Options: 阿里云短信需要这个属性来指定它自己的模板参数
C#代码示例
object data = new { code = Guid.NewGuid().ToString("N").Substring(0, 6) };
await new MetisClient("test send notify/sms")
.SetTenantId("my57a04574bf635")
.SetSendConfig(MetisSendConfig.Sms001())
.SetRetry(2, 600)
.GetTextRequest(data.ToJson())
.SendAsync();
用企业微信,发送验证码
POST http://xxxxxxxx/v20/api/metis/notify2/text.svc?tenantId={TenantId}&sender={AppName}&sendId={SendId} HTTP/1.1
Content-Type: application/json
{
"Text": "Nebula.Venus 登录码: 7777777 ,有效期2分钟。",
"MaxRetryCount": 2,
"Expiration": 600,
"Subject": "test send notify/vcode",
"SendConfig": [
{
"Mode": 102, "Target": [ "liqf01" ], "AuthConf": "WxWork.AppAuth.Config_124"
}
]
}
C#代码示例
string text = "Nebula.Venus 登录码: 7777777 ,有效期2分钟。";
NotifySendConfig sendConfig = new NotifySendConfig {
Mode = NotifySendMode.WxworkUser,
Target = new string[] { "liqf01" },
AuthConf = "WxWork.AppAuth.Config_124"
};
await new MetisClient("test send notify/vcode")
.SetTenantId("my57a04574bf635")
.SetSendConfig(sendConfig)
.SetRetry(2, 600)
.GetTextRequest(text)
.SendAsync();
发送消息的服务端响应
为分3大场景:
- 服务端成功处理请求:HTTP 200
- 客户端提交的数据不正确:HTTP 400
- 服务端出现内部错误:HTTP 500
所有场景可以不关注响应体。
消息重试
- 当出现消息发送失败时,如果不是服务端明确拒绝的错误,都将启动消息重试机制
- MaxRetryCount: 如果超过这个次数,将不再重试
- Expiration: 过期时间,单位:秒。从服务端接收到调用算起,如果超过这个时间,将不再重试
- 每次发送过程,不论是第一次发送还是后续重试发送,都产生一条发送日志
- 重试失败的消息将写入一个死信队列
重试范围
- 当消息发送到多个目标群时,如果只是其中一个群发送失败,那么仅重试那个群的发送过程
- 如果可以批量发送,例如邮件可同时发送给多个人,那么就是整个批操作重试
- 如果发送目标,既有多个群又有多个用户,那么多个用户始终做为一个批来处理,每个群是独立处理,重试也是小范围的
重试间隔时间:
- 固定间隔:3 秒
- 变长间隔:请参考下图
- 控制参数:本地参数 Nebula_LongRetryUtils_Mode = 1,启用【变长间隔】,否则使用 【固定间隔】
发送日志
通知在发送时,
- 不论成功还是失败,都是会记录一条日志
- 如果是重试执行,会单独再记录一条日志,RetryCount 会不同
所有日志会写入 ElasticSearch,索引文件名称:notifysendlog-yyyyMMdd
建议在部署Metis时在Kibana中执行以下配置
- 新建 "Index Lifecycle Policie",假设名为:applog_policy
- 新建 "Index Management", Index pattern: notifysendlog-* ,并按下面的示例来配置Settings
- 新建 "Index pattern",时间字段选 serverTime
{
"index": {
"format": "1",
"lifecycle": {
"name": "applog_policy"
}
}
}