Skip to content

Commit bb151e5

Browse files
CLDSRV-839: Add BATCH.DELETE.OBJECT lines
Each key in a multi object delete has a line in logs This line should be filtered out of access_logs for cloudserver aggregated table in s3 analytics
1 parent ec55c0e commit bb151e5

3 files changed

Lines changed: 155 additions & 40 deletions

File tree

lib/api/multiObjectDelete.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const versionIdUtils = versioning.VersionID;
3131
const { data } = require('../data/wrapper');
3232
const logger = require('../utilities/logger');
3333
const { processBytesToWrite, validateQuotas } = require('./apiUtils/quotas/quotaUtils');
34+
const { logBatchDeleteObject } = require('../utilities/serverAccessLogger');
3435

3536
/*
3637
Format of xml request:
@@ -371,14 +372,24 @@ function getObjMetadataAndDelete(authInfo, canonicalID, request,
371372
callback(err, objMD, deleteInfo, result.versionId));
372373
},
373374
], (err, objMD, deleteInfo, versionId) => {
375+
const requestID = log.getSerializedUids();
376+
374377
if (err === skipError) {
378+
// Object doesn't exist - log without object size (AWS behavior)
379+
logBatchDeleteObject(request, requestID, entry.key, null, null);
375380
return moveOn();
376381
} else if (err === objectLockedError) {
377382
errorResults.push({ entry, error: errors.AccessDenied, objectLocked: true });
383+
// Log locked object with size if available
384+
const objectSize = objMD && objMD['content-length'] ? objMD['content-length'] : null;
385+
logBatchDeleteObject(request, requestID, entry.key, objectSize, errors.AccessDenied);
378386
return moveOn();
379387
} else if (err) {
380388
log.error('error deleting object', { error: err, entry });
381389
errorResults.push({ entry, error: err });
390+
// Log error case with size if available
391+
const objectSize = objMD && objMD['content-length'] ? objMD['content-length'] : null;
392+
logBatchDeleteObject(request, requestID, entry.key, objectSize, err);
382393
return moveOn();
383394
}
384395
if (deleteInfo.deleted && objMD['content-length']) {
@@ -408,6 +419,9 @@ function getObjMetadataAndDelete(authInfo, canonicalID, request,
408419
entry, isDeleteMarker,
409420
deleteMarkerVersionId,
410421
});
422+
// Log successful deletion with object size
423+
const objectSize = objMD && objMD['content-length'] ? objMD['content-length'] : null;
424+
logBatchDeleteObject(request, requestID, entry.key, objectSize, null);
411425
return moveOn();
412426
});
413427
},

lib/utilities/serverAccessLogger.js

Lines changed: 113 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -395,69 +395,65 @@ function timestampToDateTime643(unixMS) {
395395
return (unixMS / 1000).toFixed(3);
396396
}
397397

398-
function logServerAccess(req, res) {
399-
if (!req.serverAccessLog || !res.serverAccessLog || !serverAccessLogger) {
400-
return;
401-
}
402-
403-
const params = req.serverAccessLog;
404-
const errorCode = res.serverAccessLog.errorCode;
405-
const endTurnAroundTime = res.serverAccessLog.endTurnAroundTime;
406-
const requestID = res.serverAccessLog.requestID;
407-
const bytesSent = res.serverAccessLog.bytesSent;
398+
/**
399+
* Builds a log entry object with common fields
400+
* @param {object} req - HTTP request object
401+
* @param {object} params - Server access log parameters from req.serverAccessLog
402+
* @param {object} options - Variable fields for the log entry
403+
* @return {object} Log entry object
404+
*/
405+
function buildLogEntry(req, params, options) {
408406
const authInfo = params.authInfo;
409407

410-
serverAccessLogger.write(`${JSON.stringify({
408+
return {
411409
// Werelog fields
412410
// logs.access_logs_ingest.timestamp in Clickhouse is of type DateTime so it expects seconds as an integer.
413411
time: Math.floor(Date.now() / 1000),
414412
hostname,
415413
pid: process.pid,
416414

417415
// Analytics
418-
action: params.analyticsAction ?? undefined,
416+
action: options.action ?? undefined,
419417
accountName: params.analyticsAccountName ?? undefined,
420418
userName: params.analyticsUserName ?? undefined,
421-
httpMethod: req.method ?? undefined,
422-
bytesDeleted: params.analyticsBytesDeleted ?? undefined,
423-
bytesReceived: Number.isInteger(req.parsedContentLength) ? req.parsedContentLength : undefined,
424-
bodyLength: req.headers['content-length'] !== undefined
425-
? parseInt(req.headers['content-length'], 10)
426-
: undefined,
427-
contentLength: getObjectSize(req, res) ?? undefined,
419+
httpMethod: options.httpMethod ?? undefined,
420+
bytesDeleted: options.bytesDeleted ?? undefined,
421+
bytesReceived: options.bytesReceived ?? undefined,
422+
bodyLength: options.bodyLength ?? undefined,
423+
contentLength: options.contentLength ?? undefined,
428424
// eslint-disable-next-line camelcase
429-
elapsed_ms: calculateElapsedMS(params.startTime, params.onCloseEndTime) ?? undefined,
425+
elapsed_ms: options.elapsed_ms ?? undefined,
430426

431427
// AWS access server logs fields https://docs.aws.amazon.com/AmazonS3/latest/userguide/LogFormat.html
432428
startTime: timestampToDateTime643(params.startTimeUnixMS) ?? undefined, // AWS "Time" field
433429
requester: getRequester(authInfo) ?? undefined,
434-
operation: getOperation(req),
435-
requestURI: getURI(req) ?? undefined,
436-
errorCode: errorCode ?? undefined,
437-
objectSize: params.objectSize ?? getObjectSize(req, res) ?? undefined,
438-
totalTime: calculateTotalTime(params.startTime, params.onFinishEndTime) ?? undefined,
439-
turnAroundTime: calculateTurnAroundTime(params.startTurnAroundTime, endTurnAroundTime) ?? undefined,
440-
referer: req.headers.referer ?? undefined,
441-
userAgent: req.headers['user-agent'] ?? undefined,
442-
versionID: req.query?.versionId ?? undefined,
430+
operation: options.operation ?? undefined,
431+
requestURI: options.requestURI ?? undefined,
432+
errorCode: options.errorCode ?? undefined,
433+
objectSize: options.objectSize ?? undefined,
434+
totalTime: options.totalTime ?? undefined,
435+
turnAroundTime: options.turnAroundTime ?? undefined,
436+
referer: req.headers?.referer ?? undefined,
437+
userAgent: req.headers?.['user-agent'] ?? undefined,
438+
versionID: options.versionID ?? undefined,
443439
signatureVersion: authInfo?.getAuthVersion() ?? undefined,
444-
cipherSuite: req.socket.encrypted ? req.socket.getCipher()['standardName'] : undefined,
440+
cipherSuite: req.socket?.encrypted ? req.socket.getCipher()['standardName'] : undefined,
445441
authenticationType: authInfo?.getAuthType() ?? undefined,
446-
hostHeader: req.headers.host ?? undefined,
447-
tlsVersion: req.socket.encrypted ? req.socket.getCipher()['version'] : undefined,
448-
aclRequired: undefined, // TODO: CLDSRV-774
442+
hostHeader: req.headers?.host ?? undefined,
443+
tlsVersion: req.socket?.encrypted ? req.socket.getCipher()['version'] : undefined,
444+
aclRequired: options.aclRequired ?? undefined, // TODO: CLDSRV-774
449445
// hostID: undefined, // NOT IMPLEMENTED
450446
// accessPointARN: undefined, // NOT IMPLEMENTED
451447

452448
// Shared between AWS access server logs and Analytics logs
453449
bucketOwner: params.bucketOwner ?? undefined,
454450
bucketName: params.bucketName ?? undefined, // AWS "Bucket" field
455451
// eslint-disable-next-line camelcase
456-
req_id: requestID ?? undefined, // AWS "Request ID" field
457-
bytesSent: getBytesSent(res, bytesSent) ?? undefined,
452+
req_id: options.requestID ?? undefined, // AWS "Request ID" field
453+
bytesSent: options.bytesSent ?? undefined,
458454
clientIP: getRemoteIPFromRequest(req) ?? undefined, // AWS 'Remote IP' field
459-
httpCode: res.statusCode ?? undefined, // AWS "HTTP Status" field
460-
objectKey: params.objectKey ?? undefined, // AWS "Key" field
455+
httpCode: options.httpCode ?? undefined, // AWS "HTTP Status" field
456+
objectKey: options.objectKey ?? undefined, // AWS "Key" field
461457

462458
// Scality server access logs extra fields
463459
logFormatVersion: SERVER_ACCESS_LOG_FORMAT_VERSION,
@@ -472,11 +468,90 @@ function logServerAccess(req, res) {
472468
// Rate Limiting fields (only present when rate limited)
473469
rateLimited: params.rateLimited ?? undefined,
474470
rateLimitSource: params.rateLimitSource ?? undefined,
475-
})}\n`);
471+
};
472+
}
473+
474+
/**
475+
* Logs a BATCH.DELETE.OBJECT entry for individual object deletions in multi-object delete
476+
* @param {object} req - HTTP request object
477+
* @param {string} requestID - Request ID from the response
478+
* @param {string} objectKey - Key of the object being deleted
479+
* @param {number|null} objectSize - Size of the object, or null if object doesn't exist
480+
* @param {Error|null} error - Error object if deletion failed, null if successful
481+
* @return {undefined}
482+
*/
483+
function logBatchDeleteObject(req, requestID, objectKey, objectSize, error) {
484+
if (!req.serverAccessLog || !serverAccessLogger) {
485+
return;
486+
}
487+
488+
const params = req.serverAccessLog;
489+
let httpCode = 204;
490+
let errorCode = undefined;
491+
492+
if (error) {
493+
httpCode = error.code ? parseInt(error.code, 10) : 500;
494+
errorCode = error.message || 'InternalError';
495+
}
496+
497+
const logEntry = buildLogEntry(req, params, {
498+
action: 'DeleteObject',
499+
httpMethod: 'POST',
500+
bytesDeleted: objectSize,
501+
contentLength: objectSize,
502+
operation: 'BATCH.DELETE.OBJECT',
503+
objectSize,
504+
httpCode,
505+
errorCode,
506+
objectKey,
507+
requestID,
508+
});
509+
510+
serverAccessLogger.write(`${JSON.stringify(logEntry)}\n`);
511+
}
512+
513+
function logServerAccess(req, res) {
514+
if (!req.serverAccessLog || !res.serverAccessLog || !serverAccessLogger) {
515+
return;
516+
}
517+
518+
const params = req.serverAccessLog;
519+
const errorCode = res.serverAccessLog.errorCode;
520+
const endTurnAroundTime = res.serverAccessLog.endTurnAroundTime;
521+
const requestID = res.serverAccessLog.requestID;
522+
const bytesSent = res.serverAccessLog.bytesSent;
523+
524+
const logEntry = buildLogEntry(req, params, {
525+
action: params.analyticsAction,
526+
httpMethod: req.method,
527+
bytesDeleted: params.analyticsBytesDeleted,
528+
bytesReceived: Number.isInteger(req.parsedContentLength) ? req.parsedContentLength : undefined,
529+
bodyLength: req.headers['content-length'] !== undefined
530+
? parseInt(req.headers['content-length'], 10)
531+
: undefined,
532+
contentLength: getObjectSize(req, res),
533+
// eslint-disable-next-line camelcase
534+
elapsed_ms: calculateElapsedMS(params.startTime, params.onCloseEndTime),
535+
operation: getOperation(req),
536+
requestURI: getURI(req),
537+
errorCode,
538+
objectSize: params.objectSize ?? getObjectSize(req, res),
539+
totalTime: calculateTotalTime(params.startTime, params.onFinishEndTime),
540+
turnAroundTime: calculateTurnAroundTime(params.startTurnAroundTime, endTurnAroundTime),
541+
versionID: req.query?.versionId,
542+
aclRequired: undefined, // TODO: CLDSRV-774
543+
requestID,
544+
bytesSent: getBytesSent(res, bytesSent),
545+
httpCode: res.statusCode,
546+
objectKey: params.objectKey,
547+
});
548+
549+
serverAccessLogger.write(`${JSON.stringify(logEntry)}\n`);
476550
}
477551

