• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oliver006 / redis_exporter / 17172001870

23 Aug 2025 05:58AM UTC coverage: 84.921% (-0.9%) from 85.87%
17172001870

Pull #1028

github

web-flow
Merge 891f7f01e into 7632b7b20
Pull Request #1028: sirupsen/log --> log/slog

121 of 249 new or added lines in 18 files covered. (48.59%)

6 existing lines in 1 file now uncovered.

2568 of 3024 relevant lines covered (84.92%)

13254.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

17.67
/main.go
1
package main
2

3
import (
4
        "context"
5
        "errors"
6
        "flag"
7
        "fmt"
8
        "log/slog"
9
        "net/http"
10
        "os"
11
        "os/signal"
12
        "runtime"
13
        "strconv"
14
        "strings"
15
        "syscall"
16
        "time"
17

18
        "github.com/prometheus/client_golang/prometheus"
19
        "github.com/prometheus/client_golang/prometheus/collectors"
20

21
        "github.com/oliver006/redis_exporter/exporter"
22
)
23

24
var (
25
        /*
26
                BuildVersion, BuildDate, BuildCommitSha are filled in by the build script
27
        */
28
        BuildVersion   = "<<< filled in by build >>>"
29
        BuildDate      = "<<< filled in by build >>>"
30
        BuildCommitSha = "<<< filled in by build >>>"
31

32
        logger *slog.Logger
33
)
34

35
func getEnv(key string, defaultVal string) string {
3✔
36
        if envVal, ok := os.LookupEnv(key); ok {
5✔
37
                return envVal
2✔
38
        }
2✔
39
        return defaultVal
1✔
40
}
41

42
func getEnvBool(key string, defaultVal bool) bool {
6✔
43
        if envVal, ok := os.LookupEnv(key); ok {
11✔
44
                envBool, err := strconv.ParseBool(envVal)
5✔
45
                if err == nil {
9✔
46
                        return envBool
4✔
47
                }
4✔
48
        }
49
        return defaultVal
2✔
50
}
51

52
func getEnvInt64(key string, defaultVal int64) int64 {
7✔
53
        if envVal, ok := os.LookupEnv(key); ok {
13✔
54
                envInt64, err := strconv.ParseInt(envVal, 10, 64)
6✔
55
                if err == nil {
10✔
56
                        return envInt64
4✔
57
                }
4✔
58
        }
59
        return defaultVal
3✔
60
}
61

62
// loadScripts loads Lua scripts from the provided script paths
63
func loadScripts(scriptPath string) (map[string][]byte, error) {
6✔
64
        if scriptPath == "" {
7✔
65
                return nil, nil
1✔
66
        }
1✔
67

68
        scripts := strings.Split(scriptPath, ",")
5✔
69
        ls := make(map[string][]byte, len(scripts))
5✔
70

5✔
71
        for _, script := range scripts {
12✔
72
                scriptContent, err := os.ReadFile(script)
7✔
73
                if err != nil {
9✔
74
                        return nil, err
2✔
75
                }
2✔
76
                ls[script] = scriptContent
5✔
77
        }
78

79
        return ls, nil
3✔
80
}
81

82
// createPrometheusRegistry creates and configures a Prometheus registry
83
func createPrometheusRegistry(redisMetricsOnly bool) *prometheus.Registry {
3✔
84
        registry := prometheus.NewRegistry()
3✔
85
        if !redisMetricsOnly {
4✔
86
                registry.MustRegister(
1✔
87
                        // expose process metrics like CPU, Memory, file descriptor usage etc.
1✔
88
                        collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
1✔
89
                        // expose all Go runtime metrics like GC stats, memory stats etc.
1✔
90
                        collectors.NewGoCollector(collectors.WithGoCollectorRuntimeMetrics(collectors.MetricsAll)),
1✔
91
                )
1✔
92
        }
1✔
93
        return registry
3✔
94
}
95

