[PLACEBO] Raw sketch of the upcoming logging framework, doing nothing.

This commit is contained in:
asofold 2014-11-17 23:54:33 +01:00
parent f49c64e89d
commit 2f4d689722
19 changed files with 1534 additions and 0 deletions

View File

@ -0,0 +1,25 @@
package fr.neatmonster.nocheatplus.logging;
/**
* Restrictions for registration:
* <li>Unique instances.</li>
* <li>Unique names.</li>
* <li>Custom registrations can not start with the default prefix (see AbstractLogManager).</li>
*
* @author dev1mc
*
*/
public class LoggerID {
public final String name;
public LoggerID(String name) {
this.name = name;
}
@Override
public String toString() {
return "<LoggerID " + name + ">";
}
}

View File

@ -0,0 +1,25 @@
package fr.neatmonster.nocheatplus.logging;
/**
* Restrictions for registration:
* <li>Unique instances.</li>
* <li>Unique names.</li>
* <li>Custom registrations can not start with the default prefix (see AbstractLogManager).</li>
*
* @author dev1mc
*
*/
public class StreamID {
public final String name;
public StreamID(String name) {
this.name = name;
}
@Override
public String toString() {
return "<StreamID " + name + ">";
}
}

View File

@ -0,0 +1,30 @@
package fr.neatmonster.nocheatplus.logging;
/**
* Default StreamIDs.
* @author dev1mc
*
*/
public class Streams {
// Maybe temporary: StremID instances for default usage (custom registrations can not start with "default.").
/**
* Default prefix, should not be used with custom registration (LoggerID, StreamID).
*/
public static final String defaultPrefix = "default.";
// More or less raw default streams.
/** For initialization and shutdown, not more than primary thread only is guaranteed here. Always available (fall-back). */
public static final StreamID INIT = new StreamID(defaultPrefix + "init");
/** Might not allow asynchronous access. */
public static final StreamID SERVER_LOGGER = new StreamID(defaultPrefix + "logger.server");
/** Might not allow asynchronous access. */
public static final StreamID PLUGIN_LOGGER = new StreamID(defaultPrefix + "logger.plugin");
public static final StreamID NOTIFY_INGAME = new StreamID(defaultPrefix + "chat.notify");
public static final StreamID DEFAULT_FILE = new StreamID(defaultPrefix + "file");
public static final StreamID TRACE_FILE = new StreamID(defaultPrefix + "file.trace");
// TODO: More abstract streams (init, trace, violations?), have loggers/files be LoggerID ?
}

View File

