Skip to content

Commit eef07cd

Browse files
authored
Merge pull request #1599 from oesteban/enh/new-reports
ENH: Switch to the refactored report generation from NiWorkflows
2 parents 176443f + 0be44c6 commit eef07cd

File tree

19 files changed

+219
-783
lines changed

19 files changed

+219
-783
lines changed

.circleci/config.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,12 @@ jobs:
394394
/tmp/data/ds005 /tmp/ds005/derivatives participant \
395395
--sloppy --write-graph --mem_mb 4096 \
396396
--nthreads 2 --anat-only -vv
397+
- run:
398+
name: Clean-up after anatomical run
399+
command: |
400+
rm -rf /tmp/ds005/work/fmriprep_wf/fsdir*
401+
rm -rf /tmp/ds005/work/reportlets
402+
rm -rf /tmp/ds005/derivatives/fmriprep
397403
- save_cache:
398404
key: ds005-anat-v14-{{ .Branch }}-{{ epoch }}
399405
paths:
@@ -525,6 +531,12 @@ jobs:
525531
/tmp/data/ds054 /tmp/ds054/derivatives participant \
526532
--fs-no-reconall --sloppy --write-graph \
527533
--mem_mb 4096 --nthreads 2 --anat-only -vv
534+
- run:
535+
name: Clean-up after anatomical run
536+
command: |
537+
rm -rf /tmp/ds054/work/fmriprep_wf/fsdir*
538+
rm -rf /tmp/ds054/work/reportlets
539+
rm -rf /tmp/ds054/derivatives/fmriprep
528540
- save_cache:
529541
key: ds054-anat-v12-{{ .Branch }}-{{ epoch }}
530542
paths:
@@ -643,6 +655,12 @@ jobs:
643655
/tmp/data/ds210 /tmp/ds210/derivatives participant \
644656
--fs-no-reconall --sloppy --write-graph \
645657
--mem_mb 4096 --nthreads 2 --anat-only -vv
658+
- run:
659+
name: Clean-up after anatomical run
660+
command: |
661+
rm -rf /tmp/ds210/work/fmriprep_wf/fsdir*
662+
rm -rf /tmp/ds210/work/reportlets
663+
rm -rf /tmp/ds210/derivatives/fmriprep
646664
- save_cache:
647665
key: ds210-anat-v10-{{ .Branch }}-{{ epoch }}
648666
paths:

docs/workflows.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,15 @@ T1w/T2w preprocessing
7070
bids_root='.',
7171
debug=False,
7272
freesurfer=True,
73-
fs_spaces=['T1w', 'fsnative',
74-
'template', 'fsaverage5'],
7573
hires=True,
7674
longitudinal=False,
7775
num_t1w=1,
7876
omp_nthreads=1,
7977
output_dir='.',
78+
output_spaces={'MNI152NLin2009cAsym': {'res': 2}},
8079
reportlets_dir='.',
8180
skull_strip_template='MNI152NLin2009cAsym',
8281
skull_strip_fixed_seed=False,
83-
template='MNI152NLin2009cAsym',
8482
)
8583

8684
The anatomical sub-workflow begins by constructing an average image by

fmriprep/__about__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@
105105

106106
LINKS_REQUIRES = [
107107
'git+https://github.com/poldracklab/niworkflows.git@'
108-
'b7d111c8fd36a099c74be5e7671677eedb175533#egg=niworkflows',
108+
'076aed98962b10d107c83110c05e42466a89bbc4#egg=niworkflows',
109109
'git+https://github.com/poldracklab/smriprep.git@'
110-
'423bcc43ab7300177eb3b98da62817b2cad8eb87#egg=smriprep-0.1.0',
110+
'f1cfc37bcdc346549dbf1d037cdade3a3b32d5de#egg=smriprep-0.1.0',
111111
]
112112

