Files
nginx-ui/internal/analytic/node.go

172 lines
3.7 KiB
Go

package analytic
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"sync"
"time"
"github.com/0xJacky/Nginx-UI/internal/nodeauth"
"github.com/0xJacky/Nginx-UI/internal/upstream"
"github.com/0xJacky/Nginx-UI/internal/version"
"github.com/0xJacky/Nginx-UI/model"
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/net"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
)
type NodeInfo struct {
NodeRuntimeInfo version.RuntimeInfo `json:"node_runtime_info"`
Version string `json:"version"`
CPUNum int `json:"cpu_num"`
MemoryTotal string `json:"memory_total"`
DiskTotal string `json:"disk_total"`
}
type NodeStat struct {
AvgLoad *load.AvgStat `json:"avg_load"`
CPUPercent float64 `json:"cpu_percent"`
MemoryPercent float64 `json:"memory_percent"`
DiskPercent float64 `json:"disk_percent"`
Network net.IOCountersStat `json:"network"`
Status bool `json:"status"`
ResponseAt time.Time `json:"response_at"`
UpstreamStatusMap map[string]*upstream.Status `json:"upstream_status_map"`
}
type NodeConnectionErrorCode string
const NodeConnectionErrorClockSkew NodeConnectionErrorCode = "clock_skew"
type Node struct {
*model.Node
NodeStat
NodeInfo
ConnectionError string `json:"connection_error,omitempty"`
ConnectionErrorCode NodeConnectionErrorCode `json:"connection_error_code,omitempty"`
ConnectionErrorAt *time.Time `json:"connection_error_at,omitempty"`
}
var nodeMapMu sync.RWMutex
type TNodeMap map[uint64]*Node
var NodeMap TNodeMap
func init() {
NodeMap = make(TNodeMap)
}
func cloneNode(n *Node) *Node {
if n == nil {
return nil
}
cloned := *n
if n.Node != nil {
nodeCopy := *n.Node
cloned.Node = &nodeCopy
}
if n.UpstreamStatusMap != nil {
upstreams := make(map[string]*upstream.Status, len(n.UpstreamStatusMap))
for key, status := range n.UpstreamStatusMap {
if status == nil {
upstreams[key] = nil
continue
}
statusCopy := *status
upstreams[key] = &statusCopy
}
cloned.UpstreamStatusMap = upstreams
}
return &cloned
}
func SnapshotNodeMap() TNodeMap {
nodeMapMu.RLock()
defer nodeMapMu.RUnlock()
snapshot := make(TNodeMap, len(NodeMap))
for id, node := range NodeMap {
snapshot[id] = cloneNode(node)
}
return snapshot
}
func GetNode(node *model.Node) (n *Node) {
if node == nil {
// this should never happen
logger.Error("node is nil")
return
}
if !node.Enabled {
return &Node{
Node: node,
}
}
nodeMapMu.RLock()
cached, ok := NodeMap[node.ID]
nodeMapMu.RUnlock()
if !ok || cached == nil {
return &Node{
Node: node,
}
}
cloned := cloneNode(cached)
if cloned == nil {
return &Node{
Node: node,
}
}
cloned.Node = node
return cloned
}
func InitNode(ctx context.Context, node *model.Node) (n *Node, err error) {
n = &Node{
Node: node,
}
u, err := url.JoinPath(node.URL, "/api/node")
if err != nil {
return
}
client, err := nodeauth.NewHTTPClient(node, 10*time.Second)
if err != nil {
return
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return
}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4<<10))
return n, cosy.WrapErrorWithParams(ErrNodeAnalyticsFailed, resp.Status)
}
err = json.NewDecoder(resp.Body).Decode(&n.NodeInfo)
if err != nil {
return
}
return
}