Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
48 changes: 33 additions & 15 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,16 +246,28 @@ func (a *App) setup() error {
m.clusterEnabled.Set(1)
}

stopc := make(chan struct{})
var wg sync.WaitGroup

// 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)
if err != nil {
return fmt.Errorf("error loading configuration file: %w", err)
}
stopc := make(chan struct{})
var wg sync.WaitGroup
var loader config.ConfigLoader
if opts.ConfigHTTPURL != "" {
loader = config.NewHTTPLoader(opts.ConfigHTTPURL)
logger.Info("Starting Alertmanager in HTTP configuration mode", "source", opts.ConfigHTTPURL)
} 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.
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)
}

hostname, _ := os.Hostname()
var eventRec eventrecorder.Recorder
Expand Down Expand Up @@ -457,11 +469,17 @@ func (a *App) setup() error {
})

configLogger := logger.With("component", "configuration")
configCoordinator := config.NewCoordinator(
opts.ConfigFile,
reg,
configLogger,
)
if opts.ConfigHTTPURL != "" {
loader = config.NewHTTPLoader(opts.ConfigHTTPURL)
} else {
loader = config.NewFileLoader(opts.ConfigFile)
}
configCoordinator := config.NewCoordinator(
loader,
opts.ConfigFile,
reg,
configLogger,
)
a.coordinator = configCoordinator

// The reloader owns the config-scoped subgraph (templates, routes,
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
10 changes: 7 additions & 3 deletions app/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const (
// fields default to their zero value (which generally matches the kingpin
// flag default).
type Options struct {
ConfigHTTPURL string
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Storage and lifecycle.
ConfigFile string
DataDir string
Expand Down Expand Up @@ -111,7 +112,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 +159,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