mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-30 14:20:59 +01:00
refactor(logging): simplify to daily-only rotation
- Remove rotation_strategy and max_file_size settings - Always use YYYY-MM-DD.log format for log files - Update cleanup job to parse dates from filenames - Simplify settings UI to retention days only
This commit is contained in:
@@ -55,11 +55,11 @@
|
||||
{step}
|
||||
{required}
|
||||
{disabled}
|
||||
class="block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 pr-10 text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none disabled:bg-neutral-100 disabled:text-neutral-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500 dark:disabled:bg-neutral-900 dark:disabled:text-neutral-600 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
class="block w-full [appearance:textfield] rounded-lg border border-neutral-300 bg-white px-3 py-2 pr-10 text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none disabled:bg-neutral-100 disabled:text-neutral-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500 dark:disabled:bg-neutral-900 dark:disabled:text-neutral-600 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
|
||||
<!-- Custom increment/decrement buttons -->
|
||||
<div class="absolute right-1 top-1/2 flex -translate-y-1/2 flex-col">
|
||||
<div class="absolute top-1/2 right-1 flex -translate-y-1/2 flex-col">
|
||||
<button
|
||||
type="button"
|
||||
on:click={increment}
|
||||
|
||||
@@ -51,7 +51,9 @@ class DatabaseManager {
|
||||
await Deno.mkdir(config.paths.data, { recursive: true });
|
||||
|
||||
// Check if database exists before opening
|
||||
const dbExists = await Deno.stat(config.paths.database).then(() => true).catch(() => false);
|
||||
const dbExists = await Deno.stat(config.paths.database)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!dbExists) {
|
||||
await logger.warn('Database file does not exist, creating new database', {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { migration as migration002 } from './migrations/002_remove_sync_profile.
|
||||
import { migration as migration003 } from './migrations/003_create_log_settings.ts';
|
||||
import { migration as migration004 } from './migrations/004_create_jobs_tables.ts';
|
||||
import { migration as migration005 } from './migrations/005_create_backup_settings.ts';
|
||||
import { migration as migration006 } from './migrations/006_simplify_log_settings.ts';
|
||||
|
||||
export interface Migration {
|
||||
version: number;
|
||||
@@ -223,13 +224,14 @@ export const migrationRunner = new MigrationRunner();
|
||||
* Helper function to load migrations
|
||||
* Returns all statically imported migrations
|
||||
*/
|
||||
export async function loadMigrations(): Promise<Migration[]> {
|
||||
export function loadMigrations(): Migration[] {
|
||||
const migrations: Migration[] = [
|
||||
migration001,
|
||||
migration002,
|
||||
migration003,
|
||||
migration004,
|
||||
migration005,
|
||||
migration006
|
||||
];
|
||||
|
||||
// Sort by version number
|
||||
@@ -240,6 +242,6 @@ export async function loadMigrations(): Promise<Migration[]> {
|
||||
* Run migrations
|
||||
*/
|
||||
export async function runMigrations(migrations?: Migration[]): Promise<void> {
|
||||
const migrationsToRun = migrations ?? (await loadMigrations());
|
||||
const migrationsToRun = migrations ?? loadMigrations();
|
||||
await migrationRunner.up(migrationsToRun);
|
||||
}
|
||||
|
||||
121
src/db/migrations/006_simplify_log_settings.ts
Normal file
121
src/db/migrations/006_simplify_log_settings.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { Migration } from '../migrations.ts';
|
||||
|
||||
/**
|
||||
* Migration 006: Simplify log settings to daily-only rotation
|
||||
*
|
||||
* Removes rotation_strategy and max_file_size columns.
|
||||
* Logs will now always use daily rotation (YYYY-MM-DD.log format).
|
||||
*/
|
||||
|
||||
export const migration: Migration = {
|
||||
version: 6,
|
||||
name: 'Simplify log settings to daily-only rotation',
|
||||
|
||||
up: `
|
||||
-- Remove rotation strategy and max file size columns
|
||||
-- SQLite doesn't support DROP COLUMN directly in all versions,
|
||||
-- so we need to recreate the table
|
||||
|
||||
-- Create new table with updated schema
|
||||
CREATE TABLE log_settings_new (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
|
||||
-- Retention
|
||||
retention_days INTEGER NOT NULL DEFAULT 30,
|
||||
|
||||
-- Log Level
|
||||
min_level TEXT NOT NULL DEFAULT 'INFO' CHECK (min_level IN ('DEBUG', 'INFO', 'WARN', 'ERROR')),
|
||||
|
||||
-- Enable/Disable
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
file_logging INTEGER NOT NULL DEFAULT 1,
|
||||
console_logging INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
-- Metadata
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Copy data from old table (excluding removed columns)
|
||||
INSERT INTO log_settings_new (
|
||||
id,
|
||||
retention_days,
|
||||
min_level,
|
||||
enabled,
|
||||
file_logging,
|
||||
console_logging,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
retention_days,
|
||||
min_level,
|
||||
enabled,
|
||||
file_logging,
|
||||
console_logging,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM log_settings;
|
||||
|
||||
-- Drop old table
|
||||
DROP TABLE log_settings;
|
||||
|
||||
-- Rename new table to original name
|
||||
ALTER TABLE log_settings_new RENAME TO log_settings;
|
||||
`,
|
||||
|
||||
down: `
|
||||
-- Recreate table with rotation_strategy and max_file_size columns
|
||||
|
||||
CREATE TABLE log_settings_new (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
|
||||
-- Rotation & Retention
|
||||
rotation_strategy TEXT NOT NULL DEFAULT 'daily' CHECK (rotation_strategy IN ('daily', 'size', 'both')),
|
||||
retention_days INTEGER NOT NULL DEFAULT 30,
|
||||
max_file_size INTEGER NOT NULL DEFAULT 100,
|
||||
|
||||
-- Log Level
|
||||
min_level TEXT NOT NULL DEFAULT 'INFO' CHECK (min_level IN ('DEBUG', 'INFO', 'WARN', 'ERROR')),
|
||||
|
||||
-- Enable/Disable
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
file_logging INTEGER NOT NULL DEFAULT 1,
|
||||
console_logging INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
-- Metadata
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Copy data back, adding default values for removed columns
|
||||
INSERT INTO log_settings_new (
|
||||
id,
|
||||
rotation_strategy,
|
||||
retention_days,
|
||||
max_file_size,
|
||||
min_level,
|
||||
enabled,
|
||||
file_logging,
|
||||
console_logging,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
'daily',
|
||||
retention_days,
|
||||
100,
|
||||
min_level,
|
||||
enabled,
|
||||
file_logging,
|
||||
console_logging,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM log_settings;
|
||||
|
||||
DROP TABLE log_settings;
|
||||
ALTER TABLE log_settings_new RENAME TO log_settings;
|
||||
`
|
||||
};
|
||||
@@ -5,9 +5,7 @@ import { db } from '../db.ts';
|
||||
*/
|
||||
export interface LogSettings {
|
||||
id: number;
|
||||
rotation_strategy: 'daily' | 'size' | 'both';
|
||||
retention_days: number;
|
||||
max_file_size: number;
|
||||
min_level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
||||
enabled: number;
|
||||
file_logging: number;
|
||||
@@ -17,9 +15,7 @@ export interface LogSettings {
|
||||
}
|
||||
|
||||
export interface UpdateLogSettingsInput {
|
||||
rotationStrategy?: 'daily' | 'size' | 'both';
|
||||
retentionDays?: number;
|
||||
maxFileSize?: number;
|
||||
minLevel?: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
||||
enabled?: boolean;
|
||||
fileLogging?: boolean;
|
||||
@@ -45,18 +41,10 @@ export const logSettingsQueries = {
|
||||
const updates: string[] = [];
|
||||
const params: (string | number)[] = [];
|
||||
|
||||
if (input.rotationStrategy !== undefined) {
|
||||
updates.push('rotation_strategy = ?');
|
||||
params.push(input.rotationStrategy);
|
||||
}
|
||||
if (input.retentionDays !== undefined) {
|
||||
updates.push('retention_days = ?');
|
||||
params.push(input.retentionDays);
|
||||
}
|
||||
if (input.maxFileSize !== undefined) {
|
||||
updates.push('max_file_size = ?');
|
||||
params.push(input.maxFileSize);
|
||||
}
|
||||
if (input.minLevel !== undefined) {
|
||||
updates.push('min_level = ?');
|
||||
params.push(input.minLevel);
|
||||
@@ -96,9 +84,7 @@ export const logSettingsQueries = {
|
||||
reset(): boolean {
|
||||
const affected = db.execute(`
|
||||
UPDATE log_settings SET
|
||||
rotation_strategy = 'daily',
|
||||
retention_days = 30,
|
||||
max_file_size = 100,
|
||||
min_level = 'INFO',
|
||||
enabled = 1,
|
||||
file_logging = 1,
|
||||
|
||||
@@ -50,10 +50,8 @@ CREATE TABLE arr_instances (
|
||||
CREATE TABLE log_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
|
||||
-- Rotation & Retention
|
||||
rotation_strategy TEXT NOT NULL DEFAULT 'daily' CHECK (rotation_strategy IN ('daily', 'size', 'both')),
|
||||
-- Retention
|
||||
retention_days INTEGER NOT NULL DEFAULT 30,
|
||||
max_file_size INTEGER NOT NULL DEFAULT 100,
|
||||
|
||||
-- Log Level
|
||||
min_level TEXT NOT NULL DEFAULT 'INFO' CHECK (min_level IN ('DEBUG', 'INFO', 'WARN', 'ERROR')),
|
||||
|
||||
@@ -99,7 +99,9 @@ export const actions: Actions = {
|
||||
|
||||
try {
|
||||
const backupsDir = config.paths.backups;
|
||||
const filename = file.name.startsWith('backup-') ? file.name : `backup-uploaded-${Date.now()}.tar.gz`;
|
||||
const filename = file.name.startsWith('backup-')
|
||||
? file.name
|
||||
: `backup-uploaded-${Date.now()}.tar.gz`;
|
||||
const backupPath = `${backupsDir}/${filename}`;
|
||||
|
||||
// Check if file already exists
|
||||
@@ -179,12 +181,7 @@ export const actions: Actions = {
|
||||
|
||||
// Extract backup to base directory (will overwrite data directory)
|
||||
const command = new Deno.Command('tar', {
|
||||
args: [
|
||||
'-xzf',
|
||||
backupPath,
|
||||
'-C',
|
||||
config.paths.base
|
||||
],
|
||||
args: ['-xzf', backupPath, '-C', config.paths.base],
|
||||
stdout: 'piped',
|
||||
stderr: 'piped'
|
||||
});
|
||||
@@ -205,7 +202,10 @@ export const actions: Actions = {
|
||||
meta: { filename }
|
||||
});
|
||||
|
||||
return { success: true, message: 'Backup restored successfully. Please restart the application.' };
|
||||
return {
|
||||
success: true,
|
||||
message: 'Backup restored successfully. Please restart the application.'
|
||||
};
|
||||
} catch (err) {
|
||||
await logger.error(`Failed to restore backup: ${filename}`, {
|
||||
source: 'settings/backups',
|
||||
|
||||
@@ -311,10 +311,7 @@
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td
|
||||
colspan="4"
|
||||
class="px-4 py-8 text-center text-neutral-500 dark:text-neutral-400"
|
||||
>
|
||||
<td colspan="4" class="px-4 py-8 text-center text-neutral-500 dark:text-neutral-400">
|
||||
No backups found. Create your first backup to get started.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -19,9 +19,7 @@ export const load = () => {
|
||||
|
||||
return {
|
||||
logSettings: {
|
||||
rotation_strategy: logSetting.rotation_strategy,
|
||||
retention_days: logSetting.retention_days,
|
||||
max_file_size: logSetting.max_file_size,
|
||||
min_level: logSetting.min_level,
|
||||
enabled: logSetting.enabled === 1,
|
||||
file_logging: logSetting.file_logging === 1,
|
||||
@@ -42,36 +40,24 @@ export const actions: Actions = {
|
||||
const formData = await request.formData();
|
||||
|
||||
// Parse form data
|
||||
const rotationStrategy = formData.get('rotation_strategy') as 'daily' | 'size' | 'both';
|
||||
const retentionDays = parseInt(formData.get('retention_days') as string);
|
||||
const maxFileSize = parseInt(formData.get('max_file_size') as string);
|
||||
const minLevel = formData.get('min_level') as 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
||||
const enabled = formData.get('enabled') === 'on';
|
||||
const fileLogging = formData.get('file_logging') === 'on';
|
||||
const consoleLogging = formData.get('console_logging') === 'on';
|
||||
|
||||
// Validate
|
||||
if (!rotationStrategy || !['daily', 'size', 'both'].includes(rotationStrategy)) {
|
||||
return fail(400, { error: 'Invalid rotation strategy' });
|
||||
}
|
||||
|
||||
if (isNaN(retentionDays) || retentionDays < 1 || retentionDays > 365) {
|
||||
return fail(400, { error: 'Retention days must be between 1 and 365' });
|
||||
}
|
||||
|
||||
if (isNaN(maxFileSize) || maxFileSize < 1 || maxFileSize > 1000) {
|
||||
return fail(400, { error: 'Max file size must be between 1 and 1000 MB' });
|
||||
}
|
||||
|
||||
if (!minLevel || !['DEBUG', 'INFO', 'WARN', 'ERROR'].includes(minLevel)) {
|
||||
return fail(400, { error: 'Invalid minimum log level' });
|
||||
}
|
||||
|
||||
// Update settings
|
||||
const updated = logSettingsQueries.update({
|
||||
rotationStrategy,
|
||||
retentionDays,
|
||||
maxFileSize,
|
||||
minLevel,
|
||||
enabled,
|
||||
fileLogging,
|
||||
@@ -91,9 +77,7 @@ export const actions: Actions = {
|
||||
await logger.info('Log settings updated', {
|
||||
source: 'settings/general',
|
||||
meta: {
|
||||
rotationStrategy,
|
||||
retentionDays,
|
||||
maxFileSize,
|
||||
minLevel,
|
||||
enabled,
|
||||
fileLogging,
|
||||
|
||||
@@ -40,11 +40,9 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Enable Backups -->
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-50">
|
||||
Enable Features
|
||||
</h3>
|
||||
<h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-50">Enable Features</h3>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<label class="flex cursor-pointer items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="enabled"
|
||||
@@ -61,7 +59,7 @@
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<label class="flex cursor-pointer items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="compression_enabled"
|
||||
@@ -82,9 +80,7 @@
|
||||
|
||||
<!-- Schedule Configuration -->
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-50">
|
||||
Backup Schedule
|
||||
</h3>
|
||||
<h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-50">Backup Schedule</h3>
|
||||
<div class="space-y-2">
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium text-neutral-900 dark:text-neutral-50">
|
||||
@@ -93,7 +89,7 @@
|
||||
<select
|
||||
name="schedule"
|
||||
bind:value={settings.schedule}
|
||||
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50"
|
||||
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50"
|
||||
>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
|
||||
// Default values
|
||||
const DEFAULTS = {
|
||||
rotation_strategy: 'daily',
|
||||
retention_days: 30,
|
||||
max_file_size: 100,
|
||||
min_level: 'INFO',
|
||||
enabled: true,
|
||||
file_logging: true,
|
||||
@@ -20,9 +18,7 @@
|
||||
|
||||
// Reset to defaults (client-side only)
|
||||
function resetToDefaults() {
|
||||
settings.rotation_strategy = DEFAULTS.rotation_strategy as 'daily' | 'size' | 'both';
|
||||
settings.retention_days = DEFAULTS.retention_days;
|
||||
settings.max_file_size = DEFAULTS.max_file_size;
|
||||
settings.min_level = DEFAULTS.min_level as 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
||||
settings.enabled = DEFAULTS.enabled;
|
||||
settings.file_logging = DEFAULTS.file_logging;
|
||||
@@ -65,7 +61,7 @@
|
||||
<h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-50">Enable Features</h3>
|
||||
<div class="space-y-2">
|
||||
<!-- Enable Logging -->
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<label class="flex cursor-pointer items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="enabled"
|
||||
@@ -83,7 +79,7 @@
|
||||
</label>
|
||||
|
||||
<!-- File Logging -->
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<label class="flex cursor-pointer items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="file_logging"
|
||||
@@ -99,7 +95,7 @@
|
||||
</label>
|
||||
|
||||
<!-- Console Logging -->
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<label class="flex cursor-pointer items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="console_logging"
|
||||
@@ -110,9 +106,7 @@
|
||||
<span class="text-sm font-medium text-neutral-900 dark:text-neutral-50">
|
||||
Console Logging
|
||||
</span>
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Output logs to terminal
|
||||
</p>
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-400">Output logs to terminal</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -125,7 +119,7 @@
|
||||
<div>
|
||||
<label
|
||||
for="min_level"
|
||||
class="block text-sm font-semibold text-neutral-900 dark:text-neutral-50 mb-2"
|
||||
class="mb-2 block text-sm font-semibold text-neutral-900 dark:text-neutral-50"
|
||||
>
|
||||
Minimum Log Level
|
||||
</label>
|
||||
@@ -149,77 +143,30 @@
|
||||
<!-- Divider -->
|
||||
<div class="border-t border-neutral-200 dark:border-neutral-800"></div>
|
||||
|
||||
<!-- Rotation & Retention -->
|
||||
<!-- Retention -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-50 mb-3">
|
||||
Rotation & Retention
|
||||
<h3 class="mb-3 text-sm font-semibold text-neutral-900 dark:text-neutral-50">
|
||||
Retention Policy
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Rotation Strategy -->
|
||||
<div>
|
||||
<label
|
||||
for="rotation_strategy"
|
||||
class="block text-sm font-medium text-neutral-900 dark:text-neutral-50 mb-1"
|
||||
>
|
||||
Rotation Strategy
|
||||
</label>
|
||||
<select
|
||||
id="rotation_strategy"
|
||||
name="rotation_strategy"
|
||||
bind:value={settings.rotation_strategy}
|
||||
required
|
||||
class="block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-neutral-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50"
|
||||
>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="size">Size</option>
|
||||
<option value="both">Both</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
How to rotate log files
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Retention Days -->
|
||||
<div>
|
||||
<label
|
||||
for="retention_days"
|
||||
class="block text-sm font-medium text-neutral-900 dark:text-neutral-50 mb-1"
|
||||
>
|
||||
Retention (days)
|
||||
</label>
|
||||
<NumberInput
|
||||
name="retention_days"
|
||||
id="retention_days"
|
||||
bind:value={settings.retention_days}
|
||||
min={1}
|
||||
max={365}
|
||||
required
|
||||
/>
|
||||
<p class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Keep logs for 1-365 days
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Max File Size -->
|
||||
<div class="md:col-span-2">
|
||||
<label
|
||||
for="max_file_size"
|
||||
class="block text-sm font-medium text-neutral-900 dark:text-neutral-50 mb-1"
|
||||
>
|
||||
Max File Size (MB)
|
||||
</label>
|
||||
<NumberInput
|
||||
name="max_file_size"
|
||||
id="max_file_size"
|
||||
bind:value={settings.max_file_size}
|
||||
min={1}
|
||||
max={1000}
|
||||
required
|
||||
/>
|
||||
<p class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Maximum size before rotation (1-1000 MB)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="retention_days"
|
||||
class="mb-1 block text-sm font-medium text-neutral-900 dark:text-neutral-50"
|
||||
>
|
||||
Retention (days)
|
||||
</label>
|
||||
<NumberInput
|
||||
name="retention_days"
|
||||
id="retention_days"
|
||||
bind:value={settings.retention_days}
|
||||
min={1}
|
||||
max={365}
|
||||
required
|
||||
/>
|
||||
<p class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Keep daily log files for 1-365 days. Logs are automatically rotated daily
|
||||
(YYYY-MM-DD.log format).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,7 +178,7 @@
|
||||
<button
|
||||
type="button"
|
||||
on:click={resetToDefaults}
|
||||
class="flex items-center gap-2 rounded-lg border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
|
||||
class="flex items-center gap-2 rounded-lg border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
Reset to Defaults
|
||||
@@ -239,7 +186,7 @@
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
class="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
>
|
||||
<Save size={16} />
|
||||
Save Settings
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
*/
|
||||
|
||||
export interface LogSettings {
|
||||
rotation_strategy: 'daily' | 'size' | 'both';
|
||||
retention_days: number;
|
||||
max_file_size: number;
|
||||
min_level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
||||
enabled: boolean;
|
||||
file_logging: boolean;
|
||||
|
||||
@@ -7,10 +7,10 @@ import { logger } from '$logger';
|
||||
// Helper to format schedule for display
|
||||
function formatSchedule(schedule: string): string {
|
||||
const scheduleMap: Record<string, string> = {
|
||||
'daily': 'Daily',
|
||||
'hourly': 'Hourly',
|
||||
'weekly': 'Weekly',
|
||||
'monthly': 'Monthly'
|
||||
daily: 'Daily',
|
||||
hourly: 'Hourly',
|
||||
weekly: 'Weekly',
|
||||
monthly: 'Monthly'
|
||||
};
|
||||
|
||||
// Check for cron-like patterns
|
||||
|
||||
@@ -77,7 +77,10 @@
|
||||
|
||||
<!-- Schedule -->
|
||||
<div>
|
||||
<label for="schedule" class="block text-sm font-medium text-neutral-900 dark:text-neutral-50">
|
||||
<label
|
||||
for="schedule"
|
||||
class="block text-sm font-medium text-neutral-900 dark:text-neutral-50"
|
||||
>
|
||||
Schedule <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -91,11 +94,25 @@
|
||||
/>
|
||||
<div class="mt-2 space-y-1">
|
||||
<p class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Examples:</p>
|
||||
<ul class="list-inside list-disc space-y-1 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<li><code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">daily</code> - Runs once per day</li>
|
||||
<li><code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">hourly</code> - Runs every hour</li>
|
||||
<li><code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">*/5 minutes</code> - Runs every 5 minutes</li>
|
||||
<li><code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">weekly</code> - Runs once per week</li>
|
||||
<ul
|
||||
class="list-inside list-disc space-y-1 text-sm text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
<li>
|
||||
<code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">daily</code> - Runs
|
||||
once per day
|
||||
</li>
|
||||
<li>
|
||||
<code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">hourly</code> - Runs
|
||||
every hour
|
||||
</li>
|
||||
<li>
|
||||
<code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">*/5 minutes</code
|
||||
> - Runs every 5 minutes
|
||||
</li>
|
||||
<li>
|
||||
<code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">weekly</code> - Runs
|
||||
once per week
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,14 +121,14 @@
|
||||
<div class="flex gap-3 border-t border-neutral-200 pt-6 dark:border-neutral-800">
|
||||
<button
|
||||
type="submit"
|
||||
class="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
class="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
>
|
||||
<Save size={16} />
|
||||
Save Changes
|
||||
</button>
|
||||
<a
|
||||
href="/settings/jobs"
|
||||
class="rounded-lg border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
|
||||
class="rounded-lg border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-50 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
@@ -201,14 +201,14 @@
|
||||
</code>
|
||||
{#if job.last_run_status === 'success'}
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-100"
|
||||
class="inline-flex items-center gap-1 rounded bg-green-100 px-1.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/40 dark:text-green-100"
|
||||
>
|
||||
<CheckCircle size={10} />
|
||||
Success
|
||||
</span>
|
||||
{:else if job.last_run_status === 'failure'}
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-100"
|
||||
class="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/40 dark:text-red-100"
|
||||
>
|
||||
<XCircle size={10} />
|
||||
Failed
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900">
|
||||
<div
|
||||
class="rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="border-b border-neutral-200 px-6 py-4 dark:border-neutral-800">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -98,14 +100,14 @@
|
||||
<td class="border-b border-neutral-200 px-4 py-2 dark:border-neutral-800">
|
||||
{#if run.status === 'success'}
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-100"
|
||||
class="inline-flex items-center gap-1 rounded bg-green-100 px-1.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/40 dark:text-green-100"
|
||||
>
|
||||
<CheckCircle size={10} />
|
||||
Success
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-100"
|
||||
class="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/40 dark:text-red-100"
|
||||
>
|
||||
<XCircle size={10} />
|
||||
Failed
|
||||
@@ -157,10 +159,7 @@
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td
|
||||
colspan="5"
|
||||
class="px-4 py-8 text-center text-neutral-500 dark:text-neutral-400"
|
||||
>
|
||||
<td colspan="5" class="px-4 py-8 text-center text-neutral-500 dark:text-neutral-400">
|
||||
No job runs yet
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -8,9 +8,7 @@ export const load = async ({ url }: { url: URL }) => {
|
||||
const selectedFile = url.searchParams.get('file') || logFiles[0]?.filename || '';
|
||||
|
||||
// Load logs from selected file or all files if no file selected
|
||||
const logs = selectedFile
|
||||
? await readLogsFromFile(selectedFile, 1000)
|
||||
: await readLastLogs(1000);
|
||||
const logs = selectedFile ? await readLogsFromFile(selectedFile, 1000) : await readLastLogs(1000);
|
||||
|
||||
return {
|
||||
logs,
|
||||
|
||||
@@ -118,14 +118,17 @@
|
||||
<div class="mb-6 flex flex-wrap items-center gap-4">
|
||||
<!-- Log File Selector -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="log-file-select" class="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
<label
|
||||
for="log-file-select"
|
||||
class="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Log File:
|
||||
</label>
|
||||
<select
|
||||
id="log-file-select"
|
||||
value={data.selectedFile}
|
||||
on:change={(e) => changeLogFile(e.currentTarget.value)}
|
||||
class="min-w-64 rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-sm text-neutral-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50"
|
||||
class="min-w-64 rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-sm text-neutral-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50"
|
||||
>
|
||||
{#each data.logFiles as file (file.filename)}
|
||||
<option value={file.filename}>
|
||||
@@ -153,16 +156,13 @@
|
||||
|
||||
<!-- Search Input -->
|
||||
<div class="flex flex-1 items-center gap-2">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<Search
|
||||
size={18}
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400"
|
||||
/>
|
||||
<div class="relative max-w-md flex-1">
|
||||
<Search size={18} class="absolute top-1/2 left-3 -translate-y-1/2 text-neutral-400" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Search logs..."
|
||||
class="w-full rounded-lg border border-neutral-300 bg-white py-1.5 pl-10 pr-3 text-sm text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500"
|
||||
class="w-full rounded-lg border border-neutral-300 bg-white py-1.5 pr-3 pl-10 text-sm text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,9 +256,7 @@
|
||||
>
|
||||
{log.message}
|
||||
</td>
|
||||
<td
|
||||
class="border-b border-neutral-200 px-4 py-2 text-center dark:border-neutral-800"
|
||||
>
|
||||
<td class="border-b border-neutral-200 px-4 py-2 text-center dark:border-neutral-800">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
@@ -305,5 +303,9 @@
|
||||
>
|
||||
<pre
|
||||
slot="body"
|
||||
class="max-h-[400px] overflow-auto whitespace-pre-wrap rounded-lg bg-neutral-50 p-4 font-mono text-xs text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50">{JSON.stringify(selectedMeta, null, 2)}</pre>
|
||||
class="max-h-[400px] overflow-auto rounded-lg bg-neutral-50 p-4 font-mono text-xs whitespace-pre-wrap text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50">{JSON.stringify(
|
||||
selectedMeta,
|
||||
null,
|
||||
2
|
||||
)}</pre>
|
||||
</Modal>
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { JobDefinition, JobResult } from '../types.ts';
|
||||
|
||||
/**
|
||||
* Cleanup old log files job
|
||||
* Deletes log files older than the configured retention period
|
||||
* Deletes daily log files (YYYY-MM-DD.log) older than the configured retention period
|
||||
*/
|
||||
export const cleanupLogsJob: JobDefinition = {
|
||||
name: 'cleanup_logs',
|
||||
@@ -26,40 +26,43 @@ export const cleanupLogsJob: JobDefinition = {
|
||||
const retentionDays = settings.retention_days;
|
||||
const logsDir = config.paths.logs;
|
||||
|
||||
// Calculate cutoff date
|
||||
// Calculate cutoff date (YYYY-MM-DD format)
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
|
||||
const cutoffDateStr = cutoffDate.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
|
||||
await logger.info(`Cleaning up logs older than ${retentionDays} days`, {
|
||||
source: 'CleanupLogsJob',
|
||||
meta: { cutoffDate: cutoffDate.toISOString() }
|
||||
meta: { cutoffDate: cutoffDateStr }
|
||||
});
|
||||
|
||||
// Read all files in logs directory
|
||||
let deletedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// Regex to match daily log files: YYYY-MM-DD.log
|
||||
const dateLogPattern = /^(\d{4}-\d{2}-\d{2})\.log$/;
|
||||
|
||||
try {
|
||||
for await (const entry of Deno.readDir(logsDir)) {
|
||||
if (!entry.isFile) continue;
|
||||
|
||||
// Only process log files (*.log, app-*.log, etc.)
|
||||
if (!entry.name.endsWith('.log')) continue;
|
||||
// Only process log files matching YYYY-MM-DD.log pattern
|
||||
const match = entry.name.match(dateLogPattern);
|
||||
if (!match) continue;
|
||||
|
||||
const logDate = match[1]; // Extract YYYY-MM-DD from filename
|
||||
const filePath = `${logsDir}/${entry.name}`;
|
||||
|
||||
try {
|
||||
// Get file stats
|
||||
const stat = await Deno.stat(filePath);
|
||||
|
||||
// Check if file is older than cutoff
|
||||
if (stat.mtime && stat.mtime < cutoffDate) {
|
||||
// Compare date strings directly (YYYY-MM-DD format sorts correctly)
|
||||
if (logDate < cutoffDateStr) {
|
||||
await Deno.remove(filePath);
|
||||
deletedCount++;
|
||||
|
||||
await logger.info(`Deleted old log file: ${entry.name}`, {
|
||||
source: 'CleanupLogsJob',
|
||||
meta: { file: entry.name, modifiedAt: stat.mtime.toISOString() }
|
||||
meta: { file: entry.name, logDate }
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -73,7 +76,9 @@ export const cleanupLogsJob: JobDefinition = {
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to read logs directory: ${error instanceof Error ? error.message : String(error)}`
|
||||
error: `Failed to read logs directory: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,13 +39,7 @@ export const createBackupJob: JobDefinition = {
|
||||
|
||||
// Create tar.gz archive of data directory
|
||||
const command = new Deno.Command('tar', {
|
||||
args: [
|
||||
'-czf',
|
||||
backupPath,
|
||||
'-C',
|
||||
config.paths.base,
|
||||
'data'
|
||||
],
|
||||
args: ['-czf', backupPath, '-C', config.paths.base, 'data'],
|
||||
stdout: 'piped',
|
||||
stderr: 'piped'
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Logger singleton with console and file output
|
||||
* Supports configurable settings, daily rotation, and size-based rotation
|
||||
* Supports configurable settings and daily rotation
|
||||
*/
|
||||
|
||||
import { config } from '$config';
|
||||
import { colors } from './colors.ts';
|
||||
import { logSettings } from './settings.ts';
|
||||
import type { LogOptions, LogEntry } from './types.ts';
|
||||
import type { LogEntry, LogOptions } from './types.ts';
|
||||
|
||||
class Logger {
|
||||
private formatTimestamp(): string {
|
||||
@@ -29,49 +29,11 @@ class Logger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log file path based on rotation strategy
|
||||
* Get log file path with daily rotation (YYYY-MM-DD.log)
|
||||
*/
|
||||
private getLogFilePath(): string {
|
||||
const settings = logSettings.get();
|
||||
const strategy = settings.rotation_strategy;
|
||||
|
||||
if (strategy === 'daily' || strategy === 'both') {
|
||||
// Use date-based filename: app-2025-01-20.log
|
||||
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
return `${config.paths.logs}/app-${date}.log`;
|
||||
}
|
||||
|
||||
// Default: use app.log
|
||||
return config.paths.logFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if log file needs rotation due to size
|
||||
*/
|
||||
private async checkSizeRotation(filePath: string): Promise<void> {
|
||||
const settings = logSettings.get();
|
||||
const strategy = settings.rotation_strategy;
|
||||
|
||||
if (strategy !== 'size' && strategy !== 'both') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = await Deno.stat(filePath);
|
||||
const maxSizeBytes = settings.max_file_size * 1024 * 1024; // Convert MB to bytes
|
||||
|
||||
if (stat.size >= maxSizeBytes) {
|
||||
// Rotate: rename current file with timestamp
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0];
|
||||
const rotatedPath = filePath.replace('.log', `-${timestamp}.log`);
|
||||
await Deno.rename(filePath, rotatedPath);
|
||||
}
|
||||
} catch (error) {
|
||||
// File might not exist yet, that's okay
|
||||
if (!(error instanceof Deno.errors.NotFound)) {
|
||||
console.error('Error checking file size for rotation:', error);
|
||||
}
|
||||
}
|
||||
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
return `${config.paths.logs}/${date}.log`;
|
||||
}
|
||||
|
||||
private async log(
|
||||
@@ -113,9 +75,6 @@ class Logger {
|
||||
try {
|
||||
const filePath = this.getLogFilePath();
|
||||
|
||||
// Check if file needs rotation due to size
|
||||
await this.checkSizeRotation(filePath);
|
||||
|
||||
// Write to log file
|
||||
await Deno.writeTextFile(filePath, JSON.stringify(logEntry) + '\n', {
|
||||
append: true
|
||||
|
||||
@@ -41,9 +41,7 @@ class LogSettingsManager {
|
||||
// Return defaults if not initialized
|
||||
return {
|
||||
id: 1,
|
||||
rotation_strategy: 'daily',
|
||||
retention_days: 30,
|
||||
max_file_size: 100,
|
||||
min_level: 'INFO',
|
||||
enabled: 1,
|
||||
file_logging: 1,
|
||||
|
||||
Reference in New Issue
Block a user