From d04ef52111df2aa773e248578e789b2fdc55d23c Mon Sep 17 00:00:00 2001 From: ajaz Date: Wed, 21 May 2025 17:15:18 +0530 Subject: [PATCH 01/11] feat: added new command to fetch error logs --- .../get-log-forwarding-errors.test.js | 222 ++++++++++++++++++ .../config/get/log-forwarding/errors.js | 190 +++++++++++++++ .../index.js} | 18 +- src/lib/smsClient.js | 49 ++++ 4 files changed, 474 insertions(+), 5 deletions(-) create mode 100644 src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js create mode 100644 src/commands/api-mesh/config/get/log-forwarding/errors.js rename src/commands/api-mesh/config/get/{log-forwarding.js => log-forwarding/index.js} (73%) diff --git a/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js b/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js new file mode 100644 index 00000000..06c04be2 --- /dev/null +++ b/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js @@ -0,0 +1,222 @@ +const fs = require('fs'); +const path = require('path'); +const GetLogForwardingErrorsCommand = require('../config/get/log-forwarding/errors'); +const { initSdk, promptConfirm } = require('../../../helpers'); +const { getMeshId, getLogForwardingErrors } = require('../../../lib/smsClient'); + +jest.mock('fs'); +jest.mock('axios'); +jest.mock('../../../helpers', () => ({ + initSdk: jest.fn().mockResolvedValue({}), + initRequestId: jest.fn().mockResolvedValue({}), + promptConfirm: jest.fn().mockResolvedValue(true), +})); +jest.mock('../../../lib/smsClient'); +jest.mock('../../../classes/logger'); + +describe('GetLogForwardingErrorsCommand', () => { + let parseSpy; + + beforeEach(() => { + const now = new Date(); + const startTime = new Date(now); + const endTime = new Date(now); + startTime.setMinutes(startTime.getMinutes() - 2); + const formattedStartTime = startTime.toISOString().slice(0, 19) + 'Z'; + const formattedEndTime = endTime.toISOString().slice(0, 19) + 'Z'; + + parseSpy = jest.spyOn(GetLogForwardingErrorsCommand.prototype, 'parse').mockResolvedValue({ + flags: { + startTime: formattedStartTime, + endTime: formattedEndTime, + filename: 'test.csv', + ignoreCache: false, + }, + }); + + initSdk.mockResolvedValue({ + imsOrgId: 'orgId', + imsOrgCode: 'orgCode', + projectId: 'projectId', + workspaceId: 'workspaceId', + workspaceName: 'workspaceName', + }); + getMeshId.mockResolvedValue('meshId'); + getLogForwardingErrors.mockResolvedValue({ + errorUrls: [{ key: 'error1.csv', url: 'http://example.com/someHash' }], + totalSize: 2048, + }); + promptConfirm.mockResolvedValue(true); + global.requestId = 'dummy_request_id'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('throws an error if the time difference between startTime and endTime is greater than 30 minutes', async () => { + fs.existsSync.mockReturnValue(true); + fs.statSync.mockReturnValue({ size: 0 }); + const now = new Date(); + const startTime = new Date(now); + const endTime = new Date(now); + startTime.setMinutes(startTime.getMinutes() - 45); + const formattedStartTime = startTime.toISOString().slice(0, 19) + 'Z'; + const formattedEndTime = endTime.toISOString().slice(0, 19) + 'Z'; + parseSpy.mockResolvedValueOnce({ + flags: { + startTime: formattedStartTime, + endTime: formattedEndTime, + filename: 'test.csv', + ignoreCache: false, + }, + }); + const command = new GetLogForwardingErrorsCommand([], {}); + await expect(command.run()).rejects.toThrow( + 'The maximum duration between startTime and endTime is 30 minutes. The current duration is 0 hours 45 minutes and 0 seconds.', + ); + }); + + test('throws an error if the endTime is greater than current time(now)', async () => { + fs.existsSync.mockReturnValue(true); + fs.statSync.mockReturnValue({ size: 0 }); + const now = new Date(); + const startTime = new Date(now); + const endTime = new Date(now); + endTime.setMinutes(startTime.getMinutes() + 45); + const formattedStartTime = startTime.toISOString().slice(0, 19) + 'Z'; + const formattedEndTime = endTime.toISOString().slice(0, 19) + 'Z'; + parseSpy.mockResolvedValueOnce({ + flags: { + startTime: formattedStartTime, + endTime: formattedEndTime, + filename: 'test.csv', + ignoreCache: false, + }, + }); + const command = new GetLogForwardingErrorsCommand([], {}); + await expect(command.run()).rejects.toThrow( + 'endTime cannot be in the future. Provide a valid endTime.', + ); + }); + + test('throws an error if startTime format is invalid', async () => { + parseSpy.mockResolvedValueOnce({ + flags: { + startTime: '20241213223832', + endTime: '2024-08-29T12:30:00Z', + filename: 'test.csv', + }, + }); + const command = new GetLogForwardingErrorsCommand([], {}); + const correctedStartTime = '2024-12-13T22:38:32Z'; + await expect(command.run()).rejects.toThrow( + `Use the format YYYY-MM-DDTHH:MM:SSZ for startTime. Did you mean ${correctedStartTime}?`, + ); + }); + + test('throws an error if endTime format is invalid', async () => { + parseSpy.mockResolvedValueOnce({ + flags: { + startTime: '2024-08-29T12:00:00Z', + endTime: '2024-08-29:23:45:56Z', + filename: 'test.csv', + }, + }); + const command = new GetLogForwardingErrorsCommand([], {}); + const correctedEndTime = '2024-08-29T23:45:56Z'; + await expect(command.run()).rejects.toThrow( + `Use the format YYYY-MM-DDTHH:MM:SSZ for endTime. Did you mean ${correctedEndTime}?`, + ); + }); + + test('throws an error if totalSize is 0', async () => { + fs.existsSync.mockReturnValue(true); + fs.statSync.mockReturnValue({ size: 0 }); + getLogForwardingErrors.mockResolvedValueOnce({ + errorUrls: [{ key: 'error1', url: 'http://example.com/error1' }], + totalSize: 0, + }); + const command = new GetLogForwardingErrorsCommand([], {}); + await expect(command.run()).rejects.toThrow('No log forwarding errors available to download'); + }); + + test('throws an error if errors are requested for a date older than 30 days', async () => { + const today = new Date(); + const thirtyDaysAgo = new Date(today); + thirtyDaysAgo.setUTCDate(today.getUTCDate() - 30); + const startTime = new Date(thirtyDaysAgo); + startTime.setUTCDate(thirtyDaysAgo.getUTCDate() - 1); + const formattedStartTime = startTime.toISOString().slice(0, 19) + 'Z'; + parseSpy.mockResolvedValueOnce({ + flags: { + startTime: formattedStartTime, + endTime: '2024-08-30T12:30:00Z', + filename: 'test.csv', + }, + }); + const command = new GetLogForwardingErrorsCommand([], {}); + await expect(command.run()).rejects.toThrow( + 'Cannot get logs more than 30 days old. Adjust your time range.', + ); + }); + + test('creates file if it does not exist and checks if file is empty before proceeding', async () => { + fs.existsSync.mockReturnValue(false); + fs.statSync.mockReturnValue({ size: 0 }); + const mockWriteStream = { + write: jest.fn(), + end: jest.fn(), + on: jest.fn((event, callback) => { + if (event === 'finish') { + callback(); + } + }), + }; + fs.createWriteStream.mockReturnValue(mockWriteStream); + const command = new GetLogForwardingErrorsCommand([], {}); + await command.run(); + expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(process.cwd(), 'test.csv')); + expect(fs.writeFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), 'test.csv'), ''); + expect(mockWriteStream.write).toHaveBeenCalled(); + }); + + test('throws an error if the file is not empty', async () => { + fs.existsSync.mockReturnValue(true); + fs.statSync.mockReturnValue({ size: 1024 }); + const command = new GetLogForwardingErrorsCommand([], {}); + await expect(command.run()).rejects.toThrow('Make sure the file: test.csv is empty'); + }); + + test('downloads errors if all conditions are met', async () => { + fs.existsSync.mockReturnValue(true); + fs.statSync.mockReturnValue({ size: 0 }); + const mockWriteStream = { + write: jest.fn(), + end: jest.fn(), + on: jest.fn((event, callback) => { + if (event === 'finish') { + callback(); + } + }), + }; + fs.createWriteStream.mockReturnValue(mockWriteStream); + const command = new GetLogForwardingErrorsCommand([], {}); + await command.run(); + expect(initSdk).toHaveBeenCalled(); + expect(getMeshId).toHaveBeenCalledWith('orgCode', 'projectId', 'workspaceId', 'workspaceName'); + expect(getLogForwardingErrors).toHaveBeenCalledWith( + 'orgCode', + 'projectId', + 'workspaceId', + 'meshId', + expect.any(String), + expect.any(String), + ); + expect(fs.createWriteStream).toHaveBeenCalledWith(path.resolve(process.cwd(), 'test.csv'), { + flags: 'a', + }); + expect(mockWriteStream.write).toHaveBeenCalled(); + expect(mockWriteStream.end).toHaveBeenCalled(); + }); +}); diff --git a/src/commands/api-mesh/config/get/log-forwarding/errors.js b/src/commands/api-mesh/config/get/log-forwarding/errors.js new file mode 100644 index 00000000..2d266baf --- /dev/null +++ b/src/commands/api-mesh/config/get/log-forwarding/errors.js @@ -0,0 +1,190 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const { Command } = require('@oclif/core'); +const path = require('path'); +const fs = require('fs'); +const { initSdk, promptConfirm, initRequestId } = require('../../../../../helpers'); +const { getMeshId, getLogForwardingErrors } = require('../../../../../lib/smsClient'); +const logger = require('../../../../../classes/logger'); +const axios = require('axios'); +const { + ignoreCacheFlag, + startTimeFlag, + endTimeFlag, + logFilenameFlag, + pastFlag, + suggestCorrectedDateFormat, + parsePastDuration, + validateDateTimeRange, + validateDateTimeFormat, +} = require('../../../../../utils'); + +require('dotenv').config(); + +class GetLogForwardingErrorsCommand extends Command { + static flags = { + ignoreCache: ignoreCacheFlag, + startTime: startTimeFlag, + endTime: endTimeFlag, + filename: logFilenameFlag, + past: pastFlag, + }; + + async run() { + await initRequestId(); + logger.info(`RequestId: ${global.requestId}`); + const { flags } = await this.parse(GetLogForwardingErrorsCommand); + const ignoreCache = await flags.ignoreCache; + const filename = await flags.filename; + let calculatedStartTime, calculatedEndTime, formattedStartTime, formattedEndTime; + + if (!filename || path.extname(filename).toLowerCase() !== '.csv') { + this.error('Invalid file type. Provide a filename with a .csv extension.'); + return; + } + + if (flags.startTime && flags.endTime) { + const dateTimeRegex = /^(?:(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T(0[0-9]|1[0-9]|2[0-3]):([0-5]\d):([0-5]\d)Z)$/; + if (!dateTimeRegex.test(flags.startTime)) { + const correctedStartTime = suggestCorrectedDateFormat(flags.startTime); + if (!correctedStartTime) { + this.error('Invalid date components in startTime. Correct the date.'); + } else { + this.error( + `Use the format YYYY-MM-DDTHH:MM:SSZ for startTime. Did you mean ${correctedStartTime}?`, + ); + } + return; + } + if (!dateTimeRegex.test(flags.endTime)) { + const correctedEndTime = suggestCorrectedDateFormat(flags.endTime); + if (!correctedEndTime) { + this.error('Found invalid date components for endTime. Check and correct the date.'); + } else { + this.error( + `Use the format YYYY-MM-DDTHH:MM:SSZ for endTime. Did you mean ${correctedEndTime}?`, + ); + } + return; + } + validateDateTimeRange(flags.startTime, flags.endTime); + formattedStartTime = flags.startTime.replace(/-|:|Z/g, '').replace('T', 'T'); + formattedEndTime = flags.endTime.replace(/-|:|Z/g, '').replace('T', 'T'); + } else if (flags.past) { + const pastTimeWindow = parsePastDuration(flags.past); + calculatedEndTime = new Date(); + calculatedStartTime = new Date(calculatedEndTime.getTime() - pastTimeWindow); + validateDateTimeRange(calculatedStartTime, calculatedEndTime); + formattedStartTime = validateDateTimeFormat(calculatedStartTime); + formattedEndTime = validateDateTimeFormat(calculatedEndTime); + } else if ((flags.startTime && !flags.endTime) || (!flags.startTime && flags.endTime)) { + this.error('Provide both startTime and endTime.'); + return; + } else { + this.error( + 'Missing required flags. Provide a time range with --startTime and --endTime flags, or use the --past flag for more recent errors. Use the `mesh config get log-forwarding errors --help` command for more information.', + ); + return; + } + + if (!filename) { + this.error('Missing filename. Provide a valid file in the current working directory.'); + return; + } + + const outputFile = path.resolve(process.cwd(), filename); + if (!fs.existsSync(outputFile)) { + fs.writeFileSync(outputFile, ''); + } + const stats = fs.statSync(outputFile); + if (stats.size > 0) { + throw new Error(`Make sure the file: ${filename} is empty`); + } + + logger.info('Calling initSdk...'); + const { imsOrgCode, projectId, workspaceId, workspaceName } = await initSdk({ ignoreCache }); + let meshId = null; + try { + meshId = await getMeshId(imsOrgCode, projectId, workspaceId, workspaceName); + } catch (err) { + this.error(`Unable to get mesh ID: ${err.message}.`); + } + if (!meshId) { + this.error('Mesh ID not found.'); + } + + const { errorUrls, totalSize } = await getLogForwardingErrors( + imsOrgCode, + projectId, + workspaceId, + meshId, + formattedStartTime, + formattedEndTime, + ); + if (!errorUrls || errorUrls.length === 0) { + this.error('No log forwarding errors found for the given time range.'); + } + + let shouldDownload = false; + if (totalSize > 0) { + const totalSizeKB = (totalSize / 1024).toFixed(2); + shouldDownload = await promptConfirm( + `The expected file size is ${totalSizeKB} KB. Confirm ${filename} download? (y/n)`, + ); + if (shouldDownload) { + const writer = fs.createWriteStream(outputFile, { flags: 'a' }); + const columnHeaders = + 'EventTimestampMs,ErrorType,ErrorMessage,MeshId,RayID,URL,Request Method,Response Status,Level'; + writer.write(`${columnHeaders}\n`); + for (const urlObj of errorUrls) { + const { key, url } = urlObj; + logger.info(`Downloading ${key} and appending to ${outputFile}...`); + try { + const fileContentStream = await this.downloadFileContent(url); + fileContentStream.pipe(writer, { end: false }); + await new Promise((resolve, reject) => { + fileContentStream.on('end', resolve); + fileContentStream.on('error', reject); + }); + logger.info(`${key} content appended successfully.`); + } catch (error) { + logger.error(`Error downloading or appending content of ${key}:`, error); + } + } + writer.end(); + this.log(`Successfully downloaded the log forwarding errors to ${filename}.`); + } else { + this.log('Log forwarding error files not downloaded.'); + } + } else { + this.error('No log forwarding errors available to download'); + } + } + + async downloadFileContent(url) { + return axios({ + method: 'get', + url: url, + responseType: 'stream', + }) + .then(response => response.data) + .catch(error => { + logger.error('Error downloading log forwarding error content:', error.message); + throw error; + }); + } +} + +GetLogForwardingErrorsCommand.description = + 'Download log forwarding errors for a selected time period.'; + +module.exports = GetLogForwardingErrorsCommand; diff --git a/src/commands/api-mesh/config/get/log-forwarding.js b/src/commands/api-mesh/config/get/log-forwarding/index.js similarity index 73% rename from src/commands/api-mesh/config/get/log-forwarding.js rename to src/commands/api-mesh/config/get/log-forwarding/index.js index dd925225..c0964001 100644 --- a/src/commands/api-mesh/config/get/log-forwarding.js +++ b/src/commands/api-mesh/config/get/log-forwarding/index.js @@ -10,10 +10,10 @@ governing permissions and limitations under the License. */ const { Command } = require('@oclif/core'); -const { initSdk, initRequestId } = require('../../../../helpers'); -const logger = require('../../../../classes/logger'); -const { ignoreCacheFlag, jsonFlag } = require('../../../../utils'); -const { getLogForwarding, getMeshId } = require('../../../../lib/smsClient'); +const { initSdk, initRequestId } = require('../../../../../helpers'); +const logger = require('../../../../../classes/logger'); +const { ignoreCacheFlag, jsonFlag } = require('../../../../../utils'); +const { getLogForwarding, getMeshId } = require('../../../../../lib/smsClient'); class GetLogForwardingCommand extends Command { static flags = { @@ -73,6 +73,14 @@ class GetLogForwardingCommand extends Command { } } -GetLogForwardingCommand.description = `Get log forwarding details for a given mesh`; +// GetLogForwardingCommand.description = `Get log forwarding details for a given mesh +// The 'get' command includes the following options: +// - log-forwarding : Retrieve log forwarding details for a given mesh. +// - log-forwarding:errors : Retrieve log forwarding error logs for a given mesh.`; +GetLogForwardingCommand.description = `Get log forwarding details and error logs for a given mesh. + +The 'log-forwarding' command includes the following options: +- log-forwarding : Retrieve log forwarding details for a given mesh. +- log-forwarding:errors : Download log forwarding error logs for a selected time period.`; module.exports = GetLogForwardingCommand; diff --git a/src/lib/smsClient.js b/src/lib/smsClient.js index 5ea90dbc..4a4a6abd 100644 --- a/src/lib/smsClient.js +++ b/src/lib/smsClient.js @@ -1497,6 +1497,54 @@ const deleteLogForwarding = async (organizationCode, projectId, workspaceId, mes } }; +/** + * Get log forwarding errors for a given mesh and time window. + * @param {string} organizationCode - The IMS org code + * @param {string} projectId - The project ID + * @param {string} workspaceId - The workspace ID + * @param {string} meshId - The mesh ID + * @param {string} startTime - Start time in UTC (YYYYMMDDTHHMMSS) + * @param {string} endTime - End time in UTC (YYYYMMDDTHHMMSS) + * @returns {Promise<{ errorUrls: Array<{ key: string, url: string }>, totalSize: number }>} + */ +const getLogForwardingErrors = async ( + organizationCode, + projectId, + workspaceId, + meshId, + startTime, + endTime, +) => { + const { accessToken } = await getDevConsoleConfig(); + const config = { + method: 'GET', + // url: `${SMS_BASE_URL}/organizations/${organizationCode}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}/log/forwarding/errors?startTime=${startTime}&endTime=${endTime}`, + url: `${SMS_BASE_URL}/organizations/${organizationCode}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}/logs?startDateTime=${startTime}&endDateTime=${endTime}`, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'x-request-id': global.requestId, + 'x-api-key': SMS_API_KEY, + }, + }; + logger.info('Initiating GET %s', config.url); + try { + const response = await axios(config); + logger.info('Response from GET %s', response.status); + if (response && response.status === 200 && response.data) { + return { + errorUrls: response.data.presignedUrls || [], + totalSize: response.data.totalSize || 0, + }; + } else { + logger.error('No log forwarding errors found for the given time range.'); + return { errorUrls: [], totalSize: 0 }; + } + } catch (error) { + logger.error('Error fetching log forwarding errors:', error.message); + throw new Error('Unable to fetch log forwarding errors.'); + } +}; + module.exports = { getApiKeyCredential, describeMesh, @@ -1521,4 +1569,5 @@ module.exports = { setLogForwarding, getLogForwarding, deleteLogForwarding, + getLogForwardingErrors, }; From 527fa53513530667be6e7cab846ff31ede69ab7b Mon Sep 17 00:00:00 2001 From: ajaz Date: Wed, 21 May 2025 17:20:29 +0530 Subject: [PATCH 02/11] fix: statements --- .../__tests__/get-log-forwarding-errors.test.js | 12 ++++++++++++ .../api-mesh/config/get/log-forwarding/index.js | 4 ---- src/lib/smsClient.js | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js b/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js index 06c04be2..67b5a498 100644 --- a/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js +++ b/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js @@ -1,3 +1,15 @@ +/* +Copyright 2021 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + const fs = require('fs'); const path = require('path'); const GetLogForwardingErrorsCommand = require('../config/get/log-forwarding/errors'); diff --git a/src/commands/api-mesh/config/get/log-forwarding/index.js b/src/commands/api-mesh/config/get/log-forwarding/index.js index c0964001..857f296c 100644 --- a/src/commands/api-mesh/config/get/log-forwarding/index.js +++ b/src/commands/api-mesh/config/get/log-forwarding/index.js @@ -73,10 +73,6 @@ class GetLogForwardingCommand extends Command { } } -// GetLogForwardingCommand.description = `Get log forwarding details for a given mesh -// The 'get' command includes the following options: -// - log-forwarding : Retrieve log forwarding details for a given mesh. -// - log-forwarding:errors : Retrieve log forwarding error logs for a given mesh.`; GetLogForwardingCommand.description = `Get log forwarding details and error logs for a given mesh. The 'log-forwarding' command includes the following options: diff --git a/src/lib/smsClient.js b/src/lib/smsClient.js index 4a4a6abd..dbbba189 100644 --- a/src/lib/smsClient.js +++ b/src/lib/smsClient.js @@ -1498,7 +1498,7 @@ const deleteLogForwarding = async (organizationCode, projectId, workspaceId, mes }; /** - * Get log forwarding errors for a given mesh and time window. + * Get log forwarding errors for a given mesh within a specified time range. * @param {string} organizationCode - The IMS org code * @param {string} projectId - The project ID * @param {string} workspaceId - The workspace ID From c26002461ea1431810fccd9be0e09a6a2526a53d Mon Sep 17 00:00:00 2001 From: Sumaiya <108254100+AjazSumaiya@users.noreply.github.com> Date: Thu, 22 May 2025 14:35:13 +0530 Subject: [PATCH 03/11] Apply suggestions from code review Co-authored-by: Jared Hoover <98363870+jhadobe@users.noreply.github.com> --- .../api-mesh/config/get/log-forwarding/errors.js | 14 +++++++------- .../api-mesh/config/get/log-forwarding/index.js | 2 +- src/lib/smsClient.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/commands/api-mesh/config/get/log-forwarding/errors.js b/src/commands/api-mesh/config/get/log-forwarding/errors.js index 2d266baf..b15d0289 100644 --- a/src/commands/api-mesh/config/get/log-forwarding/errors.js +++ b/src/commands/api-mesh/config/get/log-forwarding/errors.js @@ -57,7 +57,7 @@ class GetLogForwardingErrorsCommand extends Command { if (!dateTimeRegex.test(flags.startTime)) { const correctedStartTime = suggestCorrectedDateFormat(flags.startTime); if (!correctedStartTime) { - this.error('Invalid date components in startTime. Correct the date.'); + this.error('Invalid date components in the startTime. Correct the date and try again.'); } else { this.error( `Use the format YYYY-MM-DDTHH:MM:SSZ for startTime. Did you mean ${correctedStartTime}?`, @@ -68,7 +68,7 @@ class GetLogForwardingErrorsCommand extends Command { if (!dateTimeRegex.test(flags.endTime)) { const correctedEndTime = suggestCorrectedDateFormat(flags.endTime); if (!correctedEndTime) { - this.error('Found invalid date components for endTime. Check and correct the date.'); + this.error('Invalid date components in the endTime. Correct the date and try again.'); } else { this.error( `Use the format YYYY-MM-DDTHH:MM:SSZ for endTime. Did you mean ${correctedEndTime}?`, @@ -87,11 +87,11 @@ class GetLogForwardingErrorsCommand extends Command { formattedStartTime = validateDateTimeFormat(calculatedStartTime); formattedEndTime = validateDateTimeFormat(calculatedEndTime); } else if ((flags.startTime && !flags.endTime) || (!flags.startTime && flags.endTime)) { - this.error('Provide both startTime and endTime.'); + this.error('You must provide both a startTime and an endTime.'); return; } else { this.error( - 'Missing required flags. Provide a time range with --startTime and --endTime flags, or use the --past flag for more recent errors. Use the `mesh config get log-forwarding errors --help` command for more information.', + 'Missing required flags. Provide a time range with --startTime and --endTime flags, or use the --past flag for recent errors. Use the `mesh config get log-forwarding errors --help` command for more information.', ); return; } @@ -131,14 +131,14 @@ class GetLogForwardingErrorsCommand extends Command { formattedEndTime, ); if (!errorUrls || errorUrls.length === 0) { - this.error('No log forwarding errors found for the given time range.'); + this.error('No log forwarding errors found for the specified time range.'); } let shouldDownload = false; if (totalSize > 0) { const totalSizeKB = (totalSize / 1024).toFixed(2); shouldDownload = await promptConfirm( - `The expected file size is ${totalSizeKB} KB. Confirm ${filename} download? (y/n)`, + `The expected file size is ${totalSizeKB} KB. Do you want to download ${filename}? (y/n)`, ); if (shouldDownload) { const writer = fs.createWriteStream(outputFile, { flags: 'a' }); @@ -185,6 +185,6 @@ class GetLogForwardingErrorsCommand extends Command { } GetLogForwardingErrorsCommand.description = - 'Download log forwarding errors for a selected time period.'; + 'Download log forwarding errors for a specified time period.'; module.exports = GetLogForwardingErrorsCommand; diff --git a/src/commands/api-mesh/config/get/log-forwarding/index.js b/src/commands/api-mesh/config/get/log-forwarding/index.js index 857f296c..a314a1bf 100644 --- a/src/commands/api-mesh/config/get/log-forwarding/index.js +++ b/src/commands/api-mesh/config/get/log-forwarding/index.js @@ -73,7 +73,7 @@ class GetLogForwardingCommand extends Command { } } -GetLogForwardingCommand.description = `Get log forwarding details and error logs for a given mesh. +GetLogForwardingCommand.description = `Get log forwarding details and error logs for a specified mesh. The 'log-forwarding' command includes the following options: - log-forwarding : Retrieve log forwarding details for a given mesh. diff --git a/src/lib/smsClient.js b/src/lib/smsClient.js index dbbba189..24e39ed7 100644 --- a/src/lib/smsClient.js +++ b/src/lib/smsClient.js @@ -1536,7 +1536,7 @@ const getLogForwardingErrors = async ( totalSize: response.data.totalSize || 0, }; } else { - logger.error('No log forwarding errors found for the given time range.'); + logger.error('No log forwarding errors found for the specified time range.'); return { errorUrls: [], totalSize: 0 }; } } catch (error) { From 5d6af05d04f467b8d054691845e9309fd85acdc1 Mon Sep 17 00:00:00 2001 From: ajaz Date: Thu, 22 May 2025 17:09:28 +0530 Subject: [PATCH 04/11] chore: remove test url --- src/lib/smsClient.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/smsClient.js b/src/lib/smsClient.js index 24e39ed7..29ae0b5c 100644 --- a/src/lib/smsClient.js +++ b/src/lib/smsClient.js @@ -1518,8 +1518,7 @@ const getLogForwardingErrors = async ( const { accessToken } = await getDevConsoleConfig(); const config = { method: 'GET', - // url: `${SMS_BASE_URL}/organizations/${organizationCode}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}/log/forwarding/errors?startTime=${startTime}&endTime=${endTime}`, - url: `${SMS_BASE_URL}/organizations/${organizationCode}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}/logs?startDateTime=${startTime}&endDateTime=${endTime}`, + url: `${SMS_BASE_URL}/organizations/${organizationCode}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}/log/forwarding/errors?startDateTime=${startTime}&endDateTime=${endTime}`, headers: { 'Authorization': `Bearer ${accessToken}`, 'x-request-id': global.requestId, From 6e0e077c8d86713298e60fd0bde80d610f38641d Mon Sep 17 00:00:00 2001 From: Sumaiya <108254100+AjazSumaiya@users.noreply.github.com> Date: Fri, 23 May 2025 17:20:58 +0530 Subject: [PATCH 05/11] Update package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bce12725..4a25cb47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/aio-cli-plugin-api-mesh", - "version": "5.3.2", + "version": "5.4.0-beta.1", "description": "Adobe I/O CLI plugin to develop and manage API mesh sources", "keywords": [ "oclif-plugin" From 5f1ff8ee45785094a058dd762c24ea1308c67f46 Mon Sep 17 00:00:00 2001 From: ajaz Date: Thu, 29 May 2025 21:34:54 +0530 Subject: [PATCH 06/11] fix: remove startTime and endTime flags --- .../get-log-forwarding-errors.test.js | 277 ++++++++---------- .../config/get/log-forwarding/errors.js | 205 ++++++------- src/lib/smsClient.js | 36 +-- 3 files changed, 228 insertions(+), 290 deletions(-) diff --git a/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js b/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js index 67b5a498..a5db1949 100644 --- a/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js +++ b/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js @@ -9,15 +9,9 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - -const fs = require('fs'); -const path = require('path'); -const GetLogForwardingErrorsCommand = require('../config/get/log-forwarding/errors'); -const { initSdk, promptConfirm } = require('../../../helpers'); -const { getMeshId, getLogForwardingErrors } = require('../../../lib/smsClient'); - jest.mock('fs'); jest.mock('axios'); + jest.mock('../../../helpers', () => ({ initSdk: jest.fn().mockResolvedValue({}), initRequestId: jest.fn().mockResolvedValue({}), @@ -26,26 +20,25 @@ jest.mock('../../../helpers', () => ({ jest.mock('../../../lib/smsClient'); jest.mock('../../../classes/logger'); +const fs = require('fs'); +const path = require('path'); +const GetLogForwardingErrorsCommand = require('../config/get/log-forwarding/errors'); +const { initSdk, promptConfirm } = require('../../../helpers'); +const { getMeshId, getLogForwardingErrors } = require('../../../lib/smsClient'); + describe('GetLogForwardingErrorsCommand', () => { let parseSpy; + let logSpy; beforeEach(() => { - const now = new Date(); - const startTime = new Date(now); - const endTime = new Date(now); - startTime.setMinutes(startTime.getMinutes() - 2); - const formattedStartTime = startTime.toISOString().slice(0, 19) + 'Z'; - const formattedEndTime = endTime.toISOString().slice(0, 19) + 'Z'; - - parseSpy = jest.spyOn(GetLogForwardingErrorsCommand.prototype, 'parse').mockResolvedValue({ - flags: { - startTime: formattedStartTime, - endTime: formattedEndTime, - filename: 'test.csv', - ignoreCache: false, - }, - }); - + logSpy = jest + .spyOn(GetLogForwardingErrorsCommand.prototype, 'log') + .mockImplementation(() => {}); + parseSpy = jest + .spyOn(GetLogForwardingErrorsCommand.prototype, 'parse') + .mockResolvedValue({ flags: { filename: undefined, ignoreCache: false } }); + jest.spyOn(path, 'extname').mockReturnValue('.csv'); + jest.spyOn(path, 'resolve').mockImplementation((...args) => args.join('/')); initSdk.mockResolvedValue({ imsOrgId: 'orgId', imsOrgCode: 'orgCode', @@ -55,9 +48,17 @@ describe('GetLogForwardingErrorsCommand', () => { }); getMeshId.mockResolvedValue('meshId'); getLogForwardingErrors.mockResolvedValue({ - errorUrls: [{ key: 'error1.csv', url: 'http://example.com/someHash' }], - totalSize: 2048, + presignedUrls: ['http://example.com/error1', 'http://example.com/error2'], + totalSize: 1024, }); + jest + .spyOn(GetLogForwardingErrorsCommand.prototype, 'downloadFileContent') + .mockImplementation(() => createMockStream(mockLogData)); + fs.existsSync.mockReturnValue(false); + fs.writeFileSync.mockImplementation(() => {}); + fs.statSync.mockReturnValue({ size: 0 }); + path.extname = jest.fn().mockReturnValue('.csv'); + path.resolve = jest.fn().mockImplementation((...args) => args.join('/')); promptConfirm.mockResolvedValue(true); global.requestId = 'dummy_request_id'; }); @@ -66,169 +67,147 @@ describe('GetLogForwardingErrorsCommand', () => { jest.clearAllMocks(); }); - test('throws an error if the time difference between startTime and endTime is greater than 30 minutes', async () => { - fs.existsSync.mockReturnValue(true); - fs.statSync.mockReturnValue({ size: 0 }); - const now = new Date(); - const startTime = new Date(now); - const endTime = new Date(now); - startTime.setMinutes(startTime.getMinutes() - 45); - const formattedStartTime = startTime.toISOString().slice(0, 19) + 'Z'; - const formattedEndTime = endTime.toISOString().slice(0, 19) + 'Z'; - parseSpy.mockResolvedValueOnce({ - flags: { - startTime: formattedStartTime, - endTime: formattedEndTime, - filename: 'test.csv', - ignoreCache: false, - }, - }); + test('prints log lines to console when no filename provided', async () => { const command = new GetLogForwardingErrorsCommand([], {}); - await expect(command.run()).rejects.toThrow( - 'The maximum duration between startTime and endTime is 30 minutes. The current duration is 0 hours 45 minutes and 0 seconds.', - ); + await command.run(); + expect(logSpy).toHaveBeenCalledWith('Successfully fetched log forwarding errors.'); + const logCalls = logSpy.mock.calls.map(call => call[0]); + const logLines = logCalls.filter(line => line.startsWith('> ')); + expect(logLines.length).toBe(6); // 3 lines per file * 2 files + expect(logLines[0]).toBe('> Error log line 1'); + expect(logLines[1]).toBe('> Error log line 2'); + expect(logLines[2]).toBe('> Error log line 3'); }); - test('throws an error if the endTime is greater than current time(now)', async () => { - fs.existsSync.mockReturnValue(true); - fs.statSync.mockReturnValue({ size: 0 }); - const now = new Date(); - const startTime = new Date(now); - const endTime = new Date(now); - endTime.setMinutes(startTime.getMinutes() + 45); - const formattedStartTime = startTime.toISOString().slice(0, 19) + 'Z'; - const formattedEndTime = endTime.toISOString().slice(0, 19) + 'Z'; - parseSpy.mockResolvedValueOnce({ - flags: { - startTime: formattedStartTime, - endTime: formattedEndTime, - filename: 'test.csv', - ignoreCache: false, - }, - }); + test('writes to file when filename provided and user confirms', async () => { + parseSpy.mockResolvedValueOnce({ flags: { filename: 'test.csv', ignoreCache: false } }); const command = new GetLogForwardingErrorsCommand([], {}); - await expect(command.run()).rejects.toThrow( - 'endTime cannot be in the future. Provide a valid endTime.', + await command.run(); + expect(promptConfirm).toHaveBeenCalledWith( + 'The expected file size is 1.00 KB. Confirm test.csv download? (y/n)', + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('test.csv'), + expect.stringContaining('Error log line 1'), + 'utf8', + ); + expect(logSpy).toHaveBeenCalledWith( + 'Successfully downloaded the log forwarding error logs to test.csv', ); }); - test('throws an error if startTime format is invalid', async () => { - parseSpy.mockResolvedValueOnce({ - flags: { - startTime: '20241213223832', - endTime: '2024-08-29T12:30:00Z', - filename: 'test.csv', - }, - }); + test('does not write file when user declines confirmation', async () => { + parseSpy.mockResolvedValueOnce({ flags: { filename: 'test.csv', ignoreCache: false } }); + promptConfirm.mockResolvedValueOnce(false); + const command = new GetLogForwardingErrorsCommand([], {}); + await command.run(); + expect(fs.writeFileSync).toHaveBeenCalledTimes(1); // Only the initial empty file creation + expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('test.csv'), ''); + expect(logSpy).toHaveBeenCalledWith('Log forwarding errors file not downloaded.'); + }); + + test('throws error for invalid file extension', async () => { + parseSpy.mockResolvedValueOnce({ flags: { filename: 'test.txt', ignoreCache: false } }); + path.extname.mockReturnValue('.txt'); const command = new GetLogForwardingErrorsCommand([], {}); - const correctedStartTime = '2024-12-13T22:38:32Z'; await expect(command.run()).rejects.toThrow( - `Use the format YYYY-MM-DDTHH:MM:SSZ for startTime. Did you mean ${correctedStartTime}?`, + 'Invalid file type. Provide a filename with a .csv extension.', ); }); - test('throws an error if endTime format is invalid', async () => { - parseSpy.mockResolvedValueOnce({ - flags: { - startTime: '2024-08-29T12:00:00Z', - endTime: '2024-08-29:23:45:56Z', - filename: 'test.csv', - }, - }); + test('throws error if existing file is not empty', async () => { + parseSpy.mockResolvedValueOnce({ flags: { filename: 'test.csv', ignoreCache: false } }); + fs.existsSync.mockReturnValue(true); + fs.statSync.mockReturnValue({ size: 100 }); + const command = new GetLogForwardingErrorsCommand([], {}); + await expect(command.run()).rejects.toThrow('Make sure the file: test.csv is empty'); + }); + + test('creates empty file if it does not exist', async () => { + parseSpy.mockResolvedValueOnce({ flags: { filename: 'test.csv', ignoreCache: false } }); + fs.existsSync.mockReturnValue(false); + const command = new GetLogForwardingErrorsCommand([], {}); + await command.run(); + expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('test.csv'), ''); + }); + + test('throws error if no presignedUrls returned', async () => { + getLogForwardingErrors.mockResolvedValueOnce({ presignedUrls: [], totalSize: 0 }); const command = new GetLogForwardingErrorsCommand([], {}); - const correctedEndTime = '2024-08-29T23:45:56Z'; await expect(command.run()).rejects.toThrow( - `Use the format YYYY-MM-DDTHH:MM:SSZ for endTime. Did you mean ${correctedEndTime}?`, + 'No log forwarding errors found for the configured destination.', ); }); - test('throws an error if totalSize is 0', async () => { - fs.existsSync.mockReturnValue(true); - fs.statSync.mockReturnValue({ size: 0 }); + test('throws error if totalSize is 0', async () => { getLogForwardingErrors.mockResolvedValueOnce({ - errorUrls: [{ key: 'error1', url: 'http://example.com/error1' }], + presignedUrls: ['http://example.com/error1'], totalSize: 0, }); const command = new GetLogForwardingErrorsCommand([], {}); - await expect(command.run()).rejects.toThrow('No log forwarding errors available to download'); - }); - - test('throws an error if errors are requested for a date older than 30 days', async () => { - const today = new Date(); - const thirtyDaysAgo = new Date(today); - thirtyDaysAgo.setUTCDate(today.getUTCDate() - 30); - const startTime = new Date(thirtyDaysAgo); - startTime.setUTCDate(thirtyDaysAgo.getUTCDate() - 1); - const formattedStartTime = startTime.toISOString().slice(0, 19) + 'Z'; - parseSpy.mockResolvedValueOnce({ - flags: { - startTime: formattedStartTime, - endTime: '2024-08-30T12:30:00Z', - filename: 'test.csv', - }, - }); - const command = new GetLogForwardingErrorsCommand([], {}); await expect(command.run()).rejects.toThrow( - 'Cannot get logs more than 30 days old. Adjust your time range.', + 'No log forwarding error logs available for the configured destination.', ); }); - test('creates file if it does not exist and checks if file is empty before proceeding', async () => { - fs.existsSync.mockReturnValue(false); - fs.statSync.mockReturnValue({ size: 0 }); - const mockWriteStream = { - write: jest.fn(), - end: jest.fn(), - on: jest.fn((event, callback) => { - if (event === 'finish') { - callback(); - } - }), - }; - fs.createWriteStream.mockReturnValue(mockWriteStream); + test('throws error if meshId is not found', async () => { + getMeshId.mockResolvedValueOnce(null); + const command = new GetLogForwardingErrorsCommand([], {}); + await expect(command.run()).rejects.toThrow('Mesh ID not found.'); + }); + + test('throws error if getMeshId throws', async () => { + getMeshId.mockImplementationOnce(() => { + throw new Error('fail mesh'); + }); + const command = new GetLogForwardingErrorsCommand([], {}); + await expect(command.run()).rejects.toThrow('Unable to get mesh ID: fail mesh.'); + }); + + test('handles download failure gracefully', async () => { + jest + .spyOn(GetLogForwardingErrorsCommand.prototype, 'downloadFileContent') + .mockRejectedValueOnce(new Error('Download failed')); const command = new GetLogForwardingErrorsCommand([], {}); await command.run(); - expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(process.cwd(), 'test.csv')); - expect(fs.writeFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), 'test.csv'), ''); - expect(mockWriteStream.write).toHaveBeenCalled(); + expect(logSpy).toHaveBeenCalledWith('Failed to download or process log file: Download failed'); }); - test('throws an error if the file is not empty', async () => { - fs.existsSync.mockReturnValue(true); - fs.statSync.mockReturnValue({ size: 1024 }); + test('filters out empty lines from content', async () => { + const mockDataWithEmptyLines = `Line 1\n\nLine 2\n\n\nLine 3\n`; + jest + .spyOn(GetLogForwardingErrorsCommand.prototype, 'downloadFileContent') + .mockImplementation(() => createMockStream(mockDataWithEmptyLines)); const command = new GetLogForwardingErrorsCommand([], {}); - await expect(command.run()).rejects.toThrow('Make sure the file: test.csv is empty'); + await command.run(); + const logCalls = logSpy.mock.calls.map(call => call[0]); + const logLines = logCalls.filter(line => line.startsWith('> ')); + expect(logLines.length).toBe(6); // 3 non-empty lines per file * 2 files + expect(logLines[0]).toBe('> Line 1'); + expect(logLines[1]).toBe('> Line 2'); + expect(logLines[2]).toBe('> Line 3'); }); - test('downloads errors if all conditions are met', async () => { - fs.existsSync.mockReturnValue(true); - fs.statSync.mockReturnValue({ size: 0 }); - const mockWriteStream = { - write: jest.fn(), - end: jest.fn(), - on: jest.fn((event, callback) => { - if (event === 'finish') { - callback(); - } - }), - }; - fs.createWriteStream.mockReturnValue(mockWriteStream); + test('calls getLogForwardingErrors with correct parameters', async () => { const command = new GetLogForwardingErrorsCommand([], {}); await command.run(); - expect(initSdk).toHaveBeenCalled(); - expect(getMeshId).toHaveBeenCalledWith('orgCode', 'projectId', 'workspaceId', 'workspaceName'); expect(getLogForwardingErrors).toHaveBeenCalledWith( 'orgCode', 'projectId', 'workspaceId', 'meshId', - expect.any(String), - expect.any(String), ); - expect(fs.createWriteStream).toHaveBeenCalledWith(path.resolve(process.cwd(), 'test.csv'), { - flags: 'a', - }); - expect(mockWriteStream.write).toHaveBeenCalled(); - expect(mockWriteStream.end).toHaveBeenCalled(); }); }); + +const mockLogData = `Error log line 1\nError log line 2\n\nError log line 3`; + +function createMockStream(data) { + const { Readable } = require('stream'); + const stream = new Readable({ + read() {}, + }); + stream.push(data); + stream.push(null); + return stream; +} diff --git a/src/commands/api-mesh/config/get/log-forwarding/errors.js b/src/commands/api-mesh/config/get/log-forwarding/errors.js index b15d0289..9b527f43 100644 --- a/src/commands/api-mesh/config/get/log-forwarding/errors.js +++ b/src/commands/api-mesh/config/get/log-forwarding/errors.js @@ -10,109 +10,36 @@ governing permissions and limitations under the License. */ const { Command } = require('@oclif/core'); -const path = require('path'); -const fs = require('fs'); -const { initSdk, promptConfirm, initRequestId } = require('../../../../../helpers'); +const { initSdk, promptConfirm } = require('../../../../../helpers'); const { getMeshId, getLogForwardingErrors } = require('../../../../../lib/smsClient'); const logger = require('../../../../../classes/logger'); const axios = require('axios'); -const { - ignoreCacheFlag, - startTimeFlag, - endTimeFlag, - logFilenameFlag, - pastFlag, - suggestCorrectedDateFormat, - parsePastDuration, - validateDateTimeRange, - validateDateTimeFormat, -} = require('../../../../../utils'); +const { ignoreCacheFlag, fileNameFlag } = require('../../../../../utils'); +const fs = require('fs'); +const path = require('path'); require('dotenv').config(); class GetLogForwardingErrorsCommand extends Command { static flags = { ignoreCache: ignoreCacheFlag, - startTime: startTimeFlag, - endTime: endTimeFlag, - filename: logFilenameFlag, - past: pastFlag, + filename: fileNameFlag, }; async run() { - await initRequestId(); logger.info(`RequestId: ${global.requestId}`); + const { flags } = await this.parse(GetLogForwardingErrorsCommand); - const ignoreCache = await flags.ignoreCache; - const filename = await flags.filename; - let calculatedStartTime, calculatedEndTime, formattedStartTime, formattedEndTime; - if (!filename || path.extname(filename).toLowerCase() !== '.csv') { - this.error('Invalid file type. Provide a filename with a .csv extension.'); - return; - } + const { ignoreCache, filename } = await flags; - if (flags.startTime && flags.endTime) { - const dateTimeRegex = /^(?:(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T(0[0-9]|1[0-9]|2[0-3]):([0-5]\d):([0-5]\d)Z)$/; - if (!dateTimeRegex.test(flags.startTime)) { - const correctedStartTime = suggestCorrectedDateFormat(flags.startTime); - if (!correctedStartTime) { - this.error('Invalid date components in the startTime. Correct the date and try again.'); - } else { - this.error( - `Use the format YYYY-MM-DDTHH:MM:SSZ for startTime. Did you mean ${correctedStartTime}?`, - ); - } - return; - } - if (!dateTimeRegex.test(flags.endTime)) { - const correctedEndTime = suggestCorrectedDateFormat(flags.endTime); - if (!correctedEndTime) { - this.error('Invalid date components in the endTime. Correct the date and try again.'); - } else { - this.error( - `Use the format YYYY-MM-DDTHH:MM:SSZ for endTime. Did you mean ${correctedEndTime}?`, - ); - } - return; - } - validateDateTimeRange(flags.startTime, flags.endTime); - formattedStartTime = flags.startTime.replace(/-|:|Z/g, '').replace('T', 'T'); - formattedEndTime = flags.endTime.replace(/-|:|Z/g, '').replace('T', 'T'); - } else if (flags.past) { - const pastTimeWindow = parsePastDuration(flags.past); - calculatedEndTime = new Date(); - calculatedStartTime = new Date(calculatedEndTime.getTime() - pastTimeWindow); - validateDateTimeRange(calculatedStartTime, calculatedEndTime); - formattedStartTime = validateDateTimeFormat(calculatedStartTime); - formattedEndTime = validateDateTimeFormat(calculatedEndTime); - } else if ((flags.startTime && !flags.endTime) || (!flags.startTime && flags.endTime)) { - this.error('You must provide both a startTime and an endTime.'); - return; - } else { - this.error( - 'Missing required flags. Provide a time range with --startTime and --endTime flags, or use the --past flag for recent errors. Use the `mesh config get log-forwarding errors --help` command for more information.', - ); - return; - } + logger.info('Calling initSdk...'); - if (!filename) { - this.error('Missing filename. Provide a valid file in the current working directory.'); - return; - } + const { imsOrgCode, projectId, workspaceId, workspaceName } = await initSdk({ ignoreCache }); - const outputFile = path.resolve(process.cwd(), filename); - if (!fs.existsSync(outputFile)) { - fs.writeFileSync(outputFile, ''); - } - const stats = fs.statSync(outputFile); - if (stats.size > 0) { - throw new Error(`Make sure the file: ${filename} is empty`); - } + // Retrieve meshId + let meshId = ''; - logger.info('Calling initSdk...'); - const { imsOrgCode, projectId, workspaceId, workspaceName } = await initSdk({ ignoreCache }); - let meshId = null; try { meshId = await getMeshId(imsOrgCode, projectId, workspaceId, workspaceName); } catch (err) { @@ -122,55 +49,85 @@ class GetLogForwardingErrorsCommand extends Command { this.error('Mesh ID not found.'); } - const { errorUrls, totalSize } = await getLogForwardingErrors( + // fetch log forwarding errors presigned URLs + const { presignedUrls, totalSize } = await getLogForwardingErrors( imsOrgCode, projectId, workspaceId, meshId, - formattedStartTime, - formattedEndTime, ); - if (!errorUrls || errorUrls.length === 0) { - this.error('No log forwarding errors found for the specified time range.'); + + // If presigned URLs are not found, throw error saying that no log forwarding errors are found + if (!presignedUrls || presignedUrls.length === 0) { + this.error('No log forwarding errors found for the configured destination.'); } - let shouldDownload = false; - if (totalSize > 0) { - const totalSizeKB = (totalSize / 1024).toFixed(2); - shouldDownload = await promptConfirm( - `The expected file size is ${totalSizeKB} KB. Do you want to download ${filename}? (y/n)`, - ); - if (shouldDownload) { - const writer = fs.createWriteStream(outputFile, { flags: 'a' }); - const columnHeaders = - 'EventTimestampMs,ErrorType,ErrorMessage,MeshId,RayID,URL,Request Method,Response Status,Level'; - writer.write(`${columnHeaders}\n`); - for (const urlObj of errorUrls) { - const { key, url } = urlObj; - logger.info(`Downloading ${key} and appending to ${outputFile}...`); - try { - const fileContentStream = await this.downloadFileContent(url); - fileContentStream.pipe(writer, { end: false }); - await new Promise((resolve, reject) => { - fileContentStream.on('end', resolve); - fileContentStream.on('error', reject); - }); - logger.info(`${key} content appended successfully.`); - } catch (error) { - logger.error(`Error downloading or appending content of ${key}:`, error); - } + const allRows = []; + let shouldDownload = true; + + // If filename is provided, check if it has a .csv extension and if the file exists + if (filename) { + if (path.extname(filename).toLowerCase() !== '.csv') { + this.error('Invalid file type. Provide a filename with a .csv extension.'); + } + const outputFile = path.resolve(process.cwd(), filename); + if (fs.existsSync(outputFile)) { + // If the file exists, check if it is empty + const stats = fs.statSync(outputFile); + if (stats.size > 0) { + this.error(`Make sure the file: ${filename} is empty`); } - writer.end(); - this.log(`Successfully downloaded the log forwarding errors to ${filename}.`); } else { - this.log('Log forwarding error files not downloaded.'); + // If the file does not exist, create an empty file + fs.writeFileSync(outputFile, ''); + } + } + if (totalSize > 0) { + // Download and process each presigned URL + for (const url of presignedUrls) { + try { + const stream = await this.downloadFileContent(url); + const content = await this.streamToString(stream); + // Split content into lines, filter out empty lines + const lines = content.split(/\r?\n/).filter(line => line.trim() !== ''); + allRows.push(...lines); + } catch (err) { + this.log(`Failed to download or process log file: ${err.message}`); + } + } + // if filename is provided, write the content to the file + if (filename) { + const totalSizeKB = (totalSize / 1024).toFixed(2); + // Get user confirmation before downloading + shouldDownload = await promptConfirm( + `The expected file size is ${totalSizeKB} KB. Confirm ${filename} download? (y/n)`, + ); + if (shouldDownload) { + const outputFile = path.resolve(process.cwd(), filename); + const csvContent = allRows.join('\n'); + fs.writeFileSync(outputFile, csvContent + '\n', 'utf8'); + this.log(`Successfully downloaded the log forwarding error logs to ${filename}`); + } else { + this.log('Log forwarding errors file not downloaded.'); + return; + } + } + // If filename is not provided, print the logs to the console + else { + this.log(`Successfully fetched log forwarding errors.`); + // print the error logs each in a new line starting with > + allRows.forEach(rows => { + this.log(`> ${rows}`); + }); } } else { - this.error('No log forwarding errors available to download'); + this.error('No log forwarding error logs available for the configured destination.'); } } + // Download file content from the presigned URL async downloadFileContent(url) { + logger.debug(`[downloadFileContent] Downloading from URL: ${url}`); return axios({ method: 'get', url: url, @@ -182,9 +139,17 @@ class GetLogForwardingErrorsCommand extends Command { throw error; }); } + + // Parse CSV rows from the content + async streamToString(stream) { + const chunks = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + return Buffer.concat(chunks).toString('utf8'); + } } -GetLogForwardingErrorsCommand.description = - 'Download log forwarding errors for a specified time period.'; +GetLogForwardingErrorsCommand.description = 'Get log forwarding errors for the mesh.'; module.exports = GetLogForwardingErrorsCommand; diff --git a/src/lib/smsClient.js b/src/lib/smsClient.js index 29ae0b5c..eb98498f 100644 --- a/src/lib/smsClient.js +++ b/src/lib/smsClient.js @@ -1503,44 +1503,38 @@ const deleteLogForwarding = async (organizationCode, projectId, workspaceId, mes * @param {string} projectId - The project ID * @param {string} workspaceId - The workspace ID * @param {string} meshId - The mesh ID - * @param {string} startTime - Start time in UTC (YYYYMMDDTHHMMSS) - * @param {string} endTime - End time in UTC (YYYYMMDDTHHMMSS) - * @returns {Promise<{ errorUrls: Array<{ key: string, url: string }>, totalSize: number }>} */ -const getLogForwardingErrors = async ( - organizationCode, - projectId, - workspaceId, - meshId, - startTime, - endTime, -) => { +const getLogForwardingErrors = async (organizationCode, projectId, workspaceId, meshId) => { const { accessToken } = await getDevConsoleConfig(); const config = { method: 'GET', - url: `${SMS_BASE_URL}/organizations/${organizationCode}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}/log/forwarding/errors?startDateTime=${startTime}&endDateTime=${endTime}`, + url: `${SMS_BASE_URL}/organizations/${organizationCode}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}/log/forwarding/errors`, headers: { 'Authorization': `Bearer ${accessToken}`, 'x-request-id': global.requestId, - 'x-api-key': SMS_API_KEY, + 'x-api-key': 'adobeio_onboarding', }, }; logger.info('Initiating GET %s', config.url); try { const response = await axios(config); + logger.info('Response from GET %s', response.status); - if (response && response.status === 200 && response.data) { + + if (response?.status === 200) { + logger.info(`Log forwarding error Presigned urls: ${objToString(response, ['data'])}`); + const { presignedUrls, totalSize } = response.data; return { - errorUrls: response.data.presignedUrls || [], - totalSize: response.data.totalSize || 0, + presignedUrls, + totalSize, }; - } else { - logger.error('No log forwarding errors found for the specified time range.'); - return { errorUrls: [], totalSize: 0 }; } } catch (error) { - logger.error('Error fetching log forwarding errors:', error.message); - throw new Error('Unable to fetch log forwarding errors.'); + logger.error(`Error fetching log forwarding errors presigned urls: ${error}`); + return { + urls: {}, + totalSize: 0, + }; } }; From 2a708e009dce5d4bcec18d063511e4678382b149 Mon Sep 17 00:00:00 2001 From: ajaz Date: Fri, 30 May 2025 17:33:48 +0530 Subject: [PATCH 07/11] fix: statements --- .../get-log-forwarding-errors.test.js | 20 +++++++++--- .../config/get/log-forwarding/errors.js | 31 ++++++++++++------- src/lib/smsClient.js | 2 +- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js b/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js index a5db1949..924c2b12 100644 --- a/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js +++ b/src/commands/api-mesh/__tests__/get-log-forwarding-errors.test.js @@ -70,8 +70,12 @@ describe('GetLogForwardingErrorsCommand', () => { test('prints log lines to console when no filename provided', async () => { const command = new GetLogForwardingErrorsCommand([], {}); await command.run(); - expect(logSpy).toHaveBeenCalledWith('Successfully fetched log forwarding errors.'); + // Accept log message with or without leading newline const logCalls = logSpy.mock.calls.map(call => call[0]); + const found = logCalls.some( + line => line.trim() === 'Successfully fetched log forwarding errors.', + ); + expect(found).toBe(true); const logLines = logCalls.filter(line => line.startsWith('> ')); expect(logLines.length).toBe(6); // 3 lines per file * 2 files expect(logLines[0]).toBe('> Error log line 1'); @@ -153,7 +157,9 @@ describe('GetLogForwardingErrorsCommand', () => { test('throws error if meshId is not found', async () => { getMeshId.mockResolvedValueOnce(null); const command = new GetLogForwardingErrorsCommand([], {}); - await expect(command.run()).rejects.toThrow('Mesh ID not found.'); + await expect(command.run()).rejects.toThrow( + 'Unable to get mesh ID. Please check the details and try again. RequestId: dummy_request_id', + ); }); test('throws error if getMeshId throws', async () => { @@ -161,7 +167,9 @@ describe('GetLogForwardingErrorsCommand', () => { throw new Error('fail mesh'); }); const command = new GetLogForwardingErrorsCommand([], {}); - await expect(command.run()).rejects.toThrow('Unable to get mesh ID: fail mesh.'); + await expect(command.run()).rejects.toThrow( + 'Unable to get mesh ID. Please check the details and try again. RequestId: dummy_request_id', + ); }); test('handles download failure gracefully', async () => { @@ -170,7 +178,11 @@ describe('GetLogForwardingErrorsCommand', () => { .mockRejectedValueOnce(new Error('Download failed')); const command = new GetLogForwardingErrorsCommand([], {}); await command.run(); - expect(logSpy).toHaveBeenCalledWith('Failed to download or process log file: Download failed'); + const logCalls = logSpy.mock.calls.map(call => call[0]); + const found = logCalls.some(line => + line.includes('Failed to download or process log file: Download failed'), + ); + expect(found).toBe(true); }); test('filters out empty lines from content', async () => { diff --git a/src/commands/api-mesh/config/get/log-forwarding/errors.js b/src/commands/api-mesh/config/get/log-forwarding/errors.js index 9b527f43..23db19d2 100644 --- a/src/commands/api-mesh/config/get/log-forwarding/errors.js +++ b/src/commands/api-mesh/config/get/log-forwarding/errors.js @@ -35,18 +35,20 @@ class GetLogForwardingErrorsCommand extends Command { logger.info('Calling initSdk...'); - const { imsOrgCode, projectId, workspaceId, workspaceName } = await initSdk({ ignoreCache }); + const { imsOrgCode, projectId, workspaceId } = await initSdk({ ignoreCache }); // Retrieve meshId let meshId = ''; try { - meshId = await getMeshId(imsOrgCode, projectId, workspaceId, workspaceName); + meshId = await getMeshId(imsOrgCode, projectId, workspaceId, meshId); + if (!meshId) { + throw new Error('MeshIdNotFound'); + } } catch (err) { - this.error(`Unable to get mesh ID: ${err.message}.`); - } - if (!meshId) { - this.error('Mesh ID not found.'); + this.error( + `Unable to get mesh ID. Please check the details and try again. RequestId: ${global.requestId}`, + ); } // fetch log forwarding errors presigned URLs @@ -59,7 +61,9 @@ class GetLogForwardingErrorsCommand extends Command { // If presigned URLs are not found, throw error saying that no log forwarding errors are found if (!presignedUrls || presignedUrls.length === 0) { - this.error('No log forwarding errors found for the configured destination.'); + this.error( + `No log forwarding errors found for the configured destination. RequestId: ${global.requestId}`, + ); } const allRows = []; @@ -84,15 +88,18 @@ class GetLogForwardingErrorsCommand extends Command { } if (totalSize > 0) { // Download and process each presigned URL - for (const url of presignedUrls) { + for (const { url } of presignedUrls) { try { + logger.info(`[GetLogForwardingErrorsCommand] Downloading from URL: ${url}`); const stream = await this.downloadFileContent(url); const content = await this.streamToString(stream); // Split content into lines, filter out empty lines const lines = content.split(/\r?\n/).filter(line => line.trim() !== ''); allRows.push(...lines); } catch (err) { - this.log(`Failed to download or process log file: ${err.message}`); + this.log( + `Failed to download or process log file: ${err.message}. RequestId: ${global.requestId}`, + ); } } // if filename is provided, write the content to the file @@ -114,14 +121,16 @@ class GetLogForwardingErrorsCommand extends Command { } // If filename is not provided, print the logs to the console else { - this.log(`Successfully fetched log forwarding errors.`); + this.log(`\nSuccessfully fetched log forwarding errors.`); // print the error logs each in a new line starting with > allRows.forEach(rows => { this.log(`> ${rows}`); }); } } else { - this.error('No log forwarding error logs available for the configured destination.'); + this.error( + `No log forwarding error logs available for the configured destination. RequestId: ${global.requestId}`, + ); } } diff --git a/src/lib/smsClient.js b/src/lib/smsClient.js index eb98498f..5d931ca9 100644 --- a/src/lib/smsClient.js +++ b/src/lib/smsClient.js @@ -1512,7 +1512,7 @@ const getLogForwardingErrors = async (organizationCode, projectId, workspaceId, headers: { 'Authorization': `Bearer ${accessToken}`, 'x-request-id': global.requestId, - 'x-api-key': 'adobeio_onboarding', + 'x-api-key': SMS_API_KEY, }, }; logger.info('Initiating GET %s', config.url); From 85ae4167c2d98ec6e62f8c5248cdfc91e8f67c62 Mon Sep 17 00:00:00 2001 From: ajaz Date: Mon, 2 Jun 2025 14:12:59 +0530 Subject: [PATCH 08/11] fix: update command description --- src/commands/api-mesh/config/get/log-forwarding/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/commands/api-mesh/config/get/log-forwarding/index.js b/src/commands/api-mesh/config/get/log-forwarding/index.js index a314a1bf..c2a89ac7 100644 --- a/src/commands/api-mesh/config/get/log-forwarding/index.js +++ b/src/commands/api-mesh/config/get/log-forwarding/index.js @@ -75,8 +75,9 @@ class GetLogForwardingCommand extends Command { GetLogForwardingCommand.description = `Get log forwarding details and error logs for a specified mesh. -The 'log-forwarding' command includes the following options: -- log-forwarding : Retrieve log forwarding details for a given mesh. -- log-forwarding:errors : Download log forwarding error logs for a selected time period.`; +-The 'log-forwarding' command includes the following options: + +- api-mesh:config:get:log-forwarding : Retrieve log forwarding details for a given mesh. +- api-mesh:config:get:log-forwarding:errors : Download log forwarding error logs for a selected time period.`; module.exports = GetLogForwardingCommand; From 81a04bc5faa4b516ea5fe898ccb69b07d2bc4ce6 Mon Sep 17 00:00:00 2001 From: ajaz Date: Mon, 2 Jun 2025 15:50:47 +0530 Subject: [PATCH 09/11] fix: overwrite config command usage --- src/commands/api-mesh/config/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commands/api-mesh/config/index.js b/src/commands/api-mesh/config/index.js index 85b2556f..3d4b1f94 100644 --- a/src/commands/api-mesh/config/index.js +++ b/src/commands/api-mesh/config/index.js @@ -13,15 +13,18 @@ governing permissions and limitations under the License. const { Help, Command } = require('@oclif/core'); class ConfigCommand extends Command { + + static summary = 'Manage the configuration for API Mesh'; + +static usage = 'api-mesh:config [COMMAND]'; + async run() { const help = new Help(this.config); await help.showHelp(['api-mesh:config', '--help']); } } -ConfigCommand.description = `Manage the configuration for API Mesh. - -The 'config' command includes the following options: +ConfigCommand.description = `The 'config' command includes the following options: - set: Set log forwarding details for a given mesh. - get: Retrieve log forwarding details for a given mesh. - delete: Delete log forwarding details for a given mesh.`; From e34fe817efac73a052e7a576535d588f974a60b1 Mon Sep 17 00:00:00 2001 From: ajaz Date: Mon, 2 Jun 2025 15:53:05 +0530 Subject: [PATCH 10/11] fix linting --- src/commands/api-mesh/config/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/api-mesh/config/index.js b/src/commands/api-mesh/config/index.js index 3d4b1f94..d7e3c668 100644 --- a/src/commands/api-mesh/config/index.js +++ b/src/commands/api-mesh/config/index.js @@ -13,10 +13,9 @@ governing permissions and limitations under the License. const { Help, Command } = require('@oclif/core'); class ConfigCommand extends Command { - static summary = 'Manage the configuration for API Mesh'; -static usage = 'api-mesh:config [COMMAND]'; + static usage = 'api-mesh:config [COMMAND]'; async run() { const help = new Help(this.config); From 915a342db63ce737b7488eb09ba93bf58d178a6e Mon Sep 17 00:00:00 2001 From: ajaz Date: Mon, 2 Jun 2025 17:46:17 +0530 Subject: [PATCH 11/11] fix: override all log forwarding commands usage --- src/commands/api-mesh/config/delete/log-forwarding.js | 2 ++ src/commands/api-mesh/config/get/log-forwarding/errors.js | 2 ++ src/commands/api-mesh/config/get/log-forwarding/index.js | 2 ++ src/commands/api-mesh/config/set/log-forwarding.js | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/commands/api-mesh/config/delete/log-forwarding.js b/src/commands/api-mesh/config/delete/log-forwarding.js index 2bfbe43a..8b9a9f2c 100644 --- a/src/commands/api-mesh/config/delete/log-forwarding.js +++ b/src/commands/api-mesh/config/delete/log-forwarding.js @@ -21,6 +21,8 @@ class DeleteLogForwardingCommand extends Command { autoConfirmAction: autoConfirmActionFlag, }; + static usage = 'api-mesh:config:delete:log-forwarding'; + async run() { logger.info(`RequestId: ${global.requestId}`); diff --git a/src/commands/api-mesh/config/get/log-forwarding/errors.js b/src/commands/api-mesh/config/get/log-forwarding/errors.js index 23db19d2..e87d6a02 100644 --- a/src/commands/api-mesh/config/get/log-forwarding/errors.js +++ b/src/commands/api-mesh/config/get/log-forwarding/errors.js @@ -26,6 +26,8 @@ class GetLogForwardingErrorsCommand extends Command { filename: fileNameFlag, }; + static usage = 'api-mesh:config:get:log-forwarding:errors'; + async run() { logger.info(`RequestId: ${global.requestId}`); diff --git a/src/commands/api-mesh/config/get/log-forwarding/index.js b/src/commands/api-mesh/config/get/log-forwarding/index.js index c2a89ac7..a1a2f941 100644 --- a/src/commands/api-mesh/config/get/log-forwarding/index.js +++ b/src/commands/api-mesh/config/get/log-forwarding/index.js @@ -23,6 +23,8 @@ class GetLogForwardingCommand extends Command { static enableJsonFlag = true; + static usage = 'api-mesh:config:get:log-forwarding'; + async run() { await initRequestId(); diff --git a/src/commands/api-mesh/config/set/log-forwarding.js b/src/commands/api-mesh/config/set/log-forwarding.js index d6d3b91f..57b2fc79 100644 --- a/src/commands/api-mesh/config/set/log-forwarding.js +++ b/src/commands/api-mesh/config/set/log-forwarding.js @@ -42,6 +42,8 @@ class SetLogForwardingCommand extends Command { static enableJsonFlag = true; + static usage = 'api-mesh:config:set:log-forwarding'; + async run() { await initRequestId();