Add stats tracking
This commit is contained in:
44
README.md
44
README.md
@@ -56,6 +56,44 @@ make docker-wheels
|
|||||||
|
|
||||||
Supports Micron `.mu` and dynamic pages with `#!` in the micron files.
|
Supports Micron `.mu` and dynamic pages with `#!` in the micron files.
|
||||||
|
|
||||||
|
## Statistics Tracking
|
||||||
|
|
||||||
|
The node now includes comprehensive statistics tracking for monitoring peer connections and page/file requests:
|
||||||
|
|
||||||
|
### Command Line Options for Stats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Print stats every 60 seconds
|
||||||
|
rns-page-node --stats-interval 60
|
||||||
|
|
||||||
|
# Save stats to JSON file on shutdown
|
||||||
|
rns-page-node --save-stats node_stats.json
|
||||||
|
|
||||||
|
# Actively write stats to file (live updates)
|
||||||
|
rns-page-node --stats-file stats.json
|
||||||
|
|
||||||
|
# Combined: live stats file + periodic display + final save
|
||||||
|
rns-page-node --stats-file stats.json --stats-interval 300 --save-stats final_stats.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Stats Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With periodic stats display
|
||||||
|
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config ghcr.io/sudo-ivan/rns-page-node:latest --stats-interval 60
|
||||||
|
|
||||||
|
# Save stats to mounted volume
|
||||||
|
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config -v ./stats:/app/stats ghcr.io/sudo-ivan/rns-page-node:latest --save-stats /app/stats/node_stats.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tracked Metrics
|
||||||
|
|
||||||
|
- **Connection Statistics**: Total connections, active connections, peer tracking
|
||||||
|
- **Request Statistics**: Page requests, file requests, requests by path and peer
|
||||||
|
- **Performance Metrics**: Requests per hour, uptime, response patterns
|
||||||
|
- **Historical Data**: Recent request history, hourly/daily aggregations
|
||||||
|
- **Top Content**: Most requested pages and files, most active peers
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -65,9 +103,11 @@ Supports Micron `.mu` and dynamic pages with `#!` in the micron files.
|
|||||||
-f, --files-dir: The directory to serve files from.
|
-f, --files-dir: The directory to serve files from.
|
||||||
-i, --identity-dir: The directory to persist the node's identity.
|
-i, --identity-dir: The directory to persist the node's identity.
|
||||||
-a, --announce-interval: The interval to announce the node's presence.
|
-a, --announce-interval: The interval to announce the node's presence.
|
||||||
-r, --page-refresh-interval: The interval to refresh pages.
|
--page-refresh-interval: The interval to refresh pages (seconds, 0 disables).
|
||||||
-f, --file-refresh-interval: The interval to refresh files.
|
--file-refresh-interval: The interval to refresh files (seconds, 0 disables).
|
||||||
-l, --log-level: The logging level.
|
-l, --log-level: The logging level.
|
||||||
|
--stats-interval: Print stats every N seconds (0 disables).
|
||||||
|
--save-stats: Save stats to JSON file on shutdown.
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import subprocess
|
|||||||
import RNS
|
import RNS
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -25,14 +28,16 @@ You are not authorised to carry out the request.
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
class PageNode:
|
class PageNode:
|
||||||
def __init__(self, identity, pagespath, filespath, announce_interval=360, name=None, page_refresh_interval=0, file_refresh_interval=0):
|
def __init__(self, identity, pagespath, filespath, announce_interval=360, name=None, page_refresh_interval=0, file_refresh_interval=0, stats_file=None):
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
self._stats_lock = threading.Lock()
|
||||||
self.logger = logging.getLogger(f"{__name__}.PageNode")
|
self.logger = logging.getLogger(f"{__name__}.PageNode")
|
||||||
self.identity = identity
|
self.identity = identity
|
||||||
self.name = name
|
self.name = name
|
||||||
self.pagespath = pagespath
|
self.pagespath = pagespath
|
||||||
self.filespath = filespath
|
self.filespath = filespath
|
||||||
|
self.stats_file = stats_file
|
||||||
self.destination = RNS.Destination(
|
self.destination = RNS.Destination(
|
||||||
identity,
|
identity,
|
||||||
RNS.Destination.IN,
|
RNS.Destination.IN,
|
||||||
@@ -47,6 +52,9 @@ class PageNode:
|
|||||||
self.last_page_refresh = time.time()
|
self.last_page_refresh = time.time()
|
||||||
self.last_file_refresh = time.time()
|
self.last_file_refresh = time.time()
|
||||||
|
|
||||||
|
# Initialize stats tracking
|
||||||
|
self._init_stats()
|
||||||
|
|
||||||
self.register_pages()
|
self.register_pages()
|
||||||
self.register_files()
|
self.register_files()
|
||||||
|
|
||||||
@@ -113,10 +121,163 @@ class PageNode:
|
|||||||
elif os.path.isfile(path):
|
elif os.path.isfile(path):
|
||||||
self.servedfiles.append(path)
|
self.servedfiles.append(path)
|
||||||
|
|
||||||
|
def _init_stats(self):
|
||||||
|
"""Initialize statistics tracking"""
|
||||||
|
self.stats = {
|
||||||
|
'start_time': time.time(),
|
||||||
|
'total_connections': 0,
|
||||||
|
'active_connections': 0,
|
||||||
|
'total_page_requests': 0,
|
||||||
|
'total_file_requests': 0,
|
||||||
|
'page_requests_by_path': defaultdict(int),
|
||||||
|
'file_requests_by_path': defaultdict(int),
|
||||||
|
'requests_by_peer': defaultdict(int),
|
||||||
|
'recent_requests': deque(maxlen=100), # Keep last 100 requests
|
||||||
|
'connected_peers': {}, # link_id -> peer_info
|
||||||
|
'hourly_stats': defaultdict(lambda: {'pages': 0, 'files': 0}),
|
||||||
|
'daily_stats': defaultdict(lambda: {'pages': 0, 'files': 0}),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize stats file if specified
|
||||||
|
if self.stats_file:
|
||||||
|
self._init_stats_file()
|
||||||
|
|
||||||
|
def _init_stats_file(self):
|
||||||
|
"""Initialize the stats file with basic structure"""
|
||||||
|
try:
|
||||||
|
# Ensure directory exists
|
||||||
|
dir_path = os.path.dirname(os.path.abspath(self.stats_file))
|
||||||
|
if dir_path:
|
||||||
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
|
|
||||||
|
# Create initial stats file
|
||||||
|
initial_stats = {
|
||||||
|
'node_info': {
|
||||||
|
'name': self.name or 'Unnamed',
|
||||||
|
'hash': RNS.hexrep(self.destination.hash, delimit=False),
|
||||||
|
'start_time': datetime.fromtimestamp(self.stats['start_time']).isoformat()
|
||||||
|
},
|
||||||
|
'connections': [],
|
||||||
|
'requests': [],
|
||||||
|
'summary': {
|
||||||
|
'total_connections': 0,
|
||||||
|
'total_page_requests': 0,
|
||||||
|
'total_file_requests': 0,
|
||||||
|
'last_updated': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(self.stats_file, 'w') as f:
|
||||||
|
json.dump(initial_stats, f, indent=2)
|
||||||
|
|
||||||
|
self.logger.info(f"Initialized stats file: {self.stats_file}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to initialize stats file {self.stats_file}: {e}")
|
||||||
|
|
||||||
|
def _write_stats_event(self, event_type, event_data):
|
||||||
|
"""Write a single stats event to the file"""
|
||||||
|
if not self.stats_file:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read current stats
|
||||||
|
try:
|
||||||
|
with open(self.stats_file, 'r') as f:
|
||||||
|
stats_data = json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
# If file doesn't exist or is corrupted, reinitialize
|
||||||
|
self._init_stats_file()
|
||||||
|
with open(self.stats_file, 'r') as f:
|
||||||
|
stats_data = json.load(f)
|
||||||
|
|
||||||
|
# Add the new event
|
||||||
|
if event_type == 'connection':
|
||||||
|
stats_data['connections'].append(event_data)
|
||||||
|
stats_data['summary']['total_connections'] += 1
|
||||||
|
elif event_type == 'request':
|
||||||
|
stats_data['requests'].append(event_data)
|
||||||
|
if event_data['type'] == 'page':
|
||||||
|
stats_data['summary']['total_page_requests'] += 1
|
||||||
|
elif event_data['type'] == 'file':
|
||||||
|
stats_data['summary']['total_file_requests'] += 1
|
||||||
|
|
||||||
|
# Update last_updated timestamp
|
||||||
|
stats_data['summary']['last_updated'] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# Keep only last 1000 events to prevent file from growing too large
|
||||||
|
if len(stats_data['connections']) > 1000:
|
||||||
|
stats_data['connections'] = stats_data['connections'][-1000:]
|
||||||
|
if len(stats_data['requests']) > 1000:
|
||||||
|
stats_data['requests'] = stats_data['requests'][-1000:]
|
||||||
|
|
||||||
|
# Write back to file
|
||||||
|
with open(self.stats_file, 'w') as f:
|
||||||
|
json.dump(stats_data, f, indent=2, default=str)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to write stats event to {self.stats_file}: {e}")
|
||||||
|
|
||||||
|
def _record_request(self, request_type, path, remote_identity, requested_at):
|
||||||
|
"""Record a request in statistics"""
|
||||||
|
with self._stats_lock:
|
||||||
|
# Get peer identity hash with better fallback
|
||||||
|
if remote_identity:
|
||||||
|
peer_hash = RNS.hexrep(remote_identity.hash, delimit=False)
|
||||||
|
# Try to get app_data name if available
|
||||||
|
try:
|
||||||
|
app_data = RNS.Identity.recall_app_data(remote_identity.hash)
|
||||||
|
if app_data:
|
||||||
|
peer_display = app_data.decode('utf-8', errors='ignore')[:32] # Limit length
|
||||||
|
else:
|
||||||
|
peer_display = peer_hash[:16] + "..." # Show first 16 chars
|
||||||
|
except:
|
||||||
|
peer_display = peer_hash[:16] + "..."
|
||||||
|
else:
|
||||||
|
peer_hash = "anonymous"
|
||||||
|
peer_display = "anonymous"
|
||||||
|
|
||||||
|
# Record basic stats
|
||||||
|
if request_type == 'page':
|
||||||
|
self.stats['total_page_requests'] += 1
|
||||||
|
self.stats['page_requests_by_path'][path] += 1
|
||||||
|
elif request_type == 'file':
|
||||||
|
self.stats['total_file_requests'] += 1
|
||||||
|
self.stats['file_requests_by_path'][path] += 1
|
||||||
|
|
||||||
|
self.stats['requests_by_peer'][peer_hash] += 1
|
||||||
|
|
||||||
|
# Record recent request
|
||||||
|
request_info = {
|
||||||
|
'type': request_type,
|
||||||
|
'path': path,
|
||||||
|
'peer': peer_display,
|
||||||
|
'peer_hash': peer_hash,
|
||||||
|
'timestamp': requested_at,
|
||||||
|
'datetime': datetime.fromtimestamp(requested_at).isoformat()
|
||||||
|
}
|
||||||
|
self.stats['recent_requests'].append(request_info)
|
||||||
|
|
||||||
|
# Record hourly and daily stats
|
||||||
|
dt = datetime.fromtimestamp(requested_at)
|
||||||
|
hour_key = dt.strftime('%Y-%m-%d %H:00')
|
||||||
|
day_key = dt.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
if request_type == 'page':
|
||||||
|
self.stats['hourly_stats'][hour_key]['pages'] += 1
|
||||||
|
self.stats['daily_stats'][day_key]['pages'] += 1
|
||||||
|
elif request_type == 'file':
|
||||||
|
self.stats['hourly_stats'][hour_key]['files'] += 1
|
||||||
|
self.stats['daily_stats'][day_key]['files'] += 1
|
||||||
|
|
||||||
|
# Write to stats file immediately
|
||||||
|
self._write_stats_event('request', request_info)
|
||||||
|
|
||||||
def serve_default_index(self, path, data, request_id, link_id, remote_identity, requested_at):
|
def serve_default_index(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||||
|
self._record_request('page', path, remote_identity, requested_at)
|
||||||
return DEFAULT_INDEX.encode('utf-8')
|
return DEFAULT_INDEX.encode('utf-8')
|
||||||
|
|
||||||
def serve_page(self, path, data, request_id, link_id, remote_identity, requested_at):
|
def serve_page(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||||
|
self._record_request('page', path, remote_identity, requested_at)
|
||||||
file_path = path.replace("/page", self.pagespath, 1)
|
file_path = path.replace("/page", self.pagespath, 1)
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'rb') as _f:
|
with open(file_path, 'rb') as _f:
|
||||||
@@ -135,11 +296,66 @@ class PageNode:
|
|||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
def serve_file(self, path, data, request_id, link_id, remote_identity, requested_at):
|
def serve_file(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||||
|
self._record_request('file', path, remote_identity, requested_at)
|
||||||
file_path = path.replace("/file", self.filespath, 1)
|
file_path = path.replace("/file", self.filespath, 1)
|
||||||
return [open(file_path, 'rb'), {"name": os.path.basename(file_path).encode('utf-8')}]
|
return [open(file_path, 'rb'), {"name": os.path.basename(file_path).encode('utf-8')}]
|
||||||
|
|
||||||
def on_connect(self, link):
|
def on_connect(self, link):
|
||||||
pass
|
"""Called when a new link is established"""
|
||||||
|
connection_time = time.time()
|
||||||
|
with self._stats_lock:
|
||||||
|
self.stats['total_connections'] += 1
|
||||||
|
self.stats['active_connections'] += 1
|
||||||
|
|
||||||
|
# Get peer info with better identification
|
||||||
|
if link.get_remote_identity():
|
||||||
|
peer_hash = RNS.hexrep(link.get_remote_identity().hash, delimit=False)
|
||||||
|
# Try to get app_data name if available
|
||||||
|
try:
|
||||||
|
app_data = RNS.Identity.recall_app_data(link.get_remote_identity().hash)
|
||||||
|
if app_data:
|
||||||
|
peer_display = app_data.decode('utf-8', errors='ignore')[:32] # Limit length
|
||||||
|
else:
|
||||||
|
peer_display = peer_hash[:16] + "..." # Show first 16 chars
|
||||||
|
except:
|
||||||
|
peer_display = peer_hash[:16] + "..."
|
||||||
|
else:
|
||||||
|
peer_hash = "anonymous"
|
||||||
|
peer_display = "anonymous"
|
||||||
|
|
||||||
|
# Convert link_id to hex string properly
|
||||||
|
link_id_hex = RNS.hexrep(link.link_id, delimit=False) if hasattr(link, 'link_id') else "unknown"
|
||||||
|
|
||||||
|
self.stats['connected_peers'][link_id_hex] = {
|
||||||
|
'peer_hash': peer_hash,
|
||||||
|
'peer_display': peer_display,
|
||||||
|
'connected_at': connection_time,
|
||||||
|
'link_id': link_id_hex
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write connection event to stats file
|
||||||
|
connection_info = {
|
||||||
|
'event': 'connected',
|
||||||
|
'peer': peer_display,
|
||||||
|
'peer_hash': peer_hash,
|
||||||
|
'timestamp': connection_time,
|
||||||
|
'datetime': datetime.fromtimestamp(connection_time).isoformat(),
|
||||||
|
'link_id': link_id_hex
|
||||||
|
}
|
||||||
|
self._write_stats_event('connection', connection_info)
|
||||||
|
|
||||||
|
self.logger.info(f"New connection established from peer {peer_display}")
|
||||||
|
|
||||||
|
# Set callback for when link closes
|
||||||
|
link.set_link_closed_callback(self._on_link_closed)
|
||||||
|
|
||||||
|
def _on_link_closed(self, link):
|
||||||
|
"""Called when a link is closed"""
|
||||||
|
with self._stats_lock:
|
||||||
|
if link.link_id in self.stats['connected_peers']:
|
||||||
|
peer_info = self.stats['connected_peers'].pop(link.link_id)
|
||||||
|
self.stats['active_connections'] = max(0, self.stats['active_connections'] - 1)
|
||||||
|
self.logger.info(f"Connection closed from peer {peer_info['peer_hash'][:16]}...")
|
||||||
|
|
||||||
def _announce_loop(self):
|
def _announce_loop(self):
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
@@ -168,6 +384,145 @@ class PageNode:
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception("Error in refresh loop")
|
self.logger.exception("Error in refresh loop")
|
||||||
|
|
||||||
|
def get_stats(self):
|
||||||
|
"""Get current statistics"""
|
||||||
|
with self._stats_lock:
|
||||||
|
# Calculate uptime
|
||||||
|
uptime = time.time() - self.stats['start_time']
|
||||||
|
|
||||||
|
# Get top requested pages and files
|
||||||
|
top_pages = sorted(self.stats['page_requests_by_path'].items(), key=lambda x: x[1], reverse=True)[:10]
|
||||||
|
top_files = sorted(self.stats['file_requests_by_path'].items(), key=lambda x: x[1], reverse=True)[:10]
|
||||||
|
top_peers = sorted(self.stats['requests_by_peer'].items(), key=lambda x: x[1], reverse=True)[:10]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'uptime_seconds': uptime,
|
||||||
|
'uptime_formatted': self._format_duration(uptime),
|
||||||
|
'start_time': datetime.fromtimestamp(self.stats['start_time']).isoformat(),
|
||||||
|
'total_connections': self.stats['total_connections'],
|
||||||
|
'active_connections': self.stats['active_connections'],
|
||||||
|
'total_page_requests': self.stats['total_page_requests'],
|
||||||
|
'total_file_requests': self.stats['total_file_requests'],
|
||||||
|
'total_requests': self.stats['total_page_requests'] + self.stats['total_file_requests'],
|
||||||
|
'top_pages': top_pages,
|
||||||
|
'top_files': top_files,
|
||||||
|
'top_peers': [(peer[:16] + "..." if len(peer) > 16 else peer, count) for peer, count in top_peers],
|
||||||
|
'recent_requests': list(self.stats['recent_requests'])[-10:], # Last 10 requests
|
||||||
|
'connected_peers': len(self.stats['connected_peers']),
|
||||||
|
'requests_per_hour': self._calculate_requests_per_hour(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _format_duration(self, seconds):
|
||||||
|
"""Format duration in human readable format"""
|
||||||
|
days = int(seconds // 86400)
|
||||||
|
hours = int((seconds % 86400) // 3600)
|
||||||
|
minutes = int((seconds % 3600) // 60)
|
||||||
|
secs = int(seconds % 60)
|
||||||
|
|
||||||
|
if days > 0:
|
||||||
|
return f"{days}d {hours}h {minutes}m {secs}s"
|
||||||
|
elif hours > 0:
|
||||||
|
return f"{hours}h {minutes}m {secs}s"
|
||||||
|
elif minutes > 0:
|
||||||
|
return f"{minutes}m {secs}s"
|
||||||
|
else:
|
||||||
|
return f"{secs}s"
|
||||||
|
|
||||||
|
def _calculate_requests_per_hour(self):
|
||||||
|
"""Calculate average requests per hour"""
|
||||||
|
uptime_hours = (time.time() - self.stats['start_time']) / 3600
|
||||||
|
if uptime_hours < 0.1: # Less than 6 minutes
|
||||||
|
return 0
|
||||||
|
total_requests = self.stats['total_page_requests'] + self.stats['total_file_requests']
|
||||||
|
return round(total_requests / uptime_hours, 2)
|
||||||
|
|
||||||
|
def print_stats(self):
|
||||||
|
"""Print formatted statistics to console"""
|
||||||
|
stats = self.get_stats()
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("RNS PAGE NODE STATISTICS")
|
||||||
|
print("="*60)
|
||||||
|
print(f"Node Name: {self.name or 'Unnamed'}")
|
||||||
|
print(f"Started: {stats['start_time']}")
|
||||||
|
print(f"Uptime: {stats['uptime_formatted']}")
|
||||||
|
print(f"Node Hash: {RNS.hexrep(self.destination.hash, delimit=False)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("CONNECTION STATS:")
|
||||||
|
print(f" Total Connections: {stats['total_connections']}")
|
||||||
|
print(f" Active Connections: {stats['active_connections']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("REQUEST STATS:")
|
||||||
|
print(f" Total Requests: {stats['total_requests']}")
|
||||||
|
print(f" Page Requests: {stats['total_page_requests']}")
|
||||||
|
print(f" File Requests: {stats['total_file_requests']}")
|
||||||
|
print(f" Requests/Hour: {stats['requests_per_hour']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if stats['top_pages']:
|
||||||
|
print("TOP REQUESTED PAGES:")
|
||||||
|
for path, count in stats['top_pages']:
|
||||||
|
print(f" {count:4d} - {path}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if stats['top_files']:
|
||||||
|
print("TOP REQUESTED FILES:")
|
||||||
|
for path, count in stats['top_files']:
|
||||||
|
print(f" {count:4d} - {path}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if stats['top_peers']:
|
||||||
|
print("TOP REQUESTING PEERS:")
|
||||||
|
for peer, count in stats['top_peers']:
|
||||||
|
print(f" {count:4d} - {peer}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if stats['recent_requests']:
|
||||||
|
print("RECENT REQUESTS:")
|
||||||
|
for req in stats['recent_requests']:
|
||||||
|
print(f" {req['datetime']} - {req['type'].upper()} {req['path']} from {req['peer'][:16]}...")
|
||||||
|
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
def save_stats_to_file(self, filepath):
|
||||||
|
"""Save statistics to JSON file"""
|
||||||
|
try:
|
||||||
|
stats = self.get_stats()
|
||||||
|
|
||||||
|
# Ensure directory exists
|
||||||
|
dir_path = os.path.dirname(os.path.abspath(filepath))
|
||||||
|
if dir_path:
|
||||||
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
|
|
||||||
|
# Convert defaultdict and other non-serializable objects to regular dicts
|
||||||
|
with self._stats_lock:
|
||||||
|
stats_copy = dict(stats)
|
||||||
|
stats_copy['page_requests_by_path'] = dict(self.stats['page_requests_by_path'])
|
||||||
|
stats_copy['file_requests_by_path'] = dict(self.stats['file_requests_by_path'])
|
||||||
|
stats_copy['requests_by_peer'] = dict(self.stats['requests_by_peer'])
|
||||||
|
stats_copy['hourly_stats'] = {k: dict(v) for k, v in self.stats['hourly_stats'].items()}
|
||||||
|
stats_copy['daily_stats'] = {k: dict(v) for k, v in self.stats['daily_stats'].items()}
|
||||||
|
stats_copy['connected_peers'] = dict(self.stats['connected_peers'])
|
||||||
|
stats_copy['recent_requests'] = list(self.stats['recent_requests'])
|
||||||
|
|
||||||
|
with open(filepath, 'w') as f:
|
||||||
|
json.dump(stats_copy, f, indent=2, default=str)
|
||||||
|
self.logger.info(f"Statistics saved to {filepath}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to save statistics to {filepath}: {e}")
|
||||||
|
import traceback
|
||||||
|
self.logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reset_stats(self):
|
||||||
|
"""Reset all statistics"""
|
||||||
|
with self._stats_lock:
|
||||||
|
self._init_stats()
|
||||||
|
self.logger.info("Statistics reset")
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.logger.info("Shutting down PageNode...")
|
self.logger.info("Shutting down PageNode...")
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
@@ -194,6 +549,9 @@ def main():
|
|||||||
parser.add_argument('--page-refresh-interval', dest='page_refresh_interval', type=int, default=0, help='Page refresh interval in seconds, 0 disables auto-refresh')
|
parser.add_argument('--page-refresh-interval', dest='page_refresh_interval', type=int, default=0, help='Page refresh interval in seconds, 0 disables auto-refresh')
|
||||||
parser.add_argument('--file-refresh-interval', dest='file_refresh_interval', type=int, default=0, help='File refresh interval in seconds, 0 disables auto-refresh')
|
parser.add_argument('--file-refresh-interval', dest='file_refresh_interval', type=int, default=0, help='File refresh interval in seconds, 0 disables auto-refresh')
|
||||||
parser.add_argument('-l', '--log-level', dest='log_level', choices=['DEBUG','INFO','WARNING','ERROR','CRITICAL'], default='INFO', help='Logging level')
|
parser.add_argument('-l', '--log-level', dest='log_level', choices=['DEBUG','INFO','WARNING','ERROR','CRITICAL'], default='INFO', help='Logging level')
|
||||||
|
parser.add_argument('--stats-interval', dest='stats_interval', type=int, default=0, help='Print stats every N seconds (0 disables)')
|
||||||
|
parser.add_argument('--save-stats', dest='save_stats', help='Save stats to JSON file on shutdown')
|
||||||
|
parser.add_argument('--stats-file', dest='stats_file', help='Actively write stats to JSON file (live updates)')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
configpath = args.configpath
|
configpath = args.configpath
|
||||||
@@ -204,6 +562,9 @@ def main():
|
|||||||
identity_dir = args.identity_dir
|
identity_dir = args.identity_dir
|
||||||
page_refresh_interval = args.page_refresh_interval
|
page_refresh_interval = args.page_refresh_interval
|
||||||
file_refresh_interval = args.file_refresh_interval
|
file_refresh_interval = args.file_refresh_interval
|
||||||
|
stats_interval = args.stats_interval
|
||||||
|
save_stats_file = args.save_stats
|
||||||
|
stats_file = args.stats_file
|
||||||
numeric_level = getattr(logging, args.log_level.upper(), logging.INFO)
|
numeric_level = getattr(logging, args.log_level.upper(), logging.INFO)
|
||||||
logging.basicConfig(level=numeric_level, format='%(asctime)s %(name)s [%(levelname)s] %(message)s')
|
logging.basicConfig(level=numeric_level, format='%(asctime)s %(name)s [%(levelname)s] %(message)s')
|
||||||
|
|
||||||
@@ -219,15 +580,46 @@ def main():
|
|||||||
os.makedirs(pages_dir, exist_ok=True)
|
os.makedirs(pages_dir, exist_ok=True)
|
||||||
os.makedirs(files_dir, exist_ok=True)
|
os.makedirs(files_dir, exist_ok=True)
|
||||||
|
|
||||||
node = PageNode(identity, pages_dir, files_dir, announce_interval, node_name, page_refresh_interval, file_refresh_interval)
|
node = PageNode(identity, pages_dir, files_dir, announce_interval, node_name, page_refresh_interval, file_refresh_interval, stats_file)
|
||||||
logger.info("Page node running. Press Ctrl-C to exit.")
|
logger.info("Page node running. Press Ctrl-C to exit.")
|
||||||
|
|
||||||
|
if stats_interval > 0:
|
||||||
|
logger.info(f"Stats will be printed every {stats_interval} seconds")
|
||||||
|
|
||||||
|
last_stats_time = 0
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Print stats if interval is set and enough time has passed
|
||||||
|
if stats_interval > 0 and current_time - last_stats_time >= stats_interval:
|
||||||
|
node.print_stats()
|
||||||
|
last_stats_time = current_time
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("Keyboard interrupt received, shutting down...")
|
logger.info("Keyboard interrupt received, shutting down...")
|
||||||
|
|
||||||
|
# Print final stats
|
||||||
|
node.print_stats()
|
||||||
|
|
||||||
|
# Save stats if requested
|
||||||
|
if save_stats_file:
|
||||||
|
logger.info(f"Saving final statistics to {save_stats_file}")
|
||||||
|
if node.save_stats_to_file(save_stats_file):
|
||||||
|
logger.info(f"Statistics successfully saved to {save_stats_file}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to save statistics to {save_stats_file}")
|
||||||
|
|
||||||
node.shutdown()
|
node.shutdown()
|
||||||
|
finally:
|
||||||
|
# Ensure stats are saved even if something goes wrong
|
||||||
|
if save_stats_file and 'node' in locals():
|
||||||
|
try:
|
||||||
|
node.save_stats_to_file(save_stats_file)
|
||||||
|
logger.info(f"Final attempt: Statistics saved to {save_stats_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Final save attempt failed: {e}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user