You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/21 19:06:09 UTC

(camel) branch main updated: CAMEL-24718: observability must not report a CamelSqlQuery header the endpoint ignored

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new a6bc7c254143 CAMEL-24718: observability must not report a CamelSqlQuery header the endpoint ignored
a6bc7c254143 is described below

commit a6bc7c254143b83476bed5491c93cfac7c99c945
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Mon Sep 21 21:06:03 2026 +0200

    CAMEL-24718: observability must not report a CamelSqlQuery header the endpoint ignored
    
    CAMEL-24292 gated the CamelSqlQuery header behind allowQueryFromHeader,
    which is off by default, so SqlProducer ignores the header and runs the
    endpoint-configured query. Three observability sites still read the
    header unconditionally - the SqlSpanDecorator in camel-telemetry and
    camel-tracing, and SqlTraceDevConsole - and so attributed a statement
    that was never executed, with sender-controlled text, to the exchange.
    
    Each site now consults the endpoint's allowQueryFromHeader through the
    generated PropertyConfigurer (no camel-sql dependency, no per-exchange
    factory lookups) and falls back to the endpoint query. An endpoint that
    does not declare the option, such as jdbc, never honours the header.
    Tests cover both polarities in all three modules; the 4.23 upgrade
    guide notes the observability consequence. No backport: the option
    only exists from 4.23.0.
    
    Closes #26689
    
    Co-Authored-By: Claude Opus 5 <no...@anthropic.com>
---
 .../telemetry/decorators/SqlQueryHeaderHelper.java |  57 ++++++++
 .../telemetry/decorators/SqlSpanDecorator.java     |  12 +-
 .../telemetry/decorators/SqlSpanDecoratorTest.java |  83 +++++++++++-
 .../tracing/decorators/SqlQueryHeaderHelper.java   |  57 ++++++++
 .../camel/tracing/decorators/SqlSpanDecorator.java |  12 +-
 .../tracing/decorators/SqlSpanDecoratorTest.java   |  83 +++++++++++-
 .../camel/impl/console/SqlTraceDevConsole.java     |  39 +++++-
 .../console/SqlTraceDevConsoleQueryHeaderTest.java | 146 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   7 +
 9 files changed, 474 insertions(+), 22 deletions(-)

diff --git a/components/camel-telemetry/src/main/java/org/apache/camel/telemetry/decorators/SqlQueryHeaderHelper.java b/components/camel-telemetry/src/main/java/org/apache/camel/telemetry/decorators/SqlQueryHeaderHelper.java
new file mode 100644
index 000000000000..0e489a5f78ee
--- /dev/null
+++ b/components/camel-telemetry/src/main/java/org/apache/camel/telemetry/decorators/SqlQueryHeaderHelper.java
@@ -0,0 +1,57 @@
+/*
+ * 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.camel.telemetry.decorators;
+
+import org.apache.camel.Component;
+import org.apache.camel.Endpoint;
+import org.apache.camel.spi.PropertyConfigurer;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.support.DefaultEndpoint;
+
+/**
+ * Determines whether an endpoint honours the {@code CamelSqlQuery} header.
+ * <p/>
+ * The camel-sql components gate that header behind the {@code allowQueryFromHeader} option, which is disabled by
+ * default. When it is disabled the header is ignored and the endpoint-configured query is executed instead, so
+ * surfacing the header value would attribute a statement to the exchange that never ran, and would place
+ * sender-controlled text into telemetry.
+ * <p/>
+ * The option is read through the endpoint's generated {@link PropertyConfigurer} rather than by casting, because the
+ * tracing modules must not depend on camel-sql. An endpoint that does not declare the option at all (jdbc, for
+ * instance) never honours the header.
+ */
+final class SqlQueryHeaderHelper {
+
+    private static final String ALLOW_QUERY_FROM_HEADER = "allowQueryFromHeader";
+
+    private SqlQueryHeaderHelper() {
+    }
+
+    static boolean isQueryHeaderHonoured(Endpoint endpoint) {
+        if (!(endpoint instanceof DefaultEndpoint defaultEndpoint)) {
+            return false;
+        }
+        Component component = defaultEndpoint.getComponent();
+        if (component == null) {
+            return false;
+        }
+        if (component.getEndpointPropertyConfigurer() instanceof PropertyConfigurerGetter getter) {
+            return Boolean.TRUE.equals(getter.getOptionValue(endpoint, ALLOW_QUERY_FROM_HEADER, true));
+        }
+        return false;
+    }
+}
diff --git a/components/camel-telemetry/src/main/java/org/apache/camel/telemetry/decorators/SqlSpanDecorator.java b/components/camel-telemetry/src/main/java/org/apache/camel/telemetry/decorators/SqlSpanDecorator.java
index e50aaa477e06..945f8ec81730 100644
--- a/components/camel-telemetry/src/main/java/org/apache/camel/telemetry/decorators/SqlSpanDecorator.java
+++ b/components/camel-telemetry/src/main/java/org/apache/camel/telemetry/decorators/SqlSpanDecorator.java
@@ -21,6 +21,8 @@ import org.apache.camel.Exchange;
 import org.apache.camel.telemetry.Span;
 import org.apache.camel.telemetry.TagConstants;
 
