Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export default defineNuxtConfig({
// GitHub App credentials (alternative to PAT — works with any OAuth provider)
githubAppId: '', // NUXT_GITHUB_APP_ID — numeric App ID (preferred); GitHub also accepts the Client ID (NUXT_OAUTH_GITHUB_CLIENT_ID)
githubAppPrivateKey: '', // NUXT_GITHUB_APP_PRIVATE_KEY (PEM, \n-escaped)
logLevel: process.env.NUXT_LOG_LEVEL || 'info',
logFormat: process.env.NUXT_LOG_FORMAT || '',
logRequests: process.env.NUXT_LOG_REQUESTS || 'false',
session: {
// set to 6h - same as the GitHub token
maxAge: 60 * 60 * 6,
Expand Down
5 changes: 4 additions & 1 deletion server/api/admin/overview.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { getPool, isDbConfigured } from '../../storage/db';
import { getFailedSyncsForScope, getPendingSyncsForScope } from '../../storage/sync-storage';
import { getSyncStats } from '../../services/sync-service';
import { isMockMode } from '../../services/github-copilot-usage-api-mock';
import { createLogger } from '../../utils/logger'

const logger = createLogger('admin-overview')

interface OverviewResponse {
db: { connected: boolean; latencyMs?: number; error?: string };
Expand Down Expand Up @@ -159,7 +162,7 @@ export default defineEventHandler(async (event): Promise<OverviewResponse> => {
lastAttemptAt: f.lastAttemptAt,
}));
} catch (err) {
console.warn('[admin/overview] Enrichment failed:', err);
logger.warn('Enrichment failed:', err);
}

return response;
Expand Down
5 changes: 4 additions & 1 deletion server/api/ai/chat.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { aiTools, buildSystemPrompt } from '../../services/ai-tools';
import { executeTool, type ToolCallRequest } from '../../services/ai-tool-executor';
import { isMockMode } from '../../services/github-copilot-usage-api-mock';
import { generateMockChatResponse } from '../../services/ai-chat-mock';
import { createLogger } from '../../utils/logger'

const logger = createLogger('ai-chat')

interface ChatMessage {
role: 'system' | 'user' | 'assistant' | 'tool';
Expand Down Expand Up @@ -166,7 +169,7 @@ export default defineEventHandler(async (event) => {
lastResponse = await callGitHubModels(githubToken, model, messages, aiTools);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.error(`GitHub Models API error (round ${round}):`, message);
logger.error(`GitHub Models API error (round ${round}):`, error);

if (message.includes('401') || message.includes('403') || message.includes('no_access')) {
throw createError({
Expand Down
55 changes: 54 additions & 1 deletion server/middleware/log.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
import { randomUUID } from 'node:crypto'
import { createLogger, getRuntimeLoggingConfig, isLogLevelAtLeast } from '../utils/logger'

const logger = createLogger('request')
const HEALTH_PATHS = new Set(['/api/health', '/api/live', '/api/ready'])
const STATIC_PREFIXES = ['/_nuxt/', '/assets/']
const STATIC_FILE_PATTERN = /\.(?:css|js|mjs|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/i

function getPath(url?: string): string {
try {
return new URL(url || '/', 'http://localhost').pathname
} catch {
return '/'
}
}

function getRequestId(event: any): string {
const header = event.node.req.headers?.['x-request-id']
if (Array.isArray(header)) return header[0] || randomUUID()
return header || randomUUID()
}

function shouldSkipPath(path: string): boolean {
return HEALTH_PATHS.has(path)
|| STATIC_PREFIXES.some(prefix => path.startsWith(prefix))
|| STATIC_FILE_PATTERN.test(path)
}

export default defineEventHandler((event) => {
console.log('Request: ' + event.method + ' ' + getRequestURL(event))
const requestId = getRequestId(event)
event.context.requestId = requestId
event.node.res.setHeader('x-request-id', requestId)

const path = getPath(event.node.req.url)
if (shouldSkipPath(path)) return

const config = getRuntimeLoggingConfig()
const forceRequestLogs = config.logRequests === 'true'
if (!forceRequestLogs && !isLogLevelAtLeast(config.logLevel, 'debug')) return

const startedAt = Date.now()
event.node.res.on('finish', () => {
const payload = {
method: event.method || event.node.req.method,
path,
status: event.node.res.statusCode,
durationMs: Date.now() - startedAt,
requestId,
}
if (forceRequestLogs) {
logger.info('Request completed', payload)
} else {
logger.debug('Request completed', payload)
}
})
})
4 changes: 3 additions & 1 deletion server/modules/github-app-auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createPrivateKey, createSign } from 'node:crypto'
import type { H3Event, EventHandlerRequest } from 'h3'
import { createLogger } from '../utils/logger'

// ofetch fallback for standalone (non-Nitro) environments.
// In Nitro the global `$fetch` is provided automatically, but this module is
Expand All @@ -11,6 +12,7 @@ const _fetch: typeof _ofetch = typeof $fetch !== 'undefined' ? ($fetch as typeof

const TOKEN_EXPIRY_BUFFER_SECONDS = 300 // refresh 5 min before expiry
const INSTALLATIONS_CACHE_TTL_SECONDS = 300 // re-list installations every 5 min
const logger = createLogger('github-app-auth')

export interface AppInstallation {
id: number
Expand Down Expand Up @@ -59,7 +61,7 @@ function signJWT(payload: Record<string, unknown>, privateKeyPem: string): strin
/** Build a short-lived JWT for GitHub App API calls. */
function buildAppJwt(appId: string, privateKey: string): string {
const now = Math.floor(Date.now() / 1000)
console.log('[github-app-auth] Building App JWT')
logger.debug('Building App JWT')
try {
return signJWT({ iss: appId, iat: now - 10, exp: now + 600 }, privateKey)
} catch (err: unknown) {
Expand Down
8 changes: 6 additions & 2 deletions server/plugins/auth-config-check.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { createLogger } from '../utils/logger'

const logger = createLogger('auth-config-check')

/**
* Startup self-check for auth misconfiguration that could silently disable
* issue-#398 user-data restriction.
Expand Down Expand Up @@ -33,8 +37,8 @@ export default defineNitroPlugin(() => {
|| process.env.NUXT_OAUTH_GITHUB_CLIENT_SECRET)

if (oauthLooksConfigured && !authConfigured) {
console.warn(
'[auth-config-check] OAuth credentials appear to be configured ' +
logger.warn(
'OAuth credentials appear to be configured ' +
'(NUXT_OAUTH_GITHUB_CLIENT_ID/SECRET) but none of ' +
'NUXT_PUBLIC_USING_GITHUB_AUTH / NUXT_PUBLIC_REQUIRE_AUTH / ' +
'NUXT_PUBLIC_IS_PUBLIC_APP / NUXT_PUBLIC_AUTH_PROVIDERS is set. ' +
Expand Down
11 changes: 7 additions & 4 deletions server/plugins/db-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*/

import { isDbConfigured } from '../storage/db';
import { createLogger } from '../utils/logger';

const logger = createLogger('db-init');

export default defineNitroPlugin(async () => {
if (!isDbConfigured()) return;
Expand All @@ -14,9 +17,9 @@ export default defineNitroPlugin(async () => {

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`Initializing PostgreSQL schema (attempt ${attempt}/${maxRetries})...`);
logger.info(`Initializing PostgreSQL schema (attempt ${attempt}/${maxRetries})...`);
await initSchema();
console.log('PostgreSQL schema ready');
logger.info('PostgreSQL schema ready');
return;
} catch (error: unknown) {
const pgCode = error && typeof error === 'object' && 'code' in error ? (error as { code: string }).code : '';
Expand All @@ -25,10 +28,10 @@ export default defineNitroPlugin(async () => {

if ((isStartingUp || isConnRefused) && attempt < maxRetries) {
const delay = baseDelay * attempt;
console.log(`PostgreSQL not ready yet, retrying in ${delay / 1000}s...`);
logger.warn(`PostgreSQL not ready yet, retrying in ${delay / 1000}s...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
console.error('Failed to initialize PostgreSQL schema:', error);
logger.error('Failed to initialize PostgreSQL schema:', error);
return;
}
}
Expand Down
6 changes: 5 additions & 1 deletion server/routes/auth/auth0.get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { createLogger } from '../../utils/logger'

const logger = createLogger('auth-auth0')

export default defineOAuthAuth0EventHandler({
async onSuccess(event, { user }) {
const email: string = user.email || ''
Expand All @@ -18,7 +22,7 @@ export default defineOAuthAuth0EventHandler({
return sendRedirect(event, defaultOrg ? getAppBaseURL(event) : appURL('/select-org', event))
},
onError(event, error) {
console.error('Auth0 OAuth error:', error)
logger.error('Auth0 OAuth error:', error)
return sendRedirect(event, getAppBaseURL(event))
}
})
8 changes: 6 additions & 2 deletions server/routes/auth/github.get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { createLogger } from '../../utils/logger'

const logger = createLogger('auth-github')

export default defineOAuthGitHubEventHandler({
config: {
// Default scopes: read:user for profile, read:org for org membership (used by org picker).
Expand Down Expand Up @@ -63,7 +67,7 @@ export default defineOAuthGitHubEventHandler({
return sendRedirect(event, appURL(`/orgs/${organizations[0]!.login}`, event))
}
} catch (err) {
console.error('Error fetching installations:', err)
logger.warn('Error fetching installations:', err)
// Fall through to /select-org which shows a text input fallback.
}
}
Expand All @@ -72,7 +76,7 @@ export default defineOAuthGitHubEventHandler({
},
// Optional, will return a json error and 401 status code by default
onError(event, error) {
console.error('GitHub OAuth error:', error)
logger.error('GitHub OAuth error:', error)
return sendRedirect(event, getAppBaseURL(event))
},
})
6 changes: 5 additions & 1 deletion server/routes/auth/google.get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { createLogger } from '../../utils/logger'

const logger = createLogger('auth-google')

export default defineOAuthGoogleEventHandler({
async onSuccess(event, { user }) {
if (!isUserAuthorized(event, { email: user.email })) {
Expand All @@ -22,7 +26,7 @@ export default defineOAuthGoogleEventHandler({
return sendRedirect(event, getAppBaseURL(event))
},
onError(event, error) {
console.error('Google OAuth error:', error)
logger.error('Google OAuth error:', error)
return sendRedirect(event, getAppBaseURL(event))
}
})
6 changes: 5 additions & 1 deletion server/routes/auth/keycloak.get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { createLogger } from '../../utils/logger'

const logger = createLogger('auth-keycloak')

export default defineOAuthKeycloakEventHandler({
async onSuccess(event, { user }) {
const email: string = user.email || ''
Expand All @@ -18,7 +22,7 @@ export default defineOAuthKeycloakEventHandler({
return sendRedirect(event, defaultOrg ? getAppBaseURL(event) : appURL('/select-org', event))
},
onError(event, error) {
console.error('Keycloak OAuth error:', error)
logger.error('Keycloak OAuth error:', error)
return sendRedirect(event, getAppBaseURL(event))
}
})
6 changes: 5 additions & 1 deletion server/routes/auth/microsoft.get.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { createLogger } from '../../utils/logger'

const logger = createLogger('auth-microsoft')

export default defineOAuthMicrosoftEventHandler({
async onSuccess(event, { user }) {
const email: string = user.mail || user.userPrincipalName || ''
Expand All @@ -18,7 +22,7 @@ export default defineOAuthMicrosoftEventHandler({
return sendRedirect(event, defaultOrg ? getAppBaseURL(event) : appURL('/select-org', event))
},
onError(event, error) {
console.error('Microsoft OAuth error:', error)
logger.error('Microsoft OAuth error:', error)
return sendRedirect(event, getAppBaseURL(event))
}
})
18 changes: 11 additions & 7 deletions server/services/github-copilot-usage-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
*/

import { isMockMode, mockRequestDownloadLinks, mockRequestUserDownloadLinks, generateMockReport } from './github-copilot-usage-api-mock';
import { createLogger } from '../utils/logger'

const newApiLogger = createLogger('new-api')
const userMetricsLogger = createLogger('user-metrics-api')

// Import $fetch for standalone (non-Nitro) environments
// In Nitro context, $fetch is auto-imported; this is a no-op there
Expand Down Expand Up @@ -660,7 +664,7 @@ export async function requestDownloadLinks(
} catch (error: unknown) {
// Log the response body for better debugging
if (error && typeof error === 'object' && 'data' in error) {
console.error('[new-api] GitHub error response:', JSON.stringify((error as { data: unknown }).data));
newApiLogger.error('GitHub error response:', { data: (error as { data: unknown }).data });
}
throw error;
}
Expand Down Expand Up @@ -895,7 +899,7 @@ export async function requestUserDownloadLinks(
return response;
} catch (error: unknown) {
if (error && typeof error === 'object' && 'data' in error) {
console.error('[user-metrics-api] GitHub error response:', JSON.stringify((error as { data: unknown }).data));
userMetricsLogger.error('GitHub error response:', { data: (error as { data: unknown }).data });
}
throw error;
}
Expand All @@ -921,7 +925,7 @@ export async function downloadUserReport(downloadUrl: string): Promise<UserRepor
records = parsed;
} else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.user_totals)) {
// Already in UserReport shape (mock data format)
console.info('[user-metrics-api] Download is pre-aggregated UserReport');
userMetricsLogger.info('Download is pre-aggregated UserReport');
return parsed as UserReport;
} else {
records = [parsed];
Expand All @@ -936,7 +940,7 @@ export async function downloadUserReport(downloadUrl: string): Promise<UserRepor
} else if (raw && typeof raw === 'object' && 'user_totals' in (raw as Record<string, unknown>)) {
return raw as UserReport;
} else {
console.error('[user-metrics-api] Unexpected download response type:', typeof raw);
userMetricsLogger.error('Unexpected download response type:', { responseType: typeof raw });
return { report_start_day: '', report_end_day: '', user_totals: [] };
}

Expand All @@ -951,7 +955,7 @@ export async function downloadUserReport(downloadUrl: string): Promise<UserRepor
// Also surface the raw per-day per-user rows as `day_totals` so callers
// can render a day-by-day chart without making 28 follow-up 1-day calls.
const dayRecords = records as UserDayRecord[];
console.info(`[user-metrics-api] Aggregating ${dayRecords.length} daily user records`);
userMetricsLogger.info(`Aggregating ${dayRecords.length} daily user records`);
const userTotals = aggregateUserDayRecords(dayRecords);
return {
report_start_day: first.report_start_day as string || '',
Expand All @@ -965,11 +969,11 @@ export async function downloadUserReport(downloadUrl: string): Promise<UserRepor

// Pre-aggregated format (mock data / already-aggregated) — array of UserTotals
if ('login' in first) {
console.info(`[user-metrics-api] Download has ${records.length} pre-aggregated user records`);
userMetricsLogger.info(`Download has ${records.length} pre-aggregated user records`);
return { report_start_day: '', report_end_day: '', user_totals: records as UserTotals[] };
}

console.warn('[user-metrics-api] Unknown record format. Keys:', Object.keys(first));
userMetricsLogger.warn('Unknown record format. Keys:', { keys: Object.keys(first) });
return { report_start_day: '', report_end_day: '', user_totals: [] };
}

Expand Down
8 changes: 6 additions & 2 deletions server/services/github-saml-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
* Results are cached per-org for 1 hour to avoid repeated GraphQL calls.
*/

import { createLogger } from '../utils/logger'

const logger = createLogger('saml-service')

interface SamlCache {
map: Map<string, string>
expiry: number
Expand Down Expand Up @@ -86,7 +90,7 @@ export async function fetchSamlIdentities(
const json = await resp.json() as GraphQLResponse

if (json.errors?.length) {
console.warn(`[saml-service] GraphQL errors for org "${org}":`, json.errors.map(e => e.message))
logger.warn(`GraphQL errors for org "${org}":`, { errors: json.errors.map(e => e.message) })
break
}

Expand All @@ -102,7 +106,7 @@ export async function fetchSamlIdentities(
after = ext.pageInfo.hasNextPage ? ext.pageInfo.endCursor : null
} while (after)
} catch (err) {
console.warn(`[saml-service] Failed to fetch SAML identities for org "${org}":`, err)
logger.warn(`Failed to fetch SAML identities for org "${org}":`, err)
}

samlCache.set(cacheKey, { map: nameIdToLogin, expiry: Date.now() + TTL_MS })
Expand Down
Loading
Loading