@pagopa/dx-savemoney 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/__tests__/finding.test.js +22 -0
- package/dist/__tests__/finding.test.js.map +1 -1
- package/dist/azure/__tests__/analyzer-tags.test.js +28 -1
- package/dist/azure/__tests__/analyzer-tags.test.js.map +1 -1
- package/dist/azure/__tests__/config.test.js +31 -1
- package/dist/azure/__tests__/config.test.js.map +1 -1
- package/dist/azure/__tests__/report.test.js +71 -0
- package/dist/azure/__tests__/report.test.js.map +1 -1
- package/dist/azure/analyzer.d.ts +8 -1
- package/dist/azure/analyzer.d.ts.map +1 -1
- package/dist/azure/analyzer.js +57 -41
- package/dist/azure/analyzer.js.map +1 -1
- package/dist/azure/analyzers/registry.d.ts.map +1 -1
- package/dist/azure/analyzers/registry.js +4 -4
- package/dist/azure/analyzers/registry.js.map +1 -1
- package/dist/azure/analyzers/types.d.ts +6 -0
- package/dist/azure/analyzers/types.d.ts.map +1 -1
- package/dist/azure/config.d.ts.map +1 -1
- package/dist/azure/config.js +1 -5
- package/dist/azure/config.js.map +1 -1
- package/dist/azure/pricing/__tests__/cache.test.d.ts +8 -0
- package/dist/azure/pricing/__tests__/cache.test.d.ts.map +1 -0
- package/dist/azure/pricing/__tests__/cache.test.js +56 -0
- package/dist/azure/pricing/__tests__/cache.test.js.map +1 -0
- package/dist/azure/pricing/__tests__/client.test.d.ts +9 -0
- package/dist/azure/pricing/__tests__/client.test.d.ts.map +1 -0
- package/dist/azure/pricing/__tests__/client.test.js +181 -0
- package/dist/azure/pricing/__tests__/client.test.js.map +1 -0
- package/dist/azure/pricing/__tests__/pricing-service.test.d.ts +6 -0
- package/dist/azure/pricing/__tests__/pricing-service.test.d.ts.map +1 -0
- package/dist/azure/pricing/__tests__/pricing-service.test.js +283 -0
- package/dist/azure/pricing/__tests__/pricing-service.test.js.map +1 -0
- package/dist/azure/pricing/cache.d.ts +45 -0
- package/dist/azure/pricing/cache.d.ts.map +1 -0
- package/dist/azure/pricing/cache.js +79 -0
- package/dist/azure/pricing/cache.js.map +1 -0
- package/dist/azure/pricing/client.d.ts +67 -0
- package/dist/azure/pricing/client.d.ts.map +1 -0
- package/dist/azure/pricing/client.js +96 -0
- package/dist/azure/pricing/client.js.map +1 -0
- package/dist/azure/pricing/index.d.ts +6 -0
- package/dist/azure/pricing/index.d.ts.map +1 -0
- package/dist/azure/pricing/index.js +6 -0
- package/dist/azure/pricing/index.js.map +1 -0
- package/dist/azure/pricing/pricing-service.d.ts +46 -0
- package/dist/azure/pricing/pricing-service.d.ts.map +1 -0
- package/dist/azure/pricing/pricing-service.js +84 -0
- package/dist/azure/pricing/pricing-service.js.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/app-service.test.d.ts +5 -0
- package/dist/azure/pricing/resolvers/__tests__/app-service.test.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/app-service.test.js +137 -0
- package/dist/azure/pricing/resolvers/__tests__/app-service.test.js.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/disk.test.d.ts +5 -0
- package/dist/azure/pricing/resolvers/__tests__/disk.test.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/disk.test.js +160 -0
- package/dist/azure/pricing/resolvers/__tests__/disk.test.js.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/public-ip.test.d.ts +5 -0
- package/dist/azure/pricing/resolvers/__tests__/public-ip.test.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/public-ip.test.js +113 -0
- package/dist/azure/pricing/resolvers/__tests__/public-ip.test.js.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/vm.test.d.ts +9 -0
- package/dist/azure/pricing/resolvers/__tests__/vm.test.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/vm.test.js +204 -0
- package/dist/azure/pricing/resolvers/__tests__/vm.test.js.map +1 -0
- package/dist/azure/pricing/resolvers/app-service.d.ts +27 -0
- package/dist/azure/pricing/resolvers/app-service.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/app-service.js +67 -0
- package/dist/azure/pricing/resolvers/app-service.js.map +1 -0
- package/dist/azure/pricing/resolvers/disk.d.ts +59 -0
- package/dist/azure/pricing/resolvers/disk.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/disk.js +140 -0
- package/dist/azure/pricing/resolvers/disk.js.map +1 -0
- package/dist/azure/pricing/resolvers/index.d.ts +12 -0
- package/dist/azure/pricing/resolvers/index.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/index.js +8 -0
- package/dist/azure/pricing/resolvers/index.js.map +1 -0
- package/dist/azure/pricing/resolvers/public-ip.d.ts +32 -0
- package/dist/azure/pricing/resolvers/public-ip.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/public-ip.js +56 -0
- package/dist/azure/pricing/resolvers/public-ip.js.map +1 -0
- package/dist/azure/pricing/resolvers/vm.d.ts +51 -0
- package/dist/azure/pricing/resolvers/vm.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/vm.js +89 -0
- package/dist/azure/pricing/resolvers/vm.js.map +1 -0
- package/dist/azure/pricing/schema.d.ts +88 -0
- package/dist/azure/pricing/schema.d.ts.map +1 -0
- package/dist/azure/pricing/schema.js +64 -0
- package/dist/azure/pricing/schema.js.map +1 -0
- package/dist/azure/pricing/test-cache-dir.d.ts +3 -0
- package/dist/azure/pricing/test-cache-dir.d.ts.map +1 -0
- package/dist/azure/pricing/test-cache-dir.js +17 -0
- package/dist/azure/pricing/test-cache-dir.js.map +1 -0
- package/dist/azure/report.d.ts.map +1 -1
- package/dist/azure/report.js +57 -18
- package/dist/azure/report.js.map +1 -1
- package/dist/azure/resources/__tests__/app-service.test.d.ts +5 -0
- package/dist/azure/resources/__tests__/app-service.test.d.ts.map +1 -0
- package/dist/azure/resources/__tests__/app-service.test.js +105 -0
- package/dist/azure/resources/__tests__/app-service.test.js.map +1 -0
- package/dist/azure/resources/app-service.d.ts +8 -3
- package/dist/azure/resources/app-service.d.ts.map +1 -1
- package/dist/azure/resources/app-service.js +35 -2
- package/dist/azure/resources/app-service.js.map +1 -1
- package/dist/azure/resources/disk.d.ts +3 -1
- package/dist/azure/resources/disk.d.ts.map +1 -1
- package/dist/azure/resources/disk.js +35 -3
- package/dist/azure/resources/disk.js.map +1 -1
- package/dist/azure/resources/public-ip.d.ts +3 -1
- package/dist/azure/resources/public-ip.d.ts.map +1 -1
- package/dist/azure/resources/public-ip.js +41 -3
- package/dist/azure/resources/public-ip.js.map +1 -1
- package/dist/azure/resources/vm.d.ts +4 -1
- package/dist/azure/resources/vm.d.ts.map +1 -1
- package/dist/azure/resources/vm.js +49 -6
- package/dist/azure/resources/vm.js.map +1 -1
- package/dist/azure/types.d.ts +18 -3
- package/dist/azure/types.d.ts.map +1 -1
- package/dist/finding.d.ts +3 -3
- package/dist/finding.d.ts.map +1 -1
- package/dist/finding.js +7 -0
- package/dist/finding.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +5 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +5 -3
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/finding.test.ts +24 -0
- package/src/azure/__tests__/analyzer-tags.test.ts +36 -1
- package/src/azure/__tests__/config.test.ts +43 -1
- package/src/azure/__tests__/fixtures/full-override.yaml +2 -0
- package/src/azure/__tests__/report.test.ts +78 -0
- package/src/azure/analyzer.ts +70 -52
- package/src/azure/analyzers/registry.ts +8 -2
- package/src/azure/analyzers/types.ts +6 -0
- package/src/azure/config.ts +1 -5
- package/src/azure/pricing/__tests__/cache.test.ts +67 -0
- package/src/azure/pricing/__tests__/client.test.ts +212 -0
- package/src/azure/pricing/__tests__/fixtures/vm-b2s-westeurope.json +51 -0
- package/src/azure/pricing/__tests__/pricing-service.test.ts +315 -0
- package/src/azure/pricing/cache.ts +92 -0
- package/src/azure/pricing/client.ts +137 -0
- package/src/azure/pricing/index.ts +6 -0
- package/src/azure/pricing/pricing-service.ts +125 -0
- package/src/azure/pricing/resolvers/__tests__/app-service.test.ts +154 -0
- package/src/azure/pricing/resolvers/__tests__/disk.test.ts +189 -0
- package/src/azure/pricing/resolvers/__tests__/public-ip.test.ts +130 -0
- package/src/azure/pricing/resolvers/__tests__/vm.test.ts +232 -0
- package/src/azure/pricing/resolvers/app-service.ts +102 -0
- package/src/azure/pricing/resolvers/disk.ts +170 -0
- package/src/azure/pricing/resolvers/index.ts +31 -0
- package/src/azure/pricing/resolvers/public-ip.ts +87 -0
- package/src/azure/pricing/resolvers/vm.ts +125 -0
- package/src/azure/pricing/schema.ts +69 -0
- package/src/azure/pricing/test-cache-dir.ts +26 -0
- package/src/azure/report.ts +86 -21
- package/src/azure/resources/__tests__/app-service.test.ts +165 -0
- package/src/azure/resources/app-service.ts +65 -4
- package/src/azure/resources/disk.ts +46 -4
- package/src/azure/resources/public-ip.ts +61 -2
- package/src/azure/resources/vm.ts +89 -11
- package/src/azure/types.ts +19 -3
- package/src/finding.ts +10 -3
- package/src/index.ts +4 -3
- package/src/schema.ts +7 -3
- package/src/types.ts +12 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for analyzeAppServicePlan() pricing enrichment.
|
|
3
|
+
*/
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { DEFAULT_THRESHOLDS } from "../../../types.js";
|
|
6
|
+
vi.mock("../../utils.js", () => ({
|
|
7
|
+
getMetric: vi.fn(),
|
|
8
|
+
verboseLog: vi.fn(),
|
|
9
|
+
verboseLogAnalysisResult: vi.fn(),
|
|
10
|
+
verboseLogResourceStart: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
import { getMetric } from "../../utils.js";
|
|
13
|
+
import { analyzeAppServicePlan } from "../app-service.js";
|
|
14
|
+
const RESOURCE = {
|
|
15
|
+
id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Web/serverfarms/plan1",
|
|
16
|
+
location: "westeurope",
|
|
17
|
+
name: "plan1",
|
|
18
|
+
type: "Microsoft.Web/serverfarms",
|
|
19
|
+
};
|
|
20
|
+
const mockGetMetric = vi.mocked(getMetric);
|
|
21
|
+
function makeMonitorClient() {
|
|
22
|
+
return {
|
|
23
|
+
metrics: { list: vi.fn() },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function makePricingService() {
|
|
27
|
+
return {
|
|
28
|
+
resolveAppServicePlan: vi.fn().mockResolvedValue({
|
|
29
|
+
amount: 121.72,
|
|
30
|
+
currency: "EUR",
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function makeWebSiteClient(planDetails) {
|
|
35
|
+
const client = {
|
|
36
|
+
appServicePlans: {
|
|
37
|
+
get: vi.fn().mockResolvedValue({
|
|
38
|
+
location: "westeurope",
|
|
39
|
+
...planDetails,
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return client;
|
|
44
|
+
}
|
|
45
|
+
describe("analyzeAppServicePlan", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
mockGetMetric.mockReset();
|
|
48
|
+
mockGetMetric.mockResolvedValue(50);
|
|
49
|
+
});
|
|
50
|
+
it("enriches suspected unused plans with App Service Plan pricing", async () => {
|
|
51
|
+
const pricing = makePricingService();
|
|
52
|
+
const result = await analyzeAppServicePlan(RESOURCE, makeWebSiteClient({
|
|
53
|
+
numberOfSites: 0,
|
|
54
|
+
reserved: true,
|
|
55
|
+
sku: {
|
|
56
|
+
capacity: 2,
|
|
57
|
+
name: "S1",
|
|
58
|
+
tier: "Standard",
|
|
59
|
+
},
|
|
60
|
+
}), makeMonitorClient(), 30, DEFAULT_THRESHOLDS, false, undefined, pricing);
|
|
61
|
+
expect(result.estimatedMonthlySavings).toEqual({
|
|
62
|
+
amount: 121.72,
|
|
63
|
+
currency: "EUR",
|
|
64
|
+
});
|
|
65
|
+
expect(pricing.resolveAppServicePlan).toHaveBeenCalledWith({
|
|
66
|
+
armRegionName: "westeurope",
|
|
67
|
+
os: "linux",
|
|
68
|
+
skuName: "S1",
|
|
69
|
+
workerCount: 2,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it("does not price Premium underutilization as full removable cost", async () => {
|
|
73
|
+
mockGetMetric.mockResolvedValue(1);
|
|
74
|
+
const pricing = makePricingService();
|
|
75
|
+
const result = await analyzeAppServicePlan(RESOURCE, makeWebSiteClient({
|
|
76
|
+
numberOfSites: 2,
|
|
77
|
+
reserved: false,
|
|
78
|
+
sku: {
|
|
79
|
+
capacity: 3,
|
|
80
|
+
name: "P1v3",
|
|
81
|
+
tier: "PremiumV3",
|
|
82
|
+
},
|
|
83
|
+
}), makeMonitorClient(), 30, DEFAULT_THRESHOLDS, false, undefined, pricing);
|
|
84
|
+
expect(result.suspectedUnused).toBe(true);
|
|
85
|
+
expect(result.reason).toContain("Premium tier with low resource utilization");
|
|
86
|
+
expect(result.estimatedMonthlySavings).toBeUndefined();
|
|
87
|
+
expect(pricing.resolveAppServicePlan).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
it("does not treat missing site count as an empty plan", async () => {
|
|
90
|
+
mockGetMetric.mockResolvedValue(50);
|
|
91
|
+
const pricing = makePricingService();
|
|
92
|
+
const result = await analyzeAppServicePlan(RESOURCE, makeWebSiteClient({
|
|
93
|
+
reserved: true,
|
|
94
|
+
sku: {
|
|
95
|
+
capacity: 2,
|
|
96
|
+
name: "S1",
|
|
97
|
+
tier: "Standard",
|
|
98
|
+
},
|
|
99
|
+
}), makeMonitorClient(), 30, DEFAULT_THRESHOLDS, false, undefined, pricing);
|
|
100
|
+
expect(result.suspectedUnused).toBe(false);
|
|
101
|
+
expect(result.estimatedMonthlySavings).toBeUndefined();
|
|
102
|
+
expect(pricing.resolveAppServicePlan).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
//# sourceMappingURL=app-service.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-service.test.js","sourceRoot":"","sources":["../../../../src/azure/resources/__tests__/app-service.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* Azure App Service Plan analysis
|
|
3
3
|
*/
|
|
4
4
|
import type { WebSiteManagementClient } from "@azure/arm-appservice";
|
|
5
|
-
import type { MonitorClient } from "@azure/arm-monitor";
|
|
6
5
|
import * as armResources from "@azure/arm-resources";
|
|
7
6
|
import type { AnalysisResult, Thresholds } from "../../types.js";
|
|
8
|
-
import {
|
|
7
|
+
import type { PricingService } from "../pricing/pricing-service.js";
|
|
8
|
+
import { type MetricsCache, type MonitorClientLike } from "../utils.js";
|
|
9
|
+
export type AppServicePlanClientLike = {
|
|
10
|
+
appServicePlans: Pick<WebSiteManagementClient["appServicePlans"], "get">;
|
|
11
|
+
};
|
|
12
|
+
type AppServicePlanPricing = Pick<PricingService, "resolveAppServicePlan">;
|
|
9
13
|
/**
|
|
10
14
|
* Analyzes an Azure App Service Plan for potential cost optimization.
|
|
11
15
|
*
|
|
@@ -15,5 +19,6 @@ import { type MetricsCache } from "../utils.js";
|
|
|
15
19
|
* @param timespanDays - Number of days to analyze metrics
|
|
16
20
|
* @returns Analysis result with cost risk and reason
|
|
17
21
|
*/
|
|
18
|
-
export declare function analyzeAppServicePlan(resource: armResources.GenericResource, webSiteClient:
|
|
22
|
+
export declare function analyzeAppServicePlan(resource: armResources.GenericResource, webSiteClient: AppServicePlanClientLike, monitorClient: MonitorClientLike, timespanDays: number, thresholds?: Thresholds, verbose?: boolean, cache?: MetricsCache, pricing?: AppServicePlanPricing): Promise<AnalysisResult>;
|
|
23
|
+
export {};
|
|
19
24
|
//# sourceMappingURL=app-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-service.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/app-service.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"app-service.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/app-service.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAErE,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAGpE,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAIvB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,wBAAwB,GAAG;IACrC,eAAe,EAAE,IAAI,CAAC,uBAAuB,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,CAAC;CAC1E,CAAC;AAEF,KAAK,qBAAqB,GAAG,IAAI,CAAC,cAAc,EAAE,uBAAuB,CAAC,CAAC;AAE3E;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,YAAY,CAAC,eAAe,EACtC,aAAa,EAAE,wBAAwB,EACvC,aAAa,EAAE,iBAAiB,EAChC,YAAY,EAAE,MAAM,EACpB,UAAU,GAAE,UAA+B,EAC3C,OAAO,UAAQ,EACf,KAAK,CAAC,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,cAAc,CAAC,CAwGzB"}
|
|
@@ -13,7 +13,7 @@ import { getMetric, verboseLog, verboseLogAnalysisResult, verboseLogResourceStar
|
|
|
13
13
|
* @param timespanDays - Number of days to analyze metrics
|
|
14
14
|
* @returns Analysis result with cost risk and reason
|
|
15
15
|
*/
|
|
16
|
-
export async function analyzeAppServicePlan(resource, webSiteClient, monitorClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache) {
|
|
16
|
+
export async function analyzeAppServicePlan(resource, webSiteClient, monitorClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache, pricing) {
|
|
17
17
|
verboseLogResourceStart(verbose, resource.name || "unknown", "App Service Plan (microsoft.web/serverfarms)");
|
|
18
18
|
verboseLog(verbose, "Resource details:", resource);
|
|
19
19
|
const costRisk = "high";
|
|
@@ -33,8 +33,9 @@ export async function analyzeAppServicePlan(resource, webSiteClient, monitorClie
|
|
|
33
33
|
// Get detailed App Service Plan information
|
|
34
34
|
const planDetails = await webSiteClient.appServicePlans.get(resourceGroupName, planName);
|
|
35
35
|
verboseLog(verbose, "App Service Plan API details:", planDetails);
|
|
36
|
+
const hasNoApps = planDetails.numberOfSites === 0;
|
|
36
37
|
// Check if the plan has no apps
|
|
37
|
-
if (
|
|
38
|
+
if (hasNoApps) {
|
|
38
39
|
reason += "App Service Plan has no apps deployed. ";
|
|
39
40
|
}
|
|
40
41
|
// Check CPU and Memory metrics
|
|
@@ -54,6 +55,10 @@ export async function analyzeAppServicePlan(resource, webSiteClient, monitorClie
|
|
|
54
55
|
cpuPercentage < thresholds.appService.premiumCpuPercent) {
|
|
55
56
|
reason += "Premium tier with low resource utilization. ";
|
|
56
57
|
}
|
|
58
|
+
const suspectedUnused = reason.length > 0;
|
|
59
|
+
const result = await enrichWithPricing({ costRisk, reason: reason.trim(), suspectedUnused }, planDetails, resource, hasNoApps, pricing);
|
|
60
|
+
verboseLogAnalysisResult(verbose, result);
|
|
61
|
+
return result;
|
|
57
62
|
}
|
|
58
63
|
catch (error) {
|
|
59
64
|
const logger = getLogger(["savemoney", "azure"]);
|
|
@@ -65,4 +70,32 @@ export async function analyzeAppServicePlan(resource, webSiteClient, monitorClie
|
|
|
65
70
|
verboseLogAnalysisResult(verbose, result);
|
|
66
71
|
return result;
|
|
67
72
|
}
|
|
73
|
+
async function enrichWithPricing(result, planDetails, resource, hasNoApps, pricing) {
|
|
74
|
+
// Underutilized plans need right-sizing analysis, not a full removable
|
|
75
|
+
// cost-at-risk.
|
|
76
|
+
if (!pricing || !result.suspectedUnused || !hasNoApps) {
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const skuName = planDetails.sku?.name;
|
|
81
|
+
const armRegionName = planDetails.location ?? resource.location;
|
|
82
|
+
if (!skuName || !armRegionName) {
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
const estimatedMonthlySavings = await pricing.resolveAppServicePlan({
|
|
86
|
+
armRegionName,
|
|
87
|
+
os: planDetails.reserved === true ? "linux" : "windows",
|
|
88
|
+
skuName,
|
|
89
|
+
workerCount: planDetails.sku?.capacity ?? planDetails.numberOfWorkers,
|
|
90
|
+
});
|
|
91
|
+
return estimatedMonthlySavings
|
|
92
|
+
? { ...result, estimatedMonthlySavings }
|
|
93
|
+
: result;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
97
|
+
logger.warn(`Failed to enrich App Service Plan ${resource.name ?? "unknown"} with pricing: ${error instanceof Error ? error.message : error}`);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
68
101
|
//# sourceMappingURL=app-service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-service.js","sourceRoot":"","sources":["../../../src/azure/resources/app-service.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"app-service.js","sourceRoot":"","sources":["../../../src/azure/resources/app-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
import type { ComputeManagementClient } from "@azure/arm-compute";
|
|
5
5
|
import * as armResources from "@azure/arm-resources";
|
|
6
6
|
import type { AnalysisResult } from "../../types.js";
|
|
7
|
+
import type { PricingService } from "../pricing/pricing-service.js";
|
|
7
8
|
/**
|
|
8
9
|
* Analyzes an Azure Managed Disk for potential cost optimization.
|
|
9
10
|
*
|
|
10
11
|
* @param resource - The Azure resource object
|
|
11
12
|
* @param computeClient - Azure Compute client for disk details
|
|
12
13
|
* @param verbose - Whether verbose logging is enabled
|
|
14
|
+
* @param pricing - Optional pricing service for monetary enrichment
|
|
13
15
|
* @returns Analysis result with cost risk and reason
|
|
14
16
|
*/
|
|
15
|
-
export declare function analyzeDisk(resource: armResources.GenericResource, computeClient: ComputeManagementClient, verbose?: boolean): Promise<AnalysisResult>;
|
|
17
|
+
export declare function analyzeDisk(resource: armResources.GenericResource, computeClient: ComputeManagementClient, verbose?: boolean, pricing?: PricingService): Promise<AnalysisResult>;
|
|
16
18
|
//# sourceMappingURL=disk.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"disk.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/disk.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,
|
|
1
|
+
{"version":3,"file":"disk.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/disk.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAQ,MAAM,oBAAoB,CAAC;AAExE,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAIrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAUpE;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,YAAY,CAAC,eAAe,EACtC,aAAa,EAAE,uBAAuB,EACtC,OAAO,UAAQ,EACf,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,cAAc,CAAC,CA2DzB"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Azure Managed Disk analysis
|
|
3
3
|
*/
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
5
|
+
import { DiskSkuSchema } from "../pricing/resolvers/index.js";
|
|
5
6
|
import { verboseLog, verboseLogAnalysisResult, verboseLogResourceStart, } from "../utils.js";
|
|
6
7
|
/**
|
|
7
8
|
* Analyzes an Azure Managed Disk for potential cost optimization.
|
|
@@ -9,9 +10,10 @@ import { verboseLog, verboseLogAnalysisResult, verboseLogResourceStart, } from "
|
|
|
9
10
|
* @param resource - The Azure resource object
|
|
10
11
|
* @param computeClient - Azure Compute client for disk details
|
|
11
12
|
* @param verbose - Whether verbose logging is enabled
|
|
13
|
+
* @param pricing - Optional pricing service for monetary enrichment
|
|
12
14
|
* @returns Analysis result with cost risk and reason
|
|
13
15
|
*/
|
|
14
|
-
export async function analyzeDisk(resource, computeClient, verbose = false) {
|
|
16
|
+
export async function analyzeDisk(resource, computeClient, verbose = false, pricing) {
|
|
15
17
|
verboseLogResourceStart(verbose, resource.name || "unknown", "Managed Disk (microsoft.compute/disks)");
|
|
16
18
|
verboseLog(verbose, "Resource details:", resource);
|
|
17
19
|
const costRisk = "medium";
|
|
@@ -38,8 +40,9 @@ export async function analyzeDisk(resource, computeClient, verbose = false) {
|
|
|
38
40
|
reason: "Disk is unattached. ",
|
|
39
41
|
suspectedUnused: true,
|
|
40
42
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
const enriched = await enrichWithPricing(result, diskDetails, pricing);
|
|
44
|
+
verboseLogAnalysisResult(verbose, enriched);
|
|
45
|
+
return enriched;
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
catch (error) {
|
|
@@ -53,4 +56,33 @@ export async function analyzeDisk(resource, computeClient, verbose = false) {
|
|
|
53
56
|
verboseLogAnalysisResult(verbose, result);
|
|
54
57
|
return result;
|
|
55
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Best-effort: looks up the monthly price for the disk's `(sku, size,
|
|
61
|
+
* region)` triple and attaches it as `estimatedMonthlySavings`. Any
|
|
62
|
+
* failure (unsupported SKU, missing fields, resolver error) leaves the
|
|
63
|
+
* original result unchanged.
|
|
64
|
+
*/
|
|
65
|
+
async function enrichWithPricing(result, diskDetails, pricing) {
|
|
66
|
+
if (!pricing)
|
|
67
|
+
return result;
|
|
68
|
+
const sku = toSupportedSku(diskDetails.sku?.name);
|
|
69
|
+
const diskSizeGiB = diskDetails.diskSizeGB;
|
|
70
|
+
const armRegionName = diskDetails.location;
|
|
71
|
+
if (!sku || !diskSizeGiB || !armRegionName)
|
|
72
|
+
return result;
|
|
73
|
+
const money = await pricing.resolveDisk({
|
|
74
|
+
armRegionName,
|
|
75
|
+
diskSizeGiB,
|
|
76
|
+
sku,
|
|
77
|
+
});
|
|
78
|
+
return money ? { ...result, estimatedMonthlySavings: money } : result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Narrows the SDK's `sku.name` string to the resolver-supported subset
|
|
82
|
+
* (excludes `UltraSSD_LRS` and any future tiers we don't price yet).
|
|
83
|
+
*/
|
|
84
|
+
function toSupportedSku(name) {
|
|
85
|
+
const parsed = DiskSkuSchema.safeParse(name);
|
|
86
|
+
return parsed.success ? parsed.data : undefined;
|
|
87
|
+
}
|
|
56
88
|
//# sourceMappingURL=disk.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"disk.js","sourceRoot":"","sources":["../../../src/azure/resources/disk.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"disk.js","sourceRoot":"","sources":["../../../src/azure/resources/disk.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ import type { MonitorClient } from "@azure/arm-monitor";
|
|
|
5
5
|
import type { NetworkManagementClient } from "@azure/arm-network";
|
|
6
6
|
import * as armResources from "@azure/arm-resources";
|
|
7
7
|
import type { AnalysisResult, Thresholds } from "../../types.js";
|
|
8
|
+
import type { PricingService } from "../pricing/pricing-service.js";
|
|
8
9
|
import { type MetricsCache } from "../utils.js";
|
|
9
10
|
/**
|
|
10
11
|
* Analyzes an Azure Public IP for potential cost optimization.
|
|
@@ -13,7 +14,8 @@ import { type MetricsCache } from "../utils.js";
|
|
|
13
14
|
* @param networkClient - Azure Network client for Public IP details
|
|
14
15
|
* @param monitorClient - Azure Monitor client for metrics
|
|
15
16
|
* @param timespanDays - Number of days to analyze metrics
|
|
17
|
+
* @param pricing - Optional pricing facade for estimated monthly cost
|
|
16
18
|
* @returns Analysis result with cost risk and reason
|
|
17
19
|
*/
|
|
18
|
-
export declare function analyzePublicIp(resource: armResources.GenericResource, networkClient: NetworkManagementClient, monitorClient: MonitorClient, timespanDays: number, thresholds?: Thresholds, verbose?: boolean, cache?: MetricsCache): Promise<AnalysisResult>;
|
|
20
|
+
export declare function analyzePublicIp(resource: armResources.GenericResource, networkClient: NetworkManagementClient, monitorClient: MonitorClient, timespanDays: number, thresholds?: Thresholds, verbose?: boolean, cache?: MetricsCache, pricing?: PricingService): Promise<AnalysisResult>;
|
|
19
21
|
//# sourceMappingURL=public-ip.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public-ip.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/public-ip.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"public-ip.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/public-ip.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAGpE,OAAO,EAEL,KAAK,YAAY,EAIlB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,YAAY,CAAC,eAAe,EACtC,aAAa,EAAE,uBAAuB,EACtC,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,MAAM,EACpB,UAAU,GAAE,UAA+B,EAC3C,OAAO,UAAQ,EACf,KAAK,CAAC,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,cAAc,CAAC,CAmFzB"}
|
|
@@ -11,9 +11,10 @@ import { getMetric, verboseLog, verboseLogAnalysisResult, verboseLogResourceStar
|
|
|
11
11
|
* @param networkClient - Azure Network client for Public IP details
|
|
12
12
|
* @param monitorClient - Azure Monitor client for metrics
|
|
13
13
|
* @param timespanDays - Number of days to analyze metrics
|
|
14
|
+
* @param pricing - Optional pricing facade for estimated monthly cost
|
|
14
15
|
* @returns Analysis result with cost risk and reason
|
|
15
16
|
*/
|
|
16
|
-
export async function analyzePublicIp(resource, networkClient, monitorClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache) {
|
|
17
|
+
export async function analyzePublicIp(resource, networkClient, monitorClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache, pricing) {
|
|
17
18
|
verboseLogResourceStart(verbose, resource.name || "unknown", "Public IP (microsoft.network/publicipaddresses)");
|
|
18
19
|
verboseLog(verbose, "Resource details:", resource);
|
|
19
20
|
const costRisk = "medium";
|
|
@@ -29,9 +30,12 @@ export async function analyzePublicIp(resource, networkClient, monitorClient, ti
|
|
|
29
30
|
const resourceParts = resource.id.split("/");
|
|
30
31
|
const resourceGroupName = resourceParts[4];
|
|
31
32
|
const publicIpName = resourceParts[8];
|
|
33
|
+
// Capture details outside the try so the pricing enrichment below can
|
|
34
|
+
// reuse them without an extra API round-trip.
|
|
35
|
+
let publicIpDetails;
|
|
32
36
|
try {
|
|
33
37
|
// Get detailed Public IP information
|
|
34
|
-
|
|
38
|
+
publicIpDetails = await networkClient.publicIPAddresses.get(resourceGroupName, publicIpName);
|
|
35
39
|
verboseLog(verbose, "Public IP API details:", publicIpDetails);
|
|
36
40
|
// Check if Public IP is not associated with any resource
|
|
37
41
|
if (!publicIpDetails.ipConfiguration && !publicIpDetails.natGateway) {
|
|
@@ -54,8 +58,42 @@ export async function analyzePublicIp(resource, networkClient, monitorClient, ti
|
|
|
54
58
|
reason += "Could not retrieve detailed Public IP information. ";
|
|
55
59
|
}
|
|
56
60
|
const suspectedUnused = reason.length > 0;
|
|
57
|
-
const
|
|
61
|
+
const baseResult = { costRisk, reason: reason.trim(), suspectedUnused };
|
|
62
|
+
const result = await enrichWithPricing(baseResult, resource, publicIpDetails, pricing);
|
|
58
63
|
verboseLogAnalysisResult(verbose, result);
|
|
59
64
|
return result;
|
|
60
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Best-effort enrichment of a Public IP analysis result with the
|
|
68
|
+
* estimated monthly cost recoverable by removing the resource.
|
|
69
|
+
*/
|
|
70
|
+
async function enrichWithPricing(result, resource, publicIpDetails, pricing) {
|
|
71
|
+
if (!pricing || !result.suspectedUnused || !publicIpDetails) {
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const skuName = publicIpDetails.sku?.name?.toLowerCase();
|
|
76
|
+
const allocation = publicIpDetails.publicIPAllocationMethod?.toLowerCase();
|
|
77
|
+
const armRegionName = publicIpDetails.location ?? resource.location;
|
|
78
|
+
if (!armRegionName ||
|
|
79
|
+
(skuName !== "basic" && skuName !== "standard") ||
|
|
80
|
+
(allocation !== "static" && allocation !== "dynamic")) {
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
const estimatedMonthlySavings = await pricing.resolvePublicIp({
|
|
84
|
+
allocation,
|
|
85
|
+
armRegionName,
|
|
86
|
+
sku: skuName,
|
|
87
|
+
});
|
|
88
|
+
if (!estimatedMonthlySavings) {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return { ...result, estimatedMonthlySavings };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
95
|
+
logger.warn(`Failed to enrich Public IP with pricing: ${error instanceof Error ? error.message : error}`);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
61
99
|
//# sourceMappingURL=public-ip.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public-ip.js","sourceRoot":"","sources":["../../../src/azure/resources/public-ip.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"public-ip.js","sourceRoot":"","sources":["../../../src/azure/resources/public-ip.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ import type { ComputeManagementClient } from "@azure/arm-compute";
|
|
|
5
5
|
import type { MonitorClient } from "@azure/arm-monitor";
|
|
6
6
|
import * as armResources from "@azure/arm-resources";
|
|
7
7
|
import type { AnalysisResult, Thresholds } from "../../types.js";
|
|
8
|
+
import type { PricingService } from "../pricing/pricing-service.js";
|
|
8
9
|
import { type MetricsCache } from "../utils.js";
|
|
9
10
|
/**
|
|
10
11
|
* Analyzes an Azure Virtual Machine for potential cost optimization.
|
|
@@ -14,7 +15,9 @@ import { type MetricsCache } from "../utils.js";
|
|
|
14
15
|
* @param computeClient - Azure Compute client for VM details
|
|
15
16
|
* @param timespanDays - Number of days to analyze metrics
|
|
16
17
|
* @param verbose - Whether verbose logging is enabled
|
|
18
|
+
* @param cache - Run-scoped metrics cache
|
|
19
|
+
* @param pricing - Optional pricing facade for estimated monthly cost
|
|
17
20
|
* @returns Analysis result with cost risk and reason
|
|
18
21
|
*/
|
|
19
|
-
export declare function analyzeVM(resource: armResources.GenericResource, monitorClient: MonitorClient, computeClient: ComputeManagementClient, timespanDays: number, thresholds?: Thresholds, verbose?: boolean, cache?: MetricsCache): Promise<AnalysisResult>;
|
|
22
|
+
export declare function analyzeVM(resource: armResources.GenericResource, monitorClient: MonitorClient, computeClient: ComputeManagementClient, timespanDays: number, thresholds?: Thresholds, verbose?: boolean, cache?: MetricsCache, pricing?: PricingService): Promise<AnalysisResult>;
|
|
20
23
|
//# sourceMappingURL=vm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vm.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/vm.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"vm.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/vm.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAGpE,OAAO,EAEL,KAAK,YAAY,EAIlB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,YAAY,CAAC,eAAe,EACtC,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,uBAAuB,EACtC,YAAY,EAAE,MAAM,EACpB,UAAU,GAAE,UAA+B,EAC3C,OAAO,UAAQ,EACf,KAAK,CAAC,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,cAAc,CAAC,CAkHzB"}
|
|
@@ -12,9 +12,11 @@ import { getMetric, verboseLog, verboseLogAnalysisResult, verboseLogResourceStar
|
|
|
12
12
|
* @param computeClient - Azure Compute client for VM details
|
|
13
13
|
* @param timespanDays - Number of days to analyze metrics
|
|
14
14
|
* @param verbose - Whether verbose logging is enabled
|
|
15
|
+
* @param cache - Run-scoped metrics cache
|
|
16
|
+
* @param pricing - Optional pricing facade for estimated monthly cost
|
|
15
17
|
* @returns Analysis result with cost risk and reason
|
|
16
18
|
*/
|
|
17
|
-
export async function analyzeVM(resource, monitorClient, computeClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache) {
|
|
19
|
+
export async function analyzeVM(resource, monitorClient, computeClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache, pricing) {
|
|
18
20
|
verboseLogResourceStart(verbose, resource.name || "unknown", "Virtual Machine (microsoft.compute/virtualmachines)");
|
|
19
21
|
verboseLog(verbose, "Resource details:", resource);
|
|
20
22
|
const costRisk = "high";
|
|
@@ -37,20 +39,20 @@ export async function analyzeVM(resource, monitorClient, computeClient, timespan
|
|
|
37
39
|
// Check power state from instance view
|
|
38
40
|
const vmStatus = instanceView.statuses?.find((s) => s.code?.startsWith("PowerState/"));
|
|
39
41
|
if (vmStatus?.code === "PowerState/deallocated") {
|
|
40
|
-
const result = {
|
|
42
|
+
const result = await enrichWithPricing({
|
|
41
43
|
costRisk,
|
|
42
44
|
reason: "VM is deallocated. ",
|
|
43
45
|
suspectedUnused: true,
|
|
44
|
-
};
|
|
46
|
+
}, resource, computeClient, resourceGroupName, vmName, pricing);
|
|
45
47
|
verboseLogAnalysisResult(verbose, result);
|
|
46
48
|
return result;
|
|
47
49
|
}
|
|
48
50
|
if (vmStatus?.code === "PowerState/stopped") {
|
|
49
|
-
const result = {
|
|
51
|
+
const result = await enrichWithPricing({
|
|
50
52
|
costRisk,
|
|
51
53
|
reason: "VM is stopped. ",
|
|
52
54
|
suspectedUnused: true,
|
|
53
|
-
};
|
|
55
|
+
}, resource, computeClient, resourceGroupName, vmName, pricing);
|
|
54
56
|
verboseLogAnalysisResult(verbose, result);
|
|
55
57
|
return result;
|
|
56
58
|
}
|
|
@@ -69,8 +71,49 @@ export async function analyzeVM(resource, monitorClient, computeClient, timespan
|
|
|
69
71
|
if (networkIn !== null && networkIn < thresholds.vm.networkInBytesPerDay) {
|
|
70
72
|
reason += `Low network traffic (${(networkIn / 1024 / 1024).toFixed(2)} MB/day avg). `;
|
|
71
73
|
}
|
|
72
|
-
const result = { costRisk, reason, suspectedUnused: reason.length > 0 };
|
|
74
|
+
const result = await enrichWithPricing({ costRisk, reason, suspectedUnused: reason.length > 0 }, resource, computeClient, resourceGroupName, vmName, pricing);
|
|
73
75
|
verboseLogAnalysisResult(verbose, result);
|
|
74
76
|
return result;
|
|
75
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Best-effort enrichment of a VM analysis result with the estimated
|
|
80
|
+
* monthly cost recoverable by removing/right-sizing the resource.
|
|
81
|
+
*
|
|
82
|
+
* No-op when:
|
|
83
|
+
* - the result is not flagged as suspected unused
|
|
84
|
+
* - no pricing facade was provided
|
|
85
|
+
* - the VM SKU or region cannot be determined
|
|
86
|
+
* - the Retail Prices lookup returns no matching meter
|
|
87
|
+
*
|
|
88
|
+
* Any error along the way is logged at warn level and swallowed so the
|
|
89
|
+
* main analysis never fails because of a pricing problem.
|
|
90
|
+
*/
|
|
91
|
+
async function enrichWithPricing(result, resource, computeClient, resourceGroupName, vmName, pricing) {
|
|
92
|
+
if (!pricing || !result.suspectedUnused) {
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const details = await computeClient.virtualMachines.get(resourceGroupName, vmName);
|
|
97
|
+
const armSkuName = details.hardwareProfile?.vmSize;
|
|
98
|
+
const armRegionName = details.location ?? resource.location;
|
|
99
|
+
if (!armSkuName || !armRegionName) {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
const os = details.storageProfile?.osDisk?.osType?.toLowerCase();
|
|
103
|
+
const estimatedMonthlySavings = await pricing.resolveVm({
|
|
104
|
+
armRegionName,
|
|
105
|
+
armSkuName,
|
|
106
|
+
os: os === "windows" ? "windows" : "linux",
|
|
107
|
+
});
|
|
108
|
+
if (!estimatedMonthlySavings) {
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
return { ...result, estimatedMonthlySavings };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
115
|
+
logger.warn(`Failed to enrich VM ${vmName} with pricing: ${error instanceof Error ? error.message : error}`);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
76
119
|
//# sourceMappingURL=vm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vm.js","sourceRoot":"","sources":["../../../src/azure/resources/vm.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"vm.js","sourceRoot":"","sources":["../../../src/azure/resources/vm.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAK7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EACL,SAAS,EAET,UAAU,EACV,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAAsC,EACtC,aAA4B,EAC5B,aAAsC,EACtC,YAAoB,EACpB,aAAyB,kBAAkB,EAC3C,OAAO,GAAG,KAAK,EACf,KAAoB,EACpB,OAAwB;IAExB,uBAAuB,CACrB,OAAO,EACP,QAAQ,CAAC,IAAI,IAAI,SAAS,EAC1B,qDAAqD,CACtD,CAAC;IACF,UAAU,CAAC,OAAO,EAAE,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAEnD,MAAM,QAAQ,GAA8B,MAAM,CAAC;IACnD,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO;YACL,QAAQ;YACR,MAAM,EAAE,yBAAyB;YACjC,eAAe,EAAE,KAAK;SACvB,CAAC;IACJ,CAAC;IAED,sDAAsD;IACtD,MAAM,aAAa,GAAG,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,iBAAiB,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAEhC,IAAI,CAAC;QACH,uDAAuD;QACvD,MAAM,YAAY,GAAG,MAAM,aAAa,CAAC,eAAe,CAAC,YAAY,CACnE,iBAAiB,EACjB,MAAM,CACP,CAAC;QAEF,UAAU,CAAC,OAAO,EAAE,mBAAmB,EAAE,YAAY,CAAC,CAAC;QAEvD,uCAAuC;QACvC,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAoB,EAAE,EAAE,CACpE,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,aAAa,CAAC,CAClC,CAAC;QAEF,IAAI,QAAQ,EAAE,IAAI,KAAK,wBAAwB,EAAE,CAAC;YAChD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CACpC;gBACE,QAAQ;gBACR,MAAM,EAAE,qBAAqB;gBAC7B,eAAe,EAAE,IAAI;aACtB,EACD,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,MAAM,EACN,OAAO,CACR,CAAC;YACF,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC1C,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,IAAI,QAAQ,EAAE,IAAI,KAAK,oBAAoB,EAAE,CAAC;YAC5C,MAAM,MAAM,GAAG,MAAM,iBAAiB,CACpC;gBACE,QAAQ;gBACR,MAAM,EAAE,iBAAiB;gBACzB,eAAe,EAAE,IAAI;aACtB,EACD,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,MAAM,EACN,OAAO,CACR,CAAC;YACF,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC1C,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,IAAI,CACT,sCAAsC,MAAM,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAClG,CAAC;QACF,uDAAuD;IACzD,CAAC;IAED,oCAAoC;IACpC,MAAM,QAAQ,GAAG,MAAM,SAAS,CAC9B,aAAa,EACb,QAAQ,CAAC,EAAE,EACX,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,KAAK,CACN,CAAC;IACF,MAAM,SAAS,GAAG,MAAM,SAAS,CAC/B,aAAa,EACb,QAAQ,CAAC,EAAE,EACX,kBAAkB,EAClB,SAAS,EACT,YAAY,EACZ,KAAK,CACN,CAAC;IAEF,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,GAAG,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC;QAC7D,MAAM,IAAI,sBAAsB,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IAC5D,CAAC;IACD,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,GAAG,UAAU,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC;QACzE,MAAM,IAAI,wBAAwB,CAAC,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC;IACzF,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,iBAAiB,CACpC,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,EACxD,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,MAAM,EACN,OAAO,CACR,CAAC;IACF,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,KAAK,UAAU,iBAAiB,CAC9B,MAAsB,EACtB,QAAsC,EACtC,aAAsC,EACtC,iBAAyB,EACzB,MAAc,EACd,OAAwB;IAExB,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;QACxC,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,eAAe,CAAC,GAAG,CACrD,iBAAiB,EACjB,MAAM,CACP,CAAC;QACF,MAAM,UAAU,GAAG,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC;QACnD,MAAM,aAAa,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC;QAC5D,IAAI,CAAC,UAAU,IAAI,CAAC,aAAa,EAAE,CAAC;YAClC,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QACjE,MAAM,uBAAuB,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC;YACtD,aAAa;YACb,UAAU;YACV,EAAE,EAAE,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO;SAC3C,CAAC,CAAC;QACH,IAAI,CAAC,uBAAuB,EAAE,CAAC;YAC7B,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,EAAE,GAAG,MAAM,EAAE,uBAAuB,EAAE,CAAC;IAChD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,IAAI,CACT,uBAAuB,MAAM,kBAAkB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAChG,CAAC;QACF,OAAO,MAAM,CAAC;IAChB,CAAC;AACH,CAAC"}
|
package/dist/azure/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type * as armResources from "@azure/arm-resources";
|
|
5
5
|
import type { Finding } from "../finding.js";
|
|
6
|
+
import type { Config } from "../schema.js";
|
|
6
7
|
import type { AnalysisResult, BaseConfig, CostRisk, Thresholds } from "../types.js";
|
|
7
8
|
/**
|
|
8
9
|
* Azure configuration extending base config
|
|
@@ -19,14 +20,21 @@ export type AzureConfig = BaseConfig & {
|
|
|
19
20
|
* If omitted, all resources are analyzed.
|
|
20
21
|
*/
|
|
21
22
|
filterTags?: Map<string, string>;
|
|
23
|
+
/**
|
|
24
|
+
* Runtime-only pricing enrichment switch. This is intentionally not exposed
|
|
25
|
+
* in YAML config: the Retail Prices lookup uses internal defaults unless the
|
|
26
|
+
* CLI disables it for offline/debug runs.
|
|
27
|
+
*/
|
|
28
|
+
pricing?: PricingConfig;
|
|
22
29
|
/**
|
|
23
30
|
* Which finding sources to include in the run.
|
|
24
31
|
* - `"custom"` → enables the per-resource analyzer plugins
|
|
25
32
|
* - `"advisor"` → enables the Azure Advisor subscription-level analyzer
|
|
26
33
|
*
|
|
27
|
-
*
|
|
34
|
+
* Config loading applies the `["advisor", "custom"]` default, so this list
|
|
35
|
+
* is always defined and contains at least one source.
|
|
28
36
|
*/
|
|
29
|
-
sources
|
|
37
|
+
sources: Config["azure"]["sources"];
|
|
30
38
|
subscriptionIds: string[];
|
|
31
39
|
/**
|
|
32
40
|
* Analysis thresholds. Defaults from DEFAULT_THRESHOLDS are used when not provided.
|
|
@@ -71,5 +79,12 @@ export type AzureResourceReport = {
|
|
|
71
79
|
* Narrowed from `FindingSource` to exclude "aws", which is not a valid
|
|
72
80
|
* filter for Azure runs and would silently produce an empty report.
|
|
73
81
|
*/
|
|
74
|
-
export type AzureSource = "
|
|
82
|
+
export type AzureSource = Config["azure"]["sources"][number];
|
|
83
|
+
export type PricingConfig = {
|
|
84
|
+
/**
|
|
85
|
+
* Runtime switch used by the CLI `--no-pricing` flag. Pricing otherwise
|
|
86
|
+
* stays enabled with internal defaults for currency and cache settings.
|
|
87
|
+
*/
|
|
88
|
+
enabled?: boolean;
|
|
89
|
+
};
|
|
75
90
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/azure/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,QAAQ,EACR,UAAU,EACX,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/azure/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,QAAQ,EACR,UAAU,EACX,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC;IACpC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;OAEG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,QAAQ,EAAE,YAAY,CAAC,eAAe,CAAC;CACxC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC"}
|