@datadog/datadog-ci 2.16.1 → 2.17.0
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/dist/commands/git-metadata/git.d.ts +0 -1
- package/dist/commands/git-metadata/git.js +5 -20
- package/dist/commands/git-metadata/git.js.map +1 -1
- package/dist/commands/git-metadata/gitdb.js +31 -21
- package/dist/commands/git-metadata/gitdb.js.map +1 -1
- package/dist/commands/junit/api.js +16 -13
- package/dist/commands/junit/api.js.map +1 -1
- package/dist/commands/junit/interfaces.d.ts +4 -1
- package/dist/commands/junit/upload.d.ts +6 -0
- package/dist/commands/junit/upload.js +34 -12
- package/dist/commands/junit/upload.js.map +1 -1
- package/dist/commands/junit/utils.d.ts +1 -0
- package/dist/commands/junit/utils.js +11 -1
- package/dist/commands/junit/utils.js.map +1 -1
- package/dist/commands/lambda/cli.js +2 -1
- package/dist/commands/lambda/cli.js.map +1 -1
- package/dist/commands/lambda/constants.d.ts +1 -0
- package/dist/commands/lambda/constants.js +17 -1
- package/dist/commands/lambda/constants.js.map +1 -1
- package/dist/commands/lambda/flare.d.ts +127 -0
- package/dist/commands/lambda/flare.js +656 -0
- package/dist/commands/lambda/flare.js.map +1 -0
- package/dist/commands/lambda/functions/commons.d.ts +14 -1
- package/dist/commands/lambda/functions/commons.js +53 -11
- package/dist/commands/lambda/functions/commons.js.map +1 -1
- package/dist/commands/lambda/instrument.js +43 -41
- package/dist/commands/lambda/instrument.js.map +1 -1
- package/dist/commands/lambda/renderers/common-renderer.d.ts +42 -0
- package/dist/commands/lambda/renderers/common-renderer.js +51 -0
- package/dist/commands/lambda/renderers/common-renderer.js.map +1 -0
- package/dist/commands/lambda/renderers/flare-renderer.d.ts +9 -0
- package/dist/commands/lambda/renderers/flare-renderer.js +18 -0
- package/dist/commands/lambda/renderers/flare-renderer.js.map +1 -0
- package/dist/commands/lambda/{renderer.d.ts → renderers/instrument-uninstrument-renderer.d.ts} +2 -44
- package/dist/commands/lambda/{renderer.js → renderers/instrument-uninstrument-renderer.js} +30 -76
- package/dist/commands/lambda/renderers/instrument-uninstrument-renderer.js.map +1 -0
- package/dist/commands/lambda/uninstrument.js +31 -29
- package/dist/commands/lambda/uninstrument.js.map +1 -1
- package/dist/commands/react-native/renderer.js +2 -0
- package/dist/commands/react-native/renderer.js.map +1 -1
- package/dist/commands/sarif/api.js +1 -1
- package/dist/commands/sarif/api.js.map +1 -1
- package/dist/commands/synthetics/run-tests-command.d.ts +1 -0
- package/dist/commands/synthetics/run-tests-command.js +9 -2
- package/dist/commands/synthetics/run-tests-command.js.map +1 -1
- package/dist/commands/synthetics/tunnel/tunnel.d.ts +2 -2
- package/dist/commands/synthetics/tunnel/tunnel.js.map +1 -1
- package/dist/commands/synthetics/tunnel/websocket.d.ts +2 -2
- package/dist/commands/synthetics/tunnel/websocket.js.map +1 -1
- package/dist/commands/synthetics/utils.d.ts +1 -0
- package/dist/commands/synthetics/utils.js +21 -4
- package/dist/commands/synthetics/utils.js.map +1 -1
- package/dist/helpers/ci.d.ts +1 -0
- package/dist/helpers/file.d.ts +1 -1
- package/dist/helpers/file.js +3 -6
- package/dist/helpers/file.js.map +1 -1
- package/dist/helpers/git/format-git-sourcemaps-data.js +2 -2
- package/dist/helpers/git/format-git-sourcemaps-data.js.map +1 -1
- package/dist/helpers/git/get-git-data.d.ts +1 -0
- package/dist/helpers/git/get-git-data.js +14 -4
- package/dist/helpers/git/get-git-data.js.map +1 -1
- package/dist/helpers/interfaces.d.ts +2 -2
- package/dist/helpers/metrics.js +2 -2
- package/dist/helpers/metrics.js.map +1 -1
- package/dist/helpers/tags.d.ts +1 -0
- package/dist/helpers/tags.js +2 -1
- package/dist/helpers/tags.js.map +1 -1
- package/dist/helpers/utils.d.ts +3 -2
- package/dist/helpers/utils.js +26 -3
- package/dist/helpers/utils.js.map +1 -1
- package/package.json +4 -3
- package/dist/commands/lambda/renderer.js.map +0 -1
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
5
|
+
}) : (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
o[k2] = m[k];
|
|
8
|
+
}));
|
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
11
|
+
}) : function(o, v) {
|
|
12
|
+
o["default"] = v;
|
|
13
|
+
});
|
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
15
|
+
if (mod && mod.__esModule) return mod;
|
|
16
|
+
var result = {};
|
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
18
|
+
__setModuleDefault(result, mod);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
22
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
23
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
24
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
25
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
26
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
|
+
};
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.sendToDatadog = exports.getEndpointUrl = exports.zipContents = exports.sleep = exports.convertToCSV = exports.writeFile = exports.getTags = exports.getAllLogs = exports.getLogEvents = exports.getLogStreamNames = exports.createDirectories = exports.deleteFolder = exports.maskConfig = exports.validateStartEndFlags = exports.LambdaFlareCommand = void 0;
|
|
35
|
+
const fs = __importStar(require("fs"));
|
|
36
|
+
const path = __importStar(require("path"));
|
|
37
|
+
const util_1 = __importDefault(require("util"));
|
|
38
|
+
const client_cloudwatch_logs_1 = require("@aws-sdk/client-cloudwatch-logs");
|
|
39
|
+
const client_lambda_1 = require("@aws-sdk/client-lambda");
|
|
40
|
+
const axios_1 = __importDefault(require("axios"));
|
|
41
|
+
const clipanion_1 = require("clipanion");
|
|
42
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
43
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
44
|
+
const jszip_1 = __importDefault(require("jszip"));
|
|
45
|
+
const constants_1 = require("../../constants");
|
|
46
|
+
const validation_1 = require("../../helpers/validation");
|
|
47
|
+
const constants_2 = require("./constants");
|
|
48
|
+
const commons_1 = require("./functions/commons");
|
|
49
|
+
const prompt_1 = require("./prompt");
|
|
50
|
+
const commonRenderer = __importStar(require("./renderers/common-renderer"));
|
|
51
|
+
const flareRenderer = __importStar(require("./renderers/flare-renderer"));
|
|
52
|
+
const { version } = require('../../../package.json');
|
|
53
|
+
const ENDPOINT_PATH = '/api/ui/support/serverless/flare';
|
|
54
|
+
const FLARE_OUTPUT_DIRECTORY = '.datadog-ci';
|
|
55
|
+
const LOGS_DIRECTORY = 'logs';
|
|
56
|
+
const FUNCTION_CONFIG_FILE_NAME = 'function_config.json';
|
|
57
|
+
const TAGS_FILE_NAME = 'tags.json';
|
|
58
|
+
const ZIP_FILE_NAME = 'lambda-flare-output.zip';
|
|
59
|
+
const MAX_LOG_STREAMS = 50;
|
|
60
|
+
const DEFAULT_LOG_STREAMS = 3;
|
|
61
|
+
const MAX_LOG_EVENTS_PER_STREAM = 1000;
|
|
62
|
+
class LambdaFlareCommand extends clipanion_1.Command {
|
|
63
|
+
constructor() {
|
|
64
|
+
super(...arguments);
|
|
65
|
+
this.isDryRun = false;
|
|
66
|
+
this.withLogs = false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Entry point for the `lambda flare` command.
|
|
70
|
+
* Gathers lambda function configuration and sends it to Datadog.
|
|
71
|
+
* @returns 0 if the command ran successfully, 1 otherwise.
|
|
72
|
+
*/
|
|
73
|
+
execute() {
|
|
74
|
+
var _a, _b, _c;
|
|
75
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
76
|
+
this.context.stdout.write(flareRenderer.renderLambdaFlareHeader(this.isDryRun));
|
|
77
|
+
// Validate function name
|
|
78
|
+
if (this.functionName === undefined) {
|
|
79
|
+
this.context.stderr.write(commonRenderer.renderError('No function name specified. [-f,--function]'));
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
const errorMessages = [];
|
|
83
|
+
// Validate region
|
|
84
|
+
const region = (_b = (_a = commons_1.getRegion(this.functionName)) !== null && _a !== void 0 ? _a : this.region) !== null && _b !== void 0 ? _b : process.env[constants_2.AWS_DEFAULT_REGION_ENV_VAR];
|
|
85
|
+
if (region === undefined) {
|
|
86
|
+
errorMessages.push(commonRenderer.renderNoDefaultRegionSpecifiedError());
|
|
87
|
+
}
|
|
88
|
+
// Validate Datadog API key
|
|
89
|
+
this.apiKey = (_c = process.env[constants_2.CI_API_KEY_ENV_VAR]) !== null && _c !== void 0 ? _c : process.env[constants_2.API_KEY_ENV_VAR];
|
|
90
|
+
if (this.apiKey === undefined) {
|
|
91
|
+
errorMessages.push(commonRenderer.renderError('No Datadog API key specified. Set an API key with the DATADOG_API_KEY environment variable.'));
|
|
92
|
+
}
|
|
93
|
+
if (!this.isDryRun) {
|
|
94
|
+
// Validate case ID
|
|
95
|
+
if (this.caseId === undefined) {
|
|
96
|
+
errorMessages.push(commonRenderer.renderError('No case ID specified. [-c,--case-id]'));
|
|
97
|
+
}
|
|
98
|
+
// Validate email
|
|
99
|
+
if (this.email === undefined) {
|
|
100
|
+
errorMessages.push(commonRenderer.renderError('No email specified. [-e,--email]'));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Validate start/end flags if both are specified
|
|
104
|
+
let startMillis;
|
|
105
|
+
let endMillis;
|
|
106
|
+
try {
|
|
107
|
+
;
|
|
108
|
+
[startMillis, endMillis] = exports.validateStartEndFlags(this.start, this.end);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err instanceof Error) {
|
|
112
|
+
errorMessages.push(commonRenderer.renderError(err.message));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (errorMessages.length > 0) {
|
|
116
|
+
for (const message of errorMessages) {
|
|
117
|
+
this.context.stderr.write(message);
|
|
118
|
+
}
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
// Get AWS credentials
|
|
122
|
+
this.context.stdout.write('\n🔑 Getting AWS credentials...\n');
|
|
123
|
+
try {
|
|
124
|
+
this.credentials = yield commons_1.getAWSCredentials();
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
if (err instanceof Error) {
|
|
128
|
+
this.context.stderr.write(commonRenderer.renderError(err.message));
|
|
129
|
+
}
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
if (this.credentials === undefined) {
|
|
133
|
+
this.context.stdout.write('\n' + commonRenderer.renderNoAWSCredentialsFound());
|
|
134
|
+
try {
|
|
135
|
+
yield prompt_1.requestAWSCredentials();
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (err instanceof Error) {
|
|
139
|
+
this.context.stderr.write(commonRenderer.renderError(err.message));
|
|
140
|
+
}
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Get and print Lambda function configuration
|
|
145
|
+
this.context.stdout.write('\n🔍 Fetching Lambda function configuration...\n');
|
|
146
|
+
const lambdaClientConfig = {
|
|
147
|
+
region,
|
|
148
|
+
credentials: this.credentials,
|
|
149
|
+
};
|
|
150
|
+
const lambdaClient = new client_lambda_1.LambdaClient(lambdaClientConfig);
|
|
151
|
+
let config;
|
|
152
|
+
try {
|
|
153
|
+
config = yield commons_1.getLambdaFunctionConfig(lambdaClient, this.functionName);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (err instanceof Error) {
|
|
157
|
+
this.context.stderr.write(commonRenderer.renderError(`Unable to get Lambda function configuration: ${err.message}`));
|
|
158
|
+
}
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
config = exports.maskConfig(config);
|
|
162
|
+
const configStr = util_1.default.inspect(config, false, undefined, true);
|
|
163
|
+
this.context.stdout.write(`\n${configStr}\n`);
|
|
164
|
+
// Get tags
|
|
165
|
+
this.context.stdout.write('\n🏷 Getting Resource Tags...\n');
|
|
166
|
+
let tags;
|
|
167
|
+
try {
|
|
168
|
+
tags = yield exports.getTags(lambdaClient, region, config.FunctionArn);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
if (err instanceof Error) {
|
|
172
|
+
this.context.stderr.write(commonRenderer.renderError(err.message));
|
|
173
|
+
}
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const tagsLength = Object.keys(tags).length;
|
|
177
|
+
if (tagsLength === 0) {
|
|
178
|
+
this.context.stdout.write(commonRenderer.renderSoftWarning(`No resource tags were found.`));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.context.stdout.write(`✅ Found ${tagsLength} resource tags.\n`);
|
|
182
|
+
}
|
|
183
|
+
// Get CloudWatch logs
|
|
184
|
+
let logs = new Map();
|
|
185
|
+
if (this.withLogs) {
|
|
186
|
+
this.context.stdout.write('\n☁️ Getting CloudWatch logs...\n');
|
|
187
|
+
try {
|
|
188
|
+
logs = yield exports.getAllLogs(region, this.functionName, startMillis, endMillis);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err instanceof Error) {
|
|
192
|
+
this.context.stderr.write(commonRenderer.renderError(err.message));
|
|
193
|
+
}
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
// CloudWatch messages
|
|
199
|
+
if (this.withLogs) {
|
|
200
|
+
let message = '\n✅ Found log streams:\n';
|
|
201
|
+
if (logs.size === 0) {
|
|
202
|
+
message = commonRenderer.renderSoftWarning('No CloudWatch log streams were found. Logs will not be retrieved or sent.');
|
|
203
|
+
}
|
|
204
|
+
this.context.stdout.write(message);
|
|
205
|
+
for (const [logStreamName, logEvents] of logs) {
|
|
206
|
+
let warningMessage = '\n';
|
|
207
|
+
if (logEvents.length === 0) {
|
|
208
|
+
warningMessage = ' - ' + commonRenderer.renderSoftWarning('No log events found in this stream');
|
|
209
|
+
}
|
|
210
|
+
this.context.stdout.write(`• ${logStreamName}${warningMessage}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Create folders
|
|
214
|
+
this.context.stdout.write('\n💾 Saving files...\n');
|
|
215
|
+
const rootFolderPath = path.join(process.cwd(), FLARE_OUTPUT_DIRECTORY);
|
|
216
|
+
const logsFolderPath = path.join(rootFolderPath, LOGS_DIRECTORY);
|
|
217
|
+
if (fs.existsSync(rootFolderPath)) {
|
|
218
|
+
exports.deleteFolder(rootFolderPath);
|
|
219
|
+
}
|
|
220
|
+
exports.createDirectories(rootFolderPath, logsFolderPath, logs);
|
|
221
|
+
// Write files
|
|
222
|
+
const configFilePath = path.join(rootFolderPath, FUNCTION_CONFIG_FILE_NAME);
|
|
223
|
+
exports.writeFile(configFilePath, JSON.stringify(config, undefined, 2));
|
|
224
|
+
this.context.stdout.write(`• Saved function config to ${configFilePath}\n`);
|
|
225
|
+
if (tagsLength > 0) {
|
|
226
|
+
const tagsFilePath = path.join(rootFolderPath, TAGS_FILE_NAME);
|
|
227
|
+
exports.writeFile(tagsFilePath, JSON.stringify(tags, undefined, 2));
|
|
228
|
+
this.context.stdout.write(`• Saved tags to ${tagsFilePath}\n`);
|
|
229
|
+
}
|
|
230
|
+
for (const [logStreamName, logEvents] of logs) {
|
|
231
|
+
if (logEvents.length === 0) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const logFilePath = path.join(logsFolderPath, `${logStreamName.split('/').join('-')}.csv`);
|
|
235
|
+
const data = exports.convertToCSV(logEvents);
|
|
236
|
+
exports.writeFile(logFilePath, data);
|
|
237
|
+
this.context.stdout.write(`• Saved logs to ${logFilePath}\n`);
|
|
238
|
+
// Sleep for 1 millisecond so creation times are different
|
|
239
|
+
// This allows the logs to be sorted by creation time by the support team
|
|
240
|
+
yield exports.sleep(1);
|
|
241
|
+
}
|
|
242
|
+
// Exit if dry run
|
|
243
|
+
const outputMsg = `\nℹ️ Your output files are located at: ${rootFolderPath}\n\n`;
|
|
244
|
+
if (this.isDryRun) {
|
|
245
|
+
this.context.stdout.write('\n🚫 The flare files were not sent as it was executed in dry run mode.');
|
|
246
|
+
this.context.stdout.write(outputMsg);
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
// Confirm before sending
|
|
250
|
+
this.context.stdout.write('\n');
|
|
251
|
+
const answer = yield inquirer_1.default.prompt(prompt_1.confirmationQuestion('Are you sure you want to send the flare file to Datadog Support?'));
|
|
252
|
+
if (!answer.confirmation) {
|
|
253
|
+
this.context.stdout.write('\n🚫 The flare files were not sent based on your selection.');
|
|
254
|
+
this.context.stdout.write(outputMsg);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
// Zip folder
|
|
258
|
+
const zipPath = path.join(rootFolderPath, ZIP_FILE_NAME);
|
|
259
|
+
yield exports.zipContents(rootFolderPath, zipPath);
|
|
260
|
+
// Send to Datadog
|
|
261
|
+
this.context.stdout.write('\n🚀 Sending to Datadog Support...\n');
|
|
262
|
+
yield exports.sendToDatadog(zipPath, this.caseId, this.email, this.apiKey, rootFolderPath);
|
|
263
|
+
this.context.stdout.write('\n✅ Successfully sent flare file to Datadog Support!\n');
|
|
264
|
+
// Delete contents
|
|
265
|
+
exports.deleteFolder(rootFolderPath);
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
if (err instanceof Error) {
|
|
269
|
+
this.context.stderr.write(commonRenderer.renderError(err.message));
|
|
270
|
+
}
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
return 0;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
exports.LambdaFlareCommand = LambdaFlareCommand;
|
|
278
|
+
/**
|
|
279
|
+
* Validate the start and end flags and adds error messages if found
|
|
280
|
+
* @param start start time as a string
|
|
281
|
+
* @param end end time as a string
|
|
282
|
+
* @throws error if start or end are not valid numbers
|
|
283
|
+
* @returns [startMillis, endMillis] as numbers or [undefined, undefined] if both are undefined
|
|
284
|
+
*/
|
|
285
|
+
const validateStartEndFlags = (start, end) => {
|
|
286
|
+
if (!start && !end) {
|
|
287
|
+
return [undefined, undefined];
|
|
288
|
+
}
|
|
289
|
+
if (!start) {
|
|
290
|
+
throw Error('Start time is required when end time is specified. [--start]');
|
|
291
|
+
}
|
|
292
|
+
if (!end) {
|
|
293
|
+
throw Error('End time is required when start time is specified. [--end]');
|
|
294
|
+
}
|
|
295
|
+
const startMillis = Number(start);
|
|
296
|
+
let endMillis = Number(end);
|
|
297
|
+
if (isNaN(startMillis)) {
|
|
298
|
+
throw Error(`Start time must be a time in milliseconds since Unix Epoch. '${start}' is not a number.`);
|
|
299
|
+
}
|
|
300
|
+
if (isNaN(endMillis)) {
|
|
301
|
+
throw Error(`End time must be a time in milliseconds since Unix Epoch. '${end}' is not a number.`);
|
|
302
|
+
}
|
|
303
|
+
// Required for AWS SDK to work correctly
|
|
304
|
+
endMillis = Math.min(endMillis, Date.now());
|
|
305
|
+
if (startMillis >= endMillis) {
|
|
306
|
+
throw Error('Start time must be before end time.');
|
|
307
|
+
}
|
|
308
|
+
return [startMillis, endMillis];
|
|
309
|
+
};
|
|
310
|
+
exports.validateStartEndFlags = validateStartEndFlags;
|
|
311
|
+
/**
|
|
312
|
+
* Mask the environment variables in a Lambda function configuration
|
|
313
|
+
* @param config
|
|
314
|
+
*/
|
|
315
|
+
const maskConfig = (config) => {
|
|
316
|
+
var _a;
|
|
317
|
+
const environmentVariables = (_a = config.Environment) === null || _a === void 0 ? void 0 : _a.Variables;
|
|
318
|
+
if (!environmentVariables) {
|
|
319
|
+
return config;
|
|
320
|
+
}
|
|
321
|
+
const replacer = commons_1.maskStringifiedEnvVar(environmentVariables);
|
|
322
|
+
const stringifiedConfig = JSON.stringify(config, replacer);
|
|
323
|
+
return JSON.parse(stringifiedConfig);
|
|
324
|
+
};
|
|
325
|
+
exports.maskConfig = maskConfig;
|
|
326
|
+
/**
|
|
327
|
+
* Delete a folder and all its contents
|
|
328
|
+
* @param folderPath the folder to delete
|
|
329
|
+
* @throws Error if the deletion fails
|
|
330
|
+
*/
|
|
331
|
+
const deleteFolder = (folderPath) => {
|
|
332
|
+
try {
|
|
333
|
+
fs.rmSync(folderPath, { recursive: true, force: true });
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
if (err instanceof Error) {
|
|
337
|
+
throw Error(`Failed to delete files located at ${folderPath}: ${err.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
exports.deleteFolder = deleteFolder;
|
|
342
|
+
/**
|
|
343
|
+
* Creates the root folder and the logs sub-folder
|
|
344
|
+
* @param rootFolderPath path to the root folder
|
|
345
|
+
* @param logsFolderPath path to the logs folder
|
|
346
|
+
* @param logs array of logs
|
|
347
|
+
* @throws Error if the root folder cannot be deleted or folders cannot be created
|
|
348
|
+
*/
|
|
349
|
+
const createDirectories = (rootFolderPath, logsFolderPath, logs) => {
|
|
350
|
+
try {
|
|
351
|
+
fs.mkdirSync(rootFolderPath);
|
|
352
|
+
if (logs.size > 0) {
|
|
353
|
+
fs.mkdirSync(logsFolderPath);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
if (err instanceof Error) {
|
|
358
|
+
throw Error(`Unable to create directories: ${err.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
exports.createDirectories = createDirectories;
|
|
363
|
+
/**
|
|
364
|
+
* Gets the LOG_STREAM_COUNT latest log stream names, sorted by last event time
|
|
365
|
+
* @param cwlClient CloudWatch Logs client
|
|
366
|
+
* @param logGroupName name of the log group
|
|
367
|
+
* @param startMillis start time in milliseconds or undefined if no start time is specified
|
|
368
|
+
* @param endMillis end time in milliseconds or undefined if no end time is specified
|
|
369
|
+
* @returns an array of the last LOG_STREAM_COUNT log stream names or an empty array if no log streams are found
|
|
370
|
+
* @throws Error if the log streams cannot be retrieved
|
|
371
|
+
*/
|
|
372
|
+
const getLogStreamNames = (cwlClient, logGroupName, startMillis, endMillis) => __awaiter(void 0, void 0, void 0, function* () {
|
|
373
|
+
const config = {
|
|
374
|
+
logGroupName,
|
|
375
|
+
descending: true,
|
|
376
|
+
orderBy: client_cloudwatch_logs_1.OrderBy.LastEventTime,
|
|
377
|
+
limit: DEFAULT_LOG_STREAMS,
|
|
378
|
+
};
|
|
379
|
+
const rangeSpecified = startMillis !== undefined && endMillis !== undefined;
|
|
380
|
+
if (rangeSpecified) {
|
|
381
|
+
config.limit = MAX_LOG_STREAMS;
|
|
382
|
+
}
|
|
383
|
+
const command = new client_cloudwatch_logs_1.DescribeLogStreamsCommand(config);
|
|
384
|
+
const response = yield cwlClient.send(command);
|
|
385
|
+
const logStreams = response.logStreams;
|
|
386
|
+
if (logStreams === undefined || logStreams.length === 0) {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
const output = [];
|
|
390
|
+
for (const logStream of logStreams) {
|
|
391
|
+
const logStreamName = logStream.logStreamName;
|
|
392
|
+
if (!logStreamName) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (rangeSpecified) {
|
|
396
|
+
const firstEventTime = logStream.firstEventTimestamp;
|
|
397
|
+
const lastEventTime = logStream.lastEventTimestamp;
|
|
398
|
+
if (lastEventTime && lastEventTime < startMillis) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (firstEventTime && firstEventTime > endMillis) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
output.push(logStreamName);
|
|
406
|
+
}
|
|
407
|
+
// Reverse array so the oldest log is created first, so Support Staff can sort by creation time
|
|
408
|
+
return output.reverse();
|
|
409
|
+
});
|
|
410
|
+
exports.getLogStreamNames = getLogStreamNames;
|
|
411
|
+
/**
|
|
412
|
+
* Gets the log events for a log stream
|
|
413
|
+
* @param cwlClient
|
|
414
|
+
* @param logGroupName
|
|
415
|
+
* @param logStreamName
|
|
416
|
+
* @param startMillis
|
|
417
|
+
* @param endMillis
|
|
418
|
+
* @returns the log events or an empty array if no log events are found
|
|
419
|
+
* @throws Error if the log events cannot be retrieved
|
|
420
|
+
*/
|
|
421
|
+
const getLogEvents = (cwlClient, logGroupName, logStreamName, startMillis, endMillis) => __awaiter(void 0, void 0, void 0, function* () {
|
|
422
|
+
const config = {
|
|
423
|
+
logGroupName,
|
|
424
|
+
logStreamName,
|
|
425
|
+
limit: MAX_LOG_EVENTS_PER_STREAM,
|
|
426
|
+
};
|
|
427
|
+
if (startMillis !== undefined && endMillis !== undefined) {
|
|
428
|
+
config.startTime = startMillis;
|
|
429
|
+
config.endTime = endMillis;
|
|
430
|
+
}
|
|
431
|
+
const command = new client_cloudwatch_logs_1.GetLogEventsCommand(config);
|
|
432
|
+
const response = yield cwlClient.send(command);
|
|
433
|
+
const logEvents = response.events;
|
|
434
|
+
if (logEvents === undefined) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
return logEvents;
|
|
438
|
+
});
|
|
439
|
+
exports.getLogEvents = getLogEvents;
|
|
440
|
+
/**
|
|
441
|
+
* Gets all CloudWatch logs for a function
|
|
442
|
+
* @param region
|
|
443
|
+
* @param functionName
|
|
444
|
+
* @param startMillis start time in milliseconds or undefined if no end time is specified
|
|
445
|
+
* @param endMillis end time in milliseconds or undefined if no end time is specified
|
|
446
|
+
* @returns a map of log stream names to log events or an empty map if no logs are found
|
|
447
|
+
*/
|
|
448
|
+
const getAllLogs = (region, functionName, startMillis, endMillis) => __awaiter(void 0, void 0, void 0, function* () {
|
|
449
|
+
const logs = new Map();
|
|
450
|
+
const cwlClient = new client_cloudwatch_logs_1.CloudWatchLogsClient({ region });
|
|
451
|
+
if (functionName.startsWith('arn:aws')) {
|
|
452
|
+
functionName = functionName.split(':')[6];
|
|
453
|
+
}
|
|
454
|
+
const logGroupName = `/aws/lambda/${functionName}`;
|
|
455
|
+
let logStreamNames;
|
|
456
|
+
try {
|
|
457
|
+
logStreamNames = yield exports.getLogStreamNames(cwlClient, logGroupName, startMillis, endMillis);
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
const msg = err instanceof Error ? err.message : '';
|
|
461
|
+
throw new Error(`Unable to get log streams: ${msg}`);
|
|
462
|
+
}
|
|
463
|
+
for (const logStreamName of logStreamNames) {
|
|
464
|
+
let logEvents;
|
|
465
|
+
try {
|
|
466
|
+
logEvents = yield exports.getLogEvents(cwlClient, logGroupName, logStreamName, startMillis, endMillis);
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
const msg = err instanceof Error ? err.message : '';
|
|
470
|
+
throw new Error(`Unable to get log events for stream ${logStreamName}: ${msg}`);
|
|
471
|
+
}
|
|
472
|
+
logs.set(logStreamName, logEvents);
|
|
473
|
+
}
|
|
474
|
+
return logs;
|
|
475
|
+
});
|
|
476
|
+
exports.getAllLogs = getAllLogs;
|
|
477
|
+
/**
|
|
478
|
+
* Gets the tags for a function
|
|
479
|
+
* @param lambdaClient
|
|
480
|
+
* @param region
|
|
481
|
+
* @param arn
|
|
482
|
+
* @returns the tags or an empty object if no tags are found
|
|
483
|
+
* @throws Error if the tags cannot be retrieved
|
|
484
|
+
*/
|
|
485
|
+
const getTags = (lambdaClient, region, arn) => __awaiter(void 0, void 0, void 0, function* () {
|
|
486
|
+
var _a;
|
|
487
|
+
if (!arn.startsWith('arn:aws')) {
|
|
488
|
+
throw Error(`Invalid function ARN: ${arn}`);
|
|
489
|
+
}
|
|
490
|
+
const command = new client_lambda_1.ListTagsCommand({
|
|
491
|
+
Resource: arn,
|
|
492
|
+
});
|
|
493
|
+
try {
|
|
494
|
+
const response = yield lambdaClient.send(command);
|
|
495
|
+
return (_a = response.Tags) !== null && _a !== void 0 ? _a : {};
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
let message = '';
|
|
499
|
+
if (err instanceof Error) {
|
|
500
|
+
message = err.message;
|
|
501
|
+
}
|
|
502
|
+
throw Error(`Unable to get resource tags: ${message}`);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
exports.getTags = getTags;
|
|
506
|
+
/**
|
|
507
|
+
* Write the function config to a file
|
|
508
|
+
* @param filePath path to the file
|
|
509
|
+
* @param data the data to write
|
|
510
|
+
* @throws Error if the file cannot be written
|
|
511
|
+
*/
|
|
512
|
+
const writeFile = (filePath, data) => {
|
|
513
|
+
try {
|
|
514
|
+
fs.writeFileSync(filePath, data);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
if (err instanceof Error) {
|
|
518
|
+
throw Error(`Unable to create function configuration file: ${err.message}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
exports.writeFile = writeFile;
|
|
523
|
+
/**
|
|
524
|
+
* Convert the log events to a CSV string
|
|
525
|
+
* @param logEvents array of log events
|
|
526
|
+
* @returns the CSV string
|
|
527
|
+
*/
|
|
528
|
+
const convertToCSV = (logEvents) => {
|
|
529
|
+
var _a, _b;
|
|
530
|
+
const rows = [['timestamp', 'datetime', 'message']];
|
|
531
|
+
for (const logEvent of logEvents) {
|
|
532
|
+
const timestamp = `"${(_a = logEvent.timestamp) !== null && _a !== void 0 ? _a : ''}"`;
|
|
533
|
+
let datetime = '';
|
|
534
|
+
if (logEvent.timestamp) {
|
|
535
|
+
const date = new Date(logEvent.timestamp);
|
|
536
|
+
datetime = date.toISOString().replace('T', ' ').replace('Z', '');
|
|
537
|
+
}
|
|
538
|
+
const message = `"${(_b = logEvent.message) !== null && _b !== void 0 ? _b : ''}"`;
|
|
539
|
+
rows.push([timestamp, datetime, message]);
|
|
540
|
+
}
|
|
541
|
+
return rows.join('\n');
|
|
542
|
+
};
|
|
543
|
+
exports.convertToCSV = convertToCSV;
|
|
544
|
+
/**
|
|
545
|
+
* @param ms number of milliseconds to sleep
|
|
546
|
+
*/
|
|
547
|
+
const sleep = (ms) => __awaiter(void 0, void 0, void 0, function* () {
|
|
548
|
+
yield new Promise((resolve) => setTimeout(resolve, ms));
|
|
549
|
+
});
|
|
550
|
+
exports.sleep = sleep;
|
|
551
|
+
/**
|
|
552
|
+
* Zip the contents of the flare folder
|
|
553
|
+
* @param rootFolderPath path to the root folder to zip
|
|
554
|
+
* @param zipPath path to save the zip file
|
|
555
|
+
* @throws Error if the zip fails
|
|
556
|
+
*/
|
|
557
|
+
const zipContents = (rootFolderPath, zipPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
558
|
+
const zip = new jszip_1.default();
|
|
559
|
+
const addFolderToZip = (folderPath) => {
|
|
560
|
+
if (!fs.existsSync(folderPath)) {
|
|
561
|
+
throw Error(`Folder does not exist: ${folderPath}`);
|
|
562
|
+
}
|
|
563
|
+
const folder = fs.statSync(folderPath);
|
|
564
|
+
if (!folder.isDirectory()) {
|
|
565
|
+
throw Error(`Path is not a directory: ${folderPath}`);
|
|
566
|
+
}
|
|
567
|
+
const contents = fs.readdirSync(folderPath);
|
|
568
|
+
for (const item of contents) {
|
|
569
|
+
const fullPath = path.join(folderPath, item);
|
|
570
|
+
const file = fs.statSync(fullPath);
|
|
571
|
+
if (file.isDirectory()) {
|
|
572
|
+
addFolderToZip(fullPath);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
const data = fs.readFileSync(fullPath);
|
|
576
|
+
zip.file(path.relative(rootFolderPath, fullPath), data);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
try {
|
|
581
|
+
addFolderToZip(rootFolderPath);
|
|
582
|
+
const zipContent = yield zip.generateAsync({ type: 'nodebuffer' });
|
|
583
|
+
fs.writeFileSync(zipPath, zipContent);
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
if (err instanceof Error) {
|
|
587
|
+
throw Error(`Unable to zip the flare files: ${err.message}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
exports.zipContents = zipContents;
|
|
592
|
+
/**
|
|
593
|
+
* Calculates the full endpoint URL
|
|
594
|
+
* @throws Error if the site is invalid
|
|
595
|
+
* @returns the full endpoint URL
|
|
596
|
+
*/
|
|
597
|
+
const getEndpointUrl = () => {
|
|
598
|
+
var _a, _b;
|
|
599
|
+
const baseUrl = (_b = (_a = process.env[constants_2.CI_SITE_ENV_VAR]) !== null && _a !== void 0 ? _a : process.env[constants_2.SITE_ENV_VAR]) !== null && _b !== void 0 ? _b : constants_1.DATADOG_SITE_US1;
|
|
600
|
+
// The DNS doesn't redirect to the proper endpoint when a subdomain is not present in the baseUrl.
|
|
601
|
+
// There is a DNS inconsistency
|
|
602
|
+
let endpointUrl = baseUrl;
|
|
603
|
+
if ([constants_1.DATADOG_SITE_US1, constants_1.DATADOG_SITE_EU1, constants_1.DATADOG_SITE_GOV].includes(baseUrl)) {
|
|
604
|
+
endpointUrl = 'app.' + baseUrl;
|
|
605
|
+
}
|
|
606
|
+
if (!validation_1.isValidDatadogSite(baseUrl)) {
|
|
607
|
+
throw Error(`Invalid site: ${baseUrl}. Must be one of: ${constants_1.DATADOG_SITES.join(', ')}`);
|
|
608
|
+
}
|
|
609
|
+
return 'https://' + endpointUrl + ENDPOINT_PATH;
|
|
610
|
+
};
|
|
611
|
+
exports.getEndpointUrl = getEndpointUrl;
|
|
612
|
+
/**
|
|
613
|
+
* Send the zip file to Datadog support
|
|
614
|
+
* @param zipPath
|
|
615
|
+
* @param caseId
|
|
616
|
+
* @param email
|
|
617
|
+
* @param apiKey
|
|
618
|
+
* @param rootFolderPath
|
|
619
|
+
* @throws Error if the request fails
|
|
620
|
+
*/
|
|
621
|
+
const sendToDatadog = (zipPath, caseId, email, apiKey, rootFolderPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
622
|
+
var _b, _c, _d;
|
|
623
|
+
const endpointUrl = exports.getEndpointUrl();
|
|
624
|
+
const form = new form_data_1.default();
|
|
625
|
+
form.append('case_id', caseId);
|
|
626
|
+
form.append('flare_file', fs.createReadStream(zipPath));
|
|
627
|
+
form.append('datadog_ci_version', version);
|
|
628
|
+
form.append('email', email);
|
|
629
|
+
const headerConfig = {
|
|
630
|
+
headers: Object.assign(Object.assign({}, form.getHeaders()), { 'DD-API-KEY': apiKey }),
|
|
631
|
+
};
|
|
632
|
+
try {
|
|
633
|
+
yield axios_1.default.post(endpointUrl, form, headerConfig);
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
// Ensure the root folder is deleted if the request fails
|
|
637
|
+
exports.deleteFolder(rootFolderPath);
|
|
638
|
+
if (axios_1.default.isAxiosError(err)) {
|
|
639
|
+
const errResponse = (_c = (_b = err.response) === null || _b === void 0 ? void 0 : _b.data.error) !== null && _c !== void 0 ? _c : '';
|
|
640
|
+
const errorMessage = (_d = err.message) !== null && _d !== void 0 ? _d : '';
|
|
641
|
+
throw Error(`Failed to send flare file to Datadog Support: ${errorMessage}. ${errResponse}\n`);
|
|
642
|
+
}
|
|
643
|
+
throw err;
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
exports.sendToDatadog = sendToDatadog;
|
|
647
|
+
LambdaFlareCommand.addPath('lambda', 'flare');
|
|
648
|
+
LambdaFlareCommand.addOption('isDryRun', clipanion_1.Command.Boolean('-d,--dry'));
|
|
649
|
+
LambdaFlareCommand.addOption('withLogs', clipanion_1.Command.Boolean('--with-logs'));
|
|
650
|
+
LambdaFlareCommand.addOption('functionName', clipanion_1.Command.String('-f,--function'));
|
|
651
|
+
LambdaFlareCommand.addOption('region', clipanion_1.Command.String('-r,--region'));
|
|
652
|
+
LambdaFlareCommand.addOption('caseId', clipanion_1.Command.String('-c,--case-id'));
|
|
653
|
+
LambdaFlareCommand.addOption('email', clipanion_1.Command.String('-e,--email'));
|
|
654
|
+
LambdaFlareCommand.addOption('start', clipanion_1.Command.String('--start'));
|
|
655
|
+
LambdaFlareCommand.addOption('end', clipanion_1.Command.String('--end'));
|
|
656
|
+
//# sourceMappingURL=flare.js.map
|