Skip to content

Commit d2b5f46

Browse files
authored
Merge pull request #22 from buildkite/catkins/process-job-logs
2 parents 0feaf56 + 0bee574 commit d2b5f46

7 files changed

Lines changed: 532 additions & 9 deletions

File tree

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ go 1.24.2
55
require (
66
github.com/alecthomas/kong v1.10.0
77
github.com/buildkite/go-buildkite/v4 v4.1.0
8+
github.com/buildkite/terminal-to-html/v3 v3.16.8
9+
github.com/huantt/plaintext-extractor v1.1.0
810
github.com/mark3labs/mcp-go v0.20.1
911
github.com/rs/zerolog v1.34.0
1012
github.com/stretchr/testify v1.9.0
@@ -19,7 +21,7 @@ require (
1921
github.com/mattn/go-isatty v0.0.20 // indirect
2022
github.com/pmezard/go-difflib v1.0.0 // indirect
2123
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
22-
golang.org/x/net v0.38.0 // indirect
24+
golang.org/x/net v0.39.0 // indirect
2325
golang.org/x/sys v0.32.0 // indirect
2426
gopkg.in/yaml.v3 v3.0.1 // indirect
2527
)

go.sum

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,24 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
66
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
77
github.com/buildkite/go-buildkite/v4 v4.1.0 h1:n1f3EAe8/64ju9QjSWJYMjD8++mn1pOLK/tZscD5DKo=
88
github.com/buildkite/go-buildkite/v4 v4.1.0/go.mod h1:xlYVIETMCk46KUkmfRoztoIf888KwdY5uZXNinZ1PX0=
9+
github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc=
10+
github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM=
911
github.com/cenkalti/backoff v1.1.1-0.20171020064038-309aa717adbf h1:yxlp0s+Sge9UsKEK0Bsvjiopb9XRk+vxylmZ9eGBfm8=
1012
github.com/cenkalti/backoff v1.1.1-0.20171020064038-309aa717adbf/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
1113
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
1214
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1315
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1416
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
15-
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
16-
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
17+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
18+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
1719
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
1820
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
1921
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2022
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
2123
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
2224
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
25+
github.com/huantt/plaintext-extractor v1.1.0 h1:dZkJN0fGZf1o8x9UdR6hHqkZnqIwX94YlGJ/lSXUZ5c=
26+
github.com/huantt/plaintext-extractor v1.1.0/go.mod h1:zIIbG/hZnsnLgzDbZ2T8fOrA4SLGWCoHWWYZo0Anx9c=
2327
github.com/mark3labs/mcp-go v0.20.1 h1:E1Bbx9K8d8kQmDZ1QHblM38c7UU2evQ2LlkANk1U/zw=
2428
github.com/mark3labs/mcp-go v0.20.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
2529
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -39,8 +43,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
3943
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
4044
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
4145
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
42-
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
43-
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
46+
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
47+
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
4448
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4549
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4650
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

internal/buildkite/job_logs.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package buildkite
22

33
import (
44
"context"
5-
"encoding/json"
65
"fmt"
76
"io"
87
"net/http"
98

9+
"github.com/buildkite/buildkite-mcp-server/internal/buildkite/joblogs"
1010
"github.com/buildkite/go-buildkite/v4"
1111
"github.com/mark3labs/mcp-go/mcp"
1212
"github.com/mark3labs/mcp-go/server"
@@ -66,11 +66,13 @@ func GetJobLogs(ctx context.Context, client *buildkite.Client) (tool mcp.Tool, h
6666
return mcp.NewToolResultError(fmt.Sprintf("failed to get issue: %s", string(body))), nil
6767
}
6868

69-
r, err := json.Marshal(joblog)
69+
// the default logs that come from the API can be pretty dense with ANSI codes or HTML
70+
// so we can strip that out before returning it to the LLM
71+
processedLog, err := joblogs.Process(joblog)
7072
if err != nil {
71-
return nil, fmt.Errorf("failed to marshal job logs: %w", err)
73+
return nil, fmt.Errorf("failed to process job log: %w", err)
7274
}
7375

74-
return mcp.NewToolResultText(string(r)), nil
76+
return mcp.NewToolResultText(processedLog), nil
7577
}
7678
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package joblogs
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/buildkite/go-buildkite/v4"
9+
"github.com/buildkite/terminal-to-html/v3"
10+
"github.com/huantt/plaintext-extractor"
11+
)
12+
13+
var timeTagRegexp = regexp.MustCompile(`<time[^>]*>.*?</time>`)
14+
15+
// Process accepts job logs from the Buildkite API and strips out formatting
16+
// to reduce the number of tokens sent to the LLM
17+
func Process(jobLog buildkite.JobLog) (string, error) {
18+
screen, err := terminal.NewScreen()
19+
if err != nil {
20+
return "", fmt.Errorf("failed to create terminal screen: %w", err)
21+
}
22+
23+
if _, err = screen.Write([]byte(jobLog.Content)); err != nil {
24+
return "", fmt.Errorf("failed to write to terminal screen: %w", err)
25+
}
26+
27+
html := screen.AsHTML()
28+
output := strings.Builder{}
29+
extractor := plaintext.NewHtmlExtractor()
30+
31+
for line := range strings.Lines(html) {
32+
// remove timestamps to save a few more tokens
33+
line = timeTagRegexp.ReplaceAllString(line, "")
34+
35+
plainText, err := extractor.PlainText(line)
36+
if err != nil {
37+
return "", fmt.Errorf("failed to extract plain text: %w", err)
38+
}
39+
40+
if plainText == nil {
41+
empty := ""
42+
plainText = &empty
43+
}
44+
45+
output.WriteString(*plainText + "\n")
46+
}
47+
48+
return output.String(), nil
49+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package joblogs
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/buildkite/go-buildkite/v4"
8+
)
9+
10+
func TestProcess(t *testing.T) {
11+
rawLog, err := os.ReadFile("testdata/bash-example.log")
12+
if err != nil {
13+
t.Fatalf("failed to read test log file: %v", err)
14+
}
15+
16+
jobLog := buildkite.JobLog{Content: string(rawLog)}
17+
18+
// Process the job log
19+
processedLog, err := Process(jobLog)
20+
if err != nil {
21+
t.Fatalf("expected no error, got %v", err)
22+
}
23+
24+
// write out golden file if WRITE_JOB_LOG_GOLDEN_FILE is set
25+
if os.Getenv("WRITE_JOB_LOG_GOLDEN_FILE") != "" {
26+
err = os.WriteFile("testdata/processed.log", []byte(processedLog), 0644)
27+
if err != nil {
28+
t.Fatalf("failed to write processed log golden file: %v", err)
29+
}
30+
}
31+
32+
expectedLog, err := os.ReadFile("testdata/processed.log")
33+
if err != nil {
34+
t.Fatalf("failed to read processed log golden file: %v", err)
35+
}
36+
37+
// Check if the processed log is as expected
38+
if processedLog != string(expectedLog) {
39+
t.Fatalf("expected %q, got %q", expectedLog, processedLog)
40+
}
41+
}

0 commit comments

Comments
 (0)