Skip to content

Commit 40e3541

Browse files
committed
add functions for obs that do not depend on embedded files
1 parent e27525c commit 40e3541

File tree

3 files changed

+229
-10
lines changed

3 files changed

+229
-10
lines changed

framework/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ require (
125125
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
126126
github.com/google/btree v1.1.3 // indirect
127127
github.com/google/gnostic-models v0.6.8 // indirect
128+
github.com/google/go-github/v72 v72.0.0 // indirect
129+
github.com/google/go-querystring v1.1.0 // indirect
128130
github.com/google/gofuzz v1.2.0 // indirect
129131
github.com/google/s2a-go v0.1.9 // indirect
130132
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect

framework/go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,15 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
332332
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
333333
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
334334
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
335+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
335336
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
336337
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
337338
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
338339
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
339340
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
340341
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
342+
github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM=
343+
github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg=
341344
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
342345
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
343346
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

framework/observability.go

Lines changed: 224 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package framework
22

33
import (
4+
"context"
45
"embed"
56
"fmt"
67
"io/fs"
78
"os"
89
"path/filepath"
910
"strings"
11+
"time"
12+
13+
"github.com/google/go-github/v72/github"
1014
)
1115

1216
//go:embed observability/*
@@ -22,18 +26,189 @@ const (
2226
LocalPrometheusURL = "http://localhost:3000/explore?panes=%7B%22qZw%22:%7B%22datasource%22:%22PBFA97CFB590B2093%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%22,%22range%22:true,%22datasource%22:%7B%22type%22:%22prometheus%22,%22uid%22:%22PBFA97CFB590B2093%22%7D%7D%5D,%22range%22:%7B%22from%22:%22now-15m%22,%22to%22:%22now%22%7D%7D%7D&schemaVersion=1&orgId=1"
2327
LocalPostgresDebugURL = "http://localhost:3000/d/000000039/postgresql-database?orgId=1&refresh=5s&var-DS_PROMETHEUS=PBFA97CFB590B2093&var-interval=$__auto_interval_interval&var-namespace=&var-release=&var-instance=postgres_exporter_0:9187&var-datname=All&var-mode=All&from=now-15m&to=now"
2428
LocalPyroScopeURL = "http://localhost:4040/?query=process_cpu%3Acpu%3Ananoseconds%3Acpu%3Ananoseconds%7Bservice_name%3D%22chainlink-node%22%7D&from=now-15m"
29+
30+
CTFCacheDir = ".local/share/ctf"
31+
DefaultGitHubOwner = "smartcontractkit"
32+
DefaultGitHubRepo = "chainlink-testing-framework"
33+
DefaultObservabilityPath = "framework/observability"
2534
)
2635

36+
// resolveObservabilitySource determines where to load observability files from based on the source parameter
37+
// - Empty string: use embedded files
38+
// - file:// prefix: use local filesystem
39+
// - http(s):// prefix: download and cache from remote URL
40+
func resolveObservabilitySource(source string) (fs.FS, string, error) {
41+
if source == "" {
42+
// Default: use embedded files
43+
return EmbeddedObservabilityFiles, "observability", nil
44+
}
45+
46+
if strings.HasPrefix(source, "file://") {
47+
// Local filesystem path
48+
localPath := strings.TrimPrefix(source, "file://")
49+
if _, err := os.Stat(localPath); err != nil {
50+
return nil, "", fmt.Errorf("local observability path does not exist: %s: %w", localPath, err)
51+
}
52+
return os.DirFS(localPath), ".", nil
53+
}
54+
55+
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
56+
// Remote URL: download and cache
57+
cachePath, err := downloadAndCacheObservabilityFiles(source)
58+
if err != nil {
59+
return nil, "", fmt.Errorf("failed to download observability files: %w", err)
60+
}
61+
return os.DirFS(cachePath), ".", nil
62+
}
63+
64+
return nil, "", fmt.Errorf("invalid source format: %s (must be empty, file://, or http(s)://)", source)
65+
}
66+
67+
// downloadAndCacheObservabilityFiles downloads observability files from a GitHub URL and caches them
68+
func downloadAndCacheObservabilityFiles(url string) (string, error) {
69+
// Parse URL to extract repo info and create cache key
70+
// Expected format: https://github.com/owner/repo/tree/ref/path/to/observability
71+
owner, repo, ref, path, err := parseGitHubURL(url)
72+
if err != nil {
73+
return "", err
74+
}
75+
76+
// Create cache directory using just the ref
77+
homeDir, err := os.UserHomeDir()
78+
if err != nil {
79+
return "", fmt.Errorf("failed to get home directory: %w", err)
80+
}
81+
cachedPath := filepath.Join(homeDir, CTFCacheDir, "observability", ref)
82+
83+
// Check if already cached and has content
84+
if info, err := os.Stat(cachedPath); err == nil && info.IsDir() {
85+
// Verify the cache directory has files
86+
entries, err := os.ReadDir(cachedPath)
87+
if err == nil && len(entries) > 0 {
88+
L.Debug().Msgf("Using cached observability files from %s", cachedPath)
89+
return cachedPath, nil
90+
}
91+
L.Debug().Msg("Cache directory exists but is empty, re-downloading")
92+
}
93+
94+
L.Info().Msgf("Downloading observability files from GitHub: %s/%s@%s (path: %s)", owner, repo, ref, path)
95+
96+
// Create GitHub client with optional authentication and timeout context
97+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
98+
defer cancel()
99+
100+
var client *github.Client
101+
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
102+
L.Debug().Msg("Using authenticated GitHub client")
103+
client = github.NewClient(nil).WithAuthToken(token)
104+
} else {
105+
L.Debug().Msg("Using unauthenticated GitHub client")
106+
client = github.NewClient(nil)
107+
}
108+
109+
// Download directory contents recursively
110+
if err := downloadDirectoryRecursive(ctx, client, owner, repo, ref, path, cachedPath); err != nil {
111+
return "", fmt.Errorf("failed to download directory: %w", err)
112+
}
113+
114+
L.Info().Msgf("Observability files cached at: %s", cachedPath)
115+
return cachedPath, nil
116+
}
117+
118+
// parseGitHubURL parses a GitHub URL and extracts owner, repo, ref, and path
119+
func parseGitHubURL(url string) (owner, repo, ref, path string, err error) {
120+
// Expected format: https://github.com/owner/repo/tree/ref/path/to/observability
121+
url = strings.TrimPrefix(url, "https://github.com/")
122+
url = strings.TrimPrefix(url, "http://github.com/")
123+
parts := strings.Split(url, "/")
124+
125+
if len(parts) < 5 {
126+
return "", "", "", "", fmt.Errorf("invalid GitHub URL format (expected: https://github.com/owner/repo/tree|blob/ref/path)")
127+
}
128+
129+
owner = parts[0]
130+
repo = parts[1]
131+
treeOrBlob := parts[2]
132+
ref = parts[3]
133+
path = strings.Join(parts[4:], "/")
134+
135+
if treeOrBlob != "tree" && treeOrBlob != "blob" {
136+
return "", "", "", "", fmt.Errorf("unsupported GitHub URL type: %s (expected 'tree' or 'blob')", treeOrBlob)
137+
}
138+
139+
return owner, repo, ref, path, nil
140+
}
141+
142+
// downloadDirectoryRecursive recursively downloads a directory from GitHub
143+
func downloadDirectoryRecursive(ctx context.Context, client *github.Client, owner, repo, ref, path, destPath string) error {
144+
// Get directory contents
145+
_, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, &github.RepositoryContentGetOptions{
146+
Ref: ref,
147+
})
148+
if err != nil {
149+
return fmt.Errorf("failed to get directory contents: %w", err)
150+
}
151+
152+
// Create destination directory
153+
if err := os.MkdirAll(destPath, 0o755); err != nil {
154+
return fmt.Errorf("failed to create directory: %w", err)
155+
}
156+
157+
// Process each item in the directory
158+
for _, item := range directoryContent {
159+
if item.GetName() == "README.md" {
160+
continue
161+
}
162+
163+
itemPath := item.GetPath()
164+
itemName := item.GetName()
165+
targetPath := filepath.Join(destPath, itemName)
166+
167+
switch item.GetType() {
168+
case "file":
169+
// Download file
170+
fileContent, _, _, err := client.Repositories.GetContents(ctx, owner, repo, itemPath, &github.RepositoryContentGetOptions{
171+
Ref: ref,
172+
})
173+
if err != nil {
174+
return fmt.Errorf("failed to get file %s: %w", itemPath, err)
175+
}
176+
177+
content, err := fileContent.GetContent()
178+
if err != nil {
179+
return fmt.Errorf("failed to decode file %s: %w", itemPath, err)
180+
}
181+
182+
if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil {
183+
return fmt.Errorf("failed to write file %s: %w", targetPath, err)
184+
}
185+
186+
case "dir":
187+
// Recursively download subdirectory
188+
if err := downloadDirectoryRecursive(ctx, client, owner, repo, ref, itemPath, targetPath); err != nil {
189+
return err
190+
}
191+
}
192+
}
193+
194+
return nil
195+
}
196+
27197
// extractAllFiles goes through the embedded directory and extracts all files to the current directory
28198
func extractAllFiles(embeddedDir string) error {
199+
return extractAllFilesFromFS(EmbeddedObservabilityFiles, embeddedDir)
200+
}
201+
202+
// extractAllFilesFromFS goes through a filesystem and extracts all files to the current directory
203+
func extractAllFilesFromFS(fsys fs.FS, embeddedDir string) error {
29204
// Get current working directory where CLI is running
30205
currentDir, err := os.Getwd()
31206
if err != nil {
32207
return fmt.Errorf("failed to get current directory: %w", err)
33208
}
34209

35-
// Walk through the embedded files
36-
err = fs.WalkDir(EmbeddedObservabilityFiles, embeddedDir, func(path string, d fs.DirEntry, err error) error {
210+
// Walk through the files
211+
err = fs.WalkDir(fsys, embeddedDir, func(path string, d fs.DirEntry, err error) error {
37212
if err != nil {
38213
return fmt.Errorf("error walking the directory: %w", err)
39214
}
@@ -46,8 +221,8 @@ func extractAllFiles(embeddedDir string) error {
46221
return nil
47222
}
48223

49-
// Read file content from embedded file system
50-
content, err := EmbeddedObservabilityFiles.ReadFile(path)
224+
// Read file content from file system
225+
content, err := fs.ReadFile(fsys, path)
51226
if err != nil {
52227
return fmt.Errorf("failed to read file %s: %w", path, err)
53228
}
@@ -123,15 +298,28 @@ func BlockScoutDown(url string) error {
123298

124299
// ObservabilityUpOnlyLoki slim stack with only Loki to verify specific logs of CL nodes or services in tests
125300
func ObservabilityUpOnlyLoki() error {
301+
return ObservabilityUpOnlyLokiWithSource("")
302+
}
303+
304+
// ObservabilityUpOnlyLokiWithSource slim stack with only Loki using custom observability file source
305+
// source can be:
306+
// - "" (empty): use embedded files (default)
307+
// - "file:///path/to/observability": use local filesystem
308+
// - "https://github.com/owner/repo/tree/tag/framework/observability": download from GitHub
309+
func ObservabilityUpOnlyLokiWithSource(source string) error {
126310
L.Info().Msg("Creating local observability stack")
127-
if err := extractAllFiles("observability"); err != nil {
311+
fsys, dir, err := resolveObservabilitySource(source)
312+
if err != nil {
313+
return err
314+
}
315+
if err := extractAllFilesFromFS(fsys, dir); err != nil {
128316
return err
129317
}
130318
_ = DefaultNetwork(nil)
131319
if err := NewPromtail(); err != nil {
132320
return err
133321
}
134-
err := RunCommand("bash", "-c", fmt.Sprintf(`
322+
err = RunCommand("bash", "-c", fmt.Sprintf(`
135323
cd %s && \
136324
docker compose up -d loki grafana
137325
`, "compose"))
@@ -145,15 +333,28 @@ func ObservabilityUpOnlyLoki() error {
145333

146334
// ObservabilityUp standard stack with logs/metrics for load testing and observability
147335
func ObservabilityUp() error {
336+
return ObservabilityUpWithSource("")
337+
}
338+
339+
// ObservabilityUpWithSource standard stack with logs/metrics using custom observability file source
340+
// source can be:
341+
// - "" (empty): use embedded files (default)
342+
// - "file:///path/to/observability": use local filesystem
343+
// - "https://github.com/owner/repo/tree/tag/framework/observability": download from GitHub
344+
func ObservabilityUpWithSource(source string) error {
148345
L.Info().Msg("Creating local observability stack")
149-
if err := extractAllFiles("observability"); err != nil {
346+
fsys, dir, err := resolveObservabilitySource(source)
347+
if err != nil {
348+
return err
349+
}
350+
if err := extractAllFilesFromFS(fsys, dir); err != nil {
150351
return err
151352
}
152353
_ = DefaultNetwork(nil)
153354
if err := NewPromtail(); err != nil {
154355
return err
155356
}
156-
err := RunCommand("bash", "-c", fmt.Sprintf(`
357+
err = RunCommand("bash", "-c", fmt.Sprintf(`
157358
cd %s && \
158359
docker compose up -d otel-collector prometheus loki grafana
159360
`, "compose"))
@@ -170,15 +371,28 @@ func ObservabilityUp() error {
170371

171372
// ObservabilityUpFull full stack for load testing and performance investigations
172373
func ObservabilityUpFull() error {
374+
return ObservabilityUpFullWithSource("")
375+
}
376+
377+
// ObservabilityUpFullWithSource full stack for load testing using custom observability file source
378+
// source can be:
379+
// - "" (empty): use embedded files (default)
380+
// - "file:///path/to/observability": use local filesystem
381+
// - "https://github.com/owner/repo/tree/tag/framework/observability": download from GitHub
382+
func ObservabilityUpFullWithSource(source string) error {
173383
L.Info().Msg("Creating full local observability stack")
174-
if err := extractAllFiles("observability"); err != nil {
384+
fsys, dir, err := resolveObservabilitySource(source)
385+
if err != nil {
386+
return err
387+
}
388+
if err := extractAllFilesFromFS(fsys, dir); err != nil {
175389
return err
176390
}
177391
_ = DefaultNetwork(nil)
178392
if err := NewPromtail(); err != nil {
179393
return err
180394
}
181-
err := RunCommand("bash", "-c", fmt.Sprintf(`
395+
err = RunCommand("bash", "-c", fmt.Sprintf(`
182396
cd %s && \
183397
docker compose up -d
184398
`, "compose"))

0 commit comments

Comments
 (0)