fix: service usage screen

This fixes service monthly usage counts as shown in Settings.
This commit is contained in:
KernelDeimos
2024-10-28 18:47:40 -04:00
parent c0b109d4d2
commit 193da63304
7 changed files with 151 additions and 22 deletions

View File

@@ -331,6 +331,9 @@ const install = async ({ services, app, useapi, modapi }) => {
const { KernelInfoService } = require('./services/KernelInfoService');
services.registerService('kernel-info', KernelInfoService);
const { DriverUsagePolicyService } = require('./services/drivers/DriverUsagePolicyService');
services.registerService('driver-usage-policy', DriverUsagePolicyService);
}
const install_legacy = async ({ services }) => {

View File

@@ -25,6 +25,9 @@ const { CodeUtil } = require("../codex/CodeUtil");
/**
* Base class for all driver implementations.
*
* @deprecated - we use traits on services now. This class is kept for compatibility
* with EntityStoreImplementation and DBKVStore which still use this.
*/
class Driver extends AdvancedBase {
constructor (...a) {
@@ -169,6 +172,7 @@ class Driver extends AdvancedBase {
'driver.interface': this.constructor.INTERFACE,
'driver.implementation': this.constructor.ID,
'driver.method': method,
...(this.get_usage_extra ? this.get_usage_extra() : {}),
};
await svc_monthlyUsage.increment(actor, method_key, extra);
}

View File

@@ -90,6 +90,12 @@ class EntityStoreImplementation extends Driver {
super();
this.service = service;
}
get_usage_extra () {
return {
['driver.interface']: 'puter-es',
['driver.implementation']: 'puter-es:' + this.service,
};
}
static METHODS = {
create: async function ({ object, options }) {
const svc_es = this.services.get(this.service);

View File

@@ -62,29 +62,35 @@ module.exports = eggspress('/drivers/usage', {
const app = await get_app({ id: row.app_id });
const identifying_fields = {
service: row.extra,
service: JSON.parse(row.extra),
year: row.year,
month: row.month,
};
// EntityStorage identifiers weren't tracked properly. We don't realy need
// to track or show them, so this isn't a huge deal, but we need to make
// sure they don't populate garbage data into the usage report.
if ( ! identifying_fields.service['driver.implementation'] ) {
continue;
}
const svc_driverUsage = req.services.get('driver-usage-policy');
const policy = await svc_driverUsage.get_effective_policy({
actor,
service_name: identifying_fields.service['driver.implementation'],
trait_name: identifying_fields.service['driver.interface'],
});
// console.log(`POLICY FOR ${identifying_fields.service['driver.implementation']} ${identifying_fields.service['driver.interface']}`, policy);
const user_usage_key = hash_serializable_object(identifying_fields);
if ( ! usages.user[user_usage_key] ) {
usages.user[user_usage_key] = {
...identifying_fields,
policy,
};
const method_key = row.extra['driver.implementation'] +
':' + row.extra['driver.method'];
const sla_key = `driver:impl:${method_key}`;
const svc_sla = x.get('services').get('sla');
const sla = await svc_sla.get(
user_is_verified ? 'user_verified' : 'user_unverified',
sla_key
);
usages.user[user_usage_key].monthly_limit =
sla?.monthly_limit || null;
usages.user[user_usage_key].monthly_limit = policy?.['monthy-limit'] ?? null;
}
usages.user[user_usage_key].monthly_usage =
@@ -109,7 +115,8 @@ module.exports = eggspress('/drivers/usage', {
const app_usage_key = hash_serializable_object(id_plus_app);
if ( ! app_usages[app_usage_key] ) {
// DISABLED FOR NOW: need to rework this for the new policy system
if ( false ) if ( ! app_usages[app_usage_key] ) {
app_usages[app_usage_key] = {
...identifying_fields,
};

View File

@@ -132,6 +132,20 @@ class ConfigurableCountingService extends BaseService {
pricing_category: JSON.stringify(pricing_category),
};
const duplicate_update_part =
`count = count + 1${
custom_col_names.length > 0 ? ', ' : ''
} ${
custom_col_names.map((name) => `${name} = ${name} + ?`).join(', ')
}`;
const identifying_keys = [
`year`, `month`,
`service_type`, `service_name`,
`actor_key`,
`pricing_category_hash`
]
const sql =
`INSERT INTO monthly_usage_counts (${
Object.keys(required_data).join(', ')
@@ -141,11 +155,13 @@ class ConfigurableCountingService extends BaseService {
`VALUES (${
Object.keys(required_data).map(() => '?').join(', ')
}, 1, ${custom_col_values.map(() => '?').join(', ')}) ` +
`ON DUPLICATE KEY UPDATE count = count + 1${
custom_col_names.length > 0 ? ', ' : ''
} ${
custom_col_names.map((name) => `${name} = ${name} + ?`).join(', ')
}`;
this.db.case({
mysql: 'ON DUIPLICATE KEY UPDATE ' + duplicate_update_part,
sqlite: `ON CONFLICT(${
identifying_keys.map(v => `\`${v}\``).join(', ')
}) DO UPDATE SET ${duplicate_update_part}`,
})
;
const value_array = [
...Object.values(required_data),

View File

@@ -0,0 +1,88 @@
const APIError = require("../../api/APIError");
const { PermissionUtil } = require("../auth/PermissionService");
const BaseService = require("../BaseService");
// DO WE HAVE enough information to get the policy for the newer drivers?
// - looks like it: service:<name of service>:<name of trait>
class DriverUsagePolicyService extends BaseService {
async get_policies_for_option_ (option) {
// NOT FINAL: before implementing cascading monthly usage,
// this return will be removed and the code below it will
// be uncommented
return option.path;
/*
const svc_systemData = this.services.get('system-data');
const svc_su = this.services.get('su');
const policies = await Promise.all(option.path.map(async path_node => {
const policy = await svc_su.sudo(async () => {
return await svc_systemData.interpret(option.data);
});
return {
...path_node,
policy,
};
}));
return policies;
*/
}
async select_best_option_ (options) {
return options[0];
}
// TODO: DRY: This is identical to the method of the same name in
// DriverService, except after the line with a comment containing
// the string "[DEVIATION]".
async get_effective_policy ({ actor, service_name, trait_name }) {
const svc_permission = this.services.get('permission');
const reading = await svc_permission.scan(
actor,
PermissionUtil.join('service', service_name, 'ii', trait_name),
);
console.log({
perm: PermissionUtil.join('service', service_name, 'ii', trait_name),
reading: require('util').inspect(reading, { depth: null }),
});
const options = PermissionUtil.reading_to_options(reading);
console.log('OPTIONS', JSON.stringify(options, undefined, ' '));
if ( options.length <= 0 ) {
return undefined;
}
const option = await this.select_best_option_(options);
const policies = await this.get_policies_for_option_(option);
console.log('SLA', JSON.stringify(policies, undefined, ' '));
// NOT FINAL: For now we apply monthly usage logic
// to the first holder of the permission. Later this
// will be changed so monthly usage can cascade across
// multiple actors. I decided not to implement this
// immediately because it's a hefty time sink and it's
// going to be some time before we can offer this feature
// to the end-user either way.
let effective_policy = null;
for ( const policy of policies ) {
if ( policy.holder ) {
effective_policy = policy;
break;
}
}
// === [DEVIATION] In DriverService, this is part of call_new_ ===
const svc_systemData = this.services.get('system-data');
const svc_su = this.services.get('su');
effective_policy = await svc_su.sudo(async () => {
return await svc_systemData.interpret(effective_policy.data);
});
effective_policy = effective_policy.policy;
return effective_policy;
}
}
module.exports = {
DriverUsagePolicyService,
};

View File

@@ -62,12 +62,17 @@ export default {
const { monthly_limit, monthly_usage } = service;
let usageDisplay = ``;
const first_identifier = false ||
service.service['driver.implementation'] ||
service.service['driver.interface'] ||
'';
if (monthly_limit !== null) {
let usage_percentage = (monthly_usage / monthly_limit * 100).toFixed(0);
usage_percentage = usage_percentage > 100 ? 100 : usage_percentage; // Cap at 100%
usageDisplay = `
<div class="driver-usage" style="margin-bottom: 10px;">
<h3 style="margin-bottom: 5px; font-size: 14px;">${html_encode(service.service['driver.interface'])} (${html_encode(service.service['driver.method'])}):</h3>
<h3 style="margin-bottom: 5px; font-size: 14px;">${html_encode(first_identifier)} (${html_encode(service.service['driver.method'])}):</h3>
<span style="font-size: 13px; margin-bottom: 3px;">${monthly_usage} used of ${monthly_limit}</span>
<div class="usage-progbar-wrapper" style="width: 100%;">
<div class="usage-progbar" style="width: ${usage_percentage}%;"><span class="usage-progbar-percent">${usage_percentage}%</span></div>
@@ -78,7 +83,7 @@ export default {
else {
usageDisplay = `
<div class="driver-usage" style="margin-bottom: 10px;">
<h3 style="margin-bottom: 5px; font-size: 14px;">${html_encode(service.service['driver.interface'])} (${html_encode(service.service['driver.method'])}):</h3>
<h3 style="margin-bottom: 5px; font-size: 14px;">${html_encode(first_identifier)} (${html_encode(service.service['driver.method'])}):</h3>
<span style="font-size: 13px; margin-bottom: 3px;">${i18n('usage')}: ${monthly_usage} (${i18n('unlimited')})</span>
</div>
`;