From cc3fcba1d7a47e37cc8fa5acb62bcf8d66b7efc2 Mon Sep 17 00:00:00 2001 From: yo Date: Sun, 12 Jan 2025 21:00:25 +0100 Subject: [PATCH] v.0.2.3: Add thicknessquery and highlightedquery. Add human readable formatting for main|secondarystats --- config.yaml.sample | 97 +++++++++++++++++++++++++++--------------- edges.go | 22 +++++++++- main.go | 103 ++++++++++++++++++++++++++++++++++++++++----- nodes.go | 24 ++++++++++- 4 files changed, 197 insertions(+), 49 deletions(-) diff --git a/config.yaml.sample b/config.yaml.sample index ae0a803..211c56e 100644 --- a/config.yaml.sample +++ b/config.yaml.sample @@ -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. # Properties : # - 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' query: 'sum(rate(node_cpu_seconds_total{instance="router01.local.lan:9100",job="node",mode!~"idle"}[30s]))*100' 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 # 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 @@ -38,6 +47,11 @@ datasources: address: 'http://prometheus.local.lan:9090' query: 'rate(node_network_transmit_bytes_total{device="ix3", instance="router01.local.lan:9100", job="node"}[30s])' 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. # For this example named "internet", grafana URL will be : @@ -47,16 +61,25 @@ datasources: # - name: name of the context # - nodes: list of nodegraph nodes # - 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: - name: internet nodes: - - name: internet - id: internet - title: "internet" - subtitle: "The internets" - color: "grey" - # icons come from https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview - icon: "globe" + - name: host01 + id: host01 + title: host01 + subtitle: "A workstation" + color: "green" + # icon come from https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview + icon: "user" - name: router01 id: router01 title: router01 @@ -65,39 +88,43 @@ graphs: mainstatquery: '{{ node_cpu_metric }}' # Use %% if you want to display '%' in you metric label mainstatformat: '%0.2f%% cpu' - - name: host01 - id: host01 - title: host01 - subtitle: "A workstation" - color: "green" - icon: "user" + # highlight router if cpu > 80% + highlightedquery: '{{ node_cpu_metric_over_80 }}' + - name: internet + id: internet + title: "internet" + subtitle: "The internets" + color: "grey" + icon: "globe" edges: - 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 target: router01 mainstatquery: '{{ router01_lan_down_rate }}' mainstatformat: 'up %0.0f bps' 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 }}' diff --git a/edges.go b/edges.go index 64e961e..b7a1342 100644 --- a/edges.go +++ b/edges.go @@ -20,7 +20,9 @@ type Edge struct { SecondaryStatFormat string `yaml:"secondarystatformat,omitempty" json:"secondarystatformat,omitempty"` Color string `yaml:"color,omitempty" json:"color,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"` } @@ -56,10 +58,26 @@ func (e *Edge) SetMainStat(stat string) { 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) { 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 func (e Edge) MarshalJSON() ([]byte, error) { jsonRes := `{"id":"` + e.Id + `","source":"` + e.Source + `","target":"` + e.Target + `"` @@ -75,7 +93,7 @@ func (e Edge) MarshalJSON() ([]byte, error) { if e.Thickness > 0 { jsonRes += `,"thickness":` + strconv.Itoa(e.Thickness) } - if e.HighLighted { + if e.Highlighted { jsonRes += `,"highlighted":true` } // TODO : e.StrokeDashArray diff --git a/main.go b/main.go index 6318fad..81d745b 100644 --- a/main.go +++ b/main.go @@ -18,20 +18,23 @@ import ( "syscall" "net/http" "os/signal" - + "gopkg.in/yaml.v2" "github.com/spf13/viper" "github.com/gin-gonic/gin" "github.com/fsnotify/fsnotify" log "github.com/sirupsen/logrus" - + + "golang.org/x/text/language" + "golang.org/x/text/message" + "github.com/prometheus/common/model" "github.com/prometheus/client_golang/api" v1 "github.com/prometheus/client_golang/api/prometheus/v1" ) const ( - gVersion = "0.2.1" + gVersion = "0.2.3" ) type PromDataSourceConfig struct { @@ -57,7 +60,11 @@ type Item interface { GetSecondaryStat() string GetSecondaryStatQuery() string GetSecondaryStatFormat() string - SetSecondaryStat(string) + SetSecondaryStat(string) + GetThicknessQuery() string + SetThickness(float64) + GetHighlightedQuery() string + SetHighlighted(bool) } // Query arguments. Based on grafana internal variables. @@ -75,6 +82,7 @@ type MyRange struct { } var ( + gPrinter *message.Printer gDebug bool gGraphs []Graph gDataSources []PromDataSourceConfig @@ -118,7 +126,7 @@ func (d *PromDataSourceConfig) GetData(timeRange *MyRange) (float64, error) { if len(warnings) > 0 { 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 { case result.Type() == model.ValScalar: @@ -163,8 +171,9 @@ func (g Graph) BuildMetrics(items *[]Item, timeRange *MyRange) (*[]Item, error) // Acquire Read Lock. This won't prevent other queries being answered, but reloadConfig WLock will have to wait until we finish gCfgMutex.RLock() defer gCfgMutex.RUnlock() - + for _, item := range *items { + // Handle mainStat if len(item.GetMainStatQuery()) > 0 { log.Debugf("Item %s have mainstatquery: %s\n", item.GetId(), 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 { format = "%f" } - log.Debugf("buildMetrics: Replace %s mainstat with %s\n", item.GetId(), fmt.Sprintf(format, value)) - item.SetMainStat(fmt.Sprintf(format, value)) + log.Debugf("buildMetrics: Replace %s mainstat with %s\n", item.GetId(), gPrinter.Sprintf(format, value)) + item.SetMainStat(gPrinter.Sprintf(format, value)) } else { log.Errorf("buildMetrics: Item %s mainstatquery unparseable: %s\n", item.GetId(), item.GetMainStatQuery()) } } + // Handle secondaryStat if len(item.GetSecondaryStatQuery()) > 0 { log.Debugf("Item %s have mainstatquery: %s\n", item.GetId(), 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 { format = "%f" } - log.Debugf("buildMetrics: Replace %s secondarystat with %s\n", item.GetId(), fmt.Sprintf(format, value)) - item.SetSecondaryStat(fmt.Sprintf(format, value)) + log.Debugf("buildMetrics: Replace %s secondarystat with %s\n", item.GetId(), gPrinter.Sprintf(format, value)) + item.SetSecondaryStat(gPrinter.Sprintf(format, value)) } else { 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 } @@ -372,6 +436,25 @@ func reloadConfigFile() { 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 gCfgMutex.Lock() defer gCfgMutex.Unlock() diff --git a/nodes.go b/nodes.go index 236fb9d..6ad33fb 100644 --- a/nodes.go +++ b/nodes.go @@ -22,7 +22,8 @@ type Node struct { Color string `yaml:"color,omitempty" json:"color,omitempty"` Icon string `yaml:"icon,omitempty" json:"icon,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 { @@ -61,6 +62,25 @@ func (n *Node) SetSecondaryStat(stat string) { 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 func (n Node) MarshalJSON() ([]byte, error) { jsonRes := `{"name":"` + n.Name + `","id":"` + n.Id + `"` @@ -85,7 +105,7 @@ func (n Node) MarshalJSON() ([]byte, error) { if n.NodeRadius > 0 { jsonRes += `,"noderadius":` + strconv.Itoa(n.NodeRadius) } - if n.HighLighted { + if n.Highlighted { jsonRes += `,"highlighted":true` } // TODO : n.StrokeDashArray