Skip to content

Commit 3c9a33d

Browse files
committed
feat: Node 环境日志新增日期时间; 优化日志显示逻辑
1 parent 5132947 commit 3c9a33d

3 files changed

Lines changed: 198 additions & 9 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.24.9",
3+
"version": "2.24.10",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"packageManager": "pnpm@11.0.9",

backend/src/test/restful/logs.spec.js

Lines changed: 164 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ let appendLogEntry;
88
let getLogEntries;
99
let clearLogEntries;
1010
let clearLogSettingsCache;
11+
let prependConsoleTimestamp;
1112
let registerLogRoutes;
1213
let registerSettingsRoutes;
1314
let originalRead;
@@ -28,10 +29,7 @@ ${'\u2505'.repeat(44)}
2829
const DEFAULT_IGNORED_NOISE_LOGS = [
2930
'[sub-store] INFO: Surge AnyTLS Parser is activated',
3031
'[sub-store] ERROR: Fallback Base64 Pre-processor error: decoded line does not start with protocol',
31-
];
32-
const DEFAULT_VISIBLE_PREPROCESSOR_LOGS = [
33-
'[sub-store] INFO: Pre-processor [Fallback Base64 Pre-processor] activated',
34-
'[sub-store] INFO: Pre-processor [Clash Pre-processor] activated',
32+
'[sub-store] INFO: [CORS] allowed origins: https://sub-store.vercel.app,http://127.0.0.1:8888 (env:SUB_STORE_CORS_ALLOWED_ORIGINS)',
3533
];
3634

3735
function createRouteApp() {
@@ -88,6 +86,12 @@ function createResponse(routePath) {
8886
};
8987
}
9088

