mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
app/vlselect: add /select/logsql/stats_query_range endpoint for building time series panels in VictoriaLogs plugin for Grafana
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6943 Updates https://github.com/VictoriaMetrics/victorialogs-datasource/issues/61
This commit is contained in:
parent
c9bb4ddeed
commit
0a40064a6f
8 changed files with 550 additions and 10 deletions
|
@ -45,6 +45,7 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||||
}
|
}
|
||||||
if step <= 0 {
|
if step <= 0 {
|
||||||
httpserver.Errorf(w, r, "'step' must be bigger than zero")
|
httpserver.Errorf(w, r, "'step' must be bigger than zero")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain offset
|
// Obtain offset
|
||||||
|
@ -563,6 +564,137 @@ func (tp *tailProcessor) getTailRows() ([][]logstorage.Field, error) {
|
||||||
return tailRows, nil
|
return tailRows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProcessStatsQueryRangeRequest handles /select/logsql/stats_query_range request.
|
||||||
|
//
|
||||||
|
// See https://docs.victoriametrics.com/victorialogs/querying/#querying-log-range-stats
|
||||||
|
func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
q, tenantIDs, err := parseCommonArgs(r)
|
||||||
|
if err != nil {
|
||||||
|
httpserver.SendPrometheusError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain step
|
||||||
|
stepStr := r.FormValue("step")
|
||||||
|
if stepStr == "" {
|
||||||
|
stepStr = "1d"
|
||||||
|
}
|
||||||
|
step, err := promutils.ParseDuration(stepStr)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("cannot parse 'step' arg: %s", err)
|
||||||
|
httpserver.SendPrometheusError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if step <= 0 {
|
||||||
|
err := fmt.Errorf("'step' must be bigger than zero")
|
||||||
|
httpserver.SendPrometheusError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain `by(...)` fields from the last `| stats` pipe in q.
|
||||||
|
// Add `_time:step` to the `by(...)` list.
|
||||||
|
byFields, ok := q.GetStatsByFields(int64(step))
|
||||||
|
if !ok {
|
||||||
|
err := fmt.Errorf("the query must end with '| stats ...'; got [%s]", q)
|
||||||
|
httpserver.SendPrometheusError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q.Optimize()
|
||||||
|
|
||||||
|
m := make(map[string]*statsSeries)
|
||||||
|
var mLock sync.Mutex
|
||||||
|
|
||||||
|
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
|
||||||
|
clonedColumnNames := make([]string, len(columns))
|
||||||
|
for i, c := range columns {
|
||||||
|
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||||
|
}
|
||||||
|
for i := range timestamps {
|
||||||
|
timestamp := q.GetTimestamp()
|
||||||
|
labels := make([]logstorage.Field, 0, len(byFields))
|
||||||
|
for j, c := range columns {
|
||||||
|
if c.Name == "_time" {
|
||||||
|
nsec, ok := logstorage.TryParseTimestampRFC3339Nano(c.Values[i])
|
||||||
|
if ok {
|
||||||
|
timestamp = nsec
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Contains(byFields, c.Name) {
|
||||||
|
labels = append(labels, logstorage.Field{
|
||||||
|
Name: clonedColumnNames[j],
|
||||||
|
Value: strings.Clone(c.Values[i]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dst []byte
|
||||||
|
for j, c := range columns {
|
||||||
|
if !slices.Contains(byFields, c.Name) {
|
||||||
|
name := clonedColumnNames[j]
|
||||||
|
dst = dst[:0]
|
||||||
|
dst = append(dst, name...)
|
||||||
|
dst = logstorage.MarshalFieldsToJSON(dst, labels)
|
||||||
|
key := string(dst)
|
||||||
|
p := statsPoint{
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Value: strings.Clone(c.Values[i]),
|
||||||
|
}
|
||||||
|
|
||||||
|
mLock.Lock()
|
||||||
|
ss := m[key]
|
||||||
|
if ss == nil {
|
||||||
|
ss = &statsSeries{
|
||||||
|
key: key,
|
||||||
|
Name: name,
|
||||||
|
Labels: labels,
|
||||||
|
}
|
||||||
|
m[key] = ss
|
||||||
|
}
|
||||||
|
ss.Points = append(ss.Points, p)
|
||||||
|
mLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := vlstorage.RunQuery(ctx, tenantIDs, q, writeBlock); err != nil {
|
||||||
|
err = fmt.Errorf("cannot execute query [%s]: %s", q, err)
|
||||||
|
httpserver.SendPrometheusError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the collected stats by time
|
||||||
|
rows := make([]*statsSeries, 0, len(m))
|
||||||
|
for _, ss := range m {
|
||||||
|
points := ss.Points
|
||||||
|
sort.Slice(points, func(i, j int) bool {
|
||||||
|
return points[i].Timestamp < points[j].Timestamp
|
||||||
|
})
|
||||||
|
rows = append(rows, ss)
|
||||||
|
}
|
||||||
|
sort.Slice(rows, func(i, j int) bool {
|
||||||
|
return rows[i].key < rows[j].key
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
WriteStatsQueryRangeResponse(w, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
type statsSeries struct {
|
||||||
|
key string
|
||||||
|
|
||||||
|
Name string
|
||||||
|
Labels []logstorage.Field
|
||||||
|
Points []statsPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
type statsPoint struct {
|
||||||
|
Timestamp int64
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessStatsQueryRequest handles /select/logsql/stats_query request.
|
// ProcessStatsQueryRequest handles /select/logsql/stats_query request.
|
||||||
//
|
//
|
||||||
// See https://docs.victoriametrics.com/victorialogs/querying/#querying-log-stats
|
// See https://docs.victoriametrics.com/victorialogs/querying/#querying-log-stats
|
||||||
|
@ -573,8 +705,8 @@ func ProcessStatsQueryRequest(ctx context.Context, w http.ResponseWriter, r *htt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that q ends with `| stats` pipe
|
// Obtain `by(...)` fields from the last `| stats` pipe in q.
|
||||||
byFields, ok := q.GetStatsByFields()
|
byFields, ok := q.GetStatsByFields(0)
|
||||||
if !ok {
|
if !ok {
|
||||||
err := fmt.Errorf("the query must end with '| stats ...'; got [%s]", q)
|
err := fmt.Errorf("the query must end with '| stats ...'; got [%s]", q)
|
||||||
httpserver.SendPrometheusError(w, r, err)
|
httpserver.SendPrometheusError(w, r, err)
|
||||||
|
@ -611,6 +743,7 @@ func ProcessStatsQueryRequest(ctx context.Context, w http.ResponseWriter, r *htt
|
||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
Value: strings.Clone(c.Values[i]),
|
Value: strings.Clone(c.Values[i]),
|
||||||
}
|
}
|
||||||
|
|
||||||
rowsLock.Lock()
|
rowsLock.Lock()
|
||||||
rows = append(rows, r)
|
rows = append(rows, r)
|
||||||
rowsLock.Unlock()
|
rowsLock.Unlock()
|
||||||
|
|
52
app/vlselect/logsql/stats_query_range_response.qtpl
Normal file
52
app/vlselect/logsql/stats_query_range_response.qtpl
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{% stripspace %}
|
||||||
|
|
||||||
|
// StatsQueryRangeResponse generates response for /select/logsql/stats_query_range
|
||||||
|
{% func StatsQueryRangeResponse(rows []*statsSeries) %}
|
||||||
|
{
|
||||||
|
"status":"success",
|
||||||
|
"data":{
|
||||||
|
"resultType":"matrix",
|
||||||
|
"result":[
|
||||||
|
{% if len(rows) > 0 %}
|
||||||
|
{%= formatStatsSeries(rows[0]) %}
|
||||||
|
{% code rows = rows[1:] %}
|
||||||
|
{% for i := range rows %}
|
||||||
|
,{%= formatStatsSeries(rows[i]) %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endfunc %}
|
||||||
|
|
||||||
|
{% func formatStatsSeries(ss *statsSeries) %}
|
||||||
|
{
|
||||||
|
"metric":{
|
||||||
|
"__name__":{%q= ss.Name %}
|
||||||
|
{% if len(ss.Labels) > 0 %}
|
||||||
|
{% for _, label := range ss.Labels %}
|
||||||
|
,{%q= label.Name %}:{%q= label.Value %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
},
|
||||||
|
"values":[
|
||||||
|
{% code points := ss.Points %}
|
||||||
|
{% if len(points) > 0 %}
|
||||||
|
{%= formatStatsPoint(&points[0]) %}
|
||||||
|
{% code points = points[1:] %}
|
||||||
|
{% for i := range points %}
|
||||||
|
,{%= formatStatsPoint(&points[i]) %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
{% endfunc %}
|
||||||
|
|
||||||
|
{% func formatStatsPoint(p *statsPoint) %}
|
||||||
|
[
|
||||||
|
{%f= float64(p.Timestamp)/1e9 %},
|
||||||
|
{%q= p.Value %}
|
||||||
|
]
|
||||||
|
{% endfunc %}
|
||||||
|
|
||||||
|
{% endstripspace %}
|
188
app/vlselect/logsql/stats_query_range_response.qtpl.go
Normal file
188
app/vlselect/logsql/stats_query_range_response.qtpl.go
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
// Code generated by qtc from "stats_query_range_response.qtpl". DO NOT EDIT.
|
||||||
|
// See https://github.com/valyala/quicktemplate for details.
|
||||||
|
|
||||||
|
// StatsQueryRangeResponse generates response for /select/logsql/stats_query_range
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||||
|
package logsql
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||||
|
import (
|
||||||
|
qtio422016 "io"
|
||||||
|
|
||||||
|
qt422016 "github.com/valyala/quicktemplate"
|
||||||
|
)
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||||
|
var (
|
||||||
|
_ = qtio422016.Copy
|
||||||
|
_ = qt422016.AcquireByteBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||||
|
func StreamStatsQueryRangeResponse(qw422016 *qt422016.Writer, rows []*statsSeries) {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||||
|
qw422016.N().S(`{"status":"success","data":{"resultType":"matrix","result":[`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:10
|
||||||
|
if len(rows) > 0 {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:11
|
||||||
|
streamformatStatsSeries(qw422016, rows[0])
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:12
|
||||||
|
rows = rows[1:]
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:13
|
||||||
|
for i := range rows {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:13
|
||||||
|
qw422016.N().S(`,`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:14
|
||||||
|
streamformatStatsSeries(qw422016, rows[i])
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:15
|
||||||
|
}
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:16
|
||||||
|
}
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:16
|
||||||
|
qw422016.N().S(`]}}`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
}
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
func WriteStatsQueryRangeResponse(qq422016 qtio422016.Writer, rows []*statsSeries) {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
StreamStatsQueryRangeResponse(qw422016, rows)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
}
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
func StatsQueryRangeResponse(rows []*statsSeries) string {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
WriteStatsQueryRangeResponse(qb422016, rows)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
return qs422016
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||||
|
}
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:22
|
||||||
|
func streamformatStatsSeries(qw422016 *qt422016.Writer, ss *statsSeries) {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:22
|
||||||
|
qw422016.N().S(`{"metric":{"__name__":`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:25
|
||||||
|
qw422016.N().Q(ss.Name)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:26
|
||||||
|
if len(ss.Labels) > 0 {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:27
|
||||||
|
for _, label := range ss.Labels {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:27
|
||||||
|
qw422016.N().S(`,`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:28
|
||||||
|
qw422016.N().Q(label.Name)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:28
|
||||||
|
qw422016.N().S(`:`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:28
|
||||||
|
qw422016.N().Q(label.Value)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:29
|
||||||
|
}
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:30
|
||||||
|
}
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:30
|
||||||
|
qw422016.N().S(`},"values":[`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:33
|
||||||
|
points := ss.Points
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:34
|
||||||
|
if len(points) > 0 {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:35
|
||||||
|
streamformatStatsPoint(qw422016, &points[0])
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:36
|
||||||
|
points = points[1:]
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:37
|
||||||
|
for i := range points {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:37
|
||||||
|
qw422016.N().S(`,`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:38
|
||||||
|
streamformatStatsPoint(qw422016, &points[i])
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:39
|
||||||
|
}
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:40
|
||||||
|
}
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:40
|
||||||
|
qw422016.N().S(`]}`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
}
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
func writeformatStatsSeries(qq422016 qtio422016.Writer, ss *statsSeries) {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
streamformatStatsSeries(qw422016, ss)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
}
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
func formatStatsSeries(ss *statsSeries) string {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
writeformatStatsSeries(qb422016, ss)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
return qs422016
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||||
|
}
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:45
|
||||||
|
func streamformatStatsPoint(qw422016 *qt422016.Writer, p *statsPoint) {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:45
|
||||||
|
qw422016.N().S(`[`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:47
|
||||||
|
qw422016.N().F(float64(p.Timestamp) / 1e9)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:47
|
||||||
|
qw422016.N().S(`,`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:48
|
||||||
|
qw422016.N().Q(p.Value)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:48
|
||||||
|
qw422016.N().S(`]`)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
}
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
func writeformatStatsPoint(qq422016 qtio422016.Writer, p *statsPoint) {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
streamformatStatsPoint(qw422016, p)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
}
|
||||||
|
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
func formatStatsPoint(p *statsPoint) string {
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
writeformatStatsPoint(qb422016, p)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
return qs422016
|
||||||
|
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||||
|
}
|
|
@ -197,6 +197,10 @@ func processSelectRequest(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||||
logsqlStatsQueryRequests.Inc()
|
logsqlStatsQueryRequests.Inc()
|
||||||
logsql.ProcessStatsQueryRequest(ctx, w, r)
|
logsql.ProcessStatsQueryRequest(ctx, w, r)
|
||||||
return true
|
return true
|
||||||
|
case "/select/logsql/stats_query_range":
|
||||||
|
logsqlStatsQueryRangeRequests.Inc()
|
||||||
|
logsql.ProcessStatsQueryRangeRequest(ctx, w, r)
|
||||||
|
return true
|
||||||
case "/select/logsql/stream_field_names":
|
case "/select/logsql/stream_field_names":
|
||||||
logsqlStreamFieldNamesRequests.Inc()
|
logsqlStreamFieldNamesRequests.Inc()
|
||||||
logsql.ProcessStreamFieldNamesRequest(ctx, w, r)
|
logsql.ProcessStreamFieldNamesRequest(ctx, w, r)
|
||||||
|
@ -237,6 +241,7 @@ var (
|
||||||
logsqlHitsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/hits"}`)
|
logsqlHitsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/hits"}`)
|
||||||
logsqlQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/query"}`)
|
logsqlQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/query"}`)
|
||||||
logsqlStatsQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query"}`)
|
logsqlStatsQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query"}`)
|
||||||
|
logsqlStatsQueryRangeRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query_range"}`)
|
||||||
logsqlStreamFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_names"}`)
|
logsqlStreamFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_names"}`)
|
||||||
logsqlStreamFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_values"}`)
|
logsqlStreamFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_values"}`)
|
||||||
logsqlStreamIDsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_ids"}`)
|
logsqlStreamIDsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_ids"}`)
|
||||||
|
|
|
@ -16,6 +16,7 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
|
||||||
## tip
|
## tip
|
||||||
|
|
||||||
* FEATURE: add [`/select/logsql/stats_query` HTTP API](https://docs.victoriametrics.com/victorialogs/querying/#querying-log-stats), which is going to be used by [vmalert](https://docs.victoriametrics.com/vmalert/) for executing alerting and recording rules against VictoriaLogs. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6942) for details.
|
* FEATURE: add [`/select/logsql/stats_query` HTTP API](https://docs.victoriametrics.com/victorialogs/querying/#querying-log-stats), which is going to be used by [vmalert](https://docs.victoriametrics.com/vmalert/) for executing alerting and recording rules against VictoriaLogs. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6942) for details.
|
||||||
|
* FEATURE: add [`/select/logsql/stats_query_range` HTTP API](https://docs.victoriametrics.com/victorialogs/querying/#querying-log-range-stats), which is going to be used by [VictoriaLogs plugin for Grafana](https://docs.victoriametrics.com/victorialogs/victorialogs-datasource/) for building time series panels. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6943) for details.
|
||||||
* FEATURE: optimize [multi-exact queries](https://docs.victoriametrics.com/victorialogs/logsql/#multi-exact-filter) with many phrases to search. For example, `ip:in(path:="/foo/bar" | keep ip)` when there are many unique values for `ip` field among log entries with `/foo/bar` path.
|
* FEATURE: optimize [multi-exact queries](https://docs.victoriametrics.com/victorialogs/logsql/#multi-exact-filter) with many phrases to search. For example, `ip:in(path:="/foo/bar" | keep ip)` when there are many unique values for `ip` field among log entries with `/foo/bar` path.
|
||||||
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add support for displaying the top 5 log streams in the hits graph. The remaining log streams are grouped into an "other" label. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6545).
|
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add support for displaying the top 5 log streams in the hits graph. The remaining log streams are grouped into an "other" label. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6545).
|
||||||
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add the ability to customize the graph display with options for bar, line, stepped line, and points.
|
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add the ability to customize the graph display with options for bar, line, stepped line, and points.
|
||||||
|
|
|
@ -14,6 +14,7 @@ VictoriaLogs provides the following HTTP endpoints:
|
||||||
- [`/select/logsql/tail`](#live-tailing) for live tailing of query results.
|
- [`/select/logsql/tail`](#live-tailing) for live tailing of query results.
|
||||||
- [`/select/logsql/hits`](#querying-hits-stats) for querying log hits stats over the given time range.
|
- [`/select/logsql/hits`](#querying-hits-stats) for querying log hits stats over the given time range.
|
||||||
- [`/select/logsql/stats_query`](#querying-log-stats) for querying log stats at the given time.
|
- [`/select/logsql/stats_query`](#querying-log-stats) for querying log stats at the given time.
|
||||||
|
- [`/select/logsql/stats_query_range`](#querying-log-range-stats) for querying log stats over the given time range.
|
||||||
- [`/select/logsql/stream_ids`](#querying-stream_ids) for querying `_stream_id` values of [log streams](#https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields).
|
- [`/select/logsql/stream_ids`](#querying-stream_ids) for querying `_stream_id` values of [log streams](#https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields).
|
||||||
- [`/select/logsql/streams`](#querying-streams) for querying [log streams](#https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields).
|
- [`/select/logsql/streams`](#querying-streams) for querying [log streams](#https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields).
|
||||||
- [`/select/logsql/stream_field_names`](#querying-stream-field-names) for querying [log stream](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields) field names.
|
- [`/select/logsql/stream_field_names`](#querying-stream-field-names) for querying [log stream](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields) field names.
|
||||||
|
@ -107,6 +108,7 @@ See also:
|
||||||
- [Live tailing](#live-tailing)
|
- [Live tailing](#live-tailing)
|
||||||
- [Querying hits stats](#querying-hits-stats)
|
- [Querying hits stats](#querying-hits-stats)
|
||||||
- [Querying log stats](#querying-log-stats)
|
- [Querying log stats](#querying-log-stats)
|
||||||
|
- [Querying log range stats](#querying-log-range-stats)
|
||||||
- [Querying streams](#querying-streams)
|
- [Querying streams](#querying-streams)
|
||||||
- [Querying stream field names](#querying-stream-field-names)
|
- [Querying stream field names](#querying-stream-field-names)
|
||||||
- [Querying stream field values](#querying-stream-field-values)
|
- [Querying stream field values](#querying-stream-field-values)
|
||||||
|
@ -276,6 +278,7 @@ See also:
|
||||||
|
|
||||||
- [Querying logs](#querying-logs)
|
- [Querying logs](#querying-logs)
|
||||||
- [Querying log stats](#querying-log-stats)
|
- [Querying log stats](#querying-log-stats)
|
||||||
|
- [Querying log range stats](#querying-log-range-stats)
|
||||||
- [Querying streams](#querying-streams)
|
- [Querying streams](#querying-streams)
|
||||||
- [HTTP API](#http-api)
|
- [HTTP API](#http-api)
|
||||||
|
|
||||||
|
@ -285,12 +288,12 @@ VictoriaLogs provides `/select/logsql/stats_query?query=<query>&time=<t>` HTTP e
|
||||||
for the given [`query`](https://docs.victoriametrics.com/victorialogs/logsql/) at the given timestamp `t`
|
for the given [`query`](https://docs.victoriametrics.com/victorialogs/logsql/) at the given timestamp `t`
|
||||||
in the format compatible with [Prometheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries).
|
in the format compatible with [Prometheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries).
|
||||||
|
|
||||||
The `<t>` arg can contain values in [any supported format](https://docs.victoriametrics.com/#timestamp-formats).
|
|
||||||
If `<t>` is missing, then it equals to the current time.
|
|
||||||
|
|
||||||
The `<query>` must contain [`stats` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-pipe). The calculated stats is converted into metrics
|
The `<query>` must contain [`stats` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-pipe). The calculated stats is converted into metrics
|
||||||
with labels enumerated in `by(...)` clause of the `| stats by(...)` pipe.
|
with labels enumerated in `by(...)` clause of the `| stats by(...)` pipe.
|
||||||
|
|
||||||
|
The `<t>` arg can contain values in [any supported format](https://docs.victoriametrics.com/#timestamp-formats).
|
||||||
|
If `<t>` is missing, then it equals to the current time.
|
||||||
|
|
||||||
For example, the following command returns the number of logs per each `level` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
|
For example, the following command returns the number of logs per each `level` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
|
||||||
across logs over `2024-01-01` day by UTC:
|
across logs over `2024-01-01` day by UTC:
|
||||||
|
|
||||||
|
@ -345,6 +348,100 @@ The `/select/logsql/stats_query` API is useful for generating Prometheus-compati
|
||||||
|
|
||||||
See also:
|
See also:
|
||||||
|
|
||||||
|
- [Querying log range stats](#querying-log-range-stats)
|
||||||
|
- [Querying logs](#querying-logs)
|
||||||
|
- [Querying hits stats](#querying-hits-stats)
|
||||||
|
- [HTTP API](#http-api)
|
||||||
|
|
||||||
|
### Querying log range stats
|
||||||
|
|
||||||
|
VictoriaLogs provides `/select/logsql/stats_query_range?query=<query>&start=<start>&end=<end>&step=<step>` HTTP endpoint, which returns log stats
|
||||||
|
for the given [`query`](https://docs.victoriametrics.com/victorialogs/logsql/) on the given `[start ... end]` time range with the given `step` interval.
|
||||||
|
The stats is returned in the format compatible with [Prometheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries).
|
||||||
|
|
||||||
|
The `<query>` must contain [`stats` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-pipe). The calculated stats is converted into metrics
|
||||||
|
with labels enumerated in `by(...)` clause of the `| stats by(...)` pipe.
|
||||||
|
|
||||||
|
The `<start>` and `<end>` args can contain values in [any supported format](https://docs.victoriametrics.com/#timestamp-formats).
|
||||||
|
If `<start>` is missing, then it equals to the minimum timestamp across logs stored in VictoriaLogs.
|
||||||
|
If `<end>` is missing, then it equals to the maximum timestamp across logs stored in VictoriaLogs.
|
||||||
|
|
||||||
|
The `<step>` arg can contain values in [the format specified here](https://docs.victoriametrics.com/victorialogs/logsql/#stats-by-time-buckets).
|
||||||
|
If `<step>` is missing, then it equals to `1d` (one day).
|
||||||
|
|
||||||
|
For example, the following command returns the number of logs per each `level` [field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model)
|
||||||
|
across logs over `2024-01-01` day by UTC with 6-hour granularity:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl http://localhost:9428/select/logsql/stats_query_range -d 'query=* | stats by (level) count(*)' -d 'start=2024-01-01' -d 'end=2024-01-02' -d 'step=6h'
|
||||||
|
```
|
||||||
|
|
||||||
|
Below is an example JSON output returned from this endpoint:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"resultType": "matrix",
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"metric": {
|
||||||
|
"__name__": "count(*)",
|
||||||
|
"level": "info"
|
||||||
|
},
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
1704067200,
|
||||||
|
"103125"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1704088800,
|
||||||
|
"102500"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1704110400,
|
||||||
|
"103125"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1704132000,
|
||||||
|
"102500"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metric": {
|
||||||
|
"__name__": "count(*)",
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
1704067200,
|
||||||
|
"31"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1704088800,
|
||||||
|
"25"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1704110400,
|
||||||
|
"31"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1704132000,
|
||||||
|
"125"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `/select/logsql/stats_query_range` API is useful for generating Prometheus-compatible graphs in Grafana.
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
- [Querying log stats](#querying-log-stats)
|
||||||
- [Querying logs](#querying-logs)
|
- [Querying logs](#querying-logs)
|
||||||
- [Querying hits stats](#querying-hits-stats)
|
- [Querying hits stats](#querying-hits-stats)
|
||||||
- [HTTP API](#http-api)
|
- [HTTP API](#http-api)
|
||||||
|
|
|
@ -454,10 +454,12 @@ func (q *Query) Optimize() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStatsByFields returns `| stats by (...)` fields from q if q contains safe `| stats ...` pipe in the end.
|
// GetStatsByFields returns `by (...)` fields from the last `stats` pipe at q.
|
||||||
|
//
|
||||||
|
// If step > 0, then _time:step field is added to the last `stats by(...)` pipe at q.
|
||||||
//
|
//
|
||||||
// False is returned if q doesn't contain safe `| stats ...` pipe.
|
// False is returned if q doesn't contain safe `| stats ...` pipe.
|
||||||
func (q *Query) GetStatsByFields() ([]string, bool) {
|
func (q *Query) GetStatsByFields(step int64) ([]string, bool) {
|
||||||
pipes := q.pipes
|
pipes := q.pipes
|
||||||
|
|
||||||
idx := getLastPipeStatsIdx(pipes)
|
idx := getLastPipeStatsIdx(pipes)
|
||||||
|
@ -465,8 +467,13 @@ func (q *Query) GetStatsByFields() ([]string, bool) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ps := pipes[idx].(*pipeStats)
|
||||||
|
|
||||||
|
// add _time:step to ps.byFields if it doesn't contain it yet.
|
||||||
|
ps.byFields = addByTimeField(ps.byFields, step)
|
||||||
|
|
||||||
// extract by(...) field names from stats pipe
|
// extract by(...) field names from stats pipe
|
||||||
byFields := pipes[idx].(*pipeStats).byFields
|
byFields := ps.byFields
|
||||||
fields := make([]string, len(byFields))
|
fields := make([]string, len(byFields))
|
||||||
for i, f := range byFields {
|
for i, f := range byFields {
|
||||||
fields[i] = f.name
|
fields[i] = f.name
|
||||||
|
@ -525,6 +532,34 @@ func getLastPipeStatsIdx(pipes []pipe) int {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addByTimeField(byFields []*byStatsField, step int64) []*byStatsField {
|
||||||
|
if step <= 0 {
|
||||||
|
return byFields
|
||||||
|
}
|
||||||
|
stepStr := fmt.Sprintf("%d", step)
|
||||||
|
dstFields := make([]*byStatsField, 0, len(byFields)+1)
|
||||||
|
hasByTime := false
|
||||||
|
for _, f := range byFields {
|
||||||
|
if f.name == "_time" {
|
||||||
|
f = &byStatsField{
|
||||||
|
name: "_time",
|
||||||
|
bucketSizeStr: stepStr,
|
||||||
|
bucketSize: float64(step),
|
||||||
|
}
|
||||||
|
hasByTime = true
|
||||||
|
}
|
||||||
|
dstFields = append(dstFields, f)
|
||||||
|
}
|
||||||
|
if !hasByTime {
|
||||||
|
dstFields = append(dstFields, &byStatsField{
|
||||||
|
name: "_time",
|
||||||
|
bucketSizeStr: stepStr,
|
||||||
|
bucketSize: float64(step),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dstFields
|
||||||
|
}
|
||||||
|
|
||||||
func removeStarFilters(f filter) filter {
|
func removeStarFilters(f filter) filter {
|
||||||
visitFunc := func(f filter) bool {
|
visitFunc := func(f filter) bool {
|
||||||
fp, ok := f.(*filterPrefix)
|
fp, ok := f.(*filterPrefix)
|
||||||
|
|
|
@ -2101,6 +2101,35 @@ func TestQueryDropAllPipes(t *testing.T) {
|
||||||
f(`foo | filter bar:baz | stats by (x) min(y)`, `foo bar:baz`)
|
f(`foo | filter bar:baz | stats by (x) min(y)`, `foo bar:baz`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQueryGetStatsByFields_PositiveStep(t *testing.T) {
|
||||||
|
f := func(qStr string, step int64, fieldsExpected []string, qExpected string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
q, err := ParseQuery(qStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot parse [%s]: %s", qStr, err)
|
||||||
|
}
|
||||||
|
fields, ok := q.GetStatsByFields(step)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("cannot obtain byFields from the query [%s]", qStr)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(fields, fieldsExpected) {
|
||||||
|
t.Fatalf("unexpected byFields;\ngot\n%q\nwant\n%q", fields, fieldsExpected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the resulting query
|
||||||
|
qResult := q.String()
|
||||||
|
if qResult != qExpected {
|
||||||
|
t.Fatalf("unexpected query\ngot\n%s\nwant\n%s", qResult, qExpected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f(`* | count()`, nsecsPerHour, []string{"_time"}, `* | stats by (_time:3600000000000) count(*) as "count(*)"`)
|
||||||
|
f(`* | by (level) count() x`, nsecsPerDay, []string{"level", "_time"}, `* | stats by (level, _time:86400000000000) count(*) as x`)
|
||||||
|
f(`* | by (_time:1m) count() x`, nsecsPerDay, []string{"_time"}, `* | stats by (_time:86400000000000) count(*) as x`)
|
||||||
|
f(`* | by (_time:1m offset 30s,level) count() x, count_uniq(z) y`, nsecsPerDay, []string{"_time", "level"}, `* | stats by (_time:86400000000000, level) count(*) as x, count_uniq(z) as y`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestQueryGetStatsByFields_Success(t *testing.T) {
|
func TestQueryGetStatsByFields_Success(t *testing.T) {
|
||||||
f := func(qStr string, fieldsExpected []string) {
|
f := func(qStr string, fieldsExpected []string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
@ -2109,7 +2138,7 @@ func TestQueryGetStatsByFields_Success(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("cannot parse [%s]: %s", qStr, err)
|
t.Fatalf("cannot parse [%s]: %s", qStr, err)
|
||||||
}
|
}
|
||||||
fields, ok := q.GetStatsByFields()
|
fields, ok := q.GetStatsByFields(0)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("cannot obtain byFields from the query [%s]", qStr)
|
t.Fatalf("cannot obtain byFields from the query [%s]", qStr)
|
||||||
}
|
}
|
||||||
|
@ -2156,7 +2185,7 @@ func TestQueryGetStatsByFields_Failure(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("cannot parse [%s]: %s", qStr, err)
|
t.Fatalf("cannot parse [%s]: %s", qStr, err)
|
||||||
}
|
}
|
||||||
fields, ok := q.GetStatsByFields()
|
fields, ok := q.GetStatsByFields(0)
|
||||||
if ok {
|
if ok {
|
||||||
t.Fatalf("expecting failure to get byFields for the query [%s]", qStr)
|
t.Fatalf("expecting failure to get byFields for the query [%s]", qStr)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue