| name | observability |
| description | Use when instrumenting code with Sentry error reporting, Micrometer metrics (Prometheus registry), or OpenTelemetry tracing. Also covers MDC-based logging (request_id, user_id), propagating OpenTelemetry Context across coroutine dispatcher switches, and Grafana/Prometheus dashboard config. Trigger on `span`, `meter`, `Sentry.capture`, `withTracingContext`, or logging/metrics related edits. |
Observability Patterns
๊ตฌ์ฑ ์์
| ๋๊ตฌ | ์ญํ | ์ค์ ์์น |
|---|
| Sentry | ์์ธ ์ถ์ + ์ฑ๋ฅ ํธ๋์ญ์
| ssolv-api-common: SentryConfig |
| Micrometer | JVM/์ฑ ๋ฉํธ๋ฆญ ์์ง | actuator + Prometheus endpoint |
| OpenTelemetry | ๋ถ์ฐ ํธ๋ ์ด์ฑ | withTracingContext() ์ ํธ |
| Grafana Alloy | ๋ฉํธ๋ฆญ ์์ง ์์ด์ ํธ | Instance B ์ปจํ
์ด๋ |
Sentry
์๋ ์บก์ฒ (์ด๋ฏธ ์ค์ ๋จ)
GlobalExceptionHandler์์ ๋ชจ๋ DpmException ๋ฐ ์์์น ๋ชปํ ์์ธ๋ฅผ ์๋์ผ๋ก ์บก์ฒํ๋ค.
์ ์์ธ ํธ๋ค๋ฌ๋ฅผ ์ถ๊ฐํ ๋๋ ๋ฐ๋์ Sentry.captureException(e)๋ฅผ ํฌํจํ๋ค.
@ExceptionHandler(SomeDpmException::class)
fun handleSomeDpmException(e: SomeDpmException): ResponseEntity<DpmApiResponse<Unit>> {
logger.warn("...")
io.sentry.Sentry.captureException(e)
return ResponseEntity.status(...).body(...)
}
์๋ ์บก์ฒ
io.sentry.Sentry.captureException(e)
io.sentry.Sentry.captureMessage("Max retry exceeded for message: ${entry.id}")
/actuator ํธ๋์ญ์
ํํฐ๋ง
SentryConfig.beforeSendTransactionCallback()์ด /actuator ๊ฒฝ๋ก๋ฅผ ์๋ ๋๋กญํ๋ค.
ํฌ์ค์ฒดํฌ ๋
ธ์ด์ฆ๊ฐ Sentry์ ์์ด์ง ์๋๋ค.
์ฃผ์์ฌํญ
} catch (e: Exception) {
logger.warn("...")
}
} catch (e: Exception) {
logger.warn("...")
Sentry.captureException(e)
}
OpenTelemetry ํธ๋ ์ด์ฑ
๋น์ฆ๋์ค ๋ก์ง์ด ์๋ ์๋น์ค์์ withTracingContext()๋ก ๊ฐ์ผ๋ค.
suspend fun send(meetingId: Long, userId: Long) = withTracingContext() {
val result = someRepository.findById(meetingId)
}
๋จ์ CRUD ์๋น์ค์๋ ๋ถํ์. ์ธ๋ถ API ํธ์ถ + ๋ค์ค DB ์ฟผ๋ฆฌ๊ฐ ํฌํจ๋ ๋ณตํฉ ์์
์ ์ฌ์ฉํ๋ค.
Micrometer ๋ฉํธ๋ฆญ
ํ์ฌ /actuator/prometheus ์๋ํฌ์ธํธ๋ก Grafana Alloy๊ฐ ์คํฌ๋ํํ๋ค.
์ปค์คํ
๋ฉํธ๋ฆญ ์ถ๊ฐ ํจํด
@Service
class SomeService(
private val meterRegistry: MeterRegistry,
) {
private val counter = meterRegistry.counter("ssolv.some.event.total", "type", "example")
suspend operator fun invoke(...) {
counter.increment()
}
}
๋ฉํธ๋ฆญ ์ด๋ฆ ๊ท์น: ssolv.{domain}.{action}.{unit} (์: ssolv.meeting.created.total)
๋ก๊ทธ ์์ค ๊ฐ์ด๋
private val logger = KotlinLogging.logger { }
logger.debug { "..." }
logger.info { "..." }
logger.warn { "..." }
logger.error { "..." }
์ฃผ์: Redis Stream Consumer์ ์์ธ๋ catch ํ Sentry.captureException(e)๋ก ์ฒ๋ฆฌํ๊ณ ,
ACK๋ฅผ ๋ณด๋ด์ง ์์ PEL ์ฌ์๋ ํ์ ๋จ๊ธด๋ค. logger.error๊ฐ ์๋ Sentry.captureException์ด ๊ธฐ๋ณธ.
์ด์ ํ๊ฒฝ ๋ก๊ทธ ์กฐํ
/logs app-server-a --tail 200
/logs app-server-b --tail 200
Sentry ์ด์ ๋ถ์์ /iac-audit์ฒ๋ผ ์๋ํ๋ scheduled task๊ฐ ๋งค์ผ 09:00์ ์คํ๋จ.
(ADR-021 ์ฐธ๊ณ )