Skip to content

Commit fddd7c6

Browse files
committed
chore: try fix crashing on recuring scrape
1 parent 586d603 commit fddd7c6

File tree

5 files changed

+366
-368
lines changed

5 files changed

+366
-368
lines changed

README.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ This exporter queries the Local API and exposes rich Prometheus metrics includin
2121
### Method 1: Auto-Registration
2222

2323
If your CrowdSec instance supports auto-registration with a token:
24+
**Note:** When using auto-registration with a token, the provided login/password will be used for the machine registration.
2425

2526
```bash
2627
./crowdsec-exporter \
2728
--crowdsec-url http://localhost:8080 \
29+
--crowdsec-login your-machine-login \ # crowdsec-exporter
30+
--crowdsec-password your-machine-password \ # make 16+ characters
2831
--crowdsec-registration-token ${REGISTRATION_TOKEN} \
2932
--log-level debug
3033
```
3134

32-
### Method 2: Manual Machine Account
35+
### Method 2: Existing Machine Account
3336

3437
```bash
3538
./crowdsec-exporter \
@@ -43,17 +46,18 @@ Metrics are exposed at `http://localhost:9090/metrics`.
4346

4447
## Configuration Options
4548

46-
| Flag | Environment Variable | Default | Description |
47-
| ------------------------------- | ----------------------------------------------- | ----------------------- | -------------------------------------- |
48-
| `--crowdsec-url` | `CROWDSEC_EXPORTER_CROWDSEC_URL` | `http://localhost:8080` | CrowdSec Local API URL |
49-
| `--crowdsec-login` | `CROWDSEC_EXPORTER_CROWDSEC_LOGIN` | - | Machine login (manual auth) |
50-
| `--crowdsec-password` | `CROWDSEC_EXPORTER_CROWDSEC_PASSWORD` | - | Machine password (manual auth) |
51-
| `--crowdsec-registration-token` | `CROWDSEC_EXPORTER_CROWDSEC_REGISTRATION_TOKEN` | - | Registration token (auto-registration) |
52-
| `--crowdsec-machine-name` | `CROWDSEC_EXPORTER_CROWDSEC_MACHINE_NAME` | hostname | Machine name used during registration |
53-
| `--listen-address` | `CROWDSEC_EXPORTER_SERVER_LISTEN_ADDRESS` | `:9090` | Listen address |
54-
| `--metrics-path` | `CROWDSEC_EXPORTER_SERVER_METRICS_PATH` | `/metrics` | Metrics endpoint |
55-
| `--instance-name` | `CROWDSEC_EXPORTER_EXPORTER_INSTANCE_NAME` | `crowdsec` | Instance label |
56-
| `--log-level` | `CROWDSEC_EXPORTER_LOG_LEVEL` | `info` | Log level (debug, info, warn, error) |
49+
| Flag | Environment Variable | Default | Description |
50+
| ------------------------------- | ----------------------------------------------- | ----------------------- | ------------------------------------------- |
51+
| `--crowdsec-url` | `CROWDSEC_EXPORTER_CROWDSEC_URL` | `http://localhost:8080` | CrowdSec Local API URL |
52+
| `--crowdsec-login` | `CROWDSEC_EXPORTER_CROWDSEC_LOGIN` | - | Machine login (required) |
53+
| `--crowdsec-password` | `CROWDSEC_EXPORTER_CROWDSEC_PASSWORD` | - | Machine password (required) |
54+
| `--crowdsec-registration-token` | `CROWDSEC_EXPORTER_CROWDSEC_REGISTRATION_TOKEN` | - | Registration token (optional, for auto-reg) |
55+
| `--crowdsec-machine-name` | `CROWDSEC_EXPORTER_CROWDSEC_MACHINE_NAME` | hostname | Machine name used during registration |
56+
| `--crowdsec-deregister-on-exit` | `CROWDSEC_EXPORTER_CROWDSEC_DEREGISTER_ON_EXIT` | `false` | Deregister machine on exit |
57+
| `--listen-address` | `CROWDSEC_EXPORTER_SERVER_LISTEN_ADDRESS` | `:9090` | Listen address |
58+
| `--metrics-path` | `CROWDSEC_EXPORTER_SERVER_METRICS_PATH` | `/metrics` | Metrics endpoint |
59+
| `--instance-name` | `CROWDSEC_EXPORTER_EXPORTER_INSTANCE_NAME` | `crowdsec` | Instance label |
60+
| `--log-level` | `CROWDSEC_EXPORTER_LOG_LEVEL` | `info` | Log level (debug, info, warn, error) |
5761

