195 lines
4.3 KiB
Go
195 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
const (
|
|
envPostcode = "CARBON_POSTCODE"
|
|
)
|
|
|
|
type apiResponse struct {
|
|
Data []headerData `json:"data"`
|
|
}
|
|
|
|
type headerData struct {
|
|
RegionID int `json:"regionid"`
|
|
DNORegion string `json:"dnoregion"`
|
|
Shortname string `json:"shortname"`
|
|
Postcode string `json:"postcode"`
|
|
Data []carbonData `json:"data"`
|
|
}
|
|
|
|
type carbonData struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Intensity intensity `json:"intensity"`
|
|
GenerationMix []generationMix `json:"generationmix"`
|
|
}
|
|
|
|
type intensity struct {
|
|
Forecast float64 `json:"forecast"`
|
|
Index string `json:"index"`
|
|
}
|
|
|
|
type generationMix struct {
|
|
Fuel string `json:"fuel"`
|
|
Perc float64 `json:"perc"`
|
|
}
|
|
|
|
type metrics struct {
|
|
genPerc *prometheus.GaugeVec
|
|
intensityFC *prometheus.GaugeVec
|
|
}
|
|
|
|
func (m *metrics) reset() {
|
|
m.genPerc.Reset()
|
|
m.intensityFC.Reset()
|
|
}
|
|
|
|
func collect(ctx context.Context, postcode string, metrics *metrics) error {
|
|
data, err := getCarbonData(ctx, postcode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(data.Data) == 0 {
|
|
return fmt.Errorf("no data returned from API")
|
|
}
|
|
latest := data.Data[0]
|
|
|
|
if len(latest.Data) == 0 {
|
|
return fmt.Errorf("no data for provided postcode")
|
|
}
|
|
|
|
for _, datum := range latest.Data[0].GenerationMix {
|
|
labels := prometheus.Labels{"fuel": datum.Fuel, "region": latest.DNORegion}
|
|
metrics.genPerc.With(labels).Set(datum.Perc)
|
|
}
|
|
metrics.intensityFC.With(prometheus.Labels{"region": latest.DNORegion}).Set(latest.Data[0].Intensity.Forecast)
|
|
|
|
return nil
|
|
}
|
|
|
|
func getCarbonData(ctx context.Context, postcode string) (apiResponse, error) {
|
|
carbonURL := fmt.Sprintf("https://api.carbonintensity.org.uk/regional/postcode/%s", postcode)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", carbonURL, nil)
|
|
if err != nil {
|
|
return apiResponse{}, err
|
|
}
|
|
|
|
response, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return apiResponse{}, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode != 200 {
|
|
return apiResponse{}, fmt.Errorf("carbon API response code was %d", response.StatusCode)
|
|
}
|
|
|
|
var data apiResponse
|
|
err = json.NewDecoder(response.Body).Decode(&data)
|
|
if err != nil {
|
|
return apiResponse{}, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func NewMetrics(reg prometheus.Registerer) *metrics {
|
|
metrics := &metrics{
|
|
genPerc: prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: "sensor_carbon_generation_perc",
|
|
Help: "Generation percent % (float)",
|
|
},
|
|
[]string{"fuel", "region"},
|
|
),
|
|
intensityFC: prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: "sensor_carbon_intensity_forecast",
|
|
Help: "Intensity forecast grams (int)",
|
|
},
|
|
[]string{"region"},
|
|
),
|
|
}
|
|
reg.MustRegister(
|
|
metrics.genPerc,
|
|
metrics.intensityFC,
|
|
)
|
|
|
|
return metrics
|
|
}
|
|
|
|
func main() {
|
|
postcode := os.Getenv(envPostcode)
|
|
|
|
if postcode == "" {
|
|
slog.Error("missing required environment variables", "required", []string{envPostcode})
|
|
os.Exit(1)
|
|
}
|
|
|
|
var healthCheck = flag.Bool("health-check", false, "Perform health check and exit")
|
|
|
|
flag.Parse()
|
|
|
|
if *healthCheck {
|
|
resp, err := http.Get("http://127.0.0.1:8080/health")
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprint(w, "OK")
|
|
})
|
|
|
|
registry := prometheus.NewRegistry()
|
|
m := NewMetrics(registry)
|
|
|
|
promHandler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
|
|
|
|
http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
|
slog.Info("Metrics queried")
|
|
m.reset()
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
err := collect(ctx, postcode, m)
|
|
if err != nil {
|
|
slog.Warn("error collecting metrics", "error", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
io.WriteString(w, "error collecting metrics")
|
|
return
|
|
}
|
|
|
|
promHandler.ServeHTTP(w, r)
|
|
})
|
|
|
|
slog.Info("Listening", "port", 8080)
|
|
|
|
err := http.ListenAndServe(":8080", nil)
|
|
if err != nil {
|
|
slog.Error("unable to start web server", "error", err)
|
|
}
|
|
}
|