mirror of https://github.com/divan/expvarmon.git
Initial commit
This commit is contained in:
commit
cbf804914c
|
@ -0,0 +1,64 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
docker "github.com/fsouza/go-dockerclient"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func getContainers() ([]string, error) {
|
||||
cli, err := connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containers, err := cli.ListContainers(docker.ListContainersOptions{
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, cont := range containers {
|
||||
names = append(names, cont.Names[0])
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// connect establishes connection to the Docker daemon.
|
||||
func connect() (*docker.Client, error) {
|
||||
var (
|
||||
client *docker.Client
|
||||
err error
|
||||
)
|
||||
// TODO: add boot2docker shellinit support
|
||||
endpoint := os.Getenv("DOCKER_HOST")
|
||||
if endpoint == "" {
|
||||
endpoint = "unix:///var/run/docker.sock"
|
||||
}
|
||||
cert_path := os.Getenv("DOCKER_CERT_PATH")
|
||||
if cert_path != "" {
|
||||
client, err = docker.NewTLSClient(
|
||||
endpoint,
|
||||
filepath.Join(cert_path, "cert.pem"),
|
||||
filepath.Join(cert_path, "key.pem"),
|
||||
filepath.Join(cert_path, "ca.pem"),
|
||||
)
|
||||
} else {
|
||||
client, err = docker.NewClient(endpoint)
|
||||
}
|
||||
|
||||
return client, err
|
||||
}
|
||||
|
||||
// cleanContainerName clears leading '/' symbol from container name.
|
||||
func cleanContainerName(name string) string {
|
||||
return strings.TrimLeft(name, "/")
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Expvars holds all vars we support via expvars. It implements Source interface.
|
||||
|
||||
const ExpvarsUrl = "/debug/vars"
|
||||
|
||||
type ExpvarsSource struct {
|
||||
Ports []string
|
||||
}
|
||||
|
||||
type Expvars map[string]Expvar
|
||||
|
||||
type Expvar struct {
|
||||
MemStats *runtime.MemStats `json:"memstats"`
|
||||
Cmdline []string `json:"cmdline"`
|
||||
Goroutines int64 `json:"goroutines,omitempty"`
|
||||
|
||||
Err error `json:"-,omitempty"`
|
||||
}
|
||||
|
||||
func NewExpvarsSource(ports []string) *ExpvarsSource {
|
||||
return &ExpvarsSource{
|
||||
Ports: ports,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ExpvarsSource) Update() (interface{}, error) {
|
||||
vars := make(Expvars)
|
||||
for _, port := range e.Ports {
|
||||
addr := fmt.Sprintf("http://localhost:%s%s", port, ExpvarsUrl)
|
||||
resp, err := http.Get(addr)
|
||||
if err != nil {
|
||||
expvar := &Expvar{}
|
||||
expvar.Err = err
|
||||
vars[port] = *expvar
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
expvar := &Expvar{}
|
||||
expvar.Err = fmt.Errorf("Page not found. Did you import expvars?")
|
||||
vars[port] = *expvar
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
expvar, err := ParseExpvar(resp.Body)
|
||||
if err != nil {
|
||||
expvar = &Expvar{}
|
||||
expvar.Err = err
|
||||
}
|
||||
|
||||
vars[port] = *expvar
|
||||
}
|
||||
|
||||
return vars, nil
|
||||
}
|
||||
|
||||
// ParseExpvar unmarshals data to Expvar variable.
|
||||
func ParseExpvar(r io.Reader) (*Expvar, error) {
|
||||
var vars Expvar
|
||||
dec := json.NewDecoder(r)
|
||||
err := dec.Decode(&vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &vars, err
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const expvarsTestFile = "./expvars.json"
|
||||
|
||||
func TestExpvars(t *testing.T) {
|
||||
file, err := os.Open(expvarsTestFile)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open test file %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
vars, err := ParseExpvar(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(vars.Cmdline) != 3 {
|
||||
t.Fatalf("Cmdline should have 3 items, but has %d", len(vars.Cmdline))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var (
|
||||
interval = flag.Duration("i", 2*time.Second, "Polling interval")
|
||||
portsArg = flag.String("ports", "40001,40002,40000,40004", "Ports for accessing services expvars")
|
||||
dummy = flag.Bool("dummy", false, "Use dummy (console) output")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
ports, err := ParsePorts(*portsArg)
|
||||
if err != nil {
|
||||
log.Fatal("cannot parse ports:", err)
|
||||
}
|
||||
|
||||
var data Data
|
||||
var source Source = NewExpvarsSource(ports)
|
||||
for _, port := range ports {
|
||||
service := NewService(port)
|
||||
data.Services = append(data.Services, service)
|
||||
}
|
||||
|
||||
var ui UI = &TermUI{}
|
||||
if *dummy {
|
||||
ui = &DummyUI{}
|
||||
}
|
||||
ui.Init()
|
||||
defer ui.Close()
|
||||
|
||||
tick := time.NewTicker(*interval)
|
||||
evtCh := termui.EventCh()
|
||||
|
||||
update := func() {
|
||||
d, err := source.Update()
|
||||
if err != nil {
|
||||
log.Println("[ERROR] Cannot update data from source:", err)
|
||||
return
|
||||
}
|
||||
switch source.(type) {
|
||||
case *ExpvarsSource:
|
||||
dat := d.(Expvars)
|
||||
|
||||
for _, port := range source.(*ExpvarsSource).Ports {
|
||||
service := data.FindService(port)
|
||||
if service == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
service.Err = dat[port].Err
|
||||
service.Memstats = dat[port].MemStats
|
||||
service.Goroutines = dat[port].Goroutines
|
||||
service.Cmdline = strings.Join(dat[port].Cmdline, " ")
|
||||
if dat[port].Cmdline != nil {
|
||||
service.Name = filepath.Base(dat[port].Cmdline[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
data.LastTimestamp = time.Now()
|
||||
ui.Update(data)
|
||||
}
|
||||
update()
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
update()
|
||||
case e := <-evtCh:
|
||||
if e.Type == termui.EventKey && e.Ch == 'q' {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParsePorts converts comma-separated ports into strings slice
|
||||
func ParsePorts(s string) ([]string, error) {
|
||||
ports := strings.FieldsFunc(s, func(r rune) bool { return r == ',' })
|
||||
if len(ports) == 0 {
|
||||
return nil, errors.New("no ports specified")
|
||||
}
|
||||
|
||||
return ports, nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Data represents data to be passed to UI.
|
||||
type Data struct {
|
||||
Services Services
|
||||
Containers *Container
|
||||
LastTimestamp time.Time
|
||||
}
|
||||
|
||||
type Services []*Service
|
||||
|
||||
// Service represents constantly updating info about single service.
|
||||
type Service struct {
|
||||
Name string
|
||||
Port string
|
||||
IsAlive bool
|
||||
Cmdline string
|
||||
Memstats *runtime.MemStats
|
||||
Goroutines int64
|
||||
|
||||
Err error
|
||||
}
|
||||
|
||||
// NewService returns new Service object.
|
||||
func NewService(port string) *Service {
|
||||
return &Service{
|
||||
Name: port, // we have only port on start, so use it as name until resolved
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Data) FindService(port string) *Service {
|
||||
if d.Services == nil {
|
||||
return nil
|
||||
}
|
||||
for _, service := range d.Services {
|
||||
if service.Port == port {
|
||||
return service
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package main
|
||||
|
||||
type Source interface {
|
||||
Update() (interface{}, error)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package main
|
||||
|
||||
// UI represents UI module
|
||||
type UI interface {
|
||||
Init()
|
||||
Close()
|
||||
Update(Data)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pyk/byten"
|
||||
)
|
||||
|
||||
// DummyUI is an simple console UI mockup, for testing purposes.
|
||||
type DummyUI struct{}
|
||||
|
||||
func (u *DummyUI) Init() {}
|
||||
func (u *DummyUI) Close() {}
|
||||
func (u *DummyUI) Update(data Data) {
|
||||
if data.Services == nil {
|
||||
return
|
||||
}
|
||||
for _, service := range data.Services {
|
||||
fmt.Printf("%s: ", service.Name)
|
||||
if service.Err != nil {
|
||||
fmt.Printf("ERROR: %s", service.Err)
|
||||
continue
|
||||
}
|
||||
|
||||
if service.Memstats != nil {
|
||||
alloc := byten.Size(int64(service.Memstats.Alloc))
|
||||
sys := byten.Size(int64(service.Memstats.Sys))
|
||||
fmt.Printf("%s/%s ", alloc, sys)
|
||||
}
|
||||
|
||||
if service.Goroutines != 0 {
|
||||
fmt.Printf("goroutines: %d", service.Goroutines)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gizak/termui"
|
||||
"github.com/pyk/byten"
|
||||
"log"
|
||||
)
|
||||
|
||||
// TermUI is a termUI implementation of UI interface.
|
||||
type TermUI struct {
|
||||
}
|
||||
|
||||
func (t *TermUI) Init() {
|
||||
err := termui.Init()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
termui.UseTheme("helloworld")
|
||||
}
|
||||
|
||||
func (t *TermUI) Update(data Data) {
|
||||
text := fmt.Sprintf("monitoring %d services, press q to quit", len(data.Services))
|
||||
|
||||
p := termui.NewPar(text)
|
||||
p.Height = 3
|
||||
p.Width = termui.TermWidth() / 2
|
||||
p.TextFgColor = termui.ColorWhite
|
||||
p.Border.Label = "Services Monitor"
|
||||
p.Border.FgColor = termui.ColorCyan
|
||||
|
||||
text1 := fmt.Sprintf("Last update: %v", data.LastTimestamp.Format("15:04:05 02/Jan/06"))
|
||||
p1 := termui.NewPar(text1)
|
||||
p1.Height = 3
|
||||
p1.X = p.X + p.Width
|
||||
p1.Width = termui.TermWidth() - p1.X
|
||||
p1.TextFgColor = termui.ColorWhite
|
||||
p1.Border.Label = "Status"
|
||||
p1.Border.FgColor = termui.ColorCyan
|
||||
|
||||
ls := termui.NewList()
|
||||
|
||||
for _, service := range data.Services {
|
||||
if service.Err != nil {
|
||||
ls.Items = append(ls.Items, fmt.Sprintf("[E] %s failed", service.Name))
|
||||
continue
|
||||
}
|
||||
alloc := byten.Size(int64(service.Memstats.Alloc))
|
||||
sys := byten.Size(int64(service.Memstats.Sys))
|
||||
ls.Items = append(ls.Items, fmt.Sprintf("[R] %s: %s/%s goroutines: %d", service.Name, alloc, sys, service.Goroutines))
|
||||
}
|
||||
ls.ItemFgColor = termui.ColorYellow
|
||||
ls.Border.Label = "Services"
|
||||
ls.Height = 10
|
||||
ls.Width = 100
|
||||
ls.Width = termui.TermWidth()
|
||||
ls.Y = 3
|
||||
|
||||
dat := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6}
|
||||
spl0 := termui.NewSparkline()
|
||||
spl0.Data = dat[3:]
|
||||
spl0.LineColor = termui.ColorGreen
|
||||
|
||||
spls0 := termui.NewSparklines(spl0)
|
||||
spls0.Height = 2
|
||||
spls0.Width = 20
|
||||
spls0.X = 60
|
||||
spls0.Y = 3
|
||||
spls0.HasBorder = false
|
||||
|
||||
termui.Render(p, p1, ls, spls0)
|
||||
}
|
||||
|
||||
func (t *TermUI) Close() {
|
||||
termui.Close()
|
||||
}
|
Loading…
Reference in New Issue