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 db51796d70 fix custom logging in pipelines and workflows, fixes #2621 
(#7836)
db51796d70 is described below

commit db51796d70fae3e43e8429d7231e177741d87943
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Aug 7 20:34:09 2026 +0200

    fix custom logging in pipelines and workflows, fixes #2621 (#7836)
---
 .../hop/debug/action/ActionDebugGuiPlugin.java     |  10 +-
 .../action/EditActionDebugLevelExtensionPoint.java |   3 +-
 .../action/ModifyActionLogLevelExtensionPoint.java | 156 +++++++++++------
 .../SetTransformDebugLevelExtensionPoint.java      |  90 +++-------
 .../debug/transform/TransformDebugGuiPlugin.java   |  18 +-
 .../transform/TransformDebugLevelRowListener.java  | 125 +++++++++++++
 .../org/apache/hop/debug/util/DebugLevelUtil.java  |  68 +++++++
 .../ModifyActionLogLevelExtensionPointTest.java    | 131 ++++++++++++++
 .../SetTransformDebugLevelExtensionPointTest.java  | 195 +++++++++++++++++++++
 .../TransformDebugLevelRowListenerTest.java        | 188 ++++++++++++++++++++
 .../apache/hop/debug/util/DebugLevelUtilTest.java  |  23 +++
 .../transforms/addsequence/AddSequence.java        |   8 +-
 .../pipeline/transforms/calculator/Calculator.java |   3 +-
 .../transforms/columnexists/ColumnExists.java      |   2 +-
 .../hop/pipeline/transforms/constant/Constant.java |   2 +-
 .../pipeline/transforms/constant/ConstantTest.java |  34 ++++
 .../creditcardvalidator/CreditCardValidator.java   |   2 +-
 .../transforms/databaselookup/DatabaseLookup.java  |   2 +-
 .../transforms/execprocess/ExecProcess.java        |   2 +-
 .../fieldschangesequence/FieldsChangeSequence.java |   2 +-
 .../pipeline/transforms/fileexists/FileExists.java |   2 +-
 .../pipeline/transforms/filelocked/FileLocked.java |   7 +-
 .../hop/pipeline/transforms/formula/Formula.java   |   6 +-
 .../pipeline/transforms/samplerows/SampleRows.java |   2 +-
 .../transforms/tableexists/TableExists.java        |   2 +-
 .../webserviceavailable/WebServiceAvailable.java   |   6 +-
 26 files changed, 928 insertions(+), 161 deletions(-)

diff --git 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/ActionDebugGuiPlugin.java
 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/ActionDebugGuiPlugin.java
index a90bc7fe80..f7fb7809ad 100644
--- 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/ActionDebugGuiPlugin.java
+++ 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/ActionDebugGuiPlugin.java
@@ -17,7 +17,6 @@
 
 package org.apache.hop.debug.action;
 
-import java.util.HashMap;
 import java.util.Map;
 import org.apache.hop.core.action.GuiContextAction;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
@@ -70,13 +69,8 @@ public class ActionDebugGuiPlugin {
       ActionMeta action = context.getActionMeta();
       IVariables variables = context.getWorkflowGraph().getVariables();
 
-      Map<String, Map<String, String>> attributesMap = 
workflowMeta.getAttributesMap();
-      Map<String, String> debugGroupAttributesMap = 
attributesMap.get(Defaults.DEBUG_GROUP);
-
-      if (debugGroupAttributesMap == null) {
-        debugGroupAttributesMap = new HashMap<>();
-        attributesMap.put(Defaults.DEBUG_GROUP, debugGroupAttributesMap);
-      }
+      Map<String, String> debugGroupAttributesMap =
+          
DebugLevelUtil.getOrCreateDebugGroup(workflowMeta.getAttributesMap());
 
       ActionDebugLevel debugLevel =
           DebugLevelUtil.getActionDebugLevel(debugGroupAttributesMap, 
action.toString());
diff --git 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/EditActionDebugLevelExtensionPoint.java
 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/EditActionDebugLevelExtensionPoint.java
index ccf644d451..1f3ac3f797 100644
--- 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/EditActionDebugLevelExtensionPoint.java
+++ 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/EditActionDebugLevelExtensionPoint.java
@@ -23,7 +23,6 @@ import org.apache.hop.core.extension.IExtensionPoint;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.debug.util.DebugLevelUtil;
-import org.apache.hop.debug.util.Defaults;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.hopgui.HopGui;
 import 
org.apache.hop.ui.hopgui.file.workflow.extension.HopGuiWorkflowGraphExtension;
@@ -54,7 +53,7 @@ public class EditActionDebugLevelExtensionPoint
         ActionMeta actionCopy = (ActionMeta) ext.getAreaOwner().getParent();
 
         DebugLevelUtil.storeActionDebugLevel(
-            workflowMeta.getAttributesMap().get(Defaults.DEBUG_GROUP),
+            
DebugLevelUtil.getOrCreateDebugGroup(workflowMeta.getAttributesMap()),
             actionCopy.getName(),
             debugLevel);
         ext.getWorkflowGraph().redraw();
diff --git 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/ModifyActionLogLevelExtensionPoint.java
 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/ModifyActionLogLevelExtensionPoint.java
index 483b57d4e6..87c372c24b 100644
--- 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/ModifyActionLogLevelExtensionPoint.java
+++ 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/action/ModifyActionLogLevelExtensionPoint.java
@@ -173,64 +173,14 @@ public class ModifyActionLogLevelExtensionPoint
                   final ActionDebugLevel debugLevel =
                       DebugLevelUtil.getActionDebugLevel(entryLevelMap, 
actionCopy.toString());
                   if (debugLevel != null) {
-                    // Set the debug level for this one...
-                    //
-                    log.setLogLevel(jobLogLevel);
-                    workflow.setLogLevel(jobLogLevel);
-
-                    // Set the debug level back to normal...
-                    //
-
-                    if (debugLevel.isLoggingResult()) {
-                      log.logMinimal("Action results: ");
-                      log.logMinimal("  - result=" + result.isResult());
-                      log.logMinimal("  - stopped=" + result.isStopped());
-                      log.logMinimal("  - linesRead=" + 
result.getNrLinesRead());
-                      log.logMinimal("  - linesWritten=" + 
result.getNrLinesWritten());
-                      log.logMinimal("  - linesInput=" + 
result.getNrLinesInput());
-                      log.logMinimal("  - linesOutput=" + 
result.getNrLinesOutput());
-                      log.logMinimal("  - linesRejected=" + 
result.getNrLinesRejected());
-                      log.logMinimal("  - result row count=" + 
result.getRows().size());
-                      log.logMinimal(
-                          "  - result files count=" + 
result.getResultFilesList().size());
-                    }
-                    if (debugLevel.isLoggingResultRows()) {
-                      log.logMinimal("Action result rows: ");
-                      for (RowMetaAndData rmad : result.getRows()) {
-                        log.logMinimal(" - " + rmad.toString());
-                      }
-                    }
-                    if (debugLevel.isLoggingResultFiles()) {
-                      log.logMinimal("Action result files: ");
-                      for (ResultFile resultFile : 
result.getResultFilesList()) {
-                        log.logMinimal(
-                            " - "
-                                + resultFile.getFile().toString()
-                                + " from "
-                                + resultFile.getOrigin()
-                                + " : "
-                                + resultFile.getComment()
-                                + " / "
-                                + resultFile.getTypeCode());
-                      }
-                    }
-                    if (debugLevel.isLoggingVariables() && action instanceof 
IVariables) {
-                      log.logMinimal("Action notable variables: ");
-
-                      IVariables variables = (IVariables) action;
-                      // See the variables set differently from the parent 
workflow
-                      for (String var : variables.getVariableNames()) {
-                        if (!variablesToIgnore.contains(var)) {
-                          String value = variables.getVariable(var);
-                          String refValue = referenceSpace.getVariable(var);
-
-                          if (refValue == null || !refValue.equals(value)) {
-                            // Something different!
-                            //
-                            log.logMinimal(" - " + var + "=" + value);
-                          }
-                        }
-                      }
+                    try {
+                      logRequestedInformation(
+                          log, debugLevel, action, result, referenceSpace, 
variablesToIgnore);
+                    } finally {
+                      // Set the debug level back to normal...
+                      //
+                      log.setLogLevel(jobLogLevel);
+                      workflow.setLogLevel(jobLogLevel);
                     }
                   }
                 }
@@ -243,4 +193,94 @@ public class ModifyActionLogLevelExtensionPoint
       jobLog.logError("Unable to handle specific debug level for workflow", e);
     }
   }
