Browse Source
Now `LoggerAPI`, instead of simply loggin messages by itself, uses logger objects (deriving from `Logger` class) to output log messages for it (previous way of logging by `Log()` method is available by `ConsoleLogger`). Multiple loggers can be configured per each log level, which can be done via config.pull/8/head
Anton Tarasenko
4 years ago
20 changed files with 1152 additions and 278 deletions
@ -0,0 +1,41 @@ |
|||||||
|
/** |
||||||
|
* Simple logger class that uses `Log` method to print all of the |
||||||
|
* log messages. Supports all of the default `acediaStamp`, `timeStamp` and |
||||||
|
* `levelStamp` settings |
||||||
|
* Copyright 2021 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class ConsoleLogger extends Logger |
||||||
|
perObjectConfig |
||||||
|
config(AcediaSystem) |
||||||
|
dependson(LoggerAPI); |
||||||
|
|
||||||
|
public function Write(Text message, LoggerAPI.LogLevel messageLevel) |
||||||
|
{ |
||||||
|
local MutableText builder; |
||||||
|
if (message != none) |
||||||
|
{ |
||||||
|
builder = GetPrefix(messageLevel); |
||||||
|
builder.Append(message); |
||||||
|
Log(builder.ToPlainString()); |
||||||
|
builder.FreeSelf(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,355 @@ |
|||||||
|
/** |
||||||
|
* Object of this class is meant to represent a single log message that |
||||||
|
* can have parameters, specified by "%<number>" definitions |
||||||
|
* (e.g. "Thing %1 conflicts with thing %2, so we will have to remove %3"). |
||||||
|
* Log message only has to prepare (break into parts) provided human-readable |
||||||
|
* string once and then will be able to quickly perform argument insertion |
||||||
|
* (for which several convenient `Arg*()` methods are provided). |
||||||
|
* The supposed way to use `LogMessage` is is in conjunction with |
||||||
|
* `LoggerAPI`'s `Auto()` method that takes `Definition` with pre-filled |
||||||
|
* message (`m`) and type (`t`), then: |
||||||
|
* 1. (first time only) Generates a new `LogMessage` from them; |
||||||
|
* 2. Returns `LogMessage` object that, whos arguments are supposed to be |
||||||
|
* filled with `Arg*()` methods; |
||||||
|
* 3. When the appropriate amount of `Arg*()` calls (by the number of |
||||||
|
* specified "%<number>" tags) was made - logs resulting message. |
||||||
|
* (4). If message takes no arguments - no `Arg*()` calls are necessary; |
||||||
|
* (5). If new `Auto()` call is made before previous message was provided |
||||||
|
* enough arguments - error will be logged and previous message |
||||||
|
* will be discarded (along with it's arguments). |
||||||
|
* For more information about using it - refer to the documentation. |
||||||
|
* Copyright 2021 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class LogMessage extends AcediaObject; |
||||||
|
|
||||||
|
// Flag to prevent `Initialize()` being called several times in a row |
||||||
|
var private bool isInitialized; |
||||||
|
// With what level was this message initialized? |
||||||
|
var private LoggerAPI.LogLevel myLevel; |
||||||
|
|
||||||
|
/** |
||||||
|
* We essentially break specified log string into parts |
||||||
|
* (replacing empty parts with `none`) like this: |
||||||
|
* "This %1 is %3, not %2" => |
||||||
|
* * "This " |
||||||
|
* * " is " |
||||||
|
* * ", not " |
||||||
|
* * `none` |
||||||
|
* Arguments always fit between these parts, so if there is `N` parts, |
||||||
|
* there will be `N - 1` arguments. We consider that we are done filling |
||||||
|
* arguments when amount of `Arg*()` calls reaches that number. |
||||||
|
* |
||||||
|
* For future localization purposes we do not assume that arguments are |
||||||
|
* specified from left to right in order: in our example, |
||||||
|
* if we make following calls: `.Arg("one").Arg("caged").Arg("free")`, |
||||||
|
* we will get the string: "This one is free, not caged". |
||||||
|
* To remember the order we keep a special array, in our case it would be |
||||||
|
* [1, 3, 2], except normalized to start from zero: [0, 2, 1]. |
||||||
|
*/ |
||||||
|
// Parts of the initial message, broken by "%<number> tags |
||||||
|
var private array<Text> logParts; |
||||||
|
// Defines order of arguments: |
||||||
|
// `i` -> argument at what number to use at insertion place `i`? |
||||||
|
// (`i` starting from zero instead of `1`). |
||||||
|
var private array<int> normalizedArguments; |
||||||
|
|
||||||
|
// Only default value is used for this variable: it remembers what |
||||||
|
// `LogMessage` currently stores "garbage": temporary `Text` object to create |
||||||
|
// a log message. Making an `Arg*()` call on any other `LogMessage` will cause |
||||||
|
// progress of `default.dirtyLogMessage` to be reset, thus enforcing that only |
||||||
|
// one `LogMessage` can be in the process of filling itself with arguments at |
||||||
|
// a time and, therefore, only one can be "dirty": contain temporary |
||||||
|
// `Text` objects. |
||||||
|
// This way using `LogMessage` would not lead to accumulating large |
||||||
|
// amounts of trash objects, since only one of them can "make a mess". |
||||||
|
var private LogMessage dirtyLogMessage; |
||||||
|
// Arguments, collected so far by the `Arg*()` calls |
||||||
|
var private array<Text> collectedArguments; |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
isInitialized = false; |
||||||
|
_.memory.FreeMany(logParts); |
||||||
|
_.memory.FreeMany(collectedArguments); |
||||||
|
logParts.length = 0; |
||||||
|
collectedArguments.length = 0; |
||||||
|
normalizedArguments.length = 0; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Initialize new `LogMessage` object by a given definition. |
||||||
|
* Can only be done once. |
||||||
|
* |
||||||
|
* Correct functionality is guaranteed when arguments start from either |
||||||
|
* `0` or `1` and then increase in order, without gaps or repetitions. |
||||||
|
* `Initialize()` will attempt to correctly initialize `LogMessage` in case |
||||||
|
* these rules are broken, by making assumptions about user's intentions, |
||||||
|
* but behavior in that case should be considered undefined. |
||||||
|
* |
||||||
|
* @param logMessageDefinition Definition to take message parameter from. |
||||||
|
*/ |
||||||
|
public final function Initialize(LoggerAPI.Definition logMessageDefinition) |
||||||
|
{ |
||||||
|
local int nextArgument; |
||||||
|
local Parser parser; |
||||||
|
local MutableText nextLogPart, nextChunk; |
||||||
|
local Text.Character percentCharacter; |
||||||
|
local array<int> parsedArguments; |
||||||
|
if (isInitialized) { |
||||||
|
return; |
||||||
|
} |
||||||
|
isInitialized = true; |
||||||
|
myLevel = logMessageDefinition.l; |
||||||
|
percentCharacter = _.text.GetCharacter("%"); |
||||||
|
parser = _.text.ParseString(logMessageDefinition.m); |
||||||
|
nextLogPart = _.text.Empty(); |
||||||
|
// General idea is simply to repeat: parse until "%" -> parse "%<number>" |
||||||
|
while (!parser.HasFinished()) |
||||||
|
{ |
||||||
|
parser.MUntil(nextChunk, percentCharacter).Confirm(); |
||||||
|
nextLogPart.Append(nextChunk); |
||||||
|
// If we cannot parse "%" after `MUntil(nextChunk, percentCharacter)`, |
||||||
|
// then we have parsed everything |
||||||
|
if (!parser.Match(P("%")).Confirm()) { |
||||||
|
break; |
||||||
|
} |
||||||
|
// We need to check whether it i really "%<number>" tag and not |
||||||
|
// just a "%" symbol without number |
||||||
|
if (parser.MInteger(nextArgument).Confirm()) |
||||||
|
{ |
||||||
|
parsedArguments[parsedArguments.length] = nextArgument; |
||||||
|
logParts[logParts.length] = nextLogPart.Copy(); |
||||||
|
nextLogPart.Clear(); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
// If it is just a symbol - simply add it |
||||||
|
nextLogPart.AppendCharacter(percentCharacter); |
||||||
|
parser.R(); |
||||||
|
} |
||||||
|
} |
||||||
|
logParts[logParts.length] = nextLogPart.Copy(); |
||||||
|
parser.FreeSelf(); |
||||||
|
nextLogPart.FreeSelf(); |
||||||
|
CleanupEmptyLogParts(); |
||||||
|
NormalizeArguments(parsedArguments); |
||||||
|
} |
||||||
|
|
||||||
|
// Since `none` and empty `Text` will be treated the same way by the `Append()` |
||||||
|
// operation, we do not need to keep empty `Text` objects and can simply |
||||||
|
// replace them with `none`s |
||||||
|
private final function CleanupEmptyLogParts() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
for (i = 0; i < logParts.length; i += 1) |
||||||
|
{ |
||||||
|
if (logParts[i].IsEmpty()) |
||||||
|
{ |
||||||
|
logParts[i].FreeSelf(); |
||||||
|
logParts[i] = none; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Normalize enumeration by replacing them with natural numbers sequence: |
||||||
|
// [0, 1, 2, ...] in the same order: |
||||||
|
// [2, 6, 3] -> [0, 2, 1] |
||||||
|
// [-2, 0, 4, -7] -> [1, 2, 3, 0] |
||||||
|
// [1, 1, 2, 1] -> [0, 1, 3, 2] |
||||||
|
private final function NormalizeArguments(array<int> argumentsOrder) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local int nextArgument; |
||||||
|
local int lowestArgument, lowestArgumentIndex; |
||||||
|
normalizedArguments.length = 0; |
||||||
|
normalizedArguments.length = argumentsOrder.length; |
||||||
|
while (nextArgument < normalizedArguments.length) |
||||||
|
{ |
||||||
|
// Find next minimal index and record next natural number |
||||||
|
// (`nextArgument`) into it |
||||||
|
for (i = 0; i < argumentsOrder.length; i += 1) |
||||||
|
{ |
||||||
|
if (argumentsOrder[i] < lowestArgument || i == 0) |
||||||
|
{ |
||||||
|
lowestArgumentIndex = i; |
||||||
|
lowestArgument = argumentsOrder[i]; |
||||||
|
} |
||||||
|
} |
||||||
|
argumentsOrder[lowestArgumentIndex] = MaxInt; |
||||||
|
normalizedArguments[lowestArgumentIndex] = nextArgument; |
||||||
|
nextArgument += 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fills next argument in caller `LogMessage` with given `Text` argument. |
||||||
|
* |
||||||
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
||||||
|
* all arguments leads to message being logged. |
||||||
|
* |
||||||
|
* @param argument This argument will be managed by the `LogMessage`: |
||||||
|
* you should not deallocate it by hand or rely on passed `Text` to not |
||||||
|
* be deallocated. This also means that you should not pass `Text` objects |
||||||
|
* returned by `P()`, `C()` or `F()` calls. |
||||||
|
* @return Caller `LogMessage` to allow for method chaining. |
||||||
|
*/ |
||||||
|
public final function LogMessage Arg(Text argument) |
||||||
|
{ |
||||||
|
local Text assembledMessage; |
||||||
|
if (IsArgumentListFull()) { |
||||||
|
return self; |
||||||
|
} |
||||||
|
// Do we need to clean old `LogMessage` from it's arguments first? |
||||||
|
if (default.dirtyLogMessage != none && default.dirtyLogMessage != self) { |
||||||
|
default.dirtyLogMessage.Reset(); |
||||||
|
} |
||||||
|
default.dirtyLogMessage = self; // `self` is dirty with arguments now |
||||||
|
collectedArguments[collectedArguments.length] = argument; |
||||||
|
if (IsArgumentListFull()) |
||||||
|
{ |
||||||
|
// Last argument - have to log what we have collected |
||||||
|
assembledMessage = Collect(); |
||||||
|
_.logger.LogAtLevel(assembledMessage, myLevel); |
||||||
|
assembledMessage.FreeSelf(); |
||||||
|
return self; |
||||||
|
} |
||||||
|
return self; |
||||||
|
} |
||||||
|
|
||||||
|
// Check whether we have enough arguments to completely make log message: |
||||||
|
// each argument goes in between two log parts, so there is |
||||||
|
// `logParts.length - 1` arguments total. |
||||||
|
private final function bool IsArgumentListFull() |
||||||
|
{ |
||||||
|
return collectedArguments.length >= logParts.length - 1; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fills next argument in caller `LogMessage` with given `int` argument. |
||||||
|
* |
||||||
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
||||||
|
* all arguments leads to message being logged. |
||||||
|
* |
||||||
|
* @param argument This value will be converted into `Text` and pasted |
||||||
|
* into the log message. |
||||||
|
* @return Caller `LogMessage` to allow for method chaining. |
||||||
|
*/ |
||||||
|
public final function LogMessage ArgInt(int argument) |
||||||
|
{ |
||||||
|
return Arg(_.text.FromInt(argument)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fills next argument in caller `LogMessage` with given `float` argument. |
||||||
|
* |
||||||
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
||||||
|
* all arguments leads to message being logged. |
||||||
|
* |
||||||
|
* @param argument This value will be converted into `Text` and pasted |
||||||
|
* into the log message. |
||||||
|
* @return Caller `LogMessage` to allow for method chaining. |
||||||
|
*/ |
||||||
|
public final function LogMessage ArgFloat(float argument) |
||||||
|
{ |
||||||
|
return Arg(_.text.FromFloat(argument)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fills next argument in caller `LogMessage` with given `bool` argument. |
||||||
|
* |
||||||
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
||||||
|
* all arguments leads to message being logged. |
||||||
|
* |
||||||
|
* @param argument This value will be converted into `Text` and pasted |
||||||
|
* into the log message. |
||||||
|
* @return Caller `LogMessage` to allow for method chaining. |
||||||
|
*/ |
||||||
|
public final function LogMessage ArgBool(bool argument) |
||||||
|
{ |
||||||
|
return Arg(_.text.FromBool(argument)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fills next argument in caller `LogMessage` with given `class<Object>` |
||||||
|
* argument. |
||||||
|
* |
||||||
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
||||||
|
* all arguments leads to message being logged. |
||||||
|
* |
||||||
|
* @param argument This value will be converted into `Text` and pasted |
||||||
|
* into the log message. |
||||||
|
* @return Caller `LogMessage` to allow for method chaining. |
||||||
|
*/ |
||||||
|
public final function LogMessage ArgClass(class<Object> argument) |
||||||
|
{ |
||||||
|
return Arg(_.text.FromClass(argument)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Resets current progress of filling caller `LogMessage` with arguments, |
||||||
|
* deallocating already passed ones. |
||||||
|
* |
||||||
|
* @return Caller `LogMessage` to allow for method chaining. |
||||||
|
*/ |
||||||
|
public final function LogMessage Reset() |
||||||
|
{ |
||||||
|
_.memory.FreeMany(collectedArguments); |
||||||
|
collectedArguments.length = 0; |
||||||
|
return self; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns `LogMessage`, assembled with it's arguments into the `Text`. |
||||||
|
* |
||||||
|
* If some arguments were not yet filled - they will treated as empty `Text` |
||||||
|
* values. |
||||||
|
* |
||||||
|
* This result will be reset if `Reset()` method is called or another |
||||||
|
* `LogMessage` starts filling itself with arguments. |
||||||
|
* |
||||||
|
* @return Caller `LogMessage`, assembled with it's arguments into the `Text`. |
||||||
|
*/ |
||||||
|
public final function Text Collect() |
||||||
|
{ |
||||||
|
local int i, argumentIndex; |
||||||
|
local Text result; |
||||||
|
local Text nextArgument; |
||||||
|
local MutableText builder; |
||||||
|
if (logParts.length == 0) { |
||||||
|
return P("").Copy(); |
||||||
|
} |
||||||
|
builder = _.text.Empty(); |
||||||
|
for (i = 0; i < logParts.length - 1; i += 1) |
||||||
|
{ |
||||||
|
nextArgument = none; |
||||||
|
// Since arguments might not be specified in order - |
||||||
|
argumentIndex = normalizedArguments[i]; |
||||||
|
if (argumentIndex < collectedArguments.length) { |
||||||
|
nextArgument = collectedArguments[argumentIndex]; |
||||||
|
} |
||||||
|
builder.Append(logParts[i]).Append(nextArgument); |
||||||
|
} |
||||||
|
builder.Append(logParts[logParts.length - 1]); |
||||||
|
result = builder.Copy(); |
||||||
|
builder.FreeSelf(); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,151 @@ |
|||||||
|
/** |
||||||
|
* Base class for implementing "loggers" - objects that actually write log |
||||||
|
* messages somewhere. To use it - simply implement `Write()` method, |
||||||
|
* preferably making use of `GetPrefix()` method. |
||||||
|
* Copyright 2021 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class Logger extends AcediaObject |
||||||
|
perObjectConfig |
||||||
|
config(AcediaSystem) |
||||||
|
dependson(LoggerAPI) |
||||||
|
abstract; |
||||||
|
|
||||||
|
// Named loggers are stored here to avoid recreating them |
||||||
|
var private AssociativeArray loadedLoggers; |
||||||
|
|
||||||
|
// Should `Logger` display prefix indicating it's a log message from Acedia? |
||||||
|
var protected config bool acediaStamp; |
||||||
|
// Should `Logger` display time stamp prefix in front of log messages? |
||||||
|
var protected config bool timeStamp; |
||||||
|
// Should `Logger` display information about what level message was logged? |
||||||
|
var protected config bool levelStamp; |
||||||
|
|
||||||
|
var protected const int TDEBUG, TINFO, TWARNING, TERROR, TFATAL, TTIME, TACEDIA; |
||||||
|
var protected const int TSPACE; |
||||||
|
|
||||||
|
/** |
||||||
|
* Method for creating named `Logger`s that can have their settings prepared |
||||||
|
* in the config file. Only one `Logger` is made for every |
||||||
|
* unique (case insensitive) `loggerName`. |
||||||
|
* |
||||||
|
* @param loggerName Name of the logger instance to return. Consequent calls |
||||||
|
* with the same `loggerName` value will return the same `Logger`, |
||||||
|
* unless it is deallocated. |
||||||
|
* @return Logger with object name `loggerName`. |
||||||
|
*/ |
||||||
|
public final static function Logger GetLogger(Text loggerName) |
||||||
|
{ |
||||||
|
local Logger loggerInstance; |
||||||
|
local Text loggerKey; |
||||||
|
if (default.loadedLoggers == none) |
||||||
|
{ |
||||||
|
// TODO: do this in static constructor |
||||||
|
default.loadedLoggers = __().collections.EmptyAssociativeArray(); |
||||||
|
} |
||||||
|
if (loggerName == none) { |
||||||
|
return none; |
||||||
|
} |
||||||
|
loggerKey = loggerName.LowerCopy(); |
||||||
|
loggerInstance = Logger(default.loadedLoggers.GetItem(loggerKey)); |
||||||
|
if (loggerInstance == none) |
||||||
|
{ |
||||||
|
// TODO: important to redo this via `MemoryAPI` to call constructors |
||||||
|
loggerInstance = new(none, loggerName.ToPlainString()) default.class; |
||||||
|
loggerInstance._constructor(); |
||||||
|
default.loadedLoggers.SetItem(loggerKey, loggerInstance); |
||||||
|
} |
||||||
|
loggerKey.FreeSelf(); |
||||||
|
return loggerInstance; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Auxiliary method for generating log message prefix based on `acediaStamp`, |
||||||
|
* `timeStamp` and `levelStamp` flags according to their description. |
||||||
|
* Method does not provide any guarantees on how exactly. |
||||||
|
* |
||||||
|
* @param messageLevel Message level for which to generate prefix. |
||||||
|
* @return Text (mutable) representation of generated prefix. |
||||||
|
*/ |
||||||
|
protected function MutableText GetPrefix(LoggerAPI.LogLevel messageLevel) |
||||||
|
{ |
||||||
|
local MutableText builder; |
||||||
|
builder = _.text.Empty(); |
||||||
|
if (acediaStamp) { |
||||||
|
builder.Append(T(TACEDIA)); |
||||||
|
} |
||||||
|
if (timeStamp) { |
||||||
|
builder.Append(T(TTIME)); |
||||||
|
} |
||||||
|
// Make output prettier by adding a space after the "[...]" prefixes |
||||||
|
if (!levelStamp && (acediaStamp || timeStamp)) { |
||||||
|
builder.Append(T(TSPACE)); |
||||||
|
} |
||||||
|
if (!levelStamp) { |
||||||
|
return builder; |
||||||
|
} |
||||||
|
switch (messageLevel) |
||||||
|
{ |
||||||
|
case LOG_Debug: |
||||||
|
builder.Append(T(TDEBUG)); |
||||||
|
break; |
||||||
|
case LOG_Info: |
||||||
|
builder.Append(T(TINFO)); |
||||||
|
break; |
||||||
|
case LOG_Warning: |
||||||
|
builder.Append(T(TWARNING)); |
||||||
|
break; |
||||||
|
case LOG_Error: |
||||||
|
builder.Append(T(TERROR)); |
||||||
|
break; |
||||||
|
case LOG_Fatal: |
||||||
|
builder.Append(T(TFATAL)); |
||||||
|
break; |
||||||
|
default: |
||||||
|
} |
||||||
|
return builder; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Method that must perform an actual work of outputting message `message` |
||||||
|
* at level `messageLevel`. |
||||||
|
* |
||||||
|
* @param message Message to output. |
||||||
|
* @param messageLevel Level, at which message must be output. |
||||||
|
*/ |
||||||
|
public function Write(Text message, LoggerAPI.LogLevel messageLevel){} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
// Parts of the prefix for our log messages, redirected into kf log file. |
||||||
|
TDEBUG = 0 |
||||||
|
stringConstants(0) = "[Debug] " |
||||||
|
TINFO = 1 |
||||||
|
stringConstants(1) = "[Info] " |
||||||
|
TWARNING = 2 |
||||||
|
stringConstants(2) = "[Warning] " |
||||||
|
TERROR = 3 |
||||||
|
stringConstants(3) = "[Error] " |
||||||
|
TFATAL = 4 |
||||||
|
stringConstants(4) = "[Fatal] " |
||||||
|
TTIME = 5 |
||||||
|
stringConstants(5) = "[hh:mm:ss]" |
||||||
|
TACEDIA = 6 |
||||||
|
stringConstants(6) = "[Acedia]" |
||||||
|
TSPACE = 7 |
||||||
|
stringConstants(7) = " " |
||||||
|
} |
@ -1,166 +0,0 @@ |
|||||||
/** |
|
||||||
* Logger that allows to separate log messages into several levels of |
|
||||||
* significance and lets users and admins to access only the ones they want |
|
||||||
* and/or receive notifications when they happen. |
|
||||||
* Copyright 2020 Anton Tarasenko |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* This file is part of Acedia. |
|
||||||
* |
|
||||||
* Acedia is free software: you can redistribute it and/or modify |
|
||||||
* it under the terms of the GNU General Public License as published by |
|
||||||
* the Free Software Foundation, version 3 of the License, or |
|
||||||
* (at your option) any later version. |
|
||||||
* |
|
||||||
* Acedia is distributed in the hope that it will be useful, |
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||||
* GNU General Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU General Public License |
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
|
||||||
*/ |
|
||||||
class LoggerService extends Service |
|
||||||
config(AcediaLogger); |
|
||||||
|
|
||||||
// Log levels, available in Acedia. |
|
||||||
enum LogLevel |
|
||||||
{ |
|
||||||
// For the purposes of "tracing" the code, when trying to figure out |
|
||||||
// where exactly problems occurred. |
|
||||||
// Should not be used in any released version of |
|
||||||
// your packages/mutators. |
|
||||||
LOG_Track, |
|
||||||
// Information that can be used to track down errors that occur on |
|
||||||
// other people's systems, that developer cannot otherwise pinpoint. |
|
||||||
// Should be used with purpose of tracking a certain issue and |
|
||||||
// not "just in case". |
|
||||||
LOG_Debug, |
|
||||||
// Information about important events that should be occurring under |
|
||||||
// normal conditions, such as initializations/shutdowns, |
|
||||||
// successful completion of significant events, configuration assumptions. |
|
||||||
// Should not occur too often. |
|
||||||
LOG_Info, |
|
||||||
// For recoverable issues, anything that might cause errors or |
|
||||||
// oddities in behavior. |
|
||||||
// Should be used sparingly, i.e. player disconnecting might cause |
|
||||||
// interruption in some logic, but should not cause a warning, |
|
||||||
// since it is something expected to happen normally. |
|
||||||
LOG_Warning, |
|
||||||
// Use this for errors, - events that some operation cannot recover from, |
|
||||||
// but still does not require your module to shut down. |
|
||||||
LOG_Failure, |
|
||||||
// Anything that does not allow your module or game to function, |
|
||||||
// completely irrecoverable failure state. |
|
||||||
LOG_Fatal |
|
||||||
}; |
|
||||||
|
|
||||||
var private const string kfLogPrefix; |
|
||||||
var private const string traceLevelName; |
|
||||||
var private const string DebugLevelName; |
|
||||||
var private const string infoLevelName; |
|
||||||
var private const string warningLevelName; |
|
||||||
var private const string errorLevelName; |
|
||||||
var private const string fatalLevelName; |
|
||||||
|
|
||||||
var private config array< class<Manifest> > registeredManifests; |
|
||||||
var private config bool logTraceInKFLog; |
|
||||||
var private config bool logDebugInKFLog; |
|
||||||
var private config bool logInfoInKFLog; |
|
||||||
var private config bool logWarningInKFLog; |
|
||||||
var private config bool logErrorInKFLog; |
|
||||||
var private config bool logFatalInKFLog; |
|
||||||
|
|
||||||
var private array<string> traceMessages; |
|
||||||
var private array<string> debugMessages; |
|
||||||
var private array<string> infoMessages; |
|
||||||
var private array<string> warningMessages; |
|
||||||
var private array<string> errorMessages; |
|
||||||
var private array<string> fatalMessages; |
|
||||||
|
|
||||||
public final function bool ShouldAddToKFLog(LogLevel messageLevel) |
|
||||||
{ |
|
||||||
if (messageLevel == LOG_Track && logTraceInKFLog) return true; |
|
||||||
if (messageLevel == LOG_Debug && logDebugInKFLog) return true; |
|
||||||
if (messageLevel == LOG_Info && logInfoInKFLog) return true; |
|
||||||
if (messageLevel == LOG_Warning && logWarningInKFLog) return true; |
|
||||||
if (messageLevel == LOG_Failure && logErrorInKFLog) return true; |
|
||||||
if (messageLevel == LOG_Fatal && logFatalInKFLog) return true; |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
public final static function LogMessageToKFLog |
|
||||||
( |
|
||||||
LogLevel messageLevel, |
|
||||||
string message |
|
||||||
) |
|
||||||
{ |
|
||||||
local string levelPrefix; |
|
||||||
levelPrefix = default.kfLogPrefix; |
|
||||||
switch (messageLevel) |
|
||||||
{ |
|
||||||
case LOG_Track: |
|
||||||
levelPrefix = levelPrefix $ default.traceLevelName; |
|
||||||
break; |
|
||||||
case LOG_Debug: |
|
||||||
levelPrefix = levelPrefix $ default.debugLevelName; |
|
||||||
break; |
|
||||||
case LOG_Info: |
|
||||||
levelPrefix = levelPrefix $ default.infoLevelName; |
|
||||||
break; |
|
||||||
case LOG_Warning: |
|
||||||
levelPrefix = levelPrefix $ default.warningLevelName; |
|
||||||
break; |
|
||||||
case LOG_Failure: |
|
||||||
levelPrefix = levelPrefix $ default.errorLevelName; |
|
||||||
break; |
|
||||||
case LOG_Fatal: |
|
||||||
levelPrefix = levelPrefix $ default.fatalLevelName; |
|
||||||
break; |
|
||||||
default: |
|
||||||
} |
|
||||||
Log(levelPrefix @ message); |
|
||||||
} |
|
||||||
|
|
||||||
public final function LogMessage(LogLevel messageLevel, string message) |
|
||||||
{ |
|
||||||
switch (messageLevel) |
|
||||||
{ |
|
||||||
case LOG_Track: |
|
||||||
traceMessages[traceMessages.length] = message; |
|
||||||
case LOG_Debug: |
|
||||||
debugMessages[debugMessages.length] = message; |
|
||||||
case LOG_Info: |
|
||||||
infoMessages[infoMessages.length] = message; |
|
||||||
case LOG_Warning: |
|
||||||
warningMessages[warningMessages.length] = message; |
|
||||||
case LOG_Failure: |
|
||||||
errorMessages[errorMessages.length] = message; |
|
||||||
case LOG_Fatal: |
|
||||||
fatalMessages[fatalMessages.length] = message; |
|
||||||
default: |
|
||||||
} |
|
||||||
if (ShouldAddToKFLog(messageLevel)) |
|
||||||
{ |
|
||||||
LogMessageToKFLog(messageLevel, message); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties |
|
||||||
{ |
|
||||||
// Log everything by default, if someone does not like it - |
|
||||||
// he/she can disable it themselves. |
|
||||||
logTraceInKFLog = true |
|
||||||
logDebugInKFLog = true |
|
||||||
logInfoInKFLog = true |
|
||||||
logWarningInKFLog = true |
|
||||||
logErrorInKFLog = true |
|
||||||
logFatalInKFLog = true |
|
||||||
// Parts of the prefix for our log messages, redirected into kf log file. |
|
||||||
kfLogPrefix = "Acedia:" |
|
||||||
traceLevelName = "Trace" |
|
||||||
debugLevelName = "Debug" |
|
||||||
infoLevelName = "Info" |
|
||||||
warningLevelName = "Warning" |
|
||||||
errorLevelName = "Error" |
|
||||||
fatalLevelName = "Fatal" |
|
||||||
} |
|
@ -0,0 +1,170 @@ |
|||||||
|
/** |
||||||
|
* Set of tests related to `LoggerAPI`. |
||||||
|
* Copyright 2021 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class TEST_LogMessage extends TestCase |
||||||
|
abstract; |
||||||
|
|
||||||
|
// Short-hand for creating disposable `Text` out of a `string` |
||||||
|
// We need it, since `P()` always returns the same value, which might lead to |
||||||
|
// a conflict. |
||||||
|
protected static function Text A(string message) |
||||||
|
{ |
||||||
|
return __().text.FromString(message); |
||||||
|
} |
||||||
|
|
||||||
|
// Short-hand for quickly producing `LogMessage.Definition` |
||||||
|
protected static function LoggerAPI.Definition DEF(string message) |
||||||
|
{ |
||||||
|
local LoggerAPI.Definition result; |
||||||
|
result.m = message; |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
protected static function TESTS() |
||||||
|
{ |
||||||
|
Context("Testing how `LogMessage` collects given arguments."); |
||||||
|
Test_SimpleArgumentCollection(); |
||||||
|
Test_ArgumentCollection(); |
||||||
|
Test_ArgumentCollectionOrder(); |
||||||
|
Test_TypedArgumentCollection(); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_SimpleArgumentCollection() |
||||||
|
{ |
||||||
|
local LogMessage message; |
||||||
|
Issue("`Text` arguments are not correctly pasted."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Message %1and%2: %3")); |
||||||
|
message.Arg(A("umbra")).Arg(A("mumbra")).Arg(A("eleven! ")); |
||||||
|
TEST_ExpectTrue( message.Collect().ToPlainString() |
||||||
|
== "Message umbraandmumbra: eleven! "); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("%1 - was pasted.")); |
||||||
|
message.Arg(A("Heheh")); |
||||||
|
TEST_ExpectTrue(message.Collect().ToPlainString() == "Heheh - was pasted."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("This %%%1 and that %2")); |
||||||
|
message.Arg(A("one")).Arg(A("two")); |
||||||
|
TEST_ExpectTrue( message.Collect().ToPlainString() |
||||||
|
== "This %%one and that two"); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("%1%2")); |
||||||
|
message.Arg(A("one")).Arg(A("two")); |
||||||
|
TEST_ExpectTrue(message.Collect().ToPlainString() == "onetwo"); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("%1")); |
||||||
|
message.Arg(A("only")); |
||||||
|
TEST_ExpectTrue(message.Collect().ToPlainString() == "only"); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Just some string.")); |
||||||
|
TEST_ExpectTrue(message.Collect().ToPlainString() == "Just some string."); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_ArgumentCollection() |
||||||
|
{ |
||||||
|
local LogMessage message; |
||||||
|
Issue("`Text` arguments are not correctly collected after reset."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("This %1 and that %2")); |
||||||
|
message.Arg(A("one")).Arg(A("two")).Reset().Arg(A("huh")).Arg(A("muh")); |
||||||
|
TEST_ExpectTrue( message.Collect().ToPlainString() |
||||||
|
== "This huh and that muh"); |
||||||
|
|
||||||
|
Issue("`Text` arguments are not correctly collected after specifying" |
||||||
|
@ "too many."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Just %1, %2, %3, %4 and %5")); |
||||||
|
message.Arg(A("1")).Arg(A("2")).Arg(A("3")).Arg(A("4")).Arg(A("5")) |
||||||
|
.Arg(A("6")).Arg(A("7")); |
||||||
|
TEST_ExpectTrue( message.Collect().ToPlainString() |
||||||
|
== "Just 1, 2, 3, 4 and 5"); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Just")); |
||||||
|
TEST_ExpectTrue(message.Arg(A("arg")).Collect().ToPlainString() == "Just"); |
||||||
|
|
||||||
|
Issue("`Text` arguments are not correctly collected after specifying" |
||||||
|
@ "too little."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Just %1, %2, %3, %4 and %5")); |
||||||
|
message.Arg(A("1")).Arg(A("2")).Arg(A("3")); |
||||||
|
TEST_ExpectTrue( message.Collect().ToPlainString() |
||||||
|
== "Just 1, 2, 3, and "); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Maybe %1")); |
||||||
|
TEST_ExpectTrue(message.Collect().ToPlainString() == "Maybe "); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_ArgumentCollectionOrder() |
||||||
|
{ |
||||||
|
local LogMessage message; |
||||||
|
Issue("`Text` arguments are not correctly collected if are not specified" |
||||||
|
@ "in order."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("This %2 and that %1")); |
||||||
|
message.Arg(A("huh")).Arg(A("muh")); |
||||||
|
TEST_ExpectTrue( message.Collect().ToPlainString() |
||||||
|
== "This muh and that huh"); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Just %5, %3, %4, %1 and %2")); |
||||||
|
message.Arg(A("1")).Arg(A("2")).Arg(A("3")).Arg(A("4")).Arg(A("5")); |
||||||
|
TEST_ExpectTrue( message.Collect().ToPlainString() |
||||||
|
== "Just 5, 3, 4, 1 and 2"); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
|
||||||
|
Issue("`Text` arguments are not correctly collected if are not specified" |
||||||
|
@ "in order and not enough of them was specified."); |
||||||
|
message.Initialize(DEF("Just %5, %3, %4, %1 and %2")); |
||||||
|
message.Arg(A("1")).Arg(A("2")).Arg(A("3")); |
||||||
|
TEST_ExpectTrue( message.Collect().ToPlainString() |
||||||
|
== "Just , 3, , 1 and 2"); |
||||||
|
} |
||||||
|
|
||||||
|
protected static function Test_TypedArgumentCollection() |
||||||
|
{ |
||||||
|
local LogMessage message; |
||||||
|
Issue("`int` arguments are not correctly collected."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Int: %1")); |
||||||
|
TEST_ExpectTrue(message.ArgInt(-7).Collect().ToPlainString() |
||||||
|
== "Int: -7"); |
||||||
|
|
||||||
|
Issue("`float` arguments are not correctly collected."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Float: %1")); |
||||||
|
TEST_ExpectTrue(message.ArgFloat(3.14).Collect().ToPlainString() |
||||||
|
== "Float: 3.14"); |
||||||
|
|
||||||
|
Issue("`bool` arguments are not correctly collected."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Bool: %1 and %2")); |
||||||
|
TEST_ExpectTrue(message.ArgBool(true).ArgBool(false).Collect() |
||||||
|
.ToPlainString() == "Bool: true and false"); |
||||||
|
|
||||||
|
Issue("`Class` arguments are not correctly collected."); |
||||||
|
message = LogMessage(__().memory.Allocate(class'LogMessage')); |
||||||
|
message.Initialize(DEF("Class: %1")); |
||||||
|
TEST_ExpectTrue(message.ArgClass(class'M14EBRBattleRifle').Collect() |
||||||
|
.ToPlainString() == "Class: KFMod.M14EBRBattleRifle"); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
caseGroup = "Logger" |
||||||
|
caseName = "LogMessage" |
||||||
|
} |
Loading…
Reference in new issue