From 6f95d49791407784e5342ee6d88ac530f8760b56 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 12:12:16 +0200 Subject: [PATCH 01/12] Add logstash logback encoder dependency --- build.gradle | 5 +++++ gradle/libs.versions.toml | 2 ++ 2 files changed, 7 insertions(+) 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" From a77a5688eb6b638c9c9f253aa5dbe47fa8d99ce7 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 15:15:37 +0200 Subject: [PATCH 02/12] Configure structured logging --- .../stickerify/bot/Stickerify.java | 18 +++++------ .../stickerify/media/MediaHelper.java | 14 ++++----- src/main/resources/logback.xml | 30 +++++++++++++++++-- .../junit/TempFilesCleanupExtension.java | 4 +-- 4 files changed, 45 insertions(+), 21 deletions(-) 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..e374cad1 100644 --- a/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java +++ b/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java @@ -78,7 +78,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()); + LOGGER.atInfo().addKeyValue("request_description", request.getDescription()).log("Received request"); answer(request); } @@ -89,7 +89,7 @@ public int process(List updates) { @Override public void onException(TelegramException e) { - LOGGER.atError().log("There was an unexpected failure: {}", e.getMessage()); + LOGGER.atError().addKeyValue("exception_message", e.getMessage()).log("There was an unexpected failure"); } @Override @@ -176,7 +176,7 @@ private void processFailure(TelegramRequest request, BaseException e, String fil } if (e instanceof CorruptedFileException) { - LOGGER.atInfo().log("Unable to reply to the {}: the file is corrupted", request.getDescription()); + LOGGER.atInfo().addKeyValue("request_description", request.getDescription()).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); @@ -186,11 +186,11 @@ private void processFailure(TelegramRequest request, BaseException e, String fil private void processTelegramFailure(String requestDescription, TelegramApiException e, boolean logUnmatchedFailure) { 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.atInfo().addKeyValue("request_description", requestDescription).log("Unable to reply to the request: the message sent has been deleted"); + case "Forbidden: bot was blocked by the user" -> LOGGER.atInfo().addKeyValue("request_description", requestDescription).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.atError().setCause(e).addKeyValue("request_description", requestDescription).log("Unable to reply to the request"); } } } @@ -199,7 +199,7 @@ private void processTelegramFailure(String requestDescription, TelegramApiExcept 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.atInfo().addKeyValue("request_message", message).log("An unhandled message type has been received"); } answerText(request.getAnswerMessage(), request); @@ -234,10 +234,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.atInfo().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.atError().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/media/MediaHelper.java b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java index dabdcc3f..3789e6d2 100644 --- a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java @@ -90,7 +90,7 @@ public final class MediaHelper { return convertToWebp(inputFile); } } catch (MediaException e) { - LOGGER.atWarn().setCause(e).log("The file with {} MIME type could not be converted", mimeType); + LOGGER.atWarn().setCause(e).addKeyValue("mime_type", mimeType).log("The file with {} MIME type could not be converted", mimeType); throw e; } @@ -107,11 +107,11 @@ public final class MediaHelper { private static String detectMimeType(File file) throws MediaException { try { var mimeType = TIKA.detect(file); - LOGGER.atDebug().log("The file has {} MIME type", mimeType); + LOGGER.atDebug().addKeyValue("mime_type", mimeType).log("MIME type successfully detected"); return mimeType; } catch (IOException e) { - LOGGER.atError().log("Unable to retrieve MIME type for file {}", file.getName()); + LOGGER.atError().addKeyValue("file_name", file.getName()).log("Unable to retrieve MIME type"); throw new MediaException(e); } } @@ -241,7 +241,7 @@ 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()); + LOGGER.atError().addKeyValue("file_name", file.getName()).log("Unable to retrieve gzip content"); } try { @@ -255,7 +255,7 @@ private static boolean isAnimatedStickerCompliant(File file, String mimeType) th } } - LOGGER.atWarn().log("The {} doesn't meet Telegram's requirements", sticker); + LOGGER.atWarn().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"); } @@ -436,7 +436,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.atInfo().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 +485,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.atWarn().setCause(e).addKeyValue("file_name", logFileName).log("Could not delete log file"); } } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 375aae4e..dc392aea 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,4 +1,6 @@ + + @@ -8,9 +10,31 @@ - - + + + + + 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..a3a4ac3e 100644 --- a/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java +++ b/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java @@ -40,9 +40,9 @@ private void deleteFile(Path path) { try { Files.delete(path); - LOGGER.atTrace().log("The file {} has been deleted", path.getFileName()); + LOGGER.atTrace().addKeyValue("file_name", path.getFileName()).log("The file has been deleted"); } catch (IOException e) { - LOGGER.atWarn().setCause(e).log("The file {} could not be deleted from the system", path.getFileName()); + LOGGER.atWarn().setCause(e).addKeyValue("file_name", path.getFileName()).log("The file could not be deleted from the system"); } } } From 1eaae5da29540d18317b0b973355951e108e05ac Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 15:20:04 +0200 Subject: [PATCH 03/12] Remove old log configuration and classes --- .../logger/ExceptionHighlighter.java | 40 --------- .../stickerify/logger/HighlightHelper.java | 46 ----------- .../stickerify/logger/MessageHighlighter.java | 47 ----------- .../stickerify/logger/package-info.java | 4 - src/main/resources/logback.xml | 45 ++++------ .../logger/ExceptionHighlighterTest.java | 69 ---------------- .../stickerify/logger/LoggingEvent.java | 63 -------------- .../logger/MessageHighlighterTest.java | 82 ------------------- 8 files changed, 18 insertions(+), 378 deletions(-) delete mode 100644 src/main/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighter.java delete mode 100644 src/main/java/com/github/stickerifier/stickerify/logger/HighlightHelper.java delete mode 100644 src/main/java/com/github/stickerifier/stickerify/logger/MessageHighlighter.java delete mode 100644 src/main/java/com/github/stickerifier/stickerify/logger/package-info.java delete mode 100644 src/test/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighterTest.java delete mode 100644 src/test/java/com/github/stickerifier/stickerify/logger/LoggingEvent.java delete mode 100644 src/test/java/com/github/stickerifier/stickerify/logger/MessageHighlighterTest.java 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/package-info.java b/src/main/java/com/github/stickerifier/stickerify/logger/package-info.java deleted file mode 100644 index 4cff5325..00000000 --- a/src/main/java/com/github/stickerifier/stickerify/logger/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@NullMarked -package com.github.stickerifier.stickerify.logger; - -import org.jspecify.annotations.NullMarked; diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index dc392aea..68b1f0c1 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,23 +1,14 @@ - + - - - - - - %highlight([%d{"dd/MM/YYYY HH:mm:ss.SSS' CET'", CET}] %-5level) %boldCyan([%-10t] %-11logger{0}) %boldYellow(-) %msg%n%red(%ex) - - - - - - - - UTC - - - + + + + + UTC + + + { @@ -25,16 +16,16 @@ } - + - - - - + + + + - - - + + + - + 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))); - } -} From 28b2e144507bf435ee9f56269b59bdef26b145ac Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 16:17:02 +0200 Subject: [PATCH 04/12] Use ScopedValue to pass userId information in logs --- .../stickerify/bot/Stickerify.java | 68 +++++++++++++------ .../telegram/model/TelegramRequest.java | 2 +- 2 files changed, 50 insertions(+), 20 deletions(-) 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 e374cad1..7dc3d827 100644 --- a/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java +++ b/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java @@ -33,6 +33,7 @@ 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; @@ -54,6 +55,7 @@ public record Stickerify(TelegramBot bot, Executor executor) implements UpdatesL private static final Logger LOGGER = LoggerFactory.getLogger(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(); + public final static ScopedValue USER = ScopedValue.newInstance(); /** * Instantiate the bot processing requests with virtual threads. @@ -78,9 +80,7 @@ public int process(List updates) { updates.forEach(update -> executor.execute(() -> { if (update.message() != null) { var request = new TelegramRequest(update.message()); - LOGGER.atInfo().addKeyValue("request_description", request.getDescription()).log("Received request"); - - answer(request); + ScopedValue.where(USER, request.getUserId()).run(() -> answer(request)); } })); @@ -89,7 +89,7 @@ public int process(List updates) { @Override public void onException(TelegramException e) { - LOGGER.atError().addKeyValue("exception_message", e.getMessage()).log("There was an unexpected failure"); + log(Level.ERROR, "exception_message", e.getMessage(), "There was an unexpected failure"); } @Override @@ -104,22 +104,52 @@ public void close() { } private void answer(TelegramRequest request) { + log(Level.INFO, "Received request"); + var file = request.getFile(); - if (file != null) { - answerFile(request, file); - } else { + if (file == null) { answerText(request); + } else { + answerFile(request, file); } } + private static void log(Level level, String message) { + LOGGER.atLevel(level) + .addKeyValue("user_id", USER.get()) + .log(message); + } + + private static void log(Level level, String message, Throwable cause) { + LOGGER.atLevel(level) + .setCause(cause) + .addKeyValue("user_id", USER.get()) + .log(message); + } + + private static void log(Level level, String key, Object value, String message) { + LOGGER.atLevel(level) + .addKeyValue("user_id", USER.get()) + .addKeyValue(key, value) + .log(message); + } + + private static void log(Level level, String key, Object value, String message, Throwable cause) { + LOGGER.atLevel(level) + .setCause(cause) + .addKeyValue("user_id", USER.get()) + .addKeyValue(key, value) + .log(message); + } + private void answerFile(TelegramRequest request, TelegramFile file) { if (file == TelegramFile.NOT_SUPPORTED) { answerText(ERROR, request); } else if (file.canBeDownloaded()) { answerFile(request, file.id()); } else { - LOGGER.atInfo().log("Passed-in file is too large"); + log(Level.INFO, "Passed-in file is too large"); answerText(FILE_TOO_LARGE, request); } @@ -172,25 +202,25 @@ private File retrieveFile(String fileId) throws TelegramApiException, FileOperat private void processFailure(TelegramRequest request, BaseException e, String fileId) { if (e instanceof TelegramApiException telegramException) { - processTelegramFailure(request.getDescription(), telegramException, false); + processTelegramFailure(telegramException, false); } if (e instanceof CorruptedFileException) { - LOGGER.atInfo().addKeyValue("request_description", request.getDescription()).log("Unable to reply to the request: the file is corrupted"); + log(Level.INFO, "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); + log(Level.WARN, "file_id", fileId, "Unable to process file", e); answerText(ERROR, request); } } - private void processTelegramFailure(String requestDescription, TelegramApiException e, boolean logUnmatchedFailure) { + private void processTelegramFailure(TelegramApiException e, boolean logUnmatchedFailure) { switch (e.getDescription()) { - case "Bad Request: message to be replied not found" -> LOGGER.atInfo().addKeyValue("request_description", requestDescription).log("Unable to reply to the request: the message sent has been deleted"); - case "Forbidden: bot was blocked by the user" -> LOGGER.atInfo().addKeyValue("request_description", requestDescription).log("Unable to reply to the request: the user blocked the bot"); + case "Bad Request: message to be replied not found" -> log(Level.INFO, "Unable to reply to the request: the message sent has been deleted"); + case "Forbidden: bot was blocked by the user" -> log(Level.INFO, "Unable to reply to the request: the user blocked the bot"); default -> { if (logUnmatchedFailure) { - LOGGER.atError().setCause(e).addKeyValue("request_description", requestDescription).log("Unable to reply to the request"); + log(Level.ERROR, "Unable to reply to the request", e); } } } @@ -199,7 +229,7 @@ private void processTelegramFailure(String requestDescription, TelegramApiExcept private void answerText(TelegramRequest request) { var message = request.message(); if (message.text() == null) { - LOGGER.atInfo().addKeyValue("request_message", message).log("An unhandled message type has been received"); + log(Level.INFO, "An unhandled message type has been received"); } answerText(request.getAnswerMessage(), request); @@ -216,7 +246,7 @@ private void answerText(Answer answer, TelegramRequest request) { try { execute(answerWithText); } catch (TelegramApiException e) { - processTelegramFailure(request.getDescription(), e, true); + processTelegramFailure(e, true); } } @@ -234,10 +264,10 @@ private static void deleteTempFiles(Set pathsToDelete) { for (var path : pathsToDelete) { try { if (!Files.deleteIfExists(path)) { - LOGGER.atInfo().addKeyValue("file_path", path).log("Unable to delete temp file"); + log(Level.INFO, "file_path", path, "Unable to delete temp file"); } } catch (IOException e) { - LOGGER.atError().setCause(e).addKeyValue("file_path", path).log("An error occurred trying to delete temp file"); + log(Level.ERROR, "file_path", path, "An error occurred trying to delete temp file", e); } } } 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..4a267e4d 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 @@ -93,7 +93,7 @@ public String getDescription() { return description; } - private Long getUserId() { + public Long getUserId() { return message.from().id(); } From b467d644ac0e75e472d55fb75d871e03df5b1e15 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 16:34:00 +0200 Subject: [PATCH 05/12] Make logger method generic --- .../stickerify/bot/Stickerify.java | 55 ++++++------------- 1 file changed, 16 insertions(+), 39 deletions(-) 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 7dc3d827..3e6e3a2a 100644 --- a/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java +++ b/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java @@ -34,6 +34,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; +import org.slf4j.spi.LoggingEventBuilder; import java.io.File; import java.io.IOException; @@ -55,7 +56,7 @@ public record Stickerify(TelegramBot bot, Executor executor) implements UpdatesL private static final Logger LOGGER = LoggerFactory.getLogger(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(); - public final static ScopedValue USER = ScopedValue.newInstance(); + public final static ScopedValue USER_ID = ScopedValue.newInstance(); /** * Instantiate the bot processing requests with virtual threads. @@ -80,7 +81,7 @@ public int process(List updates) { updates.forEach(update -> executor.execute(() -> { if (update.message() != null) { var request = new TelegramRequest(update.message()); - ScopedValue.where(USER, request.getUserId()).run(() -> answer(request)); + ScopedValue.where(USER_ID, request.getUserId()).run(() -> answer(request)); } })); @@ -89,7 +90,7 @@ public int process(List updates) { @Override public void onException(TelegramException e) { - log(Level.ERROR, "exception_message", e.getMessage(), "There was an unexpected failure"); + logger(Level.ERROR).addKeyValue("exception_message", e.getMessage()).log("There was an unexpected failure"); } @Override @@ -104,7 +105,7 @@ public void close() { } private void answer(TelegramRequest request) { - log(Level.INFO, "Received request"); + logger(Level.INFO).log("Received request"); var file = request.getFile(); @@ -115,32 +116,8 @@ private void answer(TelegramRequest request) { } } - private static void log(Level level, String message) { - LOGGER.atLevel(level) - .addKeyValue("user_id", USER.get()) - .log(message); - } - - private static void log(Level level, String message, Throwable cause) { - LOGGER.atLevel(level) - .setCause(cause) - .addKeyValue("user_id", USER.get()) - .log(message); - } - - private static void log(Level level, String key, Object value, String message) { - LOGGER.atLevel(level) - .addKeyValue("user_id", USER.get()) - .addKeyValue(key, value) - .log(message); - } - - private static void log(Level level, String key, Object value, String message, Throwable cause) { - LOGGER.atLevel(level) - .setCause(cause) - .addKeyValue("user_id", USER.get()) - .addKeyValue(key, value) - .log(message); + private static LoggingEventBuilder logger(Level level) { + return LOGGER.atLevel(level).addKeyValue("user_id", USER_ID.get()); } private void answerFile(TelegramRequest request, TelegramFile file) { @@ -149,7 +126,7 @@ private void answerFile(TelegramRequest request, TelegramFile file) { } else if (file.canBeDownloaded()) { answerFile(request, file.id()); } else { - log(Level.INFO, "Passed-in file is too large"); + logger(Level.INFO).log("Passed-in file is too large"); answerText(FILE_TOO_LARGE, request); } @@ -206,21 +183,21 @@ private void processFailure(TelegramRequest request, BaseException e, String fil } if (e instanceof CorruptedFileException) { - log(Level.INFO, "Unable to reply to the request: the file is corrupted"); + logger(Level.INFO).log("Unable to reply to the request: the file is corrupted"); answerText(CORRUPTED, request); } else { - log(Level.WARN, "file_id", fileId, "Unable to process file", e); + logger(Level.WARN).setCause(e).addKeyValue("file_id", fileId).log("Unable to process file"); answerText(ERROR, request); } } private void processTelegramFailure(TelegramApiException e, boolean logUnmatchedFailure) { switch (e.getDescription()) { - case "Bad Request: message to be replied not found" -> log(Level.INFO, "Unable to reply to the request: the message sent has been deleted"); - case "Forbidden: bot was blocked by the user" -> log(Level.INFO, "Unable to reply to the request: the user blocked the bot"); + case "Bad Request: message to be replied not found" -> logger(Level.INFO).log("Unable to reply to the request: the message sent has been deleted"); + case "Forbidden: bot was blocked by the user" -> logger(Level.INFO).log("Unable to reply to the request: the user blocked the bot"); default -> { if (logUnmatchedFailure) { - log(Level.ERROR, "Unable to reply to the request", e); + logger(Level.ERROR).setCause(e).log("Unable to reply to the request"); } } } @@ -229,7 +206,7 @@ private void processTelegramFailure(TelegramApiException e, boolean logUnmatched private void answerText(TelegramRequest request) { var message = request.message(); if (message.text() == null) { - log(Level.INFO, "An unhandled message type has been received"); + logger(Level.INFO).log("An unhandled message type has been received"); } answerText(request.getAnswerMessage(), request); @@ -264,10 +241,10 @@ private static void deleteTempFiles(Set pathsToDelete) { for (var path : pathsToDelete) { try { if (!Files.deleteIfExists(path)) { - log(Level.INFO, "file_path", path, "Unable to delete temp file"); + logger(Level.INFO).addKeyValue("file_path", path).log("Unable to delete temp file"); } } catch (IOException e) { - log(Level.ERROR, "file_path", path, "An error occurred trying to delete temp file", e); + logger(Level.ERROR).setCause(e).addKeyValue("file_path", path).log("An error occurred trying to delete temp file"); } } } From 888a05ba13433dcb85818e6f4a1c0322544b9ca0 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 17:21:13 +0200 Subject: [PATCH 06/12] Extract logging logic to record --- .../stickerify/bot/Stickerify.java | 34 ++++++++----------- .../stickerify/logger/StructuredLogger.java | 29 ++++++++++++++++ .../stickerify/logger/package-info.java | 4 +++ .../stickerify/media/MediaHelper.java | 32 ++++++++--------- .../stickerify/process/ProcessHelper.java | 8 ++--- .../junit/TempFilesCleanupExtension.java | 10 +++--- 6 files changed, 72 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java create mode 100644 src/main/java/com/github/stickerifier/stickerify/logger/package-info.java 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 3e6e3a2a..8f0c2dd5 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.USER_ID; 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; @@ -14,6 +15,7 @@ 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,10 +33,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 org.slf4j.spi.LoggingEventBuilder; import java.io.File; import java.io.IOException; @@ -53,10 +52,9 @@ */ 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(); - public final static ScopedValue USER_ID = ScopedValue.newInstance(); /** * Instantiate the bot processing requests with virtual threads. @@ -90,7 +88,7 @@ public int process(List updates) { @Override public void onException(TelegramException e) { - logger(Level.ERROR).addKeyValue("exception_message", e.getMessage()).log("There was an unexpected failure"); + LOGGER.at(Level.ERROR).addKeyValue("exception_message", e.getMessage()).log("There was an unexpected failure"); } @Override @@ -105,7 +103,7 @@ public void close() { } private void answer(TelegramRequest request) { - logger(Level.INFO).log("Received request"); + LOGGER.at(Level.INFO).log("Received request"); var file = request.getFile(); @@ -116,17 +114,13 @@ private void answer(TelegramRequest request) { } } - private static LoggingEventBuilder logger(Level level) { - return LOGGER.atLevel(level).addKeyValue("user_id", USER_ID.get()); - } - private void answerFile(TelegramRequest request, TelegramFile file) { if (file == TelegramFile.NOT_SUPPORTED) { answerText(ERROR, request); } else if (file.canBeDownloaded()) { answerFile(request, file.id()); } else { - logger(Level.INFO).log("Passed-in file is too large"); + LOGGER.at(Level.INFO).log("Passed-in file is too large"); answerText(FILE_TOO_LARGE, request); } @@ -183,21 +177,21 @@ private void processFailure(TelegramRequest request, BaseException e, String fil } if (e instanceof CorruptedFileException) { - logger(Level.INFO).log("Unable to reply to the request: the file is corrupted"); + LOGGER.at(Level.INFO).log("Unable to reply to the request: the file is corrupted"); answerText(CORRUPTED, request); } else { - logger(Level.WARN).setCause(e).addKeyValue("file_id", fileId).log("Unable to process file"); + LOGGER.at(Level.WARN).setCause(e).addKeyValue("file_id", fileId).log("Unable to process file"); answerText(ERROR, request); } } private void processTelegramFailure(TelegramApiException e, boolean logUnmatchedFailure) { switch (e.getDescription()) { - case "Bad Request: message to be replied not found" -> logger(Level.INFO).log("Unable to reply to the request: the message sent has been deleted"); - case "Forbidden: bot was blocked by the user" -> logger(Level.INFO).log("Unable to reply to the request: the user blocked the bot"); + 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(Level.ERROR).setCause(e).log("Unable to reply to the request"); + LOGGER.at(Level.ERROR).setCause(e).log("Unable to reply to the request"); } } } @@ -206,7 +200,7 @@ private void processTelegramFailure(TelegramApiException e, boolean logUnmatched private void answerText(TelegramRequest request) { var message = request.message(); if (message.text() == null) { - logger(Level.INFO).log("An unhandled message type has been received"); + LOGGER.at(Level.INFO).log("An unhandled message type has been received"); } answerText(request.getAnswerMessage(), request); @@ -241,10 +235,10 @@ private static void deleteTempFiles(Set pathsToDelete) { for (var path : pathsToDelete) { try { if (!Files.deleteIfExists(path)) { - logger(Level.INFO).addKeyValue("file_path", path).log("Unable to delete temp file"); + LOGGER.at(Level.INFO).addKeyValue("file_path", path).log("Unable to delete temp file"); } } catch (IOException e) { - logger(Level.ERROR).setCause(e).addKeyValue("file_path", path).log("An error occurred trying to delete temp file"); + 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/StructuredLogger.java b/src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java new file mode 100644 index 00000000..01e8154c --- /dev/null +++ b/src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java @@ -0,0 +1,29 @@ +package com.github.stickerifier.stickerify.logger; + +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 USER_ID = ScopedValue.newInstance(); + public static final ScopedValue MIME_TYPE = ScopedValue.newInstance(); + + public StructuredLogger(Class clazz) { + this(LoggerFactory.getLogger(clazz)); + } + + public LoggingEventBuilder at(Level level) { + var logBuilder = logger.atLevel(level); + + if (USER_ID.isBound()) { + logBuilder = logBuilder.addKeyValue("user_id", USER_ID.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/logger/package-info.java b/src/main/java/com/github/stickerifier/stickerify/logger/package-info.java new file mode 100644 index 00000000..4cff5325 --- /dev/null +++ b/src/main/java/com/github/stickerifier/stickerify/logger/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package com.github.stickerifier.stickerify.logger; + +import org.jspecify.annotations.NullMarked; 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 3789e6d2..f88db2b4 100644 --- a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java @@ -17,6 +17,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 +25,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 +37,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(); @@ -69,7 +69,7 @@ public final class MediaHelper { 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,20 +77,20 @@ 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).addKeyValue("mime_type", mimeType).log("The file with {} MIME type could not be converted", mimeType); + LOGGER.at(Level.WARN).setCause(e).addKeyValue("mime_type", mimeType).log("The file with {} MIME type could not be converted", mimeType); throw e; } @@ -107,11 +107,11 @@ public final class MediaHelper { private static String detectMimeType(File file) throws MediaException { try { var mimeType = TIKA.detect(file); - LOGGER.atDebug().addKeyValue("mime_type", mimeType).log("MIME type successfully detected"); + LOGGER.at(Level.DEBUG).addKeyValue("mime_type", mimeType).log("MIME type successfully detected"); return mimeType; } catch (IOException e) { - LOGGER.atError().addKeyValue("file_name", file.getName()).log("Unable to retrieve MIME type"); + LOGGER.at(Level.ERROR).addKeyValue("file_name", file.getName()).log("Unable to retrieve MIME type"); throw new MediaException(e); } } @@ -241,7 +241,7 @@ 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().addKeyValue("file_name", file.getName()).log("Unable to retrieve gzip content"); + LOGGER.at(Level.ERROR).addKeyValue("file_name", file.getName()).log("Unable to retrieve gzip content"); } try { @@ -255,9 +255,9 @@ private static boolean isAnimatedStickerCompliant(File file, String mimeType) th } } - LOGGER.atWarn().addKeyValue("sticker", sticker).log("The animated sticker doesn't meet Telegram's requirements"); + 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 +305,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 +331,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 +436,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().addKeyValue("file_path", file.toPath()).log("Unable to delete file"); + 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 +485,7 @@ private static File convertToWebm(File file) throws MediaException, InterruptedE try { deleteFile(new File(logFileName)); } catch (FileOperationException e) { - LOGGER.atWarn().setCause(e).addKeyValue("file_name", logFileName).log("Could not delete log file"); + 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/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java b/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java index a3a4ac3e..025d1f27 100644 --- a/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java +++ b/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java @@ -1,9 +1,9 @@ package com.github.stickerifier.stickerify.junit; +import com.github.stickerifier.stickerify.logger.StructuredLogger; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; import java.io.IOException; import java.nio.file.Files; @@ -15,7 +15,7 @@ */ public class TempFilesCleanupExtension implements AfterAllCallback { - private static final Logger LOGGER = LoggerFactory.getLogger(TempFilesCleanupExtension.class); + private static final StructuredLogger LOGGER = new StructuredLogger(TempFilesCleanupExtension.class); @Override public void afterAll(ExtensionContext context) throws IOException { @@ -40,9 +40,9 @@ private void deleteFile(Path path) { try { Files.delete(path); - LOGGER.atTrace().addKeyValue("file_name", path.getFileName()).log("The file has been deleted"); + LOGGER.at(Level.TRACE).addKeyValue("file_name", path.getFileName()).log("The file has been deleted"); } catch (IOException e) { - LOGGER.atWarn().setCause(e).addKeyValue("file_name", path.getFileName()).log("The file could not be deleted from the system"); + LOGGER.at(Level.WARN).setCause(e).addKeyValue("file_name", path.getFileName()).log("The file could not be deleted from the system"); } } } From c78195a799f4f3c20580147941f5fb7c6b875a44 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 19:32:50 +0200 Subject: [PATCH 07/12] Refactor conversion logic to use mime type scoped value --- .../stickerify/bot/Stickerify.java | 8 +-- .../stickerify/media/MediaHelper.java | 57 +++++++++++-------- 2 files changed, 37 insertions(+), 28 deletions(-) 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 8f0c2dd5..f8ba0b38 100644 --- a/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java +++ b/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java @@ -10,10 +10,8 @@ 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; @@ -148,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); } @@ -171,7 +169,7 @@ 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(telegramException, false); } 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 f88db2b4..3d4c50ae 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; @@ -60,12 +61,41 @@ 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 { + return ScopedValue.where(MIME_TYPE, detectMimeType(inputFile)).call(() -> performConversion(inputFile, MIME_TYPE.get())); + } + + /** + * 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.at(Level.DEBUG).log("MIME type successfully detected"); + + return mimeType; + } catch (IOException e) { + LOGGER.at(Level.ERROR).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 { try { if (isSupportedVideo(mimeType)) { if (isVideoCompliant(inputFile)) { @@ -90,32 +120,13 @@ public final class MediaHelper { return convertToWebp(inputFile); } } catch (MediaException e) { - LOGGER.at(Level.WARN).setCause(e).addKeyValue("mime_type", mimeType).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.at(Level.DEBUG).addKeyValue("mime_type", mimeType).log("MIME type successfully detected"); - - return mimeType; - } catch (IOException e) { - LOGGER.at(Level.ERROR).addKeyValue("file_name", file.getName()).log("Unable to retrieve MIME type"); - throw new MediaException(e); - } - } - /** * Checks if the MIME type corresponds to one of the supported video formats. * From 730e80dd6542dcf4b0f09720404e72887d04126a Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 21:24:57 +0200 Subject: [PATCH 08/12] Remove logger from test class --- .../junit/TempFilesCleanupExtension.java | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) 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 025d1f27..4b3bc897 100644 --- a/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java +++ b/src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java @@ -1,9 +1,7 @@ package com.github.stickerifier.stickerify.junit; -import com.github.stickerifier.stickerify.logger.StructuredLogger; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import org.slf4j.event.Level; import java.io.IOException; import java.nio.file.Files; @@ -15,8 +13,6 @@ */ public class TempFilesCleanupExtension implements AfterAllCallback { - private static final StructuredLogger LOGGER = new StructuredLogger(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.at(Level.TRACE).addKeyValue("file_name", path.getFileName()).log("The file has been deleted"); - } catch (IOException e) { - LOGGER.at(Level.WARN).setCause(e).addKeyValue("file_name", path.getFileName()).log("The file could not be deleted from the system"); - } - } } From a2aae5c10cda9d44214c88418ca5b1a9cf803d96 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 21:59:42 +0200 Subject: [PATCH 09/12] Enrich logged context information --- .../stickerify/bot/Stickerify.java | 4 +-- .../stickerify/logger/StructuredLogger.java | 13 +++++++-- .../telegram/model/TelegramRequest.java | 29 ++++++++----------- 3 files changed, 24 insertions(+), 22 deletions(-) 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 f8ba0b38..11bfc738 100644 --- a/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java +++ b/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java @@ -1,6 +1,6 @@ package com.github.stickerifier.stickerify.bot; -import static com.github.stickerifier.stickerify.logger.StructuredLogger.USER_ID; +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; @@ -77,7 +77,7 @@ public int process(List updates) { updates.forEach(update -> executor.execute(() -> { if (update.message() != null) { var request = new TelegramRequest(update.message()); - ScopedValue.where(USER_ID, request.getUserId()).run(() -> answer(request)); + ScopedValue.where(REQUEST_DETAILS, request.toRequestDetails()).run(() -> answer(request)); } })); diff --git a/src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java b/src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java index 01e8154c..517a0106 100644 --- a/src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java +++ b/src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java @@ -1,5 +1,6 @@ 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; @@ -7,18 +8,24 @@ public record StructuredLogger(Logger logger) { - public static final ScopedValue USER_ID = ScopedValue.newInstance(); + 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 (USER_ID.isBound()) { - logBuilder = logBuilder.addKeyValue("user_id", USER_ID.get()); + if (REQUEST_DETAILS.isBound()) { + logBuilder = logBuilder.addKeyValue("request_details", REQUEST_DETAILS.get()); } if (MIME_TYPE.isBound()) { logBuilder = logBuilder.addKeyValue("mime_type", MIME_TYPE.get()); 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 4a267e4d..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,24 +78,12 @@ 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(); } - public Long getUserId() { - return message.from().id(); + private boolean isNewUser() { + return START_COMMAND.equals(message.text()); } public Answer getAnswerMessage() { @@ -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) {} } From 3d5eb1e49085e3f8b953b6d4be2f2e5188d155c7 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 22:27:16 +0200 Subject: [PATCH 10/12] Prevent bot answers when the message would be undeliverable --- .../stickerifier/stickerify/bot/Stickerify.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 11bfc738..9e5f8ad2 100644 --- a/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java +++ b/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java @@ -171,7 +171,10 @@ private File retrieveFile(String fileId) throws TelegramApiException, FileOperat private void processFailure(TelegramRequest request, Exception e, String fileId) { if (e instanceof TelegramApiException telegramException) { - processTelegramFailure(telegramException, false); + boolean replyToUser = processTelegramFailure(telegramException, false); + if (!replyToUser) { + return; + } } if (e instanceof CorruptedFileException) { @@ -183,7 +186,9 @@ private void processFailure(TelegramRequest request, Exception e, String fileId) } } - private void processTelegramFailure(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.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"); @@ -191,8 +196,11 @@ private void processTelegramFailure(TelegramApiException e, boolean logUnmatched if (logUnmatchedFailure) { LOGGER.at(Level.ERROR).setCause(e).log("Unable to reply to the request"); } + replyToUser = true; } } + + return replyToUser; } private void answerText(TelegramRequest request) { From 61ccd950979b48566873f835af8c6d718cda8778 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 22:34:51 +0200 Subject: [PATCH 11/12] Fix missing mime type in happy path log --- .../stickerifier/stickerify/media/MediaHelper.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 3d4c50ae..34fc29da 100644 --- a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java @@ -65,7 +65,9 @@ public final class MediaHelper { * or if the current thread is interrupted while converting a video file */ public static @Nullable File convert(File inputFile) throws Exception { - return ScopedValue.where(MIME_TYPE, detectMimeType(inputFile)).call(() -> performConversion(inputFile, MIME_TYPE.get())); + var mimeType = detectMimeType(inputFile); + + return ScopedValue.where(MIME_TYPE, mimeType).call(() -> performConversion(inputFile, mimeType)); } /** @@ -77,10 +79,7 @@ public final class MediaHelper { */ private static String detectMimeType(File file) throws MediaException { try { - var mimeType = TIKA.detect(file); - LOGGER.at(Level.DEBUG).log("MIME type successfully detected"); - - return mimeType; + return TIKA.detect(file); } catch (IOException e) { LOGGER.at(Level.ERROR).addKeyValue("file_name", file.getName()).log("Unable to retrieve MIME type"); throw new MediaException(e); @@ -96,6 +95,8 @@ private static String detectMimeType(File file) throws MediaException { * @see MediaHelper#convert(File) */ 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)) { From 515d2ec7dc8c33689cabdf4ba227bc0b4400a483 Mon Sep 17 00:00:00 2001 From: Roberto Cella Date: Sun, 12 Apr 2026 23:10:19 +0200 Subject: [PATCH 12/12] Improve failure logs --- .../com/github/stickerifier/stickerify/bot/Stickerify.java | 2 +- .../github/stickerifier/stickerify/media/MediaHelper.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 9e5f8ad2..2aa16d58 100644 --- a/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java +++ b/src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java @@ -86,7 +86,7 @@ public int process(List updates) { @Override public void onException(TelegramException e) { - LOGGER.at(Level.ERROR).addKeyValue("exception_message", e.getMessage()).log("There was an unexpected failure"); + LOGGER.at(Level.ERROR).setCause(e).addKeyValue("exception_message", e.getMessage()).log("An unexpected failure occurred"); } @Override 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 34fc29da..81d4c225 100644 --- a/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java +++ b/src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java @@ -81,7 +81,7 @@ private static String detectMimeType(File file) throws MediaException { try { return TIKA.detect(file); } catch (IOException e) { - LOGGER.at(Level.ERROR).addKeyValue("file_name", file.getName()).log("Unable to retrieve MIME type"); + LOGGER.at(Level.ERROR).setCause(e).addKeyValue("file_name", file.getName()).log("Unable to retrieve MIME type"); throw new MediaException(e); } } @@ -252,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.at(Level.ERROR).addKeyValue("file_name", file.getName()).log("Unable to retrieve gzip content"); + } catch (IOException e) { + LOGGER.at(Level.ERROR).setCause(e).addKeyValue("file_name", file.getName()).log("Unable to retrieve gzip content"); } try {