mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-24 05:04:05 -08:00
Add console and alert templates with access to all data.
Move rulemanager to it's own package to break cicrular dependency. Make NewTestTieredStorage available to tests, remove duplication. Change-Id: I33b321245a44aa727bfc3614a7c9ae5005b34e03
This commit is contained in:
parent
16ca35c07e
commit
e041c0cd46
11
main.go
11
main.go
|
@ -29,7 +29,7 @@ import (
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/notification"
|
"github.com/prometheus/prometheus/notification"
|
||||||
"github.com/prometheus/prometheus/retrieval"
|
"github.com/prometheus/prometheus/retrieval"
|
||||||
"github.com/prometheus/prometheus/rules"
|
"github.com/prometheus/prometheus/rules/manager"
|
||||||
"github.com/prometheus/prometheus/storage/metric/tiered"
|
"github.com/prometheus/prometheus/storage/metric/tiered"
|
||||||
"github.com/prometheus/prometheus/storage/remote"
|
"github.com/prometheus/prometheus/storage/remote"
|
||||||
"github.com/prometheus/prometheus/storage/remote/opentsdb"
|
"github.com/prometheus/prometheus/storage/remote/opentsdb"
|
||||||
|
@ -82,7 +82,7 @@ type prometheus struct {
|
||||||
|
|
||||||
unwrittenSamples chan *extraction.Result
|
unwrittenSamples chan *extraction.Result
|
||||||
|
|
||||||
ruleManager rules.RuleManager
|
ruleManager manager.RuleManager
|
||||||
targetManager retrieval.TargetManager
|
targetManager retrieval.TargetManager
|
||||||
notifications chan notification.NotificationReqs
|
notifications chan notification.NotificationReqs
|
||||||
storage *tiered.TieredStorage
|
storage *tiered.TieredStorage
|
||||||
|
@ -272,7 +272,7 @@ func main() {
|
||||||
notifications := make(chan notification.NotificationReqs, *notificationQueueCapacity)
|
notifications := make(chan notification.NotificationReqs, *notificationQueueCapacity)
|
||||||
|
|
||||||
// Queue depth will need to be exposed
|
// Queue depth will need to be exposed
|
||||||
ruleManager := rules.NewRuleManager(&rules.RuleManagerOptions{
|
ruleManager := manager.NewRuleManager(&manager.RuleManagerOptions{
|
||||||
Results: unwrittenSamples,
|
Results: unwrittenSamples,
|
||||||
Notifications: notifications,
|
Notifications: notifications,
|
||||||
EvaluationInterval: conf.EvaluationInterval(),
|
EvaluationInterval: conf.EvaluationInterval(),
|
||||||
|
@ -306,6 +306,10 @@ func main() {
|
||||||
RuleManager: ruleManager,
|
RuleManager: ruleManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consolesHandler := &web.ConsolesHandler{
|
||||||
|
Storage: ts,
|
||||||
|
}
|
||||||
|
|
||||||
databasesHandler := &web.DatabasesHandler{
|
databasesHandler := &web.DatabasesHandler{
|
||||||
Provider: ts.DiskStorage,
|
Provider: ts.DiskStorage,
|
||||||
RefreshInterval: 5 * time.Minute,
|
RefreshInterval: 5 * time.Minute,
|
||||||
|
@ -341,6 +345,7 @@ func main() {
|
||||||
StatusHandler: prometheusStatus,
|
StatusHandler: prometheusStatus,
|
||||||
MetricsHandler: metricsService,
|
MetricsHandler: metricsService,
|
||||||
DatabasesHandler: databasesHandler,
|
DatabasesHandler: databasesHandler,
|
||||||
|
ConsolesHandler: consolesHandler,
|
||||||
AlertsHandler: alertsHandler,
|
AlertsHandler: alertsHandler,
|
||||||
|
|
||||||
QuitDelegate: prometheus.Close,
|
QuitDelegate: prometheus.Close,
|
||||||
|
|
|
@ -20,7 +20,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"text/template"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
|
@ -84,49 +83,13 @@ func NewNotificationHandler(alertmanagerUrl string, notificationReqs <-chan Noti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interpolate alert information into summary/description templates.
|
|
||||||
func interpolateMessage(msg string, labels clientmodel.LabelSet, value clientmodel.SampleValue) string {
|
|
||||||
t := template.New("message")
|
|
||||||
|
|
||||||
// Inject some convenience variables that are easier to remember for users
|
|
||||||
// who are not used to Go's templating system.
|
|
||||||
defs :=
|
|
||||||
"{{$labels := .Labels}}" +
|
|
||||||
"{{$value := .Value}}"
|
|
||||||
|
|
||||||
if _, err := t.Parse(defs + msg); err != nil {
|
|
||||||
glog.Warning("Error parsing template: ", err)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
l := map[string]string{}
|
|
||||||
for k, v := range labels {
|
|
||||||
l[string(k)] = string(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmplData := struct {
|
|
||||||
Labels map[string]string
|
|
||||||
Value clientmodel.SampleValue
|
|
||||||
}{
|
|
||||||
Labels: l,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := t.Execute(&buf, &tmplData); err != nil {
|
|
||||||
glog.Warning("Error executing template: ", err)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a list of notifications to the configured alert manager.
|
// Send a list of notifications to the configured alert manager.
|
||||||
func (n *NotificationHandler) sendNotifications(reqs NotificationReqs) error {
|
func (n *NotificationHandler) sendNotifications(reqs NotificationReqs) error {
|
||||||
alerts := make([]map[string]interface{}, 0, len(reqs))
|
alerts := make([]map[string]interface{}, 0, len(reqs))
|
||||||
for _, req := range reqs {
|
for _, req := range reqs {
|
||||||
alerts = append(alerts, map[string]interface{}{
|
alerts = append(alerts, map[string]interface{}{
|
||||||
"Summary": interpolateMessage(req.Summary, req.Labels, req.Value),
|
"Summary": req.Summary,
|
||||||
"Description": interpolateMessage(req.Description, req.Labels, req.Value),
|
"Description": req.Description,
|
||||||
"Labels": req.Labels,
|
"Labels": req.Labels,
|
||||||
"Payload": map[string]interface{}{
|
"Payload": map[string]interface{}{
|
||||||
"Value": req.Value,
|
"Value": req.Value,
|
||||||
|
@ -140,6 +103,7 @@ func (n *NotificationHandler) sendNotifications(reqs NotificationReqs) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
glog.V(1).Infoln("Sending notifications to alertmanager:", string(buf))
|
||||||
resp, err := n.httpClient.Post(
|
resp, err := n.httpClient.Post(
|
||||||
n.alertmanagerUrl+alertmanagerApiEventsPath,
|
n.alertmanagerUrl+alertmanagerApiEventsPath,
|
||||||
contentTypeJson,
|
contentTypeJson,
|
||||||
|
|
|
@ -80,27 +80,9 @@ func TestNotificationHandler(t *testing.T) {
|
||||||
scenarios := []testNotificationScenario{
|
scenarios := []testNotificationScenario{
|
||||||
{
|
{
|
||||||
// Correct message.
|
// Correct message.
|
||||||
summary: "{{$labels.instance}} = {{$value}}",
|
summary: "Summary",
|
||||||
description: "The alert value for {{$labels.instance}} is {{$value}}",
|
description: "Description",
|
||||||
message: `[{"Description":"The alert value for testinstance is 0.3333333333333333","Labels":{"instance":"testinstance"},"Payload":{"ActiveSince":"0001-01-01T00:00:00Z","AlertingRule":"Test rule string","GeneratorUrl":"prometheus_url","Value":"0.333333"},"Summary":"testinstance = 0.3333333333333333"}]`,
|
message: `[{"Description":"Description","Labels":{"instance":"testinstance"},"Payload":{"ActiveSince":"0001-01-01T00:00:00Z","AlertingRule":"Test rule string","GeneratorUrl":"prometheus_url","Value":"0.333333"},"Summary":"Summary"}]`,
|
||||||
},
|
|
||||||
{
|
|
||||||
// Bad message referring to unknown label.
|
|
||||||
summary: "{{$labels.badlabel}} = {{$value}}",
|
|
||||||
description: "The alert value for {{$labels.badlabel}} is {{$value}}",
|
|
||||||
message: `[{"Description":"The alert value for \u003cno value\u003e is 0.3333333333333333","Labels":{"instance":"testinstance"},"Payload":{"ActiveSince":"0001-01-01T00:00:00Z","AlertingRule":"Test rule string","GeneratorUrl":"prometheus_url","Value":"0.333333"},"Summary":"\u003cno value\u003e = 0.3333333333333333"}]`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Bad message referring to unknown variable.
|
|
||||||
summary: "{{$labels.instance}} = {{$badvar}}",
|
|
||||||
description: "The alert value for {{$labels.instance}} is {{$badvar}}",
|
|
||||||
message: `[{"Description":"The alert value for {{$labels.instance}} is {{$badvar}}","Labels":{"instance":"testinstance"},"Payload":{"ActiveSince":"0001-01-01T00:00:00Z","AlertingRule":"Test rule string","GeneratorUrl":"prometheus_url","Value":"0.333333"},"Summary":"{{$labels.instance}} = {{$badvar}}"}]`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Bad message referring to unknown struct field.
|
|
||||||
summary: "{{$labels.instance}} = {{.Val}}",
|
|
||||||
description: "The alert value for {{$labels.instance}} is {{.Val}}",
|
|
||||||
message: `[{"Description":"The alert value for {{$labels.instance}} is {{.Val}}","Labels":{"instance":"testinstance"},"Payload":{"ActiveSince":"0001-01-01T00:00:00Z","AlertingRule":"Test rule string","GeneratorUrl":"prometheus_url","Value":"0.333333"},"Summary":"{{$labels.instance}} = {{.Val}}"}]`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@ type AlertingRule struct {
|
||||||
// The name of the alert.
|
// The name of the alert.
|
||||||
name string
|
name string
|
||||||
// The vector expression from which to generate alerts.
|
// The vector expression from which to generate alerts.
|
||||||
vector ast.VectorNode
|
Vector ast.VectorNode
|
||||||
// The duration for which a labelset needs to persist in the expression
|
// The duration for which a labelset needs to persist in the expression
|
||||||
// output vector before an alert transitions from PENDING to FIRING state.
|
// output vector before an alert transitions from PENDING to FIRING state.
|
||||||
holdDuration time.Duration
|
holdDuration time.Duration
|
||||||
|
@ -119,7 +119,7 @@ func (rule *AlertingRule) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *AlertingRule) EvalRaw(timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (ast.Vector, error) {
|
func (rule *AlertingRule) EvalRaw(timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (ast.Vector, error) {
|
||||||
return ast.EvalVectorInstant(rule.vector, timestamp, storage, stats.NewTimerGroup())
|
return ast.EvalVectorInstant(rule.Vector, timestamp, storage, stats.NewTimerGroup())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *AlertingRule) Eval(timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (ast.Vector, error) {
|
func (rule *AlertingRule) Eval(timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (ast.Vector, error) {
|
||||||
|
@ -185,12 +185,12 @@ func (rule *AlertingRule) ToDotGraph() string {
|
||||||
%#p[shape="box",label="ALERT %s IF FOR %s"];
|
%#p[shape="box",label="ALERT %s IF FOR %s"];
|
||||||
%#p -> %#p;
|
%#p -> %#p;
|
||||||
%s
|
%s
|
||||||
}`, &rule, rule.name, utility.DurationToString(rule.holdDuration), &rule, rule.vector, rule.vector.NodeTreeToDotGraph())
|
}`, &rule, rule.name, utility.DurationToString(rule.holdDuration), &rule, rule.Vector, rule.Vector.NodeTreeToDotGraph())
|
||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *AlertingRule) String() string {
|
func (rule *AlertingRule) String() string {
|
||||||
return fmt.Sprintf("ALERT %s IF %s FOR %s WITH %s", rule.name, rule.vector, utility.DurationToString(rule.holdDuration), rule.Labels)
|
return fmt.Sprintf("ALERT %s IF %s FOR %s WITH %s", rule.name, rule.Vector, utility.DurationToString(rule.holdDuration), rule.Labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *AlertingRule) HTMLSnippet() template.HTML {
|
func (rule *AlertingRule) HTMLSnippet() template.HTML {
|
||||||
|
@ -202,8 +202,8 @@ func (rule *AlertingRule) HTMLSnippet() template.HTML {
|
||||||
`ALERT <a href="%s">%s</a> IF <a href="%s">%s</a> FOR %s WITH %s`,
|
`ALERT <a href="%s">%s</a> IF <a href="%s">%s</a> FOR %s WITH %s`,
|
||||||
ConsoleLinkForExpression(alertMetric.String()),
|
ConsoleLinkForExpression(alertMetric.String()),
|
||||||
rule.name,
|
rule.name,
|
||||||
ConsoleLinkForExpression(rule.vector.String()),
|
ConsoleLinkForExpression(rule.Vector.String()),
|
||||||
rule.vector,
|
rule.Vector,
|
||||||
utility.DurationToString(rule.holdDuration),
|
utility.DurationToString(rule.holdDuration),
|
||||||
rule.Labels))
|
rule.Labels))
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,7 @@ func (rule *AlertingRule) ActiveAlerts() []Alert {
|
||||||
func NewAlertingRule(name string, vector ast.VectorNode, holdDuration time.Duration, labels clientmodel.LabelSet, summary string, description string) *AlertingRule {
|
func NewAlertingRule(name string, vector ast.VectorNode, holdDuration time.Duration, labels clientmodel.LabelSet, summary string, description string) *AlertingRule {
|
||||||
return &AlertingRule{
|
return &AlertingRule{
|
||||||
name: name,
|
name: name,
|
||||||
vector: vector,
|
Vector: vector,
|
||||||
holdDuration: holdDuration,
|
holdDuration: holdDuration,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
|
|
|
@ -15,6 +15,7 @@ package ast
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -200,6 +201,36 @@ func EvalToString(node Node, timestamp clientmodel.Timestamp, format OutputForma
|
||||||
panic("Switch didn't cover all node types")
|
panic("Switch didn't cover all node types")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EvalToVector evaluates the given node into a Vector. Matrices aren't supported.
|
||||||
|
func EvalToVector(node Node, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence, queryStats *stats.TimerGroup) (Vector, error) {
|
||||||
|
viewTimer := queryStats.GetTimer(stats.TotalViewBuildingTime).Start()
|
||||||
|
viewAdapter, err := viewAdapterForInstantQuery(node, timestamp, storage, queryStats)
|
||||||
|
viewTimer.Stop()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evalTimer := queryStats.GetTimer(stats.InnerEvalTime).Start()
|
||||||
|
switch node.Type() {
|
||||||
|
case SCALAR:
|
||||||
|
scalar := node.(ScalarNode).Eval(timestamp, viewAdapter)
|
||||||
|
evalTimer.Stop()
|
||||||
|
return Vector{&clientmodel.Sample{Value: scalar}}, nil
|
||||||
|
case VECTOR:
|
||||||
|
vector := node.(VectorNode).Eval(timestamp, viewAdapter)
|
||||||
|
evalTimer.Stop()
|
||||||
|
return vector, nil
|
||||||
|
case MATRIX:
|
||||||
|
return nil, errors.New("Matrices not supported by EvalToVector")
|
||||||
|
case STRING:
|
||||||
|
str := node.(StringNode).Eval(timestamp, viewAdapter)
|
||||||
|
evalTimer.Stop()
|
||||||
|
return Vector{&clientmodel.Sample{
|
||||||
|
Metric: clientmodel.Metric{"__value__": clientmodel.LabelValue(str)}}}, nil
|
||||||
|
}
|
||||||
|
panic("Switch didn't cover all node types")
|
||||||
|
}
|
||||||
|
|
||||||
// NodeTreeToDotGraph returns a DOT representation of the scalar
|
// NodeTreeToDotGraph returns a DOT representation of the scalar
|
||||||
// literal.
|
// literal.
|
||||||
func (node *ScalarLiteral) NodeTreeToDotGraph() string {
|
func (node *ScalarLiteral) NodeTreeToDotGraph() string {
|
||||||
|
|
|
@ -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 rules
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -25,7 +25,9 @@ import (
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/config"
|
"github.com/prometheus/prometheus/config"
|
||||||
"github.com/prometheus/prometheus/notification"
|
"github.com/prometheus/prometheus/notification"
|
||||||
|
"github.com/prometheus/prometheus/rules"
|
||||||
"github.com/prometheus/prometheus/storage/metric"
|
"github.com/prometheus/prometheus/storage/metric"
|
||||||
|
"github.com/prometheus/prometheus/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RuleManager interface {
|
type RuleManager interface {
|
||||||
|
@ -36,15 +38,15 @@ type RuleManager interface {
|
||||||
// Stop the rule manager's rule evaluation cycles.
|
// Stop the rule manager's rule evaluation cycles.
|
||||||
Stop()
|
Stop()
|
||||||
// Return all rules.
|
// Return all rules.
|
||||||
Rules() []Rule
|
Rules() []rules.Rule
|
||||||
// Return all alerting rules.
|
// Return all alerting rules.
|
||||||
AlertingRules() []*AlertingRule
|
AlertingRules() []*rules.AlertingRule
|
||||||
}
|
}
|
||||||
|
|
||||||
type ruleManager struct {
|
type ruleManager struct {
|
||||||
// Protects the rules list.
|
// Protects the rules list.
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
rules []Rule
|
rules []rules.Rule
|
||||||
|
|
||||||
done chan bool
|
done chan bool
|
||||||
|
|
||||||
|
@ -69,7 +71,7 @@ type RuleManagerOptions struct {
|
||||||
|
|
||||||
func NewRuleManager(o *RuleManagerOptions) RuleManager {
|
func NewRuleManager(o *RuleManagerOptions) RuleManager {
|
||||||
manager := &ruleManager{
|
manager := &ruleManager{
|
||||||
rules: []Rule{},
|
rules: []rules.Rule{},
|
||||||
done: make(chan bool),
|
done: make(chan bool),
|
||||||
|
|
||||||
interval: o.EvaluationInterval,
|
interval: o.EvaluationInterval,
|
||||||
|
@ -92,7 +94,7 @@ func (m *ruleManager) Run() {
|
||||||
m.runIteration(m.results)
|
m.runIteration(m.results)
|
||||||
iterationDuration.Add(map[string]string{intervalLabel: m.interval.String()}, float64(time.Since(start)/time.Millisecond))
|
iterationDuration.Add(map[string]string{intervalLabel: m.interval.String()}, float64(time.Since(start)/time.Millisecond))
|
||||||
case <-m.done:
|
case <-m.done:
|
||||||
glog.Info("Rule manager exiting...")
|
glog.Info("rules.Rule manager exiting...")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +107,7 @@ func (m *ruleManager) Stop() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ruleManager) queueAlertNotifications(rule *AlertingRule) {
|
func (m *ruleManager) queueAlertNotifications(rule *rules.AlertingRule, timestamp clientmodel.Timestamp) {
|
||||||
activeAlerts := rule.ActiveAlerts()
|
activeAlerts := rule.ActiveAlerts()
|
||||||
if len(activeAlerts) == 0 {
|
if len(activeAlerts) == 0 {
|
||||||
return
|
return
|
||||||
|
@ -113,21 +115,46 @@ func (m *ruleManager) queueAlertNotifications(rule *AlertingRule) {
|
||||||
|
|
||||||
notifications := make(notification.NotificationReqs, 0, len(activeAlerts))
|
notifications := make(notification.NotificationReqs, 0, len(activeAlerts))
|
||||||
for _, aa := range activeAlerts {
|
for _, aa := range activeAlerts {
|
||||||
if aa.State != FIRING {
|
if aa.State != rules.FIRING {
|
||||||
// BUG: In the future, make AlertManager support pending alerts?
|
// BUG: In the future, make AlertManager support pending alerts?
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provide the alert information to the template.
|
||||||
|
l := map[string]string{}
|
||||||
|
for k, v := range aa.Labels {
|
||||||
|
l[string(k)] = string(v)
|
||||||
|
}
|
||||||
|
tmplData := struct {
|
||||||
|
Labels map[string]string
|
||||||
|
Value clientmodel.SampleValue
|
||||||
|
}{
|
||||||
|
Labels: l,
|
||||||
|
Value: aa.Value,
|
||||||
|
}
|
||||||
|
// Inject some convenience variables that are easier to remember for users
|
||||||
|
// who are not used to Go's templating system.
|
||||||
|
defs := "{{$labels := .Labels}}{{$value := .Value}}"
|
||||||
|
|
||||||
|
expand := func(text string) string {
|
||||||
|
result, err := templates.Expand(defs+text, "__alert_"+rule.Name(), tmplData, timestamp, m.storage)
|
||||||
|
if err != nil {
|
||||||
|
result = err.Error()
|
||||||
|
glog.Warningf("Error expanding alert template %v with data '%v': %v", rule.Name(), tmplData, err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
notifications = append(notifications, ¬ification.NotificationReq{
|
notifications = append(notifications, ¬ification.NotificationReq{
|
||||||
Summary: rule.Summary,
|
Summary: expand(rule.Summary),
|
||||||
Description: rule.Description,
|
Description: expand(rule.Description),
|
||||||
Labels: aa.Labels.Merge(clientmodel.LabelSet{
|
Labels: aa.Labels.Merge(clientmodel.LabelSet{
|
||||||
AlertNameLabel: clientmodel.LabelValue(rule.Name()),
|
rules.AlertNameLabel: clientmodel.LabelValue(rule.Name()),
|
||||||
}),
|
}),
|
||||||
Value: aa.Value,
|
Value: aa.Value,
|
||||||
ActiveSince: aa.ActiveSince.Time(),
|
ActiveSince: aa.ActiveSince.Time(),
|
||||||
RuleString: rule.String(),
|
RuleString: rule.String(),
|
||||||
GeneratorUrl: m.prometheusUrl + ConsoleLinkForExpression(rule.vector.String()),
|
GeneratorUrl: m.prometheusUrl + rules.ConsoleLinkForExpression(rule.Vector.String()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
m.notifications <- notifications
|
m.notifications <- notifications
|
||||||
|
@ -138,14 +165,14 @@ func (m *ruleManager) runIteration(results chan<- *extraction.Result) {
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
m.Lock()
|
m.Lock()
|
||||||
rules := make([]Rule, len(m.rules))
|
rulesSnapshot := make([]rules.Rule, len(m.rules))
|
||||||
copy(rules, m.rules)
|
copy(rulesSnapshot, m.rules)
|
||||||
m.Unlock()
|
m.Unlock()
|
||||||
|
|
||||||
for _, rule := range rules {
|
for _, rule := range rulesSnapshot {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
// BUG(julius): Look at fixing thundering herd.
|
// BUG(julius): Look at fixing thundering herd.
|
||||||
go func(rule Rule) {
|
go func(rule rules.Rule) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
@ -160,10 +187,10 @@ func (m *ruleManager) runIteration(results chan<- *extraction.Result) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r := rule.(type) {
|
switch r := rule.(type) {
|
||||||
case *AlertingRule:
|
case *rules.AlertingRule:
|
||||||
m.queueAlertNotifications(r)
|
m.queueAlertNotifications(r, now)
|
||||||
recordOutcome(alertingRuleType, duration)
|
recordOutcome(alertingRuleType, duration)
|
||||||
case *RecordingRule:
|
case *rules.RecordingRule:
|
||||||
recordOutcome(recordingRuleType, duration)
|
recordOutcome(recordingRuleType, duration)
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("Unknown rule type: %T", rule))
|
panic(fmt.Sprintf("Unknown rule type: %T", rule))
|
||||||
|
@ -176,7 +203,7 @@ func (m *ruleManager) runIteration(results chan<- *extraction.Result) {
|
||||||
|
|
||||||
func (m *ruleManager) AddRulesFromConfig(config config.Config) error {
|
func (m *ruleManager) AddRulesFromConfig(config config.Config) error {
|
||||||
for _, ruleFile := range config.Global.RuleFile {
|
for _, ruleFile := range config.Global.RuleFile {
|
||||||
newRules, err := LoadRulesFromFile(ruleFile)
|
newRules, err := rules.LoadRulesFromFile(ruleFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %s", ruleFile, err)
|
return fmt.Errorf("%s: %s", ruleFile, err)
|
||||||
}
|
}
|
||||||
|
@ -187,22 +214,22 @@ func (m *ruleManager) AddRulesFromConfig(config config.Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ruleManager) Rules() []Rule {
|
func (m *ruleManager) Rules() []rules.Rule {
|
||||||
m.Lock()
|
m.Lock()
|
||||||
defer m.Unlock()
|
defer m.Unlock()
|
||||||
|
|
||||||
rules := make([]Rule, len(m.rules))
|
rules := make([]rules.Rule, len(m.rules))
|
||||||
copy(rules, m.rules)
|
copy(rules, m.rules)
|
||||||
return rules
|
return rules
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ruleManager) AlertingRules() []*AlertingRule {
|
func (m *ruleManager) AlertingRules() []*rules.AlertingRule {
|
||||||
m.Lock()
|
m.Lock()
|
||||||
defer m.Unlock()
|
defer m.Unlock()
|
||||||
|
|
||||||
alerts := []*AlertingRule{}
|
alerts := []*rules.AlertingRule{}
|
||||||
for _, rule := range m.rules {
|
for _, rule := range m.rules {
|
||||||
if alertingRule, ok := rule.(*AlertingRule); ok {
|
if alertingRule, ok := rule.(*rules.AlertingRule); ok {
|
||||||
alerts = append(alerts, alertingRule)
|
alerts = append(alerts, alertingRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 rules
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
|
@ -62,37 +62,8 @@ func (t testTieredStorageCloser) Close() {
|
||||||
t.directory.Close()
|
t.directory.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is copied from storage/metric/helpers_test.go, which is unfortunate but
|
|
||||||
// presently required to make things work.
|
|
||||||
func NewTestTieredStorage(t testing.TB) (storage *tiered.TieredStorage, closer test.Closer) {
|
|
||||||
var directory test.TemporaryDirectory
|
|
||||||
directory = test.NewTemporaryDirectory("test_tiered_storage", t)
|
|
||||||
storage, err := tiered.NewTieredStorage(2500, 1000, 5*time.Second, 0*time.Second, directory.Path())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if storage != nil {
|
|
||||||
storage.Close()
|
|
||||||
}
|
|
||||||
directory.Close()
|
|
||||||
t.Fatalf("Error creating storage: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if storage == nil {
|
|
||||||
directory.Close()
|
|
||||||
t.Fatalf("storage == nil")
|
|
||||||
}
|
|
||||||
started := make(chan bool)
|
|
||||||
go storage.Serve(started)
|
|
||||||
<-started
|
|
||||||
closer = &testTieredStorageCloser{
|
|
||||||
storage: storage,
|
|
||||||
directory: directory,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestStorage(t testing.TB) (storage *tiered.TieredStorage, closer test.Closer) {
|
func newTestStorage(t testing.TB) (storage *tiered.TieredStorage, closer test.Closer) {
|
||||||
storage, closer = NewTestTieredStorage(t)
|
storage, closer = tiered.NewTestTieredStorage(t)
|
||||||
if storage == nil {
|
if storage == nil {
|
||||||
t.Fatal("storage == nil")
|
t.Fatal("storage == nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.google.com/p/goprotobuf/proto"
|
"code.google.com/p/goprotobuf/proto"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/storage/raw/leveldb"
|
"github.com/prometheus/prometheus/storage/raw/leveldb"
|
||||||
|
|
110
templates/templates.go
Normal file
110
templates/templates.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright 2013 Prometheus Team
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
clientmodel "github.com/prometheus/client_golang/model"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/rules"
|
||||||
|
"github.com/prometheus/prometheus/rules/ast"
|
||||||
|
"github.com/prometheus/prometheus/stats"
|
||||||
|
"github.com/prometheus/prometheus/storage/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A version of vector that's easier to use from templates.
|
||||||
|
type sample struct {
|
||||||
|
Labels map[string]string
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
type queryResult []*sample
|
||||||
|
|
||||||
|
// Expand a template, using the given data, time and storage.
|
||||||
|
func Expand(text string, name string, data interface{}, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (result string, resultErr error) {
|
||||||
|
|
||||||
|
// It'd better to have no alert description than to kill the whole process
|
||||||
|
// if there's a bug in the template. Similarly with console templates.
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
var ok bool
|
||||||
|
resultErr, ok = r.(error)
|
||||||
|
if !ok {
|
||||||
|
resultErr = fmt.Errorf("Panic expanding template: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
funcMap := template.FuncMap{
|
||||||
|
"query": func(q string) (queryResult, error) {
|
||||||
|
return query(q, timestamp, storage)
|
||||||
|
},
|
||||||
|
"first": func(v queryResult) (*sample, error) {
|
||||||
|
if len(v) > 0 {
|
||||||
|
return v[0], nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("first() called on vector with no elements")
|
||||||
|
},
|
||||||
|
"label": func(label string, s *sample) string {
|
||||||
|
return s.Labels[label]
|
||||||
|
},
|
||||||
|
"value": func(s *sample) float64 {
|
||||||
|
return s.Value
|
||||||
|
},
|
||||||
|
"strvalue": func(s *sample) string {
|
||||||
|
return s.Labels["__value__"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
tmpl, err := template.New(name).Funcs(funcMap).Parse(text)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error parsing template %v: %v", name, err)
|
||||||
|
}
|
||||||
|
err = tmpl.Execute(&buffer, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error executing template %v: %v", name, err)
|
||||||
|
}
|
||||||
|
return buffer.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func query(q string, timestamp clientmodel.Timestamp, storage metric.PreloadingPersistence) (queryResult, error) {
|
||||||
|
exprNode, err := rules.LoadExprFromString(q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
queryStats := stats.NewTimerGroup()
|
||||||
|
vector, err := ast.EvalToVector(exprNode, timestamp, storage, queryStats)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ast.Vector is hard to work with in templates, so convert to
|
||||||
|
// base data types.
|
||||||
|
var result = make(queryResult, len(vector))
|
||||||
|
for n, v := range vector {
|
||||||
|
s := sample{
|
||||||
|
Value: float64(v.Value),
|
||||||
|
Labels: make(map[string]string),
|
||||||
|
}
|
||||||
|
for label, value := range v.Metric {
|
||||||
|
s.Labels[string(label)] = string(value)
|
||||||
|
}
|
||||||
|
result[n] = &s
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
109
templates/templates_test.go
Normal file
109
templates/templates_test.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2014 Prometheus Team
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
clientmodel "github.com/prometheus/client_golang/model"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/storage/metric/tiered"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testTemplatesScenario struct {
|
||||||
|
text string
|
||||||
|
output string
|
||||||
|
shouldFail bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateExpansion(t *testing.T) {
|
||||||
|
scenarios := []testTemplatesScenario{
|
||||||
|
{
|
||||||
|
// No template.
|
||||||
|
text: "plain text",
|
||||||
|
output: "plain text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simple value.
|
||||||
|
text: "{{ 1 }}",
|
||||||
|
output: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Get value from query.
|
||||||
|
text: "{{ query \"metric{instance='a'}\" | first | value }}",
|
||||||
|
output: "11",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Get label from query.
|
||||||
|
text: "{{ query \"metric{instance='a'}\" | first | label \"instance\" }}",
|
||||||
|
output: "a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Range over query.
|
||||||
|
text: "{{ range query \"metric\" }}{{.Labels.instance}}:{{.Value}}: {{end}}",
|
||||||
|
output: "a:11: b:21: ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Unparsable template.
|
||||||
|
text: "{{",
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Error in function.
|
||||||
|
text: "{{ query \"missing\" | first }}",
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Panic.
|
||||||
|
text: "{{ (query \"missing\").banana }}",
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
time := clientmodel.Timestamp(0)
|
||||||
|
|
||||||
|
ts, _ := tiered.NewTestTieredStorage(t)
|
||||||
|
ts.AppendSamples(clientmodel.Samples{
|
||||||
|
{
|
||||||
|
Metric: clientmodel.Metric{
|
||||||
|
clientmodel.MetricNameLabel: "metric",
|
||||||
|
"instance": "a"},
|
||||||
|
Value: 11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Metric: clientmodel.Metric{
|
||||||
|
clientmodel.MetricNameLabel: "metric",
|
||||||
|
"instance": "b"},
|
||||||
|
Value: 21,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
result, err := Expand(s.text, "test", nil, time, ts)
|
||||||
|
if s.shouldFail {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Error not returned from %v", s.text)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error returned from %v: %v", s.text, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if result != s.output {
|
||||||
|
t.Fatalf("Error in result from %v: Expected '%v' Got '%v'", s.text, s.output, result)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,10 +14,12 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/prometheus/prometheus/rules"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/prometheus/prometheus/rules"
|
||||||
|
"github.com/prometheus/prometheus/rules/manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertStatus struct {
|
type AlertStatus struct {
|
||||||
|
@ -26,7 +28,7 @@ type AlertStatus struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertsHandler struct {
|
type AlertsHandler struct {
|
||||||
RuleManager rules.RuleManager
|
RuleManager manager.RuleManager
|
||||||
|
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
74
web/consoles.go
Normal file
74
web/consoles.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright 2014 Prometheus Team
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
clientmodel "github.com/prometheus/client_golang/model"
|
||||||
|
"github.com/prometheus/prometheus/storage/metric"
|
||||||
|
"github.com/prometheus/prometheus/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
consoleTemplatesPath = flag.String("consoleTemplates", "consoles", "Path to console template directory, available at /console")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConsolesHandler struct {
|
||||||
|
Storage metric.PreloadingPersistence
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ConsolesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
file, err := http.Dir(*consoleTemplatesPath).Open(r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide URL parameters as a map for easy use. Advanced users may have need for
|
||||||
|
// parameters beyond the first, so provide RawParams.
|
||||||
|
rawParams, err := url.ParseQuery(r.URL.RawQuery)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{}
|
||||||
|
for k, v := range rawParams {
|
||||||
|
params[k] = v[0]
|
||||||
|
}
|
||||||
|
data := struct {
|
||||||
|
RawParams url.Values
|
||||||
|
Params map[string]string
|
||||||
|
}{
|
||||||
|
RawParams: rawParams,
|
||||||
|
Params: params,
|
||||||
|
}
|
||||||
|
|
||||||
|
now := clientmodel.Now()
|
||||||
|
result, err := templates.Expand(string(text), "__console_"+r.URL.Path, data, now, h.Storage)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
io.WriteString(w, result)
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/prometheus/retrieval"
|
"github.com/prometheus/prometheus/retrieval"
|
||||||
"github.com/prometheus/prometheus/rules"
|
"github.com/prometheus/prometheus/rules/manager"
|
||||||
"github.com/prometheus/prometheus/storage/metric"
|
"github.com/prometheus/prometheus/storage/metric"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ type PrometheusStatusHandler struct {
|
||||||
Config string
|
Config string
|
||||||
Curation metric.CurationState
|
Curation metric.CurationState
|
||||||
Flags map[string]string
|
Flags map[string]string
|
||||||
RuleManager rules.RuleManager
|
RuleManager manager.RuleManager
|
||||||
TargetPools map[string]*retrieval.TargetPool
|
TargetPools map[string]*retrieval.TargetPool
|
||||||
|
|
||||||
Birth time.Time
|
Birth time.Time
|
||||||
|
|
|
@ -46,6 +46,7 @@ type WebService struct {
|
||||||
DatabasesHandler *DatabasesHandler
|
DatabasesHandler *DatabasesHandler
|
||||||
MetricsHandler *api.MetricsService
|
MetricsHandler *api.MetricsService
|
||||||
AlertsHandler *AlertsHandler
|
AlertsHandler *AlertsHandler
|
||||||
|
ConsolesHandler *ConsolesHandler
|
||||||
|
|
||||||
QuitDelegate func()
|
QuitDelegate func()
|
||||||
}
|
}
|
||||||
|
@ -65,6 +66,7 @@ func (w WebService) ServeForever() error {
|
||||||
exp.Handle("/", w.StatusHandler)
|
exp.Handle("/", w.StatusHandler)
|
||||||
exp.Handle("/databases", w.DatabasesHandler)
|
exp.Handle("/databases", w.DatabasesHandler)
|
||||||
exp.Handle("/alerts", w.AlertsHandler)
|
exp.Handle("/alerts", w.AlertsHandler)
|
||||||
|
exp.Handle("/consoles/", http.StripPrefix("/consoles/", w.ConsolesHandler))
|
||||||
exp.HandleFunc("/graph", graphHandler)
|
exp.HandleFunc("/graph", graphHandler)
|
||||||
exp.HandleFunc("/heap", dumpHeap)
|
exp.HandleFunc("/heap", dumpHeap)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue