diff --git a/main.go b/main.go new file mode 100644 index 000000000..abec61a4e --- /dev/null +++ b/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "os" + "sort" + + "github.com/go-kit/log/level" + "github.com/prometheus-community/windows_exporter/pkg/collector" + "github.com/prometheus-community/windows_exporter/pkg/exporter" + "github.com/prometheus-community/windows_exporter/pkg/windows_service" + "golang.org/x/sys/windows/svc" +) + +func main() { + exporter := exporter.New() + if exporter.PrintCollectors() { + collectorNames := collector.Available() + sort.Strings(collectorNames) + fmt.Printf("Available collectors:\n") + for _, n := range collectorNames { + fmt.Printf(" - %s\n", n) + } + os.Exit(0) + } + isWinService, err := svc.IsWindowsService() + if err != nil { + _ = level.Error(exporter.GetLogger()).Log("Failed to detect [IsWindowsService]: ", "err", err) + os.Exit(1) + } + if isWinService { + windows_service.Run(exporter) + } else { + exporter.RunAsCli() + } +} diff --git a/exporter.go b/pkg/exporter/exporter.go similarity index 50% rename from exporter.go rename to pkg/exporter/exporter.go index 1119fb62a..5ad18e127 100644 --- a/exporter.go +++ b/pkg/exporter/exporter.go @@ -1,6 +1,6 @@ //go:build windows -package main +package exporter import ( "encoding/json" @@ -8,19 +8,19 @@ import ( "net/http" _ "net/http/pprof" "os" + "os/signal" "os/user" "runtime" - "sort" "strings" + "syscall" - // Its important that we do these first so that we can register with the windows service control ASAP to avoid timeouts - "github.com/prometheus-community/windows_exporter/pkg/initiate" winlog "github.com/prometheus-community/windows_exporter/pkg/log" "github.com/prometheus-community/windows_exporter/pkg/types" "github.com/prometheus-community/windows_exporter/pkg/utils" "github.com/prometheus-community/windows_exporter/pkg/wmi" "github.com/alecthomas/kingpin/v2" + "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/prometheus-community/windows_exporter/pkg/collector" "github.com/prometheus-community/windows_exporter/pkg/config" @@ -30,6 +30,43 @@ import ( webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" ) +var ( + app = kingpin.New("windows_exporter", "A metrics collector for Windows.") + configFile = app.Flag( + "config.file", + "YAML configuration file to use. Values set in this file will be overridden by CLI flags.", + ).String() + insecure_skip_verify = app.Flag( + "config.file.insecure-skip-verify", + "Skip TLS verification in loading YAML configuration.", + ).Default("false").Bool() + metricsPath = app.Flag( + "telemetry.path", + "URL path for surfacing collected metrics.", + ).Default("/metrics").String() + disableExporterMetrics = app.Flag( + "web.disable-exporter-metrics", + "Exclude metrics about the exporter itself (promhttp_*, process_*, go_*).", + ).Bool() + maxRequests = app.Flag( + "telemetry.max-requests", + "Maximum number of concurrent requests. 0 to disable.", + ).Default("5").Int() + enabledCollectors = app.Flag( + "collectors.enabled", + "Comma-separated list of collectors to use. Use '[defaults]' as a placeholder for all the collectors enabled by default."). + Default(types.DefaultCollectors).String() + printCollectors = app.Flag( + "collectors.print", + "If true, print available collectors and exit.", + ).Bool() + timeoutMargin = app.Flag( + "scrape.timeout-margin", + "Seconds to subtract from the timeout allowed by the client. Tune to allow for overhead or high loads.", + ).Default("0.5").Float64() + webConfig = webflag.AddFlags(app, ":9182") +) + // Same struct prometheus uses for their /version endpoint. // Separate copy to avoid pulling all of prometheus as a dependency type prometheusVersion struct { @@ -41,73 +78,75 @@ type prometheusVersion struct { GoVersion string `json:"goVersion"` } -func main() { - app := kingpin.New("windows_exporter", "A metrics collector for Windows.") - var ( - configFile = app.Flag( - "config.file", - "YAML configuration file to use. Values set in this file will be overridden by CLI flags.", - ).String() - insecure_skip_verify = app.Flag( - "config.file.insecure-skip-verify", - "Skip TLS verification in loading YAML configuration.", - ).Default("false").Bool() - webConfig = webflag.AddFlags(app, ":9182") - metricsPath = app.Flag( - "telemetry.path", - "URL path for surfacing collected metrics.", - ).Default("/metrics").String() - disableExporterMetrics = app.Flag( - "web.disable-exporter-metrics", - "Exclude metrics about the exporter itself (promhttp_*, process_*, go_*).", - ).Bool() - maxRequests = app.Flag( - "telemetry.max-requests", - "Maximum number of concurrent requests. 0 to disable.", - ).Default("5").Int() - enabledCollectors = app.Flag( - "collectors.enabled", - "Comma-separated list of collectors to use. Use '[defaults]' as a placeholder for all the collectors enabled by default."). - Default(types.DefaultCollectors).String() - printCollectors = app.Flag( - "collectors.print", - "If true, print available collectors and exit.", - ).Bool() - timeoutMargin = app.Flag( - "scrape.timeout-margin", - "Seconds to subtract from the timeout allowed by the client. Tune to allow for overhead or high loads.", - ).Default("0.5").Float64() - ) - - winlogConfig := &winlog.Config{} - flag.AddFlags(app, winlogConfig) +type Exporter struct { + logger log.Logger + winlogConfig winlog.Config + collectors collector.Collectors +} + +func New() *Exporter { + e := &Exporter{} + + e.winlogConfig = winlog.Config{} + flag.AddFlags(app, &e.winlogConfig) app.Version(version.Print("windows_exporter")) app.HelpFlag.Short('h') // Initialize collectors before loading and parsing CLI arguments - collectors := collector.NewWithFlags(app) + e.collectors = collector.NewWithFlags(app) + + e.loadConfiguration() + e.initiateWmi() + e.printRunAsUser() + return e +} + +func (e *Exporter) GetLogger() log.Logger { + return e.logger +} + +func (e *Exporter) PrintCollectors() bool { + return *printCollectors +} +func (e *Exporter) Start() { + e.buildCollectors() + e.buildAndStartHttpServer() +} + +func (e *Exporter) RunAsCli() { + e.Start() + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) + <-sigs + _ = level.Info(e.logger).Log("msg", "Shutting down windows_exporter") +} + +func (e *Exporter) loadConfiguration() { // Load values from configuration file(s). Executable flags must first be parsed, in order // to load the specified file(s). kingpin.MustParse(app.Parse(os.Args[1:])) - logger, err := winlog.New(winlogConfig) + + logger, err := winlog.New(&e.winlogConfig) if err != nil { - _ = level.Error(logger).Log("err", err) + fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err) os.Exit(1) } + e.logger = log.With(logger, "main", "exporter") + e.collectors.SetLogger(logger) - _ = level.Debug(logger).Log("msg", "Logging has Started") + _ = level.Debug(e.logger).Log("msg", "Logging has Started") if *configFile != "" { resolver, err := config.NewResolver(*configFile, logger, *insecure_skip_verify) if err != nil { - _ = level.Error(logger).Log("msg", "could not load config file", "err", err) + _ = level.Error(e.logger).Log("msg", "could not load config file", "err", err) os.Exit(1) } err = resolver.Bind(app, os.Args[1:]) if err != nil { - _ = level.Error(logger).Log("err", err) + _ = level.Error(e.logger).Log("err", err) os.Exit(1) } @@ -118,60 +157,48 @@ func main() { // Parse flags once more to include those discovered in configuration file(s). kingpin.MustParse(app.Parse(os.Args[1:])) + } +} - logger, err = winlog.New(winlogConfig) - if err != nil { - _ = level.Error(logger).Log("err", err) - os.Exit(1) - } +func (e *Exporter) initiateWmi() { + if err := wmi.InitWbem(e.logger); err != nil { + _ = level.Error(e.logger).Log("Initiate SWbemServices failed with: ", "err", err) + os.Exit(1) } +} - if *printCollectors { - collectorNames := collector.Available() - sort.Strings(collectorNames) +func (e *Exporter) printRunAsUser() { + if u, err := user.Current(); err != nil { + _ = level.Warn(e.logger).Log("msg", "Unable to determine which user is running this exporter. More info: https://github.com/golang/go/issues/37348") + } else { + _ = level.Info(e.logger).Log("msg", fmt.Sprintf("Running as %v", u.Username)) - fmt.Printf("Available collectors:\n") - for _, n := range collectorNames { - fmt.Printf(" - %s\n", n) + if strings.Contains(u.Username, "ContainerAdministrator") || strings.Contains(u.Username, "ContainerUser") { + _ = level.Warn(e.logger).Log("msg", "Running as a preconfigured Windows Container user. This may mean you do not have Windows HostProcess containers configured correctly and some functionality will not work as expected.") } - - return - } - - if err = wmi.InitWbem(logger); err != nil { - _ = level.Error(logger).Log("err", err) - os.Exit(1) } +} +func (e *Exporter) buildCollectors() { enabledCollectorList := utils.ExpandEnabledCollectors(*enabledCollectors) - collectors.Enable(enabledCollectorList) - collectors.SetLogger(logger) + e.collectors.Enable(enabledCollectorList) + _ = level.Info(e.logger).Log("msg", fmt.Sprintf("Enabled collectors: %v", strings.Join(enabledCollectorList, ", "))) // Initialize collectors before loading - err = collectors.Build() + err := e.collectors.Build() if err != nil { - _ = level.Error(logger).Log("msg", "Couldn't load collectors", "err", err) + _ = level.Error(e.logger).Log("msg", "Couldn't load collectors", "err", err) os.Exit(1) } +} - if u, err := user.Current(); err != nil { - _ = level.Warn(logger).Log("msg", "Unable to determine which user is running this exporter. More info: https://github.com/golang/go/issues/37348") - } else { - _ = level.Info(logger).Log("msg", fmt.Sprintf("Running as %v", u.Username)) - - if strings.Contains(u.Username, "ContainerAdministrator") || strings.Contains(u.Username, "ContainerUser") { - _ = level.Warn(logger).Log("msg", "Running as a preconfigured Windows Container user. This may mean you do not have Windows HostProcess containers configured correctly and some functionality will not work as expected.") - } - } - - _ = level.Info(logger).Log("msg", fmt.Sprintf("Enabled collectors: %v", strings.Join(enabledCollectorList, ", "))) - - http.HandleFunc(*metricsPath, withConcurrencyLimit(*maxRequests, collectors.BuildServeHTTP(*disableExporterMetrics, *timeoutMargin))) +func (e *Exporter) buildAndStartHttpServer() { + http.HandleFunc(*metricsPath, withConcurrencyLimit(*maxRequests, e.collectors.BuildServeHTTP(*disableExporterMetrics, *timeoutMargin))) http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, err := fmt.Fprintln(w, `{"status":"ok"}`) if err != nil { - _ = level.Debug(logger).Log("Failed to write to stream", "err", err) + _ = level.Debug(e.logger).Log("Failed to write to stream", "err", err) } }) http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { @@ -211,30 +238,23 @@ func main() { } landingPage, err := web.NewLandingPage(landingConfig) if err != nil { - _ = level.Error(logger).Log("msg", "failed to generate landing page", "err", err) + _ = level.Error(e.logger).Log("msg", "failed to generate landing page", "err", err) os.Exit(1) } http.Handle("/", landingPage) } - _ = level.Info(logger).Log("msg", "Starting windows_exporter", "version", version.Info()) - _ = level.Info(logger).Log("msg", "Build context", "build_context", version.BuildContext()) - _ = level.Debug(logger).Log("msg", "Go MAXPROCS", "procs", runtime.GOMAXPROCS(0)) + _ = level.Info(e.logger).Log("msg", "Starting windows_exporter", "version", version.Info()) + _ = level.Info(e.logger).Log("msg", "Build context", "build_context", version.BuildContext()) + _ = level.Debug(e.logger).Log("msg", "Go MAXPROCS", "procs", runtime.GOMAXPROCS(0)) go func() { server := &http.Server{} - if err := web.ListenAndServe(server, webConfig, logger); err != nil { - _ = level.Error(logger).Log("msg", "cannot start windows_exporter", "err", err) + if err := web.ListenAndServe(server, webConfig, e.logger); err != nil { + _ = level.Error(e.logger).Log("msg", "cannot start windows_exporter", "err", err) os.Exit(1) } }() - - for { - if <-initiate.StopCh { - _ = level.Info(logger).Log("msg", "Shutting down windows_exporter") - break - } - } } func withConcurrencyLimit(n int, next http.HandlerFunc) http.HandlerFunc { diff --git a/pkg/windows_service/windows_service.go b/pkg/windows_service/windows_service.go new file mode 100644 index 000000000..804f6ab98 --- /dev/null +++ b/pkg/windows_service/windows_service.go @@ -0,0 +1,55 @@ +// Package initiate allows us to initiate Time Sensitive components (Like registering the windows service) as early as possible in the startup process +package windows_service + +import ( + "fmt" + "os" + + "github.com/go-kit/log/level" + "github.com/prometheus-community/windows_exporter/pkg/exporter" + "golang.org/x/sys/windows/svc" +) + +const ( + serviceName = "windows_exporter" +) + +type windowsExporterService struct { + exporter *exporter.Exporter +} + +func (s *windowsExporterService) Execute(_ []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + s.exporter.Start() + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} +loop: + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + _ = level.Info(s.exporter.GetLogger()).Log("msg", "Service Stop Received") + changes <- svc.Status{State: svc.StopPending} + break loop + default: + _ = level.Error(s.exporter.GetLogger()).Log("msg", fmt.Sprintf("unexpected control request #%d", c)) + } + } + } + return +} + +func Run(e *exporter.Exporter) { + _ = level.Info(e.GetLogger()).Log("msg", "Attempting to start exporter service") + err := svc.Run(serviceName, &windowsExporterService{ + exporter: e, + }) + if err != nil { + _ = level.Error(e.GetLogger()).Log("msg", "Failed to start exporter service", "err", err) + os.Exit(1) + } + _ = level.Info(e.GetLogger()).Log("msg", "Shutting down windows_exporter") +}