Rewrote error logging code

- The new error logging only creates one log per error and has context
with the error if specified.
- Any duplicate lines in error stacktrace are not taken into account
when hashing to avoid recursive function errors being logged in different
files

Affects issues:
- Close #1246
This commit is contained in:
Risto Lahtela 2020-05-14 12:18:12 +03:00
parent 5fc9980419
commit d09a4016d3
8 changed files with 361 additions and 11 deletions

View File

@ -17,8 +17,6 @@
package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.SubSystem;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.properties.ServerProperties;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
@ -57,8 +55,7 @@ public class WebServer implements SubSystem {
private final PlanFiles files;
private final PlanConfig config;
private final ServerProperties serverProperties;
private Addresses addresses;
private final Addresses addresses;
private final RequestHandler requestHandler;
private final PluginLogger logger;
@ -75,7 +72,6 @@ public class WebServer implements SubSystem {
Locale locale,
PlanFiles files,
PlanConfig config,
ServerInfo serverInfo,
Addresses addresses,
PluginLogger logger,
ErrorHandler errorHandler,
@ -84,7 +80,6 @@ public class WebServer implements SubSystem {
this.locale = locale;
this.files = files;
this.config = config;
this.serverProperties = serverInfo.getServerProperties();
this.addresses = addresses;
this.requestHandler = requestHandler;

View File

@ -17,6 +17,7 @@
package com.djrapitops.plan.modules;
import com.djrapitops.plan.PlanPlugin;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import com.djrapitops.plugin.IPlugin;
import com.djrapitops.plugin.benchmarking.Timings;
import com.djrapitops.plugin.command.ColorScheme;
@ -70,8 +71,8 @@ public class APFModule {
@Provides
@Singleton
ErrorHandler provideErrorHandler(IPlugin plugin) {
return plugin.getErrorHandler();
ErrorHandler provideErrorHandler(ErrorLogger errorLogger) {
return errorLogger;
}
@Provides

View File

@ -59,7 +59,7 @@ public class PlanFiles implements SubSystem {
}
public Path getCustomizationDirectory() {
return dataFolder.toPath().resolve("web");
return getDataDirectory().resolve("web");
}
public File getLogsFolder() {
@ -72,6 +72,10 @@ public class PlanFiles implements SubSystem {
}
}
public Path getLogsDirectory() {
return getDataDirectory().resolve("logs");
}
public File getConfigFile() {
return configFile;
}

View File

@ -19,6 +19,7 @@ package com.djrapitops.plan.utilities.java;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
/**
* Methods that can be used as functional interfaces when dealing with Maps.
@ -35,6 +36,10 @@ public class Lists {
return new ArrayList<>();
}
public static <V> Lists.Builder<V> builder(Class<V> ofType) {
return new Lists.Builder<>();
}
/**
* Efficient replacement for List#stream().filter(keep).collect(Collectors.toList()).
*
@ -85,4 +90,30 @@ public class Lists {
}
return mapped;
}
public static class Builder<V> {
private final List<V> list;
private Builder() {
list = new ArrayList<>();
}
public Lists.Builder<V> add(V value) {
list.add(value);
return this;
}
public Lists.Builder<V> addAll(Collection<V> values) {
list.addAll(values);
return this;
}
public Lists.Builder<V> apply(UnaryOperator<Lists.Builder<V>> operator) {
return operator.apply(this);
}
public List<V> build() {
return list;
}
}
}

View File

@ -0,0 +1,77 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.utilities.logging;
import java.util.*;
/**
* Contains context for an error that might help debugging it.
*
* @author Rsl1122
*/
public class ErrorContext {
private final List<Object> related;
private String whatToDo;
private ErrorContext() {
related = new ArrayList<>();
}
public static ErrorContext.Builder builder() {
return new ErrorContext.Builder();
}
public Optional<String> getWhatToDo() {
return Optional.ofNullable(whatToDo);
}
public Collection<String> toLines() {
List<String> lines = new ArrayList<>();
for (Object o : related) {
lines.add(Objects.toString(o));
}
return lines;
}
public static class Builder {
private final ErrorContext context;
public Builder() {
context = new ErrorContext();
}
public Builder whatToDo(String whatToDo) {
context.whatToDo = whatToDo;
return this;
}
public Builder related(Object related) {
context.related.add(related);
return this;
}
public Builder related(Object... related) {
context.related.addAll(Arrays.asList(related));
return this;
}
public ErrorContext build() {
return context;
}
}
}

View File

@ -0,0 +1,242 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.utilities.logging;
import com.djrapitops.plan.PlanPlugin;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.identification.properties.ServerProperties;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.utilities.java.Lists;
import com.djrapitops.plan.version.VersionChecker;
import com.djrapitops.plugin.logging.L;
import com.djrapitops.plugin.logging.console.PluginLogger;
import com.djrapitops.plugin.logging.error.ErrorHandler;
import dagger.Lazy;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* New logger that logs errors to specific files.
*
* @author Rsl1122
*/
@Singleton
public class ErrorLogger implements ErrorHandler {
private final PlanPlugin plugin;
private final PluginLogger logger;
private final PlanFiles files;
private final Lazy<ServerProperties> serverProperties;
private final Lazy<VersionChecker> versionChecker;
private final Lazy<Formatters> formatters;
@Inject
public ErrorLogger(
PlanPlugin plugin,
PluginLogger logger,
PlanFiles files,
Lazy<ServerProperties> serverProperties,
Lazy<VersionChecker> versionChecker,
Lazy<Formatters> formatters
) {
this.plugin = plugin;
this.logger = logger;
this.files = files;
this.serverProperties = serverProperties;
this.versionChecker = versionChecker;
this.formatters = formatters;
}
public void log(L level, Throwable throwable, ErrorContext context) {
String errorName = throwable.getClass().getSimpleName();
String hash = hash(throwable);
Path logsDir = files.getLogsDirectory();
Path errorLog = logsDir.resolve(errorName + "-" + hash + ".txt");
if (Files.exists(errorLog)) {
logExisting(errorLog, throwable, context);
} else {
logNew(errorLog, throwable, context);
}
logToConsole(level, errorLog, throwable, context);
if (L.CRITICAL == level) {
plugin.getPluginLogger().error("CRITICAL error triggered a plugin shutdown.");
plugin.onDisable();
}
}
private void logExisting(Path errorLog, Throwable throwable, ErrorContext context) {
// Read existing
List<String> lines;
try (Stream<String> read = Files.lines(errorLog)) {
lines = read.collect(Collectors.toList());
} catch (IOException e) {
logAfterReadError(errorLog, throwable, context);
return;
}
int occurrences = getOccurrences(lines) + 1;
List<String> newLines = buildNewLines(context, lines, occurrences);
overwrite(errorLog, throwable, newLines);
}
private void overwrite(Path errorLog, Throwable throwable, List<String> newLines) {
try {
Files.write(errorLog, newLines, StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) {
throwable.addSuppressed(e);
Logger.getGlobal().log(Level.SEVERE, "Failed to log Plan error, see suppressed.", throwable);
}
}
private List<String> buildNewLines(ErrorContext context, List<String> lines, int occurrences) {
Lists.Builder<String> builder = Lists.builder(String.class)
.add("Last occurred: " + getTimeStamp() + " Occurrences: " + occurrences);
// 5 contexts are enough.
if (occurrences <= 5) {
builder = buildContext(context, occurrences, builder);
}
int lineCount = lines.size();
int firstContextLineIndex = findFirstContextLine(lines, lineCount);
return builder.addAll(lines.subList(firstContextLineIndex, lineCount))
.build();
}
private String getTimeStamp() {
return formatters.get().iso8601NoClockLong().apply(System.currentTimeMillis());
}
private Lists.Builder<String> buildContext(ErrorContext context, int occurrences, Lists.Builder<String> builder) {
return builder.add("---- Context " + occurrences + " ----")
.add("Plan v" + versionChecker.get().getCurrentVersion())
.add(serverProperties.get().getName() + " " + serverProperties.get().getVersion())
.add("Server v" + serverProperties.get().getImplVersion())
.add("")
.addAll(context.toLines())
.add("");
}
private void logAfterReadError(Path errorLog, Throwable throwable, ErrorContext context) {
logger.error("Failed to read " + errorLog + " deleting file");
try {
Files.deleteIfExists(errorLog);
} catch (IOException ioException) {
logger.error("Failed to delete " + errorLog);
}
logNew(errorLog, throwable, context);
}
private int getOccurrences(List<String> lines) {
String occurLine = lines.get(0);
return Integer.parseInt(StringUtils.splitByWholeSeparator(occurLine, ": ")[2].trim());
}
private int findFirstContextLine(List<String> lines, int lineCount) {
int firstContextLineIndex = 0;
for (int i = 0; i < lineCount; i++) {
if (lines.get(i).contains("---- Context")) {
firstContextLineIndex = i;
break;
}
}
return firstContextLineIndex;
}
private void logToConsole(L level, Path errorLog, Throwable throwable, ErrorContext context) {
String errorName = throwable.getClass().getSimpleName();
String errorMsg = throwable.getMessage();
String errorLocation = errorLog.toString();
logger.log(level,
"Ran into " + errorName + " - logged to " + errorLocation,
"(INCLUDE CONTENTS OF THE FILE IN ANY REPORTS)",
context.getWhatToDo().map(td -> "What to do: " + td).orElse("Error msg: \"" + errorMsg + "\"")
);
}
private void logNew(Path errorLog, Throwable throwable, ErrorContext context) {
List<String> stacktrace = buildReadableStacktrace(throwable);
List<String> lines = Lists.builder(String.class)
.add("Last occurred: " + getTimeStamp() + " Occurrences: 1")
.apply(builder -> this.buildContext(context, 1, builder))
.add("---- Stacktrace ----")
.addAll(stacktrace)
.build();
writeNew(errorLog, throwable, lines);
}
private void writeNew(Path errorLog, Throwable throwable, List<String> lines) {
try {
Files.createDirectories(errorLog.getParent());
Files.write(errorLog, lines, StandardOpenOption.CREATE_NEW, StandardOpenOption.APPEND);
} catch (IOException e) {
throwable.addSuppressed(e);
Logger.getGlobal().log(Level.SEVERE, "Failed to log Plan error, see suppressed.", throwable);
}
}
@Override
@Deprecated
public void log(L level, Class caughtBy, Throwable throwable) {
log(level, throwable, ErrorContext.builder()
.related("Caught by " + caughtBy.getName())
.build());
}
private String hash(Throwable e) {
StringBuilder seed = new StringBuilder();
Throwable cause = e;
Set<String> alreadyPresent = new HashSet<>();
while (cause != null) {
for (StackTraceElement element : cause.getStackTrace()) {
String asLine = element.toString();
if (!alreadyPresent.contains(asLine)) {
seed.append(asLine);
}
alreadyPresent.add(asLine);
}
cause = e.getCause();
}
return DigestUtils.sha256Hex(seed.toString()).substring(0, 10);
}
private List<String> buildReadableStacktrace(Throwable e) {
List<String> trace = new ArrayList<>();
trace.add(e.toString());
for (StackTraceElement element : e.getStackTrace()) {
trace.add(" " + element);
}
Throwable cause = e.getCause();
if (cause != null) {
trace.add("Caused by:");
trace.addAll(buildReadableStacktrace(cause));
}
return trace;
}
}

View File

@ -15,7 +15,7 @@ Plugin:
Create_new_locale_file_on_next_enable: false
Debug: false
Dev: false
Delete_logs_after_days: 2
Delete_logs_after_days: 7
Update_notifications:
# Display update notification on the website
Check_for_updates: true

View File

@ -14,7 +14,7 @@ Plugin:
Create_new_locale_file_on_next_enable: false
Debug: false
Dev: false
Delete_logs_after_days: 2
Delete_logs_after_days: 7
Update_notifications:
# Display update notification on the website
Check_for_updates: true