First graphing support.

This commit is contained in:
Julius Volz 2013-01-15 11:30:55 +01:00
parent ebabaa46f4
commit 2c8595f96e
9 changed files with 349 additions and 7 deletions

View File

@ -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"`
}

View File

@ -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, &timestamp, 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")
}

View File

@ -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
}

View File

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

21
static/index.html Normal file
View 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
View 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
View 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);