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 }