Repository: obsidian-tasks-group/obsidian-tasks
Pull request #969 (2022-08-02 20:28:20)

chore: add logging library for more consistent logging output
# Description

Add logging library to be used to centralize log output and levels in logging. No updates to any other files or main have happened to configure or use this. If this is done this PR will be more than one file as all instances of the console call will be replaced.

## Motivation and Context

Logging output is inconsistent and there is no central way to change the logging level. 

## How has this been tested?

Heavily used in SQL fork.

## Screenshots (if appropriate)

## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] Refactor (non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

## Checklist

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] My change has adequate Unit Test coverage.

By creating a Pull Request you agree to our [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md). For further guidance on contributing please see [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)


claremacrae: In the name of the PR, it says 'more consistent logging output' - what does that mean? Consistent with what, please?
sytone: Consistent in formatting of the output and the way levels are handled, instead of a hard coded trace/debug/etc console call it can be configured up and down. 

Also if anything needed to be changed at a later date it is one place now instead of every console.* having to be updated. 
claremacrae: > Awesome, I am excited about more configurability for logging! I am on mobile, so I have not looked at the code yet, but if I wanted to make a follow-up PR that changed the recent perf-related debug logging to use this logging system, is there an example in here of how I would do that? Thanks!

Thanks, Anna. I also wondered what the code for both setting up logging and using it would look like. That's the kind of thing that is worthwhile having in code docs.

Also Anna FYI, from memory, sytone previously added quite a lot of logging calls to his fork, and so I imagine that he will want to merge some or all of that back in to this project, for code that already exists here.

claremacrae: ## Summary

This is all good. I will then:

- [ ] merge it
- [ ] fix the plugin name (in 2 places)
- [ ] add code to `main.ts` to hook it up
- [ ] and then add what I learned to the docs.

## Trying it out

I had a go at using the logging code, by copying in code from tasks-x.

Here is what I added:

```diff
diff --git a/src/main.ts b/src/main.ts
index 2df6080..ac39f9f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -10,6 +10,7 @@ import { QueryRenderer } from './QueryRenderer';
 import { getSettings, updateSettings } from './config/Settings';
 import { SettingsTab } from './config/SettingsTab';
 import { EditorSuggestor } from './Suggestor/EditorSuggestorPopup';
+import { log, logging } from './lib/logging';
 
 export default class TasksPlugin extends Plugin {
     private cache: Cache | undefined;
@@ -17,7 +18,20 @@ export default class TasksPlugin extends Plugin {
     public queryRenderer: QueryRenderer | undefined;
 
     async onload() {
-        console.log('loading plugin "tasks"');
+        logging
+            .configure({
+                minLevels: {
+                    '': 'debug',
+                    taskssql: 'debug',
+                    'taskssql.perf': 'debug',
+                    'taskssql.QuerySql.QuerySql': 'debug',
+                },
+            })
+            .registerConsoleLogger();
+        log(
+            'info',
+            `loading plugin "${this.manifest.name}" v${this.manifest.version}`,
+        );
 
         await this.loadSettings();
         this.addSettingTab(new SettingsTab({ plugin: this }));
@@ -44,7 +58,10 @@ export default class TasksPlugin extends Plugin {
     }
 
     onunload() {
-        console.log('unloading plugin "tasks"');
+        log(
+            'info',
+            `unloading plugin "${this.manifest.name}" v${this.manifest.version}`,
+        );
         this.cache?.unload();
     }
```

Firstly, it's really nice to know how to access the plugin version number - thank you.

This was the output:

```
[20220803104521][info][taskssql] loading plugin "Tasks" v1.11.0 
```

With all the work being done on performance improvements in Tasks, over the next few releases, I'm going to be having users look at the console a lot.

I can see how to fix the `[taskssql]` bit, and seeing the code, it's clear what is going on.

## Controlling log levels

I added a line of debug output:

```typescript
        log('debug', 'wibble - debug');
```

It was displayed in the console.

Then I changed the initialisation code like this:

```diff
        logging
            .configure({
                minLevels: {
-                     '': 'debug',
+                     '': 'info',
                    taskssql: 'debug',
                    'taskssql.perf': 'debug',
                    'taskssql.QuerySql.QuerySql': 'debug',
                },
            })
            .registerConsoleLogger();
```

And then the debug log message was not written out.



Patch:
diff --git a/docs/Gemfile b/docs/Gemfile
index 32558f30..4beb1cfd 100644
--- a/docs/Gemfile
+++ b/docs/Gemfile
@@ -12,16 +12,16 @@ source "https://rubygems.org"
 #gem "minima", "~> 2.5"
 # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
 # uncomment the line below. To upgrade, run `bundle update github-pages`.
-gem "github-pages", "~> 219",  group: :jekyll_plugins
+gem "github-pages", "~> 227",  group: :jekyll_plugins
 # If you have any plugins, put them here!
 group :jekyll_plugins do
-  gem "jekyll-feed", "~> 0.12"
+  gem "jekyll-feed", "~> 0.15"
 end
 
 # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
 # and associated library.
 platforms :mingw, :x64_mingw, :mswin, :jruby do
-  gem "tzinfo", "~> 1.2"
+  gem "tzinfo", "~> 2.0.5"
   gem "tzinfo-data"
 end
 
diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock
index c782ed37..542d72bc 100644
--- a/docs/Gemfile.lock
+++ b/docs/Gemfile.lock
@@ -1,7 +1,7 @@
 GEM
   remote: https://rubygems.org/
   specs:
-    activesupport (6.0.4.1)
+    activesupport (6.0.5.1)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (>= 0.7, < 2)
       minitest (~> 5.1)
@@ -14,50 +14,35 @@ GEM
       execjs
     coffee-script-source (1.11.1)
     colorator (1.1.0)
-    commonmarker (0.23.4)
-      ruby-enum (~> 0.5)
-    concurrent-ruby (1.1.9)
-    dnsruby (1.61.7)
+    commonmarker (0.23.5)
+    concurrent-ruby (1.1.10)
+    dnsruby (1.61.9)
       simpleidn (~> 0.1)
-    em-websocket (0.5.2)
+    em-websocket (0.5.3)
       eventmachine (>= 0.12.9)
