Skip to content

Commit 2ac2811

Browse files
authored
Merge pull request #231 from HPC-SimTools/230_eventlog_without_portal
#230 - add fallback bridge component to log events without portal
2 parents c46a561 + 385cb16 commit 2ac2811

File tree

6 files changed

+373
-367
lines changed

6 files changed

+373
-367
lines changed

ipsframework/bridges/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Bridges are components which handle supplementary tasks in the IPS Framework. Applications should not need to import these classes directly, these are automatically initialized by the framework.
2+
3+
There are two bridges available:
4+
- `LocalLoggingBridge`, which provides simple system logging (always created)
5+
- `PortalBridge`, which allows interfacing with a remote IPS Portal (created if user provides a PORTAL_URL and does not disable the portal)
6+
"""
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# -------------------------------------------------------------------------------
2+
# Copyright 2006-2022 UT-Battelle, LLC. See LICENSE for more information.
3+
# -------------------------------------------------------------------------------
4+
import hashlib
5+
import json
6+
import os
7+
import time
8+
from typing import TYPE_CHECKING, Any, Literal, Union
9+
10+
from ipsframework import Component, ipsutil
11+
from ipsframework.cca_es_spec import Event
12+
from ipsframework.convert_log_function import convert_logdata_to_html
13+
14+
if TYPE_CHECKING:
15+
from io import FileIO
16+
17+
18+
class SimulationData:
19+
"""
20+
Container for simulation data.
21+
"""
22+
23+
def __init__(self):
24+
self.counter = 0
25+
self.monitor_file_prefix = ''
26+
"""The name of the file, minus the extension ('.html', '.jsonl', etc.).
27+
28+
If this is empty, you must either check to see if you can create the file, or you should assume that you can't create the file.
29+
"""
30+
self.portal_runid: Union[str, None] = None
31+
"""Portal RunID, set by component which publishes the IPS_START event. Only used for logging here."""
32+
self.parent_portal_runid: Union[str, None] = None
33+
"""Parent portal RunID, derived from locally determined portal RunID. Should explicitly be None (not empty string) if not set. Only used for logging."""
34+
self.sim_name = ''
35+
self.sim_root = ''
36+
self.monitor_file: FileIO = None # type: ignore
37+
self.json_monitor_file: FileIO = None # type: ignore
38+
self.bigbuf = ''
39+
self.phys_time_stamp = -1
40+
self.monitor_url = ''
41+
42+
43+
class LocalLoggingBridge(Component):
44+
"""
45+
Framework component meant to handle simple event logging.
46+
47+
This component should not exist in the event that the simulation is interacting with the web framework - see the PortalBridge component instead.
48+
"""
49+
50+
def __init__(self, services, config):
51+
"""
52+
Declaration of private variables and initialization of
53+
:py:class:`component.Component` object.
54+
"""
55+
super().__init__(services, config)
56+
self.sim_map: dict[str, SimulationData] = {}
57+
self.done = False
58+
self.counter = 0
59+
self.dump_freq = 10
60+
self.min_dump_interval = 300 # Minimum time interval in Sec for HTML dump operation
61+
self.last_dump_time = time.time()
62+
self.write_to_htmldir = True
63+
self.html_dir = ''
64+
self.first_portal_runid = None
65+
66+
def init(self, timestamp=0.0, **keywords):
67+
"""
68+
Subscribe to *_IPS_MONITOR* events and register callback :py:meth:`.process_event`.
69+
"""
70+
self.services.subscribe('_IPS_MONITOR', 'process_event')
71+
72+
try:
73+
freq = int(self.services.get_config_param('HTML_DUMP_FREQ', silent=True))
74+
except Exception:
75+
pass
76+
else:
77+
self.dump_freq = freq
78+
79+
try:
80+
self.html_dir = self.services.get_config_param('USER_W3_DIR', silent=True) or ''
81+
except Exception:
82+
self.services.warning('Missing USER_W3_DIR configuration - disabling web-visible logging')
83+
self.write_to_htmldir = False
84+
else:
85+
if self.html_dir.strip() == '':
86+
self.services.warning('Empty USER_W3_DIR configuration - disabling web-visible logging')
87+
self.write_to_htmldir = False
88+
else:
89+
try:
90+
os.mkdir(self.html_dir)
91+
except FileExistsError:
92+
pass
93+
except Exception:
94+
self.services.warning('Unable to create HTML directory - disabling web-visible logging')
95+
self.write_to_htmldir = False
96+
97+
def step(self, timestamp=0.0, **keywords):
98+
"""
99+
Poll for events.
100+
"""
101+
while not self.done:
102+
self.services.process_events()
103+
time.sleep(0.5)
104+
105+
def finalize(self, timestamp=0.0, **keywords):
106+
for sim_data in self.sim_map.values():
107+
try:
108+
sim_data.monitor_file.close()
109+
sim_data.json_monitor_file.close()
110+
except Exception:
111+
pass
112+
113+
def process_event(self, topicName: str, theEvent: Event):
114+
"""
115+
Process a single event *theEvent* on topic *topicName*.
116+
"""
117+
event_body = theEvent.getBody()
118+
sim_name = event_body['sim_name']
119+
portal_data = event_body['portal_data']
120+
try:
121+
portal_data['sim_name'] = event_body['real_sim_name']
122+
except KeyError:
123+
portal_data['sim_name'] = sim_name
124+
125+
if portal_data['eventtype'] == 'IPS_START':
126+
sim_root = event_body['sim_root']
127+
self.init_simulation(sim_name, sim_root, portal_data['portal_runid'])
128+
129+
sim_data = self.sim_map[sim_name]
130+
if portal_data['eventtype'] == 'PORTALBRIDGE_UPDATE_TIMESTAMP':
131+
sim_data.phys_time_stamp = portal_data['phystimestamp']
132+
return
133+
else:
134+
portal_data['phystimestamp'] = sim_data.phys_time_stamp
135+
136+
if portal_data['eventtype'] == 'PORTAL_REGISTER_NOTEBOOK':
137+
return
138+
139+
if portal_data['eventtype'] == 'PORTAL_ADD_JUPYTER_DATA':
140+
return
141+
142+
if portal_data['eventtype'] == 'PORTAL_UPLOAD_ENSEMBLE_PARAMS':
143+
return
144+
145+
portal_data['portal_runid'] = sim_data.portal_runid
146+
147+
if portal_data['eventtype'] == 'IPS_SET_MONITOR_URL':
148+
sim_data.monitor_url = portal_data['vizurl']
149+
elif sim_data.monitor_url:
150+
portal_data['vizurl'] = sim_data.monitor_url
151+
152+
if portal_data['eventtype'] == 'IPS_START' and 'parent_portal_runid' not in portal_data:
153+
portal_data['parent_portal_runid'] = sim_data.parent_portal_runid
154+
portal_data['seqnum'] = sim_data.counter
155+
156+
if 'trace' in portal_data:
157+
portal_data['trace']['traceId'] = hashlib.md5(sim_data.portal_runid.encode()).hexdigest()
158+
159+
self.send_event(sim_data, portal_data)
160+
161+
if portal_data['eventtype'] == 'IPS_END':
162+
del self.sim_map[sim_name]
163+
164+
if len(self.sim_map) == 0:
165+
self.done = True
166+
self.services.debug('No more simulation to monitor - exiting')
167+
time.sleep(1)
168+
169+
def init_simulation(self, sim_name: str, sim_root: str, portal_runid: str):
170+
"""
171+
Create and send information about simulation *sim_name* living in
172+
*sim_root* so the portal can set up corresponding structures to manage
173+
data from the sim.
174+
"""
175+
self.services.debug('Initializing simulation using BasicBridge: %s -- %s ', sim_name, sim_root)
176+
177+
sim_data = SimulationData()
178+
sim_data.sim_name = sim_name
179+
sim_data.sim_root = sim_root
180+
181+
sim_data.portal_runid = portal_runid
182+
183+
if self.first_portal_runid:
184+
sim_data.parent_portal_runid = self.first_portal_runid
185+
else:
186+
self.first_portal_runid = sim_data.portal_runid
187+
188+
if sim_data.sim_root.strip() == '.':
189+
sim_data.sim_root = os.environ['IPS_INITIAL_CWD']
190+
sim_log_dir = os.path.join(sim_data.sim_root, 'simulation_log')
191+
try:
192+
os.makedirs(sim_log_dir, exist_ok=True)
193+
except OSError as oserr:
194+
self.services.exception('Error creating Simulation Log directory %s : %d %s' % (sim_log_dir, oserr.errno, oserr.strerror))
195+
raise
196+
197+
sim_data.monitor_file_prefix = os.path.join(sim_log_dir, sim_data.portal_runid)
198+
eventlog_fname = f'{sim_data.monitor_file_prefix}.eventlog'
199+
try:
200+
sim_data.monitor_file = open(eventlog_fname, 'wb', 0)
201+
except IOError as oserr:
202+
self.services.error('Error opening file %s: error(%s): %s' % (eventlog_fname, oserr.errno, oserr.strerror))
203+
self.services.error('Using /dev/null instead')
204+
sim_data.monitor_file_prefix = ''
205+
sim_data.monitor_file = open('/dev/null', 'w')
206+
207+
if sim_data.monitor_file_prefix:
208+
json_fname = os.path.join(sim_log_dir, sim_data.portal_runid + '.jsonl')
209+
sim_data.json_monitor_file = open(json_fname, 'w')
210+
else:
211+
sim_data.json_monitor_file = open('/dev/null', 'w')
212+
213+
self.sim_map[sim_data.sim_name] = sim_data
214+
215+
def terminate(self, status: Literal[0, 1]):
216+
"""
217+
Clean up services and call :py:obj:`sys_exit`.
218+
"""
219+
220+
Component.terminate(self, status)
221+
222+
def send_event(self, sim_data: SimulationData, event_data: dict[str, Any]):
223+
"""
224+
Send contents of *event_data* and *sim_data* to portal.
225+
"""
226+
timestamp = ipsutil.getTimeString()
227+
buf = '%8d %s ' % (sim_data.counter, timestamp)
228+
for k, v in event_data.items():
229+
if len(str(v).strip()) == 0:
230+
continue
231+
if ' ' in str(v):
232+
buf += "%s='%s' " % (k, str(v))
233+
else:
234+
buf += '%s=%s ' % (k, str(v))
235+
buf += '\n'
236+
sim_data.monitor_file.write(bytes(buf, encoding='UTF-8'))
237+
sim_data.bigbuf += buf
238+
239+
buf = json.dumps(event_data)
240+
sim_data.json_monitor_file.write('%s\n' % buf)
241+
242+
freq = self.dump_freq
243+
if ((self.counter % freq == 0) and (time.time() - self.last_dump_time > self.min_dump_interval)) or (event_data['eventtype'] == 'IPS_END'):
244+
self.last_dump_time = time.time()
245+
if sim_data.monitor_file_prefix:
246+
html_filename = f'{sim_data.monitor_file_prefix}.html'
247+
html_page = convert_logdata_to_html(sim_data.bigbuf)
248+
open(html_filename, 'w').writelines(html_page)
249+
if self.write_to_htmldir:
250+
html_file = os.path.join(self.html_dir, os.path.basename(html_filename))
251+
try:
252+
open(html_file, 'w').writelines(html_page)
253+
except Exception:
254+
self.services.exception('Error writing html file into USER_W3_DIR directory')
255+
self.write_to_htmldir = False

0 commit comments

Comments
 (0)