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