Complete rewrite of the exposition library.
This rewrite had may backs and forths. In my git repository, it consists of 35 commits which I cannot group or merge into reasonable review buckets. Gerrit breaks fundamental git semantics, so I have to squash the 35 commits into one for the review. I'll push this not with refs/for/master, but with refs/for/next so that we can transition after submission in a controlled fashion. For the review, I recommend to start with looking at godoc and in particular the many examples. After that, continue with a line-by-line detailed review. (The big picture is hopefully as expected after wrapping up the discussion earlier.) Change-Id: Ib38cc46493a5139ca29d84020650929d94cac850
This commit is contained in:
parent
d3ebb29141
commit
5d40912fd2
|
@ -1,7 +1,7 @@
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.1
|
- 1.2.1
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- make -f Makefile
|
- make -f Makefile
|
||||||
|
|
217
LICENSE
217
LICENSE
|
@ -1,22 +1,201 @@
|
||||||
Copyright (c) 2013, Prometheus Team
|
Apache License
|
||||||
All rights reserved.
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this
|
1. Definitions.
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
2. 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.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
the copyright owner that is granting the License.
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
other entities that control, are controlled by, or are under common
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
control with that entity. For the purposes of this definition,
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
"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 2013 Prometheus Team
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
31
Makefile
31
Makefile
|
@ -14,19 +14,29 @@
|
||||||
OS = $(shell uname)
|
OS = $(shell uname)
|
||||||
ARCH = $(shell uname -m)
|
ARCH = $(shell uname -m)
|
||||||
|
|
||||||
|
MAC_OS_X_VERSION ?= 10.8
|
||||||
|
|
||||||
BUILD_PATH = $(PWD)/.build
|
BUILD_PATH = $(PWD)/.build
|
||||||
|
|
||||||
export GO_VERSION = 1.1
|
export GO_VERSION = 1.2.1
|
||||||
export GOOS = $(subst Darwin,darwin,$(subst Linux,linux,$(OS)))
|
export GOOS = $(subst Darwin,darwin,$(subst Linux,linux,$(OS)))
|
||||||
|
|
||||||
|
ifeq ($(GOOS),darwin)
|
||||||
|
RELEASE_SUFFIX ?= -osx$(MAC_OS_X_VERSION)
|
||||||
|
else
|
||||||
|
RELEASE_SUFFIX ?=
|
||||||
|
endif
|
||||||
|
|
||||||
export GOARCH = $(subst x86_64,amd64,$(ARCH))
|
export GOARCH = $(subst x86_64,amd64,$(ARCH))
|
||||||
export GOPKG = go$(GO_VERSION).$(GOOS)-$(GOARCH).tar.gz
|
export GOPKG = go$(GO_VERSION).$(GOOS)-$(GOARCH)$(RELEASE_SUFFIX).tar.gz
|
||||||
export GOROOT = $(BUILD_PATH)/root/go
|
export GOROOT = $(BUILD_PATH)/root/go
|
||||||
export GOPATH = $(BUILD_PATH)/root/gopath
|
export GOPATH = $(BUILD_PATH)/root/gopath
|
||||||
export GOCC = $(GOROOT)/bin/go
|
export GOCC = $(GOROOT)/bin/go
|
||||||
export TMPDIR = /tmp
|
export TMPDIR = /tmp
|
||||||
export GOENV = TMPDIR=$(TMPDIR) GOROOT=$(GOROOT) GOPATH=$(GOPATH)
|
export GOENV = TMPDIR=$(TMPDIR) GOROOT=$(GOROOT) GOPATH=$(GOPATH)
|
||||||
export GO = $(GOENV) $(GOCC)
|
export GO = $(GOENV) $(GOCC)
|
||||||
export GOFMT = $(GOROOT)/bin/gofmt
|
export GOFMT = $(GOROOT)/bin/gofmt
|
||||||
|
export GODOC = $(GOENV) $(GOROOT)/bin/godoc
|
||||||
|
|
||||||
BENCHMARK_FILTER ?= .
|
BENCHMARK_FILTER ?= .
|
||||||
|
|
||||||
|
@ -54,11 +64,10 @@ $(GOCC): $(BUILD_PATH)/root $(BUILD_PATH)/cache/$(GOPKG)
|
||||||
touch $@
|
touch $@
|
||||||
|
|
||||||
build: source_path dependencies
|
build: source_path dependencies
|
||||||
$(MAKE) -C prometheus build
|
$(GO) build ./...
|
||||||
$(MAKE) -C examples build
|
|
||||||
|
|
||||||
dependencies: source_path $(GOCC)
|
dependencies: source_path $(GOCC)
|
||||||
$(GO) get github.com/matttproud/gocheck
|
$(GO) get -d -t ./...
|
||||||
|
|
||||||
test: build
|
test: build
|
||||||
$(GO) test ./...
|
$(GO) test ./...
|
||||||
|
@ -67,14 +76,13 @@ benchmark: build
|
||||||
$(GO) test -benchmem -test.bench="$(BENCHMARK_FILTER)" ./...
|
$(GO) test -benchmem -test.bench="$(BENCHMARK_FILTER)" ./...
|
||||||
|
|
||||||
advice: test
|
advice: test
|
||||||
$(MAKE) -C prometheus advice
|
$(GO) vet ./...
|
||||||
$(MAKE) -C examples advice
|
|
||||||
|
|
||||||
format:
|
format:
|
||||||
find . -iname '*.go' | grep -v './.build/' | xargs -n1 -P1 $(GOFMT) -w -s=true
|
find . -iname '*.go' | grep -v './.build/' | xargs -n1 -P1 $(GOFMT) -w -s=true
|
||||||
|
|
||||||
search_index:
|
search_index:
|
||||||
godoc -index -write_index -index_files='search_index'
|
$(GODOC) -index -write_index -index_files='search_index'
|
||||||
|
|
||||||
# source_path is responsible for ensuring that the builder has not done anything
|
# source_path is responsible for ensuring that the builder has not done anything
|
||||||
# stupid like working on Prometheus outside of ${GOPATH}.
|
# stupid like working on Prometheus outside of ${GOPATH}.
|
||||||
|
@ -83,10 +91,9 @@ source_path:
|
||||||
[ -d "$(FULL_GOPATH)" ]
|
[ -d "$(FULL_GOPATH)" ]
|
||||||
|
|
||||||
documentation: search_index
|
documentation: search_index
|
||||||
godoc -http=:6060 -index -index_files='search_index'
|
$(GODOC) -http=:6060 -index -index_files='search_index'
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(MAKE) -C examples clean
|
|
||||||
rm -rf $(MAKE_ARTIFACTS)
|
rm -rf $(MAKE_ARTIFACTS)
|
||||||
find . -iname '*~' -exec rm -f '{}' ';'
|
find . -iname '*~' -exec rm -f '{}' ';'
|
||||||
find . -iname '*#' -exec rm -f '{}' ';'
|
find . -iname '*#' -exec rm -f '{}' ';'
|
||||||
|
|
96
README.md
96
README.md
|
@ -1,96 +1,30 @@
|
||||||
# Overview
|
# Overview
|
||||||
These [Go](http://golang.org) packages are an extraction of pieces of
|
This is the [Prometheus](http://www.prometheus.io)
|
||||||
instrumentation code I whipped-up for a personal project that a friend of mine
|
[Go](http://golang.org) client library. It provides several distinct
|
||||||
and I are working on. We were in need for some rudimentary statistics to
|
functions, and there is separate documentation for each respective
|
||||||
observe behaviors of the server's various components, so this was written.
|
component. You will want to select the appropriate topic below to
|
||||||
|
continue your journey:
|
||||||
|
|
||||||
The code here is not a verbatim copy thereof but rather a thoughtful
|
1. See the [exposition library](prometheus/README.md) if you want to
|
||||||
re-implementation should other folks need to consume and analyze such telemetry.
|
export metrics to a Prometheus server or pushgateway
|
||||||
|
|
||||||
N.B. --- I have spent a bit of time working through the model in my head and
|
2. See the [consumption library](extraction/README.md) if you want to
|
||||||
probably haven't elucidated my ideas as clearly as I need to. If you examine
|
process metrics exported by a Prometheus client. (The Prometheus server
|
||||||
examples/{simple,uniform_random}/main.go and registry.go, you'll find several
|
is using that library.)
|
||||||
examples of what types of potential instrumentation use cases this package
|
|
||||||
addresses. There are probably numerous Go language idiomatic changes that need
|
|
||||||
to be made, but this task has been deferred for now.
|
|
||||||
|
|
||||||
# Continuous Integration
|
[![GoDoc](https://godoc.org/github.com/prometheus/client_golang?status.png)](https://godoc.org/github.com/prometheus/client_golang)
|
||||||
[![Build Status](https://secure.travis-ci.org/prometheus/client_golang.png?branch=master)](http://travis-ci.org/prometheus/client_golang)
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
Please read the [generated documentation](http://go.pkgdoc.org/github.com/prometheus/client_golang)
|
|
||||||
for the project's documentation from source code.
|
|
||||||
|
|
||||||
# Basic Overview
|
|
||||||
## Metrics
|
|
||||||
A metric is a measurement mechanism.
|
|
||||||
|
|
||||||
### Gauge
|
|
||||||
A _Gauge_ is a metric that exposes merely an instantaneous value or some
|
|
||||||
snapshot thereof.
|
|
||||||
|
|
||||||
### Counter
|
|
||||||
A _Counter_ is a metric that exposes merely a sum or tally of things.
|
|
||||||
|
|
||||||
### Histogram
|
|
||||||
A _Histogram_ is a metric that captures events or samples into _Buckets_. It
|
|
||||||
exposes its values via percentile estimations.
|
|
||||||
|
|
||||||
#### Buckets
|
|
||||||
A _Bucket_ is a generic container that collects samples and their values. It
|
|
||||||
prescribes no behavior on its own aside from merely accepting a value,
|
|
||||||
leaving it up to the concrete implementation to what to do with the injected
|
|
||||||
values.
|
|
||||||
|
|
||||||
##### Accumulating Bucket
|
|
||||||
An _Accumulating Bucket_ is a _Bucket_ that appends the new sample to a queue
|
|
||||||
such that the eldest values are evicted according to a given policy.
|
|
||||||
|
|
||||||
###### Eviction Policies
|
|
||||||
Once an _Accumulating Bucket_ reaches capacity, its eviction policy is invoked.
|
|
||||||
This reaps the oldest N objects subject to certain behavior.
|
|
||||||
|
|
||||||
####### Remove Oldest
|
|
||||||
This merely removes the oldest N items without performing some aggregation
|
|
||||||
replacement operation on them.
|
|
||||||
|
|
||||||
####### Aggregate Oldest
|
|
||||||
This removes the oldest N items while performing some summary aggregation
|
|
||||||
operation thereupon, which is then appended to the list in the former values'
|
|
||||||
place.
|
|
||||||
|
|
||||||
##### Tallying Bucket
|
|
||||||
A _Tallying Bucket_ differs from an _Accumulating Bucket_ in that it never
|
|
||||||
stores any of the values emitted into it but rather exposes a simplied summary
|
|
||||||
representation thereof. For instance, if a values therein is requested,
|
|
||||||
it may situationally emit a minimum, maximum, an average, or any other
|
|
||||||
reduction mechanism requested.
|
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
|
|
||||||
* The source code is periodically indexed: [Go Exposition Client](http://godoc.org/github.com/prometheus/client_golang).
|
* The source code is periodically indexed: [Go Exposition Client](http://godoc.org/github.com/prometheus/client_golang).
|
||||||
* All of the core developers are accessible via the [Prometheus Developers Mailinglist](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers).
|
* All of the core developers are accessible via the [Prometheus Developers Mailinglist](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers).
|
||||||
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
This package employs [gocheck](http://labix.org/gocheck) for testing. Please
|
|
||||||
ensure that all tests pass by running the following from the project root:
|
|
||||||
|
|
||||||
$ go test ./...
|
$ go test ./...
|
||||||
|
|
||||||
The use of gocheck is summarily being phased out; however, old tests that use it
|
# Continuous Integration
|
||||||
still exist.
|
[![Build Status](https://secure.travis-ci.org/prometheus/client_golang.png?branch=master)]()
|
||||||
|
|
||||||
# Contributing
|
## Contributing
|
||||||
|
|
||||||
Same as for the `prometheus/prometheus` repository, we are using
|
See the contributing guidelines for the [Prometheus server](https://github.com/prometheus/prometheus/blob/master/CONTRIBUTING.md).
|
||||||
Gerrit to manage reviews of pull-requests for this repository. See
|
|
||||||
[`CONTRIBUTING.md`](https://github.com/prometheus/prometheus/blob/master/CONTRIBUTING.md)
|
|
||||||
in the `prometheus/prometheus` repository for details (but replace the
|
|
||||||
`prometheus` repository name by `client_golang`).
|
|
||||||
|
|
||||||
Please try to avoid warnings flagged by [`go
|
|
||||||
vet`](https://godoc.org/code.google.com/p/go.tools/cmd/vet) and by
|
|
||||||
[`golint`](https://github.com/golang/lint), and pay attention to 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).
|
|
||||||
|
|
2
TODO
2
TODO
|
@ -1,2 +0,0 @@
|
||||||
- Validate repository for Go code fluency and idiomatic adherence.
|
|
||||||
- Evaluate using atomic types versus locks.
|
|
|
@ -1,40 +0,0 @@
|
||||||
Please try to observe the following rules when naming metrics:
|
|
||||||
|
|
||||||
- Use underbars "_" to separate words.
|
|
||||||
|
|
||||||
- Have the metric name start from generality and work toward specificity
|
|
||||||
toward the end. For example, when working with multiple caching subsystems,
|
|
||||||
consider using the following structure "cache" + "user_credentials" →
|
|
||||||
"cache_user_credentials" and "cache" + "value_transformations" →
|
|
||||||
"cache_value_transformations".
|
|
||||||
|
|
||||||
- Have whatever is being measured follow the system and subsystem names cited
|
|
||||||
supra. For instance, with "insertions", "deletions", "evictions",
|
|
||||||
"replacements" of the above cache, they should be named as
|
|
||||||
"cache_user_credentials_insertions" and "cache_user_credentials_deletions"
|
|
||||||
and "cache_user_credentials_deletions" and
|
|
||||||
"cache_user_credentials_evictions".
|
|
||||||
|
|
||||||
- If what is being measured has a standardized unit around it, consider
|
|
||||||
providing a unit for it.
|
|
||||||
|
|
||||||
- Consider adding an additional suffix that designates what the value
|
|
||||||
represents such as a "total" or "size"---e.g.,
|
|
||||||
"cache_user_credentials_size_kb" or
|
|
||||||
"cache_user_credentials_insertions_total".
|
|
||||||
|
|
||||||
- Give heed to how future-proof the names are. Things may depend on these
|
|
||||||
names; and as your service evolves, the calculated values may take on
|
|
||||||
different meanings, which can be difficult to reflect if deployed code
|
|
||||||
depends on antique names.
|
|
||||||
|
|
||||||
Further considerations:
|
|
||||||
|
|
||||||
- The Registry's exposition mechanism is not backed by authorization and
|
|
||||||
authentication. This is something that will need to be addressed for
|
|
||||||
production services that are directly exposed to the outside world.
|
|
||||||
|
|
||||||
- Engage in as little in-process processing of values as possible. The job
|
|
||||||
of processing and aggregation of these values belongs in a separate
|
|
||||||
post-processing job. The same goes for archiving. I will need to evaluate
|
|
||||||
hooks into something like OpenTSBD.
|
|
|
@ -1,36 +0,0 @@
|
||||||
# Copyright 2013 Prometheus Team
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
all: test
|
|
||||||
|
|
||||||
build:
|
|
||||||
$(MAKE) -C delegator build
|
|
||||||
$(MAKE) -C random build
|
|
||||||
$(MAKE) -C simple build
|
|
||||||
|
|
||||||
test: build
|
|
||||||
$(MAKE) -C delegator test
|
|
||||||
$(MAKE) -C random test
|
|
||||||
$(MAKE) -C simple test
|
|
||||||
|
|
||||||
advice: test
|
|
||||||
$(MAKE) -C delegator advice
|
|
||||||
$(MAKE) -C random advice
|
|
||||||
$(MAKE) -C simple advice
|
|
||||||
|
|
||||||
clean:
|
|
||||||
$(MAKE) -C delegator clean
|
|
||||||
$(MAKE) -C random clean
|
|
||||||
$(MAKE) -C simple clean
|
|
||||||
|
|
||||||
.PHONY: advice build clean test
|
|
|
@ -1 +0,0 @@
|
||||||
delegator
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Copyright 2013 Prometheus Team
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
MAKE_ARTIFACTS = delegator
|
|
||||||
|
|
||||||
all: test
|
|
||||||
|
|
||||||
build: delegator
|
|
||||||
|
|
||||||
delegator:
|
|
||||||
$(GO) build .
|
|
||||||
|
|
||||||
test: build
|
|
||||||
$(GO) test . $(GO_TEST_FLAGS)
|
|
||||||
|
|
||||||
advice:
|
|
||||||
$(GO) tool vet .
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(MAKE_ARTIFACTS)
|
|
||||||
|
|
||||||
.PHONY: advice build clean test
|
|
|
@ -1,54 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// This skeletal example of the telemetry library is provided to demonstrate the
|
|
||||||
// use of boilerplate HTTP delegation telemetry methods.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/exp"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// helloHandler demonstrates the DefaultCoarseMux's ability to sniff a
|
|
||||||
// http.ResponseWriter (specifically http.response) implicit setting of
|
|
||||||
// a response code.
|
|
||||||
func helloHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("Hello, hello, hello..."))
|
|
||||||
}
|
|
||||||
|
|
||||||
// goodbyeHandler demonstrates the DefaultCoarseMux's ability to sniff an
|
|
||||||
// http.ResponseWriter (specifically http.response) explicit setting of
|
|
||||||
// a response code.
|
|
||||||
func goodbyeHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusGone)
|
|
||||||
w.Write([]byte("... and now for the big goodbye!"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// teapotHandler demonstrates the DefaultCoarseMux's ability to sniff an
|
|
||||||
// http.ResponseWriter (specifically http.response) explicit setting of
|
|
||||||
// a response code for pure comedic value.
|
|
||||||
func teapotHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusTeapot)
|
|
||||||
w.Write([]byte("Short and stout..."))
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
listeningAddress = flag.String("listeningAddress", ":8080", "The address to listen to requests on.")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
exp.HandleFunc("/hello", helloHandler)
|
|
||||||
exp.HandleFunc("/goodbye", goodbyeHandler)
|
|
||||||
exp.HandleFunc("/teapot", teapotHandler)
|
|
||||||
exp.Handle(prometheus.ExpositionResource, prometheus.DefaultHandler)
|
|
||||||
|
|
||||||
http.ListenAndServe(*listeningAddress, exp.DefaultCoarseMux)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
random
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Copyright 2013 Prometheus Team
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
MAKE_ARTIFACTS = random
|
|
||||||
|
|
||||||
all: test
|
|
||||||
|
|
||||||
build: random
|
|
||||||
|
|
||||||
random:
|
|
||||||
$(GO) build .
|
|
||||||
|
|
||||||
test: build
|
|
||||||
$(GO) test . $(GO_TEST_FLAGS)
|
|
||||||
|
|
||||||
advice:
|
|
||||||
$(GO) tool vet .
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(MAKE_ARTIFACTS)
|
|
||||||
|
|
||||||
.PHONY: advice clean build test
|
|
|
@ -1,81 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// A simple example of how to use this instrumentation framework in the context
|
|
||||||
// of having something that emits values into its collectors.
|
|
||||||
//
|
|
||||||
// The emitted values correspond to uniform, normal, and exponential
|
|
||||||
// distributions.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
barDomain = flag.Float64("random.fooDomain", 200, "The domain for the random parameter foo.")
|
|
||||||
barMean = flag.Float64("random.barDomain", 10, "The domain for the random parameter bar.")
|
|
||||||
fooDomain = flag.Float64("random.barMean", 100, "The mean for the random parameter bar.")
|
|
||||||
|
|
||||||
// Create a histogram to track fictitious interservice RPC latency for three
|
|
||||||
// distinct services.
|
|
||||||
rpcLatency = prometheus.NewHistogram(&prometheus.HistogramSpecification{
|
|
||||||
// Four distinct histogram buckets for values:
|
|
||||||
// - equally-sized,
|
|
||||||
// - 0 to 50, 50 to 100, 100 to 150, and 150 to 200.
|
|
||||||
Starts: prometheus.EquallySizedBucketsFor(0, 200, 4),
|
|
||||||
// Create histogram buckets using an accumulating bucket, a bucket that
|
|
||||||
// holds sample values subject to an eviction policy:
|
|
||||||
// - 50 elements are allowed per bucket.
|
|
||||||
// - Once 50 have been reached, the bucket empties 10 elements, averages the
|
|
||||||
// evicted elements, and re-appends that back to the bucket.
|
|
||||||
BucketBuilder: prometheus.AccumulatingBucketBuilder(prometheus.EvictAndReplaceWith(10, prometheus.AverageReducer), 50),
|
|
||||||
// The histogram reports percentiles 1, 5, 50, 90, and 99.
|
|
||||||
ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.90, 0.99},
|
|
||||||
})
|
|
||||||
|
|
||||||
rpcCalls = prometheus.NewCounter()
|
|
||||||
|
|
||||||
// If for whatever reason you are resistant to the idea of having a static
|
|
||||||
// registry for metrics, which is a really bad idea when using Prometheus-
|
|
||||||
// enabled library code, you can create your own.
|
|
||||||
customRegistry = prometheus.NewRegistry()
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
rpcLatency.Add(map[string]string{"service": "foo"}, rand.Float64()**fooDomain)
|
|
||||||
rpcCalls.Increment(map[string]string{"service": "foo"})
|
|
||||||
|
|
||||||
rpcLatency.Add(map[string]string{"service": "bar"}, (rand.NormFloat64()**barDomain)+*barMean)
|
|
||||||
rpcCalls.Increment(map[string]string{"service": "bar"})
|
|
||||||
|
|
||||||
rpcLatency.Add(map[string]string{"service": "zed"}, rand.ExpFloat64())
|
|
||||||
rpcCalls.Increment(map[string]string{"service": "zed"})
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
http.Handle(prometheus.ExpositionResource, customRegistry.Handler())
|
|
||||||
http.ListenAndServe(*listeningAddress, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
customRegistry.Register("rpc_latency_microseconds", "RPC latency.", prometheus.NilLabels, rpcLatency)
|
|
||||||
customRegistry.Register("rpc_calls_total", "RPC calls.", prometheus.NilLabels, rpcCalls)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
listeningAddress = flag.String("listeningAddress", ":8080", "The address to listen to requests on.")
|
|
||||||
)
|
|
|
@ -1 +0,0 @@
|
||||||
simple
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Copyright 2013 Prometheus Team
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
MAKE_ARTIFACTS = simple
|
|
||||||
|
|
||||||
all: test
|
|
||||||
|
|
||||||
build: simple
|
|
||||||
|
|
||||||
simple:
|
|
||||||
$(GO) build .
|
|
||||||
|
|
||||||
test: build
|
|
||||||
$(GO) test . $(GO_TEST_FLAGS)
|
|
||||||
|
|
||||||
advice:
|
|
||||||
$(GO) tool vet .
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(MAKE_ARTIFACTS)
|
|
||||||
|
|
||||||
.PHONY: advice build clean test
|
|
|
@ -1,26 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// A simple skeletal example of how this instrumentation framework is registered
|
|
||||||
// and invoked. Literally, this is the bare bones.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
http.Handle(prometheus.ExpositionResource, prometheus.DefaultHandler)
|
|
||||||
http.ListenAndServe(*listeningAddress, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
listeningAddress = flag.String("listeningAddress", ":8080", "The address to listen to requests on.")
|
|
||||||
)
|
|
|
@ -17,11 +17,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testDiscriminatorHTTPHeader(t test.Tester) {
|
func testDiscriminatorHTTPHeader(t testing.TB) {
|
||||||
var scenarios = []struct {
|
var scenarios = []struct {
|
||||||
input map[string]string
|
input map[string]string
|
||||||
output Processor
|
output Processor
|
||||||
|
|
|
@ -20,8 +20,9 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/utility/test"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/model"
|
"github.com/prometheus/client_golang/model"
|
||||||
"github.com/prometheus/client_golang/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var test001Time = model.Now()
|
var test001Time = model.Now()
|
||||||
|
@ -37,7 +38,7 @@ func (s *testProcessor001ProcessScenario) Ingest(r *Result) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *testProcessor001ProcessScenario) test(t test.Tester, set int) {
|
func (s *testProcessor001ProcessScenario) test(t testing.TB, set int) {
|
||||||
reader, err := os.Open(path.Join("fixtures", s.in))
|
reader, err := os.Open(path.Join("fixtures", s.in))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%d. couldn't open scenario input file %s: %s", set, s.in, err)
|
t.Fatalf("%d. couldn't open scenario input file %s: %s", set, s.in, err)
|
||||||
|
@ -64,7 +65,7 @@ func (s *testProcessor001ProcessScenario) test(t test.Tester, set int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testProcessor001Process(t test.Tester) {
|
func testProcessor001Process(t testing.TB) {
|
||||||
var scenarios = []testProcessor001ProcessScenario{
|
var scenarios = []testProcessor001ProcessScenario{
|
||||||
{
|
{
|
||||||
in: "empty.json",
|
in: "empty.json",
|
||||||
|
|
|
@ -21,8 +21,9 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/utility/test"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/model"
|
"github.com/prometheus/client_golang/model"
|
||||||
"github.com/prometheus/client_golang/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var test002Time = model.Now()
|
var test002Time = model.Now()
|
||||||
|
@ -38,7 +39,7 @@ func (s *testProcessor002ProcessScenario) Ingest(r *Result) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *testProcessor002ProcessScenario) test(t test.Tester, set int) {
|
func (s *testProcessor002ProcessScenario) test(t testing.TB, set int) {
|
||||||
reader, err := os.Open(path.Join("fixtures", s.in))
|
reader, err := os.Open(path.Join("fixtures", s.in))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%d. couldn't open scenario input file %s: %s", set, s.in, err)
|
t.Fatalf("%d. couldn't open scenario input file %s: %s", set, s.in, err)
|
||||||
|
@ -65,7 +66,7 @@ func (s *testProcessor002ProcessScenario) test(t test.Tester, set int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testProcessor002Process(t test.Tester) {
|
func testProcessor002Process(t testing.TB) {
|
||||||
var scenarios = []testProcessor002ProcessScenario{
|
var scenarios = []testProcessor002ProcessScenario{
|
||||||
{
|
{
|
||||||
in: "empty.json",
|
in: "empty.json",
|
||||||
|
|
|
@ -16,11 +16,9 @@ package model
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testLabelNames(t test.Tester) {
|
func testLabelNames(t testing.TB) {
|
||||||
var scenarios = []struct {
|
var scenarios = []struct {
|
||||||
in LabelNames
|
in LabelNames
|
||||||
out LabelNames
|
out LabelNames
|
||||||
|
|
|
@ -16,11 +16,9 @@ package model
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testLabelValues(t test.Tester) {
|
func testLabelValues(t testing.TB) {
|
||||||
var scenarios = []struct {
|
var scenarios = []struct {
|
||||||
in LabelValues
|
in LabelValues
|
||||||
out LabelValues
|
out LabelValues
|
||||||
|
|
|
@ -13,13 +13,9 @@
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/test"
|
func testMetric(t testing.TB) {
|
||||||
)
|
|
||||||
|
|
||||||
func testMetric(t test.Tester) {
|
|
||||||
var scenarios = []struct {
|
var scenarios = []struct {
|
||||||
input map[string]string
|
input map[string]string
|
||||||
hash uint64
|
hash uint64
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright 2014 Prometheus Team
|
||||||
// All rights reserved.
|
// 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
|
||||||
//
|
//
|
||||||
// Use of this source code is governed by a BSD-style
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
// license that can be found in the LICENSE file.
|
//
|
||||||
|
// 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
|
package model
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright 2014 Prometheus Team
|
||||||
// All rights reserved.
|
// 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
|
||||||
//
|
//
|
||||||
// Use of this source code is governed by a BSD-style
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
// license that can be found in the LICENSE file.
|
//
|
||||||
|
// 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
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testLabelsToSignature(t test.Tester) {
|
func testLabelsToSignature(t testing.TB) {
|
||||||
var scenarios = []struct {
|
var scenarios = []struct {
|
||||||
in map[string]string
|
in map[string]string
|
||||||
out uint64
|
out uint64
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
# Copyright 2013 Prometheus Team
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
all: test
|
|
||||||
|
|
||||||
build: dependencies
|
|
||||||
$(GO) build ./...
|
|
||||||
|
|
||||||
dependencies: $(GOCC)
|
|
||||||
$(GO) get -d
|
|
||||||
|
|
||||||
test: build
|
|
||||||
$(GO) test ./... $(GO_TEST_FLAGS)
|
|
||||||
|
|
||||||
advice:
|
|
||||||
$(GO) tool vet .
|
|
||||||
|
|
||||||
.PHONY: advice build dependencies test
|
|
|
@ -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)
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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"
|
|
||||||
"container/heap"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AccumulatingBucket struct {
|
|
||||||
elements priorityQueue
|
|
||||||
evictionPolicy EvictionPolicy
|
|
||||||
maximumSize int
|
|
||||||
mutex sync.RWMutex
|
|
||||||
observations int
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccumulatingBucketBuilder is a convenience method for generating a
|
|
||||||
// BucketBuilder that produces AccumatingBucket entries with a certain
|
|
||||||
// behavior set.
|
|
||||||
func AccumulatingBucketBuilder(evictionPolicy EvictionPolicy, maximumSize int) BucketBuilder {
|
|
||||||
return func() Bucket {
|
|
||||||
return &AccumulatingBucket{
|
|
||||||
elements: make(priorityQueue, 0, maximumSize),
|
|
||||||
evictionPolicy: evictionPolicy,
|
|
||||||
maximumSize: maximumSize,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a value to the bucket. Depending on whether the bucket is full, it may
|
|
||||||
// trigger an eviction of older items.
|
|
||||||
func (b *AccumulatingBucket) Add(value float64) {
|
|
||||||
b.mutex.Lock()
|
|
||||||
defer b.mutex.Unlock()
|
|
||||||
|
|
||||||
b.observations++
|
|
||||||
size := len(b.elements)
|
|
||||||
|
|
||||||
v := item{
|
|
||||||
Priority: -1 * time.Now().UnixNano(),
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
if size == b.maximumSize {
|
|
||||||
b.evictionPolicy(&b.elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
heap.Push(&b.elements, &v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b AccumulatingBucket) String() string {
|
|
||||||
b.mutex.RLock()
|
|
||||||
defer b.mutex.RUnlock()
|
|
||||||
|
|
||||||
buffer := &bytes.Buffer{}
|
|
||||||
|
|
||||||
fmt.Fprintf(buffer, "[AccumulatingBucket with %d elements and %d capacity] { ", len(b.elements), b.maximumSize)
|
|
||||||
|
|
||||||
for i := 0; i < len(b.elements); i++ {
|
|
||||||
fmt.Fprintf(buffer, "%f, ", b.elements[i].Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(buffer, "}")
|
|
||||||
|
|
||||||
return buffer.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b AccumulatingBucket) ValueForIndex(index int) float64 {
|
|
||||||
b.mutex.RLock()
|
|
||||||
defer b.mutex.RUnlock()
|
|
||||||
|
|
||||||
elementCount := len(b.elements)
|
|
||||||
|
|
||||||
if elementCount == 0 {
|
|
||||||
return math.NaN()
|
|
||||||
}
|
|
||||||
|
|
||||||
sortedElements := make([]float64, elementCount)
|
|
||||||
|
|
||||||
for i, element := range b.elements {
|
|
||||||
sortedElements[i] = element.Value.(float64)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Float64s(sortedElements)
|
|
||||||
|
|
||||||
// N.B.(mtp): Interfacing components should not need to comprehend what
|
|
||||||
// eviction and storage container strategies used; therefore,
|
|
||||||
// we adjust this silently.
|
|
||||||
targetIndex := int(float64(elementCount-1) * (float64(index) / float64(b.observations)))
|
|
||||||
|
|
||||||
return sortedElements[targetIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b AccumulatingBucket) Observations() int {
|
|
||||||
b.mutex.RLock()
|
|
||||||
defer b.mutex.RUnlock()
|
|
||||||
|
|
||||||
return b.observations
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *AccumulatingBucket) Reset() {
|
|
||||||
b.mutex.Lock()
|
|
||||||
defer b.mutex.RUnlock()
|
|
||||||
|
|
||||||
for i := 0; i < b.elements.Len(); i++ {
|
|
||||||
b.elements.Pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
b.observations = 0
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
. "github.com/matttproud/gocheck"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *S) TestAccumulatingBucketBuilderWithEvictOldest(c *C) {
|
|
||||||
var evictOldestThree EvictionPolicy = EvictOldest(3)
|
|
||||||
|
|
||||||
c.Assert(evictOldestThree, Not(IsNil))
|
|
||||||
|
|
||||||
bb := AccumulatingBucketBuilder(evictOldestThree, 5)
|
|
||||||
|
|
||||||
c.Assert(bb, Not(IsNil))
|
|
||||||
|
|
||||||
var b Bucket = bb()
|
|
||||||
|
|
||||||
c.Assert(b, Not(IsNil))
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 0 elements and 5 capacity] { }")
|
|
||||||
|
|
||||||
b.Add(1)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 1 elements and 5 capacity] { 1.000000, }")
|
|
||||||
|
|
||||||
b.Add(2)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 2 elements and 5 capacity] { 1.000000, 2.000000, }")
|
|
||||||
|
|
||||||
b.Add(3)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 3 elements and 5 capacity] { 1.000000, 2.000000, 3.000000, }")
|
|
||||||
|
|
||||||
b.Add(4)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 4 elements and 5 capacity] { 1.000000, 2.000000, 3.000000, 4.000000, }")
|
|
||||||
|
|
||||||
b.Add(5)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 5 elements and 5 capacity] { 1.000000, 2.000000, 3.000000, 4.000000, 5.000000, }")
|
|
||||||
|
|
||||||
b.Add(6)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 3 elements and 5 capacity] { 4.000000, 5.000000, 6.000000, }")
|
|
||||||
|
|
||||||
var bucket Bucket = b
|
|
||||||
|
|
||||||
c.Assert(bucket, Not(IsNil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestAccumulatingBucketBuilderWithEvictAndReplaceWithAverage(c *C) {
|
|
||||||
var evictAndReplaceWithAverage EvictionPolicy = EvictAndReplaceWith(3, AverageReducer)
|
|
||||||
|
|
||||||
c.Assert(evictAndReplaceWithAverage, Not(IsNil))
|
|
||||||
|
|
||||||
bb := AccumulatingBucketBuilder(evictAndReplaceWithAverage, 5)
|
|
||||||
|
|
||||||
c.Assert(bb, Not(IsNil))
|
|
||||||
|
|
||||||
var b Bucket = bb()
|
|
||||||
|
|
||||||
c.Assert(b, Not(IsNil))
|
|
||||||
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 0 elements and 5 capacity] { }")
|
|
||||||
|
|
||||||
b.Add(1)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 1 elements and 5 capacity] { 1.000000, }")
|
|
||||||
|
|
||||||
b.Add(2)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 2 elements and 5 capacity] { 1.000000, 2.000000, }")
|
|
||||||
|
|
||||||
b.Add(3)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 3 elements and 5 capacity] { 1.000000, 2.000000, 3.000000, }")
|
|
||||||
|
|
||||||
b.Add(4)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 4 elements and 5 capacity] { 1.000000, 2.000000, 3.000000, 4.000000, }")
|
|
||||||
|
|
||||||
b.Add(5)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 5 elements and 5 capacity] { 1.000000, 2.000000, 3.000000, 4.000000, 5.000000, }")
|
|
||||||
|
|
||||||
b.Add(6)
|
|
||||||
c.Check(b.String(), Equals, "[AccumulatingBucket with 4 elements and 5 capacity] { 4.000000, 5.000000, 2.000000, 6.000000, }")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestAccumulatingBucket(c *C) {
|
|
||||||
var b AccumulatingBucket = AccumulatingBucket{
|
|
||||||
elements: make(priorityQueue, 0, 10),
|
|
||||||
maximumSize: 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(b.elements, HasLen, 0)
|
|
||||||
c.Check(b.observations, Equals, 0)
|
|
||||||
c.Check(b.Observations(), Equals, 0)
|
|
||||||
|
|
||||||
b.Add(5.0)
|
|
||||||
|
|
||||||
c.Check(b.elements, HasLen, 1)
|
|
||||||
c.Check(b.observations, Equals, 1)
|
|
||||||
c.Check(b.Observations(), Equals, 1)
|
|
||||||
|
|
||||||
b.Add(6.0)
|
|
||||||
b.Add(7.0)
|
|
||||||
b.Add(8.0)
|
|
||||||
b.Add(9.0)
|
|
||||||
|
|
||||||
c.Check(b.elements, HasLen, 5)
|
|
||||||
c.Check(b.observations, Equals, 5)
|
|
||||||
c.Check(b.Observations(), Equals, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestAccumulatingBucketValueForIndex(c *C) {
|
|
||||||
var b AccumulatingBucket = AccumulatingBucket{
|
|
||||||
elements: make(priorityQueue, 0, 100),
|
|
||||||
maximumSize: 100,
|
|
||||||
evictionPolicy: EvictOldest(50),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i <= 100; i++ {
|
|
||||||
c.Assert(b.ValueForIndex(i), IsNaN)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The bucket has only observed one item and contains now one item.
|
|
||||||
b.Add(1.0)
|
|
||||||
|
|
||||||
c.Check(b.ValueForIndex(0), Equals, 1.0)
|
|
||||||
// Let's sanity check what occurs if presumably an eviction happened and
|
|
||||||
// we requested an index larger than what is contained.
|
|
||||||
c.Check(b.ValueForIndex(1), Equals, 1.0)
|
|
||||||
|
|
||||||
for i := 2.0; i <= 100; i += 1 {
|
|
||||||
b.Add(i)
|
|
||||||
|
|
||||||
// TODO(mtp): This is a sin. Provide a mechanism for deterministic testing.
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(b.ValueForIndex(0), Equals, 1.0)
|
|
||||||
c.Check(b.ValueForIndex(50), Equals, 50.0)
|
|
||||||
c.Check(b.ValueForIndex(100), Equals, 100.0)
|
|
||||||
|
|
||||||
for i := 101.0; i <= 150; i += 1 {
|
|
||||||
b.Add(i)
|
|
||||||
// TODO(mtp): This is a sin. Provide a mechanism for deterministic testing.
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The bucket's capacity has been exceeded by inputs at this point;
|
|
||||||
// consequently, we search for a given element by percentage offset
|
|
||||||
// therein.
|
|
||||||
c.Check(b.ValueForIndex(0), Equals, 51.0)
|
|
||||||
c.Check(b.ValueForIndex(50), Equals, 84.0)
|
|
||||||
c.Check(b.ValueForIndex(99), Equals, 116.0)
|
|
||||||
c.Check(b.ValueForIndex(100), Equals, 117.0)
|
|
||||||
}
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
// 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 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,31 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// The Histogram class and associated types build buckets on their own.
|
|
||||||
type BucketBuilder func() Bucket
|
|
||||||
|
|
||||||
// This defines the base Bucket type. The exact behaviors of the bucket are
|
|
||||||
// at the whim of the implementor.
|
|
||||||
//
|
|
||||||
// A Bucket is used as a container by Histogram as a collection for its
|
|
||||||
// accumulated samples.
|
|
||||||
type Bucket interface {
|
|
||||||
// Add a value to the bucket.
|
|
||||||
Add(value float64)
|
|
||||||
// Provide a count of observations throughout the bucket's lifetime.
|
|
||||||
Observations() int
|
|
||||||
// Reset is responsible for resetting this bucket back to a pristine state.
|
|
||||||
Reset()
|
|
||||||
// Provide a humanized representation hereof.
|
|
||||||
String() string
|
|
||||||
// Provide the value from the given in-memory value cache or an estimate
|
|
||||||
// thereof for the given index. The consumer of the bucket's data makes
|
|
||||||
// no assumptions about the underlying storage mechanisms that the bucket
|
|
||||||
// employs.
|
|
||||||
ValueForIndex(index int) float64
|
|
||||||
}
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
// 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 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.
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,71 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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
|
|
||||||
|
|
||||||
var (
|
|
||||||
// NilLabels is a nil set of labels merely for end-user convenience.
|
|
||||||
NilLabels map[string]string
|
|
||||||
|
|
||||||
// DefaultHandler is the default http.Handler for exposing telemetric
|
|
||||||
// data over a web services interface.
|
|
||||||
DefaultHandler = DefaultRegistry.Handler()
|
|
||||||
|
|
||||||
// DefaultRegistry with which Metric objects are associated.
|
|
||||||
DefaultRegistry = NewRegistry()
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// FlagNamespace is a prefix to be used to namespace instrumentation
|
|
||||||
// flags from others.
|
|
||||||
FlagNamespace = "telemetry."
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
|
|
||||||
// JSONAPIVersion is the version of the JSON export format.
|
|
||||||
JSONAPIVersion = "0.0.2"
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
// JSONTelemetryContentType is the content type set on telemetry data
|
|
||||||
// responses formatted as JSON.
|
|
||||||
JSONTelemetryContentType = `application/json; schema="prometheus/telemetry"; version=` + JSONAPIVersion
|
|
||||||
|
|
||||||
// ExpositionResource is the customary web services endpoint on which
|
|
||||||
// telemetric data is exposed.
|
|
||||||
ExpositionResource = "/metrics"
|
|
||||||
|
|
||||||
baseLabelsKey = "baseLabels"
|
|
||||||
docstringKey = "docstring"
|
|
||||||
metricKey = "metric"
|
|
||||||
|
|
||||||
counterTypeValue = "counter"
|
|
||||||
floatBitCount = 64
|
|
||||||
floatFormat = 'f'
|
|
||||||
floatPrecision = 6
|
|
||||||
gaugeTypeValue = "gauge"
|
|
||||||
untypedTypeValue = "untyped"
|
|
||||||
histogramTypeValue = "histogram"
|
|
||||||
typeKey = "type"
|
|
||||||
valueKey = "value"
|
|
||||||
labelsKey = "labels"
|
|
||||||
)
|
|
||||||
|
|
||||||
var blankLabelsSingleton = map[string]string{}
|
|
|
@ -1,194 +1,149 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright 2014 Prometheus Team
|
||||||
// All rights reserved.
|
// 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
|
||||||
//
|
//
|
||||||
// Use of this source code is governed by a BSD-style
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
// license that can be found in the LICENSE file.
|
//
|
||||||
|
// 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
|
package prometheus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"errors"
|
||||||
"fmt"
|
"hash/fnv"
|
||||||
"sync"
|
|
||||||
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
|
||||||
|
|
||||||
"code.google.com/p/goprotobuf/proto"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(matt): Refactor to de-duplicate behaviors.
|
// 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 {
|
type Counter interface {
|
||||||
Metric
|
Metric
|
||||||
|
Collector
|
||||||
|
|
||||||
Decrement(labels map[string]string) float64
|
// Set is used to set the Counter to an arbitrary value. It is only used
|
||||||
DecrementBy(labels map[string]string, value float64) float64
|
// if you have to transfer a value from an external counter into this
|
||||||
Increment(labels map[string]string) float64
|
// Prometheus metrics. Do not use it for regular handling of a
|
||||||
IncrementBy(labels map[string]string, value float64) float64
|
// Prometheus counter (as it can be used to break the contract of
|
||||||
Set(labels map[string]string, value float64) float64
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type counterVector struct {
|
// CounterOpts is an alias for Opts. See there for doc comments.
|
||||||
Labels map[string]string `json:"labels"`
|
type CounterOpts Opts
|
||||||
Value float64 `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCounter() Counter {
|
// NewCounter creates a new Counter based on the provided CounterOpts.
|
||||||
return &counter{
|
func NewCounter(opts CounterOpts) Counter {
|
||||||
values: map[uint64]*counterVector{},
|
desc := NewDesc(
|
||||||
}
|
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
|
opts.Help,
|
||||||
|
nil,
|
||||||
|
opts.ConstLabels,
|
||||||
|
)
|
||||||
|
result := &counter{value: value{desc: desc, valType: CounterValue}}
|
||||||
|
result.Init(result) // Init self-collection.
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
type counter struct {
|
type counter struct {
|
||||||
mutex sync.RWMutex
|
value
|
||||||
values map[uint64]*counterVector
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *counter) Set(labels map[string]string, value float64) float64 {
|
func (c *counter) Add(v float64) {
|
||||||
if labels == nil {
|
if v < 0 {
|
||||||
labels = blankLabelsSingleton
|
panic(errors.New("counter cannot decrease in value"))
|
||||||
}
|
}
|
||||||
|
c.value.Add(v)
|
||||||
signature := model.LabelValuesToSignature(labels)
|
|
||||||
|
|
||||||
metric.mutex.Lock()
|
|
||||||
defer metric.mutex.Unlock()
|
|
||||||
|
|
||||||
if original, ok := metric.values[signature]; ok {
|
|
||||||
original.Value = value
|
|
||||||
} else {
|
|
||||||
metric.values[signature] = &counterVector{
|
|
||||||
Labels: labels,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *counter) Reset(labels map[string]string) {
|
// CounterVec is a Collector that bundles a set of Counters that all share the
|
||||||
signature := model.LabelValuesToSignature(labels)
|
// 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
|
||||||
metric.mutex.Lock()
|
// (e.g. number of http requests, partitioned by response code and
|
||||||
defer metric.mutex.Unlock()
|
// method). Create instances with NewCounterVec.
|
||||||
delete(metric.values, signature)
|
//
|
||||||
|
// CounterVec embeds MetricVec. See there for a full list of methods with
|
||||||
|
// detailed documentation.
|
||||||
|
type CounterVec struct {
|
||||||
|
MetricVec
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *counter) ResetAll() {
|
// NewCounterVec creates a new CounterVec based on the provided CounterOpts and
|
||||||
metric.mutex.Lock()
|
// partitioned by the given label names. At least one label name must be
|
||||||
defer metric.mutex.Unlock()
|
// provided.
|
||||||
|
func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
|
||||||
for key, value := range metric.values {
|
desc := NewDesc(
|
||||||
for label := range value.Labels {
|
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
delete(value.Labels, label)
|
opts.Help,
|
||||||
}
|
labelNames,
|
||||||
delete(metric.values, key)
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *counter) String() string {
|
// GetMetricWithLabelValues replaces the method of the same name in
|
||||||
formatString := "[Counter %s]"
|
// MetricVec. The difference is that this method returns a Counter and not a
|
||||||
|
// Metric so that no type conversion is required.
|
||||||
metric.mutex.RLock()
|
func (m *CounterVec) GetMetricWithLabelValues(lvs ...string) (Counter, error) {
|
||||||
defer metric.mutex.RUnlock()
|
metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...)
|
||||||
|
if metric != nil {
|
||||||
return fmt.Sprintf(formatString, metric.values)
|
return metric.(Counter), err
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *counter) IncrementBy(labels map[string]string, value float64) float64 {
|
|
||||||
if labels == nil {
|
|
||||||
labels = blankLabelsSingleton
|
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
signature := model.LabelValuesToSignature(labels)
|
// 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
|
||||||
metric.mutex.Lock()
|
// type conversion is required.
|
||||||
defer metric.mutex.Unlock()
|
func (m *CounterVec) GetMetricWith(labels Labels) (Counter, error) {
|
||||||
|
metric, err := m.MetricVec.GetMetricWith(labels)
|
||||||
if original, ok := metric.values[signature]; ok {
|
if metric != nil {
|
||||||
original.Value += value
|
return metric.(Counter), err
|
||||||
} else {
|
|
||||||
metric.values[signature] = &counterVector{
|
|
||||||
Labels: labels,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *counter) Increment(labels map[string]string) float64 {
|
// WithLabelValues works as GetMetricWithLabelValues, but panics where
|
||||||
return metric.IncrementBy(labels, 1)
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *counter) DecrementBy(labels map[string]string, value float64) float64 {
|
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
|
||||||
if labels == nil {
|
// returned an error. By not returning an error, With allows shortcuts like
|
||||||
labels = blankLabelsSingleton
|
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
|
||||||
}
|
func (m *CounterVec) With(labels Labels) Counter {
|
||||||
|
return m.MetricVec.With(labels).(Counter)
|
||||||
signature := model.LabelValuesToSignature(labels)
|
|
||||||
|
|
||||||
metric.mutex.Lock()
|
|
||||||
defer metric.mutex.Unlock()
|
|
||||||
|
|
||||||
if original, ok := metric.values[signature]; ok {
|
|
||||||
original.Value -= value
|
|
||||||
} else {
|
|
||||||
metric.values[signature] = &counterVector{
|
|
||||||
Labels: labels,
|
|
||||||
Value: -1 * value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *counter) Decrement(labels map[string]string) float64 {
|
|
||||||
return metric.DecrementBy(labels, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *counter) MarshalJSON() ([]byte, error) {
|
|
||||||
metric.mutex.RLock()
|
|
||||||
defer metric.mutex.RUnlock()
|
|
||||||
|
|
||||||
values := make([]*counterVector, 0, len(metric.values))
|
|
||||||
|
|
||||||
for _, value := range metric.values {
|
|
||||||
values = append(values, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(map[string]interface{}{
|
|
||||||
valueKey: values,
|
|
||||||
typeKey: counterTypeValue,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *counter) dumpChildren(f *dto.MetricFamily) {
|
|
||||||
metric.mutex.RLock()
|
|
||||||
defer metric.mutex.RUnlock()
|
|
||||||
|
|
||||||
f.Type = dto.MetricType_COUNTER.Enum()
|
|
||||||
|
|
||||||
for _, child := range metric.values {
|
|
||||||
c := &dto.Counter{
|
|
||||||
Value: proto.Float64(child.Value),
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &dto.Metric{
|
|
||||||
Counter: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, value := range child.Labels {
|
|
||||||
p := &dto.LabelPair{
|
|
||||||
Name: proto.String(name),
|
|
||||||
Value: proto.String(value),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Label = append(m.Label, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Metric = append(f.Metric, m)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,238 +1,45 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright 2014 Prometheus Team
|
||||||
// All rights reserved.
|
// 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
|
||||||
//
|
//
|
||||||
// Use of this source code is governed by a BSD-style
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
// license that can be found in the LICENSE file.
|
//
|
||||||
|
// 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
|
package prometheus
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/test"
|
func TestCounterAdd(t *testing.T) {
|
||||||
)
|
counter := NewCounter(CounterOpts{
|
||||||
|
Name: "test",
|
||||||
func testCounter(t test.Tester) {
|
Help: "test help",
|
||||||
type input struct {
|
}).(*counter)
|
||||||
steps []func(g Counter)
|
counter.Inc()
|
||||||
|
if expected, got := 1., counter.val; expected != got {
|
||||||
|
t.Errorf("Expected %f, got %f.", expected, got)
|
||||||
}
|
}
|
||||||
type output struct {
|
counter.Add(42)
|
||||||
value string
|
if expected, got := 43., counter.val; expected != got {
|
||||||
|
t.Errorf("Expected %f, got %f.", expected, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
var scenarios = []struct {
|
if expected, got := "counter cannot decrease in value", decreaseCounter(counter).Error(); expected != got {
|
||||||
in input
|
t.Errorf("Expected error %q, got %q.", expected, got)
|
||||||
out output
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(nil, 1)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{},"value":1}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{}, 2)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{},"value":2}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{}, 3)
|
|
||||||
},
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{}, 5)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{},"value":5}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 13)
|
|
||||||
},
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{"handler": "/bar"}, 17)
|
|
||||||
},
|
|
||||||
func(g Counter) {
|
|
||||||
g.Reset(map[string]string{"handler": "/bar"})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{"handler":"/foo"},"value":13}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 13)
|
|
||||||
},
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{"handler": "/bar"}, 17)
|
|
||||||
},
|
|
||||||
func(g Counter) {
|
|
||||||
g.ResetAll()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 19)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{"handler":"/foo"},"value":19}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 23)
|
|
||||||
},
|
|
||||||
func(g Counter) {
|
|
||||||
g.Increment(map[string]string{"handler": "/foo"})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{"handler":"/foo"},"value":24}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Increment(map[string]string{"handler": "/foo"})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{"handler":"/foo"},"value":1}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Decrement(map[string]string{"handler": "/foo"})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{"handler":"/foo"},"value":-1}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 29)
|
|
||||||
},
|
|
||||||
func(g Counter) {
|
|
||||||
g.Decrement(map[string]string{"handler": "/foo"})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{"handler":"/foo"},"value":28}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 31)
|
|
||||||
},
|
|
||||||
func(g Counter) {
|
|
||||||
g.IncrementBy(map[string]string{"handler": "/foo"}, 5)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{"handler":"/foo"},"value":36}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Counter){
|
|
||||||
func(g Counter) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 37)
|
|
||||||
},
|
|
||||||
func(g Counter) {
|
|
||||||
g.DecrementBy(map[string]string{"handler": "/foo"}, 10)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"counter","value":[{"labels":{"handler":"/foo"},"value":27}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, scenario := range scenarios {
|
|
||||||
counter := NewCounter()
|
|
||||||
|
|
||||||
for _, step := range scenario.in.steps {
|
|
||||||
step(counter)
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := json.Marshal(counter)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%d. could not marshal into JSON %s", i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
asString := string(bytes)
|
|
||||||
|
|
||||||
if scenario.out.value != asString {
|
|
||||||
t.Errorf("%d. expected %q, got %q", i, scenario.out.value, asString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCounter(t *testing.T) {
|
func decreaseCounter(c *counter) (err error) {
|
||||||
testCounter(t)
|
defer func() {
|
||||||
}
|
if e := recover(); e != nil {
|
||||||
|
err = e.(error)
|
||||||
func BenchmarkCounter(b *testing.B) {
|
}
|
||||||
for i := 0; i < b.N; i++ {
|
}()
|
||||||
testCounter(b)
|
c.Add(-1)
|
||||||
}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
h.Write(b.Bytes())
|
||||||
|
for _, labelName := range labelNames {
|
||||||
|
b.Reset()
|
||||||
|
b.WriteString(labelName)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Name, lp.Value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
|
@ -1,36 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Go's standard library does not offer a factorial function.
|
|
||||||
func factorial(of int) int64 {
|
|
||||||
if of <= 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var result int64 = 1
|
|
||||||
|
|
||||||
for i := int64(of); i >= 1; i-- {
|
|
||||||
result *= i
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the value of a probability density for a given binomial statistic,
|
|
||||||
// where k is the target count of true cases, n is the number of subjects, and
|
|
||||||
// p is the probability.
|
|
||||||
func binomialPDF(k, n int, p float64) float64 {
|
|
||||||
binomialCoefficient := float64(factorial(n)) / float64(factorial(k)*factorial(n-k))
|
|
||||||
intermediate := math.Pow(p, float64(k)) * math.Pow(1-p, float64(n-k))
|
|
||||||
|
|
||||||
return binomialCoefficient * intermediate
|
|
||||||
}
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
// 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 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
|
|
@ -1,17 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 provides client side metric primitives and a telemetry
|
|
||||||
// exposition framework.
|
|
||||||
//
|
|
||||||
// This package provides both metric primitives and tools for their exposition
|
|
||||||
// to the Prometheus time series collection and computation framework.
|
|
||||||
//
|
|
||||||
// prometheus.Register("human_readable_metric_name", "metric docstring", map[string]string{"baseLabel": "baseLabelValue"}, metric)
|
|
||||||
//
|
|
||||||
// The examples under github.com/prometheus/client_golang/examples should be
|
|
||||||
// consulted.
|
|
||||||
package prometheus
|
|
|
@ -1,47 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
"container/heap"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EvictionPolicy implements some sort of garbage collection methodology for
|
|
||||||
// an underlying heap.Interface. This is presently only used for
|
|
||||||
// AccumulatingBucket.
|
|
||||||
type EvictionPolicy func(h heap.Interface)
|
|
||||||
|
|
||||||
// As the name implies, this evicts the oldest x objects from the heap.
|
|
||||||
func EvictOldest(count int) EvictionPolicy {
|
|
||||||
return func(h heap.Interface) {
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
heap.Pop(h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This factory produces an EvictionPolicy that applies some standardized
|
|
||||||
// reduction methodology on the to-be-terminated values.
|
|
||||||
func EvictAndReplaceWith(count int, reducer ReductionMethod) EvictionPolicy {
|
|
||||||
return func(h heap.Interface) {
|
|
||||||
oldValues := make([]float64, count)
|
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
oldValues[i] = heap.Pop(h).(*item).Value.(float64)
|
|
||||||
}
|
|
||||||
|
|
||||||
reduced := reducer(oldValues)
|
|
||||||
|
|
||||||
heap.Push(h, &item{
|
|
||||||
Value: reduced,
|
|
||||||
// TODO(mtp): Parameterize the priority generation since these tools are
|
|
||||||
// useful.
|
|
||||||
Priority: -1 * time.Now().UnixNano(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,177 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
"container/heap"
|
|
||||||
. "github.com/matttproud/gocheck"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *S) TestEvictOldest(c *C) {
|
|
||||||
q := make(priorityQueue, 0, 10)
|
|
||||||
heap.Init(&q)
|
|
||||||
var e EvictionPolicy = EvictOldest(5)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
var item item = item{
|
|
||||||
Priority: int64(i),
|
|
||||||
Value: float64(i),
|
|
||||||
}
|
|
||||||
|
|
||||||
heap.Push(&q, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 10)
|
|
||||||
|
|
||||||
e(&q)
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 5)
|
|
||||||
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 4.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 3.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 2.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 1.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestEvictAndReplaceWithAverage(c *C) {
|
|
||||||
q := make(priorityQueue, 0, 10)
|
|
||||||
heap.Init(&q)
|
|
||||||
var e EvictionPolicy = EvictAndReplaceWith(5, AverageReducer)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
var item item = item{
|
|
||||||
Priority: int64(i),
|
|
||||||
Value: float64(i),
|
|
||||||
}
|
|
||||||
|
|
||||||
heap.Push(&q, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 10)
|
|
||||||
|
|
||||||
e(&q)
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 6)
|
|
||||||
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 4.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 3.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 2.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 1.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 0.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 7.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestEvictAndReplaceWithMedian(c *C) {
|
|
||||||
q := make(priorityQueue, 0, 10)
|
|
||||||
heap.Init(&q)
|
|
||||||
var e EvictionPolicy = EvictAndReplaceWith(5, MedianReducer)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
var item item = item{
|
|
||||||
Priority: int64(i),
|
|
||||||
Value: float64(i),
|
|
||||||
}
|
|
||||||
|
|
||||||
heap.Push(&q, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 10)
|
|
||||||
|
|
||||||
e(&q)
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 6)
|
|
||||||
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 4.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 3.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 2.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 1.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 0.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 7.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestEvictAndReplaceWithFirstMode(c *C) {
|
|
||||||
q := make(priorityQueue, 0, 10)
|
|
||||||
heap.Init(&q)
|
|
||||||
e := EvictAndReplaceWith(5, FirstModeReducer)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
heap.Push(&q, &item{
|
|
||||||
Priority: int64(i),
|
|
||||||
Value: float64(i),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 10)
|
|
||||||
|
|
||||||
e(&q)
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 6)
|
|
||||||
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 4.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 3.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 2.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 1.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 0.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 9.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestEvictAndReplaceWithMinimum(c *C) {
|
|
||||||
q := make(priorityQueue, 0, 10)
|
|
||||||
heap.Init(&q)
|
|
||||||
var e EvictionPolicy = EvictAndReplaceWith(5, MinimumReducer)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
var item item = item{
|
|
||||||
Priority: int64(i),
|
|
||||||
Value: float64(i),
|
|
||||||
}
|
|
||||||
|
|
||||||
heap.Push(&q, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 10)
|
|
||||||
|
|
||||||
e(&q)
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 6)
|
|
||||||
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 4.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 3.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 2.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 1.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 0.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 5.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestEvictAndReplaceWithMaximum(c *C) {
|
|
||||||
q := make(priorityQueue, 0, 10)
|
|
||||||
heap.Init(&q)
|
|
||||||
var e EvictionPolicy = EvictAndReplaceWith(5, MaximumReducer)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
var item item = item{
|
|
||||||
Priority: int64(i),
|
|
||||||
Value: float64(i),
|
|
||||||
}
|
|
||||||
|
|
||||||
heap.Push(&q, &item)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 10)
|
|
||||||
|
|
||||||
e(&q)
|
|
||||||
|
|
||||||
c.Check(q, HasLen, 6)
|
|
||||||
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 4.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 3.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 2.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 1.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 0.0)
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, 9.0)
|
|
||||||
}
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
// 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 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)
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
// 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 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)
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
// 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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 is a pretty low-level approach. A more high-level approach is
|
||||||
|
// to implement a Collector directly and not an individual Metric, see the
|
||||||
|
// Collector examples.
|
||||||
|
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) {
|
||||||
|
m.Untyped = &dto.Untyped{Value: proto.Float64(cm.callback())}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,435 @@
|
||||||
|
// 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 prometheus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"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 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: 30.2
|
||||||
|
// >
|
||||||
|
// quantile: <
|
||||||
|
// quantile: 0.9
|
||||||
|
// value: 41.4
|
||||||
|
// >
|
||||||
|
// 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
|
||||||
|
// >
|
||||||
|
// quantile: <
|
||||||
|
// quantile: 0.9
|
||||||
|
// value: 41.5
|
||||||
|
// >
|
||||||
|
// 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: 30.2
|
||||||
|
// >
|
||||||
|
// quantile: <
|
||||||
|
// quantile: 0.9
|
||||||
|
// value: 41.4
|
||||||
|
// >
|
||||||
|
// quantile: <
|
||||||
|
// quantile: 0.99
|
||||||
|
// value: 41.9
|
||||||
|
// >
|
||||||
|
// >
|
||||||
|
// ]
|
||||||
|
}
|
|
@ -1,110 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be found in
|
|
||||||
// the LICENSE file.
|
|
||||||
|
|
||||||
package exp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
handler = "handler"
|
|
||||||
code = "code"
|
|
||||||
method = "method"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
coarseMux struct {
|
|
||||||
*http.ServeMux
|
|
||||||
}
|
|
||||||
|
|
||||||
handlerDelegator struct {
|
|
||||||
delegate http.Handler
|
|
||||||
pattern string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
requestCounts = prometheus.NewCounter()
|
|
||||||
requestDuration = prometheus.NewCounter()
|
|
||||||
requestDurations = prometheus.NewDefaultHistogram()
|
|
||||||
requestBytes = prometheus.NewCounter()
|
|
||||||
responseBytes = prometheus.NewCounter()
|
|
||||||
|
|
||||||
// DefaultCoarseMux is a drop-in replacement for http.DefaultServeMux that
|
|
||||||
// provides standardized telemetry for Go's standard HTTP handler registration
|
|
||||||
// and dispatch API.
|
|
||||||
//
|
|
||||||
// The name is due to the coarse grouping of telemetry by (HTTP Method, HTTP Response Code,
|
|
||||||
// and handler match pattern) triples.
|
|
||||||
DefaultCoarseMux = newCoarseMux()
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h handlerDelegator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
start := time.Now()
|
|
||||||
rwd := NewResponseWriterDelegator(w)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
duration := float64(time.Since(start) / time.Microsecond)
|
|
||||||
status := rwd.Status()
|
|
||||||
labels := map[string]string{handler: h.pattern, code: status, method: strings.ToLower(r.Method)}
|
|
||||||
requestCounts.Increment(labels)
|
|
||||||
requestDuration.IncrementBy(labels, duration)
|
|
||||||
requestDurations.Add(labels, duration)
|
|
||||||
requestBytes.IncrementBy(labels, float64(computeApproximateRequestSize(*r)))
|
|
||||||
responseBytes.IncrementBy(labels, float64(rwd.BytesWritten))
|
|
||||||
}()
|
|
||||||
|
|
||||||
h.delegate.ServeHTTP(rwd, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h handlerDelegator) String() string {
|
|
||||||
return fmt.Sprintf("handlerDelegator wrapping %s for %s", h.delegate, h.pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle registers a http.Handler to this CoarseMux. See http.ServeMux.Handle.
|
|
||||||
func (m *coarseMux) handle(pattern string, handler http.Handler) {
|
|
||||||
m.ServeMux.Handle(pattern, handlerDelegator{
|
|
||||||
delegate: handler,
|
|
||||||
pattern: pattern,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle registers a handler to this CoarseMux. See http.ServeMux.HandleFunc.
|
|
||||||
func (m *coarseMux) handleFunc(pattern string, handler http.HandlerFunc) {
|
|
||||||
m.ServeMux.Handle(pattern, handlerDelegator{
|
|
||||||
delegate: handler,
|
|
||||||
pattern: pattern,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCoarseMux() *coarseMux {
|
|
||||||
return &coarseMux{
|
|
||||||
ServeMux: http.NewServeMux(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle registers a http.Handler to DefaultCoarseMux. See http.Handle.
|
|
||||||
func Handle(pattern string, handler http.Handler) {
|
|
||||||
DefaultCoarseMux.handle(pattern, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleFunc registers a handler to DefaultCoarseMux. See http.HandleFunc.
|
|
||||||
func HandleFunc(pattern string, handler http.HandlerFunc) {
|
|
||||||
DefaultCoarseMux.handleFunc(pattern, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
prometheus.Register("http_requests_total", "A counter of the total number of HTTP requests made against the default multiplexor.", prometheus.NilLabels, requestCounts)
|
|
||||||
prometheus.Register("http_request_durations_total_microseconds", "The total amount of time the default multiplexor has spent answering HTTP requests (microseconds).", prometheus.NilLabels, requestDuration)
|
|
||||||
prometheus.Register("http_request_durations_microseconds", "The amounts of time the default multiplexor has spent answering HTTP requests (microseconds).", prometheus.NilLabels, requestDurations)
|
|
||||||
prometheus.Register("http_request_bytes_total", "The total volume of content body sizes received (bytes).", prometheus.NilLabels, requestBytes)
|
|
||||||
prometheus.Register("http_response_bytes_total", "The total volume of response payloads emitted (bytes).", prometheus.NilLabels, responseBytes)
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be found in
|
|
||||||
// the LICENSE file.
|
|
||||||
|
|
||||||
// A repository of various immature Prometheus client components that may
|
|
||||||
// assist in your use of the library. Items contained herein are regarded as
|
|
||||||
// especially interface unstable and may change without warning. Upon
|
|
||||||
// maturation, they should be migrated into a formal package for users.
|
|
||||||
package exp
|
|
|
@ -1,100 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be found in
|
|
||||||
// the LICENSE file.
|
|
||||||
|
|
||||||
package exp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
unknownStatusCode = "unknown"
|
|
||||||
statusFieldName = "status"
|
|
||||||
)
|
|
||||||
|
|
||||||
type status string
|
|
||||||
|
|
||||||
func (s status) unknown() bool {
|
|
||||||
return len(s) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s status) String() string {
|
|
||||||
if s.unknown() {
|
|
||||||
return unknownStatusCode
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func computeApproximateRequestSize(r http.Request) (s int) {
|
|
||||||
s += len(r.Method)
|
|
||||||
if r.URL != nil {
|
|
||||||
s += len(r.URL.String())
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseWriterDelegator is a means of wrapping http.ResponseWriter to divine
|
|
||||||
// the response code from a given answer, especially in systems where the
|
|
||||||
// response is treated as a blackbox.
|
|
||||||
type ResponseWriterDelegator struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
status status
|
|
||||||
BytesWritten int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ResponseWriterDelegator) String() string {
|
|
||||||
return fmt.Sprintf("ResponseWriterDelegator decorating %s with status %s and %d bytes written.", r.ResponseWriter, r.status, r.BytesWritten)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ResponseWriterDelegator) WriteHeader(code int) {
|
|
||||||
r.status = status(strconv.Itoa(code))
|
|
||||||
|
|
||||||
r.ResponseWriter.WriteHeader(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ResponseWriterDelegator) Status() string {
|
|
||||||
if r.status.unknown() {
|
|
||||||
delegate := reflect.ValueOf(r.ResponseWriter).Elem()
|
|
||||||
statusField := delegate.FieldByName(statusFieldName)
|
|
||||||
if statusField.IsValid() {
|
|
||||||
r.status = status(strconv.Itoa(int(statusField.Int())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.status.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ResponseWriterDelegator) Write(b []byte) (n int, err error) {
|
|
||||||
n, err = r.ResponseWriter.Write(b)
|
|
||||||
r.BytesWritten += n
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewResponseWriterDelegator(delegate http.ResponseWriter) *ResponseWriterDelegator {
|
|
||||||
return &ResponseWriterDelegator{
|
|
||||||
ResponseWriter: delegate,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
// 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 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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
// 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 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 >
|
||||||
|
}
|
|
@ -1,139 +1,123 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright 2014 Prometheus Team
|
||||||
// All rights reserved.
|
// 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
|
||||||
//
|
//
|
||||||
// Use of this source code is governed by a BSD-style
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
// license that can be found in the LICENSE file.
|
//
|
||||||
|
// 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
|
package prometheus
|
||||||
|
|
||||||
import (
|
import "hash/fnv"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"code.google.com/p/goprotobuf/proto"
|
// Gauge is a Metric that represents a single numerical value that can
|
||||||
|
// arbitrarily go up and down.
|
||||||
dto "github.com/prometheus/client_model/go"
|
//
|
||||||
|
// A Gauge is typically used for measured values like temperatures or current
|
||||||
"github.com/prometheus/client_golang/model"
|
// memory usage, but also "counts" that can go up and down, like the number of
|
||||||
)
|
// running goroutines.
|
||||||
|
//
|
||||||
// A gauge metric merely provides an instantaneous representation of a scalar
|
// To create Gauge instances, use NewGauge.
|
||||||
// value or an accumulation. For instance, if one wants to expose the current
|
|
||||||
// temperature or the hitherto bandwidth used, this would be the metric for such
|
|
||||||
// circumstances.
|
|
||||||
type Gauge interface {
|
type Gauge interface {
|
||||||
Metric
|
Metric
|
||||||
Set(labels map[string]string, value float64) float64
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type gaugeVector struct {
|
// GaugeOpts is an alias for Opts. See there for doc comments.
|
||||||
Labels map[string]string `json:"labels"`
|
type GaugeOpts Opts
|
||||||
Value float64 `json:"value"`
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGauge() Gauge {
|
// GaugeVec is a Collector that bundles a set of Gauges that all share the same
|
||||||
return &gauge{
|
// Desc, but have different values for their variable labels. This is used if
|
||||||
values: map[uint64]*gaugeVector{},
|
// 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...)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type gauge struct {
|
// GetMetricWithLabelValues replaces the method of the same name in
|
||||||
mutex sync.RWMutex
|
// MetricVec. The difference is that this method returns a Gauge and not a
|
||||||
values map[uint64]*gaugeVector
|
// Metric so that no type conversion is required.
|
||||||
}
|
func (m *GaugeVec) GetMetricWithLabelValues(lvs ...string) (Gauge, error) {
|
||||||
|
metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...)
|
||||||
func (metric *gauge) String() string {
|
if metric != nil {
|
||||||
formatString := "[Gauge %s]"
|
return metric.(Gauge), err
|
||||||
|
|
||||||
metric.mutex.RLock()
|
|
||||||
defer metric.mutex.RUnlock()
|
|
||||||
|
|
||||||
return fmt.Sprintf(formatString, metric.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *gauge) Set(labels map[string]string, value float64) float64 {
|
|
||||||
if labels == nil {
|
|
||||||
labels = blankLabelsSingleton
|
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
signature := model.LabelValuesToSignature(labels)
|
// 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
|
||||||
metric.mutex.Lock()
|
// type conversion is required.
|
||||||
defer metric.mutex.Unlock()
|
func (m *GaugeVec) GetMetricWith(labels Labels) (Gauge, error) {
|
||||||
|
metric, err := m.MetricVec.GetMetricWith(labels)
|
||||||
if original, ok := metric.values[signature]; ok {
|
if metric != nil {
|
||||||
original.Value = value
|
return metric.(Gauge), err
|
||||||
} else {
|
|
||||||
metric.values[signature] = &gaugeVector{
|
|
||||||
Labels: labels,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *gauge) Reset(labels map[string]string) {
|
// WithLabelValues works as GetMetricWithLabelValues, but panics where
|
||||||
signature := model.LabelValuesToSignature(labels)
|
// GetMetricWithLabelValues would have returned an error. By not returning an
|
||||||
|
// error, WithLabelValues allows shortcuts like
|
||||||
metric.mutex.Lock()
|
// myVec.WithLabelValues("404", "GET").Add(42)
|
||||||
defer metric.mutex.Unlock()
|
func (m *GaugeVec) WithLabelValues(lvs ...string) Gauge {
|
||||||
|
return m.MetricVec.WithLabelValues(lvs...).(Gauge)
|
||||||
delete(metric.values, signature)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *gauge) ResetAll() {
|
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
|
||||||
metric.mutex.Lock()
|
// returned an error. By not returning an error, With allows shortcuts like
|
||||||
defer metric.mutex.Unlock()
|
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
|
||||||
|
func (m *GaugeVec) With(labels Labels) Gauge {
|
||||||
for key, value := range metric.values {
|
return m.MetricVec.With(labels).(Gauge)
|
||||||
for label := range value.Labels {
|
|
||||||
delete(value.Labels, label)
|
|
||||||
}
|
|
||||||
delete(metric.values, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *gauge) MarshalJSON() ([]byte, error) {
|
|
||||||
metric.mutex.RLock()
|
|
||||||
defer metric.mutex.RUnlock()
|
|
||||||
|
|
||||||
values := make([]*gaugeVector, 0, len(metric.values))
|
|
||||||
for _, value := range metric.values {
|
|
||||||
values = append(values, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(map[string]interface{}{
|
|
||||||
typeKey: gaugeTypeValue,
|
|
||||||
valueKey: values,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *gauge) dumpChildren(f *dto.MetricFamily) {
|
|
||||||
metric.mutex.RLock()
|
|
||||||
defer metric.mutex.RUnlock()
|
|
||||||
|
|
||||||
f.Type = dto.MetricType_GAUGE.Enum()
|
|
||||||
|
|
||||||
for _, child := range metric.values {
|
|
||||||
c := &dto.Gauge{
|
|
||||||
Value: proto.Float64(child.Value),
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &dto.Metric{
|
|
||||||
Gauge: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, value := range child.Labels {
|
|
||||||
p := &dto.LabelPair{
|
|
||||||
Name: proto.String(name),
|
|
||||||
Value: proto.String(value),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Label = append(m.Label, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Metric = append(f.Metric, m)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,154 +1,158 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright 2014 Prometheus Team
|
||||||
// All rights reserved.
|
// 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
|
||||||
//
|
//
|
||||||
// Use of this source code is governed by a BSD-style
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
// license that can be found in the LICENSE file.
|
//
|
||||||
|
// 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
|
package prometheus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/quick"
|
||||||
"github.com/prometheus/client_golang/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testGauge(t test.Tester) {
|
func listenGaugeStream(vals, result chan float64, done chan struct{}) {
|
||||||
type input struct {
|
var sum float64
|
||||||
steps []func(g Gauge)
|
outer:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
close(vals)
|
||||||
|
for v := range vals {
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
break outer
|
||||||
|
case v := <-vals:
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type output struct {
|
result <- sum
|
||||||
value string
|
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, gge.(*value).val; math.Abs(expected-got) > 0.000001 {
|
||||||
|
t.Fatalf("expected approx. %f, got %f", expected, got)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
var scenarios = []struct {
|
if err := quick.Check(it, nil); err != nil {
|
||||||
in input
|
t.Fatal(err)
|
||||||
out output
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Gauge){},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"gauge","value":[]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Gauge){
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Set(nil, 1)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"gauge","value":[{"labels":{},"value":1}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Gauge){
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Set(map[string]string{}, 2)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"gauge","value":[{"labels":{},"value":2}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Gauge){
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Set(map[string]string{}, 3)
|
|
||||||
},
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Set(map[string]string{}, 5)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"gauge","value":[{"labels":{},"value":5}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Gauge){
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 13)
|
|
||||||
},
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Set(map[string]string{"handler": "/bar"}, 17)
|
|
||||||
},
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Reset(map[string]string{"handler": "/bar"})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"gauge","value":[{"labels":{"handler":"/foo"},"value":13}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Gauge){
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 13)
|
|
||||||
},
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Set(map[string]string{"handler": "/bar"}, 17)
|
|
||||||
},
|
|
||||||
func(g Gauge) {
|
|
||||||
g.ResetAll()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"gauge","value":[]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Gauge){
|
|
||||||
func(g Gauge) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 19)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"gauge","value":[{"labels":{"handler":"/foo"},"value":19}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, scenario := range scenarios {
|
|
||||||
gauge := NewGauge()
|
|
||||||
|
|
||||||
for _, step := range scenario.in.steps {
|
|
||||||
step(gauge)
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := json.Marshal(gauge)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%d. could not marshal into JSON %s", i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
asString := string(bytes)
|
|
||||||
|
|
||||||
if scenario.out.value != asString {
|
|
||||||
t.Errorf("%d. expected %q, got %q", i, scenario.out.value, asString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGauge(t *testing.T) {
|
func TestGaugeVecConcurrency(t *testing.T) {
|
||||||
testGauge(t)
|
it := func(n uint32) bool {
|
||||||
}
|
mutations := int(n % 10000)
|
||||||
|
concLevel := int(n%15 + 1)
|
||||||
|
vecLength := int(n%5 + 1)
|
||||||
|
|
||||||
func BenchmarkGauge(b *testing.B) {
|
var start, end sync.WaitGroup
|
||||||
for i := 0; i < b.N; i++ {
|
start.Add(1)
|
||||||
testGauge(b)
|
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], gge.WithLabelValues(string('A'+i)).(*value).val; 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
"math"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
. "github.com/matttproud/gocheck"
|
|
||||||
)
|
|
||||||
|
|
||||||
type isNaNChecker struct {
|
|
||||||
*CheckerInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// This piece provides a simple tester for the gocheck testing library to
|
|
||||||
// ascertain if a value is not-a-number.
|
|
||||||
var IsNaN = &isNaNChecker{
|
|
||||||
&CheckerInfo{Name: "IsNaN", Params: []string{"value"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
func (checker *isNaNChecker) Check(params []interface{}, names []string) (result bool, error string) {
|
|
||||||
return isNaN(params[0]), ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNaN(obtained interface{}) (result bool) {
|
|
||||||
if obtained == nil {
|
|
||||||
result = false
|
|
||||||
} else {
|
|
||||||
switch v := reflect.ValueOf(obtained); v.Kind() {
|
|
||||||
case reflect.Float64:
|
|
||||||
return math.IsNaN(obtained.(float64))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type valueEqualsChecker struct {
|
|
||||||
*CheckerInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
var ValueEquals = &valueEqualsChecker{
|
|
||||||
&CheckerInfo{Name: "IsValue", Params: []string{"obtained", "expected"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
func (checker *valueEqualsChecker) Check(params []interface{}, names []string) (result bool, error string) {
|
|
||||||
actual := params[0].(*item).Value
|
|
||||||
expected := params[1]
|
|
||||||
|
|
||||||
return actual == expected, ""
|
|
||||||
}
|
|
|
@ -1,403 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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/json"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
|
||||||
|
|
||||||
"code.google.com/p/goprotobuf/proto"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This generates count-buckets of equal size distributed along the open
|
|
||||||
// interval of lower to upper. For instance, {lower=0, upper=10, count=5}
|
|
||||||
// yields the following: [0, 2, 4, 6, 8].
|
|
||||||
func EquallySizedBucketsFor(lower, upper float64, count int) []float64 {
|
|
||||||
buckets := make([]float64, count)
|
|
||||||
|
|
||||||
partitionSize := (upper - lower) / float64(count)
|
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
m := float64(i)
|
|
||||||
buckets[i] = lower + (m * partitionSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buckets
|
|
||||||
}
|
|
||||||
|
|
||||||
// This generates log2-sized buckets spanning from lower to upper inclusively
|
|
||||||
// as well as values beyond it.
|
|
||||||
func LogarithmicSizedBucketsFor(lower, upper float64) []float64 {
|
|
||||||
bucketCount := int(math.Ceil(math.Log2(upper)))
|
|
||||||
|
|
||||||
buckets := make([]float64, bucketCount)
|
|
||||||
|
|
||||||
for i, j := 0, 0.0; i < bucketCount; i, j = i+1, math.Pow(2, float64(i+1.0)) {
|
|
||||||
buckets[i] = j
|
|
||||||
}
|
|
||||||
|
|
||||||
return buckets
|
|
||||||
}
|
|
||||||
|
|
||||||
// A HistogramSpecification defines how a Histogram is to be built.
|
|
||||||
type HistogramSpecification struct {
|
|
||||||
BucketBuilder BucketBuilder
|
|
||||||
ReportablePercentiles []float64
|
|
||||||
Starts []float64
|
|
||||||
PurgeInterval time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type Histogram interface {
|
|
||||||
Metric
|
|
||||||
Add(labels map[string]string, value float64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The histogram is an accumulator for samples. It merely routes into which
|
|
||||||
// bucket to capture an event and provides a percentile calculation mechanism.
|
|
||||||
type histogram struct {
|
|
||||||
bucketMaker BucketBuilder
|
|
||||||
// This represents the open interval's start at which values shall be added to
|
|
||||||
// the bucket. The interval continues until the beginning of the next bucket
|
|
||||||
// exclusive or positive infinity.
|
|
||||||
//
|
|
||||||
// N.B.
|
|
||||||
// - bucketStarts should be sorted in ascending order;
|
|
||||||
// - len(bucketStarts) must be equivalent to len(buckets);
|
|
||||||
// - The index of a given bucketStarts' element is presumed to
|
|
||||||
// correspond to the appropriate element in buckets.
|
|
||||||
bucketStarts []float64
|
|
||||||
mutex sync.RWMutex
|
|
||||||
// These are the buckets that capture samples as they are emitted to the
|
|
||||||
// histogram. Please consult the reference interface and its implements for
|
|
||||||
// further details about behavior expectations.
|
|
||||||
values map[uint64]*histogramVector
|
|
||||||
// These are the percentile values that will be reported on marshalling.
|
|
||||||
reportablePercentiles []float64
|
|
||||||
|
|
||||||
purgeInterval time.Duration
|
|
||||||
lastPurge time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type histogramVector struct {
|
|
||||||
buckets []Bucket
|
|
||||||
labels map[string]string
|
|
||||||
sum float64
|
|
||||||
count uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *histogram) Add(labels map[string]string, value float64) {
|
|
||||||
if labels == nil {
|
|
||||||
labels = blankLabelsSingleton
|
|
||||||
}
|
|
||||||
|
|
||||||
signature := model.LabelValuesToSignature(labels)
|
|
||||||
var histogram *histogramVector = nil
|
|
||||||
|
|
||||||
h.mutex.Lock()
|
|
||||||
defer h.mutex.Unlock()
|
|
||||||
|
|
||||||
if original, ok := h.values[signature]; ok {
|
|
||||||
histogram = original
|
|
||||||
} else {
|
|
||||||
bucketCount := len(h.bucketStarts)
|
|
||||||
histogram = &histogramVector{
|
|
||||||
buckets: make([]Bucket, bucketCount),
|
|
||||||
labels: labels,
|
|
||||||
}
|
|
||||||
for i := 0; i < bucketCount; i++ {
|
|
||||||
histogram.buckets[i] = h.bucketMaker()
|
|
||||||
}
|
|
||||||
h.values[signature] = histogram
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex := 0
|
|
||||||
|
|
||||||
for i, bucketStart := range h.bucketStarts {
|
|
||||||
if value < bucketStart {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex = i
|
|
||||||
}
|
|
||||||
|
|
||||||
histogram.buckets[lastIndex].Add(value)
|
|
||||||
|
|
||||||
histogram.sum += value
|
|
||||||
histogram.count++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *histogram) String() string {
|
|
||||||
h.mutex.RLock()
|
|
||||||
defer h.mutex.RUnlock()
|
|
||||||
|
|
||||||
stringBuffer := &bytes.Buffer{}
|
|
||||||
stringBuffer.WriteString("[Histogram { ")
|
|
||||||
|
|
||||||
for _, histogram := range h.values {
|
|
||||||
fmt.Fprintf(stringBuffer, "Labels: %s ", histogram.labels)
|
|
||||||
for i, bucketStart := range h.bucketStarts {
|
|
||||||
bucket := histogram.buckets[i]
|
|
||||||
fmt.Fprintf(stringBuffer, "[%f, inf) = %s, ", bucketStart, bucket)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stringBuffer.WriteString("}]")
|
|
||||||
|
|
||||||
return stringBuffer.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the number of previous observations up to a given index.
|
|
||||||
func previousCumulativeObservations(cumulativeObservations []int, bucketIndex int) int {
|
|
||||||
if bucketIndex == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return cumulativeObservations[bucketIndex-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the index for an element given a percentage of length.
|
|
||||||
func prospectiveIndexForPercentile(percentile float64, totalObservations int) int {
|
|
||||||
return int(percentile * float64(totalObservations-1))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the next bucket element when interim bucket intervals may be empty.
|
|
||||||
func (h histogram) nextNonEmptyBucketElement(signature uint64, currentIndex, bucketCount int, observationsByBucket []int) (*Bucket, int) {
|
|
||||||
for i := currentIndex; i < bucketCount; i++ {
|
|
||||||
if observationsByBucket[i] == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
histogram := h.values[signature]
|
|
||||||
|
|
||||||
return &histogram.buckets[i], 0
|
|
||||||
}
|
|
||||||
|
|
||||||
panic("Illegal Condition: There were no remaining buckets to provide a value.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find what bucket and element index contains a given percentile value.
|
|
||||||
// If a percentile is requested that results in a corresponding index that is no
|
|
||||||
// longer contained by the bucket, the index of the last item is returned. This
|
|
||||||
// may occur if the underlying bucket catalogs values and employs an eviction
|
|
||||||
// strategy.
|
|
||||||
func (h histogram) bucketForPercentile(signature uint64, percentile float64) (*Bucket, int) {
|
|
||||||
bucketCount := len(h.bucketStarts)
|
|
||||||
|
|
||||||
// This captures the quantity of samples in a given bucket's range.
|
|
||||||
observationsByBucket := make([]int, bucketCount)
|
|
||||||
// This captures the cumulative quantity of observations from all preceding
|
|
||||||
// buckets up and to the end of this bucket.
|
|
||||||
cumulativeObservationsByBucket := make([]int, bucketCount)
|
|
||||||
|
|
||||||
totalObservations := 0
|
|
||||||
|
|
||||||
histogram := h.values[signature]
|
|
||||||
|
|
||||||
for i, bucket := range histogram.buckets {
|
|
||||||
observations := bucket.Observations()
|
|
||||||
observationsByBucket[i] = observations
|
|
||||||
totalObservations += bucket.Observations()
|
|
||||||
cumulativeObservationsByBucket[i] = totalObservations
|
|
||||||
}
|
|
||||||
|
|
||||||
// This captures the index offset where the given percentile value would be
|
|
||||||
// were all submitted samples stored and never down-/re-sampled nor deleted
|
|
||||||
// and housed in a singular array.
|
|
||||||
prospectiveIndex := prospectiveIndexForPercentile(percentile, totalObservations)
|
|
||||||
|
|
||||||
for i, cumulativeObservation := range cumulativeObservationsByBucket {
|
|
||||||
if cumulativeObservation == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the bucket that contains the given index.
|
|
||||||
if cumulativeObservation >= prospectiveIndex {
|
|
||||||
var subIndex int
|
|
||||||
// This calculates the index within the current bucket where the given
|
|
||||||
// percentile may be found.
|
|
||||||
subIndex = prospectiveIndex - previousCumulativeObservations(cumulativeObservationsByBucket, i)
|
|
||||||
|
|
||||||
// Sometimes the index may be the last item, in which case we need to
|
|
||||||
// take this into account.
|
|
||||||
if observationsByBucket[i] == subIndex {
|
|
||||||
return h.nextNonEmptyBucketElement(signature, i+1, bucketCount, observationsByBucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &histogram.buckets[i], subIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &histogram.buckets[0], 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the histogram's estimate of the value for a given percentile of
|
|
||||||
// collected samples. The requested percentile is expected to be a real
|
|
||||||
// value within (0, 1.0].
|
|
||||||
func (h histogram) percentile(signature uint64, percentile float64) float64 {
|
|
||||||
bucket, index := h.bucketForPercentile(signature, percentile)
|
|
||||||
|
|
||||||
return (*bucket).ValueForIndex(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatFloat(value float64) string {
|
|
||||||
return strconv.FormatFloat(value, floatFormat, floatPrecision, floatBitCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *histogram) MarshalJSON() ([]byte, error) {
|
|
||||||
h.Purge()
|
|
||||||
|
|
||||||
h.mutex.RLock()
|
|
||||||
defer h.mutex.RUnlock()
|
|
||||||
|
|
||||||
values := make([]map[string]interface{}, 0, len(h.values))
|
|
||||||
|
|
||||||
for signature, value := range h.values {
|
|
||||||
percentiles := make(map[string]float64, len(h.reportablePercentiles))
|
|
||||||
|
|
||||||
for _, percentile := range h.reportablePercentiles {
|
|
||||||
formatted := formatFloat(percentile)
|
|
||||||
percentiles[formatted] = h.percentile(signature, percentile)
|
|
||||||
}
|
|
||||||
|
|
||||||
values = append(values, map[string]interface{}{
|
|
||||||
labelsKey: value.labels,
|
|
||||||
valueKey: percentiles,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(map[string]interface{}{
|
|
||||||
typeKey: histogramTypeValue,
|
|
||||||
valueKey: values,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *histogram) Purge() {
|
|
||||||
if h.purgeInterval == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.mutex.Lock()
|
|
||||||
defer h.mutex.Unlock()
|
|
||||||
if time.Since(h.lastPurge) < h.purgeInterval {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.resetAll()
|
|
||||||
h.lastPurge = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *histogram) Reset(labels map[string]string) {
|
|
||||||
signature := model.LabelValuesToSignature(labels)
|
|
||||||
|
|
||||||
h.mutex.Lock()
|
|
||||||
defer h.mutex.Unlock()
|
|
||||||
|
|
||||||
value, ok := h.values[signature]
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bucket := range value.buckets {
|
|
||||||
bucket.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(h.values, signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *histogram) ResetAll() {
|
|
||||||
h.mutex.Lock()
|
|
||||||
defer h.mutex.Unlock()
|
|
||||||
|
|
||||||
h.resetAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *histogram) resetAll() {
|
|
||||||
for signature, value := range h.values {
|
|
||||||
for _, bucket := range value.buckets {
|
|
||||||
bucket.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(h.values, signature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Produce a histogram from a given specification.
|
|
||||||
func NewHistogram(specification *HistogramSpecification) Histogram {
|
|
||||||
metric := &histogram{
|
|
||||||
bucketMaker: specification.BucketBuilder,
|
|
||||||
bucketStarts: specification.Starts,
|
|
||||||
reportablePercentiles: specification.ReportablePercentiles,
|
|
||||||
values: map[uint64]*histogramVector{},
|
|
||||||
lastPurge: time.Now(),
|
|
||||||
purgeInterval: specification.PurgeInterval,
|
|
||||||
}
|
|
||||||
|
|
||||||
return metric
|
|
||||||
}
|
|
||||||
|
|
||||||
// Furnish a Histogram with unsensible default values and behaviors that is
|
|
||||||
// strictly useful for prototyping purposes.
|
|
||||||
func NewDefaultHistogram() Histogram {
|
|
||||||
return NewHistogram(
|
|
||||||
&HistogramSpecification{
|
|
||||||
Starts: LogarithmicSizedBucketsFor(0, 4096),
|
|
||||||
BucketBuilder: AccumulatingBucketBuilder(EvictAndReplaceWith(10, AverageReducer), 50),
|
|
||||||
ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.90, 0.99},
|
|
||||||
PurgeInterval: 15 * time.Minute,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *histogram) dumpChildren(f *dto.MetricFamily) {
|
|
||||||
metric.Purge()
|
|
||||||
|
|
||||||
metric.mutex.RLock()
|
|
||||||
defer metric.mutex.RUnlock()
|
|
||||||
|
|
||||||
f.Type = dto.MetricType_SUMMARY.Enum()
|
|
||||||
|
|
||||||
for signature, child := range metric.values {
|
|
||||||
c := &dto.Summary{
|
|
||||||
SampleSum: proto.Float64(child.sum),
|
|
||||||
SampleCount: proto.Uint64(child.count),
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &dto.Metric{
|
|
||||||
Summary: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, value := range child.labels {
|
|
||||||
p := &dto.LabelPair{
|
|
||||||
Name: proto.String(name),
|
|
||||||
Value: proto.String(value),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Label = append(m.Label, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, percentile := range metric.reportablePercentiles {
|
|
||||||
q := &dto.Quantile{
|
|
||||||
Quantile: proto.Float64(percentile),
|
|
||||||
Value: proto.Float64(metric.percentile(signature, percentile)),
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Quantile = append(c.Quantile, q)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Metric = append(f.Metric, m)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// TODO(matt): Re-Add tests for this type.
|
|
|
@ -0,0 +1,287 @@
|
||||||
|
// 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 prometheus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
instLabels = []string{"handler", "method", "code"}
|
||||||
|
|
||||||
|
reqCnt = NewCounterVec(
|
||||||
|
CounterOpts{
|
||||||
|
Subsystem: "http",
|
||||||
|
Name: "requests_total",
|
||||||
|
Help: "Total number of HTTP requests made.",
|
||||||
|
},
|
||||||
|
instLabels,
|
||||||
|
)
|
||||||
|
|
||||||
|
reqDur = NewSummaryVec(
|
||||||
|
SummaryOpts{
|
||||||
|
Subsystem: "http",
|
||||||
|
Name: "request_duration_microseconds",
|
||||||
|
Help: "The HTTP request latencies in microseconds.",
|
||||||
|
},
|
||||||
|
instLabels,
|
||||||
|
)
|
||||||
|
|
||||||
|
reqSz = NewSummaryVec(
|
||||||
|
SummaryOpts{
|
||||||
|
Subsystem: "http",
|
||||||
|
Name: "request_size_bytes",
|
||||||
|
Help: "The HTTP request sizes in bytes.",
|
||||||
|
},
|
||||||
|
instLabels,
|
||||||
|
)
|
||||||
|
|
||||||
|
resSz = NewSummaryVec(
|
||||||
|
SummaryOpts{
|
||||||
|
Subsystem: "http",
|
||||||
|
Name: "response_size_bytes",
|
||||||
|
Help: "The HTTP response sizes in bytes.",
|
||||||
|
},
|
||||||
|
instLabels,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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 vector collectors (if not already done) and reports
|
||||||
|
// http metrics to the (newly or already) registered collectors:
|
||||||
|
// http_requests_total (CounterVec), http_request_duration_microseconds
|
||||||
|
// (SummaryVec), http_request_size_bytes (SummaryVec), http_response_size_bytes
|
||||||
|
// (SummaryVec). Each has three labels: handler, method, code. The value of the
|
||||||
|
// handler label is set by the handlerName parameter of this function.
|
||||||
|
func InstrumentHandler(handlerName string, handler http.Handler) http.HandlerFunc {
|
||||||
|
regReqCnt := MustRegisterOrGet(reqCnt).(*CounterVec)
|
||||||
|
regReqDur := MustRegisterOrGet(reqDur).(*SummaryVec)
|
||||||
|
regReqSz := MustRegisterOrGet(reqSz).(*SummaryVec)
|
||||||
|
regResSz := MustRegisterOrGet(resSz).(*SummaryVec)
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
delegate := &responseWriterDelegator{ResponseWriter: w}
|
||||||
|
out := make(chan int)
|
||||||
|
go computeApproximateRequestSize(r, out)
|
||||||
|
handler.ServeHTTP(delegate, r)
|
||||||
|
|
||||||
|
elapsed := float64(time.Since(now)) / float64(time.Second)
|
||||||
|
|
||||||
|
method := sanitizeMethod(r.Method)
|
||||||
|
code := sanitizeCode(delegate.status)
|
||||||
|
regReqCnt.WithLabelValues(handlerName, method, code).Inc()
|
||||||
|
regReqDur.WithLabelValues(handlerName, method, code).Observe(elapsed)
|
||||||
|
regResSz.WithLabelValues(handlerName, method, code).Observe(float64(delegate.written))
|
||||||
|
regReqSz.WithLabelValues(handlerName, method, code).Observe(float64(<-out))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeApproximateRequestSize(r *http.Request, out chan int) {
|
||||||
|
s := len(r.Method)
|
||||||
|
if r.URL != nil {
|
||||||
|
s += len(r.URL.String())
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
// 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 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)
|
||||||
|
reqCnt.Reset()
|
||||||
|
reqDur.Reset()
|
||||||
|
reqSz.Reset()
|
||||||
|
resSz.Reset()
|
||||||
|
|
||||||
|
respBody := respBody("Howdy there!")
|
||||||
|
|
||||||
|
hndlr := InstrumentHandler("test-handler", respBody)
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if want, got := 1, len(reqDur.children); want != got {
|
||||||
|
t.Errorf("want %d children in reqDur, got %d", want, got)
|
||||||
|
}
|
||||||
|
sum, err := reqDur.GetMetricWithLabelValues("test-handler", "get", "418")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out := &dto.Metric{}
|
||||||
|
sum.Write(out)
|
||||||
|
if want, got := "418", out.Label[0].GetValue(); want != got {
|
||||||
|
t.Errorf("want label value %q in reqDur, got %q", want, got)
|
||||||
|
}
|
||||||
|
if want, got := "test-handler", out.Label[1].GetValue(); want != got {
|
||||||
|
t.Errorf("want label value %q in reqDur, got %q", want, got)
|
||||||
|
}
|
||||||
|
if want, got := "get", out.Label[2].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("test-handler", "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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,146 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright 2014 Prometheus Team
|
||||||
// All rights reserved.
|
// 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
|
||||||
//
|
//
|
||||||
// Use of this source code is governed by a BSD-style
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
// license that can be found in the LICENSE file.
|
//
|
||||||
|
// 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
|
package prometheus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"strings"
|
||||||
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A Metric is something that can be exposed via the registry framework.
|
// 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 {
|
type Metric interface {
|
||||||
// Produce a JSON representation of the metric.
|
// Desc returns the descriptor for the Metric. This method idempotently
|
||||||
json.Marshaler
|
// returns the same descriptor throughout the lifetime of the
|
||||||
// Reset removes any stored values associated with a given labelset.
|
// Metric. The returned descriptor is immutable by contract.
|
||||||
Reset(labels map[string]string)
|
Desc() *Desc
|
||||||
// Reset the parent metrics and delete all child metrics.
|
// Write encodes the Metric into a "Metric" Protocol Buffer data
|
||||||
ResetAll()
|
// transmission object.
|
||||||
// Produce a human-consumable representation of the metric.
|
//
|
||||||
String() string
|
// Implementers of custom Metric types must observe concurrency safety
|
||||||
// dumpChildren populates the child metrics of the given family.
|
// as reads of this metric may occur at any time, and any blocking
|
||||||
dumpChildren(*dto.MetricFamily)
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
// 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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,48 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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
|
|
||||||
|
|
||||||
type item struct {
|
|
||||||
Priority int64
|
|
||||||
Value interface{}
|
|
||||||
index int
|
|
||||||
}
|
|
||||||
|
|
||||||
type priorityQueue []*item
|
|
||||||
|
|
||||||
func (q priorityQueue) Len() int {
|
|
||||||
return len(q)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q priorityQueue) Less(i, j int) bool {
|
|
||||||
return q[i].Priority > q[j].Priority
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q priorityQueue) Swap(i, j int) {
|
|
||||||
q[i], q[j] = q[j], q[i]
|
|
||||||
q[i].index = i
|
|
||||||
q[j].index = j
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *priorityQueue) Push(x interface{}) {
|
|
||||||
queue := *q
|
|
||||||
size := len(queue)
|
|
||||||
queue = queue[0 : size+1]
|
|
||||||
item := x.(*item)
|
|
||||||
item.index = size
|
|
||||||
queue[size] = item
|
|
||||||
*q = queue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *priorityQueue) Pop() interface{} {
|
|
||||||
queue := *q
|
|
||||||
size := len(queue)
|
|
||||||
item := queue[size-1]
|
|
||||||
item.index = -1
|
|
||||||
*q = queue[0 : size-1]
|
|
||||||
return item
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
"container/heap"
|
|
||||||
. "github.com/matttproud/gocheck"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *S) TestPriorityQueueSort(c *C) {
|
|
||||||
q := make(priorityQueue, 0, 6)
|
|
||||||
|
|
||||||
c.Check(len(q), Equals, 0)
|
|
||||||
|
|
||||||
heap.Push(&q, &item{Value: "newest", Priority: -100})
|
|
||||||
heap.Push(&q, &item{Value: "older", Priority: 90})
|
|
||||||
heap.Push(&q, &item{Value: "oldest", Priority: 100})
|
|
||||||
heap.Push(&q, &item{Value: "newer", Priority: -90})
|
|
||||||
heap.Push(&q, &item{Value: "new", Priority: -80})
|
|
||||||
heap.Push(&q, &item{Value: "old", Priority: 80})
|
|
||||||
|
|
||||||
c.Check(len(q), Equals, 6)
|
|
||||||
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, "oldest")
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, "older")
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, "old")
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, "new")
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, "newer")
|
|
||||||
c.Check(heap.Pop(&q), ValueEquals, "newest")
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
. "github.com/matttproud/gocheck"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type S struct{}
|
|
||||||
|
|
||||||
var _ = Suite(&S{})
|
|
||||||
|
|
||||||
func TestPrometheus(t *testing.T) {
|
|
||||||
TestingT(t)
|
|
||||||
}
|
|
|
@ -1,3 +1,16 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright (c) 2013, Prometheus Team
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
//
|
//
|
||||||
|
@ -7,370 +20,610 @@
|
||||||
package prometheus
|
package prometheus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"errors"
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
|
||||||
"code.google.com/p/goprotobuf/proto"
|
"code.google.com/p/goprotobuf/proto"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/model"
|
"github.com/prometheus/client_golang/_vendor/goautoneg"
|
||||||
"github.com/prometheus/client_golang/text"
|
"github.com/prometheus/client_golang/text"
|
||||||
"github.com/prometheus/client_golang/vendor/goautoneg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defRegistry = newRegistry()
|
||||||
|
errAlreadyReg = errors.New("duplicate metrics collector registration attempted")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants relevant to the HTTP interface.
|
||||||
const (
|
const (
|
||||||
authorization = "Authorization"
|
// APIVersion is the version of the format of the exported data. This
|
||||||
authorizationHeader = "WWW-Authenticate"
|
// will match this library's version, which subscribes to the Semantic
|
||||||
authorizationHeaderValue = "Basic"
|
// Versioning scheme.
|
||||||
|
APIVersion = "0.0.4"
|
||||||
|
|
||||||
acceptEncodingHeader = "Accept-Encoding"
|
// DelimitedTelemetryContentType is the content type set on telemetry
|
||||||
contentEncodingHeader = "Content-Encoding"
|
// data responses in delimited protobuf format.
|
||||||
contentTypeHeader = "Content-Type"
|
DelimitedTelemetryContentType = `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="delimited"`
|
||||||
gzipAcceptEncodingValue = "gzip"
|
// TextTelemetryContentType is the content type set on telemetry data
|
||||||
gzipContentEncodingValue = "gzip"
|
// responses in text format.
|
||||||
jsonContentType = "application/json"
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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. If the registration is successful, the registered Collector
|
||||||
|
// is returned.
|
||||||
|
//
|
||||||
|
// 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) (Collector, error) {
|
||||||
|
return defRegistry.Register(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustRegister works like Register but panics where Register would have
|
||||||
|
// returned an error.
|
||||||
|
func MustRegister(m Collector) Collector {
|
||||||
|
m, err := Register(m)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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. By default, 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
|
||||||
|
}
|
||||||
|
|
||||||
// encoder is a function that writes a dto.MetricFamily to an io.Writer in a
|
// 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
|
// certain encoding. It returns the number of bytes written and any error
|
||||||
// encountered. Note that ext.WriteDelimited and text.MetricFamilyToText are
|
// encountered. Note that ext.WriteDelimited and text.MetricFamilyToText are
|
||||||
// encoders.
|
// encoders.
|
||||||
type encoder func(io.Writer, *dto.MetricFamily) (int, error)
|
type encoder func(io.Writer, *dto.MetricFamily) (int, error)
|
||||||
|
|
||||||
// container represents a top-level registered metric that encompasses its
|
|
||||||
// static metadata.
|
|
||||||
type container struct {
|
|
||||||
BaseLabels map[string]string `json:"baseLabels"`
|
|
||||||
Docstring string `json:"docstring"`
|
|
||||||
Metric Metric `json:"metric"`
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type containers []*container
|
|
||||||
|
|
||||||
func (c containers) Len() int {
|
|
||||||
return len(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c containers) Swap(i, j int) {
|
|
||||||
c[i], c[j] = c[j], c[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c containers) Less(i, j int) bool {
|
|
||||||
return c[i].name < c[j].name
|
|
||||||
}
|
|
||||||
|
|
||||||
type registry struct {
|
type registry struct {
|
||||||
mutex sync.RWMutex
|
mtx sync.RWMutex
|
||||||
signatureContainers map[uint64]*container
|
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
|
metricFamilyInjectionHook func() []*dto.MetricFamily
|
||||||
|
|
||||||
|
panicOnCollectError, collectChecksEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry is a registrar where metrics are listed.
|
func (r *registry) Register(c Collector) (Collector, error) {
|
||||||
//
|
descChan := make(chan *Desc, capDescChan)
|
||||||
// In most situations, using DefaultRegistry is sufficient versus creating one's
|
go func() {
|
||||||
// own.
|
c.Describe(descChan)
|
||||||
type Registry interface {
|
close(descChan)
|
||||||
// Register a metric with a given name. Name should be globally unique.
|
}()
|
||||||
Register(name, docstring string, baseLabels map[string]string, metric Metric) error
|
|
||||||
// SetMetricFamilyInjectionHook sets a function that is called whenever
|
|
||||||
// metrics are requested. The MetricsFamily protobufs returned by the
|
|
||||||
// function are appended to the delivered metrics. 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. The function must be callable at
|
|
||||||
// any time and concurrently. The only thing handled by the Registry is
|
|
||||||
// the conversion if metrics are requested in a non-protobuf format. The
|
|
||||||
// deprecated JSON format, however, is not supported, i.e. metrics
|
|
||||||
// delivered as JSON will not contain the metrics injected by the
|
|
||||||
// injection hook.
|
|
||||||
SetMetricFamilyInjectionHook(func() []*dto.MetricFamily)
|
|
||||||
// Handler creates a http.HandlerFunc. Requests against it generate a
|
|
||||||
// representation of the metrics managed by this registry.
|
|
||||||
Handler() http.HandlerFunc
|
|
||||||
// YieldExporter is a legacy version of Handler and is deprecated.
|
|
||||||
// Please stop using.
|
|
||||||
YieldExporter() http.HandlerFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRegistry builds a new metric registry. It is not needed in the majority
|
newDescIDs := map[uint64]struct{}{}
|
||||||
// of cases.
|
newDimHashesByName := map[string]uint64{}
|
||||||
func NewRegistry() Registry {
|
collectorIDHash := fnv.New64a()
|
||||||
return ®istry{
|
buf := make([]byte, 8)
|
||||||
signatureContainers: make(map[uint64]*container),
|
var duplicateDescErr error
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register associates a Metric with the DefaultRegistry.
|
r.mtx.Lock()
|
||||||
func Register(name, docstring string, baseLabels map[string]string, metric Metric) error {
|
defer r.mtx.Unlock()
|
||||||
return DefaultRegistry.Register(name, docstring, baseLabels, metric)
|
// Coduct various tests...
|
||||||
}
|
for desc := range descChan {
|
||||||
|
|
||||||
// SetMetricFamilyInjectionHook implements the Registry interface.
|
// Is the descriptor valid at all?
|
||||||
func (r *registry) SetMetricFamilyInjectionHook(hook func() []*dto.MetricFamily) {
|
if desc.err != nil {
|
||||||
r.metricFamilyInjectionHook = hook
|
return c, fmt.Errorf("descriptor %s is invalid: %s", desc, desc.err)
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON implements json.Marshaler.
|
|
||||||
func (r *registry) MarshalJSON() ([]byte, error) {
|
|
||||||
containers := make(containers, 0, len(r.signatureContainers))
|
|
||||||
|
|
||||||
for _, container := range r.signatureContainers {
|
|
||||||
containers = append(containers, container)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(containers)
|
|
||||||
|
|
||||||
return json.Marshal(containers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidCandidate returns true if the candidate is acceptable for use. In the
|
|
||||||
// event of any apparent incorrect use it will report the problem, invalidate
|
|
||||||
// the candidate, or outright abort.
|
|
||||||
func (r *registry) isValidCandidate(name string, baseLabels map[string]string) (signature uint64, err error) {
|
|
||||||
if len(name) == 0 {
|
|
||||||
err = fmt.Errorf("unnamed metric named with baseLabels %s is invalid", baseLabels)
|
|
||||||
|
|
||||||
if *abortOnMisuse {
|
|
||||||
panic(err)
|
|
||||||
} else if *debugRegistration {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for label := range baseLabels {
|
// Is the descID unique?
|
||||||
if strings.HasPrefix(label, model.ReservedLabelPrefix) {
|
// (In other words: Is the fqName + constLabel combination unique?)
|
||||||
err = fmt.Errorf("metric named %s with baseLabels %s contains reserved label name %s in baseLabels", name, baseLabels, label)
|
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 hash. (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{}{}
|
||||||
|
binary.BigEndian.PutUint64(buf, desc.id)
|
||||||
|
collectorIDHash.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
if *abortOnMisuse {
|
// Are all the label names and the help string consistent with
|
||||||
panic(err)
|
// previous descriptors of the same name?
|
||||||
} else if *debugRegistration {
|
// First check existing descriptors...
|
||||||
log.Println(err)
|
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 {
|
||||||
return signature, err
|
// ...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)
|
||||||
baseLabels[string(model.MetricNameLabel)] = name
|
|
||||||
signature = model.LabelsToSignature(baseLabels)
|
|
||||||
|
|
||||||
if _, contains := r.signatureContainers[signature]; contains {
|
|
||||||
err = fmt.Errorf("metric named %s with baseLabels %s is already registered", name, baseLabels)
|
|
||||||
if *abortOnMisuse {
|
|
||||||
panic(err)
|
|
||||||
} else if *debugRegistration {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return signature, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if *useAggressiveSanityChecks {
|
|
||||||
for _, container := range r.signatureContainers {
|
|
||||||
if container.name == name {
|
|
||||||
err = fmt.Errorf("metric named %s with baseLabels %s is already registered as %s and risks causing confusion", name, baseLabels, container.BaseLabels)
|
|
||||||
if *abortOnMisuse {
|
|
||||||
panic(err)
|
|
||||||
} else if *debugRegistration {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return signature, err
|
newDimHashesByName[desc.fqName] = desc.dimHash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Did anything happen at all?
|
||||||
|
if len(newDescIDs) == 0 {
|
||||||
|
return nil, errors.New("collector has no descriptors")
|
||||||
|
}
|
||||||
|
collectorID := collectorIDHash.Sum64()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
return signature, err
|
// 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) Register(name, docstring string, baseLabels map[string]string, metric Metric) error {
|
func (r *registry) RegisterOrGet(m Collector) (Collector, error) {
|
||||||
r.mutex.Lock()
|
existing, err := r.Register(m)
|
||||||
defer r.mutex.Unlock()
|
if err != nil && err != errAlreadyReg {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
|
||||||
labels := map[string]string{}
|
func (r *registry) Unregister(c Collector) bool {
|
||||||
|
descChan := make(chan *Desc, capDescChan)
|
||||||
|
go func() {
|
||||||
|
c.Describe(descChan)
|
||||||
|
close(descChan)
|
||||||
|
}()
|
||||||
|
|
||||||
if baseLabels != nil {
|
descIDs := map[uint64]struct{}{}
|
||||||
for k, v := range baseLabels {
|
collectorIDHash := fnv.New64a()
|
||||||
labels[k] = v
|
buf := make([]byte, 8)
|
||||||
|
for desc := range descChan {
|
||||||
|
if _, exists := descIDs[desc.id]; !exists {
|
||||||
|
binary.BigEndian.PutUint64(buf, desc.id)
|
||||||
|
collectorIDHash.Write(buf)
|
||||||
|
descIDs[desc.id] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectorID := collectorIDHash.Sum64()
|
||||||
|
|
||||||
|
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) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
enc, contentType := chooseEncoder(req)
|
||||||
|
buf := r.getBuf()
|
||||||
|
defer r.giveBuf(buf)
|
||||||
|
header := w.Header()
|
||||||
|
if _, err := r.writePB(buf, enc); err != nil {
|
||||||
|
if r.panicOnCollectError {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
header.Set(contentTypeHeader, "text/plain")
|
||||||
|
fmt.Fprintf(w, "An error has occurred:\n\n%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header.Set(contentTypeHeader, contentType)
|
||||||
|
w.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) writePB(w io.Writer, writeEncoded encoder) (int, error) {
|
||||||
|
metricFamiliesByName := make(map[string]*dto.MetricFamily, len(r.dimHashesByName))
|
||||||
|
var metricHashes map[uint64]struct{}
|
||||||
|
if r.collectChecksEnabled {
|
||||||
|
metricHashes = make(map[uint64]struct{})
|
||||||
|
}
|
||||||
|
metricChan := make(chan Metric, capMetricChan)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
// Scatter.
|
||||||
|
// (Collectors could be complex and slow, so we call them all at once.)
|
||||||
|
r.mtx.RLock()
|
||||||
|
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()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
metric.Write(dtoMetric)
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signature, err := r.isValidCandidate(name, labels)
|
// Now that MetricFamilies are all set, sort their Metrics
|
||||||
if err != nil {
|
// lexicographically by their label values.
|
||||||
return err
|
for _, mf := range metricFamiliesByName {
|
||||||
|
sort.Sort(metricSorter(mf.Metric))
|
||||||
}
|
}
|
||||||
|
|
||||||
r.signatureContainers[signature] = &container{
|
// Write out MetricFamilies sorted by their name.
|
||||||
BaseLabels: labels,
|
names := make([]string, 0, len(metricFamiliesByName))
|
||||||
Docstring: docstring,
|
for name := range metricFamiliesByName {
|
||||||
Metric: metric,
|
names = append(names, name)
|
||||||
name: 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)
|
||||||
|
h.Write(buf.Bytes())
|
||||||
|
for _, lp := range dtoMetric.Label {
|
||||||
|
buf.Reset()
|
||||||
|
buf.WriteString(lp.GetValue())
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// YieldBasicAuthExporter creates a http.HandlerFunc that is protected by HTTP's
|
func (r *registry) getBuf() *bytes.Buffer {
|
||||||
// basic authentication.
|
select {
|
||||||
func (r *registry) YieldBasicAuthExporter(username, password string) http.HandlerFunc {
|
case buf := <-r.bufPool:
|
||||||
// XXX: Work with Daniel to get this removed from the library, as it is really
|
return buf
|
||||||
// superfluous and can be much more elegantly accomplished via
|
default:
|
||||||
// delegation.
|
return &bytes.Buffer{}
|
||||||
log.Println("Registry.YieldBasicAuthExporter is deprecated.")
|
|
||||||
exporter := r.YieldExporter()
|
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
authenticated := false
|
|
||||||
|
|
||||||
if auth := r.Header.Get(authorization); auth != "" {
|
|
||||||
base64Encoded := strings.SplitAfter(auth, " ")[1]
|
|
||||||
decoded, err := base64.URLEncoding.DecodeString(base64Encoded)
|
|
||||||
if err == nil {
|
|
||||||
usernamePassword := strings.Split(string(decoded), ":")
|
|
||||||
if usernamePassword[0] == username && usernamePassword[1] == password {
|
|
||||||
authenticated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if authenticated {
|
|
||||||
exporter.ServeHTTP(w, r)
|
|
||||||
} else {
|
|
||||||
w.Header().Add(authorizationHeader, authorizationHeaderValue)
|
|
||||||
http.Error(w, "access forbidden", 401)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorateWriter annotates the response writer to handle any other behaviors
|
|
||||||
// that might be beneficial to the client---e.g., GZIP encoding.
|
|
||||||
func decorateWriter(request *http.Request, writer http.ResponseWriter) io.Writer {
|
|
||||||
if !strings.Contains(request.Header.Get(acceptEncodingHeader), gzipAcceptEncodingValue) {
|
|
||||||
return writer
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Header().Set(contentEncodingHeader, gzipContentEncodingValue)
|
|
||||||
gziper := gzip.NewWriter(writer)
|
|
||||||
|
|
||||||
return gziper
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registry) YieldExporter() http.HandlerFunc {
|
|
||||||
log.Println("Registry.YieldExporter is deprecated in favor of Registry.Handler.")
|
|
||||||
|
|
||||||
return r.Handler()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *registry) dumpPB(w io.Writer, writeEncoded encoder) {
|
|
||||||
r.mutex.RLock()
|
|
||||||
defer r.mutex.RUnlock()
|
|
||||||
|
|
||||||
f := new(dto.MetricFamily)
|
|
||||||
for _, container := range r.signatureContainers {
|
|
||||||
f.Reset()
|
|
||||||
|
|
||||||
f.Name = proto.String(container.name)
|
|
||||||
f.Help = proto.String(container.Docstring)
|
|
||||||
|
|
||||||
container.Metric.dumpChildren(f)
|
|
||||||
|
|
||||||
for name, value := range container.BaseLabels {
|
|
||||||
if model.LabelName(name) == model.MetricNameLabel {
|
|
||||||
// The name is already in MetricFamily.
|
|
||||||
continue
|
|
||||||
// TODO: Once JSON is history, do not anymore
|
|
||||||
// add the __name__ label to BaseLabels and
|
|
||||||
// then remove this check.
|
|
||||||
}
|
|
||||||
p := &dto.LabelPair{
|
|
||||||
Name: proto.String(name),
|
|
||||||
Value: proto.String(value),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, child := range f.Metric {
|
|
||||||
child.Label = append(child.Label, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeEncoded(w, f)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registry) dumpExternalPB(w io.Writer, writeEncoded encoder) {
|
func (r *registry) giveBuf(buf *bytes.Buffer) {
|
||||||
if r.metricFamilyInjectionHook == nil {
|
buf.Reset()
|
||||||
return
|
select {
|
||||||
}
|
case r.bufPool <- buf:
|
||||||
for _, f := range r.metricFamilyInjectionHook() {
|
default:
|
||||||
writeEncoded(w, f)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registry) Handler() http.HandlerFunc {
|
func (r *registry) getMetricFamily() *dto.MetricFamily {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
select {
|
||||||
defer requestLatencyAccumulator(time.Now())
|
case mf := <-r.metricFamilyPool:
|
||||||
|
return mf
|
||||||
|
default:
|
||||||
|
return &dto.MetricFamily{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
requestCount.Increment(nil)
|
func (r *registry) giveMetricFamily(mf *dto.MetricFamily) {
|
||||||
header := w.Header()
|
mf.Reset()
|
||||||
|
select {
|
||||||
|
case r.metricFamilyPool <- mf:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writer := decorateWriter(req, w)
|
func (r *registry) getMetric() *dto.Metric {
|
||||||
|
select {
|
||||||
|
case m := <-r.metricPool:
|
||||||
|
return m
|
||||||
|
default:
|
||||||
|
return &dto.Metric{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if closer, ok := writer.(io.Closer); ok {
|
func (r *registry) giveMetric(m *dto.Metric) {
|
||||||
defer closer.Close()
|
m.Reset()
|
||||||
}
|
select {
|
||||||
|
case r.metricPool <- m:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
accepts := goautoneg.ParseAccept(req.Header.Get("Accept"))
|
func newRegistry() *registry {
|
||||||
for _, accept := range accepts {
|
return ®istry{
|
||||||
var enc encoder
|
collectorsByID: map[uint64]Collector{},
|
||||||
switch {
|
descIDs: map[uint64]struct{}{},
|
||||||
case accept.Type == "application" &&
|
dimHashesByName: map[string]uint64{},
|
||||||
accept.SubType == "vnd.google.protobuf" &&
|
bufPool: make(chan *bytes.Buffer, numBufs),
|
||||||
accept.Params["proto"] == "io.prometheus.client.MetricFamily":
|
metricFamilyPool: make(chan *dto.MetricFamily, numMetricFamilies),
|
||||||
switch accept.Params["encoding"] {
|
metricPool: make(chan *dto.Metric, numMetrics),
|
||||||
case "delimited":
|
}
|
||||||
header.Set(contentTypeHeader, DelimitedTelemetryContentType)
|
}
|
||||||
enc = text.WriteProtoDelimited
|
|
||||||
case "text":
|
func chooseEncoder(req *http.Request) (encoder, string) {
|
||||||
header.Set(contentTypeHeader, ProtoTextTelemetryContentType)
|
accepts := goautoneg.ParseAccept(req.Header.Get("Accept"))
|
||||||
enc = text.WriteProtoText
|
for _, accept := range accepts {
|
||||||
case "compact-text":
|
switch {
|
||||||
header.Set(contentTypeHeader, ProtoCompactTextTelemetryContentType)
|
case accept.Type == "application" &&
|
||||||
enc = text.WriteProtoCompactText
|
accept.SubType == "vnd.google.protobuf" &&
|
||||||
default:
|
accept.Params["proto"] == "io.prometheus.client.MetricFamily":
|
||||||
continue
|
switch accept.Params["encoding"] {
|
||||||
}
|
case "delimited":
|
||||||
case accept.Type == "text" &&
|
return text.WriteProtoDelimited, DelimitedTelemetryContentType
|
||||||
accept.SubType == "plain" &&
|
case "text":
|
||||||
(accept.Params["version"] == "0.0.4" || accept.Params["version"] == ""):
|
return text.WriteProtoText, ProtoTextTelemetryContentType
|
||||||
header.Set(contentTypeHeader, TextTelemetryContentType)
|
case "compact-text":
|
||||||
enc = text.MetricFamilyToText
|
return text.WriteProtoCompactText, ProtoCompactTextTelemetryContentType
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
r.dumpPB(writer, enc)
|
case accept.Type == "text" &&
|
||||||
r.dumpExternalPB(writer, enc)
|
accept.SubType == "plain" &&
|
||||||
return
|
(accept.Params["version"] == "0.0.4" || accept.Params["version"] == ""):
|
||||||
|
return text.MetricFamilyToText, TextTelemetryContentType
|
||||||
|
default:
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
// TODO: Once JSON deprecation is completed, use text format as
|
|
||||||
// fall-back.
|
|
||||||
header.Set(contentTypeHeader, JSONTelemetryContentType)
|
|
||||||
json.NewEncoder(writer).Encode(r)
|
|
||||||
}
|
}
|
||||||
|
return text.MetricFamilyToText, TextTelemetryContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
type metricSorter []*dto.Metric
|
||||||
abortOnMisuse = flag.Bool(FlagNamespace+"abortonmisuse", false, "abort if a semantic misuse is encountered (bool).")
|
|
||||||
debugRegistration = flag.Bool(FlagNamespace+"debugregistration", false, "display information about the metric registration process (bool).")
|
func (s metricSorter) Len() int {
|
||||||
useAggressiveSanityChecks = flag.Bool(FlagNamespace+"useaggressivesanitychecks", false, "perform expensive validation of metrics (bool).")
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,16 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright (c) 2013, Prometheus Team
|
||||||
// All rights reserved.
|
// All rights reserved.
|
||||||
//
|
//
|
||||||
|
@ -9,187 +22,13 @@ package prometheus
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
|
||||||
|
|
||||||
"code.google.com/p/goprotobuf/proto"
|
"code.google.com/p/goprotobuf/proto"
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
"github.com/prometheus/client_golang/model"
|
|
||||||
"github.com/prometheus/client_golang/test"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRegister(t test.Tester) {
|
|
||||||
var oldState = struct {
|
|
||||||
abortOnMisuse bool
|
|
||||||
debugRegistration bool
|
|
||||||
useAggressiveSanityChecks bool
|
|
||||||
}{
|
|
||||||
abortOnMisuse: *abortOnMisuse,
|
|
||||||
debugRegistration: *debugRegistration,
|
|
||||||
useAggressiveSanityChecks: *useAggressiveSanityChecks,
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
abortOnMisuse = &(oldState.abortOnMisuse)
|
|
||||||
debugRegistration = &(oldState.debugRegistration)
|
|
||||||
useAggressiveSanityChecks = &(oldState.useAggressiveSanityChecks)
|
|
||||||
}()
|
|
||||||
|
|
||||||
type input struct {
|
|
||||||
name string
|
|
||||||
baseLabels map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
validLabels := map[string]string{"label": "value"}
|
|
||||||
|
|
||||||
var scenarios = []struct {
|
|
||||||
inputs []input
|
|
||||||
outputs []bool
|
|
||||||
}{
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
inputs: []input{
|
|
||||||
{
|
|
||||||
name: "my_name_without_labels",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: []bool{
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
inputs: []input{
|
|
||||||
{
|
|
||||||
name: "my_name_without_labels",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "another_name_without_labels",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: []bool{
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
inputs: []input{
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: []bool{
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
inputs: []input{
|
|
||||||
{
|
|
||||||
name: "valid_name",
|
|
||||||
baseLabels: map[string]string{model.ReservedLabelPrefix + "internal": "illegal_internal_name"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: []bool{
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
inputs: []input{
|
|
||||||
{
|
|
||||||
name: "duplicate_names",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate_names",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: []bool{
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
inputs: []input{
|
|
||||||
{
|
|
||||||
name: "duplicate_names_with_identical_labels",
|
|
||||||
baseLabels: map[string]string{"label": "value"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate_names_with_identical_labels",
|
|
||||||
baseLabels: map[string]string{"label": "value"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: []bool{
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
inputs: []input{
|
|
||||||
{
|
|
||||||
name: "metric_1_with_identical_labels",
|
|
||||||
baseLabels: validLabels,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metric_2_with_identical_labels",
|
|
||||||
baseLabels: validLabels,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: []bool{
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
inputs: []input{
|
|
||||||
{
|
|
||||||
name: "duplicate_names_with_dissimilar_labels",
|
|
||||||
baseLabels: map[string]string{"label": "foo"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate_names_with_dissimilar_labels",
|
|
||||||
baseLabels: map[string]string{"label": "bar"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outputs: []bool{
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, scenario := range scenarios {
|
|
||||||
if len(scenario.inputs) != len(scenario.outputs) {
|
|
||||||
t.Fatalf("%d. expected scenario output length %d, got %d", i, len(scenario.inputs), len(scenario.outputs))
|
|
||||||
}
|
|
||||||
|
|
||||||
abortOnMisuse = proto.Bool(false)
|
|
||||||
debugRegistration = proto.Bool(false)
|
|
||||||
useAggressiveSanityChecks = proto.Bool(true)
|
|
||||||
|
|
||||||
registry := NewRegistry()
|
|
||||||
|
|
||||||
for j, input := range scenario.inputs {
|
|
||||||
actual := registry.Register(input.name, "", input.baseLabels, nil)
|
|
||||||
if scenario.outputs[j] != (actual == nil) {
|
|
||||||
t.Errorf("%d.%d. expected %t, got %t", i, j, scenario.outputs[j], actual == nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegister(t *testing.T) {
|
|
||||||
testRegister(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkRegister(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
testRegister(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeResponseWriter struct {
|
type fakeResponseWriter struct {
|
||||||
header http.Header
|
header http.Header
|
||||||
body bytes.Buffer
|
body bytes.Buffer
|
||||||
|
@ -206,11 +45,19 @@ func (r *fakeResponseWriter) Write(d []byte) (l int, err error) {
|
||||||
func (r *fakeResponseWriter) WriteHeader(c int) {
|
func (r *fakeResponseWriter) WriteHeader(c int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHandler(t test.Tester) {
|
func testHandler(t testing.TB) {
|
||||||
|
|
||||||
metric := NewCounter()
|
metricVec := NewCounterVec(
|
||||||
metric.Increment(map[string]string{"labelname": "val1"})
|
CounterOpts{
|
||||||
metric.Increment(map[string]string{"labelname": "val2"})
|
Name: "name",
|
||||||
|
Help: "docstring",
|
||||||
|
ConstLabels: Labels{"constname": "constvalue"},
|
||||||
|
},
|
||||||
|
[]string{"labelname"},
|
||||||
|
)
|
||||||
|
|
||||||
|
metricVec.WithLabelValues("val1").Inc()
|
||||||
|
metricVec.WithLabelValues("val2").Inc()
|
||||||
|
|
||||||
varintBuf := make([]byte, binary.MaxVarintLen32)
|
varintBuf := make([]byte, binary.MaxVarintLen32)
|
||||||
|
|
||||||
|
@ -227,8 +74,8 @@ func testHandler(t test.Tester) {
|
||||||
Value: proto.String("externalval1"),
|
Value: proto.String("externalval1"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: proto.String("externalbasename"),
|
Name: proto.String("externalconstname"),
|
||||||
Value: proto.String("externalbasevalue"),
|
Value: proto.String("externalconstvalue"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Counter: &dto.Counter{
|
Counter: &dto.Counter{
|
||||||
|
@ -255,7 +102,7 @@ func testHandler(t test.Tester) {
|
||||||
externalMetricFamilyAsBytes := externalBuf.Bytes()
|
externalMetricFamilyAsBytes := externalBuf.Bytes()
|
||||||
externalMetricFamilyAsText := []byte(`# HELP externalname externaldocstring
|
externalMetricFamilyAsText := []byte(`# HELP externalname externaldocstring
|
||||||
# TYPE externalname counter
|
# TYPE externalname counter
|
||||||
externalname{externallabelname="externalval1",externalbasename="externalbasevalue"} 1
|
externalname{externallabelname="externalval1",externalconstname="externalconstvalue"} 1
|
||||||
`)
|
`)
|
||||||
externalMetricFamilyAsProtoText := []byte(`name: "externalname"
|
externalMetricFamilyAsProtoText := []byte(`name: "externalname"
|
||||||
help: "externaldocstring"
|
help: "externaldocstring"
|
||||||
|
@ -266,8 +113,8 @@ metric: <
|
||||||
value: "externalval1"
|
value: "externalval1"
|
||||||
>
|
>
|
||||||
label: <
|
label: <
|
||||||
name: "externalbasename"
|
name: "externalconstname"
|
||||||
value: "externalbasevalue"
|
value: "externalconstvalue"
|
||||||
>
|
>
|
||||||
counter: <
|
counter: <
|
||||||
value: 1
|
value: 1
|
||||||
|
@ -275,7 +122,7 @@ metric: <
|
||||||
>
|
>
|
||||||
|
|
||||||
`)
|
`)
|
||||||
externalMetricFamilyAsProtoCompactText := []byte(`name:"externalname" help:"externaldocstring" type:COUNTER metric:<label:<name:"externallabelname" value:"externalval1" > label:<name:"externalbasename" value:"externalbasevalue" > 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{
|
expectedMetricFamily := &dto.MetricFamily{
|
||||||
|
@ -286,12 +133,12 @@ metric: <
|
||||||
{
|
{
|
||||||
Label: []*dto.LabelPair{
|
Label: []*dto.LabelPair{
|
||||||
{
|
{
|
||||||
Name: proto.String("labelname"),
|
Name: proto.String("constname"),
|
||||||
Value: proto.String("val1"),
|
Value: proto.String("constvalue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: proto.String("basename"),
|
Name: proto.String("labelname"),
|
||||||
Value: proto.String("basevalue"),
|
Value: proto.String("val1"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Counter: &dto.Counter{
|
Counter: &dto.Counter{
|
||||||
|
@ -301,12 +148,12 @@ metric: <
|
||||||
{
|
{
|
||||||
Label: []*dto.LabelPair{
|
Label: []*dto.LabelPair{
|
||||||
{
|
{
|
||||||
Name: proto.String("labelname"),
|
Name: proto.String("constname"),
|
||||||
Value: proto.String("val2"),
|
Value: proto.String("constvalue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: proto.String("basename"),
|
Name: proto.String("labelname"),
|
||||||
Value: proto.String("basevalue"),
|
Value: proto.String("val2"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Counter: &dto.Counter{
|
Counter: &dto.Counter{
|
||||||
|
@ -332,20 +179,20 @@ metric: <
|
||||||
expectedMetricFamilyAsBytes := buf.Bytes()
|
expectedMetricFamilyAsBytes := buf.Bytes()
|
||||||
expectedMetricFamilyAsText := []byte(`# HELP name docstring
|
expectedMetricFamilyAsText := []byte(`# HELP name docstring
|
||||||
# TYPE name counter
|
# TYPE name counter
|
||||||
name{labelname="val1",basename="basevalue"} 1
|
name{constname="constvalue",labelname="val1"} 1
|
||||||
name{labelname="val2",basename="basevalue"} 1
|
name{constname="constvalue",labelname="val2"} 1
|
||||||
`)
|
`)
|
||||||
expectedMetricFamilyAsProtoText := []byte(`name: "name"
|
expectedMetricFamilyAsProtoText := []byte(`name: "name"
|
||||||
help: "docstring"
|
help: "docstring"
|
||||||
type: COUNTER
|
type: COUNTER
|
||||||
metric: <
|
metric: <
|
||||||
label: <
|
label: <
|
||||||
name: "labelname"
|
name: "constname"
|
||||||
value: "val1"
|
value: "constvalue"
|
||||||
>
|
>
|
||||||
label: <
|
label: <
|
||||||
name: "basename"
|
name: "labelname"
|
||||||
value: "basevalue"
|
value: "val1"
|
||||||
>
|
>
|
||||||
counter: <
|
counter: <
|
||||||
value: 1
|
value: 1
|
||||||
|
@ -353,12 +200,12 @@ metric: <
|
||||||
>
|
>
|
||||||
metric: <
|
metric: <
|
||||||
label: <
|
label: <
|
||||||
name: "labelname"
|
name: "constname"
|
||||||
value: "val2"
|
value: "constvalue"
|
||||||
>
|
>
|
||||||
label: <
|
label: <
|
||||||
name: "basename"
|
name: "labelname"
|
||||||
value: "basevalue"
|
value: "val2"
|
||||||
>
|
>
|
||||||
counter: <
|
counter: <
|
||||||
value: 1
|
value: 1
|
||||||
|
@ -366,7 +213,7 @@ metric: <
|
||||||
>
|
>
|
||||||
|
|
||||||
`)
|
`)
|
||||||
expectedMetricFamilyAsProtoCompactText := []byte(`name:"name" help:"docstring" type:COUNTER metric:<label:<name:"labelname" value:"val1" > label:<name:"basename" value:"basevalue" > counter:<value:1 > > metric:<label:<name:"labelname" value:"val2" > label:<name:"basename" value:"basevalue" > 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 {
|
type output struct {
|
||||||
|
@ -386,9 +233,9 @@ metric: <
|
||||||
},
|
},
|
||||||
out: output{
|
out: output{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
"Content-Type": `application/json; schema="prometheus/telemetry"; version=0.0.2`,
|
"Content-Type": `text/plain; version=0.0.4`,
|
||||||
},
|
},
|
||||||
body: []byte("[]\n"),
|
body: []byte{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ // 1
|
{ // 1
|
||||||
|
@ -397,9 +244,9 @@ metric: <
|
||||||
},
|
},
|
||||||
out: output{
|
out: output{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
"Content-Type": `application/json; schema="prometheus/telemetry"; version=0.0.2`,
|
"Content-Type": `text/plain; version=0.0.4`,
|
||||||
},
|
},
|
||||||
body: []byte("[]\n"),
|
body: []byte{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ // 2
|
{ // 2
|
||||||
|
@ -408,9 +255,9 @@ metric: <
|
||||||
},
|
},
|
||||||
out: output{
|
out: output{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
"Content-Type": `application/json; schema="prometheus/telemetry"; version=0.0.2`,
|
"Content-Type": `text/plain; version=0.0.4`,
|
||||||
},
|
},
|
||||||
body: []byte("[]\n"),
|
body: []byte{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ // 3
|
{ // 3
|
||||||
|
@ -430,10 +277,9 @@ metric: <
|
||||||
},
|
},
|
||||||
out: output{
|
out: output{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
"Content-Type": `application/json; schema="prometheus/telemetry"; version=0.0.2`,
|
"Content-Type": `text/plain; version=0.0.4`,
|
||||||
},
|
},
|
||||||
body: []byte(`[{"baseLabels":{"__name__":"name","basename":"basevalue"},"docstring":"docstring","metric":{"type":"counter","value":[{"labels":{"labelname":"val1"},"value":1},{"labels":{"labelname":"val2"},"value":1}]}}]
|
body: expectedMetricFamilyAsText,
|
||||||
`),
|
|
||||||
},
|
},
|
||||||
withCounter: true,
|
withCounter: true,
|
||||||
},
|
},
|
||||||
|
@ -455,9 +301,9 @@ metric: <
|
||||||
},
|
},
|
||||||
out: output{
|
out: output{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
"Content-Type": `application/json; schema="prometheus/telemetry"; version=0.0.2`,
|
"Content-Type": `text/plain; version=0.0.4`,
|
||||||
},
|
},
|
||||||
body: []byte("[]\n"),
|
body: externalMetricFamilyAsText,
|
||||||
},
|
},
|
||||||
withExternalMF: true,
|
withExternalMF: true,
|
||||||
},
|
},
|
||||||
|
@ -483,8 +329,8 @@ metric: <
|
||||||
},
|
},
|
||||||
body: bytes.Join(
|
body: bytes.Join(
|
||||||
[][]byte{
|
[][]byte{
|
||||||
expectedMetricFamilyAsBytes,
|
|
||||||
externalMetricFamilyAsBytes,
|
externalMetricFamilyAsBytes,
|
||||||
|
expectedMetricFamilyAsBytes,
|
||||||
},
|
},
|
||||||
[]byte{},
|
[]byte{},
|
||||||
),
|
),
|
||||||
|
@ -525,8 +371,8 @@ metric: <
|
||||||
},
|
},
|
||||||
body: bytes.Join(
|
body: bytes.Join(
|
||||||
[][]byte{
|
[][]byte{
|
||||||
expectedMetricFamilyAsText,
|
|
||||||
externalMetricFamilyAsText,
|
externalMetricFamilyAsText,
|
||||||
|
expectedMetricFamilyAsText,
|
||||||
},
|
},
|
||||||
[]byte{},
|
[]byte{},
|
||||||
),
|
),
|
||||||
|
@ -544,8 +390,8 @@ metric: <
|
||||||
},
|
},
|
||||||
body: bytes.Join(
|
body: bytes.Join(
|
||||||
[][]byte{
|
[][]byte{
|
||||||
expectedMetricFamilyAsBytes,
|
|
||||||
externalMetricFamilyAsBytes,
|
externalMetricFamilyAsBytes,
|
||||||
|
expectedMetricFamilyAsBytes,
|
||||||
},
|
},
|
||||||
[]byte{},
|
[]byte{},
|
||||||
),
|
),
|
||||||
|
@ -563,8 +409,8 @@ metric: <
|
||||||
},
|
},
|
||||||
body: bytes.Join(
|
body: bytes.Join(
|
||||||
[][]byte{
|
[][]byte{
|
||||||
expectedMetricFamilyAsProtoText,
|
|
||||||
externalMetricFamilyAsProtoText,
|
externalMetricFamilyAsProtoText,
|
||||||
|
expectedMetricFamilyAsProtoText,
|
||||||
},
|
},
|
||||||
[]byte{},
|
[]byte{},
|
||||||
),
|
),
|
||||||
|
@ -582,8 +428,8 @@ metric: <
|
||||||
},
|
},
|
||||||
body: bytes.Join(
|
body: bytes.Join(
|
||||||
[][]byte{
|
[][]byte{
|
||||||
expectedMetricFamilyAsProtoCompactText,
|
|
||||||
externalMetricFamilyAsProtoCompactText,
|
externalMetricFamilyAsProtoCompactText,
|
||||||
|
expectedMetricFamilyAsProtoCompactText,
|
||||||
},
|
},
|
||||||
[]byte{},
|
[]byte{},
|
||||||
),
|
),
|
||||||
|
@ -593,25 +439,21 @@ metric: <
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, scenario := range scenarios {
|
for i, scenario := range scenarios {
|
||||||
registry := NewRegistry().(*registry)
|
registry := newRegistry()
|
||||||
|
registry.collectChecksEnabled = true
|
||||||
|
|
||||||
if scenario.withCounter {
|
if scenario.withCounter {
|
||||||
registry.Register(
|
registry.Register(metricVec)
|
||||||
"name", "docstring",
|
|
||||||
map[string]string{"basename": "basevalue"},
|
|
||||||
metric,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if scenario.withExternalMF {
|
if scenario.withExternalMF {
|
||||||
registry.SetMetricFamilyInjectionHook(
|
registry.metricFamilyInjectionHook = func() []*dto.MetricFamily {
|
||||||
func() []*dto.MetricFamily {
|
return externalMetricFamily
|
||||||
return externalMetricFamily
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
writer := &fakeResponseWriter{
|
writer := &fakeResponseWriter{
|
||||||
header: http.Header{},
|
header: http.Header{},
|
||||||
}
|
}
|
||||||
handler := registry.Handler()
|
handler := InstrumentHandler("prometheus", registry)
|
||||||
request, _ := http.NewRequest("GET", "/", nil)
|
request, _ := http.NewRequest("GET", "/", nil)
|
||||||
for key, value := range scenario.headers {
|
for key, value := range scenario.headers {
|
||||||
request.Header.Add(key, value)
|
request.Header.Add(key, value)
|
||||||
|
@ -636,7 +478,7 @@ metric: <
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHander(t *testing.T) {
|
func TestHandler(t *testing.T) {
|
||||||
testHandler(t)
|
testHandler(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -645,151 +487,3 @@ func BenchmarkHandler(b *testing.B) {
|
||||||
testHandler(b)
|
testHandler(b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDecorateWriter(t test.Tester) {
|
|
||||||
type input struct {
|
|
||||||
headers map[string]string
|
|
||||||
body []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type output struct {
|
|
||||||
headers map[string]string
|
|
||||||
body []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
var scenarios = []struct {
|
|
||||||
in input
|
|
||||||
out output
|
|
||||||
}{
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
headers: map[string]string{
|
|
||||||
"Accept-Encoding": "gzip,deflate,sdch",
|
|
||||||
},
|
|
||||||
body: []byte("Hi, mom!"),
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
headers: map[string]string{
|
|
||||||
"Content-Encoding": "gzip",
|
|
||||||
},
|
|
||||||
body: []byte("\x1f\x8b\b\x00\x00\tn\x88\x00\xff\xf2\xc8\xd4Q\xc8\xcd\xcfU\x04\x04\x00\x00\xff\xff9C&&\b\x00\x00\x00"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
headers: map[string]string{
|
|
||||||
"Accept-Encoding": "foo",
|
|
||||||
},
|
|
||||||
body: []byte("Hi, mom!"),
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
headers: map[string]string{},
|
|
||||||
body: []byte("Hi, mom!"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, scenario := range scenarios {
|
|
||||||
request, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
|
|
||||||
for key, value := range scenario.in.headers {
|
|
||||||
request.Header.Add(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
baseWriter := &fakeResponseWriter{
|
|
||||||
header: make(http.Header),
|
|
||||||
}
|
|
||||||
|
|
||||||
writer := decorateWriter(request, baseWriter)
|
|
||||||
|
|
||||||
for key, value := range scenario.out.headers {
|
|
||||||
if baseWriter.Header().Get(key) != value {
|
|
||||||
t.Errorf("%d. expected %s for header %s, got %s", i, value, key, baseWriter.Header().Get(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Write(scenario.in.body)
|
|
||||||
|
|
||||||
if closer, ok := writer.(io.Closer); ok {
|
|
||||||
closer.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(scenario.out.body, baseWriter.body.Bytes()) {
|
|
||||||
t.Errorf("%d. expected %s for body, got %s", i, scenario.out.body, baseWriter.body.Bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecorateWriter(t *testing.T) {
|
|
||||||
testDecorateWriter(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDecorateWriter(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
testDecorateWriter(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDumpToWriter(t test.Tester) {
|
|
||||||
type input struct {
|
|
||||||
metrics map[string]Metric
|
|
||||||
}
|
|
||||||
|
|
||||||
var scenarios = []struct {
|
|
||||||
in input
|
|
||||||
out []byte
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
out: []byte("[]"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
metrics: map[string]Metric{
|
|
||||||
"foo": NewCounter(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: []byte(`[{"baseLabels":{"__name__":"foo","label_foo":"foo"},"docstring":"metric foo","metric":{"type":"counter","value":[]}}]`),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
metrics: map[string]Metric{
|
|
||||||
"foo": NewCounter(),
|
|
||||||
"bar": NewCounter(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: []byte(`[{"baseLabels":{"__name__":"bar","label_bar":"bar"},"docstring":"metric bar","metric":{"type":"counter","value":[]}},{"baseLabels":{"__name__":"foo","label_foo":"foo"},"docstring":"metric foo","metric":{"type":"counter","value":[]}}]`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, scenario := range scenarios {
|
|
||||||
registry := NewRegistry().(*registry)
|
|
||||||
|
|
||||||
for name, metric := range scenario.in.metrics {
|
|
||||||
err := registry.Register(name, fmt.Sprintf("metric %s", name), map[string]string{fmt.Sprintf("label_%s", name): name}, metric)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%d. encountered error while registering metric %s", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actual, err := json.Marshal(registry)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%d. encountered error while dumping %s", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(scenario.out, actual) {
|
|
||||||
t.Errorf("%d. expected %q for dumping, got %q", i, scenario.out, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDumpToWriter(t *testing.T) {
|
|
||||||
testDumpToWriter(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDumpToWriter(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
testDumpToWriter(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,126 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO(mtp): Split this out into a summary statistics file once moving/rolling
|
|
||||||
// averages are calculated.
|
|
||||||
|
|
||||||
// ReductionMethod provides a method for reducing metrics into a scalar value.
|
|
||||||
type ReductionMethod func([]float64) float64
|
|
||||||
|
|
||||||
var (
|
|
||||||
medianReducer = NearestRankReducer(50)
|
|
||||||
)
|
|
||||||
|
|
||||||
// These are the canned ReductionMethods.
|
|
||||||
var (
|
|
||||||
// Reduce to the average of the set.
|
|
||||||
AverageReducer = averageReducer
|
|
||||||
|
|
||||||
// Extract the first modal value.
|
|
||||||
FirstModeReducer = firstModeReducer
|
|
||||||
|
|
||||||
// Reduce to the maximum of the set.
|
|
||||||
MaximumReducer = maximumReducer
|
|
||||||
|
|
||||||
// Reduce to the median of the set.
|
|
||||||
MedianReducer = medianReducer
|
|
||||||
|
|
||||||
// Reduce to the minimum of the set.
|
|
||||||
MinimumReducer = minimumReducer
|
|
||||||
)
|
|
||||||
|
|
||||||
func averageReducer(input []float64) float64 {
|
|
||||||
count := 0.0
|
|
||||||
sum := 0.0
|
|
||||||
|
|
||||||
for _, v := range input {
|
|
||||||
sum += v
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
|
|
||||||
if count == 0 {
|
|
||||||
return math.NaN()
|
|
||||||
}
|
|
||||||
|
|
||||||
return sum / count
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstModeReducer(input []float64) float64 {
|
|
||||||
valuesToFrequency := map[float64]int64{}
|
|
||||||
largestTally := int64(math.MinInt64)
|
|
||||||
largestTallyValue := math.NaN()
|
|
||||||
|
|
||||||
for _, v := range input {
|
|
||||||
presentCount, _ := valuesToFrequency[v]
|
|
||||||
presentCount++
|
|
||||||
|
|
||||||
valuesToFrequency[v] = presentCount
|
|
||||||
|
|
||||||
if presentCount > largestTally {
|
|
||||||
largestTally = presentCount
|
|
||||||
largestTallyValue = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return largestTallyValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the percentile by choosing the nearest neighboring value.
|
|
||||||
func nearestRank(input []float64, percentile float64) float64 {
|
|
||||||
inputSize := len(input)
|
|
||||||
|
|
||||||
if inputSize == 0 {
|
|
||||||
return math.NaN()
|
|
||||||
}
|
|
||||||
|
|
||||||
ordinalRank := math.Ceil(((percentile / 100.0) * float64(inputSize)) + 0.5)
|
|
||||||
|
|
||||||
copiedInput := make([]float64, inputSize)
|
|
||||||
copy(copiedInput, input)
|
|
||||||
sort.Float64s(copiedInput)
|
|
||||||
|
|
||||||
preliminaryIndex := int(ordinalRank) - 1
|
|
||||||
|
|
||||||
if preliminaryIndex == inputSize {
|
|
||||||
return copiedInput[preliminaryIndex-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return copiedInput[preliminaryIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a ReductionMethod based off of extracting a given percentile value.
|
|
||||||
func NearestRankReducer(percentile float64) ReductionMethod {
|
|
||||||
return func(input []float64) float64 {
|
|
||||||
return nearestRank(input, percentile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func minimumReducer(input []float64) float64 {
|
|
||||||
minimum := math.MaxFloat64
|
|
||||||
|
|
||||||
for _, v := range input {
|
|
||||||
minimum = math.Min(minimum, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return minimum
|
|
||||||
}
|
|
||||||
|
|
||||||
func maximumReducer(input []float64) float64 {
|
|
||||||
maximum := math.SmallestNonzeroFloat64
|
|
||||||
|
|
||||||
for _, v := range input {
|
|
||||||
maximum = math.Max(maximum, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return maximum
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
. "github.com/matttproud/gocheck"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *S) TestAverageOnEmpty(c *C) {
|
|
||||||
empty := []float64{}
|
|
||||||
var v float64 = AverageReducer(empty)
|
|
||||||
|
|
||||||
c.Assert(v, IsNaN)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestAverageForSingleton(c *C) {
|
|
||||||
input := []float64{5}
|
|
||||||
var v float64 = AverageReducer(input)
|
|
||||||
|
|
||||||
c.Check(v, Equals, 5.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestAverage(c *C) {
|
|
||||||
input := []float64{5, 15}
|
|
||||||
var v float64 = AverageReducer(input)
|
|
||||||
|
|
||||||
c.Check(v, Equals, 10.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestFirstModeOnEmpty(c *C) {
|
|
||||||
input := []float64{}
|
|
||||||
var v float64 = FirstModeReducer(input)
|
|
||||||
|
|
||||||
c.Assert(v, IsNaN)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestFirstModeForSingleton(c *C) {
|
|
||||||
input := []float64{5}
|
|
||||||
var v float64 = FirstModeReducer(input)
|
|
||||||
|
|
||||||
c.Check(v, Equals, 5.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestFirstModeForUnimodal(c *C) {
|
|
||||||
input := []float64{1, 2, 3, 4, 3}
|
|
||||||
var v float64 = FirstModeReducer(input)
|
|
||||||
|
|
||||||
c.Check(v, Equals, 3.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestNearestRankForEmpty(c *C) {
|
|
||||||
input := []float64{}
|
|
||||||
|
|
||||||
c.Assert(nearestRank(input, 0), IsNaN)
|
|
||||||
c.Assert(nearestRank(input, 50), IsNaN)
|
|
||||||
c.Assert(nearestRank(input, 100), IsNaN)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestNearestRankForSingleton(c *C) {
|
|
||||||
input := []float64{5}
|
|
||||||
|
|
||||||
c.Check(nearestRank(input, 0), Equals, 5.0)
|
|
||||||
c.Check(nearestRank(input, 50), Equals, 5.0)
|
|
||||||
c.Check(nearestRank(input, 100), Equals, 5.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestNearestRankForDouble(c *C) {
|
|
||||||
input := []float64{5, 5}
|
|
||||||
|
|
||||||
c.Check(nearestRank(input, 0), Equals, 5.0)
|
|
||||||
c.Check(nearestRank(input, 50), Equals, 5.0)
|
|
||||||
c.Check(nearestRank(input, 100), Equals, 5.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestNearestRankFor100(c *C) {
|
|
||||||
input := make([]float64, 100)
|
|
||||||
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
input[i] = float64(i + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(nearestRank(input, 0), Equals, 1.0)
|
|
||||||
c.Check(nearestRank(input, 50), Equals, 51.0)
|
|
||||||
c.Check(nearestRank(input, 100), Equals, 100.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestNearestRankFor101(c *C) {
|
|
||||||
input := make([]float64, 101)
|
|
||||||
|
|
||||||
for i := 0; i < 101; i++ {
|
|
||||||
input[i] = float64(i + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(nearestRank(input, 0), Equals, 1.0)
|
|
||||||
c.Check(nearestRank(input, 50), Equals, 51.0)
|
|
||||||
c.Check(nearestRank(input, 100), Equals, 101.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestMedianReducer(c *C) {
|
|
||||||
input := []float64{1, 2, 3}
|
|
||||||
|
|
||||||
c.Check(MedianReducer(input), Equals, 2.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestMinimum(c *C) {
|
|
||||||
input := []float64{5, 1, 10, 1.1, 4}
|
|
||||||
|
|
||||||
c.Check(MinimumReducer(input), Equals, 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestMaximum(c *C) {
|
|
||||||
input := []float64{5, 1, 10, 1.1, 4}
|
|
||||||
|
|
||||||
c.Check(MaximumReducer(input), Equals, 10.0)
|
|
||||||
}
|
|
|
@ -0,0 +1,425 @@
|
||||||
|
// 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 prometheus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.google.com/p/goprotobuf/proto"
|
||||||
|
"github.com/bmizerany/perks/quantile" // TODO: Vendorize?
|
||||||
|
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 = []float64{0.5, 0.9, 0.99}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 = 10
|
||||||
|
// DefBufCap is the standard buffer size for collecting Summary observations.
|
||||||
|
DefBufCap = 500
|
||||||
|
// DefEpsilon is the default error epsilon for the quantile rank estimates.
|
||||||
|
DefEpsilon = 0.001
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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. The default value is
|
||||||
|
// DefObjectives.
|
||||||
|
Objectives []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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Epsilon < 0 {
|
||||||
|
panic(fmt.Errorf("illegal value for Epsilon=%f", opts.Epsilon))
|
||||||
|
}
|
||||||
|
if opts.Epsilon == 0. {
|
||||||
|
opts.Epsilon = DefEpsilon
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &summary{
|
||||||
|
desc: desc,
|
||||||
|
|
||||||
|
objectives: opts.Objectives,
|
||||||
|
epsilon: opts.Epsilon,
|
||||||
|
|
||||||
|
labelPairs: makeLabelPairs(desc, labelValues),
|
||||||
|
|
||||||
|
hotBuf: make([]float64, 0, opts.BufCap),
|
||||||
|
coldBuf: make([]float64, 0, opts.BufCap),
|
||||||
|
streamDuration: opts.MaxAge / time.Duration(opts.AgeBuckets),
|
||||||
|
}
|
||||||
|
s.mergedTailStreams = s.newStream()
|
||||||
|
s.mergedAllStreams = s.newStream()
|
||||||
|
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]
|
||||||
|
|
||||||
|
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 []float64
|
||||||
|
epsilon float64
|
||||||
|
|
||||||
|
labelPairs []*dto.LabelPair
|
||||||
|
|
||||||
|
sum float64
|
||||||
|
cnt uint64
|
||||||
|
|
||||||
|
hotBuf, coldBuf []float64
|
||||||
|
|
||||||
|
streams []*quantile.Stream
|
||||||
|
streamDuration time.Duration
|
||||||
|
headStreamIdx int
|
||||||
|
headStreamExpTime, hotBufExpTime time.Time
|
||||||
|
|
||||||
|
headStream, mergedTailStreams, mergedAllStreams *quantile.Stream
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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()
|
||||||
|
s.mergedAllStreams.Merge(s.mergedTailStreams.Samples())
|
||||||
|
s.mergedAllStreams.Merge(s.headStream.Samples())
|
||||||
|
sum.SampleCount = proto.Uint64(s.cnt)
|
||||||
|
sum.SampleSum = proto.Float64(s.sum)
|
||||||
|
|
||||||
|
for _, rank := range s.objectives {
|
||||||
|
qs = append(qs, &dto.Quantile{
|
||||||
|
Quantile: proto.Float64(rank),
|
||||||
|
Value: proto.Float64(s.mergedAllStreams.Query(rank)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
s.mergedAllStreams.Reset()
|
||||||
|
|
||||||
|
s.mtx.Unlock()
|
||||||
|
|
||||||
|
if len(qs) > 0 {
|
||||||
|
sort.Sort(quantSort(qs))
|
||||||
|
}
|
||||||
|
sum.Quantile = qs
|
||||||
|
|
||||||
|
out.Summary = sum
|
||||||
|
out.Label = s.labelPairs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *summary) newStream() *quantile.Stream {
|
||||||
|
stream := quantile.NewTargeted(s.objectives...)
|
||||||
|
stream.SetEpsilon(s.epsilon)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
if s.hotBufExpTime.Equal(s.headStreamExpTime) {
|
||||||
|
// Fast return to avoid re-merging s.mergedTailStreams.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for !s.hotBufExpTime.Equal(s.headStreamExpTime) {
|
||||||
|
s.headStreamIdx++
|
||||||
|
if s.headStreamIdx >= len(s.streams) {
|
||||||
|
s.headStreamIdx = 0
|
||||||
|
}
|
||||||
|
s.headStream = s.streams[s.headStreamIdx]
|
||||||
|
s.headStream.Reset()
|
||||||
|
s.headStreamExpTime = s.headStreamExpTime.Add(s.streamDuration)
|
||||||
|
}
|
||||||
|
s.mergedTailStreams.Reset()
|
||||||
|
for _, stream := range s.streams {
|
||||||
|
if stream != s.headStream {
|
||||||
|
s.mergedTailStreams.Merge(stream.Samples())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushColdBuf needs mtx locked.
|
||||||
|
func (s *summary) flushColdBuf() {
|
||||||
|
for _, v := range s.coldBuf {
|
||||||
|
s.headStream.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) {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,314 @@
|
||||||
|
// 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 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%10000 + 1)
|
||||||
|
concLevel := int(n%15 + 1)
|
||||||
|
total := mutations * concLevel
|
||||||
|
ε := 0.001
|
||||||
|
|
||||||
|
var start, end sync.WaitGroup
|
||||||
|
start.Add(1)
|
||||||
|
end.Add(concLevel)
|
||||||
|
|
||||||
|
sum := NewSummary(SummaryOpts{
|
||||||
|
Name: "test_summary",
|
||||||
|
Help: "helpless",
|
||||||
|
Epsilon: ε,
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, wantQ := range DefObjectives {
|
||||||
|
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) && len(allVars) > 500 { // Avoid statistical outliers.
|
||||||
|
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)
|
||||||
|
|
||||||
|
it := func(n uint32) bool {
|
||||||
|
mutations := int(n%10000 + 1)
|
||||||
|
concLevel := int(n%15 + 1)
|
||||||
|
ε := 0.001
|
||||||
|
vecLength := int(n%5 + 1)
|
||||||
|
|
||||||
|
var start, end sync.WaitGroup
|
||||||
|
start.Add(1)
|
||||||
|
end.Add(concLevel)
|
||||||
|
|
||||||
|
sum := NewSummaryVec(
|
||||||
|
SummaryOpts{
|
||||||
|
Name: "test_summary",
|
||||||
|
Help: "helpless",
|
||||||
|
Epsilon: ε,
|
||||||
|
},
|
||||||
|
[]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 DefObjectives {
|
||||||
|
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) && len(allVars[i]) > 500 { // Avoid statistical outliers.
|
||||||
|
t.Errorf("got %f for quantile %f for label %c, want [%f,%f]", gotV, gotQ, 'A'+i, min, max)
|
||||||
|
t.Log(len(allVars[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
Epsilon: 0.001,
|
||||||
|
MaxAge: 10 * time.Millisecond,
|
||||||
|
Objectives: []float64{0.1},
|
||||||
|
})
|
||||||
|
|
||||||
|
m := &dto.Metric{}
|
||||||
|
i := 0
|
||||||
|
tick := time.NewTicker(100 * time.Microsecond)
|
||||||
|
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) {
|
||||||
|
lower := int((q - 4*ε) * float64(len(vars)))
|
||||||
|
upper := int((q+4*ε)*float64(len(vars))) + 1
|
||||||
|
min = vars[0]
|
||||||
|
if lower > 0 {
|
||||||
|
min = vars[lower]
|
||||||
|
}
|
||||||
|
max = vars[len(vars)-1]
|
||||||
|
if upper < len(vars)-1 {
|
||||||
|
max = vars[upper]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -1,158 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
lowerThird = 1.0 / 3.0
|
|
||||||
upperThird = 2.0 * lowerThird
|
|
||||||
)
|
|
||||||
|
|
||||||
// A TallyingIndexEstimator is responsible for estimating the value of index for
|
|
||||||
// a given TallyingBucket, even though a TallyingBucket does not possess a
|
|
||||||
// collection of samples. There are a few strategies listed below for how
|
|
||||||
// this value should be approximated.
|
|
||||||
type TallyingIndexEstimator func(minimum, maximum float64, index, observations int) float64
|
|
||||||
|
|
||||||
// Provide a filter for handling empty buckets.
|
|
||||||
func emptyFilter(e TallyingIndexEstimator) TallyingIndexEstimator {
|
|
||||||
return func(minimum, maximum float64, index, observations int) float64 {
|
|
||||||
if observations == 0 {
|
|
||||||
return math.NaN()
|
|
||||||
}
|
|
||||||
|
|
||||||
return e(minimum, maximum, index, observations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
minimumEstimator = emptyFilter(func(minimum, maximum float64, _, observations int) float64 {
|
|
||||||
return minimum
|
|
||||||
})
|
|
||||||
|
|
||||||
maximumEstimator = emptyFilter(func(minimum, maximum float64, _, observations int) float64 {
|
|
||||||
return maximum
|
|
||||||
})
|
|
||||||
|
|
||||||
averageEstimator = emptyFilter(func(minimum, maximum float64, _, observations int) float64 {
|
|
||||||
return AverageReducer([]float64{minimum, maximum})
|
|
||||||
})
|
|
||||||
|
|
||||||
uniformEstimator = emptyFilter(func(minimum, maximum float64, index, observations int) float64 {
|
|
||||||
if observations == 1 {
|
|
||||||
return minimum
|
|
||||||
}
|
|
||||||
|
|
||||||
location := float64(index) / float64(observations)
|
|
||||||
|
|
||||||
if location > upperThird {
|
|
||||||
return maximum
|
|
||||||
} else if location < lowerThird {
|
|
||||||
return minimum
|
|
||||||
}
|
|
||||||
|
|
||||||
return AverageReducer([]float64{minimum, maximum})
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// These are the canned TallyingIndexEstimators.
|
|
||||||
var (
|
|
||||||
// Report the smallest observed value in the bucket.
|
|
||||||
MinimumEstimator = minimumEstimator
|
|
||||||
// Report the largest observed value in the bucket.
|
|
||||||
MaximumEstimator = maximumEstimator
|
|
||||||
// Report the average of the extrema.
|
|
||||||
AverageEstimator = averageEstimator
|
|
||||||
// Report the minimum value of the index if it is in the lower-third of
|
|
||||||
// observations, the average if in the middle-third, and the maximum if in
|
|
||||||
// the largest third
|
|
||||||
UniformEstimator = uniformEstimator
|
|
||||||
)
|
|
||||||
|
|
||||||
// A TallyingBucket is a Bucket that tallies when an object is added to it.
|
|
||||||
// Upon insertion, an object is compared against collected extrema and noted
|
|
||||||
// as a new minimum or maximum if appropriate.
|
|
||||||
type TallyingBucket struct {
|
|
||||||
estimator TallyingIndexEstimator
|
|
||||||
largestObserved float64
|
|
||||||
mutex sync.RWMutex
|
|
||||||
observations int
|
|
||||||
smallestObserved float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *TallyingBucket) Add(value float64) {
|
|
||||||
b.mutex.Lock()
|
|
||||||
defer b.mutex.Unlock()
|
|
||||||
|
|
||||||
b.observations += 1
|
|
||||||
b.smallestObserved = math.Min(value, b.smallestObserved)
|
|
||||||
b.largestObserved = math.Max(value, b.largestObserved)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b TallyingBucket) String() string {
|
|
||||||
b.mutex.RLock()
|
|
||||||
defer b.mutex.RUnlock()
|
|
||||||
|
|
||||||
observations := b.observations
|
|
||||||
|
|
||||||
if observations == 0 {
|
|
||||||
return fmt.Sprintf("[TallyingBucket (Empty)]")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("[TallyingBucket (%f, %f); %d items]", b.smallestObserved, b.largestObserved, observations)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b TallyingBucket) Observations() int {
|
|
||||||
b.mutex.RLock()
|
|
||||||
defer b.mutex.RUnlock()
|
|
||||||
|
|
||||||
return b.observations
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b TallyingBucket) ValueForIndex(index int) float64 {
|
|
||||||
b.mutex.RLock()
|
|
||||||
defer b.mutex.RUnlock()
|
|
||||||
|
|
||||||
return b.estimator(b.smallestObserved, b.largestObserved, index, b.observations)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *TallyingBucket) Reset() {
|
|
||||||
b.mutex.Lock()
|
|
||||||
defer b.mutex.Unlock()
|
|
||||||
|
|
||||||
b.largestObserved = math.SmallestNonzeroFloat64
|
|
||||||
b.observations = 0
|
|
||||||
b.smallestObserved = math.MaxFloat64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Produce a TallyingBucket with sane defaults.
|
|
||||||
func DefaultTallyingBucket() TallyingBucket {
|
|
||||||
return TallyingBucket{
|
|
||||||
estimator: MinimumEstimator,
|
|
||||||
largestObserved: math.SmallestNonzeroFloat64,
|
|
||||||
smallestObserved: math.MaxFloat64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CustomTallyingBucket(estimator TallyingIndexEstimator) TallyingBucket {
|
|
||||||
return TallyingBucket{
|
|
||||||
estimator: estimator,
|
|
||||||
largestObserved: math.SmallestNonzeroFloat64,
|
|
||||||
smallestObserved: math.MaxFloat64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is used strictly for testing.
|
|
||||||
func tallyingBucketBuilder() Bucket {
|
|
||||||
b := DefaultTallyingBucket()
|
|
||||||
return &b
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
. "github.com/matttproud/gocheck"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *S) TestTallyingPercentileEstimatorMinimum(c *C) {
|
|
||||||
c.Assert(MinimumEstimator(-2, -1, 0, 0), IsNaN)
|
|
||||||
c.Check(MinimumEstimator(-2, -1, 0, 1), Equals, -2.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestTallyingPercentileEstimatorMaximum(c *C) {
|
|
||||||
c.Assert(MaximumEstimator(-2, -1, 0, 0), IsNaN)
|
|
||||||
c.Check(MaximumEstimator(-2, -1, 0, 1), Equals, -1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestTallyingPercentilesEstimatorAverage(c *C) {
|
|
||||||
c.Assert(AverageEstimator(-2, -1, 0, 0), IsNaN)
|
|
||||||
c.Check(AverageEstimator(-2, -2, 0, 1), Equals, -2.0)
|
|
||||||
c.Check(AverageEstimator(-1, -1, 0, 1), Equals, -1.0)
|
|
||||||
c.Check(AverageEstimator(1, 1, 0, 2), Equals, 1.0)
|
|
||||||
c.Check(AverageEstimator(2, 1, 0, 2), Equals, 1.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestTallyingPercentilesEstimatorUniform(c *C) {
|
|
||||||
c.Assert(UniformEstimator(-5, 5, 0, 0), IsNaN)
|
|
||||||
|
|
||||||
c.Check(UniformEstimator(-5, 5, 0, 2), Equals, -5.0)
|
|
||||||
c.Check(UniformEstimator(-5, 5, 1, 2), Equals, 0.0)
|
|
||||||
c.Check(UniformEstimator(-5, 5, 2, 2), Equals, 5.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestTallyingBucketBuilder(c *C) {
|
|
||||||
var bucket Bucket = tallyingBucketBuilder()
|
|
||||||
|
|
||||||
c.Assert(bucket, Not(IsNil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestTallyingBucketString(c *C) {
|
|
||||||
bucket := TallyingBucket{
|
|
||||||
observations: 3,
|
|
||||||
smallestObserved: 2.0,
|
|
||||||
largestObserved: 5.5,
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Check(bucket.String(), Equals, "[TallyingBucket (2.000000, 5.500000); 3 items]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) TestTallyingBucketAdd(c *C) {
|
|
||||||
b := DefaultTallyingBucket()
|
|
||||||
|
|
||||||
b.Add(1)
|
|
||||||
|
|
||||||
c.Check(b.observations, Equals, 1)
|
|
||||||
c.Check(b.Observations(), Equals, 1)
|
|
||||||
c.Check(b.smallestObserved, Equals, 1.0)
|
|
||||||
c.Check(b.largestObserved, Equals, 1.0)
|
|
||||||
|
|
||||||
b.Add(2)
|
|
||||||
|
|
||||||
c.Check(b.observations, Equals, 2)
|
|
||||||
c.Check(b.Observations(), Equals, 2)
|
|
||||||
c.Check(b.smallestObserved, Equals, 1.0)
|
|
||||||
c.Check(b.largestObserved, Equals, 2.0)
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Boilerplate metrics about the metrics reporting subservice. These are only
|
|
||||||
// exposed if the DefaultRegistry's exporter is hooked into the HTTP request
|
|
||||||
// handler.
|
|
||||||
var (
|
|
||||||
requestCount = NewCounter()
|
|
||||||
requestLatencyBuckets = LogarithmicSizedBucketsFor(0, 1000)
|
|
||||||
requestLatency = NewHistogram(&HistogramSpecification{
|
|
||||||
Starts: requestLatencyBuckets,
|
|
||||||
BucketBuilder: AccumulatingBucketBuilder(EvictAndReplaceWith(50, AverageReducer), 1000),
|
|
||||||
ReportablePercentiles: []float64{0.01, 0.05, 0.5, 0.9, 0.99},
|
|
||||||
})
|
|
||||||
|
|
||||||
startTime = NewGauge()
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
startTime.Set(nil, float64(time.Now().Unix()))
|
|
||||||
|
|
||||||
DefaultRegistry.Register("telemetry_requests_metrics_total", "A counter of the total requests made against the telemetry system.", NilLabels, requestCount)
|
|
||||||
DefaultRegistry.Register("telemetry_requests_metrics_latency_microseconds", "A histogram of the response latency for requests made against the telemetry system.", NilLabels, requestLatency)
|
|
||||||
|
|
||||||
DefaultRegistry.Register("instance_start_time_seconds", "The time at which the current instance started (UTC).", NilLabels, startTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This callback accumulates the microsecond duration of the reporting
|
|
||||||
// framework's overhead such that it can be reported.
|
|
||||||
var requestLatencyAccumulator = func(began time.Time) {
|
|
||||||
microseconds := float64(time.Since(began) / time.Microsecond)
|
|
||||||
|
|
||||||
requestLatency.Add(nil, microseconds)
|
|
||||||
}
|
|
|
@ -1,140 +1,121 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
// Copyright 2014 Prometheus Team
|
||||||
// All rights reserved.
|
// 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
|
||||||
//
|
//
|
||||||
// Use of this source code is governed by a BSD-style
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
// license that can be found in the LICENSE file.
|
//
|
||||||
|
// 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
|
package prometheus
|
||||||
|
|
||||||
import (
|
import "hash/fnv"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"code.google.com/p/goprotobuf/proto"
|
// Untyped is a Metric that represents a single numerical value that can
|
||||||
|
// arbitrarily go up and down.
|
||||||
dto "github.com/prometheus/client_model/go"
|
//
|
||||||
|
// An Untyped metric works the same as a Gauge. The only difference is that to
|
||||||
"github.com/prometheus/client_golang/model"
|
// no type information is implied.
|
||||||
)
|
//
|
||||||
|
// To create Gauge instances, use NewUntyped.
|
||||||
// An Untyped metric represents scalar values without any type implications
|
|
||||||
// whatsoever. If you need to handle values that cannot be represented by any of
|
|
||||||
// the existing metric types, you can use an Untyped type and rely on contracts
|
|
||||||
// outside of Prometheus to ensure that these values are understood correctly.
|
|
||||||
type Untyped interface {
|
type Untyped interface {
|
||||||
Metric
|
Metric
|
||||||
Set(labels map[string]string, value float64) float64
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type untypedVector struct {
|
// UntypedOpts is an alias for Opts. See there for doc comments.
|
||||||
Labels map[string]string `json:"labels"`
|
type UntypedOpts Opts
|
||||||
Value float64 `json:"value"`
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUntyped returns a newly allocated Untyped metric ready to be used.
|
// UntypedVec is a Collector that bundles a set of Untyped metrics that all
|
||||||
func NewUntyped() Untyped {
|
// share the same Desc, but have different values for their variable
|
||||||
return &untyped{
|
// labels. This is used if you want to count the same thing partitioned by
|
||||||
values: map[uint64]*untypedVector{},
|
// 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...)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type untyped struct {
|
// GetMetricWithLabelValues replaces the method of the same name in
|
||||||
mutex sync.RWMutex
|
// MetricVec. The difference is that this method returns an Untyped and not a
|
||||||
values map[uint64]*untypedVector
|
// Metric so that no type conversion is required.
|
||||||
}
|
func (m *UntypedVec) GetMetricWithLabelValues(lvs ...string) (Untyped, error) {
|
||||||
|
metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...)
|
||||||
func (metric *untyped) String() string {
|
if metric != nil {
|
||||||
formatString := "[Untyped %s]"
|
return metric.(Untyped), err
|
||||||
|
|
||||||
metric.mutex.RLock()
|
|
||||||
defer metric.mutex.RUnlock()
|
|
||||||
|
|
||||||
return fmt.Sprintf(formatString, metric.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *untyped) Set(labels map[string]string, value float64) float64 {
|
|
||||||
if labels == nil {
|
|
||||||
labels = blankLabelsSingleton
|
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
signature := model.LabelValuesToSignature(labels)
|
// 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
|
||||||
metric.mutex.Lock()
|
// type conversion is required.
|
||||||
defer metric.mutex.Unlock()
|
func (m *UntypedVec) GetMetricWith(labels Labels) (Untyped, error) {
|
||||||
|
metric, err := m.MetricVec.GetMetricWith(labels)
|
||||||
if original, ok := metric.values[signature]; ok {
|
if metric != nil {
|
||||||
original.Value = value
|
return metric.(Untyped), err
|
||||||
} else {
|
|
||||||
metric.values[signature] = &untypedVector{
|
|
||||||
Labels: labels,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *untyped) Reset(labels map[string]string) {
|
// WithLabelValues works as GetMetricWithLabelValues, but panics where
|
||||||
signature := model.LabelValuesToSignature(labels)
|
// GetMetricWithLabelValues would have returned an error. By not returning an
|
||||||
|
// error, WithLabelValues allows shortcuts like
|
||||||
metric.mutex.Lock()
|
// myVec.WithLabelValues("404", "GET").Add(42)
|
||||||
defer metric.mutex.Unlock()
|
func (m *UntypedVec) WithLabelValues(lvs ...string) Untyped {
|
||||||
|
return m.MetricVec.WithLabelValues(lvs...).(Untyped)
|
||||||
delete(metric.values, signature)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metric *untyped) ResetAll() {
|
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
|
||||||
metric.mutex.Lock()
|
// returned an error. By not returning an error, With allows shortcuts like
|
||||||
defer metric.mutex.Unlock()
|
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
|
||||||
|
func (m *UntypedVec) With(labels Labels) Untyped {
|
||||||
for key, value := range metric.values {
|
return m.MetricVec.With(labels).(Untyped)
|
||||||
for label := range value.Labels {
|
|
||||||
delete(value.Labels, label)
|
|
||||||
}
|
|
||||||
delete(metric.values, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *untyped) MarshalJSON() ([]byte, error) {
|
|
||||||
metric.mutex.RLock()
|
|
||||||
defer metric.mutex.RUnlock()
|
|
||||||
|
|
||||||
values := make([]*untypedVector, 0, len(metric.values))
|
|
||||||
for _, value := range metric.values {
|
|
||||||
values = append(values, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(map[string]interface{}{
|
|
||||||
typeKey: untypedTypeValue,
|
|
||||||
valueKey: values,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (metric *untyped) dumpChildren(f *dto.MetricFamily) {
|
|
||||||
metric.mutex.RLock()
|
|
||||||
defer metric.mutex.RUnlock()
|
|
||||||
|
|
||||||
f.Type = dto.MetricType_UNTYPED.Enum()
|
|
||||||
|
|
||||||
for _, child := range metric.values {
|
|
||||||
c := &dto.Untyped{
|
|
||||||
Value: proto.Float64(child.Value),
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &dto.Metric{
|
|
||||||
Untyped: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, value := range child.Labels {
|
|
||||||
p := &dto.LabelPair{
|
|
||||||
Name: proto.String(name),
|
|
||||||
Value: proto.String(value),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Label = append(m.Label, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Metric = append(f.Metric, m)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,154 +0,0 @@
|
||||||
// Copyright (c) 2013, Prometheus Team
|
|
||||||
// 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 (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testUntyped(t test.Tester) {
|
|
||||||
type input struct {
|
|
||||||
steps []func(g Untyped)
|
|
||||||
}
|
|
||||||
type output struct {
|
|
||||||
value string
|
|
||||||
}
|
|
||||||
|
|
||||||
var scenarios = []struct {
|
|
||||||
in input
|
|
||||||
out output
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Untyped){},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"untyped","value":[]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Untyped){
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Set(nil, 1)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"untyped","value":[{"labels":{},"value":1}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Untyped){
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Set(map[string]string{}, 2)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"untyped","value":[{"labels":{},"value":2}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Untyped){
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Set(map[string]string{}, 3)
|
|
||||||
},
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Set(map[string]string{}, 5)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"untyped","value":[{"labels":{},"value":5}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Untyped){
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 13)
|
|
||||||
},
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Set(map[string]string{"handler": "/bar"}, 17)
|
|
||||||
},
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Reset(map[string]string{"handler": "/bar"})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"untyped","value":[{"labels":{"handler":"/foo"},"value":13}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Untyped){
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 13)
|
|
||||||
},
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Set(map[string]string{"handler": "/bar"}, 17)
|
|
||||||
},
|
|
||||||
func(g Untyped) {
|
|
||||||
g.ResetAll()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"untyped","value":[]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: input{
|
|
||||||
steps: []func(g Untyped){
|
|
||||||
func(g Untyped) {
|
|
||||||
g.Set(map[string]string{"handler": "/foo"}, 19)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
out: output{
|
|
||||||
value: `{"type":"untyped","value":[{"labels":{"handler":"/foo"},"value":19}]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, scenario := range scenarios {
|
|
||||||
untyped := NewUntyped()
|
|
||||||
|
|
||||||
for _, step := range scenario.in.steps {
|
|
||||||
step(untyped)
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := json.Marshal(untyped)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%d. could not marshal into JSON %s", i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
asString := string(bytes)
|
|
||||||
|
|
||||||
if scenario.out.value != asString {
|
|
||||||
t.Errorf("%d. expected %q, got %q", i, scenario.out.value, asString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUntyped(t *testing.T) {
|
|
||||||
testUntyped(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkUntyped(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
testUntyped(b)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
// 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 prometheus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
mtx sync.RWMutex
|
||||||
|
desc *Desc
|
||||||
|
valType ValueType
|
||||||
|
val float64
|
||||||
|
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,
|
||||||
|
val: val,
|
||||||
|
labelPairs: makeLabelPairs(desc, labelValues),
|
||||||
|
}
|
||||||
|
result.Init(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *value) Desc() *Desc {
|
||||||
|
return v.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *value) Set(val float64) {
|
||||||
|
v.mtx.Lock()
|
||||||
|
defer v.mtx.Unlock()
|
||||||
|
|
||||||
|
v.val = val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *value) Inc() {
|
||||||
|
v.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *value) Dec() {
|
||||||
|
v.Add(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *value) Add(val float64) {
|
||||||
|
v.mtx.Lock()
|
||||||
|
defer v.mtx.Unlock()
|
||||||
|
|
||||||
|
v.val += val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *value) Sub(val float64) {
|
||||||
|
v.Add(val * -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *value) Write(out *dto.Metric) {
|
||||||
|
v.mtx.RLock()
|
||||||
|
val := v.val
|
||||||
|
v.mtx.RUnlock()
|
||||||
|
|
||||||
|
populateMetric(v.valType, val, 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) {
|
||||||
|
populateMetric(m.valType, m.val, m.labelPairs, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateMetric(
|
||||||
|
t ValueType,
|
||||||
|
v float64,
|
||||||
|
labelPairs []*dto.LabelPair,
|
||||||
|
m *dto.Metric,
|
||||||
|
) {
|
||||||
|
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:
|
||||||
|
panic(fmt.Errorf("encountered unknown type %v", t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
// 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 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 error 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
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
// 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 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)
|
||||||
|
}
|
||||||
|
}
|
40
test/test.go
40
test/test.go
|
@ -1,40 +0,0 @@
|
||||||
// Copyright 2013 Prometheus Team
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
// Package test provides common test helpers to the client library.
|
|
||||||
package test
|
|
||||||
|
|
||||||
// Tester is an interface implemented by both, testing.T and testing.B.
|
|
||||||
type Tester interface {
|
|
||||||
Error(args ...interface{})
|
|
||||||
Errorf(format string, args ...interface{})
|
|
||||||
Fatal(args ...interface{})
|
|
||||||
Fatalf(format string, args ...interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorEqual compares Go errors for equality.
|
|
||||||
func ErrorEqual(left, right error) bool {
|
|
||||||
if left == right {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if left != nil && right != nil {
|
|
||||||
if left.Error() == right.Error() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -18,13 +18,12 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"code.google.com/p/goprotobuf/proto"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/test"
|
"code.google.com/p/goprotobuf/proto"
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testCreate(t test.Tester) {
|
func testCreate(t testing.TB) {
|
||||||
var scenarios = []struct {
|
var scenarios = []struct {
|
||||||
in *dto.MetricFamily
|
in *dto.MetricFamily
|
||||||
out string
|
out string
|
||||||
|
@ -257,7 +256,7 @@ func BenchmarkCreate(b *testing.B) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCreateError(t test.Tester) {
|
func testCreateError(t testing.TB) {
|
||||||
var scenarios = []struct {
|
var scenarios = []struct {
|
||||||
in *dto.MetricFamily
|
in *dto.MetricFamily
|
||||||
err string
|
err string
|
||||||
|
|
|
@ -17,15 +17,14 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"code.google.com/p/goprotobuf/proto"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/test"
|
"code.google.com/p/goprotobuf/proto"
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var parser Parser
|
var parser Parser
|
||||||
|
|
||||||
func testParse(t test.Tester) {
|
func testParse(t testing.TB) {
|
||||||
var scenarios = []struct {
|
var scenarios = []struct {
|
||||||
in string
|
in string
|
||||||
out []*dto.MetricFamily
|
out []*dto.MetricFamily
|
||||||
|
@ -379,7 +378,7 @@ func BenchmarkParse(b *testing.B) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testParseError(t test.Tester) {
|
func testParseError(t testing.TB) {
|
||||||
var scenarios = []struct {
|
var scenarios = []struct {
|
||||||
in string
|
in string
|
||||||
err string
|
err string
|
||||||
|
|
Loading…
Reference in New Issue