Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions LOGGING_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# User Activity Logging Configuration

The user activity logging system captures all user interactions with the wine classification application and saves them to structured log files for analysis by log management systems.

## Features

- **Structured Logging**: All log entries are in JSON format for easy parsing
- **Comprehensive Coverage**: Logs all user interactions including login, logout, registration, wine classification, profile access, password changes, and contact form submissions
- **Security-Aware**: Avoids logging sensitive information like passwords
- **Analysis-Ready**: Includes timestamps, user identification, IP addresses, and detailed activity context

## Log File Location

By default, logs are written to `user_activity.log` in the application directory.

## Log Format

Each log entry includes:
- `timestamp`: ISO format timestamp
- `action`: Type of user activity
- `user_id`: Database user ID (when available)
- `username`: Username (when available)
- `ip_address`: Client IP address
- `user_agent`: Browser user agent string
- `details`: Action-specific details

## Sample Log Entries

```json
{"timestamp": "2024-01-15T10:30:45.123456", "action": "login_attempt", "user_id": 123, "username": "johndoe", "ip_address": "192.168.1.100", "user_agent": "Mozilla/5.0...", "details": {"success": true, "attempted_username": "johndoe"}}

{"timestamp": "2024-01-15T10:35:12.789012", "action": "wine_classification", "user_id": 123, "username": "johndoe", "ip_address": "192.168.1.100", "user_agent": "Mozilla/5.0...", "details": {"wine_characteristics": {"alcohol": 12.5, "malic_acid": 2.3, ...}, "prediction": 1}}

{"timestamp": "2024-01-15T10:40:00.456789", "action": "logout", "user_id": 123, "username": "johndoe", "ip_address": "192.168.1.100", "user_agent": "Mozilla/5.0...", "details": {}}
```

## Actions Logged

1. **login_attempt** - Both successful and failed login attempts
2. **logout** - User logout actions
3. **registration** - User registration attempts
4. **wine_classification** - Wine prediction requests with input data
5. **profile_access** - Profile page views
6. **password_change** - Password change attempts
7. **contact_form** - Contact form submissions

## Log Rotation

For production use, implement log rotation using logrotate or Python's RotatingFileHandler:

```python
from logging.handlers import RotatingFileHandler

# Example: Rotate when log file reaches 10MB, keep 5 backup files
handler = RotatingFileHandler('user_activity.log', maxBytes=10*1024*1024, backupCount=5)
```

## Privacy and Security

- Passwords are never logged
- Only successful login attempts include username in the main fields
- Failed attempts include attempted username in details only
- All logging respects user privacy while providing audit capability

## Integration with Log Management Systems

The JSON format makes these logs easy to ingest into systems like:
- Elasticsearch/ELK Stack
- Splunk
- Fluentd
- AWS CloudWatch
- Azure Monitor

## Configuration

Modify logging behavior by creating a custom UserActivityLogger instance:

```python
from user_activity_logger import UserActivityLogger

# Custom configuration
logger = UserActivityLogger(
log_file='/var/log/wine_app/user_activity.log',
log_level=logging.DEBUG
)
```
106 changes: 106 additions & 0 deletions tests/test_user_activity_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import unittest
import os
import json
import tempfile
from user_activity_logger import UserActivityLogger


class TestUserActivityLogger(unittest.TestCase):
"""Test cases for user activity logging functionality."""

def setUp(self):
"""Set up test environment."""
# Create a temporary log file for testing
self.test_log_file = tempfile.mktemp(suffix='.log')
self.logger = UserActivityLogger(log_file=self.test_log_file)

def tearDown(self):
"""Clean up test environment."""
# Remove test log file
if os.path.exists(self.test_log_file):
os.remove(self.test_log_file)

def test_log_user_activity(self):
"""Test basic user activity logging."""
self.logger.log_user_activity('test_action', details={'test': 'data'})

# Verify log entry was created
self.assertTrue(os.path.exists(self.test_log_file))

with open(self.test_log_file, 'r') as f:
log_content = f.read()
self.assertIn('test_action', log_content)
self.assertIn('test', log_content)

def test_log_login_attempt_success(self):
"""Test successful login logging."""
self.logger.log_login_attempt('testuser', success=True)

with open(self.test_log_file, 'r') as f:
log_content = f.read()
self.assertIn('login_attempt', log_content)
self.assertIn('testuser', log_content)
self.assertIn('true', log_content.lower())

def test_log_login_attempt_failure(self):
"""Test failed login logging."""
self.logger.log_login_attempt('testuser', success=False,
details={'reason': 'invalid_credentials'})

