The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/api/compat_contact_points.go

446 lines
15 KiB

package api
import (
"encoding/json"
"errors"
"fmt"
"strings"
"unsafe"
"github.com/grafana/alerting/notify"
"github.com/grafana/alerting/receivers"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/util"
)
// ContactPointFromContactPointExport parses the database model of the contact point (group of integrations) where settings are represented in JSON,
// to strongly typed ContactPoint.
func ContactPointFromContactPointExport(rawContactPoint definitions.ContactPointExport) (definitions.ContactPoint, error) {
j := jsoniter.ConfigCompatibleWithStandardLibrary
j.RegisterExtension(&contactPointsExtension{})
contactPoint := definitions.ContactPoint{
Name: rawContactPoint.Name,
}
var errs []error
for _, rawIntegration := range rawContactPoint.Receivers {
err := parseIntegration(j, &contactPoint, rawIntegration.Type, rawIntegration.DisableResolveMessage, json.RawMessage(rawIntegration.Settings))
if err != nil {
// accumulate errors to report all at once.
errs = append(errs, fmt.Errorf("failed to parse %s integration (uid:%s): %w", rawIntegration.Type, rawIntegration.UID, err))
}
}
return contactPoint, errors.Join(errs...)
}
// ContactPointToContactPointExport converts definitions.ContactPoint to notify.APIReceiver.
// It uses special extension for json-iterator API that properly handles marshalling of some specific fields.
//
//nolint:gocyclo
func ContactPointToContactPointExport(cp definitions.ContactPoint) (notify.APIReceiver, error) {
j := jsoniter.ConfigCompatibleWithStandardLibrary
// use json iterator with custom extension that has special codec for some field.
// This is needed to keep the API models clean and convert from database model
j.RegisterExtension(&contactPointsExtension{})
var integration []*notify.GrafanaIntegrationConfig
var errs []error
for _, i := range cp.Alertmanager {
el, err := marshallIntegration(j, "prometheus-alertmanager", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Dingding {
el, err := marshallIntegration(j, "dingding", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Discord {
el, err := marshallIntegration(j, "discord", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Email {
el, err := marshallIntegration(j, "email", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Googlechat {
el, err := marshallIntegration(j, "googlechat", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Kafka {
el, err := marshallIntegration(j, "kafka", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Line {
el, err := marshallIntegration(j, "line", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Opsgenie {
el, err := marshallIntegration(j, "opsgenie", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Pagerduty {
el, err := marshallIntegration(j, "pagerduty", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.OnCall {
el, err := marshallIntegration(j, "oncall", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Pushover {
el, err := marshallIntegration(j, "pushover", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Sensugo {
el, err := marshallIntegration(j, "sensugo", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Slack {
el, err := marshallIntegration(j, "slack", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Teams {
el, err := marshallIntegration(j, "teams", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Telegram {
el, err := marshallIntegration(j, "telegram", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Threema {
el, err := marshallIntegration(j, "threema", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Victorops {
el, err := marshallIntegration(j, "victorops", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Webhook {
el, err := marshallIntegration(j, "webhook", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Wecom {
el, err := marshallIntegration(j, "wecom", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
for _, i := range cp.Webex {
el, err := marshallIntegration(j, "webex", i, i.DisableResolveMessage)
integration = append(integration, el)
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return notify.APIReceiver{}, errors.Join(errs...)
}
contactPoint := notify.APIReceiver{
ConfigReceiver: notify.ConfigReceiver{Name: cp.Name},
GrafanaIntegrations: notify.GrafanaIntegrations{Integrations: integration},
}
return contactPoint, nil
}
// marshallIntegration converts the API model integration to the storage model that contains settings in the JSON format.
// The secret fields are not encrypted.
func marshallIntegration(json jsoniter.API, integrationType string, integration interface{}, disableResolveMessage *bool) (*notify.GrafanaIntegrationConfig, error) {
data, err := json.Marshal(integration)
if err != nil {
return nil, fmt.Errorf("failed to marshall integration '%s' to JSON: %w", integrationType, err)
}
e := &notify.GrafanaIntegrationConfig{
Type: integrationType,
Settings: data,
}
if disableResolveMessage != nil {
e.DisableResolveMessage = *disableResolveMessage
}
return e, nil
}
//nolint:gocyclo
func parseIntegration(json jsoniter.API, result *definitions.ContactPoint, receiverType string, disableResolveMessage bool, data json.RawMessage) error {
var err error
var disable *bool
if disableResolveMessage { // populate only if true
disable = util.Pointer(disableResolveMessage)
}
switch strings.ToLower(receiverType) {
case "prometheus-alertmanager":
integration := definitions.AlertmanagerIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Alertmanager = append(result.Alertmanager, integration)
}
case "dingding":
integration := definitions.DingdingIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Dingding = append(result.Dingding, integration)
}
case "discord":
integration := definitions.DiscordIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Discord = append(result.Discord, integration)
}
case "email":
integration := definitions.EmailIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Email = append(result.Email, integration)
}
case "googlechat":
integration := definitions.GooglechatIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Googlechat = append(result.Googlechat, integration)
}
case "kafka":
integration := definitions.KafkaIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Kafka = append(result.Kafka, integration)
}
case "line":
integration := definitions.LineIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Line = append(result.Line, integration)
}
case "opsgenie":
integration := definitions.OpsgenieIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Opsgenie = append(result.Opsgenie, integration)
}
case "pagerduty":
integration := definitions.PagerdutyIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Pagerduty = append(result.Pagerduty, integration)
}
case "oncall":
integration := definitions.OnCallIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.OnCall = append(result.OnCall, integration)
}
case "pushover":
integration := definitions.PushoverIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Pushover = append(result.Pushover, integration)
}
case "sensugo":
integration := definitions.SensugoIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Sensugo = append(result.Sensugo, integration)
}
case "slack":
integration := definitions.SlackIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Slack = append(result.Slack, integration)
}
case "teams":
integration := definitions.TeamsIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Teams = append(result.Teams, integration)
}
case "telegram":
integration := definitions.TelegramIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Telegram = append(result.Telegram, integration)
}
case "threema":
integration := definitions.ThreemaIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Threema = append(result.Threema, integration)
}
case "victorops":
integration := definitions.VictoropsIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Victorops = append(result.Victorops, integration)
}
case "webhook":
integration := definitions.WebhookIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Webhook = append(result.Webhook, integration)
}
case "wecom":
integration := definitions.WecomIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Wecom = append(result.Wecom, integration)
}
case "webex":
integration := definitions.WebexIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Webex = append(result.Webex, integration)
}
default:
err = fmt.Errorf("integration %s is not supported", receiverType)
}
return err
}
// contactPointsExtension extends jsoniter with special codecs for some integrations' fields that are encoded differently in the legacy configuration.
type contactPointsExtension struct {
jsoniter.DummyExtension
}
func (c contactPointsExtension) UpdateStructDescriptor(structDescriptor *jsoniter.StructDescriptor) {
if structDescriptor.Type == reflect2.TypeOf(definitions.EmailIntegration{}) {
bind := structDescriptor.GetField("Addresses")
codec := &emailAddressCodec{}
bind.Decoder = codec
bind.Encoder = codec
}
if structDescriptor.Type == reflect2.TypeOf(definitions.PushoverIntegration{}) {
codec := &numberAsStringCodec{}
for _, field := range []string{"AlertingPriority", "OKPriority"} {
desc := structDescriptor.GetField(field)
desc.Decoder = codec
desc.Encoder = codec
}
// the same logic is in the pushover.NewConfig in alerting module
codec = &numberAsStringCodec{ignoreError: true}
for _, field := range []string{"Retry", "Expire"} {
desc := structDescriptor.GetField(field)
desc.Decoder = codec
desc.Encoder = codec
}
}
if structDescriptor.Type == reflect2.TypeOf(definitions.WebhookIntegration{}) {
codec := &numberAsStringCodec{ignoreError: true}
desc := structDescriptor.GetField("MaxAlerts")
desc.Decoder = codec
desc.Encoder = codec
}
if structDescriptor.Type == reflect2.TypeOf(definitions.OnCallIntegration{}) {
codec := &numberAsStringCodec{ignoreError: true}
desc := structDescriptor.GetField("MaxAlerts")
desc.Decoder = codec
desc.Encoder = codec
}
}
type emailAddressCodec struct{}
func (d *emailAddressCodec) IsEmpty(ptr unsafe.Pointer) bool {
f := *(*[]string)(ptr)
return len(f) == 0
}
func (d *emailAddressCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
f := *(*[]string)(ptr)
addresses := strings.Join(f, ";")
stream.WriteString(addresses)
}
func (d *emailAddressCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
s := iter.ReadString()
emails := strings.FieldsFunc(strings.Trim(s, "\""), func(r rune) bool {
switch r {
case ',', ';', '\n':
return true
}
return false
})
*((*[]string)(ptr)) = emails
}
// converts a string representation of a number to *int64
type numberAsStringCodec struct {
ignoreError bool // if true, then ignores the error and keeps value nil
}
func (d *numberAsStringCodec) IsEmpty(ptr unsafe.Pointer) bool {
return *((*(*int))(ptr)) == nil
}
func (d *numberAsStringCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
val := *((*(*int))(ptr))
if val == nil {
stream.WriteNil()
return
}
stream.WriteInt(*val)
}
func (d *numberAsStringCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
valueType := iter.WhatIsNext()
var value int64
switch valueType {
case jsoniter.NumberValue:
value = iter.ReadInt64()
case jsoniter.StringValue:
var num receivers.OptionalNumber
err := num.UnmarshalJSON(iter.ReadStringAsSlice())
if err != nil {
iter.ReportError("numberAsStringCodec", fmt.Sprintf("failed to unmarshall string as OptionalNumber: %s", err.Error()))
}
if num.String() == "" {
return
}
value, err = num.Int64()
if err != nil {
if !d.ignoreError {
iter.ReportError("numberAsStringCodec", fmt.Sprintf("string does not represent an integer number: %s", err.Error()))
}
return
}
case jsoniter.NilValue:
iter.ReadNil()
return
default:
iter.ReportError("numberAsStringCodec", "not number or string")
}
*((*(*int64))(ptr)) = &value
}