-      http_parser.rb (~> 0.6.0)
+      http_parser.rb (~> 0)
     ethon (0.15.0)
       ffi (>= 1.15.0)
     eventmachine (1.2.7)
     execjs (2.8.1)
-    faraday (1.8.0)
-      faraday-em_http (~> 1.0)
-      faraday-em_synchrony (~> 1.0)
-      faraday-excon (~> 1.1)
-      faraday-httpclient (~> 1.0.1)
-      faraday-net_http (~> 1.0)
-      faraday-net_http_persistent (~> 1.1)
-      faraday-patron (~> 1.0)
-      faraday-rack (~> 1.0)
-      multipart-post (>= 1.2, < 3)
+    faraday (2.4.0)
+      faraday-net_http (~> 2.0)
       ruby2_keywords (>= 0.0.4)
-    faraday-em_http (1.0.0)
-    faraday-em_synchrony (1.0.0)
-    faraday-excon (1.1.0)
-    faraday-httpclient (1.0.1)
-    faraday-net_http (1.0.1)
-    faraday-net_http_persistent (1.2.0)
-    faraday-patron (1.0.0)
-    faraday-rack (1.0.0)
-    ffi (1.15.4)
+    faraday-net_http (2.1.0)
+    ffi (1.15.5)
     forwardable-extended (2.6.0)
     gemoji (3.0.1)
-    github-pages (219)
-      github-pages-health-check (= 1.17.7)
-      jekyll (= 3.9.0)
+    github-pages (227)
+      github-pages-health-check (= 1.17.9)
+      jekyll (= 3.9.2)
       jekyll-avatar (= 0.7.0)
       jekyll-coffeescript (= 1.1.1)
-      jekyll-commonmark-ghpages (= 0.1.6)
+      jekyll-commonmark-ghpages (= 0.2.0)
       jekyll-default-layout (= 0.1.4)
       jekyll-feed (= 0.15.1)
       jekyll-gist (= 1.5.0)
       jekyll-github-metadata (= 2.13.0)
+      jekyll-include-cache (= 0.2.1)
       jekyll-mentions (= 1.6.0)
       jekyll-optional-front-matter (= 0.3.2)
       jekyll-paginate (= 1.1.0)
@@ -66,7 +51,7 @@ GEM
       jekyll-relative-links (= 0.6.1)
       jekyll-remote-theme (= 0.4.3)
       jekyll-sass-converter (= 1.5.2)
-      jekyll-seo-tag (= 2.7.1)
+      jekyll-seo-tag (= 2.8.0)
       jekyll-sitemap (= 1.4.0)
       jekyll-swiss (= 1.0.0)
       jekyll-theme-architect (= 0.2.0)
@@ -84,27 +69,27 @@ GEM
       jekyll-theme-time-machine (= 0.2.0)
       jekyll-titles-from-headings (= 0.5.3)
       jemoji (= 0.12.0)
-      kramdown (= 2.3.1)
+      kramdown (= 2.3.2)
       kramdown-parser-gfm (= 1.1.0)
       liquid (= 4.0.3)
       mercenary (~> 0.3)
       minima (= 2.5.1)
-      nokogiri (>= 1.10.4, < 2.0)
+      nokogiri (>= 1.13.6, < 2.0)
       rouge (= 3.26.0)
       terminal-table (~> 1.4)
-    github-pages-health-check (1.17.7)
+    github-pages-health-check (1.17.9)
       addressable (~> 2.3)
       dnsruby (~> 1.60)
       octokit (~> 4.0)
       public_suffix (>= 3.0, < 5.0)
       typhoeus (~> 1.3)
-    html-pipeline (2.14.0)
+    html-pipeline (2.14.2)
       activesupport (>= 2)
       nokogiri (>= 1.4)
-    http_parser.rb (0.6.0)
+    http_parser.rb (0.8.0)
     i18n (0.9.5)
       concurrent-ruby (~> 1.0)
-    jekyll (3.9.0)
+    jekyll (3.9.2)
       addressable (~> 2.4)
       colorator (~> 1.0)
       em-websocket (~> 0.5)
@@ -122,12 +107,12 @@ GEM
     jekyll-coffeescript (1.1.1)
       coffee-script (~> 2.2)
       coffee-script-source (~> 1.11.1)
-    jekyll-commonmark (1.3.1)
-      commonmarker (~> 0.14)
-      jekyll (>= 3.7, < 5.0)
-    jekyll-commonmark-ghpages (0.1.6)
-      commonmarker (~> 0.17.6)
-      jekyll-commonmark (~> 1.2)
+    jekyll-commonmark (1.4.0)
+      commonmarker (~> 0.22)
+    jekyll-commonmark-ghpages (0.2.0)
+      commonmarker (~> 0.23.4)
+      jekyll (~> 3.9.0)
+      jekyll-commonmark (~> 1.4.0)
       rouge (>= 2.0, < 4.0)
     jekyll-default-layout (0.1.4)
       jekyll (~> 3.0)
@@ -138,6 +123,8 @@ GEM
     jekyll-github-metadata (2.13.0)
       jekyll (>= 3.4, < 5.0)
       octokit (~> 4.0, != 4.4.0)
+    jekyll-include-cache (0.2.1)
+      jekyll (>= 3.7, < 5.0)
     jekyll-mentions (1.6.0)
       html-pipeline (~> 2.3)
       jekyll (>= 3.7, < 5.0)
@@ -157,7 +144,7 @@ GEM
       rubyzip (>= 1.3.0, < 3.0)
     jekyll-sass-converter (1.5.2)
       sass (~> 3.4)
-    jekyll-seo-tag (2.7.1)
+    jekyll-seo-tag (2.8.0)
       jekyll (>= 3.8, < 5.0)
     jekyll-sitemap (1.4.0)
       jekyll (>= 3.7, < 5.0)
@@ -210,41 +197,34 @@ GEM
       gemoji (~> 3.0)
       html-pipeline (~> 2.2)
       jekyll (>= 3.0, < 5.0)
-    kramdown (2.3.1)
+    kramdown (2.3.2)
       rexml
     kramdown-parser-gfm (1.1.0)
       kramdown (~> 2.0)
     liquid (4.0.3)