5862
## Installation
5963

cmd/crowdsec-exporter/main.go

Lines changed: 63 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"github.com/hydazz/crowdsec-exporter/internal/config"
15+
"github.com/hydazz/crowdsec-exporter/internal/crowdsec"
1516
"github.com/hydazz/crowdsec-exporter/internal/exporter"
1617
"github.com/prometheus/client_golang/prometheus/promhttp"
1718
"github.com/spf13/cobra"
@@ -26,80 +27,66 @@ var (
2627

2728
func main() {
2829
if err := newRootCmd().Execute(); err != nil {
29-
slog.Error("Command execution failed", "error", err)
30+
slog.Error("command failed", "error", err)
3031
os.Exit(1)
3132
}
3233
}
3334

3435
func newRootCmd() *cobra.Command {
3536
cmd := &cobra.Command{
36-
Use: "crowdsec-exporter",
37-
Short: "Prometheus exporter for CrowdSec decisions",
38-
Long: `A Prometheus exporter that exposes CrowdSec decisions with rich geographical
39-
and ASN information as metrics, compatible with Grafana dashboards.`,
40-
41-
SilenceUsage: true, // Don't show usage on errors
42-
SilenceErrors: true, // We handle errors manually
37+
Use: "crowdsec-exporter",
38+
Short: "Prometheus exporter for CrowdSec decisions",
39+
Long: "A Prometheus exporter that exposes CrowdSec decisions with geographical and ASN info as metrics.",
40+
SilenceUsage: true,
41+
SilenceErrors: true,
4342
RunE: func(cmd *cobra.Command, args []string) error {
4443
return runExporter()
4544
},
4645
}
4746

48-
// Setup Viper for automatic env binding
4947
viper.SetEnvPrefix("CROWDSEC_EXPORTER")
5048
viper.AutomaticEnv()
5149
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
5250

53-
flags := cmd.Flags()
54-
flags.String("crowdsec-url", "http://localhost:8080", "CrowdSec Local API URL")
55-
flags.String("crowdsec-login", "", "CrowdSec machine login")
56-
flags.String("crowdsec-password", "", "CrowdSec machine password")
57-
flags.String("crowdsec-registration-token", "", "CrowdSec auto-registration token")
58-
flags.String("crowdsec-machine-name", "", "Machine name for auto-registration (defaults to hostname)")
59-
flags.String("listen-address", ":9090", "Address to listen on for web interface and metrics")
60-
flags.String("metrics-path", "/metrics", "Path under which to expose metrics")
61-
flags.String("instance-name", "crowdsec", "Instance name to use in metrics labels")
62-
flags.String("log-level", "info", "Log level (debug, info, warn, error)")
63-
64-
// Bind flags to viper
65-
if err := viper.BindPFlag("crowdsec.url", flags.Lookup("crowdsec-url")); err != nil {
66-
panic(fmt.Sprintf("failed to bind crowdsec-url flag: %v", err))
67-
}
68-
if err := viper.BindPFlag("crowdsec.login", flags.Lookup("crowdsec-login")); err != nil {
69-
panic(fmt.Sprintf("failed to bind crowdsec-login flag: %v", err))
70-
}
71-
if err := viper.BindPFlag("crowdsec.password", flags.Lookup("crowdsec-password")); err != nil {
72-
panic(fmt.Sprintf("failed to bind crowdsec-password flag: %v", err))
73-
}
74-
if err := viper.BindPFlag("crowdsec.registration_token", flags.Lookup("crowdsec-registration-token")); err != nil {
75-
panic(fmt.Sprintf("failed to bind crowdsec-registration-token flag: %v", err))
76-
}
77-
if err := viper.BindPFlag("crowdsec.machine_name", flags.Lookup("crowdsec-machine-name")); err != nil {
78-
panic(fmt.Sprintf("failed to bind crowdsec-machine-name flag: %v", err))
79-
}
80-
if err := viper.BindPFlag("server.listen_address", flags.Lookup("listen-address")); err != nil {
81-
panic(fmt.Sprintf("failed to bind listen-address flag: %v", err))
82-
}
83-
if err := viper.BindPFlag("server.metrics_path", flags.Lookup("metrics-path")); err != nil {
84-
panic(fmt.Sprintf("failed to bind metrics-path flag: %v", err))
85-
}
86-
if err := viper.BindPFlag("exporter.instance_name", flags.Lookup("instance-name")); err != nil {
87-
panic(fmt.Sprintf("failed to bind instance-name flag: %v", err))
88-
}
89-
if err := viper.BindPFlag("log_level", flags.Lookup("log-level")); err != nil {
90-
panic(fmt.Sprintf("failed to bind log-level flag: %v", err))
51+
f := cmd.Flags()
52+
f.String("crowdsec-url", "http://localhost:8080", "CrowdSec Local API URL")
53+
f.String("crowdsec-login", "", "CrowdSec machine login")
54+
f.String("crowdsec-password", "", "CrowdSec machine password")
55+
f.String("crowdsec-registration-token", "", "CrowdSec auto-registration token")
56+
f.String("crowdsec-machine-name", "", "Machine name for auto-registration (defaults to hostname)")
57+
f.Bool("crowdsec-deregister-on-exit", false, "Deregister machine on application exit")
58+
f.String("listen-address", ":9090", "Address to listen on for web interface and metrics")
59+
f.String("metrics-path", "/metrics", "Path under which to expose metrics")
60+
f.String("instance-name", "crowdsec", "Instance name to use in metrics labels")
61+
f.String("log-level", "info", "Log level (debug, info, warn, error)")
62+
63+
binds := map[string]string{
64+
"crowdsec.url": "crowdsec-url",
65+
"crowdsec.login": "crowdsec-login",
66+
"crowdsec.password": "crowdsec-password",
67+
"crowdsec.registration_token": "crowdsec-registration-token",
68+
"crowdsec.machine_name": "crowdsec-machine-name",
69+
"crowdsec.deregister_on_exit": "crowdsec-deregister-on-exit",
70+
"server.listen_address": "listen-address",
71+
"server.metrics_path": "metrics-path",
72+
"exporter.instance_name": "instance-name",
73+
"log_level": "log-level",
74+
}
75+
for key, flag := range binds {
76+
if err := viper.BindPFlag(key, f.Lookup(flag)); err != nil {
77+
panic(fmt.Sprintf("bind flag %q: %v", flag, err))
78+
}
9179
}
9280

9381
cmd.AddCommand(newVersionCmd())
94-
9582
return cmd
9683
}
9784

9885
func newVersionCmd() *cobra.Command {
9986
return &cobra.Command{
10087
Use: "version",
10188
Short: "Show version information",
102-
Run: func(cmd *cobra.Command, args []string) {
89+
Run: func(*cobra.Command, []string) {
10390
fmt.Printf("crowdsec-exporter %s\n", version)
10491
fmt.Printf("commit: %s\n", commit)
10592
fmt.Printf("built: %s\n", date)
@@ -108,72 +95,67 @@ func newVersionCmd() *cobra.Command {
10895
}
10996

11097
func runExporter() error {
111-
// Load config from flags and env vars
11298
cfg := &config.Config{}
11399
if err := viper.Unmarshal(cfg); err != nil {
114-
return fmt.Errorf("unable to decode config: %w", err)
100+
return fmt.Errorf("decode config: %w", err)
115101
}
116-
117-
// Validate configuration
118102
if err := cfg.Validate(); err != nil {
119-
return fmt.Errorf("configuration validation failed: %w", err)
103+
return fmt.Errorf("config validation: %w", err)
120104
}
121105

122-
// Set up structured logging with slog
123-
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
124-
Level: cfg.GetLogLevel(),
125-
}))
106+
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.GetLogLevel()}))
126107
slog.SetDefault(logger)
127108

128-
// Create exporter
129-
_, err := exporter.New(cfg)
130-
if err != nil {
131-
return fmt.Errorf("failed to create exporter: %w", err)
109+
if _, err := exporter.New(cfg); err != nil {
110+
return fmt.Errorf("create exporter: %w", err)
132111
}
133112

134-
// Set up HTTP server
135113
mux := http.NewServeMux()
136114
mux.Handle(cfg.Server.MetricsPath, promhttp.Handler())
137-
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
115+
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
138116
w.Header().Set("Content-Type", "text/html")
139-
fmt.Fprintf(w, `<html>
140-
<head><title>CrowdSec Exporter</title></head>
141-
<body>
142-
<h1>CrowdSec Exporter</h1>
143-
<p><a href="%s">Metrics</a></p>
144-
</body>
145-
</html>`, cfg.Server.MetricsPath)
117+
fmt.Fprintf(w, indexHTML, cfg.Server.MetricsPath)
146118
})
147119

