Skip to content

Commit f6d6dc7

Browse files
authored
Merge branch 'main' into TT-1956-Add-support-for-E2E-docker-tests-in-Flakeguard
2 parents a3d2b42 + 46b92fb commit f6d6dc7

File tree

10 files changed

+309
-1
lines changed

10 files changed

+309
-1
lines changed

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- [Test Configuration](./framework/test_configuration_overrides.md)
2121
- [Exposing Components](framework/components/state.md)
2222
- [Debugging Tests](framework/components/debug.md)
23+
- [Debugging CI Runs](framework/components/debug_ci.md)
2324
- [Debugging K8s Chaos Tests](framework/chaos/debug-k8s.md)
2425
- [Components Cleanup](framework/components/cleanup.md)
2526
- [Components Caching](framework/components/caching.md)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Debugging CI Runs
2+
3+
Combining test and container logs for debugging in CI can be cumbersome, so we’ve simplified the process with a command that downloads, unzips, and uploads them to a local Loki instance, enabling you to leverage the full power of LogQL.
4+
5+
Spin up the stack and upload some data
6+
```
7+
ctf obs u
8+
```
9+
10+
## Raw logs from GitHub step URL
11+
12+
Go to your test run and get the raw logs URL
13+
![raw-logs-url.png](raw-logs-url.png)
14+
15+
```
16+
ctf obs l -u "$your_url"
17+
```
18+
Click the resulting URL after upload is finished to open the filter
19+
20+
## Logs from GHA artifacts
21+
22+
Get the `Run ID` from GitHub UI, ex `actions/runs/$run_id`, download the artifact (in case of `CTFv2` it'd be just one dir), then run
23+
```
24+
gh run download $run_id
25+
ctf obs l -d $artifact_dir
26+
```
27+
Click the resulting URL after upload is finished to open the filter
28+
29+
88.4 KB
Loading

framework/.changeset/v0.5.3.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add logs upload from GHA to local Loki

framework/cmd/logs.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"encoding/json"
7+
"fmt"
8+
"github.com/pkg/errors"
9+
"github.com/smartcontractkit/chainlink-testing-framework/framework"
10+
"io"
11+
"net/http"
12+
"os"
13+
"path/filepath"
14+
"strings"
15+
"sync"
16+
"time"
17+
18+
"go.uber.org/ratelimit"
19+
)
20+
21+
var L = framework.L
22+
23+
type LokiPushRequest struct {
24+
Streams []LokiStream `json:"streams"`
25+
}
26+
27+
type LokiStream struct {
28+
Stream map[string]string `json:"stream"`
29+
Values [][2]string `json:"values"`
30+
}
31+
32+
const (
33+
lokiURL = "http://localhost:3030/loki/api/v1/push"
34+
grafanaURL = "http://localhost:3000/explore?panes=%7B%22V0P%22:%7B%22datasource%22:%22P8E80F9AEF21F6940%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%7Bjob%3D%5C%22"
35+
grafanaURL2 = "%5C%22%7D%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22P8E80F9AEF21F6940%22%7D,%22editorMode%22:%22code%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D%7D&schemaVersion=1&orgId=1"
36+
)
37+
38+
func processAndUploadDir(dirPath string, limiter ratelimit.Limiter, chunks int, jobID string) error {
39+
return filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
40+
if err != nil {
41+
return errors.Wrapf(err, "error accessing file: %s", path)
42+
}
43+
if info.IsDir() {
44+
return nil
45+
}
46+
L.Info().Msgf("Processing file: %s", path)
47+
f, err := os.Open(path)
48+
if err != nil {
49+
return errors.Wrapf(err, "error opening file: %s", path)
50+
}
51+
defer f.Close()
52+
53+
if err := processAndUploadLog(path, f, limiter, chunks, jobID); err != nil {
54+
return errors.Wrapf(err, "error processing file: %s", path)
55+
}
56+
return nil
57+
})
58+
}
59+
60+
func processAndUploadLog(source string, r io.Reader, limiter ratelimit.Limiter, chunks int, jobID string) error {
61+
scanner := bufio.NewScanner(r)
62+
var values [][2]string
63+
baseTime := time.Now()
64+
65+
// Read all log lines; each line gets a unique timestamp.
66+
for scanner.Scan() {
67+
line := scanner.Text()
68+
ts := baseTime.UnixNano()
69+
values = append(values, [2]string{fmt.Sprintf("%d", ts), line})
70+
baseTime = baseTime.Add(time.Nanosecond)
71+
}
72+
if err := scanner.Err(); err != nil {
73+
return fmt.Errorf("error scanning logs from %s: %w", source, err)
74+
}
75+
76+
totalLines := len(values)
77+
if totalLines == 0 {
78+
L.Info().Msgf("No log lines found in %s", source)
79+
return nil
80+
}
81+
// Some logs may include CL node logs, skip chunking for all that is less
82+
if totalLines <= 10000 {
83+
chunks = 1
84+
}
85+
if chunks > totalLines {
86+
chunks = totalLines
87+
}
88+
chunkSize := totalLines / chunks
89+
remainder := totalLines % chunks
90+
L.Debug().Int("total_lines", totalLines).
91+
Int("chunks", chunks).
92+
Msgf("Starting chunk processing for %s", source)
93+
var wg sync.WaitGroup
94+
errCh := make(chan error, chunks)
95+
start := 0
96+
for i := 0; i < chunks; i++ {
97+
extra := 0
98+
if i < remainder {
99+
extra = 1
100+
}
101+
end := start + chunkSize + extra
102+
chunkValues := values[start:end]
103+
startLine := start + 1
104+
endLine := end
105+
start = end
106+
107+
labels := map[string]string{
108+
"job": jobID,
109+
"chunk": fmt.Sprintf("%d", i+1),
110+
"source": source,
111+
}
112+
reqBody := LokiPushRequest{
113+
Streams: []LokiStream{
114+
{
115+
Stream: labels,
116+
Values: chunkValues,
117+
},
118+
},
119+
}
120+
data, err := json.Marshal(reqBody)
121+
if err != nil {
122+
return fmt.Errorf("error marshaling JSON for chunk %d: %w", i+1, err)
123+
}
124+
chunkMB := float64(len(data)) / (1024 * 1024)
125+
L.Debug().Int("chunk", i+1).
126+
Float64("chunk_size_MB", chunkMB).
127+
Int("start_line", startLine).
128+
Int("end_line", endLine).
129+
Msg("Prepared chunk for upload")
130+
131+
wg.Add(1)
132+
go func(chunkNum, sLine, eLine int, payload []byte, sizeMB float64) {
133+
defer wg.Done()
134+
const maxRetries = 50
135+
const retryDelay = 1 * time.Second
136+
137+
var resp *http.Response
138+
var attempt int
139+
var err error
140+
for attempt = 1; attempt <= maxRetries; attempt++ {
141+
limiter.Take()
142+
resp, err = http.Post(lokiURL, "application/json", bytes.NewReader(payload))
143+
if err != nil {
144+
if strings.Contains(err.Error(), "connection refused") {
145+
L.Fatal().Msg("connection refused, is local Loki up and running? use 'ctf obs u'")
146+
return
147+
}
148+
L.Error().Err(err).
149+
Int("status", resp.StatusCode).
150+
Int("attempt", attempt).
151+
Int("chunk", chunkNum).
152+
Float64("chunk_size_MB", sizeMB).
153+
Msg("Error sending POST request")
154+
time.Sleep(retryDelay)
155+
continue
156+
}
157+
158+
body, _ := io.ReadAll(resp.Body)
159+
resp.Body.Close()
160+
161+
if resp.StatusCode == 429 {
162+
L.Debug().Int("attempt", attempt).
163+
Int("chunk", chunkNum).
164+
Float64("chunk_size_MB", sizeMB).
165+
Msg("Received 429, retrying...")
166+
time.Sleep(retryDelay)
167+
continue
168+
}
169+
170+
if resp.StatusCode/100 != 2 {
171+
err = fmt.Errorf("loki error: %s - %s", resp.Status, body)
172+
L.Error().Err(err).Int("chunk", chunkNum).
173+
Float64("chunk_size_MB", sizeMB).
174+
Msg("Chunk upload failed")
175+
time.Sleep(retryDelay)
176+
continue
177+
}
178+
179+
L.Info().Int("chunk", chunkNum).
180+
Float64("chunk_size_MB", sizeMB).
181+
Msg("Successfully uploaded chunk")
182+
return
183+
}
184+
errCh <- fmt.Errorf("max retries reached for chunk %d; last error: %v", chunkNum, err)
185+
}(i+1, startLine, endLine, data, chunkMB)
186+
}
187+
188+
wg.Wait()
189+
close(errCh)
190+
if len(errCh) > 0 {
191+
return <-errCh
192+
}
193+
194+
return nil
195+
}

