fix(builder): truncate large error output to prevent log overflow
Build errors from nix can be very large (100k+ chars). This truncates error output to the first 50 and last 50 lines when it exceeds 100 lines, preventing journal and NATS message overflow. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const version = "0.2.0"
|
||||
const version = "0.2.1"
|
||||
|
||||
func main() {
|
||||
app := &cli.Command{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +19,18 @@ import (
|
||||
// Allows: alphanumeric, dashes, underscores, dots.
|
||||
var hostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||
|
||||
// truncateOutput truncates output to the first and last N lines if it exceeds 2*N lines.
|
||||
func truncateOutput(output string, keepLines int) string {
|
||||
lines := strings.Split(output, "\n")
|
||||
if len(lines) <= keepLines*2 {
|
||||
return output
|
||||
}
|
||||
head := lines[:keepLines]
|
||||
tail := lines[len(lines)-keepLines:]
|
||||
omitted := len(lines) - keepLines*2
|
||||
return strings.Join(head, "\n") + fmt.Sprintf("\n\n... (%d lines omitted) ...\n\n", omitted) + strings.Join(tail, "\n")
|
||||
}
|
||||
|
||||
// BuilderConfig holds the configuration for the builder.
|
||||
type BuilderConfig struct {
|
||||
NATSUrl string
|
||||
@@ -256,7 +269,7 @@ func (b *Builder) handleBuildRequest(subject string, data []byte) {
|
||||
DurationSeconds: hostDuration,
|
||||
}
|
||||
if !result.Success {
|
||||
hostResult.Error = result.Stderr
|
||||
hostResult.Error = truncateOutput(result.Stderr, 50)
|
||||
if hostResult.Error == "" && result.Error != nil {
|
||||
hostResult.Error = result.Error.Error()
|
||||
}
|
||||
|
||||
115
internal/builder/builder_test.go
Normal file
115
internal/builder/builder_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTruncateOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
keepLines int
|
||||
wantLines int
|
||||
wantOmit bool
|
||||
}{
|
||||
{
|
||||
name: "short output unchanged",
|
||||
input: "line1\nline2\nline3",
|
||||
keepLines: 50,
|
||||
wantLines: 3,
|
||||
wantOmit: false,
|
||||
},
|
||||
{
|
||||
name: "exactly at threshold unchanged",
|
||||
input: strings.Join(makeLines(100), "\n"),
|
||||
keepLines: 50,
|
||||
wantLines: 100,
|
||||
wantOmit: false,
|
||||
},
|
||||
{
|
||||
name: "over threshold truncated",
|
||||
input: strings.Join(makeLines(150), "\n"),
|
||||
keepLines: 50,
|
||||
wantLines: 103, // 50 + 1 (empty) + 1 (omitted msg) + 1 (empty) + 50
|
||||
wantOmit: true,
|
||||
},
|
||||
{
|
||||
name: "large output truncated",
|
||||
input: strings.Join(makeLines(1000), "\n"),
|
||||
keepLines: 50,
|
||||
wantLines: 103,
|
||||
wantOmit: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := truncateOutput(tt.input, tt.keepLines)
|
||||
gotLines := strings.Split(got, "\n")
|
||||
|
||||
if len(gotLines) != tt.wantLines {
|
||||
t.Errorf("got %d lines, want %d", len(gotLines), tt.wantLines)
|
||||
}
|
||||
|
||||
hasOmit := strings.Contains(got, "lines omitted")
|
||||
if hasOmit != tt.wantOmit {
|
||||
t.Errorf("got omit marker = %v, want %v", hasOmit, tt.wantOmit)
|
||||
}
|
||||
|
||||
if tt.wantOmit {
|
||||
// Verify first and last lines are preserved
|
||||
inputLines := strings.Split(tt.input, "\n")
|
||||
firstLine := inputLines[0]
|
||||
lastLine := inputLines[len(inputLines)-1]
|
||||
if !strings.HasPrefix(got, firstLine+"\n") {
|
||||
t.Errorf("first line not preserved, got prefix %q, want %q",
|
||||
gotLines[0], firstLine)
|
||||
}
|
||||
if !strings.HasSuffix(got, lastLine) {
|
||||
t.Errorf("last line not preserved, got suffix %q, want %q",
|
||||
gotLines[len(gotLines)-1], lastLine)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func makeLines(n int) []string {
|
||||
lines := make([]string, n)
|
||||
for i := range lines {
|
||||
lines[i] = "line " + strings.Repeat("x", i%80)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func TestTruncateOutputPreservesContent(t *testing.T) {
|
||||
// Create input with distinct first and last lines
|
||||
lines := make([]string, 200)
|
||||
for i := range lines {
|
||||
lines[i] = "middle"
|
||||
}
|
||||
lines[0] = "FIRST"
|
||||
lines[49] = "LAST_OF_HEAD"
|
||||
lines[150] = "FIRST_OF_TAIL"
|
||||
lines[199] = "LAST"
|
||||
|
||||
input := strings.Join(lines, "\n")
|
||||
got := truncateOutput(input, 50)
|
||||
|
||||
if !strings.Contains(got, "FIRST") {
|
||||
t.Error("missing FIRST")
|
||||
}
|
||||
if !strings.Contains(got, "LAST_OF_HEAD") {
|
||||
t.Error("missing LAST_OF_HEAD")
|
||||
}
|
||||
if !strings.Contains(got, "FIRST_OF_TAIL") {
|
||||
t.Error("missing FIRST_OF_TAIL")
|
||||
}
|
||||
if !strings.Contains(got, "LAST") {
|
||||
t.Error("missing LAST")
|
||||
}
|
||||
if !strings.Contains(got, "(100 lines omitted)") {
|
||||
t.Errorf("wrong omitted count, got: %s", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user