commit e2055d929bcc2a139270d772d0fba99c122023b9
parent 7db312c031e09f56e712947c586c425eefefec61
Author: Florian Dold <dold@taler.net>
Date: Sun, 9 Aug 2026 19:01:39 +0200
util: add MESSAGE log level
Diffstat:
8 files changed, 243 insertions(+), 9 deletions(-)
diff --git a/packages/taler-util/src/logging.test.ts b/packages/taler-util/src/logging.test.ts
@@ -0,0 +1,79 @@
+/*
+ This file is part of TALER
+ (C) 2026 Taler Systems S.A.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { strict as assert } from "node:assert";
+import test from "node:test";
+import {
+ enableNativeLogging,
+ Logger,
+ setGlobalLogLevelFromString,
+} from "./logging.js";
+
+test("MESSAGE is ordered between INFO and WARN", () => {
+ const logger = new Logger("logging-test");
+
+ setGlobalLogLevelFromString("info");
+ assert.equal(logger.shouldLogInfo(), true);
+ assert.equal(logger.shouldLogMessage(), true);
+ assert.equal(logger.shouldLogWarn(), true);
+
+ setGlobalLogLevelFromString("message");
+ assert.equal(logger.shouldLogTrace(), false);
+ assert.equal(logger.shouldLogInfo(), false);
+ assert.equal(logger.shouldLogMessage(), true);
+ assert.equal(logger.shouldLogWarn(), true);
+ assert.equal(logger.shouldLogError(), true);
+
+ setGlobalLogLevelFromString("warn");
+ assert.equal(logger.shouldLogMessage(), false);
+ assert.equal(logger.shouldLogWarn(), true);
+
+ setGlobalLogLevelFromString("info");
+});
+
+test("MESSAGE output has no timestamp or component prefix", () => {
+ const logger = new Logger("logging-test");
+ const originalWrite = process.stderr.write;
+ let output = "";
+ process.stderr.write = ((chunk: Uint8Array | string) => {
+ output += chunk.toString();
+ return true;
+ }) as typeof process.stderr.write;
+ try {
+ logger.message("A user-facing message");
+ } finally {
+ process.stderr.write = originalWrite;
+ }
+ assert.equal(output, "A user-facing message\n");
+});
+
+test("native log levels include MESSAGE", () => {
+ const levels: number[] = [];
+ (globalThis as any).__nativeLog = (level: number) => levels.push(level);
+ enableNativeLogging();
+ setGlobalLogLevelFromString("trace");
+
+ const logger = new Logger("logging-test");
+ logger.trace("trace");
+ logger.info("info");
+ logger.message("message");
+ logger.warn("warn");
+ logger.error("error");
+
+ assert.deepEqual(levels, [1, 2, 3, 4, 5]);
+ delete (globalThis as any).__nativeLog;
+ setGlobalLogLevelFromString("info");
+});
diff --git a/packages/taler-util/src/logging.ts b/packages/taler-util/src/logging.ts
@@ -25,8 +25,8 @@ const isNode =
export enum LogLevel {
Trace = "trace",
- Message = "message",
Info = "info",
+ Message = "message",
Warn = "warn",
Error = "error",
None = "none",
@@ -79,6 +79,8 @@ function getLevelForString(logLevelStr: string): LogLevel {
return LogLevel.Trace;
case "info":
return LogLevel.Info;
+ case "message":
+ return LogLevel.Message;
case "warn":
case "warning":
return LogLevel.Warn;
@@ -141,6 +143,28 @@ function writeNodeLog(
}
}
+function writeNodeMessage(message: any, args: any[]): void {
+ try {
+ let msg = `${message}`;
+ if (args.length != 0) {
+ msg += ` ${JSON.stringify(args, undefined, 2)}\n`;
+ } else {
+ msg += `\n`;
+ }
+ process.stderr.write(msg);
+ } catch (e) {
+ // Keep failures in the logger itself identifiable even though ordinary
+ // MESSAGE output deliberately has no timestamp or component prefix.
+ let msg = `${new Date().toISOString()} (logger) FATAL `;
+ if (e instanceof Error) {
+ msg += `failed to write log: ${e.message}\n`;
+ } else {
+ msg += "failed to write log\n";
+ }
+ process.stderr.write(msg);
+ }
+}
+
/**
* Logger that writes to stderr when running under node,
* and uses the corresponding console.* method to log in the browser.
@@ -157,8 +181,8 @@ export class Logger {
switch (level) {
case LogLevel.Trace:
return true;
- case LogLevel.Message:
case LogLevel.Info:
+ case LogLevel.Message:
case LogLevel.Warn:
case LogLevel.Error:
case LogLevel.None:
@@ -170,8 +194,22 @@ export class Logger {
const level = byTagLogLevel[this.tag] ?? globalLogLevel;
switch (level) {
case LogLevel.Trace:
+ case LogLevel.Info:
+ return true;
case LogLevel.Message:
+ case LogLevel.Warn:
+ case LogLevel.Error:
+ case LogLevel.None:
+ return false;
+ }
+ }
+
+ shouldLogMessage(): boolean {
+ const level = byTagLogLevel[this.tag] ?? globalLogLevel;
+ switch (level) {
+ case LogLevel.Trace:
case LogLevel.Info:
+ case LogLevel.Message:
return true;
case LogLevel.Warn:
case LogLevel.Error:
@@ -184,8 +222,8 @@ export class Logger {
const level = byTagLogLevel[this.tag] ?? globalLogLevel;
switch (level) {
case LogLevel.Trace:
- case LogLevel.Message:
case LogLevel.Info:
+ case LogLevel.Message:
case LogLevel.Warn:
return true;
case LogLevel.Error:
@@ -198,8 +236,8 @@ export class Logger {
const level = byTagLogLevel[this.tag] ?? globalLogLevel;
switch (level) {
case LogLevel.Trace:
- case LogLevel.Message:
case LogLevel.Info:
+ case LogLevel.Message:
case LogLevel.Warn:
case LogLevel.Error:
return true;
@@ -226,12 +264,27 @@ export class Logger {
}
}
+ message(message: string, ...args: any[]): void {
+ if (!this.shouldLogMessage()) {
+ return;
+ }
+ if (nativeLogging) {
+ writeNativeLog(message, this.tag, 3, args);
+ return;
+ }
+ if (isNode) {
+ writeNodeMessage(message, args);
+ } else {
+ console.info(message, ...args);
+ }
+ }
+
warn(message: string, ...args: any[]): void {
if (!this.shouldLogWarn()) {
return;
}
if (nativeLogging) {
- writeNativeLog(message, this.tag, 3, args);
+ writeNativeLog(message, this.tag, 4, args);
return;
}
if (isNode) {
@@ -249,7 +302,7 @@ export class Logger {
return;
}
if (nativeLogging) {
- writeNativeLog(message, this.tag, 4, args);
+ writeNativeLog(message, this.tag, 5, args);
return;
}
if (isNode) {
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
@@ -3792,6 +3792,7 @@ testCli
testCli.subcommand("logtest", "logtest").action(async (args) => {
logger.trace("This is a trace message.");
logger.info("This is an info message.");
+ logger.message("This is a message.");
logger.warn("This is an warning message.");
logger.error("This is an error message.");
});
@@ -3803,6 +3804,7 @@ async function read(stream: NodeJS.ReadStream) {
}
export function main() {
+ setGlobalLogLevelFromString("message");
const maybeFilename = getenv("TALER_WALLET_DEBUG_OBSERVE");
if (!!maybeFilename) {
observabilityEventFile = maybeFilename;
diff --git a/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts
@@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, TalerErrorCode } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ setGlobalLogLevelFromString,
+ TalerErrorCode,
+} from "@gnu-taler/taler-util";
import { test } from "node:test";
import assert from "node:assert";
import { CryptoDispatcher, CryptoWorkerFactory } from "./crypto-dispatcher.js";
@@ -24,6 +28,8 @@ import {
} from "./cryptoWorkerInterface.js";
export class MyCryptoWorker implements CryptoWorker {
+ readonly receivedMessages: any[] = [];
+
/**
* Function to be called when we receive a message from the worker thread.
*/
@@ -58,6 +64,7 @@ export class MyCryptoWorker implements CryptoWorker {
* Send a message to the worker thread.
*/
postMessage(msg: any): void {
+ this.receivedMessages.push(msg);
const handleRequest = async () => {
let responseMsg: CryptoWorkerResponseMessage;
if (msg.operation === "testSuccess") {
@@ -117,6 +124,22 @@ export class MyCryptoWorkerFactory implements CryptoWorkerFactory {
}
}
+test("propagates the current log level to workers", async () => {
+ const factory = new MyCryptoWorkerFactory();
+ const cryptoDisp = new CryptoDispatcher(factory);
+ setGlobalLogLevelFromString("message");
+ try {
+ await cryptoDisp.doRpc("testSuccess", 0, {});
+ assert.strictEqual(
+ factory.started[0].receivedMessages[0].logLevel,
+ "message",
+ );
+ } finally {
+ cryptoDisp.stop();
+ setGlobalLogLevelFromString("info");
+ }
+});
+
test("continues after error", async (t) => {
const cryptoDisp = new CryptoDispatcher(new MyCryptoWorkerFactory());
const resp1 = await cryptoDisp.doRpc("testSuccess", 0, {});
diff --git a/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts
@@ -24,6 +24,7 @@
* Imports.
*/
import {
+ getGlobalLogLevel,
j2s,
Logger,
makeErrorDetail,
@@ -36,7 +37,10 @@ import {
TimerHandle,
} from "@gnu-taler/taler-util";
import { nullCrypto, TalerCryptoInterface } from "../cryptoImplementation.js";
-import { CryptoWorker } from "./cryptoWorkerInterface.js";
+import {
+ CryptoWorker,
+ CryptoWorkerRequestMessage,
+} from "./cryptoWorkerInterface.js";
const logger = new Logger("cryptoDispatcher.ts");
@@ -201,10 +205,11 @@ export class CryptoDispatcher {
worker = ws.w;
}
- const msg: any = {
+ const msg: CryptoWorkerRequestMessage = {
req: work.req,
id: work.rpcId,
operation: work.operation,
+ logLevel: getGlobalLogLevel(),
};
this.resetWorkerTimeout(ws);
work.startTime = performanceNow();
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts
@@ -47,6 +47,14 @@ export type CryptoWorkerRequestMessage = {
* Operation-specific request payload.
*/
req: any;
+
+ /**
+ * Logging threshold to use while processing this request.
+ *
+ * Workers can run in a separate JavaScript realm, so they do not otherwise
+ * observe changes to the caller's global logging configuration.
+ */
+ logLevel?: string;
};
/**
diff --git a/packages/taler-wallet-core/src/crypto/workers/worker-common.test.ts b/packages/taler-wallet-core/src/crypto/workers/worker-common.test.ts
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Logger, setGlobalLogLevelFromString } from "@gnu-taler/taler-util";
+import assert from "node:assert";
+import test from "node:test";
+import { TalerCryptoInterfaceR } from "../cryptoImplementation.js";
+import { processRequestWithImpl } from "./worker-common.js";
+
+test("applies the request log level before invoking crypto code", async () => {
+ const originalWrite = process.stderr.write;
+ let output = "";
+ process.stderr.write = ((chunk: Uint8Array | string) => {
+ output += chunk.toString();
+ return true;
+ }) as typeof process.stderr.write;
+
+ setGlobalLogLevelFromString("info");
+ try {
+ const impl = {
+ emitInfo: async () => {
+ new Logger("cryptoImplementation.ts").info("must not leak");
+ return "done";
+ },
+ } as unknown as TalerCryptoInterfaceR;
+
+ const response = await processRequestWithImpl(
+ {
+ id: 1,
+ operation: "emitInfo",
+ req: {},
+ logLevel: "message",
+ },
+ impl,
+ );
+
+ assert.deepStrictEqual(response, {
+ type: "success",
+ result: "done",
+ id: 1,
+ });
+ assert.strictEqual(output, "");
+ } finally {
+ process.stderr.write = originalWrite;
+ setGlobalLogLevelFromString("info");
+ }
+});
diff --git a/packages/taler-wallet-core/src/crypto/workers/worker-common.ts b/packages/taler-wallet-core/src/crypto/workers/worker-common.ts
@@ -20,6 +20,7 @@
import {
j2s,
Logger,
+ setGlobalLogLevelFromString,
stringifyError as safeStringifyError,
TalerErrorCode,
} from "@gnu-taler/taler-util";
@@ -54,6 +55,9 @@ export async function processRequestWithImpl(
}),
};
}
+ if (typeof reqMsg.logLevel === "string") {
+ setGlobalLogLevelFromString(reqMsg.logLevel);
+ }
const id = reqMsg.id;
if (typeof id !== "number") {
const msg = "RPC id must be number";