framework/cmd/main.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,44 @@ func main() {
118118
Description: "Removes local observability stack",
119119
Action: func(c *cli.Context) error { return observabilityDown() },
120120
},
121+
{
122+
Name: "load",
123+
Usage: "ctf obs l",
124+
Aliases: []string{"l"},
125+
Description: "Loads logs to Loki",
126+
Flags: []cli.Flag{
127+
&cli.StringFlag{
128+
Name: "raw-url",
129+
Aliases: []string{"u"},
130+
Usage: "URL to GitHub raw log data",
131+
},
132+
&cli.StringFlag{
133+
Name: "dir",
134+
Aliases: []string{"d"},
135+
Usage: "Directory to logs, output of 'gh run download $run_id'",
136+
},
137+
&cli.IntFlag{
138+
Name: "rps",
139+
Aliases: []string{"r"},
140+
Usage: "RPS for uploading log chunks",
141+
Value: 30,
142+
},
143+
&cli.IntFlag{
144+
Name: "chunk",
145+
Aliases: []string{"c"},
146+
Usage: "Amount of chunks the files will be split in",
147+
Value: 100,
148+
},
149+
},
150+
Action: func(c *cli.Context) error {
151+
return loadLogs(
152+
c.String("raw-url"),
153+
c.String("dir"),
154+
c.Int("rps"),
155+
c.Int("chunk"),
156+
)
157+
},
158+
},
121159
},
122160
},
123161
{

framework/cmd/observability.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,45 @@ package main
22

33
import (
44
"fmt"
5+
"github.com/google/uuid"
6+
"github.com/pkg/errors"
57
"github.com/smartcontractkit/chainlink-testing-framework/framework"
8+
"go.uber.org/ratelimit"
9+
"net/http"
610
)
711

12+
func loadLogs(rawURL, dirPath string, rps, chunks int) error {
13+
if rawURL == "" && dirPath == "" {
14+
return fmt.Errorf("at least one source must be provided, either -u $url or -d $dir")
15+
}
16+
jobID := uuid.New().String()[0:5]
17+
framework.L.Info().Str("JobID", jobID).Msg("Loading logs into Loki")
18+
limiter := ratelimit.New(rps)
19+
if rawURL != "" {
20+
L.Info().Msg("Downloading raw logs from URL")
21+
resp, err := http.Get(rawURL)
22+
if err != nil {
23+
return errors.Wrap(err, "error downloading raw logs")
24+
}
25+
defer resp.Body.Close()
26+
27+
if resp.StatusCode/100 != 2 {
28+
return fmt.Errorf("non-success response code when downloading raw logs: %s", resp.Status)
29+
}
30+
31+
if err := processAndUploadLog(rawURL, resp.Body, limiter, chunks, jobID); err != nil {
32+
return errors.Wrap(err, "error processing raw logs")
33+
}
34+
} else if dirPath != "" {
35+
L.Info().Msgf("Processing directory: %s", dirPath)
36+
if err := processAndUploadDir(dirPath, limiter, chunks, jobID); err != nil {
37+
return errors.Wrapf(err, "error processing directory: %s", dirPath)
38+
}
39+
}
40+
framework.L.Info().Str("JobID", jobID).Str("URL", grafanaURL+jobID+grafanaURL2).Msg("Upload complete")
41+
return nil
42+
}
43+
844
func observabilityUp() error {
945
framework.L.Info().Msg("Creating local observability stack")
1046
if err := extractAllFiles("observability"); err != nil {

framework/examples/myproject/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ require (
1818
github.com/go-resty/resty/v2 v2.15.3
1919
github.com/google/uuid v1.6.0
2020
github.com/rs/zerolog v1.33.0
21-
github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.8
21+
github.com/smartcontractkit/chainlink-testing-framework/framework v0.5.2
2222
github.com/smartcontractkit/chainlink-testing-framework/havoc v1.50.2
2323
github.com/smartcontractkit/chainlink-testing-framework/seth v1.50.10
2424
github.com/smartcontractkit/chainlink-testing-framework/wasp v1.50.2

framework/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ require (
2525
github.com/stretchr/testify v1.9.0
2626
github.com/testcontainers/testcontainers-go v0.35.0
2727
github.com/urfave/cli/v2 v2.27.5
28+
go.uber.org/ratelimit v0.3.1
2829
golang.org/x/sync v0.10.0
2930
gopkg.in/guregu/null.v4 v4.0.0
3031
)
@@ -48,6 +49,7 @@ require (
4849
github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 // indirect
4950
github.com/aws/smithy-go v1.21.0 // indirect
5051
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
52+
github.com/benbjohnson/clock v1.3.0 // indirect
5153
github.com/bits-and-blooms/bitset v1.13.0 // indirect
5254
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
5355
github.com/bytedance/sonic v1.12.3 // indirect

framework/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA=
4848
github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
4949
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
5050
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
51+
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
52+
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
5153
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
5254
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
5355
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
@@ -415,6 +417,10 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
415417
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
416418
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
417419
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
420+
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
421+
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
422+
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
423+
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
418424
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
419425
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
420426
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

0 commit comments

Comments
 (0)