Skip to content

Commit e8a3b64

Browse files
HolzchopfSergeCroisechenkins
authored
feat(backend): add configurable logger (#147)
* feat(backend): add logger class * fix: missing method name fallback * fix: default log level * feat: add split-string-at utility function * feat: add command line parser and logger cli options * Update ts/backend/src/app/features/logger/logger.mts Co-authored-by: Serge Croisé <[email protected]> * Update ts/backend/src/app/features/config/command-line.mts Co-authored-by: Serge Croisé <[email protected]> * fix: line wrap with special chars * Add ElementType and ElementPropertyType utility types * Add log4js in preparation for log to file or other target (future use) * Update backend readme * Improve doc on why using a custom call stack parser --------- Co-authored-by: Serge Croisé <[email protected]> Co-authored-by: Christian Eichenberger <[email protected]>
1 parent fc34e72 commit e8a3b64

15 files changed

+394
-63
lines changed

package-lock.json

+8-17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@
4141
"json5": "^2.2.3",
4242
"jsonwebtoken": "^9.0.2",
4343
"jwks-rsa": "^3.1.0",
44+
"log4js": "^6.9.1",
4445
"postgres": "^3.4.5",
4546
"redis": "^4.7.0",
4647
"rxjs": "~7.8.0",
4748
"tslib": "^2.3.0",
49+
"wrap-ansi": "^9.0.0",
4850
"zone.js": "~0.15.0"
4951
},
5052
"devDependencies": {

ts/backend/README.md

+24-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@ This backend is a Node.js module. The module creates a server application publis
66

77
The configuration template is found at `ts/backend/src/config/config.jsonc`. It's a JSON file with comments describing the sections.
88

9+
Furthermore, run-time behavior can be controlled using command-line arguments when executing `main.min.mjs`:
10+
11+
```terminal
12+
--help, -h Print this message.
13+
14+
--log-level Set log output level.
15+
type ALL | TRACE | DEBUG | INFO | WARN | ERROR | FATAL |
16+
OFF
17+
default INFO
18+
19+
--log-stack Include logging source (method and file path) in log.
20+
type false | <any other value>
21+
default false
22+
23+
--log-colorful Enable colorful terminal output for log.
24+
type false | <any other value>
25+
default false
26+
27+
--log-stringify Output log messages as stringified JSON.
28+
type false | <any other value>
29+
default false
30+
```
31+
932
## Development server
1033

1134
To start a local development server, run the following command in `ts/backend`:
@@ -43,4 +66,4 @@ There are no CLI code scaffolding tools for FAB Backend. Manually create `.mts`
4366
* Upload `dist/backend` to web server (e.g. nginx)
4467
* Manually copy `config/config.jsonc` (first time only) and modify according to your needs. The template is `ts/backend/src/config/config.jsonc`. It's not copied over automatically is to ensure the config template won't overwrite the user-specified configs.
4568

46-
* Set up your server to execute `node main.mjs` in `app/` (intentionally leaving out further instructions, since this depends on the server in use)
69+
* Set up your server to execute `node main.min.mjs` in `app/` (intentionally leaving out further instructions, since this depends on the server in use)

ts/backend/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.0.0",
44
"description": "FAB backend",
55
"scripts": {
6-
"serve": "npm run private:build && cd ../../out-tsc && mkdir -p backend && cp -r backend-build/app backend && mkdir -p backend/config && cp -n ../ts/backend/src/config/config.jsonc \"$_\" && cd backend/app && node main.min.js",
6+
"serve": "npm run private:build && cd ../../out-tsc && mkdir -p backend && cp -r backend-build/app backend && mkdir -p backend/config && cp -n ../ts/backend/src/config/config.jsonc \"$_\" && cd backend/app && node main.min.js --help --log-level=ALL --log-colorful --log-stringify",
77
"build": "npm run private:build && mkdir -p ../../dist/backend/config && cp -r ../../out-tsc/backend-build/app ../../dist/backend",
88
"private:build": "tsc && tsc-alias && npx webpack"
99
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import ansiStyles from 'ansi-styles'
2+
import wrapAnsi from 'wrap-ansi'
3+
import { Logger } from '../logger/logger.mjs'
4+
5+
const PRETTY_PRINT_TERMINAL_WIDTH = process.stdout.columns
6+
const PRETTY_PRINT_TITLE_WIDTH = 24
7+
8+
const logger = new Logger('cli')
9+
10+
export interface cliOptions {
11+
'--help'?: string
12+
'--log-level'?: string
13+
'--log-stack'?: string
14+
'--log-colorful'?: string
15+
'--log-stringify'?: string
16+
}
17+
18+
// adhere to this and the manual writes itself
19+
interface CommandLineArg {
20+
argument: keyof cliOptions
21+
alias?: string
22+
description: string
23+
type?: string
24+
default?: string
25+
// optional evaluator - default simply returns val
26+
evaluator?: (val?: string) => string | undefined
27+
}
28+
29+
// known command line arguments - the order here defines order of evaluation
30+
const commandLineArgs: CommandLineArg[] = [
31+
{
32+
argument: '--help',
33+
alias: '-h',
34+
description: 'Print this message.',
35+
evaluator: () => {
36+
// extract the argument manual from argument definition
37+
commandLineArgs.forEach((cla) => {
38+
let title = cla.argument
39+
if (cla.alias) title += ', ' + cla.alias
40+
prettyPrintArgDef({
41+
[title]: cla.description,
42+
...(cla.type ? { type: cla.type } : {}),
43+
...(cla.default ? { default: cla.default } : {}),
44+
})
45+
})
46+
return undefined
47+
},
48+
},
49+
{
50+
argument: '--log-level',
51+
description: 'Set log output level.',
52+
type: 'ALL | TRACE | DEBUG | INFO | WARN | ERROR | FATAL | OFF',
53+
default: 'INFO',
54+
},
55+
{
56+
argument: '--log-stack',
57+
description: 'Include logging source (method and file path) in log.',
58+
type: 'false | <any other value>',
59+
default: 'false',
60+
evaluator: (val) => (val === 'false' ? 'false' : 'true'),
61+
},
62+
{
63+
argument: '--log-colorful',
64+
description: 'Enable colorful terminal output for log.',
65+
type: 'false | <any other value>',
66+
default: 'false',
67+
evaluator: (val) => (val === 'false' ? 'false' : 'true'),
68+
},
69+
{
70+
argument: '--log-stringify',
71+
description: 'Output log messages as stringified JSON.',
72+
type: 'false | <any other value>',
73+
default: 'false',
74+
evaluator: (val) => (val === 'false' ? 'false' : 'true'),
75+
},
76+
] as const
77+
78+
// read command line arguments and pass them to their respective evaluators
79+
export function parseCommandLine() {
80+
// turn into map first i.o.t. remove duplicates and let the order be defined
81+
// by consumer (that is, this file)
82+
const args = new Map(
83+
process.argv.slice(2).map((arg) => {
84+
const [key, value] = arg.split('=')
85+
return [key, value] as const
86+
}),
87+
)
88+
const options: cliOptions = {}
89+
commandLineArgs.forEach((cla) => {
90+
let has = false
91+
let val: string | undefined
92+
// check for argument (full) first
93+
if (args.has(cla.argument)) {
94+
has = true
95+
val = args.get(cla.argument)
96+
}
97+
//... and only then check for alias. Otherwise de-duplicating could be circumvented.
98+
else if (cla.alias && args.has(cla.alias)) {
99+
has = true
100+
val = args.get(cla.alias)
101+
}
102+
if (has) {
103+
// evaluate
104+
if (cla.evaluator) {
105+
options[cla.argument] = cla.evaluator(val)
106+
} else {
107+
options[cla.argument] = val
108+
}
109+
// dispose
110+
args.delete(cla.argument)
111+
if (cla.alias) args.delete(cla.alias)
112+
}
113+
})
114+
// if args are left (undeleted) it means undefined args were given
115+
Array.from(args.keys()).forEach((key) => {
116+
logger.error('Invalid command line argument', key)
117+
})
118+
return options
119+
}
120+
121+
function prettyPrintArgDef(def: Record<string, string>) {
122+
// minus 2 for the space between title and definition
123+
const maxTextWidth = Math.max(PRETTY_PRINT_TERMINAL_WIDTH - PRETTY_PRINT_TITLE_WIDTH - 2, 24)
124+
Object.entries(def).forEach(([key, value], idx) => {
125+
const textLines = wrapAnsi(value, maxTextWidth, { hard: true }).split('\n')
126+
// for first pair (argument), color title
127+
const colorStart = idx == 0 ? ansiStyles.color.blue.open : ''
128+
const colorEnd = idx == 0 ? ansiStyles.reset.close : ''
129+
// print property definition
130+
console.log(`${colorStart}${key.padStart(PRETTY_PRINT_TITLE_WIDTH)}${colorEnd} ${textLines[0]}`)
131+
textLines.slice(1).forEach((line) => {
132+
console.log(`${' '.padStart(PRETTY_PRINT_TITLE_WIDTH)} ${line}`)
133+
})
134+
})
135+
// terminate with blank line
136+
console.log()
137+
}

ts/backend/src/app/features/config/config.mts

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
import styles from 'ansi-styles'
21
import JSON5 from 'json5'
32
import fs from 'node:fs'
43
import path from 'node:path'
4+
import { Logger } from '../logger/logger.mjs'
55
import { defaults } from './defaults.mjs'
66

77
export type configuration = typeof defaults
88

9-
const VERBOSE = false
9+
const logger = new Logger('config')
1010

1111
// go through all keys in defaults and copy over values
1212
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1313
function applyDefaults(object: any, defaults: any, path = '') {
1414
for (const key in defaults) {
15-
if (VERBOSE) {
16-
console.log(`${styles.gray.open}checking configuration for ${path}${key}${styles.reset.close}`)
17-
}
15+
logger.trace(`Checking configuration for ${path}${key}`)
1816
// copy missing fields from defaults
1917
if (!Object.prototype.hasOwnProperty.call(object, key)) {
2018
// always deep-copy defaults, prevents changing defaults object via changing loaded config
2119
const defs = JSON.parse(JSON.stringify(defaults[key]))
2220
object[key] = defs
2321
// console.warn does not seem to automatically color the output
24-
console.warn(`${styles.yellow.open}Using defaults for ${path}${key}:${styles.reset.close}`, defs)
22+
logger.warn(`Using defaults for ${path}${key}`, defs)
2523
}
2624
// if both are existing objects, go deeper
2725
else if (typeof object[key] === 'object' && typeof defaults[key] === 'object') {
@@ -38,7 +36,7 @@ export function loadConfig(): configuration {
3836
if (fs.existsSync(confPath)) {
3937
loadedConfig = JSON5.parse<configuration>(fs.readFileSync(confPath, 'utf-8'))
4038
} else {
41-
console.warn(`${styles.yellow.open}No configuration file found under ${confPath}${styles.reset.close}`)
39+
logger.warn(`No configuration file found under ${confPath}`)
4240
}
4341

4442
applyDefaults(loadedConfig, defaults)

0 commit comments

Comments
 (0)