diff --git a/errors.go b/errors.go index e159164..3face41 100644 --- a/errors.go +++ b/errors.go @@ -27,7 +27,8 @@ type Error struct { InDelta uint64 `json:"in_delta,omitempty"` OutDelta uint64 `json:"out_delta,omitempty"` Utilization float64 `json:"utilization,omitempty"` - FirstSeen time.Time `json:"first_seen,omitempty"` + FirstSeen time.Time `json:"first_seen"` + LastSeen time.Time `json:"last_seen"` LastUpdated time.Time `json:"last_updated,omitempty"` } @@ -50,19 +51,22 @@ func (e *ErrorTracker) AddUnreachable(node *Node) { defer e.mu.Unlock() key := "unreachable:" + node.ID - if _, exists := e.errors[key]; exists { + now := time.Now().UTC() + + if existing, exists := e.errors[key]; exists { + existing.LastSeen = now + e.t.NotifyUpdate() return } - now := time.Now() e.nextID++ e.errors[key] = &Error{ - ID: fmt.Sprintf("err-%d", e.nextID), - NodeID: node.ID, - NodeName: node.DisplayName(), - Type: ErrorTypeUnreachable, - FirstSeen: now, - LastUpdated: now, + ID: fmt.Sprintf("err-%d", e.nextID), + NodeID: node.ID, + NodeName: node.DisplayName(), + Type: ErrorTypeUnreachable, + FirstSeen: now, + LastSeen: now, } e.t.NotifyUpdate() } @@ -83,13 +87,14 @@ func (e *ErrorTracker) AddPortError(node *Node, portName string, stats *Interfac defer e.mu.Unlock() key := node.ID + ":" + portName - now := time.Now() + now := time.Now().UTC() if existing, ok := e.errors[key]; ok { existing.InErrors = stats.InErrors existing.OutErrors = stats.OutErrors existing.InDelta += inDelta existing.OutDelta += outDelta + existing.LastSeen = now existing.LastUpdated = now } else { e.nextID++ @@ -104,6 +109,7 @@ func (e *ErrorTracker) AddPortError(node *Node, portName string, stats *Interfac InDelta: inDelta, OutDelta: outDelta, FirstSeen: now, + LastSeen: now, LastUpdated: now, } log.Printf("[ERROR] port errors on %s %s: in=%d out=%d", node.DisplayName(), portName, inDelta, outDelta) @@ -116,14 +122,15 @@ func (e *ErrorTracker) AddUtilizationError(node *Node, portName string, utilizat defer e.mu.Unlock() key := "util:" + node.ID + ":" + portName - now := time.Now() + now := time.Now().UTC() if existing, ok := e.errors[key]; ok { + existing.LastSeen = now if utilization > existing.Utilization { existing.Utilization = utilization existing.LastUpdated = now - e.t.NotifyUpdate() } + e.t.NotifyUpdate() return } @@ -136,20 +143,38 @@ func (e *ErrorTracker) AddUtilizationError(node *Node, portName string, utilizat Type: ErrorTypeHighUtilization, Utilization: utilization, FirstSeen: now, + LastSeen: now, LastUpdated: now, } log.Printf("[ERROR] high utilization on %s %s: %.0f%%", node.DisplayName(), portName, utilization) e.t.NotifyUpdate() } +func (e *ErrorTracker) UpdateUtilizationLastSeen(node *Node, portName string, utilization float64) { + e.mu.Lock() + defer e.mu.Unlock() + + key := "util:" + node.ID + ":" + portName + if existing, ok := e.errors[key]; ok { + now := time.Now().UTC() + existing.LastSeen = now + if utilization > existing.Utilization { + existing.Utilization = utilization + existing.LastUpdated = now + } + e.t.NotifyUpdate() + } +} + func (e *ErrorTracker) AddPortFlap(node *Node, portName string) { e.mu.Lock() defer e.mu.Unlock() key := "flap:" + node.ID + ":" + portName - now := time.Now() + now := time.Now().UTC() if existing, ok := e.errors[key]; ok { + existing.LastSeen = now existing.LastUpdated = now e.t.NotifyUpdate() return @@ -157,13 +182,13 @@ func (e *ErrorTracker) AddPortFlap(node *Node, portName string) { e.nextID++ e.errors[key] = &Error{ - ID: fmt.Sprintf("err-%d", e.nextID), - NodeID: node.ID, - NodeName: node.DisplayName(), - Port: portName, - Type: ErrorTypePortFlap, - FirstSeen: now, - LastUpdated: now, + ID: fmt.Sprintf("err-%d", e.nextID), + NodeID: node.ID, + NodeName: node.DisplayName(), + Port: portName, + Type: ErrorTypePortFlap, + FirstSeen: now, + LastSeen: now, } e.t.NotifyUpdate() } @@ -173,9 +198,10 @@ func (e *ErrorTracker) AddPortDown(node *Node, portName string) { defer e.mu.Unlock() key := "down:" + node.ID + ":" + portName - now := time.Now() + now := time.Now().UTC() if existing, ok := e.errors[key]; ok { + existing.LastSeen = now existing.LastUpdated = now e.t.NotifyUpdate() return @@ -183,13 +209,13 @@ func (e *ErrorTracker) AddPortDown(node *Node, portName string) { e.nextID++ e.errors[key] = &Error{ - ID: fmt.Sprintf("err-%d", e.nextID), - NodeID: node.ID, - NodeName: node.DisplayName(), - Port: portName, - Type: ErrorTypePortDown, - FirstSeen: now, - LastUpdated: now, + ID: fmt.Sprintf("err-%d", e.nextID), + NodeID: node.ID, + NodeName: node.DisplayName(), + Port: portName, + Type: ErrorTypePortDown, + FirstSeen: now, + LastSeen: now, } e.t.NotifyUpdate() } diff --git a/static/js/components.js b/static/js/components.js index aa48a47..43bda6d 100644 --- a/static/js/components.js +++ b/static/js/components.js @@ -54,7 +54,8 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn const errOut = switchConnection.errors?.out || 0; const r = switchConnection.rates; buildLinkStats(statsEl, portLabel, switchConnection.speed, errIn, errOut, - r ? {rxBytes: r.outBytes, rxPkts: r.outPkts, txBytes: r.inBytes, txPkts: r.inPkts} : null); + r ? {rxBytes: r.outBytes, rxPkts: r.outPkts, txBytes: r.inBytes, txPkts: r.inPkts} : null, + switchConnection.uptime, switchConnection.lastError); } else { const container = div.querySelector(':scope > .port-hover'); if (container) container.remove(); @@ -163,7 +164,8 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn const errOut = uplinkInfo.errors?.out || 0; const r = uplinkInfo.rates; buildLinkStats(statsEl, uplinkLabel, uplinkInfo.speed, errIn, errOut, - r ? {rxBytes: r.inBytes, rxPkts: r.inPkts, txBytes: r.outBytes, txPkts: r.outPkts} : null); + r ? {rxBytes: r.inBytes, rxPkts: r.inPkts, txBytes: r.outBytes, txPkts: r.outPkts} : null, + uplinkInfo.uptime, uplinkInfo.lastError); } else { const rootEl = div.querySelector(':scope > .root-label'); if (rootEl) rootEl.remove(); diff --git a/static/js/nodes.js b/static/js/nodes.js index 6ef9ee9..90d8ec6 100644 --- a/static/js/nodes.js +++ b/static/js/nodes.js @@ -99,3 +99,13 @@ export function getInterfaceRates(node, ifaceName) { outBytes: iface.stats.out_bytes_rate || 0 }; } + +export function getInterfaceUptime(node, ifaceName) { + const iface = findInterface(node, ifaceName); + return iface?.stats?.uptime || 0; +} + +export function getInterfaceLastError(node, ifaceName) { + const iface = findInterface(node, ifaceName); + return iface?.stats?.last_error || null; +} diff --git a/static/js/render.js b/static/js/render.js index d4a7f14..44c3f3d 100644 --- a/static/js/render.js +++ b/static/js/render.js @@ -1,4 +1,4 @@ -import { getLabel, getShortLabel, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates, getFirstName } from './nodes.js'; +import { getLabel, getShortLabel, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates, getInterfaceUptime, getInterfaceLastError, getFirstName } from './nodes.js'; import { buildSwitchUplinks, buildLocationTree, buildNodeIndex, findLocationForNode, findEffectiveSwitch } from './topology.js'; import { formatUniverse } from './format.js'; import { createNodeElement, renderLocation } from './components.js'; @@ -71,7 +71,11 @@ export function render(data, config) { errorsA: getInterfaceErrors(nodeA, link.interface_a), errorsB: getInterfaceErrors(nodeB, link.interface_b), ratesA: getInterfaceRates(nodeA, link.interface_a), - ratesB: getInterfaceRates(nodeB, link.interface_b) + ratesB: getInterfaceRates(nodeB, link.interface_b), + uptimeA: getInterfaceUptime(nodeA, link.interface_a), + uptimeB: getInterfaceUptime(nodeB, link.interface_b), + lastErrorA: getInterfaceLastError(nodeA, link.interface_a), + lastErrorB: getInterfaceLastError(nodeB, link.interface_b) }); } else if (aIsSwitch && !bIsSwitch) { const nodeLoc = nodeLocations.get(nodeB.id); @@ -84,7 +88,9 @@ export function render(data, config) { external: effectiveSwitch && !isLocalSwitch, speed: getInterfaceSpeed(nodeA, link.interface_a), errors: getInterfaceErrors(nodeA, link.interface_a), - rates: getInterfaceRates(nodeA, link.interface_a) + rates: getInterfaceRates(nodeA, link.interface_a), + uptime: getInterfaceUptime(nodeA, link.interface_a), + lastError: getInterfaceLastError(nodeA, link.interface_a) }); } else if (bIsSwitch && !aIsSwitch) { const nodeLoc = nodeLocations.get(nodeA.id); @@ -97,7 +103,9 @@ export function render(data, config) { external: effectiveSwitch && !isLocalSwitch, speed: getInterfaceSpeed(nodeB, link.interface_b), errors: getInterfaceErrors(nodeB, link.interface_b), - rates: getInterfaceRates(nodeB, link.interface_b) + rates: getInterfaceRates(nodeB, link.interface_b), + uptime: getInterfaceUptime(nodeB, link.interface_b), + lastError: getInterfaceLastError(nodeB, link.interface_b) }); } }); diff --git a/static/js/table.js b/static/js/table.js index 008c380..0d2360c 100644 --- a/static/js/table.js +++ b/static/js/table.js @@ -1,4 +1,4 @@ -import { getLabel, getFirstName, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates } from './nodes.js'; +import { getLabel, getFirstName, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates, getInterfaceUptime, getInterfaceLastError } from './nodes.js'; import { buildSwitchUplinks } from './topology.js'; import { escapeHtml, formatUniverse } from './format.js'; import { tableData, tableSortKeys, setTableSortKeys } from './state.js'; @@ -107,6 +107,8 @@ export function renderNetworkTable() { speed: getInterfaceSpeed(nodeA, link.interface_a), errors: getInterfaceErrors(nodeA, link.interface_a), rates: getInterfaceRates(nodeA, link.interface_a), + uptime: getInterfaceUptime(nodeA, link.interface_a), + lastError: getInterfaceLastError(nodeA, link.interface_a), isLocalPort: false }); } else if (bIsSwitch && !aIsSwitch) { @@ -116,6 +118,8 @@ export function renderNetworkTable() { speed: getInterfaceSpeed(nodeB, link.interface_b), errors: getInterfaceErrors(nodeB, link.interface_b), rates: getInterfaceRates(nodeB, link.interface_b), + uptime: getInterfaceUptime(nodeB, link.interface_b), + lastError: getInterfaceLastError(nodeB, link.interface_b), isLocalPort: false }); } else if (aIsSwitch && bIsSwitch) { @@ -129,7 +133,11 @@ export function renderNetworkTable() { errorsA: getInterfaceErrors(nodeA, link.interface_a), errorsB: getInterfaceErrors(nodeB, link.interface_b), ratesA: getInterfaceRates(nodeA, link.interface_a), - ratesB: getInterfaceRates(nodeB, link.interface_b) + ratesB: getInterfaceRates(nodeB, link.interface_b), + uptimeA: getInterfaceUptime(nodeA, link.interface_a), + uptimeB: getInterfaceUptime(nodeB, link.interface_b), + lastErrorA: getInterfaceLastError(nodeA, link.interface_a), + lastErrorB: getInterfaceLastError(nodeB, link.interface_b) }); } }); @@ -145,6 +153,8 @@ export function renderNetworkTable() { speed: uplink.speed, errors: uplink.errors, rates: uplink.rates, + uptime: uplink.uptime, + lastError: uplink.lastError, isLocalPort: true }); } @@ -165,6 +175,30 @@ export function renderNetworkTable() { return util.toFixed(0); }; + const formatUptime = (seconds) => { + if (!seconds || seconds <= 0) return ''; + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return d + 'd' + (h > 0 ? ' ' + h + 'h' : ''); + if (h > 0) return h + 'h' + (m > 0 ? ' ' + m + 'm' : ''); + return m + 'm'; + }; + + const formatTimeSince = (utcString) => { + if (!utcString) return ''; + const date = new Date(utcString); + const seconds = Math.floor((Date.now() - date.getTime()) / 1000); + if (seconds < 0) return ''; + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return d + 'd' + (h > 0 ? ' ' + h + 'h' : ''); + if (h > 0) return h + 'h' + (m > 0 ? ' ' + m + 'm' : ''); + if (m > 0) return m + 'm'; + return '<1m'; + }; + let rows = nodes.map(node => { const name = getLabel(node); const ips = []; @@ -178,6 +212,8 @@ export function renderNetworkTable() { const speed = isRoot ? null : (conn?.speed || 0); const errors = isRoot ? null : (conn?.errors || { in: 0, out: 0 }); const rates = isRoot ? null : (conn?.rates || { inBytes: 0, outBytes: 0 }); + const uptime = isRoot ? null : (conn?.uptime || 0); + const lastErrorTime = isRoot ? null : (conn?.lastError || null); const useLocalPerspective = isRoot || conn?.isLocalPort; const isUnreachable = node.unreachable; @@ -200,7 +236,11 @@ export function renderNetworkTable() { outUtil: outRateVal == null || !speed ? null : (outRateVal * 8 / speed) * 100, inPkts: rates == null ? null : (useLocalPerspective ? rates.inPkts : rates.outPkts), outPkts: rates == null ? null : (useLocalPerspective ? rates.outPkts : rates.inPkts), - status: isUnreachable ? 'unreachable' : (errors && (errors.in + errors.out) > 0 ? 'errors' : 'ok') + status: isUnreachable ? 'unreachable' : (errors && (errors.in + errors.out) > 0 ? 'errors' : 'ok'), + uptime, + uptimeStr: formatUptime(uptime), + lastErrorTime, + lastErrorStr: formatTimeSince(lastErrorTime) }; }); @@ -211,7 +251,7 @@ export function renderNetworkTable() { html += ''; html += 'In'; html += 'Out'; - html += ''; + html += ''; html += ''; html += 'Name'; html += 'IP'; @@ -225,6 +265,8 @@ export function renderNetworkTable() { html += '%'; html += 'Mb'; html += 'Kp'; + html += 'Uptime'; + html += 'Last Err'; html += 'Status'; html += ''; @@ -243,6 +285,8 @@ export function renderNetworkTable() { html += '' + (r.outRate == null ? '' : formatUtilLocal(r.outRate, r.speed)) + ''; html += '' + (r.outRate == null ? '' : formatMbpsLocal(r.outRate)) + ''; html += '' + (r.outPkts == null ? '' : formatKppsLocal(r.outPkts)) + ''; + html += '' + r.uptimeStr + ''; + html += '' + r.lastErrorStr + ''; html += '' + r.status + ''; html += ''; }); diff --git a/static/js/topology.js b/static/js/topology.js index e544148..a9e2fb4 100644 --- a/static/js/topology.js +++ b/static/js/topology.js @@ -15,7 +15,9 @@ export function buildSwitchUplinks(allSwitches, switchLinks) { remotePort: link.portB, localSpeed: link.speedA, localErrors: link.errorsA, - localRates: link.ratesA + localRates: link.ratesA, + localUptime: link.uptimeA, + localLastError: link.lastErrorA }); adjacency.get(link.switchB.id).push({ neighbor: link.switchA, @@ -23,7 +25,9 @@ export function buildSwitchUplinks(allSwitches, switchLinks) { remotePort: link.portA, localSpeed: link.speedB, localErrors: link.errorsB, - localRates: link.ratesB + localRates: link.ratesB, + localUptime: link.uptimeB, + localLastError: link.lastErrorB }); }); @@ -81,7 +85,9 @@ export function buildSwitchUplinks(allSwitches, switchLinks) { parentName: getLabel(current), speed: reverseEdge?.localSpeed || 0, errors: reverseEdge?.localErrors || null, - rates: reverseEdge?.localRates || null + rates: reverseEdge?.localRates || null, + uptime: reverseEdge?.localUptime || 0, + lastError: reverseEdge?.localLastError || null }); queue.push(edge.neighbor); } diff --git a/static/js/ui.js b/static/js/ui.js index 3ea1e15..9720e92 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -64,7 +64,31 @@ function formatUtilization(bytesPerSec, speed) { return util.toFixed(0) + '%'; } -export function buildLinkStats(container, portLabel, speed, errIn, errOut, rates) { +function formatUptime(seconds) { + if (!seconds || seconds <= 0) return '?'; + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return d + 'd' + (h > 0 ? ' ' + h + 'h' : ''); + if (h > 0) return h + 'h' + (m > 0 ? ' ' + m + 'm' : ''); + return m + 'm'; +} + +function formatTimeSince(utcString) { + if (!utcString) return ''; + const date = new Date(utcString); + const seconds = Math.floor((Date.now() - date.getTime()) / 1000); + if (seconds < 0) return ''; + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return d + 'd' + (h > 0 ? ' ' + h + 'h' : ''); + if (h > 0) return h + 'h' + (m > 0 ? ' ' + m + 'm' : ''); + if (m > 0) return m + 'm'; + return '<1m'; +} + +export function buildLinkStats(container, portLabel, speed, errIn, errOut, rates, uptime, lastError) { const plainLines = []; if (portLabel) { addClickableValue(container, 'PORT', portLabel, plainLines); @@ -81,6 +105,15 @@ export function buildLinkStats(container, portLabel, speed, errIn, errOut, rates container.appendChild(document.createTextNode('\n')); addClickableValue(container, 'TX', txUtil + ' ' + formatShortMbps(rates.txBytes) + ' ' + formatShortKpps(rates.txPkts), plainLines); } + if (uptime) { + container.appendChild(document.createTextNode('\n')); + addClickableValue(container, 'UP', formatUptime(uptime), plainLines); + } + if (lastError) { + const errorAge = formatTimeSince(lastError); + container.appendChild(document.createTextNode('\n')); + addClickableValue(container, 'LERR', errorAge + ' ago', plainLines); + } container.addEventListener('click', (e) => { e.stopPropagation(); navigator.clipboard.writeText(plainLines.join('\n')); @@ -160,6 +193,12 @@ export async function clearAllErrors() { await fetch('/tendrils/api/errors/clear?all=true', { method: 'POST' }); } +function formatLocalTime(utcString) { + if (!utcString) return ''; + const date = new Date(utcString); + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); +} + export function updateErrorPanel() { const panel = document.getElementById('error-panel'); const countEl = document.getElementById('error-count'); @@ -241,6 +280,11 @@ export function updateErrorPanel() { item.appendChild(typeEl); } + const timestampEl = document.createElement('div'); + timestampEl.className = 'error-timestamp'; + timestampEl.textContent = 'First: ' + formatLocalTime(err.first_seen) + ' / Last: ' + formatLocalTime(err.last_seen); + item.appendChild(timestampEl); + const dismissBtn = document.createElement('button'); dismissBtn.textContent = 'Dismiss'; dismissBtn.addEventListener('click', () => clearError(err.id)); diff --git a/static/style.css b/static/style.css index e0710f8..a67e662 100644 --- a/static/style.css +++ b/static/style.css @@ -1151,6 +1151,12 @@ body.sacn-mode .node:not(.sacn-out):not(.sacn-in):hover .node-info-wrapper { color: #888; } +.error-item .error-timestamp { + font-size: 10px; + color: #666; + margin-top: 2px; +} + .error-item button { align-self: flex-end; padding: 2px 6px; diff --git a/types.go b/types.go index dbf225e..7995691 100644 --- a/types.go +++ b/types.go @@ -386,15 +386,16 @@ func (i *Interface) MarshalJSON() ([]byte, error) { } type InterfaceStats struct { - Speed uint64 `json:"speed,omitempty"` - Uptime uint64 `json:"uptime,omitempty"` - InErrors uint64 `json:"in_errors,omitempty"` - OutErrors uint64 `json:"out_errors,omitempty"` - InPktsRate float64 `json:"in_pkts_rate,omitempty"` - OutPktsRate float64 `json:"out_pkts_rate,omitempty"` - InBytesRate float64 `json:"in_bytes_rate,omitempty"` - OutBytesRate float64 `json:"out_bytes_rate,omitempty"` - PoE *PoEStats `json:"poe,omitempty"` + Speed uint64 `json:"speed,omitempty"` + Uptime uint64 `json:"uptime,omitempty"` + InErrors uint64 `json:"in_errors,omitempty"` + OutErrors uint64 `json:"out_errors,omitempty"` + InPktsRate float64 `json:"in_pkts_rate,omitempty"` + OutPktsRate float64 `json:"out_pkts_rate,omitempty"` + InBytesRate float64 `json:"in_bytes_rate,omitempty"` + OutBytesRate float64 `json:"out_bytes_rate,omitempty"` + LastError *time.Time `json:"last_error,omitempty"` + PoE *PoEStats `json:"poe,omitempty"` } func round2(v float64) float64 { @@ -403,15 +404,16 @@ func round2(v float64) float64 { func (s *InterfaceStats) MarshalJSON() ([]byte, error) { type statsJSON struct { - Speed uint64 `json:"speed,omitempty"` - Uptime uint64 `json:"uptime,omitempty"` - InErrors uint64 `json:"in_errors,omitempty"` - OutErrors uint64 `json:"out_errors,omitempty"` - InPktsRate float64 `json:"in_pkts_rate,omitempty"` - OutPktsRate float64 `json:"out_pkts_rate,omitempty"` - InBytesRate float64 `json:"in_bytes_rate,omitempty"` - OutBytesRate float64 `json:"out_bytes_rate,omitempty"` - PoE *PoEStats `json:"poe,omitempty"` + Speed uint64 `json:"speed,omitempty"` + Uptime uint64 `json:"uptime,omitempty"` + InErrors uint64 `json:"in_errors,omitempty"` + OutErrors uint64 `json:"out_errors,omitempty"` + InPktsRate float64 `json:"in_pkts_rate,omitempty"` + OutPktsRate float64 `json:"out_pkts_rate,omitempty"` + InBytesRate float64 `json:"in_bytes_rate,omitempty"` + OutBytesRate float64 `json:"out_bytes_rate,omitempty"` + LastError *time.Time `json:"last_error,omitempty"` + PoE *PoEStats `json:"poe,omitempty"` } return json.Marshal(statsJSON{ Speed: s.Speed, @@ -422,6 +424,7 @@ func (s *InterfaceStats) MarshalJSON() ([]byte, error) { OutPktsRate: round2(s.OutPktsRate), InBytesRate: round2(s.InBytesRate), OutBytesRate: round2(s.OutBytesRate), + LastError: s.LastError, PoE: s.PoE, }) } @@ -503,6 +506,10 @@ func (n *Node) SetInterfaceStats(portName string, stats *InterfaceStats) { return } + if oldStats != nil { + stats.LastError = oldStats.LastError + } + var inDelta, outDelta uint64 if oldStats != nil { if stats.InErrors > oldStats.InErrors { @@ -512,7 +519,15 @@ func (n *Node) SetInterfaceStats(portName string, stats *InterfaceStats) { outDelta = stats.OutErrors - oldStats.OutErrors } } - if inDelta > 0 || outDelta > 0 { + + hasErrors := stats.InErrors > 0 || stats.OutErrors > 0 + hasNewErrors := inDelta > 0 || outDelta > 0 + if hasErrors && (hasNewErrors || stats.LastError == nil) { + now := time.Now().UTC() + stats.LastError = &now + } + + if hasNewErrors { n.errors.AddPortError(n, portName, stats, inDelta, outDelta) } @@ -535,6 +550,8 @@ func (n *Node) SetInterfaceStats(portName string, stats *InterfaceStats) { if oldUtilization < 70.0 && utilization >= 70.0 { n.errors.AddUtilizationError(n, portName, utilization) + } else if utilization >= 70.0 { + n.errors.UpdateUtilizationLastSeen(n, portName, utilization) } } }