478552
module.exports = {
479553
logServerAccess,
554+
logBatchDeleteObject,
480555
setServerAccessLogger,
481556
ServerAccessLogger,
482557

tests/functional/aws-node-sdk/test/serverAccessLogs/testServerAccessLogFile.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,7 +1669,7 @@ describe('Server Access Logs - File Output', async () => {
16691669
};
16701670
})(),
16711671
(() => {
1672-
// This operation tests deleting multiple objects.
1672+
// This operation tests deleting multiple objects including a non-existent one.
16731673
const method = async () => {
16741674
await s3.createBucket({ Bucket: bucketName }).promise();
16751675
await s3.putObject({ Bucket: bucketName, Key: objectKey, Body: 'test data' }).promise();
@@ -1679,7 +1679,8 @@ describe('Server Access Logs - File Output', async () => {
16791679
Delete: {
16801680
Objects: [
16811681
{ Key: objectKey },
1682-
{ Key: `${objectKey}2` }
1682+
{ Key: `${objectKey}2` },
1683+
{ Key: `${objectKey}-non-existent` }
16831684
]
16841685
}
16851686
}).promise();
@@ -1709,11 +1710,36 @@ describe('Server Access Logs - File Output', async () => {
17091710
objectKey: `${objectKey}2`,
17101711
httpMethod: 'PUT',
17111712
},
1713+
{
1714+
...commonProperties,
1715+
operation: 'BATCH.DELETE.OBJECT',
1716+
action: 'DeleteObject',
1717+
objectKey,
1718+
httpCode: 204,
1719+
httpMethod: 'POST',
1720+
},
1721+
{
1722+
...commonProperties,
1723+
operation: 'BATCH.DELETE.OBJECT',
1724+
action: 'DeleteObject',
1725+
objectKey: `${objectKey}2`,
1726+
httpCode: 204,
1727+
httpMethod: 'POST',
1728+
},
1729+
{
1730+
...commonProperties,
1731+
operation: 'BATCH.DELETE.OBJECT',
1732+
action: 'DeleteObject',
1733+
objectKey: `${objectKey}-non-existent`,
1734+
httpCode: 204,
1735+
httpMethod: 'POST',
1736+
},
17121737
{
17131738
...commonProperties,
17141739
operation: 'REST.POST.MULTI_OBJECT_DELETE',
17151740
action: 'DeleteObjects',
17161741
httpMethod: 'POST',
1742+
objectKey: null,
17171743
}
17181744
],
17191745
};

0 commit comments

Comments
 (0)