Skip to content

Commit 640908c

Browse files
Merge pull request #6 from istreamlabs/sample-rate
Add sample rate functionality to all implementations
2 parents 27b35e9 + 5d5a5c8 commit 640908c

File tree

12 files changed

+249
-11
lines changed

12 files changed

+249
-11
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ language: go
22
go:
33
- 1.8
44
- 1.9
5+
- '1.10'
56
install:
67
- go get -u github.com/golang/dep/cmd/dep
78
- dep ensure

CHANGELOG.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,28 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
- Put unreleased items here.
89

9-
- Add unreleased items here.
10+
## [1.3.0] - 2018-03-19
11+
12+
- Add `WithRate(float64)` to the metrics interface and to all clients that implement
13+
the interface. All metrics calls support sample rates.
14+
- The `LoggerClient`:
15+
- Applies the sample rate when printing log messages. If the rate is `0.1` and you call `Incr()` ten times, expect about one message to have been printed out.
16+
- Displays the sample rate for counts if it is not `1.0`, e.g: `Count foo:0.2 (2 * 0.1) [tag1 tag2]`. This shows the sampled value, the passed value, and the sample rate.
17+
- Gauges, timings, and histograms will show the sample rate, but he value is left unmodified just like the DataDog implementation.
18+
- The `RecorderClient`:
19+
- Records all sample rates for metrics calls in `MetricCall.Rate`. No calls are excluded from the call list based on the sample rate, and the value recorded is the full value before multiplying by the sample rate.
20+
- Adds a `Rate(float64)` query method to filter by sampled metrics.
21+
- The following should work:
22+
23+
```go
24+
recorder := metrics.NewRecorderClient().WithTest(t)
25+
recorder.WithRate(0.1).Count("foo", 5)
26+
recorder.Expect("foo").Rate(0.1).Value(5)
27+
```
28+
- Add `Colorized()` method to `LoggerClient`, and automatically detect a TTY and enable color when `nil` is passed to the `NewLoggerClient` constructor.
29+
- Test with Go 1.10.x.
1030

1131
## [1.2.0] - 2018-03-01
1232