+
+  /**
+   * Log the extra information that was asked for on this action: its result, 
its result rows, its
+   * result files and the variables it changed.
+   *
+   * <p>This has to happen before the log level of the action is put back to 
the level of the
+   * workflow. Doing it the other way around silently swallowed all of it 
whenever the workflow
+   * itself ran at a level that hides these messages, which is exactly the 
case in which someone
+   * sets a custom log level on a single action.
+   *
+   * <p>Everything here is logged with {@link ILogChannel#logMinimal(String)}, 
so the level is
+   * raised to at least {@link LogLevel#MINIMAL} first: the user explicitly 
ticked these options, so
+   * a workflow or custom log level of "Nothing" or "Error" should not hide 
them either.
+   *
+   * @param log the log channel of the action
+   * @param debugLevel the custom logging configuration of the action
+   * @param action the action that was executed
+   * @param result the result of the action
+   * @param referenceSpace the variables as they were at the start of the root 
workflow
+   * @param variablesToIgnore the variables that are never worth reporting
+   */
+  private static void logRequestedInformation(
+      ILogChannel log,
+      ActionDebugLevel debugLevel,
+      IAction action,
+      Result result,
+      IVariables referenceSpace,
+      Set<String> variablesToIgnore) {
+
+    if (!debugLevel.isLoggingResult()
+        && !debugLevel.isLoggingResultRows()
+        && !debugLevel.isLoggingResultFiles()
+        && !debugLevel.isLoggingVariables()) {
+      return;
+    }
+
+    if (!LogLevel.MINIMAL.isVisible(log.getLogLevel())) {
+      log.setLogLevel(LogLevel.MINIMAL);
+    }
+
+    if (debugLevel.isLoggingResult()) {
+      log.logMinimal("Action results: ");
+      log.logMinimal("  - result=" + result.isResult());
+      log.logMinimal("  - stopped=" + result.isStopped());
+      log.logMinimal("  - linesRead=" + result.getNrLinesRead());
+      log.logMinimal("  - linesWritten=" + result.getNrLinesWritten());
+      log.logMinimal("  - linesInput=" + result.getNrLinesInput());
+      log.logMinimal("  - linesOutput=" + result.getNrLinesOutput());
+      log.logMinimal("  - linesRejected=" + result.getNrLinesRejected());
+      log.logMinimal("  - result row count=" + result.getRows().size());
+      log.logMinimal("  - result files count=" + 
result.getResultFilesList().size());
+    }
+    if (debugLevel.isLoggingResultRows()) {
+      log.logMinimal("Action result rows: ");
+      for (RowMetaAndData rmad : result.getRows()) {
+        log.logMinimal(" - " + rmad.toString());
+      }
+    }
+    if (debugLevel.isLoggingResultFiles()) {
+      log.logMinimal("Action result files: ");
+      for (ResultFile resultFile : result.getResultFilesList()) {
+        log.logMinimal(
+            " - "
+                + resultFile.getFile().toString()
+                + " from "
+                + resultFile.getOrigin()
+                + " : "
+                + resultFile.getComment()
+                + " / "
+                + resultFile.getTypeCode());
+      }
+    }
+    if (debugLevel.isLoggingVariables() && action instanceof IVariables 
actionVariables) {
+      log.logMinimal("Action notable variables: ");
+
+      // See the variables set differently from the parent workflow
+      for (String var : actionVariables.getVariableNames()) {
+        if (!variablesToIgnore.contains(var)) {
+          String value = actionVariables.getVariable(var);
+          String refValue = referenceSpace.getVariable(var);
+
+          if (refValue == null || !refValue.equals(value)) {
+            // Something different!
+            //
+            log.logMinimal(" - " + var + "=" + value);
+          }
+        }
+      }
+    }
+  }
 }
diff --git 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/SetTransformDebugLevelExtensionPoint.java
 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/SetTransformDebugLevelExtensionPoint.java
index 50525e7005..c646755517 100644
--- 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/SetTransformDebugLevelExtensionPoint.java
+++ 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/SetTransformDebugLevelExtensionPoint.java
@@ -20,22 +20,17 @@ package org.apache.hop.debug.transform;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicLong;
-import org.apache.hop.core.Condition;
 import org.apache.hop.core.exception.HopException;
-import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.extension.ExtensionPoint;
 import org.apache.hop.core.extension.IExtensionPoint;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.logging.LogLevel;
-import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.debug.util.DebugLevelUtil;
 import org.apache.hop.debug.util.Defaults;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.engine.IEngineComponent;
 import org.apache.hop.pipeline.engine.IPipelineEngine;
-import org.apache.hop.pipeline.transform.IRowListener;
 
 @ExtensionPoint(
     id = "SetTransformDebugLevelExtensionPoint",
@@ -84,9 +79,7 @@ public class SetTransformDebugLevelExtensionPoint
             final LogLevel resolvedLogLevel =
                 DebugLevelUtil.resolveLogLevel(variables, 
debugLevel.getLogLevel());
 
-            if (debugLevel.getStartRow() < 0
-                && debugLevel.getEndRow() < 0
-                && debugLevel.getCondition().isEmpty()) {
+            if (!hasRowSelection(debugLevel)) {
               log.logDetailed(
                   "Set logging level for transform "
                       + transformName
@@ -106,63 +99,25 @@ public class SetTransformDebugLevelExtensionPoint
                         + transformCopy.getCopyNr());
               }
             } else {
-              // We need to look at every row
+              // We need to look at every row to see whether the custom log 
level applies to it.
               //
-              for (IEngineComponent transformCopy : transformCopies) {
+              boolean readingRows =
+                  DebugLevelUtil.isReadingRows(pipeline.getPipelineMeta(), 
transformName);
 
-                final LogLevel baseLogLevel = 
transformCopy.getLogChannel().getLogLevel();
-                final AtomicLong rowCounter = new AtomicLong(0L);
+              log.logDetailed(
+                  "Set logging level for the selected rows of transform "
+                      + transformName
+                      + " to "
+                      + resolvedLogLevel.getDescription());
 
+              for (IEngineComponent transformCopy : transformCopies) {
                 transformCopy.addRowListener(
-                    new IRowListener() {
-                      @Override
-                      public void rowReadEvent(IRowMeta rowMeta, Object[] row) 
{
-                        rowCounter.incrementAndGet();
-                        boolean enabled = false;
-
-                        Condition condition = debugLevel.getCondition();
-
-                        if (debugLevel.getStartRow() > 0
-                            && rowCounter.get() >= debugLevel.getStartRow()
-                            && debugLevel.getEndRow() >= 0
-                            && debugLevel.getEndRow() >= rowCounter.get()) {
-                          // If we have a start and an end, we want to stay 
between start and end
-                          enabled = true;
-                        } else if (debugLevel.getStartRow() <= 0
-                            && debugLevel.getEndRow() >= 0
-                            && rowCounter.get() <= debugLevel.getEndRow()) {
-                          // If don't have a start row, just and end...
-                          enabled = true;
-                        } else if (debugLevel.getEndRow() <= 0
-                            && debugLevel.getStartRow() >= 0
-                            && rowCounter.get() >= debugLevel.getStartRow()) {
-                          enabled = true;
-                        }
-
-                        if ((debugLevel.getStartRow() <= 0 && 
debugLevel.getEndRow() <= 0
-                                || enabled)
-                            && !condition.isEmpty()) {
-                          enabled = condition.evaluate(rowMeta, row);
-                        }
-
-                        if (enabled) {
-                          transformCopy.setLogLevel(resolvedLogLevel);
-                        }
-                      }
-
-                      @Override
-                      public void rowWrittenEvent(IRowMeta rowMeta, Object[] 
row)
-                          throws HopTransformException {
-                        // Set the log level back to the original value.
-                        //
-                        
transformCopy.getLogChannel().setLogLevel(baseLogLevel);
-                      }
-
-                      @Override
-                      public void errorRowWrittenEvent(IRowMeta rowMeta, 
Object[] row) {
-                        // Do nothing
-                      }
-                    });
+                    new TransformDebugLevelRowListener(
+                        transformCopy,
+                        debugLevel,
+                        resolvedLogLevel,
+                        transformCopy.getLogChannel().getLogLevel(),
+                        readingRows));
               }
             }
           }
