Files
akvorado/console/sankey_test.go
Vincent Bernat 67703cc61e console: use templates to build SQL query
This is needed if we want to be able to mix use of several tables
inside a single query (for example, flows_1m0s for a part of the query
and flows_5m0s for another part to overlay historical data).

Also, the way we handle time buckets is now cleaner. The previous way
had two stages of rounding and was incorrect. We were discarding the
first and last value for this reason. The new way only has one stage
of rounding and is correct. It tries hard to align the buckets at the
specified start time. We don't need to discard these values anymore.
We still discard the last one because it could be incomplete (when end
is "now").
2022-08-09 11:45:40 +02:00

299 lines
11 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package console
import (
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/golang/mock/gomock"
"akvorado/common/helpers"
)
func TestSankeyQuerySQL(t *testing.T) {
cases := []struct {
Description string
Input sankeyHandlerInput
Expected string
}{
{
Description: "two dimensions, no filters, l3 bps",
Input: sankeyHandlerInput{
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName},
Limit: 5,
Filter: queryFilter{},
Units: "l3bps",
},
Expected: `
{{ with context @@{"start":"2022-04-10T15:45:10Z","end":"2022-04-11T15:45:10Z","points":20,"units":"l3bps"}@@ }}
WITH
(SELECT MAX(TimeReceived) - MIN(TimeReceived) FROM {{ .Table }} WHERE {{ .Timefilter }}) AS range,
rows AS (SELECT SrcAS, ExporterName FROM {{ .Table }} WHERE {{ .Timefilter }} GROUP BY SrcAS, ExporterName ORDER BY SUM(Bytes) DESC LIMIT 5)
SELECT
{{ .Units }}/range AS xps,
[if(SrcAS IN (SELECT SrcAS FROM rows), concat(toString(SrcAS), ': ', dictGetOrDefault('asns', 'name', SrcAS, '???')), 'Other'),
if(ExporterName IN (SELECT ExporterName FROM rows), ExporterName, 'Other')] AS dimensions
FROM {{ .Table }}
WHERE {{ .Timefilter }}
GROUP BY dimensions
ORDER BY xps DESC
{{ end }}`,
}, {
Description: "two dimensions, no filters, l2 bps",
Input: sankeyHandlerInput{
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName},
Limit: 5,
Filter: queryFilter{},
Units: "l2bps",
},
Expected: `
{{ with context @@{"start":"2022-04-10T15:45:10Z","end":"2022-04-11T15:45:10Z","points":20,"units":"l2bps"}@@ }}
WITH
(SELECT MAX(TimeReceived) - MIN(TimeReceived) FROM {{ .Table }} WHERE {{ .Timefilter }}) AS range,
rows AS (SELECT SrcAS, ExporterName FROM {{ .Table }} WHERE {{ .Timefilter }} GROUP BY SrcAS, ExporterName ORDER BY SUM(Bytes) DESC LIMIT 5)
SELECT
{{ .Units }}/range AS xps,
[if(SrcAS IN (SELECT SrcAS FROM rows), concat(toString(SrcAS), ': ', dictGetOrDefault('asns', 'name', SrcAS, '???')), 'Other'),
if(ExporterName IN (SELECT ExporterName FROM rows), ExporterName, 'Other')] AS dimensions
FROM {{ .Table }}
WHERE {{ .Timefilter }}
GROUP BY dimensions
ORDER BY xps DESC
{{ end }}
`,
}, {
Description: "two dimensions, no filters, pps",
Input: sankeyHandlerInput{
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName},
Limit: 5,
Filter: queryFilter{},
Units: "pps",
},
Expected: `
{{ with context @@{"start":"2022-04-10T15:45:10Z","end":"2022-04-11T15:45:10Z","points":20,"units":"pps"}@@ }}
WITH
(SELECT MAX(TimeReceived) - MIN(TimeReceived) FROM {{ .Table }} WHERE {{ .Timefilter }}) AS range,
rows AS (SELECT SrcAS, ExporterName FROM {{ .Table }} WHERE {{ .Timefilter }} GROUP BY SrcAS, ExporterName ORDER BY SUM(Bytes) DESC LIMIT 5)
SELECT
{{ .Units }}/range AS xps,
[if(SrcAS IN (SELECT SrcAS FROM rows), concat(toString(SrcAS), ': ', dictGetOrDefault('asns', 'name', SrcAS, '???')), 'Other'),
if(ExporterName IN (SELECT ExporterName FROM rows), ExporterName, 'Other')] AS dimensions
FROM {{ .Table }}
WHERE {{ .Timefilter }}
GROUP BY dimensions
ORDER BY xps DESC
{{ end }}`,
}, {
Description: "two dimensions, with filter",
Input: sankeyHandlerInput{
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName},
Limit: 10,
Filter: queryFilter{Filter: "DstCountry = 'FR'"},
Units: "l3bps",
},
Expected: `
{{ with context @@{"start":"2022-04-10T15:45:10Z","end":"2022-04-11T15:45:10Z","points":20,"units":"l3bps"}@@ }}
WITH
(SELECT MAX(TimeReceived) - MIN(TimeReceived) FROM {{ .Table }} WHERE {{ .Timefilter }} AND (DstCountry = 'FR')) AS range,
rows AS (SELECT SrcAS, ExporterName FROM {{ .Table }} WHERE {{ .Timefilter }} AND (DstCountry = 'FR') GROUP BY SrcAS, ExporterName ORDER BY SUM(Bytes) DESC LIMIT 10)
SELECT
{{ .Units }}/range AS xps,
[if(SrcAS IN (SELECT SrcAS FROM rows), concat(toString(SrcAS), ': ', dictGetOrDefault('asns', 'name', SrcAS, '???')), 'Other'),
if(ExporterName IN (SELECT ExporterName FROM rows), ExporterName, 'Other')] AS dimensions
FROM {{ .Table }}
WHERE {{ .Timefilter }} AND (DstCountry = 'FR')
GROUP BY dimensions
ORDER BY xps DESC
{{ end }}`,
},
}
for _, tc := range cases {
tc.Expected = strings.ReplaceAll(tc.Expected, "@@", "`")
t.Run(tc.Description, func(t *testing.T) {
got, _ := tc.Input.toSQL()
if diff := helpers.Diff(strings.Split(strings.TrimSpace(got), "\n"),
strings.Split(strings.TrimSpace(tc.Expected), "\n")); diff != "" {
t.Errorf("toSQL (-got, +want):\n%s", diff)
}
})
}
}
func TestSankeyHandler(t *testing.T) {
_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
expectedSQL := []struct {
Xps float64 `ch:"xps"`
Dimensions []string `ch:"dimensions"`
}{
// [(random.randrange(100, 10000), x)
// for x in set([(random.choice(asn),
// random.choice(providers),
// random.choice(routers)) for x in range(30)])]
{9677, []string{"AS100", "Other", "router1"}},
{9472, []string{"AS300", "provider1", "Other"}},
{7593, []string{"AS300", "provider2", "router1"}},
{7234, []string{"AS200", "provider1", "Other"}},
{6006, []string{"AS100", "provider1", "Other"}},
{5988, []string{"Other", "provider1", "Other"}},
{4675, []string{"AS200", "provider3", "Other"}},
{4348, []string{"AS200", "Other", "router2"}},
{3999, []string{"AS100", "provider3", "Other"}},
{3978, []string{"AS100", "provider3", "router2"}},
{3623, []string{"Other", "Other", "router1"}},
{3080, []string{"AS300", "provider3", "router2"}},
{2915, []string{"AS300", "Other", "router1"}},
{2623, []string{"AS100", "provider1", "router1"}},
{2482, []string{"AS200", "provider2", "router2"}},
{2234, []string{"AS100", "provider2", "Other"}},
{1360, []string{"AS200", "Other", "router1"}},
{975, []string{"AS300", "Other", "Other"}},
{717, []string{"AS200", "provider3", "router2"}},
{621, []string{"Other", "Other", "Other"}},
{159, []string{"Other", "provider1", "router1"}},
}
mockConn.EXPECT().
Select(gomock.Any(), gomock.Any(), gomock.Any()).
SetArg(1, expectedSQL).
Return(nil)
helpers.TestHTTPEndpoints(t, h.Address, helpers.HTTPEndpointCases{
{
URL: "/api/v0/console/sankey",
JSONInput: gin.H{
"start": time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
"end": time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
"dimensions": []string{"SrcAS", "InIfProvider", "ExporterName"},
"limit": 10,
"filter": "DstCountry = 'FR'",
"units": "l3bps",
},
JSONOutput: gin.H{
// Raw data
"rows": [][]string{
{"AS100", "Other", "router1"},
{"AS300", "provider1", "Other"},
{"AS300", "provider2", "router1"},
{"AS200", "provider1", "Other"},
{"AS100", "provider1", "Other"},
{"Other", "provider1", "Other"},
{"AS200", "provider3", "Other"},
{"AS200", "Other", "router2"},
{"AS100", "provider3", "Other"},
{"AS100", "provider3", "router2"},
{"Other", "Other", "router1"},
{"AS300", "provider3", "router2"},
{"AS300", "Other", "router1"},
{"AS100", "provider1", "router1"},
{"AS200", "provider2", "router2"},
{"AS100", "provider2", "Other"},
{"AS200", "Other", "router1"},
{"AS300", "Other", "Other"},
{"AS200", "provider3", "router2"},
{"Other", "Other", "Other"},
{"Other", "provider1", "router1"},
},
"xps": []int{
9677,
9472,
7593,
7234,
6006,
5988,
4675,
4348,
3999,
3978,
3623,
3080,
2915,
2623,
2482,
2234,
1360,
975,
717,
621,
159,
},
// For graph
"nodes": []string{
"SrcAS: AS100",
"InIfProvider: Other",
"ExporterName: router1",
"SrcAS: AS300",
"InIfProvider: provider1",
"ExporterName: Other",
"InIfProvider: provider2",
"SrcAS: AS200",
"SrcAS: Other",
"InIfProvider: provider3",
"ExporterName: router2",
},
"links": []gin.H{
{"source": "InIfProvider: provider1", "target": "ExporterName: Other",
"xps": 9472 + 7234 + 6006 + 5988},
{"source": "InIfProvider: Other", "target": "ExporterName: router1",
"xps": 9677 + 3623 + 2915 + 1360},
{"source": "SrcAS: AS100", "target": "InIfProvider: Other",
"xps": 9677},
{"source": "SrcAS: AS300", "target": "InIfProvider: provider1",
"xps": 9472},
{"source": "InIfProvider: provider3", "target": "ExporterName: Other",
"xps": 4675 + 3999},
{"source": "SrcAS: AS100", "target": "InIfProvider: provider1",
"xps": 6006 + 2623},
{"source": "SrcAS: AS100", "target": "InIfProvider: provider3",
"xps": 3999 + 3978},
{"source": "InIfProvider: provider3", "target": "ExporterName: router2",
"xps": 3978 + 3080 + 717},
{"source": "InIfProvider: provider2", "target": "ExporterName: router1",
"xps": 7593},
{"source": "SrcAS: AS300", "target": "InIfProvider: provider2",
"xps": 7593},
{"source": "SrcAS: AS200", "target": "InIfProvider: provider1",
"xps": 7234},
{"source": "SrcAS: Other", "target": "InIfProvider: provider1",
"xps": 5988 + 159},
{"source": "SrcAS: AS200", "target": "InIfProvider: Other",
"xps": 4348 + 1360},
{"source": "SrcAS: AS200", "target": "InIfProvider: provider3",
"xps": 4675 + 717},
{"source": "InIfProvider: Other", "target": "ExporterName: router2",
"xps": 4348},
{"source": "SrcAS: Other", "target": "InIfProvider: Other",
"xps": 3623 + 621},
{"source": "SrcAS: AS300", "target": "InIfProvider: Other",
"xps": 2915 + 975},
{"source": "SrcAS: AS300", "target": "InIfProvider: provider3",
"xps": 3080},
{"source": "InIfProvider: provider1", "target": "ExporterName: router1",
"xps": 2623 + 159},
{"source": "InIfProvider: provider2", "target": "ExporterName: router2",
"xps": 2482},
{"source": "SrcAS: AS200", "target": "InIfProvider: provider2",
"xps": 2482},
{"source": "InIfProvider: provider2", "target": "ExporterName: Other",
"xps": 2234},
{"source": "SrcAS: AS100", "target": "InIfProvider: provider2",
"xps": 2234},
{"source": "InIfProvider: Other", "target": "ExporterName: Other",
"xps": 975 + 621},
},
},
},
})
}