From 2f6d3484df9a0367eed7f1423703b8aef5209d56 Mon Sep 17 00:00:00 2001 From: Mochamad Noor Syamsu Date: Mon, 23 Sep 2024 00:00:09 +0700 Subject: [PATCH 1/5] feat: add probe identification for identical probes - Added 'src/components/config/identical-probes.ts' to identify identical probes. - Updated 'src/components/config/index.ts' to utilize identical probe identification in config updates. - Enhanced 'src/context/index.ts' with probe result caching capabilities. --- src/components/config/identical-probes.ts | 133 ++++++++++++++++++++++ src/components/config/index.ts | 17 ++- src/context/index.ts | 7 ++ 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/components/config/identical-probes.ts diff --git a/src/components/config/identical-probes.ts b/src/components/config/identical-probes.ts new file mode 100644 index 000000000..dd66d559e --- /dev/null +++ b/src/components/config/identical-probes.ts @@ -0,0 +1,133 @@ +/********************************************************************************** + * 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 lodash from 'lodash' +import type { Probe } from '../../interfaces/probe' +import type { ValidatedConfig } from '../../interfaces/config' + +// Function to identify identical probes based on specific fields +export async function identifyIdenticalProbes( + config: ValidatedConfig +): Promise[]> { + const { probes } = config + // Define fields that are used to determine if probes are identical + const httpIdentifiers: (keyof Probe)[] = ['interval', 'requests'] + const socketIdentifiers: (keyof Probe)[] = ['interval', 'socket'] + const redisIdentiers: (keyof Probe)[] = ['interval', 'redis'] + const mongoIdentiers: (keyof Probe)[] = ['interval', 'mongo'] + const mariadbIdentiers: (keyof Probe)[] = ['interval', 'mariadb'] + const mysqlIdentiers: (keyof Probe)[] = ['interval', 'mysql'] + const postgresIdentiers: (keyof Probe)[] = ['interval', 'postgres'] + const pingIdentiers: (keyof Probe)[] = ['interval', 'ping'] + + return Promise.all([ + internalIdentification(httpIdentifiers, probes), + internalIdentification(socketIdentifiers, probes), + internalIdentification(redisIdentiers, probes), + internalIdentification(mongoIdentiers, probes), + internalIdentification(mariadbIdentiers, probes), + internalIdentification(mysqlIdentiers, probes), + internalIdentification(postgresIdentiers, probes), + internalIdentification(pingIdentiers, probes), + ]) +} + +async function internalIdentification( + fieldIdentifiers: (keyof Probe)[], + probes: Probe[] +): Promise> { + const identicalProbeIds: Set = new Set() + + // Iterate through each probe + // eslint-disable-next-line guard-for-in + for (const index in probes) { + const outerProbe = probes[index] + // Skip probes without requests + if (!outerProbe.requests?.length) continue + + // Create an identifier for the outer probe + const outerIdentifier = fieldIdentifiers + // Get values from each field + .map((i) => outerProbe[i]) + // Set 'alerts' fields to undefined for comparison + .map((configValue) => { + if ( + typeof configValue === 'object' && + Object.keys(configValue).includes('alerts') + ) { + return { ...configValue, alerts: undefined } + } + + if (Array.isArray(configValue)) { + return configValue.map((c) => ({ ...c, alerts: undefined })) + } + + return configValue + }) + + // Get all probes except the current one + const innerProbes = probes.filter((p) => p.id !== outerProbe.id) + + // Compare the outer probe with each inner probe + // eslint-disable-next-line guard-for-in + for (const jIndex in innerProbes) { + const innerProbe = innerProbes[jIndex] + // Skip probes without requests + if (!innerProbe.requests?.length) continue + + // Create an identifier for the inner probe + const innerIdentifier = fieldIdentifiers + // Get values from each field + .map((i) => innerProbe[i]) + // Set 'alerts' fields to undefined for comparison + .map((configValue) => { + if ( + typeof configValue === 'object' && + Object.keys(configValue).includes('alerts') + ) { + return { ...configValue, alerts: undefined } + } + + if (Array.isArray(configValue)) { + return configValue.map((c) => ({ ...c, alerts: undefined })) + } + + return configValue + }) + + // Compare outer and inner identifiers + if (lodash.isEqual(outerIdentifier, innerIdentifier)) { + // Add probe IDs to the set of identical probes + if (!identicalProbeIds.has(outerProbe.id)) { + identicalProbeIds.add(outerProbe.id) + } + + identicalProbeIds.add(innerProbe.id) + } + } + } + + // Return the set of identical probe IDs + return identicalProbeIds +} diff --git a/src/components/config/index.ts b/src/components/config/index.ts index 2d935037f..94393e0f2 100644 --- a/src/components/config/index.ts +++ b/src/components/config/index.ts @@ -35,6 +35,8 @@ import { getRawConfig } from './get' import { getProbes, setProbes } from './probe' import { sanitizeConfig } from './sanitize' import { validateConfig } from './validate' +import { identifyIdenticalProbes } from './identical-probes' +import { ProbeRequestResponse } from 'src/interfaces/request' export async function initConfig() { const { flags } = getContext() @@ -115,7 +117,20 @@ export async function updateConfig(config: Config): Promise { log.info('Config changes. Updating config...') } - setContext({ config: sanitizedConfig }) + const identicalProbeIds = await identifyIdenticalProbes(sanitizedConfig) + const initialProbeCache: Map< + Set, + ProbeRequestResponse | undefined + > = new Map() + + for (const id of identicalProbeIds) { + initialProbeCache.set(id, undefined) + } + + setContext({ + config: sanitizedConfig, + probeResultCache: { iterationId: 0, cache: initialProbeCache }, + }) setProbes(sanitizedConfig.probes) getEventEmitter().emit(events.config.updated) } diff --git a/src/context/index.ts b/src/context/index.ts index 37ea6cfa4..6e978230e 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -26,6 +26,7 @@ import type { ValidatedConfig } from '../interfaces/config' import type { ProbeAlert } from '../interfaces/probe' import { type MonikaFlags, monikaFlagsDefaultValue } from '../flag' +import { ProbeRequestResponse } from 'src/interfaces/request' export type Incident = { probeID: string @@ -34,6 +35,7 @@ export type Incident = { createdAt: Date } +type ProbeID = string type Context = { // userAgent example: @hyperjumptech/monika/1.2.3 linux-x64 node-14.17.0 userAgent: string @@ -41,6 +43,10 @@ type Context = { isTest: boolean config?: Omit flags: MonikaFlags + probeResultCache: { + iterationId: number + cache: Map, ProbeRequestResponse | undefined> + } } type NewContext = Partial @@ -50,6 +56,7 @@ const initialContext: Context = { incidents: [], isTest: process.env.NODE_ENV === 'test', flags: monikaFlagsDefaultValue, + probeResultCache: { iterationId: 0, cache: new Map() }, } let context: Context = initialContext From 8103c57d878de0abf62be2aafb11a51a379444da Mon Sep 17 00:00:00 2001 From: Mochamad Noor Syamsu Date: Sun, 1 Dec 2024 19:14:59 +0700 Subject: [PATCH 2/5] compact identical probes --- package-lock.json | 134 +++++++++++++++++--- src/commands/monika.ts | 3 +- src/components/config/compact-config.ts | 143 ++++++++++++++++++++++ src/components/config/identical-probes.ts | 133 -------------------- src/components/config/index.ts | 13 -- src/context/index.ts | 7 -- 6 files changed, 261 insertions(+), 172 deletions(-) create mode 100644 src/components/config/compact-config.ts delete mode 100644 src/components/config/identical-probes.ts diff --git a/package-lock.json b/package-lock.json index 8fe1b4e4c..2e5b3438c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1718,6 +1718,8 @@ "node_modules/@gar/promisify": { "version": "1.1.3", "license": "MIT", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, "node_modules/@gmrchk/cli-testing-library": { @@ -2147,6 +2149,8 @@ "node_modules/@npmcli/fs": { "version": "1.1.1", "license": "ISC", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", "optional": true, "dependencies": { "@gar/promisify": "^1.0.1", @@ -2156,6 +2160,9 @@ "node_modules/@npmcli/move-file": { "version": "1.1.2", "license": "MIT", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", "optional": true, "dependencies": { "mkdirp": "^1.0.4", @@ -4325,6 +4332,8 @@ "node_modules/@tootallnate/once": { "version": "1.1.2", "license": "MIT", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "optional": true, "engines": { "node": ">= 6" @@ -4950,6 +4959,8 @@ "node_modules/agentkeepalive": { "version": "4.5.0", "license": "MIT", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "optional": true, "dependencies": { "humanize-ms": "^1.2.1" @@ -4960,8 +4971,10 @@ }, "node_modules/aggregate-error": { "version": "3.1.0", - "devOptional": true, "license": "MIT", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "devOptional": true, "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -4972,8 +4985,10 @@ }, "node_modules/aggregate-error/node_modules/clean-stack": { "version": "2.2.0", - "devOptional": true, "license": "MIT", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "devOptional": true, "engines": { "node": ">=6" } @@ -5078,8 +5093,10 @@ }, "node_modules/archy": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "license": "MIT", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true }, "node_modules/are-we-there-yet": { "version": "2.0.0", @@ -5609,6 +5626,8 @@ "node_modules/cacache": { "version": "15.3.0", "license": "ISC", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", "optional": true, "dependencies": { "@npmcli/fs": "^1.0.0", @@ -5637,6 +5656,8 @@ "node_modules/cacache/node_modules/lru-cache": { "version": "6.0.0", "license": "ISC", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -5955,6 +5976,8 @@ }, "node_modules/ci-info": { "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -6848,6 +6871,8 @@ "node_modules/env-paths": { "version": "2.2.1", "license": "MIT", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "optional": true, "engines": { "node": ">=6" @@ -6856,6 +6881,8 @@ "node_modules/err-code": { "version": "2.0.3", "license": "MIT", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "optional": true }, "node_modules/error-ex": { @@ -7865,8 +7892,10 @@ }, "node_modules/fastest-levenshtein": { "version": "1.0.16", - "dev": true, "license": "MIT", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, "engines": { "node": ">= 4.9.1" } @@ -8883,8 +8912,10 @@ }, "node_modules/http-cache-semantics": { "version": "4.1.1", - "devOptional": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "devOptional": true }, "node_modules/http-call": { "version": "5.3.0", @@ -8931,6 +8962,8 @@ "node_modules/http-proxy-agent": { "version": "4.0.1", "license": "MIT", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "optional": true, "dependencies": { "@tootallnate/once": "1", @@ -8974,6 +9007,8 @@ "node_modules/humanize-ms": { "version": "1.2.1", "license": "MIT", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "optional": true, "dependencies": { "ms": "^2.0.0" @@ -9082,8 +9117,10 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "devOptional": true, "license": "MIT", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "devOptional": true, "engines": { "node": ">=0.8.19" } @@ -9098,6 +9135,8 @@ "node_modules/infer-owner": { "version": "1.0.4", "license": "ISC", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", "optional": true }, "node_modules/inflight": { @@ -9338,6 +9377,8 @@ "node_modules/is-lambda": { "version": "1.0.1", "license": "MIT", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "optional": true }, "node_modules/is-negative-zero": { @@ -9534,8 +9575,10 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "license": "MIT", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -9832,8 +9875,10 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "dev": true, - "license": "MIT" + "license": "MIT", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -10191,6 +10236,8 @@ "node_modules/make-fetch-happen": { "version": "9.1.0", "license": "ISC", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", "optional": true, "dependencies": { "agentkeepalive": "^4.1.3", @@ -10217,6 +10264,8 @@ "node_modules/make-fetch-happen/node_modules/lru-cache": { "version": "6.0.0", "license": "ISC", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -10550,6 +10599,8 @@ "node_modules/minipass-collect": { "version": "1.0.2", "license": "ISC", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", "optional": true, "dependencies": { "minipass": "^3.0.0" @@ -10561,6 +10612,8 @@ "node_modules/minipass-fetch": { "version": "1.4.1", "license": "MIT", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", "optional": true, "dependencies": { "minipass": "^3.1.0", @@ -10577,6 +10630,8 @@ "node_modules/minipass-flush": { "version": "1.0.5", "license": "ISC", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "optional": true, "dependencies": { "minipass": "^3.0.0" @@ -10588,6 +10643,8 @@ "node_modules/minipass-pipeline": { "version": "1.2.4", "license": "ISC", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "optional": true, "dependencies": { "minipass": "^3.0.0" @@ -10599,6 +10656,8 @@ "node_modules/minipass-sized": { "version": "1.0.3", "license": "ISC", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "optional": true, "dependencies": { "minipass": "^3.0.0" @@ -11119,6 +11178,8 @@ "node_modules/node-gyp": { "version": "8.4.1", "license": "MIT", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", "optional": true, "dependencies": { "env-paths": "^2.2.0", @@ -11152,6 +11213,9 @@ "node_modules/node-gyp/node_modules/are-we-there-yet": { "version": "3.0.1", "license": "ISC", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", "optional": true, "dependencies": { "delegates": "^1.0.0", @@ -11164,6 +11228,9 @@ "node_modules/node-gyp/node_modules/gauge": { "version": "4.0.4", "license": "ISC", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", "optional": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -11182,11 +11249,16 @@ "node_modules/node-gyp/node_modules/isexe": { "version": "2.0.0", "license": "ISC", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "optional": true }, "node_modules/node-gyp/node_modules/npmlog": { "version": "6.0.2", "license": "ISC", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", "optional": true, "dependencies": { "are-we-there-yet": "^3.0.0", @@ -11201,6 +11273,8 @@ "node_modules/node-gyp/node_modules/which": { "version": "2.0.2", "license": "ISC", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "optional": true, "dependencies": { "isexe": "^2.0.0" @@ -11250,8 +11324,10 @@ }, "node_modules/normalize-package-data": { "version": "6.0.2", - "dev": true, "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", @@ -14209,8 +14285,10 @@ }, "node_modules/p-map": { "version": "4.0.0", - "devOptional": true, "license": "MIT", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "devOptional": true, "dependencies": { "aggregate-error": "^3.0.0" }, @@ -14974,11 +15052,15 @@ "node_modules/promise-inflight": { "version": "1.0.1", "license": "ISC", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "optional": true }, "node_modules/promise-retry": { "version": "2.0.1", "license": "MIT", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "optional": true, "dependencies": { "err-code": "^2.0.2", @@ -14991,6 +15073,8 @@ "node_modules/promise-retry/node_modules/retry": { "version": "0.12.0", "license": "MIT", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "optional": true, "engines": { "node": ">= 4" @@ -15980,6 +16064,8 @@ "node_modules/socks-proxy-agent": { "version": "6.2.1", "license": "MIT", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", "optional": true, "dependencies": { "agent-base": "^6.0.2", @@ -16178,6 +16264,8 @@ "node_modules/ssri": { "version": "8.0.1", "license": "ISC", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", "optional": true, "dependencies": { "minipass": "^3.1.1" @@ -16565,8 +16653,10 @@ }, "node_modules/text-table": { "version": "0.2.0", - "dev": true, - "license": "MIT" + "license": "MIT", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/thread-stream": { "version": "2.7.0", @@ -16852,8 +16942,10 @@ }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", - "dev": true, "license": "MIT", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, "dependencies": { "is-typedarray": "^1.0.0" } @@ -16912,6 +17004,8 @@ "node_modules/unique-filename": { "version": "1.1.1", "license": "ISC", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", "optional": true, "dependencies": { "unique-slug": "^2.0.0" @@ -16920,6 +17014,8 @@ "node_modules/unique-slug": { "version": "2.0.2", "license": "ISC", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", "optional": true, "dependencies": { "imurmurhash": "^0.1.4" @@ -17296,8 +17392,10 @@ }, "node_modules/write-file-atomic": { "version": "3.0.3", - "dev": true, "license": "ISC", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", diff --git a/src/commands/monika.ts b/src/commands/monika.ts index 5be09e1b1..6aa375152 100644 --- a/src/commands/monika.ts +++ b/src/commands/monika.ts @@ -57,6 +57,7 @@ import { close as closeSentry, flush as flushSentry, } from '@sentry/node' +import { compactProbes } from '../components/config/compact-config' const em = getEventEmitter() let symonClient: SymonClient @@ -163,7 +164,7 @@ export default class Monika extends Command { for (;;) { const config = getValidatedConfig() - const probes = getProbes({ config, flags }) + const probes = compactProbes(getProbes({ config, flags })) // emit the sanitized probe em.emit(events.config.sanitized, probes) diff --git a/src/components/config/compact-config.ts b/src/components/config/compact-config.ts new file mode 100644 index 000000000..6ec4dd41e --- /dev/null +++ b/src/components/config/compact-config.ts @@ -0,0 +1,143 @@ +/********************************************************************************** + * 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 lodash from 'lodash' +import type { Probe } from '../../interfaces/probe' + +export function compactProbes(probes: Probe[]): Probe[] { + const identicalProbeIds = identifyIdenticalProbes(probes) + if (identicalProbeIds.length === 0) { + return probes + } + + const nonIdenticalProbes = probes.filter( + (probe) => !identicalProbeIds.some((set) => set.has(probe.id)) + ) + + const mergedProbes = identicalProbeIds.map((set) => { + const identicalProbes = probes.filter((probe) => set.has(probe.id)) + return mergeProbes(identicalProbes) + }) + + return [...nonIdenticalProbes, ...mergedProbes] +} + +function mergeProbes(probes: Probe[]): Probe { + const mergedIds = probes.map((probe) => probe.id).join('|') + const mergedAlerts = probes.flatMap((probe) => probe.alerts ?? []) + let probe = probes[0] + for (const p of probes.slice(1)) { + probe = { ...probe, ...p, alerts: mergedAlerts, id: mergedIds } + } + + return probe +} + +// Function to identify identical probes based on specific fields +function identifyIdenticalProbes(probes: Probe[]): Set[] { + // Define fields that are used to determine if probes are identical + const httpIdentifiers: (keyof Probe)[] = ['interval', 'requests'] + const socketIdentifiers: (keyof Probe)[] = ['interval', 'socket'] + const redisIdentiers: (keyof Probe)[] = ['interval', 'redis'] + const mongoIdentiers: (keyof Probe)[] = ['interval', 'mongo'] + const mariadbIdentiers: (keyof Probe)[] = ['interval', 'mariadb'] + const mysqlIdentiers: (keyof Probe)[] = ['interval', 'mysql'] + const postgresIdentiers: (keyof Probe)[] = ['interval', 'postgres'] + const pingIdentiers: (keyof Probe)[] = ['interval', 'ping'] + + return [ + ...internalIdentification(httpIdentifiers, probes), + ...internalIdentification(socketIdentifiers, probes), + ...internalIdentification(redisIdentiers, probes), + ...internalIdentification(mongoIdentiers, probes), + ...internalIdentification(mariadbIdentiers, probes), + ...internalIdentification(mysqlIdentiers, probes), + ...internalIdentification(postgresIdentiers, probes), + ...internalIdentification(pingIdentiers, probes), + ] +} + +// suppress double loop complexity +// eslint-disable-next-line complexity +function internalIdentification( + fieldIdentifiers: (keyof Probe)[], + probes: Probe[] +): Set[] { + const identicalProbeIds: Set[] = [] + + for (const outerProbe of probes) { + // skip probes with request chaining + const isChainingHttp = outerProbe.requests && outerProbe.requests.length > 1 + const isRedisChaining = outerProbe.redis && outerProbe.redis.length > 1 + const isMongoChaining = outerProbe.mongo && outerProbe.mongo.length > 1 + const isMariaDbChaining = + outerProbe.mariadb && outerProbe.mariadb.length > 1 + const isMysqlChaining = outerProbe.mysql && outerProbe.mysql.length > 1 + const isPostgresChaining = + outerProbe.postgres && outerProbe.postgres.length > 1 + const isPingChaining = outerProbe.ping && outerProbe.ping.length > 1 + if ( + isChainingHttp || + isRedisChaining || + isMongoChaining || + isMariaDbChaining || + isMysqlChaining || + isPostgresChaining || + isPingChaining + ) { + continue + } + + // skip if probe is already identified as identical + const isIdentified = identicalProbeIds.some((set) => set.has(outerProbe.id)) + if (isIdentified) { + continue + } + + const currentProbeValues = fieldIdentifiers.map((key) => outerProbe[key]) + const identicalProbeIdsSet = new Set() + for (const innerProbe of probes) { + // skip if innerProbe has the same ID as the current probe + if (outerProbe.id === innerProbe.id) { + continue + } + + const innerProbeFieldValues = fieldIdentifiers.map( + (key) => innerProbe[key] + ) + + if (lodash.isEqual(currentProbeValues, innerProbeFieldValues)) { + identicalProbeIdsSet.add(outerProbe.id) + identicalProbeIdsSet.add(innerProbe.id) + } + } + + if (identicalProbeIdsSet.size > 0) { + identicalProbeIds.push(identicalProbeIdsSet) + } + } + + // Return the set of identical probe IDs + return identicalProbeIds +} diff --git a/src/components/config/identical-probes.ts b/src/components/config/identical-probes.ts deleted file mode 100644 index dd66d559e..000000000 --- a/src/components/config/identical-probes.ts +++ /dev/null @@ -1,133 +0,0 @@ -/********************************************************************************** - * 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 lodash from 'lodash' -import type { Probe } from '../../interfaces/probe' -import type { ValidatedConfig } from '../../interfaces/config' - -// Function to identify identical probes based on specific fields -export async function identifyIdenticalProbes( - config: ValidatedConfig -): Promise[]> { - const { probes } = config - // Define fields that are used to determine if probes are identical - const httpIdentifiers: (keyof Probe)[] = ['interval', 'requests'] - const socketIdentifiers: (keyof Probe)[] = ['interval', 'socket'] - const redisIdentiers: (keyof Probe)[] = ['interval', 'redis'] - const mongoIdentiers: (keyof Probe)[] = ['interval', 'mongo'] - const mariadbIdentiers: (keyof Probe)[] = ['interval', 'mariadb'] - const mysqlIdentiers: (keyof Probe)[] = ['interval', 'mysql'] - const postgresIdentiers: (keyof Probe)[] = ['interval', 'postgres'] - const pingIdentiers: (keyof Probe)[] = ['interval', 'ping'] - - return Promise.all([ - internalIdentification(httpIdentifiers, probes), - internalIdentification(socketIdentifiers, probes), - internalIdentification(redisIdentiers, probes), - internalIdentification(mongoIdentiers, probes), - internalIdentification(mariadbIdentiers, probes), - internalIdentification(mysqlIdentiers, probes), - internalIdentification(postgresIdentiers, probes), - internalIdentification(pingIdentiers, probes), - ]) -} - -async function internalIdentification( - fieldIdentifiers: (keyof Probe)[], - probes: Probe[] -): Promise> { - const identicalProbeIds: Set = new Set() - - // Iterate through each probe - // eslint-disable-next-line guard-for-in - for (const index in probes) { - const outerProbe = probes[index] - // Skip probes without requests - if (!outerProbe.requests?.length) continue - - // Create an identifier for the outer probe - const outerIdentifier = fieldIdentifiers - // Get values from each field - .map((i) => outerProbe[i]) - // Set 'alerts' fields to undefined for comparison - .map((configValue) => { - if ( - typeof configValue === 'object' && - Object.keys(configValue).includes('alerts') - ) { - return { ...configValue, alerts: undefined } - } - - if (Array.isArray(configValue)) { - return configValue.map((c) => ({ ...c, alerts: undefined })) - } - - return configValue - }) - - // Get all probes except the current one - const innerProbes = probes.filter((p) => p.id !== outerProbe.id) - - // Compare the outer probe with each inner probe - // eslint-disable-next-line guard-for-in - for (const jIndex in innerProbes) { - const innerProbe = innerProbes[jIndex] - // Skip probes without requests - if (!innerProbe.requests?.length) continue - - // Create an identifier for the inner probe - const innerIdentifier = fieldIdentifiers - // Get values from each field - .map((i) => innerProbe[i]) - // Set 'alerts' fields to undefined for comparison - .map((configValue) => { - if ( - typeof configValue === 'object' && - Object.keys(configValue).includes('alerts') - ) { - return { ...configValue, alerts: undefined } - } - - if (Array.isArray(configValue)) { - return configValue.map((c) => ({ ...c, alerts: undefined })) - } - - return configValue - }) - - // Compare outer and inner identifiers - if (lodash.isEqual(outerIdentifier, innerIdentifier)) { - // Add probe IDs to the set of identical probes - if (!identicalProbeIds.has(outerProbe.id)) { - identicalProbeIds.add(outerProbe.id) - } - - identicalProbeIds.add(innerProbe.id) - } - } - } - - // Return the set of identical probe IDs - return identicalProbeIds -} diff --git a/src/components/config/index.ts b/src/components/config/index.ts index 94393e0f2..b1ae7dc0c 100644 --- a/src/components/config/index.ts +++ b/src/components/config/index.ts @@ -35,8 +35,6 @@ import { getRawConfig } from './get' import { getProbes, setProbes } from './probe' import { sanitizeConfig } from './sanitize' import { validateConfig } from './validate' -import { identifyIdenticalProbes } from './identical-probes' -import { ProbeRequestResponse } from 'src/interfaces/request' export async function initConfig() { const { flags } = getContext() @@ -117,19 +115,8 @@ export async function updateConfig(config: Config): Promise { log.info('Config changes. Updating config...') } - const identicalProbeIds = await identifyIdenticalProbes(sanitizedConfig) - const initialProbeCache: Map< - Set, - ProbeRequestResponse | undefined - > = new Map() - - for (const id of identicalProbeIds) { - initialProbeCache.set(id, undefined) - } - setContext({ config: sanitizedConfig, - probeResultCache: { iterationId: 0, cache: initialProbeCache }, }) setProbes(sanitizedConfig.probes) getEventEmitter().emit(events.config.updated) diff --git a/src/context/index.ts b/src/context/index.ts index 6e978230e..37ea6cfa4 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -26,7 +26,6 @@ import type { ValidatedConfig } from '../interfaces/config' import type { ProbeAlert } from '../interfaces/probe' import { type MonikaFlags, monikaFlagsDefaultValue } from '../flag' -import { ProbeRequestResponse } from 'src/interfaces/request' export type Incident = { probeID: string @@ -35,7 +34,6 @@ export type Incident = { createdAt: Date } -type ProbeID = string type Context = { // userAgent example: @hyperjumptech/monika/1.2.3 linux-x64 node-14.17.0 userAgent: string @@ -43,10 +41,6 @@ type Context = { isTest: boolean config?: Omit flags: MonikaFlags - probeResultCache: { - iterationId: number - cache: Map, ProbeRequestResponse | undefined> - } } type NewContext = Partial @@ -56,7 +50,6 @@ const initialContext: Context = { incidents: [], isTest: process.env.NODE_ENV === 'test', flags: monikaFlagsDefaultValue, - probeResultCache: { iterationId: 0, cache: new Map() }, } let context: Context = initialContext From 8e8a39fe3480c6b6cce1daace8843255f3594d22 Mon Sep 17 00:00:00 2001 From: Mochamad Noor Syamsu Date: Sun, 15 Dec 2024 23:10:22 +0700 Subject: [PATCH 3/5] compact probes complete --- src/commands/monika.ts | 3 +- src/components/config/compact-config.ts | 390 ++++++++++++++++++++++-- src/components/config/index.ts | 9 +- src/components/logger/history.ts | 25 ++ src/flag.ts | 7 + src/interfaces/probe.ts | 1 + 6 files changed, 405 insertions(+), 30 deletions(-) diff --git a/src/commands/monika.ts b/src/commands/monika.ts index 6aa375152..5be09e1b1 100644 --- a/src/commands/monika.ts +++ b/src/commands/monika.ts @@ -57,7 +57,6 @@ import { close as closeSentry, flush as flushSentry, } from '@sentry/node' -import { compactProbes } from '../components/config/compact-config' const em = getEventEmitter() let symonClient: SymonClient @@ -164,7 +163,7 @@ export default class Monika extends Command { for (;;) { const config = getValidatedConfig() - const probes = compactProbes(getProbes({ config, flags })) + const probes = getProbes({ config, flags }) // emit the sanitized probe em.emit(events.config.sanitized, probes) diff --git a/src/components/config/compact-config.ts b/src/components/config/compact-config.ts index 6ec4dd41e..eda0f9acc 100644 --- a/src/components/config/compact-config.ts +++ b/src/components/config/compact-config.ts @@ -23,35 +23,303 @@ **********************************************************************************/ import lodash from 'lodash' -import type { Probe } from '../../interfaces/probe' +import type { Probe, ProbeAlert } from '../../interfaces/probe' +import { randomUUID } from 'node:crypto' +import { log } from '../../utils/pino' + +let decompactedProbes: Probe[] = [] export function compactProbes(probes: Probe[]): Probe[] { + if (probes.length === 0) { + return [] + } + + decompactedProbes = probes const identicalProbeIds = identifyIdenticalProbes(probes) + for (const set of identicalProbeIds) { + const identicalProbeIdsArray = [...set] + log.info( + `Found identical probes, following probes IDs will be compacted on runtime: ${identicalProbeIdsArray.join( + ', ' + )}` + ) + } + if (identicalProbeIds.length === 0) { - return probes + return probes.map((probe) => { + const mergedProbeAlerts = mergeProbeAlerts([probe]) + mergedProbeAlerts.id = probe.id + delete mergedProbeAlerts.jointId + return mergedProbeAlerts + }) } - const nonIdenticalProbes = probes.filter( - (probe) => !identicalProbeIds.some((set) => set.has(probe.id)) - ) + const nonIdenticalProbes = probes + .filter((probe) => !identicalProbeIds.some((set) => set.has(probe.id))) + .map((probe) => mergeProbeAlerts([probe])) const mergedProbes = identicalProbeIds.map((set) => { const identicalProbes = probes.filter((probe) => set.has(probe.id)) - return mergeProbes(identicalProbes) + return mergeProbeAlerts(identicalProbes) }) return [...nonIdenticalProbes, ...mergedProbes] } -function mergeProbes(probes: Probe[]): Probe { - const mergedIds = probes.map((probe) => probe.id).join('|') - const mergedAlerts = probes.flatMap((probe) => probe.alerts ?? []) - let probe = probes[0] +export function getDecompactedProbesById(probeId: string): Probe | undefined { + return decompactedProbes.find((probe) => probe.id === probeId) +} + +function mergeProbeAlerts(probes: Probe[]): Probe { + const mergedAlerts = probes.flatMap((probe) => probe.alerts) + let mergedProbe: Probe = { + ...mergeInnerAlerts(probes[0], probes[0]), + alerts: deduplicateAlerts(mergedAlerts), + } + for (const p of probes.slice(1)) { - probe = { ...probe, ...p, alerts: mergedAlerts, id: mergedIds } + mergedProbe = mergeInnerAlerts(p, mergedProbe) + } + + return { + ...mergedProbe, + id: randomUUID(), + jointId: probes.map((probe) => probe.id), + } +} + +// merge corresponding alerts from the probes +function mergeInnerAlerts(newProbe: Probe, destination: Probe): Probe { + destination = mergeHttpAlerts(newProbe, destination) + destination = mergeSocketAlerts(newProbe, destination) + destination = mergeRedisAlerts(newProbe, destination) + destination = mergeMongoAlerts(newProbe, destination) + destination = mergeMariaDbAlerts(newProbe, destination) + destination = mergeMysqlAlerts(newProbe, destination) + destination = mergePostgresAlerts(newProbe, destination) + destination = mergePingAlerts(newProbe, destination) + + return destination +} + +function deduplicateAlerts(alerts: ProbeAlert[]): ProbeAlert[] { + if (alerts.filter((a) => a !== undefined).length === 0) { + return [] + } + + const deduplicatedAlerts: ProbeAlert[] = [alerts[0]] + for (const alert of alerts.slice(1)) { + // Check if the alert is already in the deduplicatedAlerts + const isAlertExist = deduplicatedAlerts.some((a) => { + const firstPayload: Partial = lodash.cloneDeep(a) + const secondPayload: Partial = lodash.cloneDeep(alert) + if (firstPayload?.id) { + delete firstPayload.id + } + + if (secondPayload?.id) { + delete secondPayload.id + } + + return lodash.isEqual(firstPayload, secondPayload) + }) + + if (alert !== undefined && !isAlertExist) { + deduplicatedAlerts.push(alert) + } + } + + return deduplicatedAlerts +} + +function mergeHttpAlerts(newProbe: Probe, destination: Probe): Probe { + if ( + destination.requests && + (newProbe.requests?.every((r) => r.alerts?.length) || + destination.requests.every((r) => r.alerts?.length)) + ) { + const mergedRequestAlerts: ProbeAlert[] = [ + ...(destination.requests?.flatMap((r) => r.alerts) || []), + ...(newProbe?.requests?.flatMap((r) => r.alerts) || []), + ] as ProbeAlert[] + + return { + ...destination, + requests: [ + ...(destination?.requests?.map((r) => ({ + ...r, + alerts: deduplicateAlerts(mergedRequestAlerts), + })) || []), + ], + } + } + + return destination +} + +function mergeSocketAlerts(newProbe: Probe, destination: Probe): Probe { + if ( + destination.socket && + (newProbe.socket?.alerts?.length || destination.socket?.alerts?.length) + ) { + const mergedSocketAlerts: ProbeAlert[] = [ + ...(destination.socket?.alerts || []), + ...(newProbe.socket?.alerts || []), + ] + return { + ...destination, + socket: { + ...destination.socket, + alerts: deduplicateAlerts(mergedSocketAlerts), + }, + } + } + + return destination +} + +function mergeRedisAlerts(newProbe: Probe, destination: Probe): Probe { + if ( + destination.redis && + (newProbe.redis?.every((r) => r.alerts?.length) || + destination.redis.every((r) => r.alerts?.length)) + ) { + const mergedRedisAlerts: ProbeAlert[] = [ + ...(destination.redis?.flatMap((r) => r.alerts) || []), + ...(newProbe.redis?.flatMap((r) => r.alerts) || []), + ] as ProbeAlert[] + return { + ...destination, + redis: [ + ...(destination.redis?.map((r) => ({ + ...r, + alerts: deduplicateAlerts(mergedRedisAlerts), + })) || []), + ], + } + } + + return destination +} + +function mergeMongoAlerts(newProbe: Probe, destination: Probe): Probe { + if ( + destination.mongo && + (destination.mongo.every((r) => r.alerts?.length) || + newProbe.mongo?.every((r) => r.alerts?.length)) + ) { + const mergedMongoAlerts: ProbeAlert[] = [ + ...(destination.mongo?.flatMap((r) => r.alerts) || []), + ...(newProbe.mongo?.flatMap((r) => r.alerts) || []), + ] as ProbeAlert[] + return { + ...destination, + mongo: [ + ...(destination.mongo?.map((r) => ({ + ...r, + alerts: deduplicateAlerts(mergedMongoAlerts), + })) || []), + ], + } + } + + return destination +} + +function mergeMariaDbAlerts(newProbe: Probe, destination: Probe): Probe { + if ( + destination.mariadb && + (destination.mariadb.every((r) => r.alerts?.length) || + newProbe.mariadb?.every((r) => r.alerts?.length)) + ) { + const mergedMariadbAlerts: ProbeAlert[] = [ + ...(destination.mariadb?.flatMap((r) => r.alerts) || []), + ...(newProbe.mariadb?.flatMap((r) => r.alerts) || []), + ] as ProbeAlert[] + return { + ...destination, + mariadb: [ + ...(destination.mariadb?.map((r) => ({ + ...r, + alerts: deduplicateAlerts(mergedMariadbAlerts), + })) || []), + ], + } } - return probe + return destination +} + +function mergeMysqlAlerts(newProbe: Probe, destination: Probe): Probe { + if ( + destination.mysql && + (destination.mysql.every((r) => r.alerts?.length) || + newProbe.mysql?.every((r) => r.alerts?.length)) + ) { + const mergedMysqlAlerts: ProbeAlert[] = [ + ...(destination.mysql?.flatMap((r) => r.alerts) || []), + ...(newProbe.mysql?.flatMap((r) => r.alerts) || []), + ] as ProbeAlert[] + return { + ...destination, + mysql: [ + ...(destination.mysql?.map((r) => ({ + ...r, + alerts: deduplicateAlerts(mergedMysqlAlerts), + })) || []), + ], + } + } + + return destination +} + +function mergePostgresAlerts(newProbe: Probe, destination: Probe): Probe { + if ( + destination.postgres && + (destination.postgres.every((r) => r.alerts?.length) || + newProbe.postgres?.every((r) => r.alerts?.length)) + ) { + const mergedPostgresAlerts: ProbeAlert[] = [ + ...(destination.postgres?.flatMap((r) => r.alerts) || []), + ...(newProbe.postgres?.flatMap((r) => r.alerts) || []), + ] as ProbeAlert[] + return { + ...destination, + postgres: [ + ...(destination.postgres?.map((r) => ({ + ...r, + alerts: deduplicateAlerts(mergedPostgresAlerts), + })) || []), + ], + } + } + + return destination +} + +function mergePingAlerts(newProbe: Probe, destination: Probe): Probe { + if ( + destination.ping && + (destination.ping.every((r) => r.alerts?.length) || + newProbe.ping?.every((r) => r.alerts?.length)) + ) { + const mergedPingAlerts: ProbeAlert[] = [ + ...(destination.ping?.flatMap((r) => r.alerts) || []), + ...(newProbe.ping?.flatMap((r) => r.alerts) || []), + ] as ProbeAlert[] + return { + ...destination, + ping: [ + ...(destination.ping?.map((r) => ({ + ...r, + alerts: deduplicateAlerts(mergedPingAlerts), + })) || []), + ], + } + } + + return destination } // Function to identify identical probes based on specific fields @@ -78,6 +346,12 @@ function identifyIdenticalProbes(probes: Probe[]): Set[] { ] } +/** + * internalIdentification is a helper function to identify identical probes based on specific fields + * @param fieldIdentifiers identifiers to determine if probes are identical + * @param probes array of probes + * @returns array of sets of identical probe IDs + */ // suppress double loop complexity // eslint-disable-next-line complexity function internalIdentification( @@ -87,24 +361,41 @@ function internalIdentification( const identicalProbeIds: Set[] = [] for (const outerProbe of probes) { + // skip probes with multiple probe types + const isChainingMultipleProbeTypes = + [ + outerProbe.requests, + outerProbe.socket, + outerProbe.redis, + outerProbe.mongo, + outerProbe.mariadb, + outerProbe.mysql, + outerProbe.postgres, + outerProbe.ping, + ].filter((probe) => probe !== undefined).length > 1 + + if (isChainingMultipleProbeTypes) { + continue + } + // skip probes with request chaining const isChainingHttp = outerProbe.requests && outerProbe.requests.length > 1 - const isRedisChaining = outerProbe.redis && outerProbe.redis.length > 1 - const isMongoChaining = outerProbe.mongo && outerProbe.mongo.length > 1 - const isMariaDbChaining = + const isChainingRedis = outerProbe.redis && outerProbe.redis.length > 1 + const isChainingMongo = outerProbe.mongo && outerProbe.mongo.length > 1 + const isChainingMariaDb = outerProbe.mariadb && outerProbe.mariadb.length > 1 - const isMysqlChaining = outerProbe.mysql && outerProbe.mysql.length > 1 - const isPostgresChaining = + const isChainingMysql = outerProbe.mysql && outerProbe.mysql.length > 1 + const isChainingPostgres = outerProbe.postgres && outerProbe.postgres.length > 1 - const isPingChaining = outerProbe.ping && outerProbe.ping.length > 1 + const isChainingPing = outerProbe.ping && outerProbe.ping.length > 1 if ( isChainingHttp || - isRedisChaining || - isMongoChaining || - isMariaDbChaining || - isMysqlChaining || - isPostgresChaining || - isPingChaining + isChainingRedis || + isChainingMongo || + isChainingMariaDb || + isChainingMysql || + isChainingPostgres || + isChainingPing ) { continue } @@ -115,7 +406,14 @@ function internalIdentification( continue } - const currentProbeValues = fieldIdentifiers.map((key) => outerProbe[key]) + // get the values of the fields to be used for comparison + const currentProbeValues = stripNameAndAlerts(outerProbe, fieldIdentifiers) + + // skip if there are undefined values from identifiers + if (currentProbeValues.includes(undefined)) { + continue + } + const identicalProbeIdsSet = new Set() for (const innerProbe of probes) { // skip if innerProbe has the same ID as the current probe @@ -123,10 +421,13 @@ function internalIdentification( continue } - const innerProbeFieldValues = fieldIdentifiers.map( - (key) => innerProbe[key] + // get the values of the fields to be used for comparison + const innerProbeFieldValues = stripNameAndAlerts( + innerProbe, + fieldIdentifiers ) + // compare the values of the fields using deep equality if (lodash.isEqual(currentProbeValues, innerProbeFieldValues)) { identicalProbeIdsSet.add(outerProbe.id) identicalProbeIdsSet.add(innerProbe.id) @@ -141,3 +442,38 @@ function internalIdentification( // Return the set of identical probe IDs return identicalProbeIds } + +function stripNameAndAlerts(probe: Probe, fieldIdentifiers: (keyof Probe)[]) { + const strippedProbe = fieldIdentifiers.map((key) => { + // clone the value to prevent mutation + let value = lodash.cloneDeep(probe[key]) + if (value !== undefined) { + value = deleteNameFromProbe(value) as never + value = deleteAlertsFromProbe(value) as never + } + + return value + }) + + return strippedProbe +} + +function deleteNameFromProbe(values: unknown): unknown { + if (typeof values === 'object' && values !== null && 'name' in values) { + delete values.name + } + + return values +} + +function deleteAlertsFromProbe(values: unknown): unknown { + if (Array.isArray(values)) { + return values.map((value) => deleteAlertsFromProbe(value)) + } + + if (typeof values === 'object' && values !== null && 'alerts' in values) { + delete values.alerts + } + + return values +} diff --git a/src/components/config/index.ts b/src/components/config/index.ts index b1ae7dc0c..435b86032 100644 --- a/src/components/config/index.ts +++ b/src/components/config/index.ts @@ -35,6 +35,7 @@ import { getRawConfig } from './get' import { getProbes, setProbes } from './probe' import { sanitizeConfig } from './sanitize' import { validateConfig } from './validate' +import { compactProbes } from './compact-config' export async function initConfig() { const { flags } = getContext() @@ -118,7 +119,13 @@ export async function updateConfig(config: Config): Promise { setContext({ config: sanitizedConfig, }) - setProbes(sanitizedConfig.probes) + + let { probes } = sanitizedConfig + if (getContext().flags['compact-probes']) { + probes = compactProbes(probes) + } + + setProbes(probes) getEventEmitter().emit(events.config.updated) } diff --git a/src/components/logger/history.ts b/src/components/logger/history.ts index 3c8e2e14e..ffd1fedbd 100644 --- a/src/components/logger/history.ts +++ b/src/components/logger/history.ts @@ -31,6 +31,7 @@ import { Probe } from '../../interfaces/probe' import type { Notification } from '@hyperjumptech/monika-notification' import { log } from '../../utils/pino' import { getErrorMessage } from '../../utils/catch-error-handler' +import { getDecompactedProbesById } from '../config/compact-config' const sqlite3 = verboseSQLite() const dbPath = path.resolve(process.cwd(), 'monika-logs.db') @@ -413,6 +414,30 @@ export async function saveProbeRequestLog({ alertQueries?: string[] error?: string }): Promise { + if (probe.jointId?.length) { + const probes = + (probe.jointId + ?.map((id) => getDecompactedProbesById(id)) + ?.filter(Boolean) as Probe[]) || [] + + await Promise.allSettled( + probes.map((originalProbe) => + saveProbeRequestLog({ + probe: originalProbe, + requestIndex, + probeRes, + alertQueries: + (originalProbe.alerts + ?.map((alert) => alert.query) + .filter(Boolean) as string[]) || [], + error: errorResp, + }) + ) + ) + + return + } + const insertProbeRequestSQL = ` INSERT INTO probe_requests ( created_at, diff --git a/src/flag.ts b/src/flag.ts index 8139fea1c..228bd33d3 100644 --- a/src/flag.ts +++ b/src/flag.ts @@ -34,6 +34,7 @@ export enum SYMON_API_VERSION { export type MonikaFlags = { 'auto-update'?: string + 'compact-probes'?: boolean config: string[] 'config-filename': string 'config-interval': number @@ -80,6 +81,7 @@ const DEFAULT_CONFIG_INTERVAL_SECONDS = 900 const DEFAULT_SYMON_REPORT_INTERVAL_MS = 10_000 export const monikaFlagsDefaultValue: MonikaFlags = { + 'compact-probes': false, config: getDefaultConfig(), 'config-filename': 'monika.yml', 'config-interval': DEFAULT_CONFIG_INTERVAL_SECONDS, @@ -126,6 +128,11 @@ export const flags = { description: 'Enable auto-update for Monika. Available options: major, minor, patch. This will make Monika terminate itself on successful update but does not restart', }), + 'compact-probes': Flags.boolean({ + default: monikaFlagsDefaultValue['compact-probes'], + description: + 'Compact probes with the same request configuration. This will not merge probes with request chaining.', + }), config: Flags.string({ char: 'c', default: monikaFlagsDefaultValue.config, diff --git a/src/interfaces/probe.ts b/src/interfaces/probe.ts index bdbb4bc63..7eaf75df6 100644 --- a/src/interfaces/probe.ts +++ b/src/interfaces/probe.ts @@ -88,6 +88,7 @@ export type Ping = { export interface Probe { id: string + jointId?: string[] name: string description?: string interval: number From 218ea3b89b145dd411ef7f7b462291ae7370f126 Mon Sep 17 00:00:00 2001 From: Mochamad Noor Syamsu Date: Sun, 15 Dec 2024 23:10:32 +0700 Subject: [PATCH 4/5] add test --- src/components/config/compact-config.test.ts | 350 +++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 src/components/config/compact-config.test.ts diff --git a/src/components/config/compact-config.test.ts b/src/components/config/compact-config.test.ts new file mode 100644 index 000000000..463b2b2fd --- /dev/null +++ b/src/components/config/compact-config.test.ts @@ -0,0 +1,350 @@ +/********************************************************************************** + * 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 'chai' +import { compactProbes, getDecompactedProbesById } from './compact-config' +import type { Probe } from '../../interfaces/probe' + +describe('Probe Compaction', () => { + describe('compactProbes', () => { + it('should return original probes if no identical probes found', () => { + const probes: Probe[] = [ + { + id: '1', + name: 'Test 1', + interval: 30, + requests: [ + { url: 'http://test1.com', followRedirects: 5, timeout: 10_000 }, + ], + alerts: [], + }, + { + id: '2', + name: 'Test 2', + interval: 60, + requests: [ + { url: 'http://test2.com', followRedirects: 5, timeout: 10_000 }, + ], + alerts: [], + }, + ] + + const result = compactProbes(probes) + expect(result).to.deep.equal(probes) + }) + + it('should merge identical HTTP probes', () => { + const probes: Probe[] = [ + { + id: '1', + name: 'Test 2', + alerts: [], + interval: 30, + requests: [ + { + url: 'http://test.com', + followRedirects: 5, + timeout: 10_000, + alerts: [ + { + id: 'alert1', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + ], + }, + ], + }, + { + id: '2', + name: 'Test 2', + alerts: [], + interval: 30, + requests: [ + { + url: 'http://test.com', + followRedirects: 5, + timeout: 10_000, + alerts: [ + { + id: 'alert2', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + ], + }, + ], + }, + ] + + const result = compactProbes(probes) + expect(result).to.have.lengthOf(1) + expect(result[0].requests?.[0].alerts).to.have.lengthOf(1) + }) + + it('should merge identical socket probes', () => { + const probes: Probe[] = [ + { + id: '1', + interval: 30, + name: 'Test 1', + alerts: [], + socket: { + host: 'test.com', + port: 80, + data: 'test', + alerts: [ + { + id: 'alert1', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + ], + }, + }, + { + id: '2', + interval: 30, + name: 'Test 2', + alerts: [], + socket: { + host: 'test.com', + port: 80, + data: 'test', + alerts: [ + { + id: 'alert2', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + ], + }, + }, + ] + + const result = compactProbes(probes) + expect(result).to.have.lengthOf(1) + expect(result[0].socket?.alerts).to.have.lengthOf(1) + }) + + it('should not merge probes with multiple probe types', () => { + const probes: Probe[] = [ + { + id: '1', + interval: 30, + name: 'Test 1', + alerts: [], + requests: [ + { url: 'http://test.com', followRedirects: 5, timeout: 10_000 }, + ], + socket: { + host: 'test.com', + port: 80, + data: 'test', + alerts: [ + { + id: 'alert1', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + ], + }, + }, + { + id: '2', + interval: 30, + name: 'Test 2', + alerts: [], + requests: [ + { url: 'http://test.com', followRedirects: 5, timeout: 10_000 }, + ], + socket: { host: 'test.com', port: 80, data: 'test' }, + }, + ] + + const result = compactProbes(probes) + expect(result).to.deep.equal(probes) + }) + + it('should not merge probes with request chaining', () => { + const probes: Probe[] = [ + { + id: '1', + interval: 30, + name: 'Test 1', + alerts: [], + requests: [ + { url: 'http://test1.com', followRedirects: 5, timeout: 10_000 }, + { url: 'http://test2.com', followRedirects: 5, timeout: 10_000 }, + ], + }, + { + id: '2', + interval: 30, + name: 'Test 2', + alerts: [], + requests: [ + { url: 'http://test1.com', followRedirects: 5, timeout: 10_000 }, + { url: 'http://test2.com', followRedirects: 5, timeout: 10_000 }, + ], + }, + ] + + const result = compactProbes(probes) + expect(result).to.deep.equal(probes) + }) + }) + + describe('getDecompactedProbesById', () => { + it('should return original probe by id', () => { + const probes: Probe[] = [ + { + id: '1', + name: 'Test 1', + interval: 30, + alerts: [], + requests: [ + { + url: 'http://test.com', + followRedirects: 5, + timeout: 10_000, + }, + ], + }, + ] + + compactProbes(probes) // Sets decompactedProbes + const result = getDecompactedProbesById('1') + expect(result).to.deep.equal(probes[0]) + }) + + it('should return undefined for non-existent probe id', () => { + const probes: Probe[] = [] + compactProbes(probes) + const result = getDecompactedProbesById('nonexistent') + expect(result).to.be.undefined + }) + + it('should deduplicate identical alerts when merging probes', () => { + const probes: Probe[] = [ + { + id: '1', + name: 'Test 1', + interval: 30, + alerts: [], + requests: [ + { + url: 'http://test.com', + followRedirects: 5, + timeout: 10_000, + alerts: [ + { + id: 'alert1', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + { + id: 'alert2', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + ], + }, + ], + }, + ] + + const result = compactProbes(probes) + expect(result[0].requests?.[0].alerts).to.have.lengthOf(1) + expect(result[0].requests?.[0].alerts?.[0]).to.deep.equal({ + id: 'alert1', + assertion: 'response_time < 200', + message: 'Response too slow', + }) + }) + + it('should deduplicate alerts across multiple probes', () => { + const probes: Probe[] = [ + { + id: '1', + name: 'Test 1', + interval: 30, + alerts: [], + socket: { + host: 'test.com', + port: 80, + data: 'test', + alerts: [ + { + id: 'alert1', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + ], + }, + }, + { + id: '2', + name: 'Test 2', + interval: 30, + alerts: [], + socket: { + host: 'test.com', + port: 80, + data: 'test', + alerts: [ + { + id: 'alert2', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + ], + }, + }, + { + id: '3', + name: 'Test 3', + interval: 30, + alerts: [], + socket: { + host: 'test.com', + port: 80, + data: 'test', + alerts: [ + { + id: 'alert1', + assertion: 'response_time < 200', + message: 'Response too slow', + }, + ], + }, + }, + ] + + const result = compactProbes(probes) + expect(result).to.have.lengthOf(1) + expect(result[0].socket?.alerts).to.have.lengthOf(1) + }) + }) +}) From 2af9c90f12e10f61f98a93769e5d436000764a6a Mon Sep 17 00:00:00 2001 From: Mochamad Noor Syamsu Date: Sun, 15 Dec 2024 23:19:03 +0700 Subject: [PATCH 5/5] add documentation --- docs/src/pages/guides/cli-options.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/src/pages/guides/cli-options.md b/docs/src/pages/guides/cli-options.md index f4f0532f0..868104d3c 100644 --- a/docs/src/pages/guides/cli-options.md +++ b/docs/src/pages/guides/cli-options.md @@ -48,6 +48,14 @@ For example, assuming you have a file named `only-notif.yml` whose content `{"no monika -c foo-monitoring.yml only-notif.yml ``` +## Compact probes + +Given multiple probes with the identical payload request / socket / db connection configuration, Monika can optionally compact them for you. This aims to do the probing for said identical probes once. With retained alerts configuration. + +```bash +monika -c config-with-identical-probes.yml --compact-probes +``` + ## Auto-update Monika supports automatic update with `--auto-update major|minor|patch`. Where `major|minor|patch` refers to [semantic versioning (semver) specification](https://semver.org/). By default, the updater will check for a new Monika version every 24 hours.