Skip to content

Commit

Permalink
Refactor: Separate Probe and Config Data (#1199)
Browse files Browse the repository at this point in the history
* feat: add probe cache

* refactor: remove dead code

* refactor: move config version state to context

* refactor: reuse update config function

* refactor: separate probe and config state

* revert: test timeout and console log

* feat: add symonGetProbesIntervalMs flag to make it easy to test

* refactor: remove unnecessary code

* feat: add stop method to SymonClient class to stop background process

* test: add test to simulate probe changes from Symon

* fix: remove dependsOn on symonGetProbesIntervalMs flag

* test: add timeout

* refactor: change log severity to info for config file update

* refactor: replace function

* chore: add some logs

* revert: setTimeout for report

* chore: improve log consistency

* chore: remove unused log

* test: update test
  • Loading branch information
haricnugraha authored Dec 1, 2023
1 parent f2402f6 commit 550daa1
Show file tree
Hide file tree
Showing 10 changed files with 636 additions and 322 deletions.
5 changes: 4 additions & 1 deletion src/commands/monika.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
retryInitialDelayMs,
retryMaxDelayMs,
symonAPIVersion,
symonGetProbesIntervalMs,
} from '../flag'
import { printSummary, savePidFile } from '../jobs/summary-notification'
import initLoaders from '../loaders'
Expand Down Expand Up @@ -222,6 +223,8 @@ export default class Monika extends Command {
description: 'API Key for Symon',
}),

symonGetProbesIntervalMs,

