提交 740f9eb3 编写于 作者: R Ramon Leon 提交者: Oleg Nenashev

[JENKINS-57223] - Java11 ClassNotFoundExceptions Telemetry (#4003)

* Proof of concept catching exceptions for java 11 removed classes

* Add ClassLoader to a lot of places. Backup

* Commenting out wip stuff, change packages, count hits findClass

* Improvements, sending and tests

* Removing the catcher from chain to make funcional tests work

* Removing ignored places and a bit of cleaning

* Cleaning unused code after removing the catcher from the chain

* Remove imports cleaning to avoid extra changes

* Cleaning and explaining comments

* Explaining comments

* fix javadoc

* [JENKINS-57223] Address feedback from Matt Sicker. Improve in concurrency

* [JENKINS-57223] Search on cause and suppressed exceptions and fix on name reported

* [JENKINS-57223] Add cycles control on exceptions and refactor reporting method

* [JENKINS-57223] Add trim to class name comparison

* [JENKINS-57223] Address Oleg's feedback and some improvements

* Avoid sending CNFE when ignored later in code
* Disable when running on Java 8 and a test
* Avoid sending empty events

* [JENKINS-57223] Polish assertion after printing all missing classes in logs

* [JENKINS-57223] Avoid extra if and add curly braces

* [JENKINS-57223] Better comment about time window check
上级 c58a3ccf
......@@ -64,6 +64,7 @@ import jenkins.install.InstallUtil;
import jenkins.model.Jenkins;
import jenkins.plugins.DetachedPluginsUtil;
import jenkins.security.CustomClassFilter;
import jenkins.telemetry.impl.java11.MissingClassTelemetry;
import jenkins.util.SystemProperties;
import jenkins.util.io.OnMaster;
import jenkins.util.xml.RestrictiveEntityResolver;
......@@ -2065,7 +2066,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
loaded.put(name, null);
}
// not found in any of the classloader. delegate.
throw new ClassNotFoundException(name);
ClassNotFoundException cnfe = new ClassNotFoundException(name);
MissingClassTelemetry.reportException(name, cnfe);
throw cnfe;
}
@Override
......
package hudson.init.impl;
import hudson.init.Initializer;
import java.io.EOFException;
import jenkins.model.Jenkins;
import jenkins.telemetry.impl.java11.MissingClassTelemetry;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.WebApp;
import org.kohsuke.stapler.compression.CompressionFilter;
import javax.servlet.ServletException;
import java.io.EOFException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.stapler.Stapler;
/**
* Deals with exceptions that get thrown all the way up to the Stapler rendering layer.
*/
......@@ -29,6 +29,10 @@ public class InstallUncaughtExceptionHandler {
}
req.setAttribute("javax.servlet.error.exception",e);
try {
// If we have an exception, let's see if it's related with missing classes on Java 11. We reach
// here with a ClassNotFoundException in an action, for example. Setting the report here is the only
// way to catch the missing classes when the plugin uses Thread.currentThread().getContextClassLoader().loadClass
MissingClassTelemetry.reportExceptionInside(e);
WebApp.get(j.servletContext).getSomeStapler().invoke(req, rsp, j, "/oops");
} catch (ServletException | IOException x) {
if (!Stapler.isSocketException(x)) {
......@@ -42,10 +46,10 @@ public class InstallUncaughtExceptionHandler {
}
catch (SecurityException ex) {
LOGGER.log(Level.SEVERE,
"Failed to set the default UncaughtExceptionHandler. " +
"Failed to set the default UncaughtExceptionHandler. " +
"If any threads die due to unhandled coding errors then there will be no logging of this information. " +
"The lack of this diagnostic information will make it harder to track down issues which will reduce the supportability of Jenkins. " +
"It is highly recommended that you consult the documentation that comes with you servlet container on how to allow the " +
"The lack of this diagnostic information will make it harder to track down issues which will reduce the supportability of Jenkins. " +
"It is highly recommended that you consult the documentation that comes with you servlet container on how to allow the " +
"`setDefaultUncaughtExceptionHandler` permission and enable it.", ex);
}
}
......@@ -70,10 +74,14 @@ public class InstallUncaughtExceptionHandler {
"A thread (" + t.getName() + '/' + t.getId()
+ ") died unexpectedly due to an uncaught exception, this may leave your Jenkins in a bad way and is usually indicative of a bug in the code.",
ex);
// If we have an exception, let's see if it's related with missing classes on Java 11. We reach
// here with a ClassNotFoundException in an action, for example. Setting the report here is the only
// way to catch the missing classes when the plugin uses Thread.currentThread().getContextClassLoader().loadClass
MissingClassTelemetry.reportExceptionInside(ex);
}
}
private InstallUncaughtExceptionHandler() {}
}
......@@ -43,6 +43,8 @@ import jenkins.security.stapler.StaplerFilteredActionListener;
import jenkins.security.stapler.StaplerDispatchable;
import jenkins.security.RedactSecretJsonInErrorMessageSanitizer;
import jenkins.security.stapler.TypedFilter;
import jenkins.telemetry.impl.java11.CatcherClassLoader;
import jenkins.telemetry.impl.java11.MissingClassTelemetry;
import jenkins.util.SystemProperties;
import hudson.cli.declarative.CLIMethod;
import hudson.cli.declarative.CLIResolver;
......@@ -870,6 +872,12 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}
// doing this early allows InitStrategy to set environment upfront
//Telemetry: add interceptor classloader
//These lines allow the catcher to be present on Thread.currentThread().getContextClassLoader() in every plugin which
//allow us to detect failures in every plugin loading classes by this way.
if (MissingClassTelemetry.enabled() && !(Thread.currentThread().getContextClassLoader() instanceof CatcherClassLoader)) {
Thread.currentThread().setContextClassLoader(new CatcherClassLoader(Thread.currentThread().getContextClassLoader()));
}
final InitStrategy is = InitStrategy.get(Thread.currentThread().getContextClassLoader());
Trigger.timer = new java.util.Timer("Jenkins cron thread");
......@@ -910,8 +918,20 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
pluginManager = PluginManager.createDefault(this);
this.pluginManager = pluginManager;
WebApp webApp = WebApp.get(servletContext);
//Telemetry: add interceptor classloader
//These lines allows the catcher to be present on Thread.currentThread().getContextClassLoader() in every plugin which
//allow us to detect failures in every plugin loading classes by this way.
// JSON binding needs to be able to see all the classes from all the plugins
webApp.setClassLoader(pluginManager.uberClassLoader);
ClassLoader classLoaderToAssign;
if (MissingClassTelemetry.enabled() && !(pluginManager.uberClassLoader instanceof CatcherClassLoader)) {
classLoaderToAssign = new CatcherClassLoader(pluginManager.uberClassLoader);
} else {
classLoaderToAssign = pluginManager.uberClassLoader;
}
webApp.setClassLoader(classLoaderToAssign);
webApp.setJsonInErrorMessageSanitizer(RedactSecretJsonInErrorMessageSanitizer.INSTANCE);
TypedFilter typedFilter = new TypedFilter();
......@@ -924,7 +944,10 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
webApp.setFilteredDoActionTriggerListener(actionListener);
webApp.setFilteredFieldTriggerListener(actionListener);
adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader,"adjuncts/"+SESSION_HASH, TimeUnit.DAYS.toMillis(365));
//Telemetry: add interceptor classloader
//These lines allows the catcher to be present on Thread.currentThread().getContextClassLoader() in every plugin which
//allow us to detect failures in every plugin loading classes at this way.
adjuncts = new AdjunctManager(servletContext, classLoaderToAssign, "adjuncts/" + SESSION_HASH, TimeUnit.DAYS.toMillis(365));
ClassFilterImpl.register();
......
/*
* The MIT License
*
* Copyright (c) 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.telemetry.impl.java11;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@Restricted(NoExternalUse.class)
public class CatcherClassLoader extends ClassLoader {
public CatcherClassLoader(ClassLoader parent) {
super(parent);
}
/**
* Usually, the {@link ClassLoader} calls its parent and finally this method. So if we are here, it's the last
* element of the chain. It doesn't happen in {@link jenkins.util.AntClassLoader} so it has an special management
* on {@link hudson.ClassicPluginStrategy}
*
*
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
ClassNotFoundException e = new ClassNotFoundException(name);
MissingClassTelemetry.reportException(name, e);
throw e;
}
}
/*
* The MIT License
*
* Copyright (c) 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.telemetry.impl.java11;
import javax.annotation.Nonnull;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
/**
* Store an event regarding missing classes. We can already catch ClassNotFoundException and NoClassDefFoundError
*/
class MissingClassEvent {
private String time;
private long occurrences;
private String stackTrace;
private String className;
String getStackTrace() {
return stackTrace;
}
void setStackTrace(String stackTrace) {
this.stackTrace = stackTrace;
}
String getClassName() {
return className;
}
void setClassName(String className) {
this.className = className;
}
MissingClassEvent(@Nonnull String name, @Nonnull Throwable t) {
this.className = name;
StringWriter stackTrace = new StringWriter();
t.printStackTrace(new PrintWriter(stackTrace));
this.stackTrace = stackTrace.toString();
this.time = MissingClassTelemetry.clientDateString();
this.occurrences = 1;
}
String getTime() {
return time;
}
long getOccurrences() {
return occurrences;
}
void setOccurrences(long occurrences) {
this.occurrences = occurrences;
}
void setTime(String time) {
this.time = time;
}
@Override
public String toString() {
return "MissingClassEvent{" +
"time='" + time + '\'' +
", occurrences=" + occurrences +
", stackTrace='" + stackTrace + '\'' +
", className='" + className + '\'' +
'}';
}
}
/*
* The MIT License
*
* Copyright (c) 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.telemetry.impl.java11;
import com.google.common.annotations.VisibleForTesting;
import javax.annotation.Nonnull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class MissingClassEvents {
/**
* Only 100 exceptions a day (period of telemetry)
*/
@VisibleForTesting
/* package */ static /* final */ int MAX_EVENTS_PER_SEND = 100;
/**
* List of events, one per stack trace.
*/
private ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> events = new ConcurrentHashMap<>(MAX_EVENTS_PER_SEND);
/**
* Add a new exception to the store. If the same exception already exists, it increases the occurrences. If we
* already get the maximum number of exceptions, it doesn't add any value.
* @param name name of the class not found
* @param t the exception to store
* @return the occurrences stored for this throwable. 1 the fist time it's stored. &gt; 1 for successive stores of the
* same <strong>stack trace</strong>. 0 if we already stored MAX_EVENTS_PER_SEND (100) events for a single send.
*/
public long put(String name, @Nonnull Throwable t) {
// A final object to pass it to the function
final AtomicLong occurrences = new AtomicLong();
// We need the key (the stack trace) to be a list and unmodifiable
List<StackTraceElement> key = Collections.unmodifiableList(Arrays.asList(t.getStackTrace()));
events.compute(key, (stackTraceElements, missingClassEvent) -> {
if (missingClassEvent == null) {
// It's a new element, the size will increase
if (events.size() < MAX_EVENTS_PER_SEND) {
// Create the new value
MissingClassEvent newEvent = new MissingClassEvent(name, t);
occurrences.set(1);
return newEvent;
} else {
return null;
}
} else {
// We update the occurrences and the last time it happened
occurrences.set(missingClassEvent.getOccurrences());
missingClassEvent.setOccurrences(occurrences.incrementAndGet());
missingClassEvent.setTime(MissingClassTelemetry.clientDateString());
return missingClassEvent;
}
});
return occurrences.get();
}
/**
* Reinitialize the events happened and return the number of events stored since last execution of this method.
* Used to send via telemetry the events and restart the events store.
* @return the number of events stored since previous call to this method.
*/
@VisibleForTesting
/* package */ synchronized @Nonnull ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> getEventsAndClean() {
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> currentEvents = events;
events = new ConcurrentHashMap<>(MAX_EVENTS_PER_SEND);
return currentEvents;
}
@Override
public String toString() {
return "MissingClassEvents{" +
"events=" + events +
'}';
}
}
/*
* The MIT License
*
* Copyright (c) 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.telemetry.impl.java11;
import com.google.common.annotations.VisibleForTesting;
import hudson.Extension;
import io.jenkins.lib.versionnumber.JavaSpecificationVersion;
import jenkins.model.Jenkins;
import jenkins.telemetry.Telemetry;
import jenkins.util.java.JavaUtils;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Telemetry class to gather information about missing classes when running on java 11. This class sends classes not
* found and in packages related with Java changes from Java 8 to Java 11. See {@link #MOVED_PACKAGES}.
**/
@Extension
@Restricted(NoExternalUse.class)
public class MissingClassTelemetry extends Telemetry {
private static final Logger LOGGER = Logger.getLogger(MissingClassTelemetry.class.getName());
// Store 100 events today
private static MissingClassEvents events = new MissingClassEvents();
// When we begin to gather these data
private final static LocalDate START = LocalDate.of(2019, 4, 1);
// Gather for 2 years (who knows how long people will need to migrate to Java 11)
private final static LocalDate END = START.plusMonths(24);
// The types of exceptions which can be reported
private static final Set reportableExceptions =
new HashSet<Class>(Arrays.asList(ClassNotFoundException.class, NoClassDefFoundError.class));
@VisibleForTesting
/* package */ static final String CIRCULAR_REFERENCE = "Circular reference found on the exception we are analysing to report via telemetry";
/**
* Packages removed from java8 up to java11
* https://blog.codefx.org/java/java-11-migration-guide/
*/
private final static String[] MOVED_PACKAGES = new String[] {"javax.activation", "javax.annotation", "javax.jws",
"javax.rmi", "javax.transaction", "javax.xml.bind", "javax.xml.soap", "javax.xml.ws", "org.omg",
"javax.activity", "com.sun", "sun"};
/**
* Places where a ClassNotFoundException is going to be thrown but it's ignored later in the code, so we
* don't have to send this exception, even though it might be related with java classes of moved packages
*/
private static String[][] IGNORED_PLACES = {
{"hudson.util.XStream2$AssociatedConverterImpl", "findConverter"},
{"org.jenkinsci.plugins.workflow.cps.global.GrapeHack", "hack"},
{"org.codehaus.groovy.runtime.callsite.CallSiteArray", "createCallStaticSite"},
{"groovy.lang.MetaClassImpl", "addProperties"},
// We set the reportException call directly in this method when it's appropriated
{"hudson.PluginManager.UberClassLoader", "findClass"},
{"hudson.ExtensionFinder$GuiceFinder$FaultTolerantScope$1", "get"},
{"hudson.ExtensionFinder$GuiceFinder$SezpozModule", "resolve"},
{"java.beans.Introspector", "findCustomizerClass"},
{"com.sun.beans.finder.InstanceFinder", "instantiate"},
{"com.sun.beans.finder.ClassFinder", "findClass"},
{"java.util.ResourceBundle$Control", "newBundle"},
//hundreds when a job is created
{"org.codehaus.groovy.control.ClassNodeResolver", "tryAsLoaderClassOrScript"},
{"org.kohsuke.stapler.RequestImpl$TypePair", "convertJSON"}
};
@Nonnull
@Override
public String getDisplayName() {
return "Missing classes related with Java updates";
}
@Nonnull
@Override
public LocalDate getStart() {
return START;
}
@Nonnull
@Override
public LocalDate getEnd() {
return END;
}
/**
* To allow asserting this info in tests.
* @return the events gathered.
*/
@VisibleForTesting
/* package */ static MissingClassEvents getEvents() {
return events;
}
/**
* This telemetry is only enabled when running on Java versions newer than Java 8.
* @return true if running on a newer Java version than Java 8
*/
public static boolean enabled() {
return JavaUtils.getCurrentJavaRuntimeVersionNumber().isNewerThan(JavaSpecificationVersion.JAVA_8);
}
@CheckForNull
@Override
public JSONObject createContent() {
// If we are on the time window of this telemetry (checked by the Telemetry class) but we are not running on
// Java > 1.8 (checked here), we don't send anything
if (!enabled()) {
return null;
}
// To avoid sending empty events
JSONArray events = formatEventsAndInitialize();
if (events.size() == 0) {
return null;
}
JSONObject info = new JSONObject();
info.put("core", Jenkins.getVersion() != null ? Jenkins.getVersion().toString() : "UNKNOWN");
info.put("clientDate", clientDateString());
info.put("classMissingEvents", events);
return JSONObject.fromObject(info);
}
/**
* Returns the events gathered as a Map ready to use in a Json object to send via telemetry and clean the map to
* gather another window of events.
* @return the map of missed classes events gathered along this window of telemetry
*/
@Nonnull
private JSONArray formatEventsAndInitialize() {
// Save the current events and clean for next (not this one) telemetry send
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> toReport = MissingClassTelemetry.events.getEventsAndClean();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Cleaned events for missing classes");
}
return formatEvents(toReport);
}
/**
* Format the events gathered in a map used to create the json object to send via telemetry. The events are named by
* the class not found. But a class could be repeated if it was thrown from several places. The interesting
* pieces of information we want to gather are the places where the {@link ClassNotFoundException} or the
* {@link NoClassDefFoundError} errors happens, rather than the class itself.
* @param events events collected in this telemetry window.
* @return the events formatted in a map.
*/
@Nonnull
private JSONArray formatEvents(@Nonnull ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> events) {
JSONArray jsonEvents = new JSONArray();
events.forEach((stackTrace, event) -> {
JSONObject eventObject = new JSONObject();
eventObject.put("className", event.getClassName());
eventObject.put("class", event.getClassName());
eventObject.put("time", event.getTime());
eventObject.put("occurrences", Long.toString(event.getOccurrences()));
eventObject.put("stacktrace", event.getStackTrace());
jsonEvents.add(eventObject);
});
return jsonEvents;
}
/**
* The current time in the same way as other telemetry implementations.
* @return the UTC time formatted with the pattern yyyy-MM-dd'T'HH:mm'Z'
*/
@Nonnull
static String clientDateString() {
TimeZone tz = TimeZone.getTimeZone("UTC");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
df.setTimeZone(tz); // strip timezone
return df.format(new Date());
}
/**
* Store the exception if it's from a split package of Java. This method report this exception directly, it doesn't
* look into the causes or suppressed exceptions of the exception specified. This method tends to be used in the
* ClassLoader directly. Outside the class loaders is best to use {@link #reportExceptionInside(Throwable)}
* @param name the name of the class
* @param e the throwable to report if needed
*/
public static void reportException(@Nonnull String name, @Nonnull Throwable e) {
if (enabled()) {
//ClassDefFoundError uses / instead of .
name = name.replace('/', '.').trim();
// We call the methods in this order because if the missing class is not java related, we don't loop over the
// stack trace to look if it's not thrown from an ignored place avoiding an impact on performance.
if (isFromMovedPackage(name) && !calledFromIgnoredPlace(e)) {
events.put(name, e);
if (LOGGER.isLoggable(Level.WARNING))
LOGGER.log(Level.WARNING, "Added a missed class for missing class telemetry. Class: " + name, e);
}
}
}
/**
* Determine if the exception specified was thrown from an ignored place
* @param throwable The exception thrown
* @return true if in the stack trace there is an ignored method / class.
*/
private static boolean calledFromIgnoredPlace(@Nonnull Throwable throwable) {
for(String[] ignoredPlace : IGNORED_PLACES) {
if (calledFrom(throwable, ignoredPlace[0], ignoredPlace[1])) {
return true;
}
}
return false;
}
/**
* Check if the throwable was thrown by the class and the method specified.
* @param throwable stack trace to look at
* @param clazz class to look for in the stack trace
* @param method method where the throwable was thrown in the clazz
* @return true if the method of the clazz has thrown the throwable
*/
private static boolean calledFrom (@Nonnull Throwable throwable, @Nonnull String clazz, @Nonnull String method){
StackTraceElement[] trace = throwable.getStackTrace();
for (StackTraceElement el : trace) {
//If the exception has the class and method searched, it's called from there
if (clazz.equals(el.getClassName()) && el.getMethodName().equals(method)) {
return true;
}
}
return false;
}
/**
* Store the exception extracting the class name from the message of the throwable specified. This method report
* this exception directly, it doesn't look into the causes or suppressed exceptions of the exception specified.
* This method tends to be used in the ClassLoader directly. Outside the class loaders is best to use
* {@link #reportExceptionInside(Throwable)}
* @param e the exception to report if needed
*/
private static void reportException(@Nonnull Throwable e) {
if (enabled()) {
String name = e.getMessage();
if (name == null || name.trim().isEmpty()) {
LOGGER.log(Level.INFO, "No class name could be extracted from the throwable to determine if it's reportable", e);
} else {
reportException(name, e);
}
}
}
private static boolean isFromMovedPackage(@Nonnull String clazz) {
for (String movedPackage : MOVED_PACKAGES) {
if (clazz.startsWith(movedPackage)) {
return true;
}
}
return false;
}
/**
* Report the class not found if this exception or any of its causes or suppressed exceptions is related to missed
* classes.
* @param e the exception to look into
*/
public static void reportExceptionInside(@Nonnull Throwable e) {
if (enabled()) {
// Use a Set with equity based on == instead of equal to find cycles
Set<Throwable> exceptionsReviewed = Collections.newSetFromMap(new IdentityHashMap<>());
reportExceptionInside(e, exceptionsReviewed);
}
}
/**
* Find the exception to report among the exception passed and its causes and suppressed exceptions. It does a
* recursion and uses a Set to avoid circular references.
* @param e the exception
* @param exceptionsReviewed the set of already reviewed exceptions
* @return true if a exception was reported
*/
private static boolean reportExceptionInside(@Nonnull Throwable e, @Nonnull Set<Throwable> exceptionsReviewed) {
if (exceptionsReviewed.contains(e)) {
LOGGER.log(Level.WARNING, CIRCULAR_REFERENCE, e);
// Don't go deeper, we already did
return false;
}
// Add this exception to the list of already reviewed exceptions before going deeper in its causes or suppressed
// exceptions
exceptionsReviewed.add(e);
// It this exception is the one searched
if (isMissedClassRelatedException(e)) {
MissingClassTelemetry.reportException(e);
return true;
}
// We search in its cause exception
if (e.getCause() != null) {
if (reportExceptionInside(e.getCause(), exceptionsReviewed)) {
return true;
}
}
// We search in its suppressed exceptions
for (Throwable suppressed: e.getSuppressed()) {
if (suppressed != null) {
if (reportExceptionInside(suppressed, exceptionsReviewed)) {
return true;
}
}
}
// If this exception or its ancestors are not related with missed classes
return false;
}
/**
* Check if the exception specified is related with a missed class, that is, defined in the
* {@link #reportableExceptions} method.
* @param e the exception to look into
* @return true if the class is related with missed classes.
*/
private static boolean isMissedClassRelatedException(Throwable e) {
return reportableExceptions.contains(e.getClass());
}
}
......@@ -17,7 +17,7 @@
*/
package jenkins.util;
import java.nio.file.Files;
import jenkins.telemetry.impl.java11.MissingClassTelemetry;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
......@@ -40,6 +40,7 @@ import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
......@@ -1070,27 +1071,36 @@ public class AntClassLoader extends ClassLoader implements SubBuildListener {
if (theClass != null) {
return theClass;
}
if (isParentFirst(classname)) {
try {
theClass = findBaseClass(classname);
log("Class " + classname + " loaded from parent loader " + "(parentFirst)",
Project.MSG_DEBUG);
} catch (ClassNotFoundException cnfe) {
theClass = findClass(classname);
log("Class " + classname + " loaded from ant loader " + "(parentFirst)",
Project.MSG_DEBUG);
}
} else {
try {
theClass = findClass(classname);
log("Class " + classname + " loaded from ant loader", Project.MSG_DEBUG);
} catch (ClassNotFoundException cnfe) {
if (ignoreBase) {
throw cnfe;
//Surround the former logic with a try-catch to report missing class exceptions via Java11 telemetry
try {
if (isParentFirst(classname)) {
try {
theClass = findBaseClass(classname);
log("Class " + classname + " loaded from parent loader " + "(parentFirst)",
Project.MSG_DEBUG);
} catch (ClassNotFoundException cnfe) {
theClass = findClass(classname);
log("Class " + classname + " loaded from ant loader " + "(parentFirst)",
Project.MSG_DEBUG);
}
} else {
try {
theClass = findClass(classname);
log("Class " + classname + " loaded from ant loader", Project.MSG_DEBUG);
} catch (ClassNotFoundException cnfe) {
if (ignoreBase) {
throw cnfe;
}
theClass = findBaseClass(classname);
log("Class " + classname + " loaded from parent loader", Project.MSG_DEBUG);
}
theClass = findBaseClass(classname);
log("Class " + classname + " loaded from parent loader", Project.MSG_DEBUG);
}
} catch (ClassNotFoundException cnfe) {
//To catch CNFE thrown from this.getClass().getClassLoader().loadClass(classToLoad); from a plugin step or
//a plugin page
MissingClassTelemetry.reportException(classname, cnfe);
throw cnfe;
}
if (resolve) {
resolveClass(theClass);
......
/*
* The MIT License
*
* Copyright (c) 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.telemetry;
import hudson.ExtensionList;
import hudson.model.UnprotectedRootAction;
import hudson.security.csrf.CrumbExclusion;
import jenkins.telemetry.impl.java11.CatcherClassLoader;
import jenkins.telemetry.impl.java11.MissingClassTelemetry;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.annotation.CheckForNull;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertEquals;
/**
* This test needs to be here to be able to modify the {@link Telemetry#ENDPOINT} as it's package protected.
*/
public class MissingClassTelemetryTest {
private static final String TELEMETRY_ENDPOINT = "uplink";
private CatcherClassLoader cl;
@Rule
public JenkinsRule j = new JenkinsRule();
private static JSONObject received = null;
@Before
public void prepare() throws Exception {
received = null;
cl = new CatcherClassLoader(this.getClass().getClassLoader());
Telemetry.ENDPOINT = j.getURL().toString() + TELEMETRY_ENDPOINT + "/events";
j.jenkins.setNoUsageStatistics(false); // tests usually don't submit this, but we need this
}
/**
* Test if the telemetry sent works and the received data is the expected for a specific case (5 occurrences of the
* same stack trace).
* @throws InterruptedException if the thread is interrupted while sleeping
*/
@Test
public void telemetrySentWorks() throws InterruptedException {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
// Generate 5 events
for(int i = 0; i < 5; i++) {
try {
cl.loadClass("sun.java.MyNonExistentClass");
} catch (ClassNotFoundException ignored) {
}
}
// Run the telemetry sent
ExtensionList.lookupSingleton(Telemetry.TelemetryReporter.class).doRun();
do {
Thread.sleep(250);
} while (received == null); // this might end up being flaky due to 1 to many active telemetry trials
// The telemetry stuff sent is the class expected, the number of events is 1, the class not found is the
// expected and the number of occurrences is the expected
assertEquals(MissingClassTelemetry.class.getName(), received.getString("type"));
JSONArray events = received.getJSONObject("payload").getJSONArray("classMissingEvents");
assertEquals(1, events.size());
assertEquals("sun.java.MyNonExistentClass", ((JSONObject) events.get(0)).get("className"));
assertEquals(5, Integer.parseInt( (String) ((JSONObject) events.get(0)).get("occurrences")));
}
/**
* Avoid crumb checking (CSRF)
*/
@TestExtension
public static class NoCrumb extends CrumbExclusion {
@Override
public boolean process(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String pathInfo = request.getPathInfo();
if (pathInfo != null && pathInfo.startsWith("/uplink")) {
chain.doFilter(request, response);
return true;
}
return false;
}
}
@TestExtension
public static class TelemetryReceiver implements UnprotectedRootAction {
public void doEvents(StaplerRequest request, StaplerResponse response) throws IOException {
StringWriter sw = new StringWriter();
IOUtils.copy(request.getInputStream(), sw, StandardCharsets.UTF_8);
received = JSONObject.fromObject(sw.toString());
}
@CheckForNull
@Override
public String getIconFileName() {
return null;
}
@CheckForNull
@Override
public String getDisplayName() {
return null;
}
@CheckForNull
@Override
public String getUrlName() {
return TELEMETRY_ENDPOINT;
}
}
}
/*
* The MIT License
*
* Copyright (c) 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.telemetry.impl.java11;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.LoggerRule;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
/**
* Tests without a running Jenkins for Java11 Telemetry of ClassNotFoundException.
*/
public class MissingClassTelemetryFasterTest {
private CatcherClassLoader cl;
@Rule
public LoggerRule logging = new LoggerRule();
@Before
public void cleanEvents() {
cl = new CatcherClassLoader(this.getClass().getClassLoader());
}
@Test
public void maxNumberEvents() {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
// Backup to restore at the end of the test
int maxEventsBefore = MissingClassEvents.MAX_EVENTS_PER_SEND;
try {
MissingClassEvents.MAX_EVENTS_PER_SEND = 1;
try {
cl.loadClass("sun.java.MyNonExistentClass");
} catch (ClassNotFoundException ignored) {
}
try {
cl.loadClass("sun.java.MyNonExistentJavaClass");
} catch (ClassNotFoundException ignored) {
}
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = MissingClassTelemetry.getEvents().getEventsAndClean();
// Only one class miss gathered with two occurrences
assertEquals(1, eventsGathered.size());
} finally {
MissingClassEvents.MAX_EVENTS_PER_SEND = maxEventsBefore;
}
}
/**
* The same class failed to be loaded in different places ends up in two records of telemetry with one occurrence
* each.
*/
@Test
public void differentEventsAlthoughSameClass() {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
try {
cl.loadClass("sun.java.MyNonExistentClass");
} catch (ClassNotFoundException ignored) {
}
try {
cl.loadClass("sun.java.MyNonExistentJavaClass");
} catch (ClassNotFoundException ignored) {
}
// Get the events gathered
MissingClassEvents events = MissingClassTelemetry.getEvents();
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = events.getEventsAndClean();
// Only one class miss gathered with two occurrences
assertEquals(2, eventsGathered.size());
assertEquals(1, eventsGathered.values().iterator().next().getOccurrences());
assertEquals(1, eventsGathered.values().iterator().next().getOccurrences());
}
/**
* The same class thrown in the same line ends up in a single event with two occurrences.
*/
@Test
public void addOccurrenceIfSameStackTrace() {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
for (int i = 0; i < 2; i++) {
try {
//Exceptions thrown at the same line, with the same stack trace become occurrences of just one event
cl.loadClass("sun.java.MyNonExistentJavaClass");
} catch (ClassNotFoundException ignored) {
}
}
// Get the events gathered
MissingClassEvents events = MissingClassTelemetry.getEvents();
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = events.getEventsAndClean();
// Only one class miss gathered with two occurrences
assertEquals(1, eventsGathered.size());
assertEquals(2, eventsGathered.values().iterator().next().getOccurrences());
}
/**
* A class not from the split packages is not gathered.
*/
@Test
public void nonJavaClassesNotGathered() {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
try {
cl.loadClass("jenkins.MyNonExistentClass");
} catch (ClassNotFoundException ignored) {
}
// Get the events gathered
MissingClassEvents events = MissingClassTelemetry.getEvents();
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = events.getEventsAndClean();
// No events gathered
assertEquals(0, eventsGathered.size());
}
/**
* Only a max number of events is gathered. In this test, just one wit two occurrences
*/
@Test
public void maxEventsLimitedSameStackTrace() {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
MissingClassEvents.MAX_EVENTS_PER_SEND = 1;
for (int i = 0; i < 2; i++) {
try {
//Exceptions thrown at the same line, with the same stack trace become occurrences of just one event
cl.loadClass("sun.java.MyNonExistentJavaClass");
} catch (ClassNotFoundException ignored) {
}
}
// Get the events gathered
MissingClassEvents events = MissingClassTelemetry.getEvents();
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = events.getEventsAndClean();
// Only one event gathered
assertEquals(1, eventsGathered.size());
assertEquals(2, eventsGathered.values().iterator().next().getOccurrences());
}
/**
* Only a max number of events is gathered. In this test, just one wit one occurrence. The second one is discarded
*/
@Test
public void maxEventsLimitedDifferentStackTrace() {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
MissingClassEvents.MAX_EVENTS_PER_SEND = 1;
try {
cl.loadClass("sun.java.MyNonExistentClassGathered");
} catch (ClassNotFoundException ignored) {
}
try {
cl.loadClass("sun.java.MyNonExistentJavaClassNotGathered");
} catch (ClassNotFoundException ignored) {
}
// Get the events gathered
MissingClassEvents events = MissingClassTelemetry.getEvents();
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = events.getEventsAndClean();
// Only one event gathered
assertEquals(1, eventsGathered.size());
assertEquals(1, eventsGathered.values().iterator().next().getOccurrences());
assertThat(eventsGathered.values().iterator().next().getStackTrace(), containsString("MyNonExistentClassGathered"));
assertThat(eventsGathered.values().iterator().next().getStackTrace(), not(containsString("MyNonExistentJavaClassNotGathered")));
}
/**
* Test the cycles in the exceptions. This specific tests shows that we first look for reportable exceptions in the
* causes and when found a reportable exception, we stop searching. So the warning because a cycle is not logged.
*/
@Test
public void cyclesNotReachedBecauseCNFEReported() {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
logging.record(MissingClassTelemetry.class, Logger.getLogger(MissingClassTelemetry.class.getName()).getLevel()).capture(5);
try {
/*
parent -> child -> cnfe
\
parent
We first look into the causes exceptions. When found, we don't look into the suppressed, so the cycle is not
found here
*/
ClassNotFoundException cnfe = new ClassNotFoundException("sun.java.MyNonExistentClassGathered");
Exception child = new Exception("child", cnfe);
Exception parent = new Exception("parent", child); // parent -> caused by -> child
child.addSuppressed(parent);
// Some extra wrapping
throw new Exception(new Exception (new Exception (parent)));
} catch (Exception e) {
// Look for anything to report
MissingClassTelemetry.reportExceptionInside(e);
// Get the events gathered
MissingClassEvents events = MissingClassTelemetry.getEvents();
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = events.getEventsAndClean();
// One event gathered
assertEquals(1, eventsGathered.size());
// the circular reference has not been recorded in the log because we reached a CNFE previously
assertEquals("No circular message was printed in logs", 0, logging.getRecords().stream().filter(r -> r.getMessage().contains(MissingClassTelemetry.CIRCULAR_REFERENCE)).count());
}
}
/**
* Test the cycles in the exceptions. This specific tests shows that we first look for reportable exceptions deep
* in the causes and suppressed exceptions in this order. We report the cycle but also the CNFE.
*/
@Test
public void cnfeFoundAfterCycle() {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
logging.record(MissingClassTelemetry.class, Logger.getLogger(MissingClassTelemetry.class.getName()).getLevel()).capture(5);
try {
ClassNotFoundException cnfe = new ClassNotFoundException("sun.java.MyNonExistentClassGathered");
/*
parent -> child
\ \
cnfe parent
*/
Exception child = new Exception("child");
Exception parent = new Exception("parent", child); // parent -> caused by -> child
child.addSuppressed(parent); // Boooomm!!!! The parent is a child suppressed exception -> cycle
parent.addSuppressed(cnfe);
// Some extra wrapping
throw new Exception(new Exception (new Exception (parent)));
} catch (Exception e) {
// Look for anything to report
MissingClassTelemetry.reportExceptionInside(e);
// Get the events gathered
MissingClassEvents events = MissingClassTelemetry.getEvents();
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = events.getEventsAndClean();
// One event gathered
assertEquals(1, eventsGathered.size());
// the circular reference has been recorded in the log
assertThat(logging, LoggerRule.recorded(containsString(MissingClassTelemetry.CIRCULAR_REFERENCE)));
}
}
/**
* Test the cycles in the exceptions. This specific tests shows that we first look for reportable exceptions deep
* in the causes and suppressed exceptions in this order. We report the cycle but also the CNFE in the parent.
*/
@Test
public void cnfeAfterCNFENotJava11AndCycle() {
Assume.assumeTrue("The telemetry should be enabled", MissingClassTelemetry.enabled());
logging.record(MissingClassTelemetry.class, Logger.getLogger(MissingClassTelemetry.class.getName()).getLevel()).capture(5);
try {
ClassNotFoundException cnfe = new ClassNotFoundException("sun.java.MyNonExistentClassGathered");
ClassNotFoundException cnfeNonJava11 = new ClassNotFoundException("MyNonExistentClassGathered");
/*
parent -> child -> grandchild
\ \ \
cnfe parent cnfe (non java11)
*/
Exception grandchild = new Exception("grandchild");
Exception child = new Exception("child");
Exception parent = new Exception("parent", child); // parent -> caused by -> child
child.addSuppressed(parent); // Boooomm!!!! The parent is a child suppressed exception -> cycle
grandchild.addSuppressed(cnfeNonJava11);
parent.addSuppressed(cnfe);
// Some extra wrapping
throw new Exception(new Exception (new Exception (parent)));
} catch (Exception e) {
// Look for anything to report
MissingClassTelemetry.reportExceptionInside(e);
// Get the events gathered
MissingClassEvents events = MissingClassTelemetry.getEvents();
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = events.getEventsAndClean();
// One event gathered
assertEquals(1, eventsGathered.size());
// the circular reference has been recorded in the log
assertThat(logging, LoggerRule.recorded(containsString(MissingClassTelemetry.CIRCULAR_REFERENCE)));
}
}
@Test
public void nothingGatheredWhenTelemetryDisabled() {
Assume.assumeFalse("The telemetry should not be enabled", MissingClassTelemetry.enabled());
try {
cl.loadClass("sun.java.MyNonExistentClass");
} catch (ClassNotFoundException ignored) {
}
ConcurrentHashMap<List<StackTraceElement>, MissingClassEvent> eventsGathered = MissingClassTelemetry.getEvents().getEventsAndClean();
// No events gathered
assertEquals(0, eventsGathered.size());
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册