@ -0,0 +1,487 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.io.File;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import fr.neatmonster.nocheatplus.logging.LoggerID;
import fr.neatmonster.nocheatplus.logging.StreamID;
import fr.neatmonster.nocheatplus.utilities.StringUtil;
/**
* Central access point for logging. Abstract class providing basic registration functionality.
* @author dev1mc
*
*/
public abstract class AbstractLogManager {
// TODO: Visibility of methods.
// TODO: Add option for per stream prefixes.
// TODO: Concept for adding in the time at the point of call/scheduling.
// TODO: Add a void stream (no loggers, allow logging unknown ids to void).
// TODO: Allow route log with non existing stream to init or void (flag).
// TODO: Attaching loggers with different options to different streams is necessary (e.g. normal -> async logging, but any thread with init).
// TODO: Re-register with other options: Add methods for LoggerID + StreamID + options.
// TODO: Hierarchical LogNode relations, to ensure other log nodes with the same logger are removed too [necessary to allow removing individual loggers].
// TODO: Temporary streams, e.g. for players, unregistering with command and/or logout.
// TODO: Mechanics of removing temporary streams: 1. remove
// TODO: Consider generalizing the (internal) implementation right away (sub registry by content class).
// TODO: Consider adding a global cache (good for re-mapping, contra: reload is not intended to happen on a regular basis).
private final LogNodeDispatcher dispatcher;
private final String defaultPrefix;
/**
* Fast streamID access map (runtime). Copy on write with registryLock.
*/
private Map<StreamID, ContentStream<String>> idStreamMap = new IdentityHashMap<StreamID, ContentStream<String>>();
/**
* Map name to Stream. Copy on write with registryLock.
*/
private Map<String, ContentStream<String>> nameStreamMap = new HashMap<String, ContentStream<String>>();
/**
* Lower-case name to StreamID.
*/
private Map<String, StreamID> nameStreamIDMap = new HashMap<String, StreamID>();
/**
* LogNode registry by LoggerID. Copy on write with registryLock.
*/
private Map<LoggerID, LogNode<String>> idNodeMap = new IdentityHashMap<LoggerID, LogNode<String>>();
/**
* LogNode registry by lower-case name. Copy on write with registryLock.
*/
private Map<String, LogNode<String>> nameNodeMap = new HashMap<String, LogNode<String>>();
/**
* Lower-case name to LoggerID.
*/
private Map<String, LoggerID> nameLoggerIDMap = new HashMap<String, LoggerID>();
/** Registry changes have to be done under this lock (copy on write) */
protected final Object registryCOWLock = new Object();
// TODO: Future: Only an init string stream or (later) always "the init stream" for all content types.
private final StreamID initStreamID;
private final StreamID voidStreamID = new StreamID("void");
/**
* Fall-back StreamID, for the case of logging to a non-existing StreamID.
* By default set to void, but can be altered. Set to null to have calls
* fail.
*/
private StreamID fallBackStreamID = voidStreamID;
/**
* Wrapping logging to the init stream.
*/
protected final ContentLogger<String> initLogger = new ContentLogger<String>() {
@Override
public void log(final Level level, final String content) {
AbstractLogManager.this.log(getInitStreamID(), level, content);
}
};
/**
*
* @param dispatcher
* @param defaultPrefix
* @param initStreamID This id is stored, the stream is created, but no loggers will be attached to it within this constructor.
*/
public AbstractLogManager(LogNodeDispatcher dispatcher, String defaultPrefix, StreamID initStreamID) {
this.dispatcher = dispatcher;
this.defaultPrefix = defaultPrefix;
this.initStreamID = initStreamID;
createInitStream();
registerInitLogger();
dispatcher.setInitLogger(initLogger);
}
/**
* Create stream if it does not exist.
*/
protected void createInitStream() {
synchronized (registryCOWLock) {
if (!hasStream(initStreamID)) {
createStringStream(initStreamID);
}
}
}
/**
* Create the minimal init logger(s). Synchronize over registryCOWLock. It's preferable not to duplicate loggers. Prefer LoggerID("init...").
*/
protected abstract void registerInitLogger();
protected LogNodeDispatcher getLogNodeDispatcher() {
return dispatcher;
}
/**
* Don't use this prefix for custom registrations with StreamID and LoggerID.
* @return
*/
public String getDefaultPrefix() {
return defaultPrefix;
}
/**
* This should be a fail-safe direct String-logger, that has the highest probability of working
* within the default context and rather skips messages instead of failing or scheduling tasks,
* typically the main application primary thread.
*
* @return
*/
public StreamID getInitStreamID() {
return initStreamID;
}
/**
* A stream that skips all messages. It's not registered officially.
* @return
*/
public StreamID getVoidStreamID() {
return voidStreamID;
}
/**
* Case-insensitive lookup.
* @param name
* @return Returns the registered StreamID or null, if not present.
*/
public StreamID getStreamID(String name) {
return nameStreamIDMap.get(name.toLowerCase());
}
/**
* Case-insensitive lookup.
* @param name
* @return Returns the registered StreamID or null, if not present.
*/
public LoggerID getLoggerID(String name) {
return nameLoggerIDMap.get(name.toLowerCase());
}
public void debug(final StreamID streamID, final String message) {
log(streamID, Level.FINE, message); // TODO: Not sure what happens with FINE and provided Logger instances.
}
public void info(final StreamID streamID, final String message) {
log(streamID, Level.INFO, message);
}
public void warning(final StreamID streamID, final String message) {
log(streamID, Level.WARNING, message);
}
public void severe(final StreamID streamID, final String message) {
log(streamID, Level.SEVERE, message);
}
public void log(final StreamID streamID, final Level level, final String message) {
if (streamID != voidStreamID) {
final ContentStream<String> stream = idStreamMap.get(streamID);
if (stream != null) {
stream.log(level, message);
} else {
handleFallBack(streamID, level, message);
}
}
}
private void handleFallBack(final StreamID streamID, final Level level, final String message) {
if (fallBackStreamID != null && streamID != fallBackStreamID) {
log(fallBackStreamID, level, message);
} else {
throw new RuntimeException("Stream not registered: " + streamID);
}
}
public void debug(final StreamID streamID, final Throwable t) {
log(streamID, Level.FINE, t); // TODO: Not sure what happens with FINE and provided Logger instances.
}
public void info(final StreamID streamID, final Throwable t) {
log(streamID, Level.INFO, t);
}
public void warning(final StreamID streamID, final Throwable t) {
log(streamID, Level.WARNING, t);
}
public void severe(final StreamID streamID, final Throwable t) {
log(streamID, Level.SEVERE, t);
}
public void log(final StreamID streamID, final Level level, final Throwable t) {
// Not sure adding streams for Throwable would be better.
log(streamID, level, StringUtil.throwableToString(t));
}
/**
* A newly created id can be used here. For logging use existing ids always.
* @param streamID
* @return
*/
public boolean hasStream(final StreamID streamID) {
return this.idStreamMap.containsKey(streamID) || this.nameStreamMap.containsKey(streamID.name.toLowerCase());
}
public boolean hasStream(String name) {
return getStreamID(name) != null;
}
/**
* Call under lock.
* @param streamID
*/
private void testRegisterStream(final StreamID streamID) {
if (streamID == null) {
throw new NullPointerException("StreamID must not be null.");
}
else if (streamID.name == null) {
throw new NullPointerException("StreamID.name must not be null.");
}
else if (streamID.name.equalsIgnoreCase(voidStreamID.name)) {
throw new RuntimeException("Can not overrite void StreamID.");
}
else if (hasStream(streamID)) {
throw new IllegalArgumentException("Stream already registered: " + streamID.name.toLowerCase());
}
}
protected ContentStream<String> createStringStream(final StreamID streamID) {
ContentStream<String> stream;
synchronized (registryCOWLock) {
testRegisterStream(streamID);
Map<StreamID, ContentStream<String>> idStreamMap = new IdentityHashMap<StreamID, ContentStream<String>>(this.idStreamMap);
Map<String, ContentStream<String>> nameStreamMap = new HashMap<String, ContentStream<String>>(this.nameStreamMap);
Map<String, StreamID> nameStreamIDMap = new HashMap<String, StreamID>(this.nameStreamIDMap);
stream = new DefaultContentStream<String>(dispatcher);
idStreamMap.put(streamID, stream);
nameStreamMap.put(streamID.name.toLowerCase(), stream);
nameStreamIDMap.put(streamID.name.toLowerCase(), streamID);
this.idStreamMap = idStreamMap;
this.nameStreamMap = nameStreamMap;
this.nameStreamIDMap = nameStreamIDMap;
}
return stream;
}
/**
* A newly created id can be used here. For logging use existing ids always.
* @param loggerID
* @return
*/
public boolean hasLogger(final LoggerID loggerID) {
return this.idNodeMap.containsKey(loggerID) || this.nameNodeMap.containsKey(loggerID.name.toLowerCase());
}
public boolean hasLogger(String name) {
return getLoggerID(name) != null;
}
/**
* Call under lock.
* @param loggerID
*/
private void testRegisterLogger(final LoggerID loggerID) {
if (loggerID == null) {
throw new NullPointerException("LoggerID must not be null.");
}
else if (loggerID.name == null) {
throw new NullPointerException("LoggerID.name must not be null.");
}
else if (hasLogger(loggerID)) {
throw new IllegalArgumentException("Logger already registered: " + loggerID.name.toLowerCase());
}
}
/**
* Convenience method.
* @param logger
* @param options
* @return
*/
protected LoggerID registerStringLogger(final ContentLogger<String> logger, final LogOptions options) {
LoggerID loggerID = new LoggerID(options.name);
registerStringLogger(loggerID, logger, options);
return loggerID;
}
/**
* Convenience method.
* @param logger
* @param options
* @return
*/
protected LoggerID registerStringLogger(final Logger logger, final LogOptions options) {
LoggerID loggerID = new LoggerID(options.name);
registerStringLogger(loggerID, logger, options);
return loggerID;
}
/**
* Convenience method.
* @param logger
* @param options
* @return
*/
protected LoggerID registerStringLogger(final File file, final LogOptions options) {
LoggerID loggerID = new LoggerID(options.name);
registerStringLogger(loggerID, file, options);
return loggerID;
}
protected LogNode<String> registerStringLogger(final LoggerID loggerID, final ContentLogger<String> logger, final LogOptions options) {
LogNode<String> node;
synchronized (registryCOWLock) {
testRegisterLogger(loggerID);
Map<LoggerID, LogNode<String>> idNodeMap = new IdentityHashMap<LoggerID, LogNode<String>>(this.idNodeMap);
Map<String, LogNode<String>> nameNodeMap = new HashMap<String, LogNode<String>>(this.nameNodeMap);
Map<String, LoggerID> nameLoggerIDMap = new HashMap<String, LoggerID>(this.nameLoggerIDMap);
node = new LogNode<String>(loggerID, logger, options);
idNodeMap.put(loggerID, node);
nameNodeMap.put(loggerID.name.toLowerCase(), node);
nameLoggerIDMap.put(loggerID.name.toLowerCase(), loggerID);
this.idNodeMap = idNodeMap;
this.nameNodeMap = nameNodeMap;
this.nameLoggerIDMap = nameLoggerIDMap;
}
return node;
}
protected LogNode<String> registerStringLogger(LoggerID loggerID, Logger logger, LogOptions options) {
LogNode<String> node;
synchronized (registryCOWLock) {
LoggerAdapter adapter = new LoggerAdapter(logger); // Low cost.
// TODO: Store loggers too to prevent redundant registration.
node = registerStringLogger(loggerID, adapter, options);
}
return node;
}
protected LogNode<String> registerStringLogger(LoggerID loggerID, File file, LogOptions options) {
LogNode<String> node;
synchronized (registryCOWLock) {
testRegisterLogger(loggerID); // Redundant checking, because file loggers are expensive.
// TODO: Detect duplicate loggers (register same logger with given id and options).
FileLoggerAdapter adapter = new FileLoggerAdapter(file);
if (adapter.isInoperable()) {
adapter.detachLogger();
throw new RuntimeException("Failed to set up file logger for id '" + loggerID + "': " + file);
}
try {
node = registerStringLogger(loggerID, adapter, options);
} catch (Exception ex) {
// TODO: Exception is still bad.
adapter.detachLogger();
throw new RuntimeException(ex);
}
}
return node;
}
/**
* Attach a logger to a stream. Redundant attaching will mean no changes.
* @param loggerID Must exist.
* @param streamID Must exist.
*/
protected void attachStringLogger(final LoggerID loggerID, final StreamID streamID) {
// TODO: More light-weight locking concept (thinking of dynamically changing per player streams)?
synchronized (registryCOWLock) {
if (!hasLogger(loggerID)) {
throw new RuntimeException("Logger is not registered: " + loggerID);
}
if (!hasStream(streamID)) {
// Note: This should also ensure the voidStreamID can't be used, because that one can't be registered.
throw new RuntimeException("Stream is not registered: " + streamID);
}
final LogNode<String> node = idNodeMap.get(loggerID);
if (streamID == initStreamID) {
// TODO: Not sure about restrictions here. Could allow attaching other streams temporarily if other stuff is wanted.
switch(node.options.callContext) {
case PRIMARY_THREAD_ONLY:
case ANY_THREAD_DIRECT:
break;
default:
throw new RuntimeException("Unsupported call context for init stream " + streamID + ": " + node.options.callContext);
}
}
idStreamMap.get(streamID).addNode(node);
}
}
// TODO: Methods to replace options for loggers (+ loggers themselves)
// TODO: Later: attach streams to streams ? [few loggers: attach loggers rather]
// TODO: logger/stream: allow id lookup logger, file, etc. ?
/**
* Remove all loggers and streams including init, resulting in roughly the
* same state as is after calling the AbstractLogger constructor. Call from
* the primary thread (policy pending). If fallBackStreamID is set, it will be replaced by the init stream (if available) or by the void stream.
*
* @param msWaitFlush
*/
protected void clear(final long msWaitFlush, final boolean recreateInitLogger) {
// TODO: enum (remove_all, recreate init, remap init, remap all to init)
synchronized (registryCOWLock) {
// Remove loggers from string streams.
for (ContentStream<String> stream : idStreamMap.values()) {
stream.clear();
}
// Flush queues.
dispatcher.flush(msWaitFlush);
// Close/flush/shutdown string loggers, where possible, remove all from registry.
for (final LogNode<String> node : idNodeMap.values()) {
if (node.logger instanceof FileLoggerAdapter) {
FileLoggerAdapter logger = (FileLoggerAdapter) node.logger;
logger.flush();
logger.detachLogger();
}
}
idNodeMap = new IdentityHashMap<LoggerID, LogNode<String>>();
nameNodeMap = new HashMap<String, LogNode<String>>();
nameLoggerIDMap = new HashMap<String, LoggerID>();
// Remove string streams.
idStreamMap = new IdentityHashMap<StreamID, ContentStream<String>>();
nameStreamMap = new HashMap<String, ContentStream<String>>();
nameStreamIDMap = new HashMap<String, StreamID>();
if (recreateInitLogger) {
createInitStream();
registerInitLogger();
if (fallBackStreamID != null && fallBackStreamID != voidStreamID) {
fallBackStreamID = initStreamID;
}
}
else if (fallBackStreamID != null) {
fallBackStreamID = voidStreamID;
}
}
}
// /**
// * Remove all registered streams and loggers, recreates init logger (and stream).
// */
// public void clear(final long msWaitFlush) {
// clear(msWaitFlush, true);
// }
/**
* Rather a graceful shutdown, including waiting for the asynchronous task, if necessary. Clear the registry. Also removes the init logger [subject to change].
* Call from the primary thread (policy pending).
*/
public void shutdown() {
clear(500, false); // TODO: Policy / making sense.
}
}

