diff --git a/extraction/discriminator.go b/extraction/discriminator.go index b443582..f0356f8 100644 --- a/extraction/discriminator.go +++ b/extraction/discriminator.go @@ -40,7 +40,16 @@ func ProcessorForRequestHeader(header http.Header) (Processor, error) { return nil, fmt.Errorf("Unsupported Encoding %s", params["encoding"]) } return MetricFamilyProcessor, nil - + case "text/plain": + switch params["version"] { + case "0.0.4": + return Processor004, nil + case "": + // Fallback: most recent version. + return Processor004, nil + default: + return nil, fmt.Errorf("Unrecognized API version %s", params["version"]) + } case "application/json": var prometheusApiVersion string diff --git a/extraction/discriminator_test.go b/extraction/discriminator_test.go index b200ae5..5c11eef 100644 --- a/extraction/discriminator_test.go +++ b/extraction/discriminator_test.go @@ -71,6 +71,21 @@ func testDiscriminatorHttpHeader(t test.Tester) { output: nil, err: fmt.Errorf("Unsupported Encoding illegal"), }, + { + input: map[string]string{"Content-Type": `text/plain; version=0.0.4`}, + output: Processor004, + err: nil, + }, + { + input: map[string]string{"Content-Type": `text/plain`}, + output: Processor004, + err: nil, + }, + { + input: map[string]string{"Content-Type": `text/plain; version=0.0.3`}, + output: nil, + err: fmt.Errorf("Unrecognized API version 0.0.3"), + }, } for i, scenario := range scenarios { diff --git a/extraction/metricfamilyprocessor.go b/extraction/metricfamilyprocessor.go index 3f8a5c6..ff6501f 100644 --- a/extraction/metricfamilyprocessor.go +++ b/extraction/metricfamilyprocessor.go @@ -31,10 +31,10 @@ type metricFamilyProcessor struct{} // // See http://godoc.org/github.com/matttproud/golang_protobuf_extensions/ext for // more details. -var MetricFamilyProcessor = new(metricFamilyProcessor) +var MetricFamilyProcessor = &metricFamilyProcessor{} func (m *metricFamilyProcessor) ProcessSingle(i io.Reader, out Ingester, o *ProcessOptions) error { - family := new(dto.MetricFamily) + family := &dto.MetricFamily{} for { family.Reset() @@ -43,30 +43,33 @@ func (m *metricFamilyProcessor) ProcessSingle(i io.Reader, out Ingester, o *Proc if err == io.EOF { return nil } - return err } - - switch *family.Type { - case dto.MetricType_COUNTER: - if err := extractCounter(out, o, family); err != nil { - return err - } - case dto.MetricType_GAUGE: - if err := extractGauge(out, o, family); err != nil { - return err - } - case dto.MetricType_SUMMARY: - if err := extractSummary(out, o, family); err != nil { - return err - } - case dto.MetricType_UNTYPED: - if err := extractUntyped(out, o, family); err != nil { - return err - } + if err := extractMetricFamily(out, o, family); err != nil { + return err } } +} +func extractMetricFamily(out Ingester, o *ProcessOptions, family *dto.MetricFamily) error { + switch *family.Type { + case dto.MetricType_COUNTER: + if err := extractCounter(out, o, family); err != nil { + return err + } + case dto.MetricType_GAUGE: + if err := extractGauge(out, o, family); err != nil { + return err + } + case dto.MetricType_SUMMARY: + if err := extractSummary(out, o, family); err != nil { + return err + } + case dto.MetricType_UNTYPED: + if err := extractUntyped(out, o, family); err != nil { + return err + } + } return nil } diff --git a/extraction/processor.go b/extraction/processor.go index 2d9b98c..f7e0c29 100644 --- a/extraction/processor.go +++ b/extraction/processor.go @@ -23,7 +23,8 @@ import ( // ProcessOptions dictates how the interpreted stream should be rendered for // consumption. type ProcessOptions struct { - // Timestamp is added to each value interpreted from the stream. + // Timestamp is added to each value from the stream that has no explicit + // timestamp set. Timestamp model.Timestamp } diff --git a/extraction/textprocessor.go b/extraction/textprocessor.go new file mode 100644 index 0000000..bdf6c81 --- /dev/null +++ b/extraction/textprocessor.go @@ -0,0 +1,40 @@ +// Copyright 2014 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package extraction + +import ( + "io" + + "github.com/prometheus/client_golang/text" +) + +type processor004 struct{} + +// Processor004 s responsible for decoding payloads from the text based variety +// of protocol version 0.0.4. +var Processor004 = &processor004{} + +func (t *processor004) ProcessSingle(i io.Reader, out Ingester, o *ProcessOptions) error { + var parser text.Parser + metricFamilies, err := parser.TextToMetricFamilies(i) + if err != nil { + return err + } + for _, metricFamily := range metricFamilies { + if err := extractMetricFamily(out, o, metricFamily); err != nil { + return err + } + } + return nil +} diff --git a/extraction/textprocessor_test.go b/extraction/textprocessor_test.go new file mode 100644 index 0000000..a0cdbfd --- /dev/null +++ b/extraction/textprocessor_test.go @@ -0,0 +1,104 @@ +// Copyright 2014 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package extraction + +import ( + "sort" + "strings" + "testing" + + "github.com/prometheus/client_golang/model" +) + +var ( + ts = model.Now() + in = ` +# Only a quite simple scenario with two metric families. +# More complicated tests of the parser itself can be found in the text package. +# TYPE mf2 counter +mf2 3 +mf1{label="value1"} -3.14 123456 +mf1{label="value2"} 42 +mf2 4 +` + out = map[model.LabelValue]*Result{ + "mf1": { + Samples: model.Samples{ + &model.Sample{ + Metric: model.Metric{model.MetricNameLabel: "mf1", "label": "value1"}, + Value: -3.14, + Timestamp: 123, + }, + &model.Sample{ + Metric: model.Metric{model.MetricNameLabel: "mf1", "label": "value2"}, + Value: 42, + Timestamp: ts, + }, + }, + }, + "mf2": { + Samples: model.Samples{ + &model.Sample{ + Metric: model.Metric{model.MetricNameLabel: "mf2"}, + Value: 3, + Timestamp: ts, + }, + &model.Sample{ + Metric: model.Metric{model.MetricNameLabel: "mf2"}, + Value: 4, + Timestamp: ts, + }, + }, + }, + } +) + +type testIngester struct { + results []*Result +} + +func (i *testIngester) Ingest(r *Result) error { + i.results = append(i.results, r) + return nil +} + +func TestTextProcessor(t *testing.T) { + var ingester testIngester + i := strings.NewReader(in) + o := &ProcessOptions{ + Timestamp: ts, + } + + err := Processor004.ProcessSingle(i, &ingester, o) + if err != nil { + t.Fatal(err) + } + if expected, got := len(out), len(ingester.results); expected != got { + t.Fatalf("Expected length %d, got %d", expected, got) + } + for _, r := range ingester.results { + expected, ok := out[r.Samples[0].Metric[model.MetricNameLabel]] + if !ok { + t.Fatalf( + "Unexpected metric name %q", + r.Samples[0].Metric[model.MetricNameLabel], + ) + } + sort.Sort(expected.Samples) + sort.Sort(r.Samples) + if !expected.equal(r) { + t.Errorf("expected %s, got %s", expected, r) + } + } +}