/** * Append-only UTF-8 logs under logical `/var/log/…` (VFS maps to the personal Hyperdrive). */ import { BARE_OS_LIFECYCLE_SCHEMA_VERSION, BARE_OS_TELEMETRY_SCHEMA_VERSION } from './bare-os-lifecycle-schema.js' export const BARE_OS_VAR_LOG_DIR = '/var/log/bare-os' export const KERNEL_CONSOLE_LOG = `${BARE_OS_VAR_LOG_DIR}/kernel-console.log` export const CRON_LOG = `${BARE_OS_VAR_LOG_DIR}/cron.log` export const INITD_LOG = `${BARE_OS_VAR_LOG_DIR}/initd.log` export const AUDIT_LOG = `${BARE_OS_VAR_LOG_DIR}/audit.log` const README_REL = `${BARE_OS_VAR_LOG_DIR}/README` const README_TEXT = `Bare OS session logs (mirrored on your personal drive under /.bare-os/var/log//). kernel-console.log — console.log / console.error from the kernel session cron.log — bare-cron job errors initd.log — bare-initd service start failures audit.log — optional execLine audit when BARE_OS_AUDIT=1 ` /** Max size before trimming older log bytes (best-effort). */ const LOG_MAX_BYTES = 512 * 1024 /** After trim, keep this many trailing bytes plus a notice line. */ const LOG_KEEP_BYTES = 256 * 1024 /** * Ensure `/var/log/bare-os` exists and a short README is present. Best-effort; never throws. * @param {Record} ctx */ export async function ensureBareOsVarLogTree(ctx) { try { const vfs = ctx.vfs if (!vfs || typeof vfs.mkdir !== 'function') return await vfs.mkdir(BARE_OS_VAR_LOG_DIR, { recursive: true }) if ( typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function' ) return const existing = await vfs.readFile(README_REL) if (existing && ctx.b4a.from(existing).length > 0) return await vfs.writeFile(README_REL, ctx.b4a.from(README_TEXT)) } catch { /* ignore */ } } /** * Append one UTF-8 line to a logical log file under `/var/log/…`. Best-effort; never throws. * @param {Record} ctx * @param {string} logicalFilePath e.g. `/var/log/bare-os/cron.log` * @param {string} kind * @param {string} line */ export async function appendVarLog(ctx, logicalFilePath, kind, line) { try { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return const prev = await vfs.readFile(logicalFilePath) const ts = new Date().toISOString() const chunk = ctx.b4a.from(`[${ts}] [${kind}] ${line}\n`) let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk if (merged.length > LOG_MAX_BYTES) { const start = Math.max(0, merged.length - LOG_KEEP_BYTES) const tail = merged.subarray(start) const notice = ctx.b4a.from( `[${ts}] [bare-os] log truncated (kept last ${LOG_KEEP_BYTES} bytes): ${logicalFilePath}\n` ) merged = ctx.b4a.concat([notice, tail]) } await vfs.writeFile(logicalFilePath, merged) await mirrorBareOsTelemetryNdjson(ctx, { type: 'varLog', path: logicalFilePath, kind, line: String(line).slice(0, 4000) }) await mirrorBareOsOtelJsonl(ctx, 'varLog', { path: logicalFilePath, kind, line: String(line).slice(0, 4000) }) } catch { /* ignore */ } } /** * @param {Record} ctx * @param {Record} rec */ async function mirrorBareOsTelemetryNdjson(ctx, rec) { try { const env = /** @type {Record} */ ( ctx.env && typeof ctx.env === 'object' ? ctx.env : {} ) const raw = env.BARE_OS_TELEMETRY_NDJSON if (raw == null || raw === '' || raw === '0' || raw === 'false') return const dest = String(raw).trim() if (!dest.startsWith('/') && !dest.startsWith('~/')) return const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return const lineage = String(env.BARE_OS_TELEMETRY_SESSION_LINEAGE_ID || '').trim() const bootAttemptId = String(env.BARE_OS_BOOT_ATTEMPT_ID || '').trim() const bareModuleCryptoStagingProbeId = String(env.BARE_OS_PROBE_ID_BARE_MODULE_CRYPTO_STAGING || '').trim() const pearInspectLoggerTlsProbeId = String(env.BARE_OS_PROBE_ID_PEAR_INSPECT_LOGGER_TLS || '').trim() const hypercorePackHrpcLifecycleProbeId = String(env.BARE_OS_PROBE_ID_HYPERCORE_PACK_HRPC_LIFECYCLE || '').trim() const line = JSON.stringify({ telemetrySchemaVersion: BARE_OS_TELEMETRY_SCHEMA_VERSION, lifecycleSchemaVersion: BARE_OS_LIFECYCLE_SCHEMA_VERSION, ts: Date.now(), bootAttemptId: bootAttemptId ? bootAttemptId.slice(0, 128) : undefined, bareModuleCryptoStagingProbeId: bareModuleCryptoStagingProbeId ? bareModuleCryptoStagingProbeId.slice(0, 128) : undefined, pearInspectLoggerTlsProbeId: pearInspectLoggerTlsProbeId ? pearInspectLoggerTlsProbeId.slice(0, 128) : undefined, hypercorePackHrpcLifecycleProbeId: hypercorePackHrpcLifecycleProbeId ? hypercorePackHrpcLifecycleProbeId.slice(0, 128) : undefined, bareRpcProbeClass: String( env.BARE_OS_BARE_RPC_PROBE_CLASS || '' ) .trim() .slice(0, 64) || undefined, bareModuleProbeClass: String( env.BARE_OS_BARE_MODULE_PROBE_CLASS || '' ).trim().slice(0, 64) || undefined, sessionLineageId: lineage ? lineage.slice(0, 128) : undefined, hdmsPairingBackoffCount: (() => { const n = Number.parseInt( env.BARE_OS_HDMS_PAIRING_BACKOFF_COUNT || '', 10 ) return Number.isFinite(n) ? Math.min(1e6, n) : undefined })(), ...rec }) + '\n' const chunk = ctx.b4a.from(line) const prev = await vfs.readFile(dest) let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk const cap = 2 * 1024 * 1024 if (merged.length > cap) { merged = merged.subarray(merged.length - cap) } await vfs.writeFile(dest, merged) } catch { /* ignore */ } } /** * Optional OpenTelemetry-inspired JSON line export (`BARE_OS_TELEMETRY_OTEL_JSONL` logical path). * @param {Record} ctx * @param {string} eventName * @param {Record} attrs */ async function mirrorBareOsOtelJsonl(ctx, eventName, attrs) { try { const env = /** @type {Record} */ ( ctx.env && typeof ctx.env === 'object' ? ctx.env : {} ) const raw = env.BARE_OS_TELEMETRY_OTEL_JSONL if (raw == null || raw === '' || raw === '0' || raw === 'false') return const dest = String(raw).trim() if (!dest.startsWith('/') && !dest.startsWith('~/')) return const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return const line = JSON.stringify({ otlSchemaVersion: 8, resourceLogs: [ { resource: { attributes: [ { key: 'service.name', value: { stringValue: 'bare-os' } }, { key: 'telemetry.sdk.language', value: { stringValue: 'javascript' } } ] }, scopeLogs: [ { scope: { name: 'bare-os-booter', version: '1.0.0' }, logRecords: [ { timeUnixNano: String(Date.now() * 1e6), severityNumber: 9, severityText: 'INFO', body: { stringValue: eventName }, events: [], links: [], exemplars: [], attributes: Object.entries(attrs).map(([k, v]) => ({ key: k, value: typeof v === 'string' ? { stringValue: v } : { stringValue: JSON.stringify(v) } })) } ] } ] } ] }) + '\n' const chunk = ctx.b4a.from(line) const prev = await vfs.readFile(dest) let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk const cap = 2 * 1024 * 1024 if (merged.length > cap) { merged = merged.subarray(merged.length - cap) } await vfs.writeFile(dest, merged) } catch { /* ignore */ } }