mirror of
https://github.com/prometheus/prometheus
synced 2024-12-25 07:52:28 +00:00
First graphing support.
This commit is contained in:
parent
ebabaa46f4
commit
2c8595f96e
@ -7,5 +7,6 @@ import (
|
||||
type MetricsService struct {
|
||||
gorest.RestService `root:"/api/" consumes:"application/json" produces:"application/json"`
|
||||
|
||||
query gorest.EndPoint `method:"GET" path:"/query?{expr:string}&{json:string}&{start:string}&{end:string}" output:"string"`
|
||||
query gorest.EndPoint `method:"GET" path:"/query?{expr:string}&{json:string}" output:"string"`
|
||||
queryRange gorest.EndPoint `method:"GET" path:"/query_range?{expr:string}&{end:int64}&{range:int64}&{step:int64}" output:"string"`
|
||||
}
|
||||
|
39
api/query.go
39
api/query.go
@ -4,10 +4,11 @@ import (
|
||||
"code.google.com/p/gorest"
|
||||
"github.com/matttproud/prometheus/rules"
|
||||
"github.com/matttproud/prometheus/rules/ast"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (serv MetricsService) Query(Expr string, Json string, Start string, End string) (result string) {
|
||||
func (serv MetricsService) Query(Expr string, Json string) (result string) {
|
||||
exprNode, err := rules.LoadExprFromString(Expr)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
@ -27,3 +28,39 @@ func (serv MetricsService) Query(Expr string, Json string, Start string, End str
|
||||
|
||||
return ast.EvalToString(exprNode, ×tamp, format)
|
||||
}
|
||||
|
||||
func (serv MetricsService) QueryRange(Expr string, End int64, Range int64, Step int64) string {
|
||||
exprNode, err := rules.LoadExprFromString(Expr)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if exprNode.Type() != ast.VECTOR {
|
||||
return "Expression does not evaluate to vector type" // TODO return errors correctly everywhere
|
||||
}
|
||||
rb := serv.ResponseBuilder()
|
||||
rb.SetContentType(gorest.Application_Json)
|
||||
|
||||
if End == 0 {
|
||||
End = time.Now().Unix()
|
||||
}
|
||||
|
||||
if Step < 1 {
|
||||
Step = 1
|
||||
}
|
||||
|
||||
if End - Range < 0 {
|
||||
Range = End
|
||||
}
|
||||
|
||||
// Align the start to step "tick" boundary.
|
||||
End -= End % Step
|
||||
|
||||
matrix := ast.EvalVectorRange(
|
||||
exprNode.(ast.VectorNode),
|
||||
time.Unix(End - Range, 0),
|
||||
time.Unix(End, 0),
|
||||
time.Duration(Step) * time.Second)
|
||||
|
||||
sort.Sort(matrix)
|
||||
return ast.TypedValueToJSON(matrix, "matrix")
|
||||
}
|
||||
|
@ -2,9 +2,11 @@ package ast
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/matttproud/prometheus/model"
|
||||
"log"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -216,6 +218,44 @@ func (node *VectorAggregation) labelsToGroupingKey(labels model.Metric) string {
|
||||
return strings.Join(keyParts, ",") // TODO not safe when label value contains comma.
|
||||
}
|
||||
|
||||
func labelsToKey(labels model.Metric) string {
|
||||
keyParts := []string{}
|
||||
for label, value := range labels {
|
||||
keyParts = append(keyParts, fmt.Sprintf("%v='%v'", label, value))
|
||||
}
|
||||
sort.Strings(keyParts)
|
||||
return strings.Join(keyParts, ",") // TODO not safe when label value contains comma.
|
||||
}
|
||||
|
||||
func EvalVectorRange(node VectorNode, start time.Time, end time.Time, step time.Duration) Matrix {
|
||||
// TODO implement watchdog timer for long-running queries.
|
||||
sampleSets := map[string]*model.SampleSet{}
|
||||
for t := start; t.Before(end); t = t.Add(step) {
|
||||
vector := node.Eval(&t)
|
||||
for _, sample := range vector {
|
||||
samplePair := model.SamplePair{
|
||||
Value: sample.Value,
|
||||
Timestamp: sample.Timestamp,
|
||||
}
|
||||
groupingKey := labelsToKey(sample.Metric)
|
||||
if sampleSets[groupingKey] == nil {
|
||||
sampleSets[groupingKey] = &model.SampleSet{
|
||||
Metric: sample.Metric,
|
||||
Values: []model.SamplePair{samplePair},
|
||||
}
|
||||
} else {
|
||||
sampleSets[groupingKey].Values = append(sampleSets[groupingKey].Values, samplePair)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matrix := Matrix{}
|
||||
for _, sampleSet := range sampleSets {
|
||||
matrix = append(matrix, sampleSet)
|
||||
}
|
||||
return matrix
|
||||
}
|
||||
|
||||
func labelIntersection(metric1, metric2 model.Metric) model.Metric {
|
||||
intersection := model.Metric{}
|
||||
for label, value := range metric1 {
|
||||
@ -483,6 +523,19 @@ func (node *MatrixLiteral) EvalBoundaries(timestamp *time.Time) Matrix {
|
||||
return values
|
||||
}
|
||||
|
||||
func (matrix Matrix) Len() int {
|
||||
return len(matrix)
|
||||
}
|
||||
|
||||
func (matrix Matrix) Less(i, j int) bool {
|
||||
return labelsToKey(matrix[i].Metric) < labelsToKey(matrix[j].Metric)
|
||||
}
|
||||
|
||||
func (matrix Matrix) Swap(i, j int) {
|
||||
matrix[i], matrix[j] = matrix[j], matrix[i]
|
||||
}
|
||||
|
||||
|
||||
func (node *StringLiteral) Eval(timestamp *time.Time) string {
|
||||
return node.str
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ func (vector Vector) ToString() string {
|
||||
labelStrings := []string{}
|
||||
for label, value := range sample.Metric {
|
||||
if label != "name" {
|
||||
// TODO escape special chars in label values here and elsewhere.
|
||||
labelStrings = append(labelStrings, fmt.Sprintf("%v='%v'", label, value))
|
||||
}
|
||||
}
|
||||
@ -144,7 +145,7 @@ func errorToJSON(err error) string {
|
||||
return string(errorJSON)
|
||||
}
|
||||
|
||||
func typedValueToJSON(data interface{}, typeStr string) string {
|
||||
func TypedValueToJSON(data interface{}, typeStr string) string {
|
||||
dataStruct := struct {
|
||||
Type string
|
||||
Value interface{}
|
||||
@ -167,7 +168,7 @@ func EvalToString(node Node, timestamp *time.Time, format OutputFormat) string {
|
||||
case TEXT:
|
||||
return fmt.Sprintf("scalar: %v", scalar)
|
||||
case JSON:
|
||||
return typedValueToJSON(scalar, "scalar")
|
||||
return TypedValueToJSON(scalar, "scalar")
|
||||
}
|
||||
case VECTOR:
|
||||
vector := node.(VectorNode).Eval(timestamp)
|
||||
@ -175,7 +176,7 @@ func EvalToString(node Node, timestamp *time.Time, format OutputFormat) string {
|
||||
case TEXT:
|
||||
return vector.ToString()
|
||||
case JSON:
|
||||
return typedValueToJSON(vector, "vector")
|
||||
return TypedValueToJSON(vector, "vector")
|
||||
}
|
||||
case MATRIX:
|
||||
matrix := node.(MatrixNode).Eval(timestamp)
|
||||
@ -183,7 +184,7 @@ func EvalToString(node Node, timestamp *time.Time, format OutputFormat) string {
|
||||
case TEXT:
|
||||
return matrix.ToString()
|
||||
case JSON:
|
||||
return typedValueToJSON(matrix, "matrix")
|
||||
return TypedValueToJSON(matrix, "matrix")
|
||||
}
|
||||
case STRING:
|
||||
str := node.(StringNode).Eval(timestamp)
|
||||
@ -191,7 +192,7 @@ func EvalToString(node Node, timestamp *time.Time, format OutputFormat) string {
|
||||
case TEXT:
|
||||
return str
|
||||
case JSON:
|
||||
return typedValueToJSON(str, "string")
|
||||
return TypedValueToJSON(str, "string")
|
||||
}
|
||||
}
|
||||
panic("Switch didn't cover all node types")
|
||||
|
55
static/graph.html
Normal file
55
static/graph.html
Normal file
@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Prometheus Expression Browser</title>
|
||||
<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="http://code.shutterstock.com/rickshaw/rickshaw.min.css">
|
||||
<script src="http://code.shutterstock.com/rickshaw/vendor/d3.min.js"></script>
|
||||
<script src="http://code.shutterstock.com/rickshaw/vendor/d3.layout.min.js"></script>
|
||||
<script src="http://code.shutterstock.com/rickshaw/rickshaw.min.js"></script>
|
||||
<script src="js/rickshaw.js"></script>
|
||||
|
||||
<style>
|
||||
#chart_container {
|
||||
position: relative;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
#chart {
|
||||
position: relative;
|
||||
left: 40px;
|
||||
}
|
||||
#y_axis {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
}
|
||||
#legend {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form action="/api/query_range" method="GET" id="queryForm">
|
||||
Expression: <input type="text" name="expr" id="expr" size="100"><br>
|
||||
Range: <input type="number" name="range" id="range" value="60">
|
||||
End: <input type="number" name="end" id="end">
|
||||
Resolution (s): <input type="text" name="step" step="5">
|
||||
<input type="submit" value="Graph" id="graph_submit">
|
||||
<img src="img/ajax-loader.gif" id="spinner">
|
||||
<div id="load_time"></div>
|
||||
</form>
|
||||
<div id="chart_container">
|
||||
<div id="y_axis"></div>
|
||||
<div id="chart"></div>
|
||||
<div id="legend"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
static/img/ajax-loader.gif
Normal file
BIN
static/img/ajax-loader.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 847 B |
21
static/index.html
Normal file
21
static/index.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Prometheus Expression Browser</title>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
|
||||
<script src="js/exprBrowser.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form action="/api/query" method="GET" id="queryForm">
|
||||
<input type="text" name="expr" size="100">
|
||||
<input type="checkbox" name="json" id="json" value="JSON"><label for="json">JSON</label>
|
||||
<input type="submit" value="Evaluate">
|
||||
</form>
|
||||
<hr>
|
||||
<pre>
|
||||
<div id="result"></div>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
26
static/js/exprBrowser.js
Normal file
26
static/js/exprBrowser.js
Normal file
@ -0,0 +1,26 @@
|
||||
function submitQuery() {
|
||||
var form = $("#queryForm");
|
||||
|
||||
$.ajax({
|
||||
method: form.attr("method"),
|
||||
url: form.attr("action"),
|
||||
dataType: "html",
|
||||
data: form.serialize(),
|
||||
success: function(data, textStatus) {
|
||||
$("#result").text(data);
|
||||
},
|
||||
error: function() {
|
||||
alert("Error executing query!");
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function bindHandlers() {
|
||||
jQuery.ajaxSetup({
|
||||
cache: false
|
||||
});
|
||||
$("#queryForm").submit(submitQuery);
|
||||
}
|
||||
|
||||
$(bindHandlers);
|
148
static/js/rickshaw.js
Normal file
148
static/js/rickshaw.js
Normal file
@ -0,0 +1,148 @@
|
||||
var url = "http://juliusv.com:9090/api/query?expr=targets_healthy_scrape_latency_ms%5B'10m'%5D&json=JSON";
|
||||
|
||||
// Graph options
|
||||
// Grid off/on
|
||||
// Stacked off/on
|
||||
// Area off/on
|
||||
// Legend position
|
||||
// Short link
|
||||
// Graph title
|
||||
// Palette
|
||||
// Background
|
||||
// Enable tooltips
|
||||
// width/height
|
||||
// Axis options
|
||||
// Y-Range min/max
|
||||
// (X-Range min/max)
|
||||
// X-Axis format
|
||||
// Y-Axis format
|
||||
// Y-Axis title
|
||||
// X-Axis title
|
||||
// Log scale
|
||||
|
||||
var graph = null;
|
||||
var data = [];
|
||||
|
||||
function submitQuery() {
|
||||
$("#spinner").show();
|
||||
$("#load_time").empty();
|
||||
var form = $("#queryForm");
|
||||
var startTime = new Date().getTime();
|
||||
|
||||
$.ajax({
|
||||
method: form.attr("method"),
|
||||
url: form.attr("action"),
|
||||
dataType: "json",
|
||||
data: form.serialize(),
|
||||
success: function(json, textStatus) {
|
||||
data = transformData(json);
|
||||
if (data.length == 0) {
|
||||
alert("No datapoints found.");
|
||||
return;
|
||||
}
|
||||
graph = null;
|
||||
$("#chart").empty();
|
||||
$("#legend").empty();
|
||||
$("#y_axis").empty();
|
||||
showGraph();
|
||||
},
|
||||
error: function() {
|
||||
alert("Error executing query!");
|
||||
},
|
||||
complete: function() {
|
||||
var duration = new Date().getTime() - startTime;
|
||||
$("#load_time").html("Load time: " + duration + "ms");
|
||||
$("#spinner").hide();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function metricToTsName(labels) {
|
||||
var tsName = labels["name"] + "{";
|
||||
var labelStrings = [];
|
||||
for (label in labels) {
|
||||
if (label != "name") {
|
||||
labelStrings.push(label + "='" + labels[label] + "'");
|
||||
}
|
||||
}
|
||||
tsName += labelStrings.join(",") + "}";
|
||||
return tsName;
|
||||
}
|
||||
|
||||
function parseValue(value) {
|
||||
if (value == "NaN") {
|
||||
return 0; // TODO: what to do here?
|
||||
} else {
|
||||
return parseFloat(value)
|
||||
}
|
||||
}
|
||||
|
||||
function transformData(json) {
|
||||
var palette = new Rickshaw.Color.Palette();
|
||||
if (json.Type != "matrix") {
|
||||
alert("Result is not of matrix type!");
|
||||
return [];
|
||||
}
|
||||
var data = json.Value.map(function(ts) {
|
||||
return {
|
||||
name: metricToTsName(ts.Metric),
|
||||
data: ts.Values.map(function(value) {
|
||||
return {
|
||||
x: value.Timestamp,
|
||||
y: parseValue(value.Value)
|
||||
}
|
||||
}),
|
||||
color: palette.color()
|
||||
};
|
||||
var metricStr = ts['name'];
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
function showGraph() {
|
||||
graph = new Rickshaw.Graph( {
|
||||
element: document.querySelector("#chart"),
|
||||
width: 1200,
|
||||
height: 800,
|
||||
renderer: 'line',
|
||||
series: data
|
||||
} );
|
||||
//graph.configure({offset: 'wiggle'});
|
||||
|
||||
var x_axis = new Rickshaw.Graph.Axis.Time( { graph: graph } );
|
||||
|
||||
var y_axis = new Rickshaw.Graph.Axis.Y( {
|
||||
element: document.querySelector("#y_axis"),
|
||||
graph: graph,
|
||||
orientation: 'left',
|
||||
tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
|
||||
} );
|
||||
|
||||
var legend = new Rickshaw.Graph.Legend( {
|
||||
element: document.querySelector('#legend'),
|
||||
graph: graph
|
||||
} );
|
||||
|
||||
graph.render();
|
||||
|
||||
var hoverDetail = new Rickshaw.Graph.HoverDetail( {
|
||||
graph: graph
|
||||
} );
|
||||
|
||||
var shelving = new Rickshaw.Graph.Behavior.Series.Toggle( {
|
||||
graph: graph,
|
||||
legend: legend
|
||||
} );
|
||||
}
|
||||
|
||||
function init() {
|
||||
jQuery.ajaxSetup({
|
||||
cache: false
|
||||
});
|
||||
$("#spinner").hide();
|
||||
$("#queryForm").submit(submitQuery);
|
||||
$("#expr").focus();
|
||||
}
|
||||
|
||||
$(init);
|
Loading…
Reference in New Issue
Block a user