diff --git a/internal/web/job/node_traffic_sync_job.go b/internal/web/job/node_traffic_sync_job.go index 87d283954..e9812dbf4 100644 --- a/internal/web/job/node_traffic_sync_job.go +++ b/internal/web/job/node_traffic_sync_job.go @@ -34,6 +34,9 @@ type NodeTrafficSyncJob struct { lastIpSync int64 globalPushMu sync.Mutex lastGlobalPush int64 + // noGuidIpEndpoint tracks nodes (by id) whose client-IP attribution endpoint + // returned 404, so an old-build node is noted once instead of every cycle. + noGuidIpEndpoint sync.Map } type atomicBool struct { @@ -303,12 +306,22 @@ func (j *NodeTrafficSyncJob) syncOne(mgr *runtime.Manager, n *model.Node, doIpSy // Per-node IP attribution: pull the node's guid-keyed subtree (its own // observations plus any descendants) so the master can tell which node each - // IP is on. Old nodes without the endpoint just return an error — skip them. + // IP is on. Old nodes without the endpoint return HTTP 404 every cycle — note + // it once per node (re-armed on recovery) instead of flooding the log. if guidTrees, err := rt.FetchClientIpsByGuid(ipCtx); err != nil { - logger.Debugf("node traffic sync: fetch client ip attribution from %s failed: %v", n.Name, err) - } else if len(guidTrees) > 0 { - if err := j.inboundService.MergeClientIpsByGuid(n, guidTrees); err != nil { - logger.Warningf("node traffic sync: merge client ip attribution from %s failed: %v", n.Name, err) + if strings.Contains(err.Error(), "HTTP 404") { + if _, seen := j.noGuidIpEndpoint.LoadOrStore(n.Id, true); !seen { + logger.Debugf("node traffic sync: node %s has no client-IP attribution endpoint (old build)", n.Name) + } + } else { + logger.Debugf("node traffic sync: fetch client ip attribution from %s failed: %v", n.Name, err) + } + } else { + j.noGuidIpEndpoint.Delete(n.Id) + if len(guidTrees) > 0 { + if err := j.inboundService.MergeClientIpsByGuid(n, guidTrees); err != nil { + logger.Warningf("node traffic sync: merge client ip attribution from %s failed: %v", n.Name, err) + } } } }