diff --git a/README.md b/README.md index 08e4427..fcbd7e0 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ A lightweight, extensible logging framework for [Office Scripts](https://learn.microsoft.com/en-us/office/dev/scripts/) (ExcelScript), inspired by frameworks like Log4j. Add robust, structured logs to your Excel automations with control over log levels, appenders, and error handling. +This framework ensures compatibility with Office Scripts and Node.js/TypeScript. --- @@ -12,6 +13,9 @@ Add robust, structured logs to your Excel automations with control over log leve - **Pluggable Appenders:** Output logs to the console or Excel cells, or build your own. - **Singleton & Lazy Initialization:** The logger and appenders are created only when first needed. - **TypeScript/Office Scripts Compatible:** Works in both Office Scripts and Node test environments. +- **Configurable Output Layout:** Users can customize the layout of log events sent to the output or use predefined layouts. +- **Log Events Support Extra Fields:** Users can add additional fields to the event, not just the log event message. +- **Support for Log Event Factory:** Extension point in case the user wants to customize how the log events are built. --- @@ -20,31 +24,32 @@ Add robust, structured logs to your Excel automations with control over log leve ### 1. Add the Logger to Your Script **Production use in Office Scripts:** -Copy the contents of `src/logger.ts` into your script in the Office Scripts editor. +Copy the contents of `dist/logger.ts` into your script in the Office Scripts editor. -> No imports or modules are needed. Just paste the code above your `main` function. +> No imports or modules are needed. Just paste the code at the beginning of your `main` function. ### 2. Initialize the Logger (Recommended) ```typescript // Set verbosity level up to INFO events, and continue on error/warning -Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) -Logger.addAppender(ConsoleAppender.getInstance()) // Add console appender +let logger = Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) +logger.addAppender(ConsoleAppender.getInstance()) // Add console appender ``` -> If you skip this step and just call `Logger.info("...")`, the logger will be created with default settings (`WARN` level, `EXIT` action) and a console appender will be used automatically. +> If you skip this step and just call `logger.info("...")`, the logger will be created with default settings (`WARN` level, `EXIT` action) and a console appender will be used automatically. --- ## Basic Usage Examples ```typescript -Logger.info("Script started") // [2025-06-25 23:41:10,585] [INFO] Script started. -Logger.warn("This might be a problem") // [2025-06-25 23:41:10,585] [WARN] This might be a problem. -Logger.error("A fatal error occurred") // [2025-06-25 23:41:10,585] [ERROR] A fatal error occurred -Logger.trace("Step-by-step details") // [2025-06-25 23:41:10,585] [TRACE] Step-by-step details +logger.info("Script started") // [2025-06-25 23:41:10,585] [INFO] Script started. +logger.warn("This might be a problem") // [2025-06-25 23:41:10,585] [WARN] This might be a problem. +logger.error("A fatal error occurred") // [2025-06-25 23:41:10,585] [ERROR] A fatal error occurred +logger.trace("Step-by-step details") // [2025-06-25 23:41:10,585] [TRACE] Step-by-step details ``` > Output as shown above uses the default layout. With the short layout, a timestamp and brackets are excluded. +> The timestamp represents when the log event was created. ### Logging to Excel Cell @@ -54,11 +59,11 @@ Display log output directly in a cell while your script runs: function main(workbook: ExcelScript.Workbook) { // Set up logger to send logs to cell B1 const cell = workbook.getActiveWorksheet().getRange("B1") - Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) - Logger.addAppender(ExcelAppender.getInstance(cell)) + let logger = Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) + logger.addAppender(ExcelAppender.getInstance(cell)) - Logger.info("Log written to Excel!") // Output in cell B1: [2025-06-25 23:41:10,586] [INFO] Log written to Excel! (green text) - Logger.trace("Trace event in cell") // Output in cell B1: [2025-06-25 23:41:10,586] [TRACE] Trace event in cell (gray text) + logger.info("Log written to Excel!") // Output in cell B1: [2025-06-25 23:41:10,586] [INFO] Log written to Excel! (green text) + logger.trace("Trace event in cell") // Output in cell B1: [2025-06-25 23:41:10,586] [TRACE] Trace event in cell (gray text) } ``` @@ -67,9 +72,13 @@ function main(workbook: ExcelScript.Workbook) { ## Lazy Initialization: What You Need to Know - **Logger Singleton:** - - If you call any logging method (e.g., `Logger.info("...")`) before calling `getInstance()`, the Logger will be automatically created with default settings (`WARN` level, `EXIT` action). + - If you call any logging method (e.g., `logger.info("...")`) before calling `getInstance()`, the Logger will be automatically created with default settings (`WARN` level, `EXIT` action). - **Default ConsoleAppender:** - If you log a message and no appender has been added, a `ConsoleAppender` will be automatically created and added to ensure logs are not lost. +- **Default Layout:** + - If the layout was not previously set, then all log events sent to all appenders will use the default layout: `[YYYY-MM-DD hh:mm:ss,XXX] [TYPE] message`, where `XXX` represents milliseconds. The format is set via the `Logger.format(event: LogEvent)` method. +- **Default factory for log event:** + - If the log event factory was not previously set via `AbstractAppender.setLogEventFactory(logEventFactory: LogEventFactory)`, then the log event will be built via: `new LogEventImpl(message: string, type: LOG_EVENT)`. **Summary:** You can start logging immediately, but for best results (and explicit configuration), initialize the Logger and add your desired appenders as shown above. @@ -108,10 +117,10 @@ Set the **maximum verbosity level** of messages to be logged: ### Manage Appenders -- Add: `Logger.addAppender(appender)` -- Remove: `Logger.removeAppender(appender)` -- Replace all: `Logger.setAppenders([appender1, appender2])` -- **Only one of each appender type is allowed; duplicates will throw an error.** +- Add: `logger.addAppender(appender)` +- Remove: `logger.removeAppender(appender)` +- Replace all: `logger.setAppenders([appender1, appender2])` +- **Only one of each appender type is allowed; attempting to add duplicates will throw an error.** - **You cannot add null/undefined as an appender, nor can you provide an array with null/undefined elements.** ### Inspect Logger State @@ -134,7 +143,7 @@ AbstractAppender.clearLayout() AbstractAppender.clearLogEventFactory() ``` > The `clear*` family of methods (`clearInstance`, `clearLayout`, `clearLogEventFactory`) are available in the source files (`src/`). They are omitted from production builds (`dist/`). -> `Logger.reset()` is always available in production and only resets error/warning counters and critical messages, not the logger/appender singletons or layout/factory. +> `logger.reset()` is always available in production and only resets error/warning counters and critical messages, not the logger/appender singletons or layout/factory. --- @@ -143,11 +152,11 @@ AbstractAppender.clearLogEventFactory() You can customize how log messages are formatted or how log events are constructed. This is useful for integrating with other systems, outputting logs in a specific structure (e.g., JSON, XML), or adapting the logger for unique workflows. > **Important:** -> All customization via `AbstractAppender.setLayout()` or `AbstractAppender.setLogEventFactory()` must happen before any logger or appender is initialized or any log event is sent. These setters will not override existing configuration once logging has begun. +> All customization via `AbstractAppender.setLayout()` or `AbstractAppender.setLogEventFactory()` must happen before any logger or appender is initialized or any log event is sent. These setters will not override existing configuration once logging has begun. When a log method is invoked it does lazy initialization for layout and log event factory, when they are required. That is why it is advised to change the configuration before logging begins. ### Customizing Layout (Log Message Format) -The content and structure of log messages sent to appenders are controlled by a `Layout` object. By default, a standard layout is used, but you can inject your own formatting logic **once** at the start of your script. +The content and structure of log messages sent to appenders are controlled by a `LayoutImpl` object. By default, a standard layout is used, but you can inject your own formatting logic **once** at the start of your script. #### Example: Short Layout (No Timestamp) @@ -158,12 +167,12 @@ The content and structure of log messages sent to appenders are controlled by a const shortLayout = new LayoutImpl(LayoutImpl.shortFormatterFun) AbstractAppender.setLayout(shortLayout) -Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) -Logger.addAppender(ConsoleAppender.getInstance()) +let logger = Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) +logger.addAppender(ConsoleAppender.getInstance()) -Logger.info("Script started.") -Logger.warn("This is a warning.") -Logger.error("An error occurred!") +logger.info("Script started.") +logger.warn("This is a warning.") +logger.error("An error occurred!") ``` Sample log output: @@ -180,10 +189,10 @@ Sample log output: const jsonLayout = new LayoutImpl(event => JSON.stringify(event)) AbstractAppender.setLayout(jsonLayout) -Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) -Logger.addAppender(ConsoleAppender.getInstance()) +let logger = Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) +logger.addAppender(ConsoleAppender.getInstance()) -Logger.info("Structured log output") +logger.info("Structured log output") ``` Sample output: ``` @@ -198,13 +207,13 @@ If you want to change all log messages globally (e.g., to add an environment pre ```typescript // Custom LogEventFactory: prefix all messages with [PROD] for production environment -const prodPrefixFactory: LogEventFactory = (msg, type) => new LogEventImpl(`[PROD] ${msg}`, type) +const prodPrefixFactory: LogEventFactory = (msg: string, type: LOG_EVENT) => new LogEventImpl(`[PROD] ${msg}`, type) AbstractAppender.setLogEventFactory(prodPrefixFactory) -Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) -Logger.addAppender(ConsoleAppender.getInstance()) +let logger = Logger.getInstance(Logger.LEVEL.INFO, Logger.ACTION.CONTINUE) +logger.addAppender(ConsoleAppender.getInstance()) -Logger.info("Script started.") +logger.info("Script started.") ``` Sample output (default layout): ``` @@ -214,42 +223,42 @@ Sample output (default layout): --- -### Using `extrafields` for Structured Logging +### Using `extraFields` for Structured Logging -The `extrafields` parameter is an advanced feature allowing you to attach additional structured data to any log event. This is useful for tagging logs with context (like function names, user IDs, or custom metadata) and for downstream integrations (e.g., exporting logs as JSON). +The `extraFields` parameter is an advanced feature allowing you to attach additional structured data to any log event. This is useful for tagging logs with context (like function names, user IDs, or custom metadata) and for downstream integrations (e.g., exporting logs as JSON). -You can pass an object with arbitrary key-value pairs as the `extrafields` argument to any logging method. These fields will be included in the underlying `LogEventImpl` instance and are available in custom layouts, factories, or appenders. +You can pass an object with arbitrary key-value pairs as the `extraFields` argument to any logging method. These fields will be included in the underlying `LogEventImpl` instance and are available in custom layouts, factories, or appenders. #### Example: Adding custom fields to a log entry -Following examples assumed a short layout configuration. +The following examples assume a short layout configuration and that the `logger` variable has already been instantiated. ```typescript -Logger.info("Processing started", { step: "init", user: "alice@example.com" }) -```` -produces the following output: +logger.info("Processing started", { step: "init", user: "alice@example.com" }) ``` -[INFO] Processing started extrafields: { step: "init", user: "alice@example.com" } +Produces the following output: +``` +[INFO] Processing started extraFields: { step: "init", user: "alice@example.com" } ``` and ```typescript -Logger.error("Failed to save", { errorCode: 42, item: "Budget2025" }) +logger.error("Failed to save", { errorCode: 42, item: "Budget2025" }) ``` -produces the following: +Produces the following: ``` -[ERROR] Failed to save (extrafields: { errorCode: 42, item: "Budget2025" }) +[ERROR] Failed to save (extraFields: { errorCode: 42, item: "Budget2025" }) ``` #### How it works -- The third argument to all logging methods is `extrafields`: - - `Logger.info(message, extrafields?)` - - `Logger.warn(message, extrafields?)` - - `Logger.error(message, extrafields?)` - - `Logger.trace(message, extrafields?)` -- `extrafields` can be any object (e.g., `{ key: value, ... }`). -- If you use a custom layout or export logs, you can access these fields from the `LogEventImpl` object. +- The third argument to all logging methods is `extraFields`: + - `Logger.info(message, extraFields?)` + - `Logger.warn(message, extraFields?)` + - `Logger.error(message, extraFields?)` + - `Logger.trace(message, extraFields?)` +- `extraFields` can be any object (e.g., `{ key: value, ... }`). +- If you use a custom layout or export logs, you can access these fields from the `LogEvent` interface. -#### Example: Exporting logs with extrafields +#### Example: Exporting logs with extraFields If you export the logger state then you can iterate over all critical events to get the extra fields: @@ -257,17 +266,17 @@ If you export the logger state then you can iterate over all critical events to const state = logger.exportState() state.criticalEvents.forEach(event => { // event.extraFields will include your custom data if present - console.log(event.extraFields) // Output per iteration for example: { key=1, value='value' } + console.log(event.extraFields) // Output per iteration for example: { key: 1, value: 'value' } }) ``` -Extra fields if present will be part of the `toString` method for the `LogEvent`: +Extra fields, if present, will be part of the `toString()` method for the `LogEvent`: ```typescript -let event = new LogEventImpl("Showing toString", LOG_EVENT.INFO, {user:"admin", sessionId:"123"}); -console.log(`event(extra fields)=${event}`) +let event = new LogEventImpl("Showing toString", LOG_EVENT.INFO, {user: "admin", sessionId: "123"}) +console.log(`event(extra fields)=${event.toString()}`) event = new LogEventImpl("Showing toString", LOG_EVENT.INFO) -console.log(`event=${event}`) +console.log(`event=${event.toString()}`) ``` -and the output will be (firt line the info event and the rest `toString()`) +Here is the `toString()` output (first line: info event with extra fields, second line: without extra fields): ``` event(extra fields)=LogEventImpl: {timestamp="2025-06-19 19:10:34,586", type="INFO", message="Showing toString", extraFields={"user":"admin","sessionId":"123"}} event=LogEventImpl: {timestamp="2025-06-19 19:10:34,586", type="INFO", message="Showing toString"} @@ -286,26 +295,26 @@ function main(workbook: ExcelScript.Workbook) { // AbstractAppender.setLayout(shortLayout) // // Example: Prefix all messages for production - // const prodPrefixFactory: LogEventFactory = (msg, type) => new LogEventImpl(`[PROD] ${msg}`, type) + // const prodPrefixFactory: LogEventFactory = (msg: string, type: LOG_EVENT) => new LogEventImpl(`[PROD] ${msg}`, type) // AbstractAppender.setLogEventFactory(prodPrefixFactory) // Set verbosity up to TRACE and continue on error/warning - Logger.getInstance(Logger.LEVEL.TRACE, Logger.ACTION.CONTINUE) + let logger = Logger.getInstance(Logger.LEVEL.TRACE, Logger.ACTION.CONTINUE) // Add appenders - Logger.addAppender(ConsoleAppender.getInstance()) + logger.addAppender(ConsoleAppender.getInstance()) const logCell = workbook.getActiveWorksheet().getRange("C2") - Logger.addAppender(ExcelAppender.getInstance(logCell)) + logger.addAppender(ExcelAppender.getInstance(logCell)) // Logging (with short layout, output shown as comments): - Logger.info("Script started.") // [INFO] Script started. - Logger.trace("This is a trace message.") // [TRACE] This is a trace message. - Logger.warn("This is a warning.") // [WARN] This is a warning. - Logger.error("This is an error!") // [ERROR] This is an error! (if ACTION.EXIT, aborts script) - - // ExcelAppender outputs in cell C2: - // [INFO] Script started. (green text) - // [TRACE] This is a trace message. (gray text) + logger.info("Script started.") // [INFO] Script started. + logger.trace("This is a trace message.") // [TRACE] This is a trace message. + logger.warn("This is a warning.") // [WARN] This is a warning. + logger.error("This is an error!") // [ERROR] This is an error! (if ACTION.EXIT, aborts script) + + // ExcelAppender outputs in cell C2: with default layout + // [2025-06-26 00:38:10,688] [INFO] Script started (green text) + // [2025-06-26 00:38:10,688] [TRACE] This is a trace message (gray text) } ``` > You can set the layout or log event factory only before any logger or appender is initialized, or before any log event is sent. This ensures consistent formatting and event structure throughout execution. @@ -320,7 +329,7 @@ This framework is designed so that the log message layout and log event factory - For testing, the `clearLayout`, `clearLogEventFactory`, and `clearInstance` methods are available in the source files but are not present in production builds. **Why this design?** -- It keeps the API simple for the common use case and ensures logging behavior is stable and predictable. +- It keeps the API simple for the common use case, and ensures logging behavior is stable and predictable. - Passing these configurations via `getInstance` would require adding extra rarely-used parameters to already overloaded constructors, making the API harder for most users. - If your scenario truly requires dynamic, runtime reconfiguration, you can fork the codebase or adapt it for your specific needs, but for most Office Scripts, stability is preferred. @@ -333,7 +342,7 @@ This framework is designed to work seamlessly in both Node.js/TypeScript environ - Office Scripts (Excel on the web) - **Usage in Office Scripts:** - To use the framework in Office Scripts, paste the source files into your script in the following order: + To use the framework in Office Scripts, just paste the `dist/logger.ts` file at the beginning of your script. If you want to run all tests, then you need to paste the source files into your script in the following order: 1. `test/unit-test-framework.ts` 2. `src/logger.ts` 3. `test/main.ts` @@ -342,6 +351,7 @@ This framework is designed to work seamlessly in both Node.js/TypeScript environ - **Office Scripts Compatibility Adjustments:** - The code avoids unsupported keywords such as `any`, `export`, and `import`. - Office Scripts does not allow calling `ExcelScript` API methods on Office objects inside class constructors; the code is structured to comply with this limitation. + - Office Scripts doesn't allow defining static properties that are functions. For example, `shortFormatterFun` must be defined outside the class. - Additional nuances and workarounds are documented in the source code comments. This ensures the logging framework is robust and reliable across both development and production Office Scripts scenarios. @@ -364,6 +374,7 @@ This ensures the logging framework is robust and reliable across both developmen - **Why do I get a `ScriptError`?** If you send an error or warning log event and `Logger.ACTION.EXIT` is set (and `Logger.LEVEL != LEVEL.OFF`), the logger will throw and abort the script. + `ScriptError` is also thrown for internal errors, such as invalid input arguments or incorrect configuration. - **Why can I only add one of each appender type?** To avoid duplicate logs on the same channel; each appender represents a unique output. @@ -371,8 +382,11 @@ This ensures the logging framework is robust and reliable across both developmen - **Why can't I send a different message to different appenders?** By design, all channels (appenders) receive the same log event message for consistency. +- **Why does the output for ExcelAppender override the previous message?** + By design, the use case for `ExcelAppender` was intended for the default configuration (i.e., a logger with `WARN` level and action `EXIT`). You may want the script to stop if there is any warning or error. Adding more than one event in the same cell (e.g., concatenating via `\n`) is possible, but this defeats the purpose of highlighting each event type with color, since the color will affect the entire cell content. To display each log event in its own cell, you would need to adjust or extend `ExcelAppender`. + - **Why am I getting unexpected results when running some tests in Office Scripts compared to Node.js/TypeScript?** - This can happen because Office Scripts executes code asynchronously, which means some operations (like logging or cell updates) may not complete in strict sequence. As a result, test outcomes may differ from those in Node.js/TypeScript, which runs synchronously and flushes operations immediately. + This can happen because Office Scripts executes code asynchronously, meaning some operations (like logging or cell updates) may not complete in strict sequence. As a result, test outcomes may differ from those in Node.js/TypeScript, which runs synchronously and flushes operations immediately. **Workaround:** To ensure reliable test results in Office Scripts, introduce a delay or use asynchronous test helpers to wait for operations to complete before making assertions. In `test/main.ts`, the `TestCase.runTestAsync` method is used for this purpose. For example: @@ -398,9 +412,10 @@ This ensures the logging framework is robust and reliable across both developmen - For developer setup, testing, or CI details, see [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md) - For debug setup, see [VSCode Debugging.md](docs/VSCode%20Debugging.md) - TypeDoc documentation: [TYPEDOC](docs/typedoc/index.html) +- Git basic documentation: [git-basics](docs/git-basics.md) ## License See [LICENSE](LICENSE) for details. ---- \ No newline at end of file +---