Expose current Prometheus config via /status/config

This PR adds the `/status/config` endpoint which exposes the currently
loaded Prometheus config. This is the same config that is displayed on
`/config` in the UI in YAML format. The response payload looks like
such:
```
{
  "status": "success",
  "data": {
    "yaml": <CONFIG>
  }
}
```
This commit is contained in:
Max Leonard Inden 2017-05-11 17:09:24 +02:00
parent 1ea9ab601e
commit 1c96fbb992
No known key found for this signature in database
GPG key ID: 5403C5464810BC26
6 changed files with 98 additions and 35 deletions

View file

@ -540,15 +540,27 @@ func TestLoadConfig(t *testing.T) {
if !reflect.DeepEqual(c, expectedConf) {
t.Fatalf("%s: unexpected config result: \n\n%s\n expected\n\n%s", "testdata/conf.good.yml", bgot, bexp)
}
// String method must not reveal authentication credentials.
s := c.String()
secretRe := regexp.MustCompile("<secret>")
matches := secretRe.FindAllStringIndex(s, -1)
if len(matches) != 6 || strings.Contains(s, "mysecret") {
t.Fatalf("config's String method reveals authentication credentials.")
}
// YAML marshalling must not reveal authentication credentials.
func TestElideSecrets(t *testing.T) {
c, err := LoadFile("testdata/conf.good.yml")
if err != nil {
t.Fatalf("Error parsing %s: %s", "testdata/conf.good.yml", err)
}
secretRe := regexp.MustCompile(`\\u003csecret\\u003e|<secret>`)
config, err := yaml.Marshal(c)
if err != nil {
t.Fatal(err)
}
yamlConfig := string(config)
matches := secretRe.FindAllStringIndex(yamlConfig, -1)
if len(matches) != 6 || strings.Contains(yamlConfig, "mysecret") {
t.Fatalf("yaml marshal reveals authentication credentials.")
}
}
func TestLoadConfigRuleFilesAbsolutePath(t *testing.T) {

View file

@ -29,6 +29,7 @@ import (
"github.com/prometheus/common/route"
"golang.org/x/net/context"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/retrieval"
"github.com/prometheus/prometheus/storage/local"
@ -104,16 +105,18 @@ type API struct {
alertmanagerRetriever alertmanagerRetriever
now func() model.Time
config func() config.Config
}
// NewAPI returns an initialized API type.
func NewAPI(qe *promql.Engine, st local.Storage, tr targetRetriever, ar alertmanagerRetriever) *API {
func NewAPI(qe *promql.Engine, st local.Storage, tr targetRetriever, ar alertmanagerRetriever, configFunc func() config.Config) *API {
return &API{
QueryEngine: qe,
Storage: st,
targetRetriever: tr,
alertmanagerRetriever: ar,
now: model.Now,
config: configFunc,
}
}
@ -147,6 +150,8 @@ func (api *API) Register(r *route.Router) {
r.Get("/targets", instr("targets", api.targets))
r.Get("/alertmanagers", instr("alertmanagers", api.alertmanagers))
r.Get("/status/config", instr("config", api.serveConfig))
}
type queryData struct {
@ -436,6 +441,17 @@ func (api *API) alertmanagers(r *http.Request) (interface{}, *apiError) {
return ams, nil
}
type prometheusConfig struct {
YAML string `json:"yaml"`
}
func (api *API) serveConfig(r *http.Request) (interface{}, *apiError) {
cfg := &prometheusConfig{
YAML: api.config().String(),
}
return cfg, nil
}
func respond(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

View file

@ -29,6 +29,7 @@ import (
"github.com/prometheus/common/route"
"golang.org/x/net/context"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/retrieval"
)
@ -45,6 +46,15 @@ func (f alertmanagerRetrieverFunc) Alertmanagers() []*url.URL {
return f()
}
var samplePrometheusCfg = config.Config{
GlobalConfig: config.GlobalConfig{},
AlertingConfig: config.AlertingConfig{},
RuleFiles: []string{},
ScrapeConfigs: []*config.ScrapeConfig{},
RemoteWriteConfigs: []*config.RemoteWriteConfig{},
RemoteReadConfigs: []*config.RemoteReadConfig{},
}
func TestEndpoints(t *testing.T) {
suite, err := promql.NewTest(t, `
load 1m
@ -91,6 +101,9 @@ func TestEndpoints(t *testing.T) {
targetRetriever: tr,
alertmanagerRetriever: ar,
now: func() model.Time { return now },
config: func() config.Config {
return samplePrometheusCfg
},
}
start := model.Time(0)
@ -445,7 +458,8 @@ func TestEndpoints(t *testing.T) {
"foo": "bar",
},
},
}, {
},
{
endpoint: api.dropSeries,
query: url.Values{
"match[]": []string{`{__name__=~".+"}`},
@ -453,7 +467,8 @@ func TestEndpoints(t *testing.T) {
response: struct {
NumDeleted int `json:"numDeleted"`
}{2},
}, {
},
{
endpoint: api.targets,
response: &TargetDiscovery{
ActiveTargets: []*Target{
@ -465,7 +480,8 @@ func TestEndpoints(t *testing.T) {
},
},
},
}, {
},
{
endpoint: api.alertmanagers,
response: &AlertmanagerDiscovery{
ActiveAlertmanagers: []*AlertmanagerTarget{
@ -475,6 +491,12 @@ func TestEndpoints(t *testing.T) {
},
},
},
{
endpoint: api.serveConfig,
response: &prometheusConfig{
YAML: samplePrometheusCfg.String(),
},
},
}
for _, test := range tests {

View file

@ -74,7 +74,7 @@ func (h *Handler) federation(w http.ResponseWriter, req *http.Request) {
}
sort.Sort(byName(vector))
externalLabels := h.externalLabels.Clone()
externalLabels := h.config.GlobalConfig.ExternalLabels.Clone()
if _, ok := externalLabels[model.InstanceLabel]; !ok {
externalLabels[model.InstanceLabel] = ""
}

View file

@ -23,6 +23,7 @@ import (
"testing"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/promql"
)
@ -178,10 +179,13 @@ func TestFederation(t *testing.T) {
storage: suite.Storage(),
queryEngine: suite.QueryEngine(),
now: func() model.Time { return 101 * 60 * 1000 }, // 101min after epoch.
config: &config.Config{
GlobalConfig: config.GlobalConfig{},
},
}
for name, scenario := range scenarios {
h.externalLabels = scenario.externalLabels
h.config.GlobalConfig.ExternalLabels = scenario.externalLabels
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(
"GET http://example.org/federate?" + scenario.params + " HTTP/1.0\r\n\r\n",
)))

View file

@ -73,26 +73,24 @@ type Handler struct {
quitCh chan struct{}
reloadCh chan chan error
options *Options
configString string
config *config.Config
versionInfo *PrometheusVersion
birth time.Time
cwd string
flagsMap map[string]string
externalLabels model.LabelSet
mtx sync.RWMutex
now func() model.Time
ready uint32 // ready is uint32 rather than boolean to be able to use atomic functions.
}
// ApplyConfig updates the status state as the new config requires.
// ApplyConfig updates the config field of the Handler struct
func (h *Handler) ApplyConfig(conf *config.Config) error {
h.mtx.Lock()
defer h.mtx.Unlock()
h.externalLabels = conf.GlobalConfig.ExternalLabels
h.configString = conf.String()
h.config = conf
return nil
}
@ -158,12 +156,23 @@ func New(o *Options) *Handler {
storage: o.Storage,
notifier: o.Notifier,
apiV1: api_v1.NewAPI(o.QueryEngine, o.Storage, o.TargetManager, o.Notifier),
now: model.Now,
ready: 0,
}
h.apiV1 = api_v1.NewAPI(
o.QueryEngine,
o.Storage,
o.TargetManager,
o.Notifier,
func() config.Config {
h.mtx.RLock()
defer h.mtx.RUnlock()
return *h.config
},
)
if o.RoutePrefix != "/" {
// If the prefix is missing for the root path, prepend it.
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
@ -184,7 +193,7 @@ func New(o *Options) *Handler {
router.Get("/graph", readyf(instrf("graph", h.graph)))
router.Get("/status", readyf(instrf("status", h.status)))
router.Get("/flags", readyf(instrf("flags", h.flags)))
router.Get("/config", readyf(instrf("config", h.config)))
router.Get("/config", readyf(instrf("config", h.serveConfig)))
router.Get("/rules", readyf(instrf("rules", h.rules)))
router.Get("/targets", readyf(instrf("targets", h.targets)))
router.Get("/version", readyf(instrf("version", h.version)))
@ -404,11 +413,11 @@ func (h *Handler) flags(w http.ResponseWriter, r *http.Request) {
h.executeTemplate(w, "flags.html", h.flagsMap)
}
func (h *Handler) config(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveConfig(w http.ResponseWriter, r *http.Request) {
h.mtx.RLock()
defer h.mtx.RUnlock()
h.executeTemplate(w, "config.html", h.configString)
h.executeTemplate(w, "config.html", h.config.String())
}
func (h *Handler) rules(w http.ResponseWriter, r *http.Request) {