Files
bare-operating-system/packages/bare-os-booter/lib/bare-os-var-log.js
T
Raven Scott 1faf9715b7 feat: POSIX/P2P roadmap — proc surfaces, syscalls, boot perf, docs, CI
- Align posix-conformance-matrix bareOsSyscallOps with getconf BARE_OS_SYSCALL_OPS
- Add verify-boot-policy-extension-signer-pins.mjs to pretest; document in scripts/README
- Document extensionSignerPinsV2–V5 env wiring in OTA_AND_BUNDLES; vault multisig sketch
- Note telemetry NDJSON redaction limits in environment appendix
- Expand booter CHANGELOG 1.40.0 (boot-perf, shell POSIX mode, subprocess errors, etc.)
- Holepunch sync README: NDJSON summary path

Seeder/kernel parity and pretest already green for bundled changes.
2026-04-05 00:02:23 -04:00

262 lines
9.2 KiB
JavaScript

/**
* 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`
/** Structured JSON lines from `/bin/logger` (syslog-style metadata). */
export const LOGGER_JSON_LOG = `${BARE_OS_VAR_LOG_DIR}/logger.jsonl`
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/<HOME-basename>/).
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
`
/**
* Strip long hex / obvious secret-shaped tokens from telemetry mirrors (best-effort).
* @param {string} s
*/
function bareOsTelemetryRedactString(s) {
return String(s)
.replace(/\b[0-9a-f]{128,}\b/gi, '<redacted_hex>')
.replace(/\bBearer\s+\S+/gi, 'Bearer <redacted>')
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
}
/**
* @param {Record<string, unknown>} rec
*/
function bareOsTelemetrySanitizeRec(rec) {
if (!rec || typeof rec !== 'object') return {}
/** @type {Record<string, unknown>} */
const o = {}
for (const [k, v] of Object.entries(rec)) {
if (typeof v === 'string') o[k] = bareOsTelemetryRedactString(v)
else o[k] = v
}
return o
}
/** 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} ctx
* @param {Record<string, unknown>} rec
*/
async function mirrorBareOsTelemetryNdjson(ctx, rec) {
try {
const env = /** @type {Record<string, string>} */ (
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 traceId = String(env.BARE_OS_TRACE_ID || '').trim()
const safeRec = bareOsTelemetrySanitizeRec(
/** @type {Record<string, unknown>} */ (rec)
)
const line =
JSON.stringify({
telemetrySchemaVersion: BARE_OS_TELEMETRY_SCHEMA_VERSION,
lifecycleSchemaVersion: BARE_OS_LIFECYCLE_SCHEMA_VERSION,
ts: Date.now(),
traceId: traceId ? traceId.slice(0, 128) : undefined,
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
})(),
...safeRec
}) + '\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<string, unknown>} ctx
* @param {string} eventName
* @param {Record<string, unknown>} attrs
*/
async function mirrorBareOsOtelJsonl(ctx, eventName, attrs) {
try {
const env = /** @type {Record<string, string>} */ (
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 safeAttrs = bareOsTelemetrySanitizeRec(
/** @type {Record<string, unknown>} */ (attrs)
)
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(safeAttrs).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 */
}
}