mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-09 23:24:05 -08:00
Merge branch 'master' into beorn7/storage3
This commit is contained in:
commit
3d86130d8c
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,20 +1,18 @@
|
||||||
## 0.17.0 / unreleased
|
## 0.17.0 / 2016-03-02
|
||||||
|
|
||||||
This version no longer works with Alertmanager 0.0.4 and earlier!
|
This version no longer works with Alertmanager 0.0.4 and earlier!
|
||||||
The alerting rule syntax has changed as well but the old syntax is supported
|
The alerting rule syntax has changed as well but the old syntax is supported
|
||||||
up until version 0.18.
|
up until version 0.18.
|
||||||
|
|
||||||
All regular expressions in PromQL are anchored now, matching the behavior of
|
All regular expressions in PromQL are anchored now, matching the behavior of
|
||||||
regular expressions in config files. Expressions in rules, alerts or dashboards
|
regular expressions in config files.
|
||||||
need to be changed if they don't match the full value so far (for example
|
|
||||||
`http_requests_total{status=~"^5"}` won't match anything anymore and needs to
|
|
||||||
be changed to `http_requests_total{status=~"5.."}`).
|
|
||||||
|
|
||||||
* [CHANGE] Integrate with Alertmanager 0.1.0 and higher
|
* [CHANGE] Integrate with Alertmanager 0.1.0 and higher
|
||||||
* [CHANGE] Degraded storage mode renamed to rushed mode
|
* [CHANGE] Degraded storage mode renamed to rushed mode
|
||||||
* [CHANGE] New alerting rule syntax
|
* [CHANGE] New alerting rule syntax
|
||||||
* [CHANGE] Add label validation on ingestion
|
* [CHANGE] Add label validation on ingestion
|
||||||
* [CHANGE] Regular expression matchers in PromQL are anchored
|
* [CHANGE] Regular expression matchers in PromQL are anchored
|
||||||
|
* [FEATURE] Add `without` aggregation modifier
|
||||||
* [FEATURE] Send alert resolved notifications to Alertmanager
|
* [FEATURE] Send alert resolved notifications to Alertmanager
|
||||||
* [FEATURE] Allow millisecond precision in configuration file
|
* [FEATURE] Allow millisecond precision in configuration file
|
||||||
* [FEATURE] Support AirBnB's Smartstack Nerve for service discovery
|
* [FEATURE] Support AirBnB's Smartstack Nerve for service discovery
|
||||||
|
@ -26,6 +24,9 @@ be changed to `http_requests_total{status=~"5.."}`).
|
||||||
* [BUGFIX] Handle OPTIONS HTTP requests to the API correctly
|
* [BUGFIX] Handle OPTIONS HTTP requests to the API correctly
|
||||||
* [BUGFIX] Parsing of ranges in PromQL fixed
|
* [BUGFIX] Parsing of ranges in PromQL fixed
|
||||||
* [BUGFIX] Correctly validate URL flag parameters
|
* [BUGFIX] Correctly validate URL flag parameters
|
||||||
|
* [BUGFIX] Log argument parse errors
|
||||||
|
* [BUGFIX] Properly handle creation of target with bad TLS config
|
||||||
|
* [BUGFIX] Fix of checkpoint timing issue
|
||||||
|
|
||||||
## 0.16.2 / 2016-01-18
|
## 0.16.2 / 2016-01-18
|
||||||
|
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -23,7 +23,7 @@ all: format build test
|
||||||
|
|
||||||
style:
|
style:
|
||||||
@echo ">> checking code style"
|
@echo ">> checking code style"
|
||||||
@! gofmt -d **/*.go | grep '^'
|
@! gofmt -d $(shell find . -path ./vendor -prune -o -name '*.go' -print) | grep '^'
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@echo ">> running tests"
|
@echo ">> running tests"
|
||||||
|
|
|
@ -26,7 +26,7 @@ import (
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/prometheus/common/log"
|
"github.com/prometheus/common/log"
|
||||||
"github.com/prometheus/prometheus/notification"
|
"github.com/prometheus/prometheus/notifier"
|
||||||
"github.com/prometheus/prometheus/promql"
|
"github.com/prometheus/prometheus/promql"
|
||||||
"github.com/prometheus/prometheus/storage/local"
|
"github.com/prometheus/prometheus/storage/local"
|
||||||
"github.com/prometheus/prometheus/storage/local/index"
|
"github.com/prometheus/prometheus/storage/local/index"
|
||||||
|
@ -42,11 +42,11 @@ var cfg = struct {
|
||||||
printVersion bool
|
printVersion bool
|
||||||
configFile string
|
configFile string
|
||||||
|
|
||||||
storage local.MemorySeriesStorageOptions
|
storage local.MemorySeriesStorageOptions
|
||||||
notification notification.HandlerOptions
|
notifier notifier.Options
|
||||||
queryEngine promql.EngineOptions
|
queryEngine promql.EngineOptions
|
||||||
web web.Options
|
web web.Options
|
||||||
remote remote.Options
|
remote remote.Options
|
||||||
|
|
||||||
prometheusURL string
|
prometheusURL string
|
||||||
influxdbURL string
|
influxdbURL string
|
||||||
|
@ -203,15 +203,15 @@ func init() {
|
||||||
|
|
||||||
// Alertmanager.
|
// Alertmanager.
|
||||||
cfg.fs.StringVar(
|
cfg.fs.StringVar(
|
||||||
&cfg.notification.AlertmanagerURL, "alertmanager.url", "",
|
&cfg.notifier.AlertmanagerURL, "alertmanager.url", "",
|
||||||
"The URL of the alert manager to send notifications to.",
|
"The URL of the alert manager to send notifications to.",
|
||||||
)
|
)
|
||||||
cfg.fs.IntVar(
|
cfg.fs.IntVar(
|
||||||
&cfg.notification.QueueCapacity, "alertmanager.notification-queue-capacity", 10000,
|
&cfg.notifier.QueueCapacity, "alertmanager.notification-queue-capacity", 10000,
|
||||||
"The capacity of the queue for pending alert manager notifications.",
|
"The capacity of the queue for pending alert manager notifications.",
|
||||||
)
|
)
|
||||||
cfg.fs.DurationVar(
|
cfg.fs.DurationVar(
|
||||||
&cfg.notification.Timeout, "alertmanager.timeout", 10*time.Second,
|
&cfg.notifier.Timeout, "alertmanager.timeout", 10*time.Second,
|
||||||
"Alert manager HTTP API timeout.",
|
"Alert manager HTTP API timeout.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/notification"
|
"github.com/prometheus/prometheus/notifier"
|
||||||
"github.com/prometheus/prometheus/promql"
|
"github.com/prometheus/prometheus/promql"
|
||||||
"github.com/prometheus/prometheus/retrieval"
|
"github.com/prometheus/prometheus/retrieval"
|
||||||
"github.com/prometheus/prometheus/rules"
|
"github.com/prometheus/prometheus/rules"
|
||||||
|
@ -84,16 +84,16 @@ func Main() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
notificationHandler = notification.New(&cfg.notification)
|
notifier = notifier.New(&cfg.notifier)
|
||||||
targetManager = retrieval.NewTargetManager(sampleAppender)
|
targetManager = retrieval.NewTargetManager(sampleAppender)
|
||||||
queryEngine = promql.NewEngine(memStorage, &cfg.queryEngine)
|
queryEngine = promql.NewEngine(memStorage, &cfg.queryEngine)
|
||||||
)
|
)
|
||||||
|
|
||||||
ruleManager := rules.NewManager(&rules.ManagerOptions{
|
ruleManager := rules.NewManager(&rules.ManagerOptions{
|
||||||
SampleAppender: sampleAppender,
|
SampleAppender: sampleAppender,
|
||||||
NotificationHandler: notificationHandler,
|
Notifier: notifier,
|
||||||
QueryEngine: queryEngine,
|
QueryEngine: queryEngine,
|
||||||
ExternalURL: cfg.web.ExternalURL,
|
ExternalURL: cfg.web.ExternalURL,
|
||||||
})
|
})
|
||||||
|
|
||||||
flags := map[string]string{}
|
flags := map[string]string{}
|
||||||
|
@ -110,7 +110,7 @@ func Main() int {
|
||||||
|
|
||||||
webHandler := web.New(memStorage, queryEngine, ruleManager, status, &cfg.web)
|
webHandler := web.New(memStorage, queryEngine, ruleManager, status, &cfg.web)
|
||||||
|
|
||||||
reloadables = append(reloadables, status, targetManager, ruleManager, webHandler, notificationHandler)
|
reloadables = append(reloadables, status, targetManager, ruleManager, webHandler, notifier)
|
||||||
|
|
||||||
if !reloadConfig(cfg.configFile, reloadables...) {
|
if !reloadConfig(cfg.configFile, reloadables...) {
|
||||||
return 1
|
return 1
|
||||||
|
@ -153,14 +153,14 @@ func Main() int {
|
||||||
}
|
}
|
||||||
// The storage has to be fully initialized before registering.
|
// The storage has to be fully initialized before registering.
|
||||||
prometheus.MustRegister(memStorage)
|
prometheus.MustRegister(memStorage)
|
||||||
prometheus.MustRegister(notificationHandler)
|
prometheus.MustRegister(notifier)
|
||||||
prometheus.MustRegister(configSuccess)
|
prometheus.MustRegister(configSuccess)
|
||||||
prometheus.MustRegister(configSuccessTime)
|
prometheus.MustRegister(configSuccessTime)
|
||||||
|
|
||||||
// The notification handler is a dependency of the rule manager. It has to be
|
// The notifieris a dependency of the rule manager. It has to be
|
||||||
// started before and torn down afterwards.
|
// started before and torn down afterwards.
|
||||||
go notificationHandler.Run()
|
go notifier.Run()
|
||||||
defer notificationHandler.Stop()
|
defer notifier.Stop()
|
||||||
|
|
||||||
go ruleManager.Run()
|
go ruleManager.Run()
|
||||||
defer ruleManager.Stop()
|
defer ruleManager.Stop()
|
||||||
|
|
|
@ -199,7 +199,7 @@ func VersionCmd(t cli.Term, _ ...string) int {
|
||||||
if err := tmpl.ExecuteTemplate(&buf, "version", version.Map); err != nil {
|
if err := tmpl.ExecuteTemplate(&buf, "version", version.Map); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
t.Out(strings.TrimSpace(buf.String()))
|
fmt.Fprintln(t.Out(), strings.TrimSpace(buf.String()))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package notification
|
package notifier
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -44,9 +44,9 @@ const (
|
||||||
|
|
||||||
// Handler is responsible for dispatching alert notifications to an
|
// Handler is responsible for dispatching alert notifications to an
|
||||||
// alert manager service.
|
// alert manager service.
|
||||||
type Handler struct {
|
type Notifier struct {
|
||||||
queue model.Alerts
|
queue model.Alerts
|
||||||
opts *HandlerOptions
|
opts *Options
|
||||||
|
|
||||||
more chan struct{}
|
more chan struct{}
|
||||||
mtx sync.RWMutex
|
mtx sync.RWMutex
|
||||||
|
@ -62,18 +62,18 @@ type Handler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandlerOptions are the configurable parameters of a Handler.
|
// HandlerOptions are the configurable parameters of a Handler.
|
||||||
type HandlerOptions struct {
|
type Options struct {
|
||||||
AlertmanagerURL string
|
AlertmanagerURL string
|
||||||
QueueCapacity int
|
QueueCapacity int
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
ExternalLabels model.LabelSet
|
ExternalLabels model.LabelSet
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler constructs a new Handler.
|
// New constructs a neww Notifier.
|
||||||
func New(o *HandlerOptions) *Handler {
|
func New(o *Options) *Notifier {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
return &Handler{
|
return &Notifier{
|
||||||
queue: make(model.Alerts, 0, o.QueueCapacity),
|
queue: make(model.Alerts, 0, o.QueueCapacity),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
@ -124,7 +124,7 @@ func New(o *HandlerOptions) *Handler {
|
||||||
|
|
||||||
// ApplyConfig updates the status state as the new config requires.
|
// ApplyConfig updates the status state as the new config requires.
|
||||||
// Returns true on success.
|
// Returns true on success.
|
||||||
func (n *Handler) ApplyConfig(conf *config.Config) bool {
|
func (n *Notifier) ApplyConfig(conf *config.Config) bool {
|
||||||
n.mtx.Lock()
|
n.mtx.Lock()
|
||||||
defer n.mtx.Unlock()
|
defer n.mtx.Unlock()
|
||||||
|
|
||||||
|
@ -134,14 +134,14 @@ func (n *Handler) ApplyConfig(conf *config.Config) bool {
|
||||||
|
|
||||||
const maxBatchSize = 64
|
const maxBatchSize = 64
|
||||||
|
|
||||||
func (n *Handler) queueLen() int {
|
func (n *Notifier) queueLen() int {
|
||||||
n.mtx.RLock()
|
n.mtx.RLock()
|
||||||
defer n.mtx.RUnlock()
|
defer n.mtx.RUnlock()
|
||||||
|
|
||||||
return len(n.queue)
|
return len(n.queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Handler) nextBatch() []*model.Alert {
|
func (n *Notifier) nextBatch() []*model.Alert {
|
||||||
n.mtx.Lock()
|
n.mtx.Lock()
|
||||||
defer n.mtx.Unlock()
|
defer n.mtx.Unlock()
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ func (n *Handler) nextBatch() []*model.Alert {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run dispatches notifications continuously.
|
// Run dispatches notifications continuously.
|
||||||
func (n *Handler) Run() {
|
func (n *Notifier) Run() {
|
||||||
// Just warn once in the beginning to prevent noisy logs.
|
// Just warn once in the beginning to prevent noisy logs.
|
||||||
if n.opts.AlertmanagerURL == "" {
|
if n.opts.AlertmanagerURL == "" {
|
||||||
log.Warnf("No AlertManager configured, not dispatching any alerts")
|
log.Warnf("No AlertManager configured, not dispatching any alerts")
|
||||||
|
@ -200,9 +200,9 @@ func (n *Handler) Run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubmitReqs queues the given notification requests for processing.
|
// Send queues the given notification requests for processing.
|
||||||
// Panics if called on a handler that is not running.
|
// Panics if called on a handler that is not running.
|
||||||
func (n *Handler) Send(alerts ...*model.Alert) {
|
func (n *Notifier) Send(alerts ...*model.Alert) {
|
||||||
n.mtx.Lock()
|
n.mtx.Lock()
|
||||||
defer n.mtx.Unlock()
|
defer n.mtx.Unlock()
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ func (n *Handler) Send(alerts ...*model.Alert) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// setMore signals that the alert queue has items.
|
// setMore signals that the alert queue has items.
|
||||||
func (n *Handler) setMore() {
|
func (n *Notifier) setMore() {
|
||||||
// If we cannot send on the channel, it means the signal already exists
|
// If we cannot send on the channel, it means the signal already exists
|
||||||
// and has not been consumed yet.
|
// and has not been consumed yet.
|
||||||
select {
|
select {
|
||||||
|
@ -239,11 +239,11 @@ func (n *Handler) setMore() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Handler) postURL() string {
|
func (n *Notifier) postURL() string {
|
||||||
return strings.TrimRight(n.opts.AlertmanagerURL, "/") + alertPushEndpoint
|
return strings.TrimRight(n.opts.AlertmanagerURL, "/") + alertPushEndpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Handler) send(alerts ...*model.Alert) error {
|
func (n *Notifier) send(alerts ...*model.Alert) error {
|
||||||
// Attach external labels before sending alerts.
|
// Attach external labels before sending alerts.
|
||||||
for _, a := range alerts {
|
for _, a := range alerts {
|
||||||
for ln, lv := range n.opts.ExternalLabels {
|
for ln, lv := range n.opts.ExternalLabels {
|
||||||
|
@ -272,14 +272,14 @@ func (n *Handler) send(alerts ...*model.Alert) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop shuts down the notification handler.
|
// Stop shuts down the notification handler.
|
||||||
func (n *Handler) Stop() {
|
func (n *Notifier) Stop() {
|
||||||
log.Info("Stopping notification handler...")
|
log.Info("Stopping notification handler...")
|
||||||
|
|
||||||
n.cancel()
|
n.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Describe implements prometheus.Collector.
|
// Describe implements prometheus.Collector.
|
||||||
func (n *Handler) Describe(ch chan<- *prometheus.Desc) {
|
func (n *Notifier) Describe(ch chan<- *prometheus.Desc) {
|
||||||
ch <- n.latency.Desc()
|
ch <- n.latency.Desc()
|
||||||
ch <- n.errors.Desc()
|
ch <- n.errors.Desc()
|
||||||
ch <- n.sent.Desc()
|
ch <- n.sent.Desc()
|
||||||
|
@ -289,7 +289,7 @@ func (n *Handler) Describe(ch chan<- *prometheus.Desc) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect implements prometheus.Collector.
|
// Collect implements prometheus.Collector.
|
||||||
func (n *Handler) Collect(ch chan<- prometheus.Metric) {
|
func (n *Notifier) Collect(ch chan<- prometheus.Metric) {
|
||||||
n.queueLength.Set(float64(n.queueLen()))
|
n.queueLength.Set(float64(n.queueLen()))
|
||||||
|
|
||||||
ch <- n.latency
|
ch <- n.latency
|
|
@ -11,7 +11,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package notification
|
package notifier
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -50,8 +50,8 @@ func TestHandlerPostURL(t *testing.T) {
|
||||||
out: "http://localhost:9093/prefix/api/v1/alerts",
|
out: "http://localhost:9093/prefix/api/v1/alerts",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
h := &Handler{
|
h := &Notifier{
|
||||||
opts: &HandlerOptions{},
|
opts: &Options{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
@ -63,7 +63,7 @@ func TestHandlerPostURL(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandlerNextBatch(t *testing.T) {
|
func TestHandlerNextBatch(t *testing.T) {
|
||||||
h := New(&HandlerOptions{})
|
h := New(&Options{})
|
||||||
|
|
||||||
for i := range make([]struct{}, 2*maxBatchSize+1) {
|
for i := range make([]struct{}, 2*maxBatchSize+1) {
|
||||||
h.queue = append(h.queue, &model.Alert{
|
h.queue = append(h.queue, &model.Alert{
|
||||||
|
@ -146,7 +146,7 @@ func TestHandlerSend(t *testing.T) {
|
||||||
|
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
h := New(&HandlerOptions{
|
h := New(&Options{
|
||||||
AlertmanagerURL: server.URL,
|
AlertmanagerURL: server.URL,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
ExternalLabels: model.LabelSet{"a": "b"},
|
ExternalLabels: model.LabelSet{"a": "b"},
|
||||||
|
@ -202,7 +202,7 @@ func TestHandlerFull(t *testing.T) {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
h := New(&HandlerOptions{
|
h := New(&Options{
|
||||||
AlertmanagerURL: server.URL,
|
AlertmanagerURL: server.URL,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
QueueCapacity: 3 * maxBatchSize,
|
QueueCapacity: 3 * maxBatchSize,
|
|
@ -99,6 +99,26 @@ var tests = []struct {
|
||||||
input: "0x123",
|
input: "0x123",
|
||||||
expected: []item{{itemNumber, 0, "0x123"}},
|
expected: []item{{itemNumber, 0, "0x123"}},
|
||||||
},
|
},
|
||||||
|
// Test strings.
|
||||||
|
{
|
||||||
|
input: "\"test\\tsequence\"",
|
||||||
|
expected: []item{{itemString, 0, `"test\tsequence"`}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "\"test\\\\.expression\"",
|
||||||
|
expected: []item{{itemString, 0, `"test\\.expression"`}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "\"test\\.expression\"",
|
||||||
|
expected: []item{
|
||||||
|
{itemError, 0, "unknown escape sequence U+002E '.'"},
|
||||||
|
{itemString, 0, `"test\.expression"`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "`test\\.expression`",
|
||||||
|
expected: []item{{itemString, 0, "`test\\.expression`"}},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// See https://github.com/prometheus/prometheus/issues/939.
|
// See https://github.com/prometheus/prometheus/issues/939.
|
||||||
input: ".٩",
|
input: ".٩",
|
||||||
|
|
|
@ -530,35 +530,34 @@ func (p *parser) expr() Expr {
|
||||||
rhs := p.unaryExpr()
|
rhs := p.unaryExpr()
|
||||||
|
|
||||||
// Assign the new root based on the precedence of the LHS and RHS operators.
|
// Assign the new root based on the precedence of the LHS and RHS operators.
|
||||||
if lhs, ok := expr.(*BinaryExpr); ok && lhs.Op.precedence() < op.precedence() {
|
expr = p.balance(expr, op, rhs, vecMatching, returnBool)
|
||||||
expr = &BinaryExpr{
|
}
|
||||||
Op: lhs.Op,
|
}
|
||||||
LHS: lhs.LHS,
|
|
||||||
RHS: &BinaryExpr{
|
|
||||||
Op: op,
|
|
||||||
LHS: lhs.RHS,
|
|
||||||
RHS: rhs,
|
|
||||||
VectorMatching: vecMatching,
|
|
||||||
ReturnBool: returnBool,
|
|
||||||
},
|
|
||||||
VectorMatching: lhs.VectorMatching,
|
|
||||||
}
|
|
||||||
if op.isComparisonOperator() && !returnBool && rhs.Type() == model.ValScalar && lhs.RHS.Type() == model.ValScalar {
|
|
||||||
p.errorf("comparisons between scalars must use BOOL modifier")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expr = &BinaryExpr{
|
|
||||||
Op: op,
|
|
||||||
LHS: expr,
|
|
||||||
RHS: rhs,
|
|
||||||
VectorMatching: vecMatching,
|
|
||||||
ReturnBool: returnBool,
|
|
||||||
}
|
|
||||||
if op.isComparisonOperator() && !returnBool && rhs.Type() == model.ValScalar && expr.Type() == model.ValScalar {
|
|
||||||
p.errorf("comparisons between scalars must use BOOL modifier")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
func (p *parser) balance(lhs Expr, op itemType, rhs Expr, vecMatching *VectorMatching, returnBool bool) *BinaryExpr {
|
||||||
|
if lhsBE, ok := lhs.(*BinaryExpr); ok && lhsBE.Op.precedence() < op.precedence() {
|
||||||
|
balanced := p.balance(lhsBE.RHS, op, rhs, vecMatching, returnBool)
|
||||||
|
if lhsBE.Op.isComparisonOperator() && !lhsBE.ReturnBool && balanced.Type() == model.ValScalar && lhsBE.LHS.Type() == model.ValScalar {
|
||||||
|
p.errorf("comparisons between scalars must use BOOL modifier")
|
||||||
|
}
|
||||||
|
return &BinaryExpr{
|
||||||
|
Op: lhsBE.Op,
|
||||||
|
LHS: lhsBE.LHS,
|
||||||
|
RHS: balanced,
|
||||||
|
VectorMatching: lhsBE.VectorMatching,
|
||||||
|
ReturnBool: lhsBE.ReturnBool,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if op.isComparisonOperator() && !returnBool && rhs.Type() == model.ValScalar && lhs.Type() == model.ValScalar {
|
||||||
|
p.errorf("comparisons between scalars must use BOOL modifier")
|
||||||
|
}
|
||||||
|
return &BinaryExpr{
|
||||||
|
Op: op,
|
||||||
|
LHS: lhs,
|
||||||
|
RHS: rhs,
|
||||||
|
VectorMatching: vecMatching,
|
||||||
|
ReturnBool: returnBool,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,20 @@ var testExpr = []struct {
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
input: "1 < bool 2 - 1 * 2",
|
||||||
|
expected: &BinaryExpr{
|
||||||
|
Op: itemLSS,
|
||||||
|
ReturnBool: true,
|
||||||
|
LHS: &NumberLiteral{1},
|
||||||
|
RHS: &BinaryExpr{
|
||||||
|
Op: itemSUB,
|
||||||
|
LHS: &NumberLiteral{2},
|
||||||
|
RHS: &BinaryExpr{
|
||||||
|
Op: itemMUL, LHS: &NumberLiteral{1}, RHS: &NumberLiteral{2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
input: "-some_metric", expected: &UnaryExpr{
|
input: "-some_metric", expected: &UnaryExpr{
|
||||||
Op: itemSUB,
|
Op: itemSUB,
|
||||||
|
|
|
@ -425,7 +425,7 @@ func (t *Test) Run() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// exec processes a single step of the test
|
// exec processes a single step of the test.
|
||||||
func (t *Test) exec(tc testCommand) error {
|
func (t *Test) exec(tc testCommand) error {
|
||||||
switch cmd := tc.(type) {
|
switch cmd := tc.(type) {
|
||||||
case *clearCmd:
|
case *clearCmd:
|
||||||
|
|
|
@ -15,7 +15,6 @@ package discovery
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -24,6 +23,7 @@ import (
|
||||||
consul "github.com/hashicorp/consul/api"
|
consul "github.com/hashicorp/consul/api"
|
||||||
"github.com/prometheus/common/log"
|
"github.com/prometheus/common/log"
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
)
|
)
|
||||||
|
@ -113,52 +113,24 @@ func NewConsulDiscovery(conf *config.ConsulSDConfig) (*ConsulDiscovery, error) {
|
||||||
return cd, nil
|
return cd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources implements the TargetProvider interface.
|
|
||||||
func (cd *ConsulDiscovery) Sources() []string {
|
|
||||||
clientConf := *cd.clientConf
|
|
||||||
clientConf.HttpClient = &http.Client{Timeout: 5 * time.Second}
|
|
||||||
|
|
||||||
client, err := consul.NewClient(&clientConf)
|
|
||||||
if err != nil {
|
|
||||||
// NewClient always returns a nil error.
|
|
||||||
panic(fmt.Errorf("discovery.ConsulDiscovery.Sources: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
srvs, _, err := client.Catalog().Services(nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error refreshing service list: %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cd.mu.Lock()
|
|
||||||
defer cd.mu.Unlock()
|
|
||||||
|
|
||||||
srcs := make([]string, 0, len(srvs))
|
|
||||||
for name := range srvs {
|
|
||||||
if _, ok := cd.scrapedServices[name]; len(cd.scrapedServices) == 0 || ok {
|
|
||||||
srcs = append(srcs, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return srcs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run implements the TargetProvider interface.
|
// Run implements the TargetProvider interface.
|
||||||
func (cd *ConsulDiscovery) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (cd *ConsulDiscovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
defer cd.stop()
|
defer cd.stop()
|
||||||
|
|
||||||
update := make(chan *consulService, 10)
|
update := make(chan *consulService, 10)
|
||||||
go cd.watchServices(update, done)
|
go cd.watchServices(update, ctx.Done())
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case srv := <-update:
|
case srv := <-update:
|
||||||
if srv.removed {
|
if srv.removed {
|
||||||
close(srv.done)
|
close(srv.done)
|
||||||
|
|
||||||
// Send clearing update.
|
// Send clearing update.
|
||||||
ch <- config.TargetGroup{Source: srv.name}
|
ch <- []*config.TargetGroup{{Source: srv.name}}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Launch watcher for the service.
|
// Launch watcher for the service.
|
||||||
|
@ -244,7 +216,7 @@ func (cd *ConsulDiscovery) watchServices(update chan<- *consulService, done <-ch
|
||||||
|
|
||||||
// watchService retrieves updates about srv from Consul's service endpoint.
|
// watchService retrieves updates about srv from Consul's service endpoint.
|
||||||
// On a potential update the resulting target group is sent to ch.
|
// On a potential update the resulting target group is sent to ch.
|
||||||
func (cd *ConsulDiscovery) watchService(srv *consulService, ch chan<- config.TargetGroup) {
|
func (cd *ConsulDiscovery) watchService(srv *consulService, ch chan<- []*config.TargetGroup) {
|
||||||
catalog := cd.client.Catalog()
|
catalog := cd.client.Catalog()
|
||||||
for {
|
for {
|
||||||
nodes, meta, err := catalog.Service(srv.name, "", &consul.QueryOptions{
|
nodes, meta, err := catalog.Service(srv.name, "", &consul.QueryOptions{
|
||||||
|
@ -288,7 +260,11 @@ func (cd *ConsulDiscovery) watchService(srv *consulService, ch chan<- config.Tar
|
||||||
default:
|
default:
|
||||||
// Continue.
|
// Continue.
|
||||||
}
|
}
|
||||||
ch <- srv.tgroup
|
// TODO(fabxc): do a copy for now to avoid races. The integration
|
||||||
|
// needs needs some general cleanup.
|
||||||
|
tg := srv.tgroup
|
||||||
|
ch <- []*config.TargetGroup{&tg}
|
||||||
|
|
||||||
cd.mu.Unlock()
|
cd.mu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/common/log"
|
"github.com/prometheus/common/log"
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
)
|
)
|
||||||
|
@ -91,7 +92,7 @@ func NewDNSDiscovery(conf *config.DNSSDConfig) *DNSDiscovery {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run implements the TargetProvider interface.
|
// Run implements the TargetProvider interface.
|
||||||
func (dd *DNSDiscovery) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (dd *DNSDiscovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
|
|
||||||
ticker := time.NewTicker(dd.interval)
|
ticker := time.NewTicker(dd.interval)
|
||||||
|
@ -104,23 +105,15 @@ func (dd *DNSDiscovery) Run(ch chan<- config.TargetGroup, done <-chan struct{})
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
dd.refreshAll(ch)
|
dd.refreshAll(ch)
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources implements the TargetProvider interface.
|
func (dd *DNSDiscovery) refreshAll(ch chan<- []*config.TargetGroup) {
|
||||||
func (dd *DNSDiscovery) Sources() []string {
|
|
||||||
var srcs []string
|
|
||||||
for _, name := range dd.names {
|
|
||||||
srcs = append(srcs, name)
|
|
||||||
}
|
|
||||||
return srcs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dd *DNSDiscovery) refreshAll(ch chan<- config.TargetGroup) {
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
wg.Add(len(dd.names))
|
wg.Add(len(dd.names))
|
||||||
for _, name := range dd.names {
|
for _, name := range dd.names {
|
||||||
go func(n string) {
|
go func(n string) {
|
||||||
|
@ -130,10 +123,11 @@ func (dd *DNSDiscovery) refreshAll(ch chan<- config.TargetGroup) {
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}(name)
|
}(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dd *DNSDiscovery) refresh(name string, ch chan<- config.TargetGroup) error {
|
func (dd *DNSDiscovery) refresh(name string, ch chan<- []*config.TargetGroup) error {
|
||||||
response, err := lookupAll(name, dd.qtype)
|
response, err := lookupAll(name, dd.qtype)
|
||||||
dnsSDLookupsCount.Inc()
|
dnsSDLookupsCount.Inc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -141,7 +135,8 @@ func (dd *DNSDiscovery) refresh(name string, ch chan<- config.TargetGroup) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var tg config.TargetGroup
|
tg := &config.TargetGroup{}
|
||||||
|
|
||||||
for _, record := range response.Answer {
|
for _, record := range response.Answer {
|
||||||
target := model.LabelValue("")
|
target := model.LabelValue("")
|
||||||
switch addr := record.(type) {
|
switch addr := record.(type) {
|
||||||
|
@ -166,7 +161,7 @@ func (dd *DNSDiscovery) refresh(name string, ch chan<- config.TargetGroup) error
|
||||||
}
|
}
|
||||||
|
|
||||||
tg.Source = name
|
tg.Source = name
|
||||||
ch <- tg
|
ch <- []*config.TargetGroup{tg}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/aws/aws-sdk-go/aws/defaults"
|
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||||
"github.com/prometheus/common/log"
|
"github.com/prometheus/common/log"
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/service/ec2"
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
|
@ -46,7 +47,6 @@ const (
|
||||||
// the TargetProvider interface.
|
// the TargetProvider interface.
|
||||||
type EC2Discovery struct {
|
type EC2Discovery struct {
|
||||||
aws *aws.Config
|
aws *aws.Config
|
||||||
done chan struct{}
|
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
port int
|
port int
|
||||||
}
|
}
|
||||||
|
@ -62,14 +62,13 @@ func NewEC2Discovery(conf *config.EC2SDConfig) *EC2Discovery {
|
||||||
Region: &conf.Region,
|
Region: &conf.Region,
|
||||||
Credentials: creds,
|
Credentials: creds,
|
||||||
},
|
},
|
||||||
done: make(chan struct{}),
|
|
||||||
interval: time.Duration(conf.RefreshInterval),
|
interval: time.Duration(conf.RefreshInterval),
|
||||||
port: conf.Port,
|
port: conf.Port,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run implements the TargetProvider interface.
|
// Run implements the TargetProvider interface.
|
||||||
func (ed *EC2Discovery) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (ed *EC2Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
|
|
||||||
ticker := time.NewTicker(ed.interval)
|
ticker := time.NewTicker(ed.interval)
|
||||||
|
@ -80,7 +79,7 @@ func (ed *EC2Discovery) Run(ch chan<- config.TargetGroup, done <-chan struct{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
} else {
|
} else {
|
||||||
ch <- *tg
|
ch <- []*config.TargetGroup{tg}
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -90,19 +89,14 @@ func (ed *EC2Discovery) Run(ch chan<- config.TargetGroup, done <-chan struct{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
} else {
|
} else {
|
||||||
ch <- *tg
|
ch <- []*config.TargetGroup{tg}
|
||||||
}
|
}
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources implements the TargetProvider interface.
|
|
||||||
func (ed *EC2Discovery) Sources() []string {
|
|
||||||
return []string{*ed.aws.Region}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ed *EC2Discovery) refresh() (*config.TargetGroup, error) {
|
func (ed *EC2Discovery) refresh() (*config.TargetGroup, error) {
|
||||||
ec2s := ec2.New(ed.aws)
|
ec2s := ec2.New(ed.aws)
|
||||||
tg := &config.TargetGroup{
|
tg := &config.TargetGroup{
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
"github.com/prometheus/common/log"
|
"github.com/prometheus/common/log"
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
"gopkg.in/fsnotify.v1"
|
"gopkg.in/fsnotify.v1"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
@ -53,23 +54,6 @@ func NewFileDiscovery(conf *config.FileSDConfig) *FileDiscovery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources implements the TargetProvider interface.
|
|
||||||
func (fd *FileDiscovery) Sources() []string {
|
|
||||||
var srcs []string
|
|
||||||
// As we allow multiple target groups per file we have no choice
|
|
||||||
// but to parse them all.
|
|
||||||
for _, p := range fd.listFiles() {
|
|
||||||
tgroups, err := readFile(p)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error reading file %q: %s", p, err)
|
|
||||||
}
|
|
||||||
for _, tg := range tgroups {
|
|
||||||
srcs = append(srcs, tg.Source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return srcs
|
|
||||||
}
|
|
||||||
|
|
||||||
// listFiles returns a list of all files that match the configured patterns.
|
// listFiles returns a list of all files that match the configured patterns.
|
||||||
func (fd *FileDiscovery) listFiles() []string {
|
func (fd *FileDiscovery) listFiles() []string {
|
||||||
var paths []string
|
var paths []string
|
||||||
|
@ -103,7 +87,7 @@ func (fd *FileDiscovery) watchFiles() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run implements the TargetProvider interface.
|
// Run implements the TargetProvider interface.
|
||||||
func (fd *FileDiscovery) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (fd *FileDiscovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
defer fd.stop()
|
defer fd.stop()
|
||||||
|
|
||||||
|
@ -123,11 +107,11 @@ func (fd *FileDiscovery) Run(ch chan<- config.TargetGroup, done <-chan struct{})
|
||||||
// Stopping has priority over refreshing. Thus we wrap the actual select
|
// Stopping has priority over refreshing. Thus we wrap the actual select
|
||||||
// clause to always catch done signals.
|
// clause to always catch done signals.
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|
||||||
case event := <-fd.watcher.Events:
|
case event := <-fd.watcher.Events:
|
||||||
|
@ -188,7 +172,7 @@ func (fd *FileDiscovery) stop() {
|
||||||
|
|
||||||
// refresh reads all files matching the discovery's patterns and sends the respective
|
// refresh reads all files matching the discovery's patterns and sends the respective
|
||||||
// updated target groups through the channel.
|
// updated target groups through the channel.
|
||||||
func (fd *FileDiscovery) refresh(ch chan<- config.TargetGroup) {
|
func (fd *FileDiscovery) refresh(ch chan<- []*config.TargetGroup) {
|
||||||
ref := map[string]int{}
|
ref := map[string]int{}
|
||||||
for _, p := range fd.listFiles() {
|
for _, p := range fd.listFiles() {
|
||||||
tgroups, err := readFile(p)
|
tgroups, err := readFile(p)
|
||||||
|
@ -198,9 +182,8 @@ func (fd *FileDiscovery) refresh(ch chan<- config.TargetGroup) {
|
||||||
ref[p] = fd.lastRefresh[p]
|
ref[p] = fd.lastRefresh[p]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, tg := range tgroups {
|
ch <- tgroups
|
||||||
ch <- *tg
|
|
||||||
}
|
|
||||||
ref[p] = len(tgroups)
|
ref[p] = len(tgroups)
|
||||||
}
|
}
|
||||||
// Send empty updates for sources that disappeared.
|
// Send empty updates for sources that disappeared.
|
||||||
|
@ -208,7 +191,9 @@ func (fd *FileDiscovery) refresh(ch chan<- config.TargetGroup) {
|
||||||
m, ok := ref[f]
|
m, ok := ref[f]
|
||||||
if !ok || n > m {
|
if !ok || n > m {
|
||||||
for i := m; i < n; i++ {
|
for i := m; i < n; i++ {
|
||||||
ch <- config.TargetGroup{Source: fileSource(f, i)}
|
ch <- []*config.TargetGroup{
|
||||||
|
{Source: fileSource(f, i)},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
)
|
)
|
||||||
|
@ -27,17 +28,17 @@ func testFileSD(t *testing.T, ext string) {
|
||||||
conf.RefreshInterval = model.Duration(1 * time.Hour)
|
conf.RefreshInterval = model.Duration(1 * time.Hour)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fsd = NewFileDiscovery(&conf)
|
fsd = NewFileDiscovery(&conf)
|
||||||
ch = make(chan config.TargetGroup)
|
ch = make(chan []*config.TargetGroup)
|
||||||
done = make(chan struct{})
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
)
|
)
|
||||||
go fsd.Run(ch, done)
|
go fsd.Run(ctx, ch)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(25 * time.Millisecond):
|
case <-time.After(25 * time.Millisecond):
|
||||||
// Expected.
|
// Expected.
|
||||||
case tg := <-ch:
|
case tgs := <-ch:
|
||||||
t.Fatalf("Unexpected target group in file discovery: %s", tg)
|
t.Fatalf("Unexpected target groups in file discovery: %s", tgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
newf, err := os.Create("fixtures/_test" + ext)
|
newf, err := os.Create("fixtures/_test" + ext)
|
||||||
|
@ -58,37 +59,45 @@ func testFileSD(t *testing.T, ext string) {
|
||||||
}
|
}
|
||||||
newf.Close()
|
newf.Close()
|
||||||
|
|
||||||
// The files contain two target groups which are read and sent in order.
|
timeout := time.After(15 * time.Second)
|
||||||
select {
|
// The files contain two target groups.
|
||||||
case <-time.After(15 * time.Second):
|
retry:
|
||||||
t.Fatalf("Expected new target group but got none")
|
for {
|
||||||
case tg := <-ch:
|
select {
|
||||||
if _, ok := tg.Labels["foo"]; !ok {
|
case <-timeout:
|
||||||
t.Fatalf("Label not parsed")
|
t.Fatalf("Expected new target group but got none")
|
||||||
}
|
case tgs := <-ch:
|
||||||
if tg.String() != fmt.Sprintf("fixtures/_test%s:0", ext) {
|
if len(tgs) != 2 {
|
||||||
t.Fatalf("Unexpected target group %s", tg)
|
continue retry // Potentially a partial write, just retry.
|
||||||
}
|
}
|
||||||
}
|
tg := tgs[0]
|
||||||
select {
|
|
||||||
case <-time.After(15 * time.Second):
|
if _, ok := tg.Labels["foo"]; !ok {
|
||||||
t.Fatalf("Expected new target group but got none")
|
t.Fatalf("Label not parsed")
|
||||||
case tg := <-ch:
|
}
|
||||||
if tg.String() != fmt.Sprintf("fixtures/_test%s:1", ext) {
|
if tg.String() != fmt.Sprintf("fixtures/_test%s:0", ext) {
|
||||||
t.Fatalf("Unexpected target group %s", tg)
|
t.Fatalf("Unexpected target group %s", tg)
|
||||||
|
}
|
||||||
|
|
||||||
|
tg = tgs[1]
|
||||||
|
if tg.String() != fmt.Sprintf("fixtures/_test%s:1", ext) {
|
||||||
|
t.Fatalf("Unexpected target groups %s", tg)
|
||||||
|
}
|
||||||
|
break retry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Based on unknown circumstances, sometimes fsnotify will trigger more events in
|
// Based on unknown circumstances, sometimes fsnotify will trigger more events in
|
||||||
// some runs (which might be empty, chains of different operations etc.).
|
// some runs (which might be empty, chains of different operations etc.).
|
||||||
// We have to drain those (as the target manager would) to avoid deadlocking and must
|
// We have to drain those (as the target manager would) to avoid deadlocking and must
|
||||||
// not try to make sense of it all...
|
// not try to make sense of it all...
|
||||||
drained := make(chan struct{})
|
drained := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
for tg := range ch {
|
for tgs := range ch {
|
||||||
// Below we will change the file to a bad syntax. Previously extracted target
|
// Below we will change the file to a bad syntax. Previously extracted target
|
||||||
// groups must not be deleted via sending an empty target group.
|
// groups must not be deleted via sending an empty target group.
|
||||||
if len(tg.Targets) == 0 {
|
if len(tgs[0].Targets) == 0 {
|
||||||
t.Errorf("Unexpected empty target group received: %s", tg)
|
t.Errorf("Unexpected empty target groups received: %s", tgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(drained)
|
close(drained)
|
||||||
|
@ -107,6 +116,6 @@ func testFileSD(t *testing.T, ext string) {
|
||||||
|
|
||||||
os.Rename(newf.Name(), "fixtures/_test"+ext)
|
os.Rename(newf.Name(), "fixtures/_test"+ext)
|
||||||
|
|
||||||
close(done)
|
cancel()
|
||||||
<-drained
|
<-drained
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
|
|
||||||
"github.com/prometheus/common/log"
|
"github.com/prometheus/common/log"
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/util/httputil"
|
"github.com/prometheus/prometheus/util/httputil"
|
||||||
|
@ -94,75 +95,35 @@ func (kd *Discovery) Initialize() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources implements the TargetProvider interface.
|
|
||||||
func (kd *Discovery) Sources() []string {
|
|
||||||
sourceNames := make([]string, 0, len(kd.apiServers))
|
|
||||||
for _, apiServer := range kd.apiServers {
|
|
||||||
sourceNames = append(sourceNames, apiServersTargetGroupName+":"+apiServer.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes, _, err := kd.getNodes()
|
|
||||||
if err != nil {
|
|
||||||
// If we can't list nodes then we can't watch them. Assume this is a misconfiguration
|
|
||||||
// & log & return empty.
|
|
||||||
log.Errorf("Unable to initialize Kubernetes nodes: %s", err)
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
sourceNames = append(sourceNames, kd.nodeSources(nodes)...)
|
|
||||||
|
|
||||||
services, _, err := kd.getServices()
|
|
||||||
if err != nil {
|
|
||||||
// If we can't list services then we can't watch them. Assume this is a misconfiguration
|
|
||||||
// & log & return empty.
|
|
||||||
log.Errorf("Unable to initialize Kubernetes services: %s", err)
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
sourceNames = append(sourceNames, kd.serviceSources(services)...)
|
|
||||||
|
|
||||||
return sourceNames
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kd *Discovery) nodeSources(nodes map[string]*Node) []string {
|
|
||||||
var sourceNames []string
|
|
||||||
for name := range nodes {
|
|
||||||
sourceNames = append(sourceNames, nodesTargetGroupName+":"+name)
|
|
||||||
}
|
|
||||||
return sourceNames
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kd *Discovery) serviceSources(services map[string]map[string]*Service) []string {
|
|
||||||
var sourceNames []string
|
|
||||||
for _, ns := range services {
|
|
||||||
for _, service := range ns {
|
|
||||||
sourceNames = append(sourceNames, serviceSource(service))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sourceNames
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run implements the TargetProvider interface.
|
// Run implements the TargetProvider interface.
|
||||||
func (kd *Discovery) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (kd *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
|
|
||||||
if tg := kd.updateAPIServersTargetGroup(); tg != nil {
|
// Send an initial full view.
|
||||||
select {
|
// TODO(fabxc): this does not include all available services and service
|
||||||
case ch <- *tg:
|
// endpoints yet. Service endpoints were also missing in the previous Sources() method.
|
||||||
case <-done:
|
var all []*config.TargetGroup
|
||||||
return
|
|
||||||
}
|
all = append(all, kd.updateAPIServersTargetGroup())
|
||||||
|
all = append(all, kd.updateNodesTargetGroup())
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ch <- all:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
retryInterval := time.Duration(kd.Conf.RetryInterval)
|
retryInterval := time.Duration(kd.Conf.RetryInterval)
|
||||||
|
|
||||||
update := make(chan interface{}, 10)
|
update := make(chan interface{}, 10)
|
||||||
|
|
||||||
go kd.watchNodes(update, done, retryInterval)
|
go kd.watchNodes(update, ctx.Done(), retryInterval)
|
||||||
go kd.startServiceWatch(update, done, retryInterval)
|
go kd.startServiceWatch(update, ctx.Done(), retryInterval)
|
||||||
|
|
||||||
var tg *config.TargetGroup
|
var tg *config.TargetGroup
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case event := <-update:
|
case event := <-update:
|
||||||
switch obj := event.(type) {
|
switch obj := event.(type) {
|
||||||
|
@ -181,8 +142,8 @@ func (kd *Discovery) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case ch <- *tg:
|
case ch <- []*config.TargetGroup{tg}:
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,7 +226,13 @@ func (kd *Discovery) updateNodesTargetGroup() *config.TargetGroup {
|
||||||
|
|
||||||
// Now let's loop through the nodes & add them to the target group with appropriate labels.
|
// Now let's loop through the nodes & add them to the target group with appropriate labels.
|
||||||
for nodeName, node := range kd.nodes {
|
for nodeName, node := range kd.nodes {
|
||||||
address := fmt.Sprintf("%s:%d", node.Status.Addresses[0].Address, kd.Conf.KubeletPort)
|
nodeAddress, err := nodeHostIP(node)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Skipping node %s: %s", node.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
address := fmt.Sprintf("%s:%d", nodeAddress.String(), kd.Conf.KubeletPort)
|
||||||
|
|
||||||
t := model.LabelSet{
|
t := model.LabelSet{
|
||||||
model.AddressLabel: model.LabelValue(address),
|
model.AddressLabel: model.LabelValue(address),
|
||||||
|
@ -744,3 +711,27 @@ func until(f func(), period time.Duration, stopCh <-chan struct{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nodeHostIP returns the provided node's address, based on the priority:
|
||||||
|
// 1. NodeInternalIP
|
||||||
|
// 2. NodeExternalIP
|
||||||
|
// 3. NodeLegacyHostIP
|
||||||
|
//
|
||||||
|
// Copied from k8s.io/kubernetes/pkg/util/node/node.go
|
||||||
|
func nodeHostIP(node *Node) (net.IP, error) {
|
||||||
|
addresses := node.Status.Addresses
|
||||||
|
addressMap := make(map[NodeAddressType][]NodeAddress)
|
||||||
|
for i := range addresses {
|
||||||
|
addressMap[addresses[i].Type] = append(addressMap[addresses[i].Type], addresses[i])
|
||||||
|
}
|
||||||
|
if addresses, ok := addressMap[NodeInternalIP]; ok {
|
||||||
|
return net.ParseIP(addresses[0].Address), nil
|
||||||
|
}
|
||||||
|
if addresses, ok := addressMap[NodeExternalIP]; ok {
|
||||||
|
return net.ParseIP(addresses[0].Address), nil
|
||||||
|
}
|
||||||
|
if addresses, ok := addressMap[NodeLegacyHostIP]; ok {
|
||||||
|
return net.ParseIP(addresses[0].Address), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("host IP unknown; known addresses: %v", addresses)
|
||||||
|
}
|
||||||
|
|
|
@ -202,11 +202,13 @@ type NodeStatus struct {
|
||||||
|
|
||||||
type NodeAddressType string
|
type NodeAddressType string
|
||||||
|
|
||||||
// These are valid address type of node.
|
// These are valid address types of node. NodeLegacyHostIP is used to transit
|
||||||
|
// from out-dated HostIP field to NodeAddress.
|
||||||
const (
|
const (
|
||||||
NodeHostName NodeAddressType = "Hostname"
|
NodeLegacyHostIP NodeAddressType = "LegacyHostIP"
|
||||||
NodeExternalIP NodeAddressType = "ExternalIP"
|
NodeHostName NodeAddressType = "Hostname"
|
||||||
NodeInternalIP NodeAddressType = "InternalIP"
|
NodeExternalIP NodeAddressType = "ExternalIP"
|
||||||
|
NodeInternalIP NodeAddressType = "InternalIP"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NodeAddress struct {
|
type NodeAddress struct {
|
||||||
|
|
|
@ -17,6 +17,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/common/log"
|
"github.com/prometheus/common/log"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/retrieval/discovery/marathon"
|
"github.com/prometheus/prometheus/retrieval/discovery/marathon"
|
||||||
)
|
)
|
||||||
|
@ -40,25 +42,13 @@ func NewMarathonDiscovery(conf *config.MarathonSDConfig) *MarathonDiscovery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources implements the TargetProvider interface.
|
|
||||||
func (md *MarathonDiscovery) Sources() []string {
|
|
||||||
var sources []string
|
|
||||||
tgroups, err := md.fetchTargetGroups()
|
|
||||||
if err == nil {
|
|
||||||
for source := range tgroups {
|
|
||||||
sources = append(sources, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sources
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run implements the TargetProvider interface.
|
// Run implements the TargetProvider interface.
|
||||||
func (md *MarathonDiscovery) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (md *MarathonDiscovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-time.After(md.refreshInterval):
|
case <-time.After(md.refreshInterval):
|
||||||
err := md.updateServices(ch)
|
err := md.updateServices(ch)
|
||||||
|
@ -69,23 +59,24 @@ func (md *MarathonDiscovery) Run(ch chan<- config.TargetGroup, done <-chan struc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MarathonDiscovery) updateServices(ch chan<- config.TargetGroup) error {
|
func (md *MarathonDiscovery) updateServices(ch chan<- []*config.TargetGroup) error {
|
||||||
targetMap, err := md.fetchTargetGroups()
|
targetMap, err := md.fetchTargetGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update services which are still present
|
all := make([]*config.TargetGroup, 0, len(targetMap))
|
||||||
for _, tg := range targetMap {
|
for _, tg := range targetMap {
|
||||||
ch <- *tg
|
all = append(all, tg)
|
||||||
}
|
}
|
||||||
|
ch <- all
|
||||||
|
|
||||||
// Remove services which did disappear
|
// Remove services which did disappear
|
||||||
for source := range md.lastRefresh {
|
for source := range md.lastRefresh {
|
||||||
_, ok := targetMap[source]
|
_, ok := targetMap[source]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debugf("Removing group for %s", source)
|
log.Debugf("Removing group for %s", source)
|
||||||
ch <- config.TargetGroup{Source: source}
|
ch <- []*config.TargetGroup{{Source: source}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/retrieval/discovery/marathon"
|
"github.com/prometheus/prometheus/retrieval/discovery/marathon"
|
||||||
|
@ -26,8 +27,8 @@ import (
|
||||||
|
|
||||||
var marathonValidLabel = map[string]string{"prometheus": "yes"}
|
var marathonValidLabel = map[string]string{"prometheus": "yes"}
|
||||||
|
|
||||||
func newTestDiscovery(client marathon.AppListClient) (chan config.TargetGroup, *MarathonDiscovery) {
|
func newTestDiscovery(client marathon.AppListClient) (chan []*config.TargetGroup, *MarathonDiscovery) {
|
||||||
ch := make(chan config.TargetGroup)
|
ch := make(chan []*config.TargetGroup)
|
||||||
md := NewMarathonDiscovery(&config.MarathonSDConfig{
|
md := NewMarathonDiscovery(&config.MarathonSDConfig{
|
||||||
Servers: []string{"http://localhost:8080"},
|
Servers: []string{"http://localhost:8080"},
|
||||||
})
|
})
|
||||||
|
@ -60,7 +61,9 @@ func TestMarathonSDEmptyList(t *testing.T) {
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
case tg := <-ch:
|
case tg := <-ch:
|
||||||
t.Fatalf("Got group: %v", tg)
|
if len(tg) > 0 {
|
||||||
|
t.Fatalf("Got group: %v", tg)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -96,7 +99,9 @@ func TestMarathonSDSendGroup(t *testing.T) {
|
||||||
})
|
})
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
case tg := <-ch:
|
case tgs := <-ch:
|
||||||
|
tg := tgs[0]
|
||||||
|
|
||||||
if tg.Source != "test-service" {
|
if tg.Source != "test-service" {
|
||||||
t.Fatalf("Wrong target group name: %s", tg.Source)
|
t.Fatalf("Wrong target group name: %s", tg.Source)
|
||||||
}
|
}
|
||||||
|
@ -121,9 +126,10 @@ func TestMarathonSDRemoveApp(t *testing.T) {
|
||||||
ch, md := newTestDiscovery(func(url string) (*marathon.AppList, error) {
|
ch, md := newTestDiscovery(func(url string) (*marathon.AppList, error) {
|
||||||
return marathonTestAppList(marathonValidLabel, 1), nil
|
return marathonTestAppList(marathonValidLabel, 1), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
up1 := <-ch
|
up1 := (<-ch)[0]
|
||||||
up2 := <-ch
|
up2 := (<-ch)[0]
|
||||||
if up2.Source != up1.Source {
|
if up2.Source != up1.Source {
|
||||||
t.Fatalf("Source is different: %s", up2)
|
t.Fatalf("Source is different: %s", up2)
|
||||||
if len(up2.Targets) > 0 {
|
if len(up2.Targets) > 0 {
|
||||||
|
@ -145,33 +151,25 @@ func TestMarathonSDRemoveApp(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarathonSDSources(t *testing.T) {
|
|
||||||
_, md := newTestDiscovery(func(url string) (*marathon.AppList, error) {
|
|
||||||
return marathonTestAppList(marathonValidLabel, 1), nil
|
|
||||||
})
|
|
||||||
sources := md.Sources()
|
|
||||||
if len(sources) != 1 {
|
|
||||||
t.Fatalf("Wrong number of sources: %s", sources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarathonSDRunAndStop(t *testing.T) {
|
func TestMarathonSDRunAndStop(t *testing.T) {
|
||||||
ch, md := newTestDiscovery(func(url string) (*marathon.AppList, error) {
|
ch, md := newTestDiscovery(func(url string) (*marathon.AppList, error) {
|
||||||
return marathonTestAppList(marathonValidLabel, 1), nil
|
return marathonTestAppList(marathonValidLabel, 1), nil
|
||||||
})
|
})
|
||||||
md.refreshInterval = time.Millisecond * 10
|
md.refreshInterval = time.Millisecond * 10
|
||||||
done := make(chan struct{})
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
case <-ch:
|
case <-ch:
|
||||||
close(done)
|
cancel()
|
||||||
case <-time.After(md.refreshInterval * 3):
|
case <-time.After(md.refreshInterval * 3):
|
||||||
close(done)
|
cancel()
|
||||||
t.Fatalf("Update took too long.")
|
t.Fatalf("Update took too long.")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
md.Run(ch, done)
|
|
||||||
|
md.Run(ctx, ch)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ch:
|
case <-ch:
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
|
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
"github.com/samuel/go-zookeeper/zk"
|
"github.com/samuel/go-zookeeper/zk"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/util/treecache"
|
"github.com/prometheus/prometheus/util/treecache"
|
||||||
|
@ -47,7 +48,7 @@ type NerveDiscovery struct {
|
||||||
conn *zk.Conn
|
conn *zk.Conn
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
sources map[string]*config.TargetGroup
|
sources map[string]*config.TargetGroup
|
||||||
sdUpdates *chan<- config.TargetGroup
|
sdUpdates *chan<- []*config.TargetGroup
|
||||||
updates chan treecache.ZookeeperTreeCacheEvent
|
updates chan treecache.ZookeeperTreeCacheEvent
|
||||||
treeCaches []*treecache.ZookeeperTreeCache
|
treeCaches []*treecache.ZookeeperTreeCache
|
||||||
}
|
}
|
||||||
|
@ -73,17 +74,6 @@ func NewNerveDiscovery(conf *config.NerveSDConfig) *NerveDiscovery {
|
||||||
return sd
|
return sd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources implements the TargetProvider interface.
|
|
||||||
func (sd *NerveDiscovery) Sources() []string {
|
|
||||||
sd.mu.RLock()
|
|
||||||
defer sd.mu.RUnlock()
|
|
||||||
srcs := []string{}
|
|
||||||
for t := range sd.sources {
|
|
||||||
srcs = append(srcs, t)
|
|
||||||
}
|
|
||||||
return srcs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sd *NerveDiscovery) processUpdates() {
|
func (sd *NerveDiscovery) processUpdates() {
|
||||||
defer sd.conn.Close()
|
defer sd.conn.Close()
|
||||||
for event := range sd.updates {
|
for event := range sd.updates {
|
||||||
|
@ -104,7 +94,7 @@ func (sd *NerveDiscovery) processUpdates() {
|
||||||
}
|
}
|
||||||
sd.mu.Unlock()
|
sd.mu.Unlock()
|
||||||
if sd.sdUpdates != nil {
|
if sd.sdUpdates != nil {
|
||||||
*sd.sdUpdates <- *tg
|
*sd.sdUpdates <- []*config.TargetGroup{tg}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,17 +104,22 @@ func (sd *NerveDiscovery) processUpdates() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run implements the TargetProvider interface.
|
// Run implements the TargetProvider interface.
|
||||||
func (sd *NerveDiscovery) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (sd *NerveDiscovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
|
||||||
// Send on everything we have seen so far.
|
// Send on everything we have seen so far.
|
||||||
sd.mu.Lock()
|
sd.mu.Lock()
|
||||||
for _, targetGroup := range sd.sources {
|
|
||||||
ch <- *targetGroup
|
all := make([]*config.TargetGroup, 0, len(sd.sources))
|
||||||
|
|
||||||
|
for _, tg := range sd.sources {
|
||||||
|
all = append(all, tg)
|
||||||
}
|
}
|
||||||
|
ch <- all
|
||||||
|
|
||||||
// Tell processUpdates to send future updates.
|
// Tell processUpdates to send future updates.
|
||||||
sd.sdUpdates = &ch
|
sd.sdUpdates = &ch
|
||||||
sd.mu.Unlock()
|
sd.mu.Unlock()
|
||||||
|
|
||||||
<-done
|
<-ctx.Done()
|
||||||
for _, tc := range sd.treeCaches {
|
for _, tc := range sd.treeCaches {
|
||||||
tc.Stop()
|
tc.Stop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
|
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
"github.com/samuel/go-zookeeper/zk"
|
"github.com/samuel/go-zookeeper/zk"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/util/strutil"
|
"github.com/prometheus/prometheus/util/strutil"
|
||||||
|
@ -57,7 +58,7 @@ type ServersetDiscovery struct {
|
||||||
conn *zk.Conn
|
conn *zk.Conn
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
sources map[string]*config.TargetGroup
|
sources map[string]*config.TargetGroup
|
||||||
sdUpdates *chan<- config.TargetGroup
|
sdUpdates *chan<- []*config.TargetGroup
|
||||||
updates chan treecache.ZookeeperTreeCacheEvent
|
updates chan treecache.ZookeeperTreeCacheEvent
|
||||||
treeCaches []*treecache.ZookeeperTreeCache
|
treeCaches []*treecache.ZookeeperTreeCache
|
||||||
}
|
}
|
||||||
|
@ -83,17 +84,6 @@ func NewServersetDiscovery(conf *config.ServersetSDConfig) *ServersetDiscovery {
|
||||||
return sd
|
return sd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources implements the TargetProvider interface.
|
|
||||||
func (sd *ServersetDiscovery) Sources() []string {
|
|
||||||
sd.mu.RLock()
|
|
||||||
defer sd.mu.RUnlock()
|
|
||||||
srcs := []string{}
|
|
||||||
for t := range sd.sources {
|
|
||||||
srcs = append(srcs, t)
|
|
||||||
}
|
|
||||||
return srcs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sd *ServersetDiscovery) processUpdates() {
|
func (sd *ServersetDiscovery) processUpdates() {
|
||||||
defer sd.conn.Close()
|
defer sd.conn.Close()
|
||||||
for event := range sd.updates {
|
for event := range sd.updates {
|
||||||
|
@ -114,7 +104,7 @@ func (sd *ServersetDiscovery) processUpdates() {
|
||||||
}
|
}
|
||||||
sd.mu.Unlock()
|
sd.mu.Unlock()
|
||||||
if sd.sdUpdates != nil {
|
if sd.sdUpdates != nil {
|
||||||
*sd.sdUpdates <- *tg
|
*sd.sdUpdates <- []*config.TargetGroup{tg}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,17 +114,22 @@ func (sd *ServersetDiscovery) processUpdates() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run implements the TargetProvider interface.
|
// Run implements the TargetProvider interface.
|
||||||
func (sd *ServersetDiscovery) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (sd *ServersetDiscovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
|
||||||
// Send on everything we have seen so far.
|
// Send on everything we have seen so far.
|
||||||
sd.mu.Lock()
|
sd.mu.Lock()
|
||||||
for _, targetGroup := range sd.sources {
|
|
||||||
ch <- *targetGroup
|
all := make([]*config.TargetGroup, 0, len(sd.sources))
|
||||||
|
|
||||||
|
for _, tg := range sd.sources {
|
||||||
|
all = append(all, tg)
|
||||||
}
|
}
|
||||||
|
ch <- all
|
||||||
|
|
||||||
// Tell processUpdates to send future updates.
|
// Tell processUpdates to send future updates.
|
||||||
sd.sdUpdates = &ch
|
sd.sdUpdates = &ch
|
||||||
sd.mu.Unlock()
|
sd.mu.Unlock()
|
||||||
|
|
||||||
<-done
|
<-ctx.Done()
|
||||||
for _, tc := range sd.treeCaches {
|
for _, tc := range sd.treeCaches {
|
||||||
tc.Stop()
|
tc.Stop()
|
||||||
}
|
}
|
||||||
|
@ -142,8 +137,8 @@ func (sd *ServersetDiscovery) Run(ch chan<- config.TargetGroup, done <-chan stru
|
||||||
|
|
||||||
func parseServersetMember(data []byte, path string) (*model.LabelSet, error) {
|
func parseServersetMember(data []byte, path string) (*model.LabelSet, error) {
|
||||||
member := serversetMember{}
|
member := serversetMember{}
|
||||||
err := json.Unmarshal(data, &member)
|
|
||||||
if err != nil {
|
if err := json.Unmarshal(data, &member); err != nil {
|
||||||
return nil, fmt.Errorf("error unmarshaling serverset member %q: %s", path, err)
|
return nil, fmt.Errorf("error unmarshaling serverset member %q: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
450
retrieval/scrape.go
Normal file
450
retrieval/scrape.go
Normal file
|
@ -0,0 +1,450 @@
|
||||||
|
// Copyright 2016 The Prometheus Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package retrieval
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/common/expfmt"
|
||||||
|
"github.com/prometheus/common/log"
|
||||||
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/net/context/ctxhttp"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/config"
|
||||||
|
"github.com/prometheus/prometheus/storage"
|
||||||
|
"github.com/prometheus/prometheus/storage/local"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
scrapeHealthMetricName = "up"
|
||||||
|
scrapeDurationMetricName = "scrape_duration_seconds"
|
||||||
|
|
||||||
|
// Capacity of the channel to buffer samples during ingestion.
|
||||||
|
ingestedSamplesCap = 256
|
||||||
|
|
||||||
|
// Constants for instrumentation.
|
||||||
|
namespace = "prometheus"
|
||||||
|
interval = "interval"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errSkippedScrape = errors.New("scrape skipped due to throttled ingestion")
|
||||||
|
|
||||||
|
targetIntervalLength = prometheus.NewSummaryVec(
|
||||||
|
prometheus.SummaryOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: "target_interval_length_seconds",
|
||||||
|
Help: "Actual intervals between scrapes.",
|
||||||
|
Objectives: map[float64]float64{0.01: 0.001, 0.05: 0.005, 0.5: 0.05, 0.90: 0.01, 0.99: 0.001},
|
||||||
|
},
|
||||||
|
[]string{interval},
|
||||||
|
)
|
||||||
|
targetSkippedScrapes = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: "target_skipped_scrapes_total",
|
||||||
|
Help: "Total number of scrapes that were skipped because the metric storage was throttled.",
|
||||||
|
},
|
||||||
|
[]string{interval},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
prometheus.MustRegister(targetIntervalLength)
|
||||||
|
prometheus.MustRegister(targetSkippedScrapes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scrapePool manages scrapes for sets of targets.
|
||||||
|
type scrapePool struct {
|
||||||
|
appender storage.SampleAppender
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
mtx sync.RWMutex
|
||||||
|
config *config.ScrapeConfig
|
||||||
|
client *http.Client
|
||||||
|
// Targets and loops must always be synchronized to have the same
|
||||||
|
// set of hashes.
|
||||||
|
targets map[uint64]*Target
|
||||||
|
loops map[uint64]loop
|
||||||
|
|
||||||
|
// Constructor for new scrape loops. This is settable for testing convenience.
|
||||||
|
newLoop func(context.Context, scraper, storage.SampleAppender, storage.SampleAppender) loop
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScrapePool(cfg *config.ScrapeConfig, app storage.SampleAppender) *scrapePool {
|
||||||
|
client, err := newHTTPClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
// Any errors that could occur here should be caught during config validation.
|
||||||
|
log.Errorf("Error creating HTTP client for job %q: %s", cfg.JobName, err)
|
||||||
|
}
|
||||||
|
return &scrapePool{
|
||||||
|
appender: app,
|
||||||
|
config: cfg,
|
||||||
|
client: client,
|
||||||
|
targets: map[uint64]*Target{},
|
||||||
|
loops: map[uint64]loop{},
|
||||||
|
newLoop: newScrapeLoop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop terminates all scrape loops and returns after they all terminated.
|
||||||
|
func (sp *scrapePool) stop() {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
sp.mtx.Lock()
|
||||||
|
defer sp.mtx.Unlock()
|
||||||
|
|
||||||
|
for fp, l := range sp.loops {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(l loop) {
|
||||||
|
l.stop()
|
||||||
|
wg.Done()
|
||||||
|
}(l)
|
||||||
|
|
||||||
|
delete(sp.loops, fp)
|
||||||
|
delete(sp.targets, fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload the scrape pool with the given scrape configuration. The target state is preserved
|
||||||
|
// but all scrape loops are restarted with the new scrape configuration.
|
||||||
|
// This method returns after all scrape loops that were stopped have fully terminated.
|
||||||
|
func (sp *scrapePool) reload(cfg *config.ScrapeConfig) {
|
||||||
|
sp.mtx.Lock()
|
||||||
|
defer sp.mtx.Unlock()
|
||||||
|
|
||||||
|
client, err := newHTTPClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
// Any errors that could occur here should be caught during config validation.
|
||||||
|
log.Errorf("Error creating HTTP client for job %q: %s", cfg.JobName, err)
|
||||||
|
}
|
||||||
|
sp.config = cfg
|
||||||
|
sp.client = client
|
||||||
|
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
interval = time.Duration(sp.config.ScrapeInterval)
|
||||||
|
timeout = time.Duration(sp.config.ScrapeTimeout)
|
||||||
|
)
|
||||||
|
|
||||||
|
for fp, oldLoop := range sp.loops {
|
||||||
|
var (
|
||||||
|
t = sp.targets[fp]
|
||||||
|
s = &targetScraper{Target: t, client: sp.client}
|
||||||
|
newLoop = sp.newLoop(sp.ctx, s, sp.sampleAppender(t), sp.reportAppender(t))
|
||||||
|
)
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(oldLoop, newLoop loop) {
|
||||||
|
oldLoop.stop()
|
||||||
|
wg.Done()
|
||||||
|
|
||||||
|
go newLoop.run(interval, timeout, nil)
|
||||||
|
}(oldLoop, newLoop)
|
||||||
|
|
||||||
|
sp.loops[fp] = newLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync takes a list of potentially duplicated targets, deduplicates them, starts
|
||||||
|
// scrape loops for new targets, and stops scrape loops for disappeared targets.
|
||||||
|
// It returns after all stopped scrape loops terminated.
|
||||||
|
func (sp *scrapePool) sync(targets []*Target) {
|
||||||
|
sp.mtx.Lock()
|
||||||
|
defer sp.mtx.Unlock()
|
||||||
|
|
||||||
|
var (
|
||||||
|
uniqueTargets = map[uint64]struct{}{}
|
||||||
|
interval = time.Duration(sp.config.ScrapeInterval)
|
||||||
|
timeout = time.Duration(sp.config.ScrapeTimeout)
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, t := range targets {
|
||||||
|
hash := t.hash()
|
||||||
|
uniqueTargets[hash] = struct{}{}
|
||||||
|
|
||||||
|
if _, ok := sp.targets[hash]; !ok {
|
||||||
|
s := &targetScraper{Target: t, client: sp.client}
|
||||||
|
l := sp.newLoop(sp.ctx, s, sp.sampleAppender(t), sp.reportAppender(t))
|
||||||
|
|
||||||
|
sp.targets[hash] = t
|
||||||
|
sp.loops[hash] = l
|
||||||
|
|
||||||
|
go l.run(interval, timeout, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Stop and remove old targets and scraper loops.
|
||||||
|
for hash := range sp.targets {
|
||||||
|
if _, ok := uniqueTargets[hash]; !ok {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(l loop) {
|
||||||
|
l.stop()
|
||||||
|
wg.Done()
|
||||||
|
}(sp.loops[hash])
|
||||||
|
|
||||||
|
delete(sp.loops, hash)
|
||||||
|
delete(sp.targets, hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all potentially stopped scrapers to terminate.
|
||||||
|
// This covers the case of flapping targets. If the server is under high load, a new scraper
|
||||||
|
// may be active and tries to insert. The old scraper that didn't terminate yet could still
|
||||||
|
// be inserting a previous sample set.
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleAppender returns an appender for ingested samples from the target.
|
||||||
|
func (sp *scrapePool) sampleAppender(target *Target) storage.SampleAppender {
|
||||||
|
app := sp.appender
|
||||||
|
// The relabelAppender has to be inside the label-modifying appenders
|
||||||
|
// so the relabeling rules are applied to the correct label set.
|
||||||
|
if mrc := sp.config.MetricRelabelConfigs; len(mrc) > 0 {
|
||||||
|
app = relabelAppender{
|
||||||
|
SampleAppender: app,
|
||||||
|
relabelings: mrc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sp.config.HonorLabels {
|
||||||
|
app = honorLabelsAppender{
|
||||||
|
SampleAppender: app,
|
||||||
|
labels: target.Labels(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app = ruleLabelsAppender{
|
||||||
|
SampleAppender: app,
|
||||||
|
labels: target.Labels(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
// reportAppender returns an appender for reporting samples for the target.
|
||||||
|
func (sp *scrapePool) reportAppender(target *Target) storage.SampleAppender {
|
||||||
|
return ruleLabelsAppender{
|
||||||
|
SampleAppender: sp.appender,
|
||||||
|
labels: target.Labels(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A scraper retrieves samples and accepts a status report at the end.
|
||||||
|
type scraper interface {
|
||||||
|
scrape(ctx context.Context, ts time.Time) (model.Samples, error)
|
||||||
|
report(start time.Time, dur time.Duration, err error)
|
||||||
|
offset(interval time.Duration) time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// targetScraper implements the scraper interface for a target.
|
||||||
|
type targetScraper struct {
|
||||||
|
*Target
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptHeader = `application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.7,text/plain;version=0.0.4;q=0.3,application/json;schema="prometheus/telemetry";version=0.0.2;q=0.2,*/*;q=0.1`
|
||||||
|
|
||||||
|
func (s *targetScraper) scrape(ctx context.Context, ts time.Time) (model.Samples, error) {
|
||||||
|
req, err := http.NewRequest("GET", s.URL().String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Accept", acceptHeader)
|
||||||
|
|
||||||
|
resp, err := ctxhttp.Do(ctx, s.client, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("server returned HTTP status %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
allSamples = make(model.Samples, 0, 200)
|
||||||
|
decSamples = make(model.Vector, 0, 50)
|
||||||
|
)
|
||||||
|
sdec := expfmt.SampleDecoder{
|
||||||
|
Dec: expfmt.NewDecoder(resp.Body, expfmt.ResponseFormat(resp.Header)),
|
||||||
|
Opts: &expfmt.DecodeOptions{
|
||||||
|
Timestamp: model.TimeFromUnixNano(ts.UnixNano()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err = sdec.Decode(&decSamples); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
allSamples = append(allSamples, decSamples...)
|
||||||
|
decSamples = decSamples[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
// Set err to nil since it is used in the scrape health recording.
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return allSamples, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// A loop can run and be stopped again. It must not be reused after it was stopped.
|
||||||
|
type loop interface {
|
||||||
|
run(interval, timeout time.Duration, errc chan<- error)
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
type scrapeLoop struct {
|
||||||
|
scraper scraper
|
||||||
|
|
||||||
|
appender storage.SampleAppender
|
||||||
|
reportAppender storage.SampleAppender
|
||||||
|
|
||||||
|
done chan struct{}
|
||||||
|
ctx context.Context
|
||||||
|
cancel func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScrapeLoop(ctx context.Context, sc scraper, app, reportApp storage.SampleAppender) loop {
|
||||||
|
sl := &scrapeLoop{
|
||||||
|
scraper: sc,
|
||||||
|
appender: app,
|
||||||
|
reportAppender: reportApp,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
sl.ctx, sl.cancel = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *scrapeLoop) run(interval, timeout time.Duration, errc chan<- error) {
|
||||||
|
defer close(sl.done)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(sl.scraper.offset(interval)):
|
||||||
|
// Continue after a scraping offset.
|
||||||
|
case <-sl.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var last time.Time
|
||||||
|
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-sl.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sl.appender.NeedsThrottling() {
|
||||||
|
var (
|
||||||
|
start = time.Now()
|
||||||
|
scrapeCtx, _ = context.WithTimeout(sl.ctx, timeout)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Only record after the first scrape.
|
||||||
|
if !last.IsZero() {
|
||||||
|
targetIntervalLength.WithLabelValues(interval.String()).Observe(
|
||||||
|
float64(time.Since(last)) / float64(time.Second), // Sub-second precision.
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
samples, err := sl.scraper.scrape(scrapeCtx, start)
|
||||||
|
if err == nil {
|
||||||
|
sl.append(samples)
|
||||||
|
} else if errc != nil {
|
||||||
|
errc <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
sl.report(start, time.Since(start), err)
|
||||||
|
last = start
|
||||||
|
} else {
|
||||||
|
targetSkippedScrapes.WithLabelValues(interval.String()).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-sl.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *scrapeLoop) stop() {
|
||||||
|
sl.cancel()
|
||||||
|
<-sl.done
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *scrapeLoop) append(samples model.Samples) {
|
||||||
|
numOutOfOrder := 0
|
||||||
|
|
||||||
|
for _, s := range samples {
|
||||||
|
if err := sl.appender.Append(s); err != nil {
|
||||||
|
if err == local.ErrOutOfOrderSample {
|
||||||
|
numOutOfOrder++
|
||||||
|
} else {
|
||||||
|
log.Warnf("Error inserting sample: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if numOutOfOrder > 0 {
|
||||||
|
log.With("numDropped", numOutOfOrder).Warn("Error on ingesting out-of-order samples")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *scrapeLoop) report(start time.Time, duration time.Duration, err error) {
|
||||||
|
sl.scraper.report(start, duration, err)
|
||||||
|
|
||||||
|
ts := model.TimeFromUnixNano(start.UnixNano())
|
||||||
|
|
||||||
|
var health model.SampleValue
|
||||||
|
if err == nil {
|
||||||
|
health = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
healthSample := &model.Sample{
|
||||||
|
Metric: model.Metric{
|
||||||
|
model.MetricNameLabel: scrapeHealthMetricName,
|
||||||
|
},
|
||||||
|
Timestamp: ts,
|
||||||
|
Value: health,
|
||||||
|
}
|
||||||
|
durationSample := &model.Sample{
|
||||||
|
Metric: model.Metric{
|
||||||
|
model.MetricNameLabel: scrapeDurationMetricName,
|
||||||
|
},
|
||||||
|
Timestamp: ts,
|
||||||
|
Value: model.SampleValue(float64(duration) / float64(time.Second)),
|
||||||
|
}
|
||||||
|
|
||||||
|
sl.reportAppender.Append(healthSample)
|
||||||
|
sl.reportAppender.Append(durationSample)
|
||||||
|
}
|
587
retrieval/scrape_test.go
Normal file
587
retrieval/scrape_test.go
Normal file
|
@ -0,0 +1,587 @@
|
||||||
|
// Copyright 2016 The Prometheus Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package retrieval
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/config"
|
||||||
|
"github.com/prometheus/prometheus/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewScrapePool(t *testing.T) {
|
||||||
|
var (
|
||||||
|
app = &nopAppender{}
|
||||||
|
cfg = &config.ScrapeConfig{}
|
||||||
|
sp = newScrapePool(cfg, app)
|
||||||
|
)
|
||||||
|
|
||||||
|
if a, ok := sp.appender.(*nopAppender); !ok || a != app {
|
||||||
|
t.Fatalf("Wrong sample appender")
|
||||||
|
}
|
||||||
|
if sp.config != cfg {
|
||||||
|
t.Fatalf("Wrong scrape config")
|
||||||
|
}
|
||||||
|
if sp.newLoop == nil {
|
||||||
|
t.Fatalf("newLoop function not initialized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testLoop struct {
|
||||||
|
startFunc func(interval, timeout time.Duration, errc chan<- error)
|
||||||
|
stopFunc func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *testLoop) run(interval, timeout time.Duration, errc chan<- error) {
|
||||||
|
l.startFunc(interval, timeout, errc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *testLoop) stop() {
|
||||||
|
l.stopFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScrapePoolStop(t *testing.T) {
|
||||||
|
sp := &scrapePool{
|
||||||
|
targets: map[uint64]*Target{},
|
||||||
|
loops: map[uint64]loop{},
|
||||||
|
}
|
||||||
|
var mtx sync.Mutex
|
||||||
|
stopped := map[uint64]bool{}
|
||||||
|
numTargets := 20
|
||||||
|
|
||||||
|
// Stopping the scrape pool must call stop() on all scrape loops,
|
||||||
|
// clean them and the respective targets up. It must wait until each loop's
|
||||||
|
// stop function returned before returning itself.
|
||||||
|
|
||||||
|
for i := 0; i < numTargets; i++ {
|
||||||
|
t := &Target{
|
||||||
|
labels: model.LabelSet{
|
||||||
|
model.AddressLabel: model.LabelValue(fmt.Sprintf("example.com:%d", i)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
l := &testLoop{}
|
||||||
|
l.stopFunc = func() {
|
||||||
|
time.Sleep(time.Duration(i*20) * time.Millisecond)
|
||||||
|
|
||||||
|
mtx.Lock()
|
||||||
|
stopped[t.hash()] = true
|
||||||
|
mtx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
sp.targets[t.hash()] = t
|
||||||
|
sp.loops[t.hash()] = l
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
stopTime := time.Now()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sp.stop()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("scrapeLoop.stop() did not return as expected")
|
||||||
|
case <-done:
|
||||||
|
// This should have taken at least as long as the last target slept.
|
||||||
|
if time.Since(stopTime) < time.Duration(numTargets*20)*time.Millisecond {
|
||||||
|
t.Fatalf("scrapeLoop.stop() exited before all targets stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mtx.Lock()
|
||||||
|
if len(stopped) != numTargets {
|
||||||
|
t.Fatalf("Expected 20 stopped loops, got %d", len(stopped))
|
||||||
|
}
|
||||||
|
mtx.Unlock()
|
||||||
|
|
||||||
|
if len(sp.targets) > 0 {
|
||||||
|
t.Fatalf("Targets were not cleared on stopping: %d left", len(sp.targets))
|
||||||
|
}
|
||||||
|
if len(sp.loops) > 0 {
|
||||||
|
t.Fatalf("Loops were not cleared on stopping: %d left", len(sp.loops))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScrapePoolReload(t *testing.T) {
|
||||||
|
var mtx sync.Mutex
|
||||||
|
numTargets := 20
|
||||||
|
|
||||||
|
stopped := map[uint64]bool{}
|
||||||
|
|
||||||
|
reloadCfg := &config.ScrapeConfig{
|
||||||
|
ScrapeInterval: model.Duration(3 * time.Second),
|
||||||
|
ScrapeTimeout: model.Duration(2 * time.Second),
|
||||||
|
}
|
||||||
|
// On starting to run, new loops created on reload check whether their preceeding
|
||||||
|
// equivalents have been stopped.
|
||||||
|
newLoop := func(ctx context.Context, s scraper, app, reportApp storage.SampleAppender) loop {
|
||||||
|
l := &testLoop{}
|
||||||
|
l.startFunc = func(interval, timeout time.Duration, errc chan<- error) {
|
||||||
|
if interval != 3*time.Second {
|
||||||
|
t.Errorf("Expected scrape interval %d but got %d", 3*time.Second, interval)
|
||||||
|
}
|
||||||
|
if timeout != 2*time.Second {
|
||||||
|
t.Errorf("Expected scrape timeout %d but got %d", 2*time.Second, timeout)
|
||||||
|
}
|
||||||
|
mtx.Lock()
|
||||||
|
if !stopped[s.(*targetScraper).hash()] {
|
||||||
|
t.Errorf("Scrape loop for %v not stopped yet", s.(*targetScraper))
|
||||||
|
}
|
||||||
|
mtx.Unlock()
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
sp := &scrapePool{
|
||||||
|
targets: map[uint64]*Target{},
|
||||||
|
loops: map[uint64]loop{},
|
||||||
|
newLoop: newLoop,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reloading a scrape pool with a new scrape configuration must stop all scrape
|
||||||
|
// loops and start new ones. A new loop must not be started before the preceeding
|
||||||
|
// one terminated.
|
||||||
|
|
||||||
|
for i := 0; i < numTargets; i++ {
|
||||||
|
t := &Target{
|
||||||
|
labels: model.LabelSet{
|
||||||
|
model.AddressLabel: model.LabelValue(fmt.Sprintf("example.com:%d", i)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
l := &testLoop{}
|
||||||
|
l.stopFunc = func() {
|
||||||
|
time.Sleep(time.Duration(i*20) * time.Millisecond)
|
||||||
|
|
||||||
|
mtx.Lock()
|
||||||
|
stopped[t.hash()] = true
|
||||||
|
mtx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
sp.targets[t.hash()] = t
|
||||||
|
sp.loops[t.hash()] = l
|
||||||
|
}
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
beforeTargets := map[uint64]*Target{}
|
||||||
|
for h, t := range sp.targets {
|
||||||
|
beforeTargets[h] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadTime := time.Now()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sp.reload(reloadCfg)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("scrapeLoop.reload() did not return as expected")
|
||||||
|
case <-done:
|
||||||
|
// This should have taken at least as long as the last target slept.
|
||||||
|
if time.Since(reloadTime) < time.Duration(numTargets*20)*time.Millisecond {
|
||||||
|
t.Fatalf("scrapeLoop.stop() exited before all targets stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mtx.Lock()
|
||||||
|
if len(stopped) != numTargets {
|
||||||
|
t.Fatalf("Expected 20 stopped loops, got %d", stopped)
|
||||||
|
}
|
||||||
|
mtx.Unlock()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(sp.targets, beforeTargets) {
|
||||||
|
t.Fatalf("Reloading affected target states unexpectedly")
|
||||||
|
}
|
||||||
|
if len(sp.loops) != numTargets {
|
||||||
|
t.Fatalf("Expected %d loops after reload but got %d", numTargets, len(sp.loops))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScrapePoolReportAppender(t *testing.T) {
|
||||||
|
cfg := &config.ScrapeConfig{
|
||||||
|
MetricRelabelConfigs: []*config.RelabelConfig{
|
||||||
|
{}, {}, {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
target := newTestTarget("example.com:80", 10*time.Millisecond, nil)
|
||||||
|
app := &nopAppender{}
|
||||||
|
|
||||||
|
sp := newScrapePool(cfg, app)
|
||||||
|
|
||||||
|
cfg.HonorLabels = false
|
||||||
|
wrapped := sp.reportAppender(target)
|
||||||
|
|
||||||
|
rl, ok := wrapped.(ruleLabelsAppender)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected ruleLabelsAppender but got %T", wrapped)
|
||||||
|
}
|
||||||
|
if rl.SampleAppender != app {
|
||||||
|
t.Fatalf("Expected base appender but got %T", rl.SampleAppender)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.HonorLabels = true
|
||||||
|
wrapped = sp.reportAppender(target)
|
||||||
|
|
||||||
|
hl, ok := wrapped.(ruleLabelsAppender)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected ruleLabelsAppender but got %T", wrapped)
|
||||||
|
}
|
||||||
|
if hl.SampleAppender != app {
|
||||||
|
t.Fatalf("Expected base appender but got %T", hl.SampleAppender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScrapePoolSampleAppender(t *testing.T) {
|
||||||
|
cfg := &config.ScrapeConfig{
|
||||||
|
MetricRelabelConfigs: []*config.RelabelConfig{
|
||||||
|
{}, {}, {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
target := newTestTarget("example.com:80", 10*time.Millisecond, nil)
|
||||||
|
app := &nopAppender{}
|
||||||
|
|
||||||
|
sp := newScrapePool(cfg, app)
|
||||||
|
|
||||||
|
cfg.HonorLabels = false
|
||||||
|
wrapped := sp.sampleAppender(target)
|
||||||
|
|
||||||
|
rl, ok := wrapped.(ruleLabelsAppender)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected ruleLabelsAppender but got %T", wrapped)
|
||||||
|
}
|
||||||
|
re, ok := rl.SampleAppender.(relabelAppender)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected relabelAppender but got %T", rl.SampleAppender)
|
||||||
|
}
|
||||||
|
if re.SampleAppender != app {
|
||||||
|
t.Fatalf("Expected base appender but got %T", re.SampleAppender)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.HonorLabels = true
|
||||||
|
wrapped = sp.sampleAppender(target)
|
||||||
|
|
||||||
|
hl, ok := wrapped.(honorLabelsAppender)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected honorLabelsAppender but got %T", wrapped)
|
||||||
|
}
|
||||||
|
re, ok = hl.SampleAppender.(relabelAppender)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected relabelAppender but got %T", hl.SampleAppender)
|
||||||
|
}
|
||||||
|
if re.SampleAppender != app {
|
||||||
|
t.Fatalf("Expected base appender but got %T", re.SampleAppender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScrapeLoopStop(t *testing.T) {
|
||||||
|
scraper := &testScraper{}
|
||||||
|
sl := newScrapeLoop(context.Background(), scraper, nil, nil)
|
||||||
|
|
||||||
|
// The scrape pool synchronizes on stopping scrape loops. However, new scrape
|
||||||
|
// loops are syarted asynchronously. Thus it's possible, that a loop is stopped
|
||||||
|
// again before having started properly.
|
||||||
|
// Stopping not-yet-started loops must block until the run method was called and exited.
|
||||||
|
// The run method must exit immediately.
|
||||||
|
|
||||||
|
stopDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
sl.stop()
|
||||||
|
close(stopDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-stopDone:
|
||||||
|
t.Fatalf("Stopping terminated before run exited successfully")
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
// Running the scrape loop must exit before calling the scraper even once.
|
||||||
|
scraper.scrapeFunc = func(context.Context, time.Time) (model.Samples, error) {
|
||||||
|
t.Fatalf("scraper was called for terminated scrape loop")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
runDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
sl.run(0, 0, nil)
|
||||||
|
close(runDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-runDone:
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
t.Fatalf("Running terminated scrape loop did not exit")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-stopDone:
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
t.Fatalf("Stopping did not terminate after running exited")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScrapeLoopRun(t *testing.T) {
|
||||||
|
var (
|
||||||
|
signal = make(chan struct{})
|
||||||
|
errc = make(chan error)
|
||||||
|
|
||||||
|
scraper = &testScraper{}
|
||||||
|
app = &nopAppender{}
|
||||||
|
reportApp = &nopAppender{}
|
||||||
|
)
|
||||||
|
defer close(signal)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
sl := newScrapeLoop(ctx, scraper, app, reportApp)
|
||||||
|
|
||||||
|
// The loop must terminate during the initial offset if the context
|
||||||
|
// is canceled.
|
||||||
|
scraper.offsetDur = time.Hour
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sl.run(time.Second, time.Hour, errc)
|
||||||
|
signal <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait to make sure we are actually waiting on the offset.
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case <-signal:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("Cancelation during initial offset failed")
|
||||||
|
case err := <-errc:
|
||||||
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The provided timeout must cause cancelation of the context passed down to the
|
||||||
|
// scraper. The scraper has to respect the context.
|
||||||
|
scraper.offsetDur = 0
|
||||||
|
|
||||||
|
block := make(chan struct{})
|
||||||
|
scraper.scrapeFunc = func(ctx context.Context, ts time.Time) (model.Samples, error) {
|
||||||
|
select {
|
||||||
|
case <-block:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
|
sl = newScrapeLoop(ctx, scraper, app, reportApp)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sl.run(time.Second, 100*time.Millisecond, errc)
|
||||||
|
signal <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errc:
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatalf("Expected timeout error but got: %s", err)
|
||||||
|
}
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatalf("Expected timeout error but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We already caught the timeout error and are certainly in the loop.
|
||||||
|
// Let the scrapes returns immediately to cause no further timeout errors
|
||||||
|
// and check whether canceling the parent context terminates the loop.
|
||||||
|
close(block)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-signal:
|
||||||
|
// Loop terminated as expected.
|
||||||
|
case err := <-errc:
|
||||||
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatalf("Loop did not terminate on context cancelation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetScraperScrapeOK(t *testing.T) {
|
||||||
|
server := httptest.NewServer(
|
||||||
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", `text/plain; version=0.0.4`)
|
||||||
|
w.Write([]byte("metric_a 1\nmetric_b 2\n"))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
serverURL, err := url.Parse(server.URL)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := &targetScraper{
|
||||||
|
Target: &Target{
|
||||||
|
labels: model.LabelSet{
|
||||||
|
model.SchemeLabel: model.LabelValue(serverURL.Scheme),
|
||||||
|
model.AddressLabel: model.LabelValue(serverURL.Host),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: http.DefaultClient,
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
samples, err := ts.scrape(context.Background(), now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected scrape error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSamples := model.Samples{
|
||||||
|
{
|
||||||
|
Metric: model.Metric{"__name__": "metric_a"},
|
||||||
|
Timestamp: model.TimeFromUnixNano(now.UnixNano()),
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metric: model.Metric{"__name__": "metric_b"},
|
||||||
|
Timestamp: model.TimeFromUnixNano(now.UnixNano()),
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(samples, expectedSamples) {
|
||||||
|
t.Errorf("Scraped samples did not match served metrics")
|
||||||
|
t.Errorf("Expected: %v", expectedSamples)
|
||||||
|
t.Fatalf("Got: %v", samples)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetScrapeScrapeCancel(t *testing.T) {
|
||||||
|
block := make(chan struct{})
|
||||||
|
|
||||||
|
server := httptest.NewServer(
|
||||||
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
<-block
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
serverURL, err := url.Parse(server.URL)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := &targetScraper{
|
||||||
|
Target: &Target{
|
||||||
|
labels: model.LabelSet{
|
||||||
|
model.SchemeLabel: model.LabelValue(serverURL.Scheme),
|
||||||
|
model.AddressLabel: model.LabelValue(serverURL.Host),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: http.DefaultClient,
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if _, err := ts.scrape(ctx, time.Now()); err != context.Canceled {
|
||||||
|
t.Fatalf("Expected context cancelation error but got: %s", err)
|
||||||
|
}
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("Scrape function did not return unexpectedly")
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
// If this is closed in a defer above the function the test server
|
||||||
|
// does not terminate and the test doens't complete.
|
||||||
|
close(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetScrapeScrapeNotFound(t *testing.T) {
|
||||||
|
server := httptest.NewServer(
|
||||||
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
serverURL, err := url.Parse(server.URL)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := &targetScraper{
|
||||||
|
Target: &Target{
|
||||||
|
labels: model.LabelSet{
|
||||||
|
model.SchemeLabel: model.LabelValue(serverURL.Scheme),
|
||||||
|
model.AddressLabel: model.LabelValue(serverURL.Host),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
client: http.DefaultClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ts.scrape(context.Background(), time.Now()); !strings.Contains(err.Error(), "404") {
|
||||||
|
t.Fatalf("Expected \"404 NotFound\" error but got: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testScraper implements the scraper interface and allows setting values
|
||||||
|
// returned by its methods. It also allows setting a custom scrape function.
|
||||||
|
type testScraper struct {
|
||||||
|
offsetDur time.Duration
|
||||||
|
|
||||||
|
lastStart time.Time
|
||||||
|
lastDuration time.Duration
|
||||||
|
lastError error
|
||||||
|
|
||||||
|
samples model.Samples
|
||||||
|
scrapeErr error
|
||||||
|
scrapeFunc func(context.Context, time.Time) (model.Samples, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *testScraper) offset(interval time.Duration) time.Duration {
|
||||||
|
return ts.offsetDur
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *testScraper) report(start time.Time, duration time.Duration, err error) {
|
||||||
|
ts.lastStart = start
|
||||||
|
ts.lastDuration = duration
|
||||||
|
ts.lastError = err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *testScraper) scrape(ctx context.Context, t time.Time) (model.Samples, error) {
|
||||||
|
if ts.scrapeFunc != nil {
|
||||||
|
return ts.scrapeFunc(ctx, t)
|
||||||
|
}
|
||||||
|
return ts.samples, ts.scrapeErr
|
||||||
|
}
|
|
@ -14,9 +14,8 @@
|
||||||
package retrieval
|
package retrieval
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"hash/fnv"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -24,200 +23,46 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/common/expfmt"
|
|
||||||
"github.com/prometheus/common/log"
|
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/net/context/ctxhttp"
|
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/storage"
|
"github.com/prometheus/prometheus/storage"
|
||||||
"github.com/prometheus/prometheus/storage/local"
|
|
||||||
"github.com/prometheus/prometheus/util/httputil"
|
"github.com/prometheus/prometheus/util/httputil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
scrapeHealthMetricName = "up"
|
|
||||||
scrapeDurationMetricName = "scrape_duration_seconds"
|
|
||||||
|
|
||||||
// Capacity of the channel to buffer samples during ingestion.
|
|
||||||
ingestedSamplesCap = 256
|
|
||||||
|
|
||||||
// Constants for instrumentation.
|
|
||||||
namespace = "prometheus"
|
|
||||||
interval = "interval"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errSkippedScrape = errors.New("scrape skipped due to throttled ingestion")
|
|
||||||
|
|
||||||
targetIntervalLength = prometheus.NewSummaryVec(
|
|
||||||
prometheus.SummaryOpts{
|
|
||||||
Namespace: namespace,
|
|
||||||
Name: "target_interval_length_seconds",
|
|
||||||
Help: "Actual intervals between scrapes.",
|
|
||||||
Objectives: map[float64]float64{0.01: 0.001, 0.05: 0.005, 0.5: 0.05, 0.90: 0.01, 0.99: 0.001},
|
|
||||||
},
|
|
||||||
[]string{interval},
|
|
||||||
)
|
|
||||||
targetSkippedScrapes = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Namespace: namespace,
|
|
||||||
Name: "target_skipped_scrapes_total",
|
|
||||||
Help: "Total number of scrapes that were skipped because the metric storage was throttled.",
|
|
||||||
},
|
|
||||||
[]string{interval},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
prometheus.MustRegister(targetIntervalLength)
|
|
||||||
prometheus.MustRegister(targetSkippedScrapes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TargetHealth describes the health state of a target.
|
// TargetHealth describes the health state of a target.
|
||||||
type TargetHealth int
|
type TargetHealth string
|
||||||
|
|
||||||
func (t TargetHealth) String() string {
|
|
||||||
switch t {
|
|
||||||
case HealthUnknown:
|
|
||||||
return "unknown"
|
|
||||||
case HealthGood:
|
|
||||||
return "up"
|
|
||||||
case HealthBad:
|
|
||||||
return "down"
|
|
||||||
}
|
|
||||||
panic("unknown state")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TargetHealth) value() model.SampleValue {
|
|
||||||
if t == HealthGood {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// The possible health states of a target based on the last performed scrape.
|
||||||
const (
|
const (
|
||||||
// HealthUnknown is the state of a Target before it is first scraped.
|
HealthUnknown TargetHealth = "unknown"
|
||||||
HealthUnknown TargetHealth = iota
|
HealthGood TargetHealth = "up"
|
||||||
// HealthGood is the state of a Target that has been successfully scraped.
|
HealthBad TargetHealth = "down"
|
||||||
HealthGood
|
|
||||||
// HealthBad is the state of a Target that was scraped unsuccessfully.
|
|
||||||
HealthBad
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TargetStatus contains information about the current status of a scrape target.
|
|
||||||
type TargetStatus struct {
|
|
||||||
lastError error
|
|
||||||
lastScrape time.Time
|
|
||||||
health TargetHealth
|
|
||||||
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// LastError returns the error encountered during the last scrape.
|
|
||||||
func (ts *TargetStatus) LastError() error {
|
|
||||||
ts.mu.RLock()
|
|
||||||
defer ts.mu.RUnlock()
|
|
||||||
|
|
||||||
return ts.lastError
|
|
||||||
}
|
|
||||||
|
|
||||||
// LastScrape returns the time of the last scrape.
|
|
||||||
func (ts *TargetStatus) LastScrape() time.Time {
|
|
||||||
ts.mu.RLock()
|
|
||||||
defer ts.mu.RUnlock()
|
|
||||||
|
|
||||||
return ts.lastScrape
|
|
||||||
}
|
|
||||||
|
|
||||||
// Health returns the last known health state of the target.
|
|
||||||
func (ts *TargetStatus) Health() TargetHealth {
|
|
||||||
ts.mu.RLock()
|
|
||||||
defer ts.mu.RUnlock()
|
|
||||||
|
|
||||||
return ts.health
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TargetStatus) setLastScrape(t time.Time) {
|
|
||||||
ts.mu.Lock()
|
|
||||||
defer ts.mu.Unlock()
|
|
||||||
|
|
||||||
ts.lastScrape = t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *TargetStatus) setLastError(err error) {
|
|
||||||
ts.mu.Lock()
|
|
||||||
defer ts.mu.Unlock()
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
ts.health = HealthGood
|
|
||||||
} else {
|
|
||||||
ts.health = HealthBad
|
|
||||||
}
|
|
||||||
ts.lastError = err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Target refers to a singular HTTP or HTTPS endpoint.
|
// Target refers to a singular HTTP or HTTPS endpoint.
|
||||||
type Target struct {
|
type Target struct {
|
||||||
// The status object for the target. It is only set once on initialization.
|
|
||||||
status *TargetStatus
|
|
||||||
// Closing scraperStopping signals that scraping should stop.
|
|
||||||
scraperStopping chan struct{}
|
|
||||||
// Closing scraperStopped signals that scraping has been stopped.
|
|
||||||
scraperStopped chan struct{}
|
|
||||||
|
|
||||||
// Mutex protects the members below.
|
|
||||||
sync.RWMutex
|
|
||||||
|
|
||||||
scrapeConfig *config.ScrapeConfig
|
|
||||||
|
|
||||||
// Labels before any processing.
|
// Labels before any processing.
|
||||||
metaLabels model.LabelSet
|
metaLabels model.LabelSet
|
||||||
// Any labels that are added to this target and its metrics.
|
// Any labels that are added to this target and its metrics.
|
||||||
labels model.LabelSet
|
labels model.LabelSet
|
||||||
|
// Additional URL parmeters that are part of the target URL.
|
||||||
|
params url.Values
|
||||||
|
|
||||||
// The HTTP client used to scrape the target's endpoint.
|
mtx sync.RWMutex
|
||||||
httpClient *http.Client
|
lastError error
|
||||||
|
lastScrape time.Time
|
||||||
|
health TargetHealth
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTarget creates a reasonably configured target for querying.
|
// NewTarget creates a reasonably configured target for querying.
|
||||||
func NewTarget(cfg *config.ScrapeConfig, labels, metaLabels model.LabelSet) (*Target, error) {
|
func NewTarget(labels, metaLabels model.LabelSet, params url.Values) *Target {
|
||||||
t := &Target{
|
return &Target{
|
||||||
status: &TargetStatus{},
|
labels: labels,
|
||||||
scraperStopping: make(chan struct{}),
|
metaLabels: metaLabels,
|
||||||
scraperStopped: make(chan struct{}),
|
params: params,
|
||||||
|
health: HealthUnknown,
|
||||||
}
|
}
|
||||||
err := t.Update(cfg, labels, metaLabels)
|
|
||||||
return t, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status returns the status of the target.
|
|
||||||
func (t *Target) Status() *TargetStatus {
|
|
||||||
return t.status
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update overwrites settings in the target that are derived from the job config
|
|
||||||
// it belongs to.
|
|
||||||
func (t *Target) Update(cfg *config.ScrapeConfig, labels, metaLabels model.LabelSet) error {
|
|
||||||
t.Lock()
|
|
||||||
|
|
||||||
t.scrapeConfig = cfg
|
|
||||||
t.labels = labels
|
|
||||||
t.metaLabels = metaLabels
|
|
||||||
|
|
||||||
t.Unlock()
|
|
||||||
|
|
||||||
httpClient, err := t.client()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot create HTTP client: %s", err)
|
|
||||||
}
|
|
||||||
t.Lock()
|
|
||||||
t.httpClient = httpClient
|
|
||||||
t.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPClient(cfg *config.ScrapeConfig) (*http.Client, error) {
|
func newHTTPClient(cfg *config.ScrapeConfig) (*http.Client, error) {
|
||||||
|
@ -265,15 +110,16 @@ func newHTTPClient(cfg *config.ScrapeConfig) (*http.Client, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Target) String() string {
|
func (t *Target) String() string {
|
||||||
return t.host()
|
return t.URL().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// fingerprint returns an identifying hash for the target.
|
// hash returns an identifying hash for the target.
|
||||||
func (t *Target) fingerprint() model.Fingerprint {
|
func (t *Target) hash() uint64 {
|
||||||
t.RLock()
|
h := fnv.New64a()
|
||||||
defer t.RUnlock()
|
h.Write([]byte(t.labels.Fingerprint().String()))
|
||||||
|
h.Write([]byte(t.URL().String()))
|
||||||
|
|
||||||
return t.labels.Fingerprint()
|
return h.Sum64()
|
||||||
}
|
}
|
||||||
|
|
||||||
// offset returns the time until the next scrape cycle for the target.
|
// offset returns the time until the next scrape cycle for the target.
|
||||||
|
@ -282,7 +128,7 @@ func (t *Target) offset(interval time.Duration) time.Duration {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
base = now % int64(interval)
|
base = now % int64(interval)
|
||||||
offset = uint64(t.fingerprint()) % uint64(interval)
|
offset = t.hash() % uint64(interval)
|
||||||
next = base + int64(offset)
|
next = base + int64(offset)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -292,92 +138,27 @@ func (t *Target) offset(interval time.Duration) time.Duration {
|
||||||
return time.Duration(next)
|
return time.Duration(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Target) client() (*http.Client, error) {
|
// Labels returns a copy of the set of all public labels of the target.
|
||||||
t.RLock()
|
func (t *Target) Labels() model.LabelSet {
|
||||||
defer t.RUnlock()
|
lset := make(model.LabelSet, len(t.labels))
|
||||||
|
for ln, lv := range t.labels {
|
||||||
return newHTTPClient(t.scrapeConfig)
|
if !strings.HasPrefix(string(ln), model.ReservedLabelPrefix) {
|
||||||
}
|
lset[ln] = lv
|
||||||
|
|
||||||
func (t *Target) interval() time.Duration {
|
|
||||||
t.RLock()
|
|
||||||
defer t.RUnlock()
|
|
||||||
|
|
||||||
return time.Duration(t.scrapeConfig.ScrapeInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Target) timeout() time.Duration {
|
|
||||||
t.RLock()
|
|
||||||
defer t.RUnlock()
|
|
||||||
|
|
||||||
return time.Duration(t.scrapeConfig.ScrapeTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Target) scheme() string {
|
|
||||||
t.RLock()
|
|
||||||
defer t.RUnlock()
|
|
||||||
|
|
||||||
return string(t.labels[model.SchemeLabel])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Target) host() string {
|
|
||||||
t.RLock()
|
|
||||||
defer t.RUnlock()
|
|
||||||
|
|
||||||
return string(t.labels[model.AddressLabel])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Target) path() string {
|
|
||||||
t.RLock()
|
|
||||||
defer t.RUnlock()
|
|
||||||
|
|
||||||
return string(t.labels[model.MetricsPathLabel])
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapAppender wraps a SampleAppender for samples ingested from the target.
|
|
||||||
// RLock must be acquired by the caller.
|
|
||||||
func (t *Target) wrapAppender(app storage.SampleAppender) storage.SampleAppender {
|
|
||||||
// The relabelAppender has to be inside the label-modifying appenders
|
|
||||||
// so the relabeling rules are applied to the correct label set.
|
|
||||||
if mrc := t.scrapeConfig.MetricRelabelConfigs; len(mrc) > 0 {
|
|
||||||
app = relabelAppender{
|
|
||||||
SampleAppender: app,
|
|
||||||
relabelings: mrc,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return lset
|
||||||
if t.scrapeConfig.HonorLabels {
|
|
||||||
app = honorLabelsAppender{
|
|
||||||
SampleAppender: app,
|
|
||||||
labels: t.unlockedLabels(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
app = ruleLabelsAppender{
|
|
||||||
SampleAppender: app,
|
|
||||||
labels: t.unlockedLabels(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return app
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapReportingAppender wraps an appender for target status report samples.
|
// MetaLabels returns a copy of the target's labels before any processing.
|
||||||
// It ignores any relabeling rules set for the target.
|
func (t *Target) MetaLabels() model.LabelSet {
|
||||||
// RLock must not be acquired by the caller.
|
return t.metaLabels.Clone()
|
||||||
func (t *Target) wrapReportingAppender(app storage.SampleAppender) storage.SampleAppender {
|
|
||||||
return ruleLabelsAppender{
|
|
||||||
SampleAppender: app,
|
|
||||||
labels: t.Labels(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL returns a copy of the target's URL.
|
// URL returns a copy of the target's URL.
|
||||||
func (t *Target) URL() *url.URL {
|
func (t *Target) URL() *url.URL {
|
||||||
t.RLock()
|
|
||||||
defer t.RUnlock()
|
|
||||||
|
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
|
|
||||||
for k, v := range t.scrapeConfig.Params {
|
for k, v := range t.params {
|
||||||
params[k] = make([]string, len(v))
|
params[k] = make([]string, len(v))
|
||||||
copy(params[k], v)
|
copy(params[k], v)
|
||||||
}
|
}
|
||||||
|
@ -402,191 +183,51 @@ func (t *Target) URL() *url.URL {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstanceIdentifier returns the identifier for the target.
|
func (t *Target) report(start time.Time, dur time.Duration, err error) {
|
||||||
func (t *Target) InstanceIdentifier() string {
|
t.mtx.Lock()
|
||||||
return t.host()
|
defer t.mtx.Unlock()
|
||||||
}
|
|
||||||
|
|
||||||
// RunScraper implements Target.
|
|
||||||
func (t *Target) RunScraper(sampleAppender storage.SampleAppender) {
|
|
||||||
defer close(t.scraperStopped)
|
|
||||||
|
|
||||||
lastScrapeInterval := t.interval()
|
|
||||||
|
|
||||||
log.Debugf("Starting scraper for target %v...", t)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(t.offset(lastScrapeInterval)):
|
|
||||||
// Continue after scraping offset.
|
|
||||||
case <-t.scraperStopping:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ticker := time.NewTicker(lastScrapeInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
t.scrape(sampleAppender)
|
|
||||||
|
|
||||||
// Explanation of the contraption below:
|
|
||||||
//
|
|
||||||
// In case t.scraperStopping has something to receive, we want to read
|
|
||||||
// from that channel rather than starting a new scrape (which might take very
|
|
||||||
// long). That's why the outer select has no ticker.C. Should t.scraperStopping
|
|
||||||
// not have anything to receive, we go into the inner select, where ticker.C
|
|
||||||
// is in the mix.
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-t.scraperStopping:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
select {
|
|
||||||
case <-t.scraperStopping:
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
took := time.Since(t.status.LastScrape())
|
|
||||||
|
|
||||||
intervalStr := lastScrapeInterval.String()
|
|
||||||
|
|
||||||
// On changed scrape interval the new interval becomes effective
|
|
||||||
// after the next scrape.
|
|
||||||
if iv := t.interval(); iv != lastScrapeInterval {
|
|
||||||
ticker.Stop()
|
|
||||||
ticker = time.NewTicker(iv)
|
|
||||||
lastScrapeInterval = iv
|
|
||||||
}
|
|
||||||
|
|
||||||
targetIntervalLength.WithLabelValues(intervalStr).Observe(
|
|
||||||
float64(took) / float64(time.Second), // Sub-second precision.
|
|
||||||
)
|
|
||||||
if sampleAppender.NeedsThrottling() {
|
|
||||||
targetSkippedScrapes.WithLabelValues(intervalStr).Inc()
|
|
||||||
t.status.setLastError(errSkippedScrape)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.scrape(sampleAppender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopScraper implements Target.
|
|
||||||
func (t *Target) StopScraper() {
|
|
||||||
log.Debugf("Stopping scraper for target %v...", t)
|
|
||||||
|
|
||||||
close(t.scraperStopping)
|
|
||||||
<-t.scraperStopped
|
|
||||||
|
|
||||||
log.Debugf("Scraper for target %v stopped.", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
const acceptHeader = `application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.7,text/plain;version=0.0.4;q=0.3,application/json;schema="prometheus/telemetry";version=0.0.2;q=0.2,*/*;q=0.1`
|
|
||||||
|
|
||||||
func (t *Target) scrape(appender storage.SampleAppender) error {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
start = time.Now()
|
|
||||||
)
|
|
||||||
defer func(appender storage.SampleAppender) {
|
|
||||||
t.report(appender, start, time.Since(start), err)
|
|
||||||
}(appender)
|
|
||||||
|
|
||||||
t.RLock()
|
|
||||||
|
|
||||||
appender = t.wrapAppender(appender)
|
|
||||||
|
|
||||||
client := t.httpClient
|
|
||||||
t.RUnlock()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", t.URL().String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Add("Accept", acceptHeader)
|
|
||||||
|
|
||||||
ctx, _ := context.WithTimeout(context.Background(), t.timeout())
|
|
||||||
resp, err := ctxhttp.Do(ctx, client, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("server returned HTTP status %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
dec := expfmt.NewDecoder(resp.Body, expfmt.ResponseFormat(resp.Header))
|
|
||||||
|
|
||||||
sdec := expfmt.SampleDecoder{
|
|
||||||
Dec: dec,
|
|
||||||
Opts: &expfmt.DecodeOptions{
|
|
||||||
Timestamp: model.TimeFromUnixNano(start.UnixNano()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
samples model.Vector
|
|
||||||
numOutOfOrder int
|
|
||||||
logger = log.With("target", t.InstanceIdentifier())
|
|
||||||
)
|
|
||||||
for {
|
|
||||||
if err = sdec.Decode(&samples); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for _, s := range samples {
|
|
||||||
err := appender.Append(s)
|
|
||||||
if err != nil {
|
|
||||||
if err == local.ErrOutOfOrderSample {
|
|
||||||
numOutOfOrder++
|
|
||||||
} else {
|
|
||||||
logger.With("sample", s).Warnf("Error inserting sample: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if numOutOfOrder > 0 {
|
|
||||||
logger.With("numDropped", numOutOfOrder).Warn("Error on ingesting out-of-order samples")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
// Set err to nil since it is used in the scrape health recording.
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Target) report(app storage.SampleAppender, start time.Time, duration time.Duration, err error) {
|
|
||||||
t.status.setLastScrape(start)
|
|
||||||
t.status.setLastError(err)
|
|
||||||
|
|
||||||
ts := model.TimeFromUnixNano(start.UnixNano())
|
|
||||||
|
|
||||||
var health model.SampleValue
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
health = 1
|
t.health = HealthGood
|
||||||
|
} else {
|
||||||
|
t.health = HealthBad
|
||||||
}
|
}
|
||||||
|
|
||||||
healthSample := &model.Sample{
|
t.lastError = err
|
||||||
Metric: model.Metric{
|
t.lastScrape = start
|
||||||
model.MetricNameLabel: scrapeHealthMetricName,
|
|
||||||
},
|
|
||||||
Timestamp: ts,
|
|
||||||
Value: health,
|
|
||||||
}
|
|
||||||
durationSample := &model.Sample{
|
|
||||||
Metric: model.Metric{
|
|
||||||
model.MetricNameLabel: scrapeDurationMetricName,
|
|
||||||
},
|
|
||||||
Timestamp: ts,
|
|
||||||
Value: model.SampleValue(float64(duration) / float64(time.Second)),
|
|
||||||
}
|
|
||||||
|
|
||||||
app = t.wrapReportingAppender(app)
|
|
||||||
|
|
||||||
app.Append(healthSample)
|
|
||||||
app.Append(durationSample)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastError returns the error encountered during the last scrape.
|
||||||
|
func (t *Target) LastError() error {
|
||||||
|
t.mtx.RLock()
|
||||||
|
defer t.mtx.RUnlock()
|
||||||
|
|
||||||
|
return t.lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastScrape returns the time of the last scrape.
|
||||||
|
func (t *Target) LastScrape() time.Time {
|
||||||
|
t.mtx.RLock()
|
||||||
|
defer t.mtx.RUnlock()
|
||||||
|
|
||||||
|
return t.lastScrape
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health returns the last known health state of the target.
|
||||||
|
func (t *Target) Health() TargetHealth {
|
||||||
|
t.mtx.RLock()
|
||||||
|
defer t.mtx.RUnlock()
|
||||||
|
|
||||||
|
return t.health
|
||||||
|
}
|
||||||
|
|
||||||
|
// Targets is a sortable list of targets.
|
||||||
|
type Targets []*Target
|
||||||
|
|
||||||
|
func (ts Targets) Len() int { return len(ts) }
|
||||||
|
func (ts Targets) Less(i, j int) bool { return ts[i].URL().String() < ts[j].URL().String() }
|
||||||
|
func (ts Targets) Swap(i, j int) { ts[i], ts[j] = ts[j], ts[i] }
|
||||||
|
|
||||||
// Merges the ingested sample's metric with the label set. On a collision the
|
// Merges the ingested sample's metric with the label set. On a collision the
|
||||||
// value of the ingested label is stored in a label prefixed with 'exported_'.
|
// value of the ingested label is stored in a label prefixed with 'exported_'.
|
||||||
type ruleLabelsAppender struct {
|
type ruleLabelsAppender struct {
|
||||||
|
@ -643,36 +284,3 @@ func (app relabelAppender) Append(s *model.Sample) error {
|
||||||
|
|
||||||
return app.SampleAppender.Append(s)
|
return app.SampleAppender.Append(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels returns a copy of the set of all public labels of the target.
|
|
||||||
func (t *Target) Labels() model.LabelSet {
|
|
||||||
t.RLock()
|
|
||||||
defer t.RUnlock()
|
|
||||||
|
|
||||||
return t.unlockedLabels()
|
|
||||||
}
|
|
||||||
|
|
||||||
// unlockedLabels does the same as Labels but does not lock the mutex (useful
|
|
||||||
// for internal usage when the mutex is already locked).
|
|
||||||
func (t *Target) unlockedLabels() model.LabelSet {
|
|
||||||
lset := make(model.LabelSet, len(t.labels))
|
|
||||||
for ln, lv := range t.labels {
|
|
||||||
if !strings.HasPrefix(string(ln), model.ReservedLabelPrefix) {
|
|
||||||
lset[ln] = lv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := lset[model.InstanceLabel]; !ok {
|
|
||||||
lset[model.InstanceLabel] = t.labels[model.AddressLabel]
|
|
||||||
}
|
|
||||||
|
|
||||||
return lset
|
|
||||||
}
|
|
||||||
|
|
||||||
// MetaLabels returns a copy of the target's labels before any processing.
|
|
||||||
func (t *Target) MetaLabels() model.LabelSet {
|
|
||||||
t.RLock()
|
|
||||||
defer t.RUnlock()
|
|
||||||
|
|
||||||
return t.metaLabels.Clone()
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ package retrieval
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -35,9 +34,8 @@ import (
|
||||||
func TestTargetLabels(t *testing.T) {
|
func TestTargetLabels(t *testing.T) {
|
||||||
target := newTestTarget("example.com:80", 0, model.LabelSet{"job": "some_job", "foo": "bar"})
|
target := newTestTarget("example.com:80", 0, model.LabelSet{"job": "some_job", "foo": "bar"})
|
||||||
want := model.LabelSet{
|
want := model.LabelSet{
|
||||||
model.JobLabel: "some_job",
|
model.JobLabel: "some_job",
|
||||||
model.InstanceLabel: "example.com:80",
|
"foo": "bar",
|
||||||
"foo": "bar",
|
|
||||||
}
|
}
|
||||||
got := target.Labels()
|
got := target.Labels()
|
||||||
if !reflect.DeepEqual(want, got) {
|
if !reflect.DeepEqual(want, got) {
|
||||||
|
@ -91,484 +89,36 @@ func TestTargetOffset(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTargetWrapReportingAppender(t *testing.T) {
|
func TestTargetURL(t *testing.T) {
|
||||||
cfg := &config.ScrapeConfig{
|
params := url.Values{
|
||||||
MetricRelabelConfigs: []*config.RelabelConfig{
|
"abc": []string{"foo", "bar", "baz"},
|
||||||
{}, {}, {},
|
"xyz": []string{"hoo"},
|
||||||
},
|
}
|
||||||
|
labels := model.LabelSet{
|
||||||
|
model.AddressLabel: "example.com:1234",
|
||||||
|
model.SchemeLabel: "https",
|
||||||
|
model.MetricsPathLabel: "/metricz",
|
||||||
|
"__param_abc": "overwrite",
|
||||||
|
"__param_cde": "huu",
|
||||||
|
}
|
||||||
|
target := NewTarget(labels, labels, params)
|
||||||
|
|
||||||
|
// The reserved labels are concatenated into a full URL. The first value for each
|
||||||
|
// URL query parameter can be set/modified via labels as well.
|
||||||
|
expectedParams := url.Values{
|
||||||
|
"abc": []string{"overwrite", "bar", "baz"},
|
||||||
|
"cde": []string{"huu"},
|
||||||
|
"xyz": []string{"hoo"},
|
||||||
|
}
|
||||||
|
expectedURL := url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: "example.com:1234",
|
||||||
|
Path: "/metricz",
|
||||||
|
RawQuery: expectedParams.Encode(),
|
||||||
}
|
}
|
||||||
|
|
||||||
target := newTestTarget("example.com:80", 10*time.Millisecond, nil)
|
if u := target.URL(); !reflect.DeepEqual(u.String(), expectedURL.String()) {
|
||||||
target.scrapeConfig = cfg
|
t.Fatalf("Expected URL %q but got %q", expectedURL, u)
|
||||||
app := &nopAppender{}
|
|
||||||
|
|
||||||
cfg.HonorLabels = false
|
|
||||||
wrapped := target.wrapReportingAppender(app)
|
|
||||||
|
|
||||||
rl, ok := wrapped.(ruleLabelsAppender)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected ruleLabelsAppender but got %T", wrapped)
|
|
||||||
}
|
|
||||||
if rl.SampleAppender != app {
|
|
||||||
t.Fatalf("Expected base appender but got %T", rl.SampleAppender)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.HonorLabels = true
|
|
||||||
wrapped = target.wrapReportingAppender(app)
|
|
||||||
|
|
||||||
hl, ok := wrapped.(ruleLabelsAppender)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected ruleLabelsAppender but got %T", wrapped)
|
|
||||||
}
|
|
||||||
if hl.SampleAppender != app {
|
|
||||||
t.Fatalf("Expected base appender but got %T", hl.SampleAppender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetWrapAppender(t *testing.T) {
|
|
||||||
cfg := &config.ScrapeConfig{
|
|
||||||
MetricRelabelConfigs: []*config.RelabelConfig{
|
|
||||||
{}, {}, {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
target := newTestTarget("example.com:80", 10*time.Millisecond, nil)
|
|
||||||
target.scrapeConfig = cfg
|
|
||||||
app := &nopAppender{}
|
|
||||||
|
|
||||||
cfg.HonorLabels = false
|
|
||||||
wrapped := target.wrapAppender(app)
|
|
||||||
|
|
||||||
rl, ok := wrapped.(ruleLabelsAppender)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected ruleLabelsAppender but got %T", wrapped)
|
|
||||||
}
|
|
||||||
re, ok := rl.SampleAppender.(relabelAppender)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected relabelAppender but got %T", rl.SampleAppender)
|
|
||||||
}
|
|
||||||
if re.SampleAppender != app {
|
|
||||||
t.Fatalf("Expected base appender but got %T", re.SampleAppender)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.HonorLabels = true
|
|
||||||
wrapped = target.wrapAppender(app)
|
|
||||||
|
|
||||||
hl, ok := wrapped.(honorLabelsAppender)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected honorLabelsAppender but got %T", wrapped)
|
|
||||||
}
|
|
||||||
re, ok = hl.SampleAppender.(relabelAppender)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Expected relabelAppender but got %T", hl.SampleAppender)
|
|
||||||
}
|
|
||||||
if re.SampleAppender != app {
|
|
||||||
t.Fatalf("Expected base appender but got %T", re.SampleAppender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOverwriteLabels(t *testing.T) {
|
|
||||||
type test struct {
|
|
||||||
metric string
|
|
||||||
resultNormal model.Metric
|
|
||||||
resultHonor model.Metric
|
|
||||||
}
|
|
||||||
var tests []test
|
|
||||||
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", `text/plain; version=0.0.4`)
|
|
||||||
for _, test := range tests {
|
|
||||||
w.Write([]byte(test.metric))
|
|
||||||
w.Write([]byte(" 1\n"))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer server.Close()
|
|
||||||
addr := model.LabelValue(strings.Split(server.URL, "://")[1])
|
|
||||||
|
|
||||||
tests = []test{
|
|
||||||
{
|
|
||||||
metric: `foo{}`,
|
|
||||||
resultNormal: model.Metric{
|
|
||||||
model.MetricNameLabel: "foo",
|
|
||||||
model.InstanceLabel: addr,
|
|
||||||
},
|
|
||||||
resultHonor: model.Metric{
|
|
||||||
model.MetricNameLabel: "foo",
|
|
||||||
model.InstanceLabel: addr,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: `foo{instance=""}`,
|
|
||||||
resultNormal: model.Metric{
|
|
||||||
model.MetricNameLabel: "foo",
|
|
||||||
model.InstanceLabel: addr,
|
|
||||||
},
|
|
||||||
resultHonor: model.Metric{
|
|
||||||
model.MetricNameLabel: "foo",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metric: `foo{instance="other_instance"}`,
|
|
||||||
resultNormal: model.Metric{
|
|
||||||
model.MetricNameLabel: "foo",
|
|
||||||
model.InstanceLabel: addr,
|
|
||||||
model.ExportedLabelPrefix + model.InstanceLabel: "other_instance",
|
|
||||||
},
|
|
||||||
resultHonor: model.Metric{
|
|
||||||
model.MetricNameLabel: "foo",
|
|
||||||
model.InstanceLabel: "other_instance",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
target := newTestTarget(server.URL, time.Second, nil)
|
|
||||||
|
|
||||||
target.scrapeConfig.HonorLabels = false
|
|
||||||
app := &collectResultAppender{}
|
|
||||||
if err := target.scrape(app); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
if !reflect.DeepEqual(app.result[i].Metric, test.resultNormal) {
|
|
||||||
t.Errorf("Error comparing %q:\nExpected:\n%s\nGot:\n%s\n", test.metric, test.resultNormal, app.result[i].Metric)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target.scrapeConfig.HonorLabels = true
|
|
||||||
app = &collectResultAppender{}
|
|
||||||
if err := target.scrape(app); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
if !reflect.DeepEqual(app.result[i].Metric, test.resultHonor) {
|
|
||||||
t.Errorf("Error comparing %q:\nExpected:\n%s\nGot:\n%s\n", test.metric, test.resultHonor, app.result[i].Metric)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestTargetScrapeUpdatesState(t *testing.T) {
|
|
||||||
testTarget := newTestTarget("bad schema", 0, nil)
|
|
||||||
|
|
||||||
testTarget.scrape(nopAppender{})
|
|
||||||
if testTarget.status.Health() != HealthBad {
|
|
||||||
t.Errorf("Expected target state %v, actual: %v", HealthBad, testTarget.status.Health())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetScrapeWithThrottledStorage(t *testing.T) {
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", `text/plain; version=0.0.4`)
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
w.Write([]byte(
|
|
||||||
fmt.Sprintf("test_metric_%d{foo=\"bar\"} 123.456\n", i),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
testTarget := newTestTarget(server.URL, time.Second, model.LabelSet{"dings": "bums"})
|
|
||||||
|
|
||||||
go testTarget.RunScraper(&collectResultAppender{throttled: true})
|
|
||||||
|
|
||||||
// Enough time for a scrape to happen.
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
|
|
||||||
testTarget.StopScraper()
|
|
||||||
// Wait for it to take effect.
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
|
|
||||||
if testTarget.status.Health() != HealthBad {
|
|
||||||
t.Errorf("Expected target state %v, actual: %v", HealthBad, testTarget.status.Health())
|
|
||||||
}
|
|
||||||
if testTarget.status.LastError() != errSkippedScrape {
|
|
||||||
t.Errorf("Expected target error %q, actual: %q", errSkippedScrape, testTarget.status.LastError())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetScrapeMetricRelabelConfigs(t *testing.T) {
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", `text/plain; version=0.0.4`)
|
|
||||||
w.Write([]byte("test_metric_drop 0\n"))
|
|
||||||
w.Write([]byte("test_metric_relabel 1\n"))
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer server.Close()
|
|
||||||
testTarget := newTestTarget(server.URL, time.Second, model.LabelSet{})
|
|
||||||
testTarget.scrapeConfig.MetricRelabelConfigs = []*config.RelabelConfig{
|
|
||||||
{
|
|
||||||
SourceLabels: model.LabelNames{"__name__"},
|
|
||||||
Regex: config.MustNewRegexp(".*drop.*"),
|
|
||||||
Action: config.RelabelDrop,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SourceLabels: model.LabelNames{"__name__"},
|
|
||||||
Regex: config.MustNewRegexp(".*(relabel|up).*"),
|
|
||||||
TargetLabel: "foo",
|
|
||||||
Replacement: "bar",
|
|
||||||
Action: config.RelabelReplace,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
appender := &collectResultAppender{}
|
|
||||||
if err := testTarget.scrape(appender); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove variables part of result.
|
|
||||||
for _, sample := range appender.result {
|
|
||||||
sample.Timestamp = 0
|
|
||||||
sample.Value = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := []*model.Sample{
|
|
||||||
{
|
|
||||||
Metric: model.Metric{
|
|
||||||
model.MetricNameLabel: "test_metric_relabel",
|
|
||||||
"foo": "bar",
|
|
||||||
model.InstanceLabel: model.LabelValue(testTarget.host()),
|
|
||||||
},
|
|
||||||
Timestamp: 0,
|
|
||||||
Value: 0,
|
|
||||||
},
|
|
||||||
// The metrics about the scrape are not affected.
|
|
||||||
{
|
|
||||||
Metric: model.Metric{
|
|
||||||
model.MetricNameLabel: scrapeHealthMetricName,
|
|
||||||
model.InstanceLabel: model.LabelValue(testTarget.host()),
|
|
||||||
},
|
|
||||||
Timestamp: 0,
|
|
||||||
Value: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metric: model.Metric{
|
|
||||||
model.MetricNameLabel: scrapeDurationMetricName,
|
|
||||||
model.InstanceLabel: model.LabelValue(testTarget.host()),
|
|
||||||
},
|
|
||||||
Timestamp: 0,
|
|
||||||
Value: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if !appender.result.Equal(expected) {
|
|
||||||
t.Fatalf("Expected and actual samples not equal. Expected: %s, actual: %s", expected, appender.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetRecordScrapeHealth(t *testing.T) {
|
|
||||||
var (
|
|
||||||
testTarget = newTestTarget("example.url:80", 0, model.LabelSet{model.JobLabel: "testjob"})
|
|
||||||
now = model.Now()
|
|
||||||
appender = &collectResultAppender{}
|
|
||||||
)
|
|
||||||
|
|
||||||
testTarget.report(appender, now.Time(), 2*time.Second, nil)
|
|
||||||
|
|
||||||
result := appender.result
|
|
||||||
|
|
||||||
if len(result) != 2 {
|
|
||||||
t.Fatalf("Expected two samples, got %d", len(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := result[0]
|
|
||||||
expected := &model.Sample{
|
|
||||||
Metric: model.Metric{
|
|
||||||
model.MetricNameLabel: scrapeHealthMetricName,
|
|
||||||
model.InstanceLabel: "example.url:80",
|
|
||||||
model.JobLabel: "testjob",
|
|
||||||
},
|
|
||||||
Timestamp: now,
|
|
||||||
Value: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !actual.Equal(expected) {
|
|
||||||
t.Fatalf("Expected and actual samples not equal. Expected: %v, actual: %v", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = result[1]
|
|
||||||
expected = &model.Sample{
|
|
||||||
Metric: model.Metric{
|
|
||||||
model.MetricNameLabel: scrapeDurationMetricName,
|
|
||||||
model.InstanceLabel: "example.url:80",
|
|
||||||
model.JobLabel: "testjob",
|
|
||||||
},
|
|
||||||
Timestamp: now,
|
|
||||||
Value: 2.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !actual.Equal(expected) {
|
|
||||||
t.Fatalf("Expected and actual samples not equal. Expected: %v, actual: %v", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetScrapeTimeout(t *testing.T) {
|
|
||||||
signal := make(chan bool, 1)
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
<-signal
|
|
||||||
w.Header().Set("Content-Type", `text/plain; version=0.0.4`)
|
|
||||||
w.Write([]byte{})
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
testTarget := newTestTarget(server.URL, 50*time.Millisecond, model.LabelSet{})
|
|
||||||
|
|
||||||
appender := nopAppender{}
|
|
||||||
|
|
||||||
// scrape once without timeout
|
|
||||||
signal <- true
|
|
||||||
if err := testTarget.scrape(appender); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// let the deadline lapse
|
|
||||||
time.Sleep(55 * time.Millisecond)
|
|
||||||
|
|
||||||
// now scrape again
|
|
||||||
signal <- true
|
|
||||||
if err := testTarget.scrape(appender); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// now timeout
|
|
||||||
if err := testTarget.scrape(appender); err == nil {
|
|
||||||
t.Fatal("expected scrape to timeout")
|
|
||||||
} else {
|
|
||||||
signal <- true // let handler continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// now scrape again without timeout
|
|
||||||
signal <- true
|
|
||||||
if err := testTarget.scrape(appender); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetScrape404(t *testing.T) {
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
testTarget := newTestTarget(server.URL, time.Second, model.LabelSet{})
|
|
||||||
appender := nopAppender{}
|
|
||||||
|
|
||||||
want := errors.New("server returned HTTP status 404 Not Found")
|
|
||||||
got := testTarget.scrape(appender)
|
|
||||||
if got == nil || want.Error() != got.Error() {
|
|
||||||
t.Fatalf("want err %q, got %q", want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetRunScraperScrapes(t *testing.T) {
|
|
||||||
testTarget := newTestTarget("bad schema", 0, nil)
|
|
||||||
|
|
||||||
go testTarget.RunScraper(nopAppender{})
|
|
||||||
|
|
||||||
// Enough time for a scrape to happen.
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
if testTarget.status.LastScrape().IsZero() {
|
|
||||||
t.Errorf("Scrape hasn't occured.")
|
|
||||||
}
|
|
||||||
|
|
||||||
testTarget.StopScraper()
|
|
||||||
// Wait for it to take effect.
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
last := testTarget.status.LastScrape()
|
|
||||||
// Enough time for a scrape to happen.
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
if testTarget.status.LastScrape() != last {
|
|
||||||
t.Errorf("Scrape occured after it was stopped.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkScrape(b *testing.B) {
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", `text/plain; version=0.0.4`)
|
|
||||||
w.Write([]byte("test_metric{foo=\"bar\"} 123.456\n"))
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
testTarget := newTestTarget(server.URL, time.Second, model.LabelSet{"dings": "bums"})
|
|
||||||
appender := nopAppender{}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
if err := testTarget.scrape(appender); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestURLParams(t *testing.T) {
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", `text/plain; version=0.0.4`)
|
|
||||||
w.Write([]byte{})
|
|
||||||
r.ParseForm()
|
|
||||||
if r.Form["foo"][0] != "bar" {
|
|
||||||
t.Fatalf("URL parameter 'foo' had unexpected first value '%v'", r.Form["foo"][0])
|
|
||||||
}
|
|
||||||
if r.Form["foo"][1] != "baz" {
|
|
||||||
t.Fatalf("URL parameter 'foo' had unexpected second value '%v'", r.Form["foo"][1])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer server.Close()
|
|
||||||
serverURL, err := url.Parse(server.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := NewTarget(
|
|
||||||
&config.ScrapeConfig{
|
|
||||||
JobName: "test_job1",
|
|
||||||
ScrapeInterval: model.Duration(1 * time.Minute),
|
|
||||||
ScrapeTimeout: model.Duration(1 * time.Second),
|
|
||||||
Scheme: serverURL.Scheme,
|
|
||||||
Params: url.Values{
|
|
||||||
"foo": []string{"bar", "baz"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
model.LabelSet{
|
|
||||||
model.SchemeLabel: model.LabelValue(serverURL.Scheme),
|
|
||||||
model.AddressLabel: model.LabelValue(serverURL.Host),
|
|
||||||
"__param_foo": "bar",
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
app := &collectResultAppender{}
|
|
||||||
if err = target.scrape(app); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -578,23 +128,9 @@ func newTestTarget(targetURL string, deadline time.Duration, labels model.LabelS
|
||||||
labels[model.AddressLabel] = model.LabelValue(strings.TrimLeft(targetURL, "http://"))
|
labels[model.AddressLabel] = model.LabelValue(strings.TrimLeft(targetURL, "http://"))
|
||||||
labels[model.MetricsPathLabel] = "/metrics"
|
labels[model.MetricsPathLabel] = "/metrics"
|
||||||
|
|
||||||
t := &Target{
|
return &Target{
|
||||||
scrapeConfig: &config.ScrapeConfig{
|
labels: labels,
|
||||||
ScrapeInterval: model.Duration(time.Millisecond),
|
|
||||||
ScrapeTimeout: model.Duration(deadline),
|
|
||||||
},
|
|
||||||
labels: labels,
|
|
||||||
status: &TargetStatus{},
|
|
||||||
scraperStopping: make(chan struct{}),
|
|
||||||
scraperStopped: make(chan struct{}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
if t.httpClient, err = t.client(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewHTTPBearerToken(t *testing.T) {
|
func TestNewHTTPBearerToken(t *testing.T) {
|
||||||
|
@ -766,7 +302,7 @@ func newTLSConfig(t *testing.T) *tls.Config {
|
||||||
return tlsConfig
|
return tlsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewTargetWithBadTLSConfig(t *testing.T) {
|
func TestNewClientWithBadTLSConfig(t *testing.T) {
|
||||||
cfg := &config.ScrapeConfig{
|
cfg := &config.ScrapeConfig{
|
||||||
ScrapeTimeout: model.Duration(1 * time.Second),
|
ScrapeTimeout: model.Duration(1 * time.Second),
|
||||||
TLSConfig: config.TLSConfig{
|
TLSConfig: config.TLSConfig{
|
||||||
|
@ -775,7 +311,7 @@ func TestNewTargetWithBadTLSConfig(t *testing.T) {
|
||||||
KeyFile: "testdata/nonexistent_client.key",
|
KeyFile: "testdata/nonexistent_client.key",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, err := NewTarget(cfg, nil, nil)
|
_, err := newHTTPClient(cfg)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error, got nil.")
|
t.Fatalf("Expected error, got nil.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/common/log"
|
"github.com/prometheus/common/log"
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/retrieval/discovery"
|
"github.com/prometheus/prometheus/retrieval/discovery"
|
||||||
|
@ -33,285 +35,102 @@ import (
|
||||||
// The TargetProvider does not have to guarantee that an actual change happened.
|
// The TargetProvider does not have to guarantee that an actual change happened.
|
||||||
// It does guarantee that it sends the new TargetGroup whenever a change happens.
|
// It does guarantee that it sends the new TargetGroup whenever a change happens.
|
||||||
//
|
//
|
||||||
// Sources() is guaranteed to be called exactly once before each call to Run().
|
// Providers must initially send all known target groups as soon as it can.
|
||||||
// On a call to Run() implementing types must send a valid target group for each of
|
|
||||||
// the sources they declared in the last call to Sources().
|
|
||||||
type TargetProvider interface {
|
type TargetProvider interface {
|
||||||
// Sources returns the source identifiers the provider is currently aware of.
|
|
||||||
Sources() []string
|
|
||||||
// Run hands a channel to the target provider through which it can send
|
// Run hands a channel to the target provider through which it can send
|
||||||
// updated target groups. The channel must be closed by the target provider
|
// updated target groups. The channel must be closed by the target provider
|
||||||
// if no more updates will be sent.
|
// if no more updates will be sent.
|
||||||
// On receiving from done Run must return.
|
// On receiving from done Run must return.
|
||||||
Run(up chan<- config.TargetGroup, done <-chan struct{})
|
Run(ctx context.Context, up chan<- []*config.TargetGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TargetManager maintains a set of targets, starts and stops their scraping and
|
// TargetManager maintains a set of targets, starts and stops their scraping and
|
||||||
// creates the new targets based on the target groups it receives from various
|
// creates the new targets based on the target groups it receives from various
|
||||||
// target providers.
|
// target providers.
|
||||||
type TargetManager struct {
|
type TargetManager struct {
|
||||||
mtx sync.RWMutex
|
appender storage.SampleAppender
|
||||||
sampleAppender storage.SampleAppender
|
scrapeConfigs []*config.ScrapeConfig
|
||||||
running bool
|
|
||||||
done chan struct{}
|
|
||||||
|
|
||||||
// Targets by their source ID.
|
mtx sync.RWMutex
|
||||||
targets map[string][]*Target
|
ctx context.Context
|
||||||
// Providers by the scrape configs they are derived from.
|
cancel func()
|
||||||
providers map[*config.ScrapeConfig][]TargetProvider
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Set of unqiue targets by scrape configuration.
|
||||||
|
targetSets map[string]*targetSet
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTargetManager creates a new TargetManager.
|
// NewTargetManager creates a new TargetManager.
|
||||||
func NewTargetManager(sampleAppender storage.SampleAppender) *TargetManager {
|
func NewTargetManager(app storage.SampleAppender) *TargetManager {
|
||||||
tm := &TargetManager{
|
return &TargetManager{
|
||||||
sampleAppender: sampleAppender,
|
appender: app,
|
||||||
targets: map[string][]*Target{},
|
targetSets: map[string]*targetSet{},
|
||||||
}
|
}
|
||||||
return tm
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge multiple target group channels into a single output channel.
|
|
||||||
func merge(done <-chan struct{}, cs ...<-chan targetGroupUpdate) <-chan targetGroupUpdate {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
out := make(chan targetGroupUpdate)
|
|
||||||
|
|
||||||
// Start an output goroutine for each input channel in cs. output
|
|
||||||
// copies values from c to out until c or done is closed, then calls
|
|
||||||
// wg.Done.
|
|
||||||
redir := func(c <-chan targetGroupUpdate) {
|
|
||||||
defer wg.Done()
|
|
||||||
for n := range c {
|
|
||||||
select {
|
|
||||||
case out <- n:
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(len(cs))
|
|
||||||
for _, c := range cs {
|
|
||||||
go redir(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the out channel if all inbound channels are closed.
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(out)
|
|
||||||
}()
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// targetGroupUpdate is a potentially changed/new target group
|
|
||||||
// for the given scrape configuration.
|
|
||||||
type targetGroupUpdate struct {
|
|
||||||
tg config.TargetGroup
|
|
||||||
scfg *config.ScrapeConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts background processing to handle target updates.
|
// Run starts background processing to handle target updates.
|
||||||
func (tm *TargetManager) Run() {
|
func (tm *TargetManager) Run() {
|
||||||
log.Info("Starting target manager...")
|
log.Info("Starting target manager...")
|
||||||
|
|
||||||
tm.done = make(chan struct{})
|
|
||||||
|
|
||||||
sources := map[string]struct{}{}
|
|
||||||
updates := []<-chan targetGroupUpdate{}
|
|
||||||
|
|
||||||
for scfg, provs := range tm.providers {
|
|
||||||
for _, prov := range provs {
|
|
||||||
// Get an initial set of available sources so we don't remove
|
|
||||||
// target groups from the last run that are still available.
|
|
||||||
for _, src := range prov.Sources() {
|
|
||||||
sources[src] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
tgc := make(chan config.TargetGroup)
|
|
||||||
// Run the target provider after cleanup of the stale targets is done.
|
|
||||||
defer func(prov TargetProvider, tgc chan<- config.TargetGroup, done <-chan struct{}) {
|
|
||||||
go prov.Run(tgc, done)
|
|
||||||
}(prov, tgc, tm.done)
|
|
||||||
|
|
||||||
tgupc := make(chan targetGroupUpdate)
|
|
||||||
updates = append(updates, tgupc)
|
|
||||||
|
|
||||||
go func(scfg *config.ScrapeConfig, done <-chan struct{}) {
|
|
||||||
defer close(tgupc)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case tg := <-tgc:
|
|
||||||
tgupc <- targetGroupUpdate{tg: tg, scfg: scfg}
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(scfg, tm.done)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge all channels of incoming target group updates into a single
|
|
||||||
// one and keep applying the updates.
|
|
||||||
go tm.handleUpdates(merge(tm.done, updates...), tm.done)
|
|
||||||
|
|
||||||
tm.mtx.Lock()
|
tm.mtx.Lock()
|
||||||
defer tm.mtx.Unlock()
|
|
||||||
|
|
||||||
// Remove old target groups that are no longer in the set of sources.
|
tm.ctx, tm.cancel = context.WithCancel(context.Background())
|
||||||
tm.removeTargets(func(src string) bool {
|
tm.reload()
|
||||||
if _, ok := sources[src]; ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
tm.running = true
|
tm.mtx.Unlock()
|
||||||
log.Info("Target manager started.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleUpdates receives target group updates and handles them in the
|
tm.wg.Wait()
|
||||||
// context of the given job config.
|
|
||||||
func (tm *TargetManager) handleUpdates(ch <-chan targetGroupUpdate, done <-chan struct{}) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case update, ok := <-ch:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Debugf("Received potential update for target group %q", update.tg.Source)
|
|
||||||
|
|
||||||
if err := tm.updateTargetGroup(&update.tg, update.scfg); err != nil {
|
|
||||||
log.Errorf("Error updating targets: %s", err)
|
|
||||||
}
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop all background processing.
|
// Stop all background processing.
|
||||||
func (tm *TargetManager) Stop() {
|
func (tm *TargetManager) Stop() {
|
||||||
tm.mtx.RLock()
|
log.Infoln("Stopping target manager...")
|
||||||
if tm.running {
|
|
||||||
defer tm.stop(true)
|
|
||||||
}
|
|
||||||
// Return the lock before calling tm.stop().
|
|
||||||
defer tm.mtx.RUnlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop background processing of the target manager. If removeTargets is true,
|
|
||||||
// existing targets will be stopped and removed.
|
|
||||||
func (tm *TargetManager) stop(removeTargets bool) {
|
|
||||||
log.Info("Stopping target manager...")
|
|
||||||
defer log.Info("Target manager stopped.")
|
|
||||||
|
|
||||||
close(tm.done)
|
|
||||||
|
|
||||||
tm.mtx.Lock()
|
tm.mtx.Lock()
|
||||||
defer tm.mtx.Unlock()
|
// Cancel the base context, this will cause all target providers to shut down
|
||||||
|
// and all in-flight scrapes to abort immmediately.
|
||||||
|
// Started inserts will be finished before terminating.
|
||||||
|
tm.cancel()
|
||||||
|
tm.mtx.Unlock()
|
||||||
|
|
||||||
if removeTargets {
|
// Wait for all scrape inserts to complete.
|
||||||
tm.removeTargets(nil)
|
tm.wg.Wait()
|
||||||
}
|
|
||||||
|
|
||||||
tm.running = false
|
log.Debugln("Target manager stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeTargets stops and removes targets for sources where f(source) is true
|
func (tm *TargetManager) reload() {
|
||||||
// or if f is nil. This method is not thread-safe.
|
jobs := map[string]struct{}{}
|
||||||
func (tm *TargetManager) removeTargets(f func(string) bool) {
|
|
||||||
if f == nil {
|
|
||||||
f = func(string) bool { return true }
|
|
||||||
}
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for src, targets := range tm.targets {
|
|
||||||
if !f(src) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wg.Add(len(targets))
|
|
||||||
for _, target := range targets {
|
|
||||||
go func(t *Target) {
|
|
||||||
t.StopScraper()
|
|
||||||
wg.Done()
|
|
||||||
}(target)
|
|
||||||
}
|
|
||||||
delete(tm.targets, src)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateTargetGroup creates new targets for the group and replaces the old targets
|
// Start new target sets and update existing ones.
|
||||||
// for the source ID.
|
for _, scfg := range tm.scrapeConfigs {
|
||||||
func (tm *TargetManager) updateTargetGroup(tgroup *config.TargetGroup, cfg *config.ScrapeConfig) error {
|
jobs[scfg.JobName] = struct{}{}
|
||||||
newTargets, err := tm.targetsFromGroup(tgroup, cfg)
|
|
||||||
if err != nil {
|
ts, ok := tm.targetSets[scfg.JobName]
|
||||||
return err
|
if !ok {
|
||||||
|
ts = newTargetSet(scfg, tm.appender)
|
||||||
|
tm.targetSets[scfg.JobName] = ts
|
||||||
|
|
||||||
|
tm.wg.Add(1)
|
||||||
|
|
||||||
|
go func(ts *targetSet) {
|
||||||
|
ts.runScraping(tm.ctx)
|
||||||
|
tm.wg.Done()
|
||||||
|
}(ts)
|
||||||
|
} else {
|
||||||
|
ts.reload(scfg)
|
||||||
|
}
|
||||||
|
ts.runProviders(tm.ctx, providersFromConfig(scfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
tm.mtx.Lock()
|
// Remove old target sets. Waiting for stopping is already guaranteed
|
||||||
defer tm.mtx.Unlock()
|
// by the goroutine that started the target set.
|
||||||
|
for name, ts := range tm.targetSets {
|
||||||
if !tm.running {
|
if _, ok := jobs[name]; !ok {
|
||||||
return nil
|
ts.cancel()
|
||||||
}
|
delete(tm.targetSets, name)
|
||||||
|
|
||||||
oldTargets, ok := tm.targets[tgroup.Source]
|
|
||||||
if ok {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
// Replace the old targets with the new ones while keeping the state
|
|
||||||
// of intersecting targets.
|
|
||||||
for i, tnew := range newTargets {
|
|
||||||
var match *Target
|
|
||||||
for j, told := range oldTargets {
|
|
||||||
if told == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if tnew.fingerprint() == told.fingerprint() {
|
|
||||||
match = told
|
|
||||||
oldTargets[j] = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update the existing target and discard the new equivalent.
|
|
||||||
// Otherwise start scraping the new target.
|
|
||||||
if match != nil {
|
|
||||||
// Updating is blocked during a scrape. We don't want those wait times
|
|
||||||
// to build up.
|
|
||||||
wg.Add(1)
|
|
||||||
go func(t *Target) {
|
|
||||||
if err := match.Update(cfg, t.labels, t.metaLabels); err != nil {
|
|
||||||
log.Errorf("Error updating target %v: %v", t, err)
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}(tnew)
|
|
||||||
newTargets[i] = match
|
|
||||||
} else {
|
|
||||||
go tnew.RunScraper(tm.sampleAppender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove all old targets that disappeared.
|
|
||||||
for _, told := range oldTargets {
|
|
||||||
if told != nil {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(t *Target) {
|
|
||||||
t.StopScraper()
|
|
||||||
wg.Done()
|
|
||||||
}(told)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
} else {
|
|
||||||
// The source ID is new, start all target scrapers.
|
|
||||||
for _, tnew := range newTargets {
|
|
||||||
go tnew.RunScraper(tm.sampleAppender)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(newTargets) > 0 {
|
|
||||||
tm.targets[tgroup.Source] = newTargets
|
|
||||||
} else {
|
|
||||||
delete(tm.targets, tgroup.Source)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pools returns the targets currently being scraped bucketed by their job name.
|
// Pools returns the targets currently being scraped bucketed by their job name.
|
||||||
|
@ -321,11 +140,16 @@ func (tm *TargetManager) Pools() map[string][]*Target {
|
||||||
|
|
||||||
pools := map[string][]*Target{}
|
pools := map[string][]*Target{}
|
||||||
|
|
||||||
for _, ts := range tm.targets {
|
// TODO(fabxc): this is just a hack to maintain compatibility for now.
|
||||||
for _, t := range ts {
|
for _, ps := range tm.targetSets {
|
||||||
|
ps.scrapePool.mtx.RLock()
|
||||||
|
|
||||||
|
for _, t := range ps.scrapePool.targets {
|
||||||
job := string(t.Labels()[model.JobLabel])
|
job := string(t.Labels()[model.JobLabel])
|
||||||
pools[job] = append(pools[job], t)
|
pools[job] = append(pools[job], t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ps.scrapePool.mtx.RUnlock()
|
||||||
}
|
}
|
||||||
return pools
|
return pools
|
||||||
}
|
}
|
||||||
|
@ -334,79 +158,196 @@ func (tm *TargetManager) Pools() map[string][]*Target {
|
||||||
// by the new cfg. The state of targets that are valid in the new configuration remains unchanged.
|
// by the new cfg. The state of targets that are valid in the new configuration remains unchanged.
|
||||||
// Returns true on success.
|
// Returns true on success.
|
||||||
func (tm *TargetManager) ApplyConfig(cfg *config.Config) bool {
|
func (tm *TargetManager) ApplyConfig(cfg *config.Config) bool {
|
||||||
tm.mtx.RLock()
|
|
||||||
running := tm.running
|
|
||||||
tm.mtx.RUnlock()
|
|
||||||
|
|
||||||
if running {
|
|
||||||
tm.stop(false)
|
|
||||||
// Even if updating the config failed, we want to continue rather than stop scraping anything.
|
|
||||||
defer tm.Run()
|
|
||||||
}
|
|
||||||
providers := map[*config.ScrapeConfig][]TargetProvider{}
|
|
||||||
|
|
||||||
for _, scfg := range cfg.ScrapeConfigs {
|
|
||||||
providers[scfg] = providersFromConfig(scfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
tm.mtx.Lock()
|
tm.mtx.Lock()
|
||||||
defer tm.mtx.Unlock()
|
defer tm.mtx.Unlock()
|
||||||
|
|
||||||
tm.providers = providers
|
tm.scrapeConfigs = cfg.ScrapeConfigs
|
||||||
|
|
||||||
|
if tm.ctx != nil {
|
||||||
|
tm.reload()
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// prefixedTargetProvider wraps TargetProvider and prefixes source strings
|
// targetSet holds several TargetProviders for which the same scrape configuration
|
||||||
// to make the sources unique across a configuration.
|
// is used. It maintains target groups from all given providers and sync them
|
||||||
type prefixedTargetProvider struct {
|
// to a scrape pool.
|
||||||
TargetProvider
|
type targetSet struct {
|
||||||
|
mtx sync.RWMutex
|
||||||
|
|
||||||
job string
|
// Sets of targets by a source string that is unique across target providers.
|
||||||
mechanism string
|
tgroups map[string][]*Target
|
||||||
idx int
|
providers map[string]TargetProvider
|
||||||
|
|
||||||
|
scrapePool *scrapePool
|
||||||
|
config *config.ScrapeConfig
|
||||||
|
|
||||||
|
syncCh chan struct{}
|
||||||
|
cancelScraping func()
|
||||||
|
cancelProviders func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tp *prefixedTargetProvider) prefix(src string) string {
|
func newTargetSet(cfg *config.ScrapeConfig, app storage.SampleAppender) *targetSet {
|
||||||
return fmt.Sprintf("%s:%s:%d:%s", tp.job, tp.mechanism, tp.idx, src)
|
ts := &targetSet{
|
||||||
}
|
tgroups: map[string][]*Target{},
|
||||||
|
scrapePool: newScrapePool(cfg, app),
|
||||||
func (tp *prefixedTargetProvider) Sources() []string {
|
syncCh: make(chan struct{}, 1),
|
||||||
srcs := tp.TargetProvider.Sources()
|
config: cfg,
|
||||||
for i, src := range srcs {
|
|
||||||
srcs[i] = tp.prefix(src)
|
|
||||||
}
|
}
|
||||||
|
return ts
|
||||||
return srcs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tp *prefixedTargetProvider) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (ts *targetSet) cancel() {
|
||||||
defer close(ch)
|
ts.mtx.RLock()
|
||||||
|
defer ts.mtx.RUnlock()
|
||||||
|
|
||||||
ch2 := make(chan config.TargetGroup)
|
if ts.cancelScraping != nil {
|
||||||
go tp.TargetProvider.Run(ch2, done)
|
ts.cancelScraping()
|
||||||
|
}
|
||||||
|
if ts.cancelProviders != nil {
|
||||||
|
ts.cancelProviders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *targetSet) reload(cfg *config.ScrapeConfig) {
|
||||||
|
ts.mtx.Lock()
|
||||||
|
ts.config = cfg
|
||||||
|
ts.mtx.Unlock()
|
||||||
|
|
||||||
|
ts.scrapePool.reload(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *targetSet) runScraping(ctx context.Context) {
|
||||||
|
ctx, ts.cancelScraping = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
ts.scrapePool.ctx = ctx
|
||||||
|
|
||||||
|
Loop:
|
||||||
for {
|
for {
|
||||||
|
// Throttle syncing to once per five seconds.
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
break Loop
|
||||||
case tg := <-ch2:
|
case <-time.After(5 * time.Second):
|
||||||
tg.Source = tp.prefix(tg.Source)
|
}
|
||||||
ch <- tg
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
break Loop
|
||||||
|
case <-ts.syncCh:
|
||||||
|
ts.mtx.RLock()
|
||||||
|
ts.sync()
|
||||||
|
ts.mtx.RUnlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We want to wait for all pending target scrapes to complete though to ensure there'll
|
||||||
|
// be no more storage writes after this point.
|
||||||
|
ts.scrapePool.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *targetSet) sync() {
|
||||||
|
var all []*Target
|
||||||
|
for _, targets := range ts.tgroups {
|
||||||
|
all = append(all, targets...)
|
||||||
|
}
|
||||||
|
ts.scrapePool.sync(all)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *targetSet) runProviders(ctx context.Context, providers map[string]TargetProvider) {
|
||||||
|
// Lock for the entire time. This may mean up to 5 seconds until the full initial set
|
||||||
|
// is retrieved and applied.
|
||||||
|
// We could release earlier with some tweaks, but this is easier to reason about.
|
||||||
|
ts.mtx.Lock()
|
||||||
|
defer ts.mtx.Unlock()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
if ts.cancelProviders != nil {
|
||||||
|
ts.cancelProviders()
|
||||||
|
}
|
||||||
|
ctx, ts.cancelProviders = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
for name, prov := range providers {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
updates := make(chan []*config.TargetGroup)
|
||||||
|
|
||||||
|
go func(name string, prov TargetProvider) {
|
||||||
|
var initial []*config.TargetGroup
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
case initial = <-updates:
|
||||||
|
// First set of all targets the provider knows.
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
// Initial set didn't arrive. Act as if it was empty
|
||||||
|
// and wait for updates later on.
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tgroup := range initial {
|
||||||
|
targets, err := targetsFromGroup(tgroup, ts.config)
|
||||||
|
if err != nil {
|
||||||
|
log.With("target_group", tgroup).Errorf("Target update failed: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ts.tgroups[name+"/"+tgroup.Source] = targets
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
|
||||||
|
// Start listening for further updates.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case tgs := <-updates:
|
||||||
|
for _, tg := range tgs {
|
||||||
|
if err := ts.update(name, tg); err != nil {
|
||||||
|
log.With("target_group", tg).Errorf("Target update failed: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(name, prov)
|
||||||
|
|
||||||
|
go prov.Run(ctx, updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We wait for a full initial set of target groups before releasing the mutex
|
||||||
|
// to ensure the initial sync is complete and there are no races with subsequent updates.
|
||||||
|
wg.Wait()
|
||||||
|
ts.sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
// update handles a target group update from a target provider identified by the name.
|
||||||
|
func (ts *targetSet) update(name string, tgroup *config.TargetGroup) error {
|
||||||
|
targets, err := targetsFromGroup(tgroup, ts.config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.mtx.Lock()
|
||||||
|
defer ts.mtx.Unlock()
|
||||||
|
|
||||||
|
ts.tgroups[name+"/"+tgroup.Source] = targets
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ts.syncCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// providersFromConfig returns all TargetProviders configured in cfg.
|
// providersFromConfig returns all TargetProviders configured in cfg.
|
||||||
func providersFromConfig(cfg *config.ScrapeConfig) []TargetProvider {
|
func providersFromConfig(cfg *config.ScrapeConfig) map[string]TargetProvider {
|
||||||
var providers []TargetProvider
|
providers := map[string]TargetProvider{}
|
||||||
|
|
||||||
app := func(mech string, i int, tp TargetProvider) {
|
app := func(mech string, i int, tp TargetProvider) {
|
||||||
providers = append(providers, &prefixedTargetProvider{
|
providers[fmt.Sprintf("%s/%d", mech, i)] = tp
|
||||||
job: cfg.JobName,
|
|
||||||
mechanism: mech,
|
|
||||||
idx: i,
|
|
||||||
TargetProvider: tp,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, c := range cfg.DNSSDConfigs {
|
for i, c := range cfg.DNSSDConfigs {
|
||||||
|
@ -451,11 +392,9 @@ func providersFromConfig(cfg *config.ScrapeConfig) []TargetProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// targetsFromGroup builds targets based on the given TargetGroup and config.
|
// targetsFromGroup builds targets based on the given TargetGroup and config.
|
||||||
func (tm *TargetManager) targetsFromGroup(tg *config.TargetGroup, cfg *config.ScrapeConfig) ([]*Target, error) {
|
func targetsFromGroup(tg *config.TargetGroup, cfg *config.ScrapeConfig) ([]*Target, error) {
|
||||||
tm.mtx.RLock()
|
|
||||||
defer tm.mtx.RUnlock()
|
|
||||||
|
|
||||||
targets := make([]*Target, 0, len(tg.Targets))
|
targets := make([]*Target, 0, len(tg.Targets))
|
||||||
|
|
||||||
for i, labels := range tg.Targets {
|
for i, labels := range tg.Targets {
|
||||||
for k, v := range cfg.Params {
|
for k, v := range cfg.Params {
|
||||||
if len(v) > 0 {
|
if len(v) > 0 {
|
||||||
|
@ -518,11 +457,12 @@ func (tm *TargetManager) targetsFromGroup(tg *config.TargetGroup, cfg *config.Sc
|
||||||
delete(labels, ln)
|
delete(labels, ln)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr, err := NewTarget(cfg, labels, preRelabelLabels)
|
|
||||||
if err != nil {
|
if _, ok := labels[model.InstanceLabel]; !ok {
|
||||||
return nil, fmt.Errorf("error while creating instance %d in target group %s: %s", i, tg, err)
|
labels[model.InstanceLabel] = labels[model.AddressLabel]
|
||||||
}
|
}
|
||||||
targets = append(targets, tr)
|
|
||||||
|
targets = append(targets, NewTarget(labels, preRelabelLabels, cfg.Params))
|
||||||
}
|
}
|
||||||
|
|
||||||
return targets, nil
|
return targets, nil
|
||||||
|
@ -539,29 +479,16 @@ func NewStaticProvider(groups []*config.TargetGroup) *StaticProvider {
|
||||||
for i, tg := range groups {
|
for i, tg := range groups {
|
||||||
tg.Source = fmt.Sprintf("%d", i)
|
tg.Source = fmt.Sprintf("%d", i)
|
||||||
}
|
}
|
||||||
return &StaticProvider{
|
return &StaticProvider{groups}
|
||||||
TargetGroups: groups,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run implements the TargetProvider interface.
|
// Run implements the TargetProvider interface.
|
||||||
func (sd *StaticProvider) Run(ch chan<- config.TargetGroup, done <-chan struct{}) {
|
func (sd *StaticProvider) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
|
||||||
defer close(ch)
|
// We still have to consider that the consumer exits right away in which case
|
||||||
|
// the context will be canceled.
|
||||||
for _, tg := range sd.TargetGroups {
|
select {
|
||||||
select {
|
case ch <- sd.TargetGroups:
|
||||||
case <-done:
|
case <-ctx.Done():
|
||||||
return
|
|
||||||
case ch <- *tg:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
<-done
|
close(ch)
|
||||||
}
|
|
||||||
|
|
||||||
// Sources returns the provider's sources.
|
|
||||||
func (sd *StaticProvider) Sources() (srcs []string) {
|
|
||||||
for _, tg := range sd.TargetGroups {
|
|
||||||
srcs = append(srcs, tg.Source)
|
|
||||||
}
|
|
||||||
return srcs
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,492 +12,3 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package retrieval
|
package retrieval
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/prometheus/common/model"
|
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPrefixedTargetProvider(t *testing.T) {
|
|
||||||
targetGroups := []*config.TargetGroup{
|
|
||||||
{
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "test-1:1234"},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "test-1:1235"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tp := &prefixedTargetProvider{
|
|
||||||
job: "job-x",
|
|
||||||
mechanism: "static",
|
|
||||||
idx: 123,
|
|
||||||
TargetProvider: NewStaticProvider(targetGroups),
|
|
||||||
}
|
|
||||||
|
|
||||||
expSources := []string{
|
|
||||||
"job-x:static:123:0",
|
|
||||||
"job-x:static:123:1",
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(tp.Sources(), expSources) {
|
|
||||||
t.Fatalf("expected sources %v, got %v", expSources, tp.Sources())
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := make(chan config.TargetGroup)
|
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
defer close(done)
|
|
||||||
go tp.Run(ch, done)
|
|
||||||
|
|
||||||
expGroup1 := *targetGroups[0]
|
|
||||||
expGroup2 := *targetGroups[1]
|
|
||||||
expGroup1.Source = "job-x:static:123:0"
|
|
||||||
expGroup2.Source = "job-x:static:123:1"
|
|
||||||
|
|
||||||
// The static target provider sends on the channel once per target group.
|
|
||||||
if tg := <-ch; !reflect.DeepEqual(tg, expGroup1) {
|
|
||||||
t.Fatalf("expected target group %v, got %v", expGroup1, tg)
|
|
||||||
}
|
|
||||||
if tg := <-ch; !reflect.DeepEqual(tg, expGroup2) {
|
|
||||||
t.Fatalf("expected target group %v, got %v", expGroup2, tg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetManagerChan(t *testing.T) {
|
|
||||||
testJob1 := &config.ScrapeConfig{
|
|
||||||
JobName: "test_job1",
|
|
||||||
ScrapeInterval: model.Duration(1 * time.Minute),
|
|
||||||
TargetGroups: []*config.TargetGroup{{
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "example.org:80"},
|
|
||||||
{model.AddressLabel: "example.com:80"},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
prov1 := &fakeTargetProvider{
|
|
||||||
sources: []string{"src1", "src2"},
|
|
||||||
update: make(chan *config.TargetGroup),
|
|
||||||
}
|
|
||||||
|
|
||||||
targetManager := &TargetManager{
|
|
||||||
sampleAppender: nopAppender{},
|
|
||||||
providers: map[*config.ScrapeConfig][]TargetProvider{
|
|
||||||
testJob1: {prov1},
|
|
||||||
},
|
|
||||||
targets: make(map[string][]*Target),
|
|
||||||
}
|
|
||||||
go targetManager.Run()
|
|
||||||
defer targetManager.Stop()
|
|
||||||
|
|
||||||
sequence := []struct {
|
|
||||||
tgroup *config.TargetGroup
|
|
||||||
expected map[string][]model.LabelSet
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
tgroup: &config.TargetGroup{
|
|
||||||
Source: "src1",
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "test-1:1234"},
|
|
||||||
{model.AddressLabel: "test-2:1234", "label": "set"},
|
|
||||||
{model.AddressLabel: "test-3:1234"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: map[string][]model.LabelSet{
|
|
||||||
"src1": {
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-1:1234"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-2:1234", "label": "set"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-3:1234"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
tgroup: &config.TargetGroup{
|
|
||||||
Source: "src2",
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "test-1:1235"},
|
|
||||||
{model.AddressLabel: "test-2:1235"},
|
|
||||||
{model.AddressLabel: "test-3:1235"},
|
|
||||||
},
|
|
||||||
Labels: model.LabelSet{"group": "label"},
|
|
||||||
},
|
|
||||||
expected: map[string][]model.LabelSet{
|
|
||||||
"src1": {
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-1:1234"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-2:1234", "label": "set"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-3:1234"},
|
|
||||||
},
|
|
||||||
"src2": {
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-1:1235", "group": "label"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-2:1235", "group": "label"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-3:1235", "group": "label"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
tgroup: &config.TargetGroup{
|
|
||||||
Source: "src2",
|
|
||||||
Targets: []model.LabelSet{},
|
|
||||||
},
|
|
||||||
expected: map[string][]model.LabelSet{
|
|
||||||
"src1": {
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-1:1234"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-2:1234", "label": "set"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-3:1234"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
tgroup: &config.TargetGroup{
|
|
||||||
Source: "src1",
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "test-1:1234", "added": "label"},
|
|
||||||
{model.AddressLabel: "test-3:1234"},
|
|
||||||
{model.AddressLabel: "test-4:1234", "fancy": "label"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: map[string][]model.LabelSet{
|
|
||||||
"src1": {
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-1:1234", "added": "label"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-3:1234"},
|
|
||||||
{model.JobLabel: "test_job1", model.InstanceLabel: "test-4:1234", "fancy": "label"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, step := range sequence {
|
|
||||||
prov1.update <- step.tgroup
|
|
||||||
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
|
|
||||||
if len(targetManager.targets) != len(step.expected) {
|
|
||||||
t.Fatalf("step %d: sources mismatch %v, %v", i, targetManager.targets, step.expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
for source, actTargets := range targetManager.targets {
|
|
||||||
expTargets, ok := step.expected[source]
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("step %d: unexpected source %q: %v", i, source, actTargets)
|
|
||||||
}
|
|
||||||
for _, expt := range expTargets {
|
|
||||||
found := false
|
|
||||||
for _, actt := range actTargets {
|
|
||||||
if reflect.DeepEqual(expt, actt.Labels()) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("step %d: expected target %v not found in actual targets", i, expt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetManagerConfigUpdate(t *testing.T) {
|
|
||||||
testJob1 := &config.ScrapeConfig{
|
|
||||||
JobName: "test_job1",
|
|
||||||
ScrapeInterval: model.Duration(1 * time.Minute),
|
|
||||||
Params: url.Values{
|
|
||||||
"testParam": []string{"paramValue", "secondValue"},
|
|
||||||
},
|
|
||||||
TargetGroups: []*config.TargetGroup{{
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "example.org:80"},
|
|
||||||
{model.AddressLabel: "example.com"},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
RelabelConfigs: []*config.RelabelConfig{
|
|
||||||
{
|
|
||||||
// Copy out the URL parameter.
|
|
||||||
SourceLabels: model.LabelNames{"__param_testParam"},
|
|
||||||
Regex: config.MustNewRegexp("(.*)"),
|
|
||||||
TargetLabel: "testParam",
|
|
||||||
Replacement: "$1",
|
|
||||||
Action: config.RelabelReplace,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// The port number is added after relabeling, so
|
|
||||||
// this relabel rule should have no effect.
|
|
||||||
SourceLabels: model.LabelNames{model.AddressLabel},
|
|
||||||
Regex: config.MustNewRegexp("example.com:80"),
|
|
||||||
Action: config.RelabelDrop,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
testJob2 := &config.ScrapeConfig{
|
|
||||||
JobName: "test_job2",
|
|
||||||
ScrapeInterval: model.Duration(1 * time.Minute),
|
|
||||||
TargetGroups: []*config.TargetGroup{
|
|
||||||
{
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "example.org:8080"},
|
|
||||||
{model.AddressLabel: "example.com:8081"},
|
|
||||||
},
|
|
||||||
Labels: model.LabelSet{
|
|
||||||
"foo": "bar",
|
|
||||||
"boom": "box",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "test.com:1234"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "test.com:1235"},
|
|
||||||
},
|
|
||||||
Labels: model.LabelSet{"instance": "fixed"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
RelabelConfigs: []*config.RelabelConfig{
|
|
||||||
{
|
|
||||||
SourceLabels: model.LabelNames{model.AddressLabel},
|
|
||||||
Regex: config.MustNewRegexp(`test\.(.*?):(.*)`),
|
|
||||||
Replacement: "foo.${1}:${2}",
|
|
||||||
TargetLabel: model.AddressLabel,
|
|
||||||
Action: config.RelabelReplace,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Add a new label for example.* targets.
|
|
||||||
SourceLabels: model.LabelNames{model.AddressLabel, "boom", "foo"},
|
|
||||||
Regex: config.MustNewRegexp("example.*?-b([a-z-]+)r"),
|
|
||||||
TargetLabel: "new",
|
|
||||||
Replacement: "$1",
|
|
||||||
Separator: "-",
|
|
||||||
Action: config.RelabelReplace,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Drop an existing label.
|
|
||||||
SourceLabels: model.LabelNames{"boom"},
|
|
||||||
Regex: config.MustNewRegexp(".*"),
|
|
||||||
TargetLabel: "boom",
|
|
||||||
Replacement: "",
|
|
||||||
Action: config.RelabelReplace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Test that targets without host:port addresses are dropped.
|
|
||||||
testJob3 := &config.ScrapeConfig{
|
|
||||||
JobName: "test_job1",
|
|
||||||
ScrapeInterval: model.Duration(1 * time.Minute),
|
|
||||||
TargetGroups: []*config.TargetGroup{{
|
|
||||||
Targets: []model.LabelSet{
|
|
||||||
{model.AddressLabel: "example.net:80"},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
RelabelConfigs: []*config.RelabelConfig{
|
|
||||||
{
|
|
||||||
SourceLabels: model.LabelNames{model.AddressLabel},
|
|
||||||
Regex: config.MustNewRegexp("(.*)"),
|
|
||||||
TargetLabel: "__address__",
|
|
||||||
Replacement: "http://$1",
|
|
||||||
Action: config.RelabelReplace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
sequence := []struct {
|
|
||||||
scrapeConfigs []*config.ScrapeConfig
|
|
||||||
expected map[string][]model.LabelSet
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
scrapeConfigs: []*config.ScrapeConfig{testJob1},
|
|
||||||
expected: map[string][]model.LabelSet{
|
|
||||||
"test_job1:static:0:0": {
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job1",
|
|
||||||
"testParam": "paramValue",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.org:80",
|
|
||||||
model.ParamLabelPrefix + "testParam": "paramValue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job1",
|
|
||||||
"testParam": "paramValue",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.com:80",
|
|
||||||
model.ParamLabelPrefix + "testParam": "paramValue"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
scrapeConfigs: []*config.ScrapeConfig{testJob1},
|
|
||||||
expected: map[string][]model.LabelSet{
|
|
||||||
"test_job1:static:0:0": {
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job1",
|
|
||||||
"testParam": "paramValue",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.org:80",
|
|
||||||
model.ParamLabelPrefix + "testParam": "paramValue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job1",
|
|
||||||
"testParam": "paramValue",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.com:80",
|
|
||||||
model.ParamLabelPrefix + "testParam": "paramValue",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
scrapeConfigs: []*config.ScrapeConfig{testJob1, testJob2},
|
|
||||||
expected: map[string][]model.LabelSet{
|
|
||||||
"test_job1:static:0:0": {
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job1",
|
|
||||||
"testParam": "paramValue",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.org:80",
|
|
||||||
model.ParamLabelPrefix + "testParam": "paramValue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job1",
|
|
||||||
"testParam": "paramValue",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.com:80",
|
|
||||||
model.ParamLabelPrefix + "testParam": "paramValue",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"test_job2:static:0:0": {
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job2",
|
|
||||||
"foo": "bar",
|
|
||||||
"new": "ox-ba",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.org:8080",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job2",
|
|
||||||
"foo": "bar",
|
|
||||||
"new": "ox-ba",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.com:8081",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"test_job2:static:0:1": {
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job2",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "foo.com:1234",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"test_job2:static:0:2": {
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job2",
|
|
||||||
model.InstanceLabel: "fixed",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "foo.com:1235",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
scrapeConfigs: []*config.ScrapeConfig{},
|
|
||||||
expected: map[string][]model.LabelSet{},
|
|
||||||
}, {
|
|
||||||
scrapeConfigs: []*config.ScrapeConfig{testJob2},
|
|
||||||
expected: map[string][]model.LabelSet{
|
|
||||||
"test_job2:static:0:0": {
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job2",
|
|
||||||
"foo": "bar",
|
|
||||||
"new": "ox-ba",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.org:8080"},
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job2",
|
|
||||||
"foo": "bar",
|
|
||||||
"new": "ox-ba",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "example.com:8081",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"test_job2:static:0:1": {
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job2",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "foo.com:1234",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"test_job2:static:0:2": {
|
|
||||||
{
|
|
||||||
model.JobLabel: "test_job2",
|
|
||||||
model.InstanceLabel: "fixed",
|
|
||||||
model.SchemeLabel: "",
|
|
||||||
model.MetricsPathLabel: "",
|
|
||||||
model.AddressLabel: "foo.com:1235",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
scrapeConfigs: []*config.ScrapeConfig{testJob3},
|
|
||||||
expected: map[string][]model.LabelSet{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
conf := &config.Config{}
|
|
||||||
*conf = config.DefaultConfig
|
|
||||||
|
|
||||||
targetManager := NewTargetManager(nopAppender{})
|
|
||||||
targetManager.ApplyConfig(conf)
|
|
||||||
|
|
||||||
targetManager.Run()
|
|
||||||
defer targetManager.Stop()
|
|
||||||
|
|
||||||
for i, step := range sequence {
|
|
||||||
conf.ScrapeConfigs = step.scrapeConfigs
|
|
||||||
targetManager.ApplyConfig(conf)
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
if len(targetManager.targets) != len(step.expected) {
|
|
||||||
t.Fatalf("step %d: sources mismatch: expected %v, got %v", i, step.expected, targetManager.targets)
|
|
||||||
}
|
|
||||||
|
|
||||||
for source, actTargets := range targetManager.targets {
|
|
||||||
expTargets, ok := step.expected[source]
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("step %d: unexpected source %q: %v", i, source, actTargets)
|
|
||||||
}
|
|
||||||
for _, expt := range expTargets {
|
|
||||||
found := false
|
|
||||||
for _, actt := range actTargets {
|
|
||||||
if reflect.DeepEqual(expt, actt.labels) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("step %d: expected target %v for %q not found in actual targets", i, expt, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleUpdatesReturnsWhenUpdateChanIsClosed(t *testing.T) {
|
|
||||||
tm := NewTargetManager(nopAppender{})
|
|
||||||
ch := make(chan targetGroupUpdate)
|
|
||||||
close(ch)
|
|
||||||
tm.handleUpdates(ch, make(chan struct{}))
|
|
||||||
}
|
|
||||||
|
|
|
@ -110,6 +110,10 @@ func (rule *AlertingRule) Name() string {
|
||||||
return rule.name
|
return rule.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *AlertingRule) equal(o *AlertingRule) bool {
|
||||||
|
return r.name == o.name && r.labels.Equal(o.labels)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *AlertingRule) sample(alert *Alert, ts model.Time, set bool) *model.Sample {
|
func (r *AlertingRule) sample(alert *Alert, ts model.Time, set bool) *model.Sample {
|
||||||
metric := model.Metric(r.labels.Clone())
|
metric := model.Metric(r.labels.Clone())
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import (
|
||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/notification"
|
"github.com/prometheus/prometheus/notifier"
|
||||||
"github.com/prometheus/prometheus/promql"
|
"github.com/prometheus/prometheus/promql"
|
||||||
"github.com/prometheus/prometheus/storage"
|
"github.com/prometheus/prometheus/storage"
|
||||||
"github.com/prometheus/prometheus/template"
|
"github.com/prometheus/prometheus/template"
|
||||||
|
@ -212,8 +212,12 @@ func (g *Group) copyState(from *Group) {
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if far.Name() == ar.Name() {
|
// TODO(fabxc): forbid same alert definitions that are not unique by
|
||||||
ar.active = far.active
|
// at least on static label or alertname?
|
||||||
|
if far.equal(ar) {
|
||||||
|
for fp, a := range far.active {
|
||||||
|
ar.active[fp] = a
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,7 +347,7 @@ func (g *Group) sendAlerts(rule *AlertingRule, timestamp model.Time) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(alerts) > 0 {
|
if len(alerts) > 0 {
|
||||||
g.opts.NotificationHandler.Send(alerts...)
|
g.opts.Notifier.Send(alerts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -359,10 +363,10 @@ type Manager struct {
|
||||||
|
|
||||||
// ManagerOptions bundles options for the Manager.
|
// ManagerOptions bundles options for the Manager.
|
||||||
type ManagerOptions struct {
|
type ManagerOptions struct {
|
||||||
ExternalURL *url.URL
|
ExternalURL *url.URL
|
||||||
QueryEngine *promql.Engine
|
QueryEngine *promql.Engine
|
||||||
NotificationHandler *notification.Handler
|
Notifier *notifier.Notifier
|
||||||
SampleAppender storage.SampleAppender
|
SampleAppender storage.SampleAppender
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager returns an implementation of Manager, ready to be started
|
// NewManager returns an implementation of Manager, ready to be started
|
||||||
|
|
|
@ -20,7 +20,7 @@ repo_path="github.com/prometheus/prometheus"
|
||||||
version=$( cat version/VERSION )
|
version=$( cat version/VERSION )
|
||||||
revision=$( git rev-parse --short HEAD 2> /dev/null || echo 'unknown' )
|
revision=$( git rev-parse --short HEAD 2> /dev/null || echo 'unknown' )
|
||||||
branch=$( git rev-parse --abbrev-ref HEAD 2> /dev/null || echo 'unknown' )
|
branch=$( git rev-parse --abbrev-ref HEAD 2> /dev/null || echo 'unknown' )
|
||||||
host=$( hostname -f )
|
host=$( hostname )
|
||||||
build_date=$( date +%Y%m%d-%H:%M:%S )
|
build_date=$( date +%Y%m%d-%H:%M:%S )
|
||||||
go_version=$( go version | sed -e 's/^[^0-9.]*\([0-9.]*\).*/\1/' )
|
go_version=$( go version | sed -e 's/^[^0-9.]*\([0-9.]*\).*/\1/' )
|
||||||
|
|
||||||
|
|
242
storage/local/heads.go
Normal file
242
storage/local/heads.go
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
// Copyright 2016 The Prometheus Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/common/model"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/storage/local/codable"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headsFileName = "heads.db"
|
||||||
|
headsTempFileName = "heads.db.tmp"
|
||||||
|
headsFormatVersion = 2
|
||||||
|
headsFormatLegacyVersion = 1 // Can read, but will never write.
|
||||||
|
headsMagicString = "PrometheusHeads"
|
||||||
|
)
|
||||||
|
|
||||||
|
// headsScanner is a scanner to read time series with their heads from a
|
||||||
|
// heads.db file. It follows a similar semantics as the bufio.Scanner.
|
||||||
|
// It is not safe to use a headsScanner concurrently.
|
||||||
|
type headsScanner struct {
|
||||||
|
f *os.File
|
||||||
|
r *bufio.Reader
|
||||||
|
fp model.Fingerprint // Read after each scan() call that has returned true.
|
||||||
|
series *memorySeries // Read after each scan() call that has returned true.
|
||||||
|
version int64 // Read after newHeadsScanner has returned.
|
||||||
|
seriesTotal uint64 // Read after newHeadsScanner has returned.
|
||||||
|
seriesCurrent uint64
|
||||||
|
chunksToPersistTotal int64 // Read after scan() has returned false.
|
||||||
|
err error // Read after scan() has returned false.
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeadsScanner(filename string) *headsScanner {
|
||||||
|
hs := &headsScanner{}
|
||||||
|
defer func() {
|
||||||
|
if hs.f != nil && hs.err != nil {
|
||||||
|
hs.f.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if hs.f, hs.err = os.Open(filename); hs.err != nil {
|
||||||
|
return hs
|
||||||
|
}
|
||||||
|
hs.r = bufio.NewReaderSize(hs.f, fileBufSize)
|
||||||
|
|
||||||
|
buf := make([]byte, len(headsMagicString))
|
||||||
|
if _, hs.err = io.ReadFull(hs.r, buf); hs.err != nil {
|
||||||
|
return hs
|
||||||
|
}
|
||||||
|
magic := string(buf)
|
||||||
|
if magic != headsMagicString {
|
||||||
|
hs.err = fmt.Errorf(
|
||||||
|
"unexpected magic string, want %q, got %q",
|
||||||
|
headsMagicString, magic,
|
||||||
|
)
|
||||||
|
return hs
|
||||||
|
}
|
||||||
|
hs.version, hs.err = binary.ReadVarint(hs.r)
|
||||||
|
if (hs.version != headsFormatVersion && hs.version != headsFormatLegacyVersion) || hs.err != nil {
|
||||||
|
hs.err = fmt.Errorf(
|
||||||
|
"unknown or unreadable heads format version, want %d, got %d, error: %s",
|
||||||
|
headsFormatVersion, hs.version, hs.err,
|
||||||
|
)
|
||||||
|
return hs
|
||||||
|
}
|
||||||
|
if hs.seriesTotal, hs.err = codable.DecodeUint64(hs.r); hs.err != nil {
|
||||||
|
return hs
|
||||||
|
}
|
||||||
|
return hs
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan works like bufio.Scanner.Scan.
|
||||||
|
func (hs *headsScanner) scan() bool {
|
||||||
|
if hs.seriesCurrent == hs.seriesTotal || hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
seriesFlags byte
|
||||||
|
fpAsInt uint64
|
||||||
|
metric codable.Metric
|
||||||
|
persistWatermark int64
|
||||||
|
modTimeNano int64
|
||||||
|
modTime time.Time
|
||||||
|
chunkDescsOffset int64
|
||||||
|
savedFirstTime int64
|
||||||
|
numChunkDescs int64
|
||||||
|
firstTime int64
|
||||||
|
lastTime int64
|
||||||
|
encoding byte
|
||||||
|
)
|
||||||
|
if seriesFlags, hs.err = hs.r.ReadByte(); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
headChunkPersisted := seriesFlags&flagHeadChunkPersisted != 0
|
||||||
|
if fpAsInt, hs.err = codable.DecodeUint64(hs.r); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hs.fp = model.Fingerprint(fpAsInt)
|
||||||
|
|
||||||
|
if hs.err = metric.UnmarshalFromReader(hs.r); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if hs.version != headsFormatLegacyVersion {
|
||||||
|
// persistWatermark only present in v2.
|
||||||
|
persistWatermark, hs.err = binary.ReadVarint(hs.r)
|
||||||
|
if hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
modTimeNano, hs.err = binary.ReadVarint(hs.r)
|
||||||
|
if hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if modTimeNano != -1 {
|
||||||
|
modTime = time.Unix(0, modTimeNano)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chunkDescsOffset, hs.err = binary.ReadVarint(hs.r); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if savedFirstTime, hs.err = binary.ReadVarint(hs.r); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if numChunkDescs, hs.err = binary.ReadVarint(hs.r); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
chunkDescs := make([]*chunkDesc, numChunkDescs)
|
||||||
|
if hs.version == headsFormatLegacyVersion {
|
||||||
|
if headChunkPersisted {
|
||||||
|
persistWatermark = numChunkDescs
|
||||||
|
} else {
|
||||||
|
persistWatermark = numChunkDescs - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headChunkClosed := true // Initial assumption.
|
||||||
|
for i := int64(0); i < numChunkDescs; i++ {
|
||||||
|
if i < persistWatermark {
|
||||||
|
if firstTime, hs.err = binary.ReadVarint(hs.r); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lastTime, hs.err = binary.ReadVarint(hs.r); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
chunkDescs[i] = &chunkDesc{
|
||||||
|
chunkFirstTime: model.Time(firstTime),
|
||||||
|
chunkLastTime: model.Time(lastTime),
|
||||||
|
}
|
||||||
|
numMemChunkDescs.Inc()
|
||||||
|
} else {
|
||||||
|
// Non-persisted chunk.
|
||||||
|
// If there are non-persisted chunks at all, we consider
|
||||||
|
// the head chunk not to be closed yet.
|
||||||
|
headChunkClosed = false
|
||||||
|
if encoding, hs.err = hs.r.ReadByte(); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
chunk := newChunkForEncoding(chunkEncoding(encoding))
|
||||||
|
if hs.err = chunk.unmarshal(hs.r); hs.err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cd := newChunkDesc(chunk, chunk.firstTime())
|
||||||
|
if i < numChunkDescs-1 {
|
||||||
|
// This is NOT the head chunk. So it's a chunk
|
||||||
|
// to be persisted, and we need to populate lastTime.
|
||||||
|
hs.chunksToPersistTotal++
|
||||||
|
cd.maybePopulateLastTime()
|
||||||
|
}
|
||||||
|
chunkDescs[i] = cd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hs.series = &memorySeries{
|
||||||
|
metric: model.Metric(metric),
|
||||||
|
chunkDescs: chunkDescs,
|
||||||
|
persistWatermark: int(persistWatermark),
|
||||||
|
modTime: modTime,
|
||||||
|
chunkDescsOffset: int(chunkDescsOffset),
|
||||||
|
savedFirstTime: model.Time(savedFirstTime),
|
||||||
|
lastTime: chunkDescs[len(chunkDescs)-1].lastTime(),
|
||||||
|
headChunkClosed: headChunkClosed,
|
||||||
|
}
|
||||||
|
hs.seriesCurrent++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// close closes the underlying file if required.
|
||||||
|
func (hs *headsScanner) close() {
|
||||||
|
if hs.f != nil {
|
||||||
|
hs.f.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpHeads writes the metadata of the provided heads file in a human-readable
|
||||||
|
// form.
|
||||||
|
func DumpHeads(filename string, out io.Writer) error {
|
||||||
|
hs := newHeadsScanner(filename)
|
||||||
|
defer hs.close()
|
||||||
|
|
||||||
|
if hs.err == nil {
|
||||||
|
fmt.Fprintf(
|
||||||
|
out,
|
||||||
|
">>> Dumping %d series from heads file %q with format version %d. <<<\n",
|
||||||
|
hs.seriesTotal, filename, hs.version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for hs.scan() {
|
||||||
|
s := hs.series
|
||||||
|
fmt.Fprintf(
|
||||||
|
out,
|
||||||
|
"FP=%v\tMETRIC=%s\tlen(chunkDescs)=%d\tpersistWatermark=%d\tchunkDescOffset=%d\tsavedFirstTime=%v\tlastTime=%v\theadChunkClosed=%t\n",
|
||||||
|
hs.fp, s.metric, len(s.chunkDescs), s.persistWatermark, s.chunkDescsOffset, s.savedFirstTime, s.lastTime, s.headChunkClosed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if hs.err == nil {
|
||||||
|
fmt.Fprintf(
|
||||||
|
out,
|
||||||
|
">>> Dump complete. %d chunks to persist. <<<\n",
|
||||||
|
hs.chunksToPersistTotal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return hs.err
|
||||||
|
}
|
|
@ -47,12 +47,6 @@ const (
|
||||||
seriesTempFileSuffix = ".db.tmp"
|
seriesTempFileSuffix = ".db.tmp"
|
||||||
seriesDirNameLen = 2 // How many bytes of the fingerprint in dir name.
|
seriesDirNameLen = 2 // How many bytes of the fingerprint in dir name.
|
||||||
|
|
||||||
headsFileName = "heads.db"
|
|
||||||
headsTempFileName = "heads.db.tmp"
|
|
||||||
headsFormatVersion = 2
|
|
||||||
headsFormatLegacyVersion = 1 // Can read, but will never write.
|
|
||||||
headsMagicString = "PrometheusHeads"
|
|
||||||
|
|
||||||
mappingsFileName = "mappings.db"
|
mappingsFileName = "mappings.db"
|
||||||
mappingsTempFileName = "mappings.db.tmp"
|
mappingsTempFileName = "mappings.db.tmp"
|
||||||
mappingsFormatVersion = 1
|
mappingsFormatVersion = 1
|
||||||
|
@ -699,190 +693,36 @@ func (p *persistence) checkpointSeriesMapAndHeads(fingerprintToSeries *seriesMap
|
||||||
// start-up while nothing else is running in storage land. This method is
|
// start-up while nothing else is running in storage land. This method is
|
||||||
// utterly goroutine-unsafe.
|
// utterly goroutine-unsafe.
|
||||||
func (p *persistence) loadSeriesMapAndHeads() (sm *seriesMap, chunksToPersist int64, err error) {
|
func (p *persistence) loadSeriesMapAndHeads() (sm *seriesMap, chunksToPersist int64, err error) {
|
||||||
var chunkDescsTotal int64
|
|
||||||
fingerprintToSeries := make(map[model.Fingerprint]*memorySeries)
|
fingerprintToSeries := make(map[model.Fingerprint]*memorySeries)
|
||||||
sm = &seriesMap{m: fingerprintToSeries}
|
sm = &seriesMap{m: fingerprintToSeries}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if sm != nil && p.dirty {
|
if p.dirty {
|
||||||
log.Warn("Persistence layer appears dirty.")
|
log.Warn("Persistence layer appears dirty.")
|
||||||
err = p.recoverFromCrash(fingerprintToSeries)
|
err = p.recoverFromCrash(fingerprintToSeries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sm = nil
|
sm = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
|
||||||
numMemChunkDescs.Add(float64(chunkDescsTotal))
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
f, err := os.Open(p.headsFileName())
|
hs := newHeadsScanner(p.headsFileName())
|
||||||
if os.IsNotExist(err) {
|
defer hs.close()
|
||||||
|
for hs.scan() {
|
||||||
|
fingerprintToSeries[hs.fp] = hs.series
|
||||||
|
}
|
||||||
|
if os.IsNotExist(hs.err) {
|
||||||
return sm, 0, nil
|
return sm, 0, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if hs.err != nil {
|
||||||
log.Warn("Could not open heads file:", err)
|
|
||||||
p.dirty = true
|
p.dirty = true
|
||||||
return
|
log.
|
||||||
|
With("file", p.headsFileName()).
|
||||||
|
With("error", hs.err).
|
||||||
|
Error("Error reading heads file.")
|
||||||
|
return sm, 0, hs.err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
return sm, hs.chunksToPersistTotal, nil
|
||||||
r := bufio.NewReaderSize(f, fileBufSize)
|
|
||||||
|
|
||||||
buf := make([]byte, len(headsMagicString))
|
|
||||||
if _, err := io.ReadFull(r, buf); err != nil {
|
|
||||||
log.Warn("Could not read from heads file:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, 0, nil
|
|
||||||
}
|
|
||||||
magic := string(buf)
|
|
||||||
if magic != headsMagicString {
|
|
||||||
log.Warnf(
|
|
||||||
"unexpected magic string, want %q, got %q",
|
|
||||||
headsMagicString, magic,
|
|
||||||
)
|
|
||||||
p.dirty = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
version, err := binary.ReadVarint(r)
|
|
||||||
if (version != headsFormatVersion && version != headsFormatLegacyVersion) || err != nil {
|
|
||||||
log.Warnf("unknown heads format version, want %d", headsFormatVersion)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, 0, nil
|
|
||||||
}
|
|
||||||
numSeries, err := codable.DecodeUint64(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode number of series:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for ; numSeries > 0; numSeries-- {
|
|
||||||
seriesFlags, err := r.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not read series flags:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
headChunkPersisted := seriesFlags&flagHeadChunkPersisted != 0
|
|
||||||
fp, err := codable.DecodeUint64(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode fingerprint:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
var metric codable.Metric
|
|
||||||
if err := metric.UnmarshalFromReader(r); err != nil {
|
|
||||||
log.Warn("Could not decode metric:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
var persistWatermark int64
|
|
||||||
var modTime time.Time
|
|
||||||
if version != headsFormatLegacyVersion {
|
|
||||||
// persistWatermark only present in v2.
|
|
||||||
persistWatermark, err = binary.ReadVarint(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode persist watermark:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
modTimeNano, err := binary.ReadVarint(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode modification time:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
if modTimeNano != -1 {
|
|
||||||
modTime = time.Unix(0, modTimeNano)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chunkDescsOffset, err := binary.ReadVarint(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode chunk descriptor offset:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
savedFirstTime, err := binary.ReadVarint(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode saved first time:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
numChunkDescs, err := binary.ReadVarint(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode number of chunk descriptors:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
chunkDescs := make([]*chunkDesc, numChunkDescs)
|
|
||||||
if version == headsFormatLegacyVersion {
|
|
||||||
if headChunkPersisted {
|
|
||||||
persistWatermark = numChunkDescs
|
|
||||||
} else {
|
|
||||||
persistWatermark = numChunkDescs - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headChunkClosed := true // Initial assumption.
|
|
||||||
for i := int64(0); i < numChunkDescs; i++ {
|
|
||||||
if i < persistWatermark {
|
|
||||||
firstTime, err := binary.ReadVarint(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode first time:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
lastTime, err := binary.ReadVarint(r)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode last time:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
chunkDescs[i] = &chunkDesc{
|
|
||||||
chunkFirstTime: model.Time(firstTime),
|
|
||||||
chunkLastTime: model.Time(lastTime),
|
|
||||||
}
|
|
||||||
chunkDescsTotal++
|
|
||||||
} else {
|
|
||||||
// Non-persisted chunk.
|
|
||||||
// If there are non-persisted chunks at all, we consider
|
|
||||||
// the head chunk not to be closed yet.
|
|
||||||
headChunkClosed = false
|
|
||||||
encoding, err := r.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Could not decode chunk type:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
chunk := newChunkForEncoding(chunkEncoding(encoding))
|
|
||||||
if err := chunk.unmarshal(r); err != nil {
|
|
||||||
log.Warn("Could not decode chunk:", err)
|
|
||||||
p.dirty = true
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
|
||||||
cd := newChunkDesc(chunk, chunk.firstTime())
|
|
||||||
if i < numChunkDescs-1 {
|
|
||||||
// This is NOT the head chunk. So it's a chunk
|
|
||||||
// to be persisted, and we need to populate lastTime.
|
|
||||||
chunksToPersist++
|
|
||||||
cd.maybePopulateLastTime()
|
|
||||||
}
|
|
||||||
chunkDescs[i] = cd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fingerprintToSeries[model.Fingerprint(fp)] = &memorySeries{
|
|
||||||
metric: model.Metric(metric),
|
|
||||||
chunkDescs: chunkDescs,
|
|
||||||
persistWatermark: int(persistWatermark),
|
|
||||||
modTime: modTime,
|
|
||||||
chunkDescsOffset: int(chunkDescsOffset),
|
|
||||||
savedFirstTime: model.Time(savedFirstTime),
|
|
||||||
lastTime: chunkDescs[len(chunkDescs)-1].lastTime(),
|
|
||||||
headChunkClosed: headChunkClosed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sm, chunksToPersist, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dropAndPersistChunks deletes all chunks from a series file whose last sample
|
// dropAndPersistChunks deletes all chunks from a series file whose last sample
|
||||||
|
|
76
storage/local/storagetool/main.go
Normal file
76
storage/local/storagetool/main.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright 2016 The Prometheus Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/util/cli"
|
||||||
|
"github.com/prometheus/prometheus/version"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/storage/local"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DumpHeadsCmd dumps metadata of a heads.db file.
|
||||||
|
func DumpHeadsCmd(t cli.Term, args ...string) int {
|
||||||
|
if len(args) != 1 {
|
||||||
|
t.Infof("usage: storagetool dump-heads <file>")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if err := local.DumpHeads(args[0], t.Out()); err != nil {
|
||||||
|
t.Errorf(" FAILED: %s", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionInfoTmpl = `
|
||||||
|
prometheus, version {{.version}} (branch: {{.branch}}, revision: {{.revision}})
|
||||||
|
build user: {{.buildUser}}
|
||||||
|
build date: {{.buildDate}}
|
||||||
|
go version: {{.goVersion}}
|
||||||
|
`
|
||||||
|
|
||||||
|
// VersionCmd prints the binaries version information.
|
||||||
|
func VersionCmd(t cli.Term, _ ...string) int {
|
||||||
|
tmpl := template.Must(template.New("version").Parse(versionInfoTmpl))
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.ExecuteTemplate(&buf, "version", version.Map); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(t.Out(), strings.TrimSpace(buf.String()))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cli.NewApp("storagetool")
|
||||||
|
|
||||||
|
app.Register("dump-heads", &cli.Command{
|
||||||
|
Desc: "dump metadata of a heads.db checkpoint file",
|
||||||
|
Run: DumpHeadsCmd,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Register("version", &cli.Command{
|
||||||
|
Desc: "print the version of this binary",
|
||||||
|
Run: VersionCmd,
|
||||||
|
})
|
||||||
|
|
||||||
|
t := cli.BasicTerm(os.Stdout, os.Stderr)
|
||||||
|
os.Exit(app.Run(t, os.Args[1:]...))
|
||||||
|
}
|
|
@ -32,7 +32,8 @@ type Command struct {
|
||||||
type Term interface {
|
type Term interface {
|
||||||
Infof(format string, v ...interface{})
|
Infof(format string, v ...interface{})
|
||||||
Errorf(format string, v ...interface{})
|
Errorf(format string, v ...interface{})
|
||||||
Out(format string)
|
Out() io.Writer
|
||||||
|
Err() io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
type basicTerm struct {
|
type basicTerm struct {
|
||||||
|
@ -52,9 +53,13 @@ func (t *basicTerm) Errorf(format string, v ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Out implements Term.
|
// Out implements Term.
|
||||||
func (t *basicTerm) Out(msg string) {
|
func (t *basicTerm) Out() io.Writer {
|
||||||
fmt.Fprint(t.out, msg)
|
return t.out
|
||||||
fmt.Fprint(t.out, "\n")
|
}
|
||||||
|
|
||||||
|
// Err implements Term.
|
||||||
|
func (t *basicTerm) Err() io.Writer {
|
||||||
|
return t.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// BasicTerm returns a Term writing Infof and Errorf to err and Out to out.
|
// BasicTerm returns a Term writing Infof and Errorf to err and Out to out.
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.17.0rc2
|
0.17.0
|
||||||
|
|
|
@ -159,7 +159,7 @@ func webUiTemplatesGraphHtml() (*asset, error) {
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var _webUiTemplatesStatusHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xcc\x57\xcd\x6e\xdc\x36\x10\xbe\xef\x53\xb0\x44\x8e\xd5\x2e\x10\xa0\x17\x63\x57\x07\x1b\x29\x1c\xc0\x29\xdc\xac\x7d\xe9\x25\xe0\x8a\xb3\x12\x5b\x9a\x14\x48\xca\xb5\xa1\xe8\xdd\x3b\x43\x49\x5e\xfd\x6d\x9a\x34\x68\xeb\xcb\x9a\x43\x0e\xe7\xe7\x9b\x6f\x46\x74\x5d\x4b\x38\x2a\x03\x8c\x17\x20\x24\x6f\x9a\xed\x0f\x49\xc2\x8c\x7a\x62\x49\x92\xd6\x35\x18\xd9\x34\xab\xd5\x49\x2b\xb3\x26\x80\x09\xa8\xb8\x62\x6c\x2b\xd5\x23\xcb\xb4\xf0\x7e\x17\x0f\x04\xaa\xb8\xe4\xa8\x2b\x25\x79\x8a\xe7\xa8\x51\xbc\x65\x4a\xee\xb8\xab\x4c\x50\x0f\xc0\xd3\x8f\xed\x82\xbd\x37\x47\xeb\x1e\x44\x50\xd6\x6c\x37\xc5\xdb\x4e\x3b\x88\x83\x86\xde\x62\x2b\xc4\xdf\x04\xad\x4b\x30\x1e\x64\x27\x1f\xac\x93\xe0\x5e\x44\x1f\x9c\x2a\x5f\xa4\xc2\x3e\x82\xeb\x02\x20\xa3\x07\x2b\x9f\x7b\x89\x64\x77\x12\x48\x2c\xd2\xfb\x92\x62\xda\x6e\x70\x39\x3a\x91\x88\xc0\x7a\x1f\x44\xa8\xfc\xfa\x52\xb9\x50\xac\xef\xef\xae\x10\xa2\x0d\x9e\x9c\xec\x6d\x4e\x06\x71\x7d\x72\x86\x02\x85\x93\xae\x46\x48\x1c\x2a\xa5\xa5\x3a\x65\xcf\xd3\x4b\xda\xf9\x1f\x01\xa9\x6b\x27\x4c\x0e\xec\xcd\x1f\xf0\xfc\x23\x7b\xf3\x28\x74\x05\xec\x62\xc7\xd6\x14\x52\xac\xf3\x39\xe0\x98\xcf\x6c\x09\x58\x5d\xfb\x27\x47\xa8\xc8\x40\x44\x67\x01\xc6\xd6\xec\x97\xb0\xa3\x40\x5a\xba\x7d\x35\x96\x08\xc2\x51\xe5\x95\xeb\x80\xbc\x1a\x8a\x03\x10\x4b\x07\x83\x42\xb6\x5a\x14\x09\xed\xaf\x26\x34\xd5\xe0\x89\xa4\xf8\x67\x66\xa0\x45\x29\x13\x5a\xb3\xde\x56\x54\x6c\x1a\x34\x7e\x7d\xf7\xe1\x66\x6f\x54\x59\x42\x60\xa5\x08\xc5\xad\xc3\x86\x79\x42\x2f\x07\xb7\xe9\xfb\x68\xc9\x63\x10\x2e\x87\x80\x3e\xef\xda\xc5\xc9\xeb\xbf\x54\xfd\x41\xbd\x7f\xb7\x07\xac\x77\x69\xad\xa6\x72\x8f\x12\x6b\xa3\xb9\xc5\x23\x3f\x60\x40\x2c\x3a\x8e\x89\x61\x79\x5b\x5e\x10\x19\x32\x54\x2e\x85\xd9\xf1\x9f\x78\x1f\x33\x7a\xf8\x44\x17\xc8\x3f\x72\x00\xc5\x8e\x1f\xe3\xc2\x2f\xb0\xab\x73\x96\xbe\x33\xb2\xb4\xca\x84\x29\xab\xfa\x73\x8a\x77\xd6\xb9\xfd\xe1\x8d\x38\x80\xf6\xe7\x4f\x7d\x60\xfb\xcc\x89\xf2\xac\x81\x77\xce\x59\x37\x3f\x9c\x46\x4f\x1a\x13\x58\xa6\x4d\x36\x80\x9d\x00\x1f\x81\x7a\x26\x79\x39\xdb\x12\xac\x40\x5a\xed\x38\xf2\xed\xfe\xe3\x0d\xfb\xcc\x72\x6d\x0f\x42\xe3\xba\x69\x08\x60\xda\x5d\xef\xb3\x02\x1e\xb0\xd3\x2e\x36\x9b\x6e\xe7\xda\xfa\x10\x49\x4a\xc2\x2d\x92\x93\x8a\x20\x52\xa4\xe6\xd4\xc3\x20\x4a\x4d\xd8\xf5\xe3\xc0\xc7\x79\x40\xd7\x7f\xad\xc0\x3d\xb3\x49\xf8\x93\xab\x6a\x38\x45\x3a\x03\x8b\x37\x30\x25\x62\x4c\xcf\x96\xe8\x92\xc5\xdf\xa4\x74\xea\x41\xb8\xe7\x48\x9b\xb8\xd3\x34\x94\x77\x3f\x46\xf8\x76\x43\x37\xe7\xf1\x4f\xa7\xc8\xdf\xed\x8f\xe7\xd1\x59\xe8\x27\x91\x0a\x0d\x2e\xb0\xf8\x9b\xd4\xf5\x4b\xd7\x5c\x83\xd0\xd8\x08\x9f\x59\x11\x17\x77\xf6\x8a\xd4\x11\x2d\xe6\x89\xa6\x9f\x94\x91\x2a\x13\xc1\x3a\x16\xe0\x29\x24\x15\x4e\x0b\x97\x09\x0f\x7c\x39\x8f\xb1\xd9\x85\x94\x96\x41\xf8\x67\x29\x65\x95\xf3\xd6\x25\xb1\xd9\xb0\x5d\x99\x14\x41\x24\xc1\xe6\xb9\xc6\x01\x1f\x90\xb2\x41\x95\x9c\x05\x15\x48\xee\x8e\xad\x53\xb9\x32\x42\x27\xdd\xf6\x25\xe0\x37\x0c\x98\x83\x58\x31\x65\xf2\x0b\xca\xe2\x03\x04\xd1\x76\x22\xb1\x74\x31\xd3\xb6\xc4\x91\x65\x71\x76\xb5\xea\x6c\xdd\xfd\xa5\x39\xc2\x19\x57\x06\x61\x34\x19\xf0\x2f\xd3\x6f\xc4\xdc\x48\x41\xdd\x79\xff\x0f\x29\xa8\x3d\x7c\xab\x3f\x7c\x62\x89\x4a\x07\x9e\x1a\x6b\xe0\xdb\xf9\xfd\x9d\x64\xa8\x6b\x75\x7c\x21\x32\x8d\xc6\x76\x32\xae\xdf\xfb\xdf\xc0\xe1\x33\xe0\x17\xc0\xaf\x48\x9f\x58\x5d\x7b\x85\x85\x58\xd0\x47\xae\x8b\xdc\x7e\x67\xaf\xcd\x62\x89\x73\x78\x29\xe7\x73\x4d\x29\x89\x0a\x6e\xda\x76\x7c\xf0\x0c\x18\x98\x3d\x87\xf5\xd7\x66\x31\xfd\x1c\xcc\xef\x8d\xde\x32\x73\x95\xe5\xd7\x0d\x06\xef\x42\x55\x1e\xb5\xc8\xf1\x7d\xb0\x6f\x25\xf6\x33\x89\xaf\xe5\x85\xd8\x61\x19\x63\x7a\x6d\x2f\x45\x5a\xe2\xff\x27\xe9\xaa\x57\xfe\x2b\x00\x00\xff\xff\xb2\x36\x91\x1f\xeb\x0c\x00\x00")
|
var _webUiTemplatesStatusHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xcc\x57\xcd\x6e\xdc\x36\x10\xbe\xef\x53\xb0\x44\x8e\xd5\x2e\x10\xa0\x17\x63\x57\x07\x1b\x29\x1c\xc0\x29\xdc\xac\x7d\xe9\x25\xe0\x8a\xb3\x12\x5b\x9a\x14\x48\xca\xf5\x42\xd1\xbb\x77\x86\x92\xa2\x9f\xd5\xa6\x49\x83\xb6\xb9\xac\x39\xe4\x70\x7e\xbe\xf9\x66\x44\xd7\xb5\x84\xa3\x32\xc0\x78\x01\x42\xf2\xa6\xd9\xfe\x90\x24\xcc\xa8\x17\x96\x24\x69\x5d\x83\x91\x4d\xb3\x5a\x0d\x5a\x99\x35\x01\x4c\x40\xc5\x15\x63\x5b\xa9\x9e\x59\xa6\x85\xf7\xbb\x78\x20\x50\xc5\x25\x47\x5d\x29\xc9\x53\x3c\x47\x8d\xe2\x35\x53\x72\xc7\x5d\x65\x82\x7a\x02\x9e\xbe\x6f\x17\xec\xad\x39\x5a\xf7\x24\x82\xb2\x66\xbb\x29\x5e\x77\xda\x41\x1c\x34\xf4\x16\x5b\x21\xfe\x26\x68\x5d\x82\xf1\x20\x3b\xf9\x60\x9d\x04\xf7\x49\xf4\xc1\xa9\xf2\x93\x54\xd8\x67\x70\x5d\x00\x64\xf4\x60\xe5\xa9\x97\x48\x76\x83\x40\x62\x91\x3e\x96\x14\xd3\x76\x83\xcb\xc9\x89\x44\x04\xd6\xfb\x20\x42\xe5\xd7\xd7\xca\x85\x62\xfd\xf8\x70\x83\x10\x6d\xf0\x64\xb0\xb7\x19\x0c\xe2\x7a\x70\x86\x02\x85\x93\xae\x26\x48\x1c\x2a\xa5\xa5\x1a\xb2\xe7\xe9\x35\xed\xfc\x8f\x80\xd4\xb5\x13\x26\x07\xf6\xea\x0f\x38\xfd\xc8\x5e\x3d\x0b\x5d\x01\xbb\xda\xb1\x35\x85\x14\xeb\x7c\x09\x38\xe6\x33\x5b\x02\x56\xd7\xfe\xc9\x11\x2a\x32\x10\xd1\x59\x80\xb1\x35\xfb\x39\xec\x28\x90\x96\x6e\x5f\x8c\x25\x82\x70\x54\x79\xe5\x3a\x20\x6f\xc6\xe2\x08\xc4\xd2\xc1\xa8\x90\xad\x16\x45\x42\xfb\xab\x19\x4d\x35\x78\x22\x29\xfe\x39\x33\xd0\xa2\x94\x09\xad\x59\x6f\x2b\x2a\x36\x0d\x1a\xbf\x7d\x78\x77\xb7\x37\xaa\x2c\x21\xb0\x52\x84\xe2\xde\x61\xc3\xbc\xa0\x97\x83\xdb\xf4\x7d\xb4\xe4\x31\x08\x97\x43\x40\x9f\x0f\xed\x62\xf0\xfa\x2f\x55\x7f\x54\xef\xdf\xed\x01\xeb\x5d\x5a\xab\xa9\xdc\x93\xc4\xda\x68\xee\xf1\xc8\x8f\x18\x10\x8b\x8e\x63\x62\x5c\xde\x96\x17\x44\x86\x0c\x95\x4b\x61\x76\xfc\x27\xde\xc7\x8c\x1e\x3e\xd0\x05\xf2\x8f\x1c\x40\xb1\xe3\xc7\xb4\xf0\x0b\xec\xea\x9c\xa5\x6f\x8c\x2c\xad\x32\x61\xce\xaa\xfe\x9c\xe2\x3d\xeb\xdc\xfe\xf0\x4e\x1c\x40\xfb\xcb\xa7\x3e\xb0\x7d\xe6\x44\x79\xd1\xc0\x1b\xe7\xac\x3b\x3f\x9c\x47\x4f\x1a\x33\x58\xe6\x4d\x36\x82\x9d\x00\x9f\x80\x7a\x21\x79\x79\xb6\x25\x58\x81\xb4\xda\x71\xe4\xdb\xe3\xfb\x3b\xf6\x91\xe5\xda\x1e\x84\xc6\x75\xd3\x10\xc0\xb4\xbb\xde\x67\x05\x3c\x61\xa7\x5d\x6d\x36\xdd\xce\xad\xf5\x21\x92\x94\x84\x7b\x24\x27\x15\x41\xa4\x48\xcd\xb9\x87\x51\x94\x9a\xb0\xeb\xc7\x81\x8f\xf3\x80\xae\xff\x5a\x81\x3b\xb1\x59\xf8\xb3\xab\x6a\x3c\x45\x3a\x03\x8b\x37\x30\x25\x62\x4c\xcf\x96\xe8\x92\xc5\xdf\xa4\x74\xea\x49\xb8\x53\xa4\x4d\xdc\x69\x1a\xca\xbb\x1f\x23\x7c\xbb\xa1\x9b\xe7\xf1\xcf\xa7\xc8\xdf\xed\x4f\xe7\xd1\x45\xe8\x67\x91\x0a\x0d\x2e\xb0\xf8\x9b\xd4\x35\x5b\xdf\x82\xd0\xd8\x01\x1f\x59\x11\x17\x0f\xf6\x86\xf4\x10\x26\xe6\x89\x9f\x1f\x94\x91\x2a\x13\xc1\x3a\x16\xe0\x25\x24\x15\x8e\x09\x97\x09\x0f\x7c\x39\x81\xce\xde\x42\x12\xcb\x69\xff\xb3\x24\xb2\xca\x79\xeb\x92\xd8\x5e\xd8\xa0\x4c\x8a\x20\x92\x60\xf3\x5c\xe3\x48\x0f\x48\xd2\xa0\x4a\xce\x82\x0a\x24\x77\xc7\xd6\xa9\x5c\x19\xa1\x93\x6e\xfb\x1a\xf0\xab\x05\xcc\x41\xac\x91\x32\xf9\x15\x85\xff\x0e\x82\x68\x7b\x8f\x78\xb9\x98\x62\x5b\xd4\xc8\xab\x38\xad\x5a\x75\xb6\xee\xfe\xd2\xe4\xe0\x8c\x2b\x83\xf8\x99\x0c\xf8\xe7\x09\x37\xe1\x6a\x24\x9d\xee\xbc\xff\x87\xa4\xd3\x1e\xbe\xd6\x1f\x3e\xaa\x44\xa5\x03\x4f\x8d\x35\xf0\xf5\x8c\xfe\x46\x32\xd4\xb5\x3a\x12\xe0\x3e\xb4\x43\x70\xfd\xd6\xff\x06\x0e\xbf\xf8\xbf\x00\x7e\x30\xfa\x8c\xea\xda\x2b\xac\xc0\x58\x11\x69\x2d\x72\xfb\x8d\xfd\x34\x78\x8f\x43\x76\x29\xbd\x4b\x1d\x27\xa9\xea\x6e\xde\x5a\x71\x00\x8e\xec\x5d\xc2\xf3\x4b\xe3\x9e\x0f\xf9\xf3\x7b\x93\x17\xca\xb9\xca\xf2\x9b\x05\xa3\x76\xa1\x2a\x8f\x5a\xe4\xf8\xd5\xdf\xb7\x12\xfb\x99\xc4\xef\xe5\xdd\xd7\xbd\x01\x62\x4c\xdf\xdb\xfb\x8f\x96\xf8\x5f\x47\xba\xea\x95\xff\x0a\x00\x00\xff\xff\xd0\x50\xb7\x0a\xc1\x0c\x00\x00")
|
||||||
|
|
||||||
func webUiTemplatesStatusHtmlBytes() ([]byte, error) {
|
func webUiTemplatesStatusHtmlBytes() ([]byte, error) {
|
||||||
return bindataRead(
|
return bindataRead(
|
||||||
|
@ -174,7 +174,7 @@ func webUiTemplatesStatusHtml() (*asset, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
info := bindataFileInfo{name: "web/ui/templates/status.html", size: 3307, mode: os.FileMode(420), modTime: time.Unix(1455530985, 0)}
|
info := bindataFileInfo{name: "web/ui/templates/status.html", size: 3265, mode: os.FileMode(420), modTime: time.Unix(1456687049, 0)}
|
||||||
a := &asset{bytes: bytes, info: info}
|
a := &asset{bytes: bytes, info: info}
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,8 +55,8 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="alert alert-{{ .Status.Health | healthToClass }} state_indicator text-uppercase">
|
<span class="alert alert-{{ .Health | healthToClass }} state_indicator text-uppercase">
|
||||||
{{.Status.Health}}
|
{{.Health}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -70,11 +70,11 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{if .Status.LastScrape.IsZero}}Never{{else}}{{since .Status.LastScrape}} ago{{end}}
|
{{if .LastScrape.IsZero}}Never{{else}}{{since .LastScrape}} ago{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{if .Status.LastError}}
|
{{if .LastError}}
|
||||||
<span class="alert alert-danger state_indicator">{{.Status.LastError}}</span>
|
<span class="alert alert-danger state_indicator">{{.LastError}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
Loading…
Reference in a new issue