mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
go-cmp is stricter and allow to catch more problems. Moreover, the output is a bit nicer.
161 lines
5.0 KiB
Go
161 lines
5.0 KiB
Go
// SPDX-FileCopyrightText: 2023 Free Mobile
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
// Package query provides query columns and query filters. These
|
|
// types are special as they need a schema to be validated.
|
|
package query
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"akvorado/common/helpers"
|
|
"akvorado/common/schema"
|
|
)
|
|
|
|
// Column represents a query column. It should be instantiated with NewColumn() or
|
|
// Unmarshal(), then call Validate().
|
|
type Column struct {
|
|
validated bool
|
|
name string
|
|
key schema.ColumnKey
|
|
}
|
|
|
|
// Columns is a set of query columns.
|
|
type Columns []Column
|
|
|
|
// NewColumn creates a new column. Validate() should be called before using it.
|
|
func NewColumn(name string) Column {
|
|
return Column{name: name}
|
|
}
|
|
|
|
func (qc Column) check() {
|
|
if !qc.validated {
|
|
panic("query column not validated")
|
|
}
|
|
}
|
|
|
|
func (qc Column) String() string {
|
|
return qc.name
|
|
}
|
|
|
|
// Equal returns true iff two columns have the same name.
|
|
func (qc Column) Equal(oqc Column) bool {
|
|
return qc.name == oqc.name
|
|
}
|
|
|
|
// MarshalText turns a column into a string.
|
|
func (qc Column) MarshalText() ([]byte, error) {
|
|
return []byte(qc.name), nil
|
|
}
|
|
|
|
// UnmarshalText parses a column. Validate() should be called before use.
|
|
func (qc *Column) UnmarshalText(input []byte) error {
|
|
name := string(input)
|
|
*qc = Column{name: name}
|
|
return nil
|
|
}
|
|
|
|
// Key returns the key for the column.
|
|
func (qc *Column) Key() schema.ColumnKey {
|
|
qc.check()
|
|
return qc.key
|
|
}
|
|
|
|
// Validate should be called before using the column. We need a schema component
|
|
// for that.
|
|
func (qc *Column) Validate(schema *schema.Component) error {
|
|
if column, ok := schema.LookupColumnByName(qc.name); ok && !column.ConsoleNotDimension && !column.Disabled {
|
|
qc.key = column.Key
|
|
qc.validated = true
|
|
return nil
|
|
}
|
|
return fmt.Errorf("unknown column name %s", qc.name)
|
|
}
|
|
|
|
// Reverse reverses the column direction
|
|
func (qc *Column) Reverse(schema *schema.Component) {
|
|
name := schema.ReverseColumnDirection(qc.Key()).String()
|
|
reverted := Column{name: name}
|
|
if reverted.Validate(schema) == nil {
|
|
*qc = reverted
|
|
}
|
|
// No modification otherwise
|
|
}
|
|
|
|
// Reverse reverses the direction of all columns
|
|
func (qcs Columns) Reverse(schema *schema.Component) {
|
|
for i := range qcs {
|
|
qcs[i].Reverse(schema)
|
|
}
|
|
}
|
|
|
|
// Validate call Validate on each column.
|
|
func (qcs Columns) Validate(schema *schema.Component) error {
|
|
for i := range qcs {
|
|
if err := qcs[i].Validate(schema); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ToSQLSelect transforms a column into an expression to use in SELECT
|
|
func (qc Column) ToSQLSelect(sch *schema.Component) string {
|
|
var strValue string
|
|
key := qc.Key()
|
|
switch key {
|
|
// Special cases
|
|
case schema.ColumnSrcAS, schema.ColumnDstAS, schema.ColumnDst1stAS, schema.ColumnDst2ndAS, schema.ColumnDst3rdAS:
|
|
strValue = fmt.Sprintf(`concat(toString(%s), ': ', dictGetOrDefault('%s', 'name', %s, '???'))`,
|
|
qc, schema.DictionaryASNs, qc)
|
|
case schema.ColumnInIfBoundary, schema.ColumnOutIfBoundary:
|
|
strValue = fmt.Sprintf(`toString(%s)`, qc.String())
|
|
case schema.ColumnEType:
|
|
strValue = fmt.Sprintf(`if(EType = %d, 'IPv4', if(EType = %d, 'IPv6', '???'))`,
|
|
helpers.ETypeIPv4, helpers.ETypeIPv6)
|
|
case schema.ColumnProto:
|
|
strValue = fmt.Sprintf(`dictGetOrDefault('%s', 'name', Proto, '???')`, schema.DictionaryProtocols)
|
|
case schema.ColumnMPLSLabels:
|
|
strValue = `arrayStringConcat(MPLSLabels, ' ')`
|
|
case schema.ColumnDstASPath:
|
|
strValue = `arrayStringConcat(DstASPath, ' ')`
|
|
case schema.ColumnDstCommunities:
|
|
strValue = `arrayStringConcat(arrayConcat(arrayMap(c -> concat(toString(bitShiftRight(c, 16)), ':', toString(bitAnd(c, 0xffff))), DstCommunities), arrayMap(c -> concat(toString(bitAnd(bitShiftRight(c, 64), 0xffffffff)), ':', toString(bitAnd(bitShiftRight(c, 32), 0xffffffff)), ':', toString(bitAnd(c, 0xffffffff))), DstLargeCommunities)), ' ')`
|
|
case schema.ColumnSrcMAC, schema.ColumnDstMAC:
|
|
strValue = fmt.Sprintf("MACNumToString(%s)", qc)
|
|
case schema.ColumnTCPFlags:
|
|
bits := []string{
|
|
"FIN",
|
|
"SYN",
|
|
"RST",
|
|
"PSH",
|
|
".ACK",
|
|
"URG",
|
|
"ECE",
|
|
"CWR",
|
|
"NS",
|
|
}
|
|
array := make([]string, len(bits))
|
|
for bit, v := range bits {
|
|
array[bit] = fmt.Sprintf("if(bitTest(%s, %d) = 1, '%s', '')", qc, bit, v[:1])
|
|
}
|
|
strValue = fmt.Sprintf("arrayStringConcat([%s], '')", strings.Join(array, ", "))
|
|
case schema.ColumnDstPort, schema.ColumnSrcPort:
|
|
strValue = fmt.Sprintf(`replaceRegexpOne(multiIf(%s==6, concat(toString(%s), '/', dictGetOrDefault('%s', 'name', %s,'')), %s==17, concat(toString(%s), '/', dictGetOrDefault('%s', 'name', %s,'')), toString(%s)), '/$', '')`,
|
|
schema.ColumnProto, qc, schema.DictionaryTCP, qc, schema.ColumnProto, qc, schema.DictionaryUDP, qc, qc)
|
|
|
|
// Generic cases
|
|
default:
|
|
strValue = qc.String()
|
|
if col, ok := sch.LookupColumnByKey(key); ok {
|
|
if strings.HasPrefix(col.ClickHouseType, "UInt") {
|
|
strValue = fmt.Sprintf(`toString(%s)`, qc)
|
|
} else if col.ClickHouseType == "IPv6" || col.ClickHouseType == "LowCardinality(IPv6)" {
|
|
strValue = fmt.Sprintf("replaceRegexpOne(IPv6NumToString(%s), '^::ffff:', '')", qc)
|
|
}
|
|
}
|
|
}
|
|
return strValue
|
|
}
|