Skip to content

Use structured logging#489

Open
rob93c wants to merge 12 commits intomainfrom
use-structured-logging
Open

Use structured logging#489
rob93c wants to merge 12 commits intomainfrom
use-structured-logging

Conversation

@rob93c
Copy link
Copy Markdown
Member

@rob93c rob93c commented Apr 12, 2026

Summary by CodeRabbit

  • New Features

    • Logs now emit structured JSON with per-request context and enriched fields (user IDs, MIME types, file/request metadata).
  • Refactor

    • Unified logging across the app to a structured logger API and removed legacy coloured/ANSI log-formatting and exception/message highlighters.
  • Chores

    • Added libraries to support structured logging and cleaned up obsolete log-formatting helpers and their tests.

@rob93c rob93c self-assigned this Apr 12, 2026
@rob93c rob93c added enhancement New feature or request logs This marks changes revolving around logs labels Apr 12, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Replaces ANSI console formatting with structured JSON logging: adds logstash encoder and jackson-core deps, introduces StructuredLogger with ScopedValue context, migrates logging call sites, removes ANSI highlighters and related tests, and updates Logback to emit JSON events. (50 words)

Changes

Cohort / File(s) Summary
Build & Dependency Configuration
build.gradle, gradle/libs.versions.toml
Added logstash-logback-encoder implementation and a jackson-core constraint; added jackson-core and logstash-logback-encoder entries to gradle/libs.versions.toml.
Structured Logging Core
src/main/java/.../logger/StructuredLogger.java
Added StructuredLogger record with ScopedValue fields (USER_ID, MIME_TYPE) and at(Level) helper to enrich LoggingEventBuilder.
Application Logging Integration
src/main/java/.../bot/Stickerify.java, src/main/java/.../media/MediaHelper.java, src/main/java/.../process/ProcessHelper.java
Replaced SLF4J Logger usage with StructuredLogger; switched calls to at(Level.*) and attached structured key/value context; adjusted exception handling sites and request-scoped binding in Stickerify.
Logback Configuration
src/main/resources/logback.xml
Removed ANSI converters and status listeners; replaced human-readable console appender with JSON encoder (LoggingEventCompositeJsonEncoder) and normalised log level casing.
Removed ANSI Highlighters & Helpers
src/main/java/.../logger/ExceptionHighlighter.java, src/main/java/.../logger/HighlightHelper.java, src/main/java/.../logger/MessageHighlighter.java
Deleted ANSI/colour-based Logback converters and the highlighting helper class.
Tests & Test Utilities
src/test/java/.../junit/TempFilesCleanupExtension.java, src/test/java/.../logger/ExceptionHighlighterTest.java, src/test/java/.../logger/MessageHighlighterTest.java, src/test/java/.../logger/LoggingEvent.java
Updated test logging to StructuredLogger; removed tests and test doubles targeting deleted highlighters.
API Visibility Change
src/main/java/.../telegram/model/TelegramRequest.java
Changed getUserId() visibility from private to public to support external context propagation for logging.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

dependencies, unit test

Suggested reviewers

  • MartelliEnrico

Poem

