blob: 002cae45b1cc2bad4f5aad2508fd715442743bce [file] [log] [blame]
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +00001//go:build helm_test
2// +build helm_test
3
4package test
5
6import (
7 "context"
8 "errors"
9 "fmt"
Mohammed Naser65cda132024-05-02 14:34:08 -040010 "io"
11 "net/http"
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +000012 "os"
13 "testing"
14 "time"
15
16 "github.com/prometheus/client_golang/api"
17 v1 "github.com/prometheus/client_golang/api/prometheus/v1"
Mohammed Naser65cda132024-05-02 14:34:08 -040018 promConfig "github.com/prometheus/common/config"
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +000019 "github.com/prometheus/common/model"
Mohammed Naser65cda132024-05-02 14:34:08 -040020 "github.com/prometheus/prometheus/model/labels"
21 "github.com/prometheus/prometheus/model/textparse"
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +000022 "github.com/stretchr/testify/require"
23)
24
Mohammed Naser65cda132024-05-02 14:34:08 -040025type testResultFunc func(t *testing.T, ctx context.Context, metric string, test func(model.SampleValue) bool, msg string) error
26
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +000027func TestCanary(t *testing.T) {
Mohammed Naser65cda132024-05-02 14:34:08 -040028
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 Naser8a2c8fb2023-02-19 17:23:55 +000045
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 Naser8a2c8fb2023-02-19 17:23:55 +000057 eventually(t, func() error {
Mohammed Naser65cda132024-05-02 14:34:08 -040058 return testResult(t, ctx, totalEntries, func(v model.SampleValue) bool {
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +000059 return v > 0
Mohammed Naser65cda132024-05-02 14:34:08 -040060 }, fmt.Sprintf("Expected %s to be greater than 0", totalEntries))
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +000061 }, timeoutDuration, "Expected Loki Canary to have entries")
62 })
63
64 t.Run("Canary should not have missed any entries", func(t *testing.T) {
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +000065 eventually(t, func() error {
Mohammed Naser65cda132024-05-02 14:34:08 -040066 return testResult(t, ctx, totalEntriesMissing, func(v model.SampleValue) bool {
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +000067 return v == 0
Mohammed Naser65cda132024-05-02 14:34:08 -040068 }, fmt.Sprintf("Expected %s to equal 0", totalEntriesMissing))
Mohammed Naser8a2c8fb2023-02-19 17:23:55 +000069 }, timeoutDuration, "Expected Loki Canary to not have any missing entries")
70 })
71}
72
73func getEnv(key, fallback string) string {
74 if value, ok := os.LookupEnv(key); ok {
75 return value
76 }
77 return fallback
78}
79
Mohammed Naser65cda132024-05-02 14:34:08 -040080func 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 Naser8a2c8fb2023-02-19 17:23:55 +000087 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 Naser8a2c8fb2023-02-19 17:23:55 +000094 return nil
95 }
96
97 return fmt.Errorf("unexpected Prometheus result type: %v ", result.Type())
98}
99
100func 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 Naser65cda132024-05-02 14:34:08 -0400112func 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 Naser8a2c8fb2023-02-19 17:23:55 +0000170func 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}