// 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 {{ call .ToStartOfInterval "TimeReceived" }} 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}) }