Skip to content

Commit 8033d83

Browse files
authored
Add Support for Tracking Multi iKey Usage & Feature Statsbeat Handler (#1438)
* Add support for tracking multi ikey usage. * Add feature statsbeat handler class and support for multi_ikey feature tracking.
1 parent 4565c88 commit 8033d83

File tree

6 files changed

+419
-8
lines changed

6 files changed

+419
-8
lines changed

src/main.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
1313

1414
import { AutoCollectLogs } from "./logs/autoCollectLogs";
1515
import { AutoCollectExceptions } from "./logs/exceptions";
16-
import { AZURE_MONITOR_STATSBEAT_FEATURES, AzureMonitorOpenTelemetryOptions } from "./types";
16+
import { AzureMonitorOpenTelemetryOptions } from "./types";
1717
import { ApplicationInsightsConfig } from "./shared/configuration/config";
1818
import { LogApi } from "./shim/logsApi";
19-
import { StatsbeatFeature, StatsbeatInstrumentation } from "./shim/types";
19+
import { StatsbeatFeature } from "./shim/types";
2020
import { RequestSpanProcessor } from "./traces/requestProcessor";
21+
import { StatsbeatFeaturesManager } from "./shared/util/statsbeatFeaturesManager";
2122

2223
let autoCollectLogs: AutoCollectLogs;
2324
let exceptions: AutoCollectExceptions;
@@ -27,11 +28,10 @@ let exceptions: AutoCollectExceptions;
2728
* @param options Configuration
2829
*/
2930
export function useAzureMonitor(options?: AzureMonitorOpenTelemetryOptions) {
30-
// Must set statsbeat features before they are read by the distro
31-
process.env[AZURE_MONITOR_STATSBEAT_FEATURES] = JSON.stringify({
32-
instrumentation: StatsbeatInstrumentation.NONE,
33-
feature: StatsbeatFeature.SHIM
34-
});
31+
// Initialize statsbeat features with default values and enable SHIM feature
32+
StatsbeatFeaturesManager.getInstance().initialize();
33+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.SHIM);
34+
3535
// Allows for full filtering of dependency/request spans
3636
options.spanProcessors = [new RequestSpanProcessor(options.enableAutoCollectDependencies, options.enableAutoCollectRequests)];
3737
distroUseAzureMonitor(options);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { AZURE_MONITOR_STATSBEAT_FEATURES } from "../../types";
5+
import { StatsbeatFeature, StatsbeatInstrumentation } from "../../shim/types";
6+
7+
/**
8+
* Interface for statsbeat features configuration
9+
*/
10+
interface StatsbeatFeaturesConfig {
11+
instrumentation: number;
12+
feature: number;
13+
}
14+
15+
/**
16+
* Utility class to manage statsbeat features using bitmap flags
17+
*/
18+
export class StatsbeatFeaturesManager {
19+
private static instance: StatsbeatFeaturesManager;
20+
21+
/**
22+
* Get the singleton instance of StatsbeatFeaturesManager
23+
*/
24+
public static getInstance(): StatsbeatFeaturesManager {
25+
if (!StatsbeatFeaturesManager.instance) {
26+
StatsbeatFeaturesManager.instance = new StatsbeatFeaturesManager();
27+
}
28+
return StatsbeatFeaturesManager.instance;
29+
}
30+
31+
/**
32+
* Get the current statsbeat features configuration from environment variable
33+
*/
34+
private getCurrentConfig(): StatsbeatFeaturesConfig {
35+
const envValue = process.env[AZURE_MONITOR_STATSBEAT_FEATURES];
36+
if (envValue) {
37+
try {
38+
return JSON.parse(envValue);
39+
} catch (error) {
40+
// If parsing fails, return default values
41+
return {
42+
instrumentation: StatsbeatInstrumentation.NONE,
43+
feature: StatsbeatFeature.SHIM
44+
};
45+
}
46+
}
47+
return {
48+
instrumentation: StatsbeatInstrumentation.NONE,
49+
feature: StatsbeatFeature.SHIM
50+
};
51+
}
52+
53+
/**
54+
* Set the statsbeat features environment variable with updated configuration
55+
*/
56+
private setConfig(config: StatsbeatFeaturesConfig): void {
57+
process.env[AZURE_MONITOR_STATSBEAT_FEATURES] = JSON.stringify(config);
58+
}
59+
60+
/**
61+
* Enable a specific statsbeat feature by setting the corresponding bit
62+
*/
63+
public enableFeature(feature: StatsbeatFeature): void {
64+
const currentConfig = this.getCurrentConfig();
65+
currentConfig.feature |= feature; // Use bitwise OR to set the bit
66+
this.setConfig(currentConfig);
67+
}
68+
69+
/**
70+
* Disable a specific statsbeat feature by clearing the corresponding bit
71+
*/
72+
public disableFeature(feature: StatsbeatFeature): void {
73+
const currentConfig = this.getCurrentConfig();
74+
currentConfig.feature &= ~feature; // Use bitwise AND with NOT to clear the bit
75+
this.setConfig(currentConfig);
76+
}
77+
78+
/**
79+
* Check if a specific statsbeat feature is enabled
80+
*/
81+
public isFeatureEnabled(feature: StatsbeatFeature): boolean {
82+
const currentConfig = this.getCurrentConfig();
83+
return (currentConfig.feature & feature) !== 0;
84+
}
85+
86+
/**
87+
* Enable a specific statsbeat instrumentation by setting the corresponding bit
88+
*/
89+
public enableInstrumentation(instrumentation: StatsbeatInstrumentation): void {
90+
const currentConfig = this.getCurrentConfig();
91+
currentConfig.instrumentation |= instrumentation; // Use bitwise OR to set the bit
92+
this.setConfig(currentConfig);
93+
}
94+
95+
/**
96+
* Disable a specific statsbeat instrumentation by clearing the corresponding bit
97+
*/
98+
public disableInstrumentation(instrumentation: StatsbeatInstrumentation): void {
99+
const currentConfig = this.getCurrentConfig();
100+
currentConfig.instrumentation &= ~instrumentation; // Use bitwise AND with NOT to clear the bit
101+
this.setConfig(currentConfig);
102+
}
103+
104+
/**
105+
* Check if a specific statsbeat instrumentation is enabled
106+
*/
107+
public isInstrumentationEnabled(instrumentation: StatsbeatInstrumentation): boolean {
108+
const currentConfig = this.getCurrentConfig();
109+
return (currentConfig.instrumentation & instrumentation) !== 0;
110+
}
111+
112+
/**
113+
* Initialize the statsbeat features environment variable with default values if not set
114+
*/
115+
public initialize(): void {
116+
if (!process.env[AZURE_MONITOR_STATSBEAT_FEATURES]) {
117+
this.setConfig({
118+
instrumentation: StatsbeatInstrumentation.NONE,
119+
feature: StatsbeatFeature.SHIM
120+
});
121+
}
122+
}
123+
}

src/shim/telemetryClient.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ import { AttributeLogProcessor } from "../shared/util/attributeLogRecordProcesso
2626
import { LogApi } from "./logsApi";
2727
import { flushAzureMonitor, shutdownAzureMonitor, useAzureMonitor } from "../main";
2828
import { AzureMonitorOpenTelemetryOptions } from "../types";
29-
import { UNSUPPORTED_MSG } from "./types";
29+
import { UNSUPPORTED_MSG, StatsbeatFeature } from "./types";
30+
import { StatsbeatFeaturesManager } from "../shared/util/statsbeatFeaturesManager";
3031

3132
/**
3233
* Application Insights telemetry client provides interface to track telemetry items, register telemetry initializers and
3334
* and manually trigger immediate sending (flushing)
3435
*/
3536
export class TelemetryClient {
37+
private static _instanceCount = 0;
3638
public context: Context;
3739
public commonProperties: { [key: string]: string };
3840
public config: Config;
@@ -48,6 +50,13 @@ export class TelemetryClient {
4850
* @param setupString the Connection String or Instrumentation Key to use (read from environment variable if not specified)
4951
*/
5052
constructor(input?: string) {
53+
TelemetryClient._instanceCount++;
54+
55+
// Set statsbeat feature if this is the second or subsequent TelemetryClient instance
56+
if (TelemetryClient._instanceCount >= 2) {
57+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.MULTI_IKEY);
58+
}
59+
5160
const config = new Config(input, this._configWarnings);
5261
this.config = config;
5362
this.commonProperties = {};

src/shim/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ export enum StatsbeatFeature {
351351
DISTRO = 8,
352352
LIVE_METRICS = 16,
353353
SHIM = 32,
354+
CUSTOMER_STATSBEAT = 64,
355+
MULTI_IKEY = 128,
354356
}
355357

356358
/**
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import * as assert from "assert";
5+
import { StatsbeatFeaturesManager } from "../../../../src/shared/util/statsbeatFeaturesManager";
6+
import { StatsbeatFeature, StatsbeatInstrumentation } from "../../../../src/shim/types";
7+
8+
describe("shared/util/StatsbeatFeaturesManager", () => {
9+
let originalEnv: NodeJS.ProcessEnv;
10+
11+
beforeEach(() => {
12+
// Save original environment
13+
originalEnv = { ...process.env };
14+
// Clear the AZURE_MONITOR_STATSBEAT_FEATURES environment variable before each test
15+
delete process.env["AZURE_MONITOR_STATSBEAT_FEATURES"];
16+
});
17+
18+
afterEach(() => {
19+
// Restore original environment
20+
process.env = originalEnv;
21+
});
22+
23+
describe("initialize", () => {
24+
it("should initialize environment variable with default values when not set", () => {
25+
StatsbeatFeaturesManager.getInstance().initialize();
26+
27+
const envValue = process.env["AZURE_MONITOR_STATSBEAT_FEATURES"];
28+
assert.ok(envValue, "AZURE_MONITOR_STATSBEAT_FEATURES should be set after initialization");
29+
30+
const config = JSON.parse(envValue);
31+
assert.strictEqual(config.instrumentation, StatsbeatInstrumentation.NONE, "instrumentation should default to NONE");
32+
assert.strictEqual(config.feature, StatsbeatFeature.SHIM, "feature should default to SHIM");
33+
});
34+
35+
it("should not overwrite existing environment variable", () => {
36+
const existingValue = JSON.stringify({
37+
instrumentation: StatsbeatInstrumentation.MONGODB,
38+
feature: StatsbeatFeature.LIVE_METRICS
39+
});
40+
process.env["AZURE_MONITOR_STATSBEAT_FEATURES"] = existingValue;
41+
42+
StatsbeatFeaturesManager.getInstance().initialize();
43+
44+
assert.strictEqual(process.env["AZURE_MONITOR_STATSBEAT_FEATURES"], existingValue, "existing value should not be overwritten");
45+
});
46+
});
47+
48+
describe("enableFeature", () => {
49+
it("should enable MULTI_IKEY feature using bitmap", () => {
50+
StatsbeatFeaturesManager.getInstance().initialize();
51+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.MULTI_IKEY);
52+
53+
const envValue = process.env["AZURE_MONITOR_STATSBEAT_FEATURES"];
54+
assert.ok(envValue, "environment variable should be set");
55+
56+
const config = JSON.parse(envValue);
57+
assert.ok((config.feature & StatsbeatFeature.MULTI_IKEY) !== 0, "MULTI_IKEY feature should be enabled");
58+
assert.ok((config.feature & StatsbeatFeature.SHIM) !== 0, "SHIM feature should remain enabled");
59+
});
60+
61+
it("should enable CUSTOMER_STATSBEAT feature using bitmap", () => {
62+
StatsbeatFeaturesManager.getInstance().initialize();
63+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.CUSTOMER_STATSBEAT);
64+
65+
const envValue = process.env["AZURE_MONITOR_STATSBEAT_FEATURES"];
66+
assert.ok(envValue, "environment variable should be set");
67+
68+
const config = JSON.parse(envValue);
69+
assert.ok((config.feature & StatsbeatFeature.CUSTOMER_STATSBEAT) !== 0, "CUSTOMER_STATSBEAT feature should be enabled");
70+
assert.ok((config.feature & StatsbeatFeature.SHIM) !== 0, "SHIM feature should remain enabled");
71+
});
72+
73+
it("should enable multiple features using bitmap", () => {
74+
StatsbeatFeaturesManager.getInstance().initialize();
75+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.MULTI_IKEY);
76+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.LIVE_METRICS);
77+
78+
const envValue = process.env["AZURE_MONITOR_STATSBEAT_FEATURES"];
79+
assert.ok(envValue, "environment variable should be set");
80+
81+
const config = JSON.parse(envValue);
82+
assert.ok((config.feature & StatsbeatFeature.MULTI_IKEY) !== 0, "MULTI_IKEY feature should be enabled");
83+
assert.ok((config.feature & StatsbeatFeature.LIVE_METRICS) !== 0, "LIVE_METRICS feature should be enabled");
84+
assert.ok((config.feature & StatsbeatFeature.SHIM) !== 0, "SHIM feature should remain enabled");
85+
});
86+
});
87+
88+
describe("disableFeature", () => {
89+
it("should disable specific feature using bitmap", () => {
90+
StatsbeatFeaturesManager.getInstance().initialize();
91+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.MULTI_IKEY);
92+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.LIVE_METRICS);
93+
94+
// Disable only MULTI_IKEY
95+
StatsbeatFeaturesManager.getInstance().disableFeature(StatsbeatFeature.MULTI_IKEY);
96+
97+
const envValue = process.env["AZURE_MONITOR_STATSBEAT_FEATURES"];
98+
assert.ok(envValue, "environment variable should be set");
99+
100+
const config = JSON.parse(envValue);
101+
assert.strictEqual((config.feature & StatsbeatFeature.MULTI_IKEY), 0, "MULTI_IKEY feature should be disabled");
102+
assert.ok((config.feature & StatsbeatFeature.LIVE_METRICS) !== 0, "LIVE_METRICS feature should remain enabled");
103+
assert.ok((config.feature & StatsbeatFeature.SHIM) !== 0, "SHIM feature should remain enabled");
104+
});
105+
});
106+
107+
describe("isFeatureEnabled", () => {
108+
it("should correctly detect enabled features", () => {
109+
StatsbeatFeaturesManager.getInstance().initialize();
110+
111+
assert.ok(StatsbeatFeaturesManager.getInstance().isFeatureEnabled(StatsbeatFeature.SHIM), "SHIM should be enabled by default");
112+
assert.ok(!StatsbeatFeaturesManager.getInstance().isFeatureEnabled(StatsbeatFeature.MULTI_IKEY), "MULTI_IKEY should not be enabled by default");
113+
114+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.MULTI_IKEY);
115+
assert.ok(StatsbeatFeaturesManager.getInstance().isFeatureEnabled(StatsbeatFeature.MULTI_IKEY), "MULTI_IKEY should be enabled after enableFeature");
116+
});
117+
});
118+
119+
describe("instrumentation management", () => {
120+
it("should enable and disable instrumentation features", () => {
121+
StatsbeatFeaturesManager.getInstance().initialize();
122+
123+
assert.ok(!StatsbeatFeaturesManager.getInstance().isInstrumentationEnabled(StatsbeatInstrumentation.MONGODB), "MONGODB should not be enabled by default");
124+
125+
StatsbeatFeaturesManager.getInstance().enableInstrumentation(StatsbeatInstrumentation.MONGODB);
126+
assert.ok(StatsbeatFeaturesManager.getInstance().isInstrumentationEnabled(StatsbeatInstrumentation.MONGODB), "MONGODB should be enabled after enableInstrumentation");
127+
128+
StatsbeatFeaturesManager.getInstance().disableInstrumentation(StatsbeatInstrumentation.MONGODB);
129+
assert.ok(!StatsbeatFeaturesManager.getInstance().isInstrumentationEnabled(StatsbeatInstrumentation.MONGODB), "MONGODB should be disabled after disableInstrumentation");
130+
});
131+
});
132+
133+
describe("error handling", () => {
134+
it("should handle malformed JSON in environment variable", () => {
135+
process.env["AZURE_MONITOR_STATSBEAT_FEATURES"] = "invalid json";
136+
137+
// Should not throw and should return default values
138+
assert.ok(!StatsbeatFeaturesManager.getInstance().isFeatureEnabled(StatsbeatFeature.MULTI_IKEY), "should handle malformed JSON gracefully");
139+
140+
// Should be able to enable features despite malformed initial value
141+
StatsbeatFeaturesManager.getInstance().enableFeature(StatsbeatFeature.MULTI_IKEY);
142+
assert.ok(StatsbeatFeaturesManager.getInstance().isFeatureEnabled(StatsbeatFeature.MULTI_IKEY), "should be able to enable features after handling malformed JSON");
143+
});
144+
});
145+
});

0 commit comments

Comments
 (0)