mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-12 06:24:10 +01:00
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").
239 lines
6.8 KiB
Go
239 lines
6.8 KiB
Go
// SPDX-FileCopyrightText: 2022 Free Mobile
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package console
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"akvorado/common/helpers"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func (c *Component) widgetFlowLastHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
query := `SELECT * FROM flows WHERE TimeReceived = (SELECT MAX(TimeReceived) FROM flows) LIMIT 1`
|
|
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([]interface{}, 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.
|
|
row := c.d.ClickHouseDB.Conn.QueryRow(ctx, query)
|
|
if err := row.Err(); err != nil {
|
|
c.r.Err(err).Msg("unable to query database")
|
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "Unable to query database."})
|
|
return
|
|
}
|
|
var result float64
|
|
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})
|
|
}
|
|
|
|
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
|
|
)
|
|
|
|
switch gc.Param("name") {
|
|
default:
|
|
gc.JSON(http.StatusNotFound, gin.H{"message": "Unknown top request."})
|
|
return
|
|
case "src-as":
|
|
selector = `concat(toString(SrcAS), ': ', dictGetOrDefault('asns', 'name', SrcAS, '???'))`
|
|
groupby = `SrcAS`
|
|
filter = "AND InIfBoundary = 'external'"
|
|
case "dst-as":
|
|
selector = `concat(toString(DstAS), ': ', dictGetOrDefault('asns', 'name', DstAS, '???'))`
|
|
groupby = `DstAS`
|
|
filter = "AND OutIfBoundary = 'external'"
|
|
case "src-country":
|
|
selector = `SrcCountry`
|
|
filter = "AND InIfBoundary = 'external'"
|
|
case "dst-country":
|
|
selector = `DstCountry`
|
|
filter = "AND OutIfBoundary = 'external'"
|
|
case "exporter":
|
|
selector = "ExporterName"
|
|
case "protocol":
|
|
selector = `dictGetOrDefault('protocols', 'name', Proto, '???')`
|
|
groupby = `Proto`
|
|
case "etype":
|
|
selector = `if(equals(EType, 34525), 'IPv6', if(equals(EType, 2048), 'IPv4', '???'))`
|
|
groupby = `EType`
|
|
case "src-port":
|
|
selector = `concat(dictGetOrDefault('protocols', 'name', Proto, '???'), '/', toString(SrcPort))`
|
|
groupby = `Proto, SrcPort`
|
|
mainTableRequired = true
|
|
case "dst-port":
|
|
selector = `concat(dictGetOrDefault('protocols', 'name', Proto, '???'), '/', toString(DstPort))`
|
|
groupby = `Proto, DstPort`
|
|
mainTableRequired = true
|
|
}
|
|
if groupby == "" {
|
|
groupby = selector
|
|
}
|
|
|
|
now := c.d.Clock.Now()
|
|
query := c.finalizeQuery(fmt.Sprintf(`
|
|
{{ with %s }}
|
|
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
|
|
{{ end }}`,
|
|
templateContext(inputContext{
|
|
Start: now.Add(-5 * time.Minute),
|
|
End: now,
|
|
MainTableRequired: mainTableRequired,
|
|
Points: 5,
|
|
}),
|
|
filter, selector, selector, filter, groupby))
|
|
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})
|
|
}
|
|
|
|
type widgetParameters struct {
|
|
Points uint `form:"points" binding:"isdefault|min=5,max=1000"`
|
|
}
|
|
|
|
func (c *Component) widgetGraphHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
|
|
var params widgetParameters
|
|
if err := gc.ShouldBindQuery(¶ms); err != nil {
|
|
gc.JSON(http.StatusBadRequest, gin.H{"message": helpers.Capitalize(err.Error())})
|
|
return
|
|
}
|
|
if params.Points == 0 {
|
|
params.Points = 200
|
|
}
|
|
now := c.d.Clock.Now()
|
|
query := c.finalizeQuery(fmt.Sprintf(`
|
|
{{ with %s }}
|
|
SELECT
|
|
{{ call .ToStartOfInterval "TimeReceived" }} AS Time,
|
|
SUM(Bytes*SamplingRate*8/{{ .Interval }})/1000/1000/1000 AS Gbps
|
|
FROM {{ .Table }}
|
|
WHERE {{ .Timefilter }}
|
|
AND InIfBoundary = 'external'
|
|
GROUP BY Time
|
|
ORDER BY Time WITH FILL
|
|
FROM {{ .TimefilterStart }}
|
|
TO {{ .TimefilterEnd }}
|
|
STEP {{ .Interval }}
|
|
{{ end }}`,
|
|
templateContext(inputContext{
|
|
Start: now.Add(-24 * time.Hour),
|
|
End: now,
|
|
MainTableRequired: false,
|
|
Points: params.Points,
|
|
})))
|
|
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})
|
|
}
|