This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new a4f2c1272d Issue #3003 : Add Output transform metrics transform (#8178)
a4f2c1272d is described below
commit a4f2c1272d7c002e926b538f5c70b29e8c1e2f36
Author: Matt Casters <[email protected]>
AuthorDate: Mon Aug 31 09:41:22 2026 +0200
Issue #3003 : Add Output transform metrics transform (#8178)
---
assemblies/debug/pom.xml | 6 +
assemblies/plugins/pom.xml | 6 +
.../images/transforms/icons/transformsmetrics.svg | 23 ++
.../output-transform-metrics-dialog-fields-tab.png | Bin 0 -> 99459 bytes
...put-transform-metrics-dialog-transforms-tab.png | Bin 0 -> 76419 bytes
docs/hop-user-manual/modules/ROOT/nav.adoc | 1 +
.../modules/ROOT/pages/pipeline/transforms.adoc | 1 +
.../transforms/outputtransformmetrics.adoc | 91 ++++++
.../transforms/0110-output-transform-metrics.hpl | 254 +++++++++++++++
.../datasets/golden-output-transform-metrics.csv | 3 +
.../main-0110-output-transform-metrics.hwf | 80 +++++
.../dataset/golden-output-transform-metrics.json | 32 ++
.../0110-output-transform-metrics UNIT.json | 36 +++
.../org/apache/hop/imports/kettle/KettleConst.java | 12 +
.../kettle/KettleImportTransformsMetricsTest.java | 128 ++++++++
plugins/transforms/pom.xml | 1 +
plugins/transforms/transformsmetrics/pom.xml | 32 ++
.../transformsmetrics/src/assembly/assembly.xml | 46 +++
.../transformsmetrics/MetricTransform.java | 58 ++++
.../transformsmetrics/TransformsMetrics.java | 203 ++++++++++++
.../transformsmetrics/TransformsMetricsData.java | 45 +++
.../transformsmetrics/TransformsMetricsDialog.java | 253 +++++++++++++++
.../transformsmetrics/TransformsMetricsMeta.java | 358 +++++++++++++++++++++
.../messages/messages_en_US.properties | 68 ++++
.../src/main/resources/transformsmetrics.svg | 23 ++
.../src/main/resources/version.xml | 20 ++
.../transforms/output-transform-metrics-basic.hpl | 201 ++++++++++++
.../TransformsMetricsMetaTest.java | 151 +++++++++
28 files changed, 2132 insertions(+)
diff --git a/assemblies/debug/pom.xml b/assemblies/debug/pom.xml
index dd528af8b8..d87ba26f14 100644
--- a/assemblies/debug/pom.xml
+++ b/assemblies/debug/pom.xml
@@ -553,6 +553,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-transform-transformsmetrics</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-validator</artifactId>
diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml
index 3d3fe213e9..69000d8f74 100644
--- a/assemblies/plugins/pom.xml
+++ b/assemblies/plugins/pom.xml
@@ -1530,6 +1530,12 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-transform-transformsmetrics</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-uniquerows</artifactId>
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/transformsmetrics.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/transformsmetrics.svg
new file mode 100644
index 0000000000..6829085d35
--- /dev/null
+++
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/transformsmetrics.svg
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+ width="42px" height="42px" viewBox="0 0 42 42">
+ <rect x="6" y="24" width="7" height="12" fill="#0E3A5A"/>
+ <rect x="17.5" y="14" width="7" height="22" fill="#0E3A5A"/>
+ <rect x="29" y="6" width="7" height="30" fill="#EC0800"/>
+</svg>
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/output-transform-metrics-dialog-fields-tab.png
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/output-transform-metrics-dialog-fields-tab.png
new file mode 100644
index 0000000000..c54e155179
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/output-transform-metrics-dialog-fields-tab.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/output-transform-metrics-dialog-transforms-tab.png
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/output-transform-metrics-dialog-transforms-tab.png
new file mode 100644
index 0000000000..5d46bf31b7
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/output-transform-metrics-dialog-transforms-tab.png
differ
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index 7e3762a580..5c7aeeca61 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -227,6 +227,7 @@ under the License.
*** xref:pipeline/transforms/numberrange.adoc[Number range]
*** xref:pipeline/transforms/odata-input.adoc[OData input]
*** xref:pipeline/transforms/orabulkloader.adoc[Oracle Bulk Loader]
+*** xref:pipeline/transforms/outputtransformmetrics.adoc[Output transform
metrics]
*** xref:pipeline/transforms/parquet-file-input.adoc[Parquet file input]
*** xref:pipeline/transforms/parquet-file-output.adoc[Parquet file output]
*** xref:pipeline/transforms/pipeline-data-probe.adoc[Pipeline Data Probe]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
index 15f47959a2..b4e71ff802 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
@@ -177,6 +177,7 @@ Those are explained once in
xref:pipeline/formatting-values.adoc[Formatting numb
* xref:pipeline/transforms/numberrange.adoc[Number range]
* xref:pipeline/transforms/odata-input.adoc[OData input]
* xref:pipeline/transforms/orabulkloader.adoc[Oracle Bulk Loader]
+* xref:pipeline/transforms/outputtransformmetrics.adoc[Output transform
metrics]
* xref:pipeline/transforms/parquet-file-input.adoc[Parquet file input]
* xref:pipeline/transforms/parquet-file-output.adoc[Parquet file output]
* xref:pipeline/transforms/pipeline-data-probe.adoc[Pipeline Data Probe]
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/outputtransformmetrics.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/outputtransformmetrics.adoc
new file mode 100644
index 0000000000..db5fd7ac81
--- /dev/null
+++
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/outputtransformmetrics.adoc
@@ -0,0 +1,91 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+:documentationPath: /pipeline/transforms/
+:language: en_US
+:description: Wait for selected transforms to finish and output their metrics
as rows.
+
+= image:transforms/icons/transformsmetrics.svg[Output transform metrics Icon,
role="image-doc-icon"] Output transform metrics
+
+[%noheader,cols="3a,1a", role="table-no-borders" ]
+|===
+|
+== Description
+
+Output transform metrics waits until the listed transform copies in the same
pipeline have finished, then emits *one output row per watched copy* with the
requested counters.
+
+This transform is a *row generator*. It does not consume incoming rows and
must not have incoming hops. Connecting an upstream hop can deadlock the
pipeline: the watched transform cannot finish while its output buffer is full,
and this transform never drains that buffer.
+
+Use this transform when a pipeline itself needs metrics (for example after a
Kettle/PDI import, or to feed Write to log). For project-wide monitoring,
prefer xref:metadata-types/pipeline-log.adoc[Pipeline logging] or
xref:pipeline/transforms/execinfo.adoc[Execution information].
+
+|
+== Supported Engines
+[%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
+!===
+!Hop Engine! image:check_mark.svg[Supported, 24]
+!Single Threaded! image:cross.svg[Not Supported, 24]
+!Native Spark! image:cross.svg[Not Supported, 24]
+!Beam Spark! image:cross.svg[Not Supported, 24]
+!Beam Flink! image:cross.svg[Not Supported, 24]
+!Beam Dataflow! image:cross.svg[Not Supported, 24]
+!===
+|===
+
+== Options
+
+=== Transforms
+
+image:transforms/output-transform-metrics-dialog-transforms-tab.png[Output
transform metrics Transforms tab, width="90%"]
+
+[options="header"]
+|===
+|Option|Description
+|Transform name|Name of this transform.
+|Metrics for the following transforms|The transform copies to wait for. You
cannot watch this transform itself or a downstream target of it.
+|Get Fields|Fill the grid with every other transform that is not a downstream
target.
+|===
+
+[options="header"]
+|===
+|Column|Description
+|Transform name|Name of the transform to watch.
+|Copy Nr|0-based copy number. Use one grid row per copy if the transform runs
multiple copies. Variables are allowed.
+|Required|When Yes, the pipeline fails if that copy cannot be found. When No,
a missing copy is skipped.
+|===
+
+=== Fields
+
+image:transforms/output-transform-metrics-dialog-fields-tab.png[Output
transform metrics Fields tab, width="90%"]
+
+Leave a field name empty to omit that column from the output.
+
+[options="header"]
+|===
+|Option|Description
+|Transform name|Name of the watched transform.
+|Transform id|Plugin id of the watched transform.
+|Lines input|Rows read from an external source.
+|Lines output|Rows written to an external destination.
+|Lines read|Rows read from previous transforms.
+|Lines updated|Rows updated.
+|Lines written|Rows written to the next transforms.
+|Lines rejected|Rows sent to an error hop (`getLinesRejected()`). This is
*not* the error count.
+|Duration (ms)|Execution duration in milliseconds.
+|===
+
+== Kettle / PDI import
+
+Kettle called this transform *Output steps metrics* (`StepsMetrics`). Hop
import rewrites the plugin type to `TransformsMetrics` and the XML tags from
`step*` to `transform*`. Stored *values* of output field names (for example
`Step name`) are left unchanged so existing pipelines keep their column names.
diff --git a/integration-tests/transforms/0110-output-transform-metrics.hpl
b/integration-tests/transforms/0110-output-transform-metrics.hpl
new file mode 100644
index 0000000000..0f3a5808bc
--- /dev/null
+++ b/integration-tests/transforms/0110-output-transform-metrics.hpl
@@ -0,0 +1,254 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+
+-->
+<pipeline>
+ <info>
+ <name>0110-output-transform-metrics</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/08/29 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/29 12:00:00.000</modified_date>
+ <key_for_session_key/>
+ <is_key_private>N</is_key_private>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>10 rows</from>
+ <to>Dummy A</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>5 rows</from>
+ <to>Dummy B</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Output transform metrics</from>
+ <to>Sort</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Sort</from>
+ <to>Verify</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>10 rows</name>
+ <type>RowGenerator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>value</name>
+ <type>Integer</type>
+ <format>#</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif>1</nullif>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <limit>10</limit>
+ <never_ending>N</never_ending>
+ <interval_in_ms>5000</interval_in_ms>
+ <row_time_field>now</row_time_field>
+ <last_time_field>FiveSecondsAgo</last_time_field>
+ <attributes/>
+ <GUI>
+ <xloc>80</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>5 rows</name>
+ <type>RowGenerator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>value</name>
+ <type>Integer</type>
+ <format>#</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif>1</nullif>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <limit>5</limit>
+ <never_ending>N</never_ending>
+ <interval_in_ms>5000</interval_in_ms>
+ <row_time_field>now</row_time_field>
+ <last_time_field>FiveSecondsAgo</last_time_field>
+ <attributes/>
+ <GUI>
+ <xloc>80</xloc>
+ <yloc>224</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Dummy A</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>256</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Dummy B</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>256</xloc>
+ <yloc>224</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Output transform metrics</name>
+ <type>TransformsMetrics</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <transforms>
+ <transform>
+ <name>Dummy A</name>
+ <copyNr>0</copyNr>
+ <transformRequired>Y</transformRequired>
+ </transform>
+ <transform>
+ <name>Dummy B</name>
+ <copyNr>0</copyNr>
+ <transformRequired>Y</transformRequired>
+ </transform>
+ </transforms>
+ <transformnamefield>Transform name</transformnamefield>
+ <transformidfield>Transform id</transformidfield>
+ <transformlineswrittenfield>Lines written</transformlineswrittenfield>
+ <attributes/>
+ <GUI>
+ <xloc>448</xloc>
+ <yloc>152</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Sort</name>
+ <type>SortRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <compress>N</compress>
+ <directory>${java.io.tmpdir}</directory>
+ <fields>
+ <field>
+ <ascending>Y</ascending>
+ <case_sensitive>Y</case_sensitive>
+ <collator_enabled>N</collator_enabled>
+ <collator_strength>0</collator_strength>
+ <name>Transform name</name>
+ <presorted>N</presorted>
+ </field>
+ </fields>
+ <sort_prefix>out</sort_prefix>
+ <sort_size>1000000</sort_size>
+ <unique_rows>N</unique_rows>
+ <attributes/>
+ <GUI>
+ <xloc>656</xloc>
+ <yloc>152</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Verify</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>832</xloc>
+ <yloc>152</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/integration-tests/transforms/datasets/golden-output-transform-metrics.csv
b/integration-tests/transforms/datasets/golden-output-transform-metrics.csv
new file mode 100644
index 0000000000..333ac5e582
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-output-transform-metrics.csv
@@ -0,0 +1,3 @@
+Transform name,Transform id,Lines written
+Dummy A,Dummy,10
+Dummy B,Dummy,5
diff --git
a/integration-tests/transforms/main-0110-output-transform-metrics.hwf
b/integration-tests/transforms/main-0110-output-transform-metrics.hwf
new file mode 100644
index 0000000000..ea2a6011d3
--- /dev/null
+++ b/integration-tests/transforms/main-0110-output-transform-metrics.hwf
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+
+-->
+<workflow>
+ <name>main-0110-output-transform-metrics</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <workflow_status>0</workflow_status>
+ <created_user>-</created_user>
+ <created_date>2026/08/29 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/29 12:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <intervalSeconds>0</intervalSeconds>
+ <intervalMinutes>60</intervalMinutes>
+ <hour>12</hour>
+ <minutes>0</minutes>
+ <weekDay>1</weekDay>
+ <DayOfMonth>1</DayOfMonth>
+ <parallel>N</parallel>
+ <xloc>128</xloc>
+ <yloc>112</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Run Pipeline Unit Tests</name>
+ <description/>
+ <type>RunPipelineTests</type>
+ <attributes/>
+ <test_names>
+ <test_name>
+ <name>0110-output-transform-metrics UNIT</name>
+ </test_name>
+ </test_names>
+ <parallel>N</parallel>
+ <xloc>320</xloc>
+ <yloc>112</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Run Pipeline Unit Tests</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/integration-tests/transforms/metadata/dataset/golden-output-transform-metrics.json
b/integration-tests/transforms/metadata/dataset/golden-output-transform-metrics.json
new file mode 100644
index 0000000000..14d7ea83d0
--- /dev/null
+++
b/integration-tests/transforms/metadata/dataset/golden-output-transform-metrics.json
@@ -0,0 +1,32 @@
+{
+ "base_filename": "golden-output-transform-metrics.csv",
+ "name": "golden-output-transform-metrics",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_format": "",
+ "field_name": "Transform name"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_format": "",
+ "field_name": "Transform id"
+ },
+ {
+ "field_comment": "",
+ "field_length": 10,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_format": "#",
+ "field_name": "Lines written"
+ }
+ ],
+ "folder_name": ""
+}
diff --git
a/integration-tests/transforms/metadata/unit-test/0110-output-transform-metrics
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0110-output-transform-metrics
UNIT.json
new file mode 100644
index 0000000000..4943dc65fc
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0110-output-transform-metrics
UNIT.json
@@ -0,0 +1,36 @@
+{
+ "variableValues": [],
+ "database_replacements": [],
+ "autoOpening": true,
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "Transform name",
+ "data_set_field": "Transform name"
+ },
+ {
+ "transform_field": "Transform id",
+ "data_set_field": "Transform id"
+ },
+ {
+ "transform_field": "Lines written",
+ "data_set_field": "Lines written"
+ }
+ ],
+ "field_order": [
+ "Transform name"
+ ],
+ "transform_name": "Verify",
+ "data_set_name": "golden-output-transform-metrics"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0110-output-transform-metrics UNIT",
+ "description": "",
+ "trans_test_tweaks": [],
+ "persist_filename": "",
+ "pipeline_filename": "./0110-output-transform-metrics.hpl",
+ "test_type": "UNIT_TEST"
+}
diff --git
a/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleConst.java
b/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleConst.java
index 1c80f67169..92f5ce4461 100644
---
a/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleConst.java
+++
b/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleConst.java
@@ -87,6 +87,17 @@ public class KettleConst {
{"step7", "transform7"},
{"step8", "transform8"},
{"step9", "transform9"},
+ // Output transform metrics (Kettle: Output steps metrics)
+ {"stepRequired", "transformRequired"},
+ {"stepnamefield", "transformnamefield"},
+ {"stepidfield", "transformidfield"},
+ {"steplinesinputfield", "transformlinesinputfield"},
+ {"steplinesoutputfield", "transformlinesoutputfield"},
+ {"steplinesreadfield", "transformlinesreadfield"},
+ {"steplinesupdatedfield", "transformlinesupdatedfield"},
+ {"steplineswrittentfield", "transformlineswrittenfield"},
+ {"steplineserrorsfield", "transformlineserrorsfield"},
+ {"stepsecondsfield", "transformsecondsfield"},
})
.collect(Collectors.toMap(data -> (String) data[0], data -> (String)
data[1]));
@@ -160,6 +171,7 @@ public class KettleConst {
{"ROOT_JOB", "ROOT_WORKFLOW"},
{"BlockingStep", "BlockingTransform"},
{"BlockUntilStepsFinish", "BlockUntilTransformsFinish"},
+ {"StepsMetrics", "TransformsMetrics"},
{"TypeExitExcelWriterStep", "TypeExitExcelWriterTransform"},
{"StepMetastructure", "TransformMetaStructure"},
{"JobExecutor", "WorkflowExecutor"},
diff --git
a/plugins/misc/import/src/test/java/org/apache/hop/imports/kettle/KettleImportTransformsMetricsTest.java
b/plugins/misc/import/src/test/java/org/apache/hop/imports/kettle/KettleImportTransformsMetricsTest.java
new file mode 100644
index 0000000000..21594944b0
--- /dev/null
+++
b/plugins/misc/import/src/test/java/org/apache/hop/imports/kettle/KettleImportTransformsMetricsTest.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.imports.kettle;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.ByteArrayInputStream;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.core.xml.XmlParserFactoryProducer;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+class KettleImportTransformsMetricsTest {
+
+ private KettleImport kettleImport;
+
+ @BeforeAll
+ static void setUpBeforeClass() throws Exception {
+ HopClientEnvironment.init();
+ }
+
+ @BeforeEach
+ void setUp() {
+ kettleImport = new KettleImport();
+ }
+
+ @Test
+ void testStepsMetricsBecomesTransformsMetrics() throws Exception {
+ Document doc = parse(kettleSnippet());
+ processNode(doc);
+
+ Node pipeline = XmlHandler.getSubNode(doc, "pipeline");
+ assertNotNull(pipeline);
+ Node transform = XmlHandler.getSubNode(pipeline, "transform");
+ assertNotNull(transform);
+ assertEquals("TransformsMetrics", XmlHandler.getTagValue(transform,
"type"));
+
+ Node list = XmlHandler.getSubNode(transform, "transforms");
+ assertNotNull(list, "nested steps list should become transforms");
+ Node watched = XmlHandler.getSubNode(list, "transform");
+ assertNotNull(watched);
+ assertEquals("A", XmlHandler.getTagValue(watched, "name"));
+ assertEquals("0", XmlHandler.getTagValue(watched, "copyNr"));
+ assertEquals("Y", XmlHandler.getTagValue(watched, "transformRequired"));
+ assertNull(XmlHandler.getTagValue(watched, "stepRequired"));
+
+ assertEquals("Step name", XmlHandler.getTagValue(transform,
"transformnamefield"));
+ assertNull(XmlHandler.getTagValue(transform, "stepnamefield"));
+ assertEquals("Lines written", XmlHandler.getTagValue(transform,
"transformlineswrittenfield"));
+ assertNull(XmlHandler.getTagValue(transform, "steplineswrittentfield"));
+ assertEquals("Lines rejected", XmlHandler.getTagValue(transform,
"transformlineserrorsfield"));
+ assertNull(XmlHandler.getTagValue(transform, "steplineserrorsfield"));
+ }
+
+ private String kettleSnippet() {
+ return "<transformation>"
+ + "<step>"
+ + "<name>metrics</name>"
+ + "<type>StepsMetrics</type>"
+ + "<steps>"
+ + "<step>"
+ + "<name>A</name>"
+ + "<copyNr>0</copyNr>"
+ + "<stepRequired>Y</stepRequired>"
+ + "</step>"
+ + "</steps>"
+ + "<stepnamefield>Step name</stepnamefield>"
+ + "<steplineswrittentfield>Lines written</steplineswrittentfield>"
+ + "<steplineserrorsfield>Lines rejected</steplineserrorsfield>"
+ + "</step>"
+ + "</transformation>";
+ }
+
+ private void processNode(Document doc) throws Exception {
+ Method method =
+ KettleImport.class.getDeclaredMethod(
+ "processNode",
+ Document.class,
+ Node.class,
+ Class.forName(entryTypeClassName()),
+ int.class);
+ method.setAccessible(true);
+ method.invoke(kettleImport, doc, doc, otherEntryType(), 0);
+ }
+
+ private static String entryTypeClassName() {
+ return "org.apache.hop.imports.kettle.KettleImport$EntryType";
+ }
+
+ private Object otherEntryType() throws Exception {
+ Class<?> entryTypeClass = Class.forName(entryTypeClassName());
+ for (Object constant : entryTypeClass.getEnumConstants()) {
+ if ("OTHER".equals(constant.toString())) {
+ return constant;
+ }
+ }
+ throw new IllegalStateException("No OTHER entry type");
+ }
+
+ private static Document parse(String xml) throws Exception {
+ return XmlParserFactoryProducer.createSecureDocBuilderFactory()
+ .newDocumentBuilder()
+ .parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
+ }
+}
diff --git a/plugins/transforms/pom.xml b/plugins/transforms/pom.xml
index c8ad5a2c5f..29ecd88387 100644
--- a/plugins/transforms/pom.xml
+++ b/plugins/transforms/pom.xml
@@ -163,6 +163,7 @@
<module>textfile</module>
<module>tika</module>
<module>tokenreplacement</module>
+ <module>transformsmetrics</module>
<module>uniquerows</module>
<module>uniquerowsbyhashset</module>
<module>update</module>
diff --git a/plugins/transforms/transformsmetrics/pom.xml
b/plugins/transforms/transformsmetrics/pom.xml
new file mode 100644
index 0000000000..e46e5045e1
--- /dev/null
+++ b/plugins/transforms/transformsmetrics/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ ~
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-plugins-transforms</artifactId>
+ <version>2.20.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>hop-transform-transformsmetrics</artifactId>
+ <packaging>jar</packaging>
+ <name>Hop Plugins Transforms Output Transform Metrics</name>
+
+</project>
diff --git a/plugins/transforms/transformsmetrics/src/assembly/assembly.xml
b/plugins/transforms/transformsmetrics/src/assembly/assembly.xml
new file mode 100644
index 0000000000..098027e8bb
--- /dev/null
+++ b/plugins/transforms/transformsmetrics/src/assembly/assembly.xml
@@ -0,0 +1,46 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ ~
+ -->
+
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0
http://maven.apache.org/xsd/assembly-2.2.0.xsd">
+ <id>hop-transform-transformsmetrics</id>
+ <formats>
+ <format>zip</format>
+ </formats>
+ <baseDirectory>.</baseDirectory>
+ <files>
+ <file>
+ <source>${project.basedir}/src/main/resources/version.xml</source>
+ <outputDirectory>${hop.plugin.libdir}</outputDirectory>
+ <filtered>true</filtered>
+ </file>
+ </files>
+
+ <fileSets>
+ <fileSet>
+ <directory>${project.basedir}/src/main/samples</directory>
+ <outputDirectory>config/projects/samples/</outputDirectory>
+ </fileSet>
+ </fileSets>
+
+
+ <componentDescriptors>
+
<componentDescriptor>${maven.multiModuleProjectDirectory}/assemblies/shared/hop-plugin-libs.xml</componentDescriptor>
+ </componentDescriptors>
+</assembly>
diff --git
a/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/MetricTransform.java
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/MetricTransform.java
new file mode 100644
index 0000000000..5be783f370
--- /dev/null
+++
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/MetricTransform.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.pipeline.transforms.transformsmetrics;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+
+@Getter
+@Setter
+public class MetricTransform {
+
+ @HopMetadataProperty(
+ injectionKey = "TRANSFORM_NAME",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.TRANSFORM_NAME")
+ private String name;
+
+ @HopMetadataProperty(
+ key = "copyNr",
+ injectionKey = "TRANSFORM_COPY_NR",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.TRANSFORM_COPY_NR")
+ private String copyNr;
+
+ @HopMetadataProperty(
+ key = "transformRequired",
+ injectionKey = "TRANSFORM_REQUIRED",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.TRANSFORM_REQUIRED")
+ private boolean required;
+
+ public MetricTransform() {}
+
+ public MetricTransform(String name, String copyNr, boolean required) {
+ this.name = name;
+ this.copyNr = copyNr;
+ this.required = required;
+ }
+
+ public MetricTransform(MetricTransform other) {
+ this.name = other.name;
+ this.copyNr = other.copyNr;
+ this.required = other.required;
+ }
+}
diff --git
a/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetrics.java
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetrics.java
new file mode 100644
index 0000000000..efc5ab5627
--- /dev/null
+++
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetrics.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.pipeline.transforms.transformsmetrics;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.RowDataUtil;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engine.EngineComponent.ComponentExecutionStatus;
+import org.apache.hop.pipeline.engine.IEngineComponent;
+import org.apache.hop.pipeline.transform.BaseTransform;
+import org.apache.hop.pipeline.transform.TransformMeta;
+
+/**
+ * Wait for selected sibling transforms to finish, then output one metrics row
per watched copy.
+ * This transform does not consume incoming rows.
+ */
+public class TransformsMetrics extends BaseTransform<TransformsMetricsMeta,
TransformsMetricsData> {
+
+ private static final Class<?> PKG = TransformsMetricsMeta.class;
+ private static final long POLL_SLEEP_MS = 20L;
+
+ public TransformsMetrics(
+ TransformMeta transformMeta,
+ TransformsMetricsMeta meta,
+ TransformsMetricsData data,
+ int copyNr,
+ PipelineMeta pipelineMeta,
+ Pipeline pipeline) {
+ super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline);
+ }
+
+ @Override
+ public boolean processRow() throws HopException {
+ if (first) {
+ first = false;
+ initializeWatchedTransforms();
+ }
+
+ List<IEngineComponent> remaining = data.getRemaining();
+ while (!remaining.isEmpty() && !isStopped()) {
+ boolean waiting = false;
+ Iterator<IEngineComponent> iterator = remaining.iterator();
+ while (iterator.hasNext()) {
+ IEngineComponent component = iterator.next();
+ if (isStillRunning(component)) {
+ waiting = true;
+ } else {
+ iterator.remove();
+ if (isDetailed()) {
+ logDetailed(
+ BaseMessages.getString(
+ PKG,
+ "TransformsMetrics.Log.Finished",
+ component.getName(),
+ String.valueOf(component.getCopyNr())));
+ }
+ emitMetricsRow(component);
+ }
+ }
+ if (waiting && !remaining.isEmpty() && !isStopped()) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(POLL_SLEEP_MS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ setStopped(true);
+ }
+ }
+ }
+
+ setOutputDone();
+ return false;
+ }
+
+ private void initializeWatchedTransforms() throws HopException {
+ if (meta.getMetricTransforms() == null ||
meta.getMetricTransforms().isEmpty()) {
+ throw new HopException(BaseMessages.getString(PKG,
"TransformsMetrics.Error.NotTransforms"));
+ }
+
+ data.setTransformNameField(resolve(meta.getTransformNameField()));
+ data.setTransformIdField(resolve(meta.getTransformIdField()));
+ data.setLinesInputField(resolve(meta.getLinesInputField()));
+ data.setLinesOutputField(resolve(meta.getLinesOutputField()));
+ data.setLinesReadField(resolve(meta.getLinesReadField()));
+ data.setLinesUpdatedField(resolve(meta.getLinesUpdatedField()));
+ data.setLinesWrittenField(resolve(meta.getLinesWrittenField()));
+ data.setLinesRejectedField(resolve(meta.getLinesRejectedField()));
+ data.setDurationField(resolve(meta.getDurationField()));
+
+ String[] nextTransforms =
getPipelineMeta().getNextTransformNames(getTransformMeta());
+ List<IEngineComponent> remaining = new ArrayList<>();
+
+ for (MetricTransform metricTransform : meta.getMetricTransforms()) {
+ String name = metricTransform.getName();
+ if (StringUtils.isEmpty(name)) {
+ continue;
+ }
+ if (name.equals(getTransformName())) {
+ throw new HopException(
+ BaseMessages.getString(PKG,
"TransformsMetrics.Error.CannotWatchSelf", name));
+ }
+ if (nextTransforms != null) {
+ for (String nextTransform : nextTransforms) {
+ if (name.equals(nextTransform)) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG, "TransformsMetrics.Error.CannotWatchTarget",
nextTransform));
+ }
+ }
+ }
+
+ int copyNr = Const.toInt(resolve(metricTransform.getCopyNr()), 0);
+ IEngineComponent component = getDispatcher().findComponent(name, copyNr);
+ if (component == null) {
+ if (metricTransform.isRequired()) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG, "TransformsMetrics.Error.TransformNotFound", name,
String.valueOf(copyNr)));
+ }
+ continue;
+ }
+ remaining.add(component);
+ }
+
+ data.setRemaining(remaining);
+
+ data.setOutputRowMeta(new RowMeta());
+ meta.getFields(
+ data.getOutputRowMeta(), getTransformName(), null, null, this,
getMetadataProvider());
+ }
+
+ private boolean isStillRunning(IEngineComponent component) {
+ ComponentExecutionStatus status = component.getStatus();
+ return status == ComponentExecutionStatus.STATUS_RUNNING
+ || status == ComponentExecutionStatus.STATUS_IDLE
+ || status == ComponentExecutionStatus.STATUS_INIT
+ || status == ComponentExecutionStatus.STATUS_PAUSED;
+ }
+
+ private void emitMetricsRow(IEngineComponent component) throws HopException {
+ Object[] rowData =
RowDataUtil.allocateRowData(data.getOutputRowMeta().size());
+ incrementLinesRead();
+
+ int index = 0;
+ if (StringUtils.isNotBlank(data.getTransformNameField())) {
+ rowData[index++] = component.getName();
+ }
+ if (StringUtils.isNotBlank(data.getTransformIdField())) {
+ rowData[index++] = pluginIdOf(component);
+ }
+ if (StringUtils.isNotBlank(data.getLinesInputField())) {
+ rowData[index++] = component.getLinesInput();
+ }
+ if (StringUtils.isNotBlank(data.getLinesOutputField())) {
+ rowData[index++] = component.getLinesOutput();
+ }
+ if (StringUtils.isNotBlank(data.getLinesReadField())) {
+ rowData[index++] = component.getLinesRead();
+ }
+ if (StringUtils.isNotBlank(data.getLinesUpdatedField())) {
+ rowData[index++] = component.getLinesUpdated();
+ }
+ if (StringUtils.isNotBlank(data.getLinesWrittenField())) {
+ rowData[index++] = component.getLinesWritten();
+ }
+ if (StringUtils.isNotBlank(data.getLinesRejectedField())) {
+ rowData[index++] = component.getLinesRejected();
+ }
+ if (StringUtils.isNotBlank(data.getDurationField())) {
+ rowData[index] = component.getExecutionDuration();
+ }
+
+ putRow(data.getOutputRowMeta(), rowData);
+ }
+
+ private String pluginIdOf(IEngineComponent component) {
+ TransformMeta watched =
getPipelineMeta().findTransform(component.getName());
+ return watched == null ? null : watched.getTransformPluginId();
+ }
+}
diff --git
a/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsData.java
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsData.java
new file mode 100644
index 0000000000..4019a0422b
--- /dev/null
+++
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsData.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.pipeline.transforms.transformsmetrics;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.pipeline.engine.IEngineComponent;
+import org.apache.hop.pipeline.transform.BaseTransformData;
+import org.apache.hop.pipeline.transform.ITransformData;
+
+@Getter
+@Setter
+public class TransformsMetricsData extends BaseTransformData implements
ITransformData {
+
+ private List<IEngineComponent> remaining = new ArrayList<>();
+ private IRowMeta outputRowMeta;
+
+ private String transformNameField;
+ private String transformIdField;
+ private String linesInputField;
+ private String linesOutputField;
+ private String linesReadField;
+ private String linesUpdatedField;
+ private String linesWrittenField;
+ private String linesRejectedField;
+ private String durationField;
+}
diff --git
a/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsDialog.java
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsDialog.java
new file mode 100644
index 0000000000..ad0914119d
--- /dev/null
+++
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsDialog.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.pipeline.transforms.transformsmetrics;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
+import org.apache.hop.ui.core.gui.GuiCompositeWidgetsAdapter;
+import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+public class TransformsMetricsDialog extends BaseTransformDialog {
+ private static final Class<?> PKG = TransformsMetricsMeta.class;
+
+ private final TransformsMetricsMeta input;
+ private GuiCompositeWidgets widgets;
+ private TableView wFields;
+ private String[] previousTransforms;
+
+ public TransformsMetricsDialog(
+ Shell parent,
+ IVariables variables,
+ TransformsMetricsMeta transformMeta,
+ PipelineMeta pipelineMeta) {
+ super(parent, variables, transformMeta, pipelineMeta);
+ input = transformMeta;
+ }
+
+ @Override
+ public String open() {
+ createShell(BaseMessages.getString(PKG,
"TransformsMetricsDialog.Shell.Title"));
+
+ changed = input.hasChanged();
+
+ buildButtonBar().ok(e -> ok()).get(e -> get()).cancel(e ->
cancel()).build();
+
+ setTransformNames();
+
+ Composite area = new Composite(shell, SWT.NONE);
+ PropsUi.setLook(area);
+ area.setLayout(new FormLayout());
+ FormData fdArea = new FormData();
+ fdArea.left = new FormAttachment(0, 0);
+ fdArea.top = new FormAttachment(wSpacer, margin);
+ fdArea.right = new FormAttachment(100, 0);
+ fdArea.bottom = new FormAttachment(wOk, -2 * margin);
+ area.setLayoutData(fdArea);
+
+ widgets = new GuiCompositeWidgets(variables);
+ widgets.registerExtraGroup(
+ BaseMessages.getString(PKG, "TransformsMetricsMeta.Group.Transforms"),
+ "10",
+ null,
+ this::addTransformsTable);
+ widgets.setWidgetsListener(
+ new GuiCompositeWidgetsAdapter() {
+ @Override
+ public void widgetModified(
+ GuiCompositeWidgets compositeWidgets, Control changedWidget,
String widgetId) {
+ input.setChanged();
+ }
+ });
+ widgets.createCompositeWidgets(
+ input, null, area, TransformsMetricsMeta.GUI_PLUGIN_ELEMENT_PARENT_ID,
null);
+
+ getData();
+ input.setChanged(changed);
+ focusTransformName();
+ BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+ return transformName;
+ }
+
+ private void addTransformsTable(Composite parent) {
+ Label wlFields = new Label(parent, SWT.NONE);
+ wlFields.setText(BaseMessages.getString(PKG,
"TransformsMetricsDialog.Fields.Label"));
+ PropsUi.setLook(wlFields);
+ FormData fdlFields = new FormData();
+ fdlFields.left = new FormAttachment(0, 0);
+ fdlFields.top = new FormAttachment(0, 0);
+ wlFields.setLayoutData(fdlFields);
+
+ String[] yesNo =
+ new String[] {
+ BaseMessages.getString(PKG, "System.Combo.No"),
+ BaseMessages.getString(PKG, "System.Combo.Yes")
+ };
+
+ ColumnInfo[] colinf = new ColumnInfo[3];
+ colinf[0] =
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"TransformsMetricsDialog.Fieldname.Transform"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ previousTransforms,
+ false);
+ colinf[1] =
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"TransformsMetricsDialog.Fieldname.CopyNr"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false);
+ colinf[1].setUsingVariables(true);
+ colinf[2] =
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"TransformsMetricsDialog.Fieldname.Required"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ yesNo);
+
+ wFields =
+ new TableView(
+ variables,
+ parent,
+ SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI,
+ colinf,
+ input.getMetricTransforms() == null ? 0 :
input.getMetricTransforms().size(),
+ e -> input.setChanged(),
+ props);
+
+ FormData fdFields = new FormData();
+ fdFields.left = new FormAttachment(0, 0);
+ fdFields.top = new FormAttachment(wlFields, margin);
+ fdFields.right = new FormAttachment(100, 0);
+ fdFields.bottom = new FormAttachment(100, 0);
+ wFields.setLayoutData(fdFields);
+ }
+
+ private void setTransformNames() {
+ previousTransforms = pipelineMeta.getTransformNames();
+ List<String> nextTransforms = getNextTransforms(new ArrayList<>(),
transformMeta);
+
+ List<String> entries = new ArrayList<>();
+ for (String previousTransform : previousTransforms) {
+ if (!previousTransform.equals(transformName) &&
!nextTransforms.contains(previousTransform)) {
+ entries.add(previousTransform);
+ }
+ }
+ previousTransforms = entries.toArray(new String[0]);
+ }
+
+ private List<String> getNextTransforms(List<String> transformNames,
TransformMeta transformMeta) {
+ List<TransformMeta> nextTransformMeta =
pipelineMeta.findNextTransforms(transformMeta);
+ for (TransformMeta nextTransform : nextTransformMeta) {
+ transformNames.add(nextTransform.getName());
+ getNextTransforms(transformNames, nextTransform);
+ }
+ return transformNames.stream().distinct().toList();
+ }
+
+ private void get() {
+ wFields.removeAll();
+ Table table = wFields.table;
+ for (int i = 0; i < previousTransforms.length; i++) {
+ TableItem ti = new TableItem(table, SWT.NONE);
+ ti.setText(0, "" + (i + 1));
+ ti.setText(1, previousTransforms[i]);
+ ti.setText(2, "0");
+ ti.setText(3, BaseMessages.getString(PKG, "System.Combo.No"));
+ }
+ wFields.removeEmptyRows();
+ wFields.setRowNums();
+ wFields.optWidth(true);
+ input.setChanged();
+ }
+
+ private void getData() {
+ widgets.setWidgetsContents(input, shell,
TransformsMetricsMeta.GUI_PLUGIN_ELEMENT_PARENT_ID);
+
+ Table table = wFields.table;
+ if (input.getMetricTransforms() != null &&
!input.getMetricTransforms().isEmpty()) {
+ table.removeAll();
+ }
+ if (input.getMetricTransforms() != null) {
+ for (MetricTransform metricTransform : input.getMetricTransforms()) {
+ TableItem ti = new TableItem(table, SWT.NONE);
+ ti.setText(1, Const.NVL(metricTransform.getName(), ""));
+ ti.setText(2, Const.NVL(metricTransform.getCopyNr(), ""));
+ ti.setText(
+ 3,
+ metricTransform.isRequired()
+ ? BaseMessages.getString(PKG, "System.Combo.Yes")
+ : BaseMessages.getString(PKG, "System.Combo.No"));
+ }
+ }
+ wFields.removeEmptyRows();
+ wFields.setRowNums();
+ wFields.optWidth(true);
+ }
+
+ private void cancel() {
+ transformName = null;
+ input.setChanged(changed);
+ dispose();
+ }
+
+ private void ok() {
+ if (Utils.isEmpty(wTransformName.getText())) {
+ return;
+ }
+
+ widgets.getWidgetsContents(input,
TransformsMetricsMeta.GUI_PLUGIN_ELEMENT_PARENT_ID);
+ transformName = wTransformName.getText();
+
+ if (input.getMetricTransforms() == null) {
+ input.setMetricTransforms(new ArrayList<>());
+ } else {
+ input.getMetricTransforms().clear();
+ }
+ String yes = BaseMessages.getString(PKG, "System.Combo.Yes");
+ for (TableItem item : wFields.getNonEmptyItems()) {
+ String name = item.getText(1);
+ if (Utils.isEmpty(name)) {
+ continue;
+ }
+ input
+ .getMetricTransforms()
+ .add(new MetricTransform(name, item.getText(2),
yes.equalsIgnoreCase(item.getText(3))));
+ }
+ input.setChanged();
+ dispose();
+ }
+}
diff --git
a/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsMeta.java
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsMeta.java
new file mode 100644
index 0000000000..2a1177628e
--- /dev/null
+++
b/plugins/transforms/transformsmetrics/src/main/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsMeta.java
@@ -0,0 +1,358 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.pipeline.transforms.transformsmetrics;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.annotations.Transform;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.gui.plugin.GuiElementType;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiWidgetElement;
+import org.apache.hop.core.gui.plugin.GuiWidgetGroupType;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.PipelineMeta.PipelineType;
+import org.apache.hop.pipeline.transform.BaseTransformMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+
+@Getter
+@Setter
+@Transform(
+ id = "TransformsMetrics,StepsMetrics",
+ image = "transformsmetrics.svg",
+ name = "i18n::TransformsMetrics.Name",
+ description = "i18n::TransformsMetrics.Description",
+ categoryDescription =
+
"i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Statistics",
+ keywords = "i18n::TransformsMetricsMeta.keyword",
+ documentationUrl = "/pipeline/transforms/outputtransformmetrics.html")
+@GuiPlugin
+public class TransformsMetricsMeta
+ extends BaseTransformMeta<TransformsMetrics, TransformsMetricsData> {
+ private static final Class<?> PKG = TransformsMetricsMeta.class;
+
+ public static final String GUI_PLUGIN_ELEMENT_PARENT_ID =
"TRANSFORMS_METRICS_DIALOG_OPTIONS";
+ public static final String GROUP_FIELDS =
"i18n::TransformsMetricsMeta.Group.Fields";
+ public static final String WIDGET_TRANSFORM_NAME_FIELD =
"TRANSFORM_NAME_FIELD";
+ public static final String WIDGET_TRANSFORM_ID_FIELD = "TRANSFORM_ID_FIELD";
+ public static final String WIDGET_LINES_INPUT_FIELD = "LINES_INPUT_FIELD";
+ public static final String WIDGET_LINES_OUTPUT_FIELD = "LINES_OUTPUT_FIELD";
+ public static final String WIDGET_LINES_READ_FIELD = "LINES_READ_FIELD";
+ public static final String WIDGET_LINES_UPDATED_FIELD =
"LINES_UPDATED_FIELD";
+ public static final String WIDGET_LINES_WRITTEN_FIELD =
"LINES_WRITTEN_FIELD";
+ public static final String WIDGET_LINES_REJECTED_FIELD =
"LINES_REJECTED_FIELD";
+ public static final String WIDGET_DURATION_FIELD = "DURATION_FIELD";
+
+ public static final String DEFAULT_TRANSFORM_NAME_FIELD = "Transform name";
+ public static final String DEFAULT_TRANSFORM_ID_FIELD = "Transform id";
+ public static final String DEFAULT_LINES_INPUT_FIELD = "Lines input";
+ public static final String DEFAULT_LINES_OUTPUT_FIELD = "Lines output";
+ public static final String DEFAULT_LINES_READ_FIELD = "Lines read";
+ public static final String DEFAULT_LINES_UPDATED_FIELD = "Lines updated";
+ public static final String DEFAULT_LINES_WRITTEN_FIELD = "Lines written";
+ public static final String DEFAULT_LINES_REJECTED_FIELD = "Lines rejected";
+ public static final String DEFAULT_DURATION_FIELD = "Duration";
+
+ @HopMetadataProperty(
+ groupKey = "transforms",
+ key = "transform",
+ injectionGroupKey = "TRANSFORMS",
+ injectionGroupDescription = "TransformsMetricsMeta.Injection.TRANSFORMS")
+ private List<MetricTransform> metricTransforms;
+
+ @GuiWidgetElement(
+ id = WIDGET_TRANSFORM_NAME_FIELD,
+ order = "0100",
+ type = GuiElementType.TEXT,
+ label = "i18n::TransformsMetricsMeta.TransformNameField.Label",
+ toolTip = "i18n::TransformsMetricsMeta.TransformNameField.Tooltip",
+ parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
+ group = GROUP_FIELDS,
+ groupOrder = "20",
+ groupType = GuiWidgetGroupType.TABS)
+ @HopMetadataProperty(
+ key = "transformnamefield",
+ injectionKey = "TRANSFORM_NAME_FIELD",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.TRANSFORM_NAME_FIELD")
+ private String transformNameField;
+
+ @GuiWidgetElement(
+ id = WIDGET_TRANSFORM_ID_FIELD,
+ order = "0200",
+ type = GuiElementType.TEXT,
+ label = "i18n::TransformsMetricsMeta.TransformIdField.Label",
+ toolTip = "i18n::TransformsMetricsMeta.TransformIdField.Tooltip",
+ parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
+ group = GROUP_FIELDS,
+ groupOrder = "20",
+ groupType = GuiWidgetGroupType.TABS)
+ @HopMetadataProperty(
+ key = "transformidfield",
+ injectionKey = "TRANSFORM_ID_FIELD",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.TRANSFORM_ID_FIELD")
+ private String transformIdField;
+
+ @GuiWidgetElement(
+ id = WIDGET_LINES_INPUT_FIELD,
+ order = "0300",
+ type = GuiElementType.TEXT,
+ label = "i18n::TransformsMetricsMeta.LinesInputField.Label",
+ toolTip = "i18n::TransformsMetricsMeta.LinesInputField.Tooltip",
+ parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
+ group = GROUP_FIELDS,
+ groupOrder = "20",
+ groupType = GuiWidgetGroupType.TABS)
+ @HopMetadataProperty(
+ key = "transformlinesinputfield",
+ injectionKey = "LINES_INPUT_FIELD",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.LINES_INPUT_FIELD")
+ private String linesInputField;
+
+ @GuiWidgetElement(
+ id = WIDGET_LINES_OUTPUT_FIELD,
+ order = "0400",
+ type = GuiElementType.TEXT,
+ label = "i18n::TransformsMetricsMeta.LinesOutputField.Label",
+ toolTip = "i18n::TransformsMetricsMeta.LinesOutputField.Tooltip",
+ parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
+ group = GROUP_FIELDS,
+ groupOrder = "20",
+ groupType = GuiWidgetGroupType.TABS)
+ @HopMetadataProperty(
+ key = "transformlinesoutputfield",
+ injectionKey = "LINES_OUTPUT_FIELD",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.LINES_OUTPUT_FIELD")
+ private String linesOutputField;
+
+ @GuiWidgetElement(
+ id = WIDGET_LINES_READ_FIELD,
+ order = "0500",
+ type = GuiElementType.TEXT,
+ label = "i18n::TransformsMetricsMeta.LinesReadField.Label",
+ toolTip = "i18n::TransformsMetricsMeta.LinesReadField.Tooltip",
+ parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
+ group = GROUP_FIELDS,
+ groupOrder = "20",
+ groupType = GuiWidgetGroupType.TABS)
+ @HopMetadataProperty(
+ key = "transformlinesreadfield",
+ injectionKey = "LINES_READ_FIELD",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.LINES_READ_FIELD")
+ private String linesReadField;
+
+ @GuiWidgetElement(
+ id = WIDGET_LINES_UPDATED_FIELD,
+ order = "0600",
+ type = GuiElementType.TEXT,
+ label = "i18n::TransformsMetricsMeta.LinesUpdatedField.Label",
+ toolTip = "i18n::TransformsMetricsMeta.LinesUpdatedField.Tooltip",
+ parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
+ group = GROUP_FIELDS,
+ groupOrder = "20",
+ groupType = GuiWidgetGroupType.TABS)
+ @HopMetadataProperty(
+ key = "transformlinesupdatedfield",
+ injectionKey = "LINES_UPDATED_FIELD",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.LINES_UPDATED_FIELD")
+ private String linesUpdatedField;
+
+ @GuiWidgetElement(
+ id = WIDGET_LINES_WRITTEN_FIELD,
+ order = "0700",
+ type = GuiElementType.TEXT,
+ label = "i18n::TransformsMetricsMeta.LinesWrittenField.Label",
+ toolTip = "i18n::TransformsMetricsMeta.LinesWrittenField.Tooltip",
+ parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
+ group = GROUP_FIELDS,
+ groupOrder = "20",
+ groupType = GuiWidgetGroupType.TABS)
+ @HopMetadataProperty(
+ key = "transformlineswrittenfield",
+ injectionKey = "LINES_WRITTEN_FIELD",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.LINES_WRITTEN_FIELD")
+ private String linesWrittenField;
+
+ @GuiWidgetElement(
+ id = WIDGET_LINES_REJECTED_FIELD,
+ order = "0800",
+ type = GuiElementType.TEXT,
+ label = "i18n::TransformsMetricsMeta.LinesRejectedField.Label",
+ toolTip = "i18n::TransformsMetricsMeta.LinesRejectedField.Tooltip",
+ parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
+ group = GROUP_FIELDS,
+ groupOrder = "20",
+ groupType = GuiWidgetGroupType.TABS)
+ @HopMetadataProperty(
+ key = "transformlineserrorsfield",
+ injectionKey = "LINES_REJECTED_FIELD",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.LINES_REJECTED_FIELD")
+ private String linesRejectedField;
+
+ @GuiWidgetElement(
+ id = WIDGET_DURATION_FIELD,
+ order = "0900",
+ type = GuiElementType.TEXT,
+ label = "i18n::TransformsMetricsMeta.DurationField.Label",
+ toolTip = "i18n::TransformsMetricsMeta.DurationField.Tooltip",
+ parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
+ group = GROUP_FIELDS,
+ groupOrder = "20",
+ groupType = GuiWidgetGroupType.TABS)
+ @HopMetadataProperty(
+ key = "transformsecondsfield",
+ injectionKey = "DURATION_FIELD",
+ injectionKeyDescription =
"TransformsMetricsMeta.Injection.DURATION_FIELD")
+ private String durationField;
+
+ public TransformsMetricsMeta() {
+ metricTransforms = new ArrayList<>();
+ }
+
+ @Override
+ public void setDefault() {
+ metricTransforms = new ArrayList<>();
+ transformNameField = DEFAULT_TRANSFORM_NAME_FIELD;
+ transformIdField = DEFAULT_TRANSFORM_ID_FIELD;
+ linesInputField = DEFAULT_LINES_INPUT_FIELD;
+ linesOutputField = DEFAULT_LINES_OUTPUT_FIELD;
+ linesReadField = DEFAULT_LINES_READ_FIELD;
+ linesUpdatedField = DEFAULT_LINES_UPDATED_FIELD;
+ linesWrittenField = DEFAULT_LINES_WRITTEN_FIELD;
+ linesRejectedField = DEFAULT_LINES_REJECTED_FIELD;
+ durationField = DEFAULT_DURATION_FIELD;
+ }
+
+ @Override
+ public void getFields(
+ IRowMeta inputRowMeta,
+ String name,
+ IRowMeta[] info,
+ TransformMeta nextTransform,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider)
+ throws HopTransformException {
+ inputRowMeta.clear();
+ addStringField(inputRowMeta, variables.resolve(transformNameField), name);
+ addStringField(inputRowMeta, variables.resolve(transformIdField), name);
+ addIntegerField(inputRowMeta, variables.resolve(linesInputField), name);
+ addIntegerField(inputRowMeta, variables.resolve(linesOutputField), name);
+ addIntegerField(inputRowMeta, variables.resolve(linesReadField), name);
+ addIntegerField(inputRowMeta, variables.resolve(linesUpdatedField), name);
+ addIntegerField(inputRowMeta, variables.resolve(linesWrittenField), name);
+ addIntegerField(inputRowMeta, variables.resolve(linesRejectedField), name);
+ addIntegerField(inputRowMeta, variables.resolve(durationField), name);
+ }
+
+ private void addStringField(IRowMeta rowMeta, String fieldName, String
origin) {
+ if (StringUtils.isBlank(fieldName)) {
+ return;
+ }
+ ValueMetaString valueMeta = new ValueMetaString(fieldName);
+ valueMeta.setOrigin(origin);
+ rowMeta.addValueMeta(valueMeta);
+ }
+
+ private void addIntegerField(IRowMeta rowMeta, String fieldName, String
origin) {
+ if (StringUtils.isBlank(fieldName)) {
+ return;
+ }
+ ValueMetaInteger valueMeta =
+ new ValueMetaInteger(fieldName, IValueMeta.DEFAULT_INTEGER_LENGTH, 0);
+ valueMeta.setOrigin(origin);
+ rowMeta.addValueMeta(valueMeta);
+ }
+
+ @Override
+ public void check(
+ List<ICheckResult> remarks,
+ PipelineMeta pipelineMeta,
+ TransformMeta transformMeta,
+ IRowMeta prev,
+ String[] input,
+ String[] output,
+ IRowMeta info,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider) {
+ if (input.length > 0) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG,
"TransformsMetricsMeta.CheckResult.IncomingHopsNotSupported"),
+ transformMeta));
+ } else {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(PKG,
"TransformsMetricsMeta.CheckResult.NoIncomingHops"),
+ transformMeta));
+ }
+
+ if (metricTransforms == null || metricTransforms.isEmpty()) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(PKG,
"TransformsMetricsMeta.CheckResult.NoTransformsEntered"),
+ transformMeta));
+ return;
+ }
+
+ boolean allFound = true;
+ for (MetricTransform metricTransform : metricTransforms) {
+ if (StringUtils.isEmpty(metricTransform.getName())) {
+ continue;
+ }
+ if (pipelineMeta.findTransform(metricTransform.getName()) == null) {
+ allFound = false;
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_WARNING,
+ BaseMessages.getString(
+ PKG,
+ "TransformsMetricsMeta.CheckResult.TransformNotFound",
+ metricTransform.getName()),
+ transformMeta));
+ }
+ }
+ if (allFound) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(PKG,
"TransformsMetricsMeta.CheckResult.AllTransformsFound"),
+ transformMeta));
+ }
+ }
+
+ @Override
+ public PipelineType[] getSupportedPipelineTypes() {
+ return new PipelineType[] {PipelineType.Normal};
+ }
+}
diff --git
a/plugins/transforms/transformsmetrics/src/main/resources/org/apache/hop/pipeline/transforms/transformsmetrics/messages/messages_en_US.properties
b/plugins/transforms/transformsmetrics/src/main/resources/org/apache/hop/pipeline/transforms/transformsmetrics/messages/messages_en_US.properties
new file mode 100644
index 0000000000..3e4f3ff9b3
--- /dev/null
+++
b/plugins/transforms/transformsmetrics/src/main/resources/org/apache/hop/pipeline/transforms/transformsmetrics/messages/messages_en_US.properties
@@ -0,0 +1,68 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+TransformsMetrics.Name=Output transform metrics
+TransformsMetrics.Description=Wait for selected transforms to finish and
output their metrics
+TransformsMetricsMeta.keyword=metrics,statistics,wait,transform,output
+TransformsMetrics.Error.NotTransforms=You must define at least one transform
for which metrics will be returned.
+TransformsMetrics.Error.CannotWatchSelf=You can not get metrics for the
current transform [{0}].
+TransformsMetrics.Error.CannotWatchTarget=You can not get metrics for the
target transform [{0}].
+TransformsMetrics.Error.TransformNotFound=We cannot get transform [{0}]
copyNr={1}.
+TransformsMetrics.Log.Finished=Finished running transform [{0} ({1})].
+TransformsMetricsDialog.Shell.Title=Output transform metrics
+TransformsMetricsDialog.Fields.Label=Metrics for the following transforms
+TransformsMetricsDialog.Fieldname.Transform=Transform name
+TransformsMetricsDialog.Fieldname.CopyNr=Copy Nr
+TransformsMetricsDialog.Fieldname.Required=Required
+TransformsMetricsMeta.Group.Transforms=Transforms
+TransformsMetricsMeta.Group.Fields=Fields
+TransformsMetricsMeta.TransformNameField.Label=Transform name
+TransformsMetricsMeta.TransformNameField.Tooltip=Output field for the watched
transform name. Leave empty to omit this field.
+TransformsMetricsMeta.TransformIdField.Label=Transform id
+TransformsMetricsMeta.TransformIdField.Tooltip=Output field for the watched
transform plugin id. Leave empty to omit this field.
+TransformsMetricsMeta.LinesInputField.Label=Lines input
+TransformsMetricsMeta.LinesInputField.Tooltip=Output field for lines input.
Leave empty to omit this field.
+TransformsMetricsMeta.LinesOutputField.Label=Lines output
+TransformsMetricsMeta.LinesOutputField.Tooltip=Output field for lines output.
Leave empty to omit this field.
+TransformsMetricsMeta.LinesReadField.Label=Lines read
+TransformsMetricsMeta.LinesReadField.Tooltip=Output field for lines read.
Leave empty to omit this field.
+TransformsMetricsMeta.LinesUpdatedField.Label=Lines updated
+TransformsMetricsMeta.LinesUpdatedField.Tooltip=Output field for lines
updated. Leave empty to omit this field.
+TransformsMetricsMeta.LinesWrittenField.Label=Lines written
+TransformsMetricsMeta.LinesWrittenField.Tooltip=Output field for lines
written. Leave empty to omit this field.
+TransformsMetricsMeta.LinesRejectedField.Label=Lines rejected
+TransformsMetricsMeta.LinesRejectedField.Tooltip=Output field for lines
rejected (error hop). Leave empty to omit this field.
+TransformsMetricsMeta.DurationField.Label=Duration (ms)
+TransformsMetricsMeta.DurationField.Tooltip=Output field for execution
duration in milliseconds. Leave empty to omit this field.
+TransformsMetricsMeta.CheckResult.IncomingHopsNotSupported=Incoming hops are
not supported and would deadlock this transform. Remove them.
+TransformsMetricsMeta.CheckResult.NoIncomingHops=Not receiving input from
other transforms.
+TransformsMetricsMeta.CheckResult.NoTransformsEntered=No transforms are
entered.
+TransformsMetricsMeta.CheckResult.TransformNotFound=Watched transform [{0}]
was not found in the pipeline.
+TransformsMetricsMeta.CheckResult.AllTransformsFound=All watched transforms
were found.
+TransformsMetricsMeta.Injection.TRANSFORMS=Transforms to watch
+TransformsMetricsMeta.Injection.TRANSFORM_NAME=Transform name
+TransformsMetricsMeta.Injection.TRANSFORM_COPY_NR=Copy number
+TransformsMetricsMeta.Injection.TRANSFORM_REQUIRED=Required
+TransformsMetricsMeta.Injection.TRANSFORM_NAME_FIELD=Transform name field
+TransformsMetricsMeta.Injection.TRANSFORM_ID_FIELD=Transform id field
+TransformsMetricsMeta.Injection.LINES_INPUT_FIELD=Lines input field
+TransformsMetricsMeta.Injection.LINES_OUTPUT_FIELD=Lines output field
+TransformsMetricsMeta.Injection.LINES_READ_FIELD=Lines read field
+TransformsMetricsMeta.Injection.LINES_UPDATED_FIELD=Lines updated field
+TransformsMetricsMeta.Injection.LINES_WRITTEN_FIELD=Lines written field
+TransformsMetricsMeta.Injection.LINES_REJECTED_FIELD=Lines rejected field
+TransformsMetricsMeta.Injection.DURATION_FIELD=Duration field
diff --git
a/plugins/transforms/transformsmetrics/src/main/resources/transformsmetrics.svg
b/plugins/transforms/transformsmetrics/src/main/resources/transformsmetrics.svg
new file mode 100644
index 0000000000..6829085d35
--- /dev/null
+++
b/plugins/transforms/transformsmetrics/src/main/resources/transformsmetrics.svg
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+ width="42px" height="42px" viewBox="0 0 42 42">
+ <rect x="6" y="24" width="7" height="12" fill="#0E3A5A"/>
+ <rect x="17.5" y="14" width="7" height="22" fill="#0E3A5A"/>
+ <rect x="29" y="6" width="7" height="30" fill="#EC0800"/>
+</svg>
diff --git
a/plugins/transforms/transformsmetrics/src/main/resources/version.xml
b/plugins/transforms/transformsmetrics/src/main/resources/version.xml
new file mode 100644
index 0000000000..36ab20e22e
--- /dev/null
+++ b/plugins/transforms/transformsmetrics/src/main/resources/version.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You 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.
+ ~
+ -->
+
+<version>${project.version}</version>
diff --git
a/plugins/transforms/transformsmetrics/src/main/samples/transforms/output-transform-metrics-basic.hpl
b/plugins/transforms/transformsmetrics/src/main/samples/transforms/output-transform-metrics-basic.hpl
new file mode 100644
index 0000000000..1fa7eb8363
--- /dev/null
+++
b/plugins/transforms/transformsmetrics/src/main/samples/transforms/output-transform-metrics-basic.hpl
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+
+-->
+<pipeline>
+ <info>
+ <name>output-transform-metrics-basic</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Wait for sibling transforms to finish and output their
metrics</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/08/29 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/29 12:00:00.000</modified_date>
+ <key_for_session_key/>
+ <is_key_private>N</is_key_private>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>10 rows</from>
+ <to>Dummy A</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>5 rows</from>
+ <to>Dummy B</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>10 rows</name>
+ <type>RowGenerator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>value</name>
+ <type>Integer</type>
+ <format>#</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif>1</nullif>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <limit>10</limit>
+ <never_ending>N</never_ending>
+ <interval_in_ms>5000</interval_in_ms>
+ <row_time_field>now</row_time_field>
+ <last_time_field>FiveSecondsAgo</last_time_field>
+ <attributes/>
+ <GUI>
+ <xloc>80</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>5 rows</name>
+ <type>RowGenerator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>value</name>
+ <type>Integer</type>
+ <format>#</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <nullif>1</nullif>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <limit>5</limit>
+ <never_ending>N</never_ending>
+ <interval_in_ms>5000</interval_in_ms>
+ <row_time_field>now</row_time_field>
+ <last_time_field>FiveSecondsAgo</last_time_field>
+ <attributes/>
+ <GUI>
+ <xloc>80</xloc>
+ <yloc>224</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Dummy A</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>256</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Dummy B</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>256</xloc>
+ <yloc>224</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Output transform metrics</name>
+ <type>TransformsMetrics</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <transforms>
+ <transform>
+ <name>Dummy A</name>
+ <copyNr>0</copyNr>
+ <transformRequired>Y</transformRequired>
+ </transform>
+ <transform>
+ <name>Dummy B</name>
+ <copyNr>0</copyNr>
+ <transformRequired>Y</transformRequired>
+ </transform>
+ </transforms>
+ <transformnamefield>Transform name</transformnamefield>
+ <transformidfield>Transform id</transformidfield>
+ <transformlinesinputfield>Lines input</transformlinesinputfield>
+ <transformlinesoutputfield>Lines output</transformlinesoutputfield>
+ <transformlinesreadfield>Lines read</transformlinesreadfield>
+ <transformlinesupdatedfield>Lines updated</transformlinesupdatedfield>
+ <transformlineswrittenfield>Lines written</transformlineswrittenfield>
+ <transformlineserrorsfield>Lines rejected</transformlineserrorsfield>
+ <transformsecondsfield>Duration</transformsecondsfield>
+ <attributes/>
+ <GUI>
+ <xloc>448</xloc>
+ <yloc>152</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/plugins/transforms/transformsmetrics/src/test/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsMetaTest.java
b/plugins/transforms/transformsmetrics/src/test/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsMetaTest.java
new file mode 100644
index 0000000000..7231bbb5a4
--- /dev/null
+++
b/plugins/transforms/transformsmetrics/src/test/java/org/apache/hop/pipeline/transforms/transformsmetrics/TransformsMetricsMetaTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.pipeline.transforms.transformsmetrics;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class TransformsMetricsMetaTest {
+
+ @BeforeAll
+ static void beforeAll() throws Exception {
+ HopClientEnvironment.init();
+ }
+
+ @Test
+ void testXmlRoundTrip() throws Exception {
+ TransformsMetricsMeta meta = new TransformsMetricsMeta();
+ meta.setDefault();
+ meta.getMetricTransforms().add(new MetricTransform("A", "0", true));
+ meta.getMetricTransforms().add(new MetricTransform("B", "1", false));
+ meta.setTransformNameField("Transform name");
+ meta.setLinesWrittenField("Lines written");
+ meta.setDurationField("");
+
+ String transformXml =
+ XmlHandler.openTag(TransformMeta.XML_TAG)
+ + meta.getXml()
+ + XmlHandler.closeTag(TransformMeta.XML_TAG);
+ TransformsMetricsMeta loaded = new TransformsMetricsMeta();
+ loaded.loadXml(XmlHandler.loadXmlString(transformXml,
TransformMeta.XML_TAG), null);
+
+ assertEquals(2, loaded.getMetricTransforms().size());
+ assertEquals("A", loaded.getMetricTransforms().get(0).getName());
+ assertEquals("0", loaded.getMetricTransforms().get(0).getCopyNr());
+ assertTrue(loaded.getMetricTransforms().get(0).isRequired());
+ assertEquals("B", loaded.getMetricTransforms().get(1).getName());
+ assertFalse(loaded.getMetricTransforms().get(1).isRequired());
+ assertEquals("Transform name", loaded.getTransformNameField());
+ assertEquals("Lines written", loaded.getLinesWrittenField());
+ assertTrue(StringUtils.isBlank(loaded.getDurationField()));
+ assertTrue(transformXml.contains("<transforms>"));
+ assertTrue(transformXml.contains("<transformnamefield>"));
+ assertTrue(transformXml.contains("<transformlineswrittenfield>"));
+ }
+
+ @Test
+ void testGetFieldsClearsIncomingAndOmitsEmptyNames() throws Exception {
+ TransformsMetricsMeta meta = new TransformsMetricsMeta();
+ meta.setDefault();
+ meta.setTransformIdField("");
+ meta.setLinesInputField(null);
+ meta.setDurationField(" ");
+
+ IRowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("incoming"));
+ meta.getFields(rowMeta, "metrics", null, null, new Variables(), null);
+
+ List<String> names = new ArrayList<>();
+ for (int i = 0; i < rowMeta.size(); i++) {
+ names.add(rowMeta.getValueMeta(i).getName());
+ }
+ assertFalse(names.contains("incoming"));
+
assertTrue(names.contains(TransformsMetricsMeta.DEFAULT_TRANSFORM_NAME_FIELD));
+
assertFalse(names.contains(TransformsMetricsMeta.DEFAULT_TRANSFORM_ID_FIELD));
+
assertFalse(names.contains(TransformsMetricsMeta.DEFAULT_LINES_INPUT_FIELD));
+
assertTrue(names.contains(TransformsMetricsMeta.DEFAULT_LINES_WRITTEN_FIELD));
+ assertEquals(IValueMeta.TYPE_INTEGER, rowMeta.searchValueMeta("Lines
written").getType());
+ }
+
+ @Test
+ void testCheckEmptyListAndIncomingHops() {
+ TransformsMetricsMeta meta = new TransformsMetricsMeta();
+ meta.setDefault();
+ TransformMeta transformMeta = new TransformMeta();
+ transformMeta.setName("metrics");
+ PipelineMeta pipelineMeta = new PipelineMeta();
+
+ List<ICheckResult> remarks = new ArrayList<>();
+ meta.check(
+ remarks,
+ pipelineMeta,
+ transformMeta,
+ new RowMeta(),
+ new String[] {"upstream"},
+ new String[0],
+ new RowMeta(),
+ new Variables(),
+ null);
+ assertTrue(
+ remarks.stream().anyMatch(r -> r.getType() ==
ICheckResult.TYPE_RESULT_ERROR),
+ "incoming hops should be an error");
+ assertTrue(
+ remarks.stream().anyMatch(r -> r.getType() ==
ICheckResult.TYPE_RESULT_ERROR),
+ "empty watch list should be an error");
+ }
+
+ @Test
+ void testCheckMissingWatchedTransformIsWarning() {
+ TransformsMetricsMeta meta = new TransformsMetricsMeta();
+ meta.setDefault();
+ meta.getMetricTransforms().add(new MetricTransform("missing", "0", false));
+ TransformMeta transformMeta = new TransformMeta();
+ transformMeta.setName("metrics");
+ PipelineMeta pipelineMeta = new PipelineMeta();
+
+ List<ICheckResult> remarks = new ArrayList<>();
+ meta.check(
+ remarks,
+ pipelineMeta,
+ transformMeta,
+ new RowMeta(),
+ new String[0],
+ new String[0],
+ new RowMeta(),
+ new Variables(),
+ null);
+ assertTrue(remarks.stream().anyMatch(r -> r.getType() ==
ICheckResult.TYPE_RESULT_WARNING));
+ }
+}