Add Godeps for the Prometheus monitoring client library and its dependencies.

See issue #1625 for discussion.
This commit is contained in:
Alex Robinson
2015-02-06 22:21:56 +00:00
parent 617e1ca7de
commit 21f9b83d91
129 changed files with 27487 additions and 0 deletions

View File

@@ -0,0 +1,355 @@
// Copyright 2013 Matt T. Proud
//
// 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 ext
import (
"bytes"
"math/rand"
"reflect"
"testing"
"testing/quick"
. "code.google.com/p/goprotobuf/proto"
. "code.google.com/p/goprotobuf/proto/testdata"
)
func TestWriteDelimited(t *testing.T) {
for _, test := range []struct {
msg Message
buf []byte
n int
err error
}{
{
msg: &Empty{},
n: 1,
buf: []byte{0},
},
{
msg: &GoEnum{Foo: FOO_FOO1.Enum()},
n: 3,
buf: []byte{2, 8, 1},
},
{
msg: &Strings{
StringField: String(`This is my gigantic, unhappy string. It exceeds
the encoding size of a single byte varint. We are using it to fuzz test the
correctness of the header decoding mechanisms, which may prove problematic.
I expect it may. Let's hope you enjoy testing as much as we do.`),
},
n: 271,
buf: []byte{141, 2, 10, 138, 2, 84, 104, 105, 115, 32, 105, 115, 32, 109,
121, 32, 103, 105, 103, 97, 110, 116, 105, 99, 44, 32, 117, 110, 104,
97, 112, 112, 121, 32, 115, 116, 114, 105, 110, 103, 46, 32, 32, 73,
116, 32, 101, 120, 99, 101, 101, 100, 115, 10, 116, 104, 101, 32, 101,
110, 99, 111, 100, 105, 110, 103, 32, 115, 105, 122, 101, 32, 111, 102,
32, 97, 32, 115, 105, 110, 103, 108, 101, 32, 98, 121, 116, 101, 32,
118, 97, 114, 105, 110, 116, 46, 32, 32, 87, 101, 32, 97, 114, 101, 32,
117, 115, 105, 110, 103, 32, 105, 116, 32, 116, 111, 32, 102, 117, 122,
122, 32, 116, 101, 115, 116, 32, 116, 104, 101, 10, 99, 111, 114, 114,
101, 99, 116, 110, 101, 115, 115, 32, 111, 102, 32, 116, 104, 101, 32,
104, 101, 97, 100, 101, 114, 32, 100, 101, 99, 111, 100, 105, 110, 103,
32, 109, 101, 99, 104, 97, 110, 105, 115, 109, 115, 44, 32, 119, 104,
105, 99, 104, 32, 109, 97, 121, 32, 112, 114, 111, 118, 101, 32, 112,
114, 111, 98, 108, 101, 109, 97, 116, 105, 99, 46, 10, 73, 32, 101, 120,
112, 101, 99, 116, 32, 105, 116, 32, 109, 97, 121, 46, 32, 32, 76, 101,
116, 39, 115, 32, 104, 111, 112, 101, 32, 121, 111, 117, 32, 101, 110,
106, 111, 121, 32, 116, 101, 115, 116, 105, 110, 103, 32, 97, 115, 32,
109, 117, 99, 104, 32, 97, 115, 32, 119, 101, 32, 100, 111, 46},
},
} {
var buf bytes.Buffer
if n, err := WriteDelimited(&buf, test.msg); n != test.n || err != test.err {
t.Fatalf("WriteDelimited(buf, %#v) = %v, %v; want %v, %v", test.msg, n, err, test.n, test.err)
}
if out := buf.Bytes(); !bytes.Equal(out, test.buf) {
t.Fatalf("WriteDelimited(buf, %#v); buf = %v; want %v", test.msg, out, test.buf)
}
}
}
func TestReadDelimited(t *testing.T) {
for _, test := range []struct {
buf []byte
msg Message
n int
err error
}{
{
buf: []byte{0},
msg: &Empty{},
n: 1,
},
{
n: 3,
buf: []byte{2, 8, 1},
msg: &GoEnum{Foo: FOO_FOO1.Enum()},
},
{
buf: []byte{141, 2, 10, 138, 2, 84, 104, 105, 115, 32, 105, 115, 32, 109,
121, 32, 103, 105, 103, 97, 110, 116, 105, 99, 44, 32, 117, 110, 104,
97, 112, 112, 121, 32, 115, 116, 114, 105, 110, 103, 46, 32, 32, 73,
116, 32, 101, 120, 99, 101, 101, 100, 115, 10, 116, 104, 101, 32, 101,
110, 99, 111, 100, 105, 110, 103, 32, 115, 105, 122, 101, 32, 111, 102,
32, 97, 32, 115, 105, 110, 103, 108, 101, 32, 98, 121, 116, 101, 32,
118, 97, 114, 105, 110, 116, 46, 32, 32, 87, 101, 32, 97, 114, 101, 32,
117, 115, 105, 110, 103, 32, 105, 116, 32, 116, 111, 32, 102, 117, 122,
122, 32, 116, 101, 115, 116, 32, 116, 104, 101, 10, 99, 111, 114, 114,
101, 99, 116, 110, 101, 115, 115, 32, 111, 102, 32, 116, 104, 101, 32,
104, 101, 97, 100, 101, 114, 32, 100, 101, 99, 111, 100, 105, 110, 103,
32, 109, 101, 99, 104, 97, 110, 105, 115, 109, 115, 44, 32, 119, 104,
105, 99, 104, 32, 109, 97, 121, 32, 112, 114, 111, 118, 101, 32, 112,
114, 111, 98, 108, 101, 109, 97, 116, 105, 99, 46, 10, 73, 32, 101, 120,
112, 101, 99, 116, 32, 105, 116, 32, 109, 97, 121, 46, 32, 32, 76, 101,
116, 39, 115, 32, 104, 111, 112, 101, 32, 121, 111, 117, 32, 101, 110,
106, 111, 121, 32, 116, 101, 115, 116, 105, 110, 103, 32, 97, 115, 32,
109, 117, 99, 104, 32, 97, 115, 32, 119, 101, 32, 100, 111, 46},
msg: &Strings{
StringField: String(`This is my gigantic, unhappy string. It exceeds
the encoding size of a single byte varint. We are using it to fuzz test the
correctness of the header decoding mechanisms, which may prove problematic.
I expect it may. Let's hope you enjoy testing as much as we do.`),
},
n: 271,
},
} {
msg := Clone(test.msg)
msg.Reset()
if n, err := ReadDelimited(bytes.NewBuffer(test.buf), msg); n != test.n || err != test.err {
t.Fatalf("ReadDelimited(%v, msg) = %v, %v; want %v, %v", test.buf, n, err, test.n, test.err)
}
if !Equal(msg, test.msg) {
t.Fatalf("ReadDelimited(%v, msg); msg = %v; want %v", test.buf, msg, test.msg)
}
}
}
func TestEndToEndValid(t *testing.T) {
for _, test := range [][]Message{
[]Message{&Empty{}},
[]Message{&GoEnum{Foo: FOO_FOO1.Enum()}, &Empty{}, &GoEnum{Foo: FOO_FOO1.Enum()}},
[]Message{&GoEnum{Foo: FOO_FOO1.Enum()}},
[]Message{&Strings{
StringField: String(`This is my gigantic, unhappy string. It exceeds
the encoding size of a single byte varint. We are using it to fuzz test the
correctness of the header decoding mechanisms, which may prove problematic.
I expect it may. Let's hope you enjoy testing as much as we do.`),
}},
} {
var buf bytes.Buffer
var written int
for i, msg := range test {
n, err := WriteDelimited(&buf, msg)
if err != nil {
// Assumption: TestReadDelimited and TestWriteDelimited are sufficient
// and inputs for this test are explicitly exercised there.
t.Fatalf("WriteDelimited(buf, %v[%d]) = ?, %v; wanted ?, nil", test, i, err)
}
written += n
}
var read int
for i, msg := range test {
out := Clone(msg)
out.Reset()
n, _ := ReadDelimited(&buf, out)
// Decide to do EOF checking?
read += n
if !Equal(out, msg) {
t.Fatalf("out = %v; want %v[%d] = %#v", out, test, i, msg)
}
}
if read != written {
t.Fatalf("%v read = %d; want %d", test, read, written)
}
}
}
// visitMessage empties the private state fields of the quick.Value()-generated
// Protocol Buffer messages, for they cause an inordinate amount of problems.
// This is because we are using an automated fuzz generator on a type with
// private fields.
func visitMessage(m Message) {
t := reflect.TypeOf(m)
if t.Kind() != reflect.Ptr {
return
}
derefed := t.Elem()
if derefed.Kind() != reflect.Struct {
return
}
v := reflect.ValueOf(m)
elem := v.Elem()
for i := 0; i < elem.NumField(); i++ {
field := elem.FieldByIndex([]int{i})
fieldType := field.Type()
if fieldType.Implements(reflect.TypeOf((*Message)(nil)).Elem()) {
visitMessage(field.Interface().(Message))
}
if field.Kind() == reflect.Slice {
for i := 0; i < field.Len(); i++ {
elem := field.Index(i)
elemType := elem.Type()
if elemType.Implements(reflect.TypeOf((*Message)(nil)).Elem()) {
visitMessage(elem.Interface().(Message))
}
}
}
}
if field := elem.FieldByName("XXX_unrecognized"); field.IsValid() {
field.Set(reflect.ValueOf([]byte{}))
}
if field := elem.FieldByName("XXX_extensions"); field.IsValid() {
field.Set(reflect.ValueOf(nil))
}
}
// rndMessage generates a random valid Protocol Buffer message.
func rndMessage(r *rand.Rand) Message {
var t reflect.Type
switch v := rand.Intn(23); v {
// TODO(br): Uncomment the elements below once fix is incorporated, except
// for the elements marked as patently incompatible.
// case 0:
// t = reflect.TypeOf(&GoEnum{})
// break
// case 1:
// t = reflect.TypeOf(&GoTestField{})
// break
case 2:
t = reflect.TypeOf(&GoTest{})
break
// case 3:
// t = reflect.TypeOf(&GoSkipTest{})
// break
// case 4:
// t = reflect.TypeOf(&NonPackedTest{})
// break
// case 5:
// t = reflect.TypeOf(&PackedTest{})
// break
case 6:
t = reflect.TypeOf(&MaxTag{})
break
case 7:
t = reflect.TypeOf(&OldMessage{})
break
case 8:
t = reflect.TypeOf(&NewMessage{})
break
case 9:
t = reflect.TypeOf(&InnerMessage{})
break
case 10:
t = reflect.TypeOf(&OtherMessage{})
break
case 11:
// PATENTLY INVALID FOR FUZZ GENERATION
// t = reflect.TypeOf(&MyMessage{})
break
// case 12:
// t = reflect.TypeOf(&Ext{})
// break
case 13:
// PATENTLY INVALID FOR FUZZ GENERATION
// t = reflect.TypeOf(&MyMessageSet{})
break
// case 14:
// t = reflect.TypeOf(&Empty{})
// break
// case 15:
// t = reflect.TypeOf(&MessageList{})
// break
// case 16:
// t = reflect.TypeOf(&Strings{})
// break
// case 17:
// t = reflect.TypeOf(&Defaults{})
// break
// case 17:
// t = reflect.TypeOf(&SubDefaults{})
// break
// case 18:
// t = reflect.TypeOf(&RepeatedEnum{})
// break
case 19:
t = reflect.TypeOf(&MoreRepeated{})
break
// case 20:
// t = reflect.TypeOf(&GroupOld{})
// break
// case 21:
// t = reflect.TypeOf(&GroupNew{})
// break
case 22:
t = reflect.TypeOf(&FloatingPoint{})
break
default:
// TODO(br): Replace with an unreachable once fixed.
t = reflect.TypeOf(&GoTest{})
break
}
if t == nil {
t = reflect.TypeOf(&GoTest{})
}
v, ok := quick.Value(t, r)
if !ok {
panic("attempt to generate illegal item; consult item 11")
}
visitMessage(v.Interface().(Message))
return v.Interface().(Message)
}
// rndMessages generates several random Protocol Buffer messages.
func rndMessages(r *rand.Rand) []Message {
n := r.Intn(128)
out := make([]Message, 0, n)
for i := 0; i < n; i++ {
out = append(out, rndMessage(r))
}
return out
}
func TestFuzz(t *testing.T) {
rnd := rand.New(rand.NewSource(42))
check := func() bool {
messages := rndMessages(rnd)
var buf bytes.Buffer
var written int
for i, msg := range messages {
n, err := WriteDelimited(&buf, msg)
if err != nil {
t.Fatalf("WriteDelimited(buf, %v[%d]) = ?, %v; wanted ?, nil", messages, i, err)
}
written += n
}
var read int
for i, msg := range messages {
out := Clone(msg)
out.Reset()
n, _ := ReadDelimited(&buf, out)
read += n
if !Equal(out, msg) {
t.Fatalf("out = %v; want %v[%d] = %#v", out, messages, i, msg)
}
}
if read != written {
t.Fatalf("%v read = %d; want %d", messages, read, written)
}
return true
}
if err := quick.Check(check, nil); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,75 @@
// Copyright 2013 Matt T. Proud
//
// 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 ext
import (
"encoding/binary"
"errors"
"io"
"code.google.com/p/goprotobuf/proto"
)
var errInvalidVarint = errors.New("invalid varint32 encountered")
// ReadDelimited decodes a message from the provided length-delimited stream,
// where the length is encoded as 32-bit varint prefix to the message body.
// It returns the total number of bytes read and any applicable error. This is
// roughly equivalent to the companion Java API's
// MessageLite#parseDelimitedFrom. As per the reader contract, this function
// calls r.Read repeatedly as required until exactly one message including its
// prefix is read and decoded (or an error has occurred). The function never
// reads more bytes from the stream than required. The function never returns
// an error if a message has been read and decoded correctly, even if the end
// of the stream has been reached in doing so. In that case, any subsequent
// calls return (0, io.EOF).
func ReadDelimited(r io.Reader, m proto.Message) (n int, err error) {
// Per AbstractParser#parsePartialDelimitedFrom with
// CodedInputStream#readRawVarint32.
headerBuf := make([]byte, binary.MaxVarintLen32)
var bytesRead, varIntBytes int
var messageLength uint64
for varIntBytes == 0 { // i.e. no varint has been decoded yet.
if bytesRead >= len(headerBuf) {
return bytesRead, errInvalidVarint
}
// We have to read byte by byte here to avoid reading more bytes
// than required. Each read byte is appended to what we have
// read before.
newBytesRead, err := r.Read(headerBuf[bytesRead : bytesRead+1])
if newBytesRead == 0 {
if err != nil {
return bytesRead, err
}
// A Reader should not return (0, nil), but if it does,
// it should be treated as no-op (according to the
// Reader contract). So let's go on...
continue
}
bytesRead += newBytesRead
// Now present everything read so far to the varint decoder and
// see if a varint can be decoded already.
messageLength, varIntBytes = proto.DecodeVarint(headerBuf[:bytesRead])
}
messageBuf := make([]byte, messageLength)
newBytesRead, err := io.ReadFull(r, messageBuf)
bytesRead += newBytesRead
if err != nil {
return bytesRead, err
}
return bytesRead, proto.Unmarshal(messageBuf, m)
}

View File

@@ -0,0 +1,16 @@
// Copyright 2013 Matt T. Proud
//
// 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 ext enables record length-delimited Protocol Buffer streaming.
package ext

View File

@@ -0,0 +1,46 @@
// Copyright 2013 Matt T. Proud
//
// 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 ext
import (
"encoding/binary"
"io"
"code.google.com/p/goprotobuf/proto"
)
// WriteDelimited encodes and dumps a message to the provided writer prefixed
// with a 32-bit varint indicating the length of the encoded message, producing
// a length-delimited record stream, which can be used to chain together
// encoded messages of the same type together in a file. It returns the total
// number of bytes written and any applicable error. This is roughly
// equivalent to the companion Java API's MessageLite#writeDelimitedTo.
func WriteDelimited(w io.Writer, m proto.Message) (n int, err error) {
buffer, err := proto.Marshal(m)
if err != nil {
return 0, err
}
buf := make([]byte, binary.MaxVarintLen32)
encodedLength := binary.PutUvarint(buf, uint64(len(buffer)))
sync, err := w.Write(buf[:encodedLength])
if err != nil {
return sync, err
}
n, err = w.Write(buffer)
return n + sync, err
}

View File

@@ -0,0 +1,103 @@
// Copyright 2010 The Go Authors. All rights reserved.
// http://code.google.com/p/goprotobuf/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package ext
import (
. "code.google.com/p/goprotobuf/proto"
. "code.google.com/p/goprotobuf/proto/testdata"
)
// FROM https://code.google.com/p/goprotobuf/source/browse/proto/all_test.go.
func initGoTestField() *GoTestField {
f := new(GoTestField)
f.Label = String("label")
f.Type = String("type")
return f
}
// These are all structurally equivalent but the tag numbers differ.
// (It's remarkable that required, optional, and repeated all have
// 8 letters.)
func initGoTest_RequiredGroup() *GoTest_RequiredGroup {
return &GoTest_RequiredGroup{
RequiredField: String("required"),
}
}
func initGoTest_OptionalGroup() *GoTest_OptionalGroup {
return &GoTest_OptionalGroup{
RequiredField: String("optional"),
}
}
func initGoTest_RepeatedGroup() *GoTest_RepeatedGroup {
return &GoTest_RepeatedGroup{
RequiredField: String("repeated"),
}
}
func initGoTest(setdefaults bool) *GoTest {
pb := new(GoTest)
if setdefaults {
pb.F_BoolDefaulted = Bool(Default_GoTest_F_BoolDefaulted)
pb.F_Int32Defaulted = Int32(Default_GoTest_F_Int32Defaulted)
pb.F_Int64Defaulted = Int64(Default_GoTest_F_Int64Defaulted)
pb.F_Fixed32Defaulted = Uint32(Default_GoTest_F_Fixed32Defaulted)
pb.F_Fixed64Defaulted = Uint64(Default_GoTest_F_Fixed64Defaulted)
pb.F_Uint32Defaulted = Uint32(Default_GoTest_F_Uint32Defaulted)
pb.F_Uint64Defaulted = Uint64(Default_GoTest_F_Uint64Defaulted)
pb.F_FloatDefaulted = Float32(Default_GoTest_F_FloatDefaulted)
pb.F_DoubleDefaulted = Float64(Default_GoTest_F_DoubleDefaulted)
pb.F_StringDefaulted = String(Default_GoTest_F_StringDefaulted)
pb.F_BytesDefaulted = Default_GoTest_F_BytesDefaulted
pb.F_Sint32Defaulted = Int32(Default_GoTest_F_Sint32Defaulted)
pb.F_Sint64Defaulted = Int64(Default_GoTest_F_Sint64Defaulted)
}
pb.Kind = GoTest_TIME.Enum()
pb.RequiredField = initGoTestField()
pb.F_BoolRequired = Bool(true)
pb.F_Int32Required = Int32(3)
pb.F_Int64Required = Int64(6)
pb.F_Fixed32Required = Uint32(32)
pb.F_Fixed64Required = Uint64(64)
pb.F_Uint32Required = Uint32(3232)
pb.F_Uint64Required = Uint64(6464)
pb.F_FloatRequired = Float32(3232)
pb.F_DoubleRequired = Float64(6464)
pb.F_StringRequired = String("string")
pb.F_BytesRequired = []byte("bytes")
pb.F_Sint32Required = Int32(-32)
pb.F_Sint64Required = Int64(-64)
pb.Requiredgroup = initGoTest_RequiredGroup()
return pb
}

View File

@@ -0,0 +1 @@
Imported at 75cd24fc2f2c from https://bitbucket.org/ww/goautoneg.

View File

@@ -0,0 +1,13 @@
include $(GOROOT)/src/Make.inc
TARG=bitbucket.org/ww/goautoneg
GOFILES=autoneg.go
include $(GOROOT)/src/Make.pkg
format:
gofmt -w *.go
docs:
gomake clean
godoc ${TARG} > README.txt

View File

@@ -0,0 +1,67 @@
PACKAGE
package goautoneg
import "bitbucket.org/ww/goautoneg"
HTTP Content-Type Autonegotiation.
The functions in this package implement the behaviour specified in
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
Copyright (c) 2011, Open Knowledge Foundation Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
Neither the name of the Open Knowledge Foundation Ltd. nor the
names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
FUNCTIONS
func Negotiate(header string, alternatives []string) (content_type string)
Negotiate the most appropriate content_type given the accept header
and a list of alternatives.
func ParseAccept(header string) (accept []Accept)
Parse an Accept Header string returning a sorted list
of clauses
TYPES
type Accept struct {
Type, SubType string
Q float32
Params map[string]string
}
Structure to represent a clause in an HTTP Accept Header
SUBDIRECTORIES
.hg

View File

@@ -0,0 +1,162 @@
/*
HTTP Content-Type Autonegotiation.
The functions in this package implement the behaviour specified in
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
Copyright (c) 2011, Open Knowledge Foundation Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
Neither the name of the Open Knowledge Foundation Ltd. nor the
names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package goautoneg
import (
"sort"
"strconv"
"strings"
)
// Structure to represent a clause in an HTTP Accept Header
type Accept struct {
Type, SubType string
Q float64
Params map[string]string
}
// For internal use, so that we can use the sort interface
type accept_slice []Accept
func (accept accept_slice) Len() int {
slice := []Accept(accept)
return len(slice)
}
func (accept accept_slice) Less(i, j int) bool {
slice := []Accept(accept)
ai, aj := slice[i], slice[j]
if ai.Q > aj.Q {
return true
}
if ai.Type != "*" && aj.Type == "*" {
return true
}
if ai.SubType != "*" && aj.SubType == "*" {
return true
}
return false
}
func (accept accept_slice) Swap(i, j int) {
slice := []Accept(accept)
slice[i], slice[j] = slice[j], slice[i]
}
// Parse an Accept Header string returning a sorted list
// of clauses
func ParseAccept(header string) (accept []Accept) {
parts := strings.Split(header, ",")
accept = make([]Accept, 0, len(parts))
for _, part := range parts {
part := strings.Trim(part, " ")
a := Accept{}
a.Params = make(map[string]string)
a.Q = 1.0
mrp := strings.Split(part, ";")
media_range := mrp[0]
sp := strings.Split(media_range, "/")
a.Type = strings.Trim(sp[0], " ")
switch {
case len(sp) == 1 && a.Type == "*":
a.SubType = "*"
case len(sp) == 2:
a.SubType = strings.Trim(sp[1], " ")
default:
continue
}
if len(mrp) == 1 {
accept = append(accept, a)
continue
}
for _, param := range mrp[1:] {
sp := strings.SplitN(param, "=", 2)
if len(sp) != 2 {
continue
}
token := strings.Trim(sp[0], " ")
if token == "q" {
a.Q, _ = strconv.ParseFloat(sp[1], 32)
} else {
a.Params[token] = strings.Trim(sp[1], " ")
}
}
accept = append(accept, a)
}
slice := accept_slice(accept)
sort.Sort(slice)
return
}
// Negotiate the most appropriate content_type given the accept header
// and a list of alternatives.
func Negotiate(header string, alternatives []string) (content_type string) {
asp := make([][]string, 0, len(alternatives))
for _, ctype := range alternatives {
asp = append(asp, strings.SplitN(ctype, "/", 2))
}
for _, clause := range ParseAccept(header) {
for i, ctsp := range asp {
if clause.Type == ctsp[0] && clause.SubType == ctsp[1] {
content_type = alternatives[i]
return
}
if clause.Type == ctsp[0] && clause.SubType == "*" {
content_type = alternatives[i]
return
}
if clause.Type == "*" && clause.SubType == "*" {
content_type = alternatives[i]
return
}
}
}
return
}

View File

@@ -0,0 +1,33 @@
package goautoneg
import (
"testing"
)
var chrome = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
func TestParseAccept(t *testing.T) {
alternatives := []string{"text/html", "image/png"}
content_type := Negotiate(chrome, alternatives)
if content_type != "image/png" {
t.Errorf("got %s expected image/png", content_type)
}
alternatives = []string{"text/html", "text/plain", "text/n3"}
content_type = Negotiate(chrome, alternatives)
if content_type != "text/html" {
t.Errorf("got %s expected text/html", content_type)
}
alternatives = []string{"text/n3", "text/plain"}
content_type = Negotiate(chrome, alternatives)
if content_type != "text/plain" {
t.Errorf("got %s expected text/plain", content_type)
}
alternatives = []string{"text/n3", "application/rdf+xml"}
content_type = Negotiate(chrome, alternatives)
if content_type != "text/n3" {
t.Errorf("got %s expected text/n3", content_type)
}
}

View File

@@ -0,0 +1,63 @@
package quantile
import (
"testing"
)
func BenchmarkInsertTargeted(b *testing.B) {
b.ReportAllocs()
s := NewTargeted(Targets)
b.ResetTimer()
for i := float64(0); i < float64(b.N); i++ {
s.Insert(i)
}
}
func BenchmarkInsertTargetedSmallEpsilon(b *testing.B) {
s := NewTargeted(TargetsSmallEpsilon)
b.ResetTimer()
for i := float64(0); i < float64(b.N); i++ {
s.Insert(i)
}
}
func BenchmarkInsertBiased(b *testing.B) {
s := NewLowBiased(0.01)
b.ResetTimer()
for i := float64(0); i < float64(b.N); i++ {
s.Insert(i)
}
}
func BenchmarkInsertBiasedSmallEpsilon(b *testing.B) {
s := NewLowBiased(0.0001)
b.ResetTimer()
for i := float64(0); i < float64(b.N); i++ {
s.Insert(i)
}
}
func BenchmarkQuery(b *testing.B) {
s := NewTargeted(Targets)
for i := float64(0); i < 1e6; i++ {
s.Insert(i)
}
b.ResetTimer()
n := float64(b.N)
for i := float64(0); i < n; i++ {
s.Query(i / n)
}
}
func BenchmarkQuerySmallEpsilon(b *testing.B) {
s := NewTargeted(TargetsSmallEpsilon)
for i := float64(0); i < 1e6; i++ {
s.Insert(i)
}
b.ResetTimer()
n := float64(b.N)
for i := float64(0); i < n; i++ {
s.Query(i / n)
}
}

View File

@@ -0,0 +1,112 @@
// +build go1.1
package quantile_test
import (
"bufio"
"fmt"
"github.com/bmizerany/perks/quantile"
"log"
"os"
"strconv"
"time"
)
func Example_simple() {
ch := make(chan float64)
go sendFloats(ch)
// Compute the 50th, 90th, and 99th percentile.
q := quantile.NewTargeted(0.50, 0.90, 0.99)
for v := range ch {
q.Insert(v)
}
fmt.Println("perc50:", q.Query(0.50))
fmt.Println("perc90:", q.Query(0.90))
fmt.Println("perc99:", q.Query(0.99))
fmt.Println("count:", q.Count())
// Output:
// perc50: 5
// perc90: 14
// perc99: 40
// count: 2388
}
func Example_mergeMultipleStreams() {
// Scenario:
// We have multiple database shards. On each shard, there is a process
// collecting query response times from the database logs and inserting
// them into a Stream (created via NewTargeted(0.90)), much like the
// Simple example. These processes expose a network interface for us to
// ask them to serialize and send us the results of their
// Stream.Samples so we may Merge and Query them.
//
// NOTES:
// * These sample sets are small, allowing us to get them
// across the network much faster than sending the entire list of data
// points.
//
// * For this to work correctly, we must supply the same quantiles
// a priori the process collecting the samples supplied to NewTargeted,
// even if we do not plan to query them all here.
ch := make(chan quantile.Samples)
getDBQuerySamples(ch)
q := quantile.NewTargeted(0.90)
for samples := range ch {
q.Merge(samples)
}
fmt.Println("perc90:", q.Query(0.90))
}
func Example_window() {
// Scenario: We want the 90th, 95th, and 99th percentiles for each
// minute.
ch := make(chan float64)
go sendStreamValues(ch)
tick := time.NewTicker(1 * time.Minute)
q := quantile.NewTargeted(0.90, 0.95, 0.99)
for {
select {
case t := <-tick.C:
flushToDB(t, q.Samples())
q.Reset()
case v := <-ch:
q.Insert(v)
}
}
}
func sendStreamValues(ch chan float64) {
// Use your imagination
}
func flushToDB(t time.Time, samples quantile.Samples) {
// Use your imagination
}
// This is a stub for the above example. In reality this would hit the remote
// servers via http or something like it.
func getDBQuerySamples(ch chan quantile.Samples) {}
func sendFloats(ch chan<- float64) {
f, err := os.Open("exampledata.txt")
if err != nil {
log.Fatal(err)
}
sc := bufio.NewScanner(f)
for sc.Scan() {
b := sc.Bytes()
v, err := strconv.ParseFloat(string(b), 64)
if err != nil {
log.Fatal(err)
}
ch <- v
}
if sc.Err() != nil {
log.Fatal(sc.Err())
}
close(ch)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,292 @@
// Package quantile computes approximate quantiles over an unbounded data
// stream within low memory and CPU bounds.
//
// A small amount of accuracy is traded to achieve the above properties.
//
// Multiple streams can be merged before calling Query to generate a single set
// of results. This is meaningful when the streams represent the same type of
// data. See Merge and Samples.
//
// For more detailed information about the algorithm used, see:
//
// Effective Computation of Biased Quantiles over Data Streams
//
// http://www.cs.rutgers.edu/~muthu/bquant.pdf
package quantile
import (
"math"
"sort"
)
// Sample holds an observed value and meta information for compression. JSON
// tags have been added for convenience.
type Sample struct {
Value float64 `json:",string"`
Width float64 `json:",string"`
Delta float64 `json:",string"`
}
// Samples represents a slice of samples. It implements sort.Interface.
type Samples []Sample
func (a Samples) Len() int { return len(a) }
func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value }
func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type invariant func(s *stream, r float64) float64
// NewLowBiased returns an initialized Stream for low-biased quantiles
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
// error guarantees can still be given even for the lower ranks of the data
// distribution.
//
// The provided epsilon is a relative error, i.e. the true quantile of a value
// returned by a query is guaranteed to be within (1±Epsilon)*Quantile.
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
// properties.
func NewLowBiased(epsilon float64) *Stream {
ƒ := func(s *stream, r float64) float64 {
return 2 * epsilon * r
}
return newStream(ƒ)
}
// NewHighBiased returns an initialized Stream for high-biased quantiles
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
// error guarantees can still be given even for the higher ranks of the data
// distribution.
//
// The provided epsilon is a relative error, i.e. the true quantile of a value
// returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile).
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
// properties.
func NewHighBiased(epsilon float64) *Stream {
ƒ := func(s *stream, r float64) float64 {
return 2 * epsilon * (s.n - r)
}
return newStream(ƒ)
}
// NewTargeted returns an initialized Stream concerned with a particular set of
// quantile values that are supplied a priori. Knowing these a priori reduces
// space and computation time. The targets map maps the desired quantiles to
// their absolute errors, i.e. the true quantile of a value returned by a query
// is guaranteed to be within (Quantile±Epsilon).
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties.
func NewTargeted(targets map[float64]float64) *Stream {
ƒ := func(s *stream, r float64) float64 {
var m = math.MaxFloat64
var f float64
for quantile, epsilon := range targets {
if quantile*s.n <= r {
f = (2 * epsilon * r) / quantile
} else {
f = (2 * epsilon * (s.n - r)) / (1 - quantile)
}
if f < m {
m = f
}
}
return m
}
return newStream(ƒ)
}
// Stream computes quantiles for a stream of float64s. It is not thread-safe by
// design. Take care when using across multiple goroutines.
type Stream struct {
*stream
b Samples
sorted bool
}
func newStream(ƒ invariant) *Stream {
x := &stream{ƒ: ƒ}
return &Stream{x, make(Samples, 0, 500), true}
}
// Insert inserts v into the stream.
func (s *Stream) Insert(v float64) {
s.insert(Sample{Value: v, Width: 1})
}
func (s *Stream) insert(sample Sample) {
s.b = append(s.b, sample)
s.sorted = false
if len(s.b) == cap(s.b) {
s.flush()
}
}
// Query returns the computed qth percentiles value. If s was created with
// NewTargeted, and q is not in the set of quantiles provided a priori, Query
// will return an unspecified result.
func (s *Stream) Query(q float64) float64 {
if !s.flushed() {
// Fast path when there hasn't been enough data for a flush;
// this also yields better accuracy for small sets of data.
l := len(s.b)
if l == 0 {
return 0
}
i := int(float64(l) * q)
if i > 0 {
i -= 1
}
s.maybeSort()
return s.b[i].Value
}
s.flush()
return s.stream.query(q)
}
// Merge merges samples into the underlying streams samples. This is handy when
// merging multiple streams from separate threads, database shards, etc.
//
// ATTENTION: This method is broken and does not yield correct results. The
// underlying algorithm is not capable of merging streams correctly.
func (s *Stream) Merge(samples Samples) {
sort.Sort(samples)
s.stream.merge(samples)
}
// Reset reinitializes and clears the list reusing the samples buffer memory.
func (s *Stream) Reset() {
s.stream.reset()
s.b = s.b[:0]
}
// Samples returns stream samples held by s.
func (s *Stream) Samples() Samples {
if !s.flushed() {
return s.b
}
s.flush()
return s.stream.samples()
}
// Count returns the total number of samples observed in the stream
// since initialization.
func (s *Stream) Count() int {
return len(s.b) + s.stream.count()
}
func (s *Stream) flush() {
s.maybeSort()
s.stream.merge(s.b)
s.b = s.b[:0]
}
func (s *Stream) maybeSort() {
if !s.sorted {
s.sorted = true
sort.Sort(s.b)
}
}
func (s *Stream) flushed() bool {
return len(s.stream.l) > 0
}
type stream struct {
n float64
l []Sample
ƒ invariant
}
func (s *stream) reset() {
s.l = s.l[:0]
s.n = 0
}
func (s *stream) insert(v float64) {
s.merge(Samples{{v, 1, 0}})
}
func (s *stream) merge(samples Samples) {
// TODO(beorn7): This tries to merge not only individual samples, but
// whole summaries. The paper doesn't mention merging summaries at
// all. Unittests show that the merging is inaccurate. Find out how to
// do merges properly.
var r float64
i := 0
for _, sample := range samples {
for ; i < len(s.l); i++ {
c := s.l[i]
if c.Value > sample.Value {
// Insert at position i.
s.l = append(s.l, Sample{})
copy(s.l[i+1:], s.l[i:])
s.l[i] = Sample{
sample.Value,
sample.Width,
math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1),
// TODO(beorn7): How to calculate delta correctly?
}
i++
goto inserted
}
r += c.Width
}
s.l = append(s.l, Sample{sample.Value, sample.Width, 0})
i++
inserted:
s.n += sample.Width
r += sample.Width
}
s.compress()
}
func (s *stream) count() int {
return int(s.n)
}
func (s *stream) query(q float64) float64 {
t := math.Ceil(q * s.n)
t += math.Ceil(s.ƒ(s, t) / 2)
p := s.l[0]
var r float64
for _, c := range s.l[1:] {
r += p.Width
if r+c.Width+c.Delta > t {
return p.Value
}
p = c
}
return p.Value
}
func (s *stream) compress() {
if len(s.l) < 2 {
return
}
x := s.l[len(s.l)-1]
xi := len(s.l) - 1
r := s.n - 1 - x.Width
for i := len(s.l) - 2; i >= 0; i-- {
c := s.l[i]
if c.Width+x.Width+x.Delta <= s.ƒ(s, r) {
x.Width += c.Width
s.l[xi] = x
// Remove element at i.
copy(s.l[i:], s.l[i+1:])
s.l = s.l[:len(s.l)-1]
xi -= 1
} else {
x = c
xi = i
}
r -= c.Width
}
}
func (s *stream) samples() Samples {
samples := make(Samples, len(s.l))
copy(samples, s.l)
return samples
}

View File

@@ -0,0 +1,185 @@
package quantile
import (
"math"
"math/rand"
"sort"
"testing"
)
var (
Targets = map[float64]float64{
0.01: 0.001,
0.10: 0.01,
0.50: 0.05,
0.90: 0.01,
0.99: 0.001,
}
TargetsSmallEpsilon = map[float64]float64{
0.01: 0.0001,
0.10: 0.001,
0.50: 0.005,
0.90: 0.001,
0.99: 0.0001,
}
LowQuantiles = []float64{0.01, 0.1, 0.5}
HighQuantiles = []float64{0.99, 0.9, 0.5}
)
const RelativeEpsilon = 0.01
func verifyPercsWithAbsoluteEpsilon(t *testing.T, a []float64, s *Stream) {
sort.Float64s(a)
for quantile, epsilon := range Targets {
n := float64(len(a))
k := int(quantile * n)
lower := int((quantile - epsilon) * n)
if lower < 1 {
lower = 1
}
upper := int(math.Ceil((quantile + epsilon) * n))
if upper > len(a) {
upper = len(a)
}
w, min, max := a[k-1], a[lower-1], a[upper-1]
if g := s.Query(quantile); g < min || g > max {
t.Errorf("q=%f: want %v [%f,%f], got %v", quantile, w, min, max, g)
}
}
}
func verifyLowPercsWithRelativeEpsilon(t *testing.T, a []float64, s *Stream) {
sort.Float64s(a)
for _, qu := range LowQuantiles {
n := float64(len(a))
k := int(qu * n)
lowerRank := int((1 - RelativeEpsilon) * qu * n)
upperRank := int(math.Ceil((1 + RelativeEpsilon) * qu * n))
w, min, max := a[k-1], a[lowerRank-1], a[upperRank-1]
if g := s.Query(qu); g < min || g > max {
t.Errorf("q=%f: want %v [%f,%f], got %v", qu, w, min, max, g)
}
}
}
func verifyHighPercsWithRelativeEpsilon(t *testing.T, a []float64, s *Stream) {
sort.Float64s(a)
for _, qu := range HighQuantiles {
n := float64(len(a))
k := int(qu * n)
lowerRank := int((1 - (1+RelativeEpsilon)*(1-qu)) * n)
upperRank := int(math.Ceil((1 - (1-RelativeEpsilon)*(1-qu)) * n))
w, min, max := a[k-1], a[lowerRank-1], a[upperRank-1]
if g := s.Query(qu); g < min || g > max {
t.Errorf("q=%f: want %v [%f,%f], got %v", qu, w, min, max, g)
}
}
}
func populateStream(s *Stream) []float64 {
a := make([]float64, 0, 1e5+100)
for i := 0; i < cap(a); i++ {
v := rand.NormFloat64()
// Add 5% asymmetric outliers.
if i%20 == 0 {
v = v*v + 1
}
s.Insert(v)
a = append(a, v)
}
return a
}
func TestTargetedQuery(t *testing.T) {
rand.Seed(42)
s := NewTargeted(Targets)
a := populateStream(s)
verifyPercsWithAbsoluteEpsilon(t, a, s)
}
func TestLowBiasedQuery(t *testing.T) {
rand.Seed(42)
s := NewLowBiased(RelativeEpsilon)
a := populateStream(s)
verifyLowPercsWithRelativeEpsilon(t, a, s)
}
func TestHighBiasedQuery(t *testing.T) {
rand.Seed(42)
s := NewHighBiased(RelativeEpsilon)
a := populateStream(s)
verifyHighPercsWithRelativeEpsilon(t, a, s)
}
func TestTargetedMerge(t *testing.T) {
rand.Seed(42)
s1 := NewTargeted(Targets)
s2 := NewTargeted(Targets)
a := populateStream(s1)
a = append(a, populateStream(s2)...)
s1.Merge(s2.Samples())
verifyPercsWithAbsoluteEpsilon(t, a, s1)
}
func TestLowBiasedMerge(t *testing.T) {
rand.Seed(42)
s1 := NewLowBiased(RelativeEpsilon)
s2 := NewLowBiased(RelativeEpsilon)
a := populateStream(s1)
a = append(a, populateStream(s2)...)
s1.Merge(s2.Samples())
verifyLowPercsWithRelativeEpsilon(t, a, s2)
}
func TestHighBiasedMerge(t *testing.T) {
rand.Seed(42)
s1 := NewHighBiased(RelativeEpsilon)
s2 := NewHighBiased(RelativeEpsilon)
a := populateStream(s1)
a = append(a, populateStream(s2)...)
s1.Merge(s2.Samples())
verifyHighPercsWithRelativeEpsilon(t, a, s2)
}
func TestUncompressed(t *testing.T) {
q := NewTargeted(Targets)
for i := 100; i > 0; i-- {
q.Insert(float64(i))
}
if g := q.Count(); g != 100 {
t.Errorf("want count 100, got %d", g)
}
// Before compression, Query should have 100% accuracy.
for quantile := range Targets {
w := quantile * 100
if g := q.Query(quantile); g != w {
t.Errorf("want %f, got %f", w, g)
}
}
}
func TestUncompressedSamples(t *testing.T) {
q := NewTargeted(map[float64]float64{0.99: 0.001})
for i := 1; i <= 100; i++ {
q.Insert(float64(i))
}
if g := q.Samples().Len(); g != 100 {
t.Errorf("want count 100, got %d", g)
}
}
func TestUncompressedOne(t *testing.T) {
q := NewTargeted(map[float64]float64{0.99: 0.01})
q.Insert(3.14)
if g := q.Query(0.90); g != 3.14 {
t.Error("want PI, got", g)
}
}
func TestDefaults(t *testing.T) {
if g := NewTargeted(map[float64]float64{0.99: 0.001}).Query(0.99); g != 0 {
t.Errorf("want 0, got %f", g)
}
}

View File

@@ -0,0 +1,110 @@
// 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 (
"fmt"
"strconv"
)
// Fingerprint provides a hash-capable representation of a Metric.
// For our purposes, FNV-1A 64-bit is used.
type Fingerprint uint64
func (f Fingerprint) String() string {
return fmt.Sprintf("%016x", uint64(f))
}
// Less implements sort.Interface.
func (f Fingerprint) Less(o Fingerprint) bool {
return f < o
}
// Equal implements sort.Interface.
func (f Fingerprint) Equal(o Fingerprint) bool {
return f == o
}
// LoadFromString transforms a string representation into a Fingerprint.
func (f *Fingerprint) LoadFromString(s string) error {
num, err := strconv.ParseUint(s, 16, 64)
if err != nil {
return err
}
*f = Fingerprint(num)
return nil
}
// Fingerprints represents a collection of Fingerprint subject to a given
// natural sorting scheme. It implements sort.Interface.
type Fingerprints []Fingerprint
// Len implements sort.Interface.
func (f Fingerprints) Len() int {
return len(f)
}
// Less implements sort.Interface.
func (f Fingerprints) Less(i, j int) bool {
return f[i] < f[j]
}
// Swap implements sort.Interface.
func (f Fingerprints) Swap(i, j int) {
f[i], f[j] = f[j], f[i]
}
// FingerprintSet is a set of Fingerprints.
type FingerprintSet map[Fingerprint]struct{}
// Equal returns true if both sets contain the same elements (and not more).
func (s FingerprintSet) Equal(o FingerprintSet) bool {
if len(s) != len(o) {
return false
}
for k := range s {
if _, ok := o[k]; !ok {
return false
}
}
return true
}
// Intersection returns the elements contained in both sets.
func (s FingerprintSet) Intersection(o FingerprintSet) FingerprintSet {
myLength, otherLength := len(s), len(o)
if myLength == 0 || otherLength == 0 {
return FingerprintSet{}
}
subSet := s
superSet := o
if otherLength < myLength {
subSet = o
superSet = s
}
out := FingerprintSet{}
for k := range subSet {
if _, ok := superSet[k]; ok {
out[k] = struct{}{}
}
}
return out
}

View File

@@ -0,0 +1,63 @@
// 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 (
"strings"
)
const (
// ExporterLabelPrefix is the label name prefix to prepend if a
// synthetic label is already present in the exported metrics.
ExporterLabelPrefix LabelName = "exporter_"
// MetricNameLabel is the label name indicating the metric name of a
// timeseries.
MetricNameLabel LabelName = "__name__"
// ReservedLabelPrefix is a prefix which is not legal in user-supplied
// label names.
ReservedLabelPrefix = "__"
// JobLabel is the label name indicating the job from which a timeseries
// was scraped.
JobLabel LabelName = "job"
)
// A LabelName is a key for a LabelSet or Metric. It has a value associated
// therewith.
type LabelName string
// LabelNames is a sortable LabelName slice. In implements sort.Interface.
type LabelNames []LabelName
func (l LabelNames) Len() int {
return len(l)
}
func (l LabelNames) Less(i, j int) bool {
return l[i] < l[j]
}
func (l LabelNames) Swap(i, j int) {
l[i], l[j] = l[j], l[i]
}
func (l LabelNames) String() string {
labelStrings := make([]string, 0, len(l))
for _, label := range l {
labelStrings = append(labelStrings, string(label))
}
return strings.Join(labelStrings, ", ")
}

View File

@@ -0,0 +1,55 @@
// 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 (
"sort"
"testing"
)
func testLabelNames(t testing.TB) {
var scenarios = []struct {
in LabelNames
out LabelNames
}{
{
in: LabelNames{"ZZZ", "zzz"},
out: LabelNames{"ZZZ", "zzz"},
},
{
in: LabelNames{"aaa", "AAA"},
out: LabelNames{"AAA", "aaa"},
},
}
for i, scenario := range scenarios {
sort.Sort(scenario.in)
for j, expected := range scenario.out {
if expected != scenario.in[j] {
t.Errorf("%d.%d expected %s, got %s", i, j, expected, scenario.in[j])
}
}
}
}
func TestLabelNames(t *testing.T) {
testLabelNames(t)
}
func BenchmarkLabelNames(b *testing.B) {
for i := 0; i < b.N; i++ {
testLabelNames(b)
}
}

View File

@@ -0,0 +1,64 @@
// 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 (
"fmt"
"sort"
"strings"
)
// A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet
// may be fully-qualified down to the point where it may resolve to a single
// Metric in the data store or not. All operations that occur within the realm
// of a LabelSet can emit a vector of Metric entities to which the LabelSet may
// match.
type LabelSet map[LabelName]LabelValue
// Merge is a helper function to non-destructively merge two label sets.
func (l LabelSet) Merge(other LabelSet) LabelSet {
result := make(LabelSet, len(l))
for k, v := range l {
result[k] = v
}
for k, v := range other {
result[k] = v
}
return result
}
func (l LabelSet) String() string {
labelStrings := make([]string, 0, len(l))
for label, value := range l {
labelStrings = append(labelStrings, fmt.Sprintf("%s=%q", label, value))
}
switch len(labelStrings) {
case 0:
return ""
default:
sort.Strings(labelStrings)
return fmt.Sprintf("{%s}", strings.Join(labelStrings, ", "))
}
}
// MergeFromMetric merges Metric into this LabelSet.
func (l LabelSet) MergeFromMetric(m Metric) {
for k, v := range m {
l[k] = v
}
}

View File

@@ -0,0 +1,36 @@
// 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 (
"sort"
)
// A LabelValue is an associated value for a LabelName.
type LabelValue string
// LabelValues is a sortable LabelValue slice. It implements sort.Interface.
type LabelValues []LabelValue
func (l LabelValues) Len() int {
return len(l)
}
func (l LabelValues) Less(i, j int) bool {
return sort.StringsAreSorted([]string{string(l[i]), string(l[j])})
}
func (l LabelValues) Swap(i, j int) {
l[i], l[j] = l[j], l[i]
}

View File

@@ -0,0 +1,55 @@
// 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 (
"sort"
"testing"
)
func testLabelValues(t testing.TB) {
var scenarios = []struct {
in LabelValues
out LabelValues
}{
{
in: LabelValues{"ZZZ", "zzz"},
out: LabelValues{"ZZZ", "zzz"},
},
{
in: LabelValues{"aaa", "AAA"},
out: LabelValues{"AAA", "aaa"},
},
}
for i, scenario := range scenarios {
sort.Sort(scenario.in)
for j, expected := range scenario.out {
if expected != scenario.in[j] {
t.Errorf("%d.%d expected %s, got %s", i, j, expected, scenario.in[j])
}
}
}
}
func TestLabelValues(t *testing.T) {
testLabelValues(t)
}
func BenchmarkLabelValues(b *testing.B) {
for i := 0; i < b.N; i++ {
testLabelValues(b)
}
}

View File

@@ -0,0 +1,151 @@
// 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/binary"
"encoding/json"
"fmt"
"hash/fnv"
"sort"
"strings"
)
// A Metric is similar to a LabelSet, but the key difference is that a Metric is
// a singleton and refers to one and only one stream of samples.
type Metric map[LabelName]LabelValue
// Equal compares the fingerprints of both metrics.
func (m Metric) Equal(o Metric) bool {
return m.Fingerprint().Equal(o.Fingerprint())
}
// Before compares the fingerprints of both metrics.
func (m Metric) Before(o Metric) bool {
return m.Fingerprint().Less(o.Fingerprint())
}
// String implements Stringer.
func (m Metric) String() string {
metricName, hasName := m[MetricNameLabel]
numLabels := len(m) - 1
if !hasName {
numLabels = len(m)
}
labelStrings := make([]string, 0, numLabels)
for label, value := range m {
if label != MetricNameLabel {
labelStrings = append(labelStrings, fmt.Sprintf("%s=%q", label, value))
}
}
switch numLabels {
case 0:
if hasName {
return string(metricName)
}
return "{}"
default:
sort.Strings(labelStrings)
return fmt.Sprintf("%s{%s}", metricName, strings.Join(labelStrings, ", "))
}
}
// Fingerprint returns a Metric's Fingerprint.
func (m Metric) Fingerprint() Fingerprint {
labelLength := len(m)
labelNames := make([]string, 0, labelLength)
for labelName := range m {
labelNames = append(labelNames, string(labelName))
}
sort.Strings(labelNames)
summer := fnv.New64a()
for _, labelName := range labelNames {
labelValue := m[LabelName(labelName)]
summer.Write([]byte(labelName))
summer.Write([]byte{0})
summer.Write([]byte(labelValue))
}
return Fingerprint(binary.LittleEndian.Uint64(summer.Sum(nil)))
}
// Clone returns a copy of the Metric.
func (m Metric) Clone() Metric {
clone := Metric{}
for k, v := range m {
clone[k] = v
}
return clone
}
// MergeFromLabelSet merges a label set into this Metric, prefixing a collision
// prefix to the label names merged from the label set where required.
func (m Metric) MergeFromLabelSet(labels LabelSet, collisionPrefix LabelName) {
for k, v := range labels {
if collisionPrefix != "" {
for {
if _, exists := m[k]; !exists {
break
}
k = collisionPrefix + k
}
}
m[k] = v
}
}
// COWMetric wraps a Metric to enable copy-on-write access patterns.
type COWMetric struct {
Copied bool
Metric Metric
}
// Set sets a label name in the wrapped Metric to a given value and copies the
// Metric initially, if it is not already a copy.
func (m COWMetric) Set(ln LabelName, lv LabelValue) {
m.doCOW()
m.Metric[ln] = lv
}
// Delete deletes a given label name from the wrapped Metric and copies the
// Metric initially, if it is not already a copy.
func (m *COWMetric) Delete(ln LabelName) {
m.doCOW()
delete(m.Metric, ln)
}
// doCOW copies the underlying Metric if it is not already a copy.
func (m *COWMetric) doCOW() {
if !m.Copied {
m.Metric = m.Metric.Clone()
m.Copied = true
}
}
// String implements fmt.Stringer.
func (m COWMetric) String() string {
return m.Metric.String()
}
// MarshalJSON implements json.Marshaler.
func (m COWMetric) MarshalJSON() ([]byte, error) {
return json.Marshal(m.Metric)
}

View File

@@ -0,0 +1,58 @@
// 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 "testing"
func testMetric(t testing.TB) {
var scenarios = []struct {
input Metric
fingerprint Fingerprint
}{
{
input: Metric{},
fingerprint: 2676020557754725067,
},
{
input: Metric{
"first_name": "electro",
"occupation": "robot",
"manufacturer": "westinghouse",
},
fingerprint: 13260944541294022935,
},
{
input: Metric{
"x": "y",
},
fingerprint: 1470933794305433534,
},
}
for i, scenario := range scenarios {
if scenario.fingerprint != scenario.input.Fingerprint() {
t.Errorf("%d. expected %d, got %d", i, scenario.fingerprint, scenario.input.Fingerprint())
}
}
}
func TestMetric(t *testing.T) {
testMetric(t)
}
func BenchmarkMetric(b *testing.B) {
for i := 0; i < b.N; i++ {
testMetric(b)
}
}

View File

@@ -0,0 +1,15 @@
// 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 contains core representation of Prometheus client primitives.
package model

View File

@@ -0,0 +1,79 @@
// 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
// Sample is a sample value with a timestamp and a metric.
type Sample struct {
Metric Metric
Value SampleValue
Timestamp Timestamp
}
// Equal compares first the metrics, then the timestamp, then the value.
func (s *Sample) Equal(o *Sample) bool {
if s == o {
return true
}
if !s.Metric.Equal(o.Metric) {
return false
}
if !s.Timestamp.Equal(o.Timestamp) {
return false
}
if !s.Value.Equal(o.Value) {
return false
}
return true
}
// Samples is a sortable Sample slice. It implements sort.Interface.
type Samples []*Sample
func (s Samples) Len() int {
return len(s)
}
// Less compares first the metrics, then the timestamp.
func (s Samples) Less(i, j int) bool {
switch {
case s[i].Metric.Before(s[j].Metric):
return true
case s[j].Metric.Before(s[i].Metric):
return false
case s[i].Timestamp.Before(s[j].Timestamp):
return true
default:
return false
}
}
func (s Samples) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Equal compares two sets of samples and returns true if they are equal.
func (s Samples) Equal(o Samples) bool {
if len(s) != len(o) {
return false
}
for i, sample := range s {
if !sample.Equal(o[i]) {
return false
}
}
return true
}

View File

@@ -0,0 +1,126 @@
// 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 (
"sort"
"testing"
)
func TestSamplesSort(t *testing.T) {
input := Samples{
&Sample{
// Fingerprint: 81f9c9ed24563f8f.
Metric: Metric{
MetricNameLabel: "A",
},
Timestamp: 1,
},
&Sample{
// Fingerprint: 81f9c9ed24563f8f.
Metric: Metric{
MetricNameLabel: "A",
},
Timestamp: 2,
},
&Sample{
// Fingerprint: 1bf6c9ed24543f8f.
Metric: Metric{
MetricNameLabel: "C",
},
Timestamp: 1,
},
&Sample{
// Fingerprint: 1bf6c9ed24543f8f.
Metric: Metric{
MetricNameLabel: "C",
},
Timestamp: 2,
},
&Sample{
// Fingerprint: 68f4c9ed24533f8f.
Metric: Metric{
MetricNameLabel: "B",
},
Timestamp: 1,
},
&Sample{
// Fingerprint: 68f4c9ed24533f8f.
Metric: Metric{
MetricNameLabel: "B",
},
Timestamp: 2,
},
}
expected := Samples{
&Sample{
// Fingerprint: 1bf6c9ed24543f8f.
Metric: Metric{
MetricNameLabel: "C",
},
Timestamp: 1,
},
&Sample{
// Fingerprint: 1bf6c9ed24543f8f.
Metric: Metric{
MetricNameLabel: "C",
},
Timestamp: 2,
},
&Sample{
// Fingerprint: 68f4c9ed24533f8f.
Metric: Metric{
MetricNameLabel: "B",
},
Timestamp: 1,
},
&Sample{
// Fingerprint: 68f4c9ed24533f8f.
Metric: Metric{
MetricNameLabel: "B",
},
Timestamp: 2,
},
&Sample{
// Fingerprint: 81f9c9ed24563f8f.
Metric: Metric{
MetricNameLabel: "A",
},
Timestamp: 1,
},
&Sample{
// Fingerprint: 81f9c9ed24563f8f.
Metric: Metric{
MetricNameLabel: "A",
},
Timestamp: 2,
},
}
sort.Sort(input)
for i, actual := range input {
actualFp := actual.Metric.Fingerprint()
expectedFp := expected[i].Metric.Fingerprint()
if !actualFp.Equal(expectedFp) {
t.Fatalf("%d. Incorrect fingerprint. Got %s; want %s", i, actualFp.String(), expectedFp.String())
}
if actual.Timestamp != expected[i].Timestamp {
t.Fatalf("%d. Incorrect timestamp. Got %s; want %s", i, actual.Timestamp, expected[i].Timestamp)
}
}
}

View File

@@ -0,0 +1,37 @@
// 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 (
"fmt"
"strconv"
)
// A SampleValue is a representation of a value for a given sample at a given
// time.
type SampleValue float64
// Equal does a straight v==o.
func (v SampleValue) Equal(o SampleValue) bool {
return v == o
}
// MarshalJSON implements json.Marshaler.
func (v SampleValue) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, v)), nil
}
func (v SampleValue) String() string {
return strconv.FormatFloat(float64(v), 'f', -1, 64)
}

View File

@@ -0,0 +1,97 @@
// Copyright 2014 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 (
"bytes"
"hash"
"hash/fnv"
)
// SeparatorByte is a byte that cannot occur in valid UTF-8 sequences and is
// used to separate label names, label values, and other strings from each other
// when calculating their combined hash value (aka signature aka fingerprint).
const SeparatorByte byte = 255
var (
// cache the signature of an empty label set.
emptyLabelSignature = fnv.New64a().Sum64()
hashAndBufPool = make(chan *hashAndBuf, 1024)
)
type hashAndBuf struct {
h hash.Hash64
b bytes.Buffer
}
func getHashAndBuf() *hashAndBuf {
select {
case hb := <-hashAndBufPool:
return hb
default:
return &hashAndBuf{h: fnv.New64a()}
}
}
func putHashAndBuf(hb *hashAndBuf) {
select {
case hashAndBufPool <- hb:
default:
}
}
// LabelsToSignature returns a unique signature (i.e., fingerprint) for a given
// label set.
func LabelsToSignature(labels map[string]string) uint64 {
if len(labels) == 0 {
return emptyLabelSignature
}
var result uint64
hb := getHashAndBuf()
defer putHashAndBuf(hb)
for k, v := range labels {
hb.b.WriteString(k)
hb.b.WriteByte(SeparatorByte)
hb.b.WriteString(v)
hb.h.Write(hb.b.Bytes())
result ^= hb.h.Sum64()
hb.h.Reset()
hb.b.Reset()
}
return result
}
// LabelValuesToSignature returns a unique signature (i.e., fingerprint) for the
// values of a given label set.
func LabelValuesToSignature(labels map[string]string) uint64 {
if len(labels) == 0 {
return emptyLabelSignature
}
var result uint64
hb := getHashAndBuf()
defer putHashAndBuf(hb)
for _, v := range labels {
hb.b.WriteString(v)
hb.h.Write(hb.b.Bytes())
result ^= hb.h.Sum64()
hb.h.Reset()
hb.b.Reset()
}
return result
}

View File

@@ -0,0 +1,120 @@
// Copyright 2014 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 (
"runtime"
"testing"
)
func testLabelsToSignature(t testing.TB) {
var scenarios = []struct {
in map[string]string
out uint64
}{
{
in: map[string]string{},
out: 14695981039346656037,
},
{
in: map[string]string{"name": "garland, briggs", "fear": "love is not enough"},
out: 12952432476264840823,
},
}
for i, scenario := range scenarios {
actual := LabelsToSignature(scenario.in)
if actual != scenario.out {
t.Errorf("%d. expected %d, got %d", i, scenario.out, actual)
}
}
}
func TestLabelToSignature(t *testing.T) {
testLabelsToSignature(t)
}
func TestEmptyLabelSignature(t *testing.T) {
input := []map[string]string{nil, {}}
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
alloc := ms.Alloc
for _, labels := range input {
LabelsToSignature(labels)
}
runtime.ReadMemStats(&ms)
if got := ms.Alloc; alloc != got {
t.Fatal("expected LabelsToSignature with empty labels not to perform allocations")
}
}
func BenchmarkLabelToSignature(b *testing.B) {
for i := 0; i < b.N; i++ {
testLabelsToSignature(b)
}
}
func benchmarkLabelValuesToSignature(b *testing.B, l map[string]string, e uint64) {
for i := 0; i < b.N; i++ {
if a := LabelValuesToSignature(l); a != e {
b.Fatalf("expected signature of %d for %s, got %d", e, l, a)
}
}
}
func BenchmarkLabelValuesToSignatureScalar(b *testing.B) {
benchmarkLabelValuesToSignature(b, nil, 14695981039346656037)
}
func BenchmarkLabelValuesToSignatureSingle(b *testing.B) {
benchmarkLabelValuesToSignature(b, map[string]string{"first-label": "first-label-value"}, 2653746141194979650)
}
func BenchmarkLabelValuesToSignatureDouble(b *testing.B) {
benchmarkLabelValuesToSignature(b, map[string]string{"first-label": "first-label-value", "second-label": "second-label-value"}, 8893559499616767364)
}
func BenchmarkLabelValuesToSignatureTriple(b *testing.B) {
benchmarkLabelValuesToSignature(b, map[string]string{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 1685970066862087833)
}
func benchmarkLabelToSignature(b *testing.B, l map[string]string, e uint64) {
for i := 0; i < b.N; i++ {
if a := LabelsToSignature(l); a != e {
b.Fatalf("expected signature of %d for %s, got %d", e, l, a)
}
}
}
func BenchmarkLabelToSignatureScalar(b *testing.B) {
benchmarkLabelToSignature(b, nil, 14695981039346656037)
}
func BenchmarkLabelToSignatureSingle(b *testing.B) {
benchmarkLabelToSignature(b, map[string]string{"first-label": "first-label-value"}, 5147259542624943964)
}
func BenchmarkLabelToSignatureDouble(b *testing.B) {
benchmarkLabelToSignature(b, map[string]string{"first-label": "first-label-value", "second-label": "second-label-value"}, 18269973311206963528)
}
func BenchmarkLabelToSignatureTriple(b *testing.B) {
benchmarkLabelToSignature(b, map[string]string{"first-label": "first-label-value", "second-label": "second-label-value", "third-label": "third-label-value"}, 15738406913934009676)
}

View File

@@ -0,0 +1,112 @@
// 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 (
"strconv"
native_time "time"
)
// TODO(julius): Should this use milliseconds/nanoseconds instead? This is
// mostly hidden from the user of these types when using the
// 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
const (
// MinimumTick is the minimum supported time resolution. This has to be
// at least native_time.Second in order for the code below to work.
MinimumTick = native_time.Millisecond
// second is the timestamp duration equivalent to one second.
second = int64(native_time.Second / MinimumTick)
// The number of nanoseconds per minimum tick.
nanosPerTick = int64(MinimumTick / native_time.Nanosecond)
)
// Equal reports whether two timestamps represent the same instant.
func (t Timestamp) Equal(o Timestamp) bool {
return t == o
}
// Before reports whether the timestamp t is before o.
func (t Timestamp) Before(o Timestamp) bool {
return t < o
}
// After reports whether the timestamp t is after o.
func (t Timestamp) After(o Timestamp) bool {
return t > o
}
// Add returns the Timestamp t + d.
func (t Timestamp) Add(d native_time.Duration) Timestamp {
return t + Timestamp(d/MinimumTick)
}
// Sub returns the Duration t - o.
func (t Timestamp) Sub(o Timestamp) native_time.Duration {
return native_time.Duration(t-o) * MinimumTick
}
// Time returns the time.Time representation of t.
func (t Timestamp) Time() native_time.Time {
return native_time.Unix(int64(t)/second, (int64(t)%second)*nanosPerTick)
}
// Unix returns t as a Unix time, the number of seconds elapsed
// since January 1, 1970 UTC.
func (t Timestamp) Unix() int64 {
return int64(t) / second
}
// UnixNano returns t as a Unix time, the number of nanoseconds elapsed
// since January 1, 1970 UTC.
func (t Timestamp) UnixNano() int64 {
return int64(t) * nanosPerTick
}
// String returns a string representation of the timestamp.
func (t Timestamp) String() string {
return strconv.FormatFloat(float64(t)/float64(second), 'f', -1, 64)
}
func (t Timestamp) MarshalJSON() ([]byte, error) {
return []byte(t.String()), nil
}
// Now returns the current time as a Timestamp.
func Now() Timestamp {
return TimestampFromTime(native_time.Now())
}
// TimestampFromTime returns the Timestamp equivalent to the time.Time t.
func TimestampFromTime(t native_time.Time) Timestamp {
return TimestampFromUnixNano(t.UnixNano())
}
// TimestampFromUnix returns the Timestamp equivalent to the Unix timestamp t
// provided in seconds.
func TimestampFromUnix(t int64) Timestamp {
return Timestamp(t * second)
}
// TimestampFromUnixNano returns the Timestamp equivalent to the Unix timestamp
// t provided in nanoseconds.
func TimestampFromUnixNano(t int64) Timestamp {
return Timestamp(t / nanosPerTick)
}

View File

@@ -0,0 +1,86 @@
// 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 (
"testing"
native_time "time"
)
func TestComparators(t *testing.T) {
t1a := TimestampFromUnix(0)
t1b := TimestampFromUnix(0)
t2 := TimestampFromUnix(2*second - 1)
if !t1a.Equal(t1b) {
t.Fatalf("Expected %s to be equal to %s", t1a, t1b)
}
if t1a.Equal(t2) {
t.Fatalf("Expected %s to not be equal to %s", t1a, t2)
}
if !t1a.Before(t2) {
t.Fatalf("Expected %s to be before %s", t1a, t2)
}
if t1a.Before(t1b) {
t.Fatalf("Expected %s to not be before %s", t1a, t1b)
}
if !t2.After(t1a) {
t.Fatalf("Expected %s to be after %s", t2, t1a)
}
if t1b.After(t1a) {
t.Fatalf("Expected %s to not be after %s", t1b, t1a)
}
}
func TestTimestampConversions(t *testing.T) {
unixSecs := int64(1136239445)
unixNsecs := int64(123456789)
unixNano := unixSecs*1000000000 + unixNsecs
t1 := native_time.Unix(unixSecs, unixNsecs-unixNsecs%nanosPerTick)
t2 := native_time.Unix(unixSecs, unixNsecs)
ts := TimestampFromUnixNano(unixNano)
if !ts.Time().Equal(t1) {
t.Fatalf("Expected %s, got %s", t1, ts.Time())
}
// Test available precision.
ts = TimestampFromTime(t2)
if !ts.Time().Equal(t1) {
t.Fatalf("Expected %s, got %s", t1, ts.Time())
}
if ts.UnixNano() != unixNano-unixNano%nanosPerTick {
t.Fatalf("Expected %d, got %d", unixNano, ts.UnixNano())
}
}
func TestDuration(t *testing.T) {
duration := native_time.Second + native_time.Minute + native_time.Hour
goTime := native_time.Unix(1136239445, 0)
ts := TimestampFromTime(goTime)
if !goTime.Add(duration).Equal(ts.Add(duration).Time()) {
t.Fatalf("Expected %s to be equal to %s", goTime.Add(duration), ts.Add(duration))
}
earlier := ts.Add(-duration)
delta := ts.Sub(earlier)
if delta != duration {
t.Fatalf("Expected %s to be equal to %s", delta, duration)
}
}

View File

@@ -0,0 +1 @@
command-line-arguments.test

View File

@@ -0,0 +1,53 @@
# Overview
This is the [Prometheus](http://www.prometheus.io) telemetric
instrumentation client [Go](http://golang.org) client library. It
enable authors to define process-space metrics for their servers and
expose them through a web service interface for extraction,
aggregation, and a whole slew of other post processing techniques.
# Installing
$ go get github.com/prometheus/client_golang/prometheus
# Example
```go
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
)
var (
indexed = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "my_company",
Subsystem: "indexer",
Name: "documents_indexed",
Help: "The number of documents indexed.",
})
size = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "my_company",
Subsystem: "storage",
Name: "documents_total_size_bytes",
Help: "The total size of all documents in the storage."}})
)
func main() {
http.Handle("/metrics", prometheus.Handler())
indexed.Inc()
size.Set(5)
http.ListenAndServe(":8080", nil)
}
func init() {
prometheus.MustRegister(indexed)
prometheus.MustRegister(size)
}
```
# Documentation
[![GoDoc](https://godoc.org/github.com/prometheus/client_golang?status.png)](https://godoc.org/github.com/prometheus/client_golang)

View File

@@ -0,0 +1,131 @@
// Copyright 2014 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 (
"testing"
)
func BenchmarkCounterWithLabelValues(b *testing.B) {
m := NewCounterVec(
CounterOpts{
Name: "benchmark_counter",
Help: "A counter to benchmark it.",
},
[]string{"one", "two", "three"},
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.WithLabelValues("eins", "zwei", "drei").Inc()
}
}
func BenchmarkCounterWithMappedLabels(b *testing.B) {
m := NewCounterVec(
CounterOpts{
Name: "benchmark_counter",
Help: "A counter to benchmark it.",
},
[]string{"one", "two", "three"},
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.With(Labels{"two": "zwei", "one": "eins", "three": "drei"}).Inc()
}
}
func BenchmarkCounterWithPreparedMappedLabels(b *testing.B) {
m := NewCounterVec(
CounterOpts{
Name: "benchmark_counter",
Help: "A counter to benchmark it.",
},
[]string{"one", "two", "three"},
)
b.ReportAllocs()
b.ResetTimer()
labels := Labels{"two": "zwei", "one": "eins", "three": "drei"}
for i := 0; i < b.N; i++ {
m.With(labels).Inc()
}
}
func BenchmarkCounterNoLabels(b *testing.B) {
m := NewCounter(CounterOpts{
Name: "benchmark_counter",
Help: "A counter to benchmark it.",
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Inc()
}
}
func BenchmarkGaugeWithLabelValues(b *testing.B) {
m := NewGaugeVec(
GaugeOpts{
Name: "benchmark_gauge",
Help: "A gauge to benchmark it.",
},
[]string{"one", "two", "three"},
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.WithLabelValues("eins", "zwei", "drei").Set(3.1415)
}
}
func BenchmarkGaugeNoLabels(b *testing.B) {
m := NewGauge(GaugeOpts{
Name: "benchmark_gauge",
Help: "A gauge to benchmark it.",
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Set(3.1415)
}
}
func BenchmarkSummaryWithLabelValues(b *testing.B) {
m := NewSummaryVec(
SummaryOpts{
Name: "benchmark_summary",
Help: "A summary 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 BenchmarkSummaryNoLabels(b *testing.B) {
m := NewSummary(SummaryOpts{
Name: "benchmark_summary",
Help: "A summary to benchmark it.",
},
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Observe(3.1415)
}
}

View File

@@ -0,0 +1,75 @@
// Copyright 2014 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
// Collector is the interface implemented by anything that can be used by
// Prometheus to collect metrics. A Collector has to be registered for
// collection. See Register, MustRegister, RegisterOrGet, and MustRegisterOrGet.
//
// The stock metrics provided by this package (like Gauge, Counter, Summary) are
// also Collectors (which only ever collect one metric, namely itself). An
// implementer of Collector may, however, collect multiple metrics in a
// coordinated fashion and/or create metrics on the fly. Examples for collectors
// already implemented in this library are the metric vectors (i.e. collection
// of multiple instances of the same Metric but with different label values)
// like GaugeVec or SummaryVec, and the ExpvarCollector.
type Collector interface {
// Describe sends the super-set of all possible descriptors of metrics
// collected by this Collector to the provided channel and returns once
// the last descriptor has been sent. The sent descriptors fulfill the
// consistency and uniqueness requirements described in the Desc
// documentation. (It is valid if one and the same Collector sends
// duplicate descriptors. Those duplicates are simply ignored. However,
// two different Collectors must not send duplicate descriptors.) This
// method idempotently sends the same descriptors throughout the
// lifetime of the Collector. If a Collector encounters an error while
// executing this method, it must send an invalid descriptor (created
// with NewInvalidDesc) to signal the error to the registry.
Describe(chan<- *Desc)
// Collect is called by Prometheus when collecting metrics. The
// implementation sends each collected metric via the provided channel
// and returns once the last metric has been sent. The descriptor of
// each sent metric is one of those returned by Describe. Returned
// metrics that share the same descriptor must differ in their variable
// label values. This method may be called concurrently and must
// therefore be implemented in a concurrency safe way. Blocking occurs
// at the expense of total performance of rendering all registered
// metrics. Ideally, Collector implementations support concurrent
// readers.
Collect(chan<- Metric)
}
// SelfCollector implements Collector for a single Metric so that that the
// Metric collects itself. Add it as an anonymous field to a struct that
// implements Metric, and call Init with the Metric itself as an argument.
type SelfCollector struct {
self Metric
}
// Init provides the SelfCollector with a reference to the metric it is supposed
// to collect. It is usually called within the factory function to create a
// metric. See example.
func (c *SelfCollector) Init(self Metric) {
c.self = self
}
// Describe implements Collector.
func (c *SelfCollector) Describe(ch chan<- *Desc) {
ch <- c.self.Desc()
}
// Collect implements Collector.
func (c *SelfCollector) Collect(ch chan<- Metric) {
ch <- c.self
}

View File

@@ -0,0 +1,175 @@
// Copyright 2014 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 (
"errors"
"hash/fnv"
)
// Counter is a Metric that represents a single numerical value that only ever
// goes up. That implies that it cannot be used to count items whose number can
// also go down, e.g. the number of currently running goroutines. Those
// "counters" are represented by Gauges.
//
// A Counter is typically used to count requests served, tasks completed, errors
// occurred, etc.
//
// To create Counter instances, use NewCounter.
type Counter interface {
Metric
Collector
// Set is used to set the Counter to an arbitrary value. It is only used
// if you have to transfer a value from an external counter into this
// Prometheus metrics. Do not use it for regular handling of a
// Prometheus counter (as it can be used to break the contract of
// monotonically increasing values).
Set(float64)
// Inc increments the counter by 1.
Inc()
// Add adds the given value to the counter. It panics if the value is <
// 0.
Add(float64)
}
// CounterOpts is an alias for Opts. See there for doc comments.
type CounterOpts Opts
// NewCounter creates a new Counter based on the provided CounterOpts.
func NewCounter(opts CounterOpts) Counter {
desc := NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
)
result := &counter{value: value{desc: desc, valType: CounterValue, labelPairs: desc.constLabelPairs}}
result.Init(result) // Init self-collection.
return result
}
type counter struct {
value
}
func (c *counter) Add(v float64) {
if v < 0 {
panic(errors.New("counter cannot decrease in value"))
}
c.value.Add(v)
}
// 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
// if you want to count the same thing partitioned by various dimensions
// (e.g. number of http requests, partitioned by response code and
// method). Create instances with NewCounterVec.
//
// CounterVec embeds MetricVec. See there for a full list of methods with
// detailed documentation.
type CounterVec struct {
MetricVec
}
// NewCounterVec creates a new CounterVec based on the provided CounterOpts and
// partitioned by the given label names. At least one label name must be
// provided.
func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
desc := NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
labelNames,
opts.ConstLabels,
)
return &CounterVec{
MetricVec: MetricVec{
children: map[uint64]Metric{},
desc: desc,
hash: fnv.New64a(),
newMetric: func(lvs ...string) Metric {
result := &counter{value: value{
desc: desc,
valType: CounterValue,
labelPairs: makeLabelPairs(desc, lvs),
}}
result.Init(result) // Init self-collection.
return result
},
},
}
}
// GetMetricWithLabelValues replaces the method of the same name in
// MetricVec. The difference is that this method returns a Counter and not a
// Metric so that no type conversion is required.
func (m *CounterVec) GetMetricWithLabelValues(lvs ...string) (Counter, error) {
metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...)
if metric != nil {
return metric.(Counter), err
}
return nil, err
}
// GetMetricWith replaces the method of the same name in MetricVec. The
// difference is that this method returns a Counter and not a Metric so that no
// type conversion is required.
func (m *CounterVec) GetMetricWith(labels Labels) (Counter, error) {
metric, err := m.MetricVec.GetMetricWith(labels)
if metric != nil {
return metric.(Counter), err
}
return nil, err
}
// WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. By not returning an
// error, WithLabelValues allows shortcuts like
// myVec.WithLabelValues("404", "GET").Add(42)
func (m *CounterVec) WithLabelValues(lvs ...string) Counter {
return m.MetricVec.WithLabelValues(lvs...).(Counter)
}
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
func (m *CounterVec) With(labels Labels) Counter {
return m.MetricVec.With(labels).(Counter)
}
// CounterFunc is a Counter whose value is determined at collect time by calling a
// provided function.
//
// To create CounterFunc instances, use NewCounterFunc.
type CounterFunc interface {
Metric
Collector
}
// NewCounterFunc creates a new CounterFunc based on the provided
// CounterOpts. The value reported is determined by calling the given function
// from within the Write method. Take into account that metric collection may
// happen concurrently. If that results in concurrent calls to Write, like in
// the case where a CounterFunc is directly registered with Prometheus, the
// provided function must be concurrency-safe. The function should also honor
// the contract for a Counter (values only go up, not down), but compliance will
// not be checked.
func NewCounterFunc(opts CounterOpts, function func() float64) CounterFunc {
return newValueFunc(NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
), CounterValue, function)
}

View File

@@ -0,0 +1,58 @@
// Copyright 2014 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"
"testing"
dto "github.com/prometheus/client_model/go"
)
func TestCounterAdd(t *testing.T) {
counter := NewCounter(CounterOpts{
Name: "test",
Help: "test help",
ConstLabels: Labels{"a": "1", "b": "2"},
}).(*counter)
counter.Inc()
if expected, got := 1., math.Float64frombits(counter.valBits); expected != got {
t.Errorf("Expected %f, got %f.", expected, got)
}
counter.Add(42)
if expected, got := 43., math.Float64frombits(counter.valBits); expected != got {
t.Errorf("Expected %f, got %f.", expected, got)
}
if expected, got := "counter cannot decrease in value", decreaseCounter(counter).Error(); expected != got {
t.Errorf("Expected error %q, got %q.", expected, got)
}
m := &dto.Metric{}
counter.Write(m)
if expected, got := `label:<name:"a" value:"1" > label:<name:"b" value:"2" > counter:<value:43 > `, m.String(); expected != got {
t.Errorf("expected %q, got %q", expected, got)
}
}
func decreaseCounter(c *counter) (err error) {
defer func() {
if e := recover(); e != nil {
err = e.(error)
}
}()
c.Add(-1)
return nil
}

View File

@@ -0,0 +1,199 @@
package prometheus
import (
"bytes"
"errors"
"fmt"
"hash/fnv"
"regexp"
"sort"
"strings"
"github.com/prometheus/client_golang/model"
dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto"
)
var (
metricNameRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_:]*$`)
labelNameRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
)
// Labels represents a collection of label name -> value mappings. This type is
// commonly used with the With(Labels) and GetMetricWith(Labels) methods of
// metric vector Collectors, e.g.:
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
//
// The other use-case is the specification of constant label pairs in Opts or to
// create a Desc.
type Labels map[string]string
// Desc is the descriptor used by every Prometheus Metric. It is essentially
// the immutable meta-data of a Metric. The normal Metric implementations
// included in this package manage their Desc under the hood. Users only have to
// deal with Desc if they use advanced features like the ExpvarCollector or
// custom Collectors and Metrics.
//
// Descriptors registered with the same registry have to fulfill certain
// consistency and uniqueness criteria if they share the same fully-qualified
// name: They must have the same help string and the same label names (aka label
// dimensions) in each, constLabels and variableLabels, but they must differ in
// the values of the constLabels.
//
// Descriptors that share the same fully-qualified names and the same label
// values of their constLabels are considered equal.
//
// Use NewDesc to create new Desc instances.
type Desc struct {
// fqName has been built from Namespace, Subsystem, and Name.
fqName string
// help provides some helpful information about this metric.
help string
// constLabelPairs contains precalculated DTO label pairs based on
// the constant labels.
constLabelPairs []*dto.LabelPair
// VariableLabels contains names of labels for which the metric
// maintains variable values.
variableLabels []string
// id is a hash of the values of the ConstLabels and fqName. This
// must be unique among all registered descriptors and can therefore be
// used as an identifier of the descriptor.
id uint64
// dimHash is a hash of the label names (preset and variable) and the
// Help string. Each Desc with the same fqName must have the same
// dimHash.
dimHash uint64
// err is an error that occured during construction. It is reported on
// registration time.
err error
}
// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc
// and will be reported on registration time. variableLabels and constLabels can
// be nil if no such labels should be set. fqName and help must not be empty.
//
// variableLabels only contain the label names. Their label values are variable
// and therefore not part of the Desc. (They are managed within the Metric.)
//
// For constLabels, the label values are constant. Therefore, they are fully
// specified in the Desc. See the Opts documentation for the implications of
// constant labels.
func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc {
d := &Desc{
fqName: fqName,
help: help,
variableLabels: variableLabels,
}
if help == "" {
d.err = errors.New("empty help string")
return d
}
if !metricNameRE.MatchString(fqName) {
d.err = fmt.Errorf("%q is not a valid metric name", fqName)
return d
}
// labelValues contains the label values of const labels (in order of
// their sorted label names) plus the fqName (at position 0).
labelValues := make([]string, 1, len(constLabels)+1)
labelValues[0] = fqName
labelNames := make([]string, 0, len(constLabels)+len(variableLabels))
labelNameSet := map[string]struct{}{}
// First add only the const label names and sort them...
for labelName := range constLabels {
if !checkLabelName(labelName) {
d.err = fmt.Errorf("%q is not a valid label name", labelName)
return d
}
labelNames = append(labelNames, labelName)
labelNameSet[labelName] = struct{}{}
}
sort.Strings(labelNames)
// ... so that we can now add const label values in the order of their names.
for _, labelName := range labelNames {
labelValues = append(labelValues, constLabels[labelName])
}
// Now add the variable label names, but prefix them with something that
// cannot be in a regular label name. That prevents matching the label
// dimension with a different mix between preset and variable labels.
for _, labelName := range variableLabels {
if !checkLabelName(labelName) {
d.err = fmt.Errorf("%q is not a valid label name", labelName)
return d
}
labelNames = append(labelNames, "$"+labelName)
labelNameSet[labelName] = struct{}{}
}
if len(labelNames) != len(labelNameSet) {
d.err = errors.New("duplicate label names")
return d
}
h := fnv.New64a()
var b bytes.Buffer // To copy string contents into, avoiding []byte allocations.
for _, val := range labelValues {
b.Reset()
b.WriteString(val)
b.WriteByte(model.SeparatorByte)
h.Write(b.Bytes())
}
d.id = h.Sum64()
// Sort labelNames so that order doesn't matter for the hash.
sort.Strings(labelNames)
// Now hash together (in this order) the help string and the sorted
// label names.
h.Reset()
b.Reset()
b.WriteString(help)
b.WriteByte(model.SeparatorByte)
h.Write(b.Bytes())
for _, labelName := range labelNames {
b.Reset()
b.WriteString(labelName)
b.WriteByte(model.SeparatorByte)
h.Write(b.Bytes())
}
d.dimHash = h.Sum64()
d.constLabelPairs = make([]*dto.LabelPair, 0, len(constLabels))
for n, v := range constLabels {
d.constLabelPairs = append(d.constLabelPairs, &dto.LabelPair{
Name: proto.String(n),
Value: proto.String(v),
})
}
sort.Sort(LabelPairSorter(d.constLabelPairs))
return d
}
// NewInvalidDesc returns an invalid descriptor, i.e. a descriptor with the
// provided error set. If a collector returning such a descriptor is registered,
// registration will fail with the provided error. NewInvalidDesc can be used by
// a Collector to signal inability to describe itself.
func NewInvalidDesc(err error) *Desc {
return &Desc{
err: err,
}
}
func (d *Desc) String() string {
lpStrings := make([]string, 0, len(d.constLabelPairs))
for _, lp := range d.constLabelPairs {
lpStrings = append(
lpStrings,
fmt.Sprintf("%s=%q", lp.GetName(), lp.GetValue()),
)
}
return fmt.Sprintf(
"Desc{fqName: %q, help: %q, constLabels: {%s}, variableLabels: %v}",
d.fqName,
d.help,
strings.Join(lpStrings, ","),
d.variableLabels,
)
}
func checkLabelName(l string) bool {
return labelNameRE.MatchString(l) &&
!strings.HasPrefix(l, model.ReservedLabelPrefix)
}

View File

@@ -0,0 +1,108 @@
// Copyright 2014 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 provides embeddable metric primitives for servers and
// standardized exposition of telemetry through a web services interface.
//
// All exported functions and methods are safe to be used concurrently unless
// specified otherwise.
//
// To expose metrics registered with the Prometheus registry, an HTTP server
// needs to know about the Prometheus handler. The usual endpoint is "/metrics".
//
// http.Handle("/metrics", prometheus.Handler())
//
// As a starting point a very basic usage example:
//
// package main
//
// import (
// "net/http"
//
// "github.com/prometheus/client_golang/prometheus"
// )
//
// var (
// cpuTemp = prometheus.NewGauge(prometheus.GaugeOpts{
// Name: "cpu_temperature_celsius",
// Help: "Current temperature of the CPU.",
// })
// hdFailures = prometheus.NewCounter(prometheus.CounterOpts{
// Name: "hd_errors_total",
// Help: "Number of hard-disk errors.",
// })
// )
//
// func init() {
// prometheus.MustRegister(cpuTemp)
// prometheus.MustRegister(hdFailures)
// }
//
// func main() {
// cpuTemp.Set(65.3)
// hdFailures.Inc()
//
// http.Handle("/metrics", prometheus.Handler())
// http.ListenAndServe(":8080", nil)
// }
//
//
// This is a complete program that exports two metrics, a Gauge and a Counter.
// It also exports some stats about the HTTP usage of the /metrics
// endpoint. (See the Handler function for more detail.)
//
// A more advanced metric type is the Summary.
//
// In addition to the fundamental metric types Gauge, Counter, and Summary, a
// very important part of the Prometheus data model is the partitioning of
// samples along dimensions called labels, which results in metric vectors. The
// fundamental types are GaugeVec, CounterVec, and SummaryVec.
//
// Those are all the parts needed for basic usage. Detailed documentation and
// examples are provided below.
//
// Everything else this package offers is essentially for "power users" only. A
// few pointers to "power user features":
//
// All the various ...Opts structs have a ConstLabels field for labels that
// never change their value (which is only useful under special circumstances,
// see documentation of the Opts type).
//
// The Untyped metric behaves like a Gauge, but signals the Prometheus server
// not to assume anything about its type.
//
// Functions to fine-tune how the metric registry works: EnableCollectChecks,
// PanicOnCollectError, Register, Unregister, SetMetricFamilyInjectionHook.
//
// For custom metric collection, there are two entry points: Custom Metric
// implementations and custom Collector implementations. A Metric is the
// fundamental unit in the Prometheus data model: a sample at a point in time
// together with its meta-data (like its fully-qualified name and any number of
// pairs of label name and label value) that knows how to marshal itself into a
// data transfer object (aka DTO, implemented as a protocol buffer). A Collector
// gets registered with the Prometheus registry and manages the collection of
// one or more Metrics. Many parts of this package are building blocks for
// Metrics and Collectors. Desc is the metric descriptor, actually used by all
// metrics under the hood, and by Collectors to describe the Metrics to be
// collected, but only to be dealt with by users if they implement their own
// Metrics or Collectors. To create a Desc, the BuildFQName function will come
// in handy. Other useful components for Metric and Collector implementation
// include: LabelPairSorter to sort the DTO version of label pairs,
// NewConstMetric and MustNewConstMetric to create "throw away" Metrics at
// collection time, MetricVec to bundle custom Metrics into a metric vector
// Collector, SelfCollector to make a custom Metric collect itself.
//
// A good example for a custom Collector is the ExpVarCollector included in this
// package, which exports variables exported via the "expvar" package as
// Prometheus metrics.
package prometheus

View File

@@ -0,0 +1,130 @@
// Copyright 2014 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_test
import (
"sync"
"github.com/prometheus/client_golang/prometheus"
)
// ClusterManager is an example for a system that might have been built without
// Prometheus in mind. It models a central manager of jobs running in a
// cluster. To turn it into something that collects Prometheus metrics, we
// simply add the two methods required for the Collector interface.
//
// An additional challenge is that multiple instances of the ClusterManager are
// run within the same binary, each in charge of a different zone. We need to
// make use of ConstLabels to be able to register each ClusterManager instance
// with Prometheus.
type ClusterManager struct {
Zone string
OOMCount *prometheus.CounterVec
RAMUsage *prometheus.GaugeVec
mtx sync.Mutex // Protects OOMCount and RAMUsage.
// ... many more fields
}
// ReallyExpensiveAssessmentOfTheSystemState is a mock for the data gathering a
// real cluster manager would have to do. Since it may actually be really
// expensive, it must only be called once per collection. This implementation,
// obviously, only returns some made-up data.
func (c *ClusterManager) ReallyExpensiveAssessmentOfTheSystemState() (
oomCountByHost map[string]int, ramUsageByHost map[string]float64,
) {
// Just example fake data.
oomCountByHost = map[string]int{
"foo.example.org": 42,
"bar.example.org": 2001,
}
ramUsageByHost = map[string]float64{
"foo.example.org": 6.023e23,
"bar.example.org": 3.14,
}
return
}
// Describe faces the interesting challenge that the two metric vectors that are
// used in this example are already Collectors themselves. However, thanks to
// the use of channels, it is really easy to "chain" Collectors. Here we simply
// call the Describe methods of the two metric vectors.
func (c *ClusterManager) Describe(ch chan<- *prometheus.Desc) {
c.OOMCount.Describe(ch)
c.RAMUsage.Describe(ch)
}
// Collect first triggers the ReallyExpensiveAssessmentOfTheSystemState. Then it
// sets the retrieved values in the two metric vectors and then sends all their
// metrics to the channel (again using a chaining technique as in the Describe
// method). Since Collect could be called multiple times concurrently, that part
// is protected by a mutex.
func (c *ClusterManager) Collect(ch chan<- prometheus.Metric) {
oomCountByHost, ramUsageByHost := c.ReallyExpensiveAssessmentOfTheSystemState()
c.mtx.Lock()
defer c.mtx.Unlock()
for host, oomCount := range oomCountByHost {
c.OOMCount.WithLabelValues(host).Set(float64(oomCount))
}
for host, ramUsage := range ramUsageByHost {
c.RAMUsage.WithLabelValues(host).Set(ramUsage)
}
c.OOMCount.Collect(ch)
c.RAMUsage.Collect(ch)
// All metrics in OOMCount and RAMUsage are sent to the channel now. We
// can safely reset the two metric vectors now, so that we can start
// fresh in the next Collect cycle. (Imagine a host disappears from the
// cluster. If we did not reset here, its Metric would stay in the
// metric vectors forever.)
c.OOMCount.Reset()
c.RAMUsage.Reset()
}
// NewClusterManager creates the two metric vectors OOMCount and RAMUsage. Note
// that the zone is set as a ConstLabel. (It's different in each instance of the
// ClusterManager, but constant over the lifetime of an instance.) The reported
// values are partitioned by host, which is therefore a variable label.
func NewClusterManager(zone string) *ClusterManager {
return &ClusterManager{
Zone: zone,
OOMCount: prometheus.NewCounterVec(
prometheus.CounterOpts{
Subsystem: "clustermanager",
Name: "oom_count",
Help: "number of OOM crashes",
ConstLabels: prometheus.Labels{"zone": zone},
},
[]string{"host"},
),
RAMUsage: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Subsystem: "clustermanager",
Name: "ram_usage_bytes",
Help: "RAM usage as reported to the cluster manager",
ConstLabels: prometheus.Labels{"zone": zone},
},
[]string{"host"},
),
}
}
func ExampleCollector_clustermanager() {
workerDB := NewClusterManager("db")
workerCA := NewClusterManager("ca")
prometheus.MustRegister(workerDB)
prometheus.MustRegister(workerCA)
// Since we are dealing with custom Collector implementations, it might
// be a good idea to enable the collect checks in the registry.
prometheus.EnableCollectChecks(true)
}

View File

@@ -0,0 +1,87 @@
// Copyright 2014 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_test
import (
"runtime"
"github.com/prometheus/client_golang/prometheus"
)
var (
allocDesc = prometheus.NewDesc(
prometheus.BuildFQName("", "memstats", "alloc_bytes"),
"bytes allocated and still in use",
nil, nil,
)
totalAllocDesc = prometheus.NewDesc(
prometheus.BuildFQName("", "memstats", "total_alloc_bytes"),
"bytes allocated (even if freed)",
nil, nil,
)
numGCDesc = prometheus.NewDesc(
prometheus.BuildFQName("", "memstats", "num_gc_total"),
"number of GCs run",
nil, nil,
)
)
// MemStatsCollector is an example for a custom Collector that solves the
// problem of feeding into multiple metrics at the same time. The
// runtime.ReadMemStats should happen only once, and then the results need to be
// fed into a number of separate Metrics. In this example, only a few of the
// values reported by ReadMemStats are used. For each, there is a Desc provided
// as a var, so the MemStatsCollector itself needs nothing else in the
// struct. Only the methods need to be implemented.
type MemStatsCollector struct{}
// Describe just sends the three Desc objects for the Metrics we intend to
// collect.
func (_ MemStatsCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- allocDesc
ch <- totalAllocDesc
ch <- numGCDesc
}
// Collect does the trick by calling ReadMemStats once and then constructing
// three different Metrics on the fly.
func (_ MemStatsCollector) Collect(ch chan<- prometheus.Metric) {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
ch <- prometheus.MustNewConstMetric(
allocDesc,
prometheus.GaugeValue,
float64(ms.Alloc),
)
ch <- prometheus.MustNewConstMetric(
totalAllocDesc,
prometheus.GaugeValue,
float64(ms.TotalAlloc),
)
ch <- prometheus.MustNewConstMetric(
numGCDesc,
prometheus.CounterValue,
float64(ms.NumGC),
)
// To avoid new allocations on each collection, you could also keep
// metric objects around and return the same objects each time, just
// with new values set.
}
func ExampleCollector_memstats() {
prometheus.MustRegister(&MemStatsCollector{})
// Since we are dealing with custom Collector implementations, it might
// be a good idea to enable the collect checks in the registry.
prometheus.EnableCollectChecks(true)
}

View File

@@ -0,0 +1,69 @@
// Copyright 2014 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_test
import (
"runtime"
"code.google.com/p/goprotobuf/proto"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/prometheus"
)
func NewCallbackMetric(desc *prometheus.Desc, callback func() float64) *CallbackMetric {
result := &CallbackMetric{desc: desc, callback: callback}
result.Init(result) // Initialize the SelfCollector.
return result
}
// TODO: Come up with a better example.
// CallbackMetric is an example for a user-defined Metric that exports the
// result of a function call as a metric of type "untyped" without any
// labels. It uses SelfCollector to turn the Metric into a Collector so that it
// can be registered with Prometheus.
//
// Note that this example is pretty much academic as the prometheus package
// already provides an UntypedFunc type.
type CallbackMetric struct {
prometheus.SelfCollector
desc *prometheus.Desc
callback func() float64
}
func (cm *CallbackMetric) Desc() *prometheus.Desc {
return cm.desc
}
func (cm *CallbackMetric) Write(m *dto.Metric) error {
m.Untyped = &dto.Untyped{Value: proto.Float64(cm.callback())}
return nil
}
func ExampleSelfCollector() {
m := NewCallbackMetric(
prometheus.NewDesc(
"runtime_goroutines_count",
"Total number of goroutines that currently exist.",
nil, nil, // No labels, these must be nil.
),
func() float64 {
return float64(runtime.NumGoroutine())
},
)
prometheus.MustRegister(m)
}

View File

@@ -0,0 +1,454 @@
// Copyright 2014 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_test
import (
"flag"
"fmt"
"math"
"net/http"
"runtime"
"sort"
dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto"
"github.com/prometheus/client_golang/prometheus"
)
func ExampleGauge() {
opsQueued := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "our_company",
Subsystem: "blob_storage",
Name: "ops_queued",
Help: "Number of blob storage operations waiting to be processed.",
})
prometheus.MustRegister(opsQueued)
// 10 operations queued by the goroutine managing incoming requests.
opsQueued.Add(10)
// A worker goroutine has picked up a waiting operation.
opsQueued.Dec()
// And once more...
opsQueued.Dec()
}
func ExampleGaugeVec() {
binaryVersion := flag.String("binary_version", "debug", "Version of the binary: debug, canary, production.")
flag.Parse()
opsQueued := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "our_company",
Subsystem: "blob_storage",
Name: "ops_queued",
Help: "Number of blob storage operations waiting to be processed, partitioned by user and type.",
ConstLabels: prometheus.Labels{"binary_version": *binaryVersion},
},
[]string{
// Which user has requested the operation?
"user",
// Of what type is the operation?
"type",
},
)
prometheus.MustRegister(opsQueued)
// Increase a value using compact (but order-sensitive!) WithLabelValues().
opsQueued.WithLabelValues("bob", "put").Add(4)
// Increase a value with a map using WithLabels. More verbose, but order
// doesn't matter anymore.
opsQueued.With(prometheus.Labels{"type": "delete", "user": "alice"}).Inc()
}
func ExampleGaugeFunc() {
if err := prometheus.Register(prometheus.NewGaugeFunc(
prometheus.GaugeOpts{
Subsystem: "runtime",
Name: "goroutines_count",
Help: "Number of goroutines that currently exist.",
},
func() float64 { return float64(runtime.NumGoroutine()) },
)); err == nil {
fmt.Println("GaugeFunc 'goroutines_count' registered.")
}
// Note that the count of goroutines is a gauge (and not a counter) as
// it can go up and down.
// Output:
// GaugeFunc 'goroutines_count' registered.
}
func ExampleCounter() {
pushCounter := prometheus.NewCounter(prometheus.CounterOpts{
Name: "repository_pushes", // Note: No help string...
})
err := prometheus.Register(pushCounter) // ... so this will return an error.
if err != nil {
fmt.Println("Push counter couldn't be registered, no counting will happen:", err)
return
}
// Try it once more, this time with a help string.
pushCounter = prometheus.NewCounter(prometheus.CounterOpts{
Name: "repository_pushes",
Help: "Number of pushes to external repository.",
})
err = prometheus.Register(pushCounter)
if err != nil {
fmt.Println("Push counter couldn't be registered AGAIN, no counting will happen:", err)
return
}
pushComplete := make(chan struct{})
// TODO: Start a goroutine that performs repository pushes and reports
// each completion via the channel.
for _ = range pushComplete {
pushCounter.Inc()
}
// Output:
// Push counter couldn't be registered, no counting will happen: descriptor Desc{fqName: "repository_pushes", help: "", constLabels: {}, variableLabels: []} is invalid: empty help string
}
func ExampleCounterVec() {
binaryVersion := flag.String("environment", "test", "Execution environment: test, staging, production.")
flag.Parse()
httpReqs := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "How many HTTP requests processed, partitioned by status code and http method.",
ConstLabels: prometheus.Labels{"env": *binaryVersion},
},
[]string{"code", "method"},
)
prometheus.MustRegister(httpReqs)
httpReqs.WithLabelValues("404", "POST").Add(42)
// If you have to access the same set of labels very frequently, it
// might be good to retrieve the metric only once and keep a handle to
// it. But beware of deletion of that metric, see below!
m := httpReqs.WithLabelValues("200", "GET")
for i := 0; i < 1000000; i++ {
m.Inc()
}
// Delete a metric from the vector. If you have previously kept a handle
// to that metric (as above), future updates via that handle will go
// unseen (even if you re-create a metric with the same label set
// later).
httpReqs.DeleteLabelValues("200", "GET")
// Same thing with the more verbose Labels syntax.
httpReqs.Delete(prometheus.Labels{"method": "GET", "code": "200"})
}
func ExampleInstrumentHandler() {
// Handle the "/doc" endpoint with the standard http.FileServer handler.
// By wrapping the handler with InstrumentHandler, request count,
// request and response sizes, and request latency are automatically
// exported to Prometheus, partitioned by HTTP status code and method
// and by the handler name (here "fileserver").
http.Handle("/doc", prometheus.InstrumentHandler(
"fileserver", http.FileServer(http.Dir("/usr/share/doc")),
))
// The Prometheus handler still has to be registered to handle the
// "/metrics" endpoint. The handler returned by prometheus.Handler() is
// already instrumented - with "prometheus" as the handler name. In this
// example, we want the handler name to be "metrics", so we instrument
// the uninstrumented Prometheus handler ourselves.
http.Handle("/metrics", prometheus.InstrumentHandler(
"metrics", prometheus.UninstrumentedHandler(),
))
}
func ExampleLabelPairSorter() {
labelPairs := []*dto.LabelPair{
&dto.LabelPair{Name: proto.String("status"), Value: proto.String("404")},
&dto.LabelPair{Name: proto.String("method"), Value: proto.String("get")},
}
sort.Sort(prometheus.LabelPairSorter(labelPairs))
fmt.Println(labelPairs)
// Output:
// [name:"method" value:"get" name:"status" value:"404" ]
}
func ExampleRegister() {
// Imagine you have a worker pool and want to count the tasks completed.
taskCounter := prometheus.NewCounter(prometheus.CounterOpts{
Subsystem: "worker_pool",
Name: "completed_tasks_total",
Help: "Total number of tasks completed.",
})
// This will register fine.
if err := prometheus.Register(taskCounter); err != nil {
fmt.Println(err)
} else {
fmt.Println("taskCounter registered.")
}
// Don't forget to tell the HTTP server about the Prometheus handler.
// (In a real program, you still need to start the http server...)
http.Handle("/metrics", prometheus.Handler())
// Now you can start workers and give every one of them a pointer to
// taskCounter and let it increment it whenever it completes a task.
taskCounter.Inc() // This has to happen somewhere in the worker code.
// But wait, you want to see how individual workers perform. So you need
// a vector of counters, with one element for each worker.
taskCounterVec := prometheus.NewCounterVec(
prometheus.CounterOpts{
Subsystem: "worker_pool",
Name: "completed_tasks_total",
Help: "Total number of tasks completed.",
},
[]string{"worker_id"},
)
// Registering will fail because we already have a metric of that name.
if err := prometheus.Register(taskCounterVec); err != nil {
fmt.Println("taskCounterVec not registered:", err)
} else {
fmt.Println("taskCounterVec registered.")
}
// To fix, first unregister the old taskCounter.
if prometheus.Unregister(taskCounter) {
fmt.Println("taskCounter unregistered.")
}
// Try registering taskCounterVec again.
if err := prometheus.Register(taskCounterVec); err != nil {
fmt.Println("taskCounterVec not registered:", err)
} else {
fmt.Println("taskCounterVec registered.")
}
// Bummer! Still doesn't work.
// Prometheus will not allow you to ever export metrics with
// inconsistent help strings or label names. After unregistering, the
// unregistered metrics will cease to show up in the /metrics http
// response, but the registry still remembers that those metrics had
// been exported before. For this example, we will now choose a
// different name. (In a real program, you would obviously not export
// the obsolete metric in the first place.)
taskCounterVec = prometheus.NewCounterVec(
prometheus.CounterOpts{
Subsystem: "worker_pool",
Name: "completed_tasks_by_id",
Help: "Total number of tasks completed.",
},
[]string{"worker_id"},
)
if err := prometheus.Register(taskCounterVec); err != nil {
fmt.Println("taskCounterVec not registered:", err)
} else {
fmt.Println("taskCounterVec registered.")
}
// Finally it worked!
// The workers have to tell taskCounterVec their id to increment the
// right element in the metric vector.
taskCounterVec.WithLabelValues("42").Inc() // Code from worker 42.
// Each worker could also keep a reference to their own counter element
// around. Pick the counter at initialization time of the worker.
myCounter := taskCounterVec.WithLabelValues("42") // From worker 42 initialization code.
myCounter.Inc() // Somewhere in the code of that worker.
// Note that something like WithLabelValues("42", "spurious arg") would
// panic (because you have provided too many label values). If you want
// to get an error instead, use GetMetricWithLabelValues(...) instead.
notMyCounter, err := taskCounterVec.GetMetricWithLabelValues("42", "spurious arg")
if err != nil {
fmt.Println("Worker initialization failed:", err)
}
if notMyCounter == nil {
fmt.Println("notMyCounter is nil.")
}
// A different (and somewhat tricky) approach is to use
// ConstLabels. ConstLabels are pairs of label names and label values
// that never change. You might ask what those labels are good for (and
// rightfully so - if they never change, they could as well be part of
// the metric name). There are essentially two use-cases: The first is
// if labels are constant throughout the lifetime of a binary execution,
// but they vary over time or between different instances of a running
// binary. The second is what we have here: Each worker creates and
// registers an own Counter instance where the only difference is in the
// value of the ConstLabels. Those Counters can all be registered
// because the different ConstLabel values guarantee that each worker
// will increment a different Counter metric.
counterOpts := prometheus.CounterOpts{
Subsystem: "worker_pool",
Name: "completed_tasks",
Help: "Total number of tasks completed.",
ConstLabels: prometheus.Labels{"worker_id": "42"},
}
taskCounterForWorker42 := prometheus.NewCounter(counterOpts)
if err := prometheus.Register(taskCounterForWorker42); err != nil {
fmt.Println("taskCounterVForWorker42 not registered:", err)
} else {
fmt.Println("taskCounterForWorker42 registered.")
}
// Obviously, in real code, taskCounterForWorker42 would be a member
// variable of a worker struct, and the "42" would be retrieved with a
// GetId() method or something. The Counter would be created and
// registered in the initialization code of the worker.
// For the creation of the next Counter, we can recycle
// counterOpts. Just change the ConstLabels.
counterOpts.ConstLabels = prometheus.Labels{"worker_id": "2001"}
taskCounterForWorker2001 := prometheus.NewCounter(counterOpts)
if err := prometheus.Register(taskCounterForWorker2001); err != nil {
fmt.Println("taskCounterVForWorker2001 not registered:", err)
} else {
fmt.Println("taskCounterForWorker2001 registered.")
}
taskCounterForWorker2001.Inc()
taskCounterForWorker42.Inc()
taskCounterForWorker2001.Inc()
// Yet another approach would be to turn the workers themselves into
// Collectors and register them. See the Collector example for details.
// Output:
// taskCounter registered.
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: [worker_id]} has different label names or a different help string
// taskCounter unregistered.
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: [worker_id]} has different label names or a different help string
// taskCounterVec registered.
// Worker initialization failed: inconsistent label cardinality
// notMyCounter is nil.
// taskCounterForWorker42 registered.
// taskCounterForWorker2001 registered.
}
func ExampleSummary() {
temps := prometheus.NewSummary(prometheus.SummaryOpts{
Name: "pond_temperature_celsius",
Help: "The temperature of the frog pond.", // Sorry, we can't measure how badly it smells.
})
// 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 summary 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:
// summary: <
// sample_count: 1000
// sample_sum: 29969.50000000001
// quantile: <
// quantile: 0.5
// value: 31.1
// >
// quantile: <
// quantile: 0.9
// value: 41.3
// >
// quantile: <
// quantile: 0.99
// value: 41.9
// >
// >
}
func ExampleSummaryVec() {
temps := prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "pond_temperature_celsius",
Help: "The temperature of the frog pond.", // Sorry, we can't measure how badly it smells.
},
[]string{"species"},
)
// Simulate some observations.
for i := 0; i < 1000; i++ {
temps.WithLabelValues("litoria-caerulea").Observe(30 + math.Floor(120*math.Sin(float64(i)*0.1))/10)
temps.WithLabelValues("lithobates-catesbeianus").Observe(32 + math.Floor(100*math.Cos(float64(i)*0.11))/10)
}
// Just for demonstration, let's check the state of the summary vector
// by (ab)using its Collect method and the Write method of its elements
// (which is usually only used by Prometheus internally - code like the
// following will never appear in your own code).
metricChan := make(chan prometheus.Metric)
go func() {
defer close(metricChan)
temps.Collect(metricChan)
}()
metricStrings := []string{}
for metric := range metricChan {
dtoMetric := &dto.Metric{}
metric.Write(dtoMetric)
metricStrings = append(metricStrings, proto.MarshalTextString(dtoMetric))
}
sort.Strings(metricStrings) // For reproducible print order.
fmt.Println(metricStrings)
// Output:
// [label: <
// name: "species"
// value: "lithobates-catesbeianus"
// >
// summary: <
// sample_count: 1000
// sample_sum: 31956.100000000017
// quantile: <
// quantile: 0.5
// value: 32.4
// >
// quantile: <
// quantile: 0.9
// value: 41.4
// >
// quantile: <
// quantile: 0.99
// value: 41.9
// >
// >
// label: <
// name: "species"
// value: "litoria-caerulea"
// >
// summary: <
// sample_count: 1000
// sample_sum: 29969.50000000001
// quantile: <
// quantile: 0.5
// value: 31.1
// >
// quantile: <
// quantile: 0.9
// value: 41.3
// >
// quantile: <
// quantile: 0.99
// value: 41.9
// >
// >
// ]
}

View File

@@ -0,0 +1,119 @@
// Copyright 2014 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 (
"encoding/json"
"expvar"
)
// ExpvarCollector collects metrics from the expvar interface. It provides a
// quick way to expose numeric values that are already exported via expvar as
// Prometheus metrics. Note that the data models of expvar and Prometheus are
// fundamentally different, and that the ExpvarCollector is inherently
// slow. Thus, the ExpvarCollector is probably great for experiments and
// prototying, but you should seriously consider a more direct implementation of
// Prometheus metrics for monitoring production systems.
//
// Use NewExpvarCollector to create new instances.
type ExpvarCollector struct {
exports map[string]*Desc
}
// NewExpvarCollector returns a newly allocated ExpvarCollector that still has
// to be registered with the Prometheus registry.
//
// The exports map has the following meaning:
//
// The keys in the map correspond to expvar keys, i.e. for every expvar key you
// want to export as Prometheus metric, you need an entry in the exports
// map. The descriptor mapped to each key describes how to export the expvar
// value. It defines the name and the help string of the Prometheus metric
// proxying the expvar value. The type will always be Untyped.
//
// For descriptors without variable labels, the expvar value must be a number or
// a bool. The number is then directly exported as the Prometheus sample
// value. (For a bool, 'false' translates to 0 and 'true' to 1). Expvar values
// that are not numbers or bools are silently ignored.
//
// If the descriptor has one variable label, the expvar value must be an expvar
// map. The keys in the expvar map become the various values of the one
// Prometheus label. The values in the expvar map must be numbers or bools again
// as above.
//
// For descriptors with more than one variable label, the expvar must be a
// nested expvar map, i.e. where the values of the topmost map are maps again
// etc. until a depth is reached that corresponds to the number of labels. The
// leaves of that structure must be numbers or bools as above to serve as the
// sample values.
//
// Anything that does not fit into the scheme above is silently ignored.
func NewExpvarCollector(exports map[string]*Desc) *ExpvarCollector {
return &ExpvarCollector{
exports: exports,
}
}
// Describe implements Collector.
func (e *ExpvarCollector) Describe(ch chan<- *Desc) {
for _, desc := range e.exports {
ch <- desc
}
}
// Collect implements Collector.
func (e *ExpvarCollector) Collect(ch chan<- Metric) {
for name, desc := range e.exports {
var m Metric
expVar := expvar.Get(name)
if expVar == nil {
continue
}
var v interface{}
labels := make([]string, len(desc.variableLabels))
if err := json.Unmarshal([]byte(expVar.String()), &v); err != nil {
ch <- NewInvalidMetric(desc, err)
continue
}
var processValue func(v interface{}, i int)
processValue = func(v interface{}, i int) {
if i >= len(labels) {
copiedLabels := append(make([]string, 0, len(labels)), labels...)
switch v := v.(type) {
case float64:
m = MustNewConstMetric(desc, UntypedValue, v, copiedLabels...)
case bool:
if v {
m = MustNewConstMetric(desc, UntypedValue, 1, copiedLabels...)
} else {
m = MustNewConstMetric(desc, UntypedValue, 0, copiedLabels...)
}
default:
return
}
ch <- m
return
}
vm, ok := v.(map[string]interface{})
if !ok {
return
}
for lv, val := range vm {
labels[i] = lv
processValue(val, i+1)
}
}
processValue(v, 0)
}
}

View File

@@ -0,0 +1,97 @@
// Copyright 2014 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_test
import (
"expvar"
"fmt"
"sort"
"strings"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/prometheus"
)
func ExampleExpvarCollector() {
expvarCollector := prometheus.NewExpvarCollector(map[string]*prometheus.Desc{
"memstats": prometheus.NewDesc(
"expvar_memstats",
"All numeric memstats as one metric family. Not a good role-model, actually... ;-)",
[]string{"type"}, nil,
),
"lone-int": prometheus.NewDesc(
"expvar_lone_int",
"Just an expvar int as an example.",
nil, nil,
),
"http-request-map": prometheus.NewDesc(
"expvar_http_request_total",
"How many http requests processed, partitioned by status code and http method.",
[]string{"code", "method"}, nil,
),
})
prometheus.MustRegister(expvarCollector)
// The Prometheus part is done here. But to show that this example is
// doing anything, we have to manually export something via expvar. In
// real-life use-cases, some library would already have exported via
// expvar what we want to re-export as Prometheus metrics.
expvar.NewInt("lone-int").Set(42)
expvarMap := expvar.NewMap("http-request-map")
var (
expvarMap1, expvarMap2 expvar.Map
expvarInt11, expvarInt12, expvarInt21, expvarInt22 expvar.Int
)
expvarMap1.Init()
expvarMap2.Init()
expvarInt11.Set(3)
expvarInt12.Set(13)
expvarInt21.Set(11)
expvarInt22.Set(212)
expvarMap1.Set("POST", &expvarInt11)
expvarMap1.Set("GET", &expvarInt12)
expvarMap2.Set("POST", &expvarInt21)
expvarMap2.Set("GET", &expvarInt22)
expvarMap.Set("404", &expvarMap1)
expvarMap.Set("200", &expvarMap2)
// Results in the following expvar map:
// "http-request-count": {"200": {"POST": 11, "GET": 212}, "404": {"POST": 3, "GET": 13}}
// Let's see what the scrape would yield, but exclude the memstats metrics.
metricStrings := []string{}
metric := dto.Metric{}
metricChan := make(chan prometheus.Metric)
go func() {
expvarCollector.Collect(metricChan)
close(metricChan)
}()
for m := range metricChan {
if strings.Index(m.Desc().String(), "expvar_memstats") == -1 {
metric.Reset()
m.Write(&metric)
metricStrings = append(metricStrings, metric.String())
}
}
sort.Strings(metricStrings)
for _, s := range metricStrings {
fmt.Println(strings.TrimRight(s, " "))
}
// Output:
// label:<name:"code" value:"200" > label:<name:"method" value:"GET" > untyped:<value:212 >
// label:<name:"code" value:"200" > label:<name:"method" value:"POST" > untyped:<value:11 >
// label:<name:"code" value:"404" > label:<name:"method" value:"GET" > untyped:<value:13 >
// label:<name:"code" value:"404" > label:<name:"method" value:"POST" > untyped:<value:3 >
// untyped:<value:42 >
}

View File

@@ -0,0 +1,147 @@
// Copyright 2014 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 "hash/fnv"
// Gauge is a Metric that represents a single numerical value that can
// arbitrarily go up and down.
//
// A Gauge is typically used for measured values like temperatures or current
// memory usage, but also "counts" that can go up and down, like the number of
// running goroutines.
//
// To create Gauge instances, use NewGauge.
type Gauge interface {
Metric
Collector
// Set sets the Gauge to an arbitrary value.
Set(float64)
// Inc increments the Gauge by 1.
Inc()
// Dec decrements the Gauge by 1.
Dec()
// Add adds the given value to the Gauge. (The value can be
// negative, resulting in a decrease of the Gauge.)
Add(float64)
// Sub subtracts the given value from the Gauge. (The value can be
// negative, resulting in an increase of the Gauge.)
Sub(float64)
}
// GaugeOpts is an alias for Opts. See there for doc comments.
type GaugeOpts Opts
// NewGauge creates a new Gauge based on the provided GaugeOpts.
func NewGauge(opts GaugeOpts) Gauge {
return newValue(NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
), GaugeValue, 0)
}
// GaugeVec is a Collector that bundles a set of Gauges that all share the 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
// (e.g. number of operations queued, partitioned by user and operation
// type). Create instances with NewGaugeVec.
type GaugeVec struct {
MetricVec
}
// NewGaugeVec creates a new GaugeVec based on the provided GaugeOpts and
// partitioned by the given label names. At least one label name must be
// provided.
func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
desc := NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
labelNames,
opts.ConstLabels,
)
return &GaugeVec{
MetricVec: MetricVec{
children: map[uint64]Metric{},
desc: desc,
hash: fnv.New64a(),
newMetric: func(lvs ...string) Metric {
return newValue(desc, GaugeValue, 0, lvs...)
},
},
}
}
// GetMetricWithLabelValues replaces the method of the same name in
// MetricVec. The difference is that this method returns a Gauge and not a
// Metric so that no type conversion is required.
func (m *GaugeVec) GetMetricWithLabelValues(lvs ...string) (Gauge, error) {
metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...)
if metric != nil {
return metric.(Gauge), err
}
return nil, err
}
// GetMetricWith replaces the method of the same name in MetricVec. The
// difference is that this method returns a Gauge and not a Metric so that no
// type conversion is required.
func (m *GaugeVec) GetMetricWith(labels Labels) (Gauge, error) {
metric, err := m.MetricVec.GetMetricWith(labels)
if metric != nil {
return metric.(Gauge), err
}
return nil, err
}
// WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. By not returning an
// error, WithLabelValues allows shortcuts like
// myVec.WithLabelValues("404", "GET").Add(42)
func (m *GaugeVec) WithLabelValues(lvs ...string) Gauge {
return m.MetricVec.WithLabelValues(lvs...).(Gauge)
}
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
func (m *GaugeVec) With(labels Labels) Gauge {
return m.MetricVec.With(labels).(Gauge)
}
// GaugeFunc is a Gauge whose value is determined at collect time by calling a
// provided function.
//
// To create GaugeFunc instances, use NewGaugeFunc.
type GaugeFunc interface {
Metric
Collector
}
// NewGaugeFunc creates a new GaugeFunc based on the provided GaugeOpts. The
// value reported is determined by calling the given function from within the
// Write method. Take into account that metric collection may happen
// concurrently. If that results in concurrent calls to Write, like in the case
// where a GaugeFunc is directly registered with Prometheus, the provided
// function must be concurrency-safe.
func NewGaugeFunc(opts GaugeOpts, function func() float64) GaugeFunc {
return newValueFunc(NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
), GaugeValue, function)
}

View File

@@ -0,0 +1,182 @@
// Copyright 2014 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"
"sync"
"testing"
"testing/quick"
dto "github.com/prometheus/client_model/go"
)
func listenGaugeStream(vals, result chan float64, done chan struct{}) {
var sum float64
outer:
for {
select {
case <-done:
close(vals)
for v := range vals {
sum += v
}
break outer
case v := <-vals:
sum += v
}
}
result <- sum
close(result)
}
func TestGaugeConcurrency(t *testing.T) {
it := func(n uint32) bool {
mutations := int(n % 10000)
concLevel := int(n%15 + 1)
var start, end sync.WaitGroup
start.Add(1)
end.Add(concLevel)
sStream := make(chan float64, mutations*concLevel)
result := make(chan float64)
done := make(chan struct{})
go listenGaugeStream(sStream, result, done)
go func() {
end.Wait()
close(done)
}()
gge := NewGauge(GaugeOpts{
Name: "test_gauge",
Help: "no help can be found here",
})
for i := 0; i < concLevel; i++ {
vals := make([]float64, mutations)
for j := 0; j < mutations; j++ {
vals[j] = rand.Float64() - 0.5
}
go func(vals []float64) {
start.Wait()
for _, v := range vals {
sStream <- v
gge.Add(v)
}
end.Done()
}(vals)
}
start.Done()
if expected, got := <-result, math.Float64frombits(gge.(*value).valBits); math.Abs(expected-got) > 0.000001 {
t.Fatalf("expected approx. %f, got %f", expected, got)
return false
}
return true
}
if err := quick.Check(it, nil); err != nil {
t.Fatal(err)
}
}
func TestGaugeVecConcurrency(t *testing.T) {
it := func(n uint32) bool {
mutations := int(n % 10000)
concLevel := int(n%15 + 1)
vecLength := int(n%5 + 1)
var start, end sync.WaitGroup
start.Add(1)
end.Add(concLevel)
sStreams := make([]chan float64, vecLength)
results := make([]chan float64, vecLength)
done := make(chan struct{})
for i := 0; i < vecLength; i++ {
sStreams[i] = make(chan float64, mutations*concLevel)
results[i] = make(chan float64)
go listenGaugeStream(sStreams[i], results[i], done)
}
go func() {
end.Wait()
close(done)
}()
gge := NewGaugeVec(
GaugeOpts{
Name: "test_gauge",
Help: "no help can be found here",
},
[]string{"label"},
)
for i := 0; i < concLevel; i++ {
vals := make([]float64, mutations)
pick := make([]int, mutations)
for j := 0; j < mutations; j++ {
vals[j] = rand.Float64() - 0.5
pick[j] = rand.Intn(vecLength)
}
go func(vals []float64) {
start.Wait()
for i, v := range vals {
sStreams[pick[i]] <- v
gge.WithLabelValues(string('A' + pick[i])).Add(v)
}
end.Done()
}(vals)
}
start.Done()
for i := range sStreams {
if expected, got := <-results[i], math.Float64frombits(gge.WithLabelValues(string('A'+i)).(*value).valBits); math.Abs(expected-got) > 0.000001 {
t.Fatalf("expected approx. %f, got %f", expected, got)
return false
}
}
return true
}
if err := quick.Check(it, nil); err != nil {
t.Fatal(err)
}
}
func TestGaugeFunc(t *testing.T) {
gf := NewGaugeFunc(
GaugeOpts{
Name: "test_name",
Help: "test help",
ConstLabels: Labels{"a": "1", "b": "2"},
},
func() float64 { return 3.1415 },
)
if expected, got := `Desc{fqName: "test_name", help: "test help", constLabels: {a="1",b="2"}, variableLabels: []}`, gf.Desc().String(); expected != got {
t.Errorf("expected %q, got %q", expected, got)
}
m := &dto.Metric{}
gf.Write(m)
if expected, got := `label:<name:"a" value:"1" > label:<name:"b" value:"2" > gauge:<value:3.1415 > `, m.String(); expected != got {
t.Errorf("expected %q, got %q", expected, got)
}
}

View File

@@ -0,0 +1,31 @@
package prometheus
import (
"runtime"
)
type goCollector struct {
goroutines Gauge
}
// NewGoCollector returns a collector which exports metrics about the current
// go process.
func NewGoCollector() *goCollector {
return &goCollector{
goroutines: NewGauge(GaugeOpts{
Name: "process_goroutines",
Help: "Number of goroutines that currently exist.",
}),
}
}
// Describe returns all descriptions of the collector.
func (c *goCollector) Describe(ch chan<- *Desc) {
ch <- c.goroutines.Desc()
}
// Collect returns the current state of all metrics of the collector.
func (c *goCollector) Collect(ch chan<- Metric) {
c.goroutines.Set(float64(runtime.NumGoroutine()))
ch <- c.goroutines
}

View File

@@ -0,0 +1,58 @@
package prometheus
import (
"reflect"
"testing"
"time"
dto "github.com/prometheus/client_model/go"
)
func TestGoCollector(t *testing.T) {
var (
c = NewGoCollector()
ch = make(chan Metric)
waitc = make(chan struct{})
closec = make(chan struct{})
old = -1
)
defer close(closec)
go func() {
c.Collect(ch)
go func(c <-chan struct{}) {
<-c
}(closec)
<-waitc
c.Collect(ch)
}()
for {
select {
case metric := <-ch:
switch m := metric.(type) {
// Attention, this also catches Counter...
case Gauge:
pb := &dto.Metric{}
m.Write(pb)
if old == -1 {
old = int(pb.GetGauge().GetValue())
close(waitc)
continue
}
if diff := int(pb.GetGauge().GetValue()) - old; diff != 1 {
// TODO: This is flaky in highly concurrent situations.
t.Errorf("want 1 new goroutine, got %d", diff)
}
return
default:
t.Errorf("want type Gauge, got %s", reflect.TypeOf(metric))
}
case <-time.After(1 * time.Second):
t.Fatalf("expected collect timed out")
}
}
}

View File

@@ -0,0 +1,322 @@
// Copyright 2014 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 (
"net/http"
"strconv"
"strings"
"time"
)
var instLabels = []string{"method", "code"}
type nower interface {
Now() time.Time
}
type nowFunc func() time.Time
func (n nowFunc) Now() time.Time {
return n()
}
var now nower = nowFunc(func() time.Time {
return time.Now()
})
func nowSeries(t ...time.Time) nower {
return nowFunc(func() time.Time {
defer func() {
t = t[1:]
}()
return t[0]
})
}
// InstrumentHandler wraps the given HTTP handler for instrumentation. It
// registers four metric collectors (if not already done) and reports http
// metrics to the (newly or already) registered collectors: http_requests_total
// (CounterVec), http_request_duration_microseconds (Summary),
// http_request_size_bytes (Summary), http_response_size_bytes (Summary). Each
// has a constant label named "handler" with the provided handlerName as
// value. http_requests_total is a metric vector partitioned by HTTP method
// (label name "method") and HTTP status code (label name "code").
func InstrumentHandler(handlerName string, handler http.Handler) http.HandlerFunc {
return InstrumentHandlerFunc(handlerName, handler.ServeHTTP)
}
// InstrumentHandlerFunc wraps the given function for instrumentation. It
// otherwise works in the same way as InstrumentHandler.
func InstrumentHandlerFunc(handlerName string, handlerFunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
return InstrumentHandlerFuncWithOpts(
SummaryOpts{
Subsystem: "http",
ConstLabels: Labels{"handler": handlerName},
},
handlerFunc,
)
}
// InstrumentHandlerWithOpts works like InstrumentHandler but provides more
// flexibility (at the cost of a more complex call syntax). As
// InstrumentHandler, this function registers four metric collectors, but it
// uses the provided SummaryOpts to create them. However, the fields "Name" and
// "Help" in the SummaryOpts are ignored. "Name" is replaced by
// "requests_total", "request_duration_microseconds", "request_size_bytes", and
// "response_size_bytes", respectively. "Help" is replaced by an appropriate
// help string. The names of the variable labels of the http_requests_total
// CounterVec are "method" (get, post, etc.), and "code" (HTTP status code).
//
// If InstrumentHandlerWithOpts is called as follows, it mimics exactly the
// behavior of InstrumentHandler:
//
// prometheus.InstrumentHandlerWithOpts(
// prometheus.SummaryOpts{
// Subsystem: "http",
// ConstLabels: prometheus.Labels{"handler": handlerName},
// },
// handler,
// )
//
// Technical detail: "requests_total" is a CounterVec, not a SummaryVec, so it
// cannot use SummaryOpts. Instead, a CounterOpts struct is created internally,
// and all its fields are set to the equally named fields in the provided
// SummaryOpts.
func InstrumentHandlerWithOpts(opts SummaryOpts, handler http.Handler) http.HandlerFunc {
return InstrumentHandlerFuncWithOpts(opts, handler.ServeHTTP)
}
// InstrumentHandlerFuncWithOpts works like InstrumentHandlerFunc but provides
// more flexibility (at the cost of a more complex call syntax). See
// InstrumentHandlerWithOpts for details how the provided SummaryOpts are used.
func InstrumentHandlerFuncWithOpts(opts SummaryOpts, handlerFunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
reqCnt := NewCounterVec(
CounterOpts{
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Name: "requests_total",
Help: "Total number of HTTP requests made.",
ConstLabels: opts.ConstLabels,
},
instLabels,
)
opts.Name = "request_duration_microseconds"
opts.Help = "The HTTP request latencies in microseconds."
reqDur := NewSummary(opts)
opts.Name = "request_size_bytes"
opts.Help = "The HTTP request sizes in bytes."
reqSz := NewSummary(opts)
opts.Name = "response_size_bytes"
opts.Help = "The HTTP response sizes in bytes."
resSz := NewSummary(opts)
regReqCnt := MustRegisterOrGet(reqCnt).(*CounterVec)
regReqDur := MustRegisterOrGet(reqDur).(Summary)
regReqSz := MustRegisterOrGet(reqSz).(Summary)
regResSz := MustRegisterOrGet(resSz).(Summary)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
delegate := &responseWriterDelegator{ResponseWriter: w}
out := make(chan int)
urlLen := 0
if r.URL != nil {
urlLen = len(r.URL.String())
}
go computeApproximateRequestSize(r, out, urlLen)
handlerFunc(delegate, r)
elapsed := float64(time.Since(now)) / float64(time.Microsecond)
method := sanitizeMethod(r.Method)
code := sanitizeCode(delegate.status)
regReqCnt.WithLabelValues(method, code).Inc()
regReqDur.Observe(elapsed)
regResSz.Observe(float64(delegate.written))
regReqSz.Observe(float64(<-out))
})
}
func computeApproximateRequestSize(r *http.Request, out chan int, s int) {
s += len(r.Method)
s += len(r.Proto)
for name, values := range r.Header {
s += len(name)
for _, value := range values {
s += len(value)
}
}
s += len(r.Host)
// N.B. r.Form and r.MultipartForm are assumed to be included in r.URL.
if r.ContentLength != -1 {
s += int(r.ContentLength)
}
out <- s
}
type responseWriterDelegator struct {
http.ResponseWriter
handler, method string
status int
written int
wroteHeader bool
}
func (r *responseWriterDelegator) WriteHeader(code int) {
r.status = code
r.wroteHeader = true
r.ResponseWriter.WriteHeader(code)
}
func (r *responseWriterDelegator) Write(b []byte) (int, error) {
if !r.wroteHeader {
r.WriteHeader(http.StatusOK)
}
n, err := r.ResponseWriter.Write(b)
r.written += n
return n, err
}
func sanitizeMethod(m string) string {
switch m {
case "GET", "get":
return "get"
case "PUT", "put":
return "put"
case "HEAD", "head":
return "head"
case "POST", "post":
return "post"
case "DELETE", "delete":
return "delete"
case "CONNECT", "connect":
return "connect"
case "OPTIONS", "options":
return "options"
case "NOTIFY", "notify":
return "notify"
default:
return strings.ToLower(m)
}
}
func sanitizeCode(s int) string {
switch s {
case 100:
return "100"
case 101:
return "101"
case 200:
return "200"
case 201:
return "201"
case 202:
return "202"
case 203:
return "203"
case 204:
return "204"
case 205:
return "205"
case 206:
return "206"
case 300:
return "300"
case 301:
return "301"
case 302:
return "302"
case 304:
return "304"
case 305:
return "305"
case 307:
return "307"
case 400:
return "400"
case 401:
return "401"
case 402:
return "402"
case 403:
return "403"
case 404:
return "404"
case 405:
return "405"
case 406:
return "406"
case 407:
return "407"
case 408:
return "408"
case 409:
return "409"
case 410:
return "410"
case 411:
return "411"
case 412:
return "412"
case 413:
return "413"
case 414:
return "414"
case 415:
return "415"
case 416:
return "416"
case 417:
return "417"
case 418:
return "418"
case 500:
return "500"
case 501:
return "501"
case 502:
return "502"
case 503:
return "503"
case 504:
return "504"
case 505:
return "505"
case 428:
return "428"
case 429:
return "429"
case 431:
return "431"
case 511:
return "511"
default:
return strconv.Itoa(s)
}
}

View File

@@ -0,0 +1,121 @@
// Copyright 2014 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 (
"net/http"
"net/http/httptest"
"testing"
"time"
dto "github.com/prometheus/client_model/go"
)
type respBody string
func (b respBody) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
w.Write([]byte(b))
}
func TestInstrumentHandler(t *testing.T) {
defer func(n nower) {
now = n.(nower)
}(now)
instant := time.Now()
end := instant.Add(30 * time.Second)
now = nowSeries(instant, end)
respBody := respBody("Howdy there!")
hndlr := InstrumentHandler("test-handler", respBody)
opts := SummaryOpts{
Subsystem: "http",
ConstLabels: Labels{"handler": "test-handler"},
}
reqCnt := MustRegisterOrGet(NewCounterVec(
CounterOpts{
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Name: "requests_total",
Help: "Total number of HTTP requests made.",
ConstLabels: opts.ConstLabels,
},
instLabels,
)).(*CounterVec)
opts.Name = "request_duration_microseconds"
opts.Help = "The HTTP request latencies in microseconds."
reqDur := MustRegisterOrGet(NewSummary(opts)).(Summary)
opts.Name = "request_size_bytes"
opts.Help = "The HTTP request sizes in bytes."
MustRegisterOrGet(NewSummary(opts))
opts.Name = "response_size_bytes"
opts.Help = "The HTTP response sizes in bytes."
MustRegisterOrGet(NewSummary(opts))
reqCnt.Reset()
resp := httptest.NewRecorder()
req := &http.Request{
Method: "GET",
}
hndlr.ServeHTTP(resp, req)
if resp.Code != http.StatusTeapot {
t.Fatalf("expected status %d, got %d", http.StatusTeapot, resp.Code)
}
if string(resp.Body.Bytes()) != "Howdy there!" {
t.Fatalf("expected body %s, got %s", "Howdy there!", string(resp.Body.Bytes()))
}
out := &dto.Metric{}
reqDur.Write(out)
if want, got := "test-handler", out.Label[0].GetValue(); want != got {
t.Errorf("want label value %q in reqDur, got %q", want, got)
}
if want, got := uint64(1), out.Summary.GetSampleCount(); want != got {
t.Errorf("want sample count %d in reqDur, got %d", want, got)
}
out.Reset()
if want, got := 1, len(reqCnt.children); want != got {
t.Errorf("want %d children in reqCnt, got %d", want, got)
}
cnt, err := reqCnt.GetMetricWithLabelValues("get", "418")
if err != nil {
t.Fatal(err)
}
cnt.Write(out)
if want, got := "418", out.Label[0].GetValue(); want != got {
t.Errorf("want label value %q in reqCnt, got %q", want, got)
}
if want, got := "test-handler", out.Label[1].GetValue(); want != got {
t.Errorf("want label value %q in reqCnt, got %q", want, got)
}
if want, got := "get", out.Label[2].GetValue(); want != got {
t.Errorf("want label value %q in reqCnt, got %q", want, got)
}
if out.Counter == nil {
t.Fatal("expected non-nil counter in reqCnt")
}
if want, got := 1., out.Counter.GetValue(); want != got {
t.Errorf("want reqCnt of %f, got %f", want, got)
}
}

View File

@@ -0,0 +1,164 @@
// Copyright 2014 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 (
"strings"
dto "github.com/prometheus/client_model/go"
)
// A Metric models a single sample value with its meta data being exported to
// Prometheus. Implementers of Metric in this package inclued Gauge, Counter,
// Untyped, and Summary. Users can implement their own Metric types, but that
// should be rarely needed. See the example for SelfCollector, which is also an
// example for a user-implemented Metric.
type Metric interface {
// Desc returns the descriptor for the Metric. This method idempotently
// returns the same descriptor throughout the lifetime of the
// Metric. The returned descriptor is immutable by contract. A Metric
// unable to describe itself must return an invalid descriptor (created
// with NewInvalidDesc).
Desc() *Desc
// Write encodes the Metric into a "Metric" Protocol Buffer data
// transmission object.
//
// Implementers of custom Metric types must observe concurrency safety
// as reads of this metric may occur at any time, and any blocking
// occurs at the expense of total performance of rendering all
// registered metrics. Ideally Metric implementations should support
// concurrent readers.
//
// The Prometheus client library attempts to minimize memory allocations
// and will provide a pre-existing reset dto.Metric pointer. Prometheus
// may recycle the dto.Metric proto message, so Metric implementations
// should just populate the provided dto.Metric and then should not keep
// any reference to it.
//
// While populating dto.Metric, labels must be sorted lexicographically.
// (Implementers may find LabelPairSorter useful for that.)
Write(*dto.Metric) error
}
// Opts bundles the options for creating most Metric types. Each metric
// implementation XXX has its own XXXOpts type, but in most cases, it is just be
// an alias of this type (which might change when the requirement arises.)
//
// It is mandatory to set Name and Help to a non-empty string. All other fields
// are optional and can safely be left at their zero value.
type Opts struct {
// Namespace, Subsystem, and Name are components of the fully-qualified
// name of the Metric (created by joining these components with
// "_"). Only Name is mandatory, the others merely help structuring the
// name. Note that the fully-qualified name of the metric must be a
// valid Prometheus metric name.
Namespace string
Subsystem string
Name string
// Help provides information about this metric. Mandatory!
//
// Metrics with the same fully-qualified name must have the same Help
// string.
Help string
// ConstLabels are used to attach fixed labels to this metric. Metrics
// with the same fully-qualified name must have the same label names in
// their ConstLabels.
//
// Note that in most cases, labels have a value that varies during the
// lifetime of a process. Those labels are usually managed with a metric
// vector collector (like CounterVec, GaugeVec, UntypedVec). ConstLabels
// serve only special purposes. One is for the special case where the
// value of a label does not change during the lifetime of a process,
// e.g. if the revision of the running binary is put into a
// label. Another, more advanced purpose is if more than one Collector
// needs to collect Metrics with the same fully-qualified name. In that
// case, those Metrics must differ in the values of their
// ConstLabels. See the Collector examples.
//
// If the value of a label never changes (not even between binaries),
// that label most likely should not be a label at all (but part of the
// metric name).
ConstLabels Labels
}
// BuildFQName joins the given three name components by "_". Empty name
// components are ignored. If the name parameter itself is empty, an empty
// string is returned, no matter what. Metric implementations included in this
// library use this function internally to generate the fully-qualified metric
// name from the name component in their Opts. Users of the library will only
// need this function if they implement their own Metric or instantiate a Desc
// (with NewDesc) directly.
func BuildFQName(namespace, subsystem, name string) string {
if name == "" {
return ""
}
switch {
case namespace != "" && subsystem != "":
return strings.Join([]string{namespace, subsystem, name}, "_")
case namespace != "":
return strings.Join([]string{namespace, name}, "_")
case subsystem != "":
return strings.Join([]string{subsystem, name}, "_")
}
return name
}
// LabelPairSorter implements sort.Interface. It is used to sort a slice of
// dto.LabelPair pointers. This is useful for implementing the Write method of
// custom metrics.
type LabelPairSorter []*dto.LabelPair
func (s LabelPairSorter) Len() int {
return len(s)
}
func (s LabelPairSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s LabelPairSorter) Less(i, j int) bool {
return s[i].GetName() < s[j].GetName()
}
type hashSorter []uint64
func (s hashSorter) Len() int {
return len(s)
}
func (s hashSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s hashSorter) Less(i, j int) bool {
return s[i] < s[j]
}
type invalidMetric struct {
desc *Desc
err error
}
// NewInvalidMetric returns a metric whose Write method always returns the
// provided error. It is useful if a Collector finds itself unable to collect
// a metric and wishes to report an error to the registry.
func NewInvalidMetric(desc *Desc, err error) Metric {
return &invalidMetric{desc, err}
}
func (m *invalidMetric) Desc() *Desc { return m.desc }
func (m *invalidMetric) Write(*dto.Metric) error { return m.err }

View File

@@ -0,0 +1,35 @@
// Copyright 2014 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 "testing"
func TestBuildFQName(t *testing.T) {
scenarios := []struct{ namespace, subsystem, name, result string }{
{"a", "b", "c", "a_b_c"},
{"", "b", "c", "b_c"},
{"a", "", "c", "a_c"},
{"", "", "c", "c"},
{"a", "b", "", ""},
{"a", "", "", ""},
{"", "b", "", ""},
{" ", "", "", ""},
}
for i, s := range scenarios {
if want, got := s.result, BuildFQName(s.namespace, s.subsystem, s.name); want != got {
t.Errorf("%d. want %s, got %s", i, want, got)
}
}
}

View File

@@ -0,0 +1,102 @@
// 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
type processCollector struct {
pid int
collectFn func(chan<- Metric)
pidFn func() (int, error)
cpuTotal Counter
openFDs, maxFDs Gauge
vsize, rss Gauge
startTime Gauge
}
// NewProcessCollector returns a collector which exports the current state of
// process metrics including cpu, memory and file descriptor usage as well as
// the process start time for the given process id under the given namespace.
func NewProcessCollector(pid int, namespace string) *processCollector {
return NewProcessCollectorPIDFn(
func() (int, error) { return pid, nil },
namespace,
)
}
// NewProcessCollectorPIDFn returns a collector which exports the current state
// of process metrics including cpu, memory and file descriptor usage as well
// as the process start time under the given namespace. The given pidFn is
// called on each collect and is used to determine the process to export
// metrics for.
func NewProcessCollectorPIDFn(
pidFn func() (int, error),
namespace string,
) *processCollector {
c := processCollector{
pidFn: pidFn,
collectFn: func(chan<- Metric) {},
cpuTotal: NewCounter(CounterOpts{
Namespace: namespace,
Name: "process_cpu_seconds_total",
Help: "Total user and system CPU time spent in seconds.",
}),
openFDs: NewGauge(GaugeOpts{
Namespace: namespace,
Name: "process_open_fds",
Help: "Number of open file descriptors.",
}),
maxFDs: NewGauge(GaugeOpts{
Namespace: namespace,
Name: "process_max_fds",
Help: "Maximum number of open file descriptors.",
}),
vsize: NewGauge(GaugeOpts{
Namespace: namespace,
Name: "process_virtual_memory_bytes",
Help: "Virtual memory size in bytes.",
}),
rss: NewGauge(GaugeOpts{
Namespace: namespace,
Name: "process_resident_memory_bytes",
Help: "Resident memory size in bytes.",
}),
startTime: NewGauge(GaugeOpts{
Namespace: namespace,
Name: "process_start_time_seconds",
Help: "Start time of the process since unix epoch in seconds.",
}),
}
// Set up process metric collection if supported by the runtime.
if processCollectSupported() {
c.collectFn = c.processCollect
}
return &c
}
// Describe returns all descriptions of the collector.
func (c *processCollector) Describe(ch chan<- *Desc) {
ch <- c.cpuTotal.Desc()
ch <- c.openFDs.Desc()
ch <- c.maxFDs.Desc()
ch <- c.vsize.Desc()
ch <- c.rss.Desc()
ch <- c.startTime.Desc()
}
// Collect returns the current state of all metrics of the collector.
func (c *processCollector) Collect(ch chan<- Metric) {
c.collectFn(ch)
}

View File

@@ -0,0 +1,84 @@
// 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.
// +build linux plan9 solaris
package prometheus
import "github.com/prometheus/procfs"
func processCollectSupported() bool {
if _, err := procfs.NewStat(); err == nil {
return true
}
return false
}
func (c *processCollector) processCollect(ch chan<- Metric) {
pid, err := c.pidFn()
if err != nil {
c.reportCollectErrors(ch, err)
return
}
p, err := procfs.NewProc(pid)
if err != nil {
c.reportCollectErrors(ch, err)
return
}
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())
ch <- c.cpuTotal
c.vsize.Set(float64(stat.VirtualMemory()))
ch <- c.vsize
c.rss.Set(float64(stat.ResidentMemory()))
ch <- c.rss
if startTime, err := stat.StartTime(); err != nil {
ch <- NewInvalidMetric(c.startTime.Desc(), err)
} else {
c.startTime.Set(startTime)
ch <- c.startTime
}
}
if fds, err := p.FileDescriptorsLen(); err != nil {
ch <- NewInvalidMetric(c.openFDs.Desc(), err)
} else {
c.openFDs.Set(float64(fds))
ch <- c.openFDs
}
if limits, err := p.NewLimits(); err != nil {
ch <- NewInvalidMetric(c.maxFDs.Desc(), err)
} else {
c.maxFDs.Set(float64(limits.OpenFiles))
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)
}

View File

@@ -0,0 +1,24 @@
// 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.
// +build !linux,!plan9,!solaris
package prometheus
func processCollectSupported() bool {
return false
}
func (c *processCollector) processCollect(ch chan<- Metric) {
panic("unreachable")
}

View File

@@ -0,0 +1,54 @@
package prometheus
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"regexp"
"testing"
"github.com/prometheus/procfs"
)
func TestProcessCollector(t *testing.T) {
if _, err := procfs.Self(); err != nil {
t.Skipf("skipping TestProcessCollector, procfs not available: %s", err)
}
registry := newRegistry()
registry.Register(NewProcessCollector(os.Getpid(), ""))
registry.Register(NewProcessCollectorPIDFn(
func() (int, error) { return os.Getpid(), nil }, "foobar"))
s := httptest.NewServer(InstrumentHandler("prometheus", registry))
defer s.Close()
r, err := http.Get(s.URL)
if err != nil {
t.Fatal(err)
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
for _, re := range []*regexp.Regexp{
regexp.MustCompile("process_cpu_seconds_total [0-9]"),
regexp.MustCompile("process_max_fds [0-9]{2,}"),
regexp.MustCompile("process_open_fds [1-9]"),
regexp.MustCompile("process_virtual_memory_bytes [1-9]"),
regexp.MustCompile("process_resident_memory_bytes [1-9]"),
regexp.MustCompile("process_start_time_seconds [0-9.]{10,}"),
regexp.MustCompile("foobar_process_cpu_seconds_total [0-9]"),
regexp.MustCompile("foobar_process_max_fds [0-9]{2,}"),
regexp.MustCompile("foobar_process_open_fds [1-9]"),
regexp.MustCompile("foobar_process_virtual_memory_bytes [1-9]"),
regexp.MustCompile("foobar_process_resident_memory_bytes [1-9]"),
regexp.MustCompile("foobar_process_start_time_seconds [0-9.]{10,}"),
} {
if !re.Match(body) {
t.Errorf("want body to match %s\n%s", re, body)
}
}
}

View File

@@ -0,0 +1,721 @@
// Copyright 2014 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.
// Copyright (c) 2013, The Prometheus Authors
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
package prometheus
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"hash/fnv"
"io"
"net/http"
"net/url"
"os"
"sort"
"strings"
"sync"
dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto"
"github.com/prometheus/client_golang/_vendor/goautoneg"
"github.com/prometheus/client_golang/model"
"github.com/prometheus/client_golang/text"
)
var (
defRegistry = newDefaultRegistry()
errAlreadyReg = errors.New("duplicate metrics collector registration attempted")
)
// Constants relevant to the HTTP interface.
const (
// APIVersion is the version of the format of the exported data. This
// will match this library's version, which subscribes to the Semantic
// Versioning scheme.
APIVersion = "0.0.4"
// DelimitedTelemetryContentType is the content type set on telemetry
// data responses in delimited protobuf format.
DelimitedTelemetryContentType = `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`
// TextTelemetryContentType is the content type set on telemetry data
// responses in text format.
TextTelemetryContentType = `text/plain; version=` + APIVersion
// ProtoTextTelemetryContentType is the content type set on telemetry
// data responses in protobuf text format. (Only used for debugging.)
ProtoTextTelemetryContentType = `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text`
// ProtoCompactTextTelemetryContentType is the content type set on
// telemetry data responses in protobuf compact text format. (Only used
// for debugging.)
ProtoCompactTextTelemetryContentType = `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text`
// Constants for object pools.
numBufs = 4
numMetricFamilies = 1000
numMetrics = 10000
// Capacity for the channel to collect metrics and descriptors.
capMetricChan = 1000
capDescChan = 10
contentTypeHeader = "Content-Type"
contentLengthHeader = "Content-Length"
contentEncodingHeader = "Content-Encoding"
acceptEncodingHeader = "Accept-Encoding"
acceptHeader = "Accept"
)
// Handler returns the HTTP handler for the global Prometheus registry. It is
// already instrumented with InstrumentHandler (using "prometheus" as handler
// name). Usually the handler is used to handle the "/metrics" endpoint.
func Handler() http.Handler {
return InstrumentHandler("prometheus", defRegistry)
}
// UninstrumentedHandler works in the same way as Handler, but the returned HTTP
// handler is not instrumented. This is useful if no instrumentation is desired
// (for whatever reason) or if the instrumentation has to happen with a
// different handler name (or with a different instrumentation approach
// altogether). See the InstrumentHandler example.
func UninstrumentedHandler() http.Handler {
return defRegistry
}
// Register registers a new Collector to be included in metrics collection. It
// returns an error if the descriptors provided by the Collector are invalid or
// if they - in combination with descriptors of already registered Collectors -
// do not fulfill the consistency and uniqueness criteria described in the Desc
// documentation.
//
// Do not register the same Collector multiple times concurrently. (Registering
// the same Collector twice would result in an error anyway, but on top of that,
// it is not safe to do so concurrently.)
func Register(m Collector) error {
_, err := defRegistry.Register(m)
return err
}
// MustRegister works like Register but panics where Register would have
// returned an error.
func MustRegister(m Collector) {
err := Register(m)
if err != nil {
panic(err)
}
}
// RegisterOrGet works like Register but does not return an error if a Collector
// is registered that equals a previously registered Collector. (Two Collectors
// are considered equal if their Describe method yields the same set of
// descriptors.) Instead, the previously registered Collector is returned (which
// is helpful if the new and previously registered Collectors are equal but not
// identical, i.e. not pointers to the same object).
//
// As for Register, it is still not safe to call RegisterOrGet with the same
// Collector multiple times concurrently.
func RegisterOrGet(m Collector) (Collector, error) {
return defRegistry.RegisterOrGet(m)
}
// MustRegisterOrGet works like Register but panics where RegisterOrGet would
// have returned an error.
func MustRegisterOrGet(m Collector) Collector {
existing, err := RegisterOrGet(m)
if err != nil {
panic(err)
}
return existing
}
// Unregister unregisters the Collector that equals the Collector passed in as
// an argument. (Two Collectors are considered equal if their Describe method
// yields the same set of descriptors.) The function returns whether a Collector
// was unregistered.
func Unregister(c Collector) bool {
return defRegistry.Unregister(c)
}
// SetMetricFamilyInjectionHook sets a function that is called whenever metrics
// are collected. The hook function must be set before metrics collection begins
// (i.e. call SetMetricFamilyInjectionHook before setting the HTTP handler.) The
// MetricFamily protobufs returned by the hook function are added to the
// delivered metrics. Each returned MetricFamily must have a unique name (also
// taking into account the MetricFamilies created in the regular way).
//
// This is a way to directly inject MetricFamily protobufs managed and owned by
// the caller. The caller has full responsibility. No sanity checks are
// performed on the returned protobufs (besides the name checks described
// above). The function must be callable at any time and concurrently.
func SetMetricFamilyInjectionHook(hook func() []*dto.MetricFamily) {
defRegistry.metricFamilyInjectionHook = hook
}
// 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
// internal server error (status code 500) is served with an error message.
func PanicOnCollectError(b bool) {
defRegistry.panicOnCollectError = b
}
// EnableCollectChecks enables (or disables) additional consistency checks
// during metrics collection. These additional checks are not enabled by default
// because they inflict a performance penalty and the errors they check for can
// only happen if the used Metric and Collector types have internal programming
// errors. It can be helpful to enable these checks while working with custom
// Collectors or Metrics whose correctness is not well established yet.
func EnableCollectChecks(b bool) {
defRegistry.collectChecksEnabled = b
}
// Push triggers a metric collection and pushes all collected metrics to the
// Pushgateway specified by addr. See the Pushgateway documentation for detailed
// implications of the job and instance parameter. instance can be left
// empty. The Pushgateway will then use the client's IP number instead. Use just
// host:port or ip:port ass addr. (Don't add 'http://' or any path.)
//
// Note that all previously pushed metrics with the same job and instance will
// be replaced with the metrics pushed by this call. (It uses HTTP method 'PUT'
// to push to the Pushgateway.)
func Push(job, instance, addr string) error {
return defRegistry.Push(job, instance, addr, "PUT")
}
// PushAdd works like Push, but only previously pushed metrics with the same
// name (and the same job and instance) will be replaced. (It uses HTTP method
// 'POST' to push to the Pushgateway.)
func PushAdd(job, instance, addr string) error {
return defRegistry.Push(job, instance, addr, "POST")
}
// encoder is a function that writes a dto.MetricFamily to an io.Writer in a
// certain encoding. It returns the number of bytes written and any error
// encountered. Note that ext.WriteDelimited and text.MetricFamilyToText are
// encoders.
type encoder func(io.Writer, *dto.MetricFamily) (int, error)
type registry struct {
mtx sync.RWMutex
collectorsByID map[uint64]Collector // ID is a hash of the descIDs.
descIDs map[uint64]struct{}
dimHashesByName map[string]uint64
bufPool chan *bytes.Buffer
metricFamilyPool chan *dto.MetricFamily
metricPool chan *dto.Metric
metricFamilyInjectionHook func() []*dto.MetricFamily
panicOnCollectError, collectChecksEnabled bool
}
func (r *registry) Register(c Collector) (Collector, error) {
descChan := make(chan *Desc, capDescChan)
go func() {
c.Describe(descChan)
close(descChan)
}()
newDescIDs := map[uint64]struct{}{}
newDimHashesByName := map[string]uint64{}
var collectorID uint64 // Just a sum of all desc IDs.
var duplicateDescErr error
r.mtx.Lock()
defer r.mtx.Unlock()
// Coduct various tests...
for desc := range descChan {
// Is the descriptor valid at all?
if desc.err != nil {
return c, fmt.Errorf("descriptor %s is invalid: %s", desc, desc.err)
}
// Is the descID unique?
// (In other words: Is the fqName + constLabel combination unique?)
if _, exists := r.descIDs[desc.id]; exists {
duplicateDescErr = fmt.Errorf("descriptor %s already exists with the same fully-qualified name and const label values", desc)
}
// If it is not a duplicate desc in this collector, add it to
// the collectorID. (We allow duplicate descs within the same
// collector, but their existence must be a no-op.)
if _, exists := newDescIDs[desc.id]; !exists {
newDescIDs[desc.id] = struct{}{}
collectorID += desc.id
}
// Are all the label names and the help string consistent with
// previous descriptors of the same name?
// First check existing descriptors...
if dimHash, exists := r.dimHashesByName[desc.fqName]; exists {
if dimHash != desc.dimHash {
return nil, fmt.Errorf("a previously registered descriptor with the same fully-qualified name as %s has different label names or a different help string", desc)
}
} else {
// ...then check the new descriptors already seen.
if dimHash, exists := newDimHashesByName[desc.fqName]; exists {
if dimHash != desc.dimHash {
return nil, fmt.Errorf("descriptors reported by collector have inconsistent label names or help strings for the same fully-qualified name, offender is %s", desc)
}
} else {
newDimHashesByName[desc.fqName] = desc.dimHash
}
}
}
// Did anything happen at all?
if len(newDescIDs) == 0 {
return nil, errors.New("collector has no descriptors")
}
if existing, exists := r.collectorsByID[collectorID]; exists {
return existing, errAlreadyReg
}
// If the collectorID is new, but at least one of the descs existed
// before, we are in trouble.
if duplicateDescErr != nil {
return nil, duplicateDescErr
}
// Only after all tests have passed, actually register.
r.collectorsByID[collectorID] = c
for hash := range newDescIDs {
r.descIDs[hash] = struct{}{}
}
for name, dimHash := range newDimHashesByName {
r.dimHashesByName[name] = dimHash
}
return c, nil
}
func (r *registry) RegisterOrGet(m Collector) (Collector, error) {
existing, err := r.Register(m)
if err != nil && err != errAlreadyReg {
return nil, err
}
return existing, nil
}
func (r *registry) Unregister(c Collector) bool {
descChan := make(chan *Desc, capDescChan)
go func() {
c.Describe(descChan)
close(descChan)
}()
descIDs := map[uint64]struct{}{}
var collectorID uint64 // Just a sum of the desc IDs.
for desc := range descChan {
if _, exists := descIDs[desc.id]; !exists {
collectorID += desc.id
descIDs[desc.id] = struct{}{}
}
}
r.mtx.RLock()
if _, exists := r.collectorsByID[collectorID]; !exists {
r.mtx.RUnlock()
return false
}
r.mtx.RUnlock()
r.mtx.Lock()
defer r.mtx.Unlock()
delete(r.collectorsByID, collectorID)
for id := range descIDs {
delete(r.descIDs, id)
}
// dimHashesByName is left untouched as those must be consistent
// throughout the lifetime of a program.
return true
}
func (r *registry) Push(job, instance, addr, method string) error {
u := fmt.Sprintf("http://%s/metrics/jobs/%s", addr, url.QueryEscape(job))
if instance != "" {
u += "/instances/" + url.QueryEscape(instance)
}
buf := r.getBuf()
defer r.giveBuf(buf)
if _, err := r.writePB(buf, text.WriteProtoDelimited); err != nil {
if r.panicOnCollectError {
panic(err)
}
return err
}
req, err := http.NewRequest(method, u, buf)
if err != nil {
return err
}
req.Header.Set(contentTypeHeader, DelimitedTelemetryContentType)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 202 {
return fmt.Errorf("unexpected status code %d while pushing to %s", resp.StatusCode, u)
}
return nil
}
func (r *registry) ServeHTTP(w http.ResponseWriter, req *http.Request) {
enc, contentType := chooseEncoder(req)
buf := r.getBuf()
defer r.giveBuf(buf)
writer, encoding := decorateWriter(req, buf)
if _, err := r.writePB(writer, enc); err != nil {
if r.panicOnCollectError {
panic(err)
}
http.Error(w, "An error has occurred:\n\n"+err.Error(), http.StatusInternalServerError)
return
}
if closer, ok := writer.(io.Closer); ok {
closer.Close()
}
header := w.Header()
header.Set(contentTypeHeader, contentType)
header.Set(contentLengthHeader, fmt.Sprint(buf.Len()))
if encoding != "" {
header.Set(contentEncodingHeader, encoding)
}
w.Write(buf.Bytes())
}
func (r *registry) writePB(w io.Writer, writeEncoded encoder) (int, error) {
var metricHashes map[uint64]struct{}
if r.collectChecksEnabled {
metricHashes = make(map[uint64]struct{})
}
metricChan := make(chan Metric, capMetricChan)
wg := sync.WaitGroup{}
r.mtx.RLock()
metricFamiliesByName := make(map[string]*dto.MetricFamily, len(r.dimHashesByName))
// Scatter.
// (Collectors could be complex and slow, so we call them all at once.)
wg.Add(len(r.collectorsByID))
go func() {
wg.Wait()
close(metricChan)
}()
for _, collector := range r.collectorsByID {
go func(collector Collector) {
defer wg.Done()
collector.Collect(metricChan)
}(collector)
}
r.mtx.RUnlock()
// Drain metricChan in case of premature return.
defer func() {
for _ = range metricChan {
}
}()
// Gather.
for metric := range metricChan {
// This could be done concurrently, too, but it required locking
// of metricFamiliesByName (and of metricHashes if checks are
// enabled). Most likely not worth it.
desc := metric.Desc()
metricFamily, ok := metricFamiliesByName[desc.fqName]
if !ok {
metricFamily = r.getMetricFamily()
defer r.giveMetricFamily(metricFamily)
metricFamily.Name = proto.String(desc.fqName)
metricFamily.Help = proto.String(desc.help)
metricFamiliesByName[desc.fqName] = metricFamily
}
dtoMetric := r.getMetric()
defer r.giveMetric(dtoMetric)
if err := metric.Write(dtoMetric); err != nil {
// TODO: Consider different means of error reporting so
// that a single erroneous metric could be skipped
// instead of blowing up the whole collection.
return 0, fmt.Errorf("error collecting metric %v: %s", desc, err)
}
switch {
case metricFamily.Type != nil:
// Type already set. We are good.
case dtoMetric.Gauge != nil:
metricFamily.Type = dto.MetricType_GAUGE.Enum()
case dtoMetric.Counter != nil:
metricFamily.Type = dto.MetricType_COUNTER.Enum()
case dtoMetric.Summary != nil:
metricFamily.Type = dto.MetricType_SUMMARY.Enum()
case dtoMetric.Untyped != nil:
metricFamily.Type = dto.MetricType_UNTYPED.Enum()
default:
return 0, fmt.Errorf("empty metric collected: %s", dtoMetric)
}
if r.collectChecksEnabled {
if err := r.checkConsistency(metricFamily, dtoMetric, desc, metricHashes); err != nil {
return 0, err
}
}
metricFamily.Metric = append(metricFamily.Metric, dtoMetric)
}
if r.metricFamilyInjectionHook != nil {
for _, mf := range r.metricFamilyInjectionHook() {
if _, exists := metricFamiliesByName[mf.GetName()]; exists {
return 0, fmt.Errorf("metric family with duplicate name injected: %s", mf)
}
metricFamiliesByName[mf.GetName()] = mf
}
}
// Now that MetricFamilies are all set, sort their Metrics
// lexicographically by their label values.
for _, mf := range metricFamiliesByName {
sort.Sort(metricSorter(mf.Metric))
}
// Write out MetricFamilies sorted by their name.
names := make([]string, 0, len(metricFamiliesByName))
for name := range metricFamiliesByName {
names = append(names, name)
}
sort.Strings(names)
var written int
for _, name := range names {
w, err := writeEncoded(w, metricFamiliesByName[name])
written += w
if err != nil {
return written, err
}
}
return written, nil
}
func (r *registry) checkConsistency(metricFamily *dto.MetricFamily, dtoMetric *dto.Metric, desc *Desc, metricHashes map[uint64]struct{}) error {
// Type consistency with metric family.
if metricFamily.GetType() == dto.MetricType_GAUGE && dtoMetric.Gauge == nil ||
metricFamily.GetType() == dto.MetricType_COUNTER && dtoMetric.Counter == nil ||
metricFamily.GetType() == dto.MetricType_SUMMARY && dtoMetric.Summary == nil ||
metricFamily.GetType() == dto.MetricType_UNTYPED && dtoMetric.Untyped == nil {
return fmt.Errorf(
"collected metric %q is not a %s",
dtoMetric, metricFamily.Type,
)
}
// Desc consistency with metric family.
if metricFamily.GetHelp() != desc.help {
return fmt.Errorf(
"collected metric %q has help %q but should have %q",
dtoMetric, desc.help, metricFamily.GetHelp(),
)
}
// Is the desc consistent with the content of the metric?
lpsFromDesc := make([]*dto.LabelPair, 0, len(dtoMetric.Label))
lpsFromDesc = append(lpsFromDesc, desc.constLabelPairs...)
for _, l := range desc.variableLabels {
lpsFromDesc = append(lpsFromDesc, &dto.LabelPair{
Name: proto.String(l),
})
}
if len(lpsFromDesc) != len(dtoMetric.Label) {
return fmt.Errorf(
"labels in collected metric %q are inconsistent with descriptor %s",
dtoMetric, desc,
)
}
sort.Sort(LabelPairSorter(lpsFromDesc))
for i, lpFromDesc := range lpsFromDesc {
lpFromMetric := dtoMetric.Label[i]
if lpFromDesc.GetName() != lpFromMetric.GetName() ||
lpFromDesc.Value != nil && lpFromDesc.GetValue() != lpFromMetric.GetValue() {
return fmt.Errorf(
"labels in collected metric %q are inconsistent with descriptor %s",
dtoMetric, desc,
)
}
}
// Is the metric unique (i.e. no other metric with the same name and the same label values)?
h := fnv.New64a()
var buf bytes.Buffer
buf.WriteString(desc.fqName)
buf.WriteByte(model.SeparatorByte)
h.Write(buf.Bytes())
for _, lp := range dtoMetric.Label {
buf.Reset()
buf.WriteString(lp.GetValue())
buf.WriteByte(model.SeparatorByte)
h.Write(buf.Bytes())
}
metricHash := h.Sum64()
if _, exists := metricHashes[metricHash]; exists {
return fmt.Errorf(
"collected metric %q was collected before with the same name and label values",
dtoMetric,
)
}
metricHashes[metricHash] = struct{}{}
r.mtx.RLock() // Remaining checks need the read lock.
defer r.mtx.RUnlock()
// Is the desc registered?
if _, exist := r.descIDs[desc.id]; !exist {
return fmt.Errorf("collected metric %q with unregistered descriptor %s", dtoMetric, desc)
}
return nil
}
func (r *registry) getBuf() *bytes.Buffer {
select {
case buf := <-r.bufPool:
return buf
default:
return &bytes.Buffer{}
}
}
func (r *registry) giveBuf(buf *bytes.Buffer) {
buf.Reset()
select {
case r.bufPool <- buf:
default:
}
}
func (r *registry) getMetricFamily() *dto.MetricFamily {
select {
case mf := <-r.metricFamilyPool:
return mf
default:
return &dto.MetricFamily{}
}
}
func (r *registry) giveMetricFamily(mf *dto.MetricFamily) {
mf.Reset()
select {
case r.metricFamilyPool <- mf:
default:
}
}
func (r *registry) getMetric() *dto.Metric {
select {
case m := <-r.metricPool:
return m
default:
return &dto.Metric{}
}
}
func (r *registry) giveMetric(m *dto.Metric) {
m.Reset()
select {
case r.metricPool <- m:
default:
}
}
func newRegistry() *registry {
return &registry{
collectorsByID: map[uint64]Collector{},
descIDs: map[uint64]struct{}{},
dimHashesByName: map[string]uint64{},
bufPool: make(chan *bytes.Buffer, numBufs),
metricFamilyPool: make(chan *dto.MetricFamily, numMetricFamilies),
metricPool: make(chan *dto.Metric, numMetrics),
}
}
func newDefaultRegistry() *registry {
r := newRegistry()
r.Register(NewProcessCollector(os.Getpid(), ""))
r.Register(NewGoCollector())
return r
}
func chooseEncoder(req *http.Request) (encoder, string) {
accepts := goautoneg.ParseAccept(req.Header.Get(acceptHeader))
for _, accept := range accepts {
switch {
case accept.Type == "application" &&
accept.SubType == "vnd.google.protobuf" &&
accept.Params["proto"] == "io.prometheus.client.MetricFamily":
switch accept.Params["encoding"] {
case "delimited":
return text.WriteProtoDelimited, DelimitedTelemetryContentType
case "text":
return text.WriteProtoText, ProtoTextTelemetryContentType
case "compact-text":
return text.WriteProtoCompactText, ProtoCompactTextTelemetryContentType
default:
continue
}
case accept.Type == "text" &&
accept.SubType == "plain" &&
(accept.Params["version"] == "0.0.4" || accept.Params["version"] == ""):
return text.MetricFamilyToText, TextTelemetryContentType
default:
continue
}
}
return text.MetricFamilyToText, TextTelemetryContentType
}
// decorateWriter wraps a writer to handle gzip compression if requested. It
// returns the decorated writer and the appropriate "Content-Encoding" header
// (which is empty if no compression is enabled).
func decorateWriter(request *http.Request, writer io.Writer) (io.Writer, string) {
header := request.Header.Get(acceptEncodingHeader)
parts := strings.Split(header, ",")
for _, part := range parts {
part := strings.TrimSpace(part)
if part == "gzip" || strings.HasPrefix(part, "gzip;") {
return gzip.NewWriter(writer), "gzip"
}
}
return writer, ""
}
type metricSorter []*dto.Metric
func (s metricSorter) Len() int {
return len(s)
}
func (s metricSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s metricSorter) Less(i, j int) bool {
for n, lp := range s[i].Label {
vi := lp.GetValue()
vj := s[j].Label[n].GetValue()
if vi != vj {
return vi < vj
}
}
return true
}

View File

@@ -0,0 +1,489 @@
// Copyright 2014 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.
// Copyright (c) 2013, The Prometheus Authors
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
package prometheus
import (
"bytes"
"encoding/binary"
"net/http"
"testing"
"code.google.com/p/goprotobuf/proto"
dto "github.com/prometheus/client_model/go"
)
type fakeResponseWriter struct {
header http.Header
body bytes.Buffer
}
func (r *fakeResponseWriter) Header() http.Header {
return r.header
}
func (r *fakeResponseWriter) Write(d []byte) (l int, err error) {
return r.body.Write(d)
}
func (r *fakeResponseWriter) WriteHeader(c int) {
}
func testHandler(t testing.TB) {
metricVec := NewCounterVec(
CounterOpts{
Name: "name",
Help: "docstring",
ConstLabels: Labels{"constname": "constvalue"},
},
[]string{"labelname"},
)
metricVec.WithLabelValues("val1").Inc()
metricVec.WithLabelValues("val2").Inc()
varintBuf := make([]byte, binary.MaxVarintLen32)
externalMetricFamily := []*dto.MetricFamily{
{
Name: proto.String("externalname"),
Help: proto.String("externaldocstring"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("externallabelname"),
Value: proto.String("externalval1"),
},
{
Name: proto.String("externalconstname"),
Value: proto.String("externalconstvalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(1),
},
},
},
},
}
marshaledExternalMetricFamily, err := proto.Marshal(externalMetricFamily[0])
if err != nil {
t.Fatal(err)
}
var externalBuf bytes.Buffer
l := binary.PutUvarint(varintBuf, uint64(len(marshaledExternalMetricFamily)))
_, err = externalBuf.Write(varintBuf[:l])
if err != nil {
t.Fatal(err)
}
_, err = externalBuf.Write(marshaledExternalMetricFamily)
if err != nil {
t.Fatal(err)
}
externalMetricFamilyAsBytes := externalBuf.Bytes()
externalMetricFamilyAsText := []byte(`# HELP externalname externaldocstring
# TYPE externalname counter
externalname{externallabelname="externalval1",externalconstname="externalconstvalue"} 1
`)
externalMetricFamilyAsProtoText := []byte(`name: "externalname"
help: "externaldocstring"
type: COUNTER
metric: <
label: <
name: "externallabelname"
value: "externalval1"
>
label: <
name: "externalconstname"
value: "externalconstvalue"
>
counter: <
value: 1
>
>
`)
externalMetricFamilyAsProtoCompactText := []byte(`name:"externalname" help:"externaldocstring" type:COUNTER metric:<label:<name:"externallabelname" value:"externalval1" > label:<name:"externalconstname" value:"externalconstvalue" > counter:<value:1 > >
`)
expectedMetricFamily := &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("docstring"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{
{
Name: proto.String("constname"),
Value: proto.String("constvalue"),
},
{
Name: proto.String("labelname"),
Value: proto.String("val1"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(1),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("constname"),
Value: proto.String("constvalue"),
},
{
Name: proto.String("labelname"),
Value: proto.String("val2"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(1),
},
},
},
}
marshaledExpectedMetricFamily, err := proto.Marshal(expectedMetricFamily)
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
l = binary.PutUvarint(varintBuf, uint64(len(marshaledExpectedMetricFamily)))
_, err = buf.Write(varintBuf[:l])
if err != nil {
t.Fatal(err)
}
_, err = buf.Write(marshaledExpectedMetricFamily)
if err != nil {
t.Fatal(err)
}
expectedMetricFamilyAsBytes := buf.Bytes()
expectedMetricFamilyAsText := []byte(`# HELP name docstring
# TYPE name counter
name{constname="constvalue",labelname="val1"} 1
name{constname="constvalue",labelname="val2"} 1
`)
expectedMetricFamilyAsProtoText := []byte(`name: "name"
help: "docstring"
type: COUNTER
metric: <
label: <
name: "constname"
value: "constvalue"
>
label: <
name: "labelname"
value: "val1"
>
counter: <
value: 1
>
>
metric: <
label: <
name: "constname"
value: "constvalue"
>
label: <
name: "labelname"
value: "val2"
>
counter: <
value: 1
>
>
`)
expectedMetricFamilyAsProtoCompactText := []byte(`name:"name" help:"docstring" type:COUNTER metric:<label:<name:"constname" value:"constvalue" > label:<name:"labelname" value:"val1" > counter:<value:1 > > metric:<label:<name:"constname" value:"constvalue" > label:<name:"labelname" value:"val2" > counter:<value:1 > >
`)
type output struct {
headers map[string]string
body []byte
}
var scenarios = []struct {
headers map[string]string
out output
withCounter bool
withExternalMF bool
}{
{ // 0
headers: map[string]string{
"Accept": "foo/bar;q=0.2, dings/bums;q=0.8",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4`,
},
body: []byte{},
},
},
{ // 1
headers: map[string]string{
"Accept": "foo/bar;q=0.2, application/quark;q=0.8",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4`,
},
body: []byte{},
},
},
{ // 2
headers: map[string]string{
"Accept": "foo/bar;q=0.2, application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=bla;q=0.8",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4`,
},
body: []byte{},
},
},
{ // 3
headers: map[string]string{
"Accept": "text/plain;q=0.2, application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.8",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: []byte{},
},
},
{ // 4
headers: map[string]string{
"Accept": "application/json",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4`,
},
body: expectedMetricFamilyAsText,
},
withCounter: true,
},
{ // 5
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: expectedMetricFamilyAsBytes,
},
withCounter: true,
},
{ // 6
headers: map[string]string{
"Accept": "application/json",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4`,
},
body: externalMetricFamilyAsText,
},
withExternalMF: true,
},
{ // 7
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: externalMetricFamilyAsBytes,
},
withExternalMF: true,
},
{ // 8
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsBytes,
expectedMetricFamilyAsBytes,
},
[]byte{},
),
},
withCounter: true,
withExternalMF: true,
},
{ // 9
headers: map[string]string{
"Accept": "text/plain",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4`,
},
body: []byte{},
},
},
{ // 10
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=bla;q=0.2, text/plain;q=0.5",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4`,
},
body: expectedMetricFamilyAsText,
},
withCounter: true,
},
{ // 11
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=bla;q=0.2, text/plain;q=0.5;version=0.0.4",
},
out: output{
headers: map[string]string{
"Content-Type": `text/plain; version=0.0.4`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsText,
expectedMetricFamilyAsText,
},
[]byte{},
),
},
withCounter: true,
withExternalMF: true,
},
{ // 12
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.2, text/plain;q=0.5;version=0.0.2",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsBytes,
expectedMetricFamilyAsBytes,
},
[]byte{},
),
},
withCounter: true,
withExternalMF: true,
},
{ // 13
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=text;q=0.5, application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.4",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsProtoText,
expectedMetricFamilyAsProtoText,
},
[]byte{},
),
},
withCounter: true,
withExternalMF: true,
},
{ // 14
headers: map[string]string{
"Accept": "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=compact-text",
},
out: output{
headers: map[string]string{
"Content-Type": `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text`,
},
body: bytes.Join(
[][]byte{
externalMetricFamilyAsProtoCompactText,
expectedMetricFamilyAsProtoCompactText,
},
[]byte{},
),
},
withCounter: true,
withExternalMF: true,
},
}
for i, scenario := range scenarios {
registry := newRegistry()
registry.collectChecksEnabled = true
if scenario.withCounter {
registry.Register(metricVec)
}
if scenario.withExternalMF {
registry.metricFamilyInjectionHook = func() []*dto.MetricFamily {
return externalMetricFamily
}
}
writer := &fakeResponseWriter{
header: http.Header{},
}
handler := InstrumentHandler("prometheus", registry)
request, _ := http.NewRequest("GET", "/", nil)
for key, value := range scenario.headers {
request.Header.Add(key, value)
}
handler(writer, request)
for key, value := range scenario.out.headers {
if writer.Header().Get(key) != value {
t.Errorf(
"%d. expected %q for header %q, got %q",
i, value, key, writer.Header().Get(key),
)
}
}
if !bytes.Equal(scenario.out.body, writer.body.Bytes()) {
t.Errorf(
"%d. expected %q for body, got %q",
i, scenario.out.body, writer.body.Bytes(),
)
}
}
}
func TestHandler(t *testing.T) {
testHandler(t)
}
func BenchmarkHandler(b *testing.B) {
for i := 0; i < b.N; i++ {
testHandler(b)
}
}

View File

@@ -0,0 +1,424 @@
// Copyright 2014 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 (
"fmt"
"hash/fnv"
"sort"
"sync"
"time"
"code.google.com/p/goprotobuf/proto"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/_vendor/perks/quantile"
)
// A Summary captures individual observations from an event or sample stream and
// summarizes them in a manner similar to traditional summary statistics: 1. sum
// of observations, 2. observation count, 3. rank estimations.
//
// A typical use-case is the observation of request latencies. By default, a
// Summary provides the median, the 90th and the 99th percentile of the latency
// as rank estimations.
//
// To create Summary instances, use NewSummary.
type Summary interface {
Metric
Collector
// Observe adds a single observation to the summary.
Observe(float64)
}
// DefObjectives are the default Summary quantile values.
var (
DefObjectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}
)
// Default values for SummaryOpts.
const (
// DefMaxAge is the default duration for which observations stay
// relevant.
DefMaxAge time.Duration = 10 * time.Minute
// DefAgeBuckets is the default number of buckets used to calculate the
// age of observations.
DefAgeBuckets = 5
// DefBufCap is the standard buffer size for collecting Summary observations.
DefBufCap = 500
)
// SummaryOpts bundles the options for creating a Summary metric. It is
// mandatory to set Name and Help to a non-empty string. All other fields are
// optional and can safely be left at their zero value.
type SummaryOpts struct {
// Namespace, Subsystem, and Name are components of the fully-qualified
// name of the Summary (created by joining these components with
// "_"). Only Name is mandatory, the others merely help structuring the
// name. Note that the fully-qualified name of the Summary must be a
// valid Prometheus metric name.
Namespace string
Subsystem string
Name string
// Help provides information about this Summary. Mandatory!
//
// Metrics with the same fully-qualified name must have the same Help
// string.
Help string
// ConstLabels are used to attach fixed labels to this
// Summary. Summaries with the same fully-qualified name must have the
// same label names in their ConstLabels.
//
// Note that in most cases, labels have a value that varies during the
// lifetime of a process. Those labels are usually managed with a
// SummaryVec. ConstLabels serve only special purposes. One is for the
// special case where the value of a label does not change during the
// lifetime of a process, e.g. if the revision of the running binary is
// put into a label. Another, more advanced purpose is if more than one
// Collector needs to collect Summaries with the same fully-qualified
// name. In that case, those Summaries must differ in the values of
// their ConstLabels. See the Collector examples.
//
// If the value of a label never changes (not even between binaries),
// that label most likely should not be a label at all (but part of the
// metric name).
ConstLabels Labels
// Objectives defines the quantile rank estimates with their respective
// absolute error. The default value is DefObjectives.
Objectives map[float64]float64
// MaxAge defines the duration for which an observation stays relevant
// for the summary. Must be positive. The default value is DefMaxAge.
MaxAge time.Duration
// AgeBuckets is the number of buckets used to exclude observations that
// are older than MaxAge from the summary. A higher number has a
// resource penalty, so only increase it if the higher resolution is
// really required. The default value is DefAgeBuckets.
AgeBuckets uint32
// BufCap defines the default sample stream buffer size. The default
// value of DefBufCap should suffice for most uses. If there is a need
// to increase the value, a multiple of 500 is recommended (because that
// is the internal buffer size of the underlying package
// "github.com/bmizerany/perks/quantile").
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
// method of perk/quantile is actually not working as advertised - and it might
// be unfixable, as the underlying algorithm is apparently not capable of
// merging summaries in the first place. To avoid using Merge, we are currently
// adding observations to _each_ age bucket, i.e. the effort to add a sample is
// essentially multiplied by the number of age buckets. When rotating age
// buckets, we empty the previous head stream. On scrape time, we simply take
// the quantiles from the head stream (no merging required). Result: More effort
// on observation time, less effort on scrape time, which is exactly the
// opposite of what we try to accomplish, but at least the results are correct.
//
// The quite elegant previous contraption to merge the age buckets efficiently
// on scrape time (see code up commit 6b9530d72ea715f0ba612c0120e6e09fbf1d49d0)
// can't be used anymore.
// NewSummary creates a new Summary based on the provided SummaryOpts.
func NewSummary(opts SummaryOpts) Summary {
return newSummary(
NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
),
opts,
)
}
func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary {
if len(desc.variableLabels) != len(labelValues) {
panic(errInconsistentCardinality)
}
if len(opts.Objectives) == 0 {
opts.Objectives = DefObjectives
}
if opts.MaxAge < 0 {
panic(fmt.Errorf("illegal max age MaxAge=%v", opts.MaxAge))
}
if opts.MaxAge == 0 {
opts.MaxAge = DefMaxAge
}
if opts.AgeBuckets == 0 {
opts.AgeBuckets = DefAgeBuckets
}
if opts.BufCap == 0 {
opts.BufCap = DefBufCap
}
s := &summary{
desc: desc,
objectives: opts.Objectives,
sortedObjectives: make([]float64, 0, len(opts.Objectives)),
labelPairs: makeLabelPairs(desc, labelValues),
hotBuf: make([]float64, 0, opts.BufCap),
coldBuf: make([]float64, 0, opts.BufCap),
streamDuration: opts.MaxAge / time.Duration(opts.AgeBuckets),
}
s.headStreamExpTime = time.Now().Add(s.streamDuration)
s.hotBufExpTime = s.headStreamExpTime
for i := uint32(0); i < opts.AgeBuckets; i++ {
s.streams = append(s.streams, s.newStream())
}
s.headStream = s.streams[0]
for qu := range s.objectives {
s.sortedObjectives = append(s.sortedObjectives, qu)
}
sort.Float64s(s.sortedObjectives)
s.Init(s) // Init self-collection.
return s
}
type summary struct {
SelfCollector
bufMtx sync.Mutex // Protects hotBuf and hotBufExpTime.
mtx sync.Mutex // Protects every other moving part.
// Lock bufMtx before mtx if both are needed.
desc *Desc
objectives map[float64]float64
sortedObjectives []float64
labelPairs []*dto.LabelPair
sum float64
cnt uint64
hotBuf, coldBuf []float64
streams []*quantile.Stream
streamDuration time.Duration
headStream *quantile.Stream
headStreamIdx int
headStreamExpTime, hotBufExpTime time.Time
}
func (s *summary) Desc() *Desc {
return s.desc
}
func (s *summary) Observe(v float64) {
s.bufMtx.Lock()
defer s.bufMtx.Unlock()
now := time.Now()
if now.After(s.hotBufExpTime) {
s.asyncFlush(now)
}
s.hotBuf = append(s.hotBuf, v)
if len(s.hotBuf) == cap(s.hotBuf) {
s.asyncFlush(now)
}
}
func (s *summary) Write(out *dto.Metric) error {
sum := &dto.Summary{}
qs := make([]*dto.Quantile, 0, len(s.objectives))
s.bufMtx.Lock()
s.mtx.Lock()
if len(s.hotBuf) != 0 {
s.swapBufs(time.Now())
}
s.bufMtx.Unlock()
s.flushColdBuf()
sum.SampleCount = proto.Uint64(s.cnt)
sum.SampleSum = proto.Float64(s.sum)
for _, rank := range s.sortedObjectives {
qs = append(qs, &dto.Quantile{
Quantile: proto.Float64(rank),
Value: proto.Float64(s.headStream.Query(rank)),
})
}
s.mtx.Unlock()
if len(qs) > 0 {
sort.Sort(quantSort(qs))
}
sum.Quantile = qs
out.Summary = sum
out.Label = s.labelPairs
return nil
}
func (s *summary) newStream() *quantile.Stream {
return quantile.NewTargeted(s.objectives)
}
// asyncFlush needs bufMtx locked.
func (s *summary) asyncFlush(now time.Time) {
s.mtx.Lock()
s.swapBufs(now)
// Unblock the original goroutine that was responsible for the mutation
// that triggered the compaction. But hold onto the global non-buffer
// state mutex until the operation finishes.
go func() {
s.flushColdBuf()
s.mtx.Unlock()
}()
}
// rotateStreams needs mtx AND bufMtx locked.
func (s *summary) maybeRotateStreams() {
for !s.hotBufExpTime.Equal(s.headStreamExpTime) {
s.headStream.Reset()
s.headStreamIdx++
if s.headStreamIdx >= len(s.streams) {
s.headStreamIdx = 0
}
s.headStream = s.streams[s.headStreamIdx]
s.headStreamExpTime = s.headStreamExpTime.Add(s.streamDuration)
}
}
// flushColdBuf needs mtx locked.
func (s *summary) flushColdBuf() {
for _, v := range s.coldBuf {
for _, stream := range s.streams {
stream.Insert(v)
}
s.cnt++
s.sum += v
}
s.coldBuf = s.coldBuf[0:0]
s.maybeRotateStreams()
}
// swapBufs needs mtx AND bufMtx locked, coldBuf must be empty.
func (s *summary) swapBufs(now time.Time) {
if len(s.coldBuf) != 0 {
panic("coldBuf is not empty")
}
s.hotBuf, s.coldBuf = s.coldBuf, s.hotBuf
// hotBuf is now empty and gets new expiration set.
for now.After(s.hotBufExpTime) {
s.hotBufExpTime = s.hotBufExpTime.Add(s.streamDuration)
}
}
type quantSort []*dto.Quantile
func (s quantSort) Len() int {
return len(s)
}
func (s quantSort) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s quantSort) Less(i, j int) bool {
return s[i].GetQuantile() < s[j].GetQuantile()
}
// 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
// if you want to count the same thing partitioned by various dimensions
// (e.g. http request latencies, partitioned by status code and method). Create
// instances with NewSummaryVec.
type SummaryVec struct {
MetricVec
}
// NewSummaryVec creates a new SummaryVec based on the provided SummaryOpts and
// partitioned by the given label names. At least one label name must be
// provided.
func NewSummaryVec(opts SummaryOpts, labelNames []string) *SummaryVec {
desc := NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
labelNames,
opts.ConstLabels,
)
return &SummaryVec{
MetricVec: MetricVec{
children: map[uint64]Metric{},
desc: desc,
hash: fnv.New64a(),
newMetric: func(lvs ...string) Metric {
return newSummary(desc, opts, lvs...)
},
},
}
}
// GetMetricWithLabelValues replaces the method of the same name in
// MetricVec. The difference is that this method returns a Summary and not a
// Metric so that no type conversion is required.
func (m *SummaryVec) GetMetricWithLabelValues(lvs ...string) (Summary, error) {
metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...)
if metric != nil {
return metric.(Summary), err
}
return nil, err
}
// GetMetricWith replaces the method of the same name in MetricVec. The
// difference is that this method returns a Summary and not a Metric so that no
// type conversion is required.
func (m *SummaryVec) GetMetricWith(labels Labels) (Summary, error) {
metric, err := m.MetricVec.GetMetricWith(labels)
if metric != nil {
return metric.(Summary), err
}
return nil, err
}
// WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. By not returning an
// error, WithLabelValues allows shortcuts like
// myVec.WithLabelValues("404", "GET").Add(42)
func (m *SummaryVec) WithLabelValues(lvs ...string) Summary {
return m.MetricVec.WithLabelValues(lvs...).(Summary)
}
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
func (m *SummaryVec) With(labels Labels) Summary {
return m.MetricVec.With(labels).(Summary)
}

View File

@@ -0,0 +1,328 @@
// Copyright 2014 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"
"sort"
"sync"
"testing"
"testing/quick"
"time"
dto "github.com/prometheus/client_model/go"
)
func benchmarkSummaryObserve(w int, b *testing.B) {
b.StopTimer()
wg := new(sync.WaitGroup)
wg.Add(w)
g := new(sync.WaitGroup)
g.Add(1)
s := NewSummary(SummaryOpts{})
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 BenchmarkSummaryObserve1(b *testing.B) {
benchmarkSummaryObserve(1, b)
}
func BenchmarkSummaryObserve2(b *testing.B) {
benchmarkSummaryObserve(2, b)
}
func BenchmarkSummaryObserve4(b *testing.B) {
benchmarkSummaryObserve(4, b)
}
func BenchmarkSummaryObserve8(b *testing.B) {
benchmarkSummaryObserve(8, b)
}
func benchmarkSummaryWrite(w int, b *testing.B) {
b.StopTimer()
wg := new(sync.WaitGroup)
wg.Add(w)
g := new(sync.WaitGroup)
g.Add(1)
s := NewSummary(SummaryOpts{})
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 BenchmarkSummaryWrite1(b *testing.B) {
benchmarkSummaryWrite(1, b)
}
func BenchmarkSummaryWrite2(b *testing.B) {
benchmarkSummaryWrite(2, b)
}
func BenchmarkSummaryWrite4(b *testing.B) {
benchmarkSummaryWrite(4, b)
}
func BenchmarkSummaryWrite8(b *testing.B) {
benchmarkSummaryWrite(8, b)
}
func TestSummaryConcurrency(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 := NewSummary(SummaryOpts{
Name: "test_summary",
Help: "helpless",
})
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.Summary.SampleCount), total; got != want {
t.Errorf("got sample count %d, want %d", got, want)
}
if got, want := *m.Summary.SampleSum, sampleSum; math.Abs((got-want)/want) > 0.001 {
t.Errorf("got sample sum %f, want %f", got, want)
}
objectives := make([]float64, 0, len(DefObjectives))
for qu := range DefObjectives {
objectives = append(objectives, qu)
}
sort.Float64s(objectives)
for i, wantQ := range objectives {
ε := DefObjectives[wantQ]
gotQ := *m.Summary.Quantile[i].Quantile
gotV := *m.Summary.Quantile[i].Value
min, max := getBounds(allVars, wantQ, ε)
if gotQ != wantQ {
t.Errorf("got quantile %f, want %f", gotQ, wantQ)
}
if gotV < min || gotV > max {
t.Errorf("got %f for quantile %f, want [%f,%f]", gotV, gotQ, min, max)
}
}
return true
}
if err := quick.Check(it, nil); err != nil {
t.Error(err)
}
}
func TestSummaryVecConcurrency(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)
sum := NewSummaryVec(
SummaryOpts{
Name: "test_summary",
Help: "helpless",
},
[]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 {
sum.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 := sum.WithLabelValues(string('A' + i))
s.Write(m)
if got, want := int(*m.Summary.SampleCount), len(allVars[i]); got != want {
t.Errorf("got sample count %d for label %c, want %d", got, 'A'+i, want)
}
if got, want := *m.Summary.SampleSum, sampleSums[i]; math.Abs((got-want)/want) > 0.001 {
t.Errorf("got sample sum %f for label %c, want %f", got, 'A'+i, want)
}
for j, wantQ := range objectives {
ε := DefObjectives[wantQ]
gotQ := *m.Summary.Quantile[j].Quantile
gotV := *m.Summary.Quantile[j].Value
min, max := getBounds(allVars[i], wantQ, ε)
if gotQ != wantQ {
t.Errorf("got quantile %f for label %c, want %f", gotQ, 'A'+i, wantQ)
}
if gotV < min || gotV > max {
t.Errorf("got %f for quantile %f for label %c, want [%f,%f]", gotV, gotQ, 'A'+i, min, max)
}
}
}
return true
}
if err := quick.Check(it, nil); err != nil {
t.Error(err)
}
}
func TestSummaryDecay(t *testing.T) {
sum := NewSummary(SummaryOpts{
Name: "test_summary",
Help: "helpless",
MaxAge: 100 * time.Millisecond,
Objectives: map[float64]float64{0.1: 0.001},
AgeBuckets: 10,
})
m := &dto.Metric{}
i := 0
tick := time.NewTicker(time.Millisecond)
for _ = range tick.C {
i++
sum.Observe(float64(i))
if i%10 == 0 {
sum.Write(m)
if got, want := *m.Summary.Quantile[0].Value, math.Max(float64(i)/10, float64(i-90)); math.Abs(got-want) > 20 {
t.Errorf("%d. got %f, want %f", i, got, want)
}
m.Reset()
}
if i >= 1000 {
break
}
}
tick.Stop()
}
func getBounds(vars []float64, q, ε float64) (min, max float64) {
// TODO: This currently tolerates an error of up to 2*ε. The error must
// be at most ε, but for some reason, it's sometimes slightly
// higher. That's a bug.
n := float64(len(vars))
lower := int((q - 2*ε) * n)
upper := int(math.Ceil((q + 2*ε) * n))
min = vars[0]
if lower > 1 {
min = vars[lower-1]
}
max = vars[len(vars)-1]
if upper < len(vars) {
max = vars[upper-1]
}
return
}

View File

@@ -0,0 +1,145 @@
// Copyright 2014 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 "hash/fnv"
// Untyped is a Metric that represents a single numerical value that can
// arbitrarily go up and down.
//
// An Untyped metric works the same as a Gauge. The only difference is that to
// no type information is implied.
//
// To create Gauge instances, use NewUntyped.
type Untyped interface {
Metric
Collector
// Set sets the Untyped metric to an arbitrary value.
Set(float64)
// Inc increments the Untyped metric by 1.
Inc()
// Dec decrements the Untyped metric by 1.
Dec()
// Add adds the given value to the Untyped metric. (The value can be
// negative, resulting in a decrease.)
Add(float64)
// Sub subtracts the given value from the Untyped metric. (The value can
// be negative, resulting in an increase.)
Sub(float64)
}
// UntypedOpts is an alias for Opts. See there for doc comments.
type UntypedOpts Opts
// NewUntyped creates a new Untyped metric from the provided UntypedOpts.
func NewUntyped(opts UntypedOpts) Untyped {
return newValue(NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
), UntypedValue, 0)
}
// UntypedVec is a Collector that bundles a set of Untyped metrics that all
// share the 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. Create instances with NewUntypedVec.
type UntypedVec struct {
MetricVec
}
// NewUntypedVec creates a new UntypedVec based on the provided UntypedOpts and
// partitioned by the given label names. At least one label name must be
// provided.
func NewUntypedVec(opts UntypedOpts, labelNames []string) *UntypedVec {
desc := NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
labelNames,
opts.ConstLabels,
)
return &UntypedVec{
MetricVec: MetricVec{
children: map[uint64]Metric{},
desc: desc,
hash: fnv.New64a(),
newMetric: func(lvs ...string) Metric {
return newValue(desc, UntypedValue, 0, lvs...)
},
},
}
}
// GetMetricWithLabelValues replaces the method of the same name in
// MetricVec. The difference is that this method returns an Untyped and not a
// Metric so that no type conversion is required.
func (m *UntypedVec) GetMetricWithLabelValues(lvs ...string) (Untyped, error) {
metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...)
if metric != nil {
return metric.(Untyped), err
}
return nil, err
}
// GetMetricWith replaces the method of the same name in MetricVec. The
// difference is that this method returns an Untyped and not a Metric so that no
// type conversion is required.
func (m *UntypedVec) GetMetricWith(labels Labels) (Untyped, error) {
metric, err := m.MetricVec.GetMetricWith(labels)
if metric != nil {
return metric.(Untyped), err
}
return nil, err
}
// WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. By not returning an
// error, WithLabelValues allows shortcuts like
// myVec.WithLabelValues("404", "GET").Add(42)
func (m *UntypedVec) WithLabelValues(lvs ...string) Untyped {
return m.MetricVec.WithLabelValues(lvs...).(Untyped)
}
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
func (m *UntypedVec) With(labels Labels) Untyped {
return m.MetricVec.With(labels).(Untyped)
}
// UntypedFunc is an Untyped whose value is determined at collect time by
// calling a provided function.
//
// To create UntypedFunc instances, use NewUntypedFunc.
type UntypedFunc interface {
Metric
Collector
}
// NewUntypedFunc creates a new UntypedFunc based on the provided
// UntypedOpts. The value reported is determined by calling the given function
// from within the Write method. Take into account that metric collection may
// happen concurrently. If that results in concurrent calls to Write, like in
// the case where an UntypedFunc is directly registered with Prometheus, the
// provided function must be concurrency-safe.
func NewUntypedFunc(opts UntypedOpts, function func() float64) UntypedFunc {
return newValueFunc(NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
nil,
opts.ConstLabels,
), UntypedValue, function)
}

View File

@@ -0,0 +1,230 @@
// Copyright 2014 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 (
"errors"
"fmt"
"math"
"sort"
"sync/atomic"
dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto"
)
// ValueType is an enumeration of metric types that represent a simple value.
type ValueType int
// Possible values for the ValueType enum.
const (
_ ValueType = iota
CounterValue
GaugeValue
UntypedValue
)
var errInconsistentCardinality = errors.New("inconsistent label cardinality")
// value is a generic metric for simple values. It implements Metric, Collector,
// Counter, Gauge, and Untyped. Its effective type is determined by
// ValueType. This is a low-level building block used by the library to back the
// implementations of Counter, Gauge, and Untyped.
type value struct {
SelfCollector
desc *Desc
valType ValueType
valBits uint64 // These are the bits of the represented float64 value.
labelPairs []*dto.LabelPair
}
// newValue returns a newly allocated value with the given Desc, ValueType,
// sample value and label values. It panics if the number of label
// values is different from the number of variable labels in Desc.
func newValue(desc *Desc, valueType ValueType, val float64, labelValues ...string) *value {
if len(labelValues) != len(desc.variableLabels) {
panic(errInconsistentCardinality)
}
result := &value{
desc: desc,
valType: valueType,
valBits: math.Float64bits(val),
labelPairs: makeLabelPairs(desc, labelValues),
}
result.Init(result)
return result
}
func (v *value) Desc() *Desc {
return v.desc
}
func (v *value) Set(val float64) {
atomic.StoreUint64(&v.valBits, math.Float64bits(val))
}
func (v *value) Inc() {
v.Add(1)
}
func (v *value) Dec() {
v.Add(-1)
}
func (v *value) Add(val float64) {
for {
oldBits := atomic.LoadUint64(&v.valBits)
newBits := math.Float64bits(math.Float64frombits(oldBits) + val)
if atomic.CompareAndSwapUint64(&v.valBits, oldBits, newBits) {
return
}
}
}
func (v *value) Sub(val float64) {
v.Add(val * -1)
}
func (v *value) Write(out *dto.Metric) error {
val := math.Float64frombits(atomic.LoadUint64(&v.valBits))
return populateMetric(v.valType, val, v.labelPairs, out)
}
// valueFunc is a generic metric for simple values retrieved on collect time
// from a function. It implements Metric and Collector. Its effective type is
// determined by ValueType. This is a low-level building block used by the
// library to back the implementations of CounterFunc, GaugeFunc, and
// UntypedFunc.
type valueFunc struct {
SelfCollector
desc *Desc
valType ValueType
function func() float64
labelPairs []*dto.LabelPair
}
// newValueFunc returns a newly allocated valueFunc with the given Desc and
// ValueType. The value reported is determined by calling the given function
// from within the Write method. Take into account that metric collection may
// happen concurrently. If that results in concurrent calls to Write, like in
// the case where a valueFunc is directly registered with Prometheus, the
// provided function must be concurrency-safe.
func newValueFunc(desc *Desc, valueType ValueType, function func() float64) *valueFunc {
result := &valueFunc{
desc: desc,
valType: valueType,
function: function,
labelPairs: makeLabelPairs(desc, nil),
}
result.Init(result)
return result
}
func (v *valueFunc) Desc() *Desc {
return v.desc
}
func (v *valueFunc) Write(out *dto.Metric) error {
return populateMetric(v.valType, v.function(), v.labelPairs, out)
}
// NewConstMetric returns a metric with one fixed value that cannot be
// changed. Users of this package will not have much use for it in regular
// operations. However, when implementing custom Collectors, it is useful as a
// throw-away metric that is generated on the fly to send it to Prometheus in
// the Collect method. NewConstMetric returns an error if the length of
// labelValues is not consistent with the variable labels in Desc.
func NewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) (Metric, error) {
if len(desc.variableLabels) != len(labelValues) {
return nil, errInconsistentCardinality
}
return &constMetric{
desc: desc,
valType: valueType,
val: value,
labelPairs: makeLabelPairs(desc, labelValues),
}, nil
}
// MustNewConstMetric is a version of NewConstMetric that panics where
// NewConstMetric would have returned an error.
func MustNewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) Metric {
m, err := NewConstMetric(desc, valueType, value, labelValues...)
if err != nil {
panic(err)
}
return m
}
type constMetric struct {
desc *Desc
valType ValueType
val float64
labelPairs []*dto.LabelPair
}
func (m *constMetric) Desc() *Desc {
return m.desc
}
func (m *constMetric) Write(out *dto.Metric) error {
return populateMetric(m.valType, m.val, m.labelPairs, out)
}
func populateMetric(
t ValueType,
v float64,
labelPairs []*dto.LabelPair,
m *dto.Metric,
) error {
m.Label = labelPairs
switch t {
case CounterValue:
m.Counter = &dto.Counter{Value: proto.Float64(v)}
case GaugeValue:
m.Gauge = &dto.Gauge{Value: proto.Float64(v)}
case UntypedValue:
m.Untyped = &dto.Untyped{Value: proto.Float64(v)}
default:
return fmt.Errorf("encountered unknown type %v", t)
}
return nil
}
func makeLabelPairs(desc *Desc, labelValues []string) []*dto.LabelPair {
totalLen := len(desc.variableLabels) + len(desc.constLabelPairs)
if totalLen == 0 {
// Super fast path.
return nil
}
if len(desc.variableLabels) == 0 {
// Moderately fast path.
return desc.constLabelPairs
}
labelPairs := make([]*dto.LabelPair, 0, totalLen)
for i, n := range desc.variableLabels {
labelPairs = append(labelPairs, &dto.LabelPair{
Name: proto.String(n),
Value: proto.String(labelValues[i]),
})
}
for _, lp := range desc.constLabelPairs {
labelPairs = append(labelPairs, lp)
}
sort.Sort(LabelPairSorter(labelPairs))
return labelPairs
}

View File

@@ -0,0 +1,241 @@
// Copyright 2014 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 (
"bytes"
"fmt"
"hash"
"sync"
)
// MetricVec is a Collector to bundle metrics of the same name that
// differ in their label values. MetricVec is usually not used directly but as a
// building block for implementations of vectors of a given metric
// type. GaugeVec, CounterVec, SummaryVec, and UntypedVec are examples already
// provided in this package.
type MetricVec struct {
mtx sync.RWMutex // Protects not only children, but also hash and buf.
children map[uint64]Metric
desc *Desc
// hash is our own hash instance to avoid repeated allocations.
hash hash.Hash64
// buf is used to copy string contents into it for hashing,
// again to avoid allocations.
buf bytes.Buffer
newMetric func(labelValues ...string) Metric
}
// Describe implements Collector. The length of the returned slice
// is always one.
func (m *MetricVec) Describe(ch chan<- *Desc) {
ch <- m.desc
}
// Collect implements Collector.
func (m *MetricVec) Collect(ch chan<- Metric) {
m.mtx.RLock()
defer m.mtx.RUnlock()
for _, metric := range m.children {
ch <- metric
}
}
// GetMetricWithLabelValues returns the Metric for the given slice of label
// values (same order as the VariableLabels in Desc). If that combination of
// label values is accessed for the first time, a new Metric is created.
// Keeping the Metric for later use is possible (and should be considered if
// performance is critical), but keep in mind that Reset, DeleteLabelValues and
// Delete can be used to delete the Metric from the MetricVec. In that case, the
// Metric will still exist, but it will not be exported anymore, even if a
// Metric with the same label values is created later. See also the CounterVec
// example.
//
// An error is returned if the number of label values is not the same as the
// number of VariableLabels in Desc.
//
// Note that for more than one label value, this method is prone to mistakes
// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as
// an alternative to avoid that type of mistake. For higher label numbers, the
// latter has a much more readable (albeit more verbose) syntax, but it comes
// with a performance overhead (for creating and processing the Labels map).
// See also the GaugeVec example.
func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
m.mtx.Lock()
defer m.mtx.Unlock()
h, err := m.hashLabelValues(lvs)
if err != nil {
return nil, err
}
return m.getOrCreateMetric(h, lvs...), nil
}
// GetMetricWith returns the Metric for the given Labels map (the label names
// must match those of the VariableLabels in Desc). If that label map is
// accessed for the first time, a new Metric is created. Implications of keeping
// the Metric are the same as for GetMetricWithLabelValues.
//
// An error is returned if the number and names of the Labels are inconsistent
// with those of the VariableLabels in Desc.
//
// This method is used for the same purpose as
// GetMetricWithLabelValues(...string). See there for pros and cons of the two
// methods.
func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) {
m.mtx.Lock()
defer m.mtx.Unlock()
h, err := m.hashLabels(labels)
if err != nil {
return nil, err
}
lvs := make([]string, len(labels))
for i, label := range m.desc.variableLabels {
lvs[i] = labels[label]
}
return m.getOrCreateMetric(h, lvs...), nil
}
// WithLabelValues works as GetMetricWithLabelValues, but panics if an error
// occurs. The method allows neat syntax like:
// httpReqs.WithLabelValues("404", "POST").Inc()
func (m *MetricVec) WithLabelValues(lvs ...string) Metric {
metric, err := m.GetMetricWithLabelValues(lvs...)
if err != nil {
panic(err)
}
return metric
}
// With works as GetMetricWith, but panics if an error occurs. The method allows
// neat syntax like:
// httpReqs.With(Labels{"status":"404", "method":"POST"}).Inc()
func (m *MetricVec) With(labels Labels) Metric {
metric, err := m.GetMetricWith(labels)
if err != nil {
panic(err)
}
return metric
}
// DeleteLabelValues removes the metric where the variable labels are the same
// as those passed in as labels (same order as the VariableLabels in Desc). It
// returns true if a metric was deleted.
//
// It is not an error if the number of label values is not the same as the
// number of VariableLabels in Desc. However, such inconsistent label count can
// never match an actual Metric, so the method will always return false in that
// case.
//
// Note that for more than one label value, this method is prone to mistakes
// caused by an incorrect order of arguments. Consider Delete(Labels) as an
// alternative to avoid that type of mistake. For higher label numbers, the
// latter has a much more readable (albeit more verbose) syntax, but it comes
// with a performance overhead (for creating and processing the Labels map).
// See also the CounterVec example.
func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
m.mtx.Lock()
defer m.mtx.Unlock()
h, err := m.hashLabelValues(lvs)
if err != nil {
return false
}
if _, has := m.children[h]; !has {
return false
}
delete(m.children, h)
return true
}
// Delete deletes the metric where the variable labels are the same as those
// passed in as labels. It returns true if a metric was deleted.
//
// It is not an error if the number and names of the Labels are inconsistent
// with those of the VariableLabels in the Desc of the MetricVec. However, such
// inconsistent Labels can never match an actual Metric, so the method will
// always return false in that case.
//
// This method is used for the same purpose as DeleteLabelValues(...string). See
// there for pros and cons of the two methods.
func (m *MetricVec) Delete(labels Labels) bool {
m.mtx.Lock()
defer m.mtx.Unlock()
h, err := m.hashLabels(labels)
if err != nil {
return false
}
if _, has := m.children[h]; !has {
return false
}
delete(m.children, h)
return true
}
// Reset deletes all metrics in this vector.
func (m *MetricVec) Reset() {
m.mtx.Lock()
defer m.mtx.Unlock()
for h := range m.children {
delete(m.children, h)
}
}
func (m *MetricVec) hashLabelValues(vals []string) (uint64, error) {
if len(vals) != len(m.desc.variableLabels) {
return 0, errInconsistentCardinality
}
m.hash.Reset()
for _, val := range vals {
m.buf.Reset()
m.buf.WriteString(val)
m.hash.Write(m.buf.Bytes())
}
return m.hash.Sum64(), nil
}
func (m *MetricVec) hashLabels(labels Labels) (uint64, error) {
if len(labels) != len(m.desc.variableLabels) {
return 0, errInconsistentCardinality
}
m.hash.Reset()
for _, label := range m.desc.variableLabels {
val, ok := labels[label]
if !ok {
return 0, fmt.Errorf("label name %q missing in label map", label)
}
m.buf.Reset()
m.buf.WriteString(val)
m.hash.Write(m.buf.Bytes())
}
return m.hash.Sum64(), nil
}
func (m *MetricVec) getOrCreateMetric(hash uint64, labelValues ...string) Metric {
metric, ok := m.children[hash]
if !ok {
// Copy labelValues. Otherwise, they would be allocated even if we don't go
// down this code path.
copiedLabelValues := append(make([]string, 0, len(labelValues)), labelValues...)
metric = m.newMetric(copiedLabelValues...)
m.children[hash] = metric
}
return metric
}

View File

@@ -0,0 +1,91 @@
// Copyright 2014 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 (
"hash/fnv"
"testing"
)
func TestDelete(t *testing.T) {
desc := NewDesc("test", "helpless", []string{"l1", "l2"}, nil)
vec := MetricVec{
children: map[uint64]Metric{},
desc: desc,
hash: fnv.New64a(),
newMetric: func(lvs ...string) Metric {
return newValue(desc, UntypedValue, 0, lvs...)
},
}
if got, want := vec.Delete(Labels{"l1": "v1", "l2": "v2"}), false; got != want {
t.Errorf("got %v, want %v", got, want)
}
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42)
if got, want := vec.Delete(Labels{"l1": "v1", "l2": "v2"}), true; got != want {
t.Errorf("got %v, want %v", got, want)
}
if got, want := vec.Delete(Labels{"l1": "v1", "l2": "v2"}), false; got != want {
t.Errorf("got %v, want %v", got, want)
}
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42)
if got, want := vec.Delete(Labels{"l2": "v2", "l1": "v1"}), true; got != want {
t.Errorf("got %v, want %v", got, want)
}
if got, want := vec.Delete(Labels{"l2": "v2", "l1": "v1"}), false; got != want {
t.Errorf("got %v, want %v", got, want)
}
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42)
if got, want := vec.Delete(Labels{"l2": "v1", "l1": "v2"}), false; got != want {
t.Errorf("got %v, want %v", got, want)
}
if got, want := vec.Delete(Labels{"l1": "v1"}), false; got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func TestDeleteLabelValues(t *testing.T) {
desc := NewDesc("test", "helpless", []string{"l1", "l2"}, nil)
vec := MetricVec{
children: map[uint64]Metric{},
desc: desc,
hash: fnv.New64a(),
newMetric: func(lvs ...string) Metric {
return newValue(desc, UntypedValue, 0, lvs...)
},
}
if got, want := vec.DeleteLabelValues("v1", "v2"), false; got != want {
t.Errorf("got %v, want %v", got, want)
}
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42)
if got, want := vec.DeleteLabelValues("v1", "v2"), true; got != want {
t.Errorf("got %v, want %v", got, want)
}
if got, want := vec.DeleteLabelValues("v1", "v2"), false; got != want {
t.Errorf("got %v, want %v", got, want)
}
vec.With(Labels{"l1": "v1", "l2": "v2"}).(Untyped).Set(42)
if got, want := vec.DeleteLabelValues("v2", "v1"), false; got != want {
t.Errorf("got %v, want %v", got, want)
}
if got, want := vec.DeleteLabelValues("v1"), false; got != want {
t.Errorf("got %v, want %v", got, want)
}
}

View File

@@ -0,0 +1,168 @@
// 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 text
import (
"bytes"
"compress/gzip"
"io"
"io/ioutil"
"testing"
dto "github.com/prometheus/client_model/go"
"github.com/matttproud/golang_protobuf_extensions/ext"
)
// Benchmarks to show how much penalty text format parsing actually inflicts.
//
// Example results on Linux 3.13.0, Intel(R) Core(TM) i7-4700MQ CPU @ 2.40GHz, go1.4.
//
// BenchmarkParseText 1000 1188535 ns/op 205085 B/op 6135 allocs/op
// BenchmarkParseTextGzip 1000 1376567 ns/op 246224 B/op 6151 allocs/op
// BenchmarkParseProto 10000 172790 ns/op 52258 B/op 1160 allocs/op
// BenchmarkParseProtoGzip 5000 324021 ns/op 94931 B/op 1211 allocs/op
// BenchmarkParseProtoMap 10000 187946 ns/op 58714 B/op 1203 allocs/op
//
// CONCLUSION: The overhead for the map is negligible. Text format needs ~5x more allocations.
// Without compression, it needs ~7x longer, but with compression (the more relevant scenario),
// the difference becomes less relevant, only ~4x.
//
// The test data contains 248 samples.
//
// BenchmarkProcessor002ParseOnly in the extraction package is not quite
// comparable to the benchmarks here, but it gives an idea: JSON parsing is even
// slower than text parsing and needs a comparable amount of allocs.
// BenchmarkParseText benchmarks the parsing of a text-format scrape into metric
// family DTOs.
func BenchmarkParseText(b *testing.B) {
b.StopTimer()
data, err := ioutil.ReadFile("testdata/text")
if err != nil {
b.Fatal(err)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
if _, err := parser.TextToMetricFamilies(bytes.NewReader(data)); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkParseTextGzip benchmarks the parsing of a gzipped text-format scrape
// into metric family DTOs.
func BenchmarkParseTextGzip(b *testing.B) {
b.StopTimer()
data, err := ioutil.ReadFile("testdata/text.gz")
if err != nil {
b.Fatal(err)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
in, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
b.Fatal(err)
}
if _, err := parser.TextToMetricFamilies(in); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkParseProto benchmarks the parsing of a protobuf-format scrape into
// metric family DTOs. Note that this does not build a map of metric families
// (as the text version does), because it is not required for Prometheus
// ingestion either. (However, it is required for the text-format parsing, as
// the metric family might be sprinkled all over the text, while the
// protobuf-format guarantees bundling at one place.)
func BenchmarkParseProto(b *testing.B) {
b.StopTimer()
data, err := ioutil.ReadFile("testdata/protobuf")
if err != nil {
b.Fatal(err)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
family := &dto.MetricFamily{}
in := bytes.NewReader(data)
for {
family.Reset()
if _, err := ext.ReadDelimited(in, family); err != nil {
if err == io.EOF {
break
}
b.Fatal(err)
}
}
}
}
// BenchmarkParseProtoGzip is like BenchmarkParseProto above, but parses gzipped
// protobuf format.
func BenchmarkParseProtoGzip(b *testing.B) {
b.StopTimer()
data, err := ioutil.ReadFile("testdata/protobuf.gz")
if err != nil {
b.Fatal(err)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
family := &dto.MetricFamily{}
in, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
b.Fatal(err)
}
for {
family.Reset()
if _, err := ext.ReadDelimited(in, family); err != nil {
if err == io.EOF {
break
}
b.Fatal(err)
}
}
}
}
// BenchmarkParseProtoMap is like BenchmarkParseProto but DOES put the parsed
// metric family DTOs into a map. This is not happening during Prometheus
// ingestion. It is just here to measure the overhead of that map creation and
// separate it from the overhead of the text format parsing.
func BenchmarkParseProtoMap(b *testing.B) {
b.StopTimer()
data, err := ioutil.ReadFile("testdata/protobuf")
if err != nil {
b.Fatal(err)
}
b.StartTimer()
for i := 0; i < b.N; i++ {
families := map[string]*dto.MetricFamily{}
in := bytes.NewReader(data)
for {
family := &dto.MetricFamily{}
if _, err := ext.ReadDelimited(in, family); err != nil {
if err == io.EOF {
break
}
b.Fatal(err)
}
families[family.GetName()] = family
}
}
}

View File

@@ -0,0 +1,265 @@
// Copyright 2014 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 text contains helper functions to parse and create text-based
// exchange formats. The package currently supports (only) version 0.0.4 of the
// exchange format. Should other versions be supported in the future, some
// versioning scheme has to be applied. Possibilities include separate packages
// or separate functions. The best way depends on the nature of future changes,
// which is the reason why no versioning scheme has been applied prematurely
// here.
package text
import (
"bytes"
"fmt"
"io"
"strings"
dto "github.com/prometheus/client_model/go"
)
// MetricFamilyToText converts a MetricFamily proto message into text format and
// writes the resulting lines to 'out'. It returns the number of bytes written
// and any error encountered. This function does not perform checks on the
// content of the metric and label names, i.e. invalid metric or label names
// will result in invalid text format output.
// This method fulfills the type 'prometheus.encoder'.
func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
var written int
// Fail-fast checks.
if len(in.Metric) == 0 {
return written, fmt.Errorf("MetricFamily has no metrics: %s", in)
}
name := in.GetName()
if name == "" {
return written, fmt.Errorf("MetricFamily has no name: %s", in)
}
if in.Type == nil {
return written, fmt.Errorf("MetricFamily has no type: %s", in)
}
// Comments, first HELP, then TYPE.
if in.Help != nil {
n, err := fmt.Fprintf(
out, "# HELP %s %s\n",
name, escapeString(*in.Help, false),
)
written += n
if err != nil {
return written, err
}
}
metricType := in.GetType()
n, err := fmt.Fprintf(
out, "# TYPE %s %s\n",
name, strings.ToLower(metricType.String()),
)
written += n
if err != nil {
return written, err
}
// Finally the samples, one line for each.
for _, metric := range in.Metric {
switch metricType {
case dto.MetricType_COUNTER:
if metric.Counter == nil {
return written, fmt.Errorf(
"expected counter in metric %s", metric,
)
}
n, err = writeSample(
name, metric, "", "",
metric.Counter.GetValue(),
out,
)
case dto.MetricType_GAUGE:
if metric.Gauge == nil {
return written, fmt.Errorf(
"expected gauge in metric %s", metric,
)
}
n, err = writeSample(
name, metric, "", "",
metric.Gauge.GetValue(),
out,
)
case dto.MetricType_UNTYPED:
if metric.Untyped == nil {
return written, fmt.Errorf(
"expected untyped in metric %s", metric,
)
}
n, err = writeSample(
name, metric, "", "",
metric.Untyped.GetValue(),
out,
)
case dto.MetricType_SUMMARY:
if metric.Summary == nil {
return written, fmt.Errorf(
"expected summary in metric %s", metric,
)
}
for _, q := range metric.Summary.Quantile {
n, err = writeSample(
name, metric,
"quantile", fmt.Sprint(q.GetQuantile()),
q.GetValue(),
out,
)
written += n
if err != nil {
return written, err
}
}
n, err = writeSample(
name+"_sum", metric, "", "",
metric.Summary.GetSampleSum(),
out,
)
if err != nil {
return written, err
}
written += n
n, err = writeSample(
name+"_count", metric, "", "",
float64(metric.Summary.GetSampleCount()),
out,
)
default:
return written, fmt.Errorf(
"unexpected type in metric %s", metric,
)
}
written += n
if err != nil {
return written, err
}
}
return written, nil
}
// writeSample writes a single sample in text format to out, given the metric
// name, the metric proto message itself, optionally an additional label name
// and value (use empty strings if not required), and the value. The function
// returns the number of bytes written and any error encountered.
func writeSample(
name string,
metric *dto.Metric,
additionalLabelName, additionalLabelValue string,
value float64,
out io.Writer,
) (int, error) {
var written int
n, err := fmt.Fprint(out, name)
written += n
if err != nil {
return written, err
}
n, err = labelPairsToText(
metric.Label,
additionalLabelName, additionalLabelValue,
out,
)
written += n
if err != nil {
return written, err
}
n, err = fmt.Fprintf(out, " %v", value)
written += n
if err != nil {
return written, err
}
if metric.TimestampMs != nil {
n, err = fmt.Fprintf(out, " %v", *metric.TimestampMs)
written += n
if err != nil {
return written, err
}
}
n, err = out.Write([]byte{'\n'})
written += n
if err != nil {
return written, err
}
return written, nil
}
// labelPairsToText converts a slice of LabelPair proto messages plus the
// explicitly given additional label pair into text formatted as required by the
// text format and writes it to 'out'. An empty slice in combination with an
// empty string 'additionalLabelName' results in nothing being
// written. Otherwise, the label pairs are written, escaped as required by the
// text format, and enclosed in '{...}'. The function returns the number of
// bytes written and any error encountered.
func labelPairsToText(
in []*dto.LabelPair,
additionalLabelName, additionalLabelValue string,
out io.Writer,
) (int, error) {
if len(in) == 0 && additionalLabelName == "" {
return 0, nil
}
var written int
separator := '{'
for _, lp := range in {
n, err := fmt.Fprintf(
out, `%c%s="%s"`,
separator, lp.GetName(), escapeString(lp.GetValue(), true),
)
written += n
if err != nil {
return written, err
}
separator = ','
}
if additionalLabelName != "" {
n, err := fmt.Fprintf(
out, `%c%s="%s"`,
separator, additionalLabelName,
escapeString(additionalLabelValue, true),
)
written += n
if err != nil {
return written, err
}
}
n, err := out.Write([]byte{'}'})
written += n
if err != nil {
return written, err
}
return written, nil
}
// escapeString replaces '\' by '\\', new line character by '\n', and - if
// includeDoubleQuote is true - '"' by '\"'.
func escapeString(v string, includeDoubleQuote bool) string {
result := bytes.NewBuffer(make([]byte, 0, len(v)))
for _, c := range v {
switch {
case c == '\\':
result.WriteString(`\\`)
case includeDoubleQuote && c == '"':
result.WriteString(`\"`)
case c == '\n':
result.WriteString(`\n`)
default:
result.WriteRune(c)
}
}
return result.String()
}

View File

@@ -0,0 +1,347 @@
// Copyright 2014 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 text
import (
"bytes"
"math"
"strings"
"testing"
"code.google.com/p/goprotobuf/proto"
dto "github.com/prometheus/client_model/go"
)
func testCreate(t testing.TB) {
var scenarios = []struct {
in *dto.MetricFamily
out string
}{
// 0: Counter, NaN as value, timestamp given.
{
in: &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("two-line\n doc str\\ing"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("labelname"),
Value: proto.String("val1"),
},
&dto.LabelPair{
Name: proto.String("basename"),
Value: proto.String("basevalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(math.NaN()),
},
},
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("labelname"),
Value: proto.String("val2"),
},
&dto.LabelPair{
Name: proto.String("basename"),
Value: proto.String("basevalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(.23),
},
TimestampMs: proto.Int64(1234567890),
},
},
},
out: `# HELP name two-line\n doc str\\ing
# TYPE name counter
name{labelname="val1",basename="basevalue"} NaN
name{labelname="val2",basename="basevalue"} 0.23 1234567890
`,
},
// 1: Gauge, some escaping required, +Inf as value, multi-byte characters in label values.
{
in: &dto.MetricFamily{
Name: proto.String("gauge_name"),
Help: proto.String("gauge\ndoc\nstr\"ing"),
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("name_1"),
Value: proto.String("val with\nnew line"),
},
&dto.LabelPair{
Name: proto.String("name_2"),
Value: proto.String("val with \\backslash and \"quotes\""),
},
},
Gauge: &dto.Gauge{
Value: proto.Float64(math.Inf(+1)),
},
},
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("name_1"),
Value: proto.String("Björn"),
},
&dto.LabelPair{
Name: proto.String("name_2"),
Value: proto.String("佖佥"),
},
},
Gauge: &dto.Gauge{
Value: proto.Float64(3.14E42),
},
},
},
},
out: `# HELP gauge_name gauge\ndoc\nstr"ing
# TYPE gauge_name gauge
gauge_name{name_1="val with\nnew line",name_2="val with \\backslash and \"quotes\""} +Inf
gauge_name{name_1="Björn",name_2="佖佥"} 3.14e+42
`,
},
// 2: Untyped, no help, one sample with no labels and -Inf as value, another sample with one label.
{
in: &dto.MetricFamily{
Name: proto.String("untyped_name"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Untyped: &dto.Untyped{
Value: proto.Float64(math.Inf(-1)),
},
},
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("name_1"),
Value: proto.String("value 1"),
},
},
Untyped: &dto.Untyped{
Value: proto.Float64(-1.23e-45),
},
},
},
},
out: `# TYPE untyped_name untyped
untyped_name -Inf
untyped_name{name_1="value 1"} -1.23e-45
`,
},
// 3: Summary.
{
in: &dto.MetricFamily{
Name: proto.String("summary_name"),
Help: proto.String("summary docstring"),
Type: dto.MetricType_SUMMARY.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Summary: &dto.Summary{
SampleCount: proto.Uint64(42),
SampleSum: proto.Float64(-3.4567),
Quantile: []*dto.Quantile{
&dto.Quantile{
Quantile: proto.Float64(0.5),
Value: proto.Float64(-1.23),
},
&dto.Quantile{
Quantile: proto.Float64(0.9),
Value: proto.Float64(.2342354),
},
&dto.Quantile{
Quantile: proto.Float64(0.99),
Value: proto.Float64(0),
},
},
},
},
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("name_1"),
Value: proto.String("value 1"),
},
&dto.LabelPair{
Name: proto.String("name_2"),
Value: proto.String("value 2"),
},
},
Summary: &dto.Summary{
SampleCount: proto.Uint64(4711),
SampleSum: proto.Float64(2010.1971),
Quantile: []*dto.Quantile{
&dto.Quantile{
Quantile: proto.Float64(0.5),
Value: proto.Float64(1),
},
&dto.Quantile{
Quantile: proto.Float64(0.9),
Value: proto.Float64(2),
},
&dto.Quantile{
Quantile: proto.Float64(0.99),
Value: proto.Float64(3),
},
},
},
},
},
},
out: `# HELP summary_name summary docstring
# TYPE summary_name summary
summary_name{quantile="0.5"} -1.23
summary_name{quantile="0.9"} 0.2342354
summary_name{quantile="0.99"} 0
summary_name_sum -3.4567
summary_name_count 42
summary_name{name_1="value 1",name_2="value 2",quantile="0.5"} 1
summary_name{name_1="value 1",name_2="value 2",quantile="0.9"} 2
summary_name{name_1="value 1",name_2="value 2",quantile="0.99"} 3
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
`,
},
}
for i, scenario := range scenarios {
out := bytes.NewBuffer(make([]byte, 0, len(scenario.out)))
n, err := MetricFamilyToText(out, scenario.in)
if err != nil {
t.Errorf("%d. error: %s", i, err)
continue
}
if expected, got := len(scenario.out), n; expected != got {
t.Errorf(
"%d. expected %d bytes written, got %d",
i, expected, got,
)
}
if expected, got := scenario.out, out.String(); expected != got {
t.Errorf(
"%d. expected out=%q, got %q",
i, expected, got,
)
}
}
}
func TestCreate(t *testing.T) {
testCreate(t)
}
func BenchmarkCreate(b *testing.B) {
for i := 0; i < b.N; i++ {
testCreate(b)
}
}
func testCreateError(t testing.TB) {
var scenarios = []struct {
in *dto.MetricFamily
err string
}{
// 0: No metric.
{
in: &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("doc string"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{},
},
err: "MetricFamily has no metrics",
},
// 1: No metric name.
{
in: &dto.MetricFamily{
Help: proto.String("doc string"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Untyped: &dto.Untyped{
Value: proto.Float64(math.Inf(-1)),
},
},
},
},
err: "MetricFamily has no name",
},
// 2: No metric type.
{
in: &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("doc string"),
Metric: []*dto.Metric{
&dto.Metric{
Untyped: &dto.Untyped{
Value: proto.Float64(math.Inf(-1)),
},
},
},
},
err: "MetricFamily has no type",
},
// 3: Wrong type.
{
in: &dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("doc string"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Untyped: &dto.Untyped{
Value: proto.Float64(math.Inf(-1)),
},
},
},
},
err: "expected counter in metric",
},
}
for i, scenario := range scenarios {
var out bytes.Buffer
_, err := MetricFamilyToText(&out, scenario.in)
if err == nil {
t.Errorf("%d. expected error, got nil", i)
continue
}
if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 {
t.Errorf(
"%d. expected error starting with %q, got %q",
i, expected, got,
)
}
}
}
func TestCreateError(t *testing.T) {
testCreateError(t)
}
func BenchmarkCreateError(b *testing.B) {
for i := 0; i < b.N; i++ {
testCreateError(b)
}
}

View File

@@ -0,0 +1,659 @@
// Copyright 2014 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 text
import (
"bufio"
"bytes"
"fmt"
"io"
"math"
"strconv"
"strings"
dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto"
"github.com/prometheus/client_golang/model"
)
// A stateFn is a function that represents a state in a state machine. By
// executing it, the state is progressed to the next state. The stateFn returns
// another stateFn, which represents the new state. The end state is represented
// by nil.
type stateFn func() stateFn
// ParseError signals errors while parsing the simple and flat text-based
// exchange format.
type ParseError struct {
Line int
Msg string
}
// Error implements the error interface.
func (e ParseError) Error() string {
return fmt.Sprintf("text format parsing error in line %d: %s", e.Line, e.Msg)
}
// Parser is used to parse the simple and flat text-based exchange format. Its
// nil value is ready to use.
type Parser struct {
metricFamiliesByName map[string]*dto.MetricFamily
buf *bufio.Reader // Where the parsed input is read through.
err error // Most recent error.
lineCount int // Tracks the line count for error messages.
currentByte byte // The most recent byte read.
currentToken bytes.Buffer // Re-used each time a token has to be gathered from multiple bytes.
currentMF *dto.MetricFamily
currentMetric *dto.Metric
currentLabelPair *dto.LabelPair
// The remaining member variables are only used for summaries.
summaries map[uint64]*dto.Metric // Key is created with LabelsToSignature.
currentLabels map[string]string // All labels including '__name__' but excluding 'quantile'.
currentQuantile float64
// These tell us if the currently processed line ends on '_count' or
// '_sum' respectively and belong to a summary, representing the sample
// count and sum of that summary.
currentIsSummaryCount, currentIsSummarySum bool
}
// TextToMetricFamilies reads 'in' as the simple and flat text-based exchange
// format and creates MetricFamily proto messages. It returns the MetricFamily
// proto messages in a map where the metric names are the keys, along with any
// error encountered.
//
// If the input contains duplicate metrics (i.e. lines with the same metric name
// and exactly the same label set), the resulting MetricFamily will contain
// duplicate Metric proto messages. Similar is true for duplicate label
// names. Checks for duplicates have to be performed separately, if required.
//
// Summaries are a rather special beast. You would probably not use them in the
// simple text format anyway. This method can deal with summaries if they are
// presented in exactly the way the text.Create function creates them.
//
// This method must not be called concurrently. If you want to parse different
// input concurrently, instantiate a separate Parser for each goroutine.
func (p *Parser) TextToMetricFamilies(in io.Reader) (map[string]*dto.MetricFamily, error) {
p.reset(in)
for nextState := p.startOfLine; nextState != nil; nextState = nextState() {
// Magic happens here...
}
// Get rid of empty metric families.
for k, mf := range p.metricFamiliesByName {
if len(mf.GetMetric()) == 0 {
delete(p.metricFamiliesByName, k)
}
}
return p.metricFamiliesByName, p.err
}
func (p *Parser) reset(in io.Reader) {
p.metricFamiliesByName = map[string]*dto.MetricFamily{}
if p.buf == nil {
p.buf = bufio.NewReader(in)
} else {
p.buf.Reset(in)
}
p.err = nil
p.lineCount = 0
if p.summaries == nil || len(p.summaries) > 0 {
p.summaries = map[uint64]*dto.Metric{}
}
p.currentQuantile = math.NaN()
}
// startOfLine represents the state where the next byte read from p.buf is the
// start of a line (or whitespace leading up to it).
func (p *Parser) startOfLine() stateFn {
p.lineCount++
if p.skipBlankTab(); p.err != nil {
// End of input reached. This is the only case where
// that is not an error but a signal that we are done.
p.err = nil
return nil
}
switch p.currentByte {
case '#':
return p.startComment
case '\n':
return p.startOfLine // Empty line, start the next one.
}
return p.readingMetricName
}
// startComment represents the state where the next byte read from p.buf is the
// start of a comment (or whitespace leading up to it).
func (p *Parser) startComment() stateFn {
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
if p.currentByte == '\n' {
return p.startOfLine
}
if p.readTokenUntilWhitespace(); p.err != nil {
return nil // Unexpected end of input.
}
// If we have hit the end of line already, there is nothing left
// to do. This is not considered a syntax error.
if p.currentByte == '\n' {
return p.startOfLine
}
keyword := p.currentToken.String()
if keyword != "HELP" && keyword != "TYPE" {
// Generic comment, ignore by fast forwarding to end of line.
for p.currentByte != '\n' {
if p.currentByte, p.err = p.buf.ReadByte(); p.err != nil {
return nil // Unexpected end of input.
}
}
return p.startOfLine
}
// There is something. Next has to be a metric name.
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
if p.readTokenAsMetricName(); p.err != nil {
return nil // Unexpected end of input.
}
if p.currentByte == '\n' {
// At the end of the line already.
// Again, this is not considered a syntax error.
return p.startOfLine
}
if !isBlankOrTab(p.currentByte) {
p.parseError("invalid metric name in comment")
return nil
}
p.setOrCreateCurrentMF()
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
if p.currentByte == '\n' {
// At the end of the line already.
// Again, this is not considered a syntax error.
return p.startOfLine
}
switch keyword {
case "HELP":
return p.readingHelp
case "TYPE":
return p.readingType
}
panic(fmt.Sprintf("code error: unexpected keyword %q", keyword))
}
// readingMetricName represents the state where the last byte read (now in
// p.currentByte) is the first byte of a metric name.
func (p *Parser) readingMetricName() stateFn {
if p.readTokenAsMetricName(); p.err != nil {
return nil
}
if p.currentToken.Len() == 0 {
p.parseError("invalid metric name")
return nil
}
p.setOrCreateCurrentMF()
// Now is the time to fix the type if it hasn't happened yet.
if p.currentMF.Type == nil {
p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
}
p.currentMetric = &dto.Metric{}
// Do not append the newly created currentMetric to
// currentMF.Metric right now. First wait if this is a summary,
// and the metric exists already, which we can only know after
// having read all the labels.
if p.skipBlankTabIfCurrentBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
return p.readingLabels
}
// readingLabels represents the state where the last byte read (now in
// p.currentByte) is either the first byte of the label set (i.e. a '{'), or the
// first byte of the value (otherwise).
func (p *Parser) readingLabels() stateFn {
// Alas, summaries are really special... We have to reset the
// currentLabels map and the currentQuantile before starting to
// read labels.
if p.currentMF.GetType() == dto.MetricType_SUMMARY {
p.currentLabels = map[string]string{}
p.currentLabels[string(model.MetricNameLabel)] = p.currentMF.GetName()
p.currentQuantile = math.NaN()
}
if p.currentByte != '{' {
return p.readingValue
}
return p.startLabelName
}
// startLabelName represents the state where the next byte read from p.buf is
// the start of a label name (or whitespace leading up to it).
func (p *Parser) startLabelName() stateFn {
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
if p.currentByte == '}' {
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
return p.readingValue
}
if p.readTokenAsLabelName(); p.err != nil {
return nil // Unexpected end of input.
}
if p.currentToken.Len() == 0 {
p.parseError(fmt.Sprintf("invalid label name for metric %q", p.currentMF.GetName()))
return nil
}
p.currentLabelPair = &dto.LabelPair{Name: proto.String(p.currentToken.String())}
if p.currentLabelPair.GetName() == string(model.MetricNameLabel) {
p.parseError(fmt.Sprintf("label name %q is reserved", model.MetricNameLabel))
return nil
}
// Once more, special summary treatment... Don't add 'quantile'
// labels to 'real' labels.
if p.currentMF.GetType() != dto.MetricType_SUMMARY ||
p.currentLabelPair.GetName() != "quantile" {
p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPair)
}
if p.skipBlankTabIfCurrentBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
if p.currentByte != '=' {
p.parseError(fmt.Sprintf("expected '=' after label name, found %q", p.currentByte))
return nil
}
return p.startLabelValue
}
// startLabelValue represents the state where the next byte read from p.buf is
// the start of a (quoted) label value (or whitespace leading up to it).
func (p *Parser) startLabelValue() stateFn {
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
if p.currentByte != '"' {
p.parseError(fmt.Sprintf("expected '\"' at start of label value, found %q", p.currentByte))
return nil
}
if p.readTokenAsLabelValue(); p.err != nil {
return nil
}
p.currentLabelPair.Value = proto.String(p.currentToken.String())
// Once more, special treatment of summaries:
// - Quantile labels are special, will result in dto.Quantile later.
// - Other labels have to be added to currentLabels for signature calculation.
if p.currentMF.GetType() == dto.MetricType_SUMMARY {
if p.currentLabelPair.GetName() == "quantile" {
if p.currentQuantile, 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 quantile label, got %q", p.currentLabelPair.GetValue()))
return nil
}
} else {
p.currentLabels[p.currentLabelPair.GetName()] = p.currentLabelPair.GetValue()
}
}
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
switch p.currentByte {
case ',':
return p.startLabelName
case '}':
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
return p.readingValue
default:
p.parseError(fmt.Sprintf("unexpected end of label value %q", p.currentLabelPair.Value))
return nil
}
}
// readingValue represents the state where the last byte read (now in
// p.currentByte) is the first byte of the sample value (i.e. a float).
func (p *Parser) readingValue() stateFn {
// When we are here, we have read all the labels, so for the
// infamous special case of a summary, we can finally find out
// if the metric already exists.
if p.currentMF.GetType() == dto.MetricType_SUMMARY {
signature := model.LabelsToSignature(p.currentLabels)
if summary := p.summaries[signature]; summary != nil {
p.currentMetric = summary
} else {
p.summaries[signature] = p.currentMetric
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
}
} else {
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
}
if p.readTokenUntilWhitespace(); p.err != nil {
return nil // Unexpected end of input.
}
value, err := strconv.ParseFloat(p.currentToken.String(), 64)
if err != nil {
// Create a more helpful error message.
p.parseError(fmt.Sprintf("expected float as value, got %q", p.currentToken.String()))
return nil
}
switch p.currentMF.GetType() {
case dto.MetricType_COUNTER:
p.currentMetric.Counter = &dto.Counter{Value: proto.Float64(value)}
case dto.MetricType_GAUGE:
p.currentMetric.Gauge = &dto.Gauge{Value: proto.Float64(value)}
case dto.MetricType_UNTYPED:
p.currentMetric.Untyped = &dto.Untyped{Value: proto.Float64(value)}
case dto.MetricType_SUMMARY:
// *sigh*
if p.currentMetric.Summary == nil {
p.currentMetric.Summary = &dto.Summary{}
}
switch {
case p.currentIsSummaryCount:
p.currentMetric.Summary.SampleCount = proto.Uint64(uint64(value))
case p.currentIsSummarySum:
p.currentMetric.Summary.SampleSum = proto.Float64(value)
case !math.IsNaN(p.currentQuantile):
p.currentMetric.Summary.Quantile = append(
p.currentMetric.Summary.Quantile,
&dto.Quantile{
Quantile: proto.Float64(p.currentQuantile),
Value: proto.Float64(value),
},
)
}
default:
p.err = fmt.Errorf("unexpected type for metric name %q", p.currentMF.GetName())
}
if p.currentByte == '\n' {
return p.startOfLine
}
return p.startTimestamp
}
// startTimestamp represents the state where the next byte read from p.buf is
// the start of the timestamp (or whitespace leading up to it).
func (p *Parser) startTimestamp() stateFn {
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
if p.readTokenUntilWhitespace(); p.err != nil {
return nil // Unexpected end of input.
}
timestamp, err := strconv.ParseInt(p.currentToken.String(), 10, 64)
if err != nil {
// Create a more helpful error message.
p.parseError(fmt.Sprintf("expected integer as timestamp, got %q", p.currentToken.String()))
return nil
}
p.currentMetric.TimestampMs = proto.Int64(timestamp)
if p.readTokenUntilNewline(false); p.err != nil {
return nil // Unexpected end of input.
}
if p.currentToken.Len() > 0 {
p.parseError(fmt.Sprintf("spurious string after timestamp: %q", p.currentToken.String()))
return nil
}
return p.startOfLine
}
// readingHelp represents the state where the last byte read (now in
// p.currentByte) is the first byte of the docstring after 'HELP'.
func (p *Parser) readingHelp() stateFn {
if p.currentMF.Help != nil {
p.parseError(fmt.Sprintf("second HELP line for metric name %q", p.currentMF.GetName()))
return nil
}
// Rest of line is the docstring.
if p.readTokenUntilNewline(true); p.err != nil {
return nil // Unexpected end of input.
}
p.currentMF.Help = proto.String(p.currentToken.String())
return p.startOfLine
}
// readingType represents the state where the last byte read (now in
// p.currentByte) is the first byte of the type hint after 'HELP'.
func (p *Parser) readingType() stateFn {
if p.currentMF.Type != nil {
p.parseError(fmt.Sprintf("second TYPE line for metric name %q, or TYPE reported after samples", p.currentMF.GetName()))
return nil
}
// Rest of line is the type.
if p.readTokenUntilNewline(false); p.err != nil {
return nil // Unexpected end of input.
}
metricType, ok := dto.MetricType_value[strings.ToUpper(p.currentToken.String())]
if !ok {
p.parseError(fmt.Sprintf("unknown metric type %q", p.currentToken.String()))
return nil
}
p.currentMF.Type = dto.MetricType(metricType).Enum()
return p.startOfLine
}
// parseError sets p.err to a ParseError at the current line with the given
// message.
func (p *Parser) parseError(msg string) {
p.err = ParseError{
Line: p.lineCount,
Msg: msg,
}
}
// skipBlankTab reads (and discards) bytes from p.buf until it encounters a byte
// that is neither ' ' nor '\t'. That byte is left in p.currentByte.
func (p *Parser) skipBlankTab() {
for {
if p.currentByte, p.err = p.buf.ReadByte(); p.err != nil || !isBlankOrTab(p.currentByte) {
return
}
}
}
// skipBlankTabIfCurrentBlankTab works exactly as skipBlankTab but doesn't do
// anything if p.currentByte is neither ' ' nor '\t'.
func (p *Parser) skipBlankTabIfCurrentBlankTab() {
if isBlankOrTab(p.currentByte) {
p.skipBlankTab()
}
}
// readTokenUntilWhitespace copies bytes from p.buf into p.currentToken. The
// first byte considered is the byte already read (now in p.currentByte). The
// first whitespace byte encountered is still copied into p.currentByte, but not
// into p.currentToken.
func (p *Parser) readTokenUntilWhitespace() {
p.currentToken.Reset()
for p.err == nil && !isBlankOrTab(p.currentByte) && p.currentByte != '\n' {
p.currentToken.WriteByte(p.currentByte)
p.currentByte, p.err = p.buf.ReadByte()
}
}
// readTokenUntilNewline copies bytes from p.buf into p.currentToken. The first
// byte considered is the byte already read (now in p.currentByte). The first
// newline byte encountered is still copied into p.currentByte, but not into
// p.currentToken. If recognizeEscapeSequence is true, two escape sequences are
// recognized: '\\' tranlates into '\', and '\n' into a line-feed character. All
// other escape sequences are invalid and cause an error.
func (p *Parser) readTokenUntilNewline(recognizeEscapeSequence bool) {
p.currentToken.Reset()
escaped := false
for p.err == nil {
if recognizeEscapeSequence && escaped {
switch p.currentByte {
case '\\':
p.currentToken.WriteByte(p.currentByte)
case 'n':
p.currentToken.WriteByte('\n')
default:
p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte))
return
}
escaped = false
} else {
switch p.currentByte {
case '\n':
return
case '\\':
escaped = true
default:
p.currentToken.WriteByte(p.currentByte)
}
}
p.currentByte, p.err = p.buf.ReadByte()
}
}
// readTokenAsMetricName copies a metric name from p.buf into p.currentToken.
// The first byte considered is the byte already read (now in p.currentByte).
// The first byte not part of a metric name is still copied into p.currentByte,
// but not into p.currentToken.
func (p *Parser) readTokenAsMetricName() {
p.currentToken.Reset()
if !isValidMetricNameStart(p.currentByte) {
return
}
for {
p.currentToken.WriteByte(p.currentByte)
p.currentByte, p.err = p.buf.ReadByte()
if p.err != nil || !isValidMetricNameContinuation(p.currentByte) {
return
}
}
}
// readTokenAsLabelName copies a label name from p.buf into p.currentToken.
// The first byte considered is the byte already read (now in p.currentByte).
// The first byte not part of a label name is still copied into p.currentByte,
// but not into p.currentToken.
func (p *Parser) readTokenAsLabelName() {
p.currentToken.Reset()
if !isValidLabelNameStart(p.currentByte) {
return
}
for {
p.currentToken.WriteByte(p.currentByte)
p.currentByte, p.err = p.buf.ReadByte()
if p.err != nil || !isValidLabelNameContinuation(p.currentByte) {
return
}
}
}
// readTokenAsLabelValue copies a label value from p.buf into p.currentToken.
// In contrast to the other 'readTokenAs...' functions, which start with the
// last read byte in p.currentByte, this method ignores p.currentByte and starts
// with reading a new byte from p.buf. The first byte not part of a label value
// is still copied into p.currentByte, but not into p.currentToken.
func (p *Parser) readTokenAsLabelValue() {
p.currentToken.Reset()
escaped := false
for {
if p.currentByte, p.err = p.buf.ReadByte(); p.err != nil {
return
}
if escaped {
switch p.currentByte {
case '"', '\\':
p.currentToken.WriteByte(p.currentByte)
case 'n':
p.currentToken.WriteByte('\n')
default:
p.parseError(fmt.Sprintf("invalid escape sequence '\\%c'", p.currentByte))
return
}
escaped = false
continue
}
switch p.currentByte {
case '"':
return
case '\n':
p.parseError(fmt.Sprintf("label value %q contains unescaped new-line", p.currentToken.String()))
return
case '\\':
escaped = true
default:
p.currentToken.WriteByte(p.currentByte)
}
}
}
func (p *Parser) setOrCreateCurrentMF() {
p.currentIsSummaryCount = false
p.currentIsSummarySum = false
name := p.currentToken.String()
if p.currentMF = p.metricFamiliesByName[name]; p.currentMF != nil {
return
}
// Try out if this is a _sum or _count for a summary.
summaryName := summaryMetricName(name)
if p.currentMF = p.metricFamiliesByName[summaryName]; p.currentMF != nil {
if p.currentMF.GetType() == dto.MetricType_SUMMARY {
if isCount(name) {
p.currentIsSummaryCount = true
}
if isSum(name) {
p.currentIsSummarySum = true
}
return
}
}
p.currentMF = &dto.MetricFamily{Name: proto.String(name)}
p.metricFamiliesByName[name] = p.currentMF
}
func isValidLabelNameStart(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_'
}
func isValidLabelNameContinuation(b byte) bool {
return isValidLabelNameStart(b) || (b >= '0' && b <= '9')
}
func isValidMetricNameStart(b byte) bool {
return isValidLabelNameStart(b) || b == ':'
}
func isValidMetricNameContinuation(b byte) bool {
return isValidLabelNameContinuation(b) || b == ':'
}
func isBlankOrTab(b byte) bool {
return b == ' ' || b == '\t'
}
func isCount(name string) bool {
return len(name) > 6 && name[len(name)-6:] == "_count"
}
func isSum(name string) bool {
return len(name) > 4 && name[len(name)-4:] == "_sum"
}
func summaryMetricName(name string) string {
switch {
case isCount(name):
return name[:len(name)-6]
case isSum(name):
return name[:len(name)-4]
default:
return name
}
}

View File

@@ -0,0 +1,529 @@
// Copyright 2014 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 text
import (
"math"
"strings"
"testing"
"code.google.com/p/goprotobuf/proto"
dto "github.com/prometheus/client_model/go"
)
var parser Parser
func testParse(t testing.TB) {
var scenarios = []struct {
in string
out []*dto.MetricFamily
}{
// 0: Empty lines as input.
{
in: `
`,
out: []*dto.MetricFamily{},
},
// 1: Minimal case.
{
in: `
minimal_metric 1.234
another_metric -3e3 103948
# Even that:
no_labels{} 3
# HELP line for non-existing metric will be ignored.
`,
out: []*dto.MetricFamily{
&dto.MetricFamily{
Name: proto.String("minimal_metric"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Untyped: &dto.Untyped{
Value: proto.Float64(1.234),
},
},
},
},
&dto.MetricFamily{
Name: proto.String("another_metric"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Untyped: &dto.Untyped{
Value: proto.Float64(-3e3),
},
TimestampMs: proto.Int64(103948),
},
},
},
&dto.MetricFamily{
Name: proto.String("no_labels"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Untyped: &dto.Untyped{
Value: proto.Float64(3),
},
},
},
},
},
},
// 2: Counters & gauges, docstrings, various whitespace, escape sequences.
{
in: `
# A normal comment.
#
# TYPE name counter
name{labelname="val1",basename="basevalue"} NaN
name {labelname="val2",basename="base\"v\\al\nue"} 0.23 1234567890
# HELP name two-line\n doc str\\ing
# HELP name2 doc str"ing 2
# TYPE name2 gauge
name2{labelname="val2" ,basename = "basevalue2" } +Inf 54321
name2{ labelname = "val1" , }-Inf
`,
out: []*dto.MetricFamily{
&dto.MetricFamily{
Name: proto.String("name"),
Help: proto.String("two-line\n doc str\\ing"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("labelname"),
Value: proto.String("val1"),
},
&dto.LabelPair{
Name: proto.String("basename"),
Value: proto.String("basevalue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(math.NaN()),
},
},
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("labelname"),
Value: proto.String("val2"),
},
&dto.LabelPair{
Name: proto.String("basename"),
Value: proto.String("base\"v\\al\nue"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(.23),
},
TimestampMs: proto.Int64(1234567890),
},
},
},
&dto.MetricFamily{
Name: proto.String("name2"),
Help: proto.String("doc str\"ing 2"),
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("labelname"),
Value: proto.String("val2"),
},
&dto.LabelPair{
Name: proto.String("basename"),
Value: proto.String("basevalue2"),
},
},
Gauge: &dto.Gauge{
Value: proto.Float64(math.Inf(+1)),
},
TimestampMs: proto.Int64(54321),
},
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("labelname"),
Value: proto.String("val1"),
},
},
Gauge: &dto.Gauge{
Value: proto.Float64(math.Inf(-1)),
},
},
},
},
},
},
// 3: The evil summary, mixed with other types and funny comments.
{
in: `
# TYPE my_summary summary
my_summary{n1="val1",quantile="0.5"} 110
decoy -1 -2
my_summary{n1="val1",quantile="0.9"} 140 1
my_summary_count{n1="val1"} 42
# Latest timestamp wins in case of a summary.
my_summary_sum{n1="val1"} 4711 2
fake_sum{n1="val1"} 2001
# TYPE another_summary summary
another_summary_count{n2="val2",n1="val1"} 20
my_summary_count{n2="val2",n1="val1"} 5 5
another_summary{n1="val1",n2="val2",quantile=".3"} -1.2
my_summary_sum{n1="val2"} 08 15
my_summary{n1="val3", quantile="0.2"} 4711
my_summary{n1="val1",n2="val2",quantile="-12.34",} NaN
# some
# funny comments
# HELP
# HELP
# HELP my_summary
# HELP my_summary
`,
out: []*dto.MetricFamily{
&dto.MetricFamily{
Name: proto.String("fake_sum"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("n1"),
Value: proto.String("val1"),
},
},
Untyped: &dto.Untyped{
Value: proto.Float64(2001),
},
},
},
},
&dto.MetricFamily{
Name: proto.String("decoy"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Untyped: &dto.Untyped{
Value: proto.Float64(-1),
},
TimestampMs: proto.Int64(-2),
},
},
},
&dto.MetricFamily{
Name: proto.String("my_summary"),
Type: dto.MetricType_SUMMARY.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("n1"),
Value: proto.String("val1"),
},
},
Summary: &dto.Summary{
SampleCount: proto.Uint64(42),
SampleSum: proto.Float64(4711),
Quantile: []*dto.Quantile{
&dto.Quantile{
Quantile: proto.Float64(0.5),
Value: proto.Float64(110),
},
&dto.Quantile{
Quantile: proto.Float64(0.9),
Value: proto.Float64(140),
},
},
},
TimestampMs: proto.Int64(2),
},
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("n2"),
Value: proto.String("val2"),
},
&dto.LabelPair{
Name: proto.String("n1"),
Value: proto.String("val1"),
},
},
Summary: &dto.Summary{
SampleCount: proto.Uint64(5),
Quantile: []*dto.Quantile{
&dto.Quantile{
Quantile: proto.Float64(-12.34),
Value: proto.Float64(math.NaN()),
},
},
},
TimestampMs: proto.Int64(5),
},
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("n1"),
Value: proto.String("val2"),
},
},
Summary: &dto.Summary{
SampleSum: proto.Float64(8),
},
TimestampMs: proto.Int64(15),
},
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("n1"),
Value: proto.String("val3"),
},
},
Summary: &dto.Summary{
Quantile: []*dto.Quantile{
&dto.Quantile{
Quantile: proto.Float64(0.2),
Value: proto.Float64(4711),
},
},
},
},
},
},
&dto.MetricFamily{
Name: proto.String("another_summary"),
Type: dto.MetricType_SUMMARY.Enum(),
Metric: []*dto.Metric{
&dto.Metric{
Label: []*dto.LabelPair{
&dto.LabelPair{
Name: proto.String("n2"),
Value: proto.String("val2"),
},
&dto.LabelPair{
Name: proto.String("n1"),
Value: proto.String("val1"),
},
},
Summary: &dto.Summary{
SampleCount: proto.Uint64(20),
Quantile: []*dto.Quantile{
&dto.Quantile{
Quantile: proto.Float64(0.3),
Value: proto.Float64(-1.2),
},
},
},
},
},
},
},
},
}
for i, scenario := range scenarios {
out, err := parser.TextToMetricFamilies(strings.NewReader(scenario.in))
if err != nil {
t.Errorf("%d. error: %s", i, err)
continue
}
if expected, got := len(scenario.out), len(out); expected != got {
t.Errorf(
"%d. expected %d MetricFamilies, got %d",
i, expected, got,
)
}
for _, expected := range scenario.out {
got, ok := out[expected.GetName()]
if !ok {
t.Errorf(
"%d. expected MetricFamily %q, found none",
i, expected.GetName(),
)
continue
}
if expected.String() != got.String() {
t.Errorf(
"%d. expected MetricFamily %s, got %s",
i, expected, got,
)
}
}
}
}
func TestParse(t *testing.T) {
testParse(t)
}
func BenchmarkParse(b *testing.B) {
for i := 0; i < b.N; i++ {
testParse(b)
}
}
func testParseError(t testing.TB) {
var scenarios = []struct {
in string
err string
}{
// 0: No new-line at end of input.
{
in: `bla 3.14`,
err: "EOF",
},
// 1: Invalid escape sequence in label value.
{
in: `metric{label="\t"} 3.14`,
err: "text format parsing error in line 1: invalid escape sequence",
},
// 2: Newline in label value.
{
in: `
metric{label="new
line"} 3.14
`,
err: `text format parsing error in line 2: label value "new" contains unescaped new-line`,
},
// 3:
{
in: `metric{@="bla"} 3.14`,
err: "text format parsing error in line 1: invalid label name for metric",
},
// 4:
{
in: `metric{__name__="bla"} 3.14`,
err: `text format parsing error in line 1: label name "__name__" is reserved`,
},
// 5:
{
in: `metric{label+="bla"} 3.14`,
err: "text format parsing error in line 1: expected '=' after label name",
},
// 6:
{
in: `metric{label=bla} 3.14`,
err: "text format parsing error in line 1: expected '\"' at start of label value",
},
// 7:
{
in: `
# TYPE metric summary
metric{quantile="bla"} 3.14
`,
err: "text format parsing error in line 3: expected float as value for quantile label",
},
// 8:
{
in: `metric{label="bla"+} 3.14`,
err: "text format parsing error in line 1: unexpected end of label value",
},
// 9:
{
in: `metric{label="bla"} 3.14 2.72
`,
err: "text format parsing error in line 1: expected integer as timestamp",
},
// 10:
{
in: `metric{label="bla"} 3.14 2 3
`,
err: "text format parsing error in line 1: spurious string after timestamp",
},
// 11:
{
in: `metric{label="bla"} blubb
`,
err: "text format parsing error in line 1: expected float as value",
},
// 12:
{
in: `
# HELP metric one
# HELP metric two
`,
err: "text format parsing error in line 3: second HELP line for metric name",
},
// 13:
{
in: `
# TYPE metric counter
# TYPE metric untyped
`,
err: `text format parsing error in line 3: second TYPE line for metric name "metric", or TYPE reported after samples`,
},
// 14:
{
in: `
metric 4.12
# TYPE metric counter
`,
err: `text format parsing error in line 3: second TYPE line for metric name "metric", or TYPE reported after samples`,
},
// 14:
{
in: `
# TYPE metric bla
`,
err: "text format parsing error in line 2: unknown metric type",
},
// 15:
{
in: `
# TYPE met-ric
`,
err: "text format parsing error in line 2: invalid metric name in comment",
},
// 16:
{
in: `@invalidmetric{label="bla"} 3.14 2`,
err: "text format parsing error in line 1: invalid metric name",
},
// 17:
{
in: `{label="bla"} 3.14 2`,
err: "text format parsing error in line 1: invalid metric name",
},
}
for i, scenario := range scenarios {
_, err := parser.TextToMetricFamilies(strings.NewReader(scenario.in))
if err == nil {
t.Errorf("%d. expected error, got nil", i)
continue
}
if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 {
t.Errorf(
"%d. expected error starting with %q, got %q",
i, expected, got,
)
}
}
}
func TestParseError(t *testing.T) {
testParseError(t)
}
func BenchmarkParseError(b *testing.B) {
for i := 0; i < b.N; i++ {
testParseError(b)
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2014 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 text
import (
"fmt"
"io"
"code.google.com/p/goprotobuf/proto"
"github.com/matttproud/golang_protobuf_extensions/ext"
dto "github.com/prometheus/client_model/go"
)
// WriteProtoDelimited writes the MetricFamily to the writer in delimited
// protobuf format and returns the number of bytes written and any error
// encountered.
func WriteProtoDelimited(w io.Writer, p *dto.MetricFamily) (int, error) {
return ext.WriteDelimited(w, p)
}
// WriteProtoText writes the MetricFamily to the writer in text format and
// returns the number of bytes written and any error encountered.
func WriteProtoText(w io.Writer, p *dto.MetricFamily) (int, error) {
return fmt.Fprintf(w, "%s\n", proto.MarshalTextString(p))
}
// WriteProtoCompactText writes the MetricFamily to the writer in compact text
// format and returns the number of bytes written and any error encountered.
func WriteProtoCompactText(w io.Writer, p *dto.MetricFamily) (int, error) {
return fmt.Fprintf(w, "%s\n", p)
}

Binary file not shown.

View File

@@ -0,0 +1,322 @@
# HELP http_request_duration_microseconds The HTTP request latencies in microseconds.
# TYPE http_request_duration_microseconds summary
http_request_duration_microseconds{handler="/",quantile="0.5"} 0
http_request_duration_microseconds{handler="/",quantile="0.9"} 0
http_request_duration_microseconds{handler="/",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/"} 0
http_request_duration_microseconds_count{handler="/"} 0
http_request_duration_microseconds{handler="/alerts",quantile="0.5"} 0
http_request_duration_microseconds{handler="/alerts",quantile="0.9"} 0
http_request_duration_microseconds{handler="/alerts",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/alerts"} 0
http_request_duration_microseconds_count{handler="/alerts"} 0
http_request_duration_microseconds{handler="/api/metrics",quantile="0.5"} 0
http_request_duration_microseconds{handler="/api/metrics",quantile="0.9"} 0
http_request_duration_microseconds{handler="/api/metrics",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/api/metrics"} 0
http_request_duration_microseconds_count{handler="/api/metrics"} 0
http_request_duration_microseconds{handler="/api/query",quantile="0.5"} 0
http_request_duration_microseconds{handler="/api/query",quantile="0.9"} 0
http_request_duration_microseconds{handler="/api/query",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/api/query"} 0
http_request_duration_microseconds_count{handler="/api/query"} 0
http_request_duration_microseconds{handler="/api/query_range",quantile="0.5"} 0
http_request_duration_microseconds{handler="/api/query_range",quantile="0.9"} 0
http_request_duration_microseconds{handler="/api/query_range",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/api/query_range"} 0
http_request_duration_microseconds_count{handler="/api/query_range"} 0
http_request_duration_microseconds{handler="/api/targets",quantile="0.5"} 0
http_request_duration_microseconds{handler="/api/targets",quantile="0.9"} 0
http_request_duration_microseconds{handler="/api/targets",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/api/targets"} 0
http_request_duration_microseconds_count{handler="/api/targets"} 0
http_request_duration_microseconds{handler="/consoles/",quantile="0.5"} 0
http_request_duration_microseconds{handler="/consoles/",quantile="0.9"} 0
http_request_duration_microseconds{handler="/consoles/",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/consoles/"} 0
http_request_duration_microseconds_count{handler="/consoles/"} 0
http_request_duration_microseconds{handler="/graph",quantile="0.5"} 0
http_request_duration_microseconds{handler="/graph",quantile="0.9"} 0
http_request_duration_microseconds{handler="/graph",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/graph"} 0
http_request_duration_microseconds_count{handler="/graph"} 0
http_request_duration_microseconds{handler="/heap",quantile="0.5"} 0
http_request_duration_microseconds{handler="/heap",quantile="0.9"} 0
http_request_duration_microseconds{handler="/heap",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/heap"} 0
http_request_duration_microseconds_count{handler="/heap"} 0
http_request_duration_microseconds{handler="/static/",quantile="0.5"} 0
http_request_duration_microseconds{handler="/static/",quantile="0.9"} 0
http_request_duration_microseconds{handler="/static/",quantile="0.99"} 0
http_request_duration_microseconds_sum{handler="/static/"} 0
http_request_duration_microseconds_count{handler="/static/"} 0
http_request_duration_microseconds{handler="prometheus",quantile="0.5"} 1307.275
http_request_duration_microseconds{handler="prometheus",quantile="0.9"} 1858.632
http_request_duration_microseconds{handler="prometheus",quantile="0.99"} 3087.384
http_request_duration_microseconds_sum{handler="prometheus"} 179886.5000000001
http_request_duration_microseconds_count{handler="prometheus"} 119
# HELP http_request_size_bytes The HTTP request sizes in bytes.
# TYPE http_request_size_bytes summary
http_request_size_bytes{handler="/",quantile="0.5"} 0
http_request_size_bytes{handler="/",quantile="0.9"} 0
http_request_size_bytes{handler="/",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/"} 0
http_request_size_bytes_count{handler="/"} 0
http_request_size_bytes{handler="/alerts",quantile="0.5"} 0
http_request_size_bytes{handler="/alerts",quantile="0.9"} 0
http_request_size_bytes{handler="/alerts",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/alerts"} 0
http_request_size_bytes_count{handler="/alerts"} 0
http_request_size_bytes{handler="/api/metrics",quantile="0.5"} 0
http_request_size_bytes{handler="/api/metrics",quantile="0.9"} 0
http_request_size_bytes{handler="/api/metrics",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/api/metrics"} 0
http_request_size_bytes_count{handler="/api/metrics"} 0
http_request_size_bytes{handler="/api/query",quantile="0.5"} 0
http_request_size_bytes{handler="/api/query",quantile="0.9"} 0
http_request_size_bytes{handler="/api/query",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/api/query"} 0
http_request_size_bytes_count{handler="/api/query"} 0
http_request_size_bytes{handler="/api/query_range",quantile="0.5"} 0
http_request_size_bytes{handler="/api/query_range",quantile="0.9"} 0
http_request_size_bytes{handler="/api/query_range",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/api/query_range"} 0
http_request_size_bytes_count{handler="/api/query_range"} 0
http_request_size_bytes{handler="/api/targets",quantile="0.5"} 0
http_request_size_bytes{handler="/api/targets",quantile="0.9"} 0
http_request_size_bytes{handler="/api/targets",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/api/targets"} 0
http_request_size_bytes_count{handler="/api/targets"} 0
http_request_size_bytes{handler="/consoles/",quantile="0.5"} 0
http_request_size_bytes{handler="/consoles/",quantile="0.9"} 0
http_request_size_bytes{handler="/consoles/",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/consoles/"} 0
http_request_size_bytes_count{handler="/consoles/"} 0
http_request_size_bytes{handler="/graph",quantile="0.5"} 0
http_request_size_bytes{handler="/graph",quantile="0.9"} 0
http_request_size_bytes{handler="/graph",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/graph"} 0
http_request_size_bytes_count{handler="/graph"} 0
http_request_size_bytes{handler="/heap",quantile="0.5"} 0
http_request_size_bytes{handler="/heap",quantile="0.9"} 0
http_request_size_bytes{handler="/heap",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/heap"} 0
http_request_size_bytes_count{handler="/heap"} 0
http_request_size_bytes{handler="/static/",quantile="0.5"} 0
http_request_size_bytes{handler="/static/",quantile="0.9"} 0
http_request_size_bytes{handler="/static/",quantile="0.99"} 0
http_request_size_bytes_sum{handler="/static/"} 0
http_request_size_bytes_count{handler="/static/"} 0
http_request_size_bytes{handler="prometheus",quantile="0.5"} 291
http_request_size_bytes{handler="prometheus",quantile="0.9"} 291
http_request_size_bytes{handler="prometheus",quantile="0.99"} 291
http_request_size_bytes_sum{handler="prometheus"} 34488
http_request_size_bytes_count{handler="prometheus"} 119
# HELP http_requests_total Total number of HTTP requests made.
# TYPE http_requests_total counter
http_requests_total{code="200",handler="prometheus",method="get"} 119
# HELP http_response_size_bytes The HTTP response sizes in bytes.
# TYPE http_response_size_bytes summary
http_response_size_bytes{handler="/",quantile="0.5"} 0
http_response_size_bytes{handler="/",quantile="0.9"} 0
http_response_size_bytes{handler="/",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/"} 0
http_response_size_bytes_count{handler="/"} 0
http_response_size_bytes{handler="/alerts",quantile="0.5"} 0
http_response_size_bytes{handler="/alerts",quantile="0.9"} 0
http_response_size_bytes{handler="/alerts",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/alerts"} 0
http_response_size_bytes_count{handler="/alerts"} 0
http_response_size_bytes{handler="/api/metrics",quantile="0.5"} 0
http_response_size_bytes{handler="/api/metrics",quantile="0.9"} 0
http_response_size_bytes{handler="/api/metrics",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/api/metrics"} 0
http_response_size_bytes_count{handler="/api/metrics"} 0
http_response_size_bytes{handler="/api/query",quantile="0.5"} 0
http_response_size_bytes{handler="/api/query",quantile="0.9"} 0
http_response_size_bytes{handler="/api/query",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/api/query"} 0
http_response_size_bytes_count{handler="/api/query"} 0
http_response_size_bytes{handler="/api/query_range",quantile="0.5"} 0
http_response_size_bytes{handler="/api/query_range",quantile="0.9"} 0
http_response_size_bytes{handler="/api/query_range",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/api/query_range"} 0
http_response_size_bytes_count{handler="/api/query_range"} 0
http_response_size_bytes{handler="/api/targets",quantile="0.5"} 0
http_response_size_bytes{handler="/api/targets",quantile="0.9"} 0
http_response_size_bytes{handler="/api/targets",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/api/targets"} 0
http_response_size_bytes_count{handler="/api/targets"} 0
http_response_size_bytes{handler="/consoles/",quantile="0.5"} 0
http_response_size_bytes{handler="/consoles/",quantile="0.9"} 0
http_response_size_bytes{handler="/consoles/",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/consoles/"} 0
http_response_size_bytes_count{handler="/consoles/"} 0
http_response_size_bytes{handler="/graph",quantile="0.5"} 0
http_response_size_bytes{handler="/graph",quantile="0.9"} 0
http_response_size_bytes{handler="/graph",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/graph"} 0
http_response_size_bytes_count{handler="/graph"} 0
http_response_size_bytes{handler="/heap",quantile="0.5"} 0
http_response_size_bytes{handler="/heap",quantile="0.9"} 0
http_response_size_bytes{handler="/heap",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/heap"} 0
http_response_size_bytes_count{handler="/heap"} 0
http_response_size_bytes{handler="/static/",quantile="0.5"} 0
http_response_size_bytes{handler="/static/",quantile="0.9"} 0
http_response_size_bytes{handler="/static/",quantile="0.99"} 0
http_response_size_bytes_sum{handler="/static/"} 0
http_response_size_bytes_count{handler="/static/"} 0
http_response_size_bytes{handler="prometheus",quantile="0.5"} 2049
http_response_size_bytes{handler="prometheus",quantile="0.9"} 2058
http_response_size_bytes{handler="prometheus",quantile="0.99"} 2064
http_response_size_bytes_sum{handler="prometheus"} 247001
http_response_size_bytes_count{handler="prometheus"} 119
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 0.55
# HELP process_goroutines Number of goroutines that currently exist.
# TYPE process_goroutines gauge
process_goroutines 70
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 8192
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 29
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 5.3870592e+07
# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.42236894836e+09
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 5.41478912e+08
# HELP prometheus_dns_sd_lookup_failures_total The number of DNS-SD lookup failures.
# TYPE prometheus_dns_sd_lookup_failures_total counter
prometheus_dns_sd_lookup_failures_total 0
# HELP prometheus_dns_sd_lookups_total The number of DNS-SD lookups.
# TYPE prometheus_dns_sd_lookups_total counter
prometheus_dns_sd_lookups_total 7
# HELP prometheus_evaluator_duration_milliseconds The duration for all evaluations to execute.
# TYPE prometheus_evaluator_duration_milliseconds summary
prometheus_evaluator_duration_milliseconds{quantile="0.01"} 0
prometheus_evaluator_duration_milliseconds{quantile="0.05"} 0
prometheus_evaluator_duration_milliseconds{quantile="0.5"} 0
prometheus_evaluator_duration_milliseconds{quantile="0.9"} 1
prometheus_evaluator_duration_milliseconds{quantile="0.99"} 1
prometheus_evaluator_duration_milliseconds_sum 12
prometheus_evaluator_duration_milliseconds_count 23
# HELP prometheus_local_storage_checkpoint_duration_milliseconds The duration (in milliseconds) it took to checkpoint in-memory metrics and head chunks.
# TYPE prometheus_local_storage_checkpoint_duration_milliseconds gauge
prometheus_local_storage_checkpoint_duration_milliseconds 0
# HELP prometheus_local_storage_chunk_ops_total The total number of chunk operations by their type.
# TYPE prometheus_local_storage_chunk_ops_total counter
prometheus_local_storage_chunk_ops_total{type="create"} 598
prometheus_local_storage_chunk_ops_total{type="persist"} 174
prometheus_local_storage_chunk_ops_total{type="pin"} 920
prometheus_local_storage_chunk_ops_total{type="transcode"} 415
prometheus_local_storage_chunk_ops_total{type="unpin"} 920
# HELP prometheus_local_storage_indexing_batch_latency_milliseconds Quantiles for batch indexing latencies in milliseconds.
# TYPE prometheus_local_storage_indexing_batch_latency_milliseconds summary
prometheus_local_storage_indexing_batch_latency_milliseconds{quantile="0.5"} 0
prometheus_local_storage_indexing_batch_latency_milliseconds{quantile="0.9"} 0
prometheus_local_storage_indexing_batch_latency_milliseconds{quantile="0.99"} 0
prometheus_local_storage_indexing_batch_latency_milliseconds_sum 0
prometheus_local_storage_indexing_batch_latency_milliseconds_count 1
# HELP prometheus_local_storage_indexing_batch_sizes Quantiles for indexing batch sizes (number of metrics per batch).
# TYPE prometheus_local_storage_indexing_batch_sizes summary
prometheus_local_storage_indexing_batch_sizes{quantile="0.5"} 2
prometheus_local_storage_indexing_batch_sizes{quantile="0.9"} 2
prometheus_local_storage_indexing_batch_sizes{quantile="0.99"} 2
prometheus_local_storage_indexing_batch_sizes_sum 2
prometheus_local_storage_indexing_batch_sizes_count 1
# HELP prometheus_local_storage_indexing_queue_capacity The capacity of the indexing queue.
# TYPE prometheus_local_storage_indexing_queue_capacity gauge
prometheus_local_storage_indexing_queue_capacity 16384
# HELP prometheus_local_storage_indexing_queue_length The number of metrics waiting to be indexed.
# TYPE prometheus_local_storage_indexing_queue_length gauge
prometheus_local_storage_indexing_queue_length 0
# HELP prometheus_local_storage_ingested_samples_total The total number of samples ingested.
# TYPE prometheus_local_storage_ingested_samples_total counter
prometheus_local_storage_ingested_samples_total 30473
# HELP prometheus_local_storage_invalid_preload_requests_total The total number of preload requests referring to a non-existent series. This is an indication of outdated label indexes.
# TYPE prometheus_local_storage_invalid_preload_requests_total counter
prometheus_local_storage_invalid_preload_requests_total 0
# HELP prometheus_local_storage_memory_chunkdescs The current number of chunk descriptors in memory.
# TYPE prometheus_local_storage_memory_chunkdescs gauge
prometheus_local_storage_memory_chunkdescs 1059
# HELP prometheus_local_storage_memory_chunks The current number of chunks in memory, excluding cloned chunks (i.e. chunks without a descriptor).
# TYPE prometheus_local_storage_memory_chunks gauge
prometheus_local_storage_memory_chunks 1020
# HELP prometheus_local_storage_memory_series The current number of series in memory.
# TYPE prometheus_local_storage_memory_series gauge
prometheus_local_storage_memory_series 424
# HELP prometheus_local_storage_persist_latency_microseconds A summary of latencies for persisting each chunk.
# TYPE prometheus_local_storage_persist_latency_microseconds summary
prometheus_local_storage_persist_latency_microseconds{quantile="0.5"} 30.377
prometheus_local_storage_persist_latency_microseconds{quantile="0.9"} 203.539
prometheus_local_storage_persist_latency_microseconds{quantile="0.99"} 2626.463
prometheus_local_storage_persist_latency_microseconds_sum 20424.415
prometheus_local_storage_persist_latency_microseconds_count 174
# HELP prometheus_local_storage_persist_queue_capacity The total capacity of the persist queue.
# TYPE prometheus_local_storage_persist_queue_capacity gauge
prometheus_local_storage_persist_queue_capacity 1024
# HELP prometheus_local_storage_persist_queue_length The current number of chunks waiting in the persist queue.
# TYPE prometheus_local_storage_persist_queue_length gauge
prometheus_local_storage_persist_queue_length 0
# HELP prometheus_local_storage_series_ops_total The total number of series operations by their type.
# TYPE prometheus_local_storage_series_ops_total counter
prometheus_local_storage_series_ops_total{type="create"} 2
prometheus_local_storage_series_ops_total{type="maintenance_in_memory"} 11
# HELP prometheus_notifications_latency_milliseconds Latency quantiles for sending alert notifications (not including dropped notifications).
# TYPE prometheus_notifications_latency_milliseconds summary
prometheus_notifications_latency_milliseconds{quantile="0.5"} 0
prometheus_notifications_latency_milliseconds{quantile="0.9"} 0
prometheus_notifications_latency_milliseconds{quantile="0.99"} 0
prometheus_notifications_latency_milliseconds_sum 0
prometheus_notifications_latency_milliseconds_count 0
# HELP prometheus_notifications_queue_capacity The capacity of the alert notifications queue.
# TYPE prometheus_notifications_queue_capacity gauge
prometheus_notifications_queue_capacity 100
# HELP prometheus_notifications_queue_length The number of alert notifications in the queue.
# TYPE prometheus_notifications_queue_length gauge
prometheus_notifications_queue_length 0
# HELP prometheus_rule_evaluation_duration_milliseconds The duration for a rule to execute.
# TYPE prometheus_rule_evaluation_duration_milliseconds summary
prometheus_rule_evaluation_duration_milliseconds{rule_type="alerting",quantile="0.5"} 0
prometheus_rule_evaluation_duration_milliseconds{rule_type="alerting",quantile="0.9"} 0
prometheus_rule_evaluation_duration_milliseconds{rule_type="alerting",quantile="0.99"} 2
prometheus_rule_evaluation_duration_milliseconds_sum{rule_type="alerting"} 12
prometheus_rule_evaluation_duration_milliseconds_count{rule_type="alerting"} 115
prometheus_rule_evaluation_duration_milliseconds{rule_type="recording",quantile="0.5"} 0
prometheus_rule_evaluation_duration_milliseconds{rule_type="recording",quantile="0.9"} 0
prometheus_rule_evaluation_duration_milliseconds{rule_type="recording",quantile="0.99"} 3
prometheus_rule_evaluation_duration_milliseconds_sum{rule_type="recording"} 15
prometheus_rule_evaluation_duration_milliseconds_count{rule_type="recording"} 115
# HELP prometheus_rule_evaluation_failures_total The total number of rule evaluation failures.
# TYPE prometheus_rule_evaluation_failures_total counter
prometheus_rule_evaluation_failures_total 0
# HELP prometheus_samples_queue_capacity Capacity of the queue for unwritten samples.
# TYPE prometheus_samples_queue_capacity gauge
prometheus_samples_queue_capacity 4096
# HELP prometheus_samples_queue_length Current number of items in the queue for unwritten samples. Each item comprises all samples exposed by one target as one metric family (i.e. metrics of the same name).
# TYPE prometheus_samples_queue_length gauge
prometheus_samples_queue_length 0
# HELP prometheus_target_interval_length_seconds Actual intervals between scrapes.
# TYPE prometheus_target_interval_length_seconds summary
prometheus_target_interval_length_seconds{interval="15s",quantile="0.01"} 14
prometheus_target_interval_length_seconds{interval="15s",quantile="0.05"} 14
prometheus_target_interval_length_seconds{interval="15s",quantile="0.5"} 15
prometheus_target_interval_length_seconds{interval="15s",quantile="0.9"} 15
prometheus_target_interval_length_seconds{interval="15s",quantile="0.99"} 15
prometheus_target_interval_length_seconds_sum{interval="15s"} 175
prometheus_target_interval_length_seconds_count{interval="15s"} 12
prometheus_target_interval_length_seconds{interval="1s",quantile="0.01"} 0
prometheus_target_interval_length_seconds{interval="1s",quantile="0.05"} 0
prometheus_target_interval_length_seconds{interval="1s",quantile="0.5"} 0
prometheus_target_interval_length_seconds{interval="1s",quantile="0.9"} 1
prometheus_target_interval_length_seconds{interval="1s",quantile="0.99"} 1
prometheus_target_interval_length_seconds_sum{interval="1s"} 55
prometheus_target_interval_length_seconds_count{interval="1s"} 117

Binary file not shown.

View File

@@ -0,0 +1,297 @@
// Code generated by protoc-gen-go.
// source: metrics.proto
// DO NOT EDIT!
/*
Package io_prometheus_client is a generated protocol buffer package.
It is generated from these files:
metrics.proto
It has these top-level messages:
LabelPair
Gauge
Counter
Quantile
Summary
Untyped
Metric
MetricFamily
*/
package io_prometheus_client
import proto "code.google.com/p/goprotobuf/proto"
import json "encoding/json"
import math "math"
// Reference proto, json, and math imports to suppress error if they are not otherwise used.
var _ = proto.Marshal
var _ = &json.SyntaxError{}
var _ = math.Inf
type MetricType int32
const (
MetricType_COUNTER MetricType = 0
MetricType_GAUGE MetricType = 1
MetricType_SUMMARY MetricType = 2
MetricType_UNTYPED MetricType = 3
)
var MetricType_name = map[int32]string{
0: "COUNTER",
1: "GAUGE",
2: "SUMMARY",
3: "UNTYPED",
}
var MetricType_value = map[string]int32{
"COUNTER": 0,
"GAUGE": 1,
"SUMMARY": 2,
"UNTYPED": 3,
}
func (x MetricType) Enum() *MetricType {
p := new(MetricType)
*p = x
return p
}
func (x MetricType) String() string {
return proto.EnumName(MetricType_name, int32(x))
}
func (x *MetricType) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(MetricType_value, data, "MetricType")
if err != nil {
return err
}
*x = MetricType(value)
return nil
}
type LabelPair struct {
Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Value *string `protobuf:"bytes,2,opt,name=value" json:"value,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *LabelPair) Reset() { *m = LabelPair{} }
func (m *LabelPair) String() string { return proto.CompactTextString(m) }
func (*LabelPair) ProtoMessage() {}
func (m *LabelPair) GetName() string {
if m != nil && m.Name != nil {
return *m.Name
}
return ""
}
func (m *LabelPair) GetValue() string {
if m != nil && m.Value != nil {
return *m.Value
}
return ""
}
type Gauge struct {
Value *float64 `protobuf:"fixed64,1,opt,name=value" json:"value,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Gauge) Reset() { *m = Gauge{} }
func (m *Gauge) String() string { return proto.CompactTextString(m) }
func (*Gauge) ProtoMessage() {}
func (m *Gauge) GetValue() float64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
type Counter struct {
Value *float64 `protobuf:"fixed64,1,opt,name=value" json:"value,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Counter) Reset() { *m = Counter{} }
func (m *Counter) String() string { return proto.CompactTextString(m) }
func (*Counter) ProtoMessage() {}
func (m *Counter) GetValue() float64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
type Quantile struct {
Quantile *float64 `protobuf:"fixed64,1,opt,name=quantile" json:"quantile,omitempty"`
Value *float64 `protobuf:"fixed64,2,opt,name=value" json:"value,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Quantile) Reset() { *m = Quantile{} }
func (m *Quantile) String() string { return proto.CompactTextString(m) }
func (*Quantile) ProtoMessage() {}
func (m *Quantile) GetQuantile() float64 {
if m != nil && m.Quantile != nil {
return *m.Quantile
}
return 0
}
func (m *Quantile) GetValue() float64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
type Summary 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"`
Quantile []*Quantile `protobuf:"bytes,3,rep,name=quantile" json:"quantile,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Summary) Reset() { *m = Summary{} }
func (m *Summary) String() string { return proto.CompactTextString(m) }
func (*Summary) ProtoMessage() {}
func (m *Summary) GetSampleCount() uint64 {
if m != nil && m.SampleCount != nil {
return *m.SampleCount
}
return 0
}
func (m *Summary) GetSampleSum() float64 {
if m != nil && m.SampleSum != nil {
return *m.SampleSum
}
return 0
}
func (m *Summary) GetQuantile() []*Quantile {
if m != nil {
return m.Quantile
}
return nil
}
type Untyped struct {
Value *float64 `protobuf:"fixed64,1,opt,name=value" json:"value,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Untyped) Reset() { *m = Untyped{} }
func (m *Untyped) String() string { return proto.CompactTextString(m) }
func (*Untyped) ProtoMessage() {}
func (m *Untyped) GetValue() float64 {
if m != nil && m.Value != nil {
return *m.Value
}
return 0
}
type Metric struct {
Label []*LabelPair `protobuf:"bytes,1,rep,name=label" json:"label,omitempty"`
Gauge *Gauge `protobuf:"bytes,2,opt,name=gauge" json:"gauge,omitempty"`
Counter *Counter `protobuf:"bytes,3,opt,name=counter" json:"counter,omitempty"`
Summary *Summary `protobuf:"bytes,4,opt,name=summary" json:"summary,omitempty"`
Untyped *Untyped `protobuf:"bytes,5,opt,name=untyped" json:"untyped,omitempty"`
TimestampMs *int64 `protobuf:"varint,6,opt,name=timestamp_ms" json:"timestamp_ms,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Metric) Reset() { *m = Metric{} }
func (m *Metric) String() string { return proto.CompactTextString(m) }
func (*Metric) ProtoMessage() {}
func (m *Metric) GetLabel() []*LabelPair {
if m != nil {
return m.Label
}
return nil
}
func (m *Metric) GetGauge() *Gauge {
if m != nil {
return m.Gauge
}
return nil
}
func (m *Metric) GetCounter() *Counter {
if m != nil {
return m.Counter
}
return nil
}
func (m *Metric) GetSummary() *Summary {
if m != nil {
return m.Summary
}
return nil
}
func (m *Metric) GetUntyped() *Untyped {
if m != nil {
return m.Untyped
}
return nil
}
func (m *Metric) GetTimestampMs() int64 {
if m != nil && m.TimestampMs != nil {
return *m.TimestampMs
}
return 0
}
type MetricFamily struct {
Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Help *string `protobuf:"bytes,2,opt,name=help" json:"help,omitempty"`
Type *MetricType `protobuf:"varint,3,opt,name=type,enum=io.prometheus.client.MetricType" json:"type,omitempty"`
Metric []*Metric `protobuf:"bytes,4,rep,name=metric" json:"metric,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *MetricFamily) Reset() { *m = MetricFamily{} }
func (m *MetricFamily) String() string { return proto.CompactTextString(m) }
func (*MetricFamily) ProtoMessage() {}
func (m *MetricFamily) GetName() string {
if m != nil && m.Name != nil {
return *m.Name
}
return ""
}
func (m *MetricFamily) GetHelp() string {
if m != nil && m.Help != nil {
return *m.Help
}
return ""
}
func (m *MetricFamily) GetType() MetricType {
if m != nil && m.Type != nil {
return *m.Type
}
return MetricType_COUNTER
}
func (m *MetricFamily) GetMetric() []*Metric {
if m != nil {
return m.Metric
}
return nil
}
func init() {
proto.RegisterEnum("io.prometheus.client.MetricType", MetricType_name, MetricType_value)
}

View File

@@ -0,0 +1,11 @@
The Prometheus project was started by Matt T. Proud (emeritus) and
Julius Volz in 2012.
Maintainers of this repository:
* Tobias Schmidt <ts@soundcloud.com>
The following individuals have contributed code to this repository
(listed in alphabetical order):
* Tobias Schmidt <ts@soundcloud.com>

View File

@@ -0,0 +1,18 @@
# Contributing
Prometheus uses GitHub to manage reviews of pull requests.
* If you have a trivial fix or improvement, go ahead and create a pull
request, addressing (with `@...`) one or more of the maintainers
(see [AUTHORS.md](AUTHORS.md)) in the description of the pull request.
* If you plan to do something more involved, first discuss your ideas
on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers).
This will avoid unnecessary work and surely give you and us a good deal
of inspiration.
* Relevant coding style guidelines are the [Go Code Review
Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments)
and the _Formatting and style_ section of Peter Bourgon's [Go: Best
Practices for Production
Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style).

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@@ -0,0 +1,7 @@
procfs provides functions to retrieve system, kernel and process
metrics from the pseudo-filesystem proc.
Copyright 2014-2015 The Prometheus Authors
This product includes software developed at
SoundCloud Ltd. (http://soundcloud.com/).

View File

@@ -0,0 +1,11 @@
# procfs
This procfs package provides functions to retrieve system, kernel and process
metrics from the pseudo-filesystem proc.
[![GoDoc](https://godoc.org/github.com/prometheus/procfs?status.png)](https://godoc.org/github.com/prometheus/procfs)
[![Circle CI](https://circleci.com/gh/prometheus/procfs.svg?style=svg)](https://circleci.com/gh/prometheus/procfs)
# Testing
$ go test

View File

@@ -0,0 +1,45 @@
// Copyright 2014 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package procfs provides functions to retrieve system, kernel and process
// metrics from the pseudo-filesystem proc.
//
// Example:
//
// package main
//
// import (
// "fmt"
// "log"
//
// "github.com/prometheus/client_golang/procfs"
// )
//
// func main() {
// p, err := procfs.Self()
// if err != nil {
// log.Fatalf("could not get process: %s", err)
// }
//
// stat, err := p.Stat()
// if err != nil {
// log.Fatalf("could not get process stat: %s", err)
// }
//
// fmt.Printf("command: %s\n", stat.Comm)
// fmt.Printf("cpu time: %fs\n", stat.CPUTime())
// fmt.Printf("vsize: %dB\n", stat.VirtualMemory())
// fmt.Printf("rss: %dB\n", stat.ResidentMemory())
// }
//
package procfs

Binary file not shown.

View File

@@ -0,0 +1,17 @@
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 62898 62898 processes
Max open files 2048 4096 files
Max locked memory 65536 65536 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 62898 62898 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us

View File

@@ -0,0 +1 @@
26231 (vim) R 5392 7446 5392 34835 7446 4218880 32533 309516 26 82 1677 44 158 99 20 0 1 0 82375 56274944 1981 18446744073709551615 4194304 6294284 140736914091744 140736914087944 139965136429984 0 0 12288 1870679807 0 0 0 17 0 0 0 31 0 0 8391624 8481048 16420864 140736914093252 140736914093279 140736914093279 140736914096107 0

View File

@@ -0,0 +1,2 @@
1020 ((a b ) ( c d) ) R 28378 1020 28378 34842 1020 4218880 286 0 0 0 0 0 0 0 20 0 1 0 10839175 10395648 155 18446744073709551615 4194304 4238788 140736466511168 140736466511168 140609271124624 0 0 0 0 0 0 0 17 5 0 0 0 0 0 6336016 6337300 25579520 140736466515030 140736466515061 140736466515061 140736466518002 0
#!/bin/cat /proc/self/stat

View File

@@ -0,0 +1,16 @@
cpu 301854 612 111922 8979004 3552 2 3944 0 0 0
cpu0 44490 19 21045 1087069 220 1 3410 0 0 0
cpu1 47869 23 16474 1110787 591 0 46 0 0 0
cpu2 46504 36 15916 1112321 441 0 326 0 0 0
cpu3 47054 102 15683 1113230 533 0 60 0 0 0
cpu4 28413 25 10776 1140321 217 0 8 0 0 0
cpu5 29271 101 11586 1136270 672 0 30 0 0 0
cpu6 29152 36 10276 1139721 319 0 29 0 0 0
cpu7 29098 268 10164 1139282 555 0 31 0 0 0
intr 8885917 17 0 0 0 0 0 0 0 1 79281 0 0 0 0 0 0 0 231237 0 0 0 0 250586 103 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 223424 190745 13 906 1283803 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 38014093
btime 1418183276
processes 26442
procs_running 2
procs_blocked 0
softirq 5057579 250191 1481983 1647 211099 186066 0 1783454 622196 12499 508444

View File

@@ -0,0 +1,36 @@
package procfs
import (
"fmt"
"os"
"path"
)
// FS represents the pseudo-filesystem proc, which provides an interface to
// kernel data structures.
type FS string
// DefaultMountPoint is the common mount point of the proc filesystem.
const DefaultMountPoint = "/proc"
// NewFS returns a new FS mounted under the given mountPoint. It will error
// if the mount point can't be read.
func NewFS(mountPoint string) (FS, error) {
info, err := os.Stat(mountPoint)
if err != nil {
return "", fmt.Errorf("could not read %s: %s", mountPoint, err)
}
if !info.IsDir() {
return "", fmt.Errorf("mount point %s is not a directory", mountPoint)
}
return FS(mountPoint), nil
}
func (fs FS) stat(p string) (os.FileInfo, error) {
return os.Stat(path.Join(string(fs), p))
}
func (fs FS) open(p string) (*os.File, error) {
return os.Open(path.Join(string(fs), p))
}

View File

@@ -0,0 +1,13 @@
package procfs
import "testing"
func TestNewFS(t *testing.T) {
if _, err := NewFS("foobar"); err == nil {
t.Error("want NewFS to fail for non-existing mount point")
}
if _, err := NewFS("procfs.go"); err == nil {
t.Error("want NewFS to fail if mount point is not a directory")
}
}

View File

@@ -0,0 +1,149 @@
package procfs
import (
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
)
// Proc provides information about a running process.
type Proc struct {
// The process ID.
PID int
fs FS
}
// Procs represents a list of Proc structs.
type Procs []Proc
func (p Procs) Len() int { return len(p) }
func (p Procs) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p Procs) Less(i, j int) bool { return p[i].PID < p[j].PID }
// Self returns a process for the current process.
func Self() (Proc, error) {
return NewProc(os.Getpid())
}
// NewProc returns a process for the given pid under /proc.
func NewProc(pid int) (Proc, error) {
fs, err := NewFS(DefaultMountPoint)
if err != nil {
return Proc{}, err
}
return fs.NewProc(pid)
}
// AllProcs returns a list of all currently avaible processes under /proc.
func AllProcs() (Procs, error) {
fs, err := NewFS(DefaultMountPoint)
if err != nil {
return Procs{}, err
}
return fs.AllProcs()
}
// NewProc returns a process for the given pid.
func (fs FS) NewProc(pid int) (Proc, error) {
if _, err := fs.stat(strconv.Itoa(pid)); err != nil {
return Proc{}, err
}
return Proc{PID: pid, fs: fs}, nil
}
// AllProcs returns a list of all currently avaible processes.
func (fs FS) AllProcs() (Procs, error) {
d, err := fs.open("")
if err != nil {
return Procs{}, err
}
defer d.Close()
names, err := d.Readdirnames(-1)
if err != nil {
return Procs{}, fmt.Errorf("could not read %s: %s", d.Name(), err)
}
p := Procs{}
for _, n := range names {
pid, err := strconv.ParseInt(n, 10, 64)
if err != nil {
continue
}
p = append(p, Proc{PID: int(pid), fs: fs})
}
return p, nil
}
// CmdLine returns the command line of a process.
func (p Proc) CmdLine() ([]string, error) {
f, err := p.open("cmdline")
if err != nil {
return nil, err
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return strings.Split(string(data[:len(data)-1]), string(byte(0))), nil
}
// FileDescriptors returns the currently open file descriptors of a process.
func (p Proc) FileDescriptors() ([]uintptr, error) {
names, err := p.fileDescriptors()
if err != nil {
return nil, err
}
fds := make([]uintptr, len(names))
for i, n := range names {
fd, err := strconv.ParseInt(n, 10, 32)
if err != nil {
return nil, fmt.Errorf("could not parse fd %s: %s", n, err)
}
fds[i] = uintptr(fd)
}
return fds, nil
}
// FileDescriptorsLen returns the number of currently open file descriptors of
// a process.
func (p Proc) FileDescriptorsLen() (int, error) {
fds, err := p.fileDescriptors()
if err != nil {
return 0, err
}
return len(fds), nil
}
func (p Proc) fileDescriptors() ([]string, error) {
d, err := p.open("fd")
if err != nil {
return nil, err
}
defer d.Close()
names, err := d.Readdirnames(-1)
if err != nil {
return nil, fmt.Errorf("could not read %s: %s", d.Name(), err)
}
return names, nil
}
func (p Proc) open(pa string) (*os.File, error) {
return p.fs.open(path.Join(strconv.Itoa(p.PID), pa))
}

View File

@@ -0,0 +1,111 @@
package procfs
import (
"bufio"
"fmt"
"regexp"
"strconv"
)
// ProcLimits represents the soft limits for each of the process's resource
// limits.
type ProcLimits struct {
CPUTime int
FileSize int
DataSize int
StackSize int
CoreFileSize int
ResidentSet int
Processes int
OpenFiles int
LockedMemory int
AddressSpace int
FileLocks int
PendingSignals int
MsqqueueSize int
NicePriority int
RealtimePriority int
RealtimeTimeout int
}
const (
limitsFields = 3
limitsUnlimited = "unlimited"
)
var (
limitsDelimiter = regexp.MustCompile(" +")
)
// NewLimits returns the current soft limits of the process.
func (p Proc) NewLimits() (ProcLimits, error) {
f, err := p.open("limits")
if err != nil {
return ProcLimits{}, err
}
defer f.Close()
var (
l = ProcLimits{}
s = bufio.NewScanner(f)
)
for s.Scan() {
fields := limitsDelimiter.Split(s.Text(), limitsFields)
if len(fields) != limitsFields {
return ProcLimits{}, fmt.Errorf(
"couldn't parse %s line %s", f.Name(), s.Text())
}
switch fields[0] {
case "Max cpu time":
l.CPUTime, err = parseInt(fields[1])
case "Max file size":
l.FileLocks, err = parseInt(fields[1])
case "Max data size":
l.DataSize, err = parseInt(fields[1])
case "Max stack size":
l.StackSize, err = parseInt(fields[1])
case "Max core file size":
l.CoreFileSize, err = parseInt(fields[1])
case "Max resident set":
l.ResidentSet, err = parseInt(fields[1])
case "Max processes":
l.Processes, err = parseInt(fields[1])
case "Max open files":
l.OpenFiles, err = parseInt(fields[1])
case "Max locked memory":
l.LockedMemory, err = parseInt(fields[1])
case "Max address space":
l.AddressSpace, err = parseInt(fields[1])
case "Max file locks":
l.FileLocks, err = parseInt(fields[1])
case "Max pending signals":
l.PendingSignals, err = parseInt(fields[1])
case "Max msgqueue size":
l.MsqqueueSize, err = parseInt(fields[1])
case "Max nice priority":
l.NicePriority, err = parseInt(fields[1])
case "Max realtime priority":
l.RealtimePriority, err = parseInt(fields[1])
case "Max realtime timeout":
l.RealtimeTimeout, err = parseInt(fields[1])
}
if err != nil {
return ProcLimits{}, err
}
}
return l, s.Err()
}
func parseInt(s string) (int, error) {
if s == limitsUnlimited {
return -1, nil
}
i, err := strconv.ParseInt(s, 10, 32)
if err != nil {
return 0, fmt.Errorf("couldn't parse value %s: %s", s, err)
}
return int(i), nil
}

View File

@@ -0,0 +1,36 @@
package procfs
import "testing"
func TestNewLimits(t *testing.T) {
fs, err := NewFS("fixtures")
if err != nil {
t.Fatal(err)
}
p, err := fs.NewProc(26231)
if err != nil {
t.Fatal(err)
}
l, err := p.NewLimits()
if err != nil {
t.Fatal(err)
}
for _, test := range []struct {
name string
want int
got int
}{
{name: "cpu time", want: -1, got: l.CPUTime},
{name: "open files", want: 2048, got: l.OpenFiles},
{name: "msgqueue size", want: 819200, got: l.MsqqueueSize},
{name: "nice priority", want: 0, got: l.NicePriority},
{name: "address space", want: -1, got: l.AddressSpace},
} {
if test.want != test.got {
t.Errorf("want %s %d, got %d", test.name, test.want, test.got)
}
}
}

View File

@@ -0,0 +1,165 @@
package procfs
import (
"bytes"
"fmt"
"io/ioutil"
"os"
)
// #include <unistd.h>
import "C"
// ProcStat provides status information about the process,
// read from /proc/[pid]/stat.
type ProcStat struct {
// The process ID.
PID int
// The filename of the executable.
Comm string
// The process state.
State string
// The PID of the parent of this process.
PPID int
// The process group ID of the process.
PGRP int
// The session ID of the process.
Session int
// The controlling terminal of the process.
TTY int
// The ID of the foreground process group of the controlling terminal of
// the process.
TPGID int
// The kernel flags word of the process.
Flags uint
// The number of minor faults the process has made which have not required
// loading a memory page from disk.
MinFlt uint
// The number of minor faults that the process's waited-for children have
// made.
CMinFlt uint
// The number of major faults the process has made which have required
// loading a memory page from disk.
MajFlt uint
// The number of major faults that the process's waited-for children have
// made.
CMajFlt uint
// Amount of time that this process has been scheduled in user mode,
// measured in clock ticks.
UTime uint
// Amount of time that this process has been scheduled in kernel mode,
// measured in clock ticks.
STime uint
// Amount of time that this process's waited-for children have been
// scheduled in user mode, measured in clock ticks.
CUTime uint
// Amount of time that this process's waited-for children have been
// scheduled in kernel mode, measured in clock ticks.
CSTime uint
// For processes running a real-time scheduling policy, this is the negated
// scheduling priority, minus one.
Priority int
// The nice value, a value in the range 19 (low priority) to -20 (high
// priority).
Nice int
// Number of threads in this process.
NumThreads int
// The time the process started after system boot, the value is expressed
// in clock ticks.
Starttime uint64
// Virtual memory size in bytes.
VSize int
// Resident set size in pages.
RSS int
fs FS
}
// NewStat returns the current status information of the process.
func (p Proc) NewStat() (ProcStat, error) {
f, err := p.open("stat")
if err != nil {
return ProcStat{}, err
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return ProcStat{}, err
}
var (
ignore int
s = ProcStat{PID: p.PID, fs: p.fs}
l = bytes.Index(data, []byte("("))
r = bytes.LastIndex(data, []byte(")"))
)
if l < 0 || r < 0 {
return ProcStat{}, fmt.Errorf(
"unexpected format, couldn't extract comm: %s",
data,
)
}
s.Comm = string(data[l+1 : r])
_, err = fmt.Fscan(
bytes.NewBuffer(data[r+2:]),
&s.State,
&s.PPID,
&s.PGRP,
&s.Session,
&s.TTY,
&s.TPGID,
&s.Flags,
&s.MinFlt,
&s.CMinFlt,
&s.MajFlt,
&s.CMajFlt,
&s.UTime,
&s.STime,
&s.CUTime,
&s.CSTime,
&s.Priority,
&s.Nice,
&s.NumThreads,
&ignore,
&s.Starttime,
&s.VSize,
&s.RSS,
)
if err != nil {
return ProcStat{}, err
}
return s, nil
}
// VirtualMemory returns the virtual memory size in bytes.
func (s ProcStat) VirtualMemory() int {
return s.VSize
}
// ResidentMemory returns the resident memory size in bytes.
func (s ProcStat) ResidentMemory() int {
return s.RSS * os.Getpagesize()
}
// StartTime returns the unix timestamp of the process in seconds.
func (s ProcStat) StartTime() (float64, error) {
stat, err := s.fs.NewStat()
if err != nil {
return 0, err
}
return float64(stat.BootTime) + (float64(s.Starttime) / ticks()), nil
}
// CPUTime returns the total CPU user and system time in seconds.
func (s ProcStat) CPUTime() float64 {
return float64(s.UTime+s.STime) / ticks()
}
func ticks() float64 {
return float64(C.sysconf(C._SC_CLK_TCK)) // most likely 100
}

View File

@@ -0,0 +1,112 @@
package procfs
import "testing"
func TestProcStat(t *testing.T) {
fs, err := NewFS("fixtures")
if err != nil {
t.Fatal(err)
}
p, err := fs.NewProc(26231)
if err != nil {
t.Fatal(err)
}
s, err := p.NewStat()
if err != nil {
t.Fatal(err)
}
for _, test := range []struct {
name string
want int
got int
}{
{name: "pid", want: 26231, got: s.PID},
{name: "user time", want: 1677, got: int(s.UTime)},
{name: "system time", want: 44, got: int(s.STime)},
{name: "start time", want: 82375, got: int(s.Starttime)},
{name: "virtual memory size", want: 56274944, got: s.VSize},
{name: "resident set size", want: 1981, got: s.RSS},
} {
if test.want != test.got {
t.Errorf("want %s %d, got %d", test.name, test.want, test.got)
}
}
}
func TestProcStatComm(t *testing.T) {
s1, err := testProcStat(26231)
if err != nil {
t.Fatal(err)
}
if want, got := "vim", s1.Comm; want != got {
t.Errorf("want comm %s, got %s", want, got)
}
s2, err := testProcStat(584)
if err != nil {
t.Fatal(err)
}
if want, got := "(a b ) ( c d) ", s2.Comm; want != got {
t.Errorf("want comm %s, got %s", want, got)
}
}
func TestProcStatVirtualMemory(t *testing.T) {
s, err := testProcStat(26231)
if err != nil {
t.Fatal(err)
}
if want, got := 56274944, s.VirtualMemory(); want != got {
t.Errorf("want virtual memory %d, got %d", want, got)
}
}
func TestProcStatResidentMemory(t *testing.T) {
s, err := testProcStat(26231)
if err != nil {
t.Fatal(err)
}
if want, got := 1981*4096, s.ResidentMemory(); want != got {
t.Errorf("want resident memory %d, got %d", want, got)
}
}
func TestProcStatStartTime(t *testing.T) {
s, err := testProcStat(26231)
if err != nil {
t.Fatal(err)
}
time, err := s.StartTime()
if err != nil {
t.Fatal(err)
}
if want, got := 1418184099.75, time; want != got {
t.Errorf("want start time %f, got %f", want, got)
}
}
func TestProcStatCPUTime(t *testing.T) {
s, err := testProcStat(26231)
if err != nil {
t.Fatal(err)
}
if want, got := 17.21, s.CPUTime(); want != got {
t.Errorf("want cpu time %f, got %f", want, got)
}
}
func testProcStat(pid int) (ProcStat, error) {
p, err := testProcess(pid)
if err != nil {
return ProcStat{}, err
}
return p.NewStat()
}

View File

@@ -0,0 +1,123 @@
package procfs
import (
"os"
"reflect"
"sort"
"testing"
)
func TestSelf(t *testing.T) {
p1, err := NewProc(os.Getpid())
if err != nil {
t.Fatal(err)
}
p2, err := Self()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(p1, p2) {
t.Errorf("want process %v to equal %v", p1, p2)
}
}
func TestAllProcs(t *testing.T) {
fs, err := NewFS("fixtures")
if err != nil {
t.Fatal(err)
}
procs, err := fs.AllProcs()
if err != nil {
t.Fatal(err)
}
sort.Sort(procs)
for i, p := range []*Proc{{PID: 584}, {PID: 26231}} {
if want, got := p.PID, procs[i].PID; want != got {
t.Errorf("want processes %d, got %d", want, got)
}
}
}
func TestCmdLine(t *testing.T) {
p1, err := testProcess(26231)
if err != nil {
t.Fatal(err)
}
c, err := p1.CmdLine()
if err != nil {
t.Fatal(err)
}
if want := []string{"vim", "test.go", "+10"}; !reflect.DeepEqual(want, c) {
t.Errorf("want cmdline %v, got %v", want, c)
}
}
func TestFileDescriptors(t *testing.T) {
p1, err := testProcess(26231)
if err != nil {
t.Fatal(err)
}
fds, err := p1.FileDescriptors()
if err != nil {
t.Fatal(err)
}
sort.Sort(byUintptr(fds))
if want := []uintptr{0, 1, 2, 3, 4}; !reflect.DeepEqual(want, fds) {
t.Errorf("want fds %v, got %v", want, fds)
}
p2, err := Self()
if err != nil {
t.Fatal(err)
}
fdsBefore, err := p2.FileDescriptors()
if err != nil {
t.Fatal(err)
}
s, err := os.Open("fixtures")
if err != nil {
t.Fatal(err)
}
defer s.Close()
fdsAfter, err := p2.FileDescriptors()
if err != nil {
t.Fatal(err)
}
if len(fdsBefore)+1 != len(fdsAfter) {
t.Errorf("want fds %v+1 to equal %v", fdsBefore, fdsAfter)
}
}
func TestFileDescriptorsLen(t *testing.T) {
p1, err := testProcess(26231)
if err != nil {
t.Fatal(err)
}
l, err := p1.FileDescriptorsLen()
if err != nil {
t.Fatal(err)
}
if want, got := 5, l; want != got {
t.Errorf("want fds %d, got %d", want, got)
}
}
func testProcess(pid int) (Proc, error) {
fs, err := NewFS("fixtures")
if err != nil {
return Proc{}, err
}
return fs.NewProc(pid)
}
type byUintptr []uintptr
func (a byUintptr) Len() int { return len(a) }
func (a byUintptr) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byUintptr) Less(i, j int) bool { return a[i] < a[j] }

Some files were not shown because too many files have changed in this diff Show More