github.com/vmware/govmomi@v0.37.1/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 }