通知服务 - Nebula.Metis

通知服务主要解决以下问题:

  • 应用程序与消息发送过程解耦:程序不需要关注消息发给谁,以及用什么方式发送
  • 应用程序与消息内容编排解耦:程序只管提交消息数据,最终发出的消息格式由模板决定
  • 消息的发送方式灵活可配置:消息可以用短信发出,或者邮件,或者聊天工具发出
  • 发送方式多样性支持:目前已支持 12 种发送渠道,3类消息格式



3类消息格式:

  • 文本消息(含模板,Markdown)
  • 文件消息
  • 图片消息



发送渠道:
  可参考本文示例中的SendConfig参数,

  • 它是一个数组,表示可以包含多个发送目标
  • Mode:发送方式
    1. Mode=1: 邮件
    2. Mode=101: 企业微信-群-webhook
    3. Mode=102: 企业微信-个人
    4. Mode=103: 企业微信-群
    5. Mode=201: 钉钉-群-webhook
    6. Mode=202: 钉钉-个人
    7. Mode=203: 钉钉-群
    8. Mode=301: 飞书-群-webhook
    9. Mode=302: 飞书-个人
    10. Mode=303: 飞书-群
    11. Mode=401: POST URL
    12. 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,启用【变长间隔】,否则使用 【固定间隔】

xx







发送日志

通知在发送时,

  • 不论成功还是失败,都是会记录一条日志
  • 如果是重试执行,会单独再记录一条日志,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"
    }
  }
}