Skip to content

Commit 163cd37

Browse files
pditommasoclaude
andauthored
Implement custom date time format support with configurable NXF_DATE_FORMAT (#6013)
- Add configurable date formatting via NXF_DATE_FORMAT environment variable - Support ISO format with timezone (NXF_DATE_FORMAT=iso) - Default format changed to 'dd-MMM-yyyy HH:mm:ss' - Add OffsetDateTime support to SysHelper.fmtDate() - Update all notification templates to use configurable formatting: * Email notifications (HTML and text) * HTML execution reports * ANSI console output - Add comprehensive test coverage for all date format variations - Fix template binding to support SysHelper in notification templates Signed-off-by: Paolo Di Tommaso <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 1d73b87 commit 163cd37

File tree

11 files changed

+133
-14
lines changed

11 files changed

+133
-14
lines changed

docs/migrations/25-10.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,29 @@ workflow {
2929

3030
This syntax is simpler and easier to use with the {ref}`strict syntax <strict-syntax-page>`. See {ref}`workflow-handlers` for details.
3131

32+
<h3>Configurable date and time formatting</h3>
33+
34+
:::{versionadded} 25.07.0-edge
35+
:::
36+
37+
You can now customize the date and time format used in notifications, reports, and console output using the `NXF_DATE_FORMAT` environment variable. This addresses previous inconsistencies in timestamp representations:
38+
39+
```bash
40+
# Use ISO format with timezone
41+
export NXF_DATE_FORMAT=iso
42+
nextflow run workflow.nf
43+
# Output: 2016-08-11T09:40:20+02:00
44+
45+
# Use custom format
46+
export NXF_DATE_FORMAT="yyyy-MM-dd HH:mm"
47+
nextflow run workflow.nf
48+
# Output: 2016-08-11 09:40
49+
50+
# Default format includes seconds
51+
nextflow run workflow.nf
52+
# Output: 11-Aug-2016 09:40:20
53+
```
54+
3255
## Breaking changes
3356

3457
- The AWS Java SDK used by Nextflow was upgraded from v1 to v2, which introduced some breaking changes to the `aws.client` config options. See {ref}`the guide <aws-java-sdk-v2-page>` for details.

docs/reference/env-vars.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ The following environment variables control the configuration of the Nextflow ru
5454
:::
5555
: When `true`, override the container entrypoint with `/bin/bash` (default: `false`).
5656

57+
`NXF_DATE_FORMAT`
58+
: :::{versionadded} 25.07.0-edge
59+
:::
60+
: Defines the format for date and time representations in notifications and reports. Supports custom formats (e.g., `yyyy-MM-dd HH:mm:ss`) or `iso` for ISO 8601 format with timezone (default: `dd-MMM-yyyy HH:mm:ss`).
61+
5762
`NXF_DEFAULT_DSL`
5863
: :::{versionadded} 22.03.0-edge
5964
:::

modules/nextflow/src/main/groovy/nextflow/script/WorkflowNotifier.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import groovy.text.GStringTemplateEngine
2222
import groovy.transform.CompileStatic
2323
import groovy.util.logging.Slf4j
2424
import nextflow.mail.Attachment
25+
import nextflow.util.SysHelper
2526
import nextflow.mail.Mail
2627
import nextflow.mail.Mailer
2728
import nextflow.mail.Notification
@@ -214,6 +215,8 @@ class WorkflowNotifier {
214215
map.putAll(variables)
215216
if( binding )
216217
map.putAll(binding)
218+
// Add SysHelper to template binding so it can be used in templates
219+
map.put('SysHelper', SysHelper)
217220

218221
def template = new GStringTemplateEngine().createTemplate(new InputStreamReader(source))
219222
template.make(map).toString()

modules/nextflow/src/main/groovy/nextflow/trace/AnsiLogObserver.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import jline.TerminalFactory
2323
import nextflow.Session
2424
import nextflow.trace.event.TaskEvent
2525
import nextflow.util.Duration
26+
import nextflow.util.SysHelper
2627
import nextflow.util.Threads
2728
import org.fusesource.jansi.Ansi
2829
import org.fusesource.jansi.AnsiConsole
@@ -347,7 +348,7 @@ class AnsiLogObserver implements TraceObserverV2 {
347348

348349
if( session.isSuccess() && stats.progressLength>0 ) {
349350
def report = ""
350-
report += "Completed at: ${new Date(endTimestamp).format('dd-MMM-yyyy HH:mm:ss')}\n"
351+
report += "Completed at: ${SysHelper.fmtDate(new Date(endTimestamp))}\n"
351352
report += "Duration : ${new Duration(delta)}\n"
352353
report += "CPU hours : ${stats.getComputeTimeFmt()}\n"
353354
report += "Succeeded : ${stats.succeedCountFmt}\n"

modules/nextflow/src/main/groovy/nextflow/trace/ReportObserver.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import nextflow.processor.TaskId
2929
import nextflow.script.WorkflowMetadata
3030
import nextflow.trace.config.ReportConfig
3131
import nextflow.trace.event.TaskEvent
32+
import nextflow.util.SysHelper
3233
import nextflow.util.TestOnly
3334
/**
3435
* Render pipeline report processes execution.
@@ -213,6 +214,8 @@ class ReportObserver implements TraceObserverV2 {
213214
final tpl_fields = [
214215
workflow : getWorkflowMetadata(),
215216
payload : renderPayloadJson(),
217+
// Add SysHelper to template binding so it can be used in templates
218+
SysHelper : SysHelper,
216219
assets_css : [
217220
readTemplate('assets/bootstrap.min.css'),
218221
readTemplate('assets/datatables.min.css')

modules/nextflow/src/main/groovy/nextflow/trace/ReportSummary.groovy

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ class ReportSummary {
6464
}
6565
}
6666

67-
68-
6967
/**
7068
* Hold the summary for each series ie. cpu, memory, time, disk reads, disk writes
7169
*/

modules/nextflow/src/main/resources/nextflow/mail/notification.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ <h2>Execution summary</h2>
5454
<table cellpadding="4" >
5555
<tr>
5656
<td>Launch time</td>
57-
<td>${workflow.start.format('dd-MMM-yyyy HH:mm:ss')}</td>
57+
<td>${SysHelper.fmtDate(workflow.start)}</td>
5858
</tr>
5959

6060
<tr>
6161
<td>Ending time</td>
62-
<td>${workflow.complete.format('dd-MMM-yyyy HH:mm:ss')} (duration: ${workflow.duration})</td>
62+
<td>${SysHelper.fmtDate(workflow.complete)} (duration: ${workflow.duration})</td>
6363
</tr>
6464

6565
<tr>

modules/nextflow/src/main/resources/nextflow/mail/notification.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ The command used to launch the workflow was as follows:
2222

2323
** Execution summary **
2424

25-
Launch time : ${workflow.start.format('dd-MMM-yyyy HH:mm:ss')}
26-
Ending time : ${workflow.complete.format('dd-MMM-yyyy HH:mm:ss')} (duration: ${workflow.duration})
25+
Launch time : ${SysHelper.fmtDate(workflow.start)}
26+
Ending time : ${SysHelper.fmtDate(workflow.complete)} (duration: ${workflow.duration})
2727
Total CPU-Hours : ${workflow.stats.computeTimeFmt ?: '-'}
2828
Tasks stats : Succeeded ${workflow.stats.succeedCountFmt}; Cached ${workflow.stats.cachedCountFmt}; Ignored ${workflow.stats.ignoredCountFmt}; Failed ${workflow.stats.failedCountFmt}
2929
Launch directory : ${workflow.launchDir}

modules/nextflow/src/main/resources/nextflow/trace/ReportTemplate.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ <h4>Workflow execution completed unsuccessfully!</h4>
137137
<dl>
138138
<dt>Run times</dt>
139139
<dd>
140-
<span id="workflow_start">${workflow.start.format('dd-MMM-yyyy HH:mm:ss')}</span> - <span id="workflow_complete">${workflow.complete.format('dd-MMM-yyyy HH:mm:ss')}</span>
140+
<span id="workflow_start">${SysHelper.fmtDate(workflow.start)}</span> - <span id="workflow_complete">${SysHelper.fmtDate(workflow.complete)}</span>
141141
(<span id="completed_fromnow"></span>duration: <strong>${workflow.duration}</strong>)
142142
</dd>
143143

modules/nf-commons/src/main/nextflow/util/SysHelper.groovy

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ package nextflow.util
1919

2020
import java.lang.management.ManagementFactory
2121
import java.text.SimpleDateFormat
22+
import java.time.OffsetDateTime
2223

2324
import com.sun.management.OperatingSystemMXBean
2425
import groovy.transform.CompileStatic
2526
import groovy.transform.Memoized
2627
import groovy.transform.PackageScope
2728
import groovy.util.logging.Slf4j
29+
import nextflow.SysEnv
2830
import nextflow.file.FileHelper
2931
/**
3032
* System helper methods
@@ -37,7 +39,7 @@ class SysHelper {
3739

3840
public static final String DEFAULT_DOCKER_PLATFORM = 'linux/amd64'
3941

40-
private static String DATE_FORMAT = 'dd-MMM-yyyy HH:mm'
42+
private static final String DATE_FORMAT = 'dd-MMM-yyyy HH:mm:ss'
4143

4244
/**
4345
* Given a timestamp as epoch time convert to a string representation
@@ -66,11 +68,34 @@ class SysHelper {
6668
* The formatted date string
6769
*/
6870
static String fmtDate(Date date, TimeZone tz=null) {
69-
def formatter=new SimpleDateFormat(DATE_FORMAT, Locale.ENGLISH)
71+
final formatter=new SimpleDateFormat(fmtEnv(), Locale.ENGLISH)
7072
if(tz) formatter.setTimeZone(tz)
7173
formatter.format(date)
7274
}
7375

76+
/**
77+
* Given a {@link java.time.OffsetDateTime} object convert to a string representation
78+
* according the {@link #DATE_FORMAT}
79+
*
80+
* @param dateTime
81+
* The OffsetDateTime to render as a string
82+
* @return
83+
* The formatted date string
84+
*/
85+
static String fmtDate(OffsetDateTime dateTime) {
86+
// Convert to Date while preserving the original timezone
87+
final date = Date.from(dateTime.toInstant())
88+
final timezone = TimeZone.getTimeZone(dateTime.getOffset())
89+
fmtDate(date, timezone)
90+
}
91+
92+
static private String fmtEnv() {
93+
final result = SysEnv.get('NXF_DATE_FORMAT', DATE_FORMAT)
94+
return result.toLowerCase() == 'iso'
95+
? "yyyy-MM-dd'T'HH:mm:ssXXX"
96+
: result
97+
}
98+
7499
/**
75100
* Read the system uptime as returned by the {@code uptime} Linux tool
76101
*
@@ -105,7 +130,7 @@ class SysHelper {
105130
}
106131

107132
static String getBootTimeString() {
108-
new SimpleDateFormat(DATE_FORMAT).format(new Date(getBootTimeMillis()))
133+
fmtDate(new Date(getBootTimeMillis()))
109134
}
110135

111136
/**

0 commit comments

Comments
 (0)