Skip to content

Commit 48187b9

Browse files
committed
feat(logging): gate trace() behind TraceManager.initialized and add KDoc
Calls to trace() before TraceManager.initialize() are now silently dropped, preventing crashes from modules that log during early startup. Added KDoc to all public APIs in Logging.kt.
1 parent ce902c1 commit 48187b9

1 file changed

Lines changed: 86 additions & 27 deletions

File tree

  • libs/logging/src/main/kotlin/com/getcode/utils

libs/logging/src/main/kotlin/com/getcode/utils/Logging.kt

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,63 +8,70 @@ import kotlin.time.Duration
88
import kotlin.time.TimeSource
99
import kotlin.time.measureTime
1010

11+
/**
12+
* Categorizes trace events for routing through Timber and [BreadcrumbSink]s.
13+
*
14+
* Each type maps to a Timber log level and is forwarded to all registered
15+
* breadcrumb sinks, except [Silent] which is only written to the local log file.
16+
*/
1117
sealed interface TraceType {
12-
/**
13-
* This is not forwarded to logging services
14-
*/
15-
18+
/** Local-only; not forwarded to external logging services. */
1619
data object Silent : TraceType
1720

18-
/**
19-
* An error event
20-
*/
21+
/** An error event. */
2122
data object Error: TraceType
2223

23-
/**
24-
* A log message
25-
*/
24+
/** A general log message. */
2625
data object Log : TraceType
2726

28-
/**
29-
* A navigation event, such as a window opening or closing
30-
*/
27+
/** A navigation event, such as a screen opening or closing. */
3128
data object Navigation : TraceType
3229

33-
/**
34-
* A background process such as a database query
35-
*/
30+
/** A background process such as a database query. */
3631
data object Process : TraceType
3732

38-
/**
39-
* A network request
40-
*/
33+
/** A network request. */
4134
data object Network : TraceType
4235

43-
/**
44-
* A change in application state, such as launch or memory warning
45-
*/
36+
/** A change in application state, such as launch or memory warning. */
4637
data object StateChange : TraceType
4738

48-
/**
49-
* A user action, such as tapping a button
50-
*/
39+
/** A user action, such as tapping a button. */
5140
data object User : TraceType
5241
}
5342