View File

@ -0,0 +1,232 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.util.List;
import java.util.logging.Level;
import fr.neatmonster.nocheatplus.utilities.ds.corw.QueueRORA;
/**
* Basic functionality, with int task ids, assuming a primary thread exists.
* @author dev1mc
*
*/
public abstract class AbstractLogNodeDispatcher implements LogNodeDispatcher { // TODO: Name
// TODO: Queues might need a drop policy with thresholds.
// TODO: Allow multiple tasks for logging, e.g. per file, also thinking of SQL logging. Could pool + round-robin.
/**
* This queue has to be processed in a task within the primary thread with calling runLogsPrimary.
*/
protected final QueueRORA<LogRecord<?>> queuePrimary = new QueueRORA<LogRecord<?>>();
protected final QueueRORA<LogRecord<?>> queueAsynchronous = new QueueRORA<LogRecord<?>>();
/** Once a queue reaches this size, it will be reduced (loss of content). */
protected int maxQueueSize = 5000;
/**
* Task id, -1 means the asynchronous task is not running. Synchronize over
* queueAsynchronous. Must be maintained.
*/
protected int taskAsynchronousID = -1;
/**
* Optional implementation for an asynchronous task, using the
* taskAsynchronousID, synchronized over queueAsynchronous.
*/
protected final Runnable taskAsynchronous = new Runnable() {
@Override
public void run() {
// TODO: A more sophisticated System to allow "wake up on burst"?
int i = 0;
while (i < 3) {
if (runLogsAsynchronous()) {
i = 0;
if (taskAsynchronousID == -1) {
// Shutdown, hard return;
return;
}
Thread.yield();
} else {
i ++;
try {
Thread.sleep(25);
} catch (InterruptedException e) {
synchronized (queueAsynchronous) {
// Ensure re-scheduling can happen.
taskAsynchronousID = -1;
}
// TODO: throw?
return;
}
}
synchronized (queueAsynchronous) {
if (queueAsynchronous.isEmpty()) {
if (i >= 3) {
// Ensure re-scheduling can happen.
taskAsynchronousID = -1;
}
} else {
i = 0;
}
}
}
}
};
/**
* Optional init logger to log errors. Should log to the init stream, no queuing.
*/
protected ContentLogger<String> initLogger = null;
public <C> void dispatch(LogNode<C> node, Level level, C content) {
// TODO: Try/catch ?
if (isWithinContext(node)) {
node.logger.log(level, content);
} else {
scheduleLog(node, level, content);
}
}
protected boolean runLogsPrimary() {
return runLogs(queuePrimary);
}
protected boolean runLogsAsynchronous() {
return runLogs(queueAsynchronous);
}
private boolean runLogs(final QueueRORA<LogRecord<?>> queue) {
// TODO: Consider allowYield + msYield, calling yield after 5 ms if async.
final List<LogRecord<?>> records = queue.removeAll();
if (records.isEmpty()) {
return false;
}
for (final LogRecord<?> record : records) {
record.run();
}
return true;
}
@Override
public void flush(long ms) {
if (!isPrimaryThread()) {
// TODO: Could also switch policy to emptying the primary-thread queue if not called from within the primary thread.
throw new IllegalStateException("Must only be called from within the primary thread.");
}
// TODO: Note that all streams should be emptied here, except the fallback logger.
// Cancel task.
synchronized (queueAsynchronous) {
if (taskAsynchronousID != -1) {
// TODO: Allow queues to set to "no more input" ?
cancelTask(taskAsynchronousID);
taskAsynchronousID = -1;
} else {
// No need to wait.
ms = 0L;
}
}
if (ms > 0) {
try {
// TODO: Replace by a better concept.
Thread.sleep(ms);
} catch (InterruptedException e) {
// Ignore.
}
}
// Log the rest (from here logging should be done via the appropriate direct-only stream).
runLogsPrimary();
runLogsAsynchronous();
}
protected <C> boolean isWithinContext(LogNode<C> node) {
switch (node.options.callContext) {
case PRIMARY_THREAD_DIRECT:
case PRIMARY_THREAD_ONLY:
return isPrimaryThread();
case ANY_THREAD_DIRECT:
return true;
case ASYNCHRONOUS_ONLY:
case ASYNCHRONOUS_DIRECT:
return !isPrimaryThread();
case PRIMARY_THREAD_TASK:
case ASYNCHRONOUS_TASK:
// TODO: Each: Consider detecting if within that task (should rather be in case of re-scheduling?).
return false; // Always schedule (!).
default:
return false; // Force scheduling.
}
}
protected <C> void scheduleLog(LogNode<C> node, Level level, C content) {
final LogRecord<C> record = new LogRecord<C>(node, level, content); // TODO: parameters.
switch (node.options.callContext) {
case ASYNCHRONOUS_TASK:
case ASYNCHRONOUS_DIRECT:
if (queueAsynchronous.add(record) > maxQueueSize) {
reduceQueue(queueAsynchronous);
}
if (taskAsynchronousID == -1) { // Works, due to add being synchronized (not sure it's really better than full sync).
scheduleAsynchronous();
}
break;
case PRIMARY_THREAD_TASK:
case PRIMARY_THREAD_DIRECT:
queuePrimary.add(record);
// TODO: Consider re-scheduling policy ?
break;
case ASYNCHRONOUS_ONLY:
case PRIMARY_THREAD_ONLY:
// SKIP LOGGING.
break;
default:
// (ANY_THREAD_DIRECT never gets scheduled.)
throw new IllegalArgumentException("Bad CallContext: " + node.options.callContext);
}
}
/**
* Hard reduce the queue (heavy locking!).
* @param queue
*/
private void reduceQueue(final QueueRORA<LogRecord<?>> queue) {
// TODO: Different dropping strategies (drop first, last, alternate).
final int dropped;
synchronized (queue) {
final int size = queue.size();
if (size < maxQueueSize) {
// Never mind :).
return;
}
logINIT(Level.WARNING, "Dropping log entries from the " + (queue == queueAsynchronous ? "asynchronous" : "primary thread") + " queue to reduce memory consumption...");
dropped = queue.reduce(maxQueueSize / 2);
}
logINIT(Level.WARNING, "Dropped " + dropped + " log entries from the " + (queue == queueAsynchronous ? "asynchronous" : "primary thread") + " queue.");
}
@Override
public void setMaxQueueSize(int maxQueueSize) {
this.maxQueueSize = maxQueueSize;
}
@Override
public void setInitLogger(ContentLogger<String> logger) {
this.initLogger = logger;
}
protected void logINIT(final Level level, final String message) {
if (initLogger != null) {
initLogger.log(level, message);
}
}
protected abstract boolean isPrimaryThread();
protected abstract void scheduleAsynchronous();
protected abstract void cancelTask(int taskId);
}

