mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
161 lines
4.1 KiB
Go
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()
|
|
}
|