Skip to content

Commit 70837bd

Browse files
committed
Major code refactor
1 parent bb52c79 commit 70837bd

File tree

13 files changed

+425
-354
lines changed

13 files changed

+425
-354
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Above capture is jplot monitoring a Go service's [expvar](https://golang.org/pkg
99

1010
```
1111
jplot --url http://:8080/debug/vars \
12-
memstats.HeapAlloc+memstats.HeapSys+memstats.HeapAlloc+memstats.HeapIdle+marker:counter:memstats.NumGC \
12+
memstats.HeapAlloc+memstats.HeapSys+memstats.HeapAlloc+memstats.HeapIdle+marker,counter:memstats.NumGC \
1313
counter:memstats.TotalAlloc \
1414
memstats.HeapObjects \
1515
memstats.StackSys+memstats.StackInuse
@@ -85,7 +85,7 @@ Here is an example command to graph a Go program memstats:
8585

8686
```
8787
jplot --url http://:8080/debug/vars \
88-
memstats.HeapAlloc+memstats.HeapSys+memstats.HeapAlloc+memstats.HeapIdle+marker:counter:memstats.NumGC \
88+
memstats.HeapAlloc+memstats.HeapSys+memstats.HeapAlloc+memstats.HeapIdle+marker,counter:memstats.NumGC \
8989
counter:memstats.TotalAlloc \
9090
memstats.HeapObjects \
9191
memstats.StackSys+memstats.StackInuse

data/datapoint.go

Lines changed: 0 additions & 49 deletions
This file was deleted.

source/http.go renamed to data/http.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package source
1+
package data
22

