# -*- 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()