96
func main() {
×
97
        var (
×
98
                redisAddr                      = flag.String("redis.addr", getEnv("REDIS_ADDR", "redis://localhost:6379"), "Address of the Redis instance to scrape")
×
99
                redisUser                      = flag.String("redis.user", getEnv("REDIS_USER", ""), "User name to use for authentication (Redis ACL for Redis 6.0 and newer)")
×
100
                redisPwd                       = flag.String("redis.password", getEnv("REDIS_PASSWORD", ""), "Password of the Redis instance to scrape")
×
101
                redisPwdFile                   = flag.String("redis.password-file", getEnv("REDIS_PASSWORD_FILE", ""), "Password file of the Redis instance to scrape")
×
102
                namespace                      = flag.String("namespace", getEnv("REDIS_EXPORTER_NAMESPACE", "redis"), "Namespace for metrics")
×
103
                checkKeys                      = flag.String("check-keys", getEnv("REDIS_EXPORTER_CHECK_KEYS", ""), "Comma separated list of key-patterns to export value and length/size, searched for with SCAN")
×
104
                checkSingleKeys                = flag.String("check-single-keys", getEnv("REDIS_EXPORTER_CHECK_SINGLE_KEYS", ""), "Comma separated list of single keys to export value and length/size")
×
105
                checkKeyGroups                 = flag.String("check-key-groups", getEnv("REDIS_EXPORTER_CHECK_KEY_GROUPS", ""), "Comma separated list of lua regex for grouping keys")
×
106
                checkStreams                   = flag.String("check-streams", getEnv("REDIS_EXPORTER_CHECK_STREAMS", ""), "Comma separated list of stream-patterns to export info about streams, groups and consumers, searched for with SCAN")
×
107
                checkSingleStreams             = flag.String("check-single-streams", getEnv("REDIS_EXPORTER_CHECK_SINGLE_STREAMS", ""), "Comma separated list of single streams to export info about streams, groups and consumers")
×
108
                streamsExcludeConsumerMetrics  = flag.Bool("streams-exclude-consumer-metrics", getEnvBool("REDIS_EXPORTER_STREAMS_EXCLUDE_CONSUMER_METRICS", false), "Don't collect per consumer metrics for streams (decreases cardinality)")
×
109
                countKeys                      = flag.String("count-keys", getEnv("REDIS_EXPORTER_COUNT_KEYS", ""), "Comma separated list of patterns to count (eg: 'db0=production_*,db3=sessions:*'), searched for with SCAN")
×
110
                checkKeysBatchSize             = flag.Int64("check-keys-batch-size", getEnvInt64("REDIS_EXPORTER_CHECK_KEYS_BATCH_SIZE", 1000), "Approximate number of keys to process in each execution, larger value speeds up scanning.\nWARNING: Still Redis is a single-threaded app, huge COUNT can affect production environment.")
×
111
                scriptPath                     = flag.String("script", getEnv("REDIS_EXPORTER_SCRIPT", ""), "Comma separated list of path(s) to Redis Lua script(s) for gathering extra metrics")
×
112
                listenAddress                  = flag.String("web.listen-address", getEnv("REDIS_EXPORTER_WEB_LISTEN_ADDRESS", ":9121"), "Address to listen on for web interface and telemetry.")
×
113
                metricPath                     = flag.String("web.telemetry-path", getEnv("REDIS_EXPORTER_WEB_TELEMETRY_PATH", "/metrics"), "Path under which to expose metrics.")
×
114
                configCommand                  = flag.String("config-command", getEnv("REDIS_EXPORTER_CONFIG_COMMAND", "CONFIG"), "What to use for the CONFIG command, set to \"-\" to skip config metrics extraction")
×
115
                connectionTimeout              = flag.String("connection-timeout", getEnv("REDIS_EXPORTER_CONNECTION_TIMEOUT", "15s"), "Timeout for connection to Redis instance")
×
116
                tlsClientKeyFile               = flag.String("tls-client-key-file", getEnv("REDIS_EXPORTER_TLS_CLIENT_KEY_FILE", ""), "Name of the client key file (including full path) if the server requires TLS client authentication")
×
117
                tlsClientCertFile              = flag.String("tls-client-cert-file", getEnv("REDIS_EXPORTER_TLS_CLIENT_CERT_FILE", ""), "Name of the client certificate file (including full path) if the server requires TLS client authentication")
×
118
                tlsCaCertFile                  = flag.String("tls-ca-cert-file", getEnv("REDIS_EXPORTER_TLS_CA_CERT_FILE", ""), "Name of the CA certificate file (including full path) if the server requires TLS client authentication")
×
119
                tlsServerKeyFile               = flag.String("tls-server-key-file", getEnv("REDIS_EXPORTER_TLS_SERVER_KEY_FILE", ""), "Name of the server key file (including full path) if the web interface and telemetry should use TLS")
×
120
                tlsServerCertFile              = flag.String("tls-server-cert-file", getEnv("REDIS_EXPORTER_TLS_SERVER_CERT_FILE", ""), "Name of the server certificate file (including full path) if the web interface and telemetry should use TLS")
×
121
                tlsServerCaCertFile            = flag.String("tls-server-ca-cert-file", getEnv("REDIS_EXPORTER_TLS_SERVER_CA_CERT_FILE", ""), "Name of the CA certificate file (including full path) if the web interface and telemetry should require TLS client authentication")
×
122
                tlsServerMinVersion            = flag.String("tls-server-min-version", getEnv("REDIS_EXPORTER_TLS_SERVER_MIN_VERSION", "TLS1.2"), "Minimum TLS version that is acceptable by the web interface and telemetry when using TLS")
×
123
                maxDistinctKeyGroups           = flag.Int64("max-distinct-key-groups", getEnvInt64("REDIS_EXPORTER_MAX_DISTINCT_KEY_GROUPS", 100), "The maximum number of distinct key groups with the most memory utilization to present as distinct metrics per database, the leftover key groups will be aggregated in the 'overflow' bucket")
×
124
                isDebug                        = flag.Bool("debug", getEnvBool("REDIS_EXPORTER_DEBUG", false), "Output verbose debug information (sets log level to DEBUG, takes precedence over \"--log-level\")")
×
125
                logLevel                       = flag.String("log-level", getEnv("REDIS_EXPORTER_LOG_LEVEL", "INFO"), "Set log level")
×
126
                logFormat                      = flag.String("log-format", getEnv("REDIS_EXPORTER_LOG_FORMAT", "txt"), "Log format, valid options are txt and json")
×
127
                setClientName                  = flag.Bool("set-client-name", getEnvBool("REDIS_EXPORTER_SET_CLIENT_NAME", true), "Whether to set client name to redis_exporter")
×
128
                isTile38                       = flag.Bool("is-tile38", getEnvBool("REDIS_EXPORTER_IS_TILE38", false), "Whether to scrape Tile38 specific metrics")
×
129
                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).")
×
130
                exportClientList               = flag.Bool("export-client-list", getEnvBool("REDIS_EXPORTER_EXPORT_CLIENT_LIST", false), "Whether to scrape Client List specific metrics")
×
131
                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")
×
NEW
132
                showVersionAndExit             = flag.Bool("version", false, "Show version information and exit")
×
133
                redisMetricsOnly               = flag.Bool("redis-only-metrics", getEnvBool("REDIS_EXPORTER_REDIS_ONLY_METRICS", false), "Whether to also export go runtime metrics")
×
134
                pingOnConnect                  = flag.Bool("ping-on-connect", getEnvBool("REDIS_EXPORTER_PING_ON_CONNECT", false), "Whether to ping the redis instance after connecting")
×
135
                inclConfigMetrics              = flag.Bool("include-config-metrics", getEnvBool("REDIS_EXPORTER_INCL_CONFIG_METRICS", false), "Whether to include all config settings as metrics")
×
136
                inclModulesMetrics             = flag.Bool("include-modules-metrics", getEnvBool("REDIS_EXPORTER_INCL_MODULES_METRICS", false), "Whether to collect Redis Modules metrics")
×
137
                disableExportingKeyValues      = flag.Bool("disable-exporting-key-values", getEnvBool("REDIS_EXPORTER_DISABLE_EXPORTING_KEY_VALUES", false), "Whether to disable values of keys stored in redis as labels or not when using check-keys/check-single-key")
×
138
                excludeLatencyHistogramMetrics = flag.Bool("exclude-latency-histogram-metrics", getEnvBool("REDIS_EXPORTER_EXCLUDE_LATENCY_HISTOGRAM_METRICS", false), "Do not try to collect latency histogram metrics")
×
139
                redactConfigMetrics            = flag.Bool("redact-config-metrics", getEnvBool("REDIS_EXPORTER_REDACT_CONFIG_METRICS", true), "Whether to redact config settings that include potentially sensitive information like passwords")
×
140
                inclSystemMetrics              = flag.Bool("include-system-metrics", getEnvBool("REDIS_EXPORTER_INCL_SYSTEM_METRICS", false), "Whether to include system metrics like e.g. redis_total_system_memory_bytes")
×
141
                skipTLSVerification            = flag.Bool("skip-tls-verification", getEnvBool("REDIS_EXPORTER_SKIP_TLS_VERIFICATION", false), "Whether to to skip TLS verification")
×
142
                skipCheckKeysForRoleMaster     = flag.Bool("skip-checkkeys-for-role-master", getEnvBool("REDIS_EXPORTER_SKIP_CHECKKEYS_FOR_ROLE_MASTER", false), "Whether to skip gathering the check-keys metrics (size, val) when the instance is of type master (reduce load on master nodes)")
×
143
                basicAuthUsername              = flag.String("basic-auth-username", getEnv("REDIS_EXPORTER_BASIC_AUTH_USERNAME", ""), "Username for basic authentication")
×
144
                basicAuthPassword              = flag.String("basic-auth-password", getEnv("REDIS_EXPORTER_BASIC_AUTH_PASSWORD", ""), "Password for basic authentication")
×
145
                inclMetricsForEmptyDatabases   = flag.Bool("include-metrics-for-empty-databases", getEnvBool("REDIS_EXPORTER_INCL_METRICS_FOR_EMPTY_DATABASES", true), "Whether to emit db metrics (like db_keys) for empty databases")
×
146
        )
