File indexing completed on 2026-09-01 09:34:26
0001 """Push-plugin delivery for the notice router (docs/NOTICE_ROUTING.md).
0002
0003 A push plugin is a named in-process delivery mode: a subscription whose
0004 ``delivery`` value is a plugin name has each matched event handed to that
0005 plugin during the routing pass. The router drives the pass protocol:
0006 ``start_pass()`` before a plugin's first delivery of the pass,
0007 ``deliver(row, extra)`` per matched event, ``end_pass()`` after the scan.
0008
0009 Push delivery is at-most-once: a delivery failure is logged and the pass
0010 continues — the routing mark advances regardless, so a push outage never
0011 stalls buffered-pull delivery. The event remains on the log record page.
0012
0013 The first plugin is ``mattermost-live``, the #epicprod-live channel
0014 publisher moved here from the publish_epicprod_live command with its
0015 formatting unchanged. Its channel is the SysConfig
0016 ``epicprod_live_channel`` knob, re-read every pass; its event selection
0017 is the ``epicprod-live`` subscription, not code.
0018 """
0019 import logging
0020 import os
0021 import re
0022
0023 import requests
0024 from django.utils import timezone
0025
0026 logger = logging.getLogger(__name__)
0027
0028 MM_URL = os.environ.get('MATTERMOST_URL', 'chat.epic-eic.org')
0029
0030
0031 MM_TOKEN = (os.environ.get('EPICPROD_LIVE_TOKEN')
0032 or os.environ.get('MATTERMOST_TOKEN', ''))
0033 MM_TEAM = os.environ.get('MATTERMOST_TEAM', 'main')
0034
0035 DEFAULT_CHANNEL = 'epicprod-live'
0036 PASS_POST_MAX = 20
0037 HTTP_TIMEOUT = 15
0038
0039
0040 def _link_base():
0041 """Open-face link base so event links work for the whole
0042 collaboration: env override, else the external-face configuration
0043 point plus the production path."""
0044 env = os.environ.get('EPICPROD_LIVE_LINK_BASE')
0045 if env:
0046 return env.rstrip('/')
0047 from monitor_app.models import external_face_base_url
0048 return f"{external_face_base_url()}/prod"
0049
0050
0051 class MattermostLivePlugin:
0052 """The #epicprod-live channel as a router delivery mode."""
0053
0054 UUID_RE = re.compile(
0055 r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
0056 re.IGNORECASE)
0057
0058 def __init__(self):
0059 self.session = None
0060 self.user_id = ''
0061 self.team_id = ''
0062 self.channel_name = ''
0063 self.channel_id = ''
0064 self._posted = 0
0065 self._skipped = 0
0066
0067
0068
0069 def start_pass(self):
0070 """Connect on first use, honor a channel rename, reset the cap."""
0071 self._posted = 0
0072 self._skipped = 0
0073 if self.session is None:
0074 if not MM_TOKEN:
0075 raise RuntimeError(
0076 "EPICPROD_LIVE_TOKEN/MATTERMOST_TOKEN not set")
0077 self.session = requests.Session()
0078 self.session.headers['Authorization'] = f'Bearer {MM_TOKEN}'
0079 self.base = f'https://{MM_URL}/api/v4'
0080 me = self._get('/users/me')
0081 self.user_id = me['id']
0082 self.team_id = self._get(f'/teams/name/{MM_TEAM}')['id']
0083 logger.info("mattermost-live plugin posting as @%s",
0084 me.get('username'))
0085 from monitor_app.models import SysConfig
0086 channel = str(SysConfig.get_setting(
0087 'epicprod_live_channel', DEFAULT_CHANNEL) or '')
0088 if not channel:
0089 logger.warning("epicprod_live_channel is blank in SysConfig; "
0090 "using default %r", DEFAULT_CHANNEL)
0091 channel = DEFAULT_CHANNEL
0092 if channel != self.channel_name:
0093 self.channel_id = self._get(
0094 f'/teams/{self.team_id}/channels/name/{channel}')['id']
0095 self.channel_name = channel
0096 try:
0097 self.session.post(
0098 f'{self.base}/channels/{self.channel_id}/members',
0099 json={'user_id': self.user_id},
0100 timeout=HTTP_TIMEOUT).raise_for_status()
0101 except Exception:
0102 logger.warning("could not self-join #%s", channel)
0103 logger.info("publishing to #%s (%s)", channel, self.channel_id)
0104
0105 def deliver(self, row, extra):
0106 if self._posted >= PASS_POST_MAX:
0107 self._skipped += 1
0108 return
0109 self._post(self._format(row, extra))
0110 self._posted += 1
0111
0112 def end_pass(self):
0113 if self._skipped:
0114 self._post(
0115 f"… and {self._skipped} more events this pass — see the "
0116 f"[live view]({_link_base()}/logs/?app_name=epicprod&live=1)")
0117
0118
0119
0120 def _format(self, row, extra):
0121 """One readable line per event: what happened, to what, by whom,
0122 and the one explanation that matters. Machine tokens stay on the
0123 record page — UUID subjects, the emitting component, and
0124 sub-10-second timings carry nothing for a channel reader."""
0125 action = extra.get('action') or row.funcname or 'action'
0126 outcome = str(extra.get('outcome') or '')
0127 if action == 'assessment_register' and outcome == 'ok':
0128 notice = self._format_assessment(row, extra)
0129 if notice:
0130 return notice
0131 subject_type = str(extra.get('subject_type') or '').replace('_', ' ')
0132 subject_key = str(extra.get('subject_key') or '')
0133 subject_label = str(extra.get('subject_label') or '')
0134 if subject_label:
0135 subject = subject_label
0136 else:
0137 if self.UUID_RE.search(subject_key):
0138 subject_key = ''
0139 subject = f'{subject_type} {subject_key}'.strip()
0140 username = str(extra.get('username') or '')
0141 reason = str(extra.get('reason') or '')
0142 summary = str(extra.get('summary') or '')
0143 dur = extra.get('duration_ms')
0144 stamp = timezone.localtime(row.timestamp).strftime('%H:%M')
0145
0146
0147
0148 title = str(extra.get('operation') or action).replace('_', ' ')
0149 parts = [f"`{stamp}`", f"**{title}**"]
0150 if subject:
0151 parts.append(subject)
0152 if username:
0153 parts.append(f"by {username}")
0154 if outcome and outcome != 'ok':
0155 parts.append(f"⚠️ **{outcome.upper()}**")
0156 explanation = reason if (outcome and outcome != 'ok' and reason) \
0157 else summary
0158 if explanation:
0159 parts.append(explanation)
0160 if isinstance(dur, (int, float)) and dur >= 10000:
0161 parts.append(f"{dur / 1000:.1f} s")
0162 parts.append(f"[record]({_link_base()}/logs/{row.id}/)")
0163 return ' · '.join(parts)
0164
0165 def _format_assessment(self, row, extra):
0166 """Linked publication notice; never duplicate the report body."""
0167 title = ' '.join(str(extra.get('report_title') or '').split())
0168 path = str(extra.get('report_path') or '').strip()
0169 if not title or not path.startswith('/ai/assessments/'):
0170 return ''
0171 url = f"{_link_base()}/{path.lstrip('/')}"
0172 stamp = timezone.localtime(row.timestamp).strftime('%H:%M %Z')
0173 subject_type = str(extra.get('subject_type') or '').strip()
0174 subject_key = str(extra.get('subject_key') or '').strip()
0175 subject = ''
0176 if subject_type and subject_key:
0177 subject = f'{subject_type.replace("_", " ").title()} {subject_key}'
0178 elif subject_key:
0179 subject = subject_key
0180 kind = str(extra.get('assessment_kind') or '').strip().lower()
0181 if kind == 'nightly':
0182 kind = 'daily'
0183 kind_label = kind.replace('_', ' ').title()
0184 verdict = str(extra.get('verdict') or '').strip()
0185 publication = (f'{kind_label} AI assessment published'
0186 if kind_label else 'AI assessment published')
0187 parts = [f'`{stamp}`', f'**{publication}**']
0188 if subject:
0189 parts.append(subject)
0190 if verdict:
0191 verdict_text = f'Verdict: **{verdict.capitalize()}**'
0192 standing = extra.get('verdict_standing')
0193 if isinstance(standing, dict):
0194 prior = int(standing.get('prior_consecutive') or 0)
0195 if prior >= 1:
0196 verdict_text += f' (standing, {prior + 1} consecutive)'
0197 parts.append(verdict_text)
0198 parts.append(f'[record]({_link_base()}/logs/{row.id}/)')
0199 notice = f'### [{title}]({url})\n' + ' · '.join(parts)
0200 narration = ' '.join(str(extra.get('narration') or '').split())
0201 if narration:
0202 notice += f'\n{narration}'
0203 return notice
0204
0205
0206
0207 def _get(self, path):
0208 r = self.session.get(self.base + path, timeout=HTTP_TIMEOUT)
0209 r.raise_for_status()
0210 return r.json()
0211
0212 def _post(self, message):
0213 r = self.session.post(self.base + '/posts', timeout=HTTP_TIMEOUT,
0214 json={'channel_id': self.channel_id,
0215 'message': message})
0216 r.raise_for_status()
0217
0218
0219 PLUGINS = {'mattermost-live': MattermostLivePlugin()}