Repository: microsoft/ApplicationInsights-node.js
Pull request #575 (2019-12-10 19:28:17)

feat: add In-Proc-Agent
- Adds in-proc-agent for node.js SDK which can be directly imported by compute platforms.
  - `Default.ts`: Base IPA which other compute speciffic platforms will start from
  - `Oryx.ts`: Sets `alr_` prefix and sets up app insights via `Default.ts`

markwolff: /cc @Mikhail-msft
markwolff: Also /cc @arroyc

Patch:
diff --git a/Bootstrap/Default.ts b/Bootstrap/Default.ts
new file mode 100644
index 0000000..6a2e81f
--- /dev/null
+++ b/Bootstrap/Default.ts
@@ -0,0 +1,88 @@
+import * as types from "../applicationinsights";
+
+// Private configuration vars
+let _appInsights: typeof types | null;
+let _logger: AgentLogger = console;
+let _prefix = "ad_"; // App Services, Default
+
+// Env var local constants
+const _setupString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || process.env.APPINSIGHTS_INSTRUMENTATION_KEY;
+const _extensionEnabled = process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION && process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION !== "disabled";
+
+export interface AgentLogger {
+    log(message?: any, ...optional: any[]): void;
+    error(message?: any, ...optional: any[]): void;
+}
+
+function sdkAlreadyExists(): boolean {
+    try {
+        // appInstance should either resolve to user SDK or crash. If it resolves to attach SDK, user probably modified their NODE_PATH
+        const appInstance = require.resolve("applicationinsights"); // assumes that the cwd is near user's package.json
+        const attachInstance = require.resolve("../applicationinsights");
+        if (appInstance !== attachInstance) {
+            _logger.log(
+                "applicationinsights module is already installed in this application; not re-attaching. Installed SDK location:",
+                appInstance
+            );
+            return true;
+        }
+        // User probably modified their NODE_PATH to resolve to this instance. Attach appinsights
+        return false;
+    } catch (e) {
+        // crashed while trying to resolve "applicationinsights", so SDK does not exist. Attach appinsights
+        return false;
+    }
+}
+
+/**
+ * Sets the attach-time logger
+ * @param logger logger which implements the `AgentLogger` interface
+ */
+export function setLogger(logger: AgentLogger) {
+    return _logger = logger;
+}
+
+/**
+ * Sets the string which is prefixed to the exsting sdkVersion, e.g. `ad_`, `alr_`
+ * @param prefix string prefix, including underscore. Defaults to `ad_`
+ */
+export function setUsagePrefix(prefix: string) {
+    _prefix = prefix;
+}
+
+/**
+ * Try to setup and start this app insights instance, if attach is enabled.
+ * @param setupString connection string or instrumentation key
+ */
+export function setupAndStart(setupString = _setupString): typeof types | null {
+    if (!_extensionEnabled && sdkAlreadyExists() === false) {
+        return null;
+    }
+    if (!setupString) {
+        _logger.error(
+            "Application Insights wanted to be started, but no Connection String or Instrumentation Key was provided",
+            setupString
+        );
+        return null;
+    }
+
+    try {
+        _appInsights = require("../applicationinsights");
+        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);
+            }
+            return true;
+        }
+        _appInsights.setup(setupString).setSendLiveMetrics(true);
+        _appInsights.defaultClient.addTelemetryProcessor(prefixInternalSdkVersion);
+        _appInsights.start();
+        _logger.log("Application Insights was started with setupString", setupString, _extensionEnabled);
+    } catch (e) {
+        _logger.error("Error setting up Application Insights", e);
+    }
+    return _appInsights;
+}
diff --git a/Bootstrap/Oryx.ts b/Bootstrap/Oryx.ts
new file mode 100644
index 0000000..5b8e42e
--- /dev/null
+++ b/Bootstrap/Oryx.ts
@@ -0,0 +1,3 @@
+import appInsightsLoader = require('./Default');
+appInsightsLoader.setUsagePrefix("alr_"); // App Services Linux Attach
+appInsightsLoader.setupAndStart();
diff --git a/applicationinsights.ts b/applicationinsights.ts
index efcf55c..7c62898 100644
--- a/applicationinsights.ts
+++ b/applicationinsights.ts
@@ -317,11 +317,11 @@ export class Configuration {
             _performanceLiveMetrics = new AutoCollectPerformance(liveMetricsClient as any, 1000, true);
             liveMetricsClient.addCollector(_performanceLiveMetrics);
             defaultClient.quickPulseClient = liveMetricsClient; // Need this so we can forward all manual tracks to live metrics via PerformanceMetricsTelemetryProcessor
-            _isSendingLiveMetrics = enable;
         } else {
             // qps client already exists; enable/disable it
             liveMetricsClient.enable(enable);
         }
