From 2fb1cab0e9f2a36aa3357e3bc10db842b043fc2c Mon Sep 17 00:00:00 2001 From: churchianity Date: Sun, 8 May 2022 23:08:24 -0400 Subject: [PATCH] initial --- README.md | 47 +++++++++ log4jesus.default.json | 39 +++++++ log4jesus.js | 229 +++++++++++++++++++++++++++++++++++++++++ log4jesus.json | 39 +++++++ 4 files changed, 354 insertions(+) create mode 100644 README.md create mode 100644 log4jesus.default.json create mode 100644 log4jesus.js create mode 100644 log4jesus.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..3826752 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ + +# Usage + +```js + const { log, logt } = require("log4jesus"); +``` + +## log +send log data to targets tagged with 'all' +```js + log("use", ["exactly", "like"], `console.log ${420 * 69}`) +``` + +## logt +send log data to only targets tagged with one of the tags in the array passed as a first arguement. +first argument can be a string or array of strings. +```js + // send only to targets with the 'verbose' tag + logt("verbose", "Download progress:", downloadProgress / totalDownloads); + + // send to targets with the 'info' and 'error' tags + log(["error", "info"], "failed to parse json at: ", filePath) +``` + +# Configuration +log4jesus 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. +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 + +## 'console' details +- stdstream: required value of 'stdout', 'stderr', or 'stdin'. It's not usually desired to make it 'stdin'. + +## '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. + +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'. + +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'. + diff --git a/log4jesus.default.json b/log4jesus.default.json new file mode 100644 index 0000000..3a31d9c --- /dev/null +++ b/log4jesus.default.json @@ -0,0 +1,39 @@ +{ + "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": "https://localhost:8181", + "method": "POST", + "headers": { + "content-type": "application/json" + } + } + ] +} diff --git a/log4jesus.js b/log4jesus.js new file mode 100644 index 0000000..d8015cf --- /dev/null +++ b/log4jesus.js @@ -0,0 +1,229 @@ + +const fs = require("fs"); +const util = require("util"); +const http = require("http"); +const https = require("https"); + + +function timestamp() { + return new Date().toISOString(); +} + +function formatOutput(...args) { + return `${timestamp()} - ${util.format(...args)}\n`; +} + +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.txt`; + + try { + file.stream = fs.createWriteStream(path, { flags: "a", autoClose: true }); + + } catch (e) { + console.error(`failed to open file at: ${path} for writing. Check the path in the config, and permissions for that directory and this process`); + process.exit(1); + } + } + } + + if (config.console) { + for (let i = 0; i < config.console.length; i++) { + const tty = config.console[i]; + + if ( + !(tty.stdstream === "stdout" + || tty.stdstream === "stderr" + || tty.stdstream === "stdin") + ) { + console.error(tty, "console items in config must have a key 'stdstream' set to one of the following: 'stdout', 'stderr', or 'stdin'. Check your config.") + process.exit(1); + } + + tty.stream = process[tty.stdstream]; + } + } + + if (config.http) { + for (let i = 0; i < config.http.length; i++) { + const h = config.http[i]; + + 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`); + } + + if (!h.url) { + console.error(`Please specify a 'url' on your http object in your configuration. The url should be full - the protocol and port should be included`); + process.exit(1); + } + + if (!h.headers || !h.headers["content-type"]) { + h.headers = { + "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); + + } catch (e) { + console.error(`invalid JSON in your configuration file: ${configFilePath}`); + process.exit(1); + } + } 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); + +function getStreamsOfTypeAndTags(tags, type) { + const streams = config[type]; + const out = []; + for (let i = 0; i < streams.length; i++) { + const stream = streams[i]; + + if (!Array.isArray(stream.tags)) { + stream.tags = [ stream.tags ]; + } + + for (let j = 0; j < stream.tags.length; j++) { + for (let k = 0; k < tags.length; k++) { + if (stream.tags[j] === tags[k]) { + out.push({ type, ...stream }); + } + } + } + } + return out; +} + +function getStreamsByTags(tags) { + if (!Array.isArray(tags)) { + tags = [ tags ]; + } + + return [].concat( + getStreamsOfTypeAndTags(tags, "file"), + getStreamsOfTypeAndTags(tags, "console"), + getStreamsOfTypeAndTags(tags, "http") + ); +} + +// convienent list of streams that we always want to send our logs to, regardless of tags +const alwaysStreams = getStreamsByTags("all"); + +function httpRequest(options, payload) { + const url = new URL(options.url); + const client = url.protocol === "https" ? https : http; + options.headers["content-length"] = payload.length; + const request = client.request(url, options, response => { + response.on("data", data => { + // @TODO, if you care what the server responds with + }); + }); + + request.on("error", error => { + // @TODO, if you care about errors on the request + }); + + request.write(payload); + request.end(); +} + +// 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); + } + } +} + +// |tags| should be a string or array of strings identifying a key in the above configuration object +// which informs the logger 'where' to output the log +function logt(tags, ...args) { + if (!args) { + log(tags); + return; + } + + if (!Array.isArray(tags)) { + tags = [ tags ]; + } + + const streams = getStreamsByTags(tags); + for (let i = 0; i < streams.length; i++) { + const s = streams[i]; + + if (s.stream) { + s.stream.write(output); + + } else if (s.type === "http") { + httpRequest(s, output); + } + } +} + +module.exports = { log, logt }; + diff --git a/log4jesus.json b/log4jesus.json new file mode 100644 index 0000000..3a31d9c --- /dev/null +++ b/log4jesus.json @@ -0,0 +1,39 @@ +{ + "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": "https://localhost:8181", + "method": "POST", + "headers": { + "content-type": "application/json" + } + } + ] +}