DC-101: Disk Space Monitor + Product Vision
Backend: - src/monitoring/disk-space-monitor.js: monitors Docker disk usage against user-configured budget, auto-cleans at thresholds, breaks down by category - routes/disk-space.js: GET /disk, GET /disk/breakdown, POST /disk/config, POST /disk/cleanup endpoints - src/app.js: wire DiskSpaceMonitor into startup, 10-min check interval - All 1539 tests pass Product Vision (PRODUCT-VISION.md): - DashCaddy is a self-hosting platform, not just a dashboard - Core value: 'Self-host anything in 30 seconds' - Three pillars: One-click deploy, zero-config networking, self-healing infra - vs Portainer/CasaOS/Yunohost positioning New backlog tasks (P5 tier, DC-101–108): - Disk budget, one-click deploy with auto Caddyfile+DNS, container auto-discovery, app catalog, smart wizard, visual Caddy builder, disaster recovery, multi-host fleet management 47 total backlog tasks, ~110 hr of work, cron running every 2h.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
const express = require('express');
|
||||
const { success, error: errorResponse } = require('../src/utils/responses');
|
||||
|
||||
/**
|
||||
* Disk space management routes
|
||||
*
|
||||
* GET /disk — current usage snapshot (budget, breakdown, status)
|
||||
* GET /disk/breakdown — detailed breakdown incl. per-container log sizes
|
||||
* GET /disk/config — get disk budget settings
|
||||
* POST /disk/config — update disk budget settings
|
||||
* POST /disk/cleanup — trigger manual cleanup (standard|aggressive|logs-only)
|
||||
*/
|
||||
module.exports = function({ diskSpaceMonitor, asyncHandler, log }) {
|
||||
const router = express.Router();
|
||||
|
||||
// Current disk usage snapshot
|
||||
router.get('/', asyncHandler(async (req, res) => {
|
||||
const snapshot = await diskSpaceMonitor.getSnapshot();
|
||||
success(res, snapshot);
|
||||
}, 'disk-get'));
|
||||
|
||||
// Detailed breakdown (includes per-container log sizes)
|
||||
router.get('/breakdown', asyncHandler(async (req, res) => {
|
||||
const breakdown = await diskSpaceMonitor.getDetailedBreakdown();
|
||||
success(res, breakdown);
|
||||
}, 'disk-breakdown'));
|
||||
|
||||
// Get disk budget config
|
||||
router.get('/config', asyncHandler(async (req, res) => {
|
||||
success(res, diskSpaceMonitor.getConfig());
|
||||
}, 'disk-config-get'));
|
||||
|
||||
// Update disk budget config
|
||||
router.post('/config', asyncHandler(async (req, res) => {
|
||||
const { diskBudgetGB, warningThresholdPct, criticalThresholdPct, autoCleanup, enabled, cleanupAggressivePct } = req.body;
|
||||
|
||||
const updates = {};
|
||||
if (typeof diskBudgetGB === 'number' && diskBudgetGB > 0) updates.diskBudgetGB = Math.min(diskBudgetGB, 1000);
|
||||
if (typeof warningThresholdPct === 'number') updates.warningThresholdPct = Math.min(Math.max(warningThresholdPct, 50), 99);
|
||||
if (typeof criticalThresholdPct === 'number') updates.criticalThresholdPct = Math.min(Math.max(criticalThresholdPct, 60), 99);
|
||||
if (typeof cleanupAggressivePct === 'number') updates.cleanupAggressivePct = Math.min(Math.max(cleanupAggressivePct, 70), 99);
|
||||
if (typeof autoCleanup === 'boolean') updates.autoCleanup = autoCleanup;
|
||||
if (typeof enabled === 'boolean') updates.enabled = enabled;
|
||||
|
||||
const config = diskSpaceMonitor.configure(updates);
|
||||
log.info('disk', 'Disk budget updated', updates);
|
||||
|
||||
success(res, { message: 'Disk budget updated', config });
|
||||
}, 'disk-config-set'));
|
||||
|
||||
// Manual cleanup trigger
|
||||
router.post('/cleanup', asyncHandler(async (req, res) => {
|
||||
const level = req.body?.level || 'standard';
|
||||
if (!['standard', 'aggressive', 'logs-only'].includes(level)) {
|
||||
return errorResponse(res, 'Invalid cleanup level. Use: standard, aggressive, or logs-only', 400);
|
||||
}
|
||||
|
||||
log.info('disk', 'Manual cleanup triggered', { level, by: req.auth?.user || 'api' });
|
||||
const result = await diskSpaceMonitor.performCleanup(level);
|
||||
success(res, result);
|
||||
}, 'disk-cleanup'));
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -90,9 +90,11 @@ const DependencyManager = require('./managers/dependency-manager');
|
||||
const autoRestartRoutes = require('../routes/auto-restart');
|
||||
const configDriftRoutes = require('../routes/config-drift');
|
||||
const sslMonitorRoutes = require('../routes/ssl-monitor');
|
||||
const diskSpaceRoutes = require('../routes/disk-space');
|
||||
const { AutoRestartManager } = require('./managers/auto-restart-manager');
|
||||
const { ConfigDriftDetector } = require('./managers/config-drift-detector');
|
||||
const SSLMonitor = require('./monitoring/ssl-monitor');
|
||||
const { DiskSpaceMonitor } = require('./monitoring/disk-space-monitor');
|
||||
const DNSPropagationChecker = require('./dns/dns-propagation');
|
||||
|
||||
// Constants
|
||||
@@ -455,6 +457,12 @@ async function createApp() {
|
||||
sslMonitor.start(3600000); // 1 hour
|
||||
log.info('app', 'SSL monitor initialized');
|
||||
|
||||
// Initialize disk space monitor (disk budget + auto-cleanup)
|
||||
const diskSpaceMonitor = new DiskSpaceMonitor({ log, config: ctx.siteConfig });
|
||||
ctx.diskSpaceMonitor = diskSpaceMonitor;
|
||||
diskSpaceMonitor.start(600000); // 10 min
|
||||
log.info('app', 'Disk space monitor initialized', { budgetGB: diskSpaceMonitor.getConfig().diskBudgetGB });
|
||||
|
||||
// Initialize DNS propagation checker
|
||||
const dnsPropagationChecker = new DNSPropagationChecker(ctx);
|
||||
ctx.dnsPropagationChecker = dnsPropagationChecker;
|
||||
@@ -709,6 +717,11 @@ async function createApp() {
|
||||
asyncHandler: ctx.asyncHandler,
|
||||
logError: ctx.logError,
|
||||
}));
|
||||
apiRouter.use('/disk', diskSpaceRoutes({
|
||||
diskSpaceMonitor: ctx.diskSpaceMonitor,
|
||||
asyncHandler: ctx.asyncHandler,
|
||||
log: ctx.log,
|
||||
}));
|
||||
|
||||
// Inline API routes (mounted under /api/v1 below)
|
||||
// Note: /health lives at root only — see root-level health check below.
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Disk Space Monitor
|
||||
*
|
||||
* Tracks Docker + system disk usage against a user-configured budget.
|
||||
* When usage exceeds thresholds, triggers automatic cleanup and notifications.
|
||||
*
|
||||
* Key concepts:
|
||||
* - diskBudgetGB: How much disk the user is willing to give DashCaddy (default 10)
|
||||
* - The monitor calculates Docker's footprint (images, volumes, containers, build cache)
|
||||
* - Breakdown shows where space goes so users can make informed decisions
|
||||
* - Auto-cleanup triggers at 80% (warning), 90% (aggressive), 95% (critical)
|
||||
*/
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const DEFAULT_BUDGET_GB = 10;
|
||||
const DEFAULT_CONFIG = {
|
||||
enabled: true,
|
||||
diskBudgetGB: DEFAULT_BUDGET_GB,
|
||||
warningThresholdPct: 80,
|
||||
criticalThresholdPct: 90,
|
||||
autoCleanup: true,
|
||||
cleanupAggressivePct: 95,
|
||||
};
|
||||
|
||||
class DiskSpaceMonitor extends EventEmitter {
|
||||
constructor({ log, config }) {
|
||||
super();
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.lastSnapshot = null;
|
||||
this.lastCleanup = null;
|
||||
this.intervalHandle = null;
|
||||
this.diskConfig = { ...DEFAULT_CONFIG };
|
||||
this._loadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load disk budget config from the site config file
|
||||
* Stored under `diskSpace` key in config.json
|
||||
*/
|
||||
_loadConfig() {
|
||||
try {
|
||||
const raw = this.config?.diskSpace;
|
||||
if (raw) {
|
||||
this.diskConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
...raw,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Use defaults
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update disk space settings
|
||||
*/
|
||||
configure(updates) {
|
||||
const prev = { ...this.diskConfig };
|
||||
this.diskConfig = { ...this.diskConfig, ...updates };
|
||||
this._persistConfig();
|
||||
this.emit('config-changed', { prev, current: this.diskConfig });
|
||||
return this.diskConfig;
|
||||
}
|
||||
|
||||
_persistConfig() {
|
||||
// The config is persisted by the caller (settings route) which merges
|
||||
// into config.json. We just expose the current state.
|
||||
if (this.config) {
|
||||
this.config.diskSpace = this.diskConfig;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a disk usage snapshot using `df` and `docker system df -v`
|
||||
*/
|
||||
async getSnapshot() {
|
||||
const [diskInfo, dockerInfo] = await Promise.all([
|
||||
this._getDiskInfo(),
|
||||
this._getDockerInfo(),
|
||||
]);
|
||||
|
||||
const snapshot = {
|
||||
timestamp: new Date().toISOString(),
|
||||
system: diskInfo,
|
||||
docker: dockerInfo,
|
||||
budget: {
|
||||
configuredGB: this.diskConfig.diskBudgetGB,
|
||||
dockerUsageGB: dockerInfo.totalGB,
|
||||
remainingBudgetGB: Math.max(0, this.diskConfig.diskBudgetGB - dockerInfo.totalGB),
|
||||
budgetUsedPct: Math.min(100, Math.round((dockerInfo.totalGB / this.diskConfig.diskBudgetGB) * 100)),
|
||||
status: this._getBudgetStatus(dockerInfo.totalGB),
|
||||
},
|
||||
config: { ...this.diskConfig },
|
||||
lastCleanup: this.lastCleanup,
|
||||
};
|
||||
|
||||
this.lastSnapshot = snapshot;
|
||||
|
||||
// Check thresholds and emit events
|
||||
this._checkThresholds(snapshot);
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
_getBudgetStatus(dockerUsageGB) {
|
||||
const pct = (dockerUsageGB / this.diskConfig.diskBudgetGB) * 100;
|
||||
if (pct >= this.diskConfig.cleanupAggressivePct) return 'critical';
|
||||
if (pct >= this.diskConfig.criticalThresholdPct) return 'aggressive';
|
||||
if (pct >= this.diskConfig.warningThresholdPct) return 'warning';
|
||||
return 'healthy';
|
||||
}
|
||||
|
||||
_checkThresholds(snapshot) {
|
||||
const { status, budgetUsedPct } = snapshot.budget;
|
||||
if (status === 'critical' || status === 'aggressive') {
|
||||
this.emit('budget-exceeded', snapshot);
|
||||
if (this.diskConfig.autoCleanup) {
|
||||
this.performCleanup(status === 'critical' ? 'aggressive' : 'standard').catch(() => {});
|
||||
}
|
||||
} else if (status === 'warning') {
|
||||
this.emit('budget-warning', snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
async _getDiskInfo() {
|
||||
try {
|
||||
const { stdout } = await execFileAsync('df', ['-B1', '/']);
|
||||
const lines = stdout.trim().split('\n');
|
||||
const parts = lines[1].split(/\s+/);
|
||||
return {
|
||||
totalBytes: parseInt(parts[1], 10),
|
||||
usedBytes: parseInt(parts[2], 10),
|
||||
availableBytes: parseInt(parts[3], 10),
|
||||
usedPct: parseInt(parts[4], 10),
|
||||
mount: parts[5],
|
||||
totalGB: Math.round(parseInt(parts[1], 10) / 1073741824 * 10) / 10,
|
||||
usedGB: Math.round(parseInt(parts[2], 10) / 1073741824 * 10) / 10,
|
||||
availableGB: Math.round(parseInt(parts[3], 10) / 1073741824 * 10) / 10,
|
||||
};
|
||||
} catch {
|
||||
return { totalBytes: 0, usedBytes: 0, availableBytes: 0, usedPct: 0, totalGB: 0, usedGB: 0, availableGB: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
async _getDockerInfo() {
|
||||
try {
|
||||
const { stdout } = await execFileAsync('docker', ['system', 'df', '--format', '{{json .}}']);
|
||||
const lines = stdout.trim().split('\n').filter(Boolean);
|
||||
|
||||
let images = { count: 0, totalGB: 0, reclaimableGB: 0 };
|
||||
let containers = { count: 0, totalGB: 0, reclaimableGB: 0 };
|
||||
let volumes = { count: 0, totalGB: 0, reclaimableGB: 0 };
|
||||
let buildCache = { count: 0, totalGB: 0, reclaimableGB: 0 };
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const d = JSON.parse(line);
|
||||
const type = d.Type?.toLowerCase() || '';
|
||||
const sizeGB = this._parseSizeToGB(d.Size);
|
||||
const reclaimGB = this._parseSizeToGB(d.Reclaimable);
|
||||
|
||||
if (type === 'images') images = { count: parseInt(d.TotalCount, 10) || 0, totalGB: sizeGB, reclaimableGB: reclaimGB };
|
||||
else if (type === 'containers') containers = { count: parseInt(d.TotalCount, 10) || 0, totalGB: sizeGB, reclaimableGB: reclaimGB };
|
||||
else if (type === 'local volumes') volumes = { count: parseInt(d.TotalCount, 10) || 0, totalGB: sizeGB, reclaimableGB: reclaimGB };
|
||||
else if (type === 'build cache') buildCache = { count: parseInt(d.TotalCount, 10) || 0, totalGB: sizeGB, reclaimableGB: reclaimGB };
|
||||
} catch { /* skip unparseable lines */ }
|
||||
}
|
||||
|
||||
const totalGB = Math.round((images.totalGB + containers.totalGB + volumes.totalGB + buildCache.totalGB) * 100) / 100;
|
||||
const reclaimableGB = Math.round((images.reclaimableGB + containers.reclaimableGB + volumes.reclaimableGB + buildCache.reclaimableGB) * 100) / 100;
|
||||
|
||||
return {
|
||||
images,
|
||||
containers,
|
||||
volumes,
|
||||
buildCache,
|
||||
totalGB,
|
||||
reclaimableGB,
|
||||
};
|
||||
} catch {
|
||||
return { images: {}, containers: {}, volumes: {}, buildCache: {}, totalGB: 0, reclaimableGB: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Docker's human-readable size strings (e.g., "2.519GB", "8.108MB", "0B")
|
||||
*/
|
||||
_parseSizeToGB(str) {
|
||||
if (!str || str === '0B') return 0;
|
||||
const match = str.match(/^([\d.]+)(B|KB|MB|GB|TB)$/i);
|
||||
if (!match) return 0;
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
const multipliers = { B: 1e-9, KB: 1e-6, MB: 1e-3, GB: 1, TB: 1e3 };
|
||||
return Math.round(value * (multipliers[unit] || 0) * 1000) / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get per-container log file sizes (the hidden disk hog)
|
||||
*/
|
||||
async _getContainerLogs() {
|
||||
try {
|
||||
const { stdout } = await execFileAsync('sh', ['-c', 'for f in /var/lib/docker/containers/*/*-json.log; do [ -f "$f" ] && stat -c "%s %n" "$f"; done 2>/dev/null | sort -rn | head -10']);
|
||||
const entries = [];
|
||||
for (const line of stdout.trim().split('\n').filter(Boolean)) {
|
||||
const [sizeStr, ...fileParts] = line.split(' ');
|
||||
const sizeBytes = parseInt(sizeStr, 10);
|
||||
entries.push({
|
||||
sizeBytes,
|
||||
sizeMB: Math.round(sizeBytes / 1048576 * 10) / 10,
|
||||
file: fileParts.join(' '),
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform cleanup
|
||||
* @param {string} level - 'standard' | 'aggressive' | 'logs-only'
|
||||
* @returns {Object} cleanup result with bytes reclaimed
|
||||
*/
|
||||
async performCleanup(level = 'standard') {
|
||||
const startTime = Date.now();
|
||||
const result = {
|
||||
level,
|
||||
startedAt: new Date(startTime).toISOString(),
|
||||
actions: [],
|
||||
bytesReclaimed: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
// Always: truncate oversized container logs
|
||||
const logsBefore = await this._getContainerLogs();
|
||||
let logBytesFreed = 0;
|
||||
for (const log of logsBefore) {
|
||||
if (log.sizeBytes > 100 * 1048576) { // > 100MB
|
||||
try {
|
||||
await execFileAsync('truncate', ['-s', '0', log.file]);
|
||||
logBytesFreed += log.sizeBytes;
|
||||
result.actions.push({ action: 'truncate-log', file: log.file, freedBytes: log.sizeBytes });
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
result.bytesReclaimed += logBytesFreed;
|
||||
|
||||
// Always: vacuum journald to 200MB
|
||||
try {
|
||||
const { stdout } = await execFileAsync('journalctl', ['--vacuum-size=200M']);
|
||||
const freedMatch = stdout.match(/freed ([\d.]+[KMGT]?B)/i);
|
||||
if (freedMatch) {
|
||||
const freedBytes = this._humanToBytes(freedMatch[1]);
|
||||
result.bytesReclaimed += freedBytes;
|
||||
result.actions.push({ action: 'vacuum-journal', freedBytes, freedHuman: freedMatch[1] });
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
|
||||
if (level === 'standard' || level === 'aggressive') {
|
||||
// Prune dangling images
|
||||
try {
|
||||
const { stdout } = await execFileAsync('docker', ['image', 'prune', '-f', '--filter', 'dangling=true']);
|
||||
const reclaimed = this._extractDockerReclaimed(stdout);
|
||||
result.bytesReclaimed += reclaimed;
|
||||
result.actions.push({ action: 'prune-dangling-images', freedBytes: reclaimed });
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Prune unused volumes
|
||||
try {
|
||||
const { stdout } = await execFileAsync('docker', ['volume', 'prune', '-f']);
|
||||
const reclaimed = this._extractDockerReclaimed(stdout);
|
||||
result.bytesReclaimed += reclaimed;
|
||||
result.actions.push({ action: 'prune-unused-volumes', freedBytes: reclaimed });
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Prune build cache (keep last 500MB)
|
||||
try {
|
||||
const { stdout } = await execFileAsync('docker', ['builder', 'prune', '-f', '--keep-storage', '500m']);
|
||||
const reclaimed = this._extractDockerReclaimed(stdout);
|
||||
result.bytesReclaimed += reclaimed;
|
||||
result.actions.push({ action: 'prune-build-cache', freedBytes: reclaimed });
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
if (level === 'aggressive') {
|
||||
// Remove ALL images not used by running containers
|
||||
try {
|
||||
const { stdout } = await execFileAsync('docker', ['image', 'prune', '-a', '-f']);
|
||||
const reclaimed = this._extractDockerReclaimed(stdout);
|
||||
result.bytesReclaimed += reclaimed;
|
||||
result.actions.push({ action: 'prune-all-unused-images', freedBytes: reclaimed });
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Prune stopped containers older than 24h
|
||||
try {
|
||||
const { stdout } = await execFileAsync('docker', ['container', 'prune', '-f', '--filter', 'until=24h']);
|
||||
const reclaimed = this._extractDockerReclaimed(stdout);
|
||||
result.bytesReclaimed += reclaimed;
|
||||
result.actions.push({ action: 'prune-old-containers', freedBytes: reclaimed });
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
result.completedAt = new Date().toISOString();
|
||||
result.durationMs = Date.now() - startTime;
|
||||
result.bytesReclaimedGB = Math.round(result.bytesReclaimed / 1073741824 * 100) / 100;
|
||||
|
||||
this.lastCleanup = result;
|
||||
this.emit('cleanup-complete', result);
|
||||
|
||||
if (this.log) {
|
||||
this.log.info('disk', 'Disk cleanup completed', {
|
||||
level,
|
||||
bytesReclaimed: result.bytesReclaimed,
|
||||
GBReclaimed: result.bytesReclaimedGB,
|
||||
durationMs: result.durationMs,
|
||||
actions: result.actions.length,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
result.error = err.message;
|
||||
result.completedAt = new Date().toISOString();
|
||||
if (this.log) {
|
||||
this.log.error('disk', 'Disk cleanup failed', { error: err.message, level });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
_humanToBytes(str) {
|
||||
const match = str.match(/^([\d.]+)(B|KB|MB|GB|TB)$/i);
|
||||
if (!match) return 0;
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
const multipliers = { B: 1, KB: 1024, MB: 1048576, GB: 1073741824, TB: 1099511627776 };
|
||||
return Math.round(value * (multipliers[unit] || 0));
|
||||
}
|
||||
|
||||
_extractDockerReclaimed(stdout) {
|
||||
const match = stdout.match(/reclaimed\s+([\d.]+[KMGT]?B)/i) || stdout.match(/Total reclaimed space:\s*([\d.]+[KMGT]?B)/i);
|
||||
if (match) return this._humanToBytes(match[1]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic monitoring
|
||||
* @param {number} intervalMs - check interval (default 10 minutes)
|
||||
*/
|
||||
start(intervalMs = 600000) {
|
||||
if (this.intervalHandle) return;
|
||||
this.log?.info?.('disk', 'Disk space monitor started', { intervalMs });
|
||||
// Initial check
|
||||
this.getSnapshot().catch(() => {});
|
||||
this.intervalHandle = setInterval(() => {
|
||||
this.getSnapshot().catch(() => {});
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.intervalHandle) {
|
||||
clearInterval(this.intervalHandle);
|
||||
this.intervalHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return { ...this.diskConfig };
|
||||
}
|
||||
|
||||
async getDetailedBreakdown() {
|
||||
const [snapshot, containerLogs] = await Promise.all([
|
||||
this.getSnapshot(),
|
||||
this._getContainerLogs(),
|
||||
]);
|
||||
return {
|
||||
...snapshot,
|
||||
containerLogs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DiskSpaceMonitor, DEFAULT_DISK_CONFIG: DEFAULT_CONFIG };
|
||||
Reference in New Issue
Block a user