v.0.2.1
This commit is contained in:
parent
f72ec3bb96
commit
4fd4c68219
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
nodegopher
|
||||||
|
config.yaml*
|
||||||
|
!config.yaml.sample
|
||||||
|
main.go.*
|
103
config.yaml.sample
Normal file
103
config.yaml.sample
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# 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.
|
||||||
|
# - address: the address of prometheus.
|
||||||
|
# - query: prometheus query. Same as typed in prometheus graph page.
|
||||||
|
# - type: type of query. "query" will get instant value, "query_range" will get all samples for the grafana period. Result will be averaged.
|
||||||
|
# - timeout: query timeout in seconds.
|
||||||
|
datasources:
|
||||||
|
- name: prom_samples_per_sec
|
||||||
|
type: query
|
||||||
|
address: 'http://prometheus.local.lan:9090'
|
||||||
|
query: 'rate(prometheus_tsdb_head_samples_appended_total{type="float"}[10m])'
|
||||||
|
timeout: 10
|
||||||
|
- name: node_cpu_metric
|
||||||
|
# Simple query, return an instant metric
|
||||||
|
type: query
|
||||||
|
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: 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
|
||||||
|
address: 'http://prometheus.local.lan:9090'
|
||||||
|
query: 'rate(node_network_receive_bytes_total{device="igb0", instance="router01.local.lan:9100", job="node"}[30s])'
|
||||||
|
timeout: 10
|
||||||
|
- name: router01_net_up_rate
|
||||||
|
type: query_range
|
||||||
|
address: 'http://prometheus.local.lan:9090'
|
||||||
|
query: 'rate(node_network_transmit_bytes_total{device="igb0", instance="router01.local.lan:9100", job="node"}[30s])'
|
||||||
|
timeout: 10
|
||||||
|
- name: router01_lan_down_rate
|
||||||
|
type: query_range
|
||||||
|
address: 'http://prometheus.local.lan:9090'
|
||||||
|
query: 'rate(node_network_receive_bytes_total{device="ix3", instance="router01.local.lan:9100", job="node"}[30s])'
|
||||||
|
timeout: 10
|
||||||
|
- name: router01_lan_up_rate
|
||||||
|
type: query_range
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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 :
|
||||||
|
# Edges: http://nodegopher.local.lan:8080/internet/edges?from=$__from&to=$__to&interval=$__interval
|
||||||
|
# Nodes: http://nodegopher.local.lan:8080/internet/nodes?from=$__from&to=$__to&interval=$__interval
|
||||||
|
# Properties :
|
||||||
|
# - name: name of the context
|
||||||
|
# - nodes: list of nodegraph nodes
|
||||||
|
# - edges: list of nodegraph edges
|
||||||
|
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: router01
|
||||||
|
id: router01
|
||||||
|
title: router01
|
||||||
|
subtitle: "A router"
|
||||||
|
color: "blue"
|
||||||
|
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"
|
||||||
|
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'
|
||||||
|
|
85
edges.go
Normal file
85
edges.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Edge implement Item interface
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: handle arc__ and details__ fields
|
||||||
|
type Edge struct {
|
||||||
|
Id string `yaml:"id" json:"id"`
|
||||||
|
Source string `yaml:"source" json:"source"`
|
||||||
|
Target string `yaml:"target" json:"target"`
|
||||||
|
MainStat string `yaml:"mainstat,omitempty" json:"mainstat,omitempty"`
|
||||||
|
MainStatQuery string `yaml:"mainstatquery,omitempty" json:"mainstatquery,omitempty"`
|
||||||
|
MainStatFormat string `yaml:"mainstatformat,omitempty" json:"mainstatformat,omitempty"`
|
||||||
|
SecondaryStat string `yaml:"secondarystat,omitempty" json:"secondarystat,omitempty"`
|
||||||
|
SecondaryStatQuery string `yaml:"secondarystatquery,omitempty" json:"secondarystatquery,omitempty"`
|
||||||
|
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"`
|
||||||
|
StrokeDashArray float32 `yaml:"strokeDasharray,omitempty" json:"strokeDasharray,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) GetId() string {
|
||||||
|
return e.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) GetMainStat() string {
|
||||||
|
return e.MainStat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) GetSecondaryStat() string {
|
||||||
|
return e.SecondaryStat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) GetMainStatQuery() string {
|
||||||
|
return e.MainStatQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) GetSecondaryStatQuery() string {
|
||||||
|
return e.SecondaryStatQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) GetMainStatFormat() string {
|
||||||
|
return e.MainStatFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) GetSecondaryStatFormat() string {
|
||||||
|
return e.SecondaryStatFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Edge) SetMainStat(stat string) {
|
||||||
|
e.MainStat = fmt.Sprintf("%s", stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Edge) SetSecondaryStat(stat string) {
|
||||||
|
e.SecondaryStat = fmt.Sprintf("%s", stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom marshaler to not send (main|secondary)statquery
|
||||||
|
func (e Edge) MarshalJSON() ([]byte, error) {
|
||||||
|
jsonRes := `{"id":"` + e.Id + `","source":"` + e.Source + `","target":"` + e.Target + `"`
|
||||||
|
if len(e.MainStat) > 0 {
|
||||||
|
jsonRes += `,"mainstat":"` + e.MainStat + `"`
|
||||||
|
}
|
||||||
|
if len(e.SecondaryStat) > 0 {
|
||||||
|
jsonRes += `,"secondarystat":"` + e.SecondaryStat + `"`
|
||||||
|
}
|
||||||
|
if len(e.Color) > 0 {
|
||||||
|
jsonRes += `,"color":"` + e.Color + `"`
|
||||||
|
}
|
||||||
|
if e.Thickness > 0 {
|
||||||
|
jsonRes += `,"thickness":` + strconv.Itoa(e.Thickness)
|
||||||
|
}
|
||||||
|
if e.HighLighted {
|
||||||
|
jsonRes += `,"highlighted":true`
|
||||||
|
}
|
||||||
|
// TODO : e.StrokeDashArray
|
||||||
|
jsonRes += `}`
|
||||||
|
|
||||||
|
return []byte(jsonRes), nil
|
||||||
|
}
|
57
go.mod
Normal file
57
go.mod
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
module nodegopher
|
||||||
|
|
||||||
|
go 1.23.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/prometheus/client_golang v1.20.5
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/spf13/viper v1.19.0
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.24.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
|
golang.org/x/net v0.26.0 // indirect
|
||||||
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
golang.org/x/text v0.16.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
132
go.sum
Normal file
132
go.sum
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
|
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||||
|
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||||
|
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||||
|
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
|
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
|
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||||
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
505
main.go
Normal file
505
main.go
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
// nodegopher is a Grafana/Prometheus nodeGraph helper
|
||||||
|
// It builds nodegraph structure by merging static data and metrics pulled from prometheus instance
|
||||||
|
// Copyright (c) 2025 yo000 <johan@nosd.in>
|
||||||
|
//
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"fmt"
|
||||||
|
"flag"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"regexp"
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"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"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PromDataSourceConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
QType string `yaml:"type"`
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
Query string `yaml:"query"`
|
||||||
|
Timeout int `yaml:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Graph struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Nodes []Item
|
||||||
|
Edges []Item
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item interface {
|
||||||
|
GetId() string
|
||||||
|
GetMainStat() string
|
||||||
|
GetMainStatQuery() string
|
||||||
|
GetMainStatFormat() string
|
||||||
|
SetMainStat(string)
|
||||||
|
GetSecondaryStat() string
|
||||||
|
GetSecondaryStatQuery() string
|
||||||
|
GetSecondaryStatFormat() string
|
||||||
|
SetSecondaryStat(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query arguments. Based on grafana internal variables.
|
||||||
|
type QueryArgs struct {
|
||||||
|
From int64 `form:"from" binding:"required"`
|
||||||
|
To int64 `form:"to" binding:"required"`
|
||||||
|
Interval string `form:"interval" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query arguments converted to prometheus_client format
|
||||||
|
type MyRange struct {
|
||||||
|
Start time.Time
|
||||||
|
End time.Time
|
||||||
|
Step time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
gDebug bool
|
||||||
|
gGraphs []Graph
|
||||||
|
gDataSources []PromDataSourceConfig
|
||||||
|
|
||||||
|
gDSVarCompRegex *regexp.Regexp
|
||||||
|
gGrafanaIntervalUnitRegex *regexp.Regexp
|
||||||
|
|
||||||
|
// manipulating In-Ram configuration Mutex. reloadConfig will lock WriteMutex, consuming queries will lock ReadMutex.
|
||||||
|
gCfgMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func (d *PromDataSourceConfig) GetData(timeRange *MyRange) (float64, error) {
|
||||||
|
client, err := api.NewClient(api.Config{
|
||||||
|
Address: d.Address,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("DataSourceConfig.GetData: Error creating client: %v\n", err)
|
||||||
|
return 0.0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v1api := v1.NewAPI(client)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(d.Timeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
var result model.Value
|
||||||
|
var warnings v1.Warnings
|
||||||
|
if d.QType == "query" {
|
||||||
|
result, warnings, err = v1api.Query(ctx, d.Query, time.Now(), v1.WithTimeout(time.Duration(d.Timeout)*time.Second))
|
||||||
|
} else if d.QType == "query_range" {
|
||||||
|
rng := v1.Range{
|
||||||
|
Start: timeRange.Start,
|
||||||
|
End: timeRange.End,
|
||||||
|
Step: timeRange.Step,
|
||||||
|
}
|
||||||
|
result, warnings, err = v1api.QueryRange(ctx, d.Query, rng, v1.WithTimeout(time.Duration(d.Timeout)*time.Second))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("DataSourceConfig.GetData: Error querying Prometheus: %v\n", err)
|
||||||
|
return 0.0, err
|
||||||
|
}
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
log.Warningf("DataSourceConfig.GetData: Warnings: %v\n", warnings)
|
||||||
|
}
|
||||||
|
log.Debugf("DataSourceConfig.GetData: Result: %v\n", result)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case result.Type() == model.ValScalar:
|
||||||
|
log.Errorf("This is a model.ValScalar type, not implemented!\n")
|
||||||
|
//scalarVal := result.(*model.Scalar)
|
||||||
|
// handle scalar stuff
|
||||||
|
case result.Type() == model.ValVector:
|
||||||
|
vectorVal := result.(model.Vector)
|
||||||
|
// FIXME: Is averaging the right thing to do?
|
||||||
|
var total float64
|
||||||
|
for _, elem := range vectorVal {
|
||||||
|
log.Debugf("DataSourceConfig.GetData: Value: %v\n", elem.Value)
|
||||||
|
total += float64(elem.Value)
|
||||||
|
}
|
||||||
|
log.Debugf("DataSourceConfig.GetData: Total average: %f\n", total/float64(len(vectorVal)))
|
||||||
|
return total/float64(len(vectorVal)), nil
|
||||||
|
// QueryRange return this type
|
||||||
|
case result.Type() == model.ValMatrix:
|
||||||
|
matrixVal := result.(model.Matrix)
|
||||||
|
// FIXME: Is averaging the right thing to do?
|
||||||
|
var total float64
|
||||||
|
var length int64
|
||||||
|
for _, val := range matrixVal {
|
||||||
|
for _, v := range val.Values {
|
||||||
|
total += float64(v.Value)
|
||||||
|
}
|
||||||
|
length += int64(len(val.Values))
|
||||||
|
}
|
||||||
|
log.Debugf("DataSourceConfig.GetData: Total average: %f\n", total/float64(length))
|
||||||
|
return total/float64(length), nil
|
||||||
|
default:
|
||||||
|
log.Errorf("Prometheus result is an unknown type: %T", result)
|
||||||
|
return 0.0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g Graph) BuildMetrics(items *[]Item, timeRange *MyRange) (*[]Item, error) {
|
||||||
|
var err 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 {
|
||||||
|
if len(item.GetMainStatQuery()) > 0 {
|
||||||
|
log.Debugf("Item %s have mainstatquery: %s\n", item.GetId(), item.GetMainStatQuery())
|
||||||
|
r := gDSVarCompRegex.FindStringSubmatch(item.GetMainStatQuery())
|
||||||
|
if len(r) > 1 {
|
||||||
|
var value float64
|
||||||
|
dsname := strings.TrimSpace(r[1])
|
||||||
|
log.Debugf("buildMetrics: datasource from mainstatquery : %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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format := item.GetMainStatFormat()
|
||||||
|
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))
|
||||||
|
} else {
|
||||||
|
log.Errorf("buildMetrics: Item %s mainstatquery unparseable: %s\n", item.GetId(), item.GetMainStatQuery())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(item.GetSecondaryStatQuery()) > 0 {
|
||||||
|
log.Debugf("Item %s have mainstatquery: %s\n", item.GetId(), item.GetSecondaryStatQuery())
|
||||||
|
r := gDSVarCompRegex.FindStringSubmatch(item.GetSecondaryStatQuery())
|
||||||
|
if len(r) > 1 {
|
||||||
|
var value float64
|
||||||
|
dsname := strings.TrimSpace(r[1])
|
||||||
|
log.Debugf("buildMetrics: datasource from secondarystatquery : %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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format := item.GetSecondaryStatFormat()
|
||||||
|
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))
|
||||||
|
} else {
|
||||||
|
log.Errorf("buildMetrics: Item %s secondarystatquery unparseable: %s\n", item.GetId(), item.GetSecondaryStatQuery())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryArgsToTimeRange(args QueryArgs) (MyRange, error) {
|
||||||
|
// interval sent by Grafana could end with : ms, s, m, h, d
|
||||||
|
var step time.Duration
|
||||||
|
matches := gGrafanaIntervalUnitRegex.FindStringSubmatch(args.Interval)
|
||||||
|
if len(matches) != 3 {
|
||||||
|
return MyRange{}, fmt.Errorf("Invalid format: interval")
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseInt(matches[gGrafanaIntervalUnitRegex.SubexpIndex("value")], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return MyRange{}, fmt.Errorf("Invalid format (can not parse value): interval")
|
||||||
|
}
|
||||||
|
switch matches[gGrafanaIntervalUnitRegex.SubexpIndex("unit")] {
|
||||||
|
case "ms":
|
||||||
|
step = time.Duration(value)*time.Millisecond
|
||||||
|
case "s":
|
||||||
|
step = time.Duration(value)*time.Second
|
||||||
|
case "m":
|
||||||
|
step = time.Duration(value)*time.Minute
|
||||||
|
case "h":
|
||||||
|
step = time.Duration(value)*time.Hour
|
||||||
|
case "d":
|
||||||
|
step = time.Duration(value)*time.Hour*24
|
||||||
|
default:
|
||||||
|
return MyRange{}, fmt.Errorf("Invalid format (can not parse unit): interval")
|
||||||
|
}
|
||||||
|
|
||||||
|
return MyRange{Start: time.Unix(args.From/1000, 0), End: time.Unix(args.To/1000, 0), Step: step}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGraph(name string) (Graph, 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 _, g := range gGraphs {
|
||||||
|
if strings.EqualFold(g.Name, name) {
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Graph{}, fmt.Errorf("Graph not found: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initRoutes(r *gin.Engine) {
|
||||||
|
r.GET("/ping", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "pong",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// An endpoint to force read of configuration file
|
||||||
|
r.POST("/reload", func(c *gin.Context) {
|
||||||
|
reloadConfigFile()
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "configuration successfully reloaded",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// An endpoint to toggle debug mode
|
||||||
|
r.POST("/debug", func(c *gin.Context) {
|
||||||
|
toggleDebug()
|
||||||
|
if gDebug {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "debug mode enabled",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "debug mode disabled",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/:graph/nodes", func(c *gin.Context) {
|
||||||
|
// Validate presence and type
|
||||||
|
var args QueryArgs
|
||||||
|
if err := c.ShouldBindQuery(&args); err != nil {
|
||||||
|
log.Errorf("Invalid query arguments\n")
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timeRange, err := queryArgsToTimeRange(args)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to parse query arguments\n")
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gName := c.Param("graph")
|
||||||
|
graph, err := getGraph(gName)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := graph.BuildMetrics(&graph.Nodes, &timeRange)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, nodes)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/:graph/edges", func(c *gin.Context) {
|
||||||
|
// Validate presence and type
|
||||||
|
var args QueryArgs
|
||||||
|
if err := c.ShouldBindQuery(&args); err != nil {
|
||||||
|
log.Errorf("Invalid query arguments\n")
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timeRange, err := queryArgsToTimeRange(args)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to parse query arguments\n")
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gName := c.Param("graph")
|
||||||
|
graph, err := getGraph(gName)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
edges, err := graph.BuildMetrics(&graph.Edges, &timeRange)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, edges)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleDebug() {
|
||||||
|
if gDebug {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
log.SetLevel(log.InfoLevel)
|
||||||
|
gDebug = false
|
||||||
|
} else {
|
||||||
|
gin.SetMode(gin.DebugMode)
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
gDebug = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadConfigFile() {
|
||||||
|
// First reread config file
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
|
log.Fatalf("config file not found")
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
log.Fatalf("unknown error looking for config file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// then clear current config, after acquiring WriteLock
|
||||||
|
gCfgMutex.Lock()
|
||||||
|
defer gCfgMutex.Unlock()
|
||||||
|
|
||||||
|
for _, g := range gGraphs {
|
||||||
|
g.Nodes = nil
|
||||||
|
g.Edges = nil
|
||||||
|
}
|
||||||
|
gGraphs = nil
|
||||||
|
gDataSources = nil
|
||||||
|
|
||||||
|
// Finally unmarshal graphs, nodes, edges and datasources
|
||||||
|
gps := viper.Get("graphs").([]interface{})
|
||||||
|
for _, g := range gps {
|
||||||
|
yd, _ := yaml.Marshal(g)
|
||||||
|
// Unmarshal on anonymous structs with []Edge and []Node well defined so unmarshaler know how to handle
|
||||||
|
// then we convert to []Items
|
||||||
|
tmp := struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Nodes []Node `yaml:"nodes"`
|
||||||
|
Edges []Edge `yaml:"edges"`
|
||||||
|
}{}
|
||||||
|
yaml.Unmarshal(yd, &tmp)
|
||||||
|
var graphNodes []Item
|
||||||
|
var graphEdges []Item
|
||||||
|
for _, n := range tmp.Nodes {
|
||||||
|
graphNodes = append(graphNodes, &n)
|
||||||
|
}
|
||||||
|
for _, e := range tmp.Edges {
|
||||||
|
graphEdges = append(graphEdges, &e)
|
||||||
|
}
|
||||||
|
|
||||||
|
graph := Graph{
|
||||||
|
Name: tmp.Name,
|
||||||
|
Nodes: graphNodes,
|
||||||
|
Edges: graphEdges,
|
||||||
|
}
|
||||||
|
gGraphs = append(gGraphs, graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.Get("datasources") == nil {
|
||||||
|
log.Printf("no datasources found, data will be static")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dss := viper.Get("datasources").([]interface{})
|
||||||
|
for _, d := range dss {
|
||||||
|
yd, _ := yaml.Marshal(d)
|
||||||
|
var ds PromDataSourceConfig
|
||||||
|
yaml.Unmarshal(yd, &ds)
|
||||||
|
gDataSources = append(gDataSources, ds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var listen string
|
||||||
|
var confFile string
|
||||||
|
|
||||||
|
flag.StringVar(&confFile, "config", "", "Path to the config file")
|
||||||
|
flag.StringVar(&listen, "listen-addr", "0.0.0.0:8080", "listen address for server")
|
||||||
|
flag.BoolVar(&gDebug, "debug", false, "Set log level to debug")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if len(confFile) == 0 {
|
||||||
|
log.Fatalf("config is mandatory")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigFile(confFile)
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
|
log.Fatalf("config file not found")
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
log.Fatalf("unknown error looking for config file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if false == gDebug {
|
||||||
|
b := viper.GetBool("debug")
|
||||||
|
if b {
|
||||||
|
gDebug = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gDebug {
|
||||||
|
gin.SetMode(gin.DebugMode)
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
} else {
|
||||||
|
log.SetLevel(log.InfoLevel)
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(listen, "0.0.0.0:8080") && len(confFile) > 0 {
|
||||||
|
l := viper.GetString("listen")
|
||||||
|
if len(l) > 0 {
|
||||||
|
listen = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Watch config changes. Does not work on FreeBSD. TODO: Test with linux
|
||||||
|
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||||
|
log.Printf("Config file changed, reloading data\n")
|
||||||
|
reloadConfigFile()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Lets reload config on SIGHUP
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGHUP)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
_ = <- sigs
|
||||||
|
log.Infof("SIGHUP received, reloading configuration\n")
|
||||||
|
reloadConfigFile()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
reloadConfigFile()
|
||||||
|
|
||||||
|
// Capture variable name. There should be only one variable. Space is tolerated before and after name.
|
||||||
|
gDSVarCompRegex = regexp.MustCompile(`^\{\{(?:\ )?([a-zA-Z0-9\-_]+)(?:\ )?\}\}$`)
|
||||||
|
|
||||||
|
// Grafana interval parser
|
||||||
|
gGrafanaIntervalUnitRegex = regexp.MustCompile(`^(?P<value>[0-9]+)(?P<unit>[mshd]+)$`)
|
||||||
|
|
||||||
|
log.Printf("Starting NodeGopher v.%s\n", gVersion)
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
initRoutes(r)
|
||||||
|
r.Run(listen)
|
||||||
|
}
|
96
nodes.go
Normal file
96
nodes.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Node implement Item interface
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: handle arc__ and details__ fields
|
||||||
|
type Node struct {
|
||||||
|
Name string `yaml:"name" json:"name"`
|
||||||
|
Id string `yaml:"id" json:"id"`
|
||||||
|
Title string `yaml:"title" json:"title",omitempty`
|
||||||
|
Subtitle string `yaml:"subtitle,omitempty" json:"subtitle,omitempty"`
|
||||||
|
MainStat string `yaml:"mainstat,omitempty" json:"mainstat,omitempty"`
|
||||||
|
MainStatQuery string `yaml:"mainstatquery,omitempty"`
|
||||||
|
MainStatFormat string `yaml:"mainstatformat,omitempty" json:"mainstatformat,omitempty"`
|
||||||
|
SecondaryStat string `yaml:"secondarystat,omitempty" json:"secondarystat,omitempty"`
|
||||||
|
SecondaryStatQuery string `yaml:"secondarystatquery,omitempty" json:"secondarystatquery,omitempty"`
|
||||||
|
SecondaryStatFormat string `yaml:"secondarystatformat,omitempty" json:"secondarystatformat,omitempty"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Node) GetId() string {
|
||||||
|
return n.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Node) GetMainStat() string {
|
||||||
|
return n.MainStat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Node) GetSecondaryStat() string {
|
||||||
|
return n.SecondaryStat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Node) GetMainStatQuery() string {
|
||||||
|
return n.MainStatQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Node) GetSecondaryStatQuery() string {
|
||||||
|
return n.SecondaryStatQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Node) GetMainStatFormat() string {
|
||||||
|
return n.MainStatFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Node) GetSecondaryStatFormat() string {
|
||||||
|
return n.SecondaryStatFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) SetMainStat(stat string) {
|
||||||
|
n.MainStat = fmt.Sprintf("%s", stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) SetSecondaryStat(stat string) {
|
||||||
|
n.SecondaryStat = fmt.Sprintf("%s", stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom marshaler to not send (main|secondary)statquery
|
||||||
|
func (n Node) MarshalJSON() ([]byte, error) {
|
||||||
|
jsonRes := `{"name":"` + n.Name + `","id":"` + n.Id + `"`
|
||||||
|
if len(n.Title) > 0 {
|
||||||
|
jsonRes += `,"title":"` + n.Title + `"`
|
||||||
|
}
|
||||||
|
if len(n.Subtitle) > 0 {
|
||||||
|
jsonRes += `,"subtitle":"` + n.Subtitle + `"`
|
||||||
|
}
|
||||||
|
if len(n.MainStat) > 0 {
|
||||||
|
jsonRes += `,"mainstat":"` + n.MainStat + `"`
|
||||||
|
}
|
||||||
|
if len(n.SecondaryStat) > 0 {
|
||||||
|
jsonRes += `,"secondarystat":"` + n.SecondaryStat + `"`
|
||||||
|
}
|
||||||
|
if len(n.Color) > 0 {
|
||||||
|
jsonRes += `,"color":"` + n.Color + `"`
|
||||||
|
}
|
||||||
|
if len(n.Icon) > 0 {
|
||||||
|
jsonRes += `,"icon":"` + n.Icon + `"`
|
||||||
|
}
|
||||||
|
if n.NodeRadius > 0 {
|
||||||
|
jsonRes += `,"noderadius":` + strconv.Itoa(n.NodeRadius)
|
||||||
|
}
|
||||||
|
if n.HighLighted {
|
||||||
|
jsonRes += `,"highlighted":true`
|
||||||
|
}
|
||||||
|
// TODO : n.StrokeDashArray
|
||||||
|
jsonRes += `}`
|
||||||
|
|
||||||
|
return []byte(jsonRes), nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user