chore: publish pxmon v0.2.0
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
chart "github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
// ChartOptions controls rendering of the node network PNG.
|
||||
type ChartOptions struct {
|
||||
Title string
|
||||
Subtitle string
|
||||
Width int
|
||||
Height int
|
||||
Percentile float64
|
||||
}
|
||||
|
||||
// RenderNodeNetworkPNG returns a PNG of the node-wide Rx+Tx time series with
|
||||
// the requested percentile drawn as a horizontal annotation line.
|
||||
func RenderNodeNetworkPNG(points []NodeSamplePoint, opts ChartOptions) ([]byte, error) {
|
||||
if opts.Width <= 0 {
|
||||
opts.Width = 1280
|
||||
}
|
||||
if opts.Height <= 0 {
|
||||
opts.Height = 640
|
||||
}
|
||||
if opts.Percentile <= 0 {
|
||||
opts.Percentile = 95
|
||||
}
|
||||
if opts.Title == "" {
|
||||
opts.Title = "Node network usage"
|
||||
}
|
||||
|
||||
if len(points) < 2 {
|
||||
return renderEmptyChart(opts, "insufficient history — need at least 2 samples")
|
||||
}
|
||||
|
||||
xs := make([]time.Time, 0, len(points))
|
||||
rxs := make([]float64, 0, len(points))
|
||||
txs := make([]float64, 0, len(points))
|
||||
totals := make([]float64, 0, len(points))
|
||||
maxVal := 0.0
|
||||
for _, p := range points {
|
||||
xs = append(xs, p.Timestamp)
|
||||
rxs = append(rxs, p.RxMbps)
|
||||
txs = append(txs, p.TxMbps)
|
||||
totals = append(totals, p.TotalMbps)
|
||||
if p.TotalMbps > maxVal {
|
||||
maxVal = p.TotalMbps
|
||||
}
|
||||
}
|
||||
|
||||
pct := PercentileMbps(points, opts.Percentile)
|
||||
yMax := maxVal * 1.15
|
||||
if pct*1.10 > yMax {
|
||||
yMax = pct * 1.10
|
||||
}
|
||||
if yMax <= 0 {
|
||||
yMax = 1
|
||||
}
|
||||
|
||||
percentileSeries := chart.ContinuousSeries{
|
||||
Name: fmt.Sprintf("P%.0f = %.1f Mbps", opts.Percentile, pct),
|
||||
Style: chart.Style{
|
||||
StrokeColor: drawing.ColorFromHex("e74c3c"),
|
||||
StrokeWidth: 2.0,
|
||||
StrokeDashArray: []float64{6, 4},
|
||||
},
|
||||
XValues: []float64{chart.TimeToFloat64(xs[0]), chart.TimeToFloat64(xs[len(xs)-1])},
|
||||
YValues: []float64{pct, pct},
|
||||
}
|
||||
|
||||
graph := chart.Chart{
|
||||
Title: opts.Title,
|
||||
TitleStyle: chart.Style{
|
||||
FontSize: 16,
|
||||
},
|
||||
Width: opts.Width,
|
||||
Height: opts.Height,
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{Top: 60, Left: 40, Right: 40, Bottom: 40},
|
||||
FillColor: drawing.Color{
|
||||
R: 0xf8, G: 0xf9, B: 0xfa, A: 0xff,
|
||||
},
|
||||
},
|
||||
XAxis: chart.XAxis{
|
||||
Style: chart.Style{FontSize: 9},
|
||||
ValueFormatter: chart.TimeValueFormatterWithFormat("15:04:05\n02 Jan"),
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Name: "Mbps",
|
||||
Style: chart.Style{FontSize: 9},
|
||||
Range: &chart.ContinuousRange{Min: 0, Max: yMax},
|
||||
ValueFormatter: func(v any) string {
|
||||
if f, ok := v.(float64); ok {
|
||||
return formatMbps(f)
|
||||
}
|
||||
return ""
|
||||
},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.TimeSeries{
|
||||
Name: "Total Rx+Tx",
|
||||
Style: chart.Style{
|
||||
StrokeColor: drawing.ColorFromHex("2d7dd2"),
|
||||
StrokeWidth: 2.0,
|
||||
FillColor: drawing.ColorFromHex("2d7dd2").WithAlpha(50),
|
||||
},
|
||||
XValues: xs,
|
||||
YValues: totals,
|
||||
},
|
||||
chart.TimeSeries{
|
||||
Name: "Rx",
|
||||
Style: chart.Style{
|
||||
StrokeColor: drawing.ColorFromHex("3cb371"),
|
||||
StrokeWidth: 1.5,
|
||||
},
|
||||
XValues: xs,
|
||||
YValues: rxs,
|
||||
},
|
||||
chart.TimeSeries{
|
||||
Name: "Tx",
|
||||
Style: chart.Style{
|
||||
StrokeColor: drawing.ColorFromHex("ffa500"),
|
||||
StrokeWidth: 1.5,
|
||||
},
|
||||
XValues: xs,
|
||||
YValues: txs,
|
||||
},
|
||||
percentileSeries,
|
||||
},
|
||||
}
|
||||
|
||||
if opts.Subtitle != "" {
|
||||
graph.Elements = []chart.Renderable{subtitleRenderable(opts.Subtitle)}
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := graph.Render(chart.PNG, buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func subtitleRenderable(text string) chart.Renderable {
|
||||
return func(r chart.Renderer, cb chart.Box, chartDefaults chart.Style) {
|
||||
r.SetFont(chartDefaults.GetFont())
|
||||
r.SetFontColor(drawing.Color{R: 90, G: 90, B: 90, A: 0xff})
|
||||
r.SetFontSize(10)
|
||||
r.Text(text, cb.Left+10, cb.Top+30)
|
||||
}
|
||||
}
|
||||
|
||||
func renderEmptyChart(opts ChartOptions, note string) ([]byte, error) {
|
||||
graph := chart.Chart{
|
||||
Title: opts.Title,
|
||||
Width: opts.Width,
|
||||
Height: opts.Height,
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{Top: 60, Left: 40, Right: 40, Bottom: 40},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: []float64{0, 1},
|
||||
YValues: []float64{0, 0},
|
||||
Style: chart.Style{
|
||||
StrokeColor: drawing.ColorTransparent,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
graph.Elements = []chart.Renderable{
|
||||
func(r chart.Renderer, cb chart.Box, cs chart.Style) {
|
||||
r.SetFont(cs.GetFont())
|
||||
r.SetFontColor(drawing.Color{R: 120, G: 120, B: 120, A: 0xff})
|
||||
r.SetFontSize(14)
|
||||
r.Text(note, cb.Left+20, cb.Top+cb.Height()/2)
|
||||
},
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
if err := graph.Render(chart.PNG, buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func formatMbps(v float64) string {
|
||||
switch {
|
||||
case v >= 1000:
|
||||
return fmt.Sprintf("%.2f Gbps", v/1000)
|
||||
case v >= 1:
|
||||
return fmt.Sprintf("%.1f Mbps", v)
|
||||
default:
|
||||
return fmt.Sprintf("%.0f Kbps", v*1000)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user