Skip to content

Commit

Permalink
Merge branch 'master' into oh_TestExtractInfoMetrics
Browse files Browse the repository at this point in the history
  • Loading branch information
oliver006 authored Dec 17, 2024
2 parents 11f6e78 + e2bb7fd commit 7c98258
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
files: ./coverage.txt
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ Prometheus uses file watches and all changes to the json file are applied immedi
| check-key-groups | REDIS_EXPORTER_CHECK_KEY_GROUPS | Comma separated list of [LUA regexes](https://www.lua.org/pil/20.1.html) for classifying keys into groups. The regexes are applied in specified order to individual keys, and the group name is generated by concatenating all capture groups of the first regex that matches a key. A key will be tracked under the `unclassified` group if none of the specified regexes matches it. |
| max-distinct-key-groups | REDIS_EXPORTER_MAX_DISTINCT_KEY_GROUPS | Maximum number of distinct key groups that can be tracked independently *per Redis database*. If exceeded, only key groups with the highest memory consumption within the limit will be tracked separately, all remaining key groups will be tracked under a single `overflow` key group. |
| config-command | REDIS_EXPORTER_CONFIG_COMMAND | What to use for the CONFIG command, defaults to `CONFIG`, , set to "-" to skip config metrics extraction. |
| basic-auth-username | REDIS_EXPORTER_BASIC_AUTH_USERNAME | Username for Basic Authentication with the redis exporter needs to be set together with basic-auth-password to be effective
| basic-auth-password | REDIS_EXPORTER_BASIC_AUTH_PASSWORD | Password for Basic Authentication with the redis exporter needs to be set together with basic-auth-username to be effective

Redis instance addresses can be tcp addresses: `redis://localhost:6379`, `redis.example.com:6379` or e.g. unix sockets: `unix:///tmp/redis.sock`.\
SSL is supported by using the `rediss://` schema, for example: `rediss://azure-ssl-enabled-host.redis.cache.windows.net:6380` (note that the port is required when connecting to a non-standard 6379 port, e.g. with Azure Redis instances).\
Expand Down
6 changes: 4 additions & 2 deletions exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ type Options struct {
RedisPwdFile string
Registry *prometheus.Registry
BuildInfo BuildInfo
BasicAuthUsername string
BasicAuthPassword string
}

// NewRedisExporter returns a new exporter of Redis metrics.
Expand Down Expand Up @@ -299,8 +301,6 @@ func NewRedisExporter(uri string, opts Options) (*Exporter, error) {
"search_global_idle": "search_global_idle",
"search_global_total": "search_global_total",
"search_bytes_collected": "search_collected_bytes",
"search_total_cycles": "search_total_cycles",
"search_total_ms_run": "search_total_run_ms",
"search_dialect_1": "search_dialect_1",
"search_dialect_2": "search_dialect_2",
"search_dialect_3": "search_dialect_3",
Expand Down Expand Up @@ -359,6 +359,8 @@ func NewRedisExporter(uri string, opts Options) (*Exporter, error) {

// Redis Modules metrics, RediSearch module
"search_total_indexing_time": "search_indexing_time_ms_total",
"search_total_cycles": "search_cycles_total",
"search_total_ms_run": "search_run_ms_total",
},
}

Expand Down
32 changes: 32 additions & 0 deletions exporter/http.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package exporter

import (
"crypto/subtle"
"errors"
"fmt"
"net/http"
"net/url"
Expand All @@ -12,6 +14,12 @@ import (
)

func (e *Exporter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := e.verifyBasicAuth(r.BasicAuth()); err != nil {
w.Header().Set("WWW-Authenticate", `Basic realm="redis-exporter, charset=UTF-8"`)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

e.mux.ServeHTTP(w, r)
}

Expand Down Expand Up @@ -107,3 +115,27 @@ func (e *Exporter) reloadPwdFile(w http.ResponseWriter, r *http.Request) {
e.Unlock()
_, _ = w.Write([]byte(`ok`))
}

func (e *Exporter) isBasicAuthConfigured() bool {
return e.options.BasicAuthUsername != "" && e.options.BasicAuthPassword != ""
}

func (e *Exporter) verifyBasicAuth(user, password string, authHeaderSet bool) error {

if !e.isBasicAuthConfigured() {
return nil
}

if !authHeaderSet {
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 nil
}
233 changes: 233 additions & 0 deletions exporter/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,239 @@ func TestReloadHandlers(t *testing.T) {
}
}

func TestIsBasicAuthConfigured(t *testing.T) {
tests := []struct {
name string
username string
password string
want bool
}{
{
name: "no credentials configured",
username: "",
password: "",
want: false,
},
{
name: "only username configured",
username: "user",
password: "",
want: false,
},
{
name: "only password configured",
username: "",
password: "pass",
want: false,
},
{
name: "both credentials configured",
username: "user",
password: "pass",
want: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e, _ := NewRedisExporter("", Options{
BasicAuthUsername: tt.username,
BasicAuthPassword: tt.password,
})

if got := e.isBasicAuthConfigured(); got != tt.want {
t.Errorf("isBasicAuthConfigured() = %v, want %v", got, tt.want)
}
})
}
}

func TestVerifyBasicAuth(t *testing.T) {
tests := []struct {
name string
configUser string
configPass string
providedUser string
providedPass string
authHeaderSet bool
wantErr bool
wantErrString string
}{
{
name: "no auth configured - no credentials provided",
configUser: "",
configPass: "",
providedUser: "",
providedPass: "",
authHeaderSet: false,
wantErr: false,
},
{
name: "auth configured - no auth header",
configUser: "user",
configPass: "pass",
providedUser: "",
providedPass: "",
authHeaderSet: false,
wantErr: true,
wantErrString: "Unauthorized",
},
{
name: "auth configured - correct credentials",
configUser: "user",
configPass: "pass",
providedUser: "user",
providedPass: "pass",
authHeaderSet: true,
wantErr: false,
},
{
name: "auth configured - wrong username",
configUser: "user",
configPass: "pass",
providedUser: "wronguser",
providedPass: "pass",
authHeaderSet: true,
wantErr: true,
wantErrString: "Unauthorized",
},
{
name: "auth configured - wrong password",
configUser: "user",
configPass: "pass",
providedUser: "user",
providedPass: "wrongpass",
authHeaderSet: true,
wantErr: true,
wantErrString: "Unauthorized",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e, _ := NewRedisExporter("", Options{
BasicAuthUsername: tt.configUser,
BasicAuthPassword: tt.configPass,
})

err := e.verifyBasicAuth(tt.providedUser, tt.providedPass, tt.authHeaderSet)

if (err != nil) != tt.wantErr {
t.Errorf("verifyBasicAuth() error = %v, wantErr %v", err, tt.wantErr)
return
}

if err != nil && err.Error() != tt.wantErrString {
t.Errorf("verifyBasicAuth() error = %v, wantErrString %v", err, tt.wantErrString)
}
})
}
}

func TestBasicAuth(t *testing.T) {
if os.Getenv("TEST_REDIS_URI") == "" {
t.Skipf("TEST_REDIS_URI not set - skipping")
}

tests := []struct {
name string
username string
password string
configUsername string
configPassword string
wantStatusCode int
}{
{
name: "No auth configured - no credentials provided",
username: "",
password: "",
configUsername: "",
configPassword: "",
wantStatusCode: http.StatusOK,
},
{
name: "Auth configured - correct credentials",
username: "testuser",
password: "testpass",
configUsername: "testuser",
configPassword: "testpass",
wantStatusCode: http.StatusOK,
},
{
name: "Auth configured - wrong username",
username: "wronguser",
password: "testpass",
configUsername: "testuser",
configPassword: "testpass",
wantStatusCode: http.StatusUnauthorized,
},
{
name: "Auth configured - wrong password",
username: "testuser",
password: "wrongpass",
configUsername: "testuser",
configPassword: "testpass",
wantStatusCode: http.StatusUnauthorized,
},
{
name: "Auth configured - no credentials provided",
username: "",
password: "",
configUsername: "testuser",
configPassword: "testpass",
wantStatusCode: http.StatusUnauthorized,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e, _ := NewRedisExporter(os.Getenv("TEST_REDIS_URI"), Options{
Namespace: "test",
Registry: prometheus.NewRegistry(),
BasicAuthUsername: tt.configUsername,
BasicAuthPassword: tt.configPassword,
})
ts := httptest.NewServer(e)
defer ts.Close()

client := &http.Client{}
req, err := http.NewRequest("GET", ts.URL+"/metrics", nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}

if tt.username != "" || tt.password != "" {
req.SetBasicAuth(tt.username, tt.password)
}

resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to send request: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != tt.wantStatusCode {
t.Errorf("Expected status code %d, got %d", tt.wantStatusCode, resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}

if tt.wantStatusCode == http.StatusOK {
if !strings.Contains(string(body), "test_up") {
t.Errorf("Expected body to contain 'test_up', got: %s", string(body))
}
} else {
if !strings.Contains(resp.Header.Get("WWW-Authenticate"), "Basic realm=\"redis-exporter") {
t.Errorf("Expected WWW-Authenticate header, got: %s", resp.Header.Get("WWW-Authenticate"))
}
}
})
}
}

func downloadURL(t *testing.T, u string) string {
_, res := downloadURLWithStatusCode(t, u)
return res
Expand Down
4 changes: 2 additions & 2 deletions exporter/modules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ func TestModules(t *testing.T) {
"search_global_idle": false,
"search_global_total": false,
"search_collected_bytes": false,
"search_total_cycles": false,
"search_total_run_ms": false,
"search_cycles_total": false,
"search_run_ms_total": false,
"search_dialect_1": false,
"search_dialect_2": false,
"search_dialect_3": false,
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.20
require (
github.com/gomodule/redigo v1.9.2
github.com/mna/redisc v1.4.0
github.com/prometheus/client_golang v1.20.4
github.com/prometheus/client_golang v1.20.5
github.com/prometheus/client_model v0.6.1
github.com/sirupsen/logrus v1.9.3
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ func main() {
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")
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")
skipTLSVerification = flag.Bool("skip-tls-verification", getEnvBool("REDIS_EXPORTER_SKIP_TLS_VERIFICATION", false), "Whether to to skip TLS verification")
basicAuthUsername = flag.String("basic-auth-username", getEnv("REDIS_EXPORTER_BASIC_AUTH_USERNAME", ""), "Username for basic authentication")
basicAuthPassword = flag.String("basic-auth-password", getEnv("REDIS_EXPORTER_BASIC_AUTH_PASSWORD", ""), "Password for basic authentication")
)
flag.Parse()

Expand Down Expand Up @@ -201,6 +203,8 @@ func main() {
CommitSha: BuildCommitSha,
Date: BuildDate,
},
BasicAuthUsername: *basicAuthUsername,
BasicAuthPassword: *basicAuthPassword,
},
)
if err != nil {
Expand Down

0 comments on commit 7c98258

Please sign in to comment.