Log Level Discipline
Log Level Discipline for Production Systems
Disiplin penggunaan TRACE, DEBUG, INFO, WARN, ERROR, expected error, unexpected error, retriable error, non-retriable error, log spam, missing log, dan review checklist untuk Java/JAX-RS production systems.
Cheatsheet Observability Part 007 — Log Level Discipline
Tujuan Part Ini
Part ini membahas log level discipline: kemampuan menentukan level log yang tepat agar log benar-benar berguna untuk operasi production, incident debugging, audit investigation, dan diagnosis distributed systems.
Fokusnya bukan sekadar hafalan bahwa INFO untuk informasi dan ERROR untuk error. Fokusnya adalah bagaimana log level mempengaruhi:
- signal quality
- alert noise
- incident triage speed
- debugging evidence
- log ingestion cost
- privacy exposure
- support readiness
- production confidence
Dalam sistem enterprise Java 17+ / JAX-RS / Jakarta RESTful Web Services, log level yang salah bisa menciptakan dua masalah ekstrem:
Terlalu banyak log -> noise, cost tinggi, query lambat, alert fatigue
Terlalu sedikit log -> blind spot, incident sulit direkonstruksi, RCA lemah
Setelah membaca part ini, Anda harus mampu:
- membedakan kapan memakai TRACE, DEBUG, INFO, WARN, dan ERROR
- mengenali expected error vs unexpected error
- mengenali retriable error vs non-retriable error
- menentukan apakah sebuah log event harus operational, business, security, atau audit signal
- menghindari log spam pada retry loop, polling, queue consumer, scheduler, dan hot path
- menghindari missing log pada state transition, external call, failed dependency, dan data mutation
- mereview PR yang menambah logging dari sudut severity, usefulness, cost, privacy, dan alertability
Ringkasan Mental Model
Log level adalah severity contract antara developer dan operator.
Saat engineer membaca log production, level log memberi sinyal:
TRACE -> detail sangat rendah untuk debugging lokal/diagnostik terbatas
DEBUG -> detail teknis untuk debugging, biasanya off di production
INFO -> event operasional normal yang penting untuk memahami lifecycle
WARN -> kondisi tidak ideal, tetapi sistem masih bisa lanjut
ERROR -> operasi gagal atau sistem kehilangan kemampuan yang seharusnya berhasil
Level log bukan gaya bahasa. Level log adalah keputusan operasional.
Contoh buruk:
log.error("User entered invalid quote id");
Jika invalid quote ID adalah validasi input normal, ini bukan ERROR. Ia bisa jadi INFO atau WARN tergantung konteks dan security relevance.
Contoh lebih baik:
log.info("quote lookup rejected due to invalid format quote_id_present={} actor_id={} correlation_id={}",
quoteId != null,
actorId,
correlationId);
Contoh buruk lain:
log.info("Processing item {}", itemId);
Jika ini terjadi jutaan kali per jam dalam batch job, ia bisa menjadi log spam. Mungkin lebih baik menjadi metric counter atau debug log yang bisa diaktifkan sementara.
Log level discipline berarti setiap log menjawab pertanyaan:
Apakah event ini normal atau abnormal?
Apakah operator harus peduli?
Apakah event ini membantu rekonstruksi incident?
Apakah level ini akan menyebabkan alert/noise/cost yang tidak perlu?
Apakah field-nya aman dan cukup untuk debugging?
Log Level sebagai Operational Contract
Log level harus dipahami sebagai kontrak terhadap pembaca log.
| Level | Arti operasional | Umumnya aktif di production? | Risiko jika salah pakai |
|---|---|---|---|
| TRACE | Detail eksekusi sangat granular | Tidak | Volume ekstrem, data sensitif, noise |
| DEBUG | Detail teknis untuk debugging | Biasanya tidak | Cost tinggi, bocor internal state |
| INFO | Event normal yang penting | Ya | Jika terlalu banyak menjadi noise |
| WARN | Kondisi abnormal yang recoverable | Ya | Jika terlalu banyak menjadi alert fatigue |
| ERROR | Failure yang butuh perhatian | Ya | Jika expected error dijadikan ERROR, alert palsu |
Log level harus konsisten antar service. Jika satu service memakai ERROR untuk validation failure dan service lain memakai INFO, incident triage akan bias.
Prinsip utama
Level log harus merepresentasikan dampak operasional, bukan emosi developer.
Developer sering menulis ERROR karena code masuk catch. Ini tidak selalu benar.
Pertanyaan yang lebih baik:
Apakah operasi yang diminta gagal?
Apakah sistem kehilangan kemampuan penting?
Apakah failure memerlukan intervensi?
Apakah failure akan terlihat oleh user/customer?
Apakah retry otomatis sudah cukup?
Apakah ini expected dalam domain?
TRACE
TRACE adalah level paling detail. Biasanya digunakan untuk memahami alur internal step-by-step.
Contoh:
log.trace("entered price rule evaluation rule_id={} quote_id={}", ruleId, quoteId);
log.trace("candidate discount rule evaluated rule_id={} matched={}", ruleId, matched);
TRACE cocok untuk:
- debugging lokal
- troubleshooting sangat spesifik
- library/framework internals
- short-lived diagnostic session
- reproduksi bug yang sulit
TRACE tidak cocok untuk:
- production default
- high-throughput endpoint
- logging request/response body
- logging loop item-level
- logging SQL parameter detail
- logging token/header/session data
Failure mode TRACE
TRACE sering menjadi masalah ketika:
TRACE aktif di production terlalu lama
TRACE mengandung PII/secrets
TRACE dipakai pada loop besar
TRACE menghasilkan log lebih mahal daripada traffic utama
TRACE membuat log backend sulit dipakai saat incident
Java/JAX-RS concern
Di JAX-RS endpoint high traffic, TRACE pada filter, mapper, serializer, atau dependency client bisa menghasilkan volume besar.
Contoh berbahaya:
log.trace("request body={}", requestBody);
Risiko:
- PII leakage
- token leakage
- payload besar
- ingestion cost tinggi
- data compliance issue
Internal verification checklist
[ ] Apakah TRACE disabled by default di production?
[ ] Apakah ada runtime log level change dengan approval/process?
[ ] Apakah TRACE pernah digunakan untuk request body, response body, SQL parameter, atau token?
[ ] Apakah ada TTL/process untuk menurunkan log level setelah debugging?
[ ] Apakah log backend bisa menahan volume jika TRACE sementara diaktifkan?
[ ] Apakah TRACE event punya privacy guardrail?
DEBUG
DEBUG dipakai untuk detail teknis yang membantu developer memahami state internal, tetapi biasanya tidak dibutuhkan untuk operasi normal.
Contoh:
log.debug("pricing rule candidate selected quote_id={} rule_count={} strategy={}",
quoteId,
rules.size(),
strategyName);
DEBUG cocok untuk:
- detail branch logic
- computed internal state
- feature flag evaluation
- decision tree debugging
- dependency request composition tanpa payload sensitif
- local/staging diagnostics
DEBUG tidak cocok untuk:
- event lifecycle penting yang harus selalu tersedia
- audit evidence
- business state transition penting
- incident-critical dependency failure
- security-relevant event
Kesalahan umum
Kesalahan besar adalah menaruh event penting di DEBUG.
Contoh buruk:
log.debug("order state changed from {} to {} order_id={}", oldState, newState, orderId);
Jika state transition penting untuk audit/debugging, jangan taruh di DEBUG. Minimal INFO, atau audit log terpisah jika merupakan business evidence.
Contoh lebih baik:
log.info("order state transitioned order_id={} from_state={} to_state={} transition={} actor_type={}",
orderId,
oldState,
newState,
transitionName,
actorType);
Guarded debug logging
Untuk computation mahal, gunakan guard:
if (log.isDebugEnabled()) {
log.debug("pricing candidates quote_id={} candidates={}", quoteId, buildDebugSummary(candidates));
}
Parameterized logging mencegah string interpolation awal, tetapi tidak selalu mencegah computation argument mahal.
Buruk:
log.debug("large payload summary={}", expensiveSerialize(payload));
Lebih aman:
if (log.isDebugEnabled()) {
log.debug("large payload summary={}", safeSummary(payload));
}
Internal verification checklist
[ ] Apakah DEBUG disabled by default di production?
[ ] Apakah DEBUG tidak menyimpan event lifecycle penting?
[ ] Apakah DEBUG tidak mengandung PII, token, body lengkap, atau SQL parameter sensitif?
[ ] Apakah expensive debug argument dijaga dengan isDebugEnabled()?
[ ] Apakah runtime log level change dapat dibatasi per logger/package?
[ ] Apakah ada prosedur menyalakan DEBUG sementara saat incident?
INFO
INFO adalah level utama untuk event operasional normal yang penting.
INFO bukan tempat untuk semua hal. INFO adalah tempat untuk event yang membantu memahami lifecycle production.
Contoh event INFO yang baik:
http.request.completed
quote.pricing.started
quote.pricing.completed
order.submission.accepted
order.state.transitioned
kafka.message.published
rabbitmq.message.consumed
background.job.completed
workflow.task.completed
cache.warmup.completed
Contoh Java:
log.info("quote pricing completed quote_id={} tenant_id={} price_version={} latency_ms={} item_count={}",
quoteId,
tenantId,
priceVersion,
latencyMs,
itemCount);
INFO harus menjawab:
Apa yang terjadi?
Untuk entity mana?
Dalam konteks request/trace/correlation mana?
Berhasil atau gagal?
Berapa durasinya?
Apa outcome pentingnya?
INFO bukan log semua step
Buruk:
log.info("Entering method A");
log.info("Calling method B");
log.info("Received result from method B");
log.info("Mapping result");
log.info("Returning response");
Ini noise. Lebih baik gunakan satu lifecycle event yang padat.
Lebih baik:
log.info("quote retrieval completed quote_id={} tenant_id={} source={} latency_ms={} status={}",
quoteId,
tenantId,
source,
latencyMs,
status);
INFO untuk business lifecycle
Dalam CPQ/order management, INFO sering cocok untuk business lifecycle event:
quote.created
quote.priced
quote.submitted_for_approval
quote.approved
quote.rejected
quote.accepted
order.created
order.validated
order.decomposed
fulfillment.started
fallout.created
cancellation.requested
amendment.requested
Namun jika event tersebut harus menjadi compliance evidence, ia juga harus masuk audit log, bukan hanya application INFO log.
INFO untuk dependency lifecycle
Contoh:
log.info("kafka publish completed topic={} event_type={} event_id={} correlation_id={} latency_ms={}",
topic,
eventType,
eventId,
correlationId,
latencyMs);
Tetapi hati-hati: publish per message pada high-throughput topic bisa mahal. Untuk volume tinggi, metric + sampled log mungkin lebih tepat.
Internal verification checklist
[ ] Apakah INFO berisi lifecycle event penting, bukan method tracing?
[ ] Apakah setiap INFO punya event name atau message yang stabil?
[ ] Apakah INFO membawa correlation_id/trace_id/request_id?
[ ] Apakah business entity key seperti quote_id/order_id dicatat sesuai policy?
[ ] Apakah INFO tidak terlalu banyak pada hot path?
[ ] Apakah event volume INFO dipantau?
[ ] Apakah event yang harus audit masuk audit log terpisah?
WARN
WARN berarti kondisi abnormal atau tidak ideal, tetapi sistem masih bisa melanjutkan atau melakukan recovery.
WARN cocok untuk:
- fallback digunakan
- retry akan dilakukan
- dependency lambat tetapi belum gagal final
- invalid external event yang bisa di-skip
- cache unavailable tetapi DB fallback berhasil
- idempotency duplicate detected
- stale data detected tetapi request masih bisa diproses
- queue message redelivered
- near-capacity condition
Contoh:
log.warn("dependency call retry scheduled dependency={} operation={} attempt={} max_attempts={} reason={} correlation_id={}",
"pricing-service",
"calculate-price",
attempt,
maxAttempts,
reason,
correlationId);
WARN bukan ERROR ringan
WARN bukan untuk failure final yang membuat operasi gagal. Jika order submission gagal total, itu ERROR. Jika retry internal terjadi dan akhirnya berhasil, retry attempt bisa WARN atau DEBUG tergantung volume dan policy.
Retry logging problem
Retry loop bisa menghasilkan WARN spam.
Buruk:
for (int attempt = 1; attempt <= 5; attempt++) {
try {
callDependency();
return;
} catch (Exception ex) {
log.warn("dependency failed", ex);
}
}
Jika dependency down dan traffic tinggi, ini menghasilkan ribuan stack trace WARN.
Lebih baik:
log.warn("dependency retry scheduled dependency={} operation={} attempt={} max_attempts={} error_class={} error_code={}",
dependency,
operation,
attempt,
maxAttempts,
ex.getClass().getName(),
errorCode);
Dan hanya log stack trace pada final failure atau sampled diagnostic.
WARN untuk fallback
Fallback harus terlihat karena bisa menyembunyikan degradasi.
log.warn("cache lookup failed using database fallback cache={} operation={} key_hash={} correlation_id={}",
"redis-quote-cache",
"getQuoteSummary",
safeKeyHash,
correlationId);
Jika fallback sering terjadi, metric harus ada:
cache_fallback_total{cache="quote-cache", reason="redis_timeout"}
WARN untuk duplicate/idempotency
Duplicate event dalam messaging bisa expected.
log.warn("duplicate event ignored event_id={} event_type={} idempotency_key={} consumer={} offset={}",
eventId,
eventType,
idempotencyKey,
consumerName,
offset);
Jika duplicate adalah normal akibat at-least-once delivery, jangan jadikan ERROR.
Internal verification checklist
[ ] Apakah WARN dipakai untuk recoverable abnormal condition?
[ ] Apakah WARN tidak menjadi spam pada retry loop?
[ ] Apakah WARN punya reason, attempt, dependency, dan correlation field?
[ ] Apakah WARN yang sering terjadi punya metric?
[ ] Apakah fallback path terlihat?
[ ] Apakah duplicate/idempotency behavior tidak salah diklasifikasikan sebagai ERROR?
[ ] Apakah WARN tertentu dipakai alert? Jika iya, apakah actionable?
ERROR
ERROR berarti operasi gagal atau sistem kehilangan kemampuan yang seharusnya tersedia.
ERROR cocok untuk:
- request gagal karena unexpected exception
- dependency call gagal final setelah retry habis
- database transaction gagal
- message processing gagal dan masuk DLQ
- order lifecycle transition gagal
- audit write gagal
- security control gagal
- workflow job gagal final
- data consistency violation
- invariant violation
Contoh:
log.error("order submission failed order_id={} quote_id={} tenant_id={} error_code={} retryable={} correlation_id={}",
orderId,
quoteId,
tenantId,
errorCode,
retryable,
correlationId,
ex);
ERROR harus punya field yang cukup untuk triage:
service
operation/event
correlation_id
trace_id
request_id
tenant_id jika allowed
business key jika allowed
error_code
exception_class
retryable
dependency jika dependency failure
attempt jika retry exhausted
latency_ms jika relevant
ERROR bukan semua exception
Tidak semua exception harus ERROR.
Contoh expected validation exception:
throw new ValidationException("invalid quote status");
Jika invalid input dari user adalah expected, response 400 bukan ERROR. Biasanya cukup INFO atau WARN jika ada indikasi abuse/security.
Contoh authorization failure:
- single normal forbidden request: mungkin
INFOatau security event - repeated forbidden attempts: security signal, mungkin WARN
- auth subsystem failure: ERROR
ERROR dan stack trace
Stack trace mahal tetapi penting untuk unexpected failure.
Gunakan stack trace untuk:
- unexpected exception
- final failure after retry exhausted
- invariant violation
- unrecoverable dependency failure
- data corruption risk
Hindari stack trace penuh untuk:
- validation error normal
- expected duplicate event
- known business rule rejection
- high-frequency timeout attempt yang akan retry
- client cancellation normal
ERROR harus tidak menggandakan stack trace
Buruk:
try {
service.submit(order);
} catch (Exception ex) {
log.error("submit failed", ex);
throw ex;
}
Lalu di ExceptionMapper:
log.error("request failed", ex);
Hasilnya stack trace ganda.
Lebih baik:
- log context di boundary yang tepat
- jangan log dan rethrow tanpa menambah value
- gunakan exception mapper sebagai central error logging untuk request failure
- service layer log hanya jika ada domain/dependency context penting yang tidak tersedia di mapper
Internal verification checklist
[ ] Apakah ERROR benar-benar final failure atau unexpected failure?
[ ] Apakah expected 4xx tidak diperlakukan sebagai ERROR secara default?
[ ] Apakah stack trace tidak digandakan antar layer?
[ ] Apakah ERROR membawa error_code dan correlation_id?
[ ] Apakah dependency final failure memiliki dependency name dan operation?
[ ] Apakah retry exhausted dicatat sebagai final failure?
[ ] Apakah ERROR dipetakan ke metric/alert yang meaningful?
[ ] Apakah ERROR tidak mengandung PII/secrets dalam message atau stack trace custom?
FATAL Awareness
Beberapa framework memiliki FATAL, sebagian tidak. Dalam SLF4J standar, level umum adalah TRACE, DEBUG, INFO, WARN, ERROR.
Jika backend mendukung FATAL, gunakan sangat hati-hati.
FATAL berarti:
Process/service tidak dapat melanjutkan secara aman.
Contoh kondisi fatal:
- konfigurasi critical hilang saat startup
- schema/migration incompatible sehingga service tidak boleh running
- secret provider unavailable saat boot dan service tidak bisa authenticate dependency
- corruption detected yang membuat proses harus dihentikan
- invariant platform yang membuat semua request tidak aman
Dalam banyak Java service, kondisi fatal biasanya direpresentasikan sebagai:
ERROR log + process exit + failed readiness/startup probe
Bukan log level khusus.
Internal verification checklist
[ ] Apakah logging framework memiliki FATAL atau tidak?
[ ] Apakah startup failure dicatat jelas sebelum process exit?
[ ] Apakah readiness/startup probe mencerminkan fatal misconfiguration?
[ ] Apakah fatal condition tidak disembunyikan sebagai WARN?
[ ] Apakah orchestrator/Kubernetes menangkap restart reason?
Operational Event vs Business Event vs Security Event vs Audit Event
Log level juga bergantung pada jenis event.
| Event type | Tujuan | Contoh | Level umum |
|---|---|---|---|
| Operational event | Memahami behavior teknis service | request completed, dependency retry | INFO/WARN/ERROR |
| Business event | Memahami lifecycle domain | quote approved, order submitted | INFO + possibly audit |
| Security event | Memahami akses/ancaman | forbidden access, token failure | INFO/WARN/ERROR |
| Audit event | Compliance evidence | who changed quote status | Audit channel, bukan sekadar level |
Business event
Contoh:
log.info("quote approved quote_id={} tenant_id={} actor_id={} approval_level={} correlation_id={}",
quoteId,
tenantId,
actorId,
approvalLevel,
correlationId);
Tetapi untuk compliance:
audit_event: quote.approved
actor_id
target_entity
before_state
after_state
time
reason
source_ip
Application INFO log bukan pengganti audit log jika policy membutuhkan audit evidence.
Security event
Contoh:
log.warn("authorization denied actor_id={} action={} resource_type={} resource_id_present={} reason={} correlation_id={}",
actorId,
action,
resourceType,
resourceId != null,
reason,
correlationId);
Hati-hati jangan log token atau credential.
Internal verification checklist
[ ] Apakah event business penting masuk application log dan/atau audit log sesuai policy?
[ ] Apakah security event tidak bocor token/header/cookie?
[ ] Apakah audit event tidak hanya mengandalkan INFO log biasa?
[ ] Apakah level security event disepakati dengan security team?
[ ] Apakah business event volume tidak membanjiri log backend?
Expected Error vs Unexpected Error
Ini salah satu pemisahan paling penting.
Expected error
Expected error adalah kondisi gagal yang merupakan bagian normal dari domain atau kontrak API.
Contoh:
- invalid request body
- missing required field
- quote not found
- quote already submitted
- duplicate idempotency key
- forbidden operation karena role tidak cukup
- optimistic lock conflict yang bisa retry
- business rule rejection
Expected error tidak otomatis ERROR.
Contoh:
log.info("quote submission rejected quote_id={} reason={} actor_id={} correlation_id={}",
quoteId,
"QUOTE_ALREADY_SUBMITTED",
actorId,
correlationId);
Atau jika abnormal/fraud/security risk:
log.warn("quote submission rejected due to suspicious actor mismatch quote_id={} actor_id={} owner_id={} correlation_id={}",
quoteId,
actorId,
ownerId,
correlationId);
Unexpected error
Unexpected error adalah failure yang tidak seharusnya terjadi dalam operasi normal.
Contoh:
- NullPointerException
- database unavailable
- migration mismatch
- serialization failure karena incompatible schema
- downstream timeout setelah retry exhausted
- invariant violation
- audit persistence failed
- corrupted state transition
Unexpected error biasanya ERROR.
log.error("unexpected quote submission failure quote_id={} tenant_id={} error_code={} correlation_id={}",
quoteId,
tenantId,
"QUOTE_SUBMISSION_UNEXPECTED_FAILURE",
correlationId,
ex);
HTTP status mapping
Tidak semua 4xx adalah INFO, dan tidak semua 5xx punya penyebab yang sama.
| HTTP class | Default interpretation | Logging guidance |
|---|---|---|
| 2xx | success | INFO for lifecycle/access log, not every internal step |
| 3xx | redirect/cache behavior | Usually access log only |
| 400 | validation/client issue | INFO, sometimes WARN |
| 401 | auth missing/invalid | INFO/WARN security-aware |
| 403 | forbidden | INFO/WARN security-aware |
| 404 | not found | INFO unless suspicious/noisy |
| 409 | conflict/idempotency/state issue | INFO/WARN depending domain |
| 429 | rate limited | INFO/WARN + metric |
| 499 | client closed request | edge/access log; often not app ERROR |
| 500 | server failure | ERROR |
| 502/503/504 | gateway/upstream issue | ERROR/WARN depending boundary and final impact |
Internal verification checklist
[ ] Apakah expected 4xx tidak mencemari ERROR rate?
[ ] Apakah unexpected 5xx selalu memiliki ERROR log dan metric?
[ ] Apakah business rejection dibedakan dari technical failure?
[ ] Apakah conflict/idempotency cases punya log level yang konsisten?
[ ] Apakah client cancellation 499 tidak salah dianggap server ERROR?
Retriable Error vs Non-Retriable Error
Retry behavior harus terlihat di log, tetapi tidak boleh menciptakan noise.
Retriable error
Retriable error adalah failure yang mungkin berhasil jika dicoba lagi.
Contoh:
- transient timeout
- temporary network issue
- database connection temporarily unavailable
- broker publish timeout
- optimistic locking conflict
- rate limit dari dependency dengan backoff
Log guidance:
Intermediate retry attempt -> DEBUG/WARN depending severity and volume
Final retry exhausted -> ERROR
Retry success after failure -> INFO or metric, depending usefulness
Contoh:
log.warn("dependency retry scheduled dependency={} operation={} attempt={} max_attempts={} backoff_ms={} error_class={} correlation_id={}",
dependency,
operation,
attempt,
maxAttempts,
backoffMs,
ex.getClass().getSimpleName(),
correlationId);
Final:
log.error("dependency call failed after retries dependency={} operation={} attempts={} latency_ms={} correlation_id={}",
dependency,
operation,
attempts,
totalLatencyMs,
correlationId,
ex);
Non-retriable error
Non-retriable error tidak akan membaik dengan retry.
Contoh:
- validation error
- forbidden access
- schema incompatible
- missing required business state
- permanent downstream rejection
- invalid event version jika tidak ada compatibility handler
Log guidance:
- business non-retryable rejection: INFO/WARN
- technical non-retryable invariant violation: ERROR
Messaging context
Kafka/RabbitMQ consumer harus sangat disiplin.
Buruk:
log.error("message processing failed", ex);
throw ex; // broker redelivers forever
Lebih baik:
log.warn("message processing retry scheduled topic={} event_id={} attempt={} reason={} correlation_id={}",
topic,
eventId,
attempt,
reason,
correlationId);
Final DLQ:
log.error("message moved to dlq topic={} dlq_topic={} event_id={} attempts={} error_code={} correlation_id={}",
topic,
dlqTopic,
eventId,
attempts,
errorCode,
correlationId,
ex);
Internal verification checklist
[ ] Apakah retry attempt dan final failure dibedakan levelnya?
[ ] Apakah retry loop tidak menulis stack trace penuh setiap attempt?
[ ] Apakah retry metrics tersedia?
[ ] Apakah DLQ movement dicatat sebagai final outcome?
[ ] Apakah non-retriable business rejection tidak di-retry tanpa henti?
[ ] Apakah retryable flag ada dalam error log atau structured field?
Log Spam
Log spam adalah log yang volumenya tinggi tetapi value diagnostiknya rendah.
Sumber umum:
- log per loop item
- log per poll cycle
- log per retry attempt dengan stack trace
- log request body besar
- log every cache hit
- log every DB row
- log every message consumed di high-throughput topic
- debug enabled di production
- repeated health check logs
- noisy framework logs
Contoh log spam
for (OrderLine line : lines) {
log.info("Processing order line order_id={} line_id={}", orderId, line.id());
}
Jika order punya ribuan line dan traffic tinggi, ini buruk.
Lebih baik:
log.info("order lines processed order_id={} line_count={} failed_count={} latency_ms={}",
orderId,
lines.size(),
failedCount,
latencyMs);
Jika butuh detail gagal:
log.warn("order line processing failed order_id={} line_id={} reason={} correlation_id={}",
orderId,
failedLineId,
reason,
correlationId);
Health check spam
Buruk:
log.info("Health check called");
Health checks bisa dipanggil sangat sering oleh Kubernetes, load balancer, dan monitoring probe.
Lebih baik:
Tidak log per health check success.
Log only health check failure or state transition healthy <-> unhealthy.
Cache spam
Buruk:
log.info("Cache hit key={}", key);
Lebih baik:
cache hit/miss -> metric
cache failure/fallback -> WARN log
cache warmup completed -> INFO log
Internal verification checklist
[ ] Apakah ada INFO log di loop besar?
[ ] Apakah health check success dilog?
[ ] Apakah cache hit/miss dilog per request, bukan metric?
[ ] Apakah retry menghasilkan stack trace spam?
[ ] Apakah framework logger terlalu verbose?
[ ] Apakah top noisy logger diketahui?
[ ] Apakah log volume per service dipantau?
Missing Log
Missing log adalah kondisi ketika event penting tidak tercatat.
Missing log lebih berbahaya daripada log spam saat incident karena menciptakan blind spot.
Contoh missing log penting:
- request gagal tanpa correlation ID
- state transition tidak tercatat
- order stuck tanpa aging event
- Kafka publish berhasil/gagal tidak terlihat
- DLQ movement tidak terlihat
- dependency timeout tidak punya dependency name
- fallback path tidak terlihat
- config change tidak terlihat
- feature flag decision tidak terlihat saat incident
- audit-relevant change hanya terjadi di database tanpa audit event
Boundary yang wajib dipikirkan
HTTP inbound
HTTP outbound
DB transaction
Kafka/RabbitMQ publish
Kafka/RabbitMQ consume
Redis fallback/failure
Camunda job/task failure
background job start/end/failure
state transition
external side effect
security decision
audit-relevant mutation
Tidak semua boundary perlu INFO log detail, tetapi semua boundary penting harus observable melalui kombinasi log, metric, trace, audit, atau event.
Internal verification checklist
[ ] Apakah setiap request failure punya log dengan correlation_id?
[ ] Apakah setiap state transition penting tercatat?
[ ] Apakah setiap external side effect punya success/failure evidence?
[ ] Apakah Kafka/RabbitMQ DLQ movement terlihat?
[ ] Apakah retry exhausted terlihat?
[ ] Apakah fallback path terlihat?
[ ] Apakah audit-relevant mutation masuk audit log?
[ ] Apakah config/deployment/feature flag change bisa dikorelasikan dengan incident?
Wrong Log Level Anti-Patterns
Anti-pattern 1: Everything is ERROR
log.error("validation failed field=name");
log.error("quote not found quote_id={}", quoteId);
log.error("duplicate request ignored idempotency_key={}", key);
Dampak:
- ERROR rate meaningless
- alert fatigue
- incident triage sulit
- expected business behavior terlihat seperti outage
Anti-pattern 2: Everything is INFO
log.info("database unavailable");
log.info("message moved to dlq");
log.info("audit write failed");
Dampak:
- failure critical tersembunyi
- alert tidak terpicu
- operator mengabaikan sinyal penting
Anti-pattern 3: Log and throw everywhere
catch (Exception ex) {
log.error("failed", ex);
throw ex;
}
Dampak:
- duplicate stack trace
- query noisy
- root cause sulit ditemukan
- cost tinggi
Anti-pattern 4: Log message tanpa structured field
log.error("Order failed");
Dampak:
- tidak bisa dicari berdasarkan order_id, tenant_id, error_code, dependency, trace_id
- tidak membantu support
Anti-pattern 5: Error message sebagai metric label
Ini bukan logging langsung, tetapi sering berhubungan.
Buruk:
http_errors_total{error_message="timeout after 5032ms calling /api/order/123"}
Gunakan error code stabil:
http_errors_total{error_code="DOWNSTREAM_TIMEOUT"}
Internal verification checklist
[ ] Apakah expected business rejection tidak menjadi ERROR?
[ ] Apakah critical failure tidak hanya INFO?
[ ] Apakah stack trace tidak diduplikasi di banyak layer?
[ ] Apakah setiap ERROR punya structured fields?
[ ] Apakah error_code stabil dipakai, bukan raw exception message sebagai dimension?
Log Level di JAX-RS Request Lifecycle
Contoh lifecycle request:
NGINX / ingress
-> Servlet container
-> JAX-RS filter
-> resource method
-> service layer
-> database / Redis / messaging / downstream
-> ExceptionMapper
-> response
Request success
Biasanya cukup access log atau request completed log:
log.info("http request completed method={} route={} status={} latency_ms={} correlation_id={} trace_id={}",
method,
routeTemplate,
status,
latencyMs,
correlationId,
traceId);
Validation failure
log.info("http request rejected method={} route={} status=400 error_code={} correlation_id={}",
method,
routeTemplate,
errorCode,
correlationId);
Authorization denied
log.warn("http request forbidden method={} route={} actor_id={} action={} resource_type={} reason={} correlation_id={}",
method,
routeTemplate,
actorId,
action,
resourceType,
reason,
correlationId);
Depending on policy, single forbidden could be INFO while suspicious pattern becomes WARN. Verify internally.
Unexpected exception
log.error("http request failed method={} route={} status=500 error_code={} correlation_id={} trace_id={}",
method,
routeTemplate,
errorCode,
correlationId,
traceId,
ex);
Client cancellation
499 is often observed at NGINX/ingress level. It may not be an application ERROR.
Internal question:
Did app continue expensive work after client disconnected?
Did gateway timeout before app finished?
Is cancellation normal or due to latency regression?
Log Level for PostgreSQL/MyBatis/JPA/JDBC
Database-related log level should reflect outcome and impact.
Slow query
Slow query is not always ERROR.
log.warn("slow database query operation={} latency_ms={} threshold_ms={} rows={} correlation_id={}",
operation,
latencyMs,
thresholdMs,
rowCount,
correlationId);
If request still succeeds, WARN may be enough. If query causes request failure, final request failure is ERROR.
Pool exhaustion
Pool exhaustion usually operationally serious.
log.error("database connection pool exhausted pool={} active={} idle={} pending={} timeout_ms={} correlation_id={}",
poolName,
active,
idle,
pending,
timeoutMs,
correlationId,
ex);
Optimistic lock conflict
Often expected in concurrent systems.
log.info("optimistic lock conflict detected entity_type={} entity_id={} operation={} retryable={} correlation_id={}",
entityType,
entityId,
operation,
true,
correlationId);
If retry exhausted:
log.warn("optimistic lock retry exhausted entity_type={} entity_id={} attempts={} correlation_id={}",
entityType,
entityId,
attempts,
correlationId);
SQL logging warning
Do not enable raw SQL parameter logging in production without privacy review.
Internal verification:
[ ] Are SQL parameters logged?
[ ] Can SQL logs expose customer data, commercial pricing, identifiers, or secrets?
[ ] Is slow query correlation possible without raw parameter leakage?
Log Level for Kafka/RabbitMQ
Messaging requires extra discipline because failures can repeat.
Message consumed successfully
For low/medium volume:
log.info("message processed topic={} event_type={} event_id={} latency_ms={} correlation_id={}",
topic,
eventType,
eventId,
latencyMs,
correlationId);
For high volume, prefer metrics and sampled logs.
Redelivery
log.warn("message redelivered queue={} event_id={} redelivery_count={} correlation_id={}",
queue,
eventId,
redeliveryCount,
correlationId);
DLQ
log.error("message moved to dlq source={} dlq={} event_type={} event_id={} attempts={} error_code={} correlation_id={}",
source,
dlq,
eventType,
eventId,
attempts,
errorCode,
correlationId,
ex);
Duplicate message
If duplicate is expected due to at-least-once delivery:
log.info("duplicate message ignored event_id={} idempotency_key={} consumer={} correlation_id={}",
eventId,
idempotencyKey,
consumerName,
correlationId);
If duplicate indicates producer bug or ordering issue, WARN may be justified.
Log Level for Redis
Redis is often used as cache, lock, idempotency store, rate limiter, or stream backend.
Cache miss
Do not log every cache miss at INFO in high-throughput path. Use metric.
cache_miss_total{cache="quote-summary"}
Redis timeout with DB fallback
log.warn("redis cache unavailable using fallback operation={} cache={} key_hash={} correlation_id={}",
operation,
cacheName,
keyHash,
correlationId);
Redis lock failure
If expected contention:
log.info("distributed lock not acquired lock_name={} owner={} operation={} correlation_id={}",
lockName,
owner,
operation,
correlationId);
If lock system broken:
log.error("distributed lock operation failed lock_name={} operation={} correlation_id={}",
lockName,
operation,
correlationId,
ex);
Key privacy
Never log raw keys if they include user/order/quote/customer data unless policy explicitly permits. Prefer hash or key pattern.
Log Level for Camunda/Workflow
Workflow systems create long-running state. Log level should distinguish expected business path from stuck/failure path.
Normal task completion
log.info("workflow task completed process_instance_id={} task_type={} business_key={} latency_ms={} correlation_id={}",
processInstanceId,
taskType,
businessKey,
latencyMs,
correlationId);
Failed job with retry remaining
log.warn("workflow job failed retry scheduled process_instance_id={} job_type={} retries_left={} error_code={} correlation_id={}",
processInstanceId,
jobType,
retriesLeft,
errorCode,
correlationId);
Incident created or retries exhausted
log.error("workflow incident created process_instance_id={} activity_id={} business_key={} error_code={} correlation_id={}",
processInstanceId,
activityId,
businessKey,
errorCode,
correlationId,
ex);
Human task aging
Human task aging is usually metric/dashboard/SLO first. Log state transition or breach event, not every polling scan.
Log Level in Docker/Kubernetes/AWS/Azure/on-prem
In containerized production systems, stdout/stderr log is often collected automatically.
Important implications
Every noisy INFO becomes platform ingestion cost.
Every multiline stack trace must be parsed correctly.
Every wrong ERROR may affect alerting or dashboards.
Every missing structured field makes log query harder.
Kubernetes probe logs
Do not log successful liveness/readiness checks on every request. It creates noise.
Pod restart diagnosis
Startup failures should log enough before exit:
log.error("application startup failed reason={} config_key={} environment={} version={}",
reason,
safeConfigKey,
environment,
version,
ex);
Cloud log cost
Cloud/on-prem/hybrid setups may have different retention and ingestion cost. Log level policy must consider:
- production vs lower environment
- regulatory retention
- audit retention
- debug enablement process
- customer tenant isolation
- log access control
Internal verification checklist
[ ] Apakah stdout/stderr parsing mempertahankan level field?
[ ] Apakah stack trace multiline diparse benar?
[ ] Apakah Kubernetes probe success tidak noisy?
[ ] Apakah startup failure punya ERROR sebelum exit?
[ ] Apakah log level berbeda per environment?
[ ] Apakah cloud/on-prem retention dan cost diketahui?
Performance Concern
Logging bisa mempengaruhi performance.
Risiko:
- string construction mahal
- serialization mahal
- huge object logging
- stack trace generation
- synchronous appender blocking
- async queue saturation
- stdout backpressure
- log collector bottleneck
- GC pressure dari log allocation
Praktik aman
log.info("order submitted order_id={} item_count={} latency_ms={}",
orderId,
itemCount,
latencyMs);
Hindari:
log.info("order submitted payload={}", objectMapper.writeValueAsString(order));
Untuk exception, jangan membuat exception hanya untuk stack trace jika tidak perlu.
Buruk:
log.warn("slow path detected", new RuntimeException("stack trace"));
Internal verification checklist
[ ] Apakah hot path logging menggunakan parameterized logging?
[ ] Apakah expensive serialization dijaga?
[ ] Apakah object besar tidak dilog?
[ ] Apakah async appender queue dipantau?
[ ] Apakah logging pernah menjadi bottleneck?
[ ] Apakah GC pressure dari logging pernah terlihat?
Security and Privacy Concern
Log level tidak membenarkan logging data sensitif.
Bahkan DEBUG/TRACE di non-prod bisa bocor jika data non-prod mengandung data production-like.
Jangan log:
- password
- token
- Authorization header
- Cookie
- session ID
- API key
- raw request body sensitif
- card/payment data
- customer personal data tanpa masking
- commercial pricing detail jika policy melarang
- SQL parameters sensitif
Contoh aman:
log.warn("authorization failed actor_id={} action={} resource_type={} reason={} correlation_id={}",
actorId,
action,
resourceType,
reasonCode,
correlationId);
Bukan:
log.warn("authorization failed token={} headers={}", token, headers);
Internal verification checklist
[ ] Apakah log review mencakup PII/secrets?
[ ] Apakah lower environment juga punya secure logging policy?
[ ] Apakah debug/trace tidak bisa menyalakan payload logging sembarangan?
[ ] Apakah security team punya guideline sensitive fields?
[ ] Apakah masking/redaction utility dipakai konsisten?
Cost Concern
Log level adalah cost lever.
Volume terbesar biasanya datang dari:
- INFO lifecycle terlalu granular
- retry WARN spam
- access log tanpa sampling untuk high-throughput endpoint
- DEBUG/TRACE production
- stack trace berulang
- framework logs noisy
- health check logs
- body logging
Cost tidak hanya storage. Cost mencakup:
- ingestion
- indexing
- retention
- query latency
- dashboard rendering
- alert evaluation
- engineer time saat triage
Cost-aware rule of thumb
High-frequency normal event -> metric first, sampled log if needed
Low-frequency important event -> INFO log
Recoverable abnormal event -> WARN, possibly aggregated/sampled
Final failure -> ERROR
Audit-relevant event -> audit log with retention policy
Internal verification checklist
[ ] Apakah top log volume per logger diketahui?
[ ] Apakah high-volume INFO bisa diganti metric?
[ ] Apakah retry logs dibatasi?
[ ] Apakah DEBUG/TRACE punya approval dan TTL?
[ ] Apakah log retention sesuai value signal?
[ ] Apakah audit retention dipisahkan dari application log retention?
Correctness Concern
Wrong log level bisa merusak interpretasi sistem.
Contoh:
Validation failure dilog ERROR -> terlihat seperti service outage
DLQ movement dilog INFO -> failure critical tersembunyi
Retry attempt dilog ERROR -> error rate palsu
Audit write failure dilog WARN -> compliance risk diremehkan
Cache miss dilog WARN -> alert noise
Correctness berarti log level harus konsisten dengan:
- API contract
- business semantics
- retry semantics
- user impact
- data integrity impact
- security impact
- operational action required
Internal verification checklist
[ ] Apakah log level mencerminkan user impact?
[ ] Apakah log level mencerminkan final vs intermediate failure?
[ ] Apakah log level mencerminkan expected vs unexpected?
[ ] Apakah log level mencerminkan recoverable vs unrecoverable?
[ ] Apakah log level konsisten antar service?
PR Review Checklist
Gunakan checklist ini saat mereview perubahan logging.
[ ] Apakah log event benar-benar perlu ada?
[ ] Apakah level log sesuai dampak operasional?
[ ] Apakah expected error tidak menjadi ERROR?
[ ] Apakah unexpected/final failure tidak disembunyikan sebagai INFO/WARN?
[ ] Apakah retry attempt dan retry exhausted dibedakan?
[ ] Apakah stack trace hanya ditulis di boundary yang tepat?
[ ] Apakah log membawa correlation_id/trace_id/request_id?
[ ] Apakah business key seperti quote_id/order_id dipakai sesuai policy?
[ ] Apakah log tidak mengandung PII, secrets, token, cookie, raw body, atau SQL parameter sensitif?
[ ] Apakah log berada di hot path atau loop besar?
[ ] Apakah high-volume event lebih cocok menjadi metric?
[ ] Apakah event audit-relevant masuk audit log, bukan hanya application log?
[ ] Apakah log message stabil dan queryable?
[ ] Apakah structured fields konsisten dengan log schema?
[ ] Apakah log akan membantu incident reconstruction?
[ ] Apakah log akan meningkatkan alert noise?
[ ] Apakah ada cost impact yang perlu dipertimbangkan?
Internal Verification Checklist
Gunakan checklist ini untuk memahami standar log level di team/internal environment.
[ ] Logging framework apa yang digunakan?
[ ] Apakah ada dokumen internal logging standard?
[ ] Apakah ada definisi internal untuk INFO/WARN/ERROR?
[ ] Apakah ERROR log memicu alert atau dashboard tertentu?
[ ] Apakah expected 4xx dihitung dalam error dashboard?
[ ] Apakah runtime log level change tersedia?
[ ] Siapa yang boleh mengubah log level di production?
[ ] Apakah ada TTL atau approval untuk DEBUG/TRACE production?
[ ] Apakah top noisy loggers diketahui?
[ ] Apakah log cost dipantau per service/team?
[ ] Apakah security/privacy review diperlukan untuk log baru?
[ ] Apakah audit log dipisahkan dari application log?
[ ] Apakah incident notes pernah menyebut missing log atau noisy log?
[ ] Apakah retry/DLQ/fallback logging sudah konsisten?
[ ] Apakah service owner punya dashboard untuk error/warn trend?
Kesimpulan
Log level discipline adalah salah satu dasar observability yang paling sering diremehkan.
Logging yang baik bukan berarti banyak log. Logging yang baik berarti:
Event penting terlihat.
Event normal tidak menutupi failure.
Expected error tidak membuat noise.
Unexpected error tidak tersembunyi.
Retry dan final failure dibedakan.
Business, security, audit, dan operational event tidak dicampur sembarangan.
Log membantu incident reconstruction.
Log tidak membocorkan data sensitif.
Log tidak membuat cost meledak.
Untuk senior backend engineer, kemampuan menentukan log level yang benar adalah bagian dari production judgment.
Di part berikutnya, kita masuk ke Log Context and MDC: bagaimana correlation ID, trace ID, request ID, tenant ID, actor ID, dan business key dibawa ke setiap log event agar log bisa dikorelasikan lintas thread, service, async job, dan messaging boundary.
You just completed lesson 07 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.