Warning, /swf-monitor/docs/ACTION_STREAM.md is written in an unsupported language. File is not indexed.
0001 # Action Stream
0002
0003 The action stream is platform machinery: the structured record of what a
0004 system's services do, riding the logging service. epicprod is its first and
0005 principal user — one record per production action — submissions, task operations, sweeps, imports,
0006 configuration edits, assessments — capturing the action, its subject, the
0007 outcome, and the measured duration. It is the raw material for the live view,
0008 for the coming digests and alarms, and for LLM assessment and reporting: the
0009 corpus AI reasons over when it answers "what happened".
0010
0011 
0012
0013 Companion docs: [EPICPROD_OPS_AGENT.md](https://github.com/BNLNPPS/swf-epicprod/blob/main/docs/EPICPROD_OPS_AGENT.md) (the agent
0014 whose handlers emit most records), [EPICPROD_OPS.md](https://github.com/BNLNPPS/swf-epicprod/blob/main/docs/EPICPROD_OPS.md) (the
0015 nightly catalog-sync runbook entry). The system-level description lives in the
0016 [ePIC WFMS documentation](https://epic-wfms-docs.readthedocs.io).
0017
0018 ## The stream
0019
0020 Records are `AppLog` rows with `app_name='epicprod'` — the action stream is a
0021 namespace within the existing DB-backed swf logging, filterable everywhere
0022 logs are filterable. `instance_name` is the component that performed the
0023 action: `web`, `ops-agent`, `mcp`, `report` (as they come). Log `level`
0024 keeps its universal meaning (INFO/ERROR) and is never repurposed.
0025
0026 Structured fields live in `extra_data`. Reserved keys:
0027
0028 | key | meaning |
0029 |---|---|
0030 | `action` | action identifier, e.g. `task_submit`, `rucio_sweep` |
0031 | `subject_type` / `subject_key` | acted-on object (assessment subject types where applicable) |
0032 | `username` | human or service account driving the action |
0033 | `outcome` | `ok`, `error`, `timeout`, `skipped`, `unrecorded` |
0034 | `reason` | short failure cause, required on every non-ok outcome (last stderr line, `rc=N`, or timeout note); also appended to the message text |
0035 | `duration_ms` | measured execution time — required in spirit for sweeps and timed operations |
0036 | `sublevel` | declared importance (below) |
0037 | `live_default` | declared live-stream recommendation (below) |
0038
0039 Any additional keys are free counts and context (`rows_added=…`,
0040 `jedi_task_id=…`, `summary=…`).
0041
0042 ## Publication axes: sublevel and live
0043
0044 Two independent axes govern publication; neither touches log level.
0045
0046 **`sublevel`** — the event's importance, declared at the call site,
0047 AUTHORITATIVE: changing it means changing the event, in code, in git. It says
0048 *which humans* an event reaches (the UI presents it as **Importance**, and
0049 filters on it as an at-or-above **importance threshold**):
0050
0051 - `high` — everyone, including digest and email audiences (submissions,
0052 sweeps, failures, configuration changes)
0053 - `normal` — live-page watchers (fetches, syncs, assessments)
0054 - `low` — deliberately verbose viewers only (routine mechanics)
0055
0056 **`live`** — a special category: "interesting to some humans, now." Each
0057 event declares a `live_default` recommendation; the effective decision is the
0058 `epicprod_live_policy` override registry in SysConfig — the runtime attention
0059 knob, flipped per action on the [live-policy page](#consuming-the-stream)
0060 without a deploy. The two axes are genuinely independent: a low-sublevel
0061 action can be temporarily fascinating (force it live while you watch), and a
0062 high-sublevel bulk operation can be force-quieted while it floods through.
0063
0064 A **channel** is an importance threshold applied to live events:
0065 `live_stream_q(min_sublevel)` in `monitor_app/epicprod_logging.py` is the one
0066 filter every channel uses. Current and planned channels:
0067
0068 | channel | filter | status |
0069 |---|---|---|
0070 | Logs page live view | live, all sublevels | operating |
0071 | Mattermost `#epicprod-live` | live, `normal`+ | operating (publisher below) |
0072 | Hourly/daily email digest | live, `high` (+ ERROR) | planned |
0073 | RSS | live, `normal`+ | planned |
0074
0075 ## Recording actions (developers)
0076
0077 Every state-changing or operationally significant action records exactly one
0078 record per outcome path. The enqueue-vs-execute rule: log at *execution*, with
0079 the requesting username carried in the message; the web tier logs only
0080 enqueue failures.
0081
0082 In-process (web views, services, MCP tools):
0083
0084 ```python
0085 from monitor_app.epicprod_logging import log_epicprod_action
0086
0087 log_epicprod_action(
0088 'web', 'campaign_set_current',
0089 subject_type='campaign', subject_key=campaign.name,
0090 username=request.user.username,
0091 sublevel='high', live_default=True,
0092 )
0093 ```
0094
0095 From the ops agent (out of process — REST twin with identical semantics):
0096
0097 ```python
0098 t0 = time.monotonic()
0099 ...run the doer...
0100 self._log_action('rucio_sweep', t0, outcome='ok',
0101 username=str(m.get('created_by') or ''),
0102 sublevel='high', live_default=True,
0103 datasets_updated=n)
0104 ```
0105
0106 Rules of the road:
0107
0108 1. Declare every new action in `ACTION_DEFAULTS`
0109 (`monitor_app/epicprod_logging.py`) — the greppable catalog the live-policy
0110 page reads. It MUST mirror the call sites. Every declaration carries a
0111 plain-English `description` — the one-liner answering "what is this
0112 action", rendered on the log entry page and the live-policy page so a
0113 stream reader is never left guessing what an event was.
0114 2. Timed operations pass the start time; every sweep reports its execution
0115 time to the log.
0116 3. Failed outcomes log at `level=logging.ERROR` and carry `reason` — an
0117 `outcome: error` with no why is itself a silent failure. The web and MCP
0118 tiers put the cause in the message text; the agent's `_log_action` takes
0119 `reason=` and exposes it in both the record and the message.
0120 4. Requester identity travels in the message (`created_by`, `owner`,
0121 `requested_by`) and is recorded at execution. Anonymous open-face requests
0122 record an empty username.
0123 5. The logging call never raises; a failed write is logged and the action
0124 proceeds.
0125
0126 ## Consuming the stream
0127
0128 **Operators.** The Logs page (`/logs/`, Logs in the epicprod nav, pre-filtered
0129 `?app_name=epicprod`) is the first live channel: the *Live stream* toggle
0130 (`?live=1`) shows live events with 30-second auto-refresh. Each record's log
0131 entry page embeds the action's catalog description, so what an `evgen_sweep`
0132 or `catalog_sync` *is* reads directly off the record. The
0133 [live-policy page](../src/monitor_app/templates/monitor_app/live_policy.html)
0134 at `/logs/live-policy/` lists every known action with its declared sublevel,
0135 live default, current override, and effective state — overrides editable in
0136 place when signed in, each save itself logged (`live_policy_edit`).
0137
0138 **Mattermost.** The `#epicprod-live` channel is fed by the publisher
0139 (`manage.py publish_epicprod_live`, systemd unit `swf-epicprod-live`) — a
0140 polling tailer that posts one compact message per live event: timestamp,
0141 action, subject, component, outcome, failure reason, duration, and a link
0142 to the log record. Assessment registrations instead use the report title as
0143 a direct link to its dedicated epicprod page, followed only by cadence,
0144 campaign, verdict, and record link; the report body is never copied into the
0145 channel. It posts as the dedicated `epicprod` bot account
0146 (`EPICPROD_LIVE_TOKEN`, falling back to the DISpatcher token until set);
0147 plain channel posts never wake DISpatcher. To follow up on an event, @mention
0148 DISpatcher in a thread under the event post — the post carries everything
0149 the bot needs to pull the full record and drill into the subject. The
0150 publisher re-reads its SysConfig knobs every cycle (`epicprod_live_channel`,
0151 `epicprod_live_min_sublevel`, `epicprod_live_poll_seconds`), so channel
0152 rename, importance threshold, and cadence are UI adjustments, no deploy. A
0153 per-cycle post cap (20) guards against floods; overflow is posted as a
0154 counted summary line, never silently dropped.
0155
0156 **LLMs and bots.** `epicprod_list_actions` is the purpose-built MCP tool —
0157 prefer `summarize=True` (counts by action with ok/error split and duration
0158 statistics) for reporting and assessment; filters on action, instance,
0159 subject, username, outcome, and time window for drill-down. `swf_list_logs
0160 (app_name='epicprod')` returns raw records. Raw listings apply an importance
0161 floor of `normal` by default so routine low-importance mechanics do not
0162 drown the real actions; the excluded records are reported as a count
0163 (`excluded_below_floor`), the floor lifts automatically for
0164 action/subject/outcome drill-downs and summaries, and `min_sublevel` sets it
0165 explicitly. Tool ergonomics are sized for the
0166 smallest consumer (the bot runs a small model): summarize-first, prescriptive
0167 docstrings.
0168
0169 **Alarms and reports (next).** The stream is the data source for the
0170 catalog-sync freshness alarm and the payload-fetch rate alarm, and the
0171 what-happened section of the automated daily/weekly reports. The
0172 born-vs-adopted task ratio (`created_by='association_sweep'` marks adopted
0173 tasks) is the standing migration metric.
0174
0175 ## Scheduled automation on the stream
0176
0177 The nightly `catalog_sync` (cron 02:15 → `enqueue-ops-message.py` → ops
0178 agent) chains: csv catalog import → questionnaire import → association sweep
0179 with auto-intake of direct group.EIC submissions → Rucio output snapshot →
0180 EVGEN assimilation → questionnaire automatch (new matches are live
0181 `questionnaire_match_found` events) → questionnaire match cache → progress
0182 refresh. Each step logs
0183 its own record with duration; the chain logs a summary record — the
0184 catalog-freshness timestamp. Measured 2026-07-05: csv 8 s, association sweep
0185 2.3 s (14-day window), Rucio snapshot 36 s, EVGEN 16 s, match 2 s, progress
0186 5 s. Runbook: [EPICPROD_OPS.md](https://github.com/BNLNPPS/swf-epicprod/blob/main/docs/EPICPROD_OPS.md#nightly-catalog-sync).
0187
0188 ## SysConfig
0189
0190 `SysConfig` (`swf_sys_config`) is the single-record JSON document of
0191 operator-set configuration — live policy overrides, channel settings
0192 (`epicprod_live_channel`, `epicprod_live_min_sublevel`,
0193 `epicprod_live_poll_seconds`), sweep knobs (`questionnaire_csv_url`) —
0194 viewable and editable at the bottom of the System page. Convention: no
0195 hidden knobs — every key a component reads from SysConfig is present in the
0196 document, at its code default when never overridden, so the System page is
0197 the complete inventory of what is adjustable. The construct
0198 `get_config().get(key, default)` is forbidden: an unset value silently
0199 becoming a hidden code default is a silent failure. Reads go through
0200 `SysConfig.get_setting(key, default)`, which seeds a missing key into the
0201 document (logged as a `sysconfig_edit` action) and returns it; an explicitly
0202 set but unusable value is logged by the reader, never silently replaced. The live policy additionally carries an explicit
0203 `endpoints_update: live` override, although live is that action's default,
0204 as the worked example of an override entry. It is distinct from `PersistentState`, which is
0205 machine-maintained state (counters, run numbers) and not for human editing.
0206 All system configuration lives in the database and is adjustable through the
0207 UI without deploys; SysConfig edits are themselves live actions in the stream.