Merge branch 'master' into dev-2.0-rebase

This commit is contained in:
Fabian Reinartz 2017-01-30 17:30:28 +01:00
commit 1d3cdd0d67
35 changed files with 923 additions and 158 deletions

15
.codeclimate.yml Normal file
View file

@ -0,0 +1,15 @@
engines:
gofmt:
enabled: true
golint:
enabled: true
govet:
enabled: true
ratings:
paths:
- "**.go"
exclude_paths:
- "/storage/remote/remote.pb.go"
- vendor/
- web/ui/static/vendor/
- "/web/ui/bindata.go"

View file

@ -1,3 +1,20 @@
## 1.5.0 / 2017-01-23
* [CHANGE] Use lexicographic order to sort alerts by name.
* [FEATURE] Add Joyent Triton discovery.
* [FEATURE] Add scrape targets and alertmanager targets API.
* [FEATURE] Add various persistence related metrics.
* [FEATURE] Add various query engine related metrics.
* [FEATURE] Add ability to limit scrape samples, and related metrics.
* [FEATURE] Add labeldrop and labelkeep relabelling actions.
* [FEATURE] Display current working directory on status-page.
* [ENHANCEMENT] Strictly use ServiceAccount for in cluster configuration on Kubernetes.
* [ENHANCEMENT] Various performance and memory-management improvements.
* [BUGFIX] Fix basic auth for alertmanagers configured via flag.
* [BUGFIX] Don't panic on decoding corrupt data.
* [BUGFIX] Ignore dotfiles in data directory.
* [BUGFIX] Abort on intermediate federation errors.
## 1.4.1 / 2016-11-28
* [BUGFIX] Fix Consul service discovery

View file

