mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2025-01-01 22:18:25 +01:00
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:
parent
5fc9980419
commit
d09a4016d3
@ -17,8 +17,6 @@
|
|||||||
package com.djrapitops.plan.delivery.webserver;
|
package com.djrapitops.plan.delivery.webserver;
|
||||||
|
|
||||||
import com.djrapitops.plan.SubSystem;
|
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.PlanConfig;
|
||||||
import com.djrapitops.plan.settings.config.paths.PluginSettings;
|
import com.djrapitops.plan.settings.config.paths.PluginSettings;
|
||||||
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
|
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
|
||||||
@ -57,8 +55,7 @@ public class WebServer implements SubSystem {
|
|||||||
private final PlanFiles files;
|
private final PlanFiles files;
|
||||||
private final PlanConfig config;
|
private final PlanConfig config;
|
||||||
|
|
||||||
private final ServerProperties serverProperties;
|
private final Addresses addresses;
|
||||||
private Addresses addresses;
|
|
||||||
private final RequestHandler requestHandler;
|
private final RequestHandler requestHandler;
|
||||||
|
|
||||||
private final PluginLogger logger;
|
private final PluginLogger logger;
|
||||||
@ -75,7 +72,6 @@ public class WebServer implements SubSystem {
|
|||||||
Locale locale,
|
Locale locale,
|
||||||
PlanFiles files,
|
PlanFiles files,
|
||||||
PlanConfig config,
|
PlanConfig config,
|
||||||
ServerInfo serverInfo,
|
|
||||||
Addresses addresses,
|
Addresses addresses,
|
||||||
PluginLogger logger,
|
PluginLogger logger,
|
||||||
ErrorHandler errorHandler,
|
ErrorHandler errorHandler,
|
||||||
@ -84,7 +80,6 @@ public class WebServer implements SubSystem {
|
|||||||
this.locale = locale;
|
this.locale = locale;
|
||||||
this.files = files;
|
this.files = files;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.serverProperties = serverInfo.getServerProperties();
|
|
||||||
this.addresses = addresses;
|
this.addresses = addresses;
|
||||||
|
|
||||||
this.requestHandler = requestHandler;
|
this.requestHandler = requestHandler;
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package com.djrapitops.plan.modules;
|
package com.djrapitops.plan.modules;
|
||||||
|
|
||||||
import com.djrapitops.plan.PlanPlugin;
|
import com.djrapitops.plan.PlanPlugin;
|
||||||
|
import com.djrapitops.plan.utilities.logging.ErrorLogger;
|
||||||
import com.djrapitops.plugin.IPlugin;
|
import com.djrapitops.plugin.IPlugin;
|
||||||
import com.djrapitops.plugin.benchmarking.Timings;
|
import com.djrapitops.plugin.benchmarking.Timings;
|
||||||
import com.djrapitops.plugin.command.ColorScheme;
|
import com.djrapitops.plugin.command.ColorScheme;
|
||||||
@ -70,8 +71,8 @@ public class APFModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
ErrorHandler provideErrorHandler(IPlugin plugin) {
|
ErrorHandler provideErrorHandler(ErrorLogger errorLogger) {
|
||||||
return plugin.getErrorHandler();
|
return errorLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
@ -59,7 +59,7 @@ public class PlanFiles implements SubSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Path getCustomizationDirectory() {
|
public Path getCustomizationDirectory() {
|
||||||
return dataFolder.toPath().resolve("web");
|
return getDataDirectory().resolve("web");
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getLogsFolder() {
|
public File getLogsFolder() {
|
||||||
@ -72,6 +72,10 @@ public class PlanFiles implements SubSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Path getLogsDirectory() {
|
||||||
|
return getDataDirectory().resolve("logs");
|
||||||
|
}
|
||||||
|
|
||||||
public File getConfigFile() {
|
public File getConfigFile() {
|
||||||
return configFile;
|
return configFile;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package com.djrapitops.plan.utilities.java;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Methods that can be used as functional interfaces when dealing with Maps.
|
* Methods that can be used as functional interfaces when dealing with Maps.
|
||||||
@ -35,6 +36,10 @@ public class Lists {
|
|||||||
return new ArrayList<>();
|
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()).
|
* Efficient replacement for List#stream().filter(keep).collect(Collectors.toList()).
|
||||||
*
|
*
|
||||||
@ -85,4 +90,30 @@ public class Lists {
|
|||||||
}
|
}
|
||||||
return mapped;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ Plugin:
|
|||||||
Create_new_locale_file_on_next_enable: false
|
Create_new_locale_file_on_next_enable: false
|
||||||
Debug: false
|
Debug: false
|
||||||
Dev: false
|
Dev: false
|
||||||
Delete_logs_after_days: 2
|
Delete_logs_after_days: 7
|
||||||
Update_notifications:
|
Update_notifications:
|
||||||
# Display update notification on the website
|
# Display update notification on the website
|
||||||
Check_for_updates: true
|
Check_for_updates: true
|
||||||
|
@ -14,7 +14,7 @@ Plugin:
|
|||||||
Create_new_locale_file_on_next_enable: false
|
Create_new_locale_file_on_next_enable: false
|
||||||
Debug: false
|
Debug: false
|
||||||
Dev: false
|
Dev: false
|
||||||
Delete_logs_after_days: 2
|
Delete_logs_after_days: 7
|
||||||
Update_notifications:
|
Update_notifications:
|
||||||
# Display update notification on the website
|
# Display update notification on the website
|
||||||
Check_for_updates: true
|
Check_for_updates: true
|
||||||
|
Loading…
Reference in New Issue
Block a user