|
| 1 | +const { Werelogs } = require('werelogs'); |
| 2 | +const { config } = require('../Config'); |
| 3 | +const fs = require('fs'); |
| 4 | +const path = require('path'); |
| 5 | + |
| 6 | +const DEFAULT_OUTPUT_FILE = './logs/api-operations.log'; |
| 7 | + |
| 8 | +function createServerAccessLogger() { |
| 9 | + if (!config.serverAccessLogs || !config.serverAccessLogs.enabled) { |
| 10 | + console.warn("ServerAccessLogs disabled returning no-op logger"); |
| 11 | + return { |
| 12 | + info: () => { }, |
| 13 | + debug: () => { }, |
| 14 | + warn: () => { }, |
| 15 | + error: () => { }, |
| 16 | + trace: () => { }, |
| 17 | + fatal: () => { }, |
| 18 | + }; |
| 19 | + } |
| 20 | + |
| 21 | + // Ensure logs directory exists |
| 22 | + const outputFile = config.serverAccessLogs.outputFile || DEFAULT_OUTPUT_FILE; |
| 23 | + const logDir = path.dirname(outputFile); |
| 24 | + |
| 25 | + try { |
| 26 | + if (!fs.existsSync(logDir)) { |
| 27 | + fs.mkdirSync(logDir, { recursive: true }); |
| 28 | + } |
| 29 | + } catch (error) { |
| 30 | + // Fall back to console-only logging if directory creation fails |
| 31 | + console.warn('Failed to create ServerAccess log directory, falling back to console logging:', error.message); |
| 32 | + |
| 33 | + let apiWerelogs = new Werelogs({ |
| 34 | + level: config.serverAccessLogs.logLevel || 'info', |
| 35 | + dump: config.serverAccessLogs.dumpLevel || 'error', |
| 36 | + streams: [ |
| 37 | + { level: 'trace', stream: process.stdout } |
| 38 | + ] |
| 39 | + }); |
| 40 | + |
| 41 | + return new apiWerelogs.Logger('ServerAccessLogger'); |
| 42 | + } |
| 43 | + |
| 44 | + // Create file stream for API logs |
| 45 | + const serverAccessLogStream = fs.createWriteStream(outputFile, { flags: 'a' }); |
| 46 | + |
| 47 | + // Handle stream errors |
| 48 | + serverAccessLogStream.on('error', error => { |
| 49 | + console.error('ServerAccessLogger log file stream error:', error); |
| 50 | + }); |
| 51 | + |
| 52 | + // Create the API-specific Werelogs instance - file output only |
| 53 | + apiWerelogs = new Werelogs({ |
| 54 | + level: config.serverAccessLogs.logLevel || 'info', |
| 55 | + dump: config.serverAccessLogs.dumpLevel || 'error', |
| 56 | + streams: [{ level: 'trace', stream: serverAccessLogStream }] |
| 57 | + }); |
| 58 | + console.info("ServerAccessLogger created successfully"); |
| 59 | + return new apiWerelogs.Logger('ServerAccessLogger'); |
| 60 | +} |
| 61 | + |
| 62 | +var serverAccessLogger = { |
| 63 | + info: () => { }, |
| 64 | + debug: () => { }, |
| 65 | + warn: () => { }, |
| 66 | + error: () => { }, |
| 67 | + trace: () => { }, |
| 68 | + fatal: () => { }, |
| 69 | +}; |
| 70 | + |
| 71 | + |
| 72 | +try { |
| 73 | + serverAccessLogger = createServerAccessLogger(); |
| 74 | +} catch (error) { |
| 75 | + console.error('Failed to create ServiceAccessLogger, using no-op logger:', error); |
| 76 | +} |
| 77 | + |
| 78 | +function getRemoteIPFromRequest(request) { |
| 79 | + let remoteIP = '-'; |
| 80 | + if (request.headers) { |
| 81 | + // Check for forwarded IP headers (proxy/load balancer scenarios) |
| 82 | + remoteIP = request.headers['x-forwarded-for'] || |
| 83 | + request.headers['x-real-ip'] || |
| 84 | + request.headers['x-client-ip'] || |
| 85 | + request.headers['cf-connecting-ip']; // Cloudflare |
| 86 | + |
| 87 | + // x-forwarded-for can contain multiple IPs, take the first one |
| 88 | + if (remoteIP && remoteIP.includes(',')) { |
| 89 | + remoteIP = remoteIP.split(',')[0].trim(); |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + // Fallback to connection remote address if no forwarded headers |
| 94 | + if (!remoteIP || remoteIP === '-') { |
| 95 | + remoteIP = (request.connection && request.connection.remoteAddress) || |
| 96 | + (request.socket && request.socket.remoteAddress) || |
| 97 | + (request.ip) || |
| 98 | + '-'; |
| 99 | + } |
| 100 | + |
| 101 | + return remoteIP; |
| 102 | +} |
| 103 | + |
| 104 | +function getOperation(req) { |
| 105 | + const methodToResType = Object.freeze({ |
| 106 | + 'bucketDelete': 'BUCKET', |
| 107 | + 'bucketDeleteCors': 'BUCKET', |
| 108 | + 'bucketDeleteEncryption': 'BUCKET', |
| 109 | + 'bucketDeleteWebsite': 'BUCKET', |
| 110 | + 'bucketGet': 'BUCKET', |
| 111 | + 'bucketGetACL': 'BUCKET', |
| 112 | + 'bucketGetCors': 'BUCKET', |
| 113 | + 'bucketGetObjectLock': 'BUCKET', |
| 114 | + 'bucketGetVersioning': 'VERSIONING', |
| 115 | + 'bucketGetWebsite': 'BUCKET', |
| 116 | + 'bucketGetLocation': 'BUCKET', |
| 117 | + 'bucketGetEncryption': 'BUCKET', |
| 118 | + 'bucketHead': 'BUCKET', |
| 119 | + 'bucketPut': 'BUCKET', |
| 120 | + 'bucketPutACL': 'BUCKET', |
| 121 | + 'bucketPutCors': 'BUCKET', |
| 122 | + 'bucketPutVersioning': 'VERSIONING', |
| 123 | + 'bucketPutTagging': 'BUCKET', |
| 124 | + 'bucketDeleteTagging': 'BUCKET', |
| 125 | + 'bucketGetTagging': 'BUCKET', |
| 126 | + 'bucketPutWebsite': 'BUCKET', |
| 127 | + 'bucketPutReplication': 'BUCKET', |
| 128 | + 'bucketGetReplication': 'BUCKET', |
| 129 | + 'bucketDeleteReplication': 'BUCKET', |
| 130 | + 'bucketDeleteQuota': 'BUCKET', |
| 131 | + 'bucketPutLifecycle': 'BUCKET', |
| 132 | + 'bucketUpdateQuota': 'BUCKET', |
| 133 | + 'bucketGetLifecycle': 'BUCKET', |
| 134 | + 'bucketDeleteLifecycle': 'BUCKET', |
| 135 | + 'bucketPutPolicy': 'BUCKETPOLICY', |
| 136 | + 'bucketGetPolicy': 'BUCKETPOLICY', |
| 137 | + 'bucketGetQuota': 'BUCKET', |
| 138 | + 'bucketDeletePolicy': 'BUCKETPOLICY', |
| 139 | + 'bucketPutObjectLock': 'BUCKET', |
| 140 | + 'bucketPutNotification': 'BUCKET', |
| 141 | + 'bucketGetNotification': 'BUCKET', |
| 142 | + 'bucketPutEncryption': 'BUCKET', |
| 143 | + 'bucketPutLogging': 'LOGGING_STATUS', |
| 144 | + 'bucketGetLogging': 'LOGGING_STATUS', |
| 145 | + // 'corsPreflight': '', |
| 146 | + 'completeMultipartUpload': 'OBJECT', |
| 147 | + 'initiateMultipartUpload': 'OBJECT', |
| 148 | + 'listMultipartUploads': 'OBJECT', |
| 149 | + 'listParts': 'OBJECT', |
| 150 | + 'metadataSearch': 'OBJECT', |
| 151 | + 'multiObjectDelete': 'OBJECT', |
| 152 | + 'multipartDelete': 'OBJECT', |
| 153 | + 'objectDelete': 'OBJECT', |
| 154 | + 'objectDeleteTagging': 'OBJECT', |
| 155 | + 'objectGet': 'OBJECT', |
| 156 | + 'objectGetACL': 'OBJECT', |
| 157 | + 'objectGetLegalHold': 'OBJECT', |
| 158 | + 'objectGetRetention': 'OBJECT', |
| 159 | + 'objectGetTagging': 'OBJECT', |
| 160 | + 'objectCopy': 'OBJECT', |
| 161 | + 'objectHead': 'OBJECT', |
| 162 | + 'objectPut': 'OBJECT', |
| 163 | + 'objectPutACL': 'OBJECT', |
| 164 | + 'objectPutLegalHold': 'OBJECT', |
| 165 | + 'objectPutTagging': 'OBJECT', |
| 166 | + 'objectPutPart': 'OBJECT', |
| 167 | + 'objectPutCopyPart': 'OBJECT', |
| 168 | + 'objectPutRetention': 'OBJECT', |
| 169 | + 'objectRestore': 'OBJECT', |
| 170 | + // 'serviceGet': '', |
| 171 | + // 'websiteGet': '', |
| 172 | + // 'websiteHead': '', |
| 173 | + }); |
| 174 | + |
| 175 | + return `REST.${req.method}.${methodToResType[req.apiMethod] ? methodToResType[req.apiMethod] : 'UNKNOWN'}` |
| 176 | +} |
| 177 | + |
| 178 | +function getRequester(authInfo) { |
| 179 | + let requester = '-'; |
| 180 | + if (authInfo) { |
| 181 | + if (authInfo.isRequesterPublicUser && authInfo.isRequesterPublicUser()) { |
| 182 | + return requester; // Unauthenticated requests |
| 183 | + } else if (authInfo.isRequesterAnIAMUser && authInfo.isRequesterAnIAMUser()) { |
| 184 | + // IAM user: include IAM user name and account |
| 185 | + const iamUserName = authInfo.getIAMdisplayName ? authInfo.getIAMdisplayName() : ''; |
| 186 | + const accountName = authInfo.getAccountDisplayName ? authInfo.getAccountDisplayName() : ''; |
| 187 | + return iamUserName && accountName ? `${iamUserName}:${accountName}` : authInfo.getCanonicalID(); |
| 188 | + } else if (authInfo.getCanonicalID) { |
| 189 | + // Regular user: canonical user ID |
| 190 | + return authInfo.getCanonicalID(); |
| 191 | + } |
| 192 | + } |
| 193 | + return requester; |
| 194 | +} |
| 195 | + |
| 196 | +function getURI(request) { |
| 197 | + let requestURI = '-'; |
| 198 | + if (request) { |
| 199 | + const method = request.method || 'UNKNOWN'; |
| 200 | + const url = request.url || request.originalUrl || '/'; |
| 201 | + const httpVersion = request.httpVersion || '1.1'; |
| 202 | + requestURI = `${method} ${url} HTTP/${httpVersion}`; |
| 203 | + } |
| 204 | + return requestURI; |
| 205 | +} |
| 206 | + |
| 207 | +function getObjectSize(request) { |
| 208 | + const objectSizeMethods = Object.freeze({ |
| 209 | + 'objectPut': true, |
| 210 | + 'objectPutPart': true, |
| 211 | + 'objectGet': true, |
| 212 | + }); |
| 213 | + |
| 214 | + if (request && objectSizeMethods[request.apiMethod]) { |
| 215 | + const len = request.getHeader('Content-Length'); |
| 216 | + return len ? len : '-'; |
| 217 | + } |
| 218 | + |
| 219 | + return '-'; |
| 220 | +} |
| 221 | + |
| 222 | +function getBytesSent(res) { |
| 223 | + if (!res) { |
| 224 | + return '-'; |
| 225 | + } |
| 226 | + if (res.getHeader('Content-Length')) { |
| 227 | + return res.getHeader('Content-Length'); |
| 228 | + } |
| 229 | + return '-'; |
| 230 | +} |
| 231 | + |
| 232 | +function logServerAccess(params, req, res) { |
| 233 | + if (!params.enabled) { |
| 234 | + return; |
| 235 | + } |
| 236 | + |
| 237 | + console.log(params) |
| 238 | + |
| 239 | + serverAccessLogger.info('SERVER_ACCESS_LOG', { |
| 240 | + // AWS fields. |
| 241 | + bucketOwner: params.bucketOwner, |
| 242 | + bucket: params.bucketName, |
| 243 | + startTime: params.startTime.toString(), |
| 244 | + remoteIP: getRemoteIPFromRequest(req), |
| 245 | + requester: getRequester(params.authInfo), |
| 246 | + // // requestID: '-', // From wherelogs. |
| 247 | + operation: getOperation(req), |
| 248 | + requestURI: getURI(req), |
| 249 | + HTTPStatus: res.statusCode, |
| 250 | + errorCode: params.errorCode ? params.errorCode : '-', |
| 251 | + bytesSent: getBytesSent(res), |
| 252 | + objectSize: getObjectSize(req), |
| 253 | + totalTime: (Number(params.endTime - params.startTime) / 1_000_000).toString(), |
| 254 | + turnAroundTime: (Number(params.endTurnAroundTime - params.startTurnAroundTime) / 1_000_000).toString(), |
| 255 | + referer: req.headers.referer ? req.headers.referer : '-', |
| 256 | + userAgent: req.headers['user-agent'] ? req.headers['user-agent'] : '-', |
| 257 | + versionID: req.query.versionId ? req.query.versionId : '-', // query inserted by arsenal. |
| 258 | + hostID: '-', // NOT IMPLEMENTED |
| 259 | + signatureVersion: params.authInfo.getAuthVersion(), |
| 260 | + cipherSuite: req.socket.encrypted ? req.socket.getCipher()['standardName'] : '-', |
| 261 | + authenticationType: params.authInfo.getAuthType(), |
| 262 | + hostHeader: req.headers.host ? req.headers.host : '-', |
| 263 | + // From https://nodejs.org/api/tls.html#tlssocketgetcipher |
| 264 | + tlsVersion: req.socket.encrypted ? req.socket.getCipher()['version'] : '-', |
| 265 | + accessPointARN: '-', // NOT IMPLEMENTED |
| 266 | + aclRequired: '-', // ??? |
| 267 | + // Scality extra fields. |
| 268 | + logFormatVersion: '-', |
| 269 | + loggingEnabled: true, |
| 270 | + loggingTargetBucket: params.loggingEnabled.TargetBucket, |
| 271 | + loggingTargetPrefix: params.loggingEnabled.TargetPrefix, |
| 272 | + raftSessionID: '-', |
| 273 | + aws_access_key_id: params.authInfo.getAccessKey() ? params.authInfo.getAccessKey() : '-', |
| 274 | + }) |
| 275 | +} |
| 276 | + |
| 277 | +module.exports = { |
| 278 | + logServerAccess, |
| 279 | +}; |
0 commit comments