org.h2.message.TraceSystem Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2004-2022 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (https://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.message;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicReferenceArray;
import org.h2.api.ErrorCode;
import org.h2.engine.Constants;
import org.h2.jdbc.JdbcException;
import org.h2.store.fs.FileUtils;
import org.h2.util.IOUtils;
/**
* The trace mechanism is the logging facility of this database. There is
* usually one trace system per database. It is called 'trace' because the term
* 'log' is already used in the database domain and means 'transaction log'. It
* is possible to write after close was called, but that means for each write
* the file will be opened and closed again (which is slower).
*/
public class TraceSystem implements TraceWriter {
/**
* The parent trace level should be used.
*/
public static final int PARENT = -1;
/**
* This trace level means nothing should be written.
*/
public static final int OFF = 0;
/**
* This trace level means only errors should be written.
*/
public static final int ERROR = 1;
/**
* This trace level means errors and informational messages should be
* written.
*/
public static final int INFO = 2;
/**
* This trace level means all type of messages should be written.
*/
public static final int DEBUG = 3;
/**
* This trace level means all type of messages should be written, but
* instead of using the trace file the messages should be written to SLF4J.
*/
public static final int ADAPTER = 4;
/**
* The default level for system out trace messages.
*/
public static final int DEFAULT_TRACE_LEVEL_SYSTEM_OUT = OFF;
/**
* The default level for file trace messages.
*/
public static final int DEFAULT_TRACE_LEVEL_FILE = ERROR;
/**
* The default maximum trace file size. It is currently 64 MB. Additionally,
* there could be a .old file of the same size.
*/
private static final int DEFAULT_MAX_FILE_SIZE = 64 * 1024 * 1024;
private static final int CHECK_SIZE_EACH_WRITES = 4096;
private int levelSystemOut = DEFAULT_TRACE_LEVEL_SYSTEM_OUT;
private int levelFile = DEFAULT_TRACE_LEVEL_FILE;
private int levelMax;
private int maxFileSize = DEFAULT_MAX_FILE_SIZE;
private String fileName;
private final AtomicReferenceArray traces =
new AtomicReferenceArray<>(Trace.MODULE_NAMES.length);
private SimpleDateFormat dateFormat;
private Writer fileWriter;
private PrintWriter printWriter;
/**
* Starts at -1 so that we check the file size immediately upon open. This
* Can be important if we open and close the trace file without managing to
* have written CHECK_SIZE_EACH_WRITES bytes each time.
*/
private int checkSize = -1;
private boolean closed;
private boolean writingErrorLogged;
private TraceWriter writer = this;
private PrintStream sysOut = System.out;
/**
* Create a new trace system object.
*
* @param fileName the file name
*/
public TraceSystem(String fileName) {
this.fileName = fileName;
updateLevel();
}
private void updateLevel() {
levelMax = Math.max(levelSystemOut, levelFile);
}
/**
* Set the print stream to use instead of System.out.
*
* @param out the new print stream
*/
public void setSysOut(PrintStream out) {
this.sysOut = out;
}
/**
* Get or create a trace object for this module id. Trace modules with id
* are cached.
*
* @param moduleId module id
* @return the trace object
*/
public Trace getTrace(int moduleId) {
Trace t = traces.get(moduleId);
if (t == null) {
t = new Trace(writer, moduleId);
if (!traces.compareAndSet(moduleId, null, t)) {
t = traces.get(moduleId);
}
}
return t;
}
/**
* Create a trace object for this module. Trace modules with names are not
* cached.
*
* @param module the module name
* @return the trace object
*/
public Trace getTrace(String module) {
return new Trace(writer, module);
}
@Override
public boolean isEnabled(int level) {
if (levelMax == ADAPTER) {
return writer.isEnabled(level);
}
return level <= this.levelMax;
}
/**
* Set the trace file name.
*
* @param name the file name
*/
public void setFileName(String name) {
this.fileName = name;
}
/**
* Set the maximum trace file size in bytes.
*
* @param max the maximum size
*/
public void setMaxFileSize(int max) {
this.maxFileSize = max;
}
/**
* Set the trace level to use for System.out
*
* @param level the new level
*/
public void setLevelSystemOut(int level) {
levelSystemOut = level;
updateLevel();
}
/**
* Set the file trace level.
*
* @param level the new level
*/
public void setLevelFile(int level) {
if (level == ADAPTER) {
String adapterClass = "org.h2.message.TraceWriterAdapter";
try {
writer = (TraceWriter) Class.forName(adapterClass).getDeclaredConstructor().newInstance();
} catch (Throwable e) {
e = DbException.get(ErrorCode.CLASS_NOT_FOUND_1, e, adapterClass);
write(ERROR, Trace.DATABASE, adapterClass, e);
return;
}
String name = fileName;
if (name != null) {
if (name.endsWith(Constants.SUFFIX_TRACE_FILE)) {
name = name.substring(0, name.length() - Constants.SUFFIX_TRACE_FILE.length());
}
int idx = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
if (idx >= 0) {
name = name.substring(idx + 1);
}
writer.setName(name);
}
}
levelFile = level;
updateLevel();
}
public int getLevelFile() {
return levelFile;
}
private synchronized String format(String module, String s) {
if (dateFormat == null) {
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ");
}
return dateFormat.format(System.currentTimeMillis()) + module + ": " + s;
}
@Override
public void write(int level, int moduleId, String s, Throwable t) {
write(level, Trace.MODULE_NAMES[moduleId], s, t);
}
@Override
public void write(int level, String module, String s, Throwable t) {
if (level <= levelSystemOut || level > this.levelMax) {
// level <= levelSystemOut: the system out level is set higher
// level > this.level: the level for this module is set higher
sysOut.println(format(module, s));
if (t != null && levelSystemOut == DEBUG) {
t.printStackTrace(sysOut);
}
}
if (fileName != null) {
if (level <= levelFile) {
writeFile(format(module, s), t);
}
}
}
private synchronized void writeFile(String s, Throwable t) {
try {
checkSize = (checkSize + 1) % CHECK_SIZE_EACH_WRITES;
if (checkSize == 0) {
closeWriter();
if (maxFileSize > 0 && FileUtils.size(fileName) > maxFileSize) {
String old = fileName + ".old";
FileUtils.delete(old);
FileUtils.move(fileName, old);
}
}
if (!openWriter()) {
return;
}
printWriter.println(s);
if (t != null) {
if (levelFile == ERROR && t instanceof JdbcException) {
JdbcException se = (JdbcException) t;
int code = se.getErrorCode();
if (ErrorCode.isCommon(code)) {
printWriter.println(t);
} else {
t.printStackTrace(printWriter);
}
} else {
t.printStackTrace(printWriter);
}
}
printWriter.flush();
if (closed) {
closeWriter();
}
} catch (Exception e) {
logWritingError(e);
}
}
private void logWritingError(Exception e) {
if (writingErrorLogged) {
return;
}
writingErrorLogged = true;
Exception se = DbException.get(
ErrorCode.TRACE_FILE_ERROR_2, e, fileName, e.toString());
// print this error only once
fileName = null;
sysOut.println(se);
se.printStackTrace();
}
private boolean openWriter() {
if (printWriter == null) {
try {
FileUtils.createDirectories(FileUtils.getParent(fileName));
if (FileUtils.exists(fileName) && !FileUtils.canWrite(fileName)) {
// read only database: don't log error if the trace file
// can't be opened
return false;
}
fileWriter = IOUtils.getBufferedWriter(
FileUtils.newOutputStream(fileName, true));
printWriter = new PrintWriter(fileWriter, true);
} catch (Exception e) {
logWritingError(e);
return false;
}
}
return true;
}
private synchronized void closeWriter() {
if (printWriter != null) {
printWriter.flush();
printWriter.close();
printWriter = null;
}
if (fileWriter != null) {
try {
fileWriter.close();
} catch (IOException e) {
// ignore
}
fileWriter = null;
}
}
/**
* Close the writers, and the files if required. It is still possible to
* write after closing, however after each write the file is closed again
* (slowing down tracing).
*/
public void close() {
closeWriter();
closed = true;
}
@Override
public void setName(String name) {
// nothing to do (the file name is already set)
}
}