Skip to content

Commit 1f6135d

Browse files
authored
Support Datadog distribution metric type (#132)
The Datadog client now has the ability to send histogram metrics as the Datadog-specific distribution metric type. The Datadog client configuration has a new `DistributionPrefixes` item which specifies the prefixes of metric names that, when reported as histograms to the stats library, are to be sent as distributions instead. For example, when the prefix list is set to `{ "dist_" }`, then any histogram metric whose name begins with "dist_" is sent as a distribution; all other histograms are sent as ordinary histograms, as before. The default configuration sends no histograms as distributions.
1 parent bed3e79 commit 1f6135d

File tree

6 files changed

+158
-21
lines changed

6 files changed

+158
-21
lines changed

datadog/client.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ const (
2525
MaxBufferSize = 65507
2626
)
2727

28-
// DefaultFilter is the default tag to filter before sending to
29-
// datadog. Using the request path as a tag can overwhelm datadog's
30-
// servers if there are too many unique routes due to unique IDs being a
31-
// part of the path. Only change the default filter if there is a static
32-
// number of routes.
3328
var (
29+
// DefaultFilters are the default tags to filter before sending to
30+
// datadog. Using the request path as a tag can overwhelm datadog's
31+
// servers if there are too many unique routes due to unique IDs being a
32+
// part of the path. Only change the default filters if there are a static
33+
// number of routes.
3434
DefaultFilters = []string{"http_req_path"}
35+
36+
// DefaultDistributionPrefixes is the default set of name prefixes for
37+
// metrics to be sent as distributions instead of as histograms.
38+
DefaultDistributionPrefixes = []string{}
3539
)
3640

3741
// The ClientConfig type is used to configure datadog clients.
@@ -44,6 +48,10 @@ type ClientConfig struct {
4448

4549
// List of tags to filter. If left nil is set to DefaultFilters.
4650
Filters []string
51+
52+
// Set of name prefixes for metrics to be sent as distributions instead of
53+
// as histograms.
54+
DistributionPrefixes []string
4755
}
4856

4957
// Client represents an datadog client that implements the stats.Handler
@@ -77,6 +85,10 @@ func NewClientWith(config ClientConfig) *Client {
7785
config.Filters = DefaultFilters
7886
}
7987

88+
if config.DistributionPrefixes == nil {
89+
config.DistributionPrefixes = DefaultDistributionPrefixes
90+
}
91+
8092
// transform filters from array to map
8193
filterMap := make(map[string]struct{})
8294
for _, f := range config.Filters {
@@ -85,7 +97,8 @@ func NewClientWith(config ClientConfig) *Client {
8597

8698
c := &Client{
8799
serializer: serializer{
88-
filters: filterMap,
100+
filters: filterMap,
101+
distPrefixes: config.DistributionPrefixes,
89102
},
90103
}
91104

@@ -124,14 +137,15 @@ func (c *Client) Close() error {
124137
}
125138

126139
type serializer struct {
127-
conn net.Conn
128-
bufferSize int
129-
filters map[string]struct{}
140+
conn net.Conn
141+
bufferSize int
142+
filters map[string]struct{}
143+
distPrefixes []string
130144
}
131145

132146
func (s *serializer) AppendMeasures(b []byte, _ time.Time, measures ...stats.Measure) []byte {
133147
for _, m := range measures {
134-
b = AppendMeasureFiltered(b, m, s.filters)
148+
b = AppendMeasureFiltered(b, m, s.filters, s.distPrefixes)
135149
}
136150
return b
137151
}

datadog/client_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,29 @@ func TestClient(t *testing.T) {
3535
}
3636
}
3737

38+
func TestClientWithDistributionPrefixes(t *testing.T) {
39+
client := NewClientWith(ClientConfig{
40+
Address: DefaultAddress,
41+
DistributionPrefixes: []string{"dist_"},
42+
})
43+
44+
client.HandleMeasures(time.Time{}, stats.Measure{
45+
Name: "request",
46+
Fields: []stats.Field{
47+
{Name: "count", Value: stats.ValueOf(5)},
48+
stats.MakeField("dist_rtt", stats.ValueOf(100*time.Millisecond), stats.Histogram),
49+
},
50+
Tags: []stats.Tag{
51+
stats.T("answer", "42"),
52+
stats.T("hello", "world"),
53+
},
54+
})
55+
56+
if err := client.Close(); err != nil {
57+
t.Error(err)
58+
}
59+
}
60+
3861
func TestClientWriteLargeMetrics(t *testing.T) {
3962
const data = `main.http.error.count:0|c|#http_req_content_charset:,http_req_content_endoing:,http_req_content_type:,http_req_host:localhost:3011,http_req_method:GET,http_req_protocol:HTTP/1.1,http_req_transfer_encoding:identity
4063
main.http.message.count:1|c|#http_req_content_charset:,http_req_content_endoing:,http_req_content_type:,http_req_host:localhost:3011,http_req_method:GET,http_req_protocol:HTTP/1.1,http_req_transfer_encoding:identity,operation:read,type:request

datadog/measure.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@ package datadog
33
import (
44
"math"
55
"strconv"
6+
"strings"
67

78
"github.com/segmentio/stats/v4"
89
)
910

11+
// Datagram format: https://docs.datadoghq.com/developers/dogstatsd/datagram_shell
12+
1013
// AppendMeasure is a formatting routine to append the dogstatsd protocol
1114
// representation of a measure to a memory buffer.
1215
func AppendMeasure(b []byte, m stats.Measure) []byte {
13-
return AppendMeasureFiltered(b, m, nil)
16+
return AppendMeasureFiltered(b, m, nil, []string{})
1417
}
1518

1619
// AppendMeasureFiltered is a formatting routine to append the dogstatsd protocol
1720
// representation of a measure to a memory buffer. Tags listed in the filters map
1821
// are removed. (some tags may not be suitable for submission to DataDog)
19-
func AppendMeasureFiltered(b []byte, m stats.Measure, filters map[string]struct{}) []byte {
22+
func AppendMeasureFiltered(b []byte, m stats.Measure, filters map[string]struct{},
23+
distPrefixes []string) []byte {
2024
for _, field := range m.Fields {
2125
b = append(b, m.Name...)
2226
if len(field.Name) != 0 {
@@ -50,7 +54,11 @@ func AppendMeasureFiltered(b []byte, m stats.Measure, filters map[string]struct{
5054
case stats.Gauge:
5155
b = append(b, '|', 'g')
5256
default:
53-
b = append(b, '|', 'h')
57+
if sendDist(field.Name, distPrefixes) {
58+
b = append(b, '|', 'd')
59+
} else {
60+
b = append(b, '|', 'h')
61+
}
5462
}
5563

5664
if n := len(m.Tags); n != 0 {
@@ -86,3 +94,15 @@ func normalizeFloat(f float64) float64 {
8694
return f
8795
}
8896
}
97+
98+
func sendDist(name string, distPrefixes []string) bool {
99+
if distPrefixes == nil {
100+
return false
101+
}
102+
for _, prefix := range distPrefixes {
103+
if strings.HasPrefix(name, prefix) {
104+
return true
105+
}
106+
}
107+
return false
108+
}

datadog/measure_test.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import (
99

1010
var (
1111
testMeasures = []struct {
12-
m stats.Measure
13-
s string
12+
m stats.Measure
13+
s string
14+
dp []string
1415
}{
1516
{
1617
m: stats.Measure{
@@ -21,6 +22,7 @@ var (
2122
},
2223
s: `request.count:5|c
2324
`,
25+
dp: []string{},
2426
},
2527

2628
{
@@ -38,18 +40,73 @@ var (
3840
s: `request.count:5|c|#answer:42,hello:world
3941
request.rtt:0.1|h|#answer:42,hello:world
4042
`,
43+
dp: []string{},
44+
},
45+
46+
{
47+
m: stats.Measure{
48+
Name: "request",
49+
Fields: []stats.Field{
50+
stats.MakeField("dist_rtt", 100*time.Millisecond, stats.Histogram),
51+
},
52+
Tags: []stats.Tag{
53+
stats.T("answer", "42"),
54+
stats.T("hello", "world"),
55+
},
56+
},
57+
s: `request.dist_rtt:0.1|d|#answer:42,hello:world
58+
`,
59+
dp: []string{"dist_"},
4160
},
4261
}
4362
)
4463

4564
func TestAppendMeasure(t *testing.T) {
4665
for _, test := range testMeasures {
4766
t.Run(test.s, func(t *testing.T) {
48-
if s := string(AppendMeasure(nil, test.m)); s != test.s {
67+
if s := string(AppendMeasureFiltered(nil, test.m, nil, test.dp)); s != test.s {
4968
t.Error("bad metric representation:")
5069
t.Log("expected:", test.s)
5170
t.Log("found: ", s)
5271
}
5372
})
5473
}
5574
}
75+
76+
var (
77+
testDistNames = []struct {
78+
n string
79+
d bool
80+
}{
81+
{
82+
n: "name",
83+
d: false,
84+
},
85+
{
86+
n: "",
87+
d: false,
88+
},
89+
{
90+
n: "dist_name",
91+
d: true,
92+
},
93+
{
94+
n: "distname",
95+
d: false,
96+
},
97+
}
98+
distPrefixes = []string{"dist_"}
99+
)
100+
101+
func TestSendDist(t *testing.T) {
102+
for _, test := range testDistNames {
103+
t.Run(test.n, func(t *testing.T) {
104+
a := sendDist(test.n, distPrefixes)
105+
if a != test.d {
106+
t.Error("distribution name detection incorrect:")
107+
t.Log("expected:", test.d)
108+
t.Log("found: ", a)
109+
}
110+
})
111+
}
112+
}

datadog/metric.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ import (
88
)
99

1010
// MetricType is an enumeration providing symbols to represent the different
11-
// metric types upported by datadog.
11+
// metric types supported by datadog.
1212
type MetricType string
1313

1414
// Metric Types.
1515
const (
16-
Counter MetricType = "c"
17-
Gauge MetricType = "g"
18-
Histogram MetricType = "h"
19-
Unknown MetricType = "?"
16+
Counter MetricType = "c"
17+
Gauge MetricType = "g"
18+
Histogram MetricType = "h"
19+
Distribution MetricType = "d"
20+
Unknown MetricType = "?"
2021
)
2122

2223
// The Metric type is a representation of the metrics supported by datadog.

datadog/metric_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,28 @@ var testMetrics = []struct {
101101
},
102102
},
103103

104+
{
105+
s: "song.length:240|d|@0.5\n",
106+
m: Metric{
107+
Type: Distribution,
108+
Name: "song.length",
109+
Value: 240,
110+
Rate: 0.5,
111+
Tags: nil,
112+
},
113+
},
114+
115+
{
116+
s: "users.uniques:1234|d\n",
117+
m: Metric{
118+
Type: Distribution,
119+
Name: "users.uniques",
120+
Value: 1234,
121+
Rate: 1,
122+
Tags: nil,
123+
},
124+
},
125+
104126
{
105127
s: "users.online:1|c|#country:china\n",
106128
m: Metric{

0 commit comments

Comments
 (0)