Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import type { BatchObservableResult, Meter, ObservableGauge } from "@opentelemet
import { diag } from "@opentelemetry/api";
import type { PeriodicExportingMetricReaderOptions } from "@opentelemetry/sdk-metrics";
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import type { AzureMonitorExporterOptions } from "../../index.js";
import * as ai from "../../utils/constants/applicationinsights.js";
import { StatsbeatMetrics } from "./statsbeatMetrics.js";
import type { AzureMonitorStatsbeatExporter } from "./statsbeatExporter.js";
import type { CustomerSDKStatsProperties, StatsbeatOptions } from "./types.js";
import {
CustomerSDKStats,
Expand All @@ -19,7 +19,6 @@ import {
} from "./types.js";
import { CustomSDKStatsCounter, STATSBEAT_LANGUAGE, TelemetryType } from "./types.js";
import { getAttachType } from "../../utils/metricUtils.js";
import { AzureMonitorStatsbeatExporter } from "./statsbeatExporter.js";
import { BreezePerformanceCounterNames } from "../../types.js";
import type { MetricsData, RemoteDependencyData, RequestData } from "../../generated/index.js";
import type { TelemetryItem as Envelope } from "../../generated/index.js";
Expand Down Expand Up @@ -55,13 +54,12 @@ export class CustomerSDKStatsMetrics extends StatsbeatMetrics {
// Customer SDK Stats properties
private customerProperties: CustomerSDKStatsProperties;

private constructor(options: StatsbeatOptions) {
private constructor(
options: StatsbeatOptions,
exporter: AzureMonitorStatsbeatExporter
) {
super();
const exporterConfig: AzureMonitorExporterOptions = {
connectionString: `InstrumentationKey=${options.instrumentationKey};IngestionEndpoint=${options.endpointUrl}`,
};

this.customerSDKStatsExporter = new AzureMonitorStatsbeatExporter(exporterConfig);
this.customerSDKStatsExporter = exporter;
// Exports Customer SDK Stats every 15 minutes
const customerMetricReaderOptions: PeriodicExportingMetricReaderOptions = {
exporter: this.customerSDKStatsExporter,
Expand Down Expand Up @@ -109,11 +107,17 @@ export class CustomerSDKStatsMetrics extends StatsbeatMetrics {
/**
* Get singleton instance of CustomerSDKStatsMetrics
* @param options - Configuration options for customer SDK Stats metrics
* @returns The singleton instance
* @returns Promise of the singleton instance
*/
public static getInstance(options: StatsbeatOptions): CustomerSDKStatsMetrics {
public static async getInstance(options: StatsbeatOptions): Promise<CustomerSDKStatsMetrics> {
if (!CustomerSDKStatsMetrics._instance) {
CustomerSDKStatsMetrics._instance = new CustomerSDKStatsMetrics(options);
// Use dynamic import to break circular dependency
const { AzureMonitorStatsbeatExporter } = await import("./statsbeatExporter.js");
const customerStatsExporterConfig = {
connectionString: `InstrumentationKey=${options.instrumentationKey};IngestionEndpoint=${options.endpointUrl}`,
};
const exporter = new AzureMonitorStatsbeatExporter(customerStatsExporterConfig);
CustomerSDKStatsMetrics._instance = new CustomerSDKStatsMetrics(options, exporter);
}
return CustomerSDKStatsMetrics._instance;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type { AzureMonitorExporterOptions } from "../../config.js";
import type { TelemetryItem as Envelope } from "../../generated/index.js";
import { resourceMetricsToEnvelope } from "../../utils/metricUtils.js";
import { AzureMonitorBaseExporter } from "../base.js";
import { HttpSender } from "../../platform/index.js";

/**
* Azure Monitor Statsbeat Exporter
Expand All @@ -21,21 +20,34 @@ export class AzureMonitorStatsbeatExporter
* Flag to determine if the Exporter is shutdown.
*/
private _isShutdown = false;
private _sender: HttpSender;
private _sender: any;
private _senderOptions: any;
Comment on lines +23 to +24
Copy link
Preview

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any types reduces type safety. Consider importing the HttpSender type separately from the implementation, or define proper interfaces for these properties to maintain type safety.

Copilot uses AI. Check for mistakes.


/**
* Initializes a new instance of the AzureMonitorStatsbeatExporter class.
* @param options - Exporter configuration
*/
constructor(options: AzureMonitorExporterOptions) {
super(options, true);
this._sender = new HttpSender({
// Store sender options for lazy initialization to avoid circular dependency
this._senderOptions = {
endpointUrl: this.endpointUrl,
instrumentationKey: this.instrumentationKey,
trackStatsbeat: this.trackStatsbeat,
exporterOptions: options,
isStatsbeatSender: true,
});
};
}

/**
* Lazily initialize the sender to avoid circular dependency
*/
private async _getSender(): Promise<any> {
Copy link
Preview

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type should be more specific than any. Consider importing the HttpSender type or defining a proper interface to maintain type safety.

Copilot uses AI. Check for mistakes.

if (!this._sender) {
const { HttpSender } = await import("../../platform/nodejs/httpSender.js");
this._sender = new HttpSender(this._senderOptions);
}
return this._sender;
}

/**
Expand Down Expand Up @@ -79,7 +91,8 @@ export class AzureMonitorStatsbeatExporter

// Supress tracing until OpenTelemetry Metrics SDK support it
context.with(suppressTracing(context.active()), async () => {
resultCallback(await this._sender.exportEnvelopes(filteredEnvelopes));
const sender = await this._getSender();
resultCallback(await sender.exportEnvelopes(filteredEnvelopes));
});
}

Expand All @@ -88,7 +101,9 @@ export class AzureMonitorStatsbeatExporter
*/
public async shutdown(): Promise<void> {
this._isShutdown = true;
return this._sender.shutdown();
if (this._sender) {
return this._sender.shutdown();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
ENV_APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL,
RetriableRestErrorTypes,
} from "../../Declarations/Constants.js";
import { CustomerSDKStatsMetrics } from "../../export/statsbeat/customerSDKStats.js";

const DEFAULT_BATCH_SEND_RETRY_INTERVAL_MS = 60_000;

Expand All @@ -38,7 +37,7 @@ export abstract class BaseSender {
private numConsecutiveRedirects: number;
private retryTimer: NodeJS.Timeout | null;
private networkStatsbeatMetrics: NetworkStatsbeatMetrics | undefined;
private customerSDKStatsMetrics: CustomerSDKStatsMetrics | undefined;
private customerSDKStatsMetrics: any;
Copy link
Preview

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any type reduces type safety. Consider creating a proper interface or using a union type with undefined to maintain type safety while accommodating the async initialization pattern.

Copilot uses AI. Check for mistakes.

private longIntervalStatsbeatMetrics;
private statsbeatFailureCount: number = 0;
private batchSendRetryIntervalMs: number = DEFAULT_BATCH_SEND_RETRY_INTERVAL_MS;
Expand Down Expand Up @@ -79,12 +78,23 @@ export abstract class BaseSender {
);
}
}
this.customerSDKStatsMetrics = CustomerSDKStatsMetrics.getInstance({
instrumentationKey: options.instrumentationKey,
endpointUrl: options.endpointUrl,
disableOfflineStorage: this.disableOfflineStorage,
networkCollectionInterval: exportInterval,
});
// Initialize customer SDK stats metrics asynchronously to avoid circular dependency
// Only initialize if not already set (e.g., by tests)
if (!this.customerSDKStatsMetrics) {
import("../../export/statsbeat/customerSDKStats.js")
.then(({ CustomerSDKStatsMetrics }) => CustomerSDKStatsMetrics.getInstance({
instrumentationKey: options.instrumentationKey,
endpointUrl: options.endpointUrl,
disableOfflineStorage: this.disableOfflineStorage,
networkCollectionInterval: exportInterval,
}))
.then((metrics) => {
this.customerSDKStatsMetrics = metrics;
})
.catch((error) => {
diag.warn("Failed to initialize customer SDK stats metrics:", error);
Comment on lines 84 to 96
Copy link
Preview

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested promise chain could be simplified using async/await for better readability and error handling. Consider wrapping this in an async function to make the code more maintainable.

Copilot uses AI. Check for mistakes.

});
}
}
}
this.persister = new FileSystemPersist(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ vi.mock("../../src/export/statsbeat/customerSDKStats.js", () => {
return {
CustomerSDKStatsMetrics: {
getInstance: vi.fn().mockImplementation(() => {
return mockCustomerSDKStatsMetrics;
return Promise.resolve(mockCustomerSDKStatsMetrics);
}),
shutdown: vi.fn(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ describe("CustomerSDKStatsMetrics", () => {
endpointUrl: "https://test.endpoint.com",
};

beforeEach(() => {
// Use getInstance to get the singleton
customerSDKStatsMetrics = CustomerSDKStatsMetrics.getInstance(mockOptions);
beforeEach(async () => {
// Use getInstance to get the singleton (now async)
customerSDKStatsMetrics = await CustomerSDKStatsMetrics.getInstance(mockOptions);
});

afterEach(async () => {
Expand Down
Loading