View File

@ -0,0 +1,16 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.util.logging.Level;
/**
* Minimal interface for logging content to.
* @author dev1mc
*
* @param <C>
*/
public interface ContentLogger <C> {
// TODO: Not sure about generics.
public void log(Level level, C content);
}

View File

@ -0,0 +1,30 @@
package fr.neatmonster.nocheatplus.logging.details;
/**
* Contracts:
* <li>Intended usage: register=seldom, log=often</li>
* <li>Asynchronous access must be possible and fast, without locks (rather copy on write). </li>
* @author dev1mc
*
* @param <C>
*/
public interface ContentStream <C> extends ContentLogger<C> {
// TODO: Maybe also an abstract class.
// Maybe not: addFilter (filter away some stuff, e.g. by regex from config).
// TODO: Consider extra arguments for efficient registratioon with COWs.
public void addNode(LogNode<C> node);
// addAdapter(ContentAdapter<C, ?> adapter) ? ID etc., attach to another stream.
// Removal and look up methods.
/**
* Remove all attached loggers and other.
*/
public void clear();
}

View File

@ -0,0 +1,51 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.util.ArrayList;
import java.util.logging.Level;
/**
* Locking is done with the internal node list, log does not block (extra to whatever loggers would do).
* @author dev1mc
*
* @param <C>
*/
public class DefaultContentStream<C> implements ContentStream<C> {
private ArrayList<LogNode<C>> nodes = new ArrayList<LogNode<C>>();
private final LogNodeDispatcher dispatcher;
public DefaultContentStream(LogNodeDispatcher dispatcher) {
this.dispatcher = dispatcher;
}
@Override
public void log(final Level level, final C content) {
final ArrayList<LogNode<C>> nodes = this.nodes;
for (int i = 0; i < nodes.size(); i++) {
dispatcher.dispatch(nodes.get(i), level, content);
}
}
@Override
public void addNode(LogNode<C> node) {
// TODO: Consider throwing things at callers, in case of duplicate logger entries?
synchronized (nodes) {
if (this.nodes.contains(node)) {
// TODO: Consider throwing something.
return;
}
ArrayList<LogNode<C>> nodes = new ArrayList<LogNode<C>>(this.nodes);
nodes.add(node);
this.nodes = nodes;
}
}
@Override
public void clear() {
synchronized (nodes) {
nodes.clear();
}
}
}

