Files
akvorado/geoip/root.go
2022-03-31 20:56:14 +02:00

161 lines
4.1 KiB
Go

// Package geoip provides ASN and country for GeoIP addresses.
package geoip
import (
"fmt"
"path/filepath"
"sync/atomic"
"time"
"github.com/fsnotify/fsnotify"
"github.com/oschwald/geoip2-golang"
"gopkg.in/tomb.v2"
"akvorado/daemon"
"akvorado/reporter"
)
// Component represents the GeoIP component.
type Component struct {
r *reporter.Reporter
d *Dependencies
t tomb.Tomb
config Configuration
db struct {
country atomic.Value // *geoip2.Reader
asn atomic.Value // *geoip2.Reader
}
metrics struct {
databaseRefresh *reporter.CounterVec
}
}
// Dependencies define the dependencies of the GeoIP component.
type Dependencies struct {
Daemon daemon.Component
}
// New creates a new GeoIP component.
func New(r *reporter.Reporter, configuration Configuration, dependencies Dependencies) (*Component, error) {
c := Component{
r: r,
d: &dependencies,
config: configuration,
}
if c.config.CountryDatabase != "" {
c.config.CountryDatabase = filepath.Clean(c.config.CountryDatabase)
}
if c.config.ASNDatabase != "" {
c.config.ASNDatabase = filepath.Clean(c.config.ASNDatabase)
}
c.d.Daemon.Track(&c.t, "geoip")
c.metrics.databaseRefresh = c.r.CounterVec(
reporter.CounterOpts{
Name: "db_refresh_total",
Help: "Refresh event for a GeoIP database.",
},
[]string{"database"},
)
return &c, nil
}
// openDatabase opens the provided database and closes the current
// one. Do nothing if the path is empty.
func (c *Component) openDatabase(which string, path string, container *atomic.Value) error {
if path == "" {
return nil
}
c.r.Debug().Str("database", path).Msgf("opening %s database", which)
db, err := geoip2.Open(path)
if err != nil {
c.r.Err(err).
Str("database", path).
Msgf("cannot open %s database", which)
return fmt.Errorf("cannot open %s database: %w", which, err)
}
old := container.Swap(db)
c.metrics.databaseRefresh.WithLabelValues(which).Inc()
if old != nil {
c.r.Debug().
Str("database", path).
Msgf("closing previous %s database", which)
old.(*geoip2.Reader).Close()
}
return nil
}
// Start starts the GeoIP component.
func (c *Component) Start() error {
if err := c.openDatabase("country", c.config.CountryDatabase, &c.db.country); err != nil {
return err
}
if err := c.openDatabase("asn", c.config.ASNDatabase, &c.db.asn); err != nil {
return err
}
if c.db.country.Load() == nil && c.db.asn.Load() == nil {
c.r.Warn().Msg("skipping GeoIP component: no database specified")
return nil
}
c.r.Info().Msg("starting GeoIP component")
// Watch for modifications
watcher, err := fsnotify.NewWatcher()
if err != nil {
c.r.Err(err).Msg("cannot setup watcher for GeoIP databases")
return fmt.Errorf("cannot setup watcher: %w", err)
}
dirs := map[string]bool{}
if c.config.CountryDatabase != "" {
dirs[filepath.Dir(c.config.CountryDatabase)] = true
}
if c.config.ASNDatabase != "" {
dirs[filepath.Dir(c.config.ASNDatabase)] = true
}
for k := range dirs {
if err := watcher.Add(k); err != nil {
c.r.Err(err).Msg("cannot watch database directory")
return fmt.Errorf("cannot watch database directory: %w", err)
}
}
c.t.Go(func() error {
errLogger := c.r.Sample(reporter.BurstSampler(10*time.Second, 1))
defer watcher.Close()
for {
// Watch both for errors and events in the
// same goroutine. fsnotify's FAQ says this is
// not a good idea.
select {
case <-c.t.Dying():
return nil
case err := <-watcher.Errors:
errLogger.Err(err).Msg("error from watcher")
case event := <-watcher.Events:
if event.Op&(fsnotify.Write|fsnotify.Create) == 0 {
continue
}
if filepath.Clean(event.Name) == c.config.CountryDatabase {
c.openDatabase("country", c.config.CountryDatabase, &c.db.country)
}
if filepath.Clean(event.Name) == c.config.ASNDatabase {
c.openDatabase("asn", c.config.ASNDatabase, &c.db.asn)
}
}
}
})
return nil
}
// Stop stops the GeoIP component.
func (c *Component) Stop() error {
if c.db.country.Load() == nil && c.db.asn.Load() == nil {
return nil
}
c.r.Info().Msg("stopping GeoIP component")
defer c.r.Info().Msg("GeoIP component stopped")
c.t.Kill(nil)
return c.t.Wait()
}