Skip to content

Commit

Permalink
서버 로깅 구현 (#349)
Browse files Browse the repository at this point in the history
* feat: impl server log

* refactor: add newline

* fix: dependency graph

* test: add mocking

* refactor: log -> event

* feat: check payload type and event type

* refactor: function -> val

* fix: build

* chore: try and catch
  • Loading branch information
jyoo0515 authored Jul 17, 2024
1 parent 3d4c96e commit 5af4c47
Show file tree
Hide file tree
Showing 23 changed files with 255 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
implementation(projects.crossCuttingConcern.application.serverEvent)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package club.staircrusher.user.application.port.`in`

import club.staircrusher.application.server_event.port.`in`.SccServerEventRecorder
import club.staircrusher.domain.server_event.NewsletterSubscribedOnSignupPayload
import club.staircrusher.stdlib.clock.SccClock
import club.staircrusher.stdlib.di.annotation.Component
import club.staircrusher.stdlib.domain.SccDomainException
Expand All @@ -25,6 +27,7 @@ class UserApplicationService(
private val passwordEncryptor: PasswordEncryptor,
private val userAuthInfoRepository: UserAuthInfoRepository,
private val stibeeSubscriptionService: StibeeSubscriptionService,
private val sccServerEventRecorder: SccServerEventRecorder,
) {

@Deprecated("닉네임 로그인은 사라질 예정")
Expand Down Expand Up @@ -133,7 +136,7 @@ class UserApplicationService(

if (isNewsLetterSubscriptionAgreed) {
transactionManager.doAfterCommit {
user.email?.let { subscribeToNewsLetter(it, user.nickname) }
user.email?.let { subscribeToNewsLetter(user.id, it, user.nickname) }
}
}

Expand Down Expand Up @@ -162,7 +165,8 @@ class UserApplicationService(
return userRepository.findAll()
}

private fun subscribeToNewsLetter(email: String, name: String) {
private fun subscribeToNewsLetter(userId: String, email: String, name: String) {
sccServerEventRecorder.record(NewsletterSubscribedOnSignupPayload(userId))
runBlocking {
stibeeSubscriptionService.registerSubscriber(
email = email,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies {

api(projects.apiSpecification.api)
implementation(projects.crossCuttingConcern.infra.persistenceModel)
implementation(projects.crossCuttingConcern.infra.serverEvent)
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework:spring-webflux")
implementation("io.projectreactor.netty:reactor-netty")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package club.staircrusher.user.infra.adapter.`in`.controller
import club.staircrusher.api.spec.dto.ApiErrorResponse
import club.staircrusher.api.spec.dto.UpdateUserInfoPost200Response
import club.staircrusher.api.spec.dto.UpdateUserInfoPostRequest
import club.staircrusher.application.server_event.port.`in`.SccServerEventRecorder
import club.staircrusher.domain.server_event.NewsletterSubscribedOnSignupPayload
import club.staircrusher.stdlib.testing.SccRandom
import club.staircrusher.user.application.port.out.web.subscription.StibeeSubscriptionService
import club.staircrusher.user.domain.model.UserMobilityTool
Expand All @@ -16,13 +18,17 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyBlocking
import org.springframework.boot.test.mock.mockito.MockBean

class UpdateUserInfoTest : UserITBase() {
@MockBean
lateinit var stibeeSubscriptionService: StibeeSubscriptionService

@MockBean
lateinit var sccServerEventRecorder: SccServerEventRecorder

@Test
fun updateUserInfoTest() {
val user = transactionManager.doInTransaction {
Expand Down Expand Up @@ -196,6 +202,7 @@ class UpdateUserInfoTest : UserITBase() {
}
.apply {
verifyBlocking(stibeeSubscriptionService, atLeastOnce()) { registerSubscriber(eq(changedEmail), eq(changedNickname), any()) }
verify(sccServerEventRecorder, atLeastOnce()).record(NewsletterSubscribedOnSignupPayload(user.id))
}
}

Expand Down Expand Up @@ -230,6 +237,7 @@ class UpdateUserInfoTest : UserITBase() {
}
.apply {
verifyBlocking(stibeeSubscriptionService, never()) { registerSubscriber(eq(changedEmail), eq(changedNickname), any()) }
verify(sccServerEventRecorder, never()).record(NewsletterSubscribedOnSignupPayload(user.id))
}
}

Expand Down Expand Up @@ -265,6 +273,7 @@ class UpdateUserInfoTest : UserITBase() {
}
.apply {
verifyBlocking(stibeeSubscriptionService, never()) { registerSubscriber(eq(changedEmail), eq(changedNickname), any()) }
verify(sccServerEventRecorder, never()).record(NewsletterSubscribedOnSignupPayload(user.id))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dependencies {
implementation(rootProject.projects.crossCuttingConcern.stdlib)
api(rootProject.projects.crossCuttingConcern.domain.serverEvent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package club.staircrusher.application.server_event.port.`in`

import club.staircrusher.domain.server_event.ServerEventPayload

interface SccServerEventRecorder {
fun record(payload: ServerEventPayload)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package club.staircrusher.application.server_event.port.`in`

import club.staircrusher.application.server_event.port.out.persistence.ServerEventRepository
import club.staircrusher.domain.server_event.ServerEvent
import club.staircrusher.domain.server_event.ServerEventPayload
import club.staircrusher.stdlib.clock.SccClock
import club.staircrusher.stdlib.di.annotation.Component
import club.staircrusher.stdlib.domain.entity.EntityIdGenerator
import club.staircrusher.stdlib.persistence.TransactionManager
import mu.KotlinLogging

@Component
class SccServerPersistentEventRecorder(
private val transactionManager: TransactionManager,
private val serverEventRepository: ServerEventRepository,
) : SccServerEventRecorder {
private val logger = KotlinLogging.logger { }

override fun record(payload: ServerEventPayload) = transactionManager.doInTransaction {
val serverEvent = try {
ServerEvent(
id = EntityIdGenerator.generateRandom(),
type = payload.type,
payload = payload,
createdAt = SccClock.instant(),
)
} catch (e: Exception){
logger.error(e) { "Failed to create server event of payload: $payload" }
null
}

serverEvent?.let { serverEventRepository.save(it) }
Unit
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package club.staircrusher.application.server_event.port.out.persistence

import club.staircrusher.domain.server_event.ServerEvent

interface ServerEventRepository {
fun save(entity: ServerEvent): ServerEvent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
dependencies {
val kotlinxSerializationVersion: String by project
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")

val jUnitJupiterVersion: String by project
testImplementation("org.junit.jupiter:junit-jupiter-api:$jUnitJupiterVersion")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jUnitJupiterVersion")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package club.staircrusher.domain.server_event

data class NewsletterSubscribedOnSignupPayload(
val userId: String,
) : ServerEventPayload {
override val type = ServerEventType.NEWSLETTER_SUBSCRIBED_ON_SIGN_UP
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package club.staircrusher.domain.server_event

import java.time.Instant

data class ServerEvent(
val id: String,
val type: ServerEventType,
val payload: ServerEventPayload,
val createdAt: Instant,
) {
init {
check(type == payload.type)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package club.staircrusher.domain.server_event

interface ServerEventPayload {
val type: ServerEventType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package club.staircrusher.domain.server_event

enum class ServerEventType {
NEWSLETTER_SUBSCRIBED_ON_SIGN_UP,
// For test
// TODO: event type 이 더 생기면 테스트 코드 변경하고 삭제하기
UNKNOWN,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package club.staircrusher.domain.server_event

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import java.time.Instant

class ServerEventTest {

@Test
fun `ServerEvent 의 type 과 주어진 payload 의 type 이 일치해야 한다`() {
val serverEventPayload = NewsletterSubscribedOnSignupPayload("example")

assertDoesNotThrow {
ServerEvent(
id = "example",
type = ServerEventType.NEWSLETTER_SUBSCRIBED_ON_SIGN_UP,
payload = serverEventPayload,
createdAt = Instant.now(),
)
}

assertThrows<IllegalStateException> {
ServerEvent(
id = "example",
type = ServerEventType.UNKNOWN,
payload = serverEventPayload,
createdAt = Instant.now(),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies {
implementation(projects.boundedContext.challenge.domain)
implementation(projects.boundedContext.quest.domain)
implementation(projects.boundedContext.user.domain)
implementation(projects.crossCuttingConcern.domain.serverEvent)

val sqlDelightVersion: String by project
api("app.cash.sqldelight:runtime-jvm:$sqlDelightVersion")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package club.staircrusher.infra.persistence.sqldelight

import app.cash.sqldelight.ColumnAdapter
import club.staircrusher.challenge.domain.model.ChallengeCondition
import club.staircrusher.domain.server_event.ServerEventPayload
import club.staircrusher.domain.server_event.ServerEventType
import club.staircrusher.infra.persistence.sqldelight.column_adapter.AccessibilityImageListStringColumnAdapter
import club.staircrusher.infra.persistence.sqldelight.column_adapter.ClubQuestPurposeTypeStringColumnAdapter
import club.staircrusher.infra.persistence.sqldelight.column_adapter.EntranceDoorTypeListStringColumnAdapter
Expand All @@ -19,6 +22,7 @@ import club.staircrusher.infra.persistence.sqldelight.migration.Club_quest
import club.staircrusher.infra.persistence.sqldelight.migration.Place
import club.staircrusher.infra.persistence.sqldelight.migration.Place_accessibility
import club.staircrusher.infra.persistence.sqldelight.migration.Scc_user
import club.staircrusher.infra.persistence.sqldelight.migration.Server_event
import club.staircrusher.infra.persistence.sqldelight.migration.User_auth_info
import club.staircrusher.stdlib.di.annotation.Component
import club.staircrusher.stdlib.persistence.Transaction
Expand Down Expand Up @@ -79,6 +83,26 @@ class DB(dataSource: DataSource) : TransactionManager {
),
club_questAdapter = Club_quest.Adapter(
purpose_typeAdapter = ClubQuestPurposeTypeStringColumnAdapter,
),
server_eventAdapter = Server_event.Adapter(
typeAdapter = object : ColumnAdapter<ServerEventType, String> {
override fun decode(databaseValue: String): ServerEventType {
return ServerEventType.valueOf(databaseValue)
}

override fun encode(value: ServerEventType): String {
return value.name
}
},
payloadAdapter = object : ColumnAdapter<ServerEventPayload, String> {
override fun decode(databaseValue: String): ServerEventPayload {
return objectMapper.readValue(databaseValue)
}

override fun encode(value: ServerEventPayload): String {
return objectMapper.writeValueAsString(value)
}
}
)
)

Expand All @@ -102,6 +126,7 @@ class DB(dataSource: DataSource) : TransactionManager {
val challengeContributionQueries = scc.challengeContributionQueries
val challengeParticipationQueries = scc.challengeParticipationQueries
val challengeRankQueries = scc.challengeRankQueries
val serverEventQueries = scc.serverEventQueries


override fun <T> doInTransaction(block: Transaction<T>.() -> T): T {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import club.staircrusher.domain.server_event.ServerEventPayload;
import club.staircrusher.domain.server_event.ServerEventType;


CREATE TABLE IF NOT EXISTS server_event (
id VARCHAR(36) NOT NULL,
type VARCHAR(64) AS ServerEventType NOT NULL,
payload TEXT AS ServerEventPayload NOT NULL DEFAULT '',
created_at TIMESTAMP(6) WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);

CREATE INDEX idx_server_event_1 ON server_event(type, created_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
save:
INSERT INTO server_event
VALUES :server_event
ON CONFLICT(id) DO UPDATE SET
id = EXCLUDED.id,
type = EXCLUDED.type,
payload = EXCLUDED.payload,
created_at = EXCLUDED.created_at;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dependencies {
implementation(projects.crossCuttingConcern.stdlib)
implementation(projects.crossCuttingConcern.infra.persistenceModel)
api(rootProject.projects.crossCuttingConcern.domain.serverEvent)
api(rootProject.projects.crossCuttingConcern.application.serverEvent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package club.staircrusher.infra.server_event.out.persistence

import club.staircrusher.application.server_event.port.out.persistence.ServerEventRepository
import club.staircrusher.domain.server_event.ServerEvent
import club.staircrusher.infra.persistence.sqldelight.DB
import club.staircrusher.infra.server_event.out.persistence.sqldelight.toPersistenceModel
import club.staircrusher.stdlib.di.annotation.Component

@Component
class ServerEventRepository(
db: DB,
) : ServerEventRepository {
private val queries = db.serverEventQueries

override fun save(entity: ServerEvent): ServerEvent {
queries.save(entity.toPersistenceModel())
return entity
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package club.staircrusher.infra.server_event.out.persistence.sqldelight

import club.staircrusher.domain.server_event.ServerEvent
import club.staircrusher.infra.persistence.sqldelight.migration.Server_event
import club.staircrusher.stdlib.time.toOffsetDateTime

fun ServerEvent.toPersistenceModel() = Server_event(
id = id,
type = type,
payload = payload,
created_at = createdAt.toOffsetDateTime(),
)

fun Server_event.toDomainModel() = ServerEvent(
id = id,
type = type,
payload = payload,
createdAt = created_at.toInstant(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonModuleKotlinVersion")
integrationTestImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonModuleKotlinVersion")
integrationTestImplementation("org.springframework.boot:spring-boot-starter-test")
integrationTestImplementation(projects.crossCuttingConcern.application.serverEvent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package club.staircrusher.spring_web.mock

import club.staircrusher.application.server_event.port.out.persistence.ServerEventRepository
import club.staircrusher.domain.server_event.ServerEvent
import club.staircrusher.stdlib.di.annotation.Component

@Component
class MockServerEventRepository : ServerEventRepository {
override fun save(entity: ServerEvent): ServerEvent {
return ServerEvent(
id = entity.id,
type = entity.type,
payload = entity.payload,
createdAt = entity.createdAt
)
}
}

0 comments on commit 5af4c47

Please sign in to comment.