React UI: Added logging (#1288)
parent
731b8967c0
commit
bfd300039e
@ -0,0 +1,209 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
const PluginRegistry = require('./plugins');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
const { LogType } = require('./enums');
|
||||
|
||||
/**
|
||||
* Class representing a single log
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Log {
|
||||
constructor(logType, payload) {
|
||||
this.onCloseCallback = null;
|
||||
|
||||
this.type = logType;
|
||||
this.payload = { ...payload };
|
||||
this.time = new Date();
|
||||
}
|
||||
|
||||
onClose(callback) {
|
||||
this.onCloseCallback = callback;
|
||||
}
|
||||
|
||||
validatePayload() {
|
||||
if (typeof (this.payload) !== 'object') {
|
||||
throw new ArgumentError('Payload must be an object');
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.stringify(this.payload);
|
||||
} catch (error) {
|
||||
const message = `Log payload must be JSON serializable. ${error.toString()}`;
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
}
|
||||
|
||||
dump() {
|
||||
const payload = { ...this.payload };
|
||||
const body = {
|
||||
name: this.type,
|
||||
time: this.time.toISOString(),
|
||||
};
|
||||
|
||||
for (const field of ['client_id', 'job_id', 'task_id', 'is_active']) {
|
||||
if (field in payload) {
|
||||
body[field] = payload[field];
|
||||
delete payload[field];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...body,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Method saves a durable log in a storage <br>
|
||||
* Note then you can call close() multiple times <br>
|
||||
* Log duration will be computed based on the latest call <br>
|
||||
* All payloads will be shallowly combined (all top level properties will exist)
|
||||
* @method close
|
||||
* @memberof module:API.cvat.classes.Log
|
||||
* @param {object} [payload] part of payload can be added when close a log
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
async close(payload = {}) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, Log.prototype.close, payload);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Log.prototype.close.implementation = function (payload) {
|
||||
this.payload.duration = Date.now() - this.time.getTime();
|
||||
this.payload = { ...this.payload, ...payload };
|
||||
|
||||
if (this.onCloseCallback) {
|
||||
this.onCloseCallback();
|
||||
}
|
||||
};
|
||||
|
||||
class LogWithCount extends Log {
|
||||
validatePayload() {
|
||||
Log.prototype.validatePayload.call(this);
|
||||
if (!Number.isInteger(this.payload.count) || this.payload.count < 1) {
|
||||
const message = `The field "count" is required for "${this.type}" log`
|
||||
+ 'It must be a positive integer';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LogWithObjectsInfo extends Log {
|
||||
validatePayload() {
|
||||
const generateError = (name, range) => {
|
||||
const message = `The field "${name}" is required for "${this.type}" log. ${range}`;
|
||||
throw new ArgumentError(message);
|
||||
};
|
||||
|
||||
if (!Number.isInteger(this.payload['track count']) || this.payload['track count'] < 0) {
|
||||
generateError('track count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['tag count']) || this.payload['tag count'] < 0) {
|
||||
generateError('tag count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['object count']) || this.payload['object count'] < 0) {
|
||||
generateError('object count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['frame count']) || this.payload['frame count'] < 1) {
|
||||
generateError('frame count', 'It must be an integer not less than 1');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['box count']) || this.payload['box count'] < 0) {
|
||||
generateError('box count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['polygon count']) || this.payload['polygon count'] < 0) {
|
||||
generateError('polygon count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['polyline count']) || this.payload['polyline count'] < 0) {
|
||||
generateError('polyline count', 'It must be an integer not less than 0');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(this.payload['points count']) || this.payload['points count'] < 0) {
|
||||
generateError('points count', 'It must be an integer not less than 0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LogWithWorkingTime extends Log {
|
||||
validatePayload() {
|
||||
Log.prototype.validatePayload.call(this);
|
||||
|
||||
if (!('working_time' in this.payload)
|
||||
|| !typeof (this.payload.working_time) === 'number'
|
||||
|| this.payload.working_time < 0
|
||||
) {
|
||||
const message = `The field "working_time" is required for ${this.type} log. `
|
||||
+ 'It must be a number not less than 0';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LogWithExceptionInfo extends Log {
|
||||
validatePayload() {
|
||||
Log.prototype.validatePayload.call(this);
|
||||
|
||||
if (typeof (this.payload.message) !== 'string') {
|
||||
const message = `The field "message" is required for ${this.type} log. `
|
||||
+ 'It must be a string';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
|
||||
if (typeof (this.payload.filename) !== 'string') {
|
||||
const message = `The field "filename" is required for ${this.type} log. `
|
||||
+ 'It must be a string';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
|
||||
if (typeof (this.payload.line) !== 'number') {
|
||||
const message = `The field "line" is required for ${this.type} log. `
|
||||
+ 'It must be a number';
|
||||
throw new ArgumentError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function logFactory(logType, payload) {
|
||||
const logsWithCount = [
|
||||
LogType.deleteObject, LogType.mergeObjects, LogType.copyObject,
|
||||
LogType.undoAction, LogType.redoAction,
|
||||
];
|
||||
|
||||
if (logsWithCount.includes(logType)) {
|
||||
return new LogWithCount(logType, payload);
|
||||
}
|
||||
if ([LogType.sendTaskInfo, LogType.loadJob, LogType.uploadAnnotations].includes(logType)) {
|
||||
return new LogWithObjectsInfo(logType, payload);
|
||||
}
|
||||
|
||||
if (logType === LogType.sendUserActivity) {
|
||||
return new LogWithWorkingTime(logType, payload);
|
||||
}
|
||||
|
||||
if (logType === LogType.sendException) {
|
||||
return new LogWithExceptionInfo(logType, payload);
|
||||
}
|
||||
|
||||
return new Log(logType, payload);
|
||||
}
|
||||
|
||||
module.exports = logFactory;
|
||||
@ -0,0 +1,169 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
const PluginRegistry = require('./plugins');
|
||||
const server = require('./server-proxy');
|
||||
const logFactory = require('./log');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
const { LogType } = require('./enums');
|
||||
|
||||
const WORKING_TIME_THRESHOLD = 100000; // ms, 1.66 min
|
||||
|
||||
class LoggerStorage {
|
||||
constructor() {
|
||||
this.clientID = Date.now().toString().substr(-6);
|
||||
this.lastLogTime = Date.now();
|
||||
this.workingTime = 0;
|
||||
this.collection = [];
|
||||
this.ignoreRules = {}; // by event
|
||||
this.isActiveChecker = null;
|
||||
|
||||
this.ignoreRules[LogType.zoomImage] = {
|
||||
lastLog: null,
|
||||
timeThreshold: 1000,
|
||||
ignore(previousLog) {
|
||||
return Date.now() - previousLog.time < this.timeThreshold;
|
||||
},
|
||||
};
|
||||
|
||||
this.ignoreRules[LogType.changeAttribute] = {
|
||||
lastLog: null,
|
||||
ignore(previousLog, currentPayload) {
|
||||
return currentPayload.object_id === previousLog.payload.object_id
|
||||
&& currentPayload.id === previousLog.payload.id;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
updateWorkingTime() {
|
||||
if (!this.isActiveChecker || this.isActiveChecker()) {
|
||||
const lastLogTime = Date.now();
|
||||
const diff = lastLogTime - this.lastLogTime;
|
||||
this.workingTime += diff < WORKING_TIME_THRESHOLD ? diff : 0;
|
||||
this.lastLogTime = lastLogTime;
|
||||
}
|
||||
}
|
||||
|
||||
async configure(isActiveChecker, activityHelper) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(
|
||||
this, LoggerStorage.prototype.configure,
|
||||
isActiveChecker, activityHelper,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
async log(logType, payload = {}, wait = false) {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait);
|
||||
return result;
|
||||
}
|
||||
|
||||
async save() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, LoggerStorage.prototype.save);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
LoggerStorage.prototype.configure.implementation = function (
|
||||
isActiveChecker,
|
||||
userActivityCallback,
|
||||
) {
|
||||
if (typeof (isActiveChecker) !== 'function') {
|
||||
throw new ArgumentError('isActiveChecker argument must be callable');
|
||||
}
|
||||
|
||||
if (!Array.isArray(userActivityCallback)) {
|
||||
throw new ArgumentError('userActivityCallback argument must be an array');
|
||||
}
|
||||
|
||||
this.isActiveChecker = () => !!isActiveChecker();
|
||||
userActivityCallback.push(this.updateWorkingTime.bind(this));
|
||||
};
|
||||
|
||||
LoggerStorage.prototype.log.implementation = function (logType, payload, wait) {
|
||||
if (typeof (payload) !== 'object') {
|
||||
throw new ArgumentError('Payload must be an object');
|
||||
}
|
||||
|
||||
if (typeof (wait) !== 'boolean') {
|
||||
throw new ArgumentError('Payload must be an object');
|
||||
}
|
||||
|
||||
if (logType in this.ignoreRules) {
|
||||
const ignoreRule = this.ignoreRules[logType];
|
||||
const { lastLog } = ignoreRule;
|
||||
if (lastLog && ignoreRule.ignore(lastLog, payload)) {
|
||||
lastLog.payload = {
|
||||
...lastLog.payload,
|
||||
...payload,
|
||||
};
|
||||
|
||||
this.updateWorkingTime();
|
||||
return ignoreRule.lastLog;
|
||||
}
|
||||
}
|
||||
|
||||
const logPayload = { ...payload };
|
||||
logPayload.client_id = this.clientID;
|
||||
if (this.isActiveChecker) {
|
||||
logPayload.is_active = this.isActiveChecker();
|
||||
}
|
||||
|
||||
const log = logFactory(logType, { ...logPayload });
|
||||
if (logType in this.ignoreRules) {
|
||||
this.ignoreRules[logType].lastLog = log;
|
||||
}
|
||||
|
||||
const pushEvent = () => {
|
||||
this.updateWorkingTime();
|
||||
log.validatePayload();
|
||||
log.onClose(null);
|
||||
this.collection.push(log);
|
||||
};
|
||||
|
||||
if (wait) {
|
||||
log.onClose(pushEvent);
|
||||
} else {
|
||||
pushEvent();
|
||||
}
|
||||
|
||||
return log;
|
||||
};
|
||||
|
||||
LoggerStorage.prototype.save.implementation = async function () {
|
||||
const collectionToSend = [...this.collection];
|
||||
const lastLog = this.collection[this.collection.length - 1];
|
||||
|
||||
const logPayload = {};
|
||||
logPayload.client_id = this.clientID;
|
||||
logPayload.working_time = this.workingTime;
|
||||
if (this.isActiveChecker) {
|
||||
logPayload.is_active = this.isActiveChecker();
|
||||
}
|
||||
|
||||
if (lastLog && lastLog.type === LogType.sendTaskInfo) {
|
||||
logPayload.job_id = lastLog.payload.job_id;
|
||||
logPayload.task_id = lastLog.payload.task_id;
|
||||
}
|
||||
|
||||
const userActivityLog = logFactory(LogType.sendUserActivity, logPayload);
|
||||
collectionToSend.push(userActivityLog);
|
||||
|
||||
await server.logs.save(collectionToSend.map((log) => log.dump()));
|
||||
|
||||
for (const rule of Object.values(this.ignoreRules)) {
|
||||
rule.lastLog = null;
|
||||
}
|
||||
this.collection = [];
|
||||
this.workingTime = 0;
|
||||
this.lastLogTime = Date.now();
|
||||
};
|
||||
|
||||
module.exports = new LoggerStorage();
|
||||
@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const PluginRegistry = require('./plugins');
|
||||
|
||||
/**
|
||||
* Class describe scheme of a log object
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Log {
|
||||
constructor(logType, continuous, details) {
|
||||
this.type = logType;
|
||||
this.continuous = continuous;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method closes a continue log
|
||||
* @method close
|
||||
* @memberof module:API.cvat.classes.Log
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async close() {
|
||||
const result = await PluginRegistry
|
||||
.apiWrapper.call(this, Log.prototype.close);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Log;
|
||||
})();
|
||||
@ -0,0 +1,14 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import getCore from 'cvat-core';
|
||||
|
||||
const core = getCore();
|
||||
const { logger } = core;
|
||||
const { LogType } = core.enums;
|
||||
|
||||
export default logger;
|
||||
export {
|
||||
LogType,
|
||||
};
|
||||
Loading…
Reference in New Issue