Repository: prometheus/common
Pull request #417 (2022-12-08 07:48:33)

Add histogram vectors to model
Addresses https://github.com/prometheus/client_golang/issues/1179

Tested the JSON decoding successfully by scraping a simple server running native histograms with Prometheus, then using the HTTP API in `client_golang` (which does not need to be updated, only this library needs updating) to query it.

beorn7: Cool. I hope I can help with a review soon…

Patch:
diff --git a/Makefile.common b/Makefile.common
index 1985fcd..e358db6 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -61,7 +61,7 @@ PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_
 SKIP_GOLANGCI_LINT :=
 GOLANGCI_LINT :=
 GOLANGCI_LINT_OPTS ?=
-GOLANGCI_LINT_VERSION ?= v1.50.1
+GOLANGCI_LINT_VERSION ?= v1.49.0
 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
 # windows isn't included here because of the path separator being different.
 ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
diff --git a/model/value.go b/model/value.go
index c9d8fb1..543399e 100644
--- a/model/value.go
+++ b/model/value.go
@@ -16,20 +16,12 @@ package model
 import (
 	"encoding/json"
 	"fmt"
-	"math"
 	"sort"
 	"strconv"
 	"strings"
 )
 
 var (
-	// ZeroSamplePair is the pseudo zero-value of SamplePair used to signal a
-	// non-existing sample pair. It is a SamplePair with timestamp Earliest and
-	// value 0.0. Note that the natural zero value of SamplePair has a timestamp
-	// of 0, which is possible to appear in a real SamplePair and thus not
-	// suitable to signal a non-existing SamplePair.
-	ZeroSamplePair = SamplePair{Timestamp: Earliest}
-
 	// ZeroSample is the pseudo zero-value of Sample used to signal a
 	// non-existing sample. It is a Sample with timestamp Earliest, value 0.0,
 	// and metric nil. Note that the natural zero value of Sample has a timestamp
@@ -38,82 +30,14 @@ var (
 	ZeroSample = Sample{Timestamp: Earliest}
 )
 
-// A SampleValue is a representation of a value for a given sample at a given
-// time.
-type SampleValue float64
-
-// MarshalJSON implements json.Marshaler.
-func (v SampleValue) MarshalJSON() ([]byte, error) {
-	return json.Marshal(v.String())
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (v *SampleValue) UnmarshalJSON(b []byte) error {
-	if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' {
-		return fmt.Errorf("sample value must be a quoted string")
-	}
-	f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64)
-	if err != nil {
-		return err
-	}
-	*v = SampleValue(f)
-	return nil
-}
-
-// Equal returns true if the value of v and o is equal or if both are NaN. Note
-// that v==o is false if both are NaN. If you want the conventional float
-// behavior, use == to compare two SampleValues.
-func (v SampleValue) Equal(o SampleValue) bool {
-	if v == o {
-		return true
-	}
-	return math.IsNaN(float64(v)) && math.IsNaN(float64(o))
-}
-
-func (v SampleValue) String() string {
-	return strconv.FormatFloat(float64(v), 'f', -1, 64)
-}
-
-// SamplePair pairs a SampleValue with a Timestamp.
-type SamplePair struct {
-	Timestamp Time
-	Value     SampleValue
-}
-
-// MarshalJSON implements json.Marshaler.
-func (s SamplePair) MarshalJSON() ([]byte, error) {
-	t, err := json.Marshal(s.Timestamp)
-	if err != nil {
-		return nil, err
-	}
-	v, err := json.Marshal(s.Value)
-	if err != nil {
-		return nil, err
-	}
-	return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (s *SamplePair) UnmarshalJSON(b []byte) error {
-	v := [...]json.Unmarshaler{&s.Timestamp, &s.Value}
-	return json.Unmarshal(b, &v)
-}
-
-// Equal returns true if this SamplePair and o have equal Values and equal
-// Timestamps. The semantics of Value equality is defined by SampleValue.Equal.
-func (s *SamplePair) Equal(o *SamplePair) bool {
-	return s == o || (s.Value.Equal(o.Value) && s.Timestamp.Equal(o.Timestamp))
-}
-
-func (s SamplePair) String() string {
-	return fmt.Sprintf("%s @[%s]", s.Value, s.Timestamp)
-}
-
-// Sample is a sample pair associated with a metric.
+// Sample is a sample pair associated with a metric. A single sample must either
+// define Value or Histogram but not both. Histogram == nil implies the Value
+// field is used, otherwise it should be ignored.
 type Sample struct {
-	Metric    Metric      `json:"metric"`
-	Value     SampleValue `json:"value"`
-	Timestamp Time        `json:"timestamp"`
+	Metric    Metric           `json:"metric"`
+	Value     SampleValue      `json:"value"`
+	Timestamp Time             `json:"timestamp"`
+	Histogram *SampleHistogram `json:"histogram"`
 }
 
 // Equal compares first the metrics, then the timestamp, then the value. The
@@ -129,11 +53,19 @@ func (s *Sample) Equal(o *Sample) bool {
 	if !s.Timestamp.Equal(o.Timestamp) {
 		return false
 	}
-
+	if s.Histogram != nil {
+		return s.Histogram.Equal(o.Histogram)
+	}
 	return s.Value.Equal(o.Value)
 }
 
 func (s Sample) String() string {
+	if s.Histogram != nil {
+		return fmt.Sprintf("%s => %s", s.Metric, SampleHistogramPair{
+			Timestamp: s.Timestamp,
+			Histogram: *s.Histogram,
+		})
+	}
 	return fmt.Sprintf("%s => %s", s.Metric, SamplePair{
 		Timestamp: s.Timestamp,
 		Value:     s.Value,
@@ -142,6 +74,19 @@ func (s Sample) String() string {
 
 // MarshalJSON implements json.Marshaler.
 func (s Sample) MarshalJSON() ([]byte, error) {
+	if s.Histogram != nil {
+		v := struct {
+			Metric    Metric              `json:"metric"`
+			Histogram SampleHistogramPair `json:"histogram"`
+		}{
+			Metric: s.Metric,
+			Histogram: SampleHistogramPair{
+				Timestamp: s.Timestamp,
+				Histogram: *s.Histogram,
+			},
+		}
+		return json.Marshal(&v)
+	}
 	v := struct {
 		Metric Metric     `json:"metric"`
 		Value  SamplePair `json:"value"`
@@ -152,21 +97,43 @@ func (s Sample) MarshalJSON() ([]byte, error) {
 			Value:     s.Value,
 		},
 	}
-
 	return json.Marshal(&v)
 }
 
+type sampleHistogramPairPtr struct {
+	Timestamp Time
+	Histogram *SampleHistogram
+}
+
+func (s *sampleHistogramPairPtr) UnmarshalJSON(buf []byte) error {
+	tmp := []interface{}{&s.Timestamp, &s.Histogram}
+	wantLen := len(tmp)
+	if err := json.Unmarshal(buf, &tmp); err != nil {
+		return err
+	}
+	if gotLen := len(tmp); gotLen != wantLen {
+		return fmt.Errorf("wrong number of fields: %d != %d", gotLen, wantLen)
+	}
+	return nil
+}
+
 // UnmarshalJSON implements json.Unmarshaler.
+// TODO: simplify and remove the need for both sampleHistogramPairPtr and SampleHistogramPair
 func (s *Sample) UnmarshalJSON(b []byte) error {
 	v := struct {
-		Metric Metric     `json:"metric"`
-		Value  SamplePair `json:"value"`
+		Metric    Metric                 `json:"metric"`
+		Value     SamplePair             `json:"value"`
+		Histogram sampleHistogramPairPtr `json:"histogram"`
 	}{
 		Metric: s.Metric,
 		Value: SamplePair{
 			Timestamp: s.Timestamp,
 			Value:     s.Value,
 		},
+		Histogram: sampleHistogramPairPtr{
+			Timestamp: s.Timestamp,
+			Histogram: s.Histogram,
+		},
 	}
 
 	if err := json.Unmarshal(b, &v); err != nil {
@@ -174,8 +141,13 @@ func (s *Sample) UnmarshalJSON(b []byte) error {
 	}
 
 	s.Metric = v.Metric
-	s.Timestamp = v.Value.Timestamp
-	s.Value = v.Value.Value
+	if v.Histogram.Histogram != nil {
+		s.Timestamp = v.Histogram.Timestamp
+		s.Histogram = v.Histogram.Histogram
+	} else {
+		s.Timestamp = v.Value.Timestamp
+		s.Value = v.Value.Value
+	}
 
 	return nil
 }
@@ -221,80 +193,76 @@ func (s Samples) Equal(o Samples) bool {
 
 // SampleStream is a stream of Values belonging to an attached COWMetric.
 type SampleStream struct {
-	Metric Metric       `json:"metric"`
-	Values []SamplePair `json:"values"`
+	Metric     Metric                `json:"metric"`
+	Values     []SamplePair          `json:"values"`
+	Histograms []SampleHistogramPair `json:"histograms"`
 }
 
 func (ss SampleStream) String() string {
-	vals := make([]string, len(ss.Values))
+	valuesLength := len(ss.Values)
+	vals := make([]string, valuesLength+len(ss.Histograms))
 	for i, v := range ss.Values {
 		vals[i] = v.String()
 	}
+	for i, v := range ss.Histograms {
+		vals[i+valuesLength] = v.String()
+	}
 	return fmt.Sprintf("%s =>\n%s", ss.Metric, strings.Join(vals, "\n"))
 }
 
-// Value is a generic interface for values resulting from a query evaluation.
-type Value interface {
-	Type() ValueType
-	String() string
+func (ss SampleStream) MarshalJSON() ([]byte, error) {
+	if len(ss.Histograms) > 0 && len(ss.Values) > 0 {
+		v := struct {
+			Metric     Metric                `json:"metric"`
+			Values     []SamplePair          `json:"values"`
+			Histograms []SampleHistogramPair `json:"histograms"`
+		}{
+			Metric:     ss.Metric,
+			Values:     ss.Values,
+			Histograms: ss.Histograms,
+		}
+		return json.Marshal(&v)
+	} else if len(ss.Histograms) > 0 {
+		v := struct {
+			Metric     Metric                `json:"metric"`
+			Histograms []SampleHistogramPair `json:"histograms"`
+		}{
+			Metric:     ss.Metric,
+			Histograms: ss.Histograms,
+		}
+		return json.Marshal(&v)
+	} else {
+		v := struct {
+			Metric Metric       `json:"metric"`
+			Values []SamplePair `json:"values"`
+		}{
+			Metric: ss.Metric,
+			Values: ss.Values,
+		}
+		return json.Marshal(&v)
+	}
 }
 
-func (Matrix) Type() ValueType  { return ValMatrix }
-func (Vector) Type() ValueType  { return ValVector }
-func (*Scalar) Type() ValueType { return ValScalar }
-func (*String) Type() ValueType { return ValString }
-
-type ValueType int
-
-const (
-	ValNone ValueType = iota
-	ValScalar
-	ValVector
-	ValMatrix
-	ValString
-)
-
-// MarshalJSON implements json.Marshaler.
-func (et ValueType) MarshalJSON() ([]byte, error) {
-	return json.Marshal(et.String())
-}
+func (ss *SampleStream) UnmarshalJSON(b []byte) error {
+	v := struct {
+		Metric     Metric                `json:"metric"`
+		Values     []SamplePair          `json:"values"`
+		Histograms []SampleHistogramPair `json:"histograms"`
+	}{
+		Metric:     ss.Metric,
+		Values:     ss.Values,
+		Histograms: ss.Histograms,
+	}
 
-func (et *ValueType) UnmarshalJSON(b []byte) error {
-	var s string
-	if err := json.Unmarshal(b, &s); err != nil {
+	if err := json.Unmarshal(b, &v); err != nil {
 		return err
 	}
-	switch s {
-	case "<ValNone>":
-		*et = ValNone
-	case "scalar":
-		*et = ValScalar
-	case "vector":
-		*et = ValVector
-	case "matrix":
-		*et = ValMatrix
-	case "string":
-		*et = ValString
-	default:
-		return fmt.Errorf("unknown value type %q", s)
-	}
-	return nil
-}
 
-func (e ValueType) String() string {
-	switch e {
-	case ValNone:
-		return "<ValNone>"
-	case ValScalar:
-		return "scalar"
-	case ValVector:
-		return "vector"
-	case ValMatrix:
-		return "matrix"
-	case ValString:
-		return "string"
-	}
-	panic("ValueType.String: unhandled value type")
+	ss.Metric = v.Metric
+	ss.Values = v.Values
+	ss.Histograms = v.Histograms
+
+	return nil
 }
 
 // Scalar is a scalar value evaluated at the set timestamp.
diff --git a/model/value_float.go b/model/value_float.go
new file mode 100644
index 0000000..9266fb9
--- /dev/null
+++ b/model/value_float.go
@@ -0,0 +1,101 @@
+// Copyright 2013 The Prometheus Authors
+// 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/json"
+	"fmt"
+	"math"
+	"strconv"
+)
+
+var (
+	// ZeroSamplePair is the pseudo zero-value of SamplePair used to signal a
+	// non-existing sample pair. It is a SamplePair with timestamp Earliest and
+	// value 0.0. Note that the natural zero value of SamplePair has a timestamp
+	// of 0, which is possible to appear in a real SamplePair and thus not
+	// suitable to signal a non-existing SamplePair.
+	ZeroSamplePair = SamplePair{Timestamp: Earliest}
+)
+
+// A SampleValue is a representation of a value for a given sample at a given
+// time.
+type SampleValue float64
+
+// MarshalJSON implements json.Marshaler.
+func (v SampleValue) MarshalJSON() ([]byte, error) {
+	return json.Marshal(v.String())
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (v *SampleValue) UnmarshalJSON(b []byte) error {
+	if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' {
+		return fmt.Errorf("sample value must be a quoted string")
+	}
+	f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64)
+	if err != nil {
+		return err
+	}
+	*v = SampleValue(f)
+	return nil
+}
+
+// Equal returns true if the value of v and o is equal or if both are NaN. Note
+// that v==o is false if both are NaN. If you want the conventional float
+// behavior, use == to compare two SampleValues.
+func (v SampleValue) Equal(o SampleValue) bool {
+	if v == o {
+		return true
+	}
+	return math.IsNaN(float64(v)) && math.IsNaN(float64(o))
+}
+
+func (v SampleValue) String() string {
+	return strconv.FormatFloat(float64(v), 'f', -1, 64)
+}
+
+// SamplePair pairs a SampleValue with a Timestamp.
+type SamplePair struct {
+	Timestamp Time
+	Value     SampleValue
+}
+
+// MarshalJSON implements json.Marshaler.
+func (s SamplePair) MarshalJSON() ([]byte, error) {
+	t, err := json.Marshal(s.Timestamp)
+	if err != nil {
+		return nil, err
+	}
+	v, err := json.Marshal(s.Value)
+	if err != nil {
+		return nil, err
+	}
+	return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (s *SamplePair) UnmarshalJSON(b []byte) error {
+	v := [...]json.Unmarshaler{&s.Timestamp, &s.Value}
+	return json.Unmarshal(b, &v)
+}
+
+// Equal returns true if this SamplePair and o have equal Values and equal
+// Timestamps. The semantics of Value equality is defined by SampleValue.Equal.
+func (s *SamplePair) Equal(o *SamplePair) bool {
+	return s == o || (s.Value.Equal(o.Value) && s.Timestamp.Equal(o.Timestamp))
+}
+
+func (s SamplePair) String() string {
+	return fmt.Sprintf("%s @[%s]", s.Value, s.Timestamp)
+}
diff --git a/model/value_histogram.go b/model/value_histogram.go
new file mode 100644
index 0000000..a4be0bc
--- /dev/null
+++ b/model/value_histogram.go
@@ -0,0 +1,171 @@
+// Copyright 2013 The Prometheus Authors
+// 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/json"
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+type FloatString float64
+
+func (v FloatString) String() string {
+	return strconv.FormatFloat(float64(v), 'f', -1, 64)
+}
+
+func (v FloatString) MarshalJSON() ([]byte, error) {
+	return json.Marshal(v.String())
+}
+
+func (v *FloatString) UnmarshalJSON(b []byte) error {
+	if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' {
+		return fmt.Errorf("float value must be a quoted string")
+	}
+	f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64)
+	if err != nil {
+		return err
+	}
+	*v = FloatString(f)
+	return nil
+}
+
+type HistogramBucket struct {
+	Boundaries int
+	Lower      FloatString
+	Upper      FloatString
+	Count      FloatString
+}
+
+func (s HistogramBucket) MarshalJSON() ([]byte, error) {
+	b, err := json.Marshal(s.Boundaries)
+	if err != nil {
+		return nil, err
+	}
+	l, err := json.Marshal(s.Lower)
+	if err != nil {
+		return nil, err
+	}
+	u, err := json.Marshal(s.Upper)
+	if err != nil {
+		return nil, err
+	}
+	c, err := json.Marshal(s.Count)
+	if err != nil {
+		return nil, err
+	}
+	return []byte(fmt.Sprintf("[%s,%s,%s,%s]", b, l, u, c)), nil
+}
+
+func (s *HistogramBucket) UnmarshalJSON(buf []byte) error {
+	tmp := []interface{}{&s.Boundaries, &s.Lower, &s.Upper, &s.Count}
+	wantLen := len(tmp)
+	if err := json.Unmarshal(buf, &tmp); err != nil {
+		return err
+	}
+	if gotLen := len(tmp); gotLen != wantLen {
+		return fmt.Errorf("wrong number of fields: %d != %d", gotLen, wantLen)
+	}
+	return nil
+}
+
+func (s *HistogramBucket) Equal(o *HistogramBucket) bool {
+	return s == o || (s.Boundaries == o.Boundaries && s.Lower == o.Lower && s.Upper == o.Upper && s.Count == o.Count)
+}
+
+func (b HistogramBucket) String() string {
+	var sb strings.Builder
+	lowerInclusive := b.Boundaries == 1 || b.Boundaries == 3
+	upperInclusive := b.Boundaries == 0 || b.Boundaries == 3
+	if lowerInclusive {
+		sb.WriteRune('[')
+	} else {
+		sb.WriteRune('(')
+	}
+	fmt.Fprintf(&sb, "%g,%g", b.Lower, b.Upper)
+	if upperInclusive {
+		sb.WriteRune(']')
+	} else {
+		sb.WriteRune(')')
+	}
+	fmt.Fprintf(&sb, ":%v", b.Count)
+	return sb.String()
+}
+
+type HistogramBuckets []*HistogramBucket
+
+func (s HistogramBuckets) Equal(o HistogramBuckets) bool {
+	if len(s) != len(o) {
+		return false
+	}
+
+	for i, bucket := range s {
+		if !bucket.Equal(o[i]) {
+			return false
+		}
+	}
+	return true
+}
+
+type SampleHistogram struct {
+	Count   FloatString      `json:"count"`
+	Sum     FloatString      `json:"sum"`
+	Buckets HistogramBuckets `json:"buckets"`
+}
+
+func (s SampleHistogram) String() string {
+	return fmt.Sprintf("Count: %f, Sum: %f, Buckets: %v", s.Count, s.Sum, s.Buckets)
+}
+
+func (s *SampleHistogram) Equal(o *SampleHistogram) bool {
+	return s == o || (s.Count == o.Count && s.Sum == o.Sum && s.Buckets.Equal(o.Buckets))
+}
+
+type SampleHistogramPair struct {
+	Timestamp Time
+	Histogram SampleHistogram
+}
+
+func (s SampleHistogramPair) MarshalJSON() ([]byte, error) {
+	t, err := json.Marshal(s.Timestamp)
+	if err != nil {
+		return nil, err
+	}
+	v, err := json.Marshal(s.Histogram)
+	if err != nil {
+		return nil, err
+	}
+	return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil
+}
+
+func (s *SampleHistogramPair) UnmarshalJSON(buf []byte) error {
+	tmp := []interface{}{&s.Timestamp, &s.Histogram}
+	wantLen := len(tmp)
+	if err := json.Unmarshal(buf, &tmp); err != nil {
+		return err
+	}
+	if gotLen := len(tmp); gotLen != wantLen {
+		return fmt.Errorf("wrong number of fields: %d != %d", gotLen, wantLen)
+	}
+	return nil
+}
+
+func (s SampleHistogramPair) String() string {
+	return fmt.Sprintf("%s @[%s]", s.Histogram, s.Timestamp)
+}
+
+func (s *SampleHistogramPair) Equal(o *SampleHistogramPair) bool {
+	return s == o || (s.Histogram.Equal(&o.Histogram) && s.Timestamp.Equal(o.Timestamp))
+}
diff --git a/model/value_type.go b/model/value_type.go
new file mode 100644
index 0000000..726c50e
--- /dev/null
+++ b/model/value_type.go
@@ -0,0 +1,83 @@
+// Copyright 2013 The Prometheus Authors
+// 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/json"
+	"fmt"
+)
+
+// Value is a generic interface for values resulting from a query evaluation.
+type Value interface {
+	Type() ValueType
+	String() string
+}
+
+func (Matrix) Type() ValueType  { return ValMatrix }
+func (Vector) Type() ValueType  { return ValVector }
+func (*Scalar) Type() ValueType { return ValScalar }
+func (*String) Type() ValueType { return ValString }
+
+type ValueType int
+
+const (
+	ValNone ValueType = iota
+	ValScalar
+	ValVector
+	ValMatrix
+	ValString
+)
+
+// MarshalJSON implements json.Marshaler.
+func (et ValueType) MarshalJSON() ([]byte, error) {
+	return json.Marshal(et.String())
+}
+
+func (et *ValueType) UnmarshalJSON(b []byte) error {
+	var s string
+	if err := json.Unmarshal(b, &s); err != nil {
+		return err
+	}
+	switch s {
+	case "<ValNone>":
+		*et = ValNone
+	case "scalar":
+		*et = ValScalar
+	case "vector":
+		*et = ValVector
+	case "matrix":
+		*et = ValMatrix
+	case "string":
+		*et = ValString
+	default:
+		return fmt.Errorf("unknown value type %q", s)
+	}
+	return nil
+}
+
+func (e ValueType) String() string {
+	switch e {
+	case ValNone:
+		return "<ValNone>"
+	case ValScalar:
+		return "scalar"
+	case ValVector:
+		return "vector"
+	case ValMatrix:
+		return "matrix"
+	case ValString:
+		return "string"
+	}
+	panic("ValueType.String: unhandled value type")
+}


Test patch:
diff --git a/model/value_float_test.go b/model/value_float_test.go
new file mode 100644
index 0000000..5ceb56e
--- /dev/null
+++ b/model/value_float_test.go
@@ -0,0 +1,293 @@
+// Copyright 2013 The Prometheus Authors
+// 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/json"
+	"math"
+	"reflect"
+	"testing"
+)
+
+func TestEqualValues(t *testing.T) {
+	tests := map[string]struct {
+		in1, in2 SampleValue
+		want     bool
+	}{
+		"equal floats": {
+			in1:  3.14,
+			in2:  3.14,
+			want: true,
+		},
+		"unequal floats": {
+			in1:  3.14,
+			in2:  3.1415,
+			want: false,
+		},
+		"positive infinities": {
+			in1:  SampleValue(math.Inf(+1)),
+			in2:  SampleValue(math.Inf(+1)),
+			want: true,
+		},
+		"negative infinities": {
+			in1:  SampleValue(math.Inf(-1)),
+			in2:  SampleValue(math.Inf(-1)),
+			want: true,
+		},
+		"different infinities": {
+			in1:  SampleValue(math.Inf(+1)),
+			in2:  SampleValue(math.Inf(-1)),
+			want: false,
+		},
+		"number and infinity": {
+			in1:  42,
+			in2:  SampleValue(math.Inf(+1)),
+			want: false,
+		},
+		"number and NaN": {
+			in1:  42,
+			in2:  SampleValue(math.NaN()),
+			want: false,
+		},
+		"NaNs": {
+			in1:  SampleValue(math.NaN()),
+			in2:  SampleValue(math.NaN()),
+			want: true, // !!!
+		},
+	}
+
+	for name, test := range tests {
+		got := test.in1.Equal(test.in2)
+		if got != test.want {
+			t.Errorf("Comparing %s, %f and %f: got %t, want %t", name, test.in1, test.in2, got, test.want)
+		}
+	}
+}
+
+func TestSamplePairJSON(t *testing.T) {
+	input := []struct {
+		plain string
+		value SamplePair
+	}{
+		{
+			plain: `[1234.567,"123.1"]`,
+			value: SamplePair{
+				Value:     123.1,
+				Timestamp: 1234567,
+			},
+		},
+	}
+
+	for _, test := range input {
+		b, err := json.Marshal(test.value)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if string(b) != test.plain {
+			t.Errorf("encoding error: expected %q, got %q", test.plain, b)
+			continue
+		}
+
+		var sp SamplePair
+		err = json.Unmarshal(b, &sp)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if sp != test.value {
+			t.Errorf("decoding error: expected %v, got %v", test.value, sp)
+		}
+	}
+}
+
+func TestSampleJSON(t *testing.T) {
+	input := []struct {
+		plain string
+		value Sample
+	}{
+		{
+			plain: `{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]}`,
+			value: Sample{
+				Metric: Metric{
+					MetricNameLabel: "test_metric",
+				},
+				Value:     123.1,
+				Timestamp: 1234567,
+			},
+		},
+	}
+
+	for _, test := range input {
+		b, err := json.Marshal(test.value)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if string(b) != test.plain {
+			t.Errorf("encoding error: expected %q, got %q", test.plain, b)
+			continue
+		}
+
+		var sv Sample
+		err = json.Unmarshal(b, &sv)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if !reflect.DeepEqual(sv, test.value) {
+			t.Errorf("decoding error: expected %v, got %v", test.value, sv)
+		}
+	}
+}
+
+func TestVectorJSON(t *testing.T) {
+	input := []struct {
+		plain string
+		value Vector
+	}{
+		{
+			plain: `[]`,
+			value: Vector{},
+		},
+		{
+			plain: `[{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]}]`,
+			value: Vector{&Sample{
+				Metric: Metric{
+					MetricNameLabel: "test_metric",
+				},
+				Value:     123.1,
+				Timestamp: 1234567,
+			}},
+		},
+		{
+			plain: `[{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]},{"metric":{"foo":"bar"},"value":[1.234,"+Inf"]}]`,
+			value: Vector{
+				&Sample{
+					Metric: Metric{
+						MetricNameLabel: "test_metric",
+					},
+					Value:     123.1,
+					Timestamp: 1234567,
+				},
+				&Sample{
+					Metric: Metric{
+						"foo": "bar",
+					},
+					Value:     SampleValue(math.Inf(1)),
+					Timestamp: 1234,
+				},
+			},
+		},
+	}
+
+	for _, test := range input {
+		b, err := json.Marshal(test.value)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if string(b) != test.plain {
+			t.Errorf("encoding error: expected %q, got %q", test.plain, b)
+			continue
+		}
+
+		var vec Vector
+		err = json.Unmarshal(b, &vec)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if !reflect.DeepEqual(vec, test.value) {
+			t.Errorf("decoding error: expected %v, got %v", test.value, vec)
+		}
+	}
+}
+
+func TestMatrixJSON(t *testing.T) {
+	input := []struct {
+		plain string
+		value Matrix
+	}{
+		{
+			plain: `[]`,
+			value: Matrix{},
+		},
+		{
+			plain: `[{"metric":{"__name__":"test_metric"},"values":[[1234.567,"123.1"],[12345.678,"123.12"]]},{"metric":{"foo":"bar"},"values":[[2234.567,"223.1"],[22345.678,"223.12"]]}]`,
+			value: Matrix{
+				&SampleStream{
+					Metric: Metric{
+						MetricNameLabel: "test_metric",
+					},
+					Values: []SamplePair{
+						{
+							Value:     123.1,
+							Timestamp: 1234567,
+						},
+						{
+							Value:     123.12,
+							Timestamp: 12345678,
+						},
+					},
+				},
+				&SampleStream{
+					Metric: Metric{
+						"foo": "bar",
+					},
+					Values: []SamplePair{
+						{
+							Value:     223.1,
+							Timestamp: 2234567,
+						},
+						{
+							Value:     223.12,
+							Timestamp: 22345678,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range input {
+		b, err := json.Marshal(test.value)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if string(b) != test.plain {
+			t.Errorf("encoding error: expected %q, got %q", test.plain, b)
+			continue
+		}
+
+		var mat Matrix
+		err = json.Unmarshal(b, &mat)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if !reflect.DeepEqual(mat, test.value) {
+			t.Errorf("decoding error: expected %v, got %v", test.value, mat)
+		}
+	}
+}
diff --git a/model/value_histogram_test.go b/model/value_histogram_test.go
new file mode 100644
index 0000000..4b7d45c
--- /dev/null
+++ b/model/value_histogram_test.go
@@ -0,0 +1,732 @@
+// Copyright 2013 The Prometheus Authors
+// 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/json"
+	"reflect"
+	"regexp"
+	"testing"
+)
+
+var (
+	noWhitespace = regexp.MustCompile(`\s`)
+)
+
+func genSampleHistogram() SampleHistogram {
+	return SampleHistogram{
+		Count: 6,
+		Sum:   3897,
+		Buckets: HistogramBuckets{
+			{
+				Boundaries: 1,
+				Lower:      -4870.992343051145,
+				Upper:      -4466.7196729968955,
+				Count:      1,
+			},
+			{
+				Boundaries: 1,
+				Lower:      -861.0779292198035,
+				Upper:      -789.6119426088657,
+				Count:      1,
+			},
+			{
+				Boundaries: 1,
+				Lower:      -558.3399591246119,
+				Upper:      -512,
+				Count:      1,
+			},
+			{
+				Boundaries: 0,
+				Lower:      2048,
+				Upper:      2233.3598364984477,
+				Count:      1,
+			},
+			{
+				Boundaries: 0,
+				Lower:      2896.3093757400984,
+				Upper:      3158.4477704354626,
+				Count:      1,
+			},
+			{
+				Boundaries: 0,
+				Lower:      4466.7196729968955,
+				Upper:      4870.992343051145,
+				Count:      1,
+			},
+		},
+	}
+}
+
+func genSampleHistogramPtr() *SampleHistogram {
+	h := genSampleHistogram()
+	return &h
+}
+
+func TestSampleHistogramPairJSON(t *testing.T) {
+	input := []struct {
+		plain string
+		value SampleHistogramPair
+	}{
+		{
+			plain: `[
+				1234.567,
+				{
+					"count":"6",
+					"sum":"3897",
+					"buckets":[
+						[
+							1,
+							"-4870.992343051145",
+							"-4466.7196729968955",
+							"1"
+						],
+						[
+							1,
+							"-861.0779292198035",
+							"-789.6119426088657",
+							"1"
+						],
+						[
+							1,
+							"-558.3399591246119",
+							"-512",
+							"1"
+						],
+						[
+							0,
+							"2048",
+							"2233.3598364984477",
+							"1"
+						],
+						[
+							0,
+							"2896.3093757400984",
+							"3158.4477704354626",
+							"1"
+						],
+						[
+							0,
+							"4466.7196729968955",
+							"4870.992343051145",
+							"1"
+						]
+					]
+				}
+			]`,
+			value: SampleHistogramPair{
+				Histogram: genSampleHistogram(),
+				Timestamp: 1234567,
+			},
+		},
+	}
+
+	for _, test := range input {
+		b, err := json.Marshal(test.value)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		trimmed := noWhitespace.ReplaceAllString(test.plain, "")
+		if string(b) != trimmed {
+			t.Errorf("encoding error: expected %q, got %q", trimmed, b)
+			continue
+		}
+
+		var sp SampleHistogramPair
+		err = json.Unmarshal(b, &sp)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if !sp.Equal(&test.value) {
+			t.Errorf("decoding error: expected %v, got %v", test.value, sp)
+		}
+	}
+}
+
+func TestSampleHistogramJSON(t *testing.T) {
+	input := []struct {
+		plain string
+		value Sample
+	}{
+		{
+			plain: `{
+				"metric":{
+					"__name__":"test_metric"
+				},
+				"histogram":[
+					1234.567,
+					{
+						"count":"6",
+						"sum":"3897",
+						"buckets":[
+							[
+								1,
+								"-4870.992343051145",
+								"-4466.7196729968955",
+								"1"
+							],
+							[
+								1,
+								"-861.0779292198035",
+								"-789.6119426088657",
+								"1"
+							],
+							[
+								1,
+								"-558.3399591246119",
+								"-512",
+								"1"
+							],
+							[
+								0,
+								"2048",
+								"2233.3598364984477",
+								"1"
+							],
+							[
+								0,
+								"2896.3093757400984",
+								"3158.4477704354626",
+								"1"
+							],
+							[
+								0,
+								"4466.7196729968955",
+								"4870.992343051145",
+								"1"
+							]
+						]
+					}
+				]
+			}`,
+			value: Sample{
+				Metric: Metric{
+					MetricNameLabel: "test_metric",
+				},
+				Histogram: genSampleHistogramPtr(),
+				Timestamp: 1234567,
+			},
+		},
+	}
+
+	for _, test := range input {
+		b, err := json.Marshal(test.value)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		trimmed := noWhitespace.ReplaceAllString(test.plain, "")
+		if string(b) != trimmed {
+			t.Errorf("encoding error: expected %q, got %q", trimmed, b)
+			continue
+		}
+
+		var sv Sample
+		err = json.Unmarshal(b, &sv)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if !reflect.DeepEqual(sv, test.value) {
+			t.Errorf("decoding error: expected %v, got %v", test.value, sv)
+		}
+	}
+}
+
+func TestVectorHistogramJSON(t *testing.T) {
+	input := []struct {
+		plain string
+		value Vector
+	}{
+		{
+			plain: `[
+				{
+					"metric":{
+						"__name__":"test_metric"
+					},
+					"histogram":[
+						1234.567,
+						{
+							"count":"6",
+							"sum":"3897",
+							"buckets":[
+							[
+								1,
+								"-4870.992343051145",
+								"-4466.7196729968955",
+								"1"
+							],
+							[
+								1,
+								"-861.0779292198035",
+								"-789.6119426088657",
+								"1"
+							],
+							[
+								1,
+								"-558.3399591246119",
+								"-512",
+								"1"
+							],
+							[
+								0,
+								"2048",
+								"2233.3598364984477",
+								"1"
+							],
+							[
+								0,
+								"2896.3093757400984",
+								"3158.4477704354626",
+								"1"
+							],
+							[
+								0,
+								"4466.7196729968955",
+								"4870.992343051145",
+								"1"
+							]
+							]
+						}
+					]
+				}
+			]`,
+			value: Vector{&Sample{
+				Metric: Metric{
+					MetricNameLabel: "test_metric",
+				},
+				Histogram: genSampleHistogramPtr(),
+				Timestamp: 1234567,
+			}},
+		},
+		{
+			plain: `[
+				{
+					"metric":{
+						"__name__":"test_metric"
+					},
+					"histogram":[
+						1234.567,
+						{
+							"count":"6",
+							"sum":"3897",
+							"buckets":[
+							[
+								1,
+								"-4870.992343051145",
+								"-4466.7196729968955",
+								"1"
+							],
+							[
+								1,
+								"-861.0779292198035",
+								"-789.6119426088657",
+								"1"
+							],
+							[
+								1,
+								"-558.3399591246119",
+								"-512",
+								"1"
+							],
+							[
+								0,
+								"2048",
+								"2233.3598364984477",
+								"1"
+							],
+							[
+								0,
+								"2896.3093757400984",
+								"3158.4477704354626",
+								"1"
+							],
+							[
+								0,
+								"4466.7196729968955",
+								"4870.992343051145",
+								"1"
+							]
+							]
+						}
+					]
+				},
+				{
+					"metric":{
+						"foo":"bar"
+					},
+					"histogram":[
+						1.234,
+						{
+							"count":"6",
+							"sum":"3897",
+							"buckets":[
+							[
+								1,
+								"-4870.992343051145",
+								"-4466.7196729968955",
+								"1"
+							],
+							[
+								1,
+								"-861.0779292198035",
+								"-789.6119426088657",
+								"1"
+							],
+							[
+								1,
+								"-558.3399591246119",
+								"-512",
+								"1"
+							],
+							[
+								0,
+								"2048",
+								"2233.3598364984477",
+								"1"
+							],
+							[
+								0,
+								"2896.3093757400984",
+								"3158.4477704354626",
+								"1"
+							],
+							[
+								0,
+								"4466.7196729968955",
+								"4870.992343051145",
+								"1"
+							]
+							]
+						}
+					]
+				}
+			]`,
+			value: Vector{
+				&Sample{
+					Metric: Metric{
+						MetricNameLabel: "test_metric",
+					},
+					Histogram: genSampleHistogramPtr(),
+					Timestamp: 1234567,
+				},
+				&Sample{
+					Metric: Metric{
+						"foo": "bar",
+					},
+					Histogram: genSampleHistogramPtr(),
+					Timestamp: 1234,
+				},
+			},
+		},
+	}
+
+	for _, test := range input {
+		b, err := json.Marshal(test.value)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		trimmed := noWhitespace.ReplaceAllString(test.plain, "")
+		if string(b) != trimmed {
+			t.Errorf("encoding error: expected %q, got %q", trimmed, b)
+			continue
+		}
+
+		var vec Vector
+		err = json.Unmarshal(b, &vec)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if !reflect.DeepEqual(vec, test.value) {
+			t.Errorf("decoding error: expected %v, got %v", test.value, vec)
+		}
+	}
+}
+
+func TestMatrixHistogramJSON(t *testing.T) {
+	input := []struct {
+		plain string
+		value Matrix
+	}{
+		{
+			plain: `[]`,
+			value: Matrix{},
+		},
+		{
+			plain: `[
+				{
+					"metric":{
+						"__name__":"test_metric"
+					},
+					"histograms":[
+						[
+							1234.567,
+							{
+							"count":"6",
+							"sum":"3897",
+							"buckets":[
+								[
+									1,
+									"-4870.992343051145",
+									"-4466.7196729968955",
+									"1"
+								],
+								[
+									1,
+									"-861.0779292198035",
+									"-789.6119426088657",
+									"1"
+								],
+								[
+									1,
+									"-558.3399591246119",
+									"-512",
+									"1"
+								],
+								[
+									0,
+									"2048",
+									"2233.3598364984477",
+									"1"
+								],
+								[
+									0,
+									"2896.3093757400984",
+									"3158.4477704354626",
+									"1"
+								],
+								[
+									0,
+									"4466.7196729968955",
+									"4870.992343051145",
+									"1"
+								]
+							]
+							}
+						],
+						[
+							12345.678,
+							{
+							"count":"6",
+							"sum":"3897",
+							"buckets":[
+								[
+									1,
+									"-4870.992343051145",
+									"-4466.7196729968955",
+									"1"
+								],
+								[
+									1,
+									"-861.0779292198035",
+									"-789.6119426088657",
+									"1"
+								],
+								[
+									1,
+									"-558.3399591246119",
+									"-512",
+									"1"
+								],
+								[
+									0,
+									"2048",
+									"2233.3598364984477",
+									"1"
+								],
+								[
+									0,
+									"2896.3093757400984",
+									"3158.4477704354626",
+									"1"
+								],
+								[
+									0,
+									"4466.7196729968955",
+									"4870.992343051145",
+									"1"
+								]
+							]
+							}
+						]
+					]
+				},
+				{
+					"metric":{
+						"foo":"bar"
+					},
+					"histograms":[
+						[
+							2234.567,
+							{
+							"count":"6",
+							"sum":"3897",
+							"buckets":[
+								[
+									1,
+									"-4870.992343051145",
+									"-4466.7196729968955",
+									"1"
+								],
+								[
+									1,
+									"-861.0779292198035",
+									"-789.6119426088657",
+									"1"
+								],
+								[
+									1,
+									"-558.3399591246119",
+									"-512",
+									"1"
+								],
+								[
+									0,
+									"2048",
+									"2233.3598364984477",
+									"1"
+								],
+								[
+									0,
+									"2896.3093757400984",
+									"3158.4477704354626",
+									"1"
+								],
+								[
+									0,
+									"4466.7196729968955",
+									"4870.992343051145",
+									"1"
+								]
+							]
+							}
+						],
+						[
+							22345.678,
+							{
+							"count":"6",
+							"sum":"3897",
+							"buckets":[
+								[
+									1,
+									"-4870.992343051145",
+									"-4466.7196729968955",
+									"1"
+								],
+								[
+									1,
+									"-861.0779292198035",
+									"-789.6119426088657",
+									"1"
+								],
+								[
+									1,
+									"-558.3399591246119",
+									"-512",
+									"1"
+								],
+								[
+									0,
+									"2048",
+									"2233.3598364984477",
+									"1"
+								],
+								[
+									0,
+									"2896.3093757400984",
+									"3158.4477704354626",
+									"1"
+								],
+								[
+									0,
+									"4466.7196729968955",
+									"4870.992343051145",
+									"1"
+								]
+							]
+							}
+						]
+					]
+				}
+			]`,
+			value: Matrix{
+				&SampleStream{
+					Metric: Metric{
+						MetricNameLabel: "test_metric",
+					},
+					Histograms: []SampleHistogramPair{
+						{
+							Histogram: genSampleHistogram(),
+							Timestamp: 1234567,
+						},
+						{
+							Histogram: genSampleHistogram(),
+							Timestamp: 12345678,
+						},
+					},
+				},
+				&SampleStream{
+					Metric: Metric{
+						"foo": "bar",
+					},
+					Histograms: []SampleHistogramPair{
+						{
+							Histogram: genSampleHistogram(),
+							Timestamp: 2234567,
+						},
+						{
+							Histogram: genSampleHistogram(),
+							Timestamp: 22345678,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range input {
+		b, err := json.Marshal(test.value)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		trimmed := noWhitespace.ReplaceAllString(test.plain, "")
+		if string(b) != trimmed {
+			t.Errorf("encoding error: expected %q, got %q", trimmed, b)
+			continue
+		}
+
+		var mat Matrix
+		err = json.Unmarshal(b, &mat)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+
+		if !reflect.DeepEqual(mat, test.value) {
+			t.Errorf("decoding error: expected %v, got %v", test.value, mat)
+		}
+	}
+}
diff --git a/model/value_test.go b/model/value_test.go
index 7936a06..b120264 100644
--- a/model/value_test.go
+++ b/model/value_test.go
@@ -16,66 +16,10 @@ package model
 import (
 	"encoding/json"
 	"math"
-	"reflect"
 	"sort"
 	"testing"
 )
 
-func TestEqualValues(t *testing.T) {
-	tests := map[string]struct {
-		in1, in2 SampleValue
-		want     bool
-	}{
-		"equal floats": {
-			in1:  3.14,
-			in2:  3.14,
-			want: true,
-		},
-		"unequal floats": {
-			in1:  3.14,
-			in2:  3.1415,
-			want: false,
-		},
-		"positive inifinities": {
-			in1:  SampleValue(math.Inf(+1)),
-			in2:  SampleValue(math.Inf(+1)),
-			want: true,
-		},
-		"negative inifinities": {
-			in1:  SampleValue(math.Inf(-1)),
-			in2:  SampleValue(math.Inf(-1)),
-			want: true,
-		},
-		"different inifinities": {
-			in1:  SampleValue(math.Inf(+1)),
-			in2:  SampleValue(math.Inf(-1)),
-			want: false,
-		},
-		"number and infinity": {
-			in1:  42,
-			in2:  SampleValue(math.Inf(+1)),
-			want: false,
-		},
-		"number and NaN": {
-			in1:  42,
-			in2:  SampleValue(math.NaN()),
-			want: false,
-		},
-		"NaNs": {
-			in1:  SampleValue(math.NaN()),
-			in2:  SampleValue(math.NaN()),
-			want: true, // !!!
-		},
-	}
-
-	for name, test := range tests {
-		got := test.in1.Equal(test.in2)
-		if got != test.want {
-			t.Errorf("Comparing %s, %f and %f: got %t, want %t", name, test.in1, test.in2, got, test.want)
-		}
-	}
-}
-
 func TestEqualSamples(t *testing.T) {
 	testSample := &Sample{}
 
@@ -116,161 +60,100 @@ func TestEqualSamples(t *testing.T) {
 			},
 			want: true,
 		},
-	}
-
-	for name, test := range tests {
-		got := test.in1.Equal(test.in2)
-		if got != test.want {
-			t.Errorf("Comparing %s, %v and %v: got %t, want %t", name, test.in1, test.in2, got, test.want)
-		}
-	}
-
-}
-
-func TestSamplePairJSON(t *testing.T) {
-	input := []struct {
-		plain string
-		value SamplePair
-	}{
-		{
-			plain: `[1234.567,"123.1"]`,
-			value: SamplePair{
-				Value:     123.1,
-				Timestamp: 1234567,
+		"equal histograms": {
+			in1: &Sample{
+				Metric:    Metric{"foo": "bar"},
+				Timestamp: 0,
+				Histogram: genSampleHistogramPtr(),
 			},
-		},
-	}
-
-	for _, test := range input {
-		b, err := json.Marshal(test.value)
-		if err != nil {
-			t.Error(err)
-			continue
-		}
-
-		if string(b) != test.plain {
-			t.Errorf("encoding error: expected %q, got %q", test.plain, b)
-			continue
-		}
-
-		var sp SamplePair
-		err = json.Unmarshal(b, &sp)
-		if err != nil {
-			t.Error(err)
-			continue
-		}
-
-		if sp != test.value {
-			t.Errorf("decoding error: expected %v, got %v", test.value, sp)
-		}
-	}
-}
-
-func TestSampleJSON(t *testing.T) {
-	input := []struct {
-		plain string
-		value Sample
-	}{
-		{
-			plain: `{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]}`,
-			value: Sample{
-				Metric: Metric{
-					MetricNameLabel: "test_metric",
-				},
-				Value:     123.1,
-				Timestamp: 1234567,
+			in2: &Sample{
+				Metric:    Metric{"foo": "bar"},
+				Timestamp: 0,
+				Histogram: genSampleHistogramPtr(),
 			},
+			want: true,
 		},
-	}
-
-	for _, test := range input {
-		b, err := json.Marshal(test.value)
-		if err != nil {
-			t.Error(err)
-			continue
-		}
-
-		if string(b) != test.plain {
-			t.Errorf("encoding error: expected %q, got %q", test.plain, b)
-			continue
-		}
-
-		var sv Sample
-		err = json.Unmarshal(b, &sv)
-		if err != nil {
-			t.Error(err)
-			continue
-		}
-
-		if !reflect.DeepEqual(sv, test.value) {
-			t.Errorf("decoding error: expected %v, got %v", test.value, sv)
-		}
-	}
-}
-
-func TestVectorJSON(t *testing.T) {
-	input := []struct {
-		plain string
-		value Vector
-	}{
-		{
-			plain: `[]`,
-			value: Vector{},
-		},
-		{
-			plain: `[{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]}]`,
-			value: Vector{&Sample{
-				Metric: Metric{
-					MetricNameLabel: "test_metric",
+		"different histogram counts": {
+			in1: &Sample{
+				Metric:    Metric{"foo": "bar"},
+				Timestamp: 0,
+				Histogram: &SampleHistogram{
+					Count: 2,
+					Sum:   4500,
+					Buckets: HistogramBuckets{
+						{
+							Boundaries: 0,
+							Lower:      4466.7196729968955,
+							Upper:      4870.992343051145,
+							Count:      1,
+						},
+					},
 				},
-				Value:     123.1,
-				Timestamp: 1234567,
-			}},
+			},
+			in2: &Sample{
+				Metric:    Metric{"foo": "bar"},
+				Timestamp: 0,
+				Histogram: genSampleHistogramPtr(),
+			},
+			want: false,
 		},
-		{
-			plain: `[{"metric":{"__name__":"test_metric"},"value":[1234.567,"123.1"]},{"metric":{"foo":"bar"},"value":[1.234,"+Inf"]}]`,
-			value: Vector{
-				&Sample{
-					Metric: Metric{
-						MetricNameLabel: "test_metric",
+		"different histogram sums": {
+			in1: &Sample{
+				Metric:    Metric{"foo": "bar"},
+				Timestamp: 0,
+				Histogram: &SampleHistogram{
+					Count: 1,
+					Sum:   4500.01,
+					Buckets: HistogramBuckets{
+						{
+							Boundaries: 0,
+							Lower:      4466.7196729968955,
+							Upper:      4870.992343051145,
+							Count:      1,
+						},
 					},
-					Value:     123.1,
-					Timestamp: 1234567,
 				},
-				&Sample{
-					Metric: Metric{
-						"foo": "bar",
+			},
+			in2: &Sample{
+				Metric:    Metric{"foo": "bar"},
+				Timestamp: 0,
+				Histogram: genSampleHistogramPtr(),
+			},
+			want: false,
+		},
+		"different histogram inner counts": {
+			in1: &Sample{
+				Metric:    Metric{"foo": "bar"},
+				Timestamp: 0,
+				Histogram: &SampleHistogram{
+					Count: 1,
+					Sum:   4500,
+					Buckets: HistogramBuckets{
+						{
+							Boundaries: 0,
+							Lower:      4466.7196729968955,
+							Upper:      4870.992343051145,
+							Count:      2,
+						},
 					},
-					Value:     SampleValue(math.Inf(1)),
-					Timestamp: 1234,
 				},
 			},
+			in2: &Sample{
+				Metric:    Metric{"foo": "bar"},
+				Timestamp: 0,
+				Histogram: genSampleHistogramPtr(),
+			},
+			want: false,
 		},
 	}
 
-	for _, test := range input {
-		b, err := json.Marshal(test.value)
-		if err != nil {
-			t.Error(err)
-			continue
-		}
-
-		if string(b) != test.plain {
-			t.Errorf("encoding error: expected %q, got %q", test.plain, b)
-			continue
-		}
-
-		var vec Vector
-		err = json.Unmarshal(b, &vec)
-		if err != nil {
-			t.Error(err)
-			continue
-		}
-
-		if !reflect.DeepEqual(vec, test.value) {
-			t.Errorf("decoding error: expected %v, got %v", test.value, vec)
+	for name, test := range tests {
+		got := test.in1.Equal(test.in2)
+		if got != test.want {
+			t.Errorf("Comparing %s, %v and %v: got %t, want %t", name, test.in1, test.in2, got, test.want)
 		}
 	}
+
 }
 
 func TestScalarJSON(t *testing.T) {