File indexing completed on 2026-11-09 10:47:24
0001
0002 """request-sizes-build.py — build the record the Request size plot page
0003 renders: how large the ePIC production requests are.
0004
0005 Every request record that states a number of events is read (a form
0006 answer in the requester's own words, or a recorded count), the
0007 distribution computed, and both stored as the cached product
0008 ``request_sizes:v1`` (swf-epicprod ``swf_epicprod/request_events.py``,
0009 docs/REQUEST_SIZES.md). The page reads that row and never builds, so a
0010 page render costs a row read.
0011
0012 The prod-ops agent's doer for ``request_sizes_build``, nightly. Requests
0013 arrive by the day, so nothing is lost by building once a night.
0014 Django-bootstrap standalone script, also usable by hand::
0015
0016 cd /data/wenauseic/github/swf-monitor/src
0017 source ../../swf-testbed/.venv/bin/activate && source ~/.env
0018 python ../scripts/request-sizes-build.py [--dry-run]
0019
0020 ``--dry-run`` reads and prints, and stores nothing.
0021 Prints one ``SUMMARY`` JSON line; exit 1 on failure.
0022 """
0023 import argparse
0024 import json
0025 import logging
0026 import os
0027 import sys
0028
0029 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
0030 sys.path.insert(0, os.path.join(THIS_DIR, '..', 'src'))
0031 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_monitor_project.settings')
0032
0033 import django
0034 django.setup()
0035
0036 KEY = 'request_sizes:v1'
0037 TTL_S = 7 * 24 * 3600
0038
0039
0040 def main():
0041 ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0])
0042 ap.add_argument('--dry-run', action='store_true',
0043 help='read and print, store nothing')
0044 ap.add_argument('--created-by', default='request-sizes',
0045 help='who ran the build (the record carries it)')
0046 args = ap.parse_args()
0047
0048 from django.utils import timezone
0049 from monitor_app.cached_product import get_product
0050 from monitor_app.epicprod_logging import log_epicprod_action
0051 from swf_epicprod.request_events import build, human
0052
0053 started = timezone.now()
0054 try:
0055 state = build()
0056 except Exception as exc:
0057 print(f'ERROR: {type(exc).__name__}: {exc}', file=sys.stderr)
0058 return 1
0059 summary = state['summary']
0060 duration_s = (timezone.now() - started).total_seconds()
0061
0062 if not args.dry_run:
0063 try:
0064 get_product(KEY, lambda: state, ttl_seconds=TTL_S, refresh=True)
0065 except Exception as exc:
0066 print(f'ERROR: storing {KEY}: {type(exc).__name__}: {exc}', file=sys.stderr)
0067 return 1
0068 log_epicprod_action(
0069 'request-sizes', 'request_sizes_build', username=args.created_by,
0070 outcome='ok', duration_ms=int(duration_s * 1000),
0071 sublevel='low', live_default=False, level=logging.INFO,
0072 message=(f'request sizes built: {summary["stated"]} of '
0073 f'{summary["requests"]} requests state a count, median '
0074 f'{human(summary["percentiles"].get("50"))}, '
0075 f'p90 {human(summary["percentiles"].get("90"))}'),
0076 requests=summary['requests'], stated=summary['stated'],
0077 unstated=summary['unstated'], total_events=summary['total'])
0078
0079 print(f"requests {summary['requests']}, stating a count {summary['stated']}, "
0080 f"median {human(summary['percentiles'].get('50'))}, "
0081 f"p90 {human(summary['percentiles'].get('90'))}, "
0082 f"max {human(summary['max'])}")
0083 print('SUMMARY ' + json.dumps(
0084 {k: v for k, v in summary.items()
0085 if k not in ('bins', 'cumulative', 'thresholds', 'apart')},
0086 default=str))
0087 return 0
0088
0089
0090 if __name__ == '__main__':
0091 sys.exit(main())