🐰
I swapped my colours for tidy JSON bright,
I bound the user id to every log at night.
No more green flashes in the stack-trace glen,
Each carrot-key is clear for humans and den. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the primary change: transitioning the codebase from standard SLF4J logging to structured logging with key-value pairs and scoped context values.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java`:
- Line 82: The current binding of ScopedValue where(USER_ID,
request.getUserId()) can NPE for channel messages because request.getUserId()
may be null; before binding, check whether request.getUserId() (or the
underlying Message.from) is non-null and only call ScopedValue.where(...) when
present, otherwise call answer(request) without the USER_ID binding (or bind an
absent/optional marker if your code expects it). Update the logic around
ScopedValue.where/answer(request) to branch: if request.getUserId() != null then
ScopedValue.where(USER_ID, request.getUserId()).run(() -> answer(request)); else
just call answer(request).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f7c0585-2644-45a3-a017-41c076183fd7

📥 Commits

Reviewing files that changed from the base of the PR and between bdffb43 and 7d5fc5c.

📒 Files selected for processing (14)
  • build.gradle
  • gradle/libs.versions.toml
  • src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java
  • src/main/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighter.java
  • src/main/java/com/github/stickerifier/stickerify/logger/HighlightHelper.java
  • src/main/java/com/github/stickerifier/stickerify/logger/MessageHighlighter.java
  • src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java
  • src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java
  • src/main/java/com/github/stickerifier/stickerify/telegram/model/TelegramRequest.java
  • src/main/resources/logback.xml
  • src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java
  • src/test/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighterTest.java
  • src/test/java/com/github/stickerifier/stickerify/logger/LoggingEvent.java
  • src/test/java/com/github/stickerifier/stickerify/logger/MessageHighlighterTest.java
💤 Files with no reviewable changes (6)
  • src/test/java/com/github/stickerifier/stickerify/logger/MessageHighlighterTest.java
  • src/main/java/com/github/stickerifier/stickerify/logger/HighlightHelper.java
  • src/main/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighter.java
  • src/test/java/com/github/stickerifier/stickerify/logger/ExceptionHighlighterTest.java
  • src/test/java/com/github/stickerifier/stickerify/logger/LoggingEvent.java
  • src/main/java/com/github/stickerifier/stickerify/logger/MessageHighlighter.java

@rob93c rob93c force-pushed the use-structured-logging branch from 7d5fc5c to 21b8daa Compare April 12, 2026 16:34
@railway-app railway-app bot temporarily deployed to Stickerify / test April 12, 2026 16:34 Inactive
@rob93c rob93c force-pushed the use-structured-logging branch from 21b8daa to 888a05b Compare April 12, 2026 17:01
@railway-app railway-app bot temporarily deployed to Stickerify / test April 12, 2026 17:01 Inactive
@railway-app railway-app bot temporarily deployed to Stickerify / test April 12, 2026 17:33 Inactive
@rob93c rob93c force-pushed the use-structured-logging branch from efa0653 to 38ef8c4 Compare April 12, 2026 17:36
@railway-app railway-app bot temporarily deployed to Stickerify / test April 12, 2026 17:36 Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java (2)

85-88: Include the exception cause in the error log for consistency.

Other error and warning logs in this file (e.g., lines 347, 501) use .setCause(e) to capture the exception details. Without it, the IOException stack trace and message are lost from the structured log output.

Proposed fix
 		} 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);
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java`
around lines 85 - 88, In the catch(IOException e) block in MediaHelper where
LOGGER.at(Level.ERROR).addKeyValue("file_name", file.getName()).log("Unable to
retrieve MIME type") is called, include the exception as the cause on the logger
(e.g., call .setCause(e) before .log(...)) so the IOException stack/message is
captured, then rethrow new MediaException(e) as before.

256-258: Consider logging the exception details before discarding.

The IOException is caught and discarded (using _), but the error log doesn't capture the exception cause. This makes it harder to diagnose why the gzip content couldn't be retrieved. If the exception is intentionally swallowed, consider logging the cause or downgrading to WARN level.

Proposed fix to capture exception details
-		} catch (IOException _) {
-			LOGGER.at(Level.ERROR).addKeyValue("file_name", file.getName()).log("Unable to retrieve gzip content");
+		} catch (IOException e) {
+			LOGGER.at(Level.WARN).setCause(e).addKeyValue("file_name", file.getName()).log("Unable to retrieve gzip content");
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java`
around lines 256 - 258, The catch block in MediaHelper swallows the IOException
and discards its details; change the catch from "catch (IOException _)" to
"catch (IOException e)" and include the exception in the log call (e.g., use
LOGGER.at(Level.ERROR).withThrowable(e).addKeyValue("file_name",
file.getName()).log("Unable to retrieve gzip content")) or, if intentionally
non-fatal, downgrade to WARN level while still attaching the exception; update
the catch site to reference the new exception variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java`:
- Around line 85-88: In the catch(IOException e) block in MediaHelper where
LOGGER.at(Level.ERROR).addKeyValue("file_name", file.getName()).log("Unable to
retrieve MIME type") is called, include the exception as the cause on the logger
(e.g., call .setCause(e) before .log(...)) so the IOException stack/message is
captured, then rethrow new MediaException(e) as before.
- Around line 256-258: The catch block in MediaHelper swallows the IOException
and discards its details; change the catch from "catch (IOException _)" to
"catch (IOException e)" and include the exception in the log call (e.g., use
LOGGER.at(Level.ERROR).withThrowable(e).addKeyValue("file_name",
file.getName()).log("Unable to retrieve gzip content")) or, if intentionally
non-fatal, downgrade to WARN level while still attaching the exception; update
the catch site to reference the new exception variable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4f5df97-90fa-4e34-8cf7-644c5c9bc596

📥 Commits

Reviewing files that changed from the base of the PR and between 888a05b and efa0653.

📒 Files selected for processing (2)
  • src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java
  • src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java (1)

88-90: Attach the throwable to the unexpected-failure log.

Line 89 only records exception_message, so you lose the exception type and stack trace exactly when diagnostics matter most.

🔧 Proposed fix
-		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("There was an unexpected failure");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java` around
lines 88 - 90, The error log in Stickerify.onException currently only records
the exception message and omits the throwable and stack trace; update the LOGGER
invocation in the onException(TelegramException e) method to attach the actual
throwable (e) to the error entry so the exception type and stack trace are
recorded — for example use the logger's withThrowable/withCause (or equivalent)
fluent method or pass the throwable as the log call's throwable parameter when
calling LOGGER.at(Level.ERROR)...log(...) so the full exception is captured
rather than only addKeyValue("exception_message", ...).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java`:
- Around line 172-193: processFailure short-circuits incorrectly: when an
exception is a TelegramApiException you call processTelegramFailure(...) but
then continue and may log/answer again; change the flow so handled Telegram
reply failures don't fall through — either make processTelegramFailure return a
boolean (e.g., handled) and in processFailure return early when handled, or have
processTelegramFailure throw/mark handled and then in processFailure check that
and return before the CorruptedFileException/else branches; update references to
processTelegramFailure and processFailure accordingly so cases like "message
deleted" or "user blocked the bot" do not lead to the extra
LOGGER.at(Level.WARN)... and answerText(ERROR, request) call.

---

Nitpick comments:
In `@src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java`:
- Around line 88-90: The error log in Stickerify.onException currently only
records the exception message and omits the throwable and stack trace; update
the LOGGER invocation in the onException(TelegramException e) method to attach
the actual throwable (e) to the error entry so the exception type and stack
trace are recorded — for example use the logger's withThrowable/withCause (or
equivalent) fluent method or pass the throwable as the log call's throwable
parameter when calling LOGGER.at(Level.ERROR)...log(...) so the full exception
is captured rather than only addKeyValue("exception_message", ...).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa78104c-d6e0-4a2f-b352-5048e4a38d28

📥 Commits

Reviewing files that changed from the base of the PR and between efa0653 and 38ef8c4.

📒 Files selected for processing (2)
  • src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java
  • src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java

@rob93c rob93c force-pushed the use-structured-logging branch from 38ef8c4 to c78195a Compare April 12, 2026 18:30
@railway-app railway-app bot temporarily deployed to Stickerify / test April 12, 2026 18:30 Inactive
@railway-app railway-app bot temporarily deployed to Stickerify / test April 12, 2026 20:00 Inactive
@railway-app railway-app bot temporarily deployed to Stickerify / test April 12, 2026 20:35 Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/main/java/com/github/stickerifier/stickerify/telegram/model/TelegramRequest.java (1)

97-120: ⚠️ Potential issue | 🔴 Critical

Handle messages without from when building RequestDetails.

toRequestDetails() now dereferences message.from().id() unconditionally. Telegram updates such as channel posts can arrive without sender data, so the new structured-log binding in src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java Line 80 can fail before the request is handled.

💡 Proposed fix
 public RequestDetails toRequestDetails() {
-	return new RequestDetails(getUserId(), isNewUser());
+	var sender = message.from();
+	return new RequestDetails(sender != null ? sender.id() : null, isNewUser());
 }
 
-public record RequestDetails(`@JsonProperty`("user_id") Long userId, `@JsonProperty`("new_user") boolean isNewUser) {}
+public record RequestDetails(`@JsonProperty`("user_id") `@Nullable` Long userId,
+		`@JsonProperty`("new_user") boolean isNewUser) {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/github/stickerifier/stickerify/telegram/model/TelegramRequest.java`
around lines 97 - 120, toRequestDetails() can throw an NPE when message.from()
is absent (channel posts), so change it to avoid dereferencing
message.from().id(); compute a nullable userId (e.g. Long userId =
Optional.ofNullable(message).map(m -> m.from()).map(from ->
from.id()).orElse(null)) and pass that into new RequestDetails(userId,
isNewUser()), ensuring RequestDetails and isNewUser() accept/handle a null user
id rather than assuming message.from() is always present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@src/main/java/com/github/stickerifier/stickerify/telegram/model/TelegramRequest.java`:
- Around line 97-120: toRequestDetails() can throw an NPE when message.from() is
absent (channel posts), so change it to avoid dereferencing message.from().id();
compute a nullable userId (e.g. Long userId = Optional.ofNullable(message).map(m
-> m.from()).map(from -> from.id()).orElse(null)) and pass that into new
RequestDetails(userId, isNewUser()), ensuring RequestDetails and isNewUser()
accept/handle a null user id rather than assuming message.from() is always
present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9dccd2bb-a51c-4f28-9623-973bd030d41c

📥 Commits

Reviewing files that changed from the base of the PR and between efa0653 and 515d2ec.

📒 Files selected for processing (5)
  • src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java
  • src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java
  • src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java
  • src/main/java/com/github/stickerifier/stickerify/telegram/model/TelegramRequest.java
  • src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/com/github/stickerifier/stickerify/junit/TempFilesCleanupExtension.java
  • src/main/java/com/github/stickerifier/stickerify/logger/StructuredLogger.java

@rob93c rob93c marked this pull request as ready for review April 12, 2026 21:21
@rob93c rob93c requested a review from MartelliEnrico April 12, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request logs This marks changes revolving around logs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant