github.com/vmware/govmomi@v0.51.0/cli/metric/sample.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package metric
     6  
     7  import (
     8  	"context"
     9  	"crypto/md5"
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"os/exec"
    15  	"path"
    16  	"strings"
    17  	"text/tabwriter"
    18  	"time"
    19  
    20  	"github.com/vmware/govmomi/cli"
    21  	"github.com/vmware/govmomi/performance"
    22  	"github.com/vmware/govmomi/vim25/mo"
    23  	"github.com/vmware/govmomi/vim25/types"
    24  )
    25  
    26  type sample struct {
    27  	*PerformanceFlag
    28  
    29  	d        int
    30  	n        int
    31  	t        bool
    32  	plot     string
    33  	instance string
    34  }
    35  
    36  func init() {
    37  	cli.Register("metric.sample", &sample{})
    38  }
    39  
    40  func (cmd *sample) Register(ctx context.Context, f *flag.FlagSet) {
    41  	cmd.PerformanceFlag, ctx = NewPerformanceFlag(ctx)
    42  	cmd.PerformanceFlag.Register(ctx, f)
    43  
    44  	f.IntVar(&cmd.d, "d", 30, "Limit object display name to D chars")
    45  	f.IntVar(&cmd.n, "n", 5, "Max number of samples")
    46  	f.StringVar(&cmd.plot, "plot", "", "Plot data using gnuplot")
    47  	f.BoolVar(&cmd.t, "t", false, "Include sample times")
    48  	f.StringVar(&cmd.instance, "instance", "*", "Instance")
    49  }
    50  
    51  func (cmd *sample) Usage() string {
    52  	return "PATH... NAME..."
    53  }
    54  
    55  func (cmd *sample) Description() string {
    56  	return `Sample for object PATH of metric NAME.
    57  
    58  Interval ID defaults to 20 (realtime) if supported, otherwise 300 (5m interval).
    59  
    60  By default, INSTANCE '*' samples all instances and the aggregate counter.
    61  An INSTANCE value of '-' will only sample the aggregate counter.
    62  An INSTANCE value other than '*' or '-' will only sample the given instance counter.
    63  
    64  If PLOT value is set to '-', output a gnuplot script.  If non-empty with another
    65  value, PLOT will pipe the script to gnuplot for you.  The value is also used to set
    66  the gnuplot 'terminal' variable, unless the value is that of the DISPLAY env var.
    67  Only 1 metric NAME can be specified when the PLOT flag is set.
    68  
    69  Examples:
    70    govc metric.sample host/cluster1/* cpu.usage.average
    71    govc metric.sample -plot .png host/cluster1/* cpu.usage.average | xargs open
    72    govc metric.sample vm/* net.bytesTx.average net.bytesTx.average
    73    govc metric.sample -instance vmnic0 vm/* net.bytesTx.average
    74    govc metric.sample -instance - vm/* net.bytesTx.average`
    75  }
    76  
    77  func (cmd *sample) Process(ctx context.Context) error {
    78  	if err := cmd.PerformanceFlag.Process(ctx); err != nil {
    79  		return err
    80  	}
    81  	return nil
    82  }
    83  
    84  type sampleResult struct {
    85  	cmd      *sample
    86  	m        *performance.Manager
    87  	counters map[string]*types.PerfCounterInfo
    88  	Sample   []performance.EntityMetric `json:"sample"`
    89  }
    90  
    91  func (r *sampleResult) name(e types.ManagedObjectReference) string {
    92  	var me mo.ManagedEntity
    93  	_ = r.m.Properties(context.Background(), e, []string{"name"}, &me)
    94  
    95  	name := me.Name
    96  
    97  	if r.cmd.d > 0 && len(name) > r.cmd.d {
    98  		return name[:r.cmd.d] + "*"
    99  	}
   100  
   101  	return name
   102  }
   103  
   104  func sampleInfoTimes(m *performance.EntityMetric) []string {
   105  	vals := make([]string, len(m.SampleInfo))
   106  
   107  	for i := range m.SampleInfo {
   108  		vals[i] = m.SampleInfo[i].Timestamp.Format(time.RFC3339)
   109  	}
   110  
   111  	return vals
   112  }
   113  
   114  func (r *sampleResult) Plot(w io.Writer) error {
   115  	if len(r.Sample) == 0 {
   116  		return nil
   117  	}
   118  
   119  	if r.cmd.plot != "-" {
   120  		cmd := exec.Command("gnuplot", "-persist")
   121  		cmd.Stdout = w
   122  		cmd.Stderr = os.Stderr
   123  		stdin, err := cmd.StdinPipe()
   124  		if err != nil {
   125  			return err
   126  		}
   127  
   128  		if err = cmd.Start(); err != nil {
   129  			return err
   130  		}
   131  
   132  		w = stdin
   133  		defer func() {
   134  			_ = stdin.Close()
   135  			_ = cmd.Wait()
   136  		}()
   137  	}
   138  
   139  	counter := r.counters[r.Sample[0].Value[0].Name]
   140  	unit := counter.UnitInfo.GetElementDescription()
   141  
   142  	fmt.Fprintf(w, "set title %q\n", counter.Name())
   143  	fmt.Fprintf(w, "set ylabel %q\n", unit.Label)
   144  	fmt.Fprintf(w, "set xlabel %q\n", "Time")
   145  	fmt.Fprintf(w, "set xdata %s\n", "time")
   146  	fmt.Fprintf(w, "set format x %q\n", "%H:%M")
   147  	fmt.Fprintf(w, "set timefmt %q\n", "%Y-%m-%dT%H:%M:%SZ")
   148  
   149  	ext := path.Ext(r.cmd.plot)
   150  	if ext != "" {
   151  		// If a file name is given, use the extension as terminal type.
   152  		// If just an ext is given, use the entities and counter as the file name.
   153  		file := r.cmd.plot
   154  		name := r.cmd.plot[:len(r.cmd.plot)-len(ext)]
   155  		r.cmd.plot = ext[1:]
   156  
   157  		if name == "" {
   158  			h := md5.New()
   159  
   160  			for i := range r.Sample {
   161  				_, _ = io.WriteString(h, r.Sample[i].Entity.String())
   162  			}
   163  			_, _ = io.WriteString(h, counter.Name())
   164  
   165  			file = fmt.Sprintf("govc-plot-%x%s", h.Sum(nil), ext)
   166  		}
   167  
   168  		fmt.Fprintf(w, "set output %q\n", file)
   169  
   170  		defer func() {
   171  			fmt.Fprintln(r.cmd.Out, file)
   172  		}()
   173  	}
   174  
   175  	switch r.cmd.plot {
   176  	case "-", os.Getenv("DISPLAY"):
   177  	default:
   178  		fmt.Fprintf(w, "set terminal %s\n", r.cmd.plot)
   179  	}
   180  
   181  	if unit.Key == string(types.PerformanceManagerUnitPercent) {
   182  		fmt.Fprintln(w, "set yrange [0:100]")
   183  	}
   184  
   185  	fmt.Fprintln(w)
   186  
   187  	var set []string
   188  
   189  	for i := range r.Sample {
   190  		name := r.name(r.Sample[i].Entity)
   191  		name = strings.Replace(name, "_", "*", -1) // underscore is some gnuplot markup?
   192  		set = append(set, fmt.Sprintf("'-' using 1:2 title '%s' with lines", name))
   193  	}
   194  
   195  	fmt.Fprintf(w, "plot %s\n", strings.Join(set, ", "))
   196  
   197  	for i := range r.Sample {
   198  		times := sampleInfoTimes(&r.Sample[i])
   199  
   200  		for _, value := range r.Sample[i].Value {
   201  			for j := range value.Value {
   202  				fmt.Fprintf(w, "%s %s\n", times[j], value.Format(value.Value[j]))
   203  			}
   204  		}
   205  
   206  		fmt.Fprintln(w, "e")
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  func (r *sampleResult) Write(w io.Writer) error {
   213  	if r.cmd.plot != "" {
   214  		return r.Plot(w)
   215  	}
   216  
   217  	cmd := r.cmd
   218  	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
   219  
   220  	for i := range r.Sample {
   221  		metric := r.Sample[i]
   222  		name := r.name(metric.Entity)
   223  		t := ""
   224  		if cmd.t {
   225  			t = metric.SampleInfoCSV()
   226  		}
   227  
   228  		for _, v := range metric.Value {
   229  			counter := r.counters[v.Name]
   230  			units := counter.UnitInfo.GetElementDescription().Label
   231  
   232  			instance := v.Instance
   233  			if instance == "" {
   234  				instance = "-"
   235  			}
   236  
   237  			fmt.Fprintf(tw, "%s\t%s\t%s\t%v\t%s\t%s\n",
   238  				name, instance, v.Name, t, v.ValueCSV(), units)
   239  		}
   240  	}
   241  
   242  	return tw.Flush()
   243  }
   244  
   245  func (cmd *sample) Run(ctx context.Context, f *flag.FlagSet) error {
   246  	m, err := cmd.Manager(ctx)
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	var paths []string
   252  	var names []string
   253  
   254  	byName, err := m.CounterInfoByName(ctx)
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	for _, arg := range f.Args() {
   260  		if _, ok := byName[arg]; ok {
   261  			names = append(names, arg)
   262  		} else {
   263  			paths = append(paths, arg)
   264  		}
   265  	}
   266  
   267  	if len(paths) == 0 || len(names) == 0 {
   268  		return flag.ErrHelp
   269  	}
   270  
   271  	if cmd.plot != "" {
   272  		if len(names) > 1 {
   273  			return flag.ErrHelp
   274  		}
   275  
   276  		if cmd.instance == "*" {
   277  			cmd.instance = ""
   278  		}
   279  	}
   280  
   281  	objs, err := cmd.ManagedObjects(ctx, paths)
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	s, err := m.ProviderSummary(ctx, objs[0])
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	if cmd.instance == "-" {
   292  		cmd.instance = ""
   293  	}
   294  
   295  	spec := types.PerfQuerySpec{
   296  		Format:     string(types.PerfFormatNormal),
   297  		MaxSample:  int32(cmd.n),
   298  		MetricId:   []types.PerfMetricId{{Instance: cmd.instance}},
   299  		IntervalId: cmd.Interval(s.RefreshRate),
   300  	}
   301  
   302  	sample, err := m.SampleByName(ctx, spec, names, objs)
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	result, err := m.ToMetricSeries(ctx, sample)
   308  	if err != nil {
   309  		return err
   310  	}
   311  
   312  	counters, err := m.CounterInfoByName(ctx)
   313  	if err != nil {
   314  		return err
   315  	}
   316  
   317  	return cmd.WriteResult(&sampleResult{cmd, m, counters, result})
   318  }