View File

@ -0,0 +1,143 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import fr.neatmonster.nocheatplus.utilities.StringUtil;
/**
* Wrap a file logger, adding convenience setup methods.
* @author dev1mc
*
*/
public class FileLogger {
protected static class FileLogFormatter extends Formatter {
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
// TODO: Consider storing a custom line break (needs adding to StringUtil.throwableToString).
@Override
public String format(final LogRecord record) {
final Throwable throwable = record.getThrown();
final String message = record.getMessage();
int roughLength = 64 + message.length();
final String throwableMessage;
if (throwable != null) {
throwableMessage = StringUtil.throwableToString(throwable);
roughLength += throwableMessage.length();
} else {
throwableMessage = null;
}
final StringBuilder builder = new StringBuilder(roughLength);
builder.append(dateFormat.format(record.getMillis()));
builder.append(" [");
builder.append(record.getLevel().getLocalizedName().toUpperCase());
builder.append("] ");
builder.append(message);
builder.append('\n');
if (throwableMessage != null) {
builder.append(throwableMessage);
}
return builder.toString();
}
}
public final Logger logger;
protected FileHandler fileHandler = null;
protected boolean inoperable = false;
/**
* Initialize with an anonymous logger and the given log-file path.
*
* @param file Path to log file or an existing directory.
*/
public FileLogger(File file) {
// TODO: Should re-add a file-name prefix (allow null).
// TODO: File encoding + line endings.
// TODO: Add options to switch file with file size, rolling files, etc.
// [could also keep track of rough size of written data, to switch file "on the fly", problem: which log? -> replace logger copy on write :p].
logger = Logger.getAnonymousLogger();
detachLogger();
final File container;
if (file.isDirectory()) {
container = file;
// Find a file name.
SimpleDateFormat dateFormat = new SimpleDateFormat("yy-MM-dd");
String prefix = dateFormat.format(System.currentTimeMillis());
int n = 1;
while (true) {
file = new File(container, prefix + "-" + n + ".log");
if (!file.exists()) {
break;
}
n ++;
}
} else {
container = file.getParentFile();
}
// Ensure the container exists.
if (!container.exists()) {
container.mkdirs();
}
// Create file and initialize logger.
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
file = null;
}
}
if (file != null) {
try {
initLogger(file);
logger.log(Level.INFO, "Logger started.");
inoperable = false;
} catch (SecurityException e) {
} catch (IOException e) {
}
}
}
protected void initLogger(File file) throws SecurityException, IOException {
logger.setUseParentHandlers(false);
logger.setLevel(Level.ALL);
fileHandler = new FileHandler(file.getCanonicalPath(), true);
fileHandler.setLevel(Level.ALL);
fileHandler.setFormatter(new FileLogFormatter());
logger.addHandler(fileHandler);
}
public void flush() {
if (fileHandler != null) {
fileHandler.flush();
}
}
public void detachLogger() {
if (fileHandler != null) {
fileHandler.close();
fileHandler = null;
}
for (Handler handler : logger.getHandlers()) {
logger.removeHandler(handler);
}
}
public boolean isInoperable () {
return inoperable;
}
}

