diff --git a/assets.go b/assets.go index 24c2fd9..4f0abd3 100644 --- a/assets.go +++ b/assets.go @@ -5,6 +5,7 @@ import ( "path" "text/template" "traefik-lazyload/pkg/config" + "traefik-lazyload/pkg/service" ) //go:embed assets/* @@ -19,3 +20,11 @@ type SplashModel struct { } var splashTemplate = template.Must(template.ParseFS(httpAssets, path.Join("assets", config.Model.Splash))) + +type StatusPageModel struct { + Active []*service.ContainerState + Qualifying []string + RuntimeMetrics string +} + +var statusPageTemplate = template.Must(template.ParseFS(httpAssets, "assets/status.html")) diff --git a/assets/status.html b/assets/status.html new file mode 100644 index 0000000..172c5a1 --- /dev/null +++ b/assets/status.html @@ -0,0 +1,44 @@ + + +
+ + + + +This are containers the lazyloader knows about and considers "active"
+| Name | +Last Active | +Rx | +Tx | +Stop Delay | +
|---|---|---|---|---|
| {{$val.Name}} | +{{$val.LastActive.Format "2006-01-02 15:04:05"}} | +{{$val.Rx}} | +{{$val.Tx}} | +{{$val.StopDelay}} | +
These are all containers that qualify to be lazy-loader managed
+{{.RuntimeMetrics}}
+ + \ No newline at end of file diff --git a/main.go b/main.go index fb5f868..b110a81 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,11 @@ package main import ( "errors" + "fmt" "io" "io/fs" "net/http" + "runtime" "traefik-lazyload/pkg/config" "traefik-lazyload/pkg/service" @@ -84,7 +86,13 @@ func ContainerHandler(w http.ResponseWriter, r *http.Request) { func StatusHandler(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": - io.WriteString(w, "Status page") + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + statusPageTemplate.Execute(w, StatusPageModel{ + Active: core.ActiveContainers(), + Qualifying: core.QualifyingContainers(), + RuntimeMetrics: fmt.Sprintf("Heap=%d, InUse=%d, Total=%d, Sys=%d, NumGC=%d", stats.HeapAlloc, stats.HeapInuse, stats.TotalAlloc, stats.Sys, stats.NumGC), + }) default: w.WriteHeader(http.StatusNotFound) io.WriteString(w, "Status page not found") diff --git a/pkg/service/container.go b/pkg/service/container.go new file mode 100644 index 0000000..b05b436 --- /dev/null +++ b/pkg/service/container.go @@ -0,0 +1,75 @@ +package service + +import ( + "strconv" + "time" + "traefik-lazyload/pkg/config" + + "github.com/docker/docker/api/types" + "github.com/sirupsen/logrus" +) + +type containerSettings struct { + stopDelay time.Duration + waitForCode int + waitForPath string +} + +type ContainerState struct { + name string + containerSettings + lastRecv, lastSend int64 // Last network traffic, used to see if idle + lastActivity time.Time +} + +func newStateFromContainer(ct *types.Container) *ContainerState { + return &ContainerState{ + name: containerShort(ct), + containerSettings: extractContainerLabels(ct), + lastActivity: time.Now(), + } +} + +func extractContainerLabels(ct *types.Container) (target containerSettings) { + { // Parse stop delay + stopDelay, _ := labelOrDefault(ct, "stopdelay", config.Model.StopDelay.String()) + if dur, stopErr := time.ParseDuration(stopDelay); stopErr != nil { + target.stopDelay = config.Model.StopDelay + logrus.Warnf("Unable to parse stopdelay for %s of %s, defaulting to %s", containerShort(ct), stopDelay, target.stopDelay.String()) + } else { + target.stopDelay = dur + } + } + { // WaitForCode + codeStr, _ := labelOrDefault(ct, "waitforcode", "200") + if code, err := strconv.Atoi(codeStr); err != nil { + target.waitForCode = 200 + logrus.Warnf("Unable to parse WaitForCode of %s, defaulting to %d", containerShort(ct), target.waitForCode) + } else { + target.waitForCode = code + } + } + + target.waitForPath, _ = labelOrDefault(ct, "waitforpath", "/") + return +} + +func (s *ContainerState) Name() string { + return s.name +} + +func (s *ContainerState) LastActive() time.Time { + return s.lastActivity +} + +func (s *ContainerState) Rx() int64 { + return s.lastRecv +} + +func (s *ContainerState) Tx() int64 { + return s.lastSend +} + +func (s *containerSettings) StopDelay() string { + return s.stopDelay.String() +} diff --git a/pkg/service/service.go b/pkg/service/service.go index bcb633f..98bb8ec 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -4,7 +4,8 @@ import ( "context" "encoding/json" "errors" - "strconv" + "fmt" + "sort" "strings" "sync" "time" @@ -17,26 +18,13 @@ import ( "github.com/sirupsen/logrus" ) -type containerSettings struct { - stopDelay time.Duration - waitForCode int - waitForPath string -} - -type containerState struct { - Name string - containerSettings - lastRecv, lastSend int64 // Last network traffic, used to see if idle - lastActivity time.Time -} - type Core struct { mux sync.Mutex term chan bool client *client.Client - active map[string]*containerState // cid -> state + active map[string]*ContainerState // cid -> state } func New(client *client.Client, pollRate time.Duration) (*Core, error) { @@ -50,7 +38,7 @@ func New(client *client.Client, pollRate time.Duration) (*Core, error) { // Make core ret := &Core{ client: client, - active: make(map[string]*containerState), + active: make(map[string]*ContainerState), term: make(chan bool), } @@ -87,7 +75,7 @@ func (s *Core) StartHost(hostname string) (*StartResult, error) { if ets, exists := s.active[ct.ID]; exists { // TODO: Handle case we think it's active, but not? (eg. crash? slow boot?) - logrus.Debugf("Asked to start host, but we already think it's started: %s", ets.Name) + logrus.Debugf("Asked to start host, but we already think it's started: %s", ets.name) return &StartResult{ WaitForCode: ets.waitForCode, WaitForPath: ets.waitForPath, @@ -162,7 +150,7 @@ func (s *Core) checkForNewContainers(ctx context.Context) { // check for containers we think are running, but aren't (destroyed, error'd, stop'd via another process, etc) for cid, cts := range s.active { if _, ok := runningContainers[cid]; !ok { - logrus.Infof("Discover container had stopped, removing %s", cts.Name) + logrus.Infof("Discover container had stopped, removing %s", cts.name) delete(s.active, cid) } } @@ -180,20 +168,20 @@ func (s *Core) watchForInactivity(ctx context.Context) { for cid, cts := range s.active { shouldStop, err := s.checkContainerForInactivity(ctx, cid, cts) if err != nil { - logrus.Warnf("error checking container state for %s: %s", cts.Name, err) + logrus.Warnf("error checking container state for %s: %s", cts.name, err) } if shouldStop { if err := s.client.ContainerStop(ctx, cid, container.StopOptions{}); err != nil { - logrus.Errorf("Error stopping container %s: %s", cts.Name, err) + logrus.Errorf("Error stopping container %s: %s", cts.name, err) } else { - logrus.Infof("Stopped container %s", cts.Name) + logrus.Infof("Stopped container %s", cts.name) delete(s.active, cid) } } } } -func (s *Core) checkContainerForInactivity(ctx context.Context, cid string, ct *containerState) (shouldStop bool, retErr error) { +func (s *Core) checkContainerForInactivity(ctx context.Context, cid string, ct *ContainerState) (shouldStop bool, retErr error) { statsStream, err := s.client.ContainerStatsOneShot(ctx, cid) if err != nil { return false, err @@ -220,45 +208,13 @@ func (s *Core) checkContainerForInactivity(ctx context.Context, cid string, ct * // No activity, stop? if time.Now().After(ct.lastActivity.Add(ct.stopDelay)) { - logrus.Infof("Found idle container %s...", ct.Name) + logrus.Infof("Found idle container %s...", ct.name) return true, nil } return false, nil } -func newStateFromContainer(ct *types.Container) *containerState { - return &containerState{ - Name: containerShort(ct), - containerSettings: extractContainerLabels(ct), - lastActivity: time.Now(), - } -} - -func extractContainerLabels(ct *types.Container) (target containerSettings) { - { // Parse stop delay - stopDelay, _ := labelOrDefault(ct, "stopdelay", config.Model.StopDelay.String()) - if dur, stopErr := time.ParseDuration(stopDelay); stopErr != nil { - target.stopDelay = config.Model.StopDelay - logrus.Warnf("Unable to parse stopdelay for %s of %s, defaulting to %s", containerShort(ct), stopDelay, target.stopDelay.String()) - } else { - target.stopDelay = dur - } - } - { // WaitForCode - codeStr, _ := labelOrDefault(ct, "waitforcode", "200") - if code, err := strconv.Atoi(codeStr); err != nil { - target.waitForCode = 200 - logrus.Warnf("Unable to parse WaitForCode of %s, defaulting to %d", containerShort(ct), target.waitForCode) - } else { - target.waitForCode = code - } - } - - target.waitForPath, _ = labelOrDefault(ct, "waitforpath", "/") - return -} - func (s *Core) findContainerByHostname(ctx context.Context, hostname string) (*types.Container, error) { containers, err := s.findAllLazyloadContainers(ctx, true) if err != nil { @@ -294,3 +250,31 @@ func (s *Core) findAllLazyloadContainers(ctx context.Context, includeStopped boo Filters: filters, }) } + +func (s *Core) ActiveContainers() []*ContainerState { + s.mux.Lock() + defer s.mux.Unlock() + + ret := make([]*ContainerState, 0, len(s.active)) + for _, item := range s.active { + ret = append(ret, item) + } + sort.Slice(ret, func(i, j int) bool { + return ret[i].name < ret[j].name + }) + return ret +} + +func (s *Core) QualifyingContainers() []string { + ct, err := s.findAllLazyloadContainers(context.Background(), true) + if err != nil { + return nil + } + + ret := make([]string, len(ct)) + for i, c := range ct { + ret[i] = fmt.Sprintf("%s - %s", containerShort(&c), c.State) + } + sort.Strings(ret) + return ret +}