mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-12 06:24:10 +01:00
This requires usage of `reflect.MethodByName`, which prevents DCE. This does not really matter as DCE is not smart enough to detect we don't use it, but the day we have an alternative not supporting functions call, we would be ready. Or maybe there is an alternative. Or we could use `strings.ReplaceAll()` instead.
275 lines
8.3 KiB
Go
275 lines
8.3 KiB
Go
// SPDX-FileCopyrightText: 2022 Free Mobile
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package console
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"akvorado/common/helpers"
|
|
"akvorado/common/schema"
|
|
)
|
|
|
|
func (c *Component) widgetFlowLastHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
replace := []struct {
|
|
key schema.ColumnKey
|
|
replaceWith string
|
|
}{
|
|
{schema.ColumnDstCommunities, `arrayMap(c -> concat(toString(bitShiftRight(c, 16)), ':',
|
|
toString(bitAnd(c, 0xffff))), DstCommunities)`},
|
|
{schema.ColumnDstLargeCommunities, `arrayMap(c -> concat(toString(bitAnd(bitShiftRight(c, 64), 0xffffffff)), ':',
|
|
toString(bitAnd(bitShiftRight(c, 32), 0xffffffff)), ':',
|
|
toString(bitAnd(c, 0xffffffff))), DstLargeCommunities)`},
|
|
{schema.ColumnSrcMAC, `MACNumToString(SrcMAC)`},
|
|
{schema.ColumnDstMAC, `MACNumToString(DstMAC)`},
|
|
}
|
|
selectClause := []string{"SELECT *"}
|
|
except := []string{}
|
|
for _, r := range replace {
|
|
if column, ok := c.d.Schema.LookupColumnByKey(r.key); ok && !column.Disabled {
|
|
except = append(except, r.key.String())
|
|
selectClause = append(selectClause, fmt.Sprintf("%s AS %s", r.replaceWith, r.key))
|
|
}
|
|
}
|
|
if len(except) > 0 {
|
|
selectClause[0] = fmt.Sprintf("SELECT * EXCEPT (%s)", strings.Join(except, ", "))
|
|
}
|
|
query := fmt.Sprintf(`
|
|
%s
|
|
FROM flows
|
|
WHERE TimeReceived=(SELECT MAX(TimeReceived) FROM flows)
|
|
LIMIT 1`, strings.Join(selectClause, ",\n "))
|
|
gc.Header("X-SQL-Query", query)
|
|
// Do not increase counter for this one.
|
|
rows, err := c.d.ClickHouseDB.Conn.Query(ctx, query)
|
|
if err != nil {
|
|
c.r.Err(err).Msg("unable to query database")
|
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "Unable to query database."})
|
|
return
|
|
}
|
|
|
|
if !rows.Next() {
|
|
gc.JSON(http.StatusNotFound, gin.H{"message": "No flow currently in database."})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var (
|
|
response = gin.H{}
|
|
columnTypes = rows.ColumnTypes()
|
|
vars = make([]any, len(columnTypes))
|
|
)
|
|
for i := range columnTypes {
|
|
vars[i] = reflect.New(columnTypes[i].ScanType()).Interface()
|
|
}
|
|
if err := rows.Scan(vars...); err != nil {
|
|
c.r.Err(err).Msg("unable to parse flow")
|
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "Unable to parse flow."})
|
|
return
|
|
}
|
|
for index, column := range rows.Columns() {
|
|
response[column] = vars[index]
|
|
}
|
|
gc.IndentedJSON(http.StatusOK, response)
|
|
}
|
|
|
|
func (c *Component) widgetFlowRateHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
query := `SELECT COUNT(*)/300 AS rate FROM flows WHERE TimeReceived > date_sub(minute, 5, now())`
|
|
gc.Header("X-SQL-Query", query)
|
|
// Do not increase counter for this one.
|
|
var result float64
|
|
row := c.d.ClickHouseDB.Conn.QueryRow(ctx, query)
|
|
if err := row.Scan(&result); err != nil {
|
|
c.r.Err(err).Msg("unable to parse result")
|
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "Unable to parse result."})
|
|
return
|
|
}
|
|
gc.IndentedJSON(http.StatusOK, gin.H{
|
|
"rate": result,
|
|
"period": "second",
|
|
})
|
|
}
|
|
|
|
func (c *Component) widgetExportersHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
query := `SELECT ExporterName FROM exporters GROUP BY ExporterName ORDER BY ExporterName`
|
|
gc.Header("X-SQL-Query", query)
|
|
// Do not increase counter for this one.
|
|
|
|
exporters := []struct {
|
|
ExporterName string
|
|
}{}
|
|
err := c.d.ClickHouseDB.Conn.Select(ctx, &exporters, query)
|
|
if err != nil {
|
|
c.r.Err(err).Msg("unable to query database")
|
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "Unable to query database."})
|
|
return
|
|
}
|
|
exporterList := make([]string, len(exporters))
|
|
for idx, exporter := range exporters {
|
|
exporterList[idx] = exporter.ExporterName
|
|
}
|
|
|
|
gc.IndentedJSON(http.StatusOK, gin.H{"exporters": exporterList})
|
|
}
|
|
|
|
// UnmarshalParam is similar to UnmarshalText but for Gin.
|
|
func (i *HomepageTopWidget) UnmarshalParam(param string) error {
|
|
var err error
|
|
*i, err = HomepageTopWidgetString(param)
|
|
return err
|
|
}
|
|
|
|
type topResult struct {
|
|
Name string `json:"name"`
|
|
Percent float64 `json:"percent"`
|
|
}
|
|
|
|
func (c *Component) widgetTopHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
var (
|
|
selector string
|
|
groupby string
|
|
filter string
|
|
mainTableRequired bool
|
|
)
|
|
|
|
type URIParams struct {
|
|
WidgetName HomepageTopWidget `uri:"name" binding:"required"`
|
|
}
|
|
var uriParams URIParams
|
|
if err := gc.ShouldBindUri(&uriParams); err != nil {
|
|
gc.JSON(http.StatusBadRequest, gin.H{"message": helpers.Capitalize(err.Error())})
|
|
return
|
|
}
|
|
|
|
switch uriParams.WidgetName {
|
|
case HomepageTopWidgetSrcAS:
|
|
selector = fmt.Sprintf(`concat(toString(SrcAS), ': ', dictGetOrDefault('%s', 'name', SrcAS, '???'))`, schema.DictionaryASNs)
|
|
groupby = `SrcAS`
|
|
case HomepageTopWidgetDstAS:
|
|
selector = fmt.Sprintf(`concat(toString(DstAS), ': ', dictGetOrDefault('%s', 'name', DstAS, '???'))`, schema.DictionaryASNs)
|
|
groupby = `DstAS`
|
|
case HomepageTopWidgetSrcCountry:
|
|
selector = `SrcCountry`
|
|
case HomepageTopWidgetDstCountry:
|
|
selector = `DstCountry`
|
|
case HomepageTopWidgetExporter:
|
|
selector = "ExporterName"
|
|
case HomepageTopWidgetProtocol:
|
|
selector = fmt.Sprintf(`dictGetOrDefault('%s', 'name', Proto, '???')`, schema.DictionaryProtocols)
|
|
groupby = `Proto`
|
|
case HomepageTopWidgetEtype:
|
|
selector = `if(equals(EType, 34525), 'IPv6', if(equals(EType, 2048), 'IPv4', '???'))`
|
|
groupby = `EType`
|
|
case HomepageTopWidgetSrcPort:
|
|
selector = fmt.Sprintf(`concat(dictGetOrDefault('%s', 'name', Proto, '???'), '/', toString(SrcPort))`, schema.DictionaryProtocols)
|
|
groupby = `Proto, SrcPort`
|
|
mainTableRequired = true
|
|
case HomepageTopWidgetDstPort:
|
|
selector = fmt.Sprintf(`concat(dictGetOrDefault('%s', 'name', Proto, '???'), '/', toString(DstPort))`, schema.DictionaryProtocols)
|
|
groupby = `Proto, DstPort`
|
|
mainTableRequired = true
|
|
default:
|
|
gc.JSON(http.StatusNotFound, gin.H{"message": "Unknown top request."})
|
|
return
|
|
}
|
|
if strings.HasPrefix(gc.Param("name"), "src-") {
|
|
filter = "AND InIfBoundary = 'external'"
|
|
} else if strings.HasPrefix(gc.Param("name"), "dst-") {
|
|
filter = "AND OutIfBoundary = 'external'"
|
|
}
|
|
if groupby == "" {
|
|
groupby = selector
|
|
}
|
|
|
|
now := c.d.Clock.Now()
|
|
template := fmt.Sprintf(`
|
|
WITH
|
|
(SELECT SUM(Bytes*SamplingRate) FROM {{ .Table }} WHERE {{ .Timefilter }} %s) AS Total
|
|
SELECT
|
|
if(empty(%s),'Unknown',%s) AS Name,
|
|
SUM(Bytes*SamplingRate) / Total * 100 AS Percent
|
|
FROM {{ .Table }}
|
|
WHERE {{ .Timefilter }}
|
|
%s
|
|
GROUP BY %s
|
|
ORDER BY Percent DESC
|
|
LIMIT 5`,
|
|
filter, selector, selector, filter, groupby)
|
|
|
|
query := c.finalizeTemplateQuery(templateQuery{
|
|
Template: template,
|
|
Context: inputContext{
|
|
Start: now.Add(-5 * time.Minute),
|
|
End: now,
|
|
MainTableRequired: mainTableRequired,
|
|
Points: 5,
|
|
},
|
|
})
|
|
gc.Header("X-SQL-Query", query)
|
|
|
|
results := []topResult{}
|
|
err := c.d.ClickHouseDB.Conn.Select(ctx, &results, strings.TrimSpace(query))
|
|
if err != nil {
|
|
c.r.Err(err).Msg("unable to query database")
|
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "Unable to query database."})
|
|
return
|
|
}
|
|
gc.JSON(http.StatusOK, gin.H{"top": results})
|
|
}
|
|
|
|
func (c *Component) widgetGraphHandlerFunc(gc *gin.Context) {
|
|
filter := c.config.HomepageGraphFilter
|
|
if filter != "" {
|
|
filter = fmt.Sprintf("AND %s", filter)
|
|
}
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
now := c.d.Clock.Now()
|
|
template := fmt.Sprintf(`
|
|
SELECT
|
|
{{ .ToStartOfInterval }} AS Time,
|
|
SUM(Bytes*SamplingRate*8/{{ .Interval }})/1000/1000/1000 AS Gbps
|
|
FROM {{ .Table }}
|
|
WHERE {{ .Timefilter }}
|
|
%s
|
|
GROUP BY Time
|
|
ORDER BY Time WITH FILL
|
|
FROM {{ .TimefilterStart }}
|
|
TO {{ .TimefilterEnd }} + INTERVAL 1 second
|
|
STEP {{ .Interval }}`,
|
|
filter)
|
|
|
|
query := c.finalizeTemplateQuery(templateQuery{
|
|
Template: template,
|
|
Context: inputContext{
|
|
Start: now.Add(-c.config.HomepageGraphTimeRange),
|
|
End: now,
|
|
MainTableRequired: false,
|
|
Points: 200,
|
|
},
|
|
})
|
|
gc.Header("X-SQL-Query", query)
|
|
|
|
results := []struct {
|
|
Time time.Time `json:"t"`
|
|
Gbps float64 `json:"gbps"`
|
|
}{}
|
|
err := c.d.ClickHouseDB.Conn.Select(ctx, &results, strings.TrimSpace(query))
|
|
if err != nil {
|
|
c.r.Err(err).Msg("unable to query database")
|
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "Unable to query database."})
|
|
return
|
|
}
|
|
|
|
gc.JSON(http.StatusOK, gin.H{"data": results})
|
|
}
|