Skip to content

Commit

Permalink
Convert Stats into a struct
Browse files Browse the repository at this point in the history
Towards hashicorp#167: Make the stats into a real structure
  • Loading branch information
ongardie committed Oct 14, 2016
1 parent 6089236 commit 3a8ac9a
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 47 deletions.
134 changes: 87 additions & 47 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -846,60 +847,108 @@ func (r *Raft) LastContact() time.Time {
return last
}

// Stats is used to return a map of various internal stats. This
// should only be used for informative purposes or debugging.
//
// Keys are: "state", "term", "last_log_index", "last_log_term",
// "commit_index", "applied_index", "fsm_pending",
// "last_snapshot_index", "last_snapshot_term",
// "latest_configuration", "last_contact", and "num_peers".
//
// The value of "state" is a numerical value representing a
// RaftState const.
//
// The value of "latest_configuration" is a string which contains
// the id of each server, its suffrage status, and its address.
type Stats struct {
State RaftState
Term Term
LastLogIndex Index
LastLogTerm Term
CommitIndex Index
AppliedIndex Index
FSMPending int
LastSnapshotIndex Index
LastSnapshotTerm Term
LatestConfiguration Configuration
LatestConfigurationIndex Index
LastContact time.Time
// numPeers is the number of other voting servers in the cluster, not
// including this node. If this node isn't part of the configuration then this
// will be 0.
NumPeers int
ProtocolVersion ProtocolVersion
ProtocolVersionMin ProtocolVersion
ProtocolVersionMax ProtocolVersion
SnapshotVersionMin SnapshotVersion
SnapshotVersionMax SnapshotVersion
}

// Stringify a Stats struct into key-value strings.
//
// The value of "last_contact" is either "never" if there
// has been no contact with a leader, "0" if the node is in the
// leader state, or the time since last contact with a leader
// formatted as a string.
//
// The value of "num_peers" is the number of other voting servers in the
// cluster, not including this node. If this node isn't part of the
// configuration then this will be "0".
//
// All other values are uint64s, formatted as strings.
func (r *Raft) Stats() map[string]string {
func (s *Stats) Strings() []struct{ K, V string } {
toString := func(v uint64) string {
return strconv.FormatUint(v, 10)
}
var lastContact string
if s.LastContact.IsZero() {
lastContact = "never"
} else if s.State == Leader {
lastContact = "0"
} else {
lastContact = fmt.Sprintf("%v", time.Now().Sub(s.LastContact))
}
return []struct{ K, V string }{
{"state", s.State.String()},
{"term", toString(uint64(s.Term))},
{"last_log_index", toString(uint64(s.LastLogIndex))},
{"last_log_term", toString(uint64(s.LastLogTerm))},
{"commit_index", toString(uint64(s.CommitIndex))},
{"applied_index", toString(uint64(s.AppliedIndex))},
{"fsm_pending", toString(uint64(s.FSMPending))},
{"last_snapshot_index", toString(uint64(s.LastSnapshotIndex))},
{"last_snapshot_term", toString(uint64(s.LastSnapshotTerm))},
{"latest_configuration", fmt.Sprintf("%+v", s.LatestConfiguration.Servers)},
{"latest_configuration_index", toString(uint64(s.LatestConfigurationIndex))},
{"last_contact", lastContact},
{"num_peers", toString(uint64(s.NumPeers))},
{"protocol_version", toString(uint64(s.ProtocolVersion))},
{"protocol_version_min", toString(uint64(s.ProtocolVersionMin))},
{"protocol_version_max", toString(uint64(s.ProtocolVersionMax))},
{"snapshot_version_min", toString(uint64(s.SnapshotVersionMin))},
{"snapshot_version_max", toString(uint64(s.SnapshotVersionMax))},
}
}

func (s *Stats) String() string {
kvs := s.Strings()
lines := make([]string, 0, len(kvs))
for _, kv := range kvs {
lines = append(lines, fmt.Sprintf("%v: %v", kv.K, kv.V))
}
return strings.Join(lines, "\n")
}

// Stats returns various internal stats.
func (r *Raft) Stats() *Stats {
lastLogIndex, lastLogTerm := r.shared.getLastLog()
lastSnapIndex, lastSnapTerm := r.shared.getLastSnapshot()
s := map[string]string{
"state": r.shared.getState().String(),
"term": toString(uint64(r.shared.getCurrentTerm())),
"last_log_index": toString(uint64(lastLogIndex)),
"last_log_term": toString(uint64(lastLogTerm)),
"commit_index": toString(uint64(r.shared.getCommitIndex())),
"applied_index": toString(uint64(r.shared.getLastApplied())),
"fsm_pending": toString(uint64(len(r.fsmCommitCh))),
"last_snapshot_index": toString(uint64(lastSnapIndex)),
"last_snapshot_term": toString(uint64(lastSnapTerm)),
"protocol_version": toString(uint64(r.protocolVersion)),
"protocol_version_min": toString(uint64(ProtocolVersionMin)),
"protocol_version_max": toString(uint64(ProtocolVersionMax)),
"snapshot_version_min": toString(uint64(SnapshotVersionMin)),
"snapshot_version_max": toString(uint64(SnapshotVersionMax)),
s := &Stats{
State: r.shared.getState(),
Term: r.shared.getCurrentTerm(),
LastLogIndex: lastLogIndex,
LastLogTerm: lastLogTerm,
CommitIndex: r.shared.getCommitIndex(),
AppliedIndex: r.shared.getLastApplied(),
FSMPending: len(r.fsmCommitCh),
LastSnapshotIndex: lastSnapIndex,
LastSnapshotTerm: lastSnapTerm,
LastContact: r.LastContact(),
ProtocolVersion: r.protocolVersion,
ProtocolVersionMin: ProtocolVersionMin,
ProtocolVersionMax: ProtocolVersionMax,
SnapshotVersionMin: SnapshotVersionMin,
SnapshotVersionMax: SnapshotVersionMax,
}

future := r.GetConfiguration()
if err := future.Error(); err != nil {
r.logger.Warn("could not get configuration for Stats: %v", err)
} else {
configuration := future.Configuration()
s["latest_configuration_index"] = toString(uint64(future.Index()))
s["latest_configuration"] = fmt.Sprintf("%+v", configuration.Servers)
s.LatestConfigurationIndex = future.Index()
s.LatestConfiguration = configuration

// This is a legacy metric that we've seen people use in the wild.
hasUs := false
Expand All @@ -916,16 +965,7 @@ func (r *Raft) Stats() map[string]string {
if !hasUs {
numPeers = 0
}
s["num_peers"] = toString(uint64(numPeers))
}

last := r.LastContact()
if last.IsZero() {
s["last_contact"] = "never"
} else if r.shared.getState() == Leader {
s["last_contact"] = "0"
} else {
s["last_contact"] = fmt.Sprintf("%v", time.Now().Sub(last))
s.NumPeers = numPeers
}
return s
}
Expand Down
12 changes: 12 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package raft

import "testing"

func TestAPI_Stats(t *testing.T) {
c := MakeCluster(1, t, nil)
s := c.rafts[0].Stats()
if s.LastLogTerm != 1 {
c.FailNowf("[ERR] err: stats.LastLogTerm expected 1, got %v", s.LastLogTerm)
}
c.Close()
}

0 comments on commit 3a8ac9a

Please sign in to comment.