Add support for primary IPs

This commit is contained in:
Jan Graefen 2022-12-14 15:29:15 +01:00
parent 5d61524891
commit 05dc439f7f
3 changed files with 225 additions and 168 deletions

View file

@ -1,96 +1,115 @@
package fetcher
import (
"context"
"strconv"
"sync"
"github.com/hetznercloud/hcloud-go/hcloud"
)
// PriceProvider provides easy access to current HCloud prices.
type PriceProvider struct {
Client *hcloud.Client
pricing *hcloud.Pricing
pricingLock sync.RWMutex
}
// FloatingIP returns the current price for a floating IP per month.
func (provider *PriceProvider) FloatingIP(ipType hcloud.FloatingIPType, location string) float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
for _, byType := range provider.getPricing().FloatingIPs {
if byType.Type == ipType {
for _, pricing := range byType.Pricings {
if pricing.Location.Name == location {
return parsePrice(pricing.Monthly.Gross)
}
}
}
}
// If the pricing can not be determined by the type and location, we fall back to the old pricing
return parsePrice(provider.getPricing().FloatingIP.Monthly.Gross)
}
// Image returns the current price for an image per GB per month.
func (provider *PriceProvider) Image() float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
return parsePrice(provider.getPricing().Image.PerGBMonth.Gross)
}
// Traffic returns the current price for a TB of extra traffic per month.
func (provider *PriceProvider) Traffic() float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
return parsePrice(provider.getPricing().Traffic.PerTB.Gross)
}
// ServerBackup returns the percentage of base price increase for server backups per month.
func (provider *PriceProvider) ServerBackup() float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
return parsePrice(provider.getPricing().ServerBackup.Percentage)
}
// Volume returns the current price for a volume per GB per month.
func (provider *PriceProvider) Volume() float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
return parsePrice(provider.getPricing().Volume.PerGBMonthly.Gross)
}
// Sync forces the provider to re-fetch prices from the HCloud API.
func (provider *PriceProvider) Sync() {
provider.pricingLock.Lock()
defer provider.pricingLock.Unlock()
provider.pricing = nil
}
func (provider *PriceProvider) getPricing() *hcloud.Pricing {
if provider.pricing == nil {
pricing, _, err := provider.Client.Pricing.Get(context.Background())
if err != nil {
panic(err)
}
provider.pricing = &pricing
}
return provider.pricing
}
func parsePrice(rawPrice string) float64 {
if price, err := strconv.ParseFloat(rawPrice, 32); err == nil {
return price
}
return 0
}
package fetcher
import (
"context"
"fmt"
"strconv"
"sync"
"github.com/hetznercloud/hcloud-go/hcloud"
)
// PriceProvider provides easy access to current HCloud prices.
type PriceProvider struct {
Client *hcloud.Client
pricing *hcloud.Pricing
pricingLock sync.RWMutex
}
// FloatingIP returns the current price for a floating IP per month.
func (provider *PriceProvider) FloatingIP(ipType hcloud.FloatingIPType, location string) float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
for _, byType := range provider.getPricing().FloatingIPs {
if byType.Type == ipType {
for _, pricing := range byType.Pricings {
if pricing.Location.Name == location {
return parsePrice(pricing.Monthly.Gross)
}
}
}
}
// If the pricing can not be determined by the type and location, we fall back to the old pricing
return parsePrice(provider.getPricing().FloatingIP.Monthly.Gross)
}
// PrimaryIP returns the current price for a primary IP per hour and month.
func (provider *PriceProvider) PrimaryIP(ipType hcloud.PrimaryIPType, datacenter string) (hourly, monthly float64, err error) {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
for _, byType := range provider.getPricing().PrimaryIPs {
if byType.Type == string(ipType) {
for _, pricing := range byType.Pricings {
if pricing.Datacenter == datacenter {
return parsePrice(pricing.Hourly.Gross), parsePrice(pricing.Monthly.Gross), nil
}
}
}
}
return 0, 0, fmt.Errorf("no primary IP pricing found for datacenter %s", datacenter)
}
// Image returns the current price for an image per GB per month.
func (provider *PriceProvider) Image() float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
return parsePrice(provider.getPricing().Image.PerGBMonth.Gross)
}
// Traffic returns the current price for a TB of extra traffic per month.
func (provider *PriceProvider) Traffic() float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
return parsePrice(provider.getPricing().Traffic.PerTB.Gross)
}
// ServerBackup returns the percentage of base price increase for server backups per month.
func (provider *PriceProvider) ServerBackup() float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
return parsePrice(provider.getPricing().ServerBackup.Percentage)
}
// Volume returns the current price for a volume per GB per month.
func (provider *PriceProvider) Volume() float64 {
provider.pricingLock.RLock()
defer provider.pricingLock.RUnlock()
return parsePrice(provider.getPricing().Volume.PerGBMonthly.Gross)
}
// Sync forces the provider to re-fetch prices from the HCloud API.
func (provider *PriceProvider) Sync() {
provider.pricingLock.Lock()
defer provider.pricingLock.Unlock()
provider.pricing = nil
}
func (provider *PriceProvider) getPricing() *hcloud.Pricing {
if provider.pricing == nil {
pricing, _, err := provider.Client.Pricing.Get(context.Background())
if err != nil {
panic(err)
}
provider.pricing = &pricing
}
return provider.pricing
}
func parsePrice(rawPrice string) float64 {
if price, err := strconv.ParseFloat(rawPrice, 32); err == nil {
return price
}
return 0
}

