mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-27 23:28:03 +09:00
Every model with a JSON-in-Text column hand-rolled the same try/except
json.loads pair — get_details/get_context/get_headers/get_payload and a
dozen more, each a place the next fix would have to land N times. The
mixin owns the mechanics (empty-is-default, corrupt-is-default so a
hand-edited row never 500s an endpoint, optional shape check,
falsy-on-write); the per-model parts — accessor name, column, default —
stay per-model.
Conversion is behaviour-preserving: identical fallbacks, identical
on-write empty forms (NULL vs '[]' vs '{}'). Deliberately NOT converted:
accessors that let json.loads raise on corrupt data (collapsing those
would be a behaviour change, not a consolidation), and inline parses in
to_dict (per-entity serialization choice). A handful of 'except
Exception' reads narrow to the dominant (JSONDecodeError, TypeError) —
the only exceptions json.loads raises for str input.
116 lines
4.2 KiB
Python
116 lines
4.2 KiB
Python
"""System-wide telemetry event model.
|
|
|
|
A single, queryable event stream that records what happened, when, and to which
|
|
resource across ServerKit. Existing domain-specific tables (audit_logs,
|
|
event_deliveries, notification_deliveries, metrics_history, etc.) remain the
|
|
authoritative sources; SystemEvent is a correlation layer on top.
|
|
"""
|
|
import json
|
|
from datetime import datetime
|
|
|
|
from app import db
|
|
from app.models.json_column_mixin import JsonColumnMixin
|
|
|
|
|
|
class SystemEvent(JsonColumnMixin, db.Model):
|
|
"""A single event in the unified telemetry stream."""
|
|
|
|
__tablename__ = 'system_events'
|
|
|
|
# Severity levels
|
|
SEVERITY_DEBUG = 'debug'
|
|
SEVERITY_INFO = 'info'
|
|
SEVERITY_WARNING = 'warning'
|
|
SEVERITY_ERROR = 'error'
|
|
SEVERITY_CRITICAL = 'critical'
|
|
VALID_SEVERITIES = [
|
|
SEVERITY_DEBUG,
|
|
SEVERITY_INFO,
|
|
SEVERITY_WARNING,
|
|
SEVERITY_ERROR,
|
|
SEVERITY_CRITICAL,
|
|
]
|
|
|
|
# Common event sources
|
|
SOURCE_AUDIT = 'audit'
|
|
SOURCE_BACKUP = 'backup'
|
|
SOURCE_DEPLOYMENT = 'deployment'
|
|
SOURCE_MONITORING = 'monitoring'
|
|
SOURCE_NOTIFICATION = 'notification'
|
|
SOURCE_WEBHOOK = 'webhook'
|
|
SOURCE_WORKFLOW = 'workflow'
|
|
SOURCE_EVENT = 'event'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
# When the event occurred (may differ from created_at if backdated)
|
|
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True)
|
|
|
|
# Categorization
|
|
source = db.Column(db.String(50), nullable=False, index=True)
|
|
event_type = db.Column(db.String(100), nullable=False, index=True)
|
|
severity = db.Column(db.String(20), nullable=False, default=SEVERITY_INFO, index=True)
|
|
|
|
# Resource scoping
|
|
resource_type = db.Column(db.String(50), nullable=True, index=True)
|
|
resource_id = db.Column(db.String(100), nullable=True, index=True)
|
|
|
|
# Actor scoping
|
|
actor_user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True, index=True)
|
|
workspace_id = db.Column(db.Integer, db.ForeignKey('workspaces.id'), nullable=True, index=True)
|
|
|
|
# Human-readable summary
|
|
message = db.Column(db.Text, nullable=True)
|
|
|
|
# Correlation / trace ID for grouping related events
|
|
correlation_id = db.Column(db.String(64), nullable=True, index=True)
|
|
|
|
# Free-form JSON payload (sanitized before storage)
|
|
payload_json = db.Column(db.Text, nullable=True)
|
|
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
|
|
|
|
# Relationships
|
|
actor = db.relationship('User', foreign_keys=[actor_user_id])
|
|
workspace = db.relationship('Workspace', foreign_keys=[workspace_id])
|
|
|
|
# Composite indexes for common query patterns
|
|
__table_args__ = (
|
|
db.Index('idx_system_events_source_timestamp', 'source', 'timestamp'),
|
|
db.Index('idx_system_events_type_timestamp', 'event_type', 'timestamp'),
|
|
db.Index('idx_system_events_resource', 'resource_type', 'resource_id'),
|
|
db.Index('idx_system_events_severity_timestamp', 'severity', 'timestamp'),
|
|
)
|
|
|
|
def get_payload(self):
|
|
"""Return parsed payload JSON."""
|
|
return self._json_read('payload_json')
|
|
|
|
def set_payload(self, payload_dict):
|
|
"""Set payload from a dictionary."""
|
|
self.payload_json = json.dumps(payload_dict) if payload_dict is not None else None
|
|
|
|
def to_dict(self, include_payload=True):
|
|
data = {
|
|
'id': self.id,
|
|
'timestamp': self.timestamp.isoformat() if self.timestamp else None,
|
|
'source': self.source,
|
|
'event_type': self.event_type,
|
|
'severity': self.severity,
|
|
'resource_type': self.resource_type,
|
|
'resource_id': self.resource_id,
|
|
'actor_user_id': self.actor_user_id,
|
|
'actor_username': self.actor.username if self.actor else None,
|
|
'workspace_id': self.workspace_id,
|
|
'workspace_name': self.workspace.name if self.workspace else None,
|
|
'message': self.message,
|
|
'correlation_id': self.correlation_id,
|
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
if include_payload:
|
|
data['payload'] = self.get_payload()
|
|
return data
|
|
|
|
def __repr__(self):
|
|
return f'<SystemEvent {self.source}.{self.event_type} {self.severity}>'
|