MCP GitHub Issue Server
by sammcj
import { Logger } from '../../../logging/index.js';
import { ErrorCodes, createError } from '../../../errors/index.js';
import { SqliteConnection } from '../database/connection.js';
import { TaskStatus } from '../../../types/task.js';
let logger: Logger | undefined;
function getLogger(): Logger {
if (!logger) {
try {
logger = Logger.getInstance().child({ component: 'StorageMetrics' });
} catch (error) {
// If logger isn't initialized yet, create a minimal logger
console.warn('Logger not initialized, using console fallback');
logger = {
error: console.error,
warn: console.warn,
info: console.info,
debug: console.debug,
child: () => logger!,
} as unknown as Logger;
}
}
return logger;
}
export interface StorageMetrics {
tasks: {
total: number;
byStatus: Record<TaskStatus, number>;
noteCount: number;
dependencyCount: number;
};
storage: {
totalSize: number;
pageSize: number;
pageCount: number;
walSize: number;
cache: {
hits: number;
misses: number;
size: number;
maxSize: number;
hitRate: number;
evictions: number;
memoryUsage: number;
};
};
}
export class SqliteMetrics {
constructor(private readonly connection: SqliteConnection) {}
/**
* Get storage metrics
*/
async getMetrics(): Promise<StorageMetrics> {
const [taskStats, statusStats, storageStats] = await Promise.all([
this.getTaskStats(),
this.getStatusStats(),
this.getStorageStats(),
]);
// Convert status stats array to object
const byStatus = Object.values(TaskStatus).reduce(
(acc, status) => {
acc[status] = statusStats.find(s => s.status === status)?.count || 0;
return acc;
},
{} as Record<TaskStatus, number>
);
const memUsage = process.memoryUsage();
return {
tasks: {
total: taskStats.total,
byStatus,
noteCount: taskStats.noteCount,
dependencyCount: taskStats.dependencyCount,
},
storage: {
totalSize: storageStats.totalSize,
pageSize: storageStats.pageSize,
pageCount: storageStats.pageCount,
walSize: storageStats.walSize,
cache: {
hits: 0,
misses: 0,
size: 0,
maxSize: 0,
hitRate: 0,
evictions: 0,
memoryUsage: memUsage.heapUsed,
},
},
};
}
/**
* Run database maintenance
*/
async maintenance(): Promise<void> {
try {
await this.connection.executeWithRetry(async () => {
await this.connection.execute(async db => {
// Run VACUUM to reclaim space and defragment
await db.exec('VACUUM;');
// Analyze tables for query optimization
await db.exec('ANALYZE tasks;');
await db.exec('ANALYZE sqlite_master;');
// Update statistics
await db.exec('PRAGMA optimize;');
// Use connection's checkpoint method which uses WALManager
await this.connection.checkpoint();
}, 'maintenance');
}, 'maintenance');
getLogger().info('Storage maintenance completed');
} catch (error) {
getLogger().error('Storage maintenance failed', { error });
throw createError(
ErrorCodes.STORAGE_ERROR,
'Storage maintenance failed',
error instanceof Error ? error.message : String(error)
);
}
}
/**
* Get task statistics
*/
private async getTaskStats(): Promise<{
total: number;
noteCount: number;
dependencyCount: number;
}> {
return this.connection.execute(async db => {
const result = await db.get<{
total: number;
noteCount: number;
dependencyCount: number;
}>(`
SELECT
COUNT(*) as total,
SUM(
CASE
WHEN notes IS NOT NULL
OR planning_notes IS NOT NULL
OR progress_notes IS NOT NULL
OR completion_notes IS NOT NULL
OR troubleshooting_notes IS NOT NULL
THEN 1
ELSE 0
END
) as noteCount,
SUM(
CASE
WHEN dependencies IS NOT NULL
AND json_valid(dependencies)
AND json_array_length(dependencies) > 0
THEN json_array_length(dependencies)
ELSE 0
END
) as dependencyCount
FROM tasks
`);
return {
total: Number(result?.total || 0),
noteCount: Number(result?.noteCount || 0),
dependencyCount: Number(result?.dependencyCount || 0),
};
}, 'getTaskStats');
}
/**
* Get status statistics
*/
private async getStatusStats(): Promise<Array<{ status: string; count: number }>> {
return this.connection.execute(async db => {
return db.all<{ status: string; count: number }[]>(`
SELECT status, COUNT(*) as count
FROM tasks
GROUP BY status
`);
}, 'getStatusStats');
}
/**
* Get storage statistics
*/
private async getStorageStats(): Promise<{
totalSize: number;
pageSize: number;
pageCount: number;
walSize: number;
}> {
const [pageStats, walStats] = await Promise.all([
this.connection.execute(async db => {
return db.get<{
page_count: number;
page_size: number;
}>(`
SELECT
page_count,
page_size
FROM pragma_page_count, pragma_page_size
LIMIT 1
`);
}, 'getPageStats'),
this.getWalSize(),
]);
const pageSize = Number(pageStats?.page_size || 0);
const pageCount = Number(pageStats?.page_count || 0);
return {
totalSize: pageSize * pageCount,
pageSize,
pageCount,
walSize: walStats,
};
}
/**
* Get WAL size from WALManager metrics
*/
private async getWalSize(): Promise<number> {
try {
const metrics = await this.connection.getWALMetrics();
return metrics?.walSize ?? 0;
} catch {
return 0; // WAL metrics not available
}
}
}