Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions sdk/monitor/monitor-opentelemetry-exporter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
### Other Changes

- Detect AKS when the `KUBERNETES_SERVICE_HOST` environment variable is defined.
- Statsbeat exports now report success on failed sends to prevent PeriodicExportingMetricReader errors from surfacing to customers.

## 1.0.0-beta.36 (2025-11-10)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ export const ENV_APPLICATIONINSIGHTS_SDKSTATS_ENABLED_PREVIEW =
export const ENV_APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL =
"APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL";

/**
* Enable verbose statsbeat logging and surfacing failures.
* @internal
*/
export const ENV_SDK_STATS_LOGGING = "SDK_STATS_LOGGING";
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

The environment variable name 'SDK_STATS_LOGGING' doesn't follow the established convention of prefixing with 'APPLICATIONINSIGHTS_' (e.g., 'APPLICATIONINSIGHTS_SDKSTATS_ENABLED_PREVIEW'). Consider renaming to 'APPLICATIONINSIGHTS_SDK_STATS_LOGGING' or 'APPLICATIONINSIGHTS_STATSBEAT_LOGGING' for consistency with other environment variables in this codebase, particularly those related to statsbeat features.

Copilot uses AI. Check for mistakes.

/**
* QuickPulse metric counter names.
* @internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { TelemetryItem as Envelope } from "../../generated/index.js";
import {
ENV_APPLICATIONINSIGHTS_SDKSTATS_ENABLED_PREVIEW,
ENV_APPLICATIONINSIGHTS_SDKSTATS_EXPORT_INTERVAL,
ENV_SDK_STATS_LOGGING,
RetriableRestErrorTypes,
} from "../../Declarations/Constants.js";
import { CustomerSDKStatsMetrics } from "../../export/statsbeat/customerSDKStats.js";
Expand Down Expand Up @@ -207,9 +208,9 @@ export abstract class BaseSender {
statusCode,
);
}
return {
return this.buildExportResult({
code: ExportResultCode.FAILED,
};
});
} else {
// calls resultCallback(ExportResult) based on result of persister.push
if (!this.isStatsbeatSender) {
Expand All @@ -230,9 +231,9 @@ export abstract class BaseSender {
this.incrementStatsbeatFailure();
this.customerSDKStatsMetrics?.countDroppedItems(envelopes, DropCode.CLIENT_EXCEPTION);
}
return {
return this.buildExportResult({
code: ExportResultCode.FAILED,
};
});
}
} catch (error: any) {
const restError = error as RestError;
Expand Down Expand Up @@ -265,7 +266,7 @@ export abstract class BaseSender {
ExceptionType.CLIENT_EXCEPTION,
);
}
return { code: ExportResultCode.FAILED, error: redirectError };
return this.buildExportResult({ code: ExportResultCode.FAILED, error: redirectError });
}
} else if (
restError.statusCode &&
Expand Down Expand Up @@ -325,7 +326,7 @@ export abstract class BaseSender {
restError.message,
);
}
return { code: ExportResultCode.FAILED, error: restError };
return this.buildExportResult({ code: ExportResultCode.FAILED, error: restError });
}
}

Expand All @@ -337,10 +338,10 @@ export abstract class BaseSender {
const success = await this.persister.push(envelopes);
return success
? { code: ExportResultCode.SUCCESS }
: {
: this.buildExportResult({
code: ExportResultCode.FAILED,
error: new Error("Failed to persist envelope in disk."),
};
});
} catch (ex: any) {
if (!this.isStatsbeatSender) {
this.networkStatsbeatMetrics?.countWriteFailure();
Expand All @@ -351,7 +352,7 @@ export abstract class BaseSender {
);
}
}
return { code: ExportResultCode.FAILED, error: ex };
return this.buildExportResult({ code: ExportResultCode.FAILED, error: ex });
}
}

Expand Down Expand Up @@ -402,4 +403,13 @@ export abstract class BaseSender {
}
return false;
}

// Silence noisy failures from statsbeat OTel metric readers unless logging is explicitly enabled
private buildExportResult(result: ExportResult): ExportResult {
const shouldSurfaceStatsbeatFailures = !!process.env[ENV_SDK_STATS_LOGGING];
if (this.isStatsbeatSender && result.code === ExportResultCode.FAILED) {
return shouldSurfaceStatsbeatFailures ? result : { code: ExportResultCode.SUCCESS };
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,76 @@ describe("BaseSender", () => {

expect(diag.error).not.toHaveBeenCalled();
});

it("should report success when statsbeat sender encounters non-retriable failure", async () => {
const originalEnv = process.env;
const newEnv = { ...process.env } as NodeJS.ProcessEnv;
delete newEnv.SDK_STATS_LOGGING;
process.env = newEnv;

sender = new TestBaseSender({
endpointUrl: "https://example.com",
instrumentationKey: "test-key",
trackStatsbeat: true,
exporterOptions: {},
isStatsbeatSender: true,
});

sender.sendMock.mockResolvedValue({
statusCode: 400,
result: "",
});

const result = await sender.exportEnvelopes([{ name: "test", time: new Date() }]);

expect(result.code).toBe(ExportResultCode.SUCCESS);

process.env = originalEnv;
});

it("should surface failure when SDK_STATS_LOGGING is enabled for statsbeat sender", async () => {
const originalEnv = process.env;
const newEnv = { ...process.env, SDK_STATS_LOGGING: "true" } as NodeJS.ProcessEnv;
process.env = newEnv;

sender = new TestBaseSender({
endpointUrl: "https://example.com",
instrumentationKey: "test-key",
trackStatsbeat: true,
exporterOptions: {},
isStatsbeatSender: true,
});

sender.sendMock.mockResolvedValue({
statusCode: 400,
result: "",
});

const result = await sender.exportEnvelopes([{ name: "test", time: new Date() }]);

expect(result.code).toBe(ExportResultCode.FAILED);

process.env = originalEnv;
});

it("should keep failure result for customer sender non-retriable failure", async () => {
sender = new TestBaseSender({
endpointUrl: "https://example.com",
instrumentationKey: "test-key",
trackStatsbeat: true,
exporterOptions: {},
isStatsbeatSender: false,
});

sender.sendMock.mockResolvedValue({
statusCode: 400,
result: "",
});

const result = await sender.exportEnvelopes([{ name: "test", time: new Date() }]);

expect(result.code).toBe(ExportResultCode.FAILED);
});
});

describe("Performance Counter Detection", () => {
Expand Down
Loading