113113
TESTS_REQUIRES = [

fmriprep/cli/run.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,6 @@ def main():
260260
"""Entry point"""
261261
from nipype import logging as nlogging
262262
from multiprocessing import set_start_method, Process, Manager
263-
from ..viz.reports import generate_reports
264263
from ..utils.bids import write_derivative_description
265264
set_start_method('forkserver')
266265

@@ -384,8 +383,6 @@ def before_send(event, hints):
384383
nlogging.getLogger('nipype.interface').setLevel(log_level)
385384
nlogging.getLogger('nipype.utils').setLevel(log_level)
386385

387-
errno = 0
388-
389386
# Call build_workflow(opts, retval)
390387
with Manager() as mgr:
391388
retval = mgr.dict()
@@ -395,9 +392,9 @@ def before_send(event, hints):
395392

396393
retcode = p.exitcode or retval.get('return_code', 0)
397394

398-
bids_dir = retval.get('bids_dir')
399-
output_dir = retval.get('output_dir')
400-
work_dir = retval.get('work_dir')
395+
bids_dir = Path(retval.get('bids_dir'))
396+
output_dir = Path(retval.get('output_dir'))
397+
work_dir = Path(retval.get('work_dir'))
401398
plugin_settings = retval.get('plugin_settings', None)
402399
subject_list = retval.get('subject_list', None)
403400
fmriprep_wf = retval.get('workflow', None)
@@ -436,32 +433,48 @@ def before_send(event, hints):
436433
sentry_sdk.add_breadcrumb(message='fMRIPrep started', level='info')
437434
sentry_sdk.capture_message('fMRIPrep started', level='info')
438435

436+
errno = 1 # Default is error exit unless otherwise set
439437
try:
440438
fmriprep_wf.run(**plugin_settings)
441-
except RuntimeError as e:
442-
errno = 1
443-
if "Workflow did not execute cleanly" not in str(e):
444-
sentry_sdk.capture_exception(e)
445-
raise
439+
except Exception as e:
440+
if not opts.notrack:
441+
from ..utils.sentry import process_crashfile
442+
crashfolders = [output_dir / 'fmriprep' / 'sub-{}'.format(s) / 'log' / run_uuid
443+
for s in subject_list]
444+
for crashfolder in crashfolders:
445+
for crashfile in crashfolder.glob('crash*.*'):
446+
process_crashfile(crashfile)
447+
448+
if "Workflow did not execute cleanly" not in str(e):
449+
sentry_sdk.capture_exception(e)
450+
logger.critical('fMRIPrep failed: %s', e)
451+
raise
446452
else:
447453
if opts.run_reconall:
448454
from templateflow import api
449455
from niworkflows.utils.misc import _copy_any
450456
dseg_tsv = str(api.get('fsaverage', suffix='dseg', extensions=['.tsv']))
451457
_copy_any(dseg_tsv,
452-
str(Path(output_dir) / 'fmriprep' / 'desc-aseg_dseg.tsv'))
458+
str(output_dir / 'fmriprep' / 'desc-aseg_dseg.tsv'))
453459
_copy_any(dseg_tsv,
454-
str(Path(output_dir) / 'fmriprep' / 'desc-aparcaseg_dseg.tsv'))
460+
str(output_dir / 'fmriprep' / 'desc-aparcaseg_dseg.tsv'))
461+
errno = 0
455462
logger.log(25, 'fMRIPrep finished without errors')
463+
if not opts.notrack:
464+
sentry_sdk.capture_message('fMRIPrep finished without errors',
465+
level='info')
456466
finally:
467+
from niworkflows.reports import generate_reports
457468
# Generate reports phase
458-
errno += generate_reports(subject_list, output_dir, work_dir, run_uuid,
459-
sentry_sdk=sentry_sdk)
460-
write_derivative_description(bids_dir, str(Path(output_dir) / 'fmriprep'))
469+
failed_reports = generate_reports(
470+
subject_list, output_dir, work_dir, run_uuid, packagename='fmriprep')
471+
write_derivative_description(bids_dir, output_dir / 'fmriprep')
461472

462-
if not opts.notrack and errno == 0:
463-
sentry_sdk.capture_message('fMRIPrep finished without errors', level='info')
464-
sys.exit(int(errno > 0))
473+
if failed_reports and not opts.notrack:
474+
sentry_sdk.capture_message(
475+
'Report generation failed for %d subjects' % failed_reports,
476+
level='error')
477+
sys.exit(int((errno + failed_reports) > 0))
465478

466479

467480
def validate_input_dir(exec_env, bids_dir, participant_label):
@@ -574,9 +587,9 @@ def build_workflow(opts, retval):
574587

575588
from nipype import logging, config as ncfg
576589
from niworkflows.utils.bids import collect_participants
590+
from niworkflows.reports import generate_reports
577591
from ..__about__ import __version__
578592
from ..workflows.base import init_fmriprep_wf
579-
from ..viz.reports import generate_reports
580593

581594
logger = logging.getLogger('nipype.workflow')
582595