View File

@ -0,0 +1,26 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.io.File;
import java.util.logging.Level;
public class FileLoggerAdapter extends FileLogger implements ContentLogger<String> {
// TODO: Store path/file either here or on FileLogger.
// TODO: Do store the string path (directory / direct file path), to reference correctly.
/**
* See FileLogger(File).
* @param file Path to log file or existing directory.
*/
public FileLoggerAdapter(File file) {
super(file);
}
@Override
public void log(Level level, String content) {
// TODO: Check loggerisInoperable() ?
logger.log(level, content);
}
}

View File

@ -0,0 +1,18 @@
package fr.neatmonster.nocheatplus.logging.details;
import fr.neatmonster.nocheatplus.logging.LoggerID;
public class LogNode<C> {
public final LoggerID loggerID;
public final ContentLogger<C> logger;
public final LogOptions options;
public LogNode(LoggerID loggerID, ContentLogger<C> logger, LogOptions options) {
this.loggerID = loggerID;
this.logger = logger;
this.options = new LogOptions(options);
}
}

View File

@ -0,0 +1,33 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.util.logging.Level;
/**
* Handle logging on LogNode instances (log directly, skip, schedule to log within tasks).
* @author dev1mc
*
*/
public interface LogNodeDispatcher { // TODO: Name.
public <C> void dispatch(LogNode<C> node, Level level, C content);
/**
* Cancel asynchronous tasks and remove all logs based on policy (log all or clear), default is to log
* all. Should be called from the primary thread, if it exists.
* @param ms Milliseconds to wait in case there is something being processed by asynchronous tasks.
*/
public abstract void flush(long ms);
/**
* Allow to add a logger, for logging errors to the init stream. Must not use the queues. Can be null (no logging).
* @param logger
*/
void setInitLogger(ContentLogger<String> logger);
/**
* Set maximum queue size. After reaching that size queues will be reduced by dropping elements (asynchronous and primaray thread individually).
* @param maxQueueSize
*/
void setMaxQueueSize(int maxQueueSize);
}

View File

@ -0,0 +1,51 @@
package fr.neatmonster.nocheatplus.logging.details;
public class LogOptions {
/**
* Describe the context in which the log method may be used.<br>
* Policies:
* <li>..._DIRECT: Will log directly if within context, otherwise schedule to a task.</li>
* <li>..._TASK: Always schedule to a task.</li>
*
* @author dev1mc
*
*/
public static enum CallContext {
// TODO: Consider making this a general enum for call contexts (API wrappers, player data etc. too?).
// Primary as long as it exists...
/** Only execute directly in the primary thread, for other threads use a task to execute within the primary thread. */
PRIMARY_THREAD_DIRECT,
/** Always schedule to execute within the primary thread. */
PRIMARY_THREAD_TASK,
/** Only log if within the primary thread. */
PRIMARY_THREAD_ONLY,
/** Execute directly independently of thread. */
ANY_THREAD_DIRECT,
/** Ensure asynchronous logging, but avoid scheduling. */
ASYNCHRONOUS_DIRECT,
/** Always schedule to execute within a (more or less) dedicated asynchronous task. */
ASYNCHRONOUS_TASK,
/** Ensure it's logged asynchronously. */
ASYNCHRONOUS_ONLY,
// CUSTOM_THREAD_DIRECT|TASK // Needs a variable (Thread, methods to sync into a specific thread would have to be registered in LogManager).
;
// TODO: Can distinguish further: Call from where, log from where directly, schedule from where (allow to skip certain contexts).
}
public final String name; // TODO: Name necessary ?
public final CallContext callContext;
public LogOptions(LogOptions options) {
this(options.name, options.callContext);
}
public LogOptions(String name, CallContext callContext) {
this.name = name;
this.callContext = callContext;
// TODO: shutdown policy (clear, log), rather with per-world threads.
}
}

