taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Apackages/taler-util/src/logging.test.ts | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/logging.ts | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mpackages/taler-wallet-cli/src/index.ts | 2++
Mpackages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.test.ts | 25++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts | 9+++++++--
Mpackages/taler-wallet-core/src/crypto/workers/cryptoWorkerInterface.ts | 8++++++++
Apackages/taler-wallet-core/src/crypto/workers/worker-common.test.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/crypto/workers/worker-common.ts | 4++++
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";