Skip to content

Commit feef9ac

Browse files
edge33mcollinatoomuchdesign
authored
feat: added timestamp rfc3339 format with nanoseconds (#2251)
* feat: added timestamp rfc3339 format with nanoseconds * chore: updated docs and tests * Update lib/time.js Co-authored-by: Andrea Carraro <toomuchdesign@users.noreply.github.com> * feat: updated and improved fn performance, fixed types * chore: updated docs and tests * chore: fixed leftover test comment --------- Co-authored-by: Matteo Collina <matteo.collina@gmail.com> Co-authored-by: Andrea Carraro <toomuchdesign@users.noreply.github.com>
1 parent f8e4161 commit feef9ac

7 files changed

Lines changed: 75 additions & 1 deletion

File tree

docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,6 +1423,7 @@ The `pino.stdTimeFunctions` object provides a very small set of common functions
14231423
* `pino.stdTimeFunctions.unixTime`: Seconds since Unix epoch
14241424
* `pino.stdTimeFunctions.nullTime`: Clears timestamp property (Used when `timestamp: false`)
14251425
* `pino.stdTimeFunctions.isoTime`: ISO 8601-formatted time in UTC
1426+
* `pino.stdTimeFunctions.isoTimeNano`: RFC 3339-formatted time in UTC with nanosecond precision
14261427
14271428
* See [`timestamp` option](#opt-timestamp)
14281429

lib/time.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,32 @@ const unixTime = () => `,"time":${Math.round(Date.now() / 1000.0)}`
88

99
const isoTime = () => `,"time":"${new Date(Date.now()).toISOString()}"` // using Date.now() for testability
1010

11-
module.exports = { nullTime, epochTime, unixTime, isoTime }
11+
const NS_PER_MS = 1_000_000n
12+
const NS_PER_SEC = 1_000_000_000n
13+
14+
const startWallTimeNs = BigInt(Date.now()) * NS_PER_MS
15+
const startHrTime = process.hrtime.bigint()
16+
17+
const isoTimeNano = () => {
18+
const elapsedNs = process.hrtime.bigint() - startHrTime
19+
const currentTimeNs = startWallTimeNs + elapsedNs
20+
21+
const secondsSinceEpoch = currentTimeNs / NS_PER_SEC
22+
const nanosWithinSecond = currentTimeNs % NS_PER_SEC
23+
24+
const msSinceEpoch = Number(secondsSinceEpoch * 1000n + nanosWithinSecond / 1_000_000n)
25+
const date = new Date(msSinceEpoch)
26+
27+
const year = date.getUTCFullYear()
28+
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0')
29+
const day = date.getUTCDate().toString().padStart(2, '0')
30+
const hours = date.getUTCHours().toString().padStart(2, '0')
31+
const minutes = date.getUTCMinutes().toString().padStart(2, '0')
32+
const seconds = date.getUTCSeconds().toString().padStart(2, '0')
33+
34+
return `,"time":"${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${nanosWithinSecond
35+
.toString()
36+
.padStart(9, '0')}Z"`
37+
}
38+
39+
module.exports = { nullTime, epochTime, unixTime, isoTime, isoTimeNano }

pino.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,10 @@ declare namespace pino {
819819
* Returns ISO 8601-formatted time in UTC
820820
*/
821821
isoTime: TimeFn;
822+
/*
823+
* Returns RFC 3339-formatted time in UTC
824+
*/
825+
isoTimeNano: TimeFn;
822826
};
823827

824828
//// Exported functions

test/timestamp-nano.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict'
2+
3+
/* eslint no-prototype-builtins: 0 */
4+
5+
const { test } = require('tap')
6+
const { sink, once } = require('./helper')
7+
8+
test('pino.stdTimeFunctions.isoTimeNano returns RFC 3339 timestamps', async ({ equal }) => {
9+
// Mock Date.now at module initialization time
10+
const now = Date.now
11+
Date.now = () => new Date('2025-08-01T15:03:45.000000000Z').getTime()
12+
13+
// Mock process.hrtime.bigint at module initialization time
14+
const hrTimeBigint = process.hrtime.bigint
15+
process.hrtime.bigint = () => 100000000000000n
16+
17+
const pino = require('../')
18+
19+
const opts = {
20+
timestamp: pino.stdTimeFunctions.isoTimeNano
21+
}
22+
const stream = sink()
23+
24+
// Mock process.hrtime.bigint at invocation time, add 1 day to the timestamp
25+
process.hrtime.bigint = () => 100000000000000n + 86400012345678n
26+
27+
const instance = pino(opts, stream)
28+
instance.info('foobar')
29+
const result = await once(stream, 'data')
30+
equal(result.hasOwnProperty('time'), true)
31+
equal(result.time, '2025-08-02T15:03:45.012345678Z')
32+
33+
Date.now = now
34+
process.hrtime.bigint = hrTimeBigint
35+
})

test/timestamp.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ test('pino exposes standard time functions', async ({ ok }) => {
1212
ok(pino.stdTimeFunctions.unixTime)
1313
ok(pino.stdTimeFunctions.nullTime)
1414
ok(pino.stdTimeFunctions.isoTime)
15+
ok(pino.stdTimeFunctions.isoTimeNano)
1516
})
1617

1718
test('pino accepts external time functions', async ({ equal }) => {

test/types/pino-top-export.test-d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ expectType<LevelMapping>(levels);
2222
expectType<MultiStreamRes>(multistream(process.stdout));
2323
expectType<SerializedError>(stdSerializers.err({} as any));
2424
expectType<string>(stdTimeFunctions.isoTime());
25+
expectType<string>(stdTimeFunctions.isoTimeNano());
2526
expectType<string>(version);
2627

2728
// Can't test against `unique symbol`, see https://github.com/SamVerschueren/tsd/issues/49

test/types/pino.test-d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@ const withTimeFn = pino({
273273
timestamp: pino.stdTimeFunctions.isoTime,
274274
});
275275

276+
const withRFC3339TimeFn = pino({
277+
timestamp: pino.stdTimeFunctions.isoTimeNano,
278+
});
279+
276280
const withNestedKey = pino({
277281
nestedKey: "payload",
278282
});

0 commit comments

Comments
 (0)