Repository: FHIR/sushi
Pull request #196 (2020-02-05 15:12:07)

Add info messages during import and export
Addresses https://standardhealthrecord.atlassian.net/browse/CIMPL-208.

Informational messages are logged during FSH import and FHIR
export. These messages indicate progress through the import and
export processes. Export messages will only appear if there were
FSH resources of the appropriate type in the tank being exported.

The additional informational messages had an unpleasant side effect
upon the test cases that monitored logged messages, so the loggerSpy
class now retrieves log entries and messages of a specific logging
level. When the level argument is omitted, log entries of any level
will be returned.

mint-thompson: @cmoesel that's a good point regarding the usefulness when troubleshooting. I think having the messages at the end of each process is still useful, though. Do you think it would be a message overload to have messages both at the start at end of these steps?
ngfreiter: At the risk of adding too many messages, I think it would also be good to have some messages for package loading. Maybe a message along the lines of:
`package <package-name> loaded from <location>`
where the location would either be their local FHIR cache (~/.fhir), or the FHIR package server (packages.fhir.org). I think that could be helpful in letting people validate that they are loading the packages they think they are loading. Also loading something like all of R4 from the package server can take relatively long.

I personally prefer too many messages to too few, but if you both think this would be too many messages I'll obey democracy.
mint-thompson: The latest commit adds more info messages to the start of tasks (thank you @cmoesel ) and during package loading (thank you @ngfreiter ).
cmoesel: I really like the addition of the info messages and ability to specify log levels in the test utility.  Thanks @mint-thompson!  I just pushed up a commit that changes some of the text in the messages and also adds some messages for the IG Exporter.  Can you take a look at my last commit and let me know if it looks OK to you?

With my changes, running against the fsh-mcode IG produces these logs:
```
info: Checking local cache for hl7.fhir.r4.core#4.0.1...
info: Found hl7.fhir.r4.core#4.0.1 in local cache.
info: Checking local cache for hl7.fhir.us.core#3.1.0...
info: Found hl7.fhir.us.core#3.1.0 in local cache.
info: Importing FSH text...
info: Preprocessed 14 documents with 38 aliases.
info: Imported 58 definitions and 27 instances.
info: Loaded package hl7.fhir.r4.core#4.0.1
info: Loaded package hl7.fhir.us.core#3.1.0
info: Converting FSH to FHIR resources...
info: Converted 31 FHIR StructureDefinitions.
info: Converted 27 FHIR ValueSets.
info: Converted 27 FHIR instances.
info: Building FHIR Implementation Guide...
info: Exporting FHIR resources as JSON for Implementation Guide...
info: Exported 85 FHIR resources as JSON for Implementation Guide.
info: Generated default index.md.
info: Generated default ig.ini.
info: Generated default package-list.json
info: Generated ImplementationGuide-fhir.us.mcode.json
info: Built FHIR Implementation Guide; ready for IG Publisher.
info:
  Profiles:    25
  Extensions:  6
  Instances:   27
  ValueSets:   27
  CodeSystems: 0
  Errors:      0
  Warnings:    0
```

Patch:
diff --git a/src/app.ts b/src/app.ts
index a83d5ef..c93350e 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -64,10 +64,15 @@ async function app() {
   const dependencyDefs: Promise<FHIRDefinitions | void>[] = [];
   for (const dep of Object.keys(config?.dependencies ?? {})) {
     dependencyDefs.push(
-      loadDependency(dep, config.dependencies[dep], defs).catch(e => {
-        logger.error(`Failed to load ${dep}#${config.dependencies[dep]}`);
-        logger.error(e.message);
-      })
+      loadDependency(dep, config.dependencies[dep], defs)
+        .then(def => {
+          logger.info(`Loaded package ${dep}#${config.dependencies[dep]}`);
+          return def;
+        })
+        .catch(e => {
+          logger.error(`Failed to load ${dep}#${config.dependencies[dep]}`);
+          logger.error(e.message);
+        })
     );
   }
 
@@ -79,10 +84,12 @@ async function app() {
       return new RawFSH(fileContent, filePath);
     });
 
+  logger.info('Importing FSH text...');
   const docs = importText(rawFSHes);
 
   const tank = new FSHTank(docs, config);
   await Promise.all(dependencyDefs);
+  logger.info('Converting FSH to FHIR resources...');
   const outPackage = exportFHIR(tank, defs);
 
   fs.ensureDirSync(program.out);
