Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-21 08:46:27

0001 """epicprod action logging — the filterable action stream in AppLog.
0002 
0003 Convention: ``app_name='epicprod'`` is the epicprod ACTION stream. Every
0004 state-changing or operationally significant action — a catalog button press,
0005 an MCP action tool, an ops-agent handler, a sweep, a submission, a report
0006 generation — records one row here, regardless of which process performed it.
0007 Process and infrastructure logs stay under their own app names; this stream
0008 answers "what happened, and how did it go", and is the primary corpus
0009 for LLM assessment and reporting.
0010 
0011 ``instance_name`` names the component performing the action: 'web',
0012 'ops-agent', 'mcp', 'catalog-sync', 'submit', 'report'.
0013 
0014 Structured fields live in ``extra_data``; reserved keys are ``action``,
0015 ``subject_type``, ``subject_key``, ``username``, ``outcome``, and
0016 ``duration_ms``. Additional numeric counts (rows_added=..., tasks_matched=...)
0017 are stored alongside them. Timed actions (sweeps in particular) must pass
0018 ``duration_ms``.
0019 
0020 This helper writes via the ORM and serves in-process Django contexts (web
0021 tier, MCP ASGI worker, management commands). Out-of-process callers such as
0022 the ops agent record actions through the existing REST log endpoint with the
0023 same app_name and field conventions.
0024 
0025 Retrieval: the ``epicprod_list_actions`` MCP tool (filtered and summarized),
0026 ``swf_list_logs(app_name='epicprod')`` (raw), and the Logs UI filtered on
0027 app_name.
0028 
0029 Publication axes (log level is separate and keeps its universal meaning):
0030 
0031 ``sublevel`` — the event's declared importance (high | normal | low),
0032 set at the call site, AUTHORITATIVE: changing it means changing the event.
0033 It says which humans an event reaches — high reaches everyone including
0034 email/digest audiences, normal reaches live-page watchers, low reaches only
0035 the deliberately verbose viewer.
0036 
0037 ``live_default`` — the event's declared RECOMMENDATION for the live stream,
0038 a special category: "interesting to some humans, now". The effective live
0039 decision is the ``epicprod_live_policy`` override registry in SysConfig
0040 (runtime attention knob, flipped on the live-policy page) over the default.
0041 
0042 A channel = its importance threshold applied to live events:
0043 ``live_stream_q(min_sublevel=...)`` is the one filter every channel uses —
0044 the deep live view takes all live events; an email digest takes live events
0045 at importance high.
0046 """
0047 
0048 import logging
0049 import os
0050 import threading
0051 
0052 from django.utils import timezone
0053 
0054 logger = logging.getLogger(__name__)
0055 
0056 EPICPROD_APP_NAME = 'epicprod'
0057 
0058 LIVE_POLICY_STATE_KEY = 'epicprod_live_policy'
0059 
0060 SUBLEVEL_VALUES = ('high', 'normal', 'low')
0061 SUBLEVEL_ORDER = {'high': 2, 'normal': 1, 'low': 0}
0062 
0063 # Catalog of known actions and their call-site declarations. This MUST mirror
0064 # the call sites (records carry the authoritative stamp; filters read the
0065 # record) — it exists so the live-policy page can show every known action
0066 # before/without records, and so new actions are declared in one greppable
0067 # place. sublevel: high = reaches everyone (news, digests), normal =
0068 # live-page watchers, low = verbose viewers only. live: the declared
0069 # live-stream recommendation, overridable at runtime. description: the
0070 # plain-English one-liner answering "what is this action" — rendered on the
0071 # log entry page and the live-policy page, so a stream reader is never left
0072 # guessing what an event was.
0073 ACTION_DEFAULTS = {
0074     # ops agent
0075     'task_submit': {
0076         'sublevel': 'high', 'live': True,
0077         'description': "Submit a campaign task to PanDA through the prun doer "
0078                        "under the production credential and record the "
0079                        "returned JEDI task id.",
0080     },
0081     'evgen_task_submit': {
0082         'sublevel': 'high', 'live': True,
0083         'description': "Submit an external-EVGEN campaign task to PanDA/JEDI "
0084                        "via the client API: build the task parameters, "
0085                        "assemble and upload the sandbox, record the JEDI "
0086                        "task id.",
0087     },
0088     'panda_task_operation': {
0089         'sublevel': 'high', 'live': True,
0090         'description': "Run a native PanDA operation on an existing JEDI "
0091                        "task: raise the allowed attempt count or retry "
0092                        "failed work.",
0093     },
0094     'rucio_sweep': {
0095         'sublevel': 'high', 'live': True,
0096         'description': "Refresh the JLab Rucio output snapshot for the "
0097                        "current (and last) campaign and rematch produced "
0098                        "RECO/FULL datasets onto each task's recorded outputs.",
0099     },
0100     'campaign_instancing': {
0101         'sublevel': 'high', 'live': True,
0102         'description': "Operator-fired campaign instancing: populate the "
0103                        "target campaign's working catalog from the source "
0104                        "campaign's continuing physics configurations — "
0105                        "adopting ingested editions and minting planned "
0106                        "ones, dispositions consumed, unresolved "
0107                        "identities left to curation.",
0108     },
0109     'campaign_promoted': {
0110         'sublevel': 'high', 'live': True,
0111         'description': "Operator-clicked campaign lifecycle rotation: the "
0112                        "named (producing) campaign becomes current, the "
0113                        "incumbent current becomes last, the incumbent last "
0114                        "becomes past.",
0115     },
0116     'past_import': {
0117         'sublevel': 'high', 'live': True,
0118         'description': "Pull the eic/epic-prod bookkeeping clone and re-run "
0119                        "the idempotent FULL/RECO past-campaign output "
0120                        "ingest, keeping every campaign's recorded "
0121                        "production content current with what the "
0122                        "production team publishes.",
0123     },
0124     'rucio_reconcile': {
0125         'sublevel': 'normal', 'live': True,
0126         'description': "Firsthand reconciliation of a producing "
0127                        "campaign's catalog records against its fetched "
0128                        "Rucio snapshot: known DIDs updated, unknown DIDs "
0129                        "resolved to physics configurations and attached "
0130                        "to existing editions or created, unresolved left "
0131                        "to curation.",
0132     },
0133     'rucio_arrivals': {
0134         'sublevel': 'normal', 'live': True,
0135         'description': "New files landed in JLab Rucio since the last "
0136                        "arrivals sweep, counted by campaign and location "
0137                        "across all versions — the signal behind the "
0138                        "derived 'producing' campaign status. Emitted only "
0139                        "when something arrived.",
0140     },
0141     'rucio_arrivals_sweep': {
0142         'sublevel': 'low', 'live': False,
0143         'description': "The clockwork arrivals-sweep step itself (outcome "
0144                        "and duration); the arrivals, when any, are the "
0145                        "rucio_arrivals event.",
0146     },
0147     'evgen_sweep': {
0148         'sublevel': 'high', 'live': True,
0149         'description': "Assimilate the JLab Rucio EVGEN input inventory "
0150                        "(epic:/EVGEN/*) and resolve each catalog request to "
0151                        "the registered input dataset(s) that realize it.",
0152     },
0153     'catalog_import': {
0154         'sublevel': 'high', 'live': True,
0155         'description': "Import the production CSV manifest catalog: create "
0156                        "or update campaign task records from the source rows.",
0157     },
0158     'association_sweep': {
0159         'sublevel': 'high', 'live': True,
0160         'description': "Associate recent PanDA tasks with campaign tasks; "
0161                        "unmatched direct group.EIC submissions are "
0162                        "auto-intaken as adopted tasks.",
0163     },
0164     'catalog_sync': {
0165         'sublevel': 'high', 'live': True,
0166         'description': "Nightly composite chain (cron 02:15): csv import, "
0167                        "epic-prod past ingest, questionnaire import, "
0168                        "association sweep, Rucio output snapshot, Rucio "
0169                        "arrivals sweep, EVGEN assimilation, questionnaire "
0170                        "automatch, match cache, progress refresh. This "
0171                        "record is the catalog-freshness timestamp.",
0172     },
0173     'agent_shutdown': {
0174         'sublevel': 'high', 'live': True,
0175         'description': "Deliberate stop of the production ops agent via the "
0176                        "message-bus back door; systemd leaves it stopped.",
0177     },
0178     'payload_log_fetch': {
0179         'sublevel': 'normal', 'live': False,
0180         'description': "Fetch one PanDA job's payload log tarball from Rucio "
0181                        "over xrootd and extract it into the shared cache for "
0182                        "the job page.",
0183     },
0184     'inventory_sync': {
0185         'sublevel': 'low', 'live': False,
0186         'description': "Refresh the monitor's ePIC production job/file "
0187                        "inventory and parsed failure diagnosis for a PanDA "
0188                        "job.",
0189     },
0190     'system_status_refresh': {
0191         'sublevel': 'low', 'live': False,
0192         'description': "Periodic refresh of the cached System page status "
0193                        "rows for services, agents, and external monitor "
0194                        "endpoints.",
0195     },
0196     'questionnaire_import': {
0197         'sublevel': 'normal', 'live': True,
0198         'description': "Import the PWG/DSC production-request questionnaire "
0199                        "CSV: create or update request records.",
0200     },
0201     'questionnaire_automatch': {
0202         'sublevel': 'normal', 'live': False,
0203         'description': "LLM matching of production requests to catalog tasks "
0204                        "using the full tag map; each new match logs its own "
0205                        "questionnaire_match_found record, and a run with new "
0206                        "matches posts one live questionnaire_new_matches line.",
0207     },
0208     'questionnaire_match_found': {
0209         'sublevel': 'normal', 'live': False,
0210         'description': "One new request-to-task match from the automatch, "
0211                        "with confidence and reason; high/medium confidence "
0212                        "lands accepted, low lands suggested.",
0213     },
0214     'questionnaire_new_matches': {
0215         'sublevel': 'normal', 'live': True,
0216         'description': "One-line summary of an automatch run's new "
0217                        "request-to-task matches; the record lists every new "
0218                        "match with confidence and reason.",
0219     },
0220     'questionnaire_match': {
0221         'sublevel': 'low', 'live': False,
0222         'description': "Rebuild the questionnaire-to-task match cache read "
0223                        "by request and task pages.",
0224     },
0225     'progress_refresh': {
0226         'sublevel': 'low', 'live': False,
0227         'description': "Rebuild current-campaign progress data and its "
0228                        "rendered progress table cache.",
0229     },
0230     # web and MCP
0231     'sysconfig_edit': {
0232         'sublevel': 'high', 'live': True,
0233         'description': "Operator edit of the SysConfig system-configuration "
0234                        "document on the System page.",
0235     },
0236     'live_policy_edit': {
0237         'sublevel': 'high', 'live': True,
0238         'description': "Operator change to an action's live-stream override "
0239                        "on the live-policy page.",
0240     },
0241     'assessment_register': {
0242         'sublevel': 'normal', 'live': True,
0243         'description': "Register an AI assessment of a production object "
0244                        "(campaign task, PanDA task, job, or queue).",
0245     },
0246     'assessment_link': {
0247         'sublevel': 'low', 'live': False,
0248         'description': "Link an existing AI assessment to a production "
0249                        "object's record.",
0250     },
0251     'task_set_status': {
0252         'sublevel': 'normal', 'live': True,
0253         'description': "Campaign task lifecycle transition (draft, ready, "
0254                        "submitted, completed, failed) with rule enforcement.",
0255     },
0256     'task_intake': {
0257         'sublevel': 'normal', 'live': True,
0258         'description': "Idempotent intake of a production request into a "
0259                        "draft campaign task.",
0260     },
0261     'dataset_intake': {
0262         'sublevel': 'normal', 'live': True,
0263         'description': "Idempotent find-or-create of a dataset from a source "
0264                        "location such as a CSV manifest.",
0265     },
0266     'task_link_input': {
0267         'sublevel': 'low', 'live': False,
0268         'description': "Link an existing dataset as a campaign task's input "
0269                        "by DID.",
0270     },
0271     'dataset_propagation_set': {
0272         'sublevel': 'normal', 'live': True,
0273         'description': "Operator change of dataset propagation disposition "
0274                        "(continue, hold, final) with required comment; one "
0275                        "event per single or bulk action, carrying the "
0276                        "changed count and the selecting filter. Carries an "
0277                        "origin stamp when executing an approved AI proposal.",
0278     },
0279     'proposal_created': {
0280         'sublevel': 'normal', 'live': True,
0281         'description': "AI proposal of a dataset propagation change, pending "
0282                        "human review; one event per propose call with the "
0283                        "proposed count and batch.",
0284     },
0285     'proposal_denied': {
0286         'sublevel': 'normal', 'live': True,
0287         'description': "Human denial of pending AI proposals; denial memory "
0288                        "prevents re-proposal until the proposer's inputs "
0289                        "change.",
0290     },
0291     'proposal_expired': {
0292         'sublevel': 'normal', 'live': True,
0293         'description': "Withdrawal of pending AI proposals (recurring-scan "
0294                        "heartbeat refresh or operator clear), with count.",
0295     },
0296     'proposal_deleted': {
0297         'sublevel': 'normal', 'live': False,
0298         'description': "Operator deletion of AI proposal list rows — "
0299                        "housekeeping for test or noise rows; removes audit "
0300                        "rows, logged with count.",
0301     },
0302     'narrative_edited': {
0303         'sublevel': 'normal', 'live': False,
0304         'description': "Expert revision of a campaign narrative document — "
0305                        "a new corun-ai version of the entry.",
0306     },
0307     'narrative_commented': {
0308         'sublevel': 'normal', 'live': True,
0309         'description': "Comment posted on a campaign narrative — the "
0310                        "non-intrusive contribution path beside editing.",
0311     },
0312     # web entity lifecycle and operator actions
0313     'campaign_set_current': {
0314         'sublevel': 'high', 'live': True,
0315         'description': "Operator designation of the current campaign.",
0316     },
0317     'campaign_set_last': {
0318         'sublevel': 'high', 'live': True,
0319         'description': "Operator designation of the last (previous) campaign.",
0320     },
0321     'questionnaire_match_add': {
0322         'sublevel': 'normal', 'live': True,
0323         'description': "Operator addition of a request-to-task match on the "
0324                        "request page.",
0325     },
0326     'questionnaire_match_remove': {
0327         'sublevel': 'normal', 'live': True,
0328         'description': "Operator removal of a request-to-task match.",
0329     },
0330     'category_create': {
0331         'sublevel': 'normal', 'live': True,
0332         'description': "Create a physics tag category.",
0333     },
0334     'tag_create': {
0335         'sublevel': 'low', 'live': False,
0336         'description': "Create a configuration tag (physics, evgen, simu, "
0337                        "reco, or background).",
0338     },
0339     'tag_edit': {
0340         'sublevel': 'low', 'live': False,
0341         'description': "Edit a draft configuration tag.",
0342     },
0343     'tag_lock': {
0344         'sublevel': 'normal', 'live': True,
0345         'description': "Lock a configuration tag — a permanent, one-way "
0346                        "transition to immutability.",
0347     },
0348     'tag_delete': {
0349         'sublevel': 'normal', 'live': True,
0350         'description': "Delete a draft configuration tag.",
0351     },
0352     'dataset_create': {
0353         'sublevel': 'normal', 'live': True,
0354         'description': "Create a dataset: a composed tag identity plus any "
0355                        "sample-variant discriminator.",
0356     },
0357     'dataset_block_add': {
0358         'sublevel': 'low', 'live': False,
0359         'description': "Add the next Rucio block (.bN) to a dataset.",
0360     },
0361     'config_create': {
0362         'sublevel': 'low', 'live': False,
0363         'description': "Create a production config template.",
0364     },
0365     'config_edit': {
0366         'sublevel': 'low', 'live': False,
0367         'description': "Edit a production config template.",
0368     },
0369     'task_delete': {
0370         'sublevel': 'normal', 'live': True,
0371         'description': "Delete a campaign task.",
0372     },
0373     'assessment_quality_set': {
0374         'sublevel': 'normal', 'live': True,
0375         'description': "Operator quality review (good, poor, wrong) recorded "
0376                        "on an AI assessment.",
0377     },
0378     'queues_update': {
0379         'sublevel': 'normal', 'live': True,
0380         'description': "Update PanDA queue records from the GitHub source.",
0381     },
0382     'endpoints_update': {
0383         'sublevel': 'normal', 'live': True,
0384         'description': "Update Rucio storage endpoint records from the "
0385                        "GitHub source.",
0386     },
0387 }
0388 
0389 
0390 def action_description(action):
0391     """Plain-English one-liner for a known action id, or ''."""
0392     return (ACTION_DEFAULTS.get(str(action)) or {}).get('description', '')
0393 
0394 RESERVED_KEYS = ('action', 'subject_type', 'subject_key', 'username',
0395                  'outcome', 'duration_ms', 'sublevel', 'live_default')
0396 
0397 
0398 def log_epicprod_action(instance, action, *, subject_type='', subject_key='',
0399                         username='', outcome='ok', duration_ms=None,
0400                         sublevel='low', live_default=False, message='',
0401                         level=logging.INFO, **counts):
0402     """Record one epicprod action in the AppLog action stream.
0403 
0404     Never raises: a failure to record is logged to the module logger and the
0405     calling action proceeds — the action log must not break the action.
0406 
0407     Args:
0408         instance: component performing the action ('web', 'ops-agent', 'mcp',
0409             'catalog-sync', 'submit', 'report').
0410         action: short action identifier, e.g. 'rucio_sweep', 'task_submit',
0411             'assessment_register'.
0412         subject_type: acted-on object type when there is one (canonical
0413             assessment subject types where applicable).
0414         subject_key: acted-on object key (composed name, JEDI id, queue, ...).
0415         username: human or service account driving the action.
0416         outcome: 'ok' or 'error' (conventional; free-form refinements allowed).
0417         duration_ms: measured execution time; required in spirit for sweeps
0418             and other timed operations.
0419         sublevel: the event's declared importance — 'high' (reaches
0420             everyone: news, digests, email), 'normal' (live-page watchers),
0421             'low' (verbose viewers only). AUTHORITATIVE: change it by
0422             changing the event, not at runtime.
0423         live_default: the event's declared RECOMMENDATION for the live
0424             stream ("interesting to some humans, now"). The effective live
0425             decision is the epicprod_live_policy override (runtime attention
0426             knob) over this default.
0427         message: optional human-readable one-liner; composed if omitted.
0428         level: python logging level; use logging.ERROR for failed actions.
0429         **counts: numeric counts worth recording (rows_added=..., etc.);
0430             reserved keys are ignored if passed here.
0431 
0432     Returns:
0433         The created AppLog row id, or None if the write failed.
0434     """
0435     from .models import AppLog
0436 
0437     if sublevel not in SUBLEVEL_VALUES:
0438         logger.warning('epicprod action %s: unknown sublevel %r, using low',
0439                        action, sublevel)
0440         sublevel = 'low'
0441     extra = {
0442         'action': str(action),
0443         'outcome': str(outcome),
0444         'sublevel': sublevel,
0445         'live_default': bool(live_default),
0446     }
0447     if subject_type:
0448         extra['subject_type'] = str(subject_type)
0449     if subject_key:
0450         extra['subject_key'] = str(subject_key)
0451     if username:
0452         extra['username'] = str(username)
0453     if duration_ms is not None:
0454         try:
0455             extra['duration_ms'] = int(duration_ms)
0456         except (TypeError, ValueError):
0457             logger.warning('epicprod action %s: non-numeric duration_ms %r',
0458                            action, duration_ms)
0459     for key, value in counts.items():
0460         if key not in RESERVED_KEYS:
0461             extra[key] = value
0462 
0463     if not message:
0464         subject = f"{subject_type}:{subject_key}" if subject_key else ''
0465         message = ' '.join(part for part in (str(action), subject, str(outcome)) if part)
0466 
0467     try:
0468         row = AppLog.objects.create(
0469             app_name=EPICPROD_APP_NAME,
0470             instance_name=str(instance),
0471             timestamp=timezone.now(),
0472             level=int(level),
0473             levelname=logging.getLevelName(int(level)),
0474             message=message,
0475             module='epicprod_logging',
0476             funcname=str(action),
0477             lineno=0,
0478             process=os.getpid(),
0479             thread=threading.get_ident(),
0480             extra_data=extra,
0481         )
0482         return row.id
0483     except Exception:
0484         logger.exception('epicprod action log write failed: %s %s',
0485                          instance, action)
0486         return None
0487 
0488 
0489 def get_live_policy():
0490     """Return the live override registry: {action_id: bool}.
0491 
0492     Stored under LIVE_POLICY_STATE_KEY in SysConfig (system configuration
0493     lives in the DB, adjustable via the live-policy page). The runtime
0494     attention knob: an action absent from the registry follows its records'
0495     live_default recommendation. Reads defensively: a missing table
0496     (pre-migration) yields an empty policy with a logged warning rather than
0497     breaking the caller.
0498     """
0499     from .models import SysConfig
0500 
0501     try:
0502         policy = SysConfig.get_setting(LIVE_POLICY_STATE_KEY, {}) or {}
0503     except Exception as exc:
0504         logger.warning('live policy read failed (empty policy used): %s', exc)
0505         return {}
0506     if not isinstance(policy, dict):
0507         return {}
0508     return {str(k): bool(v) for k, v in policy.items()}
0509 
0510 
0511 def set_live_policy_entry(action, value, username=''):
0512     """Set (True/False) or clear (None) an action's live override."""
0513     from .models import SysConfig
0514 
0515     policy = get_live_policy()
0516     key = str(action)
0517     if value is None:
0518         policy.pop(key, None)
0519     else:
0520         policy[key] = bool(value)
0521     SysConfig.update_config({LIVE_POLICY_STATE_KEY: policy}, username=username)
0522     return policy
0523 
0524 
0525 def live_policy_rows():
0526     """Rows for the live-policy page: every known action with its declared
0527     sublevel and live default, any live override, and the effective state.
0528 
0529     Known = the ACTION_DEFAULTS catalog plus any action observed in the
0530     stream (the newest record supplies declarations for actions not yet in
0531     the catalog).
0532     """
0533     from .models import AppLog
0534 
0535     policy = get_live_policy()
0536     declarations = {k: dict(v) for k, v in ACTION_DEFAULTS.items()}
0537     observed = (AppLog.objects.filter(app_name=EPICPROD_APP_NAME)
0538                 .exclude(extra_data__action__isnull=True)
0539                 .values_list('extra_data__action', flat=True)
0540                 .distinct())
0541     for action in observed:
0542         if action and action not in declarations:
0543             latest = (AppLog.objects.filter(app_name=EPICPROD_APP_NAME,
0544                                             extra_data__action=action)
0545                       .order_by('-id').first())
0546             extra = latest.extra_data if latest and isinstance(latest.extra_data, dict) else {}
0547             stamped = extra.get('sublevel')
0548             declarations[action] = {
0549                 'sublevel': stamped if stamped in SUBLEVEL_VALUES else 'low',
0550                 'live': bool(extra.get('live_default')),
0551             }
0552 
0553     rows = []
0554     for action in sorted(declarations):
0555         decl = declarations[action]
0556         override = policy.get(action)
0557         effective = override if override is not None else decl['live']
0558         rows.append({
0559             'action': action,
0560             'description': decl.get('description', ''),
0561             'sublevel': decl['sublevel'],
0562             'live_default': decl['live'],
0563             'state': 'default' if override is None else ('live' if override else 'quiet'),
0564             'effective': bool(effective),
0565         })
0566     return rows
0567 
0568 
0569 def live_stream_q(min_sublevel=None):
0570     """Q filter selecting live action records, optionally importance-gated.
0571 
0572     Live = the runtime override (epicprod_live_policy) over the record's
0573     declared live_default. A channel applies its importance threshold through
0574     min_sublevel: the deep live view passes None (all live events); an email
0575     digest passes 'high'. The single source of stream semantics for every
0576     channel.
0577     """
0578     from django.db.models import Q
0579 
0580     policy = get_live_policy()
0581     force_on = [a for a, v in policy.items() if v]
0582     force_off = [a for a, v in policy.items() if not v]
0583 
0584     q = Q(extra_data__live_default=True)
0585     if force_off:
0586         q &= ~Q(extra_data__action__in=force_off)
0587     if force_on:
0588         q |= Q(extra_data__action__in=force_on)
0589     if min_sublevel in SUBLEVEL_VALUES:
0590         min_rank = SUBLEVEL_ORDER[min_sublevel]
0591         ge_values = [v for v in SUBLEVEL_VALUES if SUBLEVEL_ORDER[v] >= min_rank]
0592         q &= Q(extra_data__sublevel__in=ge_values)
0593     return Q(app_name=EPICPROD_APP_NAME) & q