Kaynağa Gözat

feat: 增加持久化日志

lilu 3 ay önce
ebeveyn
işleme
62cc357751
1 değiştirilmiş dosya ile 350 ekleme ve 0 silme
  1. 350 0
      android/app/src/main/kotlin/app/xixi/nomo/VLog.kt

+ 350 - 0
android/app/src/main/kotlin/app/xixi/nomo/VLog.kt

@@ -5,6 +5,7 @@ import android.util.Log
 import java.io.File
 import java.io.FileWriter
 import java.io.PrintWriter
+import java.io.RandomAccessFile
 import java.text.SimpleDateFormat
 import java.util.Date
 import java.util.Locale
@@ -26,10 +27,25 @@ object VLog {
     private const val MAX_LOG_SIZE = 10 * 1024 * 1024 // 10MB
     private const val MAX_LOG_FILES = 1 // 每种类型就保留一个日志文件
     
+    // 当前日志类型,用于同步写入到 PersistentLog
+    private var currentLogType: PersistentLog.LogType? = null
+    
     /**
      * 初始化日志系统
      */
     fun init(context: Context, name: String = "service") {
+        // 同时初始化 PersistentLog
+        currentLogType = when (name.lowercase()) {
+            "client" -> {
+                PersistentLog.initClient(context)
+                PersistentLog.LogType.CLIENT
+            }
+            else -> {
+                PersistentLog.initService(context)
+                PersistentLog.LogType.SERVICE
+            }
+        }
+        
         executor.execute {
             try {
                 val logDir = File(context.filesDir, "logs")
@@ -92,6 +108,11 @@ object VLog {
      * 写入日志
      */
     private fun writeLog(level: String, tag: String, message: String, throwable: Throwable? = null) {
+        // 同时写入到 PersistentLog(不输出到 logcat,因为调用方已经输出)
+        currentLogType?.let { logType ->
+            PersistentLog.writeLogOnly(logType, level, tag, message, throwable)
+        }
+        
         executor.execute {
             lock.withLock {
                 try {
@@ -166,3 +187,332 @@ object VLog {
     }
 }
 
+/**
+ * 持久化日志管理器
+ * 支持两种独立的日志类型:
+ * - Client: 客户端日志,最大10MB
+ * - Service: 服务日志,最大30MB
+ * 
+ * 特点:
+ * - 使用固定文件名,重启后继续追加,不会重置
+ * - 超过大小限制时自动截断旧日志,保留最近内容
+ */
+object PersistentLog {
+    
+    /**
+     * 日志类型枚举
+     */
+    enum class LogType(val fileName: String, val maxSize: Long) {
+        CLIENT("client.log", 10 * 1024 * 1024L),   // 10MB
+        SERVICE("service.log", 30 * 1024 * 1024L)  // 30MB
+    }
+    
+    private data class LogWriter(
+        var file: File,
+        var writer: PrintWriter?,
+        val maxSize: Long
+    )
+    
+    private val logWriters = mutableMapOf<LogType, LogWriter>()
+    private val lock = ReentrantLock()
+    private val executor = Executors.newSingleThreadExecutor()
+    private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
+    private var logDir: File? = null
+    
+    // 当文件超过最大大小时,保留最近的这个比例的内容
+    private const val TRUNCATE_KEEP_RATIO = 0.7
+    
+    /**
+     * 初始化客户端日志系统
+     */
+    fun initClient(context: Context) {
+        init(context, LogType.CLIENT)
+    }
+    
+    /**
+     * 初始化Service日志系统
+     */
+    fun initService(context: Context) {
+        init(context, LogType.SERVICE)
+    }
+    
+    /**
+     * 初始化指定类型的日志系统
+     */
+    private fun init(context: Context, logType: LogType) {
+        executor.execute {
+            lock.withLock {
+                try {
+                    val dir = File(context.filesDir, "logs")
+                    if (!dir.exists()) {
+                        dir.mkdirs()
+                    }
+                    logDir = dir
+                    
+                    // 使用固定文件名,支持追加
+                    val logFile = File(dir, logType.fileName)
+                    
+                    // 检查文件大小,如果超过限制则截断
+                    if (logFile.exists() && logFile.length() > logType.maxSize) {
+                        truncateLogFile(logFile, logType.maxSize)
+                    }
+                    
+                    // 创建或追加到日志文件
+                    val writer = PrintWriter(FileWriter(logFile, true), true)
+                    logWriters[logType] = LogWriter(logFile, writer, logType.maxSize)
+                    
+                    val timestamp = dateFormat.format(Date())
+                    writer.println("\n[$timestamp] [INFO] [PersistentLog] ========== 日志系统启动 (${logType.name}) ==========")
+                    writer.flush()
+                    
+                    Log.i("PersistentLog", "${logType.name} 日志系统初始化成功: ${logFile.absolutePath}, 当前大小: ${logFile.length() / 1024}KB")
+                } catch (e: Exception) {
+                    Log.e("PersistentLog", "初始化 ${logType.name} 日志系统失败", e)
+                }
+            }
+        }
+    }
+    
+    /**
+     * 截断日志文件,保留最近的内容
+     */
+    private fun truncateLogFile(file: File, maxSize: Long) {
+        try {
+            val keepSize = (maxSize * TRUNCATE_KEEP_RATIO).toLong()
+            val fileSize = file.length()
+            
+            if (fileSize <= maxSize) return
+            
+            Log.i("PersistentLog", "日志文件超过限制,开始截断: ${file.name}, 当前大小: ${fileSize / 1024 / 1024}MB")
+            
+            // 读取文件末尾的内容
+            RandomAccessFile(file, "r").use { raf ->
+                val skipBytes = fileSize - keepSize
+                raf.seek(skipBytes)
+                
+                // 跳过可能的不完整行
+                raf.readLine()
+                
+                // 读取剩余内容
+                val remainingBytes = ByteArray((fileSize - raf.filePointer).toInt())
+                raf.readFully(remainingBytes)
+                
+                // 写入新文件
+                file.writeBytes(remainingBytes)
+            }
+            
+            // 在文件开头添加截断标记
+            val tempFile = File(file.parent, "${file.name}.tmp")
+            val timestamp = dateFormat.format(Date())
+            tempFile.writeText("[$timestamp] [INFO] [PersistentLog] ========== 日志已截断,保留最近内容 ==========\n")
+            tempFile.appendBytes(file.readBytes())
+            tempFile.renameTo(file)
+            
+            Log.i("PersistentLog", "日志文件截断完成: ${file.name}, 新大小: ${file.length() / 1024 / 1024}MB")
+        } catch (e: Exception) {
+            Log.e("PersistentLog", "截断日志文件失败", e)
+            // 如果截断失败,尝试直接清空文件
+            try {
+                file.writeText("")
+                Log.w("PersistentLog", "截断失败,已清空日志文件: ${file.name}")
+            } catch (e2: Exception) {
+                Log.e("PersistentLog", "清空日志文件也失败", e2)
+            }
+        }
+    }
+    
+    /**
+     * 检查并轮转日志文件
+     */
+    private fun checkAndRotateIfNeeded(logType: LogType) {
+        val logWriter = logWriters[logType] ?: return
+        try {
+            if (logWriter.file.length() > logWriter.maxSize) {
+                logWriter.writer?.close()
+                truncateLogFile(logWriter.file, logWriter.maxSize)
+                logWriter.writer = PrintWriter(FileWriter(logWriter.file, true), true)
+            }
+        } catch (e: Exception) {
+            Log.e("PersistentLog", "轮转日志文件失败", e)
+        }
+    }
+    
+    /**
+     * 写入日志到指定类型(内部使用,同时输出到 logcat)
+     */
+    private fun writeLog(logType: LogType, level: String, tag: String, message: String, throwable: Throwable? = null) {
+        writeLogInternal(logType, level, tag, message, throwable)
+    }
+    
+    /**
+     * 只写入日志到文件,不输出到 logcat(供 VLog 调用,避免重复输出)
+     */
+    fun writeLogOnly(logType: LogType, level: String, tag: String, message: String, throwable: Throwable? = null) {
+        writeLogInternal(logType, level, tag, message, throwable)
+    }
+    
+    /**
+     * 内部写入日志方法
+     */
+    private fun writeLogInternal(logType: LogType, level: String, tag: String, message: String, throwable: Throwable? = null) {
+        executor.execute {
+            lock.withLock {
+                try {
+                    val logWriter = logWriters[logType] ?: return@withLock
+                    
+                    val timestamp = dateFormat.format(Date())
+                    val logMessage = "[$timestamp] [$level] [$tag] $message"
+                    
+                    logWriter.writer?.println(logMessage)
+                    throwable?.let {
+                        logWriter.writer?.println(it.stackTraceToString())
+                    }
+                    logWriter.writer?.flush()
+                    
+                    // 检查是否需要轮转
+                    checkAndRotateIfNeeded(logType)
+                } catch (e: Exception) {
+                    Log.e("PersistentLog", "写入日志失败", e)
+                }
+            }
+        }
+    }
+    
+    // ==================== 客户端日志方法 ====================
+    
+    /**
+     * 客户端 Info 级别日志
+     */
+    fun ci(tag: String, message: String) {
+        Log.i(tag, message)
+        writeLog(LogType.CLIENT, "INFO", tag, message)
+    }
+    
+    /**
+     * 客户端 Debug 级别日志
+     */
+    fun cd(tag: String, message: String) {
+        Log.d(tag, message)
+        writeLog(LogType.CLIENT, "DEBUG", tag, message)
+    }
+    
+    /**
+     * 客户端 Warning 级别日志
+     */
+    fun cw(tag: String, message: String, throwable: Throwable? = null) {
+        Log.w(tag, message, throwable)
+        writeLog(LogType.CLIENT, "WARN", tag, message, throwable)
+    }
+    
+    /**
+     * 客户端 Error 级别日志
+     */
+    fun ce(tag: String, message: String, throwable: Throwable? = null) {
+        Log.e(tag, message, throwable)
+        writeLog(LogType.CLIENT, "ERROR", tag, message, throwable)
+    }
+    
+    // ==================== Service日志方法 ====================
+    
+    /**
+     * Service Info 级别日志
+     */
+    fun si(tag: String, message: String) {
+        Log.i(tag, message)
+        writeLog(LogType.SERVICE, "INFO", tag, message)
+    }
+    
+    /**
+     * Service Debug 级别日志
+     */
+    fun sd(tag: String, message: String) {
+        Log.d(tag, message)
+        writeLog(LogType.SERVICE, "DEBUG", tag, message)
+    }
+    
+    /**
+     * Service Warning 级别日志
+     */
+    fun sw(tag: String, message: String, throwable: Throwable? = null) {
+        Log.w(tag, message, throwable)
+        writeLog(LogType.SERVICE, "WARN", tag, message, throwable)
+    }
+    
+    /**
+     * Service Error 级别日志
+     */
+    fun se(tag: String, message: String, throwable: Throwable? = null) {
+        Log.e(tag, message, throwable)
+        writeLog(LogType.SERVICE, "ERROR", tag, message, throwable)
+    }
+    
+    // ==================== 工具方法 ====================
+    
+    /**
+     * 获取客户端日志文件路径
+     */
+    fun getClientLogFilePath(): String? {
+        return logWriters[LogType.CLIENT]?.file?.absolutePath
+    }
+    
+    /**
+     * 获取Service日志文件路径
+     */
+    fun getServiceLogFilePath(): String? {
+        return logWriters[LogType.SERVICE]?.file?.absolutePath
+    }
+    
+    /**
+     * 获取日志目录
+     */
+    fun getLogDir(): File? {
+        return logDir
+    }
+    
+    /**
+     * 关闭客户端日志系统
+     */
+    fun closeClient() {
+        close(LogType.CLIENT)
+    }
+    
+    /**
+     * 关闭Service日志系统
+     */
+    fun closeService() {
+        close(LogType.SERVICE)
+    }
+    
+    /**
+     * 关闭指定类型的日志系统
+     */
+    private fun close(logType: LogType) {
+        executor.execute {
+            lock.withLock {
+                try {
+                    logWriters[logType]?.writer?.close()
+                    logWriters.remove(logType)
+                } catch (e: Exception) {
+                    Log.e("PersistentLog", "关闭 ${logType.name} 日志系统失败", e)
+                }
+            }
+        }
+    }
+    
+    /**
+     * 关闭所有日志系统
+     */
+    fun closeAll() {
+        executor.execute {
+            lock.withLock {
+                try {
+                    logWriters.values.forEach { it.writer?.close() }
+                    logWriters.clear()
+                } catch (e: Exception) {
+                    Log.e("PersistentLog", "关闭日志系统失败", e)
+                }
+            }
+        }
+    }
+}
+