diff --git a/docker-compose.yml b/docker-compose.yml index b88400f..605ea37 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,9 @@ services: - GECKO_URL=http://wiremock:8080 - COINRANKING_URL=http://wiremock:8080 - COINRANKING_TOKEN=suchtoken + - PROMETHEUS_URL=http://wiremock:8080 + - PROMETHEUS_USERNAME=prometheus + - PROMETHEUS_PASSWORD=prometheus db: image: postgres:13-alpine diff --git a/price/pricingbyservice/config.go b/price/pricingbyservice/config.go index 1e0cfdc..649140a 100644 --- a/price/pricingbyservice/config.go +++ b/price/pricingbyservice/config.go @@ -87,6 +87,7 @@ func (cpd *ConfigProviderDB) fetchConfig() (Config, error) { type Config struct { BasePrices PriceByTypeUSD `json:"base_prices"` CountryModifiers map[ISO3166CountryCode]Modifier `json:"country_modifiers"` + DemandBoost *DemandBoostConfig `json:"demand_boost,omitempty"` } func (c Config) Validate() error { @@ -107,6 +108,86 @@ func (c Config) Validate() error { } } + if c.DemandBoost != nil { + if err := c.DemandBoost.Validate(); err != nil { + return fmt.Errorf("demand boost invalid: %w", err) + } + } + + return nil +} + +type DemandBoostConfig struct { + Countries map[ISO3166CountryCode]DemandBoostCountryCfg `json:"countries"` +} + +func (d DemandBoostConfig) Validate() error { + for country, cfg := range d.Countries { + if err := country.Validate(); err != nil { + return err + } + if err := cfg.Validate(); err != nil { + return fmt.Errorf("country %v contains invalid demand boost: %w", country, err) + } + } + + return nil +} + +type DemandBoostCountryCfg struct { + TargetDemandIndex float64 `json:"target_demand_index"` + MaxBonus float64 `json:"max_bonus"` + ServiceTypes []ServiceType `json:"service_types,omitempty"` +} + +func (d DemandBoostCountryCfg) Validate() error { + if d.TargetDemandIndex <= 0 { + return errors.New("target demand index should be higher than 0") + } + if d.MaxBonus < 0 { + return errors.New("max bonus should be non negative") + } + for _, serviceType := range d.ServiceTypes { + if err := serviceType.Validate(); err != nil { + return err + } + } + return nil +} + +type ServiceType string + +const ( + ServiceTypeWireguard ServiceType = "wireguard" + ServiceTypeScraping ServiceType = "scraping" + ServiceTypeQUICScraping ServiceType = "quic_scraping" + ServiceTypeDataTransfer ServiceType = "data_transfer" + ServiceTypeDVPN ServiceType = "dvpn" + ServiceTypeMonitoring ServiceType = "monitoring" +) + +var validServiceTypes = map[ServiceType]struct{}{ + ServiceTypeWireguard: {}, + ServiceTypeScraping: {}, + ServiceTypeQUICScraping: {}, + ServiceTypeDataTransfer: {}, + ServiceTypeDVPN: {}, + ServiceTypeMonitoring: {}, +} + +var allServiceTypes = []ServiceType{ + ServiceTypeWireguard, + ServiceTypeScraping, + ServiceTypeQUICScraping, + ServiceTypeDataTransfer, + ServiceTypeDVPN, + ServiceTypeMonitoring, +} + +func (s ServiceType) Validate() error { + if _, ok := validServiceTypes[s]; !ok { + return fmt.Errorf("%v is an invalid service type", s) + } return nil } diff --git a/price/pricingbyservice/config_test.go b/price/pricingbyservice/config_test.go index 846ea54..36b3dde 100644 --- a/price/pricingbyservice/config_test.go +++ b/price/pricingbyservice/config_test.go @@ -183,3 +183,69 @@ func TestConfig_Validate(t *testing.T) { }) } } + +func TestDemandBoostConfig_Validate(t *testing.T) { + tests := []struct { + name string + cfg DemandBoostConfig + wantErr bool + }{ + { + name: "accepts valid config", + cfg: DemandBoostConfig{ + Countries: map[ISO3166CountryCode]DemandBoostCountryCfg{ + "PL": {TargetDemandIndex: 0.1, MaxBonus: 0.5}, + }, + }, + }, + { + name: "rejects invalid country", + cfg: DemandBoostConfig{ + Countries: map[ISO3166CountryCode]DemandBoostCountryCfg{ + "pl": {TargetDemandIndex: 0.1, MaxBonus: 0.5}, + }, + }, + wantErr: true, + }, + { + name: "rejects negative max bonus", + cfg: DemandBoostConfig{ + Countries: map[ISO3166CountryCode]DemandBoostCountryCfg{ + "PL": {TargetDemandIndex: 0.1, MaxBonus: -0.1}, + }, + }, + wantErr: true, + }, + { + name: "rejects unset target demand index", + cfg: DemandBoostConfig{ + Countries: map[ISO3166CountryCode]DemandBoostCountryCfg{ + "PL": {MaxBonus: 0.5}, + }, + }, + wantErr: true, + }, + { + name: "rejects invalid service type", + cfg: DemandBoostConfig{ + Countries: map[ISO3166CountryCode]DemandBoostCountryCfg{ + "PL": { + TargetDemandIndex: 0.1, + MaxBonus: 0.5, + ServiceTypes: []ServiceType{"bad_service"}, + }, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("DemandBoostConfig.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/price/pricingbyservice/price_updater.go b/price/pricingbyservice/price_updater.go index c4d9f39..11e5049 100644 --- a/price/pricingbyservice/price_updater.go +++ b/price/pricingbyservice/price_updater.go @@ -28,6 +28,7 @@ type FiatPriceAPI interface { type PriceUpdater struct { priceAPI FiatPriceAPI + demandIndexes CountryDemandIndexProvider priceLifetime time.Duration mystBound Bound db redis.UniversalClient @@ -43,6 +44,7 @@ type PriceUpdater struct { func NewPricer( cfgProvider ConfigProvider, priceAPI FiatPriceAPI, + demandIndexes CountryDemandIndexProvider, priceLifetime time.Duration, sensibleMystBound Bound, db redis.UniversalClient, @@ -50,6 +52,7 @@ func NewPricer( pricer := &PriceUpdater{ cfgProvider: cfgProvider, priceAPI: priceAPI, + demandIndexes: demandIndexes, priceLifetime: priceLifetime, mystBound: sensibleMystBound, stop: make(chan struct{}), @@ -101,10 +104,22 @@ func (p *PriceUpdater) updatePrices() error { return err } - p.lp = p.generateNewLatestPrice(mystUSD, cfg) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() + countryDemandIndexes, err := p.demandIndexes.DemandIndexes(ctx) + if err != nil { + return err + } + countryMultipliers := DemandBoostMultipliers(cfg, countryDemandIndexes) + countryServiceMultipliers := DemandBoostServiceMultipliers(cfg, countryDemandIndexes) + if countryMultipliers != nil && updateCountryModifiers(&cfg, countryMultipliers) { + if err := p.cfgProvider.Update(cfg); err != nil { + return fmt.Errorf("update country modifiers: %w", err) + } + } + + p.lp = p.generateNewLatestPrice(mystUSD, cfg, countryServiceMultipliers) + marshalled, err := json.Marshal(p.lp) if err != nil { return err @@ -121,6 +136,78 @@ func (p *PriceUpdater) updatePrices() error { return nil } +func DemandBoostMultipliers(cfg Config, demandIndexes map[ISO3166CountryCode]float64) map[ISO3166CountryCode]float64 { + if cfg.DemandBoost == nil { + return nil + } + + multipliers := make(map[ISO3166CountryCode]float64) + for country, boostCfg := range cfg.DemandBoost.Countries { + multipliers[country] = demandBoostMultiplier(boostCfg, demandIndexes[country]) + } + + return multipliers +} + +func DemandBoostServiceMultipliers(cfg Config, demandIndexes map[ISO3166CountryCode]float64) map[ISO3166CountryCode]map[ServiceType]float64 { + if cfg.DemandBoost == nil { + return nil + } + + multipliers := make(map[ISO3166CountryCode]map[ServiceType]float64) + for country, boostCfg := range cfg.DemandBoost.Countries { + multiplier := demandBoostMultiplier(boostCfg, demandIndexes[country]) + serviceTypes := boostCfg.ServiceTypes + if len(serviceTypes) == 0 { + serviceTypes = allServiceTypes + } + + multipliers[country] = make(map[ServiceType]float64, len(serviceTypes)) + for _, serviceType := range serviceTypes { + multipliers[country][serviceType] = multiplier + } + } + + return multipliers +} + +func demandBoostMultiplier(boostCfg DemandBoostCountryCfg, currentDemandIndex float64) float64 { + gapRatio := (boostCfg.TargetDemandIndex - currentDemandIndex) / boostCfg.TargetDemandIndex + if gapRatio < 0 { + gapRatio = 0 + } + if gapRatio > 1 { + gapRatio = 1 + } + return 1 + (gapRatio * boostCfg.MaxBonus) +} + +func updateCountryModifiers(cfg *Config, multipliers map[ISO3166CountryCode]float64) bool { + modifiers := make(map[ISO3166CountryCode]Modifier, len(multipliers)) + for country, multiplier := range multipliers { + modifiers[country] = Modifier{ + Residential: multiplier, + Other: multiplier, + } + } + + if len(cfg.CountryModifiers) == len(modifiers) { + equal := true + for country, modifier := range modifiers { + if cfg.CountryModifiers[country] != modifier { + equal = false + break + } + } + if equal { + return false + } + } + + cfg.CountryModifiers = modifiers + return true +} + func (p *PriceUpdater) submitMetrics() { p.submitPriceMetric("DEFAULTS", p.lp.Defaults.Current) @@ -166,12 +253,12 @@ func (p *PriceUpdater) Stop() { p.once.Do(func() { close(p.stop) }) } -func (p *PriceUpdater) generateNewLatestPrice(mystUSD float64, cfg Config) LatestPrices { +func (p *PriceUpdater) generateNewLatestPrice(mystUSD float64, cfg Config, multipliers map[ISO3166CountryCode]map[ServiceType]float64) LatestPrices { tm := time.Now().UTC() newLP := LatestPrices{ Defaults: p.generateNewDefaults(mystUSD, cfg), - PerCountry: p.generateNewPerCountry(mystUSD, cfg), + PerCountry: p.generateNewPerCountryWithOptionalServiceMultipliers(mystUSD, cfg, multipliers), CurrentValidUntil: tm.Add(p.priceLifetime), } @@ -275,92 +362,130 @@ func (p *PriceUpdater) generateNewDefaults(mystUSD float64, cfg Config) *PriceHi } func (p *PriceUpdater) generateNewPerCountry(mystUSD float64, cfg Config) map[string]*PriceHistory { - countries := make(map[string]*PriceHistory) - for countryCode := range CountryCodeToName { - mod, ok := cfg.CountryModifiers[ISO3166CountryCode(countryCode)] + return p.generateNewPerCountryWithModifier(mystUSD, cfg, func(country ISO3166CountryCode, _ ServiceType) Modifier { + modifier, ok := cfg.CountryModifiers[country] + if !ok { + return Modifier{Residential: 1, Other: 1} + } + return modifier + }) +} + +func (p *PriceUpdater) generateNewPerCountryWithOptionalServiceMultipliers(mystUSD float64, cfg Config, multipliers map[ISO3166CountryCode]map[ServiceType]float64) map[string]*PriceHistory { + if multipliers == nil { + return p.generateNewPerCountry(mystUSD, cfg) + } + return p.generateNewPerCountryWithServiceMultipliers(mystUSD, cfg, multipliers) +} + +func (p *PriceUpdater) generateNewPerCountryWithMultipliers(mystUSD float64, cfg Config, multipliers map[ISO3166CountryCode]float64) map[string]*PriceHistory { + return p.generateNewPerCountryWithModifier(mystUSD, cfg, func(country ISO3166CountryCode, _ ServiceType) Modifier { + multiplier, ok := multipliers[country] if !ok { - mod = Modifier{ - Residential: 1, - Other: 1, + multiplier = 1 + } + return Modifier{Residential: multiplier, Other: multiplier} + }) +} + +func (p *PriceUpdater) generateNewPerCountryWithServiceMultipliers(mystUSD float64, cfg Config, multipliers map[ISO3166CountryCode]map[ServiceType]float64) map[string]*PriceHistory { + return p.generateNewPerCountryWithModifier(mystUSD, cfg, func(country ISO3166CountryCode, serviceType ServiceType) Modifier { + multiplier := float64(1) + if countryMultipliers, ok := multipliers[country]; ok { + if serviceMultiplier, ok := countryMultipliers[serviceType]; ok { + multiplier = serviceMultiplier } } + return Modifier{Residential: multiplier, Other: multiplier} + }) +} + +func (p *PriceUpdater) generateNewPerCountryWithModifier(mystUSD float64, cfg Config, modifierFor func(ISO3166CountryCode, ServiceType) Modifier) map[string]*PriceHistory { + countries := make(map[string]*PriceHistory) + for countryCode := range CountryCodeToName { + wireguardMod := modifierFor(countryCode, ServiceTypeWireguard) + scrapingMod := modifierFor(countryCode, ServiceTypeScraping) + quicScrapingMod := modifierFor(countryCode, ServiceTypeQUICScraping) + dataTransferMod := modifierFor(countryCode, ServiceTypeDataTransfer) + dvpnMod := modifierFor(countryCode, ServiceTypeDVPN) + monitoringMod := modifierFor(countryCode, ServiceTypeMonitoring) ph := &PriceHistory{ Current: &PriceByType{ Residential: &PriceByServiceType{ Wireguard: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Wireguard.PricePerHour, mod.Residential), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Wireguard.PricePerHour, mod.Residential), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Wireguard.PricePerGiB, mod.Residential), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Wireguard.PricePerGiB, mod.Residential), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Wireguard.PricePerHour, wireguardMod.Residential), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Wireguard.PricePerHour, wireguardMod.Residential), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Wireguard.PricePerGiB, wireguardMod.Residential), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Wireguard.PricePerGiB, wireguardMod.Residential), }, Scraping: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Scraping.PricePerHour, mod.Residential), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Scraping.PricePerHour, mod.Residential), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Scraping.PricePerGiB, mod.Residential), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Scraping.PricePerGiB, mod.Residential), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Scraping.PricePerHour, scrapingMod.Residential), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Scraping.PricePerHour, scrapingMod.Residential), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Scraping.PricePerGiB, scrapingMod.Residential), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Scraping.PricePerGiB, scrapingMod.Residential), }, QUICScraping: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.QUICScraping.PricePerHour, mod.Residential), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.QUICScraping.PricePerHour, mod.Residential), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.QUICScraping.PricePerGiB, mod.Residential), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.QUICScraping.PricePerGiB, mod.Residential), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.QUICScraping.PricePerHour, quicScrapingMod.Residential), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.QUICScraping.PricePerHour, quicScrapingMod.Residential), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.QUICScraping.PricePerGiB, quicScrapingMod.Residential), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.QUICScraping.PricePerGiB, quicScrapingMod.Residential), }, DataTransfer: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.DataTransfer.PricePerHour, mod.Residential), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.DataTransfer.PricePerHour, mod.Residential), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.DataTransfer.PricePerGiB, mod.Residential), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.DataTransfer.PricePerGiB, mod.Residential), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.DataTransfer.PricePerHour, dataTransferMod.Residential), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.DataTransfer.PricePerHour, dataTransferMod.Residential), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.DataTransfer.PricePerGiB, dataTransferMod.Residential), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.DataTransfer.PricePerGiB, dataTransferMod.Residential), }, DVPN: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.DVPN.PricePerHour, mod.Residential), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.DVPN.PricePerHour, mod.Residential), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.DVPN.PricePerGiB, mod.Residential), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.DVPN.PricePerGiB, mod.Residential), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.DVPN.PricePerHour, dvpnMod.Residential), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.DVPN.PricePerHour, dvpnMod.Residential), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.DVPN.PricePerGiB, dvpnMod.Residential), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.DVPN.PricePerGiB, dvpnMod.Residential), }, Monitoring: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Monitoring.PricePerHour, mod.Residential), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Monitoring.PricePerHour, mod.Residential), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Monitoring.PricePerGiB, mod.Residential), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Monitoring.PricePerGiB, mod.Residential), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Monitoring.PricePerHour, monitoringMod.Residential), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Monitoring.PricePerHour, monitoringMod.Residential), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Residential.Monitoring.PricePerGiB, monitoringMod.Residential), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Residential.Monitoring.PricePerGiB, monitoringMod.Residential), }, }, Other: &PriceByServiceType{ Wireguard: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Wireguard.PricePerHour, mod.Other), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Wireguard.PricePerHour, mod.Other), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Wireguard.PricePerGiB, mod.Other), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Wireguard.PricePerGiB, mod.Other), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Wireguard.PricePerHour, wireguardMod.Other), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Wireguard.PricePerHour, wireguardMod.Other), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Wireguard.PricePerGiB, wireguardMod.Other), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Wireguard.PricePerGiB, wireguardMod.Other), }, Scraping: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Scraping.PricePerHour, mod.Other), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Scraping.PricePerHour, mod.Other), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Scraping.PricePerGiB, mod.Other), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Scraping.PricePerGiB, mod.Other), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Scraping.PricePerHour, scrapingMod.Other), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Scraping.PricePerHour, scrapingMod.Other), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Scraping.PricePerGiB, scrapingMod.Other), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Scraping.PricePerGiB, scrapingMod.Other), }, QUICScraping: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.QUICScraping.PricePerHour, mod.Other), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.QUICScraping.PricePerHour, mod.Other), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.QUICScraping.PricePerGiB, mod.Other), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.QUICScraping.PricePerGiB, mod.Other), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.QUICScraping.PricePerHour, quicScrapingMod.Other), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.QUICScraping.PricePerHour, quicScrapingMod.Other), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.QUICScraping.PricePerGiB, quicScrapingMod.Other), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.QUICScraping.PricePerGiB, quicScrapingMod.Other), }, DataTransfer: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.DataTransfer.PricePerHour, mod.Other), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.DataTransfer.PricePerHour, mod.Other), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.DataTransfer.PricePerGiB, mod.Other), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.DataTransfer.PricePerGiB, mod.Other), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.DataTransfer.PricePerHour, dataTransferMod.Other), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.DataTransfer.PricePerHour, dataTransferMod.Other), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.DataTransfer.PricePerGiB, dataTransferMod.Other), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.DataTransfer.PricePerGiB, dataTransferMod.Other), }, DVPN: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.DVPN.PricePerHour, mod.Other), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.DVPN.PricePerHour, mod.Other), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.DVPN.PricePerGiB, mod.Other), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.DVPN.PricePerGiB, mod.Other), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.DVPN.PricePerHour, dvpnMod.Other), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.DVPN.PricePerHour, dvpnMod.Other), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.DVPN.PricePerGiB, dvpnMod.Other), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.DVPN.PricePerGiB, dvpnMod.Other), }, Monitoring: Price{ - PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Monitoring.PricePerHour, mod.Other), - PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Monitoring.PricePerHour, mod.Other), - PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Monitoring.PricePerGiB, mod.Other), - PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Monitoring.PricePerGiB, mod.Other), + PricePerHour: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Monitoring.PricePerHour, monitoringMod.Other), + PricePerHourHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Monitoring.PricePerHour, monitoringMod.Other), + PricePerGiB: calculatePriceMYST(mystUSD, cfg.BasePrices.Other.Monitoring.PricePerGiB, monitoringMod.Other), + PricePerGiBHumanReadable: calculatePriceMystFloat(mystUSD, cfg.BasePrices.Other.Monitoring.PricePerGiB, monitoringMod.Other), }, }, }, diff --git a/price/pricingbyservice/price_updater_test.go b/price/pricingbyservice/price_updater_test.go index cfe4f94..12fd9bb 100644 --- a/price/pricingbyservice/price_updater_test.go +++ b/price/pricingbyservice/price_updater_test.go @@ -1,6 +1,7 @@ package pricingbyservice import ( + "math" "math/big" "reflect" "testing" @@ -8,6 +9,146 @@ import ( "github.com/mysteriumnetwork/payments/v3/units" ) +func TestUpdateCountryModifiers(t *testing.T) { + cfg := Config{ + CountryModifiers: map[ISO3166CountryCode]Modifier{ + "US": {Residential: 1, Other: 1}, + }, + } + + changed := updateCountryModifiers(&cfg, map[ISO3166CountryCode]float64{ + "US": 1.5, + "DE": 1, + }) + + if !changed { + t.Fatal("expected country modifiers to change") + } + want := map[ISO3166CountryCode]Modifier{ + "US": {Residential: 1.5, Other: 1.5}, + "DE": {Residential: 1, Other: 1}, + } + if !reflect.DeepEqual(want, cfg.CountryModifiers) { + t.Fatalf("country modifiers = %#v, want %#v", cfg.CountryModifiers, want) + } + + if updateCountryModifiers(&cfg, map[ISO3166CountryCode]float64{"US": 1.5, "DE": 1}) { + t.Fatal("expected unchanged country modifiers not to trigger an update") + } +} + +func TestDemandBoostMultipliers(t *testing.T) { + cfg := Config{ + DemandBoost: &DemandBoostConfig{ + Countries: map[ISO3166CountryCode]DemandBoostCountryCfg{ + "PL": {TargetDemandIndex: 0.1, MaxBonus: 0.5}, + "DE": {TargetDemandIndex: 0.1, MaxBonus: 0.5}, + "US": {TargetDemandIndex: 0.1, MaxBonus: 0.5}, + "GB": {TargetDemandIndex: 0.05, MaxBonus: 1}, + }, + }, + } + + got := DemandBoostMultipliers(cfg, map[ISO3166CountryCode]float64{ + "PL": 0.00972, + "DE": 0.1, + "US": 0.2, + "GB": 0.025, + }) + + want := map[ISO3166CountryCode]float64{"PL": 1.4514, "DE": 1, "US": 1, "GB": 1.5} + for country, wantMultiplier := range want { + if math.Abs(got[country]-wantMultiplier) > 0.0000001 { + t.Fatalf("country %v multiplier = %v, want %v", country, got[country], wantMultiplier) + } + } +} + +func TestDemandBoostMultipliersDefaultsMissingDemandIndexToMaxBoost(t *testing.T) { + cfg := Config{ + DemandBoost: &DemandBoostConfig{ + Countries: map[ISO3166CountryCode]DemandBoostCountryCfg{ + "PL": {TargetDemandIndex: 0.1, MaxBonus: 0.5}, + }, + }, + } + + got := DemandBoostMultipliers(cfg, nil) + + if got["PL"] != 1.5 { + t.Fatalf("missing demand index multiplier = %v, want 1.5", got["PL"]) + } +} + +func TestDemandBoostServiceMultipliers(t *testing.T) { + cfg := Config{ + DemandBoost: &DemandBoostConfig{ + Countries: map[ISO3166CountryCode]DemandBoostCountryCfg{ + "PL": { + TargetDemandIndex: 0.1, + MaxBonus: 0.5, + ServiceTypes: []ServiceType{ServiceTypeDVPN, ServiceTypeWireguard}, + }, + "GB": {TargetDemandIndex: 0.1, MaxBonus: 0.5}, + }, + }, + } + + got := DemandBoostServiceMultipliers(cfg, map[ISO3166CountryCode]float64{ + "PL": 0, + "GB": 0, + }) + + if got["PL"][ServiceTypeDVPN] != 1.5 || got["PL"][ServiceTypeWireguard] != 1.5 { + t.Fatalf("expected PL selected service multipliers to be boosted, got %#v", got["PL"]) + } + if _, ok := got["PL"][ServiceTypeScraping]; ok { + t.Fatalf("expected PL scraping to be omitted, got %#v", got["PL"]) + } + if len(got["GB"]) != len(allServiceTypes) { + t.Fatalf("expected empty GB service list to include all services, got %#v", got["GB"]) + } +} + +func TestGenerateNewPerCountryWithServiceMultipliers(t *testing.T) { + price := PriceUSD{PricePerHour: 1, PricePerGiB: 2} + cfg := Config{ + BasePrices: PriceByTypeUSD{ + Residential: &PriceByServiceTypeUSD{ + Wireguard: price, Scraping: price, QUICScraping: price, + DataTransfer: price, DVPN: price, Monitoring: price, + }, + Other: &PriceByServiceTypeUSD{ + Wireguard: price, Scraping: price, QUICScraping: price, + DataTransfer: price, DVPN: price, Monitoring: price, + }, + }, + } + pricer := &PriceUpdater{} + + prices := pricer.generateNewPerCountryWithServiceMultipliers(1, cfg, map[ISO3166CountryCode]map[ServiceType]float64{ + "PL": { + ServiceTypeDVPN: 1.5, + }, + }) + + if prices["PL"].Current.Residential.DVPN.PricePerHourHumanReadable != 1.5 { + t.Fatalf("expected PL DVPN to be boosted") + } + if prices["PL"].Current.Residential.Wireguard.PricePerHourHumanReadable != 1 { + t.Fatalf("expected PL wireguard to remain unboosted") + } + if prices["PL"].Current.Other.DVPN.PricePerHourHumanReadable != 1.5 { + t.Fatalf("expected PL other DVPN to be boosted") + } +} + +func TestDemandBoostMultipliersDisabledWhenConfigMissing(t *testing.T) { + if got := DemandBoostMultipliers(Config{}, nil); got != nil { + t.Fatalf("demandBoostMultipliers() = %#v, want nil", got) + } +} + func Test_calculatePrice(t *testing.T) { type args struct { mystPriceUSD float64 diff --git a/price/pricingbyservice/prometheus_multiplier.go b/price/pricingbyservice/prometheus_multiplier.go new file mode 100644 index 0000000..231427b --- /dev/null +++ b/price/pricingbyservice/prometheus_multiplier.go @@ -0,0 +1,210 @@ +package pricingbyservice + +import ( + "context" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" +) + +const CountryDemandIndexQuery = `WITH ( + requests = sum_over_time(dvpn_client_connect_requests_with_country{country!=""}[24h:10m]), + VPN_DemandIndex =(0.4*sum by (country)(requests) / sum(requests) + 0.25*count by (country)(sum by (country,uuid)(requests)) / count(sum by (uuid)(requests))), + + Proxy_DemandIndex = (0.5*label_move( + sum by (session_country)(sum_over_time(client_connection_data:sum10m{session_country!=""}[24h:10m])) + / + sum(sum_over_time(client_connection_data:sum10m{}[24h:10m])) + ,"session_country","country") + + + 0.3*sum by (country)(sum_over_time(proxy_total_requests:sum10m{country!=""}[24h:10m])) + / + sum(sum_over_time(proxy_total_requests:sum10m{}[24h:10m])) + + + 0.2*label_move( + count by (session_country)(sum by (session_country,uuid,sub_user_uuid)(sum_over_time(client_connection_data:sum10m{session_country!=""}[24h:10m]))) + / + count(count by (uuid,sub_user_uuid)(sum_over_time(client_connection_data:sum10m{}[24h:10m]))) + ,"session_country","country")), + + Combined_Demand_Index = ((0.3*VPN_DemandIndex or on(country) (0 * Proxy_DemandIndex)) + + + (0.7*Proxy_DemandIndex or on(country) (0 * VPN_DemandIndex))), + + + Avg_provider_scoring = avg_over_time(provider_scoring{country!=""}[1h]), + + SupplyShare_country = ( + ((count by (country)(Avg_provider_scoring >= 85) or 0*count by (country)(Avg_provider_scoring)) + +(0.75*count by (country)(Avg_provider_scoring >= 70 and Avg_provider_scoring < 85) or 0*count by (country)(Avg_provider_scoring)) + +(0.5*count by (country)(Avg_provider_scoring >= 55 and Avg_provider_scoring < 70) or 0*count by (country)(Avg_provider_scoring)) + +(0.2*count by (country)(Avg_provider_scoring >= 40 and Avg_provider_scoring < 55) or 0*count by (country)(Avg_provider_scoring)) + +(0*count by (country)(Avg_provider_scoring < 40) or 0*count by (country)(Avg_provider_scoring))) + / + (count(Avg_provider_scoring >= 85) + +0.75*count(Avg_provider_scoring >= 70 and Avg_provider_scoring < 85) + +0.5*count (Avg_provider_scoring >= 55 and Avg_provider_scoring < 70) + +0.2*count (Avg_provider_scoring >= 40 and Avg_provider_scoring < 55) + +0*count (Avg_provider_scoring < 40))) +) + +VPN_DemandIndex` + +type CountryDemandIndexProvider interface { + DemandIndexes(context.Context) (map[ISO3166CountryCode]float64, error) +} + +type DailyCountryDemandIndexProvider struct { + provider CountryDemandIndexProvider + now func() time.Time + + lock sync.Mutex + cached map[ISO3166CountryCode]float64 + nextRefreshAt time.Time +} + +func NewDailyCountryDemandIndexProvider(provider CountryDemandIndexProvider) *DailyCountryDemandIndexProvider { + return &DailyCountryDemandIndexProvider{ + provider: provider, + now: time.Now, + } +} + +func (p *DailyCountryDemandIndexProvider) DemandIndexes(ctx context.Context) (map[ISO3166CountryCode]float64, error) { + p.lock.Lock() + defer p.lock.Unlock() + + now := p.now() + if p.cached != nil && now.Before(p.nextRefreshAt) { + return cloneFloatMap(p.cached), nil + } + + demandIndexes, err := p.provider.DemandIndexes(ctx) + if err != nil { + return nil, err + } + + p.cached = cloneFloatMap(demandIndexes) + p.nextRefreshAt = nextUTCMidnight(now) + return cloneFloatMap(p.cached), nil +} + +func nextUTCMidnight(now time.Time) time.Time { + utc := now.UTC() + return time.Date(utc.Year(), utc.Month(), utc.Day()+1, 0, 0, 0, 0, time.UTC) +} + +func cloneFloatMap(source map[ISO3166CountryCode]float64) map[ISO3166CountryCode]float64 { + result := make(map[ISO3166CountryCode]float64, len(source)) + for country, multiplier := range source { + result[country] = multiplier + } + return result +} + +type PrometheusDemandIndexProvider struct { + baseURL *url.URL + username string + password string + query string + client *http.Client +} + +func NewPrometheusDemandIndexProvider(baseURL *url.URL, username, password, query string) *PrometheusDemandIndexProvider { + return &PrometheusDemandIndexProvider{ + baseURL: baseURL, + username: username, + password: password, + query: query, + client: http.DefaultClient, + } +} + +func (p *PrometheusDemandIndexProvider) DemandIndexes(ctx context.Context) (map[ISO3166CountryCode]float64, error) { + endpoint := *p.baseURL + endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/api/v1/query" + values := url.Values{} + values.Set("query", p.query) + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + endpoint.String(), + strings.NewReader(values.Encode()), + ) + if err != nil { + return nil, fmt.Errorf("create prometheus request: %w", err) + } + if p.username != "" || p.password != "" { + req.SetBasicAuth(p.username, p.password) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := p.client.Do(req) + if err != nil { + return nil, fmt.Errorf("query prometheus: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, readErr := io.ReadAll(io.LimitReader(resp.Body, 16*1024)) + if readErr != nil { + return nil, fmt.Errorf("query prometheus: unexpected status %s (read response: %v)", resp.Status, readErr) + } + return nil, fmt.Errorf("query prometheus: unexpected status %s: %s", resp.Status, strings.TrimSpace(string(body))) + } + + var response prometheusQueryResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("decode prometheus response: %w", err) + } + if response.Status != "success" { + return nil, fmt.Errorf("query prometheus: status %q", response.Status) + } + if response.Data.ResultType != "vector" { + return nil, fmt.Errorf("query prometheus: expected vector result, got %q", response.Data.ResultType) + } + + demandIndexes := make(map[ISO3166CountryCode]float64, len(response.Data.Result)) + for _, result := range response.Data.Result { + country := ISO3166CountryCode(result.Metric.Country) + if country.Validate() != nil || len(result.Value) != 2 { + continue + } + + rawValue, ok := result.Value[1].(string) + if !ok { + continue + } + value, err := strconv.ParseFloat(rawValue, 64) + if err != nil { + continue + } + if math.IsNaN(value) || math.IsInf(value, 0) { + continue + } + demandIndexes[country] = value + } + + return demandIndexes, nil +} + +type prometheusQueryResponse struct { + Status string `json:"status"` + Data struct { + ResultType string `json:"resultType"` + Result []struct { + Metric struct { + Country string `json:"country"` + } `json:"metric"` + Value []any `json:"value"` + } `json:"result"` + } `json:"data"` +} diff --git a/price/pricingbyservice/prometheus_multiplier_test.go b/price/pricingbyservice/prometheus_multiplier_test.go new file mode 100644 index 0000000..83e3d45 --- /dev/null +++ b/price/pricingbyservice/prometheus_multiplier_test.go @@ -0,0 +1,159 @@ +package pricingbyservice + +import ( + "context" + "io" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestPrometheusDemandIndexProvider(t *testing.T) { + transport := roundTripperFunc(func(r *http.Request) (*http.Response, error) { + username, password, ok := r.BasicAuth() + require.True(t, ok) + require.Equal(t, "prom-user", username) + require.Equal(t, "prom-password", password) + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "/select/0/prometheus/api/v1/query", r.URL.Path) + require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) + require.NoError(t, r.ParseForm()) + require.Equal(t, CountryDemandIndexQuery, r.Form.Get("query")) + + return &http.Response{ + StatusCode: http.StatusOK, + Status: "200 OK", + Body: io.NopCloser(strings.NewReader(`{ + "status": "success", + "data": { + "resultType": "vector", + "result": [ + {"metric": {"country": "US"}, "value": [123, "0.5"]}, + {"metric": {"country": "DE"}, "value": [123, "0.7"]}, + {"metric": {"country": "FR"}, "value": [123, "1"]}, + {"metric": {"country": "GB"}, "value": [123, "1.3"]}, + {"metric": {"country": "CA"}, "value": [123, "2"]}, + {"metric": {"country": "invalid"}, "value": [123, "0.5"]} + ] + } + }`)), + Header: make(http.Header), + }, nil + }) + + prometheusURL, err := url.Parse("https://prometheus.example/select/0/prometheus") + require.NoError(t, err) + provider := NewPrometheusDemandIndexProvider(prometheusURL, "prom-user", "prom-password", CountryDemandIndexQuery) + provider.client = &http.Client{Transport: transport} + + got, err := provider.DemandIndexes(context.Background()) + require.NoError(t, err) + require.Equal(t, map[ISO3166CountryCode]float64{ + "US": 0.5, + "DE": 0.7, + "FR": 1, + "GB": 1.3, + "CA": 2, + }, got) +} + +func TestPrometheusDemandIndexProviderWithoutAuthentication(t *testing.T) { + transport := roundTripperFunc(func(r *http.Request) (*http.Response, error) { + _, _, ok := r.BasicAuth() + require.False(t, ok) + return &http.Response{ + StatusCode: http.StatusOK, + Status: "200 OK", + Body: io.NopCloser(strings.NewReader(`{ + "status": "success", + "data": {"resultType": "vector", "result": []} + }`)), + Header: make(http.Header), + }, nil + }) + + prometheusURL, err := url.Parse("https://prometheus.example") + require.NoError(t, err) + provider := NewPrometheusDemandIndexProvider(prometheusURL, "", "", CountryDemandIndexQuery) + provider.client = &http.Client{Transport: transport} + + _, err = provider.DemandIndexes(context.Background()) + require.NoError(t, err) +} + +type roundTripperFunc func(*http.Request) (*http.Response, error) + +func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return f(r) +} + +func TestDailyCountryDemandIndexProvider(t *testing.T) { + source := &stubCountryDemandIndexProvider{ + demandIndexes: map[ISO3166CountryCode]float64{"US": 0.05}, + } + now := time.Date(2026, time.June, 18, 10, 0, 0, 0, time.FixedZone("UTC+6", 6*60*60)) + provider := NewDailyCountryDemandIndexProvider(source) + provider.now = func() time.Time { return now } + + first, err := provider.DemandIndexes(context.Background()) + require.NoError(t, err) + first["US"] = 0.1 + + second, err := provider.DemandIndexes(context.Background()) + require.NoError(t, err) + + require.Equal(t, 1, source.calls) + require.Equal(t, 0.05, second["US"]) + + now = time.Date(2026, time.June, 19, 0, 0, 0, 0, time.UTC) + _, err = provider.DemandIndexes(context.Background()) + require.NoError(t, err) + require.Equal(t, 2, source.calls) +} + +func TestNextUTCMidnight(t *testing.T) { + now := time.Date(2026, time.June, 18, 23, 30, 0, 0, time.FixedZone("UTC+6", 6*60*60)) + require.Equal( + t, + time.Date(2026, time.June, 19, 0, 0, 0, 0, time.UTC), + nextUTCMidnight(now), + ) +} + +type stubCountryDemandIndexProvider struct { + demandIndexes map[ISO3166CountryCode]float64 + calls int +} + +func (p *stubCountryDemandIndexProvider) DemandIndexes(context.Context) (map[ISO3166CountryCode]float64, error) { + p.calls++ + return p.demandIndexes, nil +} + +func TestGenerateNewPerCountryDefaultsMissingMultiplierToBalanced(t *testing.T) { + price := PriceUSD{PricePerHour: 1, PricePerGiB: 2} + cfg := Config{ + BasePrices: PriceByTypeUSD{ + Residential: &PriceByServiceTypeUSD{ + Wireguard: price, Scraping: price, QUICScraping: price, + DataTransfer: price, DVPN: price, Monitoring: price, + }, + Other: &PriceByServiceTypeUSD{ + Wireguard: price, Scraping: price, QUICScraping: price, + DataTransfer: price, DVPN: price, Monitoring: price, + }, + }, + } + + pricer := &PriceUpdater{} + prices := pricer.generateNewPerCountryWithMultipliers(1, cfg, map[ISO3166CountryCode]float64{"US": 1.5}) + + require.Equal(t, 1.5, prices["US"].Current.Residential.Wireguard.PricePerHourHumanReadable) + require.Equal(t, 1.5, prices["US"].Current.Other.Wireguard.PricePerHourHumanReadable) + require.Equal(t, float64(1), prices["DE"].Current.Residential.Wireguard.PricePerHourHumanReadable) + require.Equal(t, float64(1), prices["DE"].Current.Other.Wireguard.PricePerHourHumanReadable) +} diff --git a/sidecar/cmd/main.go b/sidecar/cmd/main.go index 1638386..0a9c96e 100644 --- a/sidecar/cmd/main.go +++ b/sidecar/cmd/main.go @@ -35,6 +35,14 @@ func main() { log.Fatal().Err(err).Msg("Failed to read config") } + prometheusDemandIndexes := pricingbyservice.NewPrometheusDemandIndexProvider( + &cfg.PrometheusURL, + cfg.PrometheusUsername, + cfg.PrometheusPassword, + pricingbyservice.CountryDemandIndexQuery, + ) + countryDemandIndexes := pricingbyservice.NewDailyCountryDemandIndexProvider(prometheusDemandIndexes) + rdb := redis.NewUniversalClient(&redis.UniversalOptions{ Addrs: cfg.RedisAddress, Password: cfg.RedisPass, @@ -64,10 +72,10 @@ func main() { log.Info().Msg("cfger started") metrics.InitialiseMonitoring() - pricer, err := pricingbyservice.NewPricer( cfger, mrkt, + countryDemandIndexes, time.Minute*5, pricingbyservice.Bound{Min: 0.1, Max: 3.0}, rdb, @@ -79,27 +87,6 @@ func main() { log.Info().Msg("pricer started") defer pricer.Stop() - cfgerByService := pricingbyservice.NewConfigProviderDB(rdb) - _, err = cfger.Get() - if err != nil { - log.Fatal().Err(err).Msg("Failed to load cfg") - } - log.Info().Msg("cfger by service started") - - pricerByService, err := pricingbyservice.NewPricer( - cfgerByService, - mrkt, - time.Minute*5, - pricingbyservice.Bound{Min: 0.1, Max: 3.0}, - rdb, - ) - if err != nil { - log.Fatal().Err(err).Msg("Failed to initialize Pricer by service") - return - } - log.Info().Msg("pricer by service started") - defer pricerByService.Stop() - router := gin.New() router.Use(gin.Recovery()) router.GET("/metrics", gin.WrapH(promhttp.Handler())) @@ -162,17 +149,27 @@ func configureLogger() { } type Options struct { - RedisAddress []string - RedisPass string - RedisDB int - QualityOracleURL url.URL - GeckoURL url.URL - CoinRankingURL url.URL - TokenRateCacheTTL time.Duration - CoinRankingToken string + RedisAddress []string + RedisPass string + RedisDB int + QualityOracleURL url.URL + GeckoURL url.URL + CoinRankingURL url.URL + TokenRateCacheTTL time.Duration + CoinRankingToken string + PrometheusURL url.URL + PrometheusUsername string + PrometheusPassword string } func ReadConfig() (*Options, error) { + prometheusURL, err := config.RequiredEnvURL("PROMETHEUS_URL") + if err != nil { + return nil, err + } + prometheusUsername := config.OptionalEnv("PROMETHEUS_USERNAME", "") + prometheusPassword := config.OptionalEnv("PROMETHEUS_PASSWORD", "") + redisAddress, err := config.RequiredEnv("REDIS_ADDRESS") if err != nil { return nil, err @@ -211,13 +208,16 @@ func ReadConfig() (*Options, error) { return nil, err } return &Options{ - RedisAddress: strings.Split(redisAddress, ";"), - RedisPass: redisPass, - RedisDB: redisDBint, - QualityOracleURL: *qualityOracleURL, - GeckoURL: *geckoURL, - CoinRankingURL: *coinRankingURL, - TokenRateCacheTTL: *tokenRateCacheTTL, - CoinRankingToken: coinRankingToken, + RedisAddress: strings.Split(redisAddress, ";"), + RedisPass: redisPass, + RedisDB: redisDBint, + QualityOracleURL: *qualityOracleURL, + GeckoURL: *geckoURL, + CoinRankingURL: *coinRankingURL, + TokenRateCacheTTL: *tokenRateCacheTTL, + CoinRankingToken: coinRankingToken, + PrometheusURL: *prometheusURL, + PrometheusUsername: prometheusUsername, + PrometheusPassword: prometheusPassword, }, nil }