diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index e69e9f2db..d78e1420a 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -452,7 +452,7 @@ func main() { // Throw error for invalid config before starting other components. var cfgFile *config.Config - if cfgFile, err = config.LoadFile(cfg.configFile, false, log.NewNopLogger()); err != nil { + if cfgFile, err = config.LoadFile(cfg.configFile, agentMode, false, log.NewNopLogger()); err != nil { level.Error(logger).Log("msg", fmt.Sprintf("Error loading config (--config.file=%s)", cfg.configFile), "err", err) os.Exit(2) } @@ -1162,7 +1162,7 @@ func reloadConfig(filename string, expandExternalLabels bool, enableExemplarStor } }() - conf, err := config.LoadFile(filename, expandExternalLabels, logger) + conf, err := config.LoadFile(filename, agentMode, expandExternalLabels, logger) if err != nil { return errors.Wrapf(err, "couldn't load configuration (--config.file=%q)", filename) } diff --git a/cmd/prometheus/main_test.go b/cmd/prometheus/main_test.go index 6f9a1d566..6f4d58b94 100644 --- a/cmd/prometheus/main_test.go +++ b/cmd/prometheus/main_test.go @@ -37,6 +37,7 @@ import ( var promPath = os.Args[0] var promConfig = filepath.Join("..", "..", "documentation", "examples", "prometheus.yml") +var agentConfig = filepath.Join("..", "..", "documentation", "examples", "prometheus-agent.yml") var promData = filepath.Join(os.TempDir(), "data") func TestMain(m *testing.M) { @@ -349,7 +350,7 @@ func getCurrentGaugeValuesFor(t *testing.T, reg prometheus.Gatherer, metricNames } func TestAgentSuccessfulStartup(t *testing.T) { - prom := exec.Command(promPath, "-test.main", "--agent", "--config.file="+promConfig) + prom := exec.Command(promPath, "-test.main", "--agent", "--config.file="+agentConfig) err := prom.Start() require.NoError(t, err) @@ -367,3 +368,23 @@ func TestAgentSuccessfulStartup(t *testing.T) { } require.Equal(t, expectedExitStatus, actualExitStatus) } + +func TestAgentStartupWithInvalidConfig(t *testing.T) { + prom := exec.Command(promPath, "-test.main", "--agent", "--config.file="+promConfig) + err := prom.Start() + require.NoError(t, err) + + expectedExitStatus := 2 + actualExitStatus := 0 + + done := make(chan error, 1) + go func() { done <- prom.Wait() }() + select { + case err := <-done: + t.Logf("prometheus agent should not be running: %v", err) + actualExitStatus = prom.ProcessState.ExitCode() + case <-time.After(5 * time.Second): + prom.Process.Kill() + } + require.Equal(t, expectedExitStatus, actualExitStatus) +} diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go index 9b436ff71..8763e78b6 100644 --- a/cmd/promtool/main.go +++ b/cmd/promtool/main.go @@ -82,6 +82,7 @@ func main() { ).Required().ExistingFiles() checkMetricsCmd := checkCmd.Command("metrics", checkMetricsUsage) + agentMode := checkConfigCmd.Flag("agent", "Check config file for Prometheus in Agent mode.").Bool() queryCmd := app.Command("query", "Run query against a Prometheus server.") queryCmdFmt := queryCmd.Flag("format", "Output format of the query.").Short('o').Default("promql").Enum("promql", "json") @@ -202,7 +203,7 @@ func main() { switch parsedCmd { case checkConfigCmd.FullCommand(): - os.Exit(CheckConfig(*configFiles...)) + os.Exit(CheckConfig(*agentMode, *configFiles...)) case checkWebConfigCmd.FullCommand(): os.Exit(CheckWebConfig(*webConfigFiles...)) @@ -258,11 +259,11 @@ func main() { } // CheckConfig validates configuration files. -func CheckConfig(files ...string) int { +func CheckConfig(agentMode bool, files ...string) int { failed := false for _, f := range files { - ruleFiles, err := checkConfig(f) + ruleFiles, err := checkConfig(agentMode, f) if err != nil { fmt.Fprintln(os.Stderr, " FAILED:", err) failed = true @@ -317,10 +318,10 @@ func checkFileExists(fn string) error { return err } -func checkConfig(filename string) ([]string, error) { +func checkConfig(agentMode bool, filename string) ([]string, error) { fmt.Println("Checking", filename) - cfg, err := config.LoadFile(filename, false, log.NewNopLogger()) + cfg, err := config.LoadFile(filename, agentMode, false, log.NewNopLogger()) if err != nil { return nil, err } diff --git a/cmd/promtool/main_test.go b/cmd/promtool/main_test.go index 8ca078cc5..138548fc7 100644 --- a/cmd/promtool/main_test.go +++ b/cmd/promtool/main_test.go @@ -193,7 +193,7 @@ func TestCheckTargetConfig(t *testing.T) { } for _, test := range cases { t.Run(test.name, func(t *testing.T) { - _, err := checkConfig("testdata/" + test.file) + _, err := checkConfig(false, "testdata/"+test.file) if test.err != "" { require.Equalf(t, test.err, err.Error(), "Expected error %q, got %q", test.err, err.Error()) return diff --git a/config/config.go b/config/config.go index dc2ed19a2..9729642ae 100644 --- a/config/config.go +++ b/config/config.go @@ -99,7 +99,7 @@ func Load(s string, expandExternalLabels bool, logger log.Logger) (*Config, erro } // LoadFile parses the given YAML file into a Config. -func LoadFile(filename string, expandExternalLabels bool, logger log.Logger) (*Config, error) { +func LoadFile(filename string, agentMode bool, expandExternalLabels bool, logger log.Logger) (*Config, error) { content, err := ioutil.ReadFile(filename) if err != nil { return nil, err @@ -108,6 +108,25 @@ func LoadFile(filename string, expandExternalLabels bool, logger log.Logger) (*C if err != nil { return nil, errors.Wrapf(err, "parsing YAML file %s", filename) } + + if agentMode { + if len(cfg.RemoteWriteConfigs) == 0 { + return nil, errors.New("at least one remote_write target must be specified in agent mode") + } + + if len(cfg.AlertingConfig.AlertmanagerConfigs) > 0 || len(cfg.AlertingConfig.AlertRelabelConfigs) > 0 { + return nil, errors.New("field alerting is not allowed in agent mode") + } + + if len(cfg.RuleFiles) > 0 { + return nil, errors.New("field rule_files is not allowed in agent mode") + } + + if len(cfg.RemoteReadConfigs) > 0 { + return nil, errors.New("field remote_read is not allowed in agent mode") + } + } + cfg.SetDirectory(filepath.Dir(filename)) return cfg, nil } diff --git a/config/config_test.go b/config/config_test.go index a2efaa676..f26e4f3e6 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -986,7 +986,7 @@ var expectedConf = &Config{ } func TestYAMLRoundtrip(t *testing.T) { - want, err := LoadFile("testdata/roundtrip.good.yml", false, log.NewNopLogger()) + want, err := LoadFile("testdata/roundtrip.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) out, err := yaml.Marshal(want) @@ -999,7 +999,7 @@ func TestYAMLRoundtrip(t *testing.T) { } func TestRemoteWriteRetryOnRateLimit(t *testing.T) { - want, err := LoadFile("testdata/remote_write_retry_on_rate_limit.good.yml", false, log.NewNopLogger()) + want, err := LoadFile("testdata/remote_write_retry_on_rate_limit.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) out, err := yaml.Marshal(want) @@ -1015,16 +1015,16 @@ func TestRemoteWriteRetryOnRateLimit(t *testing.T) { func TestLoadConfig(t *testing.T) { // Parse a valid file that sets a global scrape timeout. This tests whether parsing // an overwritten default field in the global config permanently changes the default. - _, err := LoadFile("testdata/global_timeout.good.yml", false, log.NewNopLogger()) + _, err := LoadFile("testdata/global_timeout.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) - c, err := LoadFile("testdata/conf.good.yml", false, log.NewNopLogger()) + c, err := LoadFile("testdata/conf.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) require.Equal(t, expectedConf, c) } func TestScrapeIntervalLarger(t *testing.T) { - c, err := LoadFile("testdata/scrape_interval_larger.good.yml", false, log.NewNopLogger()) + c, err := LoadFile("testdata/scrape_interval_larger.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) require.Equal(t, 1, len(c.ScrapeConfigs)) for _, sc := range c.ScrapeConfigs { @@ -1034,7 +1034,7 @@ func TestScrapeIntervalLarger(t *testing.T) { // YAML marshaling must not reveal authentication credentials. func TestElideSecrets(t *testing.T) { - c, err := LoadFile("testdata/conf.good.yml", false, log.NewNopLogger()) + c, err := LoadFile("testdata/conf.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) secretRe := regexp.MustCompile(`\\u003csecret\\u003e|`) @@ -1051,31 +1051,31 @@ func TestElideSecrets(t *testing.T) { func TestLoadConfigRuleFilesAbsolutePath(t *testing.T) { // Parse a valid file that sets a rule files with an absolute path - c, err := LoadFile(ruleFilesConfigFile, false, log.NewNopLogger()) + c, err := LoadFile(ruleFilesConfigFile, false, false, log.NewNopLogger()) require.NoError(t, err) require.Equal(t, ruleFilesExpectedConf, c) } func TestKubernetesEmptyAPIServer(t *testing.T) { - _, err := LoadFile("testdata/kubernetes_empty_apiserver.good.yml", false, log.NewNopLogger()) + _, err := LoadFile("testdata/kubernetes_empty_apiserver.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) } func TestKubernetesWithKubeConfig(t *testing.T) { - _, err := LoadFile("testdata/kubernetes_kubeconfig_without_apiserver.good.yml", false, log.NewNopLogger()) + _, err := LoadFile("testdata/kubernetes_kubeconfig_without_apiserver.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) } func TestKubernetesSelectors(t *testing.T) { - _, err := LoadFile("testdata/kubernetes_selectors_endpoints.good.yml", false, log.NewNopLogger()) + _, err := LoadFile("testdata/kubernetes_selectors_endpoints.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) - _, err = LoadFile("testdata/kubernetes_selectors_node.good.yml", false, log.NewNopLogger()) + _, err = LoadFile("testdata/kubernetes_selectors_node.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) - _, err = LoadFile("testdata/kubernetes_selectors_ingress.good.yml", false, log.NewNopLogger()) + _, err = LoadFile("testdata/kubernetes_selectors_ingress.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) - _, err = LoadFile("testdata/kubernetes_selectors_pod.good.yml", false, log.NewNopLogger()) + _, err = LoadFile("testdata/kubernetes_selectors_pod.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) - _, err = LoadFile("testdata/kubernetes_selectors_service.good.yml", false, log.NewNopLogger()) + _, err = LoadFile("testdata/kubernetes_selectors_service.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) } @@ -1381,7 +1381,7 @@ var expectedErrors = []struct { func TestBadConfigs(t *testing.T) { for _, ee := range expectedErrors { - _, err := LoadFile("testdata/"+ee.filename, false, log.NewNopLogger()) + _, err := LoadFile("testdata/"+ee.filename, false, false, log.NewNopLogger()) require.Error(t, err, "%s", ee.filename) require.Contains(t, err.Error(), ee.errMsg, "Expected error for %s to contain %q but got: %s", ee.filename, ee.errMsg, err) @@ -1415,20 +1415,20 @@ func TestExpandExternalLabels(t *testing.T) { // Cleanup ant TEST env variable that could exist on the system. os.Setenv("TEST", "") - c, err := LoadFile("testdata/external_labels.good.yml", false, log.NewNopLogger()) + c, err := LoadFile("testdata/external_labels.good.yml", false, false, log.NewNopLogger()) require.NoError(t, err) require.Equal(t, labels.Label{Name: "bar", Value: "foo"}, c.GlobalConfig.ExternalLabels[0]) require.Equal(t, labels.Label{Name: "baz", Value: "foo${TEST}bar"}, c.GlobalConfig.ExternalLabels[1]) require.Equal(t, labels.Label{Name: "foo", Value: "${TEST}"}, c.GlobalConfig.ExternalLabels[2]) - c, err = LoadFile("testdata/external_labels.good.yml", true, log.NewNopLogger()) + c, err = LoadFile("testdata/external_labels.good.yml", false, true, log.NewNopLogger()) require.NoError(t, err) require.Equal(t, labels.Label{Name: "bar", Value: "foo"}, c.GlobalConfig.ExternalLabels[0]) require.Equal(t, labels.Label{Name: "baz", Value: "foobar"}, c.GlobalConfig.ExternalLabels[1]) require.Equal(t, labels.Label{Name: "foo", Value: ""}, c.GlobalConfig.ExternalLabels[2]) os.Setenv("TEST", "TestValue") - c, err = LoadFile("testdata/external_labels.good.yml", true, log.NewNopLogger()) + c, err = LoadFile("testdata/external_labels.good.yml", false, true, log.NewNopLogger()) require.NoError(t, err) require.Equal(t, labels.Label{Name: "bar", Value: "foo"}, c.GlobalConfig.ExternalLabels[0]) require.Equal(t, labels.Label{Name: "baz", Value: "fooTestValuebar"}, c.GlobalConfig.ExternalLabels[1]) diff --git a/documentation/examples/prometheus-agent.yml b/documentation/examples/prometheus-agent.yml new file mode 100644 index 000000000..0e5180817 --- /dev/null +++ b/documentation/examples/prometheus-agent.yml @@ -0,0 +1,22 @@ +# my global config +global: + scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + # scrape_timeout is set to the global default (10s). + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: "prometheus" + + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + + static_configs: + - targets: ["localhost:9090"] + +# When running prometheus in Agent mode, remote-write is required. +remote_write: + # Agent is able to run with a invalid remote-write URL, but, of course, will fail to push timeseries. + - url: "http://remote-write-url"