View File

@ -0,0 +1,28 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.util.logging.Level;
/**
* A log message to be executed within a task from a queue, hiding the generics.
* @author dev1mc
*
*/
public class LogRecord<C> implements Runnable {
private final LogNode<C> node;
private final Level level;
private final C content;
public LogRecord(LogNode<C> node, Level level, C content) {
this.node = node;
this.level = level;
this.content = content;
}
@Override
public void run() {
// TODO: Checks / try-catch where?
node.logger.log(level, content);
}
}

View File

@ -0,0 +1,19 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LoggerAdapter implements ContentLogger<String> {
protected final Logger logger;
public LoggerAdapter(Logger logger) {
this.logger = logger;
}
@Override
public void log(Level level, String content) {
logger.log(level, content);
}
}

View File

@ -0,0 +1,96 @@
package fr.neatmonster.nocheatplus.utilities.ds.corw;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* A replace-on-read-all queue-thing, exchanging the internal list under lock by
* a new empty one, for a small locking time, so it is not really a typical
* copy-on-read. All methods use locking, the QueueRORA instance is used for
* locking.
*
* @author dev1mc
*
*/
public class QueueRORA<E> {
private LinkedList<E> elements = new LinkedList<E>();
/**
* Add to list (synchronized).
* @param element
* @return Size of queue after adding.
*/
public int add(final E element) {
final int size;
synchronized (this) {
elements.add(element);
size = elements.size();
}
return size;
}
/**
*
* @return An ordinary (linked) List containing all elements.
*/
public List<E> removeAll() {
final List<E> result;
synchronized (this) {
result = elements;
elements = new LinkedList<E>();
}
return result;
}
/**
* Remove oldest entries until maxSize is reached.
* @param maxSize
* @return
*/
public int reduce(final int maxSize) {
int dropped = 0;
synchronized (this) {
final int size = elements.size();
if (size <= maxSize) {
return dropped;
}
while (dropped < size - maxSize) {
elements.removeFirst();
dropped ++;
}
}
return dropped;
}
public void clear() {
removeAll();
}
/**
*
* @return
*/
public boolean isEmpty() {
final boolean isEmpty;
synchronized (this) {
isEmpty = elements.isEmpty();
}
return isEmpty;
}
/**
*
* @return
*/
public int size() {
final int size;
synchronized (this) {
size = elements.size();
}
return size;
}
}

View File

