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