feat(notification/telegram): take back telegram notification after being reverted in #19088

This reverts commit f67c3a14e0.
pull/19135/head
Pavel Zavora 2020-07-29 13:58:36 +02:00
parent d0a42098a0
commit 6d086f7495
18 changed files with 985 additions and 5 deletions

View File

@ -11169,6 +11169,7 @@ components:
- $ref: "#/components/schemas/SMTPNotificationRule"
- $ref: "#/components/schemas/PagerDutyNotificationRule"
- $ref: "#/components/schemas/HTTPNotificationRule"
- $ref: "#/components/schemas/TelegramNotificationRule"
discriminator:
propertyName: type
mapping:
@ -11176,6 +11177,7 @@ components:
smtp: "#/components/schemas/SMTPNotificationRule"
pagerduty: "#/components/schemas/PagerDutyNotificationRule"
http: "#/components/schemas/HTTPNotificationRule"
telegram: "#/components/schemas/TelegramNotificationRule"
NotificationRule:
allOf:
- $ref: "#/components/schemas/NotificationRuleDiscriminator"
@ -11377,6 +11379,31 @@ components:
enum: [pagerduty]
messageTemplate:
type: string
TelegramNotificationRule:
allOf:
- $ref: "#/components/schemas/NotificationRuleBase"
- $ref: "#/components/schemas/TelegramNotificationRuleBase"
TelegramNotificationRuleBase:
type: object
required: [type, messageTemplate, channel]
properties:
type:
description: The discriminator between other types of notification rules is "telegram".
type: string
enum: [telegram]
messageTemplate:
description: The message template as a flux interpolated string.
type: string
parseMode:
description: Parse mode of the message text per https://core.telegram.org/bots/api#formatting-options . Defaults to "MarkdownV2" .
type: string
enum:
- MarkdownV2
- HTML
- Markdown
disableWebPagePreview:
description: Disables preview of web links in the sent messages when "true". Defaults to "false" .
type: boolean
NotificationEndpointUpdate:
type: object
@ -11395,12 +11422,14 @@ components:
- $ref: "#/components/schemas/SlackNotificationEndpoint"
- $ref: "#/components/schemas/PagerDutyNotificationEndpoint"
- $ref: "#/components/schemas/HTTPNotificationEndpoint"
- $ref: "#/components/schemas/TelegramNotificationEndpoint"
discriminator:
propertyName: type
mapping:
slack: "#/components/schemas/SlackNotificationEndpoint"
pagerduty: "#/components/schemas/PagerDutyNotificationEndpoint"
http: "#/components/schemas/HTTPNotificationEndpoint"
telegram: "#/components/schemas/TelegramNotificationEndpoint"
NotificationEndpoint:
allOf:
- $ref: "#/components/schemas/NotificationEndpointDiscrimator"
@ -11519,9 +11548,22 @@ components:
description: Customized headers.
additionalProperties:
type: string
TelegramNotificationEndpoint:
type: object
allOf:
- $ref: "#/components/schemas/NotificationEndpointBase"
- type: object
required: [token, channel]
properties:
token:
description: Specifies the Telegram bot token. See https://core.telegram.org/bots#creating-a-new-bot .
type: string
channel:
description: ID of the telegram channel, a chat_id in https://core.telegram.org/bots/api#sendmessage .
type: string
NotificationEndpointType:
type: string
enum: ["slack", "pagerduty", "http"]
enum: ["slack", "pagerduty", "http", "telegram"]
DBRP:
required:
- orgID

View File

@ -12,12 +12,14 @@ const (
SlackType = "slack"
PagerDutyType = "pagerduty"
HTTPType = "http"
TelegramType = "telegram"
)
var typeToEndpoint = map[string]func() influxdb.NotificationEndpoint{
SlackType: func() influxdb.NotificationEndpoint { return &Slack{} },
PagerDutyType: func() influxdb.NotificationEndpoint { return &PagerDuty{} },
HTTPType: func() influxdb.NotificationEndpoint { return &HTTP{} },
TelegramType: func() influxdb.NotificationEndpoint { return &Telegram{} },
}
// UnmarshalJSON will convert the bytes to notification endpoint.

View File

