diff --git a/api/api.go b/api/api.go index 0348879975..e6b584b72d 100644 --- a/api/api.go +++ b/api/api.go @@ -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"` } diff --git a/api/query.go b/api/query.go index 71366bab93..75fde9c68a 100644 --- a/api/query.go +++ b/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") +} diff --git a/rules/ast/ast.go b/rules/ast/ast.go index dfdb36a0b6..746d626fc1 100644 --- a/rules/ast/ast.go +++ b/rules/ast/ast.go @@ -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 } diff --git a/rules/ast/printer.go b/rules/ast/printer.go index 3b7e52cde2..c943ead11b 100644 --- a/rules/ast/printer.go +++ b/rules/ast/printer.go @@ -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") diff --git a/static/graph.html b/static/graph.html new file mode 100644 index 0000000000..8d4291417c --- /dev/null +++ b/static/graph.html @@ -0,0 +1,55 @@ + + + + + Prometheus Expression Browser + + + + + + + + + + + + + + +
+ Expression:
+ Range: + End: + Resolution (s): + + +
+
+
+
+
+
+
+ + diff --git a/static/img/ajax-loader.gif b/static/img/ajax-loader.gif new file mode 100644 index 0000000000..e192ca895c Binary files /dev/null and b/static/img/ajax-loader.gif differ diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000000..2705a18051 --- /dev/null +++ b/static/index.html @@ -0,0 +1,21 @@ + + + + + Prometheus Expression Browser + + + + + +
+ + + +
+
+
+      
+
+ + diff --git a/static/js/exprBrowser.js b/static/js/exprBrowser.js new file mode 100644 index 0000000000..ec04036a9a --- /dev/null +++ b/static/js/exprBrowser.js @@ -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); diff --git a/static/js/rickshaw.js b/static/js/rickshaw.js new file mode 100644 index 0000000000..883485fd8b --- /dev/null +++ b/static/js/rickshaw.js @@ -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);