@@ -99,9 +106,13 @@ async function app() {
   // If ig-data exists, generate an IG, otherwise, generate resources only
   const igDataPath = path.resolve(input, 'ig-data');
   if (fs.existsSync(igDataPath)) {
+    logger.info('Building FHIR Implementation Guide...');
     const igExporter = new IGExporter(outPackage, defs, igDataPath);
     igExporter.export(program.out);
+    logger.info('Built FHIR Implementation Guide; ready for IG Publisher.');
   } else {
+    logger.info('Exporting FHIR resources as JSON...');
+    let count = 0;
     for (const sd of [
       ...outPackage.profiles,
       ...outPackage.extensions,
@@ -114,7 +125,9 @@ async function app() {
         JSON.stringify(sd.toJSON(), null, 2),
         'utf8'
       );
+      count++;
     }
+    logger.info(`Exported ${count} FHIR resources as JSON.`);
   }
 
   logger.info(`
diff --git a/src/export/CodeSystemExporter.ts b/src/export/CodeSystemExporter.ts
index 257b6a9..0c29352 100644
--- a/src/export/CodeSystemExporter.ts
+++ b/src/export/CodeSystemExporter.ts
@@ -68,13 +68,17 @@ export class CodeSystemExporter {
   }
 
   export(): Package {
-    for (const cs of this.tank.getAllCodeSystems()) {
+    const codeSystems = this.tank.getAllCodeSystems();
+    for (const cs of codeSystems) {
       try {
         this.exportCodeSystem(cs);
       } catch (e) {
         logger.error(e.message, cs.sourceInfo);
       }
     }
+    if (codeSystems.length > 0) {
+      logger.info(`Converted ${codeSystems.length} FHIR CodeSystems.`);
+    }
     return this.pkg;
   }
 }
diff --git a/src/export/FHIRExporter.ts b/src/export/FHIRExporter.ts
index 7e17106..2208bd9 100644
--- a/src/export/FHIRExporter.ts
+++ b/src/export/FHIRExporter.ts
@@ -27,16 +27,16 @@ export class FHIRExporter {
       this.pkg,
       this.fisher
     );
-    this.instanceExporter = new InstanceExporter(this.tank, this.pkg, this.fisher);
     this.valueSetExporter = new ValueSetExporter(this.tank, this.pkg, this.fisher);
     this.codeSystemExporter = new CodeSystemExporter(this.tank, this.pkg, this.fisher);
+    this.instanceExporter = new InstanceExporter(this.tank, this.pkg, this.fisher);
   }
 
   export(): Package {
     this.structureDefinitionExporter.export();
-    this.instanceExporter.export();
     this.valueSetExporter.export();
     this.codeSystemExporter.export();
+    this.instanceExporter.export();
 
     return this.pkg;
   }
diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts
index 2228a3a..51feff5 100644
--- a/src/export/InstanceExporter.ts
+++ b/src/export/InstanceExporter.ts
@@ -196,7 +196,8 @@ export class InstanceExporter {
    * @returns {Package}
    */
   export(): Package {
-    for (const instance of this.tank.getAllInstances()) {
+    const instances = this.tank.getAllInstances();
+    for (const instance of instances) {
       try {
         const instanceDef = this.exportInstance(instance);
         this.pkg.instances.push(instanceDef);
@@ -204,6 +205,9 @@ export class InstanceExporter {
         logger.error(e.message, e.sourceInfo);
       }
     }
+    if (instances.length > 0) {
+      logger.info(`Converted ${instances.length} FHIR instances.`);
+    }
     return this.pkg;
   }
 }
diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts
index cbf7efc..0d31069 100644
--- a/src/export/StructureDefinitionExporter.ts
+++ b/src/export/StructureDefinitionExporter.ts
@@ -245,13 +245,17 @@ export class StructureDefinitionExporter implements Fishable {
    * @returns {Package}
    */
   export(): Package {
-    this.tank.getAllStructureDefinitions().forEach(sd => {
+    const structureDefinitions = this.tank.getAllStructureDefinitions();
+    structureDefinitions.forEach(sd => {
       try {
         this.exportStructDef(sd);
       } catch (e) {
         logger.error(e.message, e.sourceInfo || sd.sourceInfo);
       }
     });
+    if (structureDefinitions.length > 0) {
+      logger.info(`Converted ${structureDefinitions.length} FHIR StructureDefinitions.`);
+    }
     return this.pkg;
   }
 }
diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts
index 07e5e00..0b42b2c 100644
--- a/src/export/ValueSetExporter.ts
+++ b/src/export/ValueSetExporter.ts
@@ -117,13 +117,17 @@ export class ValueSetExporter {
   }
 
   export(): Package {
-    for (const valueSet of this.tank.getAllValueSets()) {
+    const valueSets = this.tank.getAllValueSets();
+    for (const valueSet of valueSets) {
       try {
         this.exportValueSet(valueSet);
       } catch (e) {
         logger.error(e.message, valueSet.sourceInfo);
       }
     }
+    if (valueSets.length > 0) {
+      logger.info(`Converted ${valueSets.length} FHIR ValueSets.`);
+    }
     return this.pkg;
   }
 
diff --git a/src/fhirdefs/load.ts b/src/fhirdefs/load.ts
index 7bb82f2..eb9fe69 100644
--- a/src/fhirdefs/load.ts
+++ b/src/fhirdefs/load.ts
@@ -6,6 +6,7 @@ import os from 'os';
 import tar from 'tar';
 import rp from 'request-promise-native';
 import temp from 'temp';
+import { logger } from '../utils';
 
 /**
  * Loads a dependency from user FHIR cache or from online
@@ -26,7 +27,13 @@ export async function loadDependency(
   const loadPath = path.join(cachePath, fullPackageName, 'package');
   let loadedPackage: string;
   if (version != 'current') {
+    logger.info(`Checking local cache for ${fullPackageName}...`);
     loadedPackage = loadFromPath(loadPath, fullPackageName, FHIRDefs);
+    if (loadedPackage) {
+      logger.info(`Found ${fullPackageName} in local cache.`);
+    } else {
+      logger.info(`Did not find ${fullPackageName} in local cache.`);
+    }
   }
   if (!loadedPackage) {
     let packageUrl: string;
@@ -69,6 +76,7 @@ export async function loadDependency(
     const targetDirectory = path.join(cachePath, fullPackageName);
     let res;
     try {
+      logger.info(`Downloading ${fullPackageName}...`);
       res = await rp.get({
         uri: packageUrl,
         encoding: null,
@@ -79,6 +87,7 @@ export async function loadDependency(
           return body;
         }
       });
+      logger.info(`Downloaded ${fullPackageName}`);
     } catch (e) {
       e.message = `${e.statusCode} - ${e.response}`;
       throw e;
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index 3dad8a1..b9cc850 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -39,14 +39,14 @@ export class IGExporter {
   export(outPath: string) {
     ensureDirSync(outPath);
     this.initIG();
+    this.addResources(outPath);
     this.addStaticFiles(outPath);
     this.addIndex(outPath);
     this.addOtherPageContent(outPath);
     this.addImages(outPath);
-    this.addResources(outPath);
-    this.addImplementationGuide(outPath);
     this.addIgIni(outPath);
     this.addPackageList(outPath);
+    this.addImplementationGuide(outPath);
   }
 
   /**
@@ -186,10 +186,13 @@ export class IGExporter {
     let generation: ImplementationGuideDefinitionPageGeneration = 'markdown';
     if (fs.existsSync(inputIndexMarkdownPath)) {
       fs.copySync(inputIndexMarkdownPath, path.join(igPath, 'input', 'pagecontent', 'index.md'));
+      logger.info('Copied ig-data/input/pagecontent/index.md');
     } else if (fs.existsSync(inputIndexXMLPath)) {
       fs.copySync(inputIndexXMLPath, path.join(igPath, 'input', 'pagecontent', 'index.xml'));
       generation = 'html';
+      logger.info('Copied ig-data/input/pagecontent/index.xml');
     } else {
+      logger.info('Generated default index.md.');
       outputFileSync(
         path.join(igPath, 'input', 'pagecontent', 'index.md'),
         this.pkg.config.description ?? ''
@@ -272,6 +275,8 @@ export class IGExporter {
    * @param igPath {string} - the path where the IG is exported to
    */
   private addResources(igPath: string) {
+    logger.info('Exporting FHIR resources as JSON for Implementation Guide...');
+    let count = 0;
     const sds = sortBy([...this.pkg.profiles, ...this.pkg.extensions], sd => sd.name);
     sds.forEach(sd => {
       const sdPath = path.join(igPath, 'input', 'resources', sd.getFileName());
@@ -282,6 +287,7 @@ export class IGExporter {
         description: sd.description,
         exampleBoolean: false
       });
+      count++;
     });
     sortBy(this.pkg.instances, instance => instance.id ?? instance.instanceName).forEach(
       instance => {
@@ -300,6 +306,7 @@ export class IGExporter {
           resource.exampleBoolean = true;
         }
         this.ig.definition.resource.push(resource);
+        count++;
       }
     );
     sortBy(this.pkg.valueSets, valueSet => valueSet.name).forEach(valueSet => {
@@ -310,6 +317,7 @@ export class IGExporter {
         name: valueSet.title ?? valueSet.name,
         description: valueSet.description
       });
+      count++;
     });
     sortBy(this.pkg.codeSystems, codeSystem => codeSystem.name).forEach(codeSystem => {
       const codeSystemPath = path.join(igPath, 'input', 'resources', codeSystem.getFileName());
@@ -319,7 +327,9 @@ export class IGExporter {
         name: codeSystem.title ?? codeSystem.name,
         description: codeSystem.description
       });
+      count++;
     });
+    logger.info(`Exported ${count} FHIR resources as JSON for Implementation Guide.`);
   }
 
   /**
@@ -330,6 +340,7 @@ export class IGExporter {
   private addImplementationGuide(igPath: string): void {
     const igJsonPath = path.join(igPath, 'input', `ImplementationGuide-${this.ig.id}.json`);
     outputJSONSync(igJsonPath, this.ig, { spaces: 2 });
+    logger.info(`Generated ImplementationGuide-${this.ig.id}.json`);
   }
 
   /**
@@ -353,7 +364,9 @@ export class IGExporter {
 
     // Then add properties from the user-provided ig.ini (if applicable)
     const inputIniPath = path.join(this.igDataPath, 'ig.ini');
+    let merged = false;
     if (fs.existsSync(inputIniPath)) {
+      merged = true;
       const inputIni = ini.parse(fs.readFileSync(inputIniPath, 'utf8'));
       if (Object.keys(inputIni).length > 1 || inputIni.IG == null) {
         logger.error('igi.ini file must contain an [IG] section with no other sections', {
@@ -387,6 +400,12 @@ export class IGExporter {
       path.join(igPath, 'ig.ini'),
       ini.encode(iniObj, { section: 'IG', whitespace: true })
     );
+
+    if (merged) {
+      logger.info('Merged ig-data/ig.ini w/ generated ig.ini');
+    } else {
+      logger.info('Generated default ig.ini.');
+    }
   }
 
   /**
@@ -417,6 +436,7 @@ export class IGExporter {
       }
       if (!mismatch) {
         fs.copySync(inputPackageListPath, path.join(igPath, 'package-list.json'));
+        logger.info('Copied ig-data/package-list.json.');
         return;
       }
     }
@@ -448,5 +468,6 @@ export class IGExporter {
       },
       { spaces: 2 }
     );
+    logger.info('Generated default package-list.json');
   }
 }
diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts
index 0c6e844..cc886ad 100644
--- a/src/import/FSHImporter.ts
+++ b/src/import/FSHImporter.ts
@@ -142,6 +142,7 @@ export class FSHImporter extends FSHVisitor {
         }
       });
     });
+    logger.info(`Preprocessed ${docs.length} documents with ${this.allAliases.size} aliases.`);
 
     // Now do the main import
     contexts.forEach((context, index) => {
@@ -152,6 +153,14 @@ export class FSHImporter extends FSHVisitor {
       this.currentFile = null;
     });
 
+    let [definitions, instances] = [0, 0];
+    docs.forEach(doc => {
+      definitions +=
+        doc.codeSystems.size + doc.extensions.size + doc.profiles.size + doc.valueSets.size;
+      instances += doc.instances.size;
+    });
+    logger.info(`Imported ${definitions} definitions and ${instances} instances.`);
+
     return docs;
   }
 


Test patch:
diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts
index 0764832..64f56af 100644
--- a/test/export/CodeSystemExporter.test.ts
+++ b/test/export/CodeSystemExporter.test.ts
@@ -145,8 +145,8 @@ describe('CodeSystemExporter', () => {
     const exported = exporter.export().codeSystems;
     expect(exported.length).toBe(1);
     expect(exported[0].id).toBe('Is this allowed?');
-    expect(loggerSpy.getLastMessage()).toMatch(/does not represent a valid FHIR id/s);
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Strange\.fsh.*Line: 2 - 6\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/does not represent a valid FHIR id/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Strange\.fsh.*Line: 2 - 6\D/s);
   });
 
   it('should log a message when the code system has an invalid name', () => {
@@ -157,8 +157,8 @@ describe('CodeSystemExporter', () => {
     const exported = exporter.export().codeSystems;
     expect(exported.length).toBe(1);
     expect(exported[0].name).toBe('Strange.Code.System');
-    expect(loggerSpy.getLastMessage()).toMatch(/does not represent a valid FHIR name/s);
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Strange\.fsh.*Line: 3 - 8\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/does not represent a valid FHIR name/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Strange\.fsh.*Line: 3 - 8\D/s);
   });
 
   // CaretValueRules
@@ -199,6 +199,6 @@ describe('CodeSystemExporter', () => {
       version: '0.0.1',
       status: 'active'
     });
-    expect(loggerSpy.getLastMessage()).toMatch(/File: InvalidValue\.fsh.*Line: 6\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: InvalidValue\.fsh.*Line: 6\D/s);
   });
 });
diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts
index b98317b..b7f64e8 100644
--- a/test/export/InstanceExporter.test.ts
+++ b/test/export/InstanceExporter.test.ts
@@ -88,7 +88,7 @@ describe('InstanceExporter', () => {
     instance.instanceOf = 'BogusParent';
     doc.instances.set(instance.name, instance);
     exporter.export();
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Bogus\.fsh.*Line: 2 - 4\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Bogus\.fsh.*Line: 2 - 4\D/s);
   });
 
   it('should export instances with InstanceOf FSHy profile', () => {
@@ -928,7 +928,7 @@ describe('InstanceExporter', () => {
 
       const exported = exporter.export().instances;
       expect(exported.length).toBe(1);
-      expect(loggerSpy.getLastMessage()).toMatch(/File: Unmeasurable\.fsh.*Line: 3\D/s);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/File: Unmeasurable\.fsh.*Line: 3\D/s);
     });
   });
 });
diff --git a/test/export/StructureDefinition.ExtensionExporter.test.ts b/test/export/StructureDefinition.ExtensionExporter.test.ts
index 7ecd5b2..fd6f539 100644
--- a/test/export/StructureDefinition.ExtensionExporter.test.ts
+++ b/test/export/StructureDefinition.ExtensionExporter.test.ts
@@ -69,7 +69,7 @@ describe('ExtensionExporter', () => {
     extension.parent = 'DoesNotExist';
     doc.extensions.set(extension.name, extension);
     exporter.export();
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Wrong\.fsh.*Line: 14 - 24\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Wrong\.fsh.*Line: 14 - 24\D/s);
   });
 
   it('should export extensions with FSHy parents', () => {
diff --git a/test/export/StructureDefinition.ProfileExporter.test.ts b/test/export/StructureDefinition.ProfileExporter.test.ts
index 39da78e2..24e3c18 100644
--- a/test/export/StructureDefinition.ProfileExporter.test.ts
+++ b/test/export/StructureDefinition.ProfileExporter.test.ts
@@ -69,7 +69,7 @@ describe('ProfileExporter', () => {
     profile.parent = 'BogusParent';
     doc.profiles.set(profile.name, profile);
     exporter.export();
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Bogus\.fsh.*Line: 2 - 4\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Bogus\.fsh.*Line: 2 - 4\D/s);
   });
 
   it('should export profiles with FSHy parents', () => {
diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts
index e1690a0..fcda15d 100644
--- a/test/export/StructureDefinitionExporter.test.ts
+++ b/test/export/StructureDefinitionExporter.test.ts
@@ -962,11 +962,9 @@ describe('StructureDefinitionExporter', () => {
 
     withDebugLogging(() => exporter.export());
 
-    const lastLog = loggerSpy.getLastLog();
-    expect(lastLog.level).toMatch(/debug/);
-    expect(lastLog.message).toMatch(/Warning: Circular .* BarQuantity and FooQuantity/);
-
-    expect(loggerSpy.getLastMessage()).toMatch(/Warning: Circular .* BarQuantity and FooQuantity/);
+    expect(loggerSpy.getLastMessage('debug')).toMatch(
+      /Warning: Circular .* BarQuantity and FooQuantity/
+    );
   });
 
   it('should log a warning message when we detect a circular dependency that causes an incomplete parent', () => {
@@ -995,12 +993,9 @@ describe('StructureDefinitionExporter', () => {
 
     exporter.export();
 
-    const lastLog = loggerSpy.getLastLog();
-    expect(lastLog.level).toMatch(/warn/);
-    expect(lastLog.message).toMatch(
-      /The definition of FooQuantity may be incomplete .* BarQuantity/
-    );
-    expect(lastLog.message).toMatch(/File: FooQuantity\.fsh.*Line: 6 - 11\D/s);
+    const lastMessage = loggerSpy.getLastMessage('warn');
+    expect(lastMessage).toMatch(/The definition of FooQuantity may be incomplete .* BarQuantity/);
+    expect(lastMessage).toMatch(/File: FooQuantity\.fsh.*Line: 6 - 11\D/s);
   });
 
   it('should not apply an incorrect OnlyRule', () => {
diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts
index 1706ffc..10143e6 100644
--- a/test/export/ValueSetExporter.test.ts
+++ b/test/export/ValueSetExporter.test.ts
@@ -95,8 +95,8 @@ describe('ValueSetExporter', () => {
     const exported = exporter.export().valueSets;
     expect(exported.length).toBe(1);
     expect(exported[0].id).toBe('Delicious!');
-    expect(loggerSpy.getLastMessage()).toMatch(/does not represent a valid FHIR id/s);
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Breakfast\.fsh.*Line: 3 - 7\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/does not represent a valid FHIR id/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Breakfast\.fsh.*Line: 3 - 7\D/s);
   });
 
   it('should log a message when the value set has an invalid name', () => {
@@ -107,8 +107,8 @@ describe('ValueSetExporter', () => {
     const exported = exporter.export().valueSets;
     expect(exported.length).toBe(1);
     expect(exported[0].name).toBe('All-you-can-eat');
-    expect(loggerSpy.getLastMessage()).toMatch(/does not represent a valid FHIR name/s);
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Breakfast\.fsh.*Line: 2 - 8\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/does not represent a valid FHIR name/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Breakfast\.fsh.*Line: 2 - 8\D/s);
   });
 
   it('should export each value set once, even if export is called more than once', () => {
@@ -432,7 +432,7 @@ describe('ValueSetExporter', () => {
     doc.valueSets.set(valueSet.name, valueSet);
     const exported = exporter.export().valueSets;
     expect(exported.length).toBe(0);
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Breakfast\.fsh.*Line: 2 - 4\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Breakfast\.fsh.*Line: 2 - 4\D/s);
   });
 
   // CaretValueRules
@@ -471,6 +471,6 @@ describe('ValueSetExporter', () => {
       version: '0.0.1',
       status: 'active'
     });
-    expect(loggerSpy.getLastMessage()).toMatch(/File: InvalidValue\.fsh.*Line: 6\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: InvalidValue\.fsh.*Line: 6\D/s);
   });
 });
diff --git a/test/ig/IGExporter.test.ts b/test/ig/IGExporter.test.ts
index b35ee55..83545f5 100644
--- a/test/ig/IGExporter.test.ts
+++ b/test/ig/IGExporter.test.ts
@@ -478,13 +478,13 @@ describe('IGExporter', () => {
 
     it('should should log errors for invalid user-specified values in ig.ini', () => {
       // Check for log messages indicating invalid input
-      expect(loggerSpy.getMessageAtIndex(-5)).toMatch(
+      expect(loggerSpy.getMessageAtIndex(-5, 'error')).toMatch(
         /igi\.ini: sushi does not currently support overriding ig value\..*File: .*[\/\\]invalid-data-ig[\/\\]ig-data[\/\\]ig.ini/s
       );
-      expect(loggerSpy.getMessageAtIndex(-4)).toMatch(
+      expect(loggerSpy.getMessageAtIndex(-4, 'error')).toMatch(
         /igi\.ini: license value \(Apache2\.0\) does not match license declared in package\.json \(CC0-1\.0\)\.  Keeping CC0-1\.0\..*File: .*[\/\\]invalid-data-ig[\/\\]ig-data[\/\\]ig.ini/s
       );
-      expect(loggerSpy.getMessageAtIndex(-3)).toMatch(
+      expect(loggerSpy.getMessageAtIndex(-3, 'error')).toMatch(
         /igi\.ini: version value \(0\.2\.0\) does not match version declared in package\.json \(0\.1\.0\)\.  Keeping 0\.1\.0\..*File: .*[\/\\]invalid-data-ig[\/\\]ig-data[\/\\]ig.ini/s
       );
 
@@ -515,17 +515,17 @@ describe('IGExporter', () => {
       expect(imageFileNames).toContain('bad.html');
 
       // Check for log messages indicating invalid input
-      expect(loggerSpy.getMessageAtIndex(-6)).toMatch(
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
         /Files not in the supported file types \(\.md and \.xml\) were detected\. These files will be copied over without any processing\..*File: .*[\/\\]invalid-data-ig[\/\\]ig-data[\/\\]input[\/\\]pagecontent/s
       );
     });
 
     it('should log an error if supplied package-list.json does not match package.json', () => {
       // Check for log messages indicating invalid input
-      expect(loggerSpy.getMessageAtIndex(-2)).toMatch(
+      expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(
         /package-list\.json: package-id value \(wrong-package-id\) does not match name declared in package\.json \(sushi-test\)\.  Ignoring custom package-list\.json\..*File: .*[\/\\]invalid-data-ig[\/\\]ig-data[\/\\]package-list.json/s
       );
-      expect(loggerSpy.getMessageAtIndex(-1)).toMatch(
+      expect(loggerSpy.getMessageAtIndex(-1, 'error')).toMatch(
         /package-list\.json: canonical value \(wrong-canonical\) does not match canonical declared in package\.json \(http:\/\/hl7\.org\/fhir\/sushi-test\)\.  Ignoring custom package-list\.json\..*File: .*[\/\\]invalid-data-ig[\/\\]ig-data[\/\\]package-list.json/s
       );
 
diff --git a/test/import/FSHImporter.Alias.test.ts b/test/import/FSHImporter.Alias.test.ts
index 1957776..33f6f4f 100644
--- a/test/import/FSHImporter.Alias.test.ts
+++ b/test/import/FSHImporter.Alias.test.ts
@@ -45,7 +45,7 @@ describe('FSHImporter', () => {
       expect(result.aliases.get('USCoreRace')).toBe(
         'http://hl7.org/fhir/us/core/ValueSet/omb-race-category'
       );
-      expect(loggerSpy.getLastMessage()).toMatch(
+      expect(loggerSpy.getLastMessage('error')).toMatch(
         /Alias USCoreRace cannot be redefined to http:\/\/hl7.org\/fhir\/us\/core\/StructureDefinition\/us-core-race; it is already defined as http:\/\/hl7.org\/fhir\/us\/core\/ValueSet\/omb-race-category\..*Line: 3\D/s
       );
     });
@@ -68,7 +68,7 @@ describe('FSHImporter', () => {
         'http://hl7.org/fhir/us/core/ValueSet/omb-race-category'
       );
       expect(results[1].aliases.size).toBe(0);
-      expect(loggerSpy.getLastMessage()).toMatch(
+      expect(loggerSpy.getLastMessage('error')).toMatch(
         /Alias USCoreRace cannot be redefined to http:\/\/hl7.org\/fhir\/us\/core\/StructureDefinition\/us-core-race; it is already defined as http:\/\/hl7.org\/fhir\/us\/core\/ValueSet\/omb-race-category\..*File: Alias2\.fsh.*Line: 2\D/s
       );
     });
@@ -95,7 +95,7 @@ describe('FSHImporter', () => {
       expect(results[1].aliases.get('USCoreRace')).toBe(
         'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race'
       );
-      expect(loggerSpy.getLastLog()).toBeUndefined();
+      expect(loggerSpy.getAllLogs('error').length).toBe(0);
     });
 
     it('should translate an alias when the alias is defined before its use', () => {
diff --git a/test/import/FSHImporter.CodeSystem.test.ts b/test/import/FSHImporter.CodeSystem.test.ts
index f289e94..041d821 100644
--- a/test/import/FSHImporter.CodeSystem.test.ts
+++ b/test/import/FSHImporter.CodeSystem.test.ts
@@ -122,9 +122,9 @@ describe('FSHImporter', () => {
         Description: "Lions and tigers and bears!"
         `;
         importSingleText(input, 'Zoo.fsh');
-        expect(loggerSpy.getMessageAtIndex(-3)).toMatch(/File: Zoo\.fsh.*Line: 6\D/s);
-        expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Zoo\.fsh.*Line: 7\D/s);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 8\D/s);
+        expect(loggerSpy.getMessageAtIndex(-3, 'error')).toMatch(/File: Zoo\.fsh.*Line: 6\D/s);
+        expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Zoo\.fsh.*Line: 7\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 8\D/s);
       });
     });
     describe('#concept', () => {
@@ -249,7 +249,7 @@ describe('FSHImporter', () => {
         const codeSystem = result.codeSystems.get('ZOO');
         expect(codeSystem.name).toBe('ZOO');
         expect(codeSystem.concepts.length).toBe(1);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 4\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 4\D/s);
       });
 
       it('should log an error when a concept includes a system declaration', () => {
@@ -267,8 +267,8 @@ describe('FSHImporter', () => {
         expect(codeSystem.concepts[0].code).toBe('goat');
         expect(codeSystem.concepts[1].code).toBe('bat');
         expect(codeSystem.concepts[2].code).toBe('jackalope');
-        expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Zoo\.fsh.*Line: 4\D/s);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 5\D/s);
+        expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Zoo\.fsh.*Line: 4\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 5\D/s);
       });
     });
 
