MNTOR-2431 - create alert for scan and profile limits (#3779)

* MNTOR-2431 - create alert for scan and profile limits
This commit is contained in:
Robert Helmer 2024-01-05 08:02:46 -08:00 коммит произвёл GitHub
Родитель a601fb59e2
Коммит 6d53d6b144
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 155 добавлений и 2 удалений

Просмотреть файл

@ -166,3 +166,9 @@ NIMBUS_SIDECAR_URL=http://localhost:8001
# The maximum number of jobs that the email breach alert worker will process.
EMAIL_BREACH_ALERT_MAX_MESSAGES = 10000
# The maximum number of scans and profiles allowed. May be used for alerts, and for redirecting to waitlist.
MAX_MANUAL_SCANS=100
MAX_INITIAL_SCANS=100
MAX_PROFILES_ACTIVATED=100
MAX_PROFILES_CREATED=100

Просмотреть файл

@ -108,7 +108,7 @@ async function onerepFetch(
}
const onerepApiKey = process.env.ONEREP_API_KEY;
if (!onerepApiKey) {
throw new Error("ONEREP_API_BASE env var not set");
throw new Error("ONEREP_API_KEY env var not set");
}
const url = new URL(path, onerepApiBase);
const headers = new Headers(options.headers);

Просмотреть файл

@ -17,7 +17,7 @@
table.timestamp('deleted_at')
table.string('owner')
})
}
export function down (knex) {

Просмотреть файл

@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export async function up(knex) {
return knex.schema
.createTable("stats", table => {
table.increments('id').primary()
table.string("name")
table.string("current")
table.string("max")
table.string("type")
table.timestamp("created_at").defaultTo(knex.fn.now())
table.timestamp("modified_at").defaultTo(knex.fn.now())
})
}
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function down(knex) {
return knex.schema
.dropTableIfExists("stats")
}

32
src/db/tables/stats.js Normal file
Просмотреть файл

@ -0,0 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import createDbConnection from "../connect.js";
const knex = createDbConnection();
export { knex as knexStats }
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
/* c8 ignore start */
/**
* @param {string} name
* @param {string} current
* @param {string} max
* @returns {Promise<import('knex/types/tables').SubscriberRow | null>} updated subscriber
*/
export async function addOnerepStats (name, current, max) {
const res = await knex("stats").insert({ name, current, max, type: "onerep"}).returning("*");
return res[0];
}
/**
* @returns {object}
*/
export async function getOnerepStats () {
const res = await knex("stats").select("name", "current", "max").where("type", "onerep");
return res[0];
}
/* c8 ignore stop */

6
src/knex-tables.d.ts поставляемый
Просмотреть файл

@ -370,4 +370,10 @@ declare module "knex/types/tables" {
Pick<EmailNotificationRow, "updated_at">
>;
}
interface StatsRow {
name: string;
current: string;
max: string;
type: string;
}
}

Просмотреть файл

@ -0,0 +1,84 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Sentry from "@sentry/nextjs";
import { addOnerepStats, knexStats } from "../db/tables/stats.js";
const SENTRY_SLUG = "cron-onerep-stats-alerts";
const MAX_MANUAL_SCANS = parseInt(process.env.MAX_MANUAL_SCANS) || 0;
const MAX_INITIAL_SCANS = parseInt(process.env.MAX_INITIAL_SCANS) || 0;
const MAX_PROFILES_ACTIVATED =
parseInt(process.env.MAX_PROFILES_ACTIVATED) || 0;
const MAX_PROFILES_CREATED = parseInt(process.env.MAX_PROFILES_CREATED) || 0;
Sentry.init({
environment: process.env.APP_ENV,
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
});
const checkInId = Sentry.captureCheckIn({
monitorSlug: SENTRY_SLUG,
status: "in_progress",
});
/**
* Fetch the latest usage statistics from OneRep's API.
*
* @see https://docs.onerep.com/#tag/Statistics
*/
export async function checkStats() {
const scans = await (await onerepFetch("/stats/scans")).json();
const profiles = await (await onerepFetch("/stats/profiles")).json();
for (const alert of [
["free_scans", scans.manual, MAX_MANUAL_SCANS],
["paid_scans", scans.initial, MAX_INITIAL_SCANS],
["profiles_activated", profiles.activated, MAX_PROFILES_ACTIVATED],
["profiles_created", profiles.created, MAX_PROFILES_CREATED],
]) {
const [name, current, max] = alert;
await addOnerepStats(name, current, max);
if (current >= max) {
const msg = `Alert: OneRep scans over limit for ${name}. Current: ${current}, max: ${max}`;
console.error(msg);
Sentry.captureMessage(msg);
}
}
}
// TODO use the shared version when this is converted to Typescript.
async function onerepFetch(path, options = {}) {
const onerepApiBase = process.env.ONEREP_API_BASE;
if (!onerepApiBase) {
throw new Error("ONEREP_API_BASE env var not set");
}
const onerepApiKey = process.env.ONEREP_API_KEY;
if (!onerepApiKey) {
throw new Error("ONEREP_API_KEY env var not set");
}
const url = new URL(path, onerepApiBase);
const headers = new Headers(options?.headers);
headers.set("Authorization", `Bearer ${onerepApiKey}`);
headers.set("Accept", "application/json");
headers.set("Content-Type", "application/json");
return fetch(url, { ...options, headers });
}
checkStats()
.then(async (_) => {
Sentry.captureCheckIn({
checkInId,
monitorSlug: SENTRY_SLUG,
status: "ok",
});
knexStats.destroy();
})
.catch((err) => {
console.error(err);
Sentry.captureException(err);
});