Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 1 | //go:build helm_test |
| 2 | // +build helm_test |
| 3 | |
| 4 | package test |
| 5 | |
| 6 | import ( |
| 7 | "context" |
| 8 | "errors" |
| 9 | "fmt" |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 10 | "io" |
| 11 | "net/http" |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 12 | "os" |
| 13 | "testing" |
| 14 | "time" |
| 15 | |
| 16 | "github.com/prometheus/client_golang/api" |
| 17 | v1 "github.com/prometheus/client_golang/api/prometheus/v1" |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 18 | promConfig "github.com/prometheus/common/config" |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 19 | "github.com/prometheus/common/model" |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 20 | "github.com/prometheus/prometheus/model/labels" |
| 21 | "github.com/prometheus/prometheus/model/textparse" |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 22 | "github.com/stretchr/testify/require" |
| 23 | ) |
| 24 | |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 25 | type testResultFunc func(t *testing.T, ctx context.Context, metric string, test func(model.SampleValue) bool, msg string) error |
| 26 | |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 27 | func TestCanary(t *testing.T) { |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 28 | |
| 29 | var testResult testResultFunc |
| 30 | |
| 31 | // Default to directly querying a canary and looking for specific metrics. |
| 32 | testResult = testResultCanary |
| 33 | totalEntries := "loki_canary_entries_total" |
| 34 | totalEntriesMissing := "loki_canary_missing_entries_total" |
| 35 | |
| 36 | // For backwards compatibility and also for anyone who wants to validate with prometheus instead of querying |
| 37 | // a canary directly, if the CANARY_PROMETHEUS_ADDRESS is specified we will use prometheus to validate. |
| 38 | address := os.Getenv("CANARY_PROMETHEUS_ADDRESS") |
| 39 | if address != "" { |
| 40 | testResult = testResultPrometheus |
| 41 | // Use the sum function to aggregate the results from multiple canaries. |
| 42 | totalEntries = "sum(loki_canary_entries_total)" |
| 43 | totalEntriesMissing = "sum(loki_canary_missing_entries_total)" |
| 44 | } |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 45 | |
| 46 | timeout := getEnv("CANARY_TEST_TIMEOUT", "1m") |
| 47 | timeoutDuration, err := time.ParseDuration(timeout) |
| 48 | require.NoError(t, err, "Failed to parse timeout. Please set CANARY_TEST_TIMEOUT to a valid duration.") |
| 49 | |
| 50 | ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration) |
| 51 | |
| 52 | t.Cleanup(func() { |
| 53 | cancel() |
| 54 | }) |
| 55 | |
| 56 | t.Run("Canary should have entries", func(t *testing.T) { |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 57 | eventually(t, func() error { |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 58 | return testResult(t, ctx, totalEntries, func(v model.SampleValue) bool { |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 59 | return v > 0 |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 60 | }, fmt.Sprintf("Expected %s to be greater than 0", totalEntries)) |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 61 | }, timeoutDuration, "Expected Loki Canary to have entries") |
| 62 | }) |
| 63 | |
| 64 | t.Run("Canary should not have missed any entries", func(t *testing.T) { |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 65 | eventually(t, func() error { |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 66 | return testResult(t, ctx, totalEntriesMissing, func(v model.SampleValue) bool { |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 67 | return v == 0 |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 68 | }, fmt.Sprintf("Expected %s to equal 0", totalEntriesMissing)) |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 69 | }, timeoutDuration, "Expected Loki Canary to not have any missing entries") |
| 70 | }) |
| 71 | } |
| 72 | |
| 73 | func getEnv(key, fallback string) string { |
| 74 | if value, ok := os.LookupEnv(key); ok { |
| 75 | return value |
| 76 | } |
| 77 | return fallback |
| 78 | } |
| 79 | |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 80 | func testResultPrometheus(t *testing.T, ctx context.Context, query string, test func(model.SampleValue) bool, msg string) error { |
| 81 | // TODO (ewelch): if we did a lot of these, we'd want to reuse the client but right now we only run a couple tests |
| 82 | client := newClient(t) |
| 83 | result, _, err := client.Query(ctx, query, time.Now()) |
| 84 | if err != nil { |
| 85 | return err |
| 86 | } |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 87 | if v, ok := result.(model.Vector); ok { |
| 88 | for _, s := range v { |
| 89 | t.Logf("%s => %v\n", query, s.Value) |
| 90 | if !test(s.Value) { |
| 91 | return errors.New(msg) |
| 92 | } |
| 93 | } |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 94 | return nil |
| 95 | } |
| 96 | |
| 97 | return fmt.Errorf("unexpected Prometheus result type: %v ", result.Type()) |
| 98 | } |
| 99 | |
| 100 | func newClient(t *testing.T) v1.API { |
| 101 | address := os.Getenv("CANARY_PROMETHEUS_ADDRESS") |
| 102 | require.NotEmpty(t, address, "CANARY_PROMETHEUS_ADDRESS must be set to a valid prometheus address") |
| 103 | |
| 104 | client, err := api.NewClient(api.Config{ |
| 105 | Address: address, |
| 106 | }) |
| 107 | require.NoError(t, err, "Failed to create Loki Canary client") |
| 108 | |
| 109 | return v1.NewAPI(client) |
| 110 | } |
| 111 | |
Mohammed Naser | 65cda13 | 2024-05-02 14:34:08 -0400 | [diff] [blame] | 112 | func testResultCanary(t *testing.T, ctx context.Context, metric string, test func(model.SampleValue) bool, msg string) error { |
| 113 | address := os.Getenv("CANARY_SERVICE_ADDRESS") |
| 114 | require.NotEmpty(t, address, "CANARY_SERVICE_ADDRESS must be set to a valid kubernetes service for the Loki canaries") |
| 115 | |
| 116 | // TODO (ewelch): if we did a lot of these, we'd want to reuse the client but right now we only run a couple tests |
| 117 | client, err := promConfig.NewClientFromConfig(promConfig.HTTPClientConfig{}, "canary-test") |
| 118 | require.NoError(t, err, "Failed to create Prometheus client") |
| 119 | |
| 120 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, address, nil) |
| 121 | require.NoError(t, err, "Failed to create request") |
| 122 | |
| 123 | rsp, err := client.Do(req) |
| 124 | if rsp != nil { |
| 125 | defer rsp.Body.Close() |
| 126 | } |
| 127 | require.NoError(t, err, "Failed to scrape metrics") |
| 128 | |
| 129 | body, err := io.ReadAll(rsp.Body) |
| 130 | require.NoError(t, err, "Failed to read response body") |
| 131 | |
| 132 | p, err := textparse.New(body, rsp.Header.Get("Content-Type"), true, nil) |
| 133 | require.NoError(t, err, "Failed to create Prometheus parser") |
| 134 | |
| 135 | for { |
| 136 | e, err := p.Next() |
| 137 | if err == io.EOF { |
| 138 | return errors.New("metric not found") |
| 139 | } |
| 140 | |
| 141 | if e != textparse.EntrySeries { |
| 142 | continue |
| 143 | } |
| 144 | |
| 145 | l := labels.Labels{} |
| 146 | p.Metric(&l) |
| 147 | |
| 148 | // Currently we aren't validating any labels, just the metric name, however this could be extended to do so. |
| 149 | name := l.Get(model.MetricNameLabel) |
| 150 | if name != metric { |
| 151 | continue |
| 152 | } |
| 153 | |
| 154 | _, _, val := p.Series() |
| 155 | t.Logf("%s => %v\n", metric, val) |
| 156 | |
| 157 | // Note: SampleValue has functions for comparing the equality of two floats which is |
| 158 | // why we convert this back to a SampleValue here for easier use intests. |
| 159 | if !test(model.SampleValue(val)) { |
| 160 | return errors.New(msg) |
| 161 | } |
| 162 | |
| 163 | // Returning here will only validate that one series was found matching the label name that met the condition |
| 164 | // it could be possible since we don't validate the rest of the labels that there is mulitple series |
| 165 | // but currently this meets the spirit of the test. |
| 166 | return nil |
| 167 | } |
| 168 | } |
| 169 | |
Mohammed Naser | 8a2c8fb | 2023-02-19 17:23:55 +0000 | [diff] [blame] | 170 | func eventually(t *testing.T, test func() error, timeoutDuration time.Duration, msg string) { |
| 171 | require.Eventually(t, func() bool { |
| 172 | queryError := test() |
| 173 | if queryError != nil { |
| 174 | t.Logf("Query failed\n%+v\n", queryError) |
| 175 | } |
| 176 | return queryError == nil |
| 177 | }, timeoutDuration, 1*time.Second, msg) |
| 178 | } |