v.0.2.3: Add thicknessquery and highlightedquery. Add human readable formatting for main|secondarystats

This commit is contained in:
yo 2025-01-12 21:00:25 +01:00
parent c64327ad0b
commit cc3fcba1d7
4 changed files with 197 additions and 49 deletions

View File

@ -1,3 +1,6 @@
# Formatting metrics in main & secondarystat. Supported: "english", "french", "german", "ukrainian", "chinese", "arabic"
language: 'english'
# datasource describe a way to get prometheus metrics. # datasource describe a way to get prometheus metrics.
# Properties : # Properties :
# - name: name of the query. To be used in edges or nodes mainstatquery or secondarystatquery. Result will be output in mainstat, or secondarystat. # - name: name of the query. To be used in edges or nodes mainstatquery or secondarystatquery. Result will be output in mainstat, or secondarystat.
@ -17,6 +20,12 @@ datasources:
address: 'http://prometheus.local.lan:9090' address: 'http://prometheus.local.lan:9090'
query: 'sum(rate(node_cpu_seconds_total{instance="router01.local.lan:9100",job="node",mode!~"idle"}[30s]))*100' query: 'sum(rate(node_cpu_seconds_total{instance="router01.local.lan:9100",job="node",mode!~"idle"}[30s]))*100'
timeout: 10 timeout: 10
- name: node_cpu_metric_over_80
type: query
address: 'http://prometheus.local.lan:9090'
# Return 1 if cpu rate > 80%
query: '(sum(rate(node_cpu_seconds_total{instance="router01.local.lan:9100",job="node",mode!~"idle"}[30s]))*100) > bool 80'
timeout: 10
- name: router01_net_down_rate - name: router01_net_down_rate
# Range query. Return all metrics from a time range. Result will be averaged from these metrics. Time range will be provided by Grafana. # Range query. Return all metrics from a time range. Result will be averaged from these metrics. Time range will be provided by Grafana.
type: query_range type: query_range
@ -38,6 +47,11 @@ datasources:
address: 'http://prometheus.local.lan:9090' address: 'http://prometheus.local.lan:9090'
query: 'rate(node_network_transmit_bytes_total{device="ix3", instance="router01.local.lan:9100", job="node"}[30s])' query: 'rate(node_network_transmit_bytes_total{device="ix3", instance="router01.local.lan:9100", job="node"}[30s])'
timeout: 10 timeout: 10
- name: router01_net_down_rate_perten
type: query
address: 'http://prometheus.moon.lan:9090'
query: 'rate(node_network_receive_bytes_total{device="igb0", instance="router01.local.lan:9100", job="node"}[30s])/62500000*10'
timeout: 10
# graphs identifies context for a nodegraph. You can have many contexts, and your grafana query will mention this context name. # graphs identifies context for a nodegraph. You can have many contexts, and your grafana query will mention this context name.
# For this example named "internet", grafana URL will be : # For this example named "internet", grafana URL will be :
@ -47,16 +61,25 @@ datasources:
# - name: name of the context # - name: name of the context
# - nodes: list of nodegraph nodes # - nodes: list of nodegraph nodes
# - edges: list of nodegraph edges # - edges: list of nodegraph edges
#
# Dynamic fields :
# nodes:
# - mainstat : use mainstatquery and mainstatformat. mainstatquery should return a metric, mainstatformat is a printf format specifier.
# - secondarystat : use secondarystatquery and secondarystatformat. same as mainstat(query|format).
# - hightlighted : use highlightedquery. if result return > 0, item will be highligthed.
# edges:
# - same list as nodes.
# - thickness: use thicknessquery.
graphs: graphs:
- name: internet - name: internet
nodes: nodes:
- name: internet - name: host01
id: internet id: host01
title: "internet" title: host01
subtitle: "The internets" subtitle: "A workstation"
color: "grey" color: "green"
# icons come from https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview # icon come from https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview
icon: "globe" icon: "user"
- name: router01 - name: router01
id: router01 id: router01
title: router01 title: router01
@ -65,39 +88,43 @@ graphs:
mainstatquery: '{{ node_cpu_metric }}' mainstatquery: '{{ node_cpu_metric }}'
# Use %% if you want to display '%' in you metric label # Use %% if you want to display '%' in you metric label
mainstatformat: '%0.2f%% cpu' mainstatformat: '%0.2f%% cpu'
- name: host01 # highlight router if cpu > 80%
id: host01 highlightedquery: '{{ node_cpu_metric_over_80 }}'
title: host01 - name: internet
subtitle: "A workstation" id: internet
color: "green" title: "internet"
icon: "user" subtitle: "The internets"
color: "grey"
icon: "globe"
edges: edges:
- id: edge0 - id: edge0
source: internet
target: router01
mainstatquery: '{{ router01_net_up_rate }}'
mainstatformat: 'up %0.0f bps'
secondarystatquery: '{{ router01_net_down_rate }}'
secondarystatformat: 'down %0.0f bps'
- id: edge1
source: router01
target: internet
mainstatquery: '{{ router01_net_up_rate }}'
mainstatformat: 'up %0.0f bps'
secondarystatquery: '{{ router01_net_down_rate }}'
secondarystatformat: 'down %0.0f bps'
- id: edge2
source: router01
target: host01
mainstatquery: '{{ router01_lan_down_rate }}'
mainstatformat: 'up %0.0f bps'
secondarystatquery: '{{ router01_lan_up_rate }}'
secondarystatformat: 'down %0.0f bps'
- id: edge3
source: host01 source: host01
target: router01 target: router01
mainstatquery: '{{ router01_lan_down_rate }}' mainstatquery: '{{ router01_lan_down_rate }}'
mainstatformat: 'up %0.0f bps' mainstatformat: 'up %0.0f bps'
secondarystatquery: '{{ router01_lan_up_rate }}' secondarystatquery: '{{ router01_lan_up_rate }}'
secondarystatformat: 'down %0.0f bps' secondarystatformat: 'down %0.0f Bps'
- id: edge1
source: router01
target: host01
mainstatquery: '{{ router01_lan_down_rate }}'
mainstatformat: 'up %0.0f bps'
secondarystatquery: '{{ router01_lan_up_rate }}'
secondarystatformat: 'down %0.0f Bps'
thicknessquery: '{{ router01_net_down_rate_perten }}'
- id: edge2
source: router01
target: internet
mainstatquery: '{{ router01_net_up_rate }}'
mainstatformat: 'up %0.0f bps'
secondarystatquery: '{{ router01_net_down_rate }}'
secondarystatformat: 'down %0.0f Bps'
- id: edge3
source: internet
target: router01
mainstatquery: '{{ router01_net_up_rate }}'
mainstatformat: 'up %0.0f bps'
secondarystatquery: '{{ router01_net_down_rate }}'
secondarystatformat: 'down %0.0f Bps'
thicknessquery: '{{ router01_net_down_rate_perten }}'

