Skip to content

Commit 1a710ec

Browse files
authored
NETOBSERV-1706: fix error displayed due to max-chunk-age (#552)
When Loki is disabled, do not try to fetch max-chunk-age. More changes alongside: - fetch max-chunk-age as part of the config fetching (ie. just 1 API call instead of 2) -> this simplifies a bit the loading sequence on the frontend - Use mapstructure for decoding loki config (refactor getting limits & max-chunk-age in that way) - Manage unset max-chunk-age more explicitly (undefined instead of NaN) - Add tests
1 parent 520f3ff commit 1a710ec

File tree

25 files changed

+2176
-118
lines changed

25 files changed

+2176
-118
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ toolchain go1.21.7
77
require (
88
github.com/gorilla/mux v1.8.1
99
github.com/json-iterator/go v1.1.12
10+
github.com/mitchellh/mapstructure v1.5.0
1011
github.com/prometheus/client_golang v1.19.0
1112
github.com/prometheus/common v0.52.3
1213
github.com/sirupsen/logrus v1.9.3

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5454
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5555
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
5656
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
57+
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
58+
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
5759
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
5860
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
5961
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

pkg/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ type Frontend struct {
127127
Fields []FieldConfig `yaml:"fields" json:"fields"`
128128
DataSources []string `yaml:"dataSources" json:"dataSources"`
129129
PromLabels []string `yaml:"promLabels" json:"promLabels"`
130+
MaxChunkAgeMs int `yaml:"maxChunkAgeMs,omitempty" json:"maxChunkAgeMs,omitempty"` // populated at query time
130131
}
131132

132133
type Config struct {

pkg/handler/frontend-config.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ func (h *Handlers) GetFrontendConfig() func(w http.ResponseWriter, r *http.Reque
1111
if err != nil {
1212
hlog.Errorf("Could not read config file: %v", err)
1313
}
14-
return func(w http.ResponseWriter, _ *http.Request) {
14+
return func(w http.ResponseWriter, r *http.Request) {
1515
if err != nil {
1616
cfg, err = config.ReadFile(h.Cfg.Frontend.BuildVersion, h.Cfg.Frontend.BuildDate, h.Cfg.Path)
1717
if err != nil {
1818
writeError(w, http.StatusInternalServerError, err.Error())
19+
}
20+
}
21+
if h.Cfg.IsLokiEnabled() {
22+
// (Re)load Loki max chunk age
23+
lokiClient := newLokiClient(&h.Cfg.Loki, r.Header, true)
24+
if maxChunkAge, err := h.fetchIngesterMaxChunkAge(lokiClient); err != nil {
25+
// Log the error, but keep returning known config
26+
hlog.Errorf("Could not get max chunk age: %v", err)
1927
} else {
20-
writeJSON(w, http.StatusOK, cfg.Frontend)
28+
cfg.Frontend.MaxChunkAgeMs = int(maxChunkAge.Milliseconds())
2129
}
22-
} else {
23-
writeJSON(w, http.StatusOK, cfg.Frontend)
2430
}
31+
writeJSON(w, http.StatusOK, cfg.Frontend)
2532
}
2633
}

pkg/handler/loki.go

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"time"
1111

12+
"github.com/mitchellh/mapstructure"
1213
"github.com/sirupsen/logrus"
1314
"gopkg.in/yaml.v3"
1415

@@ -254,65 +255,79 @@ func (h *Handlers) LokiBuildInfos() func(w http.ResponseWriter, r *http.Request)
254255
}
255256
}
256257

257-
func (h *Handlers) LokiConfig(param string) func(w http.ResponseWriter, r *http.Request) {
258-
return func(w http.ResponseWriter, r *http.Request) {
259-
if !h.Cfg.IsLokiEnabled() {
260-
writeError(w, http.StatusBadRequest, "Loki is disabled")
261-
return
262-
}
263-
lokiClient := newLokiClient(&h.Cfg.Loki, r.Header, true)
264-
baseURL := strings.TrimRight(h.Cfg.Loki.GetStatusURL(), "/")
258+
func (h *Handlers) fetchLokiConfig(cl httpclient.Caller, output any) error {
259+
baseURL := strings.TrimRight(h.Cfg.Loki.GetStatusURL(), "/")
265260

266-
resp, code, err := executeLokiQuery(fmt.Sprintf("%s/%s", baseURL, "config"), lokiClient)
267-
if err != nil {
268-
writeError(w, code, err.Error())
269-
return
270-
}
261+
resp, _, err := executeLokiQuery(fmt.Sprintf("%s/%s", baseURL, "config"), cl)
262+
if err != nil {
263+
return err
264+
}
271265

272-
cfg := make(map[string]interface{})
273-
err = yaml.Unmarshal(resp, &cfg)
274-
if err != nil {
275-
hlog.WithError(err).Errorf("cannot unmarshal, response was: %v", string(resp))
276-
writeError(w, code, err.Error())
277-
return
278-
}
279-
writeJSON(w, code, cfg[param])
266+
cfg := make(map[string]interface{})
267+
err = yaml.Unmarshal(resp, &cfg)
268+
if err != nil {
269+
hlog.WithError(err).Errorf("cannot unmarshal Loki config, response was: %v", string(resp))
270+
return err
271+
}
272+
273+
err = mapstructure.Decode(cfg, output)
274+
if err != nil {
275+
hlog.WithError(err).Errorf("cannot decode Loki config, response was: %v", cfg)
276+
return err
280277
}
278+
279+
return nil
281280
}
282281

283-
func (h *Handlers) IngesterMaxChunkAge() func(w http.ResponseWriter, r *http.Request) {
282+
func (h *Handlers) LokiLimits() func(w http.ResponseWriter, r *http.Request) {
284283
return func(w http.ResponseWriter, r *http.Request) {
285284
if !h.Cfg.IsLokiEnabled() {
286-
writeError(w, http.StatusBadRequest, "Loki is disabled")
285+
writeJSON(w, http.StatusNoContent, "Loki is disabled")
287286
return
288287
}
289288
lokiClient := newLokiClient(&h.Cfg.Loki, r.Header, true)
290-
baseURL := strings.TrimRight(h.Cfg.Loki.GetStatusURL(), "/")
291-
292-
resp, code, err := executeLokiQuery(fmt.Sprintf("%s/%s", baseURL, "config"), lokiClient)
289+
limits, err := h.fetchLokiLimits(lokiClient)
293290
if err != nil {
294-
writeError(w, code, err.Error())
291+
hlog.WithError(err).Error("cannot fetch Loki limits")
292+
writeError(w, http.StatusInternalServerError, err.Error())
295293
return
296294
}
295+
writeJSON(w, http.StatusOK, limits)
296+
}
297+
}
297298

298-
cfg := make(map[string]interface{})
299-
err = yaml.Unmarshal(resp, &cfg)
300-
if err != nil {
301-
hlog.WithError(err).Errorf("cannot unmarshal, response was: %v", string(resp))
302-
writeError(w, code, err.Error())
303-
return
304-
}
299+
func (h *Handlers) fetchLokiLimits(cl httpclient.Caller) (map[string]any, error) {
300+
type LimitsConfig struct {
301+
Limits map[string]any `mapstructure:"limits_config"`
302+
}
303+
limitsCfg := LimitsConfig{}
304+
if err := h.fetchLokiConfig(cl, &limitsCfg); err != nil {
305+
return nil, fmt.Errorf("Error when fetching Loki limits: %w", err)
306+
}
307+
return limitsCfg.Limits, nil
308+
}
309+
310+
func (h *Handlers) fetchIngesterMaxChunkAge(cl httpclient.Caller) (time.Duration, error) {
311+
type ChunkAgeConfig struct {
312+
Ingester struct {
313+
MaxChunkAge string `mapstructure:"max_chunk_age"`
314+
} `mapstructure:"ingester"`
315+
}
316+
ageCfg := ChunkAgeConfig{}
317+
if err := h.fetchLokiConfig(cl, &ageCfg); err != nil {
318+
return 0, fmt.Errorf("error when fetching Loki ingester max chunk age: %w", err)
319+
}
305320

321+
if ageCfg.Ingester.MaxChunkAge == "" {
306322
// default max chunk age is 2h
307323
// see https://grafana.com/docs/loki/latest/configure/#ingester
308-
var maxChunkAge interface{} = "2h"
309-
if cfg["ingester"] != nil {
310-
ingester := cfg["ingester"].(map[string]interface{})
311-
if ingester["max_chunk_age"] != nil {
312-
maxChunkAge = ingester["max_chunk_age"]
313-
}
314-
}
324+
return 2 * time.Hour, nil
325+
}
315326

316-
writeJSON(w, code, maxChunkAge)
327+
parsed, err := time.ParseDuration(ageCfg.Ingester.MaxChunkAge)
328+
if err != nil {
329+
return 0, fmt.Errorf("cannot parse max chunk age: %w", err)
317330
}
331+
332+
return parsed, nil
318333
}

pkg/handler/loki_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package handler
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/netobserv/network-observability-console-plugin/pkg/httpclient/httpclienttest"
11+
)
12+
13+
func TestFetchLimits(t *testing.T) {
14+
lokiClientMock := new(httpclienttest.HTTPClientMock)
15+
lokiClientMock.On("Get", "http://loki/config").Return([]byte(`{"limits_config": {"somelimit": 42}}`), 200, nil)
16+
limits, err := h.fetchLokiLimits(lokiClientMock)
17+
require.NoError(t, err)
18+
19+
assert.Equal(t, map[string]any{"somelimit": 42}, limits)
20+
}
21+
22+
func TestFetchLimits_Absent(t *testing.T) {
23+
lokiClientMock := new(httpclienttest.HTTPClientMock)
24+
lokiClientMock.On("Get", "http://loki/config").Return([]byte(`{"any": "any"}`), 200, nil)
25+
limits, err := h.fetchLokiLimits(lokiClientMock)
26+
require.NoError(t, err)
27+
28+
assert.Nil(t, limits)
29+
}
30+
31+
func TestFetchMaxChunkAge(t *testing.T) {
32+
lokiClientMock := new(httpclienttest.HTTPClientMock)
33+
lokiClientMock.On("Get", "http://loki/config").Return([]byte(`{"ingester": {"max_chunk_age": "10h"}}`), 200, nil)
34+
mca, err := h.fetchIngesterMaxChunkAge(lokiClientMock)
35+
require.NoError(t, err)
36+
37+
assert.Equal(t, 10*time.Hour, mca)
38+
}
39+
40+
func TestFetchMaxChunkAge_Absent(t *testing.T) {
41+
lokiClientMock := new(httpclienttest.HTTPClientMock)
42+
lokiClientMock.On("Get", "http://loki/config").Return([]byte(`{"any": "any"}`), 200, nil)
43+
mca, err := h.fetchIngesterMaxChunkAge(lokiClientMock)
44+
require.NoError(t, err)
45+
46+
// Default value
47+
assert.Equal(t, 2*time.Hour, mca)
48+
}

pkg/httpclient/httpclienttest/http_client_mock.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ func (o *HTTPClientMock) Get(url string) ([]byte, int, error) {
1414
}
1515

1616
func (o *HTTPClientMock) SpyURL(fn func(url string)) {
17-
o.On(
18-
"Get",
19-
mock.AnythingOfType("string"),
20-
).Run(func(args mock.Arguments) {
21-
fn(args[0].(string))
22-
}).Return([]byte{}, 0, nil)
17+
o.On("Get", mock.AnythingOfType("string")).
18+
Run(func(args mock.Arguments) { fn(args[0].(string)) }).
19+
Return([]byte{}, 0, nil)
2320
}

pkg/server/routes.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ func setupRoutes(ctx context.Context, cfg *config.Config, authChecker auth.Check
4141
api.HandleFunc("/loki/ready", h.LokiReady())
4242
api.HandleFunc("/loki/metrics", forceCheckAdmin(authChecker, h.LokiMetrics()))
4343
api.HandleFunc("/loki/buildinfo", forceCheckAdmin(authChecker, h.LokiBuildInfos()))
44-
api.HandleFunc("/loki/config/limits", forceCheckAdmin(authChecker, h.LokiConfig("limits_config")))
45-
api.HandleFunc("/loki/config/ingester/max_chunk_age", forceCheckAdmin(authChecker, h.IngesterMaxChunkAge()))
44+
api.HandleFunc("/loki/config/limits", forceCheckAdmin(authChecker, h.LokiLimits()))
4645
api.HandleFunc("/loki/flow/records", h.GetFlows(ctx))
4746
api.HandleFunc("/loki/flow/metrics", h.GetTopology(ctx))
4847
api.HandleFunc("/loki/export", h.ExportFlows(ctx))

vendor/github.com/mitchellh/mapstructure/CHANGELOG.md

Lines changed: 96 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/mitchellh/mapstructure/LICENSE

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)