From: Ruben Beltran del Rio Date: Sun, 20 Sep 2020 17:47:28 +0000 (+0200) Subject: Recover from npm package X-Git-Url: https://git.r.bdr.sh/rbdr/cologne/commitdiff_plain/58906d77975b35fe93569f8083b90140124f9c41?hp=4b10e604bf4057d7ae8286151d7528d7d7ae1cb9 Recover from npm package --- diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..8e695ec --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +doc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bb7120d --- /dev/null +++ b/.eslintrc @@ -0,0 +1,41 @@ +{ + "rules": { + "indent": [ + 2, + 2 + ], + "quotes": [ + 2, + "single" + ], + "linebreak-style": [ + 2, + "unix" + ], + "semi": [ + 2, + "always" + ], + "no-trailing-spaces": [ + 2, + { + "skipBlankLines": false + } + ], + "eqeqeq": [ + 2, + "allow-null" + ], + "consistent-return": 2, + "curly": 2, + "dot-location": [2, "property"], + "guard-for-in": 2, + "no-extra-bind": 2 + }, + "env": { + "es6": true, + "node": true, + "browser": true + }, + "extends": "eslint:recommended" +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..cc4bbcc --- /dev/null +++ b/.npmignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# JSDoc +doc diff --git a/CHANGELOG b/CHANGELOG index 6f10641..915f2e3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,52 +1,24 @@ -2015-01-21 Ben Beltran - - Version 1.1.3 - - * lib/loggers/file.js: Makes the log work in append mode instead of - overwrite mode. - -2015-01-09 Sergio de la Garza - - Version 1.1.2 - - * lib/cobalt.js (dependencies): Require colors fix the log output when - using the tocken formatter. - - -2015-01-09 Ben Beltran - - Version 1.1.1 - - * lib/loggers/file.js: Writes raw log if no formatter is present. - - * lib/cobalt.js: Logging null now converts it to the string "null"; +2016-01-22 Ruben Beltran Del Rio Version 1.1.0 - * lib/loggers/file.js: Includes the new file logger. + * lib/cologne.js (buildLog): Add a new parameter called meta that expects an + object, it will be used to extend the log. - * lib/formatters/simple.js (format): Simple formatter outputs date and - loglevel. + * lib/cologne/log_utilities.js: Add the getLevelAnsi method that will + return an ANSI keyword depending on the log level sent. - * lib/cobalt.js: Includes the file logger and simple formatter. + * lib/cologne/formatter/simple (format): Now uses getLevelAnsi, this means + that info and debug get colors (they used to be defualt.) info is blue, + debug is green. - * lib/formatters/token.js: Extends with options on - initialization, including a new option isoDate that will make the - date an ISOString. + * lib/cologne/formatter/token (format): Adds support for the {{_ansi:_level}} + token that uses getLevelAnsi to generate color depending on the level. - * README.md: Updates documentation to consider the new options, formatter - and logger. + * README.md: Adds references to all new features - -2014-03-20 Ben Beltran +2016-01-21 Ruben Beltran Del Rio Version 1.0.0 - * lib/cobalt.js (buildLog): Logging a logObject will now use the calling - method's severity instead of the logObject severity. so calling `#error` - will always provide severity level 3. - - * package.json: Updates neon dependency to 2.0, adds tellurium as - dev-dependency. - - * /test: Adds VERY basic tests before tellurium is integrated. + * all: initial release. diff --git a/README.md b/README.md index 87f808f..bf2ae2d 100644 --- a/README.md +++ b/README.md @@ -1,263 +1,300 @@ -# Cobalt # +# Cologne -Cobalt is a simple logger multiplexer that works with a JSON based format. -You can instantiate it with a set of loggers or add them later -(see API reference below). When logging anything, Cobalt will attempt to -generate an Object that conforms to the Cobalt Log Format Definition -(if required) and passes it to every logger it has by calling their `log` -method. +![https://david-dm.org/rbdr/cologne.svg][dependencies-shield] +![https://circleci.com/gh/rbdr/cologne.svg?style=shield][circle-ci-shield] -Example of instantiating a cobalt logger: +Cologne is a logger multiplexer that works mainly with a JSON format. It +can be instantiated with several loggers, or they can be changed after +the fact. + +## Usage + +Install from npm or add to your package.json: -In node: ``` -require('cobalt-log'); +$ npm install cologne ``` -In the browser just require the files. Then: +Create an instance: -``` -this.logger = new Cobalt.Console({ - from : "Breezi Client", - loggers : [ new Cobalt.Logger.JsConsole({ - formatter : Cobalt.Formatter.Token, - formatterOpts : { - formatString : "{{_from}}: {{message}}", - ansiColor : true - } - })] +```javascript +require('cologne'); + +let co = new Cologne({ + from: "Special Worker Logger", + loggers: [ + new Cologne.Logger.Console({ + formatter: new Cologne.Formatter.Token({ + formatString: '[{{_timestamp}}]{{_from}}: {{message}}' + }) + }) + ] }); ``` -This code will create an instance with a JsConsole logger that uses the -Token formatter (See loggers and formatters below). +This example would create a cologne instance with a console logger that +uses a Token formatter. (More on loggers and formatters below.); -Cobalt works in a browser or inside node, so feel free to use cobalt all -over! (Also, see the socket logger below for info on connecting cobalt -loggers) +## Do I need anything special? -## Quick API Reference ## +Node 4.0+ -* **addLogger(logger)**: Adds a logger to the cobalt instance. -* **removeLogger(logger)**: Removes a logger from the cobalt instance. -* **buildLog(item, level=7)**: Generates a cobalt log object as if you had - logged item (it will do this automatically when you log anything) -* **extendLog(object)**: Creates a dummy log object and extends it with - object. -* **buildSeparator**: Generates a cobalt log object that defines a separator +## Quick API Reference + +* **addLogger(logger)**: Adds a logger to the cologne instance. +* **removeLogger(logger)**: Removes a logger from the cologne instance. +* **buildLog(item, level, [meta])**: Generates a cologne log object as if you had + logged an item (it will do this automatically when you log anything.) + level defaults to 6. You can optionally send it an object to extend + the object with. * **log, info, notice, warn, error**: Generates a log object with the appropriate severity level and sends it to all loggers. -* **separator()**: Generates a separator log object and sends it to all - loggers. -* **space(lines)**: Logs an empty string `lines` times -* **indent()**: Increases the indent level globally. -* **indent(callback)**: Increases the indent level for anything logged - from inside the callback. -* **outdent()/outdent(callback)**: Same as indent, but decreases indent level. -* **color()**: Changes the color globally. † -* **color(callback)**: Changes the color for anything logged from inside the - callback. † -* **now()**: Returns the current time in microseconds, using performance.now() - or process.hrtime() if available. If not, falls back to miliseconds. - -† Cobalt doesn't really care about formatting or colors, but it allows you -to set the `_color` property in the generated object. In the end, it's up to -the formatter to decide if it will use this property. However, this maintains -the old cobalt API and gives you flexibility in how you color your logs. - - -## Loggers ## - -Cobalt doesn't depend on any particular logger, and the loggers it expects -to receive is any object that responds to the log method. However, since it -would pass a JSON object instead of a string, this may result in unexpected -behavior for loggers that don't expect it. To ease the use of Cobalt with -existing loggers, cobalt includes a couple of loggers that you can use out -of the box. - - -### Cobalt.Logger.JsConsole ### - -This logger communicates the Javascript console present in web browsers or -node with cobalt. It uses the logLevel to trigger the appropriate method -(e.g. info vs warn vs error). You can also initialize it with a formatter, -to convert the log object to a string: -``` - new Cobalt.Logger.JsConsole({ - formatter : Cobalt.Formatter.Token, - formatterOpts : { - formatString : "[{{_timestamp}}] {{message}} (@{{_from}})" - } - }) -``` +## Loggers -What this does is: it will trigger the method `format` on `formatter` -passing the `logObject` and `formatterOpts`. This means that a formatter is -any object that responds to `format(logObject, formatterOpts)`. It expects -a string to be returned. +Cologne loggers are any object that responds to the `#log()` method. +This methoud should be able to receive any number of arguments and +log them independently. Similar to how you can send multiple arguments +to the JS console. -### Cobalt.Logger.File ### +`#log()` will receive any number of `Cologne Log Objects`. To see what +this format includes, check further below. -This logger communicates a file via a writable stream, and is intended -only for node. Like the JSConsole logger, you can also initialize it with -a formatter to convert the log object to a string: +We include two loggers out of the box: +* `Cologne.Logger.Console` logs to the JS console +* `Cologne.Logger.File` appends to a file + +### Cologne.Logger.Console + +This logger communicates the Javascript console. It uses the log level +to trigger the appropriate method, so `error` logs would go to stderr +as expected when calling `console.error`. + +This logger can be sent a `formatter`, which is an object that responds +to the `#format()` method: it should get a cologne log object and respond +with a string. + +```javascript +new Cologne.Logger.Console({ + formatter : new Cologne.Formatter.Token({ + formatString: '[{{_timestamp}}]{{_from}}: {{message}}' + }) +}); ``` - new Cobalt.Logger.File({ - formatter : Cobalt.Formatter.Token, - formatterOpts : { - formatString : "[{{_timestamp}}] {{message}} (@{{_from}})" - } - }) -``` -What this does is: it will trigger the method `format` on `formatter` -passing the `logObject` and `formatterOpts`. This means that a formatter is -any object that responds to `format(logObject, formatterOpts)`. It expects -a string to be returned. +### Cologne.Logger.File + +This logger opens a writable stream to a file, to which it will append +everything. Like the Console logger it supports a `formatter` property +that will respond to the `#format()` method. + +It MUST include a `file` property on initialization, otherwise it won't +know where to write and you'll get an exception and be sad. + +```javascript +new Cologne.Logger.File({ + file: '/var/log/server_log.log', + formatter : new Cologne.Formatter.Token({ + formatString: '[{{_timestamp}}]{{_from}}: {{message}}' + }) +}); +``` -### Cobalt.Logger.Socket ### +### More Loggers? -This logger sends the log object to a socket using Socket.IO. It does not -format the output. To catch the log from the recipient, you have to listen -for the `log` event, and from there you can pass it to another Cobalt -instance or do whatever you want with it. +We're working on a socket logger. It's separate so you don't have to +install the socket dependencies if you don't want to. -### More Loggers? ### +If you need to, you can roll your own. Check info on the interfaces +below. You can build your own logger easily for any method of transport you find necessary (e.g. mail, database, twitter, etc). Any object that responds -to `#log(logObject)` is a valid logger: +to `#log()` is a valid logger: ```javascript // A valid, very minimalistic logger -var simpleLogger = { - log : function (logObject) { - console.log(logObject.message); +let simpleLogger = { + log : function () { + for (let logObject of arguments) { + this._doSomeMagic(logObject); + } + }, + + _doSomeMagic : function (logObject) { + console.log(logObject + "... but magical!"); } -} +}; logger.addLogger(simpleLogger); ``` -## Formatters ## -Cobalt itself makes no assumptions about the output of the logger and just -passes the object to every logger it has. However, it is clear that loggers -may want to manipulate this object. As shown in the JsConsole, a formatter -should respond to the format method and receive a `logObject` and an -`optsObject`. However, as this is not a core part of Cobalt, this is only a -recommendation (as this is the way the included JsConsole/File loggers do it) -and it is up to the logger on how to transform the object it receives. +## Formatters + +Cologne doesn't need formatters to work, and in fact they're optional in +the included workers. But if you would like to make your logs prettier, +then you can use one of the included formatters or roll your own. + +Formatters are objects that respond to the `#format()` method. It will +receive a single cologne log object (see fields it includes below), and +it should return a string. That's all there is to it. -### Cobalt.Formatter.Simple ### +We include some formatters so you can get running real quicklike: + +* `Cologne.Formatter.Simple` a simple predefined formatter +* `Cologne.Formatter.Token` a formatter that lets you define format + strings that it will use to build your final log. + +### Cologne.Formatter.Simple This is the lazy formatter, it just outputs the string in the following format: ``` -'[{{_timestamp}}][{{_logLevelString}}]{{_from}}: {{_message}}' +'[{{_timestamp}}][{{_levelString}}]{{_from}}: {{message}}' ``` Where `_timestamp` is converted to ISO. -Example output: +#### Accepted Options + +* `colorize` : whether or not to add color. False by default. + +By default we don't colorize the output, but if you enable the flag this +formatter will add a bit of color in the level string. Red for bad, +yellow for warn, blue for info, and white for everything else. +#### Usage + +```javascript +new Cologne.Formatter.Simple({ + colorize: true +}); ``` -cobalt.log("hello world"); -// -> [2015-01-09T16:02:23.102Z][INFO] Generic Cobalt Logger : hello world + +### Example Output + +``` +co.log("hello world"); +// -> [2016-01-21T05:50:36.505Z][INFO] Server Logger: hello world ``` -### Cobalt.Formatter.Token ### +### Cologne.Formatter.Token -The Token formatter is a more advanced, but still fairly simple -formatter. It takes a `formatString` and interpolates the properties -of the object. By default it transforms anything inside double curly -braces `{{likeThis}}`, however you can set a custom `replaceRule`. +The token formatter lets you build strings with simple tokens. When +instantiating, you can specify a `formatString` to interpolate +properties from the logObject. The default version looks for tokens +inside double curly braces like `{{message}}` or `{{_level}}`. If +you don't like it, you can specify your own. -#### Accepted Options #### +#### Accepted Options -* `formatString` : The string used to replace. Defaults to `"{{message}}"` -* `replaceRule` : The regex rule to use for replacement of tokens in the +* `formatString` : The string used to replace. Defaults to `"{{message}}"` +* `replaceRule` : The regex rule to use for replacement of tokens in the formatString. Defaults to `/{{(.*?)}}/g` -* `separatorLength` : How long to print separators. Defaults to 60. * `isoDate` : Whether or not to convert `_timestamp` to ISO - date. Defaults to true. -* `separatorType` : The string to use for the separator. - Defaults to `"="` -* `ansiColor` : Whether to use ANSI colors for output. - Defaults to `false`. This options depends on `colors` - -#### Options #### - -* **formatString**: A string that defines the format of the output. It is a - string with double curly braces denoting fields. For example: - `"[{{_timestamp}}] {{message}} (@{{_from}})"` would attempt to extract the - \_timestamp, message and \_from fields to create a string similar to this: - `"[124896126491.123] Testing the logger (@Client Application)"` - (defaults to `"{{message}}"`) -* **ansiColor**: A boolean value, when `true` will output the string in ANSI - color depending on the severity level (defaults to `false`) - -### More Formatters? ### - -As with loggers, cobalt itself does not worry about these things. However, -if you wish to make a formatter that is exchangable with Token, you just -need to create an object that responds to the`format(logObject, optionsObject)` -method: + date. Defaults to true. Otherwise it'll use the raw timestamp. + +#### Usage + +```javascript +new Cologne.Formatter.Token({ + formatString: '[{{_timestamp}}]{{_from}}: {{message}}' +}); +``` + +#### ANSI tokens + +If you want to add color to your logs, you can use the special \_ansi +token. It has several options which you can call like `{{_ansi:red}}` +and `{{_ansi:reset}}`. Here's a list of all the ansi stuff you can use: + +* `bold`: makes text bold +* `italics`: makes text italics +* `underline`: makes text underlined +* `inverse`: inverts foreground and background +* `strikethrough`: strikethrough text +* `bold_off`, `italics_off`, `underline_off`, `inverse_off`, and + `strikethrough_off`: turn off the specified effect. +* `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, + and `default`: change the foreground color of your text. +* `black_bg`, `red_bg`, `green_bg`, `yellow_bg`, `blue_bg`, `magenta_bg`, + `cyan_bg`, `white_bg`, and `default_bg`: change the background color of your + text. +* `reset`: makes everything normal again. +* `_level`: this is a special code that will set a color depending on + the level of the log: debug gets green, info and notice blue, warn is + yellow, and anything worse is red. + +### More Formatters? + +You can create your own formatters by creating an object that responds +to the `#format()` method, knows how to handle cologne log objects and +returns a string. + +Here's an example of a logger that surrounds a log with sparkles: ```javascript -// A valid, very minimalistic formatter var simpleFormatter = { - format : function (logObject, options) { - if (options.showDate) { - return "[" + Date(logObject._timeStamp) + "] " + logObject.message - } else { - return logObject.message; - } + format : function (logObject) { + return '✨' + logObject.message + '✨'; } } -logger.addLogger(new Cobalt.Logger.JsConsole({ - formatter: simpleFormatter, - formatterOpts : { - showDate : true - } +logger.addLogger(new Cologne.Logger.Console({ + formatter: simpleFormatter })); ``` -## The Cobalt Log Format ## +## The Cologne Log Format + +The cologne log format is a JSON based log format, based on the cobalt +log format, which is inturn based on Graylog's GELF. However, where GELF +treats all internal fields without a prefix, and all user fields with a +prefix we do it backwards so it's easier to extend the object with +metadata from existing objects. Besides, we'll probably write the +default keys automatically so you shouldn't have to do that extra work. -The Cobalt Log (CoLog) format is a JSON based log format used with cobalt. -It is partly inspired in Greylog's GELF format, but with very notorious -differences. The CoLog requires a header with certain fields that allow -cobalt and its pieces to handle it. All header fields are prefixed with -an underscore. Other than those fields, you can put whatever you want in -the object; It's up to the loggers to make sense of the structure and -display it in a way that makes sense. +You could try to build it on your own, but you can use `#buildLog()` +to build it without logging. -You can attempt to build this structure on your own, or let cobalt build it for -you. Any object you pass for logging will be converted. However, if you -build it on your own you have two options: The first one is use buildLog -to create a log object for "item" as if you had logged "item" or you can -use extendLog that will create a dummy log object and extends it with -whatever object you pass to it. +### Fields -### Required Fields ### +* **\_timestamp** : A timestamp in miliseconds with fractions of a second + in the floating point area. +* **\_cologneLog** : This is how we know if the log is already + formatted and ready to go. This field is a string containing the + version of cologne log format it's using. It's `1.0.0` right now. +* **\_from**: The sender of the log (Defaults to Generic Cologne Logger) +* **\_level**: The level of the log (Defaults to 6) +* **\_levelString**: The string corresponding to the log level (e.g. 7 -> + debug, 3 -> error, 0 -> emerg) -* **_version** : The version of cobalt this is designed to work with -* **_timestamp** : A timestamp in microseconds. -* **_cobaltLog** [true] : Cobalt will check for the \_cobaltLog to decide if -transformation will happen or not. +### A word on Log Levels -### Optional Fields ### +The log levels in cologne correspond to the syslog levels, and the +levelStrings correspond to the priority keywords: -* **\_from**: The sender of the log (Defaults to Generic Cobalt Logger) -* **\_level**: The level of the log (Defaults to 7) -* **\_levelString**: The string corresponding to the log level (e.g. 7 -> - DEBUG, 3 -> ERROR, 0 -> CRIT) -* **\_indentLevel**: The indent level of the log -* **\_color**: The color of the log -* **\_separator**: If true, indicates that this is a separator and holds no - valuable information. +* `0 -> emerg` +* `1 -> alert` +* `2 -> crit` +* `3 -> error` +* `4 -> warning` +* `5 -> notice` +* `6 -> info` +* `7 -> debug` + +This is useful when deciding how to log. You could even have a logger +filter out unnecessary levels (eg. If you have a reporting logger that +only reports error or worse.) + +## Further Improvements + +* Improve the API for buildLog +* More loggers & formatters (will not be distributed in core cologne) +* Improve tests + +[dependencies-shield]: https://david-dm.org/rbdr/cologne.svg +[circle-ci-shield]: https://circleci.com/gh/rbdr/cologne.svg?style=shield diff --git a/bower.json b/bower.json deleted file mode 100644 index 8f33b28..0000000 --- a/bower.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "cobalt-log", - "version": "1.1.0", - "homepage": "https://github.com/rbdr/cobalt", - "authors": [ - "Ben Beltran " - ], - "description": "Logger + multiplexer for JSON based logs", - "main": "lib/cobalt.js", - "keywords": [ - "log" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ], - "dependencies": { - "neon" : "*" - } -} diff --git a/config/jsdoc.json b/config/jsdoc.json new file mode 100644 index 0000000..1ad13c4 --- /dev/null +++ b/config/jsdoc.json @@ -0,0 +1,9 @@ +{ + "plugins": ["plugins/markdown"], + "opts": { + "destination": "doc", + "readme": "README.md", + "template": "node_modules/jsdoc-augmented-template", + "recurse": true + } +} diff --git a/lib/cobalt.js b/lib/cobalt.js deleted file mode 100644 index b5dd94c..0000000 --- a/lib/cobalt.js +++ /dev/null @@ -1,320 +0,0 @@ -// Load up dependencies -if (typeof require === 'function') { - require('neon'); - var colors = require('colors'); - var Microtime = require('microtime'); -} - -Cobalt = Module("Cobalt"); -Module(Cobalt, 'Logger')({}); -Module(Cobalt, 'Formatter')({}); - -// Load up loggers + formatters -if (typeof require === 'function') { - // Formatters - require('./formatters/token.js'); - require('./formatters/simple.js'); - - // Loggers - require('./loggers/console.js'); - require('./loggers/socket.js'); - require('./loggers/file.js'); -} - -Cobalt.now = function () { - if (typeof performance !== 'undefined' && performance.timing) { - return performance.timing.navigationStart + performance.now(); - } - - if (typeof Microtime !== 'undefined') { - return Microtime.nowDouble() * 1000; - } - - return Date.now(); -} - -// Stringify with circular dereference. -Cobalt.stringify = function (object) { - var cache = [], stringified; - stringified = JSON.stringify(object, function (key, value) { - if (typeof value === 'object' && value !== null) { - if (cache.indexOf(value) !== -1) { - return "[Circular]"; - } - cache.push(value); - } - return value; - }); - cache = null; - - return stringified; -} - -Class(Cobalt, 'Console')({ - prototype : { - from : "Generic Cobalt Logger", - version : "0.1.0", - currentIndent : 0, - indentSize : 2, - loggers : [], - separatorLength : 120, - currentColor : "black", - - // Initialize instance of cobalt console - // and extend configuration. - init : function (config) { - var co = this, - property; - - if (config) { - for (property in config) { - co[property] = config[property]; - } - } - }, - - addLogger : function (logger) { - this.loggers.push(logger); - }, - - removeLogger : function (logger) { - var index; - - index = this.loggers.indexOf(logger); - this.loggers.splice(index, 1); - }, - - // Builds a Cobalt Log Object - buildLog : function (item, level) { - var co = this, oldItem, logObject = {}; - - if (typeof item === "undefined" || item === null || !item._cobaltLog) { - logObject.message = item; - logObject._cobaltLog = true; - logObject._from = co.from; - logObject._level = level || 6; - logObject._levelString = co._levelString(logObject._level); - logObject._version = co.version; - logObject._timestamp = co.now(); - logObject._indentLevel = co.currentIndent; - logObject._color = co.currentColor; - logObject._separator = false; - return logObject; - } - - if (item._cobaltLog) { - item._level = level || item._level || 6; - item._levelString = co._levelString(item._level); - } - - return item; - }, - - extendLog : function (extendingObject) { - var co = this, logObject, - property; - - logObject = co.buildLog(undefined, 6); - extendingObject = extendingObject || {}; - - for (property in extendingObject) { - if (extendingObject.hasOwnProperty(property)) { - logObject[property] = extendingObject[property]; - } - } - - return logObject; - }, - - buildSeparator : function (type) { - var co = this; - return { - _cobaltLog : true, - _separator : true, - _version : co.version, - _timestamp : co.now(), - _separatorType : type, - _indentLevel : co.currentIndent, - _color : co.currentColor - } - }, - - _log : function (severity) { - var co = this, - logString, - logObjectArray = [], - i, j; - - for (i = 1; i < arguments.length; i++) { - if (typeof arguments[i] === 'undefined') { - logObjectArray.push(co.buildLog("undefined", severity)); - } else if (arguments[i] === null) { - logObjectArray.push(co.buildLog("null", severity)); - } else { - logObjectArray.push(co.buildLog(arguments[i], severity)); - } - } - - for (j = 0; j < co.loggers.length; j++) { - co.loggers[j].log.apply(co.loggers[j], logObjectArray); - } - }, - - log : function () { - this._log.apply(this, [6].concat(Array.prototype.slice.call(arguments))); - }, - - debug : function () { - this._log.apply(this, [7].concat(Array.prototype.slice.call(arguments))); - }, - - info : function () { - this._log.apply(this, [6].concat(Array.prototype.slice.call(arguments))); - }, - - notice : function () { - this._log.apply(this, [5].concat(Array.prototype.slice.call(arguments))); - }, - - warn : function () { - this._log.apply(this, [4].concat(Array.prototype.slice.call(arguments))); - }, - - error : function () { - this._log.apply(this, [3].concat(Array.prototype.slice.call(arguments))); - }, - - dir : function () { - }, - - time : function () { - }, - - timeEnd : function () { - }, - - groupCollapsed : function () { - }, - - groupEnd : function () { - }, - - separator : function (type) { - var co = this; - - co._log(7, co.buildSeparator(type)); - }, - - space : function (lines) { - var co = this, - i; - - if (typeof lines === "undefined") { - lines = 1; - } - - for (i = 0; i < lines; i++) { - co.log(' '); - } - - return co; - }, - - indent : function (callback) { - var co = this; - - if (typeof callback === "function") { - co.currentIndent = co.currentIndent + co.indentSize; - callback(); - co.currentIndent = co.currentIndent - co.indentSize; - } else { - co.currentIndent = co.currentIndent + co.indentSize; - } - - return co; - }, - - outdent : function (callback) { - var co = this; - - if (typeof callback === "function") { - co.currentIndent = co.currentIndent - co.indentSize; - if (co.currentIndent < 0) { - co.currentIndent = 0; - } - - callback(); - - co.currentIndent = co.currentIndent + co.indentSize; - } else { - co.currentIndent = co.currentIndent - co.indentSize; - if (co.currentIndent < 0) { - co.currentIndent = 0; - } - } - - return co; - }, - - color : function (color, callback) { - var co = this, - oldColor = co.currentColor; - - if (typeof callback === "function") { - co.currentColor = color; - callback(); - co.currentColor = oldColor; - } else { - co.currentColor = color; - } - - return co; - }, - - // Returns the current time in microseconds. - now : function () { - if (typeof performance !== 'undefined' && performance.timing) { - return performance.timing.navigationStart + performance.now(); - } - - if (typeof Microtime !== 'undefined') { - return Microtime.nowDouble() * 1000; - } - - return Date.now(); - }, - - _levelString : function (level) { - switch(level) { - case 0: - return "PANIC"; - break; - case 1: - return "ALERT" - break; - case 2: - return "CRIT" - break; - case 3: - return "ERROR" - break; - case 4: - return "WARN" - break; - case 5: - return "NOTICE" - break; - case 6: - return "INFO" - break; - case 7: - return "DEBUG" - break; - } - } - } -}); - -if (Cobalt.Console.__objectSpy) { - Cobalt.Console.__objectSpy.destroy(); -} diff --git a/lib/cologne.js b/lib/cologne.js new file mode 100644 index 0000000..646fe79 --- /dev/null +++ b/lib/cologne.js @@ -0,0 +1,328 @@ +'use strict'; + +let LogUtilities = require('./cologne/log_utilities'); + +/** TYPE DEFINITIONS **/ + +/** + * Main interface for Cologne Loggers + * + * @memberof Cologne + * @interface ILogger + */ + +/** + * Receives any number of cologne log objects and logs them. + * + * @memberof Cologne.ILogger + * @function + * @name log + * @returns {undefined} + */ + +/** + * Main interface for Cologne Formatters + * + * @memberof Cologne + * @interface IFormatter + */ + +/** + * Receives a cologne log object and returns a formatted string. + * + * @memberof Cologne.IFormatter + * @function + * @name format + * @param {Cologne.tCologneLog} logObject the log to be formatted + * @returns {string} the formatted log + */ + +/** + * The main cologne log format. + * + * @memberof Cologne + * @typedef {object} tCologneLog + * @property {Number} _timestamp the timestamp in miliseconds with decimal + * numbers representing fractions of miliseconds + * @property {String} _cologneLog main identifier, encodes the version of the + * cologne log format being used. + * @property {String} _from the origin of the log message. + * @property {String} _level the severity level of the log, uses syslog + * priorities. + * @property {String} _levelString the severity level keyword of the log, + * uses syslog priority keywords. + */ + +/** + * Cologne is a logger multiplexer that works mainly with a JSON format. It + * can be instantiated with several loggers, or they can be changed after + * the fact. + * + * ## Usage + * + * ``` + * require('cologne'); + * + * let co = new Cologne({ + * from: "Special Worker Logger", + * loggers: [ + * new Cologne.Logger.Console({ + * formatter: new Cologne.Formatter.Token({ + * formatString: '[{{_timestamp}}]{{_from}}: {{message}}' + * }) + * }) + * ] + * }); + * ``` + * + * @class Cologne + */ +let Cologne = class Cologne { + + constructor (config) { + + /** + * The name of this logger, useful to distinguish between different + * loggers. + * + * @name from + * @instance + * @memberof Cologne + * @type String + * @default 'Generic Cologne Logger + */ + this.from = 'Generic Cologne Logger'; + + /** + * The array containing all the loggers it will call to. + * + * @name loggers + * @instance + * @memberof Cologne + * @type Cologne.ILogger[] + * @default [] + */ + this.loggers = []; + + Object.assign(this, config || {}); + } + + /** + * Adds a logger to the current instance. + * + * @function addLogger + * @instance + * @memberof Cologne + * @param {Cologne.ILogger} logger the logger to add + * @return {undefined} + */ + addLogger (logger) { + this.loggers.push(logger); + } + + /** + * Removes a logger from the current instance. + * + * @function removeLogger + * @instance + * @memberof Cologne + * @param {Cologne.ILogger} logger the logger to remove + * @return {Cologne.ILogger[]} the removed log, inside an array. + */ + removeLogger (logger) { + let index; + + index = this.loggers.indexOf(logger); + if (index >= 0) { + this.loggers.splice(index, 1); + } + } + + /** + * Given an item, it builds a cologne log object. this is done + * automatically by the logger, though this is useful if you need + * to attach metadata before logging. + * + * @function buildLog + * @instance + * @memberof Cologne + * @param {*} item The item to log + * @return {Cologne.tCologneLog} a cologne log object + */ + buildLog (item, level, meta) { + let logObject; + + logObject = {}; + + if (typeof item === 'undefined' || item === null || !item._cologneLog) { + logObject.message = item; + logObject._cologneLog = this.constructor._version; + logObject._from = this.from; + logObject._level = level || 6; + logObject._levelString = this._levelString(logObject._level); + logObject._timestamp = LogUtilities.now(); + + if (meta && typeof meta === 'object') { + Object.assign(logObject, meta); + } + + return logObject; + } + + if (item._cologneLog) { + item._level = level || item._level || 6; + item._levelString = this._levelString(item._level); + } + + return item; + } + + /** + * Default log function. Sends arguments to loggers. If not specified in log + * object, it will set the severity to 6 - INFO. + * + * @function log + * @instance + * @memberof Cologne + * @return {undefined} + */ + log () { + this._log.apply(this, [null].concat(Array.prototype.slice.call(arguments))); + } + + /** + * Logs with debug level + * + * @function debug + * @instance + * @memberof Cologne + * @return {undefined} + */ + debug () { + this._log.apply(this, [7].concat(Array.prototype.slice.call(arguments))); + } + + /** + * Logs with info level + * + * @function info + * @instance + * @memberof Cologne + * @return {undefined} + */ + info () { + this._log.apply(this, [6].concat(Array.prototype.slice.call(arguments))); + } + + /** + * Logs with notice level + * + * @function notice + * @instance + * @memberof Cologne + * @return {undefined} + */ + notice () { + this._log.apply(this, [5].concat(Array.prototype.slice.call(arguments))); + } + + /** + * Logs with warn level + * + * @function warn + * @instance + * @memberof Cologne + * @return {undefined} + */ + warn () { + this._log.apply(this, [4].concat(Array.prototype.slice.call(arguments))); + } + + /** + * Logs with error level + * + * @function error + * @instance + * @memberof Cologne + * @return {undefined} + */ + error () { + this._log.apply(this, [3].concat(Array.prototype.slice.call(arguments))); + } + + // Private method that builds all the logs and sends them to the loggers. + _log (severity) { + let remainingArguments, logObjectArray, log, logger; + + remainingArguments = Array.prototype.slice.call(arguments, 1); + logObjectArray = []; + + for (log of remainingArguments) { + if (typeof log === 'undefined') { + logObjectArray.push(this.buildLog('undefined', severity)); + continue; + } + + if (log === null) { + logObjectArray.push(this.buildLog('null', severity)); + continue; + } + + logObjectArray.push(this.buildLog(log, severity)); + } + + for (logger of this.loggers) { + logger.log.apply(logger, logObjectArray); + } + } + + // Private utility method that will return the string for any given + // numerical severity level + _levelString (level) { + switch(level) { + case 0: + return 'emerg'; + case 1: + return 'alert'; + case 2: + return 'crit'; + case 3: + return 'error'; + case 4: + return 'warning'; + case 5: + return 'notice'; + case 6: + return 'info'; + case 7: + return 'debug'; + } + } +}; + +// Version of the Cologne Log Format used. +Cologne._version = '1.0.0'; + +/** + * Namespace that includes the built-in formatters. + * + * @namespace Formatter + * @memberof Cologne + */ +Cologne.Formatter = {}; +Cologne.Formatter.Simple = require('./cologne/formatter/simple'); +Cologne.Formatter.Token = require('./cologne/formatter/token'); + +/** + * Namespace that includes the built-in loggers. + * + * @namespace Logger + * @memberof Cologne + */ +Cologne.Logger = {}; +Cologne.Logger.Console = require('./cologne/logger/console'); +Cologne.Logger.File = require('./cologne/logger/file'); + +Cologne.LogUtilities = require('./cologne/log_utilities'); + +module.exports = Cologne; diff --git a/lib/cologne/formatter/simple.js b/lib/cologne/formatter/simple.js new file mode 100644 index 0000000..48821fc --- /dev/null +++ b/lib/cologne/formatter/simple.js @@ -0,0 +1,67 @@ +'use strict'; + +let LogUtilities = require('../log_utilities'); + +/** + * Simple formatter. Outputs a predefined format: + * `[{{_timestamp}}][{{_levelString}}] {{_from}}: {{message}}`; + * + * @memberof Cologne.Formatter + * @implements Cologne.IFormatter + * @class Simple + */ +let SimpleFormatter = class SimpleFormatter { + + constructor (config) { + + /** + * Flag that tells us whether or not to use ANSI color. Defaults to + * false. + * + * @name colorize + * @instance + * @memberof Cologne.Formatter.Simple + * @type Boolean + * @default false + */ + this.colorize = false; + + Object.assign(this, config || {}); + } + + /** + * Main entry point, it will read the incoming log object and convert + * it to the output string. + * + * @function format + * @instance + * @memberof Cologne.Formatter.Simple + * @param {Cologne.tCologneLog} logObjet the log to format + * @return {String} the formatted object + */ + format (logObject) { + let date, levelString; + + date = new Date(logObject._timestamp); + date = date.toISOString(); + levelString = this._colorize(logObject._levelString, logObject._level); + + return `[${date}][${levelString}] ${logObject._from}: ${logObject.message}`; + } + + _colorize (levelString, level) { + let escapeCode, color, reset; + + if (!this.colorize) { + return levelString; + } + + escapeCode = String.fromCharCode(27); + color = escapeCode + LogUtilities.getAnsiCode(LogUtilities.getLevelAnsi(level)); + reset = escapeCode + LogUtilities.getAnsiCode('reset'); + + return color + levelString + reset; + } +}; + +module.exports = SimpleFormatter; diff --git a/lib/cologne/formatter/token.js b/lib/cologne/formatter/token.js new file mode 100644 index 0000000..bb905ca --- /dev/null +++ b/lib/cologne/formatter/token.js @@ -0,0 +1,104 @@ +'use strict'; + +let LogUtilities = require('../log_utilities'); + +/** + * Token formatter. Given a format string it will attempt to output + * a message. + * + * @memberof Cologne.Formatter + * @implements Cologne.IFormatter + * @class Token + */ +let TokenFormatter = class TokenFormatter { + constructor (config) { + + /** + * The string to use as a template string. By default, any property + * inside double curly braces `{{likeThis}}` will be extracted from + * the object and replaced. If the object does not contain the + * property, it will leave it. + * + * @name formatString + * @instance + * @memberof Cologne.Formatter.Token + * @type String + * @default '{{message}}' + */ + this.formatString = '{{message}}'; + + /** + * The regex rule to use to match the tokens. + * + * @name replaceRule + * @instance + * @memberof Cologne.Formatter.Token + * @type RegExp + * @default /{{(.*)}}/g + */ + this.replaceRule = /{{(.*?)}}/g; + + /** + * Flag that specifies whether or not to use an isoDate when using + * `_timestamp`. If false it will output the raw timestamp. + * + * @name isoDate + * @instance + * @memberof Cologne.Formatter.Token + * @type Boolean + * @default true + */ + this.isoDate = true; + + this._ansiRe = /_ansi:.+/; + + Object.assign(this, config || {}); + } + + /** + * Main entry point, it will read the incoming log object and convert + * all the tokens to their corresponding representation, finally + * returning the string. + * + * @function format + * @instance + * @memberof Cologne.Formatter.Token + * @param {Cologne.tCologneLog} logObjet the log to format + * @return {String} the formatted object + */ + format (logObject) { + let resultString, escapeCode; + + escapeCode = String.fromCharCode(27); + + resultString = this.formatString.replace(this.replaceRule, function (match, token) { + let date, ansiType; + + if (token === '_timestamp' && this.isoDate) { + date = new Date(logObject._timestamp); + return date.toISOString(); + } + + if (token.match(this._ansiRe)) { + ansiType = token.split(':')[1]; + + // Smartish coloring + if (ansiType === '_level') { + return escapeCode + LogUtilities.getAnsiCode(LogUtilities.getLevelAnsi(logObject._level)); + } + + return escapeCode + LogUtilities.getAnsiCode(ansiType); + } + + if (!logObject.hasOwnProperty(token)) { + return match; + } + + return logObject[token]; + }.bind(this)); + + return resultString; + } +}; + +module.exports = TokenFormatter; diff --git a/lib/cologne/log_utilities.js b/lib/cologne/log_utilities.js new file mode 100644 index 0000000..fc15a8a --- /dev/null +++ b/lib/cologne/log_utilities.js @@ -0,0 +1,155 @@ +'use strict'; + +let microtime = require('microtime'); + +/** + * Container object for utilities used by loggers. + * + * @memberof Cologne + * @class LogUtilities + */ +let LogUtilities = { + + /** + * Returns the current timestamp in miliseconds as a floating point that + * includes fractions (ie. microseconds) + * + * @function now + * @memberof Cologne.LogUtilities + * @return {Number} current time in miliseconds, including fractions. + */ + now: function now() { + return microtime.nowDouble() * 1000; + }, + + /** + * Stringifies objects, avoiding circular references. + * + * @function stringify + * @memberof Cologne.LogUtilities + * @param {Object} object the object to stringify + * @return {String} the stringified object + */ + stringify: function stringify(object) { + let cache; + + cache = []; + + return JSON.stringify(object, function (key, value) { + if (typeof value === 'object' && value !== null) { + if (cache.indexOf(value) !== -1) { + return this._circularString; + } + + cache.push(value); + } + + return value; + }.bind(this)); + }, + + /** + * Given an ansi keyword, it will return the appropriate code. + * + * @function getAnsiCode + * @memberof Cologne.LogUtilities + * @param {String} ansiString the name of the desired code + * @return {String} the ansi code + */ + getAnsiCode: function getAnsiCode(ansiString) { + switch(ansiString) { + case 'bold': + return '[1m'; + case 'italics': + return '[3m'; + case 'underline': + return '[4m'; + case 'inverse': + return '[7m'; + case 'strikethrough': + return '[9m'; + case 'bold_off': + return '[22m'; + case 'italics_off': + return '[23m'; + case 'underline_off': + return '[24m'; + case 'inverse_off': + return '[27m'; + case 'strikethrough_off': + return '[29m'; + case 'black': + return '[30m'; + case 'red': + return '[31m'; + case 'green': + return '[32m'; + case 'yellow': + return '[33m'; + case 'blue': + return '[34m'; + case 'magenta': + return '[35m'; + case 'cyan': + return '[36m'; + case 'white': + return '[37m'; + case 'default': + return '[39m'; + case 'black_bg': + return '[40m'; + case 'red_bg': + return '[41m'; + case 'green_bg': + return '[42m'; + case 'yellow_bg': + return '[43m'; + case 'blue_bg': + return '[44m'; + case 'magenta_bg': + return '[45m'; + case 'cyan_bg': + return '[46m'; + case 'white_bg': + return '[47m'; + case 'default_bg': + return '[49m'; + case 'reset': // for informative purpouses + default: + return '[0m'; + } + }, + + /** + * Given a level, it will return the appropriate ansi keyword related + * to it. + * + * @function getLevelAnsi + * @memberof Cologne.LogUtilities + * @param {number} level the level of the log + * @return {String} the ansi keyword + */ + getLevelAnsi: function getLevelAnsi(level) { + switch(level) { + case 0: + case 1: + case 2: + case 3: + return 'red'; + case 4: + return 'yellow'; + case 5: + case 6: + return 'blue'; + case 7: + return 'green'; + default: + return 'default'; + } + } +}; + +// String used as default circular reference. +LogUtilities._circularString = '[Circular]'; + +module.exports = LogUtilities; diff --git a/lib/cologne/logger/console.js b/lib/cologne/logger/console.js new file mode 100644 index 0000000..3a73e42 --- /dev/null +++ b/lib/cologne/logger/console.js @@ -0,0 +1,95 @@ +'use strict'; + +let LogUtilities = require('../log_utilities'); + +/** + * Logger for the javascript console. + * + * @memberof Cologne.Logger + * @implements Cologne.ILogger + * @class Console + */ +let ConsoleLogger = class ConsoleLogger { + constructor (config) { + + /** + * The console it will write to, can be any object that looks + * and acts like a console, including other cologne objects. + * + * @name console + * @instance + * @memberof Cologne.Logger.Console + * @type Object + * @default global.console + */ + this.console = console; + + /** + * The formatter it will use to output the log. If not present it + * will output raw JSON + * + * @name formatter + * @instance + * @memberof Cologne.Logger.Console + * @type Cologne.IFormatter + * @default null + */ + this.formatter = null; + + Object.assign(this, config || {}); + } + + + /** + * Main entry point, for each incoming argument it will attempt to + * format and send to the console. + * + * @function log + * @instance + * @memberof Cologne.Logger.Console + * @return {undefined} + */ + log () { + let logObject, messages, severity; + + messages = []; + + for (logObject of arguments) { + messages.push(this._format(logObject)); + + if (!severity) { + severity = logObject._level; + } + } + + switch(severity) { + case 0: + case 1: + case 2: + case 3: + this.console.error.apply(this.console, messages); + break; + case 4: + this.console.warn.apply(this.console, messages); + break; + case 5: + case 6: + this.console.info.apply(this.console, messages); + break; + case 7: + default: + this.console.log.apply(this.console, messages); + break; + } + } + + _format (logObject) { + if (this.formatter) { + return this.formatter.format(logObject); + } + + return LogUtilities.stringify(logObject); + } +}; + +module.exports = ConsoleLogger; diff --git a/lib/cologne/logger/file.js b/lib/cologne/logger/file.js new file mode 100644 index 0000000..e9a56e7 --- /dev/null +++ b/lib/cologne/logger/file.js @@ -0,0 +1,71 @@ +'use strict'; + +let fs = require('fs'); + +let LogUtilities = require('../log_utilities'); + +/** + * Logger for files. + * + * @memberof Cologne.Logger + * @implements Cologne.ILogger + * @class File + */ +let FileLogger = class FileLogger { + constructor (config) { + + /** + * Path to the file it will write to, must be readable. + * + * @name file + * @instance + * @memberof Cologne.Logger.File + * @type string + * @default null + */ + this.file = null; + + /** + * The formatter it will use to output the log. If not present it + * will output raw JSON + * + * @name formatter + * @instance + * @memberof Cologne.Logger.File + * @type Cologne.IFormatter + * @default null + */ + this.formatter = null; + + Object.assign(this, config || {}); + + this._stream = fs.createWriteStream(this.file, {flags: 'a'}); + } + + /** + * Main entry point, for each incoming argument it will attempt to + * format and send to the stream to be written. + * + * @function log + * @instance + * @memberof Cologne.Logger.File + * @return {undefined} + */ + log () { + let logObject; + + for (logObject of arguments) { + this._stream.write(this.format(logObject) + '\n'); + } + } + + format (logObject) { + if (this.formatter) { + return this.formatter.format(logObject); + } + + return LogUtilities.stringify(logObject); + } +}; + +module.exports = FileLogger; diff --git a/lib/ext/socket_helper.js b/lib/ext/socket_helper.js deleted file mode 100644 index 50fe5bc..0000000 --- a/lib/ext/socket_helper.js +++ /dev/null @@ -1,7 +0,0 @@ -var bindEvents = function (socket, logger) { - socket.on('log', function (logArgs) { - logger.log.apply(logger, logArgs); - }); -} - -exports.bindEvents = bindEvents; diff --git a/lib/formatters/ansi.js b/lib/formatters/ansi.js deleted file mode 100644 index 806869a..0000000 --- a/lib/formatters/ansi.js +++ /dev/null @@ -1,29 +0,0 @@ -if (typeof require === 'function') { - require('colors'); -} - -Module(Cobalt.Formatter, 'Ansi')({ - format : function (logObject, opts){ - var indent, - message; - - indent = Array(logObject._indentLevel + 1).join(' '); - - message = indent + logObject.message; - - switch(logObject._level) { - case 0: - case 1: - case 2: - case 3: - return message.red; - case 4: - return message.yellow; - case 5: - case 6: - return message.blue; - default: - return message; - } - } -}); diff --git a/lib/formatters/simple.js b/lib/formatters/simple.js deleted file mode 100644 index 9eb6bbe..0000000 --- a/lib/formatters/simple.js +++ /dev/null @@ -1,11 +0,0 @@ -Module(Cobalt.Formatter, 'Simple')({ - format : function (logObject, opts){ - var indent, date; - - indent = Array(logObject._indentLevel + 1).join(' '); - - date = new Date(logObject._timestamp); - - return indent + '[' + date.toISOString() + '][' + logObject._levelString + '] ' + logObject._from + ' : ' + logObject.message; - } -}); diff --git a/lib/formatters/token.js b/lib/formatters/token.js deleted file mode 100644 index f3afba2..0000000 --- a/lib/formatters/token.js +++ /dev/null @@ -1,73 +0,0 @@ -Module(Cobalt.Formatter, 'Token')({ - formatString : "{{message}}", - replaceRule : /{{(.*?)}}/g, - separatorLength : 60, - isoDate : true, - separatorType : "-", - format : function (logObject, opts){ - var indent, indentSize, - separatorLength, separatorType, - output, property; - indentSize = logObject._indentLevel || 0; - - // Extend opts - if (opts) { - for (property in opts) { - if (opts.hasOwnProperty(property)) { - this[property] = opts[property]; - } - } - } - - indent = Array(indentSize + 1).join(' '); - - if (logObject._separator) { - separatorLength = logObject._separatorLength || this.separatorLength; - separatorType = logObject._separatorType || this.separatorType; - output = indent + Array(separatorLength - indentSize + 1).join(separatorType); - } else { - output = indent + this.parseFormatString(logObject, this.formatString); - } - - if (this.ansiColor) { - output = this.colorize(logObject._level, output); - } - - return output; - }, - - parseFormatString : function (logObject, formatString) { - var resultString = ''; - if (typeof formatString === 'undefined') { - formatString = this.formatString; - } - - resultString = formatString.replace(this.replaceRule, function(match, paren){ - var date; - if (paren === "_timestamp" && this.isoDate) { - date = new Date(logObject[paren]); - return date.toISOString(); - } - return logObject[paren] || "-"; - }.bind(this)); - - return resultString; - }, - - colorize : function (level, message) { - switch(level) { - case 0: - case 1: - case 2: - case 3: - return message.red; - case 4: - return message.yellow; - case 5: - case 6: - return message.blue; - default: - return message; - } - } -}); diff --git a/lib/loggers/console.js b/lib/loggers/console.js deleted file mode 100644 index c4031eb..0000000 --- a/lib/loggers/console.js +++ /dev/null @@ -1,70 +0,0 @@ -Class(Cobalt.Logger, 'JsConsole')({ - prototype : { - console : null, - formatterOpts : {}, - - init : function (config) { - var logger = this, - property; - - if (config) { - for (property in config) { - logger[property] = config[property]; - } - } - - if (!logger.console) { - logger.console = console; - } - }, - - log : function () { - var i, message = [], severity; - - for (i = 0; i < arguments.length; i++) { - // We're not formatting objects for now. - - if (!arguments[i].__skipConsole && !arguments[i].message.__skipConsole) { - if (typeof arguments[i].message === 'object') { - message.push(arguments[i].message); - } else { - message.push(this.format(arguments[i])); - } - if (!severity) { - severity = arguments[i]._level - } - } - } - - switch (severity){ - case 0: - case 1: - case 2: - case 3: - this.console.error.apply(this.console, message); - break; - case 4: - this.console.warn.apply(this.console, message); - break; - case 5: - case 6: - this.console.info.apply(this.console, message); - break; - case 7: - default: - this.console.log.apply(this.console, message); - break; - } - }, - - format : function (logObject) { - // Usually what you want to do here is format. Preferably using - // someone inside Cobalt.Formatter - if (this.formatter) { - return this.formatter.format(logObject, this.formatterOpts); - } - - return logObject.message; - } - } -}); diff --git a/lib/loggers/file.js b/lib/loggers/file.js deleted file mode 100644 index 0d60acd..0000000 --- a/lib/loggers/file.js +++ /dev/null @@ -1,48 +0,0 @@ -var fs = require('fs'); - -Class(Cobalt.Logger, 'File')({ - prototype : { - file : null, - formatterOpts : {}, - - init : function (config) { - if (config) { - for (property in config) { - this[property] = config[property]; - } - } - - this._stream = fs.createWriteStream(this.file, {flags: 'a'}); - }, - - log : function () { - var i, message = [], severity; - - for (i = 0; i < arguments.length; i++) { - // We're not formatting objects for now. - - if (!arguments[i].__skipConsole && !arguments[i].message.__skipConsole) { - message.push(this.format(arguments[i])); - if (!severity) { - severity = arguments[i]._level - } - } - } - - for (i = 0; i < message.length; i++) { - this._stream.write(message[i] + '\n'); - } - }, - - format : function (logObject) { - if (this.formatter) { - if (typeof logObject.message === 'object') { - return logObject.message; - } - return this.formatter.format(logObject, this.formatterOpts); - } - - return Cobalt.stringify(logObject); - } - } -}); diff --git a/lib/loggers/socket.js b/lib/loggers/socket.js deleted file mode 100644 index 7066274..0000000 --- a/lib/loggers/socket.js +++ /dev/null @@ -1,37 +0,0 @@ -if (typeof require === 'function') { - var ioClient = require('socket.io-client'); -} - -Class(Cobalt.Logger, 'Socket')({ - prototype : { - serverUrl : '/', - - init : function (config) { - var logger = this; - - if (config) { - for (property in config) { - logger[property] = config[property]; - } - } - - if (!logger.socketIo) { - logger.socketIo = ioClient; - } - - logger._socket = logger.socketIo.connect(logger.serverUrl); - }, - - log : function () { - var i, messageArray = []; - - for (i = 0; i < arguments.length; i++) { - messageArray.push(arguments[i]); - } - - if (this._socket) { - this._socket.emit('log', messageArray); - } - } - } -}); diff --git a/package.json b/package.json index f5ea0a9..6f7b73d 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,29 @@ { - "name": "cobalt-log", - "description": "Logger + multiplexer for JSON based logs, based on Cobalt ruby gem by ktlacaelel (http://rubygems.org/gems/cobalt)", + "name": "cologne", + "description": "Logger + multiplexer for JSON based logs", "author": { "name": "Ben Beltran", "email": "ben@nsovocal.com", - "url": "http://nsovocal.com" + "url": "http://unlimited.pizza" }, "repository": { "type": "git", - "url": "https://github.com/rbdr/cobalt.git" + "url": "https://github.com/rbdr/cologne.git" }, - "contributors": [ - { - "name": "Kazuyoshi Tlacaelel", - "email": "kazu.dev@gmail.com", - "url": "http://github.com/ktlacaelel" - } - ], - "version": "1.1.3", + "version": "1.1.0", "dependencies": { - "colors": "^1.0.3", - "emailjs": "^0.3.12", - "microtime": "^1.2.0", - "neon": "^2.0.0", - "socket.io-client": "^1.2.1" + "microtime": "2.0.x" }, "devDependencies": { - "tellurium": "2.0.x" + "tap": "5.1.x", + "jsdoc-augmented-template": "rbdr/jsdoc-augmented-template" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 4.0.0" }, - "main": "./lib/cobalt.js" + "scripts": { + "test": "node_modules/tap/bin/run.js test/*.js test/**/*.js test/**/**/*.js", + "document": "jsdoc -c ./config/jsdoc.json lib" + }, + "main": "./lib/cologne.js" } diff --git a/test/basic.js b/test/basic.js deleted file mode 100644 index 90aadd6..0000000 --- a/test/basic.js +++ /dev/null @@ -1,35 +0,0 @@ -if (typeof require === "function") { - require("cobalt-log"); -} - -var co = new Cobalt.Console({ - loggers : [ - new Cobalt.Logger.JsConsole({ - formatter : Cobalt.Formatter.Token, - formatterOpts : { - formatString : "[{{_level}}] {{message}} {{customParam}}" - } - }) - ] -}) - -// TODO: Do this whole thing with tellurium. - -co.log("Log - Normal"); -co.debug("Warn - Normal"); -co.info("Info - Normal"); -co.notice("Notice - Normal"); -co.warn("Warn - Normal"); -co.error("Error - Normal"); - -var logObject = co.extendLog({ - message : "Extended Log Object", - customParam : "<3" -}); - -co.log(logObject); -co.debug(logObject); -co.info(logObject); -co.notice(logObject); -co.warn(logObject); -co.error(logObject); diff --git a/test/browser.html b/test/browser.html deleted file mode 100644 index ec58b6f..0000000 --- a/test/browser.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - Tellurium Test Runner - - - - - - - - - - - - - - - - diff --git a/test/cologne.js b/test/cologne.js new file mode 100644 index 0000000..f5a6c6e --- /dev/null +++ b/test/cologne.js @@ -0,0 +1,212 @@ +'use strict'; + +let tap = require('tap'); + +let Cologne = require('../lib/cologne'); + +let dummyLogger = { + values : null, + log : function () { + let logObject; + + this.values = []; + + for (logObject of arguments) { + this.values.push(logObject); + } + } +}; + +// Prepare the test +let dummyLoggerA, dummyLoggerB, dummyLoggerC, + co, params, builtLog, meta, + valueCheck, levelCheck; + +tap.plan(18); + +dummyLoggerA = Object.assign({}, dummyLogger); +dummyLoggerB = Object.assign({}, dummyLogger); +dummyLoggerC = Object.assign({}, dummyLogger); + +co = new Cologne({ + loggers : [ + dummyLoggerA, + dummyLoggerB + ] +}); + +meta = { + rainbows: true +}; + +params = ['example1', null, undefined, 1, {example: true}]; + +/** + * TEST: #log() + */ +co.log.apply(co, params); + +// Calculate values +valueCheck = dummyLoggerA.values.reduce(function (previous, current) { + if (typeof current._cologneLog === 'string') { + previous++; + } + return previous; +}, 0); +levelCheck = dummyLoggerA.values.reduce(function (previous, current) { + if (current._level === 6) { + previous++; + } + return previous; +}, 0); + +// Now check the values + +tap.equal(dummyLoggerA.values.length, params.length, + '#log() should send every argument to the loggers'); + +tap.similar(dummyLoggerA.values, dummyLoggerB.values, + '#log() should send the same arguments to all the loggers'); + +tap.equal(valueCheck, params.length, + '#log() should send all objects in cologne log format'); + +tap.equal(levelCheck, params.length, + '#log() should default to level 6'); + +/** + * TEST: #debug() + */ +co.debug.apply(co, params); +levelCheck = dummyLoggerA.values.reduce(function (previous, current) { + if (current._level === 7) { + previous++; + } + return previous; +}, 0); + +tap.equal(levelCheck, params.length, + '#debug() should set to level 7'); + +/** + * TEST: #info() + */ +co.info.apply(co, params); +levelCheck = dummyLoggerA.values.reduce(function (previous, current) { + if (current._level === 6) { + previous++; + } + return previous; +}, 0); + +tap.equal(levelCheck, params.length, + '#info() should set to level 6'); + +/** + * TEST: #notice() + */ +co.notice.apply(co, params); +levelCheck = dummyLoggerA.values.reduce(function (previous, current) { + if (current._level === 5) { + previous++; + } + return previous; +}, 0); + +tap.equal(levelCheck, params.length, + '#notice() should set to level 5'); + +/** + * TEST: #warn() + */ +co.warn.apply(co, params); +levelCheck = dummyLoggerA.values.reduce(function (previous, current) { + if (current._level === 4) { + previous++; + } + return previous; +}, 0); + +tap.equal(levelCheck, params.length, + '#warn() should set to level 4'); + +/** + * TEST: #error() + */ +co.error.apply(co, params); +levelCheck = dummyLoggerA.values.reduce(function (previous, current) { + if (current._level === 3) { + previous++; + } + return previous; +}, 0); + +tap.equal(levelCheck, params.length, + '#error() should set to level 3'); + +/** + * TEST: #buildLog() + */ +builtLog = co.buildLog('example'); + +// With the default level +tap.equal(typeof builtLog._cologneLog, 'string', + '#buildLog() should return a cologne log'); +tap.equal(builtLog._level, 6, + '#buildLog() should default to level 6'); + +// Now with a specific value +builtLog = co.buildLog('example', 1); + +tap.equal(builtLog._level, 1, + '#buildLog() should use the specified level'); + +// Now with meta +builtLog = co.buildLog('example', 1, meta); + +tap.equal(builtLog.rainbows, true, + '#buildLog() should extend the object with meta if available'); + +/** + * TEST: #log() with builtLog. + */ +co.log(builtLog); + +levelCheck = dummyLoggerA.values.reduce(function (previous, current) { + if (current._level === 1) { + previous++; + } + return previous; +}, 0); + +tap.equal(levelCheck, 1, + '#log() calls using a pre-built cologne log should maintain the log level'); + + +/** + * TEST: #removeLogger() + */ +co.removeLogger(dummyLoggerC); + +tap.equal(co.loggers.length, 2, + '#removeLogger() should do nothing if it can\'t find a logger'); + +co.log.apply(co, params); +co.removeLogger(dummyLoggerB); +co.log(1); + +tap.equal(co.loggers.length, 1, + '#removeLogger() should remove a logger'); + +tap.notEqual(dummyLoggerB.values.length, dummyLoggerA.values.length, + '#removeLogger() should no longer affect removed logs'); + +/** + * TEST: #addLogger() + */ + +co.addLogger(dummyLoggerC); +co.log.apply(co, params); + +tap.equal(dummyLoggerC.values.length, params.length, + '#addLogger() should add loggers after instance is live'); diff --git a/test/cologne/formatter/simple.js b/test/cologne/formatter/simple.js new file mode 100644 index 0000000..8642541 --- /dev/null +++ b/test/cologne/formatter/simple.js @@ -0,0 +1,73 @@ +'use strict'; + +let tap = require('tap'); + +let SimpleFormatter = require('../../../lib/cologne/formatter/simple'); + +// Prepare the test +let logObject, colorFormatter, plainFormatter, formattedString, isoDate; + +tap.plan(12); + +logObject = { + _timestamp: Date.now() + .134, + _cologneLog: '1.0.0', + _from: 'Dummy Logger', + _level: 3, + _levelString: 'error', + message: 'testing stuff!' +}; +isoDate = (new Date(logObject._timestamp)).toISOString(); + +plainFormatter = new SimpleFormatter(); +colorFormatter = new SimpleFormatter({ + colorize: true +}); + +/** + * TEST: #format() - plain + */ + +formattedString = plainFormatter.format(logObject); + +tap.equal(typeof formattedString, 'string', + '#format() should output a string in plain mode'); + +tap.ok(formattedString.match(logObject._from), + '#format() should include the from property in plain mode'); + +tap.ok(formattedString.match(isoDate), + '#format() should include the timestamp property in iso format in plain mode'); + +tap.ok(formattedString.match(logObject._levelString), + '#format() should include the level string property in plain mode'); + +tap.ok(formattedString.match(logObject.message), + '#format() should include the message property in plain mode'); + +/** + * TEST: #format() - colorized + */ + +formattedString = colorFormatter.format(logObject); + +tap.equal(typeof formattedString, 'string', + '#format() should output a string in color mode'); + +tap.ok(formattedString.match(logObject._from), + '#format() should include the from property in color mode'); + +tap.ok(formattedString.match(isoDate), + '#format() should include the timestamp property in iso format in color mode'); + +tap.ok(formattedString.match(logObject._levelString), + '#format() should include the level string property in color mode'); + +tap.ok(formattedString.match(logObject.message), + '#format() should include the message property in color mode'); + +tap.equal(formattedString.split(String.fromCharCode(27) + '[31m').length, 2, + '#format() should colorize the string'); + +tap.equal(formattedString.split(String.fromCharCode(27) + '[0m').length, 2, + '#format() should colorize only a bit of the string'); diff --git a/test/cologne/formatter/token.js b/test/cologne/formatter/token.js new file mode 100644 index 0000000..9bc473d --- /dev/null +++ b/test/cologne/formatter/token.js @@ -0,0 +1,107 @@ +'use strict'; + +let tap = require('tap'); + +let TokenFormatter = require('../../../lib/cologne/formatter/token'); + +// Prepare the test +let logObject, defaultFormatter, customFormatter, ansiFormatter, + plainDateFormatter, customSearchFormatter, formattedString, isoDate; + +tap.plan(13); + +logObject = { + _timestamp: Date.now() + .134, + _cologneLog: '1.0.0', + _from: 'Dummy Logger', + _level: 3, + _levelString: 'error', + message: 'testing stuff!' +}; +isoDate = (new Date(logObject._timestamp)).toISOString(); + +defaultFormatter = new TokenFormatter(); +customFormatter = new TokenFormatter({ + formatString: '{{_level}} {{_cologneLog}} {{_timestamp}}' +}); +ansiFormatter = new TokenFormatter({ + formatString: 'string {{_ansi:red}}with color:{{_ansi:reset}} {{message}}' +}); +plainDateFormatter = new TokenFormatter({ + isoDate: false, + formatString: '{{_timestamp}}' +}); +customSearchFormatter = new TokenFormatter({ + formatString: '[[message]]', + replaceRule: /\[\[(.*?)\]\]/g +}); + +/** + * TEST: #format() - default + */ + +formattedString = defaultFormatter.format(logObject); + +tap.equal(typeof formattedString, 'string', + '#format() should output a string in default mode'); + +tap.equal(formattedString, logObject.message, + '#format() should include the message in default mode'); + +/** + * TEST: #format() - custom + */ + +formattedString = customFormatter.format(logObject); + +tap.equal(typeof formattedString, 'string', + '#format() should output a string in custom mode'); + +tap.ok(formattedString.match(logObject._level), + '#format() with custom string should include the specified tokens (check 1)'); + +tap.ok(formattedString.match(logObject._cologneLog), + '#format() with custom string should include the specified tokens (check 2)'); + +tap.ok(formattedString.match(isoDate), + '#format() with iso date should include the timestamp as an iso date'); + +/** + * TEST: #format() - ansi + */ + +formattedString = ansiFormatter.format(logObject); + +tap.equal(typeof formattedString, 'string', + '#format() should output a string in ansi mode'); + +tap.equal(formattedString.split(String.fromCharCode(27) + '[31m').length, 2, + '#format() with ansi tokens should colorize the string'); + +tap.equal(formattedString.split(String.fromCharCode(27) + '[0m').length, 2, + '#format() with ansi reset should reset the string'); + + +/** + * TEST: #format() - plain date + */ + +formattedString = plainDateFormatter.format(logObject); + +tap.equal(typeof formattedString, 'string', + '#format() should output a string in plain date mode'); + +tap.equal(formattedString, logObject._timestamp.toString(), + '#format() with plain date should include the timestamp as-is'); + +/** + * TEST: #format() - custom search + */ + +formattedString = customSearchFormatter.format(logObject); + +tap.equal(typeof formattedString, 'string', + '#format() should output a string in custom search mode'); + +tap.equal(formattedString, logObject.message, + '#format() with a custom search, should properly match the new tokens'); diff --git a/test/cologne/log_utilities.js b/test/cologne/log_utilities.js new file mode 100644 index 0000000..e372683 --- /dev/null +++ b/test/cologne/log_utilities.js @@ -0,0 +1,69 @@ +'use strict'; + +let tap = require('tap'); + +let LogUtilities = require('../../lib/cologne/log_utilities'); + +// Prepare the test +let t1, t2, preciseTime, regularObject, circularObject, + regularStringify, cologneStringify, circularStringify; + +tap.plan(7); + +regularObject = { + a: 1, + b: { + c: 'true', + d: false + } +}; + +circularObject = { + a: 1, + b: { + c: 'true', + d: false + } +}; +circularObject.b.circular = circularObject; + +/** + * TEST: ::now() + */ +t1 = Date.now(); +preciseTime = LogUtilities.now(); +t2 = Date.now(); + +// This test is sloppy :( +tap.ok(Math.abs(t1 - preciseTime) < 1, + '::now() should give a precise timestamp (before)'); +tap.ok(Math.abs(t2 - preciseTime) < 1, + '::now() should give a precise timestamp (after)'); + +/** + * TEST: ::stringify() + */ + +regularStringify = JSON.stringify(regularObject); +cologneStringify = LogUtilities.stringify(regularObject); +circularStringify = LogUtilities.stringify(circularObject); + +tap.equal(regularStringify, cologneStringify, + '::stringify() should behave like JSON.stringify for non-circular objects'); +tap.equal(typeof JSON.parse(circularStringify).b.circular, 'string', + '::stringify() should replace circular references with a string'); + +/** + * TEST: ::getAnsiCode() + */ + +// NOTE: This isn't even trying to be a complete test... Just testing +// that we get other than reset if valid, and the same as reset for all +// invalid ones. knowing that reset is [0m + +tap.equal(LogUtilities.getAnsiCode('reset'), '[0m', + '::getAnsiCode is sending the correct reset code'); +tap.equal(LogUtilities.getAnsiCode('someRandomString'), LogUtilities.getAnsiCode('reset'), + '::getAnsiCode() should give us a reset code if something weird is sent'); +tap.notEqual(LogUtilities.getAnsiCode('red'), LogUtilities.getAnsiCode('reset'), + '::getAnsiCode() should give us a non-reset code if it\'s something real'); diff --git a/test/cologne/logger/console.js b/test/cologne/logger/console.js new file mode 100644 index 0000000..ffc1876 --- /dev/null +++ b/test/cologne/logger/console.js @@ -0,0 +1,110 @@ +'use strict'; + +let tap = require('tap'); + +let ConsoleLogger = require('../../../lib/cologne/logger/console'); + +// Prepare the test +let dummyFormatter, dummyConsole, logObjectA, logObjectB, regularLogger, + overrideLogger, formattedLogger, params; + +dummyFormatter = { + values: [], + format: function (logObject) { + this.values.push(logObject); + return 'lol'; + } +}; + +dummyConsole = { + values: {}, + _log: function (type, args) { + this.values[type] = args; + }, + log: function() { + this._log('log', Array.prototype.splice.call(arguments, [0])); + }, + warn: function() { + this._log('warn', Array.prototype.splice.call(arguments, [0])); + }, + error: function() { + this._log('error', Array.prototype.splice.call(arguments, [0])); + }, + info: function() { + this._log('info', Array.prototype.splice.call(arguments, [0])); + } +}; + +logObjectA = { + _timestamp: Date.now() + .134, + _cologneLog: '1.0.0', + _from: 'Dummy Logger', + _level: 6, + _levelString: 'info', + message: 'MessageOne' +}; + +logObjectB = { + _timestamp: Date.now() + .134, + _cologneLog: '1.0.0', + _from: 'Dummy Logger', + _level: 6, + _levelString: 'info', + message: 'MessageTwo' +}; + +params = [logObjectA, logObjectB]; + +regularLogger = new ConsoleLogger({}); +overrideLogger = new ConsoleLogger({ + console: dummyConsole +}); +formattedLogger = new ConsoleLogger({ + console: dummyConsole, + formatter: dummyFormatter +}); + +/** + * TEST: #log() - regular + */ + +tap.equal(regularLogger.console, global.console, + 'It should default to the global console'); + +/** + * TEST: #log() - override + */ + +logObjectA._level = 5; +logObjectB._level = 6; +overrideLogger.log.apply(overrideLogger, params); // should go to info + +logObjectA._level = 4; +logObjectB._level = 4; +overrideLogger.log.apply(overrideLogger, params); // should go to warn + +logObjectA._level = 1; +logObjectB._level = 3; +overrideLogger.log.apply(overrideLogger, params); // should go to error + +logObjectA._level = 7; +logObjectB._level = 7; +overrideLogger.log.apply(overrideLogger, params); // should go to log + +tap.equal(dummyConsole.values.log.length, params.length, + 'It should send debug messages to console\'s #log'); +tap.equal(dummyConsole.values.info.length, params.length, + 'It should send info and notice messages to console\'s #info'); +tap.equal(dummyConsole.values.warn.length, params.length, + 'It should send warn messages to console\'s #warn'); +tap.equal(dummyConsole.values.error.length, params.length, + 'It should send error messages to console\'s #error'); + +/** + * TEST: #log() - with formatter + */ + +formattedLogger.log.apply(formattedLogger, params); // should go to log + +tap.similar(dummyFormatter.values, params, + 'If available, it should send the objects to the formatter'); diff --git a/test/cologne/logger/file.js b/test/cologne/logger/file.js new file mode 100644 index 0000000..01be09f --- /dev/null +++ b/test/cologne/logger/file.js @@ -0,0 +1,103 @@ +'use strict'; + +let fs = require('fs'); + +let tap = require('tap'); + +let FileLogger = require('../../../lib/cologne/logger/file'); + +// Prepare the test +let logObjectA, logObjectB, rawFile, formatterFile, rawLogger, formatterLogger, + params, dummyFormatter, formatterString; + +rawFile = './raw.log'; +formatterFile = './formatter.log'; +formatterString = 'example'; + +logObjectA = { + _timestamp: Date.now() + .134, + _cologneLog: '1.0.0', + _from: 'Dummy Logger', + _level: 6, + _levelString: 'info', + message: 'MessageOne' +}; + +logObjectB = { + _timestamp: Date.now() + .134, + _cologneLog: '1.0.0', + _from: 'Dummy Logger', + _level: 6, + _levelString: 'info', + message: 'MessageTwo' +}; + +params = [logObjectA, logObjectB]; + + +dummyFormatter = { + values: [], + format: function (logObject) { + this.values.push(logObject); + return formatterString; + } +}; + +rawLogger = new FileLogger({ + file: rawFile +}); +formatterLogger = new FileLogger({ + file: formatterFile, + formatter: dummyFormatter +}); + +/** + * TEST: #log() - regular + */ + +rawLogger.log.apply(rawLogger, params); + +setTimeout(function () { + tap.test('raw file', function (t) { + fs.readFile(rawFile, {encoding: 'utf8'}, function (error, contents) { + let lines; + + lines = contents.trim().split('\n'); + + tap.equal(lines.length, params.length, + 'it should send all params to the file'); + + tap.equal(JSON.stringify(logObjectA), lines[0], + 'it should log the raw json object'); + + fs.unlink(rawFile, function () { + t.end(); + }); + }); + }); +}, 10); // allow for flush +/** + * TEST: #log() - formatter + */ + +formatterLogger.log.apply(formatterLogger, params); + +setTimeout(function () { + tap.test('formatted file', function (t) { + fs.readFile(formatterFile, {encoding: 'utf8'}, function (error, contents) { + let lines; + + lines = contents.trim().split('\n'); + + tap.equal(lines.length, params.length, + 'it should send all params to the file'); + + tap.equal(formatterString, lines[0], + 'it should log the formatted object'); + + fs.unlink(formatterFile, function () { + t.end(); + }); + }); + }); +}, 10);