×
147
        flag.Parse()
×
148

×
NEW
149
        //
×
NEW
150
        // parse and set log level, first check for --debug flag, then check if log level is set explicitly
×
NEW
151
        //
×
NEW
152
        var lvl slog.Level
×
NEW
153
        var err error
×
NEW
154
        if *isDebug {
×
NEW
155
                lvl = slog.LevelDebug
×
NEW
156
        } else if *showVersionAndExit {
×
NEW
157
                lvl = slog.LevelInfo
×
NEW
158
        } else if lvl, err = exporter.ParseLogLevel(*logLevel); err != nil {
×
NEW
159
                fmt.Printf("Invalid log level: %s\n", *logLevel)
×
NEW
160
                os.Exit(1)
×
161
        }
×
162

163
        // Setup logger handler based on format
NEW
164
        var handler slog.Handler
×
NEW
165
        switch *logFormat {
×
NEW
166
        case "json":
×
NEW
167
                handler = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})
×
NEW
168
        default:
×
NEW
169
                handler = slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})
×
170
        }
171

NEW
172
        logger = slog.New(handler)
×
NEW
173
        slog.SetDefault(logger)
×
NEW
174

×
NEW
175
        slog.Info("Redis Metrics Exporter", "version", BuildVersion, "build_date", BuildDate, "commit_sha", BuildCommitSha, "go_version", runtime.Version(), "goos", runtime.GOOS, "goarch", runtime.GOARCH)
