mirror of
https://github.com/zix99/traefik-lazyload.git
synced 2025-12-24 06:28:31 +01:00
Add status page to expose metrics
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"text/template"
|
"text/template"
|
||||||
"traefik-lazyload/pkg/config"
|
"traefik-lazyload/pkg/config"
|
||||||
|
"traefik-lazyload/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed assets/*
|
//go:embed assets/*
|
||||||
@@ -19,3 +20,11 @@ type SplashModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var splashTemplate = template.Must(template.ParseFS(httpAssets, path.Join("assets", config.Model.Splash)))
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime"
|
||||||
"traefik-lazyload/pkg/config"
|
"traefik-lazyload/pkg/config"
|
||||||
"traefik-lazyload/pkg/service"
|
"traefik-lazyload/pkg/service"
|
||||||
|
|
||||||
@@ -84,7 +86,13 @@ func ContainerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
func StatusHandler(w http.ResponseWriter, r *http.Request) {
|
func StatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/":
|
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:
|
default:
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
io.WriteString(w, "Status page not found")
|
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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,26 +18,13 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"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 {
|
type Core struct {
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
term chan bool
|
term chan bool
|
||||||
|
|
||||||
client *client.Client
|
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) {
|
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
|
// Make core
|
||||||
ret := &Core{
|
ret := &Core{
|
||||||
client: client,
|
client: client,
|
||||||
active: make(map[string]*containerState),
|
active: make(map[string]*ContainerState),
|
||||||
term: make(chan bool),
|
term: make(chan bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +75,7 @@ func (s *Core) StartHost(hostname string) (*StartResult, error) {
|
|||||||
|
|
||||||
if ets, exists := s.active[ct.ID]; exists {
|
if ets, exists := s.active[ct.ID]; exists {
|
||||||
// TODO: Handle case we think it's active, but not? (eg. crash? slow boot?)
|
// 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{
|
return &StartResult{
|
||||||
WaitForCode: ets.waitForCode,
|
WaitForCode: ets.waitForCode,
|
||||||
WaitForPath: ets.waitForPath,
|
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)
|
// 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 {
|
for cid, cts := range s.active {
|
||||||
if _, ok := runningContainers[cid]; !ok {
|
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)
|
delete(s.active, cid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,20 +168,20 @@ func (s *Core) watchForInactivity(ctx context.Context) {
|
|||||||
for cid, cts := range s.active {
|
for cid, cts := range s.active {
|
||||||
shouldStop, err := s.checkContainerForInactivity(ctx, cid, cts)
|
shouldStop, err := s.checkContainerForInactivity(ctx, cid, cts)
|
||||||
if err != nil {
|
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 shouldStop {
|
||||||
if err := s.client.ContainerStop(ctx, cid, container.StopOptions{}); err != nil {
|
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 {
|
} else {
|
||||||
logrus.Infof("Stopped container %s", cts.Name)
|
logrus.Infof("Stopped container %s", cts.name)
|
||||||
delete(s.active, cid)
|
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)
|
statsStream, err := s.client.ContainerStatsOneShot(ctx, cid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -220,45 +208,13 @@ func (s *Core) checkContainerForInactivity(ctx context.Context, cid string, ct *
|
|||||||
|
|
||||||
// No activity, stop?
|
// No activity, stop?
|
||||||
if time.Now().After(ct.lastActivity.Add(ct.stopDelay)) {
|
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 true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, 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) {
|
func (s *Core) findContainerByHostname(ctx context.Context, hostname string) (*types.Container, error) {
|
||||||
containers, err := s.findAllLazyloadContainers(ctx, true)
|
containers, err := s.findAllLazyloadContainers(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -294,3 +250,31 @@ func (s *Core) findAllLazyloadContainers(ctx context.Context, includeStopped boo
|
|||||||
Filters: filters,
|
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