diff --git a/src/commands/monika.ts b/src/commands/monika.ts index a653a2a0a..0f6f4844a 100644 --- a/src/commands/monika.ts +++ b/src/commands/monika.ts @@ -22,7 +22,7 @@ * SOFTWARE. * **********************************************************************************/ -import { Command, Errors, Flags } from '@oclif/core' +import { Command, Errors } from '@oclif/core' import pEvent from 'p-event' import type { Config } from '../interfaces/config' @@ -33,38 +33,27 @@ import { createConfig } from '../components/config/create' import { sortProbes } from '../components/config/sort' import { printAllLogs } from '../components/logger' import { flush } from '../components/logger/flush' -import { closeLog, openLogfile } from '../components/logger/history' +import { closeLog } from '../components/logger/history' import { logStartupMessage } from '../components/logger/startup-message' import { scheduleSummaryNotification } from '../components/notification/schedule-notification' import { sendMonikaStartMessage } from '../components/notification/start-message' +import { printSummary } from '../components/summary' import { getContext, setContext } from '../context' import events from '../events' -import { - type MonikaFlags, - monikaFlagsDefaultValue, - retryInitialDelayMs, - retryMaxDelayMs, - symonAPIVersion, - symonGetProbesIntervalMs, -} from '../flag' -import { printSummary, savePidFile } from '../jobs/summary-notification' +import { type MonikaFlags, sanitizeFlags, flags } from '../flag' +import { savePidFile } from '../jobs/summary-notification' import initLoaders from '../loaders' import { sanitizeProbe, startProbing } from '../looper' import SymonClient from '../symon' import { getEventEmitter } from '../utils/events' import { log } from '../utils/pino' - -type GetProbesParams = { - config: Config - flags: MonikaFlags -} +import { fetchAndCacheNetworkInfo } from '../utils/public-ip' const em = getEventEmitter() let symonClient: SymonClient export default class Monika extends Command { static description = 'Monika command line monitoring tool' - static examples = [ 'monika', 'monika --logs', @@ -73,208 +62,7 @@ export default class Monika extends Command { 'monika --config https://raw.githubusercontent.com/hyperjumptech/monika/main/monika.example.yml --config-interval 900', ] - static flags = { - 'auto-update': Flags.string({ - description: - 'Enable auto-update for Monika. Available options: major, minor, patch. This will make Monika terminate itself on successful update but does not restart', - }), - config: Flags.string({ - char: 'c', - default: monikaFlagsDefaultValue.config, - description: - 'JSON configuration filename or URL. If none is supplied, will look for monika.yml in the current directory', - env: 'MONIKA_JSON_CONFIG', - multiple: true, - }), - - 'config-filename': Flags.string({ - default: monikaFlagsDefaultValue['config-filename'], - dependsOn: ['config'], - description: - 'The configuration filename for config file created if there is no config file found ', - }), - - 'config-interval': Flags.integer({ - default: monikaFlagsDefaultValue['config-interval'], - dependsOn: ['config'], - description: - 'The interval (in seconds) for periodic config checking if url is used as config source', - }), - - 'create-config': Flags.boolean({ - description: - 'Create config from HAR (-H), postman (-p), insomnia (-I), sitemap (--sitemap), textfile (--text) export file, or open Monika Configuration Generator using default browser', - }), - - 'native-fetch': Flags.boolean({ - default: monikaFlagsDefaultValue['native-fetch'], - description: - 'Use native fetch Node.js API instead of Axios for HTTP client', - }), - - flush: Flags.boolean({ - description: 'Flush logs', - }), - - 'follow-redirects': Flags.integer({ - default: monikaFlagsDefaultValue['follow-redirects'], - description: - 'Monika will follow redirects as many times as the specified value here. By default, Monika will follow redirects once. To disable redirects following, set the value to zero.', - }), - - force: Flags.boolean({ - default: false, - description: 'Force commands with a yes whenever Y/n is prompted.', - }), - - har: Flags.string({ - char: 'H', // (H)ar file to - description: 'Run Monika using a HAR file', - exclusive: ['postman', 'insomnia', 'sitemap', 'text'], - multiple: false, - }), - - help: Flags.help({ char: 'h' }), - - id: Flags.string({ - char: 'i', // (i)ds to run - description: 'Define specific probe ids to run', - multiple: false, - }), - - ignoreInvalidTLS: Flags.boolean({ - description: - 'Configures whether HTTPS requests should ignore invalid certificates', - default: false, - }), - - insomnia: Flags.string({ - char: 'I', // (I)nsomnia file to - description: 'Run Monika using an Insomnia json/yaml file', - exclusive: ['har', 'postman', 'sitemap', 'text'], - multiple: false, - }), - - 'keep-verbose-logs': Flags.boolean({ - default: false, - description: 'Store all requests logs to database', - }), - - logs: Flags.boolean({ - char: 'l', // prints the (l)ogs - description: 'Print all logs.', - }), - - 'one-probe': Flags.boolean({ - dependsOn: ['sitemap'], - description: 'One Probe', - }), - - output: Flags.string({ - char: 'o', // (o)utput file to write config to - description: 'Write monika config file to this file', - multiple: false, - }), - - postman: Flags.string({ - char: 'p', // (p)ostman - description: 'Run Monika using a Postman json file.', - exclusive: ['har', 'insomnia', 'sitemap', 'text'], - multiple: false, - }), - - prometheus: Flags.integer({ - description: - 'Specifies the port the Prometheus metric server is listening on. e.g., 3001. (EXPERIMENTAL)', - exclusive: ['r'], - }), - - repeat: Flags.integer({ - char: 'r', // (r)epeat - default: 0, - description: 'Repeats the test run n times', - multiple: false, - }), - - retryInitialDelayMs, - - retryMaxDelayMs, - - sitemap: Flags.string({ - description: 'Run Monika using a Sitemap xml file.', - exclusive: ['har', 'insomnia', 'postman', 'text'], - multiple: false, - }), - - 'status-notification': Flags.string({ - description: 'Cron syntax for status notification schedule', - }), - - stun: Flags.integer({ - char: 's', // (s)stun - default: monikaFlagsDefaultValue.stun, - description: 'Interval in seconds to check STUN server', - multiple: false, - }), - - summary: Flags.boolean({ - default: false, - description: 'Display a summary of monika running stats', - }), - - 'symon-api-version': symonAPIVersion(), - - symonKey: Flags.string({ - dependsOn: ['symonUrl'], - description: 'API Key for Symon', - }), - - symonGetProbesIntervalMs, - - symonLocationId: Flags.string({ - dependsOn: ['symonKey', 'symonUrl'], - description: 'Location ID for Symon (optional)', - required: false, - }), - - symonMonikaId: Flags.string({ - dependsOn: ['symonKey', 'symonUrl'], - description: 'Monika ID for Symon (optional)', - required: false, - }), - - symonReportInterval: Flags.integer({ - dependsOn: ['symonKey', 'symonUrl'], - description: 'Interval for reporting to Symon in milliseconds (optional)', - required: false, - }), - - symonReportLimit: Flags.integer({ - dependsOn: ['symonKey', 'symonUrl'], - description: 'Data limit to be reported to Symon (optional)', - required: false, - }), - - symonUrl: Flags.string({ - dependsOn: ['symonKey'], - description: 'URL of Symon', - hidden: false, - }), - - text: Flags.string({ - description: 'Run Monika using a Simple text file', - exclusive: ['postman', 'insomnia', 'sitemap', 'har'], - multiple: false, - }), - - verbose: Flags.boolean({ - default: false, - description: 'Show verbose log messages', - }), - - version: Flags.version({ char: 'v' }), - } - + static flags = flags static id = 'monika' async catch(error: Error): Promise { @@ -298,113 +86,63 @@ export default class Monika extends Command { throw error } - deprecationHandler(config: Config): Config { - const showDeprecateMsg: Record<'query', boolean> = { - query: false, - } - - const checkedConfig = { - ...config, - probes: config.probes?.map((probe) => ({ - ...probe, - requests: probe.requests?.map((request) => ({ - ...request, - alert: request.alerts?.map((alert) => { - if (alert.query) { - showDeprecateMsg.query = true - return { ...alert, assertion: alert.query } - } - - return alert - }), - })), - alerts: probe.alerts?.map((alert) => { - if (alert.query) { - showDeprecateMsg.query = true - return { ...alert, assertion: alert.query } - } - - return alert - }), - })), - } - - if (showDeprecateMsg.query) { - log.warn('"alerts.query" is deprecated. Please use "alerts.assertion"') - } - - return checkedConfig - } - - getProbes({ config, flags }: GetProbesParams): Probe[] { - const sortedProbes = sortProbes(config.probes, flags.id) - - return sortedProbes.map((probe: Probe) => - sanitizeProbe(isSymonModeFrom(flags), probe) - ) - } - async run(): Promise { - const monika = await this.parse(Monika) - const _flags: MonikaFlags = monika.flags - setContext({ flags: _flags }) + const cmd = await this.parse(Monika) + const flags = sanitizeFlags(cmd.flags) + + setContext({ flags, userAgent: this.config.userAgent }) try { - if (_flags['create-config']) { + if (flags['create-config']) { await createConfig() return } - await openLogfile() - - if (_flags.logs) { + if (flags.logs) { await printAllLogs() - await closeLog() return } - if (_flags.flush) { - await flush(_flags.force) - await closeLog() + if (flags.flush) { + await flush() return } - if (_flags.summary) { - printSummary(this.config) - await closeLog() + if (flags.summary) { + await printSummary() return } - await initLoaders(_flags, this.config) + await initLoaders(flags, this.config) + + const isSymonMode = isSymonModeFrom(flags) + await logRunningInfo({ isSymonMode, isVerbose: flags.verbose }) - if (isSymonModeFrom(_flags)) { - symonClient = new SymonClient(_flags) + if (isSymonMode) { + symonClient = new SymonClient(flags) await symonClient.initiate() } let isFirstRun = true for (;;) { - const controller = new AbortController() - const { signal } = controller const config = getConfig() - const notifications = config.notifications || [] - const probes = this.getProbes({ config, flags: _flags }) + const probes = getProbes({ config, flags }) // emit the sanitized probe em.emit(events.config.sanitized, probes) - // save some data into files for later - savePidFile(_flags.config, config) - - this.deprecationHandler(config) - + savePidFile(flags.config, config) + deprecationHandler(config) logStartupMessage({ config, - flags: _flags, + flags, isFirstRun, }) + const controller = new AbortController() + const { signal } = controller + const notifications = config.notifications || [] startProbing({ notifications, probes, @@ -418,9 +156,8 @@ export default class Monika extends Command { sendMonikaStartMessage(notifications).catch((error) => log.error(error.message) ) - // schedule status update notification - scheduleSummaryNotification({ config, flags: _flags }) + scheduleSummaryNotification({ config, flags }) isFirstRun = false @@ -430,23 +167,93 @@ export default class Monika extends Command { controller.abort('Monika configuration updated') } } catch (error: unknown) { - await closeLog() this.error((error as Error)?.message, { exit: 1 }) + } finally { + await closeLog() } } } +type RunningInfoParams = { isSymonMode: boolean; isVerbose: boolean } + +async function logRunningInfo({ isVerbose, isSymonMode }: RunningInfoParams) { + if (!isVerbose && !isSymonMode) { + log.info('Monika is running.') + return + } + + try { + const { city, hostname, isp, privateIp, publicIp } = + await fetchAndCacheNetworkInfo() + + log.info( + `Monika is running from: ${city} - ${isp} (${publicIp}) - ${hostname} (${privateIp})` + ) + } catch (error) { + log.warn(`Failed to obtain location/ISP info. Got: ${error}`) + } +} + +type GetProbesParams = { + config: Config + flags: MonikaFlags +} + +function getProbes({ config, flags }: GetProbesParams): Probe[] { + const sortedProbes = sortProbes(config.probes, flags.id) + + return sortedProbes.map((probe: Probe) => + sanitizeProbe(isSymonModeFrom(flags), probe) + ) +} + +function deprecationHandler(config: Config): Config { + const showDeprecateMsg: Record<'query', boolean> = { + query: false, + } + + const checkedConfig = { + ...config, + probes: config.probes?.map((probe) => ({ + ...probe, + requests: probe.requests?.map((request) => ({ + ...request, + alert: request.alerts?.map((alert) => { + if (alert.query) { + showDeprecateMsg.query = true + return { ...alert, assertion: alert.query } + } + + return alert + }), + })), + alerts: probe.alerts?.map((alert) => { + if (alert.query) { + showDeprecateMsg.query = true + return { ...alert, assertion: alert.query } + } + + return alert + }), + })), + } + + if (showDeprecateMsg.query) { + log.warn('"alerts.query" is deprecated. Please use "alerts.assertion"') + } + + return checkedConfig +} + /** * Show Exit Message */ process.on('SIGINT', async () => { - if (!process.env.DISABLE_EXIT_MESSAGE) { - log.info('Thank you for using Monika!') - log.info('We need your help to make Monika better.') - log.info( - 'Can you give us some feedback by clicking this link https://github.com/hyperjumptech/monika/discussions?\n' - ) - } + log.info('Thank you for using Monika!') + log.info('We need your help to make Monika better.') + log.info( + 'Can you give us some feedback by clicking this link https://github.com/hyperjumptech/monika/discussions?\n' + ) if (symonClient) { await symonClient.sendStatus({ isOnline: false }) diff --git a/src/components/config/index.ts b/src/components/config/index.ts index 7d9a8c7a5..b1fea4cd9 100644 --- a/src/components/config/index.ts +++ b/src/components/config/index.ts @@ -28,7 +28,6 @@ import isUrl from 'is-url' import events from '../../events' import type { Config } from '../../interfaces/config' import { getContext, setContext } from '../../context' -import { monikaFlagsDefaultValue } from '../../flag' import type { MonikaFlags } from '../../flag' import { getEventEmitter } from '../../utils/events' import { md5Hash } from '../../utils/hash' @@ -146,9 +145,7 @@ async function watchConfigsChange(flags: MonikaFlags) { flags.config.map((source, index) => watchConfigChange({ flags, - interval: - flags['config-interval'] || - monikaFlagsDefaultValue['config-interval'], + interval: flags['config-interval'], source, type: 'monika', index, diff --git a/src/components/logger/flush.test.ts b/src/components/logger/flush.test.ts index b9db1864d..60fe3523c 100644 --- a/src/components/logger/flush.test.ts +++ b/src/components/logger/flush.test.ts @@ -22,14 +22,15 @@ * SOFTWARE. * **********************************************************************************/ -import sinon from 'sinon' +import { type SinonStub, assert, stub } from 'sinon' +import { getContext, resetContext, setContext } from '../../context' import * as history from './history' import { flush } from './flush' -let flushAllLogsStub: sinon.SinonStub +let flushAllLogsStub: SinonStub beforeEach(() => { - flushAllLogsStub = sinon.stub(history, 'flushAllLogs').resolves() + flushAllLogsStub = stub(history, 'flushAllLogs').resolves() }) afterEach(() => { @@ -39,11 +40,16 @@ afterEach(() => { describe('Flush command', () => { describe('Force', () => { it('should flush records without asking for confirmation', async () => { + // arrange + setContext({ flags: { ...getContext().flags, force: true } }) + // act - await flush(true) + await flush() // assert - sinon.assert.calledOnce(flushAllLogsStub) + assert.calledOnce(flushAllLogsStub) + + resetContext() }) }) }) diff --git a/src/components/logger/flush.ts b/src/components/logger/flush.ts index 68d68b203..3338ee03d 100644 --- a/src/components/logger/flush.ts +++ b/src/components/logger/flush.ts @@ -1,25 +1,22 @@ import { ux } from '@oclif/core' -import { flushAllLogs } from './history' +import { getContext } from '../../context' import { log } from '../../utils/pino' +import { flushAllLogs, openLogfile } from './history' -export async function flush(isForce: boolean): Promise { - if (isForce) { - await flushAllLogs() - log.info('Records flushed, thank you.') +export async function flush(): Promise { + if (!getContext().flags.force) { + const answer = await ux.ux.prompt( + 'Are you sure you want to flush all logs in monika-logs.db (Y/n)?' + ) - return - } - - const ans = await ux.ux.prompt( - 'Are you sure you want to flush all logs in monika-logs.db (Y/n)?' - ) - - if (ans === 'Y') { - await flushAllLogs() - log.info('Records flushed, thank you.') + if (answer !== 'Y') { + log.info('Cancelled. Thank you.') - return + return + } } - log.info('Cancelled. Thank you.') + await openLogfile() + await flushAllLogs() + log.info('Records flushed, thank you.') } diff --git a/src/components/logger/history.ts b/src/components/logger/history.ts index cfe9eee45..3c8e2e14e 100644 --- a/src/components/logger/history.ts +++ b/src/components/logger/history.ts @@ -30,7 +30,6 @@ import { ProbeRequestResponse } from '../../interfaces/request' import { Probe } from '../../interfaces/probe' import type { Notification } from '@hyperjumptech/monika-notification' import { log } from '../../utils/pino' -import { getConfig } from '../config' import { getErrorMessage } from '../../utils/catch-error-handler' const sqlite3 = verboseSQLite() const dbPath = path.resolve(process.cwd(), 'monika-logs.db') @@ -84,7 +83,6 @@ export type DeleteProbeRes = { } type Summary = { - numberOfProbes: number numberOfIncidents: number numberOfRecoveries: number numberOfSentNotifications: number @@ -558,10 +556,7 @@ export async function getSummary(): Promise { 0 ) - const config = getConfig() - return { - numberOfProbes: config?.probes?.length || 0, numberOfIncidents, numberOfRecoveries, numberOfSentNotifications, diff --git a/src/components/logger/index.ts b/src/components/logger/index.ts index c5b66b680..c344edf65 100644 --- a/src/components/logger/index.ts +++ b/src/components/logger/index.ts @@ -23,15 +23,35 @@ **********************************************************************************/ import chalk from 'chalk' -import { getAllLogs } from './history' import { log } from '../../utils/pino' +import { getAllLogs, openLogfile } from './history' + +// printAllLogs dumps the content of monika-logs.db onto the screen +export async function printAllLogs(): Promise { + await openLogfile() + const data = await getAllLogs() + + for (const { + id, + probeId, + requestUrl, + responseStatus, + responseTime, + } of data) { + log.info( + `${id} id: ${probeId} responseCode: ${chalk.keyword( + getStatusColor(responseStatus) + )(String(responseStatus))} - ${requestUrl}, ${responseTime || '- '}ms` + ) + } +} /** * getStatusColor colorizes different statusCode * @param {any} responseCode is the httpStatus to colorize * @returns {string} color code based on chalk: Chalk & { supportsColor: ColorSupport }; */ -function getStatusColor(responseCode: number) { +function getStatusColor(responseCode: number): string { switch (Math.trunc(responseCode / 100)) { case 2: { return 'cyan' @@ -50,21 +70,3 @@ function getStatusColor(responseCode: number) { return 'white' } - -/** - * printAllLogs dumps the content of monika-logs.db onto the screen - * @returns Promise - */ -export async function printAllLogs(): Promise { - const data = await getAllLogs() - - for (const row of data) { - log.info( - `${row.id} id: ${row.probeId} responseCode: ${chalk.keyword( - getStatusColor(row.responseStatus) - )(String(row.responseStatus))} - ${row.requestUrl}, ${ - row.responseTime || '- ' - }ms` - ) - } -} diff --git a/src/components/logger/startup-message.test.ts b/src/components/logger/startup-message.test.ts index 0b3f49b97..c27f3209d 100644 --- a/src/components/logger/startup-message.test.ts +++ b/src/components/logger/startup-message.test.ts @@ -24,6 +24,7 @@ import chai, { expect } from 'chai' import spies from 'chai-spies' +import { sanitizeFlags } from '../../flag' import type { Config } from '../../interfaces/config' import { log } from '../../utils/pino' import { logStartupMessage } from './startup-message' @@ -82,12 +83,12 @@ describe('Startup message', () => { // act logStartupMessage({ config: { probes: [] }, - flags: { + flags: sanitizeFlags({ config: [], symonKey: 'secret-key', symonUrl: 'https://example.com', verbose: false, - }, + }), isFirstRun: false, }) @@ -102,7 +103,12 @@ describe('Startup message', () => { // arrange logStartupMessage({ config: { probes: [] }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: false }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: false, + }), isFirstRun: false, }) @@ -116,7 +122,12 @@ describe('Startup message', () => { // arrange logStartupMessage({ config: defaultConfig, - flags: { config: [], symonKey: '', symonUrl: '', verbose: false }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: false, + }), isFirstRun: true, }) @@ -128,7 +139,12 @@ describe('Startup message', () => { // arrange logStartupMessage({ config: defaultConfig, - flags: { config: [], symonKey: '', symonUrl: '', verbose: false }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: false, + }), isFirstRun: false, }) @@ -140,7 +156,12 @@ describe('Startup message', () => { // arrange logStartupMessage({ config: defaultConfig, - flags: { config: [], symonKey: '', symonUrl: '', verbose: false }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: false, + }), isFirstRun: true, }) @@ -156,7 +177,12 @@ describe('Startup message', () => { ...defaultConfig, notifications: [], }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: false }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: false, + }), isFirstRun: true, }) @@ -180,7 +206,12 @@ describe('Startup message', () => { }, ], }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -205,7 +236,12 @@ describe('Startup message', () => { }, ], }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -221,7 +257,12 @@ describe('Startup message', () => { // arrange logStartupMessage({ config: defaultConfig, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -254,7 +295,12 @@ describe('Startup message', () => { }, ], }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -268,7 +314,12 @@ describe('Startup message', () => { // arrange logStartupMessage({ config: defaultConfig, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -295,7 +346,12 @@ describe('Startup message', () => { }, ], }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -313,7 +369,12 @@ describe('Startup message', () => { ...defaultConfig, notifications: [{ id: 'UVIsL', type: 'desktop', data: undefined }], }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -341,7 +402,12 @@ describe('Startup message', () => { }, ], }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -368,7 +434,12 @@ describe('Startup message', () => { }, ], }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -390,7 +461,12 @@ describe('Startup message', () => { }, ], }, - flags: { config: [], symonKey: '', symonUrl: '', verbose: true }, + flags: sanitizeFlags({ + config: [], + symonKey: '', + symonUrl: '', + verbose: true, + }), isFirstRun: true, }) // assert @@ -404,12 +480,12 @@ describe('Startup message', () => { // act logStartupMessage({ config: defaultConfig, - flags: { + flags: sanitizeFlags({ config: ['https://example.com/monika.yaml'], symonKey: '', symonUrl: '', verbose: false, - }, + }), isFirstRun: false, }) @@ -421,12 +497,12 @@ describe('Startup message', () => { // act logStartupMessage({ config: defaultConfig, - flags: { + flags: sanitizeFlags({ config: ['./monika.yaml'], symonKey: '', symonUrl: '', verbose: false, - }, + }), isFirstRun: false, }) diff --git a/src/components/summary.ts b/src/components/summary.ts new file mode 100644 index 000000000..ffdd9c6be --- /dev/null +++ b/src/components/summary.ts @@ -0,0 +1,77 @@ +import { readFile } from 'node:fs/promises' +import { hostname } from 'node:os' +import type { Notification } from '@hyperjumptech/monika-notification' + +import { getContext } from '../context' +import type { Probe } from '../interfaces/probe' +import getIp from '../utils/ip' +import { log } from '../utils/pino' +import { publicIpAddress } from '../utils/public-ip' +import { getSummary, openLogfile } from './logger/history' + +// printSummary gathers and print some stats +export async function printSummary(): Promise { + await openLogfile() + + const [pidFileContent, summary] = await Promise.all([ + readPidFile(), + getSummary(), + ]) + const { + monikaConfigFile, + monikaNotifs, + monikaPid, + monikaProbes, + monikaStartTime, + } = pidFileContent + const { numberOfIncidents, numberOfRecoveries, numberOfSentNotifications } = + summary + const uptime = getDaysHours(monikaStartTime) + const host = `${hostname()} (${[publicIpAddress, getIp()] + .filter(Boolean) + .join('/')})` + + log.info(`Monika Summary + +Monika process id\t: ${monikaPid} +Active config file\t: ${monikaConfigFile} +Probes set\t\t: ${monikaProbes} +Notifications set\t: ${monikaNotifs} +Number of incidents\t: ${numberOfIncidents} in last 24hr +Number of recoveries\t: ${numberOfRecoveries} in last 24hr +Number of notifications\t: ${numberOfSentNotifications} in last 24h + +Up time\t\t: ${uptime} +Running on\t: ${host} +App version\t: ${getContext().userAgent}`) +} + +// getDaysHours breaks down the date into days hours minute string +function getDaysHours(startTime: Date): string { + let duration = Math.abs((Date.now() - new Date(startTime).getTime()) / 1000) + const numDays = Math.floor(duration / 86_400) + duration -= numDays * 86_400 // get the remaining hours + + const numHours = Math.floor(duration / 3600) % 24 + duration -= numHours * 3600 // get the remaining minutes + + const numMinutes = Math.floor(duration / 60) % 60 + + const numSeconds = Math.floor(duration - numMinutes * 60) + + return `${numDays} days, ${numHours} hours, ${numMinutes} minutes, ${numSeconds} seconds` +} + +type PidObject = { + monikaPid: number + monikaConfigFile: string + monikaStartTime: Date + monikaProbes: Probe + monikaNotifs: Notification +} +// readsPidFile reads a local monika.pid file and returns the information in it +async function readPidFile(): Promise { + const fileContent = await readFile('monika.pid', { encoding: 'utf8' }) + + return JSON.parse(fileContent) +} diff --git a/src/context/monika-flags.ts b/src/flag.test.ts similarity index 81% rename from src/context/monika-flags.ts rename to src/flag.test.ts index b3926281d..c0173f81f 100644 --- a/src/context/monika-flags.ts +++ b/src/flag.test.ts @@ -22,16 +22,15 @@ * SOFTWARE. * **********************************************************************************/ -import fs from 'fs' -import path from 'path' +import { expect } from '@oclif/test' +import { monikaFlagsDefaultValue, sanitizeFlags } from './flag' -export function getDefaultConfig(): Array { - const filesArray = fs.readdirSync(path.dirname('../')) - const monikaDotJsonFile = filesArray.find((x) => x === 'monika.json') - const monikaDotYamlFile = filesArray.find( - (x) => x === 'monika.yml' || x === 'monika.yaml' - ) - const defaultConfig = monikaDotYamlFile || monikaDotJsonFile +describe('Flag', () => { + it('should fill flags with the default value', () => { + // act + const sanitizedFlags = sanitizeFlags({}) - return defaultConfig ? [defaultConfig] : [] -} + // assert + expect(sanitizedFlags).deep.eq(monikaFlagsDefaultValue) + }) +}) diff --git a/src/flag.ts b/src/flag.ts index e2a2c8ea4..d7b37f83f 100644 --- a/src/flag.ts +++ b/src/flag.ts @@ -22,9 +22,10 @@ * SOFTWARE. * **********************************************************************************/ -import { Flags } from '@oclif/core' +import fs from 'node:fs' +import path from 'node:path' -import { getDefaultConfig } from './context/monika-flags' +import { Flags } from '@oclif/core' export enum SYMON_API_VERSION { 'v1' = 'v1', @@ -37,18 +38,17 @@ export type MonikaFlags = { 'config-filename': string 'config-interval': number 'create-config': boolean - 'native-fetch'?: boolean + 'native-fetch': boolean flush: boolean 'follow-redirects': number force: boolean har?: string id?: string insomnia?: string - json?: boolean 'keep-verbose-logs': boolean logs: boolean 'one-probe': boolean - output?: string + output: string postman?: string prometheus?: number repeat: number @@ -58,64 +58,231 @@ export type MonikaFlags = { 'status-notification'?: string stun: number summary: boolean - 'symon-api-version'?: SYMON_API_VERSION + 'symon-api-version': SYMON_API_VERSION symonKey?: string symonLocationId?: string symonMonikaId?: string - symonReportInterval?: number - symonReportLimit?: number + symonReportInterval: number + symonReportLimit: number symonGetProbesIntervalMs: number symonUrl?: string text?: string - ignoreInvalidTLS?: boolean + ignoreInvalidTLS: boolean verbose: boolean - version: void } +const DEFAULT_CONFIG_INTERVAL_SECONDS = 900 +const DEFAULT_SYMON_REPORT_INTERVAL_MS = 10_000 + export const monikaFlagsDefaultValue: MonikaFlags = { config: getDefaultConfig(), 'config-filename': 'monika.yml', - 'config-interval': 900, + 'config-interval': DEFAULT_CONFIG_INTERVAL_SECONDS, 'create-config': false, - 'native-fetch': false, flush: false, 'follow-redirects': 21, force: false, + ignoreInvalidTLS: false, 'keep-verbose-logs': false, logs: false, + 'native-fetch': false, 'one-probe': false, + output: 'monika.yml', repeat: 0, retryInitialDelayMs: 2000, retryMaxDelayMs: 30_000, // default is 20s interval lookup stun: 20, summary: false, - symonGetProbesIntervalMs: Number.parseInt( - process.env.FETCH_PROBES_INTERVAL ?? '60000', - 10 - ), + 'symon-api-version': SYMON_API_VERSION.v1, + symonGetProbesIntervalMs: 60_000, + symonReportInterval: DEFAULT_SYMON_REPORT_INTERVAL_MS, + symonReportLimit: 100, verbose: false, - version: undefined, } -export const symonAPIVersion = Flags.custom({ - default: SYMON_API_VERSION.v1, - description: - 'Symon API version to use. Available options: v1, v2. Default: v1', - options: [SYMON_API_VERSION.v1, SYMON_API_VERSION.v2], -}) +function getDefaultConfig(): Array { + const filesArray = fs.readdirSync(path.dirname('../')) + const monikaDotJsonFile = filesArray.find((x) => x === 'monika.json') + const monikaDotYamlFile = filesArray.find( + (x) => x === 'monika.yml' || x === 'monika.yaml' + ) + const defaultConfig = monikaDotYamlFile || monikaDotJsonFile -export const symonGetProbesIntervalMs = Flags.integer({ - default: monikaFlagsDefaultValue.symonGetProbesIntervalMs, - description: `To determine how often Monika sends a request to Symon to get probe data, in milliseconds. Defaults to ${monikaFlagsDefaultValue.symonGetProbesIntervalMs}ms`, -}) + return defaultConfig ? [defaultConfig] : [] +} -export const retryInitialDelayMs = Flags.integer({ - default: monikaFlagsDefaultValue.retryInitialDelayMs, - description: `The initial, first delay of the backoff retry when probe request is failed, in milliseconds. Defaults to ${monikaFlagsDefaultValue.retryInitialDelayMs}ms`, -}) +export const flags = { + 'auto-update': Flags.string({ + description: + 'Enable auto-update for Monika. Available options: major, minor, patch. This will make Monika terminate itself on successful update but does not restart', + }), + config: Flags.string({ + char: 'c', + default: monikaFlagsDefaultValue.config, + description: + 'JSON configuration filename or URL. If none is supplied, will look for monika.yml in the current directory', + env: 'MONIKA_JSON_CONFIG', + multiple: true, + }), + 'config-filename': Flags.string({ + default: monikaFlagsDefaultValue['config-filename'], + dependsOn: ['config'], + description: + 'The configuration filename for config file created if there is no config file found ', + }), + 'config-interval': Flags.integer({ + default: monikaFlagsDefaultValue['config-interval'], + dependsOn: ['config'], + description: + 'The interval (in seconds) for periodic config checking if url is used as config source', + }), + 'create-config': Flags.boolean({ + description: + 'Create config from HAR (-H), postman (-p), insomnia (-I), sitemap (--sitemap), textfile (--text) export file, or open Monika Configuration Generator using default browser', + default: monikaFlagsDefaultValue['create-config'], + }), + flush: Flags.boolean({ + description: 'Flush logs', + default: monikaFlagsDefaultValue.flush, + }), + 'follow-redirects': Flags.integer({ + default: monikaFlagsDefaultValue['follow-redirects'], + description: + 'Monika will follow redirects as many times as the specified value here. By default, Monika will follow redirects once. To disable redirects following, set the value to zero.', + }), + force: Flags.boolean({ + default: monikaFlagsDefaultValue.force, + description: 'Force commands with a yes whenever Y/n is prompted.', + }), + har: Flags.string({ + char: 'H', // (H)ar file to + description: 'Run Monika using a HAR file', + exclusive: ['postman', 'insomnia', 'sitemap', 'text'], + }), + help: Flags.help({ char: 'h' }), + id: Flags.string({ + char: 'i', // (i)ds to run + description: 'Define specific probe ids to run', + }), + ignoreInvalidTLS: Flags.boolean({ + description: + 'Configures whether HTTPS requests should ignore invalid certificates', + default: monikaFlagsDefaultValue.ignoreInvalidTLS, + }), + insomnia: Flags.string({ + char: 'I', // (I)nsomnia file to + description: 'Run Monika using an Insomnia json/yaml file', + exclusive: ['har', 'postman', 'sitemap', 'text'], + }), + 'keep-verbose-logs': Flags.boolean({ + default: monikaFlagsDefaultValue['keep-verbose-logs'], + description: 'Store all requests logs to database', + }), + logs: Flags.boolean({ + char: 'l', // prints the (l)ogs + description: 'Print all logs.', + default: monikaFlagsDefaultValue.logs, + }), + 'native-fetch': Flags.boolean({ + default: monikaFlagsDefaultValue['native-fetch'], + description: + 'Use native fetch Node.js API instead of Axios for HTTP client', + }), + 'one-probe': Flags.boolean({ + dependsOn: ['sitemap'], + description: 'One Probe', + }), + output: Flags.string({ + char: 'o', // (o)utput file to write config to + description: 'Write monika config file to this file', + default: monikaFlagsDefaultValue.output, + }), + postman: Flags.string({ + char: 'p', // (p)ostman + description: 'Run Monika using a Postman json file.', + exclusive: ['har', 'insomnia', 'sitemap', 'text'], + }), + prometheus: Flags.integer({ + description: + 'Specifies the port the Prometheus metric server is listening on. e.g., 3001. (EXPERIMENTAL)', + exclusive: ['r'], + }), + repeat: Flags.integer({ + char: 'r', // (r)epeat + default: monikaFlagsDefaultValue.repeat, + description: 'Repeats the test run n times', + }), + retryInitialDelayMs: Flags.integer({ + default: monikaFlagsDefaultValue.retryInitialDelayMs, + description: `The initial, first delay of the backoff retry when probe request is failed, in milliseconds. Defaults to ${monikaFlagsDefaultValue.retryInitialDelayMs}ms`, + }), + retryMaxDelayMs: Flags.integer({ + default: monikaFlagsDefaultValue.retryMaxDelayMs, + description: `Maximum backoff retry delay, in milliseconds. Defaults to ${monikaFlagsDefaultValue.retryMaxDelayMs}ms.`, + }), + sitemap: Flags.string({ + description: 'Run Monika using a Sitemap xml file.', + exclusive: ['har', 'insomnia', 'postman', 'text'], + }), + 'status-notification': Flags.string({ + description: 'Cron syntax for status notification schedule', + }), + stun: Flags.integer({ + char: 's', // (s)stun + default: monikaFlagsDefaultValue.stun, + description: 'Interval in seconds to check STUN server', + }), + summary: Flags.boolean({ + default: monikaFlagsDefaultValue.summary, + description: 'Display a summary of monika running stats', + }), + 'symon-api-version': Flags.custom({ + default: monikaFlagsDefaultValue['symon-api-version'], + description: + 'Symon API version to use. Available options: v1, v2. Default: v1', + options: [SYMON_API_VERSION.v1, SYMON_API_VERSION.v2], + })(), + symonKey: Flags.string({ + dependsOn: ['symonUrl'], + description: 'API Key for Symon', + }), + symonGetProbesIntervalMs: Flags.integer({ + default: monikaFlagsDefaultValue.symonGetProbesIntervalMs, + description: `To determine how often Monika sends a request to Symon to get probe data, in milliseconds. Defaults to ${monikaFlagsDefaultValue.symonGetProbesIntervalMs}ms`, + }), + symonLocationId: Flags.string({ + dependsOn: ['symonKey', 'symonUrl'], + description: 'Location ID for Symon (optional)', + }), + symonMonikaId: Flags.string({ + dependsOn: ['symonKey', 'symonUrl'], + description: 'Monika ID for Symon (optional)', + }), + symonReportInterval: Flags.integer({ + dependsOn: ['symonKey', 'symonUrl'], + description: 'Interval for reporting to Symon in milliseconds (optional)', + }), + symonReportLimit: Flags.integer({ + dependsOn: ['symonKey', 'symonUrl'], + description: 'Data limit to be reported to Symon (optional)', + }), + symonUrl: Flags.string({ + dependsOn: ['symonKey'], + description: 'URL of Symon', + hidden: false, + }), + text: Flags.string({ + description: 'Run Monika using a Simple text file', + exclusive: ['postman', 'insomnia', 'sitemap', 'har'], + }), + verbose: Flags.boolean({ + default: monikaFlagsDefaultValue.verbose, + description: 'Show verbose log messages', + }), + version: Flags.version({ char: 'v' }), +} -export const retryMaxDelayMs = Flags.integer({ - default: monikaFlagsDefaultValue.retryMaxDelayMs, - description: `Maximum backoff retry delay, in milliseconds. Defaults to ${monikaFlagsDefaultValue.retryMaxDelayMs}ms.`, -}) +export function sanitizeFlags(flags: Partial): MonikaFlags { + return { ...monikaFlagsDefaultValue, ...flags } +} diff --git a/src/jobs/summary-notification.ts b/src/jobs/summary-notification.ts index 757fc43d3..830648bd3 100644 --- a/src/jobs/summary-notification.ts +++ b/src/jobs/summary-notification.ts @@ -21,19 +21,14 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * * SOFTWARE. * **********************************************************************************/ -import { hostname } from 'os' + +import fs from 'node:fs' +import { hostname } from 'node:os' +import { sendNotifications } from '@hyperjumptech/monika-notification' import format from 'date-fns/format' + import { getConfig } from '../components/config' import { getSummary } from '../components/logger/history' -import { sendNotifications } from '@hyperjumptech/monika-notification' -import { - getOSName, - getMonikaInstance, -} from '../components/notification/alert-message' -import { getContext } from '../context' -import getIp from '../utils/ip' -import { log } from '../utils/pino' -import { publicIpAddress } from '../utils/public-ip' import { maxResponseTime, minResponseTime, @@ -42,16 +37,19 @@ import { resetlogs, getLogLifeTimeInHour, } from '../components/logger/response-time-log' -import fs from 'fs' -import type { Config as IConfig } from '@oclif/core' +import { + getOSName, + getMonikaInstance, +} from '../components/notification/alert-message' +import { getContext } from '../context' import events from '../events' -import { Config } from '../interfaces/config' - +import type { Config } from '../interfaces/config' import { getEventEmitter } from '../utils/events' -import type { Notification } from '@hyperjumptech/monika-notification' -import type { Probe } from '../interfaces/probe' import { getErrorMessage } from '../utils/catch-error-handler' -import { readFile } from '../utils/read-file' +import getIp from '../utils/ip' +import { log } from '../utils/pino' +import { publicIpAddress } from '../utils/public-ip' + const eventEmitter = getEventEmitter() type TweetMessage = { @@ -63,7 +61,7 @@ type TweetMessage = { export async function getSummaryAndSendNotif(): Promise { const config = getConfig() - const { notifications } = config + const { notifications, probes } = config if (!notifications) return @@ -75,13 +73,10 @@ export async function getSummaryAndSendNotif(): Promise { getOSName(), getMonikaInstance(privateIpAddress), ]) - const { - numberOfIncidents, - numberOfProbes, - numberOfRecoveries, - numberOfSentNotifications, - } = summary + const { numberOfIncidents, numberOfRecoveries, numberOfSentNotifications } = + summary const responseTimelogLifeTimeInHour = getLogLifeTimeInHour() + const numberOfProbes = probes.length const tweetMessage = createTweetMessage({ averageResponseTime, numberOfIncidents, @@ -118,6 +113,7 @@ ${tweetMessage} averageResponseTime, responseTimelogLifeTimeInHour, version: userAgent, + numberOfProbes, ...summary, }, }).catch((error) => log.error(`Summary notification: ${error.message}`)) @@ -130,30 +126,6 @@ ${tweetMessage} } } -interface PidObject { - monikaPid: number - monikaConfigFile: string - monikaStartTime: Date - monikaProbes: Probe - monikaNotifs: Notification -} - -/** - * readsPidFile reads a local monika.pid file and returns the information in it - * @returns {object} PidObject is returned - */ -async function readPidFile(): Promise { - const fileContent = await readFile('monika.pid', 'utf8') - const json = JSON.parse(fileContent) - return { - monikaPid: json.monikaPid, - monikaConfigFile: json.monikaConfigFile, - monikaStartTime: json.monikaStartTime, - monikaProbes: json.monikaProbes, - monikaNotifs: json.monikaNotifs, - } -} - /** * savePidFile saves a monika.pid file with some useful information * @param {string} configFile is the configuration file used @@ -185,61 +157,6 @@ eventEmitter.on(events.application.terminated, async () => { }) }) -/** - * getDaysHours breaks down the date into days hours minute string - * @param {Date} startTime is the start time - * @returns {string} text of the date to print - */ -function getDaysHours(startTime: Date): string { - let duration = Math.abs((Date.now() - new Date(startTime).getTime()) / 1000) - const numDays = Math.floor(duration / 86_400) - duration -= numDays * 86_400 // get the remaining hours - - const numHours = Math.floor(duration / 3600) % 24 - duration -= numHours * 3600 // get the remaining minutes - - const numMinutes = Math.floor(duration / 60) % 60 - - const numSeconds = Math.floor(duration - numMinutes * 60) - - return `${numDays} days, ${numHours} hours, ${numMinutes} minutes, ${numSeconds} seconds` -} - -/** - * printSummary gathers and print some stats - * @param {object} cliConfig is oclif config structure - * @returns Promise - */ -export async function printSummary(cliConfig: IConfig): Promise { - try { - const pidObject = await readPidFile() - const summary = await getSummary() - - const uptime = getDaysHours(pidObject.monikaStartTime) - - const host = `${hostname()} (${[publicIpAddress, getIp()] - .filter(Boolean) - .join('/')})` - - log.info(`Monika Summary \n - Monika process id \t\t: ${pidObject.monikaPid} - Active config file \t\t: ${pidObject.monikaConfigFile} - Probes set \t\t\t: ${pidObject.monikaProbes} - Notifications set \t\t: ${pidObject.monikaNotifs} - Number of incidents \t: ${summary.numberOfIncidents} in last 24hr - Number of recoveries \t: ${summary.numberOfRecoveries} in last 24hr - Number of notifications \t: ${summary.numberOfSentNotifications} in last 24h - - Up time \t: ${uptime} - Running on \t: ${host} - App version : ${cliConfig.userAgent} - - `) - } catch (error: unknown) { - log.error(`Summary notification: ${getErrorMessage(error)}`) - } -} - function createTweetMessage({ averageResponseTime, numberOfIncidents, diff --git a/src/loaders/index.ts b/src/loaders/index.ts index c915c85ec..f0c7c0d78 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -24,7 +24,8 @@ import type { Config as IConfig } from '@oclif/core' import { isSymonModeFrom, setupConfig } from '../components/config' -import { getContext, setContext } from '../context' +import { openLogfile } from '../components/logger/history' +import { getContext } from '../context' import events from '../events' import type { MonikaFlags } from '../flag' import { tlsChecker } from '../jobs/tls-check' @@ -35,10 +36,8 @@ import { startPrometheusMetricsServer, } from '../plugins/metrics/prometheus' import { getEventEmitter } from '../utils/events' -import { fetchAndCacheNetworkInfo } from '../utils/public-ip' import { jobsLoader } from './jobs' import { enableAutoUpdate } from '../plugins/updater' -import { log } from '../utils/pino' // import the subscriber file to activate the event emitter subscribers import '../events/subscribers/application' @@ -48,13 +47,9 @@ export default async function init( flags: MonikaFlags, cliConfig: IConfig ): Promise { - const isSymonMode = isSymonModeFrom(flags) - - setContext({ userAgent: cliConfig.userAgent }) - await logRunningInfo({ isSymonMode, isVerbose: flags.verbose }) - + await openLogfile() // check if connected to STUN Server and getting the public IP in the same time - loopCheckSTUNServer(flags.stun) + await loopCheckSTUNServer(flags.stun) // run auto-updater if (flags['auto-update']) { await enableAutoUpdate(cliConfig, flags['auto-update']) @@ -65,7 +60,7 @@ export default async function init( initPrometheus(flags.prometheus) } - if (!isSymonMode) { + if (!isSymonModeFrom(flags)) { await setupConfig(flags) // check TLS when Monika starts @@ -78,26 +73,6 @@ export default async function init( } } -type RunningInfoParams = { isSymonMode: boolean; isVerbose: boolean } - -async function logRunningInfo({ isVerbose, isSymonMode }: RunningInfoParams) { - if (!isVerbose && !isSymonMode) { - log.info('Monika is running.') - return - } - - try { - const { city, hostname, isp, privateIp, publicIp } = - await fetchAndCacheNetworkInfo() - - log.info( - `Monika is running from: ${city} - ${isp} (${publicIp}) - ${hostname} (${privateIp})` - ) - } catch (error) { - log.warn(`Failed to obtain location/ISP info. Got: ${error}`) - } -} - function initPrometheus(prometheusPort: number) { const eventEmitter = getEventEmitter() const { diff --git a/src/symon/index.test.ts b/src/symon/index.test.ts index 31a31b889..7fa71d441 100644 --- a/src/symon/index.test.ts +++ b/src/symon/index.test.ts @@ -26,7 +26,6 @@ import { expect } from '@oclif/test' import { type DefaultBodyType, HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' -import { monikaFlagsDefaultValue } from '../flag' import type { Config } from '../interfaces/config' import type { Probe } from '../interfaces/probe' @@ -36,6 +35,7 @@ import { deleteProbe, findProbe, getProbes } from '../components/config/probe' import { validateProbes } from '../components/config/validation' import { addIncident, findIncident } from '../components/incident' import events from '../events' +import { sanitizeFlags } from '../flag' import { getEventEmitter } from '../utils/events' import { getErrorMessage } from '../utils/catch-error-handler' import { getProbeState, initializeProbeStates } from '../utils/probe-state' @@ -109,13 +109,10 @@ describe('Symon initiate', () => { beforeEach(() => { resetContext() setContext({ - flags: { - ...getContext().flags, + flags: sanitizeFlags({ symonUrl: 'http://localhost:4000', symonKey: 'random-key', - symonGetProbesIntervalMs: - monikaFlagsDefaultValue.symonGetProbesIntervalMs, - }, + }), }) // reset probe cache @@ -134,11 +131,10 @@ describe('Symon initiate', () => { it('should send handshake data on initiate', async () => { setContext({ userAgent: 'v1.5.0', - flags: { - ...getContext().flags, + flags: sanitizeFlags({ symonUrl: 'http://localhost:4000', symonKey: 'random-key', - }, + }), }) let body: DefaultBodyType = {} // mock the outgoing requests @@ -159,10 +155,12 @@ describe('Symon initiate', () => { ) ) - const symon = new SymonClient({ - symonUrl: 'http://localhost:4000', - symonKey: 'abcd', - }) + const symon = new SymonClient( + sanitizeFlags({ + symonUrl: 'http://localhost:4000', + symonKey: 'abcd', + }) + ) await symon.initiate() await symon.stop() @@ -179,10 +177,12 @@ describe('Symon initiate', () => { }).timeout(15_000) it('should fetch probes config on initiate', async () => { - const symon = new SymonClient({ - symonUrl: 'http://localhost:4000', - symonKey: 'abcd', - }) + const symon = new SymonClient( + sanitizeFlags({ + symonUrl: 'http://localhost:4000', + symonKey: 'abcd', + }) + ) expect(getContext().config).to.be.undefined @@ -213,10 +213,12 @@ describe('Symon initiate', () => { }) ) - const symon = new SymonClient({ - symonUrl: 'http://localhost:4000', - symonKey: 'abcd', - }) + const symon = new SymonClient( + sanitizeFlags({ + symonUrl: 'http://localhost:4000', + symonKey: 'abcd', + }) + ) let errorMessage = '' try { @@ -248,11 +250,13 @@ describe('Symon initiate', () => { } ) ) - const symon = new SymonClient({ - symonUrl: 'http://localhost:4000', - symonKey: 'abcd', - symonMonikaId: '1234', - }) + const symon = new SymonClient( + sanitizeFlags({ + symonUrl: 'http://localhost:4000', + symonKey: 'abcd', + symonMonikaId: '1234', + }) + ) await symon.initiate() // act @@ -306,17 +310,18 @@ describe('Symon initiate', () => { ) const symonGetProbesIntervalMs = 100 setContext({ - flags: { - ...getContext().flags, + flags: sanitizeFlags({ symonUrl: 'http://localhost:4000', symonKey: 'random-key', symonGetProbesIntervalMs, - }, - }) - const symon = new SymonClient({ - symonUrl: 'http://localhost:4000', - symonKey: 'abcd', + }), }) + const symon = new SymonClient( + sanitizeFlags({ + symonUrl: 'http://localhost:4000', + symonKey: 'abcd', + }) + ) // 1. Check initial probe cache expect(getProbes()).deep.eq([]) @@ -359,17 +364,18 @@ describe('Symon initiate', () => { ) const symonGetProbesIntervalMs = 100 setContext({ - flags: { - ...getContext().flags, + flags: sanitizeFlags({ symonUrl: 'http://localhost:4000', symonKey: 'random-key', symonGetProbesIntervalMs, - }, - }) - const symon = new SymonClient({ - symonUrl: 'http://localhost:4000', - symonKey: 'abcd', + }), }) + const symon = new SymonClient( + sanitizeFlags({ + symonUrl: 'http://localhost:4000', + symonKey: 'abcd', + }) + ) // 1. Check initial probe cache expect(getProbes()).deep.eq([]) @@ -413,17 +419,18 @@ describe('Symon initiate', () => { ) const symonGetProbesIntervalMs = 100 setContext({ - flags: { - ...getContext().flags, + flags: sanitizeFlags({ symonUrl: 'http://localhost:4000', symonKey: 'random-key', symonGetProbesIntervalMs, - }, - }) - const symon = new SymonClient({ - symonUrl: 'http://localhost:4000', - symonKey: 'abcd', + }), }) + const symon = new SymonClient( + sanitizeFlags({ + symonUrl: 'http://localhost:4000', + symonKey: 'abcd', + }) + ) // 1. Check initial probe cache expect(getProbes()).deep.eq([]) @@ -467,17 +474,18 @@ describe('Symon initiate', () => { const symonGetProbesIntervalMs = 100 setContext({ - flags: { - ...getContext().flags, + flags: sanitizeFlags({ symonUrl: 'http://localhost:4000', symonKey: 'random-key', symonGetProbesIntervalMs, - }, - }) - const symon = new SymonClient({ - symonUrl: 'http://localhost:4000', - symonKey: 'abcd', + }), }) + const symon = new SymonClient( + sanitizeFlags({ + symonUrl: 'http://localhost:4000', + symonKey: 'abcd', + }) + ) // 1. Check initial probe cache expect(getProbes()).deep.eq([]) diff --git a/src/symon/index.ts b/src/symon/index.ts index 3b67893fa..5083f4233 100644 --- a/src/symon/index.ts +++ b/src/symon/index.ts @@ -174,12 +174,12 @@ export default class SymonClient { private url: string constructor({ - 'symon-api-version': apiVersion = SYMON_API_VERSION.v1, + 'symon-api-version': apiVersion, symonKey = '', symonLocationId = '', symonMonikaId = '', - symonReportInterval = 10_000, - symonReportLimit = 100, + symonReportInterval, + symonReportLimit, symonUrl = '', }: SymonClientParams) { this.apiKey = symonKey