Skip to content

Commit 339dd38

Browse files
authored
feat: improve error handling & messages when recording to Artillery Cloud (#2531)
* refactor: enable early shutdowns Handle cases where we need to shutdown gracefully before a test run starts running. * feat: improve error checks when recording to Artillery Cloud * Make error messages more specific * Check that the API key is valid with a preflight call to cloud API * Stop early if API key is missing or is invalid
1 parent 185276e commit 339dd38

File tree

4 files changed

+160
-25
lines changed

4 files changed

+160
-25
lines changed

packages/artillery/lib/cmds/run-fargate.js

+29-3
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
const { Command, Flags, Args } = require('@oclif/core');
66
const { CommonRunFlags } = require('../cli/common-flags');
77
const telemetry = require('../telemetry').init();
8-
const { Plugin: CloudPlugin } = require('../platform/cloud/cloud');
98

109
const runCluster = require('../platform/aws-ecs/legacy/run-cluster');
1110
const { supportedRegions } = require('../platform/aws-ecs/legacy/util');
1211
const PlatformECS = require('../platform/aws-ecs/ecs');
1312
const { ECS_WORKER_ROLE_NAME } = require('../platform/aws/constants');
14-
13+
const { Plugin: CloudPlugin } = require('../platform/cloud/cloud');
1514
class RunCommand extends Command {
1615
static aliases = ['run:fargate'];
1716
// Enable multiple args:
@@ -23,7 +22,34 @@ class RunCommand extends Command {
2322

2423
flags.platform = 'aws:ecs';
2524

26-
new CloudPlugin(null, null, { flags });
25+
const cloud = new CloudPlugin(null, null, { flags });
26+
if (cloud.enabled) {
27+
try {
28+
await cloud.init();
29+
} catch (err) {
30+
if (err.name === 'CloudAPIKeyMissing') {
31+
console.error(
32+
'Error: API key is required to record test results to Artillery Cloud'
33+
);
34+
console.error(
35+
'See https://docs.art/get-started-cloud for more information'
36+
);
37+
38+
process.exit(7);
39+
} else if (err.name === 'APIKeyUnauthorized') {
40+
console.error(
41+
'Error: API key is not recognized or is not authorized to record tests'
42+
);
43+
44+
process.exit(7);
45+
} else {
46+
console.error(
47+
'Error: something went wrong connecting to Artillery Cloud'
48+
);
49+
console.error('Check https://x.com/artilleryio for status updates');
50+
}
51+
}
52+
}
2753

2854
const ECS = new PlatformECS(
2955
null,

packages/artillery/lib/cmds/run.js

+55-15
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ RunCommand.args = {
111111
})
112112
};
113113

114+
let cloud;
114115
RunCommand.runCommandImplementation = async function (flags, argv, args) {
115116
// Collect all input files for reading/parsing - via args, --config, or -i
116117
const inputFiles = argv.concat(flags.input || [], flags.config || []);
@@ -144,13 +145,43 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) {
144145
}
145146

146147
try {
148+
cloud = new CloudPlugin(null, null, { flags });
149+
150+
if (cloud.enabled) {
151+
try {
152+
await cloud.init();
153+
} catch (err) {
154+
if (err.name === 'CloudAPIKeyMissing') {
155+
console.error(
156+
'Error: API key is required to record test results to Artillery Cloud'
157+
);
158+
console.error(
159+
'See https://docs.art/get-started-cloud for more information'
160+
);
161+
162+
await gracefulShutdown({ exitCode: 7 });
163+
} else if (err.name === 'APIKeyUnauthorized') {
164+
console.error(
165+
'Error: API key is not recognized or is not authorized to record tests'
166+
);
167+
168+
await gracefulShutdown({ exitCode: 7 });
169+
} else {
170+
console.error(
171+
'Error: something went wrong connecting to Artillery Cloud'
172+
);
173+
console.error('Check https://x.com/artilleryio for status updates');
174+
}
175+
}
176+
}
177+
147178
const testRunId = process.env.ARTILLERY_TEST_RUN_ID || generateId('t');
148179
console.log('Test run id:', testRunId);
149180
global.artillery.testRunId = testRunId;
150181

151182
const script = await prepareTestExecutionPlan(inputFiles, flags, args);
152183

153-
const runnerOpts = {
184+
var runnerOpts = {
154185
environment: flags.environment,
155186
// This is used in the worker to resolve
156187
// the path to the processor module
@@ -197,7 +228,7 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) {
197228
testRunId
198229
};
199230

200-
let launcher = await createLauncher(
231+
var launcher = await createLauncher(
201232
script,
202233
script.config.payload,
203234
runnerOpts,
@@ -212,7 +243,7 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) {
212243
metricsToSuppress
213244
});
214245

215-
let reporters = [consoleReporter];
246+
var reporters = [consoleReporter];
216247
if (process.env.CUSTOM_REPORTERS) {
217248
const customReporterNames = process.env.CUSTOM_REPORTERS.split(',');
218249
customReporterNames.forEach(function (name) {
@@ -286,8 +317,6 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) {
286317
}
287318
});
288319

289-
new CloudPlugin(null, null, { flags });
290-
291320
global.artillery.globalEvents.emit('test:init', {
292321
flags,
293322
testRunId,
@@ -306,8 +335,8 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) {
306335

307336
launcher.run();
308337

309-
let finalReport = {};
310-
let shuttingDown = false;
338+
var finalReport = {};
339+
var shuttingDown = false;
311340
process.on('SIGINT', async () => {
312341
gracefulShutdown({ earlyStop: true });
313342
});
@@ -353,16 +382,23 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) {
353382
}
354383
await Promise.allSettled(ps2);
355384

356-
await telemetry.shutdown();
385+
if (telemetry) {
386+
await telemetry.shutdown();
387+
}
388+
389+
if (launcher) {
390+
await launcher.shutdown();
391+
}
357392

358-
await launcher.shutdown();
359393
await (async function () {
360-
for (const r of reporters) {
361-
if (r.cleanup) {
362-
try {
363-
await p(r.cleanup.bind(r))();
364-
} catch (cleanupErr) {
365-
debug(cleanupErr);
394+
if (reporters) {
395+
for (const r of reporters) {
396+
if (r.cleanup) {
397+
try {
398+
await p(r.cleanup.bind(r))();
399+
} catch (cleanupErr) {
400+
debug(cleanupErr);
401+
}
366402
}
367403
}
368404
}
@@ -565,6 +601,10 @@ async function sendTelemetry(script, flags, extraProps) {
565601
if (script.config && script.config.__createdByQuickCommand) {
566602
properties['quick'] = true;
567603
}
604+
if (cloud && cloud.enabled && cloud.user) {
605+
properties.cloud = cloud.user;
606+
}
607+
568608
properties['solo'] = flags.solo;
569609
try {
570610
// One-way hash of target endpoint:

packages/artillery/lib/platform/cloud/cloud.js

+47-7
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,20 @@ const util = require('node:util');
1212

1313
class ArtilleryCloudPlugin {
1414
constructor(_script, _events, { flags }) {
15+
this.enabled = false;
16+
1517
if (!flags.record) {
1618
return this;
1719
}
1820

19-
this.apiKey = flags.key || process.env.ARTILLERY_CLOUD_API_KEY;
21+
this.enabled = true;
2022

21-
if (!this.apiKey) {
22-
console.log(
23-
'An API key is required to record test results to Artillery Cloud. See https://docs.art/get-started-cloud for more information.'
24-
);
25-
return;
26-
}
23+
this.apiKey = flags.key || process.env.ARTILLERY_CLOUD_API_KEY;
2724

2825
this.baseUrl =
2926
process.env.ARTILLERY_CLOUD_ENDPOINT || 'https://app.artillery.io';
3027
this.eventsEndpoint = `${this.baseUrl}/api/events`;
28+
this.whoamiEndpoint = `${this.baseUrl}/api/user/whoami`;
3129

3230
this.defaultHeaders = {
3331
'x-auth-token': this.apiKey
@@ -143,6 +141,10 @@ class ArtilleryCloudPlugin {
143141
global.artillery.ext({
144142
ext: 'onShutdown',
145143
method: async (opts) => {
144+
if (!this.enabled || this.off) {
145+
return;
146+
}
147+
146148
clearInterval(this.setGetLoadTestInterval);
147149
// Wait for the last logLines events to be processed, as they can sometimes finish processing after shutdown has finished
148150
await awaitOnEE(
@@ -171,6 +173,44 @@ class ArtilleryCloudPlugin {
171173
return this;
172174
}
173175

176+
async init() {
177+
if (!this.apiKey) {
178+
const err = new Error();
179+
err.name = 'CloudAPIKeyMissing';
180+
this.off = true;
181+
throw err;
182+
}
183+
184+
let res;
185+
let body;
186+
try {
187+
res = await request.get(this.whoamiEndpoint, {
188+
headers: this.defaultHeaders,
189+
throwHttpErrors: false,
190+
retry: {
191+
limit: 0
192+
}
193+
});
194+
195+
body = JSON.parse(res.body);
196+
} catch (err) {
197+
this.off = true;
198+
throw err;
199+
}
200+
201+
if (res.statusCode === 401) {
202+
const err = new Error();
203+
err.name = 'APIKeyUnauthorized';
204+
this.off = true;
205+
throw err;
206+
}
207+
208+
this.user = {
209+
id: body.id,
210+
email: body.email
211+
};
212+
}
213+
174214
async waitOnUnprocessedLogs(maxWaitTime) {
175215
let waitedTime = 0;
176216
while (this.unprocessedLogsCounter > 0 && waitedTime < maxWaitTime) {

packages/artillery/test/cli/errors-and-warnings.test.js

+29
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,35 @@ tap.test('Suggest similar commands if unknown command is used', async (t) => {
5555
);
5656
});
5757

58+
tap.test('Exit early if Artillery Cloud API is not valid', async (t) => {
59+
const [exitCode, output] = await execute([
60+
'run',
61+
'--record',
62+
'--key',
63+
'123',
64+
'test/scripts/gh_215_add_token.json'
65+
]);
66+
67+
t.equal(exitCode, 7);
68+
t.ok(output.stderr.includes('API key is not recognized'));
69+
});
70+
71+
tap.test(
72+
'Exit early if Artillery Cloud API is not valid - on Fargate',
73+
async (t) => {
74+
const [exitCode, output] = await execute([
75+
'run-fargate',
76+
'--record',
77+
'--key',
78+
'123',
79+
'test/scripts/gh_215_add_token.json'
80+
]);
81+
82+
t.equal(exitCode, 7);
83+
t.ok(output.stderr.includes('API key is not recognized'));
84+
}
85+
);
86+
5887
/*
5988
@test "Running a script that uses XPath capture when libxmljs is not installed produces a warning" {
6089
if [[ ! -z `find . -name "artillery-xml-capture" -type d` ]]; then

0 commit comments

Comments
 (0)