All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.pamirs.pradar.PradarRollingFileAppender Maven / Gradle / Ivy

There is a newer version: 1.0.2
Show newest version
package com.pamirs.pradar;


import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;

import java.io.*;
import java.nio.channels.FileLock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 支持多进程文件滚动的实现。注意:这个类的实现没有做并发保护,
 * 使用时必须保证单线程操作。一般搭配 {@link SyncAppender} 或者 {@link AsyncAppender}
 * 使用。一般配合 {@link PradarLogDaemon} 使用。
 */
class PradarRollingFileAppender extends PradarAppender {

    /**
     * 日志刷新间隔,buffer 时间超过间隔,则把缓存的日志数据刷新出去
     */
    private static final long LOG_FLUSH_INTERVAL = TimeUnit.SECONDS.toMillis(1);

    /**
     * 默认输出缓冲大小,write(2) 在  O_APPEND 模式下,写入内容小于一个 page(一般是 4K),
     * 可以保证多进程并发写同一个文件不会写乱。
     * 

* see UNIX环境高级编程(第2版) 第3章3.11节 */ private static final int DEFAULT_BUFFER_SIZE = 4 * 1024; // 4KB /** * 最大备份数 */ private int maxBackupIndex = 3; /** * 单个 Pradar 日志文件的大小 */ private final long maxFileSize; /** * 日志缓存大小 */ private final int bufferSize = DEFAULT_BUFFER_SIZE; private final String filePath; private final AtomicBoolean isRolling = new AtomicBoolean(false); private BufferedOutputStream bos = null; private long nextFlushTime = 0L; /** * 累计输出到文件的字节大小,在多进程时不能保证安全,需要定时更新 */ private long outputByteSize = 0L; // Pradar selfLog 自己打印日志时,不能递归输出,避免重入 private final boolean selfLogEnabled; /** * 标记发现了多进程同时写日志。在这种情况,需要避免一次写的日志超过 * 4KB,否则按这种实现会出现日志内容交织的问题 */ private boolean multiProcessDetected = false; private long lastFileSuffix = 0; /** * 要被清理的日志文件后缀 */ private static final String DELETE_FILE_SUBFIX = ".deleted"; public PradarRollingFileAppender(String filePath, long maxFileSize) { this(filePath, maxFileSize, true); } public PradarRollingFileAppender(String filePath, long maxFileSize, boolean selfLogEnabled) { initCurrentSize(filePath); this.filePath = filePath; this.maxFileSize = maxFileSize; this.selfLogEnabled = selfLogEnabled; setFile(); } public void shutdown() { close(); } private File getSmalllestFile() { final String fileName = new File(filePath).getName(); File file = new File(filePath + '.' + lastFileSuffix).getParentFile(); File[] files = file.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String suffix) { if (suffix.indexOf(".") == -1) { return false; } String lastSize = suffix.substring(suffix.lastIndexOf('.') + 1); return suffix.startsWith(fileName) && NumberUtils.isDigits(lastSize); } }); if (ArrayUtils.isEmpty(files)) { return null; } if (files.length <= maxBackupIndex) { return null; } long maxSize = Long.MAX_VALUE; File leastFile = null; for (File f : files) { if (!f.isFile()) { continue; } String fName = f.getName(); if (fName.indexOf('.') == -1) { continue; } String suffix = fName.substring(fName.lastIndexOf('.') + 1); if (!NumberUtils.isDigits(StringUtils.trim(suffix))) { continue; } Long size = Long.valueOf(StringUtils.trim(suffix)); if (size < maxSize) { maxSize = size; leastFile = f; } } if (maxSize == Long.MAX_VALUE || leastFile == null) { return null; } return leastFile; } private void initCurrentSize(String filePath) { File file = new File(filePath).getParentFile(); final String fileName = new File(filePath).getName(); File[] files = file.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String suffix) { if (suffix.indexOf(".") == -1) { return false; } String lastSize = suffix.substring(suffix.lastIndexOf('.') + 1); return suffix.startsWith(fileName) && NumberUtils.isDigits(lastSize); } }); if (ArrayUtils.isEmpty(files)) { lastFileSuffix = 0; return; } long maxSize = 0; for (File f : files) { if (!f.isFile()) { continue; } String fName = f.getName(); String suffix = fName.substring(fName.lastIndexOf('.') + 1); Long size = Long.valueOf(StringUtils.trim(suffix)); if (size > maxSize) { maxSize = size; } } this.lastFileSuffix = maxSize; } private void setFile() { try { File logFile = new File(filePath + '.' + lastFileSuffix); if (!logFile.exists()) { File parentFile = logFile.getParentFile(); if (!parentFile.exists() && !parentFile.mkdirs()) { doSelfLog("[ERROR] Fail to mkdirs: " + parentFile.getAbsolutePath()); return; } try { if (!logFile.createNewFile()) { doSelfLog("[ERROR] Fail to create file, it exists: " + logFile.getAbsolutePath()); } } catch (IOException e) { doSelfLog("[ERROR] Fail to create file: " + logFile.getAbsolutePath() + ", error=" + e.getMessage()); } } if (!logFile.isFile() || !logFile.canWrite()) { doSelfLog("[ERROR] Invalid file, exists=" + logFile.exists() + ", isFile=" + logFile.isFile() + ", canWrite=" + logFile.canWrite() + ", path=" + logFile.getAbsolutePath()); return; } FileOutputStream ostream = new FileOutputStream(logFile, true); // 必须 true 保证 O_APPEND this.bos = new BufferedOutputStream(ostream, bufferSize); this.outputByteSize = logFile.length(); } catch (Throwable e) { doSelfLog("[ERROR] Fail to create file to write: " + filePath + ", error=" + e.getMessage()); } } @Override public void append(String log) { BufferedOutputStream bos = this.bos; if (bos != null) { try { waitUntilRollFinish(); byte[] bytes = log.getBytes(Pradar.DEFAULT_CHARSET); int len = bytes.length; if (len > DEFAULT_BUFFER_SIZE && this.multiProcessDetected) { len = DEFAULT_BUFFER_SIZE; bytes[len - 1] = '\n'; } bos.write(bytes, 0, len); outputByteSize += len; if (outputByteSize >= maxFileSize) { rollOver(); } else { // 超过指定刷新时间没刷新,就刷新一次 if (System.currentTimeMillis() >= nextFlushTime) { flush(); } } } catch (Exception e) { doSelfLog("[ERROR] fail to write log to file " + filePath + ", error=" + e.getMessage()); close(); setFile(); } } } @Override public void flush() { final BufferedOutputStream bos = this.bos; if (bos != null) { try { bos.flush(); nextFlushTime = System.currentTimeMillis() + LOG_FLUSH_INTERVAL; } catch (Throwable e) { doSelfLog("[WARN] Fail to flush OutputStream: " + filePath + ", " + e.getMessage()); } } } @Override public void rollOver() { final String lockFilePath = filePath + ".lock"; RandomAccessFile raf = null; FileLock fileLock = null; if (!isRolling.compareAndSet(false, true)) { return; } try { raf = new RandomAccessFile(lockFilePath, "rw"); fileLock = raf.getChannel().tryLock(); if (fileLock != null) { // 重新更新一下文件大小 reload(); if (outputByteSize >= maxFileSize) { File leastFile = null; while ((leastFile = getSmalllestFile()) != null) { File target = new File(leastFile.getAbsolutePath() + DELETE_FILE_SUBFIX); if (!leastFile.renameTo(target) && !leastFile.delete()) { doSelfLog("[ERROR] Fail to delete or rename file: " + leastFile.getAbsolutePath() + " to " + target.getAbsolutePath()); } } close(); this.lastFileSuffix += (new File(filePath + "." + lastFileSuffix).length()); setFile(); } } } catch (IOException e) { doSelfLog("[ERROR] Fail rollover file: " + filePath + ", error=" + e.getMessage()); } finally { isRolling.set(false); if (fileLock != null) { try { fileLock.release(); } catch (IOException e) { doSelfLog("[ERROR] Fail to release file lock: " + lockFilePath + ", error=" + e.getMessage()); } } if (raf != null) { try { raf.close(); } catch (IOException e) { doSelfLog("[WARN] Fail to close file lock: " + lockFilePath + ", error=" + e.getMessage()); } } if (fileLock != null) { if (!new File(lockFilePath).delete()) { doSelfLog("[WARN] Fail to delete file lock: " + lockFilePath); } } } } @Override public void close() { BufferedOutputStream bos = this.bos; if (bos != null) { try { bos.close(); } catch (IOException e) { doSelfLog("[WARN] Fail to close OutputStream: " + e.getMessage()); } this.bos = null; } } /** * 如果文件变小,或者被删除,重新加载文件 */ @Override public void reload() { flush(); File logFile = new File(filePath + '.' + lastFileSuffix); long fileSize = logFile.length(); if (this.bos == null || fileSize < outputByteSize) { // 可以判断文件已经滚动,或已删除 doSelfLog("[INFO] Log file rolled over by outside: " + filePath + ", force reload"); close(); setFile(); } else if (fileSize > outputByteSize) { // 如果文件大小和预期不相等,但是在增加,一般是多进程并发写的情况, // 为了避免多进程反复打开文件,不做 reload。 // 代价是最坏情况可能给滚动后的文件多写 maxFileSize 字节的数据。 this.outputByteSize = fileSize; if (!this.multiProcessDetected) { this.multiProcessDetected = true; if (selfLogEnabled) { doSelfLog("[WARN] Multi-process file write detected: " + filePath); } } } else { // 一般是单进程写的情况,符合预期 } } /** * 自动清理日志,以 .deleted 结尾的文件要清理掉, * 单独把删除文件的动作剥离出来,是因为 Linux 删除比较大的文件耗时比较长, * 避免影响正常写逻辑 */ @Override public void cleanup() { try { File logFile = new File(filePath + '.' + lastFileSuffix); File parentDir = logFile.getParentFile(); if (parentDir != null && parentDir.isDirectory()) { final String baseFileName = new File(filePath).getName(); File[] filesToDelete = parentDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { if (name != null && name.startsWith(baseFileName) && name.endsWith(DELETE_FILE_SUBFIX)) { return true; } return false; } }); if (filesToDelete != null && filesToDelete.length > 0) { for (File f : filesToDelete) { boolean success = f.delete(); if (success) { doSelfLog("[INFO] Deleted log file: " + f.getAbsolutePath()); } else if (f.exists()) { doSelfLog("[ERROR] Fail to delete log file: " + f.getAbsolutePath()); } } } } } catch (Throwable e) { doSelfLog("[ERROR] Fail to cleanup log file, error=" + e.getMessage()); } } void waitUntilRollFinish() { while (isRolling.get()) { try { Thread.sleep(1L); } catch (Throwable e) { e.printStackTrace(); } } } private void doSelfLog(String log) { if (selfLogEnabled) { //Pradar.selfLog(log); } else { System.out.println("SIMULATOR:" + log); } } public int getMaxBackupIndex() { return maxBackupIndex; } public void setMaxBackupIndex(int maxBackupIndex) { if (maxBackupIndex < 1) { throw new IllegalArgumentException("maxBackupIndex < 1: " + maxBackupIndex); } this.maxBackupIndex = maxBackupIndex; } @Override public String toString() { return "PradarRollingFileAppender [filePath=" + filePath + "]"; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy