Skip to content

Only output config parameters that differ from the defaults for /config endpoint #3645

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
* [ENHANCEMENT] Compactor: tenants marked for deletion will now be fully cleaned up after some delay since deletion of last block. Cleanup includes removal of remaining marker files (including tenant deletion mark file) and files under `debug/metas`. #3613
* [ENHANCEMENT] Compactor: retry compaction of a single tenant on failure instead of re-running compaction for all tenants. #3627
* [ENHANCEMENT] Querier: Implement result caching for tenant query federation. #3640
* [ENHANCEMENT] API: Add a `mode` query parameter for the config endpoint: #3645
* `/config?mode=diff`: Shows the YAML configuration with all values that differ from the defaults.
* `/config?mode=defaults`: Shows the YAML configuration with all the default values.
* [ENHANCEMENT] OpenStack Swift: added the following config options to OpenStack Swift backend client: #3660
- Chunks storage: `-swift.auth-version`, `-swift.max-retries`, `-swift.connect-timeout`, `-swift.request-timeout`.
- Blocks storage: ` -blocks-storage.swift.auth-version`, ` -blocks-storage.swift.max-retries`, ` -blocks-storage.swift.connect-timeout`, ` -blocks-storage.swift.request-timeout`.
Expand Down
14 changes: 14 additions & 0 deletions docs/api/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ GET /config

Displays the configuration currently applied to Cortex (in YAML format), including default values and settings via CLI flags. Sensitive data is masked. Please be aware that the exported configuration **doesn't include the per-tenant overrides**.

#### Different modes

```
GET /config?mode=diff
```

Displays the configuration currently applied to Cortex (in YAML format) as before, but containing only the values that differ from the default values.

```
GET /config?mode=defaults
```

Displays the configuration using only the default values.

### Services status

