web: add a reverse proxy to Grafana

This commit is contained in:
Vincent Bernat
2022-03-17 22:42:44 +01:00
parent 91e3d7661f
commit ad14826f6f
6 changed files with 98 additions and 6 deletions

View File

@@ -31,6 +31,7 @@ type ServeConfiguration struct {
GeoIP geoip.Configuration GeoIP geoip.Configuration
Kafka kafka.Configuration Kafka kafka.Configuration
Core core.Configuration Core core.Configuration
Web web.Configuration
} }
// DefaultServeConfiguration is the default configuration for the serve command. // DefaultServeConfiguration is the default configuration for the serve command.
@@ -42,6 +43,7 @@ var DefaultServeConfiguration = ServeConfiguration{
GeoIP: geoip.DefaultConfiguration, GeoIP: geoip.DefaultConfiguration,
Kafka: kafka.DefaultConfiguration, Kafka: kafka.DefaultConfiguration,
Core: core.DefaultConfiguration, Core: core.DefaultConfiguration,
Web: web.DefaultConfiguration,
} }
type serveOptions struct { type serveOptions struct {
@@ -156,7 +158,7 @@ func daemonStart(r *reporter.Reporter, config ServeConfiguration, checkOnly bool
if err != nil { if err != nil {
return fmt.Errorf("unable to initialize core component: %w", err) return fmt.Errorf("unable to initialize core component: %w", err)
} }
webComponent, err := web.New(r, web.Dependencies{ webComponent, err := web.New(r, config.Web, web.Dependencies{
HTTP: httpComponent, HTTP: httpComponent,
}) })
if err != nil { if err != nil {

View File

@@ -5,6 +5,7 @@ configured through a different section:
- `reporting`: [Log and metric reporting](#reporting) - `reporting`: [Log and metric reporting](#reporting)
- `http`: [Builtin HTTP server](#http) - `http`: [Builtin HTTP server](#http)
- `web`: [Web interface](#web)
- `flow`: [Flow ingestion](#flow) - `flow`: [Flow ingestion](#flow)
- `snmp`: [SNMP poller](#snmp) - `snmp`: [SNMP poller](#snmp)
- `geoip`: [GeoIP database](#geoip) - `geoip`: [GeoIP database](#geoip)
@@ -34,6 +35,14 @@ http:
listen: 0.0.0.0:8000 listen: 0.0.0.0:8000
``` ```
## Web
The web interface presents the landing page of *Akvorado*. It also
embeds the documentation. It accepts only the following key:
- `grafanaurl` to specify the URL to Grafana and exposes it as
`/grafana`.
## Flow ## Flow
The flow component handles flow ingestion. It supports the following The flow component handles flow ingestion. It supports the following

View File

@@ -23,5 +23,6 @@ The embedded HTTP server serves the following endpoints:
- [`/healthcheck`](/healthcheck){ target=http }: are we alive? - [`/healthcheck`](/healthcheck){ target=http }: are we alive?
- [`/flows`](/flows?limit=1){ target=http }: next available flow - [`/flows`](/flows?limit=1){ target=http }: next available flow
- [`/flow.proto`](/flow.proto){ target=http }: protocol buffers definition - [`/flow.proto`](/flow.proto){ target=http }: protocol buffers definition
- [`/grafana`](/grafana): Grafana web interface (if configured)
<iframe name="http" style="width: 100%; height: 200px; border: 0; background-color: #1111"></iframe> <iframe name="http" style="width: 100%; height: 200px; border: 0; background-color: #1111"></iframe>

10
web/config.go Normal file
View File

@@ -0,0 +1,10 @@
package web
// Configuration describes the configuration for the web component.
type Configuration struct {
// GrafanaURL is the URL to acess Grafana.
GrafanaURL string
}
// DefaultConfiguration represents the default configuration for the web exporter.
var DefaultConfiguration = Configuration{}

View File

@@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
netHTTP "net/http" netHTTP "net/http"
"net/http/httputil"
"net/url"
"akvorado/http" "akvorado/http"
"akvorado/reporter" "akvorado/reporter"
@@ -16,8 +18,9 @@ var rootSite embed.FS
// Component represents the web component. // Component represents the web component.
type Component struct { type Component struct {
r *reporter.Reporter r *reporter.Reporter
d *Dependencies d *Dependencies
config Configuration
} }
// Dependencies define the dependencies of the web component. // Dependencies define the dependencies of the web component.
@@ -26,15 +29,33 @@ type Dependencies struct {
} }
// New creates a new web component. // New creates a new web component.
func New(reporter *reporter.Reporter, dependencies Dependencies) (*Component, error) { func New(reporter *reporter.Reporter, config Configuration, dependencies Dependencies) (*Component, error) {
c := Component{ c := Component{
r: reporter, r: reporter,
d: &dependencies, d: &dependencies,
config: config,
} }
data, err := fs.Sub(rootSite, "data") data, err := fs.Sub(rootSite, "data")
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to get embedded website: %w", err) return nil, fmt.Errorf("unable to get embedded website: %w", err)
} }
c.d.HTTP.AddHandler("/", netHTTP.FileServer(netHTTP.FS(data))) c.d.HTTP.AddHandler("/", netHTTP.FileServer(netHTTP.FS(data)))
if c.config.GrafanaURL != "" {
// Provide a proxy for Grafana
url, err := url.Parse(config.GrafanaURL)
if err != nil {
return nil, fmt.Errorf("unable to parse Grafana URL %q: %w", config.GrafanaURL, err)
}
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.Transport = &netHTTP.Transport{
Proxy: nil, // Disable proxy
}
proxyHandler := netHTTP.HandlerFunc(
func(w netHTTP.ResponseWriter, r *netHTTP.Request) {
fmt.Println("hello")
proxy.ServeHTTP(w, r)
})
c.d.HTTP.AddHandler("/grafana/", proxyHandler)
}
return &c, nil return &c, nil
} }

49
web/root_test.go Normal file
View File

@@ -0,0 +1,49 @@
package web
import (
"fmt"
"io/ioutil"
netHTTP "net/http"
"net/http/httptest"
"testing"
"akvorado/helpers"
"akvorado/http"
"akvorado/reporter"
)
func TestProxy(t *testing.T) {
// Mock HTTP server
server := httptest.NewServer(
netHTTP.HandlerFunc(
func(w netHTTP.ResponseWriter, r *netHTTP.Request) {
fmt.Fprintf(w, "hello world!")
}))
defer server.Close()
r := reporter.NewMock(t)
h := http.NewMock(t, r)
_, err := New(r, Configuration{
GrafanaURL: server.URL,
}, Dependencies{HTTP: h})
if err != nil {
t.Fatalf("New() error:\n%+v", err)
}
// Check the proxy works as expected
resp, err := netHTTP.Get(fmt.Sprintf("http://%s/grafana/test", h.Address))
if err != nil {
t.Fatalf("GET /grafana/test:\n%+v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("GET /grafana/test: cannot read body:\n%+v", err)
}
if resp.StatusCode != 200 {
t.Errorf("GET /grafana/test: got status code %d, not 200", resp.StatusCode)
}
if diff := helpers.Diff(string(body), "hello world!"); diff != "" {
t.Errorf("GET /grafana/test (-got, +want):\n%s", diff)
}
}