148120
server := &http.Server{
149121
Addr: cfg.Server.ListenAddress,
150122
Handler: mux,
151123
}
152124

153-
// Graceful shutdown
154125
stop := make(chan os.Signal, 1)
155126
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
156127

157128
go func() {
158-
slog.Info("Starting CrowdSec exporter", "address", cfg.Server.ListenAddress)
159-
slog.Info("Metrics available", "path", cfg.Server.MetricsPath)
129+
slog.Info("starting exporter", "address", cfg.Server.ListenAddress, "metrics_path", cfg.Server.MetricsPath)
160130
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
161-
slog.Error("Server failed to start", "error", err)
131+
slog.Error("server error", "error", err)
162132
os.Exit(1)
163133
}
164134
}()
165135

166136
<-stop
167-
slog.Info("Shutting down server...")
137+
slog.Info("shutdown initiated")
168138

169-
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
170-
defer cancel()
139+
if err := crowdsec.DeregisterMachine(); err != nil {
140+
slog.Warn("deregister failed", "error", err)
141+
}
171142

172-
if err := server.Shutdown(shutdownCtx); err != nil {
173-
slog.Error("Server forced to shutdown", "error", err)
143+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
144+
defer cancel()
145+
if err := server.Shutdown(ctx); err != nil {
146+
slog.Error("forced shutdown", "error", err)
174147
return err
175148
}
176149

177-
slog.Info("Server exited")
150+
slog.Info("server exited")
178151
return nil
179152
}
153+
154+
const indexHTML = `<!doctype html>
155+
<html>
156+
<head><meta charset="utf-8"><title>CrowdSec Exporter</title></head>
157+
<body>
158+
<h1>CrowdSec Exporter</h1>
159+
<p><a href="%s">Metrics</a></p>
160+
</body>
161+
</html>`

