// Package application wires configuration, product collectors and the HTTP server together. package application import ( "context" "errors" "fmt" "net" "net/http" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" log "github.com/sirupsen/logrus" "lostak.dev/shelly-exporter/collector" "lostak.dev/shelly-exporter/configuration" "lostak.dev/shelly-exporter/shelly" ) // Application is a configured Shelly exporter HTTP server. type Application struct { config *configuration.Configuration registry *prometheus.Registry server *http.Server } // New loads a configuration and registers one collector per Shelly device. func New(configPath string) (*Application, error) { config, err := configuration.Load(configPath) if err != nil { return nil, err } log.SetLevel(log.AllLevels[config.LogLevel]) registry := prometheus.NewRegistry() deviceCollectors := make(collector.Group, 0, len(config.Shelly.Devices)) for _, device := range config.Shelly.Devices { client, err := shelly.NewClient( device.URL, device.Username, device.Password, time.Duration(config.Shelly.TimeoutSeconds)*time.Second, ) if err != nil { return nil, fmt.Errorf("create client for device %q: %w", device.Name, err) } deviceCollector, err := collector.NewDeviceCollector(device, client) if err != nil { return nil, fmt.Errorf("configure device %q: %w", device.Name, err) } deviceCollectors = append(deviceCollectors, deviceCollector) log.Infof("Registered %s metrics for Shelly device %q.", device.Product, device.Name) } if err := registry.Register(deviceCollectors); err != nil { return nil, fmt.Errorf("register device metrics: %w", err) } mux := http.NewServeMux() mux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{EnableOpenMetrics: true})) mux.HandleFunc("/-/healthy", func(response http.ResponseWriter, _ *http.Request) { response.WriteHeader(http.StatusOK) _, _ = response.Write([]byte("OK\n")) }) address := fmt.Sprintf("%s:%d", config.Host, config.Port) return &Application{ config: config, registry: registry, server: &http.Server{ Addr: address, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, }, }, nil } // Run serves metrics until the context is cancelled or the server fails. func (app *Application) Run(ctx context.Context) error { listener, err := net.Listen("tcp", app.server.Addr) if err != nil { return fmt.Errorf("listen on %s: %w", app.server.Addr, err) } serverError := make(chan error, 1) log.Infof("Shelly exporter started on %s.", listener.Addr()) go func() { serverError <- app.server.Serve(listener) }() select { case err := <-serverError: if errors.Is(err, http.ErrServerClosed) { return nil } return err case <-ctx.Done(): shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := app.server.Shutdown(shutdownContext); err != nil { return fmt.Errorf("shut down HTTP server: %w", err) } err = <-serverError if err != nil && !errors.Is(err, http.ErrServerClosed) { return err } return nil } }