@depup/artillery 2.0.30-depup.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.
Files changed (90) hide show
  1. package/README.md +63 -0
  2. package/bin/run +29 -0
  3. package/bin/run.cmd +3 -0
  4. package/changes.json +138 -0
  5. package/console-reporter.js +1 -0
  6. package/lib/artillery-global.js +33 -0
  7. package/lib/cli/banner.js +8 -0
  8. package/lib/cli/common-flags.js +80 -0
  9. package/lib/cli/hooks/version.js +20 -0
  10. package/lib/cmds/dino.js +109 -0
  11. package/lib/cmds/quick.js +122 -0
  12. package/lib/cmds/report.js +34 -0
  13. package/lib/cmds/run-aci.js +91 -0
  14. package/lib/cmds/run-fargate.js +192 -0
  15. package/lib/cmds/run-lambda.js +96 -0
  16. package/lib/cmds/run.js +671 -0
  17. package/lib/console-capture.js +92 -0
  18. package/lib/console-reporter.js +438 -0
  19. package/lib/create-bom/built-in-plugins.js +12 -0
  20. package/lib/create-bom/create-bom.js +301 -0
  21. package/lib/dispatcher.js +9 -0
  22. package/lib/dist.js +222 -0
  23. package/lib/index.js +5 -0
  24. package/lib/launch-platform.js +439 -0
  25. package/lib/load-plugins.js +113 -0
  26. package/lib/platform/aws/aws-cloudwatch.js +106 -0
  27. package/lib/platform/aws/aws-create-sqs-queue.js +58 -0
  28. package/lib/platform/aws/aws-ensure-s3-bucket-exists.js +78 -0
  29. package/lib/platform/aws/aws-get-account-id.js +26 -0
  30. package/lib/platform/aws/aws-get-bucket-region.js +18 -0
  31. package/lib/platform/aws/aws-get-credentials.js +28 -0
  32. package/lib/platform/aws/aws-get-default-region.js +26 -0
  33. package/lib/platform/aws/aws-whoami.js +15 -0
  34. package/lib/platform/aws/constants.js +7 -0
  35. package/lib/platform/aws/iam-cf-templates/aws-iam-fargate-cf-template.yml +219 -0
  36. package/lib/platform/aws/iam-cf-templates/aws-iam-lambda-cf-template.yml +125 -0
  37. package/lib/platform/aws/iam-cf-templates/gh-oidc-fargate.yml +241 -0
  38. package/lib/platform/aws/iam-cf-templates/gh-oidc-lambda.yml +153 -0
  39. package/lib/platform/aws-ecs/ecs.js +247 -0
  40. package/lib/platform/aws-ecs/legacy/aws-util.js +134 -0
  41. package/lib/platform/aws-ecs/legacy/bom.js +528 -0
  42. package/lib/platform/aws-ecs/legacy/constants.js +27 -0
  43. package/lib/platform/aws-ecs/legacy/create-s3-client.js +24 -0
  44. package/lib/platform/aws-ecs/legacy/create-test.js +247 -0
  45. package/lib/platform/aws-ecs/legacy/errors.js +34 -0
  46. package/lib/platform/aws-ecs/legacy/find-public-subnets.js +149 -0
  47. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-inspect-script/index.js +27 -0
  48. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js +80 -0
  49. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js +202 -0
  50. package/lib/platform/aws-ecs/legacy/plugins.js +16 -0
  51. package/lib/platform/aws-ecs/legacy/run-cluster.js +1994 -0
  52. package/lib/platform/aws-ecs/legacy/sqs-reporter.js +401 -0
  53. package/lib/platform/aws-ecs/legacy/tags.js +22 -0
  54. package/lib/platform/aws-ecs/legacy/test-run-status.js +9 -0
  55. package/lib/platform/aws-ecs/legacy/time.js +67 -0
  56. package/lib/platform/aws-ecs/legacy/util.js +97 -0
  57. package/lib/platform/aws-ecs/worker/Dockerfile +64 -0
  58. package/lib/platform/aws-ecs/worker/helpers.sh +80 -0
  59. package/lib/platform/aws-ecs/worker/loadgen-worker +656 -0
  60. package/lib/platform/aws-lambda/dependencies.js +130 -0
  61. package/lib/platform/aws-lambda/index.js +734 -0
  62. package/lib/platform/aws-lambda/lambda-handler/a9-handler-dependencies.js +73 -0
  63. package/lib/platform/aws-lambda/lambda-handler/a9-handler-helpers.js +43 -0
  64. package/lib/platform/aws-lambda/lambda-handler/a9-handler-index.js +235 -0
  65. package/lib/platform/aws-lambda/lambda-handler/package.json +15 -0
  66. package/lib/platform/aws-lambda/prices.js +29 -0
  67. package/lib/platform/az/aci.js +694 -0
  68. package/lib/platform/az/aqs-queue-consumer.js +88 -0
  69. package/lib/platform/az/regions.js +52 -0
  70. package/lib/platform/cloud/api.js +72 -0
  71. package/lib/platform/cloud/cloud.js +448 -0
  72. package/lib/platform/cloud/http-client.js +19 -0
  73. package/lib/platform/local/artillery-worker-local.js +154 -0
  74. package/lib/platform/local/index.js +174 -0
  75. package/lib/platform/local/worker.js +261 -0
  76. package/lib/platform/worker-states.js +13 -0
  77. package/lib/queue-consumer/index.js +56 -0
  78. package/lib/stash.js +41 -0
  79. package/lib/telemetry.js +78 -0
  80. package/lib/util/await-on-ee.js +24 -0
  81. package/lib/util/generate-id.js +9 -0
  82. package/lib/util/parse-tag-string.js +21 -0
  83. package/lib/util/prepare-test-execution-plan.js +216 -0
  84. package/lib/util/sleep.js +7 -0
  85. package/lib/util/validate-script.js +132 -0
  86. package/lib/util.js +294 -0
  87. package/lib/utils-config.js +31 -0
  88. package/package.json +323 -0
  89. package/types.d.ts +317 -0
  90. package/util.js +1 -0
