Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a dht stat command #7221

Merged
merged 1 commit into from
May 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ func TestCommands(t *testing.T) {
"/stats",
"/stats/bitswap",
"/stats/bw",
"/stats/dht",
"/stats/repo",
"/swarm",
"/swarm/addrs",
Expand Down
1 change: 1 addition & 0 deletions core/commands/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ for your IPFS node.`,
"bw": statBwCmd,
"repo": repoStatCmd,
"bitswap": bitswapStatCmd,
"dht": statDhtCmd,
},
}

Expand Down
194 changes: 194 additions & 0 deletions core/commands/stat_dht.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package commands

import (
"fmt"
"io"
"text/tabwriter"
"time"

cmdenv "github.com/ipfs/go-ipfs/core/commands/cmdenv"

cmds "github.com/ipfs/go-ipfs-cmds"
"github.com/libp2p/go-libp2p-core/network"
pstore "github.com/libp2p/go-libp2p-core/peerstore"
dht "github.com/libp2p/go-libp2p-kad-dht"
kbucket "github.com/libp2p/go-libp2p-kbucket"
)

type dhtPeerInfo struct {
ID string
Connected bool
AgentVersion string
LastUsefulAt string
LastQueriedAt string
}

type dhtStat struct {
Name string
Buckets []dhtBucket
}

type dhtBucket struct {
LastRefresh string
Peers []dhtPeerInfo
}

var statDhtCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Returns statistics about the node's DHT(s)",
ShortDescription: `
Returns statistics about the DHT(s) the node is participating in.

This interface is not stable and may change from release to release.
`,
},
Arguments: []cmds.Argument{
cmds.StringArg("dht", false, true, "The DHT whose table should be listed (wan or lan). Defaults to both."),
},
Options: []cmds.Option{},
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
nd, err := cmdenv.GetNode(env)
if err != nil {
return err
}

if !nd.IsOnline {
return ErrNotOnline
}

if nd.DHT == nil {
return ErrNotDHT
}

id := kbucket.ConvertPeerID(nd.Identity)

dhts := req.Arguments
if len(dhts) == 0 {
dhts = []string{"wan", "lan"}
}

for _, name := range dhts {
var dht *dht.IpfsDHT
switch name {
case "wan":
dht = nd.DHT.WAN
case "lan":
dht = nd.DHT.LAN
default:
return cmds.Errorf(cmds.ErrClient, "unknown dht type: %s", name)
}

rt := dht.RoutingTable()
lastRefresh := rt.GetTrackedCplsForRefresh()
infos := rt.GetPeerInfos()
buckets := make([]dhtBucket, 0, len(lastRefresh))
for _, pi := range infos {
cpl := kbucket.CommonPrefixLen(id, kbucket.ConvertPeerID(pi.Id))
if len(buckets) <= cpl {
buckets = append(buckets, make([]dhtBucket, 1+cpl-len(buckets))...)
}

info := dhtPeerInfo{ID: pi.Id.String()}

if ver, err := nd.Peerstore.Get(pi.Id, "AgentVersion"); err == nil {
info.AgentVersion, _ = ver.(string)
} else if err == pstore.ErrNotFound {
// ignore
} else {
// this is a bug, usually.
log.Errorw(
"failed to get agent version from peerstore",
"error", err,
)
}
if !pi.LastUsefulAt.IsZero() {
info.LastUsefulAt = pi.LastUsefulAt.Format(time.RFC3339)
}

if !pi.LastSuccessfulOutboundQueryAt.IsZero() {
info.LastQueriedAt = pi.LastSuccessfulOutboundQueryAt.Format(time.RFC3339)
}

info.Connected = nd.PeerHost.Network().Connectedness(pi.Id) == network.Connected

buckets[cpl].Peers = append(buckets[cpl].Peers, info)
}
for i := 0; i < len(buckets) && i < len(lastRefresh); i++ {
refreshTime := lastRefresh[i]
if !refreshTime.IsZero() {
buckets[i].LastRefresh = refreshTime.Format(time.RFC3339)
}
}
if err := res.Emit(dhtStat{
Name: name,
Buckets: buckets,
}); err != nil {
return err
}
}

return nil
},
Encoders: cmds.EncoderMap{
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out dhtStat) error {
tw := tabwriter.NewWriter(w, 4, 4, 2, ' ', 0)
defer tw.Flush()

// Formats a time into XX ago and remove any decimal
// parts. That is, change "2m3.00010101s" to "2m3s ago".
now := time.Now()
since := func(t time.Time) string {
return now.Sub(t).Round(time.Second).String() + " ago"
}

count := 0
for _, bucket := range out.Buckets {
count += len(bucket.Peers)
}

fmt.Fprintf(tw, "DHT %s (%d peers):\t\t\t\n", out.Name, count)

for i, bucket := range out.Buckets {
lastRefresh := "never"
if bucket.LastRefresh != "" {
t, err := time.Parse(time.RFC3339, bucket.LastRefresh)
if err != nil {
return err
}
lastRefresh = since(t)
}
fmt.Fprintf(tw, " Bucket %2d (%d peers) - refreshed %s:\t\t\t\n", i, len(bucket.Peers), lastRefresh)
fmt.Fprintln(tw, " Peer\tlast useful\tlast queried\tAgent Version")

for _, p := range bucket.Peers {
lastUseful := "never"
if p.LastUsefulAt != "" {
t, err := time.Parse(time.RFC3339, p.LastUsefulAt)
if err != nil {
return err
}
lastUseful = since(t)
}

lastQueried := "never"
if p.LastUsefulAt != "" {
t, err := time.Parse(time.RFC3339, p.LastQueriedAt)
if err != nil {
return err
}
lastQueried = since(t)
}

state := " "
if p.Connected {
state = "@"
}
fmt.Fprintf(tw, " %s %s\t%s\t%s\t%s\n", state, p.ID, lastUseful, lastQueried, p.AgentVersion)
}
fmt.Fprintln(tw, "\t\t\t")
}
return nil
}),
},
Type: dhtStat{},
}