Skip to content

Commit 492ccd9

Browse files
authored
Fixed prettysteam timezone handling (#206)
ref be5ddf2 - The tests for PrettyStream are failing for me locally with incorrect timestamps - It seems there was an issue introduced with the refactor to remove moment The code adds the timezone offset, instead of subtracting it, moving it further from UTCi - This change does the following: - If a timestamp is provided in the exact format YYYY-MM-DD HH:mm:ss, use it as-is without any timezone conversion - For other timestamp formats (ISO 8601, etc.), parse them with new Date() and format them using date-format - When no timestamp is provided, use the current local time - This approach: - Fixes the timezone bug where timestamps were being shifted incorrectly - Maintains backward compatibility with existing tests - Seemingly works correctly in all timezones (UTC and non-UTC) - Includes comprehensive tests to prevent regression - It's quite hard to reason about the history of this code and what it was intended for so this change also includes extensive futher test coverage to try to enusre this doesn't regress again.
1 parent 58e3549 commit 492ccd9

File tree

2 files changed

+183
-10
lines changed

2 files changed

+183
-10
lines changed

packages/pretty-stream/lib/PrettyStream.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,23 @@ class PrettyStream extends Transform {
101101

102102
let output = '';
103103

104-
// Convert the time to UTC
105-
const now = new Date();
106-
const dataTime = new Date(data.time || now);
107-
108-
// If the timezone offset is equal to the current timezone offset, and that timezone offset is not 0,
109-
// then we need to adjust the time
110-
if (dataTime.getTimezoneOffset() === now.getTimezoneOffset() && dataTime.getTimezoneOffset() !== 0) {
111-
dataTime.setMinutes(dataTime.getMinutes() + dataTime.getTimezoneOffset());
112-
}
104+
// Handle time formatting
105+
let time;
113106

114-
const time = dateFormat.asString('yyyy-MM-dd hh:mm:ss', dataTime);
107+
if (data.time) {
108+
// If time is provided as a string in the expected format, use it directly
109+
if (typeof data.time === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(data.time)) {
110+
time = data.time;
111+
} else {
112+
// Otherwise, parse and format it
113+
const dataTime = new Date(data.time);
114+
time = dateFormat.asString('yyyy-MM-dd hh:mm:ss', dataTime);
115+
}
116+
} else {
117+
// No time provided, use current time
118+
const now = new Date();
119+
time = dateFormat.asString('yyyy-MM-dd hh:mm:ss', now);
120+
}
115121

116122
let logLevel = _private.levelFromName[data.level].toUpperCase();
117123
const codes = _private.colors[_private.colorForLevel[data.level]];

packages/pretty-stream/test/PrettyStream.test.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,4 +272,171 @@ describe('PrettyStream', function () {
272272
}));
273273
});
274274
});
275+
276+
describe('timezone handling', function () {
277+
it('should display provided timestamps consistently', function (done) {
278+
var ghostPrettyStream = new PrettyStream({mode: 'short'});
279+
var writeStream = new Writable();
280+
281+
writeStream._write = function (data) {
282+
data = data.toString();
283+
// The timestamp should be formatted consistently
284+
data.should.containEql('[2016-07-01 00:00:00]');
285+
data.should.containEql('INFO');
286+
data.should.containEql('Test message');
287+
done();
288+
};
289+
290+
ghostPrettyStream.pipe(writeStream);
291+
292+
// Write with an explicit timestamp
293+
ghostPrettyStream.write(JSON.stringify({
294+
time: '2016-07-01 00:00:00',
295+
level: 30,
296+
msg: 'Test message'
297+
}));
298+
});
299+
300+
it('should handle ISO 8601 timestamps and convert to local time', function (done) {
301+
var ghostPrettyStream = new PrettyStream({mode: 'short'});
302+
var writeStream = new Writable();
303+
304+
writeStream._write = function (data) {
305+
data = data.toString();
306+
// ISO timestamp should be parsed and converted to local time
307+
// Extract the timestamp to verify format
308+
const timestampMatch = data.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
309+
timestampMatch.should.not.be.null();
310+
311+
// Verify the timestamp represents the correct moment
312+
// 2016-07-01T00:00:00.000Z in local time
313+
const parsedTime = new Date(timestampMatch[1]);
314+
const expectedTime = new Date('2016-07-01T00:00:00.000Z');
315+
316+
// The displayed local time should represent the same moment as the UTC time
317+
// Allow for some tolerance due to date parsing
318+
Math.abs(parsedTime.getTime() - expectedTime.getTime()).should.be.below(24 * 60 * 60 * 1000);
319+
320+
data.should.containEql('INFO');
321+
data.should.containEql('ISO timestamp test');
322+
done();
323+
};
324+
325+
ghostPrettyStream.pipe(writeStream);
326+
327+
// Write with an ISO 8601 timestamp
328+
ghostPrettyStream.write(JSON.stringify({
329+
time: '2016-07-01T00:00:00.000Z',
330+
level: 30,
331+
msg: 'ISO timestamp test'
332+
}));
333+
});
334+
335+
it('should handle timestamps with timezone offsets', function (done) {
336+
var ghostPrettyStream = new PrettyStream({mode: 'short'});
337+
var writeStream = new Writable();
338+
339+
writeStream._write = function (data) {
340+
data = data.toString();
341+
// Timestamp with timezone offset should be converted to local time for display
342+
const timestampMatch = data.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
343+
timestampMatch.should.not.be.null();
344+
345+
data.should.containEql('INFO');
346+
data.should.containEql('Timezone offset test');
347+
done();
348+
};
349+
350+
ghostPrettyStream.pipe(writeStream);
351+
352+
// Write with a timestamp that includes timezone offset
353+
ghostPrettyStream.write(JSON.stringify({
354+
time: '2016-07-01T00:00:00+02:00',
355+
level: 30,
356+
msg: 'Timezone offset test'
357+
}));
358+
});
359+
360+
it('should use current local time when no timestamp is provided', function (done) {
361+
var ghostPrettyStream = new PrettyStream({mode: 'short'});
362+
var writeStream = new Writable();
363+
364+
// Capture the time before the test
365+
const beforeTime = new Date();
366+
367+
writeStream._write = function (data) {
368+
data = data.toString();
369+
370+
// Extract the timestamp from the output
371+
const timestampMatch = data.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
372+
timestampMatch.should.not.be.null();
373+
374+
const loggedTime = new Date(timestampMatch[1]);
375+
const afterTime = new Date();
376+
377+
// The logged time should be between beforeTime and afterTime
378+
loggedTime.getTime().should.be.aboveOrEqual(beforeTime.getTime());
379+
loggedTime.getTime().should.be.belowOrEqual(afterTime.getTime());
380+
381+
done();
382+
};
383+
384+
ghostPrettyStream.pipe(writeStream);
385+
386+
// Write without a timestamp
387+
ghostPrettyStream.write(JSON.stringify({
388+
level: 30,
389+
msg: 'No timestamp test'
390+
}));
391+
});
392+
393+
it('should work correctly in different timezones', function (done) {
394+
// This test verifies that string timestamps are displayed as-is
395+
var ghostPrettyStream = new PrettyStream({mode: 'short'});
396+
var writeStream = new Writable();
397+
398+
writeStream._write = function (data) {
399+
data = data.toString();
400+
// String timestamp should be displayed exactly as provided
401+
data.should.containEql('[2016-07-01 00:00:00]');
402+
data.should.containEql('String timestamp');
403+
done();
404+
};
405+
406+
ghostPrettyStream.pipe(writeStream);
407+
408+
// Test with string timestamp - should be displayed as-is
409+
ghostPrettyStream.write(JSON.stringify({
410+
time: '2016-07-01 00:00:00',
411+
level: 30,
412+
msg: 'String timestamp'
413+
}));
414+
});
415+
416+
it('regression test: string timestamps should not be affected by timezone offset', function (done) {
417+
// This test ensures the bug from commit be5ddf2 doesn't resurface
418+
// String timestamps like '2016-07-01 00:00:00' should be displayed exactly as provided
419+
// regardless of the system timezone
420+
var ghostPrettyStream = new PrettyStream({mode: 'short'});
421+
var writeStream = new Writable();
422+
423+
writeStream._write = function (data) {
424+
data = data.toString();
425+
// The exact string '2016-07-01 00:00:00' should appear in the output
426+
// It should NOT be shifted by timezone offset (e.g., NOT '2016-06-30 23:00:00')
427+
data.should.match(/^\[2016-07-01 00:00:00\]/);
428+
data.should.containEql('Regression test');
429+
done();
430+
};
431+
432+
ghostPrettyStream.pipe(writeStream);
433+
434+
// This timestamp format was causing issues in non-UTC timezones
435+
ghostPrettyStream.write(JSON.stringify({
436+
time: '2016-07-01 00:00:00',
437+
level: 30,
438+
msg: 'Regression test'
439+
}));
440+
});
441+
});
275442
});

0 commit comments

Comments
 (0)