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) { 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) 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. // YAML marshalling must not reveal authentication credentials.
s := c.String() func TestElideSecrets(t *testing.T) {
secretRe := regexp.MustCompile("<secret>") c, err := LoadFile("testdata/conf.good.yml")
matches := secretRe.FindAllStringIndex(s, -1) if err != nil {
if len(matches) != 6 || strings.Contains(s, "mysecret") { t.Fatalf("Error parsing %s: %s", "testdata/conf.good.yml", err)
t.Fatalf("config's String method reveals authentication credentials.")
} }
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) { func TestLoadConfigRuleFilesAbsolutePath(t *testing.T) {

View file

@ -29,6 +29,7 @@ import (
"github.com/prometheus/common/route" "github.com/prometheus/common/route"
"golang.org/x/net/context" "golang.org/x/net/context"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/retrieval" "github.com/prometheus/prometheus/retrieval"
"github.com/prometheus/prometheus/storage/local" "github.com/prometheus/prometheus/storage/local"
@ -103,17 +104,19 @@ type API struct {
targetRetriever targetRetriever targetRetriever targetRetriever
alertmanagerRetriever alertmanagerRetriever alertmanagerRetriever alertmanagerRetriever
now func() model.Time now func() model.Time
config func() config.Config
} }
// NewAPI returns an initialized API type. // 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{ return &API{
QueryEngine: qe, QueryEngine: qe,
Storage: st, Storage: st,
targetRetriever: tr, targetRetriever: tr,
alertmanagerRetriever: ar, alertmanagerRetriever: ar,
now: model.Now, 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("/targets", instr("targets", api.targets))
r.Get("/alertmanagers", instr("alertmanagers", api.alertmanagers)) r.Get("/alertmanagers", instr("alertmanagers", api.alertmanagers))
r.Get("/status/config", instr("config", api.serveConfig))
} }
type queryData struct { type queryData struct {
@ -436,6 +441,17 @@ func (api *API) alertmanagers(r *http.Request) (interface{}, *apiError) {
return ams, nil 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{}) { func respond(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)

View file

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

View file

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

View file

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

View file

@ -68,31 +68,29 @@ type Handler struct {
apiV1 *api_v1.API apiV1 *api_v1.API
router *route.Router router *route.Router
listenErrCh chan error listenErrCh chan error
quitCh chan struct{} quitCh chan struct{}
reloadCh chan chan error reloadCh chan chan error
options *Options options *Options
configString string config *config.Config
versionInfo *PrometheusVersion versionInfo *PrometheusVersion
birth time.Time birth time.Time
cwd string cwd string
flagsMap map[string]string flagsMap map[string]string
externalLabels model.LabelSet mtx sync.RWMutex
mtx sync.RWMutex now func() model.Time
now func() model.Time
ready uint32 // ready is uint32 rather than boolean to be able to use atomic functions. 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 { func (h *Handler) ApplyConfig(conf *config.Config) error {
h.mtx.Lock() h.mtx.Lock()
defer h.mtx.Unlock() defer h.mtx.Unlock()
h.externalLabels = conf.GlobalConfig.ExternalLabels h.config = conf
h.configString = conf.String()
return nil return nil
} }
@ -158,12 +156,23 @@ func New(o *Options) *Handler {
storage: o.Storage, storage: o.Storage,
notifier: o.Notifier, notifier: o.Notifier,
apiV1: api_v1.NewAPI(o.QueryEngine, o.Storage, o.TargetManager, o.Notifier), now: model.Now,
now: model.Now,
ready: 0, 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 o.RoutePrefix != "/" {
// If the prefix is missing for the root path, prepend it. // If the prefix is missing for the root path, prepend it.
router.Get("/", func(w http.ResponseWriter, r *http.Request) { 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("/graph", readyf(instrf("graph", h.graph)))
router.Get("/status", readyf(instrf("status", h.status))) router.Get("/status", readyf(instrf("status", h.status)))
router.Get("/flags", readyf(instrf("flags", h.flags))) 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("/rules", readyf(instrf("rules", h.rules)))
router.Get("/targets", readyf(instrf("targets", h.targets))) router.Get("/targets", readyf(instrf("targets", h.targets)))
router.Get("/version", readyf(instrf("version", h.version))) 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) 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() h.mtx.RLock()
defer h.mtx.RUnlock() 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) { func (h *Handler) rules(w http.ResponseWriter, r *http.Request) {