mirror of
https://github.com/Icinga/icingadb.git
synced 2026-02-18 18:18:00 -05:00
notifications: Custom Vars From SQL, Output Format
Rework the prior custom variable fetching code to no longer fetch everything in a looping fashion from Redis, but send SQL queries for custom variables now. In addition, for service objects now contain both the service and host custom variables, prefixed by "host.vars." or "service.vars.".
This commit is contained in:
parent
4a4792dfee
commit
ebcdadbd44
3 changed files with 175 additions and 197 deletions
159
pkg/notifications/fetch.go
Normal file
159
pkg/notifications/fetch.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in a new issue