Repository: microsoft/ApplicationInsights-node.js
Pull request #596 (2020-02-21 21:14:23)

feat: add attach diagnostic logger
- Adds `FileWriter`, matches interface of `console` and ETW logger (aka `AgentLogger` interface)
  -  drop-in replacement for `console.log`/`console.error`, but writes to file instead
- Refactor: Each logger type (`StatusLogger` vs `DiagnosticLogger`) takes an `AgentLogger` to act as its output stream
  - `console` --> output to console
  - `ETW` --> output to ETW
  - `FileWriter` --> output to file



Patch:
diff --git a/Bootstrap/ConsoleStatusLogger.ts b/Bootstrap/ConsoleStatusLogger.ts
deleted file mode 100644
index 99b241e..0000000
--- a/Bootstrap/ConsoleStatusLogger.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { StatusLogger, StatusContract } from "./StatusLogger";
-import { AgentLogger } from "./DataModel";
-
-export class ConsoleStatusLogger implements StatusLogger {
-    public FULL_PATH: string;
-
-    constructor(private _logger: AgentLogger = console) {
-    }
-
-    public isNodeVersionCompatible(): boolean {
-        return true;
-    }
-
-    public makeStatusDirs(filepath?: string): string {
-        // no op
-        return null;
-    }
-
-    public writeFile(data: StatusContract, cb?: Function): void {
-        this._logger.log(data);
-        if (typeof cb === "function") {
-            cb();
-        }
-    }
-
-    public addCloseHandler(): void {
-        // no op
-    }
-}
diff --git a/Bootstrap/DataModel.ts b/Bootstrap/DataModel.ts
index bb03196..092f3bc 100644
--- a/Bootstrap/DataModel.ts
+++ b/Bootstrap/DataModel.ts
@@ -2,3 +2,42 @@ export interface AgentLogger {
     log(message?: any, ...optional: any[]): void;
     error(message?: any, ...optional: any[]): void;
 }
+
+export const enum SeverityLevel {
+    ERROR = "ERROR",
+    WARN = "WARN",
+    INFO = "INFO"
+};
+
+export interface DiagnosticLog {
+    /**
+     * UTC
+     */
+    time: string;
+
+    /**
+     * Log severity, INFO, WARN, ERROR
+     */
+    level: SeverityLevel;
+
+    /**
+     * The logger writing this message. Usually the fully-qualified class or package name
+     */
+    logger: string;
+
+    /**
+     * The log message
+     */
+    message: string;
+
+    /**
+     * Exception (as string)
+     */
+    exception?: string
+
+    /**
+     * Any custom data related to the error/application/operation. Each field should havea string value
+     * Examples: operation, siteName, ikey, extensionVersion, sdkVersion, subscriptionId
+     */
+    properties: { [key: string]: string };
+}
diff --git a/Bootstrap/Default.ts b/Bootstrap/Default.ts
index eeab865..95b2365 100644
--- a/Bootstrap/Default.ts
+++ b/Bootstrap/Default.ts
@@ -2,18 +2,19 @@ import * as types from "../applicationinsights";
 import * as Helpers from "./Helpers";
 import * as DataModel from "./DataModel";
 import { StatusLogger, StatusContract } from "./StatusLogger";
-import { ConsoleStatusLogger } from "./ConsoleStatusLogger";
+import { DiagnosticLogger } from "./DiagnosticLogger";
 
 // Private configuration vars
 let _appInsights: typeof types | null;
-let _logger: DataModel.AgentLogger = console;
 let _prefix = "ad_"; // App Services, Default
-let _statusLogger: StatusLogger = new ConsoleStatusLogger();
+let _logger: DiagnosticLogger = new DiagnosticLogger(console);
+let _statusLogger: StatusLogger = new StatusLogger(console);
 
 // Env var local constants
 const ENV_extensionVersion = "ApplicationInsightsAgent_EXTENSION_VERSION";
 const _setupString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || process.env.APPINSIGHTS_INSTRUMENTATION_KEY;
 const _extensionEnabled = process.env[ENV_extensionVersion] && process.env[ENV_extensionVersion] !== "disabled";
