diff --git a/build.gradle b/build.gradle index 40fe80f7..a257119d 100644 --- a/build.gradle +++ b/build.gradle @@ -15,9 +15,14 @@ dependencies { compileOnly(libs.jspecify) implementation(libs.gson) implementation(libs.logback.classic) + implementation(libs.logstash.logback.encoder) implementation(libs.telegram.bot.api) implementation(libs.tika) + constraints { + implementation(libs.jackson.core) + } + testCompileOnly(libs.jspecify) testImplementation(libs.hamcrest) testImplementation(libs.junit.jupiter) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b96fafa..eb60b592 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,10 +4,12 @@ junit = "6.0.3" [libraries] gson = "com.google.code.gson:gson:2.13.2" hamcrest = "org.hamcrest:hamcrest:3.0" +jackson-core = "tools.jackson.core:jackson-core:3.1.2" jspecify = "org.jspecify:jspecify:1.0.0" junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-platform = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" } logback-classic = "ch.qos.logback:logback-classic:1.5.32" +logstash-logback-encoder = "net.logstash.logback:logstash-logback-encoder:9.0" mockwebserver = "com.squareup.okhttp3:mockwebserver3-junit5:5.3.2" telegram-bot-api = "com.github.pengrad:java-telegram-bot-api:9.6.0" tika = "org.apache.tika:tika-core:3.3.0" diff --git a/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java b/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java index 0cd49bce..2aa16d58 100644 --- a/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java +++ b/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java @@ -1,5 +1,6 @@ package com.github.stickerifier.stickerify.bot; +import static com.github.stickerifier.stickerify.logger.StructuredLogger.REQUEST_DETAILS; import static com.github.stickerifier.stickerify.telegram.Answer.CORRUPTED; import static com.github.stickerifier.stickerify.telegram.Answer.ERROR; import static com.github.stickerifier.stickerify.telegram.Answer.FILE_ALREADY_VALID; @@ -9,11 +10,10 @@ import static java.util.HashSet.newHashSet; import static java.util.concurrent.Executors.newThreadPerTaskExecutor; -import com.github.stickerifier.stickerify.exception.BaseException; import com.github.stickerifier.stickerify.exception.CorruptedFileException; import com.github.stickerifier.stickerify.exception.FileOperationException; -import com.github.stickerifier.stickerify.exception.MediaException; import com.github.stickerifier.stickerify.exception.TelegramApiException; +import com.github.stickerifier.stickerify.logger.StructuredLogger; import com.github.stickerifier.stickerify.media.MediaHelper; import com.github.stickerifier.stickerify.telegram.Answer; import com.github.stickerifier.stickerify.telegram.model.TelegramFile; @@ -31,8 +31,7 @@ import com.pengrad.telegrambot.request.SendDocument; import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.response.BaseResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; import java.io.File; import java.io.IOException; @@ -51,7 +50,7 @@ */ public record Stickerify(TelegramBot bot, Executor executor) implements UpdatesListener, ExceptionHandler, AutoCloseable { - private static final Logger LOGGER = LoggerFactory.getLogger(Stickerify.class); + private static final StructuredLogger LOGGER = new StructuredLogger(Stickerify.class); private static final String BOT_TOKEN = System.getenv("STICKERIFY_TOKEN"); private static final ThreadFactory VIRTUAL_THREAD_FACTORY = Thread.ofVirtual().name("Virtual-", 0).factory(); @@ -78,9 +77,7 @@ public int process(List updates) { updates.forEach(update -> executor.execute(() -> { if (update.message() != null) { var request = new TelegramRequest(update.message()); - LOGGER.atInfo().log("Received {}", request.getDescription()); - - answer(request); + ScopedValue.where(REQUEST_DETAILS, request.toRequestDetails()).run(() -> answer(request)); } })); @@ -89,7 +86,7 @@ public int process(List updates) { @Override public void onException(TelegramException e) { - LOGGER.atError().log("There was an unexpected failure: {}", e.getMessage()); + LOGGER.at(Level.ERROR).setCause(e).addKeyValue("exception_message", e.getMessage()).log("An unexpected failure occurred"); } @Override @@ -104,12 +101,14 @@ public void close() { } private void answer(TelegramRequest request) { + LOGGER.at(Level.INFO).log("Received request"); + var file = request.getFile(); - if (file != null) { - answerFile(request, file); - } else { + if (file == null) { answerText(request); + } else { + answerFile(request, file); } } @@ -119,7 +118,7 @@ private void answerFile(TelegramRequest request, TelegramFile file) { } else if (file.canBeDownloaded()) { answerFile(request, file.id()); } else { - LOGGER.atInfo().log("Passed-in file is too large"); + LOGGER.at(Level.INFO).log("Passed-in file is too large"); answerText(FILE_TOO_LARGE, request); } @@ -147,10 +146,10 @@ private void answerFile(TelegramRequest request, String fileId) { execute(answerWithFile); } - } catch (TelegramApiException | MediaException e) { - processFailure(request, e, fileId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); + } catch (Exception e) { + processFailure(request, e, fileId); } finally { deleteTempFiles(pathsToDelete); } @@ -170,36 +169,44 @@ private File retrieveFile(String fileId) throws TelegramApiException, FileOperat } } - private void processFailure(TelegramRequest request, BaseException e, String fileId) { + private void processFailure(TelegramRequest request, Exception e, String fileId) { if (e instanceof TelegramApiException telegramException) { - processTelegramFailure(request.getDescription(), telegramException, false); + boolean replyToUser = processTelegramFailure(telegramException, false); + if (!replyToUser) { + return; + } } if (e instanceof CorruptedFileException) { - LOGGER.atInfo().log("Unable to reply to the {}: the file is corrupted", request.getDescription()); + LOGGER.at(Level.INFO).log("Unable to reply to the request: the file is corrupted"); answerText(CORRUPTED, request); } else { - LOGGER.atWarn().setCause(e).log("Unable to process the file {}", fileId); + LOGGER.at(Level.WARN).setCause(e).addKeyValue("file_id", fileId).log("Unable to process file"); answerText(ERROR, request); } } - private void processTelegramFailure(String requestDescription, TelegramApiException e, boolean logUnmatchedFailure) { + private boolean processTelegramFailure(TelegramApiException e, boolean logUnmatchedFailure) { + boolean replyToUser = false; + switch (e.getDescription()) { - case "Bad Request: message to be replied not found" -> LOGGER.atInfo().log("Unable to reply to the {}: the message sent has been deleted", requestDescription); - case "Forbidden: bot was blocked by the user" -> LOGGER.atInfo().log("Unable to reply to the {}: the user blocked the bot", requestDescription); + case "Bad Request: message to be replied not found" -> LOGGER.at(Level.INFO).log("Unable to reply to the request: the message sent has been deleted"); + case "Forbidden: bot was blocked by the user" -> LOGGER.at(Level.INFO).log("Unable to reply to the request: the user blocked the bot"); default -> { if (logUnmatchedFailure) { - LOGGER.atError().setCause(e).log("Unable to reply to the {}", requestDescription); + LOGGER.at(Level.ERROR).setCause(e).log("Unable to reply to the request"); } + replyToUser = true; } } + + return replyToUser; } private void answerText(TelegramRequest request) { var message = request.message(); if (message.text() == null) { - LOGGER.atInfo().log("An unhandled message type has been received: {}", message); + LOGGER.at(Level.INFO).log("An unhandled message type has been received"); } answerText(request.getAnswerMessage(), request); @@ -216,7 +223,7 @@ private void answerText(Answer answer, TelegramRequest request) { try { execute(answerWithText); } catch (TelegramApiException e) { - processTelegramFailure(request.getDescription(), e, true); + processTelegramFailure(e, true); } } @@ -234,10 +241,10 @@ private static void deleteTempFiles(Set pathsToDelete) { for (var path : pathsToDelete) { try { if (!Files.deleteIfExists(path)) { - LOGGER.atInfo().log("Unable to delete temp file {}", path); + LOGGER.at(Level.INFO).addKeyValue("file_path", path).log("Unable to delete temp file"); } } catch (IOException e) { - LOGGER.atError().setCause(e).log("An error occurred trying to delete temp file {}", path); + LOGGER.at(Level.ERROR).setCause(e).addKeyValue("file_path", path).log("An error occurred trying to delete temp file"); } } } diff --git a/src/main/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighter.java b/src/main/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighter.java deleted file mode 100644 index 23a9009a..00000000 --- a/src/main/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighter.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.stickerifier.stickerify.logger; - -import static ch.qos.logback.core.pattern.color.ANSIConstants.RED_FG; -import static ch.qos.logback.core.pattern.color.ANSIConstants.RESET; -import static com.github.stickerifier.stickerify.logger.HighlightHelper.changeColorTo; -import static com.github.stickerifier.stickerify.logger.HighlightHelper.greenHighlight; -import static com.github.stickerifier.stickerify.logger.HighlightHelper.replaceFirst; -import static com.github.stickerifier.stickerify.logger.HighlightHelper.retrieveMimeType; - -import ch.qos.logback.classic.pattern.ThrowableProxyConverter; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.pattern.Converter; - -/** - * Custom converter class to be used by Logback to highlight important substrings in exception logs. - * - * @see Converter - */ -public class ExceptionHighlighter extends ThrowableProxyConverter { - - static final String CONTINUE_RED = changeColorTo(RESET + RED_FG); - - @Override - public String convert(ILoggingEvent event) { - var fullMessage = super.convert(event); - var throwable = event.getThrowableProxy(); - - if (throwable != null && throwable.getMessage() != null) { - var exceptionMessage = throwable.getMessage(); - var mimeType = retrieveMimeType(exceptionMessage); - - if (mimeType != null) { - var highlightedMessage = replaceFirst(exceptionMessage, mimeType, greenHighlight(mimeType, CONTINUE_RED)); - return replaceFirst(fullMessage, exceptionMessage, highlightedMessage); - } - } - - return fullMessage; - } -} diff --git a/src/main/java/com/github/stickerifier/stickerify/logger/HighlightHelper.java b/src/main/java/com/github/stickerifier/stickerify/logger/HighlightHelper.java deleted file mode 100644 index 7105848d..00000000 --- a/src/main/java/com/github/stickerifier/stickerify/logger/HighlightHelper.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.stickerifier.stickerify.logger; - -import static ch.qos.logback.core.pattern.color.ANSIConstants.BOLD; -import static ch.qos.logback.core.pattern.color.ANSIConstants.ESC_END; -import static ch.qos.logback.core.pattern.color.ANSIConstants.ESC_START; -import static ch.qos.logback.core.pattern.color.ANSIConstants.GREEN_FG; - -import org.jspecify.annotations.Nullable; - -import java.util.regex.Pattern; - -public final class HighlightHelper { - - static final String START_GREEN = changeColorTo(BOLD + GREEN_FG); - private static final Pattern MIME_TYPE_PATTERN = Pattern.compile(" (\\w+/[-+.\\w]+) "); - - static String changeColorTo(final String color) { - return ESC_START + color + ESC_END; - } - - /** - * Enriches the {@code message} string with ANSI color codes to highlight it in green. - * Then, the string continues with the color specified by {@code previousColor}. - * - * @param message the message to be highlighted - * @param previousColor the color to use after the highlighted text - * @return the highlighted text - */ - static String greenHighlight(final String message, String previousColor) { - return START_GREEN + message + previousColor; - } - - static @Nullable String retrieveMimeType(final String message) { - var matcher = MIME_TYPE_PATTERN.matcher(message); - - return matcher.find() ? matcher.group(1) : null; - } - - static String replaceFirst(String message, String textToReplace, String replacement) { - return message.replaceFirst(Pattern.quote(textToReplace), replacement); - } - - private HighlightHelper() { - throw new UnsupportedOperationException(); - } -} diff --git a/src/main/java/com/github/stickerifier/stickerify/logger/MessageHighlighter.java b/src/main/java/com/github/stickerifier/stickerify/logger/MessageHighlighter.java deleted file mode 100644 index 6f3aeec5..00000000 --- a/src/main/java/com/github/stickerifier/stickerify/logger/MessageHighlighter.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.stickerifier.stickerify.logger; - -import static ch.qos.logback.core.pattern.color.ANSIConstants.BOLD; -import static ch.qos.logback.core.pattern.color.ANSIConstants.DEFAULT_FG; -import static ch.qos.logback.core.pattern.color.ANSIConstants.RESET; -import static ch.qos.logback.core.pattern.color.ANSIConstants.YELLOW_FG; -import static com.github.stickerifier.stickerify.logger.HighlightHelper.changeColorTo; -import static com.github.stickerifier.stickerify.logger.HighlightHelper.greenHighlight; -import static com.github.stickerifier.stickerify.logger.HighlightHelper.replaceFirst; -import static com.github.stickerifier.stickerify.logger.HighlightHelper.retrieveMimeType; -import static com.github.stickerifier.stickerify.telegram.model.TelegramRequest.NEW_USER; - -import ch.qos.logback.classic.pattern.MessageConverter; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.pattern.Converter; -import org.jspecify.annotations.Nullable; - -/** - * Custom converter class to be used by Logback to highlight important substrings. - * - * @see Converter - */ -public class MessageHighlighter extends MessageConverter { - - private static final String START_YELLOW = changeColorTo(BOLD + YELLOW_FG); - static final String CONTINUE_WHITE = changeColorTo(RESET + DEFAULT_FG); - static final String HIGHLIGHTED_NEW_USER = " " + START_YELLOW + NEW_USER.substring(1) + CONTINUE_WHITE; - - @Override - public @Nullable String convert(ILoggingEvent event) { - var message = event.getFormattedMessage(); - - if (message != null) { - if (message.contains(NEW_USER)) { - return replaceFirst(message, NEW_USER, HIGHLIGHTED_NEW_USER); - } - - var mimeType = retrieveMimeType(message); - if (mimeType != null) { - var highlightedMimeType = greenHighlight(mimeType, CONTINUE_WHITE); - return replaceFirst(message, mimeType, highlightedMimeType); - } - } - - return message; - } -} diff --git a/src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java b/src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java new file mode 100644 index 00000000..517a0106 --- /dev/null +++ b/src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java @@ -0,0 +1,36 @@ +package com.github.stickerifier.stickerify.logger; + +import com.github.stickerifier.stickerify.telegram.model.TelegramRequest.RequestDetails; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.slf4j.spi.LoggingEventBuilder; + +public record StructuredLogger(Logger logger) { + + public static final ScopedValue REQUEST_DETAILS = ScopedValue.newInstance(); + public static final ScopedValue MIME_TYPE = ScopedValue.newInstance(); + + public StructuredLogger(Class clazz) { + this(LoggerFactory.getLogger(clazz)); + } + + /** + * Creates a {@link LoggingEventBuilder} at the specified level with request details and MIME type information, if set. + * + * @param level the level of the log + * @return the log builder with context information + */ + public LoggingEventBuilder at(Level level) { + var logBuilder = logger.atLevel(level); + + if (REQUEST_DETAILS.isBound()) { + logBuilder = logBuilder.addKeyValue("request_details", REQUEST_DETAILS.get()); + } + if (MIME_TYPE.isBound()) { + logBuilder = logBuilder.addKeyValue("mime_type", MIME_TYPE.get()); + } + + return logBuilder; + } +} diff --git a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java index dabdcc3f..81d4c225 100644 --- a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java @@ -1,5 +1,6 @@ package com.github.stickerifier.stickerify.media; +import static com.github.stickerifier.stickerify.logger.StructuredLogger.MIME_TYPE; import static com.github.stickerifier.stickerify.media.MediaConstraints.MATROSKA_FORMAT; import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_ANIMATION_DURATION_SECONDS; import static com.github.stickerifier.stickerify.media.MediaConstraints.MAX_ANIMATION_FILE_SIZE; @@ -17,6 +18,7 @@ import com.github.stickerifier.stickerify.exception.FileOperationException; import com.github.stickerifier.stickerify.exception.MediaException; import com.github.stickerifier.stickerify.exception.ProcessException; +import com.github.stickerifier.stickerify.logger.StructuredLogger; import com.github.stickerifier.stickerify.process.OsConstants; import com.github.stickerifier.stickerify.process.ProcessHelper; import com.google.gson.Gson; @@ -24,8 +26,7 @@ import com.google.gson.annotations.SerializedName; import org.apache.tika.Tika; import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; import java.io.File; import java.io.FileInputStream; @@ -37,7 +38,7 @@ public final class MediaHelper { - private static final Logger LOGGER = LoggerFactory.getLogger(MediaHelper.class); + private static final StructuredLogger LOGGER = new StructuredLogger(MediaHelper.class); private static final Tika TIKA = new Tika(); private static final Gson GSON = new Gson(); @@ -60,16 +61,46 @@ public final class MediaHelper { * * @param inputFile the file to convert * @return a resized and converted file + * @throws Exception either if the file is not supported, if the conversion failed, + * or if the current thread is interrupted while converting a video file + */ + public static @Nullable File convert(File inputFile) throws Exception { + var mimeType = detectMimeType(inputFile); + + return ScopedValue.where(MIME_TYPE, mimeType).call(() -> performConversion(inputFile, mimeType)); + } + + /** + * Analyzes the file to detect its media type. + * + * @param file the file sent to the bot + * @return the MIME type of the passed-in file + * @throws MediaException if the file could not be read + */ + private static String detectMimeType(File file) throws MediaException { + try { + return TIKA.detect(file); + } catch (IOException e) { + LOGGER.at(Level.ERROR).setCause(e).addKeyValue("file_name", file.getName()).log("Unable to retrieve MIME type"); + throw new MediaException(e); + } + } + + /** + * @param inputFile the file to convert + * @param mimeType the MIME type of the file + * @return a resized and converted file * @throws MediaException if the file is not supported or if the conversion failed * @throws InterruptedException if the current thread is interrupted while converting a video file + * @see MediaHelper#convert(File) */ - public static @Nullable File convert(File inputFile) throws MediaException, InterruptedException { - var mimeType = detectMimeType(inputFile); + private static @Nullable File performConversion(File inputFile, String mimeType) throws MediaException, InterruptedException { + LOGGER.at(Level.DEBUG).log("MIME type successfully detected"); try { if (isSupportedVideo(mimeType)) { if (isVideoCompliant(inputFile)) { - LOGGER.atInfo().log("The video doesn't need conversion"); + LOGGER.at(Level.INFO).log("The video doesn't need conversion"); return null; } @@ -77,45 +108,26 @@ public final class MediaHelper { } if (isAnimatedStickerCompliant(inputFile, mimeType)) { - LOGGER.atInfo().log("The animated sticker doesn't need conversion"); + LOGGER.at(Level.INFO).log("The animated sticker doesn't need conversion"); return null; } if (isSupportedImage(inputFile, mimeType)) { if (isImageCompliant(inputFile, mimeType)) { - LOGGER.atInfo().log("The image doesn't need conversion"); + LOGGER.at(Level.INFO).log("The image doesn't need conversion"); return null; } return convertToWebp(inputFile); } } catch (MediaException e) { - LOGGER.atWarn().setCause(e).log("The file with {} MIME type could not be converted", mimeType); + LOGGER.at(Level.WARN).setCause(e).log("The file could not be converted"); throw e; } throw new MediaException("The file with {} MIME type is not supported", mimeType); } - /** - * Analyzes the file to detect its media type. - * - * @param file the file sent to the bot - * @return the MIME type of the passed-in file - * @throws MediaException if the file could not be read - */ - private static String detectMimeType(File file) throws MediaException { - try { - var mimeType = TIKA.detect(file); - LOGGER.atDebug().log("The file has {} MIME type", mimeType); - - return mimeType; - } catch (IOException e) { - LOGGER.atError().log("Unable to retrieve MIME type for file {}", file.getName()); - throw new MediaException(e); - } - } - /** * Checks if the MIME type corresponds to one of the supported video formats. * @@ -240,8 +252,8 @@ private static boolean isAnimatedStickerCompliant(File file, String mimeType) th try (var gzipInputStream = new GZIPInputStream(new FileInputStream(file))) { uncompressedContent = new String(gzipInputStream.readAllBytes(), UTF_8); - } catch (IOException _) { - LOGGER.atError().log("Unable to retrieve gzip content from file {}", file.getName()); + } catch (IOException e) { + LOGGER.at(Level.ERROR).setCause(e).addKeyValue("file_name", file.getName()).log("Unable to retrieve gzip content"); } try { @@ -255,9 +267,9 @@ private static boolean isAnimatedStickerCompliant(File file, String mimeType) th } } - LOGGER.atWarn().log("The {} doesn't meet Telegram's requirements", sticker); + LOGGER.at(Level.WARN).addKeyValue("sticker", sticker).log("The animated sticker doesn't meet Telegram's requirements"); } catch (JsonSyntaxException _) { - LOGGER.atInfo().log("The archive isn't an animated sticker"); + LOGGER.at(Level.INFO).log("The archive isn't an animated sticker"); } } @@ -305,7 +317,7 @@ private static boolean isAnimationCompliant(@Nullable AnimationDetails animation */ private static boolean isSupportedImage(File image, String mimeType) { if ("image/webp".equals(mimeType) && isAnimatedWebp(image)) { - LOGGER.atInfo().log("The image is an animated WebP"); + LOGGER.at(Level.INFO).log("The image is an animated WebP"); return false; } @@ -331,7 +343,7 @@ private static boolean isAnimatedWebp(File file) { return isExtendedFormat && hasAnimationFlag; } catch (IOException e) { - LOGGER.atWarn().setCause(e).log("An error occurred checking if the file is an animated WebP"); + LOGGER.at(Level.WARN).setCause(e).log("An error occurred checking if the file is an animated WebP"); return false; } } @@ -436,7 +448,7 @@ private static File createTempFile(String fileExtension) throws FileOperationExc private static void deleteFile(File file) throws FileOperationException { try { if (!Files.deleteIfExists(file.toPath())) { - LOGGER.atInfo().log("Unable to delete file {}", file.toPath()); + LOGGER.at(Level.INFO).addKeyValue("file_path", file.toPath()).log("Unable to delete file"); } } catch (IOException e) { throw new FileOperationException("An error occurred deleting the file", e); @@ -485,7 +497,7 @@ private static File convertToWebm(File file) throws MediaException, InterruptedE try { deleteFile(new File(logFileName)); } catch (FileOperationException e) { - LOGGER.atWarn().setCause(e).log("Could not delete {}", logFileName); + LOGGER.at(Level.WARN).setCause(e).addKeyValue("file_name", logFileName).log("Could not delete log file"); } } diff --git a/src/main/java/com/github/stickerifier/stickerify/process/ProcessHelper.java b/src/main/java/com/github/stickerifier/stickerify/process/ProcessHelper.java index ad720d82..be54c530 100644 --- a/src/main/java/com/github/stickerifier/stickerify/process/ProcessHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/process/ProcessHelper.java @@ -3,8 +3,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.github.stickerifier.stickerify.exception.ProcessException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.github.stickerifier.stickerify.logger.StructuredLogger; +import org.slf4j.event.Level; import java.io.IOException; import java.util.StringJoiner; @@ -13,7 +13,7 @@ public final class ProcessHelper { - private static final Logger LOGGER = LoggerFactory.getLogger(ProcessHelper.class); + private static final StructuredLogger LOGGER = new StructuredLogger(ProcessHelper.class); private static final Semaphore SEMAPHORE = new Semaphore(getMaxConcurrentProcesses()); /** @@ -41,7 +41,7 @@ public static String executeCommand(final String... command) throws ProcessExcep try (var reader = process.inputReader(UTF_8)) { reader.lines().forEach(output::add); } catch (IOException e) { - LOGGER.atError().setCause(e).log("Error while closing process output reader"); + LOGGER.at(Level.ERROR).setCause(e).log("Error while closing process output reader"); } }); diff --git a/src/main/java/com/github/stickerifier/stickerify/telegram/model/TelegramRequest.java b/src/main/java/com/github/stickerifier/stickerify/telegram/model/TelegramRequest.java index df465d9a..85b8a268 100644 --- a/src/main/java/com/github/stickerifier/stickerify/telegram/model/TelegramRequest.java +++ b/src/main/java/com/github/stickerifier/stickerify/telegram/model/TelegramRequest.java @@ -5,6 +5,7 @@ import static com.github.stickerifier.stickerify.telegram.Answer.PRIVACY_POLICY; import static java.util.Comparator.comparing; +import com.fasterxml.jackson.annotation.JsonProperty; import com.github.stickerifier.stickerify.telegram.Answer; import com.pengrad.telegrambot.model.Document; import com.pengrad.telegrambot.model.Message; @@ -25,7 +26,7 @@ * @param message the message to wrap */ public record TelegramRequest(Message message) { - public static final String NEW_USER = " (new user)"; + private static final String START_COMMAND = "/start"; private static final String HELP_COMMAND = "/help"; private static final String PRIVACY_COMMAND = "/privacy"; @@ -77,26 +78,14 @@ public Integer getMessageId() { return message.messageId(); } - /** - * Creates a String describing the current request, - * writing only the user identifier and if the sender is a new user. - * - * @return the description of the request - */ - public String getDescription() { - var description = "request from user " + getUserId(); - - if (START_COMMAND.equals(message.text())) { - description += NEW_USER; - } - - return description; - } - private Long getUserId() { return message.from().id(); } + private boolean isNewUser() { + return START_COMMAND.equals(message.text()); + } + public Answer getAnswerMessage() { return switch (message.text()) { case HELP_COMMAND, START_COMMAND -> HELP; @@ -105,6 +94,10 @@ public Answer getAnswerMessage() { }; } + public RequestDetails toRequestDetails() { + return new RequestDetails(getUserId(), isNewUser()); + } + @Override public String toString() { var file = Optional.ofNullable(getFile()).map(TelegramFile::id).orElse(null); @@ -123,4 +116,6 @@ private static String writeIfNotEmpty(String field, @Nullable String value) { ? ", " + field + "=" + value : ""; } + + public record RequestDetails(@JsonProperty("user_id") Long userId, @JsonProperty("new_user") boolean isNewUser) {} } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 375aae4e..68b1f0c1 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,16 +1,31 @@ - - + - - - %highlight([%d{"dd/MM/YYYY HH:mm:ss.SSS' CET'", CET}] %-5level) %boldCyan([%-10t] %-11logger{0}) %boldYellow(-) %msg%n%red(%ex) - - + + + + + UTC + + + + + + { + "class_name": "%logger{0}" + } + + + + + + + + - - - + + + - + diff --git a/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java b/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java index 563f04b0..4b3bc897 100644 --- a/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java +++ b/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java @@ -2,8 +2,6 @@ import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.file.Files; @@ -15,8 +13,6 @@ */ public class TempFilesCleanupExtension implements AfterAllCallback { - private static final Logger LOGGER = LoggerFactory.getLogger(TempFilesCleanupExtension.class); - @Override public void afterAll(ExtensionContext context) throws IOException { deleteTempFiles(); @@ -26,23 +22,17 @@ private void deleteTempFiles() throws IOException { var tempFolder = System.getProperty("java.io.tmpdir"); try (var files = Files.list(Path.of(tempFolder))) { - files.filter(this::stickerifyFiles).forEach(this::deleteFile); + for (var file : files.toList()) { + if (isStickerifyFile(file)) { + Files.delete(file); + } + } } } - private boolean stickerifyFiles(Path path) { + private boolean isStickerifyFile(Path path) { var fileName = path.getFileName().toString(); return Files.isRegularFile(path) && (fileName.startsWith("Stickerify-") || fileName.startsWith("OriginalFile-")); } - - private void deleteFile(Path path) { - try { - Files.delete(path); - - LOGGER.atTrace().log("The file {} has been deleted", path.getFileName()); - } catch (IOException e) { - LOGGER.atWarn().setCause(e).log("The file {} could not be deleted from the system", path.getFileName()); - } - } } diff --git a/src/test/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighterTest.java b/src/test/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighterTest.java deleted file mode 100644 index 29b96bfe..00000000 --- a/src/test/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighterTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.github.stickerifier.stickerify.logger; - -import static com.github.stickerifier.stickerify.logger.ExceptionHighlighter.CONTINUE_RED; -import static com.github.stickerifier.stickerify.logger.HighlightHelper.START_GREEN; -import static com.github.stickerifier.stickerify.logger.LoggingEvent.EXCEPTION_CLASS; -import static java.lang.System.lineSeparator; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; - -import com.github.stickerifier.stickerify.junit.Tags; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - -@Tag(Tags.LOG) -class ExceptionHighlighterTest { - - private static final String LOG_MESSAGE = "Received request"; - private static final String EXCEPTION_MESSAGE = "The video could not be processed successfully"; - private static final String MIME_TYPE = "text/plain"; - - private ExceptionHighlighter exceptionHighlighter; - - @BeforeEach - void setup() { - exceptionHighlighter = new ExceptionHighlighter(); - } - - @Test - @DisplayName("Log message without any exception") - void processEventWithoutException() { - var event = new LoggingEvent(LOG_MESSAGE); - - var convertedMessage = exceptionHighlighter.convert(event); - - assertThat(convertedMessage, is(emptyString())); - } - - @Test - @DisplayName("Log exception message without MIME type") - void processExceptionEventWithoutMimeType() { - var event = new LoggingEvent(LOG_MESSAGE, EXCEPTION_MESSAGE); - var expectedMessage = "%s: %s".formatted(EXCEPTION_CLASS, EXCEPTION_MESSAGE); - - var convertedMessage = getFirstLine(exceptionHighlighter.convert(event)); - - assertThat(convertedMessage, is(equalTo(expectedMessage))); - } - - private static String getFirstLine(String text) { - return text.split(lineSeparator())[0]; - } - - @Test - @DisplayName("Log exception message with MIME type") - void processExceptionEventWithMimeType() { - var messageFormat = "The file with %s MIME type is not supported"; - var event = new LoggingEvent(LOG_MESSAGE, messageFormat.formatted(MIME_TYPE)); - var highlightedMimeType = START_GREEN + MIME_TYPE + CONTINUE_RED; - var expectedMessage = "%s: %s".formatted(EXCEPTION_CLASS, messageFormat.formatted(highlightedMimeType)); - - var convertedMessage = getFirstLine(exceptionHighlighter.convert(event)); - - assertThat(convertedMessage, is(equalTo(expectedMessage))); - } -} diff --git a/src/test/java/com/github/stickerifier/stickerify/logger/LoggingEvent.java b/src/test/java/com/github/stickerifier/stickerify/logger/LoggingEvent.java deleted file mode 100644 index 250943cd..00000000 --- a/src/test/java/com/github/stickerifier/stickerify/logger/LoggingEvent.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.stickerifier.stickerify.logger; - -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.classic.spi.IThrowableProxy; -import ch.qos.logback.classic.spi.LoggingEventVO; -import ch.qos.logback.classic.spi.StackTraceElementProxy; -import ch.qos.logback.classic.spi.ThrowableProxyVO; -import com.github.stickerifier.stickerify.exception.TelegramApiException; -import org.jspecify.annotations.Nullable; - -/** - * Test double that serves as an implementation of {@link ILoggingEvent}. - */ -class LoggingEvent extends LoggingEventVO { - - static final String EXCEPTION_CLASS = TelegramApiException.class.getName(); - - private final String formattedMessage; - private @Nullable IThrowableProxy throwableProxy; - - LoggingEvent(String formattedMessage) { - this.formattedMessage = formattedMessage; - } - - LoggingEvent(String formattedMessage, String exceptionMessage) { - this.formattedMessage = formattedMessage; - this.throwableProxy = new ThrowableProxy(exceptionMessage); - } - - private static class ThrowableProxy extends ThrowableProxyVO { - private final String message; - - ThrowableProxy(String message) { - this.message = message; - } - - @Override - public String getMessage() { - return message; - } - - @Override - public String getClassName() { - return EXCEPTION_CLASS; - } - - @Override - public StackTraceElementProxy[] getStackTraceElementProxyArray() { - return new StackTraceElementProxy[] {}; - } - } - - @Override - public String getFormattedMessage() { - return formattedMessage; - } - - @Nullable - @Override - public IThrowableProxy getThrowableProxy() { - return throwableProxy; - } -} diff --git a/src/test/java/com/github/stickerifier/stickerify/logger/MessageHighlighterTest.java b/src/test/java/com/github/stickerifier/stickerify/logger/MessageHighlighterTest.java deleted file mode 100644 index 3ad5c5f3..00000000 --- a/src/test/java/com/github/stickerifier/stickerify/logger/MessageHighlighterTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.github.stickerifier.stickerify.logger; - -import static com.github.stickerifier.stickerify.logger.HighlightHelper.START_GREEN; -import static com.github.stickerifier.stickerify.logger.MessageHighlighter.CONTINUE_WHITE; -import static com.github.stickerifier.stickerify.logger.MessageHighlighter.HIGHLIGHTED_NEW_USER; -import static com.github.stickerifier.stickerify.telegram.model.TelegramRequest.NEW_USER; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; - -import com.github.stickerifier.stickerify.junit.Tags; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - -@Tag(Tags.LOG) -class MessageHighlighterTest { - - private static final String LOG_MESSAGE = "Received request"; - private static final String MIME_TYPE = "image/vnd.microsoft.icon"; - private static final String LOG_MESSAGE_WITH_MIME_TYPE = LOG_MESSAGE + " with " + MIME_TYPE + " MIME type"; - - private MessageHighlighter messageHighlighter; - - @BeforeEach - void setup() { - messageHighlighter = new MessageHighlighter(); - } - - @Test - @DisplayName("Log message from old user") - void processEventWithOldUser() { - var event = new LoggingEvent(LOG_MESSAGE); - - var convertedMessage = messageHighlighter.convert(event); - - assertThat(convertedMessage, is(equalTo(LOG_MESSAGE))); - } - - @Test - @DisplayName("Log message from new user") - void processEventWithNewUser() { - var event = new LoggingEvent(LOG_MESSAGE + NEW_USER); - - var convertedMessage = messageHighlighter.convert(event); - - assertThat(convertedMessage, is(equalTo(LOG_MESSAGE + HIGHLIGHTED_NEW_USER))); - } - - @Test - @DisplayName("Log message with multiple new user occurrences") - void processEventWithMultipleNewUserOccurrences() { - var event = new LoggingEvent(LOG_MESSAGE + NEW_USER + NEW_USER); - - var convertedMessage = messageHighlighter.convert(event); - - assertThat(convertedMessage, is(equalTo(LOG_MESSAGE + HIGHLIGHTED_NEW_USER + NEW_USER))); - } - - @Test - @DisplayName("Log message with MIME type") - void processEventWithMimeType() { - var event = new LoggingEvent(LOG_MESSAGE_WITH_MIME_TYPE); - var highlightedMimeType = START_GREEN + MIME_TYPE + CONTINUE_WHITE; - - var convertedMessage = messageHighlighter.convert(event); - - assertThat(convertedMessage, is(equalTo(LOG_MESSAGE + " with " + highlightedMimeType + " MIME type"))); - } - - @Test - @DisplayName("Log message with multiple MIME types") - void processEventWithMultipleMimeTypes() { - var event = new LoggingEvent(LOG_MESSAGE_WITH_MIME_TYPE + " and " + MIME_TYPE); - var highlightedMimeType = START_GREEN + MIME_TYPE + CONTINUE_WHITE; - - var convertedMessage = messageHighlighter.convert(event); - - assertThat(convertedMessage, is(equalTo(LOG_MESSAGE + " with " + highlightedMimeType + " MIME type and " + MIME_TYPE))); - } -}