@ -55,7 +55,7 @@ func TestValidEndpoint(t *testing.T) {
},
},
{
name: "empty name",
name: "empty name PagerDuty",
src: &endpoint.PagerDuty{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
@ -70,6 +70,22 @@ func TestValidEndpoint(t *testing.T) {
Msg: "Notification Endpoint Name can't be empty",
},
},
{
name: "empty name Telegram",
src: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
},
Token: influxdb.SecretField{Key: id1 + "-token"},
Channel: "-1001406363649",
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Notification Endpoint Name can't be empty",
},
},
{
name: "empty slack url",
src: &endpoint.Slack{
@ -136,6 +152,36 @@ func TestValidEndpoint(t *testing.T) {
Msg: "invalid http username/password for basic auth",
},
},
{
name: "empty telegram token",
src: &endpoint.Telegram{
Base: goodBase,
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "empty telegram bot token",
},
},
{
name: "empty telegram channel",
src: &endpoint.Telegram{
Base: goodBase,
Token: influxdb.SecretField{Key: id1 + "-token"},
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "empty telegram channel",
},
},
{
name: "valid telegram token",
src: &endpoint.Telegram{
Base: goodBase,
Token: influxdb.SecretField{Key: id1 + "-token"},
Channel: "-1001406363649",
},
err: nil,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@ -226,6 +272,22 @@ func TestJSON(t *testing.T) {
Password: influxdb.SecretField{Key: "password-key"},
},
},
{
name: "simple Telegram",
src: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "nameTelegram",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
Token: influxdb.SecretField{Key: "token-key-1"},
},
},
}
for _, c := range cases {
b, err := json.Marshal(c.src)
@ -365,6 +427,40 @@ func TestBackFill(t *testing.T) {
},
},
},
{
name: "simple Telegram",
src: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
Token: influxdb.SecretField{
Value: strPtr("token-value"),
},
},
target: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
Token: influxdb.SecretField{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
},
}
for _, c := range cases {
c.src.BackfillSecretKeys()
@ -374,6 +470,133 @@ func TestBackFill(t *testing.T) {
}
}
func TestSecretFields(t *testing.T) {
cases := []struct {
name string
src influxdb.NotificationEndpoint
secrets []influxdb.SecretField
}{
{
name: "simple Slack",
src: &endpoint.Slack{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
URL: "https://slack.com/api/chat.postMessage",
Token: influxdb.SecretField{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
secrets: []influxdb.SecretField{
{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
},
{
name: "simple pagerduty",
src: &endpoint.PagerDuty{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
ClientURL: "https://events.pagerduty.com/v2/enqueue",
RoutingKey: influxdb.SecretField{
Key: id1 + "-routing-key",
Value: strPtr("routing-key-value"),
},
},
secrets: []influxdb.SecretField{
{
Key: id1 + "-routing-key",
Value: strPtr("routing-key-value"),
},
},
},
{
name: "http with user and password",
src: &endpoint.HTTP{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
AuthMethod: "basic",
URL: "http://example.com",
Username: influxdb.SecretField{
Key: id1 + "-username",
Value: strPtr("user1"),
},
Password: influxdb.SecretField{
Key: id1 + "-password",
Value: strPtr("password1"),
},
},
secrets: []influxdb.SecretField{
{
Key: id1 + "-username",
Value: strPtr("user1"),
},
{
Key: id1 + "-password",
Value: strPtr("password1"),
},
},
},
{
name: "simple Telegram",
src: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
Token: influxdb.SecretField{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
secrets: []influxdb.SecretField{
{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
},
}
for _, c := range cases {
secretFields := c.src.SecretFields()
if diff := cmp.Diff(c.secrets, secretFields); diff != "" {
t.Errorf("failed %s, NotificationEndpoint are different -got/+want\ndiff %s", c.name, diff)
}
}
}
func strPtr(s string) *string {
ss := new(string)
*ss = s

View File

@ -0,0 +1,75 @@
package endpoint
import (
"encoding/json"
"github.com/influxdata/influxdb/v2"
)
var _ influxdb.NotificationEndpoint = &Telegram{}
const telegramTokenSuffix = "-token"
// Telegram is the notification endpoint config of telegram.
type Telegram struct {
Base
// Token is the telegram bot token, see https://core.telegram.org/bots#creating-a-new-bot
Token influxdb.SecretField `json:"token"`
// Channel is an ID of the telegram channel, see https://core.telegram.org/bots/api#sendmessage
Channel string `json:"channel"`
}
// BackfillSecretKeys fill back the secret field key during the unmarshalling
// if value of that secret field is not nil.
func (s *Telegram) BackfillSecretKeys() {
if s.Token.Key == "" && s.Token.Value != nil {
s.Token.Key = s.idStr() + telegramTokenSuffix
}
}
// SecretFields return available secret fields.
func (s Telegram) SecretFields() []influxdb.SecretField {
arr := []influxdb.SecretField{}
if s.Token.Key != "" {
arr = append(arr, s.Token)
}
return arr
}
// Valid returns error if some configuration is invalid
func (s Telegram) Valid() error {
if err := s.Base.valid(); err != nil {
return err
}
if s.Token.Key == "" {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "empty telegram bot token",
}
}
if s.Channel == "" {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "empty telegram channel",
}
}
return nil
}
// MarshalJSON implement json.Marshaler interface.
func (s Telegram) MarshalJSON() ([]byte, error) {
type telegramAlias Telegram
return json.Marshal(
struct {
telegramAlias
Type string `json:"type"`
}{
telegramAlias: telegramAlias(s),
Type: s.Type(),
})
}
// Type returns the type.
func (s Telegram) Type() string {
return TelegramType
}

View File

@ -16,6 +16,7 @@ var typeToRule = map[string](func() influxdb.NotificationRule){
"slack": func() influxdb.NotificationRule { return &Slack{} },
"pagerduty": func() influxdb.NotificationRule { return &PagerDuty{} },
"http": func() influxdb.NotificationRule { return &HTTP{} },
"telegram": func() influxdb.NotificationRule { return &Telegram{} },
}
// UnmarshalJSON will convert

View File

@ -321,6 +321,41 @@ func TestJSON(t *testing.T) {
MessageTemplate: "msg1",
},
},
{
name: "simple telegram",
src: &rule.Telegram{
Base: rule.Base{
ID: influxTesting.MustIDBase16(id1),
OwnerID: influxTesting.MustIDBase16(id2),
Name: "name1",
OrgID: influxTesting.MustIDBase16(id3),
RunbookLink: "runbooklink1",
SleepUntil: &time3,
Every: mustDuration("1h"),
TagRules: []notification.TagRule{
{
Tag: influxdb.Tag{
Key: "k1",
Value: "v1",
},
Operator: influxdb.NotEqual,
},
{
Tag: influxdb.Tag{
Key: "k2",
Value: "v2",
},
Operator: influxdb.RegexEqual,
},
},
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
MessageTemplate: "blah",
},
},
}
for _, c := range cases {
b, err := json.Marshal(c.src)

View File

@ -0,0 +1,140 @@
package rule
import (
"encoding/json"
"fmt"
"github.com/influxdata/flux/ast"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/notification/endpoint"
"github.com/influxdata/influxdb/v2/notification/flux"
)
// Telegram is the notification rule config of telegram.
type Telegram struct {
Base
MessageTemplate string `json:"messageTemplate"`
ParseMode string `json:"parseMode"`
DisableWebPagePreview bool `json:"disableWebPagePreview"`
}
// GenerateFlux generates a flux script for the telegram notification rule.
func (s *Telegram) GenerateFlux(e influxdb.NotificationEndpoint) (string, error) {
telegramEndpoint, ok := e.(*endpoint.Telegram)
if !ok {
return "", fmt.Errorf("endpoint provided is a %s, not a Telegram endpoint", e.Type())
}
p, err := s.GenerateFluxAST(telegramEndpoint)
if err != nil {
return "", err
}
return ast.Format(p), nil
}
// GenerateFluxAST generates a flux AST for the telegram notification rule.
func (s *Telegram) GenerateFluxAST(e *endpoint.Telegram) (*ast.Package, error) {
f := flux.File(
s.Name,
flux.Imports("influxdata/influxdb/monitor", "contrib/sranka/telegram", "influxdata/influxdb/secrets", "experimental"),
s.generateFluxASTBody(e),
)
return &ast.Package{Package: "main", Files: []*ast.File{f}}, nil
}
func (s *Telegram) generateFluxASTBody(e *endpoint.Telegram) []ast.Statement {
var statements []ast.Statement
statements = append(statements, s.generateTaskOption())
if e.Token.Key != "" {
statements = append(statements, s.generateFluxASTSecrets(e))
}
statements = append(statements, s.generateFluxASTEndpoint(e))
statements = append(statements, s.generateFluxASTNotificationDefinition(e))
statements = append(statements, s.generateFluxASTStatuses())
statements = append(statements, s.generateLevelChecks()...)
statements = append(statements, s.generateFluxASTNotifyPipe(e))
return statements
}
func (s *Telegram) generateFluxASTSecrets(e *endpoint.Telegram) ast.Statement {
call := flux.Call(flux.Member("secrets", "get"), flux.Object(flux.Property("key", flux.String(e.Token.Key))))
return flux.DefineVariable("telegram_secret", call)
}
func (s *Telegram) generateFluxASTEndpoint(e *endpoint.Telegram) ast.Statement {
props := []*ast.Property{}
if e.Token.Key != "" {
props = append(props, flux.Property("token", flux.Identifier("telegram_secret")))
}
if s.ParseMode != "" {
props = append(props, flux.Property("parseMode", flux.String(s.ParseMode)))
}
props = append(props, flux.Property("disableWebPagePreview", flux.Bool(s.DisableWebPagePreview)))
call := flux.Call(flux.Member("telegram", "endpoint"), flux.Object(props...))
return flux.DefineVariable("telegram_endpoint", call)
}
func (s *Telegram) generateFluxASTNotifyPipe(e *endpoint.Telegram) ast.Statement {
endpointProps := []*ast.Property{}
endpointProps = append(endpointProps, flux.Property("channel", flux.String(e.Channel)))
endpointProps = append(endpointProps, flux.Property("text", flux.String(s.MessageTemplate)))
endpointProps = append(endpointProps, flux.Property("silent", s.generateSilent()))
endpointFn := flux.Function(flux.FunctionParams("r"), flux.Object(endpointProps...))
props := []*ast.Property{}
props = append(props, flux.Property("data", flux.Identifier("notification")))
props = append(props, flux.Property("endpoint",
flux.Call(flux.Identifier("telegram_endpoint"), flux.Object(flux.Property("mapFn", endpointFn)))))
call := flux.Call(flux.Member("monitor", "notify"), flux.Object(props...))
return flux.ExpressionStatement(flux.Pipe(flux.Identifier("all_statuses"), call))
}
func (s *Telegram) generateSilent() ast.Expression {
level := flux.Member("r", "_level")
return flux.If(
flux.Equal(level, flux.String("crit")),
flux.Bool(true),
flux.If(
flux.Equal(level, flux.String("warn")),
flux.Bool(true),
flux.Bool(false),
),
)
}
type telegramAlias Telegram
// MarshalJSON implement json.Marshaler interface.
func (s Telegram) MarshalJSON() ([]byte, error) {
return json.Marshal(
struct {
telegramAlias
Type string `json:"type"`
}{
telegramAlias: telegramAlias(s),
Type: s.Type(),
})
}
// Valid returns where the config is valid.
func (s Telegram) Valid() error {
if err := s.Base.valid(); err != nil {
return err
}
if s.MessageTemplate == "" {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Telegram MessageTemplate is invalid",
}
}
return nil
}
// Type returns the type of the rule config.
func (s Telegram) Type() string {
return "telegram"
}

View File

@ -0,0 +1,308 @@
package rule_test
import (
"testing"
"github.com/andreyvit/diff"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/notification"
"github.com/influxdata/influxdb/v2/notification/endpoint"
"github.com/influxdata/influxdb/v2/notification/rule"
influxTesting "github.com/influxdata/influxdb/v2/testing"
)
var _ influxdb.NotificationRule = &rule.Telegram{}
func TestTelegram_GenerateFlux(t *testing.T) {
tests := []struct {
name string
rule *rule.Telegram
endpoint influxdb.NotificationEndpoint
script string
}{
{
name: "incompatible with endpoint",
endpoint: &endpoint.Slack{
Base: endpoint.Base{
ID: idPtr(3),
Name: "foo",
},
URL: "http://whatever",
},
rule: &rule.Telegram{
MessageTemplate: "blah",
Base: rule.Base{
ID: 1,
EndpointID: 3,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{
{
Tag: influxdb.Tag{
Key: "foo",
Value: "bar",
},
Operator: influxdb.Equal,
},
{
Tag: influxdb.Tag{
Key: "baz",
Value: "bang",
},
Operator: influxdb.Equal,
},
},
},
},
script: "", //no script generater, because of incompatible endpoint
},
{
name: "notify on crit",
endpoint: &endpoint.Telegram{
Base: endpoint.Base{
ID: idPtr(3),
Name: "foo",
},
Token: influxdb.SecretField{Key: "3-key"},
Channel: "-12345",
},
rule: &rule.Telegram{
MessageTemplate: "blah",
Base: rule.Base{
ID: 1,
EndpointID: 3,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{
{
Tag: influxdb.Tag{
Key: "foo",
Value: "bar",
},
Operator: influxdb.Equal,
},
{
Tag: influxdb.Tag{
Key: "baz",
Value: "bang",
},
Operator: influxdb.Equal,
},
},
},
},
script: `package main
// foo
import "influxdata/influxdb/monitor"
import "contrib/sranka/telegram"
import "influxdata/influxdb/secrets"
import "experimental"
option task = {name: "foo", every: 1h}
telegram_secret = secrets["get"](key: "3-key")
telegram_endpoint = telegram["endpoint"](token: telegram_secret, disableWebPagePreview: false)
notification = {
_notification_rule_id: "0000000000000001",
_notification_rule_name: "foo",
_notification_endpoint_id: "0000000000000003",
_notification_endpoint_name: "foo",
}
statuses = monitor["from"](start: -2h, fn: (r) =>
(r["foo"] == "bar" and r["baz"] == "bang"))
crit = statuses
|> filter(fn: (r) =>
(r["_level"] == "crit"))
all_statuses = crit
|> filter(fn: (r) =>
(r["_time"] > experimental["subDuration"](from: now(), d: 1h)))
all_statuses
|> monitor["notify"](data: notification, endpoint: telegram_endpoint(mapFn: (r) =>
({channel: "-12345", text: "blah", silent: if r["_level"] == "crit" then true else if r["_level"] == "warn" then true else false})))`,
},
{
name: "with DisableWebPagePreview and ParseMode",
endpoint: &endpoint.Telegram{
Base: endpoint.Base{
ID: idPtr(3),
Name: "foo",
},
Token: influxdb.SecretField{Key: "3-key"},
Channel: "-12345",
},
rule: &rule.Telegram{
MessageTemplate: "blah",
DisableWebPagePreview: true,
ParseMode: "HTML",
Base: rule.Base{
ID: 1,
EndpointID: 3,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Any,
},
},
TagRules: []notification.TagRule{
{
Tag: influxdb.Tag{
Key: "foo",
Value: "bar",
},
Operator: influxdb.Equal,
},
{
Tag: influxdb.Tag{
Key: "baz",
Value: "bang",
},
Operator: influxdb.Equal,
},
},
},
},
script: `package main
// foo
import "influxdata/influxdb/monitor"
import "contrib/sranka/telegram"
import "influxdata/influxdb/secrets"
import "experimental"
option task = {name: "foo", every: 1h}
telegram_secret = secrets["get"](key: "3-key")
telegram_endpoint = telegram["endpoint"](token: telegram_secret, parseMode: "HTML", disableWebPagePreview: true)
notification = {
_notification_rule_id: "0000000000000001",
_notification_rule_name: "foo",
_notification_endpoint_id: "0000000000000003",
_notification_endpoint_name: "foo",
}
statuses = monitor["from"](start: -2h, fn: (r) =>
(r["foo"] == "bar" and r["baz"] == "bang"))
any = statuses
|> filter(fn: (r) =>
(true))
all_statuses = any
|> filter(fn: (r) =>
(r["_time"] > experimental["subDuration"](from: now(), d: 1h)))
all_statuses
|> monitor["notify"](data: notification, endpoint: telegram_endpoint(mapFn: (r) =>
({channel: "-12345", text: "blah", silent: if r["_level"] == "crit" then true else if r["_level"] == "warn" then true else false})))`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
script, err := tt.rule.GenerateFlux(tt.endpoint)
if err != nil {
if script != "" {
t.Errorf("Failed to generate flux: %v", err)
}
return
}
if got, want := script, tt.script; got != want {
t.Errorf("\n\nStrings do not match:\n\n%s", diff.LineDiff(got, want))
}
})
}
}
func TestTelegram_Valid(t *testing.T) {
cases := []struct {
name string
rule *rule.Telegram
err error
}{
{
name: "valid template",
rule: &rule.Telegram{
MessageTemplate: "blah",
Base: rule.Base{
ID: 1,
EndpointID: 3,
OwnerID: 4,
OrgID: 5,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{},
},
},
err: nil,
},
{
name: "missing MessageTemplate",
rule: &rule.Telegram{
MessageTemplate: "",
Base: rule.Base{
ID: 1,
EndpointID: 3,
OwnerID: 4,
OrgID: 5,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{},
},
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Telegram MessageTemplate is invalid",
},
},
{
name: "missing EndpointID",
rule: &rule.Telegram{
MessageTemplate: "",
Base: rule.Base{
ID: 1,
// EndpointID: 3,
OwnerID: 4,
OrgID: 5,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{},
},
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Notification Rule EndpointID is invalid",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := c.rule.Valid()
influxTesting.ErrorsEqual(t, got, c.err)
})
}
}

View File

@ -57,7 +57,7 @@ const EmptyEndpointList: FC<{searchTerm: string}> = ({searchTerm}) => {
<EmptyState.Text>
Want to send notifications to Slack,
<br />
PagerDuty or an HTTP server?
PagerDuty, Telegram or an HTTP server?
<br />
<br />
Try creating a <b>Notification Endpoint</b>

View File

@ -5,6 +5,7 @@ import React, {FC, ChangeEvent} from 'react'
import EndpointOptionsSlack from './EndpointOptionsSlack'
import EndpointOptionsPagerDuty from './EndpointOptionsPagerDuty'
import EndpointOptionsHTTP from './EndpointOptionsHTTP'
import EndpointOptionsTelegram from './EndpointOptionsTelegram'
// Types
import {
@ -12,6 +13,7 @@ import {
SlackNotificationEndpoint,
PagerDutyNotificationEndpoint,
HTTPNotificationEndpoint,
TelegramNotificationEndpoint,
} from 'src/types'
interface Props {
@ -40,6 +42,16 @@ const EndpointOptions: FC<Props> = ({
/>
)
}
case 'telegram': {
const {token, channel} = endpoint as TelegramNotificationEndpoint
return (
<EndpointOptionsTelegram
token={token}
channel={channel}
onChange={onChange}
/>
)
}
case 'http': {
const {
url,

View File

@ -0,0 +1,56 @@
// Libraries
import React, {FC, ChangeEvent} from 'react'
// Components
import {
Input,
InputType,
FormElement,
Panel,
Grid,
Columns,
} from '@influxdata/clockface'
interface Props {
token: string
channel: string
onChange: (e: ChangeEvent<HTMLInputElement>) => void
}
const EndpointOptionsTelegram: FC<Props> = ({token, channel, onChange}) => {
return (
<Panel>
<Panel.Header>
<h4>Telegram Options</h4>
</Panel.Header>
<Panel.Body>
<Grid>
<Grid.Row>
<Grid.Column widthXS={Columns.Twelve}>
<FormElement label="Bot Token">
<Input
name="token"
value={token}
testID="token"
onChange={onChange}
type={InputType.Password}
/>
</FormElement>
<FormElement label="Chat ID">
<Input
name="channel"
value={channel}
testID="channel"
onChange={onChange}
type={InputType.Text}
/>
</FormElement>
</Grid.Column>
</Grid.Row>
</Grid>
</Panel.Body>
</Panel>
)
}
export default EndpointOptionsTelegram

View File

@ -59,6 +59,13 @@ export const reducer = (
url: DEFAULT_ENDPOINT_URLS.slack,
token: '',
}
case 'telegram':
return {
...baseProps,
type: 'telegram',
token: '',
channel: '',
}
}
}
return state

View File

@ -32,6 +32,7 @@ const types: EndpointType[] = [
{name: 'HTTP', type: 'http', id: 'http'},
{name: 'Slack', type: 'slack', id: 'slack'},
{name: 'Pagerduty', type: 'pagerduty', id: 'pagerduty'},
{name: 'Telegram', type: 'telegram', id: 'telegram'},
]
const EndpointTypeDropdown: FC<Props> = ({

View File

@ -33,7 +33,7 @@ const EndpointsColumn: FC<Props> = ({history, match, endpoints, tabIndex}) => {
<br />
to a third party service that can receive notifications
<br />
like Slack, PagerDuty, or an HTTP server
like Slack, PagerDuty, Telegram, or an HTTP server
<br />
<br />
<a

View File

@ -5,6 +5,7 @@ import React, {FC} from 'react'
import SlackMessage from './SlackMessage'
import SMTPMessage from './SMTPMessage'
import PagerDutyMessage from './PagerDutyMessage'
import TelegramMessage from './TelegramMessage'
// Utils
import {useRuleDispatch} from './RuleOverlayProvider'
@ -61,6 +62,15 @@ const RuleMessageContents: FC<Props> = ({rule}) => {
/>
)
}
case 'telegram': {
const {messageTemplate} = rule
return (
<TelegramMessage
messageTemplate={messageTemplate}
onChange={onChange}
/>
)
}
case 'http': {
return <></>
}

View File

@ -0,0 +1,41 @@
// Libraries
import React, {FC, ChangeEvent} from 'react'
// Components
import {Form, TextArea} from '@influxdata/clockface'
import {TelegramNotificationRuleBase} from 'src/types/alerting'
interface EventHandlers {
onChange: (e: ChangeEvent) => void
}
type Props = Omit<TelegramNotificationRuleBase, 'type'> & EventHandlers
const TelegramMessage: FC<Props> = ({messageTemplate, onChange}) => {
return (
<Form.Element label="Message Template">
<TextArea
name="messageTemplate"
testID="slack-message-template--textarea"
value={messageTemplate}
onChange={onChange}
rows={3}
/>
</Form.Element>
/*
// keep it simple, the following elements are possible, but too advanced
<Form.Element label="Parse Mode">
<Input value={parseMode} name="parseMode" onChange={onChange} />
</Form.Element>
<Form.Element label="">
<Input
value={String(!disableWebPagePreview)}
name="disableWebPagePreview"
onChange={onChange}
type={InputType.Checkbox}
/>
</Form.Element>
*/
)
}
export default TelegramMessage

View File

@ -8,6 +8,7 @@ import {
SlackNotificationRuleBase,
SMTPNotificationRuleBase,
PagerDutyNotificationRuleBase,
TelegramNotificationRuleBase,
NotificationEndpoint,
NotificationRuleDraft,
HTTPNotificationRuleBase,
@ -22,6 +23,7 @@ type RuleVariantFields =
| SMTPNotificationRuleBase
| PagerDutyNotificationRuleBase
| HTTPNotificationRuleBase
| TelegramNotificationRuleBase
const defaultMessage =
'Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }'
@ -45,6 +47,20 @@ export const getRuleVariantDefaults = (
return {type: 'http', url: ''}
}
case 'telegram': {
// wrap all variable values into `` to prevent telegram's markdown errors
const messageTemplate = defaultMessage.replace(
/\$\{[^\}]+\}/g,
x => `\`${x}\``
)
return {
messageTemplate: messageTemplate,
parseMode: 'MarkdownV2',
disableWebPagePreview: false,
type: 'telegram',
}
}
default: {
throw new Error(`Could not find NotificationEndpoint with id "${id}"`)
}

View File

@ -19,6 +19,8 @@ import {
Threshold,
CheckBase as GenCheckBase,
NotificationEndpointBase as GenEndpointBase,
TelegramNotificationRuleBase,
TelegramNotificationEndpoint,
} from 'src/client'
import {RemoteDataState} from 'src/types'
@ -50,6 +52,8 @@ export type NotificationEndpoint =
| (Omit<PagerDutyNotificationEndpoint, 'status' | 'labels'> &
EndpointOverrides)
| (Omit<HTTPNotificationEndpoint, 'status' | 'labels'> & EndpointOverrides)
| (Omit<TelegramNotificationEndpoint, 'status' | 'labels'> &
EndpointOverrides)
export type NotificationEndpointBase = Omit<GenEndpointBase, 'labels'> &
EndpointOverrides
@ -76,7 +80,7 @@ export type NotificationRuleBaseDraft = Overwrite<
}
>
type RuleDraft = SlackRule | SMTPRule | PagerDutyRule | HTTPRule
type RuleDraft = SlackRule | SMTPRule | PagerDutyRule | HTTPRule | TelegramRule
export type NotificationRuleDraft = RuleDraft
@ -96,6 +100,10 @@ type HTTPRule = NotificationRuleBaseDraft &
HTTPNotificationRuleBase &
RuleOverrides
type TelegramRule = NotificationRuleBaseDraft &
TelegramNotificationRuleBase &
RuleOverrides
export type LowercaseCheckStatusLevel =
| 'crit'
| 'warn'
@ -189,4 +197,7 @@ export {
PostNotificationRule,
CheckPatch,
TaskStatusType,
TelegramNotificationEndpoint,
TelegramNotificationRuleBase,
TelegramNotificationRule,
} from '../client'