updates
This commit is contained in:
3
network_logger/network_logger/__init__.py
Normal file
3
network_logger/network_logger/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
|
||||
35
network_logger/network_logger/__manifest__.py
Normal file
35
network_logger/network_logger/__manifest__.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Network Traffic Logger',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Tools',
|
||||
'summary': 'Logs and displays all outgoing HTTP requests from Odoo',
|
||||
'description': """
|
||||
Network Traffic Logger for Odoo 19
|
||||
==================================
|
||||
|
||||
This module logs ALL outgoing HTTP requests made by Odoo to help identify
|
||||
external communications.
|
||||
|
||||
**Features:**
|
||||
- Logs all requests.get(), requests.post(), requests.put() calls
|
||||
- Stores logs in database for viewing in Odoo
|
||||
- Dashboard showing recent activity
|
||||
- Filter by Odoo external calls
|
||||
- Clear log functionality
|
||||
|
||||
**Access:**
|
||||
Settings → Technical → Network Traffic Logs
|
||||
""",
|
||||
'author': 'Fusion Development',
|
||||
'license': 'LGPL-3',
|
||||
'depends': ['base'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/network_log_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': True,
|
||||
}
|
||||
|
||||
4
network_logger/network_logger/models/__init__.py
Normal file
4
network_logger/network_logger/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import network_log
|
||||
from . import network_monitor
|
||||
|
||||
126
network_logger/network_logger/models/network_log.py
Normal file
126
network_logger/network_logger/models/network_log.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Network Log Model - Stores HTTP request logs in the database.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NetworkLog(models.Model):
|
||||
_name = 'network.log'
|
||||
_description = 'Network Traffic Log'
|
||||
_order = 'timestamp desc'
|
||||
|
||||
timestamp = fields.Datetime(
|
||||
string='Timestamp',
|
||||
default=fields.Datetime.now,
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
method = fields.Selection([
|
||||
('GET', 'GET'),
|
||||
('POST', 'POST'),
|
||||
('PUT', 'PUT'),
|
||||
('DELETE', 'DELETE'),
|
||||
('PATCH', 'PATCH'),
|
||||
('HEAD', 'HEAD'),
|
||||
('OPTIONS', 'OPTIONS'),
|
||||
], string='Method', required=True, index=True)
|
||||
|
||||
url = fields.Char(string='URL', required=True, index=True)
|
||||
domain = fields.Char(string='Domain', compute='_compute_domain', store=True, index=True)
|
||||
|
||||
is_odoo_call = fields.Boolean(
|
||||
string='Odoo External Call',
|
||||
default=False,
|
||||
index=True,
|
||||
help='True if this request was made to an Odoo server',
|
||||
)
|
||||
|
||||
status_code = fields.Integer(string='Status Code')
|
||||
status_type = fields.Selection([
|
||||
('success', 'Success'),
|
||||
('redirect', 'Redirect'),
|
||||
('client_error', 'Client Error'),
|
||||
('server_error', 'Server Error'),
|
||||
('blocked', 'Blocked'),
|
||||
('error', 'Error'),
|
||||
('pending', 'Pending'),
|
||||
], string='Status Type', compute='_compute_status_type', store=True)
|
||||
|
||||
response_time = fields.Float(string='Response Time (s)', digits=(10, 4))
|
||||
error_message = fields.Text(string='Error Message')
|
||||
request_headers = fields.Text(string='Request Headers')
|
||||
request_body = fields.Text(string='Request Body')
|
||||
|
||||
@api.depends('url')
|
||||
def _compute_domain(self):
|
||||
for record in self:
|
||||
try:
|
||||
parsed = urlparse(record.url or '')
|
||||
record.domain = parsed.netloc or 'unknown'
|
||||
except Exception:
|
||||
record.domain = 'unknown'
|
||||
|
||||
@api.depends('status_code', 'error_message')
|
||||
def _compute_status_type(self):
|
||||
for record in self:
|
||||
if record.error_message:
|
||||
if 'blocked' in (record.error_message or '').lower():
|
||||
record.status_type = 'blocked'
|
||||
else:
|
||||
record.status_type = 'error'
|
||||
elif not record.status_code:
|
||||
record.status_type = 'pending'
|
||||
elif 200 <= record.status_code < 300:
|
||||
record.status_type = 'success'
|
||||
elif 300 <= record.status_code < 400:
|
||||
record.status_type = 'redirect'
|
||||
elif 400 <= record.status_code < 500:
|
||||
record.status_type = 'client_error'
|
||||
elif record.status_code >= 500:
|
||||
record.status_type = 'server_error'
|
||||
else:
|
||||
record.status_type = 'pending'
|
||||
|
||||
@api.model
|
||||
def log_request(self, method, url, is_odoo_call=False, status_code=None,
|
||||
response_time=None, error_message=None):
|
||||
"""Create a new network log entry."""
|
||||
try:
|
||||
vals = {
|
||||
'method': method.upper(),
|
||||
'url': url[:2048] if url else '',
|
||||
'is_odoo_call': is_odoo_call,
|
||||
'status_code': status_code,
|
||||
'response_time': response_time,
|
||||
'error_message': error_message[:4096] if error_message else None,
|
||||
}
|
||||
return self.sudo().create(vals)
|
||||
except Exception as e:
|
||||
_logger.warning("Failed to log network request: %s", e)
|
||||
return self.browse()
|
||||
|
||||
@api.model
|
||||
def clear_old_logs(self, days=7):
|
||||
"""Delete logs older than specified days."""
|
||||
cutoff = fields.Datetime.now() - timedelta(days=days)
|
||||
old_logs = self.search([('timestamp', '<', cutoff)])
|
||||
count = len(old_logs)
|
||||
old_logs.unlink()
|
||||
_logger.info("Cleared %d network logs older than %d days", count, days)
|
||||
return count
|
||||
|
||||
@api.model
|
||||
def clear_all_logs(self):
|
||||
"""Delete all network logs."""
|
||||
count = self.search_count([])
|
||||
self.search([]).unlink()
|
||||
_logger.info("Cleared all %d network logs", count)
|
||||
return count
|
||||
|
||||
164
network_logger/network_logger/models/network_monitor.py
Normal file
164
network_logger/network_logger/models/network_monitor.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Network Traffic Monitor - Logs all outgoing HTTP requests from Odoo.
|
||||
This module patches the 'requests' library to log every HTTP call.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import functools
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_network_logger = logging.getLogger('odoo.network_traffic')
|
||||
|
||||
# Store for recent requests (in-memory buffer)
|
||||
_request_log = []
|
||||
_MAX_LOG_SIZE = 1000
|
||||
_log_lock = threading.Lock()
|
||||
|
||||
# Flag to prevent recursive logging
|
||||
_is_logging = threading.local()
|
||||
|
||||
|
||||
def _is_odoo_call(url):
|
||||
"""Check if URL is an Odoo external call."""
|
||||
odoo_domains = [
|
||||
'odoo.com', 'odoo.sh', 'iap.odoo', 'api.odoo',
|
||||
'services.odoo', 'apps.odoo', 'accounts.odoo',
|
||||
'upgrade.odoo', 'partner-autocomplete.odoo',
|
||||
]
|
||||
url_lower = str(url).lower()
|
||||
return any(domain in url_lower for domain in odoo_domains)
|
||||
|
||||
|
||||
def _save_to_database(log_entry):
|
||||
"""Save log entry to database (async-safe)."""
|
||||
try:
|
||||
if getattr(_is_logging, 'value', False):
|
||||
return
|
||||
|
||||
_is_logging.value = True
|
||||
|
||||
try:
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
|
||||
db_name = getattr(threading.current_thread(), 'dbname', None)
|
||||
if not db_name:
|
||||
from odoo.tools import config
|
||||
db_name = config.get('db_name')
|
||||
|
||||
if db_name:
|
||||
registry = Registry(db_name)
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
if 'network.log' in env:
|
||||
env['network.log'].log_request(
|
||||
method=log_entry.get('method', 'GET'),
|
||||
url=log_entry.get('url', ''),
|
||||
is_odoo_call=log_entry.get('is_odoo_call', False),
|
||||
status_code=log_entry.get('status_code'),
|
||||
response_time=log_entry.get('response_time_seconds'),
|
||||
error_message=log_entry.get('error'),
|
||||
)
|
||||
cr.commit()
|
||||
except Exception as e:
|
||||
_logger.debug("Could not save network log to database: %s", e)
|
||||
finally:
|
||||
_is_logging.value = False
|
||||
|
||||
|
||||
def _log_request(method, url, **kwargs):
|
||||
"""Log details of an HTTP request."""
|
||||
timestamp = datetime.now().isoformat()
|
||||
|
||||
log_entry = {
|
||||
'timestamp': timestamp,
|
||||
'method': method.upper(),
|
||||
'url': str(url),
|
||||
'timeout': kwargs.get('timeout'),
|
||||
'is_odoo_call': _is_odoo_call(url),
|
||||
}
|
||||
|
||||
if log_entry['is_odoo_call']:
|
||||
_network_logger.warning("ODOO EXTERNAL CALL: %s %s", method.upper(), url)
|
||||
else:
|
||||
_network_logger.info("HTTP Request: %s %s", method.upper(), url)
|
||||
|
||||
with _log_lock:
|
||||
_request_log.append(log_entry)
|
||||
if len(_request_log) > _MAX_LOG_SIZE:
|
||||
_request_log.pop(0)
|
||||
|
||||
return log_entry
|
||||
|
||||
|
||||
def _log_response(log_entry, response=None, error=None):
|
||||
"""Log the response of an HTTP request."""
|
||||
if response is not None:
|
||||
log_entry['status_code'] = response.status_code
|
||||
if hasattr(response, 'elapsed') and hasattr(response.elapsed, 'total_seconds'):
|
||||
log_entry['response_time_seconds'] = response.elapsed.total_seconds()
|
||||
|
||||
_network_logger.info("Response: %s %s -> %d",
|
||||
log_entry['method'], log_entry['url'], response.status_code)
|
||||
elif error is not None:
|
||||
log_entry['error'] = str(error)
|
||||
_network_logger.error("Error: %s %s -> %s",
|
||||
log_entry['method'], log_entry['url'], error)
|
||||
|
||||
_save_to_database(log_entry)
|
||||
|
||||
|
||||
def _wrap_request_method(original_method, method_name):
|
||||
"""Wrap a requests method to add logging."""
|
||||
@functools.wraps(original_method)
|
||||
def wrapper(url, *args, **kwargs):
|
||||
if 'localhost:65535' in str(url):
|
||||
return original_method(url, *args, **kwargs)
|
||||
|
||||
log_entry = _log_request(method_name, url, **kwargs)
|
||||
try:
|
||||
response = original_method(url, *args, **kwargs)
|
||||
_log_response(log_entry, response=response)
|
||||
return response
|
||||
except Exception as e:
|
||||
_log_response(log_entry, error=e)
|
||||
raise
|
||||
return wrapper
|
||||
|
||||
|
||||
def patch_requests_library():
|
||||
"""Monkey-patch the requests library to log all HTTP calls."""
|
||||
try:
|
||||
import requests
|
||||
|
||||
if not hasattr(requests, '_original_get'):
|
||||
requests._original_get = requests.get
|
||||
requests._original_post = requests.post
|
||||
requests._original_put = requests.put
|
||||
requests._original_delete = requests.delete
|
||||
requests._original_patch = requests.patch
|
||||
requests._original_head = requests.head
|
||||
requests._original_options = requests.options
|
||||
|
||||
requests.get = _wrap_request_method(requests._original_get, 'GET')
|
||||
requests.post = _wrap_request_method(requests._original_post, 'POST')
|
||||
requests.put = _wrap_request_method(requests._original_put, 'PUT')
|
||||
requests.delete = _wrap_request_method(requests._original_delete, 'DELETE')
|
||||
requests.patch = _wrap_request_method(requests._original_patch, 'PATCH')
|
||||
requests.head = _wrap_request_method(requests._original_head, 'HEAD')
|
||||
requests.options = _wrap_request_method(requests._original_options, 'OPTIONS')
|
||||
|
||||
_logger.info("NETWORK LOGGER: All HTTP requests are now being monitored")
|
||||
|
||||
except ImportError:
|
||||
_logger.warning("Could not import requests library for patching")
|
||||
except Exception as e:
|
||||
_logger.error("Error patching requests library: %s", e)
|
||||
|
||||
|
||||
# Apply patch when module is loaded
|
||||
patch_requests_library()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_network_log_admin,network.log.admin,model_network_log,base.group_system,1,1,1,1
|
||||
access_network_log_user,network.log.user,model_network_log,base.group_user,1,0,0,0
|
||||
|
||||
|
103
network_logger/network_logger/views/network_log_views.xml
Normal file
103
network_logger/network_logger/views/network_log_views.xml
Normal file
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Tree View -->
|
||||
<record id="view_network_log_list" model="ir.ui.view">
|
||||
<field name="name">network.log.list</field>
|
||||
<field name="model">network.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Network Logs" default_order="timestamp desc"
|
||||
decoration-danger="is_odoo_call"
|
||||
decoration-success="status_type == 'success'">
|
||||
<field name="timestamp"/>
|
||||
<field name="method"/>
|
||||
<field name="domain"/>
|
||||
<field name="url" optional="hide"/>
|
||||
<field name="is_odoo_call"/>
|
||||
<field name="status_code"/>
|
||||
<field name="status_type"/>
|
||||
<field name="response_time" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="view_network_log_form" model="ir.ui.view">
|
||||
<field name="name">network.log.form</field>
|
||||
<field name="model">network.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Network Log Entry" create="false" edit="false">
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Request Details">
|
||||
<field name="timestamp"/>
|
||||
<field name="method"/>
|
||||
<field name="domain"/>
|
||||
<field name="url"/>
|
||||
<field name="is_odoo_call"/>
|
||||
</group>
|
||||
<group string="Response Details">
|
||||
<field name="status_code"/>
|
||||
<field name="status_type"/>
|
||||
<field name="response_time"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Error Details" invisible="error_message == False">
|
||||
<field name="error_message" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View - Minimal -->
|
||||
<record id="view_network_log_search" model="ir.ui.view">
|
||||
<field name="name">network.log.search</field>
|
||||
<field name="model">network.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="url"/>
|
||||
<field name="domain"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_network_log" model="ir.actions.act_window">
|
||||
<field name="name">Network Traffic Logs</field>
|
||||
<field name="res_model">network.log</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No network traffic logged yet
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Odoo Calls Only -->
|
||||
<record id="action_network_log_odoo_calls" model="ir.actions.act_window">
|
||||
<field name="name">Odoo External Calls</field>
|
||||
<field name="res_model">network.log</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('is_odoo_call', '=', True)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<menuitem id="menu_network_logger_root"
|
||||
name="Network Logger"
|
||||
parent="base.menu_administration"
|
||||
sequence="100"/>
|
||||
|
||||
<menuitem id="menu_network_log_all"
|
||||
name="All Traffic"
|
||||
parent="menu_network_logger_root"
|
||||
action="action_network_log"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_network_log_odoo"
|
||||
name="Odoo External Calls"
|
||||
parent="menu_network_logger_root"
|
||||
action="action_network_log_odoo_calls"
|
||||
sequence="20"/>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user