with open(self.test_log_file, 'r') as f:
log_content = f.read()
self.assertIn('login_attempt', log_content)
self.assertIn('testuser', log_content)
self.assertIn('false', log_content.lower())
self.assertIn('invalid_credentials', log_content)

def test_log_registration(self):
"""Test user registration logging."""
self.logger.log_registration('newuser', success=True)

with open(self.test_log_file, 'r') as f:
log_content = f.read()
self.assertIn('registration', log_content)
self.assertIn('newuser', log_content)

def test_log_wine_classification(self):
"""Test wine classification logging."""
wine_data = {'alcohol': 12.5, 'malic_acid': 2.3}
prediction = 1

self.logger.log_wine_classification(wine_data, prediction)

with open(self.test_log_file, 'r') as f:
log_content = f.read()
self.assertIn('wine_classification', log_content)
self.assertIn('12.5', log_content)
self.assertIn('2.3', log_content)
self.assertIn('1', log_content)

def test_log_password_change(self):
"""Test password change logging."""
self.logger.log_password_change(success=True)

with open(self.test_log_file, 'r') as f:
log_content = f.read()
self.assertIn('password_change', log_content)
self.assertIn('true', log_content.lower())

def test_log_entries_are_json_format(self):
"""Test that log entries are in valid JSON format."""
self.logger.log_user_activity('test_action', details={'test': 'data'})

with open(self.test_log_file, 'r') as f:
lines = f.readlines()
# Get the JSON part of the log entry (after the timestamp and level)
log_line = lines[0]
json_part = log_line.split(' - INFO - ')[1].strip()

# Should be valid JSON
parsed_data = json.loads(json_part)
self.assertEqual(parsed_data['action'], 'test_action')
self.assertEqual(parsed_data['details']['test'], 'data')


if __name__ == '__main__':
unittest.main()
124 changes: 124 additions & 0 deletions user_activity_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import logging
import json
import os
from datetime import datetime
from functools import wraps

# Make Flask imports optional for testing
try:
from flask import session, request
HAS_FLASK = True
except ImportError:
HAS_FLASK = False
session = None
request = None

class UserActivityLogger:
"""Logger for user interactions in the wine classification application."""

def __init__(self, log_file='user_activity.log', log_level=logging.INFO):
self.log_file = log_file
self.logger = logging.getLogger('user_activity')
self.logger.setLevel(log_level)

# Remove existing handlers to avoid duplicates
for handler in self.logger.handlers[:]:
self.logger.removeHandler(handler)

# Create file handler
handler = logging.FileHandler(log_file)
handler.setLevel(log_level)

# Create formatter for structured logging
formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
handler.setFormatter(formatter)

self.logger.addHandler(handler)

def log_user_activity(self, action, details=None, user_id=None, username=None):
"""Log user activity with structured data."""
if HAS_FLASK and session:
if user_id is None and 'id' in session:
user_id = session.get('id')
if username is None and 'username' in session:
username = session.get('username')

log_entry = {
'timestamp': datetime.now().isoformat(),
'action': action,
'user_id': user_id,
'username': username,
'ip_address': request.remote_addr if HAS_FLASK and request else None,
'user_agent': request.headers.get('User-Agent') if HAS_FLASK and request else None,
'details': details or {}
}

self.logger.info(json.dumps(log_entry))

def log_login_attempt(self, username, success, details=None):
"""Log login attempts."""
self.log_user_activity(
action='login_attempt',
details={
'success': success,
'attempted_username': username,
**(details or {})
},
username=username if success else None
)

def log_logout(self, username=None):
"""Log user logout."""
self.log_user_activity(
action='logout',
username=username
)

def log_registration(self, username, success, details=None):
"""Log user registration attempts."""
self.log_user_activity(
action='registration',
details={
'success': success,
'username': username,
**(details or {})
},
username=username if success else None
)

def log_wine_classification(self, wine_data, prediction):
"""Log wine classification requests."""
self.log_user_activity(
action='wine_classification',
details={
'wine_characteristics': wine_data,
'prediction': prediction
}
)

def log_profile_access(self):
"""Log profile page access."""
self.log_user_activity(action='profile_access')

def log_password_change(self, success, details=None):
"""Log password change attempts."""
self.log_user_activity(
action='password_change',
details={
'success': success,
**(details or {})
}
)

def log_contact_form(self, success=True):
"""Log contact form submissions."""
self.log_user_activity(
action='contact_form',
details={'success': success}
)

# Global logger instance
activity_logger = UserActivityLogger()
Loading