internal/config/config.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type CrowdSecConfig struct {
2121
Password string `mapstructure:"password"`
2222
RegistrationToken string `mapstructure:"registration_token"`
2323
MachineName string `mapstructure:"machine_name"`
24+
DeregisterOnExit bool `mapstructure:"deregister_on_exit"`
2425
}
2526

2627
// ServerConfig contains HTTP server configuration
@@ -42,18 +43,16 @@ func (c *Config) Validate() error {
4243
errors = append(errors, "crowdsec.url is required")
4344
}
4445

45-
// Check if we have either login/password OR registration token
46-
hasLoginAuth := c.CrowdSec.Login != "" && c.CrowdSec.Password != ""
47-
hasTokenAuth := c.CrowdSec.RegistrationToken != ""
48-
49-
if !hasLoginAuth && !hasTokenAuth {
50-
errors = append(errors, "either (crowdsec.login and crowdsec.password) OR crowdsec.registration_token is required")
46+
// Login and Password are now required
47+
if c.CrowdSec.Login == "" {
48+
errors = append(errors, "crowdsec.login is required")
5149
}
52-
53-
if hasLoginAuth && hasTokenAuth {
54-
errors = append(errors, "cannot use both login/password and registration token authentication")
50+
if c.CrowdSec.Password == "" {
51+
errors = append(errors, "crowdsec.password is required")
5552
}
5653

54+
// Registration token is optional for auto-registration
55+
5756
if c.Server.ListenAddress == "" {
5857
c.Server.ListenAddress = ":9999"
5958
}

0 commit comments

Comments
 (0)