Gopkg.lock

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ client.WithTags(map[string]string{
4949

5050
The above code would result in `myprefix.requests.count` with a value of `1` showing up in DataDog if you have [`dogstatsd`](https://docs.datadoghq.com/guides/dogstatsd/) running locally and an environment variable `env` set to `prod`, otherwise it will print metrics to standard out. See the [`Client`](https://godoc.org/github.com/istreamlabs/go-metrics/metrics/#Client) interface for a list of available metrics methods.
5151

52+
Sometimes you wouldn't want to send a metric every single time a piece of code is executed. This is supported by setting a sample rate:
53+
54+
```go
55+
// Sample rate for high-throughput applications
56+
client.WithRate(0.01).Incr("requests.count")
57+
```
58+
59+
Sample rates apply to metrics but not events. Any count-type metric (`Incr`, `Decr`, `Count`, and timing/histogram counts) will get multiplied to the full value, while gauges are sent unmodified. For example, when emitting a 10% sampled timing metric that takes an average of `200ms` to DataDog, you would see `1 call * (1/0.1 sample rate) = 10 calls` added to the histogram count while the average value remains `200ms` in the DataDog UI.
60+
5261
Also provided are useful clients for testing. For example, the following asserts that a metric with the given name, value, and tag was emitted during a test:
5362

5463
```go

metrics/client.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
// "tag": "value"
1919
// }).Incr("requests.count")
2020
//
21+
// // Sample rate for high-throughput applications
22+
// client.WithRate(0.01).Incr("requests.count")
23+
//
2124
// Also provided are useful clients for testing, both for when you want
2225
// to assert that certain metrics are emitted and a `NullClient` for when
2326
// you want to ignore them.
@@ -49,6 +52,9 @@ type Client interface {
4952
// WithTags returns a new client with the given tags.
5053
WithTags(tags map[string]string) Client
5154

55+
// WithRate returns a new client with the given sample rate.
56+
WithRate(rate float64) Client
57+
5258
// Count/Incr/Decr set a numeric integer value.
5359
Count(name string, value int64)
5460
Incr(name string)

metrics/logger.go

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
package metrics
22

33
import (
4+
"fmt"
45
"log"
6+
"math/rand"
57
"os"
8+
"sort"
69
"time"
710

811
"github.com/DataDog/datadog-go/statsd"
12+
"github.com/mattn/go-isatty"
13+
"github.com/mgutz/ansi"
14+
)
15+
16+
var (
17+
// Colors are from the ANSI 256 color pallette.
18+
// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
19+
cname = ansi.ColorFunc("208")
20+
cvalue = ansi.ColorFunc("32")
21+
crate = ansi.ColorFunc("106")
22+
csampled = ansi.ColorFunc("43")
23+
ctag = ansi.ColorFunc("133")
924
)
1025

1126
// InfoLogger provides a method for logging info messages and is implemented
@@ -18,20 +33,46 @@ type InfoLogger interface {
1833
// locally for testing. Can be used with multiple different logging systems.
1934
type LoggerClient struct {
2035
logger InfoLogger
36+
colors bool
37+
rate float64
2138
tagMap map[string]string
2239
}
2340

2441
// NewLoggerClient creates a new logging client. If `logger` is `nil` then it
25-
// defaults to stdout using the built-in `log` package. It is equivalent to:
42+
// defaults to stdout using the built-in `log` package. It is equivalent to
43+
// the following with added auto-detection for colorized output:
2644
//
2745
// metrics.NewLoggerClient(log.New(os.Stdout, "", 0))
46+
//
47+
// You can use your own logger and enable colorized output manually via:
48+
//
49+
// metrics.NewLoggerClient(myLog).Colorized()
2850
func NewLoggerClient(logger InfoLogger) *LoggerClient {
51+
colors := false
2952
if logger == nil {
3053
logger = log.New(os.Stdout, "", 0)
54+
55+
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
56+
colors = true
57+
}
3158
}
3259

33-
return &LoggerClient{
60+
client := &LoggerClient{
3461
logger: logger,
62+
colors: colors,
63+
rate: 1.0,
64+
}
65+
66+
return client
67+
}
68+
69+
// Colorized enables colored terminal output.
70+
func (c *LoggerClient) Colorized() *LoggerClient {
71+
return &LoggerClient{
72+
logger: c.logger,
73+
rate: c.rate,
74+
colors: true,
75+
tagMap: combine(map[string]string{}, c.tagMap),
3576
}
3677
}
3778

@@ -40,13 +81,76 @@ func NewLoggerClient(logger InfoLogger) *LoggerClient {
4081
func (c *LoggerClient) WithTags(tags map[string]string) Client {
4182
return &LoggerClient{
4283
logger: c.logger,
84+
rate: c.rate,
85+
colors: c.colors,
4386
tagMap: combine(c.tagMap, tags),
4487
}
4588
}
4689

90+
// WithRate clones this client with a given sample rate. Subsequent calls
91+
// will be limited to logging metrics at this rate.
92+
func (c *LoggerClient) WithRate(rate float64) Client {
93+
return &LoggerClient{
94+
logger: c.logger,
95+
rate: rate,
96+
colors: c.colors,
97+
tagMap: combine(map[string]string{}, c.tagMap),
98+
}
99+
}
100+
101+
// print out the metric call, taking into account sample rate.
102+
func (c *LoggerClient) print(t string, name string, value interface{}, sampled interface{}) {
103+
r := fmt.Sprintf("%v", c.rate)
104+
v := value
105+
s := sampled
106+
107+
if c.colors {
108+
name = cname(name)
109+
r = crate(r)
110+
v = cvalue(fmt.Sprintf("%v", value))
111+
s = csampled(fmt.Sprintf("%v", sampled))
112+
}
113+
114+
if c.rate == 1.0 {
115+
c.logger.Printf("%s %s:%v %v", t, name, v, c.getTags())
116+
return
117+
}
118+
119+
if rand.Float64() < c.rate {
120+
if value == sampled {
121+
c.logger.Printf("%s %s:%v (%v) %v", t, name, v, r, c.getTags())
122+
} else {
123+
c.logger.Printf("%s %s:%v (%v * %v) %v", t, name, s, v, r, c.getTags())
124+
}
125+
}
126+
}
127+
128+
func (c *LoggerClient) getTags() string {
129+
if !c.colors {
130+
return fmt.Sprintf("%v", c.tagMap)
131+
}
132+
133+
keys := make([]string, 0, len(c.tagMap))
134+
for k := range c.tagMap {
135+
keys = append(keys, k)
136+
}
137+
sort.Strings(keys)
138+
139+
tags := ""
140+
for _, key := range keys {
141+
if tags != "" {
142+
tags += " "
143+
}
144+
145+
tags += fmt.Sprintf("%s:%s", ctag(key), c.tagMap[key])
146+
}
147+
148+
return "map[" + tags + "]"
149+
}
150+
47151
// Count adds some value to a metric.
48152
func (c *LoggerClient) Count(name string, value int64) {
49-
c.logger.Printf("Count %s:%d %v", name, value, c.tagMap)
153+
c.print("Count", name, value, float64(value)*c.rate)
50154
}
51155

52156
// Incr adds one to a metric.
@@ -61,7 +165,7 @@ func (c *LoggerClient) Decr(name string) {
61165

62166
// Gauge sets a numeric value.
63167
func (c *LoggerClient) Gauge(name string, value float64) {
64-
c.logger.Printf("Gauge %s:%v %v", name, value, c.tagMap)
168+
c.print("Gauge", name, value, value)
65169
}
66170

67171
// Event tracks an event that may be relevant to other metrics.
@@ -71,10 +175,10 @@ func (c *LoggerClient) Event(e *statsd.Event) {
71175

72176
// Timing tracks a duration.
73177
func (c *LoggerClient) Timing(name string, value time.Duration) {
74-
c.logger.Printf("Timing %s:%s %v", name, value, c.tagMap)
178+
c.print("Timing", name, value, value)
75179
}
76180

77181
// Histogram sets a numeric value while tracking min/max/avg/p95/etc.
78182
func (c *LoggerClient) Histogram(name string, value float64) {
79-
c.logger.Printf("Histogram %s:%v %v", name, value, c.tagMap)
183+
c.print("Histogram", name, value, value)
80184
}

metrics/logger_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,18 @@ func TestLoggerClient(t *testing.T) {
5353
ExpectEqual(t, "Count one:-1 map[]", recorder.messages[3])
5454
ExpectEqual(t, "Gauge memory:1024 map[]", recorder.messages[4])
5555
ExpectEqual(t, "Histogram histo:123 map[]", recorder.messages[5])
56+
57+
// Make sure the call works, but since it is randomly sampled we have no
58+
// assertion to make.
59+
sampled := client.WithRate(0.8)
60+
sampled.Incr("sampled")
61+
sampled.Incr("sampled")
62+
sampled.Gauge("sampled-gauge", 123)
63+
64+
// Test colorized output
65+
client.(*metrics.LoggerClient).Colorized().WithTags(map[string]string{
66+
"tag1": "val1",
67+
"tag2": "val2",
68+
}).Incr("colored")
69+
ExpectEqual(t, "Count \x1b[38;5;208mcolored\x1b[0m:\x1b[38;5;32m1\x1b[0m map[\x1b[38;5;133mtag1\x1b[0m:val1 \x1b[38;5;133mtag2\x1b[0m:val2]", recorder.messages[len(recorder.messages)-1])
5670
}

metrics/null.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ func (c *NullClient) WithTags(tags map[string]string) Client {
2222
return &NullClient{}
2323
}
2424

25+
// WithRate clones this client with a given sample rate.
26+
func (c *NullClient) WithRate(rate float64) Client {
27+
return &NullClient{}
28+
}
29+
2530
// Count adds some value to a metric.
2631
func (c *NullClient) Count(name string, value int64) {
2732
}

metrics/null_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,6 @@ func TestNullClientMethods(t *testing.T) {
3232
client.Timing("timing", time.Duration(123))
3333

3434
client.Event(&statsd.Event{})
35+
36+
client.WithRate(1.2).Incr("rated")
3537
}

metrics/query.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ type Query interface {
6565
// TagName filters out any metric or event that does not contain a given
6666
// tag with name `name`. The value does not matter.
6767
TagName(name string) Query
68+
69+
// Rate filters out any metric that does not have the given sample rate.
70+
Rate(rate float64) Query
6871
}
6972

7073
// query is an implementation of the `Query` interface.
@@ -266,3 +269,23 @@ func (q *query) TagName(name string) Query {
266269

267270
return q
268271
}
272+
273+
// Rate expects a value with the given rate to exist.
274+
func (q *query) Rate(rate float64) Query {
275+
q.history = fmt.Sprintf("%s rate(%f)", q.history, rate)
276+
q.filter(func(call Call) bool {
277+
switch t := call.(type) {
278+
case *MetricCall:
279+
return t.Rate == rate
280+
case *EventCall:
281+
return false
282+
}
283+
return false
284+
})
285+
286+
if q.checkMin && len(q.calls) < q.minCalls {
287+
q.fatalf("Expected rate '%f'", rate)
288+
}
289+
290+
return q
291+
}

0 commit comments

Comments
 (0)