symonLocationId: Flags.string({
dependsOn: ['symonKey', 'symonUrl'],
description: 'Location ID for Symon (optional)',
Expand Down Expand Up @@ -440,8 +443,8 @@ process.on('SIGINT', async () => {
}

if (symonClient) {
await symonClient.stopReport()
await symonClient.sendStatus({ isOnline: false })
await symonClient.stop()
}

em.emit(events.application.terminated)
Expand Down
19 changes: 0 additions & 19 deletions src/components/config/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,25 +68,6 @@ describe('getConfig', () => {
// act and assert
expect(getConfig()).deep.eq({ probes: [] })
})

it('should return config', () => {
// arrange
const config = {
probes: [
{
id: '1',
name: 'example',
interval: 1000,
alerts: [],
requests: [{ body: '', url: 'https://example.com', timeout: 1000 }],
},
],
}
setContext({ config })

// assert
expect(getConfig()).to.deep.eq(config)
})
})

describe('isSymonModeFrom', () => {
Expand Down
45 changes: 20 additions & 25 deletions src/components/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
getConfigFrom,
mergeConfigs,
} from './get'
import { getProbes, setProbes } from './probe'

type ScheduleRemoteConfigFetcherParams = {
configType: ConfigType
Expand Down Expand Up @@ -79,47 +80,41 @@ export const getConfig = (): Config => {
}
}

return config
return { ...config, probes: getProbes() }
}

export const updateConfig = async (
config: Config,
validate = true
): Promise<void> => {
export const updateConfig = async (config: Config): Promise<void> => {
log.info('Updating config')
let validatedConfig = config
if (validate) {
try {
validatedConfig = await validateConfig(config)
} catch (error: any) {
if (isTestEnvironment) {
// return error during tests
throw new Error(error.message)
}
try {
const validatedConfig = await validateConfig(config)
const version = md5Hash(validatedConfig)
const hasChangeConfig = getContext().config?.version !== version

log.error(error?.message)
exit(1)
if (!hasChangeConfig) {
return
}
}

const version = md5Hash(validatedConfig)
const hasChangeConfig = getContext()?.config?.version !== version

if (hasChangeConfig) {
const newConfig = addConfigVersion(validatedConfig)

setContext({ config: newConfig })
setProbes(newConfig.probes)
emitter.emit(events.config.updated, newConfig)
log.warn('config file update detected')
log.info('Config file update detected')
} catch (error: any) {
if (isTestEnvironment) {
// return error during tests
throw new Error(error.message)
}

log.error(error?.message)
exit(1)
}
}

export const setupConfig = async (flags: MonikaFlags): Promise<void> => {
const validFlag = await createConfigIfEmpty(flags)
const config = await getConfigFrom(validFlag)
const validatedConfig = await validateConfig(config)

setContext({ config: addConfigVersion(validatedConfig) })
await updateConfig(config)

watchConfigsChange(validFlag)
}
Expand Down
130 changes: 130 additions & 0 deletions src/components/config/probe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**********************************************************************************
* 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 type { Probe } from '../../interfaces/probe'

import {
addProbe,
deleteProbe,
findProbe,
getProbes,
setProbes,
updateProbe,
} from './probe'

const probe: Probe = {
alerts: [],
id: 'xVUcW',
interval: 10,
name: 'Sample Probe',
}

describe('Probe cache', () => {
beforeEach(() => {
for (const { id } of getProbes()) {
deleteProbe(id)
}
})
it('should add a probe', () => {
// act
addProbe(probe)

// assert
expect(findProbe(probe.id)).eq(probe)
})

it('should update a probe', () => {
// arrange
addProbe(probe)
const updatedName = 'Updated Probe'

// act
const isUpdated = updateProbe(probe.id, { ...probe, name: updatedName })

// assert
expect(isUpdated).eq(true)
expect(findProbe(probe.id)?.name).eq(updatedName)
})

it('should not update a nonexistent probe', () => {
// arrange
const updatedName = 'Updated Probe'

// act
const isUpdated = updateProbe('9WpFB', { ...probe, name: updatedName })

// assert
expect(isUpdated).eq(false)
expect(findProbe('9WpFB')).undefined
})

it('should get all probes', () => {
// arrange
addProbe(probe)

// act
const probes = getProbes()

// assert
expect(probes.length).eq(1)
})

it('should not remove a nonexistent probe', () => {
// arrange
addProbe(probe)

// act
const isDeleted = deleteProbe('9WpFB')

// assert
expect(isDeleted).eq(false)
expect(getProbes().length).eq(1)
})

it('should remove a probe', () => {
// arrange
addProbe(probe)

// act
const isDeleted = deleteProbe(probe.id)

// assert
expect(isDeleted).eq(true)
expect(getProbes().length).eq(0)
})

it('should set probes', () => {
// arrange
addProbe(probe)
expect(getProbes().length).eq(1)

// act
setProbes([probe])

// assert
expect(getProbes().length).eq(1)
})
})
43 changes: 43 additions & 0 deletions src/components/config/probe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Probe } from '../../interfaces/probe'

const probes: Map<string, Probe> = new Map()

export function getProbes() {
const legacyProbes: Probe[] = []
for (const probe of probes.values()) {
legacyProbes.push(probe)
}

return legacyProbes
}

export function findProbe(id: string) {
return probes.get(id)
}

export function setProbes(newProbes: Probe[]) {
probes.clear()

for (const probe of newProbes) {
addProbe(probe)
}
}

export function addProbe(newProbe: Probe) {
return probes.set(newProbe.id, newProbe)
}

export function updateProbe(id: string, data: Probe): boolean {
const probe = findProbe(id)
if (!probe) {
return false
}

const updatedProbe = { ...probe, ...data }
probes.set(id, updatedProbe)
return true
}

export function deleteProbe(id: string) {
return probes.delete(id)
}
2 changes: 1 addition & 1 deletion src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type Context = {
// userAgent example: @hyperjumptech/monika/1.2.3 linux-x64 node-14.17.0
userAgent: string
incidents: Incident[]
config?: Config
config?: Omit<Config, 'probes'>
flags: MonikaFlags
}

Expand Down
10 changes: 10 additions & 0 deletions src/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type MonikaFlags = {
symonMonikaId?: string
symonReportInterval?: number
symonReportLimit?: number
symonGetProbesIntervalMs: number
symonUrl?: string
text?: string
ignoreInvalidTLS?: boolean
Expand All @@ -87,6 +88,10 @@ export const monikaFlagsDefaultValue: MonikaFlags = {
// default is 20s interval lookup
stun: 20,
summary: false,
symonGetProbesIntervalMs: Number.parseInt(
process.env.FETCH_PROBES_INTERVAL ?? '60000',
10
),
verbose: false,
version: undefined,
}
Expand All @@ -98,6 +103,11 @@ export const symonAPIVersion = Flags.custom<SYMON_API_VERSION>({
options: [SYMON_API_VERSION.v1, SYMON_API_VERSION.v2],
})

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`,
})

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`,
Expand Down
Loading

0 comments on commit 550daa1

Please sign in to comment.