mirror of https://github.com/svaarala/duktape.git
Sami Vaarala
11 years ago
1 changed files with 289 additions and 0 deletions
@ -0,0 +1,289 @@ |
|||||
|
========================= |
||||
|
Duktape logging framework |
||||
|
========================= |
||||
|
|
||||
|
Introduction |
||||
|
============ |
||||
|
|
||||
|
Duktape contains a built-in very minimal logging framework which has a |
||||
|
small footprint (around 1kB), reasonably high performance, and makes it |
||||
|
easy to change both the frontend and the backend of logging. It is easy |
||||
|
to write log entries from both C and Ecmascript, and then redirect all |
||||
|
the log output to a custom backend. Lazy formatting is also possible. |
||||
|
|
||||
|
The framework focuses on how logger objects are created and what the |
||||
|
logger interface looks like. Other features are quite barebones; for |
||||
|
example the default backend simply writes to the stderr and there is no |
||||
|
advanced backend configuration like multiple log outputs. The user can |
||||
|
easily replace the frontend and the backend functions to extend the |
||||
|
basic feature set in a transparent manner. |
||||
|
|
||||
|
The logging framework also provides API calls to write log entries from C |
||||
|
code. This is very convenient, as these log entries will then nicely |
||||
|
interleave with log entries written from Ecmascript code. |
||||
|
|
||||
|
The logger object API is close to log4javascript except that there is no |
||||
|
special handling for an error object as the last call argument. See: |
||||
|
|
||||
|
* http://log4javascript.org/docs/quickstart.html |
||||
|
|
||||
|
Writing log entries from Ecmascript |
||||
|
=================================== |
||||
|
|
||||
|
A logger object is first created:: |
||||
|
|
||||
|
// Without arguments, the logger defaults to the fileName of the call site |
||||
|
var logger = new Duktape.Logger(); |
||||
|
|
||||
|
// Explicitly name |
||||
|
var logger = new Duktape.Logger('myLogger'); |
||||
|
|
||||
|
// Explicitly unnamed (any non-string argument, null probably best) |
||||
|
var logger = new Duktape.Logger(undefined); |
||||
|
var logger = new Duktape.Logger(null); |
||||
|
|
||||
|
After a logger has been created, log entries are written with the exposed |
||||
|
log writing calls inherited from the logger prototype. There are six |
||||
|
log levels, each with its own frontend function. Each log level also has |
||||
|
a number (0 to 5) which is used e.g. to control log level, and a 3-letter |
||||
|
abbreviation (like INF) used in the log prefix. Some example log calls:: |
||||
|
|
||||
|
logger.trace('loop iteration:', i, 'out of', n); // level 0 (TRC) |
||||
|
logger.debug('objects:', obj1, obj2, obj3); // level 1 (DBG) |
||||
|
logger.info('normal log message'); // level 2 (INF) |
||||
|
logger.warn('exceptional condition'); // level 3 (WRN) |
||||
|
logger.error('something went wrong'); // level 4 (ERR) |
||||
|
logger.fatal('something went really wrong'); // level 5 (FTL) |
||||
|
|
||||
|
The logger functions make a logging decision based on log level. If the |
||||
|
entry gets logged, call arguments are formatted into strings, concatenated |
||||
|
with spaces, and a prefix is added. The prefix contains a timestamp, the |
||||
|
log level, and the logger name. Example:: |
||||
|
|
||||
|
duk> logger.info('test', 123) |
||||
|
1395003609.854 INF myLogger: test 123 |
||||
|
|
||||
|
Each arguments is formatted separately, and if an error is thrown during |
||||
|
formatting, the argument is replaced with string coercion of the error. |
||||
|
Non-object values are formatted in pure C by the default logger functions |
||||
|
to minimize unnecessary calls. Object values are formatted by calling |
||||
|
``fmt()`` method of the logger, usually inherited from |
||||
|
``Duktape.Logger.prototype.fmt()``. |
||||
|
|
||||
|
The default object formatting function calls the ``toLogString()`` function |
||||
|
of the object if it has one, else it simply coerces with ``String(val)``. |
||||
|
The ``toLogString()`` function is a Duktape custom function which allows the |
||||
|
user to control how an object should appear in logs by default. |
||||
|
|
||||
|
Although arguments whose formatting fails get replaced by an error, the |
||||
|
logger API does **not** guarantee that no errors can be thrown. For example, |
||||
|
if formatting fails and also string coercing the formatting error fails, |
||||
|
the latter error will propagate out of the logger. As always, internal errors |
||||
|
like out-of-memory or out-of-stack can occur at any time. |
||||
|
|
||||
|
The final log string is passed to the logging backend, provided by |
||||
|
``Duktape.Logger.prototype.raw()``. The default implementation writes the |
||||
|
log message to ``stderr`` and appends a newline. |
||||
|
|
||||
|
Logger related properties like the logger name (``p``), log level (``l``), |
||||
|
and the ``fmt()`` and ``raw()`` functions are all looked up through the |
||||
|
logger instance so that they can overridden either in the prototype, or on |
||||
|
a per-logger basis. |
||||
|
|
||||
|
Writing log entries from C |
||||
|
========================== |
||||
|
|
||||
|
The Duktape API provides a snprintf-like log call which allows C code to log |
||||
|
to the same backend as Ecmascript code:: |
||||
|
|
||||
|
duk_log(ctx, DUK_LOG_INFO, "return value: %d", rc); |
||||
|
|
||||
|
Note that if you just want to format a plain string, you *must* use a ``%s`` |
||||
|
format to avoid security holes (your string might contain a ``%`` which would |
||||
|
cause uncontrolled memory accesses):: |
||||
|
|
||||
|
const char *my_plain_string = "beware of the %s"; |
||||
|
duk_log(ctx, DUK_LOG_WARN, "%s", my_plain_string); |
||||
|
|
||||
|
Log writes from C code use a representative logger instance stored in |
||||
|
``Duktape.Logger.clog``. You can manipulate or replace this logger to |
||||
|
control C log writes more explicitly. |
||||
|
|
||||
|
Logger objects |
||||
|
============== |
||||
|
|
||||
|
Each logger is an object with a few possible properties: |
||||
|
|
||||
|
* ``n``: name of the logger, added to log lines. If not given, defaults |
||||
|
to the ``fileName`` of the function where to logger is created. If even |
||||
|
that is missing, the value will be missing from the object, and a default |
||||
|
value is inherited. |
||||
|
|
||||
|
* ``l``: minimum log level of the logger. Log writes below this level |
||||
|
are dropped. If missing, a default value is inherited. |
||||
|
|
||||
|
Typically, if log levels are not altered, a logger object only contains the |
||||
|
``n`` property. Loggers are compacted at creation to ensure a minimal |
||||
|
footprint (they very rarely change state). |
||||
|
|
||||
|
Each logger object has as its internal prototype ``Duktape.Logger.prototype``. |
||||
|
The prototype provides: |
||||
|
|
||||
|
* ``n``: a default name ('anon') |
||||
|
* ``l``: a default log level (2, info level) |
||||
|
* log writing methods for each level |
||||
|
|
||||
|
Lazy formatting |
||||
|
=============== |
||||
|
|
||||
|
Lazy formatting is useful when formatting the log arguments is costly and |
||||
|
the log line is normally filtered by the log level. This is often the case |
||||
|
when debug logging complex values like deep serializations of internal state |
||||
|
objects. |
||||
|
|
||||
|
Lazy formatting is easily achievable by using the ``toLogString()`` method. |
||||
|
The simplest but not very efficient approach is:: |
||||
|
|
||||
|
function lazyJsonx1(obj) { |
||||
|
return { toLogString: function() { return Duktape.enc('jsonx', obj); } }; |
||||
|
} |
||||
|
|
||||
|
logger.debug('complex object:', lazyJsonx1(obj)); |
||||
|
|
||||
|
One can use ``bind()`` for the same effect (in this particular case):: |
||||
|
|
||||
|
function lazyJsonx2(obj) { |
||||
|
return { toLogString: Duktape.bind(null, 'jsonx', obj) }; |
||||
|
} |
||||
|
|
||||
|
logger.debug('complex object:', lazyJsonx2(obj)); |
||||
|
|
||||
|
Creating a function instance per lazy-logged value is quite expensive. |
||||
|
Because the ``toLogString()`` is called as a method, lazy values can |
||||
|
inherit from a prototype which is reasonably efficient:: |
||||
|
|
||||
|
function LazyValue(val) { |
||||
|
this.v = val; |
||||
|
} |
||||
|
LazyValue.prototype.toLogString = function () { |
||||
|
return Duktape.enc('jsonx', this.v); |
||||
|
} |
||||
|
function lazyJsonx3(val) { |
||||
|
// Per lazy value creation, only creates an object with one property. |
||||
|
return new LazyValue(val); |
||||
|
} |
||||
|
|
||||
|
logger.debug('complex object:', lazyJsonx3(obj)); |
||||
|
|
||||
|
Lazy formatting can also be done inline, though not very readably:: |
||||
|
|
||||
|
logger.debug('data:', { toLogString: function() { return Duktape.enc('jsonx', data); } }); |
||||
|
|
||||
|
Customizing logging |
||||
|
=================== |
||||
|
|
||||
|
Some options: |
||||
|
|
||||
|
* Add a ``toLogString()`` method to the prototype of interesting objects |
||||
|
to control how they are serialized into strings by the default formatter |
||||
|
``Duktape.Logger.prototype.fmt()``. For instance, you can add the method |
||||
|
to ``Object.prototype`` to provide better logging for all object values. |
||||
|
|
||||
|
* Replace ``Duktape.Logger.prototype.fmt()`` for custom formatting of |
||||
|
object values. |
||||
|
|
||||
|
* Replace ``Duktape.Logger.prototype.raw()`` for redirecting formatted |
||||
|
log lines to an alternate destination. |
||||
|
|
||||
|
* Replace the frontend functions (``Duktape.Logger.prototype.info()`` |
||||
|
etc) for custom formatting of log lines. You may also choose not to |
||||
|
call ``Duktape.Logger.prototype.raw()`` for emitting the formatted |
||||
|
log line, but rather interface with your custom backend directly. |
||||
|
|
||||
|
* Replace the entire ``Duktape.Logger`` constructor and prototype object |
||||
|
for full control over logging. |
||||
|
|
||||
|
* Of course, you can also use an external logging framework. |
||||
|
|
||||
|
Limitations |
||||
|
=========== |
||||
|
|
||||
|
The built-in logging mechanism has several limitations. Most of them are |
||||
|
intentional to keep the logger footprint small: |
||||
|
|
||||
|
* Currently a new logger is created regardless of whether or not a previous |
||||
|
logger exists with the same name. Sometimes it might desirable to return |
||||
|
the same logger instance in this case, so that e.g. the log level can be |
||||
|
controlled by finding a logger and operating on it. You can implement this |
||||
|
by overriding the constructor. |
||||
|
|
||||
|
* There is no way to modify the built-in line format except by overriding |
||||
|
the frontend functions (``Logger.prototype.info()`` etc). This is |
||||
|
intentional, as having a fixed format makes it easier to log faster and |
||||
|
reduce memory churn caused by logging. |
||||
|
|
||||
|
* There is no concept of a logging context for C code. Instead, all log |
||||
|
writes go through a single logger instance. If multiple global objects |
||||
|
exist in the Duktape heap, each global context (or more specifically |
||||
|
``Duktape.Logger`` instance) will have its own logger object. Logging |
||||
|
from C is usually less of a priority so the logging C api is kept very |
||||
|
minimal on purpose. |
||||
|
|
||||
|
Existing frameworks and related links |
||||
|
===================================== |
||||
|
|
||||
|
* http://ajaxpatterns.org/Javascript_Logging_Frameworks |
||||
|
* http://getfirebug.com/logging |
||||
|
* http://log4javascript.org/docs/quickstart.html |
||||
|
* http://log4js.berlios.de/ |
||||
|
* http://benalman.com/projects/javascript-debug-console-log/ |
||||
|
|
||||
|
Future work |
||||
|
=========== |
||||
|
|
||||
|
Format all value types in a useful manner by default |
||||
|
---------------------------------------------------- |
||||
|
|
||||
|
Like JSONX, the logger should write useful log entries for all available |
||||
|
value types by default. Currently this is not the case for e.g. buffer |
||||
|
values. |
||||
|
|
||||
|
Reduce memory churn |
||||
|
------------------- |
||||
|
|
||||
|
Memory churn can be reduced considerably by string coercing all primitive |
||||
|
types (or at least undefined, null, boolean, integer numbers) without going |
||||
|
through string interning. |
||||
|
|
||||
|
Better multiline support |
||||
|
------------------------ |
||||
|
|
||||
|
Perhaps duplicate the prefix but perhaps change the final colon to indicate |
||||
|
continuation, e.g.:: |
||||
|
|
||||
|
<timestamp> INF myLogger: multi |
||||
|
<timestamp> INF myLogger| line |
||||
|
|
||||
|
Or perhaps:: |
||||
|
|
||||
|
<timestamp> INF myLogger: multi |
||||
|
| line |
||||
|
|
||||
|
ASCII sanitization |
||||
|
------------------ |
||||
|
|
||||
|
It would be nice if logger output would be guaranteed to be printable ASCII |
||||
|
only. This needs handling either in the frontend functions (e.g. for strings) |
||||
|
or the final writer function. |
||||
|
|
||||
|
Buffer formatting |
||||
|
----------------- |
||||
|
|
||||
|
Buffer data should maybe be formatted in hex encoded form (like JSONX does). |
||||
|
Since buffers are plain objects, they don't currently go through the formatter, |
||||
|
but that would be easy to change. |
||||
|
|
||||
|
__FILE__ and __LINE__ for C log writes |
||||
|
-------------------------------------- |
||||
|
|
||||
|
Include __FILE__ and __LINE__ automatically in C log writes somehow? |
Loading…
Reference in new issue