Skip to content

Commit ea4726a

Browse files
committed
Merge branch 'enh/new-reports' into pr/1487
2 parents 3db3156 + 0be44c6 commit ea4726a

File tree

19 files changed

+219
-795
lines changed

19 files changed

+219
-795
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
@@ -73,17 +73,15 @@ T1w/T2w preprocessing
7373
bids_root='.',
7474
debug=False,
7575
freesurfer=True,
76-
fs_spaces=['T1w', 'fsnative',
77-
'template', 'fsaverage5'],
7876
hires=True,
7977
longitudinal=False,
8078
num_t1w=1,
8179
omp_nthreads=1,
8280
output_dir='.',
81+
output_spaces={'MNI152NLin2009cAsym': {'res': 2}},
8382
reportlets_dir='.',
8483
skull_strip_template='MNI152NLin2009cAsym',
8584
skull_strip_fixed_seed=False,
86-
template='MNI152NLin2009cAsym',
8785
)
8886

8987
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
@@ -107,9 +107,9 @@
107107
'git+https://github.com/nipy/nipype.git@'
108108
'd353f0d879826031334b09d33e9443b8c9b3e7fe#egg=nipype',
109109
'git+https://github.com/poldracklab/niworkflows.git@'
110-
'8ee3168e508f3e21e67c20b5104897869e728bc3#egg=niworkflows',
110+
'076aed98962b10d107c83110c05e42466a89bbc4#egg=niworkflows',
111111
'git+https://github.com/poldracklab/smriprep.git@'
112-
'423bcc43ab7300177eb3b98da62817b2cad8eb87#egg=smriprep-0.1.0',
112+
'f1cfc37bcdc346549dbf1d037cdade3a3b32d5de#egg=smriprep-0.1.0',
113113
]
114114

115115
TESTS_REQUIRES = [

fmriprep/cli/run.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ def main():
276276
"""Entry point"""
277277
from nipype import logging as nlogging
278278
from multiprocessing import set_start_method, Process, Manager
279-
from ..viz.reports import generate_reports
280279
from ..utils.bids import write_derivative_description
281280
set_start_method('forkserver')
282281

@@ -400,8 +399,6 @@ def before_send(event, hints):
400399
nlogging.getLogger('nipype.interface').setLevel(log_level)
401400
nlogging.getLogger('nipype.utils').setLevel(log_level)
402401

403-
errno = 0
404-
405402
# Call build_workflow(opts, retval)
406403
with Manager() as mgr:
407404
retval = mgr.dict()
@@ -411,9 +408,9 @@ def before_send(event, hints):
411408

412409
retcode = p.exitcode or retval.get('return_code', 0)
413410

414-
bids_dir = retval.get('bids_dir')
415-
output_dir = retval.get('output_dir')
416-
work_dir = retval.get('work_dir')
411+
bids_dir = Path(retval.get('bids_dir'))
412+
output_dir = Path(retval.get('output_dir'))
413+
work_dir = Path(retval.get('work_dir'))
417414
plugin_settings = retval.get('plugin_settings', None)
418415
subject_list = retval.get('subject_list', None)
419416
fmriprep_wf = retval.get('workflow', None)
@@ -452,32 +449,48 @@ def before_send(event, hints):
452449
sentry_sdk.add_breadcrumb(message='fMRIPrep started', level='info')
453450
sentry_sdk.capture_message('fMRIPrep started', level='info')
454451

452+
errno = 1 # Default is error exit unless otherwise set
455453
try:
456454
fmriprep_wf.run(**plugin_settings)
457-
except RuntimeError as e:
458-
errno = 1
459-
if "Workflow did not execute cleanly" not in str(e):
460-
sentry_sdk.capture_exception(e)
461-
raise
455+
except Exception as e:
456+
if not opts.notrack:
457+
from ..utils.sentry import process_crashfile
458+
crashfolders = [output_dir / 'fmriprep' / 'sub-{}'.format(s) / 'log' / run_uuid
459+
for s in subject_list]
460+
for crashfolder in crashfolders:
461+
for crashfile in crashfolder.glob('crash*.*'):
462+
process_crashfile(crashfile)
463+
464+
if "Workflow did not execute cleanly" not in str(e):
465+
sentry_sdk.capture_exception(e)
466+
logger.critical('fMRIPrep failed: %s', e)
467+
raise
462468
else:
463469
if opts.run_reconall:
464470
from templateflow import api
465471
from niworkflows.utils.misc import _copy_any
466472
dseg_tsv = str(api.get('fsaverage', suffix='dseg', extensions=['.tsv']))
467473
_copy_any(dseg_tsv,
468-
str(Path(output_dir) / 'fmriprep' / 'desc-aseg_dseg.tsv'))
474+
str(output_dir / 'fmriprep' / 'desc-aseg_dseg.tsv'))
469475
_copy_any(dseg_tsv,
470-
str(Path(output_dir) / 'fmriprep' / 'desc-aparcaseg_dseg.tsv'))
476+
str(output_dir / 'fmriprep' / 'desc-aparcaseg_dseg.tsv'))
477+
errno = 0
471478
logger.log(25, 'fMRIPrep finished without errors')
479+
if not opts.notrack:
480+
sentry_sdk.capture_message('fMRIPrep finished without errors',
481+
level='info')
472482
finally:
483+
from niworkflows.reports import generate_reports
473484
# Generate reports phase
474-
errno += generate_reports(subject_list, output_dir, work_dir, run_uuid,
475-
sentry_sdk=sentry_sdk)
476-
write_derivative_description(bids_dir, str(Path(output_dir) / 'fmriprep'))
485+
failed_reports = generate_reports(
486+
subject_list, output_dir, work_dir, run_uuid, packagename='fmriprep')
487+
write_derivative_description(bids_dir, output_dir / 'fmriprep')
477488

478-
if not opts.notrack and errno == 0:
479-
sentry_sdk.capture_message('fMRIPrep finished without errors', level='info')
480-
sys.exit(int(errno > 0))
489+
if failed_reports and not opts.notrack:
490+
sentry_sdk.capture_message(
491+
'Report generation failed for %d subjects' % failed_reports,
492+
level='error')
493+
sys.exit(int((errno + failed_reports) > 0))
481494

482495

483496
def validate_input_dir(exec_env, bids_dir, participant_label):
@@ -590,9 +603,9 @@ def build_workflow(opts, retval):
590603

591604
from nipype import logging, config as ncfg
592605
from niworkflows.utils.bids import collect_participants
606+
from niworkflows.reports import generate_reports
593607
from ..__about__ import __version__
594608
from ..workflows.base import init_fmriprep_wf
595-
from ..viz.reports import generate_reports
596609

597610
logger = logging.getLogger('nipype.workflow')
598611

@@ -750,7 +763,8 @@ def build_workflow(opts, retval):
750763
run_uuid = opts.run_uuid
751764
retval['run_uuid'] = run_uuid
752765
retval['return_code'] = generate_reports(
753-
subject_list, str(output_dir), str(work_dir), run_uuid)
766+
subject_list, output_dir, work_dir, run_uuid,
767+
packagename='fmriprep')
754768
return retval
755769

756770
# 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)