×
NEW
176
        if *showVersionAndExit {
×
NEW
177
                return
×
178
        }
×
179

NEW
180
        slog.Info("Setting log level", "level", lvl.String())
×
NEW
181

×
182
        if *isDebug {
×
NEW
183
                slog.Debug("Debug logging enabled")
×
184
        }
×
185

186
        to, err := time.ParseDuration(*connectionTimeout)
×
187
        if err != nil {
×
NEW
188
                slog.Error("Couldn't parse connection timeout duration", "error", err)
×
NEW
189
                os.Exit(1)
×
UNCOV
190
        }
×
191

192
        passwordMap := make(map[string]string)
×
193
        if *redisPwd == "" && *redisPwdFile != "" {
×
194
                passwordMap, err = exporter.LoadPwdFile(*redisPwdFile)
×
195
                if err != nil {
×
NEW
196
                        slog.Error("Error loading redis passwords from file", "file", *redisPwdFile, "error", err)
×
NEW
197
                        os.Exit(1)
×
UNCOV
198
                }
×
199
        }
200

201
        ls, err := loadScripts(*scriptPath)
×
202
        if err != nil {
×
NEW
203
                slog.Error("Error loading script files", "scriptPath", scriptPath, "error", err)
×
NEW
204
                os.Exit(1)
×
UNCOV
205
        }
×
206

207
        registry := createPrometheusRegistry(*redisMetricsOnly)
×
208

