Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): add configurable logger #147

Merged
merged 15 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 8 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@
"json5": "^2.2.3",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"log4js": "^6.9.1",
"postgres": "^3.4.5",
"redis": "^4.7.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"wrap-ansi": "^9.0.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
Expand Down
25 changes: 24 additions & 1 deletion ts/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ This backend is a Node.js module. The module creates a server application publis

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

Furthermore, run-time behavior can be controlled using command-line arguments when executing `main.min.mjs`:

```terminal
--help, -h Print this message.

--log-level Set log output level.
type ALL | TRACE | DEBUG | INFO | WARN | ERROR | FATAL |
OFF
default INFO

--log-stack Include logging source (method and file path) in log.
type false | <any other value>
default false

--log-colorful Enable colorful terminal output for log.
type false | <any other value>
default false

--log-stringify Output log messages as stringified JSON.
type false | <any other value>
default false
```

## Development server

To start a local development server, run the following command in `ts/backend`:
Expand Down Expand Up @@ -43,4 +66,4 @@ There are no CLI code scaffolding tools for FAB Backend. Manually create `.mts`
* Upload `dist/backend` to web server (e.g. nginx)
* 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.

* Set up your server to execute `node main.mjs` in `app/` (intentionally leaving out further instructions, since this depends on the server in use)
* 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)
2 changes: 1 addition & 1 deletion ts/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"description": "FAB backend",
"scripts": {
"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",
"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",
"build": "npm run private:build && mkdir -p ../../dist/backend/config && cp -r ../../out-tsc/backend-build/app ../../dist/backend",
"private:build": "tsc && tsc-alias && npx webpack"
}
Expand Down
137 changes: 137 additions & 0 deletions ts/backend/src/app/features/config/command-line.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import ansiStyles from 'ansi-styles'
import wrapAnsi from 'wrap-ansi'
import { Logger } from '../logger/logger.mjs'

const PRETTY_PRINT_TERMINAL_WIDTH = process.stdout.columns
const PRETTY_PRINT_TITLE_WIDTH = 24

const logger = new Logger('cli')

export interface cliOptions {
'--help'?: string
'--log-level'?: string
'--log-stack'?: string
'--log-colorful'?: string
'--log-stringify'?: string
}

// adhere to this and the manual writes itself
interface CommandLineArg {
argument: keyof cliOptions
alias?: string
description: string
type?: string
default?: string
// optional evaluator - default simply returns val
evaluator?: (val?: string) => string | undefined
}

// known command line arguments - the order here defines order of evaluation
const commandLineArgs: CommandLineArg[] = [
{
argument: '--help',
alias: '-h',
description: 'Print this message.',
evaluator: () => {
// extract the argument manual from argument definition
commandLineArgs.forEach((cla) => {
let title = cla.argument
if (cla.alias) title += ', ' + cla.alias
prettyPrintArgDef({
[title]: cla.description,
...(cla.type ? { type: cla.type } : {}),
...(cla.default ? { default: cla.default } : {}),
})
})
return undefined
},
},
{
argument: '--log-level',
description: 'Set log output level.',
type: 'ALL | TRACE | DEBUG | INFO | WARN | ERROR | FATAL | OFF',
default: 'INFO',
},
{
argument: '--log-stack',
description: 'Include logging source (method and file path) in log.',
type: 'false | <any other value>',
default: 'false',
evaluator: (val) => (val === 'false' ? 'false' : 'true'),
},
{
argument: '--log-colorful',
description: 'Enable colorful terminal output for log.',
type: 'false | <any other value>',
default: 'false',
evaluator: (val) => (val === 'false' ? 'false' : 'true'),
},
{
argument: '--log-stringify',
description: 'Output log messages as stringified JSON.',
type: 'false | <any other value>',
default: 'false',
evaluator: (val) => (val === 'false' ? 'false' : 'true'),
},
] as const

// read command line arguments and pass them to their respective evaluators
export function parseCommandLine() {
// turn into map first i.o.t. remove duplicates and let the order be defined
// by consumer (that is, this file)
const args = new Map(
process.argv.slice(2).map((arg) => {
const [key, value] = arg.split('=')
return [key, value] as const
}),
)
const options: cliOptions = {}
commandLineArgs.forEach((cla) => {
let has = false
let val: string | undefined
// check for argument (full) first
if (args.has(cla.argument)) {
has = true
val = args.get(cla.argument)
}
//... and only then check for alias. Otherwise de-duplicating could be circumvented.
else if (cla.alias && args.has(cla.alias)) {
has = true
val = args.get(cla.alias)
}
if (has) {
// evaluate
if (cla.evaluator) {
options[cla.argument] = cla.evaluator(val)
} else {
options[cla.argument] = val
}
// dispose
args.delete(cla.argument)
if (cla.alias) args.delete(cla.alias)
}
})
// if args are left (undeleted) it means undefined args were given
Array.from(args.keys()).forEach((key) => {
logger.error('Invalid command line argument', key)
})
return options
}

function prettyPrintArgDef(def: Record<string, string>) {
// minus 2 for the space between title and definition
const maxTextWidth = Math.max(PRETTY_PRINT_TERMINAL_WIDTH - PRETTY_PRINT_TITLE_WIDTH - 2, 24)
Object.entries(def).forEach(([key, value], idx) => {
const textLines = wrapAnsi(value, maxTextWidth, { hard: true }).split('\n')
// for first pair (argument), color title
const colorStart = idx == 0 ? ansiStyles.color.blue.open : ''
const colorEnd = idx == 0 ? ansiStyles.reset.close : ''
// print property definition
console.log(`${colorStart}${key.padStart(PRETTY_PRINT_TITLE_WIDTH)}${colorEnd} ${textLines[0]}`)
textLines.slice(1).forEach((line) => {
console.log(`${' '.padStart(PRETTY_PRINT_TITLE_WIDTH)} ${line}`)
})
})
// terminate with blank line
console.log()
}
12 changes: 5 additions & 7 deletions ts/backend/src/app/features/config/config.mts
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
import styles from 'ansi-styles'
import JSON5 from 'json5'
import fs from 'node:fs'
import path from 'node:path'
import { Logger } from '../logger/logger.mjs'
import { defaults } from './defaults.mjs'

export type configuration = typeof defaults

const VERBOSE = false
const logger = new Logger('config')

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

applyDefaults(loadedConfig, defaults)
Expand Down
Loading