Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions docs/website/content/docs/runtime/logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ It works for all model types (LLM, Whisper, NMT, Embeddings) and provides valuab

## Configuration

To configure global logging (level and console output), use a config file (`qvac.config.json`, `qvac.config.js`, or `qvac.config.ts`) and set:
- `loggerLevel`: `"error" | "warn" | "info" | "debug"`
- `loggerConsoleOutput`: `boolean`
The SDK's own server and client logs are silent on the console by default. They
are still delivered to `loggingStream()` and custom transports, so you opt in to
seeing them.

To configure global logging, use a config file (`qvac.config.json`,
`qvac.config.js`, or `qvac.config.ts`) and set:
- `loggerConsoleOutput`: `boolean` β€” print the SDK's own logs to the console. Defaults to `false`; set to `true` to see them.
- `loggerLevel`: `"error" | "warn" | "info" | "debug" | "off"` β€” minimum level for SDK loggers. Defaults to `"info"`; use `"off"` to suppress streams and transports too.

## Example

Expand Down
3 changes: 0 additions & 3 deletions packages/sdk/client/api/load-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,6 @@ async function runLoadModel(
"on typical mobile devices. Pass a `delegate` to `loadModel(...)` to " +
"run generation on a desktop peer instead.";
logger.warn(message);
// Surface via console too - if an RN host app doesn't wire getClientLogger()
// to a visible transport, logger.warn alone won't reach the dev.
console.warn(message);
}
}

