Skip to content

Commit 685501f

Browse files
motatoesZIJ
andauthored
introduce monitoring (#2503)
* Bring back docs on local development * Improve local dev docs * Agent task for moving from callbacks to webhooks * Move from callback to webhooks, again * Make repos webhook async; dont comment erorr when installed to all repos * add staging deploy * fix docker service * fix projects refresh service * add monitoring logs for ui * update request logging * copy file * further attempt to log * dont log unnecessary stuff * fix syntax error --------- Co-authored-by: Igor Zalutski <izalutski@gmail.com>
1 parent ec9c9ce commit 685501f

File tree

11 files changed

+254
-23
lines changed

11 files changed

+254
-23
lines changed

Dockerfile_ui

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ RUN --mount=type=cache,target=/root/.npm \
3838
# Bring in built assets and server entry from builder
3939
COPY --from=builder /app/dist ./dist
4040
COPY --from=builder /app/server-start.js ./
41+
COPY --from=builder /app/request-logging.js ./
4142

4243
EXPOSE 3030
4344

backend/controllers/github.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,20 @@ func (d DiggerController) GithubAppWebHook(c *gin.Context) {
6666
"installationId", *event.Installation.ID,
6767
)
6868

69-
if *event.Action == "deleted" {
70-
err := handleInstallationDeletedEvent(event, appId64)
71-
if err != nil {
72-
slog.Error("Failed to handle installation deleted event", "error", err)
73-
c.String(http.StatusAccepted, "Failed to handle webhook event.")
74-
return
75-
}
76-
} else if *event.Action == "created" || *event.Action == "unsuspended" || *event.Action == "new_permissions_accepted" {
77-
if err := handleInstallationUpsertEvent(c.Request.Context(), gh, event, appId64); err != nil {
78-
slog.Error("Failed to handle installation upsert event", "error", err)
79-
c.String(http.StatusAccepted, "Failed to handle webhook event.")
80-
return
69+
// Run in goroutine to avoid webhook timeouts for large installations
70+
go func(ctx context.Context) {
71+
defer logging.InheritRequestLogger(ctx)()
72+
if *event.Action == "deleted" {
73+
if err := handleInstallationDeletedEvent(event, appId64); err != nil {
74+
slog.Error("Failed to handle installation deleted event", "error", err)
75+
}
76+
} else if *event.Action == "created" || *event.Action == "unsuspended" || *event.Action == "new_permissions_accepted" {
77+
// Use background context so work continues after HTTP response
78+
if err := handleInstallationUpsertEvent(context.Background(), gh, event, appId64); err != nil {
79+
slog.Error("Failed to handle installation upsert event", "error", err)
80+
}
8181
}
82-
}
82+
}(c.Request.Context())
8383
case *github.InstallationRepositoriesEvent:
8484
slog.Info("Processing InstallationRepositoriesEvent",
8585
"action", event.GetAction(),

backend/controllers/github_comment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
162162
}
163163

164164
diggerYmlStr, ghService, config, projectsGraph, prSourceBranch, commitSha, changedFiles, err := getDiggerConfigForPR(gh, orgId, prLabelsStr, installationId, repoFullName, repoOwner, repoName, cloneURL, issueNumber)
165-
if err != nil {
165+
if err != nil {
166166
slog.Error("Error getting Digger config for PR",
167167
"issueNumber", issueNumber,
168168
"repoFullName", repoFullName,

backend/controllers/github_helpers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ func getDiggerConfigForPR(gh utils.GithubClientProvider, orgId uint, prLabels []
830830
"branch", prBranch,
831831
"error", err,
832832
)
833-
return "", nil, nil, nil, nil, nil, nil, fmt.Errorf("error loading digger.yml: %v", err)
833+
return "", nil, nil, nil, nil, nil, nil, fmt.Errorf("error loading digger.yml: %w", err)
834834
}
835835

836836
return diggerYmlStr, ghService, config, dependencyGraph, &prBranch, &prCommitSha, changedFiles, nil
@@ -893,7 +893,7 @@ func GetDiggerConfigForBranchOrSha(gh utils.GithubClientProvider, installationId
893893
"branch", branch,
894894
"error", err,
895895
)
896-
return "", nil, nil, nil, fmt.Errorf("error cloning and loading config %v", err)
896+
return "", nil, nil, nil, fmt.Errorf("error cloning and loading config: %w", err)
897897
}
898898

899899
projectCount := 0

backend/utils/github.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,59 @@ func GetGithubHostname() string {
408408
return githubHostname
409409
}
410410

411+
// IsAllReposInstallation checks if the GitHub App installation is configured to access all repositories
412+
// (as opposed to a selected subset). Returns true if installation is for "all" repos.
413+
// Note: This requires app-level JWT authentication, not installation token authentication.
414+
func IsAllReposInstallation(appId int64, installationId int64) (bool, error) {
415+
githubAppPrivateKey := ""
416+
githubAppPrivateKeyB64 := os.Getenv("GITHUB_APP_PRIVATE_KEY_BASE64")
417+
if githubAppPrivateKeyB64 != "" {
418+
decodedBytes, err := base64.StdEncoding.DecodeString(githubAppPrivateKeyB64)
419+
if err != nil {
420+
slog.Error("Failed to decode GITHUB_APP_PRIVATE_KEY_BASE64", "error", err)
421+
return false, fmt.Errorf("error decoding private key: %v", err)
422+
}
423+
githubAppPrivateKey = string(decodedBytes)
424+
} else {
425+
githubAppPrivateKey = os.Getenv("GITHUB_APP_PRIVATE_KEY")
426+
if githubAppPrivateKey == "" {
427+
return false, fmt.Errorf("missing GitHub app private key")
428+
}
429+
}
430+
431+
// Use app-level transport (JWT) instead of installation token
432+
atr, err := ghinstallation.NewAppsTransport(net.DefaultTransport, appId, []byte(githubAppPrivateKey))
433+
if err != nil {
434+
slog.Error("Failed to create GitHub app transport",
435+
"appId", appId,
436+
"error", err,
437+
)
438+
return false, fmt.Errorf("error creating app transport: %v", err)
439+
}
440+
441+
client := github.NewClient(&net.Client{Transport: atr})
442+
443+
installation, _, err := client.Apps.GetInstallation(context.Background(), installationId)
444+
if err != nil {
445+
slog.Error("Failed to get GitHub installation details",
446+
"installationId", installationId,
447+
"error", err,
448+
)
449+
return false, fmt.Errorf("error getting installation details: %v", err)
450+
}
451+
452+
repositorySelection := installation.GetRepositorySelection()
453+
isAllRepos := repositorySelection == "all"
454+
455+
slog.Debug("Checked installation repository selection",
456+
"installationId", installationId,
457+
"repositorySelection", repositorySelection,
458+
"isAllRepos", isAllRepos,
459+
)
460+
461+
return isAllRepos, nil
462+
}
463+
411464
func GetWorkflowIdAndUrlFromDiggerJobId(client *github.Client, repoOwner string, repoName string, diggerJobID string) (int64, string, error) {
412465
slog.Debug("Looking for workflow for job",
413466
"diggerJobId", diggerJobID,

libs/digger_config/digger_config.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import (
1818
"gopkg.in/yaml.v3"
1919
)
2020

21+
// ErrDiggerConfigNotFound is returned when neither digger.yml nor digger.yaml exists in the repository
22+
var ErrDiggerConfigNotFound = errors.New("digger config file not found")
23+
2124
type DirWalker interface {
2225
GetDirs(workingDir string, config DiggerConfigYaml) ([]string, error)
2326
}
@@ -39,7 +42,7 @@ func ReadDiggerYmlFileContents(dir string) (string, error) {
3942
slog.Error("could not read digger config file",
4043
"error", err,
4144
"dir", dir)
42-
return "", fmt.Errorf("could not read the file both digger.yml and digger.yaml are missing: %v", err)
45+
return "", fmt.Errorf("%w: both digger.yml and digger.yaml are missing: %v", ErrDiggerConfigNotFound, err)
4346
}
4447
}
4548
diggerYmlStr := string(diggerYmlBytes)

taco/Dockerfile_token_service

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ WORKDIR /go/src/github.com/diggerhq/digger/taco
77

88
# Copy go.mod/go.sum first for better layer caching
99
COPY cmd/token_service/go.mod cmd/token_service/go.sum ./cmd/token_service/
10+
COPY internal/go.mod internal/go.sum ./internal/
1011
RUN cd cmd/token_service && go mod download
1112

1213
# Copy source code

ui/request-logging.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { unsealData } from 'iron-session';
2+
import { decodeJwt } from 'jose';
3+
4+
// Request logging utilities
5+
export async function extractUserInfoFromRequest(req) {
6+
try {
7+
const cookieName = process.env.WORKOS_COOKIE_NAME || 'wos-session';
8+
const cookiePassword = process.env.WORKOS_COOKIE_PASSWORD;
9+
10+
if (!cookiePassword) {
11+
return { userId: 'anonymous', orgId: 'anonymous' };
12+
}
13+
14+
const cookieHeader = req.headers?.cookie || req.getHeader?.('cookie');
15+
if (!cookieHeader) {
16+
return { userId: 'anonymous', orgId: 'anonymous' };
17+
}
18+
19+
const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
20+
const [key, value] = cookie.trim().split('=');
21+
acc[key] = decodeURIComponent(value);
22+
return acc;
23+
}, {});
24+
25+
const sessionCookie = cookies[cookieName];
26+
if (!sessionCookie) {
27+
return { userId: 'anonymous', orgId: 'anonymous' };
28+
}
29+
30+
const session = await unsealData(sessionCookie, {
31+
password: cookiePassword,
32+
});
33+
34+
if (!session?.user?.id || !session?.accessToken) {
35+
return { userId: 'anonymous', orgId: 'anonymous' };
36+
}
37+
38+
// Decode JWT to get organization ID
39+
let orgId = 'anonymous';
40+
try {
41+
const decoded = decodeJwt(session.accessToken);
42+
orgId = decoded.org_id || 'anonymous';
43+
} catch (error) {
44+
// If JWT decode fails, just use anonymous
45+
}
46+
47+
return { userId: session.user.id, orgId };
48+
} catch (error) {
49+
return { userId: 'anonymous', orgId: 'anonymous' };
50+
}
51+
}
52+
53+
export function logRequestInit(method, path, requestId, userId, orgId) {
54+
console.log(JSON.stringify({
55+
event: 'request_initialized',
56+
method,
57+
path,
58+
requestId,
59+
userId,
60+
orgId,
61+
}));
62+
}
63+
64+
export function logResponse(method, path, requestId, latency, statusCode) {
65+
console.log(JSON.stringify({
66+
event: 'response_sent',
67+
method,
68+
path,
69+
requestId,
70+
latency,
71+
statusCode,
72+
}));
73+
}
74+

ui/server-start.js

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { fileURLToPath } from 'node:url';
66
import { Readable } from 'node:stream';
77
import { createGzip } from 'node:zlib';
88
import serverHandler from './dist/server/server.js';
9+
import { extractUserInfoFromRequest, logRequestInit, logResponse } from './request-logging.js';
10+
11+
// Verify logging functions are loaded
12+
console.log('✅ Request logging module loaded');
913

1014
const __dirname = fileURLToPath(new URL('.', import.meta.url));
1115
const PORT = process.env.PORT || 3030;
@@ -68,6 +72,42 @@ const server = createServer(async (req, res) => {
6872
const requestId = req.headers['x-request-id'] || `ssr-${Math.random().toString(36).slice(2, 10)}`;
6973
const requestStart = Date.now();
7074

75+
// Parse URL early for logging
76+
const url = new URL(req.url, `http://${req.headers.host}`);
77+
const pathname = url.pathname;
78+
const method = req.method;
79+
80+
// Debug: Log that request handler is being called
81+
console.log(`[DEBUG] Request received: ${method} ${pathname} [${requestId}]`);
82+
83+
// Extract user ID and org ID and log request initialization
84+
// Always log, even if extraction fails
85+
let userId = 'anonymous';
86+
let orgId = 'anonymous';
87+
try {
88+
const userInfo = await extractUserInfoFromRequest(req);
89+
userId = userInfo.userId;
90+
orgId = userInfo.orgId;
91+
} catch (error) {
92+
console.error(`User info extraction error [${requestId}]:`, error);
93+
}
94+
95+
// Always log request initialization
96+
try {
97+
logRequestInit(method, pathname, requestId, userId, orgId);
98+
} catch (error) {
99+
console.error(`Request logging error [${requestId}]:`, error);
100+
// Fallback to direct console.log if logging function fails
101+
console.log(JSON.stringify({
102+
event: 'request_initialized',
103+
method,
104+
path: pathname,
105+
requestId,
106+
userId,
107+
orgId,
108+
}));
109+
}
110+
71111
// Set request timeout
72112
req.setTimeout(REQUEST_TIMEOUT, () => {
73113
console.error(`⏱️ Request timeout (${REQUEST_TIMEOUT}ms): ${req.method} ${req.url} [${requestId}]`);
@@ -78,8 +118,6 @@ const server = createServer(async (req, res) => {
78118
});
79119

80120
try {
81-
const url = new URL(req.url, `http://${req.headers.host}`);
82-
const pathname = url.pathname;
83121

84122
// Try to serve static files from dist/client first
85123
// Serve: /assets/*, *.js, *.css, *.json, images, fonts, favicons
@@ -99,6 +137,13 @@ const server = createServer(async (req, res) => {
99137
'Cache-Control': 'public, max-age=31536000, immutable',
100138
});
101139
res.end(content);
140+
// Log response for static files
141+
try {
142+
const latency = Date.now() - requestStart;
143+
logResponse(method, pathname, requestId, latency, 200);
144+
} catch (err) {
145+
console.error(`Response logging error [${requestId}]:`, err);
146+
}
102147
return;
103148
} catch (err) {
104149
// File not found, fall through to SSR handler
@@ -132,9 +177,9 @@ const server = createServer(async (req, res) => {
132177

133178
// Log slow SSR requests
134179
if (ssrTime > 2000) {
135-
console.log(`🔥 VERY SLOW SSR: ${req.method} ${pathname} took ${ssrTime}ms [${requestId}]`);
180+
console.debug(`🔥 VERY SLOW SSR: ${req.method} ${pathname} took ${ssrTime}ms [${requestId}]`);
136181
} else if (ssrTime > 1000) {
137-
console.log(`⚠️ SLOW SSR: ${req.method} ${pathname} took ${ssrTime}ms [${requestId}]`);
182+
console.debug(`⚠️ SLOW SSR: ${req.method} ${pathname} took ${ssrTime}ms [${requestId}]`);
138183
}
139184

140185
// Convert Web Standard Response to Node.js response
@@ -221,13 +266,28 @@ const server = createServer(async (req, res) => {
221266
} else {
222267
res.end();
223268
}
269+
270+
// Log response after sending
271+
try {
272+
const latency = Date.now() - requestStart;
273+
logResponse(method, pathname, requestId, latency, res.statusCode);
274+
} catch (err) {
275+
console.error(`Response logging error [${requestId}]:`, err);
276+
}
224277
} catch (error) {
225278
console.error(`Server error [${requestId}]:`, error);
226279
if (!res.headersSent) {
227280
res.statusCode = 500;
228281
res.setHeader('Content-Type', 'text/plain');
229282
res.end('Internal Server Error');
230283
}
284+
// Log error response
285+
try {
286+
const latency = Date.now() - requestStart;
287+
logResponse(method, pathname, requestId, latency, res.statusCode || 500);
288+
} catch (err) {
289+
console.error(`Error response logging error [${requestId}]:`, err);
290+
}
231291
}
232292
});
233293

ui/src/authkit/serverFunctions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,12 @@ export const getWidgetsAuthToken = createServerFn({method: 'GET'})
137137
// Check cache first
138138
const cached = serverCache.getWidgetToken(userId, organizationId);
139139
if (cached) {
140-
console.log(`✅ Widget token cache hit for ${userId}:${organizationId}`);
140+
console.debug(`✅ Widget token cache hit for ${userId}:${organizationId}`);
141141
return cached;
142142
}
143143

144144
// Cache miss - generate new token
145-
console.log(`❌ Widget token cache miss, generating new token for ${userId}:${organizationId}`);
145+
console.debug(`❌ Widget token cache miss, generating new token for ${userId}:${organizationId}`);
146146
const token = await getWorkOS().widgets.getToken({
147147
userId: userId,
148148
organizationId: organizationId,

0 commit comments

Comments
 (0)