Repository: microsoft/ApplicationInsights-node.js
Pull request #1103 (2023-03-08 23:06:11)

[Beta] Updating Heartbeat


Patch:
diff --git a/src/metrics/collection/exceptionMetrics.ts b/src/metrics/collection/exceptionMetrics.ts
index d36240d..fef4f26 100644
--- a/src/metrics/collection/exceptionMetrics.ts
+++ b/src/metrics/collection/exceptionMetrics.ts
@@ -28,12 +28,24 @@ export class ExceptionMetrics {
         this._exceptionsRateGauge = this._meter.createObservableGauge(MetricName.EXCEPTION_RATE, {
             valueType: ValueType.DOUBLE,
         });
-        this._meter.addBatchObservableCallback(this._getExceptionCount.bind(this), [
-            this._exceptionsCountGauge,
-        ]);
-        this._meter.addBatchObservableCallback(this._getExceptionRate.bind(this), [
-            this._exceptionsRateGauge,
-        ]);
+    }
+
+    public enable(isEnabled: boolean) {
+        if (isEnabled) {
+            this._meter.addBatchObservableCallback(this._getExceptionCount.bind(this), [
+                this._exceptionsCountGauge,
+            ]);
+            this._meter.addBatchObservableCallback(this._getExceptionRate.bind(this), [
+                this._exceptionsRateGauge,
+            ]);
+        } else {
+            this._meter.removeBatchObservableCallback(this._getExceptionCount.bind(this), [
+                this._exceptionsCountGauge,
+            ]);
+            this._meter.removeBatchObservableCallback(this._getExceptionRate.bind(this), [
+                this._exceptionsRateGauge,
+            ]);
+        }
     }
 
     public countException(dimensions: IStandardMetricBaseDimensions) {
diff --git a/src/metrics/collection/traceMetrics.ts b/src/metrics/collection/traceMetrics.ts
index 2a54baa..9bf25a8 100644
--- a/src/metrics/collection/traceMetrics.ts
+++ b/src/metrics/collection/traceMetrics.ts
@@ -28,12 +28,25 @@ export class TraceMetrics {
         this._tracesRateGauge = this._meter.createObservableGauge(MetricName.TRACE_RATE, {
             valueType: ValueType.DOUBLE,
         });
-        this._meter.addBatchObservableCallback(this._getTraceCount.bind(this), [
-            this._tracesCountGauge,
-        ]);
-        this._meter.addBatchObservableCallback(this._getTraceRate.bind(this), [
-            this._tracesRateGauge,
-        ]);
+       
+    }
+
+    public enable(isEnabled: boolean) {
+        if (isEnabled) {
+            this._meter.addBatchObservableCallback(this._getTraceCount.bind(this), [
+                this._tracesCountGauge,
+            ]);
+            this._meter.addBatchObservableCallback(this._getTraceRate.bind(this), [
+                this._tracesRateGauge,
+            ]);
+        } else {
+            this._meter.removeBatchObservableCallback(this._getTraceCount.bind(this), [
+                this._tracesCountGauge,
+            ]);
+            this._meter.removeBatchObservableCallback(this._getTraceRate.bind(this), [
+                this._tracesRateGauge,
+            ]);
+        }
     }
 
     public countTrace(dimensions: IMetricTraceDimensions) {
diff --git a/src/metrics/handlers/heartBeatHandler.ts b/src/metrics/handlers/heartBeatHandler.ts
index 53a0fee..9c26496 100644
--- a/src/metrics/handlers/heartBeatHandler.ts
+++ b/src/metrics/handlers/heartBeatHandler.ts
@@ -1,3 +1,4 @@
+import * as crypto from "crypto";
 import * as os from "os";
 import {
     AzureMonitorExporterOptions,
@@ -15,11 +16,9 @@ import {
     PeriodicExportingMetricReaderOptions,
 } from "@opentelemetry/sdk-metrics";
 import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
-import { ApplicationInsightsConfig, AzureVirtualMachine } from "../../shared";
-import { IVirtualMachineInfo } from "../../shared/azureVirtualMachine";
-import { Logger } from "../../shared/logging";
+import { ApplicationInsightsConfig } from "../../shared";
 
-const HeartBeatMetricName = "HeartBeat";
+const HeartBeatMetricName = "HeartbeatState";
 
 export class HeartBeatHandler {
     private _collectionInterval = 900000;
@@ -30,13 +29,11 @@ export class HeartBeatHandler {
     private _meter: Meter;
     private _metricGauge: ObservableGauge;
     private _metricGaugeCallback: ObservableCallback;
-    private _isVM: boolean;
-    private _azureVm: AzureVirtualMachine;
     private _machineProperties: { [key: string]: string };
+    private _uniqueProcessId: string;
 
     constructor(config: ApplicationInsightsConfig, options?: { collectionInterval: number }) {
         this._config = config;
-        this._azureVm = new AzureVirtualMachine();
         this._meterProvider = new MeterProvider();
         const exporterConfig: AzureMonitorExporterOptions = {
             connectionString: config.connectionString,
@@ -56,59 +53,66 @@ export class HeartBeatHandler {
         this._metricGaugeCallback = this._trackHeartBeat.bind(this);
     }
 
-    public async start() {
-        this._machineProperties = await this._getMachineProperties();
-        this._metricGauge.addCallback(this._metricGaugeCallback);
+    public enable(isEnabled: boolean) {
+        if (isEnabled) {
+            this._metricGauge.addCallback(this._metricGaugeCallback);
+        } else {
+            this._metricGauge.removeCallback(this._metricGaugeCallback);
+        }
+    }
+
+    public start() {
+        this.enable(true);
     }
 
     public async shutdown(): Promise<void> {
         await this._meterProvider.shutdown();
     }
 
-    private _trackHeartBeat(observableResult: ObservableResult) {
+    public async flush(): Promise<void> {
+        await this._meterProvider.forceFlush();
+    }
+
+    private async _trackHeartBeat(observableResult: ObservableResult) {
+        this._machineProperties = this._getMachineProperties();
         observableResult.observe(0, this._machineProperties);
     }
 
-    private async _getMachineProperties(): Promise<{ [key: string]: string }> {
+    private _getMachineProperties(): { [key: string]: string } {
         const properties: { [key: string]: string } = {};
-        // TODO: Add sdk property for attach scenarios, confirm if this is only expected when attach happens, older code doing this was present in Default.ts
         const sdkVersion =
             String(
                 this._config.resource.attributes[
                 SemanticResourceAttributes.TELEMETRY_SDK_VERSION
                 ]
             ) || null;
-        properties["sdk"] = sdkVersion;
+        properties["sdkVersion"] = sdkVersion;
         properties["osType"] = os.type();
+        properties["osVersion"] = os.release();
+        //  Random GUID that would help in analysis when app has stopped and restarted.
+        if (!this._uniqueProcessId) {
+            this._uniqueProcessId = crypto.randomBytes(16).toString("hex");
+        }
+        properties["processSessionId"] = this._uniqueProcessId;
+
         if (process.env.WEBSITE_SITE_NAME) {
-            // Web apps
-            properties["appSrv_SiteName"] = process.env.WEBSITE_SITE_NAME || "";
-            properties["appSrv_wsStamp"] = process.env.WEBSITE_HOME_STAMPNAME || "";
-            properties["appSrv_wsHost"] = process.env.WEBSITE_HOSTNAME || "";
-        } else if (process.env.FUNCTIONS_WORKER_RUNTIME) {
-            // Function apps
-            properties["azfunction_appId"] = process.env.WEBSITE_HOSTNAME;
-        } else {
-            if (this._isVM === undefined) {
-                try {
-                    const vmInfo: IVirtualMachineInfo = await this._azureVm.getAzureComputeMetadata(
-                        this._config
-                    );
-                    this._isVM = vmInfo.isVM;
-                    if (this._isVM) {
-                        properties["azInst_vmId"] = vmInfo.id;
-                        properties["azInst_subscriptionId"] = vmInfo.subscriptionId;
-                        properties["azInst_osType"] = vmInfo.osType;
-                    }
-                } catch (error) {
-                    Logger.getInstance().debug(error);
-                }
-            }
+            properties["appSrv_SiteName"] = process.env.WEBSITE_SITE_NAME;
+        }
+        if (process.env.WEBSITE_HOME_STAMPNAME) {
+            properties["appSrv_wsStamp"] = process.env.WEBSITE_HOME_STAMPNAME;
+        }
+        if (process.env.WEBSITE_HOSTNAME) {
+            properties["appSrv_wsHost"] = process.env.WEBSITE_HOSTNAME;
+        }
+        if (process.env.WEBSITE_OWNER_NAME) {
+            properties["appSrv_wsOwner"] = process.env.WEBSITE_OWNER_NAME;
+        }
+        if (process.env.WEBSITE_RESOURCE_GROUP) {
+            properties["appSrv_ResourceGroup"] = process.env.WEBSITE_RESOURCE_GROUP;
+        }
+        if (process.env.WEBSITE_SLOT_NAME) {
+            properties["appSrv_SlotName"] = process.env.WEBSITE_SLOT_NAME;
         }
         return properties;
     }
-
-    public async flush(): Promise<void> {
-        await this._meterProvider.forceFlush();
-    }
 }
diff --git a/src/metrics/handlers/standardMetricsHandler.ts b/src/metrics/handlers/standardMetricsHandler.ts
index ee3d6c6..2c15d86 100644
--- a/src/metrics/handlers/standardMetricsHandler.ts
+++ b/src/metrics/handlers/standardMetricsHandler.ts
@@ -59,6 +59,13 @@ export class StandardMetricsHandler {
         this._traceMetrics = new TraceMetrics(this._meter);
     }
 
+    public start() {
+        this._requestMetrics.enable(true);
+        this._dependencyMetrics.enable(true);
+        this._exceptionMetrics.enable(true);
+        this._traceMetrics.enable(true);
+    }
+
     public async flush(): Promise<void> {
         await this._meterProvider.forceFlush();
     }
diff --git a/src/metrics/metricHandler.ts b/src/metrics/metricHandler.ts
index 3ae8a67..ce41cc5 100644
--- a/src/metrics/metricHandler.ts
+++ b/src/metrics/metricHandler.ts
@@ -35,6 +35,7 @@ export class MetricHandler {
 
     public start() {
         this._perfCounterMetricsHandler?.start();
+        this._standardMetricsHandler?.start();
         this._heartbeatHandler?.start();
     }
 


Test patch:
diff --git a/test/unitTests/metrics/heartbeat.tests.ts b/test/unitTests/metrics/heartbeat.tests.ts
index 754e548..cffdb29 100644
--- a/test/unitTests/metrics/heartbeat.tests.ts
+++ b/test/unitTests/metrics/heartbeat.tests.ts
@@ -18,12 +18,6 @@ describe("AutoCollection/HeartBeat", () => {
         config.connectionString = "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333;";
         heartbeat = new HeartBeatHandler(config, { collectionInterval: 100 });
         sandbox.stub(heartbeat["_metricReader"]["_exporter"], "export");
-        sandbox.stub(heartbeat["_azureVm"], "getAzureComputeMetadata").callsFake(
-            () =>
-                new Promise((resolve, reject) => {
-                    resolve({ isVM: true });
-                })
-        );
     });
 
     beforeEach(() => {
@@ -50,10 +44,10 @@ describe("AutoCollection/HeartBeat", () => {
             assert.strictEqual(scopeMetrics.length, 1, "scopeMetrics count");
             const metrics = scopeMetrics[0].metrics;
             assert.strictEqual(metrics.length, 1, "metrics count");
-            assert.equal(metrics[0].descriptor.name, "HeartBeat");
+            assert.equal(metrics[0].descriptor.name, "HeartbeatState");
         });
 
-        it("should not collect when disabled", async () => {
+        it("should not collect when shutdown", async () => {
             const mockExport = sandbox.stub(heartbeat["_azureExporter"], "export");
             heartbeat.start();
             heartbeat.shutdown();
@@ -63,109 +57,104 @@ describe("AutoCollection/HeartBeat", () => {
     });
 
     describe("#_getMachineProperties()", () => {
-        it("should read correct web app values from environment variable", (done) => {
+        it("should read correct web app values from environment variables", () => {
             const env1 = <{ [id: string]: string }>{};
             env1["WEBSITE_SITE_NAME"] = "site_name";
             env1["WEBSITE_HOME_STAMPNAME"] = "stamp_name";
             env1["WEBSITE_HOSTNAME"] = "host_name";
+            env1["WEBSITE_OWNER_NAME"] = "owner_name";
+            env1["WEBSITE_RESOURCE_GROUP"] = "resource_group";
+            env1["WEBSITE_SLOT_NAME"] = "slot_name";
             process.env = env1;
 
-            heartbeat["_getMachineProperties"]()
-                .then((properties) => {
-                    const keys = Object.keys(properties);
-                    assert.equal(
-                        keys.length,
-                        5,
-                        "should have 5 kv pairs added when resource type is appSrv"
-                    );
-                    assert.equal(keys[0], "sdk", "sdk should be added as a key");
-                    assert.equal(keys[1], "osType", "osType should be added as a key");
-                    assert.equal(
-                        keys[2],
-                        "appSrv_SiteName",
-                        "appSrv_SiteName should be added as a key"
-                    );
-                    assert.equal(
-                        keys[3],
-                        "appSrv_wsStamp",
-                        "appSrv_wsStamp should be added as a key"
-                    );
-                    assert.equal(
-                        keys[4],
-                        "appSrv_wsHost",
-                        "appSrv_wsHost should be added as a key"
-                    );
-                    assert.equal(
-                        properties["sdk"],
-                        config.resource.attributes[
-                            SemanticResourceAttributes.TELEMETRY_SDK_VERSION
-                        ],
-                        "sdk version should be read from Context"
-                    );
-                    assert.equal(
-                        properties["osType"],
-                        os.type(),
-                        "osType should be read from os library"
-                    );
-                    assert.equal(
-                        properties["appSrv_SiteName"],
-                        "site_name",
-                        "appSrv_SiteName should be read from environment variable"
-                    );
-                    assert.equal(
-                        properties["appSrv_wsStamp"],
-                        "stamp_name",
-                        "appSrv_wsStamp should be read from environment variable"
-                    );
-                    assert.equal(
-                        properties["appSrv_wsHost"],
-                        "host_name",
-                        "appSrv_wsHost should be read from environment variable"
-                    );
-                    done();
-                })
-                .catch((error) => done(error));
-        });
-
-        it("should read correct function app values from environment variable", (done) => {
-            const env2 = <{ [id: string]: string }>{};
-            env2["FUNCTIONS_WORKER_RUNTIME"] = "nodejs";
-            env2["WEBSITE_HOSTNAME"] = "host_name";
-            process.env = env2;
-
-            heartbeat["_getMachineProperties"]().then((properties) => {
-                const keys = Object.keys(properties);
-                assert.equal(
-                    keys.length,
-                    3,
-                    "should have 3 kv pairs added when resource type is function app"
-                );
-                assert.equal(keys[0], "sdk", "sdk should be added as a key");
-                assert.equal(keys[1], "osType", "osType should be added as a key");
-                assert.equal(
-                    keys[2],
-                    "azfunction_appId",
-                    "azfunction_appId should be added as a key"
-                );
-                assert.equal(
-                    properties["sdk"],
-                    config.resource.attributes[
-                        SemanticResourceAttributes.TELEMETRY_SDK_VERSION
-                    ],
-                    "sdk version should be read from Context"
-                );
-                assert.equal(
-                    properties["osType"],
-                    os.type(),
-                    "osType should be read from os library"
-                );
-                assert.equal(
-                    properties["azfunction_appId"],
-                    "host_name",
-                    "azfunction_appId should be read from environment variable"
-                );
-                done();
-            });
+            const properties = heartbeat["_getMachineProperties"]();
+            const keys = Object.keys(properties);
+            assert.equal(
+                keys.length,
+                10,
+                "should have 10 kv pairs added when resource type is appSrv"
+            );
+            assert.equal(keys[0], "sdkVersion", "sdk should be added as a key");
+            assert.equal(keys[1], "osType", "osType should be added as a key");
+            assert.equal(keys[2], "osVersion", "osVersion should be added as a key");
+            assert.equal(keys[3], "processSessionId", "processSessionId should be added as a key");
+            assert.equal(
+                keys[4],
+                "appSrv_SiteName",
+                "appSrv_SiteName should be added as a key"
+            );
+            assert.equal(
+                keys[5],
+                "appSrv_wsStamp",
+                "appSrv_wsStamp should be added as a key"
+            );
+            assert.equal(
+                keys[6],
+                "appSrv_wsHost",
+                "appSrv_wsHost should be added as a key"
+            );
+            assert.equal(
+                keys[7],
+                "appSrv_wsOwner",
+                "appSrv_wsOwner should be added as a key"
+            );
+            assert.equal(
+                keys[8],
+                "appSrv_ResourceGroup",
+                "appSrv_ResourceGroup should be added as a key"
+            );
+            assert.equal(
+                keys[9],
+                "appSrv_SlotName",
+                "appSrv_SlotName should be added as a key"
+            );
+            assert.equal(
+                properties["sdkVersion"],
+                config.resource.attributes[
+                SemanticResourceAttributes.TELEMETRY_SDK_VERSION
+                ],
+                "sdk version should be read from Context"
+            );
+            assert.equal(
+                properties["osType"],
+                os.type(),
+                "osType should be read from os library"
+            );
+            assert.equal(
+                properties["osVersion"],
+                os.release(),
+                "osVersion should be read from os library"
+            );
+            assert.equal(
+                properties["appSrv_SiteName"],
+                "site_name",
+                "appSrv_SiteName should be read from environment variable"
+            );
+            assert.equal(
+                properties["appSrv_wsStamp"],
+                "stamp_name",
+                "appSrv_wsStamp should be read from environment variable"
+            );
+            assert.equal(
+                properties["appSrv_wsHost"],
+                "host_name",
+                "appSrv_wsHost should be read from environment variable"
+            );
+            assert.equal(
+                properties["appSrv_wsOwner"],
+                "owner_name",
+                "appSrv_wsOwner should be read from environment variable"
+            );
+            assert.equal(
+                properties["appSrv_ResourceGroup"],
+                "resource_group",
+                "appSrv_ResourceGroup should be read from environment variable"
+            );
+            assert.equal(
+                properties["appSrv_SlotName"],
+                "slot_name",
+                "appSrv_SlotName should be read from environment variable"
+            );
         });
     });
 });
diff --git a/test/unitTests/metrics/metricHandler.tests.ts b/test/unitTests/metrics/metricHandler.tests.ts
index ddae54a..9670440 100644
--- a/test/unitTests/metrics/metricHandler.tests.ts
+++ b/test/unitTests/metrics/metricHandler.tests.ts
@@ -31,8 +31,10 @@ describe("Library/MetricHandler", () => {
             _config.enableAutoCollectStandardMetrics = true;
             const handler = new MetricHandler(_config);
             handler["_perfCounterMetricsHandler"]["_nativeMetrics"]["_metricsAvailable"] = false;
+            const stub = sinon.stub(handler["_standardMetricsHandler"], "start");
             handler.start();
             assert.ok(handler["_standardMetricsHandler"]);
+            assert.ok(stub.calledOnce, "start called");
         });
 
         it("heartbeat metrics enablement during start", () => {
diff --git a/test/unitTests/metrics/standardMetrics.tests.ts b/test/unitTests/metrics/standardMetrics.tests.ts
index a4df7a8..a7ef31b 100644
--- a/test/unitTests/metrics/standardMetrics.tests.ts
+++ b/test/unitTests/metrics/standardMetrics.tests.ts
@@ -17,6 +17,7 @@ describe("#StandardMetricsHandler", () => {
         const config = new ApplicationInsightsConfig();
         config.connectionString = "InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333;";
         autoCollect = new StandardMetricsHandler(config, { collectionInterval: 100 });
+        autoCollect.start();
         sandbox.stub(autoCollect["_metricReader"]["_exporter"], "export");
     });