@@ -0,0 +1,439 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
+
5
+ const { SSMS } = require('@artilleryio/int-core').ssms;
6
+ const { loadPlugins, loadPluginsConfig } = require('./load-plugins');
7
+
8
+ const EventEmitter = require('eventemitter3');
9
+ const debug = require('debug')('core');
10
+
11
+ const p = require('node:util').promisify;
12
+ const _ = require('lodash');
13
+
14
+ const PlatformLocal = require('./platform/local');
15
+ const PlatformLambda = require('./platform/aws-lambda');
16
+ const PlatformAzureACI = require('./platform/az/aci');
17
+
18
+ async function createLauncher(script, payload, opts, launcherOpts) {
19
+ launcherOpts = launcherOpts || {
20
+ platform: 'local',
21
+ mode: 'distribute'
22
+ };
23
+ let l;
24
+ try {
25
+ l = new Launcher(script, payload, opts, launcherOpts);
26
+ } catch (err) {
27
+ console.log(err);
28
+ return null;
29
+ }
30
+
31
+ return l;
32
+ }
33
+ class Launcher {
34
+ constructor(script, payload, opts, launcherOpts) {
35
+ this.script = script;
36
+ this.payload = payload;
37
+ this.opts = opts;
38
+
39
+ this.exitedWorkersCount = 0;
40
+ this.workerMessageBuffer = [];
41
+
42
+ this.metricsByPeriod = {}; // individual intermediates by worker
43
+ this.finalReportsByWorker = {};
44
+
45
+ this.events = new EventEmitter();
46
+
47
+ this.pluginEvents = new EventEmitter();
48
+ this.pluginEventsLegacy = new EventEmitter();
49
+
50
+ this.launcherOpts = launcherOpts;
51
+
52
+ this.periodsReportedFor = [];
53
+
54
+ if (launcherOpts.platform === 'local') {
55
+ this.platform = new PlatformLocal(script, payload, opts, launcherOpts);
56
+ } else if (launcherOpts.platform === 'aws:lambda') {
57
+ this.platform = new PlatformLambda(script, payload, opts, launcherOpts);
58
+ } else if (launcherOpts.platform === 'az:aci') {
59
+ this.platform = new PlatformAzureACI(script, payload, opts, launcherOpts);
60
+ } else {
61
+ throw new Error(`Unknown platform: ${launcherOpts.platform}`);
62
+ }
63
+
64
+ this.phaseStartedEventsSeen = {};
65
+ this.phaseCompletedEventsSeen = {};
66
+
67
+ this.eventsByWorker = {};
68
+ }
69
+
70
+ async initWorkerEvents(workerEvents) {
71
+ workerEvents.on('workerError', (_workerId, message) => {
72
+ const { id, error, level, aggregatable, logs } = message;
73
+
74
+ if (level !== 'warn') {
75
+ this.exitedWorkersCount++;
76
+ }
77
+
78
+ if (aggregatable) {
79
+ this.workerMessageBuffer.push(message);
80
+ } else {
81
+ global.artillery.log(`[${id}]: ${error.message}`);
82
+ if (logs) {
83
+ global.artillery.log(logs);
84
+ }
85
+ }
86
+
87
+ this.events.emit('workerError', message);
88
+ });
89
+
90
+ workerEvents.on('phaseStarted', (_workerId, message) => {
91
+ // Note - we send only the first event for a phase, not all of them
92
+ if (
93
+ typeof this.phaseStartedEventsSeen[message.phase.index] === 'undefined'
94
+ ) {
95
+ this.phaseStartedEventsSeen[message.phase.index] = Date.now();
96
+ const fullPhase = {
97
+ //get back original phase without any splitting for workers
98
+ ...this.script.config.phases[message.phase.index],
99
+ index: message.phase.index,
100
+ id: message.phase.id,
101
+ startTime: this.phaseStartedEventsSeen[message.phase.index]
102
+ };
103
+
104
+ this.events.emit('phaseStarted', fullPhase);
105
+ this.pluginEvents.emit('phaseStarted', fullPhase);
106
+ this.pluginEventsLegacy.emit('phaseStarted', fullPhase);
107
+
108
+ global.artillery.globalEvents.emit('phaseStarted', fullPhase);
109
+ }
110
+ });
111
+
112
+ workerEvents.on('phaseCompleted', (_workerId, message) => {
113
+ if (
114
+ typeof this.phaseCompletedEventsSeen[message.phase.index] ===
115
+ 'undefined'
116
+ ) {
117
+ this.phaseCompletedEventsSeen[message.phase.index] = Date.now();
118
+ const fullPhase = {
119
+ //get back original phase without any splitting for workers
120
+ ...this.script.config.phases[message.phase.index],
121
+ id: message.phase.id,
122
+ index: message.phase.index,
123
+ startTime: this.phaseStartedEventsSeen[message.phase.index],
124
+ endTime: message.phase.endTime
125
+ };
126
+
127
+ this.events.emit('phaseCompleted', fullPhase);
128
+ this.pluginEvents.emit('phaseCompleted', fullPhase);
129
+ this.pluginEventsLegacy.emit('phaseCompleted', fullPhase);
130
+ global.artillery.globalEvents.emit('phaseCompleted', fullPhase);
131
+ }
132
+ });
133
+
134
+ // We are not going to receive stats events from workers
135
+ // which have zero arrivals for a phase. (This can only happen
136
+ // in "distribute" mode.)
137
+ workerEvents.on('stats', (_workerId, message) => {
138
+ const workerStats = SSMS.deserializeMetrics(message.stats);
139
+ const period = workerStats.period;
140
+ if (typeof this.metricsByPeriod[period] === 'undefined') {
141
+ this.metricsByPeriod[period] = [];
142
+ }
143
+ // TODO: might want the full message here, with worker ID etc
144
+ this.metricsByPeriod[period].push(workerStats);
145
+ });
146
+
147
+ workerEvents.on('done', async (workerId, message) => {
148
+ this.exitedWorkersCount++;
149
+ this.finalReportsByWorker[workerId] = SSMS.deserializeMetrics(
150
+ message.report
151
+ );
152
+ });
153
+
154
+ workerEvents.on('log', async (_workerId, message) => {
155
+ artillery.globalEvents.emit('log', ...message.args);
156
+ });
157
+
158
+ workerEvents.on('setSuggestedExitCode', (_workerId, message) => {
159
+ artillery.suggestedExitCode = message.code;
160
+ });
161
+ }
162
+
163
+ async initPlugins() {
164
+ const plugins = await loadPlugins(
165
+ this.script.config.plugins,
166
+ this.script,
167
+ this.opts
168
+ );
169
+
170
+ //
171
+ // init plugins
172
+ //
173
+ for (const [name, result] of Object.entries(plugins)) {
174
+ if (result.isLoaded) {
175
+ if (result.version === 3) {
176
+ // TODO: load the plugin, subscribe to events
177
+ // global.artillery.plugins[name] = result.plugin;
178
+ } else {
179
+ // global.artillery.log(`WARNING: Legacy plugin detected: ${name}
180
+ // See https://artillery.io/docs/resources/core/v2.html for more details.`,
181
+ // 'warn');
182
+
183
+ // NOTE:
184
+ // We are giving v1 and v2 plugins a throw-away script
185
+ // object because we only care about the plugin setting
186
+ // up event handlers here. The plugins will be loaded
187
+ // properly in individual workers where they will have the
188
+ // opportunity to attach custom code, modify the script
189
+ // object etc.
190
+ // If we let a plugin access to the actual script object,
191
+ // and it happens to attach code to it (with a custom
192
+ // processor function for example) - spawning a worker
193
+ // will fail.
194
+ const dummyScript = JSON.parse(JSON.stringify(this.script));
195
+ dummyScript.config = {
196
+ ...dummyScript.config,
197
+ // Load additional plugins configuration from the environment
198
+ plugins: loadPluginsConfig(this.script.config.plugins)
199
+ };
200
+
201
+ if (result.version === 1) {
202
+ result.plugin = new result.PluginExport(
203
+ dummyScript.config,
204
+ this.pluginEventsLegacy
205
+ );
206
+ global.artillery.plugins.push(result);
207
+ } else if (result.version === 2) {
208
+ if (result.PluginExport.LEGACY_METRICS_FORMAT === false) {
209
+ result.plugin = new result.PluginExport.Plugin(
210
+ dummyScript,
211
+ this.pluginEvents,
212
+ this.opts
213
+ );
214
+ } else {
215
+ result.plugin = new result.PluginExport.Plugin(
216
+ dummyScript,
217
+ this.pluginEventsLegacy,
218
+ this.opts
219
+ );
220
+ }
221
+ global.artillery.plugins.push(result);
222
+ } else {
223
+ // TODO: print warning
224
+ }
225
+ }
226
+ } else {
227
+ global.artillery.log(`WARNING: Could not load plugin: ${name}`, 'warn');
228
+ global.artillery.log(result.msg, 'warn');
229
+ // global.artillery.log(result.error, 'warn');
230
+ }
231
+ }
232
+ }
233
+
234
+ async handleAllWorkersFinished() {
235
+ const allWorkersDone =
236
+ this.exitedWorkersCount === this.platform.getDesiredWorkerCount();
237
+ if (allWorkersDone) {
238
+ clearInterval(this.i1);
239
+ clearInterval(this.i2);
240
+
241
+ // Flush messages from workers
242
+ await this.flushWorkerMessages(0);
243
+ await this.flushIntermediateMetrics(true);
244
+
245
+ const pds = Object.keys(this.finalReportsByWorker).map(
246
+ (k) => this.finalReportsByWorker[k]
247
+ );
248
+
249
+ const statsByPeriod = Object.values(SSMS.mergeBuckets(pds));
250
+ const stats = SSMS.pack(statsByPeriod);
251
+
252
+ stats.summaries = {};
253
+ for (const [name, value] of Object.entries(stats.histograms || {})) {
254
+ const summary = SSMS.summarizeHistogram(value);
255
+ stats.summaries[name] = summary;
256
+ }
257
+
258
+ clearInterval(this.workerExitWatcher);
259
+
260
+ // Relay event to workers
261
+ this.pluginEvents.emit('done', stats);
262
+
263
+ global.artillery.globalEvents.emit('done', stats);
264
+ this.pluginEventsLegacy.emit('done', SSMS.legacyReport(stats));
265
+
266
+ this.events.emit('done', stats);
267
+ }
268
+ }
269
+
270
+ async flushWorkerMessages(maxAge = 9000) {
271
+ // Collect messages older than maxAge msec and group by log message:
272
+ const now = Date.now();
273
+ const okToPrint = this.workerMessageBuffer.filter(
274
+ (m) => now - m.ts > maxAge
275
+ );
276
+ this.workerMessageBuffer = this.workerMessageBuffer.filter(
277
+ (m) => now - m.ts <= maxAge
278
+ );
279
+
280
+ const readyMessages = okToPrint.reduce((acc, message) => {
281
+ const { error } = message;
282
+ // TODO: Take event type and level into account
283
+ if (typeof acc[error.message] === 'undefined') {
284
+ acc[error.message] = [];
285
+ }
286
+ acc[error.message].push(message);
287
+ return acc;
288
+ }, {});
289
+
290
+ for (const [_logMessage, messageObjects] of Object.entries(readyMessages)) {
291
+ if (messageObjects[0].error) {
292
+ global.artillery.log(
293
+ `[${messageObjects[0].id}] ${messageObjects[0].error.message}`,
294
+ messageObjects[0].level
295
+ );
296
+ } else {
297
+ // Expect a msg property:
298
+ global.artillery.log(
299
+ `[${messageObjects[0].id}] ${messageObjects[0].msg}`,
300
+ messageObjects[0].level
301
+ );
302
+ }
303
+ }
304
+ }
305
+
306
+ async flushIntermediateMetrics(flushAll = false) {
307
+ if (Object.keys(this.metricsByPeriod).length === 0) {
308
+ debug('No metrics received yet');
309
+ return;
310
+ }
311
+
312
+ // We always look at the earliest period available so that reports come in chronological order
313
+ const unreportedPeriods = Object.keys(this.metricsByPeriod)
314
+ .filter((x) => this.periodsReportedFor.indexOf(x) === -1)
315
+ .sort();
316
+
317
+ const earliestPeriodAvailable = unreportedPeriods[0];
318
+
319
+ // TODO: better name. One above is earliestNotAlreadyReported
320
+ const earliest = Object.keys(this.metricsByPeriod).sort()[0];
321
+ if (this.periodsReportedFor.indexOf(earliest) > -1) {
322
+ global.artillery.log(
323
+ 'Warning: multiple batches of metrics for period',
324
+ earliest,
325
+ new Date(Number(earliest))
326
+ );
327
+
328
+ delete this.metricsByPeriod[earliest]; // FIXME: need to merge them in for the final report
329
+ }
330
+
331
+ // Dynamically adjust the duration we're willing to wait for. This matters on SQS where messages are received
332
+ // in batches of 10 and more workers => need to wait longer.
333
+ const MAX_WAIT_FOR_PERIOD_MS =
334
+ (Math.ceil(this.platform.getDesiredWorkerCount() / 10) * 3 + 30) * 1000;
335
+
336
+ debug({
337
+ now: Date.now(),
338
+ count: this.platform.getDesiredWorkerCount(),
339
+ earliestPeriodAvailable,
340
+ earliest,
341
+ MAX_WAIT_FOR_PERIOD_MS,
342
+ numReports: this.metricsByPeriod[earliestPeriodAvailable]?.length,
343
+ periodsReportedFor: this.periodsReportedFor,
344
+ metricsByPeriod: Object.keys(this.metricsByPeriod)
345
+ });
346
+
347
+ const allWorkersReportedForPeriod =
348
+ this.metricsByPeriod[earliestPeriodAvailable]?.length ===
349
+ this.platform.getDesiredWorkerCount();
350
+ const waitedLongEnough =
351
+ Date.now() - Number(earliestPeriodAvailable) > MAX_WAIT_FOR_PERIOD_MS;
352
+
353
+ if (flushAll) {
354
+ for (const period of unreportedPeriods) {
355
+ this.emitIntermediatesForPeriod(period);
356
+ }
357
+ } else if (
358
+ typeof earliestPeriodAvailable !== 'undefined' &&
359
+ (allWorkersReportedForPeriod || waitedLongEnough)
360
+ ) {
361
+ this.emitIntermediatesForPeriod(earliestPeriodAvailable);
362
+ // TODO: autoscaling. Handle workers that drop off or join, and update count
363
+ } else {
364
+ debug('Waiting for more workerStats before emitting stats event');
365
+ }
366
+ }
367
+
368
+ emitIntermediatesForPeriod(period) {
369
+ debug(
370
+ 'Report @',
371
+ new Date(Number(period)),
372
+ 'made up of items:',
373
+ this.metricsByPeriod[String(period)].length
374
+ );
375
+
376
+ // TODO: Track how many workers provided metrics in the metrics report
377
+ // summarize histograms for console reporter:
378
+ const merged = SSMS.mergeBuckets(this.metricsByPeriod[String(period)]);
379
+ const stats = merged[String(period)];
380
+
381
+ stats.summaries = {};
382
+ for (const [name, value] of Object.entries(stats.histograms || {})) {
383
+ const summary = SSMS.summarizeHistogram(value);
384
+ stats.summaries[name] = summary;
385
+ }
386
+
387
+ delete this.metricsByPeriod[String(period)];
388
+
389
+ this.periodsReportedFor.push(period);
390
+ this.pluginEvents.emit('stats', stats);
391
+ global.artillery.globalEvents.emit('stats', stats);
392
+ this.pluginEventsLegacy.emit('stats', SSMS.legacyReport(stats));
393
+
394
+ this.events.emit('stats', stats);
395
+ }
396
+
397
+ async run() {
398
+ await this.initPlugins();
399
+
400
+ this.i1 = setInterval(async () => {
401
+ await this.flushWorkerMessages();
402
+ }, 1 * 1000).unref();
403
+
404
+ this.i2 = setInterval(async () => {
405
+ this.flushIntermediateMetrics();
406
+ }, 2 * 1000).unref();
407
+
408
+ this.workerExitWatcher = setInterval(async () => {
409
+ await this.handleAllWorkersFinished();
410
+ }, 2 * 1000);
411
+
412
+ await this.initWorkerEvents(this.platform.events);
413
+ await this.platform.startJob();
414
+ debug('workers running');
415
+ }
416
+
417
+ async shutdown() {
418
+ await this.platform.shutdown();
419
+
420
+ // TODO: flush worker messages, and intermediate stats
421
+
422
+ // Unload plugins
423
+ // TODO: v3 plugins
424
+ if (global.artillery?.plugins) {
425
+ for (const o of global.artillery.plugins) {
426
+ if (o.plugin.cleanup) {
427
+ try {
428
+ await p(o.plugin.cleanup.bind(o.plugin))();
429
+ debug('plugin unloaded:', o.name);
430
+ } catch (cleanupErr) {
431
+ global.artillery.log(cleanupErr, 'error');
432
+ }
433
+ }
434
+ }
435
+ }
436
+ }
437
+ }
438
+
439
+ module.exports = createLauncher;
@@ -0,0 +1,113 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
+
5
+ const debug = require('debug')('core');
6
+ const path = require('node:path');
7
+
8
+ // Additional paths to load plugins can be set via ARTILLERY_PLUGIN_PATH
9
+ // Additional plugin config mafy be set via ARTILLERY_PLUGINS (as JSON)
10
+ // Version may be: v1, v2, v3 or any
11
+ function loadPluginsConfig(pluginSpecs) {
12
+ let additionalPlugins = {};
13
+
14
+ if (process.env.ARTILLERY_PLUGINS) {
15
+ try {
16
+ additionalPlugins = JSON.parse(process.env.ARTILLERY_PLUGINS);
17
+ } catch (ignoreErr) {
18
+ debug(ignoreErr);
19
+ }
20
+ }
21
+
22
+ return Object.assign({}, pluginSpecs, additionalPlugins);
23
+ }
24
+
25
+ async function loadPlugins(pluginSpecs, testScript) {
26
+ let requirePaths = [''];
27
+
28
+ // requirePaths = requirePaths.concat(pro.getPluginPath());
29
+
30
+ if (process.env.ARTILLERY_PLUGIN_PATH) {
31
+ requirePaths = requirePaths.concat(
32
+ process.env.ARTILLERY_PLUGIN_PATH.split(':')
33
+ );
34
+ }
35
+
36
+ pluginSpecs = loadPluginsConfig(pluginSpecs);
37
+
38
+ const results = {};
39
+ for (const [name, config] of Object.entries(pluginSpecs)) {
40
+ const result = await loadPlugin(name, config, requirePaths, testScript);
41
+ results[name] = result;
42
+ }
43
+
44
+ return results;
45
+ }
46
+
47
+ async function loadPlugin(name, config, requirePaths, testScript) {
48
+ // TODO: Take scope in directly - don't need the full script
49
+ const pluginConfigScope = config.scope || testScript.config.pluginsScope;
50
+ const pluginPrefix = pluginConfigScope
51
+ ? pluginConfigScope
52
+ : 'artillery-plugin-';
53
+ const requireString = pluginPrefix + name;
54
+ let PluginExport, pluginErr, loadedFrom, version;
55
+
56
+ for (const p of requirePaths) {
57
+ debug('Looking for plugin in:', p);
58
+ try {
59
+ loadedFrom = path.join(p, requireString);
60
+ PluginExport = require(loadedFrom);
61
+ if (typeof PluginExport === 'function') {
62
+ version = 1;
63
+ } else if (
64
+ typeof PluginExport === 'object' &&
65
+ typeof PluginExport.Plugin === 'function'
66
+ ) {
67
+ version = 2;
68
+ } // TODO: Add v3
69
+ } catch (err) {
70
+ debug(err);
71
+ pluginErr = err;
72
+ }
73
+
74
+ if (typeof PluginExport !== 'undefined') {
75
+ break;
76
+ }
77
+ }
78
+
79
+ if (!PluginExport) {
80
+ let msg;
81
+
82
+ if (!pluginErr) {
83
+ msg = `WARNING: Could not initialize plugin: ${name}`;
84
+ } else {
85
+ if (pluginErr.code === 'MODULE_NOT_FOUND') {
86
+ msg = `WARNING: Plugin ${name} specified but module ${requireString} could not be found (${pluginErr.code})`;
87
+ } else {
88
+ msg = `WARNING: Could not initialize plugin: ${name} (${pluginErr.message})`;
89
+ }
90
+ }
91
+
92
+ return {
93
+ name,
94
+ isLoaded: false,
95
+ isInitialized: false,
96
+ msg: msg,
97
+ error: pluginErr
98
+ };
99
+ } else {
100
+ debug('Plugin %s loaded from %s', name, requireString);
101
+ return {
102
+ name,
103
+ isLoaded: true,
104
+ isInitialized: false,
105
+
106
+ PluginExport,
107
+ loadedFrom,
108
+ version
109
+ };
110
+ }
111
+ }
112
+
113
+ module.exports = { loadPlugins, loadPlugin, loadPluginsConfig };
@@ -0,0 +1,106 @@
1
+ const {
2
+ CloudWatchLogsClient,
3
+ PutRetentionPolicyCommand
4
+ } = require('@aws-sdk/client-cloudwatch-logs');
5
+ const debug = require('debug')('artillery:aws-cloudwatch');
6
+
7
+ const allowedRetentionDays = [
8
+ 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827,
9
+ 2192, 2557, 2922, 3288, 3653
10
+ ];
11
+
12
+ async function _putCloudwatchRetentionPolicy(
13
+ logGroupName,
14
+ retentionInDays,
15
+ region
16
+ ) {
17
+ const cloudwatchlogs = new CloudWatchLogsClient({
18
+ apiVersion: '2014-11-06',
19
+ region
20
+ });
21
+ const putRetentionPolicyParams = {
22
+ logGroupName,
23
+ retentionInDays
24
+ };
25
+
26
+ return cloudwatchlogs.send(
27
+ new PutRetentionPolicyCommand(putRetentionPolicyParams)
28
+ );
29
+ }
30
+
31
+ function setCloudwatchRetention(
32
+ logGroupName,
33
+ retentionInDays,
34
+ region,
35
+ options = { maxRetries: 5, waitPerRetry: 1000 }
36
+ ) {
37
+ if (!allowedRetentionDays.includes(retentionInDays)) {
38
+ console.log(
39
+ `WARNING: Skipping setting CloudWatch retention, as invalid value specified: ${retentionInDays}. Allowed values are: ${allowedRetentionDays.join(
40
+ ', '
41
+ )}`
42
+ );
43
+ return;
44
+ }
45
+
46
+ const interval = setInterval(
47
+ async (opts) => {
48
+ debug(
49
+ `Trying to set CloudWatch Log group ${logGroupName} retention policy to ${retentionInDays} days`
50
+ );
51
+ opts.incr = (opts.incr || 0) + 1;
52
+
53
+ try {
54
+ const _res = await _putCloudwatchRetentionPolicy(
55
+ logGroupName,
56
+ retentionInDays,
57
+ region
58
+ );
59
+ debug(
60
+ `Successfully set CloudWatch Logs retention policy to ${retentionInDays} days`
61
+ );
62
+ clearInterval(interval);
63
+ } catch (error) {
64
+ const resumeTestMessage =
65
+ 'The test will continue without setting the retention policy.';
66
+ if (error?.code === 'AccessDeniedException') {
67
+ console.log(`\n${error.message}`);
68
+ console.log(
69
+ '\nWARNING: Missing logs:PutRetentionPolicy permission to set CloudWatch retention policy. Please ensure the IAM role has the necessary permissions:\nhttps://docs.art/fargate#iam-permissions'
70
+ );
71
+ console.log(`${resumeTestMessage}\n`);
72
+ clearInterval(interval);
73
+ return;
74
+ }
75
+
76
+ if (error?.code !== 'ResourceNotFoundException') {
77
+ console.log(`\n${error.message}`);
78
+ console.log(
79
+ '\nWARNING: Unexpected error setting CloudWatch retention policy\n'
80
+ );
81
+ console.log(`${resumeTestMessage}\n`);
82
+ clearInterval(interval);
83
+ return;
84
+ }
85
+
86
+ if (opts.incr >= opts.maxRetries) {
87
+ console.log(`\n${error.message}`);
88
+ console.log(
89
+ `\nWARNING: Cannot find log group ${logGroupName}\nMax retries exceeded setting CloudWatch retention policy:`
90
+ );
91
+ console.log(`${resumeTestMessage}\n`);
92
+ clearInterval(interval);
93
+ return;
94
+ }
95
+ }
96
+ },
97
+ options.waitPerRetry,
98
+ options
99
+ );
100
+
101
+ return interval;
102
+ }
103
+
104
+ module.exports = {
105
+ setCloudwatchRetention
106
+ };