Skip to content

Commit

Permalink
Merge pull request #4 from henrywhitaker3/feat/metric-graphs
Browse files Browse the repository at this point in the history
  • Loading branch information
henrywhitaker3 authored Apr 20, 2024
2 parents beccef7 + 1bba059 commit ccb2362
Show file tree
Hide file tree
Showing 22 changed files with 500 additions and 100 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ services:
range: 24h
# Optional. The resoltuion of the range query (default: 5m)
step: 5m
# Optional. Whether to generate graphs based on raw values or boolean values (default: false)
bool: false
# Optional. The units to display on any graphs (default: null)
units: ""
# Settings to configure the UI
ui:
# Optional. The title of the page (default: Status Page)
title: Status Page
# Optional. The interval that the page reloads on (default: 30s)
refresh: 30s
# Optional. Configure graph settings
graphs:
# The maximum number of data points to display on the graph (default: 200)
points: 200
```
## Installation
Expand Down
5 changes: 5 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ configuration:
# How often the page reloads
refresh: 30s

# Settings for the graphs
graphs:
# The maximum number of data points to display on the graph
points: 200

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.22.2

require (
github.com/expr-lang/expr v1.16.5
github.com/go-echarts/go-echarts/v2 v2.3.3
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/labstack/echo-contrib v0.17.1
github.com/labstack/echo/v4 v4.12.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/go-echarts/go-echarts/v2 v2.3.3 h1:uImZAk6qLkC6F9ju6mZ5SPBqTyK8xjZKwSmwnCg4bxg=
github.com/go-echarts/go-echarts/v2 v2.3.3/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
Expand Down
31 changes: 29 additions & 2 deletions internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@ package collector
import (
"context"
"log"
"time"

"github.com/henrywhitaker3/prompage/internal/config"
"github.com/henrywhitaker3/prompage/internal/metrics"
"github.com/henrywhitaker3/prompage/internal/querier"
)

type SeriesItem struct {
Time time.Time
Value float64
}

type Series struct {
Query config.Query
Data []SeriesItem
}

type Result struct {
// The service the result corresponds to
Service config.Service
Expand All @@ -18,6 +29,8 @@ type Result struct {
Status bool
// The percentage uptime for the specified duration
Uptime float32
// The series of values for the range query
Series Series
}

type Collector struct {
Expand Down Expand Up @@ -76,14 +89,28 @@ func (c *Collector) collectService(ctx context.Context, svc config.Service, ch c
log.Printf("ERROR - Failed to scrape status metric for %s query %s: %s", svc.Name, svc.Query.Name, err)
res.Success = false
}
uptime, err := c.q.Uptime(ctx, svc.Query)
uptime, series, err := c.q.Uptime(ctx, svc.Query)
if err != nil {
log.Printf("ERROR - Failed to scrape uptime metric for %s query %s: %s", svc.Name, svc.Query.Name, err)
res.Success = false
}

res.Success = true
res.Series = c.mapQuerierSeries(svc.Query, series)
res.Status = status
res.Uptime = uptime
ch <- res
}

func (c *Collector) mapQuerierSeries(q config.Query, s []querier.Item) Series {
out := Series{
Query: q,
}

for _, i := range s {
out.Data = append(out.Data, SeriesItem{
Time: i.Time,
Value: i.Value,
})
}
return out
}
10 changes: 10 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type Query struct {
Expression string `yaml:"expression"`
Range time.Duration `yaml:"range"`
Step time.Duration `yaml:"step"`
BoolValue bool `yaml:"bool"`
Units string `yaml:"units"`
}

type Service struct {
Expand All @@ -27,13 +29,18 @@ type Service struct {
type UI struct {
PageTitle string `yaml:"title"`
RefreshInterval time.Duration `yaml:"refresh"`
Graphs Graphs `yaml:"graphs"`
}

type Metrics struct {
Enabled bool `yaml:"enabled"`
Port int `yaml:"port"`
}

type Graphs struct {
Points int `yaml:"points"`
}

type Config struct {
Port int `yaml:"port"`
Metrics Metrics `yaml:"metrics"`
Expand Down Expand Up @@ -100,6 +107,9 @@ func setDefaults(conf *Config) {
if conf.UI.RefreshInterval == 0 {
conf.UI.RefreshInterval = time.Second * 30
}
if conf.UI.Graphs.Points <= 0 {
conf.UI.Graphs.Points = 200
}
}

func (c *Config) Validate() error {
Expand Down
2 changes: 1 addition & 1 deletion internal/http/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func NewGetAllHandler(app *app.App, cache *ResultCache) echo.HandlerFunc {

func NewGetHandler(app *app.App, cache *ResultCache) echo.HandlerFunc {
return func(c echo.Context) error {
svc, err := cache.GetService(c.Param("name"))
svc, _, err := cache.GetService(c.Param("name"))
if err != nil {
if errors.Is(err, ErrNotFound) {
return c.JSON(http.StatusNotFound, struct{}{})
Expand Down
1 change: 1 addition & 0 deletions internal/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func NewHttp(app *app.App, cache *ResultCache) *Http {
e.Use(echoprometheus.NewMiddleware("prompage"))

e.GET("/", NewStatusPageHandler(app, cache))
e.GET("/:name", NewGetServiceHandler(app, cache))
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", http.FileServerFS(static.FS))))

e.GET("/api/services", NewGetAllHandler(app, cache))
Expand Down
8 changes: 4 additions & 4 deletions internal/http/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ func (c *ResultCache) Get() ([]collector.Result, time.Time) {
return c.results, c.time
}

func (c *ResultCache) GetService(name string) (collector.Result, error) {
results, _ := c.Get()
func (c *ResultCache) GetService(name string) (collector.Result, time.Time, error) {
results, t := c.Get()
for _, r := range results {
if r.Service.Name == name {
return r, nil
return r, t, nil
}
}
return collector.Result{}, ErrNotFound
return collector.Result{}, t, ErrNotFound
}

func (c *ResultCache) Work(ctx context.Context) {
Expand Down
56 changes: 56 additions & 0 deletions internal/http/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package http

import (
"errors"
"html/template"
"log"
"net/http"
"time"

"github.com/henrywhitaker3/prompage/internal/app"
"github.com/henrywhitaker3/prompage/internal/collector"
"github.com/henrywhitaker3/prompage/internal/config"
"github.com/henrywhitaker3/prompage/internal/resources/views"
"github.com/labstack/echo/v4"
)

type getServiceData struct {
views.Builder

Config config.Config
Age time.Duration
Version string
Result collector.Result
Graph template.HTML
}

func NewGetServiceHandler(app *app.App, cache *ResultCache) echo.HandlerFunc {
return func(c echo.Context) error {
name := c.Param("name")
svc, age, err := cache.GetService(name)
if err != nil {
if errors.Is(err, ErrNotFound) {
return c.NoContent(http.StatusNotFound)
}
log.Printf("ERROR - could not find service: %s\n", name)
return err
}
graph, err := views.GenerateLineChart(svc.Series, app.Config.UI.Graphs.Points)
if err != nil {
log.Printf("ERROR - could not generate graph: %s\n", err)
}
data := getServiceData{
Age: time.Since(age).Round(time.Second),
Result: svc,
Graph: template.HTML(graph),
}

out, err := views.Build(views.SERVICE, data)
if err != nil {
log.Printf("ERROR - could not render template: %s\n", err)
return err
}

return c.HTML(http.StatusOK, out)
}
}
26 changes: 6 additions & 20 deletions internal/http/status.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
package http

import (
"bytes"
"fmt"
"html/template"
"log"
"net/http"
"time"

"github.com/hako/durafmt"
"github.com/henrywhitaker3/prompage/internal/app"
"github.com/henrywhitaker3/prompage/internal/collector"
"github.com/henrywhitaker3/prompage/internal/config"
Expand All @@ -28,6 +23,8 @@ type group struct {
}

type statusData struct {
views.Builder

Config config.Config
Results []group
Age time.Duration
Expand All @@ -37,17 +34,7 @@ type statusData struct {
Refresh int
}

func (s statusData) Sprintf(format string, a ...any) string {
return fmt.Sprintf(format, a...)
}

func (s statusData) PrettyDuration(duration time.Duration) string {
return durafmt.Parse(duration).String()
}

func NewStatusPageHandler(app *app.App, cache *ResultCache) echo.HandlerFunc {
tmpl := template.Must(template.ParseFS(views.Views, "index.html"))

return func(c echo.Context) error {
res, t := cache.Get()
age := time.Since(t)
Expand All @@ -62,13 +49,12 @@ func NewStatusPageHandler(app *app.App, cache *ResultCache) echo.HandlerFunc {
Version: app.Version,
Refresh: int(app.Config.UI.RefreshInterval / time.Millisecond),
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
log.Printf("ERROR - could not render template: %s", err)

out, err := views.Build(views.STATUS, data)
if err != nil {
return err
}

return c.HTML(http.StatusOK, buf.String())
return c.HTML(http.StatusOK, out)
}
}

Expand Down
43 changes: 33 additions & 10 deletions internal/querier/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,39 +39,56 @@ func NewQuerier(conf *config.Config) (*Querier, error) {
}, nil
}

func (q *Querier) Uptime(ctx context.Context, query config.Query) (float32, error) {
type Item struct {
Time time.Time
Value float64
}

func (q *Querier) Uptime(ctx context.Context, query config.Query) (float32, []Item, error) {
val, _, err := q.client.QueryRange(ctx, query.Query, v1.Range{
Start: time.Now().Add(-query.Range),
End: time.Now(),
Step: query.Step,
})
if err != nil {
return 0, err
return 0, nil, err
}

switch r := val.(type) {
case model.Matrix:
if r.Len() < 1 {
return 0, errors.New("no results for query")
return 0, nil, errors.New("no results for query")
}

passing := 0
total := 0
series := []Item{}
for _, val := range r[0].Values {
res, err := q.vector(val.Value, query)
if err != nil {
panic(err)
return 0, nil, err
}
total++

value := float64(0)
if res {
passing++
value = 1
}
if !query.BoolValue {
f, err := q.asFloat(val.Value)
if err != nil {
return 0, nil, err
}
value = f
}
series = append(series, Item{Time: val.Timestamp.Time(), Value: value})
}

return (float32(passing) / float32(total)) * 100, nil
return (float32(passing) / float32(total)) * 100, series, nil
}

return 100, ErrTypeNotImplemented
return 100, nil, ErrTypeNotImplemented
}

func (q *Querier) Status(ctx context.Context, query config.Query) (bool, error) {
Expand Down Expand Up @@ -100,18 +117,24 @@ func (q *Querier) vector(v model.SampleValue, query config.Query) (bool, error)
if err != nil {
return false, fmt.Errorf("failed to compile expr: %v", err)
}

val, err := strconv.ParseFloat(v.String(), 64)
val, err := q.asFloat(v)
if err != nil {
return false, fmt.Errorf("failed to parse result: %v", err)
return false, err
}

env["result"] = val

out, err := expr.Run(exp, env)
if err != nil {
return false, err
}

return out.(bool), nil
}

func (q *Querier) asFloat(v model.SampleValue) (float64, error) {
val, err := strconv.ParseFloat(v.String(), 64)
if err != nil {
return 0, fmt.Errorf("failed to parse float: %v", err)
}
return val, nil
}
Loading

0 comments on commit ccb2362

Please sign in to comment.