Skip to content

Commit aa3c08d

Browse files
authored
eval status: enrich with related evals and placed allocs tables (#26156)
When debugging an evaluation, you almost always want to know about all the related evaluations and what allocations were placed by that evaluation (and where), not just failed placements. We can enrich the command by adding the `related` query parameter to the API, and having the command query for the evaluations allocations automatically. Emit this data as a pair of new tables and expose fields like quota limits, and previous/next/blocked eval without the `-verbose` flag. Update the docs to include the full output and remove references to long-removed behavior of the `-json` flag. Ref: https://hashicorp.atlassian.net/browse/NMD-818 Ref: https://go.hashi.co/rfc/nmd-212
1 parent 0c2fcb3 commit aa3c08d

File tree

6 files changed

+235
-53
lines changed

6 files changed

+235
-53
lines changed

.changelog/26156.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:improvement
2+
cli: Added related evals and placed allocations tables to the eval status command, and exposed more fields without requiring the `-verbose` flag.
3+
```
4+
5+
```release-note:improvement
6+
api: The `Evaluations.Info` method of the Go API now populates the `RelatedEvals` field.
7+
```

api/api_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func TestSetQueryOptions(t *testing.T) {
161161
c, s := makeClient(t, nil, nil)
162162
defer s.Stop()
163163

164-
r, _ := c.newRequest("GET", "/v1/jobs")
164+
r, _ := c.newRequest("GET", "/v1/jobs?format=baz")
165165
q := &QueryOptions{
166166
Region: "foo",
167167
Namespace: "bar",
@@ -188,6 +188,7 @@ func TestSetQueryOptions(t *testing.T) {
188188
try("index", "1000")
189189
try("wait", "100000ms")
190190
try("reverse", "true")
191+
try("format", "baz")
191192
}
192193

193194
func TestQueryOptionsContext(t *testing.T) {

api/evaluations.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (e *Evaluations) Count(q *QueryOptions) (*EvalCountResponse, *QueryMeta, er
4646
// Info is used to query a single evaluation by its ID.
4747
func (e *Evaluations) Info(evalID string, q *QueryOptions) (*Evaluation, *QueryMeta, error) {
4848
var resp Evaluation
49-
qm, err := e.client.query("/v1/evaluation/"+evalID, &resp, q)
49+
qm, err := e.client.query("/v1/evaluation/"+evalID+"?related=true", &resp, q)
5050
if err != nil {
5151
return nil, nil, err
5252
}

command/eval_status.go

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ Eval Status Options:
3636
Monitor an outstanding evaluation
3737
3838
-verbose
39-
Show full information.
39+
Show full-length IDs and exact timestamps.
4040
4141
-json
42-
Output the evaluation in its JSON format.
42+
Output the evaluation in its JSON format. This format will not include
43+
placed allocations.
4344
4445
-t
45-
Format and display evaluation using a Go template.
46+
Format and display evaluation using a Go template. This format will not
47+
include placed allocations.
4648
4749
-ui
4850
Open the evaluation in the browser.
@@ -73,10 +75,6 @@ func (c *EvalStatusCommand) AutocompleteArgs() complete.Predictor {
7375
return nil
7476
}
7577

76-
if err != nil {
77-
return nil
78-
}
79-
8078
resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Evals, nil)
8179
if err != nil {
8280
return []string{}
@@ -120,12 +118,6 @@ func (c *EvalStatusCommand) Run(args []string) int {
120118

121119
evalID := args[0]
122120

123-
// Truncate the id unless full length is requested
124-
length := shortId
125-
if verbose {
126-
length = fullId
127-
}
128-
129121
// Query the allocation info
130122
if len(evalID) == 1 {
131123
c.Ui.Error("Identifier must contain at least two characters.")
@@ -153,6 +145,12 @@ func (c *EvalStatusCommand) Run(args []string) int {
153145
return 1
154146
}
155147

148+
// Truncate the id unless full length is requested
149+
length := shortId
150+
if verbose {
151+
length = fullId
152+
}
153+
156154
// If we are in monitor mode, monitor and exit
157155
if monitor {
158156
mon := newMonitor(c.Ui, client, length)
@@ -178,6 +176,30 @@ func (c *EvalStatusCommand) Run(args []string) int {
178176
return 0
179177
}
180178

179+
placedAllocs, _, err := client.Evaluations().Allocations(eval.ID, nil)
180+
if err != nil {
181+
c.Ui.Error(fmt.Sprintf("Error querying related allocations: %s", err))
182+
return 1
183+
}
184+
185+
c.formatEvalStatus(eval, placedAllocs, verbose, length)
186+
187+
hint, _ := c.Meta.showUIPath(UIHintContext{
188+
Command: "eval status",
189+
PathParams: map[string]string{
190+
"evalID": eval.ID,
191+
},
192+
OpenURL: openURL,
193+
})
194+
if hint != "" {
195+
c.Ui.Warn(hint)
196+
}
197+
198+
return 0
199+
}
200+
201+
func (c *EvalStatusCommand) formatEvalStatus(eval *api.Evaluation, placedAllocs []*api.AllocationListStub, verbose bool, length int) {
202+
181203
failureString, failures := evalFailureStatus(eval)
182204
triggerNoun, triggerSubj := getTriggerDetails(eval)
183205
statusDesc := eval.StatusDescription
@@ -220,16 +242,27 @@ func (c *EvalStatusCommand) Run(args []string) int {
220242
basic = append(basic,
221243
fmt.Sprintf("Wait Until|%s", formatTime(eval.WaitUntil)))
222244
}
223-
224-
if verbose {
225-
// NextEval, PreviousEval, BlockedEval
245+
if eval.QuotaLimitReached != "" {
226246
basic = append(basic,
227-
fmt.Sprintf("Previous Eval|%s", eval.PreviousEval),
228-
fmt.Sprintf("Next Eval|%s", eval.NextEval),
229-
fmt.Sprintf("Blocked Eval|%s", eval.BlockedEval))
247+
fmt.Sprintf("Quota Limit Reached|%s", eval.QuotaLimitReached))
230248
}
249+
basic = append(basic,
250+
fmt.Sprintf("Previous Eval|%s", limit(eval.PreviousEval, length)),
251+
fmt.Sprintf("Next Eval|%s", limit(eval.NextEval, length)),
252+
fmt.Sprintf("Blocked Eval|%s", limit(eval.BlockedEval, length)),
253+
)
231254
c.Ui.Output(formatKV(basic))
232255

256+
if len(eval.RelatedEvals) > 0 {
257+
c.Ui.Output(c.Colorize().Color("\n[bold]Related Evaluations[reset]"))
258+
c.Ui.Output(formatRelatedEvalStubs(eval.RelatedEvals, length))
259+
}
260+
if len(placedAllocs) > 0 {
261+
c.Ui.Output(c.Colorize().Color("\n[bold]Placed Allocations[reset]"))
262+
allocsOut := formatAllocListStubs(placedAllocs, false, length)
263+
c.Ui.Output(allocsOut)
264+
}
265+
233266
if failures {
234267
c.Ui.Output(c.Colorize().Color("\n[bold]Failed Placements[reset]"))
235268
sorted := sortedTaskGroupFromMetrics(eval.FailedTGAllocs)
@@ -240,29 +273,18 @@ func (c *EvalStatusCommand) Run(args []string) int {
240273
if metrics.CoalescedFailures > 0 {
241274
noun += "s"
242275
}
243-
c.Ui.Output(fmt.Sprintf("Task Group %q (failed to place %d %s):", tg, metrics.CoalescedFailures+1, noun))
276+
c.Ui.Output(fmt.Sprintf("Task Group %q (failed to place %d %s):",
277+
tg, metrics.CoalescedFailures+1, noun))
244278
c.Ui.Output(formatAllocMetrics(metrics, false, " "))
245279
c.Ui.Output("")
246280
}
247281

248282
if eval.BlockedEval != "" {
249-
c.Ui.Output(fmt.Sprintf("Evaluation %q waiting for additional capacity to place remainder",
283+
c.Ui.Output(fmt.Sprintf(
284+
"Evaluation %q waiting for additional capacity to place remainder",
250285
limit(eval.BlockedEval, length)))
251286
}
252287
}
253-
254-
hint, _ := c.Meta.showUIPath(UIHintContext{
255-
Command: "eval status",
256-
PathParams: map[string]string{
257-
"evalID": eval.ID,
258-
},
259-
OpenURL: openURL,
260-
})
261-
if hint != "" {
262-
c.Ui.Warn(hint)
263-
}
264-
265-
return 0
266288
}
267289

268290
func sortedTaskGroupFromMetrics(groups map[string]*api.AllocationMetric) []string {
@@ -284,3 +306,20 @@ func getTriggerDetails(eval *api.Evaluation) (noun, subject string) {
284306
return "", ""
285307
}
286308
}
309+
310+
func formatRelatedEvalStubs(evals []*api.EvaluationStub, length int) string {
311+
out := make([]string, len(evals)+1)
312+
out[0] = "ID|Priority|Triggered By|Node ID|Status|Description"
313+
for i, eval := range evals {
314+
out[i+1] = fmt.Sprintf("%s|%d|%s|%s|%s|%s",
315+
limit(eval.ID, length),
316+
eval.Priority,
317+
eval.TriggeredBy,
318+
limit(eval.NodeID, length),
319+
eval.Status,
320+
eval.StatusDescription,
321+
)
322+
}
323+
324+
return formatList(out)
325+
}

command/eval_status_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ package command
66
import (
77
"strings"
88
"testing"
9+
"time"
910

1011
"github.com/hashicorp/cli"
12+
"github.com/hashicorp/nomad/api"
1113
"github.com/hashicorp/nomad/ci"
14+
"github.com/hashicorp/nomad/helper/pointer"
15+
"github.com/hashicorp/nomad/helper/uuid"
1216
"github.com/hashicorp/nomad/nomad/mock"
1317
"github.com/hashicorp/nomad/nomad/structs"
1418
"github.com/posener/complete"
@@ -88,3 +92,119 @@ func TestEvalStatusCommand_AutocompleteArgs(t *testing.T) {
8892
must.SliceLen(t, 1, res)
8993
must.Eq(t, e.ID, res[0])
9094
}
95+
96+
func TestEvalStatusCommand_Format(t *testing.T) {
97+
now := time.Now().UTC()
98+
ui := cli.NewMockUi()
99+
cmd := &EvalStatusCommand{Meta: Meta{Ui: ui}}
100+
101+
eval := &api.Evaluation{
102+
ID: uuid.Generate(),
103+
Priority: 50,
104+
Type: api.JobTypeService,
105+
TriggeredBy: structs.EvalTriggerAllocStop,
106+
Namespace: api.DefaultNamespace,
107+
JobID: "example",
108+
JobModifyIndex: 0,
109+
DeploymentID: uuid.Generate(),
110+
Status: api.EvalStatusComplete,
111+
StatusDescription: "complete",
112+
NextEval: "",
113+
PreviousEval: uuid.Generate(),
114+
BlockedEval: uuid.Generate(),
115+
RelatedEvals: []*api.EvaluationStub{{
116+
ID: uuid.Generate(),
117+
Priority: 50,
118+
Type: "service",
119+
TriggeredBy: "queued-allocs",
120+
Namespace: api.DefaultNamespace,
121+
JobID: "example",
122+
DeploymentID: "",
123+
Status: "pending",
124+
StatusDescription: "",
125+
WaitUntil: time.Time{},
126+
NextEval: "",
127+
PreviousEval: uuid.Generate(),
128+
BlockedEval: "",
129+
CreateIndex: 0,
130+
ModifyIndex: 0,
131+
CreateTime: 0,
132+
ModifyTime: 0,
133+
}},
134+
FailedTGAllocs: map[string]*api.AllocationMetric{"web": {
135+
NodesEvaluated: 6,
136+
NodesFiltered: 4,
137+
NodesInPool: 10,
138+
NodesAvailable: map[string]int{},
139+
ClassFiltered: map[string]int{},
140+
ConstraintFiltered: map[string]int{"${attr.kernel.name} = linux": 2},
141+
NodesExhausted: 2,
142+
ClassExhausted: map[string]int{},
143+
DimensionExhausted: map[string]int{"memory": 2},
144+
QuotaExhausted: []string{},
145+
ResourcesExhausted: map[string]*api.Resources{"web": {
146+
Cores: pointer.Of(3),
147+
}},
148+
Scores: map[string]float64{},
149+
AllocationTime: 0,
150+
CoalescedFailures: 0,
151+
ScoreMetaData: []*api.NodeScoreMeta{},
152+
}},
153+
ClassEligibility: map[string]bool{},
154+
EscapedComputedClass: true,
155+
QuotaLimitReached: "",
156+
QueuedAllocations: map[string]int{},
157+
SnapshotIndex: 1001,
158+
CreateIndex: 999,
159+
ModifyIndex: 1003,
160+
CreateTime: now.UnixNano(),
161+
ModifyTime: now.Add(time.Second).UnixNano(),
162+
}
163+
164+
placed := []*api.AllocationListStub{
165+
{
166+
ID: uuid.Generate(),
167+
NodeID: uuid.Generate(),
168+
TaskGroup: "web",
169+
DesiredStatus: "run",
170+
JobVersion: 2,
171+
ClientStatus: "running",
172+
CreateTime: now.Add(-10 * time.Second).UnixNano(),
173+
ModifyTime: now.Add(-2 * time.Second).UnixNano(),
174+
},
175+
{
176+
ID: uuid.Generate(),
177+
NodeID: uuid.Generate(),
178+
TaskGroup: "web",
179+
JobVersion: 2,
180+
DesiredStatus: "run",
181+
ClientStatus: "pending",
182+
CreateTime: now.Add(-3 * time.Second).UnixNano(),
183+
ModifyTime: now.Add(-1 * time.Second).UnixNano(),
184+
},
185+
{
186+
ID: uuid.Generate(),
187+
NodeID: uuid.Generate(),
188+
TaskGroup: "web",
189+
JobVersion: 2,
190+
DesiredStatus: "run",
191+
ClientStatus: "pending",
192+
CreateTime: now.Add(-4 * time.Second).UnixNano(),
193+
ModifyTime: now.UnixNano(),
194+
},
195+
}
196+
197+
cmd.formatEvalStatus(eval, placed, false, shortId)
198+
out := ui.OutputWriter.String()
199+
200+
// there isn't much logic here, so this is just a smoke test
201+
must.StrContains(t, out, `
202+
Failed Placements
203+
Task Group "web" (failed to place 1 allocation):
204+
* Constraint "${attr.kernel.name} = linux": 2 nodes excluded by filter
205+
* Resources exhausted on 2 nodes
206+
* Dimension "memory" exhausted on 2 nodes`)
207+
208+
must.StrContains(t, out, `Related Evaluations`)
209+
must.StrContains(t, out, `Placed Allocations`)
210+
}

0 commit comments

Comments
 (0)