Merge pull request #18 from prometheus/refactor/decoder-inclusion
Extract core Prometheus value decoders.
This commit is contained in:
commit
d36afbbd8d
|
@ -1,4 +1,7 @@
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.1
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- make -f Makefile.TRAVIS
|
- make -f Makefile.TRAVIS
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright 2013 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 decoding decodes Prometheus clients' data streams for consumers.
|
||||||
|
package decoding
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessorForRequestHeader interprets a HTTP request header to determine
|
||||||
|
// what Processor should be used for the given input. If no acceptable
|
||||||
|
// Processor can be found, an error is returned.
|
||||||
|
func ProcessorForRequestHeader(header http.Header) (Processor, error) {
|
||||||
|
if header == nil {
|
||||||
|
return nil, fmt.Errorf("Received illegal and nil header.")
|
||||||
|
}
|
||||||
|
|
||||||
|
mediatype, params, err := mime.ParseMediaType(header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Invalid Content-Type header %q: %s", header.Get("Content-Type"), err)
|
||||||
|
}
|
||||||
|
if mediatype != "application/json" {
|
||||||
|
return nil, fmt.Errorf("Unsupported media type %q, expected %q", mediatype, "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
var prometheusApiVersion string
|
||||||
|
|
||||||
|
if params["schema"] == "prometheus/telemetry" && params["version"] != "" {
|
||||||
|
prometheusApiVersion = params["version"]
|
||||||
|
} else {
|
||||||
|
prometheusApiVersion = header.Get("X-Prometheus-API-Version")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch prometheusApiVersion {
|
||||||
|
case "0.0.2":
|
||||||
|
return Processor002, nil
|
||||||
|
case "0.0.1":
|
||||||
|
return Processor001, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unrecognized API version %s", prometheusApiVersion)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testDiscriminatorHttpHeader(t tester) {
|
||||||
|
var scenarios = []struct {
|
||||||
|
input map[string]string
|
||||||
|
output Processor
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
output: nil,
|
||||||
|
err: fmt.Errorf("Received illegal and nil header."),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: map[string]string{"Content-Type": "application/json", "X-Prometheus-API-Version": "0.0.0"},
|
||||||
|
output: nil,
|
||||||
|
err: fmt.Errorf("Unrecognized API version 0.0.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: map[string]string{"Content-Type": "application/json", "X-Prometheus-API-Version": "0.0.1"},
|
||||||
|
output: Processor001,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: map[string]string{"Content-Type": `application/json; schema="prometheus/telemetry"; version=0.0.0`},
|
||||||
|
output: nil,
|
||||||
|
err: fmt.Errorf("Unrecognized API version 0.0.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: map[string]string{"Content-Type": `application/json; schema="prometheus/telemetry"; version=0.0.1`},
|
||||||
|
output: Processor001,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: map[string]string{"Content-Type": `application/json; schema="prometheus/telemetry"; version=0.0.2`},
|
||||||
|
output: Processor002,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, scenario := range scenarios {
|
||||||
|
var header http.Header
|
||||||
|
|
||||||
|
if len(scenario.input) > 0 {
|
||||||
|
header = http.Header{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range scenario.input {
|
||||||
|
header.Add(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual, err := ProcessorForRequestHeader(header)
|
||||||
|
|
||||||
|
if scenario.err != err {
|
||||||
|
if scenario.err != nil && err != nil {
|
||||||
|
if scenario.err.Error() != err.Error() {
|
||||||
|
t.Errorf("%d. expected %s, got %s", i, scenario.err, err)
|
||||||
|
}
|
||||||
|
} else if scenario.err != nil || err != nil {
|
||||||
|
t.Errorf("%d. expected %s, got %s", i, scenario.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scenario.output != actual {
|
||||||
|
t.Errorf("%d. expected %s, got %s", i, scenario.output, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscriminatorHttpHeader(t *testing.T) {
|
||||||
|
testDiscriminatorHttpHeader(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDiscriminatorHttpHeader(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
testDiscriminatorHttpHeader(b)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"baseLabels": {
|
||||||
|
"name": "rpc_calls_total",
|
||||||
|
"job": "batch_job"
|
||||||
|
},
|
||||||
|
"docstring": "RPC calls.",
|
||||||
|
"metric": {
|
||||||
|
"type": "counter",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"service": "zed"
|
||||||
|
},
|
||||||
|
"value": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"service": "bar"
|
||||||
|
},
|
||||||
|
"value": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"service": "foo"
|
||||||
|
},
|
||||||
|
"value": 25
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"baseLabels": {
|
||||||
|
"name": "rpc_latency_microseconds"
|
||||||
|
},
|
||||||
|
"docstring": "RPC latency.",
|
||||||
|
"metric": {
|
||||||
|
"type": "histogram",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"service": "foo"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"0.010000": 15.890724674774395,
|
||||||
|
"0.050000": 15.890724674774395,
|
||||||
|
"0.500000": 84.63044031436561,
|
||||||
|
"0.900000": 160.21100853053224,
|
||||||
|
"0.990000": 172.49828748957728
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"service": "zed"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"0.010000": 0.0459814091918713,
|
||||||
|
"0.050000": 0.0459814091918713,
|
||||||
|
"0.500000": 0.6120456642749681,
|
||||||
|
"0.900000": 1.355915069887731,
|
||||||
|
"0.990000": 1.772733213161236
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"service": "bar"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"0.010000": 78.48563317257356,
|
||||||
|
"0.050000": 78.48563317257356,
|
||||||
|
"0.500000": 97.31798360385088,
|
||||||
|
"0.900000": 109.89202084295582,
|
||||||
|
"0.990000": 109.99626121011262
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
type tester interface {
|
||||||
|
Error(args ...interface{})
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
Fatal(args ...interface{})
|
||||||
|
Fatalf(format string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorEqual compares Go errors for equality.
|
||||||
|
func errorEqual(left, right error) bool {
|
||||||
|
if left == right {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if left != nil && right != nil {
|
||||||
|
if left.Error() == right.Error() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,287 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sample struct {
|
||||||
|
Metric Metric
|
||||||
|
Value SampleValue
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sample) Equal(o *Sample) bool {
|
||||||
|
if !s.Metric.Equal(o.Metric) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !s.Timestamp.Equal(o.Timestamp) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !s.Value.Equal(o.Value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type Samples []*Sample
|
||||||
|
|
||||||
|
func (s Samples) Len() int {
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Samples) Less(i, j int) bool {
|
||||||
|
switch {
|
||||||
|
case s[i].Metric.Before(s[j].Metric):
|
||||||
|
return true
|
||||||
|
case s[i].Timestamp.Before(s[j].Timestamp):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Samples) Swap(i, j int) {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet
|
||||||
|
// may be fully-qualified down to the point where it may resolve to a single
|
||||||
|
// Metric in the data store or not. All operations that occur within the realm
|
||||||
|
// of a LabelSet can emit a vector of Metric entities to which the LabelSet may
|
||||||
|
// match.
|
||||||
|
type LabelSet map[LabelName]LabelValue
|
||||||
|
|
||||||
|
// Helper function to non-destructively merge two label sets.
|
||||||
|
func (l LabelSet) Merge(other LabelSet) LabelSet {
|
||||||
|
result := make(LabelSet, len(l))
|
||||||
|
|
||||||
|
for k, v := range l {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range other {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LabelSet) String() string {
|
||||||
|
labelStrings := make([]string, 0, len(l))
|
||||||
|
for label, value := range l {
|
||||||
|
labelStrings = append(labelStrings, fmt.Sprintf("%s='%s'", label, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(labelStrings)
|
||||||
|
|
||||||
|
return fmt.Sprintf("{%s}", strings.Join(labelStrings, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// A LabelValue is an associated value for a LabelName.
|
||||||
|
type LabelValue string
|
||||||
|
|
||||||
|
// A Metric is similar to a LabelSet, but the key difference is that a Metric is
|
||||||
|
// a singleton and refers to one and only one stream of samples.
|
||||||
|
type Metric map[LabelName]LabelValue
|
||||||
|
|
||||||
|
func (m Metric) Equal(o Metric) bool {
|
||||||
|
lFingerprint := &Fingerprint{}
|
||||||
|
rFingerprint := &Fingerprint{}
|
||||||
|
|
||||||
|
m.WriteFingerprint(lFingerprint)
|
||||||
|
o.WriteFingerprint(rFingerprint)
|
||||||
|
|
||||||
|
return lFingerprint.Equal(rFingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Metric) Before(o Metric) bool {
|
||||||
|
lFingerprint := &Fingerprint{}
|
||||||
|
rFingerprint := &Fingerprint{}
|
||||||
|
|
||||||
|
m.WriteFingerprint(lFingerprint)
|
||||||
|
o.WriteFingerprint(rFingerprint)
|
||||||
|
|
||||||
|
return m.Before(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Metric) WriteFingerprint(f *Fingerprint) {
|
||||||
|
labelLength := len(m)
|
||||||
|
labelNames := make([]string, 0, labelLength)
|
||||||
|
|
||||||
|
for labelName := range m {
|
||||||
|
labelNames = append(labelNames, string(labelName))
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(labelNames)
|
||||||
|
|
||||||
|
summer := fnv.New64a()
|
||||||
|
firstCharacterOfFirstLabelName := ""
|
||||||
|
lastCharacterOfLastLabelValue := ""
|
||||||
|
labelMatterLength := 0
|
||||||
|
|
||||||
|
for i, labelName := range labelNames {
|
||||||
|
labelValue := m[LabelName(labelName)]
|
||||||
|
labelNameLength := len(labelName)
|
||||||
|
labelValueLength := len(labelValue)
|
||||||
|
labelMatterLength += labelNameLength + labelValueLength
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
firstCharacterOfFirstLabelName = labelName[0:1]
|
||||||
|
}
|
||||||
|
if i == labelLength-1 {
|
||||||
|
lastCharacterOfLastLabelValue = string(labelValue[labelValueLength-1 : labelValueLength])
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(summer, "%s%s%s", labelName, `"`, labelValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.firstCharacterOfFirstLabelName = firstCharacterOfFirstLabelName
|
||||||
|
f.hash = binary.LittleEndian.Uint64(summer.Sum(nil))
|
||||||
|
f.labelMatterLength = uint(labelMatterLength % 10)
|
||||||
|
f.lastCharacterOfLastLabelValue = lastCharacterOfLastLabelValue
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// A SampleValue is a representation of a value for a given sample at a given
|
||||||
|
// time.
|
||||||
|
type SampleValue float64
|
||||||
|
|
||||||
|
func (v SampleValue) Equal(o SampleValue) bool {
|
||||||
|
return v == o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v SampleValue) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(fmt.Sprintf(`"%f"`, v)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v SampleValue) String() string {
|
||||||
|
return fmt.Sprint(float64(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fingerprint provides a hash-capable representation of a Metric.
|
||||||
|
type Fingerprint struct {
|
||||||
|
// A hashed representation of the underyling entity. For our purposes, FNV-1A
|
||||||
|
// 64-bit is used.
|
||||||
|
hash uint64
|
||||||
|
firstCharacterOfFirstLabelName string
|
||||||
|
labelMatterLength uint
|
||||||
|
lastCharacterOfLastLabelValue string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fingerprint) String() string {
|
||||||
|
return f.ToRowKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transforms the Fingerprint into a database row key.
|
||||||
|
func (f *Fingerprint) ToRowKey() string {
|
||||||
|
return strings.Join([]string{fmt.Sprintf("%020d", f.hash), f.firstCharacterOfFirstLabelName, fmt.Sprint(f.labelMatterLength), f.lastCharacterOfLastLabelValue}, "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fingerprint) Hash() uint64 {
|
||||||
|
return f.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fingerprint) FirstCharacterOfFirstLabelName() string {
|
||||||
|
return f.firstCharacterOfFirstLabelName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fingerprint) LabelMatterLength() uint {
|
||||||
|
return f.labelMatterLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fingerprint) LastCharacterOfLastLabelValue() string {
|
||||||
|
return f.lastCharacterOfLastLabelValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fingerprint) Less(o *Fingerprint) bool {
|
||||||
|
if f.hash < o.hash {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if f.hash > o.hash {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.firstCharacterOfFirstLabelName < o.firstCharacterOfFirstLabelName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if f.firstCharacterOfFirstLabelName > o.firstCharacterOfFirstLabelName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.labelMatterLength < o.labelMatterLength {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if f.labelMatterLength > o.labelMatterLength {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.lastCharacterOfLastLabelValue < o.lastCharacterOfLastLabelValue {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if f.lastCharacterOfLastLabelValue > o.lastCharacterOfLastLabelValue {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fingerprint) Equal(o *Fingerprint) bool {
|
||||||
|
if f.Hash() != o.Hash() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.FirstCharacterOfFirstLabelName() != o.FirstCharacterOfFirstLabelName() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.LabelMatterLength() != o.LabelMatterLength() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.LastCharacterOfLastLabelValue() == o.LastCharacterOfLastLabelValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// A basic interface only useful in testing contexts for dispensing the time
|
||||||
|
// in a controlled manner.
|
||||||
|
type instantProvider interface {
|
||||||
|
// The current instant.
|
||||||
|
Now() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time is a simple means for fluently wrapping around standard Go timekeeping
|
||||||
|
// mechanisms to enhance testability without compromising code readability.
|
||||||
|
//
|
||||||
|
// It is sufficient for use on bare initialization. A provider should be
|
||||||
|
// set only for test contexts. When not provided, it emits the current
|
||||||
|
// system time.
|
||||||
|
type Time struct {
|
||||||
|
// The underlying means through which time is provided, if supplied.
|
||||||
|
Provider instantProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit the current instant.
|
||||||
|
func (t *Time) Now() time.Time {
|
||||||
|
if t.Provider == nil {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Provider.Now()
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFingerprintComparison(t *testing.T) {
|
||||||
|
fingerprints := []*Fingerprint{
|
||||||
|
{
|
||||||
|
hash: 0,
|
||||||
|
firstCharacterOfFirstLabelName: "b",
|
||||||
|
labelMatterLength: 1,
|
||||||
|
lastCharacterOfLastLabelValue: "b",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: 1,
|
||||||
|
firstCharacterOfFirstLabelName: "a",
|
||||||
|
labelMatterLength: 0,
|
||||||
|
lastCharacterOfLastLabelValue: "a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: 1,
|
||||||
|
firstCharacterOfFirstLabelName: "a",
|
||||||
|
labelMatterLength: 1000,
|
||||||
|
lastCharacterOfLastLabelValue: "b",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: 1,
|
||||||
|
firstCharacterOfFirstLabelName: "b",
|
||||||
|
labelMatterLength: 0,
|
||||||
|
lastCharacterOfLastLabelValue: "a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: 1,
|
||||||
|
firstCharacterOfFirstLabelName: "b",
|
||||||
|
labelMatterLength: 1,
|
||||||
|
lastCharacterOfLastLabelValue: "a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: 1,
|
||||||
|
firstCharacterOfFirstLabelName: "b",
|
||||||
|
labelMatterLength: 1,
|
||||||
|
lastCharacterOfLastLabelValue: "b",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := range fingerprints {
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fingerprints[i-1].Less(fingerprints[i]) {
|
||||||
|
t.Errorf("%d expected %s < %s", i, fingerprints[i-1], fingerprints[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFingerprinting(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
fps := []*Fingerprint{
|
||||||
|
{
|
||||||
|
hash: 0,
|
||||||
|
firstCharacterOfFirstLabelName: "a",
|
||||||
|
labelMatterLength: 2,
|
||||||
|
lastCharacterOfLastLabelValue: "z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: 0,
|
||||||
|
firstCharacterOfFirstLabelName: "a",
|
||||||
|
labelMatterLength: 2,
|
||||||
|
lastCharacterOfLastLabelValue: "z",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
fps[0].Less(fps[1])
|
||||||
|
}
|
||||||
|
b.Logf("N: %v", b.N)
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
var pre runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&pre)
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
fps[0].Less(fps[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
var post runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&post)
|
||||||
|
|
||||||
|
b.Logf("allocs: %d items: ", post.TotalAlloc-pre.TotalAlloc)
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// The label name prefix to prepend if a synthetic label is already present
|
||||||
|
// in the exported metrics.
|
||||||
|
ExporterLabelPrefix = LabelName("exporter_")
|
||||||
|
|
||||||
|
// The label name indicating the metric name of a timeseries.
|
||||||
|
MetricNameLabel = LabelName("name")
|
||||||
|
|
||||||
|
// The label name indicating the job from which a timeseries was scraped.
|
||||||
|
JobLabel = LabelName("job")
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 time.Time
|
||||||
|
|
||||||
|
// BaseLabels are labels that are accumulated onto each sample, if any.
|
||||||
|
BaseLabels LabelSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processor is responsible for decoding the actual message responses from
|
||||||
|
// stream into a format that can be consumed with the end result written
|
||||||
|
// to the results channel.
|
||||||
|
type Processor interface {
|
||||||
|
// ProcessSingle treats the input as a single self-contained message body and
|
||||||
|
// transforms it accordingly. It has no support for streaming.
|
||||||
|
ProcessSingle(in io.Reader, out chan<- *Result, o *ProcessOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert map[string]string into LabelSet.
|
||||||
|
//
|
||||||
|
// NOTE: This should be deleted when support for go 1.0.3 is removed; 1.1 is
|
||||||
|
// smart enough to unmarshal JSON objects into LabelSet directly.
|
||||||
|
func labelSet(labels map[string]string) LabelSet {
|
||||||
|
labelset := make(LabelSet, len(labels))
|
||||||
|
|
||||||
|
for k, v := range labels {
|
||||||
|
labelset[LabelName(k)] = LabelValue(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to merge a target's base labels ontop of the labels of an
|
||||||
|
// exported sample. If a label is already defined in the exported sample, we
|
||||||
|
// assume that we are scraping an intermediate exporter and attach
|
||||||
|
// "exporter_"-prefixes to Prometheus' own base labels.
|
||||||
|
func mergeTargetLabels(entityLabels, targetLabels LabelSet) LabelSet {
|
||||||
|
if targetLabels == nil {
|
||||||
|
targetLabels = LabelSet{}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := LabelSet{}
|
||||||
|
|
||||||
|
for label, value := range entityLabels {
|
||||||
|
result[label] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
for label, labelValue := range targetLabels {
|
||||||
|
if _, exists := result[label]; exists {
|
||||||
|
result[ExporterLabelPrefix+label] = labelValue
|
||||||
|
} else {
|
||||||
|
result[label] = labelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result encapsulates the outcome from processing samples from a source.
|
||||||
|
type Result struct {
|
||||||
|
Err error
|
||||||
|
Samples Samples
|
||||||
|
}
|
||||||
|
|
||||||
|
// A LabelName is a key for a LabelSet or Metric. It has a value associated
|
||||||
|
// therewith.
|
||||||
|
type LabelName string
|
|
@ -0,0 +1,134 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseLabels001 = "baseLabels"
|
||||||
|
counter001 = "counter"
|
||||||
|
docstring001 = "docstring"
|
||||||
|
gauge001 = "gauge"
|
||||||
|
histogram001 = "histogram"
|
||||||
|
labels001 = "labels"
|
||||||
|
metric001 = "metric"
|
||||||
|
type001 = "type"
|
||||||
|
value001 = "value"
|
||||||
|
percentile001 = "percentile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Processor002 is responsible for decoding payloads from protocol version
|
||||||
|
// 0.0.1.
|
||||||
|
var Processor001 Processor = &processor001{}
|
||||||
|
|
||||||
|
// processor001 is responsible for handling API version 0.0.1.
|
||||||
|
type processor001 struct {
|
||||||
|
time Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// entity001 represents a the JSON structure that 0.0.1 uses.
|
||||||
|
type entity001 []struct {
|
||||||
|
BaseLabels map[string]string `json:"baseLabels"`
|
||||||
|
Docstring string `json:"docstring"`
|
||||||
|
Metric struct {
|
||||||
|
MetricType string `json:"type"`
|
||||||
|
Value []struct {
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
} `json:"value"`
|
||||||
|
} `json:"metric"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor001) ProcessSingle(in io.Reader, out chan<- *Result, o *ProcessOptions) error {
|
||||||
|
// TODO(matt): Replace with plain-jane JSON unmarshalling.
|
||||||
|
buffer, err := ioutil.ReadAll(in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entities := entity001{}
|
||||||
|
if err = json.Unmarshal(buffer, &entities); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(matt): This outer loop is a great basis for parallelization.
|
||||||
|
pendingSamples := Samples{}
|
||||||
|
for _, entity := range entities {
|
||||||
|
for _, value := range entity.Metric.Value {
|
||||||
|
entityLabels := labelSet(entity.BaseLabels).Merge(labelSet(value.Labels))
|
||||||
|
labels := mergeTargetLabels(entityLabels, o.BaseLabels)
|
||||||
|
|
||||||
|
switch entity.Metric.MetricType {
|
||||||
|
case gauge001, counter001:
|
||||||
|
sampleValue, ok := value.Value.(float64)
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("Could not convert value from %s %s to float64.", entity, value)
|
||||||
|
out <- &Result{Err: err}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingSamples = append(pendingSamples, &Sample{
|
||||||
|
Metric: Metric(labels),
|
||||||
|
Timestamp: o.Timestamp,
|
||||||
|
Value: SampleValue(sampleValue),
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case histogram001:
|
||||||
|
sampleValue, ok := value.Value.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("Could not convert value from %q to a map[string]interface{}.", value.Value)
|
||||||
|
out <- &Result{Err: err}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for percentile, percentileValue := range sampleValue {
|
||||||
|
individualValue, ok := percentileValue.(float64)
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("Could not convert value from %q to a float64.", percentileValue)
|
||||||
|
out <- &Result{Err: err}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
childMetric := make(map[LabelName]LabelValue, len(labels)+1)
|
||||||
|
|
||||||
|
for k, v := range labels {
|
||||||
|
childMetric[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
childMetric[LabelName(percentile001)] = LabelValue(percentile)
|
||||||
|
|
||||||
|
pendingSamples = append(pendingSamples, &Sample{
|
||||||
|
Metric: Metric(childMetric),
|
||||||
|
Timestamp: o.Timestamp,
|
||||||
|
Value: SampleValue(individualValue),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(pendingSamples) > 0 {
|
||||||
|
out <- &Result{Samples: pendingSamples}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,216 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testProcessor001Process(t tester) {
|
||||||
|
var scenarios = []struct {
|
||||||
|
in string
|
||||||
|
baseLabels LabelSet
|
||||||
|
out Samples
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
in: "empty.json",
|
||||||
|
err: fmt.Errorf("unexpected end of JSON input"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "test0_0_1-0_0_2.json",
|
||||||
|
baseLabels: LabelSet{
|
||||||
|
JobLabel: "batch_exporter",
|
||||||
|
},
|
||||||
|
out: Samples{
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"service": "zed", MetricNameLabel: "rpc_calls_total", "job": "batch_job", "exporter_job": "batch_exporter"},
|
||||||
|
Value: 25,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"service": "bar", MetricNameLabel: "rpc_calls_total", "job": "batch_job", "exporter_job": "batch_exporter"},
|
||||||
|
Value: 25,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"service": "foo", MetricNameLabel: "rpc_calls_total", "job": "batch_job", "exporter_job": "batch_exporter"},
|
||||||
|
Value: 25,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.010000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 0.0459814091918713,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.010000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 78.48563317257356,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.010000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 15.890724674774395,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 0.0459814091918713,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 78.48563317257356,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 15.890724674774395,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 0.6120456642749681,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 97.31798360385088,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 84.63044031436561,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.900000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 1.355915069887731,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.900000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 109.89202084295582,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.900000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 160.21100853053224,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 1.772733213161236,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 109.99626121011262,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 172.49828748957728,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, scenario := range scenarios {
|
||||||
|
inputChannel := make(chan *Result, 1024)
|
||||||
|
|
||||||
|
defer close(inputChannel)
|
||||||
|
|
||||||
|
reader, err := os.Open(path.Join("fixtures", scenario.in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d. couldn't open scenario input file %s: %s", i, scenario.in, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &ProcessOptions{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
BaseLabels: scenario.baseLabels,
|
||||||
|
}
|
||||||
|
err = Processor001.ProcessSingle(reader, inputChannel, options)
|
||||||
|
if !errorEqual(scenario.err, err) {
|
||||||
|
t.Errorf("%d. expected err of %s, got %s", i, scenario.err, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
delivered := Samples{}
|
||||||
|
|
||||||
|
for len(inputChannel) != 0 {
|
||||||
|
result := <-inputChannel
|
||||||
|
if result.Err != nil {
|
||||||
|
t.Fatalf("%d. expected no error, got: %s", i, result.Err)
|
||||||
|
}
|
||||||
|
delivered = append(delivered, result.Samples...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(delivered) != len(scenario.out) {
|
||||||
|
t.Errorf("%d. expected output length of %d, got %d", i, len(scenario.out), len(delivered))
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedElements := list.New()
|
||||||
|
for _, j := range scenario.out {
|
||||||
|
expectedElements.PushBack(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := 0; j < len(delivered); j++ {
|
||||||
|
actual := delivered[j]
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for element := expectedElements.Front(); element != nil && found == false; element = element.Next() {
|
||||||
|
candidate := element.Value.(*Sample)
|
||||||
|
|
||||||
|
if candidate.Value != actual.Value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidate.Metric) != len(actual.Metric) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
labelsMatch := false
|
||||||
|
|
||||||
|
for key, value := range candidate.Metric {
|
||||||
|
actualValue, ok := actual.Metric[key]
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if actualValue == value {
|
||||||
|
labelsMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !labelsMatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: Test time.
|
||||||
|
found = true
|
||||||
|
expectedElements.Remove(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("%d.%d. expected to find %s among candidate, absent", i, j, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessor001Process(t *testing.T) {
|
||||||
|
testProcessor001Process(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkProcessor001Process(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
testProcessor001Process(b)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Processor002 is responsible for decoding payloads from protocol version
|
||||||
|
// 0.0.2.
|
||||||
|
var Processor002 = &processor002{}
|
||||||
|
|
||||||
|
type histogram002 struct {
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
Values map[string]SampleValue `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type counter002 struct {
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
Value SampleValue `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type processor002 struct{}
|
||||||
|
|
||||||
|
func (p *processor002) ProcessSingle(in io.Reader, out chan<- *Result, o *ProcessOptions) error {
|
||||||
|
// Processor for telemetry schema version 0.0.2.
|
||||||
|
// container for telemetry data
|
||||||
|
var entities []struct {
|
||||||
|
BaseLabels map[string]string `json:"baseLabels"`
|
||||||
|
Docstring string `json:"docstring"`
|
||||||
|
Metric struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Values json.RawMessage `json:"value"`
|
||||||
|
} `json:"metric"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(in).Decode(&entities); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingSamples := Samples{}
|
||||||
|
for _, entity := range entities {
|
||||||
|
switch entity.Metric.Type {
|
||||||
|
case "counter", "gauge":
|
||||||
|
var values []counter002
|
||||||
|
|
||||||
|
if err := json.Unmarshal(entity.Metric.Values, &values); err != nil {
|
||||||
|
out <- &Result{
|
||||||
|
Err: fmt.Errorf("Could not extract %s value: %s", entity.Metric.Type, err),
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, counter := range values {
|
||||||
|
entityLabels := labelSet(entity.BaseLabels).Merge(labelSet(counter.Labels))
|
||||||
|
labels := mergeTargetLabels(entityLabels, o.BaseLabels)
|
||||||
|
|
||||||
|
pendingSamples = append(pendingSamples, &Sample{
|
||||||
|
Metric: Metric(labels),
|
||||||
|
Timestamp: o.Timestamp,
|
||||||
|
Value: counter.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case "histogram":
|
||||||
|
var values []histogram002
|
||||||
|
|
||||||
|
if err := json.Unmarshal(entity.Metric.Values, &values); err != nil {
|
||||||
|
out <- &Result{
|
||||||
|
Err: fmt.Errorf("Could not extract %s value: %s", entity.Metric.Type, err),
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, histogram := range values {
|
||||||
|
for percentile, value := range histogram.Values {
|
||||||
|
entityLabels := labelSet(entity.BaseLabels).Merge(labelSet(histogram.Labels))
|
||||||
|
entityLabels[LabelName("percentile")] = LabelValue(percentile)
|
||||||
|
labels := mergeTargetLabels(entityLabels, o.BaseLabels)
|
||||||
|
|
||||||
|
pendingSamples = append(pendingSamples, &Sample{
|
||||||
|
Metric: Metric(labels),
|
||||||
|
Timestamp: o.Timestamp,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
out <- &Result{
|
||||||
|
Err: fmt.Errorf("Unknown metric type %q", entity.Metric.Type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pendingSamples) > 0 {
|
||||||
|
out <- &Result{Samples: pendingSamples}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
// Copyright 2013 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 decoding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testProcessor002Process(t tester) {
|
||||||
|
var scenarios = []struct {
|
||||||
|
in string
|
||||||
|
baseLabels LabelSet
|
||||||
|
out Samples
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
in: "empty.json",
|
||||||
|
err: fmt.Errorf("EOF"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "test0_0_1-0_0_2.json",
|
||||||
|
baseLabels: LabelSet{
|
||||||
|
JobLabel: "batch_exporter",
|
||||||
|
},
|
||||||
|
out: Samples{
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"service": "zed", MetricNameLabel: "rpc_calls_total", "job": "batch_job", "exporter_job": "batch_exporter"},
|
||||||
|
Value: 25,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"service": "bar", MetricNameLabel: "rpc_calls_total", "job": "batch_job", "exporter_job": "batch_exporter"},
|
||||||
|
Value: 25,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"service": "foo", MetricNameLabel: "rpc_calls_total", "job": "batch_job", "exporter_job": "batch_exporter"},
|
||||||
|
Value: 25,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.010000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 0.0459814091918713,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.010000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 78.48563317257356,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.010000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 15.890724674774395,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 0.0459814091918713,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 78.48563317257356,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 15.890724674774395,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 0.6120456642749681,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 97.31798360385088,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 84.63044031436561,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.900000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 1.355915069887731,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.900000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 109.89202084295582,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.900000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 160.21100853053224,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"},
|
||||||
|
Value: 1.772733213161236,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
|
||||||
|
Metric: Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"},
|
||||||
|
Value: 109.99626121011262,
|
||||||
|
},
|
||||||
|
&Sample{
|
||||||
|
Metric: Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"},
|
||||||
|
Value: 172.49828748957728,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, scenario := range scenarios {
|
||||||
|
inputChannel := make(chan *Result, 1024)
|
||||||
|
|
||||||
|
defer close(inputChannel)
|
||||||
|
|
||||||
|
reader, err := os.Open(path.Join("fixtures", scenario.in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d. couldn't open scenario input file %s: %s", i, scenario.in, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &ProcessOptions{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
BaseLabels: scenario.baseLabels,
|
||||||
|
}
|
||||||
|
err = Processor002.ProcessSingle(reader, inputChannel, options)
|
||||||
|
if !errorEqual(scenario.err, err) {
|
||||||
|
t.Errorf("%d. expected err of %s, got %s", i, scenario.err, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
delivered := Samples{}
|
||||||
|
|
||||||
|
for len(inputChannel) != 0 {
|
||||||
|
result := <-inputChannel
|
||||||
|
if result.Err != nil {
|
||||||
|
t.Fatalf("%d. expected no error, got: %s", i, result.Err)
|
||||||
|
}
|
||||||
|
delivered = append(delivered, result.Samples...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(delivered) != len(scenario.out) {
|
||||||
|
t.Errorf("%d. expected output length of %d, got %d", i, len(scenario.out), len(delivered))
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedElements := list.New()
|
||||||
|
for _, j := range scenario.out {
|
||||||
|
expectedElements.PushBack(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := 0; j < len(delivered); j++ {
|
||||||
|
actual := delivered[j]
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for element := expectedElements.Front(); element != nil && found == false; element = element.Next() {
|
||||||
|
candidate := element.Value.(*Sample)
|
||||||
|
|
||||||
|
if candidate.Value != actual.Value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(candidate.Metric) != len(actual.Metric) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
labelsMatch := false
|
||||||
|
|
||||||
|
for key, value := range candidate.Metric {
|
||||||
|
actualValue, ok := actual.Metric[key]
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if actualValue == value {
|
||||||
|
labelsMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !labelsMatch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: Test time.
|
||||||
|
found = true
|
||||||
|
expectedElements.Remove(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("%d.%d. expected to find %s among candidate, absent", i, j, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessor002Process(t *testing.T) {
|
||||||
|
testProcessor002Process(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkProcessor002Process(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
|
||||||
|
pre := runtime.MemStats{}
|
||||||
|
runtime.ReadMemStats(&pre)
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
testProcessor002Process(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
post := runtime.MemStats{}
|
||||||
|
runtime.ReadMemStats(&post)
|
||||||
|
|
||||||
|
allocated := post.TotalAlloc - pre.TotalAlloc
|
||||||
|
|
||||||
|
b.Logf("Allocated %d at %f per cycle with %d cycles.", allocated, float64(allocated)/float64(b.N), b.N)
|
||||||
|
}
|
|
@ -4,7 +4,8 @@
|
||||||
// Use of this source code is governed by a BSD-style license that can be found
|
// Use of this source code is governed by a BSD-style license that can be found
|
||||||
// in the LICENSE file.
|
// in the LICENSE file.
|
||||||
|
|
||||||
// Prometheus' client side metric primitives and telemetry exposition framework.
|
// Package prometheus provides client side metric primitives and a telemetry
|
||||||
|
// exposition framework.
|
||||||
//
|
//
|
||||||
// This package provides both metric primitives and tools for their exposition
|
// This package provides both metric primitives and tools for their exposition
|
||||||
// to the Prometheus time series collection and computation framework.
|
// to the Prometheus time series collection and computation framework.
|
||||||
|
|
Loading…
Reference in New Issue