View File

@ -20,7 +20,9 @@ type Edge struct {
SecondaryStatFormat string `yaml:"secondarystatformat,omitempty" json:"secondarystatformat,omitempty"` SecondaryStatFormat string `yaml:"secondarystatformat,omitempty" json:"secondarystatformat,omitempty"`
Color string `yaml:"color,omitempty" json:"color,omitempty"` Color string `yaml:"color,omitempty" json:"color,omitempty"`
Thickness int `yaml:"thickness,omitempty" json:"thickness,omitempty"` Thickness int `yaml:"thickness,omitempty" json:"thickness,omitempty"`
HighLighted bool `yaml:"highlighted,omitempty" json:"hightlighted,omitempty"` ThicknessQuery string `yaml:"thicknessquery,omitempty" json:"thicknessquery,omitempty"`
Highlighted bool `yaml:"highlighted,omitempty" json:"hightlighted,omitempty"`
HighlightedQuery string `yaml:"highlightedquery,omitempty" json:"highlightedquery,omitempty"`
StrokeDashArray float32 `yaml:"strokeDasharray,omitempty" json:"strokeDasharray,omitempty"` StrokeDashArray float32 `yaml:"strokeDasharray,omitempty" json:"strokeDasharray,omitempty"`
} }
@ -56,10 +58,26 @@ func (e *Edge) SetMainStat(stat string) {
e.MainStat = fmt.Sprintf("%s", stat) e.MainStat = fmt.Sprintf("%s", stat)
} }
func (e *Edge) GetThicknessQuery() string {
return e.ThicknessQuery
}
func (e *Edge) SetThickness(thickness float64) {
e.Thickness = int(thickness)
}
func (e *Edge) SetSecondaryStat(stat string) { func (e *Edge) SetSecondaryStat(stat string) {
e.SecondaryStat = fmt.Sprintf("%s", stat) e.SecondaryStat = fmt.Sprintf("%s", stat)
} }
func (e *Edge) GetHighlightedQuery() string {
return e.HighlightedQuery
}
func (e *Edge) SetHighlighted(highlighted bool) {
e.Highlighted = highlighted
}
// Custom marshaler to not send (main|secondary)statquery // Custom marshaler to not send (main|secondary)statquery
func (e Edge) MarshalJSON() ([]byte, error) { func (e Edge) MarshalJSON() ([]byte, error) {
jsonRes := `{"id":"` + e.Id + `","source":"` + e.Source + `","target":"` + e.Target + `"` jsonRes := `{"id":"` + e.Id + `","source":"` + e.Source + `","target":"` + e.Target + `"`
@ -75,7 +93,7 @@ func (e Edge) MarshalJSON() ([]byte, error) {
if e.Thickness > 0 { if e.Thickness > 0 {
jsonRes += `,"thickness":` + strconv.Itoa(e.Thickness) jsonRes += `,"thickness":` + strconv.Itoa(e.Thickness)
} }
if e.HighLighted { if e.Highlighted {
jsonRes += `,"highlighted":true` jsonRes += `,"highlighted":true`
} }
// TODO : e.StrokeDashArray // TODO : e.StrokeDashArray

95
main.go
View File

@ -25,13 +25,16 @@ import (
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/text/language"
"golang.org/x/text/message"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"github.com/prometheus/client_golang/api" "github.com/prometheus/client_golang/api"
v1 "github.com/prometheus/client_golang/api/prometheus/v1" v1 "github.com/prometheus/client_golang/api/prometheus/v1"
) )
const ( const (
gVersion = "0.2.1" gVersion = "0.2.3"
) )
type PromDataSourceConfig struct { type PromDataSourceConfig struct {
@ -58,6 +61,10 @@ type Item interface {
GetSecondaryStatQuery() string GetSecondaryStatQuery() string
GetSecondaryStatFormat() string GetSecondaryStatFormat() string
SetSecondaryStat(string) SetSecondaryStat(string)
GetThicknessQuery() string
SetThickness(float64)
GetHighlightedQuery() string
SetHighlighted(bool)
} }
// Query arguments. Based on grafana internal variables. // Query arguments. Based on grafana internal variables.
@ -75,6 +82,7 @@ type MyRange struct {
} }
var ( var (
gPrinter *message.Printer
gDebug bool gDebug bool
gGraphs []Graph gGraphs []Graph
gDataSources []PromDataSourceConfig gDataSources []PromDataSourceConfig
@ -118,7 +126,7 @@ func (d *PromDataSourceConfig) GetData(timeRange *MyRange) (float64, error) {
if len(warnings) > 0 { if len(warnings) > 0 {
log.Warningf("DataSourceConfig.GetData: Warnings: %v\n", warnings) log.Warningf("DataSourceConfig.GetData: Warnings: %v\n", warnings)
} }
log.Debugf("DataSourceConfig.GetData: Result: %v\n", result) //log.Debugf("DataSourceConfig.GetData: Result: %v\n", result)
switch { switch {
case result.Type() == model.ValScalar: case result.Type() == model.ValScalar:
@ -165,6 +173,7 @@ func (g Graph) BuildMetrics(items *[]Item, timeRange *MyRange) (*[]Item, error)
defer gCfgMutex.RUnlock() defer gCfgMutex.RUnlock()
for _, item := range *items { for _, item := range *items {
// Handle mainStat
if len(item.GetMainStatQuery()) > 0 { if len(item.GetMainStatQuery()) > 0 {
log.Debugf("Item %s have mainstatquery: %s\n", item.GetId(), item.GetMainStatQuery()) log.Debugf("Item %s have mainstatquery: %s\n", item.GetId(), item.GetMainStatQuery())
r := gDSVarCompRegex.FindStringSubmatch(item.GetMainStatQuery()) r := gDSVarCompRegex.FindStringSubmatch(item.GetMainStatQuery())
@ -185,12 +194,13 @@ func (g Graph) BuildMetrics(items *[]Item, timeRange *MyRange) (*[]Item, error)
if len(format) == 0 { if len(format) == 0 {
format = "%f" format = "%f"
} }
log.Debugf("buildMetrics: Replace %s mainstat with %s\n", item.GetId(), fmt.Sprintf(format, value)) log.Debugf("buildMetrics: Replace %s mainstat with %s\n", item.GetId(), gPrinter.Sprintf(format, value))
item.SetMainStat(fmt.Sprintf(format, value)) item.SetMainStat(gPrinter.Sprintf(format, value))
} else { } else {
log.Errorf("buildMetrics: Item %s mainstatquery unparseable: %s\n", item.GetId(), item.GetMainStatQuery()) log.Errorf("buildMetrics: Item %s mainstatquery unparseable: %s\n", item.GetId(), item.GetMainStatQuery())
} }
} }
// Handle secondaryStat
if len(item.GetSecondaryStatQuery()) > 0 { if len(item.GetSecondaryStatQuery()) > 0 {
log.Debugf("Item %s have mainstatquery: %s\n", item.GetId(), item.GetSecondaryStatQuery()) log.Debugf("Item %s have mainstatquery: %s\n", item.GetId(), item.GetSecondaryStatQuery())
r := gDSVarCompRegex.FindStringSubmatch(item.GetSecondaryStatQuery()) r := gDSVarCompRegex.FindStringSubmatch(item.GetSecondaryStatQuery())
@ -211,12 +221,66 @@ func (g Graph) BuildMetrics(items *[]Item, timeRange *MyRange) (*[]Item, error)
if len(format) == 0 { if len(format) == 0 {
format = "%f" format = "%f"
} }
log.Debugf("buildMetrics: Replace %s secondarystat with %s\n", item.GetId(), fmt.Sprintf(format, value)) log.Debugf("buildMetrics: Replace %s secondarystat with %s\n", item.GetId(), gPrinter.Sprintf(format, value))
item.SetSecondaryStat(fmt.Sprintf(format, value)) item.SetSecondaryStat(gPrinter.Sprintf(format, value))
} else { } else {
log.Errorf("buildMetrics: Item %s secondarystatquery unparseable: %s\n", item.GetId(), item.GetSecondaryStatQuery()) log.Errorf("buildMetrics: Item %s secondarystatquery unparseable: %s\n", item.GetId(), item.GetSecondaryStatQuery())
} }
} }
// Handle highlighted
if len(item.GetHighlightedQuery()) > 0 {
log.Debugf("buildMetrics: Item %s have highlightedquery: %s\n", item.GetId(), item.GetHighlightedQuery())
r := gDSVarCompRegex.FindStringSubmatch(item.GetHighlightedQuery())
if len(r) > 1 {
var value float64
dsname := strings.TrimSpace(r[1])
log.Debugf("buildMetrics: datasource from highlightedquery : %s\n", dsname)
for _, d := range gDataSources {
if strings.EqualFold(d.Name, dsname) {
value, err = d.GetData(timeRange)
if err != nil {
return nil, err
}
break
}
}
highlight := false
if value == 0 {
highlight = false
} else {
highlight = true
}
log.Debugf("buildMetrics: Replace %s highlighted with %t\n", item.GetId(), highlight)
item.SetHighlighted(highlight)
} else {
log.Errorf("buildMetrics: Item %s highlightedquery unparseable: %s\n", item.GetId(), item.GetHighlightedQuery())
}
}
switch item.(type) {
case *Edge:
if len(item.GetThicknessQuery()) > 0 {
log.Debugf("buildMetrics: Item %s have thicknessquery: %s\n", item.GetId(), item.GetThicknessQuery())
r := gDSVarCompRegex.FindStringSubmatch(item.GetThicknessQuery())
if len(r) > 1 {
var value float64
dsname := strings.TrimSpace(r[1])
log.Debugf("buildMetrics: datasource from thicknessquery : %s\n", dsname)
for _, d := range gDataSources {
if strings.EqualFold(d.Name, dsname) {
value, err = d.GetData(timeRange)
if err != nil {
return nil, err
}
break
}
}
log.Debugf("buildMetrics: Replace %s thickness with %s\n", item.GetId(), fmt.Sprintf("%0.0f", value))
item.SetThickness(value)
} else {
log.Errorf("buildMetrics: Item %s thicknessquery unparseable: %s\n", item.GetId(), item.GetThicknessQuery())
}
}
}
} }
return items, nil return items, nil
} }
@ -372,6 +436,25 @@ func reloadConfigFile() {
os.Exit(1) os.Exit(1)
} }
} }
switch viper.Get("language").(string) {
case "english":
gPrinter = message.NewPrinter(language.English)
case "french":
gPrinter = message.NewPrinter(language.French)
case "german":
gPrinter = message.NewPrinter(language.German)
case "ukrainian":
gPrinter = message.NewPrinter(language.Ukrainian)
case "arabic":
gPrinter = message.NewPrinter(language.Arabic)
case "chinese":
gPrinter = message.NewPrinter(language.Chinese)
default:
log.Errorf("Language not implented: %s. Fallback to english\n", viper.Get("language").(string))
gPrinter = message.NewPrinter(language.English)
}
// then clear current config, after acquiring WriteLock // then clear current config, after acquiring WriteLock
gCfgMutex.Lock() gCfgMutex.Lock()
defer gCfgMutex.Unlock() defer gCfgMutex.Unlock()