+        _isSendingLiveMetrics = enable;
 
         return Configuration;
     }
@@ -351,4 +351,9 @@ export function dispose() {
     if(_clientRequests) {
         _clientRequests.dispose();
     }
+    if(liveMetricsClient) {
+        liveMetricsClient.enable(false);
+        _isSendingLiveMetrics = false;
+        liveMetricsClient = undefined;
+    }
 }
diff --git a/package.json b/package.json
index 2de1c73..9a42691 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
     "prepublishOnly": "npm run build",
     "pretest": "npm run build",
     "test": "npm run test:ts && npm run test:js",
+    "test:debug": "mocha ./out/Tests --inspect-brk --recursive --no-exit",
     "test:ts": "mocha ./out/Tests --recursive --no-exit",
     "test:js": "mocha ./Tests/js --recursive --no-exit",
     "functionaltest": "npm run build && npm pack && node --use_strict ./Tests/FunctionalTests/RunFunctionalTests.js",


Test patch:
diff --git a/Tests/Bootstrap/Default.spec.ts b/Tests/Bootstrap/Default.spec.ts
new file mode 100644
index 0000000..b0939df
--- /dev/null
+++ b/Tests/Bootstrap/Default.spec.ts
@@ -0,0 +1,111 @@
+import assert = require("assert");
+import sinon = require("sinon");
+import * as types from "../../Bootstrap/Default";
+
+const appInsights = require("../../applicationinsights");
+
+class LoggerSpy implements types.AgentLogger {
+    public logCount = 0
+    public errorCount = 0;
+
+    public log() {
+        this.logCount++;
+    }
+    public error() {
+        this.errorCount++;
+    }
+}
+
+describe("#setupAndStart()", () => {
+    const startSpy = sinon.spy(appInsights, "start");
+
+    before(() => {
+        startSpy.reset();
+    });
+
+    afterEach(() => {
+        startSpy.reset();
+        delete require.cache[require.resolve("../../Bootstrap/Default")];
+    });
+
+    after(() => {
+        startSpy.restore();
+    });
+
+    it("should setup and start the SDK", () => {
+        // Setup env vars before requiring loader
+        const logger = new LoggerSpy();
+        const origEnv = process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION;
+        process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION = "~2";
+
+        // Test
+        const Default = require("../../Bootstrap/Default") as typeof types;
+        Default.setLogger(logger);
+        const instance = Default.setupAndStart("abc");
+        assert.deepEqual(instance, appInsights);
+
+        // Cleanup
+        instance.dispose();
+        process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION = origEnv;
+
+        // start was called once
+        assert.equal(startSpy.callCount, 1);
+
+        // No logging was done
+        assert.equal(logger.logCount, 1);
+        assert.equal(logger.errorCount, 0);
+    });
+
+    it("should not setup and start the SDK if it has been disabled", () => {
+        // Setup
+        const logger = new LoggerSpy();
+        const origEnv = process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION;
+        process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION = "disabled";
+
+        // Test
+        const Default = require("../../Bootstrap/Default") as typeof types;
+        Default.setLogger(logger);
+        const instance = Default.setupAndStart("abc");
+        assert.equal(instance, null);
+
+        // Cleanup
+        process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION = origEnv;
+
+        // start was never called
+        assert.equal(startSpy.callCount, 0);
+
+        // No logging was done
+        assert.equal(logger.logCount, 0);
+        assert.equal(logger.errorCount, 0, "Do not log if attach is disabled");
+    });
+
+    it("should not setup and start the SDK if no setupString is provided", () => {
+        // Setup
+        const logger = new LoggerSpy();
+        const origEnv = process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION;
+        const origIkey = process.env.APPINSIGHTS_INSTRUMENTATION_KEY;
+        const origCs = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
+        process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION = "~2";
+        delete process.env.APPINSIGHTS_INSTRUMENTATION_KEY;
+        delete process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
+
+        // Test
+        const Default = require("../../Bootstrap/Default") as typeof types;
+        Default.setLogger(logger);
+        const instance = Default.setupAndStart();
+        assert.equal(instance, null);
+
+        // Cleanup
+        process.env.APPLICATIONINSIGHTS_EXTENSION_VERSION = origEnv;
+
+        // start was never called
+        assert.equal(startSpy.callCount, 0);
+
+        // No logging was done
+        assert.equal(logger.logCount, 0);
+        assert.equal(logger.errorCount, 1, "Should log if attach is attempted");
+
+        process.env.APPINSIGHTS_INSTRUMENTATION_KEY = origIkey;
+        process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = origCs;
+    });
+});