-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Refactor metric parsing * Labels parsing * Update carbonapi * Use ParsedMetric.RawMetric for MatchedMetric.Metric * Not include name to labels * Refactor metric parsing tests * ScanBytes -> NewBytesScanner, ByteSliceSplitScanner -> BytesScanner * Move unsafe to unsafe.go
- Loading branch information
Showing
9 changed files
with
370 additions
and
688 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package filter | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
|
||
"github.com/moira-alert/moira" | ||
) | ||
|
||
//ParsedMetric represents a result of ParseMetric. | ||
type ParsedMetric struct { | ||
Metric string | ||
Name string | ||
Labels map[string]string | ||
Value float64 | ||
Timestamp int64 | ||
} | ||
|
||
// ParseMetric parses metric from string | ||
// supported format: "<metricString> <valueFloat64> <timestampInt64>" | ||
func ParseMetric(input []byte) (*ParsedMetric, error) { | ||
if !isPrintableASCII(input) { | ||
return nil, fmt.Errorf("non-ascii or non-printable chars in metric name: '%s'", input) | ||
} | ||
|
||
var metricBytes, valueBytes, timestampBytes []byte | ||
inputScanner := moira.NewBytesScanner(input, ' ') | ||
if !inputScanner.HasNext() { | ||
return nil, fmt.Errorf("too few space-separated items: '%s'", input) | ||
} | ||
metricBytes = inputScanner.Next() | ||
if !inputScanner.HasNext() { | ||
return nil, fmt.Errorf("too few space-separated items: '%s'", input) | ||
} | ||
valueBytes = inputScanner.Next() | ||
if !inputScanner.HasNext() { | ||
return nil, fmt.Errorf("too few space-separated items: '%s'", input) | ||
} | ||
timestampBytes = inputScanner.Next() | ||
if inputScanner.HasNext() { | ||
return nil, fmt.Errorf("too many space-separated items: '%s'", input) | ||
} | ||
|
||
name, labels, err := parseNameAndLabels(metricBytes) | ||
if err != nil { | ||
return nil, fmt.Errorf("cannot parse metric: '%s' (%s)", input, err) | ||
} | ||
|
||
value, err := parseFloat(valueBytes) | ||
if err != nil { | ||
return nil, fmt.Errorf("cannot parse value: '%s' (%s)", input, err) | ||
} | ||
|
||
timestamp, err := parseFloat(timestampBytes) | ||
if err != nil { | ||
return nil, fmt.Errorf("cannot parse timestamp: '%s' (%s)", input, err) | ||
} | ||
|
||
parsedMetric := &ParsedMetric{ | ||
moira.UnsafeBytesToString(metricBytes), | ||
name, | ||
labels, | ||
value, | ||
int64(timestamp), | ||
} | ||
return parsedMetric, nil | ||
} | ||
|
||
func parseNameAndLabels(metricBytes []byte) (string, map[string]string, error) { | ||
metricBytesScanner := moira.NewBytesScanner(metricBytes, ';') | ||
if !metricBytesScanner.HasNext() { | ||
return "", nil, fmt.Errorf("too few colon-separated items: '%s'", metricBytes) | ||
} | ||
nameBytes := metricBytesScanner.Next() | ||
if len(nameBytes) == 0 { | ||
return "", nil, fmt.Errorf("empty metric name: '%s'", metricBytes) | ||
} | ||
name := moira.UnsafeBytesToString(nameBytes) | ||
labels := make(map[string]string) | ||
for metricBytesScanner.HasNext() { | ||
labelBytes := metricBytesScanner.Next() | ||
labelBytesScanner := moira.NewBytesScanner(labelBytes, '=') | ||
|
||
var labelNameBytes, labelValueBytes []byte | ||
if !labelBytesScanner.HasNext() { | ||
return "", nil, fmt.Errorf("too few equal-separated items: '%s'", labelBytes) | ||
} | ||
labelNameBytes = labelBytesScanner.Next() | ||
if !labelBytesScanner.HasNext() { | ||
return "", nil, fmt.Errorf("too few equal-separated items: '%s'", labelBytes) | ||
} | ||
labelValueBytes = labelBytesScanner.Next() | ||
if labelBytesScanner.HasNext() { | ||
return "", nil, fmt.Errorf("too many equal-separated items: '%s'", labelBytes) | ||
} | ||
if len(labelNameBytes) == 0 { | ||
return "", nil, fmt.Errorf("empty label name: '%s'", labelBytes) | ||
} | ||
labelName := moira.UnsafeBytesToString(labelNameBytes) | ||
labelValue := moira.UnsafeBytesToString(labelValueBytes) | ||
labels[labelName] = labelValue | ||
} | ||
return name, labels, nil | ||
} | ||
|
||
func parseFloat(input []byte) (float64, error) { | ||
return strconv.ParseFloat(moira.UnsafeBytesToString(input), 64) | ||
} | ||
|
||
func isPrintableASCII(b []byte) bool { | ||
for i := 0; i < len(b); i++ { | ||
if b[i] < 0x20 || b[i] > 0x7E { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} |
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,103 @@ | ||
package filter | ||
|
||
import ( | ||
"math/rand" | ||
"strconv" | ||
"testing" | ||
|
||
. "github.com/smartystreets/goconvey/convey" | ||
) | ||
|
||
func TestParseMetric(t *testing.T) { | ||
type ValidMetricCase struct { | ||
input string | ||
metric string | ||
name string | ||
labels map[string]string | ||
value float64 | ||
timestamp int64 | ||
} | ||
|
||
Convey("Given invalid metric strings, should return errors", t, func() { | ||
invalidMetrics := []string{ | ||
"Invalid.value 12g5 1234567890", | ||
"No.value.two.spaces 1234567890", | ||
"No.timestamp.space.in.the.end 12 ", | ||
"No.timestamp 12", | ||
" 12 1234567890", | ||
"Non-ascii.こんにちは 12 1234567890", | ||
"Non-printable.\000 12 1234567890", | ||
"", | ||
"\n", | ||
"Too.many.parts 1 2 3 4 12 1234567890", | ||
"Space.in.the.end 12 1234567890 ", | ||
" Space.in.the.beginning 12 1234567890", | ||
"\tNon-printable.in.the.beginning 12 1234567890", | ||
"\rNon-printable.in.the.beginning 12 1234567890", | ||
"Newline.in.the.end 12 1234567890\n", | ||
"Newline.in.the.end 12 1234567890\r", | ||
"Newline.in.the.end 12 1234567890\r\n", | ||
";empty.name.but.with.label= 1 2", | ||
"no.labels.but.delimiter.in.the.end; 1 2", | ||
"empty.label.name;= 1 2", | ||
} | ||
|
||
for _, invalidMetric := range invalidMetrics { | ||
_, err := ParseMetric([]byte(invalidMetric)) | ||
So(err, ShouldBeError) | ||
} | ||
}) | ||
|
||
Convey("Given valid metric strings, should return parsed values", t, func() { | ||
validMetrics := []ValidMetricCase{ | ||
{"One.two.three 123 1234567890", "One.two.three", "One.two.three", map[string]string{}, 123, 1234567890}, | ||
{"One.two.three 1.23e2 1234567890", "One.two.three", "One.two.three", map[string]string{}, 123, 1234567890}, | ||
{"One.two.three -123 1234567890", "One.two.three", "One.two.three", map[string]string{}, -123, 1234567890}, | ||
{"One.two.three +123 1234567890", "One.two.three", "One.two.three", map[string]string{}, 123, 1234567890}, | ||
{"One.two.three 123. 1234567890", "One.two.three", "One.two.three", map[string]string{}, 123, 1234567890}, | ||
{"One.two.three 123.0 1234567890", "One.two.three", "One.two.three", map[string]string{}, 123, 1234567890}, | ||
{"One.two.three .123 1234567890", "One.two.three", "One.two.three", map[string]string{}, 0.123, 1234567890}, | ||
{"One.two.three;four=five 123 1234567890", "One.two.three;four=five", "One.two.three", map[string]string{"four": "five"}, 123, 1234567890}, | ||
{"One.two.three;four= 123 1234567890", "One.two.three;four=", "One.two.three", map[string]string{"four": ""}, 123, 1234567890}, | ||
{"One.two.three;four=five;six=seven 123 1234567890", "One.two.three;four=five;six=seven", "One.two.three", map[string]string{"four": "five", "six": "seven"}, 123, 1234567890}, | ||
} | ||
|
||
for _, validMetric := range validMetrics { | ||
parsedMetric, err := ParseMetric([]byte(validMetric.input)) | ||
So(err, ShouldBeEmpty) | ||
So(parsedMetric.Metric, ShouldEqual, validMetric.metric) | ||
So(parsedMetric.Name, ShouldEqual, validMetric.name) | ||
So(parsedMetric.Labels, ShouldResemble, validMetric.labels) | ||
So(parsedMetric.Value, ShouldEqual, validMetric.value) | ||
So(parsedMetric.Timestamp, ShouldEqual, validMetric.timestamp) | ||
} | ||
}) | ||
|
||
Convey("Given valid metric strings with float64 timestamp, should return parsed values", t, func() { | ||
var testTimestamp int64 = 1234567890 | ||
|
||
// Create and test n metrics with float64 timestamp with fractional part of length n (n=19) | ||
// | ||
// For example: | ||
// | ||
// [n=1] One.two.three 123 1234567890.6 | ||
// [n=2] One.two.three 123 1234567890.94 | ||
// [n=3] One.two.three 123 1234567890.665 | ||
// [n=4] One.two.three 123 1234567890.4377 | ||
// ... | ||
// [n=19] One.two.three 123 1234567890.6790847778320312500 | ||
|
||
for i := 1; i < 20; i++ { | ||
rawTimestamp := strconv.FormatFloat(float64(testTimestamp)+rand.Float64(), 'f', i, 64) | ||
rawMetric := "One.two.three 123 " + rawTimestamp | ||
validMetric := ValidMetricCase{rawMetric, "One.two.three", "One.two.three", map[string]string{}, 123, testTimestamp} | ||
parsedMetric, err := ParseMetric([]byte(validMetric.input)) | ||
So(err, ShouldBeEmpty) | ||
So(parsedMetric.Metric, ShouldResemble, validMetric.metric) | ||
So(parsedMetric.Name, ShouldResemble, validMetric.name) | ||
So(parsedMetric.Labels, ShouldResemble, validMetric.labels) | ||
So(parsedMetric.Value, ShouldEqual, validMetric.value) | ||
So(parsedMetric.Timestamp, ShouldEqual, validMetric.timestamp) | ||
} | ||
}) | ||
} |
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
Oops, something went wrong.