diff --git a/pkg/notifications/fetch.go b/pkg/notifications/fetch.go new file mode 100644 index 00000000..0c2b81c9 --- /dev/null +++ b/pkg/notifications/fetch.go @@ -0,0 +1,159 @@ +package notifications + +import ( + "context" + "encoding/json" + "github.com/icinga/icinga-go-library/backoff" + "github.com/icinga/icinga-go-library/retry" + "github.com/icinga/icinga-go-library/types" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" + "time" +) + +// fetchHostServiceFromRedis retrieves the host and service names from Redis. +// +// If serviceId is nil, only the host name is fetched. Otherwise, both host and service name is fetched. +func (client *Client) fetchHostServiceFromRedis( + ctx context.Context, + hostId, serviceId types.Binary, +) (hostName string, serviceName string, err error) { + getNameFromRedis := func(ctx context.Context, typ, id string) (string, error) { + key := "icinga:" + typ + + var data string + if err := retry.WithBackoff( + ctx, + func(ctx context.Context) (err error) { + data, err = client.redisClient.HGet(ctx, key, id).Result() + return + }, + retry.Retryable, + backoff.DefaultBackoff, + retry.Settings{}, + ); err != nil { + return "", errors.Wrapf(err, "redis HGET %q, %q failed", key, id) + } + + var result struct { + Name string `json:"name"` + } + if err := json.Unmarshal([]byte(data), &result); err != nil { + return "", errors.Wrap(err, "failed to unmarshal redis result") + } + + return result.Name, nil + } + + hostName, err = getNameFromRedis(ctx, "host", hostId.String()) + if err != nil { + return + } + + if serviceId != nil { + serviceName, err = getNameFromRedis(ctx, "service", serviceId.String()) + if err != nil { + return + } + } + + return +} + +// fetchCustomVarFromSql retrieves custom variables for the hsot and service from SQL. +// +// If serviceId is nil, only the host custom vars are fetched. Otherwise, both host and service custom vars are fetched. +func (client *Client) fetchCustomVarFromSql( + ctx context.Context, + hostId, serviceId types.Binary, +) (map[string]string, error) { + type customVar struct { + Name string `db:"name"` + Value string `db:"value"` + } + + getCustomVarsFromSql := func(ctx context.Context, typ string, id types.Binary) ([]customVar, error) { + stmt, err := client.db.Preparex(client.db.Rebind( + `SELECT customvar.name AS name, customvar.value AS value + FROM ` + typ + `_customvar + LEFT JOIN customvar + ON ` + typ + `_customvar.customvar_id = customvar.id + WHERE ` + typ + `_customvar.` + typ + `_id = ?`)) + if err != nil { + return nil, err + } + + var customVars []customVar + if err := stmt.SelectContext(ctx, &customVars, id); err != nil { + return nil, err + } + + return customVars, nil + } + + customVars := make(map[string]string) + + hostVars, err := getCustomVarsFromSql(ctx, "host", hostId) + if err != nil { + return nil, err + } + + for _, hostVar := range hostVars { + customVars["host.vars."+hostVar.Name] = hostVar.Value + } + + if serviceId != nil { + serviceVars, err := getCustomVarsFromSql(ctx, "service", serviceId) + if err != nil { + return nil, err + } + + for _, serviceVar := range serviceVars { + customVars["service.vars."+serviceVar.Name] = serviceVar.Value + } + } + + return customVars, nil +} + +// hostServiceInformation contains the host name, an optional service name, and all custom variables. +// +// Returned from Client.fetchHostServiceData. +type hostServiceInformation struct { + hostName string + serviceName string + customVars map[string]string +} + +// fetchHostServiceData resolves the object names and fetches the associated custom variables. +// +// If serviceId is not nil, both host and service data will be queried. Otherwise, only host information is fetched. To +// acquire the information, the fetchHostServiceFromRedis and fetchCustomVarFromSql methods are used concurrently with +// a timeout of three seconds. +func (client *Client) fetchHostServiceData( + ctx context.Context, + hostId, serviceId types.Binary, +) (*hostServiceInformation, error) { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + ret := &hostServiceInformation{} + g, ctx := errgroup.WithContext(ctx) + + g.Go(func() error { + var err error + ret.hostName, ret.serviceName, err = client.fetchHostServiceFromRedis(ctx, hostId, serviceId) + return err + }) + g.Go(func() error { + var err error + ret.customVars, err = client.fetchCustomVarFromSql(ctx, hostId, serviceId) + return err + }) + + if err := g.Wait(); err != nil { + return nil, err + } + + return ret, nil +} diff --git a/pkg/notifications/notifications.go b/pkg/notifications/notifications.go index 8743ed36..c4ab1101 100644 --- a/pkg/notifications/notifications.go +++ b/pkg/notifications/notifications.go @@ -4,9 +4,6 @@ import ( "context" "encoding/json" "fmt" - "slices" - "sync" - "github.com/icinga/icinga-go-library/database" "github.com/icinga/icinga-go-library/logging" "github.com/icinga/icinga-go-library/notifications/event" @@ -21,6 +18,8 @@ import ( "github.com/pkg/errors" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "slices" + "sync" ) // Client is an Icinga Notifications compatible client implementation to push events to Icinga Notifications. @@ -194,8 +193,8 @@ func (client *Client) evaluateRulesForObject(ctx context.Context, hostId, servic func (client *Client) buildCommonEvent( ctx context.Context, hostId, serviceId types.Binary, -) (*event.Event, *redisLookupResult, error) { - rlr, err := client.fetchHostServiceFromRedis(ctx, hostId, serviceId) +) (*event.Event, *hostServiceInformation, error) { + info, err := client.fetchHostServiceData(ctx, hostId, serviceId) if err != nil { return nil, nil, err } @@ -206,18 +205,18 @@ func (client *Client) buildCommonEvent( objectTags map[string]string ) - if rlr.serviceName != "" { - objectName = rlr.hostName + "!" + rlr.serviceName - objectUrl = "/icingadb/service?name=" + utils.RawUrlEncode(rlr.serviceName) + "&host.name=" + utils.RawUrlEncode(rlr.hostName) + if info.serviceName != "" { + objectName = info.hostName + "!" + info.serviceName + objectUrl = "/icingadb/service?name=" + utils.RawUrlEncode(info.serviceName) + "&host.name=" + utils.RawUrlEncode(info.hostName) objectTags = map[string]string{ - "host": rlr.hostName, - "service": rlr.serviceName, + "host": info.hostName, + "service": info.serviceName, } } else { - objectName = rlr.hostName - objectUrl = "/icingadb/host?name=" + utils.RawUrlEncode(rlr.hostName) + objectName = info.hostName + objectUrl = "/icingadb/host?name=" + utils.RawUrlEncode(info.hostName) objectTags = map[string]string{ - "host": rlr.hostName, + "host": info.hostName, } } @@ -225,8 +224,8 @@ func (client *Client) buildCommonEvent( Name: objectName, URL: objectUrl, Tags: objectTags, - ExtraTags: rlr.CustomVars(), - }, rlr, nil + ExtraTags: info.customVars, + }, info, nil } // buildStateHistoryEvent builds a fully initialized event.Event from a state history entry. @@ -234,14 +233,14 @@ func (client *Client) buildCommonEvent( // The resulted event will have all the necessary information for a state change event, and must // not be further modified by the caller. func (client *Client) buildStateHistoryEvent(ctx context.Context, h *v1history.StateHistory) (*event.Event, error) { - ev, rlr, err := client.buildCommonEvent(ctx, h.HostId, h.ServiceId) + ev, info, err := client.buildCommonEvent(ctx, h.HostId, h.ServiceId) if err != nil { return nil, errors.Wrapf(err, "cannot build event for %q,%q", h.HostId, h.ServiceId) } ev.Type = event.TypeState - if rlr.serviceName != "" { + if info.serviceName != "" { switch h.HardState { case 0: ev.Severity = event.SeverityOK diff --git a/pkg/notifications/redis_fetch.go b/pkg/notifications/redis_fetch.go deleted file mode 100644 index af9666dd..00000000 --- a/pkg/notifications/redis_fetch.go +++ /dev/null @@ -1,180 +0,0 @@ -package notifications - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/icinga/icinga-go-library/backoff" - "github.com/icinga/icinga-go-library/retry" - "github.com/icinga/icinga-go-library/types" -) - -// redisCustomVar is a customvar entry from Redis. -type redisCustomVar struct { - EnvironmentID types.Binary `json:"environment_id"` - Name string `json:"name"` - Value string `json:"value"` -} - -// redisLookupResult defines the structure of the Redis message we're interested in. -type redisLookupResult struct { - hostName string - serviceName string - customVars []*redisCustomVar -} - -// CustomVars returns a mapping of customvar names to values. -func (result redisLookupResult) CustomVars() map[string]string { - m := make(map[string]string) - for _, customvar := range result.customVars { - m[customvar.Name] = customvar.Value - } - - return m -} - -// fetchHostServiceFromRedis retrieves the host and service names and customvars from Redis. -// -// It uses either the hostId or/and serviceId to fetch the corresponding names. If both are provided, -// the returned result will contain the host name and the service name accordingly. Otherwise, it will -// only contain the host name. -// -// The function has a hard coded timeout of five seconds for all HGET and HGETALL commands together. -func (client *Client) fetchHostServiceFromRedis(ctx context.Context, hostId, serviceId types.Binary) (*redisLookupResult, error) { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, 5*time.Second) - defer cancel() - - hgetFromRedis := func(key, id string) (string, error) { - var data string - err := retry.WithBackoff( - ctx, - func(ctx context.Context) (err error) { - data, err = client.redisClient.HGet(ctx, key, id).Result() - return - }, - retry.Retryable, - backoff.DefaultBackoff, - retry.Settings{}, - ) - if err != nil { - return "", fmt.Errorf("redis hget %q, %q failed: %w", key, id, err) - } - - return data, nil - } - - getNameFromRedis := func(typ, id string) (string, error) { - data, err := hgetFromRedis("icinga:"+typ, id) - if err != nil { - return "", err - } - - var result struct { - Name string `json:"name"` - } - if err := json.Unmarshal([]byte(data), &result); err != nil { - return "", fmt.Errorf("failed to unmarshal redis result: %w", err) - } - - return result.Name, nil - } - - getCustomVarFromRedis := func(id string) (*redisCustomVar, error) { - data, err := hgetFromRedis("icinga:customvar", id) - if err != nil { - return nil, err - } - - customvar := new(redisCustomVar) - if err := json.Unmarshal([]byte(data), customvar); err != nil { - return nil, fmt.Errorf("failed to unmarshal redis result: %w", err) - } - - return customvar, nil - } - - getObjectCustomVarsFromRedis := func(typ, id string) ([]*redisCustomVar, error) { - var resMap map[string]string - err := retry.WithBackoff( - ctx, - func(ctx context.Context) (err error) { - res := client.redisClient.HGetAll(ctx, "icinga:"+typ+":customvar") - if err = res.Err(); err != nil { - return - } - - resMap, err = res.Result() - return - }, - retry.Retryable, - backoff.DefaultBackoff, - retry.Settings{}, - ) - if err != nil { - return nil, fmt.Errorf("failed to HGETALL icinga:%s:customvar from Redis: %w", typ, err) - } - - var result struct { - CustomvarId string `json:"customvar_id"` - HostId string `json:"host_id"` - ServiceId string `json:"service_id"` - } - - var customvars []*redisCustomVar - for _, res := range resMap { - if err := json.Unmarshal([]byte(res), &result); err != nil { - return nil, fmt.Errorf("failed to unmarshal redis result: %w", err) - } - - switch typ { - case "host": - if result.HostId != id { - continue - } - case "service": - if result.ServiceId != id { - continue - } - default: - panic(fmt.Sprintf("unexpected object type %q", typ)) - } - - customvar, err := getCustomVarFromRedis(result.CustomvarId) - if err != nil { - return nil, fmt.Errorf("failed to fetch customvar: %w", err) - } - customvars = append(customvars, customvar) - } - - return customvars, nil - } - - var result redisLookupResult - var err error - - result.hostName, err = getNameFromRedis("host", hostId.String()) - if err != nil { - return nil, err - } - - if serviceId != nil { - result.serviceName, err = getNameFromRedis("service", serviceId.String()) - if err != nil { - return nil, err - } - } - - if serviceId == nil { - result.customVars, err = getObjectCustomVarsFromRedis("host", hostId.String()) - } else { - result.customVars, err = getObjectCustomVarsFromRedis("service", serviceId.String()) - } - if err != nil { - return nil, err - } - - return &result, nil -}