From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: adabugra <57899270+adabugra@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:24:05 +0300 Subject: [PATCH] Pufferfish: Sentry diff --git a/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java b/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java new file mode 100644 index 0000000000000000000000000000000000000000..2b830cb288ceba390ed39cd33fc1ee855357a97e --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java @@ -0,0 +1,133 @@ +package gg.pufferfish.pufferfish.sentry; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import io.sentry.Breadcrumb; +import io.sentry.Sentry; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.protocol.Message; +import io.sentry.protocol.User; + +import java.util.Map; + +import me.earthme.luminol.config.modules.misc.SentryConfig; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.filter.AbstractFilter; + +public class PufferfishSentryAppender extends AbstractAppender { + + private static final org.apache.logging.log4j.Logger logger = LogManager.getLogger(PufferfishSentryAppender.class.getSimpleName()); + private static final Gson GSON = new Gson(); + private final Level logLevel; + + public PufferfishSentryAppender(Level logLevel) { + super("PufferfishSentryAdapter", new SentryFilter(), null); + this.logLevel = logLevel; + } + + @Override + public void append(LogEvent logEvent) { + if (logEvent.getLevel().isMoreSpecificThan(logLevel) && (logEvent.getThrown() != null || !SentryConfig.onlyLogThrown)) { + try { + logException(logEvent); + } catch (Exception e) { + logger.warn("Failed to log event with sentry", e); + } + } else { + try { + logBreadcrumb(logEvent); + } catch (Exception e) { + logger.warn("Failed to log event with sentry", e); + } + } + } + + private void logException(LogEvent e) { + SentryEvent event = new SentryEvent(e.getThrown()); + + Message sentryMessage = new Message(); + sentryMessage.setMessage(e.getMessage().getFormattedMessage()); + + event.setThrowable(e.getThrown()); + event.setLevel(getLevel(e.getLevel())); + event.setLogger(e.getLoggerName()); + event.setTransaction(e.getLoggerName()); + event.setExtra("thread_name", e.getThreadName()); + + boolean hasContext = e.getContextData() != null; + + if (hasContext && e.getContextData().containsKey("pufferfishsentry_playerid")) { + User user = new User(); + user.setId(e.getContextData().getValue("pufferfishsentry_playerid")); + user.setUsername(e.getContextData().getValue("pufferfishsentry_playername")); + event.setUser(user); + } + + if (hasContext && e.getContextData().containsKey("pufferfishsentry_pluginname")) { + event.setExtra("plugin.name", e.getContextData().getValue("pufferfishsentry_pluginname")); + event.setExtra("plugin.version", e.getContextData().getValue("pufferfishsentry_pluginversion")); + event.setTransaction(e.getContextData().getValue("pufferfishsentry_pluginname")); + } + + if (hasContext && e.getContextData().containsKey("pufferfishsentry_eventdata")) { + Map eventFields = GSON.fromJson((String) e.getContextData().getValue("pufferfishsentry_eventdata"), new TypeToken>() { + }.getType()); + if (eventFields != null) { + event.setExtra("event", eventFields); + } + } + + Sentry.captureEvent(event); + } + + private void logBreadcrumb(LogEvent e) { + Breadcrumb breadcrumb = new Breadcrumb(); + + breadcrumb.setLevel(getLevel(e.getLevel())); + breadcrumb.setCategory(e.getLoggerName()); + breadcrumb.setType(e.getLoggerName()); + breadcrumb.setMessage(e.getMessage().getFormattedMessage()); + + Sentry.addBreadcrumb(breadcrumb); + } + + private SentryLevel getLevel(Level level) { + return switch (level.getStandardLevel()) { + case TRACE, DEBUG -> SentryLevel.DEBUG; + case WARN -> SentryLevel.WARNING; + case ERROR -> SentryLevel.ERROR; + case FATAL -> SentryLevel.FATAL; + default -> SentryLevel.INFO; + }; + } + + private static class SentryFilter extends AbstractFilter { + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, + Object... params) { + return this.filter(logger.getName()); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) { + return this.filter(logger.getName()); + } + + @Override + public Result filter(LogEvent event) { + return this.filter(event == null ? null : event.getLoggerName()); + } + + private Result filter(String loggerName) { + return loggerName != null && loggerName.startsWith("gg.castaway.pufferfish.sentry") ? Result.DENY + : Result.NEUTRAL; + } + } +} diff --git a/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java new file mode 100644 index 0000000000000000000000000000000000000000..c01e5b5de685eca7edbe8a87732efd45d4dd2557 --- /dev/null +++ b/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java @@ -0,0 +1,44 @@ +package gg.pufferfish.pufferfish.sentry; + +import io.sentry.Sentry; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SentryManager { + + private static final Logger logger = LogManager.getLogger(SentryManager.class); + + private SentryManager() { + + } + + private static boolean initialized = false; + + public static synchronized void init(Level logLevel) { + if (initialized) { + return; + } + if (logLevel == null) { + logger.error("Invalid log level, defaulting to WARN."); + logLevel = Level.WARN; + } + try { + initialized = true; + + Sentry.init(options -> { + options.setDsn(me.earthme.luminol.config.modules.misc.SentryConfig.sentryDsn); + options.setMaxBreadcrumbs(100); + }); + + PufferfishSentryAppender appender = new PufferfishSentryAppender(logLevel); + appender.start(); + ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addAppender(appender); + logger.info("Sentry logging started!"); + } catch (Exception e) { + logger.warn("Failed to initialize sentry!", e); + initialized = false; + } + } + +} diff --git a/src/main/java/me/earthme/luminol/config/modules/misc/SentryConfig.java b/src/main/java/me/earthme/luminol/config/modules/misc/SentryConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..55d6bb635b182d15471bfcd481a2b7c6ce26c00b --- /dev/null +++ b/src/main/java/me/earthme/luminol/config/modules/misc/SentryConfig.java @@ -0,0 +1,47 @@ +package me.earthme.luminol.config.modules.misc; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import me.earthme.luminol.config.ConfigInfo; +import me.earthme.luminol.config.EnumConfigCategory; +import me.earthme.luminol.config.IConfigModule; +import org.apache.logging.log4j.Level; + +public class SentryConfig implements IConfigModule { + + @ConfigInfo(baseName = "dsn", comments = + " Sentry DSN for improved error logging, leave blank to disable,\n" + + " Obtain from https://sentry.io/") + public static String sentryDsn = ""; + + @ConfigInfo(baseName = "log_level", comments = " Logs with a level higher than or equal to this level will be recorded.") + public static String logLevel = "WARN"; + + @ConfigInfo(baseName = "only_log_thrown", comments = " Only log with a Throwable will be recorded after enabling this.") + public static boolean onlyLogThrown = true; + + @Override + public EnumConfigCategory getCategory() { + return EnumConfigCategory.MISC; + } + + @Override + public String getBaseName() { + return "sentry"; + } + + @Override + public void onLoaded(CommentedFileConfig configInstance) { + String sentryEnvironment = System.getenv("SENTRY_DSN"); + + sentryDsn = sentryEnvironment != null && !sentryEnvironment.isBlank() + ? sentryEnvironment + : configInstance.getOrElse("sentry.dsn", sentryDsn); + + logLevel = configInstance.getOrElse("sentry.log-level", logLevel); + onlyLogThrown = configInstance.getOrElse("sentry.only-log-thrown", onlyLogThrown); + + if (sentryDsn != null && !sentryDsn.isBlank()) { + gg.pufferfish.pufferfish.sentry.SentryManager.init(Level.getLevel(logLevel)); + } + } +} \ No newline at end of file