diff --git a/lib/utilities/serverAccessLogger.js b/lib/utilities/serverAccessLogger.js index c6006bc4b9..1fa37619d9 100644 --- a/lib/utilities/serverAccessLogger.js +++ b/lib/utilities/serverAccessLogger.js @@ -386,6 +386,15 @@ function calculateElapsedMS(startTime, onCloseEndTime) { return Number(onCloseEndTime - startTime) / 1_000_000; } +function timestampToDateTime643(unixMS) { + if (unixMS === undefined || unixMS === null) { + return null; + } + + // clickhouse DateTime64(3) expects "seconds.milliseconds" (string type). + return (unixMS / 1000).toFixed(3); +} + function logServerAccess(req, res) { if (!req.serverAccessLog || !res.serverAccessLog || !serverAccessLogger) { return; @@ -420,7 +429,7 @@ function logServerAccess(req, res) { elapsed_ms: calculateElapsedMS(params.startTime, params.onCloseEndTime) ?? undefined, // AWS access server logs fields https://docs.aws.amazon.com/AmazonS3/latest/userguide/LogFormat.html - startTime: params.startTimeUnixMS ?? undefined, // AWS "Time" field - milliseconds since epoch + startTime: timestampToDateTime643(params.startTimeUnixMS) ?? undefined, // AWS "Time" field requester: getRequester(authInfo) ?? undefined, operation: getOperation(req), requestURI: getURI(req) ?? undefined, @@ -480,4 +489,5 @@ module.exports = { getBytesSent, calculateTotalTime, calculateTurnAroundTime, + timestampToDateTime643, }; diff --git a/package.json b/package.json index 5a96b61848..3e3da3b6eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenko/cloudserver", - "version": "9.2.19", + "version": "9.2.20", "description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol", "main": "index.js", "engines": { diff --git a/schema/server_access_log.schema.json b/schema/server_access_log.schema.json index e9f3c208d3..8e7a1a4717 100644 --- a/schema/server_access_log.schema.json +++ b/schema/server_access_log.schema.json @@ -62,8 +62,8 @@ "minimum": 0 }, "startTime": { - "description": "Milliseconds since Unix epoch, recorded when the server first routes the request. Represents the AWS server access log 'Time' field.", - "type": "number" + "description": "Timestamp formatted as: 'seconds.milliseconds', recorded when the server first routes the request. Represents the AWS server access log 'Time' field. String type compatible with Clickhouse DateTime64(3) type.", + "type": "string" }, "requester": { "description": "AWS server access log 'Requester' field. From AWS 'The canonical user ID of the requester, or a - for unauthenticated requests. If the requester was an IAM user, this field returns the requester's IAM user name along with the AWS account that the IAM user belongs to. This identifier is the same one used for access control purposes.'. We don't use null instead of '-' when the requester is missing.", diff --git a/tests/unit/utils/serverAccessLogger.js b/tests/unit/utils/serverAccessLogger.js index 0e42912377..642f55ba57 100644 --- a/tests/unit/utils/serverAccessLogger.js +++ b/tests/unit/utils/serverAccessLogger.js @@ -11,6 +11,7 @@ const { getBytesSent, calculateTotalTime, calculateTurnAroundTime, + timestampToDateTime643, } = require('../../../lib/utilities/serverAccessLogger'); describe('serverAccessLogger utility functions', () => { @@ -679,6 +680,48 @@ describe('serverAccessLogger utility functions', () => { }); }); + describe('timestampToDateTime643', () => { + it('should convert milliseconds to seconds with 3 decimal places', () => { + const startTimeUnixMS = 1234567890000; + const result = timestampToDateTime643(startTimeUnixMS); + assert.strictEqual(result, '1234567890.000'); + }); + + it('should handle timestamp with milliseconds', () => { + const startTimeUnixMS = 1234567890123; + const result = timestampToDateTime643(startTimeUnixMS); + assert.strictEqual(result, '1234567890.123'); + }); + + it('should handle 0', () => { + const startTimeUnixMS = 0; + const result = timestampToDateTime643(startTimeUnixMS); + assert.strictEqual(result, '0.000'); + }); + + it('should return null when startTimeUnixMS is null', () => { + const result = timestampToDateTime643(null); + assert.strictEqual(result, null); + }); + + it('should return null when startTimeUnixMS is undefined', () => { + const result = timestampToDateTime643(undefined); + assert.strictEqual(result, null); + }); + + it('should handle small timestamps', () => { + const startTimeUnixMS = 1000; + const result = timestampToDateTime643(startTimeUnixMS); + assert.strictEqual(result, '1.000'); + }); + + it('should handle timestamps with partial milliseconds', () => { + const startTimeUnixMS = 1500; + const result = timestampToDateTime643(startTimeUnixMS); + assert.strictEqual(result, '1.500'); + }); + }); + describe('logServerAccess', () => { let mockLogger; let sandbox; @@ -831,7 +874,7 @@ describe('serverAccessLogger utility functions', () => { assert.strictEqual(loggedData.elapsed_ms, 20.5); // Verify AWS access server log fields - assert.strictEqual(loggedData.startTime, 1234567890000); + assert.strictEqual(loggedData.startTime, '1234567890.000'); assert.strictEqual(loggedData.requester, 'canonical123'); assert.strictEqual(loggedData.operation, 'REST.GET.OBJECT'); assert.strictEqual(loggedData.requestURI, 'GET /test-bucket/test-key.txt HTTP/1.1');