Skip to content

Commit c259e01

Browse files
authored
Add common Firebase logging interface (#5469)
Add a common Firebase logging interface to make logging more consistent and make testing easier. With this we could make tests that assert specific things were logged, for example, Sessions could assert an event was logged.
1 parent 1ec75d4 commit c259e01

File tree

2 files changed

+211
-0
lines changed

2 files changed

+211
-0
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.logger
18+
19+
import android.util.Log
20+
import androidx.annotation.VisibleForTesting
21+
import java.util.concurrent.ConcurrentHashMap
22+
23+
/**
24+
* Common logger interface that handles Android logcat logging for Firebase SDKs.
25+
*
26+
* @hide
27+
*/
28+
sealed class Logger
29+
private constructor(
30+
val tag: String,
31+
var enabled: Boolean,
32+
var minLevel: Level,
33+
) {
34+
@JvmOverloads
35+
fun verbose(format: String, vararg args: Any?, throwable: Throwable? = null): Int =
36+
logIfAble(Level.VERBOSE, format, args, throwable = throwable)
37+
38+
@JvmOverloads
39+
fun verbose(msg: String, throwable: Throwable? = null): Int =
40+
logIfAble(Level.VERBOSE, msg, throwable = throwable)
41+
42+
@JvmOverloads
43+
fun debug(format: String, vararg args: Any?, throwable: Throwable? = null): Int =
44+
logIfAble(Level.DEBUG, format, args, throwable = throwable)
45+
46+
@JvmOverloads
47+
fun debug(msg: String, throwable: Throwable? = null): Int =
48+
logIfAble(Level.DEBUG, msg, throwable = throwable)
49+
50+
@JvmOverloads
51+
fun info(format: String, vararg args: Any?, throwable: Throwable? = null): Int =
52+
logIfAble(Level.INFO, format, args, throwable = throwable)
53+
54+
@JvmOverloads
55+
fun info(msg: String, throwable: Throwable? = null): Int =
56+
logIfAble(Level.INFO, msg, throwable = throwable)
57+
58+
@JvmOverloads
59+
fun warn(format: String, vararg args: Any?, throwable: Throwable? = null): Int =
60+
logIfAble(Level.WARN, format, args, throwable = throwable)
61+
62+
@JvmOverloads
63+
fun warn(msg: String, throwable: Throwable? = null): Int =
64+
logIfAble(Level.WARN, msg, throwable = throwable)
65+
66+
@JvmOverloads
67+
fun error(format: String, vararg args: Any?, throwable: Throwable? = null): Int =
68+
logIfAble(Level.ERROR, format, args, throwable = throwable)
69+
70+
@JvmOverloads
71+
fun error(msg: String, throwable: Throwable? = null): Int =
72+
logIfAble(Level.ERROR, msg, throwable = throwable)
73+
74+
/** Log if [enabled] is set and the given level is loggable. */
75+
private fun logIfAble(
76+
level: Level,
77+
format: String,
78+
args: Array<out Any?> = emptyArray(),
79+
throwable: Throwable?,
80+
): Int =
81+
if (enabled && (minLevel.priority <= level.priority || Log.isLoggable(tag, level.priority))) {
82+
log(level, format, args, throwable = throwable)
83+
} else {
84+
0
85+
}
86+
87+
abstract fun log(
88+
level: Level,
89+
format: String,
90+
args: Array<out Any?>,
91+
throwable: Throwable?,
92+
): Int
93+
94+
/** Simple wrapper around [Log]. */
95+
private class AndroidLogger(
96+
tag: String,
97+
enabled: Boolean,
98+
minLevel: Level,
99+
) : Logger(tag, enabled, minLevel) {
100+
override fun log(
101+
level: Level,
102+
format: String,
103+
args: Array<out Any?>,
104+
throwable: Throwable?,
105+
): Int {
106+
val msg = if (args.isEmpty()) format else String.format(format, *args)
107+
return when (level) {
108+
Level.VERBOSE -> throwable?.let { Log.v(tag, msg, throwable) } ?: Log.v(tag, msg)
109+
Level.DEBUG -> throwable?.let { Log.d(tag, msg, throwable) } ?: Log.d(tag, msg)
110+
Level.INFO -> throwable?.let { Log.i(tag, msg, throwable) } ?: Log.i(tag, msg)
111+
Level.WARN -> throwable?.let { Log.w(tag, msg, throwable) } ?: Log.w(tag, msg)
112+
Level.ERROR -> throwable?.let { Log.e(tag, msg, throwable) } ?: Log.e(tag, msg)
113+
}
114+
}
115+
}
116+
117+
/** Fake implementation that allows recording and asserting on log messages. */
118+
@VisibleForTesting
119+
class FakeLogger
120+
internal constructor(
121+
tag: String,
122+
enabled: Boolean,
123+
minLevel: Level,
124+
) : Logger(tag, enabled, minLevel) {
125+
private val record: MutableList<String> = ArrayList()
126+
127+
override fun log(
128+
level: Level,
129+
format: String,
130+
args: Array<out Any?>,
131+
throwable: Throwable?,
132+
): Int {
133+
val logMessage = toLogMessage(level, format, args, throwable = throwable)
134+
println("Log: $logMessage")
135+
record.add(logMessage)
136+
return logMessage.length
137+
}
138+
139+
/** Clear the recorded log messages. */
140+
@VisibleForTesting fun clearLogMessages(): Unit = record.clear()
141+
142+
/** Returns if the record has any message that contains the given [message] as a substring. */
143+
@VisibleForTesting
144+
fun hasLogMessage(message: String): Boolean = record.any { it.contains(message) }
145+
146+
/** Returns if the record has any message that matches the given [predicate]. */
147+
@VisibleForTesting
148+
fun hasLogMessageThat(predicate: (String) -> Boolean): Boolean = record.any(predicate)
149+
150+
/** Builds a log message from all the log params. */
151+
private fun toLogMessage(
152+
level: Level,
153+
format: String,
154+
args: Array<out Any?>,
155+
throwable: Throwable?,
156+
): String {
157+
val msg = if (args.isEmpty()) format else String.format(format, *args)
158+
return throwable?.let { "$level $msg ${Log.getStackTraceString(throwable)}" } ?: "$level $msg"
159+
}
160+
}
161+
162+
/** Log levels with each [priority] that matches [Log]. */
163+
enum class Level(internal val priority: Int) {
164+
VERBOSE(Log.VERBOSE),
165+
DEBUG(Log.DEBUG),
166+
INFO(Log.INFO),
167+
WARN(Log.WARN),
168+
ERROR(Log.ERROR),
169+
}
170+
171+
companion object {
172+
private val loggers = ConcurrentHashMap<String, Logger>()
173+
174+
/** Gets (or creates) the single instance of [Logger] with the given [tag]. */
175+
@JvmStatic
176+
fun getLogger(
177+
tag: String,
178+
enabled: Boolean = true,
179+
minLevel: Level = Level.INFO,
180+
): Logger = loggers.getOrPut(tag) { AndroidLogger(tag, enabled, minLevel) }
181+
182+
/** Sets (or replaces) the instance of [Logger] with the given [tag] for testing purposes. */
183+
@VisibleForTesting
184+
@JvmStatic
185+
fun setupFakeLogger(
186+
tag: String,
187+
enabled: Boolean = true,
188+
minLevel: Level = Level.DEBUG,
189+
): FakeLogger {
190+
val fakeLogger = FakeLogger(tag, enabled, minLevel)
191+
loggers[tag] = fakeLogger
192+
return fakeLogger
193+
}
194+
}
195+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/** @hide */
16+
package com.google.firebase.logger;

0 commit comments

Comments
 (0)