Skip to content

Commit

Permalink
Add New Features to Statsbeat (#1250)
Browse files Browse the repository at this point in the history
* Add new features to statsbeat.

* Clean up native metrics statsbeat logic and add tests.

* Update NativePerformance.tests.ts

* Update NativePerformance.tests.ts

* Ensure that undefined client/statsbeat will not error out.

* Update statsbeat value for native metrics.

* Update JsonConfig tests as overloaded readFileSync method is not attaching spy correctly.
  • Loading branch information
JacksonWeber authored Jan 2, 2024
1 parent 14ffa2d commit c949fff
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 83 deletions.
9 changes: 9 additions & 0 deletions AutoCollection/NativePerformance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Constants = require("../Declarations/Constants");
import Context = require("../Library/Context");
import Logging = require("../Library/Logging");
import { IBaseConfig } from "../Declarations/Interfaces";
import Statsbeat = require("./Statsbeat");

/**
* Interface which defines which specific extended metrics should be disabled
Expand All @@ -26,6 +27,7 @@ export class AutoCollectNativePerformance {
private _handle: NodeJS.Timer;
private _client: TelemetryClient;
private _disabledMetrics: IDisabledExtendedMetrics = {};
private _statsbeat: Statsbeat | undefined;

constructor(client: TelemetryClient) {
// Note: Only 1 instance of this can exist. So when we reconstruct this object,
Expand All @@ -35,6 +37,7 @@ export class AutoCollectNativePerformance {
}
AutoCollectNativePerformance.INSTANCE = this;
this._client = client;
this._statsbeat = this._client?.getStatsbeat();
}

/**
Expand Down Expand Up @@ -67,13 +70,19 @@ export class AutoCollectNativePerformance {

// Enable the emitter if we were able to construct one
if (this._isEnabled && AutoCollectNativePerformance._emitter) {
if (this._statsbeat) {
this._statsbeat.addFeature(Constants.StatsbeatFeature.NATIVE_METRICS);
}
// enable self
AutoCollectNativePerformance._emitter.enable(true, collectionInterval);
if (!this._handle) {
this._handle = setInterval(() => this._trackNativeMetrics(), collectionInterval);
this._handle.unref();
}
} else if (AutoCollectNativePerformance._emitter) {
if (this._statsbeat) {
this._statsbeat.removeFeature(Constants.StatsbeatFeature.NATIVE_METRICS);
}
// disable self
AutoCollectNativePerformance._emitter.enable(false);
if (this._handle) {
Expand Down
4 changes: 2 additions & 2 deletions AutoCollection/WebSnippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class WebSnippet {
private _isEnabled: boolean;
private _isInitialized: boolean;
private _isIkeyValid: boolean = true;
private _statsbeat: Statsbeat;
private _statsbeat: Statsbeat | undefined;
private _webInstrumentationIkey: string;
private _clientWebInstrumentationConfig: IWebInstrumentationConfig[];
private _clientWebInstrumentationSrc: string;
Expand All @@ -43,7 +43,7 @@ class WebSnippet {
this._clientWebInstrumentationConfig = client.config.webInstrumentationConfig;
this._clientWebInstrumentationSrc = client.config.webInstrumentationSrc;

this._statsbeat = client.getStatsbeat();
this._statsbeat = client?.getStatsbeat();
}

public enable(isEnabled: boolean, webInstrumentationConnectionString?: string ) {
Expand Down
2 changes: 2 additions & 0 deletions Declarations/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ export enum StatsbeatFeature {
DISK_RETRY = 1,
AAD_HANDLING = 2,
WEB_SNIPPET = 4,
LIVE_METRICS = 16,
NATIVE_METRICS = 8192
}

export enum StatsbeatInstrumentation {
Expand Down
9 changes: 5 additions & 4 deletions Library/JsonConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const ENV_webSnippet_connectionString = "APPLICATIONINSIGHTS_WEB_SNIPPET_CONNECT

export class JsonConfig implements IJsonConfig {
private static _instance: JsonConfig;
private _tempDir: string;

public connectionString: string;
public instrumentationKey: string;
Expand Down Expand Up @@ -130,17 +131,17 @@ export class JsonConfig implements IJsonConfig {
else {
let configFileName = "applicationinsights.json";
let rootPath = path.join(__dirname, "../../"); // Root of applicationinsights folder (__dirname = ../out/Library)
let tempDir = path.join(rootPath, configFileName); // default
this._tempDir = path.join(rootPath, configFileName); // default
let configFile = process.env[ENV_CONFIGURATION_FILE];
if (configFile) {
if (path.isAbsolute(configFile)) {
tempDir = configFile;
this._tempDir = configFile;
}
else {
tempDir = path.join(rootPath, configFile);// Relative path to applicationinsights folder
this._tempDir = path.join(rootPath, configFile);// Relative path to applicationinsights folder
}
try {
jsonString = fs.readFileSync(tempDir, "utf8");
jsonString = fs.readFileSync(this._tempDir, "utf8");
}
catch (err) {
Logging.warn("Failed to read JSON config file: ", err);
Expand Down
9 changes: 8 additions & 1 deletion Library/QuickPulseStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Context = require("./Context");

import * as http from "http";
import * as Contracts from "../Declarations/Contracts";
import Statsbeat = require("../AutoCollection/Statsbeat");
import TelemetryClient = require("./TelemetryClient");


/** State Container for sending to the QuickPulse Service */
Expand All @@ -33,12 +35,14 @@ class QuickPulseStateManager {
private _collectors: { enable: (enable: boolean) => void }[] = [];
private _redirectedHost: string = null;
private _pollingIntervalHint: number = -1;
private _statsbeat: Statsbeat | undefined;

constructor(config: Config, context?: Context, getAuthorizationHandler?: (config: Config) => AuthorizationHandler) {
constructor(config: Config, context?: Context, getAuthorizationHandler?: (config: Config) => AuthorizationHandler, client?: TelemetryClient) {
this.config = config;
this.context = context || new Context();
this._sender = new QuickPulseSender(this.config, getAuthorizationHandler);
this._isEnabled = false;
this._statsbeat = client?.getStatsbeat();
}

/**
Expand Down Expand Up @@ -78,6 +82,9 @@ class QuickPulseStateManager {
if (isEnabled && !this._isEnabled) {
this._isEnabled = true;
this._goQuickPulse();
if (this._statsbeat) {
this._statsbeat.addFeature(Constants.StatsbeatFeature.LIVE_METRICS);
}
} else if (!isEnabled && this._isEnabled) {
this._isEnabled = false;
clearTimeout(this._handle);
Expand Down
2 changes: 1 addition & 1 deletion Library/TelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Tags } from "../Declarations/Contracts";
class TelemetryClient {
private static TAG = "TelemetryClient";
private _telemetryProcessors: { (envelope: Contracts.EnvelopeTelemetry, contextObjects: { [name: string]: any; }): boolean; }[] = [];
private _statsbeat: Statsbeat;
private _statsbeat: Statsbeat | undefined;

public config: Config;
public context: Context;
Expand Down
7 changes: 7 additions & 0 deletions Tests/AutoCollection/NativePerformance.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AppInsights = require("../../applicationinsights");
import TelemetryClient = require("../../Library/TelemetryClient");
import { AutoCollectNativePerformance } from "../../AutoCollection/NativePerformance";
import { JsonConfig } from "../../Library/JsonConfig";
import * as Constants from "../../Declarations/Constants";

const ENV_nativeMetricsDisablers = "APPLICATION_INSIGHTS_DISABLE_EXTENDED_METRIC";
const ENV_nativeMetricsDisableAll = "APPLICATION_INSIGHTS_DISABLE_ALL_EXTENDED_METRICS";
Expand All @@ -25,15 +26,21 @@ describe("AutoCollection/NativePerformance", () => {
it("init should enable and dispose should stop autocollection interval", () => {
var setIntervalSpy = sandbox.spy(global, "setInterval");
var clearIntervalSpy = sandbox.spy(global, "clearInterval");
const statsAddSpy = sandbox.spy(AutoCollectNativePerformance.INSTANCE["_statsbeat"], "addFeature");
const statsRemoveSpy = sandbox.spy(AutoCollectNativePerformance.INSTANCE["_statsbeat"], "removeFeature");

AppInsights.setup("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333")
.setAutoCollectHeartbeat(false)
.setAutoCollectPerformance(false, true)
.setAutoCollectPreAggregatedMetrics(false)
.start();
if (AutoCollectNativePerformance["_metricsAvailable"]) {
assert.ok(statsAddSpy.calledOnce);
assert.strictEqual(AutoCollectNativePerformance.INSTANCE["_statsbeat"]["_feature"], Constants.StatsbeatFeature.NATIVE_METRICS + Constants.StatsbeatFeature.DISK_RETRY);
assert.equal(setIntervalSpy.callCount, 3, "setInteval should be called three times as part of NativePerformance initialization as well as Statsbeat");
AppInsights.dispose();
assert.ok(statsRemoveSpy.calledOnce);
assert.strictEqual(AutoCollectNativePerformance.INSTANCE["_statsbeat"]["_feature"], Constants.StatsbeatFeature.DISK_RETRY);
assert.equal(clearIntervalSpy.callCount, 1, "clearInterval should be called once as part of NativePerformance shutdown");
} else {
assert.equal(setIntervalSpy.callCount, 2, "setInterval should not be called if NativePerformance package is not available, Statsbeat will be called");
Expand Down
4 changes: 4 additions & 0 deletions Tests/AutoCollection/Statsbeat.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ describe("AutoCollection/Statsbeat", () => {
assert.equal(statsBeat["_feature"], 3);
statsBeat.removeFeature(Constants.StatsbeatFeature.DISK_RETRY);
assert.equal(statsBeat["_feature"], 2);
statsBeat.addFeature(Constants.StatsbeatFeature.LIVE_METRICS);
assert.equal(statsBeat["_feature"], 18);
statsBeat.addFeature(Constants.StatsbeatFeature.NATIVE_METRICS);
assert.equal(statsBeat["_feature"], 8210);
});

it("Multiple network categories and endpoints", (done) => {
Expand Down
49 changes: 39 additions & 10 deletions Tests/Library/QuickPulseStateManager.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { IncomingMessage } from "http";
import Config = require("../../Library/Config");
import QuickPulseSender = require("../../Library/QuickPulseSender");
import Util = require("../../Library/Util");
import { TelemetryClient } from "../../applicationinsights";

describe("Library/QuickPulseStateManager", () => {
Util.tlsRestrictedAgent = new https.Agent();
Expand All @@ -20,7 +21,7 @@ describe("Library/QuickPulseStateManager", () => {
});

it("should create a config with ikey", () => {
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"), undefined, undefined, new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));

assert.ok(qps.config);
assert.equal(qps.config.instrumentationKey, "1aa11111-bbbb-1ccc-8ddd-eeeeffff3333");
Expand All @@ -33,8 +34,16 @@ describe("Library/QuickPulseStateManager", () => {
assert.ok(qps["_collectors"].length === 0);
});

it("should not error if statsbeat is undefined", () => {
const client: TelemetryClient = new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333");
client["_statsbeat"] = undefined;
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"), undefined, undefined, client);
assert.ok(qps);
});

it("should reuse authorization handler if provided", () => {
var config = new Config("InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333;");
const config = new Config("InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333;");
var client = new TelemetryClient("InstrumentationKey=1aa11111-bbbb-1ccc-8ddd-eeeeffff3333;");
var handler = new AuthorizationHandler({
async getToken(scopes: string | string[], options?: any): Promise<any> {
return { token: "testToken", };
Expand All @@ -44,26 +53,46 @@ describe("Library/QuickPulseStateManager", () => {
return handler;
};
qps = new QuickPulseClient(config, null, getAuthorizationHandler);
assert.equal(qps["_sender"]["_getAuthorizationHandler"](config), handler);
assert.equal(qps["_sender"]["_getAuthorizationHandler"](client.config), handler);
});
});

describe("#enable", () => {
let qps: QuickPulseClient;

beforeEach(() => {
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"), undefined, undefined, new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
})
afterEach(() => {
qps = null;
});
it("should call _goQuickPulse() when isEnabled == true", () => {
const sinonSpy = sinon.spy(qps["_statsbeat"], "addFeature");
const qpsStub = sinon.stub(qps, "_goQuickPulse");

assert.ok(qpsStub.notCalled);
qps.enable(true);

assert.ok(sinonSpy.calledOnce);
assert.ok(qpsStub.calledOnce);
assert.equal(qps["_isEnabled"], true);
assert.equal(qps["_statsbeat"]["_feature"], 16);

qpsStub.restore();
});

it("should handle calling enable twice", () => {
const sinonSpy = sinon.spy(qps["_statsbeat"], "addFeature");
const qpsStub = sinon.stub(qps, "_goQuickPulse");

assert.ok(qpsStub.notCalled);
qps.enable(true);
qps.enable(true);

assert.ok(sinonSpy.calledOnce);
assert.ok(qpsStub.calledOnce);
assert.equal(qps["_isEnabled"], true);
assert.equal(qps["_statsbeat"]["_feature"], 16);

qpsStub.restore();
});
Expand All @@ -83,7 +112,7 @@ describe("Library/QuickPulseStateManager", () => {

describe("#reset", () => {
it("should reset metric and document buffers", () => {
let qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
let qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"), undefined, undefined, new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
(<any>qps["_metrics"]) = { foo: "bar" };
(<any>qps["_documents"]) = [{ foo: "bar" }];

Expand All @@ -103,7 +132,7 @@ describe("Library/QuickPulseStateManager", () => {
let pingStub: sinon.SinonStub;

beforeEach(() => {
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"), undefined, undefined, new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
postStub = sinon.stub(qps, "_post");
pingStub = sinon.stub(qps, "_ping");
})
Expand Down Expand Up @@ -144,7 +173,7 @@ describe("Library/QuickPulseStateManager", () => {

beforeEach(() => {
clock = sinon.useFakeTimers();
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"), undefined, undefined, new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
postStub = sinon.stub(qps, "_post");
pingStub = sinon.stub(qps, "_ping");
})
Expand Down Expand Up @@ -183,7 +212,7 @@ describe("Library/QuickPulseStateManager", () => {

beforeEach(() => {
clock = sinon.useFakeTimers();
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"), undefined, undefined, new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
submitDataStub = sinon.stub(qps['_sender'], "_submitData");
})
afterEach(() => {
Expand Down Expand Up @@ -234,7 +263,7 @@ describe("Library/QuickPulseStateManager", () => {
let qps: QuickPulseClient;

beforeEach(() => {
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"), undefined, undefined, new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));

})
afterEach(() => {
Expand Down Expand Up @@ -272,7 +301,7 @@ describe("Library/QuickPulseStateManager", () => {

beforeEach(() => {
sandbox = sinon.sandbox.create();
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
qps = new QuickPulseClient(new Config("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"), undefined, undefined, new TelemetryClient("1aa11111-bbbb-1ccc-8ddd-eeeeffff3333"));
});

afterEach(() => {
Expand Down
9 changes: 2 additions & 7 deletions Tests/Library/jsonConfig.tests.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import assert = require("assert");
import sinon = require("sinon");
import fs = require("fs");
import path = require("path");
import AppInsights = require("../../applicationinsights");
import Logging = require("../../Library/Logging");
import { JsonConfig } from "../../Library/JsonConfig";
import { IConfig } from "../../Declarations/Interfaces";


describe("Json Config", () => {
var sandbox: sinon.SinonSandbox;
Expand All @@ -31,13 +28,11 @@ describe("Json Config", () => {

describe("config path", () => {
it("Default file path", () => {
let fileSpy = sandbox.spy(fs, "readFileSync");
let loggerSpy = sandbox.spy(Logging, "info");
const config = JsonConfig.getInstance();
assert.equal(loggerSpy.callCount, 0);
assert.equal(fileSpy.called, 1);
let defaultPath = path.resolve(process.cwd(), "applicationinsights.json");
assert.equal(fileSpy.args[0][0], defaultPath);
const defaultPath = path.resolve(process.cwd(), "applicationinsights.json");
assert.deepStrictEqual(config["_tempDir"], defaultPath);
assert.equal(config.proxyHttpUrl, undefined);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Config = require("../../Library/Config");
import QuickPulse = require("../../TelemetryProcessors/PerformanceMetricsTelemetryProcessor");
import QuickPulseStateManager = require("../../Library/QuickPulseStateManager");
import AutoCollectPerformance = require("../../AutoCollection/Performance");
import { Contracts } from "../../applicationinsights";
import { Contracts, TelemetryClient } from "../../applicationinsights";

describe("TelemetryProcessors/PerformanceMetricsTelemetryProcessor", () => {
describe("#PerformanceMetricsTelemetryProcessor()", () => {
Expand Down Expand Up @@ -35,7 +35,7 @@ describe("TelemetryProcessors/PerformanceMetricsTelemetryProcessor", () => {

it("should add document to the provided client", () => {
var qpSpy = sinon.spy(QuickPulse, "performanceMetricsTelemetryProcessor");
var client: QuickPulseStateManager = new QuickPulseStateManager(new Config(ikey));
var client: QuickPulseStateManager = new QuickPulseStateManager(new Config(ikey), undefined, undefined, new TelemetryClient(ikey));
var addDocumentStub = sinon.stub(client, "addDocument");

// Act
Expand All @@ -50,5 +50,19 @@ describe("TelemetryProcessors/PerformanceMetricsTelemetryProcessor", () => {
qpSpy.restore();
addDocumentStub.restore();
});

it("should not error on undefined statsbeat", () => {
const qpSpy = sinon.spy(QuickPulse, "performanceMetricsTelemetryProcessor");
const telemetryClient = new TelemetryClient(ikey);
telemetryClient["_statsbeat"] = undefined;

const client: QuickPulseStateManager = new QuickPulseStateManager(new Config(ikey), undefined, undefined, telemetryClient);

const res = QuickPulse.performanceMetricsTelemetryProcessor(envelope, client);

assert.ok(qpSpy.calledOnce);
assert.equal(res, true);
qpSpy.restore();
})
});
});
Loading

0 comments on commit c949fff

Please sign in to comment.