Skip to content

Commit 94107da

Browse files
committed
feat: add config file support & example
Added support for YAML configuration file. Included prom-cli.example.yaml as a template. Updated Goreleaser config to include the example file in releases. Updated README with configuration documentation.
1 parent c9b73ec commit 94107da

8 files changed

Lines changed: 223 additions & 38 deletions

File tree

.goreleaser.dev.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ archives:
3232
files:
3333
- README.md
3434
- LICENSE
35+
- prom-cli.example.yaml
3536

3637
checksum:
3738
name_template: "{{ .ProjectName }}_checksums.txt"

.goreleaser.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ archives:
3939
files:
4040
- README.md
4141
- LICENSE
42+
- prom-cli.example.yaml
4243

4344
checksum:
4445
name_template: "{{ .ProjectName }}_checksums.txt"

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ A powerful command-line tool for querying Prometheus metrics with advanced autoc
2222
- [🚀 Usage](#-usage)
2323
- [Command Line Options](#command-line-options)
2424
- [Examples](#examples)
25+
- [⚙️ Configuration File](#️-configuration-file)
2526
- [🛠️ Development](#️-development)
2627
- [Prerequisites](#prerequisites)
2728
- [Building](#building)
@@ -181,6 +182,44 @@ echo "secret" > /tmp/pass
181182
./bin/prom-cli --enable-label-values=false
182183
```
183184

185+
## ⚙️ Configuration File
186+
187+
Prometheus CLI supports a YAML configuration file to persist your settings.
188+
189+
### File Location
190+
191+
By default, the application looks for a configuration file at `$HOME/.prom-cli.yaml`.
192+
You can also specify a custom path using the `--config` flag:
193+
194+
```bash
195+
./bin/prom-cli --config /path/to/config.yaml
196+
```
197+
198+
### Format
199+
200+
The configuration file uses YAML format. Keys match the command-line flags (using underscores). Here is an example with available options:
201+
202+
```yaml
203+
url: "http://prometheus-server:9090"
204+
username: "admin"
205+
# password: "secret" # Recommended to use password_file instead
206+
password_file: "/path/to/secret"
207+
insecure: false
208+
enable_label_values: true
209+
history_file: "/home/user/.prom_history"
210+
persist_history: true
211+
debug: false
212+
tips: true
213+
```
214+
215+
### Precedence
216+
217+
The application determines configuration values in the following order (highest priority first):
218+
1. **Command Line Flags** (e.g., `--url`)
219+
2. **Environment Variables** (e.g., `PROM_USERNAME`)
220+
3. **Configuration File** (values in `.prom-cli.yaml`)
221+
4. **Default Values**
222+
184223
## 📸 Screenshots
185224

186225
Here are some screenshots demonstrating the Prometheus CLI in action:

cmd/prom-cli/main.go

Lines changed: 99 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"prometheus-cli/internal/completion"
12+
"prometheus-cli/internal/config"
1213
"prometheus-cli/internal/display"
1314
"prometheus-cli/internal/prometheus"
1415

@@ -17,58 +18,83 @@ import (
1718
"github.com/prometheus/common/version"
1819
)
1920

20-
// Command-line flags for configuring the application behavior.
21-
var (
22-
// Prometheus Connection Flags
23-
url = kingpin.Flag("url", "Prometheus server URL.").Default("http://localhost:9090").String()
24-
username = kingpin.Flag("username", "Username for basic authentication.").Envar("PROM_USERNAME").String()
25-
password = kingpin.Flag("password", "Password for basic authentication.").Envar("PROM_PASSWORD").String()
26-
passwordFile = kingpin.Flag("password-file", "Path to file containing password for basic authentication.").String()
27-
insecure = kingpin.Flag("insecure", "Skip TLS certificate verification.").Bool()
28-
29-
// Autocompletion Flags
30-
enableLabelValues = kingpin.Flag("enable-label-values", "Enable autocompletion for label values.").Default("true").Bool()
31-
32-
// History Flags
33-
historyFile = kingpin.Flag("history-file", "Path to the command history file.").String()
34-
persistHistory = kingpin.Flag("persist-history", "Do not delete the history file on exit.").Bool()
35-
36-
// Display and Utility Flags
37-
debug = kingpin.Flag("debug", "Enable verbose error output for debugging.").Bool()
38-
tips = kingpin.Flag("tips", "Display detailed feature and usage tips on startup.").Bool()
39-
)
40-
4121
// main is the entry point of the Prometheus CLI application.
4222
// It initializes the Prometheus client, sets up autocompletion, and runs the interactive query loop.
4323
func main() {
44-
// Configure command-line argument parsing
45-
kingpin.Version(version.Print("prom-cli"))
46-
kingpin.HelpFlag.Short('h')
47-
kingpin.Parse()
24+
// 1. Determine config file path (Priority: Flag --config > Home Dir > Default None)
25+
configPath := findConfigPath()
26+
27+
// 2. Load configuration (start with defaults, overwrite with file if exists)
28+
cfg := config.NewConfig()
29+
if configPath != "" {
30+
loadedCfg, err := config.LoadFromFile(configPath)
31+
if err == nil {
32+
cfg = loadedCfg
33+
} else if isExplicitConfigFlag() {
34+
// Only fail if user explicitly asked for a config file that fails to load
35+
fmt.Fprintf(os.Stderr, "Error loading config file %s: %v\n", configPath, err)
36+
os.Exit(1)
37+
}
38+
}
39+
40+
// 3. Define Flags (using Config values as Defaults)
41+
// Kingpin priority: Flag > Envar > Default (which is now Config)
42+
app := kingpin.New("prom-cli", "A powerful command-line tool for querying Prometheus metrics.")
43+
app.Version(version.Print("prom-cli"))
44+
app.HelpFlag.Short('h')
45+
46+
var (
47+
cfgFile = app.Flag("config", "Path to configuration file.").Default(configPath).String()
48+
49+
// Prometheus Connection Flags
50+
url = app.Flag("url", "Prometheus server URL.").Default(cfg.URL).String()
51+
username = app.Flag("username", "Username for basic authentication.").Envar("PROM_USERNAME").Default(cfg.Username).String()
52+
password = app.Flag("password", "Password for basic authentication.").Envar("PROM_PASSWORD").Default(cfg.Password).String()
53+
passwordFile = app.Flag("password-file", "Path to file containing password for basic authentication.").Default(cfg.PasswordFile).String()
54+
insecure = app.Flag("insecure", "Skip TLS certificate verification.").Default(fmt.Sprintf("%v", cfg.Insecure)).Bool()
55+
56+
// Autocompletion Flags
57+
enableLabelValues = app.Flag("enable-label-values", "Enable autocompletion for label values.").Default(fmt.Sprintf("%v", cfg.EnableLabelValues)).Bool()
58+
59+
// History Flags
60+
historyFile = app.Flag("history-file", "Path to the command history file.").Default(cfg.HistoryFile).String()
61+
persistHistory = app.Flag("persist-history", "Do not delete the history file on exit.").Default(fmt.Sprintf("%v", cfg.PersistHistory)).Bool()
62+
63+
// Display and Utility Flags
64+
debug = app.Flag("debug", "Enable verbose error output for debugging.").Default(fmt.Sprintf("%v", cfg.Debug)).Bool()
65+
tips = app.Flag("tips", "Display detailed feature and usage tips on startup.").Default(fmt.Sprintf("%v", cfg.Tips)).Bool()
66+
)
67+
68+
kingpin.MustParse(app.Parse(os.Args[1:]))
4869

4970
// Handle password file if provided
5071
if *passwordFile != "" {
5172
if *password != "" {
52-
kingpin.FatalUsage("Cannot use both --password and --password-file")
73+
app.FatalUsage("Cannot use both --password and --password-file")
5374
}
5475
content, err := os.ReadFile(*passwordFile)
5576
if err != nil {
56-
kingpin.Fatalf("Error reading password file: %v", err)
77+
app.Fatalf("Error reading password file: %v", err)
5778
}
5879
*password = strings.TrimSpace(string(content))
5980
}
6081

6182
// Display welcome message and feature information if tips are enabled
6283
if *tips {
63-
printWelcomeMessage()
84+
printWelcomeMessage(*tips)
6485
} else {
6586
fmt.Println("Enter Prometheus queries. Press Ctrl+C to exit.")
6687
}
6788

6889
// Initialize Prometheus client with user-provided configuration
6990
if *debug {
91+
if configPath != "" && *cfgFile == configPath {
92+
fmt.Printf("Debug: Loaded configuration from %s\n", configPath)
93+
}
7094
fmt.Printf("Debug: Setting Prometheus URL to %s/api/v1\n", *url)
71-
fmt.Printf("Debug: Setting Basic Auth with username: %s\n", *username)
95+
if *username != "" {
96+
fmt.Printf("Debug: Setting Basic Auth with username: %s\n", *username)
97+
}
7298
fmt.Printf("Debug: Setting TLS InsecureSkipVerify to %t\n", *insecure)
7399
}
74100
prometheus.SetPrometheusURL(*url + "/api/v1")
@@ -146,13 +172,10 @@ func main() {
146172
}
147173

148174
// Schedule the history file to be removed if persistence is not requested.
149-
if *debug {
150-
fmt.Printf("Debug: shouldRemoveHistoryFile is %t before defer registration.\n", shouldRemoveHistoryFile)
151-
}
152175
if shouldRemoveHistoryFile {
153176
defer func() {
154177
if *debug {
155-
fmt.Printf("Debug: Inside defer. shouldRemoveHistoryFile is %t. Attempting to remove history file: %s\n", shouldRemoveHistoryFile, historyFilePath)
178+
fmt.Printf("Debug: Removing history file: %s\n", historyFilePath)
156179
}
157180
if err := os.Remove(historyFilePath); err != nil {
158181
fmt.Fprintf(os.Stderr, "Warning: could not remove history file %s: %v\n", historyFilePath, err)
@@ -179,14 +202,52 @@ func main() {
179202
}()
180203

181204
// Run the main interactive query loop
182-
runQueryLoop(l)
205+
runQueryLoop(l, *debug)
206+
}
207+
208+
// findConfigPath looks for a configuration file.
209+
// Priority:
210+
// 1. --config flag in os.Args
211+
// 2. $HOME/.prom-cli.yaml
212+
func findConfigPath() string {
213+
// 1. Check args
214+
for i, arg := range os.Args {
215+
if arg == "--config" && i+1 < len(os.Args) {
216+
return os.Args[i+1]
217+
}
218+
if strings.HasPrefix(arg, "--config=") {
219+
return strings.TrimPrefix(arg, "--config=")
220+
}
221+
}
222+
223+
// 2. Check Home Directory
224+
home, err := os.UserHomeDir()
225+
if err == nil {
226+
defaultPath := filepath.Join(home, ".prom-cli.yaml")
227+
if _, err := os.Stat(defaultPath); err == nil {
228+
return defaultPath
229+
}
230+
}
231+
232+
return ""
233+
}
234+
235+
// isExplicitConfigFlag checks if the user explicitly provided the --config flag.
236+
// This is used to decide whether to error out if the file is missing.
237+
func isExplicitConfigFlag() bool {
238+
for _, arg := range os.Args {
239+
if strings.HasPrefix(arg, "--config") {
240+
return true
241+
}
242+
}
243+
return false
183244
}
184245

185246
// printWelcomeMessage displays the welcome message and available features.
186-
func printWelcomeMessage() {
247+
func printWelcomeMessage(showTips bool) {
187248
fmt.Println("Enter Prometheus queries. Press Ctrl+C to exit.")
188249

189-
if *tips {
250+
if showTips {
190251
fmt.Print(`
191252
✨ Features:
192253
- Metric Names: Smart autocompletion for all available Prometheus metrics
@@ -206,7 +267,7 @@ func printWelcomeMessage() {
206267
}
207268

208269
// runQueryLoop runs the main interactive loop for processing user queries.
209-
func runQueryLoop(l *readline.Instance) {
270+
func runQueryLoop(l *readline.Instance, debugMode bool) {
210271
for {
211272
line, err := l.Readline()
212273
if err == readline.ErrInterrupt {
@@ -224,7 +285,7 @@ func runQueryLoop(l *readline.Instance) {
224285
// Execute the Prometheus query and display results
225286
results, err := prometheus.QueryPrometheus(query)
226287
if err != nil {
227-
if *debug {
288+
if debugMode {
228289
fmt.Printf("Error executing query: %v\n", err)
229290
} else {
230291
fmt.Printf("Error executing query. Use --debug for more details.\n")

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ require (
99
github.com/chzyer/readline v1.5.1
1010
github.com/olekukonko/tablewriter v1.0.9
1111
github.com/prometheus/common v0.65.0
12+
gopkg.in/yaml.v3 v3.0.1
1213
)
1314

1415
require (
1516
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
1617
github.com/fatih/color v1.15.0 // indirect
18+
github.com/kr/pretty v0.3.1 // indirect
1719
github.com/mattn/go-colorable v0.1.13 // indirect
1820
github.com/mattn/go-isatty v0.0.19 // indirect
1921
github.com/mattn/go-runewidth v0.0.16 // indirect

go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI
88
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
99
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
1010
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
11+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
1112
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1213
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1314
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1415
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
1516
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
17+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
18+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
19+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
20+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
1621
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
1722
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
1823
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -26,12 +31,16 @@ github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
2631
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
2732
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
2833
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
34+
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
2935
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
3036
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3137
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
3238
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
3339
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
3440
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
41+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
42+
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
43+
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
3544
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
3645
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
3746
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@@ -44,6 +53,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4453
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
4554
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
4655
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
56+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
57+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
4758
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
4859
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4960
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/config/config.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package config
2+
3+
import (
4+
"os"
5+
6+
"gopkg.in/yaml.v3"
7+
)
8+
9+
// Config holds the application configuration.
10+
type Config struct {
11+
URL string `yaml:"url"`
12+
Username string `yaml:"username"`
13+
Password string `yaml:"password"`
14+
PasswordFile string `yaml:"password_file"`
15+
Insecure bool `yaml:"insecure"`
16+
EnableLabelValues bool `yaml:"enable_label_values"`
17+
HistoryFile string `yaml:"history_file"`
18+
PersistHistory bool `yaml:"persist_history"`
19+
Debug bool `yaml:"debug"`
20+
Tips bool `yaml:"tips"`
21+
}
22+
23+
// NewConfig returns a Config with default values.
24+
func NewConfig() *Config {
25+
return &Config{
26+
URL: "http://localhost:9090",
27+
EnableLabelValues: true,
28+
Tips: false,
29+
}
30+
}
31+
32+
// LoadFromFile reads the configuration from a YAML file.
33+
func LoadFromFile(path string) (*Config, error) {
34+
data, err := os.ReadFile(path)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
config := NewConfig() // Start with defaults
40+
if err := yaml.Unmarshal(data, config); err != nil {
41+
return nil, err
42+
}
43+
44+
return config, nil
45+
}

0 commit comments

Comments
 (0)