-    listen (3.7.0)
+    listen (3.7.1)
       rb-fsevent (~> 0.10, >= 0.10.3)
       rb-inotify (~> 0.9, >= 0.9.10)
     mercenary (0.3.6)
-    mini_portile2 (2.8.0)
     minima (2.5.1)
       jekyll (>= 3.5, < 5.0)
       jekyll-feed (~> 0.9)
       jekyll-seo-tag (~> 2.1)
-    minitest (5.14.4)
-    multipart-post (2.1.1)
-    nokogiri (1.13.6)
-      mini_portile2 (~> 2.8.0)
-      racc (~> 1.4)
-    nokogiri (1.13.6-x86_64-linux)
+    minitest (5.16.2)
+    nokogiri (1.13.8)
       racc (~> 1.4)
-    octokit (4.21.0)
-      faraday (>= 0.9)
-      sawyer (~> 0.8.0, >= 0.5.3)
+    octokit (4.25.1)
+      faraday (>= 1, < 3)
+      sawyer (~> 0.9)
     pathutil (0.16.2)
       forwardable-extended (~> 2.6)
-    public_suffix (4.0.6)
+    public_suffix (4.0.7)
     racc (1.6.0)
-    rb-fsevent (0.11.0)
+    rb-fsevent (0.11.1)
     rb-inotify (0.10.1)
       ffi (~> 1.0)
     rexml (3.2.5)
     rouge (3.26.0)
-    ruby-enum (0.9.0)
-      i18n
     ruby2_keywords (0.0.5)
     rubyzip (2.3.2)
     safe_yaml (1.0.5)
@@ -253,9 +233,9 @@ GEM
     sass-listen (4.0.0)
       rb-fsevent (~> 0.9, >= 0.9.4)
       rb-inotify (~> 0.9, >= 0.9.7)
-    sawyer (0.8.2)
+    sawyer (0.9.2)
       addressable (>= 2.3.5)
-      faraday (> 0.8, < 2.0)
+      faraday (>= 0.17.3, < 3)
     simpleidn (0.2.1)
       unf (~> 0.1.4)
     terminal-table (1.8.0)
@@ -263,24 +243,23 @@ GEM
     thread_safe (0.3.6)
     typhoeus (1.4.0)
       ethon (>= 0.9.0)
-    tzinfo (1.2.9)
+    tzinfo (1.2.10)
       thread_safe (~> 0.1)
     unf (0.1.4)
       unf_ext
-    unf_ext (0.0.8)
+    unf_ext (0.0.8.2)
     unicode-display_width (1.8.0)
-    zeitwerk (2.5.1)
+    zeitwerk (2.6.0)
 
 PLATFORMS
   ruby
-  x86_64-linux
 
 DEPENDENCIES
-  github-pages (~> 219)
-  jekyll-feed (~> 0.12)
-  tzinfo (~> 1.2)
+  github-pages (~> 227)
+  jekyll-feed (~> 0.15)
+  tzinfo (~> 2.0.5)
   tzinfo-data
   wdm (~> 0.1.1)
 
 BUNDLED WITH
-   2.2.30
+   2.3.19
diff --git a/package.json b/package.json
index 9280cfe0..4cd4d841 100644
--- a/package.json
+++ b/package.json
@@ -27,13 +27,13 @@
     "@codemirror/view": "^6.1.2",
     "@tsconfig/svelte": "^3.0.0",
     "@types/jest": "^28.1.6",
-    "@typescript-eslint/eslint-plugin": "^5.31.0",
-    "@typescript-eslint/parser": "^5.31.0",
+    "@typescript-eslint/eslint-plugin": "^5.32.0",
+    "@typescript-eslint/parser": "^5.32.0",
     "async-mutex": "^0.3.1",
     "builtin-modules": "^3.3.0",
     "esbuild": "0.14.51",
     "esbuild-svelte": "^0.7.1",
-    "eslint": "^8.20.0",
+    "eslint": "^8.21.0",
     "eslint-config-prettier": "^8.5.0",
     "eslint-plugin-import": "^2.22.1",
     "eslint-plugin-prettier": "^4.2.1",
@@ -54,6 +54,7 @@
   "dependencies": {
     "boon-js": "^2.0.3",
     "chrono-node": "2.3.9",
+    "eventemitter2": "^6.4.5",
     "rrule": "^2.7.1"
   }
-}
+}
\ No newline at end of file
diff --git a/resources/sample_vaults/Tasks-Demo/Filters/Boolean Combinations.md b/resources/sample_vaults/Tasks-Demo/Filters/Boolean Combinations.md
index 30b414ce..3016ca85 100644
--- a/resources/sample_vaults/Tasks-Demo/Filters/Boolean Combinations.md
+++ b/resources/sample_vaults/Tasks-Demo/Filters/Boolean Combinations.md
@@ -1,5 +1,10 @@
 # Boolean Combinations
 
