Klogging
純Kotlinで構築されたスタンドアロンロギングライブラリ。Kotlinコルーチンとの深い統合により、構造化ログイベントをデフォルトでサポート。非同期イベント配信とコンテキスト情報の自動取得機能を提供。
ライブラリ
Klogging
概要
Kloggingは、純Kotlinで構築されたスタンドアロンロギングライブラリとして、「Kotlinイディオムによる簡単で強力なロギング」を提供し、Kotlinコルーチンとの深い統合により現代的なKotlin開発ニーズに対応しています。構造化ログイベントをデフォルトでサポートし、非同期イベント配信とコンテキスト情報の自動取得機能により、マイクロサービスと非同期処理が重要な環境での高パフォーマンスログ管理を実現。従来のJavaロギングフレームワークを超越したKotlinファーストなロギングソリューションです。
詳細
Kloggingは2025年のコルーチンベースKotlinアプリケーションで注目度が高まっている次世代ロギングライブラリです。コルーチンスコープでの情報保存と自動コンテキスト管理、マイクロ秒レベルの高精度タイムスタンプ、JDKプラットフォームロギングの実装など、現代的な分散システム要件に対応した機能を搭載。Spring Bootとの優れた統合性により、サーバーサイドKotlin開発での構造化ログとトレーサビリティを大幅に向上させ、従来のSLF4J+Logbackアプローチを補完・発展させる重要な位置付けにあります。
主な特徴
- コルーチンネイティブ: Kotlinコルーチンとの深い統合と自動コンテキスト管理
- 構造化ログデフォルト: メッセージテンプレートによる構造化イベント自動生成
- 非同期イベント配信: 高パフォーマンスな非同期ログ処理機能
- 高精度タイムスタンプ: マイクロ秒からナノ秒レベルの精密時刻記録
- Kotlinファースト: 純Kotlin実装による最適化されたAPI設計
- Spring Boot統合: 企業レベルでの導入を支援する統合機能
メリット・デメリット
メリット
- Kotlinコルーチンとの統合によりコンテキスト管理が自動化
- 構造化ログがデフォルトで現代的なログ解析・監視環境に適応
- 非同期処理により高負荷環境でのパフォーマンス向上を実現
- 純Kotlin実装でKotlinイディオムに完全準拠した自然なAPI
- マイクロサービス環境でのトレーサビリティと分散ログ管理に優位性
- Spring Bootとの統合により企業レベル開発での採用容易性
デメリット
- Kotlinエコシステムに特化しており他言語との互換性は限定的
- 比較的新しいライブラリで成熟度とコミュニティサイズがSLF4Jより小規模
- 従来JavaロギングとのAPI互換性がなく移行時の学習コストが存在
- 高度な機能が多く小規模プロジェクトではオーバーエンジニアリングの可能性
- デバッグやトラブルシューティング情報が従来ライブラリより少ない
- 企業環境での実績がまだ限定的で保守的な組織での採用ハードルが高い
参考ページ
書き方の例
インストールと基本セットアップ
// build.gradle.kts
dependencies {
implementation("io.klogging:klogging-jvm:0.10.1")
// Spring Boot統合の場合
implementation("io.klogging:slf4j-klogging:0.10.1")
}
<!-- Maven pom.xml -->
<dependency>
<groupId>io.klogging</groupId>
<artifactId>klogging-jvm</artifactId>
<version>0.10.1</version>
</dependency>
import io.klogging.Klogging
import io.klogging.NoCoLogging
import io.klogging.config.loggingConfiguration
import kotlinx.coroutines.*
// 基本的なKlogging設定
fun main() = runBlocking {
// コンソール出力の設定
loggingConfiguration {
ANSI_CONSOLE()
}
// シンプルなログ出力テスト
val logger = NoCoLogging.logger()
logger.info { "Klogging initialized successfully" }
// コルーチン環境でのログ
launch {
val coLogger = Klogging.logger()
coLogger.info { "Coroutine-based logging ready" }
}
}
// 詳細設定例
fun setupAdvancedKlogging() {
loggingConfiguration {
// 複数出力先設定
sink("console", ANSI_CONSOLE())
sink("file", FILE("logs/app.log"))
sink("json", FILE("logs/app.json", JSON_FORMAT))
// ログレベル設定
minLevel(Level.INFO)
// 構造化ログ設定
logger("com.example.service") {
minLevel(Level.DEBUG)
stopOnMatch = true
}
// 非同期配信設定
asyncDispatcher {
bufferSize = 1000
dropOnOverflow = false
}
}
}
コルーチンとの統合(基本的な使用方法)
import io.klogging.Klogging
import io.klogging.NoCoLogging
import io.klogging.events.logContext
import kotlinx.coroutines.*
// コルーチン統合ロギングクラス
class UserService : Klogging {
suspend fun processUser(userId: String, userData: UserData) = coroutineScope {
// コルーチンコンテキストにログ情報を追加
launch(logContext("userId" to userId, "operation" to "processUser")) {
logger.info { "Starting user processing" }
try {
// ユーザーデータ検証
validateUserData(userData)
logger.debug { "User data validation completed" }
// データベース保存
saveUserToDatabase(userData)
logger.info { "User data saved successfully" }
// 外部API通知
notifyExternalSystems(userId)
logger.info { "External systems notified" }
} catch (e: Exception) {
logger.error(e) { "User processing failed" }
throw e
}
}
}
private suspend fun validateUserData(userData: UserData) {
logger.debug { "Validating user data: ${userData.email}" }
if (userData.email.isBlank()) {
logger.warn { "Invalid email address provided" }
throw IllegalArgumentException("Email is required")
}
if (userData.age < 0 || userData.age > 150) {
logger.warn { "Invalid age: ${userData.age}" }
throw IllegalArgumentException("Invalid age range")
}
}
private suspend fun saveUserToDatabase(userData: UserData) {
logger.debug { "Saving to database" }
delay(100) // データベース操作シミュレーション
logger.info { "Database save completed" }
}
private suspend fun notifyExternalSystems(userId: String) {
logger.debug { "Notifying external systems" }
delay(50) // API呼び出しシミュレーション
logger.info { "External notification completed" }
}
}
// 非コルーチン環境での使用
class ConfigurationManager : NoCoLogging {
fun loadConfiguration(configPath: String): Configuration {
logger.info { "Loading configuration from: $configPath" }
return try {
val config = parseConfigFile(configPath)
logger.info { "Configuration loaded successfully. Entries: ${config.entries.size}" }
config
} catch (e: Exception) {
logger.error(e) { "Failed to load configuration" }
throw ConfigurationException("Configuration loading failed", e)
}
}
private fun parseConfigFile(path: String): Configuration {
logger.debug { "Parsing configuration file" }
// ファイル解析処理
return Configuration()
}
}
// データクラス
data class UserData(
val email: String,
val name: String,
val age: Int
)
data class Configuration(
val entries: Map<String, String> = emptyMap()
)
class ConfigurationException(message: String, cause: Throwable) : Exception(message, cause)
// 使用例
suspend fun main() {
loggingConfiguration {
ANSI_CONSOLE()
}
val userService = UserService()
val configManager = ConfigurationManager()
// コルーチンベースの処理
val userData = UserData("[email protected]", "田中太郎", 30)
userService.processUser("user123", userData)
// 非コルーチン処理
val config = configManager.loadConfiguration("/path/to/config.yaml")
}
構造化ログとコンテキスト管理
import io.klogging.Klogging
import io.klogging.events.logContext
import io.klogging.events.LogEvent
import kotlinx.coroutines.*
import java.util.*
// 構造化ログを活用したAPIサービス
class ApiService : Klogging {
suspend fun handleRequest(requestId: String, endpoint: String, payload: Any) = coroutineScope {
// リクエストコンテキストの設定
launch(logContext(
"requestId" to requestId,
"endpoint" to endpoint,
"timestamp" to System.currentTimeMillis(),
"service" to "ApiService"
)) {
logger.info { "API request received" }
val startTime = System.nanoTime()
try {
// 認証・認可チェック
authenticateRequest(requestId)
// ビジネスロジック実行
val result = processRequest(endpoint, payload)
val duration = (System.nanoTime() - startTime) / 1_000_000.0
// 成功ログ(構造化)
logger.info {
"API request completed successfully. " +
"Duration: {duration}ms, Result: {resultType}"
} withContext mapOf(
"duration" to duration,
"resultType" to result::class.simpleName,
"resultSize" to result.toString().length
)
} catch (e: AuthenticationException) {
val duration = (System.nanoTime() - startTime) / 1_000_000.0
logger.warn(e) {
"Authentication failed. Duration: {duration}ms"
} withContext mapOf(
"duration" to duration,
"errorType" to "authentication",
"clientIp" to getCurrentClientIp()
)
throw e
} catch (e: Exception) {
val duration = (System.nanoTime() - startTime) / 1_000_000.0
logger.error(e) {
"API request failed. Duration: {duration}ms, Error: {errorMessage}"
} withContext mapOf(
"duration" to duration,
"errorMessage" to e.message,
"errorType" to e::class.simpleName
)
throw e
}
}
}
private suspend fun authenticateRequest(requestId: String) {
logger.debug { "Authenticating request" }
// 認証処理シミュレーション
delay(10)
if (requestId.startsWith("invalid")) {
throw AuthenticationException("Invalid request ID")
}
logger.debug { "Authentication successful" }
}
private suspend fun processRequest(endpoint: String, payload: Any): Any {
logger.debug { "Processing business logic for endpoint: $endpoint" }
return when (endpoint) {
"/users" -> processUserRequest(payload)
"/orders" -> processOrderRequest(payload)
else -> {
logger.warn { "Unknown endpoint: $endpoint" }
throw IllegalArgumentException("Unsupported endpoint")
}
}
}
private suspend fun processUserRequest(payload: Any): Map<String, Any> {
logger.debug { "Processing user request" }
delay(50) // 処理時間シミュレーション
return mapOf(
"status" to "success",
"userId" to UUID.randomUUID().toString(),
"data" to payload
)
}
private suspend fun processOrderRequest(payload: Any): Map<String, Any> {
logger.debug { "Processing order request" }
delay(100) // 処理時間シミュレーション
return mapOf(
"status" to "success",
"orderId" to UUID.randomUUID().toString(),
"data" to payload
)
}
private fun getCurrentClientIp(): String {
// クライアントIP取得(実装は環境に依存)
return "192.168.1.100"
}
}
// カスタム例外
class AuthenticationException(message: String) : Exception(message)
// 拡張関数でコンテキスト情報を追加
infix fun String.withContext(context: Map<String, Any>): String {
return this // 実際のKloggingではコンテキストが自動的に処理される
}
// パフォーマンス監視ロガー
class PerformanceLogger : Klogging {
suspend fun <T> measureOperation(
operationName: String,
additionalContext: Map<String, Any> = emptyMap(),
operation: suspend () -> T
): T = coroutineScope {
launch(logContext(
"operation" to operationName,
"startTime" to System.currentTimeMillis()
) + additionalContext) {
val startTime = System.nanoTime()
val startMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
logger.debug { "Starting operation: $operationName" }
try {
val result = operation()
val duration = (System.nanoTime() - startTime) / 1_000_000.0
val endMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
val memoryDelta = endMemory - startMemory
logger.info {
"Operation completed: {operation}. Duration: {duration}ms, Memory: {memoryDelta}KB"
} withContext mapOf(
"operation" to operationName,
"duration" to duration,
"memoryDelta" to memoryDelta / 1024,
"success" to true
)
return@launch result
} catch (e: Exception) {
val duration = (System.nanoTime() - startTime) / 1_000_000.0
logger.error(e) {
"Operation failed: {operation}. Duration: {duration}ms, Error: {error}"
} withContext mapOf(
"operation" to operationName,
"duration" to duration,
"error" to e.message,
"success" to false
)
throw e
}
}
}
}
// 使用例
suspend fun main() {
loggingConfiguration {
sink("console", ANSI_CONSOLE())
sink("file", FILE("logs/structured.log", JSON_FORMAT))
}
val apiService = ApiService()
val perfLogger = PerformanceLogger()
// 構造化ログを伴うAPI処理
perfLogger.measureOperation(
"api_request_processing",
mapOf("endpoint" to "/users", "method" to "POST")
) {
apiService.handleRequest(
"req123",
"/users",
mapOf("name" to "田中太郎", "email" to "[email protected]")
)
}
}
Spring Boot統合と実用例
import io.klogging.Klogging
import io.klogging.NoCoLogging
import io.klogging.config.loggingConfiguration
import io.klogging.events.logContext
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.*
import org.springframework.stereotype.Service
import org.springframework.stereotype.Repository
import kotlinx.coroutines.*
import javax.annotation.PostConstruct
@SpringBootApplication
class KloggingDemoApplication : NoCoLogging {
@PostConstruct
fun setupLogging() {
loggingConfiguration {
// 開発環境設定
ANSI_CONSOLE()
sink("file", FILE("logs/spring-app.log"))
sink("json", FILE("logs/spring-app.json", JSON_FORMAT))
// パッケージ別ログレベル
logger("com.example.controller") { minLevel(Level.DEBUG) }
logger("com.example.service") { minLevel(Level.INFO) }
logger("com.example.repository") { minLevel(Level.WARN) }
// SQLログの詳細設定
logger("org.springframework.jdbc") { minLevel(Level.DEBUG) }
}
logger.info { "Klogging configuration completed for Spring Boot" }
}
}
fun main(args: Array<String>) {
runApplication<KloggingDemoApplication>(*args)
}
// RESTコントローラー
@RestController
@RequestMapping("/api/users")
class UserController(
private val userService: UserService
) : Klogging {
@PostMapping
suspend fun createUser(@RequestBody userRequest: CreateUserRequest): UserResponse = coroutineScope {
val requestId = java.util.UUID.randomUUID().toString()
launch(logContext(
"requestId" to requestId,
"endpoint" to "/api/users",
"method" to "POST",
"controller" to "UserController"
)) {
logger.info { "Creating new user: ${userRequest.email}" }
try {
val user = userService.createUser(userRequest)
logger.info {
"User created successfully. UserId: {userId}, Email: {email}"
} withContext mapOf(
"userId" to user.id,
"email" to user.email,
"requestId" to requestId
)
UserResponse(user.id, user.email, user.name, "created")
} catch (e: UserAlreadyExistsException) {
logger.warn(e) {
"User creation failed - already exists: {email}"
} withContext mapOf(
"email" to userRequest.email,
"requestId" to requestId
)
throw e
} catch (e: Exception) {
logger.error(e) {
"User creation failed: {error}"
} withContext mapOf(
"error" to e.message,
"email" to userRequest.email,
"requestId" to requestId
)
throw e
}
}
}
@GetMapping("/{userId}")
suspend fun getUser(@PathVariable userId: String): UserResponse = coroutineScope {
launch(logContext(
"userId" to userId,
"endpoint" to "/api/users/{userId}",
"method" to "GET"
)) {
logger.debug { "Retrieving user: $userId" }
val user = userService.findUser(userId)
?: throw UserNotFoundException("User not found: $userId")
logger.debug { "User retrieved successfully" }
UserResponse(user.id, user.email, user.name, "retrieved")
}
}
}
// ビジネスサービス
@Service
class UserService(
private val userRepository: UserRepository,
private val emailService: EmailService
) : Klogging {
suspend fun createUser(request: CreateUserRequest): User = coroutineScope {
launch(logContext("service" to "UserService", "operation" to "createUser")) {
logger.info { "Creating user: ${request.email}" }
// 重複チェック
val existingUser = userRepository.findByEmail(request.email)
if (existingUser != null) {
logger.warn { "User already exists: ${request.email}" }
throw UserAlreadyExistsException("User already exists")
}
// ユーザー作成
val user = User(
id = java.util.UUID.randomUUID().toString(),
email = request.email,
name = request.name
)
val savedUser = userRepository.save(user)
logger.info { "User saved to database: ${savedUser.id}" }
// ウェルカムメール送信(非同期)
launch {
emailService.sendWelcomeEmail(savedUser)
}
logger.info { "User creation completed: ${savedUser.id}" }
savedUser
}
}
suspend fun findUser(userId: String): User? = coroutineScope {
launch(logContext("service" to "UserService", "operation" to "findUser")) {
logger.debug { "Finding user: $userId" }
val user = userRepository.findById(userId)
if (user != null) {
logger.debug { "User found: ${user.email}" }
} else {
logger.debug { "User not found: $userId" }
}
user
}
}
}
// リポジトリ
@Repository
class UserRepository : Klogging {
// 簡易的なインメモリストレージ
private val users = mutableMapOf<String, User>()
private val emailIndex = mutableMapOf<String, String>()
suspend fun save(user: User): User = withContext(Dispatchers.IO) {
launch(logContext("repository" to "UserRepository", "operation" to "save")) {
logger.debug { "Saving user to database: ${user.id}" }
users[user.id] = user
emailIndex[user.email] = user.id
// データベース保存をシミュレーション
delay(10)
logger.debug { "User saved successfully: ${user.id}" }
}
user
}
suspend fun findById(userId: String): User? = withContext(Dispatchers.IO) {
launch(logContext("repository" to "UserRepository", "operation" to "findById")) {
logger.debug { "Finding user by ID: $userId" }
// データベース検索をシミュレーション
delay(5)
val user = users[userId]
logger.debug { "User search result: ${if (user != null) "found" else "not found"}" }
user
}
}
suspend fun findByEmail(email: String): User? = withContext(Dispatchers.IO) {
launch(logContext("repository" to "UserRepository", "operation" to "findByEmail")) {
logger.debug { "Finding user by email: $email" }
delay(5)
val userId = emailIndex[email]
val user = userId?.let { users[it] }
logger.debug { "Email search result: ${if (user != null) "found" else "not found"}" }
user
}
}
}
// メールサービス
@Service
class EmailService : Klogging {
suspend fun sendWelcomeEmail(user: User) = coroutineScope {
launch(logContext(
"service" to "EmailService",
"operation" to "sendWelcomeEmail",
"userId" to user.id
)) {
logger.info { "Sending welcome email to: ${user.email}" }
try {
// メール送信処理をシミュレーション
delay(100)
logger.info { "Welcome email sent successfully to: ${user.email}" }
} catch (e: Exception) {
logger.error(e) {
"Failed to send welcome email to: {email}"
} withContext mapOf(
"email" to user.email,
"userId" to user.id,
"error" to e.message
)
}
}
}
}
// データクラス
data class CreateUserRequest(
val email: String,
val name: String
)
data class UserResponse(
val id: String,
val email: String,
val name: String,
val status: String
)
data class User(
val id: String,
val email: String,
val name: String
)
// カスタム例外
class UserAlreadyExistsException(message: String) : Exception(message)
class UserNotFoundException(message: String) : Exception(message)
高度な設定とカスタマイズ
import io.klogging.Klogging
import io.klogging.config.*
import io.klogging.events.*
import io.klogging.rendering.*
import io.klogging.sending.*
import kotlinx.coroutines.*
import java.time.LocalDateTime
// 高度なKlogging設定
object AdvancedKloggingConfig {
fun setupProduction() {
loggingConfiguration {
// カスタムレンダリング設定
rendering {
// JSON形式でのカスタムレンダリング
custom("customJson") { event ->
buildString {
append("{")
append("\"timestamp\":\"${event.timestamp}\",")
append("\"level\":\"${event.level}\",")
append("\"logger\":\"${event.logger}\",")
append("\"message\":\"${event.message}\",")
append("\"thread\":\"${event.items["thread"]}\",")
append("\"context\":{")
event.items.filter { it.key != "thread" }
.entries.joinToString(",") { "\"${it.key}\":\"${it.value}\"" }
append("}}")
}
}
// カスタムコンソール出力
custom("colorConsole") { event ->
val color = when (event.level) {
Level.ERROR -> "\u001B[31m" // 赤
Level.WARN -> "\u001B[33m" // 黄
Level.INFO -> "\u001B[32m" // 緑
Level.DEBUG -> "\u001B[36m" // シアン
else -> "\u001B[0m" // リセット
}
val reset = "\u001B[0m"
"$color[${event.timestamp}] [${event.level}] ${event.logger}: ${event.message}$reset"
}
}
// カスタム送信先設定
sending {
// ファイル出力設定
custom("rotatingFile") { events ->
events.forEach { event ->
val logFile = getLogFileForDate(LocalDateTime.now())
writeToFile(logFile, renderEvent(event, "customJson"))
}
}
// 外部システム連携
custom("elasticSearch") { events ->
// ElasticSearchへの送信実装
sendToElasticSearch(events)
}
custom("metrics") { events ->
// メトリクス収集
collectMetrics(events)
}
}
// 複合シンク設定
sink("production") {
renderer = "customJson"
dispatcher = listOf("rotatingFile", "elasticSearch", "metrics")
}
sink("development") {
renderer = "colorConsole"
dispatcher = listOf("CONSOLE")
}
// ロガー別設定
logger("com.example.api") {
minLevel(Level.INFO)
sink("production")
stopOnMatch = true
}
logger("com.example.service") {
minLevel(Level.DEBUG)
sink("development")
stopOnMatch = false
}
// パフォーマンス監視設定
logger("performance") {
minLevel(Level.DEBUG)
sink("production")
additionalContext = mapOf(
"application" to "myapp",
"version" to "1.0.0"
)
}
// フィルター設定
filter { event ->
// 機密情報のマスキング
event.copy(
message = maskSensitiveData(event.message),
items = event.items.mapValues { maskSensitiveData(it.value.toString()) }
)
}
}
}
private fun getLogFileForDate(date: LocalDateTime): String {
return "logs/app-${date.toLocalDate()}.log"
}
private fun writeToFile(filename: String, content: String) {
// ファイル書き込み実装
java.io.File(filename).appendText(content + "\n")
}
private fun renderEvent(event: LogEvent, renderer: String): String {
// イベントレンダリング実装
return event.toString()
}
private suspend fun sendToElasticSearch(events: List<LogEvent>) {
// ElasticSearch送信実装
withContext(Dispatchers.IO) {
// HTTP クライアントでElasticSearchにPOST
events.forEach { event ->
// 実装詳細
}
}
}
private fun collectMetrics(events: List<LogEvent>) {
// メトリクス収集実装
events.forEach { event ->
when (event.level) {
Level.ERROR -> incrementErrorCounter()
Level.WARN -> incrementWarningCounter()
else -> incrementInfoCounter()
}
}
}
private fun maskSensitiveData(text: String): String {
return text
.replace(Regex("password[=:]\\s*\\S+", RegexOption.IGNORE_CASE), "password=***")
.replace(Regex("token[=:]\\s*\\S+", RegexOption.IGNORE_CASE), "token=***")
.replace(Regex("\\b\\d{4}-\\d{4}-\\d{4}-\\d{4}\\b"), "****-****-****-****")
}
private fun incrementErrorCounter() { /* メトリクス実装 */ }
private fun incrementWarningCounter() { /* メトリクス実装 */ }
private fun incrementInfoCounter() { /* メトリクス実装 */ }
}
// カスタムロガー実装
class AuditLogger : Klogging {
suspend fun logSecurityEvent(
eventType: String,
userId: String?,
details: Map<String, Any>
) = coroutineScope {
launch(logContext(
"eventType" to "security",
"auditCategory" to eventType,
"userId" to (userId ?: "anonymous"),
"timestamp" to System.currentTimeMillis()
)) {
logger.info {
"Security event: {eventType}. Details: {details}"
} withContext mapOf(
"eventType" to eventType,
"details" to details,
"severity" to determineSeverity(eventType)
)
}
}
suspend fun logBusinessEvent(
operation: String,
entityId: String,
changes: Map<String, Any>
) = coroutineScope {
launch(logContext(
"eventType" to "business",
"operation" to operation,
"entityId" to entityId
)) {
logger.info {
"Business operation: {operation} on entity {entityId}"
} withContext mapOf(
"operation" to operation,
"entityId" to entityId,
"changes" to changes,
"changeCount" to changes.size
)
}
}
private fun determineSeverity(eventType: String): String {
return when (eventType) {
"login_failure", "unauthorized_access" -> "high"
"login_success", "logout" -> "low"
"password_change", "permission_change" -> "medium"
else -> "low"
}
}
}
// 使用例
suspend fun main() {
AdvancedKloggingConfig.setupProduction()
val auditLogger = AuditLogger()
// セキュリティイベント
auditLogger.logSecurityEvent(
"login_failure",
"user123",
mapOf(
"ip" to "192.168.1.100",
"userAgent" to "Mozilla/5.0...",
"reason" to "invalid_password"
)
)
// ビジネスイベント
auditLogger.logBusinessEvent(
"user_update",
"user123",
mapOf(
"oldEmail" to "[email protected]",
"newEmail" to "[email protected]",
"updatedBy" to "admin"
)
)
}