Make metrics human

Issue #572 exposes metrics but they carry the peer ID in binary.

This was ok with our internal codecs but it doesn't seem to work
very well with json, and makes the output format unusable.

This makes the Metric.Peer field a string.

Additinoally, fixes calling the command without arguments and displaying
the date in the right format.

License: MIT
Signed-off-by: Hector Sanjuan <code@hector.link>
This commit is contained in:
Hector Sanjuan 2018-10-23 20:21:27 +02:00
parent 53a16fba8f
commit 19b1124999
9 changed files with 85 additions and 16 deletions

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
@ -205,9 +206,12 @@ func (c *defaultClient) GetConnectGraph() (api.ConnectGraphSerial, error) {
return graphS, err return graphS, err
} }
// Metrics returns a map with the latest metrics of matching name // Metrics returns a map with the latest valid metrics of the given name
// for the current cluster peers. // for the current cluster peers.
func (c *defaultClient) Metrics(name string) ([]api.Metric, error) { func (c *defaultClient) Metrics(name string) ([]api.Metric, error) {
if name == "" {
return nil, errors.New("bad metric name")
}
var metrics []api.Metric var metrics []api.Metric
err := c.do("GET", fmt.Sprintf("/monitor/metrics/%s", name), nil, nil, &metrics) err := c.do("GET", fmt.Sprintf("/monitor/metrics/%s", name), nil, nil, &metrics)
return metrics, err return metrics, err

View File

@ -92,9 +92,10 @@ func (c *defaultClient) handleResponse(resp *http.Response, obj interface{}) err
var apiErr api.Error var apiErr api.Error
err = json.Unmarshal(body, &apiErr) err = json.Unmarshal(body, &apiErr)
if err != nil { if err != nil {
// not json. 404s etc.
return &api.Error{ return &api.Error{
Code: resp.StatusCode, Code: resp.StatusCode,
Message: err.Error(), Message: string(body),
} }
} }
return &apiErr return &apiErr

View File

@ -9,6 +9,8 @@
package api package api
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"regexp" "regexp"
"sort" "sort"
@ -847,10 +849,10 @@ func (n *NodeWithMeta) Size() uint64 {
// the Value, which should be interpreted by the PinAllocator. // the Value, which should be interpreted by the PinAllocator.
type Metric struct { type Metric struct {
Name string Name string
Peer peer.ID // filled-in by Cluster. Peer peer.ID
Value string Value string
Expire int64 // UnixNano Expire int64
Valid bool // if the metric is not valid it will be discarded Valid bool
} }
// SetTTL sets Metric to expire after the given time.Duration // SetTTL sets Metric to expire after the given time.Duration
@ -876,6 +878,51 @@ func (m *Metric) Discard() bool {
return !m.Valid || m.Expired() return !m.Valid || m.Expired()
} }
// helper for JSON marshaling. The Metric type is already
// serializable, but not pretty to humans (API).
type metricSerial struct {
Name string `json:"name"`
Peer string `json:"peer"`
Value string `json:"value"`
Expire int64 `json:"expire"`
Valid bool `json:"valid"`
}
// MarshalJSON allows a Metric to produce a JSON representation
// of itself.
func (m *Metric) MarshalJSON() ([]byte, error) {
return json.Marshal(&metricSerial{
Name: m.Name,
Peer: peer.IDB58Encode(m.Peer),
Value: m.Value,
Expire: m.Expire,
})
}
// UnmarshalJSON decodes JSON on top of the Metric.
func (m *Metric) UnmarshalJSON(j []byte) error {
if bytes.Equal(j, []byte("null")) {
return nil
}
ms := &metricSerial{}
err := json.Unmarshal(j, ms)
if err != nil {
return err
}
p, err := peer.IDB58Decode(ms.Peer)
if err != nil {
return err
}
m.Name = ms.Name
m.Peer = p
m.Value = ms.Value
m.Expire = ms.Expire
return nil
}
// Alert carries alerting information about a peer. WIP. // Alert carries alerting information about a peer. WIP.
type Alert struct { type Alert struct {
Peer peer.ID Peer peer.ID

View File

@ -275,7 +275,10 @@ func (c *Cluster) alertsHandler() {
// only the leader handles alerts // only the leader handles alerts
leader, err := c.consensus.Leader() leader, err := c.consensus.Leader()
if err == nil && leader == c.id { if err == nil && leader == c.id {
logger.Warningf("Peer %s received alert for %s in %s", c.id, alrt.MetricName, alrt.Peer.Pretty()) logger.Warningf(
"Peer %s received alert for %s in %s",
c.id, alrt.MetricName, alrt.Peer,
)
switch alrt.MetricName { switch alrt.MetricName {
case pingMetricName: case pingMetricName:
c.repinFromPeer(alrt.Peer) c.repinFromPeer(alrt.Peer)

View File

@ -6,8 +6,10 @@ import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"time"
"github.com/ipfs/ipfs-cluster/api" "github.com/ipfs/ipfs-cluster/api"
peer "github.com/libp2p/go-libp2p-peer"
) )
func jsonFormatObject(resp interface{}) { func jsonFormatObject(resp interface{}) {
@ -216,7 +218,8 @@ func textFormatPrintAddedOutput(obj *api.AddedOutput) {
} }
func textFormatPrintMetric(obj *api.Metric) { func textFormatPrintMetric(obj *api.Metric) {
fmt.Printf("%s: %s | Expire : %d\n", obj.Peer.Pretty(), obj.Value, obj.Expire) date := time.Unix(0, obj.Expire).UTC().Format(time.RFC3339)
fmt.Printf("%s: %s | Expire: %s\n", peer.IDB58Encode(obj.Peer), obj.Value, date)
} }
func textFormatPrintError(obj *api.Error) { func textFormatPrintError(obj *api.Error) {

View File

@ -768,10 +768,21 @@ graph of the connections. Output is a dot file encoding the cluster's connectio
Description: ` Description: `
This commands displays the latest valid metrics of the given type logged This commands displays the latest valid metrics of the given type logged
by this peer for all current cluster peers. by this peer for all current cluster peers.
Currently supported metrics depend on the informer component used,
but usually are:
- freespace
- ping
`, `,
ArgsUsage: "Metric name", ArgsUsage: "<metric name>",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
resp, cerr := globalClient.Metrics(c.Args().First()) metric := c.Args().First()
if metric == "" {
checkErr("", errors.New("provide a metric name"))
}
resp, cerr := globalClient.Metrics(metric)
formatResponse(c, resp, cerr) formatResponse(c, resp, cerr)
return nil return nil
}, },

View File

@ -71,14 +71,14 @@ func (mtrs *Store) Latest(name string) []api.Metric {
// PeerMetrics returns the latest metrics for a given peer ID for // PeerMetrics returns the latest metrics for a given peer ID for
// all known metrics types. It may return expired metrics. // all known metrics types. It may return expired metrics.
func (mtrs *Store) PeerMetrics(peer peer.ID) []api.Metric { func (mtrs *Store) PeerMetrics(pid peer.ID) []api.Metric {
mtrs.mux.RLock() mtrs.mux.RLock()
defer mtrs.mux.RUnlock() defer mtrs.mux.RUnlock()
result := make([]api.Metric, 0) result := make([]api.Metric, 0)
for _, byPeer := range mtrs.byName { for _, byPeer := range mtrs.byName {
window, ok := byPeer[peer] window, ok := byPeer[pid]
if !ok { if !ok {
continue continue
} }

View File

@ -10,8 +10,8 @@ import (
// peerset // peerset
func PeersetFilter(metrics []api.Metric, peerset []peer.ID) []api.Metric { func PeersetFilter(metrics []api.Metric, peerset []peer.ID) []api.Metric {
peerMap := make(map[peer.ID]struct{}) peerMap := make(map[peer.ID]struct{})
for _, peer := range peerset { for _, pid := range peerset {
peerMap[peer] = struct{}{} peerMap[pid] = struct{}{}
} }
filtered := make([]api.Metric, 0, len(metrics)) filtered := make([]api.Metric, 0, len(metrics))

View File

@ -205,12 +205,12 @@ func (rpcapi *RPCAPI) BlockAllocate(ctx context.Context, in api.PinSerial, out *
// Returned metrics are Valid and belong to current // Returned metrics are Valid and belong to current
// Cluster peers. // Cluster peers.
metrics := rpcapi.c.monitor.LatestMetrics(pingMetricName) metrics := rpcapi.c.monitor.LatestMetrics(pingMetricName)
peers := make([]peer.ID, len(metrics), len(metrics)) peers := make([]string, len(metrics), len(metrics))
for i, m := range metrics { for i, m := range metrics {
peers[i] = m.Peer peers[i] = peer.IDB58Encode(m.Peer)
} }
*out = api.PeersToStrings(peers) *out = peers
return nil return nil
} }