Repository: microsoft/ApplicationInsights-node.js
Pull request #737 (2021-03-03 00:59:47)

Roles support, Endpoint redirects and ping interval hints for Live Metrics


Patch:
diff --git a/Declarations/Contracts/QuickPulseTypes/EnvelopeQuickPulse.ts b/Declarations/Contracts/QuickPulseTypes/EnvelopeQuickPulse.ts
index 157366e..9cc2784 100644
--- a/Declarations/Contracts/QuickPulseTypes/EnvelopeQuickPulse.ts
+++ b/Declarations/Contracts/QuickPulseTypes/EnvelopeQuickPulse.ts
@@ -6,6 +6,8 @@ export interface EnvelopeQuickPulse {
 
     Instance: string;
 
+    RoleName: string;
+
     InstrumentationKey: string;
 
     InvariantVersion: number;
diff --git a/Library/QuickPulseEnvelopeFactory.ts b/Library/QuickPulseEnvelopeFactory.ts
index f8ef2ca..2983864 100644
--- a/Library/QuickPulseEnvelopeFactory.ts
+++ b/Library/QuickPulseEnvelopeFactory.ts
@@ -19,6 +19,11 @@ class QuickPulseEnvelopeFactory {
             && context.keys.cloudRoleInstance
             && context.tags[context.keys.cloudRoleInstance]) || machineName;
 
+        const roleName = (context.tags
+            && context.keys
+            && context.keys.cloudRole
+            && context.tags[context.keys.cloudRole]) || null;
+
         var envelope: Contracts.EnvelopeQuickPulse = {
             Documents: documents.length > 0 ? documents : null,
             InstrumentationKey: config.instrumentationKey || "",
@@ -28,7 +33,8 @@ class QuickPulseEnvelopeFactory {
             Version: context.tags[context.keys.internalSdkVersion],
             StreamId: StreamId,
             MachineName: machineName,
-            Instance: instance
+            Instance: instance,
+            RoleName: roleName
         }
 
         return envelope;
diff --git a/Library/QuickPulseSender.ts b/Library/QuickPulseSender.ts
index 147c94d..d1971ce 100644
--- a/Library/QuickPulseSender.ts
+++ b/Library/QuickPulseSender.ts
@@ -12,6 +12,14 @@ import * as Contracts from "../Declarations/Contracts";
 const QuickPulseConfig = {
     method: "POST",
     time: "x-ms-qps-transmission-time",
+    pollingIntervalHint: "x-ms-qps-service-polling-interval-hint",
+    endpointRedirect: "x-ms-qps-service-endpoint-redirect",
+    instanceName: "x-ms-qps-instance-name",
+    streamId: "x-ms-qps-stream-id",
+    machineName: "x-ms-qps-machine-name",
+    roleName: "x-ms-qps-role-name",
+    streamid: "x-ms-qps-stream-id",
+    invariantVersion: "x-ms-qps-invariant-version",
     subscribed: "x-ms-qps-subscribed"
 };
 
@@ -27,21 +35,41 @@ class QuickPulseSender {
         this._consecutiveErrors = 0;
     }
 
-    public ping(envelope: Contracts.EnvelopeQuickPulse, done: (shouldPOST?: boolean, res?: http.IncomingMessage) => void): void {
-        this._submitData(envelope, done, "ping");
+    public ping(envelope: Contracts.EnvelopeQuickPulse, 
+            redirectedHostEndpoint: string,
+            done: (shouldPOST?: boolean, res?: http.IncomingMessage, redirectedHost?: string, pollingIntervalHint?: number) => void,
+        ): void {
+        
+        let pingHeaders: { name: string, value: string }[] = [
+            { name: QuickPulseConfig.streamId, value: envelope.StreamId },
+            { name: QuickPulseConfig.machineName, value: envelope.MachineName },
+            { name: QuickPulseConfig.roleName, value: envelope.RoleName },
+            { name: QuickPulseConfig.instanceName, value: envelope.Instance },
+            { name: QuickPulseConfig.invariantVersion, value: envelope.InvariantVersion.toString() }
+        ];
+        this._submitData(envelope, redirectedHostEndpoint, done, "ping", pingHeaders);
     }
 
-    public post(envelope: Contracts.EnvelopeQuickPulse, done: (shouldPOST?: boolean, res?: http.IncomingMessage) => void): void {
+    public post(envelope: Contracts.EnvelopeQuickPulse, 
+            redirectedHostEndpoint: string,
+            done: (shouldPOST?: boolean, res?: http.IncomingMessage, redirectedHost?: string, pollingIntervalHint?: number) => void,
+        ): void {
 
         // Important: When POSTing data, envelope must be an array
-        this._submitData([envelope], done, "post");
+        this._submitData([envelope], redirectedHostEndpoint, done, "post");
     }
 
-    private _submitData(envelope: Contracts.EnvelopeQuickPulse | Contracts.EnvelopeQuickPulse[], done: (shouldPOST?: boolean, res?: http.IncomingMessage) => void, postOrPing: "post" | "ping"): void {
+    private _submitData(envelope: Contracts.EnvelopeQuickPulse | Contracts.EnvelopeQuickPulse[], 
+            redirectedHostEndpoint: string,
+            done: (shouldPOST?: boolean, res?: http.IncomingMessage, redirectedHost?: string, pollingIntervalHint?: number) => void, 
+            postOrPing: "post" | "ping", 
+            additionalHeaders?: { name: string, value: string }[]
+        ): void {
+
         const payload = JSON.stringify(envelope);
         var options = {
             [AutoCollectHttpDependencies.disableCollectionRequestOption]: true,
-            host: this._config.quickPulseHost,
+            host: (redirectedHostEndpoint && redirectedHostEndpoint.length > 0) ? redirectedHostEndpoint : this._config.quickPulseHost,
             method: QuickPulseConfig.method,
             path: `/QuickPulseService.svc/${postOrPing}?ikey=${this._config.instrumentationKey}`,
             headers:{
@@ -52,6 +80,10 @@ class QuickPulseSender {
             }
         };
 
+        if (additionalHeaders && additionalHeaders.length > 0) {
+            additionalHeaders.forEach(header => options.headers[header.name] = header.value);
+        }
+
         // HTTPS only
         if (this._config.httpsAgent) {
             (<any>options).agent = this._config.httpsAgent;
@@ -61,8 +93,10 @@ class QuickPulseSender {
 
         const req = https.request(options, (res: http.IncomingMessage) => {
             const shouldPOSTData = res.headers[QuickPulseConfig.subscribed] === "true";
+            const redirectHeader = res.headers[QuickPulseConfig.endpointRedirect];
+            const pollingIntervalHint = res.headers[QuickPulseConfig.pollingIntervalHint] as number;
             this._consecutiveErrors = 0;
-            done(shouldPOSTData, res);
+            done(shouldPOSTData, res, redirectHeader, pollingIntervalHint);
         });
         req.on("error", (error: Error) => {
             // Unable to contact qps endpoint.
diff --git a/Library/QuickPulseStateManager.ts b/Library/QuickPulseStateManager.ts
index 12f8a38..db4c259 100644
--- a/Library/QuickPulseStateManager.ts
+++ b/Library/QuickPulseStateManager.ts
@@ -29,6 +29,8 @@ class QuickPulseStateManager {
     private _metrics: {[name: string]: Contracts.MetricQuickPulse} = {};
     private _documents: Contracts.DocumentQuickPulse[] = [];
     private _collectors: {enable: (enable: boolean) => void}[] = [];
+    private _redirectedHost: string = null;
+    private _pollingIntervalHint: number = -1;
 
     constructor(iKey?: string, context?: Context) {
         this.config = new Config(iKey);
@@ -130,7 +132,8 @@ class QuickPulseStateManager {
             this._ping(envelope);
         }
 
-        let currentTimeout = this._isCollectingData ? QuickPulseStateManager.POST_INTERVAL : QuickPulseStateManager.PING_INTERVAL;
+        let pingInterval = this._pollingIntervalHint > 0 ? this._pollingIntervalHint : QuickPulseStateManager.PING_INTERVAL;
+        let currentTimeout = this._isCollectingData ? QuickPulseStateManager.POST_INTERVAL : pingInterval;
         if (this._isCollectingData && Date.now() - this._lastSuccessTime >= QuickPulseStateManager.MAX_POST_WAIT_TIME && !this._lastSendSucceeded) {
             // Haven't posted successfully in 20 seconds, so wait 60 seconds and ping
             this._isCollectingData = false;
@@ -145,17 +148,18 @@ class QuickPulseStateManager {
     }
 
     private _ping(envelope: Contracts.EnvelopeQuickPulse): void {
-        this._sender.ping(envelope, this._quickPulseDone.bind(this));
+        this._sender.ping(envelope, this._redirectedHost, this._quickPulseDone.bind(this));
     }
 
     private _post(envelope: Contracts.EnvelopeQuickPulse): void {
-        this._sender.post(envelope, this._quickPulseDone.bind(this));
+        this._sender.post(envelope, this._redirectedHost, this._quickPulseDone.bind(this));
     }
 
     /**
      * Change the current QPS send state. (shouldPOST == undefined) --> error, but do not change the state yet.
      */
-    private _quickPulseDone(shouldPOST?: boolean, res?: http.IncomingMessage): void {
+    private _quickPulseDone(shouldPOST?: boolean, res?: http.IncomingMessage, 
+            redirectedHost?: string, pollingIntervalHint?: number): void {
         if (shouldPOST != undefined) {
             if (this._isCollectingData !== shouldPOST) {
                 Logging.info("Live Metrics sending data", shouldPOST);
@@ -163,6 +167,15 @@ class QuickPulseStateManager {
             }
             this._isCollectingData = shouldPOST;
 
+            if (redirectedHost && redirectedHost.length > 0) {
+                this._redirectedHost = redirectedHost;
+                Logging.info("Redirecting endpoint to: ", redirectedHost);
+            }
+
+            if (pollingIntervalHint && pollingIntervalHint > 0) {
+                this._pollingIntervalHint = pollingIntervalHint;
+            }
+
             if (res && res.statusCode < 300 && res.statusCode >= 200) {
                 this._lastSuccessTime = Date.now();
                 this._lastSendSucceeded = true;


Test patch:
diff --git a/Tests/Library/Config.tests.ts b/Tests/Library/Config.tests.ts
index c914bce..9e9c15e 100644
--- a/Tests/Library/Config.tests.ts
+++ b/Tests/Library/Config.tests.ts
@@ -120,7 +120,7 @@ describe("Library/Config", () => {
                 assert(config.proxyHttpUrl === undefined);
                 assert(config.proxyHttpsUrl === undefined);
 
-                assert(config.quickPulseHost === Constants.DEFAULT_LIVEMETRICS_HOST);
+                assert.equal(config.quickPulseHost, Constants.DEFAULT_LIVEMETRICS_HOST);
             });
 
             it("should initialize values that we claim in README (2)", () => {
diff --git a/Tests/Library/QuickPulseStateManager.tests.ts b/Tests/Library/QuickPulseStateManager.tests.ts
index d784fba..64e3b88 100644
--- a/Tests/Library/QuickPulseStateManager.tests.ts
+++ b/Tests/Library/QuickPulseStateManager.tests.ts
@@ -2,7 +2,7 @@ import assert = require("assert");
 import sinon = require("sinon");
 
 import QuickPulseClient = require("../../Library/QuickPulseStateManager");
-import { AssertionError } from "assert";
+import { IncomingMessage } from "http";
 
 describe("Library/QuickPulseStateManager", () => {
     describe("#constructor", () => {
@@ -114,4 +114,136 @@ describe("Library/QuickPulseStateManager", () => {
             qps.enable(false);
         });
     });
+
+    describe("#_goQuickPulsePingPollingHint", () => {
+        let qps: QuickPulseClient;
+        let postStub: sinon.SinonStub;
+        let pingStub: sinon.SinonStub;
+        let clock: sinon.SinonFakeTimers;
+
+        beforeEach(() => {
+            clock = sinon.useFakeTimers();
+            qps = new QuickPulseClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333");
+            postStub = sinon.stub(qps, "_post");
+            pingStub = sinon.stub(qps, "_ping");
+        })
+        afterEach(() => {
+            qps = null;
+            postStub.restore();
+            pingStub.restore();
+            clock.restore();
+        });
+
+        it("should call _ping at once every 5000ms when no pollingIntervalHint is set", () => {
+            qps.enable(true);
+
+            clock.tick(10000);
+            assert.equal(pingStub.callCount, 3);
+            assert.ok(postStub.notCalled);
+            qps.enable(false);
+        });
+
+
+        it("should call _ping at a rate according to interval hint", () => {
+            qps["_pollingIntervalHint"] = 1000;
+            qps.enable(true);
+
+            clock.tick(10000);
+            assert.equal(pingStub.callCount, 11);
+            assert.ok(postStub.notCalled);
+            qps.enable(false);
+        });
+    });
+
+    describe("#_goQuickPulsePingWithAllHeaders", () => {
+        let qps: QuickPulseClient;
+        let submitDataStub: sinon.SinonStub;
+        let clock: sinon.SinonFakeTimers;
+
+        beforeEach(() => {
+            clock = sinon.useFakeTimers();
+            qps = new QuickPulseClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333");
+            submitDataStub = sinon.stub(qps['_sender'], "_submitData");
+        })
+        afterEach(() => {
+            qps = null;
+            submitDataStub.restore();
+            clock.restore();
+        });
+
+        it("should call _ping with all expected headers set set", () => {
+            qps['context'].tags[qps['context'].keys.cloudRoleInstance] = 'instance1';
+            qps['context'].tags[qps['context'].keys.cloudRole] = 'role1';
+            qps.enable(true);
+
+            let callArgs = submitDataStub.args;
+            assert.equal((callArgs[0][4][0] as any)['name'], 'x-ms-qps-stream-id');
+            assert.ok((callArgs[0][4][0] as any)['value'].length > 0);
+            assert.equal((callArgs[0][4][1] as any)['name'], 'x-ms-qps-machine-name');
+            assert.ok((callArgs[0][4][1] as any)['value'].length > 0);
+            assert.equal((callArgs[0][4][2] as any)['name'], 'x-ms-qps-role-name');
+            assert.equal((callArgs[0][4][2] as any)['value'], 'role1');
+            assert.equal((callArgs[0][4][3] as any)['name'], 'x-ms-qps-instance-name');
+            assert.equal((callArgs[0][4][3] as any)['value'], 'instance1');
+            assert.equal((callArgs[0][4][4] as any)['name'], 'x-ms-qps-invariant-version');
+            assert.equal((callArgs[0][4][4] as any)['value'], '1');
+
+            assert.equal(submitDataStub.callCount, 1);
+            
+            qps.enable(false);
+        });
+
+        it("should call _ping with all expected headers set", () => {
+            qps.enable(true);
+            qps['_redirectedHost'] = 'www.example.com';
+
+            let callArgs = submitDataStub.args;
+
+            clock.tick(10000);
+
+            qps.enable(false);
+
+            assert.equal(submitDataStub.callCount, 3);
+            assert.equal(callArgs[0][1], undefined);
+            assert.equal(callArgs[1][1], 'www.example.com');
+            assert.equal(callArgs[2][1], 'www.example.com');
+            
+        });
+    });
+
+    describe("#_goQuickPulse", () => {
+        let qps: QuickPulseClient;
+
+        beforeEach(() => {
+            qps = new QuickPulseClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333");
+
+        })
+        afterEach(() => {
+            qps = null;
+        });
+
+        it("should call _quickPulseDone and set the _rediectedHost and pollingIntervalHint", () => {
+            
+            qps['_quickPulseDone'](true, { statusCode: 200} as IncomingMessage, 'www.example.com', 2000);
+
+            assert.equal(qps['_redirectedHost'], 'www.example.com');
+            assert.equal(qps['_pollingIntervalHint'], 2000);
+            assert.equal(qps['_isCollectingData'], true);
+            assert.equal(qps['_lastSendSucceeded'], true);
+        });
+
+        it("should call _quickPulseDone and not set the _rediectedHost and pollingIntervalHint if the arguments are null", () => {
+            qps['_pollingIntervalHint'] = 2000;
+            qps['_redirectedHost'] = 'www.example.com';
+            qps['_quickPulseDone'](true, { statusCode: 200} as IncomingMessage, null, 0);
+
+            assert.equal(qps['_redirectedHost'], 'www.example.com');
+            assert.equal(qps['_pollingIntervalHint'], 2000);
+            
+            qps['_quickPulseDone'](true, { statusCode: 200} as IncomingMessage, 'www.quickpulse.com', 5000);
+
+            assert.equal(qps['_redirectedHost'], 'www.quickpulse.com');
+            assert.equal(qps['_pollingIntervalHint'], 5000);
+        });
+    });
 });