File

src/cost/cost.service.ts

Description

Service responsible for managing cost-related operations.

Index

Properties
Methods

Constructor

constructor(prismaService: PrismaService, temporalProvider: TemporalProvider, costProvider: CostProvider)

Constructs a new instance of the CostService class.

Parameters :
Name Type Optional
prismaService PrismaService No
temporalProvider TemporalProvider No
costProvider CostProvider No

Methods

Async costEndpoints
costEndpoints(costEndpointWhereInput: Prisma.CostEndpointWhereInput)

Function to get list of cost endpoints based on where input object with decrypted secrets

Parameters :
Name Type Optional
costEndpointWhereInput Prisma.CostEndpointWhereInput No
Returns : Promise<CostEndpoint[]>

promise that resolves to list of cost endpoints with decrypted secrets

Async deleteCostEndpoint
deleteCostEndpoint(id)

Function to delete registered cost endpoint

Parameters :
Name Optional
id No
Returns : Promise<Partial<CostEndpoint>>

deleted cost endpoint

Async fetchCostMetricsForSync
fetchCostMetricsForSync(costEndpoint: any, queryOptions: literal type)
Parameters :
Name Type Optional
costEndpoint any No
queryOptions literal type No
Returns : unknown
formatQueryStringFromFilter
formatQueryStringFromFilter(filter)

Function to generate query string from object type of filter input

Parameters :
Name Optional
filter No
Returns : string

http query string

getConfiguredCosts
getConfiguredCosts(providerName)
Parameters :
Name Optional
providerName No
Returns : any
Async getCost
getCost(projectID, window)

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 :
Name Optional Description
projectID No
window No

eg: today, oneMonth, sixMonths, yearly

Returns : Promise<object>

Promise that resolves to cost data with date

Async getCostEndpoint
getCostEndpoint(projectID)

Function to list cost endpoints based on a projectID

Parameters :
Name Optional
projectID No
Returns : Promise<Partial[]>

List of cost endpoint

Async getCostEndpointDetails
getCostEndpointDetails(costEndpointId: number)

Function to get cost endpoint details using cost endpoint id

Parameters :
Name Type Optional
costEndpointId number No
Returns : Promise<CostEndpoint>

Promise that resolves to CostEndpoint

getCostOverview
getCostOverview()

Function to list project wise cost incurred till date

promise that result to project wise cost overview

getCostProvider
getCostProvider(provider)
Parameters :
Name Optional
provider No
Returns : any
getCostProviderCategories
getCostProviderCategories()
Returns : any
Async getNotificationData
getNotificationData(costEndpointId: number, event: CostEvents, channel: NotificationChannel, templateVars: any, userId: string, extraData: any)
Parameters :
Name Type Optional
costEndpointId number No
event CostEvents No
channel NotificationChannel No
templateVars any No
userId string No
extraData any No
Returns : Promise<any>
Async getResourcesByTags
getResourcesByTags(config, costProvider, tagList: string[])

Function to get resources with tag filter

Parameters :
Name Type Optional
config No
costProvider No
tagList string[] No
Returns : unknown

api response from provider get resource by tags function

Async getTagKeys
getTagKeys(config, costProvider, getTagsInput)
Parameters :
Name Optional
config No
costProvider No
getTagsInput No
Returns : unknown
Async getTagsData
getTagsData(config, costProvider, getTagsInput)

Function to get tags data with cost endpoint passed as input with credentials currently implemented just for aws

Parameters :
Name Optional
config No
costProvider No
getTagsInput No
Returns : Promise<string>

api response from provider get tags data function

Async getTagValuesForKey
getTagValuesForKey(config, costProvider, getTagsInput)
Parameters :
Name Optional
config No
costProvider No
getTagsInput No
Returns : unknown
Async lastSyncTime
lastSyncTime(costEndpointId)

Function to get last sync timestamp ISO string eg: '2024-04-17T12:30:00.000Z'

Parameters :
Name Optional
costEndpointId No
Returns : Promise<string>

ISO string or empty string in case nothing synced yet

listModels
listModels(provider)
Parameters :
Name Optional
provider No
Returns : any
Async pushCostMetrics
pushCostMetrics(metrics, costEndpoint)
Parameters :
Name Optional
metrics No
costEndpoint No
Returns : unknown
Async registerCostEndpoint
registerCostEndpoint(cost: Prisma.CostEndpointCreateInput)

Function to register cost endpoint and trigger first sync

Parameters :
Name Type Optional
cost Prisma.CostEndpointCreateInput No
Returns : Promise<Partial<CostEndpoint>>

registered cost endpoint without credentials

Async syncCostEndpoint
syncCostEndpoint(endpoint: CostEndpoint)

Function to sync cost data from given endpoint

Parameters :
Name Type Optional
endpoint CostEndpoint No
Returns : Promise<string>

string

Async testConnection
testConnection(config, costProvider)

Function to test connection with cost endpoint passed as input with credentials currently implemented just for kubecost and aws

Parameters :
Name Optional
config No
costProvider No
Returns : Promise<string>

api response from provider test connection function

Async updateCostEndpoint
updateCostEndpoint(id: number, cost: Prisma.CostEndpointUpdateInput)

Function to update registered cost endpoint partially

Parameters :
Name Type Optional
id number No
cost Prisma.CostEndpointUpdateInput No
Returns : Promise<Partial<CostEndpoint>>

registered cost endpoint without credentials

validatePushCostMetricsRequest
validatePushCostMetricsRequest(metrics, costEndpoint)
Parameters :
Name Optional
metrics No
costEndpoint No
Returns : void

Properties

Private CostTypeWorkflowMap
Type : object
Default value : { [provider.kubecost]: "childCostWorkflow", [provider.aws]: "childAWSCostWorkflow", [provider.azure]: "childAzureCostWorkflow", [provider.openai]: "SyncCostMetrics", [provider.anthropic]: "SyncCostMetrics", }

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;
  }
}

results matching ""

    No results matching ""