Skip to content

Commit c1c03e7

Browse files
authored
Merge pull request #40 from keep-network/metrics-package
Metrics package Added metrics package which provides tools useful for gathering and exposing system metrics for external monitoring tools.
2 parents 55f9673 + 0bdcc12 commit c1c03e7

8 files changed

Lines changed: 511 additions & 0 deletions

File tree

pkg/metrics/gauge.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package metrics
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"sync"
7+
"time"
8+
)
9+
10+
// Gauge is a metric type that represents a single numerical value that can
11+
// arbitrarily go up and down.
12+
type Gauge struct {
13+
name string
14+
labels map[string]string
15+
16+
value float64
17+
timestamp int64 // timestamp expressed as milliseconds
18+
mutex sync.RWMutex
19+
}
20+
21+
// Set allows setting the gauge to an arbitrary value.
22+
func (g *Gauge) Set(value float64) {
23+
g.mutex.Lock()
24+
defer g.mutex.Unlock()
25+
26+
g.value = value
27+
g.timestamp = time.Now().UnixNano() / 1e6
28+
}
29+
30+
// Exposes the gauge in the text-based exposition format.
31+
func (g *Gauge) expose() string {
32+
g.mutex.RLock()
33+
defer g.mutex.RUnlock()
34+
35+
typeLine := fmt.Sprintf("# TYPE %v %v", g.name, "gauge")
36+
37+
labelsStrings := make([]string, 0)
38+
for name, value := range g.labels {
39+
labelsStrings = append(
40+
labelsStrings,
41+
fmt.Sprintf("%v=\"%v\"", name, value),
42+
)
43+
}
44+
labels := strings.Join(labelsStrings, ",")
45+
46+
if len(labels) > 0 {
47+
labels = "{" + labels + "}"
48+
}
49+
50+
body := fmt.Sprintf("%v%v %v %v", g.name, labels, g.value, g.timestamp)
51+
52+
return fmt.Sprintf("%v\n%v", typeLine, body)
53+
}

pkg/metrics/gauge_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package metrics
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestGaugeSet(t *testing.T) {
8+
gauge := &Gauge{
9+
name: "test_gauge",
10+
labels: map[string]string{"label": "value"},
11+
}
12+
13+
if gauge.value != 0 {
14+
t.Fatal("incorrect gauge initial value")
15+
}
16+
17+
if gauge.timestamp != 0 {
18+
t.Fatal("incorrect gauge initial timestamp")
19+
}
20+
21+
newGaugeValue := float64(500)
22+
23+
gauge.Set(newGaugeValue)
24+
25+
if gauge.value != newGaugeValue {
26+
t.Fatalf(
27+
"incorrect gauge value:\n"+
28+
"expected: [%v]\n"+
29+
"actual: [%v]",
30+
newGaugeValue,
31+
gauge.value,
32+
)
33+
}
34+
35+
if gauge.timestamp == 0 {
36+
t.Fatal("timestamp should be set")
37+
}
38+
}
39+
40+
func TestGaugeExpose(t *testing.T) {
41+
gauge := &Gauge{
42+
name: "test_gauge",
43+
labels: map[string]string{"label": "value"},
44+
value: 500,
45+
timestamp: 1000,
46+
}
47+
48+
actualText := gauge.expose()
49+
50+
expectedText := "# TYPE test_gauge gauge\ntest_gauge{label=\"value\"} 500 1000"
51+
52+
if actualText != expectedText {
53+
t.Fatalf(
54+
"incorrect gauge expose text:\n"+
55+
"expected: [%v]\n"+
56+
"actual: [%v]",
57+
expectedText,
58+
actualText,
59+
)
60+
}
61+
}
62+
63+
func TestGaugeWithoutLabelsExpose(t *testing.T) {
64+
gauge := &Gauge{
65+
name: "test_gauge",
66+
labels: map[string]string{},
67+
value: 500,
68+
timestamp: 1000,
69+
}
70+
71+
actualText := gauge.expose()
72+
73+
expectedText := "# TYPE test_gauge gauge\ntest_gauge 500 1000"
74+
75+
if actualText != expectedText {
76+
t.Fatalf(
77+
"incorrect gauge expose text:\n"+
78+
"expected: [%v]\n"+
79+
"actual: [%v]",
80+
expectedText,
81+
actualText,
82+
)
83+
}
84+
}

pkg/metrics/info.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package metrics
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// Info is a metric type that represents a constant information
9+
// that cannot change in the time.
10+
type Info struct {
11+
name string
12+
labels map[string]string
13+
}
14+
15+
// Exposes the info in the text-based exposition format.
16+
func (i *Info) expose() string {
17+
labelsStrings := make([]string, 0)
18+
for name, value := range i.labels {
19+
labelsStrings = append(
20+
labelsStrings,
21+
fmt.Sprintf("%v=\"%v\"", name, value),
22+
)
23+
}
24+
labels := strings.Join(labelsStrings, ",")
25+
26+
return fmt.Sprintf("%v{%v} %v", i.name, labels, "1")
27+
}

pkg/metrics/info_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package metrics
2+
3+
import "testing"
4+
5+
func TestInfoExpose(t *testing.T) {
6+
info := &Info{
7+
name: "test_info",
8+
labels: map[string]string{"label": "value"},
9+
}
10+
11+
actualText := info.expose()
12+
13+
expectedText := "test_info{label=\"value\"} 1"
14+
15+
if actualText != expectedText {
16+
t.Fatalf(
17+
"incorrect gauge expose text:\n"+
18+
"expected: [%v]\n"+
19+
"actual: [%v]",
20+
expectedText,
21+
actualText,
22+
)
23+
}
24+
}

pkg/metrics/observer.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package metrics
2+
3+
import (
4+
"context"
5+
"time"
6+
)
7+
8+
// ObserverInput defines a source of metric data.
9+
type ObserverInput func() float64
10+
11+
// ObserverOutput defines a destination of collected metric data.
12+
type ObserverOutput interface {
13+
Set(value float64)
14+
}
15+
16+
// Observer represent a definition of a cyclic metric observation process.
17+
type Observer struct {
18+
input ObserverInput
19+
output ObserverOutput
20+
}
21+
22+
// Observe triggers a cyclic metric observation process.
23+
func (o *Observer) Observe(
24+
ctx context.Context,
25+
tick time.Duration,
26+
) {
27+
go func() {
28+
ticker := time.NewTicker(tick)
29+
defer ticker.Stop()
30+
31+
for {
32+
select {
33+
case <-ticker.C:
34+
o.output.Set(o.input())
35+
case <-ctx.Done():
36+
return
37+
}
38+
}
39+
}()
40+
}

pkg/metrics/observer_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package metrics
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestObserve(t *testing.T) {
10+
input := func() float64 {
11+
return 5000
12+
}
13+
gauge := &Gauge{}
14+
ctx, _ := context.WithTimeout(context.Background(), 5*time.Millisecond)
15+
16+
observer := &Observer{input, gauge}
17+
18+
observer.Observe(ctx, 1*time.Millisecond)
19+
20+
<-ctx.Done()
21+
22+
expectedGaugeValue := float64(5000)
23+
if gauge.value != expectedGaugeValue {
24+
t.Fatalf(
25+
"incorrect gauge value:\n"+
26+
"expected: [%v]\n"+
27+
"actual: [%v]",
28+
expectedGaugeValue,
29+
gauge.value,
30+
)
31+
}
32+
33+
if gauge.timestamp == 0 {
34+
t.Fatal("timestamp should be set")
35+
}
36+
}

0 commit comments

Comments
 (0)