@@ -172,4 +127,17 @@ public class SetTransformDebugLevelExtensionPoint
       }
     }
   }
+
+  /**
+   * Does this configuration only apply the custom log level to a selection of 
the rows?
+   *
+   * @param debugLevel the custom logging configuration
+   * @return true if a start row, an end row or a condition narrows the 
configuration down to a
+   *     selection of rows
+   */
+  private static boolean hasRowSelection(TransformDebugLevel debugLevel) {
+    return debugLevel.getStartRow() > 0
+        || debugLevel.getEndRow() > 0
+        || (debugLevel.getCondition() != null && 
!debugLevel.getCondition().isEmpty());
+  }
 }
diff --git 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/TransformDebugGuiPlugin.java
 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/TransformDebugGuiPlugin.java
index b2c8507b66..d1151d9157 100644
--- 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/TransformDebugGuiPlugin.java
+++ 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/TransformDebugGuiPlugin.java
@@ -17,7 +17,6 @@
 
 package org.apache.hop.debug.transform;
 
-import java.util.HashMap;
 import java.util.Map;
 import org.apache.hop.core.action.GuiContextAction;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
@@ -71,13 +70,8 @@ public class TransformDebugGuiPlugin {
       TransformMeta transformMeta = context.getTransformMeta();
       IVariables variables = context.getPipelineGraph().getVariables();
 
-      Map<String, Map<String, String>> attributesMap = 
pipelineMeta.getAttributesMap();
-      Map<String, String> debugGroupAttributesMap = 
attributesMap.get(Defaults.DEBUG_GROUP);
-
-      if (debugGroupAttributesMap == null) {
-        debugGroupAttributesMap = new HashMap<>();
-        attributesMap.put(Defaults.DEBUG_GROUP, debugGroupAttributesMap);
-      }
+      Map<String, String> debugGroupAttributesMap =
+          
DebugLevelUtil.getOrCreateDebugGroup(pipelineMeta.getAttributesMap());
 
       TransformDebugLevel debugLevel =
           DebugLevelUtil.getTransformDebugLevel(debugGroupAttributesMap, 
transformMeta.getName());
@@ -85,10 +79,14 @@ public class TransformDebugGuiPlugin {
         debugLevel = new TransformDebugLevel();
       }
 
-      IRowMeta inputRowMeta = pipelineMeta.getPrevTransformFields(variables, 
transformMeta);
+      // The fields a condition can be built on are the fields the condition 
is evaluated against
+      // at runtime: the incoming fields, or the transform's own fields if it 
generates rows.
+      //
+      IRowMeta conditionRowMeta =
+          DebugLevelUtil.getConditionFields(variables, pipelineMeta, 
transformMeta);
       TransformDebugLevelDialog dialog =
           new TransformDebugLevelDialog(
-              hopGui.getActiveShell(), debugLevel, inputRowMeta, variables);
+              hopGui.getActiveShell(), debugLevel, conditionRowMeta, 
variables);
       if (dialog.open()) {
         DebugLevelUtil.storeTransformDebugLevel(
             debugGroupAttributesMap, transformMeta.getName(), debugLevel);
diff --git 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/TransformDebugLevelRowListener.java
 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/TransformDebugLevelRowListener.java
new file mode 100644
index 0000000000..d3526d166c
--- /dev/null
+++ 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/transform/TransformDebugLevelRowListener.java
@@ -0,0 +1,125 @@
+/*
+ * 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.debug.transform;
+
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.hop.core.Condition;
+import org.apache.hop.core.logging.LogLevel;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.pipeline.engine.IEngineComponent;
+import org.apache.hop.pipeline.transform.IRowListener;
+
+/**
+ * Switches the configured custom log level on for the rows selected by a 
{@link
+ * TransformDebugLevel} start/end row range and/or condition, and back off for 
all other rows.
+ *
+ * <p>The level is evaluated exactly once per row and then left alone for the 
rest of that row's
+ * processing. That matters because transforms log their row <em>after</em> 
they called {@code
+ * putRow()}: switching the level back off on the write event (as this used to 
do) killed every
+ * single row-level line the transform was supposed to produce.
+ *
+ * <p>Which event drives the evaluation depends on the transform: transforms 
that read rows are
+ * evaluated on the read event (the row is then available for the condition 
before the transform
+ * does any work with it), transforms that only generate rows are evaluated on 
the write event,
+ * since they never fire a read event at all.
+ */
+public class TransformDebugLevelRowListener implements IRowListener {
+
+  private final IEngineComponent component;
+  private final TransformDebugLevel debugLevel;
+  private final LogLevel customLogLevel;
+  private final LogLevel baseLogLevel;
+  private final boolean readingRows;
+
+  private final AtomicLong rowCounter = new AtomicLong(0L);
+
+  /**
+   * @param component the transform copy to change the log level on
+   * @param debugLevel the custom logging configuration to apply
+   * @param customLogLevel the resolved log level to switch on for the 
selected rows
+   * @param baseLogLevel the log level to fall back to for all other rows
+   * @param readingRows true if the transform reads rows from other 
transforms, false if it only
+   *     generates them
+   */
+  public TransformDebugLevelRowListener(
+      IEngineComponent component,
+      TransformDebugLevel debugLevel,
+      LogLevel customLogLevel,
+      LogLevel baseLogLevel,
+      boolean readingRows) {
+    this.component = component;
+    this.debugLevel = debugLevel;
+    this.customLogLevel = customLogLevel;
+    this.baseLogLevel = baseLogLevel;
+    this.readingRows = readingRows;
+  }
+
+  @Override
+  public void rowReadEvent(IRowMeta rowMeta, Object[] row) {
+    if (readingRows) {
+      applyLogLevel(rowMeta, row);
+    }
+  }
+
+  @Override
+  public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) {
+    if (!readingRows) {
+      applyLogLevel(rowMeta, row);
+    }
+  }
+
+  @Override
+  public void errorRowWrittenEvent(IRowMeta rowMeta, Object[] row) {
+    // Error rows are handled by the error handling of the transform, leave 
the level alone.
+  }
+
+  private void applyLogLevel(IRowMeta rowMeta, Object[] row) {
+    long rowNumber = rowCounter.incrementAndGet();
+    component
+        .getLogChannel()
+        .setLogLevel(isSelected(rowNumber, rowMeta, row) ? customLogLevel : 
baseLogLevel);
+  }
+
+  /**
+   * Is the given row one of the rows we want the custom log level for?
+   *
+   * @param rowNumber the 1-based number of the row in this transform copy
+   * @param rowMeta the metadata of the row
+   * @param row the row itself
+   * @return true if the custom log level applies to this row
+   */
+  boolean isSelected(long rowNumber, IRowMeta rowMeta, Object[] row) {
+    int startRow = debugLevel.getStartRow();
+    int endRow = debugLevel.getEndRow();
+
+    // A start or end row of 0 or less simply means: no boundary on that side.
+    //
+    if (startRow > 0 && rowNumber < startRow) {
+      return false;
+    }
+    if (endRow > 0 && rowNumber > endRow) {
+      return false;
+    }
+
+    Condition condition = debugLevel.getCondition();
+    if (condition == null || condition.isEmpty()) {
+      return true;
+    }
+    return condition.evaluate(rowMeta, row);
+  }
+}
diff --git 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/util/DebugLevelUtil.java
 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/util/DebugLevelUtil.java
