Logging.kt
package com.shengzhiai.yugu
/**
* 日志输出接口。SDK 不会把 secretKey,签名值,token 与音频字节写进日志,appKey 只输出前 4 位加 `***`。
*/
public fun interface YuguLogger {
/** 输出一条日志,[error] 可为 null。 */
public fun log(level: LogLevel, tag: String, message: String, error: Throwable?)
}
/** 默认日志输出,写入 Logcat。 */
public class LogcatLogger : YuguLogger {
override fun log(level: LogLevel, tag: String, message: String, error: Throwable?) {
val priority = when (level) {
LogLevel.ERROR -> android.util.Log.ERROR
LogLevel.WARN -> android.util.Log.WARN
LogLevel.INFO -> android.util.Log.INFO
else -> android.util.Log.DEBUG
}
val text = if (error != null) message + '\n' + android.util.Log.getStackTraceString(error) else message
android.util.Log.println(priority, tag, text)
}
}
/**
* 指标回调,全部方法可选。
*
* - [onRequestStart]:REST 每次尝试发出前回调一次,`attempt` 从 1 开始。
* - [onRequestEnd]:REST 每个逻辑调用结束时回调一次,`latencyMs` 为含重试的总耗时,
* `attempts` 为总尝试次数,失败时 `error` 非空。
* - [onRetry]:每次重试前回调,`attempt` 为第几次重试。
* - [onSessionStateChanged]:实时会话状态变化。
* - [onReconnect]:实时会话每次重连的结果。
*
* 回调在调用线程或会话回调线程上执行,应尽快返回。回调抛出的异常会被 SDK 捕获并记日志。
*/
public interface YuguEventListener {
public fun onRequestStart(op: String, method: String, path: String, attempt: Int) {}
public fun onRequestEnd(op: String, httpStatus: Int, latencyMs: Long, attempts: Int, error: YuguException?) {}
public fun onRetry(op: String, attempt: Int, delayMs: Long, error: YuguException) {}
public fun onSessionStateChanged(sessionId: String, oldState: SessionState, newState: SessionState) {}
public fun onReconnect(sessionId: String, attempt: Int, succeeded: Boolean) {}
public companion object {
/** 什么都不做的实现。 */
@JvmField
public val NONE: YuguEventListener = object : YuguEventListener {}
}
}
/** Level filter, sink isolation and redaction helpers. */
internal class Log(private val level: LogLevel, private val sink: YuguLogger) {
fun enabled(l: LogLevel): Boolean = level.allows(l)
fun e(msg: String, t: Throwable? = null) = emit(LogLevel.ERROR, msg, t)
fun w(msg: String, t: Throwable? = null) = emit(LogLevel.WARN, msg, t)
fun i(msg: String) = emit(LogLevel.INFO, msg, null)
fun d(msg: String) = emit(LogLevel.DEBUG, msg, null)
fun d(block: () -> String) {
if (enabled(LogLevel.DEBUG)) emit(LogLevel.DEBUG, block(), null)
}
fun emit(l: LogLevel, msg: String, t: Throwable?) {
if (!level.allows(l)) return
try {
sink.log(l, TAG, msg, t)
} catch (_: Throwable) {
// a broken logger must never break a call
}
}
companion object {
const val TAG = "YuguSDK"
private val SECRET_QUERY = Regex("([?&](?:signature|token|secretKey)=)[^&#\\s]*", RegexOption.IGNORE_CASE)
private val APPKEY_QUERY = Regex("([?&]appKey=)([^&#\\s]*)")
/** Masks credentials in a URL: signature and token become ***, appKey keeps 4 chars. */
fun redactUrl(url: String): String {
val noSecrets = SECRET_QUERY.replace(url) { it.groupValues[1] + "***" }
return APPKEY_QUERY.replace(noSecrets) { it.groupValues[1] + Auth.maskAppKey(it.groupValues[2]) }
}
}
}