CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/database/postgres/site-license/analytics.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { PostgreSQL } from "../types";
7
import { callback2 } from "@cocalc/util/async-utils";
8
import { copy_with, isValidUUID, len } from "@cocalc/util/misc";
9
const TIMEOUT_S = 30;
10
11
export function numberRunningQuery(license_id: string): string {
12
if (!isValidUUID(license_id)) {
13
// critical to check to avoid any possible SQL injection attack.
14
throw Error("invalid license_id");
15
}
16
// "... - 'status'" in the query, because there is always a status field (which is new)
17
// an applied license not providing upgrades is just an empty object.
18
return `
19
SELECT COUNT(*)::INT
20
FROM projects
21
WHERE state ->> 'state' = 'running'
22
AND ((site_license -> '${license_id}') - 'status') != '{}'::JSONB`;
23
}
24
25
export async function number_of_running_projects_using_license(
26
db: PostgreSQL,
27
license_id: string
28
): Promise<number> {
29
/* Do a query to count the number of projects that:
30
(1) are running,
31
(2) have the given license_id has a key in their site_license field with a nontrivial value.
32
(3) we have to ignore the "status" field, which is only information but not providing upgrades.
33
*/
34
const query = numberRunningQuery(license_id);
35
const x = await db.async_query({ query, timeout_s: TIMEOUT_S });
36
return parseInt(x.rows[0].count);
37
}
38
39
export async function number_of_projects_with_license_applied(
40
db: PostgreSQL,
41
license_id: string
42
): Promise<number> {
43
/* Do a query to count the number of projects that have the given license_id has a key in their
44
site_license field possibly with a trivial value. Basically, this is the number of projects
45
somebody has set to use this license, whether or not they have successfully actually used it.
46
47
select project_id, site_license, state from projects where state#>>'{state}' in ('running', 'starting') and site_license#>>'{f3942ea1-ff3f-4d9f-937a-c5007babc693}' IS NOT NULL;
48
*/
49
50
const query = `SELECT COUNT(*) FROM projects WHERE site_license#>>'{${license_id}}' IS NOT NULL`;
51
const x = await db.async_query({ query, timeout_s: TIMEOUT_S });
52
return parseInt(x.rows[0].count);
53
}
54
55
/* Returns information about how licenses are being used across ALL running projects
56
in the system right now.
57
58
The following query excludes anything with site_license null or {}, due to how sql works:
59
60
select site_license from projects where state#>>'{state}' in ('running', 'starting') and site_license!='{}';
61
62
We then just process the result in Javascript. It would be possible to make a more complicated query that
63
does all the processing in the database, and returns less data as output, but that would be harder for me,
64
so I leave that to others or later (since this query is not likely to be used too much).
65
*/
66
export async function site_license_usage_stats(
67
db: PostgreSQL
68
): Promise<{ [license_id: string]: number }> {
69
const query =
70
"select site_license from projects where state#>>'{state}' in ('running', 'starting') and site_license!='{}'";
71
const result = await db.async_query({ query });
72
const usage: { [license_id: string]: number } = {};
73
for (let row of result.rows) {
74
for (const license_id in row.site_license) {
75
if (len(row.site_license[license_id]) > 0) {
76
if (usage[license_id] == null) {
77
usage[license_id] = 1;
78
} else {
79
usage[license_id] += 1;
80
}
81
}
82
}
83
}
84
return usage;
85
}
86
87
export function query_projects_using_site_license(
88
license_id: string,
89
cutoff?: Date
90
): { query: string; params: any[] } {
91
const params: any[] = [];
92
let query: string;
93
if (cutoff) {
94
query = `FROM projects, site_license_usage_log WHERE `;
95
query += "projects.project_id = site_license_usage_log.project_id AND ";
96
query += "site_license_usage_log.license_id = $1 AND";
97
query += "(site_license_usage_log.start >= $2 OR ";
98
query += " site_license_usage_log.stop >= $2 OR ";
99
query += " site_license_usage_log.stop IS NULL)";
100
params.push(license_id);
101
params.push(cutoff);
102
} else {
103
// easier -- just directly query the projects table.
104
query = `
105
FROM projects
106
WHERE state#>>'{state}' IN ('running', 'starting')
107
AND ((site_license -> '${license_id}') - 'status') != '{}'::JSONB`;
108
}
109
return { query, params };
110
}
111
112
export async function projects_using_site_license(
113
db: PostgreSQL,
114
opts: {
115
license_id: string;
116
fields: string[]; // assumed sanitized by caller!
117
cutoff?: Date;
118
limit?: number;
119
truncate?: number;
120
}
121
): Promise<{ [field: string]: any }[]> {
122
const query_fields = process_fields(opts.fields, opts.truncate);
123
124
const { query, params } = query_projects_using_site_license(
125
opts.license_id,
126
opts.cutoff
127
);
128
const select = `SELECT ${query_fields.join(",")} `;
129
const x = await callback2(db._query.bind(db), {
130
query: select + " " + query,
131
limit: opts.limit,
132
params,
133
});
134
const v: { [field: string]: any }[] = [];
135
for (const row of x.rows) {
136
v.push(copy_with(row, opts.fields));
137
}
138
return v;
139
}
140
141
function process_fields(
142
fields: string[],
143
truncate: number | undefined
144
): string[] {
145
const v: string[] = [];
146
for (let field of fields) {
147
if (truncate && (field == "title" || field == "description")) {
148
field = `left(projects.${field},${truncate}) as ${field}`;
149
} else if (field == "project_id") {
150
field = `distinct(projects.project_id)`;
151
} else {
152
field = `projects.${field}`;
153
}
154
v.push(field);
155
}
156
return v;
157
}
158
159
export async function number_of_projects_using_site_license(
160
db: PostgreSQL,
161
opts: {
162
license_id: string;
163
cutoff?: Date;
164
}
165
): Promise<number> {
166
const { query, params } = query_projects_using_site_license(
167
opts.license_id,
168
opts.cutoff
169
);
170
171
const x = await db.async_query({
172
query: "SELECT COUNT(DISTINCT(projects.project_id))::INT " + query,
173
params,
174
timeout_s: TIMEOUT_S,
175
});
176
return parseInt(x.rows[0].count);
177
}
178
179