You are here

Implementing custom reports

Assuming that you have started to see some data show up in the ElasticSearch store and therefore in the out-of-the-box reports, and you have used the Sense tool or cURL to develop some custom search queries of your own, you are ready to start implementing the custom Spring bean required in order to plug the report into the Process Services UI.
  1. Basic concepts

    A custom report is a custom section available in the Analytics app and also within each published app, which shows one or more custom reports.

    Each report is implemented by a Spring bean which is responsible for two things:

    1. Perform an ElasticSearch search query using the Java client API.
    2. Convert the search results (hits or aggregations) into chart or table data and add this to the response.

    The UI will automatically display the correct widgets based on the data that your bean sends.

  2. Bean implementation

    Your Spring bean will be discovered automatically via annotations but must be placed under the package com.activiti.service.reporting. Since this package is used for the out-of-the-box reports it is recommended that custom reports use the sub-package such as com.activiti.service.reporting.custom.

    The overall structure of the class will be as follows, for the full source please see the web link at the end of this section.

    package com.activiti.service.reporting.custom;
    
    import com.activiti.domain.reporting.ParametersDefinition;
    import com.activiti.domain.reporting.ReportDataRepresentation;
    import com.activiti.service.api.UserCache;
    import com.activiti.service.reporting.AbstractReportGenerator;
    import org.activiti.engine.ProcessEngine;
    import org.elasticsearch.client.Client;
    import org.springframework.stereotype.Component;
    
    import java.util.Map;
    
    @Component(CustomVariablesReportGenerator.ID)
    public class CustomVariablesReportGenerator extends AbstractReportGenerator {
    
        public static final String ID = "report.generator.fruitorders";
        public static final String NAME = "Fruit orders overview";
    
        @Override
        public String getID() {
            return ID;
        }
    
        @Override
        public String getName() {
            return NAME;
        }
    
        @Override
        public ParametersDefinition getParameterDefinitions(Map<String, Object> parameterValues) {
            return new ParametersDefinition();
        }
    
        @Override
        public ReportDataRepresentation generateReportData(ProcessEngine processEngine,
                                                           Client elasticSearchClient, String indexName, UserCache userCache,
                                                           Map<String, Object> parameterMap) {
    
            ReportDataRepresentation reportData = new ReportDataRepresentation();
    
            // Perform queries and add report data here
    
            return reportData;
        }

    You must implement the generateReportData() method which is declared abstract in the superclass, and you can choose to override the getParameterDefinitions() method if you need to collect some user-selected parameters from the UI to use in your query.

  3. Implementing generateReportData()

    The generateReportData() method of your bean is responsible for two things:

    • Perform one or more ElasticSearch queries to fetch report data

    • Populate chart/table data from the query results

    A protected helper method executeSearch() is provided which provides a concise syntax to execute an ElasticSearch search query given a query and optional aggregation, the implementation of which also provides logging of the query generated by the Java client API before it is sent. This can help with debugging your queries using Sense, or assist you in working out why the Java client is not generating the query you expect.

    return executeSearch(elasticSearchClient,
                    indexName,
                    ElasticSearchConstants.TYPE_VARIABLES,
                    new FilteredQueryBuilder(
                            new MatchAllQueryBuilder(),
                            FilterBuilders.andFilter(
                                    new TermFilterBuilder("processDefinitionKey", PROCESS_DEFINITION_KEY),
                                    new TermFilterBuilder("name._exact_name", "customername")
                            )
                    ),
                    AggregationBuilders.terms("customerOrders").field("stringValue._exact_string_value")
            );

    The log4j configuration required to log queries being sent to ElasticSearch via executeSearch() is as follows

    log4j.logger.com.activiti.service.reporting.AbstractReportGenerator=DEBUG

    Alternatively you can manually execute any custom query directly via the Client instance passed to the generateReportData() method, for example:

    return elasticSearchClient
                    .prepareSearch(indexName)
                    .setTypes(ElasticSearchConstants.TYPE_PROCESS_INSTANCES)
                    .setQuery(new FilteredQueryBuilder(new MatchAllQueryBuilder(), applyStatusProcessFilter(status)))
                    .addAggregation(
                            new TermsBuilder(AGGREGATION_PROCESS_DEFINITIONS).field(EventFields.PROCESS_DEFINITION_ID)
                                    .subAggregation(new FilterAggregationBuilder(AGGREGATION_COMPLETED_PROCESS_INSTANCES)
                                            .filter(new ExistsFilterBuilder(EventFields.END_TIME))
                                            .subAggregation(new ExtendedStatsBuilder(AGGREGATION_STATISTICS).field(EventFields.DURATION))));

    Generating chart data from queries can be accomplished easily using the converters in the com.activiti.service.reporting.converters package. This avoids the need to iterate over returned query results in order to populate chart data items.

    Initially two converters AggsToSimpleChartBasicConverter and AggsToMultiSeriesChartConverter are provided to populate data for pie charts (which take a single series of data) and bar charts (which take multiple series) respectively. These two classes are responsible for iterating over the structure of the ES data, while the member classes of com.activiti.service.reporting.converters.BucketExtractors are responsible for extracting an actual value from the buckets returned in the data.

    ReportDataRepresentation reportData = new ReportDataRepresentation();
    
    PieChartDataRepresentation pieChart = new PieChartDataRepresentation();
    pieChart.setTitle("No. of orders by customer");
    pieChart.setDescription("This chart shows the total number of orders placed by each customer");
    
    new AggsToSimpleChartBasicConverter(searchResponse, "customerOrders").setChartData(
            pieChart,
            new BucketExtractors.BucketKeyExtractor(),
            new BucketExtractors.BucketDocCountExtractor()
    );
    
    reportData.addReportDataElement(pieChart);
    
    SingleBarChartDataRepresentation chart = new SingleBarChartDataRepresentation();
    chart.setTitle("Total quantities ordered per month");
    chart.setDescription("This chart shows the total number of items that were ordered in each month");
    chart.setyAxisType("count");
    chart.setxAxisType("date_month");
    
    new AggsToMultiSeriesChartConverter(searchResponse, "ordersByMonth").setChartData(
            chart,
            new BucketExtractors.DateHistogramBucketExtractor(),
            new BucketExtractors.BucketAggValueExtractor("totalItems")
    );
    
    reportData.addReportDataElement(chart);

    For more details see the full source on the activiti-custom-reports GitHub project.