From ccd72e97d5e592f9c4d99789d6822463a786a1cb Mon Sep 17 00:00:00 2001 From: Alexander Streltsov Date: Sun, 23 Nov 2025 23:40:34 +0300 Subject: [PATCH] feat: add configurable log levels in CLI and API - Introduced `--log-level` option in CLI for granular logging control. - Enhanced `CodegenLogger` to support log levels (DEBUG, INFO, WARN, ERROR, SILENT). - Updated APIBuilder to allow log level configuration. --- examples/csharp/generate.ts | 1 - examples/typescript-ccda/generate.ts | 1 - examples/typescript-r4/generate.ts | 1 - examples/typescript-sql-on-fhir/generate.ts | 1 - src/api/builder.ts | 16 ++++----- src/api/index.ts | 1 + src/cli/commands/generate.ts | 2 -- src/cli/commands/index.ts | 39 +++++++++++++++++++-- src/cli/commands/typeschema/generate.ts | 2 -- src/typeschema/generator.ts | 4 +-- src/typeschema/types.ts | 1 - src/utils/codegen-logger.ts | 32 ++++++++++++++--- test/unit/typeschema/utils.ts | 2 +- 13 files changed, 75 insertions(+), 28 deletions(-) diff --git a/examples/csharp/generate.ts b/examples/csharp/generate.ts index ba72428c4..dcda2d088 100644 --- a/examples/csharp/generate.ts +++ b/examples/csharp/generate.ts @@ -4,7 +4,6 @@ if (require.main === module) { console.log("📦 Generating FHIR R4 Core Types..."); const builder = new APIBuilder() - .verbose() .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .csharp("SuperNameSpace", "src/api/writer-generator/csharp/staticFiles") diff --git a/examples/typescript-ccda/generate.ts b/examples/typescript-ccda/generate.ts index fb9f53942..4c801a98c 100644 --- a/examples/typescript-ccda/generate.ts +++ b/examples/typescript-ccda/generate.ts @@ -7,7 +7,6 @@ if (require.main === module) { console.log("📦 Generating FHIR R4 Core Types..."); const builder = new APIBuilder() - .verbose() .throwException() .fromPackage("hl7.cda.uv.core", "2.0.1-sd") .typescript({ withDebugComment: false }) diff --git a/examples/typescript-r4/generate.ts b/examples/typescript-r4/generate.ts index 3b266b097..690e3a964 100644 --- a/examples/typescript-r4/generate.ts +++ b/examples/typescript-r4/generate.ts @@ -7,7 +7,6 @@ if (require.main === module) { console.log("📦 Generating FHIR R4 Core Types..."); const builder = new APIBuilder() - .verbose() .throwException() .fromPackage("hl7.fhir.r4.core", "4.0.1") .typescript({ diff --git a/examples/typescript-sql-on-fhir/generate.ts b/examples/typescript-sql-on-fhir/generate.ts index 9d4934dd4..f5ee4dcad 100644 --- a/examples/typescript-sql-on-fhir/generate.ts +++ b/examples/typescript-sql-on-fhir/generate.ts @@ -1,7 +1,6 @@ import { APIBuilder } from "../../src/api/builder"; const builder = new APIBuilder() - .verbose() .throwException() .typescript({ withDebugComment: false, generateProfile: false }) .fromPackageRef("https://build.fhir.org/ig/FHIR/sql-on-fhir-v2//package.tgz") diff --git a/src/api/builder.ts b/src/api/builder.ts index e40453f9e..999aaeed5 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -15,7 +15,7 @@ import { mkTypeSchemaIndex, type TreeShake, type TypeSchemaIndex, treeShake } fr import { generateTypeSchemas } from "@typeschema/index"; import { extractNameFromCanonical, packageMetaToFhir, packageMetaToNpm, type TypeSchema } from "@typeschema/types"; import type { TypeSchemaConfig } from "../config"; -import { CodegenLogger, createLogger } from "../utils/codegen-logger"; +import { CodegenLogger, createLogger, type LogLevel } from "../utils/codegen-logger"; import { TypeScript, type TypeScriptOptions } from "./writer-generator/typescript"; import type { FileBuffer, FileSystemWriter, WriterOptions } from "./writer-generator/writer"; @@ -24,7 +24,6 @@ import type { FileBuffer, FileSystemWriter, WriterOptions } from "./writer-gener */ export interface APIBuilderOptions { outputDir?: string; - verbose?: boolean; overwrite?: boolean; // FIXME: remove cache?: boolean; // FIXME: remove cleanOutput?: boolean; @@ -35,6 +34,8 @@ export interface APIBuilderOptions { throwException?: boolean; exportTypeTree?: string; treeShake?: TreeShake; + /** Log level for the logger. Default: INFO */ + logLevel?: LogLevel; } /** @@ -70,7 +71,7 @@ export type PartialBy = Omit & Partial>; type APIBuilderConfig = PartialBy< Required, - "logger" | "typeSchemaConfig" | "typeSchemaOutputDir" | "exportTypeTree" | "treeShake" + "logger" | "typeSchemaConfig" | "typeSchemaOutputDir" | "exportTypeTree" | "treeShake" | "logLevel" > & { cleanOutput: boolean; }; @@ -188,7 +189,6 @@ export class APIBuilder { constructor(options: APIBuilderOptions = {}) { this.options = { outputDir: options.outputDir || "./generated", - verbose: options.verbose ?? false, overwrite: options.overwrite ?? true, cache: options.cache ?? true, cleanOutput: options.cleanOutput ?? true, @@ -206,8 +206,8 @@ export class APIBuilder { this.logger = options.logger || createLogger({ - verbose: this.options.verbose, prefix: "API", + level: options.logLevel, }); } @@ -259,7 +259,6 @@ export class APIBuilder { logger: new CodegenLogger({ prefix: "C#", timestamp: true, - verbose: true, suppressLoggingLevel: [], }), }); @@ -291,9 +290,8 @@ export class APIBuilder { return this; } - verbose(enabled = true): APIBuilder { - this.options.verbose = enabled; - this.logger?.configure({ verbose: enabled }); + setLogLevel(level: LogLevel): APIBuilder { + this.logger?.setLevel(level); return this; } diff --git a/src/api/index.ts b/src/api/index.ts index af0d33d8c..f8f29d828 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -7,6 +7,7 @@ * @packageDocumentation */ +export { LogLevel } from "../utils/codegen-logger"; export type { APIBuilderOptions } from "./builder"; export { APIBuilder } from "./builder"; export type { CSharpGeneratorOptions } from "./writer-generator/csharp/csharp"; diff --git a/src/cli/commands/generate.ts b/src/cli/commands/generate.ts index 20537e222..c0231dcf1 100644 --- a/src/cli/commands/generate.ts +++ b/src/cli/commands/generate.ts @@ -73,7 +73,6 @@ export const generateCommand: CommandModule, GenerateArg // Create logger for CLI command const logger = createLogger({ - verbose, prefix: "Generate", }); @@ -90,7 +89,6 @@ export const generateCommand: CommandModule, GenerateArg // Create API builder with config options const builder = new APIBuilder({ outputDir: config.outputDir || "./generated", - verbose, overwrite: config.overwrite ?? true, cache: config.cache ?? true, typeSchemaConfig: config.typeSchema, diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index ba47eda65..f6f644f7d 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -6,7 +6,7 @@ * Modern CLI with subcommands for typeschema and code generation */ -import { configure, error, header } from "@root/utils/codegen-logger"; +import { configure, error, header, LogLevel } from "@root/utils/codegen-logger"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { generateCommand } from "./generate"; @@ -19,16 +19,44 @@ export interface CLIArgv { config?: string; verbose?: boolean; debug?: boolean; + logLevel?: "debug" | "info" | "warn" | "error" | "silent"; +} + +/** + * Map string log level to LogLevel enum + */ +function parseLogLevel(level: string | undefined): LogLevel | undefined { + if (!level) return undefined; + const levelMap: Record = { + debug: LogLevel.DEBUG, + info: LogLevel.INFO, + warn: LogLevel.WARN, + error: LogLevel.ERROR, + silent: LogLevel.SILENT, + }; + return levelMap[level.toLowerCase()]; } /** * Middleware to setup logging */ async function setupLoggingMiddleware(argv: any) { + // Determine log level: explicit --log-level takes precedence over --verbose/--debug + let level = parseLogLevel(argv.logLevel); + + // If no explicit log level, use --verbose or --debug as shortcuts + if (level === undefined) { + if (argv.debug || argv.verbose) { + level = LogLevel.DEBUG; + } else { + level = LogLevel.INFO; + } + } + // Configure the CliLogger with user preferences configure({ - verbose: argv.verbose || argv.debug, timestamp: argv.debug, + level, }); } @@ -56,6 +84,13 @@ export function createCLI() { default: false, global: true, }) + .option("log-level", { + alias: "l", + type: "string", + choices: ["debug", "info", "warn", "error", "silent"] as const, + description: "Set the log level (default: info)", + global: true, + }) .option("config", { alias: "c", type: "string", diff --git a/src/cli/commands/typeschema/generate.ts b/src/cli/commands/typeschema/generate.ts index fd798e8aa..f00d8ffd5 100644 --- a/src/cli/commands/typeschema/generate.ts +++ b/src/cli/commands/typeschema/generate.ts @@ -67,7 +67,6 @@ export const generateTypeschemaCommand: CommandModule, G }, handler: async (argv) => { const logger = createLogger({ - verbose: argv.verbose, prefix: "TypeSchema", }); @@ -103,7 +102,6 @@ export const generateTypeschemaCommand: CommandModule, G // Create TypeSchema generator const generator = new TypeSchemaGenerator({ - verbose: argv.verbose, treeshake: treeshakeOptions, }); diff --git a/src/typeschema/generator.ts b/src/typeschema/generator.ts index 5bd0119f7..3b43ad9e5 100644 --- a/src/typeschema/generator.ts +++ b/src/typeschema/generator.ts @@ -30,16 +30,16 @@ import { export class TypeSchemaGenerator { private manager: ReturnType; + // biome-ignore lint/correctness/noUnusedPrivateClassMembers: currently its okay private options: TypeschemaGeneratorOptions; private logger?: CodegenLogger; constructor(options: TypeschemaGeneratorOptions = {}) { - this.options = { verbose: false, ...options }; + this.options = { ...options }; this.manager = options.manager || CanonicalManager({ packages: [], workingDir: "tmp/fhir" }); this.logger = options.logger || createLogger({ - verbose: this.options.verbose, prefix: "TypeSchema", }); } diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 428d9626b..9b522ec01 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -384,7 +384,6 @@ export const enrichValueSet = (vs: ValueSet, packageMeta: PackageMeta): RichValu /////////////////////////////////////////////////////////// export interface TypeschemaGeneratorOptions { - verbose?: boolean; logger?: import("../utils/codegen-logger").CodegenLogger; treeshake?: string[]; manager?: ReturnType | null; diff --git a/src/utils/codegen-logger.ts b/src/utils/codegen-logger.ts index e5a1285df..2896036b7 100644 --- a/src/utils/codegen-logger.ts +++ b/src/utils/codegen-logger.ts @@ -17,8 +17,9 @@ export enum LogLevel { export interface LogOptions { prefix?: string; timestamp?: boolean; - verbose?: boolean; suppressLoggingLevel?: LogLevel[] | "all"; + /** Minimum log level to display. Messages below this level are suppressed. Default: INFO */ + level?: LogLevel; } /** @@ -31,11 +32,19 @@ export class CodegenLogger { constructor(options: LogOptions = {}) { this.options = { timestamp: false, - verbose: false, + level: LogLevel.INFO, ...options, }; } + /** + * Check if a message at the given level should be logged + */ + private shouldLog(messageLevel: LogLevel): boolean { + const currentLevel = this.options.level ?? LogLevel.INFO; + return messageLevel >= currentLevel; + } + private static consoleLevelsMap: Record void> = { [LogLevel.INFO]: console.log, [LogLevel.WARN]: console.warn, @@ -58,6 +67,7 @@ export class CodegenLogger { private tryWriteToConsole(level: LogLevel, formattedMessage: string): void { if (this.isSuppressed(level)) return; + if (!this.shouldLog(level)) return; const logFn = CodegenLogger.consoleLevelsMap[level] || console.log; logFn(formattedMessage); } @@ -74,8 +84,11 @@ export class CodegenLogger { */ error(message: string, error?: Error): void { if (this.isSuppressed(LogLevel.ERROR)) return; + if (!this.shouldLog(LogLevel.ERROR)) return; console.error(this.formatMessage("X", message, pc.red)); - if (error && this.options.verbose) { + // Show error details if verbose or log level is DEBUG + const showDetails = this.options.level === LogLevel.DEBUG; + if (error && showDetails) { console.error(pc.red(` ${error.message}`)); if (error.stack) { console.error(pc.gray(error.stack)); @@ -105,10 +118,11 @@ export class CodegenLogger { } /** - * Debug message (only shows in verbose mode) + * Debug message (only shows when log level is DEBUG or verbose is true) */ debug(message: string): void { - if (this.options.verbose) { + // Debug shows if verbose is true OR log level allows DEBUG + if (this.shouldLog(LogLevel.DEBUG)) { this.tryWriteToConsole(LogLevel.DEBUG, this.formatMessage("🐛", message, pc.magenta)); } } @@ -159,6 +173,14 @@ export class CodegenLogger { configure(options: Partial): void { this.options = { ...this.options, ...options }; } + + getLevel(): LogLevel { + return this.options.level ?? LogLevel.INFO; + } + + setLevel(level: LogLevel): void { + this.options.level = level; + } } /** diff --git a/test/unit/typeschema/utils.ts b/test/unit/typeschema/utils.ts index a2641ccfc..9275c4703 100644 --- a/test/unit/typeschema/utils.ts +++ b/test/unit/typeschema/utils.ts @@ -13,7 +13,7 @@ import { export type PFS = Partial; export type PVS = Partial; -const logger = createLogger({ verbose: true, prefix: "TEST" }); +const logger = createLogger({ prefix: "TEST" }); export const r4Package = { name: "hl7.fhir.r4.core", version: "4.0.1" };