Files
badger/main.go
T
2020-08-04 15:19:12 +10:00

231 lines
7.0 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"image/color"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
svg "github.com/ajstarks/svgo"
"github.com/fogleman/gg"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/api"
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/model"
logrus "github.com/sirupsen/logrus"
)
const (
// backgroundImageFilename = "images/bg.png"
radius = 0
fontSize = 12
textMarginX = 5.0
textMarginY = -2.0
badgeWidth = 180
badgeHeight = 20
textStyle = "text-anchor:right;font-size:12px;fill:white;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;"
badgeExpireSeconds = 120
)
var (
ErrNoSuchBadgeFound = errors.New("no such badge can be found, please make sure you have provided the correct information")
queriesMap = map[string]queryParam{
"mel": {Color: color.RGBA{49, 197, 83, 255}, Query: "air_index_iaqi_pm25_v{city_name=\"Melbourne CBD\"}", ValueField: "city_name"},
"chengdu": {Color: color.RGBA{49, 197, 83, 255}, Query: "air_index_iaqi_pm25_v{city_name=\"Chengdu (成都)\"}", ValueField: "city_name"},
"dockerhub": {Color: color.RGBA{60, 174, 163, 255}, Query: "docker_hub_pull_count{name=\"aria2-ui\"}", ValueField: "name"},
"github": {Color: color.RGBA{32, 99, 155, 255}, Query: "github_repository_stars{name=\"aria2-ariang-docker\"}", ValueField: "name"},
"badge-requests-count": {Color: color.RGBA{32, 99, 155, 255}, Query: "ceil(sum(increase(badge_requested_total[500d])))", Label: "Badge Req Count"},
"junv-github-profile": {Color: color.RGBA{32, 99, 155, 255}, Query: "ceil(sum(increase(badge_requested_total{badge=\"junv-github-profile\"}[500d])))", Label: "Page Views Count"},
}
badgeRequested = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "badge_requested_total",
Help: "The total request number of badges",
}, []string{"badge"})
badgeGenerationTime = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "badge_generation_time",
Help: "The time consumed for badge generation",
})
)
type Metric struct {
Label string
Value float64
Time time.Time
}
type queryParam struct {
Color color.Color
Query string
ValueField string // nullable
Label string // when ValueField is null, then the program will use this field as the label
}
func main() {
client, err := api.NewClient(api.Config{
Address: "https://prometheus-api.home.toozhao.com",
})
if err != nil {
fmt.Printf("Error creating client: %v\n", err)
os.Exit(1)
}
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "Ping Ping!!"})
})
r.GET("/hi", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "Hello Afterpay!!"})
})
r.GET("/svg/:name", func(c *gin.Context) {
if err := generateSVG(client, c); err != nil {
logrus.Error(err.Error())
if err == ErrNoSuchBadgeFound {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "please try it again"})
}
})
r.GET("/png/:name", func(c *gin.Context) {
if err := generateBadge(client, c); err != nil {
logrus.Error(err.Error())
}
})
go func() {
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":9090", nil)
}()
log.Fatal(r.Run())
}
func generateSVG(client api.Client, c *gin.Context) error {
startTime := time.Now()
queryName := c.Param("name")
query, ok := queriesMap[queryName]
if !ok {
return ErrNoSuchBadgeFound
}
metric := queryMetric(client, query)
w := c.Writer
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, s-maxage=%d", badgeExpireSeconds, badgeExpireSeconds))
canvas := svg.New(w)
canvas.Start(badgeWidth, badgeHeight)
canvas.Rect(0, 0, badgeWidth, badgeHeight, "fill-opacity:1.00; fill:rgb(90,90,90); padding: 2px 5px;")
//labelContainerWidth := len(metric.Label)*fontSize + textMarginX
labelContainerWidth := badgeWidth * 0.6
// if labelContainerWidth < badgeWidth*0.5 {
// labelContainerWidth = badgeWidth * 0.5
// }
canvas.Rect(0, 0, int(labelContainerWidth), badgeHeight, "fill-opacity:1.00; fill:rgb(49, 197, 83); padding: 2px 5px; float: right")
label := metric.Label
//when there is metric label then use default label
if len(query.ValueField) == 0 {
label = query.Label
}
canvas.Text(textMarginX, 15, label, textStyle)
valueText := strconv.Itoa(int(metric.Value))
canvas.Text(badgeWidth*0.7, 15, valueText, textStyle)
canvas.End()
badgeGenerationTime.Observe(float64(time.Now().Sub(startTime).Microseconds()))
badgeRequested.With(map[string]string{"badge": queryName}).Inc()
return nil
}
func generateBadge(client api.Client, c *gin.Context) error {
queryName := c.Param("name")
query, ok := queriesMap[queryName]
if !ok {
query = queriesMap["mel"]
}
metric := queryMetric(client, query)
dc := gg.NewContext(badgeWidth, badgeHeight)
fontPath := filepath.Join("fonts", "SourceCodePro-Regular.ttf")
if err := dc.LoadFontFace(fontPath, fontSize); err != nil {
return fmt.Errorf("load font %w", err)
}
// black layer
dc.SetColor(color.RGBA{90, 90, 90, 255})
dc.DrawRoundedRectangle(0, 0, float64(dc.Width()), float64(dc.Height()), radius)
dc.Fill()
labelWidth, textHeight := dc.MeasureString(metric.Label)
// custom color layer
dc.SetColor(query.Color)
labelContainerWidth := labelWidth + textMarginX*2
if labelContainerWidth < badgeWidth*0.5 {
labelContainerWidth = badgeWidth * 0.5
}
dc.DrawRectangle(0, 0, labelContainerWidth, float64(dc.Height()))
dc.Fill()
dc.SetColor(color.RGBA{255, 255, 255, 255})
// label
y := float64(dc.Height()) - textHeight - textMarginY
dc.DrawString(metric.Label, textMarginX, y)
// value
valueText := strconv.Itoa(int(metric.Value))
valueWidth, valueHeight := dc.MeasureString(valueText)
dc.DrawString(valueText, float64(dc.Width())-valueWidth-textMarginX, float64(dc.Height())-valueHeight-textMarginY)
w := c.Writer
c.Status(http.StatusOK)
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, s-maxage=%d", badgeExpireSeconds, badgeExpireSeconds))
return dc.EncodePNG(w)
}
func queryMetric(client api.Client, query queryParam) *Metric {
v1api := v1.NewAPI(client)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, warnings, err := v1api.Query(ctx, query.Query, time.Now())
if err != nil {
fmt.Printf("Error querying Prometheus: %v\n", err)
os.Exit(1)
}
if len(warnings) > 0 {
fmt.Printf("Warnings: %v\n", warnings)
}
var metric Metric
for _, v := range result.(model.Vector) {
metric = Metric{
Label: string(v.Metric[model.LabelName(query.ValueField)]),
Value: float64(v.Value),
Time: v.Timestamp.Time(),
}
}
return &metric
}