@adobe/aio-cli-plugin-api-mesh 3.7.0-beta.1 → 3.8.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/oclif.manifest.json +1 -1
- package/package.json +3 -3
- package/src/commands/__fixtures__/files/requestParams.json +2 -2
- package/src/commands/__fixtures__/openapi-schema.json +3 -3
- package/src/commands/__fixtures__/requestParams.json +2 -2
- package/src/commands/__fixtures__/sample_fully_qualified_mesh.json +28 -28
- package/src/commands/__fixtures__/sample_mesh_files.json +22 -22
- package/src/commands/__fixtures__/sample_mesh_invalid_file_content.json +13 -13
- package/src/commands/__fixtures__/sample_mesh_invalid_file_name.json +24 -26
- package/src/commands/__fixtures__/sample_mesh_invalid_paths.json +22 -22
- package/src/commands/__fixtures__/sample_mesh_invalid_type.json +24 -26
- package/src/commands/__fixtures__/sample_mesh_mismatching_path.json +28 -28
- package/src/commands/__fixtures__/sample_mesh_outside_workspace_dir.json +22 -22
- package/src/commands/__fixtures__/sample_mesh_path_from_home.json +13 -13
- package/src/commands/__fixtures__/sample_mesh_subdirectory.json +22 -22
- package/src/commands/__fixtures__/sample_mesh_with_files_array.json +28 -28
- package/src/commands/__fixtures__/sample_secrets_mesh.json +1 -1
- package/src/commands/api-mesh/__tests__/log-get-bulk.test.js +253 -0
- package/src/commands/api-mesh/__tests__/log-get.test.js +234 -0
- package/src/commands/api-mesh/__tests__/log-list.test.js +162 -0
- package/src/commands/api-mesh/log-get-bulk.js +243 -0
- package/src/commands/api-mesh/log-get.js +83 -0
- package/src/commands/api-mesh/log-list.js +105 -0
- package/src/lib/devConsole.js +139 -0
- package/src/utils.js +70 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const ListLogsCommand = require('../log-list');
|
|
3
|
+
const { initSdk, promptConfirm } = require('../../../helpers');
|
|
4
|
+
const { getMeshId, listLogs } = require('../../../lib/devConsole');
|
|
5
|
+
|
|
6
|
+
jest.mock('fs');
|
|
7
|
+
jest.mock('axios');
|
|
8
|
+
jest.mock('../../../helpers', () => ({
|
|
9
|
+
initSdk: jest.fn().mockResolvedValue({}),
|
|
10
|
+
initRequestId: jest.fn().mockResolvedValue({}),
|
|
11
|
+
promptConfirm: jest.fn().mockResolvedValue(true),
|
|
12
|
+
}));
|
|
13
|
+
jest.mock('../../../lib/devConsole');
|
|
14
|
+
jest.mock('../../../classes/logger');
|
|
15
|
+
|
|
16
|
+
describe('List Logs Command', () => {
|
|
17
|
+
let parseSpy;
|
|
18
|
+
let logSpy;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Setup spies and mock functions
|
|
22
|
+
parseSpy = jest.spyOn(ListLogsCommand.prototype, 'parse').mockResolvedValue({
|
|
23
|
+
flags: {
|
|
24
|
+
filename: 'test.csv',
|
|
25
|
+
ignoreCache: false,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
logSpy = jest.spyOn(ListLogsCommand.prototype, 'log');
|
|
30
|
+
|
|
31
|
+
// initRequestId.mockResolvedValue();
|
|
32
|
+
initSdk.mockResolvedValue({
|
|
33
|
+
imsOrgId: 'orgId',
|
|
34
|
+
imsOrgCode: 'orgCode',
|
|
35
|
+
projectId: 'projectId',
|
|
36
|
+
workspaceId: 'workspaceId',
|
|
37
|
+
workspaceName: 'workspaceName',
|
|
38
|
+
});
|
|
39
|
+
getMeshId.mockResolvedValue('meshId');
|
|
40
|
+
listLogs.mockResolvedValue([
|
|
41
|
+
{
|
|
42
|
+
rayId: '8c171e8a9a47c16d',
|
|
43
|
+
timestamp: 1726052061861,
|
|
44
|
+
responseStatus: 200,
|
|
45
|
+
level: 'info',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
rayId: '8c171dd35860c16d',
|
|
49
|
+
timestamp: 1726052032540,
|
|
50
|
+
responseStatus: 200,
|
|
51
|
+
level: 'info',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
rayId: '8c171dd22f00c16d',
|
|
55
|
+
timestamp: 1726052032348,
|
|
56
|
+
responseStatus: 200,
|
|
57
|
+
level: 'info',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
rayId: '8c171dd10df2c16d',
|
|
61
|
+
timestamp: 1726052032167,
|
|
62
|
+
responseStatus: 200,
|
|
63
|
+
level: 'info',
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
fs.existsSync.mockReturnValue(false);
|
|
68
|
+
fs.appendFileSync.mockReturnValue();
|
|
69
|
+
promptConfirm.mockResolvedValue(true);
|
|
70
|
+
global.requestId = 'dummy_request_id';
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
jest.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('Throws an error if filename is not of csv extension', async () => {
|
|
78
|
+
// Mock the file system checks even if they are not the focus of this test
|
|
79
|
+
parseSpy.mockResolvedValue({
|
|
80
|
+
flags: {
|
|
81
|
+
filename: 'test.txt',
|
|
82
|
+
ignoreCache: false,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const command = new ListLogsCommand([], {});
|
|
87
|
+
await expect(command.run()).rejects.toThrow(
|
|
88
|
+
'Invalid file type. Provide a filename with a .csv extension.',
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('Throws an error if file already exists', async () => {
|
|
93
|
+
fs.existsSync.mockReturnValue(true);
|
|
94
|
+
|
|
95
|
+
const command = new ListLogsCommand([], {});
|
|
96
|
+
await expect(command.run()).rejects.toThrow(
|
|
97
|
+
'File test.csv already exists. Provide a new file name.',
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('Throws an error is meshId is not found', async () => {
|
|
102
|
+
getMeshId.mockResolvedValue(null);
|
|
103
|
+
|
|
104
|
+
const command = new ListLogsCommand([], {});
|
|
105
|
+
const result = command.run();
|
|
106
|
+
result.catch(err => {
|
|
107
|
+
expect(err.message).toMatchInlineSnapshot(
|
|
108
|
+
`"Unable to get mesh config. No mesh found for Org(orgId) -> Project(projectId) -> Workspace(workspaceId). Check the details and try again. RequestId: dummy_request_id"`,
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('Logs are listed successfully with file as output', async () => {
|
|
114
|
+
const command = new ListLogsCommand([], {});
|
|
115
|
+
await command.run();
|
|
116
|
+
expect(fs.appendFileSync).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('Logs are listed successfully', async () => {
|
|
120
|
+
parseSpy.mockResolvedValue({
|
|
121
|
+
flags: {
|
|
122
|
+
ignoreCache: false,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const command = new ListLogsCommand([], {});
|
|
126
|
+
await command.run();
|
|
127
|
+
expect(fs.appendFileSync).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('No logs found message displayed when sms returns empty array with file as output', async () => {
|
|
131
|
+
listLogs.mockResolvedValue([]);
|
|
132
|
+
const command = new ListLogsCommand([], {});
|
|
133
|
+
await command.run();
|
|
134
|
+
expect(fs.appendFileSync).not.toHaveBeenCalled();
|
|
135
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
136
|
+
`No recent logs found. Alternatively, you can use the following command to get all logs for a 30 minute time period: \naio api-mesh log-get-bulk --startTime YYYY-MM-DDTHH:MM:SSZ --endTime YYYY-MM-DDTHH:MM:SSZ --filename mesh_logs.csv`,
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('No logs found message displayed when sms returns empty array', async () => {
|
|
141
|
+
parseSpy.mockResolvedValue({
|
|
142
|
+
flags: {
|
|
143
|
+
ignoreCache: false,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
listLogs.mockResolvedValue([]);
|
|
147
|
+
const command = new ListLogsCommand([], {});
|
|
148
|
+
await command.run();
|
|
149
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
150
|
+
`No recent logs found. Alternatively, you can use the following command to get all logs for a 30 minute time period: \naio api-mesh log-get-bulk --startTime YYYY-MM-DDTHH:MM:SSZ --endTime YYYY-MM-DDTHH:MM:SSZ --filename mesh_logs.csv`,
|
|
151
|
+
);
|
|
152
|
+
expect(fs.appendFileSync).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('Throw an error if SMS call fails', async () => {
|
|
156
|
+
listLogs.mockRejectedValue(new Error('SMS call failed'));
|
|
157
|
+
const command = new ListLogsCommand([], {});
|
|
158
|
+
await expect(command.run()).rejects.toThrow(
|
|
159
|
+
'Failed to list recent logs, RequestId: dummy_request_id',
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
const { Command } = require('@oclif/core');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { initRequestId, initSdk, promptConfirm } = require('../../helpers');
|
|
5
|
+
const { getMeshId, getPresignedUrls } = require('../../lib/devConsole');
|
|
6
|
+
const logger = require('../../classes/logger');
|
|
7
|
+
const axios = require('axios');
|
|
8
|
+
const {
|
|
9
|
+
ignoreCacheFlag,
|
|
10
|
+
startTimeFlag,
|
|
11
|
+
endTimeFlag,
|
|
12
|
+
logFilenameFlag,
|
|
13
|
+
suggestCorrectedDateFormat,
|
|
14
|
+
} = require('../../utils');
|
|
15
|
+
|
|
16
|
+
require('dotenv').config();
|
|
17
|
+
|
|
18
|
+
class GetBulkLogCommand extends Command {
|
|
19
|
+
static flags = {
|
|
20
|
+
ignoreCache: ignoreCacheFlag,
|
|
21
|
+
startTime: startTimeFlag,
|
|
22
|
+
endTime: endTimeFlag,
|
|
23
|
+
filename: logFilenameFlag,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async run() {
|
|
27
|
+
// Column headers to be written as the first row in the output file
|
|
28
|
+
const columnHeaders =
|
|
29
|
+
'EventTimestampMs,Exceptions,Logs,Outcome,MeshId,RayID,URL,Request Method,Response Status,Level';
|
|
30
|
+
|
|
31
|
+
await initRequestId();
|
|
32
|
+
logger.info(`RequestId: ${global.requestId}`);
|
|
33
|
+
const { flags } = await this.parse(GetBulkLogCommand);
|
|
34
|
+
const ignoreCache = await flags.ignoreCache;
|
|
35
|
+
|
|
36
|
+
const filename = await flags.filename;
|
|
37
|
+
|
|
38
|
+
// Only supports files that end with .csv
|
|
39
|
+
if (!filename || path.extname(filename).toLowerCase() !== '.csv') {
|
|
40
|
+
this.error('Invalid file type. Provide a filename with a .csv extension.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Regular expression to validate the input date format YYYY-MM-DDTHH:MM:SSZ
|
|
44
|
+
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)$/;
|
|
45
|
+
|
|
46
|
+
// Validate user provided startTime format
|
|
47
|
+
if (!dateTimeRegex.test(flags.startTime)) {
|
|
48
|
+
const correctedStartTime = suggestCorrectedDateFormat(flags.startTime);
|
|
49
|
+
if (!correctedStartTime) {
|
|
50
|
+
this.error('Found invalid date components for startTime. Check and correct the date.');
|
|
51
|
+
} else {
|
|
52
|
+
this.error(
|
|
53
|
+
`Use the format YYYY-MM-DDTHH:MM:SSZ for startTime. Did you mean ${correctedStartTime}?`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate user provided endTime format
|
|
61
|
+
if (!dateTimeRegex.test(flags.endTime)) {
|
|
62
|
+
const correctedEndTime = suggestCorrectedDateFormat(flags.endTime);
|
|
63
|
+
//check for incorrect date components
|
|
64
|
+
if (!correctedEndTime) {
|
|
65
|
+
this.error('Found invalid date components for endTime. Check and correct the date.');
|
|
66
|
+
} else {
|
|
67
|
+
this.error(
|
|
68
|
+
`Use the format YYYY-MM-DDTHH:MM:SSZ for endTime. Did you mean ${correctedEndTime}?`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Properly format startTime and endTime strings before handing it over to SMS
|
|
75
|
+
const formattedStartTime = flags.startTime.replace(/-|:|Z/g, '').replace('T', 'T');
|
|
76
|
+
const formattedEndTime = flags.endTime.replace(/-|:|Z/g, '').replace('T', 'T');
|
|
77
|
+
|
|
78
|
+
// Convert formatted times to Date objects for comparison
|
|
79
|
+
const startTime = new Date(flags.startTime);
|
|
80
|
+
const endTime = new Date(flags.endTime);
|
|
81
|
+
|
|
82
|
+
// Require both startTime and endTime
|
|
83
|
+
if (!startTime || !endTime) {
|
|
84
|
+
this.error('Provide both startTime and endTime.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get the current date and calculate the date 30 days ago, both in UTC
|
|
89
|
+
const today = new Date();
|
|
90
|
+
const thirtyDaysAgo = new Date(today);
|
|
91
|
+
thirtyDaysAgo.setUTCDate(today.getUTCDate() - 30);
|
|
92
|
+
// Validate that logs from beyond 30 days from today are not available
|
|
93
|
+
if (startTime < thirtyDaysAgo || endTime < thirtyDaysAgo) {
|
|
94
|
+
this.error('Cannot get logs more than 30 days old. Adjust your time range.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Validate required filename flag
|
|
99
|
+
if (!filename) {
|
|
100
|
+
this.error('Missing filename. Provide a valid file in the current working directory.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if the file exists
|
|
105
|
+
const outputFile = path.resolve(process.cwd(), filename);
|
|
106
|
+
|
|
107
|
+
// Check if file exists and if doesn't, create one in the cwd and continue
|
|
108
|
+
if (!fs.existsSync(outputFile)) {
|
|
109
|
+
fs.writeFileSync(outputFile, '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
//check if the file is empty before proceeding
|
|
113
|
+
const stats = fs.statSync(outputFile);
|
|
114
|
+
if (stats.size > 0) {
|
|
115
|
+
throw new Error(`Make sure the file: ${filename} is empty`);
|
|
116
|
+
}
|
|
117
|
+
// truncate milliseconds to ensure comparison is only done up to seconds
|
|
118
|
+
startTime.setMilliseconds(0);
|
|
119
|
+
endTime.setMilliseconds(0);
|
|
120
|
+
|
|
121
|
+
// Validate startTime < endTime
|
|
122
|
+
if (startTime > endTime) {
|
|
123
|
+
this.error('endTime must be greater than startTime');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 4. Check if the duration between start and end times is greater than 30 minutes (1800 seconds)
|
|
127
|
+
const timeDifferenceInSeconds = (endTime.getTime() - startTime.getTime()) / 1000;
|
|
128
|
+
|
|
129
|
+
if (timeDifferenceInSeconds > 1800) {
|
|
130
|
+
const hours = Math.floor(timeDifferenceInSeconds / 3600); //hours calculation
|
|
131
|
+
const minutes = Math.floor((timeDifferenceInSeconds % 3600) / 60); //minutes calculation
|
|
132
|
+
const seconds = timeDifferenceInSeconds % 60; //seconds calculation
|
|
133
|
+
|
|
134
|
+
this.error(
|
|
135
|
+
`Max duration between startTime and endTime should be 30 minutes. Current duration is ${hours} hour${
|
|
136
|
+
hours !== 1 ? 's' : ''
|
|
137
|
+
} ${minutes} minute${minutes !== 1 ? 's' : ''} and ${seconds} second${
|
|
138
|
+
seconds !== 1 ? 's' : ''
|
|
139
|
+
}.`,
|
|
140
|
+
);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
logger.info('Calling initSdk...');
|
|
144
|
+
const { imsOrgId, imsOrgCode, projectId, workspaceId, workspaceName } = await initSdk({
|
|
145
|
+
ignoreCache,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Retrieve meshId
|
|
149
|
+
let meshId = null;
|
|
150
|
+
try {
|
|
151
|
+
meshId = await getMeshId(imsOrgId, projectId, workspaceId, workspaceName);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
this.error(`Unable to get mesh ID: ${err.message}.`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!meshId) {
|
|
157
|
+
this.error('Mesh ID not found.');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 5. Call downloadFiles
|
|
161
|
+
const { presignedUrls, totalSize } = await getPresignedUrls(
|
|
162
|
+
imsOrgCode,
|
|
163
|
+
projectId,
|
|
164
|
+
workspaceId,
|
|
165
|
+
meshId,
|
|
166
|
+
formattedStartTime,
|
|
167
|
+
formattedEndTime,
|
|
168
|
+
);
|
|
169
|
+
//If presigned URLs are not found, throw error saying that no logs are found
|
|
170
|
+
if (!presignedUrls || presignedUrls.length === 0) {
|
|
171
|
+
this.error('No logs found for the given time range.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let shouldDownload = false;
|
|
175
|
+
if (totalSize > 0) {
|
|
176
|
+
const totalSizeKB = (totalSize / 1024).toFixed(2); // Convert bytes to KB
|
|
177
|
+
// 7. Get user confirmation
|
|
178
|
+
shouldDownload = await promptConfirm(
|
|
179
|
+
`The expected file size is ${totalSizeKB} KB. Confirm ${filename} download? (y/n)`,
|
|
180
|
+
);
|
|
181
|
+
if (shouldDownload) {
|
|
182
|
+
//create a writer and proceed with download
|
|
183
|
+
const writer = fs.createWriteStream(outputFile, { flags: 'a' });
|
|
184
|
+
|
|
185
|
+
// Write the column headers before appending the log content
|
|
186
|
+
writer.write(`${columnHeaders}\n`);
|
|
187
|
+
|
|
188
|
+
// Stream the data from the signed URLs
|
|
189
|
+
for (const urlObj of presignedUrls) {
|
|
190
|
+
const { key, url } = urlObj;
|
|
191
|
+
logger.info(`Downloading ${key} and appending to ${outputFile}...`);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const fileContentStream = await this.downloadFileContent(url);
|
|
195
|
+
fileContentStream.pipe(writer, { end: false });
|
|
196
|
+
|
|
197
|
+
await new Promise((resolve, reject) => {
|
|
198
|
+
fileContentStream.on('end', resolve);
|
|
199
|
+
fileContentStream.on('error', reject);
|
|
200
|
+
});
|
|
201
|
+
// write a newline after each file write
|
|
202
|
+
writer.write('\n');
|
|
203
|
+
|
|
204
|
+
logger.info(`${key} content appended successfully.`);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger.error(`Error downloading or appending content of ${key}:`, error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Ensure the stream is closed
|
|
210
|
+
writer.end();
|
|
211
|
+
|
|
212
|
+
this.log(`Successfully downloaded the logs to ${filename}.`);
|
|
213
|
+
} else {
|
|
214
|
+
this.log('Log files not downloaded.');
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
this.error('No logs available to download');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Downloads the content of a file from the provided presigned URL.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} url - presigned URL to download the log from
|
|
224
|
+
* @returns {Promise<Stream.Readable>} - A promise that resolves to a readable stream of the file content
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
async downloadFileContent(url) {
|
|
228
|
+
return axios({
|
|
229
|
+
method: 'get',
|
|
230
|
+
url: url,
|
|
231
|
+
responseType: 'stream',
|
|
232
|
+
})
|
|
233
|
+
.then(response => response.data)
|
|
234
|
+
.catch(error => {
|
|
235
|
+
logger.error('Error downloading log content:', error.message);
|
|
236
|
+
throw error;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
GetBulkLogCommand.description = 'Download all mesh logs for a selected time period.';
|
|
242
|
+
|
|
243
|
+
module.exports = GetBulkLogCommand;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2021 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
7
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
8
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
9
|
+
governing permissions and limitations under the License.
|
|
10
|
+
*/
|
|
11
|
+
const { Command } = require('@oclif/core');
|
|
12
|
+
const logger = require('../../classes/logger');
|
|
13
|
+
const { initSdk, initRequestId } = require('../../helpers');
|
|
14
|
+
const { ignoreCacheFlag } = require('../../utils');
|
|
15
|
+
const { getMeshId, getLogsByRayId } = require('../../lib/devConsole');
|
|
16
|
+
require('dotenv').config();
|
|
17
|
+
|
|
18
|
+
class FetchLogsCommand extends Command {
|
|
19
|
+
static args = [{ name: 'rayId', required: true, description: 'Fetch a single log by rayID' }];
|
|
20
|
+
static flags = {
|
|
21
|
+
ignoreCache: ignoreCacheFlag,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async run() {
|
|
25
|
+
await initRequestId();
|
|
26
|
+
|
|
27
|
+
logger.info(`RequestId: ${global.requestId}`);
|
|
28
|
+
|
|
29
|
+
const { args, flags } = await this.parse(FetchLogsCommand);
|
|
30
|
+
|
|
31
|
+
const ignoreCache = flags.ignoreCache;
|
|
32
|
+
const rayId = args.rayId;
|
|
33
|
+
|
|
34
|
+
const { imsOrgId, imsOrgCode, projectId, workspaceId } = await initSdk({
|
|
35
|
+
ignoreCache,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let meshId = null;
|
|
39
|
+
try {
|
|
40
|
+
meshId = await getMeshId(imsOrgId, projectId, workspaceId, meshId);
|
|
41
|
+
if (!meshId) {
|
|
42
|
+
throw new Error('MeshIdNotFound');
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
this.error(
|
|
46
|
+
`Unable to get mesh ID. Please check the details and try again. RequestId: ${global.requestId}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const meshLog = await getLogsByRayId(imsOrgCode, projectId, workspaceId, meshId, rayId);
|
|
52
|
+
if (meshLog) {
|
|
53
|
+
this.log('Event Timestamp : %s', meshLog.eventTimestampMs);
|
|
54
|
+
this.log('Exceptions : %s', meshLog.exceptions);
|
|
55
|
+
this.log('Logs : %s', meshLog.logs);
|
|
56
|
+
this.log('Outcome : %s', meshLog.outcome);
|
|
57
|
+
this.log('Mesh ID : %s', meshLog.meshId);
|
|
58
|
+
this.log('RayId : %s', meshLog.rayId);
|
|
59
|
+
this.log('Mesh URL : %s', meshLog.url);
|
|
60
|
+
this.log('Request Method : %s', meshLog.requestMethod);
|
|
61
|
+
this.log('Request Status : %s', meshLog.responseStatus);
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.message === 'LogNotFound') {
|
|
65
|
+
this.error(
|
|
66
|
+
`No logs found for RayID ${rayId}. Check the RayID and try again. RequestId: ${global.requestId}. Alternatively, you can use the following command to get all logs for a 30 minute time period: \naio api-mesh log-get-bulk --startTime YYYY-MM-DDTHH:MM:SSZ --endTime YYYY-MM-DDTHH:MM:SSZ --filename mesh_logs.csv`,
|
|
67
|
+
);
|
|
68
|
+
} else if (error.message === 'ServerError') {
|
|
69
|
+
this.error(
|
|
70
|
+
`Server error while fetching logs for RayId ${rayId}. Please try again later. RequestId: ${global.requestId}`,
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
this.error(
|
|
74
|
+
`Unable to get mesh logs. Please check the details and try again. If the error persists please contact support. RequestId: ${global.requestId}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
FetchLogsCommand.description = 'Get the Log of a given mesh by RayId';
|
|
82
|
+
|
|
83
|
+
module.exports = FetchLogsCommand;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const { Command } = require('@oclif/core');
|
|
2
|
+
|
|
3
|
+
const logger = require('../../classes/logger');
|
|
4
|
+
const { initSdk, initRequestId } = require('../../helpers');
|
|
5
|
+
const { ignoreCacheFlag, fileNameFlag } = require('../../utils');
|
|
6
|
+
const { getMeshId, listLogs } = require('../../lib/devConsole');
|
|
7
|
+
const { appendFileSync, existsSync } = require('fs');
|
|
8
|
+
const { ux } = require('@oclif/core/lib/cli-ux');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
require('dotenv').config();
|
|
12
|
+
class ListLogsCommand extends Command {
|
|
13
|
+
static flags = {
|
|
14
|
+
ignoreCache: ignoreCacheFlag,
|
|
15
|
+
filename: fileNameFlag,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
static enableJsonFlag = true;
|
|
19
|
+
|
|
20
|
+
async run() {
|
|
21
|
+
await initRequestId();
|
|
22
|
+
|
|
23
|
+
logger.info(`RequestId: ${global.requestId}`);
|
|
24
|
+
|
|
25
|
+
const { flags } = await this.parse(ListLogsCommand);
|
|
26
|
+
|
|
27
|
+
const { ignoreCache, filename } = await flags;
|
|
28
|
+
|
|
29
|
+
if (filename) {
|
|
30
|
+
if (path.extname(filename).toLowerCase() !== '.csv') {
|
|
31
|
+
this.error('Invalid file type. Provide a filename with a .csv extension.');
|
|
32
|
+
}
|
|
33
|
+
const file = path.resolve(process.cwd(), filename);
|
|
34
|
+
if (existsSync(file)) {
|
|
35
|
+
this.error(`File ${filename} already exists. Provide a new file name.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { imsOrgId, imsOrgCode, projectId, workspaceId, workspaceName } = await initSdk({
|
|
40
|
+
ignoreCache,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let meshId = null;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
meshId = await getMeshId(imsOrgId, projectId, workspaceId, workspaceName);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
this.error(
|
|
49
|
+
`Unable to get mesh ID. Check the details and try again. RequestId: ${global.requestId}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (meshId) {
|
|
53
|
+
try {
|
|
54
|
+
const logs = await listLogs(imsOrgCode, projectId, workspaceId, meshId, filename);
|
|
55
|
+
|
|
56
|
+
if (logs && logs.length > 0) {
|
|
57
|
+
// add a new line
|
|
58
|
+
this.log();
|
|
59
|
+
ux.table(
|
|
60
|
+
logs,
|
|
61
|
+
{
|
|
62
|
+
rayId: {
|
|
63
|
+
header: 'Ray ID',
|
|
64
|
+
minWidth: 15,
|
|
65
|
+
},
|
|
66
|
+
timestamp: {
|
|
67
|
+
header: 'Timestamp',
|
|
68
|
+
minWidth: 15,
|
|
69
|
+
},
|
|
70
|
+
responseStatus: {
|
|
71
|
+
header: 'Response Status',
|
|
72
|
+
minWidth: 15,
|
|
73
|
+
},
|
|
74
|
+
level: {
|
|
75
|
+
header: 'Level',
|
|
76
|
+
minWidth: 15,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
printLine: filename
|
|
81
|
+
? line => appendFileSync(filename, line + '\n')
|
|
82
|
+
: line => this.log(line),
|
|
83
|
+
csv: filename,
|
|
84
|
+
...flags,
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
this.log(
|
|
89
|
+
'No recent logs found. Alternatively, you can use the following command to get all logs for a 30 minute time period: \naio api-mesh log-get-bulk --startTime YYYY-MM-DDTHH:MM:SSZ --endTime YYYY-MM-DDTHH:MM:SSZ --filename mesh_logs.csv',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.error(`Failed to list recent logs, RequestId: ${global.requestId}`);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
this.error(
|
|
97
|
+
`Unable to get mesh config. No mesh found for Org(${imsOrgId}) -> Project(${projectId}) -> Workspace(${workspaceId}). Check the details and try again. RequestId: ${global.requestId}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ListLogsCommand.description = 'Get recent logs of requests made to the API Mesh.';
|
|
104
|
+
|
|
105
|
+
module.exports = ListLogsCommand;
|