Files
akvorado/console/line_test.go
Gerhard Bogner 11749a17fb
Some checks failed
CI / 🤖 Check dependabot status (push) Has been cancelled
CI / 🐧 Test on Linux (${{ github.ref_type == 'tag' }}, misc) (push) Has been cancelled
CI / 🐧 Test on Linux (coverage) (push) Has been cancelled
CI / 🐧 Test on Linux (regular) (push) Has been cancelled
CI / ❄️ Build on Nix (push) Has been cancelled
CI / 🍏 Build and test on macOS (push) Has been cancelled
CI / 🧪 End-to-end testing (push) Has been cancelled
CI / 🔍 Upload code coverage (push) Has been cancelled
CI / 🔬 Test only Go (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 20) (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 22) (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 24) (push) Has been cancelled
CI / ⚖️ Check licenses (push) Has been cancelled
CI / 🐋 Build Docker images (push) Has been cancelled
CI / 🐋 Tag Docker images (push) Has been cancelled
CI / 🚀 Publish release (push) Has been cancelled
Update Nix dependency hashes / Update dependency hashes (push) Has been cancelled
console: fix index 95th percentile (#2021)
* fix index 95th percentile

* go fmt, update expected 95th percentile test results
2025-10-13 18:43:53 +02:00

1706 lines
50 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package console
import (
"fmt"
"testing"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/mock/gomock"
"akvorado/common/helpers"
"akvorado/common/schema"
"akvorado/console/query"
)
func TestGraphLineInputReverseDirection(t *testing.T) {
input := graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
schema: schema.NewMock(t),
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: query.Columns{
query.NewColumn("ExporterName"),
query.NewColumn("InIfProvider"),
},
Filter: query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"),
Units: "l3bps",
},
Points: 100,
}
original1 := fmt.Sprintf("%+v", input)
expected := graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: query.Columns{
query.NewColumn("ExporterName"),
query.NewColumn("OutIfProvider"),
},
Filter: query.NewFilter("SrcCountry = 'FR' AND DstCountry = 'US'"),
Units: "l3bps",
},
Points: 100,
}
input.Filter.Validate(input.schema)
expected.Filter.Validate(input.schema)
query.Columns(input.Dimensions).Validate(input.schema)
query.Columns(expected.Dimensions).Validate(input.schema)
got := input.reverseDirection()
original2 := fmt.Sprintf("%+v", input)
if diff := helpers.Diff(got, expected); diff != "" {
t.Fatalf("reverseDirection() (-got, +want):\n%s", diff)
}
if original1 != original2 {
t.Fatalf("reverseDirection() modified original to:\n-%s\n+%s", original1, original2)
}
}
func TestGraphPreviousPeriod(t *testing.T) {
const longForm = "Jan 2, 2006 at 15:04"
cases := []struct {
Pos helpers.Pos
Start string
End string
ExpectedStart string
ExpectedEnd string
}{
{
helpers.Mark(),
"Jan 2, 2020 at 15:04", "Jan 2, 2020 at 16:04",
"Jan 2, 2020 at 14:04", "Jan 2, 2020 at 15:04",
}, {
helpers.Mark(),
"Jan 2, 2020 at 15:04", "Jan 2, 2020 at 16:34",
"Jan 2, 2020 at 14:04", "Jan 2, 2020 at 15:34",
}, {
helpers.Mark(),
"Jan 2, 2020 at 15:04", "Jan 2, 2020 at 17:34",
"Jan 1, 2020 at 15:04", "Jan 1, 2020 at 17:34",
}, {
helpers.Mark(),
"Jan 2, 2020 at 15:04", "Jan 3, 2020 at 17:34",
"Jan 1, 2020 at 15:04", "Jan 2, 2020 at 17:34",
}, {
helpers.Mark(),
"Jan 10, 2020 at 15:04", "Jan 13, 2020 at 17:34",
"Jan 3, 2020 at 15:04", "Jan 6, 2020 at 17:34",
}, {
helpers.Mark(),
"Jan 10, 2020 at 15:04", "Jan 15, 2020 at 17:34",
"Jan 3, 2020 at 15:04", "Jan 8, 2020 at 17:34",
}, {
helpers.Mark(),
"Jan 10, 2020 at 15:04", "Jan 20, 2020 at 17:34",
"Jan 3, 2020 at 15:04", "Jan 13, 2020 at 17:34",
}, {
helpers.Mark(),
"Feb 10, 2020 at 15:04", "Feb 25, 2020 at 17:34",
"Jan 13, 2020 at 15:04", "Jan 28, 2020 at 17:34",
}, {
helpers.Mark(),
"Feb 10, 2020 at 15:04", "Mar 25, 2020 at 17:34",
"Jan 13, 2020 at 15:04", "Feb 26, 2020 at 17:34",
}, {
helpers.Mark(),
"Feb 10, 2020 at 15:04", "Jul 25, 2020 at 17:34",
"Feb 10, 2019 at 15:04", "Jul 25, 2019 at 17:34",
}, {
helpers.Mark(),
"Feb 10, 2019 at 15:04", "Jul 25, 2020 at 17:34",
"Feb 10, 2018 at 15:04", "Jul 25, 2019 at 17:34",
},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("%s to %s", tc.Start, tc.End), func(t *testing.T) {
start, err := time.Parse(longForm, tc.Start)
if err != nil {
t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.Start, err)
}
end, err := time.Parse(longForm, tc.End)
if err != nil {
t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.End, err)
}
expectedStart, err := time.Parse(longForm, tc.ExpectedStart)
if err != nil {
t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.ExpectedStart, err)
}
expectedEnd, err := time.Parse(longForm, tc.ExpectedEnd)
if err != nil {
t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.ExpectedEnd, err)
}
input := graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
schema: schema.NewMock(t),
Start: start,
End: end,
Dimensions: query.Columns{
query.NewColumn("ExporterAddress"),
query.NewColumn("ExporterName"),
},
},
}
query.Columns(input.Dimensions).Validate(input.schema)
got := input.previousPeriod()
expected := graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: expectedStart,
End: expectedEnd,
Dimensions: []query.Column{},
},
}
if diff := helpers.Diff(got, expected); diff != "" {
t.Fatalf("%spreviousPeriod() (-got, +want):\n%s", tc.Pos, diff)
}
})
}
}
func TestGraphQuerySQL(t *testing.T) {
cases := []struct {
Description string
Pos helpers.Pos
Input graphLineHandlerInput
Expected []templateQuery
}{
{
Description: "no dimensions, no filters, bps",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []query.Column{},
Filter: query.Filter{},
Units: "l3bps",
},
Points: 100,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
},
},
}, {
Description: "no dimensions, no filters, l2 bps",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []query.Column{},
Filter: query.Filter{},
Units: "l2bps",
},
Points: 100,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l2bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
},
},
}, {
Description: "no dimensions, no filters, pps",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []query.Column{},
Filter: query.Filter{},
Units: "pps",
},
Points: 100,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "pps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
},
},
}, {
Description: "truncated source address",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []query.Column{query.NewColumn("SrcAddr")},
Filter: query.NewFilter("SrcAddr << 1.0.0.0/8"),
TruncateAddrV4: 24,
TruncateAddrV6: 48,
Units: "l3bps",
},
Points: 100,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
MainTableRequired: true,
},
Template: `WITH
source AS (SELECT * REPLACE (tupleElement(IPv6CIDRToRange(SrcAddr, if(tupleElement(IPv6CIDRToRange(SrcAddr, 96), 1) = toIPv6('::ffff:0.0.0.0'), 120, 48)), 1) AS SrcAddr) FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
rows AS (SELECT SrcAddr FROM source WHERE {{ .Timefilter }} AND (SrcAddr BETWEEN toIPv6('::ffff:1.0.0.0') AND toIPv6('::ffff:1.255.255.255')) GROUP BY SrcAddr ORDER BY {{ .Units }} DESC LIMIT 0)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
if((SrcAddr) IN rows, [replaceRegexpOne(IPv6NumToString(SrcAddr), '^::ffff:', '')], ['Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (SrcAddr BETWEEN toIPv6('::ffff:1.0.0.0') AND toIPv6('::ffff:1.255.255.255'))
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS ['Other']))`,
},
},
}, {
Description: "no dimensions",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []query.Column{},
Filter: query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"),
Units: "l3bps",
},
Points: 100,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (DstCountry = 'FR' AND SrcCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
},
},
}, {
Description: "no dimensions, escaped filter",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []query.Column{},
Filter: query.NewFilter("InIfDescription = '{{ hello }}' AND SrcCountry = 'US'"),
Units: "l3bps",
},
Points: 100,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (InIfDescription = '{{"{{"}} hello }}' AND SrcCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
},
},
}, {
Description: "no dimensions, reverse direction",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []query.Column{},
Filter: query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"),
Units: "l3bps",
},
Points: 100,
Bidirectional: true,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (DstCountry = 'FR' AND SrcCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
}, {
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `SELECT 2 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (SrcCountry = 'FR' AND DstCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
},
},
}, {
Description: "no dimensions, reverse direction, inl2%",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []query.Column{},
Filter: query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"),
Units: "inl2%",
},
Points: 100,
Bidirectional: true,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "inl2%",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (DstCountry = 'FR' AND SrcCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
}, {
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "outl2%",
},
Template: `SELECT 2 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (SrcCountry = 'FR' AND DstCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
},
},
}, {
Description: "no filters",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Limit: 20,
Dimensions: []query.Column{
query.NewColumn("ExporterName"),
query.NewColumn("InIfProvider"),
},
Filter: query.Filter{},
Units: "l3bps",
},
Points: 100,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
rows AS (SELECT ExporterName, InIfProvider FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ORDER BY {{ .Units }} DESC LIMIT 20)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS ['Other', 'Other']))`,
},
},
}, {
Description: "no filters, limitType by max",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Limit: 20,
LimitType: "max",
Dimensions: []query.Column{
query.NewColumn("ExporterName"),
query.NewColumn("InIfProvider"),
},
Filter: query.Filter{},
Units: "l3bps",
},
Points: 100,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
rows AS (SELECT ExporterName, InIfProvider FROM ( SELECT ExporterName, InIfProvider, {{ .Units }} AS sum_at_time FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ) GROUP BY ExporterName, InIfProvider ORDER BY MAX(sum_at_time) DESC LIMIT 20)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS ['Other', 'Other']))`,
},
},
}, {
Description: "no filters, reverse",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Limit: 20,
Dimensions: []query.Column{
query.NewColumn("ExporterName"),
query.NewColumn("InIfProvider"),
},
Filter: query.Filter{},
Units: "l3bps",
},
Points: 100,
Bidirectional: true,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
rows AS (SELECT ExporterName, InIfProvider FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ORDER BY {{ .Units }} DESC LIMIT 20)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS ['Other', 'Other']))`,
}, {
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `SELECT 2 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
if((ExporterName, OutIfProvider) IN rows, [ExporterName, OutIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS ['Other', 'Other']))`,
},
},
}, {
Description: "no filters, previous period",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Limit: 20,
Dimensions: []query.Column{
query.NewColumn("ExporterName"),
query.NewColumn("InIfProvider"),
},
Filter: query.Filter{},
Units: "l3bps",
},
Points: 100,
PreviousPeriod: true,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Units: "l3bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
rows AS (SELECT ExporterName, InIfProvider FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ORDER BY {{ .Units }} DESC LIMIT 20)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS ['Other', 'Other']))`,
}, {
Context: inputContext{
Start: time.Date(2022, 4, 9, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
StartForTableSelection: func() *time.Time {
t := time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC)
return &t
}(),
Points: 100,
Units: "l3bps",
},
Template: `SELECT 3 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} + INTERVAL 86400 second AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }} + INTERVAL 86400 second
TO {{ .TimefilterEnd }} + INTERVAL 1 second + INTERVAL 86400 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
},
},
}, {
Description: "previous period while main table is required",
Pos: helpers.Mark(),
Input: graphLineHandlerInput{
graphCommonHandlerInput: graphCommonHandlerInput{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []query.Column{
query.NewColumn("SrcAddr"),
query.NewColumn("DstAddr"),
},
Filter: query.NewFilter("InIfBoundary = external"),
Units: "l3bps",
},
Points: 100,
PreviousPeriod: true,
},
Expected: []templateQuery{
{
Context: inputContext{
Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
MainTableRequired: true,
Points: 100,
Units: "l3bps",
},
Template: `WITH
source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
rows AS (SELECT SrcAddr, DstAddr FROM source WHERE {{ .Timefilter }} AND (InIfBoundary = 'external') GROUP BY SrcAddr, DstAddr ORDER BY {{ .Units }} DESC LIMIT 0)
SELECT 1 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} AS time,
{{ .Units }}/{{ .Interval }} AS xps,
if((SrcAddr, DstAddr) IN rows, [replaceRegexpOne(IPv6NumToString(SrcAddr), '^::ffff:', ''), replaceRegexpOne(IPv6NumToString(DstAddr), '^::ffff:', '')], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (InIfBoundary = 'external')
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }}
TO {{ .TimefilterEnd }} + INTERVAL 1 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS ['Other', 'Other']))`,
}, {
Context: inputContext{
Start: time.Date(2022, 4, 9, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
StartForTableSelection: func() *time.Time {
t := time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC)
return &t
}(),
MainTableRequired: true,
Points: 100,
Units: "l3bps",
},
Template: `SELECT 3 AS axis, * FROM (
SELECT
{{ call .ToStartOfInterval "TimeReceived" }} + INTERVAL 86400 second AS time,
{{ .Units }}/{{ .Interval }} AS xps,
emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (InIfBoundary = 'external')
GROUP BY time, dimensions
ORDER BY time WITH FILL
FROM {{ .TimefilterStart }} + INTERVAL 86400 second
TO {{ .TimefilterEnd }} + INTERVAL 1 second + INTERVAL 86400 second
STEP {{ .Interval }}
INTERPOLATE (dimensions AS emptyArrayString()))`,
},
},
},
}
for _, tc := range cases {
tc.Input.schema = schema.NewMock(t)
if err := query.Columns(tc.Input.Dimensions).Validate(tc.Input.schema); err != nil {
t.Fatalf("%sValidate() error:\n%+v", tc.Pos, err)
}
if err := tc.Input.Filter.Validate(tc.Input.schema); err != nil {
t.Fatalf("%sValidate() error:\n%+v", tc.Pos, err)
}
t.Run(tc.Description, func(t *testing.T) {
got := tc.Input.toSQL()
if diff := helpers.Diff(got, tc.Expected); diff != "" {
t.Errorf("%stoSQL (-got, +want):\n%s", tc.Pos, diff)
}
})
}
}
func TestGraphLineHandler(t *testing.T) {
_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
base := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
t.Run("sort by avg", func(t *testing.T) {
// Single direction
expectedSQL := []struct {
Axis uint8 `ch:"axis"`
Time time.Time `ch:"time"`
Xps float64 `ch:"xps"`
Dimensions []string `ch:"dimensions"`
}{
{1, base, 1000, []string{"router1", "provider1"}},
{1, base, 2000, []string{"router1", "provider2"}},
{1, base, 1200, []string{"router2", "provider2"}},
{1, base, 1100, []string{"router2", "provider3"}},
{1, base, 1900, []string{"Other", "Other"}},
{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},
}
mockConn.EXPECT().
Select(gomock.Any(), gomock.Any(), gomock.Any()).
SetArg(1, expectedSQL).
Return(nil)
// Bidirectional
expectedSQL = []struct {
Axis uint8 `ch:"axis"`
Time time.Time `ch:"time"`
Xps float64 `ch:"xps"`
Dimensions []string `ch:"dimensions"`
}{
{1, base, 1000, []string{"router1", "provider1"}},
{1, base, 2000, []string{"router1", "provider2"}},
{1, base, 1200, []string{"router2", "provider2"}},
{1, base, 1100, []string{"router2", "provider3"}},
{1, base, 1900, []string{"Other", "Other"}},
{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
// Axes can be mixed. In reality, it seems they cannot
// be interleaved, but ClickHouse documentation does
// not say it is not possible.
{2, base, 101, []string{"router1", "provider1"}},
{2, base, 200, []string{"router1", "provider2"}},
{2, base, 120, []string{"router2", "provider2"}},
{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
{2, base, 111, []string{"router2", "provider3"}},
{2, base, 190, []string{"Other", "Other"}},
{2, base.Add(time.Minute), 50, []string{"router1", "provider1"}},
{2, base.Add(time.Minute), 500, []string{"router1", "provider2"}},
{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},
{2, base.Add(time.Minute), 90, []string{"router2", "provider4"}},
{2, base.Add(time.Minute), 10, []string{"Other", "Other"}},
{2, base.Add(2 * time.Minute), 10, []string{"router1", "provider1"}},
{2, base.Add(2 * time.Minute), 300, []string{"router1", "provider2"}},
{2, base.Add(2 * time.Minute), 10, []string{"router2", "provider4"}},
{2, base.Add(2 * time.Minute), 10, []string{"Other", "Other"}},
}
mockConn.EXPECT().
Select(gomock.Any(), gomock.Any(), gomock.Any()).
SetArg(1, expectedSQL).
Return(nil)
// Previous period
expectedSQL = []struct {
Axis uint8 `ch:"axis"`
Time time.Time `ch:"time"`
Xps float64 `ch:"xps"`
Dimensions []string `ch:"dimensions"`
}{
{1, base, 1000, []string{"router1", "provider1"}},
{1, base, 2000, []string{"router1", "provider2"}},
{1, base, 1200, []string{"router2", "provider2"}},
{1, base, 1100, []string{"router2", "provider3"}},
{1, base, 1900, []string{"Other", "Other"}},
{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},
{3, base, 8000, []string{}},
{3, base.Add(time.Minute), 6000, []string{}},
{3, base.Add(2 * time.Minute), 4500, []string{}},
}
mockConn.EXPECT().
Select(gomock.Any(), gomock.Any(), gomock.Any()).
SetArg(1, expectedSQL).
Return(nil)
helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{
{
Description: "single direction",
URL: "/api/v0/console/graph/line",
JSONInput: gin.H{
"start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
"end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
"points": 100,
"limit": 20,
"limitType": "avg",
"dimensions": []string{"ExporterName", "InIfProvider"},
"filter": "DstCountry = 'FR' AND SrcCountry = 'US'",
"units": "l3bps",
"bidirectional": false,
},
JSONOutput: gin.H{
// Sorted by sum of bps
"rows": [][]string{
{"router1", "provider2"}, // 10000
{"router1", "provider1"}, // 1600
{"router2", "provider2"}, // 1200
{"router2", "provider3"}, // 1100
{"router2", "provider4"}, // 1000
{"Other", "Other"}, // 2100
},
"t": []string{
"2009-11-10T23:00:00Z",
"2009-11-10T23:01:00Z",
"2009-11-10T23:02:00Z",
},
"points": [][]int{
{2000, 5000, 3000},
{1000, 500, 100},
{1200, 0, 0},
{1100, 0, 0},
{0, 900, 100},
{1900, 100, 100},
},
"min": []int{
2000,
100,
1200,
1100,
100,
100,
},
"max": []int{
5000,
1000,
1200,
1100,
900,
1900,
},
"last": []int{
5000,
500,
0,
0,
900,
100,
},
"average": []int{
3333,
533,
400,
366,
333,
700,
},
"95th": []int{
4800,
950,
1080,
990,
820,
1720,
},
"axis": []int{
1, 1, 1, 1, 1, 1,
},
"axis-names": map[int]string{
1: "Direct",
},
},
}, {
Description: "bidirectional",
URL: "/api/v0/console/graph/line",
JSONInput: gin.H{
"start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
"end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
"points": 100,
"limit": 20,
"limitType": "avg",
"dimensions": []string{"ExporterName", "InIfProvider"},
"filter": "DstCountry = 'FR' AND SrcCountry = 'US'",
"units": "l3bps",
"bidirectional": true,
},
JSONOutput: gin.H{
// Sorted by sum of bps
"rows": [][]string{
{"router1", "provider2"}, // 10000
{"router1", "provider1"}, // 1600
{"router2", "provider2"}, // 1200
{"router2", "provider3"}, // 1100
{"router2", "provider4"}, // 1000
{"Other", "Other"}, // 2100
{"router1", "provider2"}, // 1000
{"router1", "provider1"}, // 160
{"router2", "provider2"}, // 120
{"router2", "provider3"}, // 110
{"router2", "provider4"}, // 100
{"Other", "Other"}, // 210
},
"t": []string{
"2009-11-10T23:00:00Z",
"2009-11-10T23:01:00Z",
"2009-11-10T23:02:00Z",
},
"points": [][]int{
{2000, 5000, 3000},
{1000, 500, 100},
{1200, 0, 0},
{1100, 0, 0},
{0, 900, 100},
{1900, 100, 100},
{200, 500, 300},
{101, 50, 10},
{120, 0, 0},
{111, 0, 0},
{0, 90, 10},
{190, 10, 10},
},
"min": []int{
2000,
100,
1200,
1100,
100,
100,
200,
10,
120,
111,
10,
10,
},
"max": []int{
5000,
1000,
1200,
1100,
900,
1900,
500,
101,
120,
111,
90,
190,
},
"last": []int{
5000,
500,
0,
0,
900,
100,
500,
50,
0,
0,
90,
10,
},
"average": []int{
3333,
533,
400,
366,
333,
700,
333,
53,
40,
37,
33,
70,
},
"95th": []int{
4800,
950,
1080,
990,
820,
1720,
480,
96,
108,
100,
82,
172,
},
"axis": []int{
1, 1, 1, 1, 1, 1,
2, 2, 2, 2, 2, 2,
},
"axis-names": map[int]string{
1: "Direct",
2: "Reverse",
},
},
}, {
Description: "previous period",
URL: "/api/v0/console/graph/line",
JSONInput: gin.H{
"start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
"end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
"points": 100,
"limit": 20,
"limitType": "avg",
"dimensions": []string{"ExporterName", "InIfProvider"},
"filter": "DstCountry = 'FR' AND SrcCountry = 'US'",
"units": "l3bps",
"bidirectional": false,
"previous-period": true,
},
JSONOutput: gin.H{
// Sorted by sum of bps
"rows": [][]string{
{"router1", "provider2"}, // 10000
{"router1", "provider1"}, // 1600
{"router2", "provider2"}, // 1200
{"router2", "provider3"}, // 1100
{"router2", "provider4"}, // 1000
{"Other", "Other"}, // 2100
{"Other", "Other"}, // Previous day
},
"t": []string{
"2009-11-10T23:00:00Z",
"2009-11-10T23:01:00Z",
"2009-11-10T23:02:00Z",
},
"points": [][]int{
{2000, 5000, 3000},
{1000, 500, 100},
{1200, 0, 0},
{1100, 0, 0},
{0, 900, 100},
{1900, 100, 100},
{8000, 6000, 4500},
},
"min": []int{
2000,
100,
1200,
1100,
100,
100,
4500,
},
"max": []int{
5000,
1000,
1200,
1100,
900,
1900,
8000,
},
"last": []int{
5000,
500,
0,
0,
900,
100,
6000,
},
"average": []int{
3333,
533,
400,
366,
333,
700,
6166,
},
"95th": []int{
4800,
950,
1080,
990,
820,
1720,
7800,
},
"axis": []int{
1, 1, 1, 1, 1, 1,
3,
},
"axis-names": map[int]string{
1: "Direct",
3: "Previous day",
},
},
},
})
})
t.Run("sort by max", func(t *testing.T) {
// Single direction
expectedSQL := []struct {
Axis uint8 `ch:"axis"`
Time time.Time `ch:"time"`
Xps float64 `ch:"xps"`
Dimensions []string `ch:"dimensions"`
}{
{1, base, 1000, []string{"router1", "provider1"}},
{1, base, 2000, []string{"router1", "provider2"}},
{1, base, 1200, []string{"router2", "provider2"}},
{1, base, 1100, []string{"router2", "provider3"}},
{1, base, 1900, []string{"Other", "Other"}},
{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},
}
mockConn.EXPECT().
Select(gomock.Any(), gomock.Any(), gomock.Any()).
SetArg(1, expectedSQL).
Return(nil)
// Bidirectional
expectedSQL = []struct {
Axis uint8 `ch:"axis"`
Time time.Time `ch:"time"`
Xps float64 `ch:"xps"`
Dimensions []string `ch:"dimensions"`
}{
{1, base, 1000, []string{"router1", "provider1"}},
{1, base, 2000, []string{"router1", "provider2"}},
{1, base, 1200, []string{"router2", "provider2"}},
{1, base, 1100, []string{"router2", "provider3"}},
{1, base, 1900, []string{"Other", "Other"}},
{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
// Axes can be mixed. In reality, it seems they cannot
// be interleaved, but ClickHouse documentation does
// not say it is not possible.
{2, base, 101, []string{"router1", "provider1"}},
{2, base, 200, []string{"router1", "provider2"}},
{2, base, 120, []string{"router2", "provider2"}},
{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
{2, base, 111, []string{"router2", "provider3"}},
{2, base, 190, []string{"Other", "Other"}},
{2, base.Add(time.Minute), 50, []string{"router1", "provider1"}},
{2, base.Add(time.Minute), 500, []string{"router1", "provider2"}},
{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},
{2, base.Add(time.Minute), 90, []string{"router2", "provider4"}},
{2, base.Add(time.Minute), 10, []string{"Other", "Other"}},
{2, base.Add(2 * time.Minute), 10, []string{"router1", "provider1"}},
{2, base.Add(2 * time.Minute), 300, []string{"router1", "provider2"}},
{2, base.Add(2 * time.Minute), 10, []string{"router2", "provider4"}},
{2, base.Add(2 * time.Minute), 10, []string{"Other", "Other"}},
}
mockConn.EXPECT().
Select(gomock.Any(), gomock.Any(), gomock.Any()).
SetArg(1, expectedSQL).
Return(nil)
// Previous period
expectedSQL = []struct {
Axis uint8 `ch:"axis"`
Time time.Time `ch:"time"`
Xps float64 `ch:"xps"`
Dimensions []string `ch:"dimensions"`
}{
{1, base, 1000, []string{"router1", "provider1"}},
{1, base, 2000, []string{"router1", "provider2"}},
{1, base, 1200, []string{"router2", "provider2"}},
{1, base, 1100, []string{"router2", "provider3"}},
{1, base, 1900, []string{"Other", "Other"}},
{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},
{3, base, 8000, []string{}},
{3, base.Add(time.Minute), 6000, []string{}},
{3, base.Add(2 * time.Minute), 4500, []string{}},
}
mockConn.EXPECT().
Select(gomock.Any(), gomock.Any(), gomock.Any()).
SetArg(1, expectedSQL).
Return(nil)
helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{
{
Description: "single direction",
URL: "/api/v0/console/graph/line",
JSONInput: gin.H{
"start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
"end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
"points": 100,
"limit": 20,
"limitType": "max",
"dimensions": []string{"ExporterName", "InIfProvider"},
"filter": "DstCountry = 'FR' AND SrcCountry = 'US'",
"units": "l3bps",
"bidirectional": false,
},
JSONOutput: gin.H{
// Sorted by max of bps
"rows": [][]string{
{"router1", "provider2"},
{"router2", "provider2"},
{"router2", "provider3"},
{"router1", "provider1"},
{"router2", "provider4"},
{"Other", "Other"},
},
"t": []string{
"2009-11-10T23:00:00Z",
"2009-11-10T23:01:00Z",
"2009-11-10T23:02:00Z",
},
"points": [][]int{
{2000, 5000, 3000},
{1200, 0, 0},
{1100, 0, 0},
{1000, 500, 100},
{0, 900, 100},
{1900, 100, 100},
},
"min": []int{
2000,
1200,
1100,
100,
100,
100,
},
"max": []int{
5000,
1200,
1100,
1000,
900,
1900,
},
"last": []int{
5000,
0,
0,
500,
900,
100,
},
"average": []int{
3333,
400,
366,
533,
333,
700,
},
"95th": []int{
4800,
1080,
990,
950,
820,
1720,
},
"axis": []int{
1, 1, 1, 1, 1, 1,
},
"axis-names": map[int]string{
1: "Direct",
},
},
}, {
Description: "bidirectional",
URL: "/api/v0/console/graph/line",
JSONInput: gin.H{
"start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
"end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
"points": 100,
"limit": 20,
"limitType": "max",
"dimensions": []string{"ExporterName", "InIfProvider"},
"filter": "DstCountry = 'FR' AND SrcCountry = 'US'",
"units": "l3bps",
"bidirectional": true,
},
JSONOutput: gin.H{
// Sorted by sum of bps
"rows": [][]string{
{"router1", "provider2"}, // 10000
{"router2", "provider2"}, // 1200
{"router2", "provider3"}, // 1100
{"router1", "provider1"}, // 1600
{"router2", "provider4"}, // 1000
{"Other", "Other"}, // 2100
{"router1", "provider2"}, // 1000
{"router2", "provider2"}, // 120
{"router2", "provider3"}, // 110
{"router1", "provider1"}, // 160
{"router2", "provider4"}, // 100
{"Other", "Other"}, // 210
},
"t": []string{
"2009-11-10T23:00:00Z",
"2009-11-10T23:01:00Z",
"2009-11-10T23:02:00Z",
},
"points": [][]int{
{2000, 5000, 3000},
{1200, 0, 0},
{1100, 0, 0},
{1000, 500, 100},
{0, 900, 100},
{1900, 100, 100},
{200, 500, 300},
{120, 0, 0},
{111, 0, 0},
{101, 50, 10},
{0, 90, 10},
{190, 10, 10},
},
"min": []int{
2000,
1200,
1100,
100,
100,
100,
200,
120,
111,
10,
10,
10,
},
"max": []int{
5000,
1200,
1100,
1000,
900,
1900,
500,
120,
111,
101,
90,
190,
},
"last": []int{
5000,
0,
0,
500,
900,
100,
500,
0,
0,
50,
90,
10,
},
"average": []int{
3333,
400,
366,
533,
333,
700,
333,
40,
37,
53,
33,
70,
},
"95th": []int{
4800,
1080,
990,
950,
820,
1720,
480,
108,
100,
96,
82,
172,
},
"axis": []int{
1, 1, 1, 1, 1, 1,
2, 2, 2, 2, 2, 2,
},
"axis-names": map[int]string{
1: "Direct",
2: "Reverse",
},
},
}, {
Description: "previous period",
URL: "/api/v0/console/graph/line",
JSONInput: gin.H{
"start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
"end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
"points": 100,
"limit": 20,
"limitType": "max",
"dimensions": []string{"ExporterName", "InIfProvider"},
"filter": "DstCountry = 'FR' AND SrcCountry = 'US'",
"units": "l3bps",
"bidirectional": false,
"previous-period": true,
},
JSONOutput: gin.H{
// Sorted by sum of bps
"rows": [][]string{
{"router1", "provider2"}, // 10000
{"router2", "provider2"}, // 1200
{"router2", "provider3"}, // 1100
{"router1", "provider1"}, // 1600
{"router2", "provider4"}, // 1000
{"Other", "Other"}, // 2100
{"Other", "Other"}, // Previous day
},
"t": []string{
"2009-11-10T23:00:00Z",
"2009-11-10T23:01:00Z",
"2009-11-10T23:02:00Z",
},
"points": [][]int{
{2000, 5000, 3000},
{1200, 0, 0},
{1100, 0, 0},
{1000, 500, 100},
{0, 900, 100},
{1900, 100, 100},
{8000, 6000, 4500},
},
"min": []int{
2000,
1200,
1100,
100,
100,
100,
4500,
},
"max": []int{
5000,
1200,
1100,
1000,
900,
1900,
8000,
},
"last": []int{
5000,
0,
0,
500,
900,
100,
6000,
},
"average": []int{
3333,
400,
366,
533,
333,
700,
6166,
},
"95th": []int{
4800,
1080,
990,
950,
820,
1720,
7800,
},
"axis": []int{
1, 1, 1, 1, 1, 1,
3,
},
"axis-names": map[int]string{
1: "Direct",
3: "Previous day",
},
},
},
})
})
}
func TestGetTableInterval(t *testing.T) {
_, h, _, mockClock := NewMock(t, DefaultConfiguration())
mockClock.Set(time.Date(2022, 4, 12, 15, 45, 10, 0, time.UTC))
helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{
{
Description: "simple query",
URL: "/api/v0/console/graph/table-interval",
JSONInput: gin.H{
"start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
"end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
"points": 300,
},
JSONOutput: gin.H{
"table": "flows",
"interval": 1,
},
}, {
Description: "too many points",
URL: "/api/v0/console/graph/table-interval",
JSONInput: gin.H{
"start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
"end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
"points": 86400,
},
StatusCode: 400,
JSONOutput: gin.H{
"message": `Key: 'tableIntervalInput.Points' Error:Field validation for 'Points' failed on the 'max' tag`,
},
},
})
}