思考:如何设计一个好的日志库?

思考:如何设计一个好的日志库?

需要具备的功能点:独立线程、高并发、减少IO、线程安全、日志轮换、日志压缩、日志清理

设计了一个日志管理类的雏形:

LogManager 是一个用于管理 Android 应用程序日志文件的类。它提供了日志记录、日志文件管理和日志轮换等功能。该类能够处理并存储日志信息,支持多线程环境下的安全日志写入,适合需要记录运行时信息的应用程序。

主要功能:

  • 日志文件管理
    支持指定日志文件大小(默认为 1MB)和最大日志文件数量(默认为 5)。
    实现了日志轮换功能,当日志文件数量达到上限时,自动压缩并删除最旧的日志文件。

  • 日志记录机制
    使用线程安全的队列(BlockingQueue)来存储日志条目,确保在多线程环境下的安全性。
    通过单独的线程处理日志条目,按批次(默认批量大小为 10)写入日志文件。

  • 日志条目格式
    每条日志信息包含时间戳、日志级别和消息,格式为 “ t i m e s t a m p [ timestamp [ timestamp[level]: $message\n”。

  • 文件 I/O
    使用 RandomAccessFile 和 FileChannel 进行日志写入,支持高效的文件操作和大小管理。
    flushLogBuffer 方法确保任何累积的日志条目都被及时写入文件。

  • 日志压缩
    当旧的日志文件因轮换而被删除时,会将其压缩为 .gz 格式,节省存储空间。

  • 日志读取与清除
    提供读取日志文件内容和清除日志的功能。

LogManager设计如下:

package com.example.androidx.mmap

import android.content.Context
import android.util.Log
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.RandomAccessFile
import java.nio.channels.FileChannel
import java.nio.charset.StandardCharsets.UTF_8
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.BlockingQueue
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.zip.GZIPOutputStream


class LogManager(
    context: Context,
    private val logFileSize: Int = 1 * 1024, // 日志文件大小:默认为1MB
    private val maxLogFiles: Int = 5, // 最大日志文件数量:默认为5
    private val batchSize: Int = 10 // 批量写入日志的大小
) {
    private var logFile: File =
        File(context.filesDir, "log_${System.currentTimeMillis()}$LOG_FILE_EXTENSION")
    private val QUEUE_CAPACITY: Int = 1000 // 日志队列的容量
    private val logQueue: BlockingQueue<String> = ArrayBlockingQueue(QUEUE_CAPACITY) // 存储日志消息的队列
    private val executor = Executors.newSingleThreadExecutor() // 用于日志记录的单线程执行器
    private val isRunning = AtomicBoolean(true) // 控制日志线程的运行状态
    private val logBuffer = StringBuilder() // 用于累积日志条目的缓冲区

    // 实际文件大小
    private var actualContentSize: Long = 0

    init {
        initializeLogFile() // 初始化日志文件
        startLoggingThread() // 启动日志记录线程
    }

    // 初始化日志文件的方法
    private fun initializeLogFile() {
        // 这样设置后,会出现文件大小固定为1M且初始为NULL的情况
//        RandomAccessFile(logFile, "rw").use { it.setLength(logFileSize.toLong()) }
        if (logFile.exists()) {
            actualContentSize = logFile.length()
        } else {
            logFile.createNewFile()
            actualContentSize = 0
        }
    }

    // 启动日志记录线程的方法
    private fun startLoggingThread() {
        executor.execute {
            while (isRunning.get()) {
                try {
                    val logEntry = logQueue.take() // 等待并获取日志条目
                    accumulateLog(logEntry) // 累积日志条目
                } catch (e: InterruptedException) {
                    Thread.currentThread().interrupt() // 恢复中断状态
                }
            }
            // 在关闭时写入缓冲区中剩余的日志
            flushLogBuffer()
        }
    }

    // 记录日志的方法
    fun log(level: String, message: String) {
        val timestamp = System.currentTimeMillis()
        val logEntry = "$timestamp [$level]: $message\n"
        if (!logQueue.offer(logEntry)) {
            Log.w("LogManager", "日志队列已满,日志条目已丢弃: $logEntry")
        }
    }

    // 累积日志条目的方法
    private fun accumulateLog(logEntry: String) {
        logBuffer.append(logEntry)
        if (logBuffer.lineCount() >= batchSize) {
            flushLogBuffer()
        }
    }

    // 将累积的日志写入文件的方法
    private fun flushLogBuffer() {
        if (logBuffer.isNotEmpty()) {
            writeLog(logBuffer.toString())
            logBuffer.clear()
        }
    }

    // 将日志写入文件的方法
    private fun writeLog(logEntries: String) {
        try {
            RandomAccessFile(logFile, "rw").use { randomAccessFile ->
                val fileChannel = randomAccessFile.channel
                val content = logEntries.toByteArray(UTF_8)

                // 重新映射文件,确保映射大小足够
                val buffer = fileChannel.map(
                    FileChannel.MapMode.READ_WRITE,
                    actualContentSize,
                    content.size.toLong()
                )
                buffer.put(content) // 追加写入日志条目
                actualContentSize += content.size
                fileChannel.force(true)

                // 先把上一条日志写进入,再检查是否有足够空间,没有的话进行日志轮换
                if (actualContentSize > logFileSize) {
                    Log.d("lpftag", "日志文件大小超过限制,生成新的日志文件")
                    rotateLogs()
                    actualContentSize = 0
                }
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    // 日志轮换的方法
    private fun rotateLogs() {
        // 获取当前日志文件的目录
        val logDir = logFile.parentFile ?: return
        val existingLogs = logDir.listFiles { file ->
            file.name.startsWith("log_") && file.name.endsWith(LOG_FILE_EXTENSION)
        }

        // 删除最旧的日志文件
        if (existingLogs != null && existingLogs.size >= maxLogFiles) {
            val oldestLog = existingLogs.minByOrNull { it.lastModified() }
            oldestLog?.let {
                compressLogFile(it)
                it.delete()
            }
        }

        // 创建一个新的日志文件
        logFile = File(logDir, "log_${System.currentTimeMillis()}$LOG_FILE_EXTENSION")
        initializeLogFile()
    }

    // 压缩日志文件的方法
    private fun compressLogFile(logFile: File) {
        try {
            val gzipFile = File(logFile.parent, "${logFile.name}.gz")
            FileInputStream(logFile).use { fis ->
                FileOutputStream(gzipFile).use { fos ->
                    GZIPOutputStream(fos).use { gzipOut ->
                        fis.copyTo(gzipOut)
                    }
                }
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    // 读取日志的方法
    fun readLogs(): String {
        val stringBuilder = StringBuilder()
        return try {
            logFile.let { file ->
                file.bufferedReader().use { reader ->
                    var line: String? = reader.readLine()
                    while (line != null) {
                        stringBuilder.append(line)
                        line = reader.readLine()
                    }
                }
                stringBuilder.toString()
            }
        } catch (e: IOException) {
            e.printStackTrace()
            "读取日志时出错。"
        }
    }

    // 清除日志的方法
    fun clearLogs() {
        try {
            initializeLogFile()
            logBuffer.clear()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    fun shutdown() {
        isRunning.set(false)
        executor.shutdown()
    }

    companion object {
        private const val LOG_FILE_EXTENSION: String = ".log"
    }
}

// 扩展函数:计算StringBuilder中的行数
private fun StringBuilder.lineCount(): Int {
    return this.toString().split("\n").size
}

调用LogManager使用:

package com.example.androidx.mmap

import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.common_utils.PermissionHelper
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class LogMmapActivity : AppCompatActivity() {

    var logManager: LogManager? = null

    val REQUEST_PERMISSIONS = 1
    val PERMISSIONS = arrayOf(
        android.Manifest.permission.READ_EXTERNAL_STORAGE,
        android.Manifest.permission.WRITE_EXTERNAL_STORAGE
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 检查权限
        if (!PermissionHelper.hasPermissions(this, PERMISSIONS)) {
            PermissionHelper.requestPermissions(this, PERMISSIONS, REQUEST_PERMISSIONS)
        } else {
            initLogManager()
        }

        setContent {
            LogScreen(logManager)
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        PermissionHelper.onRequestPermissionsResult(
            requestCode,
            permissions,
            grantResults,
            onPermissionsGranted = {
                initLogManager()
            },
            onPermissionsDenied = {
//                finish()
                Toast.makeText(this, "权限被拒绝", Toast.LENGTH_SHORT).show()
            }
        )
    }


    private fun initLogManager() {
        logManager = LogManager(LogMmapActivity@this)

        // 测试日志记录
        testLogManager()
    }

    private fun testLogManager() {
            // 模拟 Android Context(在实际的 Android 应用中,您应该使用实际的 Context)
            // 初始化 LogManager
//            val logManager = LogManager(this, logFileSize = 1024 * 1024, maxLogFiles = 5, batchSize = 10)
            // 创建一个固定数量线程的线程池
            val executor = Executors.newFixedThreadPool(5)
            // 模拟多个线程写入日志
            for (i in 1..10) {
                executor.submit {
                    for (j in 1..20) {
                        // 每个线程写入 20 条日志条目
                        logManager?.log("INFO", "Thread $i logging message $j")
                        // 模拟一些延迟以模拟实际日志记录场景
                        Thread.sleep((100..500).random().toLong()) // 随机休眠 100ms 到 500ms 之间
                    }
                }
            }
            // 在延迟后关闭执行器,以允许所有日志记录完成
            executor.shutdown()
            try {
                // 等待所有任务完成
                if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
                    executor.shutdownNow() // 如果未在时间内完成,则强制关闭
                }
            } catch (e: InterruptedException ) {
                executor.shutdownNow() // 如果被中断,则强制关闭
            }
            // 关闭 LogManager 确保所有日志都被写入
            logManager?.shutdown()
            println("Logging completed.")
    }
}

@Composable
fun LogScreen(logManager:LogManager? ) {
    var logMessage by remember { mutableStateOf("Hello, World!") }

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp)
    ) {
        TextField(
            value = logMessage,
            onValueChange = { logMessage = it },
            label = { Text("Log Message") },
            textStyle = TextStyle(fontSize = 16.sp),
            modifier = Modifier.padding(8.dp)
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = {
             Log.d("LogMmapActivity", logMessage)
            logManager?.log("INFO",logMessage)
        }) {
            Text("写日志")
        }
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = {
            // Log.e("LogMmapActivity", logMessage)
            val logs = logManager?.readLogs()?:"No logs"
            println(logs)
        }) {
            Text("读日志")
        }
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = {
            // Log.e("LogMmapActivity", logMessage)
            logManager?.clearLogs()
        }) {
            Text("清空日志")
        }
    }

}
可优化的点:
  • 异常处理不足:
    目前的异常处理主要是打印堆栈信息,未能有效地反馈给用户或调用者。可以考虑抛出自定义异常或使用日志框架记录异常信息。

  • 日志级别的灵活性:
    日志级别的使用较为简单,未提供更细粒度的控制(如 DEBUG、INFO、WARN、ERROR 等)。可以考虑引入更复杂的日志级别管理。

  • 性能优化:
    在高频率日志记录的场景下,可能会导致性能瓶颈。可以考虑使用更高效的日志写入方式,如异步写入

  • 缺乏配置选项:
    当前类的配置选项相对固定,缺乏动态配置的能力。可以考虑引入配置文件或用户设置以便于调整。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lpftobetheone

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值