Path: blob/master/src/packages/conat/monitor/usage.ts
1710 views
import { EventEmitter } from "events";1import json from "json-stable-stringify";23import { getLogger } from "@cocalc/conat/client";4import { ConatError } from "@cocalc/conat/core/client";5import type { JSONValue } from "@cocalc/util/types";6import { Metrics } from "../types";78const logger = getLogger("monitor:usage");910interface Options {11resource: string;12maxPerUser?: number;13max?: number;14log?: (...args) => void;15}1617export class UsageMonitor extends EventEmitter {18private options: Options;19private total = 0;20private perUser: { [user: string]: number } = {};21// metrics will be picked up periodically and exposed via e.g. prometheus22private countDeny = 0;23private metrics: Metrics = {};2425constructor(options: Options) {26super();27this.options = options;28logger.debug("creating usage monitor", this.options);29this.initLogging();30}3132stats = () => {33return { total: this.total, perUser: this.perUser };34};3536close = () => {37this.removeAllListeners();38this.perUser = {};39};4041private toJson = (user: JSONValue) => json(user) ?? "";4243private initLogging = () => {44const { log } = this.options;4546// Record metrics for all events (even if logging is disabled)47this.on("total", (total, limit) => {48this.metrics["total:count"] = total;49this.metrics["total:limit"] = limit;50if (log) {51log("usage", this.options.resource, { total, limit });52}53});54this.on("add", (user, count, limit) => {55// this.metrics["add:count"] = count;56// this.metrics["add:limit"] = limit;57if (log) {58log("usage", this.options.resource, "add", { user, count, limit });59}60});61this.on("delete", (user, count, limit) => {62// this.metrics["delete:count"] = count;63// this.metrics["delete:limit"] = limit;64if (log) {65log("usage", this.options.resource, "delete", { user, count, limit });66}67});68this.on("deny", (user, limit, type) => {69this.countDeny += 1;70this.metrics["deny:count"] = this.countDeny;71this.metrics["deny:limit"] = limit;72if (log) {73log(74"usage",75this.options.resource,76"not allowed due to hitting limit",77{78type,79user,80limit,81},82);83}84});85};8687// we return a copy88getMetrics = () => {89return { ...this.metrics };90};9192add = (user: JSONValue) => {93const u = this.toJson(user);94let count = this.perUser[u] ?? 0;95if (this.options.max && this.total >= this.options.max) {96this.emit("deny", user, this.options.max, "global");97throw new ConatError(98`There is a global limit of ${this.options.max} ${this.options.resource}. Please close browser tabs or files or come back later.`,99// http error code "429 Too Many Requests."100{ code: 429 },101);102}103if (this.options.maxPerUser && count >= this.options.maxPerUser) {104this.emit("deny", this.options.maxPerUser, "per-user");105throw new ConatError(106`There is a per user limit of ${this.options.maxPerUser} ${this.options.resource}. Please close browser tabs or files or come back later.`,107// http error code "429 Too Many Requests."108{ code: 429 },109);110}111this.total += 1;112count++;113this.perUser[u] = count;114this.emit("total", this.total, this.options.max);115this.emit("add", user, count, this.options.maxPerUser);116};117118delete = (user: JSONValue) => {119this.total -= 1;120const u = this.toJson(user);121let count = (this.perUser[u] ?? 0) - 1;122if (count <= 0) {123delete this.perUser[u];124} else {125this.perUser[u] = count;126}127this.emit("total", this.total);128this.emit("delete", user, count);129};130}131132133