@elastic/synthetics
Version:
Elastic synthetic monitoring agent
383 lines • 15.1 kB
JavaScript
;
/**
* MIT License
*
* Copyright (c) 2020-present, Elastic NV
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseSchedule = exports.getAlertKeyValue = exports.parseFields = exports.parseAlertConfig = exports.buildMonitorFromYaml = exports.createLightweightMonitors = exports.buildMonitorSchema = exports.getLocalMonitors = exports.diffMonitors = void 0;
const promises_1 = require("fs/promises");
const path_1 = require("path");
const yaml_1 = require("yaml");
const colors_1 = require("kleur/colors");
const bundler_1 = require("./bundler");
const node_buffer_1 = __importDefault(require("node:buffer"));
const helpers_1 = require("../helpers");
const public_locations_1 = require("../locations/public-locations");
const monitor_1 = require("../dsl/monitor");
const utils_1 = require("./utils");
// Allowed extensions for lightweight monitor files
const ALLOWED_LW_EXTENSIONS = ['.yml', '.yaml'];
// 1500kB Max Gzipped limit for bundled monitor code to be pushed as Kibana project monitors.
const SIZE_LIMIT_KB = 1500;
function translateLocation(locations) {
if (!locations)
return [];
return locations.map(loc => public_locations_1.LocationsMap[loc] || loc).filter(Boolean);
}
class RemoteDiffResult {
// The set of monitor IDs that have been added
newIDs = new Set();
// Monitor IDs that are different locally than remotely
changedIDs = new Set();
// Monitor IDs that are no longer present locally
removedIDs = new Set();
// Monitor IDs that are identical on the remote server
unchangedIDs = new Set();
}
function diffMonitors(local, remote) {
const result = new RemoteDiffResult();
const localMonitorsIDToHash = new Map();
for (const hashID of local) {
localMonitorsIDToHash.set(hashID.journey_id, hashID.hash);
}
const remoteMonitorsIDToHash = new Map();
for (const hashID of remote) {
remoteMonitorsIDToHash.set(hashID.journey_id, hashID.hash);
}
// Compare local to remote
for (const [localID, localHash] of localMonitorsIDToHash) {
// Hash is reset to '' when a monitor is edited on the UI
if (!remoteMonitorsIDToHash.has(localID)) {
result.newIDs.add(localID);
}
else {
const remoteHash = remoteMonitorsIDToHash.get(localID);
if (remoteHash != localHash) {
result.changedIDs.add(localID);
}
else if (remoteHash === localHash) {
result.unchangedIDs.add(localID);
}
}
// We no longer need to process this ID, removing it here
// reduces the numbers considered in the next phase
remoteMonitorsIDToHash.delete(localID);
}
for (const [id] of remoteMonitorsIDToHash) {
result.removedIDs.add(id);
}
return result;
}
exports.diffMonitors = diffMonitors;
function getLocalMonitors(schemas) {
const data = [];
for (const schema of schemas) {
data.push({
journey_id: schema.id,
hash: schema.hash,
});
}
return data;
}
exports.getLocalMonitors = getLocalMonitors;
async function buildMonitorSchema(monitors, isV2) {
/**
* Set up the bundle artifacts path which can be used to
* create the bundles required for uploading journeys
*/
const bundlePath = (0, path_1.join)(helpers_1.SYNTHETICS_PATH, 'bundles');
await (0, promises_1.mkdir)(bundlePath, { recursive: true });
const bundler = new bundler_1.Bundler();
const schemas = [];
const sizes = new Map();
for (const monitor of monitors) {
const { source, config, filter, type } = monitor;
const schema = {
...config,
locations: translateLocation(config.locations),
};
if (type === 'browser') {
const outPath = (0, path_1.join)(bundlePath, (0, utils_1.normalizeMonitorName)(config.name) + '.zip');
const content = await bundler.build(source.file, outPath);
monitor.setContent(content);
Object.assign(schema, { content, filter });
}
const size = monitor.size();
const sizeKB = Math.round(size / 1000);
if (sizeKB > SIZE_LIMIT_KB) {
let outer = (0, colors_1.bold)(`Aborted: Bundled code ${sizeKB}kB exceeds the recommended ${SIZE_LIMIT_KB}kB limit. Please check the dependencies imported.\n`);
const inner = `* ${config.id} - ${source.file}:${source.line}:${source.column}\n`;
outer += (0, helpers_1.indent)(inner);
throw (0, colors_1.red)(outer);
}
sizes.set(config.id, size);
/**
* Generate hash only after the bundled content is created
* to capture code changes in imported files
*/
if (isV2) {
schema.hash = monitor.hash();
}
schemas.push(schema);
}
await (0, promises_1.rm)(bundlePath, { recursive: true });
return { schemas, sizes };
}
exports.buildMonitorSchema = buildMonitorSchema;
async function createLightweightMonitors(workDir, options) {
const lwFiles = new Set();
// Filter monitor files based on the provided pattern
const pattern = options.grepOpts?.pattern
? new RegExp(options.grepOpts?.pattern, 'i')
: /.(yml|yaml)$/;
const ignore = /(node_modules|.github)/;
await (0, helpers_1.totalist)(workDir, (rel, abs) => {
if (!ignore.test(rel) &&
pattern.test(rel) &&
ALLOWED_LW_EXTENSIONS.includes((0, path_1.extname)(abs))) {
lwFiles.add(abs);
}
});
let warnOnce = false;
const monitors = [];
for (const file of lwFiles.values()) {
// First check encoding and warn if any files are not the correct encoding.
const bufferContent = await (0, promises_1.readFile)(file);
const isUtf8 = node_buffer_1.default.isUtf8(bufferContent);
if (!isUtf8) {
(0, helpers_1.warn)(`${file} is not UTF-8 encoded. Monitors might be skipped.`);
}
const content = bufferContent.toString('utf-8');
const lineCounter = new yaml_1.LineCounter();
const parsedDoc = (0, yaml_1.parseDocument)(content, {
lineCounter,
merge: true,
keepSourceTokens: true,
});
// Skip other yml files that are not relevant
const monitorSeq = parsedDoc.get('heartbeat.monitors');
if (!monitorSeq) {
continue;
}
// Warn users about schedule that are less than 60 seconds
if (!warnOnce) {
(0, helpers_1.warn)('Lightweight monitor schedules will be adjusted to their nearest frequency supported by our synthetics infrastructure.');
warnOnce = true;
}
// Store the offsets of each monitor in the sequence to construct the source
// location later for capturing the error
const offsets = [];
for (const monNode of monitorSeq.items) {
offsets.push(monNode.srcToken.offset);
}
const mergedConfig = parsedDoc.toJS()['heartbeat.monitors'];
for (let i = 0; i < mergedConfig.length; i++) {
const monitor = mergedConfig[i];
// Skip browser monitors from the YML files
if (monitor['type'] === 'browser') {
(0, helpers_1.warn)(`Browser monitors from ${file} are skipped.`);
continue;
}
const { line, col } = lineCounter.linePos(offsets[i]);
try {
/**
* Build the monitor object from the yaml config along with global configuration
* and perform the match based on the provided filters
*/
const mon = buildMonitorFromYaml(monitor, options);
if (!mon.isMatch(options.grepOpts?.match, options.grepOpts?.tags)) {
continue;
}
mon.setSource({ file, line, column: col });
monitors.push(mon);
}
catch (e) {
let outer = (0, colors_1.bold)(`Aborted: ${e}\n`);
outer += (0, helpers_1.indent)(`* ${monitor.id || monitor.name} - ${file}:${line}:${col}\n`);
throw (0, colors_1.red)(outer);
}
}
}
return monitors;
}
exports.createLightweightMonitors = createLightweightMonitors;
const REQUIRED_MONITOR_FIELDS = ['id', 'name'];
function buildMonitorFromYaml(config, options) {
// Validate required fields
for (const field of REQUIRED_MONITOR_FIELDS) {
if (!config[field]) {
throw `Monitor ${field} is required`;
}
}
const schedule = config.schedule && parseSchedule(String(config.schedule));
const privateLocations = config['private_locations'] ||
config.privateLocations ||
options.privateLocations;
const retestOnFailure = config['retest_on_failure'] ?? options.retestOnFailure;
const alertConfig = (0, exports.parseAlertConfig)(config, options.alert);
const mon = new monitor_1.Monitor({
enabled: config.enabled ?? options.enabled,
locations: options.locations,
tags: options.tags,
fields: (0, exports.parseFields)(config, options.fields),
...normalizeConfig(config),
retestOnFailure,
privateLocations,
schedule: schedule || options.schedule,
alert: alertConfig,
});
/**
* Params support is only available for lightweight monitors
* post 8.7.2 stack
*/
if ((0, utils_1.isParamOptionSupported)(options.kibanaVersion)) {
mon.config.params = options.params;
}
return mon;
}
exports.buildMonitorFromYaml = buildMonitorFromYaml;
// Deletes unnecessary fields from the lightweight monitor config
// that is not supported by the Kibana API
function normalizeConfig(config) {
delete config['private_locations'];
delete config['retest_on_failure'];
return config;
}
const parseAlertConfig = (config, gConfig) => {
// If the user has provided a global alert config, merge it with the monitor alert config
const status = getAlertKeyValue('status', config, gConfig);
const tls = getAlertKeyValue('tls', config, gConfig);
const result = {};
if (status) {
result['status'] = status;
}
if (tls) {
result['tls'] = tls;
}
return Object.keys(result).length > 0 ? result : undefined;
};
exports.parseAlertConfig = parseAlertConfig;
const parseFields = (config, gFields) => {
// get all keys starting with `label.`
const keys = Object.keys(config).filter(key => key.startsWith('fields.'));
const fields = {};
for (const key of keys) {
fields[key.replace('fields.', '')] = config[key];
delete config[key];
}
if (gFields) {
for (const key of Object.keys(gFields)) {
fields[key] = gFields[key];
}
}
return Object.keys(fields).length > 0 ? fields : undefined;
};
exports.parseFields = parseFields;
function getAlertKeyValue(key, config, alertConfig) {
const value = config.alert;
if (value?.[key]?.enabled !== undefined) {
return {
enabled: value[key].enabled,
};
}
if (value?.[`${key}.enabled`] !== undefined) {
const val = value?.[`${key}.enabled`];
delete value?.[`${key}.enabled`];
if (Object.keys(value).length === 0) {
delete config.alert;
}
return {
enabled: val,
};
}
const rootKey = `alert.${key}.enabled`;
if (config[rootKey] !== undefined) {
const enabled = config[rootKey];
delete config[rootKey];
return {
enabled,
};
}
return alertConfig?.[key];
}
exports.getAlertKeyValue = getAlertKeyValue;
function parseSchedule(schedule) {
const EVERY_SYNTAX = '@every';
if (!(schedule + '').startsWith(EVERY_SYNTAX)) {
throw `Monitor schedule format(${schedule}) not supported: use '@every' syntax instead`;
}
const duration = schedule.substring(EVERY_SYNTAX.length + 1);
// split between non-digit (\D) and a digit (\d)
const durations = duration.split(/(?<=\D)(?=\d)/g);
let minutes = 0;
let seconds = 0;
for (const dur of durations) {
// split between a digit and non-digit
const [value, format] = dur.split(/(?<=\d)(?=\D)/g);
// Calculate based on the duration symbol
const scheduleValue = parseInt(value, 10);
switch (format) {
case 's':
if (scheduleValue < 60) {
seconds += scheduleValue;
}
else {
minutes += Math.round(scheduleValue / 60);
}
break;
case 'm':
minutes += scheduleValue;
break;
case 'h':
minutes += scheduleValue * 60;
break;
case 'd':
minutes += scheduleValue * 24 * 60;
break;
}
}
return nearestSchedule(minutes, seconds);
}
exports.parseSchedule = parseSchedule;
// Find the nearest schedule that is supported by the platform
// from the parsed schedule value
function nearestSchedule(minutes, seconds) {
if (seconds > 0 && minutes === 0) {
// we allow only 10 and 30 seconds, return the nearest one
return seconds < 20 ? '10s' : '30s';
}
let nearest = monitor_1.ALLOWED_SCHEDULES[0];
let prev = Math.abs(nearest - minutes);
for (let i = 1; i < monitor_1.ALLOWED_SCHEDULES.length; i++) {
const curr = Math.abs(monitor_1.ALLOWED_SCHEDULES[i] - minutes);
if (curr <= prev) {
nearest = monitor_1.ALLOWED_SCHEDULES[i];
prev = curr;
}
}
return nearest;
}
//# sourceMappingURL=monitor.js.map