Skip to content

Commit 8a67fc4

Browse files
authored
Extended build failure logging (#13705)
1 parent 49e7d7d commit 8a67fc4

10 files changed

Lines changed: 987 additions & 19 deletions

File tree

toolkit/tools/go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
1111
github.com/bendahl/uinput v1.4.0
1212
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e
13+
github.com/ddddddO/gtree v1.10.7
1314
github.com/fatih/color v1.16.0
1415
github.com/gdamore/tcell v1.4.0
1516
github.com/google/uuid v1.6.0
@@ -44,12 +45,14 @@ require (
4445
github.com/mattn/go-colorable v0.1.13 // indirect
4546
github.com/mattn/go-isatty v0.0.20 // indirect
4647
github.com/mattn/go-runewidth v0.0.7 // indirect
48+
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
4749
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
4850
github.com/pmezard/go-difflib v1.0.0 // indirect
4951
github.com/rivo/uniseg v0.1.0 // indirect
50-
github.com/xrash/smetrics v0.0.0-20170218160415-a3153f7040e9 // indirect
52+
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
5153
golang.org/x/crypto v0.31.0 // indirect
5254
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
5355
golang.org/x/net v0.33.0 // indirect
56+
golang.org/x/sync v0.10.0 // indirect
5457
golang.org/x/text v0.21.0 // indirect
5558
)

toolkit/tools/go.sum

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oD
2626
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2727
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2828
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
29+
github.com/ddddddO/gtree v1.10.7 h1:bQ2kHOAdVjnXZ2PXiWeItjw5Uwa/4H1PCSMB56v7dkY=
30+
github.com/ddddddO/gtree v1.10.7/go.mod h1:1VqbPuR6Wyv8nwtk+WT4TuXV82oA67QzsXMw2IVc41s=
2931
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
3032
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
3133
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
@@ -69,6 +71,8 @@ github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vyg
6971
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
7072
github.com/muesli/crunchy v0.4.0 h1:qdiml8gywULHBsztiSAf6rrE6EyuNasNKZ104mAaahM=
7173
github.com/muesli/crunchy v0.4.0/go.mod h1:9k4x6xdSbb7WwtAVy0iDjaiDjIk6Wa5AgUIqp+HqOpU=
74+
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
75+
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
7276
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
7377
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
7478
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -82,20 +86,30 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
8286
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
8387
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
8488
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
89+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
90+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
8591
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
8692
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
93+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
94+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
95+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
8796
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
8897
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
8998
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
9099
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
91-
github.com/xrash/smetrics v0.0.0-20170218160415-a3153f7040e9 h1:w8V9v0qVympSF6GjdjIyeqR7+EVhAF9CBQmkmW7Zw0w=
92100
github.com/xrash/smetrics v0.0.0-20170218160415-a3153f7040e9/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
101+
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
102+
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
103+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
104+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
93105
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
94106
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
95107
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
96108
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
97109
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
98110
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
111+
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
112+
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
99113
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
100114
golang.org/x/sys v0.0.0-20191018095205-727590c5006e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
101115
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package pkggraph
5+
6+
import (
7+
"fmt"
8+
"io"
9+
"strings"
10+
11+
"github.com/ddddddO/gtree"
12+
"github.com/microsoft/azurelinux/toolkit/tools/internal/logger"
13+
"github.com/sirupsen/logrus"
14+
)
15+
16+
const seenNodeSuffix = "... [SEEN]"
17+
18+
// GraphPrinter is a type meant to print a graph in a human-readable format.
19+
// It uses a depth-first search (DFS) algorithm to traverse the graph and print each node.
20+
// The printer ignores any cycles in the graph and thus turns the graph into a tree.
21+
// We use the gtree package to print the tree structure.
22+
// See NewGraphPrinter for more details on how to customize the printer.
23+
//
24+
// Example:
25+
//
26+
// Graph structure:
27+
//
28+
// A -> B
29+
// A -> E
30+
// B -> C
31+
// B -> D
32+
// D -> A (loop)
33+
//
34+
// Output starting from 'A':
35+
// A
36+
// ├── B
37+
// │ ├── C
38+
// │ └── D
39+
// └── E
40+
type GraphPrinter struct {
41+
output io.Writer
42+
printNodesOnce bool
43+
}
44+
45+
type graphPrinterModifier func(*GraphPrinter)
46+
47+
// loggerOutputWrapper is a wrapper around logrus.Logger to implement the io.Writer interface.
48+
type loggerOutputWrapper struct {
49+
logLevel logrus.Level
50+
}
51+
52+
// gTreeBuilder helps build a 'gtree' package's tree from a graph.
53+
type gTreeBuilder struct {
54+
treeRoot *gtree.Node
55+
seenNodes map[*PkgNode]bool
56+
printNodesOnce bool
57+
}
58+
59+
// NewGraphPrinter creates a new GraphPrinter.
60+
// It accepts a variadic number of 'GraphPrinter*' modifiers to customize the printer's behavior.
61+
// The default settings are:
62+
// - output: logrus logger on debug level
63+
// - printNodesOnce: false
64+
func NewGraphPrinter(modifiers ...graphPrinterModifier) GraphPrinter {
65+
printer := GraphPrinter{
66+
output: loggerOutputWrapper{
67+
logLevel: logrus.DebugLevel,
68+
},
69+
printNodesOnce: false,
70+
}
71+
72+
for _, modifier := range modifiers {
73+
if modifier != nil {
74+
modifier(&printer)
75+
}
76+
}
77+
78+
return printer
79+
}
80+
81+
// GraphPrinterOutput is a config modifier passed to the graph printer's constructor
82+
// to define the output writer for the graph printer.
83+
func GraphPrinterOutput(output io.Writer) graphPrinterModifier {
84+
return func(g *GraphPrinter) {
85+
g.output = output
86+
}
87+
}
88+
89+
// GraphPrinterLogOutput is a config modifier passed to the graph printer's constructor
90+
// making the printer's output be logged at the specified log level.
91+
func GraphPrinterLogOutput(logLevel logrus.Level) graphPrinterModifier {
92+
return func(g *GraphPrinter) {
93+
g.output = loggerOutputWrapper{
94+
logLevel: logLevel,
95+
}
96+
}
97+
}
98+
99+
// GraphPrinterPrintOnce is a config modifier passed to the graph printer's constructor
100+
// to define whether the printer should print each node only once.
101+
func GraphPrinterPrintNodesOnce() graphPrinterModifier {
102+
return func(g *GraphPrinter) {
103+
g.printNodesOnce = true
104+
}
105+
}
106+
107+
// Print prints the graph. It ignores any cycles in the graph turning the graph into a tree.
108+
// It then uses the 'gtree' package to print the tree structure.
109+
func (g GraphPrinter) Print(graph *PkgGraph, rootNode *PkgNode) error {
110+
if graph == nil {
111+
return fmt.Errorf("graph is nil")
112+
}
113+
114+
if rootNode == nil {
115+
return fmt.Errorf("root node is nil")
116+
}
117+
118+
if !graph.HasNode(rootNode) {
119+
return fmt.Errorf("root node '%s' not found in the graph", rootNode.FriendlyName())
120+
}
121+
122+
treeBuilder := newGTreeBuilder(g.printNodesOnce)
123+
treeRoot, err := treeBuilder.buildTree(graph, rootNode)
124+
if err != nil {
125+
return fmt.Errorf("failed to build tree:\n%w", err)
126+
}
127+
128+
err = gtree.OutputProgrammably(g.output, treeRoot)
129+
if err != nil {
130+
return fmt.Errorf("failed to print tree:\n%w", err)
131+
}
132+
133+
return nil
134+
}
135+
136+
// Write implements the io.Writer interface.
137+
func (l loggerOutputWrapper) Write(bytes []byte) (int, error) {
138+
// Remove the trailing newline character from the log message,
139+
// as it's unnecessary when logging.
140+
line := strings.TrimSuffix(string(bytes), "\n")
141+
logger.Log.Log(l.logLevel, line)
142+
return len(bytes), nil
143+
}
144+
145+
// newGTreeBuilder creates a new gTreeBuilder instance with the specified
146+
// configuration for printing nodes once.
147+
func newGTreeBuilder(printNodesOnce bool) *gTreeBuilder {
148+
result := &gTreeBuilder{
149+
printNodesOnce: printNodesOnce,
150+
}
151+
result.resetState()
152+
return result
153+
}
154+
155+
// buildTree traverses the graph and constructs a tree representation.
156+
func (t *gTreeBuilder) buildTree(graph *PkgGraph, rootNode *PkgNode) (*gtree.Node, error) {
157+
if graph == nil {
158+
return nil, fmt.Errorf("graph is nil")
159+
}
160+
161+
if rootNode == nil {
162+
return nil, fmt.Errorf("root node is nil")
163+
}
164+
165+
t.resetState()
166+
t.buildTreeWithDFS(nil, rootNode, graph)
167+
168+
return t.treeRoot, nil
169+
}
170+
171+
func (tb *gTreeBuilder) resetState() {
172+
tb.seenNodes = make(map[*PkgNode]bool)
173+
tb.treeRoot = nil
174+
}
175+
176+
// buildTreeWithDFS builds the tree using depth-first search.
177+
// It converts a general graph into a tree structure.
178+
// It uses a map to keep track of seen nodes to avoid cycles.
179+
func (t *gTreeBuilder) buildTreeWithDFS(treeParent *gtree.Node, pkgNode *PkgNode, graph *PkgGraph) {
180+
if pkgNode == nil {
181+
return
182+
}
183+
184+
// We add a node before the "seen" check because we always
185+
// want to add the node to the tree. If it's been seen before,
186+
// we just mark it as seen as part of the text displayed for the node.
187+
treeNode := t.buildTreeNode(treeParent, pkgNode)
188+
189+
if t.seenNodes[pkgNode] {
190+
return
191+
}
192+
193+
t.seenNodes[pkgNode] = true
194+
195+
children := graph.From(pkgNode.ID())
196+
for children.Next() {
197+
t.buildTreeWithDFS(treeNode, children.Node().(*PkgNode), graph)
198+
}
199+
200+
// As we traverse the graph back up, setting the node as unseen
201+
// allows us to print it again if we encounter it later
202+
// in a DIFFERENT branch of the tree.
203+
if !t.printNodesOnce {
204+
t.seenNodes[pkgNode] = false
205+
}
206+
}
207+
208+
// buildTreeNode creates a new tree node and adds it to the parent
209+
// or sets it as the root if the parent is nil.
210+
func (t *gTreeBuilder) buildTreeNode(treeParent *gtree.Node, pkgNode *PkgNode) *gtree.Node {
211+
nodeText := t.buildNodeText(pkgNode)
212+
if treeParent == nil {
213+
t.treeRoot = gtree.NewRoot(nodeText)
214+
return t.treeRoot
215+
}
216+
return treeParent.Add(nodeText)
217+
}
218+
219+
// buildNodeText formats the node text based on whether it's been seen before.
220+
func (t *gTreeBuilder) buildNodeText(pkgNode *PkgNode) string {
221+
if t.seenNodes[pkgNode] {
222+
return pkgNode.FriendlyName() + seenNodeSuffix
223+
}
224+
return pkgNode.FriendlyName()
225+
}

0 commit comments

Comments
 (0)