Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion grails-core/src/main/groovy/grails/util/GrailsUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeanUtils;

import grails.config.Config;
import grails.config.Settings;
import grails.core.GrailsApplication;
import org.grails.exceptions.reporting.DefaultStackTraceFilterer;
import org.grails.exceptions.reporting.StackTraceFilterer;

Expand All @@ -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.2
*/
public static void initializeStackFilterer(GrailsApplication application) {
if (application == null) {
return;
}
StackTraceFilterer instance = createConfiguredFilterer(application);
if (instance != null) {
stackFilterer = instance;
}
}

/**
* Retrieves whether the current execution environment is the development one.
*
Expand Down Expand Up @@ -157,4 +202,37 @@ public static Throwable deepSanitize(Throwable t) {
return stackFilterer.filter(t, true);
}

private static StackTraceFilterer createConfiguredFilterer(GrailsApplication application) {
Class<? extends StackTraceFilterer> filtererClass = DefaultStackTraceFilterer.class;
boolean logOnFilter = true;
Config config = application.getConfig();
if (config != null) {
Class<? extends StackTraceFilterer> configured = config.getProperty(
Settings.SETTING_LOGGING_STACKTRACE_FILTER_CLASS,
Class.class, DefaultStackTraceFilterer.class);
if (configured != null) {
filtererClass = configured;
}
Boolean configuredLogOnFilter = config.getProperty(
Settings.SETTING_LOG_FULL_STACKTRACE_ON_FILTER,
Boolean.class, Boolean.TRUE);
if (configuredLogOnFilter != null) {
logOnFilter = configuredLogOnFilter;
}
}
StackTraceFilterer instance;
try {
instance = BeanUtils.instantiateClass(filtererClass, StackTraceFilterer.class);
}
catch (Throwable t) {
LOG.warn("Problem instantiating configured StackTraceFilterer [" + filtererClass.getName() +
"], falling back to default: " + t.getMessage());
instance = new DefaultStackTraceFilterer();
}
if (instance instanceof DefaultStackTraceFilterer) {
((DefaultStackTraceFilterer) instance).setLogFullStackTraceOnFilter(logOnFilter);
}
return instance;
}

}
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you point me to an integration test that exercises this logic?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently only the unit spec; same coverage level the parent PR #15564 shipped with for the resolver side of these keys. Happy to add an @Integration spec in grails-test-examples/app2 that boots a real context, sets grails.logging.stackTraceFiltererClass in application.yml, calls GrailsUtil.deepSanitize from inside the running app, and asserts the configured class was used. Prefer app2 (it already exercises exception handling) or a dedicated minimal app like config-report?


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')

cleanup:
System.setErr(originalErr)
}

def 'last initializeStackFilterer call wins when invoked more than once'() {
given:
def first = mockApplicationFor(RecordingStackTraceFilterer)
def second = mockApplicationFor(SecondRecordingStackTraceFilterer)

when:
GrailsUtil.initializeStackFilterer(first)
GrailsUtil.initializeStackFilterer(second)

then:
currentFilterer() instanceof SecondRecordingStackTraceFilterer
}

private GrailsApplication mockApplicationFor(Class<? extends StackTraceFilterer> filtererClass) {
def application = Mock(GrailsApplication)
def config = Mock(Config)
config.getProperty('grails.logging.stackTraceFiltererClass', Class, DefaultStackTraceFilterer) >> filtererClass
config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> true
application.getConfig() >> config
application
}

private static StackTraceFilterer currentFilterer() {
filtererField().get(null) as StackTraceFilterer
}

private static void setFilterer(StackTraceFilterer filterer) {
filtererField().set(null, filterer)
}

private static StackTraceFilterer fallbackFilterer() {
Field field = GrailsUtil.getDeclaredField('FALLBACK_FILTERER')
field.accessible = true
field.get(null) as StackTraceFilterer
}

private static Field filtererField() {
Field field = GrailsUtil.getDeclaredField('stackFilterer')
field.accessible = true
field
}

static class RecordingStackTraceFilterer implements StackTraceFilterer {
static RecordingStackTraceFilterer lastInstance
int singleCalls = 0
int recursiveCalls = 0

RecordingStackTraceFilterer() {
lastInstance = this
}

Throwable filter(Throwable source) { singleCalls++; source }
Throwable filter(Throwable source, boolean recursive) { recursiveCalls++; source }
void addInternalPackage(String name) {}
void setCutOffPackage(String cutOffPackage) {}
void setShouldFilter(boolean shouldFilter) {}
}

static class SecondRecordingStackTraceFilterer implements StackTraceFilterer {
Throwable filter(Throwable source) { source }
Throwable filter(Throwable source, boolean recursive) { source }
void addInternalPackage(String name) {}
void setCutOffPackage(String cutOffPackage) {}
void setShouldFilter(boolean shouldFilter) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
(`grails.logging.stackTraceFiltererClass` and `grails.exceptionresolver.logFullStackTraceOnFilter`);
`GrailsExceptionResolver.setGrailsApplication` calls `GrailsUtil.initializeStackFilterer(application)` during
Spring bean wiring, so this property controls both resolver-driven _and_ `GrailsUtil`-driven emission
(including the GSP view-render path through `GroovyPageView.handleException`). Custom `StackTraceFilterer`
implementations that don't extend `DefaultStackTraceFilterer` are responsible for their own logging policy.

The behaviour is enabled by default. To disable the side-effect emission and rely solely on
`logFullStackTrace` for resolver-driven output, set:

Expand Down
11 changes: 11 additions & 0 deletions grails-doc/src/en/guide/upgrading/upgrading71x.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
`grails.exceptionresolver.logFullStackTraceOnFilter` as well — `GrailsExceptionResolver.setGrailsApplication`
calls the new `GrailsUtil.initializeStackFilterer(GrailsApplication)` during Spring bean wiring, so non-resolver
paths (including GSP view-render exceptions routed through `GroovyPageView.handleException` →
`GrailsUtil.deepSanitize`) participate in the same emission policy as the resolver. Before initialization
runs (CLI, tests that don't boot a context, plain `main()`), `GrailsUtil` uses a single hardcoded
`DefaultStackTraceFilterer` fallback, matching the pre-PR behaviour. Pre-7.1, `GrailsUtil` held a
hardcoded `DefaultStackTraceFilterer` static field that ignored both keys; applications that previously had
to silence the `StackTrace` logger in logback purely to suppress GSP-render-time noise can now set
`logFullStackTraceOnFilter: false` and reach every caller of the filterer.
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import grails.core.GrailsApplication;
import grails.core.support.GrailsApplicationAware;
import grails.util.Environment;
import grails.util.GrailsUtil;
import grails.web.mapping.UrlMappingInfo;
import grails.web.mapping.UrlMappingsHolder;
import grails.web.mapping.exceptions.UrlMappingException;
Expand Down Expand Up @@ -131,6 +132,7 @@ public void setServletContext(ServletContext servletContext) {
public void setGrailsApplication(GrailsApplication grailsApplication) {
this.grailsApplication = grailsApplication;
createStackFilterer();
GrailsUtil.initializeStackFilterer(grailsApplication);
this.auditorAwareLookup = new AuditorAwareLookup(grailsApplication.getMainContext());
}

Expand Down
Loading