@@ -304,7 +304,7 @@ describe('FSHImporter', () => {
         const result = importSingleText(input, 'Simple.fsh');
         const codeSystem = result.codeSystems.get('ZOO');
         expect(codeSystem.rules).toHaveLength(0);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Simple\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Simple\.fsh.*Line: 3\D/s);
       });
     });
   });
diff --git a/test/import/FSHImporter.Extension.test.ts b/test/import/FSHImporter.Extension.test.ts
index 008f743..97f684f 100644
--- a/test/import/FSHImporter.Extension.test.ts
+++ b/test/import/FSHImporter.Extension.test.ts
@@ -92,8 +92,8 @@ describe('FSHImporter', () => {
         `;
 
         importSingleText(input, 'Dupe.fsh');
-        expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Dupe\.fsh.*Line: 7\D/s);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Dupe\.fsh.*Line: 8\D/s);
+        expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Dupe\.fsh.*Line: 7\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Dupe\.fsh.*Line: 8\D/s);
       });
     });
 
diff --git a/test/import/FSHImporter.Instance.test.ts b/test/import/FSHImporter.Instance.test.ts
index b164916..fb35cb0 100644
--- a/test/import/FSHImporter.Instance.test.ts
+++ b/test/import/FSHImporter.Instance.test.ts
@@ -49,7 +49,7 @@ describe('FSHImporter', () => {
 
         const result = importSingleText(input, 'Missing.fsh');
         expect(result.instances.size).toBe(0);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Missing\.fsh.*Line: 2 - 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Missing\.fsh.*Line: 2 - 3\D/s);
       });
     });
 
@@ -123,8 +123,8 @@ describe('FSHImporter', () => {
         `;
 
         importSingleText(input, 'Dupe.fsh');
-        expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Dupe\.fsh.*Line: 5\D/s);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Dupe\.fsh.*Line: 6\D/s);
+        expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Dupe\.fsh.*Line: 5\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Dupe\.fsh.*Line: 6\D/s);
       });
     });
   });