```
Expand Down
7 changes: 4 additions & 3 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,11 @@ func (a *API) RegisterAlertmanager(am *alertmanager.MultitenantAlertmanager, tar
}

// RegisterAPI registers the standard endpoints associated with a running Cortex.
func (a *API) RegisterAPI(httpPathPrefix string, cfg interface{}) {
a.indexPage.AddLink(SectionAdminEndpoints, "/config", "Current Config")
func (a *API) RegisterAPI(httpPathPrefix string, actualCfg interface{}, defaultCfg interface{}) {
a.indexPage.AddLink(SectionAdminEndpoints, "/config", "Current Config (including the default values)")
a.indexPage.AddLink(SectionAdminEndpoints, "/config?mode=diff", "Current Config (show only values that differ from the defaults)")

a.RegisterRoute("/config", configHandler(cfg), false, "GET")
a.RegisterRoute("/config", configHandler(actualCfg, defaultCfg), false, "GET")
a.RegisterRoute("/", indexHandler(httpPathPrefix, a.indexPage), false, "GET")
a.RegisterRoute("/debug/fgprof", fgprof.Handler(), false, "GET")
}
Expand Down
112 changes: 101 additions & 11 deletions pkg/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package api

import (
"context"
"fmt"
"html/template"
"net/http"
"path"
"reflect"
"regexp"
"sync"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/gorilla/mux"
"github.com/opentracing-contrib/go-stdlib/nethttp"
"github.com/opentracing/opentracing-go"
Expand Down Expand Up @@ -115,20 +116,109 @@ func indexHandler(httpPathPrefix string, content *IndexPageContent) http.Handler
}
}

func configHandler(cfg interface{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
out, err := yaml.Marshal(cfg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
func yamlMarshalUnmarshal(in interface{}) (map[interface{}]interface{}, error) {
yamlBytes, err := yaml.Marshal(in)
if err != nil {
return nil, err
}

object := make(map[interface{}]interface{})
if err := yaml.Unmarshal(yamlBytes, object); err != nil {
return nil, err
}

return object, nil
}

func diffConfig(defaultConfig, actualConfig map[interface{}]interface{}) (map[interface{}]interface{}, error) {
output := make(map[interface{}]interface{})

for key, value := range actualConfig {

defaultValue, ok := defaultConfig[key]
if !ok {
output[key] = value
continue
}

w.Header().Set("Content-Type", "text/yaml")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(out); err != nil {
level.Error(util.Logger).Log("msg", "error writing response", "err", err)
switch v := value.(type) {
case int:
defaultV, ok := defaultValue.(int)
if !ok || defaultV != v {
output[key] = v
}
case string:
defaultV, ok := defaultValue.(string)
if !ok || defaultV != v {
output[key] = v
}
case bool:
defaultV, ok := defaultValue.(bool)
if !ok || defaultV != v {
output[key] = v
}
case []interface{}:
defaultV, ok := defaultValue.([]interface{})
if !ok || !reflect.DeepEqual(defaultV, v) {
output[key] = v
}
case float64:
defaultV, ok := defaultValue.(float64)
if !ok || !reflect.DeepEqual(defaultV, v) {
output[key] = v
}
case map[interface{}]interface{}:
defaultV, ok := defaultValue.(map[interface{}]interface{})
if !ok {
output[key] = value
}
diff, err := diffConfig(defaultV, v)
if err != nil {
return nil, err
}
if len(diff) > 0 {
output[key] = diff
}
default:
return nil, fmt.Errorf("unsupported type %T", v)
}
}

return output, nil
}

func configHandler(actualCfg interface{}, defaultCfg interface{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var output interface{}
switch r.URL.Query().Get("mode") {
case "diff":
defaultCfgObj, err := yamlMarshalUnmarshal(defaultCfg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

actualCfgObj, err := yamlMarshalUnmarshal(actualCfg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

diff, err := diffConfig(defaultCfgObj, actualCfgObj)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
output = diff

case "defaults":
output = defaultCfg
default:
output = actualCfg
}

util.WriteYAMLResponse(w, output)
}
}

// NewQuerierHandler returns a HTTP handler that can be used by the querier service to
Expand Down
109 changes: 109 additions & 0 deletions pkg/api/handlers_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package api

import (
"io/ioutil"
"net/http/httptest"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -53,3 +55,110 @@ func TestIndexPageContent(t *testing.T) {
require.True(t, strings.Contains(resp.Body.String(), "/shutdown"))
require.False(t, strings.Contains(resp.Body.String(), "/compactor/ring"))
}

type diffConfigMock struct {
MyInt int `yaml:"my_int"`
MyFloat float64 `yaml:"my_float"`
MySlice []string `yaml:"my_slice"`
IgnoredField func() error `yaml:"-"`
MyNestedStruct struct {
MyString string `yaml:"my_string"`
MyBool bool `yaml:"my_bool"`
MyEmptyStruct struct{} `yaml:"my_empty_struct"`
} `yaml:"my_nested_struct"`
}

func newDefaultDiffConfigMock() *diffConfigMock {
c := &diffConfigMock{
MyInt: 666,
MyFloat: 6.66,
MySlice: []string{"value1", "value2"},
IgnoredField: func() error { return nil },
}
c.MyNestedStruct.MyString = "string1"
return c
}

func TestConfigDiffHandler(t *testing.T) {
for _, tc := range []struct {
name string
expectedStatusCode int
expectedBody string
actualConfig func() interface{}
}{
{
name: "no config parameters overridden",
expectedStatusCode: 200,
expectedBody: "{}\n",
},
{
name: "slice changed",
actualConfig: func() interface{} {
c := newDefaultDiffConfigMock()
c.MySlice = append(c.MySlice, "value3")
return c
},
expectedStatusCode: 200,
expectedBody: "my_slice:\n" +
"- value1\n" +
"- value2\n" +
"- value3\n",
},
{
name: "string in nested struct changed",
actualConfig: func() interface{} {
c := newDefaultDiffConfigMock()
c.MyNestedStruct.MyString = "string2"
return c
},
expectedStatusCode: 200,
expectedBody: "my_nested_struct:\n" +
" my_string: string2\n",
},
{
name: "bool in nested struct changed",
actualConfig: func() interface{} {
c := newDefaultDiffConfigMock()
c.MyNestedStruct.MyBool = true
return c
},
expectedStatusCode: 200,
expectedBody: "my_nested_struct:\n" +
" my_bool: true\n",
},
{
name: "test invalid input",
actualConfig: func() interface{} {
c := "x"
return &c
},
expectedStatusCode: 500,
expectedBody: "yaml: unmarshal errors:\n" +
" line 1: cannot unmarshal !!str `x` into map[interface {}]interface {}\n",
},
} {
defaultCfg := newDefaultDiffConfigMock()
t.Run(tc.name, func(t *testing.T) {

var actualCfg interface{}
if tc.actualConfig != nil {
actualCfg = tc.actualConfig()
} else {
actualCfg = newDefaultDiffConfigMock()
}

req := httptest.NewRequest("GET", "http://test.com/config?mode=diff", nil)
w := httptest.NewRecorder()

h := configHandler(actualCfg, defaultCfg)
h(w, req)
resp := w.Result()
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)

body, err := ioutil.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, tc.expectedBody, string(body))
})
}

}
11 changes: 9 additions & 2 deletions pkg/cortex/modules.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cortex

import (
"flag"
"fmt"
"os"
"time"
Expand Down Expand Up @@ -80,6 +81,13 @@ const (
All string = "all"
)

func newDefaultConfig() *Config {
defaultConfig := &Config{}
defaultFS := flag.NewFlagSet("", flag.PanicOnError)
defaultConfig.RegisterFlags(defaultFS)
Comment on lines +86 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[non blocking, just FYI] You can achieve the same with flagext.DefaultValues().

return defaultConfig
}

func (t *Cortex) initAPI() (services.Service, error) {
t.Cfg.API.ServerPrefix = t.Cfg.Server.PathPrefix
t.Cfg.API.LegacyHTTPPrefix = t.Cfg.HTTPPrefix
Expand All @@ -90,8 +98,7 @@ func (t *Cortex) initAPI() (services.Service, error) {
}

t.API = a

t.API.RegisterAPI(t.Cfg.Server.PathPrefix, t.Cfg)
t.API.RegisterAPI(t.Cfg.Server.PathPrefix, t.Cfg, newDefaultConfig())

return nil, nil
}
Expand Down
Loading