jdaugherty commented on code in PR #15666:
URL: https://github.com/apache/grails-core/pull/15666#discussion_r3610658937


##########
grails-web-mvc/src/main/groovy/org/grails/web/errors/GrailsExceptionResolver.java:
##########
@@ -131,6 +132,7 @@ public void setServletContext(ServletContext 
servletContext) {
     public void setGrailsApplication(GrailsApplication grailsApplication) {
         this.grailsApplication = grailsApplication;
         createStackFilterer();
+        GrailsUtil.initializeStackFilterer(grailsApplication);

Review Comment:
   This still couples `grails-core` static state to a `grails-web-mvc` bean: 
any context where this resolver isn't wired (a non-web app, or an app that 
replaces the `exceptionHandler` bean with a different resolver) silently stays 
on the fallback with no warning.
   
   Isn't this what `GrailsBootstrapRegistryInitializer` is meant to solve? Its 
close listener fires at the end of `prepareContext()` — environment fully 
bound, before `refresh()` — so `grails-core` could initialize its own utility 
there for every app type, and the configured filterer would be active before 
any bean instantiates (startup failures would honour the config too). It could 
also promote the filterer as a singleton (the way `PluginDiscovery.BEAN_NAME` 
is promoted) so this resolver consumes the same instance instead of 
instantiating a second copy of a possibly custom filterer class.



##########
grails-doc/src/en/guide/conf/config/logging/loggingFullStackTraces.adoc:
##########
@@ -97,6 +97,13 @@ log record. It means non-resolver code paths (for example, a 
scheduled job that
 `GrailsUtil.sanitizeRootCause(ex)` before logging via its own logger) continue 
to populate the `StackTrace`
 appender without an explicit emission call.
 
+NOTE: `GrailsUtil` honours the same config keys as the exception resolver

Review Comment:
   This NOTE bakes internal wiring into the user guide — 
`GrailsExceptionResolver` and `GroovyPageView` are both `org.grails.*` internal 
packages, and we shouldn't reference internal APIs in user-facing docs. Can we 
state the behaviour only, e.g. "`GrailsUtil` honours the same config keys once 
the application has started, so this property controls both resolver-driven and 
`GrailsUtil`-driven emission (including GSP view-rendering errors)"? Then the 
docs also don't need to change when the wiring does.



##########
grails-core/src/test/groovy/grails/util/GrailsUtilStackFiltererSpec.groovy:
##########
@@ -0,0 +1,172 @@
+/*
+ *  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
+ *
+ *    https://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 grails.util
+
+import grails.config.Config
+import grails.core.GrailsApplication
+import org.grails.exceptions.reporting.DefaultStackTraceFilterer
+import org.grails.exceptions.reporting.StackTraceFilterer
+import spock.lang.Specification
+
+import java.lang.reflect.Field
+
+/**
+ * Verifies that {@link GrailsUtil#initializeStackFilterer} resolves the 
configured filterer class
+ * from the application's config and propagates {@code 
grails.exceptionresolver.logFullStackTraceOnFilter}
+ * to {@link DefaultStackTraceFilterer} instances. Before initialization the 
FALLBACK_FILTERER
+ * (a {@link DefaultStackTraceFilterer} singleton) is used so CLI/test/main 
paths work unchanged.
+ */
+class GrailsUtilStackFiltererSpec extends Specification {
+
+    StackTraceFilterer previous
+
+    def setup() {
+        previous = currentFilterer()
+        setFilterer(fallbackFilterer())
+    }
+
+    def cleanup() {
+        setFilterer(previous)
+    }
+
+    def 'deepSanitize uses the fallback filterer before 
initializeStackFilterer is called'() {
+        when:
+        GrailsUtil.deepSanitize(new RuntimeException('boom'))
+
+        then:
+        noExceptionThrown()
+        currentFilterer().is(fallbackFilterer())
+    }
+
+    def 'initializeStackFilterer is a no-op when application is null'() {
+        when:
+        GrailsUtil.initializeStackFilterer(null)
+
+        then:
+        currentFilterer().is(fallbackFilterer())
+    }
+
+    def 'initializeStackFilterer wires the class declared by 
grails.logging.stackTraceFiltererClass'() {
+        given:
+        def application = Mock(GrailsApplication)
+        def config = Mock(Config)
+        config.getProperty('grails.logging.stackTraceFiltererClass', Class, 
DefaultStackTraceFilterer) >> RecordingStackTraceFilterer
+        
config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', 
Boolean, true) >> true
+        application.getConfig() >> config
+
+        when:
+        GrailsUtil.initializeStackFilterer(application)
+        GrailsUtil.deepSanitize(new RuntimeException('boom'))
+
+        then:
+        currentFilterer() instanceof RecordingStackTraceFilterer
+        RecordingStackTraceFilterer.lastInstance.recursiveCalls == 1
+    }
+
+    def 'initializeStackFilterer propagates logFullStackTraceOnFilter to 
DefaultStackTraceFilterer instances'() {
+        given:
+        def application = Mock(GrailsApplication)
+        def config = Mock(Config)
+        config.getProperty('grails.logging.stackTraceFiltererClass', Class, 
DefaultStackTraceFilterer) >> DefaultStackTraceFilterer
+        
config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', 
Boolean, true) >> false
+        application.getConfig() >> config
+
+        and: 'captured StackTrace logger output'
+        def originalErr = System.err
+        def baos = new ByteArrayOutputStream()
+        System.setErr(new PrintStream(baos, true))
+
+        when:
+        GrailsUtil.initializeStackFilterer(application)
+        GrailsUtil.deepSanitize(new RuntimeException('boom'))
+
+        then:
+        System.err.flush()
+        !baos.toString().contains('ERROR StackTrace')