33
import (
44
"io/ioutil"
@@ -8,7 +8,7 @@ import (
88
"github.com/elgs/gojq"
99
)
1010

11-
type HTTP struct {
11+
type httpSource struct {
1212
c chan res
1313
done chan struct{}
1414
}
@@ -18,16 +18,20 @@ type res struct {
1818
err error
1919
}
2020

21-
func NewHTTP(url string, interval time.Duration) HTTP {
22-
h := HTTP{
21+
// FromHTTP fetch data points from url every interval and keep size points.
22+
func FromHTTP(url string, interval time.Duration, size int) *Points {
23+
h := httpSource{
2324
c: make(chan res),
2425
done: make(chan struct{}),
2526
}
2627
go h.run(url, interval)
27-
return h
28+
return &Points{
29+
Size: size,
30+
Source: h,
31+
}
2832
}
2933

30-
func (h HTTP) run(url string, interval time.Duration) {
34+
func (h httpSource) run(url string, interval time.Duration) {
3135
t := time.NewTicker(interval)
3236
defer t.Stop()
3337
h.fetch(url)
@@ -41,7 +45,7 @@ func (h HTTP) run(url string, interval time.Duration) {
4145
}
4246
}
4347

44-
func (h HTTP) fetch(url string) {
48+
func (h httpSource) fetch(url string) {
4549
resp, err := http.Get(url)
4650
if err != nil {
4751
h.c <- res{err: err}
@@ -57,12 +61,12 @@ func (h HTTP) fetch(url string) {
5761
h.c <- res{jq: jq, err: err}
5862
}
5963

60-
func (h HTTP) Get() (*gojq.JQ, error) {
64+
func (h httpSource) Get() (*gojq.JQ, error) {
6165
res := <-h.c
6266
return res.jq, res.err
6367
}
6468

65-
func (h HTTP) Close() error {
69+
func (h httpSource) Close() error {
6670
close(h.done)
6771
return nil
6872
}

data/point.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package data
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"sync"
7+
8+
"github.com/elgs/gojq"
9+
)
10+
11+
// Getter is the interface used by Points to get next points.
12+
type Getter interface {
13+
io.Closer
14+
Get() (*gojq.JQ, error)
15+
}
16+
17+
// Points is a series of Size data points gathered from Source.
18+
type Points struct {
19+
// Size is the number of data point to store per metric.
20+
Size int
21+
Source Getter
22+
23+
points map[string][]float64
24+
last map[string]float64
25+
mu sync.Mutex
26+
}
27+
28+
// Run get data from the source and capture metrics following specs.
29+
func (p *Points) Run(specs []Spec) error {
30+
for {
31+
jq, err := p.Source.Get()
32+
if err != nil {
33+
return fmt.Errorf("input error: %v", err)
34+
}
35+
if jq == nil {
36+
break
37+
}
38+
for _, spec := range specs {
39+
for _, f := range spec.Fields {
40+
v, err := jq.Query(f.Name)
41+
if err != nil {
42+
return fmt.Errorf("cannot get %s: %v", f.Name, err)
43+
}
44+
n, ok := v.(float64)
45+
if !ok {
46+
return fmt.Errorf("invalid type %s: %T", f.Name, v)
47+
}
48+
p.push(f.ID, n, f.IsCounter)
49+
}
50+
}
51+
}
52+
return nil
53+
}
54+
55+
func (p *Points) push(name string, value float64, counter bool) {
56+
p.mu.Lock()
57+
defer p.mu.Unlock()
58+
d := p.getLocked(name)
59+
if counter {
60+
var diff float64
61+
if last := p.last[name]; last > 0 {
62+
diff = value - last
63+
}
64+
p.last[name] = value
65+
value = diff
66+
}
67+
d = append(append(make([]float64, 0, p.Size), d[1:]...), value)
68+
p.points[name] = d
69+
}
70+
71+
// Get gets the points vector for name.
72+
func (p *Points) Get(name string) []float64 {
73+
p.mu.Lock()
74+
defer p.mu.Unlock()
75+
return p.getLocked(name)
76+
}
77+
78+
func (p *Points) getLocked(name string) []float64 {
79+
if p.points == nil {
80+
p.points = make(map[string][]float64, 1)
81+
p.last = make(map[string]float64)
82+
}
83+
d, found := p.points[name]
84+
if !found {
85+
d = make([]float64, p.Size)
86+
p.points[name] = d
87+
}
88+
return d
89+
}
90+
91+
// Close calls Close on Source.
92+
func (p *Points) Close() error {
93+
return p.Source.Close()
94+
}

data/spec.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package data
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// Spec specify a list of field for a single graph.
9+
type Spec struct {
10+
Fields []Field
11+
}
12+
13+
// Field describe a field in a graph.
14+
type Field struct {
15+
ID string
16+
Name string
17+
IsCounter bool
18+
IsMarker bool
19+
}
20+
21+
// ParseSpec parses a graph specification. Each spec is a string with one or
22+
// more JSON path separated by + with fields options prefixed with colon and
23+
// separated by commas.
24+
func ParseSpec(args []string) ([]Spec, error) {
25+
specs := make([]Spec, 0, len(args))
26+
for i, v := range args {
27+
spec := Spec{}
28+
for j, name := range strings.Split(v, "+") {
29+
var isCounter bool
30+
var isMarker bool
31+
if strings.HasPrefix(name, "marker:counter:") {
32+
// Backward compat.
33+
name = strings.Replace(name, "marker:counter:", "marker,counter:", 1)
34+
}
35+
if idx := strings.IndexByte(name, ':'); idx != -1 {
36+
options := strings.Split(name[:idx], ",")
37+
name = name[idx+1:]
38+
for _, o := range options {
39+
switch o {
40+
case "counter":
41+
isCounter = true
42+
case "marker":
43+
isMarker = true
44+
default:
45+
return nil, fmt.Errorf("invalid field option: %s", o)
46+
}
47+
}
48+
}
49+
spec.Fields = append(spec.Fields, Field{
50+
ID: fmt.Sprintf("%d.%d.%s", i, j, name),
51+
Name: name,
52+
IsCounter: isCounter,
53+
IsMarker: isMarker,
54+
})
55+
}
56+
specs = append(specs, spec)
57+
}
58+
return specs, nil
59+
}

data/stdin.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package data
2+
3+
import (
4+
"bufio"
5+
"os"
6+
7+
"github.com/elgs/gojq"
8+
)
9+
10+
type stdin struct {
11+
scan *bufio.Scanner
12+
}
13+
14+
// FromStdin reads data from stdin as one JSON object per line.
15+
func FromStdin(size int) *Points {
16+
return &Points{
17+
Size: size,
18+
Source: stdin{bufio.NewScanner(os.Stdin)},
19+
}
20+
}
21+
22+
func (s stdin) Get() (*gojq.JQ, error) {
23+
if s.scan.Scan() {
24+
return gojq.NewStringQuery(s.scan.Text())
25+
}
26+
return nil, s.scan.Err()
27+
}
28+
29+
func (s stdin) Close() error {
30+
return nil
31+
}

graph/dash.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package graph
2+
3+
import (
4+
"image"
5+
"image/draw"
6+
"image/png"
7+
"io"
8+
9+
"github.com/rs/jplot/data"
10+
chart "github.com/wcharczuk/go-chart"
11+
)
12+
13+
type Dash struct {
14+
Specs []data.Spec
15+
Data *data.Points
16+
}
17+
18+
// Render generates a PNG with all graphs stacked.
19+
func (d Dash) Render(w io.Writer, width, height int) {
20+
graphs := make([]chart.Chart, 0, len(d.Specs))
21+
for _, spec := range d.Specs {
22+
graphs = append(graphs, New(spec, d.Data, width, height/len(d.Specs)))
23+
}
24+
canvas := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{width, height}})
25+
var top int
26+
for _, graph := range graphs {
27+
iw := &chart.ImageWriter{}
28+
graph.Render(chart.PNG, iw)
29+
img, _ := iw.Image()
30+
r := image.Rectangle{image.Point{0, top}, image.Point{width, top + graph.Height}}
31+
top += graph.Height
32+
draw.Draw(canvas, r, img, image.Point{0, 0}, draw.Src)
33+
}
34+
png.Encode(w, canvas)
35+
}

0 commit comments

Comments
 (0)