mirror of
https://github.com/zix99/traefik-lazyload.git
synced 2025-12-21 13:23:04 +01:00
Add status page to expose metrics
This commit is contained in:
@@ -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"))
|
||||
|
||||
44
assets/status.html
Normal file
44
assets/status.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="refresh" content="30">
|
||||
<title>Status</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Lazyloader Status</h1>
|
||||
<h2>Active Containers</h2>
|
||||
<p>This are containers the lazyloader knows about and considers "active"</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Last Active</th>
|
||||
<th>Rx</th>
|
||||
<th>Tx</th>
|
||||
<th>Stop Delay</th>
|
||||
</tr>
|
||||
{{range $val := .Active}}
|
||||
<tr>
|
||||
<td>{{$val.Name}}</td>
|
||||
<td>{{$val.LastActive.Format "2006-01-02 15:04:05"}}</td>
|
||||
<td>{{$val.Rx}}</td>
|
||||
<td>{{$val.Tx}}</td>
|
||||
<td>{{$val.StopDelay}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
<h2>Qualifying Containers</h2>
|
||||
<p>These are all containers that qualify to be lazy-loader managed</p>
|
||||
<ul>
|
||||
{{range $val := .Qualifying}}
|
||||
<li>{{.}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<h2>Runtime</h2>
|
||||
<p>{{.RuntimeMetrics}}</p>
|
||||
</body>
|
||||
</html>
|
||||
10
main.go
10
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")
|
||||
|
||||
75
pkg/service/container.go
Normal file
75
pkg/service/container.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user