Phase 2 (WIP): Add logger and docker context modules
- src/utils/logger.js: Structured JSON logging - src/context/docker.js: Docker API wrapper (pull, findContainer, getUsedPorts) - All modules can now be imported directly instead of via ctx
This commit is contained in:
78
dashcaddy-api/src/context/docker.js
Normal file
78
dashcaddy-api/src/context/docker.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Docker context
|
||||
* Docker API wrapper and container utilities
|
||||
*/
|
||||
|
||||
const Docker = require('dockerode');
|
||||
const { DOCKER } = require('../../constants');
|
||||
|
||||
// Docker client instance
|
||||
const docker = new Docker();
|
||||
|
||||
/**
|
||||
* Pull a Docker image with timeout protection
|
||||
* @param {string} imageName - Image name (e.g., 'nginx:latest')
|
||||
* @param {number} timeoutMs - Timeout in milliseconds
|
||||
* @returns {Promise<Array>} Pull progress output
|
||||
*/
|
||||
function dockerPull(imageName, timeoutMs = DOCKER.TIMEOUT) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error(`Docker pull timed out after ${timeoutMs / 1000}s: ${imageName}`)),
|
||||
timeoutMs,
|
||||
);
|
||||
|
||||
docker.pull(imageName, (err, stream) => {
|
||||
if (err) {
|
||||
clearTimeout(timer);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
docker.modem.followProgress(stream, (err, output) => {
|
||||
clearTimeout(timer);
|
||||
if (err) return reject(err);
|
||||
resolve(output);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a running Docker container by name substring
|
||||
* @param {string} name - Container name or substring
|
||||
* @param {object} opts - Options (e.g., { all: true } to include stopped containers)
|
||||
* @returns {Promise<object|null>} Container object or null if not found
|
||||
*/
|
||||
async function findContainerByName(name, opts = { all: false }) {
|
||||
const containers = await docker.listContainers(opts);
|
||||
const match = containers.find((c) =>
|
||||
c.Names.some((n) => n.toLowerCase().includes(name.toLowerCase())),
|
||||
);
|
||||
return match || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all host ports currently in use by Docker containers
|
||||
* @returns {Promise<Set<number>>} Set of port numbers
|
||||
*/
|
||||
async function getUsedPorts() {
|
||||
const containers = await docker.listContainers({ all: false });
|
||||
const ports = new Set();
|
||||
|
||||
for (const c of containers) {
|
||||
for (const p of (c.Ports || [])) {
|
||||
if (p.PublicPort) {
|
||||
ports.add(p.PublicPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ports;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
client: docker,
|
||||
pull: dockerPull,
|
||||
findContainer: findContainerByName,
|
||||
getUsedPorts,
|
||||
};
|
||||
@@ -6,10 +6,12 @@
|
||||
const asyncHandler = require('./async-handler');
|
||||
const { errorResponse, ok } = require('./responses');
|
||||
const { safeErrorMessage } = require('./safe-error');
|
||||
const log = require('./logger');
|
||||
|
||||
module.exports = {
|
||||
asyncHandler,
|
||||
errorResponse,
|
||||
ok,
|
||||
safeErrorMessage,
|
||||
log,
|
||||
};
|
||||
|
||||
40
dashcaddy-api/src/utils/logger.js
Normal file
40
dashcaddy-api/src/utils/logger.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Structured logging
|
||||
* JSON-formatted logs with levels (debug, info, warn, error)
|
||||
*/
|
||||
|
||||
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
const LOG_LEVEL = LOG_LEVELS[process.env.LOG_LEVEL || 'info'] || 1;
|
||||
|
||||
/**
|
||||
* Core log function
|
||||
* @param {string} level - Log level (debug, info, warn, error)
|
||||
* @param {string} context - Context label (e.g., 'server', 'docker', 'caddy')
|
||||
* @param {string} message - Log message
|
||||
* @param {object} data - Additional structured data
|
||||
*/
|
||||
function log(level, context, message, data = {}) {
|
||||
if (LOG_LEVELS[level] < LOG_LEVEL) return;
|
||||
|
||||
const entry = {
|
||||
t: new Date().toISOString(),
|
||||
level,
|
||||
ctx: context,
|
||||
msg: message,
|
||||
};
|
||||
|
||||
if (Object.keys(data).length) {
|
||||
entry.data = data;
|
||||
}
|
||||
|
||||
const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
|
||||
fn(JSON.stringify(entry));
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
log.info = (ctx, msg, data) => log('info', ctx, msg, data);
|
||||
log.warn = (ctx, msg, data) => log('warn', ctx, msg, data);
|
||||
log.error = (ctx, msg, data) => log('error', ctx, msg, data);
|
||||
log.debug = (ctx, msg, data) => log('debug', ctx, msg, data);
|
||||
|
||||
module.exports = log;
|
||||
Reference in New Issue
Block a user