commit
dbc3dbd482
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
'name': 'Odoo Log Viewer',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Tools',
|
||||
'summary': 'View Odoo log in real-time as a web page',
|
||||
'sequence': 10,
|
||||
'author': 'Abdelrahman Eltayar',
|
||||
'depends': ['base', 'web'],
|
||||
'data': [
|
||||
'views/log_viewer_template.xml',
|
||||
'data/ir_config_parameter.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_frontend': [
|
||||
('include', 'web._assets_helpers'), # إضافة هامة للتوافقية
|
||||
'odoo_log_viewer/static/src/css/log_viewer_style.css',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
|
||||
class LogViewer(http.Controller):
|
||||
|
||||
@http.route('/log_viewer', type='http', auth='public')
|
||||
def log_viewer(self, password=None):
|
||||
correct_password = request.env['ir.config_parameter'].sudo().get_param('odoo_log_viewer.password')
|
||||
|
||||
if password == correct_password:
|
||||
return request.render('odoo_log_viewer.log_viewer_template', {})
|
||||
else:
|
||||
return request.render('odoo_log_viewer.password_template', {})
|
||||
|
||||
@http.route('/log_viewer/get_logs', type='json', auth='public')
|
||||
def get_logs(self, password=None):
|
||||
correct_password = request.env['ir.config_parameter'].sudo().get_param('odoo_log_viewer.password')
|
||||
|
||||
if password == correct_password:
|
||||
log_entries = request.env['log.handler.manager'].sudo().get_log_entries()
|
||||
return {'log_content': '\n'.join(log_entries)}
|
||||
|
||||
else:
|
||||
return {'error': 'Invalid password'}
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="log_viewer_password" model="ir.config_parameter">
|
||||
<field name="key">odoo_log_viewer.password</field>
|
||||
<field name="value">$ecret@)24</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import log_handler
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import logging
|
||||
from odoo import api, models
|
||||
|
||||
# استخدام المسجل القياسي لإضافة رسائل تصحيح خاصة بهذا الموديول
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MemoryLogHandler(logging.Handler):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.max_entries = 10000 # الاحتفاظ بآخر 10000 سجل
|
||||
self.entries = []
|
||||
|
||||
def emit(self, record):
|
||||
# تجنب تسجيل الطلبات القادمة من العارض نفسه لمنع حلقة لا نهائية
|
||||
if '/log_viewer/get_logs' in getattr(record, 'message', ''):
|
||||
return
|
||||
|
||||
self.entries.append(self.format(record))
|
||||
|
||||
# إزالة أقدم سجل إذا تجاوزنا الحد الأقصى
|
||||
if len(self.entries) > self.max_entries:
|
||||
self.entries.pop(0)
|
||||
|
||||
|
||||
class LogHandlerManager(models.AbstractModel):
|
||||
_name = 'log.handler.manager'
|
||||
_description = 'Log Handler Manager'
|
||||
|
||||
# سنحتفظ بالمعالج كمتغير على مستوى الكلاس لضمان وجود نسخة واحدة فقط
|
||||
_memory_handler = None
|
||||
|
||||
@api.model
|
||||
def _get_memory_handler(self):
|
||||
# هذه الدالة الآن أصبحت أبسط، فقط تعيد المعالج الموجود
|
||||
return LogHandlerManager._memory_handler
|
||||
|
||||
@api.model
|
||||
def _register_hook(self):
|
||||
# هذه الدالة تُستدعى عند تحميل سجل الموديلات في أودو
|
||||
if LogHandlerManager._memory_handler is None:
|
||||
_logger.info("Initializing MemoryLogHandler for Odoo Log Viewer.")
|
||||
|
||||
# 1. إنشاء معالج السجلات الخاص بنا
|
||||
handler = MemoryLogHandler()
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
# 2. الحصول على مسجل الجذر وإضافة معالجنا إليه
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
# 3. !! التغيير الرئيسي هنا !!
|
||||
# ضبط المستوى على INFO لتجاهل رسائل DEBUG المزعجة.
|
||||
if root_logger.level > logging.INFO:
|
||||
root_logger.setLevel(logging.INFO)
|
||||
|
||||
LogHandlerManager._memory_handler = handler
|
||||
_logger.info("MemoryLogHandler successfully attached to the root logger at INFO level.")
|
||||
|
||||
@api.model
|
||||
def get_log_entries(self):
|
||||
handler = self._get_memory_handler()
|
||||
if handler:
|
||||
# إرجاع نسخة من القائمة لتجنب المشاكل المتعلقة بالتزامن
|
||||
return list(handler.entries)
|
||||
return []
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1,28 @@
|
|||
/* Style for the login form */
|
||||
form label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #464646;
|
||||
}
|
||||
|
||||
|
||||
#log-content {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
max-height: 80vh;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 1em;
|
||||
border-radius: 5px;
|
||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<template id="password_template" name="Log Viewer Password">
|
||||
<t t-call="web.layout">
|
||||
<t t-call-assets="odoo_log_viewer.assets"/>
|
||||
<t t-set="title">Log Viewer - Login</t>
|
||||
<div class="container py-5">
|
||||
<div class="card border-0 mx-auto bg-100 o_database_list" style="max-width: 300px;">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center mb-4">Log Viewer</h2>
|
||||
<form action="/log_viewer" method="get" class="oe_login_form">
|
||||
<div class="form-group field-login">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" autofocus="autofocus" required="required" autocomplete="off"/>
|
||||
</div>
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary btn-block">Log In</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="log_viewer_template" name="Log Viewer">
|
||||
<t t-call="web.layout">
|
||||
<t t-set="title">Odoo Log Viewer</t>
|
||||
<div class="container">
|
||||
<h1>Odoo Log</h1>
|
||||
<pre id="log-content" style="white-space: pre-wrap;"></pre>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var password = <t t-out="json.dumps(request.params.get('password'))"/>;
|
||||
|
||||
function updateLogs() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/log_viewer/get_logs', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var result = JSON.parse(xhr.responseText);
|
||||
if (result.error) {
|
||||
alert("Server Error: " + result.error);
|
||||
} else {
|
||||
var logContent = document.getElementById('log-content');
|
||||
if (logContent) {
|
||||
logContent.textContent = result.result.log_content;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error processing server response:", e);
|
||||
}
|
||||
} else {
|
||||
console.error("Request failed with status:", xhr.status);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
console.error("A network error occurred.");
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: { password: password },
|
||||
id: new Date().getTime()
|
||||
}));
|
||||
}
|
||||
|
||||
updateLogs();
|
||||
setInterval(updateLogs, 1000);
|
||||
</script>
|
||||
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue