@@ -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
2728func 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
3435func 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
9885func 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
11097func 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>`
0 commit comments