From 10dae5d10826507ec42dd0fb3d476cad668f3e9c Mon Sep 17 00:00:00 2001 From: "Matt T. Proud" Date: Tue, 11 Jun 2013 11:43:39 +0200 Subject: [PATCH] Include relevant server model artifacts. This commit introduces all relevant server-side artifacts such that the Result streams can be used by external parties for one-off tools and such. This will ultimately better enable us to support additional wireformats with much more ease. --- .../decoding => extraction}/discriminator.go | 2 +- .../discriminator_test.go | 6 +- extraction/extraction.go | 15 + .../fixtures/empty.json | 0 .../fixtures/test0_0_1-0_0_2.json | 0 .../decoding => extraction}/processor.go | 55 +++- .../decoding => extraction}/processor0_0_1.go | 24 +- .../processor0_0_1_test.go | 93 +++--- .../decoding => extraction}/processor0_0_2.go | 22 +- .../processor0_0_2_test.go | 93 +++--- model/fingerprinting.go | 163 ++++++++++ model/labelname.go | 44 +++ model/labelname_test.go | 57 ++++ model/labelset.go | 53 ++++ model/labelvalue.go | 35 +++ model/labelvalue_test.go | 57 ++++ model/metric.go | 38 +++ model/metric_test.go | 81 +++++ .../decoding/decoding.go => model/model.go | 4 +- {prometheus/decoding => model}/model_test.go | 66 ++-- model/sample.go | 59 ++++ model/samplevalue.go | 34 +++ prometheus/decoding/model.go | 287 ------------------ .../decoding/helpers_test.go => test/test.go | 9 +- 24 files changed, 842 insertions(+), 455 deletions(-) rename {prometheus/decoding => extraction}/discriminator.go (98%) rename {prometheus/decoding => extraction}/discriminator_test.go (95%) create mode 100644 extraction/extraction.go rename {prometheus/decoding => extraction}/fixtures/empty.json (100%) rename {prometheus/decoding => extraction}/fixtures/test0_0_1-0_0_2.json (100%) rename {prometheus/decoding => extraction}/processor.go (66%) rename {prometheus/decoding => extraction}/processor0_0_1.go (84%) rename {prometheus/decoding => extraction}/processor0_0_1_test.go (52%) rename {prometheus/decoding => extraction}/processor0_0_2.go (83%) rename {prometheus/decoding => extraction}/processor0_0_2_test.go (54%) create mode 100644 model/fingerprinting.go create mode 100644 model/labelname.go create mode 100644 model/labelname_test.go create mode 100644 model/labelset.go create mode 100644 model/labelvalue.go create mode 100644 model/labelvalue_test.go create mode 100644 model/metric.go create mode 100644 model/metric_test.go rename prometheus/decoding/decoding.go => model/model.go (86%) rename {prometheus/decoding => model}/model_test.go (56%) create mode 100644 model/sample.go create mode 100644 model/samplevalue.go delete mode 100644 prometheus/decoding/model.go rename prometheus/decoding/helpers_test.go => test/test.go (82%) diff --git a/prometheus/decoding/discriminator.go b/extraction/discriminator.go similarity index 98% rename from prometheus/decoding/discriminator.go rename to extraction/discriminator.go index ad2e178..458d150 100644 --- a/prometheus/decoding/discriminator.go +++ b/extraction/discriminator.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package decoding +package extraction import ( "fmt" diff --git a/prometheus/decoding/discriminator_test.go b/extraction/discriminator_test.go similarity index 95% rename from prometheus/decoding/discriminator_test.go rename to extraction/discriminator_test.go index 38e43ee..c7a0936 100644 --- a/prometheus/decoding/discriminator_test.go +++ b/extraction/discriminator_test.go @@ -11,15 +11,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package decoding +package extraction import ( "fmt" "net/http" "testing" + + "github.com/prometheus/client_golang/test" ) -func testDiscriminatorHttpHeader(t tester) { +func testDiscriminatorHttpHeader(t test.Tester) { var scenarios = []struct { input map[string]string output Processor diff --git a/extraction/extraction.go b/extraction/extraction.go new file mode 100644 index 0000000..be757ae --- /dev/null +++ b/extraction/extraction.go @@ -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 extraction decodes Prometheus clients' data streams for consumers. +package extraction diff --git a/prometheus/decoding/fixtures/empty.json b/extraction/fixtures/empty.json similarity index 100% rename from prometheus/decoding/fixtures/empty.json rename to extraction/fixtures/empty.json diff --git a/prometheus/decoding/fixtures/test0_0_1-0_0_2.json b/extraction/fixtures/test0_0_1-0_0_2.json similarity index 100% rename from prometheus/decoding/fixtures/test0_0_1-0_0_2.json rename to extraction/fixtures/test0_0_1-0_0_2.json diff --git a/prometheus/decoding/processor.go b/extraction/processor.go similarity index 66% rename from prometheus/decoding/processor.go rename to extraction/processor.go index 6f18bdf..e239abe 100644 --- a/prometheus/decoding/processor.go +++ b/extraction/processor.go @@ -11,23 +11,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -package decoding +package extraction import ( "io" "time" + + "github.com/prometheus/client_golang/model" ) const ( // The label name prefix to prepend if a synthetic label is already present // in the exported metrics. - ExporterLabelPrefix = LabelName("exporter_") + ExporterLabelPrefix model.LabelName = "exporter_" // The label name indicating the metric name of a timeseries. - MetricNameLabel = LabelName("name") + MetricNameLabel = "name" // The label name indicating the job from which a timeseries was scraped. - JobLabel = LabelName("job") + JobLabel = "job" ) // ProcessOptions dictates how the interpreted stream should be rendered for @@ -37,7 +39,7 @@ type ProcessOptions struct { Timestamp time.Time // BaseLabels are labels that are accumulated onto each sample, if any. - BaseLabels LabelSet + BaseLabels model.LabelSet } // Processor is responsible for decoding the actual message responses from @@ -53,11 +55,11 @@ type Processor interface { // // 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)) +func labelSet(labels map[string]string) model.LabelSet { + labelset := make(model.LabelSet, len(labels)) for k, v := range labels { - labelset[LabelName(k)] = LabelValue(v) + labelset[model.LabelName(k)] = model.LabelValue(v) } return labelset @@ -67,12 +69,12 @@ func labelSet(labels map[string]string) LabelSet { // 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 { +func mergeTargetLabels(entityLabels, targetLabels model.LabelSet) model.LabelSet { if targetLabels == nil { - targetLabels = LabelSet{} + targetLabels = model.LabelSet{} } - result := LabelSet{} + result := model.LabelSet{} for label, value := range entityLabels { result[label] = value @@ -91,9 +93,32 @@ func mergeTargetLabels(entityLabels, targetLabels LabelSet) LabelSet { // Result encapsulates the outcome from processing samples from a source. type Result struct { Err error - Samples Samples + Samples model.Samples } -// A LabelName is a key for a LabelSet or Metric. It has a value associated -// therewith. -type LabelName string +// 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 +} + +// Clock 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 clock struct { + // The underlying means through which time is provided, if supplied. + Provider instantProvider +} + +// Emit the current instant. +func (t *clock) Now() time.Time { + if t.Provider == nil { + return time.Now() + } + + return t.Provider.Now() +} diff --git a/prometheus/decoding/processor0_0_1.go b/extraction/processor0_0_1.go similarity index 84% rename from prometheus/decoding/processor0_0_1.go rename to extraction/processor0_0_1.go index a003077..dd53c1f 100644 --- a/prometheus/decoding/processor0_0_1.go +++ b/extraction/processor0_0_1.go @@ -11,13 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package decoding +package extraction import ( "encoding/json" "fmt" "io" "io/ioutil" + + "github.com/prometheus/client_golang/model" ) const ( @@ -39,7 +41,7 @@ var Processor001 Processor = &processor001{} // processor001 is responsible for handling API version 0.0.1. type processor001 struct { - time Time + time clock } // entity001 represents a the JSON structure that 0.0.1 uses. @@ -68,7 +70,7 @@ func (p *processor001) ProcessSingle(in io.Reader, out chan<- *Result, o *Proces } // TODO(matt): This outer loop is a great basis for parallelization. - pendingSamples := Samples{} + pendingSamples := model.Samples{} for _, entity := range entities { for _, value := range entity.Metric.Value { entityLabels := labelSet(entity.BaseLabels).Merge(labelSet(value.Labels)) @@ -83,10 +85,10 @@ func (p *processor001) ProcessSingle(in io.Reader, out chan<- *Result, o *Proces continue } - pendingSamples = append(pendingSamples, &Sample{ - Metric: Metric(labels), + pendingSamples = append(pendingSamples, &model.Sample{ + Metric: model.Metric(labels), Timestamp: o.Timestamp, - Value: SampleValue(sampleValue), + Value: model.SampleValue(sampleValue), }) break @@ -107,18 +109,18 @@ func (p *processor001) ProcessSingle(in io.Reader, out chan<- *Result, o *Proces continue } - childMetric := make(map[LabelName]LabelValue, len(labels)+1) + childMetric := make(map[model.LabelName]model.LabelValue, len(labels)+1) for k, v := range labels { childMetric[k] = v } - childMetric[LabelName(percentile001)] = LabelValue(percentile) + childMetric[model.LabelName(percentile001)] = model.LabelValue(percentile) - pendingSamples = append(pendingSamples, &Sample{ - Metric: Metric(childMetric), + pendingSamples = append(pendingSamples, &model.Sample{ + Metric: model.Metric(childMetric), Timestamp: o.Timestamp, - Value: SampleValue(individualValue), + Value: model.SampleValue(individualValue), }) } diff --git a/prometheus/decoding/processor0_0_1_test.go b/extraction/processor0_0_1_test.go similarity index 52% rename from prometheus/decoding/processor0_0_1_test.go rename to extraction/processor0_0_1_test.go index 5d31f39..b6edd10 100644 --- a/prometheus/decoding/processor0_0_1_test.go +++ b/extraction/processor0_0_1_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package decoding +package extraction import ( "container/list" @@ -20,13 +20,16 @@ import ( "path" "testing" "time" + + "github.com/prometheus/client_golang/test" + "github.com/prometheus/client_golang/model" ) -func testProcessor001Process(t tester) { +func testProcessor001Process(t test.Tester) { var scenarios = []struct { in string - baseLabels LabelSet - out Samples + baseLabels model.LabelSet + out model.Samples err error }{ { @@ -35,85 +38,85 @@ func testProcessor001Process(t tester) { }, { in: "test0_0_1-0_0_2.json", - baseLabels: LabelSet{ + baseLabels: model.LabelSet{ JobLabel: "batch_exporter", }, - out: Samples{ - &Sample{ - Metric: Metric{"service": "zed", MetricNameLabel: "rpc_calls_total", "job": "batch_job", "exporter_job": "batch_exporter"}, + out: model.Samples{ + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.010000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"}, Value: 15.890724674774395, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"}, Value: 78.48563317257356, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"}, Value: 0.6120456642749681, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"}, Value: 84.63044031436561, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.900000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"}, Value: 1.772733213161236, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"}, Value: 172.49828748957728, }, }, @@ -135,12 +138,12 @@ func testProcessor001Process(t tester) { BaseLabels: scenario.baseLabels, } err = Processor001.ProcessSingle(reader, inputChannel, options) - if !errorEqual(scenario.err, err) { + if !test.ErrorEqual(scenario.err, err) { t.Errorf("%d. expected err of %s, got %s", i, scenario.err, err) continue } - delivered := Samples{} + delivered := model.Samples{} for len(inputChannel) != 0 { result := <-inputChannel @@ -166,7 +169,7 @@ func testProcessor001Process(t tester) { found := false for element := expectedElements.Front(); element != nil && found == false; element = element.Next() { - candidate := element.Value.(*Sample) + candidate := element.Value.(*model.Sample) if candidate.Value != actual.Value { continue diff --git a/prometheus/decoding/processor0_0_2.go b/extraction/processor0_0_2.go similarity index 83% rename from prometheus/decoding/processor0_0_2.go rename to extraction/processor0_0_2.go index 840a5f4..42a94e7 100644 --- a/prometheus/decoding/processor0_0_2.go +++ b/extraction/processor0_0_2.go @@ -11,12 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package decoding +package extraction import ( "encoding/json" "fmt" "io" + + "github.com/prometheus/client_golang/model" ) // Processor002 is responsible for decoding payloads from protocol version @@ -24,13 +26,13 @@ import ( var Processor002 = &processor002{} type histogram002 struct { - Labels map[string]string `json:"labels"` - Values map[string]SampleValue `json:"value"` + Labels map[string]string `json:"labels"` + Values map[string]model.SampleValue `json:"value"` } type counter002 struct { Labels map[string]string `json:"labels"` - Value SampleValue `json:"value"` + Value model.SampleValue `json:"value"` } type processor002 struct{} @@ -51,7 +53,7 @@ func (p *processor002) ProcessSingle(in io.Reader, out chan<- *Result, o *Proces return err } - pendingSamples := Samples{} + pendingSamples := model.Samples{} for _, entity := range entities { switch entity.Metric.Type { case "counter", "gauge": @@ -68,8 +70,8 @@ func (p *processor002) ProcessSingle(in io.Reader, out chan<- *Result, o *Proces entityLabels := labelSet(entity.BaseLabels).Merge(labelSet(counter.Labels)) labels := mergeTargetLabels(entityLabels, o.BaseLabels) - pendingSamples = append(pendingSamples, &Sample{ - Metric: Metric(labels), + pendingSamples = append(pendingSamples, &model.Sample{ + Metric: model.Metric(labels), Timestamp: o.Timestamp, Value: counter.Value, }) @@ -88,11 +90,11 @@ func (p *processor002) ProcessSingle(in io.Reader, out chan<- *Result, o *Proces for _, histogram := range values { for percentile, value := range histogram.Values { entityLabels := labelSet(entity.BaseLabels).Merge(labelSet(histogram.Labels)) - entityLabels[LabelName("percentile")] = LabelValue(percentile) + entityLabels[model.LabelName("percentile")] = model.LabelValue(percentile) labels := mergeTargetLabels(entityLabels, o.BaseLabels) - pendingSamples = append(pendingSamples, &Sample{ - Metric: Metric(labels), + pendingSamples = append(pendingSamples, &model.Sample{ + Metric: model.Metric(labels), Timestamp: o.Timestamp, Value: value, }) diff --git a/prometheus/decoding/processor0_0_2_test.go b/extraction/processor0_0_2_test.go similarity index 54% rename from prometheus/decoding/processor0_0_2_test.go rename to extraction/processor0_0_2_test.go index c3aeb74..b7fbaa1 100644 --- a/prometheus/decoding/processor0_0_2_test.go +++ b/extraction/processor0_0_2_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package decoding +package extraction import ( "container/list" @@ -21,13 +21,16 @@ import ( "runtime" "testing" "time" + + "github.com/prometheus/client_golang/test" + "github.com/prometheus/client_golang/model" ) -func testProcessor002Process(t tester) { +func testProcessor002Process(t test.Tester) { var scenarios = []struct { in string - baseLabels LabelSet - out Samples + baseLabels model.LabelSet + out model.Samples err error }{ { @@ -36,85 +39,85 @@ func testProcessor002Process(t tester) { }, { in: "test0_0_1-0_0_2.json", - baseLabels: LabelSet{ + baseLabels: model.LabelSet{ JobLabel: "batch_exporter", }, - out: Samples{ - &Sample{ - Metric: Metric{"service": "zed", MetricNameLabel: "rpc_calls_total", "job": "batch_job", "exporter_job": "batch_exporter"}, + out: model.Samples{ + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.010000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"}, Value: 15.890724674774395, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"}, Value: 78.48563317257356, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.050000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"}, Value: 0.6120456642749681, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.500000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"}, Value: 84.63044031436561, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.900000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "zed", "job": "batch_exporter"}, Value: 1.772733213161236, }, - &Sample{ + &model.Sample{ - Metric: Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "bar", "job": "batch_exporter"}, + Metric: model.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"}, + &model.Sample{ + Metric: model.Metric{"percentile": "0.990000", MetricNameLabel: "rpc_latency_microseconds", "service": "foo", "job": "batch_exporter"}, Value: 172.49828748957728, }, }, @@ -136,12 +139,12 @@ func testProcessor002Process(t tester) { BaseLabels: scenario.baseLabels, } err = Processor002.ProcessSingle(reader, inputChannel, options) - if !errorEqual(scenario.err, err) { + if !test.ErrorEqual(scenario.err, err) { t.Errorf("%d. expected err of %s, got %s", i, scenario.err, err) continue } - delivered := Samples{} + delivered := model.Samples{} for len(inputChannel) != 0 { result := <-inputChannel @@ -167,7 +170,7 @@ func testProcessor002Process(t tester) { found := false for element := expectedElements.Front(); element != nil && found == false; element = element.Next() { - candidate := element.Value.(*Sample) + candidate := element.Value.(*model.Sample) if candidate.Value != actual.Value { continue diff --git a/model/fingerprinting.go b/model/fingerprinting.go new file mode 100644 index 0000000..182b43b --- /dev/null +++ b/model/fingerprinting.go @@ -0,0 +1,163 @@ +// 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 model + +import ( + "encoding/binary" + "fmt" + "hash/fnv" + "sort" + "strconv" + "strings" +) + +// 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 strings.Join([]string{fmt.Sprintf("%020d", f.Hash), f.FirstCharacterOfFirstLabelName, fmt.Sprint(f.LabelMatterLength), 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 +} + +const rowKeyDelimiter = "-" + +// LoadFromString transforms a rowKey into a Fingerprint, resetting any +// previous attributes. +func (f *Fingerprint) LoadFromString(s string) { + components := strings.Split(s, rowKeyDelimiter) + hash, err := strconv.ParseUint(components[0], 10, 64) + if err != nil { + panic(err) + } + labelMatterLength, err := strconv.ParseUint(components[2], 10, 0) + if err != nil { + panic(err) + } + + f.Hash = hash + f.FirstCharacterOfFirstLabelName = components[1] + f.LabelMatterLength = uint(labelMatterLength) + f.LastCharacterOfLastLabelValue = components[3] +} + +const reservedDelimiter = `"` + +// LoadFromMetric decomposes a Metric into this Fingerprint +func (f *Fingerprint) LoadFromMetric(m Metric) { + 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]) + } + + summer.Write([]byte(labelName)) + summer.Write([]byte(reservedDelimiter)) + summer.Write([]byte(labelValue)) + } + + f.FirstCharacterOfFirstLabelName = firstCharacterOfFirstLabelName + f.Hash = binary.LittleEndian.Uint64(summer.Sum(nil)) + f.LabelMatterLength = uint(labelMatterLength % 10) + f.LastCharacterOfLastLabelValue = lastCharacterOfLastLabelValue +} + +// Represents a collection of Fingerprint subject to a given natural sorting +// scheme. +type Fingerprints []*Fingerprint + +func (f Fingerprints) Len() int { + return len(f) +} + +func (f Fingerprints) Less(i, j int) bool { + return f[i].Less(f[j]) +} + +func (f Fingerprints) Swap(i, j int) { + f[i], f[j] = f[j], f[i] +} diff --git a/model/labelname.go b/model/labelname.go new file mode 100644 index 0000000..09eea74 --- /dev/null +++ b/model/labelname.go @@ -0,0 +1,44 @@ +// 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 model + +import ( + "strings" +) + +// A LabelName is a key for a LabelSet or Metric. It has a value associated +// therewith. +type LabelName string + +type LabelNames []LabelName + +func (l LabelNames) Len() int { + return len(l) +} + +func (l LabelNames) Less(i, j int) bool { + return l[i] < l[j] +} + +func (l LabelNames) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +func (l LabelNames) String() string { + labelStrings := make([]string, 0, len(l)) + for _, label := range l { + labelStrings = append(labelStrings, string(label)) + } + return strings.Join(labelStrings, ", ") +} diff --git a/model/labelname_test.go b/model/labelname_test.go new file mode 100644 index 0000000..f2eff05 --- /dev/null +++ b/model/labelname_test.go @@ -0,0 +1,57 @@ +// 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 model + +import ( + "sort" + "testing" + + "github.com/prometheus/client_golang/test" +) + +func testLabelNames(t test.Tester) { + var scenarios = []struct { + in LabelNames + out LabelNames + }{ + { + in: LabelNames{"ZZZ", "zzz"}, + out: LabelNames{"ZZZ", "zzz"}, + }, + { + in: LabelNames{"aaa", "AAA"}, + out: LabelNames{"AAA", "aaa"}, + }, + } + + for i, scenario := range scenarios { + sort.Sort(scenario.in) + + for j, expected := range scenario.out { + if expected != scenario.in[j] { + t.Errorf("%d.%d expected %s, got %s", i, j, expected, scenario.in[j]) + } + } + } +} + +func TestLabelNames(t *testing.T) { + testLabelNames(t) +} + +func BenchmarkLabelNames(b *testing.B) { + for i := 0; i < b.N; i++ { + testLabelNames(b) + } +} diff --git a/model/labelset.go b/model/labelset.go new file mode 100644 index 0000000..de33f57 --- /dev/null +++ b/model/labelset.go @@ -0,0 +1,53 @@ +// 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 model + +import ( + "fmt" + "sort" + "strings" +) + +// 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, ", ")) +} diff --git a/model/labelvalue.go b/model/labelvalue.go new file mode 100644 index 0000000..6a197cd --- /dev/null +++ b/model/labelvalue.go @@ -0,0 +1,35 @@ +// 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 model + +import ( + "sort" +) + +// A LabelValue is an associated value for a LabelName. +type LabelValue string + +type LabelValues []LabelValue + +func (l LabelValues) Len() int { + return len(l) +} + +func (l LabelValues) Less(i, j int) bool { + return sort.StringsAreSorted([]string{string(l[i]), string(l[j])}) +} + +func (l LabelValues) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} diff --git a/model/labelvalue_test.go b/model/labelvalue_test.go new file mode 100644 index 0000000..f9cc627 --- /dev/null +++ b/model/labelvalue_test.go @@ -0,0 +1,57 @@ +// 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 model + +import ( + "sort" + "testing" + + "github.com/prometheus/client_golang/test" +) + +func testLabelValues(t test.Tester) { + var scenarios = []struct { + in LabelValues + out LabelValues + }{ + { + in: LabelValues{"ZZZ", "zzz"}, + out: LabelValues{"ZZZ", "zzz"}, + }, + { + in: LabelValues{"aaa", "AAA"}, + out: LabelValues{"AAA", "aaa"}, + }, + } + + for i, scenario := range scenarios { + sort.Sort(scenario.in) + + for j, expected := range scenario.out { + if expected != scenario.in[j] { + t.Errorf("%d.%d expected %s, got %s", i, j, expected, scenario.in[j]) + } + } + } +} + +func TestLabelValues(t *testing.T) { + testLabelValues(t) +} + +func BenchmarkLabelValues(b *testing.B) { + for i := 0; i < b.N; i++ { + testLabelValues(b) + } +} diff --git a/model/metric.go b/model/metric.go new file mode 100644 index 0000000..4170500 --- /dev/null +++ b/model/metric.go @@ -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 model + +// 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{} + + lFingerprint.LoadFromMetric(m) + rFingerprint.LoadFromMetric(o) + + return lFingerprint.Equal(rFingerprint) +} + +func (m Metric) Before(o Metric) bool { + lFingerprint := &Fingerprint{} + rFingerprint := &Fingerprint{} + + lFingerprint.LoadFromMetric(m) + rFingerprint.LoadFromMetric(o) + + return m.Before(o) +} diff --git a/model/metric_test.go b/model/metric_test.go new file mode 100644 index 0000000..d84b891 --- /dev/null +++ b/model/metric_test.go @@ -0,0 +1,81 @@ +// 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 model + +import ( + "testing" + + "github.com/prometheus/client_golang/test" +) + +func testMetric(t test.Tester) { + var scenarios = []struct { + input map[string]string + hash uint64 + rowkey string + }{ + { + input: map[string]string{}, + rowkey: "02676020557754725067--0-", + hash: 2676020557754725067, + }, + { + input: map[string]string{ + "first_name": "electro", + "occupation": "robot", + "manufacturer": "westinghouse", + }, + rowkey: "04776841610193542734-f-6-t", + hash: 4776841610193542734, + }, + { + input: map[string]string{ + "x": "y", + }, + rowkey: "01306929544689993150-x-2-y", + hash: 1306929544689993150, + }, + } + + for i, scenario := range scenarios { + metric := Metric{} + for key, value := range scenario.input { + metric[LabelName(key)] = LabelValue(value) + } + + expectedRowKey := scenario.rowkey + expectedHash := scenario.hash + fingerprint := &Fingerprint{} + fingerprint.LoadFromMetric(metric) + actualRowKey := fingerprint.String() + actualHash := fingerprint.Hash + + if expectedRowKey != actualRowKey { + t.Errorf("%d. expected %s, got %s", i, expectedRowKey, actualRowKey) + } + if actualHash != expectedHash { + t.Errorf("%d. expected %d, got %d", i, expectedHash, actualHash) + } + } +} + +func TestMetric(t *testing.T) { + testMetric(t) +} + +func BenchmarkMetric(b *testing.B) { + for i := 0; i < b.N; i++ { + testMetric(b) + } +} diff --git a/prometheus/decoding/decoding.go b/model/model.go similarity index 86% rename from prometheus/decoding/decoding.go rename to model/model.go index ef3f9c4..ea8147c 100644 --- a/prometheus/decoding/decoding.go +++ b/model/model.go @@ -11,5 +11,5 @@ // 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 +// Package model contains core representation of Prometheus client primitives. +package model diff --git a/prometheus/decoding/model_test.go b/model/model_test.go similarity index 56% rename from prometheus/decoding/model_test.go rename to model/model_test.go index ab4dc20..e9d77b9 100644 --- a/prometheus/decoding/model_test.go +++ b/model/model_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package decoding +package model import ( "runtime" @@ -21,40 +21,40 @@ import ( func TestFingerprintComparison(t *testing.T) { fingerprints := []*Fingerprint{ { - hash: 0, - firstCharacterOfFirstLabelName: "b", - labelMatterLength: 1, - lastCharacterOfLastLabelValue: "b", + Hash: 0, + FirstCharacterOfFirstLabelName: "b", + LabelMatterLength: 1, + LastCharacterOfLastLabelValue: "b", }, { - hash: 1, - firstCharacterOfFirstLabelName: "a", - labelMatterLength: 0, - lastCharacterOfLastLabelValue: "a", + Hash: 1, + FirstCharacterOfFirstLabelName: "a", + LabelMatterLength: 0, + LastCharacterOfLastLabelValue: "a", }, { - hash: 1, - firstCharacterOfFirstLabelName: "a", - labelMatterLength: 1000, - lastCharacterOfLastLabelValue: "b", + Hash: 1, + FirstCharacterOfFirstLabelName: "a", + LabelMatterLength: 1000, + LastCharacterOfLastLabelValue: "b", }, { - hash: 1, - firstCharacterOfFirstLabelName: "b", - labelMatterLength: 0, - lastCharacterOfLastLabelValue: "a", + Hash: 1, + FirstCharacterOfFirstLabelName: "b", + LabelMatterLength: 0, + LastCharacterOfLastLabelValue: "a", }, { - hash: 1, - firstCharacterOfFirstLabelName: "b", - labelMatterLength: 1, - lastCharacterOfLastLabelValue: "a", + Hash: 1, + FirstCharacterOfFirstLabelName: "b", + LabelMatterLength: 1, + LastCharacterOfLastLabelValue: "a", }, { - hash: 1, - firstCharacterOfFirstLabelName: "b", - labelMatterLength: 1, - lastCharacterOfLastLabelValue: "b", + Hash: 1, + FirstCharacterOfFirstLabelName: "b", + LabelMatterLength: 1, + LastCharacterOfLastLabelValue: "b", }, } for i := range fingerprints { @@ -72,16 +72,16 @@ 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", }, { - hash: 0, - firstCharacterOfFirstLabelName: "a", - labelMatterLength: 2, - lastCharacterOfLastLabelValue: "z", + Hash: 0, + FirstCharacterOfFirstLabelName: "a", + LabelMatterLength: 2, + LastCharacterOfLastLabelValue: "z", }, } for i := 0; i < 10; i++ { diff --git a/model/sample.go b/model/sample.go new file mode 100644 index 0000000..845d5b2 --- /dev/null +++ b/model/sample.go @@ -0,0 +1,59 @@ +// 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 model + +import ( + "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] +} diff --git a/model/samplevalue.go b/model/samplevalue.go new file mode 100644 index 0000000..2557fe5 --- /dev/null +++ b/model/samplevalue.go @@ -0,0 +1,34 @@ +// 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 model + +import ( + "fmt" +) + +// 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)) +} diff --git a/prometheus/decoding/model.go b/prometheus/decoding/model.go deleted file mode 100644 index 8060e55..0000000 --- a/prometheus/decoding/model.go +++ /dev/null @@ -1,287 +0,0 @@ -// 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() -} diff --git a/prometheus/decoding/helpers_test.go b/test/test.go similarity index 82% rename from prometheus/decoding/helpers_test.go rename to test/test.go index ca94c29..225aa70 100644 --- a/prometheus/decoding/helpers_test.go +++ b/test/test.go @@ -11,17 +11,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package decoding +// Package test provides common test helpers to the client library. +package test -type tester interface { +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 { +// ErrorEqual compares Go errors for equality. +func ErrorEqual(left, right error) bool { if left == right { return true }