src/cost/cost.service.ts
Service responsible for managing cost-related operations.
Properties |
|
Methods |
|
constructor(prismaService: PrismaService, temporalProvider: TemporalProvider, costProvider: CostProvider)
|
||||||||||||
|
Defined in src/cost/cost.service.ts:31
|
||||||||||||
|
Constructs a new instance of the CostService class.
Parameters :
|
| Async costEndpoints | ||||||
costEndpoints(costEndpointWhereInput: Prisma.CostEndpointWhereInput)
|
||||||
|
Defined in src/cost/cost.service.ts:534
|
||||||
|
Function to get list of cost endpoints based on where input object with decrypted secrets
Parameters :
Returns :
Promise<CostEndpoint[]>
promise that resolves to list of cost endpoints with decrypted secrets |
| Async deleteCostEndpoint | ||||
deleteCostEndpoint(id)
|
||||
|
Defined in src/cost/cost.service.ts:498
|
||||
|
Function to delete registered cost endpoint
Parameters :
Returns :
Promise<Partial<CostEndpoint>>
deleted cost endpoint |
| Async fetchCostMetricsForSync | |||||||||
fetchCostMetricsForSync(costEndpoint: any, queryOptions: literal type)
|
|||||||||
|
Defined in src/cost/cost.service.ts:139
|
|||||||||
|
Parameters :
Returns :
unknown
|
| formatQueryStringFromFilter | ||||
formatQueryStringFromFilter(filter)
|
||||
|
Defined in src/cost/cost.service.ts:483
|
||||
|
Function to generate query string from object type of filter input
Parameters :
Returns :
string
http query string |
| getConfiguredCosts | ||||
getConfiguredCosts(providerName)
|
||||
|
Defined in src/cost/cost.service.ts:588
|
||||
|
Parameters :
Returns :
any
|
| Async getCost | |||||||||
getCost(projectID, window)
|
|||||||||
|
Defined in src/cost/cost.service.ts:326
|
|||||||||
|
Function to get list of cost endpoint ids for a project then query cost table for data based on window query parameter here we need to send back data grouped and summed up based on different step sizes eg: for window = today , step = hour , startTime = start of hour for window = oneMonth, step = day , startTime = start of day for window = sinMonths, step = month , startTime = start of month for window = yearly, step = year, startTime = start of year we are using $queryRaw here as we dont see any prisma groupBy syntax that supports DATE_TRUNC function fronm postgres this can be updated as we have some support in future we too considered creating database views but dont seed way to add DATE_TRUNC function in schema as well
Parameters :
Returns :
Promise<object>
Promise that resolves to cost data with date |
| Async getCostEndpoint | ||||
getCostEndpoint(projectID)
|
||||
|
Defined in src/cost/cost.service.ts:293
|
||||
|
Function to list cost endpoints based on a projectID
Parameters :
Returns :
Promise<Partial[]>
List of cost endpoint |
| Async getCostEndpointDetails | ||||||
getCostEndpointDetails(costEndpointId: number)
|
||||||
|
Defined in src/cost/cost.service.ts:207
|
||||||
|
Function to get cost endpoint details using cost endpoint id
Parameters :
Returns :
Promise<CostEndpoint>
Promise that resolves to CostEndpoint |
| getCostOverview |
getCostOverview()
|
|
Defined in src/cost/cost.service.ts:559
|
|
Function to list project wise cost incurred till date
Returns :
Promise<CostOverview[]>
promise that result to project wise cost overview |
| getCostProvider | ||||
getCostProvider(provider)
|
||||
|
Defined in src/cost/cost.service.ts:576
|
||||
|
Parameters :
Returns :
any
|
| getCostProviderCategories |
getCostProviderCategories()
|
|
Defined in src/cost/cost.service.ts:580
|
|
Returns :
any
|
| Async getNotificationData | |||||||||||||||||||||
getNotificationData(costEndpointId: number, event: CostEvents, channel: NotificationChannel, templateVars: any, userId: string, extraData: any)
|
|||||||||||||||||||||
|
Defined in src/cost/cost.service.ts:596
|
|||||||||||||||||||||
|
Parameters :
Returns :
Promise<any>
|
| Async getResourcesByTags | ||||||||||||
getResourcesByTags(config, costProvider, tagList: string[])
|
||||||||||||
|
Defined in src/cost/cost.service.ts:278
|
||||||||||||
|
Function to get resources with tag filter
Parameters :
Returns :
unknown
api response from provider get resource by tags function |
| Async getTagKeys | ||||||||
getTagKeys(config, costProvider, getTagsInput)
|
||||||||
|
Defined in src/cost/cost.service.ts:262
|
||||||||
|
Parameters :
Returns :
unknown
|
| Async getTagsData | ||||||||
getTagsData(config, costProvider, getTagsInput)
|
||||||||
|
Defined in src/cost/cost.service.ts:244
|
||||||||
|
Function to get tags data with cost endpoint passed as input with credentials currently implemented just for aws
Parameters :
Returns :
Promise<string>
api response from provider get tags data function |
| Async getTagValuesForKey | ||||||||
getTagValuesForKey(config, costProvider, getTagsInput)
|
||||||||
|
Defined in src/cost/cost.service.ts:253
|
||||||||
|
Parameters :
Returns :
unknown
|
| Async lastSyncTime | ||||
lastSyncTime(costEndpointId)
|
||||
|
Defined in src/cost/cost.service.ts:515
|
||||
|
Function to get last sync timestamp ISO string eg: '2024-04-17T12:30:00.000Z'
Parameters :
Returns :
Promise<string>
ISO string or empty string in case nothing synced yet |
| listModels | ||||
listModels(provider)
|
||||
|
Defined in src/cost/cost.service.ts:584
|
||||
|
Parameters :
Returns :
any
|
| Async pushCostMetrics | ||||||
pushCostMetrics(metrics, costEndpoint)
|
||||||
|
Defined in src/cost/cost.service.ts:63
|
||||||
|
Parameters :
Returns :
unknown
|
| Async registerCostEndpoint | ||||||
registerCostEndpoint(cost: Prisma.CostEndpointCreateInput)
|
||||||
|
Defined in src/cost/cost.service.ts:48
|
||||||
|
Function to register cost endpoint and trigger first sync
Parameters :
Returns :
Promise<Partial<CostEndpoint>>
registered cost endpoint without credentials |
| Async syncCostEndpoint | ||||||
syncCostEndpoint(endpoint: CostEndpoint)
|
||||||
|
Defined in src/cost/cost.service.ts:156
|
||||||
|
Function to sync cost data from given endpoint
Parameters :
Returns :
Promise<string>
string |
| Async testConnection | ||||||
testConnection(config, costProvider)
|
||||||
|
Defined in src/cost/cost.service.ts:226
|
||||||
|
Function to test connection with cost endpoint passed as input with credentials currently implemented just for kubecost and aws
Parameters :
Returns :
Promise<string>
api response from provider test connection function |
| Async updateCostEndpoint | |||||||||
updateCostEndpoint(id: number, cost: Prisma.CostEndpointUpdateInput)
|
|||||||||
|
Defined in src/cost/cost.service.ts:176
|
|||||||||
|
Function to update registered cost endpoint partially
Parameters :
Returns :
Promise<Partial<CostEndpoint>>
registered cost endpoint without credentials |
| validatePushCostMetricsRequest | ||||||
validatePushCostMetricsRequest(metrics, costEndpoint)
|
||||||
|
Defined in src/cost/cost.service.ts:105
|
||||||
|
Parameters :
Returns :
void
|
| Private CostTypeWorkflowMap |
Type : object
|
Default value : {
[provider.kubecost]: "childCostWorkflow",
[provider.aws]: "childAWSCostWorkflow",
[provider.azure]: "childAzureCostWorkflow",
[provider.openai]: "SyncCostMetrics",
[provider.anthropic]: "SyncCostMetrics",
}
|
|
Defined in src/cost/cost.service.ts:25
|
|
Map maintained to check which workflow to execute for a given cost Type eg: for CostType = kubecost , workflowName = ProjectApprovalWorkflow |
import { Injectable, InternalServerErrorException } from "@nestjs/common";
import { CostEndpoint, Prisma } from "@prisma/client";
import { PrismaService } from "../common/prisma/prisma.service";
import { getTextFromTemplate, getUuidSlug } from "../common/helper";
import { CostOverview, CostOverviewStatus } from "./types/cost-overview.interface";
import { TemporalProvider } from "../providers/temporal/temporal.provider";
import { CostProvider } from "./cost.provider";
import { CostProvider as provider } from "./types/cost-provider.enum";
import { CostEvents, costEventsConfig } from "./cost.events";
import {
NotificationChannel,
NotificationEntityType,
} from "../notification/types/notification.enums";
import { EventType } from "../common/events";
/**
* Service responsible for managing cost-related operations.
*/
@Injectable()
export class CostService {
/**
* Map maintained to check which workflow to execute for a
* given cost Type
* eg: for CostType = kubecost , workflowName = ProjectApprovalWorkflow
*/
private CostTypeWorkflowMap = {
[provider.kubecost]: "childCostWorkflow",
[provider.aws]: "childAWSCostWorkflow",
[provider.azure]: "childAzureCostWorkflow",
[provider.openai]: "SyncCostMetrics",
[provider.anthropic]: "SyncCostMetrics",
};
/**
* Constructs a new instance of the CostService class.
* @param prismaService
*/
constructor(
private readonly prismaService: PrismaService,
private readonly temporalProvider: TemporalProvider,
private readonly costProvider: CostProvider,
) {}
/**
* Function to register cost endpoint and trigger first sync
* @param cost
* @returns registered cost endpoint without credentials
*/
async registerCostEndpoint(
cost: Prisma.CostEndpointCreateInput,
): Promise<Partial<CostEndpoint>> {
const provider = this.getCostProvider(cost.endpointType);
cost.config = (await provider.encryptConfig(cost.config as any)) as any;
const created = await this.prismaService.costEndpoint.create({
data: { ...cost },
});
await this.syncCostEndpoint(created);
created.config = provider.maskConfigSecret(created.config as any) as any;
return created;
}
async pushCostMetrics(metrics, costEndpoint) {
const seenAt = new Date();
const turoAttributes = [
{
key: "turo.costEndpointId",
value: {
stringValue: `${costEndpoint.id}`,
},
},
{
key: "turo.projectID",
value: {
stringValue: `${costEndpoint.projectID}`,
},
},
{
key: "turo.timestamp",
value: {
intValue: seenAt.getTime(),
},
},
];
this.validatePushCostMetricsRequest(metrics, costEndpoint);
const response = await this.getCostProvider(
costEndpoint.endpointType,
).saveMetrics(metrics, turoAttributes);
// TODO:
// await this.updateLastSeenTime(costEndpoint.id, seenAt);
this.temporalProvider.runAsync(
"SyncCostMetrics",
[costEndpoint, true],
process.env.DEFAULT_QUEUE_NAME,
`workflow-SyncCostMetrics-cost-${costEndpoint.id}`,
"10s",
);
return response.json();
}
validatePushCostMetricsRequest(metrics, costEndpoint) {
const provider = this.getCostProvider(costEndpoint.endpointType);
try {
const requireAttributes = provider.getRequiredAttributesForPushMetrics();
metrics["resourceMetrics"].forEach((resource) => {
resource["scopeMetrics"].forEach((scope) => {
scope["metrics"].forEach((metric) => {
metric["gauge"]["dataPoints"].forEach((dataPoint) => {
const availableAttrData = {};
dataPoint["attributes"].forEach((attr) => {
availableAttrData[attr["key"]] = attr["value"];
});
requireAttributes.forEach((attr) => {
if (
!(attr.key in availableAttrData) ||
!availableAttrData[attr.key].hasOwnProperty(attr.valueType)
) {
throw Error(
`input metrics needs ${attr.key} as attribute with ${attr.valueType}`,
);
}
});
});
});
});
});
} catch (error) {
throw error;
}
return;
}
async fetchCostMetricsForSync(
costEndpoint: any,
queryOptions: {
startTime: number;
endTIme: number;
},
) {
return this.getCostProvider(
costEndpoint.endpointType,
).fetchCostMetricsForSync(costEndpoint, queryOptions);
}
/**
* Function to sync cost data from given endpoint
* @param endpoint
* @returns string
*/
async syncCostEndpoint(endpoint: CostEndpoint): Promise<string> {
const provider = this.getCostProvider(endpoint.endpointType);
endpoint.config = await provider.decryptConfig(endpoint.config);
const workflowName = this.CostTypeWorkflowMap[endpoint.endpointType];
return this.temporalProvider.runAsync(
workflowName,
[endpoint, true],
process.env.DEFAULT_QUEUE_NAME,
`workflow-${workflowName}-${getUuidSlug()}`,
);
}
/**
* Function to update registered cost endpoint partially
* @param id
* @param cost
* @returns registered cost endpoint without credentials
*/
async updateCostEndpoint(
id: number,
cost: Prisma.CostEndpointUpdateInput,
): Promise<Partial<CostEndpoint>> {
const existing = await this.getCostEndpointDetails(id);
const provider = this.getCostProvider(existing.endpointType);
if (cost.config) {
cost.config = await provider.encryptConfig(cost.config);
cost.config = {
...(existing.config as any),
...(cost.config as any),
};
}
const updated = await this.prismaService.costEndpoint.update({
where: {
id: id,
},
data: { ...cost },
});
updated.config = provider.maskConfigSecret(updated.config);
return updated;
}
/**
* Function to get cost endpoint details using cost endpoint id
* @param costEndpointId
* @returns Promise that resolves to CostEndpoint
*/
async getCostEndpointDetails(costEndpointId: number): Promise<CostEndpoint> {
const endpoint = await this.prismaService.costEndpoint.findUnique({
where: {
id: costEndpointId,
},
});
const provider = this.getCostProvider(endpoint.endpointType);
endpoint.config = await provider.decryptConfig(endpoint.config);
return endpoint;
}
/**
* Function to test connection with cost endpoint
* passed as input with credentials currently implemented just for
* kubecost and aws
* @param config
* @param provider
* @returns api response from provider test connection function
*/
async testConnection(config, costProvider): Promise<string> {
try {
const provider = this.getCostProvider(costProvider);
return await provider.testConnection(config);
} catch (error) {
throw new InternalServerErrorException(error.message);
}
}
/**
* Function to get tags data with cost endpoint
* passed as input with credentials currently implemented just for
* aws
* @param config
* @param provider
* @param getTagsInput
* @returns api response from provider get tags data function
*/
async getTagsData(config, costProvider, getTagsInput): Promise<string> {
try {
const provider = this.getCostProvider(costProvider);
return await provider.getTagsData(config, getTagsInput);
} catch (error) {
throw new InternalServerErrorException(error.message);
}
}
async getTagValuesForKey(config, costProvider, getTagsInput) {
try {
const provider = this.getCostProvider(costProvider);
return await provider.getTagValuesForKey(config, getTagsInput);
} catch (error) {
throw new InternalServerErrorException(error.message);
}
}
async getTagKeys(config, costProvider, getTagsInput) {
try {
const provider = this.getCostProvider(costProvider);
return await provider.getTagKeys(config, getTagsInput);
} catch (error) {
throw new InternalServerErrorException(error.message);
}
}
/**
* Function to get resources with tag filter
* @param config
* @param costProvider
* @param tagList
* @returns api response from provider get resource by tags function
*/
async getResourcesByTags(config, costProvider, tagList: string[]) {
try {
const provider = this.getCostProvider(costProvider);
return await provider.getResourcesByTags(config, tagList);
} catch (error) {
throw new InternalServerErrorException(error.message);
}
}
/**
* Function to list cost endpoints
* based on a projectID
* @param projectID
* @returns List of cost endpoint
*/
async getCostEndpoint(projectID): Promise<Partial<CostEndpoint>[]> {
const endpoints = await this.prismaService.costEndpoint.findMany({
where: {
projectID: projectID,
deleted: false,
},
});
return endpoints.map((endpoint) => {
const provider = this.getCostProvider(endpoint.endpointType);
endpoint.config = provider.maskConfigSecret(endpoint.config);
return endpoint;
});
}
/**
* Function to get list of cost endpoint ids for a project
* then query cost table for data based on window query parameter
* here we need to send back data grouped and summed up
* based on different step sizes eg:
* for window = today , step = hour , startTime = start of hour
* for window = oneMonth, step = day , startTime = start of day
* for window = sinMonths, step = month , startTime = start of month
* for window = yearly, step = year, startTime = start of year
* we are using $queryRaw here as we dont see any prisma groupBy syntax
* that supports DATE_TRUNC function fronm postgres
* this can be updated as we have some support in future
* we too considered creating database views but dont seed way to add
* DATE_TRUNC function in schema as well
* @param projectID
* @param window eg: today, oneMonth, sixMonths, yearly
* @returns Promise that resolves to cost data with date
*/
async getCost(projectID, window): Promise<object> {
let result;
const costEndpoints = await this.getCostEndpoint(projectID);
const costEndpointIds = costEndpoints.map((endpoint) => {
return endpoint.id;
});
const filters = await Promise.all(
costEndpoints.map(async (connection) => {
const [costType, tags] = await Promise.all([
this.prismaService.cost.findMany({
where: {
costEndpointId: connection.id,
},
distinct: ["type"],
select: {
type: true,
},
}),
this.prismaService.cost.findMany({
where: {
costEndpointId: connection.id,
},
distinct: ["tags"],
select: {
tags: true,
},
}),
]);
return {
connectionId: connection.id,
connectionName: connection.name,
costCategories: costType
? [...new Set(costType.flatMap(({ type }) => type))]
: [],
tags: tags ? [...new Set(tags.flatMap(({ tags }) => tags))] : [],
data: {},
};
}),
);
switch (window) {
case "today":
// dynamic query does not work well here so will
// have to run $query seperately for each case
result = await this.prismaService.$queryRaw`
SELECT DATE_TRUNC('hour', timestamp) AS step,
SUM(value) AS cost,service_name, type, cost_endpoint_id, tags
FROM cost
WHERE timestamp >= CURRENT_DATE
AND cost_endpoint_id = ANY(${costEndpointIds})
GROUP BY DATE_TRUNC('hour', timestamp),service_name, type, cost_endpoint_id, tags
ORDER BY step;
`;
break;
case "oneMonth":
// dynamic query does not work well here so will
// have to run $query seperately for each case
result = await this.prismaService.$queryRaw`
SELECT DATE_TRUNC('day', timestamp) AS step,
SUM(value) AS cost,service_name, type, cost_endpoint_id, tags
FROM cost
WHERE timestamp > CURRENT_DATE - INTERVAL '1 month'
AND cost_endpoint_id = ANY(${costEndpointIds})
GROUP BY DATE_TRUNC('day', timestamp),service_name, type, cost_endpoint_id, tags
ORDER BY step;
`;
break;
case "sixMonths":
// dynamic query does not work well here so will
// have to run $query seperately for each case
result = await this.prismaService.$queryRaw`
SELECT DATE_TRUNC('month', timestamp) AS step,
SUM(value) AS cost,service_name, type, cost_endpoint_id, tags
FROM cost
WHERE timestamp > CURRENT_DATE - INTERVAL '6 months'
AND cost_endpoint_id = ANY(${costEndpointIds})
GROUP BY DATE_TRUNC('month', timestamp),service_name, type, cost_endpoint_id, tags
ORDER BY step;
`;
break;
case "oneYear":
// dynamic query does not work well here so will
// have to run $query seperately for each case
result = await this.prismaService.$queryRaw`
SELECT DATE_TRUNC('month', timestamp) AS step,
SUM(value) AS cost,service_name, type, cost_endpoint_id, tags
FROM cost
WHERE timestamp > CURRENT_DATE - INTERVAL '12 months'
AND cost_endpoint_id = ANY(${costEndpointIds})
GROUP BY DATE_TRUNC('month', timestamp),service_name, type, cost_endpoint_id, tags
ORDER BY step;
`;
break;
case "YTD":
// dynamic query does not work well here so will
// have to run $query seperately for each case
result = await this.prismaService.$queryRaw`
SELECT DATE_TRUNC('month', timestamp) AS step,
SUM(value) AS cost, service_name, type, cost_endpoint_id, tags
FROM cost
WHERE extract(year from timestamp) = extract(year from CURRENT_DATE)
AND cost_endpoint_id = ANY(${costEndpointIds})
GROUP BY DATE_TRUNC('month', timestamp), service_name, type, cost_endpoint_id, tags
ORDER BY step;
`;
break;
case "all":
// dynamic query does not work well here so will
// have to run $query seperately for each case
// default will get all time data
result = await this.prismaService.$queryRaw`
SELECT DATE_TRUNC('year', timestamp) AS step,
SUM(value) AS cost,service_name, type, cost_endpoint_id, tags
FROM cost
WHERE cost_endpoint_id = ANY(${costEndpointIds})
GROUP BY DATE_TRUNC('year', timestamp),service_name, type, cost_endpoint_id, tags
ORDER BY step;
`;
break;
default:
throw Error("invalid window parameter");
}
const dataMap = {};
filters.forEach((connection) => {
dataMap[connection.connectionId] = connection;
});
result.forEach((data) => {
const category = data.type;
if (!(category in dataMap[data.cost_endpoint_id]["data"])) {
dataMap[data.cost_endpoint_id]["data"][category] = [];
}
dataMap[data.cost_endpoint_id]["data"][category].push({
step: data.step,
cost: data.cost,
tags: data.tags,
type: data.service_name,
});
});
return Object.keys(dataMap).map((key) => {
return dataMap[key];
});
}
/**
* Function to generate query string
* from object type of filter input
* @param filter
* @returns http query string
*/
formatQueryStringFromFilter(filter): string {
let queryString = "";
for (const key in filter) {
queryString +=
key.replace(".", "_").replace("/", "_") + "=" + filter[key];
queryString += "&";
}
return queryString;
}
/**
* Function to delete registered cost endpoint
* @param id
* @returns deleted cost endpoint
*/
async deleteCostEndpoint(id): Promise<Partial<CostEndpoint>> {
return await this.prismaService.costEndpoint.update({
where: {
id: id,
},
data: {
deleted: true,
},
});
}
/**
* Function to get last sync timestamp ISO string
* eg: '2024-04-17T12:30:00.000Z'
* @param costEndpointId
* @returns ISO string or empty string in case nothing synced yet
*/
async lastSyncTime(costEndpointId): Promise<string> {
const latestRecord = await this.prismaService.costDataRaw.findMany({
where: {
costEndpointId: costEndpointId,
},
orderBy: {
endTime: "desc",
},
take: 1,
});
return latestRecord.length > 0 ? latestRecord[0].endTime.toISOString() : "";
}
/**
* Function to get list of cost endpoints based on where input object
* @param costEndpointWhereInput
* @returns promise that resolves to list of cost endpoints
* with decrypted secrets
*/
async costEndpoints(
costEndpointWhereInput: Prisma.CostEndpointWhereInput,
): Promise<CostEndpoint[]> {
const costEndpoints = await this.prismaService.costEndpoint.findMany({
where: {
...costEndpointWhereInput,
deleted: false,
},
});
const transformedCostEndpoints = [];
for (const i in costEndpoints) {
const tmp = costEndpoints[i];
const provider = this.getCostProvider(tmp.endpointType);
tmp.config = await provider.decryptConfig(tmp.config);
transformedCostEndpoints.push(tmp);
}
return transformedCostEndpoints;
}
/**
* Function to list project wise cost incurred till date
* @returns promise that result to project wise cost overview
*/
getCostOverview(): Promise<CostOverview[]> {
return this.prismaService.costOverview.findMany({
where: {
status: {
not: {
in: [CostOverviewStatus.DELETED],
},
},
},
}).then((costs) => {
return costs.map((cost) => ({
...cost,
aggCost: cost.aggCost.toNumber(),
}));
});
}
getCostProvider(provider) {
return this.costProvider.getCostProvider(provider);
}
getCostProviderCategories() {
return this.costProvider.getCostProviderCategories();
}
listModels(provider) {
return this.getCostProvider(provider).listModels();
}
getConfiguredCosts(providerName) {
const provider = this.getCostProvider(providerName);
if (typeof provider["getConfiguredCosts"] === "function") {
return provider.getConfiguredCosts();
}
return [];
}
async getNotificationData(
costEndpointId: number,
event: CostEvents,
channel: NotificationChannel,
templateVars: any,
userId: string,
extraData: any,
): Promise<any> {
const costEndpoint = await this.getCostEndpointDetails(costEndpointId);
const config = costEventsConfig[event];
if (!config) {
throw new InternalServerErrorException(
`Event ${event} is not configured in costEventsConfig`,
);
}
if (!config.channelConfig[channel]) {
throw new InternalServerErrorException(
`Channel ${channel} is not configured for event ${event}`,
);
}
const template = config.channelConfig[channel];
const subject = getTextFromTemplate(template["subject"], templateVars);
const body = getTextFromTemplate(template["body"], templateVars);
const notificationData = {
projectId: costEndpoint.projectID,
subject: subject,
body: body,
severity: config.severity,
channel: channel,
userId: userId,
entityId: costEndpoint.id,
entityType: NotificationEntityType.COST_ENDPOINT,
eventType: EventType.COST_ENDPOINT,
eventIdentifier: event,
extraData: extraData ?? {},
};
return notificationData;
}
}