×
209
        exp, err := exporter.NewRedisExporter(
×
210
                *redisAddr,
×
211
                exporter.Options{
×
212
                        User:                           *redisUser,
×
213
                        Password:                       *redisPwd,
×
214
                        PasswordMap:                    passwordMap,
×
215
                        Namespace:                      *namespace,
×
216
                        ConfigCommandName:              *configCommand,
×
217
                        CheckKeys:                      *checkKeys,
×
218
                        CheckSingleKeys:                *checkSingleKeys,
×
219
                        CheckKeysBatchSize:             *checkKeysBatchSize,
×
220
                        CheckKeyGroups:                 *checkKeyGroups,
×
221
                        MaxDistinctKeyGroups:           *maxDistinctKeyGroups,
×
222
                        CheckStreams:                   *checkStreams,
×
223
                        CheckSingleStreams:             *checkSingleStreams,
×
224
                        StreamsExcludeConsumerMetrics:  *streamsExcludeConsumerMetrics,
×
225
                        CountKeys:                      *countKeys,
×
226
                        LuaScript:                      ls,
×
227
                        InclSystemMetrics:              *inclSystemMetrics,
×
228
                        InclConfigMetrics:              *inclConfigMetrics,
×
229
                        DisableExportingKeyValues:      *disableExportingKeyValues,
×
230
                        ExcludeLatencyHistogramMetrics: *excludeLatencyHistogramMetrics,
×
231
                        RedactConfigMetrics:            *redactConfigMetrics,
×
232
                        SetClientName:                  *setClientName,
×
233
                        IsTile38:                       *isTile38,
×
234
                        IsCluster:                      *isCluster,
×
235
                        InclModulesMetrics:             *inclModulesMetrics,
×
236
                        ExportClientList:               *exportClientList,
×
237
                        ExportClientsInclPort:          *exportClientPort,
×
238
                        SkipCheckKeysForRoleMaster:     *skipCheckKeysForRoleMaster,
×
239
                        SkipTLSVerification:            *skipTLSVerification,
×
240
                        ClientCertFile:                 *tlsClientCertFile,
×
241
                        ClientKeyFile:                  *tlsClientKeyFile,
×
242
                        CaCertFile:                     *tlsCaCertFile,
×
243
                        ConnectionTimeouts:             to,
×
244
                        MetricsPath:                    *metricPath,
×
245
                        RedisMetricsOnly:               *redisMetricsOnly,
×
246
                        PingOnConnect:                  *pingOnConnect,
×
247
                        RedisPwdFile:                   *redisPwdFile,
×
248
                        Registry:                       registry,
×
249
                        BuildInfo: exporter.BuildInfo{
×
250
                                Version:   BuildVersion,
×
251
                                CommitSha: BuildCommitSha,
×
252
                                Date:      BuildDate,
×
253
                        },
×
254
                        BasicAuthUsername:            *basicAuthUsername,
×
255
                        BasicAuthPassword:            *basicAuthPassword,
×
256
                        InclMetricsForEmptyDatabases: *inclMetricsForEmptyDatabases,
×
257
                },
×
258
        )
×
259
        if err != nil {
×
NEW
260
                slog.Error("Failed to create Redis exporter", "error", err)
×
NEW
261
                os.Exit(1)
×
UNCOV
262
        }
×
263

264
        // Verify that initial client keypair and CA are accepted
NEW
265
        if (*tlsClientCertFile != "") != (*tlsClientKeyFile != "") {
×
NEW
266
                slog.Error("TLS client key file and cert file should both be present")
×
NEW
267
                os.Exit(1)
×
268
        }
×
269
        _, err = exp.CreateClientTLSConfig()
×
270
        if err != nil {
×
NEW
271
                slog.Error("Failed to create client TLS config", "error", err)
×
NEW
272
                os.Exit(1)
×
UNCOV
273
        }
×
274

NEW
275
        slog.Info("Providing metrics", "address", *listenAddress, "path", *metricPath)
×
NEW
276
        slog.Debug("Configured redis address", "address", *redisAddr)
×
277
        server := &http.Server{
×
278
                Addr:    *listenAddress,
×
279
                Handler: exp,
×
280
        }
×
281
        go func() {
×
282
                if *tlsServerCertFile != "" && *tlsServerKeyFile != "" {
×
NEW
283
                        slog.Debug("Starting TLS server", "cert_file", *tlsServerCertFile, "key_file", *tlsServerKeyFile)
×
284

×
285
                        tlsConfig, err := exp.CreateServerTLSConfig(*tlsServerCertFile, *tlsServerKeyFile, *tlsServerCaCertFile, *tlsServerMinVersion)
×
286
                        if err != nil {
×
NEW
287
                                slog.Error("Failed to create server TLS config", "error", err)
×
NEW
288
                                os.Exit(1)
×
289
                        }
×
290
                        server.TLSConfig = tlsConfig
×
291
                        if err := server.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
×
NEW
292
                                slog.Error("TLS Server error", "error", err)
×
NEW
293
                                os.Exit(1)
×
294
                        }
×
295
                } else {
×
296
                        if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
×
NEW
297
                                slog.Error("Server error", "error", err)
×
NEW
298
                                os.Exit(1)
×
UNCOV
299
                        }
×
300
                }
301
        }()
302

303
        // graceful shutdown
304
        quit := make(chan os.Signal, 1)
×
305
        signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
×
306
        _quit := <-quit
×
NEW
307
        slog.Info("Received signal, exiting", "signal", _quit.String())
×
308
        // Create a context with a timeout
×
309
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
×
310
        defer cancel()
×
311

×
312
        // Shutdown the HTTP server gracefully
×
313
        if err := server.Shutdown(ctx); err != nil {
×
NEW
314
                slog.Error("Server shutdown failed", "error", err)
×
NEW
315
                os.Exit(1)
×
316
        }
×
NEW
317
        slog.Info("Server shut down gracefully")
×
318
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc