Skip to content

Logging System ​

DBackup uses three distinct logging systems:

  1. System Logger - For application-wide logging (errors, debug info, operations)
  2. Execution Logs - Structured logs for backup/restore job tracking (UI display)
  3. Notification Logs - Records every notification sent for audit trail and preview rendering

System Logger ​

Added in v0.9.4-beta

The centralized logging system was introduced to replace scattered console.log calls throughout the codebase.

Overview ​

The system logger provides consistent, level-based logging across the entire application.

Location: src/lib/logging/logger.ts

Basic Usage ​

typescript
import { logger } from "@/lib/logging/logger";

// Simple logging
logger.info("Backup started");
logger.debug("Processing file", { filename: "backup.sql" });
logger.warn("Connection slow", { latency: 500 });
logger.error("Operation failed", { operation: "upload" }, error);

Child Loggers ​

For component-specific logging, create a child logger with context:

typescript
import { logger } from "@/lib/logging/logger";

const log = logger.child({ service: "BackupService" });

// All logs will include { service: "BackupService" }
log.info("Starting backup job", { jobId: "abc123" });
// Output: { level: "info", service: "BackupService", jobId: "abc123", message: "Starting backup job" }

Log Levels ​

LevelUsageWhen to Use
debugDetailed debugging infoDevelopment, troubleshooting
infoNormal operationsImportant state changes
warnNon-critical issuesDegraded functionality
errorFailuresExceptions, failed operations

Environment Configuration ​

Control log output via the LOG_LEVEL environment variable:

bash
# .env
LOG_LEVEL=debug   # Show all logs (development)
LOG_LEVEL=info    # Default (production)
LOG_LEVEL=warn    # Only warnings and errors
LOG_LEVEL=error   # Only errors

Output Formats ​

Development (colored, human-readable):

[2026-02-05T10:30:00.000Z] INFO  [BackupService] Starting backup job { jobId: "abc123" }

Production (JSON, machine-parseable):

json
{"timestamp":"2026-02-05T10:30:00.000Z","level":"info","service":"BackupService","message":"Starting backup job","jobId":"abc123"}

Error Handling Integration ​

The logger integrates with the custom error system:

typescript
import { logger } from "@/lib/logging/logger";
import { wrapError, AdapterError } from "@/lib/logging/errors";

const log = logger.child({ adapter: "mysql" });

try {
  await connectToDatabase();
} catch (error) {
  // wrapError() converts unknown errors to DBackupError
  log.error("Connection failed", { host: config.host }, wrapError(error));
  throw new AdapterError("mysql", "Failed to connect to database");
}

Best Practices ​

Do

  • Use child loggers with context (service, adapter, step)
  • Include relevant metadata as the second parameter
  • Use appropriate log levels
  • Use wrapError() for error logging

Don't

  • Don't use console.log, console.error, etc. directly
  • Don't log sensitive data (passwords, keys, tokens)
  • Don't log inside hot loops (performance impact)

Middleware Logging ​

The Next.js middleware automatically logs API requests and security events.

Location: src/middleware.ts

API Request Logging ​

All API requests (except silent paths) are logged with timing information:

INFO  API request {"module":"Middleware","method":"GET","path":"/api/jobs","duration":"12ms","ip":"127.0.0.1"}
INFO  API request {"module":"Middleware","method":"POST","path":"/api/backup/run","duration":"45ms","ip":"192.168.x.x"}

Silent Paths (not logged to reduce noise):

  • /api/health - Health check endpoint (frequent polling)
  • /api/auth/get-session - Session validation (every request)

Rate Limit Event Logging ​

When a client exceeds the rate limit, a warning is logged:

WARN  Rate limit exceeded {"module":"Middleware","ip":"192.168.x.x","path":"/api/jobs","method":"POST","limiter":"mutation"}

Limiter Types:

LimiterApplies ToLimit
auth/api/auth/sign-inStrict (prevent brute force)
apiGET/HEAD requestsStandard
mutationPOST/PUT/DELETE/PATCHStricter

IP Anonymization ​

For privacy compliance (GDPR), IP addresses are anonymized in logs:

Original IPLogged As
192.168.1.100192.168.x.x
10.0.0.5010.0.x.x
127.0.0.1127.0.0.1 (localhost unchanged)
::1::1 (IPv6 localhost unchanged)
2001:db8::12001:db8:x (IPv6 prefix only)

Custom Error Classes ​

Location: src/lib/logging/errors.ts

DBackup provides a hierarchy of custom error classes for consistent error handling:

Error Hierarchy ​

DBackupError (base)
├── AdapterError       - Database/storage adapter failures
├── ConnectionError    - Network/connectivity issues
├── ConfigurationError - Invalid config or settings
├── ServiceError       - Business logic failures
├── NotFoundError      - Resource not found
├── ValidationError    - Input validation failures
├── PermissionError    - RBAC authorization failures
├── AuthenticationError - Login/session failures
├── BackupError        - Backup operation failures
├── RestoreError       - Restore operation failures
├── EncryptionError    - Encryption/decryption failures
└── QueueError         - Job queue failures

Creating Custom Errors ​

typescript
import { AdapterError, BackupError, wrapError } from "@/lib/logging/errors";

// Adapter-specific error
throw new AdapterError("mysql", "Connection timeout after 30s");

// Backup operation error
throw new BackupError("Dump failed: insufficient permissions");

// Wrapping unknown errors
try {
  await riskyOperation();
} catch (e) {
  throw wrapError(e); // Converts to DBackupError
}

Utility Functions ​

typescript
import {
  isDBackupError,
  getErrorMessage,
  getErrorCode,
  withContext
} from "@/lib/logging/errors";

// Type guard
if (isDBackupError(error)) {
  console.log(error.code); // e.g., "ADAPTER_ERROR"
}

// Safe message extraction
const message = getErrorMessage(unknownError);

// Add context to errors
throw withContext(error, { jobId: "123", attempt: 2 });

Execution Logs (Job Tracking) ​

For backup and restore operations, DBackup uses structured execution logs that are displayed in the UI.

The LogEntry Structure ​

Location: src/lib/core/logs.ts

typescript
export interface LogEntry {
  timestamp: string;      // ISO 8601 format
  level: LogLevel;        // 'info' | 'success' | 'warning' | 'error'
  type: LogType;          // 'general' | 'command' | 'storage' | 'security'
  message: string;        // Short, human-readable message
  stage?: string;         // Current execution stage
  details?: string;       // Long output (stdout, stack traces)
  context?: Record<string, any>; // Additional metadata
}

export type LogLevel = 'info' | 'success' | 'warning' | 'error';
export type LogType = 'general' | 'command' | 'storage' | 'security';

Log Levels ​

LevelUsageUI Color
infoNormal progress messagesBlue
successCompleted stepsGreen
warningNon-fatal issuesOrange
errorFailuresRed

Log Types ​

TypeUsageUI Display
generalStatus messagesNormal text
commandShell commands, SQLMonospace, collapsible

Usage in Services ​

The runner pipeline uses execution logs for job tracking:

Log Buffer Pattern ​

typescript
class BackupRunner {
  private logs: LogEntry[] = [];
  private currentStage: string = 'Initialization';

  private log(
    message: string,
    level: LogLevel = 'info',
    type: LogType = 'general',
    details?: string
  ) {
    const entry: LogEntry = {
      timestamp: new Date().toISOString(),
      message,
      level,
      type,
      stage: this.currentStage,
      details,
    };
    this.logs.push(entry);
    this.flushLogs(); // Periodic DB update
  }

  private setStage(stage: string) {
    this.currentStage = stage;
    this.log(`Starting ${stage}`, 'info');
  }
}

Example Usage ​

typescript
// Simple info message
this.log('Download started');

// Success with stage
this.setStage('Upload');
this.log('File uploaded successfully', 'success');

// Command with output
this.log(
  'Executing mysqldump',
  'info',
  'command',
  `mysqldump --host=db.example.com --user=backup mydb`
);

// Error with details
this.log(
  'Connection failed',
  'error',
  'general',
  error.stack
);

// Warning
this.log(
  'Slow connection detected',
  'warning',
  'general',
  `Latency: ${latencyMs}ms`
);

Execution Stages ​

Standard stages used throughout the pipeline:

StageDescription
InitializationLoading configuration, resolving adapters
DumpCreating database dump
CompressionApplying GZIP/Brotli
EncryptionEncrypting with vault key
UploadTransferring to storage
RetentionCleaning up old backups
CompletionFinal cleanup, notifications

For restore operations:

StageDescription
InitializationLoading configuration
DownloadFetching backup file
DecryptionDecrypting if encrypted
DecompressionExtracting if compressed
RestoreApplying to database
VerificationOptional integrity check
CompletionCleanup

Log Persistence ​

Flushing Strategy ​

Logs are buffered in memory and flushed to the database periodically:

typescript
private async flushLogs() {
  // Debounced flush every 500ms
  await db.execution.update({
    where: { id: this.executionId },
    data: { logs: JSON.stringify(this.logs) },
  });
}

Database Storage ​

prisma
model Execution {
  id           String    @id
  logs         String    // JSON string of LogEntry[]
  logsPurgedAt DateTime? // Set when data retention cleared `logs` to "[]"
  // ...
}

The Execution Logs retention setting (execution.logRetentionDays, default 365 days) clears logs of finished runs and sets logsPurgedAt, while the row itself stays. GET /api/executions/{id} and GET /api/history/runs/{id} return logsPurgedAt so the page of a run can explain the empty log. See src/services/system/execution-retention.ts.

Retrieving Logs ​

typescript
const execution = await db.execution.findUnique({
  where: { id: executionId },
});

const logs: LogEntry[] = JSON.parse(execution.logs || '[]');

Frontend Rendering ​

Stage Grouping ​

The UI groups logs by stage for better readability:

tsx
function ExecutionLogs({ logs }: { logs: LogEntry[] }) {
  const grouped = groupBy(logs, 'stage');

  return (
    <div>
      {Object.entries(grouped).map(([stage, entries]) => (
        <StageSection key={stage} name={stage}>
          {entries.map((log) => (
            <LogLine key={log.timestamp} entry={log} />
          ))}
        </StageSection>
      ))}
    </div>
  );
}

Command Collapsing ​

Command-type logs show the command itself, with expandable details:

tsx
function LogLine({ entry }: { entry: LogEntry }) {
  if (entry.type === 'command') {
    return (
      <Collapsible>
        <CollapsibleTrigger>
          <code>{entry.message}</code>
        </CollapsibleTrigger>
        <CollapsibleContent>
          <pre>{entry.details}</pre>
        </CollapsibleContent>
      </Collapsible>
    );
  }

  return <p className={levelStyles[entry.level]}>{entry.message}</p>;
}

Live Progress ​

For real-time updates during execution:

typescript
// Server: Update execution with progress
await db.execution.update({
  where: { id },
  data: {
    logs: JSON.stringify(logs),
    progress: {
      stage: currentStage,
      percent: calculatePercent(),
      message: lastLog.message,
    },
  },
});

// Client: Poll for updates
const { data } = useSWR(
  `/api/executions/${id}`,
  fetcher,
  { refreshInterval: 1000 }
);

Notification Logs ​

Added in v0.9.9-beta

Notification logging was introduced for audit trail and adapter-specific preview rendering on the History page.

Overview ​

Every notification sent through the system (per-job and system-wide) is logged to the NotificationLog table. This provides a full audit trail and enables adapter-specific preview rendering on the History page.

Service: src/services/notifications/notification-log-service.ts

Recording Notifications ​

Logging happens transparently in both dispatch points:

typescript
import { recordNotificationLog } from "@/services/notifications/notification-log-service";

// After sending a notification
await recordNotificationLog({
  eventType: "BACKUP_SUCCESS",
  channelId: channel.id,
  channelName: channel.name,
  adapterId: "discord",
  status: "success",           // or "error"
  title: payload.title,
  message: payload.message,
  fields: JSON.stringify(payload.fields),
  color: payload.color,
  renderedPayload: JSON.stringify(discordEmbed),
  executionId: execution.id,   // optional, for per-job notifications
  error: null,                 // error message if send failed
});

Key design: recordNotificationLog() is fire-and-forget - it catches and swallows all errors to never block notification delivery.

Dispatch Points ​

LocationContext
src/lib/runner/steps/04-completion.tsPer-job backup notifications
src/services/system-notification-service.tsSystem-wide events (login, restore, config backup, storage alerts)

Rendered Payloads ​

Each log entry stores adapter-specific rendered content for History page preview:

AdapterrenderedPayload ContentrenderedHtml
DiscordEmbed object (title, description, fields, color)-
SlackBlock Kit blocks array-
TeamsAdaptive Card body-
TelegramParsed HTML message-
Email-Full rendered React email HTML
Others--

Data Retention ​

Notification logs are automatically cleaned by the "Clean Old Data" system task:

  • SystemSetting key: notification.logRetentionDays
  • Default: 90 days
  • Configurable: 7 days to 5 years (Settings → Data retention, shown as Notification history)

All retention settings are defined once in src/lib/core/data-retention.ts and applied by runDataRetention() in src/services/system/data-retention-service.ts.

Released under the GNU General Public License. | Privacy · Legal Notice