Review Comment:
   `StackTraceFiltererSpec` asserts on the `'Full Stack Trace:'` marker; 
`'ERROR StackTrace'` depends on the logging pattern, so this negative assertion 
can pass vacuously even when emission occurs. Please use the same marker, and 
consider adding the positive control (flag `true` + initialized application → 
emission does happen through `GrailsUtil.deepSanitize`) so the negative case is 
proven meaningful.



##########
grails-core/src/test/groovy/grails/util/GrailsUtilStackFiltererSpec.groovy:
##########
@@ -0,0 +1,172 @@
+/*
+ *  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
+ *
+ *    https://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 grails.util
+
+import grails.config.Config
+import grails.core.GrailsApplication
+import org.grails.exceptions.reporting.DefaultStackTraceFilterer
+import org.grails.exceptions.reporting.StackTraceFilterer
+import spock.lang.Specification
+
+import java.lang.reflect.Field
+
+/**
+ * Verifies that {@link GrailsUtil#initializeStackFilterer} resolves the 
configured filterer class
+ * from the application's config and propagates {@code 
grails.exceptionresolver.logFullStackTraceOnFilter}
+ * to {@link DefaultStackTraceFilterer} instances. Before initialization the 
FALLBACK_FILTERER
+ * (a {@link DefaultStackTraceFilterer} singleton) is used so CLI/test/main 
paths work unchanged.
+ */
+class GrailsUtilStackFiltererSpec extends Specification {
+
+    StackTraceFilterer previous
+
+    def setup() {
+        previous = currentFilterer()
+        setFilterer(fallbackFilterer())
+    }
+
+    def cleanup() {
+        setFilterer(previous)
+    }
+
+    def 'deepSanitize uses the fallback filterer before 
initializeStackFilterer is called'() {

Review Comment:
   `setup()` installs `FALLBACK_FILTERER` via reflection, so this feature 
asserts the state the test itself just created rather than the class's initial 
state. As written it can't fail. Either drop it or assert only the behaviour 
(`deepSanitize` succeeds with no application wired).



##########
grails-doc/src/en/guide/upgrading/upgrading71x.adoc:
##########
@@ -849,3 +849,14 @@ Set to `false` to disable the side-effect emission and 
rely solely on `logFullSt
 output. The two flags interact — if both are enabled, a request exception with 
N causes produces N+1 `StackTrace`
 records (one resolver-driven plus one per throwable visited by the recursive 
filter walk). The Logging Full
 Stack Traces section of the user guide includes a matrix of behaviours for the 
four flag combinations.
+
+`GrailsUtil` honours both `grails.logging.stackTraceFiltererClass` and

Review Comment:
   Same doc concern as the user-guide NOTE: this names 
`GrailsExceptionResolver.setGrailsApplication`, 
`GroovyPageView.handleException`, and `GrailsUtil.initializeStackFilterer` 
wiring details. For the upgrade notes, what a user needs is: (1) the two keys 
now reach `GrailsUtil`-driven paths including GSP render errors, (2) before the 
application context is initialized `GrailsUtil` falls back to the default 
filterer (unchanged behaviour), and (3) apps that silenced the `StackTrace` 
logger in logback purely to suppress GSP-render noise can now use 
`logFullStackTraceOnFilter: false` instead. The internal method references 
should be dropped.



##########
grails-core/src/main/groovy/grails/util/GrailsUtil.java:
##########
@@ -36,11 +41,51 @@ public class GrailsUtil {
 
     private static final Log LOG = LogFactory.getLog(GrailsUtil.class);
     private static final boolean LOG_DEPRECATED = 
Boolean.valueOf(System.getProperty("grails.log.deprecated", 
String.valueOf(Environment.isDevelopmentMode())));
-    private static final StackTraceFilterer stackFilterer = new 
DefaultStackTraceFilterer();
+
+    /**
+     * Default filterer used before {@link 
#initializeStackFilterer(GrailsApplication)} runs (CLI,
+     * tests that don't boot a context, plain {@code main()} usage). Preserves 
the pre-PR behaviour
+     * of a single hardcoded {@link DefaultStackTraceFilterer} instance for 
the JVM lifetime when no
+     * application is wired.
+     */
+    private static final StackTraceFilterer FALLBACK_FILTERER = new 
DefaultStackTraceFilterer();
+
+    /**
+     * Active filterer for {@link #printSanitizedStackTrace}, {@link 
#sanitizeRootCause} and
+     * {@link #deepSanitize}. Starts as {@link #FALLBACK_FILTERER} and is 
replaced with a
+     * config-driven instance when {@link 
#initializeStackFilterer(GrailsApplication)} runs during
+     * Grails bootstrap. Volatile so the bootstrap-time write publishes safely 
to the request
+     * threads that read it later.
+     */
+    private static volatile StackTraceFilterer stackFilterer = 
FALLBACK_FILTERER;
 
     private GrailsUtil() {
     }
 
+    /**
+     * Installs a {@link StackTraceFilterer} resolved from the given 
application's config, replacing
+     * the default fallback. Reads {@link 
Settings#SETTING_LOGGING_STACKTRACE_FILTER_CLASS} for the
+     * filterer class and propagates {@link 
Settings#SETTING_LOG_FULL_STACKTRACE_ON_FILTER} to
+     * instances of {@link DefaultStackTraceFilterer}. Called by {@code 
GrailsExceptionResolver}
+     * during Spring bean wiring (which is the same point the resolver 
consults these keys for its
+     * own filterer), so request-time callers of the static {@code 
sanitize}/{@code deepSanitize}
+     * methods see the configured instance.
+     *
+     * <p>No-ops when {@code application} is null. Safe to call more than once 
— the last successful
+     * invocation wins.
+     *
+     * @since 7.1.5
+     */
+    public static void initializeStackFilterer(GrailsApplication application) {

Review Comment:
   If initialization moves to the bootstrap-registry phase, this should take 
the `Environment` (or simply the resolved class + boolean) rather than 
`GrailsApplication` — the `grailsApplication` bean doesn't exist until refresh, 
while `Environment.getProperty(key, Class.class, default)` performs the same 
String→Class conversion `Config` does. That keeps `GrailsUtil` free of any 
dependency on the application object and lets the filterer be configured before 
the context refreshes.



##########
grails-core/src/test/groovy/grails/util/GrailsUtilStackFiltererSpec.groovy:
##########
@@ -0,0 +1,172 @@
+/*
+ *  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
+ *
+ *    https://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 grails.util
+
+import grails.config.Config
+import grails.core.GrailsApplication
+import org.grails.exceptions.reporting.DefaultStackTraceFilterer
+import org.grails.exceptions.reporting.StackTraceFilterer
+import spock.lang.Specification
+
+import java.lang.reflect.Field
+
+/**
+ * Verifies that {@link GrailsUtil#initializeStackFilterer} resolves the 
configured filterer class
+ * from the application's config and propagates {@code 
grails.exceptionresolver.logFullStackTraceOnFilter}
+ * to {@link DefaultStackTraceFilterer} instances. Before initialization the 
FALLBACK_FILTERER
+ * (a {@link DefaultStackTraceFilterer} singleton) is used so CLI/test/main 
paths work unchanged.
+ */
+class GrailsUtilStackFiltererSpec extends Specification {

Review Comment:
   Following up on the earlier integration-test thread: yes, please add the 
`@Integration` spec — `app2` is the right home since it already exercises 
exception handling. Boot with `grails.logging.stackTraceFiltererClass` set in 
config, call `GrailsUtil.deepSanitize` in the running app, and assert the 
configured class is used. That's the piece the unit spec can't prove: that the 
bootstrap wiring actually fires in a real app.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to