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:
Krystie
2026-03-22 11:05:50 +01:00
parent d5a6789366
commit 6771e4775e
3 changed files with 120 additions and 0 deletions

View 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,
};

View File

@@ -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,
};

View 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;