89+
function loadDebugLogsModuleFresh() {
90+
const modulePath = require.resolve('../../utils/debug-logs');
91+
delete require.cache[modulePath];
92+
return require('../../utils/debug-logs');
93+
}
94+
9195
describe('logs routes', function () {
9296
before(async function () {
9397
({ default: $ } = require('@/core/app'));
@@ -96,6 +100,7 @@ describe('logs routes', function () {
96100
getLogEntries,
97101
clearLogEntries,
98102
clearLogSettingsCache,
103+
prependConsoleTimestamp,
99104
} = require('@/utils/debug-logs'));
100105
({ default: registerLogRoutes } = require('@/restful/logs'));
101106
({ default: registerSettingsRoutes } = require('@/restful/settings'));
@@ -138,6 +143,157 @@ describe('logs routes', function () {
138143
expect(storedLogs[1].message).to.equal('[unknown] ERROR: third');
139144
});
140145

146+
it('prefixes locale timestamp in string console output arguments', function () {
147+
const args = ['route log', { ok: true }];
148+
const originalToLocaleString = Date.prototype.toLocaleString;
149+
150+
try {
151+
Date.prototype.toLocaleString = () => 'TEST_LOCALE_TIME';
152+
153+
const timestampedArgs = prependConsoleTimestamp(args, {
154+
isNode: true,
155+
});
156+
157+
expect(timestampedArgs).to.deep.equal([
158+
'TEST_LOCALE_TIME route log',
159+
{ ok: true },
160+
]);
161+
expect(args).to.deep.equal(['route log', { ok: true }]);
162+
} finally {
163+
Date.prototype.toLocaleString = originalToLocaleString;
164+
}
165+
});
166+
167+
it('preserves node console format placeholders when timestamping', function () {
168+
const { format } = require('util');
169+
const originalToLocaleString = Date.prototype.toLocaleString;
170+
171+
try {
172+
Date.prototype.toLocaleString = () => 'TEST_LOCALE_TIME';
173+
174+
const timestampedArgs = prependConsoleTimestamp(
175+
['route log %s %d', 'ok', 1],
176+
{ isNode: true },
177+
);
178+
179+
expect(timestampedArgs).to.deep.equal([
180+
'TEST_LOCALE_TIME route log %s %d',
181+
'ok',
182+
1,
183+
]);
184+
expect(format(...timestampedArgs)).to.equal(
185+
'TEST_LOCALE_TIME route log ok 1',
186+
);
187+
} finally {
188+
Date.prototype.toLocaleString = originalToLocaleString;
189+
}
190+
});
191+
192+
it('does not treat timestamp percent signs as console format placeholders', function () {
193+
const { format } = require('util');
194+
const originalToLocaleString = Date.prototype.toLocaleString;
195+
196+
try {
197+
Date.prototype.toLocaleString = () => 'TEST_%s_TIME';
198+
199+
const stringArgs = prependConsoleTimestamp(['route log %s', 'ok'], {
200+
isNode: true,
201+
});
202+
const objectArgs = prependConsoleTimestamp([{ ok: true }], {
203+
isNode: true,
204+
});
205+
206+
expect(format(...stringArgs)).to.equal('TEST_%s_TIME route log ok');
207+
expect(format(...objectArgs)).to.equal('TEST_%s_TIME { ok: true }');
208+
} finally {
209+
Date.prototype.toLocaleString = originalToLocaleString;
210+
}
211+
});
212+
213+
it('keeps locale timestamp separate for non-string console output arguments', function () {
214+
const args = [{ ok: true }, 'route log'];
215+
const originalToLocaleString = Date.prototype.toLocaleString;
216+
217+
try {
218+
Date.prototype.toLocaleString = () => 'TEST_LOCALE_TIME';
219+
220+
const timestampedArgs = prependConsoleTimestamp(args, {
221+
isNode: true,
222+
});
223+
224+
expect(timestampedArgs).to.deep.equal([
225+
'TEST_LOCALE_TIME',
226+
{ ok: true },
227+
'route log',
228+
]);
229+
expect(args).to.deep.equal([{ ok: true }, 'route log']);
230+
} finally {
231+
Date.prototype.toLocaleString = originalToLocaleString;
232+
}
233+
});
234+
235+
it('prints locale timestamp for empty node console output arguments', function () {
236+
const originalToLocaleString = Date.prototype.toLocaleString;
237+
238+
try {
239+
Date.prototype.toLocaleString = () => 'TEST_LOCALE_TIME';
240+
241+
expect(prependConsoleTimestamp([], { isNode: true })).to.deep.equal(
242+
['TEST_LOCALE_TIME'],
243+
);
244+
} finally {
245+
Date.prototype.toLocaleString = originalToLocaleString;
246+
}
247+
});
248+
249+
it('does not prepend locale timestamp outside node runtime', function () {
250+
const args = ['route log', { ok: true }];
251+
252+
expect(prependConsoleTimestamp(args, { isNode: false })).to.deep.equal(
253+
args,
254+
);
255+
});
256+
257+
it('captures console logs outside node runtime', function () {
258+
const consoleMethods = ['log', 'info', 'warn', 'error', 'debug'];
259+
const originalConsole = Object.fromEntries(
260+
consoleMethods.map((method) => [method, console[method]]),
261+
);
262+
const testState = {
263+
[SETTINGS_KEY]: { logsMaxCount: 500 },
264+
[LOGS_KEY]: '[]',
265+
};
266+
const testApp = {
267+
env: { isNode: false },
268+
read(key) {
269+
return testState[key];
270+
},
271+
write(data, key) {
272+
testState[key] = data;
273+
return true;
274+
},
275+
};
276+
const { installConsoleLogCapture: installConsoleLogCaptureFresh } =
277+
loadDebugLogsModuleFresh();
278+
279+
try {
280+
consoleMethods.forEach((method) => {
281+
console[method] = () => {};
282+
});
283+
284+
installConsoleLogCaptureFresh(testApp);
285+
console.log('route log');
286+
287+
const storedLogs = JSON.parse(testState[LOGS_KEY]);
288+
expect(storedLogs).to.have.length(1);
289+
expect(storedLogs[0].message).to.equal('[unknown] LOG: route log');
290+
} finally {
291+
consoleMethods.forEach((method) => {
292+
console[method] = originalConsole[method];
293+
});
294+
}
295+
});
296+
141297
it('disables persistent log cache IO when logsMaxCount is zero', function () {
142298
state[SETTINGS_KEY] = { logsMaxCount: 0 };
143299
state[LOGS_KEY] = JSON.stringify([
@@ -488,13 +644,15 @@ describe('logs routes', function () {
488644
appendLogEntry($, 'info', ['before change 2']);
489645
appendLogEntry($, 'info', ['before change 3']);
490646

491-
await patchHandler({ body: { logsMaxCount: 1 } }, createResponse('/api/settings'));
647+
await patchHandler(
648+
{ body: { logsMaxCount: 1 } },
649+
createResponse('/api/settings'),
650+
);
492651

493652
appendLogEntry($, 'info', ['after change']);
494653

495654
const storedLogs = JSON.parse(state[LOGS_KEY]);
496655
expect(storedLogs).to.have.length(1);
497656
expect(storedLogs[0].message).to.equal('[unknown] INFO: after change');
498657
});
499-
500658
});

backend/src/utils/debug-logs.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,39 @@ const DEFAULT_IGNORED_SINGLE_LINE_LOG_MESSAGE_RES = [
1717
// /^Pre-processor \[Fallback Base64 Pre-processor\] activated$/,
1818
/^Pre-processor \[.+\] activated!?$/,
1919
/^Fallback Base64 Pre-processor error: decoded line does not start with protocol$/,
20+
/^\[CORS\] allowed origins: .+$/,
2021
];
2122

2223
let consoleCaptureInstalled = false;
2324
let isAppendingConsoleLog = false;
2425
let cachedLogsMaxCount = null;
2526
let cachedLogsMaxCountExpiresAt = 0;
2627

28+
export function prependConsoleTimestamp(args, { isNode } = {}) {
29+
const consoleArgs = Array.from(args || []);
30+
if (!isNode) {
31+
return consoleArgs;
32+
}
33+
34+
const timestamp = new Date().toLocaleString();
35+
if (typeof consoleArgs[0] === 'string') {
36+
const [format, ...restArgs] = consoleArgs;
37+
const safeTimestamp = restArgs.length
38+
? escapeConsoleFormatLiteral(timestamp)
39+
: timestamp;
40+
return [`${safeTimestamp} ${format}`, ...restArgs];
41+
}
42+
43+
const safeTimestamp = consoleArgs.length
44+
? escapeConsoleFormatLiteral(timestamp)
45+
: timestamp;
46+
return [safeTimestamp, ...consoleArgs];
47+
}
48+
49+
function escapeConsoleFormatLiteral(value) {
50+
return `${value}`.replace(/%/g, '%%');
51+
}
52+
2753
export function normalizeLogLimit(value, fallback = DEFAULT_LOG_LIMIT) {
2854
const normalizedFallback = normalizePositiveInteger(
2955
fallback,
@@ -149,14 +175,19 @@ export function clearLogEntries($) {
149175
}
150176

151177
export function installConsoleLogCapture($) {
152-
if (consoleCaptureInstalled || typeof console === 'undefined') return;
178+
if (consoleCaptureInstalled || typeof console === 'undefined') {
179+
return;
180+
}
153181
consoleCaptureInstalled = true;
154182

155183
LOG_LEVELS.forEach((level) => {
156184
const original = console[level];
157185
if (typeof original !== 'function') return;
158186
console[level] = function (...args) {
159-
original.apply(console, args);
187+
original.apply(
188+
console,
189+
prependConsoleTimestamp(args, { isNode: $.env.isNode }),
190+
);
160191
if (isAppendingConsoleLog) return;
161192
try {
162193
isAppendingConsoleLog = true;

0 commit comments

Comments
 (0)