Skip to content

Instantly share code, notes, and snippets.

@ngraf
Last active October 24, 2023 09:39
Show Gist options
  • Save ngraf/a0485c9834753c6299d5e2d95b3d2100 to your computer and use it in GitHub Desktop.
Save ngraf/a0485c9834753c6299d5e2d95b3d2100 to your computer and use it in GitHub Desktop.
A plugin for CodeceptJS to send test results to an Elasticsearch index

Example configuration in CodeceptJS config:

plugins: {
  elastic: {
    require: './src/plugins/ElasticReporter.js',
    enabled: false,                           // Set this to "true" if you want to activate plugin always. I prefer on-demand activation on command line with "-p elastic", thatswhy it is "false" in this Gist.
    protocol: 'https',
    server: 'MY_DNS_FOR_ELASTICSEARCH_SERVER', // <- REPLACE VALUE. Example value: 'elastic.example.org'
    port: 9200,
    index: `MY_NAME_OF_INDEX`,                  // <- REPLACE VALUE. Example value: 'qa-e2etests'

    // Optional: Adjust configuration of custom fields to your needs.
    // Every key/value pair you add here, will be added as a field in Elasticsearch index.
    customFields: {
        url: 'https://www.example.com',
        environment: process.env.ENVIRONMENT,
        testrun: 'Run-' + new Date(new Date().getTime() - (new Date().getTimezoneOffset() * 60000)).toISOString().slice(0, -5),
        jenkinsJob: process.env.JOB_NAME
    }
  }
}
const { event } = require("codeceptjs");
let httpOrHttps;
// Trigger report to Elasticsearch when hook "event.test.after" is fired
module.exports = (config) => {
checkConfig(config);
httpOrHttps =
config.protocol === "https" ? require("https") : require("http");
event.dispatcher.on(event.test.after, async (test) => {
try {
await reportToElastic(config, test);
} catch (err) {
console.error(
`[ERROR] (ElasticReporter) Report to Elasticsearch failed for ${test.title} with error: ` +
err
);
}
});
};
function checkConfig(config) {
const properties = ["server", "protocol", "index"];
properties.forEach((property) => {
if (!config[property]) {
throw new Error(
`[ERROR] (ElasticReporter) Configuration missing or empty for "${property}".`
);
}
});
}
/**
* Sends test result of a test scenario to Elasticsearch.
*
* @param config
* @param test
* @return {Promise<unknown>}
*/
async function reportToElastic(config, test) {
// If number of retries is > 0 and this test is not the last try and it did not pass, then don't report it. We wait for last try.
if (
parseInt(test._retries) > 0 &&
test._currentRetry < parseInt(test._retries) &&
test.state !== "passed"
) {
return Promise.resolve();
}
// Create JSON data out of test object
const jsonData = createJSON(test, config.customFields);
const options = {
hostname: config.server,
port: config.port,
rejectUnauthorized: false,
path: `/${config.index}/_doc/`,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": jsonData.length
}
};
return new Promise(function(resolve, reject) {
const req = httpOrHttps.request(options, function(res) {
if (res.statusCode === 201) {
resolve(res.statusCode);
} else {
reject(
`ERROR: Unexpected status code ${res.statusCode} from Elasticsearch with message "${res.statusMessage}"`
);
}
res.on("data", (d) => {
// process.stdout.write(d)
});
});
req.on("error", (e) => {
reject("ERROR during request:" + e);
});
req.write(jsonData);
req.end();
});
}
/**
* Creates JSON structure out of test scenario result together with custom fields.
*
* @param test
* @param customFields
* @return {string}
*/
function createJSON(test, customFields) {
const currentTimestampISO = new Date().toISOString();
const commonFields = {
"@timestamp": currentTimestampISO,
feature: test.parent ? test.parent.title : "unknown",
scenario: test.title,
file: test.file,
state: test.state,
duration: test.duration,
retries: test._currentRetry,
error:
test.state === "passed"
? ""
: test.err && test.err.message
? test.err.message
: "unknown",
failed: test.state === "failed" ? 1 : 0
};
const allFields = Object.assign({}, commonFields, customFields);
return JSON.stringify(allFields);
}
@ngraf
Copy link
Author

ngraf commented Oct 20, 2023

One example of a dashboard in Kibana based on the data pushed to the Elasticsearch index:

image

@kobenguyent
Copy link

kobenguyent commented Oct 23, 2023

I think we could try axios which is already available when installing codeceptjs.

...
	return axios.post(`${config.protocol}://${config.server}:${config.port}/${config.index}/_doc/`, jsonData, {headers: {
		'Content-Type': 'application/json',
		'Content-Length': jsonData.length
	} }).catch(e => console.log('ERROR during request:' + JSON.stringify(e.response.data)));
...

then we dont need to convert the JSON data to string

/**
 * Creates JSON structure out of test scenario result together with custom fields.
 *
 * @param test
 * @param customFields
 * @return {Object}
 */
function createJSON(test, customFields) {
	const currentTimestampISO = new Date().toISOString();

	const commonFields = {
		'@timestamp': currentTimestampISO,
		feature: test.parent ? test.parent.title : 'unknown',
		scenario: test.title,
		file: test.file,
		state: test.state,
		duration: test.duration,
		retries: test._currentRetry,
		error: test.state === 'passed' ? '' : test.err && test.err.message ? test.err.message : 'unknown',
		failed: test.state === 'failed' ? 1 : 0
	};
	return Object.assign({}, commonFields, customFields);
}

@ngraf
Copy link
Author

ngraf commented Oct 24, 2023

I think we could try axios which is already available when installing codeceptjs.

I see. I was not aware axios is around. That makes the HTTP request is easier to read and maintain for sure.
On the other hand you then build up some dependency that I will break once CodeceptJS decides to drop axios dependency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment