Files
akvorado/console/root.go

151 lines
4.5 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"
"net/http"
"os"
"path"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/benbjohnson/clock"
"gopkg.in/tomb.v2"
"akvorado/common/clickhousedb"
"akvorado/common/daemon"
"akvorado/common/httpserver"
"akvorado/common/reporter"
"akvorado/common/schema"
"akvorado/console/authentication"
"akvorado/console/database"
"akvorado/console/query"
)
// Component represents the console component.
type Component struct {
r *reporter.Reporter
d *Dependencies
t tomb.Tomb
config Configuration
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 *httpserver.Component
ClickHouseDB *clickhousedb.Component
Clock clock.Clock
Auth *authentication.Component
Database *database.Component
Schema *schema.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()
}
if err := query.Columns(config.DefaultVisualizeOptions.Dimensions).Validate(dependencies.Schema); err != nil {
return nil, err
}
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("/", http.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.d.HTTP.CacheByRequestPath(5*time.Second), c.widgetFlowLastHandlerFunc)
endpoint.GET("/widget/flow-rate", c.d.HTTP.CacheByRequestPath(5*time.Second), c.widgetFlowRateHandlerFunc)
endpoint.GET("/widget/exporters", c.d.HTTP.CacheByRequestPath(30*time.Second), c.widgetExportersHandlerFunc)
endpoint.GET("/widget/top/:name", c.d.HTTP.CacheByRequestPath(30*time.Second), c.widgetTopHandlerFunc)
endpoint.GET("/widget/graph", c.d.HTTP.CacheByRequestPath(5*time.Minute), c.widgetGraphHandlerFunc)
endpoint.POST("/graph/line", c.d.HTTP.CacheByRequestBody(c.config.CacheTTL), c.graphLineHandlerFunc)
endpoint.POST("/graph/sankey", c.d.HTTP.CacheByRequestBody(c.config.CacheTTL), c.graphSankeyHandlerFunc)
endpoint.POST("/graph/table-interval", c.getTableAndIntervalHandlerFunc)
endpoint.POST("/filter/validate", c.filterValidateHandlerFunc)
endpoint.POST("/filter/complete", c.d.HTTP.CacheByRequestBody(time.Minute), 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
}