43+
/**
44+
* Central coordinator for the tracing/logging subsystem.
45+
*
46+
* Must be [initialized][initialize] before any [trace] calls will produce output.
47+
* Calls to [trace] made before initialization are silently dropped so that
48+
* modules which log during early startup don't crash.
49+
*
50+
* Supports two extension points:
51+
* - [TraceLogPlugin]s — transform log messages before they reach Timber (e.g. PII masking).
52+
* - [BreadcrumbSink]s — forward structured breadcrumbs to external services (e.g. Bugsnag).
53+
*/
5454
object TraceManager {
5555
private var fileTree: FileTree? = null
56+
57+
/** `true` after [initialize] has been called. [trace] is a no-op until then. */
58+
var initialized: Boolean = false
59+
private set
60+
5661
val plugins: MutableList<TraceLogPlugin> = mutableListOf()
5762
private val breadcrumbSinks = mutableListOf<BreadcrumbSink>()
5863
private var _userId: String? = null
5964
private var onUserIdChanged: ((String?) -> Unit)? = null
6065

66+
/** The current user identifier, forwarded to error reporters when set. */
6167
var userId: String?
6268
get() = _userId
6369
set(value) {
6470
_userId = value
6571
onUserIdChanged?.invoke(value)
6672
}
6773

74+
/** Register a listener invoked whenever [userId] changes. */
6875
fun setOnUserIdChanged(listener: ((String?) -> Unit)?) {
6976
onUserIdChanged = listener
7077
}
@@ -81,32 +88,54 @@ object TraceManager {
8188
fun removeSink(sink: BreadcrumbSink) { breadcrumbSinks.remove(sink) }
8289
fun sinks(): List<BreadcrumbSink> = breadcrumbSinks
8390

91+
/**
92+
* Bootstraps Timber with a [FileTree], plants default plugins
93+
* ([PiiMaskingPlugin], [RpcBodyFilterPlugin]), and marks the manager
94+
* as [initialized]. Safe to call multiple times; subsequent calls are no-ops.
95+
*/
8496
fun initialize(context: Context) {
85-
if (fileTree != null) return
97+
if (initialized) return
8698
val tree = FileTree(context, plugins = { plugins })
8799
fileTree = tree
88100
addPlugin(PiiMaskingPlugin())
89101
addPlugin(RpcBodyFilterPlugin())
90102
Timber.plant(tree)
103+
initialized = true
91104
}
92105

106+
/** Returns the current log file, or `null` if not yet initialized. */
93107
fun getLogFile(): File? = fileTree?.getLogFile()
94108

95109
fun clearLogs() {
96110
fileTree?.clearLogs()
97111
}
98112

113+
/** Tears down all state. Intended for tests only. */
99114
@androidx.annotation.VisibleForTesting
100115
internal fun reset() {
101116
fileTree?.let { Timber.uproot(it) }
102117
fileTree = null
118+
initialized = false
103119
plugins.clear()
104120
breadcrumbSinks.clear()
105121
_userId = null
106122
onUserIdChanged = null
107123
}
108124
}
109125

126+
/**
127+
* Primary logging entry point. Writes [message] to Timber and forwards a
128+
* breadcrumb to every registered [BreadcrumbSink].
129+
*
130+
* No-op if [TraceManager] has not been [initialized][TraceManager.initialize],
131+
* so callers in early startup paths don't need their own guard.
132+
*
133+
* @param message Human-readable description of the event.
134+
* @param tag Optional tag rendered as `[tag]` prefix in the log line.
135+
* @param metadata Builder for structured key-value pairs appended to the log line.
136+
* @param error If non-null, forwarded to [ErrorUtils.handleError] after logging.
137+
* @param type Categorization that determines the Timber log level.
138+
*/
110139
@SuppressLint("TimberExceptionLogging")
111140
fun trace(
112141
message: String,
@@ -115,6 +144,8 @@ fun trace(
115144
error: Throwable? = null,
116145
type: TraceType = if (error != null) TraceType.Error else TraceType.Log,
117146
) {
147+
if (!TraceManager.initialized) return
148+
118149
val tagBlock = tag?.let { "[$it] " }
119150
val tree = if (tagBlock == null) Timber else Timber.tag(tagBlock)
120151

@@ -151,6 +182,15 @@ fun trace(
151182
error?.let(ErrorUtils::handleError)
152183
}
153184

185+
/**
186+
* Executes [block], measures its wall-clock duration, and emits a [trace]
187+
* with the duration appended to [metadata].
188+
*
189+
* The block always runs even if [TraceManager] is not initialized; only
190+
* the trace output is gated.
191+
*
192+
* @param onComplete Called after tracing with the block's result and measured duration.
193+
*/
154194
fun <T> timedTrace(
155195
message: String,
156196
tag: String? = null,
@@ -176,6 +216,13 @@ fun <T> timedTrace(
176216
return result
177217
}
178218

219+
/**
220+
* Suspend variant of [timedTrace] that additionally tracks named steps.
221+
*
222+
* Call the `onStep` lambda inside [block] to record intermediate durations;
223+
* each step's elapsed time (relative to the previous step) is appended to
224+
* the metadata alongside the total duration.
225+
*/
179226
suspend fun <T> timedTraceSuspend(
180227
message: String,
181228
tag: String? = null,
@@ -214,6 +261,17 @@ suspend fun <T> timedTraceSuspend(
214261
return result
215262
}
216263

264+
/**
265+
* DSL builder for structured key-value metadata attached to [trace] calls.
266+
*
267+
* ```
268+
* trace("payment sent", metadata = {
269+
* "amount" to 5.00
270+
* "token" to "JFY"
271+
* "currency" to "USD"
272+
* })
273+
* ```
274+
*/
217275
class MetadataBuilder {
218276
private val map = mutableMapOf<String, Any>()
219277

@@ -224,7 +282,8 @@ class MetadataBuilder {
224282
fun build(): Map<String, Any> = map
225283
}
226284

227-
fun metadata(block: MetadataBuilder.() -> Unit): Map<String, Any> {
285+
/** Convenience factory that builds a metadata map from [block]. */
286+
private fun metadata(block: MetadataBuilder.() -> Unit): Map<String, Any> {
228287
val builder = MetadataBuilder()
229288
builder.block()
230289
return builder.build()

0 commit comments

Comments
 (0)