v.0.2.1
This commit is contained in:
		
							
								
								
									
										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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Reference in New Issue
	
	Block a user