feat(discovery): add RelaySet.PriorityRelaysWithTrace + PriorityMultiHopWithTrace; wire metrics emission and sampled debug log

cognitive committed Apr 30, 2026 at 05:27 UTC 2b5b6fb678b7d3460b2e5c24a27a1d224bc39cb0
1 file changed +61 -4
portal/discovery/relayset.go
+61 -4
@@ -9,6 +9,8 @@ import (
9 "sync"
10 "time"
11
12 + "github.com/rs/zerolog/log"
13 +
14 "github.com/gosuda/portal-tunnel/v2/portal/auth"
15 "github.com/gosuda/portal-tunnel/v2/types"
16 )
@@ -220,7 +222,12 @@ func (s *RelaySet) ConfirmedRelays() []RelayState {
222 return policy.SelectConfirmed(states)
223 }
224
223 -func (s *RelaySet) PriorityRelays(clientState ClientState) []string {
225 +// PriorityRelaysWithTrace returns the same ordered relay-URL list as
226 +// PriorityRelays, plus a SelectionTrace populated with pool statistics,
227 +// eligibility classification, and the scoring parameters used. Prometheus
228 +// metrics are emitted from the trace before returning, and a sampled zerolog
229 +// debug entry is written.
230 +func (s *RelaySet) PriorityRelaysWithTrace(clientState ClientState) ([]string, SelectionTrace) {
231 s.mu.RLock()
232 states := make([]RelayState, 0, len(s.relays))
233 for _, state := range s.relays {
@@ -229,10 +236,33 @@ func (s *RelaySet) PriorityRelays(clientState ClientState) []string {
236 policy := s.policy
237 s.mu.RUnlock()
238
232 - return policy.SelectPriority(states, clientState)
239 + result, trace := policy.SelectPriorityWithTrace(states, clientState)
240 + EmitFromTrace(trace)
241 + log.Debug().
242 + Uint8("client_hash", trace.ClientHash).
243 + Int("pool_size", trace.PoolTotal).
244 + Int("output_count", len(trace.OutputURLs)).
245 + Str("mode", trace.Mode).
246 + Bool("congested", trace.Congested).
247 + Strs("top3", first3(trace.OutputURLs)).
248 + Msg("relay selection")
249 + return result, trace
250 }
251
235 -func (s *RelaySet) PriorityMultiHop(clientState ClientState) []string {
252 +// PriorityRelays returns the ordered list of relay URLs for a client. It
253 +// delegates to PriorityRelaysWithTrace and discards the trace. The public
254 +// signature is unchanged through all phases.
255 +func (s *RelaySet) PriorityRelays(clientState ClientState) []string {
256 + out, _ := s.PriorityRelaysWithTrace(clientState)
257 + return out
258 +}
259 +
260 +// PriorityMultiHopWithTrace returns the same ordered relay-URL list as
261 +// PriorityMultiHop, plus a SelectionTrace populated with pool statistics,
262 +// eligibility classification, and the scoring parameters used. Prometheus
263 +// metrics are emitted from the trace before returning, and a sampled zerolog
264 +// debug entry is written.
265 +func (s *RelaySet) PriorityMultiHopWithTrace(clientState ClientState) ([]string, SelectionTrace) {
266 s.mu.RLock()
267 states := make([]RelayState, 0, len(s.relays))
268 for _, state := range s.relays {
@@ -241,7 +271,34 @@ func (s *RelaySet) PriorityMultiHop(clientState ClientState) []string {
271 policy := s.policy
272 s.mu.RUnlock()
273
244 - return policy.SelectMultiHop(states, clientState)
274 + result, trace := policy.SelectMultiHopWithTrace(states, clientState)
275 + EmitFromTrace(trace)
276 + log.Debug().
277 + Uint8("client_hash", trace.ClientHash).
278 + Int("pool_size", trace.PoolTotal).
279 + Int("output_count", len(trace.OutputURLs)).
280 + Str("mode", trace.Mode).
281 + Bool("congested", trace.Congested).
282 + Strs("top3", first3(trace.OutputURLs)).
283 + Msg("relay selection")
284 + return result, trace
285 +}
286 +
287 +// PriorityMultiHop returns the ordered list of relay URLs for multi-hop
288 +// routing. It delegates to PriorityMultiHopWithTrace and discards the trace.
289 +// The public signature is unchanged through all phases.
290 +func (s *RelaySet) PriorityMultiHop(clientState ClientState) []string {
291 + out, _ := s.PriorityMultiHopWithTrace(clientState)
292 + return out
293 +}
294 +
295 +// first3 returns a slice containing the first three elements of s, or all
296 +// elements if s has fewer than three. It never modifies the input slice.
297 +func first3(s []string) []string {
298 + if len(s) <= 3 {
299 + return s
300 + }
301 + return s[:3]
302 }
303
304 func (s *RelaySet) OverlayPeerStates() []RelayState {