Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ You can also build just one of the binaries in this repo by passing a name to th
$ make build BINARIES=amtool
```

You can also load configuration from an HTTP endpoint:
```
$ ./alertmanager --config.http-url=http://config-server/config.yaml
```

Note: `--config.file` and `--config.http-url` are mutually exclusive - exactly one must be specified.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the repository’s command-block style.

The added fenced block triggers MD040, MD046, and MD014. Use the existing indented-command style and omit the $ prompt.

Suggested change
-```
-$ ./alertmanager --config.http-url=http://config-server/config.yaml
-```
+    ./alertmanager --config.http-url=http://config-server/config.yaml
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
You can also load configuration from an HTTP endpoint:
```
$ ./alertmanager --config.http-url=http://config-server/config.yaml
```
Note: `--config.file` and `--config.http-url` are mutually exclusive - exactly one must be specified.
You can also load configuration from an HTTP endpoint:
./alertmanager --config.http-url=http://config-server/config.yaml
Note: `--config.file` and `--config.http-url` are mutually exclusive - exactly one must be specified.
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 49-49: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 49-49: Code block style
Expected: indented; Actual: fenced

(MD046, code-block-style)


[warning] 50-50: Dollar signs used before commands without showing output

(MD014, commands-show-output)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 48 - 53, Update the HTTP configuration example in the
README to use the repository’s indented command style instead of a fenced code
block, and remove the leading shell prompt character. Preserve the command and
surrounding explanatory text.

Source: Linters/SAST tools


## Example

This is an example configuration that should cover most relevant aspects of the new YAML configuration format. The full documentation of the configuration can be found [here](https://prometheus.io/docs/alerting/configuration/).
Expand Down
22 changes: 21 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,25 @@ func (a *App) setup() error {

stopc := make(chan struct{})
var wg sync.WaitGroup
var loader config.ConfigLoader
if opts.ConfigHTTPURL != "" {
loader = config.NewHTTPLoader(opts.ConfigHTTPURL)
// Sanitize URL for logging to avoid credential leakage
sanitizedURL := config.SanitizeURL(opts.ConfigHTTPURL)
logger.Info("Starting Alertmanager in HTTP configuration mode", "source", sanitizedURL)
} else {
loader = config.NewFileLoader(opts.ConfigFile)
logger.Info("Starting Alertmanager in file configuration mode", "source", opts.ConfigFile)
}

// Load config once for both event recorder initialization and the
// first coordinator apply. Subsequent reloads go through
// configCoordinator.Reload() which reads the file again.
initialConf, err := config.LoadFile(opts.ConfigFile)
data, err := loader.Load(context.Background())
if err != nil {
return fmt.Errorf("error loading configuration: %w", err)
}
initialConf, err := config.Load(string(data))
if err != nil {
return fmt.Errorf("error loading configuration file: %w", err)
}
Expand Down Expand Up @@ -457,7 +471,13 @@ func (a *App) setup() error {
})

configLogger := logger.With("component", "configuration")
if opts.ConfigHTTPURL != "" {
loader = config.NewHTTPLoader(opts.ConfigHTTPURL)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

does this need to be reconstructed? It seems like we could use the loader that's assigned on either 253 or 258.

} else {
loader = config.NewFileLoader(opts.ConfigFile)
}
configCoordinator := config.NewCoordinator(
loader,
opts.ConfigFile,
reg,
configLogger,
Expand Down
195 changes: 195 additions & 0 deletions app/http_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright 2024 Prometheus Team

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

make: *** [Makefile.common:146: common-check_license] Error 1

please use the license header without date. eg https://github.com/prometheus/alertmanager/blob/main/config/config.go#L1-L12

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you @TheMeier for suggestions, will do the changes

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package app

import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/promslog"
"github.com/prometheus/exporter-toolkit/web"

"github.com/prometheus/alertmanager/featurecontrol"
)

func TestStartupWithHTTPConfig(t *testing.T) {
// Start a HTTP server that returns a minimal valid config.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("route:\n receiver: test\nreceivers:\n- name: test"))
}))
defer srv.Close()

// Create a temporary data directory.
dir := t.TempDir()

// Build minimal options for HTTP source.
webCfg := web.FlagConfig{}
webCfg.WebListenAddresses = &[]string{"127.0.0.1:0"}
webCfgFile := ""
webCfg.WebConfigFile = &webCfgFile

ff, err := featurecontrol.NewFlags(promslog.NewNopLogger(), "")
if err != nil {
t.Fatal(err)
}
opts := Options{
ConfigHTTPURL: srv.URL,
DataDir: dir,
Retention: DefaultRetention,
MaintenanceInterval: DefaultMaintenanceInterval,
AlertGCInterval: DefaultAlertGCInterval,
DispatchMaintenanceInterval: DefaultDispatchMaintenanceInterval,
WebConfig: &webCfg,
Logger: promslog.NewNopLogger(),
Registerer: prometheus.NewRegistry(),
Flagger: ff,
}

// Try to create the app (setup).
app, err := New(opts)
if err != nil {
t.Fatalf("failed to create app with HTTP config: %v", err)
}
defer func() { _ = app.Stop(context.Background()) }()

// Start the app.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = app.Start() }()

// Verify it started without error.
select {
case <-ctx.Done():
t.Fatal("app stopped unexpectedly")
default:
}
}

func TestStartupWithFileConfig(t *testing.T) {
// Create a temporary config file.
dir := t.TempDir()
configPath := filepath.Join(dir, "alertmanager.yml")
data := []byte("route:\n receiver: test\nreceivers:\n- name: test")
if err := os.WriteFile(configPath, data, 0o600); err != nil {
t.Fatal(err)
}

webCfg := web.FlagConfig{}
webCfg.WebListenAddresses = &[]string{"127.0.0.1:0"}
webCfgFile := ""
webCfg.WebConfigFile = &webCfgFile

ff, err := featurecontrol.NewFlags(promslog.NewNopLogger(), "")
if err != nil {
t.Fatal(err)
}
opts := Options{
ConfigFile: configPath,
DataDir: dir,
Retention: DefaultRetention,
MaintenanceInterval: DefaultMaintenanceInterval,
AlertGCInterval: DefaultAlertGCInterval,
DispatchMaintenanceInterval: DefaultDispatchMaintenanceInterval,
WebConfig: &webCfg,
Logger: promslog.NewNopLogger(),
Registerer: prometheus.NewRegistry(),
Flagger: ff,
}

app, err := New(opts)
if err != nil {
t.Fatalf("failed to create app with file config: %v", err)
}
defer func() { _ = app.Stop(context.Background()) }()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = app.Start() }()

select {
case <-ctx.Done():
t.Fatal("app stopped unexpectedly")
default:
}
}

func TestStartupWithBothSources(t *testing.T) {
// Create a temporary config file.
dir := t.TempDir()
configPath := filepath.Join(dir, "alertmanager.yml")
data := []byte("route:\n receiver: test\nreceivers:\n- name: test")
if err := os.WriteFile(configPath, data, 0o600); err != nil {
t.Fatal(err)
}

webCfg := web.FlagConfig{}
webCfg.WebListenAddresses = &[]string{"127.0.0.1:0"}
webCfgFile := ""
webCfg.WebConfigFile = &webCfgFile
ff, _ := featurecontrol.NewFlags(promslog.NewNopLogger(), "")
opts := Options{
ConfigFile: configPath,
ConfigHTTPURL: "http://example.com/config",
DataDir: dir,
Retention: DefaultRetention,
MaintenanceInterval: DefaultMaintenanceInterval,
AlertGCInterval: DefaultAlertGCInterval,
DispatchMaintenanceInterval: DefaultDispatchMaintenanceInterval,
WebConfig: &webCfg,
Logger: promslog.NewNopLogger(),
Registerer: prometheus.NewRegistry(),
Flagger: ff,
}

_, err := New(opts)
if err == nil {
t.Fatal("expected error when both config sources are set")
}
if err.Error() != "alertmanager/app: Options.ConfigFile and Options.ConfigHTTPURL are mutually exclusive" {
t.Fatalf("unexpected error message: %v", err)
}
}

func TestStartupWithNeitherSource(t *testing.T) {
webCfg := web.FlagConfig{}
webCfg.WebListenAddresses = &[]string{"127.0.0.1:0"}
webCfgFile := ""
webCfg.WebConfigFile = &webCfgFile
ff, _ := featurecontrol.NewFlags(promslog.NewNopLogger(), "")
opts := Options{
DataDir: t.TempDir(),
Retention: DefaultRetention,
MaintenanceInterval: DefaultMaintenanceInterval,
AlertGCInterval: DefaultAlertGCInterval,
DispatchMaintenanceInterval: DefaultDispatchMaintenanceInterval,
WebConfig: &webCfg,
Logger: promslog.NewNopLogger(),
Registerer: prometheus.NewRegistry(),
Flagger: ff,
}

_, err := New(opts)
if err == nil {
t.Fatal("expected error when no config source is set")
}
if err.Error() != "alertmanager/app: exactly one of Options.ConfigFile or Options.ConfigHTTPURL must be set" {
t.Fatalf("unexpected error message: %v", err)
}
}
2 changes: 1 addition & 1 deletion app/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func TestApp_reloadRouterClosedReloadChannel(t *testing.T) {
require.NoError(t, os.WriteFile(configPath, []byte(minimalConfig), 0o600))

var reloads atomic.Int64
coord := config.NewCoordinator(configPath, prometheus.NewRegistry(), promslog.NewNopLogger())
coord := config.NewCoordinator(config.NewFileLoader(configPath), configPath, prometheus.NewRegistry(), promslog.NewNopLogger())
coord.Subscribe(func(*config.Config) error {
reloads.Add(1)
return nil
Expand Down
12 changes: 9 additions & 3 deletions app/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ const (
// fields default to their zero value (which generally matches the kingpin
// flag default).
type Options struct {
// ConfigHTTPURL specifies the HTTP URL to load the Alertmanager configuration from.
// It is mutually exclusive with ConfigFile - exactly one must be specified.
ConfigHTTPURL string
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Storage and lifecycle.
ConfigFile string
DataDir string
Expand Down Expand Up @@ -111,7 +114,7 @@ type Options struct {
// Flagger) and a WebConfig before passing the result to New or Run.
func DefaultOptions() Options {
return Options{
ConfigFile: DefaultConfigFile,
ConfigFile: "",
DataDir: DefaultDataDir,
Retention: DefaultRetention,
MaintenanceInterval: DefaultMaintenanceInterval,
Expand Down Expand Up @@ -158,8 +161,11 @@ func (o *Options) validate() error {
}

// Storage and config paths.
if o.ConfigFile == "" {
return errors.New("alertmanager/app: Options.ConfigFile is required")
if o.ConfigFile == "" && o.ConfigHTTPURL == "" {
return errors.New("alertmanager/app: exactly one of Options.ConfigFile or Options.ConfigHTTPURL must be set")
}
if o.ConfigFile != "" && o.ConfigHTTPURL != "" {
return errors.New("alertmanager/app: Options.ConfigFile and Options.ConfigHTTPURL are mutually exclusive")
}
if o.DataDir == "" {
return errors.New("alertmanager/app: Options.DataDir is required")
Expand Down
12 changes: 11 additions & 1 deletion cmd/alertmanager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ func run() int {
}

var (
configFile = kingpin.Flag("config.file", "Alertmanager configuration file name.").Default("alertmanager.yml").String()
configFile = kingpin.Flag("config.file", "Alertmanager configuration file name.").String()
configHTTPURL = kingpin.Flag("config.http-url", "Alertmanager configuration URL (mutually exclusive with --config.file).").String()
dataDir = kingpin.Flag("storage.path", "Base path for data storage.").Default("data/").String()
retention = kingpin.Flag("data.retention", "How long to keep data for.").Default("120h").Duration()
maintenanceInterval = kingpin.Flag("data.maintenance-interval", "Interval between garbage collection and snapshotting to disk of the silences and the notification logs.").Default("15m").Duration()
Expand Down Expand Up @@ -97,6 +98,14 @@ func run() int {
kingpin.CommandLine.GetFlag("help").Short('h')
kingpin.Parse()

// Validate exactly one configuration source is provided.
if *configFile == "" && *configHTTPURL == "" {
kingpin.Fatalf("Need to configure one of the following --config.file or --config.http-url")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe this is a behavior regression - there may be production users of Alertmanager who depend on the default value of the --config.file flag. We cannot change this behavior without breaking those users.

}
if *configFile != "" && *configHTTPURL != "" {
kingpin.Fatalf("Need to configure only one of the following --config.file or --config.http-url")
}

logger := promslog.New(&promslogConfig)

prometheus.MustRegister(versioncollector.NewCollector("alertmanager"))
Expand Down Expand Up @@ -153,6 +162,7 @@ func run() int {

opts := app.Options{
ConfigFile: *configFile,
ConfigHTTPURL: *configHTTPURL,
DataDir: *dataDir,
Retention: *retention,
MaintenanceInterval: *maintenanceInterval,
Expand Down
Loading