github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/metricsdebug/metrics.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package metricsdebug
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"sort"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/gosuri/uitable"
    14  	"github.com/juju/cmd"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/gnuflag"
    17  	"gopkg.in/juju/names.v2"
    18  
    19  	"github.com/juju/juju/api/metricsdebug"
    20  	"github.com/juju/juju/apiserver/params"
    21  	jujucmd "github.com/juju/juju/cmd"
    22  	"github.com/juju/juju/cmd/modelcmd"
    23  )
    24  
    25  const metricsDoc = `
    26  Display recently collected metrics.
    27  `
    28  
    29  // MetricsCommand retrieves metrics stored in the juju controller.
    30  type MetricsCommand struct {
    31  	modelcmd.ModelCommandBase
    32  	out cmd.Output
    33  
    34  	Tags []string
    35  	All  bool
    36  }
    37  
    38  // New creates a new MetricsCommand.
    39  func New() cmd.Command {
    40  	return modelcmd.Wrap(&MetricsCommand{})
    41  }
    42  
    43  // Info implements Command.Info.
    44  func (c *MetricsCommand) Info() *cmd.Info {
    45  	return jujucmd.Info(&cmd.Info{
    46  		Name:    "metrics",
    47  		Args:    "[tag1[...tagN]]",
    48  		Purpose: "Retrieve metrics collected by specified entities.",
    49  		Doc:     metricsDoc,
    50  	})
    51  }
    52  
    53  // Init reads and verifies the cli arguments for the MetricsCommand
    54  func (c *MetricsCommand) Init(args []string) error {
    55  	if !c.All && len(args) == 0 {
    56  		return errors.New("you need to specify at least one unit or application")
    57  	} else if c.All && len(args) > 0 {
    58  		return errors.New("cannot use --all with additional entities")
    59  	}
    60  	c.Tags = make([]string, len(args))
    61  	for i, arg := range args {
    62  		if names.IsValidUnit(arg) {
    63  			c.Tags[i] = names.NewUnitTag(arg).String()
    64  		} else if names.IsValidApplication(arg) {
    65  			c.Tags[i] = names.NewApplicationTag(arg).String()
    66  		} else {
    67  			return errors.Errorf("%q is not a valid unit or application", args[0])
    68  		}
    69  	}
    70  	return nil
    71  }
    72  
    73  // SetFlags implements cmd.Command.SetFlags.
    74  func (c *MetricsCommand) SetFlags(f *gnuflag.FlagSet) {
    75  	c.ModelCommandBase.SetFlags(f)
    76  	c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{
    77  		"tabular": formatTabular,
    78  		"json":    cmd.FormatJson,
    79  		"yaml":    cmd.FormatYaml,
    80  	})
    81  	f.BoolVar(&c.All, "all", false, "retrieve metrics collected by all units in the model")
    82  }
    83  
    84  type GetMetricsClient interface {
    85  	GetMetrics(tags ...string) ([]params.MetricResult, error)
    86  	Close() error
    87  }
    88  
    89  var newClient = func(env modelcmd.ModelCommandBase) (GetMetricsClient, error) {
    90  	state, err := env.NewAPIRoot()
    91  	if err != nil {
    92  		return nil, errors.Trace(err)
    93  	}
    94  	return metricsdebug.NewClient(state), nil
    95  }
    96  
    97  type metricSlice []metric
    98  
    99  // Len implements the sort.Interface.
   100  func (slice metricSlice) Len() int {
   101  	return len(slice)
   102  }
   103  
   104  // Less implements the sort.Interface.
   105  func (slice metricSlice) Less(i, j int) bool {
   106  	if slice[i].Metric == slice[j].Metric {
   107  		return renderLabels(slice[i].Labels) < renderLabels(slice[j].Labels)
   108  	}
   109  	return slice[i].Metric < slice[j].Metric
   110  }
   111  
   112  // Swap implements the sort.Interface.
   113  func (slice metricSlice) Swap(i, j int) {
   114  	slice[i], slice[j] = slice[j], slice[i]
   115  }
   116  
   117  type metric struct {
   118  	Unit      string            `json:"unit" yaml:"unit"`
   119  	Timestamp time.Time         `json:"timestamp" yaml:"timestamp"`
   120  	Metric    string            `json:"metric" yaml:"metric"`
   121  	Value     string            `json:"value" yaml:"value"`
   122  	Labels    map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
   123  }
   124  
   125  // Run implements Command.Run.
   126  func (c *MetricsCommand) Run(ctx *cmd.Context) error {
   127  	client, err := newClient(c.ModelCommandBase)
   128  	if err != nil {
   129  		return errors.Trace(err)
   130  	}
   131  	var metrics []params.MetricResult
   132  	if c.All {
   133  		metrics, err = client.GetMetrics()
   134  	} else {
   135  		metrics, err = client.GetMetrics(c.Tags...)
   136  	}
   137  	if err != nil {
   138  		return errors.Trace(err)
   139  	}
   140  	defer client.Close()
   141  	if len(metrics) == 0 {
   142  		return nil
   143  	}
   144  	results := make([]metric, len(metrics))
   145  	for i, m := range metrics {
   146  		results[i] = metric{
   147  			Unit:      m.Unit,
   148  			Timestamp: m.Time,
   149  			Metric:    m.Key,
   150  			Value:     m.Value,
   151  			Labels:    m.Labels,
   152  		}
   153  	}
   154  	sortedResults := metricSlice(results)
   155  	sort.Sort(sortedResults)
   156  
   157  	return errors.Trace(c.out.Write(ctx, results))
   158  }
   159  
   160  // formatTabular returns a tabular view of collected metrics.
   161  func formatTabular(writer io.Writer, value interface{}) error {
   162  	metrics, ok := value.([]metric)
   163  	if !ok {
   164  		return errors.Errorf("expected value of type %T, got %T", metrics, value)
   165  	}
   166  	table := uitable.New()
   167  	table.MaxColWidth = 50
   168  	table.Wrap = true
   169  	for _, col := range []int{1, 2, 3, 4} {
   170  		table.RightAlign(col)
   171  	}
   172  	table.AddRow("UNIT", "TIMESTAMP", "METRIC", "VALUE", "LABELS")
   173  	for _, m := range metrics {
   174  		table.AddRow(m.Unit, m.Timestamp.Format(time.RFC3339), m.Metric, m.Value, renderLabels(m.Labels))
   175  	}
   176  	_, err := fmt.Fprint(writer, table.String())
   177  	return errors.Trace(err)
   178  }
   179  
   180  func renderLabels(m map[string]string) string {
   181  	var result []string
   182  	for k, v := range m {
   183  		result = append(result, fmt.Sprintf("%s=%s", k, v))
   184  	}
   185  	sort.Strings(result)
   186  	return strings.Join(result, ",")
   187  }