+const forceStart = process.env.APPLICATIONINSIGHTS_FORCE_START === "true";
 
 // Other local constants
 const defaultStatus: StatusContract = {
@@ -25,7 +26,7 @@ const defaultStatus: StatusContract = {
  * Sets the attach-time logger
  * @param logger logger which implements the `AgentLogger` interface
  */
-export function setLogger(logger: DataModel.AgentLogger) {
+export function setLogger(logger: DiagnosticLogger) {
     return _logger = logger;
 }
 
@@ -46,11 +47,8 @@ export function setStatusLogger(statusLogger: StatusLogger) {
  * @param setupString connection string or instrumentation key
  */
 export function setupAndStart(setupString = _setupString): typeof types | null {
-    _statusLogger.makeStatusDirs();
-    _statusLogger.addCloseHandler();
-
     if (!_extensionEnabled) {
-        _statusLogger.writeFile({
+        _statusLogger.logStatus({
             ...defaultStatus,
             AgentInitializedSuccessfully: false,
             Reason: `Extension is not enabled. env.${ENV_extensionVersion}=${process.env[ENV_extensionVersion]}`
@@ -59,8 +57,8 @@ export function setupAndStart(setupString = _setupString): typeof types | null {
     }
 
     // If app already contains SDK, skip agent attach
-    if (Helpers.sdkAlreadyExists(_logger)) {
-        _statusLogger.writeFile({
+    if (!forceStart && Helpers.sdkAlreadyExists(_logger)) {
+        _statusLogger.logStatus({
             ...defaultStatus,
             AgentInitializedSuccessfully: false,
             SDKPresent: true,
@@ -71,8 +69,8 @@ export function setupAndStart(setupString = _setupString): typeof types | null {
 
     if (!setupString) {
         const message = "Application Insights wanted to be started, but no Connection String or Instrumentation Key was provided";
-        _logger.error(message, setupString);
-        _statusLogger.writeFile({
+        _logger.logError(message, setupString);
+        _statusLogger.logStatus({
             ...defaultStatus,
             AgentInitializedSuccessfully: false,
             Reason: message,
@@ -82,12 +80,18 @@ export function setupAndStart(setupString = _setupString): typeof types | null {
 
     try {
         _appInsights = require("../applicationinsights");
+        if (_appInsights.defaultClient) {
+            // setupAndStart was already called, return the result
+            _logger.logError("Setup was attempted on the Application Insights Client multiple times. Aborting and returning the first client instance");
+            return _appInsights;
+        }
+
         const prefixInternalSdkVersion = function (envelope: types.Contracts.Envelope, _contextObjects: Object) {
             try {
                 var appInsightsSDKVersion = _appInsights.defaultClient.context.keys.internalSdkVersion;
                 envelope.tags[appInsightsSDKVersion] = _prefix + envelope.tags[appInsightsSDKVersion];
             } catch (e) {
-                _logger.error("Error prefixing SDK version", e);
+                _logger.logError("Error prefixing SDK version", e);
             }
             return true;
         }
@@ -98,14 +102,14 @@ export function setupAndStart(setupString = _setupString): typeof types | null {
         _appInsights.start();
 
         // Agent successfully instrumented the SDK
-        _logger.log("Application Insights was started with setupString", setupString, _extensionEnabled);
-        _statusLogger.writeFile({
+        _logger.logMessage("Application Insights was started with setupString: " + setupString + ", extensionEnabled: " + _extensionEnabled);
+        _statusLogger.logStatus({
             ...defaultStatus,
             AgentInitializedSuccessfully: true
         });
     } catch (e) {
-        _logger.error("Error setting up Application Insights", e);
-        _statusLogger.writeFile({
+        _logger.logError("Error setting up Application Insights", e);
+        _statusLogger.logStatus({
             ...defaultStatus,
             AgentInitializedSuccessfully: false,
             Reason: `Error setting up Application Insights: ${e && e.message}`
diff --git a/Bootstrap/DiagnosticLogger.ts b/Bootstrap/DiagnosticLogger.ts
new file mode 100644
index 0000000..ec2c814
--- /dev/null
+++ b/Bootstrap/DiagnosticLogger.ts
@@ -0,0 +1,59 @@
+"use strict";
+
+import * as path from "path";
+import * as fs from "fs";
+import * as DataModel from "./DataModel";
+import { FileWriter } from "./FileWriter";
+import { homedir } from "./Helpers/FileHelpers";
+
+export class DiagnosticLogger {
+    public static readonly DEFAULT_FILE_NAME: string = "application-insights-extension.log";
+    public static readonly DEFAULT_LOG_DIR: string = process.env.APPLICATIONINSIGHTS_LOGDIR || path.join(homedir, "LogFiles/ApplicationInsights");
+    public static DefaultEnvelope: DataModel.DiagnosticLog = {
+        message: null,
+        level: null,
+        time: null,
+        logger: "nodejs.applicationinsights",
+        properties: {}
+    }
+
+    constructor(private _writer: DataModel.AgentLogger = console) {}
+
+    logMessage(message: DataModel.DiagnosticLog | string, cb?: (err: Error) => void) {
+        if (typeof cb === "function" && this._writer instanceof FileWriter) {
+            this._writer.callback = cb;
+        }
+        if (typeof message === "string") {
+            const diagnosticMessage: DataModel.DiagnosticLog = {
+                ...DiagnosticLogger.DefaultEnvelope,
+                message,
+                level: DataModel.SeverityLevel.INFO,
+                time: new Date().toUTCString()
+            };
+            this._writer.log(diagnosticMessage);
+        } else {
+            if (message.level === DataModel.SeverityLevel.ERROR) {
+                this._writer.error(message);
+            } else {
+                this._writer.log(message);
+            }
+        }
+    }
+
+    logError(message: DataModel.DiagnosticLog | string, cb?: (err: Error) => void) {
+        if (typeof cb === "function" && this._writer instanceof FileWriter) {
+            this._writer.callback = cb;
+        }
+        if (typeof message === "string") {
+            const diagnosticMessage: DataModel.DiagnosticLog = {
+                ...DiagnosticLogger.DefaultEnvelope,
+                message,
+                level: DataModel.SeverityLevel.ERROR,
+                time: new Date().toUTCString()
+            };
+            this._writer.error(diagnosticMessage);
+        } else {
+            this._writer.error(message);
+        }
+    }
+}
diff --git a/Bootstrap/FileWriter.ts b/Bootstrap/FileWriter.ts
new file mode 100644
index 0000000..443f56a
--- /dev/null
+++ b/Bootstrap/FileWriter.ts
@@ -0,0 +1,139 @@
+"use strict";
+
+import * as path from "path";
+import * as fs from "fs";
+import * as os from "os";
+import * as DataModel from "./DataModel";
+import * as FileHelpers from "./Helpers/FileHelpers";
+
+export interface FileWriterOptions {
+    append: boolean; // Overwrite or append on file write (false)
+    deleteOnExit: boolean; // (true)
+    sizeLimit: number; // (10 KB)
+    renamePolicy: "rolling" | "overwrite" | "stop"; // What to do with file when it exceeds time/size limits
+    chmod: number; // Linux only
+}
+
+export const homedir = FileHelpers.homedir;
+
+export class FileWriter implements DataModel.AgentLogger {
+    public callback = (_err: Error) => { }; // no-op
+    private _ready = false;
+    private _options: FileWriterOptions;
+    private static _fullpathsToDelete: string[] = [];
+    private static _listenerAttached = false;
+    private static DEFAULT_OPTIONS: FileWriterOptions = {
+        append: false,
+        deleteOnExit: true,
+        sizeLimit: 10*1024,
+        renamePolicy: "stop",
+        chmod: 0o644 // rw/r/r
+    }
+
+    public static isNodeVersionCompatible() {
+        const majVer = process.versions.node.split(".")[0];
+        return parseInt(majVer) >= 1;
+    }
+
+    // leave at "keep at single file only", "write up to certain size limit", "clear old file on process startup"
+    constructor(private _filepath: string, private _filename: string, options?: Partial<FileWriterOptions>) {
+        this._options = { ...FileWriter.DEFAULT_OPTIONS, ...options };
+        this._ready = FileWriter.isNodeVersionCompatible() && FileHelpers.makeStatusDirs(this._filepath);
+        if (this._options.deleteOnExit) {
+            FileWriter._addCloseHandler();
+            FileWriter._fullpathsToDelete.push(path.join(this._filepath, this._filename));
+        }
+    }
+
+    public log(message: any) {
+        if (this._ready) {
+            let data = typeof message === "object"
+                ? JSON.stringify(message, null, 2)
+                : message.toString();
+
+            // Check if existing file needs to be renamed
+            this._shouldRenameFile((err, shouldRename) => {
+                if (err) return;
+
+                if (shouldRename) {
+                    if (this._options.renamePolicy === "rolling") {
+                        FileHelpers.renameCurrentFile(this._filepath, this._filename, (renameErr, renamedFullpath) => {
+                            if (renameErr) return;
+                            FileWriter._fullpathsToDelete.push(renamedFullpath);
+                            this._options.append
+                                ? this._appendFile(data + "\n")
+                                : this._writeFile(data);
+                        });
+                    } else if (this._options.renamePolicy === "overwrite") {
+                        // Clear the current file
+                        this._writeFile(data);
+                    } else if (this._options.renamePolicy === "stop") {
+                        // Stop future logging
+                        this._ready = false;
+                    }
+                } else {
+                    this._options.append
+                    ? this._appendFile(data + "\n")
+                    : this._writeFile(data);
+                }
+            });
+
+        }
+    }
+
+    public error(message: any) {
+        this.log(message);
+    }
+
+    private _appendFile(message: string) {
+        const fullpath = path.join(this._filepath, this._filename);
+        fs.appendFile(fullpath, message, (err) => {
+            this.callback(err);
+        });
+    }
+
+    private _writeFile(message: string) {
+        const fullpath = path.join(this._filepath, this._filename);
+        fs.writeFile(fullpath, message, { mode: this._options.chmod }, this.callback);
+    }
+
+    private static _addCloseHandler() {
+        if (!FileWriter._listenerAttached) {
+            process.on("exit", () => {
+                FileWriter._fullpathsToDelete.forEach((filename) => {
+                    try {
+                        fs.unlinkSync(filename);
+                    } catch (err) { /** ignore errors */ }
+                });
+            });
+            FileWriter._listenerAttached = true;
+        }
+    }
+
+    private _shouldRenameFile(callback?: (err: Error | null, shouldRename?: boolean) => void): void {
+        const fullpath = path.join(this._filepath, this._filename);
+        fs.stat(fullpath, (err, stats) => {
+            if (err) {
+                if (err.code === "ENOENT" && typeof callback === "function") {
+                    callback(null, false);
+                } else if (typeof callback === "function") {
+                    callback(err);
+                }
+                return;
+            }
+
+            if (stats.size > this._options.sizeLimit) {
+                callback(null, true);
+            } else {
+                const createDate = new Date(stats.birthtime);
+                const currentDate = new Date();
+                const result = (
+                    createDate.getUTCDate() !== currentDate.getUTCDate() ||
+                    createDate.getUTCMonth() !== currentDate.getUTCMonth() ||
+                    createDate.getUTCFullYear() !== currentDate.getUTCFullYear()
+                );
+                callback(null, result);
+            }
+        });
+    }
+}
diff --git a/Bootstrap/Helpers.ts b/Bootstrap/Helpers.ts
index b40486d..ad6c266 100644
--- a/Bootstrap/Helpers.ts
+++ b/Bootstrap/Helpers.ts
@@ -1,9 +1,9 @@
-import { AgentLogger } from "./DataModel";
+import { DiagnosticLogger } from "./DiagnosticLogger";
 
-export function sdkAlreadyExists(_logger: AgentLogger = console): boolean {
+export function sdkAlreadyExists(_logger: DiagnosticLogger): boolean {
     try {
         // appInstance should either resolve to user SDK or crash. If it resolves to attach SDK, user probably modified their NODE_PATH
-        let appInstance;
+        let appInstance: string;
         try {
             // Node 8.9+
             appInstance = (require.resolve as any)("applicationinsights", { paths: process.cwd() });
@@ -13,8 +13,8 @@ export function sdkAlreadyExists(_logger: AgentLogger = console): boolean {
         }
         const attachInstance = require.resolve("../applicationinsights");
         if (appInstance !== attachInstance) {
-            _logger.log(
-                "applicationinsights module is already installed in this application; not re-attaching. Installed SDK location:",
+            _logger.logMessage(
+                "applicationinsights module is already installed in this application; not re-attaching. Installed SDK location: " +
                 appInstance
             );
             return true;
diff --git a/Bootstrap/Helpers/FileHelpers.ts b/Bootstrap/Helpers/FileHelpers.ts
new file mode 100644
index 0000000..961e870
--- /dev/null
+++ b/Bootstrap/Helpers/FileHelpers.ts
@@ -0,0 +1,75 @@
+"use strict";
+
+import * as path from "path";
+import * as fs from "fs";
+import * as os from "os";
+
+export const homedir = os.homedir ? os.homedir() : (process.env[(process.platform == "win32") ? "USERPROFILE" : "HOME"]);
+
+/**
+ * Zero dependencies: recursive mkdir
+ */
+function mkDirByPathSync(HOME_DIR: string, targetDir: string, { isRelativeToScript = false } = {}) {
+    const sep = path.sep;
+    const initDir = path.isAbsolute(targetDir) ? sep : "";
+    const baseDir = isRelativeToScript ? __dirname : ".";
+
+    return targetDir.split(sep).reduce((parentDir, childDir) => {
+        const curDir = path.resolve(baseDir, parentDir, childDir);
+        try {
+            // Don't try to recreate homedir
+            if (HOME_DIR.indexOf(curDir) === -1) {
+                fs.mkdirSync(curDir);
+            }
+        } catch (err) {
+            if (err.code === "EEXIST") { // curDir already exists!
+                return curDir;
+            }
+
+            // To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows.
+            if (err.code === "ENOENT") { // Throw the original parentDir error on curDir `ENOENT` failure.
+                throw new Error(`EACCES: permission denied, mkdir "${parentDir}"`);
+            }
+
+            const caughtErr = ["EACCES", "EPERM", "EISDIR"].indexOf(err.code) > -1;
+            if (!caughtErr || caughtErr && curDir === path.resolve(targetDir)) {
+                throw err; // Throw if it's just the last created dir.
+            }
+        }
+        return curDir;
+
+    }, initDir);
+}
+
+export function makeStatusDirs(filepath: string): boolean {
+    try {
+        mkDirByPathSync(homedir, filepath.replace(/\\/g, path.sep).replace(/\//g, path.sep));
+        return true;
+    } catch (e) {
+        console.error("Error creating Application Insights status folder", e);
+        return false;
+    }
+}
+
+
+
+export function renameCurrentFile(filepath: string, filename: string, callback?: (err: Error | null, destfullpath?: string) => void): void {
+    const fullpath = path.join(filepath, filename);
+    const basename = path.basename(filename, path.extname(filename));
+    const stats = fs.stat(fullpath, (statsErr, stats) => {
+        if (statsErr) {
+            return callback(statsErr);
+        }
+
+        const createDate = new Date(stats.birthtime);
+        const destfilename = basename + "-" +
+            createDate.toISOString().replace(/[T:\.]/g, "_").replace("Z", "") +
+            path.extname(filename) + ".old";
+        const destfullpath = path.join(filepath, destfilename);
+        fs.rename(fullpath, destfullpath, (renameErr) => {
+            if (typeof callback === "function") {
+                callback(renameErr, destfullpath);
+            }
+        });
+    });
+}
diff --git a/Bootstrap/Oryx.ts b/Bootstrap/Oryx.ts
index 2f2435d..73c54aa 100644
--- a/Bootstrap/Oryx.ts
+++ b/Bootstrap/Oryx.ts
@@ -1,5 +1,26 @@
 import * as types from "../applicationinsights";
-import appInsightsLoader = require('./Default');
+import { FileWriter } from "./FileWriter";
+import { StatusLogger } from "./StatusLogger";
+import { DiagnosticLogger } from "./DiagnosticLogger";
+import appInsightsLoader = require("./Default");
+
 appInsightsLoader.setUsagePrefix("alr_"); // App Services Linux Attach
+
+// Set Status.json logger
+appInsightsLoader.setStatusLogger(new StatusLogger(new FileWriter(
+    StatusLogger.DEFAULT_FILE_PATH,
+    StatusLogger.DEFAULT_FILE_NAME,
+    { deleteOnExit: true, renamePolicy: "stop" }
+)));
+
+// Set Attach Diagnostic Logger
+appInsightsLoader.setLogger(new DiagnosticLogger(new FileWriter(
+    DiagnosticLogger.DEFAULT_LOG_DIR,
+    DiagnosticLogger.DEFAULT_FILE_NAME,
+    { append: true, deleteOnExit: true, renamePolicy: "stop" }
+)));
+
+// Start the SDK
 var appInsights = appInsightsLoader.setupAndStart();
+
 export = appInsights;
diff --git a/Bootstrap/StatusLogger.ts b/Bootstrap/StatusLogger.ts
index 0dd79db..ef9f66c 100644
--- a/Bootstrap/StatusLogger.ts
+++ b/Bootstrap/StatusLogger.ts
@@ -1,6 +1,10 @@
+"use strict";
+
 import * as os from "os";
 import * as path from "path";
 import * as fs from "fs";
+import * as DataModel from "./DataModel";
+import { FileWriter, homedir } from "./FileWriter";
 
 export interface StatusContract {
     AgentInitializedSuccessfully: boolean;
@@ -20,94 +24,29 @@ function readPackageVersion() {
         if (packageJson && typeof packageJson.version === "string") {
             return packageJson.version;
         }
-    } catch (e) {}
+    } catch (e) { }
     return "unknown";
 }
 
-/**
- * Zero dependencies: recursive mkdir
- */
-function mkDirByPathSync(HOME_DIR: string, targetDir: string, { isRelativeToScript = false } = {}) {
-    const sep = path.sep;
-    const initDir = path.isAbsolute(targetDir) ? sep : "";
-    const baseDir = isRelativeToScript ? __dirname : ".";
-
-    return targetDir.split(sep).reduce((parentDir, childDir) => {
-        const curDir = path.resolve(baseDir, parentDir, childDir);
-        try {
-            // Don't try to recreate homedir
-            if (HOME_DIR.indexOf(curDir) === -1) {
-                fs.mkdirSync(curDir);
-            }
-        } catch (err) {
-            if (err.code === "EEXIST") { // curDir already exists!
-                return curDir;
-            }
-
-            // To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows.
-            if (err.code === "ENOENT") { // Throw the original parentDir error on curDir `ENOENT` failure.
-                throw new Error(`EACCES: permission denied, mkdir "${parentDir}"`);
-            }
-
-            const caughtErr = ["EACCES", "EPERM", "EISDIR"].indexOf(err.code) > -1;
-            if (!caughtErr || caughtErr && curDir === path.resolve(targetDir)) {
-                throw err; // Throw if it"s just the last created dir.
-            }
-        }
-        return curDir;
-
-    }, initDir);
-}
-
 export class StatusLogger {
-    private _HOME_DIR?: string = os.homedir ? os.homedir() :( process.env[(process.platform == "win32") ? "USERPROFILE" : "HOME"]);
-    private _FILE_PATH?: string = `${this._HOME_DIR}/LogFiles/ApplicationInsights/status/`;
-    private _FILE_NAME?: string = `status_${os.hostname()}_${process.pid}.json`;
-
-    private static _APP_TYPE?: string = "node.js";
-    public static DEFAULT_STATUS?: StatusContract = {
+    public static readonly DEFAULT_FILE_PATH: string = path.join(homedir, "status");
+    public static readonly DEFAULT_FILE_NAME: string = `status_${os.hostname()}_${process.pid}.json`;
+    public static readonly DEFAULT_STATUS: StatusContract = {
         AgentInitializedSuccessfully: false,
         SDKPresent: false,
-        Ikey: null,
-        AppType: StatusLogger._APP_TYPE,
+        Ikey: "unknown",
+        AppType: "node.js",
         SdkVersion: readPackageVersion(),
         MachineName: os.hostname(),
         PID: String(process.pid)
     }
 
-    public FULL_PATH = this._FILE_PATH + this._FILE_NAME;
-
-    public isNodeVersionCompatible() {
-      const majVer = process.versions.node.split(".")[0];
-      return parseInt(majVer) >= 1;
-    }
-
+    constructor(public _writer: DataModel.AgentLogger = console) {}
 
-    public makeStatusDirs(filepath = this._FILE_PATH) {
-        try {
-            return mkDirByPathSync(this._HOME_DIR, filepath.replace(/\\/g, path.sep).replace(/\//g, path.sep));
-        } catch (e) {
-            console.error("Error creating Application Insights status folder", e);
+    public logStatus(data: StatusContract, cb?: (err: Error) => void) {
+        if (typeof cb === "function" && this._writer instanceof FileWriter) {
+            this._writer.callback = cb;
         }
-    }
-
-    public writeFile(data: StatusContract, cb?: Function) {
-        fs.open(this.FULL_PATH, "w", (err, fid) => {
-            if (err) return;
-            fs.writeFile(this.FULL_PATH, JSON.stringify(data, null, 2), { encoding: "utf8" }, (err) => {
-                if (err) {
-                    console.error("Error writing Application Insights status file", err);
-                }
-                if (cb && typeof cb === "function") {
-                    cb(err);
-                }
-            });
-        });
-    }
-
-    public addCloseHandler() {
-        process.on("exit", () => { try {
-            fs.unlinkSync(this.FULL_PATH);
-        } catch (err) { }});
+        this._writer.log(data);
     }
 }


Test patch:
diff --git a/Tests/Bootstrap/Default.spec.ts b/Tests/Bootstrap/Default.spec.ts
index df53293..1a636dd 100644
--- a/Tests/Bootstrap/Default.spec.ts
+++ b/Tests/Bootstrap/Default.spec.ts
@@ -1,5 +1,6 @@
 import assert = require("assert");
 import sinon = require("sinon");
+import { DiagnosticLogger } from "../../Bootstrap/DiagnosticLogger";
 import * as DataModel from "../../Bootstrap/DataModel";
 import * as Helpers from "../../Bootstrap/Helpers";
 import * as DefaultTypes from "../../Bootstrap/Default";
@@ -34,6 +35,29 @@ describe("#setupAndStart()", () => {
         startSpy.restore();
     });
 
+    it("should return the client if started multiple times", () => {
+        const logger = new LoggerSpy();
+        const origEnv = process.env.ApplicationInsightsAgent_EXTENSION_VERSION;
+        process.env.ApplicationInsightsAgent_EXTENSION_VERSION = "~2";
+        const alreadyExistsStub = sinon.stub(Helpers, "sdkAlreadyExists", () => false);
+
+        // Test
+        const Default = require("../../Bootstrap/Default") as typeof DefaultTypes;
+        Default.setLogger(new DiagnosticLogger(logger));
+        const instance1 = Default.setupAndStart("abc");
+        assert.ok(instance1.defaultClient);
+        const instance2 = Default.setupAndStart("abc");
+        assert.deepEqual(instance1.defaultClient, instance2.defaultClient);
+        assert.deepEqual(instance1.defaultClient["_telemetryProcessors"].length, 1)
+        assert.deepEqual(instance2.defaultClient["_telemetryProcessors"].length, 1)
+
+        // Cleanup
+        alreadyExistsStub.restore();
+        instance1.dispose();
+        instance2.dispose();
+        process.env.ApplicationInsightsAgent_EXTENSION_VERSION = origEnv;
+    })
+
     it("should setup and start the SDK", () => {
         // Setup env vars before requiring loader
         const logger = new LoggerSpy();
@@ -43,7 +67,7 @@ describe("#setupAndStart()", () => {
 
         // Test
         const Default = require("../../Bootstrap/Default") as typeof DefaultTypes;
-        Default.setLogger(logger);
+        Default.setLogger(new DiagnosticLogger(logger));
         const instance = Default.setupAndStart("abc");
         assert.deepEqual(instance, appInsights);
 
@@ -69,7 +93,7 @@ describe("#setupAndStart()", () => {
 
         // Test
         const Default = require("../../Bootstrap/Default") as typeof DefaultTypes;
-        Default.setLogger(logger);
+        Default.setLogger(new DiagnosticLogger(logger));
         const instance = Default.setupAndStart("abc");
         assert.equal(instance, null);
 
@@ -98,7 +122,7 @@ describe("#setupAndStart()", () => {
 
         // Test
         const Default = require("../../Bootstrap/Default") as typeof DefaultTypes;
-        Default.setLogger(logger);
+        Default.setLogger(new DiagnosticLogger(logger));
         const instance = Default.setupAndStart();
         assert.equal(instance, null);
 
diff --git a/Tests/Bootstrap/FileWriter.spec.ts b/Tests/Bootstrap/FileWriter.spec.ts
new file mode 100644
index 0000000..2607c17
--- /dev/null
+++ b/Tests/Bootstrap/FileWriter.spec.ts
@@ -0,0 +1,186 @@
+import assert = require("assert");
+import sinon = require("sinon");
+import path = require("path");
+import os = require("os");
+import fs = require("fs");
+import { FileWriter, homedir } from "../../Bootstrap/FileWriter";
+import FileHelpers = require("../../Bootstrap/Helpers/FileHelpers");
+
+describe("FileWriter", () => {
+    if (!FileWriter.isNodeVersionCompatible()) {
+        return;
+    }
+
+    const filedir = path.join(homedir, "LogFiles/ApplicationInsights/test");
+    describe("#constructor()", () => {
+        it("should return a ready FileWriter", () => {
+            if (parseInt(process.versions.node.split(".")[0]) >= 1) {
+                const writer = new FileWriter(filedir, "test.txt");
+                assert.deepEqual(writer["_ready"], true);
+                assert.deepEqual(fs.existsSync(filedir), true);
+            } else {
+                assert.ok(true, "skipped");
+            }
+        })
+    });
+
+    describe("#log()", () => {
+        it("should not log if the FileWriter is not ready", () => {
+            const writer = new FileWriter(filedir, "test.txt");
+            const stub = sinon.stub(writer, "_writeFile");
+            writer["_ready"] = false;
+
+            assert.ok(stub.notCalled);
+            writer.log("foo");
+            assert.ok(stub.notCalled);
+
+            stub.restore();
+        });
+    });
+
+    describe("#error()", () => {
+        it("should call log()", () => {
+            const writer = new FileWriter(filedir, "test.txt");
+            const stub = sinon.stub(writer, "log");
+
+            assert.ok(stub.notCalled);
+            writer.error("example");
+            assert.ok(stub.calledOnce);
+            assert.deepEqual(stub.args[0][0], "example");
+            stub.restore();
+        });
+    });
+
+    describe("#_writeFile()", () => {
+        it("should write a new file in custom folder", (done) => {
+            const writer = new FileWriter(os.tmpdir(), "tempfile.txt");
+            writer.callback = (err) => {
+                assert.deepEqual(err, null);
+                const content = fs.readFileSync(path.join(os.tmpdir(), "tempfile.txt"), "utf8");
+                assert.deepEqual(content, "temp:foo");
+                done();
+            }
+            writer.log("temp:foo");
+        });
+
+        it("should write a new file", (done) => {
+            const writer = new FileWriter(filedir, "newfile.txt");
+            writer.callback = (err) => {
+                assert.deepEqual(err, null);
+                const content = fs.readFileSync(path.join(filedir, "newfile.txt"), "utf8");
+                assert.deepEqual(content, "newfile #1");
+                done();
+            }
+            writer["_writeFile"]("newfile #1");
+        });
+
+        it("should overwrite an existing file, if one already exists", (done) => {
+            const writer = new FileWriter(filedir, "test.txt");
+            writer.callback = (err) => {
+                assert.deepEqual(err, null);
+                const content = fs.readFileSync(path.join(filedir, "test.txt"), "utf8");
+                assert.deepEqual(content, "write #1");
+                writer.callback = (err) => {
+                    assert.deepEqual(err, null);
+                    const content = fs.readFileSync(path.join(filedir, "test.txt"), "utf8");
+                    assert.deepEqual(content, "write #2");
+                    done();
+                }
+                writer["_writeFile"]("write #2");
+            }
+            writer["_writeFile"]("write #1");
+        });
+    });
+
+    describe("#_appendFile()", () => {
+        it("should append the file contents", (done) => {
+            let counter = 0;
+            try {
+                // Try to delete the file we are appending
+                fs.unlinkSync(path.join(filedir, "append.txt"));
+            } catch (e) {}
+            const writer = new FileWriter(filedir, "append.txt", { append: true });
+            writer.callback = (err) => {
+                if (counter < 3) {
+                    counter += 1;
+                    writer.log(`line #${counter}`);
+                } else {
+                    const content = fs.readFileSync(path.join(filedir, "append.txt"), "utf8");
+                    assert.deepEqual(content, "line #0\nline #1\nline #2\nline #3\n");
+                    done();
+                }
+            }
+            writer.log(`line #${counter}`);
+        });
+    });
+
+    describe("#_addCloseHandler()", () => {
+        it("should add the delete handler when configured to do so", (done) => {
+            const writer = new FileWriter(filedir, "test.txt");
+            writer.callback = (err) => {
+                assert.equal(err, null);
+                done();
+            }
+            writer.log("example");
+        });
+    });
+
+    describe("#_shouldRenameFile()", () => {
+        it("should return true when the date has changed", (done) => {
+            try {
+                // Try to delete the file we are testing
+                fs.unlinkSync(path.join(filedir, "clocktest.txt"));
+            } catch (e) {}
+            const sandbox = sinon.sandbox.create();
+            const clock = sandbox.useFakeTimers(Date.now());
+            const writer = new FileWriter(filedir, "clocktest.txt");
+            writer.callback = (err) => {
+                assert.equal(err, null);
+                writer["_shouldRenameFile"]((err1, shouldRename1) => {
+                    assert.deepEqual(err1, null);
+                    assert.deepEqual(shouldRename1, false);
+                    clock.tick(86400000); // 1 day
+                    writer["_shouldRenameFile"]((err2, shouldRename2) => {
+                        assert.deepEqual(err2, null);
+                        assert.deepEqual(shouldRename2, true);
+                        clock.restore();
+                        sandbox.restore();
+                        done();
+                    });
+                });
+            }
+            writer.log("message");
+        });
+    });
+
+    describe("#renameCurrentFile()", () => {
+        it("should rename the current file", (done) => {
+            const writer = new FileWriter(filedir, "renametest.txt");
+            writer.callback = (err) => {
+                assert.deepEqual(err, null);
+                const birthdate = new Date(fs.statSync(path.join(filedir, "renametest.txt")).birthtime);
+
+                // Rename the file
+                FileHelpers.renameCurrentFile(filedir, "renametest.txt", (err, renamedfullpath) => {
+                    // Assert previously named file no longer exists
+                    try {
+                        const content = fs.readFileSync(path.join(filedir, "renametest.txt"));
+                        assert.ok(false, "File should no longer exist");
+                    } catch (e) {
+                        assert.deepEqual(e.code, "ENOENT", "File should no longer exist");
+                    }
+
+                    // Assert renamed file has identical contents and was renamed properly
+                    assert.deepEqual(renamedfullpath, path.join(filedir, `renametest-${birthdate.toISOString().replace(/[T:\.]/g, "_").replace("Z", "")}.txt.old`));
+                    const content = fs.readFileSync(renamedfullpath, "utf8");
+                    assert.deepEqual(content, "foo");
+
+                    // Cleanup
+                    fs.unlinkSync(renamedfullpath);
+                    done();
+                });
+            }
+            writer.log("foo"); // create the file
+        });
+    });
+});
diff --git a/Tests/Bootstrap/StatusLogger.spec.ts b/Tests/Bootstrap/StatusLogger.spec.ts
index bcea0cc..11a3c14 100644
--- a/Tests/Bootstrap/StatusLogger.spec.ts
+++ b/Tests/Bootstrap/StatusLogger.spec.ts
@@ -1,36 +1,41 @@
 import * as fs from "fs";
 import assert = require("assert");
 import sinon = require("sinon");
+import path = require("path");
+import os = require("os");
 import { StatusLogger } from "../../Bootstrap/StatusLogger";
+import { FileWriter, homedir } from "../../Bootstrap/FileWriter";
 
-describe("#writeFile()", () => {
+describe("#logStatus()", () => {
     it("should write a status file to disk", (done) => {
-        const statusLogger = new StatusLogger();
-        if (!statusLogger.isNodeVersionCompatible()) {
+        const filepath = path.join(homedir, "LogFiles/ApplicationInsights/status");
+        const filename = StatusLogger.DEFAULT_FILE_NAME;
+        const fileWriter = new FileWriter(filepath, filename);
+        if (!FileWriter.isNodeVersionCompatible()) {
             done();
         } else {
-            statusLogger.makeStatusDirs();
-            statusLogger.writeFile(StatusLogger.DEFAULT_STATUS, () => {
-                const buffer = JSON.parse(fs.readFileSync(statusLogger.FULL_PATH, "utf8"));
+            const statusLogger = new StatusLogger(fileWriter);
+            statusLogger.logStatus(StatusLogger.DEFAULT_STATUS, (err: Error) => {
+                assert.equal(err, null);
+                const buffer = JSON.parse(fs.readFileSync(path.join(filepath, filename), "utf8"));
                 assert.deepEqual(buffer, StatusLogger.DEFAULT_STATUS);
                 done();
             });
         }
     });
-});
 
-describe("#addCloseHandler()", () => {
-    it("should add a process exit handler", () => {
-        const statusLogger = new StatusLogger();
-        if (statusLogger.isNodeVersionCompatible()) {
-            const processSpy = sinon.spy(process, "on");
-            assert.ok(processSpy.notCalled);
+    it("should write status to console", () => {
+        const consoleStub = sinon.stub(console, "log");
 
-            statusLogger.addCloseHandler();
-            assert.equal(processSpy.callCount, 1);
-            assert.equal(processSpy.args[0][0], "exit");
+        // Act
+        const statusLogger = new StatusLogger(console);
+        statusLogger.logStatus(StatusLogger.DEFAULT_STATUS);
 
-            processSpy.restore();
-        }
-    })
+        // Assert
+        assert.ok(consoleStub.calledOnce);
+        assert.deepEqual(consoleStub.args[0][0], StatusLogger.DEFAULT_STATUS);
+
+        // Cleanup
+        consoleStub.restore();
+    });
 });
diff --git a/Tests/FunctionalTests/RunFunctionalTests.js b/Tests/FunctionalTests/RunFunctionalTests.js
index c097a91..23fe3ad 100644
--- a/Tests/FunctionalTests/RunFunctionalTests.js
+++ b/Tests/FunctionalTests/RunFunctionalTests.js
@@ -67,7 +67,7 @@ function startDocker() {
         run("docker run -d -p 27017:27017 --name ainjsmongo mongo"),
         run("docker run -e MYSQL_ROOT_PASSWORD=dummypw -e MYSQL_DATABASE=testdb -d -p 33060:3306 --name ainjsmysql mysql:5"),
         run("docker run -d -p 63790:6379 --name ainjsredis redis:alpine"),
-        run("docker run -d -p 54320:5432 --name ainjspostgres postgres:alpine")
+        run("docker run -e POSTGRES_PASSWORD=dummypw -d -p 54320:5432 --name ainjspostgres postgres:alpine")
     ];
 
     for(let i = 0; i < tasks.length; i++) {
diff --git a/Tests/FunctionalTests/TestApp/Config.js b/Tests/FunctionalTests/TestApp/Config.js
index 5b93e5a..de44c2d 100644
--- a/Tests/FunctionalTests/TestApp/Config.js
+++ b/Tests/FunctionalTests/TestApp/Config.js
@@ -5,7 +5,7 @@ var Config = {
     MongoDbName: "testapp",
     MySqlConnectionString: "mysql://root:dummypw@localhost:33060/testdb",
     RedisConnectionString: "redis://localhost:63790",
-    PostgresConnectionString: "pg://postgres@localhost:54320/postgres",
+    PostgresConnectionString: "pg://postgres:dummypw@localhost:54320/postgres",
     InstrumentationKey: "TESTIKEY",
     AppInsightsEnabled: true,
     UseAutoCorrelation: true,
diff --git a/Tests/js/endToEnd.js b/Tests/js/endToEnd.js
index 2224bda..053f4e8 100644
--- a/Tests/js/endToEnd.js
+++ b/Tests/js/endToEnd.js
@@ -106,7 +106,7 @@ describe('module', function () {
             var handler;
             if (handler = getLegacyHandler()) {
                 handler(new Error('legacy error'));
-                assert.equal(exitStub.callCount, 1, 'Legacy handler rethrows when no');
+                assert.equal(exitStub.callCount, 1, 'Legacy handler rethrows when no handlers exist');
             } else if (handler = getMonitorHandler()) {
                 handler(new Error('monitor handler'));
                 assert.equal(exitStub.callCount, 0, 'Monitor handler does not cause the throw');