Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

notification: add alert summary to status updates #2074

Merged
merged 16 commits into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,12 @@ require (
github.com/google/uuid v1.3.0
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8
github.com/gorilla/pat v1.0.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // pinned version - see https://github.com/target/goalert/issues/1239
github.com/ian-kent/envconf v0.0.0-20141026121121-c19809918c02 // indirect
github.com/ian-kent/go-log v0.0.0-20160113211217-5731446c36ab // indirect
github.com/ian-kent/goose v0.0.0-20141221090059-c3541ea826ad // indirect
github.com/ian-kent/linkio v0.0.0-20170807205755-97566b872887 // indirect
github.com/jackc/pgconn v1.10.1
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
github.com/jackc/pgtype v1.9.1
github.com/jackc/pgx/v4 v4.14.1
github.com/jmespath/go-jmespath v0.4.0
Expand All @@ -46,22 +44,17 @@ require (
github.com/mailhog/smtp v1.0.1 // indirect
github.com/mailhog/storage v1.0.1
github.com/matcornic/hermes/v2 v2.1.0
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/ogier/pflag v0.0.1 // indirect
github.com/pelletier/go-toml v1.9.4
github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/prometheus/client_golang v1.11.0
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rubenv/sql-migrate v1.0.0
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/slack-go/slack v0.10.1
github.com/spf13/afero v1.7.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.3.0
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/t-k/fluent-logger-golang v1.0.0 // indirect
Expand All @@ -84,10 +77,8 @@ require (
google.golang.org/protobuf v1.27.1
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
honnef.co/go/tools v0.2.2
)

Expand Down Expand Up @@ -125,29 +116,36 @@ require (
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/mux v1.7.3 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/puddle v1.2.0 // indirect
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/subosito/gotenv v1.2.0 // indirect
Expand All @@ -160,5 +158,7 @@ require (
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/api v0.63.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
170 changes: 135 additions & 35 deletions notification/twilio/alertsms.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package twilio
import (
"bytes"
"context"
"errors"
"strings"
"text/template"
"unicode"

"github.com/pkg/errors"
"github.com/target/goalert/config"
"github.com/target/goalert/notification"
)

// 160 GSM characters (140 bytes) is the max for a single segment message.
Expand All @@ -19,31 +20,26 @@ import (
// then be 70 or 67 characters for single or multi-segmented messages, respectively.
const maxGSMLen = 160

type alertSMS struct {
ID int
Count int
Body string
Link string
Code int
}

var smsTmpl = template.Must(template.New("alertSMS").Parse(`
{{- if .ID}}Alert #{{.ID}}: {{.Body}}
{{- else if .Count}}Svc '{{.Body}}': {{.Count}} unacked alert{{if gt .Count 1}}s{{end}}
{{- end}}
var alertTempl = template.Must(template.New("alertSMS").Parse(`Alert #{{.AlertID}}: {{.Summary}}
{{- if .Link }}

{{.Link}}
{{- end}}
{{- if and .Count .ID }}
{{.Link}}{{end}}
{{- if .Code}}

Reply '{{.Code}}a' to ack, '{{.Code}}c' to close.{{end}}`))

var bundleTempl = template.Must(template.New("alertBundleSMS").Parse(`Svc '{{.ServiceName}}': {{.Count}} unacked alert{{if gt .Count 1}}s{{end}}

{{.Count}} other alert{{if gt .Count 1}}s have{{else}} has{{end}} been updated.
{{- end}}
{{- if .Link }}

{{.Link}}
{{end}}
{{- if .Code}}
Reply '{{.Code}}aa' to ack all, '{{.Code}}cc' to close all.{{end}}`))

Reply '{{.Code}}a{{if .Count}}a{{end}}' to ack{{if .Count}} all{{end}}, '{{.Code}}c{{if .Count}}c{{end}}' to close{{if .Count}} all{{end}}.
{{- end}}`,
))
var statusTempl = template.Must(template.New("alertStatusSMS").Parse(`Alert #{{.AlertID}}{{- if .Summary }}: {{.Summary}}{{end}}

{{.LogEntry}}`))

const gsmAlphabet = "@∆ 0¡P¿p£!1AQaq$Φ\"2BRbr¥Γ#3CScsèΛ¤4DTdtéΩ%5EUeuùΠ&6FVfvìΨ'7GWgwòΣ(8HXhxÇΘ)9IYiy\n Ξ *:JZjzØ+;KÄkäøÆ,<LÖlö\ræ-=MÑmñÅß.>NÜnüåÉ/?O§oà"

Expand Down Expand Up @@ -103,33 +99,137 @@ func hasTwoWaySMSSupport(ctx context.Context, number string) bool {
return !strings.HasPrefix(number, "+91")
}

// Render will render a single-segment SMS.
func normalizeGSM(str string) (s string) {
s = strings.Map(mapGSM, str)
s = strings.Replace(s, " ", " ", -1)
s = strings.TrimSpace(s)
return s
}

// trimString will trim the string by the difference between the maxLen and the
// buffer length. If the string is trimmed, it returns true and the buffer is reset.
func trimString(str *string, buf *bytes.Buffer, maxLen int) bool {
if buf.Len() <= maxLen {
return false
}

newLen := len(*str) - (buf.Len() - maxLen)
if newLen <= 0 {
*str = ""
} else {
*str = strings.TrimSpace((*str)[:newLen])
}
buf.Reset()

return true
}

// renderAlertMessage will render a single-segment SMS for an Alert.
//
// Non-GSM characters will be replaced with '?' and Body will be
// Non-GSM characters will be replaced with '?' and fields will be
// truncated (if needed) until the output is <= maxLen characters.
func (a alertSMS) Render(maxLen int) (string, error) {
a.Body = strings.Map(mapGSM, a.Body)
a.Body = strings.Replace(a.Body, " ", " ", -1)
a.Body = strings.TrimSpace(a.Body)

func renderAlertMessage(maxLen int, a notification.Alert, link string, code int) (string, error) {
var buf bytes.Buffer
err := smsTmpl.Execute(&buf, a)
a.Summary = normalizeGSM(a.Summary)

var data struct {
notification.Alert
Link string
Code int
}
data.Alert = a
data.Link = link
data.Code = code

err := alertTempl.Execute(&buf, data)
if err != nil {
return "", err
}

if trimString(&data.Alert.Summary, &buf, maxLen) {
err = alertTempl.Execute(&buf, data)
if err != nil {
return "", err
}
}

// should maybe revisit templates if this starts occurring
if buf.Len() > maxLen {
newBodyLen := len(a.Body) - (buf.Len() - maxLen)
if newBodyLen <= 0 {
return "", errors.New("message too long to include body")
return "", errors.New("message too long")
}

return buf.String(), nil
}

// renderAlertStatusMessage will render a single-segment SMS for an Alert Status.
//
// Non-GSM characters will be replaced with '?' and fields will be
// truncated (if needed) until the output is <= maxLen characters.
func renderAlertStatusMessage(maxLen int, a notification.AlertStatus) (string, error) {
var buf bytes.Buffer
a.Summary = normalizeGSM(a.Summary)
a.LogEntry = normalizeGSM(a.LogEntry)

err := statusTempl.Execute(&buf, a)
if err != nil {
return "", err
}

if trimString(&a.Summary, &buf, maxLen) {
err = statusTempl.Execute(&buf, a)
if err != nil {
return "", err
}
a.Body = strings.TrimSpace(a.Body[:newBodyLen])
buf.Reset()
err = smsTmpl.Execute(&buf, a)
}

if trimString(&a.LogEntry, &buf, maxLen) {
err = statusTempl.Execute(&buf, a)
if err != nil {
return "", err
}
}

// should maybe revisit templates if this starts occurring
if buf.Len() > maxLen {
return "", errors.New("message too long")
}

return buf.String(), nil
}

// renderAlertBundleMessage will render a single-segment SMS for an Alert Bundle.
//
// Non-GSM characters will be replaced with '?' and fields will be
// truncated (if needed) until the output is <= maxLen characters.
func renderAlertBundleMessage(maxLen int, a notification.AlertBundle, link string, code int) (string, error) {
var buf bytes.Buffer
a.ServiceName = normalizeGSM(a.ServiceName)

var data struct {
notification.AlertBundle
Link string
Code int
}
data.AlertBundle = a
data.Link = link
data.Code = code

err := bundleTempl.Execute(&buf, data)
if err != nil {
return "", err
}

if trimString(&data.AlertBundle.ServiceName, &buf, maxLen) {
err = bundleTempl.Execute(&buf, data)
if err != nil {
return "", err
}
}

// should maybe revisit templates if this starts occurring
if buf.Len() > maxLen {
return "", errors.New("message too long")
}

return buf.String(), nil
}
Loading