diff --git a/test/import/FSHImporter.Profile.test.ts b/test/import/FSHImporter.Profile.test.ts
index 021ea17..d6d0e0f 100644
--- a/test/import/FSHImporter.Profile.test.ts
+++ b/test/import/FSHImporter.Profile.test.ts
@@ -149,8 +149,8 @@ describe('FSHImporter', () => {
         `;
 
         importSingleText(input, 'Dupe.fsh');
-        expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Dupe\.fsh.*Line: 7\D/s);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Dupe\.fsh.*Line: 8\D/s);
+        expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Dupe\.fsh.*Line: 7\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Dupe\.fsh.*Line: 8\D/s);
       });
     });
 
@@ -900,7 +900,7 @@ describe('FSHImporter', () => {
         const result = importSingleText(input, 'Obeys.fsh');
         const profile = result.profiles.get('ObservationProfile');
         expect(profile.rules).toHaveLength(0);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Obeys\.fsh.*Line: 4\D/s);
+        expect(loggerSpy.getLastMessage('warn')).toMatch(/File: Obeys\.fsh.*Line: 4\D/s);
       });
     });
   });
diff --git a/test/import/FSHImporter.ValueSet.test.ts b/test/import/FSHImporter.ValueSet.test.ts
index b2e9c7a..5fdcd64 100644
--- a/test/import/FSHImporter.ValueSet.test.ts
+++ b/test/import/FSHImporter.ValueSet.test.ts
@@ -70,9 +70,9 @@ describe('FSHImporter', () => {
         Description: "A duplicate value set for testing metadata"
         `;
         importSingleText(input, 'Dupe.fsh');
-        expect(loggerSpy.getMessageAtIndex(-3)).toMatch(/File: Dupe\.fsh.*Line: 6\D/s);
-        expect(loggerSpy.getMessageAtIndex(-2)).toMatch(/File: Dupe\.fsh.*Line: 7\D/s);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Dupe\.fsh.*Line: 8\D/s);
+        expect(loggerSpy.getMessageAtIndex(-3, 'error')).toMatch(/File: Dupe\.fsh.*Line: 6\D/s);
+        expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(/File: Dupe\.fsh.*Line: 7\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Dupe\.fsh.*Line: 8\D/s);
       });
     });
 
