Initial commit

This commit is contained in:
Ivan Daniluk 2015-04-21 12:51:01 +03:00
commit cbf804914c
11 changed files with 440 additions and 0 deletions

64
containers.go Normal file
View File

@ -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, "/")
}

76
expvars.go Normal file
View File

@ -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
}

4
expvars.json Normal file

File diff suppressed because one or more lines are too long

25
expvars_test.go Normal file
View File

@ -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))
}
}

82
main.go Normal file
View File

@ -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
}
}
}
}

16
ports.go Normal file
View File

@ -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
}

48
service.go Normal file
View File

@ -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
}

5
source.go Normal file
View File

@ -0,0 +1,5 @@
package main
type Source interface {
Update() (interface{}, error)
}

8
ui.go Normal file
View File

@ -0,0 +1,8 @@
package main
// UI represents UI module
type UI interface {
Init()
Close()
Update(Data)
}

35
ui_dummy.go Normal file
View File

@ -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")
}
}

77
ui_termui.go Normal file
View File

@ -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()
}