diff --git a/README.md b/README.md index 67510fa..925255d 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,17 @@ # Usage ```js - const { log, logt } = require("./log4jesus"); + const { log, logt } = require("./tlog"); ``` ## log send log data to targets tagged with 'all' ```js - log("use", ["exactly", "like"], `console.log ${420 * 69}`) + log("use", ["exactly", "like"], `console.log ${17 * 35}`) ``` ## logt -send log data to only targets tagged with one of the tags in the array passed as a first arguement. +send log data to only targets tagged with one of the tags in the array passed as a first argument. first argument can be a string or array of strings. ```js // send only to targets with the 'verbose' tag @@ -23,28 +23,40 @@ first argument can be a string or array of strings. ``` # Configuration -log4jesus supports 3 types of targets for your log data: +tlog supports 3 types of targets for your log data: - file - console - http -to determine what logs go to which target, edit the file `log4jesus.json`. -there are three arrays defined in that file, associated with a key of either 'file', 'console' or 'http. +to determine what logs go to which target, edit the file `tlog.conf.js`. +there are three arrays defined in that file, associated with a key of either 'file', 'console' or 'http'. in each case, the array should be an array of objects with a `tags` property (which should be a string or array of strings), as well as some other details about how this target should operate. -## 'file' details -- path: optional string to the file to create +# Details +## 'file' object details +- path: optional string path to the file to create. defaults to the `data` directory, as a `.log` file named a ISO 8601 timestamp at time of creation. -## 'console' details -- stdstream: required value of 'stdout', 'stderr', or 'stdin'. It's not usually desired to make it 'stdin'. +## 'console' object details +- stdstream: required value of 'stdout', 'stderr', or 'stdin'. It's not usually desired to make it 'stdin'. outputs sent to stdstreams are automatically de-duplicated. even if there are multiple console outputs with the tag relevant to your logs that share the same stdstream, the output will only happen once. -## 'http' details -our http object is an extension of the [nodejs http/https options object](https://nodejs.org/api/http.html#httprequesturl-options-callback). The object is passed faithfully to http/s.request, so anything you add that is valid for http/s.request will be valid here. +## 'http' object details +our http object is an extension of the [nodejs http/https options object](https://nodejs.org/api/http.html#httprequesturl-options-callback). The object is passed faithfully to http(s).request, so anything you add that is valid for http(s).request will be valid here. -Add a 'url' field to the object to specify the url for the request, which should include both the port and protocol - you usually won't need to set 'hostname' or 'host', or 'path' or 'port' or 'protocol'. +Whichever method you choose, the data of the log will be sent in the request body. + +Add a 'url' field to the object to specify the url for the request, which should include both the port and protocol - you usually won't need to set 'hostname' or 'host', or 'path' or 'port' or 'protocol'. Certain combinations of port/protocol/path/host/hostname can behave strangely - it's recommended to specify as much information as possible in the 'url' field. You may commonly have to set the 'headers' object, and the 'content-type' header inside of that object, depending on your server. If it's not provided it's set to 'text/plain'. +The configuration file is a .js file to enable using environment variables, as you may have sensitive information in your url(s). + +### Handling Http Errors +Network requests can fail at runtime, and often those failures are also something you want to log. +Specify the `errorTags` property on the http object, and errors will attempt to be written to the target(s) specified when they occur. +If the target that failed is tagged with one of the tags on its own `errorTags` property, it will skip itself. +This feature is pretty limited and needs elaboration. + +# Other Configuration Options/Settings ## timestamps all logs are prefixed with an ISO 8601 timestamp. You can change this easily by editing the `timestamp` function, but for now it is not configurable from the config file. diff --git a/test.js b/test.js index ddc8c27..644b33e 100644 --- a/test.js +++ b/test.js @@ -1,8 +1,7 @@ -const { log, logt } = require("./log4jesus"); +const { log, logt } = require("./tlog"); - -log("use", ["exactly", "like"], `console.log ${420 * 69}`) +log("use", ["exactly", "like"], `console.log ${17 * 15}`) const downloadProgress = 51; const totalDownloads = 106; diff --git a/log4jesus.json b/tlog.conf.default similarity index 82% rename from log4jesus.json rename to tlog.conf.default index b1669fb..ff47fa5 100644 --- a/log4jesus.json +++ b/tlog.conf.default @@ -1,7 +1,8 @@ -{ + +module.exports = { "file": [ { - "tags": "all" + "tags": "httpErrors" } ], "console": [ @@ -29,11 +30,13 @@ "http": [ { "tags": "none", - "url": "http://localhost:8080", + "url": process.env.MY_SERVER_URL, + "errorTags": "httpErrors", "method": "POST", "headers": { "content-type": "text/plain" } } ] -} +}; + diff --git a/log4jesus.default.json b/tlog.conf.js similarity index 77% rename from log4jesus.default.json rename to tlog.conf.js index 3a31d9c..ff47fa5 100644 --- a/log4jesus.default.json +++ b/tlog.conf.js @@ -1,7 +1,8 @@ -{ + +module.exports = { "file": [ { - "tags": "all" + "tags": "httpErrors" } ], "console": [ @@ -29,11 +30,13 @@ "http": [ { "tags": "none", - "url": "https://localhost:8181", + "url": process.env.MY_SERVER_URL, + "errorTags": "httpErrors", "method": "POST", "headers": { - "content-type": "application/json" + "content-type": "text/plain" } } ] -} +}; + diff --git a/log4jesus.js b/tlog.js similarity index 62% rename from log4jesus.js rename to tlog.js index cc0c436..cc488b7 100644 --- a/log4jesus.js +++ b/tlog.js @@ -3,6 +3,7 @@ const fs = require("fs"); const util = require("util"); const http = require("http"); const https = require("https"); +const crypto = require("crypto"); function timestamp() { @@ -17,7 +18,9 @@ function parseStreams(config) { if (config.file) { for (let i = 0; i < config.file.length; i++) { const file = config.file[i]; - let path = file.path || `${__dirname}/${timestamp()}.log`; + file.id = crypto.randomBytes(8).toString("hex"); + + let path = file.path || `${__dirname}/data/${timestamp()}.log`; try { file.stream = fs.createWriteStream(path, { flags: "a", autoClose: true }); @@ -42,6 +45,7 @@ function parseStreams(config) { process.exit(1); } + tty.id = tty.stdstream; tty.stream = process[tty.stdstream]; } } @@ -50,11 +54,13 @@ function parseStreams(config) { for (let i = 0; i < config.http.length; i++) { const h = config.http[i]; + h.id = crypto.randomBytes(8).toString("hex"); + if (!h.method) { h.method = "POST"; } else if (h.method === "GET") { - console.warn(`using the http method 'GET' to send your logs is not recommended - in particular if you are intending to send a request body. Some system libraries are known to strip the payload completely off of GET requests, making this unreliable, despite the http 1.1 standard technically allowing it. See: https://github.com/whatwg/fetch/issues/551#issue-235203784`); + console.warn(`using the http method 'GET' to send your logs is not recommended. Unfortunately, sending payloads with a GET request can be unreliable.\nSee:\nhttps://github.com/whatwg/fetch/issues/551#issue-235203784\nRemove this line in tlog.js to suppress this warning.`); } if (!h.url) { @@ -67,68 +73,20 @@ function parseStreams(config) { "content-type": "text/plain" }; } - } - } -} -function loadAndParseConfig(configFilePath) { - let config; - try { - const fileContents = fs.readFileSync(configFilePath, { encoding: "utf-8", flag: "r" }); - try { - config = JSON.parse(fileContents); + if (!h.errorTags) { + h.errorTags = []; - } catch (e) { - console.error(`invalid JSON in your configuration file: ${configFilePath}`); - process.exit(1); + } else if (!Array.isArray(h.errorTags)) { + h.errorTags = [ h.errorTags ]; + } } - } catch(e) { - // no config file, warn about it, make a default - console.warn(`no configuration file found at: ${configFilePath}. Using a default config.`); - config = { - "file": [ - { - "tags": "all" - }, - ], - "console": [ - { - "tags": "error", - "stdstream": "stderr" - }, - { - "tags": "warn", - "stdstream": "stdout" - }, - { - "tags": "info", - "stdstream": "stdout" - }, - { - "tags": "verbose", - "stdstream": "stdout" - }, - { - "tags": [ "all", "log" ], - "stdstream": "stdout" - } - ], - "http": [ - { - "tags": "none", - "url": "http://localhost:8080" - } - ] - }; } - parseStreams(config); - return config; } -const configFilePath = __dirname + "/log4jesus.json"; -const config = loadAndParseConfig(configFilePath); +const config = parseStreams(require("./tlog.conf.js")); function getStreamsOfTypeAndTags(tags, type) { const streams = config[type]; @@ -162,9 +120,10 @@ function getStreamsByTags(tags) { // convienent list of streams that we always want to send our logs to, regardless of tags const alwaysStreams = getStreamsByTags(["all"]); -function httpRequest(options, payload) { +function httpRequest(options, ...args) { const url = new URL(options.url); const client = url.protocol === "https" ? https : http; + const payload = formatOutput(...args); options.headers["content-length"] = payload.length; const request = client.request(url, options, response => { response.on("data", data => { @@ -173,26 +132,41 @@ function httpRequest(options, payload) { }); request.on("error", error => { - // @TODO, if you care about errors on the request + const streams = getStreamsByTags(options.errorTags); + + const usedStreams = {}; + for (let i = 0; i < streams.length; i++) { + const stream = streams[i]; + + if (usedStreams[stream.id]) { + continue; + } + + usedStreams[stream.id] = true; + + if (stream.id !== options.id) { + write(stream, "error writing to http: ", ...args); + } + } }); - request.write(payload); + request.write(formatOutput(...args)); request.end(); } +function write(s, ...args) { + if (s.stream) { + s.stream.write(formatOutput(...args)); + + } else if (s.type === "http") { + httpRequest(s, ...args); + } +} + // log by itself will only log to streams tagged 'all'. if you want to specifiy tags, call 'logt' instead function log(...args) { - const output = formatOutput(...args); - for (let i = 0; i < alwaysStreams.length; i++) { - const s = alwaysStreams[i]; - - if (s.stream) { - s.stream.write(output); - - } else if (s.type === "http") { - httpRequest(s, output); - } + write(alwaysStreams[i], ...args); } } @@ -200,7 +174,6 @@ function log(...args) { // which informs the logger 'where' to output the log function logt(tags, ...args) { if (!args) { - log(tags); return; } @@ -208,18 +181,29 @@ function logt(tags, ...args) { tags = [ tags ]; } - const output = formatOutput(...args); + const usedStreams = {}; const streams = getStreamsByTags(tags); for (let i = 0; i < streams.length; i++) { const s = streams[i]; + if (usedStreams[s.id]) { + continue; + } - if (s.stream) { - s.stream.write(output); + write(s, ...args); + usedStreams[s.id] = true; + } - } else if (s.type === "http") { - httpRequest(s, output); + // now send it to the 'all' streams. we need to check if we've already done the job, because in the case a stream is tagged with both 'all' and another tag, we'd double send if we didn't de-dupe. + for (let i = 0; i < alwaysStreams.length; i++) { + const s = alwaysStreams[i]; + + if (usedStreams[s.id]) { + continue; } + + write(s, ...args); + usedStreams[s.id] = true; } }