File indexing completed on 2026-07-22 09:42:05
0001 """Human-facing Snapper report and instrument views."""
0002
0003 import json
0004
0005 from django.http import Http404
0006 from django.shortcuts import get_object_or_404, redirect, render
0007
0008 from snapper_ai.models import CaptureCursor, CurrentComponent, SystemSnap
0009
0010 from ..models import SysConfig, SystemStatus
0011
0012
0013 SCOPES = ('epicprod', 'testbed')
0014 RECENT_SNAP_LIMIT = 100
0015
0016
0017 def _validated_scope(scope):
0018 if scope not in SCOPES:
0019 raise Http404(f'Unknown Snapper scope {scope!r}')
0020 return scope
0021
0022
0023 def _scope_options(scope):
0024 return [
0025 {
0026 'name': name,
0027 'label': 'epicprod' if name == 'epicprod' else 'Testbed',
0028 'active': name == scope,
0029 }
0030 for name in SCOPES
0031 ]
0032
0033
0034 def _dict(value):
0035 return value if isinstance(value, dict) else {}
0036
0037
0038 def _json(value):
0039 return json.dumps(value, indent=2, sort_keys=True, default=str)
0040
0041
0042 def _value_at(data, path):
0043 value = data
0044 for part in str(path or '').split('.'):
0045 if not part or not isinstance(value, dict) or part not in value:
0046 return None
0047 value = value[part]
0048 return value
0049
0050
0051 def _quantity_values(registration, data):
0052 rows = []
0053 for name, definition in sorted(
0054 _dict(registration.get('quantities')).items()):
0055 definition = _dict(definition)
0056 value = _value_at(data, definition.get('path', name))
0057 is_complex = isinstance(value, (dict, list))
0058 if is_complex:
0059 display = json.dumps(value, indent=2, sort_keys=True)
0060 elif value is None:
0061 display = '—'
0062 else:
0063 display = str(value)
0064 rows.append({
0065 'name': name,
0066 'description': definition.get('description', ''),
0067 'value': display,
0068 'is_complex': is_complex,
0069 })
0070 return rows
0071
0072
0073 def _present_snap_component(name, payload):
0074 payload = _dict(payload)
0075 registration = _dict(payload.get('registration'))
0076 data = _dict(payload.get('data'))
0077 health = None
0078 if name == 'health':
0079 overall = _dict(data.get('overall'))
0080 checks = []
0081 for check_name, check in sorted(_dict(data.get('checks')).items()):
0082 check = _dict(check)
0083 checks.append({
0084 'name': check_name,
0085 'category': check.get('category', ''),
0086 'status': check.get('status', 'unknown'),
0087 'summary': check.get('summary', ''),
0088 })
0089 health = {
0090 'status': overall.get('status', 'unknown'),
0091 'reason': overall.get('reason', ''),
0092 'counts': _dict(overall.get('counts')),
0093 'checks': checks,
0094 }
0095 return {
0096 'name': name,
0097 'title': registration.get('title') or name,
0098 'description': registration.get('description', ''),
0099 'revision': payload.get('revision'),
0100 'registration_version': payload.get('registration_version'),
0101 'assessed_at': payload.get('assessed_at'),
0102 'source_as_of': payload.get('source_as_of'),
0103 'accepted_at': payload.get('accepted_at'),
0104 'publisher_identity': payload.get('publisher_identity', ''),
0105 'health': health,
0106 'quantities': _quantity_values(registration, data),
0107 'payload_json': _json(payload),
0108 }
0109
0110
0111 def _snap_row(snap):
0112 return {
0113 'id': snap.id,
0114 'snap_time': snap.snap_time,
0115 'observed_at': snap.observed_at,
0116 'reasons': ', '.join(snap.reasons or []) or '—',
0117 'changed_components': (
0118 ', '.join(snap.changed_components or []) or '—'),
0119 'capture_policy': snap.capture_policy,
0120 'encoding': snap.encoding,
0121 'component_count': len(snap.component_revisions or {}),
0122 }
0123
0124
0125 def snapper_root(request):
0126 return redirect('monitor_app:snapper_report', scope='epicprod')
0127
0128
0129 def snapper_report(request, scope, snap_id=None):
0130 scope = _validated_scope(scope)
0131 snaps = SystemSnap.objects.filter(scope=scope).order_by('-snap_time')
0132 latest_snap = snaps.first()
0133 if snap_id is None:
0134 selected_snap = latest_snap
0135 else:
0136 selected_snap = get_object_or_404(snaps, id=snap_id)
0137
0138 components = []
0139 observation_delay = None
0140 if selected_snap is not None:
0141 state = _dict(selected_snap.state)
0142 components = [
0143 _present_snap_component(name, payload)
0144 for name, payload in sorted(
0145 _dict(state.get('components')).items())
0146 ]
0147 observation_delay = (
0148 selected_snap.observed_at - selected_snap.snap_time
0149 ).total_seconds()
0150
0151 recent = list(snaps[:RECENT_SNAP_LIMIT])
0152 return render(request, 'monitor_app/snapper.html', {
0153 'active_tab': 'report',
0154 'scope': scope,
0155 'scope_label': 'epicprod' if scope == 'epicprod' else 'Testbed',
0156 'scope_options': _scope_options(scope),
0157 'selected_snap': selected_snap,
0158 'latest_snap': latest_snap,
0159 'observation_delay': observation_delay,
0160 'components': components,
0161 'selected_snap_json': (
0162 _json(selected_snap.state) if selected_snap is not None else ''),
0163 'snap_rows': [_snap_row(snap) for snap in recent],
0164 'snap_count': snaps.count(),
0165 'recent_snap_limit': RECENT_SNAP_LIMIT,
0166 })
0167
0168
0169 def _positive_int(config, key, default, minimum):
0170 raw = config.get(key, default)
0171 try:
0172 value = int(raw)
0173 if value < minimum:
0174 raise ValueError
0175 except (TypeError, ValueError):
0176 return raw, None
0177 return raw, value
0178
0179
0180 def _duration(seconds):
0181 if seconds is None:
0182 return 'unavailable'
0183 minutes, remainder = divmod(seconds, 60)
0184 if minutes and remainder:
0185 return f'{minutes}m {remainder}s'
0186 if minutes:
0187 return f'{minutes}m'
0188 return f'{remainder}s'
0189
0190
0191 def _component_registration(component):
0192 registration = _dict(component.registration)
0193 quantities = []
0194 for name, definition in sorted(
0195 _dict(registration.get('quantities')).items()):
0196 definition = _dict(definition)
0197 limits = []
0198 for key, label in (
0199 ('max_items', 'max items'),
0200 ('max_length', 'max length'),
0201 ('minimum', 'minimum'),
0202 ('maximum', 'maximum')):
0203 if key in definition:
0204 limits.append(f'{label}: {definition[key]}')
0205 if definition.get('enum'):
0206 limits.append('values: ' + ', '.join(
0207 str(value) for value in definition['enum']))
0208 quantities.append({
0209 'name': name,
0210 'path': definition.get('path', name),
0211 'kind': definition.get('kind', ''),
0212 'type': definition.get('type', ''),
0213 'required': bool(definition.get('required')),
0214 'limits': '; '.join(limits) or '—',
0215 'description': definition.get('description', ''),
0216 })
0217 event_sources = []
0218 for source in registration.get('event_sources') or []:
0219 source = _dict(source)
0220 event_sources.append({
0221 'name': source.get('name', ''),
0222 'kind': source.get('event_kind', ''),
0223 'resolver': source.get('resolver', ''),
0224 'owner': source.get('owner', ''),
0225 'visibility': source.get('visibility', ''),
0226 })
0227 return {
0228 'record': component,
0229 'title': registration.get('title') or component.name,
0230 'description': registration.get('description', ''),
0231 'owning_subsystem': registration.get('owning_subsystem', ''),
0232 'visibility': registration.get('visibility', ''),
0233 'assessment_policy': registration.get('assessment_policy', ''),
0234 'max_serialized_bytes': registration.get('max_serialized_bytes'),
0235 'quantities': quantities,
0236 'event_sources': event_sources,
0237 'registration_json': _json(registration),
0238 }
0239
0240
0241 def snapper_system(request, scope):
0242 scope = _validated_scope(scope)
0243 config = SysConfig.get_config()
0244 opportunity_key = f'snapper_opportunity_seconds_{scope}'
0245 baseline_key = f'snapper_baseline_every_{scope}'
0246 policy_key = f'snapper_capture_policy_{scope}'
0247 opportunity_raw, opportunity = _positive_int(
0248 config, opportunity_key, 10, 10)
0249 baseline_raw, baseline = _positive_int(config, baseline_key, 10, 1)
0250 lock_raw, lock_timeout = _positive_int(
0251 config, 'snapper_lock_timeout_ms', 5000, 1)
0252 max_quiet = (
0253 opportunity * baseline
0254 if opportunity is not None and baseline is not None else None)
0255
0256 cursor = (
0257 CaptureCursor.objects.select_related('latest_snap')
0258 .filter(scope=scope).first()
0259 )
0260 scheduler_status = SystemStatus.objects.filter(
0261 name=f'snapper-{scope}-scheduler').first()
0262 components = [
0263 _component_registration(component)
0264 for component in CurrentComponent.objects.filter(scope=scope)
0265 .order_by('-active', 'name')
0266 ]
0267 policy_rows = [
0268 {
0269 'setting': 'Snap opportunity',
0270 'value': f'{opportunity}s' if opportunity is not None else opportunity_raw,
0271 'key': opportunity_key,
0272 'valid': opportunity is not None,
0273 },
0274 {
0275 'setting': 'Periodic baseline',
0276 'value': (
0277 f'every {baseline} opportunities'
0278 if baseline is not None else baseline_raw),
0279 'key': baseline_key,
0280 'valid': baseline is not None,
0281 },
0282 {
0283 'setting': 'Maximum quiet interval',
0284 'value': _duration(max_quiet),
0285 'key': 'derived from opportunity × baseline',
0286 'valid': max_quiet is not None,
0287 },
0288 {
0289 'setting': 'Capture policy',
0290 'value': config.get(policy_key, f'{scope}-v1'),
0291 'key': policy_key,
0292 'valid': bool(config.get(policy_key, f'{scope}-v1')),
0293 },
0294 {
0295 'setting': 'Database lock timeout',
0296 'value': (
0297 f'{lock_timeout} ms' if lock_timeout is not None else lock_raw),
0298 'key': 'snapper_lock_timeout_ms',
0299 'valid': lock_timeout is not None,
0300 },
0301 ]
0302 return render(request, 'monitor_app/snapper.html', {
0303 'active_tab': 'system',
0304 'scope': scope,
0305 'scope_label': 'epicprod' if scope == 'epicprod' else 'Testbed',
0306 'scope_options': _scope_options(scope),
0307 'cursor': cursor,
0308 'scheduler_status': scheduler_status,
0309 'policy_rows': policy_rows,
0310 'components': components,
0311 'baseline_every': baseline,
0312 })