-
Notifications
You must be signed in to change notification settings - Fork 220
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
feat(metrics): add basic metrics type support (#789)
Co-authored-by: Michi Hoffmann <[email protected]>
- zerolog/v0.31.1
- zerolog/v0.31.0
- zerolog/v0.30.0
- v0.31.1
- v0.31.0
- v0.30.0
- v0.29.1
- v0.29.0
- v0.28.1
- v0.28.0
- slog/v0.31.1
- slog/v0.31.0
- slog/v0.30.0
- otel/v0.31.1
- otel/v0.31.0
- otel/v0.30.0
- otel/v0.29.1
- otel/v0.29.0
- otel/v0.28.1
- otel/v0.28.0
- negroni/v0.31.1
- negroni/v0.31.0
- logrus/v0.31.1
- logrus/v0.31.0
- iris/v0.31.1
- iris/v0.31.0
- gin/v0.31.1
- gin/v0.31.0
- fiber/v0.31.1
- fiber/v0.31.0
- fasthttp/v0.31.1
- fasthttp/v0.31.0
- echo/v0.31.1
- echo/v0.31.0
Showing
6 changed files
with
726 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,427 @@ | ||
package sentry | ||
|
||
import ( | ||
"fmt" | ||
"hash/crc32" | ||
"math" | ||
"regexp" | ||
"sort" | ||
"strings" | ||
) | ||
|
||
type ( | ||
NumberOrString interface { | ||
int | string | ||
} | ||
|
||
void struct{} | ||
) | ||
|
||
var ( | ||
member void | ||
keyRegex = regexp.MustCompile(`[^a-zA-Z0-9_/.-]+`) | ||
valueRegex = regexp.MustCompile(`[^\w\d\s_:/@\.{}\[\]$-]+`) | ||
unitRegex = regexp.MustCompile(`[^a-z]+`) | ||
) | ||
|
||
type MetricUnit struct { | ||
unit string | ||
} | ||
|
||
func (m MetricUnit) toString() string { | ||
return m.unit | ||
} | ||
|
||
func NanoSecond() MetricUnit { | ||
return MetricUnit{ | ||
"nanosecond", | ||
} | ||
} | ||
|
||
func MicroSecond() MetricUnit { | ||
return MetricUnit{ | ||
"microsecond", | ||
} | ||
} | ||
|
||
func MilliSecond() MetricUnit { | ||
return MetricUnit{ | ||
"millisecond", | ||
} | ||
} | ||
|
||
func Second() MetricUnit { | ||
return MetricUnit{ | ||
"second", | ||
} | ||
} | ||
|
||
func Minute() MetricUnit { | ||
return MetricUnit{ | ||
"minute", | ||
} | ||
} | ||
|
||
func Hour() MetricUnit { | ||
return MetricUnit{ | ||
"hour", | ||
} | ||
} | ||
|
||
func Day() MetricUnit { | ||
return MetricUnit{ | ||
"day", | ||
} | ||
} | ||
|
||
func Week() MetricUnit { | ||
return MetricUnit{ | ||
"week", | ||
} | ||
} | ||
|
||
func Bit() MetricUnit { | ||
return MetricUnit{ | ||
"bit", | ||
} | ||
} | ||
|
||
func Byte() MetricUnit { | ||
return MetricUnit{ | ||
"byte", | ||
} | ||
} | ||
|
||
func KiloByte() MetricUnit { | ||
return MetricUnit{ | ||
"kilobyte", | ||
} | ||
} | ||
|
||
func KibiByte() MetricUnit { | ||
return MetricUnit{ | ||
"kibibyte", | ||
} | ||
} | ||
|
||
func MegaByte() MetricUnit { | ||
return MetricUnit{ | ||
"megabyte", | ||
} | ||
} | ||
|
||
func MebiByte() MetricUnit { | ||
return MetricUnit{ | ||
"mebibyte", | ||
} | ||
} | ||
|
||
func GigaByte() MetricUnit { | ||
return MetricUnit{ | ||
"gigabyte", | ||
} | ||
} | ||
|
||
func GibiByte() MetricUnit { | ||
return MetricUnit{ | ||
"gibibyte", | ||
} | ||
} | ||
|
||
func TeraByte() MetricUnit { | ||
return MetricUnit{ | ||
"terabyte", | ||
} | ||
} | ||
|
||
func TebiByte() MetricUnit { | ||
return MetricUnit{ | ||
"tebibyte", | ||
} | ||
} | ||
|
||
func PetaByte() MetricUnit { | ||
return MetricUnit{ | ||
"petabyte", | ||
} | ||
} | ||
|
||
func PebiByte() MetricUnit { | ||
return MetricUnit{ | ||
"pebibyte", | ||
} | ||
} | ||
|
||
func ExaByte() MetricUnit { | ||
return MetricUnit{ | ||
"exabyte", | ||
} | ||
} | ||
|
||
func ExbiByte() MetricUnit { | ||
return MetricUnit{ | ||
"exbibyte", | ||
} | ||
} | ||
|
||
func Ratio() MetricUnit { | ||
return MetricUnit{ | ||
"ratio", | ||
} | ||
} | ||
|
||
func Percent() MetricUnit { | ||
return MetricUnit{ | ||
"percent", | ||
} | ||
} | ||
|
||
func CustomUnit(unit string) MetricUnit { | ||
return MetricUnit{ | ||
unitRegex.ReplaceAllString(unit, ""), | ||
} | ||
} | ||
|
||
type Metric interface { | ||
GetType() string | ||
GetTags() map[string]string | ||
GetKey() string | ||
GetUnit() string | ||
GetTimestamp() int64 | ||
SerializeValue() string | ||
SerializeTags() string | ||
} | ||
|
||
type abstractMetric struct { | ||
key string | ||
unit MetricUnit | ||
tags map[string]string | ||
// A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC). | ||
timestamp int64 | ||
} | ||
|
||
func (am abstractMetric) GetTags() map[string]string { | ||
return am.tags | ||
} | ||
|
||
func (am abstractMetric) GetKey() string { | ||
return am.key | ||
} | ||
|
||
func (am abstractMetric) GetUnit() string { | ||
return am.unit.toString() | ||
} | ||
|
||
func (am abstractMetric) GetTimestamp() int64 { | ||
return am.timestamp | ||
} | ||
|
||
func (am abstractMetric) SerializeTags() string { | ||
var sb strings.Builder | ||
|
||
values := make([]string, 0, len(am.tags)) | ||
for k := range am.tags { | ||
values = append(values, k) | ||
} | ||
sortSlice(values) | ||
|
||
for _, key := range values { | ||
val := sanitizeValue(am.tags[key]) | ||
key = sanitizeKey(key) | ||
sb.WriteString(fmt.Sprintf("%s:%s,", key, val)) | ||
} | ||
s := sb.String() | ||
if len(s) > 0 { | ||
s = s[:len(s)-1] | ||
} | ||
return s | ||
} | ||
|
||
// Counter Metric. | ||
type CounterMetric struct { | ||
value float64 | ||
abstractMetric | ||
} | ||
|
||
func (c *CounterMetric) Add(value float64) { | ||
c.value += value | ||
} | ||
|
||
func (c CounterMetric) GetType() string { | ||
return "c" | ||
} | ||
|
||
func (c CounterMetric) SerializeValue() string { | ||
return fmt.Sprintf(":%v", c.value) | ||
} | ||
|
||
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC). | ||
func NewCounterMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) CounterMetric { | ||
am := abstractMetric{ | ||
key, | ||
unit, | ||
tags, | ||
timestamp, | ||
} | ||
|
||
return CounterMetric{ | ||
value, | ||
am, | ||
} | ||
} | ||
|
||
// Distribution Metric. | ||
type DistributionMetric struct { | ||
values []float64 | ||
abstractMetric | ||
} | ||
|
||
func (d *DistributionMetric) Add(value float64) { | ||
d.values = append(d.values, value) | ||
} | ||
|
||
func (d DistributionMetric) GetType() string { | ||
return "d" | ||
} | ||
|
||
func (d DistributionMetric) SerializeValue() string { | ||
var sb strings.Builder | ||
for _, el := range d.values { | ||
sb.WriteString(fmt.Sprintf(":%v", el)) | ||
} | ||
return sb.String() | ||
} | ||
|
||
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC). | ||
func NewDistributionMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) DistributionMetric { | ||
am := abstractMetric{ | ||
key, | ||
unit, | ||
tags, | ||
timestamp, | ||
} | ||
|
||
return DistributionMetric{ | ||
[]float64{value}, | ||
am, | ||
} | ||
} | ||
|
||
// Gauge Metric. | ||
type GaugeMetric struct { | ||
last float64 | ||
min float64 | ||
max float64 | ||
sum float64 | ||
count float64 | ||
abstractMetric | ||
} | ||
|
||
func (g *GaugeMetric) Add(value float64) { | ||
g.last = value | ||
g.min = math.Min(g.min, value) | ||
g.max = math.Max(g.max, value) | ||
g.sum += value | ||
g.count++ | ||
} | ||
|
||
func (g GaugeMetric) GetType() string { | ||
return "g" | ||
} | ||
|
||
func (g GaugeMetric) SerializeValue() string { | ||
return fmt.Sprintf(":%v:%v:%v:%v:%v", g.last, g.min, g.max, g.sum, g.count) | ||
} | ||
|
||
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC). | ||
func NewGaugeMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) GaugeMetric { | ||
am := abstractMetric{ | ||
key, | ||
unit, | ||
tags, | ||
timestamp, | ||
} | ||
|
||
return GaugeMetric{ | ||
value, // last | ||
value, // min | ||
value, // max | ||
value, // sum | ||
value, // count | ||
am, | ||
} | ||
} | ||
|
||
// Set Metric. | ||
type SetMetric[T NumberOrString] struct { | ||
values map[T]void | ||
abstractMetric | ||
} | ||
|
||
func (s *SetMetric[T]) Add(value T) { | ||
s.values[value] = member | ||
} | ||
|
||
func (s SetMetric[T]) GetType() string { | ||
return "s" | ||
} | ||
|
||
func (s SetMetric[T]) SerializeValue() string { | ||
_hash := func(s string) uint32 { | ||
return crc32.ChecksumIEEE([]byte(s)) | ||
} | ||
|
||
values := make([]T, 0, len(s.values)) | ||
for k := range s.values { | ||
values = append(values, k) | ||
} | ||
sortSlice(values) | ||
|
||
var sb strings.Builder | ||
for _, el := range values { | ||
switch any(el).(type) { | ||
case int: | ||
sb.WriteString(fmt.Sprintf(":%v", el)) | ||
case string: | ||
s := fmt.Sprintf("%v", el) | ||
sb.WriteString(fmt.Sprintf(":%d", _hash(s))) | ||
} | ||
} | ||
|
||
return sb.String() | ||
} | ||
|
||
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC). | ||
func NewSetMetric[T NumberOrString](key string, unit MetricUnit, tags map[string]string, timestamp int64, value T) SetMetric[T] { | ||
am := abstractMetric{ | ||
key, | ||
unit, | ||
tags, | ||
timestamp, | ||
} | ||
|
||
return SetMetric[T]{ | ||
map[T]void{ | ||
value: member, | ||
}, | ||
am, | ||
} | ||
} | ||
|
||
func sanitizeKey(s string) string { | ||
return keyRegex.ReplaceAllString(s, "_") | ||
} | ||
|
||
func sanitizeValue(s string) string { | ||
return valueRegex.ReplaceAllString(s, "") | ||
} | ||
|
||
type Ordered interface { | ||
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string | ||
} | ||
|
||
func sortSlice[T Ordered](s []T) { | ||
sort.Slice(s, func(i, j int) bool { | ||
return s[i] < s[j] | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
package sentry | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
) | ||
|
||
func TestSanitizeKey(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
key string | ||
want string | ||
}{ | ||
{ | ||
name: "allowed characters", | ||
key: "test.metric-1", | ||
want: "test.metric-1", | ||
}, | ||
{ | ||
name: "forbidden characters", | ||
key: "@test.me^tri'@c-1{}[]", | ||
want: "_test.me_tri_c-1_", | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
if diff := cmp.Diff(sanitizeKey(test.key), test.want); diff != "" { | ||
t.Errorf("Context mismatch (-want +got):\n%s", diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestSanitizeValue(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
value string | ||
want string | ||
}{ | ||
{ | ||
name: "allowed characters", | ||
value: "test.metric-1", | ||
want: "test.metric-1", | ||
}, | ||
{ | ||
name: "forbidden characters", | ||
value: "@test.me^tri'+@c-1{}[]", | ||
want: "@test.metri@c-1{}[]", | ||
}, | ||
{ | ||
name: "allow empty character", | ||
value: "@route /test", | ||
want: "@route /test", | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
if diff := cmp.Diff(sanitizeValue(test.value), test.want); diff != "" { | ||
t.Errorf("Context mismatch (-want +got):\n%s", diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestSerializeTags(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
metric abstractMetric | ||
want string | ||
}{ | ||
{ | ||
name: "normal tags", | ||
metric: abstractMetric{ | ||
tags: map[string]string{"tag1": "val1", "tag2": "val2"}, | ||
}, | ||
want: "tag1:val1,tag2:val2", | ||
}, | ||
{ | ||
name: "empty tags", | ||
metric: abstractMetric{ | ||
tags: map[string]string{}, | ||
}, | ||
want: "", | ||
}, | ||
{ | ||
name: "un-sanitized tags", | ||
metric: abstractMetric{ | ||
tags: map[string]string{"@env": "pro+d", "vers^^ion": `\release@`}, | ||
}, | ||
want: "_env:prod,vers_ion:release@", | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
if diff := cmp.Diff(test.metric.SerializeTags(), test.want); diff != "" { | ||
t.Errorf("Context mismatch (-want +got):\n%s", diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestSerializeValue(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
metric Metric | ||
want string | ||
}{ | ||
{ | ||
name: "distribution metric", | ||
metric: DistributionMetric{ | ||
values: []float64{2.0, 4.0, 3.0, 6.0}, | ||
}, | ||
want: ":2:4:3:6", | ||
}, | ||
{ | ||
name: "gauge metric", | ||
metric: GaugeMetric{ | ||
last: 1.0, | ||
min: 1.0, | ||
max: 1.0, | ||
sum: 1.0, | ||
count: 1.0, | ||
}, | ||
want: ":1:1:1:1:1", | ||
}, | ||
{ | ||
name: "set metric with strings", | ||
metric: SetMetric[string]{ | ||
values: map[string]void{"Hello": member, "World": member}, | ||
}, | ||
want: ":4157704578:4223024711", | ||
}, | ||
{ | ||
name: "set metric with integers", | ||
metric: SetMetric[int]{ | ||
values: map[int]void{1: member, 2: member}, | ||
}, | ||
want: ":1:2", | ||
}, | ||
{ | ||
name: "counter metric", | ||
metric: CounterMetric{ | ||
value: 2.0, | ||
}, | ||
want: ":2", | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
if diff := cmp.Diff(test.metric.SerializeValue(), test.want); diff != "" { | ||
t.Errorf("Context mismatch (-want +got):\n%s", diff) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters