Files
akvorado/console/root.go
Vincent Bernat 67703cc61e console: use templates to build SQL query
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").
2022-08-09 11:45:40 +02:00

148 lines
4.0 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
// Package console exposes a web interface.
package console
import (
"io/fs"
netHTTP "net/http"
"os"
"path"
"path/filepath"
"runtime"
"sync"
"text/template"
"time"
"github.com/benbjohnson/clock"
"gopkg.in/tomb.v2"
"akvorado/common/clickhousedb"
"akvorado/common/daemon"
"akvorado/common/http"
"akvorado/common/reporter"
"akvorado/console/authentication"
"akvorado/console/database"
)
// Component represents the console component.
type Component struct {
r *reporter.Reporter
d *Dependencies
t tomb.Tomb
config Configuration
templates map[string]*template.Template
templatesLock sync.RWMutex
flowsTables []flowsTable
flowsTablesLock sync.RWMutex
metrics struct {
clickhouseQueries *reporter.CounterVec
}
}
// Dependencies define the dependencies of the console component.
type Dependencies struct {
Daemon daemon.Component
HTTP *http.Component
ClickHouseDB *clickhousedb.Component
Clock clock.Clock
Auth *authentication.Component
Database *database.Component
}
// New creates a new console component.
func New(r *reporter.Reporter, config Configuration, dependencies Dependencies) (*Component, error) {
if dependencies.Clock == nil {
dependencies.Clock = clock.New()
}
c := Component{
r: r,
d: &dependencies,
config: config,
flowsTables: []flowsTable{{"flows", 0, time.Time{}}},
}
c.d.Daemon.Track(&c.t, "console")
c.metrics.clickhouseQueries = c.r.CounterVec(
reporter.CounterOpts{
Name: "clickhouse_queries_total",
Help: "Number of requests to ClickHouse.",
}, []string{"table"},
)
return &c, nil
}
// Start starts the console component.
func (c *Component) Start() error {
c.r.Info().Msg("starting console component")
c.d.HTTP.AddHandler("/", netHTTP.HandlerFunc(c.assetsHandlerFunc))
endpoint := c.d.HTTP.GinRouter.Group("/api/v0/console", c.d.Auth.UserAuthentication())
endpoint.GET("/configuration", c.configHandlerFunc)
endpoint.GET("/docs/:name", c.docsHandlerFunc)
endpoint.GET("/widget/flow-last", c.widgetFlowLastHandlerFunc)
endpoint.GET("/widget/flow-rate", c.widgetFlowRateHandlerFunc)
endpoint.GET("/widget/exporters", c.widgetExportersHandlerFunc)
endpoint.GET("/widget/top/:name", c.widgetTopHandlerFunc)
endpoint.GET("/widget/graph", c.widgetGraphHandlerFunc)
endpoint.POST("/graph", c.graphHandlerFunc)
endpoint.POST("/sankey", c.sankeyHandlerFunc)
endpoint.POST("/filter/validate", c.filterValidateHandlerFunc)
endpoint.POST("/filter/complete", c.filterCompleteHandlerFunc)
endpoint.GET("/filter/saved", c.filterSavedListHandlerFunc)
endpoint.DELETE("/filter/saved/:id", c.filterSavedDeleteHandlerFunc)
endpoint.POST("/filter/saved", c.filterSavedAddHandlerFunc)
endpoint.GET("/user/info", c.d.Auth.UserInfoHandlerFunc)
endpoint.GET("/user/avatar", c.d.Auth.UserAvatarHandlerFunc)
c.t.Go(func() error {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := c.refreshFlowsTables(); err != nil {
c.r.Err(err).Msg("cannot refresh flows tables")
continue
}
// Once successful, do that less often
ticker.Reset(10 * time.Minute)
case <-c.t.Dying():
return nil
}
}
})
return nil
}
// Stop stops the console component.
func (c *Component) Stop() error {
defer c.r.Info().Msg("console component stopped")
c.r.Info().Msg("stopping console component")
c.t.Kill(nil)
return c.t.Wait()
}
// embedOrLiveFS returns a subset of the provided embedded filesystem,
// except if the component is configured to use the live filesystem.
// Then, it returns the provided tree.
func (c *Component) embedOrLiveFS(embed fs.FS, p string) fs.FS {
var fileSystem fs.FS
if c.config.ServeLiveFS {
_, src, _, _ := runtime.Caller(0)
fileSystem = os.DirFS(filepath.Join(path.Dir(src), p))
} else {
var err error
fileSystem, err = fs.Sub(embed, p)
if err != nil {
panic(err)
}
}
return fileSystem
}