feat: Update error handling to use getErrorMessage utility and improve logging across multiple services

This commit is contained in:
2025-11-25 08:25:54 +00:00
parent c59d56e70a
commit e94906b3bf
13 changed files with 97 additions and 75 deletions

View File

@@ -17,7 +17,7 @@
"@std/encoding": "jsr:@std/encoding@^1.0.10",
"@db/sqlite": "jsr:@db/sqlite@0.12.0",
"@push.rocks/smartdaemon": "npm:@push.rocks/smartdaemon@^2.1.0",
"@apiclient.xyz/docker": "npm:@apiclient.xyz/docker@^5.0.2",
"@apiclient.xyz/docker": "npm:@apiclient.xyz/docker@^5.1.0",
"@apiclient.xyz/cloudflare": "npm:@apiclient.xyz/cloudflare@6.4.3",
"@push.rocks/smartacme": "npm:@push.rocks/smartacme@^8.0.0",
"@push.rocks/smartregistry": "npm:@push.rocks/smartregistry@^1.8.0",

5
mod.ts
View File

@@ -7,13 +7,14 @@
*/
import { runCli } from './ts/index.ts';
import { getErrorMessage } from './ts/utils/error.ts';
if (import.meta.main) {
try {
await runCli();
} catch (error) {
console.error(`Error: ${error.message}`);
if (Deno.args.includes('--debug')) {
console.error(`Error: ${getErrorMessage(error)}`);
if (Deno.args.includes('--debug') && error instanceof Error) {
console.error(error.stack);
}
Deno.exit(1);

View File

@@ -11,6 +11,7 @@ import type {
ISslCertificate,
IServiceDeployOptions,
} from '../types.ts';
import { getErrorMessage } from '../utils/error.ts';
export class OneboxApiClient {
private baseUrl: string;
@@ -193,7 +194,7 @@ export class OneboxApiClient {
return await response.json();
} catch (error) {
if (error.name === 'TimeoutError') {
if (error instanceof Error && error.name === 'TimeoutError') {
throw new Error('Request timed out. Daemon might be unresponsive.');
}
throw error;

View File

@@ -836,7 +836,7 @@ export class OneboxDatabase {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
const values: BindValue[] = [];
if (updates.image !== undefined) {
fields.push('image = ?');
@@ -1202,7 +1202,7 @@ export class OneboxDatabase {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
const values: BindValue[] = [];
if (updates.certPath) {
fields.push('cert_path = ?');
@@ -1303,7 +1303,7 @@ export class OneboxDatabase {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
const values: BindValue[] = [];
if (updates.domain !== undefined) {
fields.push('domain = ?');
@@ -1405,7 +1405,7 @@ export class OneboxDatabase {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
const values: BindValue[] = [];
if (updates.certDomain !== undefined) {
fields.push('cert_domain = ?');
@@ -1524,7 +1524,7 @@ export class OneboxDatabase {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
const values: BindValue[] = [];
if (updates.subdomain !== undefined) {
fields.push('subdomain = ?');
@@ -1711,7 +1711,7 @@ export class OneboxDatabase {
if (!this.db) throw new Error('Database not initialized');
const fields: string[] = [];
const values: unknown[] = [];
const values: BindValue[] = [];
if (updates.status !== undefined) {
fields.push('status = ?');

View File

@@ -10,7 +10,7 @@ import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
export class OneboxDockerManager {
private dockerClient: plugins.docker.Docker | null = null;
private dockerClient: InstanceType<typeof plugins.docker.Docker> | null = null;
private networkName = 'onebox-network';
/**
@@ -695,14 +695,6 @@ export class OneboxDockerManager {
timestamps: true,
});
// v5 should return a string, but let's handle edge cases
if (typeof logs !== 'string') {
logger.error(`Unexpected logs type: ${typeof logs}, constructor: ${logs?.constructor?.name}`);
logger.error(`Logs content: ${JSON.stringify(logs).slice(0, 500)}`);
// If it's not a string, something went wrong
throw new Error(`Unexpected log format: expected string, got ${typeof logs}`);
}
// v5 returns already-parsed logs as a string
return {
stdout: logs,
@@ -809,20 +801,17 @@ export class OneboxDockerManager {
throw new Error(`Container not found: ${containerID}`);
}
const exec = await container.exec({
Cmd: cmd,
AttachStdout: true,
AttachStderr: true,
const { stream, inspect } = await container.exec(cmd, {
attachStdout: true,
attachStderr: true,
});
const stream = await exec.start({ Detach: false });
let stdout = '';
let stderr = '';
stream.on('data', (chunk: Buffer) => {
stream.on('data', (chunk: Uint8Array) => {
const streamType = chunk[0];
const content = chunk.slice(8).toString();
const content = new TextDecoder().decode(chunk.slice(8));
if (streamType === 1) {
stdout += content;
@@ -834,8 +823,8 @@ export class OneboxDockerManager {
// Wait for completion
await new Promise((resolve) => stream.on('end', resolve));
const inspect = await exec.inspect();
const exitCode = inspect.ExitCode || 0;
const execInfo = await inspect();
const exitCode = execInfo.ExitCode || 0;
return { stdout, stderr, exitCode };
} catch (error) {
@@ -928,4 +917,26 @@ export class OneboxDockerManager {
throw error;
}
}
/**
* Get a container by ID
* Public wrapper for Docker client method
*/
async getContainerById(containerID: string): Promise<any> {
if (!this.dockerClient) {
throw new Error('Docker client not initialized');
}
return this.dockerClient.getContainerById(containerID);
}
/**
* List all containers
* Public wrapper for Docker client method
*/
async listAllContainers(): Promise<any[]> {
if (!this.dockerClient) {
throw new Error('Docker client not initialized');
}
return this.dockerClient.listContainers();
}
}

View File

@@ -24,7 +24,7 @@ export class CredentialEncryption {
}
this.key = await crypto.subtle.importKey(
'raw',
keyBytes,
keyBytes.buffer as ArrayBuffer,
{ name: this.algorithm },
false,
['encrypt', 'decrypt']

View File

@@ -8,7 +8,7 @@ import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
import type { Onebox } from './onebox.ts';
import type { IApiResponse, ICreateRegistryTokenRequest, IRegistryTokenView } from '../types.ts';
import type { IApiResponse, ICreateRegistryTokenRequest, IRegistryTokenView, TPlatformServiceType } from '../types.ts';
export class OneboxHttpServer {
private oneboxRef: Onebox;
@@ -482,8 +482,8 @@ export class OneboxHttpServer {
private async handleGetLogsRequest(name: string): Promise<Response> {
try {
const logs = await this.oneboxRef.services.getServiceLogs(name);
logger.log(`handleGetLogsRequest: logs type = ${typeof logs}, constructor = ${logs?.constructor?.name}`);
logger.log(`handleGetLogsRequest: logs value = ${String(logs).slice(0, 100)}`);
logger.debug(`handleGetLogsRequest: logs type = ${typeof logs}, constructor = ${logs?.constructor?.name}`);
logger.debug(`handleGetLogsRequest: logs value = ${String(logs).slice(0, 100)}`);
return this.jsonResponse({ success: true, data: logs });
} catch (error) {
logger.error(`Failed to get logs for service ${name}: ${getErrorMessage(error)}`);
@@ -878,13 +878,13 @@ export class OneboxHttpServer {
// Get the container (handle both direct container IDs and service IDs)
logger.info(`Looking up container for service ${serviceName}, containerID: ${service.containerID}`);
let container = await this.oneboxRef.docker.dockerClient!.getContainerById(service.containerID!);
let container = await this.oneboxRef.docker.getContainerById(service.containerID!);
logger.info(`Direct lookup result: ${container ? 'found' : 'null'}`);
// If not found, it might be a service ID - try to get the actual container ID
if (!container) {
logger.info('Listing all containers to find matching service...');
const containers = await this.oneboxRef.docker.dockerClient!.listContainers();
const containers = await this.oneboxRef.docker.listAllContainers();
logger.info(`Found ${containers.length} containers`);
const serviceContainer = containers.find((c: any) => {
@@ -894,7 +894,7 @@ export class OneboxHttpServer {
if (serviceContainer) {
logger.info(`Found matching container: ${serviceContainer.Id}`);
container = await this.oneboxRef.docker.dockerClient!.getContainerById(serviceContainer.Id);
container = await this.oneboxRef.docker.getContainerById(serviceContainer.Id);
logger.info(`Second lookup result: ${container ? 'found' : 'null'}`);
} else {
logger.error(`No container found with service label matching ${service.containerID}`);
@@ -924,18 +924,21 @@ export class OneboxHttpServer {
// Demultiplex and pipe log data to WebSocket
// Docker streams use 8-byte headers: [STREAM_TYPE, 0, 0, 0, SIZE_BYTE1, SIZE_BYTE2, SIZE_BYTE3, SIZE_BYTE4]
let buffer = Buffer.alloc(0);
let buffer = new Uint8Array(0);
logStream.on('data', (chunk: Buffer) => {
logStream.on('data', (chunk: Uint8Array) => {
if (socket.readyState !== WebSocket.OPEN) return;
// Append new data to buffer
buffer = Buffer.concat([buffer, chunk]);
const newBuffer = new Uint8Array(buffer.length + chunk.length);
newBuffer.set(buffer);
newBuffer.set(chunk, buffer.length);
buffer = newBuffer;
// Process complete frames
while (buffer.length >= 8) {
// Read frame size from header (bytes 4-7, big-endian)
const frameSize = buffer.readUInt32BE(4);
const frameSize = (buffer[4] << 24) | (buffer[5] << 16) | (buffer[6] << 8) | buffer[7];
// Check if we have the complete frame
if (buffer.length < 8 + frameSize) {
@@ -946,7 +949,7 @@ export class OneboxHttpServer {
const frameData = buffer.slice(8, 8 + frameSize);
// Send the clean log line
socket.send(frameData.toString('utf8'));
socket.send(new TextDecoder().decode(frameData));
// Remove processed frame from buffer
buffer = buffer.slice(8 + frameSize);
@@ -1083,7 +1086,7 @@ export class OneboxHttpServer {
}
}
private async handleGetPlatformServiceRequest(type: string): Promise<Response> {
private async handleGetPlatformServiceRequest(type: TPlatformServiceType): Promise<Response> {
try {
const provider = this.oneboxRef.platformServices.getProvider(type);
if (!provider) {
@@ -1123,7 +1126,7 @@ export class OneboxHttpServer {
}
}
private async handleStartPlatformServiceRequest(type: string): Promise<Response> {
private async handleStartPlatformServiceRequest(type: TPlatformServiceType): Promise<Response> {
try {
const provider = this.oneboxRef.platformServices.getProvider(type);
if (!provider) {
@@ -1154,7 +1157,7 @@ export class OneboxHttpServer {
}
}
private async handleStopPlatformServiceRequest(type: string): Promise<Response> {
private async handleStopPlatformServiceRequest(type: TPlatformServiceType): Promise<Response> {
try {
const provider = this.oneboxRef.platformServices.getProvider(type);
if (!provider) {

View File

@@ -15,6 +15,7 @@ import type { IPlatformServiceProvider } from './providers/base.ts';
import { MongoDBProvider } from './providers/mongodb.ts';
import { MinioProvider } from './providers/minio.ts';
import { logger } from '../../logging.ts';
import { getErrorMessage } from '../../utils/error.ts';
import { credentialEncryption } from '../encryption.ts';
import type { Onebox } from '../onebox.ts';
@@ -126,7 +127,7 @@ export class PlatformServicesManager {
// Refresh platform service from database
platformService = this.oneboxRef.database.getPlatformServiceByType(type)!;
} catch (error) {
logger.error(`Failed to start ${provider.displayName}: ${error.message}`);
logger.error(`Failed to start ${provider.displayName}: ${getErrorMessage(error)}`);
this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'failed' });
throw error;
}
@@ -187,7 +188,7 @@ export class PlatformServicesManager {
});
logger.success(`${provider.displayName} platform service stopped`);
} catch (error) {
logger.error(`Failed to stop ${provider.displayName}: ${error.message}`);
logger.error(`Failed to stop ${provider.displayName}: ${getErrorMessage(error)}`);
this.oneboxRef.database.updatePlatformService(platformService.id!, { status: 'failed' });
throw error;
}
@@ -292,7 +293,7 @@ export class PlatformServicesManager {
this.oneboxRef.database.deletePlatformResource(resource.id!);
logger.success(`Cleaned up ${resource.resourceType} '${resource.resourceName}'`);
} catch (error) {
logger.error(`Failed to cleanup resource ${resource.id}: ${error.message}`);
logger.error(`Failed to cleanup resource ${resource.id}: ${getErrorMessage(error)}`);
// Continue with other resources even if one fails
}
}

View File

@@ -13,6 +13,7 @@ import type {
TPlatformResourceType,
} from '../../../types.ts';
import { logger } from '../../../logging.ts';
import { getErrorMessage } from '../../../utils/error.ts';
import { credentialEncryption } from '../../encryption.ts';
import type { Onebox } from '../../onebox.ts';
@@ -73,7 +74,7 @@ export class MinioProvider extends BasePlatformServiceProvider {
await Deno.mkdir('/var/lib/onebox/minio', { recursive: true });
} catch (e) {
if (!(e instanceof Deno.errors.AlreadyExists)) {
logger.warn(`Could not create MinIO data directory: ${e.message}`);
logger.warn(`Could not create MinIO data directory: ${getErrorMessage(e)}`);
}
}
@@ -127,7 +128,7 @@ export class MinioProvider extends BasePlatformServiceProvider {
return response.ok;
} catch (error) {
logger.debug(`MinIO health check failed: ${error.message}`);
logger.debug(`MinIO health check failed: ${getErrorMessage(error)}`);
return false;
}
}
@@ -205,7 +206,7 @@ export class MinioProvider extends BasePlatformServiceProvider {
}));
logger.info(`Set bucket policy for '${bucketName}'`);
} catch (e) {
logger.warn(`Could not set bucket policy: ${e.message}`);
logger.warn(`Could not set bucket policy: ${getErrorMessage(e)}`);
}
// Note: For proper per-service credentials, MinIO Admin API should be used
@@ -292,7 +293,7 @@ export class MinioProvider extends BasePlatformServiceProvider {
logger.success(`MinIO bucket '${resource.resourceName}' deleted`);
} catch (e) {
logger.error(`Failed to delete MinIO bucket: ${e.message}`);
logger.error(`Failed to delete MinIO bucket: ${getErrorMessage(e)}`);
throw e;
}
}

View File

@@ -13,6 +13,7 @@ import type {
TPlatformResourceType,
} from '../../../types.ts';
import { logger } from '../../../logging.ts';
import { getErrorMessage } from '../../../utils/error.ts';
import { credentialEncryption } from '../../encryption.ts';
import type { Onebox } from '../../onebox.ts';
@@ -69,7 +70,7 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
} catch (e) {
// Directory might already exist
if (!(e instanceof Deno.errors.AlreadyExists)) {
logger.warn(`Could not create MongoDB data directory: ${e.message}`);
logger.warn(`Could not create MongoDB data directory: ${getErrorMessage(e)}`);
}
}
@@ -135,7 +136,7 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
return true;
} catch (error) {
logger.debug(`MongoDB health check failed: ${error.message}`);
logger.debug(`MongoDB health check failed: ${getErrorMessage(error)}`);
return false;
}
}
@@ -233,7 +234,7 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
await db.command({ dropUser: credentials.username });
logger.info(`Dropped MongoDB user '${credentials.username}'`);
} catch (e) {
logger.warn(`Could not drop MongoDB user: ${e.message}`);
logger.warn(`Could not drop MongoDB user: ${getErrorMessage(e)}`);
}
// Drop the database

View File

@@ -8,6 +8,7 @@
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
export class RegistryManager {
private s3Server: any = null;
@@ -86,7 +87,7 @@ export class RegistryManager {
this.isInitialized = true;
logger.success('Onebox Registry initialized successfully');
} catch (error) {
logger.error(`Failed to initialize registry: ${error.message}`);
logger.error(`Failed to initialize registry: ${getErrorMessage(error)}`);
throw error;
}
}
@@ -102,7 +103,7 @@ export class RegistryManager {
try {
return await this.registry.handleRequest(req);
} catch (error) {
logger.error(`Registry request error: ${error.message}`);
logger.error(`Registry request error: ${getErrorMessage(error)}`);
return new Response('Internal registry error', { status: 500 });
}
}
@@ -119,7 +120,7 @@ export class RegistryManager {
const tags = await this.registry.getTags(repository);
return tags || [];
} catch (error) {
logger.warn(`Failed to get tags for ${repository}: ${error.message}`);
logger.warn(`Failed to get tags for ${repository}: ${getErrorMessage(error)}`);
return [];
}
}
@@ -146,8 +147,9 @@ export class RegistryManager {
return null;
} catch (error) {
// Only log if it's not a "not a function" error
if (!error.message.includes('not a function')) {
logger.warn(`Failed to get digest for ${repository}:${tag}: ${error.message}`);
const errMsg = getErrorMessage(error);
if (!errMsg.includes('not a function')) {
logger.warn(`Failed to get digest for ${repository}:${tag}: ${errMsg}`);
}
return null;
}
@@ -165,7 +167,7 @@ export class RegistryManager {
await this.registry.deleteManifest(repository, tag);
logger.info(`Deleted image ${repository}:${tag}`);
} catch (error) {
logger.error(`Failed to delete image ${repository}:${tag}: ${error.message}`);
logger.error(`Failed to delete image ${repository}:${tag}: ${getErrorMessage(error)}`);
throw error;
}
}
@@ -212,7 +214,7 @@ export class RegistryManager {
await this.s3Server.stop();
logger.info('smarts3 server stopped');
} catch (error) {
logger.error(`Error stopping smarts3: ${error.message}`);
logger.error(`Error stopping smarts3: ${getErrorMessage(error)}`);
}
}

View File

@@ -5,6 +5,7 @@
*/
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
import { OneboxDatabase } from './database.ts';
interface IProxyRoute {
@@ -43,7 +44,7 @@ export class OneboxReverseProxy {
try {
logger.info('Reverse proxy initialized');
} catch (error) {
logger.error(`Failed to initialize reverse proxy: ${error.message}`);
logger.error(`Failed to initialize reverse proxy: ${getErrorMessage(error)}`);
throw error;
}
}
@@ -77,7 +78,7 @@ export class OneboxReverseProxy {
logger.success(`HTTP reverse proxy started on port ${this.httpPort}`);
} catch (error) {
logger.error(`Failed to start HTTP reverse proxy: ${error.message}`);
logger.error(`Failed to start HTTP reverse proxy: ${getErrorMessage(error)}`);
throw error;
}
}
@@ -122,7 +123,7 @@ export class OneboxReverseProxy {
logger.success(`HTTPS reverse proxy started on port ${this.httpsPort}`);
} catch (error) {
logger.error(`Failed to start HTTPS reverse proxy: ${error.message}`);
logger.error(`Failed to start HTTPS reverse proxy: ${getErrorMessage(error)}`);
// Don't throw - HTTPS is optional
logger.warn('Continuing without HTTPS support');
}
@@ -197,7 +198,7 @@ export class OneboxReverseProxy {
headers: this.filterResponseHeaders(response.headers),
});
} catch (error) {
logger.error(`Proxy error for ${host}: ${error.message}`);
logger.error(`Proxy error for ${host}: ${getErrorMessage(error)}`);
return new Response('Bad Gateway', {
status: 502,
headers: { 'Content-Type': 'text/plain' },
@@ -264,7 +265,7 @@ export class OneboxReverseProxy {
return response;
} catch (error) {
logger.error(`WebSocket upgrade error: ${error.message}`);
logger.error(`WebSocket upgrade error: ${getErrorMessage(error)}`);
return new Response('WebSocket Upgrade Failed', {
status: 500,
headers: { 'Content-Type': 'text/plain' },
@@ -353,7 +354,7 @@ export class OneboxReverseProxy {
this.routes.set(domain, route);
logger.success(`Added proxy route: ${domain} -> ${targetHost}:${targetPort}`);
} catch (error) {
logger.error(`Failed to add route for ${domain}: ${error.message}`);
logger.error(`Failed to add route for ${domain}: ${getErrorMessage(error)}`);
throw error;
}
}
@@ -395,7 +396,7 @@ export class OneboxReverseProxy {
logger.success(`Loaded ${this.routes.size} proxy routes`);
} catch (error) {
logger.error(`Failed to reload routes: ${error.message}`);
logger.error(`Failed to reload routes: ${getErrorMessage(error)}`);
throw error;
}
}
@@ -423,7 +424,7 @@ export class OneboxReverseProxy {
logger.warn('HTTPS server restart required for new certificate to take effect');
}
} catch (error) {
logger.error(`Failed to add certificate for ${domain}: ${error.message}`);
logger.error(`Failed to add certificate for ${domain}: ${getErrorMessage(error)}`);
throw error;
}
}
@@ -455,7 +456,7 @@ export class OneboxReverseProxy {
try {
await this.addCertificate(cert.domain, cert.fullChainPath, cert.keyPath);
} catch (error) {
logger.warn(`Failed to load certificate for ${cert.domain}: ${error.message}`);
logger.warn(`Failed to load certificate for ${cert.domain}: ${getErrorMessage(error)}`);
}
}
}
@@ -470,7 +471,7 @@ export class OneboxReverseProxy {
await this.startHttps();
}
} catch (error) {
logger.error(`Failed to reload certificates: ${error.message}`);
logger.error(`Failed to reload certificates: ${getErrorMessage(error)}`);
throw error;
}
}

View File

@@ -382,9 +382,9 @@ export class OneboxServicesManager {
const logs = await this.docker.getContainerLogs(service.containerID, tail);
// Debug: check what we got
logger.log(`getServiceLogs: logs type = ${typeof logs}, constructor = ${logs?.constructor?.name}`);
logger.log(`getServiceLogs: logs.stdout type = ${typeof logs.stdout}`);
logger.log(`getServiceLogs: logs.stdout value = ${String(logs.stdout).slice(0, 100)}`);
logger.debug(`getServiceLogs: logs type = ${typeof logs}, constructor = ${logs?.constructor?.name}`);
logger.debug(`getServiceLogs: logs.stdout type = ${typeof logs.stdout}`);
logger.debug(`getServiceLogs: logs.stdout value = ${String(logs.stdout).slice(0, 100)}`);
// v5 API returns combined stdout/stderr with proper formatting
return logs.stdout;
@@ -746,7 +746,7 @@ export class OneboxServicesManager {
});
}
} catch (error) {
logger.error(`Failed to check updates for ${service.name}: ${error.message}`);
logger.error(`Failed to check updates for ${service.name}: ${getErrorMessage(error)}`);
}
}
}