Commit 76325ca8 authored by Alex Robinson's avatar Alex Robinson

Update the version of the prometheus client library we have vendored so

that we can use true histograms rather than the old Summary type that calculated quantiles on the client side.
parent 6c5b3901
...@@ -6,6 +6,11 @@ ...@@ -6,6 +6,11 @@
], ],
"Deps": [ "Deps": [
{ {
"ImportPath": "bitbucket.org/ww/goautoneg",
"Comment": "null-5",
"Rev": "75cd24fc2f2c2a2088577d12123ddee5f54e0675"
},
{
"ImportPath": "code.google.com/p/gcfg", "ImportPath": "code.google.com/p/gcfg",
"Rev": "c2d3050044d05357eaf6c3547249ba57c5e235cb" "Rev": "c2d3050044d05357eaf6c3547249ba57c5e235cb"
}, },
...@@ -45,6 +50,10 @@ ...@@ -45,6 +50,10 @@
"Rev": "51fe59aca108dc5680109e7b2051cbdcfa5a253c" "Rev": "51fe59aca108dc5680109e7b2051cbdcfa5a253c"
}, },
{ {
"ImportPath": "github.com/beorn7/perks/quantile",
"Rev": "b965b613227fddccbfffe13eae360ed3fa822f8d"
},
{
"ImportPath": "github.com/coreos/go-etcd/etcd", "ImportPath": "github.com/coreos/go-etcd/etcd",
"Comment": "v0.2.0-rc1-120-g23142f6", "Comment": "v0.2.0-rc1-120-g23142f6",
"Rev": "23142f6773a676cc2cae8dd0cb90b2ea761c853f" "Rev": "23142f6773a676cc2cae8dd0cb90b2ea761c853f"
...@@ -195,38 +204,28 @@ ...@@ -195,38 +204,28 @@
"Rev": "8adf9e1730c55cdc590de7d49766cb2acc88d8f2" "Rev": "8adf9e1730c55cdc590de7d49766cb2acc88d8f2"
}, },
{ {
"ImportPath": "github.com/prometheus/client_golang/_vendor/goautoneg",
"Comment": "0.1.0-11-gc70db11",
"Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06"
},
{
"ImportPath": "github.com/prometheus/client_golang/_vendor/perks/quantile",
"Comment": "0.1.0-11-gc70db11",
"Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06"
},
{
"ImportPath": "github.com/prometheus/client_golang/model", "ImportPath": "github.com/prometheus/client_golang/model",
"Comment": "0.1.0-11-gc70db11", "Comment": "0.2.0-5-gde5f7a2",
"Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06" "Rev": "de5f7a2db9d883392ce3ad667087280fe1ff9cea"
}, },
{ {
"ImportPath": "github.com/prometheus/client_golang/prometheus", "ImportPath": "github.com/prometheus/client_golang/prometheus",
"Comment": "0.1.0-11-gc70db11", "Comment": "0.2.0-5-gde5f7a2",
"Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06" "Rev": "de5f7a2db9d883392ce3ad667087280fe1ff9cea"
}, },
{ {
"ImportPath": "github.com/prometheus/client_golang/text", "ImportPath": "github.com/prometheus/client_golang/text",
"Comment": "0.1.0-11-gc70db11", "Comment": "0.2.0-5-gde5f7a2",
"Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06" "Rev": "de5f7a2db9d883392ce3ad667087280fe1ff9cea"
}, },
{ {
"ImportPath": "github.com/prometheus/client_model/go", "ImportPath": "github.com/prometheus/client_model/go",
"Comment": "model-0.0.2-10-gbc9454c", "Comment": "model-0.0.2-12-gfa8ad6f",
"Rev": "bc9454ca562dc050e060ea61a1c0e562a189850f" "Rev": "fa8ad6fec33561be4280a8f0514318c79d7f6cb6"
}, },
{ {
"ImportPath": "github.com/prometheus/procfs", "ImportPath": "github.com/prometheus/procfs",
"Rev": "92faa308558161acab0ada1db048e9996ecec160" "Rev": "6c34ef819e19b4e16f410100ace4aa006f0e3bf8"
}, },
{ {
"ImportPath": "github.com/racker/perigee", "ImportPath": "github.com/racker/perigee",
......
...@@ -5,11 +5,12 @@ package quantile_test ...@@ -5,11 +5,12 @@ package quantile_test
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"github.com/bmizerany/perks/quantile"
"log" "log"
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/beorn7/perks/quantile"
) )
func Example_simple() { func Example_simple() {
...@@ -17,7 +18,11 @@ func Example_simple() { ...@@ -17,7 +18,11 @@ func Example_simple() {
go sendFloats(ch) go sendFloats(ch)
// Compute the 50th, 90th, and 99th percentile. // Compute the 50th, 90th, and 99th percentile.
q := quantile.NewTargeted(0.50, 0.90, 0.99) q := quantile.NewTargeted(map[float64]float64{
0.50: 0.005,
0.90: 0.001,
0.99: 0.0001,
})
for v := range ch { for v := range ch {
q.Insert(v) q.Insert(v)
} }
...@@ -28,8 +33,8 @@ func Example_simple() { ...@@ -28,8 +33,8 @@ func Example_simple() {
fmt.Println("count:", q.Count()) fmt.Println("count:", q.Count())
// Output: // Output:
// perc50: 5 // perc50: 5
// perc90: 14 // perc90: 16
// perc99: 40 // perc99: 223
// count: 2388 // count: 2388
} }
...@@ -52,7 +57,7 @@ func Example_mergeMultipleStreams() { ...@@ -52,7 +57,7 @@ func Example_mergeMultipleStreams() {
// even if we do not plan to query them all here. // even if we do not plan to query them all here.
ch := make(chan quantile.Samples) ch := make(chan quantile.Samples)
getDBQuerySamples(ch) getDBQuerySamples(ch)
q := quantile.NewTargeted(0.90) q := quantile.NewTargeted(map[float64]float64{0.90: 0.001})
for samples := range ch { for samples := range ch {
q.Merge(samples) q.Merge(samples)
} }
...@@ -67,7 +72,11 @@ func Example_window() { ...@@ -67,7 +72,11 @@ func Example_window() {
go sendStreamValues(ch) go sendStreamValues(ch)
tick := time.NewTicker(1 * time.Minute) tick := time.NewTicker(1 * time.Minute)
q := quantile.NewTargeted(0.90, 0.95, 0.99) q := quantile.NewTargeted(map[float64]float64{
0.90: 0.001,
0.95: 0.0005,
0.99: 0.0001,
})
for { for {
select { select {
case t := <-tick.C: case t := <-tick.C:
......
...@@ -113,7 +113,8 @@ func TestHighBiasedQuery(t *testing.T) { ...@@ -113,7 +113,8 @@ func TestHighBiasedQuery(t *testing.T) {
verifyHighPercsWithRelativeEpsilon(t, a, s) verifyHighPercsWithRelativeEpsilon(t, a, s)
} }
func TestTargetedMerge(t *testing.T) { // BrokenTestTargetedMerge is broken, see Merge doc comment.
func BrokenTestTargetedMerge(t *testing.T) {
rand.Seed(42) rand.Seed(42)
s1 := NewTargeted(Targets) s1 := NewTargeted(Targets)
s2 := NewTargeted(Targets) s2 := NewTargeted(Targets)
...@@ -123,7 +124,8 @@ func TestTargetedMerge(t *testing.T) { ...@@ -123,7 +124,8 @@ func TestTargetedMerge(t *testing.T) {
verifyPercsWithAbsoluteEpsilon(t, a, s1) verifyPercsWithAbsoluteEpsilon(t, a, s1)
} }
func TestLowBiasedMerge(t *testing.T) { // BrokenTestLowBiasedMerge is broken, see Merge doc comment.
func BrokenTestLowBiasedMerge(t *testing.T) {
rand.Seed(42) rand.Seed(42)
s1 := NewLowBiased(RelativeEpsilon) s1 := NewLowBiased(RelativeEpsilon)
s2 := NewLowBiased(RelativeEpsilon) s2 := NewLowBiased(RelativeEpsilon)
...@@ -133,7 +135,8 @@ func TestLowBiasedMerge(t *testing.T) { ...@@ -133,7 +135,8 @@ func TestLowBiasedMerge(t *testing.T) {
verifyLowPercsWithRelativeEpsilon(t, a, s2) verifyLowPercsWithRelativeEpsilon(t, a, s2)
} }
func TestHighBiasedMerge(t *testing.T) { // BrokenTestHighBiasedMerge is broken, see Merge doc comment.
func BrokenTestHighBiasedMerge(t *testing.T) {
rand.Seed(42) rand.Seed(42)
s1 := NewHighBiased(RelativeEpsilon) s1 := NewHighBiased(RelativeEpsilon)
s2 := NewHighBiased(RelativeEpsilon) s2 := NewHighBiased(RelativeEpsilon)
......
Imported at 75cd24fc2f2c from https://bitbucket.org/ww/goautoneg.
...@@ -33,6 +33,14 @@ const ( ...@@ -33,6 +33,14 @@ const (
// JobLabel is the label name indicating the job from which a timeseries // JobLabel is the label name indicating the job from which a timeseries
// was scraped. // was scraped.
JobLabel LabelName = "job" JobLabel LabelName = "job"
// BucketLabel is used for the label that defines the upper bound of a
// bucket of a histogram ("le" -> "less or equal").
BucketLabel = "le"
// QuantileLabel is used for the label that defines the quantile in a
// summary.
QuantileLabel = "quantile"
) )
// A LabelName is a key for a LabelSet or Metric. It has a value associated // A LabelName is a key for a LabelSet or Metric. It has a value associated
......
...@@ -22,6 +22,8 @@ import ( ...@@ -22,6 +22,8 @@ import (
"strings" "strings"
) )
var separator = []byte{0}
// A Metric is similar to a LabelSet, but the key difference is that a Metric is // 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. // a singleton and refers to one and only one stream of samples.
type Metric map[LabelName]LabelValue type Metric map[LabelName]LabelValue
...@@ -64,23 +66,34 @@ func (m Metric) String() string { ...@@ -64,23 +66,34 @@ func (m Metric) String() string {
// Fingerprint returns a Metric's Fingerprint. // Fingerprint returns a Metric's Fingerprint.
func (m Metric) Fingerprint() Fingerprint { func (m Metric) Fingerprint() Fingerprint {
labelLength := len(m) labelNames := make([]string, 0, len(m))
labelNames := make([]string, 0, labelLength) maxLength := 0
for labelName := range m { for labelName, labelValue := range m {
labelNames = append(labelNames, string(labelName)) labelNames = append(labelNames, string(labelName))
if len(labelName) > maxLength {
maxLength = len(labelName)
}
if len(labelValue) > maxLength {
maxLength = len(labelValue)
}
} }
sort.Strings(labelNames) sort.Strings(labelNames)
summer := fnv.New64a() summer := fnv.New64a()
buf := make([]byte, maxLength)
for _, labelName := range labelNames { for _, labelName := range labelNames {
labelValue := m[LabelName(labelName)] labelValue := m[LabelName(labelName)]
summer.Write([]byte(labelName)) copy(buf, labelName)
summer.Write([]byte{0}) summer.Write(buf[:len(labelName)])
summer.Write([]byte(labelValue))
summer.Write(separator)
copy(buf, labelValue)
summer.Write(buf[:len(labelValue)])
} }
return Fingerprint(binary.LittleEndian.Uint64(summer.Sum(nil))) return Fingerprint(binary.LittleEndian.Uint64(summer.Sum(nil)))
......
...@@ -38,6 +38,24 @@ func testMetric(t testing.TB) { ...@@ -38,6 +38,24 @@ func testMetric(t testing.TB) {
}, },
fingerprint: 1470933794305433534, fingerprint: 1470933794305433534,
}, },
// The following two demonstrate a bug in fingerprinting. They
// should not have the same fingerprint with a sane
// fingerprinting function. See
// https://github.com/prometheus/client_golang/issues/74 .
{
input: Metric{
"a": "bb",
"b": "c",
},
fingerprint: 3734646176939799877,
},
{
input: Metric{
"a": "b",
"bb": "c",
},
fingerprint: 3734646176939799877,
},
} }
for i, scenario := range scenarios { for i, scenario := range scenarios {
......
...@@ -14,18 +14,14 @@ ...@@ -14,18 +14,14 @@
package model package model
import ( import (
"math"
"strconv" "strconv"
native_time "time" native_time "time"
) )
// TODO(julius): Should this use milliseconds/nanoseconds instead? This is // Timestamp is the number of milliseconds since the epoch
// mostly hidden from the user of these types when using the // (1970-01-01 00:00 UTC) excluding leap seconds.
// methods below, so it will be easy to change this later
// without requiring significant user code changes.
// Timestamp is the number of seconds since the epoch (1970-01-01 00:00 UTC)
// without leap seconds.
type Timestamp int64 type Timestamp int64
const ( const (
...@@ -36,6 +32,13 @@ const ( ...@@ -36,6 +32,13 @@ const (
second = int64(native_time.Second / MinimumTick) second = int64(native_time.Second / MinimumTick)
// The number of nanoseconds per minimum tick. // The number of nanoseconds per minimum tick.
nanosPerTick = int64(MinimumTick / native_time.Nanosecond) nanosPerTick = int64(MinimumTick / native_time.Nanosecond)
// Earliest is the earliest timestamp representable. Handy for
// initializing a high watermark.
Earliest = Timestamp(math.MinInt64)
// Latest is the latest timestamp representable. Handy for initializing
// a low watermark.
Latest = Timestamp(math.MaxInt64)
) )
// Equal reports whether two timestamps represent the same instant. // Equal reports whether two timestamps represent the same instant.
......
...@@ -29,9 +29,10 @@ var ( ...@@ -29,9 +29,10 @@ var (
Namespace: "my_company", Namespace: "my_company",
Subsystem: "storage", Subsystem: "storage",
Name: "documents_total_size_bytes", Name: "documents_total_size_bytes",
Help: "The total size of all documents in the storage."}}) Help: "The total size of all documents in the storage.",
})
) )
func main() { func main() {
http.Handle("/metrics", prometheus.Handler()) http.Handle("/metrics", prometheus.Handler())
...@@ -50,4 +51,3 @@ func init() { ...@@ -50,4 +51,3 @@ func init() {
# Documentation # Documentation
[![GoDoc](https://godoc.org/github.com/prometheus/client_golang?status.png)](https://godoc.org/github.com/prometheus/client_golang) [![GoDoc](https://godoc.org/github.com/prometheus/client_golang?status.png)](https://godoc.org/github.com/prometheus/client_golang)
...@@ -129,3 +129,31 @@ func BenchmarkSummaryNoLabels(b *testing.B) { ...@@ -129,3 +129,31 @@ func BenchmarkSummaryNoLabels(b *testing.B) {
m.Observe(3.1415) m.Observe(3.1415)
} }
} }
func BenchmarkHistogramWithLabelValues(b *testing.B) {
m := NewHistogramVec(
HistogramOpts{
Name: "benchmark_histogram",
Help: "A histogram to benchmark it.",
},
[]string{"one", "two", "three"},
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.WithLabelValues("eins", "zwei", "drei").Observe(3.1415)
}
}
func BenchmarkHistogramNoLabels(b *testing.B) {
m := NewHistogram(HistogramOpts{
Name: "benchmark_histogram",
Help: "A histogram to benchmark it.",
},
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Observe(3.1415)
}
}
...@@ -74,7 +74,7 @@ func (c *counter) Add(v float64) { ...@@ -74,7 +74,7 @@ func (c *counter) Add(v float64) {
// CounterVec is a Collector that bundles a set of Counters that all share the // CounterVec is a Collector that bundles a set of Counters that all share the
// same Desc, but have different values for their variable labels. This is used // same Desc, but have different values for their variable labels. This is used
// if you want to count the same thing partitioned by various dimensions // if you want to count the same thing partitioned by various dimensions
// (e.g. number of http requests, partitioned by response code and // (e.g. number of HTTP requests, partitioned by response code and
// method). Create instances with NewCounterVec. // method). Create instances with NewCounterVec.
// //
// CounterVec embeds MetricVec. See there for a full list of methods with // CounterVec embeds MetricVec. See there for a full list of methods with
......
...@@ -13,7 +13,7 @@ import ( ...@@ -13,7 +13,7 @@ import (
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto" "github.com/golang/protobuf/proto"
) )
var ( var (
......
...@@ -16,7 +16,7 @@ package prometheus_test ...@@ -16,7 +16,7 @@ package prometheus_test
import ( import (
"runtime" "runtime"
"code.google.com/p/goprotobuf/proto" "github.com/golang/protobuf/proto"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
......
...@@ -23,7 +23,7 @@ import ( ...@@ -23,7 +23,7 @@ import (
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto" "github.com/golang/protobuf/proto"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
...@@ -129,7 +129,7 @@ func ExampleCounterVec() { ...@@ -129,7 +129,7 @@ func ExampleCounterVec() {
httpReqs := prometheus.NewCounterVec( httpReqs := prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "http_requests_total", Name: "http_requests_total",
Help: "How many HTTP requests processed, partitioned by status code and http method.", Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
ConstLabels: prometheus.Labels{"env": *binaryVersion}, ConstLabels: prometheus.Labels{"env": *binaryVersion},
}, },
[]string{"code", "method"}, []string{"code", "method"},
...@@ -200,7 +200,7 @@ func ExampleRegister() { ...@@ -200,7 +200,7 @@ func ExampleRegister() {
fmt.Println("taskCounter registered.") fmt.Println("taskCounter registered.")
} }
// Don't forget to tell the HTTP server about the Prometheus handler. // Don't forget to tell the HTTP server about the Prometheus handler.
// (In a real program, you still need to start the http server...) // (In a real program, you still need to start the HTTP server...)
http.Handle("/metrics", prometheus.Handler()) http.Handle("/metrics", prometheus.Handler())
// Now you can start workers and give every one of them a pointer to // Now you can start workers and give every one of them a pointer to
...@@ -240,7 +240,7 @@ func ExampleRegister() { ...@@ -240,7 +240,7 @@ func ExampleRegister() {
// Prometheus will not allow you to ever export metrics with // Prometheus will not allow you to ever export metrics with
// inconsistent help strings or label names. After unregistering, the // inconsistent help strings or label names. After unregistering, the
// unregistered metrics will cease to show up in the /metrics http // unregistered metrics will cease to show up in the /metrics HTTP
// response, but the registry still remembers that those metrics had // response, but the registry still remembers that those metrics had
// been exported before. For this example, we will now choose a // been exported before. For this example, we will now choose a
// different name. (In a real program, you would obviously not export // different name. (In a real program, you would obviously not export
...@@ -452,3 +452,49 @@ func ExampleSummaryVec() { ...@@ -452,3 +452,49 @@ func ExampleSummaryVec() {
// > // >
// ] // ]
} }
func ExampleHistogram() {
temps := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "pond_temperature_celsius",
Help: "The temperature of the frog pond.", // Sorry, we can't measure how badly it smells.
Buckets: prometheus.LinearBuckets(20, 5, 5), // 5 buckets, each 5 centigrade wide.
})
// Simulate some observations.
for i := 0; i < 1000; i++ {
temps.Observe(30 + math.Floor(120*math.Sin(float64(i)*0.1))/10)
}
// Just for demonstration, let's check the state of the histogram by
// (ab)using its Write method (which is usually only used by Prometheus
// internally).
metric := &dto.Metric{}
temps.Write(metric)
fmt.Println(proto.MarshalTextString(metric))
// Output:
// histogram: <
// sample_count: 1000
// sample_sum: 29969.50000000001
// bucket: <
// cumulative_count: 192
// upper_bound: 20
// >
// bucket: <
// cumulative_count: 366
// upper_bound: 25
// >
// bucket: <
// cumulative_count: 501
// upper_bound: 30
// >
// bucket: <
// cumulative_count: 638
// upper_bound: 35
// >
// bucket: <
// cumulative_count: 816
// upper_bound: 40
// >
// >
}
// Copyright 2015 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 prometheus
import (
"math"
"math/rand"
"reflect"
"sort"
"sync"
"testing"
"testing/quick"
dto "github.com/prometheus/client_model/go"
)
func benchmarkHistogramObserve(w int, b *testing.B) {
b.StopTimer()
wg := new(sync.WaitGroup)
wg.Add(w)
g := new(sync.WaitGroup)
g.Add(1)
s := NewHistogram(HistogramOpts{})
for i := 0; i < w; i++ {
go func() {
g.Wait()
for i := 0; i < b.N; i++ {
s.Observe(float64(i))
}
wg.Done()
}()
}
b.StartTimer()
g.Done()
wg.Wait()
}
func BenchmarkHistogramObserve1(b *testing.B) {
benchmarkHistogramObserve(1, b)
}
func BenchmarkHistogramObserve2(b *testing.B) {
benchmarkHistogramObserve(2, b)
}
func BenchmarkHistogramObserve4(b *testing.B) {
benchmarkHistogramObserve(4, b)
}
func BenchmarkHistogramObserve8(b *testing.B) {
benchmarkHistogramObserve(8, b)
}
func benchmarkHistogramWrite(w int, b *testing.B) {
b.StopTimer()
wg := new(sync.WaitGroup)
wg.Add(w)
g := new(sync.WaitGroup)
g.Add(1)
s := NewHistogram(HistogramOpts{})
for i := 0; i < 1000000; i++ {
s.Observe(float64(i))
}
for j := 0; j < w; j++ {
outs := make([]dto.Metric, b.N)
go func(o []dto.Metric) {
g.Wait()
for i := 0; i < b.N; i++ {
s.Write(&o[i])
}
wg.Done()
}(outs)
}
b.StartTimer()
g.Done()
wg.Wait()
}
func BenchmarkHistogramWrite1(b *testing.B) {
benchmarkHistogramWrite(1, b)
}
func BenchmarkHistogramWrite2(b *testing.B) {
benchmarkHistogramWrite(2, b)
}
func BenchmarkHistogramWrite4(b *testing.B) {
benchmarkHistogramWrite(4, b)
}
func BenchmarkHistogramWrite8(b *testing.B) {
benchmarkHistogramWrite(8, b)
}
// Intentionally adding +Inf here to test if that case is handled correctly.
// Also, getCumulativeCounts depends on it.
var testBuckets = []float64{-2, -1, -0.5, 0, 0.5, 1, 2, math.Inf(+1)}
func TestHistogramConcurrency(t *testing.T) {
rand.Seed(42)
it := func(n uint32) bool {
mutations := int(n%1e4 + 1e4)
concLevel := int(n%5 + 1)
total := mutations * concLevel
var start, end sync.WaitGroup
start.Add(1)
end.Add(concLevel)
sum := NewHistogram(HistogramOpts{
Name: "test_histogram",
Help: "helpless",
Buckets: testBuckets,
})
allVars := make([]float64, total)
var sampleSum float64
for i := 0; i < concLevel; i++ {
vals := make([]float64, mutations)
for j := 0; j < mutations; j++ {
v := rand.NormFloat64()
vals[j] = v
allVars[i*mutations+j] = v
sampleSum += v
}
go func(vals []float64) {
start.Wait()
for _, v := range vals {
sum.Observe(v)
}
end.Done()
}(vals)
}
sort.Float64s(allVars)
start.Done()
end.Wait()
m := &dto.Metric{}
sum.Write(m)
if got, want := int(*m.Histogram.SampleCount), total; got != want {
t.Errorf("got sample count %d, want %d", got, want)
}
if got, want := *m.Histogram.SampleSum, sampleSum; math.Abs((got-want)/want) > 0.001 {
t.Errorf("got sample sum %f, want %f", got, want)
}
wantCounts := getCumulativeCounts(allVars)
if got, want := len(m.Histogram.Bucket), len(testBuckets)-1; got != want {
t.Errorf("got %d buckets in protobuf, want %d", got, want)
}
for i, wantBound := range testBuckets {
if i == len(testBuckets)-1 {
break // No +Inf bucket in protobuf.
}
if gotBound := *m.Histogram.Bucket[i].UpperBound; gotBound != wantBound {
t.Errorf("got bound %f, want %f", gotBound, wantBound)
}
if gotCount, wantCount := *m.Histogram.Bucket[i].CumulativeCount, wantCounts[i]; gotCount != wantCount {
t.Errorf("got count %d, want %d", gotCount, wantCount)
}
}
return true
}
if err := quick.Check(it, nil); err != nil {
t.Error(err)
}
}
func TestHistogramVecConcurrency(t *testing.T) {
rand.Seed(42)
objectives := make([]float64, 0, len(DefObjectives))
for qu := range DefObjectives {
objectives = append(objectives, qu)
}
sort.Float64s(objectives)
it := func(n uint32) bool {
mutations := int(n%1e4 + 1e4)
concLevel := int(n%7 + 1)
vecLength := int(n%3 + 1)
var start, end sync.WaitGroup
start.Add(1)
end.Add(concLevel)
his := NewHistogramVec(
HistogramOpts{
Name: "test_histogram",
Help: "helpless",
Buckets: []float64{-2, -1, -0.5, 0, 0.5, 1, 2, math.Inf(+1)},
},
[]string{"label"},
)
allVars := make([][]float64, vecLength)
sampleSums := make([]float64, vecLength)
for i := 0; i < concLevel; i++ {
vals := make([]float64, mutations)
picks := make([]int, mutations)
for j := 0; j < mutations; j++ {
v := rand.NormFloat64()
vals[j] = v
pick := rand.Intn(vecLength)
picks[j] = pick
allVars[pick] = append(allVars[pick], v)
sampleSums[pick] += v
}
go func(vals []float64) {
start.Wait()
for i, v := range vals {
his.WithLabelValues(string('A' + picks[i])).Observe(v)
}
end.Done()
}(vals)
}
for _, vars := range allVars {
sort.Float64s(vars)
}
start.Done()
end.Wait()
for i := 0; i < vecLength; i++ {
m := &dto.Metric{}
s := his.WithLabelValues(string('A' + i))
s.Write(m)
if got, want := len(m.Histogram.Bucket), len(testBuckets)-1; got != want {
t.Errorf("got %d buckets in protobuf, want %d", got, want)
}
if got, want := int(*m.Histogram.SampleCount), len(allVars[i]); got != want {
t.Errorf("got sample count %d, want %d", got, want)
}
if got, want := *m.Histogram.SampleSum, sampleSums[i]; math.Abs((got-want)/want) > 0.001 {
t.Errorf("got sample sum %f, want %f", got, want)
}
wantCounts := getCumulativeCounts(allVars[i])
for j, wantBound := range testBuckets {
if j == len(testBuckets)-1 {
break // No +Inf bucket in protobuf.
}
if gotBound := *m.Histogram.Bucket[j].UpperBound; gotBound != wantBound {
t.Errorf("got bound %f, want %f", gotBound, wantBound)
}
if gotCount, wantCount := *m.Histogram.Bucket[j].CumulativeCount, wantCounts[j]; gotCount != wantCount {
t.Errorf("got count %d, want %d", gotCount, wantCount)
}
}
}
return true
}
if err := quick.Check(it, nil); err != nil {
t.Error(err)
}
}
func getCumulativeCounts(vars []float64) []uint64 {
counts := make([]uint64, len(testBuckets))
for _, v := range vars {
for i := len(testBuckets) - 1; i >= 0; i-- {
if v > testBuckets[i] {
break
}
counts[i]++
}
}
return counts
}
func TestBuckets(t *testing.T) {
got := LinearBuckets(-15, 5, 6)
want := []float64{-15, -10, -5, 0, 5, 10}
if !reflect.DeepEqual(got, want) {
t.Errorf("linear buckets: got %v, want %v", got, want)
}
got = ExponentialBuckets(100, 1.2, 3)
want = []float64{100, 120, 144}
if !reflect.DeepEqual(got, want) {
t.Errorf("linear buckets: got %v, want %v", got, want)
}
}
...@@ -47,7 +47,7 @@ func nowSeries(t ...time.Time) nower { ...@@ -47,7 +47,7 @@ func nowSeries(t ...time.Time) nower {
} }
// InstrumentHandler wraps the given HTTP handler for instrumentation. It // InstrumentHandler wraps the given HTTP handler for instrumentation. It
// registers four metric collectors (if not already done) and reports http // registers four metric collectors (if not already done) and reports HTTP
// metrics to the (newly or already) registered collectors: http_requests_total // metrics to the (newly or already) registered collectors: http_requests_total
// (CounterVec), http_request_duration_microseconds (Summary), // (CounterVec), http_request_duration_microseconds (Summary),
// http_request_size_bytes (Summary), http_response_size_bytes (Summary). Each // http_request_size_bytes (Summary), http_response_size_bytes (Summary). Each
......
...@@ -24,26 +24,20 @@ func processCollectSupported() bool { ...@@ -24,26 +24,20 @@ func processCollectSupported() bool {
return false return false
} }
// TODO(ts): Bring back error reporting by reverting 7faf9e7 as soon as the
// client allows users to configure the error behavior.
func (c *processCollector) processCollect(ch chan<- Metric) { func (c *processCollector) processCollect(ch chan<- Metric) {
pid, err := c.pidFn() pid, err := c.pidFn()
if err != nil { if err != nil {
c.reportCollectErrors(ch, err)
return return
} }
p, err := procfs.NewProc(pid) p, err := procfs.NewProc(pid)
if err != nil { if err != nil {
c.reportCollectErrors(ch, err)
return return
} }
if stat, err := p.NewStat(); err != nil { if stat, err := p.NewStat(); err == nil {
// Report collect errors for metrics depending on stat.
ch <- NewInvalidMetric(c.vsize.Desc(), err)
ch <- NewInvalidMetric(c.rss.Desc(), err)
ch <- NewInvalidMetric(c.startTime.Desc(), err)
ch <- NewInvalidMetric(c.cpuTotal.Desc(), err)
} else {
c.cpuTotal.Set(stat.CPUTime()) c.cpuTotal.Set(stat.CPUTime())
ch <- c.cpuTotal ch <- c.cpuTotal
c.vsize.Set(float64(stat.VirtualMemory())) c.vsize.Set(float64(stat.VirtualMemory()))
...@@ -51,34 +45,19 @@ func (c *processCollector) processCollect(ch chan<- Metric) { ...@@ -51,34 +45,19 @@ func (c *processCollector) processCollect(ch chan<- Metric) {
c.rss.Set(float64(stat.ResidentMemory())) c.rss.Set(float64(stat.ResidentMemory()))
ch <- c.rss ch <- c.rss
if startTime, err := stat.StartTime(); err != nil { if startTime, err := stat.StartTime(); err == nil {
ch <- NewInvalidMetric(c.startTime.Desc(), err)
} else {
c.startTime.Set(startTime) c.startTime.Set(startTime)
ch <- c.startTime ch <- c.startTime
} }
} }
if fds, err := p.FileDescriptorsLen(); err != nil { if fds, err := p.FileDescriptorsLen(); err == nil {
ch <- NewInvalidMetric(c.openFDs.Desc(), err)
} else {
c.openFDs.Set(float64(fds)) c.openFDs.Set(float64(fds))
ch <- c.openFDs ch <- c.openFDs
} }
if limits, err := p.NewLimits(); err != nil { if limits, err := p.NewLimits(); err == nil {
ch <- NewInvalidMetric(c.maxFDs.Desc(), err)
} else {
c.maxFDs.Set(float64(limits.OpenFiles)) c.maxFDs.Set(float64(limits.OpenFiles))
ch <- c.maxFDs ch <- c.maxFDs
} }
} }
func (c *processCollector) reportCollectErrors(ch chan<- Metric, err error) {
ch <- NewInvalidMetric(c.cpuTotal.Desc(), err)
ch <- NewInvalidMetric(c.openFDs.Desc(), err)
ch <- NewInvalidMetric(c.maxFDs.Desc(), err)
ch <- NewInvalidMetric(c.vsize.Desc(), err)
ch <- NewInvalidMetric(c.rss.Desc(), err)
ch <- NewInvalidMetric(c.startTime.Desc(), err)
}
...@@ -33,11 +33,11 @@ import ( ...@@ -33,11 +33,11 @@ import (
"strings" "strings"
"sync" "sync"
dto "github.com/prometheus/client_model/go" "bitbucket.org/ww/goautoneg"
"github.com/golang/protobuf/proto"
"code.google.com/p/goprotobuf/proto" dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/_vendor/goautoneg"
"github.com/prometheus/client_golang/model" "github.com/prometheus/client_golang/model"
"github.com/prometheus/client_golang/text" "github.com/prometheus/client_golang/text"
) )
...@@ -171,7 +171,7 @@ func SetMetricFamilyInjectionHook(hook func() []*dto.MetricFamily) { ...@@ -171,7 +171,7 @@ func SetMetricFamilyInjectionHook(hook func() []*dto.MetricFamily) {
} }
// PanicOnCollectError sets the behavior whether a panic is caused upon an error // PanicOnCollectError sets the behavior whether a panic is caused upon an error
// while metrics are collected and served to the http endpoint. By default, an // while metrics are collected and served to the HTTP endpoint. By default, an
// internal server error (status code 500) is served with an error message. // internal server error (status code 500) is served with an error message.
func PanicOnCollectError(b bool) { func PanicOnCollectError(b bool) {
defRegistry.panicOnCollectError = b defRegistry.panicOnCollectError = b
...@@ -464,6 +464,8 @@ func (r *registry) writePB(w io.Writer, writeEncoded encoder) (int, error) { ...@@ -464,6 +464,8 @@ func (r *registry) writePB(w io.Writer, writeEncoded encoder) (int, error) {
metricFamily.Type = dto.MetricType_SUMMARY.Enum() metricFamily.Type = dto.MetricType_SUMMARY.Enum()
case dtoMetric.Untyped != nil: case dtoMetric.Untyped != nil:
metricFamily.Type = dto.MetricType_UNTYPED.Enum() metricFamily.Type = dto.MetricType_UNTYPED.Enum()
case dtoMetric.Histogram != nil:
metricFamily.Type = dto.MetricType_HISTOGRAM.Enum()
default: default:
return 0, fmt.Errorf("empty metric collected: %s", dtoMetric) return 0, fmt.Errorf("empty metric collected: %s", dtoMetric)
} }
......
...@@ -25,7 +25,7 @@ import ( ...@@ -25,7 +25,7 @@ import (
"net/http" "net/http"
"testing" "testing"
"code.google.com/p/goprotobuf/proto" "github.com/golang/protobuf/proto"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
) )
......
...@@ -20,11 +20,12 @@ import ( ...@@ -20,11 +20,12 @@ import (
"sync" "sync"
"time" "time"
"code.google.com/p/goprotobuf/proto" "github.com/beorn7/perks/quantile"
"github.com/golang/protobuf/proto"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/_vendor/perks/quantile" "github.com/prometheus/client_golang/model"
) )
// A Summary captures individual observations from an event or sample stream and // A Summary captures individual observations from an event or sample stream and
...@@ -35,6 +36,12 @@ import ( ...@@ -35,6 +36,12 @@ import (
// Summary provides the median, the 90th and the 99th percentile of the latency // Summary provides the median, the 90th and the 99th percentile of the latency
// as rank estimations. // as rank estimations.
// //
// Note that the rank estimations cannot be aggregated in a meaningful way with
// the Prometheus query language (i.e. you cannot average or add them). If you
// need aggregatable quantiles (e.g. you want the 99th percentile latency of all
// queries served across all instances of a service), consider the Histogram
// metric type. See the Prometheus documentation for more details.
//
// To create Summary instances, use NewSummary. // To create Summary instances, use NewSummary.
type Summary interface { type Summary interface {
Metric Metric
...@@ -44,9 +51,13 @@ type Summary interface { ...@@ -44,9 +51,13 @@ type Summary interface {
Observe(float64) Observe(float64)
} }
// DefObjectives are the default Summary quantile values.
var ( var (
// DefObjectives are the default Summary quantile values.
DefObjectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001} DefObjectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}
errQuantileLabelNotAllowed = fmt.Errorf(
"%q is not allowed as label name in summaries", model.QuantileLabel,
)
) )
// Default values for SummaryOpts. // Default values for SummaryOpts.
...@@ -110,7 +121,10 @@ type SummaryOpts struct { ...@@ -110,7 +121,10 @@ type SummaryOpts struct {
// AgeBuckets is the number of buckets used to exclude observations that // AgeBuckets is the number of buckets used to exclude observations that
// are older than MaxAge from the summary. A higher number has a // are older than MaxAge from the summary. A higher number has a
// resource penalty, so only increase it if the higher resolution is // resource penalty, so only increase it if the higher resolution is
// really required. The default value is DefAgeBuckets. // really required. For very high observation rates, you might want to
// reduce the number of age buckets. With only one age bucket, you will
// effectively see a complete reset of the summary each time MaxAge has
// passed. The default value is DefAgeBuckets.
AgeBuckets uint32 AgeBuckets uint32
// BufCap defines the default sample stream buffer size. The default // BufCap defines the default sample stream buffer size. The default
...@@ -119,10 +133,6 @@ type SummaryOpts struct { ...@@ -119,10 +133,6 @@ type SummaryOpts struct {
// is the internal buffer size of the underlying package // is the internal buffer size of the underlying package
// "github.com/bmizerany/perks/quantile"). // "github.com/bmizerany/perks/quantile").
BufCap uint32 BufCap uint32
// Epsilon is the error epsilon for the quantile rank estimate. Must be
// positive. The default is DefEpsilon.
Epsilon float64
} }
// TODO: Great fuck-up with the sliding-window decay algorithm... The Merge // TODO: Great fuck-up with the sliding-window decay algorithm... The Merge
...@@ -158,6 +168,17 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary { ...@@ -158,6 +168,17 @@ func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary {
panic(errInconsistentCardinality) panic(errInconsistentCardinality)
} }
for _, n := range desc.variableLabels {
if n == model.QuantileLabel {
panic(errQuantileLabelNotAllowed)
}
}
for _, lp := range desc.constLabelPairs {
if lp.GetName() == model.QuantileLabel {
panic(errQuantileLabelNotAllowed)
}
}
if len(opts.Objectives) == 0 { if len(opts.Objectives) == 0 {
opts.Objectives = DefObjectives opts.Objectives = DefObjectives
} }
...@@ -358,7 +379,7 @@ func (s quantSort) Less(i, j int) bool { ...@@ -358,7 +379,7 @@ func (s quantSort) Less(i, j int) bool {
// SummaryVec is a Collector that bundles a set of Summaries that all share the // SummaryVec is a Collector that bundles a set of Summaries that all share the
// same Desc, but have different values for their variable labels. This is used // same Desc, but have different values for their variable labels. This is used
// if you want to count the same thing partitioned by various dimensions // if you want to count the same thing partitioned by various dimensions
// (e.g. http request latencies, partitioned by status code and method). Create // (e.g. HTTP request latencies, partitioned by status code and method). Create
// instances with NewSummaryVec. // instances with NewSummaryVec.
type SummaryVec struct { type SummaryVec struct {
MetricVec MetricVec
...@@ -411,14 +432,14 @@ func (m *SummaryVec) GetMetricWith(labels Labels) (Summary, error) { ...@@ -411,14 +432,14 @@ func (m *SummaryVec) GetMetricWith(labels Labels) (Summary, error) {
// WithLabelValues works as GetMetricWithLabelValues, but panics where // WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. By not returning an // GetMetricWithLabelValues would have returned an error. By not returning an
// error, WithLabelValues allows shortcuts like // error, WithLabelValues allows shortcuts like
// myVec.WithLabelValues("404", "GET").Add(42) // myVec.WithLabelValues("404", "GET").Observe(42.21)
func (m *SummaryVec) WithLabelValues(lvs ...string) Summary { func (m *SummaryVec) WithLabelValues(lvs ...string) Summary {
return m.MetricVec.WithLabelValues(lvs...).(Summary) return m.MetricVec.WithLabelValues(lvs...).(Summary)
} }
// With works as GetMetricWith, but panics where GetMetricWithLabels would have // With works as GetMetricWith, but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like // returned an error. By not returning an error, With allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42) // myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21)
func (m *SummaryVec) With(labels Labels) Summary { func (m *SummaryVec) With(labels Labels) Summary {
return m.MetricVec.With(labels).(Summary) return m.MetricVec.With(labels).(Summary)
} }
...@@ -120,6 +120,10 @@ func BenchmarkSummaryWrite8(b *testing.B) { ...@@ -120,6 +120,10 @@ func BenchmarkSummaryWrite8(b *testing.B) {
} }
func TestSummaryConcurrency(t *testing.T) { func TestSummaryConcurrency(t *testing.T) {
if testing.Short() {
t.Skip("Skipping test in short mode.")
}
rand.Seed(42) rand.Seed(42)
it := func(n uint32) bool { it := func(n uint32) bool {
...@@ -195,6 +199,10 @@ func TestSummaryConcurrency(t *testing.T) { ...@@ -195,6 +199,10 @@ func TestSummaryConcurrency(t *testing.T) {
} }
func TestSummaryVecConcurrency(t *testing.T) { func TestSummaryVecConcurrency(t *testing.T) {
if testing.Short() {
t.Skip("Skipping test in short mode.")
}
rand.Seed(42) rand.Seed(42)
objectives := make([]float64, 0, len(DefObjectives)) objectives := make([]float64, 0, len(DefObjectives))
......
...@@ -22,7 +22,7 @@ import ( ...@@ -22,7 +22,7 @@ import (
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto" "github.com/golang/protobuf/proto"
) )
// ValueType is an enumeration of metric types that represent a simple value. // ValueType is an enumeration of metric types that represent a simple value.
......
...@@ -24,8 +24,10 @@ import ( ...@@ -24,8 +24,10 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"math"
"strings" "strings"
"github.com/prometheus/client_golang/model"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
) )
...@@ -116,7 +118,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) { ...@@ -116,7 +118,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
for _, q := range metric.Summary.Quantile { for _, q := range metric.Summary.Quantile {
n, err = writeSample( n, err = writeSample(
name, metric, name, metric,
"quantile", fmt.Sprint(q.GetQuantile()), model.QuantileLabel, fmt.Sprint(q.GetQuantile()),
q.GetValue(), q.GetValue(),
out, out,
) )
...@@ -139,6 +141,54 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) { ...@@ -139,6 +141,54 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
float64(metric.Summary.GetSampleCount()), float64(metric.Summary.GetSampleCount()),
out, out,
) )
case dto.MetricType_HISTOGRAM:
if metric.Histogram == nil {
return written, fmt.Errorf(
"expected summary in metric %s", metric,
)
}
infSeen := false
for _, q := range metric.Histogram.Bucket {
n, err = writeSample(
name+"_bucket", metric,
model.BucketLabel, fmt.Sprint(q.GetUpperBound()),
float64(q.GetCumulativeCount()),
out,
)
written += n
if err != nil {
return written, err
}
if math.IsInf(q.GetUpperBound(), +1) {
infSeen = true
}
}
if !infSeen {
n, err = writeSample(
name+"_bucket", metric,
model.BucketLabel, "+Inf",
float64(metric.Histogram.GetSampleCount()),
out,
)
if err != nil {
return written, err
}
written += n
}
n, err = writeSample(
name+"_sum", metric, "", "",
metric.Histogram.GetSampleSum(),
out,
)
if err != nil {
return written, err
}
written += n
n, err = writeSample(
name+"_count", metric, "", "",
float64(metric.Histogram.GetSampleCount()),
out,
)
default: default:
return written, fmt.Errorf( return written, fmt.Errorf(
"unexpected type in metric %s", metric, "unexpected type in metric %s", metric,
......
...@@ -19,7 +19,7 @@ import ( ...@@ -19,7 +19,7 @@ import (
"strings" "strings"
"testing" "testing"
"code.google.com/p/goprotobuf/proto" "github.com/golang/protobuf/proto"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
) )
...@@ -221,6 +221,98 @@ summary_name_sum{name_1="value 1",name_2="value 2"} 2010.1971 ...@@ -221,6 +221,98 @@ summary_name_sum{name_1="value 1",name_2="value 2"} 2010.1971
summary_name_count{name_1="value 1",name_2="value 2"} 4711 summary_name_count{name_1="value 1",name_2="value 2"} 4711
`, `,
}, },
// 4: Histogram
{
in: &dto.MetricFamily{
Name: proto.String("request_duration_microseconds"),
Help: proto.String("The response latency."),
Type: dto.MetricType_HISTOGRAM.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Histogram: &dto.Histogram{
SampleCount: proto.Uint64(2693),
SampleSum: proto.Float64(1756047.3),
Bucket: []*dto.Bucket{
&dto.Bucket{
UpperBound: proto.Float64(100),
CumulativeCount: proto.Uint64(123),
},
&dto.Bucket{
UpperBound: proto.Float64(120),
CumulativeCount: proto.Uint64(412),
},
&dto.Bucket{
UpperBound: proto.Float64(144),
CumulativeCount: proto.Uint64(592),
},
&dto.Bucket{
UpperBound: proto.Float64(172.8),
CumulativeCount: proto.Uint64(1524),
},
&dto.Bucket{
UpperBound: proto.Float64(math.Inf(+1)),
CumulativeCount: proto.Uint64(2693),
},
},
},
},
},
},
out: `# HELP request_duration_microseconds The response latency.
# TYPE request_duration_microseconds histogram
request_duration_microseconds_bucket{le="100"} 123
request_duration_microseconds_bucket{le="120"} 412
request_duration_microseconds_bucket{le="144"} 592
request_duration_microseconds_bucket{le="172.8"} 1524
request_duration_microseconds_bucket{le="+Inf"} 2693
request_duration_microseconds_sum 1.7560473e+06
request_duration_microseconds_count 2693
`,
},
// 5: Histogram with missing +Inf bucket.
{
in: &dto.MetricFamily{
Name: proto.String("request_duration_microseconds"),
Help: proto.String("The response latency."),
Type: dto.MetricType_HISTOGRAM.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Histogram: &dto.Histogram{
SampleCount: proto.Uint64(2693),
SampleSum: proto.Float64(1756047.3),
Bucket: []*dto.Bucket{
&dto.Bucket{
UpperBound: proto.Float64(100),
CumulativeCount: proto.Uint64(123),
},
&dto.Bucket{
UpperBound: proto.Float64(120),
CumulativeCount: proto.Uint64(412),
},
&dto.Bucket{
UpperBound: proto.Float64(144),
CumulativeCount: proto.Uint64(592),
},
&dto.Bucket{
UpperBound: proto.Float64(172.8),
CumulativeCount: proto.Uint64(1524),
},
},
},
},
},
},
out: `# HELP request_duration_microseconds The response latency.
# TYPE request_duration_microseconds histogram
request_duration_microseconds_bucket{le="100"} 123
request_duration_microseconds_bucket{le="120"} 412
request_duration_microseconds_bucket{le="144"} 592
request_duration_microseconds_bucket{le="172.8"} 1524
request_duration_microseconds_bucket{le="+Inf"} 2693
request_duration_microseconds_sum 1.7560473e+06
request_duration_microseconds_count 2693
`,
},
} }
for i, scenario := range scenarios { for i, scenario := range scenarios {
......
...@@ -24,7 +24,7 @@ import ( ...@@ -24,7 +24,7 @@ import (
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto" "github.com/golang/protobuf/proto"
"github.com/prometheus/client_golang/model" "github.com/prometheus/client_golang/model"
) )
...@@ -59,14 +59,19 @@ type Parser struct { ...@@ -59,14 +59,19 @@ type Parser struct {
currentMetric *dto.Metric currentMetric *dto.Metric
currentLabelPair *dto.LabelPair currentLabelPair *dto.LabelPair
// The remaining member variables are only used for summaries. // The remaining member variables are only used for summaries/histograms.
currentLabels map[string]string // All labels including '__name__' but excluding 'quantile'/'le'
// Summary specific.
summaries map[uint64]*dto.Metric // Key is created with LabelsToSignature. summaries map[uint64]*dto.Metric // Key is created with LabelsToSignature.
currentLabels map[string]string // All labels including '__name__' but excluding 'quantile'.
currentQuantile float64 currentQuantile float64
// Histogram specific.
histograms map[uint64]*dto.Metric // Key is created with LabelsToSignature.
currentBucket float64
// These tell us if the currently processed line ends on '_count' or // These tell us if the currently processed line ends on '_count' or
// '_sum' respectively and belong to a summary, representing the sample // '_sum' respectively and belong to a summary/histogram, representing the sample
// count and sum of that summary. // count and sum of that summary/histogram.
currentIsSummaryCount, currentIsSummarySum bool currentIsSummaryCount, currentIsSummarySum bool
currentIsHistogramCount, currentIsHistogramSum bool
} }
// TextToMetricFamilies reads 'in' as the simple and flat text-based exchange // TextToMetricFamilies reads 'in' as the simple and flat text-based exchange
...@@ -111,7 +116,11 @@ func (p *Parser) reset(in io.Reader) { ...@@ -111,7 +116,11 @@ func (p *Parser) reset(in io.Reader) {
if p.summaries == nil || len(p.summaries) > 0 { if p.summaries == nil || len(p.summaries) > 0 {
p.summaries = map[uint64]*dto.Metric{} p.summaries = map[uint64]*dto.Metric{}
} }
if p.histograms == nil || len(p.histograms) > 0 {
p.histograms = map[uint64]*dto.Metric{}
}
p.currentQuantile = math.NaN() p.currentQuantile = math.NaN()
p.currentBucket = math.NaN()
} }
// startOfLine represents the state where the next byte read from p.buf is the // startOfLine represents the state where the next byte read from p.buf is the
...@@ -224,13 +233,14 @@ func (p *Parser) readingMetricName() stateFn { ...@@ -224,13 +233,14 @@ func (p *Parser) readingMetricName() stateFn {
// p.currentByte) is either the first byte of the label set (i.e. a '{'), or the // p.currentByte) is either the first byte of the label set (i.e. a '{'), or the
// first byte of the value (otherwise). // first byte of the value (otherwise).
func (p *Parser) readingLabels() stateFn { func (p *Parser) readingLabels() stateFn {
// Alas, summaries are really special... We have to reset the // Summaries/histograms are special. We have to reset the
// currentLabels map and the currentQuantile before starting to // currentLabels map, currentQuantile and currentBucket before starting to
// read labels. // read labels.
if p.currentMF.GetType() == dto.MetricType_SUMMARY { if p.currentMF.GetType() == dto.MetricType_SUMMARY || p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
p.currentLabels = map[string]string{} p.currentLabels = map[string]string{}
p.currentLabels[string(model.MetricNameLabel)] = p.currentMF.GetName() p.currentLabels[string(model.MetricNameLabel)] = p.currentMF.GetName()
p.currentQuantile = math.NaN() p.currentQuantile = math.NaN()
p.currentBucket = math.NaN()
} }
if p.currentByte != '{' { if p.currentByte != '{' {
return p.readingValue return p.readingValue
...@@ -262,10 +272,10 @@ func (p *Parser) startLabelName() stateFn { ...@@ -262,10 +272,10 @@ func (p *Parser) startLabelName() stateFn {
p.parseError(fmt.Sprintf("label name %q is reserved", model.MetricNameLabel)) p.parseError(fmt.Sprintf("label name %q is reserved", model.MetricNameLabel))
return nil return nil
} }
// Once more, special summary treatment... Don't add 'quantile' // Special summary/histogram treatment. Don't add 'quantile' and 'le'
// labels to 'real' labels. // labels to 'real' labels.
if p.currentMF.GetType() != dto.MetricType_SUMMARY || if !(p.currentMF.GetType() == dto.MetricType_SUMMARY && p.currentLabelPair.GetName() == model.QuantileLabel) &&
p.currentLabelPair.GetName() != "quantile" { !(p.currentMF.GetType() == dto.MetricType_HISTOGRAM && p.currentLabelPair.GetName() == model.BucketLabel) {
p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPair) p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPair)
} }
if p.skipBlankTabIfCurrentBlankTab(); p.err != nil { if p.skipBlankTabIfCurrentBlankTab(); p.err != nil {
...@@ -292,14 +302,26 @@ func (p *Parser) startLabelValue() stateFn { ...@@ -292,14 +302,26 @@ func (p *Parser) startLabelValue() stateFn {
return nil return nil
} }
p.currentLabelPair.Value = proto.String(p.currentToken.String()) p.currentLabelPair.Value = proto.String(p.currentToken.String())
// Once more, special treatment of summaries: // Special treatment of summaries:
// - Quantile labels are special, will result in dto.Quantile later. // - Quantile labels are special, will result in dto.Quantile later.
// - Other labels have to be added to currentLabels for signature calculation. // - Other labels have to be added to currentLabels for signature calculation.
if p.currentMF.GetType() == dto.MetricType_SUMMARY { if p.currentMF.GetType() == dto.MetricType_SUMMARY {
if p.currentLabelPair.GetName() == "quantile" { if p.currentLabelPair.GetName() == model.QuantileLabel {
if p.currentQuantile, p.err = strconv.ParseFloat(p.currentLabelPair.GetValue(), 64); p.err != nil { if p.currentQuantile, p.err = strconv.ParseFloat(p.currentLabelPair.GetValue(), 64); p.err != nil {
// Create a more helpful error message. // Create a more helpful error message.
p.parseError(fmt.Sprintf("expected float as value for quantile label, got %q", p.currentLabelPair.GetValue())) p.parseError(fmt.Sprintf("expected float as value for 'quantile' label, got %q", p.currentLabelPair.GetValue()))
return nil
}
} else {
p.currentLabels[p.currentLabelPair.GetName()] = p.currentLabelPair.GetValue()
}
}
// Similar special treatment of histograms.
if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
if p.currentLabelPair.GetName() == model.BucketLabel {
if p.currentBucket, p.err = strconv.ParseFloat(p.currentLabelPair.GetValue(), 64); p.err != nil {
// Create a more helpful error message.
p.parseError(fmt.Sprintf("expected float as value for 'le' label, got %q", p.currentLabelPair.GetValue()))
return nil return nil
} }
} else { } else {
...@@ -328,7 +350,7 @@ func (p *Parser) startLabelValue() stateFn { ...@@ -328,7 +350,7 @@ func (p *Parser) startLabelValue() stateFn {
// p.currentByte) is the first byte of the sample value (i.e. a float). // p.currentByte) is the first byte of the sample value (i.e. a float).
func (p *Parser) readingValue() stateFn { func (p *Parser) readingValue() stateFn {
// When we are here, we have read all the labels, so for the // When we are here, we have read all the labels, so for the
// infamous special case of a summary, we can finally find out // special case of a summary/histogram, we can finally find out
// if the metric already exists. // if the metric already exists.
if p.currentMF.GetType() == dto.MetricType_SUMMARY { if p.currentMF.GetType() == dto.MetricType_SUMMARY {
signature := model.LabelsToSignature(p.currentLabels) signature := model.LabelsToSignature(p.currentLabels)
...@@ -338,6 +360,14 @@ func (p *Parser) readingValue() stateFn { ...@@ -338,6 +360,14 @@ func (p *Parser) readingValue() stateFn {
p.summaries[signature] = p.currentMetric p.summaries[signature] = p.currentMetric
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric) p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
} }
} else if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
signature := model.LabelsToSignature(p.currentLabels)
if histogram := p.histograms[signature]; histogram != nil {
p.currentMetric = histogram
} else {
p.histograms[signature] = p.currentMetric
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
}
} else { } else {
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric) p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
} }
...@@ -376,6 +406,25 @@ func (p *Parser) readingValue() stateFn { ...@@ -376,6 +406,25 @@ func (p *Parser) readingValue() stateFn {
}, },
) )
} }
case dto.MetricType_HISTOGRAM:
// *sigh*
if p.currentMetric.Histogram == nil {
p.currentMetric.Histogram = &dto.Histogram{}
}
switch {
case p.currentIsHistogramCount:
p.currentMetric.Histogram.SampleCount = proto.Uint64(uint64(value))
case p.currentIsHistogramSum:
p.currentMetric.Histogram.SampleSum = proto.Float64(value)
case !math.IsNaN(p.currentBucket):
p.currentMetric.Histogram.Bucket = append(
p.currentMetric.Histogram.Bucket,
&dto.Bucket{
UpperBound: proto.Float64(p.currentBucket),
CumulativeCount: proto.Uint64(uint64(value)),
},
)
}
default: default:
p.err = fmt.Errorf("unexpected type for metric name %q", p.currentMF.GetName()) p.err = fmt.Errorf("unexpected type for metric name %q", p.currentMF.GetName())
} }
...@@ -598,11 +647,13 @@ func (p *Parser) readTokenAsLabelValue() { ...@@ -598,11 +647,13 @@ func (p *Parser) readTokenAsLabelValue() {
func (p *Parser) setOrCreateCurrentMF() { func (p *Parser) setOrCreateCurrentMF() {
p.currentIsSummaryCount = false p.currentIsSummaryCount = false
p.currentIsSummarySum = false p.currentIsSummarySum = false
p.currentIsHistogramCount = false
p.currentIsHistogramSum = false
name := p.currentToken.String() name := p.currentToken.String()
if p.currentMF = p.metricFamiliesByName[name]; p.currentMF != nil { if p.currentMF = p.metricFamiliesByName[name]; p.currentMF != nil {
return return
} }
// Try out if this is a _sum or _count for a summary. // Try out if this is a _sum or _count for a summary/histogram.
summaryName := summaryMetricName(name) summaryName := summaryMetricName(name)
if p.currentMF = p.metricFamiliesByName[summaryName]; p.currentMF != nil { if p.currentMF = p.metricFamiliesByName[summaryName]; p.currentMF != nil {
if p.currentMF.GetType() == dto.MetricType_SUMMARY { if p.currentMF.GetType() == dto.MetricType_SUMMARY {
...@@ -615,6 +666,18 @@ func (p *Parser) setOrCreateCurrentMF() { ...@@ -615,6 +666,18 @@ func (p *Parser) setOrCreateCurrentMF() {
return return
} }
} }
histogramName := histogramMetricName(name)
if p.currentMF = p.metricFamiliesByName[histogramName]; p.currentMF != nil {
if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
if isCount(name) {
p.currentIsHistogramCount = true
}
if isSum(name) {
p.currentIsHistogramSum = true
}
return
}
}
p.currentMF = &dto.MetricFamily{Name: proto.String(name)} p.currentMF = &dto.MetricFamily{Name: proto.String(name)}
p.metricFamiliesByName[name] = p.currentMF p.metricFamiliesByName[name] = p.currentMF
} }
...@@ -647,6 +710,10 @@ func isSum(name string) bool { ...@@ -647,6 +710,10 @@ func isSum(name string) bool {
return len(name) > 4 && name[len(name)-4:] == "_sum" return len(name) > 4 && name[len(name)-4:] == "_sum"
} }
func isBucket(name string) bool {
return len(name) > 7 && name[len(name)-7:] == "_bucket"
}
func summaryMetricName(name string) string { func summaryMetricName(name string) string {
switch { switch {
case isCount(name): case isCount(name):
...@@ -657,3 +724,16 @@ func summaryMetricName(name string) string { ...@@ -657,3 +724,16 @@ func summaryMetricName(name string) string {
return name return name
} }
} }
func histogramMetricName(name string) string {
switch {
case isCount(name):
return name[:len(name)-6]
case isSum(name):
return name[:len(name)-4]
case isBucket(name):
return name[:len(name)-7]
default:
return name
}
}
...@@ -18,7 +18,7 @@ import ( ...@@ -18,7 +18,7 @@ import (
"strings" "strings"
"testing" "testing"
"code.google.com/p/goprotobuf/proto" "github.com/golang/protobuf/proto"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
) )
...@@ -335,6 +335,57 @@ my_summary{n1="val3", quantile="0.2"} 4711 ...@@ -335,6 +335,57 @@ my_summary{n1="val3", quantile="0.2"} 4711
}, },
}, },
}, },
// 4: The histogram.
{
in: `
# HELP request_duration_microseconds The response latency.
# TYPE request_duration_microseconds histogram
request_duration_microseconds_bucket{le="100"} 123
request_duration_microseconds_bucket{le="120"} 412
request_duration_microseconds_bucket{le="144"} 592
request_duration_microseconds_bucket{le="172.8"} 1524
request_duration_microseconds_bucket{le="+Inf"} 2693
request_duration_microseconds_sum 1.7560473e+06
request_duration_microseconds_count 2693
`,
out: []*dto.MetricFamily{
{
Name: proto.String("request_duration_microseconds"),
Help: proto.String("The response latency."),
Type: dto.MetricType_HISTOGRAM.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Histogram: &dto.Histogram{
SampleCount: proto.Uint64(2693),
SampleSum: proto.Float64(1756047.3),
Bucket: []*dto.Bucket{
&dto.Bucket{
UpperBound: proto.Float64(100),
CumulativeCount: proto.Uint64(123),
},
&dto.Bucket{
UpperBound: proto.Float64(120),
CumulativeCount: proto.Uint64(412),
},
&dto.Bucket{
UpperBound: proto.Float64(144),
CumulativeCount: proto.Uint64(592),
},
&dto.Bucket{
UpperBound: proto.Float64(172.8),
CumulativeCount: proto.Uint64(1524),
},
&dto.Bucket{
UpperBound: proto.Float64(math.Inf(+1)),
CumulativeCount: proto.Uint64(2693),
},
},
},
},
},
},
},
},
} }
for i, scenario := range scenarios { for i, scenario := range scenarios {
...@@ -427,7 +478,7 @@ line"} 3.14 ...@@ -427,7 +478,7 @@ line"} 3.14
# TYPE metric summary # TYPE metric summary
metric{quantile="bla"} 3.14 metric{quantile="bla"} 3.14
`, `,
err: "text format parsing error in line 3: expected float as value for quantile label", err: "text format parsing error in line 3: expected float as value for 'quantile' label",
}, },
// 8: // 8:
{ {
...@@ -500,6 +551,14 @@ metric 4.12 ...@@ -500,6 +551,14 @@ metric 4.12
in: `{label="bla"} 3.14 2`, in: `{label="bla"} 3.14 2`,
err: "text format parsing error in line 1: invalid metric name", err: "text format parsing error in line 1: invalid metric name",
}, },
// 18:
{
in: `
# TYPE metric histogram
metric_bucket{le="bla"} 3.14
`,
err: "text format parsing error in line 3: expected float as value for 'le' label",
},
} }
for i, scenario := range scenarios { for i, scenario := range scenarios {
......
...@@ -17,7 +17,7 @@ import ( ...@@ -17,7 +17,7 @@ import (
"fmt" "fmt"
"io" "io"
"code.google.com/p/goprotobuf/proto" "github.com/golang/protobuf/proto"
"github.com/matttproud/golang_protobuf_extensions/ext" "github.com/matttproud/golang_protobuf_extensions/ext"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
......
...@@ -15,27 +15,28 @@ It has these top-level messages: ...@@ -15,27 +15,28 @@ It has these top-level messages:
Quantile Quantile
Summary Summary
Untyped Untyped
Histogram
Bucket
Metric Metric
MetricFamily MetricFamily
*/ */
package io_prometheus_client package io_prometheus_client
import proto "code.google.com/p/goprotobuf/proto" import proto "github.com/golang/protobuf/proto"
import json "encoding/json"
import math "math" import math "math"
// Reference proto, json, and math imports to suppress error if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal var _ = proto.Marshal
var _ = &json.SyntaxError{}
var _ = math.Inf var _ = math.Inf
type MetricType int32 type MetricType int32
const ( const (
MetricType_COUNTER MetricType = 0 MetricType_COUNTER MetricType = 0
MetricType_GAUGE MetricType = 1 MetricType_GAUGE MetricType = 1
MetricType_SUMMARY MetricType = 2 MetricType_SUMMARY MetricType = 2
MetricType_UNTYPED MetricType = 3 MetricType_UNTYPED MetricType = 3
MetricType_HISTOGRAM MetricType = 4
) )
var MetricType_name = map[int32]string{ var MetricType_name = map[int32]string{
...@@ -43,12 +44,14 @@ var MetricType_name = map[int32]string{ ...@@ -43,12 +44,14 @@ var MetricType_name = map[int32]string{
1: "GAUGE", 1: "GAUGE",
2: "SUMMARY", 2: "SUMMARY",
3: "UNTYPED", 3: "UNTYPED",
4: "HISTOGRAM",
} }
var MetricType_value = map[string]int32{ var MetricType_value = map[string]int32{
"COUNTER": 0, "COUNTER": 0,
"GAUGE": 1, "GAUGE": 1,
"SUMMARY": 2, "SUMMARY": 2,
"UNTYPED": 3, "UNTYPED": 3,
"HISTOGRAM": 4,
} }
func (x MetricType) Enum() *MetricType { func (x MetricType) Enum() *MetricType {
...@@ -196,12 +199,69 @@ func (m *Untyped) GetValue() float64 { ...@@ -196,12 +199,69 @@ func (m *Untyped) GetValue() float64 {
return 0 return 0
} }
type Histogram struct {
SampleCount *uint64 `protobuf:"varint,1,opt,name=sample_count" json:"sample_count,omitempty"`
SampleSum *float64 `protobuf:"fixed64,2,opt,name=sample_sum" json:"sample_sum,omitempty"`
Bucket []*Bucket `protobuf:"bytes,3,rep,name=bucket" json:"bucket,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Histogram) Reset() { *m = Histogram{} }
func (m *Histogram) String() string { return proto.CompactTextString(m) }
func (*Histogram) ProtoMessage() {}
func (m *Histogram) GetSampleCount() uint64 {
if m != nil && m.SampleCount != nil {
return *m.SampleCount
}
return 0
}
func (m *Histogram) GetSampleSum() float64 {
if m != nil && m.SampleSum != nil {
return *m.SampleSum
}
return 0
}
func (m *Histogram) GetBucket() []*Bucket {
if m != nil {
return m.Bucket
}
return nil
}
type Bucket struct {
CumulativeCount *uint64 `protobuf:"varint,1,opt,name=cumulative_count" json:"cumulative_count,omitempty"`
UpperBound *float64 `protobuf:"fixed64,2,opt,name=upper_bound" json:"upper_bound,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Bucket) Reset() { *m = Bucket{} }
func (m *Bucket) String() string { return proto.CompactTextString(m) }
func (*Bucket) ProtoMessage() {}
func (m *Bucket) GetCumulativeCount() uint64 {
if m != nil && m.CumulativeCount != nil {
return *m.CumulativeCount
}
return 0
}
func (m *Bucket) GetUpperBound() float64 {
if m != nil && m.UpperBound != nil {
return *m.UpperBound
}
return 0
}
type Metric struct { type Metric struct {
Label []*LabelPair `protobuf:"bytes,1,rep,name=label" json:"label,omitempty"` Label []*LabelPair `protobuf:"bytes,1,rep,name=label" json:"label,omitempty"`
Gauge *Gauge `protobuf:"bytes,2,opt,name=gauge" json:"gauge,omitempty"` Gauge *Gauge `protobuf:"bytes,2,opt,name=gauge" json:"gauge,omitempty"`
Counter *Counter `protobuf:"bytes,3,opt,name=counter" json:"counter,omitempty"` Counter *Counter `protobuf:"bytes,3,opt,name=counter" json:"counter,omitempty"`
Summary *Summary `protobuf:"bytes,4,opt,name=summary" json:"summary,omitempty"` Summary *Summary `protobuf:"bytes,4,opt,name=summary" json:"summary,omitempty"`
Untyped *Untyped `protobuf:"bytes,5,opt,name=untyped" json:"untyped,omitempty"` Untyped *Untyped `protobuf:"bytes,5,opt,name=untyped" json:"untyped,omitempty"`
Histogram *Histogram `protobuf:"bytes,7,opt,name=histogram" json:"histogram,omitempty"`
TimestampMs *int64 `protobuf:"varint,6,opt,name=timestamp_ms" json:"timestamp_ms,omitempty"` TimestampMs *int64 `protobuf:"varint,6,opt,name=timestamp_ms" json:"timestamp_ms,omitempty"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
} }
...@@ -245,6 +305,13 @@ func (m *Metric) GetUntyped() *Untyped { ...@@ -245,6 +305,13 @@ func (m *Metric) GetUntyped() *Untyped {
return nil return nil
} }
func (m *Metric) GetHistogram() *Histogram {
if m != nil {
return m.Histogram
}
return nil
}
func (m *Metric) GetTimestampMs() int64 { func (m *Metric) GetTimestampMs() int64 {
if m != nil && m.TimestampMs != nil { if m != nil && m.TimestampMs != nil {
return *m.TimestampMs return *m.TimestampMs
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
// "fmt" // "fmt"
// "log" // "log"
// //
// "github.com/prometheus/client_golang/procfs" // "github.com/prometheus/procfs"
// ) // )
// //
// func main() { // func main() {
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
// log.Fatalf("could not get process: %s", err) // log.Fatalf("could not get process: %s", err)
// } // }
// //
// stat, err := p.Stat() // stat, err := p.NewStat()
// if err != nil { // if err != nil {
// log.Fatalf("could not get process stat: %s", err) // log.Fatalf("could not get process stat: %s", err)
// } // }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment