diff --git a/app/api/logs.py b/app/api/logs.py index 360f4f14..07103730 100644 --- a/app/api/logs.py +++ b/app/api/logs.py @@ -1,7 +1,7 @@ -from datetime import datetime +from datetime import datetime from app.root_logger import get_root_logger from ast import literal_eval - +import inspect from flask import Blueprint, request from app.lti_session_passback.auth_checkers import check_admin @@ -11,7 +11,6 @@ logger = get_root_logger() - @api_logs.route('/api/logs/', methods=['GET']) def get_logs() -> (dict, int): """ @@ -26,13 +25,15 @@ def get_logs() -> (dict, int): try: limit = request.args.get('limit', default=None, type=int) except Exception as e: - logger.info('Limit value {} is invalid.\n{}'.format(request.args.get('limit'), e)) + logger.info('Limit value {} is invalid.\n{}'.format( + request.args.get('limit'), e)) limit = None try: offset = request.args.get('offset', default=None, type=int) except Exception as e: - logger.info('Offset value {} is invalid.\n{}'.format(request.args.get('offset', default=None), e)) + logger.info('Offset value {} is invalid.\n{}'.format( + request.args.get('offset', default=None), e)) offset = None raw_filters = request.args.get('filter', default=None) @@ -42,7 +43,8 @@ def get_logs() -> (dict, int): if not isinstance(filters, dict): filters = None except Exception as e: - logger.info('Filter value {} is invalid.\n{}'.format(raw_filters, e)) + logger.info( + 'Filter value {} is invalid.\n{}'.format(raw_filters, e)) filters = None else: filters = raw_filters @@ -52,18 +54,22 @@ def get_logs() -> (dict, int): try: ordering = literal_eval(raw_ordering) if not isinstance(ordering, list) or not all(map(lambda x: x[1] in [-1, 1], ordering)): - logger.info('Ordering value {} is invalid.'.format(raw_ordering)) + logger.info( + 'Ordering value {} is invalid.'.format(raw_ordering)) ordering = None except Exception as e: - logger.info('Ordering value {} is invalid.\n{}'.format(request.args.get('ordering', default=None), e)) + logger.info('Ordering value {} is invalid.\n{}'.format( + request.args.get('ordering', default=None), e)) ordering = None else: ordering = raw_ordering try: - logs = LogsDBManager().get_logs_filtered(filters=filters, limit=limit, offset=offset, ordering=ordering) + logs = LogsDBManager().get_logs_filtered( + filters=filters, limit=limit, offset=offset, ordering=ordering) except Exception as e: - message = 'Incorrect get_logs_filtered execution, {}: {}.'.format(e.__class__, e) + message = 'Incorrect get_logs_filtered execution, {}: {}.'.format( + e.__class__, e) logger.warning(message) return {'message': message}, 404 @@ -84,3 +90,59 @@ def get_logs() -> (dict, int): logs_json['logs'][str(_id)] = current_log_json logs_json['message'] = 'OK' return logs_json, 200 + + +@api_logs.route('/logs', methods=['POST']) +def create_log(): + """ + Endpoint to receive client logs. + Expected JSON: + { + "timestamp": "...", + "message": "...", + "trainingId": "..." + } + """ + # logger.info("Received client log") + frame = inspect.currentframe() # кадр + caller = frame.f_back # кадр вызывающей функции + pathname = caller.f_code.co_filename # путь к файлу вызвывающей функции + filename = pathname.split('/')[-1] # имя файла + funcName = caller.f_code.co_name # имя функции + lineno = caller.f_lineno # номер строки + try: + data = request.get_json(force=True) + if not data: + return {"message": "Invalid json"}, 400 + + timestamp = data.get("timestamp") + message = data.get("message") + training_id = data.get("trainingId") + + if message is None: + return {"message": "message field is required"}, 400 + + if timestamp: + timestamp = datetime.fromisoformat( + timestamp.replace("Z", "+00:00")) + else: + timestamp = datetime.now() + + LogsDBManager().add_log( + timestamp=timestamp, + serviceName="client", + levelname="INFO", + levelno=20, + message=message, + pathname=pathname, + filename=filename, + funcName=funcName, + lineno=lineno, + trainingId=training_id, + ) + + return {"message": "log received"}, 201 + + except Exception as e: + logger.warning(f"Client log creation failed: {e}") + return {"message": "Internal server error"}, 500 diff --git a/app/mongo_models.py b/app/mongo_models.py index b40d4e7b..53aea86e 100644 --- a/app/mongo_models.py +++ b/app/mongo_models.py @@ -156,6 +156,7 @@ class Logs(MongoModel): filename = fields.CharField() funcName = fields.CharField() lineno = fields.IntegerField() + trainingId = fields.ObjectIdField(blank=True, required=False) class StorageMeta(MongoModel): used_size = fields.IntegerField() \ No newline at end of file diff --git a/app/mongo_odm.py b/app/mongo_odm.py index d61a6a53..3254ed24 100644 --- a/app/mongo_odm.py +++ b/app/mongo_odm.py @@ -835,7 +835,7 @@ def __new__(cls): cls.init_done = True return cls.instance - def add_log(self, timestamp, serviceName, levelname, levelno, message, pathname, filename, funcName, lineno): + def add_log(self, timestamp, serviceName, levelname, levelno, message, pathname, filename, funcName, lineno, trainingId=None): return Logs( timestamp=timestamp, serviceName=serviceName, @@ -846,6 +846,7 @@ def add_log(self, timestamp, serviceName, levelname, levelno, message, pathname, filename=filename, funcName=funcName, lineno=lineno, + trainingId=ObjectId(trainingId) if trainingId else None, ).save() def get_logs_filtered(self, filters=None, limit=None, offset=None, ordering=None): diff --git a/app/static/js/logWrapper.js b/app/static/js/logWrapper.js new file mode 100644 index 00000000..18ea7fef --- /dev/null +++ b/app/static/js/logWrapper.js @@ -0,0 +1,35 @@ +function getTrainingIdFromUrl() { + // попытка URL с id: /trainings//, /trainings/statistics// + const match = window.location.pathname.match(/\/trainings(?:\/statistics)?\/([0-9a-fA-F]{24})\//); + return match ? match[1] : null; +} + +(function () { + + // const trainingId = + // window.APP_CONTEXT?.trainingId ?? null; + + class Logger { + + log(...args) { + + const trainingId = getTrainingIdFromUrl(); + + console.log(...args); + + fetch('/logs', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + timestamp: new Date().toISOString(), + message: args.join(' '), + trainingId: trainingId + }) + }); + } + } + + window.logger = new Logger(); +})(); \ No newline at end of file diff --git a/app/static/js/recording.js b/app/static/js/recording.js index e22200ac..eee31620 100644 --- a/app/static/js/recording.js +++ b/app/static/js/recording.js @@ -6,9 +6,9 @@ let gumStream, timer; function startRecording() { - console.log("call startRecording(). Try to call navigator.mediaDevices.getUserMedia"); - console.log("navigator", navigator); - console.log("navigator.mediaDevices", navigator.mediaDevices); + logger.log("call startRecording(). Try to call navigator.mediaDevices.getUserMedia"); + logger.log("navigator", navigator); + logger.log("navigator.mediaDevices", navigator.mediaDevices); $("#alert").hide() $("#record-contain").show(); navigator.mediaDevices.getUserMedia({audio: true, video: false}).then(function (stream) { @@ -82,8 +82,8 @@ function startRecording() { $("#record")[0].disabled = true; $("#done")[0].disabled = false; }).catch( err => { - console.log('Error on calling avigator.mediaDevices.getUserMedia') - console.log(err) + logger.log('Error on calling avigator.mediaDevices.getUserMedia') + logger.log(err) $("#alert").show(); $("#error-text").html("Микрофон не доступен!"); }); diff --git a/app/static/js/show_all_trainings.js b/app/static/js/show_all_trainings.js index 2610cd99..fdecc641 100644 --- a/app/static/js/show_all_trainings.js +++ b/app/static/js/show_all_trainings.js @@ -161,7 +161,7 @@ function buildAllTrainingsTable(trainingsJson) { allTrainingsTable.appendChild(currentTrainingRowElement); }); }) - .catch(err => console.log(err)); + .catch(err => logger.log(err)); } const REF_PAGE_COUNT = document.getElementById('ref-page-count'); diff --git a/app/static/js/show_all_trainings_filters.js b/app/static/js/show_all_trainings_filters.js index 4a73579f..4398c66c 100644 --- a/app/static/js/show_all_trainings_filters.js +++ b/app/static/js/show_all_trainings_filters.js @@ -578,7 +578,7 @@ function createFilter(filterCode, initialValues = "") { */ function removeAllFiltersUI() { for (const key of Object.keys(currentFilters)) { - // console.log($("#" + key + "-filter")) + // logger.log($("#" + key + "-filter")) $("#" + key + "-filter").remove(); } } diff --git a/app/static/js/training.js b/app/static/js/training.js index c6cff383..e1467c9c 100644 --- a/app/static/js/training.js +++ b/app/static/js/training.js @@ -8,6 +8,24 @@ let pdfDoc, trainingId, currentPage; +function waitForLogger(callback, timeout = 3000) { + const start = Date.now(); + + (function check() { + if (typeof window.logger === 'object' && typeof window.logger.log === 'function') { + callback(); + return; + } + + if (Date.now() - start > timeout) { + console.error('Logger not loaded'); + return; + } + + setTimeout(check, 50); + })(); +} + function renderPage(num) { pageRendering = true; pdfDoc.getPage(num).then(function(page) { @@ -52,6 +70,7 @@ function onNextPage() { return; } pageNum++; + waitForLogger(() => logger.log('[TRAINING] slide_changed', `to=${pageNum}`)); callShowPage(); queueRenderPage(pageNum); } @@ -64,12 +83,20 @@ function setPage(pageNum){ return; } currentPage = pageNum; + waitForLogger(() => logger.log('[TRAINING] slide_set', `to=${pageNum}`)); changeURLByParam('page', currentPage); queueRenderPage(pageNum); } function setupPresentationViewer(trainingId_) { trainingId = trainingId_; + + // logger.setTrainingId(trainingId_); + waitForLogger(() => { + logger.log('[TRAINING] presentation_viewer_initialized'); + }); + + let loadingTask = pdfjsLib.getDocument(`/api/files/presentations/by-training/${trainingId_}/`); loadingTask.promise.then(function(pdfDoc_) { pdfDoc = pdfDoc_; @@ -87,6 +114,9 @@ $(document).ready(function() { scale = 1.1; canvas = $("#the-canvas")[0]; ctx = canvas.getContext("2d"); - $("#done").click(callShowPage); + $("#done").click(function () { + waitForLogger(() => logger.log('[TRAINING] done_clicked')); + callShowPage(); + }); $("#next").click(onNextPage); }); diff --git a/app/static/js/training_statistics.js b/app/static/js/training_statistics.js index ab7f2824..d1a718e6 100644 --- a/app/static/js/training_statistics.js +++ b/app/static/js/training_statistics.js @@ -58,7 +58,7 @@ function configureAudio(info) { if (this.currentTime > info[info.length-1]) setPage(info.length, info); changeURLByParam('time', this.currentTime.toFixed(1)); - console.log(this.currentTime); + logger.log(this.currentTime); } ) } @@ -114,7 +114,7 @@ function setCriteriaResults(s) { } function setRecognizedInfo(slides){ - console.log(slides) + logger.log(slides) } function renderPageButtons(info){ diff --git a/app/static/js/upload.js b/app/static/js/upload.js index 22b22bf8..9cf68d5d 100644 --- a/app/static/js/upload.js +++ b/app/static/js/upload.js @@ -80,7 +80,7 @@ function fileLoadingOnChange() { const file = $("#file-loading").prop("files")[0]; let parts = file.name.split("."); let extension = parts.pop().toLowerCase(); - console.log(`File extension ${extension}`) + logger.log(`File extension ${extension}`) /* TODO: use list with user-allowed extensions */ if (parts.length < 1 || !user_formats.includes(extension)) { $("#alert").show(); diff --git a/app/static/js/utils.js b/app/static/js/utils.js index 5394d6cb..62b144ce 100644 --- a/app/static/js/utils.js +++ b/app/static/js/utils.js @@ -8,24 +8,24 @@ function buildTitleRow(columns) { return titleRowElement; } -function recheck(trainingId){ +function recheck(trainingId) { fetch('/api/sessions/admin') - .then(response => response.json()) - .then(res => { - if (res.admin) { - fetch(`/api/trainings/${trainingId}/`, {method: "POST"}) - .then(response => response.json()) - .then(innerResponseJson => { - if (innerResponseJson["message"] === "OK") { - window.open(`/trainings/statistics/${trainingId}/`); - //location.href = `/trainings/statistics/${trainingId}/`; - } - }); - } - }); + .then(response => response.json()) + .then(res => { + if (res.admin) { + fetch(`/api/trainings/${trainingId}/`, { method: "POST" }) + .then(response => response.json()) + .then(innerResponseJson => { + if (innerResponseJson["message"] === "OK") { + window.open(`/trainings/statistics/${trainingId}/`); + //location.href = `/trainings/statistics/${trainingId}/`; + } + }); + } + }); } -function strtobool(val, onError= false) { +function strtobool(val, onError = false) { try { val = val.toLowerCase(); if (['y', 'yes', 't', 'true', 'on', '1'].includes(val)) { diff --git a/app/templates/base.html b/app/templates/base.html index 54229204..19317812 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -17,6 +17,7 @@

{{ page_title }}

{% block content %} {% endblock %} {% block footer %} {% include "footer.html" %} {% endblock %} + diff --git a/app/templates/dumps.html b/app/templates/dumps.html index fad0efa1..b168aed2 100644 --- a/app/templates/dumps.html +++ b/app/templates/dumps.html @@ -40,12 +40,12 @@ window.location.reload(); } else{ - console.log(data["message"]); + logger.log(data["message"]); alert('{{ t("Ошибка создания архива.")}}' + '\n' + data["message"]) } }) .catch(error => { - console.log(error); + logger.log(error); alert('{{ t("Ошибка создания архива.") + t("Попробуйте позже или обратитесь к администратору") }}') }); })