Expand Down
9 changes: 7 additions & 2 deletions packages/sdk/client/rpc/node-rpc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { initializeConfig } from "@/client/init-hooks";
import { resolveConfig } from "@/client/config-loader/resolve-config.node";
import { getClientLogger } from "@/logging";
import { getClientLogger, getLogger, SDK_SERVER_NAMESPACE } from "@/logging";
import {
BareRuntimeBinaryNotFoundError,
RPCInitTimeoutError,
Expand All @@ -25,6 +25,9 @@ const RPC_INIT_TIMEOUT_MS = 30_000;
const WORKER_STDERR_TAIL_CHARS = 16_384;

const logger = getClientLogger();
// Addons route real logs through their JS callback; anything written straight to the
// worker's stderr is treated as debug.
const workerLogger = getLogger(SDK_SERVER_NAMESPACE, { enableConsole: false });

let rpcInstance: RPC | null = null;
let rpcPromise: Promise<RPC> | null = null;
Comment thread
opaninakuffo marked this conversation as resolved.
Expand Down Expand Up @@ -418,7 +421,9 @@ async function ensureRPC(): Promise<RPC> {
getWorkerStderr(bareWorkerProc)?.on("data", (chunk) => {
const text = chunk.toString();
workerStderrTail = appendWorkerStderrTail(workerStderrTail, text);
process.stderr.write(chunk);
for (const line of text.split("\n")) {
if (line.trim()) workerLogger.debug(line);
}
});

bareWorkerProc.on(
Expand Down
15 changes: 9 additions & 6 deletions packages/sdk/examples/quickstart.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
loadModel,
LLAMA_3_2_1B_INST_Q4_0,
completion,
unloadModel,
} from "@qvac/sdk";
// The SDK is silent by default. Pointing QVAC_CONFIG_PATH at a config with
// `loggerConsoleOutput: true` prints the SDK's client and server logs to the
// console. Drop this line (or set the flag to false) to run quietly.
const configDir = import.meta.dirname ?? process.cwd();
process.env["QVAC_CONFIG_PATH"] =
`${configDir}/config/default/default.config.json`;

const { loadModel, LLAMA_3_2_1B_INST_Q4_0, completion, unloadModel } =
await import("@qvac/sdk");

try {
// Load a model into memory
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk/logging/client-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export function getClientLogger(options?: LoggerOptions): Logger {
}
}

const logger = getLogger(CLIENT_NAMESPACE, options);
// The SDK's own logs stay off the console unless the caller opts in.
const logger = getLogger(CLIENT_NAMESPACE, { enableConsole: false, ...options });

if (!options) {
setCachedClientLogger(logger);
Expand Down
5 changes: 1 addition & 4 deletions packages/sdk/logging/server-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ export function getServerLogger(options?: LoggerOptions): Logger {
return cachedLogger;
}

const logger = createStreamLogger(SDK_LOG_ID, SDK_SERVER_NAMESPACE, {
enableConsole: true, // SDK logs should still print to console
...options,
});
const logger = createStreamLogger(SDK_LOG_ID, SDK_SERVER_NAMESPACE, options);

if (!options) {
cachedLogger = logger;
Expand Down
8 changes: 7 additions & 1 deletion packages/sdk/logging/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { LEVEL_PRIORITIES } from "@qvac/logging/constants";
import { LEVEL_PRIORITIES, LOG_LEVELS } from "@qvac/logging/constants";
import type { LogLevel } from "@qvac/logging";
import stringify from "fast-safe-stringify";
import type { Request } from "@/schemas";

export function isLevelEnabled(messageLevel: LogLevel, currentLevel: LogLevel) {
// "off" suppresses every emission (console, streams, and transports alike),
// even though its priority sits above the real levels.
if (currentLevel === LOG_LEVELS.OFF || messageLevel === LOG_LEVELS.OFF) {
return false;
}

const messagePriority = LEVEL_PRIORITIES[messageLevel];
const currentPriority = LEVEL_PRIORITIES[currentLevel];

Expand Down
8 changes: 7 additions & 1 deletion packages/sdk/schemas/logging-stream.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { LogLevel } from "@qvac/logging";
import { z } from "zod";

const logLevelValues: LogLevel[] = ["error", "warn", "info", "debug"] as const;
const logLevelValues: LogLevel[] = [
"error",
"warn",
"info",
"debug",
"off",
] as const;
export const logLevelSchema = z.enum(logLevelValues);

const loggingParamsSchema = z.object({
Expand Down
8 changes: 4 additions & 4 deletions packages/sdk/schemas/sdk-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,15 @@ export const qvacConfigSchema = z.object({

/**
* Global log level for all SDK loggers.
* Options: "error", "warn", "info", "debug"
* Options: "error", "warn", "info", "debug", "off".
* Defaults to "info".
*/
loggerLevel: logLevelSchema.optional(),

/**
* Enable or disable console output for SDK loggers.
* When false, logs are only sent to streams/transports, not printed to console.
* Defaults to true.
* Print SDK logs to the console.
* When false, logs still reach streams and transports but are not printed.
* Defaults to false; set to true to see SDK logs on the console.
*/
loggerConsoleOutput: z.boolean().optional(),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,6 @@ export function getLoggingStreamStats() {
}

export function clearAllLoggingStreams() {
const count = loggingStreams.size;

for (const timeout of bufferingTimeouts.values()) {
clearTimeout(timeout);
}
Expand All @@ -196,8 +194,4 @@ export function clearAllLoggingStreams() {
logBuffer.clear();
modelsWithBuffering.clear();
bufferingTimeouts.clear();

if (count > 0) {
console.log(`🧹 Cleared logging streams for ${count} ID(s)`); // fallback (avoid recursion)
}
}
86 changes: 86 additions & 0 deletions packages/sdk/test/unit/logging-defaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import test from "brittle";
import { getClientLogger } from "@/logging/client-logger";
import { createBaseLogger } from "@/logging/base-logger";
import { logLevelSchema } from "@/schemas/logging-stream";

test("logLevelSchema: accepts off alongside the standard levels", (t) => {
for (const level of ["error", "warn", "info", "debug", "off"]) {
t.is(logLevelSchema.safeParse(level).success, true, level);
}
t.is(logLevelSchema.safeParse("verbose").success, false);
});

test("getClientLogger: silent console by default, still feeds transports", (t) => {
const original = console.info;
let printed = 0;
console.info = () => {
printed++;
};
t.teardown(() => {
console.info = original;
});

const received: string[] = [];
const logger = getClientLogger({
transports: [(_level, _namespace, message) => received.push(message)],
});
logger.info("hello");

t.is(printed, 0, "nothing printed to console");
t.alike(received, ["hello"], "transport still receives the log");
});

test("getClientLogger: enableConsole opts back into console output", (t) => {
const original = console.info;
let printed = 0;
console.info = () => {
printed++;
};
t.teardown(() => {
console.info = original;
});

getClientLogger({ enableConsole: true }).info("hi");

t.ok(printed > 0, "console prints when explicitly enabled");
});

test("level off: silences console, stream, and transports together", (t) => {
const originals = {
error: console.error,
warn: console.warn,
info: console.info,
debug: console.debug,
};
let printed = 0;
console.error = console.warn = console.info = console.debug = () => {
printed++;
};
t.teardown(() => {
console.error = originals.error;
console.warn = originals.warn;
console.info = originals.info;
console.debug = originals.debug;
});

const streamed: string[] = [];
const transported: string[] = [];
const logger = createBaseLogger(
"test:off",
{
level: "off",
enableConsole: true,
transports: [(_l, _n, message) => transported.push(message)],
},
{ onLog: (_l, _n, message) => streamed.push(message) },
);

logger.error("e");
logger.warn("w");
logger.info("i");
logger.debug("d");

t.is(printed, 0, "nothing printed to console at off");
t.alike(streamed, [], "stream callback receives nothing at off");
t.alike(transported, [], "transports receive nothing at off");
});
Loading