View File

@ -22,7 +22,8 @@ type Node struct {
Color string `yaml:"color,omitempty" json:"color,omitempty"` Color string `yaml:"color,omitempty" json:"color,omitempty"`
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"` Icon string `yaml:"icon,omitempty" json:"icon,omitempty"`
NodeRadius int `yaml:"noderadius,omitempty" json:"noderadius,omitempty"` NodeRadius int `yaml:"noderadius,omitempty" json:"noderadius,omitempty"`
HighLighted bool `yaml:"highlighted,omitempty" json:"hightlighted,omitempty"` Highlighted bool `yaml:"highlighted,omitempty" json:"hightlighted,omitempty"`
HighlightedQuery string `yaml:"highlightedquery,omitempty" json:"highlightedquery,omitempty"`
} }
func (n Node) GetId() string { func (n Node) GetId() string {
@ -61,6 +62,25 @@ func (n *Node) SetSecondaryStat(stat string) {
n.SecondaryStat = fmt.Sprintf("%s", stat) n.SecondaryStat = fmt.Sprintf("%s", stat)
} }
// Have to be implemented to satisfy Item interface
func (n *Node) GetThicknessQuery() string {
panic("Not an Edge")
return ""
}
// Have to be implemented to satisfy Item interface
func (n *Node) SetThickness(thickness float64) {
panic("Not an Edge")
}
func (n *Node) GetHighlightedQuery() string {
return n.HighlightedQuery
}
func (n *Node) SetHighlighted(highlighted bool) {
n.Highlighted = highlighted
}
// Custom marshaler to not send (main|secondary)statquery // Custom marshaler to not send (main|secondary)statquery
func (n Node) MarshalJSON() ([]byte, error) { func (n Node) MarshalJSON() ([]byte, error) {
jsonRes := `{"name":"` + n.Name + `","id":"` + n.Id + `"` jsonRes := `{"name":"` + n.Name + `","id":"` + n.Id + `"`
@ -85,7 +105,7 @@ func (n Node) MarshalJSON() ([]byte, error) {
if n.NodeRadius > 0 { if n.NodeRadius > 0 {
jsonRes += `,"noderadius":` + strconv.Itoa(n.NodeRadius) jsonRes += `,"noderadius":` + strconv.Itoa(n.NodeRadius)
} }
if n.HighLighted { if n.Highlighted {
jsonRes += `,"highlighted":true` jsonRes += `,"highlighted":true`
} }
// TODO : n.StrokeDashArray // TODO : n.StrokeDashArray