Logging Strategy¶
Guidelines for consistent, useful logging across the Smartsapp backend.
Stack¶
- Framework: SLF4J + Logback (Spring Boot default)
- Structured output: logstash-logback-encoder for JSON in non-dev profiles
- Config file:
backend/src/main/resources/logback-spring.xml
Output Formats¶
| Profile | Format | Use case |
|---|---|---|
default, dev |
Human-readable console with colours | Local development |
| All others | JSON (Logstash format) | Deployed environments — parseable by Loki, ELK, Datadog, etc. |
MDC Context (Automatic)¶
Every authenticated request automatically includes these fields in log output via backend/src/main/java/com/smartsapp/system/config/JwtAuthFilter.java:
| MDC Key | Source | Example |
|---|---|---|
requestId |
RequestIdFilter — client-sent X-Request-Id header, or auto-generated UUID |
a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
userId |
JWT user_id claim |
5237 |
schoolId |
JWT school_id claim |
6 |
userName |
JWT user_name claim |
[email protected] |
traceId |
OpenTelemetry (auto) | 64f2b4a8e3d1... |
spanId |
OpenTelemetry (auto) | a1b2c3d4... |
These appear in every log line — no manual action needed. In JSON mode, they are top-level fields.
Note: requestId and traceId serve different purposes. requestId is a client-facing correlation ID (echoed in X-Request-Id response header and in error bodies). traceId is the OTel distributed trace ID (echoed in X-Trace-Id response header, propagated across Kafka).
Where to Log¶
Always log¶
| Location | Level | What to log |
|---|---|---|
| Auth rejection (JwtAuthFilter) | WARN |
Missing/malformed token + HTTP method + path |
| Business rule violations (service layer) | WARN |
Rule that blocked the action + entity context |
| Entity creation/deletion (service layer) | INFO |
Entity type + ID. Not the full payload. |
| Kafka publish success (event producers) | INFO |
Topic + event type + entity ID |
| Kafka publish failure (event producers) | ERROR |
Topic + exception |
| Unexpected exceptions (GlobalExceptionHandler) | ERROR |
Full stack trace |
| Expected errors (404, 400) (GlobalExceptionHandler) | WARN |
Error message, no stack trace |
| Startup/seed events (DataSeeder) | INFO |
What was created |
Don't log¶
- Every repository/DB call — use
spring.jpa.show-sqlor OTel JDBC tracing instead - Request/response bodies — security risk (tokens, PII). Use
DEBUGlevel at most. - Happy-path controller methods — the OTel trace already captures these
- Read operations (finders/getters) — too frequent. Rely on metrics.
When to use each level¶
| Level | Meaning | Examples |
|---|---|---|
ERROR |
Something broke that shouldn't have. Needs investigation. | Unhandled exception, Kafka connection lost, DB timeout |
WARN |
Expected failure or degraded operation. | 401 auth rejection, business rule violation, 404 not found |
INFO |
Noteworthy state change. | Entity created/deleted, seed data loaded, app started |
DEBUG |
Useful for local debugging. Never leave on in production. | Request payloads, SQL queries, cache hits/misses |
How to Log¶
Use SLF4J directly — no Lombok @Slf4j:
private static final Logger log = LoggerFactory.getLogger(MyService.class);
// Good — structured parameters
log.info("School created: id={}", school.getId());
log.warn("Insufficient balance: studentId={}, required={}, available={}", studentId, required, available);
log.error("Failed to publish event to {}", topic, exception);
// Bad — string concatenation
log.info("School created: " + school.getId()); // Don't do this
Rules¶
- Use parameterised messages (
{}placeholders), never string concatenation - Pass exceptions as the last argument — SLF4J handles stack trace printing
- Don't log sensitive data — no tokens, passwords, full request bodies
- Don't log and rethrow — either log it or let it propagate to GlobalExceptionHandler, not both
- Keep messages grep-friendly — use consistent prefixes like
"School created:","Auth rejected:"