diff --git a/exporter/clients.go b/exporter/clients.go index bc1bb4e8..37b309e3 100644 --- a/exporter/clients.go +++ b/exporter/clients.go @@ -1,6 +1,7 @@ package exporter import ( + "log/slog" "regexp" "strconv" "strings" @@ -8,7 +9,6 @@ import ( "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) type ClientInfo struct { @@ -50,7 +50,7 @@ func parseClientListString(clientInfo string) (*ClientInfo, bool) { for _, kvPart := range strings.Split(clientInfo, " ") { vPart := strings.Split(kvPart, "=") if len(vPart) != 2 { - log.Debugf("Invalid format for client list string, got: %s", kvPart) + slog.Debug("Invalid format for client list string", "kvPart", kvPart) return nil, false } @@ -64,14 +64,14 @@ func parseClientListString(clientInfo string) (*ClientInfo, bool) { case "age": createdAt, err := durationFieldToTimestamp(vPart[1]) if err != nil { - log.Debugf("could not parse 'age' field(%s): %s", vPart[1], err.Error()) + slog.Debug("could not parse 'age' field", "value", vPart[1], "error", err.Error()) return nil, false } connectedClient.CreatedAt = createdAt case "idle": idleSinceTs, err := durationFieldToTimestamp(vPart[1]) if err != nil { - log.Debugf("could not parse 'idle' field(%s): %s", vPart[1], err.Error()) + slog.Debug("could not parse 'idle' field", "value", vPart[1], "error", err.Error()) return nil, false } connectedClient.IdleSince = idleSinceTs @@ -102,7 +102,7 @@ func parseClientListString(clientInfo string) (*ClientInfo, bool) { case "addr": hostPortString := strings.Split(vPart[1], ":") if len(hostPortString) < 2 { - log.Debug("Invalid value for 'addr' found in client info") + slog.Debug("Invalid value for 'addr' found in client info") return nil, false } connectedClient.Host = strings.Join(hostPortString[:len(hostPortString)-1], ":") @@ -126,7 +126,7 @@ func durationFieldToTimestamp(field string) (int64, error) { func (e *Exporter) extractConnectedClientMetrics(ch chan<- prometheus.Metric, c redis.Conn) { reply, err := redis.String(doRedisCmd(c, "CLIENT", "LIST")) if err != nil { - log.Errorf("CLIENT LIST err: %s", err) + slog.Error("CLIENT LIST err", "error", err) return } e.parseConnectedClientMetrics(reply, ch) @@ -137,7 +137,7 @@ func (e *Exporter) parseConnectedClientMetrics(input string, ch chan<- prometheu for _, s := range strings.Split(input, "\n") { info, ok := parseClientListString(s) if !ok { - log.Debugf("parseClientListString( %s ) - couldn';t parse input", s) + slog.Debug("parseClientListString - couldn't parse input", "input", s) continue } clientInfoLabels := []string{"id", "name", "flags", "db", "host"} diff --git a/exporter/exporter.go b/exporter/exporter.go index 3b380a59..ab80cc02 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -2,6 +2,7 @@ package exporter import ( "fmt" + "log/slog" "net/http" "net/url" "runtime" @@ -16,7 +17,6 @@ import ( "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - log "github.com/sirupsen/logrus" ) type BuildInfo struct { @@ -93,7 +93,7 @@ type Options struct { // NewRedisExporter returns a new exporter of Redis metrics. func NewRedisExporter(uri string, opts Options) (*Exporter, error) { - log.Debugf("NewRedisExporter options: %#v", opts) + slog.Debug("NewRedisExporter options", "options", opts) switch { case strings.HasPrefix(uri, "valkey://"): @@ -102,7 +102,7 @@ func NewRedisExporter(uri string, opts Options) (*Exporter, error) { uri = strings.Replace(uri, "valkeys://", "rediss://", 1) } - log.Debugf("NewRedisExporter = using redis uri: %s", uri) + slog.Debug("Using redis URI", "uri", uri) if opts.Registry == nil { opts.Registry = prometheus.NewRegistry() @@ -407,31 +407,31 @@ func NewRedisExporter(uri string, opts Options) (*Exporter, error) { if keys, err := parseKeyArg(opts.CheckKeys); err != nil { return nil, fmt.Errorf("couldn't parse check-keys: %s", err) } else { - log.Debugf("keys: %#v", keys) + slog.Debug("keys", "keys", keys) } if singleKeys, err := parseKeyArg(opts.CheckSingleKeys); err != nil { return nil, fmt.Errorf("couldn't parse check-single-keys: %s", err) } else { - log.Debugf("singleKeys: %#v", singleKeys) + slog.Debug("singleKeys", "singleKeys", singleKeys) } if streams, err := parseKeyArg(opts.CheckStreams); err != nil { return nil, fmt.Errorf("couldn't parse check-streams: %s", err) } else { - log.Debugf("streams: %#v", streams) + slog.Debug("streams", "streams", streams) } if singleStreams, err := parseKeyArg(opts.CheckSingleStreams); err != nil { return nil, fmt.Errorf("couldn't parse check-single-streams: %s", err) } else { - log.Debugf("singleStreams: %#v", singleStreams) + slog.Debug("singleStreams", "singleStreams", singleStreams) } if countKeys, err := parseKeyArg(opts.CountKeys); err != nil { return nil, fmt.Errorf("couldn't parse count-keys: %s", err) } else { - log.Debugf("countKeys: %#v", countKeys) + slog.Debug("countKeys", "countKeys", countKeys) } if opts.InclSystemMetrics { @@ -571,6 +571,21 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- e.targetScrapeRequestErrors.Desc() } +func ParseLogLevel(level string) (slog.Level, error) { + switch strings.ToUpper(level) { + case "DEBUG": + return slog.LevelDebug, nil + case "INFO": + return slog.LevelInfo, nil + case "WARN", "WARNING": + return slog.LevelWarn, nil + case "ERROR": + return slog.LevelError, nil + default: + return slog.LevelInfo, fmt.Errorf("invalid log level: %s", level) + } +} + // Collect fetches new metrics from the RedisHost and updates the appropriate metrics. func (e *Exporter) Collect(ch chan<- prometheus.Metric) { e.Lock() @@ -607,13 +622,13 @@ func (e *Exporter) extractConfigMetrics(ch chan<- prometheus.Metric, config []in for pos := 0; pos < len(config)/2; pos++ { strKey, err := redis.String(config[pos*2], nil) if err != nil { - log.Errorf("invalid config key name, err: %s, skipped", err) + slog.Error("invalid config key name, skipped", "error", err) continue } strVal, err := redis.String(config[pos*2+1], nil) if err != nil { - log.Debugf("invalid config value for key name %s, err: %s, skipped", strKey, err) + slog.Debug("invalid config value for key name, skipped", "key", strKey, "error", err) continue } @@ -678,7 +693,7 @@ func (e *Exporter) getKeyOperationConnection(defaultConn redis.Conn) (redis.Conn } func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error { - defer log.Debugf("scrapeRedisHost() done") + defer slog.Debug("Finished scraping Redis host", "address", e.redisAddr) startTime := time.Now() c, err := e.connectToRedis() @@ -688,72 +703,72 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error { if err != nil { var redactedAddr string if redisURL, err2 := url.Parse(e.redisAddr); err2 != nil { - log.Debugf("url.Parse( %s ) err: %s", e.redisAddr, err2) + slog.Debug("Failed to parse URL", "address", e.redisAddr, "error", err2) redactedAddr = "" } else { redactedAddr = redisURL.Redacted() } - log.Errorf("Couldn't connect to redis instance (%s)", redactedAddr) - log.Debugf("connectToRedis( %s ) err: %s", e.redisAddr, err) + slog.Error("Couldn't connect to redis instance", "address", redactedAddr) + slog.Debug("Connect to redis failed", "address", e.redisAddr, "error", err) return err } defer c.Close() - log.Debugf("connected to: %s", e.redisAddr) - log.Debugf("connecting took %f seconds", connectTookSeconds) + slog.Debug("Connected to redis", "address", e.redisAddr) + slog.Debug("connecting took seconds", "seconds", connectTookSeconds) if e.options.PingOnConnect { startTime := time.Now() if _, err := doRedisCmd(c, "PING"); err != nil { - log.Errorf("Couldn't PING server, err: %s", err) + slog.Error("Couldn't PING server", "error", err) } else { pingTookSeconds := time.Since(startTime).Seconds() e.registerConstMetricGauge(ch, "exporter_last_scrape_ping_time_seconds", pingTookSeconds) - log.Debugf("PING took %f seconds", pingTookSeconds) + slog.Debug("PING took seconds", "seconds", pingTookSeconds) } } if e.options.SetClientName { if _, err := doRedisCmd(c, "CLIENT", "SETNAME", "redis_exporter"); err != nil { - log.Errorf("Couldn't set client name, err: %s", err) + slog.Error("Couldn't set client name", "error", err) } } dbCount := 0 if e.options.ConfigCommandName == "-" { - log.Debugf("Skipping extractConfigMetrics()") + slog.Debug("Skipping config metrics extraction") } else { if config, err := redis.Values(doRedisCmd(c, e.options.ConfigCommandName, "GET", "*")); err == nil { dbCount, err = e.extractConfigMetrics(ch, config) if err != nil { - log.Errorf("Redis extractConfigMetrics() err: %s", err) + slog.Error("Failed to extract config metrics", "error", err) return err } } else { - log.Debugf("Redis CONFIG err: %s", err) + slog.Debug("Redis CONFIG err", "error", err) } } infoAll, err := redis.String(doRedisCmd(c, "INFO", "ALL")) if err != nil || infoAll == "" { - log.Debugf("Redis INFO ALL err: %s", err) + slog.Debug("Redis INFO ALL err", "error", err) infoAll, err = redis.String(doRedisCmd(c, "INFO")) if err != nil { - log.Errorf("Redis INFO err: %s", err) + slog.Error("Redis INFO err", "error", err) return err } } - log.Debugf("Redis INFO ALL result: [%#v]", infoAll) + slog.Debug("Redis INFO ALL result", "result", infoAll) if strings.Contains(infoAll, "cluster_enabled:1") { if clusterInfo, err := redis.String(doRedisCmd(c, "CLUSTER", "INFO")); err == nil { e.extractClusterInfoMetrics(ch, clusterInfo) - // in cluster mode Redis only supports one database, so no extra DB number padding needed + // in cluster mode, Redis only supports one database, so no extra DB number padding needed dbCount = 1 } else { - log.Errorf("Redis CLUSTER INFO err: %s", err) + slog.Error("Redis CLUSTER INFO err", "error", err) } } else if dbCount == 0 { // in non-cluster mode, if dbCount is zero, then "CONFIG" failed to retrieve a valid @@ -762,7 +777,7 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error { dbCount = 16 } - log.Debugf("dbCount: %d", dbCount) + slog.Debug("dbCount", "count", dbCount) role := e.extractInfoMetrics(ch, infoAll, dbCount) @@ -772,12 +787,13 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error { // skip these metrics for master if SkipCheckKeysForRoleMaster is set // (can help with reducing workload on the master node) - log.Debugf("checkKeys metric collection for role: %s SkipCheckKeysForRoleMaster flag: %#v", role, e.options.SkipCheckKeysForRoleMaster) + slog.Debug("checkKeys metric collection for role", "role", role, "SkipCheckKeysForRoleMaster", e.options.SkipCheckKeysForRoleMaster) + if role == InstanceRoleSlave || !e.options.SkipCheckKeysForRoleMaster { // For key-based operations, use cluster connection if in cluster mode keyConn, err := e.getKeyOperationConnection(c) if err != nil { - log.Errorf("failed to get key operation connection: %s", err) + slog.Error("failed to get key operation connection", "error", err) } else { defer func() { if keyConn != c { @@ -786,7 +802,7 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error { }() if err := e.extractCheckKeyMetrics(ch, keyConn); err != nil { - log.Errorf("extractCheckKeyMetrics() err: %s", err) + slog.Error("extractCheckKeyMetrics()", "error", err) } e.extractCountKeysMetrics(ch, keyConn) @@ -794,15 +810,14 @@ func (e *Exporter) scrapeRedisHost(ch chan<- prometheus.Metric) error { e.extractStreamMetrics(ch, keyConn) } } else { - log.Infof("skipping checkKeys metrics, role: %s flag: %#v", role, e.options.SkipCheckKeysForRoleMaster) + slog.Info("skipping checkKeys metrics", "role", role, "flag", e.options.SkipCheckKeysForRoleMaster) } - e.extractSlowLogMetrics(ch, c) // Key groups also need cluster connection for key operations keyGroupConn, err := e.getKeyOperationConnection(c) if err != nil { - log.Errorf("failed to get key operation connection for key groups: %s", err) + slog.Error("failed to get key operation connection for key groups", "error", err) } else { defer func() { if keyGroupConn != c { diff --git a/exporter/exporter_test.go b/exporter/exporter_test.go index 879e3e9f..3f769348 100644 --- a/exporter/exporter_test.go +++ b/exporter/exporter_test.go @@ -10,7 +10,7 @@ package exporter import ( "fmt" - "github.com/mna/redisc" + "log/slog" "net/http/httptest" "os" "strings" @@ -18,9 +18,9 @@ import ( "time" "github.com/gomodule/redigo/redis" + "github.com/mna/redisc" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" - log "github.com/sirupsen/logrus" ) const ( @@ -91,7 +91,7 @@ func getTestExporterWithAddrAndOptions(addr string, opt Options) *Exporter { func setupKeys(t *testing.T, c redis.Conn, dbNum string) error { if _, err := doRedisCmd(c, "SELECT", dbNum); err != nil { // not failing on this one - cluster doesn't allow for SELECT so we log and ignore the error - log.Printf("setupTestKeys() - couldn't setup redis, err: %s ", err) + slog.Error("setupTestKeys() - couldn't setup redis", "error", err) } testValue := 1234.56 @@ -197,7 +197,7 @@ func setupKeys(t *testing.T, c redis.Conn, dbNum string) error { func deleteKeys(c redis.Conn, dbNum string) { if _, err := doRedisCmd(c, "SELECT", dbNum); err != nil { - log.Printf("deleteTestKeys() - couldn't setup redis, err: %s ", err) + slog.Error("deleteTestKeys() - couldn't setup redis", "error", err) // not failing on this one - cluster doesn't allow for SELECT so we log and ignore the error } @@ -220,7 +220,7 @@ func deleteKeys(c redis.Conn, dbNum string) { } func setupTestKeys(t *testing.T, uri string) { - log.Debugf("setupTestKeys uri: %s", uri) + slog.Debug("setupTestKeys uri", "uri", uri) c, err := redis.DialURL(uri) if err != nil { t.Fatalf("couldn't setup redis for uri %s, err: %s ", uri, err) @@ -240,7 +240,7 @@ func setupTestKeys(t *testing.T, uri string) { } func setupTestKeysCluster(t *testing.T, uri string) { - log.Debugf("Creating cluster object") + slog.Debug("Creating cluster object") cluster := redisc.Cluster{ StartupNodes: []string{ strings.Replace(uri, "redis://", "", 1), @@ -249,17 +249,17 @@ func setupTestKeysCluster(t *testing.T, uri string) { } if err := cluster.Refresh(); err != nil { - log.Fatalf("Refresh failed: %v", err) + slog.Error("Refresh failed", "error", err) } conn, err := cluster.Dial() if err != nil { - log.Errorf("Dial() failed: %v", err) + slog.Error("Dial() failed", "error", err) } c, err := redisc.RetryConn(conn, 10, 100*time.Millisecond) if err != nil { - log.Errorf("RetryConn() failed: %v", err) + slog.Error("RetryConn() failed", "error", err) } // cluster only supports db==0 @@ -964,14 +964,21 @@ db1:keys=18,expires=13,avg_ttl=145372776312,subexpiry=0 } func init() { + var lvl slog.Level + var err error ll := strings.ToLower(os.Getenv("LOG_LEVEL")) - if pl, err := log.ParseLevel(ll); err == nil { - log.Printf("Setting log level to: %s", ll) - log.SetLevel(pl) + if lvl, err = ParseLogLevel(ll); err == nil { + fmt.Printf("Setting log level to: %s\n", ll) } else { - log.SetLevel(log.InfoLevel) + lvl = slog.LevelInfo } + slog.SetDefault( + slog.New( + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}), + ), + ) + testTimestamp := time.Now().Unix() for _, n := range []string{"john", "paul", "ringo", "george"} { diff --git a/exporter/http.go b/exporter/http.go index bad9d816..ec6461dc 100644 --- a/exporter/http.go +++ b/exporter/http.go @@ -5,13 +5,13 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "net/url" "strings" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - log "github.com/sirupsen/logrus" ) func (e *Exporter) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -156,16 +156,20 @@ func (e *Exporter) reloadPwdFile(w http.ResponseWriter, r *http.Request) { http.Error(w, "There is no pwd file specified", http.StatusBadRequest) return } - log.Debugf("Reload redisPwdFile") + + slog.Debug("Reload redisPwdFile", "file", e.options.RedisPwdFile) + passwordMap, err := LoadPwdFile(e.options.RedisPwdFile) if err != nil { - log.Errorf("Error reloading redis passwords from file %s, err: %s", e.options.RedisPwdFile, err) + slog.Error("Error reloading redis passwords from file", "file", e.options.RedisPwdFile, "error", err) http.Error(w, "failed to reload passwords file: "+err.Error(), http.StatusInternalServerError) return } + e.Lock() e.options.PasswordMap = passwordMap e.Unlock() + _, _ = w.Write([]byte(`ok`)) } @@ -174,20 +178,19 @@ func (e *Exporter) isBasicAuthConfigured() bool { } func (e *Exporter) verifyBasicAuth(user, password string, authHeaderSet bool) error { - if !e.isBasicAuthConfigured() { return nil } if !authHeaderSet { - return errors.New("Unauthorized") + return errors.New("unauthorized") } userCorrect := subtle.ConstantTimeCompare([]byte(user), []byte(e.options.BasicAuthUsername)) passCorrect := subtle.ConstantTimeCompare([]byte(password), []byte(e.options.BasicAuthPassword)) if userCorrect == 0 || passCorrect == 0 { - return errors.New("Unauthorized") + return errors.New("unauthorized") } return nil diff --git a/exporter/http_test.go b/exporter/http_test.go index 3ed3de36..ea473249 100644 --- a/exporter/http_test.go +++ b/exporter/http_test.go @@ -3,6 +3,7 @@ package exporter import ( "fmt" "io" + "log/slog" "math/rand" "net" "net/http" @@ -12,8 +13,6 @@ import ( "strings" "sync" "testing" - - log "github.com/sirupsen/logrus" ) func TestHTTPScrapeMetricsEndpoints(t *testing.T) { @@ -537,7 +536,7 @@ func TestVerifyBasicAuth(t *testing.T) { providedPass: "", authHeaderSet: false, wantErr: true, - wantErrString: "Unauthorized", + wantErrString: "unauthorized", }, { name: "auth configured - correct credentials", @@ -556,7 +555,7 @@ func TestVerifyBasicAuth(t *testing.T) { providedPass: "pass", authHeaderSet: true, wantErr: true, - wantErrString: "Unauthorized", + wantErrString: "unauthorized", }, { name: "auth configured - wrong password", @@ -566,7 +565,7 @@ func TestVerifyBasicAuth(t *testing.T) { providedPass: "wrongpass", authHeaderSet: true, wantErr: true, - wantErrString: "Unauthorized", + wantErrString: "unauthorized", }, } @@ -700,7 +699,7 @@ func downloadURL(t *testing.T, u string) string { } func downloadURLWithStatusCode(t *testing.T, u string) (int, string) { - log.Debugf("downloadURL() %s", u) + slog.Debug("downloadURL()", "url", u) resp, err := http.Get(u) if err != nil { diff --git a/exporter/info.go b/exporter/info.go index 9048ada6..e120a087 100644 --- a/exporter/info.go +++ b/exporter/info.go @@ -3,13 +3,13 @@ package exporter import ( "errors" "fmt" + "log/slog" "regexp" "strconv" "strings" "time" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) // precompiled regexps @@ -79,10 +79,10 @@ func (e *Exporter) extractInfoMetrics(ch chan<- prometheus.Metric, info string, masterPort := "" for _, line := range lines { line = strings.TrimSpace(line) - log.Debugf("info: %s", line) + slog.Debug("info", "line", line) if len(line) > 0 && strings.HasPrefix(line, "# ") { fieldClass = line[2:] - log.Debugf("set fieldClass: %s", fieldClass) + slog.Debug("set fieldClass", "fieldClass", fieldClass) continue } @@ -221,7 +221,7 @@ func (e *Exporter) extractClusterInfoMetrics(ch chan<- prometheus.Metric, info s lines := strings.Split(info, "\r\n") for _, line := range lines { - log.Debugf("info: %s", line) + slog.Debug("info", "line", line) split := strings.Split(line, ":") if len(split) != 2 { @@ -241,33 +241,33 @@ func (e *Exporter) extractClusterInfoMetrics(ch chan<- prometheus.Metric, info s valid example: db0:keys=1,expires=0,avg_ttl=0,cached_keys=0 */ func parseDBKeyspaceString(inputKey string, inputVal string) (keysTotal float64, keysExpiringTotal float64, avgTTL float64, keysCachedTotal float64, ok bool) { - log.Debugf("parseDBKeyspaceString inputKey: [%s] inputVal: [%s]", inputKey, inputVal) + slog.Debug("parseDBKeyspaceString", "inputKey", inputKey, "inputVal", inputVal) if !strings.HasPrefix(inputKey, "db") { - log.Debugf("parseDBKeyspaceString inputKey not starting with 'db': [%s]", inputKey) + slog.Debug("parseDBKeyspaceString inputKey not starting with 'db'", "inputKey", inputKey) return } split := strings.Split(inputVal, ",") if len(split) < 2 || len(split) > 4 { - log.Debugf("parseDBKeyspaceString strings.Split(inputVal) invalid: %#v", split) + slog.Debug("parseDBKeyspaceString strings.Split(inputVal) invalid", "split", split) return } var err error if keysTotal, err = extractVal(split[0]); err != nil { - log.Debugf("parseDBKeyspaceString extractVal(split[0]) invalid, err: %s", err) + slog.Debug("parseDBKeyspaceString extractVal(split[0]) invalid", "error", err) return } if keysExpiringTotal, err = extractVal(split[1]); err != nil { - log.Debugf("parseDBKeyspaceString extractVal(split[1]) invalid, err: %s", err) + slog.Debug("parseDBKeyspaceString extractVal(split[1]) invalid", "error", err) return } avgTTL = -1 if len(split) > 2 { if avgTTL, err = extractVal(split[2]); err != nil { - log.Debugf("parseDBKeyspaceString extractVal(split[2]) invalid, err: %s", err) + slog.Debug("parseDBKeyspaceString extractVal(split[2]) invalid", "error", err) return } avgTTL /= 1000 @@ -276,7 +276,7 @@ func parseDBKeyspaceString(inputKey string, inputVal string) (keysTotal float64, keysCachedTotal = -1 if len(split) > 3 { if keysCachedTotal, err = extractVal(split[3]); err != nil { - log.Debugf("parseDBKeyspaceString extractVal(split[3]) invalid, err: %s", err) + slog.Debug("parseDBKeyspaceString extractVal(split[3]) invalid", "error", err) return } } @@ -298,14 +298,14 @@ func parseConnectedSlaveString(slaveName string, keyValues string) (offset float for _, kvPart := range strings.Split(keyValues, ",") { x := strings.Split(kvPart, "=") if len(x) != 2 { - log.Debugf("Invalid format for connected slave string, got: %s", kvPart) + slog.Debug("Invalid format for connected slave string", "kvPart", kvPart) return } connectedkeyValues[x[0]] = x[1] } offset, err := strconv.ParseFloat(connectedkeyValues["offset"], 64) if err != nil { - log.Debugf("Can not parse connected slave offset, got: %s", connectedkeyValues["offset"]) + slog.Debug("Can not parse connected slave offset", "offset", connectedkeyValues["offset"]) return } @@ -315,7 +315,7 @@ func parseConnectedSlaveString(slaveName string, keyValues string) (offset float } else { lag, err = strconv.ParseFloat(lagStr, 64) if err != nil { - log.Debugf("Can not parse connected slave lag, got: %s", lagStr) + slog.Debug("Can not parse connected slave lag", "lagStr", lagStr) return } } @@ -540,7 +540,7 @@ func parseMetricsErrorStats(fieldKey string, fieldValue string) (errorType strin func (e *Exporter) handleMetricsCommandStats(ch chan<- prometheus.Metric, fieldKey string, fieldValue string) (cmd string, calls float64, usecTotal float64) { cmd, calls, rejectedCalls, failedCalls, usecTotal, extendedStats, err := parseMetricsCommandStats(fieldKey, fieldValue) if err != nil { - log.Debugf("parseMetricsCommandStats( %s , %s ) err: %s", fieldKey, fieldValue, err) + slog.Debug("parseMetricsCommandStats err", "fieldKey", fieldKey, "fieldValue", fieldValue, "error", err) return } e.createMetricDescription("commands_total", []string{"cmd"}) diff --git a/exporter/info_test.go b/exporter/info_test.go index f88c0458..77a88144 100644 --- a/exporter/info_test.go +++ b/exporter/info_test.go @@ -2,6 +2,7 @@ package exporter import ( "fmt" + "log/slog" "net/http/httptest" "os" "reflect" @@ -10,7 +11,6 @@ import ( "testing" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) func TestKeyspaceStringParser(t *testing.T) { @@ -43,7 +43,6 @@ func TestKeyspaceStringParser(t *testing.T) { }, } - log.SetLevel(log.DebugLevel) for _, tst := range tsts { if kt, kx, ttl, kc, ok := parseDBKeyspaceString(tst.db, tst.stats); true { @@ -184,7 +183,7 @@ func TestClusterMaster(t *testing.T) { defer ts.Close() body := downloadURL(t, ts.URL+"/metrics") - log.Debugf("master - body: %s", body) + slog.Debug("master - body", "body", body) for _, want := range []string{ "test_instance_info{", "test_master_repl_offset", @@ -253,7 +252,7 @@ func TestClusterSlave(t *testing.T) { defer ts.Close() body := downloadURL(t, ts.URL+"/metrics") - log.Debugf("slave - body: %s", body) + slog.Debug("slave - body", "body", body) for _, want := range []string{ "test_instance_info", "test_master_last_io_seconds", diff --git a/exporter/key_groups.go b/exporter/key_groups.go index 0141255f..9e5939f8 100644 --- a/exporter/key_groups.go +++ b/exporter/key_groups.go @@ -3,13 +3,13 @@ package exporter import ( "encoding/csv" "fmt" + "log/slog" "sort" "strings" "time" "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) type keyGroupMetrics struct { @@ -86,7 +86,7 @@ func (e *Exporter) gatherKeyGroupsMetricsForAllDatabases(c redis.Conn, dbCount i strings.NewReader(e.options.CheckKeyGroups), ).Read() if err != nil { - log.Errorf("Failed to parse key groups as csv: %s", err) + slog.Error("Failed to parse key groups as csv", "error", err) return allMetrics } for i, v := range keyGroups { @@ -104,12 +104,12 @@ func (e *Exporter) gatherKeyGroupsMetricsForAllDatabases(c redis.Conn, dbCount i } for db := 0; db < dbCount; db++ { if _, err := doRedisCmd(c, "SELECT", db); err != nil { - log.Errorf("Couldn't select database %d when getting key info.", db) + slog.Error("Couldn't select database when getting key info", "db", db, "error", err) continue } allGroups, err := gatherKeyGroupMetrics(c, e.options.CheckKeysBatchSize, keyGroupsNoEmptyStrings) if err != nil { - log.Error(err) + slog.Error("Error gathering key group metrics", "error", err) continue } allMetrics.metrics[db] = allGroups diff --git a/exporter/keys.go b/exporter/keys.go index d27947f1..4100bd6c 100644 --- a/exporter/keys.go +++ b/exporter/keys.go @@ -2,6 +2,7 @@ package exporter import ( "fmt" + "log/slog" "net/url" "regexp" "strconv" @@ -9,7 +10,6 @@ import ( "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) type dbKeyPair struct { @@ -19,7 +19,7 @@ type dbKeyPair struct { func getStringInfoNotPipelined(c redis.Conn, key string) (strVal string, keyType string, size int64, err error) { if strVal, err = redis.String(doRedisCmd(c, "GET", key)); err != nil { - log.Errorf("GET %s err: %s", key, err) + slog.Error("GET err", "key", key, "error", err) } // Check PFCOUNT first because STRLEN on HyperLogLog strings returns the wrong length @@ -44,7 +44,7 @@ func (e *Exporter) getKeyInfo(ch chan<- prometheus.Metric, c redis.Conn, dbLabel switch keyType { case "none": - log.Debugf("Key '%s' not found when trying to get type and size: using default '0.0'", keyName) + slog.Debug("Key not found when trying to get type and size: using default '0.0'", "key", keyName) e.registerConstMetricGauge(ch, "key_size", 0.0, dbLabel, keyName) return @@ -65,7 +65,7 @@ func (e *Exporter) getKeyInfo(ch chan<- prometheus.Metric, c redis.Conn, dbLabel } if err != nil { - log.Errorf("getKeyInfo() err: %s", err) + slog.Error("Failed to get key info", "error", err) return } @@ -89,25 +89,25 @@ func (e *Exporter) extractCheckKeyMetrics(ch chan<- prometheus.Metric, c redis.C if err != nil { return fmt.Errorf("couldn't parse check-keys: %w", err) } - log.Debugf("keys: %#v", keys) + slog.Debug("keys", "keys", keys) singleKeys, err := parseKeyArg(e.options.CheckSingleKeys) if err != nil { return fmt.Errorf("couldn't parse check-single-keys: %w", err) } - log.Debugf("e.singleKeys: %#v", singleKeys) + slog.Debug("e.singleKeys", "singleKeys", singleKeys) allKeys := append([]dbKeyPair{}, singleKeys...) - log.Debugf("e.keys: %#v", keys) + slog.Debug("e.keys", "keys", keys) if scannedKeys, err := getKeysFromPatterns(c, keys, e.options.CheckKeysBatchSize); err == nil { allKeys = append(allKeys, scannedKeys...) } else { - log.Errorf("Error expanding key patterns: %#v", err) + slog.Error("Error expanding key patterns", "error", err) } - log.Debugf("allKeys: %#v", allKeys) + slog.Debug("allKeys", "allKeys", allKeys) /* important: when adding, modifying, removing metrics both paths here @@ -147,9 +147,9 @@ func (e *Exporter) extractCheckKeyMetricsPipelined(ch chan<- prometheus.Metric, for dbNum, arrayOfKeys := range keysByDb { dbLabel := "db" + dbNum - log.Debugf("c.Send() SELECT [%s]", dbNum) + slog.Debug("c.Send() SELECT", "db", dbNum) if err := c.Send("SELECT", dbNum); err != nil { - log.Errorf("Couldn't select database [%s] when getting key info.", dbNum) + slog.Error("Couldn't select database when getting key info", "db", dbNum) continue } /* @@ -158,27 +158,27 @@ func (e *Exporter) extractCheckKeyMetricsPipelined(ch chan<- prometheus.Metric, */ for _, keyName := range arrayOfKeys { - log.Debugf("c.Send() TYPE [%v]", keyName) + slog.Debug("c.Send() TYPE", "key", keyName) if err := c.Send("TYPE", keyName); err != nil { - log.Errorf("c.Send() TYPE err: %s", err) + slog.Error("Failed to send TYPE command", "error", err) return } - log.Debugf("c.Send() MEMORY USAGE [%v]", keyName) + slog.Debug("c.Send() MEMORY USAGE", "key", keyName) if err := c.Send("MEMORY", "USAGE", keyName); err != nil { - log.Errorf("c.Send() MEMORY USAGE err: %s", err) + slog.Error("Failed to send MEMORY USAGE command", "error", err) return } } - log.Debugf("c.Flush()") + slog.Debug("c.Flush()") if err := c.Flush(); err != nil { - log.Errorf("FLUSH err: %s", err) + slog.Error("FLUSH err", "error", err) return } // throwaway Receive() call for the response of the SELECT() call if _, err := redis.String(c.Receive()); err != nil { - log.Errorf("Receive() err: %s", err) + slog.Error("Failed to receive response", "error", err) continue } @@ -191,12 +191,12 @@ func (e *Exporter) extractCheckKeyMetricsPipelined(ch chan<- prometheus.Metric, var err error keyTypes[idx], err = redis.String(c.Receive()) if err != nil { - log.Errorf("key: [%s] - Receive err: %s", keyName, err) + slog.Error("key - Receive err", "key", keyName, "error", err) continue } memUsageInBytes, err := redis.Int64(c.Receive()) if err != nil { - // log.Errorf("key: [%s] - memUsageInBytes Receive() err: %s", keyName, err) + // slog.Error(fmt.Sprintf("key: [%s] - memUsageInBytes Receive() err: %s", keyName, err) continue } @@ -223,66 +223,66 @@ func (e *Exporter) getKeyInfoPipelined(ch chan<- prometheus.Metric, c redis.Conn continue case "string": - log.Debugf("c.Send() PFCOUNT args: [%v]", keyName) + slog.Debug("c.Send() PFCOUNT", "key", keyName) if err := c.Send("PFCOUNT", keyName); err != nil { - log.Errorf("PFCOUNT err: %s", err) + slog.Error("PFCOUNT err", "error", err) return } - log.Debugf("c.Send() STRLEN args: [%v]", keyName) + slog.Debug("c.Send() STRLEN", "key", keyName) if err := c.Send("STRLEN", keyName); err != nil { - log.Errorf("STRLEN err: %s", err) + slog.Error("STRLEN err", "error", err) return } - log.Debugf("c.Send() GET args: [%v]", keyName) + slog.Debug("c.Send() GET", "key", keyName) if err := c.Send("GET", keyName); err != nil { - log.Errorf("GET err: %s", err) + slog.Error("GET err", "error", err) return } case "list": - log.Debugf("c.Send() LLEN args: [%v]", keyName) + slog.Debug("c.Send() LLEN", "key", keyName) if err := c.Send("LLEN", keyName); err != nil { - log.Errorf("LLEN err: %s", err) + slog.Error("LLEN err", "error", err) return } case "set": - log.Debugf("c.Send() SCARD args: [%v]", keyName) + slog.Debug("c.Send() SCARD", "key", keyName) if err := c.Send("SCARD", keyName); err != nil { - log.Errorf("SCARD err: %s", err) + slog.Error("SCARD err", "error", err) return } case "zset": - log.Debugf("c.Send() ZCARD args: [%v]", keyName) + slog.Debug("c.Send() ZCARD", "key", keyName) if err := c.Send("ZCARD", keyName); err != nil { - log.Errorf("ZCARD err: %s", err) + slog.Error("ZCARD err", "error", err) return } case "hash": - log.Debugf("c.Send() HLEN args: [%v]", keyName) + slog.Debug("c.Send() HLEN", "key", keyName) if err := c.Send("HLEN", keyName); err != nil { - log.Errorf("HLEN err: %s", err) + slog.Error("HLEN err", "error", err) return } case "stream": - log.Debugf("c.Send() XLEN args: [%v]", keyName) + slog.Debug("c.Send() XLEN", "key", keyName) if err := c.Send("XLEN", keyName); err != nil { - log.Errorf("XLEN err: %s", err) + slog.Error("XLEN err", "error", err) return } default: - log.Errorf("unknown type: %v for key: %v", keyType, keyName) + slog.Error("unknown type for key", "type", keyType, "key", keyName) continue } } - log.Debugf("c.Flush()") + slog.Debug("c.Flush()") if err := c.Flush(); err != nil { - log.Errorf("Flush() err: %s", err) + slog.Error("Failed to flush commands", "error", err) return } @@ -295,7 +295,7 @@ func (e *Exporter) getKeyInfoPipelined(ch chan<- prometheus.Metric, c redis.Conn switch keyType { case "none": - log.Debugf("Key '%s' not found, skipping", keyName) + slog.Debug("Key not found, skipping", "key", keyName) case "string": hllSize, hllErr := redis.Int64(c.Receive()) @@ -303,10 +303,10 @@ func (e *Exporter) getKeyInfoPipelined(ch chan<- prometheus.Metric, c redis.Conn var strValErr error if strVal, strValErr = redis.String(c.Receive()); strValErr != nil { - log.Errorf("c.Receive() for GET %s err: %s", keyName, strValErr) + slog.Error("Failed to receive GET response", "key", keyName, "error", strValErr) } - log.Debugf("Done with c.Receive() x 3") + slog.Debug("Done with c.Receive() x 3") if hllErr == nil { // hyperloglog @@ -330,7 +330,7 @@ func (e *Exporter) getKeyInfoPipelined(ch chan<- prometheus.Metric, c redis.Conn } if err != nil { - log.Errorf("getKeyInfo() err: %s", err) + slog.Error("Failed to get key info", "error", err) continue } @@ -356,14 +356,14 @@ func (e *Exporter) extractCheckKeyMetricsNotPipelined(ch chan<- prometheus.Metri keyType, err := redis.String(doRedisCmd(c, "TYPE", k.key)) if err != nil { - log.Errorf("TYPE err: %s", keyType) + slog.Error("TYPE err", "error", keyType) continue } if memUsageInBytes, err := redis.Int64(doRedisCmd(c, "MEMORY", "USAGE", k.key)); err == nil { e.registerConstMetricGauge(ch, "key_memory_usage_bytes", float64(memUsageInBytes), "db"+k.db, k.key) } else { - log.Errorf("MEMORY USAGE %s err: %s", k.key, err) + slog.Error("MEMORY USAGE err", "key", k.key, "error", err) } dbLabel := "db" + k.db @@ -374,18 +374,18 @@ func (e *Exporter) extractCheckKeyMetricsNotPipelined(ch chan<- prometheus.Metri func (e *Exporter) extractCountKeysMetrics(ch chan<- prometheus.Metric, c redis.Conn) { cntKeys, err := parseKeyArg(e.options.CountKeys) if err != nil { - log.Errorf("Couldn't parse given count keys: %s", err) + slog.Error("Couldn't parse given count keys", "error", err) return } for _, k := range cntKeys { if _, err := doRedisCmd(c, "SELECT", k.db); err != nil { - log.Errorf("Couldn't select database '%s' when getting stream info", k.db) + slog.Error("Couldn't select database when getting stream info", "db", k.db) continue } cnt, err := getKeysCount(c, k.key, e.options.CheckKeysBatchSize) if err != nil { - log.Errorf("couldn't get key count for '%s', err: %s", k.key, err) + slog.Error("couldn't get key count", "key", k.key, "error", err) continue } dbLabel := "db" + k.db @@ -421,7 +421,7 @@ func getKeysFromPatterns(c redis.Conn, keys []dbKeyPair, count int64) (expandedK } keyNames, err := redis.Strings(scanKeys(c, k.key, count)) if err != nil { - log.Errorf("error with SCAN for pattern: %#v err: %s", k.key, err) + slog.Error("error with SCAN for pattern", "pattern", k.key, "error", err) continue } @@ -439,7 +439,7 @@ func getKeysFromPatterns(c redis.Conn, keys []dbKeyPair, count int64) (expandedK // parseKeyArgs splits a command-line supplied argument into a slice of dbKeyPairs. func parseKeyArg(keysArgString string) (keys []dbKeyPair, err error) { if keysArgString == "" { - log.Debugf("parseKeyArg(): Got empty key arguments, parsing skipped") + slog.Debug("Got empty key arguments, skipping parsing") return keys, err } for _, k := range strings.Split(keysArgString, ",") { @@ -466,7 +466,7 @@ func parseKeyArg(keysArgString string) (keys []dbKeyPair, err error) { // We want to guarantee at the top level that invalid values // will not fall into the final Redis call. if db == "" || key == "" { - log.Errorf("parseKeyArg(): Empty value parsed in pair '%s=%s', skip", db, key) + slog.Error("Empty value parsed in pair, skipping", "db", db, "key", key) continue } diff --git a/exporter/keys_test.go b/exporter/keys_test.go index f21fa725..03537f88 100644 --- a/exporter/keys_test.go +++ b/exporter/keys_test.go @@ -2,6 +2,7 @@ package exporter import ( "fmt" + "log/slog" "net/http/httptest" "net/url" "os" @@ -12,7 +13,6 @@ import ( "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) // defaultCount is used for `SCAN whatever COUNT defaultCount` command @@ -596,12 +596,12 @@ func TestKeyValueInvalidDB(t *testing.T) { case prometheus.Gauge: for k := range dontWant { if strings.Contains(m.Desc().String(), k) { - log.Println(m.Desc().String()) + slog.Debug("metric desc", "desc", m.Desc().String()) dontWant[k] = true } } default: - log.Debugf("default: m: %#v", m) + slog.Debug("default", "metric", m) } } for k, found := range dontWant { diff --git a/exporter/latency.go b/exporter/latency.go index 6af620f0..5230e810 100644 --- a/exporter/latency.go +++ b/exporter/latency.go @@ -1,6 +1,7 @@ package exporter import ( + "log/slog" "regexp" "strconv" "strings" @@ -9,7 +10,6 @@ import ( "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) var ( @@ -31,9 +31,9 @@ func (e *Exporter) extractLatencyLatestMetrics(outChan chan<- prometheus.Metric, we're logging this only once as an Error and always as Debugf() */ logLatestErrOnce.Do(func() { - log.Errorf("WARNING, LOGGED ONCE ONLY: cmd LATENCY LATEST, err: %s", err) + slog.Error("WARNING, LOGGED ONCE ONLY: cmd LATENCY LATEST", "error", err) }) - log.Debugf("cmd LATENCY LATEST, err: %s", err) + slog.Debug("cmd LATENCY LATEST", "error", err) return } @@ -57,9 +57,9 @@ func (e *Exporter) extractLatencyHistogramMetrics(outChan chan<- prometheus.Metr reply, err := redis.Values(doRedisCmd(redisConn, "LATENCY", "HISTOGRAM")) if err != nil { logHistogramErrOnce.Do(func() { - log.Errorf("WARNING, LOGGED ONCE ONLY: cmd LATENCY HISTOGRAM, err: %s", err) + slog.Error("WARNING, LOGGED ONCE ONLY: cmd LATENCY HISTOGRAM", "error", err) }) - log.Debugf("cmd LATENCY HISTOGRAM, err: %s", err) + slog.Debug("cmd LATENCY HISTOGRAM", "error", err) return } @@ -100,7 +100,7 @@ func extractTotalUsecForCommand(infoAll string, cmd string) uint64 { usecs, err := strconv.ParseUint(match[2], 10, 0) if err != nil { - log.Warnf("Unable to parse uint from string \"%s\": %v", match[2], err) + slog.Warn("Unable to parse uint from string", "string", match[2], "error", err) continue } diff --git a/exporter/lua.go b/exporter/lua.go index fb10a0a9..f65c2287 100644 --- a/exporter/lua.go +++ b/exporter/lua.go @@ -1,24 +1,24 @@ package exporter import ( + "log/slog" "strconv" "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) func (e *Exporter) extractLuaScriptMetrics(ch chan<- prometheus.Metric, c redis.Conn, filename string, script []byte) error { - log.Debugf("Evaluating e.options.LuaScript: %s", filename) + slog.Debug("Evaluating e.options.LuaScript", "file", filename) kv, err := redis.StringMap(doRedisCmd(c, "EVAL", script, 0, 0)) if err != nil { - log.Errorf("LuaScript error: %v", err) + slog.Error("LuaScript error", "error", err) e.registerConstMetricGauge(ch, "script_result", 0, filename) return err } if len(kv) == 0 { - log.Debugf("Lua script returned no results") + slog.Debug("Lua script returned no results") e.registerConstMetricGauge(ch, "script_result", 2, filename) return nil } @@ -26,7 +26,7 @@ func (e *Exporter) extractLuaScriptMetrics(ch chan<- prometheus.Metric, c redis. for key, stringVal := range kv { val, err := strconv.ParseFloat(stringVal, 64) if err != nil { - log.Errorf("Error parsing lua script results, err: %s", err) + slog.Error("Error parsing lua script results", "error", err) e.registerConstMetricGauge(ch, "script_result", 0, filename) return err } diff --git a/exporter/metrics.go b/exporter/metrics.go index ada8cd19..1f775477 100644 --- a/exporter/metrics.go +++ b/exporter/metrics.go @@ -2,12 +2,12 @@ package exporter import ( "fmt" + "log/slog" "regexp" "strconv" "strings" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) var metricNameRE = regexp.MustCompile(`[^a-zA-Z0-9_]`) @@ -59,7 +59,7 @@ func (e *Exporter) parseAndRegisterConstMetric(ch chan<- prometheus.Metric, fiel } if err != nil { - log.Debugf("couldn't parse %s, err: %s", fieldValue, err) + slog.Debug("couldn't parse", "fieldValue", fieldValue, "error", err) return } @@ -91,7 +91,7 @@ func (e *Exporter) registerConstMetric(ch chan<- prometheus.Metric, metric strin m, err := prometheus.NewConstMetric(desc, valType, val, labelValues...) if err != nil { - log.Debugf("registerConstMetric( %s , %.2f) err: %s", metric, val, err) + slog.Debug("registerConstMetric err", "metric", metric, "value", val, "error", err) return } diff --git a/exporter/modules.go b/exporter/modules.go index 2fe82a70..77043051 100644 --- a/exporter/modules.go +++ b/exporter/modules.go @@ -1,23 +1,23 @@ package exporter import ( + "log/slog" "strings" "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) func (e *Exporter) extractModulesMetrics(ch chan<- prometheus.Metric, c redis.Conn) { info, err := redis.String(doRedisCmd(c, "INFO", "MODULES")) if err != nil { - log.Errorf("extractSearchMetrics() err: %s", err) + slog.Error("Failed to extract modules metrics", "error", err) return } lines := strings.Split(info, "\r\n") for _, line := range lines { - log.Debugf("info: %s", line) + slog.Debug("info", "line", line) split := strings.Split(line, ":") if len(split) != 2 { diff --git a/exporter/nodes.go b/exporter/nodes.go index b5562657..34cba096 100644 --- a/exporter/nodes.go +++ b/exporter/nodes.go @@ -1,11 +1,11 @@ package exporter import ( + "log/slog" "regexp" "strings" "github.com/gomodule/redigo/redis" - log "github.com/sirupsen/logrus" ) var reNodeAddress = regexp.MustCompile(`^(?P.+):(?P\d+)@(?P\d+)(?:,(?P.+))?`) @@ -13,7 +13,7 @@ var reNodeAddress = regexp.MustCompile(`^(?P.+):(?P\d+)@(?P\d+) func (e *Exporter) getClusterNodes(c redis.Conn) ([]string, error) { output, err := redis.String(doRedisCmd(c, "CLUSTER", "NODES")) if err != nil { - log.Errorf("Error getting cluster nodes: %s", err) + slog.Error("Error getting cluster nodes", "error", err) return nil, err } @@ -34,17 +34,17 @@ func (e *Exporter) getClusterNodes(c redis.Conn) ([]string, error) { eaf69c70d876558a948ba62af0884a37d42c9627 127.0.0.1:7002@17002 master - 0 1742836359057 3 connected 10923-16383 */ func parseClusterNodeString(node string) (string, bool) { - log.Debugf("parseClusterNodeString node: [%s]", node) + slog.Debug("parseClusterNodeString node", "node", node) fields := strings.Fields(node) if len(fields) < 2 { - log.Debugf("Invalid field count for node: %s", node) + slog.Debug("Invalid field count for node", "node", node) return "", false } address := reNodeAddress.FindStringSubmatch(fields[1]) if len(address) < 3 { - log.Debugf("Invalid format for node address, got: %s", fields[1]) + slog.Debug("Invalid format for node address", "address", fields[1]) return "", false } diff --git a/exporter/pwd_file.go b/exporter/pwd_file.go index dfeaf54f..b6b6a44e 100644 --- a/exporter/pwd_file.go +++ b/exporter/pwd_file.go @@ -2,30 +2,29 @@ package exporter import ( "encoding/json" + "log/slog" "os" - - log "github.com/sirupsen/logrus" ) // LoadPwdFile reads the redis password file and returns the password map func LoadPwdFile(passwordFile string) (map[string]string, error) { res := make(map[string]string) - log.Debugf("start load password file: %s", passwordFile) + slog.Debug("start load password file", "file", passwordFile) bytes, err := os.ReadFile(passwordFile) if err != nil { - log.Warnf("load password file failed: %s", err) + slog.Warn("load password file failed", "error", err) return nil, err } err = json.Unmarshal(bytes, &res) if err != nil { - log.Warnf("password file format error: %s", err) + slog.Warn("password file format error", "error", err) return nil, err } - log.Infof("Loaded %d entries from %s", len(res), passwordFile) + slog.Info("Loaded entries from password file", "count", len(res), "file", passwordFile) for k := range res { - log.Debugf("%s", k) + slog.Debug("password entry", "key", k) } return res, nil diff --git a/exporter/redis.go b/exporter/redis.go index 15bc694b..4f957a25 100644 --- a/exporter/redis.go +++ b/exporter/redis.go @@ -2,13 +2,13 @@ package exporter import ( "fmt" + "log/slog" "net/url" "strings" "time" "github.com/gomodule/redigo/redis" "github.com/mna/redisc" - log "github.com/sirupsen/logrus" ) func (e *Exporter) configureOptions(uri string) ([]redis.DialOption, error) { @@ -54,7 +54,7 @@ func (e *Exporter) lookupPasswordInPasswordMap(uri string) (string, bool) { // strip solo ":" if present in uri that has a username (and no pwd) uri = strings.Replace(uri, fmt.Sprintf(":@%s", u.Host), fmt.Sprintf("@%s", u.Host), 1) - log.Debugf("looking up in pwd map, uri: %s", uri) + slog.Debug("Looking up URI in pwd map", "uri", uri) if pwd, ok := e.options.PasswordMap[uri]; ok && pwd != "" { return pwd, true } @@ -72,15 +72,15 @@ func (e *Exporter) connectToRedis() (redis.Conn, error) { return nil, err } - log.Debugf("Trying DialURL(): %s", uri) + slog.Debug("Trying to dial URL", "uri", uri) c, err := redis.DialURL(uri, options...) if err != nil { - log.Debugf("DialURL() failed, err: %s", err) + slog.Debug("Failed to dial URL", "error", err) if frags := strings.Split(e.redisAddr, "://"); len(frags) == 2 { - log.Debugf("Trying: Dial(): %s %s", frags[0], frags[1]) + slog.Debug("Trying to dial", "protocol", frags[0], "address", frags[1]) c, err = redis.Dial(frags[0], frags[1], options...) } else { - log.Debugf("Trying: Dial(): tcp %s", e.redisAddr) + slog.Debug("Trying to dial TCP", "address", e.redisAddr) c, err = redis.Dial("tcp", e.redisAddr, options...) } } @@ -112,27 +112,27 @@ func (e *Exporter) connectToRedisCluster() (redis.Conn, error) { } } - log.Debugf("Creating cluster object") + slog.Debug("Creating cluster object") cluster := redisc.Cluster{ StartupNodes: []string{uri}, DialOptions: options, } - log.Debugf("Running refresh on cluster object") + slog.Debug("Running refresh on cluster object") if err := cluster.Refresh(); err != nil { - log.Errorf("Cluster refresh failed: %v", err) + slog.Error("Cluster refresh failed", "error", err) return nil, fmt.Errorf("cluster refresh failed: %w", err) } - log.Debugf("Creating redis connection object") + slog.Debug("Creating redis connection object") conn, err := cluster.Dial() if err != nil { - log.Errorf("Dial failed: %v", err) + slog.Error("Dial failed", "error", err) return nil, fmt.Errorf("dial failed: %w", err) } c, err := redisc.RetryConn(conn, 10, 100*time.Millisecond) if err != nil { - log.Errorf("RetryConn failed: %v", err) + slog.Error("RetryConn failed", "error", err) return nil, fmt.Errorf("retryConn failed: %w", err) } @@ -140,11 +140,11 @@ func (e *Exporter) connectToRedisCluster() (redis.Conn, error) { } func doRedisCmd(c redis.Conn, cmd string, args ...interface{}) (interface{}, error) { - log.Debugf("c.Do() - running command: %s args: [%v]", cmd, args) + slog.Debug("c.Do() - running command", "cmd", cmd, "args", args) res, err := c.Do(cmd, args...) if err != nil { - log.Debugf("c.Do() - err: %s", err) + slog.Debug("Redis command failed", "error", err) } - log.Debugf("c.Do() - done") + slog.Debug("c.Do() - done") return res, err } diff --git a/exporter/sentinels.go b/exporter/sentinels.go index c54f8b86..39fa400d 100644 --- a/exporter/sentinels.go +++ b/exporter/sentinels.go @@ -1,13 +1,13 @@ package exporter import ( + "log/slog" "regexp" "strconv" "strings" "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) func (e *Exporter) handleMetricsSentinel(ch chan<- prometheus.Metric, fieldKey string, fieldValue string) { @@ -38,16 +38,16 @@ func (e *Exporter) handleMetricsSentinel(ch chan<- prometheus.Metric, fieldKey s func (e *Exporter) extractSentinelMetrics(ch chan<- prometheus.Metric, c redis.Conn) { masterDetails, err := redis.Values(doRedisCmd(c, "SENTINEL", "MASTERS")) if err != nil { - log.Debugf("Error getting sentinel master details %s:", err) + slog.Debug("Error getting sentinel master details", "error", err) return } - log.Debugf("Sentinel master details: %#v", masterDetails) + slog.Debug("Sentinel master details", "masterDetails", masterDetails) for _, masterDetail := range masterDetails { masterDetailMap, err := redis.StringMap(masterDetail, nil) if err != nil { - log.Debugf("Error getting masterDetailmap from masterDetail: %s, err: %s", masterDetail, err) + slog.Debug("Error getting masterDetailmap from masterDetail", "masterDetail", masterDetail, "error", err) continue } @@ -68,7 +68,7 @@ func (e *Exporter) extractSentinelMetrics(ch chan<- prometheus.Metric, c redis.C masterAddr := masterIp + ":" + masterPort masterCkquorumMsg, err := redis.String(doRedisCmd(c, "SENTINEL", "CKQUORUM", masterName)) - log.Debugf("Sentinel ckquorum status for master %s: %s %s", masterName, masterCkquorumMsg, err) + slog.Debug("Sentinel ckquorum status for master", "masterName", masterName, "msg", masterCkquorumMsg, "error", err) masterCkquorumStatus := 1 if err != nil { masterCkquorumStatus = 0 @@ -87,11 +87,11 @@ func (e *Exporter) extractSentinelMetrics(ch chan<- prometheus.Metric, c redis.C e.registerConstMetricGauge(ch, "sentinel_master_setting_down_after_milliseconds", masterDownAfterMs, masterName, masterAddr) sentinelDetails, _ := redis.Values(doRedisCmd(c, "SENTINEL", "SENTINELS", masterName)) - log.Debugf("Sentinel details for master %s: %s", masterName, sentinelDetails) + slog.Debug("Sentinel details for master", "masterName", masterName, "sentinelDetails", sentinelDetails) e.processSentinelSentinels(ch, sentinelDetails, masterName, masterAddr) slaveDetails, _ := redis.Values(doRedisCmd(c, "SENTINEL", "SLAVES", masterName)) - log.Debugf("Slave details for master %s: %s", masterName, slaveDetails) + slog.Debug("Slave details for master", "masterName", masterName, "slaveDetails", slaveDetails) e.processSentinelSlaves(ch, slaveDetails, masterName, masterAddr) } } @@ -104,7 +104,7 @@ func (e *Exporter) processSentinelSentinels(ch chan<- prometheus.Metric, sentine for _, sentinelDetail := range sentinelDetails { sentinelDetailMap, err := redis.StringMap(sentinelDetail, nil) if err != nil { - log.Debugf("Error getting sentinelDetailMap from sentinelDetail: %s, err: %s", sentinelDetail, err) + slog.Debug("Error getting sentinelDetailMap from sentinelDetail", "sentinelDetail", sentinelDetail, "error", err) continue } @@ -128,7 +128,7 @@ func (e *Exporter) processSentinelSlaves(ch chan<- prometheus.Metric, slaveDetai for _, slaveDetail := range slaveDetails { slaveDetailMap, err := redis.StringMap(slaveDetail, nil) if err != nil { - log.Debugf("Error getting slavedetailMap from slaveDetail: %s, err: %s", slaveDetail, err) + slog.Debug("Error getting slavedetailMap from slaveDetail", "slaveDetail", slaveDetail, "error", err) continue } @@ -162,7 +162,7 @@ func parseSentinelMasterString(master string, masterInfo string) (masterName str for _, kvPart := range strings.Split(masterInfo, ",") { x := strings.Split(kvPart, "=") if len(x) != 2 { - log.Errorf("Invalid format for sentinel's master string, got: %s", kvPart) + slog.Error("Invalid format for sentinel's master string", "kvPart", kvPart) continue } matchedMasterInfo[x[0]] = x[1] @@ -173,12 +173,12 @@ func parseSentinelMasterString(master string, masterInfo string) (masterName str masterAddr = matchedMasterInfo["address"] masterSlaves, err := strconv.ParseFloat(matchedMasterInfo["slaves"], 64) if err != nil { - log.Debugf("parseSentinelMasterString(): couldn't parse slaves value, got: %s, err: %s", matchedMasterInfo["slaves"], err) + slog.Debug("Failed to parse slaves value", "slaves", matchedMasterInfo["slaves"], "error", err) return } masterSentinels, err = strconv.ParseFloat(matchedMasterInfo["sentinels"], 64) if err != nil { - log.Debugf("parseSentinelMasterString(): couldn't parse sentinels value, got: %s, err: %s", matchedMasterInfo["sentinels"], err) + slog.Debug("Failed to parse sentinels value", "sentinels", matchedMasterInfo["sentinels"], "error", err) return } ok = true diff --git a/exporter/streams.go b/exporter/streams.go index 7e5dd1ac..488e6f32 100644 --- a/exporter/streams.go +++ b/exporter/streams.go @@ -1,12 +1,12 @@ package exporter import ( + "log/slog" "strconv" "strings" "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) // All fields of the streamInfo struct must be exported @@ -70,24 +70,24 @@ func getStreamInfo(c redis.Conn, key string) (*streamInfo, error) { return nil, err } - log.Debugf("getStreamInfo() stream: %#v", &stream) + slog.Debug("Retrieved stream info", "stream", &stream) return &stream, nil } func getStreamEntryId(redisValue []interface{}, index int) string { if values, ok := redisValue[index].([]interface{}); !ok || len(values) < 2 { - log.Debugf("Failed to parse StreamEntryId") + slog.Debug("Failed to parse StreamEntryId") return "" } if len(redisValue) < index || redisValue[index] == nil { - log.Debugf("Failed to parse StreamEntryId") + slog.Debug("Failed to parse StreamEntryId") return "" } entryId, ok := redisValue[index].([]interface{})[0].([]byte) if !ok { - log.Debugf("Failed to parse StreamEntryId") + slog.Debug("Failed to parse StreamEntryId") return "" } return string(entryId) @@ -103,14 +103,14 @@ func scanStreamGroups(c redis.Conn, stream string) ([]streamGroupsInfo, error) { for _, g := range groups { v, err := redis.Values(g, nil) if err != nil { - log.Errorf("Couldn't convert group values for stream '%s': %s", stream, err) + slog.Error("Couldn't convert group values for stream", "stream", stream, "error", err) continue } - log.Debugf("streamGroupsInfo value: %#v", v) + slog.Debug("streamGroupsInfo value", "value", v) var group streamGroupsInfo if err := redis.ScanStruct(v, &group); err != nil { - log.Errorf("Couldn't scan group in stream '%s': %s", stream, err) + slog.Error("Couldn't scan group in stream", "stream", stream, "error", err) continue } @@ -122,7 +122,7 @@ func scanStreamGroups(c redis.Conn, stream string) ([]streamGroupsInfo, error) { result = append(result, group) } - log.Debugf("groups: %v", result) + slog.Debug("groups", "result", result) return result, nil } @@ -137,21 +137,21 @@ func scanStreamGroupConsumers(c redis.Conn, stream string, group string) ([]stre v, err := redis.Values(c, nil) if err != nil { - log.Errorf("Couldn't convert consumer values for group '%s' in stream '%s': %s", group, stream, err) + slog.Error("Couldn't convert consumer values for group in stream", "group", group, "stream", stream, "error", err) continue } - log.Debugf("streamGroupConsumersInfo value: %#v", v) + slog.Debug("streamGroupConsumersInfo value", "value", v) var consumer streamGroupConsumersInfo if err := redis.ScanStruct(v, &consumer); err != nil { - log.Errorf("Couldn't scan consumers for group '%s' in stream '%s': %s", group, stream, err) + slog.Error("Couldn't scan consumers for group in stream", "group", group, "stream", stream, "error", err) continue } result = append(result, consumer) } - log.Debugf("consumers: %v", result) + slog.Debug("consumers", "result", result) return result, nil } @@ -161,12 +161,12 @@ func parseStreamItemId(id string) float64 { } frags := strings.Split(id, "-") if len(frags) == 0 { - log.Errorf("Couldn't parse StreamItemId: %s", id) + slog.Error("Couldn't parse StreamItemId", "id", id) return 0 } parsedId, err := strconv.ParseFloat(strings.Split(id, "-")[0], 64) if err != nil { - log.Errorf("Couldn't parse given StreamItemId: [%s] err: %s", id, err) + slog.Error("Couldn't parse given StreamItemId", "id", id, "error", err) } return parsedId } @@ -174,33 +174,33 @@ func parseStreamItemId(id string) float64 { func (e *Exporter) extractStreamMetrics(ch chan<- prometheus.Metric, c redis.Conn) { streams, err := parseKeyArg(e.options.CheckStreams) if err != nil { - log.Errorf("Couldn't parse given stream keys: %s", err) + slog.Error("Couldn't parse given stream keys", "error", err) return } singleStreams, err := parseKeyArg(e.options.CheckSingleStreams) if err != nil { - log.Errorf("Couldn't parse check-single-streams: %s", err) + slog.Error("Couldn't parse check-single-streams", "error", err) return } allStreams := append([]dbKeyPair{}, singleStreams...) scannedStreams, err := getKeysFromPatterns(c, streams, e.options.CheckKeysBatchSize) if err != nil { - log.Errorf("Error expanding key patterns: %s", err) + slog.Error("Error expanding key patterns", "error", err) } else { allStreams = append(allStreams, scannedStreams...) } - log.Debugf("allStreams: %#v", allStreams) + slog.Debug("allStreams", "allStreams", allStreams) for _, k := range allStreams { if _, err := doRedisCmd(c, "SELECT", k.db); err != nil { - log.Debugf("Couldn't select database '%s' when getting stream info", k.db) + slog.Debug("Couldn't select database when getting stream info", "db", k.db) continue } info, err := getStreamInfo(c, k.key) if err != nil { - log.Errorf("couldn't get info for stream '%s', err: %s", k.key, err) + slog.Error("couldn't get info for stream", "stream", k.key, "error", err) continue } dbLabel := "db" + k.db diff --git a/exporter/streams_test.go b/exporter/streams_test.go index f6e69cff..6934eaf5 100644 --- a/exporter/streams_test.go +++ b/exporter/streams_test.go @@ -1,6 +1,7 @@ package exporter import ( + "log/slog" "net/http/httptest" "os" "strings" @@ -8,7 +9,6 @@ import ( "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) type scanStreamFixture struct { @@ -532,8 +532,8 @@ func TestStreamsExtractStreamMetrics(t *testing.T) { for m := range chM { for k := range want { - log.Debugf("metric: %s", m.Desc().String()) - log.Debugf("want: %s", k) + slog.Debug("metric", "desc", m.Desc().String()) + slog.Debug("want", "key", k) if strings.Contains(m.Desc().String(), k) { want[k] = true @@ -593,16 +593,16 @@ func TestStreamsExtractStreamMetricsExcludeConsumer(t *testing.T) { for m := range chM { for k := range want { - log.Debugf("metric: %s", m.Desc().String()) - log.Debugf("want: %s", k) + slog.Debug("metric", "desc", m.Desc().String()) + slog.Debug("want", "key", k) if strings.Contains(m.Desc().String(), k) { want[k] = true } } for k := range dontWant { - log.Debugf("metric: %s", m.Desc().String()) - log.Debugf("don't want: %s", k) + slog.Debug("metric", "desc", m.Desc().String()) + slog.Debug("don't want", "key", k) if strings.Contains(m.Desc().String(), k) { dontWant[k] = true diff --git a/exporter/tile38.go b/exporter/tile38.go index 9c93c606..7a570bba 100644 --- a/exporter/tile38.go +++ b/exporter/tile38.go @@ -1,17 +1,17 @@ package exporter import ( + "log/slog" "strings" "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" ) func (e *Exporter) extractTile38Metrics(ch chan<- prometheus.Metric, c redis.Conn) { info, err := redis.Strings(doRedisCmd(c, "SERVER", "EXT")) if err != nil { - log.Errorf("extractTile38Metrics() err: %s", err) + slog.Error("Failed to extract Tile38 metrics", "error", err) return } @@ -22,7 +22,7 @@ func (e *Exporter) extractTile38Metrics(ch chan<- prometheus.Metric, c redis.Con } fieldValue := info[i+1] - log.Debugf("tile38 key:%s val:%s", fieldKey, fieldValue) + slog.Debug("tile38", "key", fieldKey, "val", fieldValue) if !e.includeMetric(fieldKey) { continue diff --git a/exporter/tls.go b/exporter/tls.go index 0dc84642..3b75fca8 100644 --- a/exporter/tls.go +++ b/exporter/tls.go @@ -4,9 +4,8 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "log/slog" "os" - - log "github.com/sirupsen/logrus" ) // CreateClientTLSConfig verifies configured files and return a prepared tls.Config @@ -107,7 +106,7 @@ func GetConfigForClientFunc(certFile, keyFile, caCertFile string) func(*tls.Clie // LoadKeyPair reads and parses a public/private key pair from a pair of files. // The files must contain PEM encoded data. func LoadKeyPair(certFile, keyFile string) (*tls.Certificate, error) { - log.Debugf("Load key pair: %s %s", certFile, keyFile) + slog.Debug("Load key pair", "cert", certFile, "key", keyFile) cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, err @@ -118,7 +117,7 @@ func LoadKeyPair(certFile, keyFile string) (*tls.Certificate, error) { // LoadCAFile reads and parses CA certificates from a file into a pool. // The file must contain PEM encoded data. func LoadCAFile(caFile string) (*x509.CertPool, error) { - log.Debugf("Load CA cert file: %s", caFile) + slog.Debug("Load CA cert file", "file", caFile) pemCerts, err := os.ReadFile(caFile) if err != nil { return nil, err diff --git a/go.mod b/go.mod index f625a82d..41d5eee0 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/mna/redisc v1.4.0 github.com/prometheus/client_golang v1.23.0 github.com/prometheus/client_model v0.6.2 - github.com/sirupsen/logrus v1.9.3 ) require ( diff --git a/go.sum b/go.sum index 435bb6a9..8752fcca 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,6 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -37,7 +35,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/main.go b/main.go index 8ee17cdb..533fe090 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,8 @@ import ( "context" "errors" "flag" + "fmt" + "log/slog" "net/http" "os" "os/signal" @@ -15,7 +17,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" - log "github.com/sirupsen/logrus" "github.com/oliver006/redis_exporter/exporter" ) @@ -27,6 +28,8 @@ var ( BuildVersion = "<<< filled in by build >>>" BuildDate = "<<< filled in by build >>>" BuildCommitSha = "<<< filled in by build >>>" + + logger *slog.Logger ) func getEnv(key string, defaultVal string) string { @@ -56,30 +59,6 @@ func getEnvInt64(key string, defaultVal int64) int64 { return defaultVal } -// parseLogLevel parses a log level string and returns the corresponding logrus level -func parseLogLevel(level string) (log.Level, error) { - switch strings.ToUpper(level) { - case "DEBUG": - return log.DebugLevel, nil - case "INFO": - return log.InfoLevel, nil - case "WARN", "WARNING": - return log.WarnLevel, nil - case "ERROR": - return log.ErrorLevel, nil - default: - return log.InfoLevel, errors.New("invalid log level: " + level) - } -} - -// validateTLSClientConfig validates TLS client configuration -func validateTLSClientConfig(certFile, keyFile string) error { - if (certFile != "") != (keyFile != "") { - return errors.New("TLS client key file and cert file should both be present") - } - return nil -} - // loadScripts loads Lua scripts from the provided script paths func loadScripts(scriptPath string) (map[string][]byte, error) { if scriptPath == "" { @@ -100,29 +79,6 @@ func loadScripts(scriptPath string) (map[string][]byte, error) { return ls, nil } -// setupLogging configures logging based on the provided parameters -func setupLogging(isDebug bool, logLevel, logFormat string) error { - switch logFormat { - case "json": - log.SetFormatter(&log.JSONFormatter{}) - default: - log.SetFormatter(&log.TextFormatter{}) - } - - lvl := log.InfoLevel - if isDebug { - lvl = log.DebugLevel - } else { - parsedLvl, err := parseLogLevel(logLevel) - if err == nil { - lvl = parsedLvl - } - } - - log.SetLevel(lvl) - return nil -} - // createPrometheusRegistry creates and configures a Prometheus registry func createPrometheusRegistry(redisMetricsOnly bool) *prometheus.Registry { registry := prometheus.NewRegistry() @@ -173,7 +129,7 @@ func main() { isCluster = flag.Bool("is-cluster", getEnvBool("REDIS_EXPORTER_IS_CLUSTER", false), "Whether this is a redis cluster (Enable this if you need to fetch key level data on a Redis Cluster).") exportClientList = flag.Bool("export-client-list", getEnvBool("REDIS_EXPORTER_EXPORT_CLIENT_LIST", false), "Whether to scrape Client List specific metrics") exportClientPort = flag.Bool("export-client-port", getEnvBool("REDIS_EXPORTER_EXPORT_CLIENT_PORT", false), "Whether to include the client's port when exporting the client list. Warning: including the port increases the number of metrics generated and will make your Prometheus server take up more memory") - showVersion = flag.Bool("version", false, "Show version information and exit") + showVersionAndExit = flag.Bool("version", false, "Show version information and exit") redisMetricsOnly = flag.Bool("redis-only-metrics", getEnvBool("REDIS_EXPORTER_REDIS_ONLY_METRICS", false), "Whether to also export go runtime metrics") pingOnConnect = flag.Bool("ping-on-connect", getEnvBool("REDIS_EXPORTER_PING_ON_CONNECT", false), "Whether to ping the redis instance after connecting") inclConfigMetrics = flag.Bool("include-config-metrics", getEnvBool("REDIS_EXPORTER_INCL_CONFIG_METRICS", false), "Whether to include all config settings as metrics") @@ -190,43 +146,62 @@ func main() { ) flag.Parse() - if *showVersion { - log.SetOutput(os.Stdout) + // + // parse and set log level, first check for --debug flag, then check if log level is set explicitly + // + var lvl slog.Level + var err error + if *isDebug { + lvl = slog.LevelDebug + } else if *showVersionAndExit { + lvl = slog.LevelInfo + } else if lvl, err = exporter.ParseLogLevel(*logLevel); err != nil { + fmt.Printf("Invalid log level: %s\n", *logLevel) + os.Exit(1) } - log.Printf("Redis Metrics Exporter %s build date: %s sha1: %s Go: %s GOOS: %s GOARCH: %s", - BuildVersion, BuildDate, BuildCommitSha, - runtime.Version(), - runtime.GOOS, - runtime.GOARCH, - ) - if *showVersion { - return + + // Setup logger handler based on format + var handler slog.Handler + switch *logFormat { + case "json": + handler = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}) + default: + handler = slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}) } - if err := setupLogging(*isDebug, *logLevel, *logFormat); err != nil { - log.Fatalf("Failed to setup logging: %v", err) + logger = slog.New(handler) + slog.SetDefault(logger) + + slog.Info("Redis Metrics Exporter", "version", BuildVersion, "build_date", BuildDate, "commit_sha", BuildCommitSha, "go_version", runtime.Version(), "goos", runtime.GOOS, "goarch", runtime.GOARCH) + if *showVersionAndExit { + return } + + slog.Info("Setting log level", "level", lvl.String()) + if *isDebug { - log.Debugln("Enabling debug output") + slog.Debug("Debug logging enabled") } - log.Infof(`Setting log level to "%s"`, log.GetLevel().String()) to, err := time.ParseDuration(*connectionTimeout) if err != nil { - log.Fatalf("Couldn't parse connection timeout duration, err: %s", err) + slog.Error("Couldn't parse connection timeout duration", "error", err) + os.Exit(1) } passwordMap := make(map[string]string) if *redisPwd == "" && *redisPwdFile != "" { passwordMap, err = exporter.LoadPwdFile(*redisPwdFile) if err != nil { - log.Fatalf("Error loading redis passwords from file %s, err: %s", *redisPwdFile, err) + slog.Error("Error loading redis passwords from file", "file", *redisPwdFile, "error", err) + os.Exit(1) } } ls, err := loadScripts(*scriptPath) if err != nil { - log.Fatalf("Error loading script files: %s", err) + slog.Error("Error loading script files", "scriptPath", scriptPath, "error", err) + os.Exit(1) } registry := createPrometheusRegistry(*redisMetricsOnly) @@ -282,39 +257,45 @@ func main() { }, ) if err != nil { - log.Fatal(err) + slog.Error("Failed to create Redis exporter", "error", err) + os.Exit(1) } // Verify that initial client keypair and CA are accepted - if err := validateTLSClientConfig(*tlsClientCertFile, *tlsClientKeyFile); err != nil { - log.Fatal(err) + if (*tlsClientCertFile != "") != (*tlsClientKeyFile != "") { + slog.Error("TLS client key file and cert file should both be present") + os.Exit(1) } _, err = exp.CreateClientTLSConfig() if err != nil { - log.Fatal(err) + slog.Error("Failed to create client TLS config", "error", err) + os.Exit(1) } - log.Infof("Providing metrics at %s%s", *listenAddress, *metricPath) - log.Debugf("Configured redis addr: %#v", *redisAddr) + slog.Info("Providing metrics", "address", *listenAddress, "path", *metricPath) + slog.Debug("Configured redis address", "address", *redisAddr) server := &http.Server{ Addr: *listenAddress, Handler: exp, } go func() { if *tlsServerCertFile != "" && *tlsServerKeyFile != "" { - log.Debugf("Bind as TLS using cert %s and key %s", *tlsServerCertFile, *tlsServerKeyFile) + slog.Debug("Starting TLS server", "cert_file", *tlsServerCertFile, "key_file", *tlsServerKeyFile) tlsConfig, err := exp.CreateServerTLSConfig(*tlsServerCertFile, *tlsServerKeyFile, *tlsServerCaCertFile, *tlsServerMinVersion) if err != nil { - log.Fatal(err) + slog.Error("Failed to create server TLS config", "error", err) + os.Exit(1) } server.TLSConfig = tlsConfig if err := server.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("TLS Server error: %v", err) + slog.Error("TLS Server error", "error", err) + os.Exit(1) } } else { if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("Server error: %v", err) + slog.Error("Server error", "error", err) + os.Exit(1) } } }() @@ -323,14 +304,15 @@ func main() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) _quit := <-quit - log.Infof("Received %s signal, exiting", _quit.String()) + slog.Info("Received signal, exiting", "signal", _quit.String()) // Create a context with a timeout ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Shutdown the HTTP server gracefully if err := server.Shutdown(ctx); err != nil { - log.Fatalf("Server shutdown failed: %v", err) + slog.Error("Server shutdown failed", "error", err) + os.Exit(1) } - log.Infof("Server shut down gracefully") + slog.Info("Server shut down gracefully") } diff --git a/main_test.go b/main_test.go index 3343fbf4..3e4f2955 100644 --- a/main_test.go +++ b/main_test.go @@ -5,8 +5,6 @@ import ( "path/filepath" "strings" "testing" - - log "github.com/sirupsen/logrus" ) func TestGetEnv(t *testing.T) { @@ -212,79 +210,6 @@ func TestGetEnvInt64(t *testing.T) { } } -func TestParseLogLevel(t *testing.T) { - tests := []struct { - name string - level string - expected log.Level - expectError bool - }{ - {"debug level", "debug", log.DebugLevel, false}, - {"DEBUG level", "DEBUG", log.DebugLevel, false}, - {"info level", "info", log.InfoLevel, false}, - {"INFO level", "INFO", log.InfoLevel, false}, - {"warn level", "warn", log.WarnLevel, false}, - {"WARN level", "WARN", log.WarnLevel, false}, - {"warning level", "warning", log.WarnLevel, false}, - {"WARNING level", "WARNING", log.WarnLevel, false}, - {"error level", "error", log.ErrorLevel, false}, - {"ERROR level", "ERROR", log.ErrorLevel, false}, - {"invalid level", "invalid", log.InfoLevel, true}, - {"empty level", "", log.InfoLevel, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := parseLogLevel(tt.level) - if tt.expectError { - if err == nil { - t.Errorf("parseLogLevel(%s) expected error but got none", tt.level) - } - // For invalid levels, we still expect InfoLevel as default - if result != tt.expected { - t.Errorf("parseLogLevel(%s) = %v, expected %v", tt.level, result, tt.expected) - } - } else { - if err != nil { - t.Errorf("parseLogLevel(%s) unexpected error: %v", tt.level, err) - } - if result != tt.expected { - t.Errorf("parseLogLevel(%s) = %v, expected %v", tt.level, result, tt.expected) - } - } - }) - } -} - -func TestValidateTLSClientConfig(t *testing.T) { - tests := []struct { - name string - certFile string - keyFile string - expectError bool - }{ - {"both files provided", "/path/to/cert.pem", "/path/to/key.pem", false}, - {"both files empty", "", "", false}, - {"only cert file provided", "/path/to/cert.pem", "", true}, - {"only key file provided", "", "/path/to/key.pem", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateTLSClientConfig(tt.certFile, tt.keyFile) - if tt.expectError { - if err == nil { - t.Errorf("validateTLSClientConfig(%s, %s) expected error but got none", tt.certFile, tt.keyFile) - } - } else { - if err != nil { - t.Errorf("validateTLSClientConfig(%s, %s) unexpected error: %v", tt.certFile, tt.keyFile, err) - } - } - }) - } -} - func TestLoadScripts(t *testing.T) { // Create temporary directory for test scripts tmpDir := t.TempDir() @@ -353,53 +278,6 @@ func TestLoadScripts(t *testing.T) { } } -func TestSetupLogging(t *testing.T) { - // Save original log level to restore after tests - originalLevel := log.GetLevel() - defer log.SetLevel(originalLevel) - - tests := []struct { - name string - isDebug bool - logLevel string - logFormat string - expectedLevel log.Level - }{ - {"debug enabled", true, "info", "txt", log.DebugLevel}, - {"info level", false, "info", "txt", log.InfoLevel}, - {"warn level", false, "warn", "txt", log.WarnLevel}, - {"error level", false, "error", "txt", log.ErrorLevel}, - {"invalid level defaults to info", false, "invalid", "txt", log.InfoLevel}, - {"json format", false, "info", "json", log.InfoLevel}, - {"text format", false, "info", "txt", log.InfoLevel}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := setupLogging(tt.isDebug, tt.logLevel, tt.logFormat) - if err != nil { - t.Errorf("setupLogging() unexpected error: %v", err) - } - - if log.GetLevel() != tt.expectedLevel { - t.Errorf("setupLogging() level = %v, expected %v", log.GetLevel(), tt.expectedLevel) - } - - // Check formatter type (basic check) - formatter := log.StandardLogger().Formatter - if tt.logFormat == "json" { - if _, ok := formatter.(*log.JSONFormatter); !ok { - t.Errorf("setupLogging() expected JSONFormatter for json format") - } - } else { - if _, ok := formatter.(*log.TextFormatter); !ok { - t.Errorf("setupLogging() expected TextFormatter for txt format") - } - } - }) - } -} - func TestCreatePrometheusRegistry(t *testing.T) { tests := []struct { name string @@ -447,19 +325,6 @@ func TestMainFunctionsIntegration(t *testing.T) { t.Fatalf("Failed to create test script: %v", err) } - // Test logging setup - originalLevel := log.GetLevel() - defer log.SetLevel(originalLevel) - - err := setupLogging(false, "debug", "json") - if err != nil { - t.Errorf("setupLogging failed: %v", err) - } - - if log.GetLevel() != log.DebugLevel { - t.Errorf("Expected debug level, got %v", log.GetLevel()) - } - // Test script loading scripts, err := loadScripts(scriptFile) if err != nil { @@ -474,15 +339,6 @@ func TestMainFunctionsIntegration(t *testing.T) { t.Errorf("Script content mismatch") } - // Test TLS validation - if err := validateTLSClientConfig("/cert.pem", ""); err == nil { - t.Error("Expected TLS validation error for mismatched cert/key") - } - - if err := validateTLSClientConfig("/cert.pem", "/key.pem"); err != nil { - t.Errorf("Unexpected TLS validation error: %v", err) - } - // Test registry creation registry := createPrometheusRegistry(true) if registry == nil {