You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
12 KiB

=========================
Duktape logging framework
=========================
Introduction
============
Duktape 1.x 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.
In Duktape 2.x the logging framework was moved into an optional extra
(``extras/logging``) to avoid portability issues on exotic target platforms.
The extra has the same feature set as Duktape 1.x built-in logging and
provides the same bindings (``Duktape.Logger`` object, ``duk_log()`` and
``duk_log_va()`` API calls).
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)
2014-03-19T02:42:20.425Z INF myLogger: test 123
11 years ago
Each argument 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 message is passed as a buffer to the logging backend, provided
by ``Duktape.Logger.prototype.raw()``. The default implementation writes the
log message to ``stderr`` and appends a newline. The log message is provided
as a buffer (instead of a string) to avoid interning the message unnecessarily.
For short messages, a single dynamic buffer is reused over and over by modifying
the visible size of the buffer (the dynamic buffer is not reallocated), to
eliminate memory churn.
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 lazyJx1(obj) {
return { toLogString: function() { return Duktape.enc('jx', obj); } };
}
logger.debug('complex object:', lazyJx1(obj));
One can use ``bind()`` for the same effect (in this particular case)::
function lazyJx2(obj) {
return { toLogString: Duktape.bind(null, 'jx', obj) };
}
logger.debug('complex object:', lazyJx2(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('jx', this.v);
}
function lazyJx3(val) {
// Per lazy value creation, only creates an object with one property.
return new LazyValue(val);
}
logger.debug('complex object:', lazyJx3(obj));
Lazy formatting can also be done inline, though not very readably::
logger.debug('data:', { toLogString: function() { return Duktape.enc('jx', 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. Be careful to avoid unnecessary
memory and string table churn.
* 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 JX, 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 JX 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?