Skip to content

Commit 48abfd8

Browse files
authored
Prepend every rqd log line with a timestamp (#1286)
* Prepend every rqd log line with a timestamp Not all applications launched on rqd have logs with a timestamp, which makes is difficult to debug jobs that are taking more than expected on the cue. This feature prepends a timestamp for every line. * Add rqconstant to turn timestamp feature on/off * Add rqconstant to turn timestamp feature on/off
1 parent 6fa72ff commit 48abfd8

File tree

3 files changed

+108
-6
lines changed

3 files changed

+108
-6
lines changed

rqd/rqd/rqconstants.py

+3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
RQD_BECOME_JOB_USER = True
7474
RQD_CREATE_USER_IF_NOT_EXISTS = True
7575
RQD_TAGS = ''
76+
RQD_PREPEND_TIMESTAMP = False
7677

7778
KILL_SIGNAL = 9
7879
if platform.system() == 'Linux':
@@ -197,6 +198,8 @@
197198
if config.has_option(__section, "FILE_LOG_LEVEL"):
198199
level = config.get(__section, "FILE_LOG_LEVEL")
199200
FILE_LOG_LEVEL = logging.getLevelName(level)
201+
if config.has_option(__section, "RQD_PREPEND_TIMESTAMP"):
202+
RQD_PREPEND_TIMESTAMP = config.getboolean(__section, "RQD_PREPEND_TIMESTAMP")
200203
# pylint: disable=broad-except
201204
except Exception as e:
202205
logging.warning(

rqd/rqd/rqcore.py

+104-4
Original file line numberDiff line numberDiff line change
@@ -315,14 +315,17 @@ def runLinux(self):
315315
else:
316316
tempCommand += [self._createCommandFile(runFrame.command)]
317317

318-
# Actual cwd is set by /shots/SHOW/home/perl/etc/qwrap.cuerun
318+
if rqd.rqconstants.RQD_PREPEND_TIMESTAMP:
319+
file_descriptor = subprocess.PIPE
320+
else:
321+
file_descriptor = self.rqlog
319322
# pylint: disable=subprocess-popen-preexec-fn
320323
frameInfo.forkedCommand = subprocess.Popen(tempCommand,
321324
env=self.frameEnv,
322325
cwd=self.rqCore.machine.getTempPath(),
323326
stdin=subprocess.PIPE,
324-
stdout=self.rqlog,
325-
stderr=self.rqlog,
327+
stdout=file_descriptor,
328+
stderr=file_descriptor,
326329
close_fds=True,
327330
preexec_fn=os.setsid)
328331
finally:
@@ -335,6 +338,8 @@ def runLinux(self):
335338
self.rqCore.updateRss)
336339
self.rqCore.updateRssThread.start()
337340

341+
if rqd.rqconstants.RQD_PREPEND_TIMESTAMP:
342+
pipe_to_file(frameInfo.forkedCommand.stdout, frameInfo.forkedCommand.stderr, self.rqlog)
338343
returncode = frameInfo.forkedCommand.wait()
339344

340345
# Find exitStatus and exitSignal
@@ -535,7 +540,7 @@ def run(self):
535540
else:
536541
raise RuntimeError(err)
537542
try:
538-
self.rqlog = open(runFrame.log_dir_file, "w", 1)
543+
self.rqlog = open(runFrame.log_dir_file, "w+", 1)
539544
self.waitForFile(runFrame.log_dir_file)
540545
# pylint: disable=broad-except
541546
except Exception as e:
@@ -1161,3 +1166,98 @@ def sendStatusReport(self):
11611166
def isWaitingForIdle(self):
11621167
"""Returns whether the host is waiting until idle to take some action."""
11631168
return self.__whenIdle
1169+
1170+
def pipe_to_file(stdout, stderr, outfile):
1171+
"""
1172+
Prepend entries on stdout and stderr with a timestamp and write to outfile.
1173+
1174+
The logic to poll stdout/stderr is inspired by the Popen.communicate implementation.
1175+
This feature is linux specific
1176+
"""
1177+
# Importing packages internally to avoid compatibility issues with Windows
1178+
1179+
if stdout is None or stderr is None:
1180+
return
1181+
outfile.flush()
1182+
os.fsync(outfile)
1183+
1184+
import select
1185+
import errno
1186+
1187+
fd2file = {}
1188+
fd2output = {}
1189+
1190+
poller = select.poll()
1191+
1192+
def register_and_append(file_ojb, eventmask):
1193+
poller.register(file_ojb, eventmask)
1194+
fd2file[file_ojb.fileno()] = file_ojb
1195+
1196+
def close_and_unregister_and_remove(fd, close=False):
1197+
poller.unregister(fd)
1198+
if close:
1199+
fd2file[fd].close()
1200+
fd2file.pop(fd)
1201+
1202+
def print_and_flush_ln(fd, last_timestamp):
1203+
txt = ''.join(fd2output[fd])
1204+
lines = txt.split('\n')
1205+
next_line_timestamp = None
1206+
1207+
# Save the timestamp of the first break
1208+
if last_timestamp is None:
1209+
curr_line_timestamp = datetime.datetime.now().strftime("%H:%M:%S")
1210+
else:
1211+
curr_line_timestamp = last_timestamp
1212+
1213+
# There are no line breaks
1214+
if len(lines) < 2:
1215+
return curr_line_timestamp
1216+
else:
1217+
next_line_timestamp = datetime.datetime.now().strftime("%H:%M:%S")
1218+
1219+
remainder = lines[-1]
1220+
for line in lines[0:-1]:
1221+
print("[%s] %s" % (curr_line_timestamp, line), file=outfile)
1222+
outfile.flush()
1223+
os.fsync(outfile)
1224+
fd2output[fd] = [remainder]
1225+
1226+
if next_line_timestamp is None:
1227+
return curr_line_timestamp
1228+
else:
1229+
return next_line_timestamp
1230+
1231+
def translate_newlines(data):
1232+
data = data.decode("utf-8", "ignore")
1233+
return data.replace("\r\n", "\n").replace("\r", "\n")
1234+
1235+
select_POLLIN_POLLPRI = select.POLLIN | select.POLLPRI
1236+
# stdout
1237+
register_and_append(stdout, select_POLLIN_POLLPRI)
1238+
fd2output[stdout.fileno()] = []
1239+
1240+
# stderr
1241+
register_and_append(stderr, select_POLLIN_POLLPRI)
1242+
fd2output[stderr.fileno()] = []
1243+
1244+
while fd2file:
1245+
try:
1246+
ready = poller.poll()
1247+
except select.error as e:
1248+
if e.args[0] == errno.EINTR:
1249+
continue
1250+
raise
1251+
1252+
first_chunk_timestamp = None
1253+
for fd, mode in ready:
1254+
if mode & select_POLLIN_POLLPRI:
1255+
data = os.read(fd, 4096)
1256+
if not data:
1257+
close_and_unregister_and_remove(fd)
1258+
if not isinstance(data, str):
1259+
data = translate_newlines(data)
1260+
fd2output[fd].append(data)
1261+
first_chunk_timestamp = print_and_flush_ln(fd, first_chunk_timestamp)
1262+
else:
1263+
close_and_unregister_and_remove(fd)

rqd/tests/rqcore_tests.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ def setUp(self):
567567

568568
@mock.patch('platform.system', new=mock.Mock(return_value='Linux'))
569569
@mock.patch('tempfile.gettempdir')
570+
@mock.patch('rqd.rqcore.pipe_to_file', new=mock.MagicMock())
570571
def test_runLinux(self, getTempDirMock, permsUser, timeMock, popenMock): # mkdirMock, openMock,
571572
# given
572573
currentTime = 1568070634.3
@@ -632,8 +633,6 @@ def test_runLinux(self, getTempDirMock, permsUser, timeMock, popenMock): # mkdir
632633
self.assertTrue(os.path.exists(logDir))
633634
self.assertTrue(os.path.isfile(logFile))
634635
_, kwargs = popenMock.call_args
635-
self.assertEqual(logFile, kwargs['stdout'].name)
636-
self.assertEqual(logFile, kwargs['stderr'].name)
637636

638637
rqCore.network.reportRunningFrameCompletion.assert_called_with(
639638
rqd.compiled_proto.report_pb2.FrameCompleteReport(

0 commit comments

Comments
 (0)