github.com/vmware/govmomi@v0.37.2/govc/metric/sample.go (about)

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