@ -36,6 +36,7 @@ public class DefaultConfig extends ConfigFile {
// not set(ConfPaths.CONFIGVERSION_SAVED, -1);
set(ConfPaths.LOGGING_ACTIVE, true);
set(ConfPaths.LOGGING_DEBUG, false);
set(ConfPaths.LOGGING_MAXQUEUESIZE, 5000);
set(ConfPaths.LOGGING_BACKEND_CONSOLE_ACTIVE, true);
set(ConfPaths.LOGGING_BACKEND_CONSOLE_PREFIX, "[NoCheatPlus] ");
set(ConfPaths.LOGGING_BACKEND_FILE_ACTIVE, true);

View File

@ -0,0 +1,154 @@
package fr.neatmonster.nocheatplus.logging;
import java.io.File;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.config.ConfPaths;
import fr.neatmonster.nocheatplus.config.ConfigFile;
import fr.neatmonster.nocheatplus.config.ConfigManager;
import fr.neatmonster.nocheatplus.logging.details.AbstractLogManager;
import fr.neatmonster.nocheatplus.logging.details.BukkitLogNodeDispatcher;
import fr.neatmonster.nocheatplus.logging.details.ContentLogger;
import fr.neatmonster.nocheatplus.logging.details.FileLoggerAdapter;
import fr.neatmonster.nocheatplus.logging.details.LogOptions;
import fr.neatmonster.nocheatplus.logging.details.LogOptions.CallContext;
/**
* Central access point for logging. The default loggers use the stream names,
* at least as prefixes).<br>
* Note that logging to the init/plugin/server with debug/fine or finer, might
* result in the server loggers suppressing those. As long as default file is
* activated, logging to init will log all levels to the file.
*
* @author dev1mc
*
*/
public class LogManager extends AbstractLogManager {
// TODO: Make LogManager an interface <- AbstractLogManager <- BukkitLogManager (hide some / instanceof).
// TODO: ingame logging [ingame needs api to keep track of players who receive notifications.].
// TODO: Later: Custom loggers (file, other), per-player-streams (debug per player), custom ingame loggers (one or more players).
protected final Plugin plugin;
/**
* This will create all default loggers as well.
* @param plugin
*/
public LogManager(Plugin plugin) {
super(new BukkitLogNodeDispatcher(plugin), Streams.defaultPrefix, Streams.INIT);
this.plugin = plugin;
ConfigFile config = ConfigManager.getConfigFile();
createDefaultLoggers(config);
getLogNodeDispatcher().setMaxQueueSize(config.getInt(ConfPaths.LOGGING_MAXQUEUESIZE));
}
@Override
protected void registerInitLogger() {
synchronized (registryCOWLock) {
if (!hasStream(Streams.INIT)) {
createInitStream();
}
else if (hasLogger("init")) {
// Shallow check.
return;
}
// Attach a new restrictive init logger.
// TODO: If thread-safe use ANY_THREAD_DIRECT (should then allow to interrupt).
LoggerID initLoggerID = registerStringLogger(new ContentLogger<String>() {
@Override
public void log(Level level, String content) {
try {
Bukkit.getLogger().log(level, content);
} catch (Throwable t) {}
}
}, new LogOptions(Streams.INIT.name, CallContext.PRIMARY_THREAD_ONLY));
attachStringLogger(initLoggerID, Streams.INIT);
}
}
/**
* Create default loggers and streams.
*/
protected void createDefaultLoggers(ConfigFile config) {
for (StreamID streamID : new StreamID[] {
Streams.SERVER_LOGGER, Streams.PLUGIN_LOGGER,
Streams.NOTIFY_INGAME,
Streams.DEFAULT_FILE, Streams.TRACE_FILE,
}) {
createStringStream(streamID);
}
// TODO: Consult configuration and/or detect what options can or want to be used here.
CallContext bukkitLoggerContext = CallContext.PRIMARY_THREAD_TASK; // TODO: Config + individually.
LoggerID tempID;
// Server logger.
tempID = registerStringLogger(new ContentLogger<String>() {
@Override
public void log(Level level, String content) {
Bukkit.getLogger().log(level, content);
}
}, new LogOptions(Streams.SERVER_LOGGER.name, bukkitLoggerContext));
// Plugin logger.
tempID = registerStringLogger(plugin.getLogger(), new LogOptions(Streams.PLUGIN_LOGGER.name, bukkitLoggerContext));
attachStringLogger(tempID, Streams.PLUGIN_LOGGER);
// Ingame logger (assume not thread-safe at first).
tempID = registerStringLogger(new ContentLogger<String>() {
@Override
public void log(Level level, String content) {
// Ignore level for now.
NCPAPIProvider.getNoCheatPlusAPI().sendAdminNotifyMessage(content);
}
}, new LogOptions(Streams.NOTIFY_INGAME.name, CallContext.PRIMARY_THREAD_DIRECT));
attachStringLogger(tempID, Streams.NOTIFY_INGAME);
File file;
// TODO: Allow absolute paths ?
// Default file logger.
file = new File(plugin.getDataFolder(), config.getString(ConfPaths.LOGGING_BACKEND_FILE_FILENAME));
ContentLogger<String> defaultFileLogger = new FileLoggerAdapter(file);
tempID = registerStringLogger(defaultFileLogger, new LogOptions(Streams.DEFAULT_FILE.name, CallContext.ASYNCHRONOUS_DIRECT));
attachStringLogger(tempID, Streams.DEFAULT_FILE);
// Attach default file logger to init too, to log something, even if asynchronous, direct from any thread.
tempID = registerStringLogger(defaultFileLogger, new LogOptions(Streams.DEFAULT_FILE.name +".init", CallContext.ANY_THREAD_DIRECT));
attachStringLogger(tempID, Streams.INIT);
// Trace file logger.
// TODO: Create a real if "needed", dedicated file.
attachStringLogger(getLoggerID(Streams.DEFAULT_FILE.name), Streams.TRACE_FILE); // Direct to default file for now.
}
/**
* Not "official". TODO: Hide.
*/
public void onReload() {
// Hard clear and re-do loggers. Might result in loss of content.
clear(0L, true); // Can not afford to wait.
createDefaultLoggers(ConfigManager.getConfigFile());
}
/**
* Necessary logging to a primary thread task (TickTask).
*/
public void startTasks() {
// TODO: Schedule / hide.
((BukkitLogNodeDispatcher) getLogNodeDispatcher()).startTasks();
}
}

View File

@ -0,0 +1,69 @@
package fr.neatmonster.nocheatplus.logging.details;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import fr.neatmonster.nocheatplus.components.TickListener;
import fr.neatmonster.nocheatplus.utilities.TickTask;
public class BukkitLogNodeDispatcher extends AbstractLogNodeDispatcher { // TODO: Name.
/**
* Permanent TickListener for logging [TODO: on-demand scheduling, but thread-safe. With extra lock.]
*/
private final TickListener taskPrimary = new TickListener() {
@Override
public void onTick(final int tick, final long timeLast) {
if (runLogsPrimary()) {
// TODO: Here or within runLogsPrimary, handle rescheduling.
}
}
};
/**
* Needed for scheduling.
*/
private final Plugin plugin;
public BukkitLogNodeDispatcher(Plugin plugin) {
this.plugin = plugin;
}
public void startTasks() {
// TODO: This is a temporary solution. Needs on-demand scheduling [or a wrapper task].
TickTask.addTickListener(taskPrimary);
}
@Override
protected void logINIT(Level level, String message) {
// TODO: This is cheating !
}
@Override
protected void scheduleAsynchronous() {
synchronized (queueAsynchronous) {
if (taskAsynchronousID == -1) {
// Deadlocking should not be possible.
taskAsynchronousID = Bukkit.getScheduler().runTaskAsynchronously(plugin, taskAsynchronous).getTaskId();
// TODO: Re-check task id here.
}
}
}
@Override
protected final boolean isPrimaryThread() {
return Bukkit.isPrimaryThread();
}
@Override
protected void cancelTask(int taskId) {
Bukkit.getScheduler().cancelTask(taskId);
}
}