Java 使用带有追加的 getHead() 进行记录

Java logging using getHead() with append

我们正在开发一个 Web 应用程序,我正在服务器端实现事件日志记录。我决定一个不错的方法是扩展 Java 日志记录 API 以将事件记录到 CSV 文件中。我创建了一个 java.util.logging.Formatter class 如下:

public class EventLogCsvFormatter extends Formatter {
private static final SimpleDateFormat SDF = new SimpleDateFormat("MM/dd/yyyy h:mm a");
private static final String[] COL_HEADERS = {"Date/Time", "Source", "Event Type", "Application Context", "Description", "Fields"};

public EventLogCsvFormatter() {
}

@Override
public String getHead(Handler h) {
    return String.join(",", COL_HEADERS);
}

@Override
public String format(LogRecord record) {
    if (record instanceof EventLogRecord) {
        EventLogRecord customLogRecord = (EventLogRecord) record;
        String[] fields = customLogRecord.getFields();
        List<String> textList = new ArrayList<>();

        textList.add(SDF.format(new Date(record.getMillis())));
        textList.add(customLogRecord.getSource() != null ? customLogRecord.getSource() : "Not Given");
        textList.add(customLogRecord.getEventType() != null ? customLogRecord.getEventType().toString() : "Not Given");
        textList.add(customLogRecord.getEventContext() != null ? customLogRecord.getEventContext().toString() : "Not Given");
        textList.add(customLogRecord.getMessage());

        if (fields != null && fields.length > 0) {
            for (String field : fields) {
                textList.add(field);
            }
        }

        String retVal = "\n" + String.join(",", textList);
        return retVal;
    }

    return "";
}

}

实现 getHead() 方法的想法是为每个 CSV 文件提供 header 列。请注意,Java 自己的 XMLFormatter 在其 getHead() 方法中返回 XML 文件 header 字符串时做了类似的事情。事实证明,Java 日志记录在调用 getHead() 时没有考虑追加标志。附加标志基本上告诉记录器 re-open 现有文件并在启动时继续记录到它。因为我们的测试服务器经常被反弹,所以生成的大多数 CSV 文件在文件的多个位置都有 header 列名称,而不仅仅是在顶部。

我认为没有任何解决办法,因为几乎所有 Java 日志处理程序代码都有私有或包范围的字段和方法。所以,我什至无法编写自己的自定义处理程序来在这里工作。这是一个错误,我是 SOL?我应该继续使用不同的日志记录 API(例如 Log4J)吗?

与此问题相关的 FileHandler RFE 是 JDK-4629315: Appending of XML Logfiles doesn't merge new records and JDK-5036335: Provide method to obtain log file name(s) and path(s)

完成这项工作的诀窍是您必须能够在请求 header 时查询当前日志文件。如果当前日志文件长度为零,则格式化程序必须 return headers.

在您的自定义格式化程序中,您可以让 getHead 方法尝试定位打开的文件并使用 java.iojava.nio.

查询长度
@Override
public String getHead(Handler h) {
    boolean writeHeader = true;
    try {
        if (h instanceof FileHandler) {
            writeHeader = lengthOpen((FileHandler) h).longValue() == 0L;
        }
    } catch (SecurityException ignore) {
    }

    if (writeHeader) {
        return ""; //TODO: Insert your CSV headers.
    } else {
        return super.getHead(h); //Skip headers.
    }
}

private Number lengthOpen(Handler h) {
    if (h instanceof FileHandler) {
        String p = h.getClass().getName();
        LogManager manager = LogManager.getLogManager();
        p = manager.getProperty(p.concat(".pattern"));
        //TODO: Deal with FileHandler patterns.
        if (p != null) {
            File f = new File(p);
            //TODO: Implement file listing and filtering.
            return f.length();
        }
    }
    return 0L;
}

否则,如果您想进行一些黑客攻击,可以使用反射。

@Override
public String getHead(Handler h) {
    boolean writeHeader = true;
    try {
        if (h instanceof FileHandler) {
            writeHeader = lengthFrom((FileHandler) h).longValue() == 0L;
        }
    } catch (SecurityException ignore) {
    }

    if (writeHeader) {
        return ""; //TODO: Insert your CSV headers here.
    } else {
        return super.getHead(h); //Skip headers.
    }
}

private Number lengthFrom(FileHandler h) {
    try {
        Field f = StreamHandler.class.getDeclaredField("output");
        f.setAccessible(true);
        OutputStream out = (OutputStream) f.get(h);
        f = out.getClass().getDeclaredField("written");
        f.setAccessible(true);
        return (Number) f.get(out);
    } catch (ReflectiveOperationException roe) {
        h.getErrorManager().error(null, roe, ErrorManager.FORMAT_FAILURE);
    }
    return 0L;
}