This package enables unified logging between development and production. In development, display can be controlled via a verbosity number. In production, recent log messages are stored and can be retrieved to support error reporting.
A log message is displayed if in development mode (this can be changed) and the message verbosity level is at most the selected verbosity level.
It also supports the "structured management of exception throwing" (see below).
Messages are stored in a client-side collection to support client debugging and end-user reporting. On the server, similar provisions are made albeit with more flexibility to enable choices like "off-site (different Meteor server) recording" (esp. in production).
- Usage:
- Structured Exception Throwing
- Beyond
Meteor.isDevelopment
- Utilities for Records
- Additional Usage Hints
- Implementation Patterns
Begin with:
import { Log } from "meteor/convexset:log";
Simple logging:
Log.info(/* verbosity level: */ 5, "This is message number", 1);
Log.warn(/* verbosity level: */ 2, "This is message number", 2);
Log.error(/* verbosity level: */ 1, "This is message number", 3);
Log.log(/* verbosity level: */ 6, "This is message number", 4); /* not displayed on screen */
Note that the default verbosity level is 5
, so the last message above (via Log.log
) will not be displayed.
To set the verbosity level, simply do:
Log.verbosity = 2; // fewer messages than the default of 5
Generally, more important messages should be logged with a lower verbosity level.
Logging with tags:
Log.info({
verbosity: 3,
tags: ["cats", "pets"]
}, "I have one cat.");
To display line numbers properly, set
Log.displayLineNumbers = true;
It is false by default.
To exclude line numbers by content (sub-strings),
Log.excludeLineNumbersWith = ['file-of-much-logging-and-little-consequence', 'other-file'];
// or even...
Log.excludeLineNumbersWith.push('this-file');
Log.excludeLineNumbersWith.push('that-file');
It is []
by default.
Simply use set the appendStackTrace
option to true
(default: `false):
Log.info({
appendStackTrace: true
}, "Stack trace:");
and a stack-trace will be appended to the list of arguments passed into the logger.
To only record (but not display) the stack trace, set the recordStackTrace
option to true
(default: `false):
Log.info({
recordStackTrace: true
}, "Stack trace recorded.");
and a stack-trace will be stored in the relevant collection(s) if recording is enabled.
To add a, typically, temporary filter on what to display (e.g.: to focus on one component), simply add a filter like so:
Log.currentDisplayFilter = function({verbosity, tags, record}) {
// contains a certain tag
return tags.indexOf("hungry-hungry-hippos") !== -1;
};
Tags are perhaps the most useful way to use this tool.
The Log.currentDisplayFilter
is invoked with a single argument taking the form of objects (with 4 keys) looking like:
{
logLevel: "log", // one of "log", "info", "warn" or "error"
verbosity: 5,
tags: ["something", "something-else"],
record: true
}
Note that exceptions are tagged with "exception"
and the name of the exception.
To unset:
Log.currentDisplayFilter = null;
Note: This collection is cleared on logout.
Setting the number of hours of data to keep (default: 0.25):
Log.hoursOfDataToKeep = 5 * (1/60); // Keep 5 min of data
Showing history of log messages (including those that were not displayed), use Log.allRecords
. For example:
Log.allRecords.forEach(x => console.log(x))
To obtain everything in serialized form use Log.allRecordsSerialized
.
To not store a message on the client (possibly because of frequency concerns):
Log.info({
verbosity: 3,
tags: ["cats", "pets"],
record: false /* default: true */
}, "I have one cat.");
Server-side recording can be set-up as follows:
Log.storeServerMessages({
collection: someCollection,
timeToLiveInHours: 24,
additionalLoggingPredicate: () => true,
publications: [
{
pubName: "log-all",
selector: {},
pubAuthFunction: function () {
return !!this.userId;
}
}, {
pubName: "log-100",
selector: {},
options: {
limit: 100,
sort: {
ts: 1
}
}
pubAuthFunction: function () {
return !!this.userId;
}
},
]
});
To elaborate on the options:
collection
:- default:
new Mongo.Collection(null)
- For logging on a different server, use something like
new Meteor.Collection("log", { /* connection: ... */ })
- default:
timeToLiveInHours
:- default:
24
- default:
additionalLoggingPredicate
:- default:
24
- default:
publications
: an array of objects that describe the publications to create- default:
[]
- Keys:
pubName
: the name of the publication to create Defaults to"log"
.selector
: the selector for items to return. Defaults to{}
. See this for more info.options
: options for the relevant find query. Defaults to{sort: {ts: 1}}
.pubAuthFunction
: authorization function for publication; this should returntrue
if authorized andfalse
otherwise. Will be invoked with the usual context for Meteor publications (i.e.: the usualthis
). Defaults to() => true
.
- default:
Where a set of parameters are repeated frequently, especially a set of tags coupled with a verbosity level (even a decision whether to store the log record), a "partially applied" logger maybe prepared with those parameters pre-specified. In particular, with:
const catLogger = Log.info.withParams({
verbosity: 3,
tags: ["cats", "pets"],
record: false
});
..., the code
catLogger("I have one cat.");
catLogger("You have two cats.");
is equivalent to:
Log.info({
verbosity: 3,
tags: ["cats", "pets"],
record: false
}, "I have one cat.");
Log.info({
verbosity: 3,
tags: ["cats", "pets"],
record: false
}, "You have two cats.");
"Partially applied loggers" may be generated by calling withParams
on Log.log
, Log.info
, Log.warn
and Log.error
.
One can get some additional code to run whenever Log.log
, Log.info
, Log.warn
or Log.error
are called by registering handlers like so:
Log.registerAdditionalLogHandler(function({ args, logLevel, verbosity, tags, record }) {
/* do something */
})
Each added hook is invoked with a single argument taking the form of objects (with 5 keys) looking like:
{
args: ["the logging arguments", "passed into", 1, "of", 4, "log functions"],
logLevel: "info",
verbosity: 8,
tags: ["something", "something-else"],
record: true
}
// as a result of Log.info({
// verbosity: 8,
// tags: ["something", "something-else"],
// }, "the logging arguments", "passed into", 1, "of", 4, "log functions");
The handlers are fired after logging and recording (into collections, where applicable) happen.
See example app (which is more akin to a dump truck than an "app") for an instance of using an additional log handler to send messages of a certain type (by logLevel
and tags
) to a chat room. (This is useful for alerting customer support of something bad happening that would probably require a quick response... Or to give the illusion of rapid and effective response.)
One should not have to repeat or repeatedly generate messages for exceptions. It should be "write once, use everywhere". Therefore, consider the following.
Exceptions should be registered using Log.registerException
as follows:
Log.registerException(exceptionName, exceptionMessage);
// Specifically
Log.registerException("invalid-argument", "Your argument is invalid.");
Log.registerException("not-a-number", function nanMessage(item) {
return `${item} is not a number.`;
});
Log.registerException("item-out-of-range", function oorMessage({validRange, item}) {
return `Item out of range (item: ${item}, valid range: ${validRange})`;
});
Generally speaking, if the exception message argument is a function, it will be taken to be a function of one argument (note the use of destructuring in the last example.)
To throw an exception, simply do:
Log.throwException("invalid-argument");
// or
Log.throwException("not-a-number", "9");
// or
Log.throwException("item-out-of-range", {
item: 10,
validRange: [0, 1]
});
(Note that an exception with code "exception-not-registered"
will be thrown in the case of an exception that has not been registered using Log.registerException
.)
To just generate the exception message, use Log.generateExceptionMessage
instead of Log.throwException
.
To just generate the exception for later throwing, use Log.makeException
instead of Log.throwException
.
What will happen is that a message at "log level" error
will be logged with verbosity level 0
(and it will be given two tags "exception"
and the name of the exception). The stack trace will also be included. You may customize verbosity and log level as follows:
Log.throwException("invalid-argument", null, {
logLevel: "warn", // valid choices: "log", "info", "warn", "error"
verbosity: 7
});
// or
Log.throwException("not-a-number", "9", {
logLevel: "warn", // valid choices: "log", "info", "warn", "error"
verbosity: 7
});
// or
Log.throwException("item-out-of-range", {
item: 10,
validRange: [0, 1]
}, {
logLevel: "error", // valid choices: "log", "info", "warn", "error"
verbosity: 7
});
Partial application support is available for exceptions as well. See the following examples:
const throwNaNException = Log.prepareExceptionThrower("not-a-number", {
verbosity: 1,
logLevel: "warn"
});
// invoke with: throwNaNException("7");
const throwNaNException_Simpler = Log.prepareExceptionThrower("not-a-number");
// invoke with: throwNaNException("7");
Specifically, to prepare an "exception thrower", pass in the name of the exception and an optional set of parameters that one might otherwise pass as the third parameter of Log.throwException
.
Simply invoke the returned "exception thrower" to throw the exception. If the exception takes a data
parameter, pass in that data as the sole argument.
By default, the criterion for the display of log
To log under different circumstances set a different "display predicate" as follows:
Log.displayPredicate = function display30PercentOfMessagesAtRandomAndOnlyIfInDevMode() {
return (Math.random() < 0.3) && Meteor.isDevelopment;
}
(Verbosity requirements still apply.)
It was previously mentioned that the entire history of log messages (including those that were not displayed), can be obtained using Log.allRecords
. For example:
Log.allRecords.forEach(x => console.log(x))
(To obtain everything in serialized form one may use Log.allRecordsSerialized
.)
Here are some additional utilities that might help:
Log.displayRecords(records)
: displays an array of records "nicely" (e.g.: Log.displayRecords(Log.allRecords)
)
Log.getRecordsWithTag(tag)
: returns all records with a particular tag
Log.getRecordsWithSomeTagInList(tags)
: returns all records with at least one tag in a list of tags (tags
being an array of tags)
Log.getRecordsWithAllTags(tags)
: returns all records with all of the list of tags (tags
being an array of tags)
One might like to have a (client-side) file with:
import { Log } from "meteor/validatis:log";
Meteor.startup(function() {
if (Meteor.isDevelopment) {
_Log = Log;
}
});
- Detailed logging and verbosity level shifting for development
- Initial development at high verbosity and status messages (tagged by component) with similar verbosity levels
- Down-shifting verbosity for preventing display overload as the code for the component stabilizes
- Viewing detailed log messages by tag and other things during development without needing to restart to reproduce errors
- Being able to "focus" in development by setting/unsetting
Log.currentDisplayFilter
- Bug report data capture using the client side collection
- It is not possible to not expose
Log
on the client in production (c.f.:Packages
). Too much detail makes reverse engineering your product easier. Strike a balance. - Off-site server recording in production (to not overload the main production database)
- Hooking of additional handlers whenever logging methods are called
- e.g.: on-screen notifications
- e.g.: major issues -> Slack/HipChat/IM
- Clean exception handling