Merge pull request 'alert_optimize' (#135) from alert_optimize into master
This commit is contained in:
commit
350549507b
9
main.go
9
main.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
_ "expvar"
|
_ "expvar"
|
||||||
|
"infini.sh/console/plugin/api/email"
|
||||||
_ "time/tzdata"
|
_ "time/tzdata"
|
||||||
|
|
||||||
log "github.com/cihub/seelog"
|
log "github.com/cihub/seelog"
|
||||||
|
@ -135,6 +136,7 @@ func main() {
|
||||||
orm.RegisterSchemaWithIndexName(task1.Task{}, "task")
|
orm.RegisterSchemaWithIndexName(task1.Task{}, "task")
|
||||||
orm.RegisterSchemaWithIndexName(model.Layout{}, "layout")
|
orm.RegisterSchemaWithIndexName(model.Layout{}, "layout")
|
||||||
orm.RegisterSchemaWithIndexName(model.Notification{}, "notification")
|
orm.RegisterSchemaWithIndexName(model.Notification{}, "notification")
|
||||||
|
orm.RegisterSchemaWithIndexName(model.EmailServer{}, "email-server")
|
||||||
api.RegisterSchema()
|
api.RegisterSchema()
|
||||||
|
|
||||||
if global.Env().SetupRequired() {
|
if global.Env().SetupRequired() {
|
||||||
|
@ -150,6 +152,13 @@ func main() {
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
task1.RunWithinGroup("initialize_email_server", func(ctx context.Context) error {
|
||||||
|
err := email.InitEmailServer()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("init email server error: %v", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if !global.Env().SetupRequired() {
|
if !global.Env().SetupRequired() {
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import "infini.sh/framework/core/orm"
|
import (
|
||||||
|
"infini.sh/framework/core/orm"
|
||||||
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
orm.ORMObjectBase
|
orm.ORMObjectBase
|
||||||
|
@ -13,6 +15,9 @@ type Channel struct {
|
||||||
Priority int `json:"priority,omitempty"`
|
Priority int `json:"priority,omitempty"`
|
||||||
Webhook *CustomWebhook `json:"webhook,omitempty" elastic_mapping:"webhook:{type:object}"`
|
Webhook *CustomWebhook `json:"webhook,omitempty" elastic_mapping:"webhook:{type:object}"`
|
||||||
SearchText string `json:"-" elastic_mapping:"search_text:{type:text,index_prefixes:{},index_phrases:true, analyzer:suggest_text_search }"`
|
SearchText string `json:"-" elastic_mapping:"search_text:{type:text,index_prefixes:{},index_phrases:true, analyzer:suggest_text_search }"`
|
||||||
|
SubType string `json:"sub_type" elastic_mapping:"sub_type:{type:keyword,copy_to:search_text}"`
|
||||||
|
Email *Email `json:"email,omitempty" elastic_mapping:"email:{type:object}"`
|
||||||
|
Enabled bool `json:"enabled" elastic_mapping:"enabled:{type:boolean}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,9 @@ import (
|
||||||
|
|
||||||
type Metric struct {
|
type Metric struct {
|
||||||
insight.Metric
|
insight.Metric
|
||||||
Expression string `json:"expression" elastic_mapping:"expression:{type:keyword,copy_to:search_text}"` //告警表达式,自动生成 eg: avg(cpu) > 80
|
Title string `json:"title,omitempty"` //text template
|
||||||
Title string `json:"title"` //text template
|
Message string `json:"message,omitempty"` // text template
|
||||||
Message string `json:"message"` // text template
|
Expression string `json:"expression,omitempty" elastic_mapping:"expression:{type:keyword,copy_to:search_text}"` //告警表达式,自动生成 eg: avg(cpu) > 80
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,11 @@ type Rule struct {
|
||||||
Enabled bool `json:"enabled" elastic_mapping:"enabled:{type:keyword}"`
|
Enabled bool `json:"enabled" elastic_mapping:"enabled:{type:keyword}"`
|
||||||
Resource Resource `json:"resource" elastic_mapping:"resource:{type:object}"`
|
Resource Resource `json:"resource" elastic_mapping:"resource:{type:object}"`
|
||||||
Metrics Metric `json:"metrics" elastic_mapping:"metrics:{type:object}"`
|
Metrics Metric `json:"metrics" elastic_mapping:"metrics:{type:object}"`
|
||||||
Conditions Condition `json:"conditions" elastic_mapping:"conditions:{type:object}"`
|
Conditions Condition `json:"conditions" elastic_mapping:"conditions:{type:object}"`
|
||||||
Channels RuleChannel `json:"channels" elastic_mapping:"channels:{type:object}"`
|
Channels *NotificationConfig `json:"channels,omitempty" elastic_mapping:"channels:{type:object}"`
|
||||||
Schedule Schedule `json:"schedule" elastic_mapping:"schedule:{type:object}"`
|
NotificationConfig *NotificationConfig `json:"notification_config,omitempty" elastic_mapping:"notification_config:{type:object}"`
|
||||||
|
RecoveryNotificationConfig *RecoveryNotificationConfig `json:"recovery_notification_config,omitempty" elastic_mapping:"recovery_notification_config:{type:object}"`
|
||||||
|
Schedule Schedule `json:"schedule" elastic_mapping:"schedule:{type:object}"`
|
||||||
LastNotificationTime time.Time `json:"-" elastic_mapping:"last_notification_time:{type:date}"`
|
LastNotificationTime time.Time `json:"-" elastic_mapping:"last_notification_time:{type:date}"`
|
||||||
LastTermStartTime time.Time `json:"-"` //标识最近一轮告警的开始时间
|
LastTermStartTime time.Time `json:"-"` //标识最近一轮告警的开始时间
|
||||||
LastEscalationTime time.Time `json:"-"` //标识最近一次告警升级发送通知的时间
|
LastEscalationTime time.Time `json:"-"` //标识最近一次告警升级发送通知的时间
|
||||||
|
@ -54,9 +56,24 @@ func (rule *Rule) GetOrInitExpression() (string, error){
|
||||||
rule.Expression = strings.ReplaceAll(sb.String(), "result", metricExp)
|
rule.Expression = strings.ReplaceAll(sb.String(), "result", metricExp)
|
||||||
return rule.Expression, nil
|
return rule.Expression, nil
|
||||||
}
|
}
|
||||||
|
//GetNotificationConfig for adapter old version config
|
||||||
|
func (rule *Rule) GetNotificationConfig() *NotificationConfig {
|
||||||
|
if rule.NotificationConfig != nil {
|
||||||
|
return rule.NotificationConfig
|
||||||
|
}
|
||||||
|
return rule.Channels
|
||||||
|
}
|
||||||
|
func (rule *Rule) GetNotificationTitleAndMessage() (string, string) {
|
||||||
|
if rule.NotificationConfig != nil {
|
||||||
|
return rule.NotificationConfig.Title, rule.NotificationConfig.Message
|
||||||
|
}
|
||||||
|
return rule.Metrics.Title, rule.Metrics.Message
|
||||||
|
}
|
||||||
|
|
||||||
type RuleChannel struct {
|
type NotificationConfig struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
Title string `json:"title,omitempty"` //text template
|
||||||
|
Message string `json:"message,omitempty"` // text template
|
||||||
Normal []Channel `json:"normal,omitempty"`
|
Normal []Channel `json:"normal,omitempty"`
|
||||||
Escalation []Channel `json:"escalation,omitempty"`
|
Escalation []Channel `json:"escalation,omitempty"`
|
||||||
ThrottlePeriod string `json:"throttle_period,omitempty"` //沉默周期
|
ThrottlePeriod string `json:"throttle_period,omitempty"` //沉默周期
|
||||||
|
@ -65,6 +82,15 @@ type RuleChannel struct {
|
||||||
EscalationEnabled bool `json:"escalation_enabled,omitempty"`
|
EscalationEnabled bool `json:"escalation_enabled,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RecoveryNotificationConfig struct {
|
||||||
|
Enabled bool `json:"enabled"` // channel enabled
|
||||||
|
Title string `json:"title"` //text template
|
||||||
|
Message string `json:"message"` // text template
|
||||||
|
AcceptTimeRange TimeRange `json:"accept_time_range,omitempty"`
|
||||||
|
Normal []Channel `json:"normal,omitempty"`
|
||||||
|
EventEnabled bool `json:"event_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
type MessageTemplate struct{
|
type MessageTemplate struct{
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
|
|
|
@ -6,6 +6,7 @@ package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"infini.sh/console/model/insight"
|
||||||
"infini.sh/framework/core/util"
|
"infini.sh/framework/core/util"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -60,22 +61,25 @@ func TestCreateRule( t *testing.T) {
|
||||||
//},
|
//},
|
||||||
|
|
||||||
Metrics: Metric{
|
Metrics: Metric{
|
||||||
PeriodInterval: "1m",
|
Metric: insight.Metric{
|
||||||
Items: []MetricItem{
|
Groups: []insight.MetricGroupItem{{"metadata.labels.cluster_id", 10}, {"metadata.labels.node_id", 10}},
|
||||||
{Name: "a", Field: "payload.elasticsearch.node_stats.fs.total.free_in_bytes", Statistic: "min", Group: []string{"metadata.labels.cluster_id", "metadata.labels.node_id"}},
|
Items: []insight.MetricItem{
|
||||||
{Name: "b", Field: "payload.elasticsearch.node_stats.fs.total.total_in_bytes", Statistic: "max", Group: []string{"metadata.labels.cluster_id", "metadata.labels.node_id"}},
|
{Name: "a", Field: "payload.elasticsearch.node_stats.fs.total.free_in_bytes", Statistic: "min" },
|
||||||
|
{Name: "b", Field: "payload.elasticsearch.node_stats.fs.total.total_in_bytes", Statistic: "max"},
|
||||||
|
},
|
||||||
|
BucketSize: "1m",
|
||||||
|
Formula: "a/b*100",
|
||||||
},
|
},
|
||||||
Formula: "a/b*100",
|
|
||||||
//Expression: "min(fs.free_in_bytes)/max(fs.total_in_bytes)*100",
|
//Expression: "min(fs.free_in_bytes)/max(fs.total_in_bytes)*100",
|
||||||
},
|
},
|
||||||
Conditions: Condition{
|
Conditions: Condition{
|
||||||
Operator: "any",
|
Operator: "any",
|
||||||
Items: []ConditionItem{
|
Items: []ConditionItem{
|
||||||
{MinimumPeriodMatch: 1, Operator: "lte", Values: []string{"76"}, Priority: "error", Message: "磁盘可用率小于10%"},
|
{MinimumPeriodMatch: 1, Operator: "lte", Values: []string{"76"}, Priority: "error"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Channels: RuleChannel{
|
Channels: NotificationConfig{
|
||||||
Normal: []Channel{
|
Normal: []Channel{
|
||||||
{Name: "钉钉", Type: ChannelWebhook, Webhook: &CustomWebhook{
|
{Name: "钉钉", Type: ChannelWebhook, Webhook: &CustomWebhook{
|
||||||
HeaderParams: map[string]string{
|
HeaderParams: map[string]string{
|
||||||
|
|
|
@ -10,3 +10,14 @@ type CustomWebhook struct {
|
||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
Body string `json:"body" elastic_mapping:"body:{type:text}"`
|
Body string `json:"body" elastic_mapping:"body:{type:text}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Email struct {
|
||||||
|
ServerID string `json:"server_id" elastic_mapping:"server_id:{type:keyword}"`
|
||||||
|
Recipients struct {
|
||||||
|
To []string `json:"to,omitempty" elastic_mapping:"to:{type:keyword}"`
|
||||||
|
CC []string `json:"cc,omitempty" elastic_mapping:"cc:{type:keyword}"`
|
||||||
|
BCC []string `json:"bcc,omitempty" elastic_mapping:"bcc:{type:keyword}"`
|
||||||
|
} `json:"recipients" elastic_mapping:"recipients:{type:object}"`
|
||||||
|
Subject string `json:"subject" elastic_mapping:"subject:{type:text}"`
|
||||||
|
Body string `json:"body" elastic_mapping:"body:{type:text}"`
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* Copyright © INFINI Ltd. All rights reserved.
|
||||||
|
* Web: https://infinilabs.com
|
||||||
|
* Email: hello#infini.ltd */
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"infini.sh/framework/core/elastic"
|
||||||
|
"infini.sh/framework/core/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailServer struct {
|
||||||
|
orm.ORMObjectBase
|
||||||
|
Name string `json:"name" elastic_mapping:"name:{type:text}"`
|
||||||
|
Host string `json:"host" elastic_mapping:"host:{type:keyword}"`
|
||||||
|
Port int `json:"port" elastic_mapping:"port:{type:keyword}"`
|
||||||
|
TLS bool `json:"tls" elastic_mapping:"tls:{type:keyword}"`
|
||||||
|
Auth *elastic.BasicAuth `json:"auth" elastic_mapping:"auth:{type:object}"`
|
||||||
|
Enabled bool `json:"enabled" elastic_mapping:"enabled:{type:boolean}"`
|
||||||
|
CredentialID string `json:"credential_id" elastic_mapping:"credential_id:{type:keyword}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (serv *EmailServer) Validate(requireName bool) error {
|
||||||
|
if serv.Host == "" {
|
||||||
|
return fmt.Errorf("host can not be empty")
|
||||||
|
}
|
||||||
|
if serv.Port <= 0 {
|
||||||
|
return fmt.Errorf("invalid port [%d]", serv.Port)
|
||||||
|
}
|
||||||
|
if requireName && serv.Name == "" {
|
||||||
|
return fmt.Errorf("name can not be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ func (alert *AlertAPI) Init() {
|
||||||
api.HandleAPIMethod(api.DELETE, "/alerting/channel", alert.RequirePermission(alert.deleteChannel, enum.PermissionAlertChannelWrite))
|
api.HandleAPIMethod(api.DELETE, "/alerting/channel", alert.RequirePermission(alert.deleteChannel, enum.PermissionAlertChannelWrite))
|
||||||
api.HandleAPIMethod(api.PUT, "/alerting/channel/:channel_id", alert.RequirePermission(alert.updateChannel, enum.PermissionAlertChannelWrite))
|
api.HandleAPIMethod(api.PUT, "/alerting/channel/:channel_id", alert.RequirePermission(alert.updateChannel, enum.PermissionAlertChannelWrite))
|
||||||
api.HandleAPIMethod(api.GET, "/alerting/channel/_search", alert.RequirePermission(alert.searchChannel, enum.PermissionAlertChannelRead))
|
api.HandleAPIMethod(api.GET, "/alerting/channel/_search", alert.RequirePermission(alert.searchChannel, enum.PermissionAlertChannelRead))
|
||||||
|
api.HandleAPIMethod(api.POST, "/alerting/channel/test", alert.RequirePermission(alert.testChannel, enum.PermissionAlertChannelWrite))
|
||||||
|
|
||||||
api.HandleAPIMethod(api.GET, "/alerting/alert/_search", alert.RequirePermission(alert.searchAlert, enum.PermissionAlertHistoryRead))
|
api.HandleAPIMethod(api.GET, "/alerting/alert/_search", alert.RequirePermission(alert.searchAlert, enum.PermissionAlertHistoryRead))
|
||||||
api.HandleAPIMethod(api.GET, "/alerting/alert/:alert_id", alert.RequirePermission(alert.getAlert, enum.PermissionAlertHistoryRead))
|
api.HandleAPIMethod(api.GET, "/alerting/alert/:alert_id", alert.RequirePermission(alert.getAlert, enum.PermissionAlertHistoryRead))
|
||||||
|
|
|
@ -6,9 +6,11 @@ package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"infini.sh/console/service/alerting/common"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/cihub/seelog"
|
log "github.com/cihub/seelog"
|
||||||
"infini.sh/console/model/alerting"
|
"infini.sh/console/model/alerting"
|
||||||
|
@ -33,10 +35,7 @@ func (h *AlertAPI) createChannel(w http.ResponseWriter, req *http.Request, ps ht
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.WriteJSON(w, util.MapStr{
|
h.WriteCreatedOKJSON(w, obj.ID)
|
||||||
"_id": obj.ID,
|
|
||||||
"result": "created",
|
|
||||||
}, 200)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,11 +59,7 @@ func (h *AlertAPI) getChannel(w http.ResponseWriter, req *http.Request, ps httpr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.WriteJSON(w, util.MapStr{
|
h.WriteGetOKJSON(w, id, obj)
|
||||||
"found": true,
|
|
||||||
"_id": id,
|
|
||||||
"_source": obj,
|
|
||||||
}, 200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AlertAPI) updateChannel(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
func (h *AlertAPI) updateChannel(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
@ -101,10 +96,7 @@ func (h *AlertAPI) updateChannel(w http.ResponseWriter, req *http.Request, ps ht
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.WriteJSON(w, util.MapStr{
|
h.WriteUpdatedOKJSON(w, id)
|
||||||
"_id": obj.ID,
|
|
||||||
"result": "updated",
|
|
||||||
}, 200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AlertAPI) deleteChannel(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
func (h *AlertAPI) deleteChannel(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
@ -179,3 +171,43 @@ func (h *AlertAPI) searchChannel(w http.ResponseWriter, req *http.Request, ps ht
|
||||||
}
|
}
|
||||||
h.Write(w, res.Raw)
|
h.Write(w, res.Raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AlertAPI) testChannel(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
obj := alerting.Channel{}
|
||||||
|
err := h.DecodeJSON(req, &obj)
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := map[string]interface{}{
|
||||||
|
"title": "test title",
|
||||||
|
"message": "test message",
|
||||||
|
"rule_id": util.GetUUID(),
|
||||||
|
"rule_name": "test rule",
|
||||||
|
"resource_id": util.GetUUID(),
|
||||||
|
"resource_name": "test resource",
|
||||||
|
"event_id": util.GetUUID(),
|
||||||
|
"timestamp": time.Now().UnixMilli(),
|
||||||
|
"first_group_value": "first group value",
|
||||||
|
"first_threshold": "90",
|
||||||
|
"priority": "critical",
|
||||||
|
"results": []util.MapStr{
|
||||||
|
{"threshold": "90",
|
||||||
|
"priority": "critical",
|
||||||
|
"group_values": []string{"first group value", "second group value"},
|
||||||
|
"issue_timestamp": time.Now().UnixMilli()-500,
|
||||||
|
"result_value": 90,
|
||||||
|
"relation_values": util.MapStr{"a": 100, "b": 90},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
_, err, _ = common.PerformChannel(&obj, ctx)
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.WriteAckOKJSON(w)
|
||||||
|
}
|
|
@ -135,6 +135,20 @@ func (alertAPI *AlertAPI) getRule(w http.ResponseWriter, req *http.Request, ps h
|
||||||
alertAPI.WriteError(w, err.Error(), http.StatusInternalServerError)
|
alertAPI.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// adapter version smaller than 1.6.0
|
||||||
|
if obj.Channels != nil && obj.NotificationConfig == nil {
|
||||||
|
obj.NotificationConfig = obj.Channels
|
||||||
|
for i := range obj.NotificationConfig.Normal {
|
||||||
|
obj.NotificationConfig.Normal[i].Enabled = true
|
||||||
|
}
|
||||||
|
for i := range obj.NotificationConfig.Escalation {
|
||||||
|
obj.NotificationConfig.Escalation[i].Enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if obj.NotificationConfig != nil && obj.NotificationConfig.Message == "" && obj.Metrics.Message != "" {
|
||||||
|
obj.NotificationConfig.Message = obj.Metrics.Message
|
||||||
|
obj.NotificationConfig.Title = obj.Metrics.Title
|
||||||
|
}
|
||||||
|
|
||||||
alertAPI.WriteJSON(w, util.MapStr{
|
alertAPI.WriteJSON(w, util.MapStr{
|
||||||
"found": true,
|
"found": true,
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/* Copyright © INFINI Ltd. All rights reserved.
|
||||||
|
* Web: https://infinilabs.com
|
||||||
|
* Email: hello#infini.ltd */
|
||||||
|
|
||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"infini.sh/console/plugin/api/email/common"
|
||||||
|
"infini.sh/framework/core/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailAPI struct {
|
||||||
|
api.Handler
|
||||||
|
}
|
||||||
|
func InitAPI() {
|
||||||
|
email := EmailAPI{}
|
||||||
|
api.HandleAPIMethod(api.POST, "/email/server/_test", email.testEmailServer)
|
||||||
|
api.HandleAPIMethod(api.GET, "/email/server/:email_server_id", email.getEmailServer)
|
||||||
|
api.HandleAPIMethod(api.POST, "/email/server", email.createEmailServer)
|
||||||
|
api.HandleAPIMethod(api.PUT, "/email/server/:email_server_id", email.updateEmailServer)
|
||||||
|
api.HandleAPIMethod(api.DELETE, "/email/server/:email_server_id", email.deleteEmailServer)
|
||||||
|
api.HandleAPIMethod(api.GET, "/email/server/_search", email.searchEmailServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitEmailServer() error {
|
||||||
|
if !common.CheckEmailPipelineExists() {
|
||||||
|
return common.RefreshEmailServer()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/* Copyright © INFINI Ltd. All rights reserved.
|
||||||
|
* Web: https://infinilabs.com
|
||||||
|
* Email: hello#infini.ltd */
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"infini.sh/console/model"
|
||||||
|
"infini.sh/framework/core/credential"
|
||||||
|
"infini.sh/framework/core/elastic"
|
||||||
|
"infini.sh/framework/core/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBasicAuth(srv *model.EmailServer) (basicAuth elastic.BasicAuth, err error) {
|
||||||
|
if srv.Auth != nil && srv.Auth.Username != "" {
|
||||||
|
basicAuth = *srv.Auth
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if srv.CredentialID != "" {
|
||||||
|
cred := credential.Credential{}
|
||||||
|
cred.ID = srv.CredentialID
|
||||||
|
_, err = orm.Get(&cred)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var dv interface{}
|
||||||
|
dv, err = cred.Decode()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if auth, ok := dv.(elastic.BasicAuth); ok {
|
||||||
|
basicAuth = auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
/* Copyright © INFINI Ltd. All rights reserved.
|
||||||
|
* Web: https://infinilabs.com
|
||||||
|
* Email: hello#infini.ltd */
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"infini.sh/console/model"
|
||||||
|
"infini.sh/framework/core/global"
|
||||||
|
"infini.sh/framework/core/orm"
|
||||||
|
"infini.sh/framework/core/util"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const emailServerConfigFile = "send_email.yml"
|
||||||
|
func RefreshEmailServer() error {
|
||||||
|
q := orm.Query{
|
||||||
|
Size: 10,
|
||||||
|
}
|
||||||
|
q.Conds = orm.And(orm.Eq("enabled", true))
|
||||||
|
err, result := orm.Search(model.EmailServer{}, &q )
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(result.Result) == 0 {
|
||||||
|
return StopEmailServer()
|
||||||
|
}
|
||||||
|
servers := make([]model.EmailServer,0, len(result.Result))
|
||||||
|
for _, row := range result.Result {
|
||||||
|
emailServer := model.EmailServer{}
|
||||||
|
buf := util.MustToJSONBytes(row)
|
||||||
|
util.MustFromJSONBytes(buf, &emailServer)
|
||||||
|
err = emailServer.Validate(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
auth, err := GetBasicAuth(&emailServer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
emailServer.Auth = &auth
|
||||||
|
servers = append(servers, emailServer)
|
||||||
|
}
|
||||||
|
pipeCfgStr := GeneratePipelineConfig(servers)
|
||||||
|
cfgDir := global.Env().GetConfigDir()
|
||||||
|
sendEmailCfgFile := path.Join(cfgDir, emailServerConfigFile)
|
||||||
|
_, err = util.FilePutContent(sendEmailCfgFile, pipeCfgStr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopEmailServer() error {
|
||||||
|
cfgDir := global.Env().GetConfigDir()
|
||||||
|
sendEmailCfgFile := path.Join(cfgDir, emailServerConfigFile)
|
||||||
|
if util.FilesExists(sendEmailCfgFile) {
|
||||||
|
return os.RemoveAll(sendEmailCfgFile)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckEmailPipelineExists() bool {
|
||||||
|
cfgDir := global.Env().GetConfigDir()
|
||||||
|
sendEmailCfgFile := path.Join(cfgDir, emailServerConfigFile)
|
||||||
|
return util.FilesExists(sendEmailCfgFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func GeneratePipelineConfig(servers []model.EmailServer) string {
|
||||||
|
if len(servers) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
smtpServers := map[string]util.MapStr{}
|
||||||
|
for _, srv := range servers {
|
||||||
|
smtpServers[srv.ID] = util.MapStr{
|
||||||
|
"server": util.MapStr{
|
||||||
|
"host": srv.Host,
|
||||||
|
"port": srv.Port,
|
||||||
|
"tls": srv.TLS,
|
||||||
|
},
|
||||||
|
"auth": util.MapStr{
|
||||||
|
"username": srv.Auth.Username,
|
||||||
|
"password": srv.Auth.Password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pipelineCfg := util.MapStr{
|
||||||
|
"pipeline": []util.MapStr{
|
||||||
|
{
|
||||||
|
"name": "send_email_service",
|
||||||
|
"auto_start": true,
|
||||||
|
"keep_running": true,
|
||||||
|
"retry_delay_in_ms": 5000,
|
||||||
|
"processor": []util.MapStr{
|
||||||
|
{
|
||||||
|
"consumer": util.MapStr{
|
||||||
|
"consumer": util.MapStr{
|
||||||
|
"fetch_max_messages": 1,
|
||||||
|
},
|
||||||
|
"max_worker_size": 200,
|
||||||
|
"num_of_slices": 1,
|
||||||
|
"idle_timeout_in_seconds": 30,
|
||||||
|
"queue_selector": util.MapStr{
|
||||||
|
"keys": []string{"email_messages"},
|
||||||
|
},
|
||||||
|
"processor": []util.MapStr{
|
||||||
|
{
|
||||||
|
"smtp": util.MapStr{
|
||||||
|
"idle_timeout_in_seconds": 1,
|
||||||
|
"servers": smtpServers,
|
||||||
|
"templates": util.MapStr{
|
||||||
|
"raw": util.MapStr{
|
||||||
|
"content_type": "text/plain",
|
||||||
|
"subject": "$[[subject]]",
|
||||||
|
"body": "$[[body]]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := yaml.Marshal(pipelineCfg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(buf)
|
||||||
|
}
|
|
@ -0,0 +1,370 @@
|
||||||
|
/* Copyright © INFINI Ltd. All rights reserved.
|
||||||
|
* Web: https://infinilabs.com
|
||||||
|
* Email: hello#infini.ltd */
|
||||||
|
|
||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/cihub/seelog"
|
||||||
|
"infini.sh/console/model"
|
||||||
|
"infini.sh/console/model/alerting"
|
||||||
|
"infini.sh/console/plugin/api/email/common"
|
||||||
|
httprouter "infini.sh/framework/core/api/router"
|
||||||
|
"infini.sh/framework/core/credential"
|
||||||
|
"infini.sh/framework/core/orm"
|
||||||
|
"infini.sh/framework/core/util"
|
||||||
|
"net/http"
|
||||||
|
"src/github.com/buger/jsonparser"
|
||||||
|
"src/github.com/gopkg.in/gomail.v2"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *EmailAPI) createEmailServer(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
var obj = &model.EmailServer{}
|
||||||
|
err := h.DecodeJSON(req, obj)
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := util.MapStr{
|
||||||
|
"size": 1,
|
||||||
|
"query": util.MapStr{
|
||||||
|
"bool": util.MapStr{
|
||||||
|
"must": []util.MapStr{
|
||||||
|
{
|
||||||
|
"term": util.MapStr{
|
||||||
|
"host": util.MapStr{
|
||||||
|
"value": obj.Host,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"term": util.MapStr{
|
||||||
|
"port": util.MapStr{
|
||||||
|
"value": obj.Port,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
query := orm.Query{
|
||||||
|
RawQuery: util.MustToJSONBytes(q),
|
||||||
|
}
|
||||||
|
err, result := orm.Search(obj, &query)
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(result.Result) > 0 {
|
||||||
|
h.WriteError(w, fmt.Sprintf("email server [%s:%d] already exists", obj.Host, obj.Port), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if obj.CredentialID == "" && obj.Auth != nil && obj.Auth.Username != ""{
|
||||||
|
credentialID, err := saveBasicAuthToCredential(obj)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
obj.CredentialID = credentialID
|
||||||
|
}
|
||||||
|
obj.Auth = nil
|
||||||
|
|
||||||
|
err = orm.Create(&orm.Context{
|
||||||
|
Refresh: "wait_for",
|
||||||
|
}, obj)
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if obj.Enabled {
|
||||||
|
err = common.RefreshEmailServer()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.WriteCreatedOKJSON(w, obj.ID)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveBasicAuthToCredential(srv *model.EmailServer)(string, error){
|
||||||
|
if srv == nil {
|
||||||
|
return "", fmt.Errorf("param email config can not be empty")
|
||||||
|
}
|
||||||
|
cred := credential.Credential{
|
||||||
|
Name: srv.Name,
|
||||||
|
Type: credential.BasicAuth,
|
||||||
|
Tags: []string{"Email"},
|
||||||
|
Payload: map[string]interface{}{
|
||||||
|
"basic_auth": map[string]interface{}{
|
||||||
|
"username": srv.Auth.Username,
|
||||||
|
"password": srv.Auth.Password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cred.ID = util.GetUUID()
|
||||||
|
err := cred.Encode()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = orm.Create(nil, &cred)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return cred.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *EmailAPI) getEmailServer(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
id := ps.MustGetParameter("email_server_id")
|
||||||
|
|
||||||
|
obj := model.EmailServer{}
|
||||||
|
obj.ID = id
|
||||||
|
|
||||||
|
exists, err := orm.Get(&obj)
|
||||||
|
if !exists || err != nil {
|
||||||
|
h.WriteJSON(w, util.MapStr{
|
||||||
|
"_id": id,
|
||||||
|
"found": false,
|
||||||
|
}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.WriteGetOKJSON(w, id, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *EmailAPI) updateEmailServer(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
id := ps.MustGetParameter("email_server_id")
|
||||||
|
obj := model.EmailServer{}
|
||||||
|
|
||||||
|
obj.ID = id
|
||||||
|
exists, err := orm.Get(&obj)
|
||||||
|
if !exists || err != nil {
|
||||||
|
h.WriteJSON(w, util.MapStr{
|
||||||
|
"_id": id,
|
||||||
|
"result": "not_found",
|
||||||
|
}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id = obj.ID
|
||||||
|
create := obj.Created
|
||||||
|
newObj := model.EmailServer{}
|
||||||
|
err = h.DecodeJSON(req, &newObj)
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !newObj.Enabled && obj.Enabled {
|
||||||
|
if err = checkEmailServerReferenced(&obj); err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if newObj.Auth != nil && newObj.CredentialID == "" {
|
||||||
|
credentialID, err := saveBasicAuthToCredential(&newObj)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newObj.CredentialID = credentialID
|
||||||
|
newObj.Auth = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//protect
|
||||||
|
newObj.ID = id
|
||||||
|
newObj.Created = create
|
||||||
|
err = orm.Update(&orm.Context{
|
||||||
|
Refresh: "wait_for",
|
||||||
|
}, &newObj)
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = common.RefreshEmailServer()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.WriteUpdatedOKJSON(w, obj.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *EmailAPI) deleteEmailServer(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
id := ps.MustGetParameter("email_server_id")
|
||||||
|
|
||||||
|
obj := model.EmailServer{}
|
||||||
|
obj.ID = id
|
||||||
|
|
||||||
|
exists, err := orm.Get(&obj)
|
||||||
|
if !exists || err != nil {
|
||||||
|
h.WriteJSON(w, util.MapStr{
|
||||||
|
"_id": id,
|
||||||
|
"result": "not_found",
|
||||||
|
}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = checkEmailServerReferenced(&obj); err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = orm.Delete(&orm.Context{
|
||||||
|
Refresh: "wait_for",
|
||||||
|
}, &obj)
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if obj.Enabled {
|
||||||
|
err = common.RefreshEmailServer()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.WriteDeletedOKJSON(w, obj.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkEmailServerReferenced(srv *model.EmailServer) error {
|
||||||
|
q := &orm.Query{
|
||||||
|
Size: 1,
|
||||||
|
}
|
||||||
|
q.Conds = orm.And(orm.Eq("email.server_id", srv.ID))
|
||||||
|
err, result := orm.Search(alerting.Channel{}, q)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(result.Result) > 0 {
|
||||||
|
var chName interface{} = ""
|
||||||
|
if m, ok := result.Result[0].(map[string]interface{}); ok {
|
||||||
|
chName = m["name"]
|
||||||
|
}
|
||||||
|
return fmt.Errorf("email server used by channel [%s]", chName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *EmailAPI) searchEmailServer(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
strSize = h.GetParameterOrDefault(req, "size", "20")
|
||||||
|
strFrom = h.GetParameterOrDefault(req, "from", "0")
|
||||||
|
strEnabled = h.GetParameterOrDefault(req, "enabled", "true")
|
||||||
|
)
|
||||||
|
size, _ := strconv.Atoi(strSize)
|
||||||
|
if size <= 0 {
|
||||||
|
size = 20
|
||||||
|
}
|
||||||
|
from, _ := strconv.Atoi(strFrom)
|
||||||
|
if from < 0 {
|
||||||
|
from = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
q := orm.Query{
|
||||||
|
From: from,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
if strEnabled == "true" {
|
||||||
|
q.Conds = orm.And(orm.Eq("enabled", true))
|
||||||
|
}else if strEnabled == "false" {
|
||||||
|
q.Conds = orm.And(orm.Eq("enabled", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
err, res := orm.Search(&model.EmailServer{}, &q)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//remove password field
|
||||||
|
hitsBuf := bytes.Buffer{}
|
||||||
|
hitsBuf.Write([]byte("["))
|
||||||
|
jsonparser.ArrayEach(res.Raw, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
|
||||||
|
value = jsonparser.Delete(value, "_source", "auth", "password")
|
||||||
|
hitsBuf.Write(value)
|
||||||
|
hitsBuf.Write([]byte(","))
|
||||||
|
}, "hits", "hits")
|
||||||
|
buf := hitsBuf.Bytes()
|
||||||
|
if buf[len(buf)-1] == ',' {
|
||||||
|
buf[len(buf)-1] = ']'
|
||||||
|
}else{
|
||||||
|
hitsBuf.Write([]byte("]"))
|
||||||
|
}
|
||||||
|
res.Raw, err = jsonparser.Set(res.Raw, hitsBuf.Bytes(), "hits", "hits")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
h.ErrorInternalServer(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.Write(w, res.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *EmailAPI) testEmailServer(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
reqBody := &struct {
|
||||||
|
SendTo []string `json:"send_to"`
|
||||||
|
model.EmailServer
|
||||||
|
}{}
|
||||||
|
err := h.DecodeJSON(req, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(reqBody.SendTo) == 0 {
|
||||||
|
h.WriteError(w, "receiver email address can not be empty", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = reqBody.Validate(false); err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if reqBody.CredentialID != "" {
|
||||||
|
auth, err := common.GetBasicAuth(&reqBody.EmailServer)
|
||||||
|
if err != nil {
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqBody.Auth = &auth
|
||||||
|
}
|
||||||
|
if reqBody.Auth == nil {
|
||||||
|
h.WriteError(w, "auth info required", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message := gomail.NewMessage()
|
||||||
|
message.SetHeader("From", reqBody.Auth.Username)
|
||||||
|
message.SetHeader("To", reqBody.SendTo...)
|
||||||
|
message.SetHeader("Subject", "test email")
|
||||||
|
|
||||||
|
message.SetBody("text/plain", "This is just a test email, do not reply!")
|
||||||
|
d := gomail.NewDialerWithTimeout(reqBody.Host, reqBody.Port, reqBody.Auth.Username, reqBody.Auth.Password, 3*time.Second)
|
||||||
|
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
d.SSL = reqBody.TLS
|
||||||
|
|
||||||
|
err = d.DialAndSend(message)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.WriteAckOKJSON(w)
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"infini.sh/console/plugin/api/email"
|
||||||
"infini.sh/console/plugin/api/license"
|
"infini.sh/console/plugin/api/license"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
@ -75,4 +76,5 @@ func Init(cfg *config.AppConfig) {
|
||||||
notification.InitAPI()
|
notification.InitAPI()
|
||||||
|
|
||||||
license.InitAPI()
|
license.InitAPI()
|
||||||
|
email.InitAPI()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* Copyright © INFINI Ltd. All rights reserved.
|
||||||
|
* Web: https://infinilabs.com
|
||||||
|
* Email: hello#infini.ltd */
|
||||||
|
|
||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"infini.sh/console/model/alerting"
|
||||||
|
"infini.sh/framework/core/queue"
|
||||||
|
"infini.sh/framework/core/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailAction struct {
|
||||||
|
Data *alerting.Email
|
||||||
|
Subject string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmailQueueName = "email_messages"
|
||||||
|
|
||||||
|
func (act *EmailAction) Execute()([]byte, error){
|
||||||
|
queueCfg := queue.GetOrInitConfig(EmailQueueName)
|
||||||
|
emailMsg := util.MapStr{
|
||||||
|
"server_id": act.Data.ServerID,
|
||||||
|
"email": act.Data.Recipients.To,
|
||||||
|
"template": "raw",
|
||||||
|
"variables": util.MapStr{
|
||||||
|
"subject": act.Subject,
|
||||||
|
"body": act.Body,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
emailMsgBytes := util.MustToJSONBytes(emailMsg)
|
||||||
|
err := queue.Push(queueCfg, emailMsgBytes)
|
||||||
|
return nil, err
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/* Copyright © INFINI Ltd. All rights reserved.
|
||||||
|
* Web: https://infinilabs.com
|
||||||
|
* Email: hello#infini.ltd */
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"infini.sh/console/model/alerting"
|
||||||
|
"infini.sh/console/service/alerting/action"
|
||||||
|
"infini.sh/console/service/alerting/funcs"
|
||||||
|
"infini.sh/framework/core/orm"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PerformChannel(channel *alerting.Channel, ctx map[string]interface{}) ([]byte, error, []byte) {
|
||||||
|
if channel == nil {
|
||||||
|
return nil, fmt.Errorf("empty channel"), nil
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
act action.Action
|
||||||
|
message []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch channel.Type {
|
||||||
|
|
||||||
|
case alerting.ChannelWebhook:
|
||||||
|
message, err = ResolveMessage(channel.Webhook.Body, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err, message
|
||||||
|
}
|
||||||
|
wh := *channel.Webhook
|
||||||
|
urlBytes, err := ResolveMessage(wh.URL, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err, message
|
||||||
|
}
|
||||||
|
wh.URL = string(urlBytes)
|
||||||
|
act = &action.WebhookAction{
|
||||||
|
Data: &wh,
|
||||||
|
Message: string(message),
|
||||||
|
}
|
||||||
|
case alerting.ChannelEmail:
|
||||||
|
message, err = ResolveMessage(channel.Email.Body, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err, message
|
||||||
|
}
|
||||||
|
subjectBytes, err := ResolveMessage(channel.Email.Subject, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err, nil
|
||||||
|
}
|
||||||
|
act = &action.EmailAction{
|
||||||
|
Data: channel.Email,
|
||||||
|
Subject: string(subjectBytes),
|
||||||
|
Body: string(message),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported action type: %s", channel.Type), message
|
||||||
|
}
|
||||||
|
executeResult, err := act.Execute()
|
||||||
|
return executeResult, err, message
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveMessage(messageTemplate string, ctx map[string]interface{}) ([]byte, error){
|
||||||
|
msg := messageTemplate
|
||||||
|
tmpl, err := template.New("alert-message").Funcs(funcs.GenericFuncMap()).Parse(msg)
|
||||||
|
if err !=nil {
|
||||||
|
return nil, fmt.Errorf("parse message temlate error: %w", err)
|
||||||
|
}
|
||||||
|
msgBuffer := &bytes.Buffer{}
|
||||||
|
err = tmpl.Execute(msgBuffer, ctx)
|
||||||
|
return msgBuffer.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RetrieveChannel(ch *alerting.Channel) (*alerting.Channel, error) {
|
||||||
|
if ch == nil {
|
||||||
|
return nil, fmt.Errorf("empty channel")
|
||||||
|
}
|
||||||
|
enabled := ch.Enabled
|
||||||
|
if ch.ID != "" {
|
||||||
|
_, err := orm.Get(ch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ch.Enabled = enabled
|
||||||
|
}
|
||||||
|
return ch, nil
|
||||||
|
}
|
|
@ -5,7 +5,6 @@
|
||||||
package elasticsearch
|
package elasticsearch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/Knetic/govaluate"
|
"github.com/Knetic/govaluate"
|
||||||
|
@ -13,8 +12,7 @@ import (
|
||||||
"infini.sh/console/model"
|
"infini.sh/console/model"
|
||||||
"infini.sh/console/model/alerting"
|
"infini.sh/console/model/alerting"
|
||||||
alerting2 "infini.sh/console/service/alerting"
|
alerting2 "infini.sh/console/service/alerting"
|
||||||
"infini.sh/console/service/alerting/action"
|
"infini.sh/console/service/alerting/common"
|
||||||
"infini.sh/console/service/alerting/funcs"
|
|
||||||
"infini.sh/framework/core/elastic"
|
"infini.sh/framework/core/elastic"
|
||||||
"infini.sh/console/model/insight"
|
"infini.sh/console/model/insight"
|
||||||
"infini.sh/framework/core/kv"
|
"infini.sh/framework/core/kv"
|
||||||
|
@ -25,7 +23,6 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -679,6 +676,21 @@ func (engine *Engine) Do(rule *alerting.Rule) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("save alert message error: %w", err)
|
return fmt.Errorf("save alert message error: %w", err)
|
||||||
}
|
}
|
||||||
|
// todo add recover notification to inner system message
|
||||||
|
// send recover message to channel
|
||||||
|
recoverCfg := rule.RecoveryNotificationConfig
|
||||||
|
if recoverCfg != nil && recoverCfg.EventEnabled && recoverCfg.Enabled {
|
||||||
|
paramsCtx = newParameterCtx(rule, checkResults, util.MapStr{
|
||||||
|
alerting2.ParamEventID: alertItem.ID,
|
||||||
|
alerting2.ParamTimestamp: alertItem.Created.Unix(),
|
||||||
|
})
|
||||||
|
err = attachTitleMessageToCtx(recoverCfg.Title, recoverCfg.Message, paramsCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
actionResults, _ := performChannels(recoverCfg.Normal, paramsCtx)
|
||||||
|
alertItem.ActionExecutionResults = actionResults
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -698,7 +710,8 @@ func (engine *Engine) Do(rule *alerting.Rule) error {
|
||||||
})
|
})
|
||||||
|
|
||||||
alertItem.Priority = priority
|
alertItem.Priority = priority
|
||||||
err = attachTitleMessageToCtx(rule, paramsCtx)
|
title, message := rule.GetNotificationTitleAndMessage()
|
||||||
|
err = attachTitleMessageToCtx(title, message, paramsCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -707,7 +720,7 @@ func (engine *Engine) Do(rule *alerting.Rule) error {
|
||||||
if alertMessage == nil || alertMessage.Status == alerting.MessageStateRecovered {
|
if alertMessage == nil || alertMessage.Status == alerting.MessageStateRecovered {
|
||||||
msg := &alerting.AlertMessage{
|
msg := &alerting.AlertMessage{
|
||||||
RuleID: rule.ID,
|
RuleID: rule.ID,
|
||||||
Created: time.Now(),
|
Created: alertItem.Created,
|
||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
ID: util.GetUUID(),
|
ID: util.GetUUID(),
|
||||||
ResourceID: rule.Resource.ID,
|
ResourceID: rule.Resource.ID,
|
||||||
|
@ -756,12 +769,13 @@ func (engine *Engine) Do(rule *alerting.Rule) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// if channel is not enabled return
|
// if channel is not enabled return
|
||||||
if !rule.Channels.Enabled {
|
notifyCfg := rule.GetNotificationConfig()
|
||||||
|
if notifyCfg == nil || !notifyCfg.Enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if rule.Channels.AcceptTimeRange.Include(time.Now()) {
|
if notifyCfg.AcceptTimeRange.Include(time.Now()) {
|
||||||
periodDuration, err := time.ParseDuration(rule.Channels.ThrottlePeriod)
|
periodDuration, err := time.ParseDuration(notifyCfg.ThrottlePeriod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
alertItem.Error = err.Error()
|
alertItem.Error = err.Error()
|
||||||
return err
|
return err
|
||||||
|
@ -787,7 +801,7 @@ func (engine *Engine) Do(rule *alerting.Rule) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if alertMessage == nil || period > periodDuration {
|
if alertMessage == nil || period > periodDuration {
|
||||||
actionResults, errCount := performChannels(rule.Channels.Normal, paramsCtx)
|
actionResults, errCount := performChannels(notifyCfg.Normal, paramsCtx)
|
||||||
alertItem.ActionExecutionResults = actionResults
|
alertItem.ActionExecutionResults = actionResults
|
||||||
//change and save last notification time in local kv store when action error count equals zero
|
//change and save last notification time in local kv store when action error count equals zero
|
||||||
if errCount == 0 {
|
if errCount == 0 {
|
||||||
|
@ -798,8 +812,8 @@ func (engine *Engine) Do(rule *alerting.Rule) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rule.Channels.EscalationEnabled {
|
if notifyCfg.EscalationEnabled {
|
||||||
throttlePeriod, err := time.ParseDuration(rule.Channels.EscalationThrottlePeriod)
|
throttlePeriod, err := time.ParseDuration(notifyCfg.EscalationThrottlePeriod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -819,7 +833,7 @@ func (engine *Engine) Do(rule *alerting.Rule) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if time.Now().Sub(rule.LastEscalationTime.Local()) > periodDuration {
|
if time.Now().Sub(rule.LastEscalationTime.Local()) > periodDuration {
|
||||||
actionResults, errCount := performChannels(rule.Channels.Escalation, paramsCtx)
|
actionResults, errCount := performChannels(notifyCfg.Escalation, paramsCtx)
|
||||||
alertItem.ActionExecutionResults = actionResults
|
alertItem.ActionExecutionResults = actionResults
|
||||||
//todo init last escalation time when create task (by last alert item is escalated)
|
//todo init last escalation time when create task (by last alert item is escalated)
|
||||||
if errCount == 0 {
|
if errCount == 0 {
|
||||||
|
@ -836,17 +850,17 @@ func (engine *Engine) Do(rule *alerting.Rule) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachTitleMessageToCtx(rule *alerting.Rule, paramsCtx map[string]interface{}) error{
|
func attachTitleMessageToCtx(title, message string, paramsCtx map[string]interface{}) error{
|
||||||
var (
|
var (
|
||||||
tplBytes []byte
|
tplBytes []byte
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
tplBytes, err = resolveMessage(rule.Metrics.Message, paramsCtx)
|
tplBytes, err = common.ResolveMessage(message, paramsCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resolve message template error: %w", err)
|
return fmt.Errorf("resolve message template error: %w", err)
|
||||||
}
|
}
|
||||||
paramsCtx[alerting2.ParamMessage] = string(tplBytes)
|
paramsCtx[alerting2.ParamMessage] = string(tplBytes)
|
||||||
tplBytes, err = resolveMessage(rule.Metrics.Title, paramsCtx)
|
tplBytes, err = common.ResolveMessage(title, paramsCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resolve title template error: %w", err)
|
return fmt.Errorf("resolve title template error: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -927,7 +941,8 @@ func (engine *Engine) Test(rule *alerting.Rule) ([]alerting.ActionExecutionResul
|
||||||
alerting2.ParamEventID: util.GetUUID(),
|
alerting2.ParamEventID: util.GetUUID(),
|
||||||
alerting2.ParamTimestamp: time.Now().Unix(),
|
alerting2.ParamTimestamp: time.Now().Unix(),
|
||||||
} )
|
} )
|
||||||
err = attachTitleMessageToCtx(rule, paramsCtx)
|
title, message := rule.GetNotificationTitleAndMessage()
|
||||||
|
err = attachTitleMessageToCtx(title, message, paramsCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -945,7 +960,15 @@ func performChannels(channels []alerting.Channel, ctx map[string]interface{}) ([
|
||||||
var errCount int
|
var errCount int
|
||||||
var actionResults []alerting.ActionExecutionResult
|
var actionResults []alerting.ActionExecutionResult
|
||||||
for _, channel := range channels {
|
for _, channel := range channels {
|
||||||
resBytes, err, messageBytes := performChannel(&channel, ctx)
|
if !channel.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err := common.RetrieveChannel(&channel)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resBytes, err, messageBytes := common.PerformChannel(&channel, ctx)
|
||||||
var errStr string
|
var errStr string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCount++
|
errCount++
|
||||||
|
@ -963,46 +986,8 @@ func performChannels(channels []alerting.Channel, ctx map[string]interface{}) ([
|
||||||
return actionResults, errCount
|
return actionResults, errCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveMessage(messageTemplate string, ctx map[string]interface{}) ([]byte, error){
|
|
||||||
msg := messageTemplate
|
|
||||||
tmpl, err := template.New("alert-message").Funcs(funcs.GenericFuncMap()).Parse(msg)
|
|
||||||
if err !=nil {
|
|
||||||
return nil, fmt.Errorf("parse message temlate error: %w", err)
|
|
||||||
}
|
|
||||||
msgBuffer := &bytes.Buffer{}
|
|
||||||
err = tmpl.Execute(msgBuffer, ctx)
|
|
||||||
return msgBuffer.Bytes(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func performChannel(channel *alerting.Channel, ctx map[string]interface{}) ([]byte, error, []byte) {
|
|
||||||
var (
|
|
||||||
act action.Action
|
|
||||||
message []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
switch channel.Type {
|
|
||||||
|
|
||||||
case alerting.ChannelWebhook:
|
|
||||||
message, err = resolveMessage(channel.Webhook.Body, ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err, message
|
|
||||||
}
|
|
||||||
wh := *channel.Webhook
|
|
||||||
urlBytes, err := resolveMessage(wh.URL, ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err, message
|
|
||||||
}
|
|
||||||
wh.URL = string(urlBytes)
|
|
||||||
act = &action.WebhookAction{
|
|
||||||
Data: &wh,
|
|
||||||
Message: string(message),
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported action type: %s", channel.Type), message
|
|
||||||
}
|
|
||||||
executeResult, err := act.Execute()
|
|
||||||
return executeResult, err, message
|
|
||||||
}
|
|
||||||
func (engine *Engine) GenerateTask(rule alerting.Rule) func(ctx context.Context) {
|
func (engine *Engine) GenerateTask(rule alerting.Rule) func(ctx context.Context) {
|
||||||
return func(ctx context.Context) {
|
return func(ctx context.Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
@ -70,7 +70,7 @@ func TestEngine( t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Channels: alerting.RuleChannel{
|
Channels: alerting.NotificationConfig{
|
||||||
Normal: []alerting.Channel{
|
Normal: []alerting.Channel{
|
||||||
{Name: "钉钉", Type: alerting.ChannelWebhook, Webhook: &alerting.CustomWebhook{
|
{Name: "钉钉", Type: alerting.ChannelWebhook, Webhook: &alerting.CustomWebhook{
|
||||||
HeaderParams: map[string]string{
|
HeaderParams: map[string]string{
|
||||||
|
@ -156,7 +156,7 @@ func TestGeneratePercentilesAggQuery(t *testing.T) {
|
||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
//
|
//
|
||||||
// Channels: alerting.RuleChannel{
|
// Channels: alerting.NotificationConfig{
|
||||||
// Normal: []alerting.Channel{
|
// Normal: []alerting.Channel{
|
||||||
// {Name: "钉钉", Type: alerting.ChannelWebhook, Webhook: &alerting.CustomWebhook{
|
// {Name: "钉钉", Type: alerting.ChannelWebhook, Webhook: &alerting.CustomWebhook{
|
||||||
// HeaderParams: map[string]string{
|
// HeaderParams: map[string]string{
|
||||||
|
@ -216,7 +216,7 @@ func TestGeneratePercentilesAggQuery(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Channels: alerting.RuleChannel{
|
Channels: alerting.NotificationConfig{
|
||||||
Normal: []alerting.Channel{
|
Normal: []alerting.Channel{
|
||||||
{Name: "钉钉", Type: alerting.ChannelWebhook, Webhook: &alerting.CustomWebhook{
|
{Name: "钉钉", Type: alerting.ChannelWebhook, Webhook: &alerting.CustomWebhook{
|
||||||
HeaderParams: map[string]string{
|
HeaderParams: map[string]string{
|
||||||
|
|
Loading…
Reference in New Issue