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