Merge pull request #70 from prometheus/julius-hud

Cleanups and beginnings of a /status endpoint
This commit is contained in:
juliusv 2013-02-14 10:04:30 -08:00
commit db5868f1b3
23 changed files with 445 additions and 155 deletions

31
appstate/appstate.go Normal file
View file

@ -0,0 +1,31 @@
// 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 appstate
import (
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/retrieval"
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/storage/metric"
)
// ApplicationState is an encapsulation of all relevant Prometheus application
// runtime state. It enables simpler passing of this state to components that
// require it.
type ApplicationState struct {
Config *config.Config
Persistence metric.MetricPersistence
RuleManager rules.RuleManager
TargetManager retrieval.TargetManager
}

View file

@ -17,6 +17,7 @@ import (
"errors"
"fmt"
"github.com/prometheus/prometheus/model"
"github.com/prometheus/prometheus/utility"
"time"
)
@ -55,7 +56,7 @@ func (config *Config) AddJob(options map[string]string, targets []Targets) error
return errors.New("Missing job name")
}
if len(targets) == 0 {
return errors.New(fmt.Sprintf("No targets configured for job '%v'", name))
return fmt.Errorf("No targets configured for job '%v'", name)
}
job := &JobConfig{
Targets: tmpJobTargets,
@ -69,18 +70,18 @@ func (config *Config) AddJob(options map[string]string, targets []Targets) error
return nil
}
func (config *GlobalConfig) SetOption(option string, value string) error {
func (config *GlobalConfig) SetOption(option string, value string) (err error) {
switch option {
case "scrape_interval":
config.ScrapeInterval = stringToDuration(value)
config.ScrapeInterval, err = utility.StringToDuration(value)
return nil
case "evaluation_interval":
config.EvaluationInterval = stringToDuration(value)
return nil
config.EvaluationInterval, err = utility.StringToDuration(value)
return err
default:
return errors.New(fmt.Sprintf("Unrecognized global configuration option '%v'", option))
err = fmt.Errorf("Unrecognized global configuration option '%v'", option)
}
return nil
return
}
func (config *GlobalConfig) SetLabels(labels model.LabelSet) {
@ -95,18 +96,16 @@ func (config *GlobalConfig) AddRuleFiles(ruleFiles []string) {
}
}
func (job *JobConfig) SetOption(option string, value string) error {
func (job *JobConfig) SetOption(option string, value string) (err error) {
switch option {
case "name":
job.Name = value
return nil
case "scrape_interval":
job.ScrapeInterval = stringToDuration(value)
return nil
job.ScrapeInterval, err = utility.StringToDuration(value)
default:
return errors.New(fmt.Sprintf("Unrecognized job configuration option '%v'", option))
err = fmt.Errorf("Unrecognized job configuration option '%v'", option)
}
return nil
return
}
func (job *JobConfig) AddTargets(endpoints []string, labels model.LabelSet) {

86
config/config_test.go Normal file
View file

@ -0,0 +1,86 @@
// 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 config
import (
"fmt"
"io/ioutil"
"path"
"strings"
"testing"
)
var fixturesPath = "fixtures"
var configTests = []struct {
inputFile string
printedFile string
shouldFail bool
errContains string
}{
{
inputFile: "minimal.conf",
printedFile: "minimal.conf.printed",
}, {
inputFile: "sample.conf",
printedFile: "sample.conf.printed",
}, {
// TODO: Options that are not provided should be set to sane defaults or
// create errors during config loading (as appropriate). Right now, these
// options remain at their zero-values, which is probably not what we want.
inputFile: "empty.conf",
printedFile: "empty.conf.printed",
},
// TODO: To enable testing of bad configs, we first need to change config
// loading so that it doesn't exit when loading a bad config. Instead, the
// configuration error should be passed back all the way to the caller.
//
//{
// inputFile: "bad_job_option.conf",
// shouldFail: true,
// errContains: "Missing job name",
//},
}
func TestConfigs(t *testing.T) {
for _, configTest := range configTests {
testConfig, err := LoadFromFile(path.Join(fixturesPath, configTest.inputFile))
if err != nil {
if !configTest.shouldFail {
t.Errorf("Error parsing config %v: %v", configTest.inputFile, err)
} else {
if !strings.Contains(err.Error(), configTest.errContains) {
t.Errorf("Expected error containing '%v', got: %v", configTest.errContains, err)
}
}
} else {
printedConfig, err := ioutil.ReadFile(path.Join(fixturesPath, configTest.printedFile))
if err != nil {
t.Errorf("Error reading config %v: %v", configTest.inputFile, err)
continue
}
expected := string(printedConfig)
actual := testConfig.ToString(0)
if actual != expected {
t.Errorf("%v: printed config doesn't match expected output", configTest.inputFile)
t.Errorf("Expected:\n%v\n\nActual:\n%v\n", expected, actual)
t.Errorf("Writing expected and actual printed configs to /tmp for diffing (see test source for paths)")
ioutil.WriteFile(fmt.Sprintf("/tmp/%s.expected", configTest.printedFile), []byte(expected), 0600)
ioutil.WriteFile(fmt.Sprintf("/tmp/%s.actual", configTest.printedFile), []byte(actual), 0600)
}
}
}
}

View file

@ -0,0 +1,8 @@
global {
scrape_interval = "0y"
evaluation_interval = "0y"
rule_files = [
]
}

View file

@ -0,0 +1,20 @@
global {
scrape_interval = "30s"
evaluation_interval = "30s"
labels {
monitor = "test"
}
rule_files = [
"prometheus.rules"
]
}
job {
name = "prometheus"
scrape_interval = "15s"
targets {
endpoints = [
"http://localhost:9090/metrics.json"
]
}
}

View file

@ -0,0 +1,49 @@
global {
scrape_interval = "30s"
evaluation_interval = "30s"
labels {
monitor = "test"
}
rule_files = [
"prometheus.rules"
]
}
job {
name = "prometheus"
scrape_interval = "15s"
targets {
endpoints = [
"http://localhost:9090/metrics.json"
]
labels {
group = "canary"
}
}
}
job {
name = "random"
scrape_interval = "30s"
targets {
endpoints = [
"http://random.com:8080/metrics.json",
"http://random.com:8081/metrics.json",
"http://random.com:8082/metrics.json",
"http://random.com:8083/metrics.json",
"http://random.com:8084/metrics.json"
]
labels {
group = "production"
}
}
targets {
endpoints = [
"http://random.com:8085/metrics.json",
"http://random.com:8086/metrics.json"
]
labels {
group = "canary"
}
}
}

View file

@ -17,9 +17,6 @@ import (
"fmt"
"github.com/prometheus/prometheus/model"
"log"
"regexp"
"strconv"
"time"
)
// Unfortunately, more global variables that are needed for parsing.
@ -30,7 +27,9 @@ var tmpTargetLabels = model.LabelSet{}
func configError(error string, v ...interface{}) {
message := fmt.Sprintf(error, v...)
log.Fatal(fmt.Sprintf("Line %v, char %v: %s", yyline, yypos, message))
// TODO: Don't just die here. Pass errors back all the way to the caller
// instead.
log.Fatalf("Line %v, char %v: %s", yyline, yypos, message)
}
func PushJobOption(option string, value string) {
@ -66,26 +65,3 @@ func PopJob() {
tmpJobOptions = map[string]string{}
tmpJobTargets = []Targets{}
}
func stringToDuration(durationStr string) time.Duration {
durationRE := regexp.MustCompile("^([0-9]+)([ywdhms]+)$")
matches := durationRE.FindStringSubmatch(durationStr)
if len(matches) != 3 {
configError("Not a valid duration string: '%v'", durationStr)
}
value, _ := strconv.Atoi(matches[1])
unit := matches[2]
switch unit {
case "y":
value *= 60 * 60 * 24 * 365
case "w":
value *= 60 * 60 * 24 * 7
case "d":
value *= 60 * 60 * 24
case "h":
value *= 60 * 60
case "m":
value *= 60
}
return time.Duration(value) * time.Second
}

View file

@ -27,7 +27,7 @@ import (
var yylval *yySymType // For storing extra token information, like the contents of a string.
var yyline int // Line number within the current file or buffer.
var yypos int // Character position within the current line.
var parsedConfig = New() // Temporary variable for storing the parsed configuration.
var parsedConfig *Config // Temporary variable for storing the parsed configuration.
type ConfigLexer struct {
errors []string
@ -45,6 +45,7 @@ func (lexer *ConfigLexer) Error(errorStr string) {
}
func LoadFromReader(configReader io.Reader) (*Config, error) {
parsedConfig = New()
yyin = configReader
yypos = 1
yyline = 1

View file

@ -16,6 +16,7 @@ package config
import (
"fmt"
"github.com/prometheus/prometheus/model"
"github.com/prometheus/prometheus/utility"
"strings"
)
@ -33,45 +34,54 @@ func (config *Config) ToString(indent int) string {
for _, job := range config.Jobs {
jobs = append(jobs, job.ToString(indent))
}
return indentStr(indent, "%v\n%v\n", global, strings.Join(jobs, "\n"))
return indentStr(indent, "%v\n%v", global, strings.Join(jobs, "\n"))
}
func labelsToString(indent int, labels model.LabelSet) string {
str := indentStr(indent, "labels {\n")
labelStrings := []string{}
for label, value := range labels {
str += indentStr(indent+1, "%v = \"%v\",\n", label, value)
labelStrings = append(labelStrings, indentStr(indent+1, "%v = \"%v\"", label, value))
}
str += strings.Join(labelStrings, ",\n") + "\n"
str += indentStr(indent, "}\n")
return str
}
func stringListToString(indent int, list []string) string {
listString := []string{}
for _, item := range list {
listString = append(listString, indentStr(indent, "\"%v\"", item))
}
return strings.Join(listString, ",\n") + "\n"
}
func (global *GlobalConfig) ToString(indent int) string {
str := indentStr(indent, "global {\n")
str += indentStr(indent+1, "scrape_interval = \"%vs\"\n", global.ScrapeInterval)
str += indentStr(indent+1, "evaluation_interval = \"%vs\"\n", global.EvaluationInterval)
str += labelsToString(indent+1, global.Labels)
str += indentStr(indent, "}\n")
str += indentStr(indent+1, "rule_files = [\n")
for _, ruleFile := range global.RuleFiles {
str += indentStr(indent+2, "\"%v\",\n", ruleFile)
str += indentStr(indent+1, "scrape_interval = \"%s\"\n", utility.DurationToString(global.ScrapeInterval))
str += indentStr(indent+1, "evaluation_interval = \"%s\"\n", utility.DurationToString(global.EvaluationInterval))
if len(global.Labels) > 0 {
str += labelsToString(indent+1, global.Labels)
}
str += indentStr(indent+1, "rule_files = [\n")
str += stringListToString(indent+2, global.RuleFiles)
str += indentStr(indent+1, "]\n")
str += indentStr(indent, "}\n")
return str
}
func (job *JobConfig) ToString(indent int) string {
str := indentStr(indent, "job {\n")
str += indentStr(indent+1, "job {\n")
str += indentStr(indent+1, "name = \"%v\"\n", job.Name)
str += indentStr(indent+1, "scrape_interval = \"%vs\"\n", job.ScrapeInterval)
str += indentStr(indent+1, "scrape_interval = \"%s\"\n", utility.DurationToString(job.ScrapeInterval))
for _, targets := range job.Targets {
str += indentStr(indent+1, "targets {\n")
str += indentStr(indent+2, "endpoints = [\n")
for _, endpoint := range targets.Endpoints {
str += indentStr(indent+3, "\"%v\",\n", endpoint)
}
str += stringListToString(indent+3, targets.Endpoints)
str += indentStr(indent+2, "]\n")
str += labelsToString(indent+2, targets.Labels)
if len(targets.Labels) > 0 {
str += labelsToString(indent+2, targets.Labels)
}
str += indentStr(indent+1, "}\n")
}
str += indentStr(indent, "}\n")

10
main.go
View file

@ -15,6 +15,7 @@ package main
import (
"flag"
"github.com/prometheus/prometheus/appstate"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/retrieval"
"github.com/prometheus/prometheus/retrieval/format"
@ -73,7 +74,14 @@ func main() {
log.Fatalf("Error loading rule files: %v", err)
}
web.StartServing(persistence)
appState := &appstate.ApplicationState{
Config: conf,
Persistence: persistence,
RuleManager: ruleManager,
TargetManager: targetManager,
}
web.StartServing(appState)
for {
select {

View file

@ -16,6 +16,7 @@ package ast
import (
"encoding/json"
"fmt"
"github.com/prometheus/prometheus/utility"
"sort"
"strings"
"time"
@ -65,29 +66,6 @@ func exprTypeToString(exprType ExprType) string {
return exprTypeMap[exprType]
}
func durationToString(duration time.Duration) string {
seconds := int64(duration / time.Second)
factors := map[string]int64{
"y": 60 * 60 * 24 * 365,
"d": 60 * 60 * 24,
"h": 60 * 60,
"m": 60,
"s": 1,
}
unit := "s"
switch int64(0) {
case seconds % factors["y"]:
unit = "y"
case seconds % factors["d"]:
unit = "d"
case seconds % factors["h"]:
unit = "h"
case seconds % factors["m"]:
unit = "m"
}
return fmt.Sprintf("%v%v", seconds/factors[unit], unit)
}
func (vector Vector) ToString() string {
metricStrings := []string{}
for _, sample := range vector {
@ -228,7 +206,7 @@ func (node *VectorLiteral) ToString() string {
func (node *MatrixLiteral) ToString() string {
vectorString := (&VectorLiteral{labels: node.labels}).ToString()
intervalString := fmt.Sprintf("['%v']", durationToString(node.interval))
intervalString := fmt.Sprintf("['%v']", utility.DurationToString(node.interval))
return vectorString + intervalString
}

View file

@ -14,48 +14,15 @@
package rules
import (
"errors"
"fmt"
"github.com/prometheus/prometheus/model"
"github.com/prometheus/prometheus/rules/ast"
"regexp"
"strconv"
"time"
"github.com/prometheus/prometheus/utility"
)
func rulesError(error string, v ...interface{}) error {
return errors.New(fmt.Sprintf(error, v...))
}
// TODO move to common place, currently duplicated in config/
func stringToDuration(durationStr string) (time.Duration, error) {
durationRE := regexp.MustCompile("^([0-9]+)([ywdhms]+)$")
matches := durationRE.FindStringSubmatch(durationStr)
if len(matches) != 3 {
return 0, rulesError("Not a valid duration string: '%v'", durationStr)
}
value, _ := strconv.Atoi(matches[1])
unit := matches[2]
switch unit {
case "y":
value *= 60 * 60 * 24 * 365
case "w":
value *= 60 * 60 * 24
case "d":
value *= 60 * 60 * 24
case "h":
value *= 60 * 60
case "m":
value *= 60
case "s":
value *= 1
}
return time.Duration(value) * time.Second, nil
}
func CreateRule(name string, labels model.LabelSet, root ast.Node, permanent bool) (*Rule, error) {
if root.Type() != ast.VECTOR {
return nil, rulesError("Rule %v does not evaluate to vector type", name)
return nil, fmt.Errorf("Rule %v does not evaluate to vector type", name)
}
return NewRule(name, labels, root.(ast.VectorNode), permanent), nil
}
@ -63,18 +30,18 @@ func CreateRule(name string, labels model.LabelSet, root ast.Node, permanent boo
func NewFunctionCall(name string, args []ast.Node) (ast.Node, error) {
function, err := ast.GetFunction(name)
if err != nil {
return nil, rulesError("Unknown function \"%v\"", name)
return nil, fmt.Errorf("Unknown function \"%v\"", name)
}
functionCall, err := ast.NewFunctionCall(function, args)
if err != nil {
return nil, rulesError(err.Error())
return nil, fmt.Errorf(err.Error())
}
return functionCall, nil
}
func NewVectorAggregation(aggrTypeStr string, vector ast.Node, groupBy []model.LabelName) (*ast.VectorAggregation, error) {
if vector.Type() != ast.VECTOR {
return nil, rulesError("Operand of %v aggregation must be of vector type", aggrTypeStr)
return nil, fmt.Errorf("Operand of %v aggregation must be of vector type", aggrTypeStr)
}
var aggrTypes = map[string]ast.AggrType{
"SUM": ast.SUM,
@ -84,7 +51,7 @@ func NewVectorAggregation(aggrTypeStr string, vector ast.Node, groupBy []model.L
}
aggrType, ok := aggrTypes[aggrTypeStr]
if !ok {
return nil, rulesError("Unknown aggregation type '%v'", aggrTypeStr)
return nil, fmt.Errorf("Unknown aggregation type '%v'", aggrTypeStr)
}
return ast.NewVectorAggregation(aggrType, vector.(ast.VectorNode), groupBy), nil
}
@ -107,11 +74,11 @@ func NewArithExpr(opTypeStr string, lhs ast.Node, rhs ast.Node) (ast.Node, error
}
opType, ok := opTypes[opTypeStr]
if !ok {
return nil, rulesError("Invalid binary operator \"%v\"", opTypeStr)
return nil, fmt.Errorf("Invalid binary operator \"%v\"", opTypeStr)
}
expr, err := ast.NewArithExpr(opType, lhs, rhs)
if err != nil {
return nil, rulesError(err.Error())
return nil, fmt.Errorf(err.Error())
}
return expr, nil
}
@ -123,9 +90,9 @@ func NewMatrix(vector ast.Node, intervalStr string) (ast.MatrixNode, error) {
break
}
default:
return nil, rulesError("Intervals are currently only supported for vector literals.")
return nil, fmt.Errorf("Intervals are currently only supported for vector literals.")
}
interval, err := stringToDuration(intervalStr)
interval, err := utility.StringToDuration(intervalStr)
if err != nil {
return nil, err
}

View file

@ -48,7 +48,9 @@ AVG|SUM|MAX|MIN { yylval.str = yytext; return AGGR_OP }
{L}({L}|{D})+ { yylval.str = yytext; return IDENTIFIER }
\-?{D}+(\.{D}*)? { num, err := strconv.ParseFloat(yytext, 32);
if (err != nil) { rulesError("Invalid float %v", yytext) }
if (err != nil && err.(*strconv.NumError).Err == strconv.ErrSyntax) {
panic("Invalid float")
}
yylval.num = model.SampleValue(num)
return NUMBER }

View file

@ -436,8 +436,8 @@ var yyrules []yyrule = []yyrule{{regexp.MustCompile("[^\\n]"), nil, []yystartcon
}()
{
num, err := strconv.ParseFloat(yytext, 32)
if err != nil {
rulesError("Invalid float %v", yytext)
if err != nil && err.(*strconv.NumError).Err == strconv.ErrSyntax {
panic("Invalid float")
}
yylval.num = model.SampleValue(num)
return yyactionreturn{NUMBER, yyRT_USER_RETURN}

74
utility/strconv.go Normal file
View file

@ -0,0 +1,74 @@
// 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 utility
import (
"fmt"
"regexp"
"strconv"
"time"
)
var durationRE = regexp.MustCompile("^([0-9]+)([ywdhms]+)$")
func DurationToString(duration time.Duration) string {
seconds := int64(duration / time.Second)
factors := map[string]int64{
"y": 60 * 60 * 24 * 365,
"d": 60 * 60 * 24,
"h": 60 * 60,
"m": 60,
"s": 1,
}
unit := "s"
switch int64(0) {
case seconds % factors["y"]:
unit = "y"
case seconds % factors["d"]:
unit = "d"
case seconds % factors["h"]:
unit = "h"
case seconds % factors["m"]:
unit = "m"
}
return fmt.Sprintf("%v%v", seconds/factors[unit], unit)
}
func StringToDuration(durationStr string) (duration time.Duration, err error) {
matches := durationRE.FindStringSubmatch(durationStr)
if len(matches) != 3 {
err = fmt.Errorf("Not a valid duration string: '%v'", durationStr)
return
}
durationSeconds, _ := strconv.Atoi(matches[1])
duration = time.Duration(durationSeconds) * time.Second
unit := matches[2]
switch unit {
case "y":
duration *= 60 * 60 * 24 * 365
case "w":
duration *= 60 * 60 * 24 * 7
case "d":
duration *= 60 * 60 * 24
case "h":
duration *= 60 * 60
case "m":
duration *= 60
case "s":
duration *= 1
default:
panic("Invalid time unit in duration string.")
}
return
}

View file

@ -17,8 +17,8 @@ type MetricsService struct {
time utility.Time
}
func NewMetricsService(p metric.MetricPersistence) *MetricsService {
func NewMetricsService(persistence metric.MetricPersistence) *MetricsService {
return &MetricsService{
persistence: p,
persistence: persistence,
}
}

23
web/static/css/graph.css Normal file
View file

@ -0,0 +1,23 @@
body {
margin: 0;
}
.graph_container {
font-family: Arial, Helvetica, sans-serif;
}
.graph {
position: relative;
}
svg {
border: 1px solid #aaa;
margin-bottom: 5px;
}
.legend {
display: inline-block;
vertical-align: top;
margin: 0 0 0 0px;
background-
}

View file

@ -1,5 +1,4 @@
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
@ -7,26 +6,6 @@ body {
background-color: #eee;
}
.graph_container {
font-family: Arial, Helvetica, sans-serif;
}
.graph {
position: relative;
}
svg {
border: 1px solid #aaa;
margin-bottom: 5px;
}
.legend {
display: inline-block;
vertical-align: top;
margin: 0 0 0 0px;
background-
}
input:not([type=submit]):not([type=file]):not([type=button]) {
border: 1px solid #aaa;
-webkit-border-radius: 3px;

View file

@ -6,6 +6,7 @@
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.15/jquery-ui.min.js"></script>
<link type="text/css" rel="stylesheet" href="css/prometheus.css">
<link type="text/css" rel="stylesheet" href="css/graph.css">
<!-- copy all these CSSen/JSen to our own location -->
<link type="text/css" rel="stylesheet" href="http://code.shutterstock.com/rickshaw/rickshaw.min.css">

View file

@ -4,6 +4,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Prometheus Expression Browser</title>
<link type="text/css" rel="stylesheet" href="css/prometheus.css">
<link type="text/css" rel="stylesheet" href="css/graph.css">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="js/exprBrowser.js"></script>
</head>

42
web/status.go Normal file
View file

@ -0,0 +1,42 @@
// 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 web
import (
"github.com/prometheus/prometheus/appstate"
"html/template"
"net/http"
)
type PrometheusStatus struct {
Config string
Rules string
Status string
Targets string
}
type StatusHandler struct {
appState *appstate.ApplicationState
}
func (h *StatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
status := &PrometheusStatus{
Config: h.appState.Config.ToString(0),
Rules: "TODO: list rules here",
Status: "TODO: add status information here",
Targets: "TODO: list targets here",
}
t, _ := template.ParseFiles("web/templates/status.html")
t.Execute(w, status)
}

33
web/templates/status.html Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Prometheus Status</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<link type="text/css" rel="stylesheet" href="static/css/prometheus.css">
</head>
<body>
<h2>Status</h2>
<div class="grouping_box">
{{.Status}}
</div>
<h2>Configuration</h2>
<div class="grouping_box">
<pre>
{{.Config}}
</pre>
</div>
<h2>Rules</h2>
<div class="grouping_box">
{{.Rules}}
</div>
<h2>Targets</h2>
<div class="grouping_box">
{{.Targets}}
</div>
</body>
</html>

View file

@ -17,7 +17,7 @@ import (
"code.google.com/p/gorest"
"flag"
"github.com/prometheus/client_golang"
"github.com/prometheus/prometheus/storage/metric"
"github.com/prometheus/prometheus/appstate"
"github.com/prometheus/prometheus/web/api"
"net/http"
_ "net/http/pprof"
@ -28,11 +28,13 @@ var (
listenAddress = flag.String("listenAddress", ":9090", "Address to listen on for web interface.")
)
func StartServing(persistence metric.MetricPersistence) {
gorest.RegisterService(api.NewMetricsService(persistence))
func StartServing(appState *appstate.ApplicationState) {
gorest.RegisterService(api.NewMetricsService(appState.Persistence))
exporter := registry.DefaultRegistry.YieldExporter()
http.Handle("/", gorest.Handle())
http.Handle("/metrics.json", registry.DefaultHandler)
http.Handle("/status", &StatusHandler{appState: appState})
http.Handle("/api/", gorest.Handle())
http.Handle("/metrics.json", exporter)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
go http.ListenAndServe(*listenAddress, nil)