How to instrument custom Gradle Tasks with monitoring

I have a use case that I believe Gradle is able to solve elegantly. But as my knowledge is limited to basic Plugin development, I would likely end up with the wrong pattern if I didn’t consult the community before :slight_smile: I’ll try to summarise…

Let’s say I have a bunch of custom Gradle Tasks for different purposes which are used by hundreds of projects in our controlled environment. My need is to instrument those Tasks to report build statistics to some monitoring applications (i.e. Splunk) as, for instance, start and completion timestamps, the status, error message etc…
Basically, I need the Tasks to do something before and after its execution (probably just send HTTP POST requests) . The before and after events are the same for every Task.

Additionally, those events should be enabled/disabled by the project’s build configuration. For example, if the project provides ‘monitoring {…}’ configuration with server URL etc, these events are executed for each Tasks it runs. Otherwise, they are ignored.

As for the Task development, the devs shouldn’t have to care about implementing the behavior of those before and after events at all (after all, they are the same for every Task). It should be something as simple as apply-and-forget.


So, at first I thought I would need to implement my own customBaseTask - extending the DefaultTask - with the before and after events. Next, my custom Tasks would extend the customBaseTask

However, I’m afraid I might be getting it all wrong… Perhaps, there is already a pattern or feature that does that out-of-the-box with Gradle. Maybe this inheritance isn’t even necessary and Gradle is able to instrument even arbitrary Tasks already…

You see, I’m looking for a design pattern / best practice in Gradle. So, bear with me :slight_smile: and sorry for the long question.
Thanks, people!

I’d simply us an OperationCompletionListener instead.
That should give you all information you need.
Here a small ad-hoc example in Kotlin DSL which of course should be refactored into stand-alone class and properly used in plugin and so on:

object TaskLogger : OperationCompletionListener {
    override fun onFinish(event: FinishEvent?) {
        if (event is TaskFinishEvent) {
            println("${event.descriptor.taskPath} / ${event.result.startTime} / ${event.result.endTime}")
            println("Status: ${
                run {
                    when (val result = event.result) {
                        is TaskSuccessResult -> when {
                            result.isUpToDate -> "UP TO DATE"
                            result.isFromCache -> "FROM CACHE"
                            else -> "SUCCESSFUL"
                        }
                        is TaskSkippedResult -> "Skipped because of '${result.skipMessage}'"
                        is TaskFailureResult -> {
                            result.failures.joinToString("\n") {
                                it.message ?: ""
                            }
                        }
                        else -> error("Unexpected result type ${result::class}")
                    }
                }
            }")
        }
    }
}
interface BuildEventsListenerRegistryProvider {
    @get:Inject
    val buildEventsListenerRegistry: BuildEventsListenerRegistry
}
objects.newInstance<BuildEventsListenerRegistryProvider>().buildEventsListenerRegistry.onTaskCompletion(provider { TaskLogger })

Another thing you might consider is to get an instance of Gradle Enterprise which probably already gives you all information and statistics you want out of the box. :slight_smile:

1 Like

Thank you, Björn! I’ll try this out and mark this as Solution.

About the suggestion with Gradle Enterprise, well, my example of monitoring was an oversimplification of what I need to achieve. I am planning indeed to send event to a Splunk for statistics, and that’s covered by Gradle Enterprise. But there’s more to that: we use a nasty IBM tool (Rational Team Concert, for what matters) as Build interface for the devs. In there, Build Result objects keep track of progress, input and outputs etc. My objective is to send events to this tool as well, and that’s sadly irreplaceable in our ecosystem.

Thanks for the suggestion though! Very much appreciated!

Hi Björn. As I said, I needed to be able to add common actions before and after my custom Tasks. A Completion Listener alone wouldn’t help me much.
I can take the chance that the custom Tasks are created within our own project. That means, I need to instrument foreign Tasks at all. It’s all under out control.

So, I decided to proceed with a customBaseTask with the use of the Template design pattern.
Basically, my customBaseTask is an abstract Task defining some events in itself, leaving the “gaps” to be filled by the children Tasks.

public abstract class DefaultCITask extends DefaultTask {

    /** Gradle logger */
    protected Logger logger;

    protected DefaultCITask () {
        super.setGroup("Poject CI Tasks");
        super.setDescription(this.getDescription());
        this.logger = getProject().getLogger();
    }

    public abstract String getDescription();

    /**
     * Executes the full Task including precondition check, command execution and file publishing.
     * 
     * It uses the Template Design Pattern, in which certain steps are done in this order, but
     * the children Tasks must implement the individual steps on their own.
     */
    @TaskAction
    public void task() {
        logger.lifecycle("Executing VALIDATE");
        this.validate();

        logger.lifecycle("Executing EXECUTE");
        this.execute();

        logger.lifecycle("Executing PUBLISH");
        this.publish();
    }

    /**
     * Checks whether all the preconditions to execute the Task are met.
     */
    protected void validate() {

        this.getPreConditions().forEach(precondition -> {
            precondition.validate();
        });
    }

    /**
     * Processes the publishing by iterating over the list of provided files.
     */
    protected void publish() {

        this.getPublishers().forEach(publisher -> {
            publisher.publish();
        });
    }

    /**
     * Executes the actual Task.
     * For instance, it can process data, execute commands etc.
     */
    protected abstract void execute();

    /**
     * Retrieves the list of preconditions which are checked before the Task is executed.
     * Preconditions such as existing folders or set environment variables should
     * be provided in this method.
     * 
     * @return List of preconditions.
     */
    @Internal
    protected abstract List<BasePreCondition> getPreConditions();

    /**
     * Retrieves the list of publishers which are processed after the Task is executed.
     * Artifacts and Logs to be published should be provided in this method.
     * 
     * @return List of files to be published.
     */
    @Internal
    protected abstract List<BasePublisher> getPublishers();

}

With that, my custom Tasks only need to implement the core processing (execute()), and a list of preconditions (getPreConditions()) and publishers (getPublishers()) which are also provided by our plugin.

public class EccStatusTask extends DefaultCITask {

    /** Name of the Test Execution as displayed in the Ecc interface. */
    private String testExecutionName;

    @Option(option = "testExecution", description = "Name of the 'Test Execution' in the ECC.")
    public void setTestExecutionName(String testExecutionName) {
        this.testExecutionName = testExecutionName;
    }

    @Override
    public void execute() {
        EccClient eccClient = new EccClient();
        eccClient.getStatus(testExecutionName);
    }

    @Override
    public String getDescription() {
        return "Retrieves simulation status from the ECC environment.";
    }

    @Override
    protected List<BasePreCondition> getPreConditions() {
        List<BasePreCondition> preconditions = new ArrayList<BasePreCondition>();
        preconditions.add(new EnvVarMustBeSet("ECC_CLI_HOME"));
        return preconditions;
    }

    @Override
    protected List<BasePublisher> getPublishers() {
        List<BasePublisher> publishers = new ArrayList<BasePublisher>();
        return publishers;
    }
}

Once again, I really appreciate your answer! I’m glad to know about the existence of the OperationCompletionListener.
But if you think what I’m doing here isn’t utterly stupid, I’ll end up marking my own answer as the correct one, because it’s the closest I’ve got to my needs.
Thanks!

Sounds perfectly fine.
You could even enrich 3rd party tasks using doFirst() or doLast() actions.
But as all are own tasks, having a base task with the enclosing logic sounds perfectly fine.

1 Like