feat(monitoring): add Redis exporter + Sentry docs to deployment guide
Some checks failed
Some checks failed
- Add redis-exporter container to docker-compose (oliver006/redis_exporter, 32MB) - Add Redis scrape target to Prometheus config - Add 4 Redis alert rules: RedisDown, HighMemory, HighConnections, RejectedConnections - Document Step 19b (Sentry Error Tracking) in Hetzner deployment guide - Document Step 19c (Redis Monitoring) in Hetzner deployment guide - Update resource budget and port reference tables Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,16 +82,16 @@ Update your `admin_service.py` to log actions:
|
||||
from app.services.admin_audit_service import admin_audit_service
|
||||
|
||||
class AdminService:
|
||||
|
||||
|
||||
def create_store_with_owner(
|
||||
self, db: Session, store_data: StoreCreate
|
||||
) -> Tuple[Store, User, str]:
|
||||
"""Create store with owner user account."""
|
||||
|
||||
|
||||
# ... existing code ...
|
||||
|
||||
|
||||
store, owner_user, temp_password = # ... your creation logic
|
||||
|
||||
|
||||
# LOG THE ACTION
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
@@ -105,19 +105,19 @@ class AdminService:
|
||||
"owner_email": owner_user.email
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return store, owner_user, temp_password
|
||||
|
||||
|
||||
def toggle_store_status(
|
||||
self, db: Session, store_id: int, admin_user_id: int
|
||||
) -> Tuple[Store, str]:
|
||||
"""Toggle store status with audit logging."""
|
||||
|
||||
|
||||
store = self._get_store_by_id_or_raise(db, store_id)
|
||||
old_status = store.is_active
|
||||
|
||||
|
||||
# ... toggle logic ...
|
||||
|
||||
|
||||
# LOG THE ACTION
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
@@ -130,7 +130,7 @@ class AdminService:
|
||||
"new_status": "active" if store.is_active else "inactive"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return store, message
|
||||
```
|
||||
|
||||
@@ -154,9 +154,9 @@ def create_store_with_owner(
|
||||
store_data=store_data,
|
||||
admin_user_id=current_admin.id # Pass admin ID for audit logging
|
||||
)
|
||||
|
||||
|
||||
# Audit log is automatically created inside the service
|
||||
|
||||
|
||||
return {
|
||||
**StoreResponse.model_validate(store).model_dump(),
|
||||
"owner_email": owner_user.email,
|
||||
@@ -205,9 +205,9 @@ def delete_store(
|
||||
# Get request metadata
|
||||
ip_address = request.client.host if request.client else None
|
||||
user_agent = request.headers.get("user-agent")
|
||||
|
||||
|
||||
message = admin_service.delete_store(db, store_id)
|
||||
|
||||
|
||||
# Log with full context
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
@@ -219,7 +219,7 @@ def delete_store(
|
||||
user_agent=user_agent,
|
||||
details={"confirm": True}
|
||||
)
|
||||
|
||||
|
||||
return {"message": message}
|
||||
```
|
||||
|
||||
@@ -300,15 +300,15 @@ from app.services.admin_settings_service import admin_settings_service
|
||||
|
||||
def can_create_store(db: Session) -> bool:
|
||||
"""Check if platform allows creating more stores."""
|
||||
|
||||
|
||||
max_stores = admin_settings_service.get_setting_value(
|
||||
db=db,
|
||||
key="max_stores_allowed",
|
||||
default=1000
|
||||
)
|
||||
|
||||
|
||||
current_count = db.query(Store).count()
|
||||
|
||||
|
||||
return current_count < max_stores
|
||||
|
||||
|
||||
@@ -331,7 +331,7 @@ def is_maintenance_mode(db: Session) -> bool:
|
||||
<!-- templates/admin/audit_logs.html -->
|
||||
<div x-data="auditLogs()" x-init="loadLogs()">
|
||||
<h1>Audit Logs</h1>
|
||||
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters">
|
||||
<select x-model="filters.action" @change="loadLogs()">
|
||||
@@ -341,7 +341,7 @@ def is_maintenance_mode(db: Session) -> bool:
|
||||
<option value="toggle_store_status">Toggle Status</option>
|
||||
<option value="update_setting">Update Setting</option>
|
||||
</select>
|
||||
|
||||
|
||||
<select x-model="filters.target_type" @change="loadLogs()">
|
||||
<option value="">All Targets</option>
|
||||
<option value="store">Stores</option>
|
||||
@@ -349,7 +349,7 @@ def is_maintenance_mode(db: Session) -> bool:
|
||||
<option value="setting">Settings</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Logs Table -->
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
@@ -379,7 +379,7 @@ def is_maintenance_mode(db: Session) -> bool:
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination">
|
||||
<button @click="previousPage()" :disabled="skip === 0">Previous</button>
|
||||
@@ -400,45 +400,45 @@ function auditLogs() {
|
||||
skip: 0,
|
||||
limit: 50,
|
||||
total: 0,
|
||||
|
||||
|
||||
async loadLogs() {
|
||||
const params = new URLSearchParams({
|
||||
skip: this.skip,
|
||||
limit: this.limit,
|
||||
...this.filters
|
||||
});
|
||||
|
||||
|
||||
const response = await apiClient.get(`/api/v1/admin/audit/logs?${params}`);
|
||||
this.logs = response.logs;
|
||||
this.total = response.total;
|
||||
},
|
||||
|
||||
|
||||
showDetails(log) {
|
||||
// Show modal with full details
|
||||
console.log('Details:', log.details);
|
||||
},
|
||||
|
||||
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleString();
|
||||
},
|
||||
|
||||
|
||||
get currentPage() {
|
||||
return Math.floor(this.skip / this.limit) + 1;
|
||||
},
|
||||
|
||||
|
||||
get totalPages() {
|
||||
return Math.ceil(this.total / this.limit);
|
||||
},
|
||||
|
||||
|
||||
get hasMore() {
|
||||
return this.skip + this.limit < this.total;
|
||||
},
|
||||
|
||||
|
||||
nextPage() {
|
||||
this.skip += this.limit;
|
||||
this.loadLogs();
|
||||
},
|
||||
|
||||
|
||||
previousPage() {
|
||||
this.skip = Math.max(0, this.skip - this.limit);
|
||||
this.loadLogs();
|
||||
@@ -454,23 +454,23 @@ function auditLogs() {
|
||||
<!-- templates/admin/settings.html -->
|
||||
<div x-data="platformSettings()" x-init="loadSettings()">
|
||||
<h1>Platform Settings</h1>
|
||||
|
||||
|
||||
<!-- Category Tabs -->
|
||||
<div class="tabs">
|
||||
<button
|
||||
<button
|
||||
@click="selectedCategory = 'system'"
|
||||
:class="{'active': selectedCategory === 'system'}"
|
||||
>System</button>
|
||||
<button
|
||||
<button
|
||||
@click="selectedCategory = 'security'"
|
||||
:class="{'active': selectedCategory === 'security'}"
|
||||
>Security</button>
|
||||
<button
|
||||
<button
|
||||
@click="selectedCategory = 'payments'"
|
||||
:class="{'active': selectedCategory === 'payments'}"
|
||||
>Payments</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Settings List -->
|
||||
<div class="settings-list">
|
||||
<template x-for="setting in filteredSettings" :key="setting.id">
|
||||
@@ -480,10 +480,10 @@ function auditLogs() {
|
||||
<span class="badge" x-text="setting.value_type"></span>
|
||||
</div>
|
||||
<p class="setting-description" x-text="setting.description"></p>
|
||||
|
||||
|
||||
<div class="setting-value">
|
||||
<input
|
||||
type="text"
|
||||
<input
|
||||
type="text"
|
||||
:value="setting.value"
|
||||
@change="updateSetting(setting.key, $event.target.value)"
|
||||
>
|
||||
@@ -492,7 +492,7 @@ function auditLogs() {
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Add New Setting -->
|
||||
<button @click="showAddModal = true" class="btn-primary">
|
||||
Add New Setting
|
||||
@@ -505,17 +505,17 @@ function platformSettings() {
|
||||
settings: [],
|
||||
selectedCategory: 'system',
|
||||
showAddModal: false,
|
||||
|
||||
|
||||
async loadSettings() {
|
||||
const response = await apiClient.get('/api/v1/admin/settings');
|
||||
this.settings = response.settings;
|
||||
},
|
||||
|
||||
|
||||
get filteredSettings() {
|
||||
if (!this.selectedCategory) return this.settings;
|
||||
return this.settings.filter(s => s.category === this.selectedCategory);
|
||||
},
|
||||
|
||||
|
||||
async updateSetting(key, newValue) {
|
||||
try {
|
||||
await apiClient.put(`/api/v1/admin/settings/${key}`, {
|
||||
@@ -527,7 +527,7 @@ function platformSettings() {
|
||||
showNotification('Failed to update setting', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleString();
|
||||
}
|
||||
@@ -557,7 +557,7 @@ def test_log_admin_action(db_session, test_admin_user):
|
||||
target_id="123",
|
||||
details={"store_code": "TEST"}
|
||||
)
|
||||
|
||||
|
||||
assert log is not None
|
||||
assert log.action == "create_store"
|
||||
assert log.target_type == "store"
|
||||
@@ -574,12 +574,12 @@ def test_query_audit_logs(db_session, test_admin_user):
|
||||
target_type="test",
|
||||
target_id=str(i)
|
||||
)
|
||||
|
||||
|
||||
# Query logs
|
||||
from models.schemas.admin import AdminAuditLogFilters
|
||||
filters = AdminAuditLogFilters(limit=10)
|
||||
logs = admin_audit_service.get_audit_logs(db_session, filters)
|
||||
|
||||
|
||||
assert len(logs) == 5
|
||||
```
|
||||
|
||||
@@ -590,20 +590,20 @@ def test_query_audit_logs(db_session, test_admin_user):
|
||||
def test_create_setting(db_session, test_admin_user):
|
||||
"""Test creating platform setting."""
|
||||
from models.schemas.admin import AdminSettingCreate
|
||||
|
||||
|
||||
setting_data = AdminSettingCreate(
|
||||
key="test_setting",
|
||||
value="test_value",
|
||||
value_type="string",
|
||||
category="test"
|
||||
)
|
||||
|
||||
|
||||
result = admin_settings_service.create_setting(
|
||||
db=db_session,
|
||||
setting_data=setting_data,
|
||||
admin_user_id=test_admin_user.id
|
||||
)
|
||||
|
||||
|
||||
assert result.key == "test_setting"
|
||||
assert result.value == "test_value"
|
||||
|
||||
@@ -617,7 +617,7 @@ def test_get_setting_value_with_type_conversion(db_session):
|
||||
category="system"
|
||||
)
|
||||
admin_settings_service.create_setting(db_session, setting_data, 1)
|
||||
|
||||
|
||||
# Get value (should be converted to int)
|
||||
value = admin_settings_service.get_setting_value(db_session, "max_stores")
|
||||
assert isinstance(value, int)
|
||||
@@ -630,10 +630,10 @@ def test_get_setting_value_with_type_conversion(db_session):
|
||||
|
||||
You now have a complete admin infrastructure with:
|
||||
|
||||
✅ **Audit Logging**: Track all admin actions for compliance
|
||||
✅ **Platform Settings**: Manage global configuration
|
||||
✅ **Notifications**: System alerts for admins (structure ready)
|
||||
✅ **Platform Alerts**: Health monitoring (structure ready)
|
||||
✅ **Audit Logging**: Track all admin actions for compliance
|
||||
✅ **Platform Settings**: Manage global configuration
|
||||
✅ **Notifications**: System alerts for admins (structure ready)
|
||||
✅ **Platform Alerts**: Health monitoring (structure ready)
|
||||
✅ **Session Tracking**: Monitor admin logins (structure ready)
|
||||
|
||||
### Next Steps
|
||||
@@ -646,4 +646,4 @@ You now have a complete admin infrastructure with:
|
||||
6. **Implement notification service** (notifications.py stubs)
|
||||
7. **Add monitoring** for platform alerts
|
||||
|
||||
These additions make your platform production-ready with full compliance and monitoring capabilities!
|
||||
These additions make your platform production-ready with full compliance and monitoring capabilities!
|
||||
|
||||
Reference in New Issue
Block a user