+import static org.apache.camel.telemetry.decorators.SqlQueryHeaderHelper.isQueryHeaderHonoured;
+
 public class SqlSpanDecorator extends AbstractSpanDecorator {
 
     public static final String CAMEL_SQL_QUERY = "CamelSqlQuery";
@@ -40,9 +42,13 @@ public class SqlSpanDecorator extends AbstractSpanDecorator {
         super.beforeTracingEvent(span, exchange, endpoint);
         span.setTag(TagConstants.DB_SYSTEM, "sql");
 
-        String query = exchange.getIn().getHeader(CAMEL_SQL_QUERY, String.class);
-        if (query != null) {
-            span.setTag(TagConstants.DB_STATEMENT, query);
+        // the header only reaches the database when the endpoint opts in, so tagging it
+        // unconditionally would report a statement that was never executed
+        if (isQueryHeaderHonoured(endpoint)) {
+            String query = exchange.getIn().getHeader(CAMEL_SQL_QUERY, String.class);
+            if (query != null) {
+                span.setTag(TagConstants.DB_STATEMENT, query);
+            }
         }
     }
 
diff --git a/components/camel-telemetry/src/test/java/org/apache/camel/telemetry/decorators/SqlSpanDecoratorTest.java b/components/camel-telemetry/src/test/java/org/apache/camel/telemetry/decorators/SqlSpanDecoratorTest.java
index fae04200536c..bcc08664f91d 100644
--- a/components/camel-telemetry/src/test/java/org/apache/camel/telemetry/decorators/SqlSpanDecoratorTest.java
+++ b/components/camel-telemetry/src/test/java/org/apache/camel/telemetry/decorators/SqlSpanDecoratorTest.java
@@ -16,9 +16,14 @@
  */
 package org.apache.camel.telemetry.decorators;
 
+import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
 import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
+import org.apache.camel.spi.PropertyConfigurer;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.support.DefaultEndpoint;
 import org.apache.camel.telemetry.SpanDecorator;
 import org.apache.camel.telemetry.TagConstants;
 import org.apache.camel.telemetry.mock.MockSpanAdapter;
@@ -26,14 +31,49 @@ import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 
 public class SqlSpanDecoratorTest {
 
     private static final String SQL_STATEMENT = "select * from customer";
 
     @Test
-    public void testPre() {
-        Endpoint endpoint = Mockito.mock(Endpoint.class);
+    public void testPreTagsTheQueryHeaderWhenTheEndpointAllowsIt() {
+        MockSpanAdapter span = decorate(endpointAllowingQueryHeader(true));
+
+        assertEquals("sql", span.tags().get(TagConstants.DB_SYSTEM));
+        assertEquals(SQL_STATEMENT, span.tags().get(TagConstants.DB_STATEMENT));
+    }
+
+    @Test
+    public void testPreIgnoresTheQueryHeaderWhenTheEndpointDisallowsIt() {
+        MockSpanAdapter span = decorate(endpointAllowingQueryHeader(false));
+
+        // allowQueryFromHeader is disabled by default, so this statement was never executed and
+        // tagging it would put sender-controlled text into the span
+        assertEquals("sql", span.tags().get(TagConstants.DB_SYSTEM));
+        assertNull(span.tags().get(TagConstants.DB_STATEMENT));
+    }
+
+    @Test
+    public void testPreIgnoresTheQueryHeaderForAnEndpointWithoutTheOption() {
+        // e.g. jdbc, which takes its query from the message body
+        MockSpanAdapter span = decorate(Mockito.mock(Endpoint.class));
+
+        assertEquals("sql", span.tags().get(TagConstants.DB_SYSTEM));
+        assertNull(span.tags().get(TagConstants.DB_STATEMENT));
+    }
+
+    @Test
+    public void testQueryHeaderIsNotHonouredWhenTheComponentIsUnknown() {
+        DefaultEndpoint endpoint = Mockito.mock(DefaultEndpoint.class);
+        Mockito.when(endpoint.getComponent()).thenReturn(null);
+
+        assertFalse(SqlQueryHeaderHelper.isQueryHeaderHonoured(endpoint));
+    }
+
+    private static MockSpanAdapter decorate(Endpoint endpoint) {
         Exchange exchange = Mockito.mock(Exchange.class);
         Message message = Mockito.mock(Message.class);
 
@@ -42,13 +82,44 @@ public class SqlSpanDecoratorTest {
         Mockito.when(message.getHeader(SqlSpanDecorator.CAMEL_SQL_QUERY, String.class)).thenReturn(SQL_STATEMENT);
 
         SpanDecorator decorator = new SqlSpanDecorator();
-
         MockSpanAdapter span = new MockSpanAdapter();
-
         decorator.beforeTracingEvent(span, exchange, endpoint);
+        return span;
+    }
 
-        assertEquals("sql", span.tags().get(TagConstants.DB_SYSTEM));
-        assertEquals(SQL_STATEMENT, span.tags().get(TagConstants.DB_STATEMENT));
+    private static Endpoint endpointAllowingQueryHeader(boolean allow) {
+        Component component = Mockito.mock(Component.class);
+        Mockito.when(component.getEndpointPropertyConfigurer()).thenReturn(new AllowQueryFromHeaderConfigurer(allow));
+
+        DefaultEndpoint endpoint = Mockito.mock(DefaultEndpoint.class);
+        Mockito.when(endpoint.getComponent()).thenReturn(component);
+        return endpoint;
     }
 
+    /**
+     * Stands in for the generated {@code SqlEndpointConfigurer}, which camel-telemetry cannot depend on.
+     */
+    private static class AllowQueryFromHeaderConfigurer implements PropertyConfigurer, PropertyConfigurerGetter {
+
+        private final boolean allow;
+
+        AllowQueryFromHeaderConfigurer(boolean allow) {
+            this.allow = allow;
+        }
+
+        @Override
+        public boolean configure(CamelContext camelContext, Object target, String name, Object value, boolean ignoreCase) {
+            return false;
+        }
+
+        @Override
+        public Class<?> getOptionType(String name, boolean ignoreCase) {
+            return "allowQueryFromHeader".equals(name) ? boolean.class : null;
+        }
+
+        @Override
+        public Object getOptionValue(Object target, String name, boolean ignoreCase) {
+            return "allowQueryFromHeader".equals(name) ? allow : null;
+        }
+    }
 }
diff --git a/components/camel-tracing/src/main/java/org/apache/camel/tracing/decorators/SqlQueryHeaderHelper.java b/components/camel-tracing/src/main/java/org/apache/camel/tracing/decorators/SqlQueryHeaderHelper.java
new file mode 100644
index 000000000000..d3e21c0ebeec
--- /dev/null
+++ b/components/camel-tracing/src/main/java/org/apache/camel/tracing/decorators/SqlQueryHeaderHelper.java
@@ -0,0 +1,57 @@
+/*
+ * 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.camel.tracing.decorators;
+
+import org.apache.camel.Component;
+import org.apache.camel.Endpoint;
+import org.apache.camel.spi.PropertyConfigurer;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.support.DefaultEndpoint;
+
+/**
+ * Determines whether an endpoint honours the {@code CamelSqlQuery} header.
+ * <p/>
+ * The camel-sql components gate that header behind the {@code allowQueryFromHeader} option, which is disabled by
+ * default. When it is disabled the header is ignored and the endpoint-configured query is executed instead, so
+ * surfacing the header value would attribute a statement to the exchange that never ran, and would place
+ * sender-controlled text into telemetry.
+ * <p/>
+ * The option is read through the endpoint's generated {@link PropertyConfigurer} rather than by casting, because the
+ * tracing modules must not depend on camel-sql. An endpoint that does not declare the option at all (jdbc, for
+ * instance) never honours the header.
+ */
+final class SqlQueryHeaderHelper {
+
+    private static final String ALLOW_QUERY_FROM_HEADER = "allowQueryFromHeader";
+
+    private SqlQueryHeaderHelper() {
+    }
+
+    static boolean isQueryHeaderHonoured(Endpoint endpoint) {
+        if (!(endpoint instanceof DefaultEndpoint defaultEndpoint)) {
+            return false;
+        }
+        Component component = defaultEndpoint.getComponent();
+        if (component == null) {
+            return false;
+        }
+        if (component.getEndpointPropertyConfigurer() instanceof PropertyConfigurerGetter getter) {
+            return Boolean.TRUE.equals(getter.getOptionValue(endpoint, ALLOW_QUERY_FROM_HEADER, true));
+        }
+        return false;
+    }
+}
diff --git a/components/camel-tracing/src/main/java/org/apache/camel/tracing/decorators/SqlSpanDecorator.java b/components/camel-tracing/src/main/java/org/apache/camel/tracing/decorators/SqlSpanDecorator.java
index 8077de5305df..b6709c84d223 100644
--- a/components/camel-tracing/src/main/java/org/apache/camel/tracing/decorators/SqlSpanDecorator.java
+++ b/components/camel-tracing/src/main/java/org/apache/camel/tracing/decorators/SqlSpanDecorator.java
@@ -21,6 +21,8 @@ import org.apache.camel.Exchange;
 import org.apache.camel.tracing.SpanAdapter;
 import org.apache.camel.tracing.TagConstants;
 
+import static org.apache.camel.tracing.decorators.SqlQueryHeaderHelper.isQueryHeaderHonoured;
+
 @Deprecated(since = "4.19.0")
 public class SqlSpanDecorator extends AbstractSpanDecorator {
 
@@ -41,9 +43,13 @@ public class SqlSpanDecorator extends AbstractSpanDecorator {
         super.pre(span, exchange, endpoint);
         span.setTag(TagConstants.DB_SYSTEM, "sql");
 
-        String query = exchange.getIn().getHeader(CAMEL_SQL_QUERY, String.class);
-        if (query != null) {
-            span.setTag(TagConstants.DB_STATEMENT, query);
+        // the header only reaches the database when the endpoint opts in, so tagging it
+        // unconditionally would report a statement that was never executed
+        if (isQueryHeaderHonoured(endpoint)) {
+            String query = exchange.getIn().getHeader(CAMEL_SQL_QUERY, String.class);
+            if (query != null) {
+                span.setTag(TagConstants.DB_STATEMENT, query);
+            }
         }
     }
 
diff --git a/components/camel-tracing/src/test/java/org/apache/camel/tracing/decorators/SqlSpanDecoratorTest.java b/components/camel-tracing/src/test/java/org/apache/camel/tracing/decorators/SqlSpanDecoratorTest.java
index 3df32dee7d67..f06b6780d2a2 100644
--- a/components/camel-tracing/src/test/java/org/apache/camel/tracing/decorators/SqlSpanDecoratorTest.java
+++ b/components/camel-tracing/src/test/java/org/apache/camel/tracing/decorators/SqlSpanDecoratorTest.java
@@ -16,9 +16,14 @@
  */
 package org.apache.camel.tracing.decorators;
 
+import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
 import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
+import org.apache.camel.spi.PropertyConfigurer;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.support.DefaultEndpoint;
 import org.apache.camel.tracing.MockSpanAdapter;
 import org.apache.camel.tracing.SpanDecorator;
 import org.apache.camel.tracing.TagConstants;
@@ -26,6 +31,8 @@ import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 
 @Deprecated(since = "4.19.0")
 public class SqlSpanDecoratorTest {
@@ -33,8 +40,41 @@ public class SqlSpanDecoratorTest {
     private static final String SQL_STATEMENT = "select * from customer";
 
     @Test
-    public void testPre() {
-        Endpoint endpoint = Mockito.mock(Endpoint.class);
+    public void testPreTagsTheQueryHeaderWhenTheEndpointAllowsIt() {
+        MockSpanAdapter span = decorate(endpointAllowingQueryHeader(true));
+
+        assertEquals("sql", span.tags().get(TagConstants.DB_SYSTEM));
+        assertEquals(SQL_STATEMENT, span.tags().get(TagConstants.DB_STATEMENT));
+    }
+
+    @Test
+    public void testPreIgnoresTheQueryHeaderWhenTheEndpointDisallowsIt() {
+        MockSpanAdapter span = decorate(endpointAllowingQueryHeader(false));
+
+        // allowQueryFromHeader is disabled by default, so this statement was never executed and
+        // tagging it would put sender-controlled text into the span
+        assertEquals("sql", span.tags().get(TagConstants.DB_SYSTEM));
+        assertNull(span.tags().get(TagConstants.DB_STATEMENT));
+    }
+
+    @Test
+    public void testPreIgnoresTheQueryHeaderForAnEndpointWithoutTheOption() {
+        // e.g. jdbc, which takes its query from the message body
+        MockSpanAdapter span = decorate(Mockito.mock(Endpoint.class));
+
+        assertEquals("sql", span.tags().get(TagConstants.DB_SYSTEM));
+        assertNull(span.tags().get(TagConstants.DB_STATEMENT));
+    }
+
+    @Test
+    public void testQueryHeaderIsNotHonouredWhenTheComponentIsUnknown() {
+        DefaultEndpoint endpoint = Mockito.mock(DefaultEndpoint.class);
+        Mockito.when(endpoint.getComponent()).thenReturn(null);
+
+        assertFalse(SqlQueryHeaderHelper.isQueryHeaderHonoured(endpoint));
+    }
+
+    private static MockSpanAdapter decorate(Endpoint endpoint) {
         Exchange exchange = Mockito.mock(Exchange.class);
         Message message = Mockito.mock(Message.class);
 
@@ -43,13 +83,44 @@ public class SqlSpanDecoratorTest {
         Mockito.when(message.getHeader(SqlSpanDecorator.CAMEL_SQL_QUERY, String.class)).thenReturn(SQL_STATEMENT);
 
         SpanDecorator decorator = new SqlSpanDecorator();
-
         MockSpanAdapter span = new MockSpanAdapter();
-
         decorator.pre(span, exchange, endpoint);
+        return span;
+    }
 
-        assertEquals("sql", span.tags().get(TagConstants.DB_SYSTEM));
-        assertEquals(SQL_STATEMENT, span.tags().get(TagConstants.DB_STATEMENT));
+    private static Endpoint endpointAllowingQueryHeader(boolean allow) {
+        Component component = Mockito.mock(Component.class);
+        Mockito.when(component.getEndpointPropertyConfigurer()).thenReturn(new AllowQueryFromHeaderConfigurer(allow));
+
+        DefaultEndpoint endpoint = Mockito.mock(DefaultEndpoint.class);
+        Mockito.when(endpoint.getComponent()).thenReturn(component);
+        return endpoint;
     }
 
+    /**
+     * Stands in for the generated {@code SqlEndpointConfigurer}, which camel-tracing cannot depend on.
+     */
+    private static class AllowQueryFromHeaderConfigurer implements PropertyConfigurer, PropertyConfigurerGetter {
+
+        private final boolean allow;
+
+        AllowQueryFromHeaderConfigurer(boolean allow) {
+            this.allow = allow;
+        }
+
+        @Override
+        public boolean configure(CamelContext camelContext, Object target, String name, Object value, boolean ignoreCase) {
+            return false;
+        }
+
+        @Override
+        public Class<?> getOptionType(String name, boolean ignoreCase) {
+            return "allowQueryFromHeader".equals(name) ? boolean.class : null;
+        }
+
+        @Override
+        public Object getOptionValue(Object target, String name, boolean ignoreCase) {
+            return "allowQueryFromHeader".equals(name) ? allow : null;
+        }
+    }
 }
diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java
index c99504cf7aff..ca8a4578f10a 100644
--- a/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java
+++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlTraceDevConsole.java
@@ -25,12 +25,16 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import org.apache.camel.Component;
+import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
 import org.apache.camel.NonManagedService;
 import org.apache.camel.spi.CamelEvent;
 import org.apache.camel.spi.Configurer;
 import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.PropertyConfigurerGetter;
 import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.DefaultEndpoint;
 import org.apache.camel.support.EventNotifierSupport;
 import org.apache.camel.support.ResourceHelper;
 import org.apache.camel.support.console.AbstractDevConsole;
@@ -234,6 +238,29 @@ public class SqlTraceDevConsole extends AbstractDevConsole {
         return "resource:" + uri;
     }
 
+    /**
+     * Whether the endpoint honours the {@code CamelSqlQuery} header.
+     * <p/>
+     * camel-sql gates that header behind the {@code allowQueryFromHeader} option, disabled by default; when it is
+     * disabled the endpoint-configured query runs instead, so reporting the header would show a statement that never
+     * executed. The option is read through the generated property configurer rather than by casting, because
+     * camel-console must not depend on camel-sql. Endpoints that do not declare the option at all - jdbc, which takes
+     * its query from the body - never honour the header.
+     */
+    private static boolean isQueryHeaderHonoured(Endpoint endpoint) {
+        if (!(endpoint instanceof DefaultEndpoint defaultEndpoint)) {
+            return false;
+        }
+        Component component = defaultEndpoint.getComponent();
+        if (component == null) {
+            return false;
+        }
+        if (component.getEndpointPropertyConfigurer() instanceof PropertyConfigurerGetter getter) {
+            return Boolean.TRUE.equals(getter.getOptionValue(endpoint, "allowQueryFromHeader", true));
+        }
+        return false;
+    }
+
     private static String detectCategory(String query) {
         if (query != null && !query.isEmpty()) {
             String upper = query.stripLeading().toUpperCase(Locale.ENGLISH);
@@ -273,11 +300,15 @@ public class SqlTraceDevConsole extends AbstractDevConsole {
                 if (uri.startsWith("sql:") || uri.startsWith("jdbc:")) {
                     Exchange exchange = ese.getExchange();
 
-                    // prefer the CamelSqlQuery header (runtime override) over the URI
+                    // prefer the CamelSqlQuery header (runtime override) over the URI, but only when the
+                    // endpoint actually honours it - otherwise the console would report a statement that
+                    // was never executed
                     String query = null;
-                    Object headerQuery = exchange.getMessage().getHeader("CamelSqlQuery");
-                    if (headerQuery != null) {
-                        query = headerQuery.toString();
+                    if (isQueryHeaderHonoured(ese.getEndpoint())) {
+                        Object headerQuery = exchange.getMessage().getHeader("CamelSqlQuery");
+                        if (headerQuery != null) {
+                            query = headerQuery.toString();
+                        }
                     }
                     if (query == null) {
                         query = extractQuery(uri);
diff --git a/core/camel-console/src/test/java/org/apache/camel/impl/console/SqlTraceDevConsoleQueryHeaderTest.java b/core/camel-console/src/test/java/org/apache/camel/impl/console/SqlTraceDevConsoleQueryHeaderTest.java
new file mode 100644
index 000000000000..f26d3a703d6d
--- /dev/null
+++ b/core/camel-console/src/test/java/org/apache/camel/impl/console/SqlTraceDevConsoleQueryHeaderTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.camel.impl.console;
+
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.spi.PropertyConfigurer;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The {@code CamelSqlQuery} header only changes the executed statement when the endpoint sets
+ * {@code allowQueryFromHeader=true}, which is off by default. The console must not report the header as the executed
+ * query when the endpoint ignored it - the statement never ran, and the header is sender-controlled.
+ *
+ * @see <a href="https://issues.apache.org/jira/browse/CAMEL-24718">CAMEL-24718</a>
+ */
+public class SqlTraceDevConsoleQueryHeaderTest extends ContextTestSupport {
+
+    private static final String ENDPOINT_QUERY = "select * from projects";
+    private static final String HEADER_QUERY = "select * from secrets";
+
+    @Test
+    public void testHeaderQueryIsIgnoredWhenTheEndpointDisallowsIt() throws Exception {
+        Assertions.assertEquals(ENDPOINT_QUERY, tracedQuery(false));
+    }
+
+    @Test
+    public void testHeaderQueryIsReportedWhenTheEndpointAllowsIt() throws Exception {
+        Assertions.assertEquals(HEADER_QUERY, tracedQuery(true));
+    }
+
+    private String tracedQuery(boolean allowQueryFromHeader) throws Exception {
+        context.addComponent("sql", new FakeSqlComponent(allowQueryFromHeader));
+
+        DevConsole con = PluginHelper.getDevConsoleResolver(context).resolveDevConsole("sql-trace");
+        Assertions.assertNotNull(con);
+        ServiceHelper.startService(con);
+
+        template.sendBodyAndHeader("sql:" + ENDPOINT_QUERY, "body", "CamelSqlQuery", HEADER_QUERY);
+
+        JsonObject out = (JsonObject) con.call(DevConsole.MediaType.JSON);
+        JsonArray statements = (JsonArray) out.get("statements");
+        Assertions.assertNotNull(statements, "the console should have traced the sql: send");
+        Assertions.assertEquals(1, statements.size());
+        return (String) ((JsonObject) statements.get(0)).get("query");
+    }
+
+    /**
+     * Stands in for camel-sql, which camel-console must not depend on.
+     */
+    private static final class FakeSqlComponent extends DefaultComponent {
+
+        private final PropertyConfigurer configurer;
+
+        private FakeSqlComponent(boolean allowQueryFromHeader) {
+            this.configurer = new AllowQueryFromHeaderConfigurer(allowQueryFromHeader);
+        }
+
+        @Override
+        public PropertyConfigurer getEndpointPropertyConfigurer() {
+            return configurer;
+        }
+
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) {
+            return new FakeSqlEndpoint(uri, this);
+        }
+    }
+
+    private static final class FakeSqlEndpoint extends DefaultEndpoint {
+
+        private FakeSqlEndpoint(String uri, DefaultComponent component) {
+            super(uri, component);
+        }
+
+        @Override
+        public Producer createProducer() {
+            return new DefaultProducer(this) {
+                @Override
+                public void process(Exchange exchange) {
+                    // the statement is not actually executed; only the traced metadata matters here
+                }
+            };
+        }
+
+        @Override
+        public Consumer createConsumer(Processor processor) {
+            throw new UnsupportedOperationException("Consumer not supported");
+        }
+    }
+
+    private static final class AllowQueryFromHeaderConfigurer implements PropertyConfigurer, PropertyConfigurerGetter {
+
+        private final boolean allow;
+
+        private AllowQueryFromHeaderConfigurer(boolean allow) {
+            this.allow = allow;
+        }
+
+        @Override
+        public boolean configure(CamelContext camelContext, Object target, String name, Object value, boolean ignoreCase) {
+            return false;
+        }
+
+        @Override
+        public Class<?> getOptionType(String name, boolean ignoreCase) {
+            return "allowQueryFromHeader".equals(name) ? boolean.class : null;
+        }
+
+        @Override
+        public Object getOptionValue(Object target, String name, boolean ignoreCase) {
+            return "allowQueryFromHeader".equals(name) ? allow : null;
+        }
+    }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 1a642e457dce..aa72b55e2601 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -2340,6 +2340,13 @@ stored-procedure template text and no longer resolves it through `SqlHelper.reso
 as a resource, consistent with how `camel-sql` already treats the body under `useMessageBodyForSql=true`. A route
 that relied on the body being a resource location must resolve it to the template text before the `sql-stored`
 endpoint.
+
+Observability follows the gate. The `db.statement` span tag set by the `sql` span decorator (camel-telemetry and
+the deprecated camel-tracing) is now taken from the `CamelSqlQuery` header only when the endpoint sets
+`allowQueryFromHeader=true`; otherwise the tag is omitted rather than reporting a statement that was never
+executed. A route that reads `db.statement` from a `sql:` span and relies on the header value must set
+`allowQueryFromHeader=true`.
+
 === camel-core - the inheritErrorHandler attribute on circuitBreaker and failoverLoadBalancer is now a String
 
 `CircuitBreakerDefinition.inheritErrorHandler` and `FailoverLoadBalancerDefinition.inheritErrorHandler` are now