VIBN Frontend for Coolify deployment
This commit is contained in:
232
lib/server/logs.ts
Normal file
232
lib/server/logs.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Server-side logging utilities
|
||||
*
|
||||
* Logs project events to Firestore for monitoring, debugging, and analytics.
|
||||
*/
|
||||
|
||||
import { getAdminDb } from '@/lib/firebase/admin';
|
||||
import { FieldValue } from 'firebase-admin/firestore';
|
||||
import type { CreateProjectLogInput, ProjectLogEntry, ProjectLogFilters, ProjectLogStats } from '@/lib/types/logs';
|
||||
|
||||
/**
|
||||
* Log a project-related event
|
||||
*
|
||||
* This is a fire-and-forget operation - errors are logged but not thrown
|
||||
* to avoid impacting the main request flow.
|
||||
*
|
||||
* @param input - Log entry data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await logProjectEvent({
|
||||
* projectId: 'proj123',
|
||||
* userId: 'user456',
|
||||
* eventType: 'chat_interaction',
|
||||
* mode: 'vision_mode',
|
||||
* phase: 'vision_ready',
|
||||
* artifactsUsed: ['Product Model', '5 Vector Chunks'],
|
||||
* usedVectorSearch: true,
|
||||
* vectorChunkCount: 5,
|
||||
* promptVersion: '1.0',
|
||||
* modelUsed: 'gemini-2.0-flash-exp',
|
||||
* success: true,
|
||||
* errorMessage: null,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function logProjectEvent(input: CreateProjectLogInput): Promise<void> {
|
||||
try {
|
||||
const adminDb = getAdminDb();
|
||||
const docRef = adminDb.collection('project_logs').doc();
|
||||
|
||||
await docRef.set({
|
||||
...input,
|
||||
id: docRef.id,
|
||||
createdAt: FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
// Silent success
|
||||
} catch (error) {
|
||||
// Log to console but don't throw - logging should never break the main flow
|
||||
console.error('[Logs] Failed to log project event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query project logs with filters
|
||||
*
|
||||
* @param filters - Query filters
|
||||
* @returns Array of log entries
|
||||
*/
|
||||
export async function queryProjectLogs(
|
||||
filters: ProjectLogFilters
|
||||
): Promise<ProjectLogEntry[]> {
|
||||
try {
|
||||
const adminDb = getAdminDb();
|
||||
let query = adminDb.collection('project_logs').orderBy('createdAt', 'desc');
|
||||
|
||||
// Apply filters
|
||||
if (filters.projectId) {
|
||||
query = query.where('projectId', '==', filters.projectId) as any;
|
||||
}
|
||||
|
||||
if (filters.userId) {
|
||||
query = query.where('userId', '==', filters.userId) as any;
|
||||
}
|
||||
|
||||
if (filters.eventType) {
|
||||
query = query.where('eventType', '==', filters.eventType) as any;
|
||||
}
|
||||
|
||||
if (filters.mode) {
|
||||
query = query.where('mode', '==', filters.mode) as any;
|
||||
}
|
||||
|
||||
if (filters.phase) {
|
||||
query = query.where('phase', '==', filters.phase) as any;
|
||||
}
|
||||
|
||||
if (filters.success !== undefined) {
|
||||
query = query.where('success', '==', filters.success) as any;
|
||||
}
|
||||
|
||||
if (filters.startDate) {
|
||||
query = query.where('createdAt', '>=', filters.startDate) as any;
|
||||
}
|
||||
|
||||
if (filters.endDate) {
|
||||
query = query.where('createdAt', '<=', filters.endDate) as any;
|
||||
}
|
||||
|
||||
if (filters.limit) {
|
||||
query = query.limit(filters.limit) as any;
|
||||
}
|
||||
|
||||
const snapshot = await query.get();
|
||||
|
||||
return snapshot.docs.map((doc) => {
|
||||
const data = doc.data();
|
||||
return {
|
||||
...data,
|
||||
createdAt: data.createdAt?.toDate?.() ?? data.createdAt,
|
||||
} as ProjectLogEntry;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Logs] Failed to query project logs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated stats for a project
|
||||
*
|
||||
* @param projectId - Project ID to analyze
|
||||
* @param since - Optional date to filter from
|
||||
* @returns Aggregated statistics
|
||||
*/
|
||||
export async function getProjectLogStats(
|
||||
projectId: string,
|
||||
since?: Date
|
||||
): Promise<ProjectLogStats> {
|
||||
try {
|
||||
const filters: ProjectLogFilters = { projectId, limit: 1000 };
|
||||
if (since) {
|
||||
filters.startDate = since;
|
||||
}
|
||||
|
||||
const logs = await queryProjectLogs(filters);
|
||||
|
||||
const stats: ProjectLogStats = {
|
||||
totalLogs: logs.length,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
byEventType: {},
|
||||
byMode: {},
|
||||
avgVectorChunks: 0,
|
||||
vectorSearchUsageRate: 0,
|
||||
};
|
||||
|
||||
let totalVectorChunks = 0;
|
||||
let vectorSearchCount = 0;
|
||||
|
||||
logs.forEach((log) => {
|
||||
// Success/error counts
|
||||
if (log.success) {
|
||||
stats.successCount++;
|
||||
} else {
|
||||
stats.errorCount++;
|
||||
}
|
||||
|
||||
// By event type
|
||||
stats.byEventType[log.eventType] = (stats.byEventType[log.eventType] ?? 0) + 1;
|
||||
|
||||
// By mode
|
||||
if (log.mode) {
|
||||
stats.byMode[log.mode] = (stats.byMode[log.mode] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Vector search stats
|
||||
if (log.usedVectorSearch) {
|
||||
vectorSearchCount++;
|
||||
if (log.vectorChunkCount) {
|
||||
totalVectorChunks += log.vectorChunkCount;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate averages
|
||||
if (vectorSearchCount > 0) {
|
||||
stats.avgVectorChunks = totalVectorChunks / vectorSearchCount;
|
||||
stats.vectorSearchUsageRate = vectorSearchCount / logs.length;
|
||||
}
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[Logs] Failed to get project log stats:', error);
|
||||
return {
|
||||
totalLogs: 0,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
byEventType: {},
|
||||
byMode: {},
|
||||
avgVectorChunks: 0,
|
||||
vectorSearchUsageRate: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete old logs (for maintenance/cleanup)
|
||||
*
|
||||
* @param before - Delete logs older than this date
|
||||
* @returns Number of logs deleted
|
||||
*/
|
||||
export async function deleteOldLogs(before: Date): Promise<number> {
|
||||
try {
|
||||
const adminDb = getAdminDb();
|
||||
const snapshot = await adminDb
|
||||
.collection('project_logs')
|
||||
.where('createdAt', '<', before)
|
||||
.limit(500) // Process in batches to avoid overwhelming Firestore
|
||||
.get();
|
||||
|
||||
if (snapshot.empty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const batch = adminDb.batch();
|
||||
snapshot.docs.forEach((doc) => {
|
||||
batch.delete(doc.ref);
|
||||
});
|
||||
|
||||
await batch.commit();
|
||||
|
||||
console.log(`[Logs] Deleted ${snapshot.size} old logs`);
|
||||
|
||||
return snapshot.size;
|
||||
} catch (error) {
|
||||
console.error('[Logs] Failed to delete old logs:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user