mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 23:57:58 +09:00
328 more routes used a JSON body as a dict straight from
request.get_json(), so a body of null, [] or a bare string crashed them
into a 500. Bare reads become json_object(); the
'get_json(silent=True) or {}' reads of action routes become
json_object(optional=True), which still treats a missing body as {}.
Routes that take arrays or pass the body through untouched are left
alone. The full backend suite shows no new failures.
Also carries three files' bare section citations (apps, backups,
notifications) and the extended citation guard, which needed them.
150 lines
5.2 KiB
Python
150 lines
5.2 KiB
Python
"""Telemetry / system event stream API."""
|
|
from datetime import datetime, timedelta
|
|
|
|
from flask import Blueprint, request, jsonify
|
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|
from sqlalchemy import func
|
|
|
|
from app.api._query import PageQuery
|
|
from app import db
|
|
from app.middleware.rbac import admin_required
|
|
from app.models.system_event import SystemEvent
|
|
from app.services.telemetry_service import TelemetryService
|
|
from app.api._request import json_object
|
|
|
|
telemetry_bp = Blueprint('telemetry', __name__)
|
|
|
|
|
|
@telemetry_bp.route('/events', methods=['GET'])
|
|
@jwt_required()
|
|
def list_events():
|
|
"""List system events with filtering and pagination."""
|
|
paging = PageQuery.from_request(request, max_per_page=200)
|
|
source = request.args.get('source') or None
|
|
event_type = request.args.get('event_type') or None
|
|
severity = request.args.get('severity') or None
|
|
resource_type = request.args.get('resource_type') or None
|
|
resource_id = request.args.get('resource_id') or None
|
|
correlation_id = request.args.get('correlation_id') or None
|
|
message_query = request.args.get('q') or None
|
|
|
|
start_date = _parse_iso_datetime(request.args.get('start_date'))
|
|
end_date = _parse_iso_datetime(request.args.get('end_date'))
|
|
|
|
pagination = TelemetryService.get_events(
|
|
page=paging.page,
|
|
per_page=paging.per_page,
|
|
source=source,
|
|
event_type=event_type,
|
|
severity=severity,
|
|
resource_type=resource_type,
|
|
resource_id=resource_id,
|
|
correlation_id=correlation_id,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
message_query=message_query,
|
|
)
|
|
|
|
return jsonify({
|
|
'events': [event.to_dict(include_payload=True) for event in pagination.items],
|
|
'total': pagination.total,
|
|
'pages': pagination.pages,
|
|
'page': pagination.page,
|
|
'per_page': pagination.per_page,
|
|
}), 200
|
|
|
|
|
|
@telemetry_bp.route('/events/<int:event_id>', methods=['GET'])
|
|
@jwt_required()
|
|
def get_event(event_id):
|
|
"""Get a single system event by ID."""
|
|
event = TelemetryService.get_event_by_id(event_id)
|
|
if not event:
|
|
return jsonify({'error': 'Event not found'}), 404
|
|
return jsonify(event.to_dict(include_payload=True)), 200
|
|
|
|
|
|
@telemetry_bp.route('/events/by-correlation/<correlation_id>', methods=['GET'])
|
|
@jwt_required()
|
|
def get_events_by_correlation(correlation_id):
|
|
"""Get all events sharing a correlation ID."""
|
|
limit = request.args.get('limit', 100, type=int)
|
|
events = TelemetryService.get_events_by_correlation(correlation_id, limit=limit)
|
|
return jsonify({
|
|
'correlation_id': correlation_id,
|
|
'events': [event.to_dict(include_payload=True) for event in events],
|
|
}), 200
|
|
|
|
|
|
@telemetry_bp.route('/stats', methods=['GET'])
|
|
@jwt_required()
|
|
def get_stats():
|
|
"""Get telemetry aggregate stats."""
|
|
hours = request.args.get('hours', 24, type=int)
|
|
source = request.args.get('source') or None
|
|
stats = TelemetryService.get_stats(hours=hours, source=source)
|
|
stats['recent_event_types'] = TelemetryService.get_recent_event_types()
|
|
return jsonify(stats), 200
|
|
|
|
|
|
@telemetry_bp.route('/sources', methods=['GET'])
|
|
@jwt_required()
|
|
def list_sources():
|
|
"""Return all distinct event sources."""
|
|
sources = (db.session.query(SystemEvent.source)
|
|
.distinct()
|
|
.order_by(SystemEvent.source)
|
|
.all())
|
|
return jsonify({'sources': [s[0] for s in sources if s[0]]}), 200
|
|
|
|
|
|
@telemetry_bp.route('/event-types', methods=['GET'])
|
|
@jwt_required()
|
|
def list_event_types():
|
|
"""Return all distinct event types, optionally filtered by source."""
|
|
source = request.args.get('source') or None
|
|
query = db.session.query(SystemEvent.event_type).distinct()
|
|
if source:
|
|
query = query.filter(SystemEvent.source == source)
|
|
types = query.order_by(SystemEvent.event_type).all()
|
|
return jsonify({'event_types': [t[0] for t in types if t[0]]}), 200
|
|
|
|
|
|
@telemetry_bp.route('/events', methods=['DELETE'])
|
|
@admin_required
|
|
def cleanup_events():
|
|
"""Delete events older than the specified number of days (admin only)."""
|
|
days = request.args.get('days', 90, type=int)
|
|
deleted = TelemetryService.cleanup_old_events(days=days)
|
|
return jsonify({'deleted': deleted}), 200
|
|
|
|
|
|
@telemetry_bp.route('/events/test', methods=['POST'])
|
|
@admin_required
|
|
def emit_test_event():
|
|
"""Emit a test telemetry event (admin only)."""
|
|
data = json_object(optional=True)
|
|
user_id = get_jwt_identity()
|
|
event = TelemetryService.emit(
|
|
source=data.get('source', 'system'),
|
|
event_type=data.get('event_type', 'telemetry.test'),
|
|
message=data.get('message', 'Test telemetry event'),
|
|
severity=data.get('severity', 'info'),
|
|
actor_user_id=user_id,
|
|
payload=data.get('payload', {'test': True}),
|
|
commit=True,
|
|
)
|
|
if event:
|
|
return jsonify(event.to_dict(include_payload=True)), 201
|
|
return jsonify({'error': 'Failed to emit test event'}), 500
|
|
|
|
|
|
def _parse_iso_datetime(value):
|
|
"""Parse an ISO datetime string, returning None on failure."""
|
|
if not value:
|
|
return None
|
|
try:
|
|
return datetime.fromisoformat(value.replace('Z', '+00:00')).replace(tzinfo=None)
|
|
except (ValueError, TypeError):
|
|
return None
|