import { methodValidator } from "../utils/methodValidator";
import { log } from "../utils/logging";
import { ClientSecretCredential } from "@azure/identity";
import { SubscriptionClient } from "@azure/arm-subscriptions";
import { ResourceManagementClient } from "@azure/arm-resources";
import { CostManagementClient } from "@azure/arm-costmanagement";
import getDatabase from "../utils/mongoClient";
const TENANT_ID = import.meta.env.AZURE_TENANT_ID as string;
const CLIENT_ID = import.meta.env.AZURE_CLIENT_ID as string;
const CLIENT_SECRET = import.meta.env.AZURE_CLIENT_SECRET as string;
const credential = new ClientSecretCredential(
const getSubscriptions = async () => {
log("Fetching subscriptions...", "DEBUG");
const client = new SubscriptionClient(credential);
const subscriptions = [];
for await (const subscription of client.subscriptions.list()) {
subscriptions.push(subscription);
log(`Fetched ${subscriptions.length} subscriptions.`, "DEBUG");
const getResourceGroups = async (subscriptionId: string) => {
log(`Fetching resource groups for subscription: ${subscriptionId}`, "DEBUG");
const client = new ResourceManagementClient(credential, subscriptionId);
const resourceGroups = [];
for await (const rg of client.resourceGroups.list()) {
`Fetched ${resourceGroups.length} resource groups for subscription ${subscriptionId}.`,
const getCostData = async (
resourceGroupName: string,
`Fetching cost data for resource group: ${resourceGroupName} in subscription: ${subscriptionId}`,
const costClient = new CostManagementClient(credential);
const result = await costClient.query.usage(
`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}`,
timeframe: "BillingMonth",
name: "ResourceLocation",
name: "MeterSubCategory",
`Fetched cost data for resource group ${resourceGroupName}: ${JSON.stringify(result)}`,
export const GET = async ({ request }: { request: Request }) => {
const url = new URL(request.url);
const organization = url.searchParams.get("organization");
const workspace = url.searchParams.get("workspace");
const projectId = url.searchParams.get("projectId");
log("Received GET request.", "DEBUG");
if (!methodValidator(request, ["GET"])) {
log("Invalid method used. Only GET is allowed.", "ERROR");
return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
headers: { "Content-Type": "application/json" },
log("Organization parameter is missing.", "ERROR");
JSON.stringify({ error: "Organization is required." }),
headers: { "Content-Type": "application/json" },
if ((workspace && projectId) || (!workspace && !projectId)) {
"Invalid parameters: Provide either workspace or projectId, but not both.",
error: "Provide either workspace or projectId, but not both.",
headers: { "Content-Type": "application/json" },
log("Checking database connection", "DEBUG");
const db = await getDatabase();
const identifier = workspace || projectId || "";
`Checking cached data for organization: ${organization}, identifier: ${identifier}`,
const collection = db.collection("azure_cost_data");
const cachedData = await collection.findOne({
`Returning cached data for organization: ${organization}, identifier: ${identifier}`,
return new Response(JSON.stringify(cachedData), {
headers: { "Content-Type": "application/json" },
log("Database not available, proceeding without caching.", "DEBUG");
log("Fetching subscriptions...", "DEBUG");
const subscriptions = await getSubscriptions();
totalAllRGsByLocation?: Record<string, string>;
totalAllRGsByTier?: Record<string, string>;
resourceGroupName: string;
costByCategory: Record<string, string>;
costByLocation: Record<string, string>;
costByTier: Record<string, string>;
results[identifier] = { organization, identifier, resourceGroups: [] };
let costCurrency: string | null = null;
const totalCostByLocation: Record<string, number> = {};
const totalCostByTier: Record<string, number> = {};
for (const subscription of subscriptions) {
const subscriptionId = subscription.subscriptionId;
log("Subscription ID is undefined.", "ERROR");
log(`Processing subscription: ${subscriptionId}`, "DEBUG");
const resourceGroups = await getResourceGroups(subscriptionId);
const matchingResourceGroups = resourceGroups.filter((rg: any) => {
const tags = rg.tags || {};
tags.organization === organization &&
((workspace && tags.workspace === workspace) ||
(projectId && tags.projectId === projectId))
`Found ${matchingResourceGroups.length} matching resource groups.`,
for (const rg of matchingResourceGroups) {
const resourceGroupName = rg.name;
if (!resourceGroupName) {
`Resource group name is undefined for subscription ${subscriptionId}`,
const costData = await getCostData(subscriptionId, resourceGroupName);
`Cost data response for ${resourceGroupName}: ${JSON.stringify(costData)}`,
if (!costData || !costData.columns || !costData.rows) {
log("No columns or rows in cost data result.", "ERROR");
const costIndex = costData.columns.findIndex(
(col: any) => col.name === "PreTaxCost",
const serviceNameIndex = costData.columns.findIndex(
(col: any) => col.name === "ServiceName",
const locationIndex = costData.columns.findIndex(
(col: any) => col.name === "ResourceLocation",
const tierIndex = costData.columns.findIndex(
(col: any) => col.name === "MeterSubCategory",
const currencyIndex = costData.columns.findIndex(
(col: any) => col.name === "Currency",
const costByCategory: Record<string, number> = {};
const costByLocation: Record<string, number> = {};
const costByTier: Record<string, number> = {};
for (const row of costData.rows) {
const cost = row[costIndex];
const serviceName = row[serviceNameIndex];
const resourceLocation = row[locationIndex];
const tier = row[tierIndex] || "No Tier Info";
currency = row[currencyIndex];
costByCategory[serviceName] =
(costByCategory[serviceName] || 0) + cost;
costByLocation[resourceLocation] =
(costByLocation[resourceLocation] || 0) + cost;
costByTier[tier] = (costByTier[tier] || 0) + cost;
totalCostSum += rgTotalCost;
for (const [location, cost] of Object.entries(costByLocation)) {
totalCostByLocation[location] =
(totalCostByLocation[location] || 0) + cost;
for (const [tier, cost] of Object.entries(costByTier)) {
totalCostByTier[tier] = (totalCostByTier[tier] || 0) + cost;
const formattedTotalCost = new Intl.NumberFormat("es-ES", {
const formattedCostByCategory: Record<string, string> = {};
for (const [serviceName, cost] of Object.entries(costByCategory)) {
formattedCostByCategory[serviceName] = new Intl.NumberFormat(
const formattedCostByLocation: Record<string, string> = {};
for (const [location, cost] of Object.entries(costByLocation)) {
formattedCostByLocation[location] = new Intl.NumberFormat("es-ES", {
const formattedCostByTier: Record<string, string> = {};
for (const [tier, cost] of Object.entries(costByTier)) {
formattedCostByTier[tier] = new Intl.NumberFormat("es-ES", {
results[identifier].resourceGroups.push({
totalCost: formattedTotalCost,
costByCategory: formattedCostByCategory,
costByLocation: formattedCostByLocation,
costByTier: formattedCostByTier,
log(`Error processing resource group ${rg.name}: ${error}`, "ERROR");
if (results[identifier].resourceGroups.length === 0) {
log("No matching resource groups found.", "ERROR");
JSON.stringify({ error: "No matching resource groups found." }),
headers: { "Content-Type": "application/json" },
results[identifier].totalAllRGs = new Intl.NumberFormat("es-ES", {
const formattedTotalAllRGsByLocation: Record<string, string> = {};
for (const [location, cost] of Object.entries(totalCostByLocation)) {
formattedTotalAllRGsByLocation[location] = new Intl.NumberFormat(
results[identifier].totalAllRGsByLocation =
formattedTotalAllRGsByLocation;
const formattedTotalAllRGsByTier: Record<string, string> = {};
for (const [tier, cost] of Object.entries(totalCostByTier)) {
formattedTotalAllRGsByTier[tier] = new Intl.NumberFormat("es-ES", {
results[identifier].totalAllRGsByTier = formattedTotalAllRGsByTier;
`Caching data for organization: ${organization}, identifier: ${identifier}`,
const collection = db.collection("azure_cost_data");
await collection.updateOne(
{ organization, identifier },
{ $set: results[identifier] },
return new Response(JSON.stringify(results[identifier]), {
headers: { "Content-Type": "application/json" },
} catch (error: unknown) {
`Internal server error: ${error instanceof Error ? error.stack : String(error)}`,
return new Response(JSON.stringify({ error: "Internal server error" }), {
headers: { "Content-Type": "application/json" },