refactor: redesign logging system with single file + size-based cleanup

- Change LogAction to string type for flexibility
- Rename todayLogs to memoryLogs
- Use single log file (todoist-sync-logs.json)
- Add size-based cleanup (maxLogFileSize, logRetentionPercent)
- Remove batch buffer, use memoryLogs directly
- Update settings with new log config options
- Remove legacy logs array and logRetentionDays
This commit is contained in:
HeroBlackInk 2026-02-22 00:19:56 +08:00
parent 1450a120b3
commit c209a6d1ea
4 changed files with 126 additions and 332 deletions

View file

@ -302,9 +302,9 @@ export default class UltimateTodoistSyncForObsidian extends Plugin {
// Flush log buffer before unloading
try {
await this.logOperation?.flushLogBuffer();
await this.logOperation?.flushToFile();
} catch (error) {
console.error('An error occurred in flushLogBuffer:', error);
console.error('An error occurred in flushToFile:', error);
}
await this.saveSettings()
@ -838,9 +838,9 @@ export default class UltimateTodoistSyncForObsidian extends Plugin {
// Flush log buffer after sync completes
try {
await this.logOperation?.flushLogBuffer();
await this.logOperation?.flushToFile();
} catch (error) {
console.error('An error occurred in flushLogBuffer:', error);
console.error('An error occurred in flushToFile:', error);
}
console.log("Todoist scheduled synchronization task completed at", new Date().toLocaleString());

View file

@ -3,158 +3,32 @@ import UltimateTodoistSyncForObsidian from "../main";
export interface LogEntry {
timestamp: number;
action: LogAction;
action: string;
details: string;
filePath?: string;
taskId?: string;
}
export type LogAction =
// Task operations - User actions in Obsidian
| 'OBSIDIAN_TASK_CREATED'
| 'OBSIDIAN_TASK_MODIFIED'
| 'OBSIDIAN_TASK_DELETED'
| 'OBSIDIAN_TASK_COMPLETED'
| 'OBSIDIAN_TASK_REOPENED'
| 'OBSIDIAN_TASK_CONTENT_CHANGED'
| 'OBSIDIAN_TASK_DUEDATE_CHANGED'
| 'OBSIDIAN_TASK_PRIORITY_CHANGED'
| 'OBSIDIAN_TASK_LABEL_CHANGED'
// File operations
| 'FILE_MODIFIED'
| 'FILE_RENAMED'
| 'FILE_TODOIST_TAG_ADDED'
| 'FILE_TASK_COMPLETED'
| 'FILE_TASK_UNCOMPLETED'
| 'FILE_TASK_CONTENT_SYNCED'
| 'FILE_TASK_DUEDATE_SYNCED'
| 'FILE_TASK_NOTE_ADDED'
// Cache operations
| 'CACHE_TASK_ADDED'
| 'CACHE_TASK_UPDATED'
| 'CACHE_TASK_DELETED'
| 'CACHE_TASK_NONACTIVE'
| 'CACHE_TASK_CONFLICTED'
| 'CACHE_TASK_ISSUE'
| 'CACHE_TASK_NON_ID'
| 'CACHE_TASK_COMPLETED'
| 'CACHE_TASK_REOPENED'
| 'CACHE_FILE_METADATA_UPDATED'
| 'CACHE_FILE_METADATA_DELETED'
| 'CACHE_RENAMED'
| 'CACHE_REBUILT'
| 'DATABASE_CHECK'
| 'DATABASE_CHECKED'
// Todoist API operations
| 'TODOIST_TASK_CREATED'
| 'TODOIST_TASK_UPDATED'
| 'TODOIST_TASK_DELETED'
| 'TODOIST_TASK_COMPLETED'
| 'TODOIST_TASK_REOPENED'
// Sync operations
| 'SYNC_START'
| 'SYNC_COMPLETED'
| 'SYNC_ERROR'
| 'BACKUP_CREATED'
| 'PROJECT_UPDATED'
| 'PLUGIN_INITIALIZED'
| 'SETTINGS_UPDATED';
export type LogAction = string;
export class LogOperation {
app: App;
plugin: UltimateTodoistSyncForObsidian;
private maxLogs = 500;
private currentDate = '';
private todayLogs: LogEntry[] = [];
private lastCleanupDate = '';
private logBuffer: LogEntry[] = [];
private app: App;
private plugin: UltimateTodoistSyncForObsidian;
private memoryLogs: LogEntry[] = [];
private unsavedCount = 0;
private readonly BATCH_SIZE = 20;
constructor(app: App, plugin: UltimateTodoistSyncForObsidian) {
this.app = app;
this.plugin = plugin;
this.initializeLogs();
this.loadTodayLogsFromFile();
this.loadLogsFromFile();
}
private initializeLogs(): void {
const today = this.getTodayDateString();
this.currentDate = today;
this.lastCleanupDate = today;
if (this.plugin.settings.logFileEnabled) {
this.migrateLegacyLogs();
}
this.todayLogs = [];
if (this.plugin.settings.debugMode) {
console.log(`[LogOperation] Initialized`);
}
}
private async loadTodayLogsFromFile(): Promise<void> {
if (!this.plugin.storagePathManager) {
return;
}
try {
const logPath = await this.plugin.storagePathManager.getTodayLogPath();
const adapter = this.plugin.app.vault.adapter;
const exists = await adapter.exists(logPath);
if (exists) {
const content = await adapter.read(logPath);
try {
const data = JSON.parse(content);
if (Array.isArray(data)) {
this.todayLogs = data;
if (this.plugin.settings.debugMode) {
console.log(`[LogOperation] Loaded ${data.length} logs from today's file`);
}
}
} catch {
this.todayLogs = [];
}
}
} catch (error) {
console.error('[LogOperation] Failed to load today logs from file:', error);
this.todayLogs = [];
}
}
private getTodayDateString(): string {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
}
private migrateLegacyLogs(): void {
const legacyLogs = this.plugin.settings.logs;
if (legacyLogs && legacyLogs.length > 0) {
if (this.plugin.settings.debugMode) {
console.log(`[LogOperation] Migrating ${legacyLogs.length} legacy logs to file`);
}
for (const entry of legacyLogs) {
this.appendToLogFile(entry);
}
this.plugin.settings.logs = [];
this.plugin.saveSettings();
}
}
log(action: LogAction, details: string, filePath?: string, taskId?: string): void {
log(action: string, details: string, filePath?: string, taskId?: string): void {
if (!this.plugin.settings.enableLog) {
return;
}
const today = this.getTodayDateString();
if (today !== this.currentDate) {
this.handleDateChange(today);
}
const logEntry: LogEntry = {
timestamp: Date.now(),
action,
@ -163,190 +37,154 @@ export class LogOperation {
taskId
};
this.todayLogs.push(logEntry);
if (this.todayLogs.length > this.maxLogs) {
this.todayLogs.shift();
}
this.memoryLogs.push(logEntry);
if (this.plugin.settings.logFileEnabled) {
this.logBuffer.push(logEntry);
if (this.logBuffer.length >= this.BATCH_SIZE) {
this.flushLogBuffer();
this.unsavedCount++;
if (this.unsavedCount >= this.BATCH_SIZE) {
this.flushToFile();
}
}
this.cleanOldLogsIfNeeded();
if (this.plugin.settings.debugMode) {
console.log(`[Log] ${action}: ${details}`, { filePath, taskId });
}
}
private handleDateChange(newDate: string): void {
this.todayLogs = [];
this.currentDate = newDate;
if (this.plugin.settings.debugMode) {
console.log(`[LogOperation] Date changed to ${newDate}, buffer cleared`);
}
}
private async appendToLogFile(entry: LogEntry): Promise<void> {
if (!this.plugin.storagePathManager) {
console.warn('[LogOperation] StoragePathManager not initialized');
private async loadLogsFromFileAsync(): Promise<void> {
if (!this.plugin.settings.logFileEnabled || !this.plugin.storagePathManager) {
return;
}
try {
await this.plugin.storagePathManager.ensureAllDirs();
const logPath = await this.plugin.storagePathManager.getTodayLogPath();
await this.plugin.storagePathManager.appendJsonToFile(logPath, entry);
} catch (error) {
console.error('[LogOperation] Failed to append log to file:', error);
}
}
public async flushLogBuffer(): Promise<void> {
if (this.logBuffer.length === 0) {
return;
}
if (!this.plugin.storagePathManager) {
console.warn('[LogOperation] StoragePathManager not initialized');
this.logBuffer = [];
return;
}
try {
await this.plugin.storagePathManager.ensureAllDirs();
const logPath = await this.plugin.storagePathManager.getTodayLogPath();
const adapter = this.plugin.app.vault.adapter;
let data: LogEntry[] = [];
const logPath = this.plugin.storagePathManager.getLogFilePath();
const adapter = this.app.vault.adapter;
const exists = await adapter.exists(logPath);
if (exists) {
const content = await adapter.read(logPath);
try {
data = JSON.parse(content);
if (!Array.isArray(data)) {
data = [];
if (content) {
try {
const data = JSON.parse(content);
if (Array.isArray(data)) {
this.memoryLogs = data;
if (this.plugin.settings.debugMode) {
console.log(`[LogOperation] Loaded ${data.length} logs from file`);
}
}
} catch {
this.memoryLogs = [];
}
} catch {
data = [];
}
}
data.push(...this.logBuffer);
await adapter.write(logPath, JSON.stringify(data, null, 2));
this.logBuffer = [];
if (this.plugin.settings.debugMode) {
console.log(`[LogOperation] Flushed ${this.logBuffer.length} logs to file`);
}
} catch (error) {
console.error('[LogOperation] Failed to flush log buffer:', error);
console.error('[LogOperation] Failed to load logs from file:', error);
this.memoryLogs = [];
}
}
private cleanOldLogsIfNeeded(): void {
const today = this.getTodayDateString();
if (today === this.lastCleanupDate) {
private loadLogsFromFile(): void {
this.loadLogsFromFileAsync();
}
public async flushToFile(): Promise<void> {
if (!this.plugin.settings.logFileEnabled || !this.plugin.storagePathManager) {
this.unsavedCount = 0;
return;
}
this.lastCleanupDate = today;
if (this.plugin.settings.logFileEnabled && this.plugin.storagePathManager) {
this.cleanOldLogs().catch(error => {
console.error('[LogOperation] Failed to clean old logs:', error);
});
}
}
private async cleanOldLogs(): Promise<void> {
const retentionDays = this.plugin.settings.logRetentionDays || 365;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
const cutoffString = `${cutoffDate.getFullYear()}-${String(cutoffDate.getMonth() + 1).padStart(2, '0')}-${String(cutoffDate.getDate()).padStart(2, '0')}`;
try {
const logsPath = await this.plugin.storagePathManager.getLogsPath();
const files = await this.plugin.storagePathManager.listFiles(logsPath);
await this.plugin.storagePathManager.ensureDir(
this.plugin.storagePathManager.getLogsBasePath()
);
for (const filePath of files) {
const fileName = filePath.split('/').pop() || '';
const dateMatch = fileName.match(/_(\d{4}-\d{2}-\d{2})\.json$/);
if (dateMatch) {
const fileDate = dateMatch[1];
if (fileDate < cutoffString) {
await this.plugin.storagePathManager.deleteFile(filePath);
if (this.plugin.settings.debugMode) {
console.log(`[LogOperation] Deleted old log file: ${filePath}`);
const logPath = this.plugin.storagePathManager.getLogFilePath();
const adapter = this.app.vault.adapter;
let currentLogs: LogEntry[] = [];
const exists = await adapter.exists(logPath);
if (exists) {
const content = await adapter.read(logPath);
if (content) {
try {
currentLogs = JSON.parse(content);
if (!Array.isArray(currentLogs)) {
currentLogs = [];
}
} catch {
currentLogs = [];
}
}
}
const maxSize = this.plugin.settings.maxLogFileSize || (1024 * 1024);
const content = JSON.stringify(currentLogs, null, 2);
const currentSize = content.length;
if (currentSize > maxSize) {
await this.cleanupBySize();
} else {
currentLogs.push(...this.memoryLogs);
await adapter.write(logPath, JSON.stringify(currentLogs, null, 2));
}
this.memoryLogs = [];
this.unsavedCount = 0;
if (this.plugin.settings.debugMode) {
console.log(`[LogOperation] Flushed logs to file`);
}
} catch (error) {
console.error('[LogOperation] Error cleaning old logs:', error);
console.error('[LogOperation] Failed to flush logs to file:', error);
}
}
private async cleanupBySize(): Promise<void> {
try {
const logPath = this.plugin.storagePathManager!.getLogFilePath();
const adapter = this.app.vault.adapter;
let currentLogs: LogEntry[] = [];
const exists = await adapter.exists(logPath);
if (exists) {
const content = await adapter.read(logPath);
if (content) {
try {
currentLogs = JSON.parse(content);
if (!Array.isArray(currentLogs)) {
currentLogs = [];
}
} catch {
currentLogs = [];
}
}
}
const retentionPercent = this.plugin.settings.logRetentionPercent || 80;
const keepCount = Math.floor(currentLogs.length * retentionPercent / 100);
const trimmedLogs = currentLogs.slice(-keepCount);
trimmedLogs.push(...this.memoryLogs);
await adapter.write(logPath, JSON.stringify(trimmedLogs, null, 2));
this.memoryLogs = trimmedLogs.slice(-this.BATCH_SIZE);
if (this.plugin.settings.debugMode) {
console.log(`[LogOperation] Cleaned up logs, kept ${keepCount} entries`);
}
} catch (error) {
console.error('[LogOperation] Failed to cleanup logs:', error);
}
}
getLogs(): LogEntry[] {
return this.todayLogs;
}
getAllLogs(): LogEntry[] {
if (!this.plugin.settings.logFileEnabled || !this.plugin.storagePathManager) {
return this.todayLogs;
}
return this.todayLogs;
}
async getLogsFromFiles(days = 30): Promise<LogEntry[]> {
if (!this.plugin.settings.logFileEnabled || !this.plugin.storagePathManager) {
return [];
}
const logs: LogEntry[] = [];
try {
const logsPath = await this.plugin.storagePathManager.getLogsPath();
const files = await this.plugin.storagePathManager.listFiles(logsPath);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
for (const filePath of files) {
const fileName = filePath.split('/').pop() || '';
const dateMatch = fileName.match(/_(\d{4}-\d{2}-\d{2})\.json$/);
if (dateMatch) {
const fileDate = new Date(dateMatch[1]);
if (fileDate >= cutoffDate) {
const data = await this.plugin.storagePathManager.readJsonFile<LogEntry[]>(filePath);
if (data && Array.isArray(data)) {
logs.push(...data);
}
}
}
}
} catch (error) {
console.error('[LogOperation] Failed to read logs from files:', error);
}
return logs;
return this.memoryLogs;
}
getLogsAsText(): string {
const logs = this.getLogs();
if (logs.length === 0) {
return 'No logs available. (Today\'s logs will appear here)';
return 'No logs available.';
}
return logs.map(log => {
@ -357,49 +195,8 @@ export class LogOperation {
}).join('\n');
}
async getLogsAsTextFromFiles(days = 7): Promise<string> {
const logs = await this.getLogsFromFiles(days);
if (logs.length === 0) {
return 'No logs available in files.';
}
logs.sort((a, b) => a.timestamp - b.timestamp);
return logs.map(log => {
const date = new Date(log.timestamp).toLocaleString();
const fileInfo = log.filePath ? ` [${log.filePath}]` : '';
const taskInfo = log.taskId ? ` (task: ${log.taskId})` : '';
return `[${date}] ${log.action}: ${log.details}${fileInfo}${taskInfo}`;
}).join('\n');
}
clearLogs(): void {
this.todayLogs = [];
this.flushLogBuffer();
}
async exportLogsToFile(): Promise<string | null> {
const logsText = await this.getLogsAsTextFromFiles(365);
try {
const logsPath = await this.plugin.storagePathManager.getLogsPath();
await this.plugin.storagePathManager.ensureDir(logsPath);
const exportFileName = `logs-export-${this.getTodayDateString()}.md`;
const exportPath = `${logsPath}/${exportFileName}`;
const adapter = this.app.vault.adapter;
await adapter.write(exportPath, logsText);
return exportPath;
} catch (error) {
console.error('[LogOperation] Failed to export logs:', error);
return null;
}
}
exportLogs(): string {
return this.getLogsAsText();
this.memoryLogs = [];
this.flushToFile();
}
}

View file

@ -1,6 +1,5 @@
import { App, Notice, PluginSettingTab, Setting } from 'obsidian';
import UltimateTodoistSyncForObsidian from "../main";
import { LogAction } from './logOperation';
export interface UltimateTodoistSyncSettings {
initialized: boolean;
@ -30,15 +29,9 @@ export interface UltimateTodoistSyncSettings {
syncDataCache: Record<string, any> | null;
deviceIdGenerated: boolean;
enableLog: boolean;
logs: Array<{
timestamp: number;
action: LogAction;
details: string;
filePath?: string;
taskId?: string;
}>;
logFileEnabled: boolean;
logRetentionDays: number;
maxLogFileSize: number;
logRetentionPercent: number;
maxBackupsPerFile: number;
storageDirectory: string;
lastStorageDirectory: string | null;
@ -65,9 +58,9 @@ export const DEFAULT_SETTINGS: UltimateTodoistSyncSettings = {
syncDataCache: null,
deviceIdGenerated: false,
enableLog: true,
logs: [],
logFileEnabled: true,
logRetentionDays: 365,
maxLogFileSize: 1024 * 1024,
logRetentionPercent: 80,
maxBackupsPerFile: 100,
storageDirectory: 'ultimate-todoist-sync',
lastStorageDirectory: null,

View file

@ -24,6 +24,10 @@ export class StoragePathManager {
return `${this.getBasePath()}/logs`;
}
getLogFilePath(): string {
return `${this.getLogsBasePath()}/todoist-sync-logs.json`;
}
getBackupsBasePath(): string {
return `${this.getBasePath()}/backups`;
}