commit cbf804914c4057d7177984901ea077b2097ffaee Author: Ivan Daniluk Date: Tue Apr 21 12:51:01 2015 +0300 Initial commit diff --git a/containers.go b/containers.go new file mode 100644 index 0000000..f207f60 --- /dev/null +++ b/containers.go @@ -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, "/") +} diff --git a/expvars.go b/expvars.go new file mode 100644 index 0000000..12c7fbc --- /dev/null +++ b/expvars.go @@ -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 +} diff --git a/expvars.json b/expvars.json new file mode 100644 index 0000000..881b029 --- /dev/null +++ b/expvars.json @@ -0,0 +1,4 @@ +{ +"cmdline": ["./geo.service/geo.service","-p=:40004","-dsn=root:@tcp(localhost:3306)/geo"], +"memstats": {"Alloc":306192,"TotalAlloc":18134040,"Sys":3999992,"Lookups":961,"Mallocs":219206,"Frees":218107,"HeapAlloc":306192,"HeapSys":1851392,"HeapIdle":1163264,"HeapInuse":688128,"HeapReleased":1114112,"HeapObjects":1099,"StackInuse":245760,"StackSys":245760,"MSpanInuse":6968,"MSpanSys":16384,"MCacheInuse":1200,"MCacheSys":16384,"BuckHashSys":1445672,"GCSys":137579,"OtherSys":286821,"NextGC":499776,"LastGC":1429554826339587426,"PauseTotalNs":58953963,"PauseNs":[149673,116970,116282,152960,241678,269277,2345682,2813202,395563,896996,413271,524246,411490,438143,465269,410017,407874,429678,407380,400152,462808,416069,396655,411999,2235261,483709,532587,423111,402061,410275,527945,377815,454049,398089,439974,428984,454590,448865,438237,408540,432028,476609,461090,459348,407046,447736,442569,501791,390718,451293,411698,554251,612890,454626,539950,475728,481920,436132,537898,407788,366017,528018,445895,475310,426996,510830,563059,562691,632919,605119,580397,469276,593978,815423,426771,575456,872826,929380,699880,1099353,727790,825090,815515,1143818,801543,2286288,826334,791492,817455,440831,376237,2237337,519402,694540,782088,948074,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"PauseEnd":[1429551836329548506,1429551836329992823,1429551836330364410,1429551836330972293,1429551836332086156,1429551836332787858,1429551896183637429,1429552016336417703,1429552038892146378,1429552038905749325,1429552038920018001,1429552038931843671,1429552038945437201,1429552038960316731,1429552038974053317,1429552038989797695,1429552039002185699,1429552039014183958,1429552039026587369,1429552039038164871,1429552039051065585,1429552039062402317,1429552039074574600,1429552039087577539,1429552039099006520,1429552039113868847,1429552039126842855,1429552039138210121,1429552039150669078,1429552045073734820,1429552045084533164,1429552045097820035,1429552045111401775,1429552045124221035,1429552045136039350,1429552045146856774,1429552045158451390,1429552045172432052,1429552045183862574,1429552045195450754,1429552045206890854,1429552045217811105,1429552045229223666,1429552045239924164,1429552045252202806,1429552045263999145,1429552045275883429,1429552045286550637,1429552045299642824,1429552045310732720,1429552045322523365,1429552049787455566,1429552049801233968,1429552049813748109,1429552049826657828,1429552049838428456,1429552049849704476,1429552049860731436,1429552049872770863,1429552049887348193,1429552049899354726,1429552049910091446,1429552049922791888,1429552049935592566,1429552049949048309,1429552049961817616,1429552049975874584,1429552049992176815,1429552050006202883,1429552050021524454,1429552050035892415,1429552050051383684,1429552050063614314,1429552170337189191,1429552291337252450,1429552412338543040,1429552533335058879,1429552653337824128,1429552774336205167,1429552895338586147,1429553016337759124,1429553136339470606,1429553257339468848,1429553378336184002,1429553499338033468,1429553619340990287,1429553740339511139,1429553861334820854,1429553981339514309,1429554102337221147,1429554223338016039,1429554344338826682,1429554465336311605,1429554585339334313,1429554706335732334,1429554826339586952,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"NumGC":96,"EnableGC":true,"DebugGC":false,"BySize":[{"Size":0,"Mallocs":0,"Frees":0},{"Size":8,"Mallocs":39541,"Frees":39497},{"Size":16,"Mallocs":47557,"Frees":47188},{"Size":32,"Mallocs":25990,"Frees":25864},{"Size":48,"Mallocs":36196,"Frees":36005},{"Size":64,"Mallocs":9930,"Frees":9822},{"Size":80,"Mallocs":585,"Frees":542},{"Size":96,"Mallocs":5530,"Frees":5518},{"Size":112,"Mallocs":8345,"Frees":8336},{"Size":128,"Mallocs":921,"Frees":916},{"Size":144,"Mallocs":23,"Frees":21},{"Size":160,"Mallocs":1451,"Frees":1432},{"Size":176,"Mallocs":13320,"Frees":13295},{"Size":192,"Mallocs":10,"Frees":2},{"Size":208,"Mallocs":3866,"Frees":3841},{"Size":224,"Mallocs":1,"Frees":0},{"Size":240,"Mallocs":1,"Frees":0},{"Size":256,"Mallocs":3,"Frees":0},{"Size":288,"Mallocs":62,"Frees":31},{"Size":320,"Mallocs":4,"Frees":3},{"Size":352,"Mallocs":69,"Frees":62},{"Size":384,"Mallocs":2,"Frees":0},{"Size":416,"Mallocs":932,"Frees":925},{"Size":448,"Mallocs":0,"Frees":0},{"Size":480,"Mallocs":2,"Frees":0},{"Size":512,"Mallocs":0,"Frees":0},{"Size":576,"Mallocs":30,"Frees":19},{"Size":640,"Mallocs":5,"Frees":2},{"Size":704,"Mallocs":4,"Frees":3},{"Size":768,"Mallocs":0,"Frees":0},{"Size":896,"Mallocs":13,"Frees":9},{"Size":1024,"Mallocs":1,"Frees":0},{"Size":1152,"Mallocs":21,"Frees":18},{"Size":1280,"Mallocs":2,"Frees":2},{"Size":1408,"Mallocs":3,"Frees":1},{"Size":1536,"Mallocs":0,"Frees":0},{"Size":1664,"Mallocs":8,"Frees":4},{"Size":2048,"Mallocs":70,"Frees":68},{"Size":2304,"Mallocs":20,"Frees":14},{"Size":2560,"Mallocs":2,"Frees":1},{"Size":2816,"Mallocs":1,"Frees":1},{"Size":3072,"Mallocs":0,"Frees":0},{"Size":3328,"Mallocs":4,"Frees":1},{"Size":4096,"Mallocs":1856,"Frees":1852},{"Size":4608,"Mallocs":19,"Frees":18},{"Size":5376,"Mallocs":7,"Frees":4},{"Size":6144,"Mallocs":22,"Frees":17},{"Size":6400,"Mallocs":0,"Frees":0},{"Size":6656,"Mallocs":1,"Frees":0},{"Size":6912,"Mallocs":0,"Frees":0},{"Size":8192,"Mallocs":0,"Frees":0},{"Size":8448,"Mallocs":0,"Frees":0},{"Size":8704,"Mallocs":2,"Frees":2},{"Size":9472,"Mallocs":4,"Frees":3},{"Size":10496,"Mallocs":1,"Frees":0},{"Size":12288,"Mallocs":1,"Frees":1},{"Size":13568,"Mallocs":0,"Frees":0},{"Size":14080,"Mallocs":0,"Frees":0},{"Size":16384,"Mallocs":0,"Frees":0},{"Size":16640,"Mallocs":0,"Frees":0},{"Size":17664,"Mallocs":0,"Frees":0}]} +} diff --git a/expvars_test.go b/expvars_test.go new file mode 100644 index 0000000..d039adf --- /dev/null +++ b/expvars_test.go @@ -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)) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9896557 --- /dev/null +++ b/main.go @@ -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 + } + } + } +} diff --git a/ports.go b/ports.go new file mode 100644 index 0000000..686a0ff --- /dev/null +++ b/ports.go @@ -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 +} diff --git a/service.go b/service.go new file mode 100644 index 0000000..37433ce --- /dev/null +++ b/service.go @@ -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 +} diff --git a/source.go b/source.go new file mode 100644 index 0000000..17920dc --- /dev/null +++ b/source.go @@ -0,0 +1,5 @@ +package main + +type Source interface { + Update() (interface{}, error) +} diff --git a/ui.go b/ui.go new file mode 100644 index 0000000..4a5bd73 --- /dev/null +++ b/ui.go @@ -0,0 +1,8 @@ +package main + +// UI represents UI module +type UI interface { + Init() + Close() + Update(Data) +} diff --git a/ui_dummy.go b/ui_dummy.go new file mode 100644 index 0000000..0b117c3 --- /dev/null +++ b/ui_dummy.go @@ -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") + } +} diff --git a/ui_termui.go b/ui_termui.go new file mode 100644 index 0000000..620efa6 --- /dev/null +++ b/ui_termui.go @@ -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() +}