Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

서버 로깅 구현 #349

Merged
merged 9 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
implementation(projects.crossCuttingConcern.application.serverLog)
}
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_log.port.`in`.SccServerLogger
import club.staircrusher.domain.server_log.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 sccServerLogger: SccServerLogger,
) {

@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) {
sccServerLogger.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.serverLog)
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_log.port.`in`.SccServerLogger
import club.staircrusher.domain.server_log.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 sccServerLogger: SccServerLogger

@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(sccServerLogger, 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(sccServerLogger, 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(sccServerLogger, 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.serverLog)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package club.staircrusher.application.server_log.port.`in`

import club.staircrusher.domain.server_log.ServerLogPayload

interface SccServerLogger {
fun record(payload: ServerLogPayload)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package club.staircrusher.application.server_log.port.`in`

import club.staircrusher.application.server_log.port.out.persistence.ServerLogRepository
import club.staircrusher.domain.server_log.ServerLog
import club.staircrusher.domain.server_log.ServerLogPayload
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

@Component
class SccServerPersistentLogger(
private val transactionManager: TransactionManager,
private val serverLogRepository: ServerLogRepository,
) : SccServerLogger {
override fun record(payload: ServerLogPayload) = transactionManager.doInTransaction {
val serverLog = ServerLog(
id = EntityIdGenerator.generateRandom(),
type = payload.type,
payload = payload,
createdAt = SccClock.instant(),
)

serverLogRepository.save(serverLog)
Unit
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package club.staircrusher.application.server_log.port.out.persistence

import club.staircrusher.domain.server_log.ServerLog

interface ServerLogRepository {
fun save(entity: ServerLog): ServerLog
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dependencies {
val kotlinxSerializationVersion: String by project
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package club.staircrusher.domain.server_log

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

import java.time.Instant

data class ServerLog(
Copy link
Contributor

Choose a reason for hiding this comment

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

저는 domain event랑 약간 겹치더라도 log 보다는 event라는 이름이 더 좋을 것 같은데요, log랑 event라는 워딩에서 느껴지는 차이가 꽤 분명한 것 같기 때문입니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

0857cc9
log -> event 로 이름 변경했습니다

val id: String,
val type: ServerLogType,
val payload: ServerLogPayload,
val createdAt: Instant,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package club.staircrusher.domain.server_log

interface ServerLogPayload {
val type: ServerLogType
}
Copy link
Contributor

Choose a reason for hiding this comment

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

ServerLogType이 이미 ServerLog에 있는데 굳이 여기도 들어갈 필요가 있으려나요?
여기도 넣으실 거면 ServerLog class의 init에 type == payload.type을 check 하면 좋겠습니당

Copy link
Member Author

Choose a reason for hiding this comment

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

사실상 type 별로 payload 가 생길거라고 생각했고 그래서 payload 만 알면 type 을 바로 알 수 있도록 interface 의 val 로 넣었습니다. ServerLog (ServerEvent) 의 필드로 넣은 것은 db 에 인덱스 걸고 저장하고 싶어서였는데, db 스키마와 애플리케이션 모델이 꼭 같은 필요는 없으니 다른 더 좋은 표현 방법이 있나 고민해보겠습니다
없으면 그냥 init 블록 둘 것 같네요 ㅎㅎ

Copy link
Member Author

@jyoo0515 jyoo0515 Jul 17, 2024

Choose a reason for hiding this comment

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

지금도 중복 저장되고 있겠군요.. 음

Copy link
Member Author

@jyoo0515 jyoo0515 Jul 17, 2024

Choose a reason for hiding this comment

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

4291255
db 에 중복저장 안되도록 함수로 빼고 init 블럭 안에 체크로직 넣었습니다
가독성을 위해서 type 은 text 로 함께 묶어서 들어가지 않고 따로 칼럼으로 두는게 좋을 것 같아서요

Copy link
Member Author

Choose a reason for hiding this comment

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

어차피 constructor 에 없는 필드라 저장은 안되겠네요
따로 converter 에서 paylod 와 타입을 매핑해주는 것보다 type 이 payload 에 종속적인게 관리하기 좋을 것 같아서 이렇게 두고,
만에 하나 runtime 에 에러가 날 수 있는 여지가 있기 때문에 try catch 로 감쌌습니다
6a9b16c

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package club.staircrusher.domain.server_log

enum class ServerLogType {
NEWSLETTER_SUBSCRIBED_ON_SIGN_UP,
}
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.serverLog)

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_log.ServerLogPayload
import club.staircrusher.domain.server_log.ServerLogType
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_log
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_logAdapter = Server_log.Adapter(
typeAdapter = object : ColumnAdapter<ServerLogType, String> {
override fun decode(databaseValue: String): ServerLogType {
return ServerLogType.valueOf(databaseValue)
}

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

override fun encode(value: ServerLogPayload): 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 serverLogQueries = scc.serverLogQueries


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

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

CREATE INDEX idx_server_log_1 ON server_log(type, created_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
save:
INSERT INTO server_log
VALUES :server_log
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.serverLog)
api(rootProject.projects.crossCuttingConcern.application.serverLog)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package club.staircrusher.infra.server_log.out.persistence

import club.staircrusher.application.server_log.port.out.persistence.ServerLogRepository
import club.staircrusher.domain.server_log.ServerLog
import club.staircrusher.infra.persistence.sqldelight.DB
import club.staircrusher.infra.server_log.out.persistence.sqldelight.toPersistenceModel
import club.staircrusher.stdlib.di.annotation.Component

@Component
class ServerLogRepository(
db: DB,
) : ServerLogRepository {
private val queries = db.serverLogQueries

override fun save(entity: ServerLog): ServerLog {
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_log.out.persistence.sqldelight

import club.staircrusher.domain.server_log.ServerLog
import club.staircrusher.infra.persistence.sqldelight.migration.Server_log
import club.staircrusher.stdlib.time.toOffsetDateTime

fun ServerLog.toPersistenceModel() = Server_log(
id = id,
type = type,
payload = payload,
created_at = createdAt.toOffsetDateTime(),
)

fun Server_log.toDomainModel() = ServerLog(
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.serverLog)
}
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_log.port.out.persistence.ServerLogRepository
import club.staircrusher.domain.server_log.ServerLog
import club.staircrusher.stdlib.di.annotation.Component

@Component
class MockServerLogRepository : ServerLogRepository {
override fun save(entity: ServerLog): ServerLog {
return ServerLog(
id = entity.id,
type = entity.type,
payload = entity.payload,
createdAt = entity.createdAt
)
}
}
Loading