@ -4,6 +4,8 @@
[![Docker Repository on Quay](https://quay.io/repository/prometheus/prometheus/status)][quay]
[![Docker Pulls](https://img.shields.io/docker/pulls/prom/prometheus.svg?maxAge=604800)][hub]
[![Go Report Card](https://goreportcard.com/badge/github.com/prometheus/prometheus)](https://goreportcard.com/report/github.com/prometheus/prometheus)
[![Code Climate](https://codeclimate.com/github/prometheus/prometheus/badges/gpa.svg)](https://codeclimate.com/github/prometheus/prometheus)
[![Issue Count](https://codeclimate.com/github/prometheus/prometheus/badges/issue_count.svg)](https://codeclimate.com/github/prometheus/prometheus)
Visit [prometheus.io](https://prometheus.io) for the full documentation,
examples and guides.
@ -82,15 +84,15 @@ The Makefile provides several targets:
* The source code is periodically indexed: [Prometheus Core](http://godoc.org/github.com/prometheus/prometheus).
* You will find a Travis CI configuration in `.travis.yml`.
* All of the core developers are accessible via the [Prometheus Developers Mailinglist](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers) and the `#prometheus` channel on `irc.freenode.net`.
* See the [Community page](https://prometheus.io/community) for how to reach the Prometheus developers and users on various communication channels.
## Contributing
Refer to [CONTRIBUTING.md](CONTRIBUTING.md)
Refer to [CONTRIBUTING.md](https://github.com/prometheus/prometheus/blob/master/CONTRIBUTING.md)
## License
Apache License 2.0, see [LICENSE](LICENSE).
Apache License 2.0, see [LICENSE](https://github.com/prometheus/prometheus/blob/master/LICENSE).
[travis]: https://travis-ci.org/prometheus/prometheus

View file

@ -1 +1 @@
1.4.1
1.5.0

View file

@ -27,6 +27,8 @@ import (
"github.com/asaskevich/govalidator"
"github.com/prometheus/common/log"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/notifier"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/web"
@ -226,6 +228,43 @@ func validateAlertmanagerURL(u string) error {
return nil
}
func parseAlertmanagerURLToConfig(us string) (*config.AlertmanagerConfig, error) {
u, err := url.Parse(us)
if err != nil {
return nil, err
}
acfg := &config.AlertmanagerConfig{
Scheme: u.Scheme,
PathPrefix: u.Path,
Timeout: cfg.notifierTimeout,
ServiceDiscoveryConfig: config.ServiceDiscoveryConfig{
StaticConfigs: []*config.TargetGroup{
{
Targets: []model.LabelSet{
{
model.AddressLabel: model.LabelValue(u.Host),
},
},
},
},
},
}
if u.User != nil {
acfg.HTTPClientConfig = config.HTTPClientConfig{
BasicAuth: &config.BasicAuth{
Username: u.User.Username(),
},
}
if password, isSet := u.User.Password(); isSet {
acfg.HTTPClientConfig.BasicAuth.Password = password
}
}
return acfg, nil
}
var helpTmpl = `
usage: prometheus [<args>]
{{ range $cat, $flags := . }}{{ if ne $cat "." }} == {{ $cat | upper }} =={{ end }}

View file

@ -52,3 +52,46 @@ func TestParse(t *testing.T) {
}
}
}
func TestParseAlertmanagerURLToConfig(t *testing.T) {
tests := []struct {
url string
username string
password string
}{
{
url: "http://alertmanager.company.com",
username: "",
password: "",
},
{
url: "https://user:password@alertmanager.company.com",
username: "user",
password: "password",
},
}
for i, test := range tests {
acfg, err := parseAlertmanagerURLToConfig(test.url)
if err != nil {
t.Errorf("%d. expected alertmanager URL to be valid, got %s", i, err)
}
if acfg.HTTPClientConfig.BasicAuth != nil {
if test.username != acfg.HTTPClientConfig.BasicAuth.Username {
t.Errorf("%d. expected alertmanagerConfig username to be %q, got %q",
i, test.username, acfg.HTTPClientConfig.BasicAuth.Username)
}
if test.password != acfg.HTTPClientConfig.BasicAuth.Password {
t.Errorf("%d. expected alertmanagerConfig password to be %q, got %q", i,
test.password, acfg.HTTPClientConfig.BasicAuth.Username)
}
continue
}
if test.username != "" || test.password != "" {
t.Errorf("%d. expected alertmanagerConfig to have basicAuth filled, but was not", i)
}
}
}

View file

@ -20,7 +20,6 @@ import (
_ "net/http/pprof" // Comment this line to disable pprof endpoint.
"os"
"os/signal"
"runtime"
"runtime/trace"
"syscall"
"time"
@ -58,8 +57,6 @@ var (
func init() {
prometheus.MustRegister(version.NewCollector("prometheus"))
runtime.SetMutexProfileFraction(20)
runtime.SetBlockProfileRate(20)
}
// Main manages the stup and shutdown lifecycle of the entire Prometheus server.

View file

@ -156,6 +156,13 @@ var (
RefreshInterval: model.Duration(5 * time.Minute),
}
// DefaultTritonSDConfig is the default Triton SD configuration.
DefaultTritonSDConfig = TritonSDConfig{
Port: 9163,
RefreshInterval: model.Duration(60 * time.Second),
Version: 1,
}
// DefaultRemoteWriteConfig is the default remote write configuration.
DefaultRemoteWriteConfig = RemoteWriteConfig{
RemoteTimeout: model.Duration(30 * time.Second),
@ -437,6 +444,8 @@ type ServiceDiscoveryConfig struct {
EC2SDConfigs []*EC2SDConfig `yaml:"ec2_sd_configs,omitempty"`
// List of Azure service discovery configurations.
AzureSDConfigs []*AzureSDConfig `yaml:"azure_sd_configs,omitempty"`
// List of Triton service discovery configurations.
TritonSDConfigs []*TritonSDConfig `yaml:"triton_sd_configs,omitempty"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline"`
@ -497,6 +506,8 @@ type ScrapeConfig struct {
MetricsPath string `yaml:"metrics_path,omitempty"`
// The URL scheme with which to fetch metrics from targets.
Scheme string `yaml:"scheme,omitempty"`
// More than this many samples post metric-relabelling will cause the scrape to fail.
SampleLimit uint `yaml:"sample_limit,omitempty"`
// We cannot do proper Go type embedding below as the parser will then parse
// values arbitrarily into the overflow maps of further-down types.
@ -986,6 +997,11 @@ func (c *KubernetesSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) er
if c.BasicAuth != nil && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) {
return fmt.Errorf("at most one of basic_auth, bearer_token & bearer_token_file must be configured")
}
if c.APIServer.URL == nil &&
(c.BasicAuth != nil || c.BearerToken != "" || c.BearerTokenFile != "" ||
c.TLSConfig.CAFile != "" || c.TLSConfig.CertFile != "" || c.TLSConfig.KeyFile != "") {
return fmt.Errorf("to use custom authentication please provide the 'api_server' URL explicitly")
}
return nil
}
@ -1086,6 +1102,42 @@ func (c *AzureSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return checkOverflow(c.XXX, "azure_sd_config")
}
// TritonSDConfig is the configuration for Triton based service discovery.
type TritonSDConfig struct {
Account string `yaml:"account"`
DNSSuffix string `yaml:"dns_suffix"`
Endpoint string `yaml:"endpoint"`
Port int `yaml:"port"`
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
Version int `yaml:"version"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *TritonSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultTritonSDConfig
type plain TritonSDConfig
err := unmarshal((*plain)(c))
if err != nil {
return err
}
if c.Account == "" {
return fmt.Errorf("Triton SD configuration requires an account")
}
if c.DNSSuffix == "" {
return fmt.Errorf("Triton SD configuration requires a dns_suffix")
}
if c.Endpoint == "" {
return fmt.Errorf("Triton SD configuration requires an endpoint")
}
if c.RefreshInterval <= 0 {
return fmt.Errorf("Triton SD configuration requires RefreshInterval to be a positive integer")
}
return checkOverflow(c.XXX, "triton_sd_config")
}
// RelabelAction is the action to be performed on relabeling.
type RelabelAction string

View file

@ -133,6 +133,7 @@ var expectedConf = &Config{
ScrapeInterval: model.Duration(50 * time.Second),
ScrapeTimeout: model.Duration(5 * time.Second),
SampleLimit: 1000,
HTTPClientConfig: HTTPClientConfig{
BasicAuth: &BasicAuth{
@ -413,6 +414,33 @@ var expectedConf = &Config{
},
},
},
{
JobName: "service-triton",
ScrapeInterval: model.Duration(15 * time.Second),
ScrapeTimeout: DefaultGlobalConfig.ScrapeTimeout,
MetricsPath: DefaultScrapeConfig.MetricsPath,
Scheme: DefaultScrapeConfig.Scheme,
ServiceDiscoveryConfig: ServiceDiscoveryConfig{
TritonSDConfigs: []*TritonSDConfig{
{
Account: "testAccount",
DNSSuffix: "triton.example.com",
Endpoint: "triton.example.com",
Port: 9163,
RefreshInterval: model.Duration(60 * time.Second),
Version: 1,
TLSConfig: TLSConfig{
CertFile: "testdata/valid_cert_file",
KeyFile: "testdata/valid_key_file",
},
},
},
},
},
},
AlertingConfig: AlertingConfig{
AlertmanagerConfigs: []*AlertmanagerConfig{

View file

@ -70,6 +70,8 @@ scrape_configs:
scrape_interval: 50s
scrape_timeout: 5s
sample_limit: 1000
metrics_path: /my_path
scheme: https
@ -179,6 +181,18 @@ scrape_configs:
- targets:
- localhost:9090
- job_name: service-triton
triton_sd_configs:
- account: 'testAccount'
dns_suffix: 'triton.example.com'
endpoint: 'triton.example.com'
port: 9163
refresh_interval: 1m
version: 1
tls_config:
cert_file: testdata/valid_cert_file
key_file: testdata/valid_key_file
alerting:
alertmanagers:
- scheme: https

View file

@ -46,7 +46,7 @@
</tr>
<tr>
<td>Checkpoint Duration</td>
<td>{{ template "prom_query_drilldown" (args (printf "prometheus_local_storage_checkpoint_duration_seconds{job='prometheus',instance='%s'}" .Params.instance) "" "humanizeDuration") }}</td>
<td>{{ template "prom_query_drilldown" (args (printf "prometheus_local_storage_checkpoint_last_duration_seconds{job='prometheus',instance='%s'}" .Params.instance) "" "humanizeDuration") }}</td>
</tr>
<tr>

View file

@ -28,6 +28,7 @@ import (
"github.com/prometheus/prometheus/discovery/gce"
"github.com/prometheus/prometheus/discovery/kubernetes"
"github.com/prometheus/prometheus/discovery/marathon"
"github.com/prometheus/prometheus/discovery/triton"
"github.com/prometheus/prometheus/discovery/zookeeper"
"golang.org/x/net/context"
)
@ -106,6 +107,14 @@ func ProvidersFromConfig(cfg config.ServiceDiscoveryConfig) map[string]TargetPro
for i, c := range cfg.AzureSDConfigs {
app("azure", i, azure.NewDiscovery(c))
}
for i, c := range cfg.TritonSDConfigs {
t, err := triton.New(log.With("sd", "triton"), c)
if err != nil {
log.Errorf("Cannot create Triton discovery: %s", err)
continue
}
app("triton", i, t)
}
if len(cfg.StaticConfigs) > 0 {
app("static", 0, NewStaticProvider(cfg.StaticConfigs))
}

View file

@ -82,11 +82,38 @@ func New(l log.Logger, conf *config.KubernetesSDConfig) (*Kubernetes, error) {
err error
)
if conf.APIServer.URL == nil {
// Use the Kubernetes provided pod service account
// as described in https://kubernetes.io/docs/admin/service-accounts-admin/
kcfg, err = rest.InClusterConfig()
if err != nil {
return nil, err
}
// Because the handling of configuration parameters changes
// we should inform the user when their currently configured values
// will be ignored due to precedence of InClusterConfig
l.Info("Using pod service account via in-cluster config")
if conf.TLSConfig.CAFile != "" {
l.Warn("Configured TLS CA file is ignored when using pod service account")
}
if conf.TLSConfig.CertFile != "" || conf.TLSConfig.KeyFile != "" {
l.Warn("Configured TLS client certificate is ignored when using pod service account")
}
if conf.BearerToken != "" {
l.Warn("Configured auth token is ignored when using pod service account")
}
if conf.BasicAuth != nil {
l.Warn("Configured basic authentication credentials are ignored when using pod service account")
}
} else {
kcfg = &rest.Config{
Host: conf.APIServer.String(),
TLSClientConfig: rest.TLSClientConfig{
CAFile: conf.TLSConfig.CAFile,
CertFile: conf.TLSConfig.CertFile,
KeyFile: conf.TLSConfig.KeyFile,
},
Insecure: conf.TLSConfig.InsecureSkipVerify,
}
token := conf.BearerToken
if conf.BearerTokenFile != "" {
bf, err := ioutil.ReadFile(conf.BearerTokenFile)
@ -95,24 +122,15 @@ func New(l log.Logger, conf *config.KubernetesSDConfig) (*Kubernetes, error) {
}
token = string(bf)
}
kcfg.BearerToken = token
kcfg = &rest.Config{
Host: conf.APIServer.String(),
BearerToken: token,
TLSClientConfig: rest.TLSClientConfig{
CAFile: conf.TLSConfig.CAFile,
},
if conf.BasicAuth != nil {
kcfg.Username = conf.BasicAuth.Username
kcfg.Password = conf.BasicAuth.Password
}
}
kcfg.UserAgent = "prometheus/discovery"
if conf.BasicAuth != nil {
kcfg.Username = conf.BasicAuth.Username
kcfg.Password = conf.BasicAuth.Password
}
kcfg.TLSClientConfig.CertFile = conf.TLSConfig.CertFile
kcfg.TLSClientConfig.KeyFile = conf.TLSConfig.KeyFile
kcfg.Insecure = conf.TLSConfig.InsecureSkipVerify
kcfg.UserAgent = "prometheus/discovery"
c, err := kubernetes.NewForConfig(kcfg)
if err != nil {

169
discovery/triton/triton.go Normal file
View file

@ -0,0 +1,169 @@
// Copyright 2017 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 triton
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/util/httputil"
"golang.org/x/net/context"
)
const (
tritonLabel = model.MetaLabelPrefix + "triton_"
tritonLabelMachineId = tritonLabel + "machine_id"
tritonLabelMachineAlias = tritonLabel + "machine_alias"
tritonLabelMachineImage = tritonLabel + "machine_image"
tritonLabelServerId = tritonLabel + "server_id"
namespace = "prometheus"
)
var (
refreshFailuresCount = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "prometheus_sd_triton_refresh_failures_total",
Help: "The number of Triton-SD scrape failures.",
})
refreshDuration = prometheus.NewSummary(
prometheus.SummaryOpts{
Name: "prometheus_sd_triton_refresh_duration_seconds",
Help: "The duration of a Triton-SD refresh in seconds.",
})
)
func init() {
prometheus.MustRegister(refreshFailuresCount)
prometheus.MustRegister(refreshDuration)
}
type DiscoveryResponse struct {
Containers []struct {
ServerUUID string `json:"server_uuid"`
VMAlias string `json:"vm_alias"`
VMImageUUID string `json:"vm_image_uuid"`
VMUUID string `json:"vm_uuid"`
} `json:"containers"`
}
// Discovery periodically performs Triton-SD requests. It implements
// the TargetProvider interface.
type Discovery struct {
client *http.Client
interval time.Duration
logger log.Logger
sdConfig *config.TritonSDConfig
}
// New returns a new Discovery which periodically refreshes its targets.
func New(logger log.Logger, conf *config.TritonSDConfig) (*Discovery, error) {
tls, err := httputil.NewTLSConfig(conf.TLSConfig)
if err != nil {
return nil, err
}
transport := &http.Transport{TLSClientConfig: tls}
client := &http.Client{Transport: transport}
return &Discovery{
client: client,
interval: time.Duration(conf.RefreshInterval),
logger: logger,
sdConfig: conf,
}, nil
}
// Run implements the TargetProvider interface.
func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
defer close(ch)
ticker := time.NewTicker(d.interval)
defer ticker.Stop()
// Get an initial set right away.
tg, err := d.refresh()
if err != nil {
d.logger.With("err", err).Error("Refreshing targets failed")
} else {
ch <- []*config.TargetGroup{tg}
}
for {
select {
case <-ticker.C:
tg, err := d.refresh()
if err != nil {
d.logger.With("err", err).Error("Refreshing targets failed")
} else {
ch <- []*config.TargetGroup{tg}
}
case <-ctx.Done():
return
}
}
}
func (d *Discovery) refresh() (tg *config.TargetGroup, err error) {
t0 := time.Now()
defer func() {
refreshDuration.Observe(time.Since(t0).Seconds())
if err != nil {
refreshFailuresCount.Inc()
}
}()
var endpoint = fmt.Sprintf("https://%s:%d/v%d/discover", d.sdConfig.Endpoint, d.sdConfig.Port, d.sdConfig.Version)
tg = &config.TargetGroup{
Source: endpoint,
}
resp, err := d.client.Get(endpoint)
if err != nil {
return tg, fmt.Errorf("an error occurred when requesting targets from the discovery endpoint. %s", err)
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return tg, fmt.Errorf("an error occurred when reading the response body. %s", err)
}
dr := DiscoveryResponse{}
err = json.Unmarshal(data, &dr)
if err != nil {
return tg, fmt.Errorf("an error occurred unmarshaling the disovery response json. %s", err)
}
for _, container := range dr.Containers {
labels := model.LabelSet{
tritonLabelMachineId: model.LabelValue(container.VMUUID),
tritonLabelMachineAlias: model.LabelValue(container.VMAlias),
tritonLabelMachineImage: model.LabelValue(container.VMImageUUID),
tritonLabelServerId: model.LabelValue(container.ServerUUID),
}
addr := fmt.Sprintf("%s.%s:%d", container.VMUUID, d.sdConfig.DNSSuffix, d.sdConfig.Port)
labels[model.AddressLabel] = model.LabelValue(addr)
tg.Targets = append(tg.Targets, labels)
}
return tg, nil
}

View file

@ -0,0 +1,178 @@
// 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 triton
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"
"github.com/prometheus/common/log"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/config"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
var (
conf = config.TritonSDConfig{
Account: "testAccount",
DNSSuffix: "triton.example.com",
Endpoint: "127.0.0.1",
Port: 443,
Version: 1,
RefreshInterval: 1,
TLSConfig: config.TLSConfig{InsecureSkipVerify: true},
}
badconf = config.TritonSDConfig{
Account: "badTestAccount",
DNSSuffix: "bad.triton.example.com",
Endpoint: "127.0.0.1",
Port: 443,
Version: 1,
RefreshInterval: 1,
TLSConfig: config.TLSConfig{
InsecureSkipVerify: false,
KeyFile: "shouldnotexist.key",
CAFile: "shouldnotexist.ca",
CertFile: "shouldnotexist.cert",
},
}
logger = log.Base()
)
func TestTritonSDNew(t *testing.T) {
td, err := New(logger, &conf)
assert.Nil(t, err)
assert.NotNil(t, td)
assert.NotNil(t, td.client)
assert.NotNil(t, td.interval)
assert.NotNil(t, td.logger)
assert.Equal(t, logger, td.logger, "td.logger equals logger")
assert.NotNil(t, td.sdConfig)
assert.Equal(t, conf.Account, td.sdConfig.Account)
assert.Equal(t, conf.DNSSuffix, td.sdConfig.DNSSuffix)
assert.Equal(t, conf.Endpoint, td.sdConfig.Endpoint)
assert.Equal(t, conf.Port, td.sdConfig.Port)
}
func TestTritonSDNewBadConfig(t *testing.T) {
td, err := New(logger, &badconf)
assert.NotNil(t, err)
assert.Nil(t, td)
}
func TestTritonSDRun(t *testing.T) {
var (
td, err = New(logger, &conf)
ch = make(chan []*config.TargetGroup)
ctx, cancel = context.WithCancel(context.Background())
)
assert.Nil(t, err)
assert.NotNil(t, td)
go td.Run(ctx, ch)
select {
case <-time.After(60 * time.Millisecond):
// Expected.
case tgs := <-ch:
t.Fatalf("Unexpected target groups in triton discovery: %s", tgs)
}
cancel()
}
func TestTritonSDRefreshNoTargets(t *testing.T) {
tgts := testTritonSDRefresh(t, "{\"containers\":[]}")
assert.Nil(t, tgts)
}
func TestTritonSDRefreshMultipleTargets(t *testing.T) {
var (
dstr = `{"containers":[
{
"server_uuid":"44454c4c-5000-104d-8037-b7c04f5a5131",
"vm_alias":"server01",
"vm_image_uuid":"7b27a514-89d7-11e6-bee6-3f96f367bee7",
"vm_uuid":"ad466fbf-46a2-4027-9b64-8d3cdb7e9072"
},
{
"server_uuid":"a5894692-bd32-4ca1-908a-e2dda3c3a5e6",
"vm_alias":"server02",
"vm_image_uuid":"a5894692-bd32-4ca1-908a-e2dda3c3a5e6",
"vm_uuid":"7b27a514-89d7-11e6-bee6-3f96f367bee7"
}]
}`
)
tgts := testTritonSDRefresh(t, dstr)
assert.NotNil(t, tgts)
assert.Equal(t, 2, len(tgts))
}
func TestTritonSDRefreshNoServer(t *testing.T) {
var (
td, err = New(logger, &conf)
)
assert.Nil(t, err)
assert.NotNil(t, td)
tg, rerr := td.refresh()
assert.NotNil(t, rerr)
assert.Contains(t, rerr.Error(), "an error occurred when requesting targets from the discovery endpoint.")
assert.NotNil(t, tg)
assert.Nil(t, tg.Targets)
}
func testTritonSDRefresh(t *testing.T, dstr string) []model.LabelSet {
var (
td, err = New(logger, &conf)
s = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, dstr)
}))
)
defer s.Close()
u, uperr := url.Parse(s.URL)
assert.Nil(t, uperr)
assert.NotNil(t, u)
host, strport, sherr := net.SplitHostPort(u.Host)
assert.Nil(t, sherr)
assert.NotNil(t, host)
assert.NotNil(t, strport)
port, atoierr := strconv.Atoi(strport)
assert.Nil(t, atoierr)
assert.NotNil(t, port)
td.sdConfig.Port = port
assert.Nil(t, err)
assert.NotNil(t, td)
tg, err := td.refresh()
assert.Nil(t, err)
assert.NotNil(t, tg)
return tg.Targets
}

View file

@ -166,7 +166,7 @@ func New(o *Options) *Notifier {
Namespace: namespace,
Subsystem: subsystem,
Name: "dropped_total",
Help: "Total number of alerts dropped due to alert manager missing in configuration.",
Help: "Total number of alerts dropped due to errors when sending to Alertmanager.",
}),
queueLength: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,

View file

@ -22,6 +22,7 @@ import (
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log"
"github.com/prometheus/prometheus/pkg/labels"
"github.com/prometheus/prometheus/pkg/timestamp"
@ -32,12 +33,35 @@ import (
)
const (
namespace = "prometheus"
subsystem = "engine"
// The largest SampleValue that can be converted to an int64 without overflow.
maxInt64 = 9223372036854774784
// The smallest SampleValue that can be converted to an int64 without underflow.
minInt64 = -9223372036854775808
)
var (
currentQueries = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "queries",
Help: "The current number of queries being executed or waiting.",
})
maxConcurrentQueries = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "queries_concurrent_max",
Help: "The max number of concurrent queries.",
})
)
func init() {
prometheus.MustRegister(currentQueries)
prometheus.MustRegister(maxConcurrentQueries)
}
// convertibleToInt64 returns true if v does not over-/underflow an int64.
func convertibleToInt64(v float64) bool {
return v <= maxInt64 && v >= minInt64
@ -142,6 +166,7 @@ func NewEngine(queryable Queryable, o *EngineOptions) *Engine {
if o == nil {
o = DefaultEngineOptions
}
maxConcurrentQueries.Set(float64(o.MaxConcurrentQueries))
return &Engine{
queryable: queryable,
gate: newQueryGate(o.MaxConcurrentQueries),
@ -226,6 +251,9 @@ func (ng *Engine) newTestQuery(f func(context.Context) error) Query {
// At this point per query only one EvalStmt is evaluated. Alert and record
// statements are not handled by the Engine.
func (ng *Engine) exec(ctx context.Context, q *query) (Value, error) {
currentQueries.Inc()
defer currentQueries.Dec()
ctx, cancel := context.WithTimeout(ctx, ng.options.Timeout)
q.cancel = cancel

View file

@ -34,9 +34,10 @@ import (
)
const (
scrapeHealthMetricName = "up"
scrapeDurationMetricName = "scrape_duration_seconds"
scrapeSamplesMetricName = "scrape_samples_scraped"
scrapeHealthMetricName = "up"
scrapeDurationMetricName = "scrape_duration_seconds"
scrapeSamplesMetricName = "scrape_samples_scraped"
samplesPostRelabelMetricName = "scrape_samples_post_metric_relabeling"
)
var (
@ -77,6 +78,12 @@ var (
},
[]string{"scrape_job"},
)
targetScrapeSampleLimit = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "prometheus_target_scrapes_exceeded_sample_limit_total",
Help: "Total number of scrapes that hit the sample limit and were rejected.",
},
)
)
func init() {
@ -85,6 +92,7 @@ func init() {
prometheus.MustRegister(targetReloadIntervalLength)
prometheus.MustRegister(targetSyncIntervalLength)
prometheus.MustRegister(targetScrapePoolSyncsCounter)
prometheus.MustRegister(targetScrapeSampleLimit)
}
// scrapePool manages scrapes for sets of targets.
@ -285,6 +293,15 @@ func (sp *scrapePool) sampleAppender(target *Target) storage.Appender {
if err != nil {
panic(err)
}
// The limit is applied after metrics are potentially dropped via relabeling.
if sp.config.SampleLimit > 0 {
app = &limitAppender{
Appender: app,
limit: int(sp.config.SampleLimit),
}
}
// 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 {
@ -427,6 +444,7 @@ func (sl *scrapeLoop) run(interval, timeout time.Duration, errc chan<- error) {
}
var (
total, added int
start = time.Now()
scrapeCtx, _ = context.WithTimeout(sl.ctx, timeout)
)
@ -438,14 +456,13 @@ func (sl *scrapeLoop) run(interval, timeout time.Duration, errc chan<- error) {
)
}
n := 0
buf := bytes.NewBuffer(getScrapeBuf())
err := sl.scraper.scrape(scrapeCtx, buf)
if err == nil {
b := buf.Bytes()
if n, err = sl.append(b, start); err != nil {
if total, added, err = sl.append(b, start); err != nil {
log.With("err", err).Error("append failed")
}
putScrapeBuf(b)
@ -453,7 +470,7 @@ func (sl *scrapeLoop) run(interval, timeout time.Duration, errc chan<- error) {
errc <- err
}
sl.report(start, time.Since(start), n, err)
sl.report(start, time.Since(start), total, added, err)
last = start
select {
@ -490,7 +507,7 @@ func (s samples) Less(i, j int) bool {
return s[i].t < s[j].t
}
func (sl *scrapeLoop) append(b []byte, ts time.Time) (n int, err error) {
func (sl *scrapeLoop) append(b []byte, ts time.Time) (total, added int, err error) {
var (
app = sl.appender()
p = textparse.New(b)
@ -498,6 +515,8 @@ func (sl *scrapeLoop) append(b []byte, ts time.Time) (n int, err error) {
)
for p.Next() {
total++
t := defTime
met, tp, v := p.At()
if tp != nil {
@ -508,6 +527,7 @@ func (sl *scrapeLoop) append(b []byte, ts time.Time) (n int, err error) {
ref, ok := sl.cache[mets]
if ok {
if err = app.Add(ref, t, v); err == nil {
added++
continue
} else if err != storage.ErrNotFound {
break
@ -517,31 +537,36 @@ func (sl *scrapeLoop) append(b []byte, ts time.Time) (n int, err error) {
if !ok {
var lset labels.Labels
p.Metric(&lset)
ref, err = app.SetSeries(lset)
// TODO(fabxc): also add a dropped-cache?
if err == errSeriesDropped {
continue
}
if err != nil {
break
}
if err = app.Add(ref, t, v); err != nil {
break
}
added++
}
sl.cache[mets] = ref
n++
}
if err == nil {
err = p.Err()
}
if err != nil {
app.Rollback()
return 0, err
return total, 0, err
}
if err := app.Commit(); err != nil {
return 0, err
return total, 0, err
}
return n, nil
return total, added, nil
}
func (sl *scrapeLoop) report(start time.Time, duration time.Duration, scrapedSamples int, err error) error {
func (sl *scrapeLoop) report(start time.Time, duration time.Duration, scraped, appended int, err error) error {
sl.scraper.report(start, duration, err)
ts := timestamp.FromTime(start)
@ -561,7 +586,11 @@ func (sl *scrapeLoop) report(start time.Time, duration time.Duration, scrapedSam
app.Rollback()
return err
}
if err := sl.addReportSample(app, scrapeSamplesMetricName, ts, float64(scrapedSamples)); err != nil {
if err := sl.addReportSample(app, scrapeSamplesMetricName, ts, float64(scraped)); err != nil {
app.Rollback()
return err
}
if err := sl.addReportSample(app, samplesPostRelabelMetricName, ts, float64(appended)); err != nil {
app.Rollback()
return err
}

View file

@ -285,6 +285,7 @@ func TestScrapePoolSampleAppender(t *testing.T) {
}
cfg.HonorLabels = true
cfg.SampleLimit = 100
wrapped = sp.sampleAppender(target)
hl, ok := wrapped.(honorLabelsAppender)
@ -295,7 +296,11 @@ func TestScrapePoolSampleAppender(t *testing.T) {
if !ok {
t.Fatalf("Expected relabelAppender but got %T", hl.Appender)
}
if _, ok := re.Appender.(nopAppender); !ok {
lm, ok := re.Appender.(*limitAppender)
if !ok {
t.Fatalf("Expected limitAppender but got %T", lm.Appender)
}
if _, ok := lm.Appender.(nopAppender); !ok {
t.Fatalf("Expected base appender but got %T", re.Appender)
}
}
@ -305,7 +310,7 @@ func TestScrapeLoopStop(t *testing.T) {
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
// loops are started 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.

View file

@ -227,6 +227,26 @@ 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] }
// limitAppender limits the number of total appended samples in a batch.
type limitAppender struct {
storage.Appender
limit int
i int
}
func (app *limitAppender) Add(ref uint64, t int64, v float64) error {
if app.i+1 > app.limit {
return errors.New("sample limit exceeded")
}
if err := app.Appender.Add(ref, t, v); err != nil {
return fmt.Errorf("sample limit of %d exceeded", app.limit)
}
app.i++
return nil
}
// 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_'.
type ruleLabelsAppender struct {

View file

@ -136,16 +136,16 @@ func (tm *TargetManager) reload() {
}
// Targets returns the targets currently being scraped bucketed by their job name.
func (tm *TargetManager) Targets() []Target {
func (tm *TargetManager) Targets() []*Target {
tm.mtx.RLock()
defer tm.mtx.RUnlock()
targets := []Target{}
targets := []*Target{}
for _, ps := range tm.targetSets {
ps.sp.mtx.RLock()
for _, t := range ps.sp.targets {
targets = append(targets, *t)
targets = append(targets, t)
}
ps.sp.mtx.RUnlock()

View file

@ -31,6 +31,7 @@ type Decoder interface {
Decode(*dto.MetricFamily) error
}
// DecodeOptions contains options used by the Decoder and in sample extraction.
type DecodeOptions struct {
// Timestamp is added to each value from the stream that has no explicit timestamp set.
Timestamp model.Time
@ -142,6 +143,8 @@ func (d *textDecoder) Decode(v *dto.MetricFamily) error {
return nil
}
// SampleDecoder wraps a Decoder to extract samples from the metric families
// decoded by the wrapped Decoder.
type SampleDecoder struct {
Dec Decoder
Opts *DecodeOptions
@ -149,37 +152,51 @@ type SampleDecoder struct {
f dto.MetricFamily
}
// Decode calls the Decode method of the wrapped Decoder and then extracts the
// samples from the decoded MetricFamily into the provided model.Vector.
func (sd *SampleDecoder) Decode(s *model.Vector) error {
if err := sd.Dec.Decode(&sd.f); err != nil {
err := sd.Dec.Decode(&sd.f)
if err != nil {
return err
}
*s = extractSamples(&sd.f, sd.Opts)
return nil
*s, err = extractSamples(&sd.f, sd.Opts)
return err
}
// Extract samples builds a slice of samples from the provided metric families.
func ExtractSamples(o *DecodeOptions, fams ...*dto.MetricFamily) model.Vector {
var all model.Vector
// ExtractSamples builds a slice of samples from the provided metric
// families. If an error occurs during sample extraction, it continues to
// extract from the remaining metric families. The returned error is the last
// error that has occured.
func ExtractSamples(o *DecodeOptions, fams ...*dto.MetricFamily) (model.Vector, error) {
var (
all model.Vector
lastErr error
)
for _, f := range fams {
all = append(all, extractSamples(f, o)...)
some, err := extractSamples(f, o)
if err != nil {
lastErr = err
continue
}
all = append(all, some...)
}
return all
return all, lastErr
}
func extractSamples(f *dto.MetricFamily, o *DecodeOptions) model.Vector {
func extractSamples(f *dto.MetricFamily, o *DecodeOptions) (model.Vector, error) {
switch f.GetType() {
case dto.MetricType_COUNTER:
return extractCounter(o, f)
return extractCounter(o, f), nil
case dto.MetricType_GAUGE:
return extractGauge(o, f)
return extractGauge(o, f), nil
case dto.MetricType_SUMMARY:
return extractSummary(o, f)
return extractSummary(o, f), nil
case dto.MetricType_UNTYPED:
return extractUntyped(o, f)
return extractUntyped(o, f), nil
case dto.MetricType_HISTOGRAM:
return extractHistogram(o, f)
return extractHistogram(o, f), nil
}
panic("expfmt.extractSamples: unknown metric family type")
return nil, fmt.Errorf("expfmt.extractSamples: unknown metric family type %v", f.GetType())
}
func extractCounter(o *DecodeOptions, f *dto.MetricFamily) model.Vector {

View file

@ -11,14 +11,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// A package for reading and writing Prometheus metrics.
// Package expfmt contains tools for reading and writing Prometheus metrics.
package expfmt
// Format specifies the HTTP content type of the different wire protocols.
type Format string
// Constants to assemble the Content-Type values for the different wire protocols.
const (
TextVersion = "0.0.4"
TextVersion = "0.0.4"
ProtoType = `application/vnd.google.protobuf`
ProtoProtocol = `io.prometheus.client.MetricFamily`
ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";"

View file

@ -23,6 +23,8 @@ import (
"github.com/Sirupsen/logrus"
)
var _ logrus.Formatter = (*syslogger)(nil)
func init() {
setSyslogFormatter = func(appname, local string) error {
if appname == "" {
@ -43,7 +45,7 @@ func init() {
}
}
var ceeTag = []byte("@cee:")
var prefixTag []byte
type syslogger struct {
wrap logrus.Formatter
@ -56,6 +58,11 @@ func newSyslogger(appname string, facility string, fmter logrus.Formatter) (*sys
return nil, err
}
out, err := syslog.New(priority, appname)
_, isJSON := fmter.(*logrus.JSONFormatter)
if isJSON {
// add cee tag to json formatted syslogs
prefixTag = []byte("@cee:")
}
return &syslogger{
out: out,
wrap: fmter,
@ -92,7 +99,7 @@ func (s *syslogger) Format(e *logrus.Entry) ([]byte, error) {
}
// only append tag to data sent to syslog (line), not to what
// is returned
line := string(append(ceeTag, data...))
line := string(append(prefixTag, data...))
switch e.Level {
case logrus.PanicLevel:

View file

@ -80,14 +80,18 @@ const (
QuantileLabel = "quantile"
)
// LabelNameRE is a regular expression matching valid label names.
// LabelNameRE is a regular expression matching valid label names. Note that the
// IsValid method of LabelName performs the same check but faster than a match
// with this regular expression.
var LabelNameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$")
// A LabelName is a key for a LabelSet or Metric. It has a value associated
// therewith.
type LabelName string
// IsValid is true iff the label name matches the pattern of LabelNameRE.
// IsValid is true iff the label name matches the pattern of LabelNameRE. This
// method, however, does not use LabelNameRE for the check but a much faster
// hardcoded implementation.
func (ln LabelName) IsValid() bool {
if len(ln) == 0 {
return false
@ -106,7 +110,7 @@ func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error {
if err := unmarshal(&s); err != nil {
return err
}
if !LabelNameRE.MatchString(s) {
if !LabelName(s).IsValid() {
return fmt.Errorf("%q is not a valid label name", s)
}
*ln = LabelName(s)
@ -119,7 +123,7 @@ func (ln *LabelName) UnmarshalJSON(b []byte) error {
if err := json.Unmarshal(b, &s); err != nil {
return err
}
if !LabelNameRE.MatchString(s) {
if !LabelName(s).IsValid() {
return fmt.Errorf("%q is not a valid label name", s)
}
*ln = LabelName(s)

View file

@ -160,7 +160,7 @@ func (l *LabelSet) UnmarshalJSON(b []byte) error {
// LabelName as a string and does not call its UnmarshalJSON method.
// Thus, we have to replicate the behavior here.
for ln := range m {
if !LabelNameRE.MatchString(string(ln)) {
if !ln.IsValid() {
return fmt.Errorf("%q is not a valid label name", ln)
}
}

View file

@ -21,8 +21,11 @@ import (
)
var (
separator = []byte{0}
MetricNameRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_:]*$`)
separator = []byte{0}
// MetricNameRE is a regular expression matching valid metric
// names. Note that the IsValidMetricName function performs the same
// check but faster than a match with this regular expression.
MetricNameRE = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`)
)
// A Metric is similar to a LabelSet, but the key difference is that a Metric is
@ -41,7 +44,7 @@ func (m Metric) Before(o Metric) bool {
// Clone returns a copy of the Metric.
func (m Metric) Clone() Metric {
clone := Metric{}
clone := make(Metric, len(m))
for k, v := range m {
clone[k] = v
}
@ -85,6 +88,8 @@ func (m Metric) FastFingerprint() Fingerprint {
}
// IsValidMetricName returns true iff name matches the pattern of MetricNameRE.
// This function, however, does not use MetricNameRE for the check but a much
// faster hardcoded implementation.
func IsValidMetricName(n LabelValue) bool {
if len(n) == 0 {
return false

View file

@ -33,18 +33,19 @@ func WithParam(ctx context.Context, p, v string) context.Context {
return context.WithValue(ctx, param(p), v)
}
type contextFn func(r *http.Request) (context.Context, error)
// ContextFunc returns a new context for a request.
type ContextFunc func(r *http.Request) (context.Context, error)
// Router wraps httprouter.Router and adds support for prefixed sub-routers
// and per-request context injections.
type Router struct {
rtr *httprouter.Router
prefix string
ctxFn contextFn
ctxFn ContextFunc
}
// New returns a new Router.
func New(ctxFn contextFn) *Router {
func New(ctxFn ContextFunc) *Router {
if ctxFn == nil {
ctxFn = func(r *http.Request) (context.Context, error) {
return context.Background(), nil

32
vendor/vendor.json vendored
View file

@ -516,40 +516,40 @@
"revisionTime": "2015-02-12T10:17:44Z"
},
{
"checksumSHA1": "mHyjbJ3BWOfUV6q9f5PBt0gaY1k=",
"checksumSHA1": "jG8qYuDUuaZeflt4JxBBdyQBsXw=",
"path": "github.com/prometheus/common/expfmt",
"revision": "85637ea67b04b5c3bb25e671dacded2977f8f9f6",
"revisionTime": "2016-10-02T21:02:34Z"
"revision": "dd2f054febf4a6c00f2343686efb775948a8bff4",
"revisionTime": "2017-01-08T23:12:12Z"
},
{
"checksumSHA1": "GWlM3d2vPYyNATtTFgftS10/A9w=",
"path": "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg",
"revision": "85637ea67b04b5c3bb25e671dacded2977f8f9f6",
"revisionTime": "2016-10-02T21:02:34Z"
"revision": "dd2f054febf4a6c00f2343686efb775948a8bff4",
"revisionTime": "2017-01-08T23:12:12Z"
},
{
"checksumSHA1": "UU6hIfhVjnAYDADQEfE/3T7Ddm8=",
"checksumSHA1": "ZA4MLHNAP905WiAOLy4BBzmcuxM=",
"path": "github.com/prometheus/common/log",
"revision": "85637ea67b04b5c3bb25e671dacded2977f8f9f6",
"revisionTime": "2016-10-02T21:02:34Z"
"revision": "dd2f054febf4a6c00f2343686efb775948a8bff4",
"revisionTime": "2017-01-08T23:12:12Z"
},
{
"checksumSHA1": "nFie+rxcX5WdIv1diZ+fu3aj6lE=",
"checksumSHA1": "vopCLXHzYm+3l5fPKOf4/fQwrCM=",
"path": "github.com/prometheus/common/model",
"revision": "85637ea67b04b5c3bb25e671dacded2977f8f9f6",
"revisionTime": "2016-10-02T21:02:34Z"
"revision": "dd2f054febf4a6c00f2343686efb775948a8bff4",
"revisionTime": "2017-01-08T23:12:12Z"
},
{
"checksumSHA1": "QQKJYoGcY10nIHxhBEHwjwUZQzk=",
"checksumSHA1": "ZbbESWBHHcPUJ/A5yrzKhTHuPc8=",
"path": "github.com/prometheus/common/route",
"revision": "85637ea67b04b5c3bb25e671dacded2977f8f9f6",
"revisionTime": "2016-10-02T21:02:34Z"
"revision": "dd2f054febf4a6c00f2343686efb775948a8bff4",
"revisionTime": "2017-01-08T23:12:12Z"
},
{
"checksumSHA1": "91KYK0SpvkaMJJA2+BcxbVnyRO0=",
"path": "github.com/prometheus/common/version",
"revision": "85637ea67b04b5c3bb25e671dacded2977f8f9f6",
"revisionTime": "2016-10-02T21:02:34Z"
"revision": "dd2f054febf4a6c00f2343686efb775948a8bff4",
"revisionTime": "2017-01-08T23:12:12Z"
},
{
"checksumSHA1": "W218eJZPXJG783fUr/z6IaAZyes=",

View file

@ -69,7 +69,11 @@ func (e *apiError) Error() string {
}
type targetRetriever interface {
Targets() []retrieval.Target
Targets() []*retrieval.Target
}
type alertmanagerRetriever interface {
Alertmanagers() []string
}
type response struct {
@ -94,20 +98,22 @@ type API struct {
Storage storage.Storage
QueryEngine *promql.Engine
targetRetriever targetRetriever
targetRetriever targetRetriever
alertmanagerRetriever alertmanagerRetriever
context func(r *http.Request) context.Context
now func() time.Time
}
// NewAPI returns an initialized API type.
func NewAPI(qe *promql.Engine, st storage.Storage, tr targetRetriever) *API {
func NewAPI(qe *promql.Engine, st storage.Storage, tr targetRetriever, ar alertmanagerRetriever) *API {
return &API{
QueryEngine: qe,
Storage: st,
targetRetriever: tr,
context: route.Context,
now: time.Now,
QueryEngine: qe,
Storage: st,
targetRetriever: tr,
alertmanagerRetriever: ar,
context: route.Context,
now: time.Now,
}
}
@ -140,6 +146,7 @@ func (api *API) Register(r *route.Router) {
r.Del("/series", instr("drop_series", api.dropSeries))
r.Get("/targets", instr("targets", api.targets))
r.Get("/alertmanagers", instr("alertmanagers", api.alertmanagers))
}
type queryData struct {
@ -360,16 +367,20 @@ type Target struct {
// Any labels that are added to this target and its metrics.
Labels map[string]string `json:"labels"`
ScrapeUrl string `json:"scrapeUrl"`
ScrapeURL string `json:"scrapeUrl"`
LastError string `json:"lastError"`
LastScrape time.Time `json:"lastScrape"`
Health retrieval.TargetHealth `json:"health"`
}
type TargetDiscovery struct {
ActiveTargets []*Target `json:"activeTargets"`
}
func (api *API) targets(r *http.Request) (interface{}, *apiError) {
targets := api.targetRetriever.Targets()
res := make([]*Target, len(targets))
res := &TargetDiscovery{ActiveTargets: make([]*Target, len(targets))}
for i, t := range targets {
lastErrStr := ""
@ -378,10 +389,10 @@ func (api *API) targets(r *http.Request) (interface{}, *apiError) {
lastErrStr = lastErr.Error()
}
res[i] = &Target{
res.ActiveTargets[i] = &Target{
DiscoveredLabels: t.DiscoveredLabels().Map(),
Labels: t.Labels().Map(),
ScrapeUrl: t.URL().String(),
ScrapeURL: t.URL().String(),
LastError: lastErrStr,
LastScrape: t.LastScrape(),
Health: t.Health(),
@ -391,6 +402,25 @@ func (api *API) targets(r *http.Request) (interface{}, *apiError) {
return res, nil
}
type AlertmanagerDiscovery struct {
ActiveAlertmanagers []*AlertmanagerTarget `json:"activeAlertmanagers"`
}
type AlertmanagerTarget struct {
URL string `json:"url"`
}
func (api *API) alertmanagers(r *http.Request) (interface{}, *apiError) {
urls := api.alertmanagerRetriever.Alertmanagers()
ams := &AlertmanagerDiscovery{ActiveAlertmanagers: make([]*AlertmanagerTarget, len(urls))}
for i := range urls {
ams.ActiveAlertmanagers[i] = &AlertmanagerTarget{URL: urls[i]}
}
return ams, nil
}
func respond(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

View file

@ -35,9 +35,15 @@ import (
"github.com/prometheus/prometheus/retrieval"
)
type targetRetrieverFunc func() []retrieval.Target
type targetRetrieverFunc func() []*retrieval.Target
func (f targetRetrieverFunc) Targets() []retrieval.Target {
func (f targetRetrieverFunc) Targets() []*retrieval.Target {
return f()
}
type alertmanagerRetrieverFunc func() []string
func (f alertmanagerRetrieverFunc) Alertmanagers() []string {
return f()
}
@ -59,9 +65,9 @@ func TestEndpoints(t *testing.T) {
now := time.Now()
tr := targetRetrieverFunc(func() []retrieval.Target {
return []retrieval.Target{
*retrieval.NewTarget(
tr := targetRetrieverFunc(func() []*retrieval.Target {
return []*retrieval.Target{
retrieval.NewTarget(
labels.FromMap(map[string]string{
model.SchemeLabel: "http",
model.AddressLabel: "example.com:8080",
@ -73,11 +79,16 @@ func TestEndpoints(t *testing.T) {
}
})
ar := alertmanagerRetrieverFunc(func() []string {
return []string{"http://alertmanager.example.com:8080/api/v1/alerts"}
})
api := &API{
Storage: suite.Storage(),
QueryEngine: suite.QueryEngine(),
targetRetriever: tr,
now: func() time.Time { return now },
Storage: suite.Storage(),
QueryEngine: suite.QueryEngine(),
targetRetriever: tr,
alertmanagerRetriever: ar,
now: func() time.Time { return now },
}
start := time.Unix(0, 0)

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -3,6 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Prometheus Time Series Collection and Processing Server</title>
<link rel="shortcut icon" href="{{ pathPrefix }}/static/img/favicon.ico">
<script src="{{ pathPrefix }}/static/vendor/js/jquery.min.js"></script>
<script src="{{ pathPrefix }}/static/vendor/bootstrap-3.3.1/js/bootstrap.min.js"></script>

View file

@ -155,7 +155,7 @@ func New(o *Options) *Handler {
storage: o.Storage,
notifier: o.Notifier,
apiV1: api_v1.NewAPI(o.QueryEngine, o.Storage, o.TargetManager),
apiV1: api_v1.NewAPI(o.QueryEngine, o.Storage, o.TargetManager, o.Notifier),
now: model.Now,
}
@ -274,7 +274,7 @@ func (h *Handler) Run() {
func (h *Handler) alerts(w http.ResponseWriter, r *http.Request) {
alerts := h.ruleManager.AlertingRules()
alertsSorter := byAlertStateSorter{alerts: alerts}
alertsSorter := byAlertStateAndNameSorter{alerts: alerts}
sort.Sort(alertsSorter)
alertStatus := AlertStatus{
@ -373,14 +373,14 @@ func (h *Handler) rules(w http.ResponseWriter, r *http.Request) {
func (h *Handler) targets(w http.ResponseWriter, r *http.Request) {
// Bucket targets by job label
tps := map[string][]retrieval.Target{}
tps := map[string][]*retrieval.Target{}
for _, t := range h.targetManager.Targets() {
job := t.Labels().Get(model.JobLabel)
tps[job] = append(tps[job], t)
}
h.executeTemplate(w, "targets.html", struct {
TargetPools map[string][]retrieval.Target
TargetPools map[string][]*retrieval.Target
}{
TargetPools: tps,
})
@ -541,18 +541,20 @@ type AlertStatus struct {
AlertStateToRowClass map[rules.AlertState]string
}
type byAlertStateSorter struct {
type byAlertStateAndNameSorter struct {
alerts []*rules.AlertingRule
}
func (s byAlertStateSorter) Len() int {
func (s byAlertStateAndNameSorter) Len() int {
return len(s.alerts)
}
func (s byAlertStateSorter) Less(i, j int) bool {
return s.alerts[i].State() > s.alerts[j].State()
func (s byAlertStateAndNameSorter) Less(i, j int) bool {
return s.alerts[i].State() > s.alerts[j].State() ||
(s.alerts[i].State() == s.alerts[j].State() &&
s.alerts[i].Name() < s.alerts[j].Name())
}
func (s byAlertStateSorter) Swap(i, j int) {
func (s byAlertStateAndNameSorter) Swap(i, j int) {
s.alerts[i], s.alerts[j] = s.alerts[j], s.alerts[i]
}