@@ -8,63 +8,70 @@ import kotlin.time.Duration
88import kotlin.time.TimeSource
99import 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+ */
1117sealed 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+ */
5454object 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" )
111140fun 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+ */
154194fun <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+ */
179226suspend 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+ */
217275class 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