console: split graph.go into query.go and graph.go

We will introduce more query types.
This commit is contained in:
Vincent Bernat
2022-05-21 19:53:08 +02:00
parent 0879da9512
commit 375a0fe88f
5 changed files with 184 additions and 170 deletions

View File

@@ -80,8 +80,8 @@ console/filter/parser.go: console/filter/parser.peg | $(PIGEON) ; $(info $(M) ge
console/frontend/node_modules: console/frontend/package.json console/frontend/yarn.lock
console/frontend/node_modules: ; $(info $(M) fetching node modules)
$Q yarn install --silent --frozen-lockfile --cwd console/frontend && touch $@
console/frontend/data/fields.json: console/graph.go ; $(info $(M) generate list of selectable fields)
$Q sed -En -e 's/^\tgraphColumn([a-zA-Z]+)( .*|$$)/ "\1"/p' $< \
console/frontend/data/fields.json: console/query.go ; $(info $(M) generate list of selectable fields)
$Q sed -En -e 's/^\tqueryColumn([a-zA-Z]+)( .*|$$)/ "\1"/p' $< \
| sed -E -e '1i [' -e '$$ ! s/$$/,/' -e '$$a ]'> $@
$Q test -s $@
console/data/frontend: Makefile console/frontend/node_modules

View File

@@ -1,7 +1,6 @@
package console
import (
"errors"
"fmt"
"net/http"
"sort"
@@ -11,7 +10,6 @@ import (
"github.com/gin-gonic/gin"
"akvorado/common/helpers"
"akvorado/console/filter"
)
// graphQuery describes the input for the /graph endpoint.
@@ -19,126 +17,9 @@ type graphQuery struct {
Start time.Time `json:"start" binding:"required"`
End time.Time `json:"end" binding:"required"`
Points int `json:"points" binding:"required"` // minimum number of points
Dimensions []graphColumn `json:"dimensions"` // group by ...
Dimensions []queryColumn `json:"dimensions"` // group by ...
Limit int `json:"limit"` // limit product of dimensions
Filter graphFilter `json:"filter"` // where ...
}
type graphColumn int
const (
graphColumnExporterAddress graphColumn = iota + 1
graphColumnExporterName
graphColumnExporterGroup
graphColumnSrcAS
graphColumnSrcCountry
graphColumnInIfName
graphColumnInIfDescription
graphColumnInIfSpeed
graphColumnInIfConnectivity
graphColumnInIfProvider
graphColumnInIfBoundary
graphColumnEType
graphColumnProto
graphColumnSrcPort
graphColumnSrcAddr
graphColumnDstAS
graphColumnDstCountry
graphColumnOutIfName
graphColumnOutIfDescription
graphColumnOutIfSpeed
graphColumnOutIfConnectivity
graphColumnOutIfProvider
graphColumnOutIfBoundary
graphColumnDstAddr
graphColumnDstPort
graphColumnForwardingStatus
)
var graphColumnMap = helpers.NewBimap(map[graphColumn]string{
graphColumnExporterAddress: "ExporterAddress",
graphColumnExporterName: "ExporterName",
graphColumnExporterGroup: "ExporterGroup",
graphColumnSrcAddr: "SrcAddr",
graphColumnDstAddr: "DstAddr",
graphColumnSrcAS: "SrcAS",
graphColumnDstAS: "DstAS",
graphColumnSrcCountry: "SrcCountry",
graphColumnDstCountry: "DstCountry",
graphColumnInIfName: "InIfName",
graphColumnOutIfName: "OutIfName",
graphColumnInIfDescription: "InIfDescription",
graphColumnOutIfDescription: "OutIfDescription",
graphColumnInIfSpeed: "InIfSpeed",
graphColumnOutIfSpeed: "OutIfSpeed",
graphColumnInIfConnectivity: "InIfConnectivity",
graphColumnOutIfConnectivity: "OutIfConnectivity",
graphColumnInIfProvider: "InIfProvider",
graphColumnOutIfProvider: "OutIfProvider",
graphColumnInIfBoundary: "InIfBoundary",
graphColumnOutIfBoundary: "OutIfBoundary",
graphColumnEType: "EType",
graphColumnProto: "Proto",
graphColumnSrcPort: "SrcPort",
graphColumnDstPort: "DstPort",
graphColumnForwardingStatus: "ForwardingStatus",
})
func (gc graphColumn) MarshalText() ([]byte, error) {
got, ok := graphColumnMap.LoadValue(gc)
if ok {
return []byte(got), nil
}
return nil, errors.New("unknown field")
}
func (gc graphColumn) String() string {
got, _ := graphColumnMap.LoadValue(gc)
return got
}
func (gc *graphColumn) UnmarshalText(input []byte) error {
got, ok := graphColumnMap.LoadKey(string(input))
if ok {
*gc = got
return nil
}
return errors.New("unknown field")
}
type graphFilter struct {
filter string
}
func (gf graphFilter) MarshalText() ([]byte, error) {
return []byte(gf.filter), nil
}
func (gf *graphFilter) UnmarshalText(input []byte) error {
got, err := filter.Parse("", input)
if err != nil {
return fmt.Errorf("cannot parse filter: %s", filter.HumanError(err))
}
*gf = graphFilter{got.(string)}
return nil
}
// toSQLSelect transforms a column into an expression to use in SELECT
func (gc graphColumn) toSQLSelect() string {
var strValue string
switch gc {
case graphColumnExporterAddress, graphColumnSrcAddr, graphColumnDstAddr:
strValue = fmt.Sprintf("IPv6NumToString(%s)", gc)
case graphColumnSrcAS, graphColumnDstAS:
strValue = fmt.Sprintf(`concat(toString(%s), ': ', dictGetOrDefault('asns', 'name', %s, '???'))`,
gc, gc)
case graphColumnEType:
strValue = `if(EType = 0x800, 'IPv4', if(EType = 0x86dd, 'IPv6', '???'))`
case graphColumnProto:
strValue = `dictGetOrDefault('protocols', 'name', Proto, '???')`
case graphColumnInIfSpeed, graphColumnOutIfSpeed, graphColumnSrcPort, graphColumnDstPort, graphColumnForwardingStatus:
strValue = fmt.Sprintf("toString(%s)", gc)
default:
strValue = gc.String()
}
return strValue
Filter queryFilter `json:"filter"` // where ...
}
// graphQueryToSQL converts a graph query to an SQL request

View File

@@ -19,41 +19,6 @@ import (
"akvorado/common/reporter"
)
func TestGraphColumnSQLSelect(t *testing.T) {
cases := []struct {
Input graphColumn
Expected string
}{
{
Input: graphColumnSrcAddr,
Expected: `IPv6NumToString(SrcAddr)`,
}, {
Input: graphColumnDstAS,
Expected: `concat(toString(DstAS), ': ', dictGetOrDefault('asns', 'name', DstAS, '???'))`,
}, {
Input: graphColumnProto,
Expected: `dictGetOrDefault('protocols', 'name', Proto, '???')`,
}, {
Input: graphColumnEType,
Expected: `if(EType = 0x800, 'IPv4', if(EType = 0x86dd, 'IPv6', '???'))`,
}, {
Input: graphColumnOutIfSpeed,
Expected: `toString(OutIfSpeed)`,
}, {
Input: graphColumnExporterName,
Expected: `ExporterName`,
},
}
for _, tc := range cases {
t.Run(tc.Input.String(), func(t *testing.T) {
got := tc.Input.toSQLSelect()
if diff := helpers.Diff(got, tc.Expected); diff != "" {
t.Errorf("toSQLWhere (-got, +want):\n%s", diff)
}
})
}
}
func TestGraphQuerySQL(t *testing.T) {
cases := []struct {
Description string
@@ -66,8 +31,8 @@ func TestGraphQuerySQL(t *testing.T) {
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Dimensions: []graphColumn{},
Filter: graphFilter{},
Dimensions: []queryColumn{},
Filter: queryFilter{},
},
Expected: `
WITH
@@ -86,8 +51,8 @@ ORDER BY time`,
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Dimensions: []graphColumn{},
Filter: graphFilter{"DstCountry = 'FR' AND SrcCountry = 'US'"},
Dimensions: []queryColumn{},
Filter: queryFilter{"DstCountry = 'FR' AND SrcCountry = 'US'"},
},
Expected: `
WITH
@@ -107,11 +72,11 @@ ORDER BY time`,
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Limit: 20,
Dimensions: []graphColumn{
graphColumnExporterName,
graphColumnInIfProvider,
Dimensions: []queryColumn{
queryColumnExporterName,
queryColumnInIfProvider,
},
Filter: graphFilter{},
Filter: queryFilter{},
},
Expected: `
WITH
@@ -229,11 +194,11 @@ func TestGraphHandler(t *testing.T) {
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Points: 100,
Limit: 20,
Dimensions: []graphColumn{
graphColumnExporterName,
graphColumnInIfProvider,
Dimensions: []queryColumn{
queryColumnExporterName,
queryColumnInIfProvider,
},
Filter: graphFilter{"DstCountry = 'FR' AND SrcCountry = 'US'"},
Filter: queryFilter{"DstCountry = 'FR' AND SrcCountry = 'US'"},
}
payload := new(bytes.Buffer)
err = json.NewEncoder(payload).Encode(input)

126
console/query.go Normal file
View File

@@ -0,0 +1,126 @@
package console
import (
"errors"
"fmt"
"akvorado/common/helpers"
"akvorado/console/filter"
)
type queryColumn int
const (
queryColumnExporterAddress queryColumn = iota + 1
queryColumnExporterName
queryColumnExporterGroup
queryColumnSrcAS
queryColumnSrcCountry
queryColumnInIfName
queryColumnInIfDescription
queryColumnInIfSpeed
queryColumnInIfConnectivity
queryColumnInIfProvider
queryColumnInIfBoundary
queryColumnEType
queryColumnProto
queryColumnSrcPort
queryColumnSrcAddr
queryColumnDstAS
queryColumnDstCountry
queryColumnOutIfName
queryColumnOutIfDescription
queryColumnOutIfSpeed
queryColumnOutIfConnectivity
queryColumnOutIfProvider
queryColumnOutIfBoundary
queryColumnDstAddr
queryColumnDstPort
queryColumnForwardingStatus
)
var queryColumnMap = helpers.NewBimap(map[queryColumn]string{
queryColumnExporterAddress: "ExporterAddress",
queryColumnExporterName: "ExporterName",
queryColumnExporterGroup: "ExporterGroup",
queryColumnSrcAddr: "SrcAddr",
queryColumnDstAddr: "DstAddr",
queryColumnSrcAS: "SrcAS",
queryColumnDstAS: "DstAS",
queryColumnSrcCountry: "SrcCountry",
queryColumnDstCountry: "DstCountry",
queryColumnInIfName: "InIfName",
queryColumnOutIfName: "OutIfName",
queryColumnInIfDescription: "InIfDescription",
queryColumnOutIfDescription: "OutIfDescription",
queryColumnInIfSpeed: "InIfSpeed",
queryColumnOutIfSpeed: "OutIfSpeed",
queryColumnInIfConnectivity: "InIfConnectivity",
queryColumnOutIfConnectivity: "OutIfConnectivity",
queryColumnInIfProvider: "InIfProvider",
queryColumnOutIfProvider: "OutIfProvider",
queryColumnInIfBoundary: "InIfBoundary",
queryColumnOutIfBoundary: "OutIfBoundary",
queryColumnEType: "EType",
queryColumnProto: "Proto",
queryColumnSrcPort: "SrcPort",
queryColumnDstPort: "DstPort",
queryColumnForwardingStatus: "ForwardingStatus",
})
func (gc queryColumn) MarshalText() ([]byte, error) {
got, ok := queryColumnMap.LoadValue(gc)
if ok {
return []byte(got), nil
}
return nil, errors.New("unknown field")
}
func (gc queryColumn) String() string {
got, _ := queryColumnMap.LoadValue(gc)
return got
}
func (gc *queryColumn) UnmarshalText(input []byte) error {
got, ok := queryColumnMap.LoadKey(string(input))
if ok {
*gc = got
return nil
}
return errors.New("unknown field")
}
type queryFilter struct {
filter string
}
func (gf queryFilter) MarshalText() ([]byte, error) {
return []byte(gf.filter), nil
}
func (gf *queryFilter) UnmarshalText(input []byte) error {
got, err := filter.Parse("", input)
if err != nil {
return fmt.Errorf("cannot parse filter: %s", filter.HumanError(err))
}
*gf = queryFilter{got.(string)}
return nil
}
// toSQLSelect transforms a column into an expression to use in SELECT
func (gc queryColumn) toSQLSelect() string {
var strValue string
switch gc {
case queryColumnExporterAddress, queryColumnSrcAddr, queryColumnDstAddr:
strValue = fmt.Sprintf("IPv6NumToString(%s)", gc)
case queryColumnSrcAS, queryColumnDstAS:
strValue = fmt.Sprintf(`concat(toString(%s), ': ', dictGetOrDefault('asns', 'name', %s, '???'))`,
gc, gc)
case queryColumnEType:
strValue = `if(EType = 0x800, 'IPv4', if(EType = 0x86dd, 'IPv6', '???'))`
case queryColumnProto:
strValue = `dictGetOrDefault('protocols', 'name', Proto, '???')`
case queryColumnInIfSpeed, queryColumnOutIfSpeed, queryColumnSrcPort, queryColumnDstPort, queryColumnForwardingStatus:
strValue = fmt.Sprintf("toString(%s)", gc)
default:
strValue = gc.String()
}
return strValue
}

42
console/query_test.go Normal file
View File

@@ -0,0 +1,42 @@
package console
import (
"testing"
"akvorado/common/helpers"
)
func TestQueryColumnSQLSelect(t *testing.T) {
cases := []struct {
Input queryColumn
Expected string
}{
{
Input: queryColumnSrcAddr,
Expected: `IPv6NumToString(SrcAddr)`,
}, {
Input: queryColumnDstAS,
Expected: `concat(toString(DstAS), ': ', dictGetOrDefault('asns', 'name', DstAS, '???'))`,
}, {
Input: queryColumnProto,
Expected: `dictGetOrDefault('protocols', 'name', Proto, '???')`,
}, {
Input: queryColumnEType,
Expected: `if(EType = 0x800, 'IPv4', if(EType = 0x86dd, 'IPv6', '???'))`,
}, {
Input: queryColumnOutIfSpeed,
Expected: `toString(OutIfSpeed)`,
}, {
Input: queryColumnExporterName,
Expected: `ExporterName`,
},
}
for _, tc := range cases {
t.Run(tc.Input.String(), func(t *testing.T) {
got := tc.Input.toSQLSelect()
if diff := helpers.Diff(got, tc.Expected); diff != "" {
t.Errorf("toSQLWhere (-got, +want):\n%s", diff)
}
})
}
}