From f9f2c338f9fdfdc637c238fab6ffdeeb7e15f17e Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Tue, 18 Jul 2023 19:41:31 +0200 Subject: [PATCH] Implement Weblog feature as an external plugin This commit remove the weblog as a core feature and move it into a plugin maintained separately. See the link below https://github.com/nextflow-io/nf-weblog Signed-off-by: Paolo Di Tommaso --- docs/config.md | 14 - docs/tracing.md | 202 ------------- .../nextflow/config/ConfigBuilder.groovy | 3 +- .../trace/DefaultObserverFactory.groovy | 15 - .../nextflow/trace/WebLogObserver.groovy | 268 ------------------ .../nextflow/config/ConfigBuilderTest.groovy | 3 +- .../nextflow/trace/WebLogObserverTest.groovy | 120 -------- .../main/nextflow/plugin/PluginsFacade.groovy | 3 + 8 files changed, 5 insertions(+), 623 deletions(-) delete mode 100644 modules/nextflow/src/main/groovy/nextflow/trace/WebLogObserver.groovy delete mode 100644 modules/nextflow/src/test/groovy/nextflow/trace/WebLogObserverTest.groovy diff --git a/docs/config.md b/docs/config.md index f4e740f71d..19f68d5418 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1410,20 +1410,6 @@ trace { Read the {ref}`trace-report` page to learn more about the execution report that can be generated by Nextflow. -(config-weblog)= - -### Scope `weblog` - -The `weblog` scope allows you to send detailed {ref}`trace ` information as HTTP POST requests to a webserver, shipped as a JSON object. - -Detailed information about the JSON fields can be found in the {ref}`weblog description`. - -`weblog.enabled` -: If `true` it will send HTTP POST requests to a given url. - -`weblog.url` -: The url where to send HTTP POST requests (default: `http:localhost`). - (config-miscellaneous)= ### Miscellaneous diff --git a/docs/tracing.md b/docs/tracing.md index 98cdb167b4..686e3173cc 100644 --- a/docs/tracing.md +++ b/docs/tracing.md @@ -426,205 +426,3 @@ And the final image produced with the [Mermaid Live Editor](https://mermaid-js.g ```{image} images/dag-mermaid.png ``` - -(weblog-service)= - -## Weblog via HTTP - -Nextflow can send detailed workflow execution metadata and runtime statistics to a HTTP endpoint. To enable this feature, use the `-with-weblog` as shown below: - -```bash -nextflow run -with-weblog [url] -``` - -Workflow events are sent as HTTP POST requests to the given URL. The message consists of the following JSON structure: - -```json -{ - "runName": "", - "runId": "", - "event": "", - "utcTime": "", - "trace": { }, - "metadata": { } -} -``` - -The JSON object contains the following attributes: - -`runName` -: The workflow execution run name. - -`runId` -: The workflow execution unique ID. - -`event` -: The workflow execution event. One of `started`, `process_submitted`, `process_started`, `process_completed`, `error`, `completed`. - -`utcTime` -: The UTC timestamp in ISO 8601 format. - -`trace` -: *Included only for the following events: `process_submitted`, `process_started`, `process_completed`, `error`* -: The task runtime information as described in the {ref}`trace fields` section. -: The set of included fields is determined by the `trace.fields` setting in the Nextflow configuration file. See the {ref}`Trace configuration` and [Trace report](#trace-report) sections to learn more. - -`metadata` -: *Included only for the following events: `started`, `completed`* -: The workflow metadata including the {ref}`config manifest`. For a list of all fields, have a look at the bottom message examples. - -### Example `started` event - -When a workflow execution is started, a message like the following is posted to the specified end-point. Be aware that the properties in the parameter scope will look different for your workflow. Here is an example output from the `nf-core/hlatyping` pipeline with the weblog feature enabled: - -```json -{ - "runName": "friendly_pesquet", - "runId": "170aa09c-105f-49d0-99b4-8eb6a146e4a7", - "event": "started", - "utcTime": "2018-10-07T11:42:08Z", - "metadata": { - "params": { - "container": "nfcore/hlatyping:1.1.4", - "help": false, - "outdir": "results", - "bam": true, - "singleEnd": false, - "single-end": false, - "reads": "data/test*{1,2}.fq.gz", - "seqtype": "dna", - "solver": "glpk", - "igenomes_base": "./iGenomes", - "multiqc_config": "/Users/sven1103/.nextflow/assets/nf-core/hlatyping/conf/multiqc_config.yaml", - "clusterOptions": false, - "cluster-options": false, - "enumerations": 1, - "beta": 0.009, - "prefix": "hla_run", - "base_index": "/Users/sven1103/.nextflow/assets/nf-core/hlatyping/data/indices/yara/hla_reference_", - "index": "/Users/sven1103/.nextflow/assets/nf-core/hlatyping/data/indices/yara/hla_reference_dna", - "custom_config_version": "master", - "custom_config_base": "https://raw.githubusercontent.com/nf-core/configs/master" - }, - "workflow": { - "start": "2019-03-25T12:09:52Z", - "projectDir": "/Users/sven1103/.nextflow/assets/nf-core/hlatyping", - "manifest": { - "nextflowVersion": ">=18.10.1", - "defaultBranch": "master", - "version": "1.1.4", - "homePage": "https://github.com/nf-core/hlatyping", - "gitmodules": null, - "description": "Precision HLA typing from next-generation sequencing data.", - "name": "nf-core/hlatyping", - "mainScript": "main.nf", - "author": null - }, - "complete": null, - "profile": "docker,test", - "homeDir": "/Users/sven1103", - "workDir": "/Users/sven1103/git/nextflow/work", - "container": "nfcore/hlatyping:1.1.4", - "commitId": "4bcced898ee23600bd8c249ff085f8f88db90e7c", - "errorMessage": null, - "repository": "https://github.com/nf-core/hlatyping.git", - "containerEngine": "docker", - "scriptFile": "/Users/sven1103/.nextflow/assets/nf-core/hlatyping/main.nf", - "userName": "sven1103", - "launchDir": "/Users/sven1103/git/nextflow", - "runName": "shrivelled_cantor", - "configFiles": [ - "/Users/sven1103/.nextflow/assets/nf-core/hlatyping/nextflow.config" - ], - "sessionId": "7f344978-999c-480d-8439-741bc7520f6a", - "errorReport": null, - "scriptId": "2902f5aa7f297f2dccd6baebac7730a2", - "revision": "master", - "exitStatus": null, - "commandLine": "./launch.sh run nf-core/hlatyping -profile docker,test -with-weblog 'http://localhost:4567'", - "nextflow": { - "version": "19.03.0-edge", - "build": 5137, - "timestamp": "2019-03-28T14:46:55Z" - }, - }, - "stats": { - "computeTimeFmt": "(a few seconds)", - "cachedCount": 0, - "cachedDuration": 0, - "failedDuration": 0, - "succeedDuration": 0, - "failedCount": 0, - "cachedPct": 0.0, - "cachedCountFmt": "0", - "succeedCountFmt": "0", - "failedPct": 0.0, - "failedCountFmt": "0", - "ignoredCountFmt": "0", - "ignoredCount": 0, - "succeedPct": 0.0, - "succeedCount": 0, - "ignoredPct": 0.0 - }, - "resume": false, - "success": false, - "scriptName": "main.nf", - "duration": null - } -} -``` - -### Example `completed` event - -When a task is completed, a message like the following is posted to the specified end-point: - -```json -{ - "runName": "friendly_pesquet", - "runId": "170aa09c-105f-49d0-99b4-8eb6a146e4a7", - "event": "process_completed", - "utcTime": "2018-10-07T11:45:30Z", - "trace": { - "task_id": 2, - "status": "COMPLETED", - "hash": "a1/0024fd", - "name": "make_ot_config", - "exit": 0, - "submit": 1538912529498, - "start": 1538912529629, - "process": "make_ot_config", - "tag": null, - "module": [ - - ], - "container": "nfcore/hlatyping:1.1.1", - "attempt": 1, - "script": "\n configbuilder --max-cpus 2 --solver glpk > config.ini\n ", - "scratch": null, - "workdir": "/home/sven1103/git/hlatyping-workflow/work/a1/0024fd028375e2b601aaed44d112e3", - "queue": null, - "cpus": 1, - "memory": 7516192768, - "disk": null, - "time": 7200000, - "env": "PATH=/home/sven1103/git/hlatyping-workflow/bin:$PATH\n", - "error_action": null, - "complete": 1538912730599, - "duration": 201101, - "realtime": 69, - "%cpu": 0.0, - "%mem": 0.1, - "vmem": 54259712, - "rss": 10469376, - "peak_vmem": 20185088, - "peak_rss": 574972928, - "rchar": 7597, - "wchar": 162, - "syscr": 16, - "syscw": 4083712, - "read_bytes": 4096, - "write_bytes": 0, - "native_id": 27185 - } -} -``` diff --git a/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy index 0ed8d4c6b2..bb963d1684 100644 --- a/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy @@ -39,7 +39,6 @@ import nextflow.trace.GraphObserver import nextflow.trace.ReportObserver import nextflow.trace.TimelineObserver import nextflow.trace.TraceFileObserver -import nextflow.trace.WebLogObserver import nextflow.util.HistoryFile import nextflow.util.SecretHelper /** @@ -678,7 +677,7 @@ class ConfigBuilder { if( cmdRun.withWebLog != '-' ) config.weblog.url = cmdRun.withWebLog else if( !config.weblog.url ) - config.weblog.url = WebLogObserver.DEF_URL + config.weblog.url = 'http://localhost' } // -- sets tower options diff --git a/modules/nextflow/src/main/groovy/nextflow/trace/DefaultObserverFactory.groovy b/modules/nextflow/src/main/groovy/nextflow/trace/DefaultObserverFactory.groovy index 42325c723a..6c391625c9 100644 --- a/modules/nextflow/src/main/groovy/nextflow/trace/DefaultObserverFactory.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/trace/DefaultObserverFactory.groovy @@ -24,7 +24,6 @@ class DefaultObserverFactory implements TraceObserverFactory { createReportObserver(result) createTimelineObserver(result) createDagObserver(result) - createWebLogObserver(result) createAnsiLogObserver(result) return result } @@ -36,20 +35,6 @@ class DefaultObserverFactory implements TraceObserverFactory { } } - /** - * Create workflow message observer - * @param result - */ - protected void createWebLogObserver(Collection result) { - Boolean isEnabled = config.navigate('weblog.enabled') as Boolean - String url = config.navigate('weblog.url') as String - if ( isEnabled ) { - if ( !url ) url = WebLogObserver.DEF_URL - def observer = new WebLogObserver(url) - result << observer - } - } - /** * Create workflow report file observer */ diff --git a/modules/nextflow/src/main/groovy/nextflow/trace/WebLogObserver.groovy b/modules/nextflow/src/main/groovy/nextflow/trace/WebLogObserver.groovy deleted file mode 100644 index ae7bab3c69..0000000000 --- a/modules/nextflow/src/main/groovy/nextflow/trace/WebLogObserver.groovy +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2013-2023, Seqera Labs - * Copyright 2018, University of Tübingen, Quantitative Biology Center (QBiC) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package nextflow.trace - -import groovy.json.JsonGenerator -import groovy.transform.CompileStatic -import groovy.util.logging.Slf4j -import groovyx.gpars.agent.Agent -import nextflow.Const -import nextflow.NextflowMeta -import nextflow.Session -import nextflow.processor.TaskHandler -import nextflow.script.ScriptBinding.ParamsMap -import nextflow.script.WorkflowMetadata -import nextflow.util.Duration -import nextflow.util.SimpleHttpClient - -import java.nio.file.Path - -/** - * Send out messages via HTTP to a configured URL on different workflow - * execution events. - * - * @author Sven Fillinger - * @author Paolo Di Tommaso - */ -@Slf4j -@CompileStatic -class WebLogObserver implements TraceObserver{ - - private Session session - - private static final TimeZone UTC = TimeZone.getTimeZone("UTC") - - /** - * Workflow identifier, will be taken from the Session() object later - */ - private String runName - - /** - * Store the sessions unique ID for downstream reference purposes - */ - private String runId - - /** - * The default url is localhost - */ - static public String DEF_URL = 'http://localhost' - - /** - * Simple http client object that will send out messages - */ - private SimpleHttpClient httpClient = new SimpleHttpClient() - - /** - * An agent for the http request in an own thread - */ - private Agent webLogAgent - - /** - * Json generator for weblog payloads - */ - private JsonGenerator generator - - private String endpoint - - /** - * Constructor that consumes a URL and creates - * a basic HTTP client. - * @param url The target address for sending messages to - */ - WebLogObserver(String url) { - this.endpoint = checkUrl(url) - this.webLogAgent = new Agent<>(this) - this.generator = createJsonGeneratorForPayloads() - } - - /** - * only for testing purpose -- do not use - */ - protected WebLogObserver() { - - } - - /** - * Check the URL and create an HttpPost() object. If a invalid i.e. protocol is used, - * the constructor will raise an exception. - * - * The RegEx was taken and adapted from http://urlregex.com - * - * @param url String with target URL - * @return The requested url or the default url, if invalid - */ - protected String checkUrl(String url){ - if( url =~ "^(https|http)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]" ) { - return url - } - throw new IllegalArgumentException("Only http and https are supported -- The given URL was: ${url}") - } - - /** - * On workflow start, submit a message with some basic - * information, like Id, activity and an ISO 8601 formatted - * timestamp. - * @param session The current Nextflow session object - */ - @Override - void onFlowCreate(Session session) { - this.session = session - runName = session.getRunName() - runId = session.getUniqueId() - - asyncHttpMessage("started", createFlowPayloadFromSession(session)) - } - - /** - * Send an HTTP message when the workflow is completed. - */ - @Override - void onFlowComplete() { - asyncHttpMessage("completed", createFlowPayloadFromSession(this.session)) - webLogAgent.await() - } - - /** - * Send an HTTP message when a process has been submitted - * - * @param handler A {@link TaskHandler} object representing the task submitted - * @param trace A {@link TraceRecord} object holding the task metadata and runtime info - */ - @Override - void onProcessSubmit(TaskHandler handler, TraceRecord trace) { - asyncHttpMessage("process_submitted", trace) - } - - /** - * Send an HTTP message, when a process has started - * - * @param handler A {@link TaskHandler} object representing the task started - * @param trace A {@link TraceRecord} object holding the task metadata and runtime info - */ - @Override - void onProcessStart(TaskHandler handler, TraceRecord trace) { - asyncHttpMessage("process_started", trace) - } - - /** - * Send an HTTP message, when a process completed - * - * @param handler A {@link TaskHandler} object representing the task completed - * @param trace A {@link TraceRecord} object holding the task metadata and runtime info - */ - @Override - void onProcessComplete(TaskHandler handler, TraceRecord trace) { - asyncHttpMessage("process_completed", trace) - } - - /** - * Send an HTTP message, when a workflow has failed - * - * @param handler A {@link TaskHandler} object representing the task that caused the workflow execution to fail (it may be null) - * @param trace A {@link TraceRecord} object holding the task metadata and runtime info (it may be null) - */ - @Override - void onFlowError(TaskHandler handler, TraceRecord trace) { - asyncHttpMessage("error", trace) - } - - /** - * Little helper method that sends a HTTP POST message as JSON with - * the current run status, ISO 8601 UTC timestamp, run name and the TraceRecord - * object, if present. - * @param event The current run status. One of {'started', 'process_submit', 'process_start', - * 'process_complete', 'error', 'completed'} - * @param payload An additional object to send. Must be of type TraceRecord or Manifest - */ - protected void sendHttpMessage(String event, Object payload = null){ - - // Set the message info - final time = new Date().format(Const.ISO_8601_DATETIME_FORMAT, UTC) - - final message = new HashMap(4) - message.runName = runName - message.runId = runId - message.event = event - message.utcTime = time - - if (payload instanceof TraceRecord) - message.trace = (payload as TraceRecord).store - else if (payload instanceof FlowPayload) - message.metadata = payload - else if (payload != null) - throw new IllegalArgumentException("Only TraceRecord and Manifest class types are supported: [${payload.getClass().getName()}] $payload") - - // The actual HTTP request - httpClient.sendHttpMessage(endpoint, generator.toJson(message)) - logHttpResponse() - } - - protected static FlowPayload createFlowPayloadFromSession(Session session) { - def params = session.binding.getProperty('params') as ParamsMap - def workflow = session.getWorkflowMetadata() - new FlowPayload(params, workflow) - } - - /** - * Asynchronous HTTP POST request wrapper. - * @param event The workflow run status - * @param payload An additional object to send. Must be of type TraceRecord or Manifest - */ - protected void asyncHttpMessage(String event, Object payload = null){ - webLogAgent.send{sendHttpMessage(event, payload)} - } - - /** - * Little helper function that can be called for logging upon an incoming HTTP response - */ - protected void logHttpResponse(){ - def statusCode = httpClient.getResponseCode() - if (statusCode >= 200 && statusCode < 300) { - log.debug "Successfully sent message to ${endpoint} -- received status code ${statusCode}." - } else { - def msg = """\ - Unexpected HTTP response. - Failed to send message to ${endpoint} -- received - - status code : $statusCode - - response msg: ${httpClient.getResponse()} - """.stripIndent(true) - log.debug msg - } - } - - private static JsonGenerator createJsonGeneratorForPayloads() { - new JsonGenerator.Options() - .addConverter(Path) { Path p, String key -> p.toUriString() } - .addConverter(Duration) { Duration d, String key -> d.durationInMillis } - .addConverter(NextflowMeta) { meta, key -> meta.toJsonMap() } - .dateFormat(Const.ISO_8601_DATETIME_FORMAT).timezone("UTC") - .build() - } - - private static class FlowPayload { - - final ParamsMap parameters - - final WorkflowMetadata workflow - - FlowPayload(ParamsMap params, WorkflowMetadata workflow ) { - this.parameters = params - this.workflow = workflow - } - } -} diff --git a/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy b/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy index 048fb1a3e4..c5d9b5d6bc 100644 --- a/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy @@ -27,7 +27,6 @@ import nextflow.cli.Launcher import nextflow.exception.AbortOperationException import nextflow.exception.ConfigParseException import nextflow.trace.TraceHelper -import nextflow.trace.WebLogObserver import nextflow.util.ConfigHelper import spock.lang.Ignore import spock.lang.Specification @@ -1000,7 +999,7 @@ class ConfigBuilderTest extends Specification { then: config.weblog instanceof Map config.weblog.enabled - config.weblog.url == WebLogObserver.DEF_URL + config.weblog.url == 'http://localhost' } diff --git a/modules/nextflow/src/test/groovy/nextflow/trace/WebLogObserverTest.groovy b/modules/nextflow/src/test/groovy/nextflow/trace/WebLogObserverTest.groovy deleted file mode 100644 index 56e4dffa83..0000000000 --- a/modules/nextflow/src/test/groovy/nextflow/trace/WebLogObserverTest.groovy +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2013-2023, Seqera Labs - * Copyright 2018, University of Tübingen, Quantitative Biology Center (QBiC) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package nextflow.trace - -import spock.lang.Specification - -import groovy.json.JsonGenerator -import groovy.json.JsonSlurper -import nextflow.Session -import nextflow.processor.TaskHandler -import nextflow.script.ScriptBinding -import nextflow.script.WorkflowMetadata -import nextflow.util.SimpleHttpClient - -class WebLogObserverTest extends Specification { - - def 'do not send messages on wrong formatted url'() { - - when: - new WebLogObserver("localhost") - - then: - thrown(IllegalArgumentException) - } - - def 'send message on different workflow events' () { - - given: - WebLogObserver httpPostObserver0 = Spy(WebLogObserver, constructorArgs: ["http://localhost"]) - WorkflowMetadata workflowMeta = Mock(WorkflowMetadata) - - def bindingStub = Mock(ScriptBinding){ - getProperty('params') >> new ScriptBinding.ParamsMap() - } - def sessionStub = Mock(Session){ - getRunName() >> "testRun" - getUniqueId() >> UUID.randomUUID() - getBinding() >> bindingStub - getWorkflowMetadata() >> workflowMeta - } - def traceStub = Mock(TraceRecord) - def handlerStub = Mock(TaskHandler) - - when: - def payload = WebLogObserver.createFlowPayloadFromSession(sessionStub) - httpPostObserver0.onFlowCreate(sessionStub) - httpPostObserver0.onProcessSubmit(handlerStub, traceStub) - httpPostObserver0.onProcessStart(handlerStub, traceStub) - httpPostObserver0.onProcessComplete(handlerStub, traceStub) - httpPostObserver0.onFlowError(handlerStub, traceStub) - httpPostObserver0.onFlowComplete() - - then: - assert payload.workflow instanceof WorkflowMetadata - 6 * httpPostObserver0.asyncHttpMessage(!null, !null) >> null - - } - - def 'should create a json message' () { - - given: - def observer = Spy(WebLogObserver) - def CLIENT = Mock(SimpleHttpClient) - observer.@endpoint = 'http://foo.com' - observer.@httpClient = CLIENT - observer.@runName = 'foo' - observer.@runId = 'xyz' - observer.@generator = new JsonGenerator.Options().build() - def TRACE = new TraceRecord([hash: '4a4a4a', process: 'bar']) - - when: - observer.sendHttpMessage('started', TRACE) - - then: - 1 * observer.logHttpResponse() >> null - 1 * CLIENT.sendHttpMessage( 'http://foo.com', _ as String ) >> { it -> - def message = (Map)new JsonSlurper().parseText((String)it[1]) - assert message.runName == 'foo' - assert message.runId == 'xyz' - assert message.event == 'started' - assert message.trace.hash == '4a4a4a' - assert message.trace.process == 'bar' - return null - } - - } - - def 'should validate URL' () { - given: - def observer = new WebLogObserver() - - expect: - observer.checkUrl('http://localhost') == 'http://localhost' - observer.checkUrl('http://google.com') == 'http://google.com' - observer.checkUrl('https://google.com') == 'https://google.com' - observer.checkUrl('http://google.com:8080') == 'http://google.com:8080' - observer.checkUrl('http://google.com:8080/foo/bar') == 'http://google.com:8080/foo/bar' - - when: - observer.checkUrl('ftp://localhost') - then: - def e = thrown(IllegalArgumentException) - e.message == 'Only http and https are supported -- The given URL was: ftp://localhost' - } -} diff --git a/modules/nf-commons/src/main/nextflow/plugin/PluginsFacade.groovy b/modules/nf-commons/src/main/nextflow/plugin/PluginsFacade.groovy index db68dbe6a7..bc5049d305 100644 --- a/modules/nf-commons/src/main/nextflow/plugin/PluginsFacade.groovy +++ b/modules/nf-commons/src/main/nextflow/plugin/PluginsFacade.groovy @@ -401,6 +401,9 @@ class PluginsFacade implements PluginStateListener { if( executor == 'azurebatch' || workDir?.startsWith('az://') || bucketDir?.startsWith('az://') ) plugins << defaultPlugins.getPlugin('nf-azure') + if( Bolts.navigate(config, 'weblog.enabled')) + plugins << new PluginSpec('nf-weblog') + return plugins }