feat(telemetry): account for Exceptions in usage billing and add avg exception row size

- Update TelemetryUsageBilling description to include Exceptions.
- Add AverageExceptionRowSizeInBytes env/config (env example, docker-compose, Helm values & schema).
- Use ExceptionInstanceService in TelemetryUsageBillingService to include exception row counts when estimating bytes for Traces.
- Add helper to read average exception row size and adjust billing calculations.
This commit is contained in:
Nawaz Dhandala
2025-10-27 16:26:46 +00:00
parent 90fcfd1c7e
commit 34737fbba4
8 changed files with 84 additions and 9 deletions

View File

@@ -39,7 +39,7 @@ export const DEFAULT_RETENTION_IN_DAYS: number = 15;
pluralName: "Telemetry Usage Billings",
icon: IconProp.Billing,
tableDescription:
"Stores historical usage billing data for your telemetry data like Logs, Metrics, and Traces.",
"Stores historical usage billing data for your telemetry data like Logs, Metrics, Traces, and Exceptions.",
})
@Entity({
name: "TelemetryUsageBilling",

View File

@@ -380,6 +380,9 @@ export const AverageMetricRowSizeInBytes: number = parsePositiveNumberFromEnv(
1024,
);
export const AverageExceptionRowSizeInBytes: number =
parsePositiveNumberFromEnv("AVERAGE_EXCEPTION_ROW_SIZE_IN_BYTES", 1024);
export const SlackAppClientId: string | null =
process.env["SLACK_APP_CLIENT_ID"] || null;
export const SlackAppClientSecret: string | null =

View File

@@ -15,6 +15,7 @@ import TelemetryServiceService from "./TelemetryServiceService";
import SpanService from "./SpanService";
import LogService from "./LogService";
import MetricService from "./MetricService";
import ExceptionInstanceService from "./ExceptionInstanceService";
import AnalyticsQueryHelper from "../Types/AnalyticsDatabase/QueryHelper";
import DiskSize from "../../Types/DiskSize";
import logger from "../Utils/Logger";
@@ -24,6 +25,7 @@ import {
AverageSpanRowSizeInBytes,
AverageLogRowSizeInBytes,
AverageMetricRowSizeInBytes,
AverageExceptionRowSizeInBytes,
IsBillingEnabled,
} from "../EnvironmentConfig";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
@@ -76,8 +78,21 @@ export class Service extends DatabaseService<Model> {
const averageRowSizeInBytes: number = this.getAverageRowSizeForProduct(
data.productType,
);
const averageExceptionRowSizeInBytes: number =
this.getAverageExceptionRowSize();
if (averageRowSizeInBytes <= 0) {
if (
data.productType !== ProductType.Traces &&
averageRowSizeInBytes <= 0
) {
return;
}
if (
data.productType === ProductType.Traces &&
averageRowSizeInBytes <= 0 &&
averageExceptionRowSizeInBytes <= 0
) {
return;
}
@@ -129,11 +144,11 @@ export class Service extends DatabaseService<Model> {
continue;
}
let rowCount: number = 0;
let estimatedBytes: number = 0;
try {
if (data.productType === ProductType.Traces) {
const count: PositiveNumber = await SpanService.countBy({
const spanCount: PositiveNumber = await SpanService.countBy({
query: {
projectId: data.projectId,
serviceId: telemetryService.id,
@@ -146,7 +161,30 @@ export class Service extends DatabaseService<Model> {
},
});
rowCount = count.toNumber();
const exceptionCount: PositiveNumber =
await ExceptionInstanceService.countBy({
query: {
projectId: data.projectId,
serviceId: telemetryService.id,
time: AnalyticsQueryHelper.inBetween(startOfDay, endOfDay),
},
skip: 0,
limit: LIMIT_INFINITY,
props: {
isRoot: true,
},
});
const totalSpanCount: number = spanCount.toNumber();
const totalExceptionCount: number = exceptionCount.toNumber();
if (totalSpanCount <= 0 && totalExceptionCount <= 0) {
continue;
}
estimatedBytes =
totalSpanCount * averageRowSizeInBytes +
totalExceptionCount * averageExceptionRowSizeInBytes;
} else if (data.productType === ProductType.Logs) {
const count: PositiveNumber = await LogService.countBy({
query: {
@@ -161,7 +199,13 @@ export class Service extends DatabaseService<Model> {
},
});
rowCount = count.toNumber();
const totalRowCount: number = count.toNumber();
if (totalRowCount <= 0) {
continue;
}
estimatedBytes = totalRowCount * averageRowSizeInBytes;
} else if (data.productType === ProductType.Metrics) {
const count: PositiveNumber = await MetricService.countBy({
query: {
@@ -176,7 +220,13 @@ export class Service extends DatabaseService<Model> {
},
});
rowCount = count.toNumber();
const totalRowCount: number = count.toNumber();
if (totalRowCount <= 0) {
continue;
}
estimatedBytes = totalRowCount * averageRowSizeInBytes;
}
} catch (error) {
logger.error(
@@ -186,11 +236,10 @@ export class Service extends DatabaseService<Model> {
continue;
}
if (rowCount <= 0) {
if (estimatedBytes <= 0) {
continue;
}
const estimatedBytes: number = rowCount * averageRowSizeInBytes;
const estimatedGigabytes: number = DiskSize.byteSizeToGB(estimatedBytes);
if (!Number.isFinite(estimatedGigabytes) || estimatedGigabytes <= 0) {
@@ -344,6 +393,20 @@ export class Service extends DatabaseService<Model> {
return value;
}
private getAverageExceptionRowSize(): number {
const fallbackSize: number = 1024;
if (!Number.isFinite(AverageExceptionRowSizeInBytes)) {
return fallbackSize;
}
if (AverageExceptionRowSizeInBytes <= 0) {
return fallbackSize;
}
return AverageExceptionRowSizeInBytes;
}
}
export default new Service();

View File

@@ -493,6 +493,8 @@ Usage:
- name: AVERAGE_METRIC_ROW_SIZE_IN_BYTES
value: {{ $.Values.billing.telemetry.averageMetricRowSizeInBytes | quote }}
- name: AVERAGE_EXCEPTION_ROW_SIZE_IN_BYTES
value: {{ $.Values.billing.telemetry.averageExceptionRowSizeInBytes | quote }}
{{- end }}

View File

@@ -611,6 +611,10 @@
"averageMetricRowSizeInBytes": {
"type": "integer",
"minimum": 1
},
"averageExceptionRowSizeInBytes": {
"type": "integer",
"minimum": 1
}
},
"additionalProperties": false

View File

@@ -234,6 +234,7 @@ billing:
averageSpanRowSizeInBytes: 1024
averageLogRowSizeInBytes: 1024
averageMetricRowSizeInBytes: 1024
averageExceptionRowSizeInBytes: 1024
subscriptionPlan:
basic:

View File

@@ -208,6 +208,7 @@ BILLING_PRIVATE_KEY=
AVERAGE_SPAN_ROW_SIZE_IN_BYTES=1024
AVERAGE_LOG_ROW_SIZE_IN_BYTES=1024
AVERAGE_METRIC_ROW_SIZE_IN_BYTES=1024
AVERAGE_EXCEPTION_ROW_SIZE_IN_BYTES=1024
# Use this when you want to disable incident creation.
DISABLE_AUTOMATIC_INCIDENT_CREATION=false

View File

@@ -122,6 +122,7 @@ x-common-server-variables: &common-server-variables
AVERAGE_SPAN_ROW_SIZE_IN_BYTES: ${AVERAGE_SPAN_ROW_SIZE_IN_BYTES}
AVERAGE_LOG_ROW_SIZE_IN_BYTES: ${AVERAGE_LOG_ROW_SIZE_IN_BYTES}
AVERAGE_METRIC_ROW_SIZE_IN_BYTES: ${AVERAGE_METRIC_ROW_SIZE_IN_BYTES}
AVERAGE_EXCEPTION_ROW_SIZE_IN_BYTES: ${AVERAGE_EXCEPTION_ROW_SIZE_IN_BYTES}
WORKFLOW_SCRIPT_TIMEOUT_IN_MS: ${WORKFLOW_SCRIPT_TIMEOUT_IN_MS}
WORKFLOW_TIMEOUT_IN_MS: ${WORKFLOW_TIMEOUT_IN_MS}