index b8ef3c5332..5d51e9f0f5 100644
--- 
a/plugins/misc/debug/src/main/java/org/apache/hop/debug/util/DebugLevelUtil.java
+++ 
b/plugins/misc/debug/src/main/java/org/apache/hop/debug/util/DebugLevelUtil.java
@@ -20,17 +20,23 @@ package org.apache.hop.debug.util;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.StandardCharsets;
 import java.util.Base64;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.core.Condition;
 import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.exception.HopValueException;
 import org.apache.hop.core.exception.HopXmlException;
 import org.apache.hop.core.logging.LogLevel;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.debug.action.ActionDebugLevel;
 import org.apache.hop.debug.transform.TransformDebugLevel;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
 
 public class DebugLevelUtil {
 
@@ -87,8 +93,70 @@ public class DebugLevelUtil {
     return debugLevel;
   }
 
+  /**
+   * Does the given transform read rows from other transforms, or does it only 
generate them? Custom
+   * logging hooks into the read event of the former and into the write event 
of the latter, so both
+   * the runtime and the dialog use this to know which rows the start/end row 
range counts and which
+   * fields a condition can be built on.
+   *
+   * @param pipelineMeta the pipeline the transform lives in
+   * @param transformName the name of the transform
+   * @return true if the transform has one or more (info) input transforms
+   */
+  public static boolean isReadingRows(PipelineMeta pipelineMeta, String 
transformName) {
+    return isReadingRows(pipelineMeta, 
pipelineMeta.findTransform(transformName));
+  }
+
+  /**
+   * @see #isReadingRows(PipelineMeta, String)
+   * @param pipelineMeta the pipeline the transform lives in
+   * @param transformMeta the transform
+   * @return true if the transform has one or more (info) input transforms
+   */
+  public static boolean isReadingRows(PipelineMeta pipelineMeta, TransformMeta 
transformMeta) {
+    return transformMeta != null
+        && !pipelineMeta.findPreviousTransforms(transformMeta, true).isEmpty();
+  }
+
+  /**
+   * Get the fields a custom logging condition is evaluated against for the 
given transform: the
+   * incoming fields for a transform that reads rows, its own output fields 
for a transform that
+   * only generates rows.
+   *
+   * @param variables the variables to resolve the metadata with
+   * @param pipelineMeta the pipeline the transform lives in
+   * @param transformMeta the transform
+   * @return the fields to build a condition on, never null
+   */
+  public static IRowMeta getConditionFields(
+      IVariables variables, PipelineMeta pipelineMeta, TransformMeta 
transformMeta)
+      throws HopTransformException {
+    IRowMeta rowMeta =
+        isReadingRows(pipelineMeta, transformMeta)
+            ? pipelineMeta.getPrevTransformFields(variables, transformMeta)
+            : pipelineMeta.getTransformFields(variables, transformMeta);
+    return rowMeta == null ? new RowMeta() : rowMeta;
+  }
+
+  /**
+   * Get the attributes group custom logging is stored in, creating it if the 
pipeline or workflow
+   * doesn't have one yet.
+   *
+   * @param attributesMap the attributes map of a pipeline or workflow
+   * @return the debug group attributes, never null
+   */
+  public static Map<String, String> getOrCreateDebugGroup(
+      Map<String, Map<String, String>> attributesMap) {
+    return attributesMap.computeIfAbsent(Defaults.DEBUG_GROUP, key -> new 
HashMap<>());
+  }
+
   public static void clearDebugLevel(
       Map<String, String> debugGroupAttributesMap, String transformName) {
+    if (debugGroupAttributesMap == null) {
+      // Nothing was ever configured on this pipeline or workflow, so nothing 
to clear.
+      //
+      return;
+    }
     debugGroupAttributesMap.remove(transformName + " : " + 
Defaults.TRANSFORM_ATTR_LOGLEVEL);
     debugGroupAttributesMap.remove(transformName + " : " + 
Defaults.TRANSFORM_ATTR_START_ROW);
     debugGroupAttributesMap.remove(transformName + " : " + 
Defaults.TRANSFORM_ATTR_END_ROW);
diff --git 
a/plugins/misc/debug/src/test/java/org/apache/hop/debug/action/ModifyActionLogLevelExtensionPointTest.java
 
b/plugins/misc/debug/src/test/java/org/apache/hop/debug/action/ModifyActionLogLevelExtensionPointTest.java
new file mode 100644
index 0000000000..78c34a1686
--- /dev/null
+++ 
b/plugins/misc/debug/src/test/java/org/apache/hop/debug/action/ModifyActionLogLevelExtensionPointTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.debug.action;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.logging.DefaultLogLevel;
+import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.logging.HopLoggingEvent;
+import org.apache.hop.core.logging.IHopLoggingEventListener;
+import org.apache.hop.core.logging.LogLevel;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.debug.util.DebugLevelUtil;
+import org.apache.hop.debug.util.Defaults;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.action.ActionMeta;
+import org.apache.hop.workflow.actions.dummy.ActionDummy;
+import org.apache.hop.workflow.actions.start.ActionStart;
+import org.apache.hop.workflow.engines.local.LocalWorkflowEngine;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Custom logging on an action offers to log the result, the result rows, the 
result files and the
+ * changed variables of that action. That output has to survive a workflow 
that itself logs nothing,
+ * which is the whole point of setting a custom level on a single action.
+ */
+class ModifyActionLogLevelExtensionPointTest {
+
+  private static final String ACTION = "dummy";
+
+  @BeforeAll
+  static void beforeAll() throws Exception {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void theActionResultIsLoggedWhenTheWorkflowItselfLogsNothing() throws 
Exception {
+    ActionDebugLevel debugLevel = new ActionDebugLevel(LogLevel.DETAILED);
+    debugLevel.setLoggingResult(true);
+
+    List<String> logged = runAndCollectLog(debugLevel, LogLevel.NOTHING);
+
+    assertTrue(
+        logged.stream().anyMatch(line -> line.contains("Action results:")),
+        "the action result was not logged, log was: " + logged);
+  }
+
+  /**
+   * The custom level can also be lower than the workflow's own level. Asking 
for the result of an
+   * action still has to log it - the checkbox is more specific than the level.
+   */
+  @Test
+  void theActionResultIsLoggedWhenTheCustomLevelIsLowerThanTheWorkflowLevel() 
throws Exception {
+    ActionDebugLevel debugLevel = new ActionDebugLevel(LogLevel.ERROR);
+    debugLevel.setLoggingResult(true);
+
+    List<String> logged = runAndCollectLog(debugLevel, LogLevel.BASIC);
+
+    assertTrue(
+        logged.stream().anyMatch(line -> line.contains("Action results:")),
+        "the action result was not logged, log was: " + logged);
+  }
+
+  /** Run a "start -> dummy" workflow with a custom logging configuration on 
the dummy action. */
+  private List<String> runAndCollectLog(ActionDebugLevel debugLevel, LogLevel 
workflowLogLevel)
+      throws Exception {
+    WorkflowMeta workflowMeta = new WorkflowMeta();
+    workflowMeta.setName("custom action logging");
+
+    ActionMeta start = new ActionMeta(new ActionStart("start"));
+    ActionMeta dummy = new ActionMeta(new ActionDummy());
+    dummy.setName(ACTION);
+    workflowMeta.addAction(start);
+    workflowMeta.addAction(dummy);
+    workflowMeta.addWorkflowHop(new 
org.apache.hop.workflow.WorkflowHopMeta(start, dummy));
+
+    Map<String, String> debugGroup = new HashMap<>();
+    DebugLevelUtil.storeActionDebugLevel(debugGroup, ACTION, debugLevel);
+    workflowMeta.getAttributesMap().put(Defaults.DEBUG_GROUP, debugGroup);
+
+    // The workflow creates its log channel when it starts, and without a 
parent logging object
+    // that channel takes its level from the default log level - the same knob 
the GUI and hop-run
+    // turn when you pick a log level for a run.
+    //
+    LogLevel previousDefault = DefaultLogLevel.getLogLevel();
+    DefaultLogLevel.setLogLevel(workflowLogLevel);
+
+    List<String> logged = new ArrayList<>();
+    IHopLoggingEventListener listener =
+        (HopLoggingEvent event) -> {
+          synchronized (logged) {
+            logged.add(event.getMessage().toString());
+          }
+        };
+    HopLogStore.getAppender().addLoggingEventListener(listener);
+    try {
+      LocalWorkflowEngine workflow = new LocalWorkflowEngine(workflowMeta);
+      workflow.initializeFrom(new Variables());
+      workflow.setLogLevel(workflowLogLevel);
+      workflow.startExecution();
+    } finally {
+      HopLogStore.getAppender().removeLoggingEventListener(listener);
+      DefaultLogLevel.setLogLevel(previousDefault);
+    }
+
+    synchronized (logged) {
+      return new ArrayList<>(logged);
+    }
+  }
+}
diff --git 
a/plugins/misc/debug/src/test/java/org/apache/hop/debug/transform/SetTransformDebugLevelExtensionPointTest.java
 
b/plugins/misc/debug/src/test/java/org/apache/hop/debug/transform/SetTransformDebugLevelExtensionPointTest.java
new file mode 100644
index 0000000000..2cf33edc32
--- /dev/null
+++ 
b/plugins/misc/debug/src/test/java/org/apache/hop/debug/transform/SetTransformDebugLevelExtensionPointTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.debug.transform;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.core.Condition;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.logging.LogLevel;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.ValueMetaAndData;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.debug.util.DebugLevelUtil;
+import org.apache.hop.debug.util.Defaults;
+import org.apache.hop.pipeline.PipelineHopMeta;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.RowProducer;
+import org.apache.hop.pipeline.engine.IEngineComponent;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transform.IRowListener;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.dummy.DummyMeta;
+import org.apache.hop.pipeline.transforms.injector.InjectorMeta;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Runs a real pipeline with a pipeline log level that logs nothing and custom 
logging configured on
+ * one transform, to verify that the row level lines of the selected rows -- 
and only those -- end
+ * up in the log.
+ */
+class SetTransformDebugLevelExtensionPointTest {
+
+  private static final String INPUT = "input";
+  private static final String DUMMY = "dummy";
+  private static final int NR_ROWS = 5;
+  private static final String WROTE_ROW = "Wrote row #";
+
+  @BeforeAll
+  static void beforeAll() throws Exception {
+    HopEnvironment.init();
+  }
+
+  @Test
+  void onlyTheRowsInTheRangeAreLogged() throws Exception {
+    TransformDebugLevel debugLevel = new 
TransformDebugLevel(LogLevel.ROWLEVEL);
+    debugLevel.setStartRow(2);
+    debugLevel.setEndRow(4);
+
+    assertEquals(List.of(2L, 3L, 4L), runAndCollectLoggedRows(debugLevel));
+  }
+
+  @Test
+  void onlyTheRowsMatchingTheConditionAreLogged() throws Exception {
+    TransformDebugLevel debugLevel = new 
TransformDebugLevel(LogLevel.ROWLEVEL);
+    debugLevel.setCondition(
+        new Condition(
+            "seq",
+            Condition.Function.LARGER,
+            null,
+            new ValueMetaAndData(new ValueMetaInteger("constant"), 3L)));
+
+    assertEquals(List.of(4L, 5L), runAndCollectLoggedRows(debugLevel));
+  }
+
+  @Test
+  void withoutARangeOrConditionEveryRowIsLogged() throws Exception {
+    assertEquals(
+        List.of(1L, 2L, 3L, 4L, 5L),
+        runAndCollectLoggedRows(new TransformDebugLevel(LogLevel.ROWLEVEL)));
+  }
+
+  /**
+   * Run an "injector -> dummy" pipeline with the pipeline log level set to 
nothing and the given
+   * custom logging configuration on the dummy transform.
+   *
+   * @param debugLevel the custom logging configuration for the dummy transform
+   * @return the numbers of the rows the dummy transform logged, in order
+   */
+  private List<Long> runAndCollectLoggedRows(TransformDebugLevel debugLevel) 
throws Exception {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.setName("custom logging");
+
+    TransformMeta input = new TransformMeta(INPUT, new InjectorMeta());
+    TransformMeta dummy = new TransformMeta(DUMMY, new DummyMeta());
+    pipelineMeta.addTransform(input);
+    pipelineMeta.addTransform(dummy);
+    pipelineMeta.addPipelineHop(new PipelineHopMeta(input, dummy));
+
+    Map<String, String> debugGroup = new HashMap<>();
+    DebugLevelUtil.storeTransformDebugLevel(debugGroup, DUMMY, debugLevel);
+    pipelineMeta.getAttributesMap().put(Defaults.DEBUG_GROUP, debugGroup);
+
+    LocalPipelineEngine pipeline = new LocalPipelineEngine(pipelineMeta, new 
Variables(), null);
+    pipeline.setLogLevel(LogLevel.NOTHING);
+    pipeline.prepareExecution();
+
+    IEngineComponent dummyCopy = pipeline.getComponentCopies(DUMMY).get(0);
+    String dummyLogChannelId = dummyCopy.getLogChannelId();
+
+    // A dummy transform doesn't log anything per row itself, so stand in for 
a transform that does.
+    //
+    dummyCopy.addRowListener(new RowLevelLogger(dummyCopy));
+
+    RowProducer rowProducer = pipeline.addRowProducer(INPUT, 0);
+    pipeline.startThreads();
+
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaInteger("seq"));
+    for (long i = 1; i <= NR_ROWS; i++) {
+      rowProducer.putRow(rowMeta, new Object[] {i});
+    }
+    rowProducer.finished();
+    pipeline.waitUntilFinished();
+
+    assertTrue(pipeline.getErrors() == 0, "The pipeline reported errors");
+
+    return 
loggedRowNumbers(HopLogStore.getAppender().getBuffer(dummyLogChannelId, false));
+  }
+
+  /** Pick the row numbers out of the "Wrote row #N" lines that reached the 
log. */
+  private static List<Long> loggedRowNumbers(CharSequence logText) {
+    List<Long> rowNumbers = new ArrayList<>();
+    for (String line : logText.toString().split("\n")) {
+      int index = line.indexOf(WROTE_ROW);
+      if (index >= 0) {
+        String number = line.substring(index + WROTE_ROW.length()).split(" 
")[0];
+        rowNumbers.add(Long.valueOf(number));
+      }
+    }
+    return rowNumbers;
+  }
+
+  /**
+   * Logs one row level line per written row, the way the transforms that 
support custom logging do.
+   * Row level lines only make it into the log while the custom log level is 
switched on, so the
+   * lines this produces are exactly the rows the configuration selected.
+   *
+   * <p>Logging on the write event -- like the real transforms, which log 
after they called {@code
+   * putRow()} -- keeps this independent of the order the row listeners were 
added in: the custom
+   * log level is switched on when the row is read, well before this listener 
is called for it.
+   */
+  private static class RowLevelLogger implements IRowListener {
+
+    private final IEngineComponent component;
+
+    private RowLevelLogger(IEngineComponent component) {
+      this.component = component;
+    }
+
+    @Override
+    public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) throws 
HopTransformException {
+      try {
+        component.getLogChannel().logRowlevel(WROTE_ROW + 
rowMeta.getInteger(row, 0));
+      } catch (HopValueException e) {
+        throw new HopTransformException(e);
+      }
+    }
+
+    @Override
+    public void rowReadEvent(IRowMeta rowMeta, Object[] row) {
+      // Nothing to log here, see rowWrittenEvent()
+    }
+
+    @Override
+    public void errorRowWrittenEvent(IRowMeta rowMeta, Object[] row) {
+      // Nothing to log here, see rowWrittenEvent()
+    }
+  }
+}
diff --git 
a/plugins/misc/debug/src/test/java/org/apache/hop/debug/transform/TransformDebugLevelRowListenerTest.java
 
b/plugins/misc/debug/src/test/java/org/apache/hop/debug/transform/TransformDebugLevelRowListenerTest.java
new file mode 100644
index 0000000000..c5e7f7c6e5
--- /dev/null
+++ 
b/plugins/misc/debug/src/test/java/org/apache/hop/debug/transform/TransformDebugLevelRowListenerTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.debug.transform;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hop.core.Condition;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.logging.LogLevel;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.ValueMetaAndData;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.pipeline.engine.IEngineComponent;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The custom log level has to be switched on for exactly the selected rows, 
and it has to still be
+ * switched on by the time the transform logs the row, which transforms do 
after they called {@code
+ * putRow()}.
+ */
+class TransformDebugLevelRowListenerTest {
+
+  private static final LogLevel BASE = LogLevel.NOTHING;
+  private static final LogLevel CUSTOM = LogLevel.ROWLEVEL;
+
+  private final AtomicReference<LogLevel> currentLevel = new 
AtomicReference<>(BASE);
+
+  @BeforeAll
+  static void beforeAll() throws HopException {
+    HopClientEnvironment.init();
+  }
+
+  // ------------------------------------------------------------------ helpers
+
+  private IEngineComponent component() {
+    currentLevel.set(BASE);
+    ILogChannel logChannel = mock(ILogChannel.class);
+    doAnswer(
+            invocation -> {
+              currentLevel.set(invocation.getArgument(0));
+              return null;
+            })
+        .when(logChannel)
+        .setLogLevel(any(LogLevel.class));
+    when(logChannel.getLogLevel()).thenAnswer(invocation -> 
currentLevel.get());
+
+    IEngineComponent component = mock(IEngineComponent.class);
+    when(component.getLogChannel()).thenReturn(logChannel);
+    return component;
+  }
+
+  private static TransformDebugLevel debugLevel(int startRow, int endRow, 
Condition condition) {
+    TransformDebugLevel debugLevel = new TransformDebugLevel(CUSTOM);
+    debugLevel.setStartRow(startRow);
+    debugLevel.setEndRow(endRow);
+    if (condition != null) {
+      debugLevel.setCondition(condition);
+    }
+    return debugLevel;
+  }
+
+  private static IRowMeta rowMeta() {
+    IRowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaInteger("seq"));
+    return rowMeta;
+  }
+
+  /**
+   * Push a number of rows through the listener the way a transform does and 
collect, per row, the
+   * log level that is active at the moment the transform logs its row: right 
after {@code putRow()}
+   * returned.
+   */
+  private List<LogLevel> levelsWhenLogging(
+      TransformDebugLevelRowListener listener, boolean readingRows, int nrRows)
+      throws HopException {
+    IRowMeta rowMeta = rowMeta();
+    List<LogLevel> levels = new ArrayList<>();
+    for (int i = 1; i <= nrRows; i++) {
+      Object[] row = new Object[] {(long) i};
+      if (readingRows) {
+        listener.rowReadEvent(rowMeta, row);
+      }
+      listener.rowWrittenEvent(rowMeta, row);
+      levels.add(currentLevel.get());
+    }
+    return levels;
+  }
+
+  // -------------------------------------------------------------------- tests
+
+  @Test
+  void rangeIsAppliedWhileTheTransformLogsTheRow() throws Exception {
+    TransformDebugLevelRowListener listener =
+        new TransformDebugLevelRowListener(component(), debugLevel(2, 4, 
null), CUSTOM, BASE, true);
+
+    assertEquals(List.of(BASE, CUSTOM, CUSTOM, CUSTOM, BASE), 
levelsWhenLogging(listener, true, 5));
+  }
+
+  @Test
+  void rangeIsAppliedForTransformsThatOnlyGenerateRows() throws Exception {
+    // No read events at all: the row counter has to run on the written rows 
instead.
+    //
+    TransformDebugLevelRowListener listener =
+        new TransformDebugLevelRowListener(
+            component(), debugLevel(2, 4, null), CUSTOM, BASE, false);
+
+    assertEquals(
+        List.of(BASE, CUSTOM, CUSTOM, CUSTOM, BASE), 
levelsWhenLogging(listener, false, 5));
+  }
+
+  @Test
+  void startRowWithoutEndRowRunsUntilTheEnd() throws Exception {
+    TransformDebugLevelRowListener listener =
+        new TransformDebugLevelRowListener(
+            component(), debugLevel(3, -1, null), CUSTOM, BASE, true);
+
+    assertEquals(List.of(BASE, BASE, CUSTOM, CUSTOM, CUSTOM), 
levelsWhenLogging(listener, true, 5));
+  }
+
+  @Test
+  void endRowWithoutStartRowStartsAtTheFirstRow() throws Exception {
+    TransformDebugLevelRowListener listener =
+        new TransformDebugLevelRowListener(
+            component(), debugLevel(-1, 2, null), CUSTOM, BASE, true);
+
+    assertEquals(List.of(CUSTOM, CUSTOM, BASE, BASE, BASE), 
levelsWhenLogging(listener, true, 5));
+  }
+
+  @Test
+  void conditionSelectsTheMatchingRows() throws Exception {
+    Condition condition =
+        new Condition(
+            "seq",
+            Condition.Function.EQUAL,
+            null,
+            new ValueMetaAndData(new ValueMetaInteger("constant"), 3L));
+
+    TransformDebugLevelRowListener listener =
+        new TransformDebugLevelRowListener(
+            component(), debugLevel(-1, -1, condition), CUSTOM, BASE, true);
+
+    assertEquals(List.of(BASE, BASE, CUSTOM, BASE, BASE), 
levelsWhenLogging(listener, true, 5));
+  }
+
+  @Test
+  void conditionIsCombinedWithTheRange() throws Exception {
+    Condition condition =
+        new Condition(
+            "seq",
+            Condition.Function.LARGER,
+            null,
+            new ValueMetaAndData(new ValueMetaInteger("constant"), 2L));
+
+    TransformDebugLevelRowListener listener =
+        new TransformDebugLevelRowListener(
+            component(), debugLevel(-1, 4, condition), CUSTOM, BASE, true);
+
+    // Row 1 and 2 fail the condition, row 5 falls outside the range.
+    //
+    assertEquals(List.of(BASE, BASE, CUSTOM, CUSTOM, BASE), 
levelsWhenLogging(listener, true, 5));
+  }
+}
diff --git 
a/plugins/misc/debug/src/test/java/org/apache/hop/debug/util/DebugLevelUtilTest.java
 
b/plugins/misc/debug/src/test/java/org/apache/hop/debug/util/DebugLevelUtilTest.java
index 733a15968b..36a0e03f8b 100644
--- 
a/plugins/misc/debug/src/test/java/org/apache/hop/debug/util/DebugLevelUtilTest.java
+++ 
b/plugins/misc/debug/src/test/java/org/apache/hop/debug/util/DebugLevelUtilTest.java
@@ -17,9 +17,11 @@
 
 package org.apache.hop.debug.util;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 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 static org.junit.jupiter.api.Assertions.assertSame;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -31,6 +33,27 @@ import org.junit.jupiter.api.Test;
 
 class DebugLevelUtilTest {
 
+  /**
+   * "Clear custom logging" is offered on every transform and action, also on 
the ones in a pipeline
+   * or workflow that never had any custom logging - and those have no debug 
attributes group at
+   * all.
+   */
+  @Test
+  void clearingDebugLevelWithoutADebugGroupDoesNothing() {
+    assertDoesNotThrow(() -> DebugLevelUtil.clearDebugLevel(null, 
"MyTransform"));
+  }
+
+  @Test
+  void theDebugGroupIsCreatedOnDemand() {
+    Map<String, Map<String, String>> attributesMap = new HashMap<>();
+
+    Map<String, String> group = 
DebugLevelUtil.getOrCreateDebugGroup(attributesMap);
+    group.put("key", "value");
+
+    assertSame(group, DebugLevelUtil.getOrCreateDebugGroup(attributesMap));
+    assertEquals("value", attributesMap.get(Defaults.DEBUG_GROUP).get("key"));
+  }
+
   @Test
   void storeAndLoadTransformDebugLevelWithCode() throws Exception {
     Map<String, String> map = new HashMap<>();
diff --git 
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequence.java
 
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequence.java
index 701c1959b2..55a6c3fd49 100644
--- 
a/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequence.java
+++ 
b/plugins/transforms/addsequence/src/main/java/org/apache/hop/pipeline/transforms/addsequence/AddSequence.java
@@ -116,14 +116,18 @@ public class AddSequence extends 
BaseTransform<AddSequenceMeta, AddSequenceData>
     }
 
     try {
-      putRow(data.outputRowMeta, addSequence(getInputRowMeta(), r));
+      // The row we wrote is the row with the sequence value in it, so render 
it with the output
+      // metadata: the incoming metadata doesn't know about the value we just 
added.
+      //
+      Object[] outputRowData = addSequence(getInputRowMeta(), r);
+      putRow(data.outputRowMeta, outputRowData);
 
       if (isRowLevel()) {
         logRowlevel(
             BaseMessages.getString(PKG, "AddSequence.Log.WriteRow")
                 + getLinesWritten()
                 + " : "
-                + getInputRowMeta().getString(r));
+                + data.outputRowMeta.getString(outputRowData));
       }
       if (checkFeedback(getLinesRead()) && isBasic()) {
         logBasic(BaseMessages.getString(PKG, "AddSequence.Log.LineNumber") + 
getLinesRead());
diff --git 
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/Calculator.java
 
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/Calculator.java
index 34f94e5fcc..e3658443a1 100644
--- 
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/Calculator.java
+++ 
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/Calculator.java
@@ -174,7 +174,8 @@ public class Calculator extends 
BaseTransform<CalculatorMeta, CalculatorData> {
       putRow(data.getOutputRowMeta(), row); // copy row to possible alternate 
rowset(s).
 
       if (isRowLevel()) {
-        logRowlevel("Wrote row #" + getLinesWritten() + " : " + 
getInputRowMeta().getString(r));
+        logRowlevel(
+            "Wrote row #" + getLinesWritten() + " : " + 
data.getOutputRowMeta().getString(row));
       }
       if (checkFeedback(getLinesRead()) && isBasic()) {
         logBasic(BaseMessages.getString(PKG, "Calculator.Log.Linenr", "" + 
getLinesRead()));
diff --git 
a/plugins/transforms/columnexists/src/main/java/org/apache/hop/pipeline/transforms/columnexists/ColumnExists.java
 
b/plugins/transforms/columnexists/src/main/java/org/apache/hop/pipeline/transforms/columnexists/ColumnExists.java
index 50240da44f..0f8bff667b 100644
--- 
a/plugins/transforms/columnexists/src/main/java/org/apache/hop/pipeline/transforms/columnexists/ColumnExists.java
+++ 
b/plugins/transforms/columnexists/src/main/java/org/apache/hop/pipeline/transforms/columnexists/ColumnExists.java
@@ -146,7 +146,7 @@ public class ColumnExists extends 
BaseTransform<ColumnExistsMeta, ColumnExistsDa
             BaseMessages.getString(
                 PKG,
                 "ColumnExists.LineNumber",
-                getLinesRead() + " : " + getInputRowMeta().getString(r)));
+                getLinesRead() + " : " + 
data.outputRowMeta.getString(outputRowData)));
       }
     } catch (HopException e) {
       if (getTransformMeta().isDoingErrorHandling()) {
diff --git 
a/plugins/transforms/constant/src/main/java/org/apache/hop/pipeline/transforms/constant/Constant.java
 
b/plugins/transforms/constant/src/main/java/org/apache/hop/pipeline/transforms/constant/Constant.java
index ed56864a4e..56cfc743d4 100644
--- 
a/plugins/transforms/constant/src/main/java/org/apache/hop/pipeline/transforms/constant/Constant.java
+++ 
b/plugins/transforms/constant/src/main/java/org/apache/hop/pipeline/transforms/constant/Constant.java
@@ -312,7 +312,7 @@ public class Constant extends BaseTransform<ConstantMeta, 
ConstantData> {
               PKG,
               "Constant.Log.Wrote.Row",
               Long.toString(getLinesWritten()),
-              getInputRowMeta().getString(r)));
+              data.outputMeta.getString(r)));
     }
 
     if (checkFeedback(getLinesWritten()) && isBasic()) {
diff --git 
a/plugins/transforms/constant/src/test/java/org/apache/hop/pipeline/transforms/constant/ConstantTest.java
 
b/plugins/transforms/constant/src/test/java/org/apache/hop/pipeline/transforms/constant/ConstantTest.java
index 231bdba821..66e3a713ff 100644
--- 
a/plugins/transforms/constant/src/test/java/org/apache/hop/pipeline/transforms/constant/ConstantTest.java
+++ 
b/plugins/transforms/constant/src/test/java/org/apache/hop/pipeline/transforms/constant/ConstantTest.java
@@ -120,6 +120,40 @@ class ConstantTest {
     assertFalse(success);
   }
 
+  /**
+   * The row that is logged at row level is the row this transform wrote, so 
it has to be rendered
+   * with the output row metadata. Using the incoming metadata silently 
dropped the constants that
+   * were just added (issue #2621).
+   */
+  @Test
+  void testRowLevelLoggingShowsTheAddedConstants() throws Exception {
+    IRowMeta inputRowMeta = new RowMeta();
+    inputRowMeta.addValueMeta(new ValueMetaString("field_01"));
+
+    doReturn(new Object[] {"START"}).when(constantSpy).getRow();
+    doReturn(inputRowMeta).when(constantSpy).getInputRowMeta();
+    doReturn(new Object[] {"ALL"}).when(rowMetaAndData).getData();
+    mockHelper.iTransformData.firstRow = true;
+    // The transform meta is a mock, so play the part of getFields() and add 
the constant field.
+    Mockito.doAnswer(
+            invocation -> {
+              IRowMeta outputRowMeta = invocation.getArgument(0);
+              outputRowMeta.addValueMeta(new ValueMetaString("constant_01"));
+              return null;
+            })
+        .when(mockHelper.iTransformMeta)
+        .getFields(any(), any(), any(), any(), any(), any());
+    when(mockHelper.iLogChannel.isRowLevel()).thenReturn(true);
+
+    assertTrue(constantSpy.processRow());
+
+    ArgumentCaptor<String> logged = ArgumentCaptor.forClass(String.class);
+    Mockito.verify(mockHelper.iLogChannel).logRowlevel(logged.capture());
+    assertTrue(
+        logged.getValue().contains("START") && 
logged.getValue().contains("ALL"),
+        "the logged row should contain the added constant, but was: " + 
logged.getValue());
+  }
+
   /**
    * The dialog offers every registered value type, so buildRow() has to cope 
with types it has no
    * dedicated case for. Types the value meta plugin can build from a string 
have to work.
diff --git 
a/plugins/transforms/creditcardvalidator/src/main/java/org/apache/hop/pipeline/transforms/creditcardvalidator/CreditCardValidator.java
 
b/plugins/transforms/creditcardvalidator/src/main/java/org/apache/hop/pipeline/transforms/creditcardvalidator/CreditCardValidator.java
index 801d28014e..9526c33422 100644
--- 
a/plugins/transforms/creditcardvalidator/src/main/java/org/apache/hop/pipeline/transforms/creditcardvalidator/CreditCardValidator.java
+++ 
b/plugins/transforms/creditcardvalidator/src/main/java/org/apache/hop/pipeline/transforms/creditcardvalidator/CreditCardValidator.java
@@ -136,7 +136,7 @@ public class CreditCardValidator
             BaseMessages.getString(
                 PKG,
                 "CreditCardValidator.LineNumber",
-                getLinesRead() + " : " + getInputRowMeta().getString(row)));
+                getLinesRead() + " : " + 
data.outputRowMeta.getString(outputRow)));
       }
 
     } catch (Exception e) {
diff --git 
a/plugins/transforms/databaselookup/src/main/java/org/apache/hop/pipeline/transforms/databaselookup/DatabaseLookup.java
 
b/plugins/transforms/databaselookup/src/main/java/org/apache/hop/pipeline/transforms/databaselookup/DatabaseLookup.java
index 7ff1682500..80d1b0044e 100644
--- 
a/plugins/transforms/databaselookup/src/main/java/org/apache/hop/pipeline/transforms/databaselookup/DatabaseLookup.java
+++ 
b/plugins/transforms/databaselookup/src/main/java/org/apache/hop/pipeline/transforms/databaselookup/DatabaseLookup.java
@@ -457,7 +457,7 @@ public class DatabaseLookup extends 
BaseTransform<DatabaseLookupMeta, DatabaseLo
         if (isRowLevel()) {
           logRowlevel(
               BaseMessages.getString(PKG, 
"DatabaseLookup.Log.WroteRowToNextTransform")
-                  + getInputRowMeta().getString(r));
+                  + data.outputRowMeta.getString(outputRow));
         }
         if (checkFeedback(getLinesRead()) && isBasic()) {
           logBasic("linenr " + getLinesRead());
diff --git 
a/plugins/transforms/execprocess/src/main/java/org/apache/hop/pipeline/transforms/execprocess/ExecProcess.java
 
b/plugins/transforms/execprocess/src/main/java/org/apache/hop/pipeline/transforms/execprocess/ExecProcess.java
index 922ba64803..05dfd5b292 100644
--- 
a/plugins/transforms/execprocess/src/main/java/org/apache/hop/pipeline/transforms/execprocess/ExecProcess.java
+++ 
b/plugins/transforms/execprocess/src/main/java/org/apache/hop/pipeline/transforms/execprocess/ExecProcess.java
@@ -127,7 +127,7 @@ public class ExecProcess extends 
BaseTransform<ExecProcessMeta, ExecProcessData>
             BaseMessages.getString(
                 PKG,
                 "ExecProcess.LineNumber",
-                getLinesRead() + " : " + getInputRowMeta().getString(r)));
+                getLinesRead() + " : " + 
data.outputRowMeta.getString(outputRow)));
       }
     } catch (HopException e) {
       if (getTransformMeta().isDoingErrorHandling()) {
diff --git 
a/plugins/transforms/fieldschangesequence/src/main/java/org/apache/hop/pipeline/transforms/fieldschangesequence/FieldsChangeSequence.java
 
b/plugins/transforms/fieldschangesequence/src/main/java/org/apache/hop/pipeline/transforms/fieldschangesequence/FieldsChangeSequence.java
index 8c27585aae..a7ab6ff78c 100644
--- 
a/plugins/transforms/fieldschangesequence/src/main/java/org/apache/hop/pipeline/transforms/fieldschangesequence/FieldsChangeSequence.java
+++ 
b/plugins/transforms/fieldschangesequence/src/main/java/org/apache/hop/pipeline/transforms/fieldschangesequence/FieldsChangeSequence.java
@@ -131,7 +131,7 @@ public class FieldsChangeSequence
             BaseMessages.getString(PKG, "FieldsChangeSequence.Log.WriteRow")
                 + getLinesWritten()
                 + " : "
-                + getInputRowMeta().getString(r));
+                + data.outputRowMeta.getString(outputRowData));
       }
 
       if (checkFeedback(getLinesRead()) && isBasic()) {
diff --git 
a/plugins/transforms/fileexists/src/main/java/org/apache/hop/pipeline/transforms/fileexists/FileExists.java
 
b/plugins/transforms/fileexists/src/main/java/org/apache/hop/pipeline/transforms/fileexists/FileExists.java
index ea94998d7e..6db30dc618 100644
--- 
a/plugins/transforms/fileexists/src/main/java/org/apache/hop/pipeline/transforms/fileexists/FileExists.java
+++ 
b/plugins/transforms/fileexists/src/main/java/org/apache/hop/pipeline/transforms/fileexists/FileExists.java
@@ -147,7 +147,7 @@ public class FileExists extends 
BaseTransform<FileExistsMeta, FileExistsData> {
             BaseMessages.getString(
                 PKG,
                 "FileExists.LineNumber",
-                getLinesRead() + " : " + getInputRowMeta().getString(r)));
+                getLinesRead() + " : " + 
data.outputRowMeta.getString(outputRow)));
       }
     } catch (Exception e) {
       if (getTransformMeta().isDoingErrorHandling()) {
diff --git 
a/plugins/transforms/filelocked/src/main/java/org/apache/hop/pipeline/transforms/filelocked/FileLocked.java
 
b/plugins/transforms/filelocked/src/main/java/org/apache/hop/pipeline/transforms/filelocked/FileLocked.java
index ba72636d1a..2790a63437 100644
--- 
a/plugins/transforms/filelocked/src/main/java/org/apache/hop/pipeline/transforms/filelocked/FileLocked.java
+++ 
b/plugins/transforms/filelocked/src/main/java/org/apache/hop/pipeline/transforms/filelocked/FileLocked.java
@@ -115,16 +115,15 @@ public class FileLocked extends 
BaseTransform<FileLockedMeta, FileLockedData> {
       }
 
       // add file locked
-      putRow(
-          data.outputRowMeta,
-          RowDataUtil.addValueData(r, data.NrPrevFields, fileLocked)); // copy 
row to output
+      Object[] outputRow = RowDataUtil.addValueData(r, data.NrPrevFields, 
fileLocked);
+      putRow(data.outputRowMeta, outputRow); // copy row to output
 
       if (isRowLevel()) {
         logRowlevel(
             BaseMessages.getString(
                 PKG,
                 "FileLocked.LineNumber",
-                getLinesRead() + " : " + getInputRowMeta().getString(r)));
+                getLinesRead() + " : " + 
data.outputRowMeta.getString(outputRow)));
       }
     } catch (Exception e) {
       boolean sendToErrorRow = false;
diff --git 
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/Formula.java
 
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/Formula.java
index a92b3e8bca..938861aa25 100644
--- 
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/Formula.java
+++ 
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/Formula.java
@@ -21,7 +21,6 @@ import static 
org.apache.hop.pipeline.transforms.formula.util.FormulaFieldsExtra
 
 import java.io.IOException;
 import java.sql.Timestamp;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.stream.IntStream;
@@ -136,7 +135,7 @@ public class Formula extends BaseTransform<FormulaMeta, 
FormulaData> {
     int tempIndex = getInputRowMeta().size();
 
     if (isRowLevel()) {
-      logRowlevel("Read row #" + getLinesRead() + " : " + Arrays.toString(r));
+      logRowlevel("Read row #" + getLinesRead() + " : " + 
getInputRowMeta().getString(r));
     }
 
     Object[] outputRowData = RowDataUtil.resizeArray(r, 
data.outputRowMeta.size());
@@ -219,7 +218,8 @@ public class Formula extends BaseTransform<FormulaMeta, 
FormulaData> {
     putRow(data.outputRowMeta, outputRowData);
 
     if (isRowLevel()) {
-      logRowlevel("Wrote row #" + getLinesWritten() + " : " + 
Arrays.toString(r));
+      logRowlevel(
+          "Wrote row #" + getLinesWritten() + " : " + 
data.outputRowMeta.getString(outputRowData));
     }
     if (checkFeedback(getLinesRead()) && isBasic()) {
       logBasic("Linenr " + getLinesRead());
diff --git 
a/plugins/transforms/samplerows/src/main/java/org/apache/hop/pipeline/transforms/samplerows/SampleRows.java
 
b/plugins/transforms/samplerows/src/main/java/org/apache/hop/pipeline/transforms/samplerows/SampleRows.java
index c16d860550..537458388f 100644
--- 
a/plugins/transforms/samplerows/src/main/java/org/apache/hop/pipeline/transforms/samplerows/SampleRows.java
+++ 
b/plugins/transforms/samplerows/src/main/java/org/apache/hop/pipeline/transforms/samplerows/SampleRows.java
@@ -115,7 +115,7 @@ public class SampleRows extends 
BaseTransform<SampleRowsMeta, SampleRowsData> {
             BaseMessages.getString(
                 PKG,
                 "SampleRows.Log.LineNumber",
-                linesRead + " : " + getInputRowMeta().getString(r)));
+                linesRead + " : " + 
data.outputRowMeta.getString(data.outputRow)));
       }
     }
 
diff --git 
a/plugins/transforms/tableexists/src/main/java/org/apache/hop/pipeline/transforms/tableexists/TableExists.java
 
b/plugins/transforms/tableexists/src/main/java/org/apache/hop/pipeline/transforms/tableexists/TableExists.java
index 2448395107..3c02295d4d 100644
--- 
a/plugins/transforms/tableexists/src/main/java/org/apache/hop/pipeline/transforms/tableexists/TableExists.java
+++ 
b/plugins/transforms/tableexists/src/main/java/org/apache/hop/pipeline/transforms/tableexists/TableExists.java
@@ -103,7 +103,7 @@ public class TableExists extends 
BaseTransform<TableExistsMeta, TableExistsData>
             BaseMessages.getString(
                 PKG,
                 "TableExists.LineNumber",
-                getLinesRead() + " : " + getInputRowMeta().getString(r)));
+                getLinesRead() + " : " + 
data.outputRowMeta.getString(outputRowData)));
       }
     } catch (HopException e) {
       if (getTransformMeta().isDoingErrorHandling()) {
diff --git 
a/plugins/transforms/webserviceavailable/src/main/java/org/apache/hop/pipeline/transforms/webserviceavailable/WebServiceAvailable.java
 
b/plugins/transforms/webserviceavailable/src/main/java/org/apache/hop/pipeline/transforms/webserviceavailable/WebServiceAvailable.java
index a148558533..c1b4502a8d 100644
--- 
a/plugins/transforms/webserviceavailable/src/main/java/org/apache/hop/pipeline/transforms/webserviceavailable/WebServiceAvailable.java
+++ 
b/plugins/transforms/webserviceavailable/src/main/java/org/apache/hop/pipeline/transforms/webserviceavailable/WebServiceAvailable.java
@@ -151,15 +151,15 @@ public class WebServiceAvailable
 
       // addwebservice available to the row
       // copy row to output rowset(s)
-      putRow(
-          data.outputRowMeta, RowDataUtil.addValueData(r, data.nrPrevFields, 
webServiceAvailable));
+      Object[] outputRow = RowDataUtil.addValueData(r, data.nrPrevFields, 
webServiceAvailable);
+      putRow(data.outputRowMeta, outputRow);
 
       if (isRowLevel()) {
         logRowlevel(
             BaseMessages.getString(
                 PKG,
                 "FileExists.LineNumber",
-                getLinesRead() + " : " + getInputRowMeta().getString(r)));
+                getLinesRead() + " : " + 
data.outputRowMeta.getString(outputRow)));
       }
     } catch (Exception e) {
       boolean sendToErrorRow = false;

Reply via email to