Compare commits

6 Commits

7 changed files with 204 additions and 52 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
nodegopher nodegopher
config.yaml* config.yaml*
!config.yaml.sample !config.sample.yml
main.go.* main.go.*

View File

@ -12,16 +12,20 @@ It gets time range values from Grafana, so it can relay them to prometheus if yo
see config.yaml.sample see config.yaml.sample
## Grafana datasource ## Grafana datasource
Successfully tested with yesoreyeram-infinity-datasource datasource Successfully tested with [yesoreyeram-infinity-datasource](https://github.com/grafana/grafana-infinity-datasource) datasource
Create an infinity query, name it "Edges". Type "JSON", Parser "Backend", Source "URL", Format "Table", Method "GET" Create an infinity query, name it "Edges". Type "JSON", Parser "Backend", Source "URL", Format "Table", Method "GET"
URL: "http://my-nodegopher-host:8080/graph1/edges?from=$__from&to=$__to&interval=$__interval" URL: "http://my-nodegopher-host:8080/graph1/edges?from=$__from&to=$__to&interval=$__interval"
![Edges configuration](https://git.nosd.in/yo/nodegopher/raw/branch/main/docs/nodegopher_grafana_edges.png "Edges configuration")
Create a second infinity query named "Nodes", same config, Create a second infinity query named "Nodes", same config,
URL: "http://my-nodegopher-host:8080/graph1/nodes?from=$__from&to=$__to&interval=$__interval" URL: "http://my-nodegopher-host:8080/graph1/nodes?from=$__from&to=$__to&interval=$__interval"
switch to visualization type "NodeGraph", and voila switch to visualization type "NodeGraph", and voila
![NodeGraph sample](https://git.nosd.in/yo/nodegopher/raw/branch/main/docs/nodegopher_sample.png "NodeGraph sample")
## Instance management ## Instance management
### Reload configuration ### Reload configuration
You can reload configuration file when the API is running, with either sending a signal, or make a POST request. You can reload configuration file when the API is running, with either sending a signal, or make a POST request.

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.local.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 }}'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 34 KiB

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

103
main.go
View File

@ -18,20 +18,23 @@ import (
"syscall" "syscall"
"net/http" "net/http"
"os/signal" "os/signal"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"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 {
@ -57,7 +60,11 @@ type Item interface {
GetSecondaryStat() string GetSecondaryStat() string
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:
@ -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 // Acquire Read Lock. This won't prevent other queries being answered, but reloadConfig WLock will have to wait until we finish
gCfgMutex.RLock() gCfgMutex.RLock()
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