37
fetcher/primaryip.go Normal file
View file

@ -0,0 +1,37 @@
package fetcher
import (
"github.com/hetznercloud/hcloud-go/hcloud"
)
var _ Fetcher = &floatingIP{}
// NewPrimaryIP creates a new fetcher that will collect pricing information on primary IPs.
func NewPrimaryIP(pricing *PriceProvider) Fetcher {
return &floatingIP{newBase(pricing, "floatingip", "location")}
}
type primaryIP struct {
*baseFetcher
}
func (primaryIP primaryIP) Run(client *hcloud.Client) error {
primaryIPs, _, err := client.PrimaryIP.List(ctx, hcloud.PrimaryIPListOpts{})
if err != nil {
return err
}
for _, p := range primaryIPs {
datacenter := p.Datacenter
hourlyPrice, monthlyPrice, err := primaryIP.pricing.PrimaryIP(p.Type, datacenter.Name)
if err != nil {
return err
}
primaryIP.hourly.WithLabelValues(p.Name, datacenter.Name).Set(hourlyPrice)
primaryIP.monthly.WithLabelValues(p.Name, datacenter.Name).Set(monthlyPrice)
}
return nil
}

145
main.go
View file

@ -1,72 +1,73 @@
package main
import (
"flag"
"fmt"
"net/http"
"os"
"time"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/jangraefen/hcloud-pricing-exporter/fetcher"
"github.com/jtaczanowski/go-scheduler"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
defaultPort = 8080
defaultFetchInterval = 1 * time.Minute
)
var (
hcloudAPIToken string
port uint
fetchInterval time.Duration
)
func handleFlags() {
flag.StringVar(&hcloudAPIToken, "hcloud-token", "", "the token to authenticate against the HCloud API")
flag.UintVar(&port, "port", defaultPort, "the port that the exporter exposes its data on")
flag.DurationVar(&fetchInterval, "fetch-interval", defaultFetchInterval, "the interval between data fetching cycles")
flag.Parse()
if hcloudAPIToken == "" {
if envHCloudAPIToken, present := os.LookupEnv("HCLOUD_TOKEN"); present {
hcloudAPIToken = envHCloudAPIToken
}
}
if hcloudAPIToken == "" {
panic(fmt.Errorf("no API token for HCloud specified, but required"))
}
}
func main() {
handleFlags()
client := hcloud.NewClient(hcloud.WithToken(hcloudAPIToken))
priceRepository := &fetcher.PriceProvider{Client: client}
fetchers := fetcher.Fetchers{
fetcher.NewFloatingIP(priceRepository),
fetcher.NewLoadbalancer(priceRepository),
fetcher.NewLoadbalancerTraffic(priceRepository),
fetcher.NewServer(priceRepository),
fetcher.NewServerBackup(priceRepository),
fetcher.NewServerTraffic(priceRepository),
fetcher.NewSnapshot(priceRepository),
fetcher.NewVolume(priceRepository),
}
fetchers.MustRun(client)
scheduler.RunTaskAtInterval(func() { fetchers.MustRun(client) }, fetchInterval, 0)
scheduler.RunTaskAtInterval(priceRepository.Sync, 10*fetchInterval, 10*fetchInterval)
registry := prometheus.NewRegistry()
fetchers.RegisterCollectors(registry)
http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil); err != nil {
panic(err)
}
}
package main
import (
"flag"
"fmt"
"net/http"
"os"
"time"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/jangraefen/hcloud-pricing-exporter/fetcher"
"github.com/jtaczanowski/go-scheduler"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
defaultPort = 8080
defaultFetchInterval = 1 * time.Minute
)
var (
hcloudAPIToken string
port uint
fetchInterval time.Duration
)
func handleFlags() {
flag.StringVar(&hcloudAPIToken, "hcloud-token", "", "the token to authenticate against the HCloud API")
flag.UintVar(&port, "port", defaultPort, "the port that the exporter exposes its data on")
flag.DurationVar(&fetchInterval, "fetch-interval", defaultFetchInterval, "the interval between data fetching cycles")
flag.Parse()
if hcloudAPIToken == "" {
if envHCloudAPIToken, present := os.LookupEnv("HCLOUD_TOKEN"); present {
hcloudAPIToken = envHCloudAPIToken
}
}
if hcloudAPIToken == "" {
panic(fmt.Errorf("no API token for HCloud specified, but required"))
}
}
func main() {
handleFlags()
client := hcloud.NewClient(hcloud.WithToken(hcloudAPIToken))
priceRepository := &fetcher.PriceProvider{Client: client}
fetchers := fetcher.Fetchers{
fetcher.NewFloatingIP(priceRepository),
fetcher.NewPrimaryIP(priceRepository),
fetcher.NewLoadbalancer(priceRepository),
fetcher.NewLoadbalancerTraffic(priceRepository),
fetcher.NewServer(priceRepository),
fetcher.NewServerBackup(priceRepository),
fetcher.NewServerTraffic(priceRepository),
fetcher.NewSnapshot(priceRepository),
fetcher.NewVolume(priceRepository),
}
fetchers.MustRun(client)
scheduler.RunTaskAtInterval(func() { fetchers.MustRun(client) }, fetchInterval, 0)
scheduler.RunTaskAtInterval(priceRepository.Sync, 10*fetchInterval, 10*fetchInterval)
registry := prometheus.NewRegistry()
fetchers.RegisterCollectors(registry)
http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil); err != nil {
panic(err)
}
}