Skip to content

Commit c0de736

Browse files
committed
turnclient integration
1 parent 73edd32 commit c0de736

15 files changed

Lines changed: 4019 additions & 21 deletions

File tree

cmd/prcost/main.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func main() {
2525
eventMinutes := flag.Float64("event-minutes", 10, "Minutes per GitHub event (commits, comments, etc.)")
2626
format := flag.String("format", "human", "Output format: human or json")
2727
verbose := flag.Bool("verbose", false, "Show verbose logging output")
28+
dataSource := flag.String("data-source", "prx", "Data source for PR data: prx (direct GitHub API) or turnserver")
2829

2930
// Org/Repo sampling flags
3031
org := flag.String("org", "", "GitHub organization to analyze (optionally with --repo for single repo)")
@@ -124,7 +125,7 @@ func main() {
124125
"samples", *samples,
125126
"days", *days)
126127

127-
err := analyzeRepository(ctx, *org, *repo, *samples, *days, cfg, token)
128+
err := analyzeRepository(ctx, *org, *repo, *samples, *days, cfg, token, *dataSource)
128129
if err != nil {
129130
log.Fatalf("Repository analysis failed: %v", err)
130131
}
@@ -135,7 +136,7 @@ func main() {
135136
"samples", *samples,
136137
"days", *days)
137138

138-
err := analyzeOrganization(ctx, *org, *samples, *days, cfg, token)
139+
err := analyzeOrganization(ctx, *org, *samples, *days, cfg, token, *dataSource)
139140
if err != nil {
140141
log.Fatalf("Organization analysis failed: %v", err)
141142
}
@@ -151,11 +152,19 @@ func main() {
151152

152153
slog.Info("Starting PR cost analysis", "pr_url", prURL, "format", *format)
153154

154-
// Fetch PR data
155-
slog.Info("Fetching PR data from GitHub")
156-
prData, err := github.FetchPRData(ctx, prURL, token)
155+
// Fetch PR data using configured data source
156+
slog.Info("Fetching PR data", "source", *dataSource)
157+
var prData cost.PRData
158+
var err error
159+
if *dataSource == "turnserver" {
160+
// Use turnserver - pass time.Now() since we don't have updatedAt for single PR requests
161+
prData, err = github.FetchPRDataViaTurnserver(ctx, prURL, token, time.Now())
162+
} else {
163+
// Use prx - pass time.Now() since we don't have updatedAt for single PR requests
164+
prData, err = github.FetchPRData(ctx, prURL, token, time.Now())
165+
}
157166
if err != nil {
158-
slog.Error("Failed to fetch PR data", "error", err)
167+
slog.Error("Failed to fetch PR data", "source", *dataSource, "error", err)
159168
log.Fatalf("Failed to fetch PR data: %v", err)
160169
}
161170
slog.Info("Successfully fetched PR data",

cmd/prcost/repository.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
// analyzeRepository performs repository-wide cost analysis by sampling PRs.
1515
// Uses library functions from pkg/github and pkg/cost for fetching, sampling,
1616
// and extrapolation - all functionality is available to external clients.
17-
func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days int, cfg cost.Config, token string) error {
17+
func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days int, cfg cost.Config, token string, dataSource string) error {
1818
slog.Info("Fetching PR list from repository")
1919

2020
// Calculate since date
@@ -55,10 +55,18 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
5555
"number", pr.Number,
5656
"progress", fmt.Sprintf("%d/%d", i+1, len(samples)))
5757

58-
// Fetch full PR data
59-
prData, err := github.FetchPRData(ctx, prURL, token)
58+
// Fetch full PR data using configured data source
59+
var prData cost.PRData
60+
var err error
61+
if dataSource == "turnserver" {
62+
// Use turnserver with updatedAt for effective caching
63+
prData, err = github.FetchPRDataViaTurnserver(ctx, prURL, token, pr.UpdatedAt)
64+
} else {
65+
// Use prx with updatedAt for effective caching
66+
prData, err = github.FetchPRData(ctx, prURL, token, pr.UpdatedAt)
67+
}
6068
if err != nil {
61-
slog.Warn("Failed to fetch PR data, skipping", "pr_number", pr.Number, "error", err)
69+
slog.Warn("Failed to fetch PR data, skipping", "pr_number", pr.Number, "source", dataSource, "error", err)
6270
continue
6371
}
6472

@@ -83,7 +91,7 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
8391
// analyzeOrganization performs organization-wide cost analysis by sampling PRs across all repos.
8492
// Uses library functions from pkg/github and pkg/cost for fetching, sampling,
8593
// and extrapolation - all functionality is available to external clients.
86-
func analyzeOrganization(ctx context.Context, org string, sampleSize, days int, cfg cost.Config, token string) error {
94+
func analyzeOrganization(ctx context.Context, org string, sampleSize, days int, cfg cost.Config, token string, dataSource string) error {
8795
slog.Info("Fetching PR list from organization")
8896

8997
// Calculate since date
@@ -124,10 +132,18 @@ func analyzeOrganization(ctx context.Context, org string, sampleSize, days int,
124132
"number", pr.Number,
125133
"progress", fmt.Sprintf("%d/%d", i+1, len(samples)))
126134

127-
// Fetch full PR data
128-
prData, err := github.FetchPRData(ctx, prURL, token)
135+
// Fetch full PR data using configured data source
136+
var prData cost.PRData
137+
var err error
138+
if dataSource == "turnserver" {
139+
// Use turnserver with updatedAt for effective caching
140+
prData, err = github.FetchPRDataViaTurnserver(ctx, prURL, token, pr.UpdatedAt)
141+
} else {
142+
// Use prx with updatedAt for effective caching
143+
prData, err = github.FetchPRData(ctx, prURL, token, pr.UpdatedAt)
144+
}
129145
if err != nil {
130-
slog.Warn("Failed to fetch PR data, skipping", "pr_number", pr.Number, "error", err)
146+
slog.Warn("Failed to fetch PR data, skipping", "pr_number", pr.Number, "source", dataSource, "error", err)
131147
continue
132148
}
133149

cmd/server/main.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Command server runs the PR Cost API server.
2+
package main
3+
4+
import (
5+
"context"
6+
"errors"
7+
"flag"
8+
"log/slog"
9+
"net/http"
10+
"os"
11+
"os/signal"
12+
"runtime"
13+
"syscall"
14+
"time"
15+
16+
"github.com/codeGROOVE-dev/prcost/internal/server"
17+
)
18+
19+
const (
20+
defaultPort = "8080"
21+
shutdownTimeout = 10 * time.Second
22+
readHeaderTimeout = 5 * time.Second
23+
writeTimeout = 30 * time.Second
24+
idleTimeout = 120 * time.Second
25+
maxHeaderBytes = 1 << 20 // 1MB
26+
)
27+
28+
// Build variables - set by ldflags.
29+
var (
30+
GitCommit = "unknown"
31+
GitBranch = "unknown"
32+
BuildTime = "unknown"
33+
)
34+
35+
func main() {
36+
// Create root context
37+
ctx := context.Background()
38+
39+
// Set up logging
40+
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
41+
AddSource: true,
42+
Level: slog.LevelInfo,
43+
}))
44+
slog.SetDefault(logger)
45+
46+
// Log startup information
47+
logger.InfoContext(ctx, "starting server",
48+
"commit", GitCommit,
49+
"branch", GitBranch,
50+
"built", BuildTime,
51+
"go", runtime.Version(),
52+
"pid", os.Getpid())
53+
54+
// Parse flags
55+
var (
56+
port = flag.String("port", "", "Port to run the server on")
57+
version = flag.Bool("version", false, "Print version and exit")
58+
corsOrigins = flag.String("cors-origins",
59+
"https://github.com,https://api.github.com",
60+
"Comma-separated list of allowed CORS origins (supports *.domain.com wildcards)")
61+
allowAllCors = flag.Bool("allow-all-cors", false, "Allow all CORS origins (use only for development)")
62+
rateLimit = flag.Int("rate-limit", 100, "Requests per second rate limit")
63+
rateBurst = flag.Int("rate-burst", 100, "Rate limit burst size")
64+
validateTokens = flag.Bool("validate-tokens", false, "Validate GitHub tokens server-side")
65+
githubAppID = flag.String("github-app-id", "", "GitHub App ID for token validation")
66+
githubAppKey = flag.String("github-app-key-file", "", "Path to GitHub App private key file")
67+
dataSource = flag.String("data-source", "turnserver", "Data source for PR data (turnserver or prx)")
68+
)
69+
flag.Parse()
70+
71+
if *version {
72+
logger.InfoContext(ctx, "prcost-server version",
73+
"commit", GitCommit,
74+
"branch", GitBranch,
75+
"built", BuildTime,
76+
"go", runtime.Version())
77+
os.Exit(0)
78+
}
79+
80+
// Determine port
81+
serverPort := *port
82+
if serverPort == "" {
83+
serverPort = os.Getenv("PORT")
84+
}
85+
if serverPort == "" {
86+
serverPort = defaultPort
87+
}
88+
89+
// Create server
90+
prcostServer := server.New()
91+
prcostServer.SetCommit(GitCommit)
92+
prcostServer.SetCORSConfig(*corsOrigins, *allowAllCors)
93+
prcostServer.SetRateLimit(*rateLimit, *rateBurst)
94+
prcostServer.SetDataSource(*dataSource)
95+
if *validateTokens {
96+
if *githubAppID == "" || *githubAppKey == "" {
97+
logger.ErrorContext(ctx, "github app ID and key file are required when token validation is enabled")
98+
os.Exit(1)
99+
}
100+
if err := prcostServer.SetTokenValidation(*githubAppID, *githubAppKey); err != nil {
101+
logger.ErrorContext(ctx, "failed to configure token validation", "error", err)
102+
os.Exit(1)
103+
}
104+
}
105+
srv := &http.Server{
106+
Addr: ":" + serverPort,
107+
Handler: prcostServer,
108+
ReadTimeout: readHeaderTimeout,
109+
ReadHeaderTimeout: readHeaderTimeout,
110+
WriteTimeout: writeTimeout,
111+
IdleTimeout: idleTimeout,
112+
MaxHeaderBytes: maxHeaderBytes,
113+
}
114+
115+
// Start server in goroutine
116+
serverErrors := make(chan error, 1)
117+
go func() {
118+
logger.InfoContext(ctx, "server listening", "port", serverPort)
119+
serverErrors <- srv.ListenAndServe()
120+
}()
121+
122+
// Set up signal handling
123+
sigChan := make(chan os.Signal, 1)
124+
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
125+
126+
// Wait for shutdown signal or server error
127+
select {
128+
case err := <-serverErrors:
129+
if err != nil && !errors.Is(err, http.ErrServerClosed) {
130+
logger.ErrorContext(ctx, "server error", "error", err)
131+
os.Exit(1)
132+
}
133+
case sig := <-sigChan:
134+
logger.InfoContext(ctx, "received signal", "signal", sig)
135+
136+
// Graceful shutdown
137+
logger.InfoContext(ctx, "starting graceful shutdown")
138+
139+
shutdownCtx, cancel := context.WithTimeout(ctx, shutdownTimeout)
140+
141+
// Shutdown application components
142+
prcostServer.Shutdown()
143+
144+
if err := srv.Shutdown(shutdownCtx); err != nil {
145+
cancel()
146+
logger.WarnContext(ctx, "graceful shutdown failed", "error", err)
147+
// Force close
148+
if err := srv.Close(); err != nil {
149+
logger.ErrorContext(ctx, "server close error", "error", err)
150+
os.Exit(1)
151+
}
152+
} else {
153+
cancel()
154+
}
155+
}
156+
157+
logger.InfoContext(ctx, "server stopped")
158+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.25.3
55
require (
66
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22
77
github.com/codeGROOVE-dev/prx v0.0.0-20251024000018-35ba2605d031
8+
github.com/codeGROOVE-dev/turnclient v0.0.0-20251022064427-5a712e1e10e6
89
golang.org/x/time v0.14.0
910
)
1011

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ github.com/codeGROOVE-dev/prx v0.0.0-20251024000018-35ba2605d031 h1:wLeo/dwpE5F2
44
github.com/codeGROOVE-dev/prx v0.0.0-20251024000018-35ba2605d031/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w=
55
github.com/codeGROOVE-dev/retry v1.2.0 h1:xYpYPX2PQZmdHwuiQAGGzsBm392xIMl4nfMEFApQnu8=
66
github.com/codeGROOVE-dev/retry v1.2.0/go.mod h1:8OgefgV1XP7lzX2PdKlCXILsYKuz6b4ZpHa/20iLi8E=
7+
github.com/codeGROOVE-dev/turnclient v0.0.0-20251022064427-5a712e1e10e6 h1:7FCmaftkl362oTZHVJyUg+xhxqfQFx+JisBf7RgklL8=
8+
github.com/codeGROOVE-dev/turnclient v0.0.0-20251022064427-5a712e1e10e6/go.mod h1:fYwtN9Ql6lY8t2WvCfENx+mP5FUwjlqwXCLx9CVLY20=
79
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
810
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=

hacks/check-config/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Package main prints the default cost configuration values.
2+
package main
3+
4+
import (
5+
"fmt"
6+
7+
"github.com/codeGROOVE-dev/prcost/pkg/cost"
8+
)
9+
10+
func main() {
11+
cfg := cost.DefaultConfig()
12+
fmt.Printf("EventDuration: %v\n", cfg.EventDuration)
13+
fmt.Printf("SessionGapThreshold: %v\n", cfg.SessionGapThreshold)
14+
fmt.Printf("ContextSwitchDuration: %v\n", cfg.ContextSwitchDuration)
15+
}

0 commit comments

Comments
 (0)