Initial commit
This commit is contained in:
164
network_logger/models/network_monitor.py
Normal file
164
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()
|
||||
|
||||
Reference in New Issue
Block a user