diff --git a/.gitignore b/.gitignore index 4109fdf..cc4add4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .idea *.iml forecastmetrics.yaml +locations.yaml /ForecastMetrics /forecastmetrics .DS_Store \ No newline at end of file diff --git a/config.go b/config.go index dacd70d..0bfacc8 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "slices" "sync" @@ -25,7 +26,6 @@ type InfluxConfig struct { // Config is the configuration for ForecastMetrics. type Config struct { - Locations []Location InfluxDB InfluxConfig `yaml:"influxdb"` ForecastMeasurementName string `yaml:"forecast_measurement_name"` AstronomyMeasurementName string `yaml:"astronomy_measurement_name"` @@ -43,44 +43,64 @@ type Config struct { } } -// mustParseConfig parses the config from the given config file, or panics. -func mustParseConfig(configFile string) Config { +// ConfigService provides a way to update and get the latest list of locations that have regular +// forecasts exported to the database. +type ConfigService struct { + Config Config + locationsFile string + lock *sync.Mutex + locations []Location +} + +// NewConfigService initializes a ConfigService by parsing the main config and the locations files. +// It panics if it can't read or parse the configs. +func NewConfigService(configFile, locationsFile string) *ConfigService { // read config - file, err := os.ReadFile(configFile) + cf, err := os.ReadFile(configFile) if err != nil { - panic(err) + panic(fmt.Sprintf("Error reading config file %s: %s", configFile, err)) } var config Config - err = yaml.Unmarshal(file, &config) + err = yaml.Unmarshal(cf, &config) if err != nil { - panic(err) + panic(fmt.Sprintf("Error loading config from %s: %s", configFile, err)) + } + lf, err := os.ReadFile(locationsFile) + if err != nil { + panic(fmt.Sprintf("Error reading locations file %s: %s", locationsFile, err)) + } + var locations []Location + err = yaml.Unmarshal(lf, &locations) + if err != nil { + panic(fmt.Sprintf("Error loading locations from %s: %s", locationsFile, err)) + } + return &ConfigService{ + Config: config, + locationsFile: locationsFile, + lock: &sync.Mutex{}, + locations: locations, } - return config -} - -// ConfigService provides a way to update and get the latest list of locations that have regular -// forecasts exported to the database. -type ConfigService struct { - Config Config - ConfigFile string - lock *sync.Mutex } // HasLocation returns true if this Location is being regularly exported. func (c *ConfigService) HasLocation(location Location) bool { - return slices.Contains(c.Config.Locations, location) + return slices.Contains(c.locations, location) } -// GetLocations returns all actively exported locations. +// GetLocations returns a copy of all actively exported locations. func (c *ConfigService) GetLocations() []Location { - return c.Config.Locations + c.lock.Lock() + defer c.lock.Unlock() + locsCopy := make([]Location, len(c.locations)) + copy(locsCopy, c.locations) + return locsCopy } // AddLocation adds a new location to be regularly exported. It is saved to the config file. func (c *ConfigService) AddLocation(location Location) { c.lock.Lock() defer c.lock.Unlock() - c.Config.Locations = append(c.Config.Locations, location) + c.locations = append(c.locations, location) c.marshall() } @@ -88,22 +108,30 @@ func (c *ConfigService) AddLocation(location Location) { func (c *ConfigService) RemoveLocation(location Location) { c.lock.Lock() defer c.lock.Unlock() - locs := c.Config.Locations + locs := c.locations idx := slices.Index(locs, location) if idx > -1 { - c.Config.Locations = append(locs[:idx], locs[idx+1:]...) + c.locations = append(locs[:idx], locs[idx+1:]...) c.marshall() } } // marshall writes the current configuration to the config file. +// It should only be called while holding the lock. func (c *ConfigService) marshall() { - bytes, err := yaml.Marshal(c.Config) + f, err := os.OpenFile(c.locationsFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + panic(fmt.Sprintf("Error opening locations file %s: %s", c.locationsFile, err)) + } + defer f.Close() + encoder := yaml.NewEncoder(f) + encoder.SetIndent(2) + err = encoder.Encode(c.locations) if err != nil { - panic(err) + panic(fmt.Sprintf("Error saving locations to %s: %s", c.locationsFile, err)) } - err = os.WriteFile(c.ConfigFile, bytes, 0644) + err = encoder.Close() if err != nil { - panic(err) + panic(fmt.Sprintf("Error saving locations to %s: %s", c.locationsFile, err)) } } diff --git a/config/forecastmetrics.example.yaml b/forecastmetrics.example.yaml similarity index 89% rename from config/forecastmetrics.example.yaml rename to forecastmetrics.example.yaml index 511e16a..4af88a6 100644 --- a/config/forecastmetrics.example.yaml +++ b/forecastmetrics.example.yaml @@ -1,8 +1,3 @@ -locations: - - name: Washington Monument - latitude: 38.8895 - longitude: -77.0352 - influxdb: host: http://localhost:8086 # for influx 1.8/VictoriaMetrics, use "user:password" diff --git a/locations.example.yaml b/locations.example.yaml new file mode 100644 index 0000000..6acedf5 --- /dev/null +++ b/locations.example.yaml @@ -0,0 +1,3 @@ +- name: Washington Monument + latitude: 38.8895 + longitude: -77.0352 \ No newline at end of file diff --git a/main.go b/main.go index 8dfd604..9c9f875 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "slices" - "sync" "github.com/gregjones/httpcache" "github.com/gregjones/httpcache/diskcache" @@ -24,19 +23,16 @@ var ( func main() { // parse flags configFile := flag.String("config", "forecastmetrics.yaml", "Config file") + locationsFile := flag.String("locations", "locations.yaml", "Locations file") versionFlag := flag.Bool("v", false, "Show version and exit") flag.Parse() fmt.Printf("ForecastMetrics version %s built on %s with %s\n", version, buildDate, goVersion) if *versionFlag { os.Exit(0) } - config := mustParseConfig(*configFile) + configService := NewConfigService(*configFile, *locationsFile) + config := configService.Config locationService := LocationService{BingToken: config.BingToken} - configService := &ConfigService{ - Config: config, - ConfigFile: *configFile, - lock: &sync.Mutex{}, - } forecasters := MakeForecasters(config.Sources.Enabled, config.HttpCacheDir, config.Sources.VisualCrossing.Key) c := influxdb2.NewClient(config.InfluxDB.Host, config.InfluxDB.AuthToken) writeApi := c.WriteAPIBlocking(config.InfluxDB.Org, config.InfluxDB.Bucket) @@ -96,7 +92,8 @@ func MakeForecasters(enabled []string, cacheDir string, vcKey string) map[string // todo // deployment stuff -// increment version // grafana dashboards // make influx forwarded token and our required auth token allowed to be different // update readme +// allow http server functionality to be turned off if desired, by not including a port to listen on or something +// also allow proxy to be turned off