@@ -734,7 +747,8 @@ def build_workflow(opts, retval):
734747
run_uuid = opts.run_uuid
735748
retval['run_uuid'] = run_uuid
736749
retval['return_code'] = generate_reports(
737-
subject_list, str(output_dir), str(work_dir), run_uuid)
750+
subject_list, output_dir, work_dir, run_uuid,
751+
packagename='fmriprep')
738752
return retval
739753

740754
# Build main workflow

fmriprep/utils/sentry.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""Stripped out routines for Sentry"""
4+
import re
5+
from niworkflows.utils.misc import read_crashfile
6+
import sentry_sdk
7+
8+
CHUNK_SIZE = 16384
9+
# Group common events with pre specified fingerprints
10+
KNOWN_ERRORS = {
11+
'permission-denied': [
12+
"PermissionError: [Errno 13] Permission denied"
13+
],
14+
'memory-error': [
15+
"MemoryError",
16+
"Cannot allocate memory",
17+
"Return code: 134",
18+
],
19+
'reconall-already-running': [
20+
"ERROR: it appears that recon-all is already running"
21+
],
22+
'no-disk-space': [
23+
"[Errno 28] No space left on device",
24+
"[Errno 122] Disk quota exceeded"
25+
],
26+
'segfault': [
27+
"Segmentation Fault",
28+
"Segfault",
29+
"Return code: 139",
30+
],
31+
'potential-race-condition': [
32+
"[Errno 39] Directory not empty",
33+
"_unfinished.json",
34+
],
35+
'keyboard-interrupt': [
36+
"KeyboardInterrupt",
37+
],
38+
}
39+
40+
41+
def process_crashfile(crashfile):
42+
"""Parse the contents of a crashfile and submit sentry messages"""
43+
crash_info = read_crashfile(str(crashfile))
44+
with sentry_sdk.push_scope() as scope:
45+
scope.level = 'fatal'
46+
47+
# Extract node name
48+
node_name = crash_info.pop('node').split('.')[-1]
49+
scope.set_tag("node_name", node_name)
50+
51+
# Massage the traceback, extract the gist
52+
traceback = crash_info.pop('traceback')
53+
# last line is probably most informative summary
54+
gist = traceback.splitlines()[-1]
55+
exception_text_start = 1
56+
for line in traceback.splitlines()[1:]:
57+
if not line[0].isspace():
58+
break
59+
exception_text_start += 1
60+
61+
exception_text = '\n'.join(
62+
traceback.splitlines()[exception_text_start:])
63+
64+
# Extract inputs, if present
65+
inputs = crash_info.pop('inputs', None)
66+
if inputs:
67+
scope.set_extra('inputs', dict(inputs))
68+
69+
# Extract any other possible metadata in the crash file
70+
for k, v in crash_info.items():
71+
strv = list(_chunks(str(v)))
72+
if len(strv) == 1:
73+
scope.set_extra(k, strv[0])
74+
else:
75+
for i, chunk in enumerate(strv):
76+
scope.set_extra('%s_%02d' % (k, i), chunk)
77+
78+
fingerprint = ''
79+
issue_title = '{}: {}'.format(node_name, gist)
80+
for new_fingerprint, error_snippets in KNOWN_ERRORS.items():
81+
for error_snippet in error_snippets:
82+
if error_snippet in traceback:
83+
fingerprint = new_fingerprint
84+
issue_title = new_fingerprint
85+
break
86+
if fingerprint:
87+
break
88+
89+
message = issue_title + '\n\n'
90+
message += exception_text[-(8192 - len(message)):]
91+
if fingerprint:
92+
sentry_sdk.add_breadcrumb(message=fingerprint, level='fatal')
93+
else:
94+
# remove file paths
95+
fingerprint = re.sub(r"(/[^/ ]*)+/?", '', message)
96+
# remove words containing numbers
97+
fingerprint = re.sub(r"([a-zA-Z]*[0-9]+[a-zA-Z]*)+", '', fingerprint)
98+
# adding the return code if it exists
99+
for line in message.splitlines():
100+
if line.startswith("Return code"):
101+
fingerprint += line
102+
break
103+
104+
scope.fingerprint = [fingerprint]
105+
sentry_sdk.capture_message(message, 'fatal')
106+
107+
108+
def _chunks(string, length=CHUNK_SIZE):
109+
"""
110+
Splits a string into smaller chunks
111+
>>> list(_chunks('some longer string.', length=3))
112+
['som', 'e l', 'ong', 'er ', 'str', 'ing', '.']
113+
"""
114+
return (string[i:i + length]
115+
for i in range(0, len(string), length))

fmriprep/viz/__init__.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)