mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
This is a first step to make it accept configuration. Most of the changes are quite trivial, but I also ran into some difficulties with query columns and filters. They need the schema for parsing, but parsing happens before dependencies are instantiated (and even if it was not the case, parsing is stateless). Therefore, I have added a `Validate()` method that must be called after instantiation. Various bits `panic()` if not validated to ensure we catch all cases. The alternative to make the component manages a global state would have been simpler but it would break once we add the ability to add or disable columns.
150 lines
4.5 KiB
Go
150 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"
|
|
netHTTP "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/http"
|
|
"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 *http.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("/", 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.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", c.d.HTTP.CacheByRequestBody(c.config.CacheTTL), c.graphHandlerFunc)
|
|
endpoint.POST("/sankey", c.d.HTTP.CacheByRequestBody(c.config.CacheTTL), c.sankeyHandlerFunc)
|
|
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
|
|
}
|