diff --git a/src/js/src/js/ConsoleLogger.js b/src/js/src/js/ConsoleLogger.js new file mode 100644 index 0000000..727a703 --- /dev/null +++ b/src/js/src/js/ConsoleLogger.js @@ -0,0 +1,180 @@ +import { SyslogStmt } from "./SyslogStmt.js"; + +export class ConsoleLogger { + #level = 1; + #version = 1; + #facility = 16; + #hostname = SyslogStmt.NILVALUE; + #appname = SyslogStmt.NILVALUE; + #procId = SyslogStmt.NILVALUE; + #msgId = SyslogStmt.NILVALUE; + #validator = new SyslogStmt(); + + constructor() { + ['emerg', 'alert', 'crit', 'err', 'warn', 'notice', 'info', 'debug'].forEach(level => { + this[level] = (syslogStmt) => { + const upperCase = level.charAt(0).toUpperCase() + level.slice(1) + const finalStmt = this.createSyslogStmt(syslogStmt).sev(upperCase); + this.log(finalStmt); + return this; + }; + }); + } + /** + * このロガーの設定を元にSyslogStmtを設定、生成する + * @param {SyslogStmt | undefined} syslogStmt ログの元となるSyslogStmt。指定がなければ新規に作成する + * @returns {SyslogStmt} 設定済みのSyslogStmt + */ + createSyslogStmt(syslogStmt) { + if (typeof syslogStmt === 'undefined') { + syslogStmt = new SyslogStmt(); + } else if (!(syslogStmt instanceof SyslogStmt)) { + throw new Error("Invalid syslogStmt"); + } + + syslogStmt.app(this.#appname) + .ver(this.#version) + .fac(this.#facility) + .host(this.#hostname) + .proc(this.#procId) + .msgId(this.#msgId); + + return syslogStmt; + } + + /** + * + * @param {*} version + * @returns + */ + ver(version) { + this.#validator.ver(version); + this.#version = version; + return this; + } + + /** + * + * @param {*} facility + * @returns + */ + fac(facility) { + this.#validator.fac(facility); + this.#facility = facility; + return this; + } + + /** + * ログのホスト名を設定する。 + * @param {string} hostname + * @returns {ConsoleLogger} + */ + host(hostname) { + this.#validator.host(hostname); + this.#hostname = hostname; + return this; + } + + /** + * ログのアプリケーション名を設定する。 + * @param {string} appname + * @returns {ConsoleLogger} + */ + app(appname) { + this.#validator.app(appname); + this.#appname = appname; + return this; + } + + /** + * ログのプロセスIDを設定する。 + * @param {string} procId + * @returns {ConsoleLogger} + */ + proc(procId) { + this.#validator.proc(procId); + this.#procId = procId; + return this; + } + + /** + * ログのメッセージIDを設定する。 + * @param {string} msgId + * @returns {ConsoleLogger} + */ + msgId(msgId) { + this.#validator.msgId(msgId); + this.#msgId = msgId; + return this; + } + + /** + * syslogStmtをこのロガーの設定でSyslogStmtを生成し、ログを出力する。 + * @param {SyslogStmt} syslogStmt + * @param {string | undefined} format + */ + log(syslogStmt, format) { + if (syslogStmt.isOutput(this.#level)) { + console.log(syslogStmt.toString(format)); + } + } + + /** + * 引数のログを複製し、Emergレベルに設定して出力する。 + * @param {SyslogStmt} syslogStmt + * @returns {ConsoleLogger} + */ + emerg(syslogStmt) { } + + /** + * 引数のログを複製し、Critレベルに設定して出力する。 + * @param {SyslogStmt} syslogStmt + * @returns {ConsoleLogger} + */ + crit(syslogStmt) { } + + /** + * 引数のログを複製し、Alertレベルに設定して出力する。 + * @param {SyslogStmt} syslogStmt + * @returns {ConsoleLogger} + */ + alert(syslogStmt) { } + + /** + * 引数のログを複製し、Errorレベルに設定して出力する。 + * @param {SyslogStmt} syslogStmt + * @returns {ConsoleLogger} + */ + err(syslogStmt) { } + + + /** + * 引数のログを複製し、Warnレベルに設定して出力する。 + * @param {SyslogStmt} syslogStmt + * @returns {ConsoleLogger} + */ + warn(syslogStmt) { } + + /** + * 引数のログを複製し、Noticeレベルに設定して出力する。 + * @param {SyslogStmt} syslogStmt + * @returns {ConsoleLogger} + */ + notice(syslogStmt) { } + + /** + * 引数のログを複製し、Infoレベルに設定して出力する。 + * @param {SyslogStmt} syslogStmt + * @returns {ConsoleLogger} + */ + info(syslogStmt) { } + + /** + * 引数のログを複製し、Debugレベルに設定して出力する。 + * @param {SyslogStmt} syslogStmt + * @returns {ConsoleLogger} + */ + debug(syslogStmt) { } + + +} \ No newline at end of file diff --git a/src/js/src/js/StructuredData.js b/src/js/src/js/StructuredData.js new file mode 100644 index 0000000..d86f42a --- /dev/null +++ b/src/js/src/js/StructuredData.js @@ -0,0 +1,106 @@ +import { SyslogStmt } from "./SyslogStmt.js"; + +export class StructuredData { + #elements = new Map(); // Map> + #currentSdId = null; + + /** + * 構造化データにキーと値のペアを追加する。 + * arg1はSD-ID、arg2はPARAM-NAME、arg3はPARAM-VALUEとして扱う。 + * arg3に値が指定されなかった場合は、arg1をPARAM-NAME、arg2をPARAM-VALUEとして扱う。 + * また、use(arg1)と同じ効果を持つ。 + * SD-PARAM(PARAM-VALUE) + * @param {string} arg1 + * @param {string} arg2 + * @param {string} arg3 + * @returns {StructuredData} + */ + add(arg1, arg2, arg3) { + let sdId, key, value; + + if (arg3 !== undefined) { + // 3つ引数がある場合: add(sdId, key, value) + [sdId, key, value] = [arg1, arg2, arg3]; + } else { + // 2つ引数がある場合: add(key, value) -> 直前のSDIDを使用 + if (!this.#currentSdId) { + throw new Error("No previous SDID found"); + } + [sdId, key, value] = [this.#currentSdId, arg1, arg2]; + } + + this.#validateSDName(sdId); + this.#validateSDName(key); + value = SyslogStmt.escapeControlChars(this.#escape(value)); + + // 状態の更新 + this.#currentSdId = sdId; + + // 以下、Mapへの追加処理... + if (!this.#elements.has(sdId)) { + this.#elements.set(sdId, new Map()); + } + this.#elements.get(sdId).set(key, value); + + return this; + } + + /** + * addを呼び出すだけです。addという語がしっくり来ない時にどうぞ。 + * @param {string} arg1 + * @param {string} arg2 + * @param {string} arg3 + * @returns {StructuredData} + * @see {@link add} addメソッドを参照 + */ + set(arg1, arg2, arg3) { + return this.add(arg1, arg2, arg3); + } + + /** + * 指定したSDIDを現在の編集対象にする。 + * @param {*} sdId + * @returns {StructuredData} + */ + use(sdId) { + this.#currentSdId = sdId; + return this; + } + + #escape(val) { + // ], ", \ をエスケープする処理 + return String(val).replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/]/g, '\\]'); + } + + toString() { + if (this.#elements.size === 0) { + return SyslogStmt.NILVALUE; + } + + let res = ""; + for (const [sdId, params] of this.#elements) { + res += `[${sdId}`; + for (const [k, v] of params) { + res += ` ${k}="${v}"`; + } + res += "]"; + } + return res; + } + + #validateSDName(name) { + // 1. 長さのチェック + if (!/^[\x21-\x7E]{1,32}$/.test(name)) { + throw new Error("SD-NAMEは1〜32文字の範囲で指定してください。"); + } + + // 2. 禁止文字のチェック + if (/[=\]"\s]/.test(name)) { + throw new Error('SD-NAMEに不正な文字(=, 空白, ], ")が含まれています。'); + } + + return true; + } +} \ No newline at end of file diff --git a/src/js/src/js/SyslogStmt.js b/src/js/src/js/SyslogStmt.js index dcbb20d..72de35c 100644 --- a/src/js/src/js/SyslogStmt.js +++ b/src/js/src/js/SyslogStmt.js @@ -2,35 +2,123 @@ * @typedef { 'Emerg' | 'Alert' | 'Crit' | 'Err' | 'Warn' | 'Notice' | 'Info' | 'Debug' } Severity */ +import { StructuredData } from "./StructuredData.js"; + export class SyslogStmt { - static sevNum = { - Emerg: 0, - Alert: 1, - Crit: 2, - Err: 3, - Warn: 4, - Notice: 5, - Info: 6, - Debug: 7, - }; + static get sevNum() { + return { + Emerg: 0, + Alert: 1, + Crit: 2, + Err: 3, + Warn: 4, + Notice: 5, + Info: 6, + Debug: 7, + }; + } + + static get sevStr() { + return [ + "Emerg", + "Alert", + "Crit", + "Err", + "Warn", + "Notice", + "Info", + "Debug", + ]; + } + + static get facNum() { + return { + kern: 0, + user: 1, + mail: 2, + daemon: 3, + auth: 4, + syslog: 5, + lpr: 6, + news: 7, + uucp: 8, + cron: 9, + authpriv: 10, + ftp: 11, + ntp: 12, + audit: 13, + alert: 14, + clock: 15, + local0: 16, + local1: 17, + local2: 18, + local3: 19, + local4: 20, + local5: 21, + local6: 22, + local7: 23, + }; + } + + static get facStr() { + return [ + "kern", + "user", + "mail", + "daemon", + "auth", + "syslog", + "lpr", + "news", + "uucp", + "cron", + "authpriv", + "ftp", + "ntp", + "audit", + "alert", + "clock", + "local0", + "local1", + "local2", + "local3", + "local4", + "local5", + "local6", + "local7", + ]; + } static sev = this.sevNum; + static get NILVALUE() { + return "-"; + } #facility = 16; #severity = 1; #timestamp = new Date(); #version = 1; - #hostname = "-"; - #appname = "-"; - #procId = "-"; - #msgId = "-"; - #structuredData = "-"; + #hostname = SyslogStmt.NILVALUE; + #appname = SyslogStmt.NILVALUE; + #procId = SyslogStmt.NILVALUE; + #msgId = SyslogStmt.NILVALUE; + #structuredData = new StructuredData(); #msg = ""; + /** + * PRI値を取得する。 + * @returns {number} + */ get pri() { return this.#facility * 8 + this.#severity; } - /** clone with message */ + /** + * タイムスタンプと構造化データを覗いて複製する。 + * 引数で新しいメッセージを設定する。 + * 申し訳ないがMSGにバイナリがRFC5424的には許されるが、ここでは対応してない(勘弁して欲しい) + * @param {string} msg + * @returns {SyslogStmt} + **/ gen(msg) { const result = new SyslogStmt(); result.#severity = this.#severity; @@ -40,13 +128,30 @@ result.#appname = this.#appname; result.#procId = this.#procId; result.#msgId = this.#msgId; - result.#structuredData = this.#structuredData; + //structuredDataとmsgはコピーしない。 + result.#msg = msg; return result; } + /** + * 現在のインスタンスのコピーを作成する。 + * @returns {SyslogStmt} + */ + clone() { + const result = this.gen(); + result.#structuredData = this.#structuredData; + result.#timestamp = this.#timestamp; + return result; + } + + /** + * 重大度を設定する。 + * @param {string | number} severity + * @returns {SyslogStmt} + */ sev(severity) { - if (sevNum.hasOwn(severity)) { + if (typeof severity === "string" && SyslogStmt.sevNum.hasOwnProperty(severity)) { this.#severity = SyslogStmt.sevNum[severity]; } else if (Number.isInteger(severity) && 0 <= severity && severity <= 7) { this.#severity = severity; @@ -56,11 +161,28 @@ return this; } + /** + * ファシリティを設定する。 + * @param {string | number} facility + * @returns {SyslogStmt} + */ fac(facility) { - this.#facility = facility; + if (typeof facility === "string" && SyslogStmt.facNum.hasOwnProperty(facility)) { + this.#facility = SyslogStmt.facNum[facility]; + } else if (Number.isInteger(facility) && 0 <= facility && facility <= 23) { + this.#facility = facility; + } else { + throw new Error(`Invalid facility: ${facility}`); + } return this; } + /** + * タイムスタンプを設定する。現在時刻はログ生成時に自動設定される。 + * 引数がDate型の場合はそのインスタンスを使用、そうでない場合はDateコンストラクタに渡す。 + * @param {*} timestamp + * @returns {SyslogStmt} + */ time(timestamp) { if (timestamp instanceof Date) { this.#timestamp = timestamp; @@ -70,49 +192,272 @@ return this; } + /** + * バージョンを設定する。とりあえず0か1しか設定しない。 + * @param {number} version + * @returns {SyslogStmt} + */ ver(version) { - this.#version = version; + if (Number.isInteger(version) && 0 <= version && version <= 1) { + this.#version = version; + } else { + throw new Error(`Invalid version: ${version}`); + } return this; } + /** + * ホスト名を設定する。 + * @param {string} hostname 空文字、null、undefined を与えられると、NILVALUEとして解釈します。 + * @returns {SyslogStmt} + */ host(hostname) { + const regExp = /^[\x21-\x7E]{1,255}$/; + hostname = this.#nilOrString(hostname); + if (typeof hostname !== "string") { + throw new Error(`Invalid hostname: ${hostname}`); + } + + if (!regExp.test(hostname) && hostname !== "-") { + throw new Error(`Invalid hostname: ${hostname}`); + } + + this.#hostname = hostname; return this; } + /** + * アプリケーション名を設定する。 + * @param {string} appname 空文字、null、undefined を与えられると、NILVALUEとして解釈します。 + * @returns {SyslogStmt} + */ app(appname) { + const regExp = /^[\x21-\x7E]{1,48}$/; + appname = this.#nilOrString(appname); + if (typeof appname !== "string") { + throw new Error(`Invalid appname: ${appname}`); + } + + if (!regExp.test(appname) && appname !== "-") { + throw new Error(`Invalid appname: ${appname}`); + } + this.#appname = appname; return this; } + /** + * プロセスIDを設定する。 + * @param {string} procId 空文字、null、undefined を与えられると、NILVALUEとして解釈します。 + * @returns {SyslogStmt} + */ proc(procId) { + const regExp = /^[\x21-\x7E]{1,128}$/; + procId = this.#nilOrString(procId); + if (typeof procId !== "string") { + throw new Error(`Invalid procId: ${procId}`); + } + + if (!regExp.test(procId) && procId !== "-") { + throw new Error(`Invalid procId: ${procId}`); + } + this.#procId = procId; return this; } + /** + * メッセージIDを設定する。 + * @param {string} msgId 空文字、null、undefined を与えられると、NILVALUEとして解釈します。 + * @returns {SyslogStmt} + */ msgId(msgId) { + const regExp = /^[\x21-\x7E]{1,32}$/; + msgId = this.#nilOrString(msgId); + if (typeof msgId !== "string") { + throw new Error(`Invalid msgId: ${msgId}`); + } + + if (!regExp.test(msgId) && msgId !== "-") { + throw new Error(`Invalid msgId: ${msgId}`); + } + this.#msgId = msgId; return this; } + /** + * 引数で指定した構造化データを設定する。 + * 文字列が渡された場合は、そのまま設定する。 + * StructuredDataが渡された場合は、toString()を実行してから設定する。 + * @param {string | StructuredData} structuredData + * @returns {SyslogStmt} + */ sd(structuredData) { - this.#structuredData = structuredData; + if (typeof structuredData === 'string') { + this.#structuredData = structuredData; + } else if (structuredData instanceof StructuredData) { + this.#structuredData = structuredData.toString(); + } else { + throw new Error(`Invalid structuredData: ${structuredData}`); + } + return this; } + /** + * RFC5424形式でログを出力する。 + * @returns {string} + */ toRFC5424() { const msg = this.#msg ? " ".concat("\uFEFF", this.#msg) : ""; - return `${this.#header()} ${this.#structuredData}${msg}`; + return `${this.#header()} ${this.#structuredData.toString()}${msg}`; + } + + /** + * [重大度] タイムスタンプ メッセージの形式でログを出力する + * @returns {string} + */ + toSimple() { + const sevStr = this.constructor.sevStr[this.#severity] ?? "Unknown"; + return `[${sevStr}] ${this.#timestamp.toISOString()} ${this.#msg}`; + } + + /** + * ログの出力形式を指定して出力する + * @param { "rfc5424" | "simple" } format + * @returns {string} + */ + toString(format) { + if (format === "simple") { + return this.toSimple(); + } + return this.toRFC5424(); } get str() { return this.toRFC5424() }; + /** + * ログレベルをemergに設定する。 + * @returns {SyslogStmt} + */ + emerg() { + return this.sev(0); + } + + /** + * ログレベルをcritに設定する。 + * @returns {SyslogStmt} + */ + crit() { + return this.sev(1); + + } + + /** + * ログレベルをalertに設定する。 + * @returns {SyslogStmt} + */ + alert() { + return this.sev(2); + } + + /** + * ログレベルをerrに設定する。 + * @returns {SyslogStmt} + */ + err() { + return this.sev(3); + } + + /** + * ログレベルをwarnに設定する。 + * @returns {SyslogStmt} + */ + warn() { + return this.sev(4); + } + + /** + * ログレベルをnoticeに設定する。 + * @returns {SyslogStmt} + */ + notice() { + return this.sev(5); + } + + /** + * ログレベルをinfoに設定する。 + * @returns {SyslogStmt} + */ + info() { + return this.sev(6); + } + + /** + * ログレベルをdebugに設定する。 + * @returns {SyslogStmt} + */ + debug() { + return this.sev(7); + } + + /** + * 指定したログレベルが現在の重大度以上であるかを判定する。 + * @param {number} level + * @returns {boolean} + */ + isOutput(level) { + return this.#severity <= level; + } + + /** + * ログのメッセージ中の制御文字をエスケープする。 + * 対象:ANSI制御文字の0x00-0x1F (LF:0x0A, TAB:0x09 を除く) および 0x7F + * UTF8の\u200B-\u200F、\uFEFF\uFFFE\uFFFF + * 例:"h\x00\u200B\uFEFFtest" -> "h\\x00\\u200B\\uFEFFtest" + * @param {string} str + * @returns {string} + */ + static escapeControlChars(str) { + // 0x00-0x1F (LF:0x0A, TAB:0x09 を除く) および 0x7F を対象にする + // \x00-\x08: NUL~BS + // \x0B-\x0C: VT, FF + // \x0E-\x1F: SO~US + // \x7F: DEL + // \u200B-\u200F,\uFEFF, \uFFFE, \uFFFF:非文字 + return str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\u200B-\u200F\uFEFF\uFFFE\uFFFF]/g, (match) => { + const code = match.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'); + return `\\x${code}`; + }); + } + + /** + * ヘッダーを生成する。 + * @returns {string} + */ #header() { - const timestamp = - this.#timestamp instanceof Date - ? this.#timestamp.toISOString() - : "-"; + const timestamp = this.#timestamp instanceof Date + ? this.#timestamp.toISOString() + : SyslogStmt.NILVALUE; return `<${this.pri}> ${this.#version} ${timestamp} ${this.#hostname} ${this.#appname} ${this.#procId} ${this.#msgId}`; } + + /** + * undefined, null, 空文字を NILVALUE("-") に置換する + * @param {*} src + * @returns + */ + #nilOrString(src) { + let result = src ?? ""; + if (result === "") { + result = SyslogStmt.NILVALUE; + } + return result; + } + + + }