Repository: hyperjumptech/monika
Pull request #1037 (2023-05-02 12:28:18)

Refactoring: Separate Initialize and Update Config
# Monika Pull Request (PR)

## What feature/issue does this PR add

Separate update config from initialize config

## How did you implement / how did you fix it

1. Add some tests
2. Move Monika config state from variable to context
3. Extract isSymonMode function
4. Change the implementation of Monika config watcher from generator functions to simple for loop. It makes the watcher more efficient because it doesn't need to skip the first loop to run.
5. Separate update config from initialize config

## Screenshot

### Before

![before](https://github.com/hyperjumptech/monika/assets/15191978/2148163e-e565-40f1-8e93-bf45b4414d71)

### After

![after](https://github.com/hyperjumptech/monika/assets/15191978/8af4617e-512b-4f5f-beeb-ed56580be2a1)

## How to test

1. Run `npm test`
2. Run Monika as usual


codecov[bot]: ## [Codecov](https://codecov.io/gh/hyperjumptech/monika/pull/1037?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) Report
> Merging [#1037](https://codecov.io/gh/hyperjumptech/monika/pull/1037?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) (ef73074) into [main](https://codecov.io/gh/hyperjumptech/monika/commit/0953645b0358aa17411a7a668fcdfee2a3ba647f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) (0953645) will **increase** coverage by `0.03%`.
> The diff coverage is `79.31%`.

> :exclamation: Current head ef73074 differs from pull request most recent head 6b593d8. Consider uploading reports for the commit 6b593d8 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main    #1037      +/-   ##
==========================================
+ Coverage   49.90%   49.93%   +0.03%     
==========================================
  Files         110      110              
  Lines        3006     3016      +10     
  Branches      579      577       -2     
==========================================
+ Hits         1500     1506       +6     
- Misses       1321     1325       +4     
  Partials      185      185              
```


| [Impacted Files](https://codecov.io/gh/hyperjumptech/monika/pull/1037?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) | Coverage Δ | |
|---|---|---|
| [src/context/index.ts](https://codecov.io/gh/hyperjumptech/monika/pull/1037?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbnRleHQvaW5kZXgudHM=) | `100.00% <ø> (ø)` | |
| [src/components/probe/index.ts](https://codecov.io/gh/hyperjumptech/monika/pull/1037?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvcHJvYmUvaW5kZXgudHM=) | `21.83% <50.00%> (+0.90%)` | :arrow_up: |
| [src/components/probe/prober/http/index.ts](https://codecov.io/gh/hyperjumptech/monika/pull/1037?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvcHJvYmUvcHJvYmVyL2h0dHAvaW5kZXgudHM=) | `25.42% <50.00%> (+1.28%)` | :arrow_up: |
| [src/components/config/index.ts](https://codecov.io/gh/hyperjumptech/monika/pull/1037?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvY29uZmlnL2luZGV4LnRz) | `59.37% <80.95%> (-2.30%)` | :arrow_down: |
| [src/index.ts](https://codecov.io/gh/hyperjumptech/monika/pull/1037?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2luZGV4LnRz) | `53.27% <100.00%> (+0.93%)` | :arrow_up: |
| [src/loaders/index.ts](https://codecov.io/gh/hyperjumptech/monika/pull/1037?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2xvYWRlcnMvaW5kZXgudHM=) | `81.81% <100.00%> (+3.03%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech)


Patch:
diff --git a/src/components/config/index.ts b/src/components/config/index.ts
index f409d469..fb28a636 100644
--- a/src/components/config/index.ts
+++ b/src/components/config/index.ts
@@ -26,10 +26,10 @@ import chokidar from 'chokidar'
 import { CliUx } from '@oclif/core'
 import { existsSync, writeFileSync } from 'fs'
 import isUrl from 'is-url'
-import pEvent from 'p-event'
 
 import events from '../../events'
-import { Config } from '../../interfaces/config'
+import type { Config } from '../../interfaces/config'
+import { getContext, setContext } from '../../context'
 import { monikaFlagsDefaultValue } from '../../context/monika-flags'
 import type { MonikaFlags } from '../../context/monika-flags'
 import { getEventEmitter } from '../../utils/events'
@@ -65,27 +65,15 @@ type WatchConfigFileParams = {
 const isTestEnvironment = process.env.CI || process.env.NODE_ENV === 'test'
 const emitter = getEventEmitter()
 
-let cfg: Config
-let defaultConfigs: Partial<Config>[]
+const defaultConfigs: Partial<Config>[] = []
 let nonDefaultConfig: Partial<Config>
 
-export const getConfig = (skipConfigCheck = true): Config => {
-  if (!skipConfigCheck && !cfg)
-    throw new Error('Configuration setup has not been run yet')
-  return cfg
-}
-
-export async function* getConfigIterator(
-  skipConfigCheck = true
-): AsyncGenerator<Config, void, undefined> {
-  if (!skipConfigCheck && !cfg)
-    throw new Error('Configuration setup has not been run yet')
+export const getConfig = (): Config => {
+  const { config } = getContext()
 
-  yield cfg
+  if (!config) throw new Error('Configuration setup has not been run yet')
 
-  if (!isTestEnvironment) {
-    yield* pEvent.iterator<string, Config>(emitter, events.config.updated)
-  }
+  return config
 }
 
 export const updateConfig = async (
@@ -107,11 +95,14 @@ export const updateConfig = async (
     }
   }
 
-  const lastConfigVersion = cfg?.version
-  cfg = config
-  cfg.version = cfg.version || md5Hash(cfg)
-  if (lastConfigVersion !== undefined && lastConfigVersion !== cfg.version) {
-    emitter.emit(events.config.updated, cfg)
+  const version = md5Hash(config)
+  const hasChangeConfig = getContext()?.config?.version !== version
+
+  if (hasChangeConfig) {
+    const newConfig = { ...config, version }
+
+    setContext({ config: newConfig })
+    emitter.emit(events.config.updated, newConfig)
     log.warn('config file update detected')
   }
 }
@@ -119,10 +110,12 @@ export const updateConfig = async (
 export const setupConfig = async (flags: MonikaFlags): Promise<void> => {
   const sanitizedFlags = await sanitizeFlags(flags)
   const config = await getConfigFrom(sanitizedFlags)
+  await validateConfig(config)
+  const version = config.version || md5Hash(config)
 
-  watchConfigsChange(sanitizedFlags)
+  setContext({ config: { ...config, version } })
 
-  await updateConfig(config)
+  watchConfigsChange(sanitizedFlags)
 }
 
 async function sanitizeFlags(flags: MonikaFlags): Promise<MonikaFlags> {
@@ -310,3 +303,10 @@ export const createConfig = async (flags: MonikaFlags): Promise<void> => {
     log.info(`${file} file has been created.`)
   }
 }
+
+export function isSymonModeFrom({
+  symonKey,
+  symonUrl,
+}: Pick<MonikaFlags, 'symonKey' | 'symonUrl'>): boolean {
+  return Boolean(symonUrl) && Boolean(symonKey)
+}
diff --git a/src/components/logger/startup-message.ts b/src/components/logger/startup-message.ts
index 0b3e9cee..49567505 100644
--- a/src/components/logger/startup-message.ts
+++ b/src/components/logger/startup-message.ts
@@ -26,68 +26,52 @@ import path from 'path'
 import isUrl from 'is-url'
 import boxen from 'boxen'
 import chalk from 'chalk'
+import type { MonikaFlags } from '../../context/monika-flags'
 import type { Config } from '../../interfaces/config'
 import type { Notification } from '../notification/channel'
 import { channels } from '../notification/channel'
 import type { Probe, ProbeAlert } from '../../interfaces/probe'
 import type { RequestConfig } from '../../interfaces/request'
 import { log } from '../../utils/pino'
+import { isSymonModeFrom } from '../config'
 
 type LogStartupMessage = {
   config: Config
-  configFlag: string[]
+  flags: Pick<MonikaFlags, 'config' | 'symonKey' | 'symonUrl' | 'verbose'>
   isFirstRun: boolean
-  isSymonMode: boolean
-  isVerbose: boolean
 }
 
 export function logStartupMessage({
   config,
-  configFlag,
+  flags,
   isFirstRun,
-  isSymonMode,
-  isVerbose,
 }: LogStartupMessage): void {
-  const startupMessage = generateStartupMessage({
-    config,
-    isFirstRun,
-    isSymonMode,
-    isVerbose,
-  })
-
-  if (isSymonMode) {
-    log.info(startupMessage)
+  if (isSymonModeFrom(flags)) {
+    log.info('Running in Symon mode')
     return
   }
 
-  for (const x in configFlag) {
-    if (isUrl(configFlag[x])) {
-      log.info('Using remote config:', configFlag[x])
-    } else if (configFlag[x].length > 0) {
-      log.info(`Using config file: ${path.resolve(configFlag[x])}`)
+  for (const configSource of flags.config) {
+    if (isUrl(configSource)) {
+      log.info(`Using remote config: ${configSource}`)
+    } else if (configSource.length > 0) {
+      log.info(`Using config file: ${path.resolve(configSource)}`)
     }
   }
 
+  const startupMessage = generateStartupMessage({
+    config,
+    flags,
+    isFirstRun,
+  })
   console.log(startupMessage)
 }
 
-type GenerateStartupMessageParams = {
-  config: Config
-  isFirstRun: boolean
-  isVerbose: boolean
-  isSymonMode: boolean
-}
-
 function generateStartupMessage({
   config,
+  flags,
   isFirstRun,
-  isVerbose,
-  isSymonMode,
-}: GenerateStartupMessageParams): string {
-  if (isSymonMode) {
-    return 'Running in Symon mode'
-  }
-
+}: LogStartupMessage): string {
   const { notifications = [], probes } = config
   const notificationTotal = notifications.length
   const probeTotal = probes.length
@@ -106,7 +90,7 @@ function generateStartupMessage({
     probeTotal,
   })
 
-  if (isVerbose) {
+  if (flags.verbose) {
     startupMessage += generateProbeMessage(probes)
     startupMessage += generateNotificationMessage(notifications || [])
   }
diff --git a/src/components/notification/schedule-notification.ts b/src/components/notification/schedule-notification.ts
index fd51ee77..b28d62dc 100644
--- a/src/components/notification/schedule-notification.ts
+++ b/src/components/notification/schedule-notification.ts
@@ -1,17 +1,19 @@
 import cron from 'node-cron'
 import type { ScheduledTask } from 'node-cron'
+import type { MonikaFlags } from '../../context/monika-flags'
+import type { Config } from '../../interfaces/config'
 import { getSummaryAndSendNotif } from '../../jobs/summary-notification'
+import { isSymonModeFrom } from '../config'
 
 type scheduleSummaryNotification = {
-  isSymonMode: boolean
-  statusNotificationConfig?: string
-  statusNotificationFlag?: string
+  config: Pick<Config, 'status-notification'>
+  flags: Pick<MonikaFlags, 'status-notification' | 'symonKey' | 'symonUrl'>
 }
 
 let scheduledTasks: ScheduledTask[] = []
 
 // Stop and clear all previous cron tasks
-export function resetScheduledTasks(): void {
+function resetScheduledTasks(): void {
   for (const task of scheduledTasks) {
     task.stop()
   }
@@ -20,21 +22,22 @@ export function resetScheduledTasks(): void {
 }
 
 export function scheduleSummaryNotification({
-  isSymonMode,
-  statusNotificationConfig,
-  statusNotificationFlag,
+  config,
+  flags,
 }: scheduleSummaryNotification): void {
-  if (isSymonMode || statusNotificationFlag === 'false') {
+  if (isSymonModeFrom(flags) || flags['status-notification'] === 'false') {
     return
   }
 
+  resetScheduledTasks()
+
   // defaults to 6 AM
   // default value is not defined in flag configuration,
   // because the value can also come from config file
   const DEFAULT_SCHEDULE_CRON_EXPRESSION = '0 6 * * *'
   const schedule =
-    statusNotificationFlag ||
-    statusNotificationConfig ||
+    flags['status-notification'] ||
+    config['status-notification'] ||
     DEFAULT_SCHEDULE_CRON_EXPRESSION
 
   const scheduledStatusUpdateTask = cron.schedule(
diff --git a/src/components/probe/index.ts b/src/components/probe/index.ts
index c3fcad8e..e4b2c280 100644
--- a/src/components/probe/index.ts
+++ b/src/components/probe/index.ts
@@ -34,6 +34,7 @@ import validateResponse, {
 import { getEventEmitter } from '../../utils/events'
 import { log } from '../../utils/pino'
 import { setProbeFinish, setProbeRunning } from '../../utils/probe-state'
+import { isSymonModeFrom } from '../config'
 import { RequestLog } from '../logger'
 import { sendAlerts } from '../notification'
 import { processThresholds } from '../notification/process-server-status'
@@ -172,7 +173,7 @@ function responseProcessing({
   index,
 }: respProsessingParams): void {
   const { flags } = getContext()
-  const isSymonMode = Boolean(flags.symonUrl) && Boolean(flags.symonKey)
+  const isSymonMode = isSymonModeFrom(flags)
   const eventEmitter = getEventEmitter()
   const isVerbose = isSymonMode || flags['keep-verbose-logs']
   const { alerts } = probe
diff --git a/src/components/probe/prober/http/index.ts b/src/components/probe/prober/http/index.ts
index 70aa5431..c044ff76 100644
--- a/src/components/probe/prober/http/index.ts
+++ b/src/components/probe/prober/http/index.ts
@@ -1,3 +1,4 @@
+import { isSymonModeFrom } from '../../../config'
 import { checkThresholdsAndSendAlert } from '../..'
 import { getContext } from '../../../../context'
 import events from '../../../../events'
@@ -24,7 +25,7 @@ export async function probeHTTP(
 ): Promise<void> {
   const eventEmitter = getEventEmitter()
   const { flags } = getContext()
-  const isSymonMode = Boolean(flags.symonUrl) && Boolean(flags.symonKey)
+  const isSymonMode = isSymonModeFrom(flags)
   const isVerbose = isSymonMode || flags['keep-verbose-logs']
   const responses = []
 
diff --git a/src/context/index.ts b/src/context/index.ts
index 99db1cdb..45227ff2 100644
--- a/src/context/index.ts
+++ b/src/context/index.ts
@@ -22,6 +22,7 @@
  * SOFTWARE.                                                                      *
  **********************************************************************************/
 
+import type { Config } from '../interfaces/config'
 import { monikaFlagsDefaultValue } from './monika-flags'
 import type { MonikaFlags } from './monika-flags'
 
@@ -35,6 +36,7 @@ type Context = {
   // userAgent example: @hyperjumptech/monika/1.2.3 linux-x64 node-14.17.0
   userAgent: string
   incidents: Incident[]
+  config?: Config
   flags: MonikaFlags
 }
 
diff --git a/src/index.ts b/src/index.ts
index 3a09a100..8c60b96c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -23,21 +23,19 @@
  **********************************************************************************/
 
 import { Command, Errors, Flags, Interfaces } from '@oclif/core'
+import pEvent from 'p-event'
 
 import { flush, help } from './commands'
-import { createConfig, getConfigIterator } from './components/config'
+import { createConfig, getConfig, isSymonModeFrom } from './components/config'
 import { printAllLogs } from './components/logger'
 import { closeLog, openLogfile } from './components/logger/history'
 import { logStartupMessage } from './components/logger/startup-message'
 import { sendMonikaStartMessage } from './components/notification/start-message'
-import {
-  resetScheduledTasks,
-  scheduleSummaryNotification,
-} from './components/notification/schedule-notification'
+import { scheduleSummaryNotification } from './components/notification/schedule-notification'
 import { setContext } from './context'
 import events from './events'
-import { Config } from './interfaces/config'
-import { Probe } from './interfaces/probe'
+import type { Config } from './interfaces/config'
+import type { Probe } from './interfaces/probe'
 import { printSummary, savePidFile } from './jobs/summary-notification'
 import initLoaders from './loaders'
 import { sanitizeProbe, startProbing } from './looper'
@@ -48,6 +46,11 @@ import { getEventEmitter } from './utils/events'
 import { log } from './utils/pino'
 import { sortProbes } from './components/config/sort'
 
+type GetProbesParams = {
+  config: Config
+  flags: MonikaFlags
+}
+
 const em = getEventEmitter()
 let symonClient: SymonClient
 
@@ -293,68 +296,58 @@ class Monika extends Command {
 
       await initLoaders(_flags, this.config)
 
-      const isSymonMode = Boolean(_flags.symonUrl) && Boolean(_flags.symonKey)
-      if (isSymonMode) {
-        symonClient = new SymonClient({
-          url: _flags.symonUrl as string,
-          apiKey: _flags.symonKey as string,
-          locationId: _flags.symonLocationId as string,
-          monikaId: _flags.symonMonikaId as string,
-          reportInterval: _flags.symonReportInterval,
-          reportLimit: _flags.symonReportLimit,
-        })
+      if (isSymonModeFrom(_flags)) {
+        symonClient = new SymonClient(_flags)
         await symonClient.initiate()
       }
 
+      let isFirstRun = true
       let abortCurrentLooper: (() => void) | undefined
-      const isTestEnvironment = process.env.NODE_ENV === 'test'
-
-      for await (const config of getConfigIterator(isSymonMode)) {
-        if (!config) continue
 
-        const { notifications, probes } = config
-        const sortedProbes = sortProbes(probes, _flags.id)
-        const sanitizedProbe = sortedProbes.map((probe: Probe) =>
-          sanitizeProbe(isSymonMode, probe)
-        )
-
-        // emit the sanitized probe
-        em.emit(events.config.sanitized, sanitizedProbe)
+      for (;;) {
+        const config = getConfig()
+        const notifications = config.notifications || []
+        const probes = this.getProbes({ config, flags: _flags })
 
         if (abortCurrentLooper) {
           abortCurrentLooper()
         }
 
-        resetScheduledTasks()
+        // emit the sanitized probe
+        em.emit(events.config.sanitized, probes)
 
-        if (!isTestEnvironment) {
-          await sendMonikaStartMessage(notifications ?? [])
-        }
+        // save some data into files for later
+        savePidFile(_flags.config, config)
 
-        await this.deprecationHandler(config)
+        this.deprecationHandler(config)
 
         logStartupMessage({
           config,
-          configFlag: _flags.config,
-          isFirstRun: !abortCurrentLooper,
-          isSymonMode,
-          isVerbose: _flags.verbose,
+          flags: _flags,
+          isFirstRun,
         })
 
-        // save some data into files for later
-        savePidFile(_flags.config, config)
+        abortCurrentLooper = startProbing({
+          probes,
+          notifications,
+        })
+
+        if (process.env.NODE_ENV === 'test') {
+          break
+        }
+
+        sendMonikaStartMessage(notifications).catch((error) =>
+          log.error(error.message)
+        )
 
         // schedule status update notification
-        scheduleSummaryNotification({
-          isSymonMode,
-          statusNotificationConfig: config['status-notification'],
-          statusNotificationFlag: _flags['status-notification'],
-        })
+        scheduleSummaryNotification({ config, flags: _flags })
 
-        abortCurrentLooper = startProbing({
-          probes: sanitizedProbe,
-          notifications: notifications ?? [],
-        })
+        isFirstRun = false
+
+        // block the loop until receives config updated event
+        // eslint-disable-next-line no-await-in-loop
+        await pEvent(em, events.config.updated)
       }
     } catch (error) {
       await closeLog()
@@ -383,7 +376,7 @@ class Monika extends Command {
     throw error
   }
 
-  async deprecationHandler(config: Config): Promise<Config> {
+  deprecationHandler(config: Config): Config {
     let showDeprecateMsg = false
 
     const checkedConfig = {
@@ -418,6 +411,14 @@ class Monika extends Command {
 
     return checkedConfig
   }
+
+  getProbes({ config, flags }: GetProbesParams): Probe[] {
+    const sortedProbes = sortProbes(config.probes, flags.id)
+
+    return sortedProbes.map((probe: Probe) =>
+      sanitizeProbe(isSymonModeFrom(flags), probe)
+    )
+  }
 }
 
 /**
diff --git a/src/loaders/index.ts b/src/loaders/index.ts
index fcd5f178..1f6f2166 100644
--- a/src/loaders/index.ts
+++ b/src/loaders/index.ts
@@ -23,7 +23,7 @@
  **********************************************************************************/
 
 import type { Config as IConfig } from '@oclif/core'
-import { setupConfig } from '../components/config'
+import { isSymonModeFrom, setupConfig } from '../components/config'
 import { setContext } from '../context'
 import events from '../events'
 import type { MonikaFlags } from '../context/monika-flags'
@@ -49,7 +49,7 @@ export default async function init(
 ): Promise<void> {
   const eventEmitter = getEventEmitter()
   const isTestEnvironment = process.env.CI || process.env.NODE_ENV === 'test'
-  const isSymonMode = Boolean(flags.symonUrl) && Boolean(flags.symonKey)
+  const isSymonMode = isSymonModeFrom(flags)
 
   setContext({ userAgent: cliConfig.userAgent })
 
diff --git a/src/looper/index.ts b/src/looper/index.ts
index 67f4a835..be6d06ea 100644
--- a/src/looper/index.ts
+++ b/src/looper/index.ts
@@ -27,7 +27,7 @@ import { differenceInSeconds } from 'date-fns'
 import { doProbe } from '../components/probe'
 import { getContext } from '../context'
 import type { Notification } from '../components/notification/channel'
-import { Probe } from '../interfaces/probe'
+import type { Probe } from '../interfaces/probe'
 import { log } from '../utils/pino'
 import {
   getProbeContext,
@@ -158,17 +158,16 @@ export function startProbing({
   probes,
   notifications,
 }: StartProbingArgs): () => void {
-  const flags = getContext().flags
-  const repeat = flags.repeat
-
   initializeProbeStates(probes)
 
   const probeInterval = setInterval(() => {
+    const { repeat, stun } = getContext().flags
+
     if (repeat) {
-      const finishedProbe = probes.every((probe) => {
-        const context = getProbeContext(probe.id)
+      const finishedProbe = probes.every(({ id }) => {
+        const context = getProbeContext(id)
 
-        return context.cycle === repeat && getProbeState(probe.id) === 'idle'
+        return context.cycle === repeat && getProbeState(id) === 'idle'
       })
 
       if (finishedProbe) {
@@ -177,7 +176,7 @@ export function startProbing({
       }
     }
 
-    if ((isConnectedToSTUNServer && !isPaused) || flags.stun === DISABLE_STUN) {
+    if ((isConnectedToSTUNServer && !isPaused) || stun === DISABLE_STUN) {
       for (const probe of probes) {
         const probeState = getProbeState(probe.id)
         const context = getProbeContext(probe.id)
diff --git a/src/symon/index.ts b/src/symon/index.ts
index 2948e5b4..bbccda56 100644
--- a/src/symon/index.ts
+++ b/src/symon/index.ts
@@ -33,6 +33,7 @@ import path from 'path'
 import { updateConfig } from '../components/config'
 import { getOSName } from '../components/notification/alert-message'
 import { getContext } from '../context'
+import type { MonikaFlags } from '../context/monika-flags'
 import events from '../events'
 import { Config } from '../interfaces/config'
 import { Probe } from '../interfaces/probe'
@@ -161,44 +162,45 @@ class SymonClient {
   })
 
   constructor({
-    url,
-    apiKey,
-    locationId,
-    monikaId,
-    reportInterval,
-    reportLimit,
-  }: {
-    url: string
-    apiKey: string
-    locationId?: string | undefined
-    monikaId?: string | undefined
-    reportInterval?: number | undefined
-    reportLimit?: number | undefined
-  }) {
+    symonUrl = '',
+    symonKey = '',
+    symonLocationId,
+    symonMonikaId,
+    symonReportInterval,
+    symonReportLimit,
+  }: Pick<
+    MonikaFlags,
+    | 'symonUrl'
+    | 'symonKey'
+    | 'symonLocationId'
+    | 'symonMonikaId'
+    | 'symonReportInterval'
+    | 'symonReportLimit'
+  >) {
     this.httpClient = axios.create({
-      baseURL: `${url}/api/v1/monika`,
+      baseURL: `${symonUrl}/api/v1/monika`,
       headers: {
-        'x-api-key': apiKey,
+        'x-api-key': symonKey,
       },
       timeout: DEFAULT_TIMEOUT,
     })
 
-    this.url = url
+    this.url = symonUrl
 
-    this.apiKey = apiKey
+    this.apiKey = symonKey
 
-    this.locationId = locationId || ''
+    this.locationId = symonLocationId || ''
 
-    this.monikaId = monikaId || ''
+    this.monikaId = symonMonikaId || ''
 
     this.fetchProbesInterval = Number.parseInt(
       process.env.FETCH_PROBES_INTERVAL ?? '60000',
       10
     )
 
-    this.reportProbesInterval = reportInterval ?? 10_000
+    this.reportProbesInterval = symonReportInterval ?? 10_000
 
-    this.reportProbesLimit = reportLimit ?? 100
+    this.reportProbesLimit = symonReportLimit ?? 100
   }
 
   async initiate(): Promise<void> {
@@ -437,6 +439,10 @@ class SymonClient {
       log.warn(`Warning: Can't send status to Symon. ${error?.message}`)
     }
   }
+
+  async stopReport(): Promise<void> {
+    await this.bree.stop()
+  }
 }
 
 export default SymonClient


Test patch:
diff --git a/src/components/config/index.test.ts b/src/components/config/index.test.ts
new file mode 100644
index 00000000..1253227b
--- /dev/null
+++ b/src/components/config/index.test.ts
@@ -0,0 +1,197 @@
+/**********************************************************************************
+ * MIT License                                                                    *
+ *                                                                                *
+ * Copyright (c) 2021 Hyperjump Technology                                        *
+ *                                                                                *
+ * 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.                                                                      *
+ **********************************************************************************/
+
+import { expect } from '@oclif/test'
+import { getConfig, isSymonModeFrom, updateConfig } from '.'
+import { getContext, resetContext, setContext } from '../../context'
+import type { Config } from '../../interfaces/config'
+import events from '../../events'
+import { md5Hash } from '../../utils/hash'
+import { getEventEmitter } from '../../utils/events'
+
+describe('getConfig', () => {
+  afterEach(() => {
+    resetContext()
+  })
+
+  it('should throw error when config is empty', () => {
+    expect(() => getConfig()).to.throw(
+      'Configuration setup has not been run yet'
+    )
+  })
+
+  it('should return config', () => {
+    // arrange
+    const config = {
+      probes: [
+        {
+          id: '1',
+          name: 'example',
+          interval: 1000,
+          incidentThreshold: 1,
+          recoveryThreshold: 1,
+          alerts: [],
+          requests: [{ body: '', url: 'https://example.com', timeout: 1000 }],
+        },
+      ],
+    }
+    setContext({ config })
+
+    // assert
+    expect(getConfig()).to.deep.eq(config)
+  })
+})
+
+describe('isSymonModeFrom', () => {
+  it('should return true', () => {
+    // act
+    const isSymonMode = isSymonModeFrom({
+      symonKey: 'secret-key',
+      symonUrl: 'https://example.com',
+    })
+
+    // assert
+    expect(isSymonMode).eq(true)
+  })
+
+  it('should return false when symon key and url is empty', () => {
+    // act
+    const isSymonMode = isSymonModeFrom({
+      symonKey: '',
+      symonUrl: '',
+    })
+
+    // assert
+    expect(isSymonMode).eq(false)
+  })
+
+  it('should return false when symon key is empty', () => {
+    // act
+    const isSymonMode = isSymonModeFrom({
+      symonKey: '',
+      symonUrl: 'https://example.com',
+    })
+
+    // assert
+    expect(isSymonMode).eq(false)
+  })
+
+  it('should return false when symon url is empty', () => {
+    // act
+    const isSymonMode = isSymonModeFrom({
+      symonKey: 'secret-key',
+      symonUrl: '',
+    })
+
+    // assert
+    expect(isSymonMode).eq(false)
+  })
+})
+
+describe('updateConfig', () => {
+  afterEach(() => {
+    resetContext()
+  })
+
+  it('should throw error when config is invalid', async () => {
+    // arrange
+    let errorMessage = ''
+
+    try {
+      // act
+      await updateConfig({ probes: [] })
+    } catch (error: any) {
+      errorMessage = error.message
+    }
+
+    // assert
+    expect(errorMessage).eq(
+      'Probes object does not exists or has length lower than 1!'
+    )
+  })
+
+  it('should update config', async () => {
+    // arrange
+    const config: Config = {
+      probes: [
+        {
+          id: '1',
+          name: '',
+          interval: 1000,
+          requests: [{ url: 'https://example.com', body: '', timeout: 1000 }],
+          incidentThreshold: 1,
+          recoveryThreshold: 1,
+          alerts: [],
+        },
+      ],
+    }
+    let configFromEmitter = ''
+    const eventEmitter = getEventEmitter()
+    eventEmitter.once(events.config.updated, (data) => {
+      configFromEmitter = data
+    })
+
+    // act
+    await updateConfig(config)
+
+    // assert
+    expect(getContext().config).to.deep.eq({
+      ...config,
+      version: md5Hash(config),
+    })
+    expect(configFromEmitter).not.eq('')
+  })
+
+  it('should should not update config', async () => {
+    // arrange
+    const config: Config = {
+      probes: [
+        {
+          id: '1',
+          name: '',
+          interval: 1000,
+          requests: [{ url: 'https://example.com', body: '', timeout: 1000 }],
+          incidentThreshold: 1,
+          recoveryThreshold: 1,
+          alerts: [],
+        },
+      ],
+    }
+    setContext({ config: { ...config, version: md5Hash(config) } })
+    let configFromEmitter = ''
+    const eventEmitter = getEventEmitter()
+    const eventListener = (data: any) => {
+      configFromEmitter = data
+    }
+
+    eventEmitter.once(events.config.updated, eventListener)
+
+    // act
+    await updateConfig(config)
+
+    // assert
+    expect(configFromEmitter).eq('')
+    eventEmitter.removeListener(events.config.updated, eventListener)
+  })
+})
diff --git a/src/components/logger/startup-message.test.ts b/src/components/logger/startup-message.test.ts
index 6f863252..c7e6d48f 100644
--- a/src/components/logger/startup-message.test.ts
+++ b/src/components/logger/startup-message.test.ts
@@ -72,10 +72,13 @@ describe('Startup message', () => {
       // act
       logStartupMessage({
         config: { probes: [] },
-        configFlag: [],
+        flags: {
+          config: [],
+          symonKey: 'secret-key',
+          symonUrl: 'https://example.com',
+          verbose: false,
+        },
         isFirstRun: false,
-        isSymonMode: true,
-        isVerbose: false,
       })
 
       // assert
@@ -89,10 +92,8 @@ describe('Startup message', () => {
         // arrange
         logStartupMessage({
           config: { probes: [] },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: false },
           isFirstRun: false,
-          isSymonMode: false,
-          isVerbose: false,
         })
 
         // assert
@@ -105,10 +106,8 @@ describe('Startup message', () => {
         // arrange
         logStartupMessage({
           config: defaultConfig,
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: false },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: false,
         })
 
         // assert
@@ -119,10 +118,8 @@ describe('Startup message', () => {
         // arrange
         logStartupMessage({
           config: defaultConfig,
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: false },
           isFirstRun: false,
-          isSymonMode: false,
-          isVerbose: false,
         })
 
         // assert
@@ -133,10 +130,8 @@ describe('Startup message', () => {
         // arrange
         logStartupMessage({
           config: defaultConfig,
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: false },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: false,
         })
 
         // assert
@@ -151,10 +146,8 @@ describe('Startup message', () => {
             ...defaultConfig,
             notifications: [],
           },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: false },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: false,
         })
 
         // assert
@@ -177,10 +170,8 @@ describe('Startup message', () => {
               },
             ],
           },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include('uYJaw')
@@ -206,10 +197,8 @@ describe('Startup message', () => {
               },
             ],
           },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include('uYJaw')
@@ -224,10 +213,8 @@ describe('Startup message', () => {
         // arrange
         logStartupMessage({
           config: defaultConfig,
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include('GET')
@@ -260,10 +247,8 @@ describe('Startup message', () => {
               },
             ],
           },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include('POST')
@@ -276,10 +261,8 @@ describe('Startup message', () => {
         // arrange
         logStartupMessage({
           config: defaultConfig,
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include(
@@ -304,10 +287,8 @@ describe('Startup message', () => {
               },
             ],
           },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include(
@@ -324,10 +305,8 @@ describe('Startup message', () => {
             ...defaultConfig,
             notifications: [{ id: 'UVIsL', type: 'desktop', data: undefined }],
           },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include('Notifications')
@@ -354,10 +333,8 @@ describe('Startup message', () => {
               },
             ],
           },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include('john@example.com, jane@example.com')
@@ -383,10 +360,8 @@ describe('Startup message', () => {
               },
             ],
           },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include('https://example.com')
@@ -407,10 +382,8 @@ describe('Startup message', () => {
               },
             ],
           },
-          configFlag: [],
+          flags: { config: [], symonKey: '', symonUrl: '', verbose: true },
           isFirstRun: true,
-          isSymonMode: false,
-          isVerbose: true,
         })
         // assert
         expect(consoleLogMessage).include('https://example.com')
@@ -423,10 +396,13 @@ describe('Startup message', () => {
       // act
       logStartupMessage({
         config: defaultConfig,
-        configFlag: ['https://example.com/monika.yaml'],
+        flags: {
+          config: ['https://example.com/monika.yaml'],
+          symonKey: '',
+          symonUrl: '',
+          verbose: false,
+        },
         isFirstRun: false,
-        isSymonMode: false,
-        isVerbose: false,
       })
 
       // assert
@@ -437,10 +413,13 @@ describe('Startup message', () => {
       // act
       logStartupMessage({
         config: defaultConfig,
-        configFlag: ['./monika.yaml'],
+        flags: {
+          config: ['./monika.yaml'],
+          symonKey: '',
+          symonUrl: '',
+          verbose: false,
+        },
         isFirstRun: false,
-        isSymonMode: false,
-        isVerbose: false,
       })
 
       // assert
diff --git a/src/components/notification/schedule-notification.test.ts b/src/components/notification/schedule-notification.test.ts
index 7504737a..bca1910a 100644
--- a/src/components/notification/schedule-notification.test.ts
+++ b/src/components/notification/schedule-notification.test.ts
@@ -2,10 +2,7 @@ import cron from 'node-cron'
 import sinon from 'sinon'
 import chai, { expect } from 'chai'
 import spies from 'chai-spies'
-import {
-  resetScheduledTasks,
-  scheduleSummaryNotification,
-} from './schedule-notification'
+import { scheduleSummaryNotification } from './schedule-notification'
 
 chai.use(spies)
 
@@ -31,14 +28,16 @@ describe('Schedule notification', () => {
   afterEach(() => {
     sinon.restore()
     cronExpression = ''
-    resetScheduledTasks()
     taskStopCalledTotal = 0
   })
 
   describe('Schedule summary notification', () => {
     it('should not schedule notification on Symon mode', () => {
       // act
-      scheduleSummaryNotification({ isSymonMode: true })
+      scheduleSummaryNotification({
+        config: {},
+        flags: { symonKey: 'secret-key', symonUrl: 'https://example.com' },
+      })
 
       // assert
       sinon.assert.notCalled(cronScheduleStub)
@@ -47,8 +46,8 @@ describe('Schedule notification', () => {
     it('should not schedule notification if status notification flag is false', () => {
       // act
       scheduleSummaryNotification({
-        isSymonMode: false,
-        statusNotificationFlag: 'false',
+        config: {},
+        flags: { 'status-notification': 'false' },
       })
 
       // assert
@@ -58,9 +57,10 @@ describe('Schedule notification', () => {
     it('should schedule notification based on notification flag value', () => {
       // act
       scheduleSummaryNotification({
-        isSymonMode: false,
-        statusNotificationFlag: '* * * * *',
-        statusNotificationConfig: '0 0 0 0 0',
+        config: { 'status-notification': '0 0 0 0 0' },
+        flags: {
+          'status-notification': '* * * * *',
+        },
       })
 
       // assert
@@ -71,8 +71,8 @@ describe('Schedule notification', () => {
     it('should schedule notification based on notification config value', () => {
       // act
       scheduleSummaryNotification({
-        isSymonMode: false,
-        statusNotificationConfig: '0 0 0 0 0',
+        config: { 'status-notification': '0 0 0 0 0' },
+        flags: {},
       })
 
       // assert
@@ -82,9 +82,7 @@ describe('Schedule notification', () => {
 
     it('should schedule notification use default schedule', () => {
       // act
-      scheduleSummaryNotification({
-        isSymonMode: false,
-      })
+      scheduleSummaryNotification({ config: {}, flags: {} })
 
       // assert
       sinon.assert.calledOnce(cronScheduleStub)
@@ -94,16 +92,9 @@ describe('Schedule notification', () => {
 
   describe('Reset schedule notification', () => {
     it('should stop all running scheduled notification', () => {
-      // arrange
-      scheduleSummaryNotification({
-        isSymonMode: false,
-      })
-      scheduleSummaryNotification({
-        isSymonMode: false,
-      })
-
       // act
-      resetScheduledTasks()
+      scheduleSummaryNotification({ config: {}, flags: {} })
+      scheduleSummaryNotification({ config: {}, flags: {} })
 
       // arrange
       expect(taskStopCalledTotal).eq(2)
diff --git a/src/symon/index.test.ts b/src/symon/index.test.ts
index cf51dbc2..c5200851 100644
--- a/src/symon/index.test.ts
+++ b/src/symon/index.test.ts
@@ -129,12 +129,13 @@ describe('Symon initiate', () => {
     })
 
     const symon = new SymonClient({
-      url: 'http://localhost:4000',
-      apiKey: 'abcd',
+      symonUrl: 'http://localhost:4000',
+      symonKey: 'abcd',
     })
     sinon.spy(symon, 'report')
 
     await symon.initiate()
+    await symon.stopReport()
     expect(symon.monikaId).equals('1234')
 
     const body = JSON.parse(sentBody)
@@ -208,14 +209,15 @@ describe('Symon initiate', () => {
     })
 
     const symon = new SymonClient({
-      url: 'http://localhost:4000',
-      apiKey: 'abcd',
+      symonUrl: 'http://localhost:4000',
+      symonKey: 'abcd',
     })
     sinon.spy(symon, 'report')
 
     expect(symon.config).to.be.null
 
     await symon.initiate()
+    await symon.stopReport()
 
     expect(symon.config).deep.equals(config)
   })
@@ -259,12 +261,13 @@ describe('Symon initiate', () => {
     })
 
     const symon = new SymonClient({
-      url: 'http://localhost:4000',
-      apiKey: 'abcd',
+      symonUrl: 'http://localhost:4000',
+      symonKey: 'abcd',
     })
     const reportSpy = sinon.spy(symon, 'report')
 
     await symon.initiate()
+    await symon.stopReport()
 
     expect(reportSpy.called).equals(true)
   })
@@ -290,8 +293,8 @@ describe('Send incident or recovery event', () => {
     })
 
     const symon = new SymonClient({
-      url: 'http://localhost:4000',
-      apiKey: 'abcd',
+      symonUrl: 'http://localhost:4000',
+      symonKey: 'abcd',
     })
     sinon.spy(symon, 'report')
     symon.monikaId = '1234'