+This file contains a few examples that were useful when testing
+the feature to allow Boolean combining of filters.
+
+Full documentation is available: see [Combining Filters](https://obsidian-tasks-group.github.io/obsidian-tasks/queries/combining-filters/).
+
 ## Sample tasks for the searches below
 
 - [ ] #task task 1 #XX
diff --git a/src/Task.ts b/src/Task.ts
index 4365d24b..436090d6 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -867,7 +867,20 @@ export class Task {
      * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
      */
     private escapeRegExp(s: string) {
-        return s.replace(/([.*+?^=!:${}()|[]\/\\])/g, '\\$1');
+        // NOTE: = is not escaped, as doing so gives error:
+        //         Invalid regular expression: /(^|\s)hello\=world($|\s)/: Invalid escape
+        // NOTE: ! is not escaped, as doing so gives error:
+        //         Invalid regular expression: /(^|\s)hello\!world($|\s)/: Invalid escape
+        // NOTE: : is not escaped, as doing so gives error:
+        //         Invalid regular expression: /(^|\s)hello\:world($|\s)/: Invalid escape
+        //
+        // Explanation from @AnnaKornfeldSimpson in:
+        // https://github.com/esm7/obsidian-tasks/pull/18#issuecomment-1196115407
+        // From what I can tell, the three missing characters from the original regex - : ! =
+        // are all only considered to have special meanings if they directly follow
+        // a ? (all 3) or a ?< (! and =).
+        // So theoretically if the ? are all escaped, those three characters do not have to be.
+        return s.replace(/([.*+?^${}()|[\]/\\])/g, '\\$1');
     }
 
     /**
diff --git a/src/lib/logging.ts b/src/lib/logging.ts
new file mode 100644
index 00000000..6acdb218
--- /dev/null
+++ b/src/lib/logging.ts
@@ -0,0 +1,453 @@
+import moment from 'moment';
+import { Platform, Plugin } from 'obsidian';
+/*
+ * EventEmitter2 is an implementation of the EventEmitter module found in Node.js.
+ * In addition to having a better benchmark performance than EventEmitter and being
+ * browser-compatible, it also extends the interface of EventEmitter with many
+ * additional non-breaking features.
+ *
+ * This has been added as EventEmitter in Node.JS is not available in the browser.
+ * https://www.npmjs.com/package/eventemitter2
+ */
+import { EventEmitter2 } from 'eventemitter2';
+
+/**
+ * All possible log levels
+ * @public
+ */
+export interface ILogLevel {
+    1: 'trace';
+    2: 'debug';
+    3: 'info';
+    4: 'warn';
+    5: 'error';
+}
+
+/**
+ * Logger class to handle consistency of logs across the plugin.
+ *
+ * @export
+ * @interface LogEntry
+ */
+export interface LogEntry {
+    traceId?: string;
+    level: string;
+    module: string;
+    location?: string;
+    message: string;
+    objects: any;
+}
+
+/**
+ * Logging options structure.
+ *
+ * @export
+ * @interface LogOptions
+ */
+export interface LogOptions {
+    minLevels: { [module: string]: string };
+}
+
+/**
+ * Log level IDs (1 - 5)
+ * @public
+ */
+export type TLogLevelId = keyof ILogLevel;
+
+/**
+ * Log level names (trace - error)
+ * @public
+ */
+export type TLogLevelName = ILogLevel[TLogLevelId];
+
+/**
+ * Logger class to handle consistency of logs across the plugin.
+ *
+ * @export
+ * @class LogManager
+ * @extends {EventEmitter2}
+ */
+export class LogManager extends EventEmitter2 {
+    private options: LogOptions = {
+        minLevels: {
+            '': 'info',
+            tasks: 'info',
+        },
+    };
+
+    // Prevent the console logger from being added twice
+    private consoleLoggerRegistered: boolean = false;
+
+    /**
+     * Set the minimum log levels for the module name or global.
+     *
+     * @param {LogOptions} options
+     * @return {*}  {LogManager}
+     * @memberof LogManager
+     */
+    public configure(options: LogOptions): LogManager {
+        this.options = Object.assign({}, this.options, options);
+        return this;
+    }
+
+    /**
+     * Returns a logger instance for the given module name.
+     *
+     * @param {string} module
+     * @return {*}  {Logger}
+     * @memberof LogManager
+     */
+    public getLogger(module: string): Logger {
+        let minLevel = 'none';
+        let match = '';
+
+        for (const key in this.options.minLevels) {
+            if (module.startsWith(key) && key.length >= match.length) {
+                minLevel = this.options.minLevels[key];
+                match = key;
+            }
+        }
+        return new Logger(this, module, minLevel);
+    }
+
+    /**
+     *
+     *
+     * @param {(logEntry: LogEntry) => void} listener
+     * @return {*}  {LogManager}
+     * @memberof LogManager
+     */
+    public onLogEntry(listener: (logEntry: LogEntry) => void): LogManager {
+        this.on('log', listener);
+        return this;
+    }
+
+    // private period: number = 0;
+    arrAvg = (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length;
+
+    /**
+     * Registers a logger that write to the console.
+     *
+     * @return {*}  {LogManager}
+     * @memberof LogManager
+     */
+    public registerConsoleLogger(): LogManager {
+        if (this.consoleLoggerRegistered) return this;
+
+        this.onLogEntry((logEntry) => {
+            let msg = `[${moment().format('YYYYMMDDHHmmss')}][${
+                logEntry.level
+            }][${logEntry.module}]`;
+
+            if (logEntry.traceId) {
+                msg += `[${logEntry.traceId}]`;
+            }
+
+            msg += ` ${logEntry.message}`;
+            if (logEntry.objects === undefined) {
+                logEntry.objects = '';
+            }
+
+            switch (logEntry.level) {
+                case 'trace':
+                    console.trace(msg, logEntry.objects);
+                    break;
+                case 'debug':
+                    console.debug(msg, logEntry.objects);
+                    break;
+                case 'info':
+                    console.info(msg, logEntry.objects);
+                    break;
+                case 'warn':
+                    console.warn(msg, logEntry.objects);
+                    break;
+                case 'error':
+                    console.error(msg, logEntry.objects);
+                    break;
+                default:
+                    console.log(`{${logEntry.level}} ${msg}`, logEntry.objects);
+            }
+        });
+
+        this.consoleLoggerRegistered = true;
+        return this;
+    }
+}
+
+export const logging = new LogManager();
+
+/**
+ * Main logging library, to view the logs a logger listener must be added. The
+ * Console Logger is already implemented for this project.
+ *
+ * @export
+ * @class Logger
+ */
+export class Logger {
+    private logManager: EventEmitter2;
+    private minLevel: number;
+    private module: string;
+    private readonly levels: { [key: string]: number } = {
+        trace: 1,
+        debug: 2,
+        info: 3,
+        warn: 4,
+        error: 5,
+    };
+
+    /**
+     * Creates an instance of Logger.
+     * @param {EventEmitter2} logManager
+     * @param {string} module
+     * @param {string} minLevel
+     * @memberof Logger
+     */
+    constructor(logManager: EventEmitter2, module: string, minLevel: string) {
+        this.logManager = logManager;
+        this.module = module;
+        this.minLevel = this.levelToInt(minLevel);
+    }
+
+    /**
+     * Converts a string level (trace/debug/info/warn/error) into a number
+     *
+     * @param minLevel
+     */
+    private levelToInt(minLevel: string): number {
+        if (minLevel.toLowerCase() in this.levels)
+            return this.levels[minLevel.toLowerCase()];
+        else return 99;
+    }
+
+    /**
+     * Central logging method.
+     * @param logLevel
+     * @param message
+     */
+    public log(logLevel: string, message: string, objects?: any): void {
+        const level = this.levelToInt(logLevel);
+        if (level < this.minLevel) return;
+
+        const logEntry: LogEntry = {
+            level: logLevel,
+            module: this.module,
+            message,
+            objects,
+            traceId: undefined,
+        };
+
+        // Obtain the line/file through a thoroughly hacky method
+        // This creates a new stack trace and pulls the caller from it.  If the caller
+        // if .trace()
+        // const error = new Error('');
+        // if (error.stack) {
+        //     const cla = error.stack.split('\n');
+        //     let idx = 1;
+        //     while (idx < cla.length && cla[idx].includes('at Logger.Object.')) idx++;
+        //     if (idx < cla.length) {
+        //         logEntry.location = cla[idx].slice(cla[idx].indexOf('at ') + 3, cla[idx].length);
+        //     }
+        // }
+
+        this.logManager.emit('log', logEntry);
+    }
+
+    public trace(message: string, objects?: any): void {
+        this.log('trace', message, objects);
+    }
+    public debug(message: string, objects?: any): void {
+        this.log('debug', message, objects);
+    }
+    public info(message: string, objects?: any): void {
+        this.log('info', message, objects);
+    }
+    public warn(message: string, objects?: any): void {
+        this.log('warn', message, objects);
+    }
+    public error(message: string, objects?: any): void {
+        this.log('error', message, objects);
+    }
+
+    /**
+     * Central logging method with a trace ID to track calls between modules/components.
+     * @param logLevel
+     * @param message
+     */
+    public logWithId(
+        logLevel: string,
+        traceId: string,
+        message: string,
+        objects?: any,
+    ): void {
+        const level = this.levelToInt(logLevel);
+        if (level < this.minLevel) return;
+
+        const logEntry: LogEntry = {
+            level: logLevel,
+            module: this.module,
+            message,
+            objects,
+            traceId,
+        };
+
+        this.logManager.emit('log', logEntry);
+    }
+
+    public traceWithId(traceId: string, message: string, objects?: any): void {
+        this.logWithId('trace', traceId, message, objects);
+    }
+    public debugWithId(traceId: string, message: string, objects?: any): void {
+        this.logWithId('debug', traceId, message, objects);
+    }
+    public infoWithId(traceId: string, message: string, objects?: any): void {
+        this.logWithId('info', traceId, message, objects);
+    }
+    public warnWithId(traceId: string, message: string, objects?: any): void {
+        this.logWithId('warn', traceId, message, objects);
+    }
+    public errorWithId(traceId: string, message: string, objects?: any): void {
+        this.logWithId('error', traceId, message, objects);
+    }
+}
+
+type TimingMap = {
+    // count, avg, min, max
+    [id: string]: number[];
+};
+
+const timingMap: TimingMap = {};
+
+/**
+ * This deceleration will log the time taken to run the function it is attached to. Be
+ * careful where it is added as it increases the output.
+ *
+ * @export
+ * @return {*}
+ */
+export const logCall = (
+    target: Object,
+    propertyKey: string,
+    descriptor: PropertyDescriptor,
+) => {
+    const originalMethod = descriptor.value;
+    //const logger = logging.getLogger('taskssql.perf');
+    descriptor.value = function (...args: any[]) {
+        const startTime = new Date(Date.now());
+        const result = originalMethod.apply(this, args);
+        const endTime = new Date(Date.now());
+        const name = `${target?.constructor?.name}${propertyKey}`;
+        const time = endTime.getTime() - startTime.getTime();
+        if (timingMap[name] === undefined) {
+            timingMap[name] = [];
+        }
+        timingMap[name].push(time);
+
+        //console.log(timingMap);
+        // if (endTime.getTime() - startTime.getTime() > 50) {
+        //     console.debug(
+        //         `[debug][taskssql.perf] ${String(timingMap[name].avg).padEnd(4)}${String(
+        //             endTime.getTime() - startTime.getTime(),
+        //         ).padEnd(4)} ${target?.constructor?.name.padEnd(10)}${propertyKey.padEnd(20)}`,
+        //     );
+
+        //     // logger.debug(
+        //     //     `${target?.constructor?.name}:${propertyKey}:called with ${args.length} arguments. Took: ${
+        //     //         endTime.getTime() - startTime.getTime()
+        //     //     }ms`,
+        //     // );
+        // }
+        return result;
+    };
+
+    return descriptor;
+};
+
+export function logCallDetails() {
+    return function (
+        target: any,
+        propertyKey: string,
+        descriptor: PropertyDescriptor,
+    ) {
+        const originalMethod = descriptor.value;
+        const logger = logging.getLogger('taskssql');
+
+        descriptor.value = async function (...args: any[]) {
+            const startTime = new Date(Date.now());
+            const result = await originalMethod.apply(this, args);
+            const endTime = new Date(Date.now());
+            const elapsed = endTime.getTime() - startTime.getTime();
+
+            logger.debug(
+                `${typeof target}:${propertyKey} called with ${
+                    args.length
+                } arguments. Took: ${elapsed}ms ${JSON.stringify(args)}`,
+            );
+            return result;
+        };
+        return descriptor;
+    };
+}
+
+/**
+ * Provides a simple log function that can be used to log messages against default module.
+ *
+ * @export
+ * @param {TLogLevelName} logLevel
+ * @param {string} message
+ */
+export function log(logLevel: TLogLevelName, message: string) {
+    const logger = logging.getLogger('taskssql');
+
+    switch (logLevel) {
+        case 'trace':
+            logger.trace(message);
+            break;
+        case 'debug':
+            logger.debug(message);
+            break;
+        case 'info':
+            logger.info(message);
+            break;
+        case 'warn':
+            logger.warn(message);
+            break;
+        case 'error':
+            logger.error(message);
+            break;
+        default:
+            break;
+    }
+}
+
+/**
+ * This allows the plugin to be debugged in a mobile application
+ * add it when debugging on a device. Not meant to be used by
+ * end users. Add it into main.ts and remove before you commit.
+ *
+ * @export
+ * @param {Plugin} plugin
+ * @return {*}
+ */
+export function monkeyPatchConsole(plugin: Plugin) {
+    if (!Platform.isMobile) {
+        return;
+    }
+
+    const logFile = `${plugin.manifest.dir}/tasks-sql-logs.txt`;
+    const logs: string[] = [];
+    const logMessages =
+        (prefix: string) =>
+        (...messages: unknown[]) => {
+            logs.push(`\n[${prefix}]`);
+            for (const message of messages) {
+                logs.push(String(message));
+            }
+            plugin.app.vault.adapter.write(logFile, logs.join(' '));
+        };
+
+    console.debug = logMessages('debug');
+    console.error = logMessages('error');
+    console.info = logMessages('info');
+    console.log = logMessages('log');
+    console.warn = logMessages('warn');
+}
diff --git a/yarn.lock b/yarn.lock
index 54ccb746..a0da2e6a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -547,15 +547,20 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@humanwhocodes/config-array@^0.9.2":
-  version "0.9.5"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"
-  integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==
+"@humanwhocodes/config-array@^0.10.4":
+  version "0.10.4"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c"
+  integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==
   dependencies:
     "@humanwhocodes/object-schema" "^1.2.1"
     debug "^4.1.1"
     minimatch "^3.0.4"
 
+"@humanwhocodes/gitignore-to-minimatch@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d"
+  integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==
+
 "@humanwhocodes/object-schema@^1.2.1":
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
@@ -1014,14 +1019,14 @@
   dependencies:
     "@types/yargs-parser" "*"
 
-"@typescript-eslint/eslint-plugin@^5.31.0":
-  version "5.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.31.0.tgz#cae1967b1e569e6171bbc6bec2afa4e0c8efccfe"
-  integrity sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==
+"@typescript-eslint/eslint-plugin@^5.32.0":
+  version "5.32.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.32.0.tgz#e27e38cffa4a61226327c874a7be965e9a861624"
+  integrity sha512-CHLuz5Uz7bHP2WgVlvoZGhf0BvFakBJKAD/43Ty0emn4wXWv5k01ND0C0fHcl/Im8Td2y/7h44E9pca9qAu2ew==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.31.0"
-    "@typescript-eslint/type-utils" "5.31.0"
-    "@typescript-eslint/utils" "5.31.0"
+    "@typescript-eslint/scope-manager" "5.32.0"
+    "@typescript-eslint/type-utils" "5.32.0"
+    "@typescript-eslint/utils" "5.32.0"
     debug "^4.3.4"
     functional-red-black-tree "^1.0.1"
     ignore "^5.2.0"
@@ -1029,69 +1034,69 @@
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/parser@^5.31.0":
-  version "5.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.31.0.tgz#7f42d7dcc68a0a6d80a0f3d9a65063aee7bb8d2c"
-  integrity sha512-UStjQiZ9OFTFReTrN+iGrC6O/ko9LVDhreEK5S3edmXgR396JGq7CoX2TWIptqt/ESzU2iRKXAHfSF2WJFcWHw==
+"@typescript-eslint/parser@^5.32.0":
+  version "5.32.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.32.0.tgz#1de243443bc6186fb153b9e395b842e46877ca5d"
+  integrity sha512-IxRtsehdGV9GFQ35IGm5oKKR2OGcazUoiNBxhRV160iF9FoyuXxjY+rIqs1gfnd+4eL98OjeGnMpE7RF/NBb3A==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.31.0"
-    "@typescript-eslint/types" "5.31.0"
-    "@typescript-eslint/typescript-estree" "5.31.0"
+    "@typescript-eslint/scope-manager" "5.32.0"
+    "@typescript-eslint/types" "5.32.0"
+    "@typescript-eslint/typescript-estree" "5.32.0"
     debug "^4.3.4"
 
-"@typescript-eslint/scope-manager@5.31.0":
-  version "5.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz#f47a794ba84d9b818ab7f8f44fff55a61016c606"
-  integrity sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==
+"@typescript-eslint/scope-manager@5.32.0":
+  version "5.32.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.32.0.tgz#763386e963a8def470580cc36cf9228864190b95"
+  integrity sha512-KyAE+tUON0D7tNz92p1uetRqVJiiAkeluvwvZOqBmW9z2XApmk5WSMV9FrzOroAcVxJZB3GfUwVKr98Dr/OjOg==
   dependencies:
-    "@typescript-eslint/types" "5.31.0"
-    "@typescript-eslint/visitor-keys" "5.31.0"
+    "@typescript-eslint/types" "5.32.0"
+    "@typescript-eslint/visitor-keys" "5.32.0"
 
-"@typescript-eslint/type-utils@5.31.0":
-  version "5.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.31.0.tgz#70a0b7201360b5adbddb0c36080495aa08f6f3d9"
-  integrity sha512-7ZYqFbvEvYXFn9ax02GsPcEOmuWNg+14HIf4q+oUuLnMbpJ6eHAivCg7tZMVwzrIuzX3QCeAOqKoyMZCv5xe+w==
+"@typescript-eslint/type-utils@5.32.0":
+  version "5.32.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.32.0.tgz#45a14506fe3fb908600b4cef2f70778f7b5cdc79"
+  integrity sha512-0gSsIhFDduBz3QcHJIp3qRCvVYbqzHg8D6bHFsDMrm0rURYDj+skBK2zmYebdCp+4nrd9VWd13egvhYFJj/wZg==
   dependencies:
-    "@typescript-eslint/utils" "5.31.0"
+    "@typescript-eslint/utils" "5.32.0"
     debug "^4.3.4"
     tsutils "^3.21.0"
 
-"@typescript-eslint/types@5.31.0":
-  version "5.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.31.0.tgz#7aa389122b64b18e473c1672fb3b8310e5f07a9a"
-  integrity sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==
+"@typescript-eslint/types@5.32.0":
+  version "5.32.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.32.0.tgz#484273021eeeae87ddb288f39586ef5efeb6dcd8"
+  integrity sha512-EBUKs68DOcT/EjGfzywp+f8wG9Zw6gj6BjWu7KV/IYllqKJFPlZlLSYw/PTvVyiRw50t6wVbgv4p9uE2h6sZrQ==
 
-"@typescript-eslint/typescript-estree@5.31.0":
-  version "5.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.31.0.tgz#eb92970c9d6e3946690d50c346fb9b1d745ee882"
-  integrity sha512-3S625TMcARX71wBc2qubHaoUwMEn+l9TCsaIzYI/ET31Xm2c9YQ+zhGgpydjorwQO9pLfR/6peTzS/0G3J/hDw==
+"@typescript-eslint/typescript-estree@5.32.0":
+  version "5.32.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.32.0.tgz#282943f34babf07a4afa7b0ff347a8e7b6030d12"
+  integrity sha512-ZVAUkvPk3ITGtCLU5J4atCw9RTxK+SRc6hXqLtllC2sGSeMFWN+YwbiJR9CFrSFJ3w4SJfcWtDwNb/DmUIHdhg==
   dependencies:
-    "@typescript-eslint/types" "5.31.0"
-    "@typescript-eslint/visitor-keys" "5.31.0"
+    "@typescript-eslint/types" "5.32.0"
+    "@typescript-eslint/visitor-keys" "5.32.0"
     debug "^4.3.4"
     globby "^11.1.0"
     is-glob "^4.0.3"
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/utils@5.31.0":
-  version "5.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.31.0.tgz#e146fa00dca948bfe547d665b2138a2dc1b79acd"
-  integrity sha512-kcVPdQS6VIpVTQ7QnGNKMFtdJdvnStkqS5LeALr4rcwx11G6OWb2HB17NMPnlRHvaZP38hL9iK8DdE9Fne7NYg==
+"@typescript-eslint/utils@5.32.0":
+  version "5.32.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.32.0.tgz#eccb6b672b94516f1afc6508d05173c45924840c"
+  integrity sha512-W7lYIAI5Zlc5K082dGR27Fczjb3Q57ECcXefKU/f0ajM5ToM0P+N9NmJWip8GmGu/g6QISNT+K6KYB+iSHjXCQ==
   dependencies:
     "@types/json-schema" "^7.0.9"
-    "@typescript-eslint/scope-manager" "5.31.0"
-    "@typescript-eslint/types" "5.31.0"
-    "@typescript-eslint/typescript-estree" "5.31.0"
+    "@typescript-eslint/scope-manager" "5.32.0"
+    "@typescript-eslint/types" "5.32.0"
+    "@typescript-eslint/typescript-estree" "5.32.0"
     eslint-scope "^5.1.1"
     eslint-utils "^3.0.0"
 
-"@typescript-eslint/visitor-keys@5.31.0":
-  version "5.31.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz#b0eca264df01ce85dceb76aebff3784629258f54"
-  integrity sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==
+"@typescript-eslint/visitor-keys@5.32.0":
+  version "5.32.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.32.0.tgz#b9715d0b11fdb5dd10fd0c42ff13987470525394"
+  integrity sha512-S54xOHZgfThiZ38/ZGTgB2rqx51CMJ5MCfVT2IplK4Q7hgzGfe0nLzLCcenDnc/cSjP568hdeKfeDcBgqNHD/g==
   dependencies:
-    "@typescript-eslint/types" "5.31.0"
+    "@typescript-eslint/types" "5.32.0"
     eslint-visitor-keys "^3.3.0"
 
 abab@^2.0.5, abab@^2.0.6:
@@ -1122,11 +1127,16 @@ acorn@^7.1.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
-acorn@^8.5.0, acorn@^8.7.1:
+acorn@^8.5.0:
   version "8.7.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
   integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
 
+acorn@^8.8.0:
+  version "8.8.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
+  integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
+
 agent-base@6:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -1984,13 +1994,14 @@ eslint-visitor-keys@^3.3.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
   integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
 
-eslint@^8.20.0:
-  version "8.20.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.20.0.tgz#048ac56aa18529967da8354a478be4ec0a2bc81b"
-  integrity sha512-d4ixhz5SKCa1D6SCPrivP7yYVi7nyD6A4vs6HIAul9ujBzcEmZVM3/0NN/yu5nKhmO1wjp5xQ46iRfmDGlOviA==
+eslint@^8.21.0:
+  version "8.21.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.21.0.tgz#1940a68d7e0573cef6f50037addee295ff9be9ef"
+  integrity sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==
   dependencies:
     "@eslint/eslintrc" "^1.3.0"
-    "@humanwhocodes/config-array" "^0.9.2"
+    "@humanwhocodes/config-array" "^0.10.4"
+    "@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
     ajv "^6.10.0"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
@@ -2000,14 +2011,17 @@ eslint@^8.20.0:
     eslint-scope "^7.1.1"
     eslint-utils "^3.0.0"
     eslint-visitor-keys "^3.3.0"
-    espree "^9.3.2"
+    espree "^9.3.3"
     esquery "^1.4.0"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
     file-entry-cache "^6.0.1"
+    find-up "^5.0.0"
     functional-red-black-tree "^1.0.1"
     glob-parent "^6.0.1"
     globals "^13.15.0"
+    globby "^11.1.0"
+    grapheme-splitter "^1.0.4"
     ignore "^5.2.0"
     import-fresh "^3.0.0"
     imurmurhash "^0.1.4"
@@ -2025,12 +2039,12 @@ eslint@^8.20.0:
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
 
-espree@^9.3.2:
-  version "9.3.2"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596"
-  integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==
+espree@^9.3.2, espree@^9.3.3:
+  version "9.3.3"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.3.tgz#2dd37c4162bb05f433ad3c1a52ddf8a49dc08e9d"
+  integrity sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==
   dependencies:
-    acorn "^8.7.1"
+    acorn "^8.8.0"
     acorn-jsx "^5.3.2"
     eslint-visitor-keys "^3.3.0"
 
@@ -2068,6 +2082,11 @@ esutils@^2.0.2:
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
+eventemitter2@^6.4.5:
+  version "6.4.7"
+  resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
+  integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
+
 execa@^5.0.0:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -2173,6 +2192,14 @@ find-up@^4.0.0, find-up@^4.1.0:
     locate-path "^5.0.0"
     path-exists "^4.0.0"
 
+find-up@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+  dependencies:
+    locate-path "^6.0.0"
+    path-exists "^4.0.0"
+
 flat-cache@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
@@ -2345,6 +2372,11 @@ graceful-fs@^4.1.3, graceful-fs@^4.2.9:
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
   integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
 
+grapheme-splitter@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
+  integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
+
 has-bigints@^1.0.1, has-bigints@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@@ -3174,6 +3206,13 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
+locate-path@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+  dependencies:
+    p-locate "^5.0.0"
+
 lodash.memoize@4.x:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -3474,7 +3513,7 @@ p-limit@^2.2.0:
   dependencies:
     p-try "^2.0.0"
 
-p-limit@^3.1.0:
+p-limit@^3.0.2, p-limit@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
   integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
@@ -3495,6 +3534,13 @@ p-locate@^4.1.0:
   dependencies:
     p-limit "^2.2.0"
 
+p-locate@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+  integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+  dependencies:
+    p-limit "^3.0.2"
+
 p-try@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"


Test patch:
diff --git a/tests/Task.test.ts b/tests/Task.test.ts
index 7ea6a26c..dcaa78a8 100644
--- a/tests/Task.test.ts
+++ b/tests/Task.test.ts
@@ -979,6 +979,11 @@ describe('check removal of the global filter', () => {
             markdownTask: '- [ ] task with #t multiple global filters #t',
             expectedDescription: 'task with multiple global filters',
         },
+        {
+            globalFilter: '#t',
+            markdownTask: '- [ ] #t', // confirm behaviour when the description is empty
+            expectedDescription: '',
+        },
     ])(
         'should parse "$markdownTask" and extract "$expectedDescription"',
         ({ globalFilter, markdownTask, expectedDescription }) => {
@@ -1004,3 +1009,128 @@ describe('check removal of the global filter', () => {
         },
     );
 });
+
+describe('check removal of the global filter exhaustively', () => {
+    type GlobalFilterRemoval = {
+        globalFilter: string;
+    };
+
+    function checkDescription(
+        markdownLine: string,
+        expectedDescription: string,
+    ) {
+        const task = constructTaskFromLine(markdownLine);
+
+        // Assert
+        expect(task).not.toBeNull();
+        expect(task!.getDescriptionWithoutGlobalFilter()).toEqual(
+            expectedDescription,
+        );
+    }
+
+    test.each<GlobalFilterRemoval>([
+        {
+            globalFilter: '#t',
+        },
+        // The characters listed below are the ones that are - or were - escaped by
+        // Task.escapeRegExp().
+        // See the developer.mozilla.org reference in that method.
+        // This test validates the escaping of each of those characters.
+        {
+            globalFilter: '.',
+        },
+        {
+            globalFilter: '*',
+        },
+        {
+            globalFilter: '+',
+        },
+        {
+            globalFilter: '?',
+        },
+        {
+            globalFilter: '^',
+        },
+        {
+            // Failed attempt at creating a failing test for when = was not escaped.
+            // When I make Task.escapeRegExp() escape =, I get:
+            // Invalid regular expression: /(^|\s)hello\=world($|\s)/: Invalid escape
+            globalFilter: 'hello=world',
+        },
+        {
+            // Failed attempt at creating a failing test for when ! was not escaped.
+            // When I make Task.escapeRegExp() escape !, I get:
+            // Invalid regular expression: /(^|\s)hello\!world($|\s)/: Invalid escape
+            globalFilter: 'hello!world',
+        },
+        {
+            // Failed attempt at creating a failing test for when : was not escaped.
+            // When I make Task.escapeRegExp() escape :, I get:
+            // Invalid regular expression: /(^|\s)hello\:world($|\s)/: Invalid escape
+            globalFilter: 'hello:world',
+        },
+        {
+            globalFilter: '$',
+        },
+        {
+            globalFilter: '{',
+        },
+        {
+            globalFilter: '}',
+        },
+        {
+            globalFilter: '(',
+        },
+        {
+            globalFilter: ')',
+        },
+        {
+            globalFilter: '|',
+        },
+        {
+            globalFilter: '[',
+        },
+        {
+            globalFilter: ']',
+        },
+        {
+            // Failed attempt at creating a failing test for when / was not escaped
+            globalFilter: '///',
+        },
+        {
+            globalFilter: '\\',
+        },
+    ])(
+        'should parse global filter "$globalFilter" edge cases correctly',
+        ({ globalFilter }) => {
+            // Arrange
+            const originalSettings = getSettings();
+            if (globalFilter != '') {
+                updateSettings({ globalFilter: globalFilter });
+            }
+
+            // Act
+
+            // global filter removed at beginning, middle and end
+            let markdownLine = `- [ ] ${globalFilter} 1 ${globalFilter} 2 ${globalFilter}`;
+            let expectedDescription = '1 2';
+            checkDescription(markdownLine, expectedDescription);
+
+            // global filter not removed if non-empty non-tag characters before or after it
+            markdownLine = `- [ ] ${globalFilter}x 1 x${globalFilter} ${globalFilter}x 2 x${globalFilter}`;
+            expectedDescription = `${globalFilter}x 1 x${globalFilter} ${globalFilter}x 2 x${globalFilter}`;
+            checkDescription(markdownLine, expectedDescription);
+
+            // global filter not removed if non-empty sub-tag characters after it.
+            // Include at least one occurrence of global filter, so we don't pass by luck.
+            markdownLine = `- [ ] ${globalFilter}/x 1 x${globalFilter} ${globalFilter}/x 2 ${globalFilter} ${globalFilter}/x`;
+            expectedDescription = `${globalFilter}/x 1 x${globalFilter} ${globalFilter}/x 2 ${globalFilter}/x`;
+            checkDescription(markdownLine, expectedDescription);
+
+            // Cleanup
+            if (globalFilter != '') {
+                updateSettings(originalSettings);
+            }
+        },
+    );
+});