@@ -250,7 +250,7 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetConceptComponent(valueSet.components[0], undefined, undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
       });
 
       it('should log an error when a concept component with a list of concepts does not have a system', () => {
@@ -264,7 +264,7 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetConceptComponent(valueSet.components[0], undefined, undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
       });
 
       it('should log an error when a concept component has a system specified more than once', () => {
@@ -277,7 +277,7 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetConceptComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
       });
     });
 
@@ -400,8 +400,8 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/"=".*string/);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/"=".*string/);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
       });
 
       it('should parse a value set that uses filter operator is-a', () => {
@@ -434,8 +434,8 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('AllUrsinesVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/"is-a".*code/);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Ursines\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/"is-a".*code/);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Ursines\.fsh.*Line: 3\D/s);
       });
 
       it('should parse a value set that uses filter operator descendent-of', () => {
@@ -488,8 +488,8 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('AllFelinesVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/"descendent-of".*code/);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Felines\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/"descendent-of".*code/);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Felines\.fsh.*Line: 3\D/s);
       });
 
       it('should parse a value set that uses filter operator is-not-a', () => {
@@ -522,8 +522,8 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('NonCanineVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/"is-not-a".*code/);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: NonCanine\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/"is-not-a".*code/);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: NonCanine\.fsh.*Line: 3\D/s);
       });
 
       it('should parse a value set that uses filter operator regex', () => {
@@ -554,8 +554,8 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('ProbablyDogsVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/"regex".*regex/);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: MostlyDogs\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/"regex".*regex/);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: MostlyDogs\.fsh.*Line: 3\D/s);
       });
 
       it('should parse a value set that uses filter operator in', () => {
@@ -586,8 +586,8 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('CatAndDogVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/"in".*string/);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: CatDog\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/"in".*string/);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: CatDog\.fsh.*Line: 3\D/s);
       });
 
       it('should parse a value set that uses filter operator not-in', () => {
@@ -618,8 +618,8 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('NoGooseVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/"not-in".*string/);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: NoGoose\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/"not-in".*string/);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: NoGoose\.fsh.*Line: 3\D/s);
       });
 
       it('should parse a value set that uses filter operator generalizes', () => {
@@ -652,8 +652,8 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('MustelidVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/"generalizes".*code/);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Mustelids\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/"generalizes".*code/);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Mustelids\.fsh.*Line: 3\D/s);
       });
 
       it('should parse a value set that uses filter operator exists', () => {
@@ -692,8 +692,8 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/"exists".*boolean/);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/"exists".*boolean/);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
       });
 
       it('should parse a value set that uses multiple filters on a single component', () => {
@@ -749,7 +749,7 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
       });
 
       it('should log an error when a filter (other than the exists filter) has no value', () => {
@@ -762,7 +762,7 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], 'ZOO', undefined, []);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
       });
 
       it('should log an error when a filter component has at least one filter, but no system', () => {
@@ -775,7 +775,7 @@ describe('FSHImporter', () => {
         const valueSet = result.valueSets.get('ZooVS');
         expect(valueSet.components.length).toBe(1);
         assertValueSetFilterComponent(valueSet.components[0], undefined, ['OtherZooVS'], []);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Zoo\.fsh.*Line: 3\D/s);
       });
     });
 
@@ -812,7 +812,7 @@ describe('FSHImporter', () => {
         const result = importSingleText(input, 'Simple.fsh');
         const valueSet = result.valueSets.get('SimpleVS');
         expect(valueSet.rules).toHaveLength(0);
-        expect(loggerSpy.getLastMessage()).toMatch(/File: Simple\.fsh.*Line: 3\D/s);
+        expect(loggerSpy.getLastMessage('error')).toMatch(/File: Simple\.fsh.*Line: 3\D/s);
       });
     });
   });
diff --git a/test/import/FSHImporter.test.ts b/test/import/FSHImporter.test.ts
index 3222485..9389149 100644
--- a/test/import/FSHImporter.test.ts
+++ b/test/import/FSHImporter.test.ts
@@ -33,7 +33,7 @@ describe('FSHImporter', () => {
     Pizza: Large
     `;
     importSingleText(input, 'Pizza.fsh');
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Pizza\.fsh.*Line: 3\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Pizza\.fsh.*Line: 3\D/s);
   });
 
   it('should report extraneous input errors from antlr', () => {
@@ -42,7 +42,7 @@ describe('FSHImporter', () => {
     Parent: Spacious
     `;
     importSingleText(input, 'Space.fsh');
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Space\.fsh.*Line: 2\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Space\.fsh.*Line: 2\D/s);
   });
 
   it('should recover from extraneous input errors from antlr', () => {
@@ -56,7 +56,7 @@ describe('FSHImporter', () => {
     const profile = result.profiles.get('Foo');
     expect(profile.name).toBe('Foo');
     expect(profile.parent).toBe('FooDad');
-    expect(loggerSpy.getLastMessage()).toMatch(/File: Extra\.fsh.*Line: 2\D/s);
+    expect(loggerSpy.getLastMessage('error')).toMatch(/File: Extra\.fsh.*Line: 2\D/s);
   });
 
   it('should parse escaped double-quote and backslash characters in strings', () => {
@@ -124,7 +124,7 @@ describe('FSHImporter', () => {
     //Comment`;
     const result = importSingleText(input);
     expect(result.profiles.size).toBe(1);
-    expect(loggerSpy.getAllLogs().length).toBe(0);
+    expect(loggerSpy.getAllLogs('error').length).toBe(0);
   });
 
   it('should allow a FSH document with a block comment at EOF without newline', () => {
@@ -137,7 +137,7 @@ describe('FSHImporter', () => {
     */`;
     const result = importSingleText(input);
     expect(result.profiles.size).toBe(1);
-    expect(loggerSpy.getAllLogs().length).toBe(0);
+    expect(loggerSpy.getAllLogs('error').length).toBe(0);
   });
 
   it('should adjust indentation of multi-line strings that include blank lines', () => {
@@ -224,4 +224,15 @@ describe('FSHImporter', () => {
     const profile = result.profiles.get('ObservationProfile');
     expect(profile.description).toBe('Descriptions come in only one size.');
   });
+
+  it('should log info messages during import', () => {
+    const input = '';
+    importSingleText(input);
+    const allLogs = loggerSpy.getAllLogs();
+    expect(allLogs.length).toBe(2);
+    expect(allLogs[0].level).toMatch(/info/);
+    expect(allLogs[0].message).toMatch(/Preprocessed/);
+    expect(allLogs[1].level).toMatch(/info/);
+    expect(allLogs[1].message).toMatch(/Imported 0 definitions/);
+  });
 });
diff --git a/test/testhelpers/loggerSpy.ts b/test/testhelpers/loggerSpy.ts
index 30fc23a..648f951 100644
--- a/test/testhelpers/loggerSpy.ts
+++ b/test/testhelpers/loggerSpy.ts
@@ -4,37 +4,43 @@ import { LogEntry } from 'winston';
 class LoggerSpy {
   private mockWriter = jest.spyOn(logger.transports[0], 'write');
 
-  getAllLogs(): LogEntry[] {
-    return this.mockWriter.mock.calls.map(m => m[0]);
+  getAllLogs(level?: string): LogEntry[] {
+    const logs = this.mockWriter.mock.calls.map(m => m[0]);
+    if (level) {
+      return logs.filter(entry => RegExp(level).test(entry.level));
+    } else {
+      return logs;
+    }
   }
 
-  getLogAtIndex(index: number): LogEntry {
-    const i = index < 0 ? this.mockWriter.mock.calls.length + index : index;
-    return this.mockWriter.mock.calls[i]?.[0];
+  getLogAtIndex(index: number, level?: string): LogEntry {
+    const logs = this.getAllLogs(level);
+    const i = index < 0 ? logs.length + index : index;
+    return logs[i];
   }
 
-  getFirstLog(): LogEntry {
-    return this.getLogAtIndex(0);
+  getFirstLog(level?: string): LogEntry {
+    return this.getLogAtIndex(0, level);
   }
 
-  getLastLog(): LogEntry {
-    return this.getLogAtIndex(-1);
+  getLastLog(level?: string): LogEntry {
+    return this.getLogAtIndex(-1, level);
   }
 
-  getAllMessages(): string[] {
-    return this.getAllLogs().map(l => l.message);
+  getAllMessages(level?: string): string[] {
+    return this.getAllLogs(level).map(l => l.message);
   }
 
-  getMessageAtIndex(index: number): string {
-    return this.getLogAtIndex(index)?.message;
+  getMessageAtIndex(index: number, level?: string): string {
+    return this.getLogAtIndex(index, level)?.message;
   }
 
-  getFirstMessage(): string {
-    return this.getMessageAtIndex(0);
+  getFirstMessage(level?: string): string {
+    return this.getMessageAtIndex(0, level);
   }
 
-  getLastMessage(): string {
-    return this.getMessageAtIndex(-1);
+  getLastMessage(level?: string): string {
+    return this.getMessageAtIndex(-1, level);
   }
 
   reset(): void {